归并排序是一种基于 "分治思想" 的排序算法,核心逻辑是 "将大问题拆解为小问题,解决小问题后合并结果",具有 稳定排序、时间复杂度稳定 等特点,广泛应用于大数据量排序场景(如外部排序)。
核心原理(分治思想)
1. 分(Divide):拆解问题
将待排序数组 递归拆解 为两个长度大致相等的子数组,直到每个子数组仅含 1 个元素。
2. 合(Merge):合并有序子数组
将两个 有序子数组 合并为一个大的有序数组,递归向上合并,最终得到完整的有序数组。
- 合并核心:需要一个临时数组,通过 "双指针" 比较两个子数组的元素,按从小到大顺序放入临时数组,最后将临时数组拷贝回原数组。
拆解(递归)→ 合并(递归回溯)→ 最终有序数组,本质是 "自顶向下" 的分治过程。
底层细节:为什么要这么设计?
1. 为什么需要临时数组?
-
若直接在原数组上合并,会覆盖未处理的元素(如合并
[5,9]和[1,4]时,原数组位置 0 的5会被1覆盖,导致后续5无法被处理); -
临时数组的作用是 "暂存有序结果",合并完成后再拷贝回原数组,保证数据不丢失。
2. 为什么是稳定排序?
-
稳定排序定义:排序后,相等元素的相对顺序不变 (如
[2, 1, 2]排序后仍为[1, 2, 2],两个2的原始顺序保留); -
归并排序稳定的关键:合并时用
arr[i] <= arr[j](而非<)。当左子数组元素等于右子数组元素时,优先选择左子数组的元素,确保相等元素的相对顺序不变。
3. 为什么递归终止条件是left >= right?
-
当
left == right时,子数组长度为 1(天然有序),无需拆分; -
当
left > right时,子数组为空(无元素可处理),直接返回。
完整实现(C 语言):递归版 + 非递归版
1.合并两个有序数组(Merge)
cpp
void Merge(int arr[], int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
int* brr = (int*)malloc(sizeof(int) * n1);
int* crr = (int*)malloc(sizeof(int) * n2);
for (int i = 0; i < n1; i++)brr[i] = arr[i + left];
for (int r = 0; r < n2; r++)crr[r] = arr[mid + 1 + r];
int i = 0, j = 0;
while (i < n1 && j < n2) {
if (brr[i] > crr[j]) {
arr[left++] = brr[i++];
}
else {
arr[left++] = crr[j++];
}
}
while (i < n1) {
arr[left++] = brr[i++];
}
while (j < n2) {
arr[left++] = crr[j++];
}
}
2.分治拆解问题(Divide)递归版
cpp
void Divide1(int arr[], int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
Divide1(arr, left, mid);
Divide1(arr, mid + 1, right);
Merge(arr, left, mid, right);
}
}
3.分治拆解问题(Divide)非递归版
cpp
//非递归实现
void Divide(int arr[], int left, int right) {
for (int i = 1; i < right - left + 1; i = i * 2) {
for (int r = left; r < right - left + 1-i ; r = r + i * 2) {
int mid = r + i-1;
int right1 = (r + i * 2-1) < (right - left + 1) ? (r + i * 2-1) : (right - left );
Merge(arr, r, mid, right1);
}
}
}
代码关键说明
-
临时数组优化:仅在入口函数申请 1 次临时数组,避免递归中频繁申请释放内存(提升效率);
-
稳定排序保证 :合并时用
arr[i] <= arr[j](而非<),确保相等元素的相对顺序不变; -
溢出避免 :
mid = left + (right - left)/2替代(left+right)/2,防止left+right超出整数范围; -
递归终止条件 :
left >= right(子数组长度为 1 或 0 时停止拆解)。
时间复杂度与空间复杂度
1. 时间复杂度
-
最好 / 最坏 / 平均时间复杂度 :
O(n log n)(分治过程的深度为log n,每层合并的总操作数为O(n),整体为n * log n); -
稳定性:归并排序是 稳定排序(相等元素的相对顺序不变),适合需要保持原始顺序的场景(如按成绩排序后,保持同成绩学生的录入顺序)。
2. 空间复杂度
-
递归实现:
O(n + log n)(n为临时数组空间,log n为递归调用栈空间); -
非递归实现:
O(n)(仅需临时数组空间,无递归栈开销)。
适用场景与注意事项
1. 适用场景
-
大数据量排序(如百万级、千万级数据);
-
需稳定排序的场景(如多字段排序:先按成绩排序,再按姓名排序);
-
外部排序(数据存储在磁盘,无法全部载入内存);
-
递归深度可控的场景(或用非递归版避免栈溢出)。
2. 注意事项
-
空间开销:需额外
O(n)临时数组,内存紧张场景需谨慎; -
小规模数据效率:递归版在小规模数据上不如插入排序、快速排序,需结合优化;
-
递归栈溢出:递归版在
n极大(如千万级)时可能栈溢出,需用非递归版。
总结
归并排序的核心优势是 时间复杂度稳定(O (n log n))、稳定排序 ,核心劣势是 空间开销较大(O (n))。其分治思想不仅适用于排序,还可延伸到其他问题(如逆序对计数、大规模数据合并)。