线段树(二):RMQ

之前我们给大家介绍过了SG的概念和引入了RMQ,这一讲我们就来深入看看RMQ的算法。

1. RMQ的结构

RMQ的结构我们之前也已经见过了,简单来说,就是存储该区间最小值的SG。但和教材上面不同的是,我们这里维护一个同时支持两种RMQ类型的SG,即Range minimum and maximum query,所以每个节点需要同时保存最小值(min)和最大值(max),比如之前题目的RMQ结构为:

在这里插入图片描述

那SG的初始化也非常简单了,我们从上往下进行初始化,每次计算当前区间的[min, max],并创建当前的节点,之后递归创建左右两个子节点。这里需要多讲一下代码实现的技巧,因为SG和Heap一样,基本结构都是基于完美二叉树,所以我们在初始化树的时候,不用真正地去创建一颗树,而是用一个数组就能表示一颗完美二叉树,这个给我们的实现带来很多便利。而且父亲下标和子下标有如下的规律,对于下标为k的节点:

父亲的下标:( k - 1 ) / 2

左孩子的下标:k * 2 + 1

右孩子的下标:k * 2 + 2

对这个实现思路不是很清楚的童鞋,可以看看下面的图例进行理解哦:

在这里插入图片描述

2. RMQ的查询

现在我们有了SG的数据结构,那给定某个区间,我们该如何进行查询呢?其实很简单,我们以查询最小值为例,则有下面的查询伪代码1

如果所查询的区间和当前节点对应的区间完全没有交集,那么就返回null。

如果所查询的区间完全包含了当前节点对应的区间,则返回当前节点的值。

以上两者情况都不满足,就对左右节点递归处理,返回两个结果中的较小者。

比如之前题目,查询区间[0, 7)的流程图如下:

在这里插入图片描述

绿点表示我们经过的节点,红点表示我们返回并且采用的节点,黄点表示我们返回但未采用的节点,其余节点均被舍弃,并未被查询过。

3. RMQ的更新

我们的SG除了支持查询(Query)操作之外,还需要支持更新(Update)操作,因为有时我们会更改某个原始元素,但我们也希望维护原本SG的性质,这样更新之后,我们依然能快速进行查询。我们先来看一个更新操作的例子,还是之前的SG,我们需要把原数组中下标为0的元素更改为2,也就是5 -> 2:

在这里插入图片描述

同样,绿色点表示我们更新需要经过的节点。所以对于RMQ的更新,我们只需要从某个叶节点开始,不断向上更新max或min,直到我们抵达Root。而且这个更新操作和Heap删除一个节点,之后向上Bubble-up是非常类似的,至于Heap插入和删除操作,有兴趣的童鞋可以网上自己查查相关资料,或者以后有机会再给大家讲解啦哒~

4. 复杂度分析

到此,我们已经把SG和RMQ基本给大家讲解结束了。最后,我们还剩复杂度的分析,现在我们就来看看。对于基于SG的RMQ的时间复杂度,这个非常简单,之前的图例已经给大家说明了,已经忘了的童鞋可以再看看这个图例:

在这里插入图片描述

对于n个元素组成的SG,它的高度必定是logn,而且SG是完美二叉树,所以高度不会像BST出现极端情况,而退化成n,所以SG的查询和更新时间复杂度都是O(logn)。

那空间复杂度呢?我们看到最底层的元素个数有n个,而且树的高度是logn,那空间复杂度岂不是O(nlogn)?其实不是哦,这点有点误导人,SG的空间复杂是O(n)。大家可以注意一下,图例把每层的元素个数标记出来的,如果我们把每层的个数都加起来:
n + n 2 + n 4 + . . . + 4 + 2 + 1 = n ∗ 1 − ( 1 2 ) l o g n 1 − 1 2 = 2 n ∗ ( 1 − ( 1 2 ) l o g n ) < = 2 n n + \frac{n}{2} + \frac{n}{4} + ... + 4 + 2 + 1 = n * \frac{1-(\frac{1}{2})^{logn}}{1-\frac{1}{2}} = 2n * ( 1 - (\frac{1}{2})^{logn}) <= 2n n+2n+4n+...+4+2+1=n1211(21)logn=2n(1(21)logn)<=2n
这里需要注意一下,从上往下,节点总数从1每层递增两倍,但从下往上,节点总数从n每层递减两倍,所以整个求和公式是以1/2为公倍数的等比数列,所以SG的空间复杂度不是O(nlog),而是O(n),非常的nice,时间复杂度是O(log),空间复杂度是O(n),和我们对一维数组进行离散二分查找没有任何区别,性能一致。

可是,是不是到这里就完事大吉了呢?No!可能有聪明的童鞋可能已经发现了,SG的空间复杂度是O(n),那就意味着初始化SG的时间复杂度为O(n),加上查询时间复杂度O(logn),O(n) + O(logn) = O(n),嗯?最后整体的时间复杂度岂不和之前暴力解的时间复杂度一样嘛?而且暴力解不需要额外空间,所以不会有额外初始化的时间消耗。Wait,也就是说我们讲了半天,SG的效率和暴力解并没有多少优势,至少从渐进的意义上,两者都是O(n)。

其实这个分析是没有问题,但这只针对查询次数为1的情况,如果我们假设我们查询了k次,那么两个算法的整体时间复杂度为:

Brute Force: O( k * n );

Segement Tree: O( n ) + O( k * logn ) = O( n + k * logn );

如果k = n,则:

Brute Force: O( k * n ) = O( n * n ) = O( n ^ 2 );

Segement Tree: O( n + k * logn ) = O( n + n * logn ) = O( nlogn );

也就是说在大量查询的情况下,SG的性能就会优于暴力解,而且即使不是大量查询,SG也不会比暴力解逊色,所以SG它不香么?这种思路在实际应用的非常普遍,比如我们有一组数据,但每天都会有大量用户对这个数据进行查询,我们初始化查询用的数据结构只需要一次,但用户的查询次数是成千上万,或者百万千万,所以有时候我们需要把这些因素考虑到我们的算法设计当中,这也是Offline Algorithm和Online Algorithm的重要区别哦。

到这里,我们讲解完了RMQ的算法思路,包括查询和更新,接下来我们将进入到代码实现的解析当中。

上一节:线段树(一):基本概念

下一节:线段树(三):代码解析

系列汇总:超详细!线段树讲解文章汇总(含代码)

5. 参考资料

  1. 《挑战程序设计竞赛(第2版)》,(日)秋叶拓哉 等著,人民邮电出版社;

6. 免责声明

※ 本文之中如有错误和不准确的地方,欢迎大家指正哒~
※ 此项目仅用于学习交流,请不要用于任何形式的商用用途,谢谢呢;


在这里插入图片描述


  1. 《挑战程序设计竞赛(第2版)》,(日)秋叶拓哉 等著,人民邮电出版社 ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值