Red Black Tree – 红黑树透彻分析

本文基于《算法第四版》中红黑树作者Robert Sedgewick的原理讲解及代码实现,从2-3树角度来观察和理解红黑树平衡调整设计动机,源码分析说明基于JDK 1.8版本HashMap,转载请注明出处

概述

RBT(Red Black Tree,红黑树)是一种BST(Balanced Search Tree,平衡二叉搜索树),RBT不是完全平衡二叉树,这导致其树高最小 $\scriptsize O(\lg N)$ 最大 $\scriptsize O(2 \lg N)$ ,即红黑树的的插入和查询性能

红黑树相比于其他BST的优点是 $\scriptsize O(1)$ 平衡维护成本,避免逐层传递的平衡调整,因而被广泛使用

应用

  • linux内核CFS进程调度器
  • EXT3文件系统
  • Java和C++中的Map和Set

性质

  • 节点仅红色或仅黑色
  • 根节点为黑色
  • 叶节点为黑色(NULL也算叶节点)
  • 红节点的父、子节点为黑色,黑节点父、子节点可红可黑
  • 任一节点为根出发到叶节点的所有路径上黑色节点数(黑高)相同
  • RBT中序遍历为升序序列

实现

以Java中的HashMap源码为例

平衡旋转操作

旋转的目的是降低后文中LL和RR型树的高度,使树更平衡。分为左旋与右旋,此处以左旋为例

rotate operation

static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root, TreeNode<K,V> p) {
    TreeNode<K,V> r, pp, rl;
    if (p != null && (r = p.right) != null) {
        if ((rl = p.right = r.left) != null)       // 1 左旋后rl成为p的子节点
            rl.parent = p;
        if ((pp = r.parent = p.parent) == null)    // 2 左旋后r替代原p位置,成为root
            (root = r).red = false;
        else if (pp.left == p)                     // 2 左旋后r替代原p位置,成为pp.left
            pp.left = r;
        else
            pp.right = r;                          // 2 左旋后r替代原p位置,成为pp.right
        r.left = p;                                // 3 左旋修改指针
        p.parent = r;                              // 4 左旋修改指针
    }
    return root;
}

插入操作

插入节点

根据BST性质找到插入位置

final void treeify(HashMap.Node<K,V>[] tab) {
    HashMap.TreeNode<K,V> root = null;
    for (HashMap.TreeNode<K,V> x = this, next; x != null; x = next) {
        next = (HashMap.TreeNode<K,V>)x.next;
        x.left = x.right = null;
        if (root == null) {                                       // 根节点单独处理
            x.parent = null;
            x.red = false;
            root = x;
        }
        else {
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            for (HashMap.TreeNode<K,V> p = root;;) {              // 迭代找到插入位置
                int dir, ph;
                K pk = p.key;
                if ((ph = p.hash) > h)                            // 小节点插入位置在左子树
                    dir = -1;
                else if (ph < h)                                  // 大节点插入位置在右子树
                    dir = 1;
                else if ((kc == null &&
                        (kc = comparableClassFor(k)) == null) ||
                        (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);
 
                HashMap.TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) { // null为插入位置,否则迭代找子树
                    x.parent = xp;
                    if (dir <= 0)
                        xp.left = x;                               // 执行小节点插入左子树
                    else
                        xp.right = x;                              // 执行大节点插入右子树
                    root = balanceInsertion(root, x);              // 平衡调整
                    break;
                }
            }
        }
    }
    moveRootToFront(tab, root);
}

平衡调整

需要检查插入的新节点是否破坏了RBT性质,若破坏则需要进行平衡调整以保证RBT性质使树平衡。导致不满足RBT性质的原因有三:

  • CASE1:分为L型和R型,此处分析L型
    case1
  • CASE2:分为LR型和RL型,此处分析LR型
    case1
  • CASE3:分为LL型和RR型,此处分析LL型
    case1
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) {
    x.red = true;                                              // 新插节点均视为RED节点
    for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
        if ((xp = x.parent) == null) {
            x.red = false;
            return x;
        }
        else if (!xp.red || (xpp = xp.parent) == null)         // 红节点才是不平衡原因,黑节点以上均满足RBT性质
            return root;                                       // 第一次插入根节点时、向上检查至根节点时全树已平衡
        if (xp == (xppl = xpp.left)) {
            if ((xppr = xpp.right) != null && xppr.red) {      // CASE1:L
                xppr.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            else {
                if (x == xp.right) {                           // CASE2:LR
                    root = rotateLeft(root, x = xp);
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) {                              // CASE3:LL
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        root = rotateRight(root, xpp);
                    }
                }
            }
        }
        else {
            if (xppl != null && xppl.red) {                    // CASE1:R
                xppl.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            else {
                if (x == xp.left) {                            // CASE2:RL
                    root = rotateRight(root, x = xp);
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) {                              // CASE3:RR
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        root = rotateLeft(root, xpp);
                    }
                }
            }
        }
    }
}

删除操作

删除节点

首先查找到待删节点,再确定合适的子节点替换待删节点,其实就是BST的节点删除,详见下图

bst-delete

待删节点只有左孩子或只有右孩子的情况可取子节点直接替代

但若同时有左右孩子,仍取直接子节点替换的话会带来问题:如取l替换u需要将LR分支插入r子树的L分支,可能会增加树的高度降低查询性能。r替换u同理。不推荐

为了维护BST性质,取l<v<r的节点v替换节点u较为合适。而取r子树的最小节点v时,v只可能有右孩子,此时修改最少;选择lmax同理

r右子树的最小节点就是r本身、或无右孩子时可直接替换待删节点。若不是则说明r右子树的最小节点v还有右子树vr,需要依次执行v替换u、vr替换v

安全地删除红色节点

从2-3树的视角来看,删除红色节点全树仍然保持平衡:
所有红色节点均在3-节点中,删除红色节点仅是将3-节点变为2-节点,2-3树任何子树的高度均未发生变化


从红黑树的视角来看,删除红色节点不改变红黑树的性质:
删除红色节点并不会改变所在路径上的黑色节点数目,即黑高不变
待删红色节点被黑色节点替代时,必然不会引起双红问题
待删红色节点被红色节点替代时,其直接父子节点必为黑色,直接子节点替换时必然不会产生双红,即使替换节点v是红色也不会造成原待删位置u发生双红(下图例3),而红色的替换节点v限制rl和vr必为黑色,其右孩子vr替换v时也不会出现双红(下图例4)
由于红色节点不是根节点,删除红色节点后的替代不是根节点,也没有向根节点的颜色调整,根仍然保持黑色


delete-check-1

谨慎地删除黑色节点

从2-3树的视角来看,删除红色节点全树仍然保持平衡:
所有红色节点均在3-节点中,删除红色节点仅是将3-节点变为2-节点,2-3树任何子树的高度均未发生变化


从红黑树的视角来看,删除红色节点不改变红黑树的性质:
删除红色节点并不会改变所在路径上的黑色节点数目,即黑高不变
待删红色节点被黑色节点替代时,必然不会引起双红问题
待删红色节点被红色节点替代时,其直接父子节点必为黑色,直接子节点替换时必然不会产生双红,即使替换节点v是红色也不会造成原待删位置u发生双红(下图例3),而红色的替换节点v限制rl和vr必为黑色,其右孩子vr替换v时也不会出现双红(下图例4)
由于红色节点不是根节点,删除红色节点后的替代不是根节点,也没有向根节点的颜色调整,根仍然保持黑色


delete-check-2

平衡调整

case1
case2
case3
case4

参考资料

Thomas H. Cormen,Charles E. Leiserson,Ronald L. Rivest,Clifford Stein.Introduction to Algorithms, Third Edition.[M]. 北京:机械工业出版社, 2013. 1
Sedgewick R, Wayne K. Algorithms, Fourth Edition[M]. 4. 人民邮电出版社, 2012.
RedBlackBST – Robert Sedgewick – Princeton University
邓俊辉. 数据结构(C++语言版 第3版)[M]. 3. 清华大学出版社, 2013.

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

©2018-2024 Howell版权所有 备案号:冀ICP备19000576号