红黑树之插入操作

一、背景引入

    二叉排序树的性能取决于二叉树的层数,最好的情况O(log n)存在于完全二叉排序树的情况下,其访问性能近似于折半查找(图左);最差的情况是O(n),比如插入的元素是有序的,生成的二叉树就是一个链表,这种情况下,需要遍历全部元素才能找到目标元素(图右)。


image.png

    为了改变排序二叉树存在的不足,Rudolf Bayer 在 1972 年发明了另一种改进后的排序二叉树:红黑树,他将这种排序二叉树称为“对称二叉 B 树”,而红黑树这个名字则由 Leo J. Guibas 和 Robert Sedgewick 于 1978 年首次提出。

二、什么是红黑树

先来看看Wikipedia 中有关Red–black tree的介绍:

A red–black tree is a kind of self-balancing binary search tree. Each node of the binary tree has an extra bit, and that bit is often interpreted as the color (red or black) of the node. These color bits are used to ensure the tree remains approximately balanced during insertions and deletions.
简单翻译:

红黑树,一种自平衡的二叉查找树,但在每个结点上有一个额外的存储位表示结点的颜色,可以是Red或Black。这些颜色位用来确保红黑树在插入和删除操作后仍能够近乎平衡。

性能分析:插入、删除、查找 最坏时间复杂度都为
image.png
image.png
应用领域

    由于统计性能好于平衡二叉树(AVL树),因此在很多地方都有应用。,比如在 Java 集合框架中,很多部分(HashMap, TreeMap, TreeSet 等)都有红黑树的应用,这些集合均提供了很好的性能。

应用举例

    HashMap作为Map接口的一个实现类,在java开发中经常用到。其数据结构如下图所示:


image.png

    从图中可以看出,HashMap底层使用的结构包括两部分:table和bucket。table指的是数组,是HashMap的主体,哈希表利用数组一次定位的特性,将当前元素的关键字通过某个函数映射到数组中的某个位置,通过数组下标一次定位就可以完成操作,存储位置=f(关键字)。bucket指的是链表,主要是为了解决哈希冲突而存在的,试想如果插入的元素中存在大量哈希冲突,那么链表的长度会越来越长,这样对于查找某一个元素可能需要遍历整个链表才能找到,其性能可想而知。因此在jdk1.8中HashMap的bucket部分引入了红黑树。

    首先看看红黑树长啥样:
image.png

    图中的叶节点并没有画出来,而是以NIL指针代替,这是因为叶节点在这里并不包含数据,只是作为结束符。
    为了便于理解,我们拿TreeMap来分析:

红黑树的数据结构
static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left = null;
    Entry<K,V> right = null;
    Entry<K,V> parent;
    boolean color = BLACK;  
    .......
}

其中Entry是TreeMap的内部类。

红黑树特性

红黑树在原有的二叉查找树基础上增加了如下几个要求:
1.Every node is either red or black.
2.The root is black.
3.Every leaf (NIL) is black.
4.If a node is red, then both its children are black.
5.For each node, all simple paths from the node to descendant leaves contain the same number of black nodes.
中文意思是:
1.每个节点要么是红色,要么是黑色;
2.根节点永远是黑色的;
3.所有的叶节点都是是黑色的(注意这里说叶子节点其实是上图中的 NIL 节点);
4.每个红色节点的两个子节点一定都是黑色;
5.从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点;

其中:
性质 3 中指定红黑树的每个叶子节点都是空节点,而且并叶子节点都是黑色。但 Java 实现的红黑树将使用 null 来代表空节点,因此遍历红黑树时将看不到黑色的叶子节点,反而看到每个叶子节点都是红色的。
性质 4 的意思是:从每个根到节点的路径上不会有两个连续的红色节点,但黑色节点是可以连续的。 因此若给定黑色节点的个数 N,最短路径的情况是连续的 N 个黑色,树的高度为 N - 1;最长路径的情况为节点红黑相间,树的高度为 2(N - 1)
性质 5 是成为红黑树最主要的条件,后序的插入、删除操作都是为了遵守这个规定。

红黑树并不是标准平衡二叉树,它以性质 5 作为一种平衡方法,使自己的性能得到了提升。

三、红黑树的三大基本操作

左旋:
image.png

右旋:


image.png

着色

红黑树的左旋右旋
image.png

    红黑树的左右旋是比较重要的操作,左右旋的目的是调整红黑节点结构,转移黑色节点位置,使其在进行插入、删除后仍能保持红黑树的 5 条性质。比如 X 左旋(右图转成左图)的结果,是让在 Y 左子树的黑色节点跑到 X 右子树去。

左旋

指定节点 x 的左旋 (右图转成左图):

//这里 p 代表 x
private void rotateLeft(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> r = p.right; // p 是上图中的 x,r 就是 y
        p.right = r.left;       // 左旋后,x 的右子树变成了 y 的左子树 β 
        if (r.left != null)         
            r.left.parent = p;  //β 确认父亲为 x
        r.parent = p.parent;        //y 取代 x 的第一步:认 x 的父亲为爹
        if (p.parent == null)       //要是 x 没有父亲,那 y 就是最老的根节点
            root = r;
        else if (p.parent.left == p) //如果 x 有父亲并且是它父亲的左孩子,x 的父亲现在认 y 为左孩子,不要 x 了
            p.parent.left = r;
        else                            //如果 x 是父亲的右孩子,父亲就认 y 为右孩子,抛弃 x
            p.parent.right = r;
        r.left = p;     //y 逆袭成功,以前的爸爸 x 现在成了它的左孩子
        p.parent = r;
    }
}

可以看到,x 节点的左旋就是把 x 变成 右孩子 y 的左孩子,同时把 y 的左孩子送给 x 当右孩子

右旋

指定节点 y 的右旋 (左图转成右图):

private void rotateRight(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> l = p.left;
        p.left = l.right;
        if (l.right != null) l.right.parent = p;
        l.parent = p.parent;
        if (p.parent == null)
            root = l;
        else if (p.parent.right == p)
            p.parent.right = l;
        else p.parent.left = l;
        l.right = p;
        p.parent = l;
    }
}

同理,y 节点的右旋就是把 y 变成 左孩子 x 的右孩子,同时把 x 的右孩子送给 x 当左子树。

四、红黑树的平衡插入

红黑树的插入主要分两步:
首先和二叉查找树的插入一样,查找、插入
然后调整结构,保证满足红黑树状态
      对结点进行重新着色
     以及对树进行相关的旋转操作

红黑树的插入在二叉查找树插入的基础上,为了重新恢复平衡,继续做了插入修复操作。

红黑树的插入修复

   红黑树的第 5 条特征规定,任一节点到它子树的每个叶子节点的路径中都包含同样数量的黑节点。也就是说当我们往红黑树中插入一个黑色节点时,会违背这条特征。

   同时第 4 条特征规定红色节点的左右孩子一定都是黑色节点,当我们给一个红色节点下插入一个红色节点时,会违背这条特征。

  因此我们需要在插入黑色节点后进行结构调整,保证红黑树始终满足这 5 条特征。

调整思想

插入、染红后的调整有 2 种情况:
情况1.父亲节点和叔叔节点都是红色:


image.png

情况2.父亲节点为红色,叔叔节点为黑色


image.png

前面说了,插入一个节点后要担心违反特征 4 和 5,数学里最常用的一个解题技巧就是把多个未知数化解成一个未知数。我们这里采用同样的技巧,把插入的节点直接染成红色,这样就不会影响特征 5,只要专心调整满足特征 4 就好了。这样比同时满足 4、5 要简单一些。染成红色后,我们只要关心父节点是否为红,如果是红的,就要把父节点进行变化,让父节点变成黑色,或者换一个黑色节点当父亲,这些操作的同时不能影响 不同路径上的黑色节点数一致的规则。
情况1:父亲节点和叔叔节点都是红色:
image.png

假设插入的是节点 N,这时父亲节点 P 和叔叔节点 U 都是红色,爷爷节点 G 一定是黑色。

红色节点的孩子不能是红色,这时不管 N 是 P 的左孩子还是右孩子,只要同时把 P 和 U 染成黑色,G 染成红色即可。这样这个子树左右两边黑色个数一致,也满足特征 4。

但是这样改变后 G 染成红色,G 的父亲如果是红色岂不是又违反特征 4 了?
这个问题和我们插入、染红后一致,因此需要以 爷爷节点 G 为新的调整节点,再次进行调整操作,以此循环,直到父亲节点不是红的,就没有问题了。

情况2:父亲节点为红色,叔叔节点为黑色
image.png

假设插入的是节点 N,这时父亲节点 P 是红色,叔叔节点 U 是黑色,爷爷节点 G 一定是黑色。

红色节点的孩子不能是红色,但是直接把父亲节点 P 涂成黑色也不行,这条路径多了个黑色节点。怎么办呢?

既然改变不了你,那我们就此别过吧,我换一个更适合我的!

我们怎么把 P 弄走呢?看来看去,还是右旋最合适,通过把 爷爷节点 G 右旋,P 变成了这个子树的根节点,G 变成了 P 的右子树。

右旋后 G 跑到了右子树上,这时把 P 变成黑的,多了一个黑节点,再把 G 变成红的,就平衡了!

情况2:父亲节点为红色,叔叔节点为黑色
image.png

N 位于 P 的右孩子位置,将 P 左旋,就化解成上述情况了。

源码分析
private void fixAfterInsertion(Entry<K,V> x) {
    x.color = RED;  //直接染成红色,少点麻烦
    //这里分析的都是父亲节点为红色的情况,不是红色就不用调整了
    while (x != null && x != root && x.parent.color == RED) {
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { // 插入节点 x 的父亲节点位于左孩子    
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));  // y 是 x 的叔叔节点
            if (colorOf(y) == RED) {    //如果 y 也是红色,只要把父亲节点和 y 都变成黑色,爷爷节点变成红的,就 Ok 了
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {    //如果叔叔节点 y 不是红色,就需要右旋,让父亲节点变成根节点,爷爷节点去右子树去,然后把父亲节点变成黑色、爷爷节点变成红色
                    //特殊情况:x 是父亲节点的右孩子,需要对父亲节点进行左旋,把 x 移动到左子树
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateLeft(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateRight(parentOf(parentOf(x)));
            }
        } else {  。。。。。。}}  //和上面对称的操作

五、一句话概括

在插入调整时为了简化操作我们直接把插入的节点涂成红色,这样只要保证插入节点的父节点不是红色就可以了。

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

推荐阅读更多精彩内容