線段樹的設計思路和基本實現

線段樹是個啥?

在平常見到的樹形數據結構中,操作對象都是單個元素,像二分搜索樹……;假設要對一個區間進行操作(比如求某個子區間的和),可以使用數組來表示區間,直接對數組進行操作,明顯缺點就是時間複雜度過高;這裏可以將一個區間拆分爲一個個子區間,所有的區間作爲二叉樹的結點,這顆二叉樹是一顆平衡二叉樹,即線段樹。

如將區間 {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)]);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章