算法-效率的度量和函数的渐进增长

2.7 算法效率的度量方法

刚才我们提到设计算法要提高效率 。这里效率大都指算法的执行时间 。 那么我们如何度量一个算法的执行时间呢?
正所谓"是骡子是马,拉出来遛遛"。比较容易想到的方法就是,我们通过对算法的数据测试,利用计算机的计时功能,来计算不同算法的效率是高还是低。

2.7.1 事后统计方法

事后统计方法:这种方法主要是通过设计好的测试程序和数据,利用计算机计时
器对不同算法编制的程序的运行时间进行比较,从而确定算法效率的高低。

但这种方法显然是有很大缺陷的:
• 必须依据算法事先编制好程序,这通常需要花费大量的时间和精力 。如果编制出来发现它根本是很糟糕的算法,不是竹篮打水一场空吗?
• 时间的比较依赖计算机硬件和软件等环境因素,有时会掩盖算法本身的优劣 。 要知道,现在的一台四核处理器的计算机, 跟当年 286、 386、 486 等老爷爷辈的机器相比,在处理算法的运算速度上,是不能相提并论的; 而所用的操作系统、编译器、运行框架等软件的不同,也可以影响它们的结果;就算是同一台机器, CPU使用率和内存占用情况不一样,也会造成细微的差异。
• 算法的测试数据设计困难,并且程序的运行时间往往还与测试数据的规模有很大关系,效率高的算法在小的测试数据面前往往得不到体现 。 比如 10个数字的排序,不管用什么算法, 差异几乎是零 。 而如果有一百万个随机数字排序,那不同算法的差异就非常大了。那么我们为了比较算法,到底
用多少数据来测试,这是很难判断的问题。
基于事后统计方法有这样那样的缺陷,我们考虑不予采纳 。

2.7.2 事前分析估算方法

我们的计算机前辈们,为了对算法的评判更科学,研究出了一种叫做事前分析估算的方法。
事前分析估算方法:在计算机程序编制前,依据统计方法对算法进行估算。
经过分析, 我们发现, 一个用高级程序语言编写的程序在计算机上运行时所消耗的时 间取决于下列因素:
1. 算法采用的策略、 方法。
2 . 编译产生的代码质量 。
3 . 问题的输入规模。
4. 机器执行指令的速度。
第 1 条当然是算法好坏的根本,第 2 条要由软件来支持, 第 4 条要看硬件性能。也就是说,抛开这些与计算机硬件、软件有关的因素,一个程序的运行时间,依赖于算法的好坏和问题的输入规模。 所谓问题输入规模是指输入量的多少。
我们来看看今天刚上课时举的例子 , 两种求和的算法 :
第一种算法 :

int  sum = 0,  n=100;		/ * 执行 1 次 */
for (int i = 1; i < = n; i++)	/ *执行了 n+1次*/
sum = sum +i ;		 / * 执行n次*/
System.out.println(sum);		/ * 执行 1 次 */
第二种算法 :
int 1, sum = 0,n = 100;		/ * 执行1次 */
sum = (1 + n) * n /2;		/ * 执行1次 */
System.out.println(sum);		/ * 执行1次 */
显然,第一种算法,执行了 1+ ( n+1 ) +n+1 次 =2n+3次; 而第二种算法,是1+1+1=3 次。事实上两个算法的第一条和最后一条语句是一样的,所以我们关注的代码其实是中间的那部分,我们把循环看作一个整体,忽略头尾循环判断的开销, 那么这两个算法其实就是 n 次与 1 次的差距。算法好坏显而易见。
我们再来延伸一下上面这个例子:
int i,j,x=0,sum=0, n = 100;		/* 执行1次 */
   for(i = 1; i <= n; i++){
       for(j = 1; j <= n; j++){
 	   x++;					/* 执行n×n次 */
	   sum+=x;
       }
    }
System.out.println(sum);		/* 执行1次 */
这个例子中, i 从 1 到 100 ,每次都要让 j 循环 100 次,而当中的 x++和 sum+=x ; 其实就1+2+3+…+10000 ,也就是 100^2 (100的2次方)次,所以这个算法当中,循环部分的代码整体需要执行 n^2 (忽略循环体头尾的开销)次。显然这个算法的执行次数对于同样的输入规模 n =100 , 要多于前面两种算法,这个算法的执行时间随着 n 的增加也将远远多于前面两个。
此时你会看到,测定运行时间最可靠的方法就是计算对运行时间有消耗的基本操作的执行次数, 运行时间与这个计数成正比。
我们不关心编写程序所用的程序设计语言是什么,也不关心这些程序将跑在什么样的计算机中,我们只关心它所实现的算法。这样,不计那些循环索引的递增和循环终止条件、 变量声明、打印结果等操作, 最终,在分析程序的运行时间时,最重要的是把程序看成是独立于程序设计语言的算法或一系列步骤。
可以从问题描述中得到启示,同样问题的输入规模是 n ,求和算法的第一种,求1+2+… +n 需要一段代码运行 n 次 。那么这个问题的输入规模使得操作数量是 f ( n)= n ,显然运行 100 次的同一段代码规模是运算 10 次的 10 倍。而第二种,无论 n 为多少,运行次数都为 1 ,即 f (n) =1 ; 第三种,运算 100 次是运算 10 次的 100倍。因为它是 f (n) =n^2 。
我们在分析一个算法的运行时间时,重要的是把基本操作的数量与输入规模关联起来,即基本操作的数量必须表示成输入规模的函数(如图 2-7-1 所示) 。


我们可以这样认为,随着 n 值的越来越大,它们在时间效率上的差异也就越来越大 。好比你们当中有些人每天都在学习,我指有用的学习,而不是只为考试的死读书 , 每天都在进步,而另一些人,打打游戏,睡睡大觉。 入校时大家都一样 , 但毕业时结果可能就大不一样,前者名企争抢着要,后者求职无门 。

2.8 函数的渐近增

我们现在来判断一下,两个算法 A 和 B 哪个更好。 假设两个算法的输入规模都是n ,算法 A 要做 2n + 3 次操作,你可以理解为先有一个 n 次的循环 ,执行完成后,再有一于 n 次循环,最后有三次赋值或运算,共 2n + 3 次操作。 算法 B 要做 3n + 1 次操作。 你觉得它们谁更快呢?
准确说来,答案是不一定的(如表 2-8-1 所示)。


当 n = 1 时,算法 A 效率不如算法 B (次数比算法 B 要多一次)。 而当 n = 2时,两者效率相同 ; 当 n>2 时,算法 A 就开始优于算法 B 了,随着 n 的增加,算法 A 比算法 B 越来越好了: (执行的次数比 B 要少)。于是我们可以得出结论,算法 A 总体上要好过算法 B 。
此时我们给出这样的定义,输入规模 n 在没有限制的情况下,只要超过一个数值N ,这个函数就总是大于另一个函数,我的称函数是渐近增长的。
函数的渐近增长:给定两个函数 f(n)和g(n), 如果存在-个整数 N ,使得对于所有的 n>N, f(n)总是比g(n)大,那么 , 我们说f(n)的增长渐近快于 g(n)。
从中我们发现,随着 n 的增大,后面的 +3 还是 +1 其实是不影响最终的算法变化的,例如算法 A' 与算法 B ' ,所以, 我们可以忽略这些加法常数。 后面的例子,这样的常数被忽略的意义可能会更加明显 。
我们来看第二个例子,算法 C 是 4n+8 ,算法 D 是 2n^2 + 1 (如表 2-8-2 所示) 。


当 n <= 3 的时候,算法 C 要差于算法 D (因为算法 C 次数 比较多) ,但当 n > 3后,算法 C 的优势就越来越优于算法D了,到后来更是远远胜过。而当后面的常数去掉后,我们发现其实结果没有发生改变。 甚至我们再观察发现, 哪怕去掉与 n 相乘的常数,这样的结果也没发生改变,算法 C 的次数随着 n 的增长,还是远小于算法D 。 也就是说, 与最高次项相乘的常数并不重要
我们再来看第三个例子。算法 E 是 2n^2(n的2次方) + 3n + 1 ,算法 F 是 2n^3 + 3n + 1 (如表2-8-3 所示)。

当 n=l 的时候 ,算法E与算法 F 结果相同,但当 n>l 后 , 算法E的优势就要开始优于算法F,随着 n 的增大,差异非常明显。通过观察发现,最高次项的指数大的,函数随着 n 的增长,结果也会变得增长特别快。
我们来看最后一个例子。算法G是 2n^2,算法 H 是 3n + 1 , 算法 I 是 2n^2 + 3n + 1(如表 2-8-4 所示) 。

这组数据应该就看得很清楚。 当 n 的值越来越大时,你会发现 , 3n+l 已经没法和2n^2 的结果相比较 ,最终几乎可以忽略不计。也就是说 , 随着 n 值变得非常大以后,算法 G 其实已经很趋近于算法 I。于是我们可以得到这样一个结论,判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数。
判断一个算法好不好,我们只通过少量的数据是不能做出准确判断的。根据刚才的几个样例 , 我们发现,如果我们可以对比这几个算法的关键执行次数函数的渐近增长性,基本就可以分析出:某个算法,随着 n 的增大,它会越来越优于另一算法,或者越来越差于另一算法。这其实就是事前估算方法的理论依据 ,通过算法时间复杂度来估算算法时间效率。
引用《大话数据结构》作者:程杰
  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值