《算法导论》读书笔记之红黑树

什么是红黑树?

红黑树是一棵二叉搜索树,它的结点上新增了一个存储位来表示结点的颜色,颜色可以是红或者黑。通过颜色进行约束,确保没有一条路径会比其他路径长出两倍,因此是近似平衡的。

红黑树满足如下五条性质:

  1. 每个结点要么是红色,要么是黑色。
  2. 根结点是黑色的。
  3. 每个叶结点是黑色的。
  4. 如果一个结点是红色的,那么它的两个子结点必然是黑色的。
  5. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点。

** 请注意红黑树的这五点性质非常重要,因为红黑树所有操作都必须满足这五点性质,否则就不能称为红黑树。 **

另外为了简洁和节省空间,我们定义一个叶结点NIL,它的颜色属性为黑,leftrightparent都指向自身且key值没有意义,所有指向叶结点的结点都指向这一个结点,根结点的父亲也指向这个叶结点NIL。下图便是一个简单的红黑树:

image

下面是结点的定义:

public enum Color {RED, BLACK};

    class Node {
        public Node left;
        public Node right;
        public Node parent;
        public int key;
        public Color color;
    }

旋转

红黑树的插入和删除操作都要借助于旋转,所以这里先讲解下旋转操作。一共有两种旋转:左旋和右旋。当某个结点x在做左旋时,假设它的右孩子是y而不是NIL。左旋以x到y的链为支轴进行。它使y成为子树的新根结点,x成为y的左孩子,y的左孩子成为x的右孩子。并且左旋操作保持了二叉搜索树的性质。右旋操作同理,如下图所示:


image

下面是旋转代码:

public void leftRotate(Node x) {
        if (x == NIL || x.right == NIL) return;
        Node y = x.right;
        x.right = y.left; //将y的左孩子接到x的右结点上
        if (y.left != NIL) {
            y.left.parent = x;
        }
        //使y成为子树的新根结点
        if (x.parent == NIL) {
            root = y;
        } else if (x == x.parent.left) {
            x.parent.left = y;
        } else {
            x.parent.right = y;
        }
        y.parent = x.parent;
        y.left = x; //x接到y的左结点上
        x.parent = y;
    }

    public void rightRatate(Node x) {
        if (x == NIL || x.left == NIL) return;
        Node y = x.left;
        x.left = y.right;
        if (y.right != NIL) {
            y.right.parent = x;
        }
        if (x.parent == NIL) {
            root = y;
        } else if (x.parent.left == x) {
            x.parent.left = y;
        } else {
            x.parent.right = y;
        }
        y.parent = x.parent;
        y.right = x;
        x.parent = y;
    }

红黑树的插入

红黑树的插入思想是新结点以二叉搜索树的性质插入到书中,并且把新节点染成红色,再以新结点往上进行调整使之满足红黑树的性质。下面是红黑树的插入代码:

public void rb_insert(int key) {
        Node newNode = new Node();//包装成新结点
        newNode.key = key;
        newNode.left = NIL;
        newNode.right = NIL;
        newNode.parent = NIL;
        newNode.color = Color.RED;
        //将新结点以二叉搜索树的性质插入到树种
        Node x = root;
        Node y = NIL;
        while (x != NIL) {
            y = x;
            if (key < x.key) {
                x = x.left;
            } else {
                x = x.right;
            }
        }
        newNode.parent = y;
        if (y == NIL) {
            root = newNode;
        } else if (key < y.key) {
            y.left = newNode;
        } else {
            y.right = newNode;
        }
        rb_insert_fixup(newNode); //再对新结点进行调整以满足红黑树的性质
    }

插入操作中性质1和性质3一定不会破坏,另外新结点被染成红色,那么各条路径上的黑结点数量不会变,那么性质5也不会破坏。这样一来,插入操作仅有可能破坏的性质就是2和4。维护性质2很简单,只需要最后将root结点染成黑色。重点就是如何在rb_insert_fixup中维护性质4且不破坏其他性质。这里我们设新结点为z,z父结点p为红色(z父结点为黑时此时已经满足了性质4),z父结点为红色显然z父结点的父结点q不是NIL(根结点为黑色),设p为q的left(p为q的right时可以同理),设y为q的right。rb_insert_fixup主要分为下面三种情况:

  1. z的叔结点也就是y为红色


    image

    这种情况下,将z父结点和y结点都染成黑色,z父结点的父结点染成红色,此时z父结点的父结点成为新的z。从图中可以看出这个操作并没有破坏原来的性质。新结点z还要再进行三种情况的判断。

  2. z的叔结点y是黑色,并且z是父结点的一个右孩子
    这种情况下,z的父结点成为新的z并且以新z进行左旋此时就会转变为情况3进行处理。

  3. z的叔结点y是黑色,并且z是父结点的一个左孩子

    image

    这种情况下将z父结点染黑,z父结点的父结点染红并以z父结点进行右旋处理。从图中我们可以看出旋转过程中始终保持了性质5没有被破坏,并且情况3的处理修复了性质4。这样一来就完成了红黑树的插入调整。下面是rb_insert_fixup代码:

private void rb_insert_fixup(Node z) {
        while (z.parent != NIL && z.parent.color == Color.RED) {
            if (z.parent == z.parent.parent.left) {
                Node y = z.parent.parent.right;
                if (y.color == Color.RED) { //情况1
                    y.color = Color.BLACK;
                    z.parent.color = Color.BLACK;
                    z.parent.parent.color = Color.RED;
                    z = z.parent.parent;
                } else if (z == z.parent.right) { //情况2
                    z = z.parent;
                    leftRotate(z);
                } else { //情况3
                    z.parent.color = Color.BLACK;
                    z.parent.parent.color = Color.RED;
                    rightRatate(z.parent.parent);
                }
            } else {
                Node y = z.parent.parent.left;
                if (y.color == Color.RED) { //情况1 
                    y.color = Color.BLACK;
                    z.parent.color = Color.BLACK;
                    z.parent.parent.color = Color.RED;
                    z = z.parent.parent;
                } else if (z == z.parent.left) { //情况2
                    z = z.parent;
                    rightRatate(z);
                } else { //情况3
                    z.parent.color = Color.BLACK;
                    z.parent.parent.color = Color.RED;
                    leftRotate(z.parent.parent);
                }
            }
        }
        root.color = Color.BLACK; //满足红黑树性质2 
    }

用一个具体实例看下整个调整的流程:

image

a图此时属于情况1,经过调整后7结点成为了新的z继续向上调整。此时z叔结点1已经是黑色并且z是一个右孩子属于情况2,经过情况2的左旋处理后变成了情况3,经过情况3的右旋处理后调整成功,现在满足红黑树的所有性质,又成为了一棵红黑树。红黑树的整个插入过程运行时间为O(lgn)

红黑树的删除

红黑树的删除操作比插入更加复杂,也更加不好理解,这里我就写下我自己的理解可能细节方面有些偏差。首先删除操作需要用到子过程rb_transplant,它的作用和二叉搜索树中的transplant相似,只是判断NULL变成了判断NIL叶结点。下面是rb_transplant代码:

private void rb_transplant(Node p, Node q) {
        if (p == NIL) return;
        Node parent = p.parent;
        if (parent == NIL) this.root = q;
        else if (parent.left == p) parent.left = q;
        else parent.right = q;
        if (q != NIL) q.parent = parent;
    }

接下来就是删除操作了,注意红黑树的删除操作是基于二叉搜索树修改的,所以要理解红黑树的删除就必须先理解二叉搜索树的删除。这里我们设要删除的结点为z并且z在树中真实存在。y指向“需要删除的结点”,注意这里"需要删除的结点"意思是逻辑上需要删除的结点,比如:如果z两个孩子结点都不是NIL,那么y会指向z右子树的最小孩子,这是因为我们知道二叉搜索树删除操作中y最终会顶替z成为新的z,我们只需要将y结点的颜色变为z的颜色,这样y就成为了逻辑上要删除的结点。我们设x指向顶替原来y结点的结点,这里有点拗口,解释一下,如果z左孩子是NIL,那么y指向的是z,顶替原来y结点的结点就是z的右孩子所以x指向z的右孩子,如果z两个孩子都不是NIL,那么y会指向z右子树的最小结点,此时顶替原来y结点的结点就是y的右孩子,所以x指向y的右孩子。如果原来y的颜色为红色,那么红黑树的性质全都没有被破坏,完成删除操作。但如果是黑色就要进行调整操作了。下面是删除的代码:

public Node rb_delete(int val) {
        Node z = search(val);
        if (z == NIL) return z; //找到要删除的结点
        Node y = z;
        Node x;
        Color originalColor = y.color;//y会被赋予z的颜色所以要保存原来y的颜色
        f(z.left == NIL) {
            x = z.right;
            rb_transplant(z, z.right);
        } else if (z.right == NIL) {
            x = z.left;
            rb_transplant(z, z.left);
        } else {
            y = z.right;  //z孩子都不是NIL那么y会指向z右孩子的最小结点
            while (y.left != NIL) {
                y = y.left;
            }
            originalColor = y.color;//记得要保存原来y的颜色
            x = y.right;
            if (y != z.right) {
                rb_transplant(y, y.right);
                y.right = z.right;
                z.right.parent = y;
            }
            rb_transplant(z, y);
            y.left = z.left;
            z.left.parent = y;
            y.color = z.color;//z的颜色赋给y,相当于要删除的结点为原来的y
        }
        if (originalColor == Color.BLACK) {//如果原来y为黑色要调整以满足红黑树性质
            rb_deleteFixUp(x);
        }
        return z;
    }

如果原来的y是黑色会有下面三种情况破坏红黑树的性质:

  1. 如果删除的是个根结点并且根结点的一个红孩子成为了新根结点,这就不满足性质2,这种情况需要将顶替的结点染成黑色即可。

  2. 如果x是红色的并且x的父结点是红色这就不满足性质4,这种情况需要接下来分情况讨论。

  3. 在树中移动y,使得任何包含y的简单路径上黑色结点个数少1,这就不满足性质5,这种情况我们可以将现在占有原来y位置的x增加一重额外的黑色(y是没有左孩子的)。这样如果x的颜色为黑色那么它就是双重黑色,如果x的颜色为红色,那么它就是红黑色。这其实破坏了性质1,我们只需要配合旋转,重新染色等调整后将x的颜色变成红色,这时只需将x染黑就满足了性质1,或者x成为根结点,这就相当于移除了一重黑色也就满足了性质1。

我们设x结点为黑色并且x不是根结点(如果x是红色或者x是根结点只要简单的将x染黑便满足所有性质),c为x的父结点,设x是c的left,w是c的right也就是x的兄弟结点,下面就来分情况讨论:(注意图中黑色代表黑色结点,灰色代表红色结点,白色代表任意红或黑结点)


image
  1. x的兄弟结点w为红色
    这种情况下,我们只需要将w染黑将x的父结点染红并且以x的父结点进行左旋,w重新指向x的兄弟结点。这样就将情况1转化为情况2、3或4处理。

  2. x的兄弟结点w为黑色,并且w的左右子结点也为黑色
    注意此时x是双重黑色,所以w必然有两个不为NIL的子结点,否则不满足性质5。这种情况下只需将w染红,然后x指向x的父结点,注意到如果新x是红色,那么只要将x染成黑色便已满足所有红黑树性质。

  3. x的兄弟结点w为黑色,并且w的左孩子为红色,右孩子为黑色
    这种情况下,我们需要将w染红,w的左孩子染黑然后以w进行右旋,w重新指向x的兄弟结点从而转化为情况4。

  4. x的兄弟结点w为黑色,并且w的右孩子为红色
    这种情况下,w赋予c的颜色,c染黑,w的右孩子染黑,再以c进行左旋。从图中便可看出这种情况的处理满足除性质2外的所有性质,所以将x指向root,最终将x染黑便满足了所有性质。

x是父结点的右孩子时同理,至此就完成了整个删除调整操作,以下是删除调整的代码:

private void rb_deleteFixUp(Node x) {
        while (x != root && x.color == Color.BLACK && x != NIL) {
            if (x == x.parent.left) {
                Node w = x.parent.right;
                if (w.color == Color.RED) { //情况1
                    w.color = Color.BLACK;
                    x.parent.color = Color.RED;
                    leftRotate(x.parent);
                    w = x.parent.right;
                }
                if (w.left.color == Color.BLACK && w.right.color == Color.BLACK) {//情况2
                    w.color = Color.RED;
                    x = x.parent;
                } else if (w.right.color == Color.BLACK) { //情况3
                    w.color = Color.RED;
                    w.left.color = Color.BLACK;
                    rightRatate(w);
                    w = x.parent.right;
                }
                if (w.right.color == Color.RED) { //情况4
                    w.color = x.parent.color;
                    w.right.color = Color.BLACK;
                    x.parent.color = Color.BLACK;
                    leftRotate(x.parent);
                    x = root;
                }
            } else {
                Node w = x.parent.left;
                if (w.color == Color.RED) { //情况1
                    w.color = Color.BLACK;
                    x.parent.color = Color.RED;
                    rightRatate(x.parent);
                    w = x.parent.left;
                }
                if (w.left.color == Color.BLACK && w.right.color == Color.BLACK) {//情况2
                    w.color = Color.RED;
                    x = x.parent;
                } else if (w.left.color == Color.BLACK) { //情况3
                    w.color = Color.RED;
                    w.right.color = Color.BLACK;
                    leftRotate(w);
                    w = x.parent.left;
                }
                if (w.left.color == Color.RED) { //情况4
                    w.color = x.parent.color;
                    w.left.color = Color.BLACK;
                    x.parent.color = Color.BLACK;
                    rightRatate(x.parent);
                    x = root;
                }
            }
        }
        x.color = Color.BLACK;
    }

整个删除过程的运行时间为O(lgn)

总结

花了两天时间终于把红黑树啃完了,大体上算是理解了,可能细节方面有些偏差。理解之后就会发现当初发明红黑树的人是多么的牛X,其实红黑树用途挺广泛的,比如java8的Hashmap中就用到了红黑树,以往冲突部分是链表相连,现在当链表中的结点大于某个值是就采用红黑树存储,这样在冲突较高的场合下效率有了显著的提升。

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

推荐阅读更多精彩内容

  • 这个周看算法导论,看到红黑树,看的我云里雾里绕啊。虽然最后看懂了,据我估计,要是过一个星期不看保证忘干净,因此决定...
    充满活力的早晨阅读 1,939评论 0 3
  • 0.目录 1.算法导论的红黑树本质上是2-3-4树 2.红黑树的结构和性质 3.红黑树的插入 4.红黑树的删除 5...
    王侦阅读 2,463评论 1 2
  • 目录 0.树0.1 一般树的定义0.2 二叉树的定义 1.查找树ADT 2.查找树的实现2.1 二叉查找树2.2 ...
    王侦阅读 7,124评论 0 3
  • 红黑树为一棵二叉搜索树,它为每个结点增加一个变量存储结点颜色,利用结点颜色对树的形状进行约束,使其近似平衡(并非完...
    LRC_cheng阅读 476评论 0 1
  • 1964年,拍过007的英国大导演迈克尔·艾普泰德开始记录14个7岁的英国小孩,每隔7年就会找到这些孩子,拍下他们...
    CARRIE1阅读 658评论 0 3