跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C算法

快速排序核心原理与多版本实现详解

快速排序基于分治策略,通过基准值将数组划分为小于、等于、大于三部分。深入剖析 Hoare、挖坑法及 Lomuto 三种经典分区实现,探讨随机选基准、三数取中、小区间插入排序等性能优化手段,并对比三路划分在处理大量重复数据时的效率优势,最后给出非递归迭代方案以避免栈溢出风险。

NodeJser发布于 2026/3/29更新于 2026/6/927 浏览
快速排序核心原理与多版本实现详解

一、快速排序思想

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

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

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


二、Hoare 双指针分区

Hoare 版本介绍

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

针对升序排序:选取区间首元素作为基准值 key,右指针从右向左寻找首个小于 key 的元素,左指针再从左向右寻找首个大于 key 的元素,交换两指针指向的元素并重复此过程,直到两指针相遇。最后将基准值与相遇位置的元素交换,即可完成分区。

文章配图

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

文章配图

代码实现
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--;
        }
        // 左指针从前往后找:第一个比基准值大的元素
         (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);
}
while
// 交换找到的两个元素,让小值到左、大值到右
// 指针相遇时,该位置就是基准值的最终位置,交换归位
// 递归处理基准值左右两侧的子区间,完成整体排序
1
1

这里有个细节值得注意:为什么 left 和 right 指定的数据和 key 值相等时不能交换?如果相等就交换,可能会导致死循环或者逻辑错误,具体取决于指针移动的顺序。在 Hoare 版本中,必须确保指针在找到严格小于或大于基准值的元素时才停止。

复杂度分析

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

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

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

边界情况优化
随机基准选择

即使输入是有序数组,随机选基准也能大概率避免'每次选到最左 / 最右元素',让递归树尽可能平衡,将最坏时间复杂度 O(n²) 优化为概率上的 O(nlogn)。

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]);
    
    // 处理 [begin end] 区间
    while (begin < end) {
        // 右边找小:升序
        while (begin < end && a[end] >= a[keyi]) {
            end--;
        }
        // 左边找大:升序
        while (begin < end && a[begin] <= a[keyi]) {
            begin++;
        }
        // 找到后交换
        swap(&a[begin], &a[end]);
    }
    
    // 走到这代表 begin 和 end 当前位置就是 key 的合适位置
    swap(&a[keyi], &a[begin]);
    keyi = begin;
    
    // 递归子区间
    QuickSort1(a, left, keyi - 1);
    QuickSort1(a, keyi + 1, right);
}
三数取中
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 { // a[left] > a[midi]
        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]);
    
    // 处理 [begin end] 区间
    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 函数从区间的左、中、右三个位置中选出数值居中的元素下标,将其交换到区间左端点作为基准值。以此避免在有序数组中选到极值导致的性能退化,让递归树更接近平衡,从而将时间复杂度稳定在 O(NlogN)。

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

稳定性分析

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


三、挖坑法

挖坑法介绍

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

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

文章配图

代码实现
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 前后指针版本

Lomuto 版本介绍

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

文章配图

代码实现
void QuickSort3(int* a, int left, int right) {
    if (left >= right) {
        return;
    }
    
    int keyi = left;
    // prev 从头开始的设计,是为了让遍历结束时,它刚好标记出基准值应处的位置
    int prev = left;
    int cur = left + 1;
    
    // 随机选基准优化
    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);
}
小区间优化

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

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)。


五、迭代版本(非递归)

递归的缺陷

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

非递归思路

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

代码实现
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);
}

六、三路划分

三路划分思想

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

文章配图

代码实现
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++;
        }
    }
    
    // [left begin - 1] [begin end] [end + 1 right]
    QuickSortT(a, left, begin - 1);
    QuickSortT(a, end + 1, right);
}
普通快排和三路划分效率对比
数据场景三路划分时间复杂度普通快排时间复杂度
全重复值(如全 2)O(n)O(n²)
大量重复值(如 80% 是 2)接近 O(n)O(nlogn)~O(n²)
无重复值(随机数组)O(nlogn)O(nlogn)

目录

  1. 一、快速排序思想
  2. 二、Hoare 双指针分区
  3. Hoare 版本介绍
  4. 代码实现
  5. 复杂度分析
  6. 边界情况优化
  7. 随机基准选择
  8. 三数取中
  9. 稳定性分析
  10. 三、挖坑法
  11. 挖坑法介绍
  12. 代码实现
  13. 四、Lomuto 前后指针版本
  14. Lomuto 版本介绍
  15. 代码实现
  16. 小区间优化
  17. 五、迭代版本(非递归)
  18. 递归的缺陷
  19. 非递归思路
  20. 代码实现
  21. 六、三路划分
  22. 三路划分思想
  23. 代码实现
  24. 普通快排和三路划分效率对比
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • 快速排序算法原理与实现详解
  • 基于 Whisper 的本地语音识别与隐私保护方案
  • Gitee 推送报错 400/403 排查与 SSH 协议解决方案
  • C++ 命名空间(namespace)详解与实战指南
  • AI 聊天机器人前端界面构建与生产环境部署
  • 2024 大模型典型示范应用案例深度解析
  • 攻防世界 Web 题解:SQL 注入与文件包含漏洞分析
  • 从 ComfyUI 到 Agent 工作流:设计思路、实践案例与优化策略
  • FRCRN 开源模型实战:WebAssembly 浏览器端轻量化部署
  • Magnet Player:基于 Web 的磁力链媒体播放器
  • C++11 深度剖析:列表初始化与右值引用
  • Stable Diffusion 教程:额外功能、后期处理与高清化
  • 绿联云 NAS 配置 WebDAV 实现 Zotero 公网同步
  • Llama-2-7B 昇腾 NPU 性能测评与部署实践指南
  • Ollama 本地大模型部署与 WebAPI 调用实战指南
  • Ollama:本地大模型 WebAPI 调用实战指南
  • Git Push 失败?配置 SSH Key 实现代码推送
  • Ubuntu 部署 OpenClaw 完整教程
  • 基于 Unity 2022 LTS 与 Rokid UXR 3.0 SDK 开发轻量级 AR 消消乐游戏
  • Windows 安装 Python 后 CMD 命令行无法识别

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如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