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

目录

  1. 快速排序的单趟排序 - 算法思想
  2. 快速排序的单趟排序 - 代码实现
  3. Hoare 版本
  4. 挖坑法
  5. 前后指针
  6. 三个版本我该改用哪一个?
  7. 快速排序的整体排序
  8. 快速排序的整体排序的算法思路
  9. 快速排序的整体排序的代码实现
  10. 找 key 值的方式
  11. 为什么要提出找 key 值的方式?
  12. 随机数选 key
  13. 三数取中
  14. 快速排序的非递归
C算法

快速排序详解:三种单趟实现及非递归优化

快速排序算法。涵盖单趟排序的三种实现方式:Hoare 法、挖坑法和前后指针法。介绍了整体排序的递归逻辑及优化 key 值选取的策略,包括随机数选 key 和三数取中法,以解决最坏时间复杂度问题。最后讲解了如何利用栈将递归改为非递归实现,避免栈溢出风险。

山野诗人发布于 2026/3/30更新于 2026/4/130 浏览
快速排序详解:三种单趟实现及非递归优化

快速排序的单趟排序 - 算法思想

在开始写代码时,理解算法的思路是不可或缺的一部分。接下来我们来理解一下快速排序的算法思想(以升序排序为主)。

核心思想:在待排序的数组中,选择一个值作为 key 值。key 值的选择会影响快速排序的效率。随后定义两个变量 left 和 right,控制待排序数组的区间。假定待排序数组的第一个元素就是 key 值,此时一定要让 right 变量先动,right 往前动的目的是找比 key 值小的数。之后让 left 变量往后动,left 往前动的目的是找比 key 值大的数。将这两个数进行位置上的交换。这个过程一直持续到 right 和 left 相遇,此时将 key 值一开始所在的位置与相遇位置的值进行互换。最终达到的效果是比 key 值小的数都在 key 值的左边,比 key 值大的数都在 key 值的右边。

图 1

基于上述例子进行步骤拆解。

例子讲解

最终效果:

最终效果

快速排序的单趟排序 - 代码实现

上述思路由 Hoare 提出。该版本代码理解起来有一定难度,书写容易出错,主要是在选最左边的值为 key 时得让 right 先走。为此,后面又出现了挖坑法、以及前后指针法。

Hoare 版本

void QuickSortPart1(int* a, int left, int right) {
    // Hoare 版本 - 单趟排序
    // 我们选择最左边为 key 值,这里我们可以采用记录下标的方式。
    int keyi = left;
    while (left < right) {
        // 先让 right 动,因为我们选取的是左边的 key
        // right 的目的是找到比 key 值小的元素的位置
        while (right > left && a[right] >= a[keyi]) {
            --right;
        }
        // 之后在让 left 动
        // left 的目的是找到比 key 值大的元素的位置
        while (left < right && a[left] <= a[keyi]) {
            ++left;
        }
        // 到这里,就说明都完成了各自的使命,最后再将它们的位置上的数交换一下。
        Swap(&a[left], &a[right]);
    }
    
    Swap(&a[left], &a[keyi]);
    keyi = left;
}
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • 常见排序算法原理及代码实现
  • 数据结构:栈与队列详解及实现
  • 基于正交匹配追踪(OMP)算法的信号稀疏分解 MATLAB 实现
  • 双指针经典算法题解析
  • LIO-SAM 算法在 Ubuntu 22.04 与 ROS2 Humble 下的仿真实现
  • STL 容器适配器:stack 与 queue 底层模拟及算法实践
  • 算法:滑动窗口技巧与应用
  • HDFS DataNode 故障处理:节点下线、数据重建、磁盘更换全流程

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,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

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

// 出了 while 循环后,就说明了此时 left=right。那我们就得将此位置上的数与 keyi 上面的数进行交换。

细节处理不到位可能导致预期不一样,例如将 a[right] >= a[keyi] 写成 a[right] > a[keyi],或者没有判断 right 和 left 越界导致程序崩溃。

后来,人们根据 Hoare 的思想研发了另一种方式——'挖坑法'。

挖坑法

这个方法的底层就是 Hoare 大佬的思想。

挖坑法 - gif

可以看到上面的动图,当我们在选取的 key 值的时候,就相当于在这个位置上面挖个坑,我们可以用一个变量记录此时坑出现的位置。然后按照 Hoare 大佬的思想,将需要移动数据移动到旧坑,那么此时就会出现新的坑,我们更新记录坑这个位置的变量即可。

void QuickSortPart2(int* a, int left, int right) {
    // 挖坑法版本 - 单趟排序
    // 我们选择最左边为 key 值,这里我们就直接采用记录值的方式。
    int key = a[left];
    // 定义一个坑
    int hole = left;
    while (left < right) {
        // 先让 right 动,因为我们选取的是左边的 key
        // right 的目的是找到比 key 值小的元素的位置
        while (right > left && a[right] >= key) {
            --right;
        }
        // 找到了新的坑了,先把旧坑填了
        a[hole] = a[right];
        hole = right;
        // 之后在让 left 动
        // left 的目的是找到比 key 值大的元素的位置
        while (left < right && a[left] <= key) {
            ++left;
        }
        // 找到了新的坑了,先把旧坑填了
        a[hole] = a[left];
        hole = left;
    }
    // 出了 while 循环后,就说明了此时 left=right。那我们就得将此位置上的数与 keyi 上面的数进行交换。
    a[hole] = key;
}

前后指针

除了 Hoare 大佬这个思想之外,有人还提出了一种新的思想 —— '用前后指针来实现'。其本质就是将待排序的数组分割成两个子区间,其中一个子区间里面的元素都大于基准值(key 值),另一个子区间里面的元素都小于基准值(key 值)。

前后指针 - gif

可以看到的是:它会分为两种情况:当 key 值大于 cur 指向的值时,先让 prev 往后走一步,紧接着,再让 prev 位置上的值与 cur 位置上的值交换位置。最后,cur 再往后面走一步;当变量 cur 所指向的值大于 key 值时,直接让 cur 往后面走一步即可。

上代码:

void QuickSortPart3(int* a, int left, int right) {
    // 前后指针版本 - 单趟排序
    int n = right - left + 1;
    // 我们选择最左边为 key 值,这里我们就直接采用下标的方式。
    int keyi = left;
    // 定义一个 prev 和 cur
    int prev = left;
    int cur = left + 1;
    while (cur < n) {
        if (a[keyi] > a[cur]) {
            prev++;
            Swap(&a[prev], &a[cur]);
        }
        cur++;
    }
    Swap(&a[prev], &a[keyi]);
    keyi = prev;
}

至此,关于快速排序单趟排序的三个版本的代码演示就完成了。

三个版本我该改用哪一个?

这三个版本都能实现同一个功能。如果你实在记不住那么多的话,强烈推荐前后指针这个版本。这个版本也是现在很多人写快速排序算法时会用到的方法。此外,面试中也难免会碰到这个问题,建议大家都要掌握。

快速排序的整体排序

快速排序的整体排序的算法思路

从单趟排序我们就可以知道,单趟排序的目的就是将我们所选的 key 值放到待排序数组中正确的位置上。然后将待排序数组划分成两个区间 [begin, keyi-1] 和 [keyi+1, end]。然后,我们又可以对这两个区间里的值再使用单趟排序的思路,这就是递归。

图解

快速排序的整体排序的代码实现

void QuickSort(int* a, int left, int right) {
    if (left >= right) {
        return;
    }
    int begin = left, end = right;
    // Hoare 版本 - 单趟排序
    // 我们选择最左边为 key 值,这里我们可以采用记录下标的方式。
    int keyi = left;
    while (left < right) {
        // 先让 right 动,因为我们选取的是左边的 key
        // right 的目的是找到比 key 值小的元素的位置
        while (right > left && a[right] >= a[keyi]) {
            --right;
        }
        // 之后在让 left 动
        // left 的目的是找到比 key 值大的元素的位置
        while (left < right && a[left] <= a[keyi]) {
            ++left;
        }
        // 到这里,就说明都完成了各自的使命,最后再将它们的位置上的数交换一下。
        Swap(&a[left], &a[right]);
    }
    // 出了 while 循环后,就说明了此时 left=right。那我们就得将此位置上的数与 keyi 上面的数进行交换。
    Swap(&a[left], &a[keyi]);
    keyi = left;
    // 递归
    QuickSort(a, begin, keyi - 1);
    QuickSort(a, keyi + 1, end);
}

有了上面的思想,递归看起来也不是很难理解。

找 key 值的方式

我们为什么要谈论这个问题呢?相信大家在上面已经察觉到了,我们的 key 值目前来说要不就是去待排序数组开头的元素,要不就是待排序数组结尾的元素。这样做会有问题吗?

为什么要提出找 key 值的方式?

有一部分的读者会认为:直接选待排序数组开头的元素,要不就是待排序数组结尾的元素作为 key 值挺好的啊。

可以想一下快速排序的时间复杂度是多少?应该是 O(N*logN)。

但是大家有没有想过这么一个问题:我们用刚才写的快速排序来排一个已经有序(升序、降序)的数组的时间复杂度又是多大呢?

升序数组

可以看到它的时间复杂度变为了 O(N^2)。这个时间复杂度对于排序算法来说是个大忌。我们有什么办法能够抢救一下这个时间复杂度吗?方法肯定是有的。

可以看到时间复杂度之所以这么高,是因为 keyi 的位置所导致的。总结上面的思想,我们希望看到的是 keyi 值越是取整个待排序数组中较为中间的值时,它的效率是最大的。如果 keyi 所在位置的值是待排序数组的较为中间的值,那么经过一次单趟排序后,其分出的两个子区间各自的元素数量较为均等,这样就越接近 logN 这个量级,而不是 N 这个量级了。

所以人们就提出了两种选择 key 值的策略:

  1. 随机数选 key
  2. 三数取中

随机数选 key

这个方式我不是很推荐大家使用,不过这个方法现在仍有人在玩。

随机数选 key 的代码很简单就是利用 rand 函数生成一个区间范围内的数,将它与最左边或最右边的值互换。

可能有的读者就会问了,为什么要互换呢,我直接选了就直接用不好吗?这里要说明的一点是,肯定是要交换位置的!原因是承接我们上面代码的思想。

void QuickSort(int* a, int left, int right) {
    srand((unsigned int)time(NULL));
    if (left >= right) {
        return;
    }
    int begin = left, end = right;
    // Hoare 版本 - 单趟排序
    // 我们选择最左边为 key 值,这里我们可以采用记录下标的方式。
    // 随机数选 key
    int randi = left + rand() % (right - left + 1);
    Swap(&a[randi], &a[left]);
    int keyi = left;
    while (left < right) {
        // 先让 right 动,因为我们选取的是左边的 key
        // right 的目的是找到比 key 值小的元素的位置
        while (right > left && a[right] >= a[keyi]) {
            --right;
        }
        // 之后在让 left 动
        // left 的目的是找到比 key 值大的元素的位置
        while (left < right && a[left] <= a[keyi]) {
            ++left;
        }
        // 到这里,就说明都完成了各自的使命,最后再将它们的位置上的数交换一下。
        Swap(&a[left], &a[right]);
    }
    // 出了 while 循环后,就说明了此时 left=right。那我们就得将此位置上的数与 keyi 上面的数进行交换。
    Swap(&a[left], &a[keyi]);
    keyi = left;
    // 递归
    QuickSort(a, begin, keyi - 1);
    QuickSort(a, keyi + 1, end);
}

三数取中

选取的三个数字分别位于待排序数组开头位置的元素、中间位置的元素以及结尾位置的元素。

代码如下:

// 三数取中
int GetMidNum(int* a, int left, int right) {
    int mid = (left + right) / 2;
    if (a[left] > a[right]) {
        if (a[mid] > a[right]) {
            return mid;
        } else // 说明 a[mid]<=a[right]
        {
            return right;
        }
    } else // 说明 a[left]<=a[right]
    {
        if (a[left] > a[mid]) {
            return left;
        } else // 说明 a[left]<=a[mid]
        {
            return mid;
        }
    }
}
void QuickSort(int* a, int left, int right) {
    if (left >= right) {
        return;
    }
    int begin = left, end = right;
    // Hoare 版本 - 单趟排序
    // 我们选择最左边为 key 值,这里我们可以采用记录下标的方式。
    // 三数取中
    int midi = GetMidNum(a, left, right);
    if (midi != left) {
        Swap(&a[midi], &a[left]);
    }
    int keyi = left;
    while (left < right) {
        // 先让 right 动,因为我们选取的是左边的 key
        // right 的目的是找到比 key 值小的元素的位置
        while (right > left && a[right] >= a[keyi]) {
            --right;
        }
        // 之后在让 left 动
        // left 的目的是找到比 key 值大的元素的位置
        while (left < right && a[left] <= a[keyi]) {
            ++left;
        }
        // 到这里,就说明都完成了各自的使命,最后再将它们的位置上的数交换一下。
        Swap(&a[left], &a[right]);
    }
    // 出了 while 循环后,就说明了此时 left=right。那我们就得将此位置上的数与 keyi 上面的数进行交换。
    Swap(&a[left], &a[keyi]);
    keyi = left;
    // 递归
    QuickSort(a, begin, keyi - 1);
    QuickSort(a, keyi + 1, end);
}

这样我们就能很好的解决快速排序时间复杂度为 O(N^2) 的问题了。

快速排序的非递归

我们都知道如果递归深度过深时,会导致栈溢出的情况,从而使整个程序崩溃。所以我们就必须具备一种能力,将递归改为非递归。

将递归改为非递归有两种方式:

  1. 直接改造成循环(就先斐波那契数列一样);
  2. 利用栈这个数据结构来实现。

我们在这里用的是第二种方式,如果有对栈这个数据结构还不理解的读者,建议先复习相关概念。

这里我们可以这么想,我们通过单趟排序使得区间划分成了两个部分,然后我们在对这两部分区间再次使用单趟排序的思想。所以这里的关键就在于区间的划分,我们可以使用栈先将我们要后排序的区间先入栈,先排序的区间最后在入栈。

代码的实现如下:

int QuickSortPart(int* a, int left, int right) {
    // Hoare 版本 - 单趟排序
    // 我们选择最左边为 key 值,这里我们可以采用记录下标的方式。
    // 三数取中
    int midi = GetMidNum(a, left, right);
    if (midi != left) {
        Swap(&a[midi], &a[left]);
    }
    int keyi = left;
    while (left < right) {
        // 先让 right 动,因为我们选取的是左边的 key
        // right 的目的是找到比 key 值小的元素的位置
        while (right > left && a[right] >= a[keyi]) {
            --right;
        }
        // 之后在让 left 动
        // left 的目的是找到比 key 值大的元素的位置
        while (left < right && a[left] <= a[keyi]) {
            ++left;
        }
        // 到这里,就说明都完成了各自的使命,最后再将它们的位置上的数交换一下。
        Swap(&a[left], &a[right]);
    }
    // 出了 while 循环后,就说明了此时 left=right。那我们就得将此位置上的数与 keyi 上面的数进行交换。
    Swap(&a[left], &a[keyi]);
    keyi = left;
    return keyi;
}

void QuickSortNonR(int* a, int left, int right) {
    Stack st;
    STInit(&st);
    STPush(&st, right);
    STPush(&st, left);
    while (!STEmpty(&st)) {
        int begin = STTop(&st);
        STPop(&st);
        int end = STTop(&st);
        STPop(&st);
        int keyi = QuickSortPart(a, begin, end);
        if (keyi + 1 < end) // 用来判断右子区间
        {
            STPush(&st, end);
            STPush(&st, keyi + 1);
        }
        if (keyi - 1 > begin) // 用来判断左子区间
        {
            STPush(&st, keyi - 1);
            STPush(&st, begin);
        }
    }
    STDestory(&st);
}

到这里,本文的内容就全部讲完了。

  • LeetCode 141: 环形链表判断算法详解
  • Python 3.12 内置函数全图鉴:71 个核心工具详解
  • XGBoost Python 机器学习实战教程与参数详解
  • MetaTrader5 Python 库数据获取与交易接口详解
  • Python IDE 选型指南:主流工具对比与场景适配
  • 全国计算机等级考试二级 Python 历年真题及参考答案(综合应用题)
  • Python openpyxl 和 pandas 使用详解
  • 30 行 Python 实现公开接口数据抓取与本地存储
  • Python 开源库 Streamlit 详解与实战
  • Python 英文界面切换为中文的方法
  • Python 爬虫实战:爬取网易云热歌榜歌曲
  • 子串与滑动窗口算法典型题解 Python 实现