概念:
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。
图解:
对于一维数组:numberArray = [10, 8, 9, 7, 6, 1, 4, 2, 3]
那么它构成的线段树就是下面这个样子:
线段树的根节点 0 [0, 8] 表示,在区间 [0, 8] 这些数据的总和为 50
1 [0, 4] 表示,在区间 [0, 4] 这些数据的总和为 40
2 [5, 8] 表示,在区间 [5, 8] 这些数据的总和为 10
......
这里的区间是以一维数组的下标作为左右端点的
那么构建这颗线段树有什么好处呢?
查询区间和:
当我们要求一维数组中,[3, 5] 这个区间的值,我们可以快速的通过遍历这棵线段树得到,而且时间复杂度在 O(log(n)) 级别
那么我们如何得到呢?
1.首先从根节点开始遍历,因为 [3, 5] 处于 0 [0, 8] 区间,接着查询 0 号根节点的左右子树
2.查询 1 [0, 4],因为 [0, 4] 包含 [3, 5] 区间的一部分,接着查询 1 号节点的左右子树
3.查询 3 [0, 2],因为 [0, 2] 不在 [3, 5] 区间内部,所以舍弃,返回0,回到第 2 步查询 1 号节点的右子树
4.查询 4 [3, 4],因为 [3, 4] 在 [3, 5] 区间内部,所以返回 4 号节点的值 13,1 号节点的左右子树访问完毕,返回左右子树值的和,也就是返回 0 + 13 = 13,此时 0号根节点的左子树访问完毕,然后回到第 1 步,访问 0 号根节点的右子树
5.查询 2 [5, 8],因为 [5, 8] 包含 [3, 5] 区间的一部分,接着查询 2 号节点的左右子树
6.查询 5 [5, 6],因为 [5, 6] 包含 [3, 5] 区间的一部分,接着查询 5 号节点的左右子树
7.查询 11 [5, 5],因为 [5, 5] 在 [3, 5] 区间内部,返回 11 号节点的值 1,5 号节点的左子树访问完毕,接着访问 5 号节点的右子树
8.查询 12 [6, 6],因为 [6, 6] 不在 [3, 5] 区间内部,所以舍弃,返回0,回到第 6 步,此时 5 号节点的左右子树均访问完毕,返回左右子树值的和,也就是返回 1 + 0 = 1,此时 5 号节点左右子树访问完毕,回到第 5 步,该访问 2 号节点的右子树
9.查询 6 [7, 8],因为 [7, 8] 不在 [3, 5] 区间内部,所以舍弃,返回0,回到第 5 步,此时 2 号节点的左右子树访问完毕,返回左右子树值的和,也就是返回 1 + 0 = 1,此时 2 号节点左右子树访问完毕,回到第 1 步,此时发现根节点的左右子树也访问完毕,所以返回左右子树值的和,也就是返回 13 + 1 = 14
最终得到 [3, 5] 区间和为 14
上面有点啰嗦,看下面的图,也就是找到这些蓝色节点,将蓝色节点的值相加在一起就得到了 [3, 5] 区间的和
创建线段树:
创建线段树的方式很简单,就是递归的创建,如果你学过二叉查找树,那么这个树的建立方式也是和它大同小异的,唯一的区别是,为当前节点创建左右子树,需要左右孩子的值相加之和赋值给当前节点,具体看下面的代码就好了
更新节点:
比如我们想把一维数组中下标为 3 的元素从 7 改为 5,那我们同样要对整颗线段树进行更新
更新的方式和创建的方式差不多,只不过根据更改的下标改对应的左子树或者右子树
就像上面说的那个,把 7 改成 5,我们只需要改动的节点为下图标红的节点值
重点问题:为什么创建的线段树所占空间要是原始数据数组的空间大小的 4 倍呢?
这个线段树就是平衡二叉树,但是同样具备完全二叉树的特点,但是不是真正的完全二叉树
完全二叉树的总节点个数为 2n + 2,n 表示的是原始数据数组的空间大小
我上面画的只能说是凑巧是一颗完全二叉树
推导一下哦:
如果原始数据数组大小为 n
假设:n = 2 ^ k,此时构成的树就是满二叉树,所占空间大小就是 2n
那么如果:n > 2 ^ k,那就意味着需要多一层来存储最后的叶子节点
那么就相当于 n = 2 ^ (k +1)
那么所占空间大小就应该是 4n,实际上用不到 4n,但是为了方便就设置为 4n,3n 是绝对不ok的,不信,你试试 ^_^
既然说到这里,我们回顾一下之前的树节点总个数是怎么算的?
假设树的高度为 k,那么对于满二叉树来说,每层节点都是满的
那么第一层 1, 第二层 2,第三层 4,第 k 层 2^(k-1)
正好为等比数列,根据等比数列的求和公式:
a1 = 1,,q = 2,n = k,所以 Sn = 2^k - 1
线段树代码:博主使用的 Java 语言
public class SegmentTree {
/**
* nodeArray节点数组
*/
int[] nodeArray;
/**
* numberArray 一维数据数组
*/
int[] numberArray;
/**
* 构造线段树
*
* @param numberArray 一维数据数组
*/
SegmentTree(int[] numberArray) {
this.numberArray = numberArray;
// 线段树的节点数组一般要开辟 4 倍大小空间,防止数组越界
this.nodeArray = new int[this.numberArray.length * 4];
}
/**
* 构建线段树,节点 position [left, right, number]
*
* @param position 当前节点位置(节点编号,左节点编号 position * 2 + 1,右节点编号 position * 2 + 2),编号代表在节点数组的下标位置
* @param left 节点表示区间的左端点
* @param right 节点表示区间的右端点
*/
void buildSegmentTree(int position, int left, int right) {
// 如果区间的左端点 = 右端点,说明此节点为线段树的叶子节点
if (left == right) {
nodeArray[position] = numberArray[left];
return;
}
// 区间取半,得到区间中点
int mid = (left + right) / 2;
// 建立线段树的左子树
buildSegmentTree(position * 2 + 1, left, mid);
// 建立线段树的右子树
buildSegmentTree(position * 2 + 2, mid + 1, right);
// 当前节点值 = 左孩子的值 + 右孩子的值
nodeArray[position] = nodeArray[position * 2 + 1] + nodeArray[position * 2 + 2];
}
/**
* 更新线段树
*
* @param position 当前节点位置,所在节点数组的下标位置
* @param left 节点表示区间的左端点
* @param right 节点表示区间的右端点
* @param index 要更改的数据在数据数组中的下标位置
* @param number 更改的数据
*/
void updateSegmentTree(int position, int left, int right, int index, int number) {
// 如果此时为线段树的叶子节点,且叶子节点的左右端点 = 要更改的数据在数据数组中的下标位置,进行重新赋值
if (left == right && left == index) {
nodeArray[position] = number;
return;
}
// 区间取半,得到区间中点
int mid = (left + right) / 2;
// 如果要更改的数据在数据数组中的下标位置在区间中点的左边,则更新左子树
if (index <= mid) {
updateSegmentTree(position * 2 + 1, left, mid, index, number);
} else {
// 如果要更改的数据在数据数组中的下标位置在区间中点的右边,则更新右子树
updateSegmentTree(position * 2 + 2, mid + 1, right, index, number);
}
// 当前节点值 = 左孩子的值 + 右孩子的值
nodeArray[position] = nodeArray[position * 2 + 1] + nodeArray[position * 2 + 2];
}
/**
* 给定区间,查询该区间的和
*
* @param position 当前节点位置,所在节点数组的下标位置
* @param left 节点表示区间的左端点
* @param right 节点表示区间的右端点
* @param rangeLeft 给定区间的左端点
* @param rangeRight 给定区间的右端点
* @return 该区间的和
*/
int sumRangeSegmentTree(int position, int left, int right, int rangeLeft, int rangeRight) {
// 如果节点表示区间不在给定区间内部,则返回 0
if (rangeLeft > right || rangeRight < left) {
return 0;
}
// 如果节点表示区间在给定区间内部,则返回节点值
if (rangeLeft <= left && rangeRight >= right) {
return nodeArray[position];
}
// 区间取半,得到区间中点
int mid = (left + right) / 2;
// 返回左子树(其中节点在给定区间的值) + 右子树(其中节点在给定区间的值)
return sumRangeSegmentTree(position * 2 + 1, left, mid, rangeLeft, rangeRight)
+ sumRangeSegmentTree(position * 2 + 2, mid + 1, right, rangeLeft, rangeRight);
}
}
测试:
public static void main(String[] args) {
int[] numberArray = new int[]{10, 8, 9, 7, 6, 1, 4, 2, 3};
SegmentTree segmentTree = new SegmentTree(numberArray);
segmentTree.buildSegmentTree(0, 0, numberArray.length - 1);
int result = segmentTree.sumRangeSegmentTree(0, 0, numberArray.length - 1, 3, 5);
System.out.println("[3, 5] = " + result);
segmentTree.updateSegmentTree(0, 0, numberArray.length - 1, 3, 5);
result = segmentTree.sumRangeSegmentTree(0, 0, numberArray.length - 1, 3, 5);
System.out.println("numberArray[3] = 5,再次查询[3, 5] = " + result);
}
总结:
如果读者发现博客中有什么错误,可以再下方评论,我看到后会进行改正!
如果有什么不懂的地方,也可以评论,我们互相探讨,共同进步!
最后说一句:好好学习,天天向上!^_^