数据结构——手撕红黑树(开肝!)

数据结构——红黑树(开撕!)

红黑树介绍

在查找算法中,二叉搜索树是一种查找速度非常快的数据结构,平均查找事件复杂度可以达到O(log2(n))但是对于二叉搜索树,其查找性能还是不太稳定,如下面一颗二叉搜索树,如果要查找5这个结点所在的位置,查找过程依然要达到5次,也即线性查找时间,要保证查找速度稳定控制在**O(log2(n))**左右,就需要对这颗二叉搜索树维持平衡,保证其查找速度

在这里插入图片描述

所以在二叉搜索树的基础上,有了红黑树这种维护二叉搜索树平衡,保证查找速度保持在对数级别的数据结构,对于红黑树,其结点定义除了二叉搜索树的配置之外,还有一个本身颜色的标识,一颗红黑树首先必须满足是二叉搜索树,然后它有下列的规定,这是笔者在一个教程中摘录下来的

1、Every Node is BLACK or RED

2、Root node must be BLACK

3、New insections are always RED

4、Every path from root to leaf has the same number of BLACK nodes

5、No path can have two consecutive RED nodes

6、Every null node is BLACK node

总结下来就是

  • 红黑树结点只有两种颜色,红色和黑色
  • 根结点必须为黑色
  • 所有新插入的结点在重构红黑树之前都是红色
  • 从根结点到每一个叶子结点所经过的路径黑色结点的个数相同
  • 任意一条路径中都不会存在两个相连的红色结点(即红色结点的孩子结点颜色必定是黑色)
  • 空的结点被认定为黑色结点

下面有一个动画,演示了依次插入(3,1,5,7,8,9,10)这几个关键字,红黑树的变化过程
在这里插入图片描述

从上面的动画中,红黑树的重构大致细分为一些下列情况

  1. 异常出现在祖母结点的左边

    • 在父亲结点左侧插入结点

      • 父亲结点与叔叔结点颜色不同

    在这里插入图片描述

    • 父亲结点与叔叔结点颜色相同

在这里插入图片描述

  • 在父亲结点的右侧插入结点

    • 父亲结点与叔叔结点颜色不同

在这里插入图片描述

  • 父亲结点与叔叔结点颜色相同

在这里插入图片描述

  1. 异常出先在祖母结点的右边

    • 在父亲结点左侧插入结点

      • 父亲结点与叔叔结点颜色不同

在这里插入图片描述

 父亲结点与叔叔结点颜色相同

在这里插入图片描述

  • 在父亲结点右侧插入结点

    • 父亲结点与叔叔结点颜色不同

在这里插入图片描述

 父亲结点与叔叔结点颜色不同

在这里插入图片描述

肝代码

红黑树的冲突判断情况涉及到一共四个结点的判断,分别是冲突结点x,最上层结点祖母结点g,祖母结点的儿子,冲突结点的父亲父亲结点p,以及祖母结点的儿子,冲突结点的叔叔叔叔结点u,总结上述的八种情况,可以总结出一个更一般的规律,在发生冲突的情况下

  • 叔叔结点与父亲结点不同色,需要进行旋转操作
  • 叔叔结点与父亲结点同色,则需要将祖母结点与父亲结点和叔叔结点的颜色进行翻转

更复杂的红黑树的构造,也不过只是将这些步骤整合到一起,去构造一颗更复杂的红黑树,下面来分析代码实现,首先用C++来试试水

为了突出红黑树的特性,就不用泛型编程了,能省一个是一个哈哈哈,不这不是博主不会,这是为突出重心哈哈哈哈

在这里插入图片描述

数据组织

# include<iostream>
# define RED true  //树结点颜色的标识宏定义
# define BLACK false

using KeyType = int;  //关键字

typedef struct node{
    KeyType key;  
    bool color; //结点颜色标识
    node* left; //左孩子
    node* right; //右孩子
    node(KeyType key):key(key){
        color = RED;
        left = nullptr;
        right = nullptr;
    }
} RbtTree;

往红黑树中插入结点

从上面的情况分析中可以看到,需要重建红黑树的情况只有一种,那就是***往红色结点下面插入结点时***会导致冲突,需要重新平衡红黑树,即rebalance操作,当

  • 叔叔结点与父亲结点颜色不相同时,需要进行旋转操作
  • 叔叔结点与父亲结点颜色相同时,需要进行颜色翻转操作

实现这个思路,需要一些步骤

1、怎么去找到冲突位置?

  • 这个问题很好理解,当往一个红黑树的红色结点处插入一个结点时,就出现了冲突,也就是说,一条路径上不可能存在两个连着的红色结点

2、怎么对冲突情况进行区分?

  • 也就是前面说的,颜色相同与颜色不相同的问题

3、怎么在恢复当前冲突位置之后,去检查上层是否条件满足,逐级检查上去?

  • 可以从上到下,逐级恢复上去

那么大致思路就是这样,首先按照二叉搜索树的插入方法,将一颗红色结点插入,然后对这棵树进行rebalance,从根往下找冲突,并且从下面往上面解决冲突

1、首先是集成起来的外部接口,传入的参数root一定是根结点,在中间调用了插入二叉搜索树结点和重新恢复红黑树的方法,并在最后维护根结点为黑色

//首先是insertNode,它的结构如下
void insertNode(RbtTree *&root,KeyType key){
    if(root == nullptr){
        root = new RbtTree(key);
    }else{
        if(key < root->key){
            insert(root->left,key); //依照二叉搜索树的形式先插入结点
        }else if(key > root->key){
            insert(root->right,key);
        }
    }
    rebalance(root);  //对以root为根结点的红黑树进行维护
    //维护根结点为黑色的情况
    root->color = BLACK;
}

2、insert就是简简单单的插入二叉搜索树的方法

void insert(RbtTree *&root,KeyType key){
    if(root == nullptr){
        root = new RbtTree(key);
    }else{
        if(key < root->key){
            insert(root->left,key);
        }else if(key > root->key){
            insert(root->right,key);
        }
    }
}

3、第三个也是最难理解的,代码也是最长的…

img

依照当前的思路,从下往上维护红黑树,下面的语句也就是对上面分析的八种情况的操作,可能这样的写法有的不太简洁,但是按照逻辑来还是比较好理解

img

void rebalance(RbtTree *&root){
    //从根结点开始查找冲突结点
    if(root == nullptr)
        return;
    else{
        rebalance(root->left);  
        rebalance(root->right);
        //(1)、在祖母的左边插入结点导致的不平衡
          //a、在(1)的条件下在父亲的左边插入结点
            //1、在(1)a的条件下父亲与叔叔的颜色不同,叔叔为nil
        if(root->left != nullptr && root->color == RED
        && root->left->left != nullptr&& root->left->left->color == RED
        && (root->right == nullptr)){
            //先进行父祖换色,然后进行右旋操作
            root->left->color = BLACK;
            root->color = RED;
            RbtTree *p = root->left;
            root->left = p->right;
            p->right = root;
            root = p;
        }
            //2、在(1)a的条件下,父亲与叔叔的颜色相同,都为红色结点
        else if(root->left != nullptr && root->color == RED
        && root->left->left != nullptr&& root->left->left->color == RED
        && root->right != nullptr && root->right->color == RED){
            //进行颜色翻转
            root->color = RED;
            root->left->color = BLACK;
            root->right->color = BLACK;
        }
          //b、在(1)的条件下,在父亲的右边插入结点
            //1、在(1)b的条件下,父亲与叔叔的颜色不同,叔叔为nil或者黑色结点
        else if(root->left != nullptr && root->left->color == RED
        && root->left->right != nullptr && root->left->right->color == RED
        && root->right == nullptr){
            //先交换子祖颜色,然后左旋再右旋
            root->color = RED;
            root->left->right->color = BLACK;
            RbtTree *p = root->left->right;
            root->left->right = root->left->right->left;
            root->left = p->right;
            p->left = root->left;
            p->right = root;
            root = p;
        }
            //2、在(1)b的条件下,父亲与叔叔颜色相同,都为红色
        else if(root->left != nullptr && root->left->color == RED
        && root->left->right != nullptr && root->left->right->color == RED
        && root->right != nullptr && root->right->color == RED){
            //颜色翻转
            root->color = RED;
            root->left->color = BLACK;
            root->right->color = BLACK;
        }
        //(2)、在祖母的右边插入结点导致的不平衡
          //a、在(2)的条件下在父亲的右边插入结点
            //1、在(2)a的条件下,父亲与叔叔的颜色不同,叔叔为nil
        else if(root->right != nullptr && root->right->color == RED
        && root->right->right != nullptr && root->right->right->color == RED
        && root->left == nullptr){
            //父祖换色,左旋操作
            root->color = RED;
            root->right->color = BLACK;
            RbtTree *p = root->right;
            root->right = p->left;
            p->left = root;
            root = p;
        }
            //2、在(2)a的条件下,父亲与叔叔的颜色相同,都为红色
        else if(root->right != nullptr && root->right->color == RED
        &&root->right->right != nullptr && root->right->right->color == RED
        &&root->left != nullptr && root->left->color == RED)  {
            //颜色翻转
            root->color = RED;
            root->left->color = BLACK;
            root->right->color = BLACK;
        }
          //b、在(2)的条件下,在父亲的左边插入结点
            //1、在(2)b的条件下,父亲与叔叔的颜色不同,叔叔为nil
        else if(root->right != nullptr && root->right->color == RED
        &&root->right->left != nullptr && root->right->left->color == RED
        &&root->left == nullptr){
            //子祖换色,先右旋再左旋操作
            root->color = RED;
            root->right->left->color = BLACK;
            RbtTree *p = root->right->left;
            root->right->left = p->right;
            p->right = root->right;
            root->right = p->left;
            p->left = root;
            root = p;
        }
            //2、在(2)b的条件下,父亲与叔叔的颜色相同,都为红色
        else if(root->right != nullptr && root->right->color == RED
        && root->right->left != nullptr && root->right->left->color == RED
        &&root->left != nullptr && root->left->color == RED){
            //颜色翻转
            root->color = RED;
            root->left->color = BLACK;
            root->right->color = BLACK;
        }
        else
            return;
    }
}

4、然后就是测试方法了,我写了三个遍历,先序、中序、后序

void midOrder(RbtTree *root){
    if(root == nullptr)
        return;
    midOrder(root->left);
    if(root->color){
        cout<<"("<<root->key<<" , "<<"RED)  ";
    }else{
        cout<<"("<<root->key<<" , "<<"BLACK)  ";
    }
    midOrder(root->right);
}

void prevOrder(RbtTree *root){
    if(root == nullptr)
        return;
    if(root->color){
        cout<<"("<<root->key<<" , "<<"RED)  ";
    }else{
        cout<<"("<<root->key<<" , "<<"BLACK)  ";
    }
    prevOrder(root->left);
    prevOrder(root->right);
}

void lasteOrder(RbtTree *root){
    if(root == nullptr)
        return;
    lasteOrder(root->left);
    lasteOrder(root->right);
    if(root->color){
        cout<<"("<<root->key<<" , "<<"RED)  ";
    }else{
        cout<<"("<<root->key<<" , "<<"BLACK)  ";
    }
}

5、最后在main函数上测试上面git动图上面的红黑树插入状态,并观察其插入情况,发现运行结果与gif动图中演示的红黑树插入过程十分吻合

#include "RBTree.h"
#include<iostream>
using namespace std;

void shuchu(RbtTree *roots){
    RbtTree *root = roots;
    cout<<"##############\n";
    prevOrder(root);
    cout<<endl;
}

int main(){
    RbtTree *root = nullptr;
    insertNode(root,3);
    shuchu(root);
    insertNode(root,1);
    shuchu(root);
    insertNode(root,5);
    shuchu(root);
    insertNode(root,7);
    shuchu(root);
    insertNode(root,8);
    shuchu(root);
    insertNode(root,9);
    shuchu(root);
    insertNode(root,10);
    shuchu(root);
    return 0;
}

运行结果

在这里插入图片描述

如何去验证一颗树是红黑树?

对于这个问题,抓住前面对红黑树性质的规定,对于一颗红黑树

  • 任意一条路径不会存在两个相连的红色结点
  • 从根结点出发到叶子结点总会经过相同的黑色结点数量

也就是说,一颗树要满足红黑树,也就必须满足上面两个条件,缺一不可,相当于这两个条件求与,可以先写两个判断方法,一个方法判断一个条件,最后综合到一起

//判断任意一条路径是否存在相连的两个红色结点,如果存在返回true,如果不存在返回false
bool hasConnectRedNode(RbtTree *root){
    if(root == nullptr)
        return false;
    else{
        if(root->color == BLACK){
            return hasConnectRedNode(root->left) || hasConnectRedNode(root->right);
        }else{
            if(root->left != nullptr && root->left->color == RED){
                return true;
            }else if(root->right != nullptr && root->right->color == RED){
                return true;
            }else{
                return hasConnectRedNode(root->right) || hasConnectRedNode(root->left);
            }
        }
    }
}
//从根结点出发,访问所有叶子结点,看经过的黑色结点数量是否都相等
/**
 * 因为要满足从根结点出发到叶子结点的所有路径经过的黑色结点的数量必须是一个常数
 * 那么先走一条路径,把这个常数求出来,然后再去跟其它路径对比
 * @param root
 * @return
 */
bool isPathCommonBlackNode(RbtTree *root){
    if(root == nullptr)
        return true;
    int nodecount = 0;
    RbtTree *q = root;
    while(q != nullptr){
        if(q->color == BLACK)
            nodecount++;
        q = q->left;
    }
    return isPathCommonBlackNode(root,0,nodecount);
}


bool isPathCommonBlackNode(RbtTree *root, int i, const int nodecount) {
    if(root == nullptr){
        return true;
    }
    else{
        if(root->color == BLACK){
            i++;
        }
        if(root->left == nullptr && root->right == nullptr){
            if(i != nodecount)
                return false;
            else
                return true;
        }else{
            return isPathCommonBlackNode(root->left,i,nodecount) && isPathCommonBlackNode(root->right,i,nodecount);
        }
    }
}

最后将这两个方法综合到一起

//判断一个结点是不是红黑树
bool isRBTree(RbtTree *root){
    return !hasConnectRedNode(root) && isPathCommonBlackNode(root);
}

然后把添加的方法再次放到main函数中进行测试

#include "RBTree.h"
#include<iostream>
using namespace std;

void shuchu(RbtTree *root){
    cout<<"##############\n";
    prevOrder(root);
    cout<<endl;
    if(isRBTree(root)){
        cout<<"is RBTree\n";
    }else{
        cout<<"isn't RBTree\n";
    }
}

int main(){
    RbtTree *root = nullptr;
    insertNode(root,3);
    shuchu(root);
    insertNode(root,1);
    shuchu(root);
    insertNode(root,5);
    shuchu(root);
    insertNode(root,7);
    shuchu(root);
    insertNode(root,8);
    shuchu(root);
    insertNode(root,9);
    shuchu(root);
    insertNode(root,10);
    shuchu(root);
    return 0;
}

得到的结果为

在这里插入图片描述

在写的过程中,笔者又对上面红黑树插入结点的代码进行了优化,因为有些条件可以整合到一起,因为他们的操作是一样的,但是保留了上面的代码,是为了让思路清晰一点,从上面分析的条件中可以看到,对于叔叔和父亲相同颜色的情况,只需要将颜色翻转即可

在这里插入图片描述

void rebalance1(RbtTree *&root){
    //从根结点开始查找冲突结点
    if(root == nullptr)
        return;
    else{
        rebalance(root->left);
        rebalance(root->right);
        //(1)、在祖母的左边插入结点导致的不平衡
        //a、在(1)的条件下在父亲的左边插入结点
        //1、在(1)a的条件下父亲与叔叔的颜色不同,叔叔为nil
        if(root->left != nullptr && root->color == RED
        && root->left->left != nullptr&& root->left->left->color == RED
        && (root->right == nullptr)){
            //先进行父祖换色,然后进行右旋操作
            root->left->color = BLACK;
            root->color = RED;
            RbtTree *p = root->left;
            root->left = p->right;
            p->right = root;
            root = p;
        }
        //b、在(1)的条件下,在父亲的右边插入结点
        //1、在(1)b的条件下,父亲与叔叔的颜色不同,叔叔为nil或者黑色结点
        else if(root->left != nullptr && root->left->color == RED
        && root->left->right != nullptr && root->left->right->color == RED
        && root->right == nullptr){
            //先交换子祖颜色,然后左旋再右旋
            root->color = RED;
            root->left->right->color = BLACK;
            RbtTree *p = root->left->right;
            root->left->right = root->left->right->left;
            root->left = p->right;
            p->left = root->left;
            p->right = root;
            root = p;
        }
        //(2)、在祖母的右边插入结点导致的不平衡
        //a、在(2)的条件下在父亲的右边插入结点
        //1、在(2)a的条件下,父亲与叔叔的颜色不同,叔叔为nil
        else if(root->right != nullptr && root->right->color == RED
        && root->right->right != nullptr && root->right->right->color == RED
        && root->left == nullptr){
            //父祖换色,左旋操作
            root->color = RED;
            root->right->color = BLACK;
            RbtTree *p = root->right;
            root->right = p->left;
            p->left = root;
            root = p;
        }
        //b、在(2)的条件下,在父亲的左边插入结点
        //1、在(2)b的条件下,父亲与叔叔的颜色不同,叔叔为nil
        else if(root->right != nullptr && root->right->color == RED
        &&root->right->left != nullptr && root->right->left->color == RED
        &&root->left == nullptr){
            //子祖换色,先右旋再左旋操作
            root->color = RED;
            root->right->left->color = BLACK;
            RbtTree *p = root->right->left;
            root->right->left = p->right;
            p->right = root->right;
            root->right = p->left;
            p->left = root;
            root = p;
        }
        else if(root->left != nullptr && root->left->color == RED
        && root->right != nullptr && root->right->color == RED
        &&((root->right->left != nullptr && root->right->left->color == RED)
        ||(root->right->right != nullptr && root->right->right->color == RED)
        ||(root->left->right != nullptr && root->left->right->color == RED)
        ||(root->left->left != nullptr && root->left->left->color == RED)
        )){
            //进行颜色翻转
            root->color = RED;
            root->left->color = BLACK;
            root->right->color = BLACK;
        }
        else
            return;
    }
}

最后得到的测试结果也是一样的

在这里插入图片描述

最后的话

红黑树是查找算法中喜欢被用到的一种数据结构,比如像Java中的ConcurrentHashMap,和一些架构中,也是面试官经常问的一个问题,它有着十分优异的性能,虽然平均查找速度比平衡二叉树差了点,但是整体性能包括重构红黑树的性能是打爆平衡二叉树的,而且本次的案例分析仅仅是作者看了一些教程之后自己总结出来的,可能它的插入情况还没考虑全面,测试案例过少,欢迎大佬批评指正!

img

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值