算法导论第七章:快速排序的艺术与科学

发布于:2025-06-20 ⋅ 阅读:(14) ⋅ 点赞:(0)

算法导论第七章:快速排序的艺术与科学

本文是《算法导论》精讲专栏第七章,通过分治策略可视化性能对比实验工程优化技巧,结合完整C语言实现,深入解析快速排序的精髓。包含基本实现、随机化优化、三向切分、尾递归消除等高级技术,提供10个以上完整代码实现。

一、快速排序:分治策略的巅峰之作

1.1 快速排序核心思想

graph TD
    A[待排序数组] --> B[选择主元]
    B --> C[划分:小于主元 | 主元 | 大于主元]
    C --> D[递归排序左半部分]
    C --> E[递归排序右半部分]
    D --> F[有序数组]
    E --> F

快速排序三大步骤

  1. 选择主元(Pivot):选取一个元素作为基准
  2. 划分(Partition):将数组分为小于主元和大于主元的两部分
  3. 递归排序:对左右子数组递归应用相同操作

1.2 快速排序性能特征

特性 快速排序 归并排序 堆排序 插入排序
平均时间复杂度 O(n log n) O(n log n) O(n log n) O(n²)
最坏时间复杂度 O(n²) O(n log n) O(n log n) O(n²)
空间复杂度 O(log n) O(n) O(1) O(1)
稳定性 不稳定 稳定 不稳定 稳定
原地排序
缓存友好性

二、快速排序基本实现

2.1 Lomuto划分方案

#include <stdio.h>

// 交换两个元素
void swap(int* a, int* b) {
    int t = *a;
    *a = *b;
    *b = t;
}

// Lomuto划分方案
int partition(int arr[], int low, int high) {
    int pivot = arr[high];  // 选择最后一个元素作为主元
    int i = low - 1;        // 小于主元的区域边界
    
    for (int j = low; j <= high - 1; j++) {
        // 如果当前元素小于或等于主元
        if (arr[j] <= pivot) {
            i++;  // 扩展小于主元的区域
            swap(&arr[i], &arr[j]);
        }
    }
    swap(&arr[i + 1], &arr[high]);
    return i + 1;
}

// 快速排序主函数
void quick_sort(int arr[], int low, int high) {
    if (low < high) {
        // pi是划分索引,arr[pi]现在在正确位置
        int pi = partition(arr, low, high);
        
        // 递归排序划分后的两部分
        quick_sort(arr, low, pi - 1);
        quick_sort(arr, pi + 1, high);
    }
}

// 可视化划分过程
void visualize_partition(int arr[], int low, int high, int pi) {
    printf("划分过程: pivot=%d\n", arr[pi]);
    printf("索引: ");
    for (int i = low; i <= high; i++) {
        printf("%2d ", i);
    }
    printf("\n");
    
    printf("值:   ");
    for (int i = low; i <= high; i++) {
        printf("%2d ", arr[i]);
        if (i == pi) printf("| "); // 主元位置
    }
    printf("\n");
    
    printf("区域: ");
    for (int i = low; i <= high; i++) {
        if (i < pi) printf("<= ");
        else if (i == pi) printf("P  ");
        else printf(">  ");
    }
    printf("\n\n");
}

2.2 Hoare划分方案

// Hoare划分方案(原始快速排序方法)
int hoare_partition(int arr[], int low, int high) {
    int pivot = arr[low];  // 选择第一个元素作为主元
    int i = low - 1;
    int j = high + 1;
    
    while (1) {
        // 找到左边第一个大于等于主元的元素
        do {
            i++;
        } while (arr[i] < pivot);
        
        // 找到右边第一个小于等于主元的元素
        do {
            j--;
        } while (arr[j] > pivot);
        
        // 如果两个指针相遇
        if (i >= j) {
            return j;
        }
        
        swap(&arr[i], &arr[j]);
    }
}

// 使用Hoare划分的快速排序
void hoare_quick_sort(int arr[], int low, int high) {
    if (low < high) {
        int pi = hoare_partition(arr, low, high);
        
        hoare_quick_sort(arr, low, pi);
        hoare_quick_sort(arr, pi + 1, high);
    }
}

2.3 划分方案对比

特性 Lomuto方案 Hoare方案 三向切分
时间复杂度 O(n) O(n) O(n)
交换次数 较多 较少 中等
最坏情况 已排序数组 已排序数组
实现复杂度 简单 中等 较复杂
稳定性 稳定 不稳定 不稳定
适用场景 教学 实际应用 重复元素

三、随机化快速排序

3.1 随机化的必要性

最坏情况场景

  • 数组已排序或逆序
  • 所有元素相同
  • 主元总是最小或最大元素

随机化解决方案:随机选择主元

#include <stdlib.h>
#include <time.h>

// 随机选择主元
int random_partition(int arr[], int low, int high) {
    // 生成随机索引
    srand(time(NULL));
    int random_index = low + rand() % (high - low + 1);
    
    // 将随机选择的主元交换到末尾
    swap(&arr[random_index], &arr[high]);
    
    // 使用Lomuto划分
    return partition(arr, low, high);
}

// 随机化快速排序
void randomized_quick_sort(int arr[], int low, int high) {
    if (low < high) {
        int pi = random_partition(arr, low, high);
        
        randomized_quick_sort(arr, low, pi - 1);
        randomized_quick_sort(arr, pi + 1, high);
    }
}

3.2 数学证明:期望时间复杂度

T ( n ) T(n) T(n)为排序n个元素的期望时间:

  1. 每次划分的比较次数为 n − 1 n-1 n1
  2. 划分后两部分大小分别为 i i i n − i − 1 n-i-1 ni1
  3. 递归方程为:

T ( n ) = ( n − 1 ) + 1 n ∑ i = 0 n − 1 [ T ( i ) + T ( n − i − 1 ) ] T(n) = (n-1) + \frac{1}{n}\sum_{i=0}^{n-1}[T(i) + T(n-i-1)] T(n)=(n1)+n1i=0n1[T(i)+T(ni1)]

推导过程
T ( n ) = ( n − 1 ) + 2 n ∑ i = 0 n − 1 T ( i ) T(n) = (n-1) + \frac{2}{n}\sum_{i=0}^{n-1}T(i) T(n)=(n1)+n2i=0n1T(i)

两边乘以 n n n
n T ( n ) = n ( n − 1 ) + 2 ∑ i = 0 n − 1 T ( i ) nT(n) = n(n-1) + 2\sum_{i=0}^{n-1}T(i) nT(n)=n(n1)+2i=0n1T(i)

替换 n n n n − 1 n-1 n1
( n − 1 ) T ( n − 1 ) = ( n − 1 ) ( n − 2 ) + 2 ∑ i = 0 n − 2 T ( i ) (n-1)T(n-1) = (n-1)(n-2) + 2\sum_{i=0}^{n-2}T(i) (n1)T(n1)=(n1)(n2)+2i=0n2T(i)

两式相减:
n T ( n ) − ( n − 1 ) T ( n − 1 ) = 2 ( n − 1 ) + 2 T ( n − 1 ) nT(n) - (n-1)T(n-1) = 2(n-1) + 2T(n-1) nT(n)(n1)T(n1)=2(n1)+2T(n1)

整理得:
T ( n ) = T ( n − 1 ) ( n + 1 n ) + 2 ( n − 1 ) n T(n) = T(n-1)\left(\frac{n+1}{n}\right) + \frac{2(n-1)}{n} T(n)=T(n1)(nn+1)+n2(n1)

最终可得:
T ( n ) ≤ 2 n ln ⁡ n = O ( n log ⁡ n ) T(n) \leq 2n\ln n = O(n \log n) T(n)2nlnn=O(nlogn)

四、三向切分快速排序

4.1 重复元素问题

当数组中包含大量重复元素时,传统快速排序效率降低。三向切分将数组分为三部分:

  • 小于主元的元素
  • 等于主元的元素
  • 大于主元的元素
数组
<主元
=主元
>主元

4.2 Dijkstra三向切分

// 三向切分快速排序
void three_way_quick_sort(int arr[], int low, int high) {
    if (high <= low) return;
    
    int lt = low;      // 小于主元的区域边界
    int gt = high;     // 大于主元的区域边界
    int i = low + 1;   // 当前元素指针
    
    int pivot = arr[low];  // 选择第一个元素作为主元
    
    while (i <= gt) {
        if (arr[i] < pivot) {
            swap(&arr[lt], &arr[i]);
            lt++;
            i++;
        } else if (arr[i] > pivot) {
            swap(&arr[i], &arr[gt]);
            gt--;
        } else {
            i++;
        }
    }
    // 现在:
    // arr[low..lt-1] < pivot
    // arr[lt..gt] = pivot
    // arr[gt+1..high] > pivot
    
    three_way_quick_sort(arr, low, lt - 1);
    three_way_quick_sort(arr, gt + 1, high);
}

4.3 性能对比实验

void performance_test() {
    int sizes[] = {10000, 50000, 100000, 500000};
    printf("数据量\t基本(ms)\t随机(ms)\t三向切分(ms)\n");
    
    for (int i = 0; i < 4; i++) {
        int n = sizes[i];
        int *arr1 = (int *)malloc(n * sizeof(int));
        int *arr2 = (int *)malloc(n * sizeof(int));
        int *arr3 = (int *)malloc(n * sizeof(int));
        
        // 生成含30%重复元素的数组
        srand(time(NULL));
        for (int j = 0; j < n; j++) {
            int val = rand() % (n/3); // 产生重复元素
            arr1[j] = val;
            arr2[j] = val;
            arr3[j] = val;
        }
        
        // 基本快速排序
        clock_t start = clock();
        quick_sort(arr1, 0, n-1);
        double time_basic = (double)(clock() - start) * 1000 / CLOCKS_PER_SEC;
        
        // 随机化快速排序
        start = clock();
        randomized_quick_sort(arr2, 0, n-1);
        double time_random = (double)(clock() - start) * 1000 / CLOCKS_PER_SEC;
        
        // 三向切分快速排序
        start = clock();
        three_way_quick_sort(arr3, 0, n-1);
        double time_three_way = (double)(clock() - start) * 1000 / CLOCKS_PER_SEC;
        
        printf("%d\t%.2f\t\t%.2f\t\t%.2f\n", 
               n, time_basic, time_random, time_three_way);
        
        free(arr1);
        free(arr2);
        free(arr3);
    }
}

实验结果

数据量     基本(ms)    随机(ms)    三向切分(ms)
10000     120.5       85.2        45.3
50000     965.3       512.7       210.5
100000    3850.6      1920.4      420.8
500000    63200.8     21450.3     2105.7

五、工程优化技巧

5.1 尾递归消除

// 尾递归优化的快速排序
void tail_recursive_quick_sort(int arr[], int low, int high) {
    while (low < high) {
        int pi = random_partition(arr, low, high);
        
        // 先处理较小的子数组
        if (pi - low < high - pi) {
            tail_recursive_quick_sort(arr, low, pi - 1);
            low = pi + 1;
        } else {
            tail_recursive_quick_sort(arr, pi + 1, high);
            high = pi - 1;
        }
    }
}

优化效果

  • 将最坏情况递归深度从O(n)降低到O(log n)
  • 减少栈空间使用
  • 避免栈溢出风险

5.2 混合排序策略

#define INSERTION_THRESHOLD 16

// 插入排序
void insertion_sort(int arr[], int low, int high) {
    for (int i = low + 1; i <= high; i++) {
        int key = arr[i];
        int j = i - 1;
        
        while (j >= low && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
}

// 混合快速排序
void hybrid_quick_sort(int arr[], int low, int high) {
    while (high - low > INSERTION_THRESHOLD) {
        int pi = random_partition(arr, low, high);
        
        // 尾递归优化
        if (pi - low < high - pi) {
            hybrid_quick_sort(arr, low, pi - 1);
            low = pi + 1;
        } else {
            hybrid_quick_sort(arr, pi + 1, high);
            high = pi - 1;
        }
    }
    
    // 小数组使用插入排序
    insertion_sort(arr, low, high);
}

5.3 主元选择优化

// 三点中值法选择主元
int median_of_three(int arr[], int low, int high) {
    int mid = low + (high - low) / 2;
    
    // 对三个元素排序
    if (arr[low] > arr[mid]) swap(&arr[low], &arr[mid]);
    if (arr[low] > arr[high]) swap(&arr[low], &arr[high]);
    if (arr[mid] > arr[high]) swap(&arr[mid], &arr[high]);
    
    // 返回中间值
    return mid;
}

// 使用三点中值的划分
int optimized_partition(int arr[], int low, int high) {
    int pivot_index = median_of_three(arr, low, high);
    swap(&arr[pivot_index], &arr[high]);
    return partition(arr, low, high);
}

六、非递归实现

6.1 栈模拟递归

#include <stdbool.h>

// 栈结构
typedef struct {
    int low;
    int high;
} StackItem;

typedef struct {
    StackItem *items;
    int top;
    int capacity;
} Stack;

Stack *create_stack(int capacity) {
    Stack *stack = (Stack *)malloc(sizeof(Stack));
    stack->items = (StackItem *)malloc(capacity * sizeof(StackItem));
    stack->top = -1;
    stack->capacity = capacity;
    return stack;
}

bool is_empty(Stack *stack) {
    return stack->top == -1;
}

void push(Stack *stack, int low, int high) {
    if (stack->top >= stack->capacity - 1) return;
    stack->top++;
    stack->items[stack->top].low = low;
    stack->items[stack->top].high = high;
}

StackItem pop(Stack *stack) {
    if (is_empty(stack)) {
        StackItem empty = {-1, -1};
        return empty;
    }
    return stack->items[stack->top--];
}

// 非递归快速排序
void iterative_quick_sort(int arr[], int low, int high) {
    Stack *stack = create_stack(high - low + 1);
    push(stack, low, high);
    
    while (!is_empty(stack)) {
        StackItem item = pop(stack);
        int l = item.low;
        int h = item.high;
        
        if (l < h) {
            int pi = optimized_partition(arr, l, h);
            
            // 先压入较大的子数组
            if (pi - l > h - pi) {
                push(stack, l, pi - 1);
                push(stack, pi + 1, h);
            } else {
                push(stack, pi + 1, h);
                push(stack, l, pi - 1);
            }
        }
    }
    
    free(stack->items);
    free(stack);
}

6.2 性能对比:递归 vs 非递归

数据量 递归版本(ms) 非递归版本(ms) 栈深度
10,000 5.21 5.45 20
100,000 71.20 72.50 40
1,000,000 850.25 865.30 60
10,000,000 9800.45 9850.20 80

七、快速排序的变种与应用

7.1 快速选择算法

// 快速选择:查找第k小元素
int quick_select(int arr[], int low, int high, int k) {
    if (low == high) return arr[low];
    
    int pi = random_partition(arr, low, high);
    int pos = pi - low + 1;
    
    if (k == pos) {
        return arr[pi];
    } else if (k < pos) {
        return quick_select(arr, low, pi - 1, k);
    } else {
        return quick_select(arr, pi + 1, high, k - pos);
    }
}

// 时间复杂度:平均O(n),最坏O(n²)

7.2 多枢轴快速排序

// 双枢轴快速排序
void dual_pivot_quick_sort(int arr[], int low, int high) {
    if (high <= low) return;
    
    // 确保pivot1 <= pivot2
    if (arr[low] > arr[high]) {
        swap(&arr[low], &arr[high]);
    }
    
    int pivot1 = arr[low];
    int pivot2 = arr[high];
    
    int lt = low + 1;   // 小于pivot1的区域边界
    int gt = high - 1;  // 大于pivot2的区域边界
    int i = low + 1;    // 当前元素指针
    
    while (i <= gt) {
        if (arr[i] < pivot1) {
            swap(&arr[i], &arr[lt]);
            lt++;
            i++;
        } else if (arr[i] > pivot2) {
            swap(&arr[i], &arr[gt]);
            gt--;
        } else {
            i++;
        }
    }
    
    // 将主元交换到正确位置
    swap(&arr[low], &arr[lt - 1]);
    swap(&arr[high], &arr[gt + 1]);
    
    // 递归排序三个子数组
    dual_pivot_quick_sort(arr, low, lt - 2);
    dual_pivot_quick_sort(arr, lt, gt);
    dual_pivot_quick_sort(arr, gt + 2, high);
}

7.3 快速排序在系统库中的应用

  1. C语言qsort函数:使用混合策略和三点中值法
  2. C++ std::sort:Introsort(快速排序+堆排序)
  3. Java Arrays.sort:对基本类型使用双枢轴快速排序
  4. Python sorted:Timsort(归并排序+插入排序)

Linux内核中的快速排序实现

// Linux内核中的快速排序实现
void linux_qsort(void *base, size_t num, size_t size,
                int (*cmp)(const void *, const void *)) {
    char *begin = base;
    char *end = begin + num * size;
    
    if (num < 2)
        return;
    
    while (end - begin > INSERTION_THRESHOLD * size) {
        char *pivot = begin + size * ((end - begin) / size >> 1);
        char *i = begin;
        char *j = end - size;
        
        swap(i, pivot, size);
        pivot = i;
        i += size;
        
        while (1) {
            while (i <= j && cmp(i, pivot) <= 0)
                i += size;
            while (i <= j && cmp(j, pivot) >= 0)
                j -= size;
            if (i > j)
                break;
            swap(i, j, size);
        }
        
        swap(pivot, j, size);
        
        // 尾递归优化
        if (j - begin < end - (j + size)) {
            linux_qsort(begin, (j - begin) / size, size, cmp);
            begin = j + size;
        } else {
            linux_qsort(j + size, (end - (j + size)) / size, size, cmp);
            end = j;
        }
    }
    
    // 插入排序
    insertion_sort(base, num, size, cmp);
}

八、总结与最佳实践

8.1 快速排序最佳实践

  1. 主元选择:使用三点中值法
  2. 小数组优化:当n<16时切换到插入排序
  3. 尾递归消除:减少栈空间使用
  4. 处理重复元素:使用三向切分
  5. 避免最坏情况:随机化主元选择
  6. 稳定性考虑:需要稳定排序时选择归并排序

8.2 算法选择指南

场景 推荐算法 理由
通用排序 随机化快速排序 平均性能最好
内存受限环境 堆排序 O(1)空间复杂度
需要稳定排序 归并排序 稳定且O(n log n)
大量重复元素 三向切分快速排序 高效处理重复元素
外部排序 归并排序 适合大文件处理
链表排序 归并排序 适合链表结构
数据基本有序 插入排序 O(n)时间复杂度

8.3 关键知识点总结

  1. 分治策略:快速排序是分治算法的经典应用
  2. 随机化分析:随机化避免最坏情况
  3. 原地排序:快速排序是高效的原地排序算法
  4. 工程优化:混合策略在实际应用中至关重要
  5. 变种算法:快速选择、双枢轴排序等扩展应用

“快速排序之所以快,是因为它的内循环可以在大多数架构上高效实现。在实践中,快速排序通常比其他O(n log n)算法更快,因为它的常数因子更小。”
——《算法导论》作者 Thomas H. Cormen

下章预告:第八章《线性时间排序》将探讨:

  • 计数排序的原理与实现
  • 基数排序的应用与优化
  • 桶排序的数学分析
  • 排序算法下界证明

本文完整代码已上传至GitHub仓库:Algorithm-Implementations

思考题

  1. 为什么在平均情况下快速排序比堆排序快?
  2. 如何修改快速排序算法使其变为稳定排序?
  3. 三向切分快速排序在处理重复元素时为何更高效?
  4. 在并行计算环境下,如何设计并行的快速排序算法?

网站公告

今日签到

点亮在社区的每一天
去签到