最近在學習位運算,正好把樹狀數組總結下,也算是能正式給data structure
建個分類。算法
那麼,樹狀數組到底有什麼用呢?誠然,同樣沒什麼卵用的東西咱們學它幹嗎。數組
下面舉個樹狀數組的經典應用:區間求和。數據結構
假設咱們有以下數組(數組元素從 index=1
開始):函數
var a = [X, 1, 2, 3, 4, 5, 6, 7, 8, 9];
咱們設定兩種操做,modify(index, x)
表示將 a[index]
元素加上x, query(n, m)
表示求解 a[n] ~ a[m]
之間元素的和。若是不瞭解樹狀數組(固然假設更不瞭解線段樹等其餘數據結構),你可能會很容易地寫下以下代碼:學習
var a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; function query(n, m) { var sum = 0; for (var i = n; i <= m; i++) sum += a[i]; return sum; } function modify(index, x) { a[index] += x; }
Ok,複雜度爲O(1)的刪改和複雜度爲O(n)的查詢。若是數據量很大,這樣反覆的查詢是至關耗時的。咱們退一步想,若是隻有 query(n, m)
這個操做,很容易想到用sum數組預處理前n項的和,而後用 sum[m] - sum[n-1]
得到答案。可是若是要修改 a[index]
的值,由於該項影響全部index以後的sum數組元素,因此若是這樣作複雜度變爲O(1)的查詢和O(n)的刪改,並無什麼卵用。code
可是這個思路是美好的,咱們能夠用一個sum數組保存一段特定的區間段的值。假設咱們有 a[1] ~ a[9]
9個元素,咱們根據一個特定的規則:blog
sum[1] = a[1]; sum[2] = a[1] + a[2]; sum[3] = a[3]; sum[4] = a[1] + a[2] + a[3] + a[4]; sum[5] = a[5]; sum[6] = a[5] + a[6]; sum[7] = a[7]; sum[8] = a[1] + a[2] + a[3] + a[4] + a[5] + a[6] + a[7] + a[8]; sum[9] = a[9];
若是要求 a[1] ~ a[9]
的和,即爲 sum[9] + sum[8]
,若是要求 a[1] ~ a[7]
的和,即爲 sum[7] + sum[6] + sum[4]
,若是要改變 a[1]
的值,改變sum數組中和 a[1]
有關的項便可(即 sum[1]
sum[2]
sum[4]
sum[8]
)。 這就是樹狀數組!實現了O(logn)的查詢和刪改。可是如何將a數組和sum數組聯繫起來?it
來觀察這個圖:
io
令這棵樹的結點編號爲C1,C2...Cn。令每一個結點的值爲這棵樹的值的總和,那麼容易發現(如上所說):function
C1 = A1 C2 = A1 + A2 C3 = A3 C4 = A1 + A2 + A3 + A4 C5 = A5 C6 = A5 + A6 C7 = A7 C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8
這裏有一個有趣的性質:設節點編號爲x,那麼這個節點管轄的區間爲 2^k
(其中k爲x二進制末尾0的個數)個元素。由於這個區間最後一個元素必然爲Ax,因此很明顯:Cn = A(n – 2^k + 1) + ... + An,算這個2^k有一個快捷的辦法,定義一個函數以下便可(求解2^k即求二進制碼右邊第一位1的值):
int lowbit(int x) { return x & (-x); }
當想要查詢一個SUM(n)(求a[1]~a[n]的和),能夠依據以下算法便可:
能夠看出,這個算法就是將這一個個區間的和所有加起來。
那麼修改呢,修改一個節點,必須修改其全部祖先,最壞狀況下爲修改第一個元素,最多有log(n)的祖先。因此修改算法以下(給某個結點i加上x):
關於這部分的代碼,將在下文樹狀數組的具體三大應用中給出。
關於樹狀數組,有一點須要注意,爲了方便,樹狀數組的a數組基本都是從 index=1
開始的。
下文中樓主會分析下樹狀數組的三大應用場景:改點求段,改段求點,改段求段。