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

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

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月前
|
机器学习/深度学习 算法 安全
【无人机三维路径规划】基于非支配排序的鲸鱼优化算法NSWOA与多目标螳螂搜索算法MOMSA求解无人机三维路径规划研究(Matlab代码实现)
【无人机三维路径规划】基于非支配排序的鲸鱼优化算法NSWOA与多目标螳螂搜索算法MOMSA求解无人机三维路径规划研究(Matlab代码实现)
119 5
|
2月前
|
机器学习/深度学习 运维 算法
基于非支配排序遗传算法NSGAII的综合能源优化调度(Matlab代码实现)
基于非支配排序遗传算法NSGAII的综合能源优化调度(Matlab代码实现)
218 0
基于非支配排序遗传算法NSGAII的综合能源优化调度(Matlab代码实现)
|
2月前
|
供应链 算法 Java
【柔性作业车间调度问题FJSP】基于非支配排序的多目标小龙虾优化算法求解柔性作业车间调度问题FJSP研究(Matlab代码实现)
【柔性作业车间调度问题FJSP】基于非支配排序的多目标小龙虾优化算法求解柔性作业车间调度问题FJSP研究(Matlab代码实现)
|
2月前
|
机器学习/深度学习 算法 安全
【无人机三维路径规划】基于非支配排序的鱼鹰优化算法NSOOA求解无人机三维路径规划研究(Matlab代码实现)
【无人机三维路径规划】基于非支配排序的鱼鹰优化算法NSOOA求解无人机三维路径规划研究(Matlab代码实现)
|
30天前
|
数据采集 分布式计算 并行计算
mRMR算法实现特征选择-MATLAB
mRMR算法实现特征选择-MATLAB
103 2
|
2月前
|
传感器 机器学习/深度学习 编解码
MATLAB|主动噪声和振动控制算法——对较大的次级路径变化具有鲁棒性
MATLAB|主动噪声和振动控制算法——对较大的次级路径变化具有鲁棒性
171 3
|
21天前
|
机器学习/深度学习 算法 机器人
【水下图像增强融合算法】基于融合的水下图像与视频增强研究(Matlab代码实现)
【水下图像增强融合算法】基于融合的水下图像与视频增强研究(Matlab代码实现)
112 0
|
2月前
|
存储 编解码 算法
【多光谱滤波器阵列设计的最优球体填充】使用MSFA设计方法进行各种重建算法时,图像质量可以提高至多2 dB,并在光谱相似性方面实现了显著提升(Matlab代码实现)
【多光谱滤波器阵列设计的最优球体填充】使用MSFA设计方法进行各种重建算法时,图像质量可以提高至多2 dB,并在光谱相似性方面实现了显著提升(Matlab代码实现)
|
21天前
|
机器学习/深度学习 算法 机器人
使用哈里斯角Harris和SIFT算法来实现局部特征匹配(Matlab代码实现)
使用哈里斯角Harris和SIFT算法来实现局部特征匹配(Matlab代码实现)
106 8
|
21天前
|
机器学习/深度学习 算法 自动驾驶
基于导向滤波的暗通道去雾算法在灰度与彩色图像可见度复原中的研究(Matlab代码实现)
基于导向滤波的暗通道去雾算法在灰度与彩色图像可见度复原中的研究(Matlab代码实现)

热门文章

最新文章