第9章 线段树/区间树(Leetcode原题)

第九章 线段树(区间树)

9-1 什么是线段树
9-2 线段树基础表示
9-3 创建线段树
9-4 线段树中的区间查询
9-5 Leetcode上线段树相关的问题
9-6 线段树中的更新操作
9-7 更多线段树相关的话题


9-1 什么是线段树
为什么要使用线段树Segment Tree: 对于有一类问题,我们关心的是线段(或者区间)

  • 最经典的线段树问题:区间染色
    有一面墙,长度为n,每次选择一段墙进行染色,那么经过m次操作后,我们可以看见多少种颜色?m次操作后,我们可以在[i, j]区间内看见多少种颜色?
    使用数组实现:染色操作(更新区间)O(n);查询操作(查询区间)O(n)

在这里插入图片描述

  • 另一类经典问题:区间查询
    查询一个区间[i, j]的最大值,最小值,或者区间数字和。实质:基于区间的统计查询
    2017年注册用户中消费最高的用户?消费最少的用户?学习时间最长的用户?
    某个太空区间中天体总量?
    使用数组实现:更新O(n),查询O(n)
    使用线段树:更新O(logn),查询O(logn)
    在这里插入图片描述
    对于给定区间:
    更新:更新区间中一个元素或者一个区间的值(区间内数据是动态的)
    查询一个区间[i, j]的最大值,最小值,或者区间数字和

  • 什么是线段树?

    线段树:将区间数据不断二分,分成若干层,直至最后一层仅存一个元素
    一般不考虑在线段树中添加或者删除元素的,在大多数情况下,对于线段树解决的问题,区间本身是固定的。

9-2 线段树基础表示

1.数组区间中数据量为奇数或偶数时,线段树的形态不同
2.线段树不一定满足二叉树和完全二叉树,但满足平衡二叉树(最大深度与最小深度的差最多为1,不会像二分搜索树可能会退化成链表),依然可以用数组表示
3.堆也是平衡二叉树,二分搜索树不一定是平衡二叉树

  • 如果区间有n个元素,数组表示需要有多少节点?:需要4n的空间
    0层:1, 1层:2,2层:4,3层:8…h-1层:2^(h-1)
    对于满二叉树:h层,一共有2h - 1个节点(大约是2h);最后一层(h-1层),有2(h-1)个节点;最后一层的节点树大致等于前面所有层节点之和。

    我们线段树不考虑添加元素,即区间固定,使用4n的静态空间即可。

线段树的非空叶子节点肯定等于区间中元素的个数n,并且最后一层与倒数第二层的高度不能超过1,也就是最好的情况是最后一层刚刚铺满,则2(h-1) = n,整个树节点总数为2h-1 = 2 * n-1,如果最后一层未铺满,倒数第二层的满节点个数2(h-2) 小于n(倒数第二层叶子节点树 + 最后一层叶子节点数 = n,最后一层叶子节点数>= 倒数第二层非叶子节点),则总节点树: 2h-1 小于4 * n-1.

public class SegmentTree<E> {
    private E[] tree;
    private E[] data;
    public SegmentTree(E[] arr){
        data = (E[])new Object[arr.length];
        for(int i = 0 ; i < arr.length ; i ++)
            data[i] = arr[i];
        tree = (E[])new Object[4 * arr.length];
    }
    public int getSize(){
        return data.length;
    }
    public E get(int index){
        if(index < 0 || index >= data.length)
            throw new IllegalArgumentException("Index is illegal.");
        return data[index];
    }
    // 返回完全二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引
    private int leftChild(int index){
        return 2*index + 1;
    }
    // 返回完全二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引
    private int rightChild(int index){
        return 2*index + 2;
    }
}

9-3 创建线段树

public SegmentTree(E[] arr, Merger<E> merger){
        this.merger = merger;
        data = (E[])new Object[arr.length];
        for(int i = 0 ; i < arr.length ; i ++)
            data[i] = arr[i];
        tree = (E[])new Object[4 * arr.length];
        buildSegmentTree(0, 0, arr.length - 1);
    }
    // 在treeIndex的位置创建表示区间[l...r]的线段树
    private void buildSegmentTree(int treeIndex, int l, int r){
        if(l == r){
            tree[treeIndex] = data[l];
            return;
        }
        int leftTreeIndex = leftChild(treeIndex);
        int rightTreeIndex = rightChild(treeIndex);
        // int mid = (l + r) / 2;
        int mid = l + (r - l) / 2;
        buildSegmentTree(leftTreeIndex, l, mid);
        buildSegmentTree(rightTreeIndex, mid + 1, r);
        tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);
    }

merge接口定义的是:将两个参数a,b通过merge转化成一个参数

public interface Merger<E> {
    E merge(E a, E b);
}

9-4 线段树中的区间查询

简单说明:从根节点开始出发,那么在根节点它包含了从0到7这个区间相应的信息,显然[2,5]是[0,7]区间的一个子集,所以我们要向下对它的子节点进行查找。注意,对于我们线段树来说,每一个节点它的左右子树都是对于当前节点的区间从中间给分隔开,所以我们是知道这个分隔的位置的,左子树是[0,3],右子树是[4,7],而我们想要的[2,5]有一部分在左子树有一部分在右子树中,然后逐次类推,知道需要查询的子节点。

  • 将区间包装成一个节点类,它所表示的区间范围[1, r] 直接通过索引treeIndex的形式进行访问,进而访问到区间所表示的区间范围;
  • 以treeIndex所表示的区间范围以参数的形式进行传递
// 返回区间[queryL, queryR]的值
    public E query(int queryL, int queryR){
        if(queryL < 0 || queryL >= data.length ||
                queryR < 0 || queryR >= data.length || queryL > queryR)
            throw new IllegalArgumentException("Index is illegal.");
        return query(0, 0, data.length - 1, queryL, queryR);
    }
    // 在以treeIndex为根的线段树中[l...r]的范围里,搜索区间[queryL...queryR]的值
    private E query(int treeIndex, int l, int r, int queryL, int queryR){
        if(l == queryL && r == queryR)
            return tree[treeIndex];
        int mid = l + (r - l) / 2;
        // treeIndex的节点分为[l...mid]和[mid+1...r]两部分
        int leftTreeIndex = leftChild(treeIndex);
        int rightTreeIndex = rightChild(treeIndex);
        if(queryL >= mid + 1)
            return query(rightTreeIndex, mid + 1, r, queryL, queryR);
        else if(queryR <= mid)
            return query(leftTreeIndex, l, mid, queryL, queryR);
        E leftResult = query(leftTreeIndex, l, mid, queryL, mid);
        E rightResult = query(rightTreeIndex, mid + 1, r, mid + 1, queryR);
        return merger.merge(leftResult, rightResult);
    }

9-5 Leetcode上线段树相关的问题
Leetcode-303 区域和检索-数组不可变
在这里插入图片描述
实现代码如下:
(1)使用线段树

class NumArray {
    private SegmentTree<Integer> segmentTree;
    public NumArray(int[] nums) {
        if(nums.length > 0){
            Integer[] data = new Integer[nums.length];
            for (int i = 0; i < nums.length; i++)
                data[i] = nums[i];
            segmentTree = new SegmentTree<>(data, (a, b) -> a + b);
        }
    }
    public int sumRange(int i, int j) {
        if(segmentTree == null)
            throw new IllegalArgumentException("Segment Tree is null");
        return segmentTree.query(i, j);
    }
}

(2)不使用线段树(使用sum进行预处理)

public class NumArray2 {
    private int[] sum; // sum[i]存储前i个元素和, sum[0] = 0
                       // 即sum[i]存储nums[0...i-1]的和
                       // sum(i, j) = sum[j + 1] - sum[i]
    public NumArray2(int[] nums) {
        sum = new int[nums.length + 1];
        sum[0] = 0;
        for(int i = 1 ; i < sum.length ; i ++)
            sum[i] = sum[i - 1] + nums[i - 1];
    }
    public int sumRange(int i, int j) {
        return sum[j + 1] - sum[i];
    }
}

接下来和以上303号问题进行一个比较,将Leetcode上第307号问题也解决一下:
Leetcode-307 区域和检索-数组可修改

实现代码如下:
(1)以下代码会出现“超出时间限制”的问题

// 使用sum数组的思路:TLE
class NumArray3 {
    private int[] data;
    private int[] sum;
    public NumArray3(int[] nums) {
        data = new int[nums.length];
        for(int i = 0 ; i < nums.length ; i ++)
            data[i] = nums[i];
        sum = new int[nums.length + 1];
        sum[0] = 0;
        for(int i = 1 ; i <= nums.length ; i ++)
            sum[i] = sum[i - 1] + nums[i - 1];
    }
    public int sumRange(int i, int j) {
        return sum[j + 1] - sum[i];
    }
    public void update(int index, int val) {
        data[index] = val;
        for(int i = index + 1 ; i < sum.length ; i ++)
            sum[i] = sum[i - 1] + data[i - 1];
    }
}

(2)解决以上问题的解法,在9-6中:

9-6 线段树中的更新操作
首先在segmentTree文件中(1)更新index位置为e;(2)在以treeIndex为根的线段树中更新index的值为e

   // 将index位置的值,更新为e
    public void set(int index, E e){
        if(index < 0 || index >= data.length)
            throw new IllegalArgumentException("Index is illegal");
        data[index] = e;
        set(0, 0, data.length - 1, index, e);
    }
    
    // 在以treeIndex为根的线段树中更新index的值为e
    private void set(int treeIndex, int l, int r, int index, E e){
        if(l == r){
            tree[treeIndex] = e;
            return;
        }
        int mid = l + (r - l) / 2;
        // treeIndex的节点分为[l...mid]和[mid+1...r]两部分
        int leftTreeIndex = leftChild(treeIndex);
        int rightTreeIndex = rightChild(treeIndex);
        if(index >= mid + 1)
            set(rightTreeIndex, mid + 1, r, index, e);
        else // index <= mid
            set(leftTreeIndex, l, mid, index, e);
        tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);
    }

加上上面这个更新的函数,就能得到正确的307解决方案:
(注意提交代码的时候,需要带上merge和segmentTree俩文件)

private SegmentTree<Integer> segTree;
    public NumArrayComplete(int[] nums) {
        if(nums.length != 0){
            Integer[] data = new Integer[nums.length];
            for(int i = 0 ; i < nums.length ; i ++)
                data[i] = nums[i];
            segTree = new SegmentTree<>(data, (a, b) -> a + b);
        }
    }
    public void update(int i, int val) {
        if(segTree == null)
            throw new IllegalArgumentException("Error");
        segTree.set(i, val);
    }
    public int sumRange(int i, int j) {
        if(segTree == null)
            throw new IllegalArgumentException("Error");
        return segTree.query(i, j);
    }
}

9-7 更多线段树相关的话题
这里要注意一点:如果不去参加算法竞赛的话,线段树相对而言不是一个重点,合理分配时间

  • 线段树是一种思想:利用树结构将大的数据单元递归分成更小的数据单元。线段树与二分搜索树不同之处在于,线段树递归之后还需要将左右孩子进行融合(实际上是后序遍历的思想)
  • 对区间进行更新操作,需要更新所有叶子节点和所有parent节点。可以在中间节点进行具体操作,然后对父类进行融合,除此之外,还需要对每个节点进行更新,此时复杂度为O(n) - - - - - - >解决方案是:引入lazy数组记录未更新的内容,在下一次查询或更新的时候,对所需要使用的内容进行更新。
  • 线段树包括:一维线段树,二维线段树和三维线段树(比如二维线段树:一个节点记录一个矩阵的内容,每个节点记录四个内容,有四个child nodes;三维线段树就是一个立方体,三维矩阵,8个数据单元)
  • 动态线段树:如果元素过多,我们没必要一上来就做一个含有所有元素的线段树,更好的办法是针对所需要进行操作的元素进行划分。
  • 树状数组(Binary Index Tree):在某些方面功能与线段树重叠,但其他方面要优于线段树。
  • 另外还有与区间相关的RMQ(Range Minimum Query)问题。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值