导论的第一章和第二章为基础内容,我的《算法导论》的学习将从算法的复杂性理论分析开始。
渐进记号
本节介绍了一些常见的渐进记号,并介绍了它们的使用方法。
Θ 记号
对于一个给定的函数 g(n),用 Θ(g(n)) 来表示以下函数的集合:
Θ(g(n))={f(n)∣∃c1,c2>0,n0,s.t.∀n≥n0,0≤c1g(n)≤f(n)≤c2g(n)}
尽管 Θ(g(n)) 是一个集合,我们也可以使用 f(n)=Θ(g(n)) 来表示一个满足条件的 f(n)。f(n) 和 g(n) 的关系如下图所示:
换句话说,对所有的 n≥n0,f(n) 在一个常量因子等于 g(n),我们称 g(n) 是 f(n) 的一个渐进紧确界。既然如此,我们完全可以抛弃这个常量因子,只从高阶项来考虑渐进关系,也就是说,Θ 记号相当于扔掉低阶项并忽略最高项前的系数。例如:
21n2−3n=Θ(n2)
接下来我们形式化地用定义证明这个式子,首先先确定 c1,c2,n0 这三个常量,对 ∀n≥n0,有:
c1n2≤21n2−3n≤c2n2
也就是:
c1≤21−n3≤c2
只需要选择 c1=141,c2=21,n0=7 即可证明。对于其它的例子,有点类似于数学分析中极限的 ϵ−N 语言,这里不再赘述。
O 记号
前文的 Θ 记号相当于给出了函数在一个常量因子内的上界和下界,当只有一个渐进上界时,使用 O 记号,也就是说,O(g(n)) 是如下的集合:
O(g(n))={f(n)∣∃c>0,n0,s.t.∀n≥n0,0≤f(n)≤cg(n)}
我们称 g(n) 为 f(n) 的渐进上界。
由定义可知,Θ 记号是一个比 O 记号更强的概念,用集合论的写法,就是 Θ(g(n))⊆O(g(n))。同时,一个 f(n) 可以有很多个渐进上界,例如,我们很容易证明 f(n)=an+b=O(n),也很容易证明 f(n)=an+b=O(n2),我们在算法复杂度分析中更关注渐进上确界,也就是 O(n)。
O 记号太常用了,以至于我们在分析算法的时间复杂度和空间复杂度时,基本都使用 O 记号。由于其代表的时函数的渐进上确界,在多数情况下反映的是算法的最坏情况运行时间和运行空间。例如,插入排序算法的实现中有一个双重嵌套循环,对于特定规模 n 的输入,它的运行时间可能并不能到达 O(n2) 的量级,但我们说其时间复杂度为 O(n2),意指对于任意规模 n 的输入,其运行时间的上确界都是 O(n2)。
Ω 记号
类似于 O 记号,Ω 记号表示的是算法的渐进下界,也就是如下的集合:
Ω(g(n))={f(n)∣∃c>0,n0,s.t.∀n≥n0,0≤cg(n)≤f(n)}
对比着看,Ω 记号可以用来表达算法最好情况下的运行时间和运行空间度,例如,插入排序的运行时间为 Ω(n)。
关于 Θ 记号、O 记号和 Ω 记号,显然有如下定理:
f(n)=Θ(g(n))⇔f(n)=O(g(n))且f(n)=Ω(g(n))
o 记号
和 O 记号对应,表示 O(g(n)) 的集合中不是渐进上确界的那一部分函数。用数学语言说,若 f(n)=o(g(n)),则有:
n→∞limg(n)f(n)=0
ω 记号
和 Ω 记号对应,表示 Ω(g(n)) 的集合中不是渐进下确界的那一部分函数。用数学语言说,若 f(n)=ω(g(n)),则有:
n→∞limg(n)f(n)=∞
等式和不等式中的渐进记号
渐进符号可以用于数学公式中,例如 2n2+3n+1=2n2+Θ(n),这是因为 3n+1=Θ(n),这就是虽然 Θ(g(n)) 是一个集合,但是我们仍然使用 f(n)=Θ(g(n)) 而很少使用 f(n)∈Θ(g(n))。
这种表示是有好处的,例如在归并排序中,每一次分治地将一个大的排序问题划分成两个小的排序问题,且两个子问题和原问题的算法是一模一样的。我们用 T(n) 来表示输入规模为 n 的算法的最坏情况运行时间,则根据归并排序的分治过程,我们有如下的递归式:
T(n)=2T(n/2)+Θ(n)
其中,2T(n/2) 表示两个子问题所需要的时间,Θ(n) 表示最后的Merge操作,即将两个升序数组合并成一个升序数组的过程,这一部分的时间和问题规模成正比。
渐进记号的性质
渐进记号可以看作两个函数之间的二元关系,它具有自反性、对称性以及传递性,以 Θ 记号为例:
f(n)=Θ(g(n)),g(n)f(n)f(n)=Θ(g(n))=Θ(h(n))⇒f(n)=Θ(h(n))=Θ(f(n))⇔g(n)=Θ(f(n))
但是对于 O 记号和 Ω 记号,对称性要体现在转置上:
f(n)=O(g(n))⇔g(n)=Ω(f(n))
练习
3.1-1
不妨设 h(n)=max(f(n),g(n)),这说明 h(n)≥f(n) 且 h(n)≥g(n),要证明 max(f(n),g(n))=Θ(f(n)+g(n)),根据定义,需要选取 c1,c2,n0,使得 ∀n>n0,有 c1(f(n)+g(n))≤h(n)≤c2(f(n)+g(n)),对于右边,由于 f(n),g(n) 均非负,故有 h(n)≤f(n)+g(n),直接取 c2=1 即可;对于右边,有 f(n)+g(n)≤h(n)+h(n)=2h(n),故直接取 c1=21 即可。
3.1-2
需要选取 c1,c2,n0,使得 c1nb≤(n+a)b≤c2nb,考虑如下极限:
n→∞limnb(n+a)b=n→∞lim(1+na)b=1
由极限的定义,取 ϵ=21,则∃n0,当 n>n0 时,有 ∣∣∣∣nb(n+a)b−1∣∣∣∣<21,即 21<nb(n+a)b<23,即 21nb≤(n+a)b≤23nb,直接取 c1=21,c2=23 即可。
3.1-3
显然,因为 O(n2) 表示的是算法运行时间的上界,表示“最多”是这么多,而不是“至少”是这么多。
3.1-4
2n+1=2⋅2n=2⋅O(2n)=O(2n),但是 22⋅2=16>4=22,故 22n=O(2n),事实上,22n=Ω(2n)。
3.1-5
这个证明是初等的。必要性显然,充分性只需对 Ω 记号取 c1,n1,O 记号取 c2,n2,最后的 Θ 记号的三个常数为 c1,c2,max(n1,n2) 即可。
3.1-6
这个命题也没什么好讲的,前文已经说明 O(g(n)) 和 Ω(g(n)) 在算法分析中的意义。
3.1-7
从两种角度来理解,一个是直观理解:o(g(n)) 为除了渐进上确界以外的渐进上界,ω(g(n)) 为除了渐进下确界以外的渐进下界,它们显然没有交集。另一个是从极限角度证明:f(n)=o(g(n)) 和 f(n)=ω(g(n)) 的极限定义如下:
n→∞limg(n)f(n)n→∞limg(n)f(n)=0=∞
这两个式子显然不能同时满足。
3.1-8
类似地定义二元函数,此处略。
3.2-1
这个是高中题了,做差即可。
3.2-2
左边取以 b 为底的对数,得到:
logb(alogbc)=logbc⋅logba
右边取以 b 为底的对数,得到:
logb(clogba)=logba⋅logbc
两边是完全一样的。
3.2-3
这里给出两种证明,一种是直接用放缩得到上下界:
log(n!)log(n!)=i=1∑nlogi≤i=1∑nlogn=nlogn=O(nlogn)=log1+log2+⋯+log⌊2n⌋+log(⌊2n⌋+1)+⋯+logn≥log(2n)2n=2nlog2n=21nlogn−2log2n=Ω(nlogn)
另一种是直接用斯特林公式:
log(n!)=21log(2πn)+nlogen+Θ(1)=21logn+nlogn+Θ(1)=Θ(nlogn)
证明第二个命题,直接用放缩配合极限:
2nn!≥2n2n2nn→∞lim(8n)2n=223nn2n=(8n)2n=∞
故
n→∞lim2nn!=∞
故 n!=ω(2n)。后半部分仍然考虑极限:
n→∞limnnn!=n→∞limk=1∏nnk
分半考虑,对于前半段,即 k=1,2,⋯,⌊2n⌋ 有:
nk≤21
对于后半段,即 k=⌊2n⌋+1,⋯,n 有:
0<nk≤1
故
k=1∏nnk≤21⌊2n⌋→0
于是极限
n→∞limnnn!=n→∞limk=1∏nnk=0
故 n!=o(nn)。