线段树属于一种扩展的数据结果, 是有一定的难度的
什么是线段树?
parent的value等于两个child之和
-
叶子结点存储输入的数组元素(也就是叶子结点才是真正的每个结点的值)
-
非叶子结点是存储的某个区间上的元素的和
-
但是注意: 其实我们可以说线段树中的结点中存储的就是某个区间上的元素的和, 即使是叶子结点我们也可以看做是存储的某个区间长度为1的区间上的元素的和, 也就是叶子结点中存储的其实就是某个结点的值, 但是即使是某个具体的结点, 我们也可以将其看做是一个区间, 比如第二个元素, 我们就可以将其看作为是[2,2]
-
每个非叶子结点表示某些叶子结点的合并(merge)
- 当然注意: 如果我们是要求某个区间上的最大值和最小值的时候这个时候我们的线段树的非叶子结点就不是对应叶子结点的和了, 而是对应区间内值最大的或者最小的叶子结点值, 具体问题我们要具体建树
其实我们的线段树就是每个结点都表示了一个区间, 这个区间最小长度为1, 区间长度为1的结点就是叶子结点, 而这些结点中都是存储的区间中的所有数据的一个分析后的结果, 但是具体的线段树中要存储什么数据要根据问题来进行具体分析:
- 如果我们是想要求某个区间上的元素的和, 这个时候我们的线段树结点中就存储对应区间的值的和, 如果我们是要求某个区间上的最大值, 这个时候我们的线段树结点中就存储对应区间中最大的值
如果我们使用数组形式来表示线段树, 我们都会构建线段树的时候将其构建为一个完全二叉树, 如果不满足完全二叉树的结点的要求, 那么我们直接就将这个线段树补位一个完全二叉树结构即可, 这样就方便我们进行各个元素之间的关系表示, 如果某个线段树是一个完全二叉树结构, 那么这个时候对于下标为i的结点, 其左孩子为2*i+1, 其右孩子为2 *i + 2, 父节点为: (i - 1) / 2
线段树提出是想解决什么问题?
给定一个长度为n的序列, 需要频繁的求其中某个区间的最值, 以及更新某个区间的所有值的和时, 使用最朴素的算法是遍历查询与插入, 则查询的时间复杂度为O(q * n), q为查询的个数, 在数据量大时, 查询次数多的时候效率很低
- 另一种思路是一个O(n2)的数组, a[i] [j]表示区间[i,j]上的最小值, 这样查询的时间复杂度就是O(1), 相当于空间换时间, 但是修改某个区间的值又会比较麻烦, 因为修改一个值之后, 只有是区间范围中包含这个修改的值的数组对应位置都要修改, 修改会很麻烦, 并且空间复杂度也较大, 所以使用起来其实也不是很建议
- 而我们使用线段树之后可以很好的解决这类需要维护区间信息的问题, 线段树可以在O(logn)的时间复杂度之内实现查询区间和, 修改结点值等
对于线段树而言:
单点修改: O(log n)
区间修改: O(n log n)
- 但是使用到lazy propogation(延时标记)之后可以将时间复杂度优化为O(log n)
区间查询: O(log n)
- 这里的区间查询包括: 区间求和, 求区间最大值, 求区间最小值, 求区间中值的最小公倍数以及求区间中值的最大公约数等
我们可以使用线段树来解决上述的这些问题: 包括单点修改, 区间修改, 区间查询, 最好的时间复杂度都是在O(log n)
处理rangeQuery系列问题的数据结构:(也就是处理区间查询, 也就是区间问题常用的数据结构)
- prefixSum —> 前缀和数组
- BinaryIndexTree —> 树状数组(也称之为: 二分索引树)
- SegmentTree —> 线段树
一般来讲: 凡是可以使用树状数组解决的问题, 使用线段树也是可以解决的, 但是线段树能够解决的问题树状数组未必是能够解决的(例如: 求区间中值的最大值)
- 树状数组就是BinaryIndexTree, 也称之为: 二分索引树
什么情况之下我们无法使用线段树?
如果我们删除或者是增加区间中的元素, 那么区间的大小将发生变化, 此时是无法使用线段树解决这种问题的, 但是如果只是求元素的个数(count), 即使是删除和增加区间中的元素, 则我们可以转化为bucket sort(桶排序)之后再做rangeSum(区间范围中的满足条件的元素求和), 这个时候就没有元素的增加和减少了
区间更新:
区间更新是指对某个长度为m的区间内的全部元素执行相同的操作
-
简单的方式是使用暴力法: 也就是执行m次单点更新, 时间复杂度为O(mlogn)
-
还有一种方式就是先递归的更新左子树和右子树, 然后再更新当前节点, 但是这种方法需要递归的更新m个叶子结点, 时间复杂度为O(m)
-
最后就是使用我们的延时标记的方式:
-
因为线段树中引入了延时标记的概念, 可以在O(logn)的时间之内完成区间的更新, 简而言之, 在更新某个区间的所有值的时候, 比没有立刻更新每个叶结点的值, 而是将更新操作暂存在这个区间所对应的结点上, 之后在特定的时机中再将操作下传给子节点
-
那么什么时候就是特定时机?
-
在访问任何任何结点的子节点的时候就进行一个下传, 这里的"访问"既包括区间更新时的访问, 也包括区间查询时的访问
-
我们在访问任何结点的子节点之前, 如果当前节点有延时标记, 那么要先将延时标记下传给其左右子节点之后, 清除当前节点的延迟标记之后, 再去访问它的子节点, 这样能保证每次访问某个结点的时候该结点已经是按照之前的所有更新操作修改过了, 该节点的值是最新的
补充:
我们上面讲的只是普通的线段树, 还有一种竞赛常用的线段树:zkw线段树
- zkw线段树: 张昆伟线段树(又称之为: 重口味线段树), 相对于普通的线段树, zkw线段树的实现代码要短很多, 但是学习起来的难度比较大, 因此zkw线段树更加多的是使用在各种竞赛中
关于线段树的练习题: 307, 315
区间更新: