定义

线段树(segment tree),顾名思义, 是用来存放给定区间(segment, or interval)内对应信息的一种数据结构。与树状数组(binary indexed tree)相似,线段树也用来处理数组相应的区间查询(range query)和元素更新(update)操作。与树状数组不同的是,线段树不止可以适用于区间求和的查询,也可以进行区间最大值,区间最小值(Range Minimum/Maximum Query problem)或者区间异或值的查询。

对应于树状数组,线段树进行更新(update)的操作为O(logn),进行区间查询(range query)的操作也为O(logn)

实现原理

从数据结构的角度来说,线段树是用一个完全二叉树来存储对应于其每一个区间(segment)的数据。该二叉树的每一个结点中保存着相对应于这一个区间的信息。同时,线段树所使用的这个二叉树是用一个数组保存的,与堆(Heap)的实现方式相同。

例如,给定一个长度为N的数组arr,其所对应的线段树T各个结点的含义如下:
1. T的根结点代表整个数组所在的区间对应的信息,及arr[0:N]不含N)所对应的信息。
2. T的每一个叶结点存储对应于输入数组的每一个单个元素构成的区间arr[i]所对应的信息,此处0≤i<N
3. T的每一个中间结点存储对应于输入数组某一区间arr[i:j]对应的信息,此处0≤i<j<N

以根结点为例,根结点代表arr[0:N]区间所对应的信息,接着根结点被分为两个子树,分别存储arr[0:(N-1)/2]arr[(N-1)/2+1:N]两个子区间对应的信息。也就是说,对于每一个结点,其左右子结点分别存储母结点区间拆分为两半之后各自区间的信息。也就是说对于长度为N的输入数组,线段树的高度为logN

对于一个线段树来说,其应该支持的两种操作为:
1. Update:更新输入数组中的某一个元素并对线段树做相应的改变。
2. Query:用来查询某一区间对应的信息(如最大值,最小值,区间和等)。

线段树的初始化

线段树的初始化是自底向上进行的。从每一个叶子结点开始(也就是原数组中的每一个元素),沿从叶子结点到根结点的路径向上按层构建。在构建的每一步中,对应两个子结点的数据将被用来构建应当存储于它们母结点中的值。每一个中间结点代表它的左右两个子结点对应区间融合过后的大区间所对应的值。这个融合信息的过程可能依所需要处理的问题不同而不同(例如对于保存区间最小值的线段树来说,merge的过程应为min()函数,用以取得两个子区间中的最小区间最小值作为当前融合过后的区间的区间最小值)。但从叶子节点(长度为1的区间)到根结点(代表输入的整个区间)更新的这一过程是统一的。

注意此处我们对于segmentTree]数组的索引从1开始算起。则对于数组中的任意结点i,其左子结点为2*i,右子结点为2*i + 1,其母结点为i/2

构建线段树的算法描述如下:

construct(arr):n = length(arr)segmentTree = new int[2*n]for i from n to 2*n-1:segmentTree[i] = arr[i - n]for i from n - 1 to 1:segmentTree[i] = merge(segmentTree[2*i], segmentTree[2*i+1])

例如给定一个输入数组[1,5,3,7,3,2,5,7],其所对应的最小值线段树应如下图所示:

上图所示线段树每一个结点代表的区间则如下图所示:

如果用其数组表示来说,则数组segmentTree中的每一个位置代表的区间如下:

segmentTree[1] = arr[0:8)
segmentTree[2] = arr[0:4)
segmentTree[3] = arr[4:8)
segmentTree[4] = arr[0:2)
segmentTree[5] = arr[2:4)
segmentTree[6] = arr[4:6)
segmentTree[7] = arr[6:8)
segmentTree[8] = arr[0]
segmentTree[9] = arr[1]
segmentTree[10] = arr[2]
segmentTree[11] = arr[3]
segmentTree[12] = arr[4]
segmentTree[13] = arr[5]
segmentTree[14] = arr[6]
segmentTree[15] = arr[7]

更新

更新一个线段树的过程与上述构造线段树的过程相同。当输入数组中位于i位置的元素被更新时,我们只需从这一元素对应的叶子结点开始,沿二叉树的路径向上更新至更结点即可。显然,这一过程是一个O(logn)的操作。其算法如下。

update(i, value):i = i + nsegmentTree[i] = valuewhile i > 1:i = i / 2segmentTree[i] = merge(segmentTree[2*i], segmentTree[2*i+1])

例如对于上图中的线段树,如果我们调用update(5, 6),则其更新过程如下所示。

区间查询

区间查询大体上可以分为3种情况讨论:
1. 当前结点所代表的区间完全位于给定需要被查询的区间之外,则不应考虑当前结点
2. 当前结点所代表的区间完全位于给定需要被查询的区间之内,则可以直接查看当前结点的母结点
3. 当前结点所代表的区间部分位于需要被查询的区间之内,部分位于其外,则我们先考虑位于区间外的部分,后考虑区间内的(注意总有可能找到完全位于区间内的结点,因为叶子结点的区间长度为1,因此我们总能组合出合适的区间)

以求最小值为例,其算法如下:

minimum(left, right):left = left + nright = right + nminimum = Integer.MAX_VALUEwhile left < right:if left is odd:// left is out of range of parent interval, check value of left node first, then shift it right in the same levelminimum = min(minimum, segmentTree[left])left = left + 1if right is odd:// right is out of range of current interval, shift it left in the same level and then check the valueright = right - 1minimum = min(minimum, segmentTree[right])// move left and right one level upleft = left / 2right = right / 2

n不是2的次方怎么办?

注意上面的讨论中我们由于需要不断二分区间,给定的输入数组的长度n为2的次方。那么当n不是2的次方,或者说,当n无法被完全二分为一些长度为1的区间时,该如何处理呢?

一个简单的方法是在原数组的结尾补0,直到其长度正好为2的次方位置。但事实上这个方法比较低效。最坏情况下,我们需要O(4n)的空间来存储相应的线段树。例如,如果输入数组的长度刚好为2^x+1,则我们首先需要补0直到数组长度为2^(x+1) = 2*2^x为止。那么对于这个补0过后的数组,我们需要的线段树数组的长度为2*2*2^x = 4*2^x = O(4n)

其实上面所说的算法对于n不是2的次方的情况同样适用。这也是为什么我在上文中说线段树是一棵完全二叉树而非满二叉树的原因。

例如对于输入数组[4,3,9,1,6,7],其构造出的线段树应当如下图所示:

可以看出,在构造过程中我们事实上把一些长度为1的区间直接放在了树的倒数第二层来实现这个线段树。

Java代码

Range Minimum Query

public class MinSegmentTree {private ArrayList<Integer> minSegmentTree;private int n;public MinSegmentTree(int[] arr) {n = arr.length;minSegmentTree = new ArrayList<>(2 * n);for  (int i = 0; i < n; i++) {minSegmentTree.add(0);}for (int i = 0; i < n; i++) {minSegmentTree.add(arr[i]);}for (int i = n - 1; i > 0; i--) {minSegmentTree.set(i, Math.min(minSegmentTree.get(2 * i), minSegmentTree.get(2 * i + 1)));}}public void update(int i, int value) {i += n;minSegmentTree.set(i,  value);while (i > 1) {i /= 2;minSegmentTree.set(i, Math.min(minSegmentTree.get(2 * i), minSegmentTree.get(2 * i + 1)));}}/*** Get the minimum of range [left, right)*/public int minimum(int left, int right) {left += n;right += n;int min = Integer.MAX_VALUE;while (left < right) {if ((left & 1) == 1) {min = Math.min(min, minSegmentTree.get(left));left++;}if ((right & 1) == 1) {right--;min = Math.min(min,  minSegmentTree.get(right));}left >>= 1;right >>= 1;}return min;}
}

Range Sum Query

public class SumSegmentTree {private ArrayList<Integer> sumSegmentTree;private int n;public SumSegmentTree(int[] arr) {n = arr.length;sumSegmentTree = new ArrayList<>(2 * n);for  (int i = 0; i < n; i++) {sumSegmentTree.add(0);}for (int i = 0; i < n; i++) {sumSegmentTree.add(arr[i]);}for (int i = n - 1; i > 0; i--) {sumSegmentTree.set(i, sumSegmentTree.get(2 * i) + sumSegmentTree.get(2 * i + 1));}}public void update(int i, int value) {i += n;sumSegmentTree.set(i,  value);while (i > 1) {i /= 2;sumSegmentTree.set(i, sumSegmentTree.get(2 * i) + sumSegmentTree.get(2 * i + 1));}}/*** Get the sum of range [left, right)*/public int sum(int left, int right) {left += n;right += n;int sum = 0;while (left < right) {if ((left & 1) == 1) {sum += sumSegmentTree.get(left);left++;}if ((right & 1) == 1) {right--;sum += sumSegmentTree.get(right);}left >>= 1;right >>= 1;}return sum;}
}

Reference:
https://www.youtube.com/watch?v=Oq2E2yGadnU

线段树(segment tree),看这一篇就够了相关推荐

  1. 线段树(Segment Tree)

    1.概述 线段树,也叫区间树,是一个完全二叉树,它在各个节点保存一条线段(即"子数组"),因而常用于解决数列维护问题,基本能保证每个操作的复杂度为O(lgN). 线段树是一种二叉搜 ...

  2. BZOJ.4695.最假女选手(线段树 Segment tree Beats!)

    题目链接 区间取\(\max,\ \min\)并维护区间和是普通线段树无法处理的. 对于操作二,维护区间最小值\(mn\).最小值个数\(t\).严格次小值\(se\). 当\(mn\geq x\)时 ...

  3. C语言实现段树segment tree(附完整源码)

    C语言实现段树segment tree 段树结构体定义 实现以下6个接口 完整实现和main测试源码 段树结构体定义 typedef struct segment_tree {void *root; ...

  4. 2019-5-25-win10-uwp-win2d-入门-看这一篇就够了

    title author date CreateTime categories win10 uwp win2d 入门 看这一篇就够了 lindexi 2019-5-25 20:0:52 +0800 2 ...

  5. 面试被问到 ConcurrentHashMap答不出 ,看这一篇就够了!

    本文汇总了常考的 ConcurrentHashMap 面试题,面试 ConcurrentHashMap,看这一篇就够了!为帮助大家高效复习,专门用"★ "表示面试中出现的频率,&q ...

  6. api网关选型_如何轻松打造百亿流量API网关?看这一篇就够了(下)

    如何轻松打造百亿流量API网关?看这一篇就够了(上) 上篇整体描述了网关的背景,涉及职能.分类.定位环节,本篇进入本文的重点,将会具体谈下百亿级流量API网关的演进过程. 准备好瓜子花生小板凳开始积累 ...

  7. python装饰器功能是冒泡排序怎么做_传说中Python最难理解的点|看这完篇就够了(装饰器)...

    https://mp.weixin.qq.com/s/B6pEZLrayqzJfMtLqiAfpQ 1.什么是装饰器 网上有人是这么评价装饰器的,我觉得写的很有趣,比喻的很形象 每个人都有的内裤主要是 ...

  8. serviceloader java_【java编程】ServiceLoader使用看这一篇就够了

    转载:https://www.jianshu.com/p/7601ba434ff4 想必大家多多少少听过spi,具体的解释我就不多说了.但是它具体是怎么实现的呢?它的原理是什么呢?下面我就围绕这两个问 ...

  9. docker 删除所有镜像_关于 Docker 镜像的操作,看完这篇就够啦 !(下)| 文末福利...

    紧接着上篇<关于 Docker 镜像的操作,看完这篇就够啦 !(上)>,奉上下篇 !!! 镜像作为 Docker 三大核心概念中最重要的一个关键词,它有很多操作,是您想学习容器技术不得不掌 ...

  10. mysql ip比较大小_MySQL优化/面试,看这一篇就够了

    原文链接:http://www.zhenganwen.top/articles/2018/12/25/1565048860202.html 作者:Anwen~ 链接:https://www.nowco ...

最新文章

  1. 拍卖源码java_Java并发的AQS原理详解
  2. android 静态编译链接,Android NDK:使用预编译的静态库链接
  3. 在WINCE中的一些VB.NET2005通用方法
  4. 在任意目录导入自定义库
  5. LaTeX伪代码写法总结
  6. php仿微信界面设计,仿微信源码-泡泡IM
  7. 阿里美女面试官问我:Flink资源管理有了解吗
  8. idea 插件开发教程
  9. 年度Sweb绩效考评表
  10. 求ax2+bx+c=0方程的解,要求(1) a=0,不是二次方程。(2) b2-4ac=0,有两个相同的实根。(3)b2-4ac>0,有两个不等的实根。(4)b2-4ac<有两个共轭的复根
  11. 双问号??在 js 中的应用
  12. 用illustrator、AI将边框线转换为填充形状
  13. 【Mac软件推荐】10个你不能没有的Mac菜单栏应用程序
  14. iphone html阅读,iPhone如何使用Safari浏览器阅读列表功能
  15. 每日记录 8.28 TP(真阳率) NP(假阳率) FP
  16. Qt中UI线程与子线程的交互
  17. 基于Anaconda 搭建 OpenCV for Python 环境(全平台通用)
  18. win11 HEVC 扩展
  19. Checked异常和Runtime异常
  20. ISO,GB,GB/T等的区别?

热门文章

  1. OMPL官方教程学习State Validity Checking
  2. 通过添加css样式cursor属性,改变鼠标的外形,变成放大镜
  3. 数据分析EXCEL入门必备
  4. ov7725图像帧率计算公式总结
  5. 关于protel 99 SE如何建立自己的元件库,导入Sch文件file is not recognized.
  6. TPshop商城——windows部署(保姆级)
  7. oop-klass_在PHP和MySQL中处理时间和日期-OOP版本
  8. C++桌面小精灵:实现像Office助手一样的帮助精灵
  9. 安卓手机卸载手机自带软件(adb)
  10. (数位dp) 算法竞赛入门到进阶 书本题集