将数组中的数逆序存放_树状数组求解逆序对问题

还记得我们在《归并排序只能用来排序???》中做过的寻找逆序对个数的问题吗?除了使用归并排序来解决外,使用树状数组也是一种可行的方式。接下来我们将详细介绍这种简洁优美的数据结构。

1 什么是树状数组

首先,我们需要了解什么是树状数组以及它用来解决哪种问题。树状数组(Binary Index Tree)也称二叉索引树,一般来说,树状数组支持与线段树相同的两种操作,单点修改和区间查询,时间复杂度均为O(logN):

  • 单点修改:更改数组中一个元素的值
  • 区间查询:查询一个区间内所有元素的聚集信息(如总和、最值、平均值等)

那么,它与线段树究竟有什么区别呢?

2 树状数组的实现

树状数组实际上是一个一维数组,在设计上,它采用了类似于线段树的思想:每个元素代表一个小区间的和单点更新时,只更新包含这个元素的区间,在区间查询时,将对应的小区间进行组合从而得到大区间的和。那么这个小区间是如何确定的呢?

树状数组巧妙的利用了二进制运算,举个例子,11的二进制形式为1011,如果我们要求前11个元素和,可以将其分为0000-10001000-10101010-1011三个小区间,分别求出它们的和后再相加即为前11个元素的和。实际上,类似1000-1010这种小区间的和正是树状数组的元素所代表的。可能有的人已经看出规律了,这不就是不断去掉二进制右侧1的过程吗?8e3ab2bec5368f365b2c8598e8d19a17.png

此时我们不得不介绍一个lowbit函数,它的作用是只保留二进制最右边的1,这个函数的实现如下:

public static int lowbit(int x) {
        return x & (-x);
    }

如6 = 00000110和-6 = 11111010  相与为 00000010 

虽然有点匪夷所思,但x & (-x)确实能够实现这个功能,大家可以用笔来算一算,这属于经典的二进制位运算技巧,类似的还有x & (x-1)可以将二进制的最后一位1置为0,位运算的相关内容会在后续文章中介绍。

所以,介绍到这里,我们应该能自己画一下树状数组的结构图了,假设数组的最后索引为8,所构建的树状数组如下:

b5dc9251aa7f69cb8e7419d72526eab7.png

在上面的介绍中我们可以了解到,树状数组的查询是不断查询(x-lowbit(x), x]区间的和并把它们求和得到前n个元素的和,代码实现如下:

public int query(int x) {
        int ret = 0;
        while (x != 0) {
            ret += tree[x];
            x -= lowbit(x);
        }
        return ret;
    }

那么树状数组如何进行更新操作呢,我们知道,单点更新后需要更新所有包含此元素的区间和,对于特定元素如100110,我们可以找到包含它的区间:

6a6f3169e5abc228c49cd0733f042ccc.png

可以看出,树状数组每次进行更新时,会把右边起连续的1变为0,再把这一系列1的前一位0变为1,这实际上可以看作一个不断进位的过程,每一次所加的数正是lowbit(x)。代码实现如下:

public void update(int x, int d) {
        while (x <= n) {
            tree[x] += d;
            x += lowbit(x);
        }
    }

3 用树状数组解决逆序对问题

我们假设数组中所有元素均在1-10之间,使用一个桶数组来表示原始数组每一个元素出现的次数,如对于原数组a = {4, 5, 2, 3, 6},其桶数组表示如下:

index  ->  1 2 3 4 5 6 7 8 9
value -> 0 1 1 1 1 1 0 0 0

因此,我们可以采用这种方法来求数组的逆序对:反向遍历数组,对遍历到的元素ai,我们把对应的桶自增1,并将i-1位置的前缀和加入到逆序对总数中。为什么这样能够计算逆序对?试想,对于某个元素ai,其i-1位置的前缀和表示的意义是小于ai的所有元素的个数,而这些元素在ai之前进入桶数组,因为我们是从后向前遍历的,所以这些元素在原序列中的位置在ai之后,再加上这些元素比ai小,因此ai与它们构成了逆序对。

以a数组为例,我们未添加元素5时,桶数组的情况如下:

index  ->  1 2 3 4 5 6 7 8 9
value -> 0 1 1 0 0 1 0 0 0

在添加5后,我们计算桶数组前4项的和为2,可以清楚的看到,这两个逆序对代表的是(5,2)和(5,3)。

设数组中元素的值域为size,当元素较为分散时,我们需要开辟的桶空间也会更大,而这个桶数组中很多位置的元素为0,实际上,在逆序对问题中,我们并不关注元素的实际大小,而是关注它们的相对大小,只需要知道哪些数比哪些数大还是小,这就足够了,因此为了节约不必要的空间开销,我们可以对数组元素进行离散化处理。全部代码如下:

class Solution {
    public int reversePairs(int[] nums) {
        Set set = new TreeSet<>();for(int num : nums) set.add(num);int idx = 1;int n = set.size();
        Map map = new HashMap<>();for(int num : set){
            map.put(num, idx++);
        }
        BIT bit = new BIT(n);int ans = 0;for (int i = nums.length - 1; i >= 0; --i) {
            ans += bit.query(map.get(nums[i]) - 1);
            bit.update(map.get(nums[i]),1);
        }return ans;
    }
}class BIT {private int[] tree;private int n;public BIT(int n) {this.n = n;this.tree = new int[n + 1];
    }public static int lowbit(int x) {return x & (-x);
    }public int query(int x) {int ret = 0;while (x != 0) {
            ret += tree[x];
            x -= lowbit(x);
        }return ret;
    }public void update(int x,int d) {while (x <= n) {
            tree[x]+=d;
            x += lowbit(x);
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值