再谈复杂度分析

复杂度分析

主定理

函数 \(f(n)\) 表示数据规模为 \(n\) 时的计算量。

渐进符号的定义:

  • 渐进符号表示了函数增长趋势的重要部分,而忽略增长较慢的部分以及各项系数。大白话就是表示了函数的规模。

  • \(f(n)=\Theta(g(n))\),表示存在 \(c_1,c_2,n_0>0\) 使得对于所有 \(n\ge n_0\),有 \(c_1\times g(n)\le f(n)\le c_2\times g(n)\)。

  • \(f(n)=O(g(n))\),表示 \(0\le f(n)\le c_1\times g(n)\),此处应有 \(c_1,n_0\) 但省略,下同。

  • \(f(n)=\Omega(g(n))\),表示 \(0\le c_1\times g(n)\le f(n)\)。

  • 还有 \(o\) 和 \(\omega\) 分别把上面的等号去掉,用于对无穷的分析,不展开。

记忆:\(\Theta\) 看做等于,\(O\) 看做小于等于,\(\Omega\) 看做大于等于。

实际书写的时候常常只用 \(O\),因为我们关心程序的最坏运行效率。

我们认为渐进符号相当于一个集合,\(f(n)=O(g(n))\) 是说 \(f(n)\in O(g(n))\)。那么渐进符号的运算就是集合中选数运算后的结果构成的集合,其也能用渐进符号表示,那么有如下性质:

  • \(O(f_1(n))+O(f_2(n))=O(\max(f_1(n),f_2(n)))\)。
  • \(O(f_1(n))\times O(f_2(n))=O(f_1(n)\times f_2(n))\)。
  • \(\log _an=O(\log _2n)\),因此我们一般不写底数。

需要指出的是两个相等的渐进符号做减法或除法并不等于 \(0\) 或 \(1\)。

主定理的表达式

对于递推式

\[T(n)=a\times T(\frac nb)+f(n) \]

那么

\[T(n) = \begin{cases} \Theta(n^{\log_b a}) & f(n) = O(n^{\log_b(a)-\epsilon}), \epsilon > 0 \\ \Theta(f(n)) & f(n) = \Omega(n^{\log_b(a)+\epsilon}), \epsilon > 0 \\ \Theta(n^{\log_b a} \log^{k+1} n) & f(n) = \Theta(n^{\log_b a} \log^k n), k \ge 0 \end{cases} \]

这里有一种更易于理解的形式,若 \(f(n)=O(n^d \log^k n)\),则:

\[T(n) = \begin{cases} O(n^d) & d > \log_b a \\ O(n^d \log^{\textcolor{red}{k+1}} n) & d = \log_b a \\ O(n^{\log_b a}) & d < \log_b a \end{cases} \]

例子

  • \(T(n)=2T(n/2)+O(n^2)\)。
    解:\(d=2,\log _ba=1\),属于情况一,得 \(T(n)=O(n^2)\)。
  • \(T(n)=2T(n/2)+O(n \log n)\)。
    解:\(d=1,\log _ba=1\),属于情况二,得 \(T(n)=O(n\log ^2n)\)。
  • \(T(n)=2T(n/2)+O(\sqrt n)\)。
    解:\(d=0.5,\log _ba=1\),属于情况三,得 \(T(n)=O(n)\)。
  • \(T(n)=2T(\sqrt n)+O(\log n)\)。
    解:设 \(2^m=n\),原式可以写成 \(T(2^m)=2T(2^{m/2})+O(m)\),设 \(S(m)=T(2^m)\) 则 \(S(m)=2S(m/2)+O(m)\)。
    利用主定理得 \(S(m)=O(m\log m)\),代入 \(m=\log n\) 得 \(T(n)=O(\log n\log\log n)\)。

势能分析

定义状态 \(S\) 为数据结构的状态,其包含数据结构所有信息。

定义数据结构的势能 \(\Phi(S)\),要满足对于初态 \(S_0\) 以及任意 \(S\) 满足 \(\Phi(S)\ge \Phi(S_0)\)。

定义 \(c_i\) 表示第 \(i\) 次操作的实际计算量,\(S_i\) 表示第 \(i\) 次操作后数据结构的状态,\(p_i\) 表示第 \(i\) 次操作的均摊成本。其中

\[p_i=c_i+\Phi(S_i)-\Phi(S_{i-1}) \]

那么 \(m\) 次操作的总实际成本为

\[\sum _{i=1}^{m} c_i=\sum _{i=1}^{m}p_i-\Phi(S_i)+\Phi(S_{i-1})=\Phi(S_0)-\Phi(S_m)+ \sum _{i=1}^m p_i \]

因为 \(\Phi(S_m)\ge \Phi(S_0)\),所以得到总实际成本小于等于总均摊成本。

因此总均摊成本的上界就是总实际成本的一个上界。

这意味着,我们可以给数据结构定义一个满足条件的合适的势能,如果计算出总均摊成本等于 \(O(T(n))\),那么就证明了总实际成本等于 \(O(T(n))\)。

例子:vector 扩容的分析

我们知道 vector 的扩容规则是,记录一个当前长度 \(n\) 和当前容量 \(m\),初始 \(m=1\)。插入时若 \(n<m\) 那么直接插入这个元素;否则先扩容:\(m\gets m\times 2\),再插入元素。

我们定义势能为 \(2n-m\),则初态势能为 \(-2\),而显然任意态势能都 \(\ge -2\),符合定义。

  • 操作一,直接插入,实际成本为 \(1\),则势能显然增加 \(1\),于是此时 \(p_i=2\)。
  • 操作二,扩容并插入,实际成本为 \(m+1\),而势能增加了 \((2(n+1)-2m)-(2n-m)=2-m\),于是此时 \(p_i=3\)。

可见任意次操作,每次操作的均摊成本是 \(O(1)\),总实际成本就是与操作次数线性相关。

需要指出的问题

  • 为什么插入操作成本要看做 \(1\),扩容的成本要看做 \(m\)?

    答:这并不是说扩容的成本恰好是插入的 \(m\) 倍,因为他们本质不同所以实际的运算量不可比,但是规模可以比。这里的 \(1,m\) 指的操作的规模。插入操作与当前容量 \(m\) 无关于是成本规模为 \(1\),而扩容操作与 \(m\) 有关成本规模为 \(m\)。

  • 为什么实际成本与势能可以相互加减呢?

    答:实际成本的和是一个表示规模的量,而上文已经说过均摊成本是大于等于实际成本的,因此可以用均摊成本的规模表示实际规模。

  • 如果把实际成本多乘一个系数 \(k\) 会怎么样?

    答:多乘一个 \(k\),那么只需要换一个更合适的势能函数即可。

posted @ 2026-09-18 21:06  dengchengyu  阅读(11)  评论(0)    收藏  举报