快速排序(Quick Sort)详解 —— 从原理到 C 语言实现(附动图与示例)

196 阅读4分钟

一、快速排序是什么?为什么这么重要?

快速排序(Quick Sort)是目前综合性能最强的通用排序算法之一,由英国计算机科学家 Tony Hoare 在 1960 年发明。 它的平均时间复杂度为 O(n log n),而且原地排序(几乎不需要额外空间),在实际工程中被广泛使用(C++ STL 的 std::sort、Java 的 Arrays.sort 等底层大多都用快速排序 + 插入排序的混合实现)。

快排

一句话总结快速排序的核心思想:

「选一个数当基准(pivot),把比它小的数全部放到左边,比它大的全部放到右边,然后对左右两边递归地继续做同样的事情。」

二、快速排序最形象的比喻

想象你要给一堆身高不同的小朋友排队(从矮到高):

  1. 随便挑一个小孩当“老大”(pivot)
  2. 所有其他小孩跟老大比: 比老大矮的 → 全部站到老大左边 比老大高的 → 全部站到老大右边
  3. 老大就站在自己应该在的位置(已经排好了!)
  4. 然后对左边那群人、右边那群人,分别再挑一个新老大,继续重复上面的过程

不断把大区间拆成小区间,直到每个小组只有 0 或 1 个人,就全部有序了。

下面为快速排序过程,注意不同缩进代表进展不同

假设有一个待排序的列表 [3, 6, 8, 10, 1, 2, 1],选择最后一个元素作为基准(pivot),排序过程如下:

  1. 初始状态:

    1.列表:[3, 6, 8, 10, 1, 2, 1]。

    2.基准元素:1(最后一个元素)。

  2. 第一轮分区:

    1.将小于基准的元素放在左侧,大于基准的元素放在右侧。

    2.分区后列表:[1, 1, 2, 10, 6, 8, 3]。

    3.基准元素 1 的位置确定。

  3. 递归排序:

    1.对左侧子列表 [1] 和右侧子列表 [2, 10, 6, 8, 3] 分别进行快速排序。

    2.左侧子列表已经有序。

    3.对右侧子列表 [2, 10, 6, 8, 3] 选择基准元素 3(最后一个元素):

       1.分区后列表:[2, 3, 6, 8, 10]2.基准元素 3 的位置确定。
    

    4.继续递归排序右侧子列表 [6, 8, 10]:

    1.选择基准元素 10(最后一个元素):
    
       1.分区后列表:[6, 8, 10]2.基准元素 10 的位置确定。
    
    2.继续递归排序左侧子列表 [6, 8]1.选择基准元素 8(最后一个元素):
    
            1.分区后列表:[6, 8]2.基准元素 8 的位置确定。
    
       2.继续递归排序左侧子列表 [6],已经有序。
        
    
  4. 最终结果:

    列表完全有序:[1, 1, 2, 3, 6, 8, 10]。

三、快速排序最常用的分区方法 —— Lomuto 分区

目前教学和面试中最常用的写法是 Lomuto 分区(单指针方案):

// 返回 pivot 最终落下的位置
int partition(int arr[], int left, int right) {
    int pivot = arr[right];     // 简单起见,选最右边元素做基准
    int i = left - 1;           // 小于等于区的右边界(初始在外面)

    for (int j = left; j < right; j++) {
        if (arr[j] <= pivot) {  // 注意这里是 <= ,这样等于的元素也会交换到左边
            i++;
            // 交换,把小的元素放到 i 的位置
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }
    // 最后把 pivot 放到正确位置
    int temp = arr[i + 1];
    arr[i + 1] = arr[right];
    arr[right] = temp;

    return i + 1;  // 返回 pivot 最终的位置
}

四、完整快速排序代码(C语言)

#include <stdio.h>

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

// Lomuto 分区方案
int partition(int arr[], int left, int right) {
    int pivot = arr[right];
    int i = left - 1;

    for (int j = left; j < right; j++) {
        if (arr[j] <= pivot) {
            i++;
            swap(&arr[i], &arr[j]);
        }
    }
    swap(&arr[i + 1], &arr[right]);
    return i + 1;
}

// 快速排序递归函数
void quickSort(int arr[], int left, int right) {
    if (left < right) {
        int pivotIndex = partition(arr, left, right);
        
        // 分别对左右两边递归
        quickSort(arr, left, pivotIndex - 1);
        quickSort(arr, pivotIndex + 1, right);
    }
}

// 对外接口
void quick_sort(int arr[], int n) {
    if (n <= 1) return;
    quickSort(arr, 0, n - 1);
}

// 测试
int main() {
    int arr[] = {64, 34, 25, 12, 22, 11, 90, 5, 87, 1, 73, 31, 45};
    int n = sizeof(arr) / sizeof(arr[0]);

    printf("排序前:");
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");

    quick_sort(arr, n);

    printf("排序后:");
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");

    return 0;
}

六、真实工程中的常见优化

  1. 随机化基准(防止最坏情况) 随机选择一个元素与末尾交换,再用末尾做 pivot
  2. 小数组切换插入排序 当区间长度 ≤ 8~16 时,改用插入排序(常数更小)
  3. 三路快速排序(处理大量相同元素) 把等于 pivot 的元素单独分一组
  4. 三数取中(median-of-three) 取 left、mid、right 中间大小的值做 pivot

七、结语

快速排序之所以强大,是因为它平均情况极快 + 原地 + 实现相对简单。 虽然最坏情况下会退化,但通过随机化、三数取中等简单优化,就能让它在绝大多数实际场景中成为最快的排序算法。 希望这篇文章 + 推荐的图片能帮助你更好地理解和讲解快速排序!