1.2.5 红黑树
红黑树,Red-Black Tree 「RBT」是一个自平衡(不是绝对的平衡)的二叉查找树(BST),树上的每个节点都遵循下面的规则:
1 每个节点要么是黑色,要么是红色。
2 根节点是黑色。
3 每个叶子节点(NIL)是黑色。
4 每个红色结点的两个子结点一定都是黑色。
5 任意一结点到每个叶子结点的路径都包含数量相同的黑结点。
红黑树能自平衡,它靠的是什么?三种操作:左旋、右旋和变色
左旋:以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,<br>右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。
右旋 以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,<br/>左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。
变色 结点的颜色由红变黑或由黑变红。
旋转操作
左旋:以某个节点作为旋转点,其右子节点变为旋转节点的父节点,右子节点的左子节点变为旋转节点的右子节点,左子节点保持不变。


右旋:以某!个节点作为旋转点,其左子节点变为旋转节点的父节点,左子节点的右子节点变为旋转节点的左子节点,右子节点保持不变。


Java代码实现旋转: 先进行类结构定义
package com.bobo.util.treemap;
public class BRTree {
private static final boolean RED = false;
private static final boolean BLACK = true;
private RBNode root;
public RBNode getRoot() {
return root;
}
public void setRoot(RBNode root) {
this.root = root;
}
/**
* 表示 节点
* @param <K>
* @param <V>
*/
static class RBNode<K extends Comparable<K>,V>{
// 节点是双向的
private RBNode parent;
private RBNode left;
private RBNode right;
private boolean color;
private K key;
private V value;
public RBNode() {
}
public RBNode(RBNode parent, RBNode left, RBNode right, boolean color, K key, V value) {
this.parent = parent;
this.left = left;
this.right = right;
this.color = color;
this.key = key;
this.value = value;
}
public RBNode getParent() {
return parent;
}
public void setParent(RBNode parent) {
this.parent = parent;
}
public RBNode getLeft() {
return left;
}
public void setLeft(RBNode left) {
this.left = left;
}
public RBNode getRight() {
return right;
}
public void setRight(RBNode right) {
this.right = right;
}
public boolean isColor() {
左旋代码实现
/**
* 围绕p左旋
* p pr(r)
* / | / \
* pl pr(r) => p rr
* / \ / \
* rl rr pl rl
*
* 左旋的时候
* p-pl 和 pr-rr的关系不变
* pr-rl 要变为 p-rl
* 也就是 rl要变为 p的右子节点
* 同时 p要成为 rl 的父节点
* 还有就是要判断 p 是否有父节点
* 如果没有
* r 变为 root 节点
* 如果有
* r.parent = p.parent
* 还要设置 r为 p.parent 的子节点(可能左也可能右)
* 如果 p.parent.left == p
* p.parent.left = r;
* 否则
* p.parent.right = r;
* 最后
* p.parent = r;
* r.left = p;
* @param p
*/
private void leftRotate(RBNode p){
if(p != null){
RBNode r = p.right;
// 1.设置 pr-rl 要变为 p-rl
// 把rl设置到p的右子节点
p.right = r.left;
if(r.left != null){
// 设置rl的父节点为p
r.left.parent = p;
}
// 2.判断p的父节点情况
r.parent = p.parent; // 不管 p是否有父节点,都把这个父节点设置为 r的父节点
if(p.parent == null){
root = r; // p没有父节点 则r为root节点
}else if(p.parent.left == p){
p.parent.left = r; // 如果p为 p.parent的左子节点 则 r 也为 p.parent的左子节点
}else{
p.parent.right = r; // 反之设置 r 为 p.parent的右子节点
}
// 最后 设置 p 为 r 的左子节点
r.left = p;
p.parent = r;
}
}
右旋实现:
/**
* 围绕p右旋
* @param p
*/
public void rightRotate(RBNode p){
if(p != null){
RBNode r = p.left;
p.left = r.right;
if(r.right != null){
r.right.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.right = p;
p.parent = r;
}
新增节点:https://www.processon.com/view/link/60c21e25e401fd34a1514d25
2-3-4树中结点添加需要遵守以下规则:
● 插入都是向最下面一层插入
● 升元:将插入结点由 2-结点升级成 3-结点,或由 3-结点升级成 4-结点;
● 向 4-结点插入元素后,需要将中间元素提到父结点升元,原结点变成两个 2-结点,再把元素插入2-结点中,如果父结点也是 4-结点,则递归向上层升元,至到根结点后将树高加1;
而将这些规则对应到红黑树里,就是:
● 新插入的结点颜色为 红色 ,这样才可能不会对红黑树的高度产生影响。
● 2-结点对应红黑树中的单个黑色结点,插入时直接成功(对应 2-结点升元)。
● 3-结点对应红黑树中的 黑+红 子树,插入后将其修复成 红+黑+红 子树(对应 3-结点升元);
● 4-结点对应红黑树中的 红+黑+红 子树,插入后将其修复成 红色祖父+黑色父叔+红色孩子 子树,然后再把祖父结点当成新插入的红色结点递归向上层修复,直至修复成功或遇到 root 结点;
公式:红黑树+新增一个节点(红色)=对等的2-3-4树+新增一个节点
新增节点案例 我们通过新增2-3-4树的过程来映射对应的红黑树的节点新增
1.新增一个节点,2 节点
2.新增一个节点,与2节点合并,直接合并
3.新增一个节点,与3节点合并,直接合并 插入的值的位置会有3种情况


4.新增一个节点,与4节点合并,此时需要分裂
插入值的位置可能是

对应的红黑树的结构为:
新增代码实现
红黑树的新增规则我们理清楚了,接下来就可以通过Java代码来具体的实现了。
先实现插入节点,这就是一个普通的二叉树的插入
/**
* 新增节点
* @param key
* @param value
*/
public void put(K key , V value){
RBNode t = this.root;
if(t == null){
// 说明之前没有元素,现在插入的元素是第一个
root = new RBNode<>(key , value == null ? key : value,null);
return ;
}
int cmp ;
// 寻找插入位置
// 定义一个双亲指针
RBNode parent;
if(key == null){
throw new NullPointerException();
}
// 沿着跟节点找插入位置
do{
parent = t;
cmp = key.compareTo((K)t.key);
if(cmp < 0){
// 左侧找
t = t.left;
}else if(cmp > 0){
// 右侧找
t = t.right;
}else{
// 插入节点的值==比较的节点。值替换
t.setValue(value==null?key:value);
return;
}
}while (t != null);
// 找到了插入的位置 parent指向 t 的父节点 t为null
// 创建要插入的节点
RBNode<K, Object> e = new RBNode<>(key, value == null ? key : value, parent);
// 然后判断要插入的位置 是 parent的 左侧还是右侧
if(cmp < 0){
parent.left = e;
}else{
parent.right = e;
}
// 调整 变色 旋转
fixAfterPut(e);
}
然后再根据红黑树的特点来实现调整(旋转,变色)
private boolean colorOf(RBNode node){
return node == null ? BLACK:node.color;
}
private RBNode parentOf(RBNode node){
return node != null ? node.parent:null;
}
private RBNode leftOf(RBNode node){
return node != null ? node.left:null;
}
private RBNode rightOf(RBNode node){
return node != null ? node.right:null;
}
private void setColor(RBNode node ,boolean color){
if(node != null){
node.setColor(color);
}
}
/**
* 插入节点后的调整处理
* 1. 2-3-4树 新增元素 2节点添加一个元素将变为3节点 直接合并,节点中有两个元素
* 红黑树:新增一个红色节点,这个红色节点会添加在黑色节点下(2节点) --- 这种情况不需要调整
2. 2-3-4树 新增元素 3节点添加一个元素变为4节点合并 节点中有3个元素
* 这里有6中情况,( 根左左 根左右 根右右 根右左)这四种要调整 (左中右的两种)不需要调整
* 红黑树:新增红色节点 会添加到 上黑下红的节点中 = 排序后中间节点是黑色,两边节点是红色
*
* 3. 2-3-4树:新增一个元素 4节点添加一个元素需要裂变:中间元素升级为父节点,新增元素与剩下的其中一个合并
* 红黑树:新增节点是红色+爷爷节点是黑色,父亲节点和叔叔节点为红色 调整为
* 爷爷节点变红色,父亲和叔叔节点变为黑色,如果爷爷节点为root节点则调整为黑色
* @param x
*/
private void fixAfterPut(RBNode<K, Object> x) {
x.color = RED;
// 本质上就是父节点是黑色的就不需要调整,对应的 2 3的情况
while(x != null && x != root && x.parent.color == RED){
// 1. x 的父节点是爷爷的 左孩子
if(parentOf(x) == parentOf(parentOf(x)).left){
// 获取当前节点的叔叔节点
RBNode y = rightOf(parentOf(parentOf(x)));
// 情况3
if(colorOf(y) == RED){
// 说明是 上3的情况 变色处理
// 父亲节点和叔叔节点设置为黑色
setColor(parentOf(x),BLACK);
setColor(y,BLACK);
// 爷爷节点设置为 红色
setColor(parentOf(parentOf(x)),RED);
// 递归处理
x = parentOf(parentOf(x));
}else{
// 情况 2
if(x == parentOf(x).right){
// 如果x是父节点的右节点那么我们需要先根据 父节点 左旋
x = parentOf(x);
leftRotate(x);
}
// 叔叔节点为空 对应于 上面的情况2
// 将父节点变为黑色
setColor(parentOf(x),BLACK);
// 将爷爷节点变为红色
setColor(parentOf(parentOf(x)),RED);
// 右旋转 根据爷爷节点右旋转
rightRotate(parentOf(parentOf(x)));
}
}else{
// x 的父节点是爷爷是右孩子
// 获取父亲的叔叔节点
RBNode y = leftOf(parentOf(parentOf(x)));
if(colorOf(y) == RED){
// 情况3
setColor(parentOf(x),BLACK);
setColor(y,BLACK);
-
红黑树的删除操作:
-
红黑树的节点的删除其实也分为两步:
-
1 先删除节点(这步和普通的二叉树删除是一样的)
-
2 然后再调整 要删除这个节点先需要找到这个节点,找到节点就是普通的二分查找,
-
具体代码如下
private RBNode getNode(K key){ RBNode node = this.root; while (node != null ){ int cmp = key.compareTo((K) node.key); if(cmp < 0){ // 在左子树 node = node.left; }else if(cmp >0){ // 右子树 node = node.right; }else{ return node; } } return null; }在整理红黑树节点的删除操作时我们需要先理解清楚红黑树删除和2-3-4树删除的等价关系,这样理解起来才会比较容易 核心理论:红黑树删除操作的本质其实就是删除2-3-4树的叶子节点

-
情况一

-
情况2:删除的是非情况1的节点,根据我们前面介绍的删除的规则,会找到对应的前驱和后继节点,那么最终删除的还是叶子节点

首先删除节点的代码为:
/**
* 删除节点
* @param key
* @return
*/
public V remove(K key){
// 先找到这个节点
RBNode node = getNode(key);
if(node == null){
return null;
}
// 把值存起来 删除后 返回
V oldValue = (V) node.value;
deleteNode(node);
return oldValue;
}
/**
* 删除节点
* 3种情况
* 1.删除叶子节点,直接删除
* 2.删除的节点有一个子节点,那么用子节点来替代
* 3.如果删除的节点右两个子节点,此时需要找到前驱节点或者后继节点来替代
* 可以转换为 1、2的情况
* @param node
*/
private void deleteNode(RBNode node){
// 3.node节点有两个子节点
if(node.left !=null && node.right != null){
// 找到要删除节点的后继节点
RBNode successor = successor(node);
// 然后用后继节点的信息覆盖掉 要删除节点的信息
node.key = successor.key;
node.value = successor.value;
// 然后我们要删除的节点就变为了 后继节点
node = successor;
}
// 2.删除有一个子节点的情况
RBNode replacement = node.left != null ? node.left : node.right;
if(replacement != null){
// 替代者的父指针指向原来 node 的父节点
replacement.parent = node.parent;
if(node.parent == null){
// 说明 node 是root节点
root = replacement;
}else if(node == node.parent.left){
// 双向绑定
node.parent.left = replacement;
}else{
node.parent.right = replacement;
}
// 将node的左右孩子指针和父指针都指向null node等待GC
node.left = node.right = node.parent = null;
// 替换完成后需要调整平衡
if(node.color == BLACK){
// fixAfterRemove(replacement)
}
}else if(node.parent == null){
// 说明要删除的是root节点
root = null;
}else{
// 1. node节点是叶子节点 replacement为null
// 先调整
if(node.color == BLACK){
// fixAfterRemove(node)
}
// 再删除
if(node.parent != null){
if(node == node.parent.left){
node.parent.left = null;
}else{
node.parent.right = null;
}
node = null;
}
}
}
然后就是需要调整红黑树的平衡了。
删除后的平衡调整
1.情况一:自己能搞定的,对应叶子节点是3节点和4节点
2.情况二:自己搞不定,需要兄弟借,但是兄弟不借,找父亲借,父亲下来,然后兄弟找一个人去代替父亲当家
这种情况就是兄弟节点是3节点或者4节点
找兄弟节点
如果找到的兄弟节点是红色其实还要调整
执行如下调整先,先变色,然后左旋
找兄弟节点借
然后沿着7节点左旋
3.情况三:跟兄弟借,兄弟也没有(情同手足,同时自损) 兄弟节点是2节点,同时当前节点的父节点是红色节点的情况
删除后直接变色就可以了 兄弟节点是2节点,同时当前节点的父节点是黑色节点
变更操作为如下,如果继续有父节点那么还要递归处理
分析清楚了删除的3中情况,我们就可以撸处删除的调整的代码了
/**
* 2-3-4树删除操作:
* 1.情况一:自己能搞定的,对应叶子节点是3节点和4节点
* 2.情况二:自己搞不定,需要兄弟借,但是兄弟不借,找父亲借,父亲下来,然后兄弟找一个人去代替父亲当家
* 3.情况三:跟兄弟借,兄弟也没有
* @param x
*/
private void fixAfterRemove(RBNode x){
// 情况2、3
while(x != root && colorOf(x) == BLACK){
// 这种情况才需要调整
// x 是左孩子的情况
if(x == leftOf(parentOf(x))){
// 找兄弟节点
RBNode rNode = rightOf(parentOf(x));
// 判断此时的兄弟节点是否是真正的兄弟节点 兄弟是红色的情况要调整
if(colorOf(rNode) == RED){ // 2-3-4树的 3节点 交换颜色,然后左旋一次就可以了
setColor(rNode,BLACK);
setColor(parentOf(x),RED);
leftRotate(parentOf(x)); // 左旋一次
rNode = rightOf(parentOf(x)); // 找到真正的兄弟节点
}
// 情况3 找兄弟借 没得借
if(colorOf(leftOf(rNode)) == BLACK && colorOf(rightOf(rNode)) == BLACK){
// 情况复杂
setColor(rNode,RED);
x=parentOf(x); // 向上递归
}else{
// 情况2 找兄弟借 有借
// 兄弟节点是 3节点或者4节点
if(colorOf(rightOf(rNode)) == BLACK){
// 右孩子为空,则左孩子肯定不为空
// 兄弟节点 先要左一次右旋
setColor(rNode,RED);
setColor(leftOf(rNode),BLACK);
rightRotate(rNode);
// 重新调整叔叔节点的位置
rNode = rightOf(parentOf(x));
}
// 变色 兄弟节点是 3节点还是4节点 都旋转一次
setColor(rNode, colorOf(parentOf(x)));
setColor(parentOf(x),BLACK);
setColor(rightOf(rNode),BLACK);
// 左旋
leftRotate(parentOf(x));
x = root; // 结束循环 递归 针对的是 情况3
}
}else{
// 找兄弟节点
RBNode rNode = leftOf(parentOf(x));
// 判断此时的兄弟节点是否是真正的兄弟节点 兄弟是红色的情况要调整
if(colorOf(rNode) == RED){ // 2-3-4树的 3节点 交换颜色,然后左旋一次就可以了
setColor(rNode,BLACK);
setColor(parentOf(x),RED);
rightRotate(parentOf(x)); // 左旋一次
rNode = leftOf(parentOf(x)); // 找到真正的兄弟节点
}
// 情况3 找兄弟借 没得借
if(colorOf(rightOf(rNode)) == BLACK && colorOf(leftOf(rNode)) == BLACK){
// 情况复杂
setColor(rNode,RED);
x=parentOf(x); // 向上递归
}else{
// 情况2 找兄弟借 有借
// 兄弟节点是 3节点或者4节点
if(colorOf(leftOf(rNode)) == BLACK){
// 右孩子为空,则左孩子肯定不为空
// 兄弟节点 先要左一次右旋
setColor(rNode,RED);
setColor(leftOf(rNode),BLACK);
leftRotate(rNode);
// 重新调整叔叔节点的位置
rNode = leftOf(parentOf(x));
}
// 变色 兄弟节点是 3节点还是4节点 都旋转一次
setColor(rNode, colorOf(parentOf(x)));
setColor(parentOf(x),BLACK);
setColor(leftOf(rNode),BLACK);
// 左旋
rightRotate(parentOf(x));
x = root; // 结束循环 递归 针对的是 情况3
}
}
}
// 情况1:替代节点是红色,直接染黑 在情况3的情况下 补偿删除的黑色节点,这样红黑树依然保存平衡
setColor(x,BLACK);
}
当然不会就此结束,还有更多的Java场景题,因为篇幅原因,无法给大家全部展示出来,有需要的看板老爷们,
查看下方小名片即可直接白嫖拿走哦!


1149

被折叠的 条评论
为什么被折叠?



