线段树不是一颗二叉树,而是利用树的思想的一个数组。
线段树常常用来维护区间信息,可以在O(logN)的时间复杂度内完成单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作。
假设我们有一个长度为5的数组a : {10 ,11 , 12 , 13, 14},要将其转换成线段树,会是什么样子呢?
如图便是我们得到的线段树的样子。我们可以从中发现,这棵树的每一个节点都存储了一个区间内的求和信息。如d[4],存储的就是下表为0,1的元素10+11的和,即为21。并且我们还能观察到,每个节点的左字节点的序号是父节点的2倍,同时右子节点的序号则是2倍+1。据此,我们可以将一个区间划分成一颗线段树了。但是没个区间长度该怎么划分呢?很简单,一半一半。
左子节点区间是父节点区间[s,t]的[s,(s+t)/2],右子节点区间是父节点的[(s+t)/2+1,t]。直到区间长度为1为止。据此,我们实现了一个数组的线段树的构建。
线段树的构建:
有了线段树,我们就可以查询区间信息,修改单点或者区间的值。下面来看看代码怎么实现构建线段树吧。
void build(int s, int t, int p) {
// 对 [s,t] 区间建立线段树,当前根的编号为 p
if (s == t) {
d[p] = a[s];
return;
}
int m = s + ((t - s) >> 1);
// 移位运算符的优先级小于加减法,所以加上括号
// 如果写成 (s + t) >> 1 可能会超出 int 范围
build(s, m, p * 2), build(m + 1, t, p * 2 + 1);
// 递归对左右区间建树
d[p] = d[p * 2] + d[(p * 2) + 1];
}
如果区间长度为1,递归返回。 否则我们分别对两边进行递归。同时别忘记在递归的过程中记录信息:d[p] = d[p * 2] + d[(p * 2) + 1];
线段树的查询:
如果我们需要查询一个区间内元素的和,我们可能有以下情况:
1️⃣ 查询的区间在线段树内存在,如查询[1,5]的和,直接返回d[0]即可
2️⃣查询的区间在线段树内不直接存在,如查询[3,5]的和,我们就需要分两段来查询。
首先我们需要查询[3,3]的值,然后我们需要得知[4,5]的值,将两个区间的和相加即可得到最终结果。
在代码实现上面,我们是这样做的:
int getsum(int l, int r, int s, int t, int p) {
// [l, r] 为查询区间, [s, t] 为当前节点包含的区间, p 为当前节点的编号
if (l <= s && t <= r)
return d[p]; // 当前区间为询问区间的子集时直接返回当前区间的和
int m = s + ((t - s) >> 1), sum = 0;
if (l <= m) sum += getsum(l, r, s, m, p * 2);
// 如果左儿子代表的区间 [s, m] 与询问区间有交集, 则递归查询左儿子
if (r > m) sum += getsum(l, r, m + 1, t, p * 2 + 1);
// 如果右儿子代表的区间 [m + 1, t] 与询问区间有交集, 则递归查询右儿子
return sum;
}
首先如果当前的区间在目标区间当中,我们直接将当前值返回。
否则我们将当前区间一分为2,如果左区间与目标区间有交集,我们直接将左区间递归查询,将sum值不断累加,直到区间信息完全在内。
如果右区间与目标区间有交集,我们直接将右区间递归查询,同理。注意此处左区间每次递归时节点编号都是直接乘2,右区间则是乘2+1.
线段树的区间修改与懒惰标记
当我们需要对于线段树的一个区间内的信息进行修改时,我们会发现这样做的时间复杂度显然会比较高。因此我们需要引入懒惰标记。
什么是懒惰标记呢?简言之,就是先记上帐,后面再去处理。
原本的线段树长这样,我们给[3,5]区间内的元素都加5,显然我们需要给[3,3] , [4,5]都进行加操作。我们直接在两个节点上操作,打上标记。
至于他们的儿子们吗,就慢慢等待以后的更新了。
在后面的操作中,如查询[4,4]区间的值的过程中,我们会先找到[3,5]。但是还带着标记呢,我们就在这时把标记下放,变成了:
代码实现(有标记的前提下):
这个update方法的作用是下放区间的值。第一个if在添加标记值的同时,将区间长度内的所有元素加上了标记值。
void update(int l, int r, int c, int s, int t, int p) {
// [l, r] 为修改区间, c 为被修改的元素的变化量, [s, t] 为当前节点包含的区间, p
// 为当前节点的编号
if (l <= s && t <= r) {
d[p] += (t - s + 1) * c, b[p] += c;
return;
} // 当前区间为修改区间的子集时直接修改当前节点的值,然后打标记,结束修改
int m = s + ((t - s) >> 1);
if (b[p] && s != t) {
// 如果当前节点的懒标记非空,则更新当前节点两个子节点的值和懒标记值
d[p * 2] += b[p] * (m - s + 1), d[p * 2 + 1] += b[p] * (t - m);
b[p * 2] += b[p], b[p * 2 + 1] += b[p]; // 将标记下传给子节点
b[p] = 0; // 清空当前节点的标记
}
if (l <= m) update(l, r, c, s, m, p * 2);
if (r > m) update(l, r, c, m + 1, t, p * 2 + 1);
d[p] = d[p * 2] + d[p * 2 + 1];
}
下面的操作则是对左右子节点进行更新的操作。其中if (b[p] && s != t)则是判断[s,t]区间是否有子节点,同时标记值是否有效(不是0)
区间求和:
int getsum(int l, int r, int s, int t, int p) {
// [l, r] 为查询区间, [s, t] 为当前节点包含的区间, p 为当前节点的编号
if (l <= s && t <= r) return d[p];
// 当前区间为询问区间的子集时直接返回当前区间的和
int m = s + ((t - s) >> 1);
if (b[p]) {
// 如果当前节点的懒标记非空,则更新当前节点两个子节点的值和懒标记值
d[p * 2] += b[p] * (m - s + 1), d[p * 2 + 1] += b[p] * (t - m);
b[p * 2] += b[p], b[p * 2 + 1] += b[p]; // 将标记下传给子节点
b[p] = 0; // 清空当前节点的标记
}
int sum = 0;
if (l <= m) sum = getsum(l, r, s, m, p * 2);
if (r > m) sum += getsum(l, r, m + 1, t, p * 2 + 1);
return sum;
}
如果你是要实现区间修改为某一个值而不是加上某一个值的话,代码如下:
void update(int l, int r, int c, int s, int t, int p) {
if (l <= s && t <= r) {
d[p] = (t - s + 1) * c, b[p] = c;
return;
}
int m = s + ((t - s) >> 1);
// 额外数组储存是否修改值
if (v[p]) {
d[p * 2] = b[p] * (m - s + 1), d[p * 2 + 1] = b[p] * (t - m);
b[p * 2] = b[p * 2 + 1] = b[p];
v[p * 2] = v[p * 2 + 1] = 1;
v[p] = 0;
}
if (l <= m) update(l, r, c, s, m, p * 2);
if (r > m) update(l, r, c, m + 1, t, p * 2 + 1);
d[p] = d[p * 2] + d[p * 2 + 1];
}
int getsum(int l, int r, int s, int t, int p) {
if (l <= s && t <= r) return d[p];
int m = s + ((t - s) >> 1);
if (v[p]) {
d[p * 2] = b[p] * (m - s + 1), d[p * 2 + 1] = b[p] * (t - m);
b[p * 2] = b[p * 2 + 1] = b[p];
v[p * 2] = v[p * 2 + 1] = 1;
v[p] = 0;
}
int sum = 0;
if (l <= m) sum = getsum(l, r, s, m, p * 2);
if (r > m) sum += getsum(l, r, m + 1, t, p * 2 + 1);
return sum;
}