文章目录
- 红黑树
- 1. 概念性质
- 2. 红黑树节点定义
- 3. 红黑树的插入
- 情况1
- 情况2
- 情况3
- 其它细节问题
- 插入代码实现
- 4. 红黑树的验证
- 5.性能分析
红黑树
1. 概念性质
红黑树也是一种二插搜索树,每一个节点上比普通二插搜索树都增加了一个存储位置表示节点的颜色,可以是Red或者Black.通过对任何一条从根到叶子的路径上各个节点上色的方式限制,红黑树确保没有一条路径会比其他路径长出2倍,从而得出红黑树是接近平衡的。
红黑树的性质
-
每个节点不是红色就是黑色
-
根节点是黑色的
-
如果一个节点是红色的,则它的两个孩子节点是黑色的,红黑树不能有2个连续的红色节点
-
对于每个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数目的黑色节点
-
每个叶子节点都是黑色的(此处的叶子节点指的是是空节点,比如上图的NIL)
通过以上几条性质就能保证最长路径中的节点个数不会超过最短路径节点个数的两倍,因为不能出现两个连续的红色节点,假设有一条路径全是黑色节点,由于每条路径的黑色节点个数是相等的,假设有一种情况是红黑交替,那么全黑节点路径就是最短路径的,而红黑交替路径就是最上路径,就可以保证长路径中的节点个数不会超过最短路径节点个数的两倍。
最长路径和最短路径图:
如图最短路径
2. 红黑树节点定义
enum COLOR {
RED,BLACK;
}
private static class RBTreeNode {
int val;
RBTreeNode parent;
RBTreeNode left;
RBTreeNode right;
COLOR color;
public RBTreeNode(int val) {
this.val = val;
this.left = null;
this.right = null;
// 插入节点默认为红色
this.color = COLOR.RED;
}
}
3. 红黑树的插入
红黑树插入节点默认应该插入红色的,因为如果默认插入的节点是黑色,因为红黑树的性质4每条路径中的黑色节点数目相同,如果默认插入的节点为黑色就需要新增节点,所以默认插入节点应该为红色。
红黑树的插入步骤:
- 以二插搜索树的插入方式进行插入新节点
- 插入节点后,检测红黑树的性质是否被影响
我们约定cur为插入节点、p为插入节点的父亲节点、g为插入节点的爷爷节点、u为插入节点的叔叔节点
情况1
情况1:cur为红,p为红,g为黑,u存且为红
需要注意的是这里需要分两种情况,一种是看到的是一棵完整的树,另一种是一棵树的子树,此处为抽象图:
当把cur插入到红黑树当中,因为插入的节点是红色的,此时就不满足红黑树的性质3,所以此时就需要把p和u改成黑色的。
但是这还没完,假设g不是根节点,这棵树是另外一棵树的子树,那么g还有父节点。有可能g的父节点是红色的,也有可能是黑色的。假设g的父亲节点是黑色的,它的兄弟节点也是黑的。那么此时把p和u修改成的黑色节点,那么路径上的黑色节点就增加了,此时就需要把g修改成红色。那如果g本身就是根节点呢?这个可以放在最后再来处理,处理完所有情况后,不管g是红色和是黑色都把g改成黑色。
还有一种情况就是如果g父亲的节点本身就是一个红色的节点,如果g的父亲节点是红色的说明其上面肯定还有节点,因为根节点是黑色的,也就是g的父亲节点可定不是根节点。此时就以同样的方式继续向上调整。所以解决方式就是:**将p,u改为黑,g改为红,然后把g当成cur,继续向上调整 **
情况2
情况2:cur为红、p为红、u不存在或者u为黑
情况2抽象图如下:
需要注意的是情况2这种抽象图是在红黑树调整过程中产生的,因为它并不遵循红黑树的性质:每条路径上的黑色节点个数一致,那么其实p的右边其实是有黑色节点的,同样cur也应该是黑色节点,cur变成红色就是因为在调整过程中改变了颜色。
具象图如下:
我们在情况1的条件下修改完对应节点颜色后进行向上调整,发现就得到了情况2,所以说情况2是在调整过程中的到的。
调整情况2第一步就是进行右旋,再修改颜色。
- 对g节点进行右单旋
- 修改p的颜色为黑色、修改g的颜色为红色
此处讨论的是grandFather.left == parent,如果是grandFather.right == parent,那么就要是左单旋g节点
情况3
情况3:cur为红,p为红,g为黑,u不存在或者u为黑
情况3也是和情况2类似也是在红黑树的调整过程中产生的,在调整过程中cur变成了红色。抽象图如下:
再来看一个对应的具象图:
对应情况3我们先要将p节点进行左单旋。
对p节点进行左单旋后,发现此时的树节点的颜色情况和情况2非常相似,只是引用不一致。我们回想一下情况2的条件:
- 情况2:cur为红、p为红、u不存在或者u为黑
- 对比情况2只是p和cur的指向不一样入下图所示
所以对于情况3我们只需要左旋后将p和cur的引用进行交换,再以情况2的方式进行处理即可。
还有一种情况grandFather.right == parent,此时的判断又不一样了,此时的条件是cur == parent.left
,且是对parent进行右单旋,再修改指向。
其它细节问题
- 注意每次插入后都要将根节点root的颜色修改为黑色,避免调整时root被修改成红色,从而导致问题
- 情况2和情况3,要分两种情况讨论
grandFather.right == parent
和grandFather.left == parent
grandFather.left == parent
的时候情况2是对grandFather进行右单旋,当grandFather.right == parent
的时候情况2是对grandFather进行左单旋grandFather.left == parent
情况3判断的是cur == parent.right
并且对parent进行左单旋,如果是grandFather.right == parent
情况3判断的是cur == parent.left
并且对parent进行右单旋- 但情况1是不需要区分的
插入代码实现
/**
* 插入节点
* @param val
*/
public void insert(int val) {
RBTreeNode node = new RBTreeNode(val);
if (root == null) {
root = node;
// 根节点是黑色的
root.color = COLOR.BLACK;
return;
}
// 以搜索树树的方式进行插入
RBTreeNode cur = root;
RBTreeNode parent = cur;
while (cur != null) {
parent = cur;
if (cur.val > val) {
cur = parent.left;
} else if (cur.val < val) {
cur = parent.right;
} else {
System.out.println("元素: "+val+"+已经存在,插入失败!");
return;
}
}
if (parent.val > val) {
parent.left = node;
} else {
parent.right = node;
}
// 修改指向
node.parent = parent;
cur = node;
//调整红黑树
// 如果parent为红色说明,parent一定不是根节点
while (parent != null && parent.color == COLOR.RED) {
RBTreeNode grandFather = parent.parent;
if (grandFather.left == parent) {
RBTreeNode uncle = grandFather.right;
// 情况1
if (uncle != null && uncle.color == COLOR.RED) {
parent.color = COLOR.BLACK;
uncle.color = COLOR.BLACK;
grandFather.color = COLOR.RED;
// 继续向上调整
cur = grandFather;
parent = cur.parent;
} else {
//uncle不存在 或者 uncle是黑色的
// 情况3:把情况3修改成情况2
if (cur == parent.right) {
rotateLeft(parent);
// 修改引用指向
RBTreeNode tmp = cur;
cur = parent;
parent = tmp;
}
// 情况2
rotateRight(grandFather);
parent.color = COLOR.BLACK;
grandFather.color = COLOR.RED;
}
} else {
// grandFather.right == parent
RBTreeNode uncle = grandFather.left;
// 情况1
if (uncle != null && uncle.color == COLOR.RED) {
parent.color = COLOR.BLACK;
uncle.color = COLOR.BLACK;
grandFather.color = COLOR.RED;
// 继续向上调整
cur = grandFather;
parent = cur.parent;
} else {
//uncle不存在 或者 uncle是黑色的
// 情况3:把情况3修改成情况2
if (cur == parent.left) {
rotateRight(parent);
// 修改引用指向
RBTreeNode tmp = cur;
cur = parent;
parent = tmp;
}
// 情况2:
rotateLeft(grandFather);
parent.color = COLOR.BLACK;
grandFather.color = COLOR.RED;
}
}
}
// 关键一步,防止向上调整的时候把根节点的颜色给修改了
root.color = COLOR.BLACK;
}
/**
* 对grandFather节点进行右单旋
* @param grandFather
*/
private void rotateRight(RBTreeNode grandFather) {
// 定义相关节点
RBTreeNode gLeft = grandFather.left;
RBTreeNode gLR = gLeft.right;
RBTreeNode gParent = grandFather.parent;
// 修改引用
grandFather.left = gLR;
grandFather.parent = gLeft;
gLeft.right = grandFather;
if (gLR != null) {
gLR.parent = gParent;
}
// 判断g是否是根节点
if (grandFather == root) {
gLeft.parent = null;
root = gLeft;
} else {
// 如果不是根节点那么就分时gParent的左节点和右节点
if (gParent.left == grandFather) {
gParent.left = gLeft;
} else {
gParent.right = gLeft;
}
gLeft.parent = gParent;
}
}
/**
* 对parent节点进行左旋
* @param parent
*/
private void rotateLeft(RBTreeNode parent) {
// 记录对应节点
RBTreeNode parentR = parent.right;
RBTreeNode parentRL = parentR.left;
RBTreeNode pParent = parent.parent;
// 修改节点
parent.right = parentRL;
// 如果parentRL存在
if (parentRL != null) {
parentRL.parent = parent;
}
parent.parent = parentR;
parentR.left = parent;
// 如果旋转的是根节点
if (parent == root) {
root = parentR;
parentR.parent = null;
} else {
// 如果旋转的不是根节点就判断旋转的是pParent的左子树还是右子树
if (pParent.left == parent) {
pParent.left = parentR;
} else {
pParent.right = parentR;
}
parentR.parent = pParent;
}
}
4. 红黑树的验证
根据红黑树的性质编写代码验证:
-
每个节点不是红色就是黑色
-
根节点是黑色的
-
如果一个节点是红色的,则它的两个孩子节点是黑色的,红黑树不能有2个连续的红色节点
-
对于每个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数目的黑色节点
-
再对红黑树进行中序遍历验证
/**
* 判断当前是否红黑树
* @return
*/
public boolean isRBTree() {
if (root == null) {
return true;
}
// 1.根节点为黑色
if (root.color != COLOR.BLACK) {
System.out.println("根节点不为黑色!");
return false;
}
// 计算某一条黑色节点个数
int checkNum = 0;
RBTreeNode cur = root;
while (cur != null) {
if (cur.color == COLOR.BLACK) {
checkNum++;
}
cur = cur.left;
}
// 中序遍历
inorderTraversal(root);
// 红黑树不能有两个连续的红色节点&& 每条路径的黑色节点个数一致
return checkRedColor(root) && checkBlackNum(root,0,checkNum);
}
/**
* 判断是否有两个连续红色节点
* @param node
* @return
*/
private boolean checkRedColor(RBTreeNode node) {
if (node == null) {
return true;
}
if (node.color == COLOR.RED) {
if (node.left != null && node.left.color == COLOR.RED) {
return false;
}
if (node.right != null && node.right.color == COLOR.RED) {
return false;
}
}
return checkRedColor(node.left) && checkRedColor(node.right);
}
/**
* 判断每条路径上的黑色节点个数
* @param root
* @param count
* @param checkNum
* @return
*/
private boolean checkBlackNum(RBTreeNode root,int count,int checkNum) {
if (root == null) {
return true;
}
if (root.color == COLOR.BLACK) {
count++;
}
if (root.left == null && root.right == null && count != checkNum) {
System.out.println("每条路径黑色节点个数不一致");
return false;
}
return checkBlackNum(root.left,count,checkNum) && checkBlackNum(root.right,count,checkNum);
}
private void inorderTraversal(RBTreeNode root) {
if (root == null) return;
inorderTraversal(root.left);
System.out.print(root.val+" ");
inorderTraversal(root.right);
}
5.性能分析
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是 O ( l o g n ) O(logn) O(logn)
-
AVL树通过左旋右旋来保证树的绝对平衡(左右子树的高度差不超过1),所以旋转的次数比较多
-
红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,通过修改颜色来降低旋转的次数。
-
红黑树对比AVL树,其通过修改颜色大大减低了旋转的次数,在增删的场景中使用红黑树更优,而AVL树只是适合查找,所以红黑树在实际运用更多的是红黑树,比如说TreeMap和TreeSet。