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

相关推荐
聪明蛋子哟6 分钟前
不仅仅是向量检索:结合知识图谱与Tool Calling的混合增强生成(Hybrid RAG)方案
人工智能·算法·知识图谱
Pniubi6 分钟前
力扣55跳跃游戏(贪心)
算法·leetcode·游戏
三月微暖寻春笋27 分钟前
【和春笋一起学C++】(七十二)类模板
开发语言·c++·实例·类模板·使用类模板·数组模板
xlq2232228 分钟前
全面复习4
c++
jyyyx的算法博客37 分钟前
传球游戏【思维】
算法
FL16238631291 小时前
关于2026年南昌大学811信号与系统最后一题电路系统解题思路
算法
殷色玫瑰1 小时前
C++ STL:map 与 set 详解——从底层红黑树到实际应用
c语言·数据结构·c++·算法·visualstudio·rpc
行者全栈架构师1 小时前
【鸿蒙心迹】鸿蒙网络请求架构实战——@ohos.net.http 到 Axios 封装、拦截器与统一错误处理(HarmonyOS 7.x)
前端·算法·架构
Tri_Function1 小时前
背包:从入门到实战
c++
j7~1 小时前
【C++ 标准项目】发布订阅消息队列 (篇七):项目创建与基础工具模块搭建
开发语言·c++·log4j·uuid·protobuf·helper工具·消息定义类型和交换机类型定义