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

数据结构:归并排序与分治思想详解

归并排序作为分治法的经典应用,通过递归或迭代方式将数组不断二分直至单元素,再合并有序子数组。该算法时间复杂度稳定在 O(n log n),空间复杂度为 O(n),且具备稳定性,适合处理海量数据及外部排序场景。代码实现需注意区间划分避免死循环及边界条件处理。

深海蔚蓝发布于 2026/3/25更新于 2026/10/2488 浏览
数据结构:归并排序与分治思想详解

归并排序:分治思想的完美演绎

基本思想

归并排序(Merge Sort)是分治法(Divide and Conquer)的经典应用,由计算机科学先驱约翰·冯·诺依曼于 1945 年提出。其核心逻辑非常直观:将大问题分解为小问题,解决小问题后合并结果。

算法流程主要包含两个阶段:

  1. 分(Divide):递归地将当前数组分割成两个子数组,直到每个子数组只包含一个元素(此时天然有序)。
  2. 治(Conquer):将两个有序子数组合并成一个新的有序数组。

归并的核心操作是合并两个有序数组。简单来说,就是每次比较两个数组的首元素,将较小者放入新数组,直到所有元素合并完成。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 辅助函数:合并两个有序区间 [begin, mid] 和 [mid+1, end]
void _MergeSort(int* a, int* tmp, int begin, int end) {
    // 递归终止条件:当子数组只有一个元素时,无需再分
    if (begin == end) {
        return;
    }

    // 计算中间点
    int mid = (begin + end) / 2;

    // 注意:这里区间分为 [begin, mid] 和 [mid+1, end]
    // 如果分为 [begin, mid-1] 和 [mid, end] 会出现死循环或越界问题
    _MergeSort(a, tmp, begin, mid);
    _MergeSort(a, tmp, mid + 1, end);

    // 开始归并
    int begin1 = begin, end1 = mid;
    int begin2 = mid + 1, end2 = end;
    int i = begin;

    // 只要两边都有元素,就继续比较
    while (begin1 <= end1 && begin2 <= end2) {
        if (a[begin1] < a[begin2]) {
            tmp[i++] = a[begin1++];
        } else {
            tmp[i++] = a[begin2++];
        }
    }

    // 处理剩余元素:其中一个区间结束后,将另一个区间的剩余元素全部拷贝
     (begin1 <= end1) {
        tmp[i++] = a[begin1++];
    }
     (begin2 <= end2) {
        tmp[i++] = a[begin2++];
    }

    
    (a + begin, tmp + begin, (end - begin + ) * ());
}


  {
    
    * tmp = (*)(() * n);
     (tmp == ) {
        perror();
        ;
    }

    
    _MergeSort(a, tmp, , n - );

    
    (tmp);
    tmp = ;
}


  {
     a[] = {, , , , , };
    MergeSort(a, );
     ( i = ; i < (a) / (a[]); i++) {
        (, a[i]);
    }
     ;
}
while
while
// 将合并结果复制回原数组
memcpy
1
sizeof
int
// 归并排序入口函数
void
MergeSort
(int* a, int n)
// 分配临时数组空间
int
int
malloc
sizeof
int
if
NULL
"malloc failed"
return
// 调用递归函数
0
1
// 释放临时数组
free
NULL
// 测试案例
int
main
()
int
2
3
5
4
7
1
6
for
int
0
sizeof
sizeof
0
printf
"%d "
return
0

为什么区间划分要注意?

在递归拆分时,我们通常使用 [begin, mid] 和 [mid+1, end]。如果错误地写成 [begin, mid-1] 和 [mid, end],在某些情况下会导致死循环。

假设数组有 10 个数据,下标从 0 到 9。如果取 mid = 4,按错误写法分成 [0, 3] 和 [4, 9]。在 [0, 3] 中 mid-1=1,分成 [0, 1] 和 [2, 3]。看似没问题,但在某些边界条件下,比如 [2, 3] 再次拆分时,若 mid 计算导致区间重叠或无法收敛,就会出问题。最稳妥的方式始终是让中间点归属左半部分,右半部分从 mid+1 开始。

操作时间复杂度说明
分解数组O(log n)递归深度
合并子数组O(n)每层需要遍历所有元素
总复杂度O(n log n)稳定的高效率排序
空间复杂度

归并排序需要 O(n) 的额外空间来存储合并过程中的临时数组。虽然递归调用栈占用 O(log n),但通常临时数组的空间需求占主导地位。

非递归实现

除了递归,归并排序也可以用迭代方式实现。这可以理解为:先进行 1+1 归并,再进行 2+2 归并,以此类推。

void MergeSortNonR(int* a, int n) {
    int* tmp = (int*)malloc(sizeof(int) * n);
    if (tmp == NULL) {
        perror("malloc failed");
        return;
    }

    // gap 是当前归并子数组的大小,从 1 开始,每次翻倍
    int gap = 1;
    for (int i = 0; i < n; i += 2 * gap) {
        int begin1 = i, end1 = i + gap - 1;
        int begin2 = i + gap, end2 = i + 2 * gap - 1;

        // 第二组完全越界,跳过本次合并
        if (begin2 >= n) {
            break;
        }

        // 第二组的 begin2 没越界,但 end2 越界了,需要修正边界
        if (end2 >= n) {
            end2 = n - 1;
        }

        int j = i;
        while (begin1 <= end1 && begin2 <= end2) {
            if (a[begin1] < a[begin2]) {
                tmp[j++] = a[begin1++];
            } else {
                tmp[j++] = a[begin2++];
            }
        }

        while (begin1 <= end1) {
            tmp[j++] = a[begin1++];
        }
        while (begin2 <= end2) {
            tmp[j++] = a[begin2++];
        }

        // 将合并结果复制回原数组
        memcpy(a + i, tmp + i, (end2 - i + 1) * sizeof(int));
    }
    free(tmp);
    tmp = NULL;
}

非递归实现要点

  1. gap 参数:控制当前归并的子数组大小,初始为 1,每次迭代翻倍。
  2. 边界处理:这是最容易出错的地方。数组长度往往不是 2 的幂次方,必须处理以下两种情况:
    • 当第二个子数组完全越界(begin2 >= n),说明没有可合并的数据,直接跳出循环。
    • 当第二个子数组部分越界(end2 >= n),将其右边界修正为 n - 1。
  3. 迭代过程:
    • 第 1 轮:1+1 归并 → 每个子数组大小为 1
    • 第 2 轮:2+2 归并 → 子数组大小为 2
    • 第 k 轮:2^k + 2^k 归并 → 子数组大小为 2^k

每一层的时间复杂度都是 O(n),一共 log n 层,所以总时间复杂度依然是 O(n log n)。空间复杂度同样是 O(n)。

归并排序特性总结

  1. 稳定排序:
    • 当两个元素相等时,归并排序优先选择左子数组的元素。
    • 保持相等元素的原始相对位置不变。
    • 适用于对稳定性有要求的场景(如多关键字排序)。
  2. 时间复杂度:
    • 最优、平均、最差均为 O(n log n)。
    • 不受输入数据影响,性能非常稳定。
  3. 空间复杂度:
    • O(n) 额外空间。
    • 递归实现还需 O(log n) 的栈空间。
  4. 外排序优势:
    • 归并排序是少数能高效处理外部存储(如硬盘)数据的排序算法。
    • 特别适合处理超过内存容量的海量数据。
    • 工作流程:将大文件分割为能放入内存的小块 -> 内存中排序每个小块 -> 合并已排序的小块。

结语

归并排序是分治思想的完美体现,通过'分而治之'的策略达到 O(n log n) 的高效排序。尽管它需要 O(n) 的额外空间,但在现代计算机系统中,这种空间换时间的策略往往是值得的。

它的核心优势在于稳定可靠、性能不受数据分布影响,以及强大的外排序能力。这使得它在数据库系统、大数据处理等场景中发挥着不可替代的作用,是每个程序员必须掌握的经典算法之一。

目录

  1. 归并排序:分治思想的完美演绎
  2. 基本思想
  3. 为什么区间划分要注意?
  4. 空间复杂度
  5. 非递归实现
  6. 非递归实现要点
  7. 归并排序特性总结
  8. 结语

更多推荐文章

查看全部
  • C++ 核心语法解析:引用、内联函数与 nullptr
  • Python 虚拟环境搭建与 PyCharm 配置实战
  • AI 魔术师:基于视觉的增强现实特效
  • Stable Diffusion 镜像交付标准:Dockerfile 透明与构建可追溯
  • AI 大模型开发必备书籍推荐:从入门到实战
  • 深度视觉赋能足式机器人极限运动与跑酷技术解析
  • 基于多版本YOLO与SpringBoot的实时跌倒检测系统
  • Java 核心面试高频考点与解析
  • C++ STL 容器详解:map 与 set 原理及实战
  • C++与Linux基础:虚拟文件系统 VFS 详解
  • WebView 并发初始化竞争风险分析
  • Python Web UI 自动化测试:推送本地代码到 Git 远程仓库
  • MacOS OpenClaw 安装指南及常见问题解决方案
  • 大模型现状及行业落地趋势分析
  • 微调阿里 Qwen2.5 大模型:详细步骤与技巧
  • LLaMA Factory 训练可视化管理:Loss 曲线解析与性能优化
  • 宇树 G1 机器人强化学习训练环境搭建与奖励函数解析
  • Java static 关键字:静态与非静态成员访问规则详解
  • Python 数据分析神器:ydata_profiling 模块详解
  • MiroFish:基于多智能体技术的开源 AI 推演预测引擎

相关免费在线工具

  • 加密/解密文本

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