算法及其评价.docx
- 文档编号:7133662
- 上传时间:2023-01-21
- 格式:DOCX
- 页数:41
- 大小:90.10KB
算法及其评价.docx
《算法及其评价.docx》由会员分享,可在线阅读,更多相关《算法及其评价.docx(41页珍藏版)》请在冰豆网上搜索。
算法及其评价
算法及其评价
[内容提要]
1、算法及其描述;
2、算法的特性;
3、算法的评价;
4、时间和空间复杂性的计算和讨论;
5、算法的优化方法;
[重点难点]
1、算法的描述及其结构化思想;
2、算法的评价:
时间和空间复杂性;
3、算法的优化:
如何解决时空的矛盾;
[内容讲授]
一、算法及其描述
算法是对特定问题求解步骤的一种描述,它是指令的有限序列,其中每一条指令表示一个或多个操作。
常见的算法有:
穷举法,分治法,筛选法,构造法,贪心法,动态规划等。
一般说来,算法可以用自然语言、流程图和程序来描述。
下面通过一个具体的例子来说明算法的描述方法。
例1、H数问题(Hnum)
[问题描述]
所谓H数,是指该数除1以外最多只有2,3,5,7四种因子,如630即为H数,而22不是。
要求对键盘输入的自然数N,求出第N个H数。
如N=30,应输出49。
规定要求的H数不超出长整型数的范围。
[算法分析]
从H数的定义可以看出,如果一个数是H数,那么将它的2,3,5,7四种因子去掉以后必然是剩下1。
下面就根据H数的这一特性用穷举法来解决这个问题。
首先用自然语言描述该算法。
算法1_1:
穷举法计算第n个H数
第一步:
从键盘输入一个自然数给N;
第二步:
将h置为1,order置为1;{表示第一个H数为1}
第三步:
如果order=n,则转第七步;否则转第四步;
第四步:
h增加1,并将k的值置为h;
第五步:
将k中的2,3,5,7四种因子去掉;
第六步:
如果k=1,则order增加1;
第七步:
输出h;
以上描述中第五步的描述不够明确,下面对去掉k中因子i作更进一步的说明:
第1步:
如果k是i的倍数,则转第2步,否则算法结束;
第2步:
将k置为k/i;
第3步:
转第1步;
有了上述用自然语言描述的算法后,就很容易编写出求解H数问题的程序(Hnum1.pas),借助计算机来计算结果了。
程序中将2,3,5,7存放在数组mark中。
[参考程序1]
programHnum1(input,output);constmark:
array[1..4]ofinteger=(2,3,5,7);vari,h,k,n,order:
longint;beginwrite('Inputn:
');readln(n);
h:
=1;order:
=1;
whileorder begin h: =h+1;k: =h; fori: =1to4do whilekmodmark[i]=0dok: =kdivmark[i]; ifk=1thenorder: =order+1 end; writeln('Theno.',n,'Hnumberis',h) end. [运行程序]下划线表示输入 Inputn: 450 Theno.450Hnumberis23814 Inputn: 1998 Theno.1998Hnumberis7056000 Inputn: 4095 Theno.4095Hnumberis260112384 Inputn: 5910 Theno.5910Hnumberis2143750000 [备注] 程序运行后虽然出了正确的解,但大家发现,随着n越来越大,出解的时间就变得越来越慢,尤其是n>2000时,更是无法满足竞赛的时间要求(一般每个测试点的时限为1秒)。 后面将讨论这一问题的更高效的算法。 二、算法的特性 通过上面的例子,大家应该了解了算法在程序设计中的作用,以及如何描述算法了。 既然算法是求解问题的步骤或规则的描述,至于用什么工具实现或由谁去执行,算法描述中并没有规定。 现代算法通常是作为编程的依据而言的。 一个算法虽然与用什么语言去遍程、在什么计算机上运行没有必然的关联,但作为算法设计者来说,对于算法的一些重要特性是应该掌握的。 一个算法应该具有下列特性: ①有穷性: 一个算法必须总是(对任何合法的输入值)在执行有穷步之后结束,且每一步都可在有穷时间内完成。 ②确定性: 算法的每一步,必须是确切地定义的,读者理解时不会产生二义性。 并且,在任何条件下,算法只有唯一的一条执行路径,即对于相同的输入只能得出相同的输出。 ③输入: 一个算法有0个或多个输入。 它们是在算法开始前对算法给定的量,这些输入取自于特定对象的集合。 ④输出: 一个算法有一个或多个输出。 它们是同输入有某种特定关系的量。 ⑤可行性: 算法应该是可行的。 这意味着算法中所有有待实现的运算都是相当基本的,也就是说,它们原则上都是能够精确地进行的,甚至人们仅用笔和纸做有穷次运算即可完成。 三、算法的评价 对于解决同一个问题,往往能够设计出许多不同的算法。 例如,对于数据的排序问题,我们可以用选择排序、冒泡排序、插入排序、快速排序、希尔排序等多种算法,对于这些排序算法,他们各有优缺点,其算法性能如何有待用户的评价。 因此,对问题求解的算法优劣的评定称为“算法评价”。 算法评价的目的,在于从解决同一问题的不同算法中选择出较为合适的一种算法,或者是对原有的算法进行改造、加工,使其更优、更好。 一般对算法进行评价主要有四个方面: 1、算法的正确性 正确性是设计和评价一个算法的首要条件,如果一个算法不正确,其它方面就无从谈起。 一个正确的算法是指在合理的输入数据下,能在有限的运行时间内得到正确的结果。 通过对数据输入的所有可能情况的分析和上机调试,以证明算法是否正确。 2、算法的简单性 算法简单有利于阅读,也使得证明算法正确性比较容易,同时有利于程序的编写、修改和调试。 但是算法简单往往并不是最有效。 因此,对于问题的求解,我们往往更注意有效性。 有效性比简单性更重要。 3、算法的运行时间: 时间复杂性 算法的运行时间是指一个算法在计算机上运算所花费的时间。 它大致等于计算机执行简单操作(如赋值操作,比较操作等)所需要的时间与算法中进行简单操作次数的乘积。 通常把算法中包含简单操作次数的多少叫做“算法的时间复杂性”。 它是一个算法运行时间的相对量度,一般用数量级的形式给出。 度量一个程序的执行时间通常有以下两种方法: ① 一种是“事后统计”的方法。 因为很多计算机内部都有计时功能,有的甚至可精确到毫秒级,不同算法的程序可通过一组或若干组相同的统计数据以分辨优劣。 但这种方法有两个缺陷: 一是必须先运行依据算法编制的程序;二是所得时间的统计量依赖于计算机的硬件、软件等环境因素,有时容易掩盖算法本身的优劣。 因此人们常常采用另一种“事前分析估算”的方法。 ②“事前分析估算”的方法基于: 一个用高级程序语言编写的程序在计算机上运行时所消耗的时间取决于下列因素: 1依据的算法选用何种策略。 不同算法、不同策略所消耗的CPU时间显然是不同的; ②问题的规模。 例如求100以内还是1000000以内的素数; ③书写程序的语言。 对于同一个算法,实现语言的级别越高,执行效率就越低; ④编译程序所产生的机器代码的质量: 编译器的区别、版本会有所不同; ⑤机器执行指令的速度。 显然,同一个算法用不同的语言实现,或者用不同的编译程序进行编译,或者在不同的计算机上运行时,效率均不相同。 这表明使用绝对的时间单位衡量算法的效率是不合适的。 撇开这些与计算机硬件、软件有关的因素,可以认为一个特定算法“运行工作量”的大小,只依赖于问题的规模(通常用整数量n表示),或者说,它是问题规模的函数。 一个算法是由控制结构(顺序、分支和循环三种)和原操作(指固有数据类型的操作)构成的,则算法时间取决于两者的综合效果。 为了便于比较同一问题的不同算法,通常的做法是,从算法中选取一种对于所研究的问题(或算法类型)来说是基本运算的原操作,以该基本操作重复执行的次数作为算法的时间度量。 例如,在如下所示的两个N*N的矩阵相乘的算法中,“乘法”运算是“矩阵相乘问题”的基本操作。 整个算法的执行时间与该基本操作(乘法)重复执行的次数n3成正比,记作 T(n)=O(n3)。 fori: =1tondo forj: =1tondo begin c[i,j]: =0; fork: =1tondoc[i,j]: =c[i,j]+a[i,k]*b[k,j] end; 一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数f(n),算法的时间量度记作: T(n)=O(f(n)) 它表示问题规模n的增大、算法执行时间的增长率和f(n)的增长率相同,称作算法的渐进时间复杂度,简称时间复杂度。 显然,被称作问题的基本操作的原操作应是其重复执行次数和算法的执行时间成正比的原操作,多数情况下它是最深层循环内的语句中的原操作,它的执行次数和包含它的语句的频度相同。 语句的频度指的是该语句重复执行的次数,例如: 在下列三个程序段中, 1x: =x+1 2fori: =1tondox: =x+1; 3forj: =1tondo fork: =1tondox: =x+1 含基本操作“x增1”的语句x: =x+1的频度分别为1,n和n2,则这三个程序段的时间复杂度分别为O (1),O(n),O(n2),分别称为常量阶、线性阶和平方阶。 算法还可能呈现的时间复杂度有: 对数阶O(logn),指数阶O(2n)等。 在n很大时,不同数量级时间复杂度显然有O (1) 一般情况下,对一个问题(或一类算法)只需选择一种基本操作来讨论算法的时间复杂度即可,有时也需要同时考虑几种基本操作,甚至可以对不同的操作赋以不同权值,以反映执行不同操作所需的相对时间,这种做法便于综合比较解决同一问题的两种完全不同的算法。 由于算法的时间复杂度考虑的只是对于问题规模n的增长率,则在难以计算基本操作执行次数(或语句频度)的情况下,只需求出它关于n的增长率或阶即可,一般可忽略常数项、底阶项、甚至系数。 例如,在下列程序段中: fori: =2tondo forj: =2toi-1dox: =x+1 语句x: =x+1执行次数关于n的增长率为n2,它是语句频度表达式(n-1)(n-2)/2中增长最快的一项。 4、算法所占用的存储空间: 空间复杂性 算法在运行过程中临时占用的存储空间的大小被定义为“算法的空间复杂性”。 空间复杂性包括程序中的变量、过程或函数中的局部变量等所占用的存储空间以及系统为了实现递归所使用的堆栈两部分。 算法的空间复杂性一般也以数量级的形式给出。 类似于算法的时间复杂度,以空间复杂度作为算法所需存储空间的量度,记作 S(n)=O(f(n)) 其中n为问题的规模(或大小)。 一个上机执行的程序除了需要存储空间来寄存本身所用指令、常数、变量和输入数据外,也需要一些对数据进行操作的工作单元和存储一些为实现计算所需信息的辅助空间。 若输入数据所占空间只取决于问题本身,和算法无关,则只需要分析除输入和程序之外的额外空间,否则应同时考虑输入本身所需空间(和输入数据的表示形式有关)。 若额外空间相对于输入数据量来说是常数,则称此算法为原地工作。 如果所占空间量依赖于特定的输入,则除特别指明外,均按最坏情况来分析,即以所占空间可能达到的最大值作为其空间复杂度。 5、时间和空间复杂性的计算和讨论 下面我们通过一个实例,了解算法的时间复杂性和空间复杂性的计算方法。 例2、计算y=anxn+an-1xn-1+an-2xn-2+……+a1x+a0的值。 [问题分析] 该问题计算的规模在于n的大小,n越大计算量也越大,乘积和累加的次数也愈多,这里n就是问题的规模。 对于该问题的求解的基本条件是: 已知x的值和系数a[0]~a[n]。 (与问题规模n有关,空间的单元是固定的,若相差几个单元,可以忽略不计) 算法2_1: y=a[0]; fork: =1tondo begin s: =a[k]; forj: =1tokdo s: =s*x y: =y+s; end; writeln(‘y=’,y); 在该运算中所需存储单元a[0],a[1],……,a[n],以及固定的几个简单变量,所以其空间复杂性与规模n成正比,即为O(n)。 而时间复杂性是内循环乘法计算和外循环的累加计算,计算y共用乘法次数是: 1+2+3+……+n=n(n+1)/2,加法仅n次,而在计算机内部实现乘法要比加法花费更多的时间,所以该算法的时间复杂性为O(n(n+1)/2)≈O(n*n/2)≈O(n^2)。 算法2_2: 算法2_1中的乘法运算有重复计算: xk不需要每次从1,x,x2,x3,……,xk乘法去实现,只要在原先xk-1基础上再做一次乘法就可以得到xk的结果,多用一个b数组存放1,x,x2,x3,……,xn,该问题的空间复杂性为O(2n)≈O(n)。 b[0]: =1; fork: =1tondo b[k]: =n[k-1]*x; y: =a[0]; fork: =1tondo y: =y+a[k]*b[k]; writeln(‘y=’,y); 此算法用了两次单循环命令,共用了2n次乘法和n次加法,时间复杂性为O(2n)≈O(n)。 算法2_3: 利用数学中的秦九邵公式,将y的计算公式改写为: y=(…(((an*x+an-1)*x+an-2)…+a1)*x+a0 a[0]a[0],a[1],……,a[n]存放系数,x为初始数据,则其程序为: y: =a[n]; fork: =n-1downto0do y: =y*x+a[k]; writeln(‘y=’,y); 该算法所用的存储空间为n个单元,即空间复杂性为O(n),而只用了n次乘法和n次加法,所以时间复杂性为O(n)。 由此,我们可以看到,在以上3个求多项式的值的算法中,算法2_3可以获得最好的效果。 四、算法的优化 1、以空间换时间 算法中的时间和空间往往是矛盾的,时间复杂性和空间复杂性在一定条件下也是可以相互转化的,有时候为了提高程序运行速度,在算法的空间要求不苛刻的前提下,设计算法时可考虑充分利用有限的剩余空间来存储程序中反复要计算的数据,这就是“用空间换时间”策略,是优化程序的一种常用方法。 相应的,在空间要求十分苛刻时,程序所能支配的自由空间不够用时,也可以以牺牲时间为代价来换取空间,由于当今计算机硬件技术发展很快,程序所能支配的自由空间一般比较充分,这一方法在程序设计中不常用到,下面看一个例子。 例3、阿姆斯特朗数 [问题描述] 编一个程序找出所有的三位数到七位数中的阿姆斯特朗数。 阿姆斯特朗数也叫水仙花数,它的定义如下: 若一个n位自然数的各位数字的n次方之和等于它本身,则称这个自然数为阿姆斯特朗数。 例如153(153=1*1*1+3*3*3+5*5*5)是一个三位数的阿姆斯特朗数,8208则是一个四位数的阿姆斯特朗数。 [算法设计] 由于阿姆斯特朗数是没有规律的,所以程序只能采用穷举法,一一验证范围内的数是否阿姆斯特朗数,若是则打印之。 但若对任一个数K,都去求它的各位的若干次方,再求和判断是否等于K,效率比较差,因为我们注意到每个位只可能是0~9,而且只要算3~7次方。 所以,为了使得程序尽快运行出正确结果,我们采用“以空间换时间”的策略,使用一个数组power存放所有数字的各次幂之值,power[i,j]等于i的j次方。 变量currentnumber存放当前要被验证的数,数组digit存放当前数的各位数字,开始时digit[3]=1,其它元素均为0,此时表示当前数为100。 highest为当前数的位数。 [参考程序] programAmst(input,outoutp);constmaxlen=7;vari,j,currentnumber,highest,sum,total: longint;digit: array[0..maxlen+1]ofinteger;power: array[0..9,0..maxlen]oflongint; begin fori: =0to9do begin power[i,0]: =1; forj: =1tomaxlendopower[i,j]: =power[i,j-1]*i end; fori: =1tomaxlendodigit[i]: =0; digit[3]: =1;highest: =3;currentnumber: =100;total: =0; whiledigit[maxlen+1]=0do begin sum: =0; fori: =1tohighestdosum: =sum+power[digit[i],highest]; ifsum=currentnumber thenbegintotal: =total+1; write(currentnumber: maxlen+5); iftotalmod6=0thenwritelnend; digit[1]: =digit[1]+1;i: =1; whiledigit[i]=10do begindigit[i+1]: =digit[i+1]+1;digit[i]: =0;i: =i+1end; ifi>highestthenhighest: =i; currentnumber: =currentnumber+1 end; writeln end. [运行结果] 1533703714071634820894745474892727930845488341741725421081898008179926315 2、尽可能利用前面已有的结论 比如递推法、构造法和动态规划就是这一策略的典型应用。 3、寻找问题的本质特征,以减少重复操作 比如: 例1中求H数问题的算法空间复杂度显然为O (1),而时间复杂度很难估算,下面我们通过对算法的改进来对它作进一步的分析。 算法1_2: 构造法计算第n个H数 在H数问题中,由于所要求的H数在长整型范围,最多可达2的31次方数量级,显然,用穷举与逐一判断的方法效率太低,对序号大的H数很难在规定时间内运行出正确结果,有没有更好的办法呢? 有,那就是构造法。 分析H数问题,发现H数因子只有4种,可以考虑从因子出发由小到大地生成H数。 假如用一个线性表来存放H数,称这个表为H数表,则H数表中每个元素的2倍,3倍,5倍及7倍均是H数,不妨将由H数表中每个元素的2倍组成的线性表称为H数表的2倍表,设想再用4个线性表分别存放H数表的2倍表,3倍表,5倍表和7倍表,然后利用这5个表来生成H数表,生成方法如下: 开始时H数表中存有第一个H数1,其它四个表为空表。 将当前的H数的2倍,3倍,5倍及7倍依次添加到四个表中去,这时四个表中各有一个元素,接下去的所有的H数将由这四个表来生成,每次将四个表的第一个元素中的最小者取出来,这个数就是下一个要求的H数,将它添加到H数表中去,并将这个H数的2倍,3倍,5倍及7倍依次添加到四个表中去,然后从四个表中删除这个元素,如果表中有这个元素的话。 重复这一过程,直到所要求的H数找到为止。 程序中为了求出第n个H数,必须将它前面的所有的H数都求出来并加以保存,所以这一算法的空间复杂度为O(n);在计算每一个H数的过程中,对4个H数的倍数表要进行删除操作,对线性表的删除操作的时间复杂度为O(n),所以这一算法的总的时间复杂度为O(n)*O(n),即O(n*n)。 [参考程序] programHnum2(input,putput);constmaxn=3000;vari,j,n,min,t2,t3,t5,t7: longint;h,h2,h3,h5,h7: array[1..maxn]oflongint;beginwrite('Inputn(n<=',maxn,'): ');readln(n); h[1]: =1;h2[1]: =2;h3[1]: =3;h5[1]: =5;h7[1]: =7; t2: =1;t3: =1;t5: =1;t7: =1; fori: =2tondo begin min: =h2[1]; ifh3[1] =h3[1]; ifh5[1] =h5[1]; ifh7[1] =h7[1]; h[i]: =min; t2: =t2+1;h2[t2]: =h[i]*2; t3: =t3+1;h3[t3]: =h[i]*3; t5: =t5+1;h5[t5]: =h[i]*5; t7: =t7+1;h7[t7]: =h[i]*7; ifh2[1]=min thenbeginforj: =1tot2-1doh2[j]: =h2[j+1];t2: =t2-1end; ifh3[1]=min thenbeginforj: =1tot3-1doh3[j]: =h3[j+1];t3: =t3-1end; ifh5[1]=min thenbeginforj: =1tot5-1doh5[j]: =h5[j+1];t5: =t5-1end; ifh7[1]=min thenbeginforj: =1tot7-1doh7[j]: =h7[j+1];t7: =t7-1end end; writeln('Theno.',n,'Hnumberis',h[n]) end. 算法1_3: 优化构造法 通过运行程序,你会很遗憾地发现,虽然程序运行的速度大大优于第一种算法,但当n较大时(如n=5000)时,程序运行将会造成空间不够。 这是为什么呢? 这是因为PASCAL语言允许程序使用的存储空间为64KB,而一个长整型数是用32位二进制数来表示的,即要用4个字节(Byte)来存放。 而64KB=64*1024B=65536B,只能存放16000多个长整型数,而上述算法中的空间复杂度为O(n),具体的值大约为n的5倍,因此当n太大(如n=5000)时,n*5的值就会远远超出16000,造成运行空间不够。 如何解决这个问题呢? 仔细分析上述算法,可以发现H数的四个倍数表中的所有元素与H数表中的所有元素相比,只相差一个倍数而已,可不可以不用这四个倍数表,而借用H数表来表示它们呢? 答案是肯定的,为了说明问题,首先引进线性表的指针的概念,在用数组描述线性表时,线性表中的元素的位置完全取决于数组下标的值,我们不妨将这个下标值即一个整数看作指向线性表中某一元素的指针。 需要说明的是,这里的所谓的指针只是为了便于说明问题而定义的,与PASCAL语言中所指的指针是完全不同的。 这样就可以用四个指针分别指向H数表中的四个数,这四个数的2倍,3倍,5倍及7倍的值分别是四个倍数表中的首元素,以此表示H数的四个倍数表。 一开始,它们均指向1,即当前的四个倍数表的首元素分别为2*1,3*1,5*1,7*1,取四数中最小者作为第二个H数2,记入H数表中,并将代表H数的2倍表的指针下移一,指向2,再比较2*2,3*1,5*1,7*1,取3,将代表H数的3倍表的指
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 算法 及其 评价
![提示](https://static.bdocx.com/images/bang_tan.gif)