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

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

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;
}
相关文章
|
2月前
|
搜索推荐 算法 C语言
【排序算法】八大排序(下)(c语言实现)(附源码)
本文继续学习并实现了八大排序算法中的后四种:堆排序、快速排序、归并排序和计数排序。详细介绍了每种排序算法的原理、步骤和代码实现,并通过测试数据展示了它们的性能表现。堆排序利用堆的特性进行排序,快速排序通过递归和多种划分方法实现高效排序,归并排序通过分治法将问题分解后再合并,计数排序则通过统计每个元素的出现次数实现非比较排序。最后,文章还对比了这些排序算法在处理一百万个整形数据时的运行时间,帮助读者了解不同算法的优劣。
170 7
|
2月前
|
搜索推荐 算法 C语言
【排序算法】八大排序(上)(c语言实现)(附源码)
本文介绍了四种常见的排序算法:冒泡排序、选择排序、插入排序和希尔排序。通过具体的代码实现和测试数据,详细解释了每种算法的工作原理和性能特点。冒泡排序通过不断交换相邻元素来排序,选择排序通过选择最小元素进行交换,插入排序通过逐步插入元素到已排序部分,而希尔排序则是插入排序的改进版,通过预排序使数据更接近有序,从而提高效率。文章最后总结了这四种算法的空间和时间复杂度,以及它们的稳定性。
139 8
|
3月前
|
搜索推荐 算法
数据结构与算法学习十四:常用排序算法总结和对比
关于常用排序算法的总结和对比,包括稳定性、内排序、外排序、时间复杂度和空间复杂度等术语的解释。
45 0
数据结构与算法学习十四:常用排序算法总结和对比
|
3月前
|
算法 搜索推荐 Java
数据结构与算法学习十三:基数排序,以空间换时间的稳定式排序,速度很快。
基数排序是一种稳定的排序算法,通过将数字按位数切割并分配到不同的桶中,以空间换时间的方式实现快速排序,但占用内存较大,不适合含有负数的数组。
49 0
数据结构与算法学习十三:基数排序,以空间换时间的稳定式排序,速度很快。
|
3月前
|
算法
❤️算法笔记❤️-(每日一刷-83、删除排序链表中的重复项)
❤️算法笔记❤️-(每日一刷-83、删除排序链表中的重复项)
39 0
|
3月前
|
机器学习/深度学习 搜索推荐 算法
探索数据结构:初入算法之经典排序算法
探索数据结构:初入算法之经典排序算法
|
3天前
|
算法 数据安全/隐私保护 计算机视觉
基于Retinex算法的图像去雾matlab仿真
本项目展示了基于Retinex算法的图像去雾技术。完整程序运行效果无水印,使用Matlab2022a开发。核心代码包含详细中文注释和操作步骤视频。Retinex理论由Edwin Land提出,旨在分离图像的光照和反射分量,增强图像对比度、颜色和细节,尤其在雾天条件下表现优异,有效解决图像去雾问题。
|
3天前
|
算法 数据可视化 安全
基于DWA优化算法的机器人路径规划matlab仿真
本项目基于DWA优化算法实现机器人路径规划的MATLAB仿真,适用于动态环境下的自主导航。使用MATLAB2022A版本运行,展示路径规划和预测结果。核心代码通过散点图和轨迹图可视化路径点及预测路径。DWA算法通过定义速度空间、采样候选动作并评估其优劣(目标方向性、障碍物距离、速度一致性),实时调整机器人运动参数,确保安全避障并接近目标。
|
13天前
|
算法 数据安全/隐私保护
室内障碍物射线追踪算法matlab模拟仿真
### 简介 本项目展示了室内障碍物射线追踪算法在无线通信中的应用。通过Matlab 2022a实现,包含完整程序运行效果(无水印),支持增加发射点和室内墙壁设置。核心代码配有详细中文注释及操作视频。该算法基于几何光学原理,模拟信号在复杂室内环境中的传播路径与强度,涵盖场景建模、射线发射、传播及接收点场强计算等步骤,为无线网络规划提供重要依据。
|
14天前
|
机器学习/深度学习 数据采集 算法
基于GA遗传优化的CNN-GRU-SAM网络时间序列回归预测算法matlab仿真
本项目基于MATLAB2022a实现时间序列预测,采用CNN-GRU-SAM网络结构。卷积层提取局部特征,GRU层处理长期依赖,自注意力机制捕捉全局特征。完整代码含中文注释和操作视频,运行效果无水印展示。算法通过数据归一化、种群初始化、适应度计算、个体更新等步骤优化网络参数,最终输出预测结果。适用于金融市场、气象预报等领域。
基于GA遗传优化的CNN-GRU-SAM网络时间序列回归预测算法matlab仿真

热门文章

最新文章