树查找(二)二叉平衡树

转自:http://www.cnblogs.com/huangxincheng/archive/2012/07/22/2603956.html

上一篇我们聊过,二叉查找树不是严格的O(logN),导致了在真实场景中没有用武之地,谁也不愿意有O(N)的情况发生,

作为一名码农,肯定会希望能把“范围查找”做到地球人都不能优化的地步。

     当有很多数据灌到我的树中时,我肯定会希望最好是以“完全二叉树”的形式展现,这样我才能做到“查找”是严格的O(logN),

比如把这种”树“调正到如下结构。

     

这里就涉及到了“树节点”的旋转,也是我们今天要聊到的内容。

 

一:平衡二叉树(AVL)

1:定义

       父节点的左子树和右子树的高度之差不能大于1,也就是说不能高过1层,否则该树就失衡了,此时就要旋转节点,在

编码时,我们可以记录当前节点的高度,比如空节点是-1,叶子节点是0,非叶子节点的height往根节点递增,比如在下图

中我们认为树的高度为h=2。

复制代码
 1 #region 平衡二叉树节点
 2     /// <summary>
 3     /// 平衡二叉树节点
 4     /// </summary>
 5     /// <typeparam name="K"></typeparam>
 6     /// <typeparam name="V"></typeparam>
 7     public class AVLNode<K, V>
 8     {
 9         /// <summary>
10         /// 节点元素
11         /// </summary>
12         public K key;
13 
14         /// <summary>
15         /// 增加一个高度信息
16         /// </summary>
17         public int height;
18 
19         /// <summary>
20         /// 节点中的附加值
21         /// </summary>
22         public HashSet<V> attach = new HashSet<V>();
23 
24         /// <summary>
25         /// 左节点
26         /// </summary>
27         public AVLNode<K, V> left;
28 
29         /// <summary>
30         /// 右节点
31         /// </summary>
32         public AVLNode<K, V> right;
33 
34         public AVLNode() { }
35 
36         public AVLNode(K key, V value, AVLNode<K, V> left, AVLNode<K, V> right)
37         {
38             //KV键值对
39             this.key = key;
40             this.attach.Add(value);
41 
42             this.left = left;
43             this.right = right;
44         }
45     }
46     #endregion
复制代码

 

2:旋转

    节点再怎么失衡都逃不过4种情况,下面我们一一来看一下。

① 左左情况(左子树的左边节点)

我们看到,在向树中追加“节点1”的时候,根据定义我们知道这样会导致了“节点3"失衡,满足“左左情况“,可以这样想,把这

棵树比作齿轮,我们在“节点5”处把齿轮往下拉一个位置,也就变成了后面这样“平衡”的形式,如果用动画解释就最好理解了。

复制代码
 1         #region 第一种:左左旋转(单旋转)
 2         /// <summary>
 3         /// 第一种:左左旋转(单旋转)
 4         /// </summary>
 5         /// <param name="node"></param>
 6         /// <returns></returns>
 7         public AVLNode<K, V> RotateLL(AVLNode<K, V> node)
 8         {
 9             //top:需要作为顶级节点的元素
10             var top = node.left;
11 
12             //先截断当前节点的左孩子
13             node.left = top.right;
14 
15             //将当前节点作为temp的右孩子
16             top.right = node;
17 
18             //计算当前两个节点的高度
19             node.height = Math.Max(Height(node.left), Height(node.right)) + 1;
20             top.height = Math.Max(Height(top.left), Height(top.right)) + 1;
21 
22             return top;
23         }
24         #endregion
复制代码

 

② 右右情况(右子树的右边节点)

同样,”节点5“满足”右右情况“,其实我们也看到,这两种情况是一种镜像,当然操作方式也大同小异,我们在”节点1“的地方

将树往下拉一位,最后也就形成了我们希望的平衡效果。

复制代码
 1         #region 第二种:右右旋转(单旋转)
 2         /// <summary>
 3         /// 第二种:右右旋转(单旋转)
 4         /// </summary>
 5         /// <param name="node"></param>
 6         /// <returns></returns>
 7         public AVLNode<K, V> RotateRR(AVLNode<K, V> node)
 8         {
 9             //top:需要作为顶级节点的元素
10             var top = node.right;
11 
12             //先截断当前节点的右孩子
13             node.right = top.left;
14 
15             //将当前节点作为temp的右孩子
16             top.left = node;
17 
18             //计算当前两个节点的高度
19             node.height = Math.Max(Height(node.left), Height(node.right)) + 1;
20             top.height = Math.Max(Height(top.left), Height(top.right)) + 1;
21 
22             return top;
23         }
24         #endregion
复制代码

 

③左右情况(左子树的右边节点)

从图中我们可以看到,当我们插入”节点3“时,“节点5”处失衡,注意,找到”失衡点“是非常重要的,当面对”左右情况“时,我们将

失衡点的左子树进行"右右情况旋转",然后进行”左左情况旋转“,经过这样两次的旋转就OK了,很有意思,对吧。

复制代码
 1         #region 第三种:左右旋转(双旋转)
 2         /// <summary>
 3         /// 第三种:左右旋转(双旋转)
 4         /// </summary>
 5         /// <param name="node"></param>
 6         /// <returns></returns>
 7         public AVLNode<K, V> RotateLR(AVLNode<K, V> node)
 8         {
 9             //先进行RR旋转
10             node.left = RotateRR(node.left);
11 
12             //再进行LL旋转
13             return RotateLL(node);
14         }
15         #endregion
复制代码

 

④右左情况(右子树的左边节点)

这种情况和“情景3”也是一种镜像关系,很简单,我们找到了”节点15“是失衡点,然后我们将”节点15“的右子树进行”左左情况旋转“,

然后进行”右右情况旋转“,最终得到了我们满意的平衡。

复制代码
 1         #region 第四种:右左旋转(双旋转)
 2         /// <summary>
 3         /// 第四种:右左旋转(双旋转)
 4         /// </summary>
 5         /// <param name="node"></param>
 6         /// <returns></returns>
 7         public AVLNode<K, V> RotateRL(AVLNode<K, V> node)
 8         {
 9             //执行左左旋转
10             node.right = RotateLL(node.right);
11 
12             //再执行右右旋转
13             return RotateRR(node);
14 
15         }
16         #endregion
复制代码

 

3:添加

    如果我们理解了上面的这几种旋转,那么添加方法简直是轻而易举,出现了哪一种情况调用哪一种方法而已。

复制代码
 1  #region 添加操作
 2         /// <summary>
 3         /// 添加操作
 4         /// </summary>
 5         /// <param name="key"></param>
 6         /// <param name="value"></param>
 7         /// <param name="tree"></param>
 8         /// <returns></returns>
 9         public AVLNode<K, V> Add(K key, V value, AVLNode<K, V> tree)
10         {
11             if (tree == null)
12                 tree = new AVLNode<K, V>(key, value, null, null);
13 
14             //左子树
15             if (key.CompareTo(tree.key) < 0)
16             {
17                 tree.left = Add(key, value, tree.left);
18 
19                 //如果说相差等于2就说明这棵树需要旋转了
20                 if (Height(tree.left) - Height(tree.right) == 2)
21                 {
22                     //说明此时是左左旋转
23                     if (key.CompareTo(tree.left.key) < 0)
24                     {
25                         tree = RotateLL(tree);
26                     }
27                     else
28                     {
29                         //属于左右旋转
30                         tree = RotateLR(tree);
31                     }
32                 }
33             }
34 
35             //右子树
36             if (key.CompareTo(tree.key) > 0)
37             {
38                 tree.right = Add(key, value, tree.right);
39 
40                 if ((Height(tree.right) - Height(tree.left) == 2))
41                 {
42                     //此时是右右旋转
43                     if (key.CompareTo(tree.right.key) > 0)
44                     {
45                         tree = RotateRR(tree);
46                     }
47                     else
48                     {
49                         //属于右左旋转
50                         tree = RotateRL(tree);
51                     }
52                 }
53             }
54 
55             //将value追加到附加值中(也可对应重复元素)
56             if (key.CompareTo(tree.key) == 0)
57                 tree.attach.Add(value);
58 
59             //计算高度
60             tree.height = Math.Max(Height(tree.left), Height(tree.right)) + 1;
61 
62             return tree;
63         }
64         #endregion
复制代码

4:删除

删除方法跟添加方法也类似,当删除一个结点的时候,可能会引起祖先结点的失衡,所以在每次”结点“回退的时候计算结点高度。

复制代码
 1 #region 删除当前树中的节点
 2         /// <summary>
 3         /// 删除当前树中的节点
 4         /// </summary>
 5         /// <param name="key"></param>
 6         /// <param name="tree"></param>
 7         /// <returns></returns>
 8         public AVLNode<K, V> Remove(K key, V value, AVLNode<K, V> tree)
 9         {
10             if (tree == null)
11                 return null;
12 
13             //左子树
14             if (key.CompareTo(tree.key) < 0)
15             {
16                 tree.left = Remove(key, value, tree.left);
17 
18                 //如果说相差等于2就说明这棵树需要旋转了
19                 if (Height(tree.left) - Height(tree.right) == 2)
20                 {
21                     //说明此时是左左旋转
22                     if (key.CompareTo(tree.left.key) < 0)
23                     {
24                         tree = RotateLL(tree);
25                     }
26                     else
27                     {
28                         //属于左右旋转
29                         tree = RotateLR(tree);
30                     }
31                 }
32             }
33             //右子树
34             if (key.CompareTo(tree.key) > 0)
35             {
36                 tree.right = Remove(key, value, tree.right);
37 
38                 if ((Height(tree.right) - Height(tree.left) == 2))
39                 {
40                     //此时是右右旋转
41                     if (key.CompareTo(tree.right.key) > 0)
42                     {
43                         tree = RotateRR(tree);
44                     }
45                     else
46                     {
47                         //属于右左旋转
48                         tree = RotateRL(tree);
49                     }
50                 }
51             }
52             /*相等的情况*/
53             if (key.CompareTo(tree.key) == 0)
54             {
55                 //判断里面的HashSet是否有多值
56                 if (tree.attach.Count > 1)
57                 {
58                     //实现惰性删除
59                     tree.attach.Remove(value);
60                 }
61                 else
62                 {
63                     //有两个孩子的情况
64                     if (tree.left != null && tree.right != null)
65                     {
66                         //根据平衡二叉树的中顺遍历,需要找到”有子树“的最小节点
67                         tree.key = FindMin(tree.right).key;
68 
69                         //删除右子树的指定元素
70                         tree.right = Remove(tree.key, value, tree.right);
71                     }
72                     else
73                     {
74                         //自减高度
75                         tree = tree.left == null ? tree.right : tree.left;
76 
77                         //如果删除的是叶子节点直接返回
78                         if (tree == null)
79                             return null;
80                     }
81                 }
82             }
83 
84             //统计高度
85             tree.height = Math.Max(Height(tree.left), Height(tree.right)) + 1;
86 
87             return tree;
88         }
89         #endregion
复制代码

5: 测试

不像上一篇不能在二叉树中灌有序数据,平衡二叉树就没关系了,我们的需求是检索2012-7-30 4:00:00 到 2012-7-30 5:00:00

的登陆用户的ID,数据量在500w,看看平衡二叉树是如何秒杀对手。

View Code

wow,相差98倍,这个可不是一个级别啊...AVL神器。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值