事后统计方法:
这种方法主要是通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的程序的运行时间进行比较,从而确定算法效率的高低
但是这种方法显然是有很大缺项的;
必须依据算法事先编制好测试程序,通常需要花费大量时间和精力,最后发觉测试的是糟糕的算法,那岂不是功亏一篑?
不同的测试环境差别不是一般的大
事前分析估算方法:
在计算机程序编写前,依据统计方法对算法进行估算
经过总结 ->
我们发现一个高级语言编写的程序在计算机上运行时所消耗的时间取决于下列因素:
1、算法采用的策略,方案
2、编译产生的代码质量
3、问题的输入规模
4、机器执行指令的速度
由此可见,抛开这些与计算机硬件、软件有关的因素,一个程序的运行时间依赖于算法的好坏和问题的输入规模。(所谓的问题输入规模是指输入量的多少)
我们再来看看 数学家高斯 设计的等差数列的算法:
第一种算法【执行了 1+( n + 1 )+ n = 2n + 2 次】:
- int main() {
- int i = 0;
- int sum = 0;
- int n = 100; //执行 1 次
- for(i = 0;i <= n;i++) { //执行了n + 1 次
- sum = sum + i; //执行 n 次
- }
- return 0;
- }
第二种算法【执行了 1 + 1 = 2 次】:
- int main() {
- int sum = 0;
- int n = 100; //执行 1 次
- sum = (1 + n)*n / 2 //执行 1 次
- return 0;
- }
如果我们把循环看做一个整体,忽略头尾判断的开销,那么这两个算法其实就是 n 和 1 的差距
- int main() {
- int x = 0;
- int sum = 0;
- int i = 0;
- for(i = 0;i < 100;i++) {
- int j = 0;
- for(j = 0;j < 100;j++) {
- x++;
- sum = sum + x;
- }
- }
- return 0;
- }
这个例子中,循环条件 i 从 1 到 100,每次都要让 j 循环100次,如果非常较真的研究总共精确执行次数,那是非常累的、
另一方面,我们研究算法的复杂度,侧重的是研究算法随着输入规模扩大增长量的一个抽象,而不是精确的定位需要执行多少次,因为如果是这样的话,我们就又得考虑编译器优化等问题~
所以,对于刚才例子的算法,我们可以果断判定需要执行 100^2次
我们不关心编写程序所用的语言是什么,也不关心这些程序将跑在上面样的计算机上,我们只关心它锁实现的算法
这样,不计算那些循环索引的递增和循环终止条件、变量声明带你结果等操作。最终在分析程序的运行时间时,最重要的是把程序看成是独立于程序设计语言的算法或一些列步骤
我们在分析一个算法的运行时间时,重要的是把基本操作的数量和输入模式关联起来
当 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 其实是不影响最终的算法变化曲线的
例如 A2 , B2 ,在图中他们压根就是被覆盖了,所以我们可以忽略这些加法常数
我们观察发现,哪怕去掉与 n 相乘的常数,两者的结果还是没有改变,算法 C2 的次数随着 n 的增长,还是远小于算法 D2
也就是说,与最高次项相乘的常数并不重要,也可以忽略~
这次我们发现,最高次项的指数大的,函数随着 n 的增长,结果也会变得增长特别快
这组数据我们看的很清楚了,当 n 的值变得非常大的时候,3n + 1 已经没法和 2n ^ 2 的结果相比较了,最终几乎可以忽略不计。而算法 G 在跟算法 I 基本已经重合了;
于是我们可以得到这样一个结论,判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高项)的阶数
注意,判断一个算法好不好,我们只通过少量的数据是不能做出准确判断的,很容易以偏概全