2019年到了,祝愿大家心想事成,万事如意。 :)
很多人说线段树需要O(2n) ~ O(4n) 的空间。对于线段树这样一个完全二叉树来说,怎么可能没有严格2n的实现方式呢?原因在于对于线段树结构的理解不同。线段树自底向上构建,的确是一棵完全二叉树;然而大部分递归构建的线段树,本身采用的是二分策略,必会导致大量的无用空间。
这里简要说明一下自底向上、迭代地构建线段树的方法:
假设输入长度为size,开辟2*size的空间足矣。构建步骤如下:
- 将原数据复制到开辟的[size, 2*size - 1]区间内
- j从size-1到1迭代,每一步将秩为2j和2j+1的数据合并,储存在秩为j处。
当更新原秩为index处的数据 操作如下:
- 先直接更新size+index位置处的数据
- 令i从(size+index)/2(即其父节点)开始迭代,每一步合并秩为2i和2i+1处的数据,赋值于秩为i处,然后i/=2(这一步其实就是沿着元素的搜索路径重新构建了一次)当i小于等于1结束
查询[left, right)区间的数据之*“和”* 通过递归实现如下:
- left >= right 时停止(递归基,返回空)
- 若left是奇数,则合并left处数据,和递归[left+1,right)后的结果
- 若right是奇数,则合并right-1处数据,和递归[left,right-1)后的结果
- 否则,返回递归[left/2,right/2)的结果
关于为什么分奇偶,简要说明一下:按照区间左闭右开的原则,考察线段树各节点奇偶性不难发现,若区间左端点left是奇数,那么必然是父节点的右孩子,在这种情况下,只能单独把秩为left的元素抽出(这时left+1就是其父的兄弟的左孩子,这是完全二叉树的性质); 如果右端点right是偶数也是同理的。只有在二者都是偶数的时候,线段树的上一层才会完全包含要查询的区间。
总结来说,就是这种线段树的实现不再用节点存储其所对应的区间范围,而是通过树的拓扑结构间接推得。这么做的确是节省了不少空间,但是理解起来确实要难一些。
合并是关键的一步,应该由使用者自己给定。因此设计泛化的线段树如下:
|
|
其中,具体怎么合并两个数据项由函数对象_merge指定,比如求和操作可以用λ表达式实现为:[](auto a, auto b){return a+b}
显然,这样构建的线段树空间复杂度为$\Omega(2n)$。
P.S. 实际运用时需要加上运行时边界检查。