一般情况下,对一个问题(或一类算法)只需选择一种基本操作来讨论算法的时间复杂度即可,有时也需要同时考虑几种基本操作,甚至可以对不同的操作赋以不同权值,以反映执行不同操作所需的相对时间,这种做法便于综合比较解决同一问题的两种完全不同的算法。
由于算法的时间复杂度考虑的只是对于问题规模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倍表的指