归并排序(Merge Sort)原理与实现

一、 核心思想:分而治之(Divide and Conquer)

归并排序是建立在分治法基础上的一种高效排序算法。所谓"分治",顾名思义就是"分而治之"。对于一个大规模的乱序数组,归并排序的解题思路分为三个标准的步骤:

  1. 分(Divide):将当前序列平均分成两个子序列,直到子序列的长度为1(长度为1的序列天然是有序的)。

  2. 治(Conquer):对这两个子序列分别进行递归排序。

  3. 合(Combine):将两个已经排好序的子序列,合并成一个最终的有序序列。

我们可以把它想象成公司里的一个大项目:老板(主函数)把项目分成两半交给两个总监,总监再往下分给经理,直到分给每个基层的员工。员工把自己的小任务做好(有序)后,再一层层往上汇报合并,最终完成整个大项目。

二、 算法拆解与图解步骤

假设我们有一个数组 [38, 27, 43, 3, 9, 82, 10]

1. "分"的过程

算法会不断地从中间将数组切开:

  • 第一层切分:[38, 27, 43, 3][9, 82, 10]

  • 第二层切分:[38, 27][43, 3][9, 82][10]

  • 第三层切分:[38], [27], [43], [3], [9], [82], [10] 此时,每个子数组只有一个元素,拆分完成。

2. "合"的过程(合并两个有序数组)

这是归并排序的核心所在。我们需要准备一个临时数组(辅助空间),然后用两个指针分别指向两个待合并的子数组的起始位置:

  • 比较两个指针指向的元素,将较小的元素放入临时数组,并将该指针后移一位。

  • 重复上述过程,直到其中一个数组被遍历完。

  • 将另一个数组剩下的元素直接追加到临时数组的末尾。

  • 最后,将临时数组中的元素拷贝回原数组。

合并的轨迹如下:

  • [38][27]合并为 [27, 38][43][3]合并为 [3, 43]...

  • [27, 38][3, 43]合并为 [3, 27, 38, 43]...

  • 最终左右两半合并为:[3, 9, 10, 27, 38, 43, 82]

三、 代码实现(C++版)

cpp 复制代码
#include <iostream>
#include <vector>

using namespace std;

// 合并函数
void merge(vector<int>& arr, int left, int mid, int right) {
    // 创建一个临时数组(借助 vector),大小为合并后数组的总长度
    vector<int> help(right - left + 1);
    int i = 0;          // 临时数组的索引
    int p1 = left;      // 左半部分数组的指针
    int p2 = mid + 1;   // 右半部分数组的指针

    // 比较两个子数组的元素,谁小就把谁放入临时数组
    while (p1 <= mid && p2 <= right) {
        help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
    }

    // 如果左边还有剩余,直接拷贝
    while (p1 <= mid) {
        help[i++] = arr[p1++];
    }

    // 如果右边还有剩余,直接拷贝
    while (p2 <= right) {
        help[i++] = arr[p2++];
    }

    // 将排好序的临时 vector 拷贝回原 vector
    for (i = 0; i < help.size(); i++) {
        arr[left + i] = help[i];
    }
}

// 递归分治函数
void process(vector<int>& arr, int left, int right) {
    // base case:当分解到只有一个元素时,直接返回
    if (left == right) {
        return;
    }
    
    // 计算中间位置,防止溢出,等同于 (left + right) / 2
    int mid = left + ((right - left) >> 1);
    
    // 1. 递归排序左半部分
    process(arr, left, mid);
    // 2. 递归排序右半部分
    process(arr, mid + 1, right);
    // 3. 合并左右两部分
    merge(arr, left, mid, right);
}

// 主函数:调用归并排序
void mergeSort(vector<int>& arr) {
    if (arr.empty() || arr.size() < 2) {
        return;
    }
    process(arr, 0, arr.size() - 1);
}

// 测试一下
int main() {
    vector<int> arr = {38, 27, 43, 3, 9, 82, 10};
    
    mergeSort(arr);
    
    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;
    // 输出: 3 9 10 27 38 43 82 
    
    return 0;
}

四、 复杂度与特性分析

特性 描述
时间复杂度 最好、最坏、平均均为 O(n \log n)。树的深度为 \\log_2 n,每一层合并的时间复杂度为 O(n)。
空间复杂度 O(n) 。在合并过程中需要创建一个与原数组大小相同的临时数组 help
稳定性 稳定 。在合并过程中,当 arr[p1] == arr[p2] 时,我们优先拷贝左边的元素(arr[p1] <= arr[p2]),保证了相同元素的相对位置不变。

优缺点总结

  • 优点:性能非常稳定,不受输入数据初始状态的影响;是稳定的排序算法。

  • 缺点:不是原地排序算法(In-place),需要额外的内存空间 O(n),在内存极其受限的场景下不如快速排序(Quick Sort)实用。

相关推荐
运行时记录5 小时前
别再手动写提示词了 — SkillOpt 让技能文档自己进化
算法
起床困难户5755 小时前
条款20:协助完成返回值优化
c++
啦啦啦啦啦zzzz5 小时前
算法总结(二分查找、双指针)
c++·算法
qq_8573058195 小时前
python语法
开发语言·python·算法
DXM05216 小时前
第9期|从机器学习到深度学习:AI遥感解译的进化逻辑
人工智能·算法·计算机视觉
小蒋学算法6 小时前
算法-阶乘函数后K个零
算法
weixin_307779136 小时前
智能模拟数据生成平台:生成式AI合成数据技术重塑开发测试效能
人工智能·测试工具·算法·测试用例
Darling噜啦啦6 小时前
JavaScript 数组深度解析:从纯函数到二维数组陷阱,一文吃透前端数据结构核心
前端·javascript·数据结构
不负岁月无痕6 小时前
C++ 模板核心内容与高频面试题汇总
java·开发语言·c++
无限进步_7 小时前
从零实现一个迷你Shell——深入理解Linux命令行解释器
linux·运维·服务器·开发语言·c++·chrome