还记得我们在《归并排序只能用来排序???》中做过的寻找逆序对个数的问题吗?除了使用归并排序来解决外,使用树状数组也是一种可行的方式。接下来我们将详细介绍这种简洁优美的数据结构。
1 什么是树状数组
首先,我们需要了解什么是树状数组以及它用来解决哪种问题。树状数组(Binary Index Tree)
也称二叉索引树,一般来说,树状数组支持与线段树相同的两种操作,单点修改和区间查询,时间复杂度均为O(logN)
:
- 单点修改:更改数组中一个元素的值
- 区间查询:查询一个区间内所有元素的聚集信息(如总和、最值、平均值等)
那么,它与线段树究竟有什么区别呢?
2 树状数组的实现
树状数组实际上是一个一维数组,在设计上,它采用了类似于线段树的思想:每个元素代表一个小区间的和,单点更新时,只更新包含这个元素的区间,在区间查询时,将对应的小区间进行组合从而得到大区间的和。那么这个小区间是如何确定的呢?
树状数组巧妙的利用了二进制运算,举个例子,11的二进制形式为1011
,如果我们要求前11个元素和,可以将其分为0000-1000
,1000-1010
,1010-1011
三个小区间,分别求出它们的和后再相加即为前11个元素的和。实际上,类似1000-1010
这种小区间的和正是树状数组的元素所代表的。可能有的人已经看出规律了,这不就是不断去掉二进制右侧1的过程吗?
此时我们不得不介绍一个lowbit函数,它的作用是只保留二进制最右边的1,这个函数的实现如下:
public static int lowbit(int x) {
return x & (-x);
}
如6 = 00000110和-6 = 11111010 相与为 00000010
虽然有点匪夷所思,但x & (-x)确实能够实现这个功能,大家可以用笔来算一算,这属于经典的二进制位运算技巧,类似的还有x & (x-1)可以将二进制的最后一位1置为0,位运算的相关内容会在后续文章中介绍。
所以,介绍到这里,我们应该能自己画一下树状数组的结构图了,假设数组的最后索引为8,所构建的树状数组如下:
在上面的介绍中我们可以了解到,树状数组的查询是不断查询(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,我们可以找到包含它的区间:
可以看出,树状数组每次进行更新时,会把右边起连续的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);
}
}
}