一直很想写一个关于树结构的专题,再一个就是很多初级点的码农会认为树结构无用论,其实归根到底还是不清楚树的实际用途。
一:场景:
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的情况下读取效率如何...
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)。