(译)算法之美(3)--大O表示法
0.3 大O表示法
我们刚才已经看到草率地对运行时间进行分析会导致结果中出现让人不能接受的错误。但错误还是会存在的:不可能做到完全正确。一个有见解的分析是基于正确的简化之上的。
前面的基本运算步骤这个术语表达了运算时间。而一个步骤所耗费的时间主要依赖于特定的处理器甚至依赖于缓存策略等(这样在不同计算机上执行所得到的结果会稍有不同)。如果按照这种具体架构的方式来进行分析,我们的任务会非常复杂且难以让人接受。得出的结果也不能适用于多台计算机。因此,找到一个通用的,独立于机器特性的描述算法效率的方法就变得非常有意义。最终,我们通过计算基本运算步骤的次数做为运行时间的依据,即是问题规模的某个函数。
这种简化导致另一种结果,而不是报告一个算法需要花费多少时间。如输入参数为5n^3+4n+3个步骤,可以舍弃低阶项如4n和3(它们对n的增长毫无意义),甚至高阶项的系数5也可以舍弃,也就是说一个算法花费的时间为O(n^3)(称为“big oh of n^3)(译者注:oh表示字母O)。
是时候给这个符号下个精确的定义了。下文中,如果把f(n)和g(n)做为两个算法在问题规模n下的运行时间。
f(n)和g(n)是从正整数到正实数的函数。当存在一个正常数使得f(n)≤c·g(n),我们说f=O(g)(这意味着f的增长不比g快)。
f=O(g)是非常松散的类似于“f≤g”。它跟平常使用的符号≤并不相同,这是因为常数c的存在,例如10n=O(n)。可以忽略这个常数值。例如,假设我们为一个特定的计算任务选定了两个算法。一个使用f1(n)=n^2步骤,而另一个使用f2(n)=2n+20个步骤(如图0.2)。哪个更好呢?好,这取决于n的值。当n<5时,f1更小;随后,f2明显胜出。在本例中,当n增长时,f2的增长率更小,因此它更好。
这种优越性用大O表示法:f2=O(f1)更容易体现出来,因为下式中对于所有n来说:
(译者注:把n的最小值1代入,可得到结果22,当n越大结果越小,所以得到<=22的结果)
从另一方面来说,f1≠O(f2),因为f1(n)/f2(n)=n^2/(2n+20)可以无限大,没有常数c也可以下这样的定义。
现在出现了另一个算法,需要使用f3(n)=n+1个步骤。它比f2好吗?这是肯定的,但它们的速度比是一个常数。为了从更高的高度看问题,当两个函数的不同之处只是乘以了一个常数,那么我们认为这两个常数相等。
使用大O表示法,我们说f2=O(f3):
当然,f3=O(f2),所以这一次,c=1。
O(.)仅仅表示了≤的类似体,我们也可以表示≥和=的类似体如下:
f=Ω(g) 表示 g=O(f)
f=θ(g)表示f=O(g)和f=Ω(g)
在上一个例子中,f2=θ(f3)并且f1=Ω(f3)。
大O表示法可以让我们站在更高的地方看问题。当面对3n^2+4n+5这样复杂的函数,我们可以简单地使用O(f(n))或f(n)来代替。此例中我们可以使用O(n^2),因为二次幂在整个多项式和中占主导地位。下面的一些常用的简化规则可以帮助我们忽略不受控制的项目。
l 常数系数可以忽略:14n^2简化为n^2;
l 当a>b,则n^a支配(dominate)n^b:n^2支配n;
l 任何指数幂都支配多项式:3^n支配n^5(它也支配2^n)
l 任何多项式都支配对数:n支配(log(n))^3,这意味着n^2支配nlogn。
不要误解这种对于常数的骑士态度(译者注:意为傲慢的态度)。程序员和算法开发者对常数很感兴趣,并很愿意为了系数2整夜不眠来设计一个算法以使程序运行得更快。但如果站在本书的高度理解算法,不可能不涉及简单的大O表示法。