算法学习—归并排序

本文最后更新于 2025年2月22日 下午

归并排序分治思想的运用

🤯基本思想:分治之美

将待排序元素分成大小大致相同的2个子集合,分别对2个子集合进行排序,最终将排好序的子集合合并成为所要求的排好序的集合。

归并排序(Merge Sort) 是分治思想的经典应用。其核心理念是:

  1. 分解:将复杂的大问题分割成简单的小问题

  2. 解决:逐步解决小问题

  3. 合并:将解决后的小问题结果组合成最终解

💡 核心口诀:分、治、合!

💻核心算法

✂️分治流程:

  1. 分割阶段:将待排序数组递归地分成两个大小近似的子数组

  2. 排序阶段:分别对这两个子数组进行排序

  3. 合并阶段:将已排序的子数组合并成一个有序数组

1
2
3
4
5
6
7
8
9
10
11
12
// 归并排序
void MergeSort(Type a[], int left, int right)
{
if (left<right) //至少有2个元素
{
int i=(left+right)/2; // 取中点
mergeSort(a, left, i); // 处理左半边
mergeSort(a, i+1, right); // 处理右半边
merge(a, b, left, i, right); // 合并到数组b
copy(a, b, left, right); // 复制回数组a
}
}

📽️过程演示

归并排序过程

⌛分步实现

  1. 确定分界点

1
2
// 确定分界点
int mid = (l + r) >> 1; // 求中点坐标
  • 归并排序每次都是分成大致相同的两部分,所以分界点是 12\dfrac {1}{2} 处。

💡 小技巧:使用右移运算符>>代替除法,计算更高效

  1. 递归处理子问题

1
2
3
// 递归处理子问题
mergeSort(a, l, mid); // 处理半左区间
mergeSort(a, mid + 1, r); // 处理半右区间
  • 分治法都是通过递归处理子问题。

💡 递归精髓:将大问题不断拆分,直到问题足够小(通常是1-2个元素)

  1. 合并子问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 合并子问题
int i = l, j = mid + 1;
int k = 0;
while (i <= mid && j <= r) // 直到有一半区间遍历完
{
if (a[i] <= a[j])
temp[k++] = a[i++];
else
temp[k++] = a[j++];
}

// 扫尾:将剩余的元素加到中间数组后
while (i <= mid)
temp[k++] = a[i++];
while (j <= r)
temp[k++] = a[j++];

💡 合并技巧:总是选择两个子数组中最小的元素

经过步骤2对左右区间进行递归处理后,左右两个子区间都是有序的,这里通过两个指针ij分别遍历左右子区间,比较当前元素大小,把较小的元素依次放入临时数组temp。最后,把左右子区间剩余的元素也都放入temp。故扫尾时直接将剩余的元素加到中间数组后

1
2
3
4
  // 将合并后的数组赋值给原数组
for (int i = l, j = 0; i <= r; ++i, ++j)
a[i] = temp[j];
}

注意:这里复制的赋值是从递归的起始位置l开始的,因为每次排序只是针对lr这个范围的区间进行的


⌨️完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <iostream>

using namespace std;

const int N = 100005; // 最大n的范围
int n;
int a[N], temp[N]; // 待排序数组和中间数组

void mergeSort(int a[], int l, int r)
{
// 递归结束条件:到达原子问题
if (l >= r)
return;
// 确定分界点
int mid = (l + r) >> 1;
// 递归处理子问题
mergeSort(a, l, mid);
mergeSort(a, mid + 1, r);

// 合并子问题
int i = l, j = mid + 1;
int k = 0;
while (i <= mid && j <= r)
{
if (a[i] <= a[j])
temp[k++] = a[i++];
else
temp[k++] = a[j++];
}

// 扫尾
while (i <= mid)
temp[k++] = a[i++];
while (j <= r)
temp[k++] = a[j++];

// 将合并后的数组赋值给原数组: 注意这里的赋值是从递归的起始位置l开始的,因为每次排序只是针对l到r这个范围的区间进行的
for (int i = l, j = 0; i <= r; ++i, ++j)
a[i] = temp[j];
}

int main()
{
cin >> n;

for (int i = 0; i < n; ++i)
cin >> a[i];

mergeSort(a, 0, n - 1); // 归并排序:从0开始,到n-1结束

for (int i = 0; i < n; ++i)
{
cout << a[i] << ' ';
}

return 0;
}

🚀 性能分析

  • 时间复杂度

最好/平均/最坏:O(nlogn)O(n log n)稳定且高效的排序算法

  • 空间复杂度

O(n)O(n):需要额外的临时数组空间

❓常见问题

  • 内存溢出
    如果数据量非常大,可能会因为临时数组占用太多内存导致内存溢出。解决办法可以考虑使用外部排序,或者优化临时数组的使用,比如复用临时数组。

  • 小数组性能
    在处理小数组时,归并排序的递归开销可能会使性能不如一些简单的排序算法,比如插入排序。可以在小数组时切换到插入排序来提高性能。

💡优化建议

  • 减少递归深度
    在数据量较小时,使用迭代的方式代替递归,减少栈空间的使用。

  • 优化合并操作
    在合并时,可以提前判断是否左半部分已经全部小于右半部分,如果是,就可以直接跳过合并操作,提高效率。


算法学习—归并排序
https://huazzi.me/posts/2128098402/
作者
Huazzi
发布于
2025年1月25日
许可协议