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

数据结构:快速排序非递归实现、优化及内省排序详解

综述由AI生成深入讲解快速排序的非递归实现,利用栈结构模拟递归过程。介绍了快排的优化策略,包括随机选取基准值避免最坏情况以及三路划分处理重复数据。阐述了内省排序(IntroSort)原理,结合插入排序、堆排序和快速排序的优势,根据数据规模和递归深度动态切换算法以保证 O(N log N) 性能。最后分析了常见排序算法的时间复杂度与稳定性。

PentesterX发布于 2026/3/26更新于 2026/5/2326 浏览
数据结构:快速排序非递归实现、优化及内省排序详解

前言

基于递归的方式学习了快速排序和归并排序,今天我们来深究快速排序,使用栈的数据结构非递归实现快排,优化快排(三路划分)。

一、非递归实现快排

上篇博客基于递归实现了三个版本的快排,hoare 版本,挖坑法,前后指针法。其实就是围绕基准值进行操作,不管哪一种版本,都离不开找基准值,递归得到子区间。快排的非递归版本也离不开找基准值,但是对区间进行了处理,使用到栈的数据结构。

把一个大区间分成几个小区间。

给定初始数据样例,我们正常使用前后指针的方法进行快排,找基准值。

基准值,以及区间的下标。

我们把 0-2 的区间左右下标入栈,4-5 的区间下标入栈,相当于递归到子区间的操作。栈是遵循先进后出的规则,刚好和递归的区间的遍历顺序一样。每次前后指针找完基准值,就把分出来的左右区间下标入栈。但还是要注意越界的情况,比如基准值的节点在最左边或者最右边。

假设基准值的下标为 keyi,那么右区间就是 [keyi+1,end],左区间就是 [begin,keyi-1]。

上图的有些区间就是不符合条件的。

基本思路都叙述的差不多了,上代码。

void QuickSortNonR(int* a, int left, int right) {
    std::stack<int> st; // 定义一个栈
    st.push(right); // 这里先让右端下标入栈 因为栈是先进后出的
    st.push(left); // 再让左端下标入栈
    while (!st.empty()) {
        int begin = st.top(); // 取当前栈顶元素,也就是区间的左端
        st.pop();
        int end = st.top(); // 取右端元素
        st.pop();
        int prev = begin, cur = prev + 1; // 然后就是前后指针找基准值
        int keyi = begin;
        while (cur <= end) {
            if (a[cur] < a[keyi] && ++prev != cur) {
                swap(a[prev], a[cur]);
            }
            ++cur;
        }
        swap(a[keyi], a[prev]);
        keyi = prev; // 这里找到了基准值
        if (keyi + 1 < end) // 再根据基准值,分出左区间和右区间进行入栈
        {
            st.push(end);
            st.push(keyi + 1); // 右区间
        }
        if (keyi - 1 > begin) {
            st.push(keyi - 1);
            st.push(begin); // 左区间
        }
    }
}

非递归版本的快速排序就完成啦。

二、快排的优化版本

快排的缺陷在上篇博客和大家讲过,如果数据有序或者数据全部相同的情况,快速排序的时间复杂度可能会到 O(N^2)。这里对初始基准值的确定进行优化,如果数据有序,不从第一个数据取基准值。以及在前后指针的方法上引入三路划分,对相同的数据进行处理。其次三路划分针对有大量重复数据时,效率很好,其他场景就一般,但是三路划分思路还是很价值的,有些快排思想变形体,要用划分去选数,他能保证跟 key 相等的数都排到中间去,三路划分的价值就体现出来了。

基准值确定的优化,使用 rand 函数,在区间中间随机找一个数据,比确定第一个数据要好很多,避免了一些极端情况。

int randi = left + (rand() % (right - left + 1)); // 取随机数值

根据上图的三路划分思路以及示例图有如下代码:

void QuickSort(int* arr, int left, int right) // 三路划分
{
    if (left >= right) { return; }
    int begin = left;
    int end = right;
    int randi = left + (rand() % (right - left + 1)); // 取随机数值作为基准值
    swap(arr[randi], arr[left]); // 把基准值放在最左边
    int key = arr[left]; // 定义 key 值
    int cur = left + 1; // 这里类似于前后指针法 但是做了一些优化
    while (cur <= right) // 左右同时往中间推
    {
        // 解除了中间数据相同影响性能的问题
        if (arr[cur] < key) // 遇到比 key 小的数值 交换数值 left++,cur++
        {
            swap(arr[cur], arr[left]);
            left++;
            cur++;
        }
        else if (arr[cur] > key) // 遇到比 key 大的数据 不管 right 此时为什么 直接交换
        {
            swap(arr[cur], arr[right]);
            right--;
        }
        else { cur++; }
    }
    // 每次都看 cur 指定的值 如果小于 key 就放左边 大于 right 就放右边 等于就继续走
    // left-right 区间都是相同的值 不用进一步递归
    QuickSort(arr, begin, left - 1); // 左区间
    QuickSort(arr, right + 1, end); // 右区间
}

三、内省排序

内省排序是基于直接插入排序,堆排序,快排实现的,在合适的情景使用合适的排序方式,使排序最优化,差不多和 c++ 里面的 sort 排序的底层是一样的。内省排序可以认为不受数据分布的影响,无论什么原因划分不均匀,导致递归深度太深,他就是转换堆排了,堆排不受数据分布影响。

内省排序要处理的就是递归的深度,递归层次太深的话,就转用堆排序,数据很少的话就直接使用直接插入排序。

void InsertSort(int* arr, int n) // 直接插入排序
{
    for (int i = 0; i < n - 1; i++) {
        int end = i;
        int tmp = arr[end + 1];
        while (end >= 0) {
            if (arr[end] > tmp) { arr[end + 1] = arr[end]; end--; }
            else { break; }
        }
        arr[end + 1] = tmp;
    }
}

void AdjustDown(int* arr, int parent, int n) // 堆排序向下调整算法
{
    int child = parent * 2 + 1;
    while (child < n) {
        if (child + 1 < n && arr[child] < arr[child + 1]) { child++; }
        if (arr[child] > arr[parent]) { swap(arr[child], arr[parent]); parent = child; child = parent * 2 + 1; }
        else { break; }
    }
}

void HeapSort(int* arr, int n) // 堆排
{
    for (int i = (n - 1 - 1) / 2; i >= 0; i--) { AdjustDown(arr, i, n); }
    int end = n - 1;
    while (end > 0) { swap(arr[0], arr[end]); AdjustDown(arr, 0, end); end--; }
}

void IntroSort(int* arr, int left, int right, int depth, int defaltDepth) // 内省排序 优化排序性能 保持稳定 n*logn
{
    if (left >= right) { return; }
    if (right - left + 1 < 16) // 区间大小比较小时 用插入排序
    {
        InsertSort(arr + left, right - left + 1);
        return;
    }
    if (depth > defaltDepth) // 当递归层次太深时 转用 heap 堆排序
    {
        HeapSort(arr + left, right - left + 1);
        return;
    }
    depth++;
    int begin = left;
    int end = right;
    int randi = left + (rand() % (right - left + 1)); // 随机找基准值
    swap(arr[randi], arr[left]);
    int key = arr[left];
    int cur = left + 1;
    while (cur <= right) {
        if (arr[cur] < key) { swap(arr[cur], arr[left]); left++; cur++; }
        else if (arr[cur] > key) { swap(arr[cur], arr[right]); right--; }
        else { cur++; }
    }
    IntroSort(arr, begin, left - 1, depth, defaltDepth); // 递归左右部分
    IntroSort(arr, right + 1, end, depth, defaltDepth);
}

void QuickSort1(int* arr, int left, int right) // 内省排序 对应数据对应处理办法
{
    int depth = 0;
    int logn = 0;
    int n = right - left + 1;
    for (int i = 1; i < n; i *= 2) { logn++; // 递归层数 }
    IntroSort(arr, left, right, depth, logn * 2);
}

代码涵盖了前面所学习的各种排序算法,插入,选择,交换都涉及到了。对于快排,从最开始的 hoare 版本,挖坑,前后指针,都有一些些小缺陷,到现在优化到三路快排,内省排序,把时间复杂度尽量调整到了 n*logn。

四、排序算法复杂度以及稳定性的分析

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且 r[i] 在 r[j] 之前,而在排序后的序列中,r[i] 仍在 r[j] 之前,则称这种排序算法是稳定的;否则称为不稳定的。相等的元素依然按照之前的相对顺序不发生改变就是稳定的。

通过这几天的学习,已经把初阶数据结构的排序算法都学完了。冒泡是具有教学意义的存在。直接一点的选择和插入都是情理之中。带有 gap 的直接插入变成了希尔,让直男变的有情商。快排是虽然快,但是也有发挥不好的时候。堆和归并两兄弟是发挥一直很出色,速度也很快。稳定性高,而又快速的就属归并排序。

总结

本篇博客下来,快排也能一直处于稳定的时间复杂度。想想内省排序,才是对症下药,给什么样的数据,用对应克制他的排序,根据需求解决问题。优化快排的同时,有对前面的排序知识有了更深刻的认知。排序的学习就到这里了,初阶数据结构也结束啦。

目录

  1. 前言
  2. 一、非递归实现快排
  3. 二、快排的优化版本
  4. 三、内省排序
  5. 四、排序算法复杂度以及稳定性的分析
  6. 总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • 基于 Prophet 的家庭用电数据时间序列预测分析
  • Java 编译错误:源发行版 17 需要目标发行版 17 解决方案
  • Z-Image ComfyUI 网页端部署与文生图实战
  • Java 状态机详解:三种实现方式消除 if-else 嵌套
  • 从三年前端到 CS 硕士:韩国留学复盘与前端回归之路
  • SpringBoot 3 SpringDoc OpenAPI 常用注解详解与实战示例
  • SpringDoc OpenAPI 常用注解详解与实战示例
  • .NET 集成 GoView 低代码可视化大屏实战指南
  • JSP 高速公路管理系统设计与实现
  • TypeScript 核心概念与高频面试题解析
  • 零基础入门AI绘画:Z-Image-Turbo 使用教程
  • PhotoEdit:一款高性能的 Android 图片编辑开源库
  • 注意力机制与 Transformer 模型实战
  • DigitalOcean 注册、验证及云主机创建教程
  • 项目分享|LiveKit Agents Playground:快速搭建WebRTC服务端Agent原型的利器
  • Qwen3-4B 极速文本对话:搭建 AI 写作助手
  • MCP 模型上下文协议详解:原理、组件与应用
  • Visual C++ 运行库缺失解决方案与安装指南
  • C++ 进阶:从裸指针到智能指针的内存管理进化
  • 金仓数据库 KingbaseES:多模融合架构与全场景替代方案

相关免费在线工具

  • 加密/解密文本

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