算法:红黑树
算法:红黑树
红黑树介绍
红黑树(英语:Red–black tree)是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。它是在1972年由鲁道夫·贝尔发明的,他称之为"对称二叉B树",它现代的名字是在Leo J. Guibas和Robert Sedgewick于1978年写的一篇论文中获得的。
红黑树本质上是一种二叉查找树,但它在二叉查找树的基础上额外添加了一个标记(颜色),同时具有一定的规则。这些规则使红黑树保证了一种平衡,插入、删除、查找的最坏时间复杂度都为 O(logn)。它的统计性能要好于平衡二叉树(AVL树),因此,红黑树在很多地方都有应用。比如在 Java 集合框架中,很多部分(HashMap, TreeMap, TreeSet 等)都有红黑树的应用,这些集合均提供了很好的性能。
由于 TreeMap 就是由红黑树实现的,因此本文将使用 TreeMap 的相关操作的代码进行分析、论证。
二叉查找树问题
二叉排序树的性能取决于二叉树的层数:
- 最好的情况是 O(logn),存在于完全二叉排序树情况下,其访问性能近似于折半查找;
- 最差时候会是 O(n),比如插入的元素是有序的,生成的二叉排序树就是一个链表,这种情况下,需要遍历全部元素才行.
如上图所示,同一组元素构成的形态不同,性能不同的二种树形结构。所以呢,我们引入红黑树是要解决这种树形不平衡的问题。
红黑树的性质
红黑树是每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色。在二叉查找树的性质之上,红黑树还有如下的一些性质:
1.节点是或者黑色,但是根节点一定是黑色。
2.所有叶子节点都是黑色(叶子是NIL节点)
3.每个红色节点必须有两个黑色子节点,红色节点不连续,但是黑色节点是可以连续的。
4.从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。
树的旋转知识
当我们在对红黑树进行插入和删除等操作时,对树做了修改,那么可能会违背红黑树的性质。
为了继续保持红黑树的性质,我们可以通过对结点进行重新着色,以及对树进行相关的旋转操作,即修改树中某些结点的颜色及指针结构,来达到对红黑树进行插入或删除结点等操作后,继续保持它的性质或平衡。
树的旋转,分为左旋和右旋,以下借助图来做形象的解释和介绍:
左旋
从右往左看是左旋,首先我们要左旋X,以X-Y为轴左转,X到达原先a的位置,那谁来接替X的位置呢,当然是Y,也就是X本来就小于Y,Y到达X的位置后,X必然要当Y的左节点。那此时Y节点的左边指向X的话,那么原先指向的b应该去哪里呢,b大于X,小于Y,固然只能挂靠在X的右边。C节点本来就大于Y,故仍然在Y的右边。同理A节点本来就小于X,故仍然在X的左边。
TreeMap的实现代码
private void rotateLeft(Entry<K,V> p) { if (p != null) { Entry<K,V> r = p.right; p.right = r.left; // if (r.left != null) r.left.parent = p; r.parent = p.parent; if (p.parent == null) root = r; else if (p.parent.left == p) p.parent.left = r; else p.parent.right = r; r.left = p; p.parent = r; } }
右旋
右旋的思路同左旋,此处不在赘述。
TreeMap的实现代码
private void rotateRight(Entry<K,V> p) { if (p != null) { Entry<K,V> l = p.left; p.left = l.right; if (l.right != null) l.right.parent = p; l.parent = p.parent; if (p.parent == null) root = l; else if (p.parent.right == p) p.parent.right = l; else p.parent.left = l; l.right = p; p.parent = l; } }
此处我需要大家实地的画一下图来理解一下,毕竟实践出真知,虽然思路还是比较清晰的。
红黑树的插入及调整
红黑树的插入主要分为两个部分:
首先是像二叉查找树一样的插入。
然后调整树的结构,保证其满足红黑树的状态。
二叉查找树的实质是一个二分查找,找到合适的位置就放进去。关于二叉查找树的插入,请查看我的相关文章。
插入带来的问题
红黑树的第 5 条特征规定,任一节点到它子树的每个叶子节点的路径中都包含同样数量的黑节点。也就是说当我们往红黑树中插入一个黑色节点时,会违背第5特征。同时第 4 条特征规定红色节点的左右孩子一定都是黑色节点,当我们给一个红色节点下插入一个红色节点时,会违背第4条特征。
因此我们需要在插入黑色节点后进行结构调整,保证红黑树始终满足这 5 条特征。
调整思想
前面说了,插入一个节点后要担心违反特征 4 和 5,数学里最常用的一个解题技巧就是把多个未知数化解成一个未知数。我们这里采用同样的技巧,把插入的节点直接染成红色,这样就不会影响特征 5,只要专心调整满足特征 4 就好了。这样比同时满足 4、5 要简单一些。
染成红色后,我们只要关心父节点是否为红,如果是红的,就要把父节点进行变化,让父节点变成黑色,或者换一个黑色节点当父亲,这些操作的同时不能影响 不同路径上的黑色节点数一致的规则。
第一种情况:父节点和叔叔节点都是红色
假设插入的是节点c,这时父亲节点 a 和叔叔节点 b 都是红色,爷爷节点 X 一定是黑色。
红色节点的孩子不能是红色,这时不管 c 是 a 的左孩子还是右孩子,只要同时把 a 和 b 染成黑色,X 染成红色即可。这样这个子树左右两边黑色个数一致,也满足特征 4。
但是这样改变后 X 染成红色,X 的父亲如果是红色岂不是又违反特征 4 了? 这个问题和我们插入、染红后一致,因此需要以 爷爷节点 X 为新的调整节点,再次进行调整操作,以此循环,直到父亲节点不是红的,就没有问题了。
第二种情况:父节点为红色,叔节点为黑色
假设插入的是节点c,这时父亲节点 a 是红色,叔叔节点 b 是黑色,爷爷节点 X 一定是黑色。红色节点的孩子不能是红色,但是直接把父亲节点 a 涂成黑色也不行,这条路径多了个黑色节点。怎么办呢?
既然改变不了你,那我们就此别过吧,我换一个更适合我的!我们怎么把 a 弄走呢?看来看去,还是右旋最合适,通过把 爷爷节点 X 右旋,a 变成了这个子树的根节点,X 变成了 a 的右子树。右旋后 X 跑到了右子树上,这时把 a 变成黑的,多了一个黑节点,再把 X 变成红的,就平衡了!上面讲的是插入节点 c 在父亲节点 a 的左孩子位置,如果 c 是 a 的右孩子,就需要多进行一次左旋,把情况化解成上述情况。
TreeMap的实现代码
private void fixAfterInsertion(Entry<K,V> x) { x.color = RED; while (x != null && x != root && x.parent.color == RED) { if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { Entry<K,V> y = rightOf(parentOf(parentOf(x))); if (colorOf(y) == RED) { setColor(parentOf(x), BLACK); setColor(y, BLACK); setColor(parentOf(parentOf(x)), RED); x = parentOf(parentOf(x)); } else { if (x == rightOf(parentOf(x))) { x = parentOf(x); rotateLeft(x); } setColor(parentOf(x), BLACK); setColor(parentOf(parentOf(x)), RED); rotateRight(parentOf(parentOf(x))); } } else { Entry<K,V> y = leftOf(parentOf(parentOf(x))); if (colorOf(y) == RED) { setColor(parentOf(x), BLACK); setColor(y, BLACK); setColor(parentOf(parentOf(x)), RED); x = parentOf(parentOf(x)); } else { if (x == leftOf(parentOf(x))) { x = parentOf(x); rotateRight(x); } setColor(parentOf(x), BLACK); setColor(parentOf(parentOf(x)), RED); rotateLeft(parentOf(parentOf(x))); } } } root.color = BLACK; }
红黑树的删除及调整
三种情况删除:
1、删除的是叶子节点,直接删除就OK
2、有左孩子或右孩子,直接把孩子移动到要删除的位置:
3、有两个孩子,需要选一个合适的孩子节点作为新的根节点,该节点为继承节点:
删除后的结构调整
根据红黑树的第 5 个特性:
如果当前待删除节点是红色的,它被删除之后对当前树的特性不会造成任何破坏影响。 而如果被删除的节点是黑色的,这就需要进行进一步的调整来保证后续的树结构满足要求。这里研究的是删除黑色节点的情况。
为了保证删除节点父亲节点左右两边黑色节点数一致,需要重点关注父亲节点没删除的那一边节点是不是黑色。如果删除后父亲节点另一边比删除的一边黑色节点多,就要想办法搞到平衡,具体的平衡方法有如下几种方法:
- 把父亲节点另一边(即删除节点的兄弟树)其中一个节点弄成红色,也少一个黑色
- 或者把另一边多的黑色节点转过来一个
删除节点在父亲节点的左子树还是右子树,调整方式都是对称的,这里以当前节点为父节点的左孩子为例进行分析。
删除后的调整主要分三步:
第一步:
- 兄弟如果是红的,说明孩子都是黑的 【旋转的情况 1 】
- 把兄弟搞成黑的
- 父亲搞成红的
- 左旋转父亲(嘿嘿,兄弟给我分一个黑孩子)
- 接下来对比旋转后的兄弟
第一步解释:
这一步的目的是将兄弟节点变成黑的,转变成第二步两种情形中的某一种情况。在做后续变化前,这棵树还是保持着原来的平衡。
第二步,有两种情况:
情况1 :兄弟节点的孩子都是黑色
- 把兄弟搞成红的
- continue 下一波(这个子树搞完了,研究父亲节点,去搞上一级树,进入第三步)
第二步情况 1 解释:
这里将兄弟节点变成红色后,从它的父节点到下面的所有路径就都统一少了 1 个,同时也不影响别的特征,但是把兄弟节点变红后,如果有父亲节点也是红的,就可能违反红黑树的特征 4,因此需要到更高一级树进行鉴别、调整。
情况2 :兄弟节点的孩子至多有一个是黑的
- 把不是黑的那个孩子搞黑 【旋转的情况 2 】
- 兄弟搞红
- 兄弟右旋转
- 以后对比旋转后的兄弟
- 把兄弟涂成跟父亲一样的颜色 【旋转的情况 3 】
- 然后把父亲搞黑
- 把兄弟的右孩子搞黑
- 父亲节点左旋
- 研究根节点,进入第三步
第二步情况 2 解释:
旋转的情况 2 是将兄弟节点的左右孩子都移动到右边,方便后续操作,如下图所示:
旋转的情况 3 将兄弟的孩子移到左边来,同时黑色的父亲变到了左边(总之就是让左边多些黑色节点),如下图所示:
第三步:
- 如果研究的不是根节点并且是黑的,重新进入第一步,研究上一级树;
- 如果研究的是根节点或者这个节点不是黑的,就退出
- 把研究的这个节点涂成黑的。
第三步解释:
第三步中选择根节点为结束标志,是因为在第二步中,有可能出现我们正好给删除黑色节点的子树补上了一个黑色节点,同时不影响其他子树,这时我们的调整已经完成,可以直接设置调整节点 x = root,等于宣告调整结束。
因为我们当前调整的可能只是一棵树中间的子树,这里头的节点可能还有父节点,这么一直往上到根节点。当前子树少了一个黑色节点,要保证整体合格还是不够的。
这里需要在代码里有一个保证。假设这里 B 已经是红色的了。那么调整结束,最后对 B 节点,也就是调整目标 x 所指向的这个节点涂成黑色。这样保证前面亏的那一个黑色节点就补回来了。
前面讨论的这4种情况是在当前节点是父节点的左子节点的条件下进行的。如果当前节点是父节点的右子节点,则可以对应的做对称的操作处理,过程也是一样的。
其中具体旋转方向根据调整节点在父节点的左/右位置决定。
TreeMap的实现代码
private void fixAfterDeletion(Entry x) { while (x != root && colorOf(x) == BLACK) { if (x == leftOf(parentOf(x))) { Entry sib = rightOf(parentOf(x)); //左旋,把黑色节点移到左边一个 if (colorOf(sib) == RED) { setColor(sib, BLACK); setColor(parentOf(x), RED); rotateLeft(parentOf(x)); sib = rightOf(parentOf(x)); } if (colorOf(leftOf(sib)) == BLACK && colorOf(rightOf(sib)) == BLACK) { setColor(sib, RED); x = parentOf(x); } else { if (colorOf(rightOf(sib)) == BLACK) { setColor(leftOf(sib), BLACK); setColor(sib, RED); rotateRight(sib); sib = rightOf(parentOf(x)); } setColor(sib, colorOf(parentOf(x))); setColor(parentOf(x), BLACK); setColor(rightOf(sib), BLACK); rotateLeft(parentOf(x)); x = root; } } else { //处理的节点在 右边,相同逻辑,只不过旋转的方向相反 Entry sib = leftOf(parentOf(x)); if (colorOf(sib) == RED) { setColor(sib, BLACK); setColor(parentOf(x), RED); rotateRight(parentOf(x)); sib = leftOf(parentOf(x)); } if (colorOf(rightOf(sib)) == BLACK && colorOf(leftOf(sib)) == BLACK) { setColor(sib, RED); x = parentOf(x); } else { if (colorOf(leftOf(sib)) == BLACK) { setColor(rightOf(sib), BLACK); setColor(sib, RED); rotateLeft(sib); sib = leftOf(parentOf(x)); } setColor(sib, colorOf(parentOf(x))); setColor(parentOf(x), BLACK); setColor(leftOf(sib), BLACK); rotateRight(parentOf(x)); x = root; } } } setColor(x, BLACK); }