说明:本文是在学习红黑树并看了N篇文章后的梳理,并非原创,感谢下面几篇文章:
0. 为啥要有红黑树
可以先看看这篇文章:漫画:什么是红黑树
二叉查找树的理想高度是logN,最坏情况可能是所有节点都在一条斜线上,树的高度就变成为N,直接影响了树的查找效率。
为了解决上面问题,产生了一种新的树—-平衡树,红黑树是平衡树的一种。红黑树在插入和删除过程中,会通过旋转变色操作将高度保持在logN。
所以下面要讨论的问题是在插入和删除过程中,如何保持红黑树的平衡。
1. 红黑树的性质
- 任何一个节点都有颜色,黑色或者红色
- 根节点是黑色
- 如果是空的叶子节点,必须是黑色
- 如果一个节点是红色,它的叶子节点必须是黑色,也就是说不能有两个连续的红色节点
- 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点
2. 左旋右旋
2.1左旋
对节点X进行左旋,就是意味着将X变成一个左节点,步骤如下:
节点y的左孩子β
成为节点x的右孩子
节点x
成为节点y的左孩子
举例:
private void leftRotate(RBNode node) {
if (node == null) {
return;
}
if (node.rightChild != null) {
RBNode right = node.rightChild;
node.rightChild = right.leftChild;
if (right.leftChild != null) {
right.leftChild.parent = node;
}
right.leftChild = node;
if (node.parent == null) {
root = right;
} else {
if (isLeftChild(node)) {
node.parent.leftChild = right;
} else {
node.parent.rightChild = right;
}
}
right.parent = node.parent;
node.parent = right;
}
}
2.2右旋
对节点y进行右旋,就意味着将节点y变成一个右节点,步骤如下:
x的右孩子β
成为y的左孩子
节点y
成为节点x的右孩子
举例:
private void rightRotate(RBNode node) {
if (node == null) {
return;
}
if (node.leftChild != null) {
RBNode left = node.leftChild;
node.leftChild = left.rightChild;
if (left.rightChild != null) {
left.rightChild.parent = node;
}
left.rightChild = node;
if (node.parent == null) {
root = left;
} else {
if (isLeftChild(node)) {
node.parent.leftChild = left;
} else {
node.parent.rightChild = left;
}
}
left.parent = node.parent;
node.parent = left;
}
}
3.红黑树的添加
步骤如下:
- 将红黑树当做一棵二叉查找树,将节点插入
- 将插入的节点设为红色
- 从插入的节点开始,一直往上将树调整回红黑树。直到当前节点的父节点是黑色或者到达根节点时,才停止调整
问题1:为啥插入的节点设为红色
因为如果插入节点设为黑色,则直接违背了性质5:从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点
,能少违背一个就少违背一个。
问题2:插入节点是红色,还会违背其他性质吗
可能会违背性质4:如果一个节点是红色,它的叶子节点必须是黑色
下面对树进行调整:
情况1:插入的节点是根节点
直接将根节点设成黑色
情况2:插入节点的父节点是黑色的
什么也不需要做,节点插入后,依然是红黑树
情况3:插入节点的父节点是红色的
这时候会违背性质4
。这种情况下,肯定满足下面两个条件:
- 肯定存在非空祖父节点(因为父节点是红色,肯定不是根节点)
- 肯定存在叔叔节点(即使叔叔节点为空,也看做存在,空节点本身是黑色节点)
理解了这点之后,可以将情况3继续分成3种情况:
情况3.1:当前节点的父节点是红色,叔叔节点也是红色
如上图当前节点是45,处理策略:
- 将父节点设为黑色
- 将叔叔节点设为黑色
- 将祖父节点设为红色
- 将祖父节点设为
当前节点
,对当前节点
继续操作
情况3.2:当前节点的父节点是红色,叔叔节点是黑色,且当前节点和父节点不在一个方向上
不在一个方向上指的是如果父节点是祖父节点的左孩子,当前节点是父节点的右孩子。继续上面的例子,当前节点是节点60。
处理策略:
- 将父节点设为
当前节点
- 以
新的当前节点
为支点进行左旋(或右旋),就是把节点60移到上面去
情况3.3:当前节点的父节点是红色,叔叔节点是黑色,且当前节点和父节点在一个方向上
处理策略:
- 将
父节点
设为黑色 - 将
祖父节点
设为红色 - 以
祖父节点
为支点进行左旋(或右旋),就是把节点40移到上面去
/**
* 插入后需要旋转着色成红黑树
*/
private void balanceInsert(RBNode node) {
if (node == root) { //情况1:是根节点
setColor(node, RBNode.BLACK);
return;
}
if (colorOf(node.parent) == RBNode.BLACK) { //情况2:父节点是黑色
return;
}
while (node != null && node != root && colorOf(node.parent) == RBNode.RED) {
RBNode uncle = uncleOf(node);
if (colorOf(uncle) == RBNode.RED) { //情况3.1:叔叔节点是红色
setColor(node.parent, RBNode.BLACK);
setColor(uncle, RBNode.BLACK);
setColor(node.parent.parent, RBNode.RED);
node = node.parent.parent;
} else {
boolean parentIsLeft = isLeftChild(node.parent);
boolean childIsLeft = isLeftChild(node);
if (parentIsLeft ^ childIsLeft) { //情况3.2:叔叔节点是黑色,父节点和子节点方向不同
node = node.parent;
if (childIsLeft) {
rightRotate(node);
} else {
leftRotate(node);
}
} else { //情况3.3:叔叔节点是黑色,父节点和子节点方向相同
setColor(node.parent, RBNode.BLACK);
setColor(node.parent.parent, RBNode.RED);
if (parentIsLeft) {
rightRotate(node.parent.parent);
} else {
leftRotate(node.parent.parent);
}
}
}
}
setColor(root, RBNode.BLACK); //情况3.1后祖父节点变成"新的当前节点",如果祖父节点是root,需要设为黑色
}
4.红黑树的删除
需要先明白两个概念:
前驱节点:左子树中最大的节点,如下图节点25是节点50的前驱节点
后继节点:右子树中最小的节点,如下图节点66是节点50的后继节点
删除节点步骤如下:
- 找到待删除节点A
- 找节点A的前驱或后继节点B,如果节点B不为空,则将节点B的值赋给节点A
- 如果节点B为空,则
实际删除节点
在节点A的位置上;否则就在节点B的位置上 - 以
实际删除节点
为当前节点进行调整,直到当前节点是红色或者是根节点,才停止调整
说明:这里假设实际删除节点
为X
(至多只有一个孩子节点),其孩子节点为N
,X的兄弟节点为S
,S的左节点为SL
,右节点为SR
,接下来讨论是建立在节点X被删除,节点N替换X的基础上进行的。如果N为空,即X没有子节点,则是对X删除前的位置进行调整。
情况1:S为红色,其他节点为黑色
删除前P和N的路径有三个黑色节点(P -> X -> N),现在只剩下两个了(P -> N)。所以不满足性质5
。
处理策略:
- 将S和P颜色互换
- 如果S是右节点,对P进行左旋;如果S是左节点,对P进行右旋。总之就是将S节点弄到父节点的位置
情况2:P节点可红可黑,S节点和S的子节点都是黑色
处理策略:
- 将S变成红色
- 将P看做新的当前节点继续进行处理
解释:经过S路径的黑色节点就比之前少一个,所以经过N节点和经过S节点的黑色节点数一致,但是不经过这两个节点的路径的黑色节点数还是多一,所以其实还是不平衡。
情况3:N的父节点颜色可红可黑,S为黑色,S的红子节点和S节点不在一边
处理策略:
- 将S的红色子节点和S的颜色互换
- 对S进行左旋或右旋,就是将S的红色子节点弄到原来S的位置
情况4:N的父节点颜色可红可黑,S为黑色,S的红色子节点和S节点在同一边
处理策略:
- 将P和S颜色互换
- 并将S的红色子节点变黑
- 如果S是右孩子,则对P进行左旋;如果S是左孩子,则对P进行右旋。总之就是把S弄到父节点
- 将根节点看做新的当前节点,结束调整
/**
* 删除节点后需要旋转着色成红黑树
*
* @param node 实际要删除的节点,至多有一个子节点
*/
private void balanceRemove(RBNode node) {
System.out.println("balanceRemove(),node:" + node);
if (colorOf(node) == RBNode.RED) {
setColor(node, RBNode.BLACK);
return;
}
while (node != null && node != root && colorOf(node) == RBNode.BLACK) {
if (isLeftChild(node)) {
System.out.println("删除:" + node.value);
RBNode sib = brotherOf(node);
if (colorOf(sib) == RBNode.RED) { //情况1,兄弟节点为红色,它的子节点肯定是黑色
System.out.println("左-情况1");
setColor(sib, RBNode.BLACK);
setColor(node.parent, RBNode.RED);
leftRotate(node.parent);
sib = brotherOf(node);
}
if (colorOf(sib.leftChild) == RBNode.BLACK && colorOf(sib.rightChild) == RBNode.BLACK) {
System.out.println("左-情况2");
setColor(sib, RBNode.RED);
node = node.parent;
} else {
if (sib != null && colorOf(sib) == RBNode.BLACK && colorOf(sib.leftChild) == RBNode.RED && colorOf(sib.rightChild) == RBNode.BLACK) { //情况5
System.out.println("左-情况3");
setColor(sib, RBNode.RED);
setColor(sib.leftChild, RBNode.BLACK);
rightRotate(sib);
sib = brotherOf(node);
}
if (sib != null && colorOf(sib) == RBNode.BLACK && colorOf(sib.rightChild) == RBNode.RED) { //情况6
System.out.println("左-情况4");
setColor(sib, node.parent.color);
setColor(node.parent, RBNode.BLACK);
setColor(sib.rightChild, RBNode.BLACK);
leftRotate(node.parent);
node = root;
}
}
} else {
System.out.println("删除:" + node.value);
RBNode sib = brotherOf(node);
if (colorOf(sib) == RBNode.RED) { //情况1,兄弟节点为红色,它的子节点肯定是黑色
System.out.println("右-情况1");
setColor(sib, RBNode.BLACK);
setColor(node.parent, RBNode.RED);
rightRotate(node.parent);
sib = brotherOf(node);
}
if (colorOf(sib.leftChild) == RBNode.BLACK && colorOf(sib.rightChild) == RBNode.BLACK) {
System.out.println("右-情况2");
setColor(sib, RBNode.RED);
node = node.parent;
} else {
if (sib != null && colorOf(sib) == RBNode.BLACK && colorOf(sib.rightChild) == RBNode.RED && colorOf(sib.leftChild) == RBNode.BLACK) { //情况3
System.out.println("右-情况3");
setColor(sib, RBNode.RED);
setColor(sib.rightChild, RBNode.BLACK);
leftRotate(sib);
sib = brotherOf(node);
}
if (sib != null && colorOf(sib) == RBNode.BLACK && colorOf(sib.leftChild) == RBNode.RED) { //情况4
System.out.println("右-情况4");
setColor(sib, node.parent.color);
setColor(node.parent, RBNode.BLACK);
setColor(sib.leftChild, RBNode.BLACK);
rightRotate(node.parent);
node = root;
}
}
}
}
setColor(node, RBNode.BLACK);
}
具体实现可以看这里:https://github.com/cnting/BSTDemo
墙裂推荐一个演示红黑树过程的网址:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html