摘要
排序是算法学习中最经典的主题之一。不同排序算法在时间复杂度、空间占用、稳定性和实现复杂度方面各有取舍。理解排序过程,可以帮助开发者建立分治、交换、选择和合并等基本算法思想。
本文从冒泡排序、选择排序和插入排序开始,介绍归并排序和快速排序的基本实现,并比较它们的复杂度、稳定性和适用场景。
一、背景与问题
排序看似只是把数字从小到大排列,但真实业务中还需要考虑:
- 数据量有多大。
- 数据是否已经部分有序。
- 是否允许修改原数据。
- 是否需要保持相等元素的原始顺序。
- 是否需要稳定的最坏情况性能。
- 是否有额外内存限制。
例如,按订单金额排序很简单,但如果两个订单金额相同,还希望较早创建的订单排在前面,就涉及稳定性问题。
排序算法不是越复杂越好。应根据数据规模、数据特征和资源约束选择实现。
二、核心概念
1. 比较排序
比较排序通过比较元素大小确定顺序。冒泡、选择、插入、归并和快速排序都属于比较排序。
2. 稳定性
稳定排序会保持相等元素原有的相对顺序:
text
原始:A(90), B(80), C(90)
按分数稳定排序:B(80), A(90), C(90)
稳定性在多字段排序和业务记录排序中很重要。
3. 原地排序
原地排序主要在输入数组上操作,只使用有限额外空间。归并排序通常需要额外数组,快速排序可以通过原地分区减少空间。
4. 分治
归并排序和快速排序都使用分治思想:
text
大问题
→ 拆成更小的子问题
→ 分别解决
→ 合并或组织结果
分治是很多高效算法的核心模式。
5. 复杂度
| 算法 | 平均时间 | 最坏时间 | 额外空间 | 稳定性 |
|---|---|---|---|---|
| 冒泡 | O(n²) |
O(n²) |
O(1) |
稳定 |
| 选择 | 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) 平均 |
通常不稳定 |
三、工作原理
1. 冒泡排序
冒泡排序反复比较相邻元素,把较大元素逐步移动到数组末尾:
text
[5, 3, 4, 1]
→ [3, 4, 1, 5]
→ [3, 1, 4, 5]
→ [1, 3, 4, 5]
它容易理解,但大量数据下效率较低。
2. 选择排序
选择排序每轮从未排序区间寻找最小值,与当前位置交换。它的比较次数基本固定,适合理解"选择最优元素"的思想。
3. 插入排序
插入排序维护一个已排序区间,把新元素插入到正确位置。对于基本有序的数据,它可能比复杂排序更有效。
4. 归并排序
归并排序把数组不断拆分,再将两个有序数组合并:
text
[8, 3, 5, 4]
→ [8, 3] + [5, 4]
→ [3, 8] + [4, 5]
→ [3, 4, 5, 8]
每层合并处理 n 个元素,总共有 log n 层,因此复杂度为 O(n log n)。
5. 快速排序
快速排序选择一个基准值,把数组分成较小和较大的部分,再递归处理子区间。基准选择不理想时,可能退化为 O(n²)。
四、实战示例
1. 冒泡排序
python
def bubble_sort(values: list[int]) -> list[int]:
result = values.copy()
for end in range(len(result) - 1, 0, -1):
swapped = False
for index in range(end):
if result[index] > result[index + 1]:
result[index], result[index + 1] = (
result[index + 1],
result[index],
)
swapped = True
if not swapped:
break
return result
如果某一轮没有发生交换,说明数组已经有序,可以提前结束。
2. 选择排序
python
def selection_sort(values: list[int]) -> list[int]:
result = values.copy()
for start in range(len(result)):
min_index = start
for index in range(start + 1, len(result)):
if result[index] < result[min_index]:
min_index = index
result[start], result[min_index] = (
result[min_index],
result[start],
)
return result
选择排序的交换次数较少,但比较次数仍然是 O(n²)。
3. 插入排序
python
def insertion_sort(values: list[int]) -> list[int]:
result = values.copy()
for index in range(1, len(result)):
current = result[index]
position = index - 1
while position >= 0 and result[position] > current:
result[position + 1] = result[position]
position -= 1
result[position + 1] = current
return result
插入排序只移动大于当前值的元素,因此可以保持相等元素的顺序。
4. 合并两个有序数组
python
def merge(left: list[int], right: list[int]) -> list[int]:
result: list[int] = []
left_index = 0
right_index = 0
while left_index < len(left) and right_index < len(right):
if left[left_index] <= right[right_index]:
result.append(left[left_index])
left_index += 1
else:
result.append(right[right_index])
right_index += 1
result.extend(left[left_index:])
result.extend(right[right_index:])
return result
使用 <= 优先取左数组元素,可以保持归并排序的稳定性。
5. 归并排序
python
def merge_sort(values: list[int]) -> list[int]:
if len(values) <= 1:
return values.copy()
middle = len(values) // 2
left = merge_sort(values[:middle])
right = merge_sort(values[middle:])
return merge(left, right)
示例实现使用切片,会产生额外复制。工程代码可以根据内存和性能要求采用索引区间优化。
6. 快速排序
python
def quick_sort(values: list[int]) -> list[int]:
if len(values) <= 1:
return values.copy()
pivot = values[len(values) // 2]
smaller = [value for value in values if value < pivot]
equal = [value for value in values if value == pivot]
larger = [value for value in values if value > pivot]
return quick_sort(smaller) + equal + quick_sort(larger)
这个版本易于理解,但会创建多个列表。在数据量较大或递归深度不可控时,优先使用 Python 内置排序。
7. 使用内置排序
python
orders = [
{"id": "A001", "amount": 1200},
{"id": "A002", "amount": 800},
{"id": "A003", "amount": 1200},
]
sorted_orders = sorted(
orders,
key=lambda order: order["amount"],
)
Python 的 sorted 和 list.sort 使用稳定的高效排序实现。业务代码通常应优先使用内置排序,而不是重复实现算法。
8. 多字段稳定排序
python
sorted_orders = sorted(
orders,
key=lambda order: (
order["amount"],
order["id"],
),
)
元组键可以表达多字段排序规则。生产代码中应明确升序、降序和空值处理。
9. 基准测试
python
from random import Random
from timeit import timeit
rng = Random(42)
values = [rng.randint(0, 1_000_000) for _ in range(2_000)]
print(timeit(lambda: insertion_sort(values), number=3))
print(timeit(lambda: merge_sort(values), number=3))
print(timeit(lambda: sorted(values), number=3))
基准测试应使用相同数据、重复多次,并区分随机数据、有序数据和逆序数据。
五、常见问题与实践建议
1. 为什么学习低效排序?
冒泡、选择和插入排序有助于理解比较、交换、循环不变量和复杂度。学习它们是为了掌握思想,不是为了在生产环境替代高效内置实现。
2. 归并排序为什么需要额外空间?
合并两个有序数组时,通常需要一个新数组保存结果。因此经典实现的额外空间为 O(n)。
3. 快速排序为什么会退化?
如果每次选择的基准都接近最大值或最小值,分区会极度不平衡,递归规模接近 n,复杂度退化为 O(n²)。
4. 稳定性在什么场景重要?
多次排序时,如果先按时间排序,再按金额排序,稳定性可以保留相同金额记录的时间顺序。报表和业务列表中经常需要稳定排序。
5. sort 和 sorted 有什么区别?
list.sort() 原地修改列表并返回 None;sorted() 返回一个新的有序列表,适合不希望修改原数据的场景。
六、进阶思考
1. 外部排序
当数据无法全部放入内存时,可以分块排序,再归并多个有序文件:
text
大文件
→ 分块读取
→ 每块独立排序
→ 写入临时文件
→ 多路归并
→ 输出最终结果
2. Top K 问题
如果只需要最大或最小的 K 个元素,不必完整排序全部数据,可以使用堆维护 K 个候选值,复杂度通常为 O(n log k)。
3. 排序键的成本
复杂的排序键函数可能被调用很多次。可以先提取键、缓存计算结果,或使用结构化字段减少重复计算。
4. 空值和特殊值
生产排序必须明确缺失值放在前面还是后面,字符串大小写如何处理,日期是否统一时区,金额精度如何比较。
结论
排序算法展示了交换、选择、插入、分治和合并等核心思想。简单排序便于学习,归并排序提供稳定的 O(n log n) 性能,快速排序平均高效但需要关注最坏情况。
实际 Python 项目中优先使用稳定成熟的内置排序,同时根据数据规模、稳定性、内存和局部有序性选择合适方案。
参考资料
- Python 排序技术:https://docs.python.org/3/howto/sorting.html
- Python
sorted文档:https://docs.python.org/3/library/functions.html#sorted - Introduction to Algorithms:https://mitpress.mit.edu/9780262046305/introduction-to-algorithms/