segment tree(线段树)详解
什么是线段树
线段树是一棵平衡搜索树,但是不是完全二叉树,其实也是一棵二分搜索树,它储存的是一个区间的信息。
每个节点以结构体的方式存储,结构体包含以下几个信息:
区间左端点、右端点;(这两者必有)
区间内要维护的信息(实际情况而定,数目不等)。
一个具体的线段树如下所示,这里是一数组作为底层数据结构来阐述线段树,所以就简单的将线段树看为满二叉树,这样的话肯定是会造成空间的浪费,但是这里就是为了理解线段树这种数据结构,不去考虑其更高层的实现,要想空间不浪费的话就动态的创建线段树就可以了。
构建二叉树
其实只要涉及到树的一些操作就避免不了使用递归的思想来创建树,以为每一棵树都是有一棵棵子树构成的.
主体的思路就是:
- 对于二分到的每一个结点,给它的左右端点确定范围。
- 如果是叶子节点,存储要维护的信息。
- 子树合并。
1 | public class SegmentTree<E> { |
上面初始化树的空间的时候使用的空间大小是要存储的数组的大小的4倍,具体是怎么来的?
对于一棵满二叉树,其第h层的元素有2^h-1个节点,并且第h层元素的节点的个数等于前1—>h-1
层元素个数的总和。那么来看存储n个元素的时候需要多大的空间?需要4n的空间,具体如下图:
这里实现了Merger的接口,这个结接口就是用来应对对线段树的不同操作而设置的,用户可以根据自己想要实现的功能自己进行这个接口的具体实现。
1 | public interface Merger<E> { |
树的查询操作
查询一个线段树其实使用递归的思想来实现,例如说我们要查询[2,5]这个区间内的元素,然后从根节点开始递归,[2,5]是在[0,7]之内的,然后求出根的左右孩子和中间分割点mid,查询区间的左右边界与mid比较,然后根据其满足的条件看需要在左子树还是右子树继续查询,不断递归直到找到要查询的区间。说的有点啰嗦了,具体看下面:
1 | /** |
树中元素的更新
更新操作其实与查询操作的实现差不多,但是有一点区别的是,更新某个节点的匀速之后其父节点的元素也需要更新,因为其父节点也是包含该元素的,因此更新操作需要返回更新后状态。
具体实现如下:
1 | /** |
完整代码
1 | package SegmentTree; |
应用
在LeetCode上有这么一道题:
Given an integer array nums, find the sum of the elements between indices i and j (i ≤ j), inclusive.
The update(i, val) function modifies nums by updating the element at index i to val.
Example:
1 | Given nums = [1, 3, 5] |
Note:
- The array is only modifiable by the update function.
- You may assume the number of calls to update and sumRange function is distributed evenly.
这道题目其实就是动态查询某个数组区间内的和,一般的想法是进行预处理,就是把前i个元素的和求出来,然后每次求的时候就可以借助这个和来进行sumRange的操作,然后更新操作的话就是初始化一个新的数组存放原来nums的值,然后对其进行跟新,这样更新的话同时sum的和也需要更新,就是从更新的那个元素开始后面的元素的和全部要更新。事件复杂度nO(n)的,超时了。
然后这里要是使用线段树来做的话实现复杂度就是nO(logn)的,能够运行通过,具体如下:
1 | class NumArray { |