归并排序详解

浅谈归并排序

排序算法有很多,今天让我说一说:

冒泡选择和插入,希尔基数和堆桶;

还有快排很好写,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;
}
posted @ 2019-10-11 19:21  Seaway-Fu  阅读(2110)  评论(0编辑  收藏  举报