有一種數據結構是神奇的,神秘的,它展現了位運算與數組結合的神奇魅力,太牛逼的,它就是樹狀數組,這種數據結構不是神人是發現不了的。
一:概序
假如我現在有個需求,就是要頻繁的求數組的前n項和,並且存在着數組中某些數字的頻繁修改,那么我們該如何實現這樣的需求?當然大家可以往
真實項目上靠一靠。
① 傳統方法:根據索引修改為O(1),但是求前n項和為O(n)。
②空間換時間方法:我開一個數組sum[],sum[i]=a[1]+....+a[i],那么有點意思,求n項和為O(1),但是修改卻成了O(N),這是因為我的Sum[i]中牽
涉的數據太多了,那么問題來了,我能不能在相應的sum[i]中只保存某些a[i]的值呢?好吧,下面我們看張圖。
從圖中我們可以看到S[]的分布變成了一顆樹,有意思吧,下面我們看看S[i]中到底存放着哪些a[i]的值。
S[1]=a[1];
S[2]=a[1]+a[2];
S[3]=a[3];
S[4]=a[1]+a[2]+a[3]+a[4];
S[5]=a[5];
S[6]=a[5]+a[6];
S[7]=a[7];
S[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8];
之所以采用這樣的分布方式,是因為我們使用的是這樣的一個公式:S[i]=a[i-2k+1]+....+a[i]。
其中:2k 中的k表示當前S[i]在樹中的層數,它的值就是i的二進制中末尾連續0的個數,2k也就是表示S[i]中包含了哪些a[],
舉個例子: i=610=01102 ;可以發現末尾連續的0有一個,即k=1,則說明S[6]是在樹中的第二層,並且S[6]中有21項,隨后我們求出了起始項:
a[6-21+1]=a[5],但是在編碼中求出k的值還是有點麻煩的,所以我們采用更靈巧的Lowbit技術,即:2k=i&-i 。
則:S[6]=a[6-21+1]=a[6-(6&-6)+1]=a[5]+a[6]。
二:代碼
1:神奇的Lowbit函數
1 #region 當前的sum數列的起始下標 2 /// <summary> 3 /// 當前的sum數列的起始下標 4 /// </summary> 5 /// <param name="i"></param> 6 /// <returns></returns> 7 public static int Lowbit(int i) 8 { 9 return i & -i; 10 } 11 #endregion
2:求前n項和
比如上圖中,如何求Sum(6),很顯然Sum(6)=S4+S6,那么如何尋找S4呢?即找到6以前的所有最大子樹,很顯然這個求和的復雜度為logN。
1 #region 求前n項和 2 /// <summary> 3 /// 求前n項和 4 /// </summary> 5 /// <param name="x"></param> 6 /// <returns></returns> 7 public static int Sum(int x) 8 { 9 int ans = 0; 10 11 var i = x; 12 13 while (i > 0) 14 { 15 ans += sumArray[i - 1]; 16 17 //當前項的最大子樹 18 i -= Lowbit(i); 19 } 20 21 return ans; 22 } 23 #endregion
3:修改
如上圖中,如果我修改了a[5]的值,那么包含a[5]的S[5],S[6],S[8]的區間值都需要同步修改,我們看到只要沿着S[5]一直回溯到根即可,
同樣它的時間復雜度也為logN。
1 public static void Modify(int x, int newValue) 2 { 3 //拿出原數組的值 4 var oldValue = arr[x]; 5 6 for (int i = x; i < arr.Length; i += Lowbit(i + 1)) 7 { 8 //減去老值,換一個新值 9 sumArray[i] = sumArray[i] - oldValue + newValue; 10 } 11 }
最后上總的代碼:

1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Diagnostics; 6 using System.Threading; 7 using System.IO; 8 9 namespace ConsoleApplication2 10 { 11 public class Program 12 { 13 static int[] sumArray = new int[8]; 14 15 static int[] arr = new int[8]; 16 17 public static void Main() 18 { 19 Init(); 20 21 Console.WriteLine("A數組的值:{0}", string.Join(",", arr)); 22 Console.WriteLine("S數組的值:{0}", string.Join(",", sumArray)); 23 24 Console.WriteLine("修改A[1]的值為3"); 25 Modify(1, 3); 26 27 Console.WriteLine("A數組的值:{0}", string.Join(",", arr)); 28 Console.WriteLine("S數組的值:{0}", string.Join(",", sumArray)); 29 30 Console.Read(); 31 } 32 33 #region 初始化兩個數組 34 /// <summary> 35 /// 初始化兩個數組 36 /// </summary> 37 public static void Init() 38 { 39 for (int i = 1; i <= 8; i++) 40 { 41 arr[i - 1] = i; 42 43 //設置其實坐標:i=1開始 44 int start = (i - Lowbit(i)); 45 46 var sum = 0; 47 48 while (start < i) 49 { 50 sum += arr[start]; 51 52 start++; 53 } 54 55 sumArray[i - 1] = sum; 56 } 57 } 58 #endregion 59 60 public static void Modify(int x, int newValue) 61 { 62 //拿出原數組的值 63 var oldValue = arr[x]; 64 65 arr[x] = newValue; 66 67 for (int i = x; i < arr.Length; i += Lowbit(i + 1)) 68 { 69 //減去老值,換一個新值 70 sumArray[i] = sumArray[i] - oldValue + newValue; 71 } 72 } 73 74 #region 求前n項和 75 /// <summary> 76 /// 求前n項和 77 /// </summary> 78 /// <param name="x"></param> 79 /// <returns></returns> 80 public static int Sum(int x) 81 { 82 int ans = 0; 83 84 var i = x; 85 86 while (i > 0) 87 { 88 ans += sumArray[i - 1]; 89 90 //當前項的最大子樹 91 i -= Lowbit(i); 92 } 93 94 return ans; 95 } 96 #endregion 97 98 #region 當前的sum數列的起始下標 99 /// <summary> 100 /// 當前的sum數列的起始下標 101 /// </summary> 102 /// <param name="i"></param> 103 /// <returns></returns> 104 public static int Lowbit(int i) 105 { 106 return i & -i; 107 } 108 #endregion 109 } 110 }