尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

十大排序算法深度解析:从原理到实战选型指南

十大排序算法深度解析:从原理到实战选型指南 1. 排序算法从“知其然”到“知其所以然”聊到排序算法很多人的第一反应是“面试八股文”或者觉得在现成的sort()函数面前这些底层实现早已过时。我刚开始也这么想直到有一次处理一个内存受限的嵌入式设备上的传感器数据流系统自带的排序库因为内存开销过大直接崩溃我才被迫回头去手写一个最基础的插入排序。那一刻我意识到理解这些经典算法从来不是为了应付考试而是为了在关键时刻你能清晰地知道该用哪把“手术刀”以及为什么这把刀最合适。排序是将一组无序的数据元素按照某种规则如数字大小、字典序重新排列的过程。它是计算机科学中最基础、最核心的课题之一堪称算法的“基本功”。我们日常用的Array.sort()或list.sort()其内部很可能就是快速排序、归并排序或TimsortPython、Java采用的混合优化实现。但如果你只停留在调用API那么当遇到性能瓶颈、需要定制排序规则、或者在特殊环境如内存极小、数据近乎有序下你就会束手无策。所谓“十大排序算法”通常指的是最经典、最具教学和实战价值的十种。它们可以从多个维度分类按时间复杂度有O(n²)的简单排序冒泡、选择、插入也有O(n log n)的高级排序快排、归并、堆排按稳定性即相等元素的相对顺序在排序后是否保持不变按是否基于比较还可以分出非比较类的桶、计数、基数排序。理解这些分类是选择算法的第一步。接下来我将不按常规的“从慢到快”顺序而是按照其思想脉络和内在联系带你逐一拆解这十种算法重点讲清楚它们各自的核心思想、适用场景以及那些在教科书里不会写的“坑”和实战优化技巧。2. 排序算法的基石三种O(n²)简单排序算法虽然它们效率不高但思想极其重要是理解更复杂算法的基础。更重要的是在特定的小数据量或近乎有序的场景下它们可能比那些“高级”算法更有效。2.1 冒泡排序最直观的“邻里交换”冒泡排序的思想就像它的名字每一轮遍历相邻的元素两两比较如果顺序错误就交换这样每一轮都会将当前未排序部分的最大或最小元素“冒泡”到正确位置。核心操作与代码实现它的实现非常简单。假设我们要将数组arr按升序排列。def bubble_sort(arr): n len(arr) for i in range(n): # 控制排序轮数 # 优化点设置一个标志位记录本轮是否发生交换 swapped False # 每一轮比较相邻元素最大的元素会“沉”到最后 for j in range(0, n - i - 1): 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²/2次比较和交换。它的时间复杂度和空间复杂度都是最直观的O(n²)和O(1)。但这里有个关键点很多人只记住了O(n²)却忽略了它的最好情况时间复杂度是O(n)——当输入数组已经有序时通过swapped标志优化只需遍历一次即可结束。这意味着对于几乎已经排好序的数据比如在已排序列表尾部新增少量数据冒泡排序可能是一个不错的选择。实战心得与避坑永远记得加“提前终止”优化不加swapped判断的冒泡排序是“无脑”的即使数组已经有序它也会傻傻地跑完n轮。加上这个判断是编写冒泡排序的基本素养。不要用于任何严肃的生产环境除非数据量极小比如n10且你非常确定数据特性否则应避免使用。它的主要价值在于教学和理解算法思想。2.2 选择排序每次找到“最小元”选择排序的思路更符合人类直觉在未排序序列中找到最小或最大元素存放到排序序列的起始位置然后从剩余未排序元素中继续寻找最小元素放到已排序序列的末尾。如此反复直到所有元素均排序完毕。核心操作与代码实现def selection_sort(arr): n len(arr) for i in range(n): # 假设当前索引i的元素是最小值 min_idx i # 在i1到末尾的区间内寻找真正的最小值索引 for j in range(i 1, n): if arr[j] arr[min_idx]: min_idx j # 将找到的最小元素与第i个位置的元素交换 arr[i], arr[min_idx] arr[min_idx], arr[i] return arr算法特点分析选择排序的比较次数固定为n(n-1)/2与数据初始状态无关。它的交换次数很少最多为n-1次。这是一个不稳定的排序算法。试想一个数组[5, 8, 5, 2, 9]第一个5记为5a和2交换后会跑到第二个5记为5b的后面破坏了稳定性。为什么理解“不稳定”很重要假设你有一份学生成绩单先按姓名排序再按分数排序。如果第二次排序用的是不稳定的选择排序那么同分数的学生他们之前的姓名顺序就可能被打乱。在需要多关键字排序的场景下稳定性是关键考量因素。2.3 插入排序像整理扑克牌一样自然插入排序是简单排序中我最推崇的一种也是很多高级排序算法如TimSort在处理小规模子数组时采用的最终手段。它的工作方式类似于我们整理手中的扑克牌对于未排序的数据在已排序序列中从后向前扫描找到相应位置并插入。核心操作与代码实现def insertion_sort(arr): n len(arr) # 从第二个元素开始索引1认为第一个元素自身是有序的 for i in range(1, n): key arr[i] # 当前待插入的元素 j i - 1 # 从i的前一个元素开始比较 # 将比key大的元素都向后移动一位为key腾出位置 while j 0 and key arr[j]: arr[j 1] arr[j] j - 1 # 将key插入到找到的正确位置 arr[j 1] key return arr性能与适用场景插入排序的最好情况时间复杂度是O(n)数组已有序最坏和平均是O(n²)。但它有两大突出优点对小规模数据或近乎有序数据极其高效由于它的内循环实际工作量很小主要是比较和移动当n很小通常认为50时它的常数因子很小实际运行时间可能优于O(n log n)的算法。这也是为什么快速排序、归并排序在递归到小数组时会切换成插入排序。原地、稳定空间复杂度O(1)且是稳定的排序算法。一个高级技巧二分查找插入排序对于已经找到的待插入元素key我们可以在已排序部分arr[0...i-1]中使用二分查找来定位其应该插入的位置这样可以将比较次数从O(n)降到O(log n)。但注意元素的移动操作仍然是O(n)所以整体时间复杂度依然是O(n²)但在比较成本很高的场景下比如比较的是复杂的字符串或自定义对象这个优化能带来显著提升。def binary_insertion_sort(arr): n len(arr) for i in range(1, n): key arr[i] # 使用二分查找找到插入位置 left, right 0, i - 1 while left right: mid (left right) // 2 if key arr[mid]: right mid - 1 else: left mid 1 # left 就是key应该插入的位置 # 将left到i-1的元素整体后移一位 for j in range(i - 1, left - 1, -1): arr[j 1] arr[j] arr[left] key return arr3. 分治思想的典范归并排序与快速排序当数据量变大O(n²)的算法就力不从心了。这时需要借助“分而治之”的思想将大问题分解为小问题来解决。归并排序和快速排序是这一思想的两种不同实现路径它们的时间复杂度都能达到O(n log n)。3.1 归并排序稳定的“分割与合并”归并排序的核心思想非常清晰如果数组长度大于1就将其一分为二分别对左右两半递归地进行归并排序然后将两个已经有序的子数组合并成一个大的有序数组。核心操作——合并Merge这是归并排序的灵魂。给定两个有序数组A和B如何高效地合并它们答案是使用双指针。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 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)时间复杂度与空间复杂度分析归并排序的时间复杂度非常稳定最好、最坏、平均情况下都是O(n log n)。因为它无论如何都要完成“分”和“合”的过程。它的代价是需要额外的空间空间复杂度为O(n)用于存储合并过程中的临时数组。这是一个稳定的排序算法。实战应用与优化外部排序的基石当数据量大到无法全部装入内存时比如要对100GB的文件排序归并排序是唯一可行的方案。思路是将大文件分割成多个能装入内存的小块每块用内部排序如快排排好序然后多路归并这些小文件。链表排序的最佳选择对于链表这种数据结构归并排序可以做到O(1)的额外空间递归栈除外因为链表的合并操作不需要像数组那样开辟新空间来移动元素只需改变节点指针。原地归并的挑战标准的归并排序需要额外空间。存在一些复杂的原地归并算法如手摇算法但会大幅增加常数时间实践中很少使用。通常我们接受O(n)的空间开销来换取清晰和稳定。3.2 快速排序高效的“分区与征服”快速排序是实际应用中最广泛的排序算法大多数语言标准库的排序函数都以其为蓝本进行优化。它的思想是选择一个基准元素通过一趟排序将待排记录分隔成独立的两部分其中一部分的所有数据都比另一部分的所有数据小然后递归地对这两部分数据继续进行快速排序。核心操作——分区Partition这是快排的核心目标是将数组围绕基准值pivot重新排列。def partition(arr, low, high): Lomuto分区方案易于理解 pivot arr[high] # 选择最后一个元素作为基准 i low - 1 # i指向小于pivot区域的最后一个元素 for j in range(low, high): if arr[j] pivot: i 1 arr[i], arr[j] arr[j], arr[i] # 将pivot放到正确位置 arr[i 1], arr[high] arr[high], arr[i 1] return i 1 def quick_sort(arr, low, high): if low high: pi partition(arr, low, high) # 获取分区点 quick_sort(arr, low, pi - 1) # 递归排序左半部分 quick_sort(arr, pi 1, high) # 递归排序右半部分为什么快排通常最快虽然平均时间复杂度也是O(n log n)但它的常数因子非常小。在内存中它进行的是连续的内存访问和交换对CPU缓存友好。然而它的最坏情况时间复杂度是O(n²)——当每次选择的pivot都是最大或最小值导致分区极度不平衡时。这就引出了最关键的问题如何选择基准值基准值选择的艺术与避坑固定选择如最后一个元素最简单但极易在输入数组已有序或逆序时导致最坏情况。绝对不要在生产环境中使用这种朴素策略。随机选择在[low, high]区间随机选择一个元素作为pivot。这是避免最坏情况的简单有效方法将期望时间复杂度稳定在O(n log n)。三数取中法选取数组头、尾、中间三个元素的中位数作为pivot。这种方法能有效避免在已排序数组上出现最坏情况且不需要随机数生成的开销是很多库函数的默认策略。双指针分区Hoare分区比上面的Lomuto方案更高效交换次数更少。它使用两个指针一个从左边找大于pivot的一个从右边找小于pivot的然后交换它们。def partition_hoare(arr, low, high): Hoare分区方案通常更高效 pivot arr[(low high) // 2] # 选择中间元素作为基准 i, j low - 1, high 1 while True: i 1 while arr[i] pivot: i 1 j - 1 while arr[j] pivot: j - 1 if i j: return j arr[i], arr[j] arr[j], arr[i]工程实践中的优化小数组切换插入排序当递归到子数组规模较小如长度15时直接调用插入排序。因为插入排序在小数据量上常数因子更小且能避免快排递归调用的开销。尾递归优化对较小的那个分区进行递归调用对较大的分区使用循环或显式栈来处理可以减少递归深度防止栈溢出。处理大量重复元素当数组中有大量重复元素时标准快排效率会下降。可以使用三路快排将数组分为“小于pivot”、“等于pivot”、“大于pivot”三部分能显著提升效率。4. 基于数据结构的排序堆排序与希尔排序这类算法巧妙利用了特定数据结构的性质来提升排序效率。4.1 堆排序利用“二叉堆”的选择排序堆排序可以看作是选择排序的一种高级变种。它利用“二叉堆”这种数据结构能在O(log n)的时间内找到最大或最小元素从而将选择排序中“查找最值”的O(n)操作降为O(log n)。核心数据结构二叉堆二叉堆是一个近似完全二叉树并满足堆性质父节点的值总是大于等于大顶堆或小于等于小顶堆其子节点的值。它通常用数组来实现对于下标为i的节点其左子节点下标为2*i1右子节点为2*i2父节点为(i-1)//2。算法两步走建堆与排序建堆将无序数组调整成一个大顶堆。可以从最后一个非叶子节点开始自底向上进行“下沉”操作。排序将堆顶元素最大值与堆末尾元素交换此时最大值已就位。然后将剩余元素重新调整为大顶堆重复此过程。def heapify(arr, n, i): 维护以i为根节点的子树满足大顶堆性质 largest i left 2 * i 1 right 2 * i 2 if left n and arr[left] arr[largest]: largest left if right n and arr[right] arr[largest]: largest right if largest ! i: arr[i], arr[largest] arr[largest], arr[i] # 递归调整被破坏的子堆 heapify(arr, n, largest) def heap_sort(arr): n len(arr) # 1. 构建大顶堆 # 从最后一个非叶子节点开始向上调整 for i in range(n // 2 - 1, -1, -1): heapify(arr, n, i) # 2. 逐个提取元素 for i in range(n - 1, 0, -1): # 将堆顶最大值与当前末尾交换 arr[i], arr[0] arr[0], arr[i] # 调整剩余的前i个元素使其保持堆性质 heapify(arr, i, 0) return arr堆排序的特点时间复杂度建堆O(n)每次调整O(log n)总复杂度O(n log n)。且最好、最坏、平均情况都是如此非常稳定。空间复杂度O(1)原地排序。稳定性不稳定。在交换堆顶和堆尾元素时可能破坏相等元素的相对顺序。优缺点优点是原地、时间复杂度稳定不受输入数据顺序影响。缺点是缓存不友好跳跃式访问元素实际运行速度通常慢于快速排序和归并排序。但它有一个不可替代的优势可以高效地解决“Top K”问题。例如找一亿个数中的前100个最大值只需维护一个大小为100的小顶堆遍历一遍数据即可时间复杂度O(n log K)空间复杂度O(K)。4.2 希尔排序插入排序的威力增强版希尔排序是插入排序的改进由Donald Shell提出。它通过引入“增量”的概念让元素可以一次移动多位从而快速减少整体的逆序对数量。核心思想跳跃式分组插入希尔排序将整个待排序序列分割成若干子序列由增量间隔决定分别进行直接插入排序。然后逐步缩小增量直至增量为1此时整个序列已基本有序再做一次标准的插入排序效率就很高。def shell_sort(arr): n len(arr) # 初始增量间隔设定通常为n//2并逐步缩小 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 arr增量序列的选择希尔排序的性能高度依赖于增量序列的选择。上面代码使用的是Shell原始序列n/2, n/4, ... 1但这不是最优的。Hibbard增量序列1, 3, 7, 15, ..., 2^k - 1。最坏情况复杂度可优化到O(n^(3/2))。Sedgewick增量序列通过复杂公式生成是目前已知最好的序列之一最坏情况复杂度可达O(n^(4/3))平均约O(n^(7/6))。希尔排序的定位它是一个不稳定的排序算法。它的时间复杂度介于O(n log n)和O(n²)之间具体取决于增量序列。在实际应用中对于中等规模的数据几千到几万希尔排序的表现往往不错代码简单且是原地排序。它像是插入排序和高级排序之间的一个“桥梁”。但在大规模数据面前它通常还是被快排和归并取代。5. 非比较排序当数据有特殊属性时之前讨论的算法都是基于“比较”的排序它们的时间复杂度下界是O(n log n)。但如果待排序的数据具有某些特殊属性比如是有限范围内的整数我们可以突破这个下界达到线性的时间复杂度O(n)。5.1 计数排序统计频率的直方图法计数排序要求输入的数据必须是有确定范围的整数或者能被映射到整数。它的核心思想是统计每个元素出现的次数然后根据计数结果直接计算出每个元素在输出数组中的最终位置。算法步骤详解找出待排序数组中的最大值max和最小值min。创建计数数组count长度为max - min 1用于统计每个值出现的次数。遍历原数组统计每个元素出现的次数。例如元素x出现则count[x - min]。将计数数组变形使得count[i]表示小于等于imin的元素个数。即从第二个元素开始每个元素加上前一个元素的值count[i] count[i-1]。反向遍历原数组为了保持稳定性将元素放入输出数组的正确位置。def counting_sort(arr): if not arr: return [] # 1. 获取最大值和最小值 max_val, min_val max(arr), min(arr) range_of_elements max_val - min_val 1 # 2. 创建并填充计数数组 count [0] * range_of_elements for num in arr: count[num - min_val] 1 # 3. 将计数数组转换为位置数组 for i in range(1, len(count)): count[i] count[i - 1] # 4. 构建输出数组反向遍历保证稳定性 output [0] * len(arr) for i in range(len(arr) - 1, -1, -1): num arr[i] position count[num - min_val] - 1 output[position] num count[num - min_val] - 1 return output适用场景与限制时间复杂度O(n k)其中k是数据的范围max-min1。当kO(n)时复杂度为O(n)。空间复杂度O(n k)需要输出数组和计数数组。稳定性稳定通过反向遍历实现。适用场景数据范围不大k不能太大且为整数的场景。例如对百万个0-100分的考试成绩进行排序计数排序是绝佳选择。关键限制只能用于整数。对于浮点数或字符串需要能映射到有限的整数键。5.2 桶排序化整为零的分治策略桶排序是计数排序的推广。它假设输入数据均匀分布在一个范围内然后将该范围划分为若干个大小相同的子区间称为“桶”。将数据分到各个桶里每个桶再单独排序可以使用其他排序算法最后按顺序合并所有桶的结果。算法步骤设置一个定量的数组作为空桶。遍历输入数据把每个元素放入对应的桶中。对每个非空桶进行排序例如使用插入排序。从所有桶中按顺序取出元素放回原数组。def bucket_sort(arr, bucket_size5): if len(arr) 0: return arr # 确定数据的范围 min_val, max_val min(arr), max(arr) # 计算需要的桶的数量 bucket_count (max_val - min_val) // bucket_size 1 buckets [[] for _ in range(bucket_count)] # 将数据分配到各个桶中 for num in arr: index (num - min_val) // bucket_size buckets[index].append(num) # 对每个桶进行排序并合并结果 sorted_arr [] for bucket in buckets: # 这里使用内置的TimSort对小数据量效率很高 sorted_arr.extend(sorted(bucket)) # 也可以用insertion_sort(bucket) return sorted_arr性能分析与关键点时间复杂度平均O(n k)最坏O(n²)所有数据集中到一个桶里。取决于桶内排序的算法。空间复杂度O(n k)。稳定性取决于桶内排序算法的稳定性。如果使用稳定的插入排序则桶排序也是稳定的。核心要点桶排序的性能依赖于数据的分布。如果数据均匀分布每个桶的大小接近则效率很高。如果数据全部集中在一个桶里则退化为单一的插入排序效率低下。因此选择合适的桶大小和桶数量至关重要这需要对数据分布有一定的先验知识。5.3 基数排序按位比较的“分发-收集”基数排序是一种非比较的整数排序算法它根据键值的每位数字或字符来分配和收集元素。可以从最低位LSD或最高位MSD开始。LSD基数排序步骤以十进制为例找到数组中最大数的位数这决定了需要进行的“分配-收集”轮数。从最低位开始对待排序数组的每一位进行排序。通常使用稳定的计数排序作为子程序。重复步骤2直到最高位排序完成。def radix_sort(arr): if not arr: return [] # 找到最大数确定最大位数 max_num max(arr) exp 1 # 从个位开始 while max_num // exp 0: # 使用计数排序作为子程序对当前位进行排序 counting_sort_by_digit(arr, exp) exp * 10 return arr def counting_sort_by_digit(arr, exp): 根据某一位exp指定进行计数排序 n len(arr) output [0] * n count [0] * 10 # 十进制数字0-9 # 统计当前位上每个数字的出现次数 for i in range(n): index (arr[i] // exp) % 10 count[index] 1 # 将计数转换为位置 for i in range(1, 10): count[i] count[i - 1] # 反向构建输出数组以保证稳定性 for i in range(n - 1, -1, -1): index (arr[i] // exp) % 10 output[count[index] - 1] arr[i] count[index] - 1 # 将排序好的数组拷贝回原数组 for i in range(n): arr[i] output[i]基数排序的特点时间复杂度O(d * (n k))其中d是最大数字的位数k是基数十进制下k10。由于d通常远小于n所以可以近似看作线性复杂度。空间复杂度O(n k)。稳定性稳定因为使用了稳定的子排序算法。适用场景适用于整数或字符串可以按字符比较的排序且数据的位数或长度不能太大。例如对手机号码、身份证号等固定长度的数字字符串排序非常高效。MSD与LSDLSD从低位开始实现简单。MSD从高位开始更像是一种递归的分治方法有时可以提前结束某些分支的排序但实现稍复杂。6. 算法对比与实战选型指南了解了所有算法后最关键的问题是我该用哪一个没有“最好”的算法只有“最合适”的算法。选择取决于数据规模、数据特征、内存限制、稳定性要求等多个因素。6.1 综合性能对比表排序算法平均时间复杂度最坏时间复杂度最好时间复杂度空间复杂度稳定性核心思想适用场景冒泡排序O(n²)O(n²)O(n)O(1)稳定相邻交换教学、小规模或近乎有序数据选择排序O(n²)O(n²)O(n²)O(1)不稳定选择最值交换次数要求少的场景如Flash内存写操作昂贵插入排序O(n²)O(n²)O(n)O(1)稳定构建有序序列小规模数据、近乎有序数据、作为高级排序的子过程希尔排序O(n log n) ~ O(n²)O(n²)O(n log n)O(1)不稳定缩小增量插入中等规模数据简单且无需额外空间归并排序O(n log n)O(n log n)O(n log n)O(n)稳定分治与合并链表排序、外部排序、需要稳定性的场景快速排序O(n log n)O(n²)O(n log n)O(log n) ~ O(n)不稳定分区与递归通用场景大规模内存内排序标准库默认堆排序O(n log n)O(n log n)O(n log n)O(1)不稳定堆数据结构原地排序且要求最坏情况O(n log n)、Top K问题计数排序O(n k)O(n k)O(n k)O(n k)稳定统计频率数据范围较小的整数排序桶排序O(n k)O(n²)O(n)O(n k)稳定数据分桶数据均匀分布范围已知基数排序O(d * (n k))O(d * (n k))O(d * (n k))O(n k)稳定按位分配整数或定长字符串排序注k代表数据范围或桶的数量d代表最大位数。6.2 实战选型决策树面对一个具体的排序问题你可以按以下思路决策数据规模有多大极小n 50优先考虑插入排序。它的常数因子小代码简单且对有序数据友好。小到中等50 n 1000希尔排序或经过优化的快速排序小数组切换插入排序都是不错的选择。大规模n 1000快速排序随机化或三数取中基准通常是默认选择。如果担心快排的最坏情况且需要稳定排序则用归并排序。如果内存非常紧张考虑堆排序。数据有什么特殊性质数据是整数且范围不大k ~ O(n)无脑用计数排序线性时间又快又稳。数据是整数或定长字符串考虑基数排序。数据已基本有序插入排序或冒泡排序带提前终止会表现出接近O(n)的性能。快速排序在这种情况下如果基准选择不好性能会退化。数据中有大量重复元素使用三路快速排序避免对重复元素进行不必要的递归。有什么额外约束需要稳定排序排除快排、堆排、选择排序、希尔排序。优先考虑归并排序、插入排序、计数/桶/基数排序。内存空间极其有限原地排序排除归并、计数、桶、基数排序。考虑堆排序、快速排序、希尔排序或插入排序。排序的是链表归并排序是天然适合链表结构的王者。需要找Top K个元素而非完全排序使用堆排序的思想维护一个大小为K的堆时间复杂度O(n log K)比完全排序快。一个真实的踩坑案例我曾负责一个日志分析系统需要按时间戳对海量日志排序。最初直接调用list.sort()Python的Timsort混合了归并和插入发现内存占用飙升。原因是Timsort需要O(n)的额外空间。后来切换到堆排序虽然速度慢了约15%但内存使用变为O(1)成功在有限内存的服务器上稳定运行。这就是理解算法特性带来的直接价值。7. 超越比较从理论下界到实际工程我们讨论了O(n²)的简单排序、O(n log n)的基于比较的高级排序以及突破O(n log n)下界的非比较排序。这里有一个著名的理论基于比较的排序算法其时间复杂度下界是Ω(n log n)。这个结论来自于决策树模型每一次比较最多将可能性减少一半要区分n!种排列至少需要log₂(n!) ≈ n log n次比较。非比较排序之所以能突破这个下界是因为它们利用了数据本身的额外信息如整数范围、位数跳出了“比较”的框架。这给了我们一个重要的工程启示在设计系统时如果能对输入数据的特性做出假设或约束往往能获得巨大的性能提升。比如知道用户ID是32位整数就可以考虑用基数排序知道分数在0-100之间计数排序就是最优解。最后现代编程语言的标准库排序函数如C的std::sort Python的sorted Java的Arrays.sort都是高度优化的混合算法。例如Python的Timsort结合了归并排序和插入排序能自适应地处理不同规模、不同有序度的数据。在绝大多数情况下直接使用这些内置函数是最佳选择。但正如开篇所说理解它们背后的原理是为了在“绝大多数情况”之外当内置函数不再适用时你能自信地选出甚至写出最适合当前场景的那把“排序手术刀”。这份底气就来自于你对这“十大排序算法”从原理到细节从优点到局限的透彻理解。
返回列表