算法复杂性
算法是指对特定问题求解步骤的一种描述。
算法具有以下特性。
(1)有穷性:算法是由若干条指令组成的有穷序列,总是在执行若干次后结束,不可能永不停止。
(2)确定性:每条语句有确定的含义,无歧义。
(3)可行性:算法在当前环境条件下可以通过有限次运算实现。
(4)输入输出:有零个或多个输入,一个或多个输出。
“好”算法的标准如下。
(1)正确性:正确性是指算法能够满足具体问题的需求,程序运行正常,无语法错误,能够通过典型的软件测试,达到预期的需求。
(2)易读性:算法遵循标识符命名规则,简洁易懂,注释语句恰当适量,方便自己和他人阅读,便于后期调试和修改。
(3)健壮性:算法对非法数据及操作有较好的反应和处理。
(4)高效性:高效性是指算法运行效率高,即算法运行所消耗的时间短。算法时间复杂度就是算法运行需要的时间。我们一般用算法基本运算的执行次数来衡量算法的效率。
(5)低存储性:低存储性是指算法所需要的存储空间低。算法占用的空间大小称为空间复杂度。
时间复杂度
算法运行需要的时间,一般将算法的执行次数作为时间复杂度的度量标准。例子:
sum = 0; //运行1次
total = 0; //运行1次
for(i = 1; i <= n; i++) { //运行n+1次
sum = sum + i; //运行n次
for(j = 1; j <= n; j++) //运行n*n+1次
total = total + i * j; //运行n*n次
}
把算法的所有语句的运行次数加起来:1+1+n+1+n+n(n+1)+nn,可以用一个函数T(n)表示:
当n足够大时,例如时,,我们可以看到算法运行时间主要取决于第一项,后面的甚至可以忽略不计。
用极限表示为:
如果用时间复杂度的渐进上界表示,如下图:
从图中可以看出,当时,,当足够大时,和近似相等。因此,我们用来表示时间复杂度渐近上界,通常用这种表示法衡量算法时间复杂度。
上面算法的时间复杂度渐近上界为,用极限表示为:
还有渐近下界符号,如下图所示:
从上图可以看出,当时,,当足够大时,和近似相等,因此,我们用来表示时间复杂度渐近下界。
渐近精确界符号,如下图所示:
从上图可以看出,当时,,当足够大时,和近似相等。这种两边逼近的方式,更加精确近似,因此,用来表示时间复杂度渐近精确界。
我们通常使用时间复杂度渐近上界来表示时间复杂度。
观察以下算法,并分析算法的时间复杂度。
i = 1; //运行1次
while(i <= n){ //可假设运行x次
i = i * 2; //可假设运行x次
}
观察上面算法,无法立即确定while及i=i*2运行了多少次。这时可假设运行了x次,每次运算后i值为,当时结束,即时结束,则,那么算法的运算次数为,时间复杂度渐近上界为。
在算法分析中,渐近复杂度是对算法运行次数的粗略估计,大致反映问题规模增长趋势,而不必精确计算算法的运行时间。在计算渐近时间复杂度时,可以只考虑对算法运行时间贡献大的语句,而忽略那些运算次数少的语句,循环语句中处在循环内存的语句往往运行次数最多,即为对运行时间贡献最大的语句。
注意:不是每个算法都能直接计算运行次数。
例如下面这个算法,在a[n]数组中顺序查找,返回其下标,如果没找到,则返回-1。
findx(int x) { //在a[n]数组中顺序查找x
for(i = 0; i < n; i++) {
if(a[i] == x) {
return i;
}
return -1;
}
}
我们很难计算以上程序到底执行了多少次,因为运行次数依赖于在数组中的位置,如果第一个元素就是,则执行了1次(最好情况);如果最后一个元素是,则执行次(最坏情况);如果分布概率均等,则平均执行次数为。
有些算法,如排序、查找、插入等算法,可以分为最好、最坏和平均情况分别求算法渐近复杂度,但我们考察一个算法通常考察最坏的情况,而不是考察最好的情况,最坏情况对衡量算法的好坏具有实际意义。
空间复杂度
算法占用的空间大小。一般将算法的辅助空间作为衡量空间复杂度的标准。
空间复杂度的本意是指算法在运行过程中占用了多少存储空间。算法占用的存储空间包括:
(1)输入/输出数据;
(2)算法本身;
(3)额外需要的辅助空间。
输入/输出数据占用的空间是必须的,算法本身占用的空间可以通过精简算法来缩减,但这个压缩的量是很小的,可以忽略不计。而在运行时使用的辅助变量所占用的空间,即辅助空间是衡量空间复杂度的关键因素。
例子:两数交换,分析其空间复杂度。
swap(int x, int y) {
int temp;
temp = x; //temp为辅助空间
x = y;
y = temp;
}
该算法使用了一个辅助空间temp,空间复杂度为;
注意:递归算法中,每一次递归需要一个栈空间来保存调用记录,因此,空间复杂度需要计算递归栈的辅助空间。
例子:计算的阶乘,并分析其空间复杂度。
fac(int n) { //计算n的阶乘
if(n < 0) { //小于零的数无阶乘值
printf("n<0, data error!");
return -1;
} else if(n == 0 || n == 1) {
return 1;
} else {
return n * fac(n - 1);
}
}
阶乘是典型的递归调用问题,递归包括递推和回归。递推是将原问题不断分解成子问题,直到达到结束条件,返回最近子问题的解;然后逆向逐一回归,最终到达递推开始的原问题,返回原问题的解。
试求5的阶乘,5的阶乘的递推和回归过程如下图:
上图的递推、回归过程是我们从逻辑思维上推理,用图的方式形象地表达出来的,计算机内部是怎样处理的呢?计算机使用一种称为“栈”的数据结构,如下图:
从上图的进栈、出栈过程中,我们可以清晰地看到,首先把子问题一步步地压进栈,直到得到返回值,再一步步地出栈,最终得到递归结果。在运算过程中,使用了个栈空间作为辅助空间,因此阶乘递归算法的空间复杂度为。上面的阶乘算法的时间复杂度也为,因为的阶乘仅比的阶乘多了一次乘法运算,fac(n)=n*fac(n-1)。如果用表示fac(n)的时间复杂度,可表示为:
常见的算法复杂度有以下几类。
(1)常数阶
常数阶算法运行的次数是一个常数,如5、20、100。常数阶算法时间复杂度通常用表示
(2)多项式阶
很多算法时间复杂度是多项式,通常用等表示。
(3)指数阶
指数阶时间复杂度运行效率极差。常见的有等。使用这样的算法要慎重。
(4)对数阶
对数阶时间复杂度运行效率较高,常见的有等。
常见的时间复杂度函数曲线如下:
从图中可以看出,指数阶增量随着的增加而急剧增加,而对数阶增加缓慢。它们之间的关系为:
设计算法时要注意算法复杂度增量的问题,尽量避免爆炸级增量。