關鍵詞:位運算、前綴和的查詢與更新。
第 1 節 樹狀數組能解決的問題
樹狀數組,也稱作「二叉索引樹」(Binary Indexed Tree)或 Fenwick 樹。
它能高效地實現下面兩個操作:
- 數組的「前綴和」查詢;
- 數組的「單點更新」。
下面具體解釋這兩個操作。
數組的「前綴和」查詢
首先看下面這個例子,瞭解什麼是數組的前綴和查詢。
例 1
已知數組 arr = [10, 15, 17, 19, 20, 14, 12]
,求下標 0
至下標 4
的所有元素的和。
分析:
- 「前綴和」定義了一個數組從「頭」開始的區間,計算的是從下標位置是
0
開始的區間內的所有元素的和; - 注意:理解「前綴」的意思,下標位置必須從
0
開始計算; - 其它不是從
0
開始的數組的區間和可以轉化成前面的這個問題。
解:在 Python 語言中,可以使用 sum(arr[0:5])
得到下標 0
至下標 4
的所有元素的和。
說明:在 Python 的語法中,切片操作不包括結下標的數值,因此 arr[0:5]=[arr[0], arr[1], arr[2], arr[3], arr[4]]
。
數組的「單點更新」
例 2
已知數組
[10, 15, 17, 19, 20, 14, 12]
。
- 將下標爲
4
的元素增加2
;- 將下標爲
6
的元素減少3
。
分析:
- 給出這個例題只是爲了讓大家熟悉這個提法,「單點更新」並不關心這個數「變成了什麼」,它的提法是給出一個數變化了多少,因爲增加一個數等價於減去這個數的相反數,因此以上兩個提法其實可以合併成:將某個下標的元素增加多少;
- 如果我們不使用任何任何數據結構,僅依靠定義,「單點更新」操作的時間複雜度是 ,數組的「前綴和」查詢的時間複雜度是 ,要掃描這個區間的一大部分元素,才能得到這個區間的和。優化的做法是:先計算出一個“前綴和”數組,這個數組的每個元素的值對應的正是原來數組的前綴和。
例 3
已知數組
arr = [1, 2, 3, 4, 5, 6, 7]
,計算「前綴和」數組cumsum(arr)
。
分析:根據「前綴和」的定義,容易計算出前綴和數組是 cumsum(arr) = [1, 3, 6, 10, 15, 21, 28]
。
Python 代碼:
arr = [1, 2, 3, 4, 5, 6, 7]
cumsum = [0] * len(arr)
cumsum[0] = arr[0]
for i in range(1, len(arr)):
cumsum[i] = cumsum[i - 1] + arr[i]
print(cumsum)
有了「前綴和」數組以後,每次查詢「前綴和」的時間複雜度變成了 ,此時計算「區間和」就容易了。
例 4
已知數組
arr = [1, 2, 3, 4, 5, 6, 7]
,求第3
個元素到第7
個元素(包括第7
個元素)的和。
分析:
- 第
3
個元素到第7
個元素(包括第7
個元素)的和可以表示爲sum(arr[2:7])
; - 注意:第幾個元素與下標的序號有一個位置的偏移,並且 Python 中的切片不包含結尾端點);
- 我們假設我們有了「前綴和」數組,就可以以 時間複雜度完成這個問題。
第1
個元素到第7
個元素(包括第7
個元素)的和可以表示成:
cumsum(arr[0:7]) = nums[0] + nums[1] + nums[2] + nums[3] + nums[4] + nums[5] + nums[6]
第 1
個元素到第 2
個元素(包括第 2
個元素)的和可以表示成:
cumsum(arr[0:2]) = nums[0] + nums[1]
於是第 3
個元素到第 7
個元素(包括第 7
個元素)的和:
sum(arr[2:7]) = cumsum(arr[0:7]) - cumsum(arr[0:2])
那麼問題來了:執行「單點更新」操作,就得更新「前綴和」數組又得計算一次前綴和,時間複雜度爲 。那如果一次業務場景中計算「前綴和」和「單點更新操作」的次數都很多,使用「前綴和」數組就不高效了。而 Fenwick 樹就是一個實現了快速計算「前綴和」和「單點更新」操作這兩個操作的數據結構。
(本節完)