防老年痴呆系列之手撕红黑树

红黑树 是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组(可以用非负整数或者字符串等不同类型的值作索引的特殊数组)。它是在1972年由鲁道夫·贝尔发明的,他称之为“对称二叉B树”,它现代的名字是在 Leo J. Guibas 和 Robert Sedgewick 于1978年写的一篇论文中获得的。它是复杂的,但它的操作有着良好的最坏情况运行时间,并且在实践中是高效的:它可以在O(log n)时间内做查找,插入和删除,这里的n是树中元素的数目。

红黑树性质

红黑树是每个节点都带有颜色属性的二叉查找树,因此红黑树不仅具备二叉查找树的性质外,还具备以下性质:

性质1. 节点是红色或黑色。

性质2. 根是黑色。

性质3. 所有叶子都是黑色(叶子是NIL节点)。

性质4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)

性质5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

由以上性质可以推倒出以下结论:从根节点到叶子节点的最长路径长度不大于最短路径的两倍。因此红黑树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。

为什么这些特性确保了这个结果?注意到性质4要求路径不能有两个毗连的红色节点。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。因为根据性质5可知所有最长的路径都有相同数目的黑色节点,这就表明了没有路径能多于任何其他路径的两倍长。

在很多树数据结构的表示中,一个节点有可能只有一个子节点,而叶子节点包含数据。用这种范例表示红黑树是可能的,但是这会改变一些属性并使算法复杂。为此,本文中我们使用 "NIL 叶子" 或"空(NULL)叶子",如上图所示,它不包含数据而只充当树在此结束的指示。这些节点在绘图中经常被省略,导致了这些树好像同上述原则相矛盾,而实际上不是这样。与此有关的结论是所有节点都有两个子节点,尽管其中的一个或两个可能是空叶子。另外基于这种表示,黑色节点数是包含叶子节点数的,否则可能得出错误的结论,比如以下两种方式表示的有两个黑色节点的红黑树,但是右图比左图更容易识别其是不合法的红黑树。

红黑树操作

红黑树的常用操作有查找、插入和删除,其中插入和删除是难点,本文主要讨论插入和删除操作。在讲述具体操作前,先普及其中涉及到的旋转操作。

旋转

左旋:将当前旋转节点变成其右孩子的左节点。旋转操作方法:以旋转节点右孩子为支点,以逆时针方向(向左)旋转当前节点到右孩子的左下方(左孩子)。此时当前旋转节点的父节点指针指向旋转支点(当前旋转节点的右孩子),如下图沿着PN轴,以N为支点逆时针旋转:

//将子节点转向父节点同一方向
private void leftRotate(RedBlackNode pivotNode) {
    pivotNode.leftChild = pivotNode.parent;
    pivotNode.parent.rightChild = null;
    pivotNode.parent = pivotNode.parent.parent;
    pivotNode.parent.leftChild = pivotNode;
    pivotNode.leftChild.parent = pivotNode;
}

注:上面右图中沿着NP轴以P为支点顺时针旋转可以还原到左图,代码描述为:

//将子节点和父节点从同一方向转离
private void leftRotate2(RedBlackNode pivotNode) {
    if (pivotNode.parent.parent != null && pivotNode.parent == pivotNode.parent.parent.leftChild) {
        pivotNode.parent.parent.leftChild = pivotNode;
    } else if (pivotNode.parent.parent != null && pivotNode.parent == pivotNode.parent.parent.rightChild) {
        pivotNode.parent.parent.rightChild = pivotNode;
    }
    pivotNode.leftChild = pivotNode.parent;
    pivotNode.parent = pivotNode.parent.parent;
    if (pivotNode.parent == null)
        this.root = pivotNode;
    pivotNode.leftChild.parent = pivotNode;
    pivotNode.leftChild.rightChild = null;
}

两个方法的区别是前者是将支点的父节点和子节点旋转到同一方向,后者是将

两节点转离同一方向。另外沿着不同的旋转轴旋转可能形成不同的根节点,比如如果沿着GN右转,新的根节点为N。

右旋:将当前旋转节点变成其左孩子的右节点。旋转操作方法:以旋转节点左孩子为支点,以顺时针方向(向右)旋转当前节点到左孩子的右下方(右孩子),如下图所示沿着GP轴,以P为支点顺时针旋转:

//将子节点转向父节点同一方向
private void rightRotate(RedBlackNode pivotNode) {
    pivotNode.rightChild = pivotNode.parent;
    pivotNode.parent.leftChild = null;
    pivotNode.parent = pivotNode.parent.parent;
    pivotNode.parent.rightChild = pivotNode;
    pivotNode.rightChild.parent = pivotNode;
}

同上上面右图沿着PG方向以G为支点逆时针旋转还原到左图:

//将子节点和父节点从同一方向转离
private void rightRotate2(RedBlackNode pivotNode) {
    if (pivotNode.parent.parent != null && pivotNode.parent == pivotNode.parent.parent.leftChild) {
        pivotNode.parent.parent.leftChild = pivotNode;
    } else if (pivotNode.parent.parent != null && pivotNode.parent == pivotNode.parent.parent.rightChild) {
        pivotNode.parent.parent.rightChild = pivotNode;
    }
    pivotNode.rightChild = pivotNode.parent;
    pivotNode.parent = pivotNode.parent.parent;
    if (pivotNode.parent == null)
        this.root = pivotNode;
    pivotNode.rightChild.parent = pivotNode;
    pivotNode.rightChild.leftChild = null;
}

由图可知, 左旋只影响旋转结点和其右子树的结构,把右子树的结点往左子树挪了;右旋只影响旋转结点和其左子树的结构,把左子树的结点往右子树挪了。同时对支点的选取也符合排序二叉树的性质,以右旋为例:

插入

根据排序二叉树的插入规则,新插入的节点一定位于叶子节点(不考虑节点已经存在的情况,如果节点已经存在直接返回)。同时为了满足红黑树的性质,减少违规的可能性,新增的节点初始为红色,然后根据情况再做调整,具体有以下几种情况:

1. 当前树为空,新增节点为树根节点;

第1种情况最简单,直接将红色改为黑色即可。

2. 新增节点的父节点为黑色;

不论新增的红色节点是左孩子还是右孩子,第2种情况插入的红色节点(N)不影响红黑树的性质,所以不需要调整。

3. 新增节点的父节点为红色;

最难的是第3种,因为父节点(P)为红色,祖父节(G)点为黑色,于是调整就要看叔叔节点(U)的“脸色”了。注意此时叔叔节点只可能为红色或者不存在。因为如果叔叔节点为黑色,根据红黑树性质,父节点必然有两个黑色子节点,跟能插入新的子节点矛盾。于是下面按照叔叔节点不存在和叔叔节点为红色两类分别说明。

3.1.叔叔节点不存在

这种情况下新增节点必然有一个黑色的兄弟节点(B),根据父节点和当前新增节点的位置又组合为4种情况:

3.1.1 父节点和新增节点都为左孩子

private void rotateRight(RedBlackNode pivotNode) {
    rightRotate2(pivotNode);
    Color tempColor = pivotNode.color;
    pivotNode.color = pivotNode.rightChild.color;
    pivotNode.rightChild.color = tempColor;
}

此时以GP为轴,P为支点向右旋转,同时交换P、G节点颜色。

3.1.2 父节点为左孩子,新增节点为右孩子

leftRotate(newNode);
rotateRight(newNode);

此时以PN为轴,N为支点向左旋转,然后按照3.1.1的操作继续操作,如上图所示。

3.1.3 父节点和新增节点都为右孩子

此时沿着GP轴,P为支点向左旋转,同时交换PG颜色。

private void rotateLeft(RedBlackNode pivotNode) {
    leftRotate2(pivotNode);
    Color tempColor = pivotNode.color;
    pivotNode.color = pivotNode.leftChild.color;
    pivotNode.leftChild.color = tempColor;
}

3.1.4 父节点为右孩子,新增节点为左孩子

此时沿着PN轴,以N为支点,向右旋转,然后按照3.1.3的方式操作。

rightRotate(newNode);
rotateLeft(newNode);

2 叔叔节点为红色

这种情况将父叔节点和祖父节点交换颜色,然后以祖父节点为新的插入节点,递归向上作插入调整。

private void adjustAfterInsertion(RedBlackNode newNode) {
    if (newNode.parent == null) {
        newNode.color = Color.BLACK;
        return;
    }
    //父节点为红色
    if (newNode.parent != null && newNode.parent.color == Color.RED) {
        //新节点和父节点都为左孩子,叔叔节点不存在
        if (newNode == newNode.parent.leftChild && newNode.parent == newNode.parent.parent.leftChild && newNode.parent.parent.rightChild == null) {
            rotateRight(newNode.parent);
        }
        //新节点为右节点、父节点为左孩子,叔叔节点不存在
        else if (newNode == newNode.parent.rightChild && newNode.parent == newNode.parent.parent.leftChild && newNode.parent.parent.rightChild == null) {
            leftRotate(newNode);
            rotateRight(newNode);
        }
        //新节点和父节点都为右孩子
        else if (newNode == newNode.parent.rightChild && newNode.parent == newNode.parent.parent.rightChild && newNode.parent.parent.leftChild == null) {
            rotateLeft(newNode.parent);
        }
        //新节点为左节点、父节点为右孩子
        else if (newNode == newNode.parent.leftChild && newNode.parent == newNode.parent.parent.rightChild && newNode.parent.parent.leftChild == null) {
            rightRotate(newNode);
            rotateLeft(newNode);
        }
        //叔叔节点存在且为必为红色
        else {
            newNode.parent.color = Color.BLACK;
            //如果叔叔为左子节点
            if (newNode.parent == newNode.parent.parent.rightChild)
                newNode.parent.parent.leftChild.color = Color.BLACK;
            else
                newNode.parent.parent.rightChild.color = Color.BLACK;
            newNode.parent.parent.color = Color.RED;
            adjustAfterInsertion(newNode.parent.parent);
        }
    }
}

删除

待删除节点(D)根据孩子数量可划分成3种情况:无孩子节点,有一个孩子节点和有2个孩子节点。根据二叉排序树的性质可知,任意节点的后继节点(R)一定位于树末节点(最多有一个子节点的节点,不包括NIL节点)。于是对于删除有2个孩子的节点可以转化为先用后继节点代替待删除节点,然后再删除后继节点。所谓后继节点就是大于当前节点的最小节点,为了更形象的展示,将所有节点投影到X水平轴上:

比如节点2的后继节点为3,也即节点2右孩子的最左孩子节点。因为删除节点可能会导致黑色数量减少1个,引起“黑色冲突”,于是需要一个修正过程,接下来删除节点就以前两种情况详细说明修正过程:

1. 待删除节点只有一个孩子节点

此时待删除节点一定是黑色而且孩子节点是红色。因为该孩子节点是待删除节点的唯一后继节点,只需要删除后继节点,然后用后继节点的值代替待删除节点的值即可。因为删除红色的后继节点没有影响红黑树的性质,不需要修正过程。

2. 待删除节点无孩子节点

待删除节点没有孩子,根据颜色可以划分成两种情况

2.1 待删除节点为红色

此时父节点(P)为黑色,兄弟节点(B)也为红色且无孩子节点(如果存在的话)。因为删除红色的后继节点没有影响红黑树的性质,不需要修正过程。

2.2 待删除节点为黑色,父节点为红色

此时调整不仅要看父节点和兄弟节点的“脸色”还要看兄弟节点的“体型”,由于红色的父节点决定兄弟节点颜色必须也是黑色,我们根据父节点的颜色和兄弟节点有无孩子节点再拆分以下几种情况:

2.2.1 父节点为红色且兄弟节点左孩子不为空

删除节点分左右子节点两种情况。当删除节点为右子节点的时候,需要区分兄弟节点的右子节点存在和不存在两种情况,如下所示:

//先以兄弟左孩子右转
rightRotate(successor.parent.rightChild.leftChild);
//再以原来兄弟左孩子(当前为兄弟)左转
leftRotate2(successor.parent.rightChild);

//先以兄弟为支点左转
leftRotate(successor.parent.leftChild.rightChild);
//再以父节点为支点右转
rightRotate2(successor.parent.leftChild);

//以兄弟为支点右转
rightRotate2(successor.parent.leftChild);

2.2.2 父节点为红色且兄弟节点右孩子不为空,左孩子为空

删除节点分左孩子和右孩子两种情况,如下图:

//以兄弟为支点左转
leftRotate2(successor.parent.rightChild);

//以兄弟右孩子为支点左转
leftRotate(successor.parent.rightChild);
//以原来兄弟的右孩子(现在的兄弟节点)为支点右转
rightRotate2(successor.parent.leftChild);

2.2.3 父节点为红色且兄弟节点无孩子节点

2.3 待删除节点为黑色,父节点为黑色

2.3.1 父节点为黑色且兄弟节点左孩子不为空

同2.2.1,删除节点分左右子节点两种情况。当删除节点为右子节点的时候,需要区分兄弟节点的右子节点存在和不存在两种情况,如下所示:

以上图第一个为例:

//先以兄弟左孩子右转

rightRotate(successor. parent . rightChild . leftChild )

;

//再以原来兄弟左孩子(当前为兄弟)左转

leftRotate2(successor. parent . rightChild )

;

successor. parent . parent . color = Color. BLACK

;

//以兄弟为支点右转
rightRotate2(successor.parent.leftChild);

2.3.2 父节点为黑色且兄弟节点右孩子不为空,左孩子为空

删除节点分左孩子和右孩子两种情况,如下图:

以上图第二个图为例:

//以兄弟右孩子为支点左转

leftRotate(successor. parent . rightChild )

;

//以原来兄弟的右孩子(现在的兄弟节点)为支点右转

rightRotate2(successor. parent . leftChild )

;

successor. parent . parent . color = Color. BLACK

;

2.3.3 父节点为黑色且兄弟节点无孩子节点

这种情况下将兄弟节点变成红色,但是无法通过调整子树达到平衡,只能把当前子树当做一个被删除了的虚拟的黑色节点,重复以上步骤递归向上调整.

RedBlackNode newSuccessor = successor.parent;

adjustBeforeRemove(newSuccessor) ;

总体的删除节点处理流程描述为:

public void removeNode(RedBlackNode removeNode) {
    RedBlackNode successor = findSuccessor(removeNode);
    if (successor != null) {
        removeNode.key = successor.key;
        removeNode.value = successor.value;
    }
    if (successor == null) {
        successor = removeNode;
    }
    if (successor.leftChild == null && successor.rightChild == null) {
        adjustBeforeRemove(successor);
    }
    freeSuccessor(successor);

}

//释放最多一个孩子的节点
private void freeSuccessor(RedBlackNode successor) {
    //后继有一个节点且为红色的右子节点
    if (successor.rightChild != null) {
        successor.key = successor.rightChild.key;
        successor.value = successor.rightChild.value;
        //释放右子节点
        successor.rightChild.parent = null;
        successor.rightChild = null;
    }
    //右边为空,左边一定为红
    else if (successor.leftChild != null) {
        successor.key = successor.leftChild.key;
        successor.value = successor.leftChild.value;
        successor.leftChild.parent = null;
        successor.leftChild = null;
    }
    //后继节点无子节点
    else {
        if (successor.parent != null && successor == successor.parent.leftChild)
            successor.parent.leftChild = null;
        else if (successor.parent != null && successor == successor.parent.rightChild)
            successor.parent.rightChild = null;
        else
            this.root = null;
        successor.parent = null;
    }
}
我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章