经典算法题每日演练——第十二题 线段树

原文: 经典算法题每日演练——第十二题 线段树

       这一篇我们来看树状数组的加强版线段树,树状数组能玩的线段树一样可以玩,而且能玩的更好,他们在区间求和,最大,平均

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

 

 

你可能感兴趣的:(线段树)