線段樹總結-java版

目錄

爲什麼要有線段樹

簡介

得到min的線段樹樣例

java實現

合成器

線段樹

測試


 


爲什麼要有線段樹

下面我們從一個經典的例子來了解線段樹,問題描述如下:從數組arr[0...n-1]中查找某個數組某個區間內的最小值,其中數組大小固定,但是數組中的元素的值可以隨時更新。

對這個問題一個簡單的解法是:遍歷數組區間找到最小值,時間複雜度是O(n),額外的空間複雜度O(1)。當數據量特別大,而查詢操作很頻繁的時候,耗時可能會不滿足需求。

另一種解法:使用一個二維數組來保存提前計算好的區間[i,j]內的最小值,那麼預處理時間爲O(n^2),查詢耗時O(1), 但是需要額外的O(n^2)空間,當數據量很大時,這個空間消耗是龐大的,而且當改變了數組中的某一個值時,更新二維數組中的最小值也很麻煩。

簡介

線段樹之所以稱爲“樹”,是因爲其具有樹的結構特性。線段樹由於本身是專門用來處理區間問題的(包括RMQ、RSQ問題等)。

對於每一個子節點而言,都表示整個序列中的一段子區間;對於每個葉子節點而言,都表示序列中的單個元素信息子節點不斷向自己的父親節點傳遞信息,而父節點存儲的信息則是他的每一個子節點信息的整合。

有沒有覺得很熟悉?對,線段樹就是分塊思想的樹化,或者說是對於信息處理的二進制化——用於達到O(logn)級別的處理速度,log以2爲底。(其實以幾爲底都只不過是個常數,可忽略)。而分塊的思想,則是可以用一句話總結爲:通過將整個序列分爲有窮個小塊,對於要查詢的一段區間,總是可以整合成k個所分塊與m個單個元素的信息的並(0≤k,m≤sqrt{n})。但普通的分塊不能高效率地解決很多問題,所以作爲log級別的數據結構,線段樹應運而生。

得到min的線段樹樣例

我們可以用線段樹來解決這個問題:預處理耗時O(n),查詢、更新操作O(logn),需要額外的空間O(n)。根據這個問題我們構造如下的二叉樹

  • 葉子節點是原始組數arr中的元素
  • 非葉子節點代表它的所有子孫葉子節點所在區間的最小值

例如對於數組[2, 5, 1, 4, 9, 3]可以構造如下的二叉樹(背景爲白色表示葉子節點,非葉子節點的值是其對應數組區間內的最小值,例如根節點表示數組區間arr[0...5]內的最小值是1):                                                                                                                

由於線段樹的父節點區間是平均分割到左右子樹,因此線段樹是完全二叉樹,對於包含n個葉子節點的完全二叉樹,它一定有n-1個非葉節點,總共2n-1個節點,因此存儲線段是需要的空間複雜度是O(n)。

java實現

合成器

合成器,代表父節點,根據兩個子節點得到的value

如果設置爲最大或者最小之類的,怎麼設置看測試那裏

package datastructure.tree.segementtree;

/**合成器接口
 * @author xusy
 *
 * @param <E>
 */
public interface Merger<E>{
	
	/**合成方法,a和b代表一個父節點下的兩個子節點的值
	 * @param a
	 * @param b
	 * @return 根據a和b,計算出的父節點對應的值
	 */
	public E merge(E a,E b);

}

線段樹

可以看到每個節點對應的data中的左邊界和右邊界,沒有記錄在節點中,是在方法中不斷遞歸計算的

一開始的root,手動設置index=0,左邊界爲0,右邊界爲length-1

然後子節點,左孩子對應[left,mid]  右孩子對應[mid+1,right]

如果想要得到某個區間,對應的問題的值,調用query(left,right) 即可

修改用set(index,value)

 

package datastructure.tree.segementtree;

/** 線段樹
 * @author xusy
 *
 * @param <E>
 */
public class SegementTree<E>{
	
	/**
	 * 線段樹中傳入的值,存儲的副本
	 */
	public E[] data;
	
	/**
	 * 線段樹中的節點,其中父節點的值爲它的兩個子節點merge後的值
	 */
	public E[] tree;
	
	/**
	 * 合成器,構造線段樹時候同時傳入合成器
	 */
	public Merger<E> merger;
	
	/**構造線段樹
	 * @param data  傳入的數據
	 * @param merger  傳入的合成器
	 */
	public SegementTree(E[] data,Merger<E> merger){
		this.merger=merger;
		int length=data.length;
		this.data=(E[])new Object[length];
		//複製數據到data中
		for(int i=0;i<length;i++){
			this.data[i]=data[i];
		}
		//總共n個葉子節點,n-1個非葉子節點
		tree=(E[])new Object[length*2-1];
		//構造線段樹
		buildSegementTree(0,0,length-1);		
	}
	
	/** 構造線段樹中的tree中的節點
	 * @param treeIndex tree中對應節點的index
	 * @param left  這個節點對應data中的範圍的左邊界,root對應0
	 * @param right 這個節點對應data中的範圍的右邊界,root對應length-1
	 */
	public void buildSegementTree(int treeIndex,int left,int right){		
		if(left==right){
			//如果left==right,證明遞歸結束,在對應的index設置data裏left的值
			tree[treeIndex]=data[left];
			return;
		}
		//tree中父節點爲treeIndex,的左右孩子的index
		int leftChildIndex=getLeftChild(treeIndex);
		int rightChildIndex=getRightChild(treeIndex);
		int mid=left+(right-left)/2;
		//構造左右孩子節點
		buildSegementTree(leftChildIndex, left, mid);
		buildSegementTree(rightChildIndex, mid+1, right);
		//根據左右孩子的值,通過合成器,決定父節點的值
		tree[treeIndex]=merger.merge(tree[leftChildIndex], tree[rightChildIndex]);
	}
	
	/**返回左孩子在數組中的位置
	 * @param index  父節點的index
	 * @return 左孩子節點的index
	 */
	public int getLeftChild(int index){
		//可以這樣看,root節點,index:0
		//root的左孩子,index:1
		//root的右孩子,index:2
		//root的左孩子的左孩子,index:3
		//root的左孩子的有孩子,index:4
		return 2*index+1;
	}
	
	/**返回右孩子在數組中的位置
	 * @param index  父節點的index
	 * @return 右孩子節點的index
	 */
	public int getRightChild(int index){
		return 2*index+2;
	}
	
	/**
	 * 打印線段樹
	 */
	public void printSegementTree(){
		System.out.println("開始打印線段樹----------");
		System.out.println("線段樹數據的長度爲"+data.length);
		for(int i=0;i<tree.length;i++){
			System.out.println("位置"+i+": "+tree[i]);
		}
		
		System.out.println("打印線段樹結束----------");
	}
	
	/** 返回data中區間left和right間,對應的值
	 * @param left
	 * @param right
	 * @return
	 */
	public E query(int left,int right){
		if(left<0||right<0||left>=data.length||right>=data.length||left>right){
			return null;
		}
		return queryRange(0,0,data.length-1,left,right);
		
	}
	
	/** 在以tree中位置爲treeIndex爲根節點,而且該節點對應的data中的範圍爲[treeLeft,treeRight] <br>
	 * 	查詢範圍爲[queryLeft,queryRight]對應的值
	 * @param treeIndex
	 * @param treeLeft
	 * @param treeRight
	 * @param queryLeft
	 * @param queryRight
	 * @return
	 */
	public E queryRange(int treeIndex,int treeLeft,int treeRight,int queryLeft,int queryRight){
		if(treeLeft==queryLeft&&treeRight==queryRight){
			//如果該節點的範圍正好對應查詢範圍,直接返回
			return tree[treeIndex];
		}
		int leftChildIndex=getLeftChild(treeIndex);
		int rightChildIndex=getRightChild(treeIndex);
		int mid=treeLeft+(treeRight-treeLeft)/2;
		if(queryLeft>=mid+1){
			//如果查詢範圍僅僅對應左孩子或者右孩子
			return queryRange(rightChildIndex, mid+1, treeRight, queryLeft, queryRight);
		}
		else{
			if(queryRight<=mid){
				return queryRange(leftChildIndex, treeLeft, mid, queryLeft, queryRight);
			}
		}
		//查詢範圍,左右孩子都有
		E resultLeft=queryRange(leftChildIndex, treeLeft, mid, queryLeft, mid);
		E resultRight=queryRange(rightChildIndex, mid+1, treeRight, mid+1, queryRight);
		//最終結果是左右孩子的合併
		E result=merger.merge(resultLeft, resultRight);		
		return result;
	}


	/**在線段樹中修改data中index的元素,設置新的值爲value
	 * @param index
	 * @param value
	 */
	public void set(int index,E value){
		if(index<0||index>=data.length){
			return;
		}
		setValue(0,0,data.length-1,index,value);
	}
	
	/**在以tree中位置爲treeIndex爲根節點,而且該節點對應的data中的範圍爲[treeLeft,treeRight] 下,<br>
	 * 修改data中index的元素,設置新的值爲value
	 * @param treeIndex
	 * @param treeLeft
	 * @param treeRight
	 * @param index
	 * @param value
	 */
	public void setValue(int treeIndex,int treeLeft,int treeRight,int index,E value){
		if(treeLeft==treeRight){
			tree[treeIndex]=value;
			return;
		}
		int leftChildIndex=getLeftChild(treeIndex);
		int rightChildIndex=getRightChild(treeIndex);
		int mid=treeLeft+(treeRight-treeLeft)/2;
		if(index<=mid){
			setValue(leftChildIndex, treeLeft, mid, index, value);
		}
		else{
			setValue(rightChildIndex, mid+1, treeRight, index, value);
		}
		tree[treeIndex]=merger.merge(tree[leftChildIndex], tree[rightChildIndex]);
	}
	
}

測試

下面就有merger對象的生成方式,得到小的值

package datastructure.tree.segementtree;

public class Main {

	public static void main(String[] args) {
		Merger<Integer> merger=new Merger<Integer>() {			
			@Override
			public Integer merge(Integer a, Integer b) {
				if(a<b){
					return a;
				}
				else{
					return b;
				}
			}
		};
		
		Integer[] data=new Integer[]{1,4,7,-4,3};
		SegementTree<Integer> tree=new SegementTree<>(data, merger);
		tree.printSegementTree();
		System.out.println(tree.query(1, 4));
		tree.set(3, 0);
		System.out.println(tree.query(1, 4));

	}

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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