一种数据结构的优劣是由实现其各种运算的算法具体体现的,对数据结构的分析实质上就是实现运算算法的分析,除了要验证算法是否正确解决该问题之外,还需要对算法的效率作性能评价。在计算机程序设计中,算法分析是十分重要的。
算法分析是每个程序设计人员应该掌握的技术。评价算法性能的标准主要从算法执行时间与占用内存空间两方面考虑,即算法执行所需的时间和存储空间来判断一个算法的优劣。
1.性能评价
对问题规模与该算法在运行时所占用空间与所耗费时间给出一个数量关系的评价。
数量关系评价体现在时间上,即算法经编程实现后在计算机中运行所耗费的时间。
数量关系评价体现在空间上,即算法经编程实现后再计算机中运行所占用的存储量。
2.问题规模
算法性能与问题规模相关。问题规模是问题大小的本质表示,对不同的问题其表现形式不同,算法求解问题的输入量称为问题的规模,一般用整数表示。一个图论问题的规模则是图中的顶点数或边数,对矩阵而言是其阶数,对多项式运算而言是多项式项数,对集合运算而言是集合中的元素个数,可以说算法效率应是问题规模的函数。
算法的时间性能分析
1.算法耗费的时间
一个算法的执行时间是指算法中所有语句执行时间的总和。每条语句的执行时间等于该条语句的执行次数乘以执行一次所需实际时间。
由于语句的执行要由源程序经编译程序翻译成目标代码,目标代码经装配后再执行,语句执行一次实际所需的具体时间是与计算机的软、硬件环境(计算机速度、编译程序质量、输入数据量等)密切相关的,故难以精确估计。
2.语句频度
质量一个算法的效率应当抛弃具体计算机条件,仅仅考虑算法本身的效率高低。算法时间分析质量的标准并不是针对实际执行时间精确算法执行的时间,而是根据算法中语句的执行次数做出估计,从中得到算法执行时间的信息。
语句频度是指该语句在一个算法中重复执行的次数。一个算法的时间耗费就是该算法中所有语句频度之和。
例1. 求两个n阶方阵的乘积C=A*B。
#define n 100 /*n可根据需求定义,这里假定为100*/
void MatrixMulti(int a[n][n],int b [n][n],int c[n][n])
{ 该算法每一语句的语句频度为
(1)for(i=0;i<n;i++) n+1
(2) for(j=0;j<n;j++) n(n+1)
(3) { c[i][j]=0; n^2
(4) for(k=0;k<n;k++) n^2(n+1)
(5) c[i][j]=c[i][j]+a[i][k]*b[k][j]; n^3
}
}
【分析】语句(1)的循环控制变量i从0增加到n,测试条件i=n成立才会终止,故它的语句频度是n+1,但是它的循环体却只能执行n次。语句(2)作为语句(1)循环体内的语句应该执行n次,但语句(2)作为语句(1)循环体内的语句应该执行n次,但语句(2)本身要执行n+1次,所以语句(2)的频度是n(n+1).同理可得语句(3)、(4)和(5)的频度分别是n^2、n^2(n+1)和n^3.
该算法中所有语句的频度之和(即算法的时间耗费)为
f(n)=2n^3+3n^2+2n+1
也就是说、该矩阵乘积算法的问题规模是矩阵的阶数n,时间耗费是矩阵阶数n的函数。
3.算法的时间复杂度
为便于比较解决同一问题的不同算法,通常以算法中基本操作重复执行的频度作为度量标准。基本操作是指算法中选取一种对所研究问题是基本运算的操作,用随着问题规模增加的函数来表征,以此作为时间量度。
对于算法分析,关心的是算法中语句总的执行次数f(n)是问题规模n的函数,进而分析f(n)随n的变化情况并确定T(n)的数量级(Order of Magnitude)。这里用“O”来表示数量级,给出算法的时间复杂度概念。算法的时间复杂度T(n)是该算法的时间度量,记作
T(n)=O(f(n))
它表示随问题规模n的增大,算法的执行时间的增长率和f(n)的增长率相同,称作算法的渐进时间复杂度,简称时间复杂度。
在例1中算法MatrixMulti中,当n充分大时,f(n)和n^3之比是一个不等于零的常数,即f(n)和n^3是同阶的,或者说f(n)和n^3的数量级相同,O(n^3)是算法MatrixMulti的渐进时间复杂度。
数学符号“O”的严格数学定义为:
若T(n)和f(n)是定义在正整数集合上的两个函数,则T(n)=O(f(n))表示存在正的常数C和n0,使得当n>=n0时都满足0<=T(n)<=Cf(n).
4.渐进时间复杂度
由于算法执行的实际机器时间难以精确统计,因此主要考虑用算法时间复杂度的数量级(即算法的渐进时间复杂度)来评价一个算法的时间性能。
例如,由两个算法A1和A2求解同一问题,时间复杂度分别是T1(n)=100n^2,T2(n)=5n^3.
①当输入量n<20时,有T1(n)>T2(n),后者花费的时间较少。
②随着问题规模的增大,两个算法的时间开销之比5n^3/100n^2=n/20亦随着增大。即当问题规模较大时,算法A1比算法A2有效的多。
它们的渐进时间复杂度O(n^2)和O(n^3)从宏观上评价了这两个算法在时间方面的质量。在算法分析时,往往对算法的时间复杂度渐进时间复杂度不予区分,而经常是讲渐进时间复杂度T(n)=O(f(n))简称为时间复杂度,其中的f(n)一般是算法中频度最大的语句频度。例1中的算法MatrixMulti的时间复杂度为T(n)=O(n^3),f(n)=n^3是该算法中语句(5)的频度。
下面在举例说明如何求算法的时间复杂度。
例1.4 常数阶实例
temp=i;
i=j;
j=temp;
以上三条单个语句的频度均为1,该程序段的执行时间是一个与问题规模n无关的常数。算法的时间复杂度为常数阶,记作T(n)=O(1)。如果算法执行的时间不随着问题规模n的增大而增长,即使算法中有上千条语句,其执行时间也不过是一个较大的常数。此类算法的时间复杂度是O(1)。
例1.5 线性阶实例
for(i=1;i<=n;i++)
x=x+1;
其时间复杂度为O(n),称为线性阶。
例1.6 平方阶实例
(1)x=0;y=0;
(2)for(k=1;k<=n;k++)
(3)x++;
(4)for(i=1;i<=n;i++)
(5)for(j=1;j<=n;j++)
(6)y++;
一般情况下,对步进循环语句只需考虑循环体中语句的执行次数,忽略该语句中步长加1、终值判断、控制转移等成分。因此,以上程序段中频度最大的是语句(6),其频度为f(n)=n^2,所以该程序段的时间复杂度为T(n)=O(n^2).
当有若干个循环语句时,算法的时间复杂度是由嵌套层数最多的循环语句中最内层语句的频度f(n)决定的。
例1.7给出源操作x=x+1;语句频度及程序的时间复杂度分析。
(1)x=1;
(2)for(i=1;i<=n;i++)
(3)for(j=1;j<=i;j++)
(4)x=x+1
该程序段中频度最大的是语句(4),内循环的执行次数虽然与问题规模n没有直接关系,但是却与外层循环的变量取值有关,而最外层循环的次数直接与n有关,因此可以从内层循环向外层分析语句(4)的执行次数:当i=1时,j取值范围是1~1,原操作执行次数为1;当i=2时,j取值范围是1~2;当i=3时,j取值范围是1~3,执行次数为3,以此类推,故原操作x=x+1;执行的总次数为
f(n)=(1+2+3+...+n)=n(n+1)/2=n^2/2+n/2
则该程序段的时间复杂度为T(n)=O(n^2/2+n/2)=O(n^2).
5.常用算法时间复杂度
数据结构中常用的时间复杂度频率计数有以下7种:
O(1)常数型,O(n)线性型,O(n^2)平方型,O(n^3)立方型,O(2^n)指数型,O(log2n)对数型(ps:以2为底n的对数),O(nlog2n)二维型(ps:同上)
按时间复杂度由小到大递增排列如下所示。一般来说,前三种可实现,后三种虽理论上是可实现的,但实际上只有当n限制在很小的范围时才有意义,当n较大时实现困难。
常用的时间复杂度频率表
log2n n nlog2n n^2 n^3 2^n
0 1 0 1 1 2
1 2 2 4 8 4
2 4 8 16 64 14
3 8 24 64 512 256
4 16 64 256 5096 65536
5 32 160 1024 32768 2147483648
6.最坏时间复杂度和平均时间复杂度
算法的时间复杂度不仅仅依赖于问题的规模,还输入实例的初始状态有关。
例1.8 在数值A[0..n-1]中查找给定值k的算法大致如下,分析其时间复杂度。
(1)i=n-1;
(2)while(i>=0&&(a[i]!=k))
(3)i--;
(4)return i;
此算法中,语句(3)的频度不仅与问题规模n有关,还与A的各元素值及k的取值有关。
①最坏情况:若A中没有与k相等的元素,则语句(3)的频度f(n)=n。
②最好情况:若A的最后一个元素等于k,则语句(3)的频度f(n)是常数0.
最坏情况下的时间复杂度称最坏时间复杂度。一般不特别说明,讨论的时间复杂度均是最坏情况下的时间复杂度。这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的上界,这就保证了算法的运行时间不会比这一上界更长。由此可知,上述算法的时间复杂度为T(n)=O(n)。
平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,算法的期望运行时间。
算法的空间性能分析
一般情况下,一个程序在机器执行时,除了需要存储本身所用的指令、常数、变量和输入数据以外,还需要一些对数据进行操作的辅助存储空间。其中,对于输入数据所占用的具体存储量取决于问题本身,与算法无关,这样只需分析该算法在实现时所需要的辅助空间个数即可。
1.算法耗费的空间
一个算法的占用空间是指算法实际占用的辅助空间总和。
由于实际占用空间与计算机的软件(编译系统)、硬件(字长等)环境密切相关,以整形为例,可能在一种系统中需要2字节,在另一个系统中需要4字节,实际占用空间的多少难以相互类比。算法空间分析度量的标准并不是计算实际占用空间,而是计算整个算法的辅助空间单元个数。
2.算法的空间复杂度
算法的空间复杂度S(n)定义为该算法所耗费的存储空间数量级,它是问题规模n的函数。记作
S(n)=O(f(n))
若算法执行时所需的辅助空间相对于输入数据量而言是一个常数,则称这个算法为原地工作。辅助空间为O(1)。
例1.9 将一维数组a中的n个数据逆序存放到原数组中,给出实现该问题的两种算法。
【算法1】
for (i=0;<n;i++)
b[i]=a[n-i-1];
for(i=0;<n;i++)
a[i]=b[i];
【算法2】
for(i=0;i<n/2;i++)
{
t=a[i];
a[i]=a[n-i-1];
a[n-i-1]=t;
}
算法1的空间复杂度为O(n),需要一个大小为n的辅助数组b。
算法2的空间复杂度为O(1),仅需要一个变量t,与问题规模没有关系。
算法的时间复杂度和空间复杂度合称为算法的复杂度。
算法性能选择
要想使一个算法即占用存储空间少,又运行时间短,而且其他性能也好,这是很难做到的。原因是上述要求有时相互抵触:要节约算法的执行时间往往要以牺牲更多的空间为代价,而为了节约空间可能要耗费更多的计算时间。因此,只要根据具体的情况进行取舍。
①若程序使用次数少,则力求算法简明易懂
②对于反复使用的程序,应尽可能选用快速的算法。
③若待解决的问题数据量较大,计算机存储空间较小,则相应算法主要考虑如何节省空间。