文章目錄
線段樹是個啥?
在平常見到的樹形數據結構中,操作對象都是單個元素,像二分搜索樹……;假設要對一個區間進行操作(比如求某個子區間的和),可以使用數組來表示區間,直接對數組進行操作,明顯缺點就是時間複雜度過高;這裏可以將一個區間拆分爲一個個子區間,所有的區間作爲二叉樹的結點,這顆二叉樹是一顆平衡二叉樹,即線段樹。
如將區間 {1, 5, 3, 9, 12, 7, 15, 10} 存入線段樹中,它得樹形結構如下:
在邏輯上,線段樹的每一個結點的確都是區間,但是在物理存儲上,線段樹中無須存放區間(這樣會增加內存的開銷),只需要存放我們想要的區間操作結果即可。
線段樹的操作一般不會涉及到在區間中動態添加元素的問題,所以使用樹的順序存儲形式是比較好的。
如何創建一個線段樹
線段樹不會在結點中存放區間,那麼就需要一個單獨的數組來存放這個區間,同時,需要記住我們所有的操作都是基於區間的。
數組空間的大小分配
存放區間的數組空間是固定的,這裏需要注意存放樹結點的數組空間大小,首先需要了解二叉樹的兩個性質:
性質1:在二叉樹的第 i 層至多有 2 i-1 個結點(i >= 1)
性質2:深度爲 k 的二叉樹至多有 2 k - 1 個結點(k >= 1)
假設滿二叉樹的高度爲 n + 1,則前 n 層有 2 n -1 個結點,第 n + 1 層 有 2 n 個結點
結論: 滿二叉樹的第 n 層的結點數目大致等於 前 n-1 層的結點數目。
線段樹不一定是滿二叉樹,但是用滿二叉樹的空間絕對好使:
- 當區間的大小爲 n(n % 2 == 0) 時,此時的線段樹是一顆滿二叉樹,分配 2n 個空間就好使
- 當區間的大小爲 n + 1 (n % 2 != 0)時,此時的線段樹不是滿二叉樹;同時,線段樹是一顆平衡二叉樹,所以結點爲單個元素的區間之間的高度差不會超過 1 ,也就是說,在滿二叉樹的基礎上再分配一層的空間絕對夠使,所以分配 4n 個空間。
線段樹的初始化:buildSegementTree 操作
將一個區間劃分爲若干子區間時,採用的是等分操作,如下:
那麼,我們是如何在樹中找到區間呢?
此處,我們利用每一個區間上下邊界的中間值 mid ,來劃分左孩子和右孩子中的區間範圍:
- 左孩子區間範圍爲[ 0, mid ]
- 右孩子區間範圍爲[ mid+1, data.length]
通過上面的劃分策略,很容易能看出來,線段樹其實也是一顆二分搜索樹。
工具類 Merge 操作擴大線段樹的使用範圍
Merge工具類使用方法類似於 Comparator 的使用,我們只需要在實現 Merge 接口的類中重寫一個方法,該方法中規定了對區間進行操作的規則,比如:區間求和、求最大值等,操作結果會存入線段樹中。
public interface Merge<E> {
E merge(E argument1, E argument2);
}
線段樹的基礎代碼
public class SegementTree<E> {
E[] data; //data用來存儲區間中的元素
E[] tree; //tree用來存儲線段樹中結點
Merge<E> merge;
public SegementTree(E[] arr, Merge<E> merge) {
this.merge = merge;
data =(E[]) new Object[arr.length];
for(int i = 0; i < arr.length; i++) {
data[i] = arr[i];
}
if(arr.length % 2 == 0)
tree = (E[]) new Object[arr.length * 2];
else
tree = (E[]) new Object[arr.length * 4];
buildSegementTree(0, 0, data.length-1);
}
public int getSize() {
return data.length;
}
public E get(int idx) {
if(idx < 0 || idx >= data.length) {
throw new IllegalArgumentException("Illegal Idx");
}
return data[idx];
}
private void buildSegementTree(int treeIdx, int l, int r) {
if(l == r) {
tree[treeIdx] = data[l];
return;
}
int mid = l + (r - l)/2;
buildSegementTree(leftChild(treeIdx), l, mid);
buildSegementTree(rightChild(treeIdx), mid+1, r);
tree[treeIdx] = merge.merge(tree[leftChild(treeIdx)], tree[rightChild(treeIdx)]);
}
//獲得左孩子的索引值
private int leftChild(int idx) {
return idx * 2 + 1;
}
//獲得右孩子的索引值
private int rightChild(int idx) {
return idx * 2 + 2;
}
public String toString() {
StringBuilder str = new StringBuilder();
str.append('[');
for(int i = 0; i < tree.length; i++) {
str.append(tree[i]);
if(i != tree.length - 1)
str.append(',');
}
str.append(']');
return str.toString();
}
}
測試線段樹的方法
public class MainTest {
public static void main(String[] args) {
Integer[] arr = new Integer[] {1, 5, 3, 9, 12, 7, 15, 10};
SegementTree<Integer> segement = new SegementTree<>(arr, new Merge<Integer>() {
@Override
public Integer merge(Integer argument1, Integer argument2) {
return argument1+argument2;
}
});
System.out.println(segement);
}
}
Tip: 在使用構造器時,需要傳入一個 Merge 類的實例對象(這裏叫它融合器),融合器中定義了操作的規則。
操作結果:
線段樹的查詢:Query 操作
假設查詢區間 [2, 5] 的和,實現算法思路中有三種情況:
第一種,區間全部包含在左子樹中,即 右邊界是小於等於中間值 mid 的
第二種,區間全部包含在右子樹中,即 左邊界是大於中間值 mid 的
第三種,就是例中的區間包含在左子樹和右子樹中
public E query(int idxL, int idxR) {
return query(0, idxL, idxR, 0, data.length-1);
}
private E query(int treeIdx, int idxL, int idxR, int l, int r) {
if(idxL == l && idxR == r) {
return tree[treeIdx];
}
int mid = l + (r-l)/2;
if(idxL > mid)
return query(rightChild(treeIdx), idxL, idxR, mid+1, r);
if(idxR <= mid)
return query(leftChild(treeIdx), idxL, idxR, l, mid);
E leftMerge = query(leftChild(treeIdx), idxL, mid, l, mid);
E rightMerge = query(rightChild(treeIdx), mid+1, idxR, mid+1, r);
return merge.merge(leftMerge, rightMerge);
}
線段樹的更新:Update 操作
這裏的更新操作同樣是對區間中元素進行更新,例如將區間中位置爲 0 的元素更新爲 13:
這樣,更改一個元素,會引起線段樹中包含該元素的所有區間結點都進行更新。
public void update(int idx, E element) {
if(idx < 0 || idx >= data.length) {
throw new IllegalArgumentException("Illegal Idx");
}
data[idx] = element;
updata(0, 0, data.length-1, idx, element);
}
private void updata(int treeIdx, int l, int r, int idx, E element) {
if(l == r) {
tree[treeIdx] = element;
return;
}
int mid = l + (r-l)/2;
if(idx <= mid)
updata(leftChild(treeIdx), l, mid, idx, element);
if(idx > mid)
updata(rightChild(treeIdx), mid+1, r, idx, element);
tree[treeIdx] = merge.merge(tree[leftChild(treeIdx)], tree[rightChild(treeIdx)]);
}