二叉查找树之红黑树

Posted 小欢欢

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二叉查找树之红黑树相关的知识,希望对你有一定的参考价值。

 关于红黑树,先上一张图,这里推荐一个可视化数据结构的网站:可视化带动画的数据结构基本的在线操作,我在这上面生成了一张:

网上有很多教程,写的也非常棒,这里简单的总结一下其基本性质:

 

1.每一个节点或者是红色,或者是黑色;

2. 根节点是黑色的;

3.如果一个节点是红色的,那么它的子节点必须是黑色的;

4.从一个节点到一个null引用(如上图 的 60节点的左子节点其实是没有的)的每一条路径一定包含相同数目的黑色节点

5.规定 null引用指向的节点(即空节点)是黑色的。

 

二 .为啥有了AVL树还要有红黑树

根据二者的结构图很容易看出红黑树的查找速度明显不如AVL树,为啥,因为它的平衡度没有AVL树好,即某些节点的深度会比AVL树大。那是为啥呢,稍后做个数据抽样测试:

 

三 .红黑树在插入和删除的时候是如何维护平衡的

根据上面描述的基本性质,可以推断维护平衡的其中一个条件是 给节点涂上颜色,但是涂上颜色后不一定平衡, 这就用到了前面AVL树的维持平衡的方式:旋转

综合即两种方式:

1. 改变节点的颜色

2.旋转节点

四 .红黑树的插入实现,有图有代码实现:

假设我们新插入的节点都是红色, 一会儿说明原因,下面先分析插入位置的几种情况,就像AVL树那样:

1.写入根节点

         即当前的树是空树,第一次插入 ,直接new 一个节点,根据红黑树的定义 根节点是黑色的,指定这个节点是黑色,如插入节点8:

 

2. 插入的节点的父节点是黑色节点

        为了满足上面定义的性质 后3条,插入的节点必须是红色的,而且插入后的树一定是平衡的,无需任何操作,这得益于上面的第4条性质,如下面的树插入值是2 的节点

                   

插入2以后:    

 

从这里能明显说明:如果插入的节点是红色,对于维护树的性质 不需要过多的操作,要简单的多, 所以规定先插入的节点的颜色默认是红色 。

 

3. 插入的节点的父节点是红色,叔叔节点也是红色

          这里强调一下,叔叔节点 是插入节点的祖父节点的另一个子节点,如图下面红黑树,插入节点3,其父节点 4 和叔叔节点 7 都是红色

 

插入3后: 

          这时,违反了性质3: 红色节点的子节点只能是黑色节点

解决办法:

父亲和叔叔都变成黑色,祖父变成红色:

4. 插入的节点的父节点是红色,叔叔节点是黑色

          a. 插入到左子节点,接着上图,如下图,这里插入节点 65:

这里说明一下:父节点是红色,叔叔节点因为是祖父节点的null引用,所以是一个黑色节点,即空节点是黑色

这里也违反了 性质3:红色节点的子节点必须是黑色节点

解决办法:

先以插入节点的父节点 为支点右旋,然后就变成另外一种情况b 了,即 新节点插入到右子节点。

如图先右旋:

此时转向下面的情况:

           b. 插入到右子节点,接着上图,此时把70当插入节点,65当父节点:

           解决办法:

                 父节点 65 变黑,祖父节点 60 变红,并以祖父节点60 为支点左旋:

 

为了验证这种操作的通用性,这里我再做一组实验

 接着上图,这里我插入值是 2 的节点,再次验证上面的  a情况 :

 这里为了对比直接将上面的图拷下来:

插入2 后: 

此时,按上面 a 的解决方案,直接右旋插入节点的父节点,就变成了:

右旋 3 后: 

 

这又变成了上面的情况 b,直接把插入节点指向原来的父节点,再对 新插入的节点(即现在的 3)进行情况b的操作,即: 3 的父节点 2 变成黑色,祖父节点 4 变成红色,左旋祖父节点 4 :

嘿 !,问题出来,左旋 4 后,因为4没有右节点,原来 4 的位置不是空吗, 这不对啊, 仔细观察发现 :这时 应该左旋 2 ,即新节点的父节点,旋转后就变成了上面那个图了,又回去了,这样就成死循环了。

忽略这个图,回到上面的图,这里再次把那个图拷过来:

 

 

仔细观察,发现正常的操作应该是 :

父节点 3 变黑,祖父节点 4 变红,右旋祖父节点 4:

这样维护了整个树的平衡,但是同样 父节点是红色,叔叔节点是黑色,插入的值在父节点的左儿子上,为啥跟上面的  a 情况不一样呢!

回顾一下前面的AVL树的插入  其中  左右 和 右左两种情况分别需要旋转2次才能解决, 同样这里也是一样:在 a 情况中 插入 65 就是 右 左 情况, 同理 b 情况也会出现类似的现象

所以在4 的情况下 插入后整个树的平衡(即满足红黑树的4条性质)调整不仅依赖于 插入节点是否是左右节点,还依赖于其父节点是否是左节点还是右节点

下面根据上面几幅图的操作,总结一下 插入的节点的父节点是红色,叔叔节点是黑色 的整个操作

  • 插在左子树上,且父节点是祖父的左子节点:

                父节点变黑,祖父节点变红, 右旋祖父节点

  • 插在左子树上,且父节点是祖父的右子节点:

               右旋父节点,并把插入节点指向父节点,即原来的父节点变成了插入节点(新节点),

            新节点的父节点变黑 ,新节点的祖父节点变红, 左旋新节点的祖父节点 【这里参考对象变了,所以描述的比较啰嗦】

  • 插在右子树上,且父节点是祖父的右子节点:

            父节点变黑,祖父节点变红,左旋祖父节点  (跟 2 的第二步一模一样)

  • 插在右子树上,且父节点是祖父的左子节点:

              左旋父节点,并把插入节点指向父节点,即原来的父节点变成的插入节点(新节点),

              新节点的父节点变黑 ,新节点的祖父节点变红, 右旋新节点的祖父节点 【这里参考对象变了,所以描述的比较啰嗦】

 

 

红黑树的插入完整编码实现:

  1 public class RBTree {
  2 
  3     /**
  4      * 红黑树的根节点
  5      */
  6     private TreeNode root;
  7 
  8     /**
  9      * 根据红黑树的性质,定义树节点
 10      * 树节点对象
 11      */
 12     static class TreeNode {
 13         /**
 14          * 节点的值
 15          */
 16         private int val;
 17 
 18         /**
 19          * 左子节点
 20          */
 21         private TreeNode left;
 22 
 23         /**
 24          * 右子节点
 25          */
 26         private TreeNode right;
 27 
 28         /**
 29          * 父亲节点
 30          */
 31         private TreeNode parent;
 32 
 33         /**
 34          * 是否红色节点
 35          */
 36         private boolean red;
 37 
 38         /**
 39          * 默认插入的节点是红色的
 40          *
 41          * @param val
 42          */
 43         public TreeNode(int val) {
 44             this.val = val;
 45             this.red = true;
 46         }
 47 
 48     }
 49 
 50     /**
 51      * @param value 待插入的值
 52      */
 53     public void add(int value) {
 54         TreeNode t = root;
 55         if (root == null) {
 56             root = new TreeNode(value);
 57             //根节点是黑色的
 58             root.red = false;
 59             return;
 60         }
 61         TreeNode parent = null;
 62         while (t != null) {
 63             //第一次parent指向root, t 也是root
 64             parent = t;
 65             if (value < t.val)
 66                 t = t.left;
 67             else if (value > t.val)
 68                 t = t.right;
 69             else
 70                 t.val = value;
 71         }
 72         t = new TreeNode(value);
 73         //parent是上一次循环后的t
 74         t.parent = parent;
 75         if (parent != null) {
 76             if (t.val < parent.val)
 77                 parent.left = t;
 78             else
 79                 parent.right = t;
 80         }
 81         //添加完后 t是新的插入节点,对新插入的节点进行平衡调整
 82         fixAfterInsertion(t);
 83         //修复完后,最终的根节点的颜色是黑色
 84         root.red = false;
 85     }
 86 
 87     /**
 88      * 插入后新插入的节点进行从下向上变色,和旋转处理
 89      *
 90      * @param t
 91      * @solutions :
 92      * 1. 插在左子树上,且父节点是祖父的左子节点:
 93      * 父节点变黑,祖父节点变红, 右旋祖父节点
 94      * 2. 插在左子树上,且父节点是祖父的右子节点:
 95      * 右旋父节点,并把插入节点指向父节点,即原来的父节点变成了插入节点(新节点),
 96      * 新节点的父节点变黑 ,新节点的祖父节点变红, 左旋新节点的祖父节点 【这里参考对象变了,所以描述的比较啰嗦】
 97      * 3. 插在右子树上,且父节点是祖父的右子节点:
 98      * 父节点变黑,祖父节点变红,左旋祖父节点  (跟 2 的第二步一模一样)
 99      * 4. 插在右子树上,且父节点是祖父的左子节点:
100      * 左旋父节点,并把插入节点指向父节点,即原来的父节点变成的插入节点(新节点),
101      * 新节点的父节点变黑 ,新节点的祖父节点变红, 右旋新节点的祖父节点 【这里参考对象变了,所以描述的比较啰嗦】
102      */
103     private void fixAfterInsertion(TreeNode t) {
104         while (t != null && t != root && t.red && t.parent.red) {
105             TreeNode parent = t.parent;
106             TreeNode grand = parentOf(parent);
107             TreeNode uncle = uncleOf(t);
108             //叔叔是null或者是黑色, 即叔叔都是黑色
109             if (uncle == null || (!uncle.red)) {
110                 //1.插在左子树上
111                 if (parent.left == t) {
112                     //1.1. 父节点是祖父的左子节点:  父节点变黑,祖父节点变红, 右旋祖父节点
113                     if (parent == grand.left) {
114                         setColor(parent, false);
115                         setColor(grand, true);
116                         rotateRight(grand);
117                     } else {
118                         //1.2. 父节点是祖父的右子节点:
119                         // 右旋父节点,并把插入节点指向父节点,即原来的父节点变成了插入节点(新节点),下面这一步就递归了。
120                         // 新节点的父节点变黑 ,新节点的祖父节点变红, 左旋新节点的祖父节点
121                         t = parent;
122                         rotateRight(parent);
123                     }
124                 } else {
125                     //2. 插在右子树上
126                     //2.1. 父节点是祖父的右子节点:  父节点变黑,祖父节点变红,左旋祖父节点
127                     if (parent == grand.right) {
128                         setColor(parent, false);
129                         setColor(grand, true);
130                         rotateLeft(grand);
131                     } else {
132                         //2.2. 父节点是祖父的左子节点:
133                         //左旋父节点,并把插入节点指向父节点,即原来的父节点变成的插入节点(新节点),
134                         //新节点的父节点变黑 ,新节点的祖父节点变红, 右旋新节点的祖父节点
135                         t = parent;
136                         rotateLeft(parent);
137 
138                     }
139                 }
140             } else {
141                 //叔叔是红色,直接把叔叔和父亲变成黑色,爷爷变成红色
142                 //父亲,叔叔变黑
143                 setColor(parentOf(t), false);
144                 setColor(uncleOf(t), false);
145                 //爷爷变红
146                 setColor(grandfatherOf(t), true);
147             }
148         }
149     }
150 
151     /**
152      * 给指定的节点上色
153      *
154      * @param treeNode
155      * @param b        false : 黑色,true : 红色
156      */
157     private void setColor(TreeNode treeNode, boolean b) {
158         if (treeNode != null)
159             treeNode.red = b;
160     }
161 
162     /**
163      * 左旋
164      * 基本的左旋,左旋与avl树一样
165      *
166      * @param node
167      */
168     private void rotateLeft(TreeNode node) {
169         TreeNode right = node.right;
170         node.right = right.left;
171         if (right.left != null)
172             right.left.parent = node;
173         right.parent = node.parent;
174         //重新给父节点的孩子赋值
175         if (node.parent == null)
176             root = right;
177         else if (node.parent.left == node)
178             node.parent.left = right;
179         else
180             node.parent.right = right;
181         //重新给孩子的父节点赋值
182         right.left = node;
183         node.parent = right;
184     }
185 
186     /**
187      * 右旋
188      * 基本的右旋, 右旋与avl树一样
189      *
190      * @param node
191      */
192     private void rotateRight(TreeNode node) {
193         TreeNode left = node.left;
194         node.left = left.right;
195         if (left.right != null)
196             left.right.parent = node;
197         left.parent = node.parent;
198         //重新给父节点的孩子赋值
199         if (node.parent == null)
200             root = left;
201         else if (node.parent.left == node)
202             node.parent.left = left;
203         else
204             node.parent.right = left;
205         //重新给孩子的父节点赋值
206         left.right = node;
207         node.parent = left;
208     }
209 
210     /**
211      * 获取指定节点node 的父节点
212      *
213      * @param node
214      * @return
215      */
216     private TreeNode parentOf(TreeNode node) {
217         return node == null ? null : node.parent;
218     }
219 
220     /**
221      * 获取指定节点node的叔叔节点(父亲的兄弟)
222      *
223      * @param node
224      * @return
225      */
226     private TreeNode uncleOf(TreeNode node) {
227         TreeNode parent = parentOf(node);
228         TreeNode left = leftOf(grandfatherOf(node));
229         if (parent == left)
230             return rightOf(grandfatherOf(node));
231         else
232             return left;
233     }
234 
235     /**
236      * 获取指定节点的爷爷节点
237      *
238      * @param node
239      * @return
240      */
241     private TreeNode grandfatherOf(TreeNode node) {
242         return parentOf(parentOf(node));
243     }
244 
245     /**
246      * 获取指定节点的左子节点
247      *
248      * @param node
249      * @return
250      */
251     private TreeNode leftOf(TreeNode node) {
252         return node == null ? null : node.left;
253     }
254 
255     /**
256      * 获取指定节点node的右子节点
257      *
258      * @param node
259      * @return
260      */
261     private TreeNode rightOf(TreeNode node) {
262         return node == null ? null : node.right;
263     }
264 
265 
266     /**
267      * 断点测试一下
268      * @param args
269      */
270     public static void main(String[] args) {
271         RBTree obj = new RBTree();
272         obj.add(4);
273         obj.add(2);
274         obj.add(1);
275         obj.add(5);
276         obj.add(6);
277         obj.add(10);
278         obj.add(8);
279         System.err.println(obj.root);
280     }
281     //嗯,画出图来,好使
282 }

 

不错,要的就是这儿效果

 

以上是关于二叉查找树之红黑树的主要内容,如果未能解决你的问题,请参考以下文章

特化的AVL树之红黑树学习及原理解析

浅谈算法和数据结构: 九 平衡查找树之红黑树

算法基础8:平衡树之红黑树

算法红黑树-二叉树-算法

浅谈算法和数据结构:平衡查找树之红黑树

stl mapset之红黑树