Algorithm · Complexity Analysis

复杂度分析

大 O 与 Θ · 常见量级 · 主定理 · 均摊分析 —— 面试里每道算法题的第二问

渐进记号

O / Ω / Θ 的精确定义,别再把"上界"当"精确量级"

递归式求解

主定理三 case 与递归树,以及它什么时候失灵

均摊 vs 平均

动态数组翻倍为什么是 O(1),哈希表为什么"敢说" O(1)

开场定位:复杂度分析是所有算法 deck 的前置语言,后面排序、二分、DP 每篇都要用。三个卡片对应三大考点:记号精确性、递归式求解、均摊与平均的辨析。

Why It Matters

复杂度是唯一跨机器的算法标尺

先给一个能算出结果的对照:10 万条数据去重,用 map 是 10⁵ 次操作(约 1 毫秒),用切片逐个比对是 10¹⁰ 次(约 100 秒)——同一台机器、同一个任务,差 10 万倍

机器快慢只改常数,不改量级

换 CPU、换语言改的是常数因子;量级 O(n) → O(n log n) → O(n²) 由算法结构决定。所以面试官不关心你机器多快,只关心规模翻 10 倍时代价怎么涨。

大 O 回答一个问题:规模怎么影响代价

n 从 10⁵ 到 10⁶,O(n) 算法慢 10 倍,O(n²) 算法慢 100 倍。数据规模一给,能用的复杂度档位就锁死了(见第 11 题"规模估算")。

它是所有后续 deck 的公共语言

排序对比、堆的建堆代价、哈希扩容、并查集近似 O(1)——每个结论都是复杂度语言写的。本篇先把这些语言规则钉死。

面试标准动作(四件套):
① 时间复杂度——分开说平均与最坏
② 空间复杂度——递归栈深度算进去
③ 涉及扩容/缩容的结构,补一句均摊
④ 指出当前瓶颈,以及"如果再优化,往哪个量级走"。

反例

"这个算法是 O(1)"——哈希查找光说 O(1),被追问最坏 O(n) 就露馅。

正例

"平均 O(1)、最坏 O(n),Go map 用渐进扩容把搬迁摊薄"——一句话展示三层理解。

强调"标尺"属性:复杂度剥离了机器差异,只剩算法结构。右侧四件套是每道题答题的固定收尾动作,后续每篇算法 deck 都按这个格式给复杂度。

Prerequisites & Glossary

先把词认全:下面每一页都会用到它们

术语一句话理解(先记住这个,细节后面展开)
输入规模 n我们讨论"代价随什么变大而变大"的那个变量。说复杂度前必须先说清 n 是谁(元素个数?位数?边数?)
时间复杂度 / 空间复杂度代价随 n 增长的量级,不是具体的毫秒或字节
O / Ω / Θ渐进上界 / 下界 / 紧确界。日常说"是 O(n)"其实多半想说"是 Θ(n)"
基本操作我们决定去数的那个动作(一次比较、一次加法、一次哈希)。数它而不数别的,是因为它们只差常数倍
最坏 / 平均 / 均摊三种取数口径:最坏=最倒霉那次;平均=对输入分布取期望;均摊=一整串操作算总账再除
递归式 recurrence用"规模更小的自己"来描述代价的等式,如 T(n)=2T(n/2)+O(n)
主定理T(n)=aT(n/b)+f(n) 这类分治递归式的现成公式,对照三个 case 直接出答案
递归树 / 递归深度把递归一层层展开成的树;树高就是递归深度,它决定空间复杂度
均摊分析一整串操作算总代价再除以次数。它对任意操作序列都成立,不靠运气
原地 in-place额外空间 O(1)——不管输入多大,辅助空间是个常数

这一篇几乎不需要前置

只要你能读懂循环、递归和数组下标就够了。想看复杂度被怎么用在真实算法上,可以对照这几篇:
排序算法 → 同一问题五种复杂度解法
二分查找 → O(log n) 从哪来的
Go slice 底层 → 均摊扩容的真实代码

本 deck 的最小心智模型

把复杂度分析记成一个固定三步动作:① 选定随什么变大(n 是谁);② 数"基本操作执行了多少次",看它随 n 怎么长;③ 只保留增长最快的那一项,系数全部丢掉。后面所有工具——主定理、递归树、均摊分析——都只是帮你在不同结构里把第 ② 步的"数次数"做出来

为什么"先说清 n 是谁"值得单独记

同一个算法换个 n 定义,复杂度就变了:判断质数按数值大小是 O(√N),按二进制位数就是 O(2^(k/2))(指数级)。面试里没说清 n 就报复杂度,是最容易被追问到露馅的地方。

阅读提示:术语不用背,忘了回来查这一页;真正要能脱口而出的只有速查页里那张"主定理三 case + 规模反推档位",以及面试答题四件套。
前置页:十个术语先定义再使用。心智模型把"主定理/递归树/均摊"统一成"帮你数次数"的工具,避免读者把它们当成三套互不相干的知识点;并用"质数判定按数值 vs 按位数"的例子说明"先说清 n 是谁"不是废话。

Asymptotic Notation · CLRS ch3

O 只是上界,Θ 才是"精确量级"

记号定义(n → ∞)直觉例:f(n) = 3n² + 5n + 2
O(g)∃c,n₀:f(n) ≤ c·g(n)渐进上界:不超过f = O(n²) ✓,但 f = O(n³) 也"对"(只是没信息量)
Ω(g)∃c,n₀:f(n) ≥ c·g(n)渐进下界:不低于f = Ω(n²) ✓,Ω(n) 也对
Θ(g)c₁·g(n) ≤ f(n) ≤ c₂·g(n)紧确界:恰好这个量级f = Θ(n²),且只有 Θ(n²)
o(g) / ω(g)上/下界取不到(严格)严格慢于 / 严格快于f = o(n³),f = ω(n)
四个高频误区:
① 把 O 当精确量级说——"快排是 O(n log n)"其实说的是期望 Θ(n log n),最坏 Θ(n²);
② 无意义放大——二分查找说成 O(n) 技术上没错,但等于没说;说复杂度要给最紧的上界
③ 纠结 log 底数——log₂n 与 log₁₀n 只差常数因子 1/log₂10,Θ 意义下等价;
④ 把 2n 写成 O(2n)——常数因子全部丢掉,O(2n) = O(n)。

为什么工程上用 O 而不是 Θ?

历史习惯 + 保守承诺:O 给的是"承诺不超过",最坏情形导向的思维对工程更安全。但面试口头表达里,说 "平均 Θ(n log n)、最坏 O(n²)" 这种混合句式最专业——哪里精确、哪里保守,清清楚楚。

这页是语言规则。重点敲四个误区:O 被滥用成 Θ、无意义放大、log 底数、常数因子。快排那句务必展开——它同时连着第 14 页 QA1 和排序 deck。

Growth Ladder

七档量级:从常数到阶乘

复杂度名称典型算法 / 操作n = 10⁶ 时的量级(基准 10⁸ 简单运算/秒)
O(1)常数数组下标访问、哈希查找(平均)、栈顶弹出≈ 10ns,与 n 无关
O(log n)对数二分查找、平衡树查找、堆上浮/下沉≈ 20 次运算 → 亚微秒
O(n)线性遍历、快排单轮划分、桶计数10⁶ 次 → 10ms
O(n log n)线性对数归并/堆排序、快排平均,比较排序的下界2×10⁷ 次 → 0.2s
O(n²)平方冒泡/选择/插入(平均)、朴素双指针枚举10¹² 次 → ≈ 2.8 小时
O(2ⁿ)指数枚举全部子集、暴力搜索解空间n=40 已需数小时,n=10⁶ 不可行
O(n!)阶乘枚举全排列(TSP 暴力解)n=15 已超 10¹²,彻底不可行
实战换算口诀:1 秒 ≈ 10⁸ 次简单运算。n=10⁵ → O(n log n) 稳过、O(n²) 悬;n=10⁶ → 只剩 O(n) / O(n log n);看到 n≤20 要条件反射想到 O(2ⁿ) 状压/回溯,n≤12 想到 O(n!) 全排列。
表格建议背下第三列的"代表算法"和第四列的手感。底部口诀是 LeetCode 现场估算工具:从数据规模反推可行复杂度档位,选错档位基本等于解法方向错了。

Growth Curves · log₂ 纵轴

量级差距在图上长什么样

常见复杂度增长曲线对比(对数纵轴) 在 log2 刻度的纵轴上对比 O(log n)、O(n)、O(n log n)、O(n平方)、O(2的n次方) 五条增长曲线:n=16 时量级已从 4 拉开到 65536,其中 O(n log n) 与 O(n) 在小规模时接近、随后明显分离。 124 8163264 1481216 输入规模 n O(log n) O(n) O(n log n) O(n²) O(2ⁿ) 纵轴 log₂ 刻度 · n ≤ 16 · 曲线到顶即"出画",表示该量级在更大 n 下遥遥领先(落后)
看点两个:一,n log n(蓝色)与 n 在小规模贴得很近,这就是"小数据上 O(n²) 插入排序反而快"的原因;二,对数纵轴下直线 = 指数增长,2ⁿ 那条虚线其实是最陡的。提醒:这是 log 轴,视觉差距比真实差距温和得多。

How To Analyze

现场分析四板斧:加、乘、折半、递归

① 顺序段:相加,取最大项

先 O(n) 预处理再 O(n²) 主循环 → O(n²)。低阶项与常数在大 n 下全部忽略。

② 嵌套循环:相乘——但先看内层次数依不依赖外层

内层固定 n 次就相乘;内层 j < i 则是 Σi = n(n+1)/2,仍是 O(n²)。依赖关系决定是"乘"还是"求和"。

③ 折半/倍增:出现对数

每轮把规模除以 2(或乘以 2)→ 循环轮数是 O(log n)。二分、快速幂、i *= 2 循环都是这个形状。

④ 递归:列递归式,再解它

把"子问题个数 × 子问题规模 + 本层代价"写成 T(n) = a·T(n/b) + f(n),用主定理或递归树解——下两页展开。

// ② 嵌套:内层依赖外层 → 求和
for i := 1; i < n; i++ {
    for j := 0; j < i; j++ { // Σi = n²/2
    }
}                          // O(n²)

// ③ 倍增:轮数取对数
for i := 1; i < n; i *= 2 {
    for j := 0; j < n; j++ {
    }
}                          // O(n·log n)
经典陷阱:把上面两段"杂交"——外层 i *= 2、内层 j < i,答案不是 O(n log n) 而是 O(n):总工作量 = 1+2+4+…+n = 2n−1。求和看总账,别把两层复杂度硬乘(QA 第 10 题原题)。
四板斧按使用频率排序:嵌套乘、顺序加最常用;折半出对数次之;递归式最难也最值钱。右侧陷阱题务必现场推一遍——它检验的是"求和 vs 相乘"的判断力,不是背结论。

Master Theorem · CLRS ch4

主定理:分治递归式的查表解法

主定理三种 case 的判定流程 分治递归式 T(n)=aT(n/b)+f(n) 的求解:比较 f(n) 与 n 的 log 以 b 为底 a 次幂;f 多项式更小取 Case1 结果 Θ(n 的 log_b a 次幂),同阶取 Case2 结果再多乘一个 log n,多项式更大且满足正则条件取 Case3 结果 Θ(f(n))。 代入递归式 f 更小 同阶 f 更大 T(n) = a·T(n/b) + f(n) a ≥ 1 · b > 1 · f:合并子问题解的代价 比较 f(n) 与 n^(log_b a) n^(log_b a):叶子层总代价,看谁主导 CASE 1 · f 多项式更小 f(n) = O(n^(log_b a − ε)) T(n) = Θ(n^(log_b a)) 9T(n/3) + n → Θ(n²) CASE 2 · f 与之同阶 f(n) = Θ(n^(log_b a)) T(n) = Θ(n^(log_b a) · log n) 归并 2T(n/2)+n → Θ(n log n) · 二分 T(n/2)+1 → Θ(log n) CASE 3 · f 多项式更大 f(n) = Ω(n^(log_b a + ε)) 且正则条件 T(n) = Θ(f(n)) 2T(n/2) + n² → Θ(n²) 正则条件:a·f(n/b) ≤ c·f(n)(c<1)· 多项式差异不满足时主定理失效 → 递归树 / Akra–Bazzi
记忆锚点:先算 n^(log_b a)(叶子总代价),再和 f(n)(本层总代价 a·f)比大小——谁多项式意义上大谁主导;同阶则每层都要付、共 log n 层,多乘一个 log n。三个例子都要能现场算:9T(n/3)+n、2T(n/2)+n、2T(n/2)+n²。

When Master Theorem Fails · Recursion Tree

经典陷阱:自底向上建堆是 O(n),不是 O(n log n)

// 自底向上建堆:从最后一个非叶节点逐个下沉
func buildHeap(a []int) {
    for i := len(a)/2 - 1; i >= 0; i-- {
        siftDown(a, i, len(a)) // 代价 = O(h(i))
    }
}
错误分析:"每层 O(n) × log n 层 = O(n log n)"。
错在把节点的代价当成了的代价——底层节点一大堆,但它们高度低、几乎不用下沉。
另一条路:写成递归式 T(n) = 2T(n/2) + O(log n),主定理 Case 1(log n 比 n 多项式更小)→ 同样得 Θ(n)

正确姿势:按"高度"加权求和

高度为 h 的节点至多 ⌈n/2^(h+1)⌉ 个,每个下沉 O(h):

Σh=0⌊log₂n⌋ (n/2^(h+1)) · O(h)
= O(n) · Σ h/2^h = O(n) · 2 = O(n)

(用到 Σh≥0 h/2^h = 2,几何级数逐项求导)

递归树视角

把树倒过来看:一半节点是叶子(代价 0),1/4 最多沉 1 层,1/8 最多沉 2 层……代价集中在稀疏的顶层,总账被底层"稀释"到 O(n)。

面试关联

堆排序先花 O(n) 建堆、再 n 次 O(log n) 下沉 → 总 O(n log n);TopK 用建堆 O(n) + k 次弹出 O(k log n)。详见 排序 deck堆 deck

这页是本 deck 的招牌题:建堆 O(n)。两个抓手——高度加权求和的级数、递归式 2T(n/2)+log n 走主定理 Case 1。错误版本"每层 O(n)×log n 层"要能一句话点破错在哪。

Amortized Analysis · CLRS ch17

均摊分析:把偶尔的大账摊到每次操作头上

动态数组 append:为什么敢说 O(1)

满员时翻倍扩容、整体搬迁一次 O(n)。单看那一次是 O(n),但任何 n 次 append 的总代价 ≤ 2n——按"序列总账"算,每次操作背负的均摊代价就是 O(1)。

方法一:聚合(总账法)

n 次 push:写 n 次 + 搬 1+2+4+…+2^k < 2n 次 → 总代价 < 3n → 均摊 O(1)。最直观,面试首选。

方法二:记账(预付费)

每次 append 收 3 元:1 元付本次写入,2 元存着付未来它被搬迁的两次(2 倍扩容下每个元素最多被搬 log n 次,但按"当前存的钱"总够付)。证明了均摊 3 = O(1)。

方法三:势能法

定义势 Φ = 2·size − capacity ≥ 0;每次插入的实际代价 + ΔΦ ≤ 3。最强大也最抽象,处理"代价与状态相关"的场合(如二项堆)才必须上。

定义:均摊代价 = 最坏操作序列的总代价 ÷ 操作次数。它不是平均值,而是对任意序列都成立的最坏保证——这就是它与"平均复杂度"的本质区别(下一页展开)。

工程里的均摊

Go slice append(slice deck)、map 渐进扩容(哈希 deck)、vector push_back、splay tree、二项堆。

同族结论

并查集"近似 O(1)"(α(n),并查集 deck)、动态数组按 1.5 倍扩容仍是均摊 O(1)(几何级数通吃)。

反面教材:扩容时若按 固定 +10 增量(非倍增),n 次 push 总搬迁 = n²/20 → 均摊 O(n)。必须按倍增/几何增长,均摊 O(1) 才成立。
三方法层层递进:聚合法面试必会,记账法给直觉(3 元 = 1 写 + 2 搬),势能法点到为止。底部"固定增量扩容是均摊 O(n)"是最容易被追问的边角——几何级数是均摊 O(1) 的命根子。

Doubling · Aggregate Account

翻倍扩容的总账:n 次 push 搬运 < 2n 次

动态数组倍增扩容与累计搬迁代价 容量按 1、2、4、8、16 翻倍增长的动态数组,每次满员扩容需要整体搬迁当前全部元素;16 次 push 的总代价为 16 次写入加 15 次搬运,不超过 2n,因此单次均摊 O(1)。 搬 1 个 搬 2 个 搬 4 个 搬 8 个 cap=1cap=2cap=4cap=8cap=16 a a b a b c a b c d e f g 再满 → 再翻倍 …(几何级数) 16 次 push = 写 16 次 + 搬 (1+2+4+8) = 15 次 → 总代价 31 < 2n 一般化:n 次 push 总代价 < 3n → 均摊 O(1) · 扩容比例必须 ≥ 常数倍(倍增 / 1.5 倍皆可) Go slice append 同款策略(growslice),扩容细节见 slice deck
图解聚合法的核心账本:搬迁代价 1+2+4+8 是几何级数,被最后一项主导,总和 < 2×最后一项 = O(n)。讲的时候强调"虚线格子"是空位,实心是元素——总账里写入 n 次 + 搬运 < n 次。

Amortized ≠ Average

均摊 O(1) 与平均 O(1):一字之差,保证不同

均摊 O(1) —— 序列级的最坏保证

任意长度为 n 的操作序列,总代价 ≤ c·n。不管来的是什么序列(哪怕全是恶意构造),平均到每次都是 O(1)。动态数组 append、并查集(α(n))属于这类——无随机性、无分布假设,铁证

平均 O(1) —— 依赖输入分布的期望

在"输入随机/散列均匀"的假设下,单次操作的期望代价是 O(1)。快排期望 O(n log n)(随机枢轴)、哈希表查找 O(1)(简单均匀散列假设)属于这类——假设一旦被打破,保证失效

说法成立前提被恶意打爆?典型例子
均摊 O(1)无(对任意序列成立)不能动态数组 append、并查集 union
平均/期望 O(1)散列均匀 / 输入随机能——构造全碰撞键 → 单桶链 → O(n)哈希表查找、快排随机枢轴
最坏 O(n)哈希表全碰撞时的真实下界
面试一句话:"哈希表是平均 O(1)、最坏 O(n),工程上靠好散列函数 + 负载因子上限控制住;动态数组是均摊 O(1),这是对任意序列的硬保证。"——两个词用对了,比背十个复杂度更显功底。
本 deck 第二块招牌:均摊与平均的辨析。判别口诀——有没有"对任意序列都成立"的证明:有则是均摊;要靠随机性/分布假设则是平均。快排随机化把最坏 O(n²) 变成"期望 O(n log n)",但不是均摊——随机性来源不同。

Space Complexity

空间:只算"额外"的,但递归栈要算

定义:额外辅助空间,输入不计

原地(in-place)算法 O(1) 额外空间;开辅助数组 O(n)。输入本身占多少与本算法无关。

递归栈是隐形的 O(深度)

递归深度 d 层、每帧 O(1) 局部变量 → 空间 O(d)。树递归遍历是 O(h)(h 为树高),不是 O(n)——同一时刻只有一条根到叶的路径存活在栈上。

Go 没有尾调用优化

尾递归在 Go 里不省栈(规范不保证 TCO,编译器不做);深递归(如百万级链表)要手写迭代或分批处理,否则栈溢出。

时间空间经常可交换

哈希换时间(O(n) 空间换 O(n) 时间)、记忆化(空间换重复计算)、滚动数组(DP 里 O(nd) → O(d))。面试常追问:"空间能压到 O(1) 吗?代价是什么?"

算法时间额外空间
快排平均 O(n log n)O(log n) 递归栈(最坏 O(n))
归并排序O(n log n)O(n) 辅助数组
堆排序O(n log n)O(1)(真·原地)
计数/桶排序O(n+k)O(k) 桶数组(k 为值域)
链表反转O(n)O(1) 三指针
二叉树遍历(递归)O(n)O(h) 栈深(平衡 O(log n),最坏链状 O(n))
面试陷阱:"归并排序空间 O(n log n)"——错。归并递归栈 O(log n) 辅助数组 O(n),相加取最大 → O(n)。顺序段规则在空间上同样适用。
两个最常被追问的点:递归栈深度计入空间(树遍历是 O(h) 不是 O(n));归并空间 O(n) 不是 O(n log n)。Go 无 TCO 是 Go 岗位的加分细节,和 slice/栈深一脉相承。

Cheat Sheet · 本系列总纲

常用数据结构操作复杂度总表

结构访问查找插入删除关键词 & 详见
数组O(1) 下标O(n)O(n)O(n)尾部插入均摊 O(1);数组与链表
链表O(n)O(n)O(1)¹O(1)¹¹ 已定位节点的前提下;数组与链表
哈希表O(1)²O(1)²O(1)²² 平均;最坏 O(n) 全碰撞;哈希表
平衡 BSTO(log n)O(log n)O(log n)O(log n)红黑/AVL;有序性是哈希给不了的;平衡树
跳表O(log n)³O(log n)³O(log n)³O(log n)³³ 期望;Redis zset 的实现;跳表
堆顶 O(1)O(n)O(log n)弹顶 O(log n)TopK / 优先队列主力;
TrieO(L)O(L)O(L)L 为串长,与元素总数无关;Trie
并查集O(α(n))⁴不支持⁴ 近似常数,合并+查询;并查集
布隆过滤器O(k)⁵O(k)不支持⁵ k 为哈希个数;可误判不漏判;布隆过滤器

均摊/期望/平均已分别标注;"访问"指按位置随机访问。有序场景(范围查询、TopK、前驱后继)哈希表无能为力,需要树/跳表——这是"哈希 vs 树"面试题的题眼。

这页是整个数据结构系列的"地图":每行链接一个后续 deck。讲的时候带着走两列——哈希表为什么没有"访问"列(无位置语义)、有序结构为什么不可替代(范围查询)。

Cheat Sheet

一页带走:主定理 · 三种口径 · 规模反推

① 主定理:T(n) = aT(n/b) + f(n),三 case 一句话判定

Case条件(比 n^(log_b a) 和 f(n))结论
Case 1f(n) 多项式级更慢地增长Θ(f(n)) —— 分治外的活儿主导
Case 2两者同阶(差一个 log^k n)Θ(n^(log_b a) · log^(k+1) n)
Case 3f(n) 多项式级更快地增长 + 正则条件Θ(f(n)) —— 递归不再是瓶颈
失灵时子问题规模不等、f 不满足多项式差距退回递归树 / 求和(如建堆 O(n))

② 分析四板斧(拿到代码按这个顺序走)

① 定 n先说清"随什么变大"——元素个数、位数、边数?
② 数次数循环看嵌套层数与上界是否跟随外层(跟随要求和,不是相乘)
③ 只留最大项3n²+5n+2 → Θ(n²);系数、低阶项、log 底数全部丢掉
④ 分开口径平均与最坏分开说;有扩容/缩容的补一句均摊

③ 三种口径:别再说混了

口径成立前提典型例子
最坏无(对任意输入都成立)快排 O(n²)、哈希 O(n)
平均 / 期望依赖输入分布或随机性假设快排期望 O(n log n)、哈希平均 O(1)
均摊无——对任意操作序列成立动态数组 append O(1)、并查集 O(α(n))

④ 规模反推档位(1 秒 ≈ 10⁸ 简单运算)

n ≤ 10³O(n²) 随便写;O(2ⁿ) 也可一试
n ≤ 10⁵O(n log n) 稳过;O(n²) 基本挂
n ≤ 10⁶只剩 O(n) 与 O(n log n)
n ≤ 20 / 12条件反射想到 O(2ⁿ) 状压回溯 / O(n!) 全排列
面试答题四件套:① 时间——平均与最坏分开说;② 空间——递归栈算进去;③ 有扩容的结构补一句均摊;④ 指出瓶颈与"再优化往哪个量级走"。
一句话背下来:复杂度 = 先定 n、再数次数、只留最大项;主定理与均摊分析都只是"帮你把次数数出来"的工具。
速查页:左半是"主定理三 case + 分析四板斧"(推导工具),右半是"三种口径辨析 + 规模反推档位 + 答题四件套"(表达工具)。这两块正好对应面试的两个动作:把复杂度算出来、把复杂度说清楚。

Interview QA · Part 1

高频追问:记号、量级与陷阱

先自答,再对照:每题先说出你的答案(哪怕只说关键词),再展开下面那行——卡住的那 30 秒才是真正长记性的部分。

1 · O 和 Θ 的区别?说"快排是 O(n log n)"严谨吗?

上界≠紧确期望/最坏分开说

O 只是上界(冒泡排序也是 O(n!)),Θ 才是精确量级。严谨说法:快排期望 Θ(n log n)、最坏 Θ(n²)。能补一句"随机枢轴时期望成立"更佳。

2 · 为什么最好/最坏/平均要分开说?

输入分布

插入排序:已有序输入 O(n),逆序 O(n²)。只说一个数字 = 丢弃了算法对输入的敏感度;面试官要的是你知道它在什么输入下退化

3 · 现场用主定理:T(n)=4T(n/2)+n 和 T(n)=2T(n/2)+n log n?

Case 1Case 2 变体

前者 n^(log₂4)=n² 主导 → Θ(n²);后者 f=n log n 与 n 同阶但多一个 log(Case 2 的 k=1 变体)→ Θ(n log²n)。第二问专考你有没有背"主定理扩展形式"。

4 · 主定理为什么不能直接套自底向上建堆?

非分治高度加权

建堆不是"划分-递归-合并"结构,是按高度求和 Σ (n/2^(h+1))·O(h) = O(n)。硬套"每层 O(n)×log n 层"会把底层海量低代价节点错算成 O(n log n)。

5 · 均摊 O(1) 和平均 O(1) 的本质区别?

任意序列 vs 分布假设

均摊:任意序列总代价/次数,无假设、有硬保证(动态数组);平均:依赖输入分布/散列均匀假设的期望值(哈希表),假设被打破就退化。

6 · 哈希表为什么敢说 O(1)?最坏呢?

均匀散列负载因子

平均 O(1) 建立在散列均匀 + 负载因子受控上;最坏全碰撞 O(n)。工程上用好的散列函数 + 扩容/树化兜底(Java 8 链表超 8 转红黑树,最坏降到 O(log n))。

7 · 空间复杂度:递归栈算不算?输入算不算?

栈算输入不算

输入不计(那是问题规模),递归栈必须算:递归深度 d、每帧 O(1) → O(d)。树递归遍历是 O(h)——同一时刻只有一条路径在栈上。

8 · 快排和归并的空间复杂度?

栈深辅助数组

快排:递归栈平均 O(log n)、最坏 O(n)(划分极不均匀);归并:O(log n) 栈 + O(n) 辅助数组,相加取最大 → O(n)。堆排 O(1) 是三者中唯一真原地。

QA 前八题集中在记号与辨析:O/Θ、三种情形、主定理两算例、建堆、均摊 vs 平均、哈希最坏、栈空间、快排归并空间。第三题的 Case 2 k=1 变体(n log n → n log²n)是拉开差距的追问。

Interview QA · Part 2

高频追问:推导、估算与现场分析

先自答,再对照。这一组的重点是"能推"——先把答案自己算出来,再看下面那行验证你的步骤对不对。

9 · 二分查找为什么是 O(log n)?推导一下。

每次排除一半

k 轮后剩余 n/2^k,停机条件 n/2^k ≤ 1 → k ≥ log₂n。反过来写循环不变量:区间长度 L 每轮至少减半,L 从 n 到 1 的减半次数就是 log₂n。

10 · 外层 i*=2、内层 j<i,复杂度是多少?

陷阱题O(n)

总工作量 = 1+2+4+…+<n = 2n−1 → O(n),不是 O(n log n)。内层上界跟外层走时必须求和;只有内层独立满 n 次才是乘出 O(n log n)。

11 · n=10⁵ / 10⁶,什么复杂度能过?

10⁸ ops/s反推档位

1 秒 ≈ 10⁸ 简单运算:n=10⁵ → O(n log n)(≈1.7×10⁶)稳、O(n²)(10¹⁰)悬;n=10⁶ → O(n log n) 尚可、O(n²) 必挂。从数据规模反推可行档位,是选题第一步。

12 · 为什么动态数组扩容后还能说均摊 O(1)?

几何级数

n 次 push 总搬迁 = 1+2+4+…+2^k < 2n(几何级数被末项主导),加 n 次写入总代价 < 3n → 均摊 O(1)。关键在倍增:改成固定 +10 增量就是均摊 O(n)。

13 · 记账法/势能法怎么用?各举一例。

预付费势函数

记账:动态数组每次 push 收 3 元(1 写 + 2 搬),任意时刻累计余额 ≥ 0 → 均摊 3。势能:Φ=2·size−capacity ≥ 0,摊还代价 = 实际代价 + ΔΦ ≤ 3。势能法适合状态相关的场合(二项堆、splay)。

14 · 递归算法的时间/空间分别怎么数?

节点数×单点最大栈深

时间 = 递归树节点数 × 每节点代价(解递归式);空间 = 同时存活的最大栈深 × 每帧空间。二叉树 DFS:时间 O(n),空间 O(h)——两者根本不是一回事。

15 · log 底数重要吗?O(log₂n) 和 O(log₁₀n)?

换底=常数

换底公式 log₁₀n = log₂n / log₂10,只差常数因子,Θ 意义下等价。但 O(2^log n) = O(n) 这种"对数在指数上"不能乱约——底数在指数里就不再是常数因子了。

后七题偏推导与实战:二分推导、i*=2 陷阱、规模反推、扩容总账、记账/势能、递归时空分离、log 底数。第 10 题和第 6 页陷阱是同一题,面试出现率极高,务必现场会推。

Related & References

相关知识点与参考

算法系列(本分类)

排序算法 →(各排序复杂度全景,比较排序下界)
二分查找 →(O(log n) 的代表选手)
动态规划 →(记忆化 = 时空交换的典型)
回溯 →(O(2ⁿ)/O(n!) 解空间的代表)

数据结构系列(theory)

数组与链表 →(动态数组均摊扩容的实体)
哈希表 →(平均 O(1) 的成立前提)
堆与优先队列 →(建堆 O(n) 的主场)
Go slice 底层 →(growslice 扩容工程实现)

参考来源(本 deck 结论可溯源至下列一手材料)

CLRS《Introduction to Algorithms》ch3/ch4/ch17渐进记号定义 · 主定理与递归树 · 均摊分析三方法
oi-wiki.org/basic/complexity复杂度入门:量级、主定理、均摊(中文对照)
bigocheatsheet.com数据结构/排序操作复杂度速查表(本 deck 第 13 页同源)
Cornell CS3110 · Lecture 20: Amortized Analysis聚合/记账/势能三方法的独立讲义佐证
GeeksforGeeks · Introduction to Amortized Analysis动态数组三方法算例(与 CLRS ch17 互勘)
收尾页:算法系列 + 数据结构系列两条链接线,参考来源五条。复杂度结论都是教科书级稳定知识,出处为 CLRS 与 OI Wiki,无版本敏感项。