玩轉數據結構(九)線段樹

線段樹是一種特殊的樹結構。這種數據結構主要用於解決“線段”或者是“區間”這種特殊的數據,是算法競賽中的常客。在這一章,我們將從底層實現屬於我們自己的線段樹,完成線段樹的創建,查詢,更新三個操作,並且通過實際比較,看到線段樹解決“線段”相關問題的巨大優勢。

9-1 什麼是線段樹(區間樹)


  • 對於有一類問題,我們關心的是線段(或者區間)
  • 最經典的線段樹問題:區間染色
    在這裏插入圖片描述

在這裏插入圖片描述

  • 另一類經典問題:區間查詢 在這裏插入圖片描述
    在這裏插入圖片描述
    在這裏插入圖片描述
    在這裏插入圖片描述
    在這裏插入圖片描述

9-2 線段樹基礎表示


在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

在這裏插入圖片描述

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 class SegmentTree<E> {

    private E[] tree;
    private E[] data;
    private Merger<E> merger;

    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,data.length-1);
    }

    //在treeIndex的位置創建表示區間[1....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 爲避免l和r都特別大的時候使得l+r產生整形溢出的情況
        int mid = l + (r-l)/2;
        buildSegmentTree(leftTreeIndex,l,mid);
        buildSegmentTree(rightTreeIndex,mid+1,r);

        tree[treeIndex] = merger.merge(tree[leftTreeIndex],tree[rightTreeIndex]);
    }

    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;
    }

    @Override
    public String toString(){
        StringBuilder res = new StringBuilder();
        res.append('[');
        for (int i=0;i<tree.length;i++){
            if (tree[i]!=null)
                res.append(tree[i]);
            else
                res.append("null");

            if (i!=tree.length-1)
                res.append(", ");
        }
        res.append(']');
        return res.toString();
    }
}

public interface Merger<E> {

    //這裏用戶定義兩個區間如何融合的規則
    E merge(E a,E b);
}

現在測試一下:

public class Main {

    public static void main(String[] args) {

        Integer[] nums = {-2,0,3,-5,2,-1};
//        SegmentTree<Integer> segTree = new SegmentTree<>(nums, new Merger<Integer>() {
//            @Override
//            public Integer merge(Integer a, Integer b) {
//                return a+b;
//            }
//        });
        SegmentTree<Integer> segTree = new SegmentTree<>(nums,(a,b)->a+b);
        System.out.println(segTree);
    }
}

在這裏插入圖片描述

9-4 線段樹中的區間查詢


在這裏插入圖片描述

//返回區間[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....qeuryR]的值
    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;
        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);

    }

我們測試一下:

public class Main {

    public static void main(String[] args) {

        Integer[] nums = {-2,0,3,-5,2,-1};;
        SegmentTree<Integer> segTree = new SegmentTree<>(nums,(a,b)->a+b);
        System.out.println(segTree.query(0,2));
        System.out.println(segTree.query(2,5));
        System.out.println(segTree.query(0,5));
    }
}

在這裏插入圖片描述

9-5 Leetcode上線段樹相關的問題


在這裏插入圖片描述

public 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 Tress is null");
        
        return segmentTree.query(i,j);
    }

}

想快速查詢某個區間的元素和,而且這個區間中的元素不會改變,對於這樣的需求可以進行預處理

public class NumArray {

    private int[] sum; // sum[i]存儲前i個元素和, sum[0] = 0
                       // 即sum[i]存儲nums[0...i-1]的和
                       // sum(i, j) = sum[j + 1] - sum[i]
    public NumArray(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];
    }
}

在這裏插入圖片描述

/// Leetcode 307. Range Sum Query - Mutable
/// https://leetcode.com/problems/range-sum-query-mutable/description/
///
/// 使用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];
    }
}

使用數組雖然可以實現,但是耗時非常多,會超出時間限制。
這個時候就要用到我們的線段樹。

9-6 線段樹中的更新操作


//將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);
    }

307號問題的解答

public class NumArray4 {

    private SegmentTree<Integer> segTree;

    public NumArray4(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 更多線段樹相關的話題


線段樹的相關問題都比較困難,是高級數據結構。一般面試不會考,更多應用於算法競賽。

更多延伸

  • 對於一個區間進行更新,例如將[2,5]去加中的所有元素+3
  • 懶惰更新,使用lazy數組記錄未更新的內容。
  • 二維線段樹
  • 動態線段樹
  • 區間操作相關另外一個重要數據結構:樹狀數組
  • 區間相關問題:RMQ Range Minimum Query

最後

附上代碼完整定義

public interface Merger<E> {

    //這裏用戶定義兩個區間如何融合的規則
    E merge(E a,E b);
}

public class SegmentTree<E> {

    private E[] tree;
    private E[] data;
    private Merger<E> merger;

    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,data.length-1);
    }

    //在treeIndex的位置創建表示區間[1....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 爲避免l和r都特別大的時候使得l+r產生整形溢出的情況
        int mid = l + (r-l)/2;
        buildSegmentTree(leftTreeIndex,l,mid);
        buildSegmentTree(rightTreeIndex,mid+1,r);

        tree[treeIndex] = merger.merge(tree[leftTreeIndex],tree[rightTreeIndex]);
    }

    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;
    }

    //返回區間[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....qeuryR]的值
    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;
        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);
    }

    //將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]);
    }


    @Override
    public String toString(){
        StringBuilder res = new StringBuilder();
        res.append('[');
        for (int i=0;i<tree.length;i++){
            if (tree[i]!=null)
                res.append(tree[i]);
            else
                res.append("null");

            if (i!=tree.length-1)
                res.append(", ");
        }
        res.append(']');
        return res.toString();
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章