排序算法:从冒泡排序到归并排序

摘要

排序是算法学习中最经典的主题之一。不同排序算法在时间复杂度、空间占用、稳定性和实现复杂度方面各有取舍。理解排序过程,可以帮助开发者建立分治、交换、选择和合并等基本算法思想。

本文从冒泡排序、选择排序和插入排序开始,介绍归并排序和快速排序的基本实现,并比较它们的复杂度、稳定性和适用场景。

一、背景与问题

排序看似只是把数字从小到大排列,但真实业务中还需要考虑:

  • 数据量有多大。
  • 数据是否已经部分有序。
  • 是否允许修改原数据。
  • 是否需要保持相等元素的原始顺序。
  • 是否需要稳定的最坏情况性能。
  • 是否有额外内存限制。

例如,按订单金额排序很简单,但如果两个订单金额相同,还希望较早创建的订单排在前面,就涉及稳定性问题。

排序算法不是越复杂越好。应根据数据规模、数据特征和资源约束选择实现。

二、核心概念

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 项目中优先使用稳定成熟的内置排序,同时根据数据规模、稳定性、内存和局部有序性选择合适方案。

参考资料

  1. Python 排序技术:https://docs.python.org/3/howto/sorting.html
  2. Python sorted 文档:https://docs.python.org/3/library/functions.html#sorted
  3. Introduction to Algorithms:https://mitpress.mit.edu/9780262046305/introduction-to-algorithms/
相关推荐
万年咸鱼1 小时前
C语言内功心法篇——变量与数据类型
c语言·jvm·算法
Omics Pro1 小时前
反式感知虚拟细胞!超越线性序列
数据库·人工智能·算法·机器学习·自然语言处理
-dzk-1 小时前
【动态规划】LC 152.乘积最大子数组
算法·动态规划
积流成海1 小时前
一致性哈希原理与实现:分布式系统负载均衡
算法·负载均衡·哈希算法·哈希函数·盘子工具站
Ada's11 小时前
【计算机基础系列】003:Python数据结构
开发语言·数据结构·python
Nil20812 小时前
leetcode 118杨辉三角
算法·leetcode·职场和发展
wanderist.14 小时前
线性筛法详解:从筛质数到欧拉函数、Möbius 函数与约数函数
java·数据结构·算法
All for pursuit.14 小时前
【设计-1】208.实现Trie (前缀树)
数据结构·c++·算法·leetcode
weixin_3077791314 小时前
C++代码实现MATLAB中的crossval函数功能
开发语言·c++·算法·matlab