排序是计算机科学中最基础、最重要的算法问题之一。无论是数据库索引的构建、搜索引擎结果的呈现,还是操作系统进程的调度,排序算法都扮演着不可或缺的角色。Donald Knuth 在其经典著作《计算机程序设计艺术》中曾指出,计算机约有 25% 的运行时间花费在排序上。深入理解各种排序算法的原理、性能特征和适用场景,是每一位程序员的必修课。
本文将对十种经典排序算法进行全面而深入的对比分析,涵盖原理讲解、复杂度分析、代码实现以及实际应用场景。
一、冒泡排序(Bubble Sort)
原理简述: 冒泡排序反复遍历待排序序列,每次比较相邻的两个元素,若顺序错误则交换。每一轮遍历会将当前未排序部分的最大元素”冒泡”到正确位置。
复杂度分析:
- 最好时间复杂度:O(n)(序列已有序,仅遍历一次)
- 最坏时间复杂度:O(n²)(序列完全逆序)
- 平均时间复杂度:O(n²)
- 空间复杂度:O(1)
- 稳定性:稳定
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
二、选择排序(Selection Sort)
原理简述: 选择排序每次从未排序部分中找到最小(或最大)元素,将其放到已排序部分的末尾。算法维护两个子序列:已排序部分和未排序部分。
复杂度分析:
- 最好时间复杂度:O(n²)
- 最坏时间复杂度:O(n²)
- 平均时间复杂度:O(n²)
- 空间复杂度:O(1)
- 稳定性:不稳定(交换可能打破相同元素的相对顺序)
def selection_sort(arr):
n = len(arr)
for i in range(n):
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
三、插入排序(Insertion Sort)
原理简述: 插入排序将数组分为已排序和未排序两部分,每次取未排序部分的第一个元素,在已排序部分中找到合适的位置插入。类似于我们打牌时整理手牌的过程。
复杂度分析:
- 最好时间复杂度:O(n)(序列已有序)
- 最坏时间复杂度:O(n²)(序列完全逆序)
- 平均时间复杂度:O(n²)
- 空间复杂度:O(1)
- 稳定性:稳定
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
插入排序在小规模数据和近乎有序的数据上表现极为优秀,许多高级排序算法(如 Timsort)在处理小规模子数组时会回退到插入排序。
四、希尔排序(Shell Sort)
原理简述: 希尔排序是插入排序的改进版本,核心思想是通过设定一个递减的间隔序列(gap sequence),先对间隔较大的元素进行排序,逐步缩小间隔,最终在间隔为 1 时退化为标准插入排序。由于前期的大间隔排序已使数据趋于有序,最终的插入排序效率大大提高。
复杂度分析:
- 最好时间复杂度:O(n log n)(取决于间隔序列)
- 最坏时间复杂度:O(n²)(使用原始 Shell 间隔序列)至 O(n^(4/3))(使用 Sedgewick 序列)
- 平均时间复杂度:取决于间隔序列,通常介于 O(n log n) 和 O(n²) 之间
- 空间复杂度:O(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 arr
五、快速排序(Quick Sort)
原理简述: 快速排序采用分治策略,选取一个基准元素(pivot),将数组划分为两部分——小于基准的元素和大于基准的元素,然后对两部分递归排序。快速排序的核心在于分区操作(partition)。
复杂度分析:
- 最好时间复杂度:O(n log n)(每次恰好均匀划分)
- 最坏时间复杂度:O(n²)(每次取到最值作为基准,如已排序数组取首元素)
- 平均时间复杂度:O(n log n)
- 空间复杂度:O(log n)(递归栈深度)
- 稳定性:不稳定
def quick_sort(arr, low=0, high=None):
if high is None:
high = len(arr) - 1
if low < high:
pivot_idx = partition(arr, low, high)
quick_sort(arr, low, pivot_idx - 1)
quick_sort(arr, pivot_idx + 1, high)
return arr
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
快速排序是实践中最常用的排序算法之一,其平均性能极为优秀。通过随机化选取 pivot 或三数取中法(median-of-three),可以有效避免最坏情况。
六、归并排序(Merge Sort)
原理简述: 归并排序同样采用分治策略,将数组递归地分成两半,分别排序后再合并(merge)。合并操作是归并排序的核心,它将两个有序子数组合并为一个有序数组。
复杂度分析:
- 最好时间复杂度:O(n log n)
- 最坏时间复杂度:O(n log n)
- 平均时间复杂度:O(n log n)
- 空间复杂度:O(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):
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
归并排序性能稳定,不受输入数据分布影响,是外部排序(数据量超出内存时)的首选算法。
七、堆排序(Heap Sort)
原理简述: 堆排序利用二叉堆(通常是最大堆)这种数据结构。首先将数组构建为最大堆,然后反复将堆顶(最大值)与末尾元素交换,缩小堆的范围并重新调整堆结构,从而实现排序。
复杂度分析:
- 最好时间复杂度:O(n log n)
- 最坏时间复杂度:O(n log n)
- 平均时间复杂度:O(n log n)
- 空间复杂度:O(1)
- 稳定性:不稳定
def heap_sort(arr):
n = len(arr)
# 构建最大堆
for i in range(n // 2 - 1, -1, -1):
heapify(arr, n, i)
# 逐个提取堆顶元素
for i in range(n - 1, 0, -1):
arr[0], arr[i] = arr[i], arr[0]
heapify(arr, i, 0)
return arr
def heapify(arr, n, 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)
堆排序的优势在于最坏情况下仍保证 O(n log n) 且只需 O(1) 额外空间,但由于缓存不友好(跳跃式内存访问),实际运行速度通常不如快速排序。
八、计数排序(Counting Sort)
原理简述: 计数排序是一种非比较排序算法。它通过统计每个元素出现的次数,然后根据统计信息将元素放到正确位置。适用于数据范围有限的整数排序。
复杂度分析:
- 时间复杂度:O(n + k),其中 k 为数据范围
- 空间复杂度:O(n + k)
- 稳定性:稳定
def counting_sort(arr):
if not arr:
return arr
max_val = max(arr)
min_val = min(arr)
range_val = max_val - min_val + 1
count = [0] * range_val
output = [0] * len(arr)
for num in arr:
count[num - min_val] += 1
# 累加计数
for i in range(1, range_val):
count[i] += count[i - 1]
# 从后向前遍历以保持稳定性
for i in range(len(arr) - 1, -1, -1):
output[count[arr[i] - min_val] - 1] = arr[i]
count[arr[i] - min_val] -= 1
return output
九、桶排序(Bucket Sort)
原理简述: 桶排序将数据分配到若干个”桶”中,每个桶内部独立排序(通常使用插入排序或其他排序算法),最后将所有桶的结果拼接起来。当数据均匀分布时,桶排序效率极高。
复杂度分析:
- 最好时间复杂度:O(n + k)(数据均匀分布,k 为桶的数量)
- 最坏时间复杂度:O(n²)(所有数据落入同一个桶)
- 平均时间复杂度:O(n + n²/k + k),当 k ≈ n 时为 O(n)
- 空间复杂度:O(n + k)
- 稳定性:稳定(取决于桶内排序算法)
def bucket_sort(arr, bucket_count=10):
if not arr:
return arr
min_val, max_val = min(arr), max(arr)
bucket_range = (max_val - min_val) / bucket_count + 1
buckets = [[] for _ in range(bucket_count)]
for num in arr:
idx = int((num - min_val) / bucket_range)
buckets[idx].append(num)
result = []
for bucket in buckets:
result.extend(insertion_sort(bucket)) # 桶内使用插入排序
return result
十、基数排序(Radix Sort)
原理简述: 基数排序按照位数从低位到高位(LSD)或从高位到低位(MSD)依次进行排序,每一位的排序使用稳定的排序算法(通常是计数排序)。适用于整数或等长字符串排序。
复杂度分析:
- 时间复杂度:O(d × (n + k)),d 为最大位数,k 为基数(如十进制 k=10)
- 空间复杂度:O(n + k)
- 稳定性:稳定
def radix_sort(arr):
if not arr:
return arr
max_val = max(arr)
exp = 1
while max_val // exp > 0:
counting_sort_by_digit(arr, exp)
exp *= 10
return arr
def counting_sort_by_digit(arr, exp):
n = len(arr)
output = [0] * n
count = [0] * 10
for num in arr:
idx = (num // exp) % 10
count[idx] += 1
for i in range(1, 10):
count[i] += count[i - 1]
for i in range(n - 1, -1, -1):
idx = (arr[i] // exp) % 10
output[count[idx] - 1] = arr[i]
count[idx] -= 1
for i in range(n):
arr[i] = output[i]
完整对比表格
| 排序算法 | 最好时间 | 平均时间 | 最坏时间 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|---|
| 冒泡排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 |
| 选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
| 插入排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 |
| 希尔排序 | O(n log n) | O(n^1.3) | O(n²) | O(1) | 不稳定 |
| 快速排序 | O(n log n) | O(n log n) | O(n²) | O(log n) | 不稳定 |
| 归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 |
| 堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 |
| 计数排序 | O(n+k) | O(n+k) | O(n+k) | O(n+k) | 稳定 |
| 桶排序 | O(n+k) | O(n+k) | O(n²) | O(n+k) | 稳定 |
| 基数排序 | O(d(n+k)) | O(d(n+k)) | O(d(n+k)) | O(n+k) | 稳定 |
注:k 为数据范围或桶数量,d 为最大位数。
实际应用场景分析
小规模数据(n < 50): 插入排序是最佳选择。常数因子小,且对近乎有序的数据极为高效。
中等规模通用排序: 快速排序在大多数场景中是最快的比较排序算法,平均情况下的常数因子很小。
需要稳定排序: 归并排序是稳定且性能可预测的 O(n log n) 算法。在需要保持相等元素原始顺序的场景中(如多关键字排序),稳定性至关重要。
内存受限: 堆排序只需 O(1) 额外空间,适合内存紧张的嵌入式系统。
数据范围有限的整数排序: 计数排序或基数排序可以实现线性时间复杂度,远超比较排序的 O(n log n) 理论下界。
大规模外部排序: 归并排序天然适合外部排序,因为其顺序访问模式适合磁盘 I/O。
数据均匀分布的浮点数排序: 桶排序可以实现接近线性的排序效率。
各语言标准库排序的底层实现
Python — Timsort
Python 的内置 sort() 和 sorted() 使用 Timsort 算法,由 Tim Peters 于 2002 年设计。Timsort 是一种混合排序算法,结合了归并排序和插入排序的优点:
- 首先识别数据中已有序的子序列(称为 “run”)
- 对于短的 run,使用插入排序扩展到最小长度(通常为 32 或 64)
- 然后使用归并排序合并这些 run
- 通过巧妙的合并策略(galloping mode)优化合并效率
Timsort 的时间复杂度为 O(n log n),在已部分有序的数据上表现尤为出色,最好情况可达 O(n)。
C — qsort
C 标准库的 qsort() 通常基于快速排序实现,但具体实现因编译器而异。glibc 的实现采用了归并排序(当有足够内存时)或快速排序(内存不足时),并在小数组上使用插入排序优化。MSVC 的实现使用内省排序(Introsort),结合快速排序、堆排序和插入排序。
Java — Dual-Pivot Quicksort 与 Timsort
Java 对基本类型数组使用 Dual-Pivot Quicksort(双基准快速排序,由 Vladimir Yaroslavskiy 设计),选取两个基准将数组分为三部分,在实践中比经典快速排序更快。对于对象数组,Java 使用 Timsort,以保证排序的稳定性。
C++ — Introsort
C++ STL 的 std::sort 通常使用 Introsort(内省排序),由 David Musser 于 1997 年提出。它以快速排序开始,当递归深度超过 2×log₂(n) 时切换到堆排序以保证 O(n log n) 的最坏情况,对于小数组则使用插入排序。
Go — Pattern-Defeating Quicksort (pdqsort)
Go 语言标准库采用 pdqsort,这是一种现代化的混合排序算法,在随机数据上接近快速排序的性能,同时能高效处理已排序、逆序、大量重复等特殊模式的数据。
总结与选择建议
排序算法没有绝对的”最好”,只有最适合特定场景的选择。以下是简要的决策指南:
- 不确定时,使用语言标准库:现代语言标准库的排序实现经过精心优化,是大多数场景的最佳选择。
- 追求通用性能:快速排序及其变体(Introsort、pdqsort)是最佳选择。
- 需要稳定排序:选择归并排序或 Timsort。
- 内存极度受限:选择堆排序(O(1) 空间、O(n log n) 时间)。
- 数据特征明确:
- 数据范围小 → 计数排序
- 整数且位数有限 → 基数排序
- 均匀分布 → 桶排序
- 近乎有序 → 插入排序或 Timsort
- 教学与面试:冒泡排序和选择排序适合理解排序的基本概念,但在生产环境中几乎不使用。
理解这些算法不仅有助于选择合适的排序方案,更能培养分析问题、设计算法的思维能力。比较排序的理论下界是 O(n log n),而非比较排序在特定条件下可以突破这一限制——这一思想本身就是算法设计中”打破常规约束”的精妙范例。