引言
前面我们学习了快速排序和堆排序两种 O(n log n) 的排序算法。今天要讲的归并排序 ,同样是 O(n log n) 级别,但它有一个独特的优势------稳定。
归并排序是分治思想最经典的体现:把一个大问题拆成两个小问题,分别解决后再合并。它的时间复杂度始终是 O(n log n),没有快排的最坏退化问题,因此在需要稳定排序的场景中(如对象按多个条件排序)是不可替代的选择。

第一部分:算法思想
一、合并两个有序数组
归并排序的核心是 Merge 操作:把两个已经有序的数组合并成一个有序数组。

合并有三种情况:
| 情况 | 处理 |
|---|---|
| 两边都还有元素 | 比较 arr[i] 和 arr[j],取较小的 |
左边空了(i > mid) |
直接取右边的 |
右边空了(j > right) |
直接取左边的 |
二、合并代码
void Merge(int arr[], int left, int mid, int right) {
// 申请临时数组
int* brr = (int*)malloc(sizeof(int) * (right - left + 1));
if (brr == NULL) return;
int i = left; // 左半指针
int j = mid + 1; // 右半指针
int k = 0; // 临时数组指针
while (i <= mid || j <= right) {
// 情况1:左边空了 → 直接取右边
if (i > mid) {
brr[k++] = arr[j++];
}
// 情况2:右边空了 → 直接取左边
else if (j > right) {
brr[k++] = arr[i++];
}
// 情况3:两边都有 → 比较取小的(等号保证稳定性)
else if (arr[i] <= arr[j]) {
brr[k++] = arr[i++];
}
else {
brr[k++] = arr[j++];
}
}
// 把临时数组拷贝回原数组
for (int t = left; t <= right; t++) {
arr[t] = brr[t - left];
}
free(brr);
}
注意 :比较条件是 arr[i] <= arr[j] 而不是 <。等号保证左边等于右边时优先取左边 ,这是归并排序保持稳定的关键。
第二部分:完整实现
一、递归拆分
void Dvide(int arr[], int left, int right) {
if (left < right) {
int mid = (left + right) / 2;
Dvide(arr, left, mid); // 左半递归
Dvide(arr, mid + 1, right); // 右半递归
Merge(arr, left, mid, right); // 合并左右
}
}
void Merge_Sort(int arr[], int len) {
Dvide(arr, 0, len - 1);
}
二、完整排序过程

第三部分:完整测试代码
cpp
#include <stdio.h>
#include <stdlib.h>
void Merge(int arr[], int left, int mid, int right) {
int* brr = (int*)malloc(sizeof(int) * (right - left + 1));
if (brr == NULL) return;
int i = left, j = mid + 1, k = 0;
while (i <= mid || j <= right) {
if (i > mid)
brr[k++] = arr[j++];
else if (j > right)
brr[k++] = arr[i++];
else if (arr[i] <= arr[j])
brr[k++] = arr[i++];
else
brr[k++] = arr[j++];
}
for (int t = left; t <= right; t++) {
arr[t] = brr[t - left];
}
free(brr);
}
void Dvide(int arr[], int left, int right) {
if (left < right) {
int mid = (left + right) / 2;
Dvide(arr, left, mid);
Dvide(arr, mid + 1, right);
Merge(arr, left, mid, right);
}
}
void Merge_Sort(int arr[], int len) {
Dvide(arr, 0, len - 1);
}
void printArray(int arr[], int len, const char* msg) {
printf("%s", msg);
for (int i = 0; i < len; i++) printf("%d ", arr[i]);
printf("\n");
}
int main() {
int arr1[] = {8, 3, 6, 1, 7, 2, 5, 4};
int len1 = sizeof(arr1) / sizeof(arr1[0]);
printArray(arr1, len1, "排序前:");
Merge_Sort(arr1, len1);
printArray(arr1, len1, "排序后:");
int arr2[] = {8, 7, 6, 5, 4, 3, 2, 1};
int len2 = sizeof(arr2) / sizeof(arr2[0]);
printf("\n逆序测试:\n");
printArray(arr2, len2, "排序前:");
Merge_Sort(arr2, len2);
printArray(arr2, len2, "排序后:");
int arr3[] = {5, 2, 8, 2, 9, 1, 5};
int len3 = sizeof(arr3) / sizeof(arr3[0]);
printf("\n重复元素测试(稳定性验证):\n");
printArray(arr3, len3, "排序前:");
Merge_Sort(arr3, len3);
printArray(arr3, len3, "排序后:");
return 0;
}
第四部分:算法分析
一、时间复杂度
| 情况 | 时间复杂度 | 说明 |
|---|---|---|
| 最好 | O(n log n) | 始终如此 |
| 最坏 | O(n log n) | 始终如此 |
| 平均 | O(n log n) | 始终如此 |
推导:

二、空间复杂度
O(n),需要临时数组存放合并结果。这是归并排序的主要缺点------不像快排和堆排是原地排序。
三、稳定性
归并排序是稳定的 。因为合并时 arr[i] <= arr[j] 优先取左边的,相等元素保持原有顺序。
第五部分:O(n log n) 排序终极对比
| 对比项 | 快速排序 | 堆排序 | 归并排序 |
|---|---|---|---|
| 平均 | O(n log n) | O(n log n) | O(n log n) |
| 最坏 | O(n²) | O(n log n) | O(n log n) |
| 空间 | O(log n) | O(1) | O(n) |
| 稳定性 | ❌ | ❌ | ✅ |
| 实际速度 | ⭐⭐⭐ | ⭐⭐ | ⭐⭐ |
| 适用场景 | 通用首选 | 内存受限 | 需要稳定 |
一句话选择指南:
| 场景 | 选谁 |
|---|---|
| 通用排序 | 快速排序(三数取中优化) |
| 内存极度受限 | 堆排序 |
| 需要稳定排序 | 归并排序 |
| 链表排序 | 归并排序(天然适合) |
| 外部排序(大文件) | 归并排序 |
第六部分:所有排序算法总结
| 排序算法 | 平均 | 最坏 | 空间 | 稳定 | 文章 |
|---|---|---|---|---|---|
| 冒泡排序 | O(n²) | O(n²) | O(1) | ✅ | 第一篇 |
| 选择排序 | O(n²) | O(n²) | O(1) | ❌ | 第二篇 |
| 插入排序 | O(n²) | O(n²) | O(1) | ✅ | 第三篇 |
| 基数排序 | O(d×n) | O(d×n) | O(n+r) | ✅ | 第四篇 |
| 快速排序 | O(n log n) | O(n²) | O(log n) | ❌ | 第五篇 |
| 希尔排序 | O(n^1.3) | O(n²) | O(1) | ❌ | 第六篇 |
| 堆排序 | O(n log n) | O(n log n) | O(1) | ❌ | 第七篇 |
| 归并排序 | O(n log n) | O(n log n) | O(n) | ✅ | 本篇 |
总结
一、核心要点
| 要点 | 内容 |
|---|---|
| 算法思想 | 分治:拆分到底 → 合并两个有序数组 |
| 时间复杂度 | 始终 O(n log n) |
| 空间复杂度 | O(n)(需要临时数组) |
| 稳定性 | ✅ 稳定(<= 优先取左边) |
| 核心操作 | Merge:比较两个有序数组,依次取小的 |
二、代码框架记忆

三、一句话记忆
归并排序先递归拆分成单个元素,再两两合并有序数组。合并时等号优先取左边保证稳定。时间复杂度始终 O(n log n),但需要 O(n) 额外空间,是链表排序和外部排序的首选。