思与行,思考过后的实践非常重要,这是将技能融汇贯通的重要一步。
线段树,光从名字上有些莫名奇妙不知所云。
标准线段树是一种可以在 任意范围上进行 querySum 操作 add 操作 update 操作的时间复杂度都达到 O(logN) 的级别。
为达到这种时间复杂度,实际思想采用了空间换时间的方法,对原本数据集进行分段,维护懒更新数组,懒添加数组,对于范围的增加和更新操作阻塞下发,仅在必要时下发阻塞任务(如更新范围小于阻塞范围时),构建这种逻辑概念上的树型结构(实际实现上未必是树结构)
举个例子,比如我有如下一组数
[1,2,3,4,5,6,7,8]
sum 数组对应会组织成的树结构,表示对应范围的数据总和
1-8
1-4 5-8
1-2 3-4 5-6 7-8
1-1 2-2 3-3 4-4 5-5 6-6 7-7 8-8
最底层是所有的单个数据,父节点是一个范围段,根节点是整个范围 [1,8] 这里为了保持满二叉树会保证 2^n 个节点,那对于元数据集不是 2^n 的情况会进行补充不足的位置补 0 。
我们需要一个结构来存储拓展后的逻辑树型结构,这里采用数组的形式,给定原数组 4 倍的容量,这个容量可能会有空余,为了避免动态计算这里简单选择给定一个冗余空间。在初始化时我们需要构建初始化 sum 数组,这里通过递归的方式进行计算
public class SegmentTree {
private int orgLength;
private int helpLength;
private int[] ogArray;
private int[] sum;
public SegmentTree(int[] arr) {
orgLength = arr.length + 1;
helpLength = orgLength << 2;
// 下标从 1 开始
ogArray = new int[orgLength];
for (int i = 0; i < arr.length; i++) {
ogArray[i + 1] = arr[i];
}
lazyAdd = new int[helpLength];
sum = new int[helpLength];
build(1, 1, orgLength - 1);
}
/**
* @param index 从 index 位置开始构建左右子树的 sum 信息
* @param l 源数组左范围
* @param r 源数组右范围
* @return void
* @description: 对于 i 位置 二分左孩子是 2*i i << 1 二分右孩子是 2*i+1 = i << 1 | 1
*/
public void build(int index, int l, int r) {
if (l > r) {
return;
}
if (l == r) {
sum[index] = ogArray[l];
return;
}
int mid = (l + r) >> 1;
// 递归调用把做孩子和有孩子都构建好
build(index << 1, l, mid);
build(index << 1 | 1, mid + 1, r);
// 构建好后对当前 index 节点进行赋值
sum[index] = sum[index << 1] + sum[index << 1 | 1];
}
}
对于范围添加方法,想要实现 O(logN) 的时间复杂度,需要增加存储结构来实现,这我们增加懒添加数组来实现 add 方法
/**
* @param addL addL-addR 任务的范围
* @param addR
* @param L L-R 表达的范围
* @param R
* @param addNum 在哪个位置上找表达范围
* @return void
* @description: 给 [addL,addR] 范围上的数增加 addNum 个数
*/
private void add(int addL, int addR, int L, int R, int index, int addNum) {
// 如果全包则阻塞记录懒信息,更新当前范围位置 sum 值
if (addL <= L && R <= addR) {
sum[index] += addNum * (R - L + 1);
lazyAdd[index] += addNum;
return;
}
// 如果包不住,需要分隔下发
int mid = (L + R) >> 1;
// 分发前先将当前的懒任务下发下去
pushDownLazyTask(index, mid - L + 1, R - mid);
if (addL <= mid) {
add(addL, addR, L, mid, index << 1, addNum);
}
if (addR > mid) {
add(addL, addR, mid + 1, R, index << 1 | 1, addNum);
}
// 添加后更新当前节点懒信息
sum[index] = sum[index << 1] + sum[index << 1 | 1];
}
private void pushDownLazyTask(int index, int leftRange, int rightRange) {
if (lazyAdd[index] != 0) {
int lazyNum = lazyAdd[index];
lazyAdd[index] = 0;
lazyAdd[index << 1] += lazyNum;
lazyAdd[index << 1 | 1] += lazyNum;
sum[index << 1] += leftRange * lazyNum;
sum[index << 1 | 1] += rightRange * lazyNum;
}
}
这里 add 方法我们可以写一个重载方法,便于简单调用
public void add(int l, int r, int addNum) {
add(l, r, 1, orgLength - 1, 1, addNum);
}
到这里我们已经处理好了初始化以及添加方法,接下来实现范围的 query 方法
public int query(int queryL, int queryR) {
return query(queryL, queryR, 1, orgLength - 1, 1);
}
/**
* @param queryL [queryL,queryR] 要查询的范围
* @param queryR
* @param chooseL [chooseL,chooseR] 当前选择的表达范围(扩充后线段树数组范围)
* @param chooseR
* @param chooseIndex 表达范围的 index
* @return long
* @description:
*/
private int query(int queryL, int queryR, int chooseL, int chooseR, int chooseIndex) {
if (queryL <= chooseL && queryR >= chooseR) {
return sum[chooseIndex];
}
// 1,2,3,4,5,6,7,8
// 1+7 >> 1 = 4 为啥 queryL 是小于呢,因为左半边是 1-4 右半边是 5-8 右半边得 mid+1
int mid = (chooseL + chooseR) >> 1;
// 当前节点的数据不全包命中,则需要尝试将懒任务下发,不下发则会导致最终数据不准
// 4-1 +1 = 4 8-4 = 4
pushDownLazyTask(chooseIndex, mid - chooseL + 1, chooseR - mid);
int ans = 0;
if (queryL <= mid) {
ans += query(queryL, queryR, chooseL, mid, chooseIndex << 1);
}
if (queryR > mid) {
ans += query(queryL, queryR, mid + 1, chooseR, chooseIndex << 1 | 1);
}
return ans;
}
到此为止通过借助 sum 数组,lazy 数组就已经实现了在范围内统一增加一个数值,查询范围总和的时间复杂度为 O(logN) 了,接下来要引入更新操作,对于更新操作我们需要再借助两个数组来维护更新的懒操作。