一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情。
树状数组 和 线段树
树状数组能解决的问题,线段树都能解决;但是线段树能解决的问题,树状数组不一定能解决;那么为什么还要用树状数组呢,直接都用线段树就行了呀。但是我们需要综合考虑,因为线段树代码很长,而且常数很大,实际表现不算很好,我们只有在不得不用的时候才考虑线段树。而树状数组的常数小,代码简洁,不易出错。
树状数组的时间复杂度
- 单点修改:
- 区间查询:
- n次操作总时间复杂度为:
前置概念
lowbit(x):表示求x在二进制表示下最低位的1及其后面的0所构成的数值。例如:- lowbit(44) = lowbit((101100)₂) = (100)₂ = 4
- lowbit(56) = lowbit((111000)₂) = (1000)₂ = 8
- lowbit(9) = lowbit((1001)₂) = (1)₂ = 1
lowbit(x)的代码实现
int lowbit(int x){
return x & (-x);
}
- 为什么是
x & (-x)呢?
这是因为在计算机中数据的存储是以补码的形式来进行存储的。- 正数的原码 = 反码 = 补码;
- 负数的反码 = 负数的原码符除符号位外按位取反;
- 负数的补码 = 负数的反码加1(也就是负数的原码除了符号位外按位取反后加1)
- 己有原码, 为何还有反码和补码?
这是因为计算机为了方便计算负数所引入的。
这里需要注意的是负数的补码是除了符号位按位取反后加1
这里以44为例:lowbit(44) = lowbit((101100)₂) = (100)₂ = 4
+44的原码=反码=补码,所以在计算机中存储为0010 1100(注意这里最高位为符号位0表示正数)
-44的原码为1010 1100(注意这里的最高位为符号位1表示负数),那么反码为原码除了符号位以外按位取反得到1101 0011,补码为反码加一得1101 0100,那么将+44的补码和-44的补码进行按位与操作0010 1100 & 1101 0100 = (100)₂ = 4
这样就可以得到44的最低位的1及其后面的0所构成的数值,也就是lowbit的值。
核心思想
树状数组的核心思想就是为了平衡单点修改和区间查询的时间复杂度所以采用树形结构存储数据,而难点就在于对这棵树每个节点的理解以及对lowbit规律的使用。思想核心也在于理解lowbit()的操作规律,也就是x ± lowbit(x)的规律。
由图我们可以观察得知:
t[x]覆盖的节点长度就是lowbit(x);t[x]的父节点就是t[x + lowbit(x)];- 整棵树的深度位
log(n) + 1
单点修改
对于单点修改操作,我们除了对该节点进行修改以外,还需要对它的父亲节点进行修改,这里通过规律可以发现
x的父亲节点即为x + lowbit(x),那么我们可以通过这个规律不断寻找x的父亲节点进行修改。
单点修改代码实现
void updata(int x, int k){
x++;
while(x <= n){
t[x] += k;
x += lowbit(x);
}
}
这里需要注意的是x++和x <= n
x++和x <= n是因为传入的x为原数组下标(假设原数组是从下标0开始存放的),而我们树状数组的t[]是从1开始存放的。
区间查询
对于区间查询操作(其实树状数组的区间查询结果是前缀和),由图我们可以发现规律,每次
x只需要减去lowbit(x)就可以得到前面的一段和,且通过不断进行x - lowbit(x)操作可以求出其前缀和,且不重不漏的包含了所有值。
如果需要求
[l, r]区间和,我们可以通过前缀和相减得到。getsum(r) - getsum(l - 1)
单点修改代码实现
int getsum(int x){
int res = 0;
x++;
while(x > 0){
res += t[x];
x -= lowbit(x);
}
return res;
}
这里需要注意的是x++和x>0
x++和x > 0是因为传入的x为原数组下标(假设原数组是从下标0开始存放的),而我们树状数组的t[]是从1开始存放的。
总结
树状数组其实并不难,本身是一个很简单的数据结构,但是要搞懂其为什么可以这么修改和查询还是比较困难的,这是需要从「二进制分解」进行出发理解,核心在于理解lowbit()的操作规律,也就是x ± lowbit(x)的规律。
结束语
好的生活不是拼命透支,而是款款而行。当我们被欲望追赶,步子迈得太快,就容易丧失自我。懂得给欲望做减法,学会与内心和平相处,坚守一份清醒与自持,保持自己的步调,才是真正的内心强大。