經典算法題每日演練——第十二題 線段樹


       這一篇我們來看樹狀數組的加強版線段樹,樹狀數組能玩的線段樹一樣可以玩,而且能玩的更好,他們在區間求和,最大,平均

等經典的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 }

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM