跳到主要内容
极客日志极客日志
首页博客AI提示词GitHub精选代理工具
搜索
|注册
博客列表
C算法

快速排序核心思想与多种实现方式详解

综述由AI生成快速排序基于分治策略,通过选基准、分区、递归子区间实现高效排序。文章详细讲解了 Hoare 双指针、挖坑法及 Lomuto 前后指针三种经典分区实现,并对比了它们在有序数组下的表现。针对性能退化问题,介绍了随机选基准、三数取中、小区间插入排序优化及三路划分处理重复数据等进阶技巧。此外还涵盖了非递归迭代版本的栈模拟实现,帮助读者全面掌握快排原理与工程落地细节。

菩提发布于 2026/3/23更新于 2026/5/1015 浏览
快速排序核心思想与多种实现方式详解

一、快速排序思想

快速排序的核心在于分治策略。简单来说,就是'分而治之',通过三步实现高效排序:

  1. 选基准:从待排序数组中选择一个元素作为「基准值」(pivot)。
  2. 分区操作:遍历数组,将小于基准值的元素放到左侧,大于基准值的放到右侧。这一步结束后,基准值会被放到最终排序的正确位置。
  3. 递归排序子区间:对基准值左右两侧的子数组,重复执行上述步骤,直到子数组长度为 0 或 1。

这种思路的本质是:通过一次遍历将大问题拆分为两个规模更小的子问题,且子问题独立解决后无需额外合并,这正是分治思想的典型体现。

二、Hoare 版本

1. Hoare 版本介绍

Hoare 版本是经典的原地分区实现,由算法发明者 Tony Hoare 提出,核心是双指针相向遍历。

核心步骤(升序):选取区间首元素为基准值 key,右指针从右向左找首个小于 key 的元素,左指针从左向右找首个大于 key 的元素,交换两者并重复,直到两指针相遇。最后将基准值与相遇位置交换。

文章配图

特点:原地分区、无额外空间开销。但需注意遵循「右指针先行」规则,否则会导致基准归位错误。

文章配图

2. 代码实现

void QuickSort1(int* a, int left, int right) {
    // 递归终止条件:区间元素个数≤1 时,无需排序直接返回
    if (left >= right) {
        return;
    }
    
    // 初始化指针:begin/end 遍历区间,keyi 标记基准值初始位置(选左端点为基准)
    int begin = left;
    int end = right;
    int keyi = left;
    
    // 双指针相向遍历,完成区间分区
    while (begin < end) {
        // 右指针从后往前找:第一个比基准值小的元素
        while (begin < end && a[end] >= a[keyi]) {
            end--;
        }
        // 左指针从前往后找:第一个比基准值大的元素
        while (begin < end && a[begin] <= a[keyi]) {
            begin++;
        }
        
        swap(&a[begin], &a[end]);
    }
    
    
    swap(&a[keyi], &a[begin]);
    keyi = begin;
    
    
    QuickSort1(a, left, keyi - );
    QuickSort1(a, keyi + , right);
}
// 交换找到的两个元素,让小值到左、大值到右
// 指针相遇时,该位置就是基准值的最终位置,交换归位
// 递归处理基准值左右两侧的子区间
1
1

这里有个细节要注意:为什么 left 和 right 指定的数据和 key 值相等时不能交换?这涉及到指针移动的逻辑边界,实际运行中需确保循环条件严谨。

3. 复杂度分析

在最优情况下,每次划分都能将数组均匀分成两部分,递归树有 logN 层,每层处理约 N 个元素,总时间复杂度为 O(NlogN)。

有些同学可能会疑惑,为啥每层遍历的元素都是 N?实际上每层遍历的元素个数是 N 减去该层基准值的数量,但基准值数量相对于 N 是低阶项,在大 O 表示法中可以忽略不计。

那如果数组是有序的情况还能达到均分吗?显然不能,此时会退化为 O(n²)。但在实际开发中,我们会对快排进行优化,让其能近似达到一个平衡二叉树的状态。

4. 有序情况优化

4.1 随机选 keyi

即使输入是有序数组,随机选基准也能大概率避免'每次选到最左/最右元素',让递归树尽可能平衡。

void QuickSort1(int* a, int left, int right) {
    if (left >= right) {
        return;
    }
    int begin = left;
    int end = right;
    int keyi = left;
    
    // 优化 1:随机选 keyi
    int randi = left + rand() % (right - left + 1);
    swap(&a[randi], &a[keyi]);
    
    while (begin < end) {
        while (begin < end && a[end] >= a[keyi]) {
            end--;
        }
        while (begin < end && a[begin] <= a[keyi]) {
            begin++;
        }
        swap(&a[begin], &a[end]);
    }
    
    swap(&a[keyi], &a[begin]);
    keyi = begin;
    QuickSort1(a, left, keyi - 1);
    QuickSort1(a, keyi + 1, right);
}
4.2 三数取中
int GetMidNumi(int* a, int left, int right) {
    int midi = (right + left) / 2;
    if (a[left] < a[midi]) {
        if (a[right] < a[left]) {
            return left;
        } else if (a[right] > a[midi]) {
            return midi;
        } else {
            return right;
        }
    } else {
        if (a[right] > a[left]) {
            return left;
        } else if (a[midi] > a[right]) {
            return midi;
        } else {
            return right;
        }
    }
}

void QuickSort1(int* a, int left, int right) {
    if (left >= right) {
        return;
    }
    int begin = left;
    int end = right;
    int keyi = left;
    
    // 优化 2:三数取中
    int midi = GetMidNumi(a, left, right);
    swap(&a[midi], &a[keyi]);
    
    while (begin < end) {
        while (begin < end && a[end] >= a[keyi]) {
            end--;
        }
        while (begin < end && a[begin] <= a[keyi]) {
            begin++;
        }
        swap(&a[begin], &a[end]);
    }
    
    swap(&a[keyi], &a[begin]);
    keyi = begin;
    QuickSort1(a, left, keyi - 1);
    QuickSort1(a, keyi + 1, right);
}

这段代码实现了三数取中优化,核心是通过 GetMidNumi 函数从左、中、右三个位置选出数值居中的元素下标,将其交换到左端点作为基准值,以此避免在有序数组中选到极值导致的性能退化。

注意:随机选基准和三数取中可避免快排在有序数组下的性能退化,二选一即可;但面对大量重复数据时效率仍会下降,后续将介绍三路划分来解决这一问题。

5. 稳定性分析

由于快排在分区过程中会进行跨位置的交换操作,这会打乱相等元素的相对位置,因此快速排序是不稳定的排序算法。

三、挖坑法

1. 挖坑法介绍

挖坑法快排(以升序为例)的核心思路是:先选一个基准值并把它的位置设为第一个'坑',然后用右指针从后向前找比基准值小的元素填入左坑,左指针再从前向后找比基准值大的元素填入右坑,不断形成新坑,直到双指针相遇,最后把基准值填入最终的坑位完成分区。

如果是降序排序,只需调整指针查找条件:右指针找比基准值大的元素,左指针找比基准值小的元素即可。

文章配图

2. 代码实现

void QuickSort2(int* a, int left, int right) {
    if (left >= right) {
        return;
    }
    
    // 基准值
    int key = a[left];
    // 坑
    int hole = left;
    // 区间控制
    int begin = left;
    int end = right;
    
    // 随机选 keyi 优化
    int randi = left + (rand() % (right - left + 1));
    swap(&a[randi], &a[hole]);
    
    // 相等就表示已经为 Key 找到合适的坑位了
    while (begin < end) {
        // 右边先走,找小
        while (begin < end && key <= a[end]) {
            end--;
        }
        a[hole] = a[end];
        hole = end;
        
        // 左边后走,找大
        while (begin < end && key >= a[begin]) {
            begin++; // 修复原代码中的双分号
        }
        a[hole] = a[begin];
        hole = begin;
    }
    
    // 让基准值入坑
    a[hole] = key;
    
    // 递归子区间
    QuickSort2(a, left, hole - 1);
    QuickSort2(a, hole + 1, right);
}

四、Lomuto 前后指针版本

1. Lomuto 前后指针版本介绍

Lomuto 前后指针版快排(以升序为例)的核心思路是:先选一个基准值,初始时 prev 和 cur 指向同一位置,然后用 cur 指针从左向右遍历,遇到比基准值小的元素时,prev 右移一位并交换二者位置;遇到比基准值大的元素时,cur 直接右移。遍历结束后,交换 prev 与基准值的位置完成分区。

文章配图

2. 代码实现

void QuickSort3(int* a, int left, int right) {
    if (left >= right) {
        return;
    }
    
    int keyi = left;
    // prev 从头开始的设计,是为了让遍历结束时,它刚好标记出基准值应处的位置
    int prev = left;
    int cur = left + 1;
    
    // 随机选 keyi 优化
    int randi = left + (rand() % (right - left + 1));
    swap(&a[randi], &a[keyi]);
    
    while (cur <= right) {
        // cur 找到了小于 key 的值
        if (a[cur] < a[keyi]) {
            prev++;
            swap(&a[prev], &a[cur]);
            cur++;
        } else {
            cur++;
        }
    }
    
    swap(&a[prev], &a[keyi]);
    keyi = prev;
    
    QuickSort3(a, left, keyi - 1);
    QuickSort3(a, keyi + 1, right);
}

3. 小区间优化

在快速排序中,小区间优化是一种常见的策略。当递归到小区间时,继续使用快速排序可能会因为递归调用的开销而导致性能下降。此时采用插入排序等简单排序算法来处理小区间,能减少递归深度和调用次数,降低栈空间的使用,同时利用插入排序在小规模数据上的优势。

void InsertSort(int* a, int n) {
    for (int i = 0; i < n - 1; i++) {
        int end = i;
        int tmp = a[i + 1];
        while (end >= 0) {
            if (a[end] > tmp) {
                a[end + 1] = a[end];
                end--;
            } else {
                break;
            }
        }
        a[end + 1] = tmp;
    }
}

void QuickSort3(int* a, int left, int right) {
    if (left >= right) {
        return;
    }
    
    // 小区间优化
    if ((right - left + 1) < 15) {
        InsertSort(a + left, right - left + 1);
        return;
    }
    
    int keyi = left;
    int prev = left;
    int cur = left + 1;
    
    int randi = left + (rand() % (right - left + 1));
    swap(&a[randi], &a[keyi]);
    
    while (cur <= right) {
        if (a[cur] < a[keyi]) {
            prev++;
            swap(&a[prev], &a[cur]);
            cur++;
        } else {
            cur++;
        }
    }
    
    swap(&a[prev], &a[keyi]);
    keyi = prev;
    
    QuickSort3(a, left, keyi - 1);
    QuickSort3(a, keyi + 1, right);
}

小区间优化的大小一般设置为 10~20(行业通用经验值,最常用的是 15)。

五、迭代版本(非递归)

1. 递归的缺陷

递归版快排虽逻辑清晰,但存在函数调用开销与递归深度过大导致的栈溢出风险。因此可通过手动维护栈来模拟递归调用,实现非递归版本。

2. 非递归思路

非递归快排的核心思路:用栈模拟递归调用,先压入整个数组的边界,然后循环弹出边界、分区,再把左右子区间的边界压入栈,直到栈为空,排序完成。

3. 代码实现

void QuickSortNonR(int* a, int left, int right) {
    Stack st;
    STInit(&st);
    
    // 入 [left right] 区间
    // 先入 right 是为了能正确取到 [left right] 区间
    STPush(&st, right);
    STPush(&st, left);
    
    while (!STEmpty(&st)) {
        int begin = STTop(&st);
        STPop(&st);
        int end = STTop(&st);
        STPop(&st);
        
        // 小区间优化
        if (end - begin + 1 < 15) {
            InsertSort(a + begin, end - begin + 1);
            continue;
        }
        
        // 随机选 keyi
        int randi = begin + (rand() % (end - begin + 1));
        swap(&a[randi], &a[begin]);
        
        // 前后指针版本
        int keyi = begin;
        int prev = begin;
        int cur = begin + 1;
        
        while (cur <= end) {
            if (a[cur] < a[keyi]) {
                prev++;
                swap(&a[cur], &a[prev]);
                cur++;
            } else {
                cur++;
            }
        }
        
        swap(&a[prev], &a[keyi]);
        keyi = prev;
        
        // 子区间处理
        if (begin < keyi - 1) {
            STPush(&st, keyi - 1);
            STPush(&st, begin);
        }
        if (keyi + 1 < end) {
            STPush(&st, end);
            STPush(&st, keyi + 1);
        }
    }
    
    STDestroy(&st);
}

六、三路划分

1. 三路划分思想

三路划分思想:把数组一次分成小于基准、等于基准、大于基准三部分,等于基准的元素直接就位不再递归,只递归左右两区。大量重复数据时效率极高,可直接替代普通快排。

2. 代码实现

文章配图

void QuickSortT(int* a, int left, int right) {
    if (left >= right) {
        return;
    }
    
    // 小区间优化
    if ((right - left + 1) < 15) {
        InsertSort(a + left, right - left + 1);
        return;
    }
    
    int begin = left;
    int end = right;
    int cur = left + 1;
    
    // 随机选 keyi
    int randi = left + (rand() % (right - left + 1));
    swap(&a[randi], &a[left]);
    int key = a[left];
    
    while (cur <= end) {
        if (a[cur] > key) {
            swap(&a[cur], &a[end]);
            end--;
            // 不能直接++cur 是因为从尾部交换过来的值我们还没检查过
        } else if (a[cur] < key) {
            swap(&a[begin], &a[cur]);
            begin++;
            // cur 可以直接++是因为 begin 的值已经被我们检查过了
            cur++;
        } else {
            cur++;
        }
    }
    
    QuickSortT(a, left, begin - 1);
    QuickSortT(a, end + 1, right);
}

3. 普通快排和三路划分效率对比

数据场景三路划分时间复杂度普通快排时间复杂度
全重复值(如全 2)O(n)O(n²)
大量重复值(如 80% 是 2)接近 O(n)O(nlogn)~O(n²)
无重复值(随机数组)O(nlogn)O(nlogn)

目录

  1. 一、快速排序思想
  2. 二、Hoare 版本
  3. 1. Hoare 版本介绍
  4. 2. 代码实现
  5. 3. 复杂度分析
  6. 4. 有序情况优化
  7. 4.1 随机选 keyi
  8. 4.2 三数取中
  9. 5. 稳定性分析
  10. 三、挖坑法
  11. 1. 挖坑法介绍
  12. 2. 代码实现
  13. 四、Lomuto 前后指针版本
  14. 1. Lomuto 前后指针版本介绍
  15. 2. 代码实现
  16. 3. 小区间优化
  17. 五、迭代版本(非递归)
  18. 1. 递归的缺陷
  19. 2. 非递归思路
  20. 3. 代码实现
  21. 六、三路划分
  22. 1. 三路划分思想
  23. 2. 代码实现
  24. 3. 普通快排和三路划分效率对比
  • 💰 8折买阿里云服务器限时8折了解详情
  • GPT-5.5 超高智商模型1元抵1刀ChatGPT中转购买
  • 代充Chatgpt Plus/pro 帐号了解详情
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 鸿蒙webview开发中web内部网络请求访问资源跨域问题,客户端解决方案
  • Java Object 类详解:继承体系与常用方法解析
  • 深度学习模型优化策略与实战调参
  • AI 时代,写作是比编程更核心的元技能
  • 深度学习模型优化策略与实战调参
  • C++ 模板编程入门:函数与类模板详解
  • OpenClaw AI 智能体安装配置与使用指南
  • AI 工具一键去除豆包及即梦图片与视频水印方法
  • Java 随机数实战:从范围字符串解析到动态区间生成
  • Nginx 502 Bad Gateway:基于上游日志与 FastCGI 超时的深度排查
  • Java Selenium 自动化测试基础教程
  • Qwen3Guard-Gen-WEB 实战测评:真实业务场景下的安全审核表现
  • LeetCode Hot 100 哈希表经典题目解析
  • AudioLDM-S 在虚拟现实中的应用:3D 空间音效生成
  • Linux 下调试 C/C++ 程序的核心 GDB 命令
  • Python 字典核心知识点与实战技巧
  • LeetCode 202. 快乐数:快慢指针解法详解
  • 通义千问 1.5-1.8B Chat GPTQ Int4 体验:vLLM 部署与 Chainlit 前端
  • LLM 大模型常见问题:幻觉现象解析与应对策略
  • JavaScript 设计模式:策略模式实战案例解析

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online