线段树及其应用
线段树简介
线段树(Segment Tree)是一种非常灵活的数据结构,它能够高效处理与区间相关的问题,如区间求和、区间最大值(或最小值)以及区间更新等操作。
线段树是一个二叉树,其中每个节点代表一个区间(或一个段),并且这个区间可以被分解成两个子区间,这些子区间分别由当前节点的两个子节点所表示。
线段树的思想源自于二叉搜索树,都有区间的概念,不过BST的节点是区间分割点,而线段树的节点本身就是区间的整体信息,这个整体信息可以是任何该区间的整体信息,只要该区间的整体信息可以由两个子区间的整体信息合并来即可,如区间和
基本特性
线段树的每个节点都代表一个区间。
线段树的唯一根节点,代表整个[1, n]区间。
线段树的每个叶节点,都代表长度为1的区间[x, x]。
对于每个内部节点[l, r],它的左节点[l, mid],右节点[mid+1, r],其中m i d = ⌊ (l + r) / 2 ⌋
线段树除去最后一层,是一个满二叉树。
可以发现树的最后一层节点在数组中存储,位置是不连续的。理想情况下:n 个节点的满二叉树有2∗n−1个节点,但由于最后一层产生空余,为了要保证数组能存储整棵树,最后一层也要开到2 ∗ n的空间。因此一共需要开辟4n的空间存储线段树。
使用场景
线段树的使用场景非常广泛,主要集中在对区间数据进行快速查询和更新上,以下是一些常见的应用场景:
区间查询:查询一个区间内的最大值、最小值或者区间值的和。例如,在股票市场分析中,可能需要查询某个时间段内的最高股价或者平均股价。
区间更新:在某个区间内更新一个或多个值,而且这些更新可能是增加、减少或者赋新值等操作。线段树可以在对数时间内完成这些操作,并且能够保持区间查询的正确性。
懒惰传递:这是线段树的一个高级特性,允许节点延迟更新。当对一个区间进行更新操作时,并不立即更新所有相关节点,而是仅标记这个区间,当需要查询时再进行实际的更新。这样可以大大提高批量更新操作的效率。
动态查询:不仅仅限于静态数据,线段树也能够适用于数据动态变化的情况,比如数据集合中新增或删除数据,线段树可以相应地调整,以维护查询和更新的准确性和效率。
示例
一个典型的应用实例是对于一个长度为的数组,支持以下两种操作:
更新操作:更新数组的某个元素或某个区间内的所有元素。
查询操作:查询数组中某个区间(比如到)的和、最大或最小值。
使用普通的数组或链表,这些操作的时间复杂度可能是线性的,即。利用线段树,可以将更新和查询操作的时间复杂度降低到,从而能够处理更大规模的数据,或者更频繁的查询和更新请求。
总之,线段树是处理区间查询和修改问题的有力工具,特别是当涉及到频繁的修改和需要快速响应查询时,线段树提供了一个非常有效的解决方案。
线段树的JS实现
树的建立(build)
- 先开辟4n大小的数组空间
- 从根节点开始建立,根节点表示[1, n]
- 递归的二分构建左子树[l, mid],右子树[mid + 1, r]
- 向下递归到叶子节点结束,叶节点维护区间[x, x],存储 $ a_x $的值
- 回溯过程中,从下往上更新信息,即 val(l,r) = fn(val(l, mid), val(mid + 1, r))
1 | class SegmentTree { |
树的更新(懒惰传递更新)
单点更新
更新arr数组中索引为index的值为val
1 | updateSingleNode(index, val, node = 1, start = 0, end = this.n - 1) { |
这其中涉及了懒惰更新,可能大家会有疑问,哪里懒惰更新了,明明每次递归都有处理lazy的数据
其实,注意看,每次处理完当前的lazy数据后,都会将变化同时传递给左子节点和右子节点的lazy,也就是这段代码
1 | if (start !== end) { |
但其实,如果要更新的节点只会存在于其中的一个子树中,另一个子树的更新确实没有在本次更新中实际去更新,这样就可以做到,每次更新不需要遍历整个线段树
区间更新
更新inputArray中索引在[l, r]之间的数据为val
1 | updateRange(node, start, end, l, r, val) { |
查询(懒惰传递更新)
1 | query(node, start, end, l, r) { |
懒惰传递更新
大家可以发现,在更新和查询的操作中有一段完全相同的代码,这段代码就是用来懒惰传递更新的
1 | if (this.lazy[node] !== 0) { |
所谓的懒惰更新,其实是因为,每次更新都会影响到整个树的中一半节点的值,但其实更新到的节点并不一定会立即用到,我们可以先把这些先把当前要更新的值存下来,等到真的查询到的时候再一次性把多次更新的结果一并应用到tree节点上
具体的方案就是:
- 当需要更新某个tree节点的时候,但是不去具体修改,等真的要查询的时候,再修改当前tree的节点的值,并且同时标记当前tree节点的子节点要更新
- 等到真正查询到子节点的时候再在子节点上递归应用上述操作
完整代码
1 | class SegmentTree { |