算法导论——函数的增长

上次提到的增长量级简单的说明了算法的效率,而且还使用了一个符号\(\Theta\)。这次给出这个符号更精确的定义。在数学上,运行时间随着数据量增长的规律可以看为一个函数,因此这篇文章主要从更全局泛化的谈谈函数的增长。

当输入规模足够大,使得只有运行时间的增长量级有关时,我们需要研究算法的渐近效率。这里的渐近效率指的是当输入规模无限增加时,在极限中,算法的运行时间如何随着输入规模的变大而增加。下面首先定义几种监禁记号。

渐近记号

一般来说,对于运行时间函数的定义域是\(\mathbb N = \{0,1,2,....\}\),也就是自然数。但是渐近记号也可以活用到实数域,或者限制到自然数的一个子集。对于上次插入排序的分析,我们知道它的运行时间可以被总结为\(an^2 +bn + c\),我们将它记为\(\Theta(n^2)\)。渐近记号除了用于表示运行时间,也可以用来分析运行空间,甚至是与算法无关的函数的分析。

虽然之前关注的情况是最坏情况的输入,但是分析时我们希望能用渐近记号来描述任何输入,下面介绍几种不同渐近记号的定义。

\(\Theta\)记号

对于一个给定函数\(g(n)\),那么\(\Theta(g(n))\)表示的是以下函数的集合: \[ \Theta(g(n)) = \{f(n):存在正常量c_1,c_2,n_0,使得对所有n\ge n_0,有0\leq c_1g(n)\leq f(n) \leq c_2 g(n)\} \] 这里可以看到,我们通过函数\(g(n)\)和两个常数\(c_1,c_2\),给了函数\(f(n)\)一个上界和一个下界。由于\(\Theta(n)\)是一个集合,因此更严格的写法是:\(f(n) \in \Theta(g(n))\),但是我们简单记成\(f(n) = \Theta(g(n))\),这样更简便。对于所有\(n \ge n_0\),函数\(f(n)\)在一个常量因子内等于\(g(n)\)。我们称\(g(n)\)\(f(n)\)的一个渐近紧确界

\(\Theta(g(n))\)的定义要求每个成员\(f(n)\in \Theta(g(n))\)都渐近非负,这意味着\(g(n)\)也需渐近非负。这个假设对其他的渐近记号也是一样成立。 ### \(O\)记号 ### \(\Theta\)记号渐近地给出一个函数的上界和下界,当只有一个渐近上界时,我们使用\(O\)记号。\(O(g(n))\)表示的是以下函数的集合: \[ O(g(n)) = \{f(n):存在正常量c,n_0,使得对所有n\ge n_0,有0 \leq f(n) \leq c g(n)\} \]

由于\(O\)记号的定义,下面这个式子也是正确的:\(n = O(n^2)\),因为\(O(n^2)\)给出的是上界,但是并不要求是多么确切的上界。

使用\(O\)记号的好处是,它用来表示最坏情况下的运行时间时,对任何输入都是正确的。比如之前的插入排序,我们说了,最坏情况下运行时间为\(\Theta(n^2)\),但这并不意味着对于所有输入都满足,因为在最好的情况下运行时间为\(\Theta(n)\),如果使用\(O\)来描述则可以拓展到任意输入,因为不管最坏的情况就是给了一个上界。 ### \(\Omega\)记号 ###

不用说,\(\Omega\)记号给出的就是渐近下届: \[ \Omega(g(n)) = \{f(n):存在正常量c,n_0,使得对所有n\ge n_0,有0\leq cg(n)\leq f(n) \} \]

\(\Omega\)记号就适用于最优情况,例如插入排序,我们可以指出它的运行时间为\(\Omega(n)\),但是说它最坏情况下运行时间为\(\Omega(n^2)\)也是正确的。显然\(\Omega\)记号的实际应用是最少的。

等式与不等式中的渐近记号

使用渐近记号是非常有用的。我们以后可能会使用类似下式的“奇怪”写法: \[ 2n^2 + 3n + 1 = 2n^2 + \Theta(n) \] 这样的写法是没有问题。我们可以使用渐近记号消除一个等式中无关紧要的细节。例如之前的归并排序,我们可以将最坏情况的运行时间写成递归式: \[ T(n) = 2T(\frac{n}{2})+ \Theta(n) \] (一个归并排序被分解成两个序列的归并排序,在加上一个\(\Theta(n)\)的合并消耗)

后者用渐近记号表示的函数成为匿名函数。一个表达式中匿名函数的数目可以理解为等于渐近记号出现的次数。比如在表达式\(\sum_{i=1}^nO(i)\)中,只有一个匿名函数\(O(i)\),而不同于下式: \[ O(1)+O(2)+...+O(n) \] 实际上后者并没有什么清晰的解释(对于前者我的理解是就像插入排序最坏的情况一样,只有一个匿名函数,最后运行时间是\(nO(n) = O(n^2)\))。

有时候渐近记号也出现在等式的左边: \[ 2n^2 + \Theta(n) = \Theta(n^2) \] 总之在等式链中渐近记号的使用是非常灵活的,而这也让我们能更关注于最需要关心的部分。 ### \(o\)记号 ### 之前说的\(O\)记号,表示的渐近上界,它有可能是一个紧确的,也可能不是,例如\(2n = O(n^2)\)就不是紧确的,而\(2n^2 = O(n^2)\)则是紧确的。因此这里再引入一个新的渐近记号\(o\),表示的是非渐近紧确上界。我们定义\(o(g(n))\)如下: \[ o(g(n)) = \{f(n): 对任意常量c>0,存在常量n_0>0,使得对于所有n\ge n_0,有0\leq f(n) < cg(n) \} \] 举个例子:\(2n = o(n^2)\)是正确的,但是\(2n^2 = o(n^2)\)就是错误的了。 直观上来说,对于渐近记号\(o\),当\(n\)趋于无穷时,函数\(f(n)\)相对于\(g(n)\)就微不足道了,即: \[ \lim_{n\rightarrow \infty} \frac{f(n)}{g(n)} = 0 \] 实际上我们在数学中经常看到用\(o\)来表示高阶无穷小,也符合上述的等式,但是在这里,我们依然限定函数是渐近非负的。

###\(\omega\)记号 ### \(\omega\)记号与\(\Omega\)记号的关系类似于\(o\)记号与\(O\)记号的关系。\(\omega\)表示一个非渐近紧确的下界: \[ \omega(g(n)) = \{f(n): 对任意常量c>0,存在常量n_0>0,使得对于所有n\ge n_0,有0\leq cg(n) < f(n) \} \]

举个例子:\(n^2 = \omega(n)\)是正确的,但是\(2n = \omega(n)\)是错误的。同样,直观上讲,对于渐近记号\(\omega\),当\(n\)趋于无穷时,函数\(g(n)\)相对于函数\(f(n)\)就微不足道了: \[ \lim_{n\rightarrow \infty} \frac{f(n)}{g(n)} = \infty \]

对于两个函数\(f,g\)的渐近比较和两个实数\(a,b\)的比较之间可以做一个类比: \[ f(n) = O(g(n)) \rightarrow a\leq b\\ f(n) = o(g(n)) \rightarrow a < b\\ f(n) = \Omega(g(n)) \rightarrow a\ge b\\ f(n) = \omega(g(n)) \rightarrow a > b\\ f(n) = \Theta(g(n)) \rightarrow a = b \]

但是需要注意的是,任意两个实数\(a,b\)一定满足于\(a>b,a < b,a =b\)三种情况的一种,但是不是任意的函数之间都可以做渐近比较,例如我们不能渐近比较\(n^2\)\(n^{1+\sin n}\)

其他

除了渐近记号,这一章还介绍了一些其他的概念,单调性,向上向下取整,模运算等等。对于这些基本的知识这篇文章就不多提了,仅写一些平时不容易被注意到知识点。

阶乘

实际上阶乘是一个很简单的概念,定义如下: \[ n!=n\cdot(n-1)\cdot...\cdot 1 \]

我在这里要介绍的是阶乘有一个斯特林近似公式: \[ n!=\sqrt{2\pi n}\left(\frac{n}{e}\right)^n\left(1 + \Theta(\frac{1}{n})\right) \]

它给出了一个更紧致的上界和下届。实际上我们很容易得到: \[ n!=o(n^n)\\ n!=\omega(2^n)\\ \] 而通过斯特林公式可以进一步证明: \[ \text{lg}(n!) = \Theta(n\text{lg}n) \] 对于所有\(n\ge 1\),也有: \[ n!=\sqrt{2\pi n}\left(\frac{n}{e}\right)^ne^{a_n} \] 其中\(\frac{1}{12n+1}< a_n < \frac{1}{12n}\)

多重函数

我们使用记号\(f^{(i)}(n)\)来表示函数\(f(n)\)多次作用于一个初值\(n\)上:对于非负数\(i\): \[ f^{(i)}(n) = \{ \begin{array} n & i=0\\ f(f^{(i-1)}(n)) & i>0 \end{array} \] 例如\(f(n) = 2n\),则\(f^{(i)}(n) = 2^in\)。 ### 多重对数函数 ###

我们使用\(\text{lg}^*\)来表示多重对数。假设\(\lg^{(i)}\)定义如上,那么多重对数函数定义为: \[ \text{lg}^* = \min\{i\ge 0:\text{lg}^{(i)}n\leq 1\} \] 多重对数函数增长非常缓慢: \[ \text{lg}^*2 = 1\\ \text{lg}^*4 = 2\\ \text{lg}^*16 = 3\\ \text{lg}^*65536=4\\ \text{lg}^*2^{65536}=5 \] 实际中,我们很难遇到\(n>5\)的规模。

斐波那契数

我们一直知道斐波那契数列是以指数形式增长的,这里从另一个更新奇的角度说明这个问题。 \[ F_0=0\\ F_1 = 1\\ F_i = F_{i-1} + F_{i-2},i\ge 2 \]

实际上斐波那契数列与黄金分割率\(\phi\)以及其共轭数\(\hat\phi\)有关,他们是下面方程的两个根: \[ x^2 = x+1 \] 并且: \[ \phi = \frac{1+\sqrt 5}{2},\hat\phi = \frac{1-\sqrt 5}{2} \] 我们有: \[ F_i = \frac{\phi^i - \hat\phi^i}{\sqrt 5} \] 由于后一项随着\(i\)增大可忽略不计,因此从上式可以看出,斐波那契数是指数形式增长的。有趣的是当\(n\)增大时,前一项与后一项之比越来越趋于黄金分割。