续哈希表那两篇博客
我们已经知道了哈希表有多么多么的牛逼,但是呢,有没有想过一个问题:对于用拉链法构造出来的哈希表,我们在查找一个元素时,还是要遍历后面的拉链。解决这个问题的一个比较好的方法就是红黑二叉树。
这篇博客分析红黑二叉树的
- 左旋
- 右旋
- 插入
参考文章:
- https://www.cnblogs.com/lycroseup/p/7325668.html
- https://blog.csdn.net/sd09044901guic/article/details/84955116#commentBox
- https://blog.csdn.net/x763795151/article/details/86583800
- https://www.cnblogs.com/skywang12345/p/3624343.html
一、红黑二叉树简介
红黑树(Red Black Tree) 是一种自平衡二叉查找树
红黑树和AVL树类似,都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。
它可以在O(log n)时间内做查找,插入和删除,这里的n 是树中元素的数目。 ——百度百科
红黑二叉树的五条性质(五条全部满足就是红黑二叉树,只要有一条不符则不是)
1. 每个结点的颜色只能是红色或黑色。
2. 根结点是黑色的。
3. 每个叶子结点都带有两个空的黑色结点(被称为黑哨兵),如果一个结点n的只有一个左孩子,那么n的右孩子是一个黑哨兵;如果结点n只有一个右孩子,那么n的左孩子是一个黑哨兵。
4. 如果一个结点是红的,则它的两个儿子都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。
5. 对于每个结点来说,从该结点到其子孙叶结点的所有路径上包含相同数目的黑结点。
二、定义结点、颜色等
public class RBNode<T extends Comparable<T>> {
RBNode<T> left; // 左孩子
RBNode<T> right; // 右孩子
RBNode<T> parent; // 父节点
boolean color; // 颜色
T key; // 关键字
public RBNode(T key, boolean color, RBNode<T> left, RBNode<T> right, RBNode<T> parent) {
this.key = key;
this.color = color;
this.left = left;
this.right = right;
this.parent = parent;
}
}
左旋和右旋比较绕,需要仔细慢慢消化理解,我花了很长时间才搞明白。
一定要结合图去写代码,否则必出错(大神除外)
三、左旋
图片源:https://blog.csdn.net/sd09044901guic/article/details/84955116#commentBox
private void leftRotate(RBNode<T> x) {
// 首先设置x的右孩子y
RBNode<T> y = x.right;
y.parent = x;
// 判断x是否有父亲
if (x.parent == null) { // 没有就把y变成父亲
y = root;
} else {
if (x == x.parent.left) { // 如果x是父亲的左孩子,就把y变成父亲的左孩子
x.parent.left = y;
} else if (x == x.parent.right) { // 同上
x.parent.right = y;
}
}
x.parent = y;
x.right = y.left; // y的左孩子变成x的右孩子
y.left = x; // x变成y的左孩子
}
四、右旋
private void rightRotate(RBNode<T> y) {
// 右旋,y是x的父亲,y变成x的儿子,x右儿子变成y的左儿子
// 设置y的左儿子x
RBNode<T> x = y.left;
x.parent = y;
if (y.parent == null) { // y没爸,就把x变成根节点
x = root;
} else {
if (y == y.parent.left) { // 如果y是它爸的左儿子,就把x变成y爸的左儿子
x = y.parent.left;
} else if (y == y.parent.right) { // 同上
x = y.parent.right;
}
}
y.parent = x; // 把x变成y的父亲
y.left = x.right; // x右儿子给y做左儿子
x.right = y; // y变成x的右儿子
}
五、插入
首先要注意一个问题,插入的元素初始颜色都是红色,因为红色不会违背性质五。这样使需要讨论的情况更少,问题更简单,
插入要分情况讨论,每种情况都要去满足红黑二叉树的五条性质
-
父亲结点是黑色
- 此时直接满足五条性质 -
父亲节点是红色(此时考虑叔叔结点)
叔叔结点为红1、父亲、叔叔变黑 2、新节点、祖父变红 3、由于祖父变红,两红不相邻,往上查找,循环使其满足性质四
叔叔结点为黑
①父亲是祖父右孩子,新节点是父亲右孩子 1、父变黑 2、祖父变红 3、祖父右旋 ②父亲是祖父左孩子,新节点是父亲右孩子 1、父亲左旋 2、交换新节点与父亲的位置 3、经过上两步就变成了①,再用①操作即可 ③父亲是祖父右孩子,新节点是父亲右孩子 1、父变黑 2、祖父变红 3、祖父左旋 ④父亲是祖父右孩子,新节点是父亲左孩子 1、父右旋 2、交换新节点与父亲位置 3、经过上两步就变成了③,再用③操作即可
//定义一些常用的方法
private RBNode<T> parentOf(RBNode<T> node) {
return node != null ? node.parent : null;
}
private boolean isRed(RBNode<T> node) {
return ((node != null) && (node.color == RED)) ? true : false;
}
private void setBlack(RBTree<T>.RBNode<T> node) {
if (node != null) {
node.color = BLACK;
}
}
private void setRed(RBNode<T> node) {
if (node != null) {
node.color = RED;
}
}
新插入的结点都先插到叶子节点处
//先进行插入,再去满足性质
private void insert(RBNode<T> node) {
int cmp;
RBNode<T> y = null;
RBNode<T> x = this.root;
// 1、将红黑树当作一颗二叉查找树,将结点添加到二叉查找树中
while (x != null) {
y = x;
cmp = node.key.compareTo(x.key);
if (cmp < 0)
x = x.left;
else
x = x.right;
}
node.parent = y;
if (y != null) {
cmp = node.key.compareTo(y.key);
if (cmp < 0) {
y.left = node;
} else {
y.right = node;
}
} else {
this.root = node;
}
// 2、设置节点的颜色为红色
node.color = RED;
// 3、将他重新修正为一颗二叉查找树
insertFixUp(node);
}
下面对插入结点后的树进行修正,使其变成合法的红黑二叉树
private void insertFixUp(RBNode<T> node) {
RBNode<T> parent, gparent;
// 当父亲节点存在并且父亲节点是黑色,则不用考虑,不影响
// 若父节点存在并且父节点是红色
while (((parent = parentOf(node)) != null) && isRed(parent)) {
/*
* 分几种情况考虑:
* 1、叔叔红色 把叔叔、父亲变成黑,祖父变红(再往上判断祖父的父亲,循环)
* 2、叔叔黑色
* (1.1)父亲是左孩子,新插入结点是左孩子
* (1.2)父亲是左孩子,新节点是右孩子
* (2.1)父亲是右孩子,新插入结点是右孩子
* (2.2)父亲是右孩子,新插入结点是左孩子
*/
gparent = parentOf(parent);
//当父亲是左孩子
if (parent == gparent.left) {
RBNode<T> uncle = gparent.right;
// case 1:叔叔结点是红色
if ((uncle != null) && isRed(uncle)) {
setBlack(uncle);
setBlack(parent);
setRed(gparent);
node = gparent;
continue;
}
//case2:叔叔是黑色,新节点是右孩子
if(parent.right == node){
RBNode<T> tmp;
leftRotate(parent);
tmp = parent;
parent = node;
node = tmp;
} //把这种情况变成case3
// case3:叔叔是黑色,新节点是左孩子
setBlack(parent);
setRed(gparent);
rightRotate(gparent);
}else{
//当父亲是右孩子
RBNode<T> uncle = gparent.left;
//case1:叔叔结点是红色
if((uncle!=null) && isRed(uncle)){
setBlack(uncle);
setBlack(parent);
setRed(gparent);
node = gparent;
continue;
}
//case2:叔叔是黑色,当前结点是左孩子
if(parent.left == node){
RBNode<T> tmp;
rightRotate(parent);
tmp = parent;
parent = node;
node = tmp;
}
//case3:叔叔是黑色,当前节点是右孩子
setBlack(parent);
setRed(gparent);
leftRotate(gparent);
}
}
//将根节点设置为黑色
setBlack(this.root);
}
插入操作相对于删除操作还要简单一点,这篇就到这,下篇学习删除操作