效率
事后统计方法:这种方法主要是通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的程序的运行时间进行比较,从而确定算法效率的高低。
但这种方法显然是有很大缺陷的:
- 必须依据算法事先编制好测试程序,通常需要花费大量时间和精力,完了发觉测试的是糟糕的算法,那不是功亏一篑?赔了娘子又折兵?
- 不同测试环境差别不是一般的大!
事前分析估算方法:在计算机程序编写前,依据统计方法对算法进行估算。
经过总结,我们发现一个高级语言编写的程序在计算机上运行时所消耗的时间取决于下列因素:
- 算法采用的策略,方案
- 编译产生的代码质量
- 问题的输入规模
- 机器执行指令的速度
由此可见,抛开这些与计算机硬件、软件有关的因素,一个程序的运行时间依赖于算法的好坏和问题的输入规模。(所谓的问题输入规模是指输入量的多少)
高斯算法示例分析
普通算法:
int i, sum = 0, n = 100; // 执行1次
for( i=1; i <= n; i++ ) // 执行了n+1次
{
sum = sum + i; // 执行n次
}
高斯算法:
int sum = 0, n = 100; // 执行1次
sum = (1+n)*n/2; // 执行1次
第一种算法执行了1+(n+1)+n=2n+2次。
第二种算法,是1+1=2次
如果我们把循环看做一个整体,忽略头尾判断的开销,那么这两个算法其实就是n和1的差距。
问题: 循环判断在算法1里边执行了n+1次,看起来是个不小的数量,凭什么说忽略就能忽略?
再来一个例子
int i, j, x=0, sum=0, n=100;
for( i=1; i <= n; i++ ) {
for( j=1; j <= n; j++ ) {
x++;
sum = sum + x;
}
}
这个例子中,循环条件i从1到100,每次都要让j循环100次,如果非常较真的研究总共精确执行次数,那是非常累的。
另一方面,我们研究算法的复杂度,侧重的是研究算法随着输入规模扩大增长量的一个抽象,而不是精确地定位需要执行多少次,因为如果这样的话,我们就又得考虑回编译器优化等问题,然后,然后就永远也没有然后了!
所以,对于这个例子的算法,我们可以果断判定需要执行100^2次。
函数的渐进增长
测试一
假设两个算法的输入规模都是n,算法A要做2n+3次操作,你可以这么理解:先执行n次的循环,执行完成后再有一个n次的循环,最后有3次运算。
算法B要做3n+1次操作,理解同上,你觉得它们哪一个更快些呢?
当n=1时,算法A1效率不如算法B1,当n=2时,两者效率相同;当n>2时,算法A1就开始优于算法B1了,随着n的继续增加,算法A1比算法B1逐步拉大差距。所以总体上算法A1比算法B1优秀。
函数的渐近增长:给定两个函数f(n)和g(n),如果存在一个整数N,使得对于所有的n>N,f(n)总是比g(n)大,那么,我们说f(n)的增长渐近快于g(n)。
从刚才的对比中我们还发现,随着n的增大,后面的+3和+1其实是不影响最终的算法变化曲线的。
测试二
第二个测试,算法C是4n+8,算法D是2n^2+1。
我们观察发现,哪怕去掉与n相乘的常数,两者的结果还是没有改变,算法C2的次数随着n的增长,还是远小于算法D2。
也就是说,与最高次项相乘的常数并不重要,也可以忽略。
测试三
第三个测试,算法E是2n^2 + 3n+1,算法F是2n^3+3n+1。
我们通过观察又发现,最高次项的指数大的,函数随着n的增长,结果也会变得增长特别快。
测试四
这组数据我们看得很清楚,当n的值变得非常大的时候,3n+1已经没法和2n^2的结果相比较,最终几乎可以忽略不计。而算法G在跟算法I基本已经重合了。
结论
于是我们可以得到这样一个结论,判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高项)的阶数。
注意,判断一个算法好不好,我们只通过少量的数据是不能做出准确判断的,很容易以偏概全。