本讲义翻译自Berkleyand U.C.San Diego University的Dasgupta, C. H. Papadimitriou, 以及U. V. Vazirani 的课程,并且还在翻译制作过程中,本人水平有限,斗胆翻译名家专著,只为兴趣使然,有不对的地方敬请谅解.....
序言
现代的计算机和网络世界构建在两个主要的技术基础之上:
- 微电子学和芯片设计
- 高效的算法
1 算法的来历
公元前600年印度发明的十进制系统带来了定量推理领域的革新,很多复杂的数学计算都可以通过基本的演算步骤纪录下来。到了九世纪,一位生活在巴格达的学者Al Khwarizmi提出了加、减、乘、除甚至求平方根和计算圆周率的基本计算方法,这些方法准确、无二意、形式化、高效简短,后来被人们称为算法(Algorithm)。十进制系统加速了科学技术的发展,在西方文明中扮演重要的角色。
2 Fibonacci数列
实际上,如果不是15世纪著名的意大利数学家Leonardo Fibonacci极力的研究和宣传算法系统,Al Khwarizmi的工作很可能得不到西方社会的承认。不过今天,Fibonacci却以他命名的数字序列而闻名:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, …
其中每项都是前两项的和。用以下公式表示F(n):
F(n) = F(n-1) + F(n-2) if n>1
F(n) = 1
if n=1
F(n) = 0
if n=0
实际上,Fibonacci数列几乎以2次幂的方式快速增长。因此,我们需要确定的算法来计算F(n).
-
指数级算法
一个最直观的方法就是递归的实现F(n),伪代码如下:
function F(n)
if n = 0: return 0
if n = 1: return 1
return F(n-1) + F(n-2)
设计一个算法时都应该考虑以下
三个基本问题:
-
正确吗?
-
消耗了的时间同n的关系是什么?
-
能否继续改进?
对Fibonacci数列来说,问题1显然是肯定的;对于问题2,设T(n)为F(n)的计算步骤数,我们可以得到T(n)的计算公式:
T(n) = T(n-1) + T(n-2) + 3 if n>1
T(n) <= 2 if n<=1
显然,T(n)>=F(n)。这是一个坏消息!因为这意味着T(n)也将是指数幂次。例如,计算F(200)时,计算步骤T(200)>=F(200)>=2^138。因此,问题3,我们能否改进这个算法呢?
-
多项式算法
更sensible的方法是:存储中间结果——F(0), F(1), F(2)…F(n-1):
function F(n)
if n = 0 return 0
create an array f[0…n]
f[0] = 0, f[1] = 1
for i = 2 … n:
f[i] = f[i-1] + f[i-2]
return f[n]
F(n)
的计算步骤将是n的线性函数——非常大的突破!因此,我们以后将会看到,正确的算法将使一切都不同!
3. Big-O notation
将运行时间表示成基本的计算机指令数虽然简单直接,但是却依赖于特定的计算机结构,不具有普遍性。因此寻找一种更简洁、独立于计算机特性的算法表示显得更为合理,本书中我们将运行时间表示成输入大小(n)的函数。
在简洁化过程中,我们同时将复杂的算法表示中的低序(lower-order)项移除,例如5n^3+4n+3中,4n和3两项,甚至系数5都可以忽略,这样该算法复杂度可以表示成O(n^3)。
接下来我们将准确定义这个概念,假定f(n)和g(n)分别表示输入大小为n的两种算法的复杂度表示:
假定f(n)和g(n)是正整数或正实数函数,如果存在常数c>0,使得f(n) <= g(n),我们就说f = O(g) (意味着f增长速率大于g)。
换句话说f = O(g) 相当于“f <= g”,同样我们可以定义“>=”和“=”:
f = Ω(g) 当且尽当 g = O(f)
f = Ө(g) 当且尽当 f = O(g) 并且 f = Ω(g)
大O符号让我们可以侧重在大局上。当处理比较复杂的表达式,例如3n^2+4n+5时,可以将其替代为O(f(n)),并且f(n)尽可能简单,所以这里f(n)=n^2,因为二次幂在整个多项式和中占主导。下面是一些常用的简化规则:
-
常数系数可以忽略:14n^2简化为n^2;
-
当a>b,则n^a支配(dominate)n^b:n^2支配n;
-
任何指数幂都支配多项式:3^n支配n^5;
-
任何多项式都支配对数:n支配(log(n))^3