归并排序(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)实用。

相关推荐
阿豪学编程2 小时前
LeetCode438: 字符串中所有字母异位词
算法·leetcode
Trouvaille ~2 小时前
【递归、搜索与回溯】专题(七):FloodFill 算法——勇往直前的洪水灌溉
c++·算法·leetcode·青少年编程·面试·蓝桥杯·递归搜索回溯
地平线开发者2 小时前
征程 6P codec decoder sample
算法·自动驾驶
地平线开发者2 小时前
征程 6X Camera 接入数据评估
算法·自动驾驶
Storynone2 小时前
【Day23】LeetCode:455. 分发饼干,376. 摆动序列,53. 最大子序和
python·算法·leetcode
小付同学呀3 小时前
C语言学习(八)——C判断(switch语句)
c语言·学习·算法
zhooyu3 小时前
二维坐标转三维坐标的实现原理
c++·3d·opengl
zhojiew3 小时前
为agent实现渐进式Skills能力的思考和实践
linux·python·算法
10Eugene3 小时前
C++/Qt自制八股文
java·开发语言·c++