小白也能看懂的归并排序!!!
2023-03-14 22:50:13 时间
1.前言
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。 首先考虑下如何将将二个有序数列合并。这个非常简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。
#include<iostream>
using namespace std;
#include<algorithm>
//合并两个有序数组
void MemeryArray(int a[],int la,int b[],int lb,int c[])
{
int i=0, j=0, k=0;
while (i < la && j < lb)
{
if (a[i] < b[j])
c[k++] = a[i++];
else
c[k++] = b[j++];
}
while(i < la)
c[k++] = a[i++];
while (j < lb)
c[k++] = b[j++];
}
void test()
{
int a[] = { 1,5,8 };
int b[] = { 2,3,6,10,12 };
int c[8];
MemeryArray(a, 3, b, 5,c);
for_each(c, c + 8, [](int val) {cout << val << " "; });
}
int main()
{
test();
return 0;
}
- 可以看出合并有序数列的效率是比较高的,可以达到 O(n)。
- 解决了上面的合并有序数列问题,再来看归并排序,其的基本思路就是将数组分成二组 A,B,如果这二组组内的数据都是有序的,那么就可以很方便的将这二组数据进行排序。如何让这二组组内数据有序了?
- 可以将 A,B 组各自再分成二组。依次类推,当分出来的小组只有一个数据时,可以认为这个小组组内已经达到了有序,然后再合并相邻的二个小组就可以了。这样通过先递归的分解数列,再合并数列就完成了归并排序。
2.归并排序
一,概念及其介绍
- 归并排序(Merge sort)是建立在归并操作上的一种有效、稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
二、适用说明
- 当有 n 个记录时,需进行 logn 轮归并排序,每一轮归并,其比较次数不超过 n,元素移动次数都是 n,因此,归并排序的时间复杂度为 O(nlogn)。归并排序时需要和待排序记录个数相等的存储空间,所以空间复杂度为 O(n)。
- 归并排序适用于数据量大,并且对稳定性有要求的场景。
三、过程图示
归并排序是递归算法的一个实例,这个算法中基本的操作是合并两个已排序的数组,取两个输入数组 A 和 B,一个输出数组 C,以及三个计数器 i、j、k,它们初始位置置于对应数组的开始端。 A[i] 和 B[j] 中较小者拷贝到 C 中的下一个位置,相关计数器向前推荐一步。 当两个输入数组有一个用完时候,则将另外一个数组中剩余部分拷贝到 C 中。
- 自顶向下的归并排序,递归分组图示:
- 对第三行两个一组的数据进行归并排序
- 对第二行四个一组的数据进行归并排序
- 整体进行归并排序
3.归并排序递归代码
#include<iostream>
using namespace std;
#include<algorithm>
//合并两个有序序列
void mergearray(int a[], int first, int mid, int last, int temp[])
{
int i =first, j = mid+1, k = 0;
while (i <=mid && j <=last)
{
if (a[i] < a[j])
temp[k++] = a[i++];
else
temp[k++] = a[j++];
}
while (i <=mid)
{
temp[k++] = a[i++];
}
while (j <=last)
{
temp[k++] = a[j++];
}
//最后把temp里面存放的合并后为有序的左右序列倒回原数组a
//注意a的起点
for (int i = 0; i < k; i++)
{
a[first+i] = temp[i];
}
}
//拆分数组,有序合并
void mergesort(int a[],int first,int last,int temp[])
{
//没有拆分到只剩一个元素的极小序列
if(first < last)
{
int mid = (first + last) / 2;
mergesort(a, first, mid, temp);//拆分左边序列,合并为有序序列
mergesort(a, mid + 1, last, temp);//拆分右边序列,合并为有序序列
mergearray(a, first, mid, last, temp);//将左序列和右序列合并为一整个有序序列
}
}
//归并排序主函数
void MergeSort(int a[],int n)
{
int* p = new int[n];
if (!p)
return;
mergesort(a,0,n-1,p);
delete[] p;
}
void test()
{
int a[] = { 1,3,5,2,7,9,10 };
MergeSort(a, 7);
for_each(a, a + 7, [](int val) {cout << val << " "; });
}
int main()
{
test();
return 0;
}
- 注:有的书上是在 mergearray() 合并有序数列时分配临时数组,但是过多的new操作会非常费时。因此作了下小小的变化。只在 MergeSort() 中new 一个临时数组。后面的操作都共用这一个临时数组。
4.递归排序的迭代
- 下面这张图演示的是递归的过程
- 而迭代的过程就是化成一个个小的排序问题,再合并到一起
- 迭代的过程只有递归过程的一半,因此可以节约不少资源和空间,因此一般在用归并排序的时候会采用迭代的实现方式
#include<iostream>
using namespace std;
#include<algorithm>
//合并两个有序序列
void mergearray(int a[], int first, int mid, int last, int temp[])
{
int i =first, j = mid+1, k = 0;
while (i <=mid && j <=last)
{
if (a[i] < a[j])
temp[k++] = a[i++];
else
temp[k++] = a[j++];
}
while (i<=mid)
{
temp[k++] = a[i++];
}
while (j <=last)
{
temp[k++] = a[j++];
}
//最后把temp里面存放的合并后为有序的左右序列倒回原数组a
//注意a的起点
for (int i = 0; i < k; i++)
{
a[first+i] = temp[i];
}
}
//拆分数组,有序合并
void mergesort(int a[],int first,int last,int temp[])
{
//迭代的实现是直接从最小子序列(含一个元素)开始往上两两合并
int k = 1;//子序列长度
int Last=0;
int First=0;
int mid=0;
while (k<last)
{
First =0;
//3.剩余大于等于两个子序列的元素个数
while (First<=last-2*k+1)//加一的原因是因为下标从0算起
{
mid = First + k - 1;
Last = First + 2 * k - 1;
mergearray(a, First, mid, Last, temp);
First += 2 * k;
}
//当剩下的没有合并处理过的元素数量不足2k,即无法构成两个子序列进行合并操作时,要分类处理
//1.剩下小于等于一个子序列的元素个数
if (last-First<=k)
{
mid = First + (last - First) / 2;
mergearray(a, First, mid, last, temp);
}
//2.剩下大于一个小于两个的子序列元素个数
else
{
mid = First + k - 1;;
mergearray(a,First, mid, last, temp);
}
k *= 2;
}
//说明此时是特殊奇数情况,数组末尾还单着一个元素没有于前面一个完整的子序列合并
if (First==last)
{
mergearray(a, first, last - 1, last, temp);
}
}
//归并排序主函数
void MergeSort(int a[],int n)
{
int* p = new int[n];
if (!p)
return;
mergesort(a,0,n-1,p);
delete[] p;
}
void test()
{
int a[] = { 3,4,5,6,1,2,7,9,32,6,89,0,5,21,56,22,0,3,1};
MergeSort(a,19);
for_each(a, a+19, [](int val) {cout << val << " "; });
}
int main()
{
test();
return 0;
}
下面是画图分析子序列剩余的三种情况
(1)剩下小于等于一个子序列的元素个数
(2)剩下大于一个小于两个的子序列元素个数
(3)剩余大于等于两个子序列的元素个数
(4)特殊奇数情况,数组末尾还单着一个元素没有于前面一个完整的子序列合并
- 当数组长度为17时,k至多为16,也会剩余一个元素,此时first==last,还需额外进行一次合并。当数组长度为33…等等,这种特殊奇数情况,大家可以好好思考一下
完结撒花!!!
相关文章
- CentOS 停服!我们有哪些顶流的国产操作系统
- Containerd ctr、crictl、nerdctl 客户端命令介绍与实战操作
- Kubernetes 网络排错骨灰级中文指南
- 如何列出连接到 Linux 系统的 USB 设备
- 惊艳!Linux 中迷人的 Shell 脚本工具
- Dubbo-go-Mesh 开启新一代 Go 微服务形态
- 微软将显著优化 Windows 11 的 SMB 压缩,减少网络文件的传输时间
- Linux 命令 socat - netcat 实用程序的出色替代品
- OpenHarmony南向设备应用程序启动流程分析
- 跟着小白一起学鸿蒙之第一个OpenHarmony程序
- 都 2022 年了,手动搭建 React 开发环境很难吗?
- 如何在 Linux 中更改 GRUB 主题
- Linux 中的绝对路径和相对路径,有什么区别?
- Thread.sleep(0)的意义& 多线程详解
- Windows 11本月可选更新明显改善SMB压缩算法
- Linux 中的 Socat 命令示例
- 面试突击:线程休眠的方法有几种?
- 一段代码,告诉你什么是装饰器、可调用类、自定义运算符、函数式编程
- 三个可在 Linux 上玩旧 NES 游戏的 NES 模拟器
- 图解如何升级到 Linux Mint 21