【从蛋壳到满天飞】JS 数据结构解析和算法实现-线段树

思惟导图

前言

【从蛋壳到满天飞】JS 数据结构解析和算法实现,所有文章大概的内容以下: Arrays(数组)、Stacks(栈)、Queues(队列)、LinkedList(链表)、Recursion(递归思想)、BinarySearchTree(二分搜索树)、Set(集合)、Map(映射)、Heap(堆)、PriorityQueue(优先队列)、SegmentTree(线段树)、Trie(字典树)、UnionFind(并查集)、AVLTree(AVL 平衡树)、RedBlackTree(红黑平衡树)、HashTable(哈希表)html

源代码有三个:ES6(单个单个的 class 类型的 js 文件) | JS + HTML(一个 js 配合一个 html)| JAVA (一个一个的工程)git

所有源代码已上传 github,点击我吧,光看文章可以掌握两成,动手敲代码、动脑思考、画图才能够掌握八成。github

本文章适合 对数据结构想了解而且感兴趣的人群,文章风格一如既往如此,就以为手机上看起来比较方便,这样显得比较有条理,整理这些笔记加源码,时间跨度也算将近半年时间了,但愿对想学习数据结构的人或者正在学习数据结构的人群有帮助。面试

线段树(区间树)

  1. 堆(Heap)是一种树结构
  2. 线段树(Segment Tree)也是一种树结构
    1. 也叫作区间树(Interval Tree)

线段树简介

为何使用线段树

  1. 为何使用线段树,线段树解决什么样的特殊问题
    1. 对于有一类的问题,只须要关心的是一个线段(或者区间),
    2. 有一道竞赛的题目,也是最经典的线段树问题:区间染色,
    3. 它是一个很是好的应用线段树的场景。
    4. 有一面墙,长度为 n,每次选择一段儿墙进行染色,
    5. m 次操做后,能够看到多少种颜色?
    6. m 次操做后,能够在[i,j]区间内看到多少种颜色?
    7. 染色操做(更新区间)、查询操做(查询区间)。
  2. 彻底可使用数组实现来解决这个问题
    1. 若是你要对某一段区间进行染色,
    2. 那么就遍历这一段区间,把相应的值修改为新的元素就 ok 了,
    3. 染色操做(更新区间)相应的复杂度就是O(n)级别的,
    4. 查询操做(查询区间)也只须要遍历一遍这个区间就 ok 了,
    5. 相应的复杂度也是O(n)级别的,
    6. 若是对于一种数据结构,其中某一些操做是O(n)级别的话,
    7. 若是动态的使用这种数据结构,相应的性能颇有多是不够的,
    8. 在实际的环境中颇有可能须要性能更加好的这样一种时间复杂度,
    9. 这就是使用数组来实现区间染色问题相应的一个局限性
  3. 在这样的问题中主要关注的是区间或一个个的线段,
    1. 因此此时线段树这样的数据结构就有用武之地了,
    2. 在平时使用计算机来处理数据的时候,
    3. 有一类很经典的同时也是应用范围很是广的的问题,
    4. 就是进行区间查询,相似统计操做的查询,
    5. 查询一个区间[i,j]的最大值、最小值,或者区间数字和。
  4. 实质:基于区间的统计查询
    1. 问题一:2017 年注册用户中到如今为止消费最高的用户、消费最少的用户、
    2. 学习时间最长的用户?
    3. 2017 年注册的到如今为止,这个数据其实还在不断的变化,
    4. 是一种动态的状况,此时线段树就是一个好的选择。
    5. 问题二:某个太空区间中天体总量?
    6. 因为天体不断的在运动,总会有一个天体从一个区间来到另一个区间,
    7. 甚至发生爆炸消失之类的物理现象,在某个区间中或某几个区间中
    8. 都多了一些天体,会存在这样的现象的,因此就须要使用线段树了。
  5. 对于这面墙有一个不停的对一个区间进行染色这样的一个操做,
    1. 就是更新这个操做,于此同时基于整个数据不时在不停的在更新,
    2. 还须要进行查询这样的两个操做,同理其实对于这些问题,
    3. 可使用数组来实现,不过它的复杂度都是O(n)级别的,
    4. 可是若是使用线段树的话,那么在区间类的统计查询这一类的问题上,
    5. 更新和查询这两个操做均可以在O(logn)这个复杂度内完成。
  6. 对于线段树来讲它必定也是一种二叉树的结构
    1. 对于线段树抽象出来就是解决这样的一类问题,
    2. 对于一个给定的区间,相应的要支持两个操做,
    3. 更新:更新区间中一个元素或者一个区间的值,
    4. 查询:查询一个区间[i,j]的最大值、最小值、或者区间数字和,
    5. 对于一个区间能够查询的内容会不少,要根据实际的业务逻辑进调整,
    6. 不过总体是基于一个区间进行这种统计查询的。
  7. 如何使用 logn 复杂度去实现这一点
    1. 首先对于给定的数组进行构建,假若有八个元素,
    2. 把它们构建成一棵线段树,对于线段树来讲,
    3. 不考虑往线段树中添加元素或者删除元素的。
    4. 在大多数状况下线段树所解决的问题它的区间是固定的,
    5. 好比一面墙进行区间染色,那面墙自己是固定的,
    6. 不去考虑这面墙后面又建起了新的一面墙这种状况,
    7. 只考虑给定的这一面墙进行染色;
    8. 好比统计 2017 年注册的用户,那么这个区间是固定的;
    9. 或者观察天体,观察的外太空所划分的区间已经固定了,
    10. 只是区间中的元素可能会发生变化;
    11. 因此对于这个数组直接使用静态数组就行了。
  8. 线段树也是一棵树,每个节点表示一个区间内相应的信息
    1. 好比 以线段树来统计区间内的和为例,
    2. 在这样的状况下,
    3. 线段树每个节点存储的就是一段区间的数字和,
    4. 根节点存储的就是整个区间相应的数字和,
    5. 以后从根节点平均将整个的区间分红两段,
    6. 这两段表明着两个节点,相应的这两个节点会再分出两个区间,
    7. 直至最后每个叶子节点只会存一个元素,
    8. 从区间的角度上来说,每一个元素自己就是一个区间,
    9. 只不过每个区间的长度为 1 而已,
    10. 也就是对于整棵线段树来讲每个节点存储的是
    11. 一个区间中相应的统计值,好比使用线段树求和,
    12. 每一个节点存储的是一个区间中数字和,
    13. 当你查询某个区间的话,相应的你只须要找到对应的某个节点便可,
    14. 一步就到了这个节点,并不须要将全部的元素所有都遍历一遍了,
    15. 可是并非全部的节点每次都知足这样的条件,
    16. 因此有时候须要到两个节点,将两个节点进行相应的结合,
    17. 结合以后就能够获得你想要的结果,尽管如此,当数据量很是大的时候,
    18. 依然能够经过线段树很是快的找到你所关心的那个区间对应的一个或者多个节点,
    19. 而后在对那些节点的内容进行操做,
    20. 而不须要对那个区间中全部的元素中每个元素相应的进行一次遍历,
    21. 这一点也是线段树的优点。

线段树基础表示

  1. 线段树就是二叉树每个节点存储的是一个线段(区间)相应的信息
    1. 这个相应的信息不是指把这个区间中全部的元素都存进去,
    2. 好比 以求和操做为例,
    3. 那么每个节点相应的存储的就是这个节点所对应的区间的那个数字和。
  2. 线段树不必定是满的二叉树,
    1. 线段树也不必定是一棵彻底二叉树,
    2. 线段树是一棵平衡二叉树,
    3. 也就是说对于整棵树来讲,最大的深度和最小的深度,
    4. 它们之间的差最多只能为 1,堆也是一棵平衡二叉树,
    5. 彻底二叉树自己也是一棵平衡二叉树,
    6. 线段树虽然不是一棵彻底二叉树,可是它知足平衡二叉树的定义,
    7. 二分搜索树不必定是一棵平衡二叉树,
    8. 由于二分搜索树没有任何机制可以保证最大深度和最小深度之间差不超过 1。
  3. 平衡二叉树的优点
    1. 不会像二分搜索树那样在最差的状况下退化为一个链表,
    2. 一棵平衡二叉树整棵树的高度和它的节点之间的关系必定是一个 log 之间的关系,
    3. 这使得在平衡二叉树上搜索查询是很是高效的。
  4. 线段树虽然不是彻底二叉树
    1. 可是这样的一个平衡二叉树,
    2. 也可使用数组的方式来表示,
    3. 对线段树来讲其实能够把它看做是一棵满二叉树,
    4. 可是可能在最后一层不少节点是不存在的,
    5. 对于这些不存在的节点只须要把它看做是空便可,
    6. 这样一来就是一棵满二叉树了,满二叉树是一棵特殊的彻底二叉树,
    7. 那么它就必定可使用数组来表示。
  5. 满二叉树的性质
    1. 满二叉树每层的节点数与层数成次方关系,0 层就是 2^0,1 层就是 2^1,
    2. 最后一层的节点数是 前面全部层的节点之和 而后再加上一
    3. (当前层节点数是 前面全部层节点数的总和 而后另外再加一),
    4. 最后一层的节点数是 前面一层节点的两倍
    5. (当前层节点数是 前一层节点数的两倍)
    6. 整棵满二叉树实际的节点个数就是2^h-1
    7. (最后一层也就是(h-1层),有2^(h-1)个节点,
    8. 最后一层节点数是 前面全部层节点数的总和 另外再加一,
    9. 因此总节点数也就是2 * 2^(h-1)-1个节点,这样一来就是2^h-1个)。
  6. 那么就有一个问题了,若是区间中有 n 个元素
    1. 那么使用数组表示时那么数组的空间大小是多少,
    2. 也就是这棵线段树上应该有多少个节点,
    3. 对于一棵满的二叉树,这一棵的层数和每一层的节点之间是有规律的,
    4. 第 0 层节点数为 1,第 1 层节点数为 2,第 2 层节点数为 4,第 3 层节点数为 8,
    5. 那么第(h-1)层节点数为2^(h-1),下层节点的数量是上层节点数量的 2 倍,
    6. 第 3 层的节点数量是第 2 层的节点数量的 2 倍,
    7. 因此对于满二叉树来讲,h 层,一共有2^h-1个节点(大约是2^h),
    8. 这是等比数列求和的公式,
    9. 那么当数组的空间为2^h时必定能够装下满二叉树全部的元素,
    10. 最后一层(h-1层),有2^(h-1)个节点,
    11. 那么最后一层的节点数大体等于前面全部层节点之和。
  7. 那么原来的问题是若是区间有 n 个元素,数组表示须要有多少节点?
    1. 答案是 log 以 2 为底的 n 为多少,也就是 2 的多少次方为 n,
    2. 若是这 n 是 2 的整数次幂,那么只须要 2n 的空间,
    3. 这是由于除了最后一层以外,上层的全部节点大概也等于 n,
    4. 虽然实际来讲是 n-1,可是这一个空间富余出来没有关系,
    5. 只须要 2n 的空间就足以存储整棵树了,
    6. 可是关键是一般这个 n 不必定是 2 的 k 次方幂,
    7. 也就是这个 n 不必定是 2 的整数次幂,如 n=2^k+r,r 确定不等于 2^k,
    8. 那么在最坏的状况下,若是 n=2^k+1,
    9. 那么最后一层不足以存储整个叶子节点的,
    10. 由于叶子节点的索引范围会超出 2n 的数组范围内,n=2^k+3 就会超出,
    11. 那么叶子节点确定是在倒数的两层的范围里,
    12. 那么就还须要再加一层,加的这一层若是使用满二叉树的方式存储的话,
    13. 那么就在原来的基础上再加一倍的空间,此时整棵满二叉树须要 4n 的空间,
    14. 这样才能够存储全部的节点,对于建立的这棵线段树来讲,
    15. 若是你考虑的这个区间一共有 n 个元素,
    16. 那么选择使用数组的方式进行存储的话,
    17. 只须要有 4n 的空间就能够存储整棵线段树了,
    18. 在这 4n 的空间里并非全部的空间都被利用了,
    19. 由于这个计算自己是一个估计值,
    20. 在计算的过程当中不是严格的正好能够存储整个线段树的全部的节点,
    21. 其实作了一些富余,对于线段树来讲并不必定是一棵满二叉树,
    22. 因此才在最后一层的地方,颇有可能不少位置都是空的,
    23. 这 4n 的空间有多是有浪费掉的,
    24. 在最坏的状况下至少有一半的空间是被浪费掉的,
    25. 可是不过分的考虑这些浪费的状况,
    26. 对于现代计算机来讲存储空间自己并非问题,
    27. 作算法的关键就是使用空间来换时间,但愿在时间性能上有巨大的提高,
    28. 这部分浪费自己也是能够避免的,不使用数组来存储整棵线段树,
    29. 而使用链式的结构如二分搜索树那种节点的方式来存储整棵线段树,
    30. 就能够避免这种空间的浪费。
  8. 若是区间有 n 个元素,数组须要开 4n 的空间就行了,
    1. 于此同时这 4n 的空间是一个静态的空间,
    2. 由于对于线段树来讲并不考虑添加元素,
    3. 也就是说考虑的整个区间是固定的,这个区间的大小不会再改变了,
    4. 真正改变的是区间中的元素,因此不须要使用本身实现的动态数组,
    5. 直接开 4n 的静态空间便可。

代码示例(class: MySegmentTree)

  1. MySegmentTree算法

    // 自定义线段树 SegmentTree
    class MySegmentTree {
       constructor(array) {
          // 拷贝一份参数数组中的元素
          this.data = new Array(array.length);
          for (var i = 0; i < array.length; i++) this.data[i] = array[i];
    
          // 初始化线段树 开4倍的空间 这样才能在全部状况下存储线段树上全部的节点
          this.tree = new Array(4 * this.data.length);
       }
    
       // 获取线段树中实际的元素个数
       getSize() {
          return this.data.length;
       }
    
       // 根据索引获取元素
       get(index) {
          if (index < 0 || index >= this.getSize())
             throw new Error('index is illegal.');
          return this.data[index];
       }
    
       // 辅助函数:返回彻底二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引
       // 计算出线段树中指定索引位置的元素其左孩子节点的索引 -
       calcLeftChildIndex(index) {
          return index * 2 + 1;
       }
    
       // 辅助函数:返回彻底二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引
       // 计算出线段树中指定索引位置的元素其右孩子节点的索引 -
       calcRightChildIndex(index) {
          return index * 2 + 2;
       }
    }
    复制代码

建立线段树

  1. 将线段树看做是一棵满的二叉树
    1. 这样一来就可使用数组来存储整个线段树上全部的节点了,
    2. 若是考虑的这个区间中有 n 个元素,那么这个数组就须要开 4n 个空间。
  2. 在数组中存储什么才能够构建出一棵线段树
    1. 这个逻辑是一个很是典型的递归逻辑,对于这个线段树的定义,
    2. 根节点所存储的信息实际上就是它的两个孩子所存储的信息相应的一个综合,
    3. 怎么去综合是以业务逻辑去定义的,
    4. 好比是以求和为例,建立这棵线段树是为了查询区间中数据的元素和这样的一个操做,
    5. 相应的每个节点存储的就是相应的一个区间中全部元素的和,
    6. 好比有十个元素,那么根节点存储的就是这十个元素的和,
    7. 相应它分出两个孩子节点,左孩子就是这十个元素中前五个元素相应的和,
    8. 右孩子就是这十个元素中后五个元素相应的和,
    9. 这两个节点下面的左右孩子节点依次再这样的划分,直到到达叶子节点。
  3. 这整个过程至关因而建立这棵线段树根
    1. 建立这棵线段树的根必须先建立好这个根节点对应的左右两个子树,
    2. 只要有了这左右两个子树的根节点,
    3. 那么这个线段树的根节点对应的这个值就是它的两个孩子所对应的值进行一下加法运算便可,
    4. 对于左右两棵子树的建立也是如此,为了要建立它们的根节点,
    5. 那么仍是要建立这个根节点对应的左右两个子树,依此类推,直到递归到底为止,
    6. 也就是这个节点所对应的区间不可以再划分了,该节点所存储的这个区间的长度只为 1 了,
    7. 这个区间只有一个元素,对于这一个元素,它的和就是这一个元素自己,那么就递归到底了,
    8. 总体这个递归结构就是如此清晰的。
  4. BuildingSegmentTree 的方法
    1. 有三个参数;
    2. 第一个参数是在初始的时候这个线段树对应的索引,索引应该为 0,表示从 0 开始;
    3. 第2、三参数是指对于这个节点它所表示的那个线段(区间)左右端点是什么,初始的时候,
    4. 左端点的索引应该为 0,右端点的索引应该为原数组的长度减 1;
    5. 递归使用的时候,
    6. 也就是在 treeIndex 的位置建立表示区间[l...r]的线段树。
  5. BuildingSegmentTree 的方法的逻辑
    1. 若是真的要表示一个区间的话,那么相应的处理方式是这样的,
    2. 先获取这个区间的左右节点的索引,这个节点必定会有左右孩子,
    3. 先建立和这个节点的左右子树,基于两个区间才能建立线段树,
    4. 计算这个区间的左右范围,计算公式:mid = (left + right) / 2
    5. 这个计算可能会出现整型溢出的问题,可是几率很低,
    6. 那么计算公式能够换一种写法:mid = left + (right - left) / 2
    7. 左子树区间为 left至mid,右子树区间为 mid+1至right
    8. 递归建立线段树,以后进行业务处理操做,
    9. 例如 求和、取最大值、取最小值,综合左右两个线段的信息,
    10. 来获得当前的更大的这个线段相应的信息,若是去综合,是根据你的业务逻辑来决定的,
    11. 使用一个如何去综合的接口,这样一来就会根据你传入的方法来进行综合的操做。
    12. 这个和 自定义的优先队列中的 updateCompare 传入的 方法的意义是同样的,
    13. 只不过 updateCompare 是传入比较的方法,用来在优先队列中如何比较两个元素值,
    14. 而 updateMerge 是传入融合的方法,用来线段树中构建线段树时两个元素如何去融合。

代码示例

  1. (class: MySegmentTree, class: Main)数组

  2. MySegmentTree:线段树数据结构

    // 自定义线段树 SegmentTree
    class MySegmentTree {
       constructor(array) {
          // 拷贝一份参数数组中的元素
          this.data = new Array(array.length);
          for (var i = 0; i < array.length; i++) this.data[i] = array[i];
    
          // 初始化线段树 开4倍的空间 这样才能在全部状况下存储线段树上全部的节点
          this.tree = new Array(4 * this.data.length);
    
          // 开始构建线段树
          this.buildingSegmentTree(0, 0, this.data.length - 1);
       }
    
       // 获取线段树中实际的元素个数
       getSize() {
          return this.data.length;
       }
    
       // 根据索引获取元素
       get(index) {
          if (index < 0 || index >= this.getSize())
             throw new Error('index is illegal.');
          return this.data[index];
       }
    
       // 构建线段树
       buildingSegmentTree(treeIndex, left, right) {
          // 解决最基本问题
          // 当一条线段的两端相同时,说明这个区间只有一个元素,
          // 那么递归也到底了
          if (left === right) {
             this.tree[treeIndex] = this.data[left];
             return;
          }
    
          // 计算当前线段树的左右子树的索引
          const leftChildIndex = this.calcLeftChildIndex(treeIndex);
          const rightChildIndex = this.calcRightChildIndex(treeIndex);
    
          // 将一个区间拆分为两段,而后继续构建其左右子线段树
          let middle = Math.floor(left + (right - left) / 2); //(left + right) / 2
    
          // 构建左子线段树
          this.buildingSegmentTree(leftChildIndex, left, middle);
          // 构建右子线段树
          this.buildingSegmentTree(rightChildIndex, middle + 1, right);
    
          // 融合左子线段树和右子线段树
          this.tree[treeIndex] = this.merge(
             this.tree[leftChildIndex],
             this.tree[rightChildIndex]
          );
       }
    
       // 辅助函数:返回彻底二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引
       // 计算出线段树中指定索引位置的元素其左孩子节点的索引 -
       calcLeftChildIndex(index) {
          return index * 2 + 1;
       }
    
       // 辅助函数:返回彻底二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引
       // 计算出线段树中指定索引位置的元素其右孩子节点的索引 -
       calcRightChildIndex(index) {
          return index * 2 + 2;
       }
    
       // 辅助函数: 融合两棵线段树,也就是对线段树进行业务逻辑的处理
       merge(treeElementA, treeElmentB) {
          // 默认进行求和操做
          return treeElementA + treeElmentB;
       }
    
       // 辅助函数:更新融合的方法,也就是自定义处理线段树融合的业务逻辑
       updateMerge(mergeMethod) {
          this.merge = mergeMethod;
       }
    
       // @Override toString() 2018-11-7 jwl
       toString() {
          let segmentTreeConsoleInfo = ''; // 控制台信息
          let segmentTreePageInfo = ''; // 页面信息
    
          // 输出头部信息
          segmentTreeConsoleInfo += 'SegmentTree:';
          segmentTreePageInfo += 'SegmentTree:';
          segmentTreeConsoleInfo += '\r\n';
          segmentTreePageInfo += '<br/><br/>';
    
          // 输出传入的数据信息
          segmentTreeConsoleInfo += 'data = [';
          segmentTreePageInfo += 'data = [';
    
          for (let i = 0; i < this.data.length - 1; i++) {
             segmentTreeConsoleInfo += this.data[i] + ',';
             segmentTreePageInfo += this.data[i] + ',';
          }
    
          if (this.data != null && this.data.length != 0) {
             segmentTreeConsoleInfo += this.data[this.data.length - 1];
             segmentTreePageInfo += this.data[this.data.length - 1];
          }
          segmentTreeConsoleInfo += '],\r\n';
          segmentTreePageInfo += '],<br/><br/>';
    
          // 输出生成的线段树信息
          segmentTreeConsoleInfo += 'tree = [';
          segmentTreePageInfo += 'tree = [';
          let treeSize = 0;
          for (let i = 0; i < this.tree.length - 1; i++) {
             if (this.tree[i] !== undefined) treeSize++;
             segmentTreeConsoleInfo += this.tree[i] + ',';
             segmentTreePageInfo += this.tree[i] + ',';
          }
          if (this.tree != null && this.tree.length != 0) {
             if (this.tree[this.tree.length - 1] !== undefined) treeSize++;
             segmentTreeConsoleInfo += this.tree[this.tree.length - 1];
             segmentTreePageInfo += this.tree[this.tree.length - 1];
          }
          segmentTreeConsoleInfo += '],\r\n';
          segmentTreePageInfo += '],<br/><br/>';
          segmentTreeConsoleInfo += 'originArraySize:' + this.getSize() + ',';
          segmentTreePageInfo += 'originArraySize:' + this.getSize() + ',';
          segmentTreeConsoleInfo +=
             'treeCapacity: ' + this.tree.length + ',treeSize: ' + treeSize;
          segmentTreePageInfo +=
             'treeCapacity: ' + this.tree.length + ',treeSize: ' + treeSize;
    
          // 返回输出的总信息
          document.body.innerHTML += segmentTreePageInfo;
          return segmentTreeConsoleInfo;
       }
    }
    复制代码
  3. Main:主函数ide

    // main 函数
    class Main {
       constructor() {
          this.alterLine('MySegmentTree Area');
          // 初始数据
          const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
          // 初始化线段树,将初始数据和融合器传入进去
          let mySegmentTree = new MySegmentTree(nums);
          // 指定线段树的融合器
          mySegmentTree.updateMerge((a, b) => a + b);
    
          // 输出
          console.log(mySegmentTree.toString());
       }
    
       // 将内容显示在页面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展现分割线
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    
    // 页面加载完毕
    window.onload = function() {
       // 执行主函数
       new Main();
    };
    复制代码

线段树查询

  1. 要有两个查询方法,一个普通查询,一个是递归查询。
  2. 普通查询
    1. 有两个参数,也就是你要查询的区间,左端点与右端点的索引,
    2. 先检查 待查询的区间左右两端的索引是否符合要求,有没有越界,
    3. 而后调用递归查询,
    4. 首次递归函数调用时,须要从根节点开始,也就是第一个参数索引为 0,
    5. 以及搜索范围从根节点的左右两端开始也就是从 0 到原数组的长度减 1,
    6. 而后就是你要指定要查询的线段(区间),也就是从一个大范围内找到一个小线段(区间),
    7. 最后也是获取这个线段(区间),
    8. 其实就是获取这个线段(区间)在进行过业务处理操做后获得的结果,
    9. 如 求和、取最大值、取最小值,综合线段(区间)树的信息返回最终结果。
  3. 递归查询
    1. 有五个参数,
    2. 第一个 当前节点所对应的索引,
    3. 第二个第三个 当前节点它所表示的那个线段(区间)左右端点是什么,
    4. 第四个第五个 待查询的线段(区间),也就是要查询的这个线段(区间)的左右端点。
  4. 递归查询的逻辑
    1. 若是查询范围的左右端点恰好与待查询的线段(区间)的左右端点一致,
    2. 那么就说明当前正好就查询到了待查询的这个线段了,那么直接返回当前当前节点便可,
    3. 不一致的话,说明还须要向下缩小可查询范围,从而可以匹配到待查询的这个线段(区间)。
    4. 向下缩小范围的方式,就是当前这个节点的左右端点之和除以 2,获取左右端点的中间值,
    5. 求出 middle 以后,再继续递归,查询当前节点的左右孩子节点,
    6. 查询范围是当前节点的左端点到 middle 以及 middle+1 到右端点,
    7. 可是查询以前要判断待查询的线段(区间)到底在当前节点左子树中仍是右子树中,
    8. 若是在左子树中那么就直接把查询范围定位到当前节点左孩子节点中,
    9. 若是在右子树中那么就直接把查询范围定位到当前节点右孩子节点中,
    10. 这样就完成了在一个节点的左子线段树或右子线段树中再继续查找了,
    11. 这个查询范围在很明确状况下开始收缩,
    12. 直到查询范围的左右端点恰好与待查询的线段(区间)的左右端点彻底一致,
    13. 递归查询就完毕了,直接返回那个线段(区间)的节点便可。
    14. 可是问题来了,若是待查询的线段(区间)很不巧的同时分布在
    15. 某一个线段(区间)左右子线段树中,这样一来就永远都没法匹配到
    16. 查询范围的左右端点恰好与待查询的线段(区间)的左右端点一致的状况,
    17. 那就麻烦了,那么就须要同时在某一个线段(区间)左右子线段树中查询,
    18. 查询的时候待查询的线段(区间)也要作相应的缩小,由于查询的范围也缩小了,
    19. 若是待查询的线段(区间)不作相应的缩小,那就会造成死递归,
    20. 由于永远没法彻底匹配,随着查询的范围缩小,待查询的线段(区间)会大于这个查询范围,
    21. 待查询的线段(区间)缩小的方式和查询范围缩小的方式一致,
    22. 从待查询的线段(区间)左端点到 middle 以及 middle+1 到右端点,
    23. 最后将查询到的两个结果进行一下融合,最终返回这个融合的结果,
    24. 同样能够达到如此的效果。

代码示例

  1. (class: MySegmentTree, class: Main)函数

  2. MySegmentTree:线段树性能

    // 自定义线段树 SegmentTree
    class MySegmentTree {
       constructor(array) {
          // 拷贝一份参数数组中的元素
          this.data = new Array(array.length);
          for (var i = 0; i < array.length; i++) this.data[i] = array[i];
    
          // 初始化线段树 开4倍的空间 这样才能在全部状况下存储线段树上全部的节点
          this.tree = new Array(4 * this.data.length);
    
          // 开始构建线段树
          this.buildingSegmentTree(0, 0, this.data.length - 1);
       }
    
       // 获取线段树中实际的元素个数
       getSize() {
          return this.data.length;
       }
    
       // 根据索引获取元素
       get(index) {
          if (index < 0 || index >= this.getSize())
             throw new Error('index is illegal.');
          return this.data[index];
       }
    
       // 构建线段树
       buildingSegmentTree(treeIndex, left, right) {
          // 解决最基本问题
          // 当一条线段的两端相同时,说明这个区间只有一个元素,
          // 那么递归也到底了
          if (left === right) {
             this.tree[treeIndex] = this.data[left];
             return;
          }
    
          // 计算当前线段树的左右子树的索引
          const leftChildIndex = this.calcLeftChildIndex(treeIndex);
          const rightChildIndex = this.calcRightChildIndex(treeIndex);
    
          // 将一个区间拆分为两段,而后继续构建其左右子线段树
          let middle = Math.floor(left + (right - left) / 2); //(left + right) / 2
    
          // 构建左子线段树
          this.buildingSegmentTree(leftChildIndex, left, middle);
          // 构建右子线段树
          this.buildingSegmentTree(rightChildIndex, middle + 1, right);
    
          // 融合左子线段树和右子线段树
          this.tree[treeIndex] = this.merge(
             this.tree[leftChildIndex],
             this.tree[rightChildIndex]
          );
       }
    
       // 查询指定区间的线段树数据
       // 返回区间[queryLeft, queryRight]的值
       query(queryLeft, queryRight) {
          if (
             queryLeft < 0 ||
             queryRight < 0 ||
             queryLeft > queryRight ||
             queryLeft >= this.data.length ||
             queryRight >= this.data.length
          )
             throw new Error('queryLeft or queryRight is illegal.');
    
          // 调用递归的查询方法
          return this.recursiveQuery(
             0,
             0,
             this.data.length - 1,
             queryLeft,
             queryRight
          );
       }
    
       // 递归的查询方法 -
       // 在以treeIndex为根的线段树中[left...right]的范围里,
       // 搜索区间[queryLeft...queryRight]的值
       recursiveQuery(treeIndex, left, right, queryLeft, queryRight) {
          // 若是查询范围 与 指定的线段树的区间 相同,那么说明彻底匹配,
          // 直接返回当前这个线段便可,每个节点表明 一个线段(区间)处理后的结果
          if (left === queryLeft && right === queryRight)
             return this.tree[treeIndex];
    
          // 求出当前查询范围的中间值
          const middle = Math.floor(left + (right - left) / 2);
    
          // 满二叉树确定有左右孩子节点
          // 上面的判断没有彻底匹配,说明须要继续 缩小查询范围,也就是要在左右子树中进行查询了
          const leftChildIndex = this.calcLeftChildIndex(treeIndex);
          const rightChildIndex = this.calcRightChildIndex(treeIndex);
    
          // 判断:
          // 1. 从左子树中查仍是右子树中查,又或者从左右子树中同时查,而后将两个查询结果融合。
          // 2. 若是 待查询的区间的左端点大于查询范围的中间值,说明只须要从右子树中进行查询便可。
          // 3. 若是 待查询的区间的右端点小于查询范围的中间值 + 1,说明只须要从左子树中进行查询。
          // 4. 若是 待查询的区间在左右端点各分部一部分,说明要同时从左右子树中进行查询。
          if (queryLeft > middle)
             return this.recursiveQuery(
                rightChildIndex,
                middle + 1,
                right,
                queryLeft,
                queryRight
             );
          else if (queryRight < middle + 1)
             return this.recursiveQuery(
                leftChildIndex,
                left,
                middle,
                queryLeft,
                queryRight
             );
          else {
             // 求出 左子树中一部分待查询区间中的值
             const leftChildValue = this.recursiveQuery(
                leftChildIndex,
                left,
                middle,
                queryLeft,
                middle
             );
             // 求出 右子树中一部分待查询区间中的值
             const rightChildValue = this.recursiveQuery(
                rightChildIndex,
                middle + 1,
                right,
                middle + 1,
                queryRight
             );
             // 融合左右子树种的数据并返回
             return this.merge(leftChildValue, rightChildValue);
          }
       }
    
       // 辅助函数:返回彻底二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引
       // 计算出线段树中指定索引位置的元素其左孩子节点的索引 -
       calcLeftChildIndex(index) {
          return index * 2 + 1;
       }
    
       // 辅助函数:返回彻底二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引
       // 计算出线段树中指定索引位置的元素其右孩子节点的索引 -
       calcRightChildIndex(index) {
          return index * 2 + 2;
       }
    
       // 辅助函数: 融合两棵线段树,也就是对线段树进行业务逻辑的处理 -
       merge(treeElementA, treeElmentB) {
          // 默认进行求和操做
          return treeElementA + treeElmentB;
       }
    
       // 辅助函数:更新融合的方法,也就是自定义处理线段树融合的业务逻辑 +
       updateMerge(mergeMethod) {
          this.merge = mergeMethod;
       }
    
       // @Override toString() 2018-11-7 jwl
       toString() {
          let segmentTreeConsoleInfo = ''; // 控制台信息
          let segmentTreePageInfo = ''; // 页面信息
    
          // 输出头部信息
          segmentTreeConsoleInfo += 'SegmentTree:';
          segmentTreePageInfo += 'SegmentTree:';
          segmentTreeConsoleInfo += '\r\n';
          segmentTreePageInfo += '<br/><br/>';
    
          // 输出传入的数据信息
          segmentTreeConsoleInfo += 'data = [';
          segmentTreePageInfo += 'data = [';
    
          for (let i = 0; i < this.data.length - 1; i++) {
             segmentTreeConsoleInfo += this.data[i] + ',';
             segmentTreePageInfo += this.data[i] + ',';
          }
    
          if (this.data != null && this.data.length != 0) {
             segmentTreeConsoleInfo += this.data[this.data.length - 1];
             segmentTreePageInfo += this.data[this.data.length - 1];
          }
          segmentTreeConsoleInfo += '],\r\n';
          segmentTreePageInfo += '],<br/><br/>';
    
          // 输出生成的线段树信息
          segmentTreeConsoleInfo += 'tree = [';
          segmentTreePageInfo += 'tree = [';
          let treeSize = 0;
          for (let i = 0; i < this.tree.length - 1; i++) {
             if (this.tree[i] !== undefined) treeSize++;
             segmentTreeConsoleInfo += this.tree[i] + ',';
             segmentTreePageInfo += this.tree[i] + ',';
          }
          if (this.tree != null && this.tree.length != 0) {
             if (this.tree[this.tree.length - 1] !== undefined) treeSize++;
             segmentTreeConsoleInfo += this.tree[this.tree.length - 1];
             segmentTreePageInfo += this.tree[this.tree.length - 1];
          }
          segmentTreeConsoleInfo += '],\r\n';
          segmentTreePageInfo += '],<br/><br/>';
          segmentTreeConsoleInfo += 'originArraySize:' + this.getSize() + ',';
          segmentTreePageInfo += 'originArraySize:' + this.getSize() + ',';
          segmentTreeConsoleInfo +=
             'treeCapacity: ' + this.tree.length + ',treeSize: ' + treeSize;
          segmentTreePageInfo +=
             'treeCapacity: ' + this.tree.length + ',treeSize: ' + treeSize;
    
          // 返回输出的总信息
          document.body.innerHTML += segmentTreePageInfo;
          return segmentTreeConsoleInfo;
       }
    }
    复制代码
  3. Main:主函数

    // main 函数
    class Main {
       constructor() {
          this.alterLine('MySegmentTree Area');
          // 初始数据
          const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
          // 初始化线段树,将初始数据和融合器传入进去
          let mySegmentTree = new MySegmentTree(nums);
          // 指定线段树的融合器
          mySegmentTree.updateMerge((a, b) => a + b);
    
          // 输出
          console.log(mySegmentTree.toString());
          this.show('');
          this.alterLine('MySegmentTree Queue Area');
          console.log('查询区间[0, 2]:' + mySegmentTree.query(0, 2));
          this.show('查询区间[0, 2]:' + mySegmentTree.query(0, 2));
          console.log('查询区间[3, 9]:' + mySegmentTree.query(3, 9));
          this.show('查询区间[3, 9]:' + mySegmentTree.query(3, 9));
          console.log('查询区间[0, 9]:' + mySegmentTree.query(0, 9));
          this.show('查询区间[0, 9]:' + mySegmentTree.query(0, 9));
       }
    
       // 将内容显示在页面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展现分割线
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    
    // 页面加载完毕
    window.onload = function() {
       // 执行主函数
       new Main();
    };
    复制代码

Leetcode 上与线段树相关的问题

  1. 303.区域和检索-数组不可变
    1. https://leetcode-cn.com/problems/range-sum-query-immutable/
    2. 方式一:使用线段树
    3. 方式二:对数组进行必定的预处理
  2. 307.区域和检索 - 数组可修改
    1. https://leetcode-cn.com/problems/range-sum-query-mutable/
    2. 方式一:对数组进行必定的预处理,可是性能不是很好

代码示例

  1. 303 方式一 和 方式二

    // 答题
    class Solution {
       // leetcode 303. 区域和检索-数组不可变
       NumArray(nums) {
          /** * @param {number[]} nums * 处理方式一:对原数组进行预处理操做 */
          var NumArray = function(nums) {
             if (nums.length > 0) {
                this.data = new Array(nums.length + 1);
                this.data[0] = 0;
                for (var i = 0; i < nums.length; i++) {
                   this.data[i + 1] = this.data[i] + nums[i];
                }
             }
          };
    
          /** * @param {number} i * @param {number} j * @return {number} */
          NumArray.prototype.sumRange = function(i, j) {
             return this.data[j + 1] - this.data[i];
          };
    
          /** * Your NumArray object will be instantiated and called as such: * var obj = Object.create(NumArray).createNew(nums) * var param_1 = obj.sumRange(i,j) */
    
          /** * @param {number[]} nums * 处理方式二:使用线段树 */
          var NumArray = function(nums) {
             if (nums.length > 0) {
                this.mySegmentTree = new MySegmentTree(nums);
             }
          };
    
          /** * @param {number} i * @param {number} j * @return {number} */
          NumArray.prototype.sumRange = function(i, j) {
             return this.mySegmentTree.query(i, j);
          };
    
          return new NumArray(nums);
       }
    }
    复制代码
  2. 307 方式一

    // 答题
    class Solution {
       // leetcode 307. 区域和检索 - 数组可修改
       NumArray2(nums) {
          /** * @param {number[]} nums * 方式一:对原数组进行预处理操做 */
          var NumArray = function(nums) {
             // 克隆一份原数组
             this.data = new Array(nums.length);
             for (var i = 0; i < nums.length; i++) {
                this.data[i] = nums[i];
             }
    
             if (nums.length > 0) {
                this.sum = new Array(nums.length + 1);
                this.sum[0] = 0;
                for (let i = 0; i < nums.length; i++)
                   this.sum[i + 1] = this.sum[i] + nums[i];
             }
          };
    
          /** * @param {number} i * @param {number} val * @return {void} */
          NumArray.prototype.update = function(i, val) {
             this.data[i] = val;
    
             for (let j = 0; j < this.data.length; j++)
                this.sum[j + 1] = this.sum[j] + this.data[j];
          };
    
          /** * @param {number} i * @param {number} j * @return {number} */
          NumArray.prototype.sumRange = function(i, j) {
             return this.sum[j + 1] - this.sum[i];
          };
    
          /** * Your NumArray object will be instantiated and called as such: * var obj = Object.create(NumArray).createNew(nums) * obj.update(i,val) * var param_2 = obj.sumRange(i,j) */
       }
    }
    复制代码
  3. Main

    // main 函数
    class Main {
       constructor() {
          this.alterLine('leetcode 303. 区域和检索-数组不可变');
          let s = new Solution();
          let nums = [-2, 0, 3, -5, 2, -1];
          let numArray = s.NumArray(nums);
    
          console.log(numArray.sumRange(0, 2));
          this.show(numArray.sumRange(0, 2));
          console.log(numArray.sumRange(2, 5));
          this.show(numArray.sumRange(2, 5));
          console.log(numArray.sumRange(0, 5));
          this.show(numArray.sumRange(0, 5));
       }
    
       // 将内容显示在页面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展现分割线
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    
    // 页面加载完毕
    window.onload = function() {
       // 执行主函数
       new Main();
    };
    复制代码

线段树中的更新操做

  1. 经过 leetcode 上 303 及 307 号题目能够分析出
    1. 使用数组实现时,更新的操做是O(n)级别的,
    2. 查询的操做是O(1)级别的,只不过初始化操做时是O(n)级别的。
    3. 使用线段树实现时,更新和查询的操做都是O(logn)级别的,
    4. 可是线段树在建立的时候是O(n)的复杂度,
    5. 更准确的说是 4 倍的 O(n)的复杂度,
    6. 由于所用的空间是 4n 个,而且要对每一个空间进行赋值。
  2. 对于线段树来讲,要考虑区间这样的这样的一种数据,
    1. 尤为是要查询区间相关的统计信息的时候,
    2. 同时数据是动态的,不时的还须要更新你的数据,在这样的状况下,
    3. 线段树是一种很是好的数据结构,不过对于线段树来讲,
    4. 大多数本科甚至是研究生的算法教材中都不会涉及这种数据结构,
    5. 它自己是一种高级的数据结构,更多的应用于算法竞赛中。
  3. 更新操做和构建线段树的操做相似。
    1. 若是要修改某个索引位置的值,
    2. 那么就须要知道这个索引位置所对应的叶子节点,
    3. 递归到底后就可以知道这个叶子节点,这时候只须要赋值一下,
    4. 而后 从新进行融合操做,由于该索引位置所在的区间须要进行更新,
    5. 只有这样才可以达到修改线段树中某一个节点的值后
    6. 也能够改变相应的线段(区间)。

代码示例

  1. (class: MySegmentTree, class: NumArray2, class: Main)

  2. MySegmentTree

    // 自定义线段树 SegmentTree
    class MySegmentTree {
       constructor(array) {
          // 拷贝一份参数数组中的元素
          this.data = new Array(array.length);
          for (var i = 0; i < array.length; i++) this.data[i] = array[i];
    
          // 初始化线段树 开4倍的空间 这样才能在全部状况下存储线段树上全部的节点
          this.tree = new Array(4 * this.data.length);
    
          // 开始构建线段树
          this.buildingSegmentTree(0, 0, this.data.length - 1);
       }
    
       // 获取线段树中实际的元素个数
       getSize() {
          return this.data.length;
       }
    
       // 根据索引获取元素
       get(index) {
          if (index < 0 || index >= this.getSize())
             throw new Error('index is illegal.');
          return this.data[index];
       }
    
       // 构建线段树
       buildingSegmentTree(treeIndex, left, right) {
          // 解决最基本问题
          // 当一条线段的两端相同时,说明这个区间只有一个元素,
          // 那么递归也到底了
          if (left === right) {
             this.tree[treeIndex] = this.data[left];
             return;
          }
    
          // 计算当前线段树的左右子树的索引
          const leftChildIndex = this.calcLeftChildIndex(treeIndex);
          const rightChildIndex = this.calcRightChildIndex(treeIndex);
    
          // 将一个区间拆分为两段,而后继续构建其左右子线段树
          let middle = Math.floor(left + (right - left) / 2); //(left + right) / 2
    
          // 构建左子线段树
          this.buildingSegmentTree(leftChildIndex, left, middle);
          // 构建右子线段树
          this.buildingSegmentTree(rightChildIndex, middle + 1, right);
    
          // 融合左子线段树和右子线段树
          this.tree[treeIndex] = this.merge(
             this.tree[leftChildIndex],
             this.tree[rightChildIndex]
          );
       }
    
       // 查询指定区间的线段树数据
       // 返回区间[queryLeft, queryRight]的值
       query(queryLeft, queryRight) {
          if (
             queryLeft < 0 ||
             queryRight < 0 ||
             queryLeft > queryRight ||
             queryLeft >= this.data.length ||
             queryRight >= this.data.length
          )
             throw new Error('queryLeft or queryRight is illegal.');
    
          // 调用递归的查询方法
          return this.recursiveQuery(
             0,
             0,
             this.data.length - 1,
             queryLeft,
             queryRight
          );
       }
    
       // 递归的查询方法 -
       // 在以treeIndex为根的线段树中[left...right]的范围里,
       // 搜索区间[queryLeft...queryRight]的值
       recursiveQuery(treeIndex, left, right, queryLeft, queryRight) {
          // 若是查询范围 与 指定的线段树的区间 相同,那么说明彻底匹配,
          // 直接返回当前这个线段便可,每个节点表明 一个线段(区间)处理后的结果
          if (left === queryLeft && right === queryRight)
             return this.tree[treeIndex];
    
          // 求出当前查询范围的中间值
          const middle = Math.floor(left + (right - left) / 2);
    
          // 满二叉树确定有左右孩子节点
          // 上面的判断没有彻底匹配,说明须要继续 缩小查询范围,也就是要在左右子树中进行查询了
          const leftChildIndex = this.calcLeftChildIndex(treeIndex);
          const rightChildIndex = this.calcRightChildIndex(treeIndex);
    
          // 判断:
          // 1. 从左子树中查仍是右子树中查,又或者从左右子树中同时查,而后将两个查询结果融合。
          // 2. 若是 待查询的区间的左端点大于查询范围的中间值,说明只须要从右子树中进行查询便可。
          // 3. 若是 待查询的区间的右端点小于查询范围的中间值 + 1,说明只须要从左子树中进行查询。
          // 4. 若是 待查询的区间在左右端点各分部一部分,说明要同时从左右子树中进行查询。
          if (queryLeft > middle)
             return this.recursiveQuery(
                rightChildIndex,
                middle + 1,
                right,
                queryLeft,
                queryRight
             );
          else if (queryRight < middle + 1)
             return this.recursiveQuery(
                leftChildIndex,
                left,
                middle,
                queryLeft,
                queryRight
             );
          else {
             // 求出 左子树中一部分待查询区间中的值
             const leftChildValue = this.recursiveQuery(
                leftChildIndex,
                left,
                middle,
                queryLeft,
                middle
             );
             // 求出 右子树中一部分待查询区间中的值
             const rightChildValue = this.recursiveQuery(
                rightChildIndex,
                middle + 1,
                right,
                middle + 1,
                queryRight
             );
             // 融合左右子树种的数据并返回
             return this.merge(leftChildValue, rightChildValue);
          }
       }
    
       // 设置指定索引位置的元素 更新操做
       set(index, element) {
          if (index < 0 || index >= this.data.length)
             throw new Error('index is illegal.');
    
          this.recursiveSet(0, 0, this.data.length - 1, index, element);
       }
    
       // 递归的设置指定索引位置元素的方法 -
       // 在以treeIndex为根的线段树中更新index的值为element
       recursiveSet(treeIndex, left, right, index, element) {
          // 解决最基本的问题 递归到底了就结束
          // 由于找到了该索引位置的节点了
          if (left === right) {
             this.tree[treeIndex] = element;
             this.data[index] = element;
             return;
          }
    
          // 求出当前查询范围的中间值
          const middle = Math.floor(left + (right - left) / 2);
    
          // 满二叉树确定有左右孩子节点
          // 上面的判断没有彻底匹配,说明须要继续 缩小查询范围,也就是要在左右子树中进行查询了
          const leftChildIndex = this.calcLeftChildIndex(treeIndex);
          const rightChildIndex = this.calcRightChildIndex(treeIndex);
    
          // 若是指定的索引大于 查询范围的中间值,那就说明 该索引的元素在右子树中
          // 不然该索引元素在左子树中
          if (index > middle)
             this.recursiveSet(
                rightChildIndex,
                middle + 1,
                right,
                index,
                element
             );
          // index < middle + 1
          else this.recursiveSet(leftChildIndex, left, middle, index, element);
    
          // 将改变后的左右子树再进行一下融合,由于递归到底时修改了指定索引位置的元素,
          // 那么指定索引位置所在的线段(区间)也须要再次进行融合操做,
          // 从而达到修改一个值改变 相应的线段(区间)
          this.tree[treeIndex] = this.merge(
             this.tree[leftChildIndex],
             this.tree[rightChildIndex]
          );
       }
    
       // 辅助函数:返回彻底二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引
       // 计算出线段树中指定索引位置的元素其左孩子节点的索引 -
       calcLeftChildIndex(index) {
          return index * 2 + 1;
       }
    
       // 辅助函数:返回彻底二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引
       // 计算出线段树中指定索引位置的元素其右孩子节点的索引 -
       calcRightChildIndex(index) {
          return index * 2 + 2;
       }
    
       // 辅助函数: 融合两棵线段树,也就是对线段树进行业务逻辑的处理 -
       merge(treeElementA, treeElmentB) {
          // 默认进行求和操做
          return treeElementA + treeElmentB;
       }
    
       // 辅助函数:更新融合的方法,也就是自定义处理线段树融合的业务逻辑 +
       updateMerge(mergeMethod) {
          this.merge = mergeMethod;
       }
    
       // @Override toString() 2018-11-7 jwl
       toString() {
          let segmentTreeConsoleInfo = ''; // 控制台信息
          let segmentTreePageInfo = ''; // 页面信息
    
          // 输出头部信息
          segmentTreeConsoleInfo += 'SegmentTree:';
          segmentTreePageInfo += 'SegmentTree:';
          segmentTreeConsoleInfo += '\r\n';
          segmentTreePageInfo += '<br/><br/>';
    
          // 输出传入的数据信息
          segmentTreeConsoleInfo += 'data = [';
          segmentTreePageInfo += 'data = [';
    
          for (let i = 0; i < this.data.length - 1; i++) {
             segmentTreeConsoleInfo += this.data[i] + ',';
             segmentTreePageInfo += this.data[i] + ',';
          }
    
          if (this.data != null && this.data.length != 0) {
             segmentTreeConsoleInfo += this.data[this.data.length - 1];
             segmentTreePageInfo += this.data[this.data.length - 1];
          }
          segmentTreeConsoleInfo += '],\r\n';
          segmentTreePageInfo += '],<br/><br/>';
    
          // 输出生成的线段树信息
          segmentTreeConsoleInfo += 'tree = [';
          segmentTreePageInfo += 'tree = [';
          let treeSize = 0;
          for (let i = 0; i < this.tree.length - 1; i++) {
             if (this.tree[i] !== undefined) treeSize++;
             segmentTreeConsoleInfo += this.tree[i] + ',';
             segmentTreePageInfo += this.tree[i] + ',';
          }
          if (this.tree != null && this.tree.length != 0) {
             if (this.tree[this.tree.length - 1] !== undefined) treeSize++;
             segmentTreeConsoleInfo += this.tree[this.tree.length - 1];
             segmentTreePageInfo += this.tree[this.tree.length - 1];
          }
          segmentTreeConsoleInfo += '],\r\n';
          segmentTreePageInfo += '],<br/><br/>';
          segmentTreeConsoleInfo += 'originArraySize:' + this.getSize() + ',';
          segmentTreePageInfo += 'originArraySize:' + this.getSize() + ',';
          segmentTreeConsoleInfo +=
             'treeCapacity: ' + this.tree.length + ',treeSize: ' + treeSize;
          segmentTreePageInfo +=
             'treeCapacity: ' + this.tree.length + ',treeSize: ' + treeSize;
    
          // 返回输出的总信息
          document.body.innerHTML += segmentTreePageInfo;
          return segmentTreeConsoleInfo;
       }
    }
    复制代码
  3. NumArray2

    // 答题
    class Solution {
       // leetcode 307. 区域和检索 - 数组可修改
       NumArray2(nums) {
          /** * @param {number[]} nums * 方式一:对原数组进行预处理操做 */
          var NumArray = function(nums) {
             // 克隆一份原数组
             this.data = new Array(nums.length);
             for (var i = 0; i < nums.length; i++) {
                this.data[i] = nums[i];
             }
    
             if (nums.length > 0) {
                this.sum = new Array(nums.length + 1);
                this.sum[0] = 0;
                for (let i = 0; i < nums.length; i++)
                   this.sum[i + 1] = this.sum[i] + nums[i];
             }
          };
    
          /** * @param {number} i * @param {number} val * @return {void} */
          NumArray.prototype.update = function(i, val) {
             this.data[i] = val;
    
             for (let j = 0; j < this.data.length; j++)
                this.sum[j + 1] = this.sum[j] + this.data[j];
          };
    
          /** * @param {number} i * @param {number} j * @return {number} */
          NumArray.prototype.sumRange = function(i, j) {
             return this.sum[j + 1] - this.sum[i];
          };
    
          /** * Your NumArray object will be instantiated and called as such: * var obj = Object.create(NumArray).createNew(nums) * obj.update(i,val) * var param_2 = obj.sumRange(i,j) */
    
          /** * @param {number[]} nums * 方式二:对原数组进行预处理操做 */
          var NumArray = function(nums) {
             this.tree = new MySegmentTree(nums);
          };
    
          /** * @param {number} i * @param {number} val * @return {void} */
          NumArray.prototype.update = function(i, val) {
             this.tree.set(i, val);
          };
    
          /** * @param {number} i * @param {number} j * @return {number} */
          NumArray.prototype.sumRange = function(i, j) {
             return this.tree.query(i, j);
          };
    
          return new NumArray(nums);
       }
    }
    复制代码
  4. Main

    // main 函数
    class Main {
       constructor() {
          this.alterLine('leetcode 307. 区域和检索 - 数组可修改');
          let s = new Solution();
          let nums = [1, 3, 5];
          let numArray = s.NumArray2(nums);
    
          console.log(numArray.sumRange(0, 2));
          this.show(numArray.sumRange(0, 2));
          numArray.update(1, 2);
          console.log(numArray.sumRange(0, 2));
          this.show(numArray.sumRange(0, 2));
       }
    
       // 将内容显示在页面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展现分割线
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    
    // 页面加载完毕
    window.onload = function() {
       // 执行主函数
       new Main();
    };
    复制代码

更多线段树相关的话题

  1. 在 leetcode 上能够找到线段树相关的题目
    1. https://leetcode-cn.com/tag/segment-tree/
    2. 题目整体难度都是困难的,因此线段树是一种高级数据结构,
    3. 在通常的面试环节是不会看到线段树的影子的,
    4. 线段树的题目总体是有必定的难度的,
    5. 尤为是这些问题在具体使用线段树的时候,
    6. 不必定是直接的使用线段树,颇有可能须要绕几个弯子,
    7. 若是你不去参加算法竞赛的话,线段树不是一个重点。
  2. 线段树虽然不是一个彻底二叉树,可是能够把它看做是一棵满二叉树,
    1. 进而可使用数组的方式去存储这些结构,
    2. 这和以前实现的堆相应的存储方式是一致的,
    3. 对线段树的学习能够深刻理解树这种结构,
    4. 当节点中存储的内容不同的时候它所表示的意义也不同的时候,
    5. 相应的就能够来解决各类各样的问题,
    6. 它使用的范围是很是普遍的,对于线段树的构建,
    7. 对于线段树节点存储的是什么,它的左右子树表明的是什么意思,
    8. 其实和二分搜索树是彻底不一样的,
    9. 当你赋予这种结构合理的定义以后,就能够很是高效的处理一些特殊的问题,
    10. 好比说对于线段树来讲,就能够很是高效的处理了和线段(区间)有关的问题。
  3. 自定义线段树实现了三个方法
    1. 建立线段树、查询线段树、更新线段树中一个元素,
    2. 这三个方法都使用了递归的操做,
    3. 同时这个递归的写法在有一些层面和以前的二分搜索树是不一样的,
    4. 很大程度的不一样是表如今递归以后
    5. 最终仍是要对线段树中左右两个孩子的节点进行一个融合的操做,
    6. 这其实是一种后序遍历的思想。
  4. 递归的代码不管是在宏观的角度上仍是从微观的角度
    1. 都可以更深一步的对递归有进一步的认识。
  5. 对于线段树来讲其实还有不少能够深刻挖掘的东西
    1. 例如对线段树中一个区间进行更新,对应的时间复杂度是O(n)级别的,
    2. 由于这个区间里全部的元素都要访问到,这个操做相对来讲是比较慢的,
    3. 为了解决这个问题,在线段树中有一个专门的方式来解决它,
    4. 对应的方法一般称之为懒惰更新,也能够叫作懒惰的传播,
    5. 在本身实现的动态数组中有一个缩容的操做,
    6. 就有使用到懒惰的这个概念,在线段树中也可使用这样的思想,
    7. 在更新了中间节点的时候其实还要更新下面的叶子节点,
    8. 可是先不进行这个更新,这就是懒的地方,
    9. 先使用另一个叫作 lazy 的数组记录此次未更新的内容,
    10. 有了这个记录,就不须要实际的去更新这些节点,
    11. 当你再有一次更新或者查询操做的时候,
    12. 也就是当你再碰到这些节点的时候,
    13. 那么碰到这些节点以前都要先查一下已经记录的这个 lazy 数组中
    14. 是否有以前须要更新的内容,若是没有更新,那么在访问它们以前,
    15. 先将 lazy 数组中记录的未更新的内容进行一下更新,
    16. 更新之后再来进行应该进行的访问操做,这样作在更新一个区间的内容的时候,
    17. 就又变成了 logn 的复杂度了,只须要访问到中间节点就够了,
    18. 不须要对底层的全部节点都进行访问,
    19. 于此同时对于其余的查询或者新的更新操做,也依然是这样的一个复杂度,
    20. 只不过碰到相应的节点的时候看一下 lazy 数组中有没有记录相应的内容就行了,
    21. 这个思想在实现的时候,有相应的不少细节须要注意,
    22. 这一点也是一个比较高级的话题,有一个印象有一个概念就 ok。
  6. 本身实现的线段树本质上是一个一维的线段树
    1. 线段树还能够扩充到二维,
    2. 一维线段树就是指处理的空间是在一个一维空间中,是在一个坐标轴中,
    3. 若是根节点是一个线段的话,左边半段就是它的左节点,
    4. 右边半段就是它的右节点,可是能够把这个思想扩展成二维空间中,
    5. 对于根节点能够记录的是一个矩阵的内容,而后对这个矩阵进行分块儿,
    6. 把它分红四块儿,分别是左上、右上、左下、右下这四块儿,
    7. 这样一来就可让每个节点有四个孩子,
    8. 对于这四个孩子每一个孩子表示这个矩阵中相应的一起,
    9. 对于每个孩子它们依旧是一个更小的矩阵,
    10. 对于这个更小的矩阵又能够把它分红四块儿,
    11. 相应的每个节点有四个孩子,依此类推,
    12. 直到在叶子节点的时候每个节点只表示一个元素,
    13. 这样的一种线段树就叫作二维线段树,因此对于二维区间相应的查询问题,
    14. 也可使用线段树这样的思路来解决。
    15. 因此不只是二维线段树,其实也能够设计出三维线段树,
    16. 那么对于一个三维的矩阵,或者是对于一个立方体上的数据,
    17. 能够把这个立方体切成八块儿,那么每个节点能够分红八个节点,
    18. 对于每个小的节点,它是一个更小的立方体,而后能够这样继续细分下去,
  7. 线段树自己它就是一个思想,是在如何使用树这种数据结构,
    1. 将一个大的数据单元拆分红不一样的小的数据单元,递归的来表示这些数据,
    2. 同时利用这种递归的结构能够高效的进行访问,
    3. 从而进行诸如像更新查询这样的操做,这自己就是树这种结构的一个实质。
  8. 本身实现的线段树是一个数组的存储方式,
    1. 使用数组的存储方式,相应的就会出现若是你有 n 个元素,
    2. 那么就须要开辟 4n 个存储空间,在这个空间中其实有不少空间是被浪费的,
    3. 对于线段树其实可使用链式的方式进行存储,
    4. 能够设计一个单独的线段树所使用的节点类,
    5. 在这个节点类中就能够存储所表示的区间
    6. 它的左边界是谁、右边界是谁、相应的元素值是谁、以及它的左右孩子,
    7. 对于这样的一个节点也可使用链式的方式也能够建立出这个线段树,
    8. 在这种状况下,不须要浪费任何的空间,
    9. 若是你的线段树要处理的节点很是多的话,
    10. 有可能开 4n 的空间对你的电脑的存储资源负担比较大,
    11. 这时候就能够考虑使用链式这种所谓动态线段树。
  9. 实际上对于动态线段树来讲有一个更加剧要的应用
    1. 在本身所实现的线段树,
    2. 对于一个区间中相应的每个元素都要使用一个节点来表达,
    3. 这样的结果就是整个线段树所占的空间大小是 4n,
    4. 若是想要探讨的这个区间特别大的话,例若有一亿这么大的一个区间,
    5. 可是其实颇有可能你并不会对这么大的一个区间中每个长度为 1 的子区间都感兴趣,
    6. 在这种状况下颇有可能不须要一上来就创建一个巨大的线段树,
    7. 就从根节点开始,初始的时候就这一个节点,它表示从 0 到一亿这样的一个区间,
    8. 若是你关注[5,16]这样的一个区间,在这种状况下再开始动态的建立这个线段树,
    9. 那么这个动态建立的方法,多是首先将这个线段树根节点分红两部分,
    10. 左孩子表示[0,4]这样的一个区间,右孩子表示 5 到一亿这样的一个区间,
    11. 进而对 5 到一亿这样的区间再给分红两部分,左半部分表示[5,16]
    12. 右半部分表示 17 到一亿这个区间。至此对于这棵线段树来讲,
    13. 只有 5 个节点,也能够很是快速的关注到[5,16]这个区间相应的内容,
    14. 那么使用这样的方式,若是你的区间很是大,
    15. 可是你关注的区间其实并不会分布到这个大区间中每个小部分的时候,
    16. 能够实现这样的一个动态线段树,由于更加的有利。

区间操做相关-另一个重要的数据结构

  1. 树状数组(Binary Index Tree)
    1. 对区间这种数据进行操做时,就可能会使用到这种数据结构了,
    2. 也就是树状数组,也被简称为 BIT,也叫二叉索引树,
    3. 树状数组也是一个很是经典的树状结构,
    4. 也是算法竞赛中的常客,在某一些问题上树状数组解决的问题和线段树是重叠的,
    5. 不过在另一些问题上树状数组也有它独特的优点。

区间相关的问题

  1. 对于区间相关的问题不必定使用线段树或者树状数组这样的专门的数据结构来解决
    1. 和区间相关的有一类很是经典的问题,叫作 RMQ(Range Minimum Query),
    2. 也就是在一个区间中去相应的查询最小值,
    3. 其实使用本身实现的线段树彻底能够解决这个问题,
    4. 不过对于 RMQ 问题因为它太过经典,
    5. 有很是多个研究相应的也产生了很是多的其它办法来解决这个问题,
    6. 而不只仅是使用线段树或者是使用树状数组。
相关文章
相关标签/搜索