zoukankan      html  css  js  c++  java
  • 归并排序详解

    浅谈归并排序

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

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

    还有快排很好写,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;
    }
    
  • 相关阅读:
    第1课 Git、谁与争锋
    程序员最真实的10个瞬间
    程序员最真实的10个瞬间
    一文读懂前端缓存
    一文读懂前端缓存
    一文读懂前端缓存
    EF使用CodeFirst创建数据库和表
    EF使用CodeFirst创建数据库和表
    EF使用CodeFirst创建数据库和表
    ASP.NET MVC的过滤器笔记
  • 原文地址:https://www.cnblogs.com/fusiwei/p/11656294.html
Copyright © 2011-2022 走看看