红黑树

红黑树

更多面经和面试题目解答欢迎访问https://github.com/darkmoon233/GreenPills-CSNotes

1.二叉搜索树:任何节点的键值一定大于其左子树中的每一个节点的键值,并小于其右子树中的每一个节点的键值。

2.平衡二叉搜索树(没有一个节点过深)

3.AVL tree 任何一个节点的左右子树高度相差最多1.

4.红黑树:

4.1红黑树的规则

“叶结点” 或"NULL结点",它不包含数据而只充当树在此结束的指示,这些结点以及它们的父结点,在绘图中都会经常被省略。

  1. 每个结点要么是红的,要么是黑的。
  2. 根结点是黑的。
  3. 每个叶结点(叶结点即指树尾端NIL指针或NULL结点)是黑的。
  4. 如果一个结点是红的,那么它的俩个儿子都是黑的。
  5. 对于任一结点而言,其到叶结点树尾端NIL指针的每一条路径都包含相同数目的黑结点

4.2树的旋转

当树的出现深度不平衡时,就需要进行部分子树的旋转。

4.2.1单旋转

在二叉平衡树的外侧插入(插入到X节点左子树的左节点,或是插入到X节点右子树的右节点)引起的平衡被破坏,通过单旋转(左旋或是右旋)可以解决

红黑树_第1张图片

伪代码:

//T为二叉搜索树,x为要进行旋转的节点
Left-Rotate(T,x)
    y <- x.right                   // 对y赋值
    x.right <- y.left              // 将y的左节点赋给x做右节点(一开始y是x的右节点,最终目标是将y的左节点给x做右节点,y和x的地位互换)
    y.left.p <- x                  // 修改y的左节点的父节点为x(双(三)向赋值才算是一次完整的节点换位
    y.p <- x.p                     // 将x的父节点赋给y的父节点
    // 根据x所处的位置不同进行分类处理,x如果是根节点则根节点改为y,x如果在父节点的左(右)节点,将父节点的左(右)节点换为y节点的
    if x.p = null
    else if x=x.p.left
        then x.p.left <- y
        else x.p.right <- y
    y.left <- x
    x.p <- y

右旋的过程是非常像的,不多赘述。

4.2.2 双旋转

在二叉平衡树的内侧插入(插入到X节点左子树的右节点,或是插入到X节点左子树的右节点)引起的平衡被破坏,通过双旋转(单旋转两次)可以解决
红黑树_第2张图片

4.3红黑树的插入

4.3.1二叉查找树插入

二叉查找树插入就是左右比较一下,找到位置直接插就行。(不存在插入的值已经存在的情况)
伪代码:

Btree-Insert(T,z)
    y <- null
    x <- T.root
    while x!= null
        do y <- x
            if x.key > z.key
                then x <- x.left
            else x <- right
    z.p <- y
    if y=null
        then z <- T.root
    else if y.key > z.key
            then y.left <- z
        else y.right <- z

4.3.2红黑树插入和插入和插入修复

由于红黑树除了引入二叉平衡树的基础外,还引入了颜色等一系列规则,因此在调整之后需要修改。
红黑树的插入(和普通的二叉搜索树一样,只是增加了最后的涂色操作和修正操作):

Rb-Insert(T,z)
    y <- null
    x <- T.root
    while x != null
        do y <- x
            if z.key < x.key
                then x <-x.left
                else x <- x.right
    z.p <- y
    if y = null
        then T.root <- z // 空树
        else if z.key < y.key
            then y.left <- z
            else y.right <- z
    z.left = null
    z.right = null
    z.color = red
    Rb-Insert-Fixup(T,z)  // 进行修正

分析一下插入的几种情况:

  1. z没有父节点(插入的是空树),z成为红黑树的根节点,违反了根节点的颜色性质;

红黑树_第3张图片

  1. z的父节点为黑色,不违反性质;

红黑树_第4张图片

  1. z的父节点为红色,违反相邻两节点不能都为红色的性质(但这种情况必有祖父节点,且祖父必为黑色);

红黑树_第5张图片

对于情况1,直接将节点涂黑就可以了(因为不涉及到其他的规则)
对于情况2,红黑树没有被破坏,不需要进行额外的工作
对于情况3,单纯的变色不能完全解决问题,需要进行更进一步的分类。

修复红黑树的伪代码:

Rb-Insert-Fixup(T,z)
    while z.p.color = red
        do if z.p = z.p.p.left
            then y <- z.p.p.right
                if y.color = red                    // 情况1 :uncle节点为红色
                    then z.p.color <- black         // 父节点重染色为黑色
                         y.color <- black           // uncle节点重染色为黑色
                         z.p.p.color <- red         // 祖父节点重染色为红色
                         z <- z.p.p                 // z指向祖父节点,重新进行染色流程(因为将祖父变色可能会带来违反红黑树规则的连锁反应)
                    else if z = z.p.right           // 情况2 :uncle节点为黑色,且自己为父节点的右子树
                         then z <- z.p              // z指向z的父节点,以当前节点进行左旋
                              Left-Rotate(T,z)
                         z.p.color <- black         // 情况3 :uncle节点为黑色,自己为父节点的左子树(如果是经历了左旋的话,那么z已经是左节点了)
                         z.p.p.color <- red         // 父节点染色为黑色,祖父节点染色为红色。
                         Right-Rotate(T,z.p.p)      // 以祖父节点进行右旋
            else                                    // 交换一下旋转方向
    T.root.color <- black

下面用一个例子表现一下插入一个节点之后的过程(图片来源:julycoding)

  1. 新插入了4这个节点,当前节点是N

红黑树_第6张图片

  1. 我们发现,他父节点和他的uncle都是红色节点,启动第一方案,重新染色,把他的father、uncle和grandpa全换一遍色,然后当前节点指向grandpa

红黑树_第7张图片

  1. 这时候一看,当前节点的father是红的,uncle是黑色的,自己是右子树,启动方案2!左旋,当前节点指向父节点

红黑树_第8张图片

  1. 再一看,父节点红色,uncle是黑色的,自己又变成了左子树,启动方案3!右旋(其实方案3+方案2就是双旋转)

红黑树_第9张图片

这时候大功告成,没有问题了。

为了避免父子节点皆为红色的情况持续性的向上发展导致时间上的瓶颈,可以用以下的方法解决:新增节点为A,就沿着A的路径,只要看到某节点X的两个节点都是红色,就把X改为红色,两个子节点改为黑色,但是如果X的父节点也是红色,就使用之前的策略进行调整,进行单旋转或是双旋转并改变颜色。这样就可以避免不断地向上回溯(因为已经把回溯的问题解决了)
过程和如下图示是一致的。

红黑树_第10张图片

红黑树_第11张图片

红黑树_第12张图片

4.4红黑树的删除

待删除节点有三种情况:

  1. 没有子节点。这种时候将父节点的子节点指向NULL就行了。
  2. 只有一个儿子。子节点升格成父节点的子节点就行了。
  3. 有俩儿子。选一个儿子继承自己的位置。

红黑树的删除节点的过程实际上是先用二叉查找树(BST)的方法进行删除,再用红黑树特有的方法进行调整。

二叉查找树的删除节点的情况如下:

  1. 删除叶节点,即当前被删除节点没有子节点。这种时候将父节点的子节点指向NULL就行了。

红黑树_第13张图片

  1. 删除只有一个子节点的节点。待删除的节点的父节点的儿子节点指针指向待删除的节点的子节点(就是我的父指向我的子)

红黑树_第14张图片

  1. 删除有两个个子节点的节点。方法如下:
    • 找到左子树(右子树)的最大值(最小值)的值value(一般是左)
    • 将当前节点的值替换为value
    • 在子树中删除值为value的节点

红黑树_第15张图片

红黑树删除节点的过程如下

Rb-Tree-Delete(T,z)
    if z.left = null or z.right = null  // 至少一个子节点为空
        then y <- z                     // y用来表示待删除的节点
        else y <- Tree-Successor(z)     // 将z的后继节点赋值给y(后继节点是中序遍历后的集合每个元素的下一个元素)
    // 在至少一个子节点为空的情况下用x代替那个节点
    if y.left != null
        then x <- y.left
        else x <- y.right
    // 删除的如果是根节点,则用子节点代替父节点成为根节点,否则就是用子节点替换删除节点的位置
    x.p <- y.p                          // 删除操作
    if y.p = null
        then T.root <- x
        else if y = y.p.left
            then y.p.left <- x
            else y.p.right <- y
    // 有两个儿子时候用儿子的value替换当前节点的值
    if y!=z
        then z.key <- y.key
            copy y's satellite data into z
    if y.color = Black                   // 删除红色节点不会带来规则的违反,删除黑色可能会带来各种问题
        then Rb-Delete-Fixup(T,x)
    return y                             // 返回被删除节点

删除后恢复红黑树特性:

Rb-Delete-Fixup(T,x)
    while x != T.root and x.color = black
        do if x = x.p.left                                                  // 若x是父节点的左孩子,令w是他的兄弟节点
            then w <- x.p.right
                if w.color = red                                            // 兄弟是红色的
                    then w.color <- black                                   // case1:将兄弟节点X设置为黑色(这里导致后边w的颜色总是黑色的)
                         x.p.color <- red                                   // case1:将父节点置为红色
                         Left-Rotate(T,x.p)                                 // case1:以x的父节点为支点进行左旋
                         w <- x.p.right                                     // case1: w定义为x的新的兄弟
                if w.left.color = black and w.right.color = black           // w的两个儿子都是黑色
                    then w.color <- red                                     // case2: 兄弟节点设为红色
                         x <- x.p                                           // case2: 当前节点变为父节点
                    else if w.right.color = black                           // 兄弟节点的右儿子是黑色,左儿子红色(根据之前的if来的)
                            then w.left.color <- black                      // case3: 兄弟节点的左儿子的颜色变为黑色
                                w.color <- red                              // case3: 兄弟节点变红色
                                Right-Rotate(T,w)                           // case3: 右旋
                                w <- x.p.right                              // case3: w指向新的兄弟节点
                        w.color <- x.p.color                                // case4:兄弟节点用他的父亲的颜色染色
                        x.p.color <- black                                  // case4: 父节点染成黑色
                        w.right.color <- black                              // case4: 兄弟节点的右儿子染成黑色
                        Left-Rotate(T,x.p)                                  // case4: 以x的父节点进行左旋
                        x <- T.root                                         // case4: 设置x为根节点
            else (same as then clause with "right" and "left" exchanged)
    x.color <- black

上面的修复情况看起来有些复杂,下面我们用一个分析技巧:我们从被删结点后来顶替它的那个结点开始调整,并认为它有额外的一重黑色。这里额外一重黑色是什么意思呢,我们不是把红黑树的结点加上除红与黑的另一种颜色,这里只是一种假设,我们认为我们当前指向它,因此空有额外一种黑色,可以认为它的黑色是从它的父结点被删除后继承给它的,它现在可以容纳两种颜色,如果它原来是红色,那么现在是红+黑,如果原来是黑色,那么它现在的颜色是黑+黑。有了这重额外的黑色,原红黑树性质5就能保持不变。现在只要恢复其它性质就可以了,做法还是尽量向根移动和穷举所有可能性。"--saturnman。

对不起我没看懂saturnman的话。 --greenpill

对于恢复红黑树的性质有如下的几种情况:

  1. 当前节点是黑色且为根节点,此时不需要任何操作(对应第一个判断)
  2. 当前节点是红色的(红+黑),此时只需要将红色重新染色为黑色,就完事了。(对应的是最后一句直接染色(因为接替的是黑色节点的位置,无论如何都要染成黑色的))
  3. case1: 当前节点是黑+黑,且兄弟节点是红色的(因为兄弟是红色,所以他的子节点都是黑的)
  4. case2: 当前节点是黑+黑,且兄弟节点是黑色的,且兄弟节点的子节点都是黑色的
  5. case3: 当前节点是黑+黑,且兄弟节点是黑色的,且兄弟节点的左子是红色的,右子是黑色的
  6. case4: 当前节点是黑+黑,且兄弟节点是黑色的,且兄弟节点的右子是红色的,左子是任意的

前两种情况实际上已经解决了,现在把后面四种情况处理一下。
case1:当前节点是黑+黑,且兄弟节点是红色的(因为兄弟是红色,所以他的子节点都是黑的)
兄弟节点染成黑色,父节点染成红色,然后左旋。
还要记得重设兄弟节点方便后续操作。

红黑树_第16张图片

case2:当前节点是黑+黑,且兄弟节点是黑色的,且兄弟节点的子节点都是黑色的
兄弟节点染色为红色,当前节点变为父节点

红黑树_第17张图片

case3: 当前节点是黑+黑,且兄弟节点是黑色的,且兄弟节点的左子是红色的,右子是黑色的
把兄弟节点染红,之后以兄弟节点未支点进行右旋,更新w为新的兄弟节点

红黑树_第18张图片

case4:当前节点是黑+黑,且兄弟节点是黑色的,且兄弟节点的右子是红色的,左子是任意的
兄弟节点用他的父亲的颜色染色,父节点染成黑色,兄弟节点的右儿子染成黑色,以x的父节点进行左旋,设置x为根节点

红黑树_第19张图片

待添加删除节点的示例。

PS.后继节点的求法

后继节点指的是中序遍历中的后一个节点
若一个节点有右子树,那么该节点的后继节点是其右子树中val值最小的节点(也就是右子树中所谓的leftMostNode)
若一个节点没有右子树,那么判断该节点和其父节点的关系
2.1 若该节点是其父节点的左边孩子,那么该节点的后继结点即为其父节点
2.2 若该节点是其父节点的右边孩子,那么需要沿着其父亲节点一直向树的顶端寻找,直到找到一个节点P,P节点是其父节点Q的左边孩子(可参考例子2的前驱结点是1),那么Q就是该节点的后继节点

ps.ps 我也看到有使用前驱节点来考虑红黑树的,我觉得这两种没什么太大区别
本文github地址:https://github.com/darkmoon233/GreenPill-Notes/blob/master/网易面试/红黑树.md 会介绍我的面试和笔试的内容。

你可能感兴趣的:(面试)