
有一次帮朋友准备算法笔试他背了很多排序模板。从冒泡到快速排序代码写得很熟练但当我问他“快速排序的最坏情况什么时候出现”“归并排序为什么一定是 O(n log n)”时他沉默了。这不是个例。很多学习者的真实状态是会写算法但不会分析算法。于是我把斯坦福算法专项的第一部分推给了他——标题里正好写着三个关键词分治、排序、随机化。这门课由 Roughgarden 主讲看起来像是一门“排序专题课”但真正训练的是另一件事用可证明、可推导的方式理解算法的设计边界。从我自己学习和重刷这门课的经验来看它的核心价值不在于让你多背几个排序实现而在于帮你建立一套“先分析、再动手”的算法思维。递归、复杂度递推、概率分析和随机化是主线排序只是用来承载这些思想的最佳载体。这篇文章我会拆开讲这门课到底在教什么分治和随机化为什么重要工程中的排序和教科书里的排序有什么不同以及学习中容易被忽略的坑和我的建议。1. 先搞清楚这门课真正解决的是哪类问题1.1 它不是一门“排序算法合集”只看标题很多人会误以为这是一门把各种排序算法挨个讲一遍的课。但这门课的真正组织逻辑是围绕“设计和分析算法的方法”展开的。分治是一种算法设计范式排序只是它的经典应用场景随机化则是一类更高级的策略用来应对那些“输入总是不怀好意”的情况。课程把这三者放在一起本质上是想告诉你排序不是重点怎么设计一个算法、怎么证明它快、怎么让它对坏输入不敏感才是重点。所以如果你带着“学会快排和归并就能应付笔试”的预期来上这门课会很失望。它不会给你一叠可以直接套用的模板而是逼你去回答那些面试官真正喜欢追问的问题为什么这个算法是对的为什么它的复杂度是这个数最坏发生在什么时候你打算怎么修复最坏情况1.2 最稀缺的能力不是“会写”而是“会分析”我见过不少学习者的代码写得很快但一遇到复杂度分析就靠猜。问他“归并排序的空间复杂度”他能说 O(n)但说不清这个 n 是递归栈还是辅助数组问他“快速排序为什么期望是 O(n log n)”他只知道“因为平均情况是这样”却说不清平均是对谁取的。这门课会强迫你把这些问题变成可推导的结论。你会学到用递推式描述递归算法的代价用递归树或主定理求解复杂度用决策树证明下界用概率分析证明随机化算法的期望表现。这套分析能力才是把“算法学习者”和“算法工程师”区分开的东西。模板能解决已知问题分析能力才能解决未知问题。1.3 适合谁、不适合谁以及需要什么前置条件从我的体验看这门课更适合这些人已经能熟练写代码但算法理论基础比较薄弱的人。刷题时“看得懂答案但下次遇到变体还是不会”的人。准备面试想真正搞懂复杂度分析而不只是背结论的人。打算继续深入算法专项后续课程的人。反过来也有几类人可能不适合只想快速背模板应付笔试的人。这门课要求你推导会觉得很慢。对“证明”完全没有兴趣的人。那里面的决策树、递推式推导会劝退你。期待课程直接教你工程框架和性能优化技巧的人。这门课更偏理论思维工程细节需要自己在实践中补。前置条件其实不高至少熟悉一种编程语言理解数组、递归、循环见过一点基础概率更好。就算概率基础一般课程也会从随机化算法的使用场景逐步带出来。2. 分治法把“递归能跑”变成“复杂度可证明”2.1 分治三步和归并排序的标准示范分治法的套路很清晰分解、递归、合并。把一个规模为 n 的问题拆成几个规模更小的子问题递归解决子问题再把子问题的结果合并成原问题的答案。归并排序是这门课里最先出现的经典示范。代码结构几乎是分治法的标准模板def merge_sort(arr): if len(arr) 1: return arr mid len(arr) // 2 left merge_sort(arr[:mid]) right merge_sort(arr[mid:]) return merge(left, right) def merge(left, right): res [] i j 0 while i len(left) and j len(right): if left[i] right[j]: res.append(left[i]) i 1 else: res.append(right[j]) j 1 res.extend(left[i:]) res.extend(right[j:]) return res这里的核心步骤是merge。两个有序数组合并时只需要线性扫描一遍就能得到新的有序数组也就是合并一次代价是 O(n)。为什么归并排序一定是 O(n log n)因为每次递归都把数组分成两半递归树的层数大约是 log n每一层所有子数组的合并加在一起总共需要处理 n 个元素。层数乘每层代价就是 O(n log n)。这门课最可贵的地方就在这里。它不是让你记住“归并排序是 O(n log n)”而是让你能从递归结构里把这个结论推出来。以后哪怕遇到一个完全没见过的分治算法你也能用同样的方式分析它。2.2 递推式与主定理给复杂度一个系统化框架分治算法的时间复杂度通常都能写成一个递推式。归并排序对应的是T(n) 2T(n/2) O(n)意思是一个规模为 n 的问题拆成两个规模为 n/2 的子问题合并代价是 O(n)。课程里会系统化地处理这类递推式。最常用的工具是主定理。它把常见的分治递推式归成三类分别对应“递归代价占主导”“合并代价占主导”“两者相当”三种情况。比如说T(n) 2T(n/2) O(n)结果是 O(n log n)。T(n) 2T(n/2) O(n²)合并代价太贵结果会被 O(n²) 主导。T(n) 4T(n/2) O(n)子问题数量变多结果会被递归部分主导。注意不要把一个递推式硬套到主定理上。使用前先确认它是否满足 a≥1、b1、f(n) 渐近正这三条基本条件。不满足时展开递归树往往更稳妥。主定理的价值不是让你背三个结论而是给了一种判断分治策略是否合理的眼光。你看到一个递归算法先写递推式再用主定理判断复杂度再决定是否值得优化合并过程。这套流程在课程后面分析其他算法时会反复出现。2.3 分治不是万能合并成本才是真正的命门分治看起来通用很多问题也确实能用它解决比如逆序数对计数、最近点对、大整数乘法、矩阵乘法等。但它的适用边界很明显合并这一步必须足够高效。如果一个问题拆开之后递归部分很轻松但合并时要付出很高代价那整个算法就会被合并过程拖垮。最典型的就是上面举例的T(n) 2T(n/2) O(n²)主定理会直接告诉你结果差到不行。所以学这门课时真正要训练的不是“怎么拆”而是“拆完之后怎么合”。很多分治算法的难点都在合并逻辑而不是递归本身。这也是为什么课程会花大量精力在归并排序的合并步骤上——它简单但它是理解所有复杂合并逻辑的起点。3. 快速排序与随机化从“怕最坏”到“让最坏很难发生”3.1 轴点、划分与最坏情况快速排序是这门课的另一条主线。它的思路和归并相反归并是先递归再合并快排则是先划分再递归。一个常见的快速排序实现如下import random def quick_sort(arr, l, r): if l r: return p partition(arr, l, r) quick_sort(arr, l, p - 1) quick_sort(arr, p 1, r) def partition(arr, l, r): pivot_idx random.randint(l, r) arr[l], arr[pivot_idx] arr[pivot_idx], arr[l] pivot arr[l] i l 1 for j in range(l 1, r 1): if arr[j] pivot: arr[i], arr[j] arr[j], arr[i] i 1 arr[l], arr[i - 1] arr[i - 1], arr[l] return i - 1快速排序的平均复杂度是 O(n log n)但最坏情况是 O(n²)。最坏什么时候发生当每次选出的轴点都恰好是当前子数组的最小值或最大值时划分极度不平衡递归深度变成 n。如果你不理解这一点只在“排序好的数组”上测试代码会跑得很快一旦遇到已经有序的输入而轴点又固定选在某个位置可能直接退化到 O(n²)。面试里那个经典追问“快排什么时候最慢”考察的就是你有没有真正理解划分逻辑。3.2 随机化不是玄学而是可证明的不确定性既然轴点选不好会退化那怎么让最坏情况尽量不要发生课程给出的答案是随机化。随机选轴点本质上是在说我不再假设你的输入长什么样而是让算法内部引入一个随机选择。这样一来即使输入是“故意构造出来针对某个固定策略的最坏情况”由于轴点位置不可预测它也很难稳定触发最坏场景。这里有一层很容易混淆的概念期望分析和平均情况分析不是一回事。平均情况分析通常是对所有输入分布取期望隐含假设了输入的分布期望分析则是在最坏输入上对算法内部的随机选择取期望。随机化算法保证的是无论输入是什么你都可以信任随机选择的期望表现。换句话说随机化不是“靠运气”。它是在面对恶意输入时让算法获得一种可证明的稳健性。这个思想在课程后续的很多算法里都会复用也是为什么这门课要把“随机化”和“排序”放在同一个标题下。更稳妥地测试性能时不要把随机种子固定成同一个值跑多次然后取最好成绩。这样会掩盖真实分布。应该让每次运行使用不同的随机选择观察多次运行后的时间和结果分布。3.3 从排序延伸到选择问题快速选择的思路学会了划分之后课程往往还会往前走一步把排序方法延伸到更一般的选择问题。问题是这样的给定一个无序数组找出第 k 小的元素。最直接的方法是排序后取第 k 个复杂度 O(n log n)。但有没有可能更快快速选择算法给出了答案思路利用快排的划分操作每次把数组分成小于轴点和大于轴点的两半。如果第 k 小恰好落在某一半就只递归处理那一半另一半直接丢弃。这样期望复杂度是 O(n)而不是 O(n log n)。为什么因为每次只需要处理一侧代价序列大致是 n n/2 n/4 ...加起来收敛到 2n 左右。这个例子特别能体现课程的编排逻辑快排学到的不只是排序本身而是划分这个操作它可以被复用到选择问题、找中位数、Top-K 问题里。课程讲的不是一个孤立算法而是一套可迁移的方法。3.4 随机化思想的工程边界需要提醒的是课程里学习随机化算法和工程里最终采用什么实现是两回事。C 标准库里的std::sort并不是简单的随机化快速排序它通常是内省排序先用快速排序但一旦递归深度超过某个阈值就切换成堆排序避免最坏情况。工程库之所以这么做是因为它们还需要考虑确定性、可复现性、最坏时延和平台差异。纯随机化快排虽然在理论上很漂亮但在某些延迟敏感系统里随机选择本身可能带来不可预测性。所以学这门课理解随机化的证明和思想是第一位的到了工程落地你再结合稳定性、内存、时延、可复现性等约束去选合适的实现。课程教你的不是“所有场景都用快排”而是“在你决定用快排时你清楚它的风险在哪里”。4. 排序不只是排序比较下界与工程排序4.1 为什么 O(n log n) 是大部分排序算法的天花板很多人学过排序后会有个疑问能不能设计一个基于比较、但比 O(n log n) 更快的通用排序算法这门课会用决策树模型告诉你不能这是一个理论下界。思路大致是这样对 n 个元素做排序正确答案有 n! 种可能。任何基于比较的排序算法都可以看成是一棵决策树每次比较走一个分支最终到达某个叶子代表一种排列结果。树高是多少至少是 log₂(n!)。由斯特林公式log₂(n!) Θ(n log n)。这意味着归并排序、堆排序的 O(n log n) 不是“碰巧做到”而是基于比较的排序算法里最可能的结局。这个结论的意义在于当你在设计一个排序方案时如果继续用比较器就别指望在渐进复杂度上有奇迹。你真正能优化的地方是常数、空间、稳定性、缓存局部性或者是跳出比较模型。这个下界也给后续很多算法设计提了个醒如果某个问题能归约成排序那么它的下界也不会低。这种分析眼光比记住几个复杂度数字有用得多。4.2 工程里为什么会混合多种排序策略教科书中我们习惯于把排序区分成“插入排序”“快速排序”“归并排序”等独立算法。但真实的工程库几乎都是混合策略。原因是每种排序都有自己的强项组合起来才能覆盖不同场景。常见的工程排序实现包括算法平均时间复杂度最坏情况常见空间稳定性插入排序O(n²)O(n²)O(1)稳定归并排序O(n log n)O(n log n)O(n)稳定快速排序O(n log n)O(n²)O(log n)不稳定堆排序O(n log n)O(n log n)O(1)不稳定TimSortO(n log n)O(n log n)O(n)稳定注意这里写的是“常见空间”具体实现不同会有差异。为什么没有“万能排序”因为约束太多。稳定性、额外空间、最坏情况、输入是否接近有序、数据规模、比较器成本这些维度经常互相冲突。比如如果你需要稳定排序归并排序比快速排序更合适。如果内存极其紧张堆排序的 O(1) 额外空间很有吸引力。如果输入规模很小插入排序可能比快速排序更快因为它没有递归和复杂划分的开销。如果数据接近有序Timsort 会先识别出有序片段直接降低工作量。如果你最在意的是稳定性或内存占用那么“最牛”的算法并不存在只有当前约束下最合适的实现。4.3 从理论回归工程把课程知识落进真实代码学完课程里的排序理论之后建议你做一件事去读一门语言标准库的排序源码。比如 C 的std::sort或者 Python 内置的sorted背后的Timsort思路。你会发现工程排序根本没有完全照搬教科书算法。它会在小数组上切换到插入排序在快排递归过深时切换堆排在检测到接近有序的输入时走特殊路径。这些优化的目标不是改变渐进复杂度而是把常数压低、把缓存命中率提升、把稳定性补上。所以这门课学完之后不要停在“我能默写快排了”。再往前走一步思考一个问题如果我现在要给某个业务场景写一个排序函数我会怎么选数据量多大内存限制多少需要稳定吗会不会经常传入接近有序的数据这些才是工程里真正会遇到的排序问题。5. 学这门课最容易踩的坑以及我的学习建议5.1 只记结论不推过程我在刷题群里见过太多人能把“归并 O(n log n)、快排 O(n log n)、堆排 O(n log n)”背得很熟但是一旦题目变成“求一个分治算法的复杂度”或者“设计一个满足特定约束的排序方案”立刻卡住。原因就在于只记结论不记推导路径。课程里大量时间在讲递推式、递归树、主定理、决策树下界就是在帮我们把结论变成可以现场推导的过程。如果你只是快进看结论等于把最值钱的部分跳过了。我的建议是每学完一个算法先在空白纸上推一遍复杂度。推不出来就回头重看。第一次会很难但推个三四个算法之后你会发现自己对“为什么是 O(n log n)”不再依赖记忆。5.2 跳过概率分析随机化就白学了随机化算法这一块很多人的学习方式是知道随机选轴点能让快排避免最坏情况就行了。但这远远不够。因为随机化不只在快排里出现。后续课程里的哈希表、随机算法、概率分析都会延续这里的概念。如果这一章你只记住了“随机选轴点”到后面看 Las Vegas 算法、蒙特卡洛算法时会更吃力。学习随机化部分时最该掌握的是两件事期望分析为什么成立最坏输入下算法内部随机选择的期望代价可控。随机化和“平均情况”的区别课程强调的不是“假设输入随机”而是“不假设输入但让算法自己引入随机性”。把这两点想透快排的随机化就不仅是一个技巧而是一种可迁移设计思想。5.3 写了代码却不验证一套快速排查链路很多学员在课程作业里写完快排或快速选择一旦结果不对就不知道从哪里下手。这里给你一条可复现的排查链路先看结果错在哪里。是全部乱序还是只有某一段乱序或是极少数元素错位全部乱序多半是划分逻辑整体写错局部乱序多半是区间边界处理错误。再看划分过程。在小数组上打印每次partition后的数组检查轴点是不是落到了正确位置。再看递归边界。左右区间的起点、终点是否正确有没有漏掉中间元素或重复处理同一个位置再看随机数逻辑。随机轴点选在哪个区间是否真的覆盖了当前子数组最后构造特殊输入。空数组、单元素数组、全部相同元素、升序数组、降序数组每个都能暴露不同问题。这五步可以反复用。每次写新的分治或随机化算法都可以按这个顺序做一轮自检。不要追求一次性把一遍写过。第一次实现能在小数组上跑对就已经赢过很多人。复杂算法原本就是靠反复调试才稳定的。5.4 建议采用的三步学习法课程内容不少我建议你按“三步法”来消化第一步把伪代码变成可运行代码。不能只看不写。建议自己先实现一版哪怕不优化先保证结果正确。第二步用手工推演一个中小规模例子。比如让归并排序跑在[3, 1, 4, 1, 5, 9, 2, 6]上把每一层递归的结果画出来。这一步能让你真正理解递归过程。第三步做小规模实验观察增长率。分别让 n 取 100、1000、10000、100000记录运行时间画出曲线和理论复杂度对照。理论说 O(n log n)实测曲线就应该是那个形状。如果偏差很大回去查代码或测量方法。这套流程不只适用于这门课。以后学动态规划、图算法、贪心算法你都可以用同样的“实现、推演、实验”三步走。6. 这门课真正长期的价值不是算法是算法思维6.1 建立“先分析、再动手”的习惯这门课最让我受益的不是后来面试时能答出快排的期望复杂度而是养成了一种条件反射拿到一个算法方案先问它有没有复杂度分析再问它最坏情况是什么最后才决定要不要用它。这个习惯在工程里同样重要。设计一个接口、选择一种数据结构、评估一次批量任务的性能背后都需要这种分析意识。你可能不会天天手写快排但你每天都在做“哪个方案更合适”的判断。算法思维训练出来的正是这种判断力。6.2 在面试、工程和后续研究里这门课分别留下什么面试里分治、排序、随机化是高频考点。归并排序的合并思想会延伸到链表排序快速排序的划分思想会延伸到 Top-K 问题、数组中的第 k 大元素随机化思想会延伸到蓄水池抽样、随机打乱等场景。工程里这门课的价值会更隐性。你未必会自己实现一个排序算法但你会更理解为什么标准库的排序有时快有时慢为什么比较器本身也能成为性能瓶颈为什么稳定性在业务排序里是一个必须明确的规则。如果之后要走研究路线这门课打下的分析基础更重要。后续算法专项中的图算法、贪心算法、动态规划、NP 问题都需要你能够流畅地推导复杂度、分析最坏情况、理解随机化的意义。分治和随机化不是独立知识点而是整个专项后续内容的两根支柱。6.3 下一步可以往哪里延伸如果你把这一部分学扎实了下一步可以往几个方向延伸继续学算法专项的后续课程看图和贪心问题是怎样复用这些分析工具的。开始刷算法题专门挑选分治和排序相关的高频题验证自己的复杂度分析能力。读一门语言标准库的排序源码把课程里的理论实现和工程实现的差距补上。如果对随机算法特别感兴趣可以再深入哈希、概率数据结构、随机化图算法等领域。这些方向都有一个共同前提你必须真的把分治递推、主定理、期望分析这些基础过一遍。这也是为什么这门课值得慢慢学而不是赶进度。如果让我用一句话总结这门课带来的东西我会说它不是让你记住几个排序代码而是让你有底气地写出“这个算法为什么这样设计它最坏会怎样以及你如何让最坏情况不容易发生”。把这份底气带到后续每一个算法、每一段工程代码里才是这门课真正值得的地方。如果你正准备开始别急着赶进度。第一个分治法、第一道递推式、第一次用随机化让程序在坏输入面前依然从容都值得你慢慢消化。