排序算法完全指南(五):快速排序深度详解

引言

前面我们学习了冒泡、选择、插入、基数四种排序。今天要讲的快速排序 ,是二十世纪十大算法之一,也是实际工程中最常用的排序算法。C 语言的 qsort、C++ 的 std::sort、Java 的 Arrays.sort,底层都是快速排序或其变体。

快速排序的核心是分治思想:选一个基准值,把数组分成"比基准小"和"比基准大"两半,然后递归处理两半。这种"大事化小"的策略,让它的平均时间复杂度达到了 O(n log n),而且常数因子比归并排序和堆排序都小。

第一部分:算法思想

一、核心原理

快速排序的三步:

  1. 选基准:从数组中选一个元素作为基准值(pivot)

  2. 划分:把小于基准的放左边,大于基准的放右边

  3. 递归:对左右两部分分别重复这个过程

二、挖坑法划分过程

挖坑法口诀:左边挖坑右边填,右边挖坑左边填,左右相遇放基准。


第二部分:代码实现

一、挖坑法划分函数

cpp 复制代码
int Partition(int arr[], int left, int right) {
    int tmp = arr[left];  // 基准值,左边留下第一个坑

    while (left != right) {
        // 从右向左找比基准小的,填左坑
        while (left < right && arr[right] > tmp) {
            right--;
        }
        if (left == right) break;
        arr[left] = arr[right];   // 小的放左边,右边留下坑
        
        // 从左向右找比基准大的,填右坑
        while (left < right && arr[left] < tmp) {
            left++;
        }
        if (left == right) break;
        arr[right] = arr[left];   // 大的放右边,左边留下坑
    }
    
    arr[left] = tmp;  // 基准值归位
    return left;       // 返回基准位置
}

二、递归版本

cpp 复制代码
void Quick_Recursion(int arr[], int left, int right) {
    // 区间不合法或只有一个元素 → 终止
    if (left >= right) return;

    // 划分,得到基准位置
    int par = Partition(arr, left, right);

    // 递归处理左右两部分
    Quick_Recursion(arr, left, par - 1);
    Quick_Recursion(arr, par + 1, right);
}

void Quick_Sort(int arr[], int len) {
    Quick_Recursion(arr, 0, len - 1);
}

三、非递归版本(用栈模拟递归)

cpp 复制代码
#include <stack>

void Quick_Sort_No_Recursion(int arr[], int len) {
    std::stack<int> s;
    s.push(0);           // 先压左边界
    s.push(len - 1);     // 再压右边界

    while (!s.empty()) {
        int right = s.top(); s.pop();  // 先取右边界
        int left  = s.top(); s.pop();  // 再取左边界

        int par = Partition(arr, left, right);

        // 左半部分还有多个元素 → 压栈
        if (left < par - 1) {
            s.push(left);
            s.push(par - 1);
        }

        // 右半部分还有多个元素 → 压栈
        if (right > par + 1) {
            s.push(par + 1);
            s.push(right);
        }
    }
}

递归 vs 非递归

版本 优点 缺点
递归 代码简洁 深度过大可能栈溢出
非递归 栈在堆上,不易溢出 代码稍长

第三部分:完整测试代码

cpp 复制代码
#include <stdio.h>
#include <stack>

int Partition(int arr[], int left, int right) {
    int tmp = arr[left];
    while (left != right) {
        while (left < right && arr[right] > tmp) right--;
        if (left == right) break;
        arr[left] = arr[right];
        
        while (left < right && arr[left] < tmp) left++;
        if (left == right) break;
        arr[right] = arr[left];
    }
    arr[left] = tmp;
    return left;
}

void Quick_Recursion(int arr[], int left, int right) {
    if (left >= right) return;
    int par = Partition(arr, left, right);
    Quick_Recursion(arr, left, par - 1);
    Quick_Recursion(arr, par + 1, right);
}

void Quick_Sort(int arr[], int len) {
    Quick_Recursion(arr, 0, len - 1);
}

void printArray(int arr[], int len, const char* msg) {
    printf("%s", msg);
    for (int i = 0; i < len; i++) printf("%d ", arr[i]);
    printf("\n");
}

int main() {
    int arr1[] = {5, 3, 8, 1, 2, 7, 6, 4};
    int len1 = sizeof(arr1) / sizeof(arr1[0]);
    printArray(arr1, len1, "排序前:");
    Quick_Sort(arr1, len1);
    printArray(arr1, len1, "排序后:");

    int arr2[] = {1, 2, 3, 4, 5, 6, 7, 8};
    int len2 = sizeof(arr2) / sizeof(arr2[0]);
    printf("\n已有序测试:\n");
    printArray(arr2, len2, "排序前:");
    Quick_Sort(arr2, len2);
    printArray(arr2, len2, "排序后:");

    int arr3[] = {8, 7, 6, 5, 4, 3, 2, 1};
    int len3 = sizeof(arr3) / sizeof(arr3[0]);
    printf("\n逆序测试:\n");
    printArray(arr3, len3, "排序前:");
    Quick_Sort(arr3, len3);
    printArray(arr3, len3, "排序后:");

    return 0;
}

第四部分:算法分析

一、时间复杂度

情况 时间复杂度 条件
最好 O(n log n) 每次划分均匀,基准恰好是中位数
最坏 O(n²) 每次基准是最大/最小值(如已有序数组取左端为基准)
平均 O(n log n) 随机数据

最坏情况演示

二、空间复杂度

版本 空间 说明
递归 O(log n) 递归栈深度
非递归 O(log n) 显式栈

三、稳定性

快速排序是不稳定的。因为划分时的跳跃式填坑会破坏相等元素的相对顺序。


第五部分:优化策略

一、三数取中法(避免最坏情况)

cpp 复制代码
// 取 left、mid、right 三者的中间值作为基准
int GetMidIndex(int arr[], int left, int right) {
    int mid = left + (right - left) / 2;
    
    if (arr[left] < arr[mid]) {
        if (arr[mid] < arr[right]) return mid;
        else if (arr[left] < arr[right]) return right;
        else return left;
    } else {
        if (arr[left] < arr[right]) return left;
        else if (arr[mid] < arr[right]) return right;
        else return mid;
    }
}

int Partition_Optimized(int arr[], int left, int right) {
    // 三数取中,交换到 left 位置
    int mid = GetMidIndex(arr, left, right);
    int tmp = arr[left];
    arr[left] = arr[mid];
    arr[mid] = tmp;
    
    // 后面和普通 Partition 一样
    tmp = arr[left];
    while (left != right) {
        while (left < right && arr[right] > tmp) right--;
        if (left == right) break;
        arr[left] = arr[right];
        while (left < right && arr[left] < tmp) left++;
        if (left == right) break;
        arr[right] = arr[left];
    }
    arr[left] = tmp;
    return left;
}

二、小数据量切换插入排序

当区间小于一定阈值(如 16)时,插入排序比快速排序更快:

cpp 复制代码
void Quick_Recursion_Optimized(int arr[], int left, int right) {
    if (left >= right) return;
    
    // 小数据量直接用插入排序
    if (right - left + 1 <= 16) {
        // 插入排序...
        return;
    }
    
    int par = Partition_Optimized(arr, left, right);
    Quick_Recursion_Optimized(arr, left, par - 1);
    Quick_Recursion_Optimized(arr, par + 1, right);
}

第六部分:与各排序算法对比

排序算法 平均 最坏 空间 稳定
冒泡排序 O(n²) O(n²) O(1)
选择排序 O(n²) O(n²) O(1)
插入排序 O(n²) O(n²) O(1)
基数排序 O(d×n) O(d×n) O(n+r)
快速排序 O(n log n) O(n²) O(log n)
希尔排序 O(n^1.3) O(n²) O(1)
归并排序 O(n log n) O(n log n) O(n)
堆排序 O(n log n) O(n log n) O(1)

总结

一、核心要点

要点 内容
算法思想 分治:选基准 → 划分 → 递归
时间复杂度 最好/平均 O(n log n),最坏 O(n²)
空间复杂度 O(log n)(递归栈)
稳定性 ❌ 不稳定
核心优化 三数取中避最坏,小数据切插入

二、代码框架记忆

三、一句话记忆

快速排序选基准,小的放左大的放右,递归处理两边。平均 O(n log n) 且常数因子最小,是最快的通用排序算法。但最坏会退化到 O(n²),需要用三数取中优化。

相关推荐
万事大吉CC16 小时前
Python 笔试输入模板总结
python·算法
lihao lihao16 小时前
Linux信号
开发语言·c++·算法
大白话_NOI16 小时前
【洛谷 P2249】查找(深基 13. 例 1)+ 详细分析
c++·算法
吠品16 小时前
C++实现m行n列带边框的长方形输出
算法
智者知已应修善业16 小时前
【51单片机2个外部中断显示中断历时,初始化8左移3位共阳数码管】2024-6-6
c++·经验分享·笔记·算法·51单片机
西安邮电大学17 小时前
分治算法详细讲解
java·后端·其他·算法·面试
code bean17 小时前
平衡相关性与多样性:推荐系统中的永恒博弈与 MMR 算法详解
算法
青梅橘子皮17 小时前
Linux---进程控制(2)(进程程序替换)
linux·c++·算法
Shan120517 小时前
经典问题——验证栈序列
数据结构·算法
2501_9065651217 小时前
勾股定理证明
算法