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

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

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天前
|
搜索推荐 算法 C语言
【排序算法】八大排序(上)(c语言实现)(附源码)
本文介绍了四种常见的排序算法:冒泡排序、选择排序、插入排序和希尔排序。通过具体的代码实现和测试数据,详细解释了每种算法的工作原理和性能特点。冒泡排序通过不断交换相邻元素来排序,选择排序通过选择最小元素进行交换,插入排序通过逐步插入元素到已排序部分,而希尔排序则是插入排序的改进版,通过预排序使数据更接近有序,从而提高效率。文章最后总结了这四种算法的空间和时间复杂度,以及它们的稳定性。
22 8
|
1天前
|
搜索推荐 算法 C语言
【排序算法】八大排序(下)(c语言实现)(附源码)
本文继续学习并实现了八大排序算法中的后四种:堆排序、快速排序、归并排序和计数排序。详细介绍了每种排序算法的原理、步骤和代码实现,并通过测试数据展示了它们的性能表现。堆排序利用堆的特性进行排序,快速排序通过递归和多种划分方法实现高效排序,归并排序通过分治法将问题分解后再合并,计数排序则通过统计每个元素的出现次数实现非比较排序。最后,文章还对比了这些排序算法在处理一百万个整形数据时的运行时间,帮助读者了解不同算法的优劣。
19 7
|
28天前
|
搜索推荐 算法
数据结构与算法学习十四:常用排序算法总结和对比
关于常用排序算法的总结和对比,包括稳定性、内排序、外排序、时间复杂度和空间复杂度等术语的解释。
17 0
数据结构与算法学习十四:常用排序算法总结和对比
|
28天前
|
算法 搜索推荐 Java
数据结构与算法学习十三:基数排序,以空间换时间的稳定式排序,速度很快。
基数排序是一种稳定的排序算法,通过将数字按位数切割并分配到不同的桶中,以空间换时间的方式实现快速排序,但占用内存较大,不适合含有负数的数组。
22 0
数据结构与算法学习十三:基数排序,以空间换时间的稳定式排序,速度很快。
|
27天前
|
算法
❤️算法笔记❤️-(每日一刷-83、删除排序链表中的重复项)
❤️算法笔记❤️-(每日一刷-83、删除排序链表中的重复项)
29 0
|
28天前
|
机器学习/深度学习 搜索推荐 算法
探索数据结构:初入算法之经典排序算法
探索数据结构:初入算法之经典排序算法
|
16天前
|
算法 安全 数据安全/隐私保护
基于game-based算法的动态频谱访问matlab仿真
本算法展示了在认知无线电网络中,通过游戏理论优化动态频谱访问,提高频谱利用率和物理层安全性。程序运行效果包括负载因子、传输功率、信噪比对用户效用和保密率的影响分析。软件版本:Matlab 2022a。完整代码包含详细中文注释和操作视频。
|
1天前
|
算法 数据挖掘 数据安全/隐私保护
基于FCM模糊聚类算法的图像分割matlab仿真
本项目展示了基于模糊C均值(FCM)算法的图像分割技术。算法运行效果良好,无水印。使用MATLAB 2022a开发,提供完整代码及中文注释,附带操作步骤视频。FCM算法通过隶属度矩阵和聚类中心矩阵实现图像分割,适用于灰度和彩色图像,广泛应用于医学影像、遥感图像等领域。
|
2天前
|
算法 调度
基于遗传模拟退火混合优化算法的车间作业最优调度matlab仿真,输出甘特图
车间作业调度问题(JSSP)通过遗传算法(GA)和模拟退火算法(SA)优化多个作业在并行工作中心上的加工顺序和时间,以最小化总完成时间和机器闲置时间。MATLAB2022a版本运行测试,展示了有效性和可行性。核心程序采用作业列表表示法,结合遗传操作和模拟退火过程,提高算法性能。
|
3天前
|
存储 算法 决策智能
基于免疫算法的TSP问题求解matlab仿真
旅行商问题(TSP)是一个经典的组合优化问题,目标是寻找经过每个城市恰好一次并返回起点的最短回路。本文介绍了一种基于免疫算法(IA)的解决方案,该算法模拟生物免疫系统的运作机制,通过克隆选择、变异和免疫记忆等步骤,有效解决了TSP问题。程序使用MATLAB 2022a版本运行,展示了良好的优化效果。