归并排序的几种变形
归并排序的基础概念就不讲了,我的博客只会写有创造性的东西。
归并排序的代码如下:
void mergesort(int* arr,int n){
if(n>1){
mergesort(arr,n/2);
mergesort(arr+n/2,n-n/2);
merge(buff,arr,n/2,arr+n/2,n-n/2);
memcpy(arr,buff,n);
}
}
迭代版
标准的归并排序是个递归过程,不过要用迭代过程表达也很简单,就是先两个两个一排,再四个四个一排。。。。
如:
98 15 73 20 76 27 34 82
15 98 20 73 27 76 34 82 //第一趟
15 20 73 98 27 34 76 82 //第二趟
15 20 27 34 73 76 82 98 //第三趟
代码不写了,代码比递归版的复杂一点,效率也提高了一点
原地归并算法
标准归并排序使用的归并算法要额外申请一个大小为n的数组暂时存放归并数据,原地归并算法就不用,不过要以时间为代价,它的一次归并过程的时间复杂度是O(n^2)。
举个例子:
15 20 76 98 27 34 73 82
这里有两段有序数组,检测到应该把27、34、73插入到20和76之间,也就是把
76 98 27 34 73 这段数组循环左移两位。
如何把数组循环左移k位呢?
用的是翻大饼算法,先把前k个元素翻转过来,再把后n-k个元素翻转过来,在翻转这n个元素。
过程如下:
15 20 76 98 27 34 73 82 //检测到应该把27-73插入到20和76之间
15 20 98 76 27 34 73 82 //翻转98、76这段数组
15 20 98 76 73 34 27 82 //翻转27、34、73这段数组
15 20 27 34 73 76 98 82 //翻转这5个元素
15 20 27 34 73 76 98 82 //前面的数组已经有序,继续归并后面的数组
假设待排序元素是均匀分布的,那么待归并的前后两段有序数组元素是交叉排列,一次翻大饼算法只能插入O(1)个元素,因此原地归并算法的时间复杂度是T(n)=T(n-1)+n,
得到T(n)=O(n^2)。
那么使用了原地归并算法的归并排序的时间复杂度就是T(n)=2T(n/2)+n^2,得到T(n)=n^2。
原地归并排序在效率上肯定是不如插入排序的,具体有什么优势暂时没想到。
交替归并排序
从标准归并排序的代码中可以看到,每次利用缓冲区完成归并算法后,又要把缓冲区的内容复制回原数组里面去,这浪费了一些时间。
利用缓冲区完成归并算法后,完全可以把缓冲区和原数组的身份对调,就省去了复制回去的时间。
98 15 73 20 76 27 34 82 //在A数组
15 98 20 73 27 76 34 82 //在B数组
15 20 73 98 27 34 76 82 //在A数组
15 20 27 34 73 76 82 98 //在B数组
这里有个问题,最后的有序序列在A数组还是在B数组,写代码的时候注意一下就可以了。
链表归并排序
链表归并排序很容易理解了,就是把前后两半先排完,再用链表归并算法。
链表归并排序不存在数组归并排序需要缓冲区的问题,好处还是很大的。
表归并排序
这个肯定又是我的重新发明了
不知道大家有没有看过严蔚敏写的《数据结构(C语言版)》里面的表插入排序呢?
同样的,归并排序也可以完美地加上一个“表”字。
刚才说过,数组归并排序需要额外申请一个缓冲区,链表归并排序却不存在这个问题。
在表归并排序里,申请缓冲区是必须的,但存的不是元素的副本,而是元素的下标,两个数组形成一个静态链表。
直接举例子:
一开始数组无序,初始化下标数组,全都初始为-1
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
98 |
15 |
73 |
20 |
76 |
27 |
34 |
82 |
-1 |
-1 |
-1 |
-1 |
-1 |
-1 |
-1 |
-1 |
第一趟归并,变成两个一组,15->98,20->73,27->76,34->82
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
98 |
15 |
73 |
20 |
76 |
27 |
34 |
82 |
-1 |
0 |
-1 |
2 |
-1 |
4 |
7 |
-1 |
第二趟归并,变成四个一组,15->20->73->98,27->34->76->82
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
98 |
15 |
73 |
20 |
76 |
27 |
34 |
82 |
-1 |
3 |
0 |
2 |
7 |
6 |
4 |
-1 |
第三趟,成为有序的静态链表
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
98 |
15 |
73 |
20 |
76 |
27 |
34 |
82 |
-1 |
3 |
4 |
5 |
7 |
6 |
2 |
0 |
这里又有个问题,需要记住每个链表的首元素的下标,这个很容易解决,这里不讨论了。
现在,链表已经有序了,还要把元素归位,要用的是严蔚敏写的《数据结构(C语言版)》重排记录的算法。
描述有点复杂,这里不写了,想了解自己看书去吧。
这个表归并排序算法,时间复杂度还是O(nlogn),空间复杂度还是O(n)。
不过还是有一点优势的,特别是对于元素块头比较大的数组。
时间上说,在排序时元素不需要移动,只有在归位时,需要交换n-1次。
空间上说,申请的辅助空间只要存整型变量,不用存整个结构体。
也有点劣势,它毕竟是链表,不符合空间局部性原理,cache对它就没用了。
多路归并
归并多个有序数组,可以用堆,可以用败者树,也可以用哈夫曼树,不写了。