手撕排序算法4:归并排序及其非递归版本(上)

简介: 手撕排序算法4:归并排序及其非递归版本(上)

一.归并排序

1.算法思想

归并排序是一个不太好理解的排序算法,

我们先看一下一张图片,了解一下归并排序的整体思想

不过归并的假设前提是:左区间有序,右区间有序

而给定一个数组,该怎么才能让它左半区间有序,右半区间也有序呢?

> 我们可以类比一下快排的递归版本的思想
> 在那里,我们成功使得key前面的数字都小于key,key后面的数字都大于key
> 那么我们当时在想:怎么才能让整个数组都有序呢?
> 只要让它的左半区间有序,右半区间有序,那么整体不久有序了吗?
> 所以我们进行递归调用,不断缩小区间长度
> 最坏的情况下,当某个对应区间的长度等于1时,该区间必定有序

所以我们类比得出,我们可以采用递归调用的方式,来不断缩小区间长度,最后进行多次归并得到有序序列

下面我们看一下归并排序整体的实现逻辑

但是我们注意到当我们进行多次的归并时,如果我们每个数组都去动态开辟出来(malloc),那开辟的是不是太多了呢?

所以我们这里只开辟了一个临时数组,那么怎么做到的呢?我们接下来来看一下

因为我们需要开辟临时数组,所以我们会有空间复杂度的消耗

临时数组需要能够存放下原始数组的所有数据

所以临时数组的长度需要跟原数组相同

所以空间复杂度为O(N)

2.代码实现

void _MergeSort(int* a, int left, int right,int* tmp)//一般某个主函数的子函数在前面加上_
{
  if (left >= right)//区间长度为1,则必然有序
  {
    return;
  }
  int mid = (left + right) >> 1;
  //等价于int mid = (left+right)/2;
  //假设[left,mid]  [mid+1,right] 有序,那么我们就可以进行归并了
  //那么如何让左右区间均有序呢?
  //类比递归版本的快排和二叉树的深度优先遍历
  //我们可以这样
  _MergeSort(a, left, mid, tmp);
  _MergeSort(a, mid + 1, right, tmp);
  //此时该小区间已经有序了,那么下面开始归并,也就是将有序序列存放到临时数组中
  int begin1 = left, end1 = mid;
  int begin2 = mid + 1, end2 = right;
  int index = left;
  while (begin1 <= end1 && begin2 <= end2)//左右区间有一个结束那么就结束循环
  {
    if (a[begin1] < a[begin2])
    {
      tmp[index++] = a[begin1++];
    }
    else
    {
      tmp[index++] = a[begin2++];//a[begin1]和a[begin2]相等的时候,放谁进入临时数组都可以
    }
  }
  //虽然我们在这里写了两个循环,但是只会进入其中一个
  while (begin1 <= end1)
  {
    tmp[index++] = a[begin1++];
  }
  while (begin2 <= end2)
  {
    tmp[index++] = a[begin2++];
  }
  //拷贝回去
  int i = 0;
  for (i = left; i <= right; i++)
  {
    a[i] = tmp[i];
  }
  //则该小区间已经有序了,对该小区间的递归调用结束
  //每递归到最后时,区间长度为1,已经有序,无需归并,直接return返回即可
  //当递归开始返回时,随着区间长度的增大,该小区间开始进行归并与拷贝,将该小区间变为有序区间
  //随着递归调用的返回,有序区间的长度开始逐渐增大,直到整个数组均有序为止
}
void MergeSort(int* a, int n)
{
  int* tmp = (int*)malloc(sizeof(int) * n);//tmp:临时数组
  _MergeSort(a, 0, n - 1, tmp);
  free(tmp);
}

1.每递归到最后时,区间长度为1,已经有序,无需归并,直接return返回即可

2.当递归开始返回时,随着区间长度的增大,该小区间开始进行归并与拷贝,将该小区间变为有序区间

3.随着递归调用的返回,有序区间的长度开始逐渐增大,直到整个数组均有序为止

4.可以类比递归版本的快排和二叉树的深度优先遍历,

思想上类似于二叉树的后序遍历:

左,右,根, 左,右,根, 左,右,根, 左,右,根,左,右,根,…

//这里是:

左,右,归并,左,右,归并,左,右,归并,左,右,归并,左,右,归并…

回来后还要进行排序

而快排类似于:

先序遍历:

根,左,右,根,左,右,根,左,右,根,左,右,根,左,右

回来后就已经有序了

void _MergeSort(int* a, int left, int right, int* tmp)
{
  if (left >= right)
  {
    return;
  }
  int mid = (left + right) >> 1;
  _MergeSort(a, left, mid, tmp);
  _MergeSort(a, mid + 1, right, tmp);
  //开始归并
}

下面是递归调用图,其中当区间长度减为1时,直接返回即可,无需进行归并,其余长度的区间都必须进行归并

3.时间复杂度和稳定性

归并排序(递归跟非递归)的时间复杂度是O(N*log(2)N),

稳定性:稳定

因为它是采取分割为最小区间再重新组合的思想

因为归并的时候

当左区间和右区间的值相等时

我们只要控制让左区间的值复制到临时数组中即可

相关文章
|
7天前
|
搜索推荐 算法 Java
Java数据结构与算法:排序算法之归并排序
Java数据结构与算法:排序算法之归并排序
|
14天前
|
存储 算法 程序员
数据结构与算法===递归
数据结构与算法===递归
|
2天前
|
算法 搜索推荐 C#
|
13天前
|
搜索推荐 算法 Java
Java中的快速排序、归并排序和堆排序是常见的排序算法。
【6月更文挑战第21天】Java中的快速排序、归并排序和堆排序是常见的排序算法。快速排序采用分治,以基准元素划分数组并递归排序;归并排序同样分治,先分割再合并有序子数组;堆排序通过构建堆来排序,保持堆性质并交换堆顶元素。每种算法各有优劣:快排平均高效,最坏O(n²);归并稳定O(n log n)但需额外空间;堆排序O(n log n)且原地排序,但不稳定。
19 3
|
10天前
|
搜索推荐 C语言
【C/排序算法】:快速排序和归并排序的非递归实现
【C/排序算法】:快速排序和归并排序的非递归实现
10 0
|
10天前
|
搜索推荐 算法
【C/排序算法】:归并排序和计数排序
【C/排序算法】:归并排序和计数排序
9 0
|
10天前
|
搜索推荐
归并排序算法总结
归并排序算法总结
|
5天前
|
算法 安全 数据库
基于结点电压法的配电网状态估计算法matlab仿真
**摘要** 该程序实现了基于结点电压法的配电网状态估计算法,旨在提升数据的准确性和可靠性。在MATLAB2022a中运行,显示了状态估计过程中的电压和相位估计值,以及误差随迭代变化的图表。算法通过迭代计算雅可比矩阵,结合基尔霍夫定律解决线性方程组,估算网络节点电压。状态估计过程中应用了高斯-牛顿或莱文贝格-马夸尔特法,处理量测数据并考虑约束条件,以提高估计精度。程序结果以图形形式展示电压幅值和角度估计的比较,以及估计误差的演变,体现了算法在处理配电网状态估计问题的有效性。
|
1天前
|
数据采集 存储 算法
基于BP算法的SAR成像matlab仿真
**摘要:** 基于BP算法的SAR成像研究,利用MATLAB2022a进行仿真。SAR系统借助相对运动合成大孔径,提供高分辨率图像。BP算法执行回波数据预处理、像素投影及图像重建,实现精确成像。优点是高精度和强适应性,缺点是计算量大、内存需求高。代码示例展示了回波生成、数据处理到插值显示的全过程。
|
9天前
|
机器学习/深度学习 自然语言处理 算法
m基于深度学习的OFDM+QPSK链路信道估计和均衡算法误码率matlab仿真,对比LS,MMSE及LMMSE传统算法
**摘要:** 升级版MATLAB仿真对比了深度学习与LS、MMSE、LMMSE的OFDM信道估计算法,新增自动样本生成、复杂度分析及抗频偏性能评估。深度学习在无线通信中,尤其在OFDM的信道估计问题上展现潜力,解决了传统方法的局限。程序涉及信道估计器设计,深度学习模型通过学习导频信息估计信道响应,适应频域变化。核心代码展示了信号处理流程,包括编码、调制、信道模拟、降噪、信道估计和解调。
31 8