
简介面向数据结构学习者与计算机专业师生的英文教学课件以排序Sorting为核心专题系统讲解排序基本概念、稳定性规则与比较函数设计重点剖析插入排序、冒泡排序、选择排序三种经典简单算法适合作为课堂讲义、自学笔记或数据分析、大数据与数据挖掘方向的基础参考资料。资源为单个PDF文件容量449KB内容采用英文原版排版层次清晰便于直接阅读、投影或打印。目前已有114人浏览学习。课件从任意排列到有序序列的重排目标切入逐一说明各算法的执行过程、适用场景与时间复杂度并延伸讨论升序/降序、相等键值、非数值数据排序等实战议题同时借助二分查找、最近邻对、元素唯一性、频率分布等应用示例直观展示排序为何被誉为最基础的算法问题帮助读者快速搭建排序算法知识框架为后续攻克高效排序方法及大数据处理场景奠定扎实基础。1. 拿到 22_sorting_01.pdf 这份课件先别急着翻页sorting 到底在数据结构里承担什么角色一份命名为“22_sorting_01.pdf”的英文教学课件几乎可以肯定是某门数据结构与算法课程里排序专题的第一讲。编号 22 通常意味着这是第 22 讲sorting_01 则说明排序不是一个讲座能讲完的内容后续还会有快排改进、堆排序、线性排序等续集。这份课件面向的读者是正在学数据结构、准备考试或准备面试的从业者——也包括那些在工作中写业务代码写久了、想回头把排序这块地基重新夯实的人。排序算法在数据结构里的位置很像“集大成者”它把数组、链表、递归、分治、复杂度分析、稳定性这些前面学过的概念一次性串起来。你学链表时觉得没什么用学递归时觉得抽象到了 sorting 这一讲这些东西全都要上场。课件通常会用一页动画或一张对比表告诉你不同算法的比较次数和移动次数但真正的价值不在于记住那张表而在于理解“为什么同一个问题不同算法的差距能差出几个数量级”。这篇笔记就按一份典型 sorting_01 课件的推进顺序带你把它讲透、能复现、敢说“我真会了”。2. sorting 课件的主线从无序数组到有序数组课件里藏着四种典型套路一份 sorting_01 课件通常不会一上来就给快排它会先让你看几个“直觉型”算法再慢慢引出分治思想。这个过程不是浪费时间它对应着一类很重要的工程思维先保证正确再谈效率。下面这四类算法基本覆盖了课件前 40 页的绝大多数内容也是数据结构考研、数据结构408和面试手撕代码时最常被问到的范围。2.1 选择排序和冒泡排序课件里的“暴力美学”选择排序和冒泡排序是课件最喜欢拿来开场的两个算法因为它们几乎不需要动脑选择排序每次从未排序部分挑出最小值放到前面冒泡排序反复扫描相邻元素把大的往后“冒”。它们的共同点是时间复杂度稳定在 O(n²)但行为细节差别很大课件会在旁边放一张比较次数的表来提醒你选择排序的比较次数永远是 n(n-1)/2而冒泡排序在数据基本有序时可以提前退出。这个“提前退出”的判断在课件里通常以一个小脚注出现但实际工程里很多排序优化都从这里借了思路。我认为这里最值得跟课件对着学的是不要只看动画效果要自己数数。比如 n6 的数组选择排序第一轮要比较 5 次选出最小值第二轮比较 4 次……每轮都做完整扫描。冒泡排序如果一轮扫描没有发生任何交换说明数组已有序可以 break。课件把这种优化叫“short-circuit”不少学生容易忽略它觉得反正最坏复杂度都一样。但在近乎有序的场景里这个小小的提前退出能把实际耗时从 O(n²) 降到接近 O(n)我在给业务代码做局部排序时经常用到这个特性这一点后面讲验证时还会展开。2.2 插入排序为什么部分有序时它最能打插入排序在课件里的篇幅通常不多但它被反复提起因为它有个其它 O(n²) 算法不具备的优势对“几乎有序”的数据非常友好。它的思路像打扑克牌理牌把新元素向已排序部分逐个比较后插入。最好情况下只需要 O(n) 次比较最坏情况才退化成 O(n²)。课件里通常会强调它的“在线性”——可以边读数据边排序不需要一次拿到全部数据。我个人觉得插入排序是 sorting 课件里性价比最高的一个算法也是新手理解“算法对数据分布敏感”这件事的第一课。快速排序、归并排序再快也无法利用“数据已经大体有序”这个信息而插入排序可以。正因如此很多工程排序实现会在递归到较小规模时切换成插入排序这个叫“阈值优化”的做法在很多成熟库的源码里都能看到。课件里如果不提这一点你可以自己补上——它是从理论走向工程实践最短的一个桥梁。2.3 归并排序分治思想的样板片段分治是数据结构与算法里最重要的范式之一而归并排序是课件里第一个完整展示“分、治、合”三步的案例。它把数组从中间切开递归排序左右两半再用一个合并过程把两个有序子数组合并成一个完整有序数组。归并排序最吸引人的地方在于它稳定且在任何数据集上都能保持 O(n log n) 的复杂度代价是需要 O(n) 的额外空间。课件的合并过程通常是整个文档里最长的伪代码块之一因为要同时维护两个子数组的游标还要考虑某一半边先走完时的收尾处理。这里有一个很典型的边界问题合并时如果左侧耗尽要把右侧剩余元素全部拷回去如果右侧先耗尽则相反。很多读者第一次实现时容易把“比较后拷贝”和“剩余元素收尾”混在一起写结果越界。课件会用一个带颜色的动画来演示这个过程但动画看懂了代码写不对的情况依然普遍。我的建议是不要跳过伪代码的角落——也就是那两个 while 循环它们才是归并排序真正的考点。2.4 快速排序课件与工程实现之间差在哪如果 sorting_01 只讲一个高级排序那大概率是快速排序。课件会给你展示一个围绕 pivot 划分数组的过程左边放小于 pivot 的右边放大于 pivot 的然后递归处理左右两侧。快速排序平均复杂度是 O(n log n)而且原地划分时额外空间很小所以长期霸占通用排序的首选位置。但课件里通常会用小字标注一句很重要的话最坏情况是 O(n²)。触发条件最常见的是“数据已经有序且每次取第一个或最后一个元素当 pivot”。快排在课件里的实现版本五花八门最常见的教学版本是 Lomuto 分区——用一个游标从左向右扫把小于 pivot 的元素换到前面来。工程实现里更常见的是 Hoare 分区两个游标从两端向中间移动交换逆序对。课件不一定会同时给出两个版本但你一定要知道它们的差异Lomuto 简单、容易讲清楚但交换次数多Hoare 平均更快但循环条件更容易写错尤其是两个游标相遇时的处理。许多数据结构c语言版教材在讲快排时也常混用这两个版本给读者造成了不小的困惑。如果课件只给了一个版本另一个版本可以作为自主动手实验的内容去实现这才是把课件读活的方式。3. 把课件的伪代码翻译成能跑的代码一份最小可复现的作业模板课件里的伪代码是给人看的不是给机器跑的。把伪代码变成真实代码是数据结构实验报告、期末考试和面试的共同要求。这一章给出一套可以直接在本地跑起来的最小实现重点是处理伪代码里最常见的“1-based 下标”问题以及选择排序、插入排序、归并排序和快速排序各自容易出错的循环边界。3.1 课件常见的 1-based 下标怎么安全换成 0-based英文教材和课件大量使用 1-based 下标描述数组因为这样写伪代码最接近人的直觉第一个元素是 A[1]最后一个元素是 A[n]。但 C 语言和 Python 的数组是 0-based直接照抄伪代码必然出问题。最常见的就是快速排序的划分函数伪代码写的是 for j p to r-1翻译时把循环边界算错少处理一个元素或者数组越界。我一般的做法是先在草稿纸上把“伪代码下标”和“实际下标”的映射写出来如果伪代码区间是 [p, r]那么实际区间是 [p-1, r-1]。归并排序是重灾区因为它要计算 mid (p r) / 2在 1-based 下这个公式得到了正确的分割点但换成 0-based 后mid 要落到左半边的最后一个元素也就是 (left right) // 2 在 Python 里的行为是向下取整大多数情况下能对上但如果你用 (left right 1) // 2就会出现左半边多一个元素、右半边少一个元素的微妙失衡。这个失衡不影响正确性但会让递归深度分布不均极端情况下可能让快排在特定数据上表现异常。所以翻译伪代码时我的原则是先把伪代码中的每个区间端点都写出来再逐行对照翻译不要边看边写。3.2 一个能跑的最小实现选择、插入、归并、快速排序的 Python 版本下面给出一份 Python 实现它对应课件里最主流的四种写法。代码刻意保持教材风格方便你拿它跟课件伪代码一行行对照也方便移植成 C 语言版练习。def selection_sort(arr): n len(arr) for i in range(n - 1): min_idx i for j in range(i 1, n): if arr[j] arr[min_idx]: min_idx j arr[i], arr[min_idx] arr[min_idx], arr[i] return arr def insertion_sort(arr): for i in range(1, len(arr)): key arr[i] j i - 1 while j 0 and arr[j] key: arr[j 1] arr[j] j - 1 arr[j 1] key return arr 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 def quick_sort(arr, low, high): if low high: p partition(arr, low, high) quick_sort(arr, low, p - 1) quick_sort(arr, p 1, high) def partition(arr, low, high): pivot arr[high] # 课件常用的最右元素 i low - 1 for j in range(low, high): if arr[j] pivot: i 1 arr[i], arr[j] arr[j], arr[i] arr[i 1], arr[high] arr[high], arr[i 1] return i 1这段代码里选择排序的range(n - 1)是内层循环的边界来源当 i 到倒数第二个位置时最后一个位置自动成为剩余最小值无需再比。插入排序的while j 0 and arr[j] key把“移动元素”和“找插入点”在一个循环里完成退出循环后arr[j 1] key把当前元素放回正确位置注意这里的j 1不能写成j。归并排序的mid len(arr) // 2就是前面说的 0-based 下的分割点左半边取[:mid]右半边取[mid:]两边长度差最多为 1。快速排序的partition返回的位置 p 已经是最终位置所以递归区间必须排除 p 本身也就是[low, p-1]和[p1, high]。如果想改成 C 语言版本主要的差异在于数组作为指针传递时需要额外传入长度或区间参数merge 函数必须预先分配一块临时数组而不是在函数内部反复创建。这两种差异恰好是数据结构c语言版教材里最常见的两个考点——指针数组参数和临时空间的分配时机。3.3 main 函数怎么写才不算糊弄比较计数与数据构造很多读者照着上面的代码写完四个排序后直接在 main 里跑一个print(sorted_arr)就交差了。但数据结构实验报告的要求通常不只是“排序结果正确”还要输出比较次数或交换次数。下面这段代码演示了如何用闭包来统计比较次数完全不改动排序函数本身的结构def make_comparator(): count 0 def compare(a, b): nonlocal count count 1 return a b compare.count count return compare这里nonlocal count是 Python 3 的语法它让内层函数可以修改外层函数的局部变量。调用compare(a, b)每次都会把计数器加一然后返回a b的结果。把参数换成、、可以统计不同语义下的比较次数。问题在于非局部变量的读取compare.count在你最终打印之前不会自动更新所以更稳妥的做法是返回一个包含计数器的字典或者直接用一个列表[0]当作可变计数器。构造测试数据时我建议至少准备四种输入随机数组、有序数组、逆序数组、近乎有序数组。前两者用于观察最好和最坏情况后两者可以清晰地看出插入排序相对其它 O(n²) 算法的优势。数据量取 1000、2000、4000、8000 四档每档跑多次取平均避免被系统调度和 Python 解释器波动干扰。这样构造出来的实验数据才是数据结构实验报告里真正有分量的内容。4. 课件里没展开说的复杂度细节稳定性、原地性与比较下界sorting_01 课件的主线是算法流程但考试和面试问得更多的是一些藏在图表和脚注里的结论稳定的定义、空间开销的差异、为什么比较排序最快也只能做到 O(n log n)。这一章把这些细节补齐你会发现很多动画演示里根本没有体现的信息恰恰是区分“背过”和“理解”的分水岭。4.1 从课件图表反推复杂度动画里看不到的比较次数课件里的排序演示通常以横条高度代表数值每做一次比较就把两个横条高亮。动画节奏很快你记住了“它在交换”“它在插入”却很难记下比较次数的变化趋势。我建议在复现时给每个算法装上计数器然后看同一组随机数据下的比较次数选择排序稳定在 n(n-1)/2插入排序在随机数据下大约是 n²/4归并排序大约是 n log n快速排序在随机数据下也接近 n log n 但常数通常比归并小。理解这个“常数”很重要它解释了为什么快速排序在平均意义上比归并排序快——两者复杂度相同但快排的比较次数和交换次数更少。课件里那张对比表只写“O(n log n)”不会告诉你常量因子但实际工程选型时常量因子经常比复杂度级数还关键。这也是为什么很多考研数据结构和数据结构408的题目会拿“比较次数”设问它考的不是背复杂度而是读得懂算法的过程。4.2 稳定排序在工程里的真实价值先按城市排序再按时间排序稳定性的定义一句话就能说清如果两个元素键值相等排序后它们的相对顺序保持不变。但课件里往往只给一个“是否稳定”的表不讲为什么在乎这个性质。真实业务场景里常见的做法是先用次要字段排序再用主字段做稳定排序这样主字段相同的一组数据里次字段仍然保持有序。比如快递列表先按时间排序再按城市做稳定排序就能得到每个城市内部按时间排列的完整列表。如果第二次排序不稳定城市相同但时间乱套数据就毁了。到具体算法上归并排序在合并时如果写成left[i] right[j]取左侧元素就能保证稳定性。只要把这个条件改成稳定性立刻破坏——因为左右子数组所有元素都来自原数组左半边的相对顺序而相等的右半边元素会被排到左侧元素前面。快速排序天然不稳定因为划分过程会把 pivot 与比它大的元素做跨越式的交换。选择排序也不稳定数组 [5a, 3, 5b, 1] 第一轮把 1 和 5a 交换后两个 5 的相对顺序就反了。这个例子我建议你自己画一下比背任何定义都有用。4.3 比较排序的 n log n 下界为什么课件非要讲这个证明课件在一页角落里往往会放一个决策树用来解释一个结论只通过两两比较来决定有序性的排序算法最坏情况至少需要 log2(n!) ≈ n log2 n 次比较。这个下界说明归并排序在很多数据集上已经逼近理论极限想突破它必须放弃“纯比较”这个前提。这句话对工程选型的影响远超想象如果你想排序的数据有额外结构——比如整数取值范围固定、字符串长度固定那么基数排序或计数排序可以做得比 O(n log n) 更快。课时有限许多课件对决策树的证明只是一笔带过但数据结构408里却可能出选择题。我的经验是你不需要完整复述证明过程但要能说清楚两个关键词“最坏情况”和“两两比较”。任何基于比较的排序都会得到一个从输入排列到叶子节点的映射叶子节点数至少要覆盖 n! 种输入排列所以树的高度必然不小于 log2(n!)。这棵树的想象是面试官非常爱考的说法比硬记结论更让人信服。4.4 一份能当面试提纲的算法对比表课件最后通常会有一张汇总表这里给出我自己的整理版本它在考研数据结构复习时可以当纲要在面试前也可以当速查表排序算法平均复杂度最坏复杂度额外空间稳定性选择排序O(n²)O(n²)O(1)不稳定插入排序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(log n)这是递归栈的深度不是额外数组。如果课件写成 O(1)那是把原地交换误认为是零额外空间忽略了递归栈——这个概念在考试里经常设坑。而归并排序的 O(n) 空间来自合并时的临时数组工程上可以通过复用同一块缓冲区来降低分配开销但理论复杂度不会变。5. 照着课件复现排序时最容易踩的 5 个坑从下标到递归栈很多读者觉得四个排序算法都写出来了实验报告也交了但换一道题、换一组数据就翻车。这里整理我在带实验课和帮同事查代码时最常见的五个问题每条都按“现象 → 原因 → 解决”的顺序写你可以直接对照自己的代码排查。坑 5.1C 语言版把 0 下标空出来结果循环条件全线错位现象自己在数组下标 1 到 n 存放数据0 位置空着不用但快速排序的递归区间写成了quick_sort(arr, 1, n)和quick_sort(arr, 1, p-1)在某个数据规模下栈溢出。原因partition 函数内部以low为起点扫描如果 low 传 1 而数组有效范围是 1 到 n那么arr[0]被忽略没问题但如果 low 传 0arr[0]是未初始化数据分区结果就会包含垃圾值。解决要么统一从 0 下标开始包括 0 位置要么从 1 下标开始且所有循环条件写 n而非 n。关键是全代码统一约定不要一半 0-based 一半 1-based。坑 5.2动画看多了以为选择排序是稳定的现象写完后跟人辩论选择排序到底稳不稳定各说各有理。原因课件动画里两个相同值的元素在交换时看不出顺序因为动画里数值相等时横条高度相同。用 [5a, 3, 5b, 1] 这个反例一跑就清楚第一轮 1 与 5a 交换后数组变成 [1, 3, 5b, 5a]两个 5 的相对顺序已经翻转但稳定排序要求 5a 仍在 5b 前面。解决把这个反例记下来判断稳定性时不要凭直觉用反例验证。坑 5.3快速排序拿最后一个元素当 pivot遇到有序数组直接 O(n²)现象随机数据上跑得好好的改成升序数组后运行时间暴涨。原因pivot 恒为最后一个元素有序数组的 partition 每次只能把区间长度减一递归深度变成 n时间复杂度退化成 O(n²)。课件动画里数据是随机的所以看不到这个问题。解决最常见的做法是随机选 pivot比如pivot arr[random.randint(low, high)]然后与最后一个位置互换更工程化的做法是三数取中取 low、mid、high 三个位置的中位数当 pivot。这样有序数据也能保持接近 O(n log n)。数据结构408里这道题考得不算多但面试手撕快排时几乎必考。坑 5.4归并排序在递归里反复 new 临时数组内存使用翻倍现象数据规模到几十万时程序内存占用飙升甚至 OOM。原因每个递归层级都创建一个临时数组层级深度 O(log n)总分配次数和总内存峰值远超 O(n)。课件伪代码里写“使用临时数组 B”但没强调 B 应该分配一次还是每层分配一次。解决我一般会预先分配一个与原数组等长的temp数组把它作为参数传给 merge 函数merge 内只在这个 temp 里做拷贝和回写。这样空间复杂度保持在 O(n)且不会因频繁分配拖慢速度。这个问题在内存受限的嵌入式环境下尤其致命做嵌入式排序的同事应该深有体会。坑 5.5大 O 记法把平均情况说成最坏情况被面试官一次性问穿现象被问到“快排时间复杂度是多少”时回答 O(n log n)接着被追问“最坏呢”就卡住。原因很多网课和博客习惯用平均复杂度代替全部复杂度课件里表格虽然写了 Best/Average/Worst 三列但阅读顺序让大家只看平均列。解决把四个算法的三列全部背熟并且理解为什么归并排序的最好和最坏都是 O(n log n)——它的划分不依赖数据分布合并始终要做 n 次比较。这个问题也是考研期末复习里最常见的概念辨析题之一。记法本身没有错但要说清楚是谁的复杂度。6. 进阶用对比计数验证渐近复杂度把课件里的理论换成自己的判断力课件教你“归并排序是 O(n log n)”这是别人告诉你的结论。真正能让你在面试和真实工程里拍板的是自己亲手测出来的数据。这一章给出一个很轻的验证方法只统计比较次数不看墙钟时间在四种输入分布下看不同规模 n 的计数增长趋势。用比较次数而不是运行时间是为了排除机器差异、语言解释器开销和系统负载的干扰。比较次数是算法层面的度量同一份代码在任何机器上跑结果完全一致。下面这段 Python 代码在上一章的基础上给插入排序加了计数器你可以照这个模式扩展其它算法def insertion_sort_counted(arr): n len(arr) compare_count 0 for i in range(1, n): key arr[i] j i - 1 while j 0: compare_count 1 if arr[j] key: arr[j 1] arr[j] j - 1 else: break arr[j 1] key return arr, compare_count这个实现里compare_count只统计实际发生的元素比较j 0属于循环条件判断不计入比较次数。细心的读者会发现这个统计口径与课件伪代码里的“一次比较”含义基本一致——只要执行到arr[j] key就算一次。如果你想把课件教学版的插入排序也统计进去也就是每次循环都做两次判断那计数结果会略大但增长趋势不变。接下来跑四组数据n 取 1000、2000、4000、8000每组数据分别使用随机排列、升序、降序、近乎有序四种构造。对插入排序而言升序数据的比较次数是 n-1近乎有序数据的比较次数与逆序对数量成正比随机数据则接近 n²/4。你把计数结果画在双对数坐标里升序曲线斜率接近 1随机和降序曲线斜率接近 2这就是复杂度最直观的证据。对归并排序做同样的验证你会发现四种输入下比较次数几乎一样曲线斜率稳定在 1.0 附近也就是 O(n log n)。而快速排序在随机输入下斜率接近 1但升序输入时如果不加随机 pivot曲线斜率会变成 2——这就是退化的实证。我手头给新同学演示时最常看到的表情是“原来课件没说谎但也没说全是骗人的”。这些实验做完后你对排序的理解就不停留在口诀层面了。再往后你可以在自己的工程代码里复用一个技巧当待排序区间长度小于 16 时从递归排序切换成插入排序。这个阈值不是拍脑袋定的它来自实测——归并和快排递归到小规模时函数调用开销和局部性损失开始超过插入排序的 O(n²) 成本。你可以把阈值从 8 到 32 逐档试一遍对比计数和墙钟时间找到你当前语言和机器上的最优值。这个优化习惯我保持了很多年几乎每次排序都受益。最后说一句我的真实感受课件会过期但方法不会。无论是《数据结构与算法分析》还是英文原版课件核心永远是那几件事——把伪代码画成图把图写成代码用计数器验证复杂度用反例验证稳定性。我自己当年做实验时也踩过 5.3 里那个快排退化的坑后来把三种 pivot 策略都写了一遍才彻底想明白。这份 22_sorting_01.pdf 如果你能照着这个流程读完收获会比只翻动画大得多。希望帮到你。本文还有配套的精品资源点击获取