oneDay
- 為什么要使用線段樹
因為對于某一類問題,我們關心的就只是線段(區間) - 線段樹的一些經典問題
區間染色問題:對于給定的一段區間,經過m次染色后,問區間[i,j]的顏色的種類(0=<i,j<=n-1)
區間查詢問題:查詢在一段時間[i,j]區間內的要查詢數據的動態情況
對于這兩個問題,使用數組也是可以解決的,但是時間復雜度為O(n),而使用線段樹的時間復雜度為O(logn) - 線段樹的基本操作
- 更新:更新區間中的一個元素或者一個區間的值
- 查詢:查詢一個區間[i,j]內元素的最大值,最小值,或者區間數字的和
-
線段樹的結構
捕獲.PNG
可以看到這個線段樹是一個滿二叉樹,但是線段樹不一定是滿二叉樹或者是完全二叉樹,但它是一個平衡二叉樹,也就是數中最大的高度和最小的高度的差小于等于1
- 線段樹的底層實現
- 這里使用數組來實現二叉樹,首先先來分析一下對于給定的區間,數組的大小應該是多少(注意:這里將線段樹統一視為滿二叉樹,也就是對于空位置的節點設為null就好了)
對于一棵滿二叉樹,每一層節點的個數與層數之間的關系為2^(n-1),其中最后一層的節點個數為2^(h-1),由于滿二叉樹的所有的節點個數為2^(h)-1,也就近似等于2^h,所以可以發現最后一層的節點的個數近似等于總結點數的一半,而最后一層的節點個數在當給定的區間的個數為n=2^k時,此時的n就是最后一層的節點的個數,所以總結點個數為2n,而如果n=2^k+1,這個時候n比最后一層的節點個數要多,也就是這個滿二叉樹要增加一層,而這一層上面的所有層的節點個數為2n,這一層也為2n,所以總共需要4n大小的數組來存儲線段樹,n代表的就是區間內元素的個數
綜上,使用數組來實現線段樹需要4*n大小的空間(n為區間內元素的個數) - 線段樹沒有添加元素的操作,區間是固定的,使用4*n的靜態空間就好了
twoDay
- 線段樹的創建
線段樹的創建使用了遞歸的方法,因為由線段樹的結構可以知道,根節點的值就是兩個子節點值的合并(這里合并可以是加,減等一系列的操作),在創建這棵線段樹的時候,就先創建它的左右的孩子,然后合并它們的值就是根節點的值(這里在創建的時候,為了使得用戶可以更改對某個區間的操作,創建了一個類似于Comparator的接口merge,使得在初始化線段樹的時候,可以向其中放入對某個區間的操作) - 線段樹的查詢
同樣的,實現線段樹的查詢操作也是使用遞歸來完成的, 對于給定的區間,如果只出現在右邊,那就只需要在右邊查詢,如果只出現在左邊,那就只需要在左邊查詢,但是如果即出現在左邊,又出現在右邊,那就要在左右查詢,最后將左右的結果合并就是最后的結果 。
threeDay
- 線段樹的更新操作
給定一個index和要更改為的值val,同樣使用遞歸的方法,如果左右的邊界相同了,說明這時候找到了index位置的這個點,那就更新線段樹中的值,如果當前沒有找到,就得到當前的線段樹的頂點表示的區間的中點,如果index比中點大,就在右邊找,如果比區間小,就在左邊找,修改了左邊或右邊的值之后,對它們根節點的值也要更新,也就是對左右節點重新合并
到這里,線段樹的所有的操作都已經完了,下面附上代碼
- 對接口Merger的定義,它用于實現對區間的合并操作
//用于實現兩個區間的合并操作
public interface Merger<E> {
E merge(E a,E b);
}
- 對線段樹的書寫
//實現一個線段樹
public class SegmentTree<E> {
//創建線段樹
private E[]tree;
//這里添加一個data,賦值用戶給出的區間
private E[]data;
//創建一個merge,用于實現兩個區間的合并
private Merger<E> merge;
//實現構造函數,用戶只需要給出一個區間
public SegmentTree(E[]arr,Merger<E> merge) {
this.merge=merge;
data=(E[])new Object[arr.length];
for(int i=0;i<arr.length;i++) {
data[i]=arr[i];
}
tree=(E[])new Object[4*arr.length];
//一個線段樹的節點的個數是區間個數的四倍
//創建線段樹,傳入參數就是根節點的坐標,還有根節點表示的區間的范圍
buildSegmentTree(0,0,data.length-1);
}
//實現線段樹的創建操作
private void buildSegmentTree(int treeIndex,int l,int r) {
//這里創建這個線段樹要使用遞歸的方法來創建
//設置基線條件
if(l==r) {
tree[treeIndex]=data[l];
return;
}//如果左右的邊界相同,那么這個節點表示的區間的值就是data[l]的值
//否則的話,取構建這個節點的左邊和右邊
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]=merge.merge(tree[leftTreeIndex],tree[rightTreeIndex]);
}
//實現線段樹的查詢操作
public E query(int queryL,int queryR) {
if(queryL<0||queryL>=data.length||queryR<0||queryR>=data.length) {
throw new IllegalArgumentException("the segment is illegal");
}//判斷給定的區間的合法性
return query(0,0,data.length-1,queryL,queryR);
}
private E query(int treeIndex,int l,int r,int queryL,int queryR) {
//首先設立基線條件就是當要查找的區間等于目前的區間的話,直接返回這個區間對應的值就好了
if(queryL==l&&queryR==r) {
return tree[treeIndex];
}
//這里先獲取左節點的位置和右節點的位置
int leftTreeIndex=LeftChild(treeIndex);
int rightTreeIndex=RightChild(treeIndex);
//獲取區間的中點
int mid=l+(r-l)/2;
if(queryL>=mid+1) {
return query(rightTreeIndex, mid+1, r, queryL, queryR);
}//如果區間在右邊,那就無須看左邊了
else if(queryR<=mid) {
return query(leftTreeIndex, l, mid, queryL, queryR);
}//如果區間在左邊,就無須看右邊了
else {
//最后一種情況就是這個要查詢的區間在左右子樹都有
E leftValue=query(leftTreeIndex, l, mid, queryL, mid);//在左邊找到queryL到mid的值
E rightValue=query(rightTreeIndex, mid+1,r,mid+1,queryR);//在右邊找到mid+1到qureyR的值
return merge.merge(leftValue, rightValue);//將左右合并返回就好了
}
}
public void set(int index,E val) {
if(index<0||index>=data.length) {
throw new IllegalArgumentException("the index is illegal");
}
data[index]=val;
set(0,0,data.length-1,index,val);
}//將第index位置的元素變為val
private void set(int treeIndex,int l,int r,int index,E val) {
if(l==r) {
tree[treeIndex]=val;
return;
}//如果左右邊界相等,證明這個時候只有一個元素,那就是index位置的元素
int mid=l+(r-l)/2;//獲得區間中點的值
int leftIndex=LeftChild(treeIndex);//獲取左孩子的位置
int rightIndex=RightChild(treeIndex);//獲取右孩子的位置
if(index>=mid+1) {
set(rightIndex, mid+1,r,index,val);
}//如果index位置在右邊,就去修改右邊的值
else {
set(leftIndex, l,mid,index,val);
}//如果index位置在左邊,就去左邊修改值
tree[treeIndex]=merge.merge(tree[leftIndex], tree[rightIndex]);//最后更新根節點的值
}
private E get(int index) {
//獲得某個位置的數據
if(index<0||index>data.length) {
throw new IllegalArgumentException("The index is nort legal");
}
return data[index];
}
private int getSize() {
//獲取區間的大小
return data.length;
}
private int LeftChild(int index) {
//獲取左孩子節點的位置
return 2*index+1;
}
private int RightChild(int index) {
//獲取右孩子節點的位置
return 2*index+2;
}
@Override
public String toString() {
StringBuffer res=new StringBuffer();
res.append('[');
for(int i=0;i<tree.length;i++) {
res.append(tree[i]);
if(i!=tree.length-1) {
res.append(", ");
}
}
res.append(']');
return res.toString();
}
}
這里還有一種和線段樹很相似的數據結構:樹狀數組,它也可以完成對于數組中某個區間的查詢和更新的操作,但是實現起來比線段樹要簡單,其中要使用到lowbit思想,對于lowbit就是指的某個數字的最低位的1所對應的10進制數字,例如10的lowbit為2,它的求解方法也很簡單,就是num&(-num),我們來看一下樹狀數組的兩種操作
- query(int x) 指的是數組中[1,x]的所有元素的和
int res=0;
while x>0:
res+=nums[x];
x-=lowbit(x);
- update(int x,int val) 指的是將某個位置的元素加val,實現的簡略代碼如下:
while x<=n:
nums[x]+=val;
x=x+lowbit(x);
對于樹狀數組,具體的實現代碼如下:
public class FenwickTree {
public int[]tree;
public int[]data;
public int size;
//設置構造函數
public FenwickTree(int[] nums) {
int n=nums.length;
data=new int[n+1];
tree=new int[n+1];
this.size=n;
for(int i=1;i<=n;i++) {
data[i]=nums[i-1];
}
for(int i=1;i<=n;i++) {
this.update(i,nums[i-1]);
}
}
public void update(int x,int val) {
int n=this.size;
while(x<=n) {
this.tree[x]+=val;
x+=lowbit(x);
}
}
public int query(int x) {
int res=0;
while(x>0) {
res+=this.tree[x];
x-=lowbit(x);
}
return res;
}
private int lowbit(int num) {
return num&(-num);
}
}