2分法解决逆序对数

  在一个数组中,如果两个元素 ai a i aj a j ,有 i<j i < j 并且 ai>aj a i > a j ,我们则成这两个元素对 (ai,aj) ( a i , a j ) 次序出错,是一个逆序对。例如数组[2, 4, 1, 3, 5]中逆序对有3个,分别是 (2,1),(4,1),(4,3) ( 2 , 1 ) , ( 4 , 1 ) , ( 4 , 3 )

  现在我们的问题是,如果判断一个数组中逆序对的数量?

  最简单的方法就是检查每一对数 (ai,aj) ( a i , a j ) ,看是否是逆序的,这将用 O(n2) O ( n 2 ) 时间。我们能够用更快的方法来计算呢,答案是肯定的!

  我们将原来n个元素的数组A,分成两半,即让 m=n/2 m = n / 2 (向上取整)。那么可以将数组分为两部分: a1,a2,...,am a 1 , a 2 , . . . , a m am,am+1,...,an a m , a m + 1 , . . . , a n 。我们首先分别计算这两半中各自的逆序对数,然后再计算这两半中交叉的逆序对数,即计算逆序对 (ai,aj) ( a i , a j ) ,其中 ai a i aj a j 分别属于不同的两半。然后将上面计算的3种逆序数相加,就等于数组A中的逆序数(这是很好理解的)。

  所以我们总共需要计算3种逆序对数。1.前一半中的逆序对数。2.后一半中的逆序对数。3.前半和后半中交叉的逆序对数。我们考虑递归2分下去,我们会发现,到最后每个前半部分只有一个元素,每个后半部分也只有一个元素,在这种情况下,其实我们发现仅有一个元素数组的第1种和第2种逆序对数为0(一个元素无法构成逆序对),第3种逆序对数需要计算出来。假设我们计算出了第3种逆序对数,然后向上回溯,我们会发现,根据上一层的计算结果,第1种和第2中逆序对数都已经已知了,等于上一层的三种逆序对数的总和;重点是如何计算第3种逆序对数。依次类推向上回溯,我们会发现,其实第1种和第2种逆序对数总是已知的,因为可以根据上一层的计算结果得到,重点是如何计算第3种逆序对数。所以问题的重点就是如何计算交叉情况下的逆序对数

  假设我们已经递归的排序了某个数组前一半和后一半并且计数了每个部分的逆序对。我们现在有两个排好序的数组A和数组B,分别包含前一半和后一半,我们想把他们合并成一个排好序的数组C,同时计算交叉的逆序对数 (a,b) ( a , b ) ,其中 aA,bB a ∈ A , b ∈ B 。我们假设大家已经了解了如何合并两个有序数组,我们每一次从两个数组中挑取当前最小的如果当前挑取的这个元素a来自数组A,因为a是当前最小的,并且A是前一半,所以显然不可能构成逆序对(根据逆序对定义);如果当前挑取的这个元素b来自B,那么因为b是当前最小的,所以它应该比A中剩余的没有遍历过的元素都小,构成了多个逆序对,数量是A中剩余的没有遍历的元素个数。所以我们可以用合并排序解决这个问题,合并排序能够生成两个有序的子数组,并且也是2分过程,而对于逆序对个数的计算可以放在合并过程中,实现代码如下:

#include<iostream>
using namespace std;

int  count = 0;                                                     //计算逆序对数量
/*
*   判断start 和 end之间的元素个数,如果超过一个,则进行递归2分;否则返回return
*   在回溯的过程中,已经知道了start 到 mid 和 mid 到 end之间都是有序的,合并过程
*   相当于合并两个有序数组
*
*   params:
*       A: 要排列的数组
*       start: 开始位置
*       end: 结束位置
*   注意前后都包含
*/
void merge_solve(int* A, int start, int end){
    if(start >= end) return;                                        //只剩下一个元素时

    int s = start;
    int e = end;
    int mid = (start + end) / 2;                                    //中间的元素

    merge_solve(A, s, mid);
    merge_solve(A, mid + 1, e);

    //回溯阶段,两个有序数组合并,需要一个新的数组记住拍好的序之后的值
    int* tmp = new int[end - start + 1];                            //存临时结果的数组
    int j = 0;
    int s2 = mid + 1;

    while(s <= mid && s2 <= e){
        if(A[s] <= A[s2])
            tmp[j++] = A[s++];
        else{
            count += (mid - s + 1);                                 //逆序对数等于前半中剩余元素的个数
            tmp[j++] = A[s2++];
        }
    }

    while(s <= mid)
        tmp[j++] = A[s++];
    while(s2 <= e)
        tmp[j++] = A[s2++];

    //复制数据
    s = start;
    while(s <= e){
        A[s] = tmp[s - start];
        s++;
    }
    delete[] tmp;
}

int main(){
    //int A[12] = {1, 9, 2, 14, 13, 11, 7, 56, 21, 3, 9, 12};
    int A[5] = {2, 4, 1, 3, 5};
    //merge_solve(A, 0, 11);
    merge_solve(A, 0, 4);

    for(int i = 0; i < 5; i++)
        cout<<A[i]<<" ";
    cout<<endl;
    cout<<"逆序对数: "<<count<<endl;

    return 0;
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值