0Pricing
Coding Interview Prep · 课时

非比较排序与 Python 的 sort()

探索整数数组的计数排序和基数排序,并理解 Python 内置 sort 调用底层的 Timsort 工作方式。

非比较排序与 Python 的 sort() 是 CoddyKit 上的免费 Coding Interview Prep 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Coding Interview Prep 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Coding Interview Prep 课程共包含 4 节课。

比较排序的 O(n log n) 下界

任何仅通过元素比较来确定顺序的排序算法,在最坏情况下都至少需要进行 Ω(n log n) 次比较。这可以通过决策树论证得到:对 n 个元素排序,需要区分 n! 种可能的排列。二叉决策树中的每个节点都代表一次比较,因此至少需要 log₂(n!) ≈ n log₂(n) 层。要突破这个下界,就需要关于元素的额外信息,例如元素是有界整数。

import math

for n in [5, 10, 100, 1000]:
    lower_bound = n * math.log2(n)
    factorial_log = sum(math.log2(i) for i in range(1, n+1))
    print(f'n={n}: n*log2(n)={lower_bound:.1f}, log2(n!)={factorial_log:.1f}')

# n log n is a tight bound on comparison-based sorting

计数排序:按频次排序

计数排序通过统计每个值出现的频次,再根据这些计数重建有序数组。它要求事先知道值的范围 [0, k)。时间复杂度为 O(n + k),空间复杂度为 O(k)。当 k 相对于 n 较小时(例如对 0-120 岁的年龄或个位数进行排序),计数排序优于所有比较排序。当 k 很大时,O(k) 的空间开销会使它变得不实用。

def counting_sort(arr, k=None):
    if not arr: return []
    if k is None: k = max(arr) + 1
    count = [0] * k
    for n in arr:
        count[n] += 1
    result = []
    for val, freq in enumerate(count):
        result.extend([val] * freq)
    return result

arr = [4, 2, 2, 8, 3, 3, 1]
print(counting_sort(arr))  # [1, 2, 2, 3, 3, 4, 8]
# O(n + k) where k = 9 (max value + 1)

使用累积计数的稳定计数排序

对于稳定计数排序(按键对对象进行排序时非常重要),请计算累积计数,使得 cum[v] 表示值 v 在输出中的起始位置。从右向左扫描输入数组,将每个元素放置在位置 cum[key] - 1,然后将该位置递减。这样即可得到稳定排序——具有相同键的元素会保持其原始相对顺序。

def counting_sort_stable(arr, k):
    count = [0] * k
    for n in arr: count[n] += 1
    # Cumulative counts: count[v] = first position for value v
    for i in range(1, k): count[i] += count[i-1]
    output = [0] * len(arr)
    # Fill from right to maintain stability
    for n in reversed(arr):
        count[n] -= 1
        output[count[n]] = n
    return output

print(counting_sort_stable([4,2,2,8,3,3,1], 9))
# [1, 2, 2, 3, 3, 4, 8]

基数排序:逐位排序

基数排序按位对整数进行排序,从最低有效位(LSD)到最高有效位(MSD),在每个数位位置使用稳定排序(例如计数排序)。经过 d 轮处理(每一轮对应一个数位)后,数组就会完全有序。时间复杂度为 O(d × (n + k)),其中 d = 数位数量,k = 基数(通常为 10)。对于上界为 W 的 n 个整数,d = log_k(W),因此总复杂度为 O(n log_k(W))。

def radix_sort(arr):
    if not arr: return []
    max_val = max(arr)
    exp = 1  # current digit position (1, 10, 100, ...)
    while max_val // exp > 0:
        arr = 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 n_ in arr: count[(n_ // exp) % 10] += 1
    for i in range(1, 10): count[i] += count[i-1]
    for n_ in reversed(arr):
        d = (n_ // exp) % 10
        count[d] -= 1
        output[count[d]] = n_
    return output

print(radix_sort([170, 45, 75, 90, 802, 24, 2, 66]))
# [2, 24, 45, 66, 75, 90, 170, 802]

桶排序:分配到各个桶

桶排序根据值的范围将元素分配到固定数量的桶中,对每个桶进行排序(对于较小的桶使用插入排序),然后将各个桶连接起来。对于均匀分布在 [0, 1) 中的数据,使用 n 个桶时平均时间复杂度为 O(n)。时间复杂度平均为 O(n + k),最坏情况下为 O(n²)(所有元素都落入同一个桶)。当您已知数据分布且其近似均匀时,这种排序最有用。

def bucket_sort(arr):
    if not arr: return []
    n = len(arr)
    min_v, max_v = min(arr), max(arr)
    if min_v == max_v: return arr[:]
    buckets = [[] for _ in range(n)]
    # Map each value to a bucket index
    for v in arr:
        idx = int((v - min_v) / (max_v - min_v + 1e-9) * n)
        idx = min(idx, n - 1)
        buckets[idx].append(v)
    result = []
    for bucket in buckets:
        bucket.sort()  # insertion sort for small buckets
        result.extend(bucket)
    return result

print(bucket_sort([0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21]))
# sorted list

Python 的 Timsort 原理

Python 的 sorted() 和 list.sort() 使用Timsort,由 Tim Peters 于 2002 年设计。Timsort 是归并排序和插入排序的混合算法。它会扫描“自然运行段”(已经有序的子序列),并使用插入排序将运行段扩展到最多 64 个元素。随后,它使用带有多项优化的归并排序来合并这些运行段:跳跃合并(当某个运行段占优势时批量跳过元素)以及运行段长度堆叠。

# Timsort properties:
# - Stable
# - O(n log n) worst case
# - O(n) best case (data already sorted)
# - O(n) auxiliary space
# - Highly optimised for real-world data with runs

import time

# Nearly sorted data: Timsort is extremely fast
nearly_sorted = list(range(10000))
nearly_sorted[-1] = 0  # one mis-placed element

t = time.perf_counter()
not_used = sorted(nearly_sorted)
elapsed = time.perf_counter() - t
print(f'Timsort on nearly-sorted n=10000: {elapsed*1000:.3f} ms')

Python 的 sort() 与 sorted():关键区别

list.sort() 会原地排序,返回 None,并且只能用于列表。sorted(iterable) 可用于任何可迭代对象(元组、生成器、字典),并返回一个新列表。两者都接受 key 和 reverse 参数。一个常见错误是:将 lst.sort() 的返回值赋给变量,然后疑惑它为什么是 None。当您需要排序后的版本并且希望保留原对象时,请始终使用 sorted()。

nums = [3, 1, 4, 1, 5, 9]

# in-place: returns None
result = nums.sort()
print(result)  # None  (common bug!)
print(nums)    # [1, 1, 3, 4, 5, 9]  (modified)

nums2 = [3, 1, 4, 1, 5, 9]
# out-of-place: returns new list
result2 = sorted(nums2)
print(result2)  # [1, 1, 3, 4, 5, 9]
print(nums2)    # [3, 1, 4, 1, 5, 9]  (unchanged)

面试中的自定义排序键

Python 的 sort 接受一个 key 函数,并且每个元素只计算一次(不同于 C 语言中会针对每一对元素调用的比较器)。面试中常见的排序键包括:使用 len 获取字符串长度,使用 lambda x: -x 实现降序,使用 lambda x: (x[1], x[0]) 实现多关键字排序,以及使用 str.lower 实现不区分大小写的排序。Python 的排序保证稳定,因此多关键字排序能够正确工作。

# Sort by length, then alphabetically
words = ['banana', 'fig', 'apple', 'date', 'kiwi']
print(sorted(words, key=lambda w: (len(w), w)))
# ['fig', 'date', 'kiwi', 'apple', 'banana']

# Sort integers as strings (largest concatenation first)
nums = [3, 30, 34, 5, 9]
print(sorted(map(str, nums), key=lambda a: a*10, reverse=True))
# ['9', '5', '34', '3', '30']  => '9534330'

# Descending sort
print(sorted([3,1,4,1,5], reverse=True))  # [5,4,3,1,1]

面试中何时使用各种排序

请根据具体情境选择合适的排序:

  • 使用 Python 的 sorted()/list.sort():所有面试题的默认选择——Timsort 是最优的
  • 计数排序:当值是范围较小的有界整数时(0 到 k,且 k 较小)
  • 基数排序:当需要对大量具有已知位宽或数位数量的整数进行排序时
  • 桶排序:当数据是落在已知范围内且均匀分布的浮点数时
  • 实现归并排序:当题目要求您从头编写稳定的 O(n log n) 排序时

# Problem: sort array of 0s, 1s, 2s efficiently
# Counting sort: O(n), O(1) space  (k=3 is tiny)

def sort_012(arr):
    count = [0, 0, 0]
    for n in arr:
        count[n] += 1
    i = 0
    for val in range(3):
        for _ in range(count[val]):
            arr[i] = val; i += 1

arr = [2, 0, 2, 1, 1, 0]
sort_012(arr)
print(arr)  # [0, 0, 1, 1, 2, 2]

不使用排序实现排序:用堆查找前 k 个元素

许多面试题要求得到“类似排序”的结果,却不要求进行完整排序。查找前 k 个元素时,大小为 k 的最小堆的复杂度为 O(n log k)——当 k 远小于 n 时,比 O(n log n) 更快。查找第 k 大的元素时,快速选择的平均复杂度为 O(n)。查找中位数时,双堆方法每次插入的复杂度为 O(log n)。这些部分排序方法值得掌握,因为它们是完整排序的更快替代方案。

import heapq

# Top-k with heap: O(n log k)
def top_k(nums, k):
    return heapq.nlargest(k, nums)  # uses heap of size k internally

print(top_k([3,2,1,5,6,4], 2))    # [6, 5]

# kth largest: quickselect O(n) average
import random
def kth_largest(nums, k):
    def _select(lo, hi, target):
        if lo >= hi: return nums[lo]
        rand_i = random.randint(lo, hi)
        nums[rand_i], nums[hi] = nums[hi], nums[rand_i]
        pivot = nums[hi]; i = lo - 1
        for j in range(lo, hi):
            if nums[j] >= pivot: i+=1; nums[i],nums[j]=nums[j],nums[i]
        nums[i+1],nums[hi]=nums[hi],nums[i+1]
        p = i + 1
        if p == target: return nums[p]
        return _select(lo, p-1, target) if target < p else _select(p+1, hi, target)
    return _select(0, len(nums)-1, k-1)

print(kth_largest([3,2,1,5,6,4], 2))  # 5

多关键字排序中的排序稳定性

稳定性可以实现正确的多关键字排序:先按次要键进行稳定排序,再按主要键进行稳定排序。对于主要键相同的元素,其次要键的顺序会得到保留。这种技术用于数据库(ORDER BY col1, col2)以及基数排序(每一轮数位排序都必须稳定,整个算法才能正确)。Python 的排序始终稳定,因此这种模式能够可靠地工作。

data = [
    ('Alice', 'Math',    90),
    ('Bob',   'Science', 85),
    ('Carol', 'Math',    90),
    ('Dave',  'Science', 90),
]
# Sort by score DESC, then by subject ASC (for ties)
# Step 1: sort by subject (secondary)
data.sort(key=lambda x: x[1])
# Step 2: sort by score DESC (primary, stable)
data.sort(key=lambda x: x[2], reverse=True)
for row in data:
    print(row)
# All score=90 rows: Math before Science (preserved from step 1)

快速检查

请测试您对本课数据结构 & 算法——编程面试准备相关概念的理解。

课程回顾

本课您学到了:基于比较的排序下界为 O(n log n)——要突破这一界限,就需要使用有界整数等非比较信息;计数排序通过统计频率实现 O(n + k),基数排序通过处理各个数位实现总复杂度 O(d × (n + k)),桶排序则利用均匀分布实现平均 O(n);以及Python 的 Timsort 是实际应用中的默认选择——它稳定,最坏情况下为 O(n log n),最佳情况下为 O(n),对于真实数据而言比任何手写替代方案都更快。接下来我们将掌握经典的二分查找。

常见问题解答

「非比较排序与 Python 的 sort()」课时是免费的吗?

是的 — 「非比较排序与 Python 的 sort()」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Coding Interview Prep 课程的其余内容,请升级到 CoddyKit PRO。 Coding Interview Prep 课程共包含 4 节课。

「非比较排序与 Python 的 sort()」这节课中我会学到什么?

探索整数数组的计数排序和基数排序,并理解 Python 内置 sort 调用底层的 Timsort 工作方式。 你通过在浏览器中直接运行的动手代码来练习 Coding Interview Prep,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Coding Interview Prep 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Coding Interview Prep 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。

「非比较排序与 Python 的 sort()」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Coding Interview Prep 课中编写并运行代码吗?

能。每节 Coding Interview Prep 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 冒泡排序与插入排序
  2. 归并排序:分治、排序、合并
  3. 快速排序与枢轴选择
  4. 非比较排序与 Python 的 sort()
← 返回 Coding Interview Prep