归并排序之逆序对的数量

89 阅读5分钟

为什么逆序对要用归并排序呢

首先我们来看归并排序的原理

归并模板

void merage(int l,int r)
{
    if(l>=r)return ;
    int mid=l+r>>1;
    merage(l,mid);      //分割左边区间
    merage(mid+1,r);    //分割右边区间
    int k=0,i=l,j=mid+1;
    while(i<=mid&&j<=r) //块与块内部元素的比较后合并
    {
        if(a[i]<=a[j])
        tmp[k++]=a[i++];
        else
        tmp[k++]=a[j++];
    }
    while(i<=mid)tmp[k++]=a[i++]; //合并残留
    while(j<=r)tmp[k++]=a[j++];
    for (i = l, j = 0; i <= r; i ++, j ++ ) a[i] = tmp[j] //每次利用临时数组tmp排好的数据赋给                                                                        
    }                                                     //数组a到最最开始的一层递归a就是排                
                                                          //序好的数组                                                            

看下面这几行代码

    if(l>=r)return ;
    int mid=l+r>>1;
    merage(l,mid);
    merage(mid+1,r);

我们发现函数体开始就是不断的制造分界点 分割区域然后调用函数递归

所以我们脑子里面可以大概有一个被分割完成的画面 因为mid取值为中值;

​编辑

 下面的区域会越来越小 然后精准分割到每一个区域为一个元素

然后我们再看以下代码

    int k=0,i=l,j=mid+1;
    while(i<=mid&&j<=r) //块与块内部元素的比较后合并
    {
        if(a[i]<=a[j])
        tmp[k++]=a[i++];
        else
        tmp[k++]=a[j++];
    }

它们的功能就是实现块与块之间内部元素的比较

注:tmp数组存在的意义就是暂时保留排序好的数组称为临时数组

例如:

左区域:2,3,5 (i)

右区域:   1,4,6(j)

等等 为什么出现的都是有序的区域 你是不是故意这样写的 容易理解

那我们回顾一下上面 (我真的服了你这个老6)归并的递归调用会一直调用到一个区域为一个元素 然后通过这块代码合并出来的

通过以上代码可变为1,2,3,4,5

???那6呢 因为while循环已经不符合执行条件了 i 已经超出范围了 那么这么办 别急

看以下代码

    while(i<=mid)tmp[k++]=a[i++]; //合并残留
    while(j<=r)tmp[k++]=a[j++];

这是收尾工作 把那可恶的老6给找出来

然后最终收尾

for (i = l, j = 0; i <= r; i ++, j ++ ) a[i] = tmp[j]

这是把每一段合并好的元素从临时数组tmp赋值给a 方便下一次对数组a的操作

最后一次就是排序好的数组 不相信的宝子们可以找一个大于50个元素的数组试一下

好的 这就是归并排序

???

那逆序对呢

归并排序逆序对的应用

快看这个老6

​
    int k=0,i=l,j=mid+1;
    while(i<=mid&&j<=r) //块与块内部元素的比较后合并
    {
        if(a[i]<=a[j])
        tmp[k++]=a[i++];
        else
        tmp[k++]=a[j++];
    }

​

我们仔细观察这行代码 tmp[k++]=a[j++]

它是什么条件下成立的呢

显然 左区域中某个元素大于右区域中某个元素的时候

然而这不就是逆序对存在的条件吗

例   2,3,4,5,1

然后你灵机一动 心想:这样的话,那它交换一次不就是一个逆序对吗,那我写个变量加一不就行了吗 这时你的嘴角微微一扬 自信的拿起键盘 定义了一个public变量 劈里啪啦

long long  merage(int l,int r)
{
    if(l>=r)return ;
    int mid=l+r>>1;
    merage(l,mid);
    merage(mid+1,r);    
    int k=0,i=l,j=mid+1;
    while(i<=mid&&j<=r)
    {
        if(a[i]<=a[j])
        tmp[k++]=a[i++];
        else
        tmp[k++]=a[j++];
        res++;
    }
    while(i<=mid)tmp[k++]=a[i++]; //合并残留
    while(j<=r)tmp[k++]=a[j++];
    for (i = l, j = 0; i <= r; i ++, j ++ ) a[i] = tmp[j];                                                      
    return res;
}                                                    

你为函数写上了返回值 你还细心的把返回值的范围写大了

然后就报红了 然后你就不是当时的嘴角上扬而是痛苦面具

真相只有一个

然而这就是我们要解决的问题

当res++面对这样的数据时

左区域:3,4 (i)

右区域:   1,2(j)

显得非常的无力

我们都能看出逆序对的数量是4个 但是res++计算不了

但是我们发现 如果a[i]>a[j]的话 那么左区域中i~mid的范围都是大于mid 因为内部都是有序排列

哦 那就好办了

求i~mid之间元素的个数呗

res=res+mid-i+1;

7到10有几个元素  10-7+1=4  7没有的8 9 10 算上它本身就是4个 尾项减去首项+1;

你开开心心的提交了代码

通过了

然后你匪夷所思的看着最后一行代码

 for (i = l, j = 0; i <= r; i ++, j ++ ) a[i] = tmp[j];           

这个还有用吗

你灵机一动 哦 这个是输出归并排序结果的 我不输出结果我只是用他的计算过程 删了删了

心想:又少一行代码 程序有高级不少啊 

然后就又红了 破口大骂 这是干什么的呀 我又不输出

 现在回顾一下上面(真的服了你这个老六了)

注意这行代码

 res=res+mid-i+1;    

它成立的条件就是需要内部有序,而你删除的代码就是变成有序序列的步骤

否则你运算的就一直是原序列

现在 你有纳闷了 我改变顺序会不会影响到逆序对的数量呀

答案是肯定的

肯定不会的

你计算的都是区域跟区域间的

就好像 左区域的5不管在哪个索引位置都比右区域的2要大 永远构成逆序对 排序只是一种更高效的算法实现