Py算法与数据结构

14 快速排序

双指针分区、枢轴、退化与短侧优先

1. 分而治之先做划分

选定枢轴 pivot,把不大于它的元素分到左边,不小于它的分到右边;枢轴放到最终位置后,再分别排序两个子数组。相等键可能出现在两侧,不能把有重复值的例子理解为严格小于/大于。

2. 原文双指针划分的 Python 版本

def median_index(a, low, high, key=lambda x: x):
    mid = (low+high)//2
    return sorted([low, mid, high], key=lambda i: key(a[i]))[1]
def partition(a, low, high, key=lambda x: x, median=False):
    if median:
        p = median_index(a, low, high, key)
        a[p], a[high] = a[high], a[p]
    pivot = key(a[high])
    i, j = low, high-1
    while True:
        while i <= j and key(a[i]) < pivot:
            i += 1
        while i <= j and key(a[j]) > pivot:
            j -= 1
        if i >= j:
            break
        a[i], a[j] = a[j], a[i]
        i += 1
        j -= 1
    a[i], a[high] = a[high], a[i]
    return i

使用闭区间 [low, high]。pivot 在最右端,i 向右找不小于 pivot 的值,j 向左找不大于 pivot 的值。交换后两指针都前进,保证重复值时也能结束。最后交换 a[i] 与枢轴。教材显式检查边界,而不是依赖越界位置。

3. 递归版

def quick_sort(a, key=lambda x: x, median=False, cutoff=0):
    def visit(low, high):
        if low >= high:
            return
        if high-low+1 <= cutoff:
            for i in range(low+1, high+1):
                item, j = a[i], i
                while j > low and key(a[j-1]) > key(item):
                    a[j] = a[j-1]
                    j -= 1
                a[j] = item
            return
        p = partition(a, low, high, key, median)
        visit(low, p-1)
        visit(p+1, high)
    visit(0, len(a)-1)
    return a

终止条件 low >= high,同时覆盖空区间和单项;递归区间排除已经固定的枢轴,否则问题规模可能不缩小。

4. 比较、交换与递归树

一次划分工作为 O(m),m 是当前区间长度。若分割比较均衡,深度约 log n,每层总工作 O(n),平均时间 O(n log n)。若连续分成 n−1 与 0,总工作 1+2+…+(n−1),为 O(n²)。

5. 枢轴策略与哨兵

固定最右值遇到升序数据容易退化。三数取中取左、中、右三项的中位值,不是平均数;仍不能保证避免所有最坏输入。随机枢轴可改善期望表现,但也不是逐次最坏 O(n log n) 保证。

原书右端 pivot 可作为左向右扫描的哨兵;本教材保留边界判断,更方便检查。Python a[-1] 合法,不意味着可以把负下标当成数组左边界保护。

6. 小区间切换插入排序

小区间上复杂划分的开销可能较大。可以设 cutoff,剩余小区间用插入排序。本例用 8 作演示参数,并非所有机器的最优阈值。

7. 显式栈和较短侧优先

def quick_sort_iterative(a, key=lambda x: x, median=True, cutoff=8):
    stack = [(0, len(a)-1)]
    while stack:
        low, high = stack.pop()
        while low < high:
            if high-low+1 <= cutoff:
                for i in range(low+1, high+1):
                    item, j = a[i], i
                    while j > low and key(a[j-1]) > key(item):
                        a[j] = a[j-1]
                        j -= 1
                    a[j] = item
                break
            p = partition(a, low, high, key, median)
            if p-low < high-p:
                if p+1 < high:
                    stack.append((p+1, high))
                high = p-1  # 立即处理较短侧
            else:
                if low < p-1:
                    stack.append((low, p-1))
                low = p+1
    return a

将较长区间压栈,立即处理较短区间。每次立即进入的短侧规模至多约为原区间一半,待处理区间栈可控制在 O(log n)。这个改进控制空间,不能把坏枢轴导致的 O(n²) 时间变成 O(n log n)。

8. 稳定性与空间

该原地快排不稳定:[2A,2B,1X] 按末尾 1X 划分后可得到 [1X,2B,2A]。递归版平均栈 O(log n),最坏 O(n);较短侧优先的迭代版工作栈 O(log n)。这里的“原地”指不需要 O(n) 合并缓冲,不表示完全无栈空间。

9. 运行示例

a = [55,74,3,45,13,87,46,30]
print(quick_sort(a.copy()))
print(quick_sort_iterative(a.copy(), median=True, cutoff=8))
print(quick_sort([]))  # []
print(quick_sort([5,5,5]))  # [5,5,5]

10. 测试重点

空输入、单项、升序、降序、全相等、重复键、负数,以及划分后两侧的关系。不要把“快速排序”这个名称当作一定最快的保证。深递归在 Python 中可能触发 RecursionError,迭代版可以避免函数递归限制。