Segment Tree 線段樹總結

2021-07-23 11:12:43 字數 3641 閱讀 2222

準確的來說,線段樹是一種平衡二叉樹,當我們的要換分的區間大小是2的冪的話,剛好我們的線段樹就是一顆滿二叉樹,但是如果不是的話,我們最多只能叫他是平衡二叉樹

正因為是一顆平衡二叉樹,所以說,線段樹的劃分是均勻的,樹高也穩定在logn上(這也就是我們的優化的源泉)

上面只是個人的一點小理解,下面我們步入正題

線段樹segment tree,是bst二叉搜尋樹而一種應用,是一種應用廣泛的高階資料結構

主要用來查詢區間的覆蓋問題等

定義:線段樹並不是如字面意思上來的說,我們每乙個樹中結點都保留一串資料,那樣的話我們的空間複雜度就會變得難以想象了,在這裡我們資料只有乙份,但是我們的線段樹中的節點儲存的是區間的範圍

另外我們還會儲存一些其他的資料域,這些都是看具體的題目的要求了

作為一種資料結構,我們對線段樹有以下幾個基本操作的描述

1.建樹

2.插入

3.修改

4.查詢

5.先上維護資料域

6.向下繼承lazy-tag(這個我們之後會講解)

1.鏈式

我們一開始首先可以聯想到線段樹的鏈式儲存方式,鏈式儲存的優勢在於我們可以動態分配記憶體,從而我們能夠最大限度的利用的我們的空間,不會出現我們無法確定線段戶的記憶體的大小的問題(之後會講解為什麼至少需要4倍記憶體空間)

但是對於鏈式儲存來說,我們還需要額外開闢記憶體來儲存指標變數,但是在數量很大並且捉摸不透的時候,採用鏈式儲存是乙個明智的選擇

2.陣列

在這裡,我們的陣列的儲存方式是有些類似於二叉堆的儲存的方式結構的

因為線段樹和堆一樣是一顆平衡二叉樹,所以說對於節點的標號為n的話,n*2必定代表的是左兒子,n*2+1必定代表的是右兒子

這樣的話,相對於鏈式儲存我們可以節約出來兩個指標域的記憶體空間,但是這樣的話,我們就需要開闢至少4倍的記憶體空間的大小

在本文中,為了方便敘述,我們採用的是陣列的形式

資料

typedef struct node

point;

空間複雜度是o(4*n):

首先我們需要認識到

線段樹作為一種平衡二叉樹,當我們的最終的區間長度是n的時候,我們的線段樹最少需要2*n-1的節點記憶體

但是,因為上述情況只有在我們的n是2的冪的時候才成立,但是當n不是2的冪的時候,我們因為在儲存的時候,不是滿二叉樹

所以說實際上會存在記憶體的浪費,極限的情況下會需要遠遠比2*n-1大的記憶體

這時候,我們進行粗略的估計操作,我們找到比n大的最小的2的冪k,大致需要2*k-1的記憶體大小

但是在最糟糕的情況下,k的值可能幾乎就是n的2倍

所以說,大致上我們開闢4*n的記憶體就完全夠我們 的線段樹的儲存需要了 

建樹操作,相當於我們前序遍歷二叉樹

我們對當前的節點進行初始化,然後依次的向下遞迴至我們的葉子節點

遞迴的終止是在於我們遞迴到的節點的區間為1,代表我們遞迴到了葉子節點,直接返回初始化並且更新就可以返回了

大致**如下:

//建樹操作    o(n)

void build(int left,int right,int pox) //pox代表的是線段樹種的節點儲存的物理順序位址 ,初始的時候是1

//遞迴操作,位運算加速

build(left,(left+right)/2,pox<<1);

build((left+right)/2+1,right,(pox<<1)+1);

}

時間複雜度是o(n),因為我們需要建立所有的葉子節點,故是o(n)的時間複雜度

在這裡,我們需要引入乙個叫做完全覆蓋的概念,這也是我們線段樹優化的核心所在

如果我們將樹中結點的資訊都完全的保留在當前的樹中結點的話,我們就完全沒必要一直訪問到葉子節點,我們找到完全福該節點的話,就可以直接的獲取我們的需要的值然後退出就好了

//查詢區間的資料域   o(logn)

int find(int left,int right,int pox)

pox+=1;

if(tree[pox].left<=right)

return sum;

}

因為查詢的深度就是我們的線段樹的額深度,所以說我們的時間複雜度就是 o(logn)

之後的lazy-tag等問題就要牽扯到區間更新了

如果是區間更新的話,我們的樸素做法無非就是將區間整個的遍歷一遍然後統一進行修改操作,時間複雜度是o(n)

但是對於線段樹來說,如果我們每次都是一直遞迴到所有的葉子結點的話,我們也會發現,我們需要將所有的葉子結點

都要修改覆蓋,時間複雜度也是o(n),這樣我們就並沒有實現複雜度上的優化,在這裡我們就要引入lazy變數

可以就其本質上來說的話,父節點的區間是完全的包含我們的子節點的區間的,所以說,我們就只用父節點區間來代表子節點區間就好了

在這裡,我們的引入的lazy變數就是這樣的,我們將更新操作進行到完全覆蓋的樹中的節點的時候我們就退出,不對之後的葉子結點進行操作

但是我會對該節點是加乙個lazy標記,代表我們的更新只進行到這裡,該節點之後的子節點並沒有執行更新操作

只有我們再次進行訪問該節點額子節點的區間的時候,我們才會往下繼續執行我們的更新操作,這樣的話,通過完全覆蓋的父區間,我們可以減少操作

的複雜度,降低到 o(logn),也就是最多我們也就執行到樹的深度為止 

所以說,我們的lazy-tag是非常有必要的

為了直白額表示我們上述的過程,附上一段**

void pushdown_lazy(int pox)    //傳遞lazy標記,並更新子區間的val,注意我們的pox節點已經將val域更新了 

} void pushup_val(int pox) //資料域的向上更新函式,一會在update_segment函式我再解釋該函式以及上乙個函式的額具體作用

void update_segment(int left,int right,int pox,int k) //k代表區間更新的值,left和right代表的是當前需要更新的區間的範圍,pox代表當前的樹中節點的物理下標

if(tree[pox].left==tree[pox].right) return ; //更新到了葉子結點,進行遞迴終止

//以下**的執行的原因是還沒有找到完全覆蓋的樹中節點,所以繼續向下更新樹

pushdown_lazy(pox); //沒有找到完全覆蓋的節點,向下查詢完全覆蓋的節點,並將我們的lazy標記傳遞下去,但是這裡我們需要注意,pox節點的val域沒有更新維護,還是空的

if(right<=tree[pox<<1].right) update_segment(left,right,pox<<1,k);

else if(left>=tree[(pox<<1)+1].left) update_segment(left,right,(pox<<1)+1,k);

else

pushup_val(pox); //上面已經說了,pox節點並沒有更新維護,所以說,我們在回溯的時候必須要將我們的資料域更新,一直返還到我們的你根結點處,但是我們只要更新了,就必須要回溯的時候,對資料域必須要進行同步的更新

//當然該函式也可以封裝在pushdown函式裡面,這都無所謂了

}

上面一段的**的作用是將一段子區間同時加上乙個數的操作,主要的解釋都已經注釋了

線段樹介紹(segment tree)

給定乙個區間 1,n 希望你實現一種資料結構,支援以下操作 1.修改 i 號節點的值。2.詢問區間 i,j 中所有節點的和。這不是樹狀陣列板子 3.修改區間 i,j 中所有節點的值 4.詢問 i 號節點的值 這不還是樹狀陣列板子 如果我要求乙個資料結構,同時滿足這四個要求。那樹狀陣列就不行了。樹狀陣...

線段樹 02 構建線段樹

public inte ce merger 不能再縮小的基本問題是 對treeindex指向的節點的情況進行討論 public class segmenttree 在treeindex的位置建立表示區間 l.r 的線段樹 private void buildsegmenttree int treei...

線段樹 01 線段樹基礎

物理上 public class segmenttree public int getsize public e get int index 返回完全二叉樹的陣列表示中,乙個索引所表示的元素的左孩子節點的索引 private int leftchild int index 返回完全二叉樹的陣列表示中...