前言:
我们在上一篇博客认识到了平衡二叉树(AVLTree,BBST),了解到AVL树的性质,也了解了平衡二叉搜索树优于普通二叉搜索树的诸多特点,其实平衡二叉树最大的作用就是查找,AVL树的查找、插入和删除在平均和最坏情况下都是O(logn)。AVL树的效率就是高在这个地方。
红黑树(RB Tree)的引出背景:
对于平衡二叉搜索树是根据平衡因子来维持绝对平衡。
为了让它重新维持在一个平衡状态,就需要对其进行旋转处理, 那么创建一颗平衡二叉树的成本其实不小. 这个时候就有人开始思考,并且提出了红黑树的理论,那么红黑树到底比AVL树好在哪里?
红黑树与AVL树的比较:
1.AVL树的时间复杂度虽然优于红黑树,但是对于现在的计算机,cpu太快,可以忽略性能差异
2.红黑树的插入删除比AVL树更便于控制操作
3.红黑树整体性能略优于AVL树(红黑树旋转次数情况少于AVL树)
红黑树的应用场景:红黑树:C++ STL(比如 map,set),java的TreeMap,TreeSet,HashMap,HashSet,Linux的进程调度,Nginx的timer管理
红黑树的五条性质
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(Null)是黑色。 [注意:这里叶子节点,是指为空(NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。【通过任意一条从根到叶子简单路径上颜色的约束,红黑树保证最长路径不超过最短路径的二倍,因而近似平衡。】
插入:
- 红黑树插入节点过程大致分析:
- RBTree为二叉搜索树,我们按照二叉搜索树的方法对其进行节点插入
- RBTree有颜色约束性质,因此我们在插入新节点之后要进行颜色调整
具体步骤如下:
- 根节点为NULL,直接插入新节点并将其颜色置为黑色
- 根节点不为NULL,找到要插入新节点的位置
- 插入新节点
- 判断新插入节点对全树颜色的影响,更新调整颜色
对于添加一共可以分为以下12种情况 【可分为三大类】
第一类:其中根据红黑树的性质,【7,8,11,12】这四种情况不需要处理直接添加,且插入后默认为红色处理【判断条件为parent为黑色】
添加 25 节点【默认为红色】,直接添加
---------------------------------> 添加后:
第二类:【5,6,9,10】该四种可以作为同一类型处理。【判断条件:uncle叔父节点(父节点的兄弟节点) 不是 RED 【注意:Null默认为黑色。】】
- 处理方案 【注:parent为新添加的父节点,grand为父节点的父节点,即祖父节点】
- 1.parent 染成BLACK,grand 染成RED
- 2.grand 进行单旋操作 【LL:右旋转 , RR:左旋转, /2.双旋操作:----> RL:parent右旋转,grand左旋转; LR:parent左旋转,grand右旋转】
例子:添加 24 节点 【grand单旋,右旋转】
---------------------------------> 添加后
第三类:判断条件为uncle叔父节点的颜色为红色时
- 处理方案
- 1.parent 染成BLACK,uncle染成黑色
- 将祖父节点染成 红色 ,再将祖父节点当做新添加的节点来看待,作为新添加的节点【新添加的节点默认为红色】进行上溢,即递归调用
例:添加23
---------------------------------> 添加后:
代码实现如下:
/**
* 红黑树add之后处理 (新添加的元素默认为:RED)
* @param node
* 一:父节点为黑色的情况【最简单的四种】 直接 return
* 二:判断条件:uncle 不是 RED 【其中四种】
* 1.parent 染成BLACK,grand 染成RED
* 2.grand 进行单旋操作 【LL:右旋转 , RR:左旋转,
* /2.双旋操作:----> RL:parent右旋转,grand左旋转; LR:parent左旋转,grand右旋转】
* 三:【最后4种】
*
*
* 红黑树是通过颜色的约束来维持二叉树的平衡!!
*/
public void afterAdd(Red_Node node){
Red_Node parent = node.parent;
//添加的是根节点,将根节点染成黑色 【 或者上溢到达根节点 】
if(parent==null){
black(node);
}
//四种情况中的父节点为黑色的情况!,不做处理直接返回
if(isBlack(parent))return; //isBlack()函数中,假如传进的是空节点null,则返回黑色
//uncle 节点
Red_Node uncle = parent.sibling();
Red_Node grand = parent.parent;
//叔父节点是红色且parent也是红色的情况!
if(isRed(uncle)){
black(parent);
black(uncle); //【两个染成黑色是为了实现:假如一个节点为红色,则它的两个节点都是黑色】
afterAdd(red(grand)); //祖父节点当成新添加的节点上溢,【上溢之前染红色】
return; //叔父节点的逻辑写完了,因此return
}
//叔父节点不是红色但是parent是红色的情况
if(parent.isParentLeft_Child()){
if(node.isParentLeft_Child()){ //LL
black(parent);
red(grand);
}else { //LR
black(node);
red(grand);
while_left(parent);
}
while_right(grand);
}
else { //R
if(node.isParentLeft_Child()){ //RL
black(node);
red(grand);
while_right(parent);
}else { //RR
black(parent);
red(grand);
}
while_left(grand);
}
}
删除
- 红黑树删除节点过程大致分析:
- RBTree为二叉搜索树,我们按照二叉搜索树的方法对其进行节点删除
- RBTree有颜色约束性质,因此我们在删除节点之后要进行颜色调整
分为两种:
1.删除红色节点
对于删除红色节点的情况,因为删除红色节点不会影响红黑树的性质,因此删除红色节点不需要处理,直接删除即可
//代码体现
if(isRed(node)){return;} //红色叶子节点(一种)
2.删除黑色节点
- 对于删除黑色节点的情况分为三种情况:
- 拥有两个 RED 子节点的 BLACK 节点,不可能被直接删除,因为会找到它的子节点替代删除【即都会转化为下面两种情况的判断】,因此不需要考虑这样的情况
- 删除拥有一个 RED 子节点的BLACK 节点 【情况一】
- 删除BLACK 叶子节点 【情况二】
对于上面只需要考虑两种情况
情况一:拥有一个 RED 子节点的BLACK 节点的删除
对于拥有一个red红色子节点的黑色节点的删除而言,只需要将替代被删黑色节点的红色的子节点染黑即可,这样就能保证红黑树的第五条性质。
例如:
上图表示删除 60 这个黑色节点,即将78指向60的线指向60的子节点,再将47这个节点染黑,如下所示:
情况二:删除的是BLACK 叶子节点
删除BLACK叶子节点分为两种情况,第一种情况为删除根节点(根节点为黑色),直接删除即可,不做任何处理,root = null。
第二种情况为删除的不是根节点,即又分为两种情况:
1.被删除的节点的兄弟节点为黑色
2.被删除的节点的兄弟节点为红色 (且可以根据红黑树的性质得:该红节点必定有黑色的子节点,这里是为后面处理该情况埋下伏笔)
【补充:对于上面两种情况,可能有人会问:存在没有兄弟的情况吗?答案是不可能,除非是 root 节点,上面已经考虑了。为什么呢?因为这是根据红黑树的性质决定,比如第五条性质中:从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。】
对于上面的兄弟节点为黑色的情况又分为两种情况:一种是被删除的兄弟节点有至少一个红色子节点,一种是没有红色子节点(也可以是说没有子节点)
一:对于被删除的兄弟节点有至少一个红色子节点的情况
- 解决方案为:
- 对被删之后的情况进行旋转【分为LL/LR/RL/RR】
- 旋转之后的中心节点继承 被删节点的 parent 的颜色
- 旋转之后的左右节点染成BLACK
例:删除91 【这里可以看出节点 91 删完后 50 -44 -41为LL情况,对父节点:50进行右旋转即可】
删后 如下所示: 【注解:中心44继承继承 被删节点的 parent 的颜色:BLACK,旋转之后的左右节点染成BLACK】
二:对于被删除的兄弟节点是黑色且该黑色兄弟节点也没有红色子节点的情况
- 分为两种情况
- 1.被删除节点的父节点为红色的情况
- 2.被删除节点的父节点为黑色的情况
对于被删除节点的父节点为红色的情况
- 解决方案
- 1.父节点染黑----> black(parent);
- 2.黑兄弟染红-----> red(sibling);
如下图所示删除95 【 black(parent); red(sibling); 】
删除95后
对于被删除节点的父节点为黑色的情况 【会导致parent 下溢】
- 解决方案
- 1.和上面一样操作
- 2.因为导致parent下溢,这时只需要将 parent 当成被删除的节点处理即可【递归调用】
被删除的节点的兄弟节点为红色
- 解决方案
- 兄弟节点染成BLACK,parent 染成 RED ,进行旋转
- 于是又回到兄弟节点为BLACK的情况
对于上面的情况,删除86这个节点,该节点的兄弟节点为 10 红色节点,假如我们可以让 86 的父节点 81 的左left 指向31,这样我们就回到了上面 86 兄弟节点为黑色的情况!【妙啊~~~】
如何实现呢?
我们想到了旋转!,对81这个父节点右旋转,即可让81的左孩子指针指向 31 这个节点,其他情况以此类推~
removeAfter()完整代码:
public void removeAfter(Red_Node node,Red_Node replacement){
if(isRed(node)){return;} //红色叶子节点(一种)
if(isRed(replacement)){ //度为一的节点(两种)
black(replacement);
return;
}
/*** “ 删除黑色的情况 ”
* 最后一种情况:度为 " 一 " 且 被删除的节点颜色为黑色的情况
* 看被删除的节点的兄弟节点的颜色判断进行是否可以借用或者进行下溢
* sibling : 兄弟节点
* 当黑色的节点存在时,必定在同阶的高度中有节点,即:----> 黑节点一定存在兄弟节点
*/
Red_Node parent = node.parent;
if(parent == null){
return; //删除的是根节点【度为一且为黑】,也是递归出口!!!
}
boolean left = parent.left ==null;
Red_Node sibling = left ? parent.right:parent.left;
/**
* 因为旋转的方向不同,分为被删除的节点的位置不一样,在右边和左边的情况
*/
if(left){ //被删除的节点在左边,兄弟节点在右边
if(isRed(sibling)){
red(parent);
black(sibling);
while_left(parent);
sibling = parent.right;
}
//兄弟节点必定是黑色的
if(isBlack(sibling.left) && isBlack(sibling.right)){ //兄弟节点的左右节点都是黑色!且当子节点是null时默认为黑色
boolean parentBlack = isBlack(parent);
black(parent);
red(sibling);
if(parentBlack){
removeAfter(parent,null);
}
}else { //兄弟节点中至少有一个子节点为红色【即可以被node节点借!!】
if(isBlack(sibling.right)){
while_right(sibling);
sibling = parent.right;
}
color(sibling,colorOf(parent));
black(sibling.right);
black(parent);
while_left(parent);
}
}else { //被删除的节点在右边,兄弟节点在左边
/**
* 先处理兄弟是红色的情况 --- > 转为黑色情况
* 后面直接处理黑色情况即可
*/
if(isRed(sibling)){
red(parent);
black(sibling);
while_right(parent);
sibling = parent.left;
}
//兄弟节点必定是黑色的
if(isBlack(sibling.left) && isBlack(sibling.right)){ //兄弟节点的左右节点都是黑色!且当子节点是null时默认为黑色
boolean parentBlack = isBlack(parent);
black(parent);
red(sibling);
if(parentBlack){
removeAfter(parent,null);
}
}else { //兄弟节点中至少有一个子节点为红色【即可以被node节点借!!】
if(isBlack(sibling.left)){
while_left(sibling);
sibling = parent.left;
}
color(sibling,colorOf(parent));
black(sibling.left);
black(parent);
while_right(parent);
}
}
}
待续……
红黑树完整代码:https://blog.csdn.net/m0_46542703/article/details/110354479