線段樹(Java)

線段樹

線段樹是一種二叉搜索樹,與區間樹相似,它將一個區間劃分成一些單元區間,每個單元區間對應線段樹中的一個葉結點。
使用線段樹可以快速的查找某一個節點在若干條線段中出現的次數,時間複雜度爲O(logN)。而未優化的空間複雜度爲2N,實際應用時一般還要開4N的數組以免越界,因此有時需要離散化讓空間壓縮。
在這裏插入圖片描述
這不是一棵完全二叉樹,也不是滿二叉樹,是一棵平衡二叉樹,如果將這棵樹的最後一層不存在的節點定義爲null,補齊就可以看做是一棵滿二叉樹,就可以使用數組作爲底層進行表示。

SegmentTree.java(線段樹)

//線段樹
public class SegmentTree<E> {
	private E[] data;// 底層數組
	private E[] tree;// 構建線段樹
	private Merger<E> merger;// 線段樹融合器

	// 構造方法 傳入arr數組與融合器匿名函數
	public SegmentTree(E[] arr, Merger<E> merger) {
		// TODO Auto-generated constructor stub
		this.merger = merger;
		data = (E[]) new Object[arr.length];// 初始化arr數組
		for (int i = 0; i < arr.length; i++)
			data[i] = arr[i];
		tree = (E[]) new Object[4 * arr.length];// 初始化tree數組存放線段樹
		buildSegmentTree(0, 0, arr.length - 1);
	}

	// 在treeIndex的位置創建表示區間[l...r]的線段樹 遞歸函數
	private void buildSegmentTree(int treeIndex, int l, int r) {
		if (l == r) { // 遞歸的終止條件
			tree[treeIndex] = data[l];// 當線段樹子樹長度爲1,也就是它本身
			return;
		}
		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]爲當前兩個孩子融合得到的結果
		tree[treeIndex] = merger.meger(tree[leftTreeIndex], tree[rightTreeIndex]);
	}

	public E get(int index) {
		if (index < 0 || index >= data.length)
			try {
				throw new Exception("index越界");
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		return data[index];
	}

	public int getSize() {
		return data.length;
	}

	// 獲取該索引的左孩子的索引
	private int leftChild(int index) {
		return 2 * index + 1;
	}

	// 獲取該索引的右孩子的索引
	private int rightChild(int index) {
		return 2 * index + 2;
	}

	// 返回區間[queryL,queryR]的值
	public E query(int queryL, int queryR) {
		if (queryL < 0 || queryL >= data.length || queryR < 0 || queryR >= data.length || queryL > queryR)
			try {
				throw new Exception("[queryL,queryR]區間錯誤");
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		return query(0, 0, data.length - 1, queryL, queryR);
	}

	// 在以treeIndex爲根節點的線段樹中[l,r]的範圍內,搜索[queryL,queryR]的值
	// 遞歸函數
	private E query(int treeIndex, int l, int r, int queryL, int queryR) {
		// TODO Auto-generated method stub
		if (l == queryL && r == queryR)
			return tree[treeIndex];
		int mid = l + (r - l) / 2;
		int leftTreeIndex = leftChild(treeIndex);
		int rightTreeIndex = rightChild(treeIndex);
		// [queryL,queryR]完全在左子樹區間或右子樹區間
		if (queryL >= mid + 1)// 完全在右子樹查詢
			return query(rightTreeIndex, mid + 1, r, queryL, queryR);
		else if (queryR <= mid)// 完全在左子樹查詢
			return query(leftTreeIndex, l, mid, queryL, queryR);

		// [queryL,queryR]不完全在左子樹區間或右子樹區間
		E leftResult = query(leftTreeIndex, l, mid, queryL, mid);// 在左邊區間查詢
		E rightResult = query(rightTreeIndex, mid + 1, r, mid + 1, queryR);// 在右邊區間查詢
		return merger.meger(leftResult, rightResult);// 融合返回
	}

	// 將index位置的值,更新爲e
	public void set(int index, E e) {
		if (index < 0 || index >= data.length)
			try {
				throw new Exception("index越界");
			} catch (Exception e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}
		data[index] = e;
		set(0, 0, data.length - 1, index, e);
	}

	// 在treeIndex爲根的線段樹中更新index的值爲e
	// 遞歸函數
	private void set(int treeIndex, int l, int r, int index, E e) {
		if (l == r) { // 遞歸終止條件
			tree[treeIndex] = e; // 當線段樹子樹長度爲1,也就是它本身
			return;
		}
		int mid = l + (r - l) / 2;// 線段樹分界點
		int leftTreeIndex = leftChild(treeIndex);
		int rightTreeIndex = rightChild(treeIndex);
		if (index >= mid + 1)// 在右子樹進行修改
			set(rightTreeIndex, mid + 1, r, index, e);
		else // 即 index<=mid 在左子樹進行修改
			set(leftTreeIndex, l, mid, index, e);
		// 由於index節點進行了修改,index的祖輩節點也應該被修改,即重新調用融合器
		tree[treeIndex] = merger.meger(tree[leftTreeIndex], tree[rightTreeIndex]);
	}

	public String toString() {
		StringBuilder res = new StringBuilder();
		res.append('[');
		for (int i = 0; i < tree.length; i++) {
			if (tree[i] != null)
				res.append(tree[i]);
			else
				res.append("null");
			if (i != tree.length - 1)
				res.append(',');
		}
		res.append(']');
		return res.toString();
	}

}

Merger.java(融合器接口)

//融合器接口
public interface Merger<E> {
		E meger(E a,E b);//通過一個meger操作把a和b兩個元素轉換成一個元素返回
}

Main.java(測試)


public class Main {
	public static void main(String[] args) {
		Integer[] nums = { -2, 0, 3, -5, 2, -1 };
		//匿名函數
		// SegmentTree<Integer> segTree = new SegmentTree<>(nums, new Merger<Integer>()
		// {
		//
		// @Override
		// public Integer meger(Integer a, Integer b) {
		// // TODO Auto-generated method stub
		// return a+b;
		// }
		// });
		SegmentTree<Integer> segTree = new SegmentTree<>(nums, (a, b) -> a + b);//傳入a和b,返回a+b
		System.out.println(segTree);//輸出線段樹
		System.out.println(segTree.query(0, 2));//輸出線段樹[0-2]區間的和
		segTree.set(0, 6);//將data[0]修改爲6
		System.out.println(segTree.query(0,2));//輸出線段樹[0-2]區間的和
	}
}

測試結果

在這裏插入圖片描述

總結

線段樹查詢與更新操作時間複雜度爲O(logn)
利用線段樹,我們可以高效地詢問和修改一個數列中某個區間的信息,並且代碼也不算特別複雜。
但是線段樹也是有一定的侷限性的,其中最明顯的就是數列中數的個數必須固定,即不能添加或刪除數列中的數。

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