欢迎来到繁星的CSDN,本期内容主要包括归并排序(MergeSort)的实现
一、归并排序的主要思路
归并排序和上一期讲的快速排序很像,都利用了分治的思想,将一整个数组拆成一个个小数组,排序完毕后进行再排序,直到整个数组排序完毕。
唯一不同的是,归并排序是Out-place(需要额外空间),快速排序是In-place(不需要额外空间)。
整体思路如下:
1、单趟排序中,开辟一个等同于该数组大小的数组,并将其劈成两半。
2、将两个子数组的元素从首元素开始进行比较(原因是递归会使得子数组已排序完毕,此时子数组的首元素一定为最小值。),然后将更小值塞入开辟的数组中。结束条件是某一个子数组全部塞完,此时将另一个数组的剩余元素全部追加到开辟数组的尾端即可。
3、完成步骤二后,这两个子数组已排序完毕,将开辟数组的元素全部拷贝回这一轮的数组,返回,并开始上一级数组的再排序。
所以主体代码如下:
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
_MergeSort(a, tmp, 0, n - 1);
free(tmp);
tmp = NULL;
}
其中_MergeSort函数为MergeSort函数的子函数(即单趟排序和递归)。
tmp数组即我们开辟的额外数组。
虽然可以频繁开辟数组并free,但多余的开辟操作无疑造成了操作上的繁杂,所以子函数中我们多了两个参数,就是为了定位到开辟的大数组中。
二、归并排序的主体代码
void _MergeSort(int* a, int* tmp, int begin, int end)
{
if (begin >= end)
return;
int mid = (begin + end) / 2;
// 如果[begin, mid][mid+1, end]有序就可以进行归并了
_MergeSort(a, tmp, begin, mid);
_MergeSort(a, tmp, mid + 1, end);
// 归并
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}
以上便是归并排序的主体代码,最后一行memcpy可以被替代为遍历赋值。整体代码思想和快排差别不大,只是开辟了额外空间,可以被理解为以空间换时间。
归并排序的空间复杂度为O(n)(即开辟的数组),时间复杂度为O(nlogn),与快排一个量级,但没有快排那么需要小区间优化以及三数取中降低最坏情况的糟糕程度。
1000000的量级,各大高效排序各显神威,都交出了一份极强的答卷。
不过归并排序和希尔排序、堆排序、快速排序最大的区别就在于额外空间,属于典型的以空间换时间的做法。
尽管如此,我们在实践中还是常用快速排序作为排序手段。
饶是如此,归并排序的练习也可以帮助我们练习调试、增加递归的思路等等,具有较强的实际意义和练习价值。
三、归并排序的非递归版本
和快速排序一样,归并排序作为一个递归版本的排序方式,一定有非递归版的来解决。
首先还是找到归并排序为了什么而递归。
_MergeSort这一子函数中,又再度引用了两个参数begin和end。
不同于QuickSort用栈这一数据结构来解决问题,MergeSort必须从小到大排序,导致了用栈来模拟实现,无法得到数据的准确位置(因为每次QuickSort都能得到一个元素的最终位置)。
换句话说,似乎只能像希尔排序一样,利用gap来解决问题了。
代码如下:
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
// gap每组归并数据的数据个数
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
// [begin1, end1][begin2, end2]
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
// 第二组都越界不存在,这一组就不需要归并
if (begin2 >= n)
break;
// 第二的组begin2没越界,end2越界了,需要修正一下,继续归并
if (end2 >= n)
end2 = n - 1;
int j = i;
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));
}
gap *= 2;
}
free(tmp);
tmp = NULL;
}
gap的增长是以2为倍数的指数级增长,所以效率上没有拉下多少。同时将有关gap的部分去除,易发现剩余部分和_MergeSort这一子函数的内容没有多大差别。
本篇内容到此结束,谢谢大家的观看!
觉得写的还不错的可以点点关注,收藏和赞,一键三连。
我们下期再见~
往期栏目:
排序(一)——冒泡排序、直接插入排序、希尔排序(BubbleSort,InsertSort,ShellSort)-CSDN博客