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

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

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

等经典的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值,看看他的表现如何。

  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 }

时间: 2024-10-12 17:34:00

经典算法题每日演练——第十二题 线段树的相关文章

经典算法题每日演练——第二十二题 奇偶排序

原文:经典算法题每日演练--第二十二题 奇偶排序 这个专题因为各种原因好久没有继续下去了,MM吧...你懂的,嘿嘿,不过还得继续写下去,好长时间不写,有些东西有点生疏了, 这篇就从简单一点的一个“奇偶排序”说起吧,不过这个排序还是蛮有意思的,严格来说复杂度是O(N2),不过在多核的情况下,可以做到 N2 /(m/2)的效率,这里的m就是待排序的个数,当m=100,复杂度为N2 /50,还行把,比冒泡要好点,因为重点是解决问题的奇思妙想. 下面我们看看这个算法是怎么描述的,既然是奇偶,肯定跟位数有

经典算法题每日演练——第十六题 Kruskal算法

原文:经典算法题每日演练--第十六题 Kruskal算法 这篇我们看看第二种生成树的Kruskal算法,这个算法的魅力在于我们可以打一下算法和数据结构的组合拳,很有意思的. 一:思想 若存在M={0,1,2,3,4,5}这样6个节点,我们知道Prim算法构建生成树是从”顶点”这个角度来思考的,然后采用“贪心思想” 来一步步扩大化,最后形成整体最优解,而Kruskal算法有点意思,它是站在”边“这个角度在思考的,首先我有两个集合. 1. 顶点集合(vertexs): 比如M集合中的每个元素都可以认

经典算法题每日演练——第二十五题 块状链表

原文:经典算法题每日演练--第二十五题 块状链表 在数据结构的世界里,我们会认识各种各样的数据结构,每一种数据结构都能解决相应领域的问题,每一种数据结构都像 是降龙十八掌中的某一掌,掌掌毙命... 当然每个数据结构,有他的优点,必然就有它的缺点,那么如何创造一种数据结构 来将某两种数据结构进行扬长避短,那就非常完美了.这样的数据结构也有很多,比如:双端队列,还有就是今天讲的 块状链表, 我们都知道 数组 具有 O(1)的查询时间,O(N)的删除,O(N)的插入... 链表 具有 O(N)的查询时

经典算法题每日演练——第十五题 并查集

原文:经典算法题每日演练--第十五题 并查集 这一篇我们看看经典又神奇的并查集,顾名思义就是并起来查,可用于处理一些不相交集合的秒杀. 一:场景 有时候我们会遇到这样的场景,比如:M={1,4,6,8},N={2,4,5,7},我的需求就是判断{1,2}是否属于同一个集合,当然实现方法 有很多,一般情况下,普通青年会做出O(MN)的复杂度,那么有没有更轻量级的复杂度呢?嘿嘿,并查集就是用来解决这个问题的. 二:操作 从名字可以出来,并查集其实只有两种操作,并(Union)和查(Find),并查集

经典算法题每日演练——第十九题 双端队列

原文:经典算法题每日演练--第十九题 双端队列 话说大学的时候老师说妹子比工作重要~,工作可以再换,妹子这个...所以...这两个月也就一直忙着Fall in love,嗨,慢慢调整心态吧, 这篇就选一个简单的数据结构聊一聊,话说有很多数据结构都在玩组合拳,比如说:块状链表,块状数组,当然还有本篇的双端队列,是的,它就是 栈和队列的组合体. 一:概念 我们知道普通队列是限制级的一端进,另一端出的FIFO形式,栈是一端进出的LIFO形式,而双端队列就没有这样的限制级,也就是我们可以在 队列两端进行

经典算法题每日演练——第十八题 外排序

原文:经典算法题每日演练--第十八题 外排序 说到排序,大家第一反应基本上是内排序,是的,算法嘛,玩的就是内存,然而内存是有限制的,总有装不下的那一天,此时就可以来玩玩 外排序,当然在我看来,外排序考验的是一个程序员的架构能力,而不仅仅局限于排序这个层次. 一:N路归并排序 1.概序 我们知道算法中有一种叫做分治思想,一个大问题我们可以采取分而治之,各个突破,当子问题解决了,大问题也就KO了,还有一点我们知道 内排序的归并排序是采用二路归并的,因为分治后有LogN层,每层两路归并需要N的时候,最

经典算法题每日演练——第二十四题 梳排序

原文:经典算法题每日演练--第二十四题 梳排序 这篇再看看一个经典的排序,梳排序,为什么取名为梳,可能每个梳都有自己的gap吧,大梳子gap大一点,小梳子gap小一点. 上一篇我们看到鸡尾酒排序是在冒泡排序上做了一些优化,将单向的比较变成了双向,同样这里的梳排序也是在冒泡排序上做了一些优化. 冒泡排序上我们的选择是相邻的两个数做比较,就是他们的gap为1,其实梳排序提出了不同的观点,如果将这里的gap设置为一定的大小, 效率反而必gap=1要高效的多. 下面我们看看具体思想,梳排序有这样一个1.

经典算法题每日演练——第二十题 三元组

原文:经典算法题每日演练--第二十题 三元组 我们知道矩阵是一个非常强大的数据结构,在动态规划以及各种图论算法上都有广泛的应用,当然矩阵有着不足的地方就是空间和时间 复杂度都维持在N2上,比如1w个数字建立一个矩阵,在内存中会占用1w*1w=1亿的类型空间,这时就会遇到outofmemory...那么面 临的一个问题就是如何来压缩矩阵,当然压缩的方式有很多种,这里就介绍一个顺序表的压缩方式:三元组. 一:三元组 有时候我们的矩阵中只有零星的一些非零元素,其余的都是零元素,那么我们称之为稀疏矩阵,

经典算法题每日演练——第十题 树状数组

原文:经典算法题每日演练--第十题 树状数组 有一种数据结构是神奇的,神秘的,它展现了位运算与数组结合的神奇魅力,太牛逼的,它就是树状数组,这种数据结构不是神人是发现不了的. 一:概序 假如我现在有个需求,就是要频繁的求数组的前n项和,并且存在着数组中某些数字的频繁修改,那么我们该如何实现这样的需求?当然大家可以往 真实项目上靠一靠. ① 传统方法:根据索引修改为O(1),但是求前n项和为O(n). ②空间换时间方法:我开一个数组sum[],sum[i]=a[1]+....+a[i],那么有点意