鲁迅说过:一项科技或技术被发明出来,一定是遇到了之前无法解决的问题。线段树也不例外
问题提出:
我们有一个数组,我们需要频繁地对数组执行以下两个操作:
- 求数组中下标为[ l - r ] 的和
- 对数组中下标为 i 的元素的值进行更新
初步方案:
方案一:
最简单的方式,我们在计算机中使用数组存储这一串数字。
- 针对问题一,我们直接遍历求和,复杂度为O(n)
- 针对问题二,我们使用数组可以直接进行更新,复杂度为O(1)
方案二:
当然了,我们也可以选择使用前缀和来对问题一进行优化。具体的做法是:
- 维护一个前缀和数组,pre[ i ] 表示前 i 个元素的和,可以提前使用O(n)的复杂度将数组维护好。
- 针对问题一,我们就可以使用O(1)的复杂度求得
- 针对问题二,我们就需要使用O(n)的复杂度对前缀和进行维护。
这两种方式可以根据问题的规模的不同来适当的取舍,在实际问题中,如果更新的次数远大于求和的次数,并且需要求和的次数又比较少,那么使用方案一不失为一种好的方案。同样的,如果求和的次数远大于更新的次数,而更新的次数又比较少,那么我们使用方案二也是非常优秀的。
思考:
如果两个操作出现的次数相当,并且都非常多的话,显然,整体O(n)的复杂度是不能忍受的。我们需要更好的方案。
在优化的时候可以考虑分治的思想
思路从前缀和而来,我们在维护这个前缀和的时候,本质上只维护了一个和,那就是所有元素到起始元素的和。所以其中某一个元素修改之后,后面所有的前缀和就都要修改。这实际上对资源是一种浪费。
如果我们在更新一个元素后,只修改部分的 “前缀和”,是不是就可以将复杂度降下去呢?
比如说,我把全部的元素分为两个数组,维护两个前缀和,那么在计算区间和的时候,如果全部落在一个区间中,直接在那个区间中计算即可,如果落在两个区间的交界处,对两个分别进行求和再相加即可,时间复杂度同样为O(1)。如下图所示:
在修改元素的时候,我们维护前缀,时间复杂度会从O(n)到O(n)。想必这里读者应该看出不一样的地方了吧,我在分成两半的时候,时间复杂度就会变成原来的二分之一,因为我只需要维护最多一个区间上的前缀和,虽然还是O(n)级别,但是说明我们已经对其进行了优化。
那么如果我们分的更多一点呢?事实证明,我们分的越多,我们在维护所谓“前缀和”的时候,需要花费的时间就越少。同时,在求区间和的时候,需要进行的判断也就越多,这里的时间花销也同样会增长,那么如何让其进行一个平衡呢?
引入线段树:
这里需要一点点二叉树的知识
我们直接说明他是如何实现的,如下图所示:
假如数组中有六个元素,那么我们就需要构造这样一棵树,这一颗树的根节点的值表示元素1~6的和,其左子树和右子树将其父节点表示的范围分成两半,左子树表示元素1 ~ 3的和,右子树表示元素4 ~ 6的和,以此类推,当遇到节点表示的和的个数为奇数的时候,我们通常倾向于给左子树分配多一个节点。
可以看出,父节点的值等于其左子树和右子树的和
那么针对这样一个结构,我们那两个问题的表现如何呢?
- 针对问题一,求解的时候如果遇到刚好是这个结点的值的时候,直接返回即可,如果遇到有部分重合的地方,只需要更深层的遍历即可找到结果,总体的时间复杂度为O(logn)
- 针对问题二,在这个结构中,单个元素总是叶子节点,在更新某个元素的时候,只需要沿着叶子节点向上一路更新到根节点即可。时间复杂度同样是O(logn)
这样我们就找到了让两个需求相对平均一点的方案,将这个问题整体的时间复杂度从O(n)降低到了O(logn)。
这就是线段树,线段树不仅可以应用在求和这一个问题上,涉及到整体与个体的关系的结构中,我们都可以考虑使用线段树来求解。这类的问题的特点一般是往往修改整体中的某一个元素,从结果层面上看,会对整体进行修改,但是维护这样的修改需要耗费很多的时间,我们就可以让整体的这个小变化尽可能的不影响到其他小整体,进而降低时间复杂度,这本质上是一种分治。
下一节中,我会介绍线段树在C语言中的几种常见实现方式。
- 觉得我对你有帮助的话,动动小手点个赞再走呗~~