• 线段树(区间树)


    线段树(Interval Tree),也叫区间树。它在各个节点保存一个区间(即“子数组”),适用于和区间统计有关的问题。比如某些数据可以按区间进行划分,按区间动态进行修改,而且还需要按区间多次进行查询,那么使用线段树可以达到较快查询速度。实际可应用于例如RMQ,线段求长,矩形交,矩形并等,它基本能保证每个操作的复杂度为O(log n)。

    一、基本结构

    对于一个[0 , N-1]的序列,它对应的线段树的根节点表示区间[0 , N-1],即所有N个数所组成的一个区间,然后,把区间分成两半,分别由左右子树表示。它的左右子树可以有多种表示方法:

    方案一:左结点代表的区间为[a , (a + b) / 2],右结点代表的区间为[ (a + b) / 2 + 1 , b ]。叶子节点为区间[a , a]。

    其中(a + b) / 2将向下取整,这是最常用的表示方法,当只考虑区间内的整数点时,在这种表示方案情况下的问题求解比较清晰。


    叶子节点数目为N,即整个线段区间的长度,而线段树的总结点数可用数学归纳法证明为2N-1。

    方案二:  对于根节点为[0 , N)的区间。

    左结点代表的区间为[a , (a + b) / 2),右结点代表的区间为[ (a + b) / 2 , b ]。叶子节点为区间[a , a+1)。

    这种方案能覆盖区间内的所有实数点。

    不管采用哪种方案,线段树都是平衡二叉树。线段树的总结点数为O(2N),高度为O(log n)。

    二、基本操作

    建树,插入,删除,查询,更新。
    因为它是一棵二叉树,所以它的操作一般除了建树是O(N),其余的都是O(log n)的。

    线段树的一般解题过程是先建树,然后插入数据,然后更新,查询。建树和插入删除操作都可由定义直接递归得到。我们来看看线段树的查询和更新操作,特别是在某个区间内进行时的实现方法。

    2.1 查询

    2.1.1 单点查询

    单点查询指查询一个叶节点的信息,只要按树的结构搜索,每次搜索只需要考虑一个分支。

    2.1.2 区间查询

    区间查询指用户输入一个区间,获取该区间的有关信息,如区间中最大值,最小值,第N大的值等。

    比如在前面一个图中所示的树中查询最小值,如果询问区间是线段树的一个完整节点,比如是[0,2]或者是[3,3],则可以直接找到对应的节点。但比如[0,3],是由两个区间组合而成。需要把[0,2]和[3,3]两个区间(它们在整数意义上是相连的两个区间)的最小值“合并”起来,也就是求这两个最小值的最小值,才能求出[0,3]范围的最小值。同理,对于其他询问的区间,也都可以找到若干个相连的区间,合并后可以得到询问的区间。

    // node 为线段树的结点类型,其中Left 和Right 分别表示区间左右端点
    // Lch 和Rch 分别表示指向左右孩子的指针
    void Query(node *p, int a, int b) // 当前考察结点为p,查询区间为(a,b]
    {
      if (a <= p->Left && p->Right <= b)
      // 如果当前结点的区间包含在查询区间内
      {
         ...... // 更新结果
         return;
      }
      Push_Down(p); // 将A的标记p移到子结点中,需要更新子结点的值和标记
      int mid = (p->Left + p->Right) / 2; // 计算左右子结点的分隔点
      if (a < mid) Query(p->Lch, a, b); // 和左孩子有交集,考察左子结点
      if (b > mid) Query(p->Rch, a, b); // 和右孩子有交集,考察右子结点
    }

    这样的过程一定选出了尽量少的区间,它们相连后正好涵盖了整个[l,r],没有重复也没有遗漏。同时,这样的区间集合在每层的节点最多会被选取2个,一共选取的节点数也是O(log n)的,因此查询的时间复杂度也是O(log n)。

    线段树并不适合所有区间查询情况,它的使用条件是“相邻的区间的信息可以被合并成两个区间的并区间的信息”。即问题是可以被分解解决的。

    2.2 更新

    2.2.1 单个点的更新

    对于单个节点,从根开始按子树的划分确定是访问哪个子树,递归下去,修改叶节点信息,然后回溯修改父节点的信息。

    2.2.2 区间更新

    当用户更新一个区间的值时,如果连同其子孙全部更新,则改动的节点数为O(n)个。因而,如果要想把区间更新操作也控制在O(log n)的时间内,只更新O(log n)个节点的信息就成为必要。
    借鉴前一节区间查询用到的思路:区间更新时如果区间,
    完全包含了区间A,则只更新A并回溯更新A的所有祖先节点,但不去更新它的儿子节点。为了记录A的儿子节点的信息事实上已经被改变了这就需要我们在A节点里增设一个域:标记。

    标记记录这个结点是否已被进行了某种更新操作(这种更新操作会影响其所有子结点,但不会影响A本身)。还是像上面的一样,对于任意区间A,如果A完全包含于需要更新的区间,则A结点标上标记p并更新A的值,然后回溯到祖先节点,但并不给A的祖先节点标标记p,也不给子节点标标记p。在更新和查询的时候,如果我们到了一个结点B,并且当我们决定考虑其子结点,那么就要看看结点B有没有标记p,如果有,就要按照标记p更新其子结点的值,并且给子结点都标上相同的标记p,同时消掉B的标记p

    // node 为线段树的结点类型,其中Left 和Right 分别表示区间左右端点
     // Lch 和Rch 分别表示指向左右孩子的指针
     
    void Update(node *A, int a, int b) // 当前考察结点为A,更新区间为[a,b]
    {
      if (a <= A->Left && A->Right <= b)
      // 如果当前结点的区间包含在更新区间内
      {
         ...... // 修改当前结点的信息
    	 ...//标上标记p
    	 return;//从此处开始回溯到祖先节点
      }
      Push_Down(p); // 将A的标记p移到子结点中,需要更新子结点的值和标记
    
      int mid = (A->Left + A->Right) / 2; // 计算左右子结点的分隔点
      if (a < mid) Update(A->Lch, a, b); // 和左孩子有交集,考察左子结点
      if (b > mid) Update(A->Rch, a, b); // 和右孩子有交集,考察右子结点
      Update(A); // 递归后回溯修改A的信息(因为其子结点的信息可能有更改)
    }


  • 相关阅读:
    Android使用SQLite数据库(2)
    Android使用SQLite数据库(1)
    使用Eclipse为Android定义style
    SharedPreferences写入和读出数据
    AlertDialog.Builder弹出对话框
    Android退出时关闭所有Activity的方法
    获取PC或移动设备的所有IP地址
    Android文件的分割和组装
    到底什么是跨域?附解决方案!
    超详细 Nginx 极简教程
  • 原文地址:https://www.cnblogs.com/zhuyuanhao/p/3262866.html
Copyright © 2020-2023  润新知