挑战全网最详细 红黑树 教程, 手把手教你手撕 红黑树

826 阅读8分钟

背景: 红黑树是一种相对平衡的二叉树。之所以相对平衡,是因为它是一个"黑平衡"的二叉树,并不绝对平衡(AVL 绝对平衡)相较于AVL, 红黑树的查询速度略慢, 但维护起来没有AVL代价那么高,所以被更加广泛使用。java 中的 hashmap, 当发生 hash 冲突时, 就使用了红黑树。红黑树极其复杂,网上很少有彻底讲通的教程,很多教程一上来就讲红黑树的定义,让人看了十分的头大。为此专门写一篇技术贴。完整代码可以访问我的 GitHub, 也欢迎大家底下留言。

2-3 树 与 红-黑 树

要想讲清楚什么是红黑树,就得从2-3树讲起。红黑树,本质上就是2-3树

什么是2-3树?

一句话,2-3树一个绝对平衡的多叉树,每一个节点的左右子树高度差不超过1。

2-3树中有两种节点。一种是2节点,与二分搜索树中的节点相同,每个2节点有两个子树。另一种是3节点,每个节点包含两个元素,每个节点有三个子树。 假设依次向2-3树中添加 21,17,15,13,11节点,整个过程如下:

image.png

当只有一个节点“21”时,这就是一个普通的二分搜索树,只有一个2节点。随后加入新节点“17”,“17”选择与“21”融合,形成一个3节点。继续加入新节点“15”,“15”先选择与现有的3节点融合形成4节点,随后分裂,形成3个2节点。继续加入新节点“13”,新节点直接与“15”融合形成一个3节点。接续加入新节点“11”, 新节点“11”先于“13”“15”融合形成一个4节点,随后分裂成三个 2节点,“13”节点继续向上融合,与上一层的“17”节点融合成3节点。

可以看出,即使我们以最极端的方式,从大到小(或是 从小到大)插入节点,2-3树依旧能保持平衡。

总结一下2-3树的操作规则:

  1. 新节点加入的时候永远选择与现有的节点进行融合

  2. 若融合成一个3节点,则到此为止

  3. 若融合成一个4节点(一个节点有三个元素),则进行分裂,分裂出来的上面一个节点向上继续融合,进入步骤1。

红-黑 树 的本质

红-黑树 本质上 就是将 2-3树 转换为 二叉树。其中的红色节点就是 2-3 树中的3节点的 左边一个节点,且红色节点是 2-3 树中的3节点的右边节点的左孩子

根据形象化地对比 红-黑 树 与 2-3 树可以很轻松地得出红黑树具有以下特征:

  • 红黑树的根节点一定是黑色。
  • 红黑树是一个相对平衡的二叉树,或者说是一个绝对 黑平衡 的二叉树,任意节点的左右子树的 黑高度 相等,任意两个叶子节点到他们的祖先节点经过的黑节点个数一定相等。

红-黑 树的实现

构造方法

红黑树的构造方法部分与二叉树相差不大,对于每一个节点额外用了一个 color 变量来表示节点的颜色。特别地,我们将新节点永远设置成红色节点,这样做的目的是为了在与2节点,三节点融合时标志出来,后续操作再根据实际情况设置颜色。

public class  RBTree<K extends Comparable<K>,V> {

    private static final boolean RED=true;
    private static final boolean BLACK=false;

    private class Node{
    
        public K key;
        public V value;
        public Node left,right;
        public boolean color;

        public Node(K key, V value){
            this.key=key;
            this.value=value;
            left=null;
            right=null;
            color=RED;//2-3树插入一个节点,永远先去和叶子节点去融合
        }

        public Node(){
            this.key=null;
            this.value=null;
            left=null;
            right=null;
        }
    }

    private Node root;
    private  int size;

    public RBTree(){
        root=null;
        size=0;
    }
    
}

其他常用方法

红黑树额外多了一个 getColor 方法用来返回节点 颜色

private boolean getColor(Node node){
    if (node==null){
        return BLACK;
    }
    return  node.color;
}

下面几个方法与一般的二分搜索树一样,不做解释

public boolean contains(K key) {
    return contains(root ,key);
}

private boolean contains(Node node,K key){
    if (node==null){
        return false;
    }
    if (key.compareTo(node.key)==0){
        return true;
    }else if (key.compareTo(node.key)<0){
        return contains(node.left,key);
    }else {
        return contains(node.right,key);
    }
}


public V get(K key) {
    Node retNode=getNode(root,key);
    if (retNode!=null){
        return retNode.value;
    }else {
        return null;
        }
}

private Node getNode(Node node,K key) {
    if (node==null){
        return null;
    }
    if (key.compareTo(node.key)==0){
        return node;
    }else if (key.compareTo(node.key)<0){
        return getNode(node.left,key);
    }else {
        return getNode(node.right,key);
    }
}


public void set(K key, V newValue) {
    Node setNode=getNode(root,key);
    if (setNode!=null){
        setNode.value=newValue;
    }else{
        throw new IllegalArgumentException(key + "doesn't exist!");
    }
}


public int getSize() {
    return size;
}


public boolean isEmpty() {
    return size==0;
}

向 红-黑 树 中增加节点

上一节说过 红-黑 树 本质上就是 2-3 树的二叉树表现形式。向红黑树中增加节点本质上就是向2-3树中增加节点。那么根据新节点的插入位置可以分为以下5中情况:

  1. 在二节点的左侧插入

  2. 在二节点的右侧插入

  3. 在三节点左侧插入

  4. 在三节点中间插入

  5. 在三节点右侧插入

在二节点左侧插入

这种情况最简单,直接与二节点进行合并形成一个三节点,在红黑树中只需要将新节点设置成红色即可。

image.png

特别的是,这里的 “37” 节点仍有左右子树,这是因为 “37” 节点可能是新加入的节点,也可能是下面节点向上进行融合的节点。这是不失一般性地表示。

在二节点的右侧插入

在二节点的右侧插入,需要与二节点融合成一个三节点。根据 红颜色 的节点定义,红节点是三节点左侧的节点,且红色节点是 2-3 树中的3节点的右边节点的左孩子。为此,需要对三节点的左侧节点进行左旋操作,然后再更改三节点中左右两个节点的颜色,将左侧设置为红色,将右侧设置为黑色。整个过程如下图所示:

image.png

特别的是,这里的 “42” 节点仍有左右子树,这是因为 “42” 节点可能是新加入的节点,也可能是下面节点向上进行融合的节点。这是不失一般性地表示。

左旋操作代码


private Node leftRotate(Node node){
        
    Node x=node.right;
    Node T1=node.left;
    Node T2=x.left;
    Node T3=x.right;

    x.left=node;
    node.right=T2;

    return x;
 }

在三节点的左侧插入

在三节点的左侧插入,融合成一个四节点。四节点的中间节点与两侧节点分离,两侧节点形成新的二节点,中间节点继续向上,与上面一层的节点进行融合。为此需要将中间节点设置为红色,两侧节点设置为黑色。在对应的红黑树中,需要对四节点的右侧节点进行右旋,然后进行颜色变换,将两侧节点设置为黑色,中间节点设置为红色,继续与上一层的节点进行融合。整个过程如下图所示:

image.png

右旋操作代码

private Node rightRotate(Node node){
    Node x=node.left;
    Node T1=x.right;
    Node T2=node.right;

    x.right=node;
    node.left=T1;

    return x;
}

在三节点中间插入

在三节点中间插入新节点,融合成一个四节点。四节点的中间节点与两侧节点分离,两侧节点形成新的二节点,中间节点继续向上,与上面一层的节点进行融合。为此需要将中间节点设置为红色,两侧节点设置为黑色。在对应的红黑树中,需要对四节点的左侧节点进行左旋,再对原四节点的右侧节点进行右旋。然后进行颜色变换,将两侧节点设置为黑色,中间节点设置为红色,继续与上一层的节点进行融合。整个过程如下图所示:

image.png

在三节点右侧插入

在三节点中间插入新节点,融合成一个四节点。四节点的中间节点与两侧节点分离,两侧节点形成新的二节点,中间节点继续向上,与上面一层的节点进行融合。为此需要将中间节点设置为红色,两侧节点设置为黑色。在红黑树中,直接进行颜色变换即可,无需额外操作。整个过程如下图所示:

image.png

至此增加节点的所有情况都已经分析完毕, 我们将其整合到一起就是这样:

public void add(K key, V value) {
    root=add(root, key,value);
    root.color=BLACK;
}

//向以node为根的二分搜索树中加入e
//并且返回node
private Node add(Node node, K key, V value){
    if (node==null){
        size++;
        return new Node(key,value);
    }


    if (key.compareTo(node.key)==0){
        node.value=value;
    }else if (key.compareTo(node.key)<0 ){
        node.left=add(node.left,key,value);
    }else {
        node.right=add(node.right,key,value);
    }

    //node字树改变了,需要维护平衡

    //2节点左侧插入
    if (getColor(node.left)==RED  ){
            return node;
    }

    //2节点右侧 插入
    if (getColor(node)==BLACK && getColor(node.right)==RED){
        node=leftRotate(node);
        node.color=BLACK;
        node.left.color=BLACK;
    }

    //3节点左侧插入
    if (getColor(node.left)==RED  && getColor(node.left.left)==RED  ){
        node=rightRotate(node);
        //改变颜色
        node.color=RED;
        node.left.color=BLACK;
        node.right.color=BLACK;
        return node;
    }

    //3节点中间插入
    if (getColor(node)==BLACK && getColor(node.left)==RED && getColor(node.left.right)==RED){
        node.left=leftRotate(node.left);
        node=rightRotate(node);
        node.left.color=BLACK;
        node.right.color=BLACK;
        node.color= RED;
        return node;
    }

    //3节点右侧插入
    if (getColor(node)==BLACK && getColor(node.left)==RED && getColor(node.right)==RED){
        node.color=RED;
        node.left.color=BLACK;
        node.right.color=BLACK;
        return node;
    }

    //其他情况,说明当前节点的子树没有节点向上融合,直接return
    return  node;
}

我们可以看出,与二分搜索树相比,红黑树主要多出了在各种插入新节点的情况下,维护树的黑平衡。特别的是,每当插入新节点时,都需要逐层检查,看下层是否有新节点对与当层节点进行融合。

删除节点

红黑树删除节点更加复杂, 以后有空了再更新hhh。相信大家看到这里,已经完全明白了为什么要设计红黑树,红黑树如何维护平衡了哈