线段树-从入门到入土
省流:本文将介绍用指针实现线段树,并介绍如何在题目中运用线段树解决问题。
线段树 の 实现
线段树一般用于维护区间信息。
题目示例
要求维护一个序列 {an},实现两种操作:
- 给定 l, r 和 x,让 a 中 [l, r] 区间内的每一个数加上 x(区间加)。
- 给定 l, r,求
(区间求和)。
朴素实现 / Bruteforce
- 前缀和:发现有区间求和,容易想到用前缀和 O(1) 维护。但是对于区间加,最坏情况下要修改整个前缀和序列,时间复杂度为 O(n)。
- 差分:发现有区间加,容易想到用差分 O(1) 维护。但是对于区间求和,每次需要计算一次差分序列的前缀和,时间复杂度为 O(n)。
线段树 / Segment Tree
有没有一种可以均衡两个操作复杂度,每次操作是 O(log n) 的数据结构呢?
线段树是将序列分治的过程,并且将分治的过程记录下来。
线段树维护的信息需要满足可合并性,即可以从儿子的信息得到父亲的信息。
存储方式
线段树将序列中若干个区间在树上用结点来表示,其中 [1, n](在下文的讨论中 n 均表示序列长度)为树的根。
对于区间 [l, r],可以将其均分为两段,设
对于上面的题目,可以这样设计结点:
1 | struct TreeNode { |
下面是一颗维护 [1, 7] 的线段树形态:

性质
对于线段树上的任一结点,儿子数量为 0 或 1,不存在只有一个儿子的情况。
证明
非常显然。
一个长度为 n 的序列建立的线段树只有 (2n − 1) 个结点。
证明
首先线段树上必然有 n 个叶子结点。
每两个叶子结点会合并,添加一个结点,此时没有父结点的点数减一。
最后没有父结点的点数为 1,即根节点,所以添加了 (n − 1) 个结点。
故总共有 (2n − 1) 个结点。
一个长度为 n 的序列建立的线段树的高度为 O(log n)(O(f(n)) 的意思是与 f(n) 同阶)。
证明
首先对于一个结点 [l, r],设其长度 t = r − l + 1。
则其长度最大的儿子的长度为
。 那么设树高为 h,有
这个式子迭代 h 次之后等于 1。 因为上式
,那么有 那么容易解得 h = O(log n),那么一般认为 h = ⌈log n⌉。
空间复杂度
线段树是一个类似完全二叉树的结构,考虑从上至下的每层结点数量之和,得到
1 + 2 + 4 + ⋯ + 2⌈log n⌉ ≤ 2⌈log n⌉ + 1 一般结点数量接近 4n。
建线段树
树是递归定义的,所以也可以递归建树。
线段树需要从儿子的信息合并得到父亲的信息,考虑设计一个辅助函数:
1 | void pushup(TreeNode *u) { |
这样就可以从儿子的区间和得到父亲的区间和了。
每次修改操作结束后都需要一次
pushup!
建树只需要将当前区间分为两部分,然后分别递归下去即可,详见代码。
1 | // 假设根节点的 l,r 都已经设置好 |
时间复杂度为 Θ(n)。
时间复杂度证明 1
容易发现每次调用 build
就新建了一个线段树结点,而结点数量为 Θ(n),那么时间复杂度也就是
Θ(n) 了。
时间复杂度证明 2
递归函数的时间函数为
那么在主定理中 a = b = 2,logba = log22 = 1,ϵ 可以取 (0, 1] 范围内的数字,满足主定理的第一条情况,所以 T(n) = Θ(n)。
区间修改
由于单点修改相当于修改一个区间 [p, p],所以仅讨论区间修改。
暴力修改 / Bruteforce
遍历区间内的每一个位置,然后在线段树上定位修改。
这样做的时间复杂度是 O(nlog n) 的,甚至不如用数组维护。
懒标记 / Lazy Tag
考虑引入懒标记(Lazy Tag),在结点处记录修改信息。
设当前递归到的区间为 [l, r],需要修改的区间为 [L, R],需要区间加上 x。
如果 [l, r] 被
[L, R]
完全包含,那么可以在 [l, r]
所对应的结点上标记这颗子树需要 +x,并且更新当前结点的
w,然后直接返回。
每次访问到一个结点时,需要先将懒标记下放到子结点上,然后递归。
正确性比较显然。
举个例子
假设初始序列为 {1, 5, 4, 2, 3},要对 [1, 4] 的每一个数加上 5。
线段树初始形态:
![]()
区间 [1, 4] 在线段树上一路递归下去发现可以划分为 [1, 3] 和 [4, 4] 两个序列。
此时将 [1, 3] 和 [4, 4] 对应的结点的 lzy ← 5,并且更新两个结点的区间和,同时向上回溯
pushup。修改后的线段树:
区间查询
由于单点查询相当于查询一个区间 [p, p],所以仅讨论区间查询。
这里采用懒标记的实现方法。
