一、 核心思想:分而治之(Divide and Conquer)
归并排序是建立在分治法基础上的一种高效排序算法。所谓"分治",顾名思义就是"分而治之"。对于一个大规模的乱序数组,归并排序的解题思路分为三个标准的步骤:
-
分(Divide):将当前序列平均分成两个子序列,直到子序列的长度为1(长度为1的序列天然是有序的)。
-
治(Conquer):对这两个子序列分别进行递归排序。
-
合(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)实用。