浅谈归并排序
排序算法有很多,今天让我说一说:
冒泡选择和插入,希尔基数和堆桶;
还有快排很好写,STL大法没得说。
还有一个叫归并,时间稳定不爆锅。
—— 一个会说相声的博主的引言
相比于一些复杂度不太稳定的排序算法(比如快排,最坏的时候会退化成(O(n^2))级别的)或者时间稳定但是本来就是(O(n^2))级别的(汗),归并排序的好处就是将时间复杂度妥妥地控制在(O(nlogn))级别,在数据量很大的情况下会比(O(n^2))算法快很多。但是我们在设计程序的时候,的确一般都用快排(因为码量少,STL,退化的概率实在是很低)。所以归并排序在这个时候显得很是鸡肋。但是为了更高远的目标(用归并排序来理解分治思想,二分手段),或者了解一下归并排序的小技巧,还是非常有用的。
归并排序的概念
归并排序,顾名思义就是一种“递归合并”的排序方法(这个理解很重要)。对于一个数列,我们把它进行二分处理,依次递归下去,然后将小范围的数进行排序,最后将其合并在一起。就实现了归并排序。
这实际上是运用了分治思想,显然,想要把一个数列排好序,最终达到的目的就是它的任何一部分都是有序的。这样的话,我们可以考虑分别把数列分成N多个部分,让每个部分分别有序,然后再将其统一,变成所有的东西都有序。这样就实现了排序。这个想法就叫分治思想。
语言总是无力的,还是看图更容易理解一些:
(图片来自简书)
归并排序的实现
通过看图和读文。我们发现:归并排序其实只有两种操作:
分治排序,子序列合并。
通过对刚才的归并排序概念的理解,我们很容易得出,归并排序可以用递归实现。而递归的手段或者说是依据,就是二分。
在放代码之前,我先来简单捋一下实现归并排序的大致思路。
在开始归并排序之前,我们需要一个辅助数组。这个辅助数组的作用可以参照上图中下半部分的那些方块(后面又被退回去的那些),也就是说,这个数组存的元素是临时的,只是起保存原数组的元素以不至于丢失的作用。(所以最后还要置零)
递归开始:我们用两个指针(变量表示数组下标)来表示左半部分的第一个元素和右半部分第一个元素。判断这两个指针对应的数的大小。如果左面小,就在辅助数组里加上当前左指针所指的元素的同时右移左指针。同理,如果右面小,就在辅助数组里加上当前右指针所指的元素的同时右移右指针。然后,当我们退出循环的时候,肯定是左边的跑完了或者右边的跑完了,那么剩下的那些元素一定是比当前元素都大的东西,这个时候我们依次往里加就可以,最后,把已经处理好的辅助数组映射回原数组,同时辅助数组置零。
看不懂没关系,要是我第一次学归并排序,我也看不懂...如有不懂的小伙伴可以结合代码理解:
void merge_sort(int l,int r)
{
if(l==r)
return;
int mid=(l+r)>>1;
merge_sort(l,mid);
merge_sort(mid+1,r);
int i=l,j=mid+1,k=l;
while(i<=mid && j<=r)
{
if(a[i]<=a[j])
b[k++]=a[i++];
else
b[k++]=a[j++];
}
while(i<=mid)
b[k++]=a[i++];
while(j<=r)
b[k++]=a[j++];
for(int p=l;p<=r;p++)
a[p]=b[p],b[p]=0;
}
归并排序求逆序对
说实话,我觉得前面讲的这些全是在为这个部分做铺垫:
先放一波啥是逆序对...
对于一个数列(a),假如(a[i]>a[j])并且(i<j),那么这个(a[i],a[j])就叫做这个数列的一个逆序对。
简单理解一下:假如本来这个数列是单调递增的,突然出来了一对不和谐的,它非要皮一下,两个数调换一下位置。那么这个不和谐的数对就叫做逆序对。
归并排序是求逆序对的一个常见并好用的手段。其实另一种手段是树状数组,我会在另一篇博客细谈:
首先来放结论:如果在归并排序过程中,出现(a[i]>a[j]),那么就会产生(mid-i+1)个逆序对。
很奇妙吧?下面来讲证明:
因为我们做归并排序是用到了分治的思想,最后的操作其实就是递归回溯,从小到大地合并,所以这个时候,我们的两个子序列(即(l-mid),(mid+1-r)其实都是已经排好序的),这个时候,出现了一个不和谐的(a[i]),说明从这个数一直到(a[mid])的所有数都是不和谐的。我们直接累加就好。
代码只需要在模板归并排序的基础上再加一行即可:(已经用(Attention)标明)
void merge_sort(int l,int r)
{
if(l==r)
return;
int mid=(l+r)>>1;
merge_sort(l,mid);
merge_sort(mid+1,r);
int i=l,j=mid+1,k=l;
while(i<=mid && j<=r)
{
if(a[i]<=a[j])
b[k++]=a[i++];
else
{
b[k++]=a[j++];
ans+=mid-i+1;//Attention
}
}
while(i<=mid)
b[k++]=a[i++];
while(j<=r)
b[k++]=a[j++];
for(int p=l;p<=r;p++)
a[p]=b[p],b[p]=0;
}