【算法学习】分治法应用—归并排序

归并排序分治思想的运用。

🤯基本思想:分治之美

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

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

  1. 分解:将复杂的大问题分割成简单的小问题
  2. 解决:逐步解决小问题
  3. 合并:将解决后的小问题结果组合成最终解

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

💻核心算法

✂️分治流程:

  1. 分割阶段:将待排序数组递归地分成两个大小近似的子数组
  2. 排序阶段:分别对这两个子数组进行排序
  3. 合并阶段:将已排序的子数组合并成一个有序数组
// 归并排序
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. 确定分界点
// 确定分界点
  int mid = (l + r) >> 1; // 求中点坐标
  • 归并排序每次都是分成大致相同的两部分,所以分界点是 1 2 \dfrac {1}{2} 21 处。

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

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

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

  1. 合并子问题
 // 合并子问题
  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。
扫尾时直接将剩余的元素加到中间数组后

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

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


⌨️完整代码

#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 ( n l o g n ) O(n log n) O(nlogn)
稳定且高效的排序算法

  • 空间复杂度

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

❓常见问题

  • 内存溢出
    如果数据量非常大,可能会因为临时数组占用太多内存导致内存溢出。解决办法可以考虑使用外部排序,或者优化临时数组的使用,比如复用临时数组。
  • 小数组性能
    在处理小数组时,归并排序的递归开销可能会使性能不如一些简单的排序算法,比如插入排序。可以在小数组时切换到插入排序来提高性能。

💡优化建议

  • 减少递归深度
    在数据量较小时,使用迭代的方式代替递归,减少栈空间的使用。
  • 优化合并操作
    在合并时,可以提前判断是否左半部分已经全部小于右半部分,如果是,就可以直接跳过合并操作,提高效率。

这样,我们就完成了对归并排序的学习啦!希望大家都能掌握这个强大的算法。
如果在学习过程中有任何问题,欢迎在评论区留言交流哦💬!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Huazzi_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值