- admin 的博客
归并排序 (Merge Sort)
- @ 2026-6-7 19:56:04
算法概述
归并排序(Merge Sort)是一种基于分治思想的稳定排序算法,由约翰·冯·诺依曼于 1945 年提出。它将数组递归地分成两半,分别排序,再将两个有序子数组合并成一个有序数组。
归并排序是外部排序(处理无法全部装入内存的大文件)的基石,也是许多稳定排序需求的默认选择。
核心思想
- 分(Divide):将当前数组从中间分成两个子数组,直到每个子数组只剩下一个元素(单个元素自然有序)。
- 治(Conquer):递归地对两个子数组进行归并排序。
- 合(Combine):将两个已排序的子数组合并(Merge)成一个有序数组。
归并排序的"归并"二字即来源于合并两个有序序列这一核心操作。
与快速排序的区别:
- 快速排序是"先分区,后递归"——分区操作在递归之前完成。
- 归并排序是"先递归到底,再回溯合并"——合并操作在递归返回时完成,是典型的后序遍历。
算法步骤 / 伪代码
合并两个有序子数组
function merge(arr, left, mid, right):
// 创建两个临时数组存放左右子数组
n1 = mid - left + 1
n2 = right - mid
L = arr[left .. mid]
R = arr[mid+1 .. right]
i = 0, j = 0, k = left
while i < n1 and j < n2:
if L[i] <= R[j]:
arr[k] = L[i]; i++
else:
arr[k] = R[j]; j++
k++
// 将剩余元素复制回原数组
while i < n1: arr[k++] = L[i++]
while j < n2: arr[k++] = R[j++]
整体递归流程
function mergeSort(arr, left, right):
if left < right:
mid = (left + right) / 2
mergeSort(arr, left, mid) // 排序左半部分
mergeSort(arr, mid + 1, right) // 排序右半部分
merge(arr, left, mid, right) // 合并两个有序部分
C++ 代码实现
#include <iostream>
#include <vector>
using namespace std;
// 合并两个已排序的子数组 arr[left..mid] 和 arr[mid+1..right]
void merge(vector<int>& arr, int left, int mid, int right) {
int n1 = mid - left + 1; // 左子数组长度
int n2 = right - mid; // 右子数组长度
// 创建临时数组
vector<int> L(n1), R(n2);
for (int i = 0; i < n1; ++i) L[i] = arr[left + i];
for (int j = 0; j < n2; ++j) R[j] = arr[mid + 1 + j];
// 合并两个有序数组
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k++] = L[i++];
} else {
arr[k++] = R[j++];
}
}
// 复制剩余元素
while (i < n1) arr[k++] = L[i++];
while (j < n2) arr[k++] = R[j++];
}
// 归并排序递归函数
void mergeSort(vector<int>& arr, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2; // 防止溢出
mergeSort(arr, left, mid); // 排序左半部分
mergeSort(arr, mid + 1, right); // 排序右半部分
merge(arr, left, mid, right); // 合并两个有序部分
}
}
// ============ 应用:计算数组中的逆序对(LeetCode 剑指 Offer 51)============
// 在归并过程中统计逆序对
long long mergeAndCount(vector<int>& arr, int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
vector<int> L(n1), R(n2);
for (int i = 0; i < n1; ++i) L[i] = arr[left + i];
for (int j = 0; j < n2; ++j) R[j] = arr[mid + 1 + j];
long long invCount = 0;
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k++] = L[i++];
} else {
// L[i] > R[j]:L 中从 i 到 n1-1 的所有元素都与 R[j] 构成逆序对
arr[k++] = R[j++];
invCount += (n1 - i);
}
}
while (i < n1) arr[k++] = L[i++];
while (j < n2) arr[k++] = R[j++];
return invCount;
}
// 归并排序的同时统计逆序对
long long mergeSortAndCount(vector<int>& arr, int left, int right) {
long long invCount = 0;
if (left < right) {
int mid = left + (right - left) / 2;
invCount += mergeSortAndCount(arr, left, mid);
invCount += mergeSortAndCount(arr, mid + 1, right);
invCount += mergeAndCount(arr, left, mid, right);
}
return invCount;
}
int reversePairs(vector<int>& nums) {
vector<int> temp = nums; // 复制一份,避免修改原数组
return (int)mergeSortAndCount(temp, 0, temp.size() - 1);
}
// ============ 主函数 ============
int main() {
// ========== 测试归并排序 ==========
vector<int> arr = {12, 11, 13, 5, 6, 7, 3, 9, 8, 1};
cout << "排序前: ";
for (int x : arr) cout << x << " ";
cout << endl;
mergeSort(arr, 0, arr.size() - 1);
cout << "排序后: ";
for (int x : arr) cout << x << " ";
cout << endl;
// ========== 测试逆序对(剑指 Offer 51)==========
vector<int> nums1 = {7, 5, 6, 4};
cout << "\n数组: ";
for (int x : nums1) cout << x << " ";
cout << endl;
cout << "逆序对数量: " << reversePairs(nums1) << " (期望: 5)" << endl;
vector<int> nums2 = {1, 3, 2, 3, 1};
cout << "数组: ";
for (int x : nums2) cout << x << " ";
cout << endl;
cout << "逆序对数量: " << reversePairs(nums2) << " (期望: 4)" << endl;
return 0;
}
复杂度分析
| 指标 | 所有情况 |
|---|---|
| 时间 | O(n log n) |
| 空间 | O(n) |
| 稳定性 | ✅ 稳定 |
-
时间复杂度:无论最好、最坏还是平均情况,归并排序的时间复杂度都是 O(n log n)。递归树共有 log n 层,每一层合并操作的代价为 O(n)。
-
空间复杂度:O(n)。合并时需要额外的临时数组来存放两个子数组,这是归并排序的主要缺点。不过也有一些原地归并的实现方式(如块归并),但通常会影响时间效率。
-
稳定性:归并排序是稳定的。在合并两个有序子数组时,如果两个元素相等,我们会优先取左边子数组的元素(
L[i] <= R[j]时取 L[i]),从而保持了相等元素的原始相对顺序。
应用场景
-
需要稳定排序的场景:当排序的对象有多个字段,需要先按一个字段排序再按另一个字段排序时,稳定性很重要。归并排序是许多语言中稳定排序的首选。
-
外部排序:当数据太大无法全部装入内存时,可以采用多路归并的方式对磁盘上的大文件进行排序。核心思想就是分段排序后反复归并。
-
链表排序:归并排序非常适合链表的排序,因为链表可以在 O(1) 时间内找到中间节点(快慢指针),且合并两个有序链表不需要额外空间。
-
求逆序对:归并排序在合并过程中可以优雅地统计逆序对数量(见经典例题),这是归并排序的经典应用。
-
计数问题:利用归并过程,还可以高效解决诸如"右侧小于当前元素的个数"(LeetCode 315)等问题。
经典例题
剑指 Offer 51 / LeetCode 面试题51 — 数组中的逆序对
题目描述:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
示例:
输入: [7,5,6,4]
输出: 5
解释: 逆序对为 (7,5), (7,6), (7,4), (5,4), (6,4)
归并排序解法思路:
- 利用归并排序的分治框架,在合并两个有序子数组时统计逆序对。
- 假设左右两个子数组 L 和 R 都已经有序。当合并时,如果发现
L[i] > R[j],由于 L 是有序的,L 中从i到末尾的所有元素都大于R[j],因此新增n1 - i个逆序对。 - 最终逆序对总数 = 左半部分的逆序对 + 右半部分的逆序对 + 合并过程中发现的跨部分逆序对。
(完整 C++ 代码已包含在上方的代码实现中)