《算导》整理·渐进记号

· · 个人记录

这篇文章是对五种渐进记号(Θ、O、Ω、o、ω)的整理。加深一下自己的理解。

先说一下渐进记号是干什么的吧。

渐进记号通常是用来描述一个函数的增长趋势的,或许也正是因此,《算法导论》中介绍渐进记号的章节的名字是“函数的增长”。也正是因此,渐进记号可以用来刻画算法的时间复杂度与空间复杂度。

OIer可能更熟悉的是,用“O”记号刻画算法的复杂度。这看起来没有什么问题,毕竟大家都用得好好的,至少“看起来”是这样。不过也仅仅是“看起来”而已。很多时候,O记号并不能很准确地描述算法的时空复杂度,甚至有的时候(比如用于描述下界)是荒唐的(我们后面会看到)。

另外,滥用O记号可能会使文章的逻辑混乱,尤其是在推导复杂度的时候。

因此,学会合理运用渐进记号是有必要的,至少可以用来装逼,不是么?

当然,这里要说明一下:对于大多数OIer来说,O记号已经足够用了(纵然可能使用不当)。渐进记号的使用几乎并不能影响您的OI成绩,所以这篇文章的受众(如果存在的话)应该是以这篇文章扩充知识面或是打发时间为目的。

现在,我们正式开始正文部分:

对了,忘了说了:我们接下来涉及到的函数,如果没有特殊说明,默认是非负的。

1.Θ记号及其使用

先上定义: > 对于一个给定的函数$g(n)$,用$Θ(g(n))$来表示一下函数的集合: > $Θ(g(n))$={ $f(n)$: 存在正常量$c_1$、$c_2$和$n_0$,使得对于所有$n>=n_0$,有$0 <= c_1g(n) <= f(n) <= c_2g(n)$ } 啧啧啧……看懂了吗? 看懂了当然最好,没看懂也无妨,我后续还会做一些说明帮助各位理解。 首先,Θ记号表示的是一个函数的**集合**,也就是满足条件的一类函数。 这个条件是什么样的呢? 感性地理解一下,就是**n足够大的时候,函数$f(n)$应该被夹在$c_1g(n)$和$c_2g(n)$之间**: ![符合条件的f(n)](https://upload-images.jianshu.io/upload_images/3403753-c036d40025f1ac4d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/408) 这样的$f(n)$就是符合条件的。因为$Θ(g(n))$是集合,$f(n)$是集合中的一个元素,所以我们本应该用$f(n)∈Θ(g(n))$来描述两者之间的关系。然而这样并不是很方便,所以我们通常**略作活用**,用$f(n)=Θ(g(n))$来表示这个关系。 这样做并不是没有理由。它可以给我们带来方便~~,虽然在这里不太看得出来~~。 到这里,你应该差不多明白$Θ$记号的含义了。对于满足$f(n)=Θ(g(n))$的$f(n)$,我们称$g(n)$是$f(n)$的**渐进紧确界**,因为它同时用上界和下界“束缚”住了$f(n)$(后面会看到只有上界和只有下界的情况,那就不是渐进紧确的了)。 我们再举几个栗子。 有如下几个函数: $g(n)=n^2 f_1(n)=n^2 f_2(n)=100n^2+999n+100000 f_3(n)=0.001n^3

首先,很容易看出,f_1(n)=Θ(g(n))。只要任取0<c_1<=1c_2>=1,就可以满足Θ记号的条件。

第二个可能具有一点点的迷惑性。然而大多数OIer都应该看得出来——f_2(n)=Θ(g(n)),当n足够大的时候,后面的低阶项是可以被忽略的。

而第三个,f_3(n)≠Θ(g(n)),当n足够大的时候,前面的系数起到的作用也不大。实际上,f_3(n)=Θ(n^3)

上述三个栗子对于初步分析过算法复杂度的OIer应该都不会有任何难度。从直觉上来看,我们需要做的是:扔掉低阶项、忽略最高阶项的系数。而这样做也的确是对的。

2.O记号及其使用

终于到了大家都熟悉的O记号了。这个记号我们一般读作“O”,或者读作“大O”(因为后面还有小o,加以区分)。

Θ记号不同的是,O记号仅仅对上界进行了描述。它的定义如下:

对于一个给定的函数g(n),用O(g(n))来表示一下函数的集合:

Θ记号是不是非常相似?Θ记号渐进地给出了函数的上界和下界,而O记号仅仅给出了函数的上界(突然想起那个典故:下面没有了也(逃))。

上图片:

有了Θ记号的基础,想必很容易就可以理解O记号——比Θ记号少了一个限定条件嘛。

但是!!

这一个限定条件其实很重要。记得在本文开始的时候我说过什么么:

很多时候,O记号并不能很准确地描述算法的时空复杂度,甚至有的时候(比如用于描述下界)是荒唐的(我们后面会看到)。

看下面这个例子:

g(n) = n^3 f(n) = n

那么这个命题:f(n) = O(g(n))是正确的么?你的第一反应可能是:当然不对,f(n) = O(n)才对!但其实,f(n) = O(n)是对的,而f(n) = O(n^3)也是对的!

这就是O记号的局限性——它只描述了上界,而下界是任意的。因此,O记号给出的界限往往不一定是准确的。

也正是因此,有时用O记号描述下界会很荒唐。我们来看一道《算导》中的习题:

练习3.1-3 解释为什么“算法A的运行时间至少是O(n^2)”这一表述是无意义的。

仔细想一想就可以明白:O(n^2)只给出了一个上界,而没有给出下界。给它冠以“至少”的限定是无意义的。O记号通常用来描述上界,如算法最坏情况时的复杂度。后面我们会给出一个可以描述下界的渐进记号。

3.Ω记号及其使用

想必很多人已经猜到了$Ω$记号的定义: > 对于一个给定的函数$g(n)$,用$Ω(g(n))$来表示一下函数的集合: > $Ω(g(n))$={ $f(n)$: 存在正常量$c$和$n_0$,使得对于所有$n>=n_0$,有$0 <= cg(n) <= f(n)$ } 再放一张图: ![](https://upload-images.jianshu.io/upload_images/3403753-1af27838c200e645.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/321) $Ω$记号仅仅给出的渐进下界。与$O$记号类似的是,我们不能说“算法A的运行时间至多是$Ω(n^2)$”,这是无意义的。 有了前两个记号的讲述,$Ω$记号很容易理解,这里就不赘述了(才发现我写了好长。。) *** 是不是觉得我要开始讲第四个了?错! 接下来要说的是一个定理: > 对于任意的两个函数$f(n)$和$g(n)$,我们有$f(n) = Θ(g(n))$,当且仅当$f(n) = O(g(n))$且$f(n) = Ω(g(n))

这个定理很易于理解:f(n) = O(g(n))给出了渐进上界,f(n) = Ω(g(n))给出了渐进下界。上界和下界“拼在一起”,就成了渐进紧确界,得到f(n) = Θ(g(n))。通常,在f(n) = Θ(g(n))难以直接证明的时候,会分别证明其上界和下界,最后用此定理证明紧确界。

可以通过渐进记号的定义进行严谨的证明,不多说了。

好,我们继续。

4.o记号及其使用

$O$记号给出的渐进上界可能是也可能不是渐进紧确的;$o$记号给出的渐进上界一定不是渐进紧确的。 我们把$o$记号和$O$记号的定义放在一起比对一下: > $O(g(n))$={ $f(n)$: 存在正常量$c$ 和 $n_0$,使得对于所有$n>=n_0$,有$0 <= f(n) <= cg(n)$ } > $o(g(n))$={ $f(n)$: _**对任意**_ 正常量$c$,存在 $n_0$,使得对于所有$n>=n_0$,有$0 <= f(n) < cg(n)$ } 这样就可以看出来了:$o$记号的范围更小,$o(g(n))$是$O(g(n))$的真子集(请原谅我打不出“含于”符号……)。比如,$n^2 = O(n^2)$,但$n^2 ≠ o(n^2)$。$o$记号给出的是一个很宽松的上界。 这样看来,在$n$非常大,甚至趋于无穷的时候,$f(n)$相对于$g(n)$来说就微不足道了。因此,根据《算导》所述,有些学者把下面这个极限作为$o$记号的定义: $${\lim_{n \to \infty} {\frac{f(n)} {g(n)} } } = 0$$ ## 5.ω记号及其使用 $ω$记号和$Ω$记号很像,它们之间的区别,和$o$记号与$O$记号的区别差不多: $Ω$记号给出的渐进下界可能是也可能不是渐进紧确的;$ω$记号给出的渐进下界一定不是渐进紧确的。 我们把两者的定义对比一下: > $Ω(g(n))$={ $f(n)$: 存在正常量$c$和$n_0$,使得对于所有$n>=n_0$,有$0 <= cg(n) <= f(n)$ } > $ω(g(n))$={ $f(n)$: _**对任意**_ 正常量$c$,存在 $n_0$,使得对于所有$n>=n_0$,有$0 <= cg(n) < f(n)$ } 有了$o$记号的讲述,这里就不再多说了。 同样,$n$非常大,甚至趋于无穷的时候,$g(n)$相对于$f(n)$也变得微不足道了,因此$ω$记号也有另一种定义: $${ \lim_{n \to \infty} { \frac{f(n)} {g(n)} } } = \infty$$ 除此之外,$ω$记号和$o$记号还有这样一种关系: > $f(n) ∈ ω(g(n))$当且仅当$g(n) ∈ o(f(n))

这也是很易于理解的。

哇哈哈哈!是不是没想到还有?

6.渐进记号的性质

以下性质由定义易得,不证明。

1.传递性。

f(n) = Θ(g(n))g(n) = Θ(h(n)),则有f(n) = Θ(h(n))

该性质对其它渐进记号也成立。

2.自反性。

f(n) = Θ(f(n))

该性质对OΩ成立,但是对oω不成立。

3.对称性。

f(n) = Θ(g(n))$当且仅当$g(n) = Θ(f(n)) f(n) = O(g(n))$当且仅当$g(n) = Ω(f(n)) f(n) = o(g(n))$当且仅当$g(n) = ω(f(n))

emmm……该说的差不多就都说完了,让我们留一点课后作业吧(手动滑稽)

我们都知道,n!n的阶乘的意思。

试证明:

n! = o(n^n) n! = ω(2^n)

再根据斯特林近似公式:

n! = \sqrt{2 \pi n}(\frac{n}{e})^n(1 + Θ(\frac 1 n))

证明(虽说不用那个公式,用积分近似的方式也可以证明):

lg(n!) = Θ(nlgn)

注意!据《算导》所述,计算机科学家发现2是对数最自然的底,故在此定义:

lgn = log_{2}{n}

这也解决了我长久以来的疑惑,为什么很多算法明明是二分却要写nlgn……虽说在数学里lgn = log_{10}{n}……

这篇文章到此为止就真的结束了……应该是不会有人看的,所以这篇文章算是写给自己的吧,作为一个温习与加深理解的方式。

现学{L_aT^ex},好累……

内容及习题整理自《算法导论》,强烈推荐,她是我现任女朋友。

图片出自Moonsmile的简书文章:算法基础+分治策略(算法复习第1弹),他写的很好,清楚简练。