這一篇我們來看樹狀數組的加強版線段樹,樹狀數組能玩的線段樹一樣可以玩,而且能玩的更好,他們在區間求和,最大,平均
等經典的RMQ問題上有着對數時間的優越表現。
一:線段樹
線段樹又稱"區間樹”,在每個節點上保存一個區間,當然區間的划分采用折半的思想,葉子節點只保存一個值,也叫單元節點,所
以最終的構造就是一個平衡的二叉樹,擁有CURD的O(lgN)的時間。

從圖中我們可以清楚的看到[0-10]被划分成線段的在樹中的分布情況,針對區間[0-N],最多有2N個節點,由於是平衡二叉樹的形
式也可以像堆那樣用數組來玩,不過更加耗費空間,為最多4N個節點,在針對RMQ的問題上,我們常常在每個節點上增加一些sum,
max,min等變量來記錄求得的累加值,當然你可以理解成動態規划的思想,由於擁有logN的時間,所以在RMQ問題上比數組更加優美。
二:代碼
1:在節點中定義一些附加值,方便我們處理RMQ問題。
1 #region 線段樹的節點
2 /// <summary>
3 /// 線段樹的節點
4 /// </summary>
5 public class Node
6 {
7 /// <summary>
8 /// 區間左端點
9 /// </summary>
10 public int left;
11
12 /// <summary>
13 /// 區間右端點
14 /// </summary>
15 public int right;
16
17 /// <summary>
18 /// 左孩子
19 /// </summary>
20 public Node leftchild;
21
22 /// <summary>
23 /// 右孩子
24 /// </summary>
25 public Node rightchild;
26
27 /// <summary>
28 /// 節點的sum值
29 /// </summary>
30 public int Sum;
31
32 /// <summary>
33 /// 節點的Min值
34 /// </summary>
35 public int Min;
36
37 /// <summary>
38 /// 節點的Max值
39 /// </summary>
40 public int Max;
41 }
42 #endregion
2:構建(Build)
前面我也說了,構建有兩種方法,數組的形式或者鏈的形式,各有特點,我就采用后者,時間為O(N)。
1 #region 根據數組構建“線段樹"
2 /// <summary>
3 /// 根據數組構建“線段樹"
4 /// </summary>
5 /// <param name="length"></param>
6 public Node Build(int[] nums)
7 {
8 this.nums = nums;
9
10 return Build(nodeTree, 0, nums.Length - 1);
11 }
12 #endregion
13
14 #region 根據數組構建“線段樹"
15 /// <summary>
16 /// 根據數組構建“線段樹"
17 /// </summary>
18 /// <param name="left"></param>
19 /// <param name="right"></param>
20 public Node Build(Node node, int left, int right)
21 {
22 //說明已經到根了,當前當前節點的max,sum,min值(回溯時統計上一層節點區間的值)
23 if (left == right)
24 {
25 return new Node
26 {
27 left = left,
28 right = right,
29 Max = nums[left],
30 Min = nums[left],
31 Sum = nums[left]
32 };
33 }
34
35 if (node == null)
36 node = new Node();
37
38 node.left = left;
39
40 node.right = right;
41
42 node.leftchild = Build(node.leftchild, left, (left + right) / 2);
43
44 node.rightchild = Build(node.rightchild, (left + right) / 2 + 1, right);
45
46 //統計左右子樹的值(min,max,sum)
47 node.Min = Math.Min(node.leftchild.Min, node.rightchild.Min);
48 node.Max = Math.Max(node.leftchild.Max, node.rightchild.Max);
49 node.Sum = node.leftchild.Sum + node.rightchild.Sum;
50
51 return node;
52 }
53 #endregion
3:區間查詢
在線段樹中,區間查詢還是有點小麻煩的,存在三種情況。
① 完全包含:也就是節點的線段范圍完全在查詢區間的范圍內,這說明我們要么到了“單元節點",要么到了一個子區間,這種情況
就是我找到了查詢區間的某一個子區間,直接累積該區間值就可以了。
② 左交集: 這種情況我們需要到左子樹去遍歷。
③右交集: 這種情況我們需要到右子樹去遍歷。
比如說:我要查詢Sum[4-8]的值,最終會成為:Sum總=Sum[4-4]+Sum[5-5]+Sum[6-8],時間為log(N)。
1 #region 區間查詢
2 /// <summary>
3 /// 區間查詢(分解)
4 /// </summary>
5 /// <returns></returns>
6 public int Query(int left, int right)
7 {
8 int sum = 0;
9
10 Query(nodeTree, left, right, ref sum);
11
12 return sum;
13 }
14
15 /// <summary>
16 /// 區間查詢
17 /// </summary>
18 /// <param name="left">查詢左邊界</param>
19 /// <param name="right">查詢右邊界</param>
20 /// <param name="node">查詢的節點</param>
21 /// <returns></returns>
22 public void Query(Node node, int left, int right, ref int sum)
23 {
24 //說明當前節點完全包含在查詢范圍內,兩點:要么是單元節點,要么是子區間
25 if (left <= node.left && right >= node.right)
26 {
27 //獲取當前節點的sum值
28 sum += node.Sum;
29 return;
30 }
31 else
32 {
33 //如果當前的left和right 和node的left和right無交集,此時可返回
34 if (node.left > right || node.right < left)
35 return;
36
37 //找到中間線
38 var middle = (node.left + node.right) / 2;
39
40 //左孩子有交集
41 if (left <= middle)
42 {
43 Query(node.leftchild, left, right, ref sum);
44 }
45 //右孩子有交集
46 if (right >= middle)
47 {
48 Query(node.rightchild, left, right, ref sum);
49 }
50
51 }
52 }
53 #endregion
4:更新操作
這個操作跟樹狀數組中的更新操作一樣,當遞歸的找到待修改的節點后,改完其值然后在當前節點一路回溯,並且在回溯的過程中一
路修改父節點的附加值直到根節點,至此我們的操作就完成了,復雜度同樣為logN。
1 #region 更新操作
2 /// <summary>
3 /// 更新操作
4 /// </summary>
5 /// <param name="index"></param>
6 /// <param name="key"></param>
7 public void Update(int index, int key)
8 {
9 Update(nodeTree, index, key);
10 }
11
12 /// <summary>
13 /// 更新操作
14 /// </summary>
15 /// <param name="index"></param>
16 /// <param name="key"></param>
17 public void Update(Node node, int index, int key)
18 {
19 if (node == null)
20 return;
21
22 //取中間值
23 var middle = (node.left + node.right) / 2;
24
25 //遍歷左子樹
26 if (index >= node.left && index <= middle)
27 Update(node.leftchild, index, key);
28
29 //遍歷右子樹
30 if (index <= node.right && index >= middle + 1)
31 Update(node.rightchild, index, key);
32
33 //在回溯的路上一路更改,復雜度為lgN
34 if (index >= node.left && index <= node.right)
35 {
36 //說明找到了節點
37 if (node.left == node.right)
38 {
39 nums[index] = key;
40
41 node.Sum = node.Max = node.Min = key;
42 }
43 else
44 {
45 //回溯時統計左右子樹的值(min,max,sum)
46 node.Min = Math.Min(node.leftchild.Min, node.rightchild.Min);
47 node.Max = Math.Max(node.leftchild.Max, node.rightchild.Max);
48 node.Sum = node.leftchild.Sum + node.rightchild.Sum;
49 }
50 }
51 }
52 #endregion
最后我們做個例子,在2000000的數組空間中,尋找200-3000區間段的sum值,看看他的表現如何。
View Code
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 public static void Main() 14 { 15 int[] nums = new int[200 * 10000]; 16 17 for (int i = 0; i < 10000 * 200; i++) 18 { 19 nums[i] = i; 20 } 21 22 Tree tree = new Tree(); 23 24 //將當前數組構建成 “線段樹” 25 tree.Build(nums); 26 27 var watch = Stopwatch.StartNew(); 28 29 var sum = tree.Query(200, 3000); 30 31 watch.Stop(); 32 33 Console.WriteLine("耗費時間:{0}ms, 當前數組有:{1}個數字, 求出Sum=:{2}", watch.ElapsedMilliseconds, nums.Length, sum); 34 35 Console.Read(); 36 } 37 } 38 39 public class Tree 40 { 41 #region 線段樹的節點 42 /// <summary> 43 /// 線段樹的節點 44 /// </summary> 45 public class Node 46 { 47 /// <summary> 48 /// 區間左端點 49 /// </summary> 50 public int left; 51 52 /// <summary> 53 /// 區間右端點 54 /// </summary> 55 public int right; 56 57 /// <summary> 58 /// 左孩子 59 /// </summary> 60 public Node leftchild; 61 62 /// <summary> 63 /// 右孩子 64 /// </summary> 65 public Node rightchild; 66 67 /// <summary> 68 /// 節點的sum值 69 /// </summary> 70 public int Sum; 71 72 /// <summary> 73 /// 節點的Min值 74 /// </summary> 75 public int Min; 76 77 /// <summary> 78 /// 節點的Max值 79 /// </summary> 80 public int Max; 81 } 82 #endregion 83 84 Node nodeTree = new Node(); 85 86 int[] nums; 87 88 #region 根據數組構建“線段樹" 89 /// <summary> 90 /// 根據數組構建“線段樹" 91 /// </summary> 92 /// <param name="length"></param> 93 public Node Build(int[] nums) 94 { 95 this.nums = nums; 96 97 return Build(nodeTree, 0, nums.Length - 1); 98 } 99 #endregion 100 101 #region 根據數組構建“線段樹" 102 /// <summary> 103 /// 根據數組構建“線段樹" 104 /// </summary> 105 /// <param name="left"></param> 106 /// <param name="right"></param> 107 public Node Build(Node node, int left, int right) 108 { 109 //說明已經到根了,當前當前節點的max,sum,min值(回溯時統計上一層節點區間的值) 110 if (left == right) 111 { 112 return new Node 113 { 114 left = left, 115 right = right, 116 Max = nums[left], 117 Min = nums[left], 118 Sum = nums[left] 119 }; 120 } 121 122 if (node == null) 123 node = new Node(); 124 125 node.left = left; 126 127 node.right = right; 128 129 node.leftchild = Build(node.leftchild, left, (left + right) / 2); 130 131 node.rightchild = Build(node.rightchild, (left + right) / 2 + 1, right); 132 133 //統計左右子樹的值(min,max,sum) 134 node.Min = Math.Min(node.leftchild.Min, node.rightchild.Min); 135 node.Max = Math.Max(node.leftchild.Max, node.rightchild.Max); 136 node.Sum = node.leftchild.Sum + node.rightchild.Sum; 137 138 return node; 139 } 140 #endregion 141 142 #region 區間查詢 143 /// <summary> 144 /// 區間查詢(分解) 145 /// </summary> 146 /// <returns></returns> 147 public int Query(int left, int right) 148 { 149 int sum = 0; 150 151 Query(nodeTree, left, right, ref sum); 152 153 return sum; 154 } 155 156 /// <summary> 157 /// 區間查詢 158 /// </summary> 159 /// <param name="left">查詢左邊界</param> 160 /// <param name="right">查詢右邊界</param> 161 /// <param name="node">查詢的節點</param> 162 /// <returns></returns> 163 public void Query(Node node, int left, int right, ref int sum) 164 { 165 //說明當前節點完全包含在查詢范圍內,兩點:要么是單元節點,要么是子區間 166 if (left <= node.left && right >= node.right) 167 { 168 //獲取當前節點的sum值 169 sum += node.Sum; 170 return; 171 } 172 else 173 { 174 //如果當前的left和right 和node的left和right無交集,此時可返回 175 if (node.left > right || node.right < left) 176 return; 177 178 //找到中間線 179 var middle = (node.left + node.right) / 2; 180 181 //左孩子有交集 182 if (left <= middle) 183 { 184 Query(node.leftchild, left, right, ref sum); 185 } 186 //右孩子有交集 187 if (right >= middle) 188 { 189 Query(node.rightchild, left, right, ref sum); 190 } 191 192 } 193 } 194 #endregion 195 196 #region 更新操作 197 /// <summary> 198 /// 更新操作 199 /// </summary> 200 /// <param name="index"></param> 201 /// <param name="key"></param> 202 public void Update(int index, int key) 203 { 204 Update(nodeTree, index, key); 205 } 206 207 /// <summary> 208 /// 更新操作 209 /// </summary> 210 /// <param name="index"></param> 211 /// <param name="key"></param> 212 public void Update(Node node, int index, int key) 213 { 214 if (node == null) 215 return; 216 217 //取中間值 218 var middle = (node.left + node.right) / 2; 219 220 //遍歷左子樹 221 if (index >= node.left && index <= middle) 222 Update(node.leftchild, index, key); 223 224 //遍歷右子樹 225 if (index <= node.right && index >= middle + 1) 226 Update(node.rightchild, index, key); 227 228 //在回溯的路上一路更改,復雜度為lgN 229 if (index >= node.left && index <= node.right) 230 { 231 //說明找到了節點 232 if (node.left == node.right) 233 { 234 nums[index] = key; 235 236 node.Sum = node.Max = node.Min = key; 237 } 238 else 239 { 240 //回溯時統計左右子樹的值(min,max,sum) 241 node.Min = Math.Min(node.leftchild.Min, node.rightchild.Min); 242 node.Max = Math.Max(node.leftchild.Max, node.rightchild.Max); 243 node.Sum = node.leftchild.Sum + node.rightchild.Sum; 244 } 245 } 246 } 247 #endregion 248 } 249 }

