此篇文章用于记录《玩转数据结构》课程的学习笔记
什么是线段树
线段树也被称为区间树,英文名为Segment Tree
或者Interval tree
,是一种高级的数据结构。这种数据结构更多出现在竞赛中,在常见的本科数据结构教材里没有介绍这种数据结构。但是,在面试中却有可能碰到和线段树相关的问题。那么为什么会产生线段树这种数据结构,线段树到底是为了解决什么样的一种问题呢?
其实这里的线段
可以理解为区间
,线段树就是为了解决区间问题的。
有一个很经典的线段树问题是:区间染色。
假设有一面墙,长度为 n,每次选择一段墙进行染色。
在区间染色的过程中,每次选择一段区间进行染色,这时新的颜色可能会覆盖之前的颜色。
最后的问题是:
- 在经过 m 次染色操作后,我们可以在整个区间看见多少种颜色?
更加普遍的说法是:
- 在经过 m 次染色操作后,我们可以在区间
[i, j]
内看见多少种颜色?
由于第一个问题是第二个问题的一个特例,我们采用第二种问题来思考解决方法。
从上面可以看出,我们对于区间,有 2 种操作,分别是染色操作和查询区间的颜色,使用更加一般的说法,染色操作就是更新区间,查询区间的颜色就是查询区间。
这类问题里面,更加常见的的是区间查询:一个数组存放的不再是颜色,而是具体的数字,查询某个区间[i, j]
的统计值。这里的统计值是指:区间内最大值、最小值、或者这个区间的数字和。
比如:
- 查询 2018 年注册的用户中消费最高的用户
- 查询 2018 年注册的用户中消费最低的用户
注意上面两种情况都是动态查询,我们查询的消费数据不只是 2018 的消费数据。
如果我们想查询 2018 年中消费最高的用户,那么 2018 年的数据已经固定了,我们直接在这一年的数据中进行统计分析就行了。
但是一个 2018 年注册的用户,在 2019 年、2020 年都可能会有消费。我们实际上查询的是:2018 年注册的用户中,到现在为止,消费最高的用户。
这种情况下,数据是在动态变化的, 也就是说:2017 年注册的用户中,每个用户的消费额是会更新的,这就对应到更新区间的操作。
此时线段树就是一种好的选择。
按照通常的思路,使用数组存储上述的元素是比较好的,思考上面两个操作的时间复杂度:
- 更新区间:每次根据需要更新的区间的首尾索引,逐个遍历区间中的元素进行更新,时间复杂度为
O(n)
。 - 查询区间:每次根据需要更新的区间的首尾索引,逐个遍历区间种的元素进行查询,时间复杂度为
O(n)
。
两个操作的时间复杂度均为O(n)
,对于需要多次动态使用的场景来说,性能可能是不够好的。
在这类问题中,我们关注的是一个个区间内的元素的情况,线段树就有用武之地了,线段树的优点就是把两个操作的时间复杂度降到了O(logn)
。
操作 | 使用数组 | 使用线段树 |
---|---|---|
更新 | O(n) | O(logn) |
查询 | O(n) | O(logn) |
这里提一点,如果你看到一个算法的时间复杂度是O(logn)
,那这个算法大多与二叉树、分治算法有关。
这里也不例外,线段树就是使用二叉树来实现的。
那么一个区间是如何被构建成为一个二叉树的?
对于一个数组 A,如下所示:
对应的线段树就是:
二叉树中每个非叶子节点表示的是区间内元素的统计值,叶子节点存储的就是元素本身。上面说了统计值是指:区间内最大值、最小值、或者这个区间的数字和。比如你要求区间的最大值,每个每个节点存储的就是这个区间内元素的最大值。像下面这样:
假设你要查询[4,7]
区间内的最大值,那么不用查到叶子节点,而是查到A[4, 7]
这个节点就行了。
当然,并不是所有的区间都恰好落在一个节点,比如你要求[2, 5]
区间内的最大值。那么就要分别找到A[2, 3]
和A[4,5]
的最大值,再进行比较。
可以看出,线段树的查询区间操作不需要遍历区间中的每一个元素,只要找到对应的树节点就可以返回,时间复杂度为O(logn)
。
总结
从更加抽象的角度来讲,线段树的使用场景就是,对于给定区间,进行更新区间和查询区间操作:
- 更新区间:更新区间中的一个元素或者一个区间的值。
- 查询区间:查询一个区间
[i, j]
的最大值、最小值、或者区间的数字和。
注意,在大多数情况下,我们是不考虑区间里添加元素和删除元素的,我们假设区间的大小是固定的。
线段树的表示
树的一般表示方法是链式存储,每个节点有两个指针,一个指向左孩子,一个指向右孩子。但是满二叉树,和完全二叉树,除了使用链表法来存储,还可以使用数组来表示。
在满二叉树和完全二叉树的数组表示中,假设一个节点的所以是i
,那么左孩子的索引就是 2 × i + 1 2 \times i +1 2×i+1,右孩子的索引就是 2 × i + 2 2 \times i +2 2×i+2。
那么线段树是不是满二叉树或者完全二叉树呢? 能不能使用数组来表示呢?
在上面的例子中,我们的二叉树恰好是一棵满二叉树,这是因为我们的数组大小恰好是 8,也就是 2 3 2^3 23,只有数组大小恰好是 2 的 n 次幂,所对应的线段树才会是一个满二叉树。在大部分情况下,线段树并不是一个满二叉树。如果一个数组的大小是 10 ,对应的线段树如下图所示。
所以线段树不是满二叉树,也不是完全二叉树。但实际上:线段树是平衡二叉树,是可以保证O(logn)
的时间复杂度的,这里就不证明了。
其实平衡二叉树可以看作特殊的满二叉树,进而使用数组来表示。
下一步就是确定:对于大小为 n 的数组,需要多大的空间来存储线段树。
首先说一个结论,对于高为 h 层的满二叉树,一共有 2 h − 1 2^{h}-1 2h−1个节点,而最后一层有 2 h − 1 2^{h-1} 2h−1个节点。那么: 除 了 最 后 一 层 的 前