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、線段樹的一般結構
上面的例子當中,正好數組大小正好是 ,所以看起來像是滿二叉樹,其實不是。我們看下面的圖:
看得出來,這並不是一個滿的二叉樹,也不是一個完全二叉樹。但是這是一個平衡二叉樹。
平衡二叉樹: 最大深度和最小深度之間的差值爲 1 。
2.2、線段樹存儲所需空間
我們知道線段樹是我們損失空間來減小時間的,那我們實際過程中,需要多少空間呢。下面我們就來推導一下。
層數 | 節點個數 |
---|---|
0 | 1 |
1 | 2 |
2 | 4 |
3 | 8 |
··· | ··· |
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
源碼地址:可在公衆號內回覆 數據結構與算法源碼 即可獲得。