樹狀數組(Binary Indexed Tree,BIT)

來源:互聯網
上載者:User

標籤:應該   tps   最優   基礎   問題   方法   代碼實現   bit   節省空間的   

 

 

樹狀數組(Binary Indexed Tree)

 

前面幾篇文章我們分享的都是關於區間求和問題的幾種解決方案,同時也介紹了線段樹這樣的資料結構,我們從中可以體會到合理解決方案帶來的便利,對於大部分區間問題,線段樹都有其絕對的優勢,今天這篇文章,我們就來欣賞由線段樹變形的另外一個資料結構--樹狀數組,樹狀數組通常也用於解決區間求和、單點更新的問題,而且效率比線段樹高一些(樹狀數組區間求和和單點更新的時間複雜度均為o(log n)),相對而言,線段樹的應用範圍可能更廣泛一些。但不得不承認,樹狀數組確實也是一種優雅高效的結構。接下來,我們就一起來揭開它的神秘面紗。

第一點,樹狀數組的結構和線段樹類似,但是比線段樹的節點少,第二點,樹狀數組的每個節點中儲存的也是對應範圍的元素和,同時與線段樹一樣,樹狀數組也是採用數組儲存結構,第三點,樹狀數組與線段樹的下標定義規則不同。如所示,這是線段樹的儲存圖:

 

我們從上篇文章(文章連結:線段樹第二彈(區間更新))中可以發現,線段樹的下標編碼規則是由上而下、從左至右依次編碼,在樹狀數組中,不再採用這樣的編碼方式,具體如何編碼稍後將會解釋,現在我們先來觀察一下線段樹的特徵。

我們假設,此線段樹中每個節點儲存的是相應的區間範圍內所有元素的和。這時我們可以發現,對於每個右孩子節點的值,我們總能通過其父親節點值減去左兄弟節點值來計算,這就意味著,即使沒有右節點,也絲毫不影響我們求解對應區間的區間和,那我們何不節省空間的,但這樣又引發了另外一個問題,在去掉所有的右節點之後,之前的下標編碼方式肯定是用不了了,這時候我們就需要一套新的下標編碼方式,既能節省這部分空間,又不會將原來的問題複雜化,最好能將問題進一步簡化,這就是樹狀數組的產生背景。至於新的一套編碼方式一路走來經曆怎樣的探索過程,不是我們今天要說明的重點,在此我就不作過多的解釋,現在我就直接拋出樹狀數組最後確定版的下標編碼方式,使用這個方式的原因當然是:它簡單啊、好用啊、優雅啊,何以見得?等看完這篇文章,你大概就能體會到它的魅力了。

如所示為樹狀數組的邏輯結構,其中每個節點中儲存的依然是對應區間的元素和:

這是樹狀數組的邏輯結構,其中方塊中的數字表示對應區間的下標範圍,紅色字型表示節點的下標,圖中的藍色粗線條將每個節點和其對應的下標串連(線條這麼粗,大概不會有人看不清楚了吧)。乍一看覺得節點下標的編碼方式似乎有點無理取鬧,同一層級的兩個節點竟然下標不相連,這是什麼邏輯?每當我們感覺走投無路的時候,也就是我們需要重新審視手裡掌握的所有線索的時候,只有不放過任何一個細微的線索,才能找到破解之法。不妨我們就將所有能觀察到的線索一一列出。

上面我們介紹的其實是樹狀數組的邏輯結構,它的實體儲存體就是一個一維的數組。我們將的特徵製表如下:

觀察上表,我們可以得出如下結論:

一、節點下標為 i 時,節點中對應的最後一個元素下標為 i 

二、節點下標對應的位元末尾有 k 個 0 ,節點中對應的元素個數為 2 ^ k 

三、節點中對應的元素下標是連續的

四、樹狀數組的節點個數和原資料元素個數相等

 

以上便是樹狀數組的主要基本特徵,知道了這些特徵之後,我們可以發現,要改變原資料數組中的一個元素值,在樹狀數組中最多需要更改 o(log n)個節點值,因此單點更新的時間複雜度為 o(log n)。單點更新的具體實現怎麼做,在文章末尾會向大家展示,現在先繼續討論接下來的問題。

這時候我們會發現,剛才所列出來的所有特徵,似乎沒什麼用得上的,就像在生活中我們手裡掌握的零碎的知識、技能、人脈等看起來是一片散沙,我們不知道什麼時候才會用得到,甚至窮盡一生也不可能全都用得到,但是一旦有機會用,我們才能真正意識到那些是多麼的重要,其中的聯絡是多麼緊密。與其說學習演算法是在學一門技術,不如說是在學習一門藝術,因為在此期間接觸到的很多方法都可以從生活中找到影子。所以我們暫時不要灰心,繼續研究,也許更深入些,這些瑣碎的特徵就會變得有用。

 

樹狀數組方便處理的其實是“前 i 個元素和”這種問題,

以為例:

前 1 個元素和為

 sum[1] = sum[0001] = tree[1]= tree[0001]

前 2 個元素和為

 sum[2] = sum[0010] = tree[2]=tree[0010]

前 3 個元素和為

 sum[3] = sum[0011] = tree[2]+tree[3]= tree[0011] + tree[0010]

前 4 個元素和為

sum[4] =sum[0100] = tree[4]= tree[0100]

前 5 個元素和為

sum[5] = sum[0101]= tree[4]+tree[5]=tree[0101]+tree[0100]

前 6 個元素和為

sum[6] = sum[0110] = tree[4]+tree[6]= tree[0110] + tree[0100]

前 7 個元素和為

sum[7]=sum[0111]=tree[7]+tree[6]+tree[4]=tree[0111]+tree[0110]+tree[0100]

前 8 個元素和為 

sum[8] = sum[1000] = tree[8]= tree[1000]

紅色部分是下標的二進位表示形式,我覺得到目前為止,我們可能真的是走投無路,才無所不用其極,連下標也不放過。仔細觀察這些下標,似乎還是有一定的規律可循的,現在我們就挑一個運算式最長,能說明問題的來研究一下

sum[7]=sum[0111]=tree[7]+tree[6]+tree[4]=tree[0111]+tree[0110]+tree[0100]

由上述運算式可以發現,前7個元素和 sum[0111] 的加數包括 tree[0111], 在此基礎上,每次將下標從右向左數第一位 1 抹去作為下一個加數的下標, 直到數字變為 0 結束,0111 抹去最後一位 1 得到 0110,0110 抹去最後一位 1 得到 0100,0100 抹去最後一位 1 得到 0000 結束運算。這似乎勉強可以算作一個規律吧,經過驗證發現,以上所有的運算式均符合這個規律。在此我可以告訴大家,經過無數的高手驗證,這個規律確實存在,所以我們可以大膽的使用。

但是,在我們用話語描述的時候,可以說抹掉最後一位 1 ,在實際的實現中,我們就需要用規範的語言來表達,要想達到抹掉最後一位 1 的效果,就需要減去一個數 x ,將 0111 抹掉最後一位 1 得到 0110 時,x = 0001,將0110抹掉最後一位 1 得到 0100 時,x = 0010 ,將 0100 抹掉最後一位 1 時,x = 0100 。也就意味著,原數值需要抹掉哪一位,那麼 x 的哪一位就為 1 ,其餘各位均為 0 。如何求原數值最後一位 1 是哪一位呢?我們可以發現原數值末位有 k 個 0 時,x = 2 ^ k,現在,我們前面列出來的特徵就有聯絡了。目前我們的任務就是求 k 的值,當然對於人來說,一眼看出一個數末尾有幾個0簡直易如反掌,但對於電腦,似乎沒那麼容易,這時候如果將原數值的位元看作整體,似乎不合理,我們需要將其各位分離,這就用到了位元運算。對於當前的問題,有一個求解技巧可以分享給大家,我們都知道在電腦內部兩數的運算用的是補碼實現的,對於正數來說,補碼和原碼形式是一樣的,但對於負數來說,補碼便是將原碼按位取反後在末位加 1 ,這就導致一個負數絕對值的補碼和這個負數的補碼在形式上滿足:以從右向左數第一個非零位為界,在左側,負數絕對值的補碼和負數的補碼各位均不相同,在右側,負數絕對值的補碼和負數的補碼各位均為0,將兩數做 and 運算得到的數字剛好就是我們需要求的 x ,我們知道 負數的絕對值和負數本身互為相反數,同時也說明一個正數的補碼與其相反數的補碼做 and 運算 得到的數字就是 x 。

驗證執行個體如下:

1 &(-1)補碼運算 : 0001 & 1111 = 0001

2 &(-2)補碼運算 : 0010 & 1110 = 0010

3 &(-3)補碼運算 : 0011 & 1101 = 0001

4 &(-4)補碼運算 : 0100 & 1100 = 0100

5 &(-5)補碼運算 : 0101 & 1011 = 0001

6 &(-6)補碼運算 : 0110 & 1010 = 0010

7 &(-7)補碼運算 : 0111 & 1001 = 0001

8 &(-8)補碼運算 : 1000 & 1000 = 1000

由此,我們得到每次的減數 x =i & ( -i )

 

至此,求 前 n 項和的規律已經找到,我們的任務就是將這個規律用規範的語句描述並嘗試著用代碼實現。

求和具體實現描述如下:

假設 當前求前 i 項 之和

 

第一步:判斷 i > 0 是否成立。如果成立進行下一步,否則退出迴圈

第二步:sum = sum + tree[ i ] , x= i &(-i)

第三步:i = i - x ,回到第一步

 

知道了前 i 項和的求解方法之後,要求解區間和相對來說就容易多了,例如求解區間為 i ~ j ,顯然,我們就可以得知 

sum[ i~j ] = sum [ j ] - sum [ i ]  。

 

解決了前 i 項和的問題之後,現在我們再回過頭來分析單點更新的問題,還是剛才的圖:

假設我們現在要修改的元素在原資料中下標為 3 ,那麼在樹狀數組中,我們需要修改的節點下標分別為 3 、4、8

還是按照剛才的分析方法,在原資料中待修改元素下標為 3 = 0011

在樹狀數組中,需要修改的節點下標為 3 = 0011、4 =0100、8 =1000,

觀察發現,0011 +0001 = 0100 ,0100 +0100 = 1000 ,發現規律了嗎?x 值依然是剛才的求法,現在是每次給當前的下標值加 x 就得到下一個加數的下標值,直到下標值大於元素的總個數停止。具體的執行個體不過多贅述,大家可以自己私下驗證。

單點更新的具體描述如下:(假設更新的規則是給 下標為 i 的元素加 y )

假設待更新元素下標為 i  ,元素總個數為 n 

 

第一步:判斷 i <= n 是否成立,成立則進行下一步,不成立結束迴圈

第二步:tree[ i ] = tree[ i ] + y , x = i & ( -i ) ,進行下一步

第三步:i = i + x ,跳回第一步

 

 

以上就是今天內容的理論部分,下面為大家奉上核心代碼實現部分,希望今天的分享能讓大家有收穫。代碼如下:

除了並查集,這應該是見過的最精簡最優雅最高效的代碼了。

 

還沒有關注公眾號的朋友可以長按識別圖中二維碼關注我。

 

 

老規矩,開啟網頁http://paste.ubuntu.com/25548013/查看網頁版代碼。

 

樹狀數組(Binary Indexed Tree,BIT)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.