二叉查找树之红黑树
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 }
不错,要的就是这儿效果
以上是关于二叉查找树之红黑树的主要内容,如果未能解决你的问题,请参考以下文章