数据结构-红黑树

郑重说明:尽管网络上有很多的资源可以借鉴,但是笔者还是需要很多的帮助才能写出这些总结笔记
1️⃣首先也是最重要的慕课网给了我巨大的帮助,自从第一次打开慕课网以后,从此就在我心里播下了一颗学习的种子;
2️⃣要特别鸣谢慕课网的liuyubobobo老师,从他那里我”偷了”许多的想法放到这篇笔记里边;
3️⃣本文内容直接出自liuyubobobo老师的课程<玩转数据结构 从入门到进阶>
4️⃣清醒时做事,糊涂时学习,祝大家都能梦想成真;

一 红黑树与2-3树

1⃣️ 红黑树

R-B Tree,全称是Red-Black Tree,又称为“红黑树”,它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。
《数据结构-红黑树》 红黑树图示

红黑树的特性(算法导论):
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

上边的红黑树定义的特别形式化,初次接触可能不太习惯,那么我们接下来先来介绍和红黑树等价的另外一种树2-3树;如果能把2-3树搞明白对于学习红黑树以及B类树都是有很大的好处的;

2⃣️ 2-3树

满足二分搜索树的基本性质,一个节点可以放置一个元素或者两个元素,每个子节点可以有两个元素或者三个元素,就叫做2-3树;
《数据结构-红黑树》 2-3树1

综上所属,2-3树也有可能会在一棵树中有这样的两种节点组成
《数据结构-红黑树》 2-3树2

在2-3树中有一个很重要的性质:2-3树是一个绝对平衡的树,这个特性是和2-3树的插入操作息息相关的;绝对平衡的意思就是说从根节点到任意一个叶子节点,所经过的节点数量一定是相同的。

二 2-3树的绝对平衡性

1⃣️ 2-3树是如何维护绝对的平衡的?

首先,我们往2-3树中添加第一个节点
《数据结构-红黑树》 2-3树的绝对平衡性1 由于此时2-3树是空的,所以42这个节点就理所当然的成为了根节点;
《数据结构-红黑树》 2-3树的绝对平衡性2

此时我们在添加一个新的节点37,由于2-3树一个节点可以存放两个元素且不能往空的节点添加新的元素,所以我们可以把新的元素与原来的元素进行融合形成一个三节点;
《数据结构-红黑树》 2-3树的绝对平衡性3 如果此时在添加一个新的元素12,由于不能往空节点存放数据,所以我们将这个新的元素也放在根节点,从而形成了一个四节点;
《数据结构-红黑树》 2-3树的绝对平衡性4 这个时候我们可以将这个四节点拆分成一个二分搜索树,当然这个二分搜索树也是绝对平衡的;
《数据结构-红黑树》 2-3树的绝对平衡性5 如果此时我们在添加一个新元素18,那么这个元素可以直接放在子节点中,与12这个元素形成一个三节点;
《数据结构-红黑树》 2-3树的绝对平衡性6 此时在新加入一个元素6,我们将重复以上的动作,将这个元素放在子节点中形成四元素;
《数据结构-红黑树》 2-3树的绝对平衡性7

对于四元素的节点我们依然选择对它进行拆分,但是此时拆分后它就没有绝对平衡性了
《数据结构-红黑树》 2-3树的绝对平衡性8 然后我们将新拆分的二分搜索树的根节点12与这颗树的根节点进行融合形成一个新的具有绝对平衡性的2-3树;
《数据结构-红黑树》 2-3树的绝对平衡性9 继续添加新的元素,我们仍然采用融合的方式进行数据的存储;
《数据结构-红黑树》 2-3树的绝对平衡性10 继续采用融合的存储方式添加元素;
《数据结构-红黑树》 2-3树的绝对平衡性11 拆分四节点;
《数据结构-红黑树》 2-3树的绝对平衡性12 继续融合根节点,让根节点成为四节点;
《数据结构-红黑树》 2-3树的绝对平衡性13 最后我们对根节点的元素进行拆分从而形成绝对平衡的2-3树;

这里所说的三节点,四节点是指子节点的数量,假设一个存储两个元素的根节点,那么它们的子节点一共有三个分别是左中右; 《数据结构-红黑树》 2-3树的绝对平衡性14 分析到这里我们可以得出,2-3树维护绝对平衡性的操作其实就是融合与拆分 《数据结构-红黑树》 2-3树的绝对平衡性15

三 红黑树与2-3树的等价性

在某种程度上,红黑树与2-3树是具有等价性的,这种等价性在2-3树每个节点为2节点时尤为明显
《数据结构-红黑树》 红黑树与2-3树的等价性1

对于2-3树中的三节点在红黑树中的对应表示可能没有那么明显
《数据结构-红黑树》 红黑树与2-3树的等价性2 我们将b与c连接的那个边表示为红色,意思就是b与c如果转化为2-3树的话将会是一个三节点,但是这个并不是红黑树最终的表示形式,如果使用红黑树表述的话将会是这样的
《数据结构-红黑树》 红黑树与2-3树的等价性3

如果理解了上边的推导过程,那么下边的实例应该是非常简单的
《数据结构-红黑树》 红黑树与2-3树的等价性4

像这样的一个2-3树如果转换成红黑树将会是这样的
《数据结构-红黑树》 红黑树与2-3树的等价性5 为什么在这个红黑树中只有三个红色的节点呢,因为在转换前的2-3树中只有三个三节点,三节点转换后将会变成三个红色的节点,从这里我们也可以自己定义一个结论红色的节点一直在左侧;如果看起来抽象的话,我们可以对这个红黑树进行一次变形
《数据结构-红黑树》 红黑树与2-3树的等价性6

代码实现

package com.mufeng.RedBlackTree;


/**
 * Created by wb-yxk397023 on 2018/7/14.
 */
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的颜色
     * @param node
     * @return
     */
    private boolean isRed(Node node){
        if (node == null){
            return BLACK;
        }
        return node.color;
    }

    // 向二分搜索树中添加新的元素(key, value)
    public void add(K key, V value){
        root = add(root, key, value);
    }

    // 向以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;

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

四 保持根节点为黑色和左旋转

1⃣️ 保持根节点为黑色

// 向二分搜索树中添加新的元素(key, value)
    public void add(K key, V value){
        root = add(root, key, value);
        // 保持根节点为黑色
        root.color = BLACK;
    }

2⃣️ 向红黑树中添加新的元素(二节点)

  1. 如果新添加的元素比根元素要小的话也就是说是往根节点的左节点中插入元素的话,我们就不用考虑节点颜色的问题;
  1. 但是如果要添加的元素是往根节点的右侧添加,我们就需要处理一下节点颜色的问题,因为根据我们的定义右侧不能有红色的节点; 《数据结构-红黑树》 红黑树添加新的元素1

    为了不失一般性,我们给每个节点都加上子节点,这个时候这棵树是不满足红黑树的要求的 《数据结构-红黑树》 红黑树添加新的元素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;
    }

五 颜色反转和右旋转

1⃣️ 颜色反转

首先我们添加一个元素的时候(三节点)
《数据结构-红黑树》 颜色反转1 如果默认是往左侧添加,则直接添加即可;
《数据结构-红黑树》 颜色反转2

如果继续添加,则就会变成这样因为我们默认添加的元素都是红色的
《数据结构-红黑树》 颜色反转3 如果将这个红黑树转换为2-3树的话就是现在的样子;
《数据结构-红黑树》 颜色反转4 我们在2-3树中对一个临时的四节点需要进行拆分,拆分成二分搜索树的形式;拆分后每一个子节点就相当于一个二节点存在,与之对应的就是红黑树表示方式就是一个单独的黑色的节点,所以我们要对红黑树的左右子节点进行颜色的维护就变成了现在这样;
《数据结构-红黑树》 颜色反转5 我们在对2-3树拆分以后我们需要将拆分后的根节点于其祖先节点进行融合,所以对应的我们的黑红书的根节点颜色就需要维护成红色;这个过程就叫做颜色反转;

2⃣️ 代码实现颜色反转

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

3⃣️ 右旋转

如果我们向一个三节点中添加新的元素
《数据结构-红黑树》 右旋转1 紧接着我们在添加一个新元素
《数据结构-红黑树》 右旋转2

这个时候的红黑树就等同于2-3树的临时四节点
《数据结构-红黑树》 右旋转3 现在这样的红黑树形状我们怎么把它变成由三个二节点组成的子树呢?这个时候我们就需要使用右旋转来实现;

《数据结构-红黑树》 右旋转实现图示1
《数据结构-红黑树》 右旋转实现图示2
《数据结构-红黑树》 右旋转实现图示3
《数据结构-红黑树》 右旋转实现图示4
《数据结构-红黑树》 右旋转实现图示5
《数据结构-红黑树》 右旋转实现图示6
《数据结构-红黑树》 右旋转实现图示7

4⃣️ 右旋转代码实现

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

六 红黑树中添加新元素

由于之前已经分析过添加新元素的原理,在这里就不再重复了,添加新元素的过程可以参考图示:
《数据结构-红黑树》 添加新元素图示1

这个是最复杂的添加情况的实现过程,其他两种稍微简单的添加情况(当添加一个小于三节点的值,或者添加一个大于三节点的值)情况就是这样的
《数据结构-红黑树》 添加新元素图示2 维护的时机和AVL树是一样的,采用的方式也是和AVL一样,添加成功后回溯向上维护;

代码实现

// 向红黑树中添加新的元素(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)){
            filpColors(node);
        }
        return node;
    }

另外红黑树的其他操作:由于红黑树底层结构使用的是二分搜索树,所以其他操作可以参考二分搜索树,还有就是红黑树的删除操作由于过于复杂这里就不再进行分析;

    原文作者:十丈_红尘
    原文地址: https://www.jianshu.com/p/d6d89ad8c3dc
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞