• 线段树


    Segment tree

    ruby实现的代码: git 

    Wiki(翻译)

    一种静态树的数据结构。用于储存intervals, segments。它可以查询储存的segments。

    在原理上,这个结构建立后就不能修改。

    翻译称为“线段树”。

    线段树储存 a set I of n intervals, 创建的时间复杂度O(n log n).

    它搜索k个intervals的时间复杂度是O(log n + k)

    它的使用领域:

    在计算几何学和地理数据系统 computational geometry, and geographic information systems.

    线段树能够被概括到高纬领域“The segment tree can be generalized to higher dimension spaces.”


    英文文章的理解

    Segment tree用在数组的范围查询和这个数组的元素修改。例如,查询一个数组的一段(从索引L到R)的所有元素的和, 或者它的查询最小值查询。

    个人认为,线段树就是对一个超长数组的预先计算,比如范围求和,求最/小大值。把这些结果存储在线段树节点内,当需要查询的时候,无需遍历数组,而是以更快的速度O(lgN),从线段树内得到查询结果。

    什么是线段树?

    一个二叉树。每个节点是一个线段interval/或者叫区间segment。

    假如一个数组A, 长度为N, 使用线段树T:

    1. T的root是整个数组A[0: N-1],即整个数组的sum。
    2. 每个叶节点代表单个元素A[i],  0=< i < N
    3. 每个内部节点代表一段区间A[i: j], 就是从A[i]到A[j]的和sum。

    根节点代表整个数组。然后被分开成2部分区间,分别代表左儿子和右儿子。

    每个父节点和子节点的关系,都是这样的。所以线段树的高度(时间复杂度)是 O(lg N)。

    N个叶节点代表N个数组的元素。非叶节点的数量为N-1。因此全部节点的数量为2*N - 1。

    线段树被建立后,结构就不能修改了。但可以修改节点的值,因此它可以有2类操作:

    1. Update: 更新数组元素A, 并在线段树上做出相应的变化。
    2. Query:可以查询一个区间并返回结果(指定区间,求最大/小值,求和)

     

    操作

    既然线段树是一个二叉树,那么一个一纬数组就可以代表它。tree = [], 然后构建线段树后,tree的元素存储root/内节点/叶子节点的值。

    其中:

    tree[1] = array[0..6] 

    tree[2] = array[0..3]

    tree[3] = array[4..6]

    ...略

    建立Segment-tree

    首先,确认“节点内储存什么数据?”的问题。例如:如果问题是查找一段区间的元素的和sum,那么每个非叶子节点储存它的儿子节点的数据的和sum。

    然后,使用递归方法建立树结构。因为叶节点储存数组元素,所以在递归回归的过程中,通过儿子节点的值相加得到父亲节点的值。

    代码见下:

    tree[1] == A[0..6] 

    void build(int node, int start, int end)   #node代表节点编号。
    {
        if(start == end)
        {
            // Leaf node will have a single element, A代表数组。
            tree[node] = A[start];
        }
        else
        {
            int mid = (start + end) / 2;
            // Recurse on the left child,建立左儿子节点,并在回归时,返回它自身。
            build(2*node, start, mid);
            // Recurse on the right child
            build(2*node+1, mid+1, end);
            // Internal node will have the sum of both of its children。父节点的值:sum。
            tree[node] = tree[2*node] + tree[2*node+1];
        }
    }

    ruby代码的实现:

    array = [1,3,5,7,9,11]
    # 用tree来存线段树。
    tree = []
    def build(node, start, tail, array, tree)
      # 判断是否是叶节点,然后把数组array的元素存入线段树的叶节点。
      if start == tail
        tree[node] = array[start]
      else
        mid = (start + tail)/2.to_i
        # 建立左儿子和右儿子
        build(2*node, start, mid, array, tree)
        build(2*node + 1, mid + 1, tail, array, tree)
        # 递归的回归过程中(bottom_up),计算内节点的值
        tree[node] = tree[2*node] + tree[2*node +1]
      end
    end
    
    build(1, 0, array.size - 1 , array, tree)

    p tree
    #结果是:
    # [nil, 36, 9, 27, 4, 5, 16, 11, 1, 3, nil, nil, 7, 9]

    小结:

    线段树可以使用递归方法建立Recursion(bottom-up 方法)。从root向下形成树结构,再从叶节点一路向上到root,更新从叶节点到root的过程中的节点的值。

    每一步,2个儿子节点形成它们的父节点。每个内部节点都代表它们儿子区间的合成。最后递归在root完成,root代表了了整个array的sum。

    Update

    代码和构建代码的过程类似,都是从root开始,根据元素的index,从root开始判断,找到它所在的叶节点。

    然后,更新叶节点的值,并返回叶节点的值给父节点,然后一路向上,回归的过程中更新内部节点。

    时间

    def update(node, start, tail, idx, val, tree)
      if start == tail
        tree[node] = val
        return
      end
      # 判断idx在左子树还算右子树
      mid = (start + tail)/2.to_i
      if start <= idx && idx <= mid
        update(2*node, start, mid, idx, val, tree)
      else
        update(2*node + 1, mid+1, tail, idx, val, tree)
      end
      tree[node] = tree[2*node] + tree[2*node +1]
    end
    
    build(1, 0, array.size - 1 , array, tree)
    p tree
    #结果是:
    # [nil, 36, 9, 27, 4, 5, 16, 11, 1, 3, nil, nil, 7, 9]
    # 更新array的元素 array[1] == 2
    array[1] = 2
    # 更新线段树
    update(1, 0, array.size - 1, 1, 2, tree)
    p tree
    #结果: [nil, 35, 8, 27, 3, 5, 16, 11, 1, 2, nil, nil, 7, 9]
    # tree[9] 即array[1]

     

    Query

    范围查询是线段树结构的优势。时间复杂度lgN。

    方法:

    假如想要得到从L到R的范围的array的元素的sum。首先,从root开始递归,并检查内部节点是否在范围(L to R)内。然后,判断有几种情况:

    • 要查询范围正好和节点的范围完全重合(start == L && R == tail),直接返回节点的值。
    • 要查询范围完全不在节点的范围内(不相交) ( R <start || L > tail),返回值0即可。
    • 要查询范围在节点的范围内部(被包含)(start < L && R < tail),继续向下递归查询。
    • 要查询范围包含了节点的范围 (包含),(L < start && tail < R), 直接返回节点的值。
    • 要查询范围部分在节点的范围内 (部分相交),继续向下递归查询。

    经过整理其实就三种情况:

    • L <= start && tail <= R, 返回node的值
    • R <start || L > tail, 返回0
    • 其他2种:部分相交和要查询范围在节点范围内(但不重合)的情况。继续向下递归

    还使用上面的例子:数组array, 用户想要查询array[2]到array[4]的sum。怎么通过线段树结构计算sum?

    代码:

    # node为当前查找的节点的在线段树的索引
    # queryL, 用户查找的左边界
    # queryR, 用户查找的右边界
    def query(node, start, tail, queryL, queryR)
      # 不相交,返回0
      if queryR < start || tail < queryL
        return 0
      # 查询范围包含(或完全重合)了当前节点的范围。
      elsif queryL <= start && tail <= queryR
        return tree[node]
      else
      #其他情况:部分相交和要查询范围在节点范围内(但不重合)的情况,递归:  
        mid = (start + tail)/2.to_i
        p1 = query(2*node, start, mid, queryL, queryR)
        p2 = query(2*node + 1, mid + 1, tail, queryL, queryR)
        return p1 + p2
      end
    end

    对一段数组来说,按它的基本结构:求和操作的话花费O(N)时间;更新元素的值花费O(1)

    换成线段树结构后:

    根据需求构建它使用O(N),  完全更新一个元素是O(logN), query是O(logN)。

    参考:

    https://www.hackerearth.com/zh/practice/data-structures/advanced-data-structures/segment-trees/tutorial/

    https://en.wikipedia.org/wiki/Segment_tree

  • 相关阅读:
    1.33 (累积互素数)
    1.33 (过滤累积和 求区间内所有素数之和)
    1.32 (更高层次的抽象! 乘法与加法本来就是一回事)
    1.31 (另一种求圆周率的算法)
    1.30 (递归的sum变迭代)
    习题1.29 (积分方法的优化---simpson规则)
    1.3.1 (对过程的抽象)
    SICP习题 1.23(素数查找的去偶数优化)
    SICP习题 1.22(素数)
    pom.xml
  • 原文地址:https://www.cnblogs.com/chentianwei/p/11611626.html
Copyright © 2020-2023  润新知