逻辑结构.树.红黑树

236 阅读5分钟

引言

二叉查找树其实就是二分查找在数据结构上的体现,所以理想情况下在bst中搜索某个结点的时间复杂度为O(lgn)O(lgn)。但这只是理想情况,bst其实是不稳定的,若出现了斜树这种极端不平衡的情况,搜索复杂度就是线性的:

此时的树就已经退化成了链表。所以,为了维护二叉树的平衡性,在每次插入删除后尽量保持树的高度在O(lgn)O(lgn),就出现了AVL树、红黑树。今日就先来学习学习红黑树的插入操作部分。

红黑树

红黑树就是一种==自平衡的==二叉查找树,且满足以下的性质:

  1. 结点是红色或者黑色。
  2. 根节点是黑色
  3. 叶子结点NIL也看作是黑色的。(Leaf不包含任何数据信息)
  4. 路径上不会有连续的两个红色结点,即每个红色结点的两个子结点都是黑色。
  5. 从任意结点,到其叶子结点的简单路径,包含相同数目的黑色结点。

在以上的限制下,红黑树从根到最远的叶子节点的路径长度不会大于从根到最近的叶子节点长度的2倍。一个极端的例子即可证明,最近的情况下,根到叶子全部是黑色节点;最远情况下就是黑红相间。由性质5,根到叶子路径上的黑色节点个数相同,那么这个极端的例子最远路径长度就恰好是最近路径长度的2倍。

红黑树插入

红黑树的插入其实就是在普通BST插入后,维护其红黑性质。维护性质主要用到的就是旋转和变色,变色就是红变黑、黑变红,非常容易理解。旋转通过两幅图其实也非常好理解:

  1. 在结点x处的左旋:

  2. 在结点x处的右旋:

插入的新节点初始都是Red的。因为若插入的新节点是黑色,一定会导致该节点到根的路径上增加一个黑色节点,导致破坏红黑树的性质。所以新节点设置为红色,是为了尽量减少插入后维护红黑性质的可能。

在插入完成后,就需要对红黑树的性质进行维护。维护的情况有多种,为了方便表述:令K=新插入的结点,P=父结点,G=爷爷结点,U=父亲的兄弟结点,T=树,eg:

1. T为空

直接令根节点为K,同时将根变成黑色。

2. P为Black

插入了新结点没有破坏任何性质。

3. P为Red

此时插入了K之后,P、K为两个连续的Red结点,不满足性质4,需要维护。由于初始的时候P为Red,故P必然不是根结点,所以P存在一个黑色的父节点G,接着继续分情况讨论:

3.1 P为Red且U为Red

将G、U、P的进行反色那么当前子树就满足了红黑性质。由于G变成的Red,有可能会继续向上影响,所以==将G看作K继续维护==,直至G的父节点为空或者Black。

3.2 P为Red且U为Black/NIL

这种情况中又可以进一步细分

3.2.1 Right-Right case

P为G的右孩子,K为P右孩子

3.2.2 Right-Left case

P为G的右孩子,K为P的左孩子。只需要在P处进行一次右旋,就变成了3.2.1。

3.2.3 Left-Left case

就是与3.2.1情况的镜像

3.2.4 Left-Right case

3.2.2的镜像,在P进行左旋,就转换成了4.2.3,不再赘述。

jdk11 HashMap红黑树插入部分源码

// 树节点的结构
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // 用于在删除操作的时候删除下一个节点
    boolean red;
    TreeNode(int hash, K key, V val, Node<K,V> next) {
        super(hash, key, val, next);
    }
    ...
}
// 返回平衡后的根结点
// root 当前根结点
// x    新插入的节点
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                            TreeNode<K,V> x) {
    x.red = true;
    for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
        if ((xp = x.parent) == null) {  // x没有父节点说明本身就是根,变色返回即可
            x.red = false;
            return x;
        }
        else if (!xp.red || (xpp = xp.parent) == null) // 父节点为黑色 或者父节点就是根结点 => 直接插入节点不会影响红黑树性质
            return root;
        // 父节点为红色,必须要进行平衡操作了
        if (xp == (xppl = xpp.left)) {  // 爸爸是爷爷的左孩子
            if ((xppr = xpp.right) != null && xppr.red) { // case4.1: 叔叔节点为红色,只需要反色处理
                xppr.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;    // 反色完成后,爷爷变成了红色的节点,可能会向上继续有影响。故将爷爷重新看作插入节点x
            }
            else {  // case4.2:叔叔为黑色节点的情况
                if (x == xp.right) { // case4.2.4: left-right 左旋统一转换成 left-left
                    root = rotateLeft(root, x = xp);
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) { // case4.2.3: left-left 右旋+变色
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        root = rotateRight(root, xpp);
                    }
                }
            }
        }
        // 爸爸是爷爷的右孩子,(其实就是上边的所有情况的镜像)
        else {
            if (xppl != null && xppl.red) { // case4.1: 叔叔节点为红色,反色处理
                xppl.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp; // 反色后,爷爷变成红色,向上继续有影响。爷爷重新看作插入节点x
            }
            else {  // case4.2: 叔叔节点为黑色
                if (x == xp.left) { // case4.2.2: right-left case,右旋转统一成 left-left case
                    root = rotateRight(root, x = xp);
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) { // case4.2.1: left-left case 左旋并变色
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        root = rotateLeft(root, xpp);
                    }
                }
            }
        }
    }
}