3

一、引言

红黑树是一种平衡的二叉树,其在二分搜索树的基础上添加了一些特性,保证其不会退化成链表。

1、红黑树的定义

  1. 满足二分搜索树的基本性质
  2. 每个节点要么是红色的,要么是黑色的
  3. 根节点是黑色的
  4. 每一个叶子节点(最后的空节点)是黑色的
  5. 如果一个节点是红色的,那么它的孩子节点都是黑色的
  6. 从任意一个节点到叶子节点,经过的黑色节点是一样的

说明:以上红黑树的定义看完很快就会忘记,疑惑点在于为什么要这么定义。为什么有的节点是红色或是黑色。为什么这么几条限制就能保证红黑树的平衡性。这些疑问点在看完下面对于“2-3树”的介绍后会逐渐变得明朗,“2-3树”和红黑树是等价的,红黑树的各个特性可以类比于“2-3树”。

二、2-3树

1、什么是2-3树

2-3树是一颗绝对平衡的树,从根节点到任意叶子节点所应该的节点数量是相等的;它满足二分搜索树的基本性质;有的节点仅包含一个元素,它允许有2个孩子节点,被称为“2节点”;有的节点包含两个元素,它允许有三个孩子节点,被称为“3节点”。
2-3树.png

2、2-3树的定义

  • 满足二分搜索树的基本性质
  • 节点可以存放一个元素或者两个元素
  • 每个节点有2个(2节点)后者3个孩子(3节点)

2-3树的节点.png

3、2-3树维持绝对平衡

1、元素添加到2节点

元素添加到2节点.png

说明:元素添加到2-3树的2节点直接融合成一个3节点即可。

2、元素添加到3节点

元素添加到3节点.png

说明:元素添加到2-3树的3节点会临时融合成“4节点”,然后会裂变成三个2节点,以其中一个节点为根,裂变后仍然满足二分搜索树的性质。

3、元素添加到3节点,且父亲节点为2节点

元素添加到3节点,且父亲节点为2节点.png

说明:元素添加到2-3树的3节点会临时融合成“4节点”,然后会裂变成三个2节点,以其中一个节点为根;此临时4节点裂变后产生的新子树高度增加了1,破坏了绝对平衡性,于是这颗新子树的根需要继续向其父亲节点融合,融合后新子树增加的高度被抹平,重新保持绝对平衡。

4、元素添加到3节点,且父亲节点为3节点

元素添加到3节点,且父亲节点为3节点.png

说明:2-3树维护绝对平衡,靠的就是融合、裂变的操作,融合后树的高度保持变后者高度降低,而裂变操作后会增加树的高度,因此裂变后会伴随着融和操作,融合操作中和裂变操作。从而保证了2-3的绝对平衡。

三、红黑树和2-3树

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

红黑树与2-3树的节点等价性.png

红黑树和2-3树.png

红黑树和2-3树类比.png

  • 红黑树用黑色节点表示2-3树的2节点
  • 红黑树用红色节点表示与其祖先节点融合组成3节点

说明:红黑树之所以定义节点的红黑颜色,实际上就是模拟2-3树的2节点或者3节点。我们再来回顾一下关于开头的红黑树定义,下面会做个简单说明。

  • 为什么每个节点要么是红色的,要么是黑色的?

说明:模拟2-3树的2节点和3节点只需要两种颜色即可,红色节点表示与其父亲节点融合。

  • 为什么根节点是黑色的?

说明:根节点作为红黑树的根,不需要向上融合了。

  • 为什么一个节点是红色的,那么它的孩子节点都是黑色的?

说明:因为一个红色节点与其父亲节点已经组成了类似于2-3树的3节点了,如果其孩子节点依然是红色的,那就构成了2-3树中的临时“4节点”,此时2-3树要进行裂变和融合操作保证绝对平衡性。那么类似于2-3树,红黑树有连续两个节点是红色的话,红黑树需要进行旋转操作(类似于2-3树的裂变操作)配合颜色翻转操作(类似于2-3树的融合操作),以使得红黑树保持平衡性。连续连续两个红色节点也是红黑树出发自平衡的触发点,所以不会出现连续两个红色节点。

  • 为什么从任意一个节点到叶子节点,经过的黑色节点是一样的?

说明:由于红黑树是模拟于2-3树,而2-3树是绝对平衡的树,所以除去表示融合的红色节点,红黑树中的黑色节点也是绝对平衡的,红黑树也被称为是“黑平衡”的二叉树,因此从红黑树的任意节点出发到叶子节点,所经历的黑色节点数是一致的。

四、红黑树相关操作

红黑树在添加元素、删除元素后会影响树的平衡性,可能会破坏红黑树的定义。类似于2-3树的平衡操作(裂变节点、融合节点),红黑树也有其平衡操作,左右旋转类似于2-3树的裂变节点;颜色翻转类似于2-3树的融合节点。
红黑树相关操作.png

说明:以下会实现一个左倾红黑树,在递归算法实现的红黑树添加操作中,当递归到深层的叶子节点融合元素(红色节点)后可能造成不平衡,需要递归逐层向上维护节点状态。

1、红黑树左旋操作

  • 左倾左旋操作前

左旋操作前.png

说明:左倾红黑树规定节点的右孩子不能是红色节点,需要左旋操作。

  • 左旋操作前后

左旋操作前后.png

说明:左旋操作后当前子树的根节点发生了变化,当前的根节点需要继承保留原有根节点的颜色,同时原来的根节点变成红色节点。如果原有根节点是红色节点,那么上层需要继续进行相关操作。

2、红黑树的右旋操作

  • 右旋操作前

右旋操作前.png

说明:连续两个节点为红色节点,违反了红黑树的定义,类似于形成了2-3树的临时4节点,此时红黑树的平衡被破坏,需要再平衡操作。

  • 右旋操作后

右旋操作后.png

说明:右旋操作后,发现节点状态依然类似于2-3树的临时4节点状态,需要继续裂变和融合操作,而红黑树此种场景只需要向上融合即可,即需要颜色翻转,使x节点变成红色节点表示向上融合。

3、颜色翻转操作

  • 颜色翻转前

颜色翻转前.png

  • 颜色翻转后

颜色翻转后.png

总结:红黑树模拟于2-3树也等价于2-3树。红黑树的左右旋转等价于2-3树的裂变节点操作;红黑树的颜色翻转等价于2-3树的融合节点操作;不同点在于红黑树的左右旋转会可以降低子树的高度,而2-3树的裂变节点操作会增加子树的高度;红黑树的颜色翻转操作不会改变子树的高度,是一个抽象的融合操作,而2-3树的融合操作可以降低子树高度。最终红黑树也会趋于平衡,而2-3会保持绝对平衡。

五、红黑树的实现

1、左倾红黑树的实现

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

    /**
     * 判断节点是否是红色
     * @param node 树节点
     * @return
     */
    private boolean isRed(Node node) {
        if (node == null) {
            return BLACK;
        }
        return node.color;
    }

    /**
     * 节点左旋转操作
     *   node                     x
     *  /   \     左旋转         /  \
     * T1   x   --------->   node   T3
     *     / \              /   \
     *    T2 T3            T1   T2
     * @param node
     * @return
     */
    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
     * @param node
     * @return
     */
    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;
    }

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

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

    /**
     * 向以node为根的二分搜索树中插入元素(key, value),递归算法
     * 返回插入新节点后二分搜索树的根
     * @param node 树节点
     * @param key 元素key
     * @param value 元素value
     * @return 返回插入新节点后二分搜索树的根
     */
    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所在的节点
     * @param node 节点
     * @param key 元素key
     * @return
     */
    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为根的二分搜索树的最小值所在的节点
     * @param node
     * @return
     */
    private Node minimum(Node node){
        if(node.left == null)
            return node;
        return minimum(node.left);
    }
}

六、时间复杂度分析

红黑树相比于AVL树,其实是牺牲了平衡性的,红黑树并不完全满足平衡二叉树的定义,红黑树的最大高度达到了2logn的高度,红色节点影响了红黑树的的平衡性。红黑树虽然牺牲了一定的查询性能,但是在增删改操作的性能得到了弥补,红黑树的综合性能还是要优于AVL树的。

七、其它数据结构


neojayway
52 声望10 粉丝

学无止境,每天进步一点点