前面讲了AVL平衡树的实现https://blog.csdn.net/weixin_43696529/article/details/104701374,但由于AVL是高度平衡的树(高度差小于等于1),而红黑树是根据颜色来不严格的实现平衡,因此在插入和删除节点时,红黑树的调整次数较少,尤其是在大量数据面前时,红黑树的效率会更高。
一、定义介绍
首先我们需要知道什么是2-节点,什么是3-节点。
2-节点就是我们常说的二叉节点,一个节点两个链接
3-节点则是两个节点三个链接
红黑树中,树的链接有两种类型:红链接和黑链接
一个红链接将两个2-节点连接起来,形成一个3-节点
黑链接就是一个普通连接,如下:
上面的2-3树对应下面的红黑树
定义如下:
1.红链接都是左链接,不允许为右链接(仅因减小代码量)
2.没有任何一个节点同时和两条红链接相连
3.该树是完美黑色平衡的,即任意空连接到根节点的路径上的黑链接数量相同。
二、数据结构
如下:
每个节点增加了一个布尔类型color变量,表示指向该节点的链接的颜色,color的取值为RED(true)
和BLACK(false)
public class RedBlackTree<Key extends Comparable<Key>,Value> {
private static final boolean RED=true;
private static final boolean BLACK= false;
private Node root;
private class Node{
// 左右孩子
private Node left,right;
/**
* 其父节点指向该节点的链接颜色
*/
private boolean color;
//键
private Key key;
//键关联的值
private Value value;
/**
* 子树中的节点总数
*/
private int size;
public Node( boolean color,Key key,Value value, int size) {
this.key=key;
this.color = color;
this.value = value;
this.size = size;
}
}
}
三、旋转操作
在对红黑树进行操作时,难免会出现连续的两个红链接相连,红链接可能是连续两个左链接,也可能是一个右链接,因此我们需要旋转链接进行修复。
红黑树的旋转一共有两个情况:
1.对一个右红链接左旋转
如下图,对右链接为红色的节点f进行左旋转,变为下面一个图:
代码如下:
代码如下:
/**
* 左旋转(右链接为红色)
* @param node
* @return
*/
private Node rotateLeft(Node node){
//这里旋转逻辑同AVL
Node right=node.right;
node.right=right.left;
right.left=node;
//但是需要修改两个节点的颜色
//将右节点的颜色改为node的颜色
right.color=node.color;
//将node 的颜色改为红色,因为是把红链接旋转过来,这里必然是红色
node.color=RED;
right.size=node.size;
node.size=1+size(node.left)+size(node.right);
return right;
}
2.对一个左红链接右旋转
如下图,对左链接为红色的节点b进行右旋转,变为下面一个图:
代码如下:
逻辑和上面一样,相反而已
/**
* 右旋转(左链接为红色)
* @param node
* @return
*/
private Node rotateRight(Node node){
Node left=node.left;
node.left=left.right;
left.right=node;
left.color=node.color;
node.color=RED;
left.size=node.size;
node.size=1+size(node.left)+size(node.right);
return left;
}
四、插入操作
我们在插入一个节点的时候,默认让其颜色为红色,这样就会出现以下几种情况:
当插入到一个2-节点时:
此时有两种情况:
- 指向新节点的链接是父节点的左链接:
此时父链接直接成了一个3-节点,如下图,插入节点a,此时a与父节点b成为一个3-节点
- 指向新节点的链接是父节点的右链接:
此时是一个错误的3-节点,因为我们规定红链接必须的左链接,因此我们需要对其进行左旋转:
如下图新插入了一个节点c,指向它的链接为父链接节点b的右链接
对齐进行左旋转修正,如下图:
当插入到一个3-节点时:
1.插入的是3-节点的右链接
此时需要将两个链接都变为黑色:
这个过程叫做颜色转换:
private void flipColors(Node node){
node.color = !node.color;
node.left.color = !node.left.color;
node.right.color = !node.right.color;
}
书上给的是让node变为红色,左右孩子变为黑色,但因为后面还要用到这个方法,并且是转换为与原来颜色对应的颜色,所以这里直接写成这样,同样适用。
在我们每次插入后都会让根节点变为黑色,而如果根节点从红变为黑是,就意味着其高度加1
2.插入的是3-节点的中链接
此时需要先对节点a进行左旋转变成右边的图,然后再对节点c进行右旋转,如下:
此时变成了第一种情况,按第一种情况继续处理即可
3.插入的是3-节点的左链接
如上图,插入节点a,此时变成了第二种情况左旋转后的状态,按上面的步骤处理即可。
综合可以得出插入算法的步骤:
1.如果插入的是2-节点,只需按2-节点的两种情况操作即可
2.如果插入的是3-节点,则临时创建了一个4-节点,我们需要将其分解并将红链接向上传递,直到遇到一个2-节点或根节点。观察上面的几种方法,都是为了完成这个目标。
而在代码中的体现就是:
1.如果右孩子是红色,左孩子为黑色,则左旋转
2.如果左孩子和左孩子的左孩子都是红色, 则进行右旋转
3.如果左孩子和右孩子都是红色,则进行颜色转换
观察上述3点,其可以涵盖我们上面的任意情况。
插入算法如下:
public void put(Key key,Value value){
if (key == null) throw new IllegalArgumentException("key为空");
if(value==null){
delete(key);
return;
}
root=put(root,key,value);
//插入后将根节点颜色变为黑色
root.color=BLACK;
}
private Node put(Node node,Key key,Value value) {
if (node==null){
//插入的节点,总是让它的颜色初始化为红
return new Node(RED,key,value,1);
}
int result=key.compareTo(node.key);
if (result<0){
node.left=put(node.left,key,value);
}else if (result>0){
node.right=put(node.right,key,value);
}else {
node.value=value;
}
/*
右链接为红色
1.即向2-节点右边插入
2 或是 向3-节点中间插入
*/
if (isRed(node.right)&&!isRed(node.left)){
//左旋转
node=rotateLeft(node);
}
//3.指向新节点的链接为3-节点的左链接
//4.或是情况2左旋转后的状态
if (isRed(node.left)&&isRed(node.left.left)){
//右旋转
node=rotateRight(node);
}
//5.左右链接均为红,即成为一个4-节点(3、4 右旋转后的状态)
if (isRed(node.left)&&isRed(node.right)){
//转换颜色
flipColors(node);
}
node.size=size(node.left)+size(node.right)+1;
return node;
}
private int size(Node node) {
return node==null?0:node.size;
}
判断当前节点是否被红链接指向:
五、删除最小值
删除操作比较麻烦,也是看的很久才看懂(hhhh)
如果待删除的节点是一个3-节点,那么直接删除就好了,但是如果是一个2-节点,删除后便后印象树的结构,因此删除最小值的思路如下:
从根节点开始向下寻找最小值,路径上的每一个节点都需要满足以下条件之一:
1.当前节点的左孩子是3-节点,过
2.当前节点的左孩子是2-节点,但是兄弟节点是3-节点,此时可以向兄弟节点借一个过来,保证自己不是2-节点;
3.当前节点左孩子右孩子都是2-节点,则向父节点借一个,并将借的节点和左孩子右孩子合并。
按如上操作遍历到最小值处,此时最小值就在一个3-节点或者4-节点中,删除接即可。然后就可以自底向上修复临时的4-节点(同前面的步骤)。
public void delMin(){
if (isEmpty()){
throw new NoSuchElementException("树为空");
}
if(!isRed(root.left) && !isRed(root.right)){
root.color = RED; // 如果根节点的左右子节点是2-节点,我们要先根设为红的,这样才能进行后面的moveRedLeft操作,因为左孩子要从根节点借一个
}
root = delMin(root);
root.color = BLACK; // 借完以后,我们将根节点的颜色复原
}
private Node delMin(Node node) {
if (node.left==null){
return null;
}
if (!isRed(node.left) && !isRed(node.left.left)){
// node的左节点如果是2-节点,则按上面的方法编程3-节点或是临时4-节点
node=moveRedLeft(node);
}
node.left=delMin(node.left);
return balance(node); // 平衡临时组成的4-节点
}
以下是2-节点变3-节点或4-节点的方法:
private Node moveRedLeft(Node node) {
/**
* 因为我们规定红链接只能在左,
* 因此当前节点的左右子节点都是2-节点,这时候我们就需要通过颜色转换,将这三个节点合并在一起
*/
flipColors(node);
//如果兄弟节点为2-节点的话,那么到上一步就结束了
if(isRed(node.right.left)){ // 而如果兄弟节点不是2-节点的话,我们就需要通过旋转从兄弟节点借一个过来
node.right = rotateRight(node.right);
node = rotateLeft(node);
// 因为条件2要求我们只向兄弟节点借一个,
// 而一开始从父节点那里借了一个,因此需要还一个给父节点
flipColors(node);
}
return node;
}
删除完毕后,我们需要自顶向上分解临时的4-节点:
以下代码和上面的put最后几个if相同,只是在开始添加了一个条件,但是这个条件去掉后也不影响,因为
如果第一个if左旋转后,第二个if必然不会再走,因为左孩子必然是红色;
如果去掉第一个if,如果左黑右红,则进行左旋转,如果左红右红,那就属于最后一个情况,转换颜色即可,不影响结果
private Node balance(Node node){
if (isRed(node.right)) {
node = rotateLeft(node);
}
if (isRed(node.right) && !isRed(node.left)) {
node=rotateLeft(node);
}
if (isRed(node.left) && isRed(node.left.left)) {
node=rotateRight(node);
}
if (isRed(node.left) && isRed(node.right)) {
flipColors(node);
}
node.size = size(node.left)+size(node.right)+1;
return node;
}
六、删除最大值
逻辑同删除最小值相同,方向相反。
public void delMax(){
if (isEmpty()){
throw new NoSuchElementException("树为空");
}
if (!isRed(root.left) && !isRed(root.right)){
root.color=RED;
}
root=delMax(root);
root.color=BLACK;
}
在删除中需要添加一个左孩子是否为红链接的判断,
因为最大值要么不存在子孩子,要么最多存在一个左链接(从以上几个平衡旋转可以发现,最大值节点一定是这两个情况)
如果有左红链接,则应该将此节点右旋转,让最大值没有一个孩子,这样就可以直接删除,否则会破坏树的结构,丢失该左孩子
private Node delMax(Node node) {
if(isRed(node.left)){
node = rotateRight(node);
}
if (node.right==null){
return null;
}
if (!isRed(node.right) && !isRed(node.right.left)){
node=moveRedRight(node);
}
node.right=delMax(node.right);
return balance(node);
}
七、删除任意key
删除操作则就是将删除最小值和删除最大值组合在一起,但需要考虑key不在树底的情况,此时按二叉查找树的删除逻辑删除即可,具体如下:
public void delete(Key key) {
if (key == null) throw new IllegalArgumentException("argument to delete() is null");
if (!contains(key)) return;
if (!isRed(root.left) && !isRed(root.right))
root.color = RED;
root = delete(root, key);
if (!isEmpty()) root.color = BLACK;
}
private Node delete(Node node, Key key) {
if (key.compareTo(node.key) < 0) {
//key在左子树,按删除最小值一样删除
if (!isRed(node.left) && !isRed(node.left.left))
node = moveRedLeft(node);
node.left = delete(node.left, key);
}
else {//key在右子树,按删除最大值一样删除
if (isRed(node.left))
node = rotateRight(node);
//需首先判断待删除的节点是否在树底,否则下一步的node.right.left会出现空指针
if (key.compareTo(node.key) == 0 && (node.right == null))
return null;
//递归在右子树中删除
if (!isRed(node.right) && !isRed(node.right.left))
node = moveRedRight(node);
//如果待删除的节点不在树底
if (key.compareTo(node.key) == 0) {
//像二叉查找树一样删除,从node右子树找到最小的节点放在当前位置,然后再将该最小节点从右子树删除
Node x = min(node.right);
node.key = x.key;
node.value = x.value;
node.right = delMin(node.right);
}
else {
//key仍然大于该node,继续在右子树递归
node.right = delete(node.right, key);
}
}
return balance(node);
}
获取给定键的值,同前面二叉查找树相同。
public Value get(Key key){
if (key==null){
return null;
}
return get(root,key);
}
private Value get(Node node, Key key) {
while (node!=null){
int result=key.compareTo(node.key);
if (result<0){
node=node.left;
}else if (result>0){
node=node.right;
} else {
return node.value;
}
}
return null;
}
总结:
1.一颗大小为N的红黑树的高度不会超过2lgN
2.红黑树最坏情况下的运行时间的增长数量级:
查找:2lgN
删除:2lgN
平均情况:
查找:lgN
插入:lgN
3. AVL树追求完美平衡,读取的性能略高;但维护较慢,空间开销较大。
红黑树的读取性能略低(但最多也就多比较一次左右),空间复杂度同AVL差不多,但数据量大时,红黑树的综合性能更高
4.因此在查找大于插入或删除的场景时,使用AVL树,如果次数都差不多,就用红黑树。**