【排序算法(四)】归并排序&&计数排序(非比较排序)以及八大排序算法的总结(上)

简介: 【排序算法(四)】归并排序&&计数排序(非比较排序)以及八大排序算法的总结(上)

1、归并排序

1.1 算法思想

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。


如果说快速排序是前序遍历,那么归并恰巧就是它的对立面,归并排序相当于是二叉树的后序遍历


归并排序算法思想(排升序为例):

1.假设数组有N个元素,先将数组不断地二分,直到将数组划分为N个由单个元素构成的子数组,整个划分过程中所有子数组构成满二叉树(或接近满二叉树)的逻辑结构,如图:

image.png2.数组划分完后再逐层向上将二叉树兄弟结点子数组(具有相同前驱结构)两两进行归并操作完成排序:image.png归并排序是将区间逐个分解为一个个小区间,直到不能分割为止,然后一步步 归并起来 ,逐层返回。而这一过程需要借助一个辅助数组 tmp 来完成归并过程。

eg.两个有序数组归并:依次比小,小的尾插到新空间

image.gif

1.2 两个有序子序的归并(排升序)

arr是被分割的原数组,tmp是用于归并操作的临时数组,begin是arr的左端下标,end是arr的右端下标

假设数组arr被二等分为两个子序列(两个子序列都是有序的):

image.png接下来我们将上图中的[begin,(begin+end)/2)和[(begin+end)/2),end)两个子序列(有序)合并到一个tmp数组中构成一个新的有序序列(通过三指针完成)image.png代码:

void _MergeSort(int* a, int begin, int end, int* tmp)
{
  if (begin >= end)//不存在返回
  {
    return;
  }
  int mid = (begin + end) >> 1;// /2
  //[begin,mid] [mid+1,end]归并
  int begin1 = begin, end1 = mid;
  int begin2 = mid + 1, end2 = end;
  /*
  * 第一种拷贝回原数组的方式 - memset
  * 此种做法 cnt 从 begin 开始
  * memset 从 begin 位置开始,一共拷贝 end - begin + 1 个元素
  * 和下面做法道理相同
  */
  // int cnt = begin;
  /*
  * 第二种拷贝回原数组的方式 - 循环拷贝
  * 此种做法 cnt 从 0 开始
  * 开始拷贝的位置从 begin 开始
  * cnt 最终的长度就是 [begin, end] 之间的长度
  * 没问题
  */
  int cnt = 0; 
  while (begin1 <= end1 && begin2 <= end2)
  {
    // 保持稳定性
    if (a[begin1] <= a[begin2])
    {
      tmp[cnt++] = a[begin1++];
    }
    else
    {
      tmp[cnt++] = a[begin2++];
    }
  }
  while (begin1 <= end1) 
  {
    tmp[cnt++] = a[begin1++];
  }
  while (begin2 <= end2) 
  {
    tmp[cnt++] = a[begin2++];
  }
  // 方法1
  // memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
  //目标 源 大小
    // 方法2
    for (int i = begin, j = 0; i <= end; i++, j++)
  {
    a[i] = tmp[j];
  }
}

1.3 归并递归版本

完成arr数组[left,right)区间序列排序的过程可以拆分为如下三个步骤:

1.先完成左子区间[left,left + (right - left) / 2)的排序

2.再完成右子区间[left + (right - left) / 2,right)的排序

3.最后将左右子区间进行归并完成[left,right)区间序列的排序

数组二分的递归框架:

void _MergeSort(int* a, int begin, int end, int* tmp)
{
  if (begin >= end)//不存在返回
  {
    return;
  }
  int mid = (begin + end) >> 1;// /2
  //[begin,mid] [mid+1,end] 子区间递归排序
    // 递归到底部
  _MergeSort(a, begin, mid, tmp);
  _MergeSort(a, mid + 1, end, tmp);
}

思路:

1.对于归并排序来说,首先开辟一个辅助数组 tmp 。我们每一次取一个中间点 mid=(begin + end)/2 。

2.按照后序遍历的方式,分别递归左右区间:[begin,mid] ,[mid+1,end] 一直递归到底部,递归的返回条件为begin>=end 。

3.然后归并,设定相关变量,将两区间内对应元素由小到大放置到tmp 数组对应位置处。

4.如果放置过程结束,一个数组没有放置完,则需要在循环结束后,将数组的数据全部倒入 tmp 数组中。

5.在上面的过程完毕之后,再把tmp 数组中的数据拷贝回原数组。

6.最终,递归逐层返回后,就完成了归并过程。

代码:

//子函数
void _MergeSort(int* a, int begin, int end, int* tmp)
{
  if (begin >= end)//不存在返回
  {
    return;
  }
  int mid = (begin + end) >> 1;// /2
  //[begin,mid] [mid+1,end] 子区间递归排序
    // 递归到底部
  _MergeSort(a, begin, mid, tmp);
  _MergeSort(a, mid + 1, end, tmp);
  //[begin,mid] [mid+1,end]归并
  int begin1 = begin, end1 = mid;
  int begin2 = mid + 1, end2 = end;
  /*
  * 第一种拷贝回原数组的方式 - memset
  * 此种做法 cnt 从 begin 开始
  * memset 从 begin 位置开始,一共拷贝 end - begin + 1 个元素
  * 和下面做法道理相同
  */
  // int cnt = begin;
  /*
  * 第二种拷贝回原数组的方式 - 循环拷贝
  * 此种做法 cnt 从 0 开始
  * 开始拷贝的位置从 begin 开始
  * cnt 最终的长度就是 [begin, end] 之间的长度
  * 没问题
  */
  int cnt = 0; 
  while (begin1 <= end1 && begin2 <= end2)
  {
    // 保持稳定性
    if (a[begin1] <= a[begin2])
    {
      tmp[cnt++] = a[begin1++];
    }
    else
    {
      tmp[cnt++] = a[begin2++];
    }
  }
  while (begin1 <= end1) 
  {
    tmp[cnt++] = a[begin1++];
  }
  while (begin2 <= end2) 
  {
    tmp[cnt++] = a[begin2++];
  }
  // 方法1
  // memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
  //目标 源 大小
    // 方法2
    for (int i = begin, j = 0; i <= end; i++, j++)
  {
    a[i] = tmp[j];
  }
}
void MergeSort(int* a, int n)
{
  int* tmp = (int*)malloc(sizeof(int) * n);
  if (tmp == NULL)
  {
    perror("mallol fail");
    return;
  }
  _MergeSort(a, 0, n - 1, tmp);
}

1.4 归并排序非递归版本

归并排序的非递归版本在这一块是一个难点,因为它本身就很难想到。

首先想一下,对于归并排序来说,能不能像快速排序那样借助数据结构-栈来实现?


如果用栈,是不太行的。因为归并是一种类似二叉树后序遍历的排序,当将区间入栈后,把区间拿出来处理,之后要继续分割时,一段区间可能就不见了,所以借助数据结构-栈时不太行的。


所以我们可以不借助数据结构,用一种相对简单的方法完成。


我们可以设定一个 gap ,控制我们的区间大小,gap 就是归并时每组的数据个数。由于我们是类似二叉树后序遍历的方式,所以我们一开始的归并实际上就是 gap 为 1 情况。


如下图:

image.pngarr代表待排序的数组,size为待排序数组的元素个数


通过每次改变gap 实际上也就是改变了区间大小,就模拟除了归并递归到底,从小区间合并逐渐到大区间合并的过程。所以我们就让gap 每次 × 2,这样子就是归并每次扩大区间的过程。


使用一个变量i来遍历每一个gap情形下的各个进行归并的序列组(每个序列组由两个子数组构成):

for (int gap = 1; gap < size; gap *= 2)      //完成logN个层次的子数组的归并
  {
    for (int i = 0; i < size; i += 2 * gap)  //i每次跳过一个归并序列组(每个序列组有两个子数组)
    {
      //对子数组[i,i+gap-1]和子数组[i+gap,i+2*gap-1]进行归并操作
    }
  }

image.png但是上面的方法只能解决数组长度恰巧被整除的情况,对于无法被整除的情况可能就会造成越界。比如 n=17 。在gap=4 时,最后一段区间 [ 17 , 20 ] 越界,所以这里需要调整


我们设置四个点begin1 = i, end1 = i + gap - 1, begin2 = i + gap, end2 = i + 2 * gap - 1四个点来规定两段区间。


列举一下,这四个点的越界情况,我们可以分为三种情况:

1 .end1, begin2, end2越界image.png

2.end1没有越界,begin2,end2 越界image.png3.end2 越界image.png以上是三种越界情况,需要分别处理,处理方式分为 修正区间不修正区间

修正区间 :

第一种越界情况,实际上就是 end1≥n ,那么这种情况修正区间的话,这时将end1=n−1 ,之后将没有越界的部分拷贝到 tmp 数组中,然后将[begin2,end2] 修正为一个不存在的区间,这样就不会进入后面循环,也就不会拷贝进数据。

反例:如果begin2 =end2 = n - 1,就会出现有一个数据重复归并的bug

if (end1 >= n)
{
  end1 = n - 1;
  // begin2 和 end2 修正为不存在的区间
  begin2 = n;
  end2 = n - 1;
}

第二种越界情况,就是 begin2≥n ,这种情况下直接将[begin2,end2] 修正为不存在的区间即可。

else if (begin2 >= n)
{
  begin2 = n;
  end2 = n - 1;
}

第三种越界情况,就是end2≥n,这种情况将end2=n−1 ,让两端区间正常归并。

else if (end2 >= n)
{
  end2 = n - 1;
}

这种情况可以边归并边拷贝,也可以一组归并完了拷贝。

循环外拷贝,修正区间后,直接一把全部拷贝:

void MergeSortNonR(int* a, int n)
{
  int* tmp = (int*)malloc(sizeof(int) * n);
  if (tmp == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  int gap = 1;
  while (gap < n)
  {
    // 一组归并的跨距为 2 * gap
    for (int i = 0; i < n; i += 2 * gap)
    {
      int begin1 = i, end1 = i + gap - 1;
      int begin2 = i + gap, end2 = i + 2 * gap - 1;
      int j = i;
      // 修正区间
      if (end1 >= n)
      {
        end1 = n - 1;
        // begin2 和 end2 修正为不存在的区间
        begin2 = n;
        end2 = n - 1;
      }
      else if (begin2 >= n)
      {
        begin2 = n;
        end2 = n - 1;
      }
      else if (end2 >= n)
      {
        end2 = n - 1;
      }
      //归并
      while (begin1 <= end1 && begin2 <= end2)
      {
        if (a[begin1] <= a[begin2])
        {
          tmp[j++] = a[begin1++];
        }
        else
        {
          tmp[j++] = a[begin2++];
        }
      }
      //拷贝
      while (begin1 <= end1)
      {
        tmp[j++] = a[begin1++];
      }
      while (begin2 <= end2)
      {
        tmp[j++] = a[begin2++];
      }
      // 可以局部拷贝
      //memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
    }
    memcpy(a, tmp, sizeof(int) * n);
    gap *= 2;
  }
  free(tmp);
  tmp = NULL;
}
相关文章
|
1月前
|
算法
【算法】二分查找——在排序数组中查找元素的第一个和最后一个位置
【算法】二分查找——在排序数组中查找元素的第一个和最后一个位置
|
24天前
|
算法 搜索推荐 Java
算法实战:手写归并排序,让复杂排序变简单!
归并排序是一种基于“分治法”的经典算法,通过递归分割和合并数组,实现O(n log n)的高效排序。本文将通过Java手写代码,详细讲解归并排序的原理及实现,帮助你快速掌握这一实用算法。
36 0
|
24天前
|
数据采集 搜索推荐 算法
【高手进阶】Java排序算法:从零到精通——揭秘冒泡、快速、归并排序的原理与实战应用,让你的代码效率飙升!
【8月更文挑战第21天】Java排序算法是编程基础的重要部分,在算法设计与分析及实际开发中不可或缺。本文介绍内部排序算法,包括简单的冒泡排序及其逐步优化至高效的快速排序和稳定的归并排序,并提供了每种算法的Java实现示例。此外,还探讨了排序算法在电子商务、搜索引擎和数据分析等领域的广泛应用,帮助读者更好地理解和应用这些算法。
17 0
|
30天前
|
算法 搜索推荐
算法设计 (分治法应用实验报告)基于分治法的合并排序、快速排序、最近对问题
这篇文章是关于分治法应用的实验报告,详细介绍了如何利用分治法实现合并排序和快速排序算法,并探讨了使用分治法解决二维平面上的最近对问题的方法,包括伪代码、源代码实现及时间效率分析,并附有运行结果和小结。
|
9天前
|
算法 BI Serverless
基于鱼群算法的散热片形状优化matlab仿真
本研究利用浴盆曲线模拟空隙外形,并通过鱼群算法(FSA)优化浴盆曲线参数,以获得最佳孔隙度值及对应的R值。FSA通过模拟鱼群的聚群、避障和觅食行为,实现高效全局搜索。具体步骤包括初始化鱼群、计算适应度值、更新位置及判断终止条件。最终确定散热片的最佳形状参数。仿真结果显示该方法能显著提高优化效率。相关代码使用MATLAB 2022a实现。
|
9天前
|
算法 数据可视化
基于SSA奇异谱分析算法的时间序列趋势线提取matlab仿真
奇异谱分析(SSA)是一种基于奇异值分解(SVD)和轨迹矩阵的非线性、非参数时间序列分析方法,适用于提取趋势、周期性和噪声成分。本项目使用MATLAB 2022a版本实现从强干扰序列中提取趋势线,并通过可视化展示了原时间序列与提取的趋势分量。代码实现了滑动窗口下的奇异值分解和分组重构,适用于非线性和非平稳时间序列分析。此方法在气候变化、金融市场和生物医学信号处理等领域有广泛应用。
|
1月前
|
算法
基于模糊控制算法的倒立摆控制系统matlab仿真
本项目构建了一个基于模糊控制算法的倒立摆控制系统,利用MATLAB 2022a实现了从不稳定到稳定状态的转变,并输出了相应的动画和收敛过程。模糊控制器通过对小车位置与摆的角度误差及其变化量进行模糊化处理,依据预设的模糊规则库进行模糊推理并最终去模糊化为精确的控制量,成功地使倒立摆维持在直立位置。该方法无需精确数学模型,适用于处理系统的非线性和不确定性。
基于模糊控制算法的倒立摆控制系统matlab仿真
|
10天前
|
资源调度 算法
基于迭代扩展卡尔曼滤波算法的倒立摆控制系统matlab仿真
本课题研究基于迭代扩展卡尔曼滤波算法的倒立摆控制系统,并对比UKF、EKF、迭代UKF和迭代EKF的控制效果。倒立摆作为典型的非线性系统,适用于评估不同滤波方法的性能。UKF采用无迹变换逼近非线性函数,避免了EKF中的截断误差;EKF则通过泰勒级数展开近似非线性函数;迭代EKF和迭代UKF通过多次迭代提高状态估计精度。系统使用MATLAB 2022a进行仿真和分析,结果显示UKF和迭代UKF在非线性强的系统中表现更佳,但计算复杂度较高;EKF和迭代EKF则更适合维数较高或计算受限的场景。
|
11天前
|
算法
基于SIR模型的疫情发展趋势预测算法matlab仿真
该程序基于SIR模型预测疫情发展趋势,通过MATLAB 2022a版实现病例增长拟合分析,比较疫情防控力度。使用SIR微分方程模型拟合疫情发展过程,优化参数并求解微分方程组以预测易感者(S)、感染者(I)和移除者(R)的数量变化。![]该模型将总人群分为S、I、R三部分,通过解析或数值求解微分方程组预测疫情趋势。
|
11天前
|
算法 数据可视化 数据安全/隐私保护
基于LK光流提取算法的图像序列晃动程度计算matlab仿真
该算法基于Lucas-Kanade光流方法,用于计算图像序列的晃动程度。通过计算相邻帧间的光流场并定义晃动程度指标(如RMS),可量化图像晃动。此版本适用于Matlab 2022a,提供详细中文注释与操作视频。完整代码无水印。