线段树详解与实现

线段树是一种高级数据结构,常用于解决区间问题,如区间染色、区间查询等。它通过二叉树实现,将区间内的元素统计值存储在节点中,支持更新区间和查询区间操作,时间复杂度为O(logn)。文章介绍了线段树的构建、查询和更新方法,以及在LeetCode上的应用题目。
摘要由CSDN通过智能技术生成

此篇文章用于记录《玩转数据结构》课程的学习笔记

什么是线段树

线段树也被称为区间树,英文名为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 2h1个节点,而最后一层有 2 h − 1 2^{h-1} 2h1个节点。那么: 除 了 最 后 一 层 的 前

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值