一步一步理解線段樹

目錄php

概述html

、從一個例子理解線段樹數組

  建立線段樹數據結構

  線段樹區間查詢函數

  單節點更新post

  區間更新字體

、線段樹實戰ui

--------------------------spa

一 概述.net

線段樹,相似區間樹,它在各個節點保存一條線段(數組中的一段子數組),主要用於高效解決連續區間的動態查詢問題,因爲二叉結構的特性,它基本能保持每一個操做的複雜度爲O(logn)。

線段樹的每一個節點表示一個區間,子節點則分別表示父節點的左右半區間,例如父親的區間是[a,b],那麼(c=(a+b)/2)左兒子的區間是[a,c],右兒子的區間是[c+1,b]。

二 從一個例子理解線段樹

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

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

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

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

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

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

因爲線段樹的父節點區間是平均分割到左右子樹,所以線段樹是徹底二叉樹,對於包含n個葉子節點的徹底二叉樹,它必定有n-1個非葉節點,總共2n-1個節點,所以存儲線段是須要的空間複雜度是O(n)。那麼線段樹的操做:建立線段樹、查詢、節點更新 是如何運做的呢(如下全部代碼都是針對求區間最小值問題)?

2.1 建立線段樹

對於線段樹咱們能夠選擇和普通二叉樹同樣的鏈式結構。因爲線段樹是徹底二叉樹,咱們也能夠用數組來存儲,下面的討論及代碼都是數組來存儲線段樹,節點結構以下(注意到用數組存儲時,有效空間爲2n-1,實際空間確不止這麼多,好比上面的線段樹中葉子節點一、3雖然沒有左右子樹,可是的確佔用了數組空間,實際空間是滿二叉樹的節點數目: 2 * 2 ^{\lceil \log_2{n}  \rceil} - 1 \lceil \log_2{n}  \rceil 是樹的高度,可是這個空間複雜度也是O(n)的 )。

struct SegTreeNode

{

  int val;

};

定義包含n個節點的線段樹 SegTreeNode segTree[n],segTree[0]表示根節點。那麼對於節點segTree[i],它的左孩子是segTree[2*i+1],右孩子是segTree[2*i+2]。

咱們能夠從根節點開始,平分區間,遞歸的建立線段樹,線段樹的建立函數以下:

 1 const int MAXNUM = 1000;
 2 struct SegTreeNode
 3 {
 4     int val;
 5 }segTree[MAXNUM];//定義線段樹
 6 
 7 /*
 8 功能:構建線段樹
 9 root:當前線段樹的根節點下標
10 arr: 用來構造線段樹的數組
11 istart:數組的起始位置
12 iend:數組的結束位置
13 */
14 void build(int root, int arr[], int istart, int iend)
15 {
16     if(istart == iend)//葉子節點
17         segTree[root].val = arr[istart];
18     else
19     {
20         int mid = (istart + iend) / 2;
21         build(root*2+1, arr, istart, mid);//遞歸構造左子樹
22         build(root*2+2, arr, mid+1, iend);//遞歸構造右子樹
23         //根據左右子樹根節點的值,更新當前根節點的值
24         segTree[root].val = min(segTree[root*2+1].val, segTree[root*2+2].val);
25     }
26 }

2.2 查詢線段樹

已經構建好了線段樹,那麼怎樣在它上面超找某個區間的最小值呢?查詢的思想是選出一些區間,使他們相連後剛好涵蓋整個查詢區間,所以線段樹適合解決「相鄰的區間的信息能夠被合併成兩個區間的並區間的信息」的問題。代碼以下,具體見代碼解釋

 1 /*
 2 功能:線段樹的區間查詢
 3 root:當前線段樹的根節點下標
 4 [nstart, nend]: 當前節點所表示的區間
 5 [qstart, qend]: 這次查詢的區間
 6 */
 7 int query(int root, int nstart, int nend, int qstart, int qend)
 8 {
 9     //查詢區間和當前節點區間沒有交集
10     if(qstart > nend || qend < nstart)
11         return INFINITE;
12     //當前節點區間包含在查詢區間內
13     if(qstart <= nstart && qend >= nend)
14         return segTree[root].val;
15     //分別從左右子樹查詢,返回二者查詢結果的較小值
16     int mid = (nstart + nend) / 2;
17     return min(query(root*2+1, nstart, mid, qstart, qend),
18                query(root*2+2, mid + 1, nend, qstart, qend));
19 
20 }

舉例說明(對照上面的二叉樹):

一、當咱們要查詢區間[0,2]的最小值時,從根節點開始,要分別查詢左右子樹,查詢左子樹時節點區間[0,2]包含在查詢區間[0,2]內,返回當前節點的值1,查詢右子樹時,節點區間[3,5]和查詢區間[0,2]沒有交集,返回正無窮INFINITE,查詢結果取兩子樹查詢結果的較小值1,所以結果是1.

二、查詢區間[0,3]時,從根節點開始,查詢左子樹的節點區間[0,2]包含在區間[0,3]內,返回當前節點的值1;查詢右子樹時,繼續遞歸查詢右子樹的左右子樹,查詢到非葉節點4時,又要繼續遞歸查詢:葉子節點4的節點區間[3,3]包含在查詢區間[0,3]內,返回4,葉子節點9的節點區間[4,4]和[0,3]沒有交集,返回INFINITE,所以非葉節點4返回的是min(4, INFINITE) = 4,葉子節點3的節點區間[5,5]和[0,3]沒有交集,返回INFINITE,所以非葉節點3返回min(4, INFINITE) = 4, 所以根節點返回 min(1,4) = 1。

2.3單節點更新

單節點更新是指只更新線段樹的某個葉子節點的值,可是更新葉子節點會對其父節點的值產生影響,所以更新子節點後,要回溯更新其父節點的值。

 1 /*
 2 功能:更新線段樹中某個葉子節點的值
 3 root:當前線段樹的根節點下標
 4 [nstart, nend]: 當前節點所表示的區間
 5 index: 待更新節點在原始數組arr中的下標
 6 addVal: 更新的值(原來的值加上addVal)
 7 */
 8 void updateOne(int root, int nstart, int nend, int index, int addVal)
 9 {
10     if(nstart == nend)
11     {
12         if(index == nstart)//找到了相應的節點,更新之
13             segTree[root].val += addVal;
14         return;
15     }
16     int mid = (nstart + nend) / 2;
17     if(index <= mid)//在左子樹中更新
18         updateOne(root*2+1, nstart, mid, index, addVal);
19     else updateOne(root*2+2, mid+1, nend, index, addVal);//在右子樹中更新
20     //根據左右子樹的值回溯更新當前節點的值
21     segTree[root].val = min(segTree[root*2+1].val, segTree[root*2+2].val);
22 }

好比咱們要更新葉子節點4(addVal = 6),更新後值變爲10,那麼其父節點的值從4變爲9,非葉結點3的值更新後不變,根節點更新後也不變。

2.4 區間更新

區間更新是指更新某個區間內的葉子節點的值,由於涉及到的葉子節點不止一個,而葉子節點會影響其相應的非葉父節點,那麼回溯須要更新的非葉子節點也會有不少,若是一次性更新完,操做的時間複雜度確定不是O(lgn),例如當咱們要更新區間[0,3]內的葉子節點時,須要更新出了葉子節點3,9外的全部其餘節點。爲此引入了線段樹中的延遲標記概念,這也是線段樹的精華所在。

延遲標記:每一個節點新增長一個標記,記錄這個節點是否進行了某種修改(這種修改操做會影響其子節點),對於任意區間的修改,咱們先按照區間查詢的方式將其劃分紅線段樹中的節點,而後修改這些節點的信息,並給這些節點標記上表明這種修改操做的標記。在修改和查詢的時候,若是咱們到了一個節點p,而且決定考慮其子節點,那麼咱們就要看節點p是否被標記,若是有,就要按照標記修改其子節點的信息,而且給子節點都標上相同的標記,同時消掉節點p的標記。

所以須要在線段樹結構中加入延遲標記域,本文例子中咱們加入標記與addMark,表示節點的子孫節點在原來的值的基礎上加上addMark的值,同時還須要修改建立函數build 和 查詢函數 query,修改的代碼用紅色字體表示,其中區間更新的函數爲update,代碼以下:

  1 const int INFINITE = INT_MAX;
  2 const int MAXNUM = 1000;
  3 struct SegTreeNode
  4 {
  5     int val;
  6     int addMark;//延遲標記
  7 }segTree[MAXNUM];//定義線段樹
  8 
  9 /*
 10 功能:構建線段樹
 11 root:當前線段樹的根節點下標
 12 arr: 用來構造線段樹的數組
 13 istart:數組的起始位置
 14 iend:數組的結束位置
 15 */
 16 void build(int root, int arr[], int istart, int iend)
 17 {
 18     segTree[root].addMark = 0;//----設置標延遲記域
 19     if(istart == iend)//葉子節點
 20         segTree[root].val = arr[istart];
 21     else
 22     {
 23         int mid = (istart + iend) / 2;
 24         build(root*2+1, arr, istart, mid);//遞歸構造左子樹
 25         build(root*2+2, arr, mid+1, iend);//遞歸構造右子樹
 26         //根據左右子樹根節點的值,更新當前根節點的值
 27         segTree[root].val = min(segTree[root*2+1].val, segTree[root*2+2].val);
 28     }
 29 }
 30 
 31 /*
 32 功能:當前節點的標誌域向孩子節點傳遞
 33 root: 當前線段樹的根節點下標
 34 */
 35 void pushDown(int root)
 36 {
 37     if(segTree[root].addMark != 0)
 38     {
 39         //設置左右孩子節點的標誌域,由於孩子節點可能被屢次延遲標記又沒有向下傳遞
 40         //因此是 「+=」
 41         segTree[root*2+1].addMark += segTree[root].addMark;
 42         segTree[root*2+2].addMark += segTree[root].addMark;
 43         //根據標誌域設置孩子節點的值。由於咱們是求區間最小值,所以當區間內每一個元
 44         //素加上一個值時,區間的最小值也加上這個值
 45         segTree[root*2+1].val += segTree[root].addMark;
 46         segTree[root*2+2].val += segTree[root].addMark;
 47         //傳遞後,當前節點標記域清空
 48         segTree[root].addMark = 0;
 49     }
 50 }
 51 
 52 /*
 53 功能:線段樹的區間查詢
 54 root:當前線段樹的根節點下標
 55 [nstart, nend]: 當前節點所表示的區間
 56 [qstart, qend]: 這次查詢的區間
 57 */
 58 int query(int root, int nstart, int nend, int qstart, int qend)
 59 {
 60     //查詢區間和當前節點區間沒有交集
 61     if(qstart > nend || qend < nstart)
 62         return INFINITE;
 63     //當前節點區間包含在查詢區間內
 64     if(qstart <= nstart && qend >= nend)
 65         return segTree[root].val;
 66     //分別從左右子樹查詢,返回二者查詢結果的較小值
 67     pushDown(root); //----延遲標誌域向下傳遞
 68     int mid = (nstart + nend) / 2;
 69     return min(query(root*2+1, nstart, mid, qstart, qend),
 70                query(root*2+2, mid + 1, nend, qstart, qend));
 71 
 72 }
 73 
 74 /*
 75 功能:更新線段樹中某個區間內葉子節點的值
 76 root:當前線段樹的根節點下標
 77 [nstart, nend]: 當前節點所表示的區間
 78 [ustart, uend]: 待更新的區間
 79 addVal: 更新的值(原來的值加上addVal)
 80 */
 81 void update(int root, int nstart, int nend, int ustart, int uend, int addVal)
 82 {
 83     //更新區間和當前節點區間沒有交集
 84     if(ustart > nend || uend < nstart)
 85         return ;
 86     //當前節點區間包含在更新區間內
 87     if(ustart <= nstart && uend >= nend)
 88     {
 89         segTree[root].addMark += addVal;
 90         segTree[root].val += addVal;
 91         return ;
 92     }
 93     pushDown(root); //延遲標記向下傳遞
 94     //更新左右孩子節點
 95     int mid = (nstart + nend) / 2;
 96     update(root*2+1, nstart, mid, ustart, uend, addVal);
 97     update(root*2+2, mid+1, nend, ustart, uend, addVal);
 98     //根據左右子樹的值回溯更新當前節點的值
 99     segTree[root].val = min(segTree[root*2+1].val, segTree[root*2+2].val);
100 }

區間更新舉例說明:當咱們要對區間[0,2]的葉子節點增長2,利用區間查詢的方法從根節點開始找到了非葉子節點[0-2],把它的值設置爲1+2 = 3,而且把它的延遲標記設置爲2,更新完畢;當咱們要查詢區間[0,1]內的最小值時,查找到區間[0,2]時,發現它的標記不爲0,而且還要向下搜索,所以要把標記向下傳遞,把節點[0-1]的值設置爲2+2 = 4,標記設置爲2,節點[2-2]的值設置爲1+2 = 3,標記設置爲2(其實葉子節點的標誌是不起做用的,這裏是爲了操做的一致性),而後返回查詢結果:[0-1]節點的值4;當咱們再次更新區間[0,1](增長3)時,查詢到節點[0-1],發現它的標記值爲2,所以把它的標記值設置爲2+3 = 5,節點的值設置爲4+3 = 7;

其實當區間更新的區間左右值相等時([i,i]),就至關於單節點更新,單節點更新只是區間更新的特例。

三 線段樹實戰

 求區間的最大值、區間求和等問題都是採用相似上面的延遲標記域。下面會經過acm的一些題目來運用一下線段樹。

 

等待更新......

參考資料

GeeksforGeeks: http://www.geeksforgeeks.org/segment-tree-set-1-range-minimum-query/

GeeksforGeeks: http://www.geeksforgeeks.org/segment-tree-set-1-sum-of-given-range/

懂得博客[數據結構之線段樹]:http://dongxicheng.org/structure/segment-tree/

MetaSeed[數據結構專題—線段樹]: http://blog.csdn.net/metalseed/article/details/8039326

NotOnlySuccess[徹底版 線段樹]: http://www.notonlysuccess.com/index.php/segment-tree-complete/

【版權聲明】轉載請註明出處:http://www.cnblogs.com/TenosDoIt/p/3453089.html

相關文章
相關標籤/搜索