目录
前言
归并排序(Merge Sort)是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
这些子问题解决的方法都是类似的,解决掉这些小的问题之后,归并子问题的结果,就得到了“大”问题的解。
时间复杂度:平均情况O(nlogn);最好情况(nlogn);最坏情况O(nlogn)
空间复杂度:O(n)
稳定性: 稳定
基本思想:首先将所有数据看做一个个单独的个体(单独的个体有序),然后两两融合,直到所有的数据都在同一个组内。
一、排序规则
1.一开始就将所有值都看作单独的个体,这个时候,从另一个角度来讲,他们独自有序。
2.两个部分两个部分融合(保证融合后有序且稳定)。
3.重复第二步,直到所有的数据都在同一个队列中。
两两融合规则
左手右手各拿起左右两部分中第一个值(也即是最小值),进行比较,谁小,谁先放置(递增序列),如果相等则先放置左边,这是为了保证原有的顺序性。
此处的"放置",我们需要借助辅助空间,也就是另一个数组来帮助实现。
我们依旧设立一组数来帮助理解:{21 12 7 9 11 98 56 88 66 68};
下面图示中数字下的“横线”,表示分组;
上图只为表现归并思想,省去了辅助空间。
即不断进行两两融合,直到最后(第四次),数据全部存在于一个队列中,则排序结束。
上述为非递归式二路归并排序,而递归式仅是将整个队列规模层层降低,变成一个个单独的元素后再进行上述操作。
因非递归式较易理解,我们在此仅进行非递归分析。
二、代码实现
1.非递归实现
因为代码较长,且难以理解,在此简单分析一下:
首先,MergeSort函数中,给出了数据每次进行归并时的规模,并调用Merge。
Merge函数中,首先构造了辅助空间,其大小与原数据队列相同。
而后我们确定了,左右数据队列的范围。
左手的左边界:low1 = 0; //即为数组最左边元素的位置
左手的右边界:high1 = low1 + gap - 1; //右边界的确定与问题规模有关,若规模为1,则high = 0 + 1 - 1 = 0;即左手抓住一个值,以此类推。
右手的左边界:low2 = high + 1; //因为左右两个数据队列相邻
右手的右边界:low2 + gap - 1 < len ? low2 + gap - 1 : len - 1; //此处与左边空间规则相同,但需要考虑合法性问题,即右边界不可能超过数组。
在while中我们完成了将归并融合的值放入辅助空间的过程。
其余问题皆在注释中进行分析,逐句理解即可。
void Merge(int arr[], int len, int gap)//gap:代表合并的两个组中单独的组个数是多少(几几合并的几)
{
//第一步,先整出来辅助空间brr
int* brr = (int*)malloc(len * sizeof(int));
assert(brr != NULL);
int i = 0; //i是辅助空间brr的下标 代表下一个值插入位置
int low1 = 0;//左手的左边界
int high1 = low1 + gap - 1;//左手的右边界
int low2 = high1 + 1;//右手的左边界
int high2 = low2 + gap - 1 < len ? low2 + gap - 1 : len - 1;//右手的右边界
while (low2 <= high2)//确保左手和右手都抓到值(左右手都不空),只要右手还有值,左手肯定抓满值了
{
//代表着,融合比较过程中,左右手还不空
//有一个空了,就不用比较了,直接向brr挪动另一个手中的数据即可
while (low1 <= high1 && low2 <= high2)
{
if (arr[low1] <= arr[low2])//如果左手的第一个值 <= 右手的第一个值
{
brr[i++] = arr[low1++];//将左手的第一个值放到brr中
}
else//如果左手的第一个值 > 右手的第一个值
{
brr[i++] = arr[low2++];//将右手的第一个值放到brr中
}
}
//当内层while循环退出,代表着肯定有一个手空了
//这里可以不用判断到底哪个手空了,简单粗暴一点(两个数组都调用一下,将其剩余数据向brr挪动)
while (low1 <= high1)//将左手剩余数据向brr中挪动
{
brr[i++] = arr[low1++];
}
while (low2 <= high2)//将右手剩余数据向brr中挪动
{
brr[i++] = arr[low2++];
}
//左手右手数据处理完毕,平移,让左手右手再抓两个组
low1 = high2 + 1;
high1 = low1 + gap - 1;
low2 = high1 + 1;
high2 = low2 + gap - 1 < len ? low2 + gap - 1 : len - 1;
}
//此时,代表着处理到最后一组了:这时候退出有两种情况:1.左右手都没有抓到值 2.左手不空,右手空
//会不会存在第三种情况:左手空,右手不空? 这种情况不存在
//那么怎么处理上面两种可以出现的情况:第一种皆大欢喜,不用管了
// 第二种,需要处理一下
//处理2:左手的值放到brr里,右手肯定为空 不用管
while (low1 < len)//将左手剩余数据向brr中挪动 //这里是low1<len
{
brr[i++] = arr[low1++];
}
//最后一步,brr里放满了值, 这时只需要将brr的值同步到arr里即可
for (int k = 0; k < len; k++)
{
arr[k] = brr[k];
}
free(brr);//释放辅助空间brr
brr = NULL;
}
void MergeSort(int* arr, int len)
{
assert(arr != NULL);
//i代表几几合并的几 当i小于len时,才有必要进行一次融合
//当i大于等于len时,没有必要进行一次融合,因为这时所有的值都在同一个组内
for (int i = 1; i < len; i *= 2) //logn
{
Merge(arr, len, i);
}
}
2.递归实现
递归实现中,Merge函数的实现与非递归基本相同,代码量还有些许减少。
主要问题在于理解MergePass中递归思想,其将整个规模层层剥离减少,到达非递归中的初始,即一个元素为一个队列时开始回归,而后进行与非递归相同操作。
需要注意的是,递归式中的辅助空间则是在外部申请,因为需要防止在递归中同时申请大量空间的情况。
void Copy(int* src, int* dest, int left, int right)
{
while (left <= right)
{
src[left] = dest[left];
left++;
}
}
void Merge(int* src, int* dest, int left, int m, int right)
{
int i = left; //i是辅助空间brr的下标 代表下一个值插入位置
int j = m + 1;
int k = left;
while (i <= m && j <= right)//代表着,融合比较过程中,左右手还不空(有一个空了,就不用比较了,直接向brr挪动另一个手中的数据即可)
{
if (src[i] <= src[j])//如果左手的第一个值 <= 右手的第一个值
{
dest[k++] = src[i++];//将左手的第一个值放到brr中
}
else//如果左手的第一个值 > 右手的第一个值
{
dest[k++] = src[j++];//将右手的第一个值放到brr中
}
}
//当内层while循环退出,代表着肯定有一个手空了
//这里可以不用判断到底哪个手空了,简单粗暴一点(两个数组都调用一下,将其剩余数据向brr挪动)
while (i <= m)//将左手剩余数据向brr中挪动
{
dest[k++] = src[i++];
}
while (j <= right)//将右手剩余数据向brr中挪动
{
dest[k++] = src[j++];
}
}
void MergePass(int* src, int* dest, int left, int right)
{
if (left < right)
{
int mid = (left + right) / 2;
MergePass(src, dest, left, mid);
MergePass(src, dest, mid + 1, right);
Merge(src, dest, left, mid, right);
Copy(src, dest, left, right);
}
}
void MergeSort(int* arr, int len)
{
if (len < 2 || arr == NULL)
{
return;
}
int* tmp = (int*)malloc(sizeof(int) * len);
MergePass(arr, tmp, 0, len - 1);
free(tmp);
}
总结
以上就是今天学习的内容,本文介绍了归并排序的排序规则和代码实现,归并排序速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列。