数据结构-红黑树

红黑树简介

R-B Tree,全称是Red-Black Tree,又称为“红黑树”,它一种特殊的二分搜索树。红黑树的每个节点上都有存储为表示节点的颜色,可以是红(Red)或黑(Black)。

1、红黑树的特性

红黑树特性

2、2-3树

说到红黑树,不得不说与它等价的一种树——2-3树。一种节点可以存放最多2个元素的树。


2-3树

2-3树是一个绝对平衡的树:对于任意一个节点来说,左右子树高度一定是相等的。2-3树也满足二分搜索树的特性。


2-3树节点
  • 2-3树插入元素的演变过程
    当插入元素 4 时,根据二分搜索树的特性,是在根节点6的左侧,然后与3节点2-5融合成为4节点2-4-5,又由于2-3树最多只能允许3节点,于是需要拆分,即将4节点2-4-5根据二分搜索树特性拆分成一个子的二分搜索树,然后该子二分搜索树的根节点4继续与其父亲节点融合。假如融合之后不是2节点或者3节点,则需要继续拆分。


    2-3树插入元素的演变过程

3、红黑树与二分搜索树

其实,红黑树和2-3树是等价的,只是红黑树的节点只能存放一个元素,并且颜色只能红或黑。我们可以将红色节点视为2-3树中3节点较小的那个元素,并且约定:红黑树中,所以红色节点都是左倾斜的。注意:这里这样的约定,是为了后续操作简单,但是这并不是红黑树的特性,红黑树的特性只有上面介绍的5个。

红黑树与2-3树节点关系

红黑树和2-3树的等价关系,如下图:
红黑树和2-3树

红黑树是保持“黑平衡”的二叉树,黑平衡:从任意一个节点出发到叶子结点,所经过的黑色节点数量是一样的。严格意义上,红黑树不是平衡二叉树。因为最坏情况下,每个黑色节点都有一个红色的左孩子,使树的高度接近2h。

4、红黑树的源码结构

由于红黑树也是二分搜索树,于是我们直接使用之前的二分搜索树的结构,只是Node节点新增了颜色标识的变量color,并且新增的节点颜色为红色。因为红色节点是表示2-3树中的3节点的一个元素,即在红黑树中,是和其父亲节点融合的元素。并且不会违背"特性5"!少违背一条特性,就意味着我们需要处理的情况越少。

/**
 * 红黑树
 */
public class RBTree<K extends Comparable<K>, V> implements Map<K, V> {

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

    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;
            left = null;
            right = null;
            /**
             * 新增的节点都是需要和2-3树中其父亲节点进行融合的,
             * 而在2-3树中的2节点(只有一个元素的节点)在红黑树中是用黑节点表示的,
             * 所以新增节点采用红色。
             */
            color = RED;
        }
    }

    private Node root;
    private int size;

红黑树的左旋转

和AVL一样,新增或者删除元素,都会破坏二分搜索树的特性,也会破坏红黑树自身的特性。所以需要通过旋转和颜色翻转来使其重新回到红黑树的特性上,我们先讲左旋转。
如下图:当我们新增节点 x 时,破坏了我们之前的约定(红色节点只能在左侧),于是需要左旋转(和AVL左旋转类似,就是多了着色操作)。


左旋转之前

对node节点进行左旋转,让node节点的右子树指向x节点的左子树,即node.right = x.left;然后让x节点的左子树指向node节点,即x.left = node;然后让x节点的颜色变成node节点的颜色,即x.color = node.color;最后将旋转之后的根节点置为红节点,继续与其父亲节点进行融合操作。


左旋转之后

左旋转代码实现:
    // 红黑树左旋转:由于我们约定红色节点只能在左侧,
    // 于是需要对红色节点在右侧的情况进行左旋转
    //   node                     x
    //  /   \     左旋转         /  \
    // T1   x   --------->   node   T3
    //     / \              /   \
    //    T2 T3            T1   T2
    private Node leftRotate(Node node){
        if (node == null) {
            return node;
        }
        Node x = node.right;
        node.right = x.left;
        x.left = node;

        x.color = node.color;
        // 这里必须变为红色,表示要和父亲节点x融合的
        node.color = RED;
        return x;
    }

红黑树的右旋转

右旋转和左旋转是相反的,我就不累赘了,直接给视图。


右旋转之前

这里需要注意的是,右旋转完成之后,x的右孩子成了红色节点,这个与我们约定的违背了,于是需要颜色翻转,下面介绍颜色翻转。


右旋转之后

右旋转代码实现:
    // 红黑树右旋转:当左侧连续有两个红色节点时,则需要右旋转
    //     node                   x
    //    /   \     右旋转       /  \
    //   x    T2   ------->   y   node
    //  / \                       /  \
    // y  T1                     T1  T2
    private Node rightRotate(Node node){
        if (node == null) {
            return node;
        }
        Node x = node.left;
        node.left = x.right;
        x.right = node;

        x.color = node.color;
        // 这里必须变为红色,表示要和父亲节点x融合的
        node.color = RED;
        return x;
    }

红黑树的颜色翻转

当左右孩子的节点都为红色时,这时候违背了我们约定的红色节点只能在左侧的特性。其实,这种情况,在2-3树中就是一个4节点,是需要拆分成二分搜索树的。在红黑树中,我们把当前3个节点的颜色翻转一下就OK了。


颜色翻转

如下图:
向2-3树中的3节点添加一个元素,会进行融合拆分的过程。对于红黑树来说,此时左右孩子都是红色节点,满足颜色翻转的条件。


颜色翻转之前

颜色翻转之后,如下图:
颜色翻转之后

颜色翻转代码实现:

    // 颜色翻转:当向2-3树中的3节点添加元素时,需要进行融合再分散;
    // 当新增元素之后,形成的红黑树是根节点为黑色,左右孩子为红色时,
    // 此时节点位置不用变化,直接翻转下节点颜色,即根节点为红色,左右孩子为黑色。
    // 注意:这里把根节点置为红色是为了后面和其父亲节点进行融合,因为融合节点是红色。
     private void flipColors(Node node){
         if (node == null) {
             return;
         }
         node.color = RED;
         node.left.color = BLACK;
         node.right.color = BLACK;
     }

红黑树的添加元素

讲完红黑树的左右旋转和颜色翻转,就可以进行添加操作了。
当新增元素的父亲节点(所需融合的节点)为2节点,这个简单,假如比此2节点的值小,则放入左孩子;比此2节点的值大,先放入右孩子,然后进行左旋转,旋转之后的根节点当成当前节点,继续与其父亲节点进行融合。
当新增节点的父亲节点是3节点,当新增的元素的值在3节点的两个元素值之间,则需要依次进行左旋转,右旋转,颜色翻转操作;当新增的元素的值比3节点的两个元素的最小值还小,需要依次执行右旋转,颜色翻转;当新增的元素的值比3节点的两个元素值的最大值还大,则直接颜色翻转。


添加元素

我们可以发现,红黑树的添加元素操作,操作顺序都是 左旋转——> 右旋转 ——> 颜色翻转,3个操作步骤可以跳过,但是顺序不变。
红黑树的添加元素操作我们是在二分搜索树的添加操作之上进行的。
添加元素的代码实现:

    @Override
    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) {
            //表示到达最后一个根节点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;
    }

最终,我们需要对根节点的颜色置为黑色。所以在递归之后,需要执行root.color = BLACK;递归添加元素还是二分搜索树的逻辑,我们需要做的就是在添加完成之后,对破坏了红黑树特性的情况使其重新恢复红黑树的特性。根据上面的分析,添加的动作执行的操作顺序是,左旋转——> 右旋转 ——> 颜色翻转。于是我们依次根据条件进行各个操作,如果符合左旋转条件:右孩子为红色节点并且左孩子为黑色节点,则执行左旋转操作,并且把旋转之后的根节点赋值给node,因为需要继续执行后续操作;如果符合右旋转条件:左孩子和左孩子的左孩子都为红色节点,则进行右旋转操作,并且旋转之后的根节点赋值给node;如果符合颜色翻转条件:左右孩子都为红色节点,则执行颜色翻转操作。

红黑树的性能总结

对于完全随机的数据,普通的二分搜索树很好用,因为没有旋转的操作从而没有性能的损耗,缺点是:极端情况退化成链表或者高度不平衡。对于查询较多的情况,AVL树很好用,因为红黑树牺牲了平衡性,树的高度可以达到近2h(h 为相同节点数量下的AVL树的高度)。但是添加或者删除元素,红黑树性能则优于AVL树,因为AVL是平衡的二叉树,对于平衡性的依赖极高,添加或者删除元素,极可能的破坏其平衡性,所以相对于红黑树,AVL的旋转操作次数会更多,从而影响了性能。所以对于统计性能(综合增删改查的所有操作)来说,红黑树是更优的。


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

推荐阅读更多精彩内容