午后两点,城市图书馆的借阅室一片狼藉——一万本图书散落在推车上,管理员小林要按书名字母顺序把它们全部归位。她望着面前的书山,深吸一口气:整理全部一万本确实令人头大,但如果是让五位图书管理员分工整理五个书架,每个书架只负责两千本呢?分而治之,先拆散,再逐层治理,这就是计算机科学里最优雅的策略之一——分治算法。本篇文章的主角,快速排序与归并排序,正是分治思想在排序领域的两颗明珠,从图书馆的书架到顶级互联网公司的海量数据,它们在背后默默指挥着亿万次有条不紊的“整理动作”。
理解分治排序不仅是面试的敲门砖,更是你设计高效算法的思维底色,掌握它,你将学会把庞大混乱的难题,拆解成优雅有序的解法。
可视化演示
快速排序和归并排序是基于分治思想的高效排序算法,平均时间复杂度O(n log n)。本文讲解快速排序的分区策略... 切换模式查看不同维度。
本节配有交互动效(进入视口后自动加载,约 3KB)。
分治思想与排:核心原理 动态展示关键流程与数据流动 交互控件调节参数实时渲染 f(x) = transform(x) 核心概念的可视化呈现
分治思想起源
小林的图书馆里,馆长下达了死命令:三天内将一万本书全部按书名排序上架。一万本!小林盯着书堆,觉得这比自己活过的日子还多。但经验丰富的老馆员陈叔走过来,指着墙上的楼层分布图说:“我们不需要一次处理一万本。把书按字母区间分成十堆,你负责 A–F,我负责 G–M,小张负责 N–R……每堆最多一千本,然后再把十个书架按顺序接起来,不就成了?”小林恍然大悟——这就是分治思想:一个庞大到无从下手的任务,拆成若干个独立的小任务,各自解决后再把结果拼接回完整答案。
计算机科学中的分治算法正是这样的“图书馆楼层分配方案”。它有三个固定步骤:分解(Divide)——将原问题递归分成规模更小的子问题;治理(Conquer)——子问题小到可以直接解决;合并(Combine)——将子问题的解组合成原问题的解。排序领域里,快速排序和归并排序是这个三招套路的杰出弟子,它们都把“排好一万本”拆成“排好两千本”,再拆成“排好四百本”,直到每堆只剩一本书——一本书当然是有序的。
这个递推式展开后,每一层都要处理 n 个元素,一共有 $\log_2 n$ 层,所以总工作量就是 O(n log n)。小林后来发现,陈叔这个“分区分配”的策略,正是计算机里递归的雏形——领导把任务拆下去,基层把结果报上来,每一层都在做同样的三件事。
- 递推的骨架$T(n)$ 表示处理 n 个元素所需时间,它由 “两个规模为 n/2 的子问题” 加上 “一次合并的开销” 构成。
- 拆解两次$2T(\frac{n}{2})$ 指分治把原任务一分为二,每个子任务规模减半,两个子任务都要独立完成。
- 合并开销$O(n)$ 表示把两个有序半区拼成一个完整有序序列的线性代价——就像图书馆把两排已排序的书架首尾检查一遍。
快速排序原理
第二天,小林在 F 书架前面犯了难:这一堆书有两百本,如果再用“按字母分区”的办法,她需要额外找一张大桌子来放临时分区。有没有办法不用额外空间就完成排序?这时她灵机一动:从书堆里随手抽出一本《三体》,以“S”这个字母为基准(枢轴),把书名首字母在 S 之前的书全放到左边,在 S 之后的全放到右边。经过一轮“分区”,《三体》自己就落在了它最终该在的位置上!接下来,左右两堆书各自重复这个过程,每一轮选一本新“基准书”,把更小的书放它左边、更大的放右边,直到每堆只剩一本。
快速排序的核心正是这个“抽基准书”的分区(partition)操作。算法流程清晰:从待排序区间选择一个元素作为枢轴(pivot);然后遍历区间内所有元素,将小于等于枢轴的元素交换到左侧,大于枢轴的元素交换到右侧;这一步完成后,枢轴已经在整个数组的正确位置上,且左右两侧天然形成了两个待排序子区间——它们之间永远不需要再交换元素了。对左右子区间递归执行同样的分区,排序便像剥洋葱一样一层层完成。
小林发现,快速排序就像一个眼光独到的图书管理员:每次抽一本书出来做“标尺”,把书堆分成两半,然后对左右两半分别执行同样的套路。因为每一次分区都会把一个元素放到最终位置上,所以只要递归地做下去,不需要任何临时大桌子,整个书架就在原地被整理得井井有条。这也是它名字里“快速”二字的底气所在。
- 区间界定$A[l..r]$ 表示当前要排序的数组片段,$l$ 是左边界,$r$ 是右边界。
- 左区约束$A[l..q-1] \le A[q]$ 表示完成分区后,枢轴左侧的所有元素都不大于枢轴——相当于所有“S 之前的书”都堆在了《三体》左边。
- 右区约束$A[q] \le A[q+1..r]$ 表示枢轴右侧的所有元素都不小于枢轴——相当于“S 之后的书”都堆在右边。
随机化快排
可麻烦来了。小林发现,如果每次抽到的“基准书”恰好是当前书堆里书名最靠前的那一本,比如抽到《阿Q正传》,那么分区后左边空无一书,右边足足有 199 本——这哪里是分而治之,分明是把所有工作量都甩给了下一层!照这个运气,处理 n 本书要做 n 次分区,总共 O(n²) 次操作,跟最笨的冒泡排序一个档次,完全配不上“快速”的名号。小林意识到:枢轴选得好不好,直接决定快速排序的命运。
解决方案意外地朴素:既然无法预知哪本书适合做枢轴,那就从整堆书里随机抽一本。随机化快速排序在每次分区前,先从当前区间随机选择一个元素与最后一个元素交换,再以它为枢轴进行划分。这样一来,出现极端不平衡分区的概率被大大摊薄——虽然理论上仍可能抽到最坏情况,但对任何固定输入,算法的期望时间复杂度都是 O(n log n),并且不依赖于输入数据的初始分布。
小林把“闭眼抽书”的策略推广到整个图书馆后,再也没遇到“分区全偏到一边”的尴尬。随机化并没有让算法在每次运行都最快,但它保证了平均而言几乎永远处于高效状态。就像老话说的:与其赌运气碰上一个好枢轴,不如把好运气均匀地分给每一个可能的枢轴。
- 平均意义$\mathbb{E}[T(n)]$ 表示“随机选择枢轴”条件下处理 n 个元素的期望时间,是一种平均意义上的承诺。
- 等概率加权$\frac{1}{n}\sum_{i=0}^{n-1}$ 表示每个位置成为枢轴的概率都是 1/n,把所有可能的划分方式按概率加权求和——因为随机抽书,每本书被抽中的机会均等。
- 左右两边递归$T(i) + T(n-i)$ 表示如果枢轴排在第 i 个位置,左右两区间分别要处理 i 和 n-i 个元素。
- 结果承诺$= O(n\log n)$ 是这个加权和的最终解,即随机化后算法的期望复杂度稳定在对数线性级别。
归并排序合并
第三天的任务更难:馆长要求把所有书按作者姓氏稳定排列,也就是说,同一位作者的多本书,要按它们进馆的先后顺序排。小林试了试快速排序,发现它做不到——每次分区时,交换操作会把相同作者的书相对顺序打乱。一旁的陈叔笑了:“那就试试我的办法,先拆得碎碎的,再老老实实地合起来。”
归并排序的思路像一个严谨的档案馆管理员:先把整堆书从中间一分为二,递归地对左半堆和右半堆分别排序,最后将两个已经有序的半堆合并成一个完整的有序序列。合并动作使用双指针:两个指针分别指向两个半堆的起点,每次把较小的那本书放入结果数组,并前进对应指针,直到某一堆取完,再将另一堆剩余的书整体接在后面。因为两个半堆内部有序,合并时遇到相等元素总是先取左半堆的,所以相同作者的书会保持它们在原数组中的先后顺序——这就是稳定性的来源。
小林按照这个流程操作:先把 200 本书拆成两堆 100 本,两堆 50 本……直到每堆只剩一本,然后从最小的堆开始两两合并。整个过程没有“随机抽基准”的运气成分,每一轮合并都是踏踏实实的线性扫描,因此无论书怎么乱,归并排序一定在 O(n log n) 时间内完成。代价是她需要一张额外的长桌来摆放合并过程中的临时序列——这就是归并排序的空间开销 O(n),拿空间换来了稳定和确定。
- 基本情况判定$L[0] < R[0]$ 表示比较两个序列头元素,若左小则左头出列。
- 左头出列$L + \text{merge}(L[1:], R)$ 表示把左头元素放入结果,再对 L 去掉头部的剩余部分和完整 R 继续合并。
- 右头出列$R + \text{merge}(L, R[1:])$ 表示否则取右头元素,再对完整 L 和 R 去掉头部的剩余部分继续合并。
稳定性对比
小林现在面临一个选择难题:快速排序省空间但会打乱同作者书籍的相对顺序,归并排序稳定但需要额外空间。为了帮助她理解,陈叔做了一个小实验:取两本同作者的书,《呐喊》(上架编号 001)和《呐喊》(上架编号 002),在输入数组中它们一前一后。快速排序分区时,由于枢轴交换,编号 002 可能蹿到 001 前面;而归并排序合并时,遇到两本《呐喊》,双指针永远先取左半堆的 001,再取右半堆的 002,相对顺序纹丝不动。
稳定性(stability)是排序算法的重要性质:如果两个元素的排序键相等,排序后它们的相对顺序保持原样,则称该算法是稳定的。快速排序之所以不稳定,根源在分区操作中的“跨越式交换”——一个远在右侧的等值元素可能被交换到左侧等值元素之前。归并排序的稳定性则来自合并时“相等取左”的严格规则。在编程语言的内置排序中,Python 的 TimSort、Java 的对象数组排序都选择了稳定排序,正是因为现实场景(如按姓名排序后再按班级排序,希望保持第一次排序的相对顺序)频繁依赖这一点。
小林最终明白,稳定性和速度是两种不同维度的“品质”。快速排序就像一个手脚麻利但有点粗心的临时工,它能在原地快速完成任务,却可能打乱相同书籍的先后次序;归并排序则像慢工出细活的老员工,多占一张桌子但绝不乱序。实际工程里,是否需要稳定性往往成为选型的决胜因素,尤其是当待排序数据已经按某个次要键排过一次序时。
- 等值前提$a_i = a_j$ 表示两个元素的排序键相等,它们谁先谁后,单看键值无法区分。
- 原始顺序$i < j$ 表示在输入数组中元素 $a_i$ 位于 $a_j$ 之前——像编号 001 的《呐喊》排在 002 前面。
- 排序后保序$\text{rank}(a_i) < \text{rank}(a_j)$ 表示排序后 $a_i$ 仍然排在 $a_j$ 前面,即相对顺序未被破坏。
复杂度对比
陈叔拿出一张手绘的表格,把两种算法的时间与空间开销放在一起对比。小林看到,在平均情况下,快速排序和归并排序的时间复杂度都是 O(n log n),差别不大;但空间维度上,快速排序是“原地”算法,只需递归栈的 O(log n) 额外空间,而归并排序每次都开辟一个临时数组来合并,需要 O(n) 的额外空间。她想起昨天整理 F 书架时,快速排序就在书架本身上操作,而归并排序却把书拿到长桌上合并再放回来。
“所以没有免费的午餐。”陈叔总结道,“快排像在书架原地腾挪,省了桌子但怕遇到极端乱序;归并像搬到长桌上操作,多费空间但绝不出岔子。”小林点点头,她把这组复杂度对比记在笔记本上:如果数据量巨大且内存紧张,选择快速排序;如果稳定性和性能确定性更关键,归并排序是更稳妥的选择。
- 快排三态$O(n\log n)_{\text{avg}}$ 表示平均情况下的时间开销,$O(n^2)_{\text{worst}}$ 表示极端不平衡分区时的最坏时间,$O(\log n)_{\text{space}}$ 是递归栈深度。
- 归并恒定$O(n\log n)_{\text{all cases}}$ 表示无论输入如何,归并排序的时间开销都保持稳定,这是它的金字招牌。
- 空间花销$O(n)_{\text{space}}$ 表示归并排序需要与数据规模成正比的额外空间来存放合并结果。
最坏情况优化
小林看着表格里的 O(n²) 陷入了沉思:哪怕用了随机化,快速排序在极端情况下仍然可能退化,比如当数据量巨大且恰好每次枢轴都选中位数(或最小)时,递归树会退化成一条长链。陈叔告诉她,工程实践中有几招专门用来“抢救”快速排序的最坏情况。第一招是三数取中(median-of-three):从区间首、中、尾三个位置各取一个元素,以它们的中位数作为枢轴——这几乎不可能选到全局最小或最大,极大地避免了失衡分区;第二招是混合插入排序:当子区间长度小于某个阈值(如 10)时,改用插入排序直接完成,因为小区间上插入排序的常数开销远小于递归调用;第三招是尾递归优化:对递归过程中较大的子区间使用循环结构处理,从而将递归栈深度控制在 O(log n) 内,防止栈溢出。
这三招如同给快速排序上了三道保险:三数取中从源头把好枢轴关,插入排序给小区间提速,尾递归优化压低了调用栈的深度。小林在笔记本上写道:“优化不是花架子,而是针对已知弱点逐点设防。” 经过组合优化后的快速排序,在实际数据上几乎不会触发最坏情况,这也是为什么它成为多数通用排序库的底层默认选项。
- 三个候选$A[l],\; A[\frac{l+r}{2}],\; A[r]$ 分别是从左边界、中间位置、右边界取的三个元素,作为选枢轴的“选民”。
- 取中位数$\text{median}(\cdot)$ 表示从三个候选中选出大小居中的那一个,这个值天然避开了最小和最大。
- 结果赋值将选出的中位数作为枢轴,再做标准分区,退化概率被大幅压低。
场景与选型
最后一天,小林需要把整个图书馆的排序方案定下来。陈叔递给她一张决策清单:如果排序完全发生在内存中,数据量不大且内存紧张,首选快速排序——Java 的 Arrays.sort() 对基本类型就是这么做的;如果数据量大到放不进内存,需要借助磁盘分批排序,归并排序则是绝对的王者,它按块读入、排序、写回,再合并,这正是外部排序(external sorting)的标准玩法。稳定性同样关键:当需要按多个关键字依次排序时,使用稳定排序可以保证后一次排序不破坏前一次的顺序,这也是 Python sorted() 选择稳定算法的重要原因。
小林最终把方案拍板:内存内的日常图书借阅列表用快速排序,馆际互借的百万册备份数据用归并排序。两种算法殊途同归,都建立在“分而治之”这一条朴素智慧之上:把大问题拆到足够小,小问题就自然迎刃而解,而把解好的部分优雅地拼回去,就是整个算法最动人的一步。
- 快排条件$\text{memory-limited}$ 表示内存是瓶颈时,快速排序的原地特性能减少空间压力。
- 归并条件$\text{stable required}$ 表示业务要求排序保持相等元素的原始顺序时,选择归并排序。
- 外部数据$\text{external data}$ 表示排序数据无法完全载入内存时,归并排序天然支持分块读写,适合这类外部排序场景。
AI 应用场景
搜索引擎的文档排序
阶段:数据预处理与索引构建
解决的问题:搜索引擎每天需要抓取数亿网页并按键值排序建立倒排索引,传统冒泡或简单选择排序在百万级数据上耗时以小时计,无法满足索引天级更新的要求。采用归并排序后,可通过分块方式对海量网页URL排序,配合稳定的排序属性保证相同权重的文档相对顺序可预测。
具体案例:某电商搜索团队在构建商品索引时,先用归并排序按商品销量排序,再用另一键值(相关性分)做第二次稳定排序,从而让“销量较高”的商品在相关性相同时排在前面。
推荐系统的用户分层
阶段:特征排序与模型训练样本生成
解决的问题:推荐系统需要对海量用户按活跃度、消费能力等多维度打分并排序,数据量达到 TB 级且分布在多台机器上。使用快速排序的原地划分特性在单机内存内完成用户特征排序,结合三数取中优化,显著降低排序耗时。
具体案例:某短视频平台在晚间高峰前对 5000 万用户按实时活跃分排序,采用三数取中优化的快速排序,排序时长从 8 分钟压至 2 分钟,顺利赶在流量高峰前完成模型样本更新。
Python 演示
import randomimport time def quick_sort(arr, left=0, right=None): if right is None: right = len(arr) - 1 if left >= right: return pivot = arr[right] i = left - 1 for j in range(left, right): if arr[j] <= pivot: i += 1 arr[i], arr[j] = arr[j], arr[i] arr[i + 1], arr[right] = arr[right], arr[i + 1] mid = i + 1 quick_sort(arr, left, mid - 1) quick_sort(arr, mid + 1, right) def merge_sort(arr): if len(arr) <= 1: return arr mid = len(arr) // 2 left = merge_sort(arr[:mid]) right = merge_sort(arr[mid:]) merged = [] i = j = 0 while i < len(left) and j < len(right): if left[i] <= right[j]: merged.append(left[i]) i += 1 else: merged.append(right[j]) j += 1 merged.extend(left[i:]) merged.extend(right[j:]) return merged def benchmark(): n = 10000 base = list(range(n)) random.shuffle(base) arr1 = base[:] arr2 = base[:] t0 = time.time() quick_sort(arr1) t1 = time.time() merge_sort(arr2) t2 = time.time() print("QuickSort time: %.4fs" % (t1 - t0)) print("MergeSort time: %.4fs" % (t2 - t1)) assert arr1 == sorted(base) assert arr2 == sorted(base) if __name__ == "__main__": benchmark()
pivot = arr[right]每次都取区间最后一个元素做枢轴,简单直接,是分区的基础。if arr[j] <= pivot:把小于等于枢轴的元素逐步交换到左侧,完成“左边小右边大”的划分。arr[i + 1], arr[right] = arr[right], arr[i + 1]把枢轴放到最终位置,此刻它已经不再需要移动。left = merge_sort(arr[:mid])递归排序左半段,体现了分治的“拆解”动作。while i < len(left) and j < len(right):双指针合并:每次比较左右头部,取较小者放入结果。merged.extend(left[i:])一方耗尽后,把另一方剩余的有序序列整体接上,完成合并。
常见误区
误区网上都说快排叫“快速”排序,那不管什么数据,它都应该比归并快吧?我直接无脑选快排就行了。
正解快排的“快”是平均意义上的快。在处理近乎有序的数据时,若没有随机化或三数取中优化,快排会退化到 O(n²),远慢于稳定在 O(n log n) 的归并排序。就像小林抽基准书,如果总是抽中《阿Q正传》,分区就完全失衡。选型必须结合数据分布、稳定性需求和内存限制,不能只看名字。
为何常见:“快速”这个命名带有强烈的主观暗示,加上大多数教材展示的都是平均情况,初学者很容易把平均优势误当成绝对优势。
误区多开一个数组而已,反正现在内存都很大,O(n) 空间不算什么。
正解当数据规模达到千万级甚至亿级时,O(n) 意味着额外开辟与原始数据等大的内存,这可能导致内存翻倍甚至触发 OOM。归并排序每次合并都要创建临时数组,频繁分配与回收也带来额外开销。在实际的大数据排序场景(如外部排序),数据根本装不进内存,归并排序恰恰通过“分块读写+有序归并”在磁盘上解决问题,其空间代价是设计时无法回避的核心成本。
为何常见:算法课上测试数据只有几千个元素,空间差异不明显;但真实工业数据动辄 GB 级,O(n) 空间就变成了绕不开的瓶颈。
误区既然随机化能避免最坏情况,那随机化快排每次运行都应该比固定枢轴的快排更快。
正解随机化的作用是“摊薄”最坏情况出现的概率,让算法对任何输入都不再敏感,而不是让单次运行更快。它用一个随机数生成的额外耗时,换取了性能的确定性期望。实际中三数取中等启发式策略在多数场景下比纯随机更高效,因为随机数生成本身也有成本。小林后来发现,随机抽书虽然公平,但每次都“闭眼摸”也要花时间,不如先看头中尾三本书再定基准。
为何常见:人们常把“更安全”等同于“更快”,其实随机化解决的是方差问题,不是均值问题。
误区排序后值都一样,谁排前面不都一样吗?稳定不稳定有什么关系?
正解当元素只有一个排序键时,稳定性确实无关紧要;但当元素包含多个字段(比如先按班级排、再按学号排),稳定性就决定第二次排序能否保留第一次排序的顺序。例如先按“借阅次数”排序,再按“书名”排序,稳定排序能保证借阅次数相同的书之间保持书名字母序,而非稳定排序则会彻底打乱第一次排序的相对位置,导致两次排序的叠加效果失效。
为何常见:初学者在课本上接触的排序对象通常只是单个整数,看不出多键排序下稳定性的意义,因此天然低估它的价值。
面试题
Q1. 快速排序的核心思想是什么?
- A.每次选出最大元素放到末尾
- B.任选一个枢轴,将序列分为小于和大于枢轴的两部分,再递归排序← 正确答案
- C.将序列两两比较后交换相邻元素
- D.将序列分成两个子序列再交叉合并
答案:B。快速排序的本质是“选枢轴+分区+递归”:任选或按策略选一个枢轴,一趟分区后枢轴落在最终位置,左右两边递归执行同样流程。A 是选择排序的思路,C 是冒泡排序,D 更接近归并排序的描述。
Q2. 归并排序中“合并”两个有序数组的时间复杂度是?
- A.O(1)
- B.O(log n)
- C.O(n)← 正确答案
- D.O(n log n)
答案:C。合并两个有序数组时,双指针各自从头部扫描,每个元素被比较一次并放入结果数组,因此总操作与元素个数成正比,是线性时间 O(n)。O(n log n) 是整个归并排序的总复杂度而非单次合并。
Q3. 快速排序在什么情况下会退化到 O(n²)?
- A.数据量超过 100 万
- B.每次分区枢轴都恰好是当前区间的最小值或最大值← 正确答案
- C.数据已经全部随机打乱
- D.递归深度超过 log n
答案:B。当每次选取的枢轴都是区间的最值时,一侧没有元素,另一侧有 n-1 个元素,递归树退化成链表,比较次数逼近 n²/2。随机化或三数取中就是为了压低这种情况的发生概率。
Q4. 关于归并排序的稳定性,下列说法正确的是?
- A.归并排序是不稳定排序,因为合并时会发生交换
- B.归并排序是稳定排序,因为合并时相等元素总是先取左半部分← 正确答案
- C.归并排序的稳定性取决于输入数据是否有序
- D.归并排序的稳定性取决于枢轴的选择
答案:B。归并排序的稳定性来自合并规则的严格性:两个子序列头部元素相等时,取左子序列的元素。整个过程中没有跨距离的交换,所以相等元素的相对顺序得以保留。归并排序没有枢轴概念,稳定性与输入有序性无关。
Q5. 下列哪些策略可以优化快速排序的最坏情况?
- A.三数取中选枢轴← 正确答案
- B.子区间较小时改用插入排序← 正确答案
- C.对所有子区间都用递归处理
- D.对递归中较大子区间使用尾递归优化← 正确答案
答案:ABD。三数取中能避免枢轴取到极端值,混合插入排序减少小区间的递归开销,尾递归优化压低栈深度,这三者都是经典优化手段。C 选项“所有子区间都用递归”是普通实现,与优化无关。
Q6. 快速排序与归并排序在空间复杂度上的主要差异是?
- A.快排是原地排序,额外空间 O(log n);归并需要 O(n) 的额外数组← 正确答案
- B.快排需要 O(n) 空间,归并是原地排序
- C.两者都需要 O(n) 额外空间
- D.两者都不需要额外空间
答案:A。快排在原数组上交换元素完成分区,递归栈深度平均 O(log n);归并排序每次合并都要创建临时数组存放有序结果,累积需要 O(n) 额外空间。这一差异是选型时的关键权衡因素。
Q7. 在库函数如 Arrays.sort() 或 sorted() 的实现中,为什么有时选择归并排序而不是快速排序?
- A.归并排序是稳定排序,能保留相等元素的原始顺序← 正确答案
- B.归并排序在任何输入下都能保证 O(n log n)← 正确答案
- C.归并排序不需要额外空间
- D.归并排序在外部排序场景中天然支持分块处理← 正确答案
答案:ABD。库函数对对象排序时,稳定性是重要考量,归并排序天然稳定且不受输入分布影响;在数据量超过内存时,归并排序的分块思想也是外部排序的基础。归并排序需要 O(n) 额外空间,所以 C 错误。
Q8. 给定递推式 T(n) = 2T(n/2) + O(n),其解是?
- A.O(log n)
- B.O(n)
- C.O(n log n)← 正确答案
- D.O(n²)
答案:C。该递推式对应分治排序的模式:拆成两个 n/2 的子问题,合并开销 O(n)。按主定理展开,递归树每层总工作量 O(n),层数 log n,总复杂度 O(n log n)。这正是快排平均情况和归并排序所有情况的共同复杂度。
Q9. 关于“三数取中”选枢轴策略,以下说法正确的有?
- A.从区间的首、中、尾取三个元素,用它们的中位数作枢轴← 正确答案
- B.可以完全避免最坏情况 O(n²)
- C.能显著降低选到最小或最大元素的概率← 正确答案
- D.比随机化策略更差,没有任何优势
答案:AC。三数取中从三个位置取中位数,避免枢轴落在极端值上,从而让分区更均匀,C 正确;但它不提供数学上的“完全避免”,仍存在构造特殊输入使三数同为极端的情况,故 B 错误;D 显然错误,三数取中在实际中往往优于纯随机。
Q10. 外部排序(如对磁盘上的海量数据排序)通常选择哪种算法作为基础?
- A.快速排序,因为它是原地排序
- B.冒泡排序,因为实现最简单
- C.归并排序,因为天然支持分块读入、排序、合并← 正确答案
- D.插入排序,因为对小数据集高效
答案:C。外部排序的核心在于数据无法一次性载入内存,必须分成多个块,每块内部排序后写入磁盘,再通过多路归并得到整体有序结果。归并排序的分治合并结构正好匹配这一流程;快速排序的原地划分依赖随机访问完整数据,在磁盘场景下效率极低。