线段树要解决的问题是: 在一个实时更新的动态数组中查询区间和(或广义的区间状态,如区间积,区间最大最小值).
若所查询的数组为静态数组,则此问题只需要使用数组前缀和
即能解决.
线段树是一种平衡二叉树,其每个节点存储数组中一段区间和
.其左子节点
存储区间左半部分和
;其右子节点
存储区间右半部分和
.
线段树是一个平衡二叉树,可以像完全二叉树那样用数组存储.例如: 将数组[0,1,2,3,4,5]
保存在线段树数组中,树的结构如下:
线段树数组的内容为[15, 3, 12, 1, 2, 7, 5, 0, 1, 0, 0, 3, 4]
,注意到数组中对应完全二叉树的下标9
和下标10
的节点不存在.
为了便于下标计算,我们的实际线段树数组下标是从1开始的,0位被浪费了.
线段树的递归构造: 若某节点存储原数组nums[leftIndex] ~ nums[rightIndex]
区间数组和,其左子节点存储原数组nums[leftIndex] ~ nums[midIndex]
区间数组和,其右子节点存储原数组nums[midIndex+1] ~ nums[rightIndex]
区间数组和.
线段树开辟的数组长度应为原数组长度的四倍,证明如下:
若原数组nums
的长度为N
,线段树深度H
应为 ⌈ l o g 2 N + 1 ⌉ \lceil log_{2}N+1\rceil ⌈log2N+1⌉,则该完全二叉树的数组长度应为 2 H + 1 ⩽ 4 N 2^{H+1}\leqslant4N 2H+1⩽4N.
private int[] segmentTree; // 线段树的结构
// 构造线段树
private void buildSegmentTree(int[] nums) {
segmentTree = new int[4 * nums.length]; // 线段树数组长度要开到原数组的4倍
buildSegmentTree(nums, 1, 0, nums.length - 1);
}
// 构造线段树上的segment[pos]节点,该节点存储原nums数组[leftIndex,rightIndex]部分的数组和
private int buildSegmentTree(int[] nums, int pos, int leftIndex, int rightIndex) {
if (leftIndex == rightIndex) {
// 若该节点存储原数组区间范围内只有一个节点,则其没有子节点
segmentTree[pos] = nums[leftIndex];
} else {
// 若该节点存储原数组区间范围内有多于一个节点,则递归创建其左右节点
// 其左子节点存储原该节点存储原nums数组[leftIndex,midIndex]部分的数组和
// 其左子节点存储原该节点存储原nums数组[midIndex+1,rightIndex]部分的数组和
int midIndex = (leftIndex + rightIndex) / 2;
segmentTree[pos] = buildSegmentTree(nums, pos * 2, leftIndex, midIndex)
+ buildSegmentTree(nums, pos * 2 + 1, midIndex + 1, rightIndex);
}
// 返回该节点对应的区间和
return segmentTree[pos];
}
// 打印整颗线段树
public void printSegmentTree() {
printSegmentTree(1, 0, nums.length, 0);
}
// 先序遍历打印线段树第pos位为树根的子树,该节点存储原nums数组leftIndex至rightIndex部分的数组和
private void printSegmentTree(int pos, int leftIndex, int rightIndex, int layer) {
// 显示层数
for (int i = 0; i < layer; i++) {
System.out.print("---");
}
// 显示树根节点
System.out.println("[" + leftIndex + " " + rightIndex + "]: " + segmentTree[pos]);
// 若存在子树,则递归打印左右子树
if (leftIndex < rightIndex) {
int midIndex = (leftIndex + rightIndex) / 2;
printSegmentTree(pos * 2, leftIndex, midIndex, layer + 1);
printSegmentTree(pos * 2 + 1, midIndex + 1, rightIndex, layer + 1);
}
}
打印结果如下:
区间和的查询应用二分思想,不断将当前区间二分直到当前区间全部位于查询区间内,这时将该节点所存储的区间和
与其它所有位于查询区间内的子区间和
相加得到总的查询区间和
.
// 查询原sum数组qleftIndex到qrightIndex部分数组和
public int sumRange(int qleft, int qright) {
return sumRange(1, 0, nodeNum - 1, qleft, qright);
}
// 在 `线段树数组中代表原sum数组[leftIndex,rightIndex]区间内` 二分查询 `原sum数组[qleftIndex,qrightIndex]区间和`
private int sumRange(int pos, int leftIndex, int rightIndex, int qleftIndex, int qrightIndex) {
// [leftIndex, rightIndex]表示 当前进入线段树的区间
// [qleftIndex, qrightIndex]表示 查询原数组区间
// 当前进入线段树的区间和 保存在 segmentTree[pos] 中
if (qleftIndex > rightIndex || qrightIndex < leftIndex) {
// 若 当前进入线段树的区间 与 查询原数组区间 不重合,则不存在求和项,返回0
return 0;
} else if (qleftIndex <= leftIndex && qrightIndex >= rightIndex) {
// 若 当前进入线段树的区间 被 查询原数组区间 覆盖,则整个[leftIndex, rightIndex]区间都是求和项,直接返回当前区间和
return segmentTree[pos];
} else {
// 若 当前进入线段树的区间 与 查询原数组区间 互有重合,则二分查找当前进入区间的两个子区间
int mid = (leftIndex + rightIndex) / 2;
return sumRange(pos * 2, leftIndex, mid, qleftIndex, qrightIndex) + sumRange(pos * 2 + 1, mid + 1, rightIndex, qleftIndex, qrightIndex);
}
}
若更新了数组某一位,则线段树中包含该位的左右区间和也必然被更新.
从根节点(对应原nums
数组[0, nums.len-1]
区间和)开始,不断将其所有包含第i位
的子区间加上对应的偏移量.
// 将原数组nums第i位的值改为val
public void update(int i, int val) {
int delta = val - nums[i]; // 求出增量
nums[i] = val; // 更新原数组
// 上边两行顺序不能互换,务必要先求增量再更新数组(这TM不是废话么)
update(1, i, delta, 0, nodeNum - 1); // 修改线段树
}
// 将线段树中 包含原数组第i位的所有区间和 加上一个 增量delta
private void update(int pos, int i, int delta, int leftInndex, int rightIndex) {
segmentTree[pos] += delta; // 加上增量
// 当前进入到 原数组区间[leftIndex, rightIndex] ,进入函数前已保证区间内包含i
// 若其存在子区间,则其中一个子区间必然也包含i
if (leftInndex != rightIndex) {
int mid = (leftInndex + rightIndex) / 2;
// 将该节点包含第i位的子节点也 加上增量delta
if (i <= mid)
update(pos * 2, i, delta, leftInndex, mid);
else
update(pos * 2 + 1, i, delta, mid + 1, rightIndex);
}
}