6天通吃树结构—— 第一天 二叉查找树

       

        一直很想写一个关于树结构的专题,再一个就是很多初级点的码农会认为树结构无用论,其实归根到底还是不清楚树的实际用途。

 

一:场景:

1:现状

    前几天我的一个大学同学负责的网站出现了严重的性能瓶颈,由于业务是写入和读取都是密集型,如果做缓存,时间间隔也只能在30s左

右,否则就会引起客户纠纷,所以同学也就没有做缓存,通过测试发现慢就慢在数据读取上面,总共需要10s,天啊...原来首页的加载关联

到了4张表,而且表数据中最多的在10w条以上,可以想象4张巨大表的关联,然后就是排序+范围查找等等相关的条件,让同学抓狂。

 

2:我个人的提供解决方案

 ① 读取问题

    既然不能做缓存,那没办法,我们需要自己维护一套”内存数据库“,数据如何组织就靠我们的算法功底了,比如哈希适合等于性的查找,

树结构适合”范围查找“,lucene适合字符串的查找,我们在添加和更新的时候同时维护自己的内存数据库,最终杜绝表关联,老同学,还

是先应急,把常用的表灌倒内存,如果真想项目好的话,改架构吧...

② 添加问题

   或许你的Add操作还没有达到瓶颈这一步,如果真的达到了那就看情况来进行”表切分“,”数据库切分“吧,让用户的Add或者Update

操作分流,虽然做起来很复杂,但是没办法,总比用户纠纷强吧,可对...

 

二:二叉查找树

    正式切入主题,从上面的说明我们知道了二叉树非常适合于范围查找,关于树的基本定义,这里我就默认大家都知道,我就直接从

查找树说起了。

1:定义

   查找树的定义非常简单,一句话就是左孩子比父节点小,右孩子比父节点大,还有一个特性就是”中序遍历“可以让结点有序。

2:树节点

为了具有通用性,我们定义成泛型模板,在每个结点中增加一个”数据附加域”。

 1     /// <summary>
 2     /// 二叉树节点
 3     /// </summary>
 4     /// <typeparam name="K"></typeparam>
 5     /// <typeparam name="V"></typeparam>
 6     public class BinaryNode<K, V>
 7     {
 8         /// <summary>
 9         /// 节点元素
10         /// </summary>
11         public K key;
12 
13         /// <summary>
14         /// 节点中的附加值
15         /// </summary>
16         public HashSet<V> attach = new HashSet<V>();
17 
18         /// <summary>
19         /// 左节点
20         /// </summary>
21         public BinaryNode<K, V> left;
22 
23         /// <summary>
24         /// 右节点
25         /// </summary>
26         public BinaryNode<K, V> right;
27 
28         public BinaryNode() { }
29 
30         public BinaryNode(K key, V value, BinaryNode<K, V> left, BinaryNode<K, V> right)
31         {
32             //KV键值对
33             this.key = key;
34             this.attach.Add(value);
35 
36             this.left = left;
37             this.right = right;
38         }
39     }

 

3:添加

   根据查找树的性质我们可以很简单的写出Add的代码,一个一个的比呗,最终形成的效果图如下

这里存在一个“重复节点”的问题,比如说我在最后的树中再插入一个元素为15的结点,那么此时该怎么办,一般情况下,我们最好

不要在树中再追加一个重复结点,而是在“重复节点"的附加域中进行”+1“操作。

 1        #region 添加操作
 2         /// <summary>
 3         /// 添加操作
 4         /// </summary>
 5         /// <param name="key"></param>
 6         /// <param name="value"></param>
 7         public void Add(K key, V value)
 8         {
 9             node = Add(key, value, node);
10         }
11         #endregion
12 
13         #region 添加操作
14         /// <summary>
15         /// 添加操作
16         /// </summary>
17         /// <param name="key"></param>
18         /// <param name="value"></param>
19         /// <param name="tree"></param>
20         /// <returns></returns>
21         public BinaryNode<K, V> Add(K key, V value, BinaryNode<K, V> tree)
22         {
23             if (tree == null)
24                 tree = new BinaryNode<K, V>(key, value, null, null);
25 
26             //左子树
27             if (key.CompareTo(tree.key) < 0)
28                 tree.left = Add(key, value, tree.left);
29 
30             //右子树
31             if (key.CompareTo(tree.key) > 0)
32                 tree.right = Add(key, value, tree.right);
33 
34             //将value追加到附加值中(也可对应重复元素)
35             if (key.CompareTo(tree.key) == 0)
36                 tree.attach.Add(value);
37 
38             return tree;
39         }
40         #endregion

 

4:范围查找

    这个才是我们使用二叉树的最终目的,既然是范围查找,我们就知道了一个”min“和”max“,其实实现起来也很简单,

第一步:我们要在树中找到min元素,当然min元素可能不存在,但是我们可以找到min的上界,耗费时间为O(logn)。

第二步:从min开始我们中序遍历寻找max的下界。耗费时间为m。m也就是匹配到的个数。

 

最后时间复杂度为M+logN,要知道普通的查找需要O(N)的时间,比如在21亿的数据规模下,匹配的元素可能有30个,那么最后

的结果也就是秒杀和几个小时甚至几天的巨大差异,后面我会做实验说明。

 1         #region 树的指定范围查找
 2         /// <summary>
 3         /// 树的指定范围查找
 4         /// </summary>
 5         /// <param name="min"></param>
 6         /// <param name="max"></param>
 7         /// <returns></returns>
 8         public HashSet<V> SearchRange(K min, K max)
 9         {
10             HashSet<V> hashSet = new HashSet<V>();
11 
12             hashSet = SearchRange(min, max, hashSet, node);
13 
14             return hashSet;
15         }
16         #endregion
17 
18         #region 树的指定范围查找
19         /// <summary>
20         /// 树的指定范围查找
21         /// </summary>
22         /// <param name="range1"></param>
23         /// <param name="range2"></param>
24         /// <param name="tree"></param>
25         /// <returns></returns>
26         public HashSet<V> SearchRange(K min, K max, HashSet<V> hashSet, BinaryNode<K, V> tree)
27         {
28             if (tree == null)
29                 return hashSet;
30 
31             //遍历左子树(寻找下界)
32             if (min.CompareTo(tree.key) < 0)
33                 SearchRange(min, max, hashSet, tree.left);
34 
35             //当前节点是否在选定范围内
36             if (min.CompareTo(tree.key) <= 0 && max.CompareTo(tree.key) >= 0)
37             {
38                 //等于这种情况
39                 foreach (var item in tree.attach)
40                     hashSet.Add(item);
41             }
42 
43             //遍历右子树(两种情况:①:找min的下限 ②:必须在Max范围之内)
44             if (min.CompareTo(tree.key) > 0 || max.CompareTo(tree.key) > 0)
45                 SearchRange(min, max, hashSet, tree.right);
46 
47             return hashSet;
48         }
49         #endregion

 

5:删除

   对于树来说,删除是最复杂的,主要考虑两种情况。

<1>单孩子的情况

     这个比较简单,如果删除的节点有左孩子那就把左孩子顶上去,如果有右孩子就把右孩子顶上去,然后打完收工。

<2>左右都有孩子的情况。

     首先可以这么想象,如果我们要删除一个数组的元素,那么我们在删除后会将其后面的一个元素顶到被删除的位置,如图

       

那么二叉树操作同样也是一样,我们根据”中序遍历“找到要删除结点的后一个结点,然后顶上去就行了,原理跟"数组”一样一样的。

同样这里也有一个注意的地方,在Add操作时,我们将重复元素的值追加到了“附加域”,那么在删除的时候,就可以先判断是

不是要“-1”操作而不是真正的删除节点,其实这里也就是“懒删除”,很有意思。

 1         #region 删除当前树中的节点
 2         /// <summary>
 3         /// 删除当前树中的节点
 4         /// </summary>
 5         /// <param name="key"></param>
 6         /// <returns></returns>
 7         public void Remove(K key, V value)
 8         {
 9             node = Remove(key, value, node);
10         }
11         #endregion
12 
13         #region 删除当前树中的节点
14         /// <summary>
15         /// 删除当前树中的节点
16         /// </summary>
17         /// <param name="key"></param>
18         /// <param name="tree"></param>
19         /// <returns></returns>
20         public BinaryNode<K, V> Remove(K key, V value, BinaryNode<K, V> tree)
21         {
22             if (tree == null)
23                 return null;
24 
25             //左子树
26             if (key.CompareTo(tree.key) < 0)
27                 tree.left = Remove(key, value, tree.left);
28 
29             //右子树
30             if (key.CompareTo(tree.key) > 0)
31                 tree.right = Remove(key, value, tree.right);
32 
33             /*相等的情况*/
34             if (key.CompareTo(tree.key) == 0)
35             {
36                 //判断里面的HashSet是否有多值
37                 if (tree.attach.Count > 1)
38                 {
39                     //实现惰性删除
40                     tree.attach.Remove(value);
41                 }
42                 else
43                 {
44                     //有两个孩子的情况
45                     if (tree.left != null && tree.right != null)
46                     {
47                         //根据二叉树的中顺遍历,需要找到”有子树“的最小节点
48                         tree.key = FindMin(tree.right).key;
49 
50                         //删除右子树的指定元素
51                         tree.right = Remove(key, value, tree.right);
52                     }
53                     else
54                     {
55                         //单个孩子的情况
56                         tree = tree.left == null ? tree.right : tree.left;
57                     }
58                 }
59             }
60 
61             return tree;
62         }
63         #endregion

 

三:测试

   假如现在我们有一张User表,我要查询"2012/7/30 4:30:00"到"2012/7/30 4:40:00"这个时间段登陆的用户,我在txt中生成一个

33w的userid和time的数据,看看在33w的情况下读取效率如何...

View Code
  1 using System;
  2 using System.Collections.Generic;
  3 using System.Linq;
  4 using System.Text;
  5 using System.Threading;
  6 using System.IO;
  7 using System.Diagnostics;
  8 
  9 namespace DataStruct
 10 {
 11     class Program
 12     {
 13         static void Main(string[] args)
 14         {
 15             List<long> list = new List<long>();
 16 
 17             Dictionary<DateTime, int> dic = new Dictionary<DateTime, int>();
 18 
 19             BinaryTree<DateTime, int> tree = new BinaryTree<DateTime, int>();
 20 
 21             using (StreamReader sr = new StreamReader(Environment.CurrentDirectory + "//1.txt"))
 22             {
 23                 var line = string.Empty;
 24 
 25                 while (!string.IsNullOrEmpty(line = sr.ReadLine()))
 26                 {
 27                     var userid = Convert.ToInt32(line.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)[0]);
 28 
 29                     var time = Convert.ToDateTime(line.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)[1]);
 30 
 31                     //防止dic出错,为了进行去重处理
 32                     if (!dic.ContainsKey(time))
 33                     {
 34                         dic.Add(time, userid);
 35 
 36                         tree.Add(time, userid);
 37                     }
 38                 }
 39             }
 40 
 41             var min = Convert.ToDateTime("2012/7/30 4:30:00");
 42 
 43             var max = Convert.ToDateTime("2012/7/30 4:40:00");
 44 
 45             var watch = Stopwatch.StartNew();
 46 
 47             var result1 = dic.Keys.Where(i => i >= min && i <= max).Select(i => dic[i]).ToList();
 48 
 49             watch.Stop();
 50 
 51             Console.WriteLine("字典查找耗费时间:{0}ms,获取总数:{1}", watch.ElapsedMilliseconds, result1.Count);
 52 
 53             watch = Stopwatch.StartNew();
 54 
 55             var result2 = tree.SearchRange(min, max);
 56 
 57             watch.Stop();
 58 
 59             Console.WriteLine("二叉树耗费时间:{0}ms,获取总数:{1}", watch.ElapsedMilliseconds, result2.Count);
 60         }
 61     }
 62 
 63     #region 二叉树节点
 64     /// <summary>
 65     /// 二叉树节点
 66     /// </summary>
 67     /// <typeparam name="K"></typeparam>
 68     /// <typeparam name="V"></typeparam>
 69     public class BinaryNode<K, V>
 70     {
 71         /// <summary>
 72         /// 节点元素
 73         /// </summary>
 74         public K key;
 75 
 76         /// <summary>
 77         /// 节点中的附加值
 78         /// </summary>
 79         public HashSet<V> attach = new HashSet<V>();
 80 
 81         /// <summary>
 82         /// 左节点
 83         /// </summary>
 84         public BinaryNode<K, V> left;
 85 
 86         /// <summary>
 87         /// 右节点
 88         /// </summary>
 89         public BinaryNode<K, V> right;
 90 
 91         public BinaryNode() { }
 92 
 93         public BinaryNode(K key, V value, BinaryNode<K, V> left, BinaryNode<K, V> right)
 94         {
 95             //KV键值对
 96             this.key = key;
 97             this.attach.Add(value);
 98 
 99             this.left = left;
100             this.right = right;
101         }
102     }
103     #endregion
104 
105     public class BinaryTree<K, V> where K : IComparable
106     {
107         public BinaryNode<K, V> node = null;
108 
109         #region 添加操作
110         /// <summary>
111         /// 添加操作
112         /// </summary>
113         /// <param name="key"></param>
114         /// <param name="value"></param>
115         public void Add(K key, V value)
116         {
117             node = Add(key, value, node);
118         }
119         #endregion
120 
121         #region 添加操作
122         /// <summary>
123         /// 添加操作
124         /// </summary>
125         /// <param name="key"></param>
126         /// <param name="value"></param>
127         /// <param name="tree"></param>
128         /// <returns></returns>
129         public BinaryNode<K, V> Add(K key, V value, BinaryNode<K, V> tree)
130         {
131             if (tree == null)
132                 tree = new BinaryNode<K, V>(key, value, null, null);
133 
134             //左子树
135             if (key.CompareTo(tree.key) < 0)
136                 tree.left = Add(key, value, tree.left);
137 
138             //右子树
139             if (key.CompareTo(tree.key) > 0)
140                 tree.right = Add(key, value, tree.right);
141 
142             //将value追加到附加值中(也可对应重复元素)
143             if (key.CompareTo(tree.key) == 0)
144                 tree.attach.Add(value);
145 
146             return tree;
147         }
148         #endregion
149 
150         #region 是否包含指定元素
151         /// <summary>
152         /// 是否包含指定元素
153         /// </summary>
154         /// <param name="key"></param>
155         /// <returns></returns>
156         public bool Contain(K key)
157         {
158             return Contain(key, node);
159         }
160         #endregion
161 
162         #region 是否包含指定元素
163         /// <summary>
164         /// 是否包含指定元素
165         /// </summary>
166         /// <param name="key"></param>
167         /// <param name="tree"></param>
168         /// <returns></returns>
169         public bool Contain(K key, BinaryNode<K, V> tree)
170         {
171             if (tree == null)
172                 return false;
173             //左子树
174             if (key.CompareTo(tree.key) < 0)
175                 return Contain(key, tree.left);
176 
177             //右子树
178             if (key.CompareTo(tree.key) > 0)
179                 return Contain(key, tree.right);
180 
181             return true;
182         }
183         #endregion
184 
185         #region 树的指定范围查找
186         /// <summary>
187         /// 树的指定范围查找
188         /// </summary>
189         /// <param name="min"></param>
190         /// <param name="max"></param>
191         /// <returns></returns>
192         public HashSet<V> SearchRange(K min, K max)
193         {
194             HashSet<V> hashSet = new HashSet<V>();
195 
196             hashSet = SearchRange(min, max, hashSet, node);
197 
198             return hashSet;
199         }
200         #endregion
201 
202         #region 树的指定范围查找
203         /// <summary>
204         /// 树的指定范围查找
205         /// </summary>
206         /// <param name="range1"></param>
207         /// <param name="range2"></param>
208         /// <param name="tree"></param>
209         /// <returns></returns>
210         public HashSet<V> SearchRange(K min, K max, HashSet<V> hashSet, BinaryNode<K, V> tree)
211         {
212             if (tree == null)
213                 return hashSet;
214 
215             //遍历左子树(寻找下界)
216             if (min.CompareTo(tree.key) < 0)
217                 SearchRange(min, max, hashSet, tree.left);
218 
219             //当前节点是否在选定范围内
220             if (min.CompareTo(tree.key) <= 0 && max.CompareTo(tree.key) >= 0)
221             {
222                 //等于这种情况
223                 foreach (var item in tree.attach)
224                     hashSet.Add(item);
225             }
226 
227             //遍历右子树(两种情况:①:找min的下限 ②:必须在Max范围之内)
228             if (min.CompareTo(tree.key) > 0 || max.CompareTo(tree.key) > 0)
229                 SearchRange(min, max, hashSet, tree.right);
230 
231             return hashSet;
232         }
233         #endregion
234 
235         #region 找到当前树的最小节点
236         /// <summary>
237         /// 找到当前树的最小节点
238         /// </summary>
239         /// <returns></returns>
240         public BinaryNode<K, V> FindMin()
241         {
242             return FindMin(node);
243         }
244         #endregion
245 
246         #region 找到当前树的最小节点
247         /// <summary>
248         /// 找到当前树的最小节点
249         /// </summary>
250         /// <param name="tree"></param>
251         /// <returns></returns>
252         public BinaryNode<K, V> FindMin(BinaryNode<K, V> tree)
253         {
254             if (tree == null)
255                 return null;
256 
257             if (tree.left == null)
258                 return tree;
259 
260             return FindMin(tree.left);
261         }
262         #endregion
263 
264         #region 找到当前树的最大节点
265         /// <summary>
266         /// 找到当前树的最大节点
267         /// </summary>
268         /// <returns></returns>
269         public BinaryNode<K, V> FindMax()
270         {
271             return FindMin(node);
272         }
273         #endregion
274 
275         #region 找到当前树的最大节点
276         /// <summary>
277         /// 找到当前树的最大节点
278         /// </summary>
279         /// <param name="tree"></param>
280         /// <returns></returns>
281         public BinaryNode<K, V> FindMax(BinaryNode<K, V> tree)
282         {
283             if (tree == null)
284                 return null;
285 
286             if (tree.right == null)
287                 return tree;
288 
289             return FindMax(tree.right);
290         }
291         #endregion
292 
293         #region 删除当前树中的节点
294         /// <summary>
295         /// 删除当前树中的节点
296         /// </summary>
297         /// <param name="key"></param>
298         /// <returns></returns>
299         public void Remove(K key, V value)
300         {
301             node = Remove(key, value, node);
302         }
303         #endregion
304 
305         #region 删除当前树中的节点
306         /// <summary>
307         /// 删除当前树中的节点
308         /// </summary>
309         /// <param name="key"></param>
310         /// <param name="tree"></param>
311         /// <returns></returns>
312         public BinaryNode<K, V> Remove(K key, V value, BinaryNode<K, V> tree)
313         {
314             if (tree == null)
315                 return null;
316 
317             //左子树
318             if (key.CompareTo(tree.key) < 0)
319                 tree.left = Remove(key, value, tree.left);
320 
321             //右子树
322             if (key.CompareTo(tree.key) > 0)
323                 tree.right = Remove(key, value, tree.right);
324 
325             /*相等的情况*/
326             if (key.CompareTo(tree.key) == 0)
327             {
328                 //判断里面的HashSet是否有多值
329                 if (tree.attach.Count > 1)
330                 {
331                     //实现惰性删除
332                     tree.attach.Remove(value);
333                 }
334                 else
335                 {
336                     //有两个孩子的情况
337                     if (tree.left != null && tree.right != null)
338                     {
339                         //根据二叉树的中顺遍历,需要找到”有子树“的最小节点
340                         tree.key = FindMin(tree.right).key;
341 
342                         //删除右子树的指定元素
343                         tree.right = Remove(tree.key, value, tree.right);
344                     }
345                     else
346                     {
347                         //单个孩子的情况
348                         tree = tree.left == null ? tree.right : tree.left;
349                     }
350                 }
351             }
352 
353             return tree;
354         }
355         #endregion
356     }
357 }

 

比普通的dictionary效率还仅仅是快11倍,从数量级来说还不是非常明显,为什么说不是非常明显,这是因为普通的查找树的时间复杂度

不是严格的log(N),在最坏的情况下会出现“链表”的形式,复杂度退化到O(N),比如下图。

     

不过总会有解决办法的,下一篇我们继续聊如何旋转,保持最坏复杂度在O(logN)。

 

   

posted @ 2012-07-21 15:44  一线码农  阅读(44049)  评论(47编辑  收藏  举报