線段樹和樹狀數組學習日志

oneDay

  1. 為什么要使用線段樹
    因為對于某一類問題,我們關心的就只是線段(區間)
  2. 線段樹的一些經典問題
    區間染色問題:對于給定的一段區間,經過m次染色后,問區間[i,j]的顏色的種類(0=<i,j<=n-1)
    區間查詢問題:查詢在一段時間[i,j]區間內的要查詢數據的動態情況
    對于這兩個問題,使用數組也是可以解決的,但是時間復雜度為O(n),而使用線段樹的時間復雜度為O(logn)
  3. 線段樹的基本操作
  • 更新:更新區間中的一個元素或者一個區間的值
  • 查詢:查詢一個區間[i,j]內元素的最大值,最小值,或者區間數字的和
  1. 線段樹的結構


    捕獲.PNG

    可以看到這個線段樹是一個滿二叉樹,但是線段樹不一定是滿二叉樹或者是完全二叉樹,但它是一個平衡二叉樹,也就是數中最大的高度和最小的高度的差小于等于1

  2. 線段樹的底層實現
  • 這里使用數組來實現二叉樹,首先先來分析一下對于給定的區間,數組的大小應該是多少(注意:這里將線段樹統一視為滿二叉樹,也就是對于空位置的節點設為null就好了)
    對于一棵滿二叉樹,每一層節點的個數與層數之間的關系為2^(n-1),其中最后一層的節點個數為2^(h-1),由于滿二叉樹的所有的節點個數為2^(h)-1,也就近似等于2^h,所以可以發現最后一層的節點的個數近似等于總結點數的一半,而最后一層的節點個數在當給定的區間的個數為n=2^k時,此時的n就是最后一層的節點的個數,所以總結點個數為2n,而如果n=2^k+1,這個時候n比最后一層的節點個數要多,也就是這個滿二叉樹要增加一層,而這一層上面的所有層的節點個數為2n,這一層也為2n,所以總共需要4n大小的數組來存儲線段樹,n代表的就是區間內元素的個數
    綜上,使用數組來實現線段樹需要4*n大小的空間(n為區間內元素的個數)
  • 線段樹沒有添加元素的操作,區間是固定的,使用4*n的靜態空間就好了

twoDay

  1. 線段樹的創建
    線段樹的創建使用了遞歸的方法,因為由線段樹的結構可以知道,根節點的值就是兩個子節點值的合并(這里合并可以是加,減等一系列的操作),在創建這棵線段樹的時候,就先創建它的左右的孩子,然后合并它們的值就是根節點的值(這里在創建的時候,為了使得用戶可以更改對某個區間的操作,創建了一個類似于Comparator的接口merge,使得在初始化線段樹的時候,可以向其中放入對某個區間的操作)
  2. 線段樹的查詢
    同樣的,實現線段樹的查詢操作也是使用遞歸來完成的, 對于給定的區間,如果只出現在右邊,那就只需要在右邊查詢,如果只出現在左邊,那就只需要在左邊查詢,但是如果即出現在左邊,又出現在右邊,那就要在左右查詢,最后將左右的結果合并就是最后的結果 。

threeDay

  1. 線段樹的更新操作
    給定一個index和要更改為的值val,同樣使用遞歸的方法,如果左右的邊界相同了,說明這時候找到了index位置的這個點,那就更新線段樹中的值,如果當前沒有找到,就得到當前的線段樹的頂點表示的區間的中點,如果index比中點大,就在右邊找,如果比區間小,就在左邊找,修改了左邊或右邊的值之后,對它們根節點的值也要更新,也就是對左右節點重新合并

到這里,線段樹的所有的操作都已經完了,下面附上代碼

  • 對接口Merger的定義,它用于實現對區間的合并操作
//用于實現兩個區間的合并操作
public interface Merger<E> {
   E merge(E a,E b);
}

  • 對線段樹的書寫
//實現一個線段樹
public class SegmentTree<E> {
    //創建線段樹
    private E[]tree;
    //這里添加一個data,賦值用戶給出的區間
    private E[]data;
    //創建一個merge,用于實現兩個區間的合并
    private Merger<E> merge;
    //實現構造函數,用戶只需要給出一個區間 
    public SegmentTree(E[]arr,Merger<E> merge) {
        this.merge=merge;
        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);
    }
    //實現線段樹的創建操作
    private void buildSegmentTree(int treeIndex,int l,int r) {
        //這里創建這個線段樹要使用遞歸的方法來創建
        //設置基線條件
        if(l==r) {
            tree[treeIndex]=data[l];
            return;
        }//如果左右的邊界相同,那么這個節點表示的區間的值就是data[l]的值
        //否則的話,取構建這個節點的左邊和右邊
        int leftTreeIndex=LeftChild(treeIndex);
        int rightTreeIndex=RightChild(treeIndex);
        //找到區間的中點,將區間分為兩部分
        int mid=l+(r-l)/2;
        buildSegmentTree(leftTreeIndex, l, mid);
        buildSegmentTree(rightTreeIndex, mid+1, r);
        //左右的區間都創建完畢后,根節點的區間就是合并左右兩個區間
        tree[treeIndex]=merge.merge(tree[leftTreeIndex],tree[rightTreeIndex]);
    }
    //實現線段樹的查詢操作
    public E query(int queryL,int queryR) {
        if(queryL<0||queryL>=data.length||queryR<0||queryR>=data.length) {
            throw new IllegalArgumentException("the segment is illegal");
        }//判斷給定的區間的合法性
        return query(0,0,data.length-1,queryL,queryR);
    }
    private E query(int treeIndex,int l,int r,int queryL,int queryR) {
        //首先設立基線條件就是當要查找的區間等于目前的區間的話,直接返回這個區間對應的值就好了
        if(queryL==l&&queryR==r) {
            return tree[treeIndex];
        }
        //這里先獲取左節點的位置和右節點的位置
        int leftTreeIndex=LeftChild(treeIndex);
        int rightTreeIndex=RightChild(treeIndex);
        //獲取區間的中點
        int mid=l+(r-l)/2;
        if(queryL>=mid+1) {
            return query(rightTreeIndex, mid+1, r, queryL, queryR);
        }//如果區間在右邊,那就無須看左邊了
        else if(queryR<=mid) {
            return query(leftTreeIndex, l, mid, queryL, queryR);
        }//如果區間在左邊,就無須看右邊了
        else {
        //最后一種情況就是這個要查詢的區間在左右子樹都有
        E leftValue=query(leftTreeIndex, l, mid, queryL, mid);//在左邊找到queryL到mid的值
        E rightValue=query(rightTreeIndex, mid+1,r,mid+1,queryR);//在右邊找到mid+1到qureyR的值
        return merge.merge(leftValue, rightValue);//將左右合并返回就好了
        }
    }
    public void set(int index,E val) {
        if(index<0||index>=data.length) {
            throw new IllegalArgumentException("the index is illegal");
        }
        data[index]=val;
        set(0,0,data.length-1,index,val);
    }//將第index位置的元素變為val
    private void set(int treeIndex,int l,int r,int index,E val) {
        if(l==r) {
            tree[treeIndex]=val;
            return;
        }//如果左右邊界相等,證明這個時候只有一個元素,那就是index位置的元素
        int mid=l+(r-l)/2;//獲得區間中點的值
        int leftIndex=LeftChild(treeIndex);//獲取左孩子的位置
        int rightIndex=RightChild(treeIndex);//獲取右孩子的位置
        if(index>=mid+1) {
            set(rightIndex, mid+1,r,index,val);
        }//如果index位置在右邊,就去修改右邊的值
        else {
            set(leftIndex, l,mid,index,val);
        }//如果index位置在左邊,就去左邊修改值
        tree[treeIndex]=merge.merge(tree[leftIndex], tree[rightIndex]);//最后更新根節點的值
    }
    private E get(int index) {
        //獲得某個位置的數據
        if(index<0||index>data.length) {
            throw new IllegalArgumentException("The index is nort legal");
        }
        return data[index];
    }
    private int getSize() {
        //獲取區間的大小
        return data.length;
    }
    private int LeftChild(int index) {
        //獲取左孩子節點的位置
        return 2*index+1;
    }
    private int RightChild(int index) {
        //獲取右孩子節點的位置
        return 2*index+2;
    }
    @Override
    public String toString() {
        StringBuffer res=new StringBuffer();
        res.append('[');
        for(int i=0;i<tree.length;i++) {
                res.append(tree[i]);
            if(i!=tree.length-1) {
                res.append(", ");
            }
        }
        res.append(']');
        return res.toString();
    }
}

這里還有一種和線段樹很相似的數據結構:樹狀數組,它也可以完成對于數組中某個區間的查詢和更新的操作,但是實現起來比線段樹要簡單,其中要使用到lowbit思想,對于lowbit就是指的某個數字的最低位的1所對應的10進制數字,例如10的lowbit為2,它的求解方法也很簡單,就是num&(-num),我們來看一下樹狀數組的兩種操作

  1. query(int x) 指的是數組中[1,x]的所有元素的和
int res=0;
while x>0:
    res+=nums[x];
    x-=lowbit(x);
  1. update(int x,int val) 指的是將某個位置的元素加val,實現的簡略代碼如下:
while x<=n:
    nums[x]+=val;
    x=x+lowbit(x);

對于樹狀數組,具體的實現代碼如下:

public class FenwickTree {
    public int[]tree;
    public int[]data;
    public int size;
    
    //設置構造函數
    public FenwickTree(int[] nums) {
        int n=nums.length;
        data=new int[n+1];
        tree=new int[n+1];
        this.size=n;
        for(int i=1;i<=n;i++) {
            data[i]=nums[i-1];
        }
        
        
        for(int i=1;i<=n;i++) {
            this.update(i,nums[i-1]);
        }
    }
    
    public void update(int x,int val) {
        int n=this.size;
        while(x<=n) {
            this.tree[x]+=val;
            x+=lowbit(x);
        }
    }
    
    public int query(int x) {
        int res=0;
        while(x>0) {
            res+=this.tree[x];
            x-=lowbit(x);
        }
        
        return res;
    }
    
    private int lowbit(int num) {
        return num&(-num);
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容