【数据结构】红黑树与2-3树

什么是红黑树?

红黑树的定义

  • 每个节点或者是红色的,或者是黑色的。
  • 根节点是黑色的。
  • 每一个叶子节点(最后的空节点)是黑色的。
  • 如果一个节点是红色的,那么他的孩子节点都是黑色的。
  • 从任意一个节点到叶子节点,经过的黑色节点是一样的。

直接看到这些定义是非常难以理解的,红黑树为什么这样定义?

在算法4这本书中对于红黑树的介绍直接绕过了红黑树的基本性质,而是首先探索了另外一种平衡树,这种平衡树就是2-3树,事实上红黑树与2-3树是等价的,如果理解的红黑树与2-3树之间的等价关系,其实红黑树并不难!

什么是2-3树?

在讲解红黑树之前,首先学习2-3树,通过2-3树来理解红黑树。

2-3树满足二分搜索树的基本性质,但其节点可以存放一个元素或二个元素,每个节点有两个或者三个孩子,所以称为2-3树。通常存放一个元素有两个孩子的节点称为两节点,一个元素有三个孩子的节点称为三节点。

如图:

2-3树.png

==注意:2-3树是一颗绝对平衡的树。==

查找元素

2-3树的查找类似二分搜索树的查找,根据元素的大小来决定查找的方向是左孩子还是右孩子。要判断一个元素是否存在,首先将待查询的元素与根节点进行比较,如果待查找的元素和其中任意一个元素相等,那就查找到了,否则根据比较的结果来选择查找的方向。

如图:

2-3树—查找元素.png

插入元素

==对于2-3树来说,添加节点永远不会添加到空的位置上==。那么添加到哪呢?

如图:

2-3树—插入元素1.png

例如添加一个节点37,依然按照二分搜索树的性质来看待,由于37小于42,应该放于42的左子树中,由于42的左子树为空,那么此时新节点将会融合到添加的过程中所找到的最后一个叶子节点。

如图:

2-3树—插入元素2.png

由于42本来是个2节点,但是融合后,变成了一个3节点,并且依然是平衡的。

此时再添加一个节点12,由于12小于37,应该放于37的左子树中,由于37的左子树为空,那么此时新节点将会融合到添加过程中所找到的最后一个叶子节点。

如图:

2-3树—插入元素3.png

虽然再添加过程中的最后一个叶子节点是一个3节点,但是依然进行融合,暂时形成一个4节点。但是对于2-3树来说,它并没有4节点,只有2节点或3节点。对于4节点可以直接分裂成子树。

如图:

2-3树—插入元素4.png

上图中,由4节点分裂成了由三个2节点组成一颗平衡的树。

此时再添加一个节点18,由于18小于37,进入37的左子树中继续比较,此时18大于12,应该放于12的右子树中,由于12的右子树为空,那么此时新节点将会融合到添加过程中所找到的最后一个叶子节点。

如图:

2-3树—插入元素5.png

此时再添加一个节点6,由于6小于37,进入37的左子树中继续比较,此时6小于12,应该放于12的左子树当中,但是12的左子树为空,那么此时新节点将会融合到添加过程中所找到的最后一个叶子节点。

如图:

2-3树—插入元素6.png

由于2-3中没有4节点,对于节点可以直接分裂成子树。

如图:

2-3树—插入元素7.png

注意,上图中由于4节点分裂成3个2节点的子树,造成整个树并不是一颗绝对平衡的树。对于2-3树来说,如果一个叶子节点本来是一个3节点了,添加了新节点变成了4节点的话,对于这个4节点,分裂为三个2节点,并且有一个根节点,对于根节点需要跟父亲节点进行融合,对于根节点和父亲节点融合分为两种情况:

  • 父节点是一个2节点

    如图:

    2-3树—插入元素8.png
  • 父节点是一个3节点

    如图:

    2-3树—插入元素9.png
    2-3树—插入元素10.png
    2-3树—插入元素11.png

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

2-3树中的3节点的元素是并列关系,但两个黑色的节点无法表示出并列的关系。那么如何在红黑树中标识出并列的关系呢?

如图:

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

如上图中,可以将b节点设置为红色,表示b、c节点在2-3树中是并且的关系。也就是说,一个红色节点和它的父亲节点在2-3树中是一个3节点。

红黑树的实现

注意:在这里实现的红黑树以红色节点向左倾斜为基础的。

插入节点

在2-3树中插入一个节点,始终都是与叶子节点进行融合,那么在红黑树中红色节点代表着与父亲节点

并列的关系,所以在红黑树中添加一个节点始终都是红色的。

红黑树—插入节点1.png

上图中,添加第一个节点42,由于添加一个节点始终是红色的,所以要把根节点变为黑色。

此时,再添加一个节点37,根据二分搜索树的性质,由于37<42,所以将节点37添加到根节点的左孩子。

红黑树—插入节点2.png
  • 情况1

    注意,如果新节点大于根节点,就会将新节点插入到根节点的右孩子位置,那么此时就不符合红色节点向左倾斜的定义了,如何解决?对根节点进行左旋转操作,并且根据红色节点向左倾斜的定义,还需把37节点置位红色,42节点置位黑色。注:这里对应在2-3树中相当于把一个新的元素放于2节点中融合为3节点的操作。

    红黑树—插入节点3.png
  • 情况2

    此时再插入一个节点66,根据二分搜索树的性质,由于66大于42,放于42的右孩子中。

    红黑树—插入节点4.png

    上图中,在2-3树中对应一个临时的4节点,在2-3树对临时的4节点所处理的方式是什么?

    处理的方式是拆分成三个2节点所组成的一棵子树。

    那么在红黑树中三个2节点代表着什么意思?其实就是代表这三个节点都是黑色的节点,每一个黑色的节点,如果它的左侧没有红色节点,它本身就代表一个单独的二节点,所以在这种情况下,节点根本不需要选择操作,只需要改变颜色即可,让节点42的左右孩子分别改为黑色。

    红黑树—插入节点5.png

    注:这个样子相当于2-3树中由临时4节点所组成三个2节点的子树。

    注意,在2-3树中由临时4节点所组成的三个2节点的子树,有可能会造成整颗树不是绝对平衡的,所以拆分的子树的根节点会继续向父亲节点融合。这意味着,新的根节点在红黑树中变为红色节点。

    红黑树—插入节点6.png

    注:原来的42节点为黑色,37节点和66节点为红色,经过一系列操作之后,其实所有节点的颜色正好相反过来了,这个过程称为颜色翻转。

  • 情况3

    红黑树—插入节点7.png

    如上图,添加节点12,根据二分搜索树的性质,添加到节点37的左孩子中。这样的形状也等同于2-3树中的临时4节点。

    红黑树—插入节点8.png

    在2-3树中需要把临时4节点拆分成三个2节点,那么在红黑树中如何解决呢?

    对这颗子树的根节点进行右旋转操作,旋转后再对新的根节点变为原来根节点的颜色。

    红黑树—插入节点9.png

    通过这样的处理之后,其实本质上12、37、42这些节点还是应该是一个临时的4节点,所以最后再把原来的根节点变为红色节点,这样就表示节点42与父亲节点37是融合在一起的。

    红黑树—插入节点10.png

    这样就完成了整个右旋转的过程。在这个过程中,根节点变为节点37,并且临时4节点的样子变为根节点的左右孩子都为红色节点,这种情况就是颜色翻转,处理这种情况,把它变成对应2-3树中由三个2节点组成的子树的方法,就是再运行一下颜色翻转的过程。

整体代码

/**
 * 描述:红黑树。
 * <p>
 * Create By ZhangBiao
 * 2020/5/18
 */
public class RedBlackTree<K extends Comparable<K>, V> {

    private Node root;

    private int size;

    private static final boolean RED = true;

    private static final boolean BLACK = false;

    public RedBlackTree() {
        this.root = null;
        this.size = 0;
    }

    /**
     * 判断节点node的颜色
     *
     * @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
    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;
    }

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

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

    /**
     * 向以node为根节点的红黑树中插入元素(key, value)(递归算法)并返回插入新节点后红黑树的根节点
     *
     * @param node
     * @param key
     * @param 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 {
            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;
    }

    @Override
    public V remove(K key) {
        throw new IllegalArgumentException("No remove in RedBlackTree!");
    }

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

    @Override
    public V get(K key) {
        Node node = getNode(root, key);
        return node == null ? null : node.value;
    }

    @Override
    public void set(K key, V newValue) {
        Node node = getNode(root, key);
        if (node == null) {
            throw new IllegalArgumentException(key + " not exist!");
        }
        node.value = newValue;
    }

    @Override
    public int getSize() {
        return this.size;
    }

    @Override
    public boolean isEmpty() {
        return this.size == 0;
    }

    /**
     * 返回以node为根节点的二分搜索树中key所在的节点
     *
     * @param node
     * @param 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 {
            return getNode(node.right, key);
        }
    }

    private class Node {

        public K key;

        public V value;

        public Node left;

        public Node right;

        public boolean color;

        public Node(K key, V value) {
            this.key = key;
            this.value = value;
            this.left = null;
            this.right = null;
            this.color = RED;
        }


    }

}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342