引入
线段树和树状数组是两个十分相似的数据结构,它们能使对一个区间的数据修改以及查询的速度提升许多。两个结构本质相同,但是各有优缺点,下面我们实现它们的单点修改,单点查询,区间修改,区间查询。
线段树
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,实际应用时一般还要开4N的数组以免越界,因此有时需要离散化让空间压缩。
树状数组
树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree)是一个查询和修改复杂度都为log(n)的数据结构。主要用于查询任意两位之间的所有元素之和,但是每次只能修改一个元素的值;经过简单修改可以在log(n)的复杂度下进行范围修改,但是这时只能查询其中一个元素的值(如果加入多个辅助数组则可以实现区间修改与区间查询)。
线段树
- 线段树的构造
因为树状数组不需要构造过程,下面谈一谈线段树的构造。
使用递归,代码如下:
void build(int left, int right, int index)
{
tree[index].left = left;
tree[index].right = right;
if (left == right) return;
int mid = (left + right) / 2;
build(left, mid, index * 2);
build(mid + 1, right, index * 2 + 1);
}
- 线段树单点修改
单点修改就是每遍历到一个节点,如果这个节点代表着的区间包括这个点,那么就修改它。
// 第dis个数加上k
void add(int index, int dis, int k)
{
if (tree[index].left == tree[index].right)
{
tree[index].sum += k;
return ;
}
if (dis <= tree[index].r)
add(index * 2, dis, k);
else
add(index * 2 + 1, dis, k);
tree[index].sum = tree[index * 2].sum + tree[index * 2 + 1].sum;
}
- 线段树区间修改
① 如果当前区间全部在要加的区间范围之内,那么对区间,也就是该节点修改, 并return。
② 如果当前区间的right>=目标区间的left,说明这两个区间有交集,那么进入这个区间。
③ 如果当前区间的left<=目标区间的right,说明这两个区间有交集,那么进入这个区间。
// 区间[left, right]数据加上k
void add(int index, int left, int right, int k)
{
if (left <= tree[index].left && tree[index].right <= right)
{
tree[index].sum += k;
return ;
}
if (tree[index * 2].right >= left)
add(index * 2, left, right, k);
if (tree[index * 2 + 1].left <= right)
add(index * 2 + 1, left, right, k);
}
- 线段树区间查询
同修改
// 区间查询
void search(int index, int left, int right)
{
if (left <= tree[index].left && tree[index].right <= right)
{
ans += tree[index].num;
return ;
}
if (tree[index * 2].right >= left)
search(index * 2, left, right);
if (tree[index * 2 + 1].left <= right)
search(index * 2 + 1, left, right);
}
- 线段树单点查询
从根节点,一直搜索到目标节点,然后一路上都加上就好了。
// 单点查询
void search(int index, int dis)
{
ans += tree[index].num;
if(tree[index].left == tree[index].right)
return ;
if(dis <= tree[index * 2].right)
search(index* 2, dis);
if(dis >= tree[index * 2 + 1].left)
search(index * 2 + 1, dis);
}
- lowbit
lowbit这个东西很神奇,它可以将一个二进制数的所有高位一都去掉,只留下最低位的1,比如lowbit(5) =0001(即0101->0001)。
int lowbit(int k)
{
return k & -k;
}
- 树状数组单点修改
如果我们要修改某个数 x 的值,只要加上lowbit(x),直到等于 n.。举个例子:总共有8个数第2个数要加上 k,具体过程如下:
① c[0010] += k;
② c[0010 + 0010] (c[0100]) += k;
③ c[0100+0100] (c[1000]) += k;
这样我们能维护整个树状数组了。
// 单点修改
void add(int x, int k)
{
while (x <= n)
{
tree[x] += k;
x += lowbit(x);
}
}
- 树状数组区间修改
如果我们要在 x 到 y 区间加上一个 k,可以分两步做。
① x 到 n 都加上一个 k。
② y +1 到 n 加上一个 -k。
// 区间修改
add(x, k);
add(y + 1, k);
- 树状数组区间查询
其实就是求前缀和,比如我们要求 x 到 y 区间的和,那么就将从 1 到 y 的和减去从 1 到 x 的和。方法是这样的,比如求从 1 到 y 的和,先将 y 转为二进制,然后一直减去 lowbit(y),直到 y 等于0。下面用具体数据:
如求1到7的和,应该是这样子的:
ans += c[0111];//start
ans += c[0111 - 0001];
ans += c[0110 - 0010];
ans += c[0100 - 0100];// end
// 区间查询
int sum(int x)
{
int ans = 0;
while(x != 0)
{
ans += tree[x];
x -= lowbit(x);
}
return ans;
}
res = sum(y) - sum(x - 1);// 区间[x, y]的和
- 树状数组单点查询
从 x 开始,不断进行 x -= lowbit(x),ans += tree[x],直到x 等于 0.
int search(int x)
{
int ans = 0;
while(x != 0)
{
ans += tree[x];
x -= lowbit(x);
}
return ans;
}