数据结构——平衡树之红黑树

一、2-3树

在了解什么是红黑树之前,首先需要补充一下什么是2-3树,因为它有助于我们对红黑树的理解,包括对B类树的理解。

2-3树可以有两个孩子或三个孩子,所以也就被称为2-3树,且2-3满足二分搜索树的基本性质。如左图中,a的左孩子值 < aa右孩子的值 > a,在右图中可以存放两个元素b、c,该树有三个孩子,左孩子的值 < bb < 中间孩子的值 < c右孩子的值 > c。通常称左图含有两个孩子的节点为2节点,称右图中含有三个孩子的节点为3节点。

当我们需要对2-3进行搜索的时候,实际上也是和二分搜索树思路一致的,当我们搜索的过程来到一个3节点的话,搜索数x如果小于3节点的左值,则在3节点的左子树中继续寻找,如果搜索的这个树大于3节点的右值,到三节点的右子树中继续查找,如果搜索数x的值介于3节点的左值与右值之间,则在3节点的中间子树继续寻找。

对于2-3树来说,它是一个绝对平衡的树。即从根节点到任意一个叶子节点,所经过的节点数量一定是相同的。

理解在2-3树中添加节点时,是如何维护绝对平衡的,对我们理解红黑树的机制很有帮助。

2-3树在添加节点时,如果子树为空,则进行融合,如果融合之后是四节点,则分裂。

如图:

     在添加元素6时,元素12不存在左子树,则元素12与元素6融合形成3节点              

     融合形成4节点则分裂

插入后形成3节点,且父亲节点为2节点时

插入后形成3节点,且父亲节点为3节点时

二、2-3树与红黑树的等价性

黑色为普通节点,红色为特殊节点,红黑组合相当于2-3树中的3节点,我们把3节点中的左节点b作为右节点c的 左子节点 ,因为左节点 < 右节点 ,之所以b为红色是因为在2-3树中,元素b元素c是作为3节点相融在一起的,但在红黑树中只有2节点,红色表示【红色的b】与其父节点【黑色的c】在2-3树是以3节点相融的,在红黑树中,所有的红色节点都是向左倾斜的

转化为对应的红黑树:

三、红黑树的五大特性

1、每个节点或者是红色的或者是黑色的。

2、根节点是黑色的。

3、每一个叶子节点(最后的空节点NIL)是黑色的。

4、如果一个节点是红色的,那么他的孩子节点一定是黑色的。

5、从任意一个节点到叶子节点,所经过的黑色节点数量相同。

性质2证明: 根节点是黑色的

性质3是一种性质:每一个叶子节点(最后的空节点NIL)是黑色的

对于一棵空树本身,其根节点一定是黑色的,在极端的环境下,空树即是叶节点又是根节点,都是黑色的

性质4证明:如果一个节点是红色的,那么他的孩子节点一定是黑色的

在红黑树中,红黑节点组合代表的是2-3树中的3节点,3节点中左侧元素b对应红黑树中的红色节点,该红色节点的孩子为对应2-3树中的左孩子和中间孩子,若该孩子节点为2节点,如左边图所示,则一定为黑色;若孩子节点为3节点,则连接的形状如右图所示,先连接黑色节点,黑色节点的左孩子才为空色节点,所以如果一个节点它是红色的,它的孩子节点一定是黑色的。

拓展:对于一个黑色节点,他的左孩子可能为红色【对应2-3树的融合】,也可能为黑色。 总的来说:它的左孩子右可能为红色,有可能为黑色,但右节点一定是黑色节点

性质5【红黑树核心特性】证明:从任意一个节点到叶子节点,所经过的黑色节点数量相同

因为红黑树和2-3树是等价的,2-3树是一棵绝对平衡的树。对于2-3树的任意一个节点出发,到叶子节点所经过的节点数一样多,由于2-3树是绝对平衡的树,所有的叶子节点都在同一层中。

在2-3树转化为红黑树时,对应2节点/3节点分别转化为黑色节点/红色和黑色节点2个节点,不管如何转化一定会存在一个黑色节点,所以从任意一个节点到叶子节点,对于2-3树来说经过的节点数相同,对红黑树来说经过的黑色节点数量相同。

红黑树是保持"黑平衡"的二叉树,严格意义上讲红黑树并不是平衡二叉树,左右子树的黑色节点保持着绝对的平衡,对红黑树来说,如果存在节点个数n,那么最大高度为2logn,时间复杂度为O(logn)。

AVL  vs  红黑树

查找效率: AVL树 > 红黑树

增删改效率:  AVL树 < 红黑树

四、向红黑树中添加新元素

由于2-3树和红黑树具有等价性,先回忆在2-3树中添加新节点时,永远不会添加到一个空节点【要么融合,要么产生临时4节点分裂】,所以添加进2节点时,会形成一个3节点,当添加进3节点时,会暂时形成一个4节点后分裂。

在红黑树中添加新元素时,设置为永远是红色节点,在递归结束后,我们需要手动设置根节点为黑色BLACK(boolean)。

新增元素时,有几种情况:

添加到对应2-3树中的2节点下:

1、新增元素添加到左右子树都为空的黑色节点[2-node]

新插入节点比其根节点小,则作为根节点的左子节点,这种情况比较简单【相当于2-3树融合】

新插入节点比根节点大,则作为根节点的右子节点,我们知道红色节点不可能在右侧,所以需要做调整

                            

执行左旋转

步骤1:

步骤2:

为了表示37、42是一个3节点,则原来的元素37需要变为红色节点 

其中第三句x.color = node.color,如果原来node颜色为红色节点,则打破了红黑树的基本性质,在这里左旋转只是一个子过程,左旋转后形成子树新的根节点x将会被返回做后续处理,在左旋转时并不维持红黑树的基本性质,只需要保证37、42两个元素对应2-3树中的三节点即可。

左旋转代码:

    //   node                     x
    //  /   \     左旋转         /  \
    // T1   x   --------->   node   T3
    //     / \              /   \
    //    T2 T3            T1   T2
    private Node leftRotate(Node node){
        Node x = node.right;
        node.right = x.left;
        x.left = node;
        //切换颜色
        x.color = node.color;
        node.color = RED;
        //返回旋转之后的根节点
        return x;
    }

2、新增元素添加到黑色节点[3-node]

向原3节点的红黑树中添加一个大于其根节点42的值

步骤1:根据二分搜索树的添加规则,66添加到37的右节点上

对应的是一个临时的4节点【融合】 处理方式为:

步骤2:拆分成3个2节点

对应红黑树为3个黑色节点,让42的左右子节点都改变为黑色

步骤3:

此时其根节点42应该与42的父节点进行融合,所以42需要变为红色

我们发现42由黑色变为了红色,而37和66都由红色变为了黑色,该动作称为颜色翻转【flipColors】

    //颜色翻转
    private void flipColors(Node node){
        node.color = RED;
        node.left.color = BLACK;
        node.right.color = BLACK;
    }

向原3节点的红黑树中添加一个小于其父节点的值

添加后:

此时产生了一个临时的4节点,处理方式仍然为分裂,即变为由3个2节点的子树,如图:

此时应该执行右旋转步骤1

旋转之前:

旋转之后:

步骤2:此时需要变换颜色,x的颜色需要改变为node的颜色,node的颜色需要设置为红色

最后执行我们编写过的颜色翻转即可,变为:

右旋转代码:

    //     node                   x
    //    /   \     右旋转       /  \
    //   x    T2   ------->   y   node
    //  / \                       /  \
    // y  T1                     T1  T2
    public Node rightNode(Node node){
        Node x = node.left;
        node.left = x.right;
        x.right = node;
        //切换颜色
        x.color = node.color;
        node.color = RED;
        return x;
    }

向原3节点的红黑树中添加一个大于根节点且大于其父节点的值

添加后形成的红黑树情况:

步骤1:对节点37进行左旋转,如图

步骤2:对节点42进行右旋转,如图:

步骤3:变色操作,对节点40变为42的颜色,将节点42变为红色

步骤4:执行颜色翻转

添加元素总结

这种过程即为我上述的向红黑树3节点中添加元素【添加的元素小于根节点大于添加位置的父节点】的全过程

向红黑树3节点中添加元素【添加的元素小于根节点于添加位置的父节点】的全过程,即直接跳到了第三步

向红黑树3节点中添加元素【添加的元素大于根节点】的全过程,即直接跳到了第四步

 左右旋转条件总体逻辑代码编写:

左旋转 右节点为红色且左节点不为红色

右旋转 左节点为红色且左节点的左节点也为红色

颜色翻转 

在add方法中:【使用3个if 每次都顺序判断】

    // 向以node为根的二分搜索树中插入元素(key, value),递归算法
    // 返回插入新节点后二分搜索树的根
    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.left = add(node.left, key, value);
        else if(key.compareTo(node.key) > 0)
            node.right = add(node.right, key, value);
        else // key.compareTo(node.key) == 0
            node.value = value;

        //左旋转 右节点为红色且左节点不为红色
        if(isRed(node.right) && !isRed(node.left))
            node = leftRotate(node);
        //右旋转 左节点为红色且左节点的左节点也为红色
        if(isRed(node.left) && isRed(node.left.left))
            node = rightNode(node);
        //颜色翻转
        if(isRed(node.left) && isRed(node.right))
            flipColors(node);
        return node;
    }

红黑树的性能总结:

红黑树完整代码:

import java.util.ArrayList;

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;
        }
    }

    private Node root;
    private int size;

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

    public int getSize(){
        return size;
    }

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

    // 判断节点node的颜色
    private boolean isRed(Node node){
        if(node == null)
            return BLACK;
        return node.color;
    }

    //   node                     x
    //  /   \     左旋转         /  \
    // T1   x   --------->   node   T3
    //     / \              /   \
    //    T2 T3            T1   T2
    private Node leftRotate(Node node){

        Node x = node.right;

        // 左旋转
        node.right = x.left;
        x.left = node;

        x.color = node.color;
        node.color = RED;

        return x;
    }

    //     node                   x
    //    /   \     右旋转       /  \
    //   x    T2   ------->   y   node
    //  / \                       /  \
    // y  T1                     T1  T2
    private Node rightRotate(Node node){

        Node x = node.left;

        // 右旋转
        node.left = x.right;
        x.right = node;

        x.color = node.color;
        node.color = RED;

        return x;
    }

    // 颜色翻转
    private void flipColors(Node node){

        node.color = RED;
        node.left.color = BLACK;
        node.right.color = BLACK;
    }

    // 向红黑树中添加新的元素(key, value)
    public void add(K key, V value){
        root = add(root, key, value);
        root.color = BLACK; // 最终根节点为黑色节点
    }

    // 向以node为根的红黑树中插入元素(key, value),递归算法
    // 返回插入新节点后红黑树的根
    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.left = add(node.left, key, value);
        else if(key.compareTo(node.key) > 0)
            node.right = add(node.right, key, value);
        else // key.compareTo(node.key) == 0
            node.value = value;

        if (isRed(node.right) && !isRed(node.left))
            node = leftRotate(node);

        if (isRed(node.left) && isRed(node.left.left))
            node = rightRotate(node);

        if (isRed(node.left) && isRed(node.right))
            flipColors(node);

        return node;
    }

    // 返回以node为根节点的二分搜索树中,key所在的节点
    private Node getNode(Node node, K key){

        if(node == null)
            return null;

        if(key.equals(node.key))
            return node;
        else if(key.compareTo(node.key) < 0)
            return getNode(node.left, key);
        else // if(key.compareTo(node.key) > 0)
            return getNode(node.right, key);
    }

    public boolean contains(K key){
        return getNode(root, key) != null;
    }

    public V get(K key){

        Node node = getNode(root, key);
        return node == null ? null : node.value;
    }

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

        node.value = newValue;
    }

    // 返回以node为根的二分搜索树的最小值所在的节点
    private Node minimum(Node node){
        if(node.left == null)
            return node;
        return minimum(node.left);
    }

    // 删除掉以node为根的二分搜索树中的最小节点
    // 返回删除节点后新的二分搜索树的根
    private Node removeMin(Node node){

        if(node.left == null){
            Node rightNode = node.right;
            node.right = null;
            size --;
            return rightNode;
        }

        node.left = removeMin(node.left);
        return node;
    }

    // 从二分搜索树中删除键为key的节点
    public V remove(K key){

        Node node = getNode(root, key);
        if(node != null){
            root = remove(root, key);
            return node.value;
        }
        return null;
    }

    private Node remove(Node node, K key){

        if( node == null )
            return null;

        if( key.compareTo(node.key) < 0 ){
            node.left = remove(node.left , key);
            return node;
        }
        else if(key.compareTo(node.key) > 0 ){
            node.right = remove(node.right, key);
            return node;
        }
        else{   // key.compareTo(node.key) == 0

            // 待删除节点左子树为空的情况
            if(node.left == null){
                Node rightNode = node.right;
                node.right = null;
                size --;
                return rightNode;
            }

            // 待删除节点右子树为空的情况
            if(node.right == null){
                Node leftNode = node.left;
                node.left = null;
                size --;
                return leftNode;
            }

            // 待删除节点左右子树均不为空的情况

            // 找到比待删除节点大的最小节点, 即待删除节点右子树的最小节点
            // 用这个节点顶替待删除节点的位置
            Node successor = minimum(node.right);
            successor.right = removeMin(node.right);
            successor.left = node.left;

            node.left = node.right = null;

            return successor;
        }
    }

}

猜你喜欢

转载自blog.csdn.net/itcats_cn/article/details/83547603