算法设计基础.docx
《算法设计基础.docx》由会员分享,可在线阅读,更多相关《算法设计基础.docx(32页珍藏版)》请在冰豆网上搜索。
![算法设计基础.docx](https://file1.bdocx.com/fileroot1/2023-1/4/200de94c-29e1-4dc6-b2ed-ff3d3ad71c70/200de94c-29e1-4dc6-b2ed-ff3d3ad71c701.gif)
算法设计基础
第1章算法设计基础
本章讨论如何评价一个算法,常见的基本的算法设计方法。
算法在C语言程序设计中具有如此重要的位置,以至于我们不得不用单独一章来讨论算法。
仅仅掌握C语言程序设计语法规则并不能保证写出一个好的程序。
好与坏的标准在计算机科学中有特定的含义,它是程序的执行过程花费的时间代价和空间代价的少与多的度量。
好的算法给我们带来经济上的好处,可以使得我们设计出更优秀的操作系统,提高计算机的运行效率;或者使得我们可以提高机密数据的安全性,不被别人窃取;或者可以提供给我们更好的影音效果,让我们能享受生活的乐趣;或者使得我们在搜索引擎上更快的搜索到需要的数据。
好的算法能在有限时间和有限空间条件下得到问题的解,而不好的算法可能终生都得不到解。
历史上有许多著名科学家都提出了针对特定问题的著名算法。
伟大的智者——DonE.Knuth(1938-),他是算法和程序设计技术的先驱者。
Knuth的经典著作《计算机程序设计艺术》更是被誉为算法教材中“真正”的圣经。
BillGates说:
“如果能作对书里所有的习题,就直接来微软上班吧!
”。
首席算法官UdiManber——世界上还有如此奇特的职位?
但是对于Amazon乃至Google来说,这一点也不奇怪。
UdiManber,这位前Amazon的“首席算法官”,现在是Google负责工程事务的副总裁。
Udi还因为他所著《IntroductiontoAlgorithms——ACreativeApproach》而被大家称道。
谦逊的长者——EdsgerWybeDijkstra,1930年出生于荷兰阿姆斯特丹,2002年逝世于荷兰纽南。
他在祖国荷兰获得数据和物理学学士,理论物理博士学位,2000年退休前一直是美国Texas大学的计算机科学和数学教授。
因他发现了图论中的最短路径算法(Dijkstra算法)而闻名于世,1972年因发明ALGOL第二代编程语言而获得图灵奖。
运筹学大师——GeorgeDantizig,在大学时代就解决了两个统计学悬而未决的难题。
George后来在运筹学建树极高,获得了包括“冯诺伊曼理论奖”在内的诸多奖项。
他在《Linearprogrammingandextensions》一书中研究了线性编程模型,为计算机语言的发展做出了不可磨灭的贡献。
推动时代前进的人—JamesCooley(1926-)是美国数学家,哥伦比亚大学的数学博士,因为发明快速傅立叶变换(FFT)而著名。
FFT的数学意义不光在于使大家明白了傅立叶(Fourier)变换计算起来是多么容易,而且使得数字信号处理技术取得了突破性的进展,对于现在的网络通信,图形图像处理等等领域的发展与前进奠定了基础。
FORTRAN之父——JohnBackus。
在IBM,Backus的才华得到了施展,发明了人类历史上前所未有第一个高级语言——FORTRAN,对计算机科学计算和程序语言做出了杰出贡献。
1977年JohnBackus被授予图灵奖。
实践探索先锋——JonBentley。
他的著作中,最著名的莫过于涵盖从算法理论到软件工程各种主题的《ProgrammingPearls》(编程珠玑),这其实是他发表过的论文的合集。
在这些文章里,Jon从工程实现的角度出发,为程序员们提供了一个个艰难问题的解决方案,犹如一颗颗闪闪发亮的珍珠。
Bentley的珍珠超出了可靠工程学的范畴,利用他的洞察力和创造力为那些恼人的算法问题提供了巧妙解决方案。
Pascal之父——NicklausWirth,如果说有一个人因为一句话而著名,那么这个人就是NicklausWirth,这句话就是他提出的著名公式“算法+数据结构=程序”。
他创建与实现了Pascal语言——当时世界上最受欢迎的语言之一。
算法的讲解者——RobertSedgewick是普林斯顿大学的计算机科学教授。
他还是AdobeSystems的一名主管,也曾作为访问学者在XeroxPARC、IDA和INRIA工作。
他在斯坦福大学获得博士学位。
他的著作包括《AlgorithminC》、《AlgorithminC++》、《AlgorithminJava》等系列书籍,这些都再版多次。
计算机领域的爵士——TonyHoare,1934年出生于英国,1959年博士毕业于俄罗斯莫斯科国立大学,获得语言机器翻译专业学士学位。
1960年发布了使他闻名于世的快速排序算法(QuickSort),这个算法也是当前世界上使用最广泛的算法之一。
中国历史上也有著名算法,如圆周率PI的计算方法。
近代诞生的遗传算法对处理相当广泛的大规模问题有突出效果。
这些算法都是人类智慧的结晶。
要写出好的程序,应该向这些优秀的算法学习,阅读大师的著作,掌握这些算法的精髓,在应用开发中才能做到融会贯通。
1
1.1算法的基本概念
算法是对特定问题求解步骤的一种描述,算法是指令的有限序列,其中每一条指令表示一个或多个操作。
算法具有以下五个特征:
1)有穷一个算法必须总是在执行有穷步骤之后结束,且每一步都在有穷的时间内完成。
2)确定算法中每一条指令必须有确切的含义,不应该存在二义性。
且算法只有一个入口和一个出口。
3)可行一个算法是可行的。
即算法描述的操作都是可以通过已经实现的基本运算执行有限的次数来实现。
4)输入一个算法有0个或多个输入,这些输入取自于某个特定的对象的数据集合。
5)输出一个算法有一个或多个输出,这些输出是同输入有着某些特定关系的数据。
评价一个好的算法有以下几个标准:
1)正确性(Correctness)——算法应满足具体问题的需求,得到问题的解。
2)可读性(Readability)——算法应该好读。
以有利于阅读者对程序的理解。
3)健壮性(Robustness)——算法应具有容错处理。
当输入非法数据时,算法应该能有适当反应,而不是产生莫名其妙的输出结果或者程序崩溃。
4)效率与存储量的需求——效率指的是算法执行所需要的CPU时间;存储空间需求量是指算法执行过程中所需要的最大存储空间。
一般,这两者与问题的数据规模有关。
1.2算法分析
算法的效率和优劣的客观判断和明确分析准则,是由美国人霍泼克洛夫特提出的。
他提出了一种称为“最坏情况渐近分析法”(Worst-caseasymptoticanalysisofalgorithm),这种方法先确定问题的大小尺度或者数据规模,然后把CPU计算时间当作问题大小尺度的一个函数去算出CPU计算时间的增长率,以此衡量算法的效率和优劣。
由于这个方法与机器性能及所用语言无关,成为衡量算法好坏的客观的数学准则,被科学界所广泛接受。
1.2.1算法评价标准
●算法时间复杂度的数学定义
从数学上定义,给定算法A,如果存在函数F(n),当输入数据规模n=k时,F(k)表示算法A在输入规模为k的情况下的运行时间,则称F(n)为算法A的时间复杂度。
首先要定义输入规模的概念。
输入规模是指算法A所接受输入的基本类型数据的个数。
例如,对于排序算法来说,输入规模一般就是待排序元素的个数,而对于求两个同型方阵乘积的算法,输入规模可以看作是单个方阵的维数。
为了简单起见,在下面的讨论中,我们总是假设算法的输入规模是用大于零的整数表示的,即1,2,3,……,k,……n。
我们还知道,对于同一个算法,每次执行的时间不仅取决于输入规模,还取决于输入的特性和具体的硬件环境在某次执行时的状态。
所以想要得到一个统一精确的F(n)是不可能的。
为了解决这个问题,我们作两个假设:
1.硬件及环境因素,假设每次执行时硬件条件和环境条件是完全一致的。
2.对于输入数据集特性的差异,假设不同情况的出现概率是相同的。
●算法时间复杂度的分析示例
为了便于理解,使用一个十分简单的算法作为示例。
我们先来定义问题。
问题定义:
输入——此问题输入为一个有序序列,其元素个数为n,n为大于零的整数。
序列中的元素为从1到n。
这n个整数排列次序为完全随机。
要求输出——元素n所在的位置,第一个元素位置为0。
这个问题非常简单,下面用伪代码直接给出其解决算法之一:
LocationN(A)
{
for(inti=0;i<=n;i++)//t1
{
if(A[i]==n)//t2
{
returni;}//t3
}
}
我们来看看这个算法。
其中t1、t2和t3分别表示此行代码执行一次需要的时间。
很明显,t1、t2和t3是和具体的硬件环境和语言有关。
首先,输入规模n是影响算法执行时间的因素之一。
在n固定的情况下,不同的输入序列也会影响其执行时间。
最好情况下,n就排在序列的第一个位置,那么此时的运行时间为“t1+t2+t3”。
最坏情况下,n排在序列最后一位,则运行时间为F(n)=n*t1+n*t2+t3=(t1+t2)*n+t3
可以看到,最好情况下运行时间是一个常数,而最坏情况下运行时间是输入规模的线性函数。
那么,平均情况如何呢?
问题定义说输入序列完全随机,即n出现在1...n这n个不同位置上可能性是相等的,即概率均为1/n。
而平均情况下的执行次数即为执行次数的数学期望,其解为:
E=p(n=1)*1+p(n=2)*2+...+p(n=n)*n
=(1/n)*(1+2+...+n)
=(1/n)*((n/2)*(1+n))
=(n+1)/2
即在平均情况下for循环要执行(n+1)/2次,则平均运行时间为
F(n)=(t1+t2)*(n+1)/2+t3
由此我们得出分析结论:
t1+t2+t3<=F(n)<=(t1+t2)*n+t3;
在平均情况下F(n)=(t1+t2)*(n+1)/2+t3
●算法的渐近时间复杂度
我们对算法的时间复杂度F(n)进行了精确分析。
但是,很多时候,我们不需要进行如此精确的分析,原因如下:
1)在较复杂的算法中,进行精确分析非常复杂,因而是不现实的。
2)实际上,大多数时候我们并不关心F(n)的精确度量,而只是关心它的量级。
基于此,提出渐近时间复杂度的概念。
在正式给出渐近时间复杂度之前,要先给出几个数学定义:
定义1:
Θ(g(n))={f(n)|如果存在正常数c1、c2和正整数n0,使得当n>=n0时,
0定义2:
Ο(g(n))={f(n)|如果存在正常数c和正整数n0,使得当n>=n0时,0<=f(n)<=cg(n)恒成立};
定义3:
Ω(g(n))={f(n)|如果存在正常数c和正整数n0,使得当n>=n0时,0<=cg(n)<=f(n)恒成立};
可以看到,三个定义其实都定义了一个函数集合,只不过集合中的函数需要满足的条件不同。
有了以上定义,就可以定义渐近时间复杂度了。
不过这里还有个问题:
F(n)是不确定的,他是在一个范围内变动的,那么我们关心哪个F(n)呢?
一般在分析算法时,我们使用最坏情况下的F(n)来评价算法效率,原因有两点:
1)如果知道了最坏情况,我们就可以保证算法在任何时候都不能比这个情况更坏了。
2)很多时候,算法运行发生在最坏情况下的概率还是很大的,如查找问题中要查找的元素不存在的情况。
而且在很多时候,平均情况的渐近时间复杂度和最坏情况下的渐近时间复杂度是同一个量级的。
于是给出如下定义:
设F(n)为算法A在最坏情况下F(n),则如果F(n)属于Θ(g(n)),则说算法A的渐近时间复杂度为g(n),且g(n)为F(n)的渐近确界。
还是以上面的例子为例,则在上面定义中F(n)=(t1+t2)*n+t3。
则F(n)的渐近确界为n,其证明如下:
证明:
设c1=t1+t2,c2=t1+t2+t3,n0=2
又因为t1,t2,t3均大于0
则,当n>n0时,0所以F(n)属于Θ(n)
所以n是F(n)的渐近确界
证毕
在实际应用中,我们一般都是使用渐近时间复杂度代替实际时间复杂度来进行算法效率分析。
我们把这个时间复杂度记为O(n)。
时间复杂度O(n)说明了最坏情况下的计算时间随着数据规模变化的函数关系是数据规模的线性函数。
我们用这个函数关系作为算法的评价标准,来评价一个算法的优劣。
注意这时时间复杂度与具体的计算机硬件环境和计算机语言没有关系了,只是算法本身时间复杂程度的一个客观评鉴标准。
一般认为,一个渐近复杂度为O(n)的算法要优于渐近复杂度为O(n2)的算法。
O
(1)时间复杂度的算法计算时间与数据规模没有关系,是一个常量。
以下6种计算算法时间复杂度的多项式函数是最常用的。
其关系为:
O
(1)指数时间复杂度的关系为:
O(2n))当n取得很大时,指数时间复杂度算法和多项式时间复杂度算法在所需时间上非常悬殊。
为了比较各种时间复杂度算法的计算时间,取数据规模n=100,那么各种时间复杂度算法的时间值的量级比较如下表
•O(Log(N))10-7seconds
•O(N)10-6seconds
•O(N*Log(N))10-5seconds
•O(N2)10-4seconds
•O(N6)3minutes
•O(2N)1014years.
•O(N!
)10142years.
因此,只要有人能将现有指数时间复杂度算法中的任何一个算法化简化为多项式时间复杂度算法,那就取得了一个伟大的成就。
注意,时间复杂度的比较是有前提条件的。
渐近复杂度为O(n)的算法并不是在任何情况下都一定比O(n2)的算法更高效,而是说在输入规模足够大后(大于临界条件n0),则前一个算法的最坏情况总是好于后一个算法的最坏情况。
事实证明,在实践中这种分析是合理且有效,能够用来鉴别不同的算法优劣。
类似的,还可以给出算法时间复杂度的上确界和下确界:
设F(n)为算法A在最坏情况下F(n),则如果F(n)属于Ο(g(n)),则说算法A的渐近时间复杂度上限为g(n),且g(n)为F(n)的渐近上确界。
设F(n)为算法A在最坏情况下F(n),则如果F(n)属于Ω(g(n)),则说算法A的渐近时间复杂度下限为g(n),且g(n)为F(n)的渐近下确界。
这里一定要注意,由于我们是以F(n)最坏情况分析的,所以,我们可以保证在输入规模超过临界条件n0时,算法的运行时间一定不会高于渐近上确界,但是并不能保证算法运行时间不会低于渐近下确界,而只能保证算法在最坏情况下运行时间不会低于渐近下确界。
1.2.2空间复杂性
空间复杂度:
算法所需存储空间的度量,记作:
S(n)=O(f(n))
其中n为问题的规模(或大小)。
1.2.3算法优化
1.3问题求解
1.4算法的描述
1.4.1流程图
1.4.2三种基本结构
1.5算法设计的基本方法
一般地,构造求解问题的算法可以采用的方法包括迭代,递推,穷举,递归,贪心选择,分治,回溯和动态规划。
每种方法各有特点,各自有其适用范围。
可以有选择的使用。
迭代和递推通过下面的例子很容易理解。
穷举,递归,贪心选择,分治,回溯和动态规划各需要专门阐明。
迭代法常用于工程计算。
最简单的迭代是用于求方程或方程组近似根。
设方程为f(x)=0,用某种数学方法导出等价的形式x=g(x),然后按以下步骤执行:
1)选定一个方程的近似根,赋给变量x0;2)将x0的值赋值给变量x1,然后计算g(x1),并将结果赋值给变量x0;3)当x0与x1的差的绝对值还大于指定的精度要求时重复步骤2)的计算,否则结束计算。
若方程有根,并且用上述方法计算出来的近似解序列收敛,则按上述方法求得的x0就认为是方程的根。
例XX:
迭代法求方程的根伪代码:
solutionP(){
x0=初始近似根;
do{
x1=x0;
x0=g(x1);/*按特定的方程计算新的近似的根*/
}while(fabs(x0-x1)>Epsilon);
printf(“方程的近似根是%f\n”,x0);
}
迭代算法也常用于求方程组的根,令
X=(x0,x1,…,xn-1)
设方程组为:
xi=gi(X)(i=0,1,…,n-1)
则求方程组根的迭代算法可描述如下:
例XX:
迭代法求方程组的根
solutionP(){
for(i=0;i x[i]=初始近似根;
do{
for(i=0;i y[i]=x[i];
for(i=0;i x[i]=gi(X);
for(delta=0.0,i=0;i if(fabs(y[i]-x[i])>delta)delta=fabs(y[i]-x[i]);
}while(delta>Epsilon);
for(i=0;i printf(“变量x[%d]的近似根是%f”,I,x[i]);
printf(“\n”);
}
具体使用迭代法求根时应注意以下两种可能发生的情况:
1)如果方程无解,算法求出的近似解序列就不会收敛,迭代过程会变成死循环,因此在使用迭代算法前应先考察方程是否有解,并在程序中对迭代的次数给予限制;2)方程虽然有解,但迭代公式选择不当,或迭代的初始近似解选择不合理,也会导致迭代失败。
迭代法用于工程数值计算需要考虑算法的收敛性问题和稳定性问题。
简单地讲,只有收敛的算法才可以用计算机计算,稳定性与迭代计算的步长有关。
比如微分方程组和偏微分方程组的数值求解都需要考虑这个问题。
详细内容请参看数值计算方面资料。
递推法是利用问题本身所具有的一种递推关系求解问题的一种方法。
设要求问题规模为N的解,当N=1时,解或为已知,或能非常方便地得到。
能采用递推法构造算法的问题有重要的递推性质,即当得到问题规模为i-1的解后,由问题的递推性质,能从已求得的规模为1,2,…,i-1的一系列解,构造出问题规模为i的解。
这样,程序可从i=0或i=1出发,重复地,由已知至i-1规模的解,通过递推,获得规模为i的解,直至得到规模为N的解。
考虑使用迭代算法计算阶乘的问题。
要求编写程序,对给定的n(n≦100),计算并输出k的阶乘k!
(k=1,2,…,n)的全部有效数字。
由于要求的整数可能大大超出C语言整型变量的有效位数,程序用一维数组存放长整数,存放长整数数组的每个元素只存放长整数的一位数字。
如有m位成整型量N用数组a[]存储:
N=a[m]×10m-1+a[m-1]×10m-2+…+a[2]×101+a[1]×100
并用a[0]存储长整型量N的位数m,即a[0]=m。
按上述约定,数组的每个元素存储k的阶乘k!
的一位数字,并从低位到高位依次存于数组的第二个元素、第三个元素……。
例如,5!
=120,在数组中的存储形式为:
3021……
首元素3表示长整数是一个3位数,接着是从低位到高位依次是0、2、1,表示成整数120。
计算阶乘k!
可采用对已求得的阶乘(k-1)!
连续累加k-1次后求得。
例如,已知4!
=24,计算5!
,5!
=4!
*(1+1+1+1+1)。
原来的24累加4次24后得到120。
一般地,k!
可以由(k-1)!
累加k-1次得到。
反过来讲,知道(k-1)!
值,累加k-1次即可得到k!
的值。
这就是递推的思想。
例XX:
递推算法求超过类型长的大数的阶乘。
#include
#include
#defineMAXN1000
voidpnext(inta[],intk){
int*b,m=a[0],i,j,r,carry;
b=(int*)malloc(sizeof(int)*(m+1));
for(i=1;i<=m;i++)b[i]=a[i];
for(j=1;j<=k;j++){
for(carry=0,i=1;i<=m;i++){
r=(i a[i]=r%10;
carry=r/10;
}
if(carry)a[++m]=carry;
}
free(b);
a[0]=m;
}
voidwrite(int*a,intk){
inti;
printf(“%4d!
=”,k);
for(i=a[0];i>0;i--)
printf(“%d”,a[i]);
printf(“\n\n”);
}
voidmain(){
inta[MAXN],n,k;
printf(“Enterthenumbern:
“);
scanf(“%d”,&n);
a[0]=1;
a[1]=1;
write(a,1);
for(k=2;k<=n;k++){
pnext(a,k);
write(a,k);
getchar();
}
}
1.5.1穷举法
穷举法的思想是对可行解空间众多候选解按某种顺序进行逐一枚举和检验,并从中找出那些符合要求的候选解作为问题的解。
枚举过程中要穷尽所有可能,即遍历整个可行解解空间。
穷举法虽然在数据规模较大时效率不高,但是在数据规模较小时还是可行的。
它的主要优点是思路很简单。
考虑这个问题:
将A、B、C、D、E、F这六个变量排成如图所示的三角形,这六个变量分别取[1,6]上的整数,且均不相同。
求使三角形三条边上的变量之和相等的全部解。
如图就是一个解。
程序引入变量a、b、c、d、e、f,并让它们分别顺序取1至6的整数,在它们互不相同的条件下,测试由它们排成的如图所示的三角形三条边上的变量之和是否相等,如相等即为一种满足要求的排列,把它们输出。
当这些变量取尽所有的组合后,程序就可得到全部可能的解。
例XX:
三角形边长整数排列问题伪代码。
TriangleP(){
inta,b,c,d,e,f;
intTriangleIsTrue=0,isTrue1,isTrue2,isTrue3;
for(a=1;a<=6;a++)
for(b=1;b<=6;b++){
if(b==a)continue;//不满足互不相同条件
for(c=1;c<=6;c++){
if(c==a)||(c==b)continue;//不满足互不相同条件
for(d=1;d<=6;d++){
if(d==a)||(d==b)||(d==c)continue;//不满足互不相同条件
for(e=1;e<=6;e++){
if(e==a)||(e==b)||(e==c)||(e==d)continue;//不满足互不相同条件
f=21-(a+b+c+d+e);
if((a+b+c==c+d+e))&&(a+b+c==e+f+a)){//满足题目约束
isTrue1=(a+b>c)&&(a+c>b)&&(b+c>a);
isTrue2=(c+d>e)&&(e+c>d)&&(d+e>c);
isTrue3=(a+e>f)&&(a+f>e)&&(e+f>a);
TriangleIsTru