目錄
爲什麼要有線段樹
下面我們從一個經典的例子來了解線段樹,問題描述如下:從數組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));
}
}