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

数据结构——排序算法:冒泡、快速排序与归并排序详解

数据结构中的交换排序与归并排序详解。内容涵盖冒泡排序基础原理,以及快速排序的 Hoare、挖坑法和 Lomuto 前后指针三种实现方式,分析各自的时间复杂度与边界情况。同时介绍基于分治法的归并排序,对比其与快排在稳定性和性能上的差异,提供完整的 C/C++ 代码实现与特性总结。

人间过客发布于 2026/3/16更新于 2026/7/2037 浏览
数据结构——排序算法:冒泡、快速排序与归并排序详解

前言

继上篇学习了排序的前面两个部分:直接插入排序和选择排序,今天我们来学习排序中常用的交换排序以及非常稳定的归并排序。快排有多种方法,高速列车即将发车。

一、交换排序

交换排序基本思想: 所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置。 交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

1.1 冒泡排序

冒泡排序是一种最基础的交换排序。之所以叫做冒泡排序,因为每一个元素都可以像小气泡一样,根据自身大小一点一点向数组的一侧移动。这个算法在平常算法题中基本不用(因为太慢了),只能说具有教学意义。

void BubbleSort(int* a, int n) {
    int exchange = 0;
    for (int i = 0; i < n; i++) {
        exchange = 0;
        for (int j = 0; j < n - 1 - i; j++) {
            if (a[j] > a[j + 1]) {
                exchange = 1;
                swap(a[j], a[j + 1]);
            }
        }
        if (!exchange)
            break;
    }
}

冒泡排序的特性总结:

  • 时间复杂度:O(N^2)
  • 空间复杂度:O(1)

1.2 快速排序

快速排序是 Hoare 于 1962 年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后对左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

快速排序实现主框架: 其实快排主要就是递归,把一个大区间不断划分成子区间

void QuickSort(int* a, int left,  right) {
     (left >= right)
        ;
    
     meet = (a, left, right);
    (a, left, meet - );
    (a, meet + , right);
}
int
if
return
// _QuickSort_Hoare 用于按照基准值将区间 [left, right] 中的元素进行划分
int
QuickSort_Hoare
QuickSort
1
QuickSort
1
1.2.1 Hoare 版本 快排

算法思路: 创建左右指针,确定基准值。 从右向左找出比基准值小的数据,从左向右找出比基准值大的数据,左右指针数据交换,进入下次循环。 其实就是确定一个数为基准值,然后根据基准值,把当前区域的数据分成两部分,左边小于基准值,右边大于基准值,然后再递归到分好的区域,继续重复操作,一个大问题划分成无数个一样的子问题。

int QuickSort_Hoare(int* a, int left, int right) {
    int keyi = left; // 先定义区间第一个数为基准值
    while (left <= right) { // 只要 left<right 就继续循环
        // 我们这里 right 是找比基准值小的数据,left 是找比基准值大的数据,然后进行调换
        while (left <= right && a[right] > a[keyi]) // 当右边的值大于基准值时 right-- 直到找到小于基准值的再跳出循环
            right--;
        while (left <= right && a[left] < a[keyi]) // 当左边的数据小于基准值时 left++ 直到找到大于基准值的再跳出循环
            left++;
        // 两个循环跳出后 left 对应的数据大于基准值,right 对应的数据小于基准值
        // 对数据进行调换,这样就把小的放在左边,大的放在右边
        if (left <= right) {
            swap(a[right--], a[left++]);
        }
    }
    // 当 left>right 的时候,交换最开始的基准值的位置,这个时候新的基准值就取 right
    swap(a[right], a[keyi]);
    return right; // 到此,新的区间划分就处理好了,新的基准值返回就好啦
}

为什么跳出循环后 right 位置的值一定不大于 key? 当 left > right 时,即 right 走到 left 的左侧,而 left 扫描过的数据均不大于 key,因此 right 此时指向的数据一定不大于 key。

问题 2:为什么 left 和 right 指定的数据和 key 值相等时也要交换? 相等的值参与交换确实有一些额外消耗。实际还有各种复杂的场景,假设数组中的数据大量重复时,相等也交换能进行有效的分割排序。 如果不相等才交换的话,假设数据全是相同的数据,那每次基准值只能找到初始基准值的下一个,时间复杂度会变成 O(N^2)。

快排是挺快的,但好东西总有缺陷。

快排:Hoare 版本的时间复杂度 划分区间递归的时间复杂度为 logn 每次区间内找新的基准值时间复杂度为 n 时间复杂度为 N*logN 但是在数据有序的时候,时间复杂度还是 O(N^2),新的基准值只能找到 key 的下一个数字,划分区间的效率很低

1.2.2 挖坑法 快排

思路: 创建左右指针。首先从右向左找出比基准小的数据,找到后立即放入左边坑中,当前位置变为新的'坑',然后从左向右找出比基准大的数据,找到后立即放入右边坑中,当前位置变为新的'坑',结束循环后将最开始存储的分界值放入当前的'坑'中,返回当前'坑'下标(即分界值下标)。 就是先从右往左找,再从左往右找,不断循环,直到 left>right,过程中数值一直在迭代交换,这个时候最后一个坑刚好放最开始挖的值。

相比 Hoare 还是有差别的。

int QuickSort_Pit(int* a, int left, int right) {
    int hole = left; // 找到第一个坑
    int key = a[left]; // 把第一个坑保存起来
    while (left < right) { // left==right 时跳出循环,最后一个坑
        while (left < right && a[right] >= key) // 从右开始往左找
            right--;
        a[hole] = a[right]; // 当 right--的循环跳出后,这个时候 right 对应的值小于 key,把当前 right 的值换到坑里
        hole = right; // right 变成新的坑
        while (left < right && a[left] <= key) // 从左开始往右找
            left++;
        a[hole] = a[left]; // 当 left++的循环跳出后,这个时候 left 对应的值大于 key,把当前 left 的值换到坑里
        hole = left; // left 变成新坑
    }
    a[hole] = key; // 大循环结束后 left=right
    return hole; // 这个新坑留给一开始的 key 值,返回新的基准值下标
}

挖坑法完毕。

挖坑法和 Hoare 版本的时间复杂度一样 n*logn 但是在特殊情况也会有不好的地方,在数据有序的时候或者数据全部相同时,时间复杂度还是会变成 O(N^2)

1.2.3 Lomuto 前后指针 快排

创建前后指针,从左往右找比基准值小的进行交换,使得小的都排在基准值的左边。 前后指针是我认为最好理解,也是代码最简单的一个。 就是定义一个 cur 指针向前走,一个 prev 指针在后面跟着,cur 找比基准值小的数据。

话不多说,上代码。

int QuickSort_Lomuto(int* a, int left, int right) {
    int prev = left; // 定义 prev cur 指针
    int key = left;
    int cur = left + 1;
    while (cur <= right) {
        if (a[cur] < a[key] && ++prev != cur) // 当 cur 对应的值小于 key 时,可以考虑将 prev 与 cur 对应的值交换
        {
            // 但如果这个时候,cur 刚好是 prev 的下一个值,是没有必要交换的
            // 所以要判断 prev++与 cur 是否相等
            swap(a[prev], a[cur]);
        }
        ++cur; // 每次循环 cur++一次
    }
    swap(a[key], a[prev]); // 循环结束之后,prev 对应的值是小于 key 的,prev 的下一个就是大于 key 的,这个时候调换 key 和 prev 的值
    return prev; // 找到新的基准值下标返回
}

仔细了解前后指针的流程,想必也会感觉到,当数据有序或者是全部相同时,前后指针也是 O(N^2) 的时间复杂度。 想想数据全部相同或者有序,其实也没有排序的需要了,除非是算法题卡了数据相同的样例。 所以快排的三种方法还是可行的。

快速排序特性总结:

  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(logn)

快排的基本内容就到这里啦。


二、归并排序

归并排序算法思想: 归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

底层核心还是递归,把一个大区间逐渐分成无数个小区间,(一个大问题分成无数个相同的子问题),快排和归并都是用到了递归,想想递归真的好用。

还有一个问题就是在递归到最后一层之后,怎么合并两个子区间让他们有序,这里我想到前面我们做过的习题,供参考。

话不多说上代码。

void MergeSort(int* arr, int left, int right, int* tmp) {
    // 把大区间分配数个小空间
    // 两个小空间 排序成一个空间 用 tmp 接受 返回赋值给原数组
    if (left >= right) // 递归出口
        return;
    int mid = left + (right - left) / 2; // 分成两个区间 采用二分
    MergeSort(arr, left, mid, tmp); // 左区间
    MergeSort(arr, mid + 1, right, tmp); // 右区间
    // 递归处理之后 现在就是合并两个子区间 使他们有序
    int begin1 = left, end1 = mid; // 第一个区间的 begin 和 end
    int begin2 = mid + 1, end2 = right; // 第二个区间的 begin 和 end
    int index = begin1; // 新的下标 对应 tmp 数组
    while (begin1 <= end1 && begin2 <= end2) // 合并两个数组的流程 不多赘述啦
    {
        if (arr[begin1] < arr[begin2]) {
            tmp[index++] = arr[begin1++];
        } else {
            tmp[index++] = arr[begin2++];
        }
    }
    while (begin1 <= end1) // 有数组没有全部传给 tmp 的情况
        tmp[index++] = arr[begin1++];
    while (begin2 <= end2)
        tmp[index++] = arr[begin2++];
    for (int i = left; i <= right; i++) // 赋值返回原数组
        arr[i] = tmp[i];
}

void MergeSortWrapper(int* arr, int n) {
    int* tmp = (int*)malloc(sizeof(int) * n); // 我们这里传一个新开的 tmp 数组空间进去,辅助合并两个子区间
    MergeSort(arr, 0, n - 1, tmp);
    free(tmp);
}

仔细回看,归并其实也不难,就是一个递归的处理,然后再合并两个区间而已。

实话实说,归并稳定,时间复杂度一直是 O(nlogn),不管数据是否有序是否相同。

归并排序特性总结:

  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(n)

目录

  1. 前言
  2. 一、交换排序
  3. 1.1 冒泡排序
  4. 1.2 快速排序
  5. 1.2.1 Hoare 版本 快排
  6. 1.2.2 挖坑法 快排
  7. 1.2.3 Lomuto 前后指针 快排
  8. 二、归并排序
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • 基于 LangChain 开发大模型 RAG 知识问答应用
  • FPGA 实现高速数字信号处理的核心技术与工程实践
  • Web 项目 UI 自动化测试实战:从零搭建博客系统测试框架
  • C++ 位运算实战:从基础操作到经典算法题解
  • Python Web 框架对比与实战:Django vs Flask vs FastAPI
  • 从 Prompt 到爆款短片:AI视频生成 10 分钟上手指南
  • MySQL 数据库基础核心知识点梳理
  • CC-Switch:AI 编码助手配置管理工具
  • LLM 大模型技术:入门、应用场景与行业机遇分析
  • 两个月从入门到独立进行漏洞挖掘的渗透测试指南
  • PyCharm 与 GitHub Copilot 配置指南:学生认证流程详解
  • 基于 Spring Cloud 的分布式智能推荐系统实现
  • GitHub 十大 Claude Skills 精选,实战提升开发效率
  • VLA 机器人革命:解析 10 篇关键视觉 - 语言 - 动作模型论文
  • EgoPoseFormer v2:AR/VR 第一视角人体动捕技术解析
  • iceoryx 附录:C++ 内存模型与原子操作详解
  • SkyWalking 与 Spring Cloud Alibaba 全链路追踪实战
  • OpenClaw 数字员工核心逻辑与架构全景解析
  • 前端响应式进阶:从 vw/vh 到 clamp() 的痛点与进化
  • 存储扇区分配表:NAND Flash 与 SD NAND 架构差异

相关免费在线工具

  • 加密/解密文本

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