首页 / 资讯中心 / 文章详情

十大排序算法从原理到实战:复杂度、稳定性与选型指南

十大排序算法从原理到实战:复杂度、稳定性与选型指南 ★ FEATURED ARTICLE
排序这件事不夸张地说是算法世界里最“家常”的一类问题。业务系统里一个列表加载慢Java 后端的小伙伴可能下意识去查 SQL 慢日志前端同事往往先怀疑渲染管线但最后定睛一看常常是某个接口在对几千条数据做多层嵌套排序时用了 O(n²) 的写法直接把响应时间拉爆。所以这次我把十大排序算法从头到尾好好捋了一遍从原理、代码到工程里真正会踩的坑一次讲透。我假定看到这篇内容的你是这样的状态基本知道什么是时间复杂度和空间复杂度写过一点 Python 或 C但排序算法停留在“会用 sort()”的水平。没问题这正好是这篇的起点。全文不是教科书式的罗列而是按“怎么想 - 怎么写 - 为什么这么写 - 实际项目里该选谁”的顺序来讲读完后你能真正把它们用到代码里而不是背完就忘。1. 内容整体设计与思路拆解十大排序算法听起来吓人其实完全不需要一个挨一个死记。我见过不少初学者把它们当成十个孤立的算法去背结果就是背了冒泡忘了快排背了归并又和堆排搅成一团。真正有效的学习方法是先做个分类看清它们之间的血缘关系。根据排序过程中是否依赖元素之间的大小比较可以分成两大类比较排序和非比较排序。比较排序里又有两条分支一条是暴力枚举型的典型代表是冒泡、选择、插入它们的共同特点是代码简单、效率低适合在数据量很小的时候用另一条是分治跳跃型的典型代表是归并、快排、堆排它们利用了“一次比较能排除更多可能性”的思路把复杂度从 O(n²) 拉到了 O(n log n)。非比较排序则是计数、桶、基数这三位它们不比较大小而是利用元素本身的分布特征直接“放”到正确的位置上所以能把复杂度降到 O(n)但代价是对数据范围有严格限制。这样理解之后你会发现自己要掌握的其实不是十套独立逻辑而是三个核心思想暴力穷举、分治递归、按值分类。只要抓住这三个思想代码细节都只是它们的变体。另外多数教材按“难易程度”排序讲解但我更推荐按“思想递进”来学。先写一遍冒泡感受一下“交换相邻逆序对”是干什么再写选择理解“每一轮选一个极值放到位”和冒泡的本质差别在哪里然后写插入这时候你会发现插入排序特别适合“基本有序”的数据这个洞察后面在快排优化里还会再出场。这样学完基础三兄弟再看希尔、归并、快排、堆排你会明显感觉到它们身上都带着基础版的影子只不过加了“跳过无效工作量”的巧思。从实战角度看理解这些算法的第二个价值是帮你在面对不同数据特征时做出正确选型。处理“百万级整数且范围已知”时计数排序能甩快排几条街处理“几百条数据库记录”时插入排序反而因为开销小、缓存友好而表现不差处理“内存紧张但数据量大”时堆排或原地快排才是正解。这些判断力不是靠背复杂度表得来的而是靠真正动笔写过一轮之后形成的直觉。2. 简单排序三兄弟冒泡、选择、插入2.1 冒泡排序最直观但最容易写歪冒泡排序的直觉非常朴素从前往后走一遍相邻两个数如果顺序不对就交换。一轮结束后最大的数就像气泡一样浮到数组末尾。重复 n-1 轮就排好了。以下是标准写法def bubble_sort(arr): n len(arr) for i in range(n - 1): swapped False for j in range(n - 1 - i): if arr[j] arr[j 1]: arr[j], arr[j 1] arr[j 1], arr[j] swapped True if not swapped: break return arr这个实现里有两个细节值得注意。第一个是内层循环的边界n - 1 - i因为每过一轮末尾已经有 i 个元素是排好的没必要再去碰它们。第二个是swapped标志位它记录着这一轮有没有发生交换如果一轮下来一个都没换说明数组已经有序直接跳出循环。这个优化让冒泡排序在几乎有序的数组上能以 O(n) 时间跑完这是一个很多人忽略的“隐藏优势”。但冒泡排序有个天生缺陷交换太频繁。即使数组已经比较有序它大概率还是要交换很多次。在真实项目里它的定位往往是教学示范除非你明确知道数据量极小比如少于 50 条且有部分有序特征否则不建议上生产。提示写冒泡排序最容易出的 bug 是内层循环忘了减 i结果每一轮都把整个数组又扫一遍功能上没错但白白多出一倍操作量。面试手撕代码时这个细节也能看出来一个人是不是真写过。2.2 选择排序交换次数最少的分拣思路选择排序的思路更接近于人类整理扑克牌的习惯每次从剩下的牌里挑出最小的一张放到已排序部分的末尾。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选择排序最突出的优点是交换次数固定为 n-1这在“交换元素开销极大”的场景下是巨大的优势。比如你要排序的不是内存数组而是磁盘上的大数据块每次交换都意味着一次 I/O那选择排序的交换次数就比特意用冒泡划算得多。它的缺点是无论数据是否有序比较次数都固定是 O(n²)。也就是说它没有任何“提前终止”的优化空间。另外需要特别强调的是选择排序是不稳定的。举个例子数组[2a, 1, 2b]第一轮结束后最小的 1 被放到开头剩下两个 2 的相对顺序会因为 1 的位置变化而被打乱。实际项目中如果要保持相同元素的原有先后顺序选择排序直接排除。实操心得选择排序我平时用得很少但它在“要求交换次数最少”的嵌入式场景里非常香。之前做某个小型控制器的排序模块时数据写在较大容量的慢速存储上每次交换代价极高那时候我想起选择排序的交换次数恒定这个特点替换掉了原本的快速排序整体耗时反而降下来了。2.3 插入排序处理偏有序数据的隐藏王者插入排序的直觉是打扑克摸牌新拿到的牌从后往前找找到合适的位置插进去后面的牌整体后移一位。def insertion_sort(arr): n len(arr) for i in range(1, n): 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很多人小看插入排序但它有两个碾压级别的优势。第一个是“自适应”。如果数组基本有序它的内层 while 循环几乎不会执行相当于一个 O(n) 的算法。这个特性在工程上极其重要因为真实世界的数据往往带有很强的局部有序性比如新增一条记录、用户手动拖拽一行都是对已有列表做微调这时候插入排序远胜其他 O(n log n) 算法。第二个是“对小块数据的低开销”。一个归并或快排的递归调用即使只排 10 个元素也要承担函数调用、临时空间分配等固定开销而插入排序就是几行循环常数极小。所以很多工业级排序实现里都有一条“阈值规则”当待排序区间小于某个阈值比如 16 或 32时直接改用插入排序收尾不再递归。这个细节后面讲快排时还会再次出现。注意插入排序要小心“整体后移”这一步。如果你不小心把移动写成了两两交换那它就跑偏成了一种多层交换的低效版失去了插入思想的优势。正确写法是把当前元素存到 key 里再腾位置。3. 进阶跳跃希尔、归并、快速、堆排3.1 希尔排序分组的威力希尔排序本质上是对插入排序的升级。它的想法是插入排序在数据“大体有序”时效率很高那我就先想办法让数据变得大体有序再做最后一次常规插入排序。怎么快速让数据大体有序答案是分组。def shell_sort(arr): n len(arr) gap n // 2 while gap 0: for i in range(gap, n): temp arr[i] j i while j gap and arr[j - gap] temp: arr[j] arr[j - gap] j - gap arr[j] temp gap // 2 return arrgap 的选取决定了希尔排序的性能。上面代码用的是每次减半这是一种简单但不算最优的序列。业界还有 Hibbard 序列、Sedgewick 序列等可以让最坏情况达到 O(n^1.3) 甚至更好。不过需要泼盆冷水希尔排序现在出镜率不高因为它的时间复杂度和 gap 序列强相关理论分析复杂实用上也很难超越经过精心调校的归并和快排。但它的价值在于让你理解“预处理数据特征可以让简单算法变得威力很大”这个思路。3.2 归并排序稳定且可控的分而治之归并排序的思路是经典的分治法把数组从中间切成两半分别排序然后合并两个有序数组。合并这一步如同把两手已经理好的牌按大小合成一手线性时间就能完成。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): result [] i j 0 while i len(left) and j len(right): if left[i] right[j]: result.append(left[i]) i 1 else: result.append(right[j]) j 1 result.extend(left[i:]) result.extend(right[j:]) return result归并排序有两个无可替代的优点。第一是稳定。合并过程中遇到相等的元素时我们总是先取左半部分的那个所以相同元素的相对顺序不会改变。在对“先按人名排序再按部门排序”这类多关键字排序中稳定性是一个核心需求归并排序在这种场景下是首选。第二是复杂度预测性强。它的最坏、最好、平均时间稳定在 O(n log n)不存在快速排序那种退化到 O(n²) 的风险。代价是它需要 O(n) 的额外空间。如果你处理的是链表归并排序甚至可以做到 O(1) 额外空间因为链表的“切分和合并”只涉及指针调整不需要搬移整段数据。实战中还有一个“自底向上归并”的变体它不用递归切分而是先把数组看成 n 个长度为 1 的小段两两归并成长度 2 的小段再两两归并成长度 4 的段依次进行直到整体有序。这种写法避免了递归调用栈的开销对超大数组更友好。实操心得归并排序是我在真实项目里用得最多的高级排序之一尤其是处理外部排序场景。比如内存装不下一个上 GB 的文件时可以分块读入、每块排序后写回磁盘最后用归并思路把多个有序文件流式地合并成一个。整个过程中内存占用恒定逻辑非常清爽。3.3 快速排序工程之王但必须会躲退化快速排序是绝大多数编程语言标准库底层选择的大规模排序核心算法。它的思路同样是分治但比归并更激进选取一个基准值然后把数组分成“小于基准”和“大于等于基准”的两个区间再对这两个区间递归排序。经典写法Lomuto 分区def quick_sort(arr, low, high): if low high: return p partition_lomuto(arr, low, high) quick_sort(arr, low, p - 1) quick_sort(arr, p 1, high) def partition_lomuto(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快排的性能关键完全取决于 pivot 选得好不好。如果 pivot 每次都恰好在数组中央递归深度是 log n整体是 O(n log n)。但如果你对已经有序的数组使用固定取最后一个元素的方案每次切出来的两个区间一个长度为 n-1、另一个为 0递归深度一下子变成 n时间复杂度直接退化成 O(n²)。这是快排最经典的坑面试里十题有八题都在考这个。怎么避免退化业界有四个通用手段。第一是“三数取中”。在 low、mid、high 三个位置取大小居中者作为 pivot能有效避免正序、逆序等有序数据的退化。第二是“随机选取 pivot”。在每次分区前随机一个位置和末尾交换让恶意构造的输入无法稳定触发最坏情况。第三是“小区间切换插入排序”。当待排序区间长度小于某个阈值时不再递归调用而是直接插入排序。这个手段减少了很多层无意义的递归调用。第四是“三路切分”。针对大量重复元素的老大难问题把数组分成小于 pivot、等于 pivot、大于 pivot 三个区间。等于 pivot 的区间不需要再递归这样处理百万级重复值的数组时性能从 O(n log n) 降到了接近 O(n)。def quick_sort_3way(arr, low, high): if low high: return lt low gt high i low 1 pivot arr[low] while i gt: if arr[i] pivot: arr[lt], arr[i] arr[i], arr[lt] lt 1 i 1 elif arr[i] pivot: arr[i], arr[gt] arr[gt], arr[i] gt - 1 else: i 1 quick_sort_3way(arr, low, lt - 1) quick_sort_3way(arr, gt 1, high)提示快速排序是不稳定的。如果业务上要求“排序后相同元素的相对顺序保持不变”必须在选型阶段就把快排排除掉不管它跑得多快。稳定性是算法性质不可以通过优化代码来补救。3.4 堆排序原地排序但小心“假 O(n log n)”堆排序的思路是先把整个数组构建成一个大顶堆然后反复把堆顶元素和堆末尾交换、缩小堆范围、下沉调整。它的特点是排序过程中只需要 O(1) 的额外空间属于非常“省内存”的排序。def heap_sort(arr): n len(arr) for i in range(n // 2 - 1, -1, -1): sink(arr, i, n) for i in range(n - 1, 0, -1): arr[0], arr[i] arr[i], arr[0] sink(arr, 0, i) return arr def sink(arr, i, n): while True: left 2 * i 1 if left n: break right left 1 max_idx left if right n and arr[right] arr[left]: max_idx right if arr[max_idx] arr[i]: arr[i], arr[max_idx] arr[max_idx], arr[i] i max_idx else: break很多人背堆排序就背“建堆 O(n)排序 O(n log n)”但实际操作中有一个很微妙的问题堆排序的“常数”通常比归并和快排大因为每次交换堆顶和末尾后都要把新堆顶一路下沉到合适位置这个过程涉及到的指针跳转在这类数据上访问局部性很差缓存命中率低。因此堆排序在真实跑分中往往干不过快排和归并。它的适用场景很特殊要求 O(1) 额外空间同时不要求稳定性并且还要能快速拿到当前最大元素。比如一个只允许使用固定内存的嵌入式环境或者一个需要实时从数据流中选前 K 大的场景。优先队列本身就是堆的典型应用搞懂堆排序之后再去写 TopK、定时器调度、Dijkstra 这类算法会顺畅很多。注意自己写堆排序时最容易出 bug 的是 sink 函数的下标计算。一定要画一棵数组模拟的完全二叉树把2*i1、2*i2、父节点(i-1)//2的关系彻底搞清楚再动手不然一调就乱。4. 线性排序三剑客计数、桶、基数接下来这三个算法完全是另一条赛道不靠比较靠的是“数据本身的可数性”或“可分组性”。它们能把时间复杂度降到 O(n)但适用范围非常窄属于“一招鲜”的存在。很多程序员工作多年都没亲手写过它们我建议你至少写一遍因为它们是理解“以空间换时间”的最佳教材。4.1 计数排序范围小、分布密时的降维打击计数排序的思路直观到令人惊讶既然数据都在一个有限的整数范围里那我干脆直接统计每个值出现了几次然后按顺序填回去。整个过程不比较大小只做统计、前缀和、回填。def counting_sort(arr): if not arr: return arr min_val min(arr) max_val max(arr) offset -min_val freq [0] * (max_val - min_val 1) for x in arr: freq[x offset] 1 for i in range(1, len(freq)): freq[i] freq[i - 1] output [0] * len(arr) for x in reversed(arr): position freq[x offset] - 1 output[position] x freq[x offset] - 1 return output注意这里关键的一步是从数组尾部开始倒序遍历回填加上前缀和统计此举能让输出数组中相同元素的相对顺序保持不变因此计数排序是稳定的。它的时间开销为 O(n k)k 是数值范围。当 k 远小于 n 时非常猛比如给十万个 0 到 100 之间的整数排序计数排序几乎是秒出结果。但它的缺点也很致命数值跨度大时会分配一个巨大的频次数组。比如给 1 和 10 亿两个数排序k 是十亿量级分配这么大空间显然不可行。这时候就要考虑用哈希表压缩频次存储但那样会失去“连续空间高命中”的优势性能优势也会缩水。4.2 桶排序数据分布均匀时的分布式思维桶排序的思路是把数据按区间分到若干个桶里桶内分别排序再依次收集起来。分桶的过程其实就是在做“分类放箱”类似快递分拣员按尾号把包裹丢进不同格口。def bucket_sort(arr, bucket_size5): if not arr: return arr min_val min(arr) max_val max(arr) bucket_count (max_val - min_val) // bucket_size 1 buckets [[] for _ in range(bucket_count)] for x in arr: idx (x - min_val) // bucket_size buckets[idx].append(x) result [] for bucket in buckets: insertion_sort(bucket) result.extend(bucket) return result桶排序的复杂度分析比较灵活如果数据均匀分布每个桶里的元素很少桶内排序接近常数整体趋近 O(n)。但如果数据严重倾斜比如 99% 的数据都落在同一个桶里桶内排序又变成了 O(k log k)整体性能就会退化。桶排序的典型应用场景是对浮点数或自定义对象排序因为它们不能直接作为计数数组的下标但可以依据区间映射到桶。我之前处理过一批日志里的耗时数据几百万条分布在 0 到几秒之间直接用桶按区间分桶后再桶内快排比全局快排快了不少因为各个桶可以并行处理。4.3 基数排序按位拆解多轮稳定排序基数排序的思路更巧妙不一次性比较整个数的大小而是把位数拆出来从最低位到最高位依次做多轮稳定排序通常用计数排序实现。每轮处理一位全部处理完后自然有序。def radix_sort(arr): if not arr: return arr max_val max(arr) exp 1 while max_val // exp 0: arr counting_sort_by_digit(arr, exp) exp * 10 return arr def counting_sort_by_digit(arr, exp): freq [0] * 10 for x in arr: digit (x // exp) % 10 freq[digit] 1 for i in range(1, 10): freq[i] freq[i - 1] output [0] * len(arr) for x in reversed(arr): digit (x // exp) % 10 output[freq[digit] - 1] x freq[digit] - 1 return output这个算法的前提条件是“稳定排序”。每一轮必须保证相同位数的元素维持上一轮的相对顺序否则前面的轮次就白做了。我见过不少初学者听说过基数排序就去手写结果用的比较器是普通快排最后排序结果完全不对。基数排序的时间复杂度是 O(d·(nk))d 是最大值的位数k 是进制大小。在固定位宽的数据集上它能把时间压到线性。但是把它推广到负数、小数、字符串时需要做额外处理前缀补位、统一符号这些细节一多代码复杂度就上去了。5. 十大算法全景对比到底该怎么选理解了全部算法之后最实用的还是那张“选型对照表”。这十种算法在时间、空间、稳定性上的差异非常明显我在项目里做技术方案时经常来回翻这个表。算法平均时间最坏时间额外空间稳定适用场景冒泡排序O(n²)O(n²)O(1)是教学、近乎有序小数组选择排序O(n²)O(n²)O(1)否交换代价高的场景插入排序O(n²)O(n²)O(1)是小数组、基本有序数据希尔排序O(n^1.3)依gap序列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)否内存受限、TopK计数排序O(nk)O(nk)O(k)是小范围整数桶排序O(nk)O(n²)O(n)依桶内算法均匀分布的浮点数基数排序O(d·n)O(d·n)O(nk)是定长整数、字符串选型时我习惯按下面的顺序问自己三个问题第一问数据量多大、内存紧不紧。如果只有几百条数据直接插入排序就行别去背负递归或分桶的复杂度。如果内存很紧张堆排序或希尔排序是首选。第二问要不要保持稳定顺序。如果业务里的排序键是复合的比如先按时间再按用户或者后面还要做二次排序那稳定排序会省掉大量麻烦此时首选归并或计数系列。第三问数据本身有没有“送分题”特征。整数且范围小选计数浮点且分布均匀选桶定宽字符串或整数选基数基本有序的小数组选插入。如果都没有明显特征默认上快排的改良版。实操心得很多人喜欢把“时间最快”当唯一标准但工程里排序的瓶颈往往不只是 CPU还有内存分配和缓存。归并虽然稳定但每次递归都分配临时数组在超大数组上内存开销和 GC 压力都不小。快排之所以成为工程默认选择很大程度上是它在“缓存友好度”和“空间开销”上取得了很好的平衡。6. 工程实战从手写算法到项目落地的最后一步6.1 标准库排序是怎么“偷懒”的很多人学会十大排序后有个错觉生产环境里也要自己写排序。实际上编程语言的标准库排序经过大量调优效率远超普通人手写的版本。理解标准库的实现逻辑更重要因为那是十大排序思想的终极融合。以某经典动态语言的标准库排序为例它不是单一算法而是一个混合策略检测到数据量小于某个阈值时直接用插入排序数据量大时使用类似归并的思路借助局部模式检测对已经有序或大致有序的切片做快速合并处理。这是典型的多算法融合先用快排或归并把大结构打散再用插入排序收拾残局。另一个广泛使用的思路是“二分插入排序”。在数组整体有序性较好的前提下用二分查找定位插入位置能减少一部分比较次数虽然移动次数不变但整体常数变小。所以正确做法是默认情况下用语言自带 sort()不要自己手搓排序。只有在三个特殊场景下才值得手写排序一是数据量极大并且内存极度受限二是数据有极其特殊的分布特征标准库无法利用三是在实时性要求极高的领域针对特定规模做极致优化。6.2 一次排序优化的实战记录我之前处理过一个数据分发模块需要对内存里约两千万条短整型记录排序。一开始图省事直接用通用快排跑完需要数秒内存峰值也很高。后来发现这些数据范围很窄分布在 0 到 10 万之间于是改成计数排序分配一个 10 万元素的计数数组一遍统计加一遍回填排序总耗时降了一个数量级。这个案例的关键不是“计数排序代替快排”这一句结论而是它体现的优化流程先分析数据分布特征再根据特征选择算法。如果不是那批数据的范围已知且紧凑计数排序完全发挥不出优势我也不会这么改。所以工程优化的第一步永远是测量数据和理解数据而不是直接猜测算法。注意任何排序优化都要先量化当前瓶颈。用性能分析工具看清楚时间到底花在比较还是移动上再决定替换为哪种排序。盲目替换很可能让本来不错的缓存局部性变得一塌糊涂反而更慢。7. 常见问题与排查技巧实录写排序算法看似简单实际调试起来经常一头雾水。我整理了这十年里同事和我自己踩过的一些高频问题你可以直接当排查手册用。7.1 为什么快排在有序数组上慢到离谱这是快排最经典的退化问题。固定选最后一个元素或第一个元素做 pivot遇到已经有序或逆序的数据时每次分区都只能排除一个元素递归深度变成 n时间复杂度退化为 O(n²)。排查办法很简单在排序前打印递归深度如果深度接近数组长度基本可以断定是 pivot 选得不好。修复方案是加上三数取中或随机化策略别再用固定位置直接当 pivot。7.2 归并排序结果对但数组真的被改了归并排序如果写成每次返回新列表、不修改原数组原始调用方会拿不到排序后的结果。实际写代码时最好明确区分“原地改”和“返回新数组”两种语义。如果函数名是merge_sort但内部没有对原列表做赋值调用方很容易拿到未排序的旧引用。排查办法是打印函数内部排序后的第一步和最后一步看原数组引用有没有被整体替换。我常建议写一个简单的测试脚本比较排序前后数组中第一个元素和最后一个元素的变化快速定位是不是引用问题。7.3 堆排序的结果出现局部乱序堆排序如果下沉操作写错很容易出现排完以后中间有几个元素仍是乱序的。典型原因是下沉时没有正确处理左右孩子都大于当前节点的情况或者堆末尾的边界忘记缩小导致已排好序的部分又被重新调进堆里。排查思路是构造一个规模很小的数组比如[5, 3, 8, 1, 9]手动在纸上把建堆和每次交换后的状态画出来和代码输出的中间态对照。一旦中间态对不上bug 基本就锁定在下沉函数中。7.4 计数排序出现下标越界计数排序的下标越界几乎总是因为忽略了偏移量。如果你只拿元素值直接访问计数数组而数据里存在负数就会立刻越界。另一种情况是最大最小值算错导致频次数组长度不够。正确做法是先获得 min 和 max再做一次减 min 的偏移。数组长度是max - min 1而不是max 1。这个细节在写任何“使用值做下标”的算法时都要特别警惕。7.5 稳定排序却排出了非稳定效果如果你明确用了归并或计数这种稳定排序但最后结果仍然不稳定多半在分区或回填时用了非稳定的技巧。比如在合并时写成if left[i] right[j]而不是相等的元素会被优先取走右半部分破坏了稳定性。排查办法是用一组带编号的重复元素做测试比如[2a, 1, 2b]这种构造型数组看排序完成后 2a 是否仍然在 2b 前面。如果顺序反了就去检查比较运算符是不是漏了等于号。独家经验我调试排序算法时几乎不用大数组永远先跑一个长度 10 以内的最小用例。把数组在每次操作后的状态全打印出来几轮就能定位问题。等最小用例通过后再慢慢增大规模测性能这个节奏效率最高。写到这里这十大排序算法算是完完整整过了一遍。我个人的体会是排序算法的学习价值从来不只是为了面试它能真正改变你对代码性能的直觉。当你见过快排在两千万数据上火花带闪电地跑完也见过错误选型让一个百万级排序慢到怀疑人生之后你在设计任何系统时都会本能地多问一句这里的数据有什么分布特征哪个算法能把它的特征利用起来这种直觉才是折腾这十种算法最大的回报。
阅读完成 · 觉得有帮助?
咨询建站