数据结构讲义new.docx
《数据结构讲义new.docx》由会员分享,可在线阅读,更多相关《数据结构讲义new.docx(66页珍藏版)》请在冰豆网上搜索。
数据结构讲义new
《数据结构》讲义(new)
第1章C程序设计
程序设计=数据描述+算法设计,即程序=数据结构+算法。
用计算机去解决问题过程:
首先用数据结构的知识表示出问题,然后设计出解决问题的算法,最后写出程序在计算机上运行输出问题解决的结果。
本章我们将对C的主要知识作回顾,为学习数据结构奠定基础。
1.1表达式
算术运算:
5/3值为1,2.5/5值为0.5,10%3值为1,'A'+1值为66
赋值运算:
a=3+5a值为8,式值为8。
b=a=3+5a,b值均为8,式值为8。
a+=3即a=a+3;a-=3;a*=3;a/=3;a%=3.i++,++i,即i=i+1;i...,...i.区别:
x=i++;与x=++i;
关系运算:
3==2,式值为0,3!
=2,式值为1。
逻辑运算:
01,!
(x<0).注:
“非零”为1。
条件运算:
b>=0?
a+b:
a-b,即a+︱b︱
逗号运算:
a=6,a*3,a+3式值为9
1.2函数与参数
1.2.1传值参数
例1.1输入四个数,输出其中的最大数。
intmax(intx,inty)/*x,y是形参*/
{intt;
if(x>y)t=x;elset=y;
returnt;
}
main()
{inta,b,c,d,m;
scanf(“%d%d%d%d”,&a,&b,&c,&d);
m=max(max(a,b),max(c,d));/*a,b,c,d,max(a,b),max(c,d)是实参,传值调用*/
printf(“%d”,m);
}
1.2.1传址参数
例1.2输入二个数到变量a与b,交换变量a与b的值后输出。
swap(int*x,int*y)/*x,y为传址参数*/
{intt;
t=*x;*x=*y;*y=t;
}
main()
{inta,b;
scanf("%d%d",&a,&b);
swap(&a,&b); /*传址调用*/
printf("%d%d",a,b);
}
例1.3输入n个数,升序排列后输出。
sort(inta[],intn) /*数组名作为参数,为地址参数*/
{inti,j,t;
for(i=0;ifor(j=i+1;jif(a[i]>a[j]){t=a[i];a[i]=a[j];a[j]=t;}
}
main()
{intb[100],i,m;
scanf("%d",&m);
for(i=0;isort(b,m); /*地址传递与值传递*/
for(i=0;i}
1.2.3递归函数
例1.4计算n!
递归函数f(n)的一个递归定义(假定是直接递归)必须满足如下条件:
•包含一个基本部分,其中对于n的一个或多个值,f(n)必须是直接定义的(即非递归)。
如本例中f
(1)=1。
•递归部分,右侧所出现的所有f的参数都必须有一个比n小,以便重复运用递归部分来改变右侧出现的f,直至出现f的基本部分。
longf(intn)
{if(n==1)return1;
elsereturnn*f(n-1);
}
main()
{intk;
scanf("%d",&k);
printf("%ld",f(k));
}
例1.5计算Fibonacci序列:
intfib(intn)
{if(n<=2)return1;
elsereturnfib(n-2)+fib(n-1);
}
main()
{inti,k;
scanf("%d",&k);
for(i=1;i<=k;i++)
printf("%d",fib(i));
}
例1.6.HanoiTower问题:
1.有三根杆子A,B,C。
A杆上有n只碟子
2.每次移动一只碟子,小的只能叠在大的上面
3.把所有碟子从A杆经C杆全部移到B杆上.
递归求解:
基本部分:
若n=1,只有1只碟子,直接将它从A杆移到B杆;
递归部分:
若n>1,上面n-1只碟子从A杆经B杆移动到C杆,将A杆上第n只碟子移到B杆;然后再将n-1只碟子从C杆经A杆移到B杆。
Han(intn,chara,charb,charc)
{if(n==1)printf("%c->%c\n",a,b);
else
{Han(n-1,a,c,b);
printf("%c->%c\n",a,b);
Han(n-1,c,b,a);
}
}
main()
{intn;
scanf("%d",&n);
Han(n,'A','B','C');
}
例1.7[排列]通常我们希望检查n个不同元素的所有排列方式以确定一个最佳的排列。
比如,a,b和c的排列方式有:
abc,acb,bac,bca,cab和cba。
n个元素的排列方式共有n!
种。
输出n个不同元素的所有排列。
由于采用非递归的C函数来输出n个元素的所有排列很困难,可以开发一个递归函数来实现。
令E={e1,...,en}表示n个元素的集合,Ei为E中移去元素ei以后所获得的集合,p(X)表示集合X的全部排列,ei.p(X)表示在p(X)中的每个排列的前面均加上ei所得排列。
例如,如果E={a,b,c},那么E1={b,c},p(E1)=(bc,cb),e1.p(E1)=(abc,acb)。
当n=3并且E=(a,b,c)时,按照前面的递归定义可得p(E)=a.p({b,c})+b.p({a,c})+c.p({a,b})=ab.p({c})+ac.p({b})+ba.p({c})+bc.p({a})+ca.p({b})+cb.p({a})=(abc,acb,bac,bca,cab,cba)。
注意a.p({b,c})实际上包含两个排列方式:
abc和acb,a是它们的前缀,p({b,c})是它们的后缀。
同样地,ac.p({b})表示前缀为ac、后缀为p({b})的排列。
设函数p(list,k,m)表示前缀为list[0]...list[k-1],后缀为list[k]...list[m]的排列。
调用p(list,0,n-1)得到list[0]…list[n-1]的所有n!
个排列。
当k=m时,仅有一个后缀list[m],因此list[0]…list[m]即是所要产生的输出。
当kPerm(inta[],ints,inte)
{inti,t;
if(s==e)
{for(i=0;i<=s;i++)printf("%d",a[i]);
printf("\n");
}
else
for(i=s;i<=e;i++)
{t=a[s];a[s]=a[i];a[i]=t;
Perm(a,s+1,e);
t=a[s];a[s]=a[i];a[i]=t;
}
}
main()
{inta[10],n,i;
scanf("%d",&n);
for(i=0;iPerm(a,0,n-1);
}
1.3简单算法
1、分离数字
例1.8.寻找满足下列条件的数n:
(1)n是不超过7位的平方数,
(2)n是回文数。
所谓回文数是指这样的数,它的各位数码是左右对称的。
例如1,121、676、94249
2、累加,记数。
例1.9.输入一组非零整数,以0作为结束标记.输出这组数的平均值及其中正整数与负整数的个数.
3、递推和迭代。
递推:
前项(后项)推出后项(前项)。
迭代:
旧值推出新值。
例1.10.已知Fibonacci序列的前两项是1,1.输出项值不大于100的Fibonacci序列.
例1.11.将一个十进制数转换为一个二进制数。
4、找出最大数和最小数。
例1.11.找n个整数中的最大数.
例1.12.找n个整数中的最大数和次大数.
5、高精度计算:
例1.12.计算n!
(n<=100)
(unsignedlong:
0~4294967295,13!
=6227020800,100!
为158位)
(1)将被乘数按数位存放在数组a中从0单元到k单元,乘数存放在变量n中;
(2)n与a的每一数位作竖式乘法,乘积按数位存放在数组a中;
intmul(inta[],intk,intn)
{inti,j,s,c;
for(c=0,i=0;i<=k;i++)
{s=a[i]*n+c;
a[i]=s%10;
c=s/10;
}
for(;c>0;i++){a[i]=c%10;c/=10;} /*将最后的进位按数位分离存放*/
returni-1;
}
main()
{staticinta[300],i,j,n;
scanf("%d",&n);
for(a[0]=1,j=1,i=2;i<=n;i++)
j=mul(a,j,i);
for(i=j-1;i>=0;i...)printf("%d",a[i]);
}
6、穷举搜索法
穷举法也叫枚举法,它的基本思想是依题目的部分条件确定答案的大致范围,在此范围内对所有可能的情况逐一验证,直到全部情况验证完。
若某个情况经验证符合题目的全部条件,则为本题的一个答案。
若全部情况经验证后都不符合题目的全部条件,则本题无答案。
用穷举法解题时,答案所在的范围总是要求是有限的,怎样才能使我们不重复的、一个不漏、一个不增的逐个列举答案所在范围的所有情况,就是本节所讲的“列举方法”。
列举方法按答案的数据类型,常用的有下面三种:
顺序列举:
顺序列举是指答案范围内的各种情况很容易与自然数对应甚至就是自然数,可以按自然数的变化顺序去列举。
例1.13.输出全部3位素数。
例1.14.四大湖问题,上地理课时,四个学生回答我国四大淡水湖的大小时说
A学生:
洞庭湖最大,洪泽湖最小,鄱阳湖第三。
B学生:
洪泽湖最大,洞庭湖最小,鄱阳湖第二,太湖第三。
C学生:
洪泽湖最小,洞庭湖第三。
D学生:
鄱阳湖最大,太湖最小,洪泽湖第二,洞庭湖第三。
对于每个湖的大小,每人仅答对一个。
请判断四个湖的大小。
问题分析:
可以设洞庭湖、洪泽湖、鄱阳湖、太湖分别用变量A、B、C、D表示。
每个变量的取值是1~4。
因为每个只答对了一个,所以,他们的叙述可以表示为:
甲:
(A=1)+(B=4)+(C=3)=-1
乙:
(B=1)+(A=4)+(C=2)+(D=3)=-1
丙:
(B=4)+(A=3)=-1
丁:
(C=1)+(D=4)+(B=2)+(A=3)=-1
只有以上条件都满足时,才能找到正确答案。
排列列举:
有时答案的数据形式是一组数的排列,列举出所有答案所在范围内的排列,称为排列列举。
例1.15.八皇后问题。
组合列举:
当答案的数据形式为一些元素的组合时,往往需要用组合列举。
从n个不同的元素中任取m个元素组成的一组,就叫做从n个元素取m个元素的一个组合。
组合是无序的,如:
123,132,321,312,213,231是同一个组合,但是6个不同的排列。
1.4动态存储分配函数
1.malloc函数
例如,int*p,*q;
p=malloc(8);/*分配一个长度为8字节的内存空间,返回其首地址*/
q=malloc(sizeof(int));
又如,structstud
{charname[10];
longnum;
intage;
charsex;
floatscore;
};
structstud*p;
p=malloc(sizeof(structstud));
或, deftypestruct
{charname[10];
longnum;
intage;
charsex;
floatscore;
}STUD;
STUD*p;
p=malloc(sizeof(STUD));
2.free函数
例如,STUD*p;
p=malloc(sizeof(sizeof(STUD)));
……
free(p);/*将p指向的存储空间释放,交还系统*/
1.5测试与调试
例1.17求解二次方程:
ax2+bx+c=0
1.5.1什么是测试
正确性是一个程序最重要的属性。
由于采用严格的数学证明方法来证明一个程序的正确性是非常困难的(哪怕是一个很小的程序),所以我们想转而求助于程序测试过程来实施这项工作。
所谓程序测试是指在目标计算机上利用输入数据,把程序的实际行为与所期望的行为进行比较。
如果两种行为不同,就可判定程序中有问题存在。
当然,两种行为相同,也不能够断定程序就是正确的。
如果使用了许多组测试数据都能够看到这两种行为是一样的,我们可以增加对程序正确性的信心。
1.5.2设计测试数据
在设计测试数据的时候,应当牢记:
测试的目标是去发现错误。
如果用来寻找错误的测试数据找不到错误,我们就可以有信心相信程序的正确性。
为了弄清楚对于一个给定的测试数据,程序是否存在错误,首先必须知道对于该测试数据,程序的正确结果应是什么。
设计测试数据的技术分为两类:
黑盒法和白盒法。
在黑盒法中,考虑的是程序的功能,而不是实际的代码。
在白盒法中,通过检查程序代码来设计测试数据,以便使测试数据的执行结果能很好地覆盖程序的语句以及执行路径。
1.黑盒法
最流行的黑盒法是I/O分类及因果图,本节仅探讨I/O分类。
在这种方法中,输入数据和/或输出数据空间被分成若干类,不同类中的数据会使程序所表现出的行为有质的不同,而相同类中的数据则使程序表现出本质上类似的行为。
二次方程求解的例子中有三种本质上不同的行为:
产生复数根,产生实数根且不同,产生实数根且相同。
可以根据这三种行为把输入空间分为三类。
第一类中的数据将产生第一种行为;第二类中的数据将产生第二种行为;而第三类中的数据将产生第三种行为。
一个测试集应至少从每一类中抽取一个输入数据。
2.白盒法
白盒法基于对代码的考察来设计测试数据。
对一个测试集最起码的要求就是使程序中的每一条语句都至少执行一次。
这种要求被称为语句覆盖。
对于二次方程求解的例子,测试集{(1,-5,6),(1,-8,16),(1,2,5)}将使程序中的每一条语句都得以执行,而测试集{(1,-5,6),(1,3,2),(2,5,2)}则不能提供语句覆盖。
在分支覆盖中要求测试集要能够使程序中的每一个条件都分别能出现true和false两种情况。
程序1-30中的代码有两个条件:
d>0和d==0。
在进行分支覆盖测试时,要求测试集至少能使条件d>0和d==0分别出现一次为true、一次为false的情况。
你所使用的测试数据应至少提供语句覆盖。
此外,你必须测试那些可能会使你的程序出错的特定情形。
例如,对于一个用来对n≥0个元素进行排序的程序,除了测试n的正常取值外,还必须测试n=0,1这两种特殊情形。
如果该程序使用数组a[0:
99],还需要测试n=100的情形。
n=0,1和100分别表示边界条件为空,单值和全数组的情形。
1.5.3调试
测试能够发现程序中的错误。
一旦测试过程中产生的结果与所期望的结果不同,就可以了解到程序中存在错误。
确定并纠正程序错误的过程被称为调试。
可以用逻辑推理的方法来确定错误语句,如果这种方法失败,还可以进行程序跟踪,以确定程序什么时候开始出现错误。
如果对于给定的测试数据程序需要运行很多指令,因而需要跟踪太多语句,很难人工确定错误,此时,这种方法就不太可行了,在这种情况下,必须试着把可疑的代码分离出来,专门跟踪这段代码。
第2章程序性能
程序性能是指运行一个程序所需要的内存大小和时间。
可以采用两种方法来确定一个程序的性能,一个是分析的方法,一个是实验的方法。
在进行性能分析时,采用分析的方法,而在进行性能测量时,借助于实验的方法。
程序的空间复杂度是指运行完一个程序所需要的内存大小。
程序的时间复杂度是指运行完该程序所需要的时间。
2.1空间复杂度
2.1.1空间复杂度的组成
程序所需要的空间主要由以下部分构成:
•指令空间:
指令空间是指用来存储经过编译之后的程序指令所需的空间。
•数据空间:
数据空间是指用来存储所有常量和所有变量值所需的空间。
数据空间由两个部分构成:
1)存储常量和简单变量所需要的空间。
2)存储复合变量所需要的空间。
这一类空间包括数据结构所需的空间及动态分配的空间。
•环境栈空间:
环境栈用来保存函数调用返回时恢复运行所需要的信息。
例如,如果函数fun1调用了函数fun2,那么至少必须保存fun2结束时fun1将要继续执行的指令的地址。
把一个程序所需要的空间分成两部分:
•固定部分,它独立于问题的输入量n(问题规模)。
一般来说,这一部分包含指令空间、简单变量及定长复合变量所占用空间、常量所占用空间等。
•可变部分,它由以下部分构成:
复合变量所需的空间(这些变量的大小依赖于所解决的具体问题),动态分配的空间(这种空间一般都依赖于n),以及递归栈所需的空间(该空间也依赖于n)。
任意程序P所需要的空间S(P)可以表示为:
S(P)=c+S(n)
其中c是一个常量,表示固定部分所需要的空间,S(n)表示可变部分所需要的空间。
一个精确的分析还应当包括在编译期间所产生的临时变量所需要的空间,这种空间是与编译器直接相关的,除依赖于递归函数外,它还依赖于实例的特征。
本书将忽略这种空间。
在分析程序的空间复杂度时,我们将把注意力集中在估算S(n)上。
对于任意给定的问题,首先需要确定n以便于估算空间需求。
n的选择是一个很具体的问题,我们将求助于介绍各种可能性的实际例子。
一般来说,我们的选择程序输入和输出的规模。
渐近空间复杂度:
通常取S(n)=O(f(n)),即
。
(即,存在两个正常数c和n0时,对所有的n≥n0,都有S(n)≤f(n),则S(n)=O(f(n)).)
例2.1:
[顺序搜索]程序从左至右检查数组a[0:
n-1]中的元素,以查找与x相等的那些元素。
如果找到一个元素与x相等,则函数返回x第一次出现所在的位置。
如果在数组中没有找到这样的元素,函数返回-1。
intSqSearch(inta[],intn,intx)
{inti;
for(i=0;iif(a[i]==x)returni;
return-1;
}
数据空间为12个字节。
因为该空间独立于n,所以S(n)=0。
S(n)=O
(1)。
例2.2[阶乘运算]计算n!
。
intfac(intn)
{if(n==1)return1;
elsereturnfac(n-1);
}
递归深度为n。
每次调用函数Fac时,递归栈需要保留返回地址(2个字节)和n的值(2个字节)。
此外没有其他依赖于n的空间,所以S(n)=4n。
S(n)=O(n)。
2.3时间复杂度
2.3.1时间复杂度的组成
一个程序P所占用的时间T(P)=编译时间+运行时间。
编译时间与问题规模n无关。
另外,可以假定一个编译过的程序可以运行若干次而不需要重新编译。
因此我们将主要关注程序的运行时间。
运行时间通常用T(n)表示。
2.3.2 时间复杂度
1、语句频度:
语句执行次数。
问题规模n:
问题的输入量。
2、时间复杂度T(n):
程序中所有语句的频度之和。
3、渐近时间复杂度:
通常取T(n)=O(f(n)),即
。
(即,存在两个正常数c和n0时,对所有的n≥n0,都有T(n)≤f(n),则T(n)=O(f(n)).)
4、f(n)一般取最深层循环体的执行次数。
例:
矩阵乘法:
计算两个n阶方阵A与B的乘积C。
mm(inta[][N],intb[][N],intc[][N],intn)
{inti,j,k;
for(i=0;ifor(j=0;jfor(c[i][j]=0,k=0;kc[i][j]+=a[i][k]*b[k][j];/*语句频度n3*/
}
T(n)=2n3+2n2+2n+1
因为
所以取f(n)=n3
渐近时间复杂度T(n)=O(n3)
例:
分析下列程序段的时间复杂度T(n)
(1)t=a;a=b;b=t;
T(n)=3,T(n)=O
(1)
(2)x=0;y=0;
for(k=1;k<=n;i++)x++;
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)y++;
f(n)=n2
T(n)=O(n2)
(3)x=0;
for(i=1;i<=n;i++)
for(j=1;j<=i;j++)
for(k=1;k<=j;k++)
x++;
最深层循环体的执行次数为:
所以
(4)intfac(intn)
{if(n<=1)return1;
elsereturn(n*fac(n-1));
}
T(n)=O(n)
(5)for(x=0,i=1;i<=n;i*=2)x++;
执行次数f(n)与n的关系是n=2f(n)
T(n)=O(log2n)
4、最坏、最好和平均时间复杂度。
例:
[顺序搜索]当x是a中的一员时称查找是成功的,其他情况都称为不成功查找。
每当进行一次不成功查找,就需要执行n次比较。
对于成功查找来说,最好的比较次数是1,最坏的比较次数为n。
为了计算平均查找次数,假定所有的数组元素都是不同的,并且每个元素被查找的概率是相同的。
成功查找的平均比较次数为:
6、常见时间复杂度递增排序:
O
(1),O(log2n),O(n),O(nlog2n),O(n2),O(n3),O(2n).
第一章绪论(参考答案)
1.3
(1)O(n)
(2) O(n)
(3) O(n)
(4) O(n1/2)
(5) 执行程序段的过程中,x,y值变化如下:
循环次数xy
0(初始)91100
192100
293100
………………
9100100
10101100
119199
1292100
………………
2010199
219198
………………
3010198
319197
到y=0时,要执行10*100次,可记为O(10*y)=O(n)
1.52100,(2/3)n,log2n,n1/2,n3/2,(3/2)n,nlog2n,2n,n!
nn
第3章线性表
3.1线性表的定义
1.线性表是由同类型的有限个数据元素(结点)构成的一个有限序列,