【算法】逆序对问题的四种解法(归并排序,BST,树状数组,线段树)及变形

引发我对逆序对这个问题思考的源自这道题:315. Count of Smaller Numbers After Self这道题要找出某个元素之后出现的比这个元素小的元素个数:ex. Given[5, 2, 6, 1], return [2, 1, 1, 0]

这道题只是求逆序对个数的另外一个变形而已,所以这篇就着重说一下数组中逆序对个数的四种求法。

归并方法

以前我以为求一个序列逆序对个数的解法就是归并排序的改版。归并解法:
因为要求每个元素的逆序对个数,所以一下解法的相当一部分花在来找原数组的对应关系上面。

vector<int> res;
void mergesort(vector<II>& nums,int left,int right){
    if(left>= right)
        return;
    int mid = (left+right)/2;
    mergesort(nums,left,mid);
    mergesort(nums,mid+1,right);
    vector<II> leftnums(mid-left+1),rightnums(right-mid);

    for(int i = left;i<=mid;i++)
        leftnums[i-left] = nums[i];
    for(int i = mid+1;i<=right;i++)
        rightnums[i-mid-1] = nums[i];
    leftnums.push_back(II(INT_MAX,-1));
    rightnums.push_back(II(INT_MAX,-1));
    int i = 0,j=0,k = left;
    int mark = 0;
    while(i<leftnums.size()-1||j<rightnums.size()-1){
        if(leftnums[i]<=rightnums[j]){
            res[leftnums[i].second] += mark;
            nums[k++] = leftnums[i++];
        }else{
            mark++;
            nums[k++] = rightnums[j++];
        }
    }
    return;
}
vector<int> countSmaller(vector<int>& nums) {
    res = vector<int>(nums.size(),0);
    vector<II> new_nums(nums.size());
    for(int i = 0;i<nums.size();i++)
        new_nums[i] = II(nums[i],i);
    mergesort(new_nums,0,nums.size()-1);
    return res;
}

BST(二叉搜索树)

先说一个额外的变形题型:

《程序员面试金典》上面的题目:P77 11.8题:读取一串整数流,要求尽可能短的时间内,返回已读取的数字流中,比x小的元素个数。设计
void add(int x)和int getRankOfNumber(int x)两个函数接口。

如果不是一个数字流的话,就直接将这个数组排序,然后每个元素排名出现的位置,就是整个数组中比其小的元素个数。
但是如果处在流中,则每次排序花费太大,此时需要一个数组结构能够完成这个工作。所以此时就想到二叉树是一个非常好的数据结构。【一般需要用到数据结构的问题中,就想一下:堆,二叉树,栈,队列这四个方面想就八九不离十了】
所以弄一个BST,每个节点中额外记录一下,这个节点左子树的元素个数LeftNum。

getRankOfNumber(int x)函数
就沿着BST数去查找这个元素所在的数节点,在查找的途中,如果要往左子树查找的话,就不做改变,如果要往右子树查找的话,即当前节点和其左子树所有节点都比它小,所以res
+= LeftNum+1

add(int x)函数
就是一个经典的BST插入,不过当其要插入当前节点的左子树节点中时,就将当前节点的LeftNum值加一即可。

再回到这道题本身,其实当时第一眼看到这道题的时候,我并没有往逆序对这种情况上去想,而是直接就参照上面那题的思路用二叉树去做了,二叉树的解法就是,从右往左一次扫描这个二叉树,进行建树,在插入节点的时候,就直接找出当前情况下它的rank值存到数组中即可。
我的代码如下:

struct TreeNode {
    int val;
    int count;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x,int c):val(x),count(c),left(NULL), right(NULL) {}
};
vector<int> countSmaller(vector<int>& nums) {
    vector<int> res(nums.size(),0);
    if(nums.size() == 0)
        return res;
    TreeNode *root = new TreeNode(nums[nums.size()-1],0);
    res[nums.size()-1] = 0;
    for(int i = nums.size()-2;i>=0;i--){
        TreeNode *father ;
        TreeNode *now = root;
        int sum = 0;
        while(now != NULL){
            father = now;
    //对于这种判断大小的问题,一定要注意等于的情况怎么处理,这道题中当其相等的话,就将其放到左子树中,是一个合理的做法。因为往右边走的话,需要加上这个数本身的所有左节点个数,与题意不符。
            if(nums[i]<=now->val){
                now->count++;
                now = now->left;
                if(now == NULL){
                    father->left = new TreeNode(nums[i],0);
                    break;
                }
            }else{
                sum += now->count+1;
                now = now->right;
                if(now == NULL){
                    father->right = new TreeNode(nums[i],0);
                    break;
                }
            }
        }
        res[i] = sum;

    }
    return res;
}

一如上面注释中所说的,其实这种比较大小的,或者涉及到类似于排序类的问题,一定要注意等于的情况怎么处理的。之前一篇Blog:快速排序程序及易错点总结提到的也是等于情况的处理问题。

树状数组

对于树状数组我看的一个还不错的参考资料:
数据结构之树状数组是北理ACM的课件
简单来说树状数组是一个数据结构,它能高效( O(logN) )的复杂度下,完成计算a[i~j]元素和的任务。所以先将原数组给离散化成1~n之间的数,然后将其从后到前,依次放到这个树状数组中,然后每次放进去之前,先计算从1~n中有多少个元素了。

int lowbit(int x){  return x&(-x);}
int getsum(int x){
    int sum = 0;
    while(x>0){
        sum += c[x];
        x = x-lowbit(x);
    }
    return sum;
}
void update(int x,int val){
    while(x<c.size()){
        c[x] += val;
        x = x+lowbit(x);
    }
}
vector<int> countSmaller(vector<int>& nums) {
    vector<int> res = vector<int>(nums.size(),0);
    vector<II> new_nums(nums.size());
    vector<int> n(nums.size());
    c = vector<int>(nums.size()+1,0);
    for(int i = 0 ;i<nums.size();i++)
        new_nums[i] = II(nums[i],i);
    //注意这个地方的sort能够处理重复元素的情况,因为pair进行比较的时候是先比的first再比的second,所以此时即使5,5,5这样的序列,在排序的时候稳定性是不变的,最后结果会是{1,2,3}这样的大小顺序
    sort(new_nums.begin(),new_nums.end());
    for(int i = 0;i<nums.size();i++)
        n[new_nums[i].second] = i+1;
    for(int i = n.size()-1;i>=0;i--){
        res[i] = getsum(n[i]);
        update(n[i],1);
    }
    return res;
}

今天看了一个大神的解法,更是惊为天人,直接用map/unordered_map去做的这道题,就免去了序列化的步骤了。

    unordered_map<long long,int> c;

    long long int lowbit(long long int x){  return x&(-x);}
    int getsum(long long int x){
        int sum = 0;
        //因为x可能为负值所以需要将其加上2^32
        x += (1ll<<32);
        while(x>0){
            sum += c[x];
            x = x-lowbit(x);
        }
        return sum;
    }
    void update(long long int x,int val){
        x += (1ll<<32);
        //其实此处最好的办法是x < (1ll<<32)+max(nums)
        while(x<(1ll<<34)){
            c[x] += val;
            x = x+lowbit(x);
        }
    }
    vector<int> countSmaller(vector<int>& nums) {
        vector<int> res = vector<int>(nums.size(),0);
        for(int i = nums.size()-1;i>=0;i--){
            //注意这个地方找nums[i]-1ll来去处理相等的情况,实在是精妙啊
            res[i] = getsum((long long)nums[i]-1ll);
            update(nums[i],1);
        }
        return res;
    }

上面的写法更加简洁,因为不用序列化了。但是其实时间复杂度会更高一些。(当然如果nums里面的元素足够的多,上面这种方法反而会更加快速)
其实呢,又做了下面那道题493. Reverse Pairs,这道题用离散化的方法就不行了,因为要统计的是nums[i]<2*nums[j]的个数,如果再做离散化的话其序关系会发生变化的。

线段树解法

线段树我也是第一次写,中间写的也是感觉坑点多多,详细的见我下篇博文吧。这个只记录一下程序怎么写。

typedef pair<int, int> II;
struct segtree{
    int left;
    int right;
    int count;
    segtree() :left(0), right(0), count(0){
    }
};
vector<segtree> tree;
void buildtree(int root, int left, int right){
    tree[root].left = left;
    tree[root].right = right;
    tree[root].count = 0;
    if (left == right)
        return;
    int mid = (left + right) >> 1;
    buildtree(root << 1, left, mid);
    buildtree(root << 1 |1, mid + 1, right);
}
void update(int left, int right, int root, int value){
    tree[root].count += value;
    if (tree[root].left == tree[root].right)
        return;
    int mid = (tree[root].left + tree[root].right) >> 1;
    if (right <= mid)
        update(left, right, root << 1, value);
    else
        if (left>mid)
            update(left, right, root << 1|1, value);
        else{
            update(left, mid, root << 1, value);
            update(mid + 1, right, root << 1 |1, value);
        }

}
int query(int left, int right, int root){
    if (left == tree[root].left&&right == tree[root].right)
        return tree[root].count;
    int mid = (tree[root].left + tree[root].right) >> 1;
    if (right <= mid)
        return query(left, right, root << 1);
    else
        if (left >= mid)
            return query(left, right, root << 1| 1);
        else{
            return query(left, mid, root << 1) + query(mid + 1, right, root << 1 |1);
        }

}
vector<int> countSmaller(vector<int>& nums) {
    //前面还是跟树状数组一样,是离散化部分,只不过它需要判断nums为0的情况
    //因为线段树需要自始至终left都小于right
    vector<int> res = vector<int>(nums.size(), 0);
    if(nums.size()==0)
        return res;
    vector<II> new_nums(nums.size());
    vector<int> n(nums.size());
    for (int i = 0; i<nums.size(); i++){
        new_nums[i] = II(nums[i], i);
    }
    sort(new_nums.begin(), new_nums.end());
    for (int i = 0; i<nums.size(); i++)
        n[new_nums[i].second] = i + 1;
    //注意线段树一定要开刀4N的大小,否则会有数组越界的情况
    tree = vector<segtree>(nums.size() << 2);
    buildtree(1, 1, nums.size());
    for (int i = n.size() - 1; i >= 0; i--){
        res[i] = query(1, n[i], 1);
        update(n[i], n[i], 1, 1);
    }
    return res;
}

逆序对问题变形

补充,今天早上LeetCode周周赛又有一道这样的变种题:493. Reverse Pairs链接还没出,等到时候再补上。这道题跟上道题类似,找出这个数组中i<jnums[i]>2*nums[j]的元素总个数。

先说一下我的思路,我是直接套用315题的解法,只不过需要分成两个步骤,先是去查找按照nums[i]<2*val 的判断条件去查找,还是按照上面的方法,左子树走的话,不变,往右子树走的话res += LeftNum + 1。找完之后在进行插入操作,插入就再按照nums[i]<val的情况进行插入操作即可。
另外这题有一个坑点的地方在于会有INT_MAX的数出现,所以需要用到long long int 的类型,防止其乘二之后溢出。
然后看了第一名的大神用了个amazing的解法,即树状数组。大神方法如下,瞻仰一下:

class Solution {
public:
    map<long long, int> bit;
    long long lowbit(long long x)
    {
        return x & (-x);
    }
    void add(long long x)
    {
    //加34是为了处理负数情况
        for(x += (1ll << 34); x <= (1ll << 36); x += lowbit(x))
            ++bit[x];
    }
    int que(long long x)
    {
        int ans = 0;
        for(x += (1ll << 34); x; x -= lowbit(x))
            ans += bit[x];
        return ans;
    }
    int reversePairs(vector<int>& nums) {
        bit.clear();
        int n = nums.size();
        int ans = 0;
        for(int i = n - 1; i >= 0; --i)
        {
            ans += que((long long) nums[i] - 1ll);
            add((long long) nums[i] * 2ll);
        }
        return ans;
    }
};

看北理那个课件,也出现了一个poj上的题,有n个坐标点 (x,y) 让你求出每个坐标点左下角的元素个数。

这道题呢,表面上看上去与求逆序对无关不过需要再仔细想想,这个不就相当于一个二维尺度上的求找出小于一个数的全部数字吗。
先将所有 左边按照y值大小从小到大排序,然后按照x来找逆序对。

    原文作者:排序算法
    原文地址: https://blog.csdn.net/haolexiao/article/details/54989306
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞