Java底層實現 SegmentTree 線段樹

SegmentTree 線段樹(區間樹)


1、爲什麼使用線段樹

  相信大家都見過一個經典的比賽題目(區間染色):在一個數組結構當中,對某一端區間不斷的進行染色。在m次操作後,在這個數組中包含了多少種顏色。以及在m次操作後我們在某一區間可以看見多少種顏色。

  其中主要包含兩種操作,染色操作(更新區間)以及查詢操作(查詢區間)。我們發現我們主要是針對區間進行操作,而且我們只關心區間的顏色種類的個數,並不關心它的顏色是什麼。

  如果我們採用最基本的數組遍歷的操作。使用數組實現,兩種操作的時間複雜度均爲O(N)級別的操作。這樣我們的線段樹顯得就尤爲重要。

  還有一種經典問題就是區間查詢:例如我們在一段數組內查詢某一個區間的最大值、最小值或者區間數字和。針對區間進行操作的我們都要想要線段樹這種數據結構。由於我們線段樹採用的樹結構,所以時間複雜度就會很低。

時間複雜度:

數組實現 線段樹實現
更新操作 O(N) O(log(N))
查詢操作 O(N) O(log(N))

2、線段樹的基本結構

  在線段樹中我們並不考慮增加元素和刪除元素,我們只考慮在已有的數組結構中構建線段樹這種數據結構。

基本結構

  我們可以看出,一個數組分成了很多區間,基本上都是對半劈開,小夥伴可能會問了,結構變得更加複雜了。在這裏正是運用了計算機領域的一句話 :

用空間換取時間

注意: 每一個區間並不是存儲一個區間,而是一個值。例如我們以求和爲例,A[0 ··· 3] 節點存儲的是這個區間的和。如果是最大值,那麼存儲的就是這一個區間的最大值。

例子: 當我們查詢A[2-5]區間的和,那我們只需要知道A[2-3]和A[4-5]的和就可以啦,我們並不需要遍歷2-5的所有數據。

2.1、線段樹的一般結構

  上面的例子當中,正好數組大小正好是 23=82^3 = 8,所以看起來像是滿二叉樹,其實不是。我們看下面的圖:

基本結構

  看得出來,這並不是一個滿的二叉樹,也不是一個完全二叉樹。但是這是一個平衡二叉樹。

平衡二叉樹: 最大深度和最小深度之間的差值爲 1 。

2.2、線段樹存儲所需空間

  我們知道線段樹是我們損失空間來減小時間的,那我們實際過程中,需要多少空間呢。下面我們就來推導一下。

層數 節點個數
0 1
1 2
2 4
3 8
··· ···
h - 1 2h12^{h-1}

  對於滿的二叉樹來說,hh 層一共有2h12^h-1個節點。所以大約等於 2h2^h 個。而且我們看到最後一層的個數爲2h12^{h-1}正好等於總個數的一半。於是我們可以得到:

最後一層的節點數大致等於(差1)前面所有層數節點之和。

  假設我們的數組有 n 個元素,按照滿的二叉樹的形式來看,最底下一層元素的個數爲 n。那麼他上面的所有節點和也爲n,所以對應的總個數爲2 * n。這只是還在滿二叉樹的情況下。如果我們的元素不是2的整數次冪,那麼它就構成了平衡二叉樹,所以需要向下擴充一層,這一層的個數等於我們上面所有節點的和也就是 2 * n。加起來就是 4 * n。我們可以得出結論:

對於存儲 n 個元素的線段樹,我們需要開闢 4 倍的空間大小。

  雖然我們浪費了大量的空間來存儲,但是我們在時間複雜度上有着巨大的提升。隨着社會的發展,存儲已經變得不再是問題,問題變成了時間速度問題,所以說犧牲空間提升時間是一件非常有意義的事情。

3、線段樹的實現

3.1、Merge 函數

  在實際過程中底層並不確定用戶對線段樹執行什麼操作,求和,最大值還是最小值操作,所以這裏我們引入merge函數,用戶來指定他們需要對線段樹執行什麼操作。
Merge接口函數:

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

3.2、構造函數

  在構造函數裏面需要用戶傳入用戶設定的Merge實例對象,以對線段樹進行用戶想要類型的操作。
構造函數實現:

public SegmentTree(E[] arr, Merger<E> merger) {

    this.merger = merger; // 指定merge實例

    data = (E[])new Object[arr.length]; //拷貝數組
    System.arraycopy(arr, 0, data, 0, arr.length);

    tree = (E[])new Object[4 * arr.length]; //開啓4倍的空間
    buildSegmentTree(0, 0, data.length - 1);
}

3.3、基本操作函數

  主要包含數據的接口操作,例如getSize和get操作。

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

  這裏我們線段樹的底層依然是數組,所以我們依然可以按照我們之前的 MaxHeap 最大堆 那種結構來實現。

private int leftChild(int index) {
    return index * 2 + 1;
}

private int rightChild(int index) {
    return index * 2 + 2;
}

3.4、構建線段樹

  我們需要在treeIndex索引的位置創建數組區間。這裏我們採用遞歸方式進行構建樹結構。爲什麼採用遞歸的形式呢,因爲我們需要慢慢回朔到頂層,而且在構建的時候我們需要採用後序遍歷的形式,這樣才能保證節點最後合併孩子節點。

private void buildSegmentTree(int treeIndex, int l, int r) {
    if (l == r) {
        tree[treeIndex] = data[l];
        return;
    }
    int mid = l + (r - l) / 2;  //中間值
    buildSegmentTree(leftChild(treeIndex), l, mid);
    buildSegmentTree(rightChild(treeIndex), mid + 1, r);
    tree[treeIndex] = merger.merge(tree[leftChild(treeIndex)], tree[rightChild(treeIndex)]);
}

3.5、查詢操作

  查詢操作主要涉及的問題就是區間查找匹配的問題,所以在遞歸的問題匹配主要分爲三種情況:

  • 查詢區間完全在右孩子區間上
  • 查詢區間完全在左孩子區間上
  • 查詢區間部分在左孩子區間上,部分在右孩子區間上
public E query(int queryL, int queryR) {
    if (queryL < 0 || queryL >= data.length || queryR < 0 || queryR >= data.length)
        throw new IllegalArgumentException("index 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 (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 left = query(leftTreeIndex, l, mid, queryL, mid); // 分開查詢
    E right = query(rightTreeIndex, mid + 1, r, mid + 1, queryR);
    return merger.merge(left, right); //將分開的元素合併
}

3.6、更改操作

  當我們對數組的某一個索引位置進行修改,我們需要先找到這個樹結構的索引位置,然後不斷回朔,重新合併。

程序實現:

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

private void set(int treeIndex, int l, int r, int index, E e) {
    if (l == r){
        tree[treeIndex] = e;
        return;
    }
    int mid = l + (l - r) / 2;
    if (index > mid)
        set(rightChild(treeIndex), mid + 1, r, index, e);
    else
        set(leftChild(treeIndex), l, mid, index, e);
    tree[treeIndex] = merger.merge(tree[leftChild(treeIndex)], tree[rightChild(treeIndex)]);
}

最後

更多精彩內容,大家可以轉到我的主頁:曲怪曲怪的主頁

或者關注我的微信公衆號:TeaUrn

源碼地址:可在公衆號內回覆 數據結構與算法源碼 即可獲得。

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