想象你是一家餐厅的主厨,每次接到一份新菜谱,都要先在心里盘算:这道菜要备多少料、花多少时间、占几个灶台。算法复杂度分析就是程序员的“备菜预算单”——它不看代码能跑多快,而是看当数据量 n 越来越大时,运行时间和内存的“增长趋势”。大 O 表示法像在菜谱上写“最多不超过两小时”;主定理则像老师傅的经验表,看一眼“这菜分几份、每份再递归做多少、最后合并要多久”,就能直接报出总工时。这篇文章就用这套“后厨管理”的思路,把渐进分析、递归方程、主定理、均摊分析和势能法串成一套可落地的复杂度工具箱。

岗位关联

对 AI 从业者而言,掌握复杂度分析能让你在选模型、设计训练流水线、优化推理服务时,像经验丰富的架构师一样预判资源消耗,避免上线后被数据量“反噬”。

目录
  1. 可视化演示
  2. 渐进分析三记号
  3. 大O表示法
  4. 递归方程求解
  5. 主定理全解
  6. 主定理条件
  7. 均摊分析
  8. 势能法
  9. 复杂度增长与工程
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

通过增长速率对比、主定理递归树和均摊分析三个模式,展示大O表示法的渐进分析与复杂度求解方法。

本节配有交互动效(进入视口后自动加载,约 3KB)。

增长速率:常见复杂度函数对比 常数对数线性等低复杂度函数随n增长缓慢适合大规模输入 指数阶复杂度随n爆炸增长仅适用于小规模问题需优化算法 O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ) 理解增长速率差异是算法选型的关键依据

渐进分析三记号

厨房里,大家讨论一道菜的时间往往用三种说法:有人问“最久要多久”,你会说“最多不超过两小时”,这是上界;有人问“再快能多快”,你会说“起码半小时”,这是下界;还有人想知道“通常稳定在多少”,你会说“大约四十五分钟,上下浮动十分钟”,这是紧密界。渐进分析就是把“数据规模 n 越来越大”这个条件代入,看运行时间函数被什么样的标准曲线框住。

在数学上,我们用函数集合来描述这种直觉。设 \(f(n)\) 是算法实际运行时间,\(g(n)\) 是我们拿来比较的标准函数:

\[

\begin{aligned}

O(g(n)) &= \{ f(n): \exists c>0, n_0>0,\ 0\le f(n)\le c\,g(n),\ \forall n\ge n_0 \},\\

\Omega(g(n)) &= \{ f(n): \exists c>0, n_0>0,\ 0\le c\,g(n)\le f(n),\ \forall n\ge n_0 \},\\

\Theta(g(n)) &= \{ f(n): \exists c_1,c_2>0,\ n_0>0,\ c_1 g(n)\le f(n)\le c_2 g(n),\ \forall n\ge n_0 \}.

\end{aligned}

\]

1. O 上界条件:存在一个常数 \(c\),让 \(g(n)\) 放大若干倍后,从某个规模 \(n_0\) 开始一直压住 \(f(n)\)。

0≤f(n)≤c g(n),∀n≥n00\le f(n)\le c\,g(n), \quad \forall n\ge n_0

2. Ω 下界条件:反过来,放大后的 \(g(n)\) 能被 \(f(n)\) 压住,说明算法至少需要这个量级。

0≤c g(n)≤f(n),∀n≥n00\le c\,g(n)\le f(n), \quad \forall n\ge n_0

3. Θ 夹逼条件:同一次比较中,上下各一个常数倍数,把 \(f(n)\) 死死夹在 \(g(n)\) 附近。

c1g(n)≤f(n)≤c2g(n)c_1 g(n)\le f(n)\le c_2 g(n)

4. n₀ 的含义:我们只关心“足够大的数据量”,小规模的偶然抖动不算数。

n≥n0⇒进入稳定增长区n\ge n_0 \Rightarrow \text{进入稳定增长区}

厨师不会因为某桌客人只点一碗白饭就重估整本菜谱,他们关心的是晚市高峰期的大批量订单;渐进分析同理,关注大 n 下的趋势,而非小样本的偶然表现。

大O表示法

主厨做周末宴席时,会假设最忙的晚上八点:每桌都点招牌菜、每个灶台都在烧、每口锅都在炒。大 O 表示法也是这样,习惯性地看“最坏情况”。它回答的问题是:当输入规模 n 大到失控时,时间最多涨成什么样?

举例来说,某个算法的运行时间是 \(f(n)=3n^2+2n+1\)。我们想证明它属于 \(O(n^2)\),只需要找到一个常数 \(c\) 和一个起始点 \(n_0\),让放大后的 \(n^2\) 能压住所有项:

\[

3n^2+2n+1 \le 3n^2+2n^2+n^2 = 6n^2, \quad n\ge 1

\]

1. 放大每一项:把 \(2n\) 和 \(1\) 分别放大成 \(2n^2\) 和 \(n^2\),让它们都能被 \(n^2\) 统一衡量。

3n2+2n+1≤3n2+2n2+n23n^2+2n+1 \le 3n^2+2n^2+n^2

2. 合并同类项:右边合并成 \(6n^2\),于是找到常数 \(c=6\)。

3n2+2n2+n2=6n23n^2+2n^2+n^2 = 6n^2

3. 找到 n₀:因为放大操作依赖 \(n\ge 1\),所以取 \(n_0=1\)。

∀n≥1, 3n2+2n+1≤6n2\forall n\ge 1,\ 3n^2+2n+1 \le 6n^2

4. 下结论:这就是 \(f(n)=O(n^2)\),说明当 n 增大时,时间会被一个二次函数“兜住”。

f(n)=O(n2)f(n)=O(n^2)

这里的 \(c\)、\(n_0\) 就像“可以多开两个备菜窗口”的余量。真实运行中常数可能很大,但只要增长趋势是二次的,数据量翻倍时耗时大约会变四倍——这才是大 O 想传达的警告。

递归方程求解

有些菜谱需要反复套娃:炖一锅高汤,先把食材对半切开,分别小火炖,最后再把两锅混合调味。每次切割是 \(O(1)\),合并是 \(O(n)\),子汤的规模变成原来一半。这种“自己调用自己”的时间开销,写成递归方程就是 \(T(n)=2T(n/2)+n\)。

迭代展开法非常直观:把 \(T(n/2)\) 再拆开,一层层往外拉,直到拆到边界条件 \(T(1)=1\)。

\[

T(n)=2T\left(\frac n2\right)+n

=2\left(2T\left(\frac n4\right)+\frac n2\right)+n

=4T\left(\frac n4\right)+2n

\]

继续展开 \(k\) 层后,子问题规模变成 \(n/2^k\),每一层的额外成本都是 \(n\),所以:

\[

T(n)=2^kT\left(\frac{n}{2^k}\right)+kn \xrightarrow{k=\log_2 n} n\log_2 n + n = \Theta(n\log n)

\]

1. 展开第一层:代入 \(T(n/2)\),拆成两个子问题加一次合并成本。

T(n)=2T(n/2)+n=2(2T(n/4)+n/2)+nT(n)=2T(n/2)+n=2\left(2T(n/4)+n/2\right)+n

2. 观察层数成本:每展开一层,子问题数量翻倍,但每个子问题规模减半,每层合并成本总和仍是 \(n\)。

4T(n/4)+2n=8T(n/8)+3n4T(n/4)+2n = 8T(n/8)+3n

3. 推广到 k 层:写通式,\(2^k\) 个子问题,每个规模 \(n/2^k\),累计了 \(k\) 个 \(n\)。

T(n)=2kT(n/2k)+knT(n)=2^kT(n/2^k)+kn

4. 代入终止条件:当 \(n/2^k=1\) 时 \(k=\log_2 n\),此时子问题数为 \(n\),边界总成本为 \(n\)。

T(n)=nT(1)+nlog⁡2n=n+nlog⁡2nT(n)=nT(1)+n\log_2 n = n + n\log_2 n

5. 决定量级:\(\log_2 n\) 项增长快于常数项,所以最终是 \(\Theta(n\log n)\)。

nlog⁡2n+n=Θ(nlog⁡n)n\log_2 n + n = \Theta(n\log n)

递归树方法就是把上面的展开过程画成一棵树,每层节点数量乘以每个节点的成本求和。很多分治算法,比如归并排序、快速排序的期望情况,最后都会落到这张“分层成本表”上。

主定理全解

老师傅看递归方程,不需要每次一层层展开,而是直接用一套“行情表”快速估价。主定理就是这张表,它针对形如 \(T(n)=aT(n/b)+f(n)\) 的递归式:把规模为 n 的问题拆成 a 个子问题,每个规模为 \(n/b\),再花 \(f(n)\) 做合并/拆解。关键看“子问题的总工作量 \(n^{\log_b a}\)”和“合并成本 \(f(n)\)”谁在增长中占主导:

\[

T(n)=

\begin{cases}

\Theta(n^{\log_b a}), & f(n)=O(n^{\log_b a-\varepsilon}),\ \varepsilon>0\\

\Theta(n^{\log_b a}\log n), & f(n)=\Theta(n^{\log_b a})\\

\Theta(f(n)), & f(n)=\Omega(n^{\log_b a+\varepsilon}),\ \varepsilon>0\text{ 且 } af(n/b)\le cf(n)

\end{cases}

\]

1. 标准形式:\(a\) 是子问题个数,\(b\) 是规模缩小的倍数,\(f(n)\) 是拆分和合并成本。

T(n)=aT(n/b)+f(n),a≥1, b>1T(n)=aT(n/b)+f(n),\quad a\ge1,\ b>1

2. 计算关键指数:\(\log_b a\) 决定“子问题总数带来的总叶子成本”增长得有多快。

nlog⁡ban^{\log_b a}

3. 情况一:叶子主导:如果 \(f(n)\) 多项式地小于这个指数,总成本由最底层的大量叶子决定。

f(n)=O(nlog⁡ba−ε)⇒T(n)=Θ(nlog⁡ba)f(n)=O(n^{\log_b a-\varepsilon}) \Rightarrow T(n)=\Theta(n^{\log_b a})

4. 情况二:两边均衡:如果 \(f(n)\) 正好落在同阶,每一层成本相当,总成本要再乘一个 \(\log n\)。

f(n)=Θ(nlog⁡ba)⇒T(n)=Θ(nlog⁡balog⁡n)f(n)=\Theta(n^{\log_b a}) \Rightarrow T(n)=\Theta(n^{\log_b a}\log n)

5. 情况三:合并主导:如果 \(f(n)\) 多项式地大于指数,总成本由顶层的合并过程决定。

f(n)=Ω(nlog⁡ba+ε)⇒T(n)=Θ(f(n))f(n)=\Omega(n^{\log_b a+\varepsilon}) \Rightarrow T(n)=\Theta(f(n))

例如归并排序 \(T(n)=2T(n/2)+n\),\(a=2,b=2\),\(\log_2 2=1\),而 \(f(n)=n=\Theta(n^1)\),落入情况二,所以是 \(\Theta(n\log n)\)。主定理让“递归复杂度估时”从手工展开变成了查表。

主定理条件

再好的行情表也有使用前提,就像“例汤炖两小时”的规律,只适用于稳定火力和固定食材。主定理要成立,必须满足几个约束:\(a\ge1\) 是至少有一个子问题,\(b>1\) 是每个子问题规模必须严格缩小。更重要的是,\(f(n)\) 与 \(n^{\log_b a}\) 之间必须存在“多项式差距”,而不是仅仅差一个对数因子。

正则条件进一步要求合并成本不能“越合并越离谱”:

\[

a f\left(\frac nb\right) \le c f(n), \quad c<1, \ \text{对足够大的 } n

\]

1. 参数约束:\(a\) 必须是不小于 1 的常数,\(b\) 必须大于 1,否则问题规模根本不缩小。

a≥1,b>1a\ge1,\quad b>1

2. 多项式差:情况一和情况三要求 \(f(n)\) 与 \(n^{\log_b a}\) 相差一个 \(n^\varepsilon\) 的“数量级鸿沟”。

差距=nlog⁡ba±ε,ε>0\text{差距}=n^{\log_b a\pm\varepsilon},\quad \varepsilon>0

3. 正则条件:约束合并成本不会在递归中累积失控,这是情况三额外需要的条件。

af(n/b)≤cf(n),c<1a f(n/b)\le c f(n),\quad c<1

4. 不适用情形:如果 \(f(n)\) 是 \(n^{\log_b a}\log n\) 这类“差一点”的函数,或递归式不是标准分割形式(如 \(T(n)=T(n-1)+n\)),主定理就不能直接用。

一旦主定理不适用,就要退回迭代展开、递归树或代入法。这就像遇到非标准菜谱时,老师傅也只能老老实实重新称料。

均摊分析

宴席上有一道佛跳墙要炖三天,其他菜只要十分钟。如果只盯着一道菜,会觉得厨师根本忙不过来;但把整场宴席几十道菜放在一起算,三天一锅汤可被无数个十分钟匀开。均摊分析就是这个思路:不关心单次操作是否昂贵,只关心连续执行 \(n\) 次操作的总成本,再平均到每次。

最经典的例子是动态数组(比如 Python 的 list 追加元素)。平时一次插入就是写一个格子,成本是 1;但当数组装满时,需要申请一块更大的空间,把旧元素全部复制过去,这个瞬间的成本是 \(O(n)\)。

\[

\sum_{i=1}^{n} c_i \le n + \sum_{j=0}^{\lfloor\log_2 n\rfloor} 2^j < n + 2n = 3n

\]

1. 普通插入成本:数组没满时,一次追加写一个元素,成本为 1。

ci=1c_i = 1

2. 扩容复制成本:第 \(j\) 次扩容需要复制 \(2^j\) 个旧元素,成本为 \(2^j\)。

ci=2j(扩容那一刻)c_i = 2^j\quad(\text{扩容那一刻})

3. 累加所有扩容:从 1 开始不断翻倍,所有复制成本之和小于 \(2n\)。

∑j=0log⁡2n2j=2log⁡2n+1−1<2n\sum_{j=0}^{\log_2 n} 2^j = 2^{\log_2 n+1}-1 < 2n

4. 总成本上界:\(n\) 次普通插入加上复制成本,整体不超过 \(3n\)。

∑i=1nci<3n\sum_{i=1}^{n} c_i < 3n

5. 均摊结论:总成本除以次数 \(n\),本质是有常数上界的。

1n∑i=1nci=O(1)\frac{1}{n}\sum_{i=1}^{n} c_i = O(1)

均摊分析不是“平均情况分析”,它不依赖输入的概率分布;它保证的是:对于任何连续 \(n\) 次操作,总成本都不会超过某个渐进上界。这就是佛跳墙账本的严谨版。

势能法

均摊分析像事后算总账,而势能法像提前办一张“时间银行卡”:每次操作如果很便宜,就多存一些“时间币”进账户;如果遇到昂贵操作,就从账户里取币覆盖额外开销。这个账户余额就被称为势能函数 \(\Phi(D_i)\)。

设 \(D_i\) 是第 \(i\) 次操作后的数据结构状态,\(\Phi(D_i)\) 是当前账户余额。第 \(i\) 次操作的实际成本是 \(c_i\),但记账时我们记“摊还成本”:

\[

\hat{c}_i = c_i + \Phi(D_i) - \Phi(D_{i-1})

\]

1. 定义势能差:势能差 \(\Phi(D_i)-\Phi(D_{i-1})\) 模拟“这次是存钱还是取钱”。

ΔΦ=Φ(Di)−Φ(Di−1)\Delta\Phi = \Phi(D_i) - \Phi(D_{i-1})

2. 摊还成本公式:把实际开销和势能变化加在一起,就是这次操作记在账本上的成本。

c^i=ci+ΔΦ\hat{c}_i = c_i + \Delta\Phi

3. 总成本关系:把所有操作加起来,势能变化会相互抵消,只剩下头尾两项。

∑i=1nc^i=∑i=1nci+Φ(Dn)−Φ(D0)\sum_{i=1}^{n}\hat{c}_i = \sum_{i=1}^{n} c_i + \Phi(D_n)-\Phi(D_0)

4. 可覆盖条件:只要最终势能不小于初始势能,摊还总和就大于等于实际总和,说明没有“虚记”。

Φ(Dn)≥Φ(D0)⇒∑c^i≥∑ci\Phi(D_n)\ge \Phi(D_0) \Rightarrow \sum \hat c_i \ge \sum c_i

回到动态数组:设势能函数 \(\Phi=2\times size-capacity\),每次普通插入存入 2 个单位,扩容时用存款支付复制成本,最终每次操作的摊还成本都是常数。势能法比聚合分析更灵活,因为它可以按“状态”而非“次数”来做账。

复杂度增长与工程

菜单从 10 人变成 1000 人时,不同做法的差距会被迅速放大:O(1) 的“取一碗饭”始终是一勺;O(log n) 的“查楼层号”只是多按几次电梯;O(n) 的“逐桌点单”人数翻十倍时间翻十倍;O(n²) 的“同桌两两碰杯”则让人直接崩溃。建立增长阶梯,才能在选算法时不靠感觉:

\[

O(1) \prec O(\log n) \prec O(n) \prec O(n\log n) \prec O(n^2) \prec O(2^n)

\]

为了感受差距,可以看 \(n\) 取 1000 时,\(n^2\) 比 \(n\log_2 n\) 大多少倍:

\[

\lim_{n\to\infty}\frac{n^2}{n\log_2 n} = \lim_{n\to\infty}\frac{n}{\log_2 n} = \infty

\]

1. 复杂度阶梯:按照增长快慢,把常见复杂度从低到高排成一个链条。

O(1)≺O(log⁡n)≺O(n)≺O(nlog⁡n)≺O(n2)≺O(2n)O(1) \prec O(\log n) \prec O(n) \prec O(n\log n) \prec O(n^2) \prec O(2^n)

2. 写出比值:取 \(O(n^2)\) 和 \(O(n\log n)\) 做比值,看谁增长更快。

n2nlog⁡2n=nlog⁡2n\frac{n^2}{n\log_2 n} = \frac{n}{\log_2 n}

3. 取极限:分子线性增长,分母对数增长,比值趋向无穷。

lim⁡n→∞nlog⁡2n=∞\lim_{n\to\infty}\frac{n}{\log_2 n}=\infty

4. 工程启示:在千万级数据上,一个 \(O(n\log n)\) 的排序算法可能几秒完成,而 \(O(n^2)\) 的暴力算法要等到天荒地老;但常数太夸张的 \(O(n\log n)\) 在小数据上也可能输给优化良好的 \(O(n^2)\)。

工程选型时,先看渐进复杂度划掉明显不可行的方案,再结合数据规模、常数因子和空间开销做二次筛选。复杂度分析不给你精确秒表,却给你一张不会迷路的地图。

AI 应用场景

场景 1:数据预处理中的排序对比

阶段:数据准备与特征工程

解决的问题:处理百万级日志、样本或特征时,暴力插入排序 \(O(n^2)\) 会拖垮整个训练流程。用复杂度分析确认归并排序 \(O(n\log n)\) 的渐进优势,避免数据量翻倍后耗时爆炸。

具体案例:一个推荐系统每天清理 500 万条行为日志,用 Python 内置排序(\(O(n\log n)\))比手写冒泡排序快数十倍。复杂度预算让团队在写代码前就做出正确选择。

场景 2:模型推理缓存与动态扩容

阶段:模型部署与在线推理

解决的问题:推理服务常用哈希表/动态数组缓存请求结果,扩容偶尔发生但代价高。均摊分析证明反复插入操作的平均成本仍是 \(O(1)\),保证服务不会因为单次扩容而频繁超时。

具体案例:一个 LLM 服务的 prompt 缓存数组在高峰期频繁追加,团队用势能法设计两倍扩容策略,并把扩容量纳入监控,成功把尾延迟从 2 秒降到 200 毫秒。

场景 3:分治式训练与主定理预算

阶段:分布式训练与任务调度

解决的问题:当训练数据被分片到多个节点,每个节点递归训练,再合并梯度时,用主定理快速判断“通信合并成本”是否会主导训练时间。

具体案例:某联邦学习任务把 10000 个客户端分成 100 组,每组合并模型的成本为 \(O(m)\),通过主定理识别出合并成本与子任务成本的关系,将同步策略从全量合并改为分层聚合。

Python 演示

python55 行
class DynamicArray:    def __init__(self):        self.size = 0        self.capacity = 1     def push(self, value):        if self.size == self.capacity:            old_capacity = self.capacity            self.capacity *= 2            copies = old_capacity        else:            copies = 0        actual = copies + 1        self.size += 1        return actual  def simulate(n):    """    用势能法验证动态数组连续插入的均摊成本。    摊还成本 = 实际成本 + 势能变化    """    size = 0    cap = 1    total_actual = 0    total_amortized = 0     for _ in range(n):        phi_before = 2 * size - cap        if size == cap:            copies = cap            cap *= 2        else:            copies = 0        actual = copies + 1        size += 1        phi_after = 2 * size - cap        amortized = actual + (phi_after - phi_before)        total_actual += actual        total_amortized += amortized     return total_actual, total_amortized  def main():    n = 100000    actual, amortized = simulate(n)    print("实际总成本:", actual)    print("摊还总成本:", amortized)    print("平均摊还成本:", amortized / n)    print("3n 理论界限:", 3 * n)  if __name__ == "__main__":    main()
目的:用动态数组的扩容模型,展示势能法如何把“偶发昂贵操作”熨平成常数均摊成本。
  1. phi_before = 2 * size - cap在操作前计算“时间币余额”,也就是势能值。
  2. if size == cap: copies = cap; cap *= 2数组满时执行扩容,复制旧元素需要额外成本。
  3. amortized = actual + (phi_after - phi_before)这是势能法的核心公式:摊还成本等于实际成本加势能变化。
  4. total_amortized += amortized累加摊还成本,最后与理论界限 3n 对比。

常见误区

误区“我们的接口在大数据量下测出来是 5 秒,所以时间复杂度就是 O(5)。”

正解大 O 描述的是运行时间随输入规模增长的变化趋势,而不是某个固定耗时。就像“这道菜最多两小时”不意味着实际每次都要两小时,可能半小时就出锅了。复杂度相同不代表耗时相同,常数和硬件差异都很大。

为何常见:初学者常把“复杂度”和“性能”混为一谈,看到一次测试结果就想映射成复杂度公式。

误区“只要是递归,套主定理就能直接算出复杂度。”

正解主定理只适用于标准形式的 \(T(n)=aT(n/b)+f(n)\),且要求 \(a\ge1\)、\(b>1\)、函数间有足够的多项式差距。像 \(T(n)=T(n-1)+n\) 这种“每次只缩小 1”的递归就不满足形式,必须用迭代展开或递归树求解。

为何常见:因为教材最常展示主定理的成功案例,让人误以为它是一把万能钥匙。

误区“动态数组偶发扩容很贵,平均下来每次插入是 O(1),所以平均情况就是 O(1)。”

正解均摊分析是对同一个数据结构的任意操作序列取总成本平均值,不依赖输入分布;平均情况分析则依赖某种概率假设。均摊像“按整桌宴席的成本算每道菜”,平均情况像“按点菜概率算每道菜的期望成本”,二者完全不同。

为何常见:都叫“平均”,但一个是对最坏序列的记账技巧,一个是对随机输入的期望统计。

误区“O(n log n) 的排序在任何数据量下都比 O(n²) 的插入排序快,没必要看常数。”

正解渐进复杂度描述的是 n 趋向无穷时的行为。当 n 很小时,O(n²) 的朴素算法如果常数极小,可能比常数很大的 O(n log n) 算法更快。就像骑自行车(O(n²))在 100 米内可能比叫直升机(O(n log n))更先到,但跨城时直升机完胜。

为何常见:教材为了教学简洁,突出渐进比较而淡化常数,导致初学者忽略工程中的规模判断。

面试题

Q1. 大 O 表示法主要描述算法的什么特性?

单选题 · easy

  • A.代码实际运行的精确毫秒数
  • B.算法运行时间随输入规模增长的上界← 正确答案
  • C.算法最好情况下的时间
  • D.算法占用内存的绝对值

答案:B。大 O 描述的是最坏情况下运行时间的增长趋势,不是精确时间,也不限于内存。就像说“最多两小时”给的是一个上界,而不是精确秒表。

Q2. 以下哪个复杂度增长最慢?

单选题 · easy

  • A.O(n²)
  • B.O(n log n)
  • C.O(log n)← 正确答案
  • D.O(n)

答案:C。对数增长比线性、线性对数、平方都慢。把 n 翻倍时,log n 只是多了一点点,而 n² 会变成四倍,所以 O(log n) 最优。

Q3. 函数 \(f(n)=3n^2+100n+10\) 的渐近上界是?

单选题 · easy

  • A.O(n)
  • B.O(n²)← 正确答案
  • C.O(n³)
  • D.O(2ⁿ)

答案:B。最高阶项是 \(3n^2\),低阶项 \(100n\) 和常数 \(10\) 在大 n 时都可以被 \(n^2\) 的常数倍吸收,所以是 O(n²)。

Q4. 关于渐进符号,以下说法正确的是?

多选题 · mid

  • A.Ω 表示算法时间的下界← 正确答案
  • B.Θ 表示一个紧确界← 正确答案
  • C.常数因子不影响渐进类别← 正确答案
  • D.主定理适用于所有递归方程

答案:ABC。Ω 给出下界,Θ 同时给出上下界并夹得很紧,常数因子在渐进分析中可忽略。但主定理只适用于标准形式的递归,并不是所有递归都能用。

Q5. \(T(n)=2T(n/2)+n\) 用主定理求解的复杂度是?

单选题 · mid

  • A.Θ(n)
  • B.Θ(n log n)← 正确答案
  • C.Θ(n²)
  • D.Θ(log n)

答案:B。\(a=2,b=2,\log_b a=1\),而 \(f(n)=n=\Theta(n^1)\),对应主定理情况二,结果是 \(\Theta(n\log n)\)。

Q6. \(T(n)=4T(n/2)+n\) 的渐近复杂度是?

单选题 · mid

  • A.Θ(n²)← 正确答案
  • B.Θ(n² log n)
  • C.Θ(n³)
  • D.Θ(n log n)

答案:A。\(\log_2 4=2\),\(f(n)=n=O(n^{2-\varepsilon})\),落入主定理情况一,所以结果是 \(\Theta(n^2)\),子问题数量主导总成本。

Q7. 对动态数组从空开始连续执行 n 次 push(含扩容),单次均摊时间复杂度是多少?

单选题 · hard

  • A.O(1)← 正确答案
  • B.O(n)
  • C.O(log n)
  • D.O(n²)

答案:A。虽然扩容时单次成本是 O(n),但连续 n 次的总成本小于 \(3n\),均摊下来每次 O(1)。这就像宴席中可能有一道很贵的菜,整桌菜平均下来依然可控。

Q8. 关于势能法的摊还成本公式 \(\hat c_i = c_i + \Phi(D_i)-\Phi(D_{i-1})\),正确的理解是?

多选题 · mid

  • A.单次势能差可以为负← 正确答案
  • B.最终势能不小于初始势能时,摊还总成本能覆盖实际总成本← 正确答案
  • C.势能函数像“时间币”存款,用来平滑昂贵操作← 正确答案
  • D.势能法只能用于数组结构

答案:ABC。单次操作确实可能取用存款导致势能差为负;只要整体不透支,摊还总和就能覆盖实际总和;势能函数可推广到图、栈等很多结构,不限于数组。

Q9. 递归式 \(T(n)=T(n-1)+n\) 的时间复杂度是?

单选题 · mid

  • A.O(log n)
  • B.O(n)
  • C.O(n²)← 正确答案
  • D.O(2ⁿ)

答案:C。这不是标准主定理形式,因为每次只让规模减 1。展开后是 \(1+2+\cdots+n\),和 \(n(n+1)/2\) 同阶,所以是 O(n²)。

Q10. 关于复杂度增长与工程实践,以下说法正确的是?

多选题 · hard

  • A.O(n log n) 渐进上慢于 O(n²)← 正确答案
  • B.常数因子会改变渐进复杂度类别
  • C.小数据量下 O(n²) 的简单实现可能快于 O(n log n)← 正确答案
  • D.空间复杂度也可以用大 O 表示← 正确答案

答案:ACD。常数因子不改变渐进类别,B 错误。渐进分析看的是大 n 趋势,小数据时常数与实现细节往往更重要;空间消耗同样可以用增长率描述,所以 A、C、D 正确。