用 Python 看懂五种排序算法:从冒泡排序到 Timsort

2026-09-23 33 预计阅读时间: 1 分钟
来源: realpython.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:9 分钟

排序算法不只是面试题。它们集中展示了时间复杂度、空间换时间、稳定性以及输入特征如何影响程序性能。冒泡排序、插入排序、归并排序、快速排序和 Timsort 的差别,也不能只用一个 Big O 符号概括。

五种算法到底在做什么

算法 最好时间 平均时间 最坏时间 额外空间 稳定性
冒泡排序 O(n) O(n²) O(n²) O(1) 稳定
插入排序 O(n) O(n²) O(n²) O(1) 稳定
归并排序 O(n log n) O(n log n) O(n log n) O(n) 通常稳定
快速排序 O(n log n) O(n log n) O(n²) 取决于实现 通常不稳定
Timsort O(n) O(n log n) O(n log n) O(n) 稳定

这里的“最好情况”依赖具体实现。例如,冒泡排序只有在加入“本轮是否发生交换”的检测后,才能对已排序输入达到 O(n)。快速排序则高度依赖枢轴选择:持续选中极端值可能产生严重不平衡的分区,使时间复杂度退化为 O(n²)。

这几种算法的核心策略可以概括为:

  • 冒泡排序反复比较相邻元素,把较大的元素逐步推向末尾。
  • 插入排序维护一个已排序区间,将新元素插入合适位置。
  • 归并排序拆分序列,再线性合并已经排好序的子序列。
  • 快速排序围绕枢轴分区,再递归处理两侧。
  • Timsort利用现实数据中已有的有序片段,并结合插入排序与归并策略。

Big O 相同,不代表实际表现相同

归并排序、快速排序和 Timsort 的典型复杂度都可以出现 O(n log n),但它们的工程表现并不等价。

归并排序的最坏时间仍是 O(n log n),性能容易预测,代价是通常需要 O(n) 辅助空间。快速排序往往具有良好的局部性,但朴素实现可能退化,而且递归深度也需要控制。Timsort 会识别输入中已经递增或递减的连续片段,因此面对部分有序的数据时尤其有效。

稳定性同样会改变结果。稳定排序会保留“键相同元素”的原始相对顺序。假设员工记录已经按入职时间排列,再按部门执行稳定排序,同一部门内的员工仍会保持原有的入职时间次序。Python 的 list.sort()sorted() 都是稳定排序。

可运行实验:比较结果、稳定性与耗时

下面的程序实现了冒泡排序、插入排序、归并排序和一个教学用途的快速排序,并把它们与 Python 内置的 Timsort 进行比较。将代码保存为 sorting_demo.py 后运行 python sorting_demo.py 即可。

from __future__ import annotations

from random import Random
from time import perf_counter
from typing import Callable, TypeVar

T = TypeVar("T")


def bubble_sort(values: list[T]) -> list[T]:
    result = values.copy()
    for end in range(len(result) - 1, 0, -1):
        swapped = False
        for i in range(end):
            if result[i] > result[i + 1]:
                result[i], result[i + 1] = result[i + 1], result[i]
                swapped = True
        if not swapped:
            break
    return result


def insertion_sort(values: list[T]) -> list[T]:
    result = values.copy()
    for i in range(1, len(result)):
        current = result[i]
        j = i - 1
        while j >= 0 and result[j] > current:
            result[j + 1] = result[j]
            j -= 1
        result[j + 1] = current
    return result


def merge_sort(values: list[T]) -> list[T]:
    if len(values) <= 1:
        return values.copy()

    middle = len(values) // 2
    left = merge_sort(values[:middle])
    right = merge_sort(values[middle:])
    merged: list[T] = []
    i = j = 0

    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            merged.append(left[i])
            i += 1
        else:
            merged.append(right[j])
            j += 1

    return merged + left[i:] + right[j:]


def quicksort(values: list[T]) -> list[T]:
    if len(values) <= 1:
        return values.copy()

    pivot = values[len(values) // 2]
    lower = [value for value in values if value < pivot]
    equal = [value for value in values if value == pivot]
    higher = [value for value in values if value > pivot]
    return quicksort(lower) + equal + quicksort(higher)


def benchmark(name: str, algorithm: Callable[[list[int]], list[int]], data: list[int]) -> None:
    start = perf_counter()
    result = algorithm(data)
    elapsed_ms = (perf_counter() - start) * 1000
    assert result == sorted(data)
    print(f"{name:14} {elapsed_ms:9.3f} ms")


def main() -> None:
    rng = Random(42)
    data = [rng.randrange(100_000) for _ in range(3_000)]

    algorithms = {
        "bubble": bubble_sort,
        "insertion": insertion_sort,
        "merge": merge_sort,
        "quicksort": quicksort,
        "built-in": sorted,
    }

    for name, algorithm in algorithms.items():
        benchmark(name, algorithm, data)

    employees = [
        {"name": "Chen", "department": "Platform"},
        {"name": "Li", "department": "Data"},
        {"name": "Wang", "department": "Platform"},
    ]
    print(sorted(employees, key=lambda item: item["department"]))


if __name__ == "__main__":
    main()

这段代码适合观察趋势,但不是严谨的科学基准。进程调度、Python 解释器开销、数据规模和输入分布都会影响结果。进行正式测量时,可以使用标准库 timeitpython -m timeit,并重复运行多次。

教学版 quicksort() 还创建了三个新列表,因此不能把它的内存表现等同于原地快速排序。它的意义是清楚展示“分区并递归”的结构,而不是替代生产环境中的 sorted()

用问题检查自己是否真的理解

阅读代码后,可以尝试回答这些问题:

  1. 为什么带提前退出判断的冒泡排序能在已排序输入上达到 O(n)?
  2. 为什么插入排序虽然最坏为 O(n²),却仍适合处理很短或接近有序的序列?
  3. 归并排序为什么能保证 O(n log n),又为什么通常需要额外空间?
  4. 什么样的枢轴选择会使快速排序退化为 O(n²)?
  5. 当记录具有相同排序键时,稳定排序保留的是什么信息?
  6. 为什么不能仅根据平均时间复杂度断定快速排序一定优于 Timsort?

还可以把示例中的 data 分别替换为 list(range(3000))、逆序列表和包含大量重复值的列表,观察不同算法如何响应输入结构。

实际项目中的选择

在 Python 业务代码中,默认选择通常应是 sorted(iterable, key=...)list.sort(key=...)。前者返回新列表,后者原地修改现有列表;两者都使用稳定的 Timsort,并经过成熟优化。

只有在学习算法、满足特殊内存约束、实现底层库,或者需要针对特定数据结构优化时,才值得自行实现排序。做决定时检查四件事:输入规模、数据是否部分有序、是否要求稳定、是否允许分配额外内存。Big O 是重要的起点,但真正的工程选择还要结合常数开销、数据分布和实现质量。


相关推荐