红黑树(Red-Black Tree)是二叉搜索树(Binary Search Tree)的一种改进。我们知道二叉搜索树在最坏的情况下可能会变成一个链表(当所有节点按从小到大的顺序依次插入后)。而红黑树在每一次插入或删除节点之后都会花O(log N)的时间来对树的结构作修改,以保持树的平衡。也就是说,红黑树的查找方法与二叉搜索树完全一样;插入和删除节点的的方法前半部分节与二叉搜索树完全一样,而后半部分添加了一些修改树的结构的操作。
红黑树的每个节点上的属性除了有一个key、3个指针:parent、lchild、rchild以外,还多了一个属性:color。它只能是两种颜色:红或黑。而红黑树除了具有二叉搜索树的所有性质之外,还具有以下4点性质:
1. 根节点是黑色的。
2. 空节点是黑色的(红黑树中,根节点的parent以及所有叶节点lchild、rchild都不指向NULL,而是指向一个定义好的空节点)。
3. 红色节点的父、左子、右子节点都是黑色。
4. 在任何一棵子树中,每一条从根节点向下走到空节点的路径上包含的黑色节点数量都相同。
如下图就是一棵红黑树:
有了这几条规则,就可以保证整棵树的平衡,也就等于保证了搜索的时间为O(log N)。
但是在插入、删除节点后,就有可能破坏了红黑树的性质。所以我们要做一些操作来把整棵树修补好。下面我就来介绍一下。
首先有一个预备知识,那就是节点的Left-Rotate和Right-Rotate操作。所谓Left-Rotate(x)就是把节点x向左下方向移动一格,然后让x原来的右子节点代替它的位置。而Right-Rotate当然就是把Left-Rotate左、右互反一下。如下图:
注意,Left-Rotate(x)后,x的右子树变成了原来y的左子树,Right-Rotate反之。思考一下,这样一次变换后,仍然满足二叉搜索树的性质。在红黑树的插入、删除中,要用到很多Left-Rotate和Right-Rotate操作。
一、 插入
插入首先是按部就班二叉搜索树的插入步骤,把新节点z插入到某一个叶节点的位置上。
接下来把z的颜色设成红色。为什么?还记得红黑树的性质吗,从根节点向下到空节点的每一条路径上的黑色节点数要相同。如果新插入的是黑色节点,那么它所在的路径上就多出了一个黑色的节点了。所以新插入的节点一定要设成红色。但是这样可能又有一个矛盾,如果z的父节点也是红色,怎么办,前面说过红色节点的子节点必须是黑色。因此我们要执行下面一个迭代的过程,称为Insert-Fixup,来修补这棵红黑树。
在Insert-Fixup中,每一次迭代的开始,指针z一定都指向一个红色的节点。如果z->parent是黑色,那我们就大功告成了;如果z->parent是红色,显然这就违返了红黑的树性质,那么我们要想办法把z或者z->parent变成黑色,但这要建立在不破坏红黑树的其他性质的基础上。
这里再引入两个指针:grandfather,指向z->parent->parent,也就是z的爷爷(显然由于z->parent为红色,grandfather一定是黑色);uncle,指向grandfather除了z->parent之外的另一个子节点,也就是z的父亲的兄弟,所以叫uncle。
(为了说话方便,我们这里都假设z->parent是grandfather的左子节点,而uncle是grandfather的右子节点。如果遇到的实际情况不是这样,那也只要把所有操作中的左、右互反就可以了。)
在每一次迭代中,我们可能遇到以下三种情况。
Case 1. uncle也是红色。这时只要把z->parent和uncle都设成黑色,并把grandfather设成红色。这样仍然确保了每一条路径上的黑色节点数不变。然后把z指向grandfather,并开始新一轮的迭代。如下图:
Case 2. uncle是黑色,并且z是z->parent的右子节点。这时我们只要把z指向z->parent,然后做一次Left-Rotate(z)。就可以把情况转化成Case 3。
Case 3. uncle是黑色,并且z是z->parent的左子节点。到了这一步,我们就剩最后一步了。只要把z->parent设成黑色,把grandfather设成红色,再做一次Right-Rotate(grandfather),整棵树就修补完毕了。可以思考一下,这样一次操作之后,确实满足了所有红黑树的性质。Case 2和Case 3如下图:
反复进行迭代,直到某一次迭代开始时z->parent为黑色而告终,也就是当遇到Case 3后,做完它而告终。
二、删除
让我们来回顾一下二叉搜索树的删除节点z的过程:如果z没有子节点,那么直接删除即可;如果z只有一个子节点,那么让这个子节点来代替z的位置,然后把z删除即可;如果z有两个子节点,那么找到z在中序遍历中的后继节点s(也就是从z->rchild开始向左下方一直走到底的那一个节点),把s的key赋值给z的key,然后删除s。
红黑树中删除一个节点z的方法也是首先按部就班以上的过程。
如果删除的节点是黑色的,那么显然它所在的路径上就少一个黑色节点,那么红黑树的性质就被破坏了。这时我们就要执行一个称为Delete-Fixup的过程,来修补这棵树。下面我就来讲解一下。
一个节点被删除之后,一定有一个它的子节点代替了它的位置(即使是叶节点被删除后,也会有一个空节点来代替它的位置。前面说过,在红黑树中,空节点是一个实际存在的节点。)。我们就设指针x指向这个代替位置的节点。
显然,如果x是红色的,那么我们只要把它设成黑色,它所在的路径上就重新多出了一个黑色节点,那么红黑树的性质就满足了。
然而,如果x是黑色的,那我们就要假想x上背负了2个单位的黑色。那么红黑树的性质也同样不破坏,但是我们要找到某一个红色的节点,把x上“超载”的这1个单位的黑色丢给它,这样才算完成。Delete-Fixup做的就是这个工作。
Delete-Fixup同样是一个循环迭代的过程。每一次迭代开始时,如果指针x指向一个红色节点,那么大功告成,把它设成黑色即告终。相反如果x黑色,那么我们就会面对以下4种情况。
这里引入另一个指针w,指向x的兄弟。这里我们都默认x是x->parent的左子节点,则w是x->parent的右子节点。(如果实际遇到相反的情况,只要把所有操作中的左、右互反一下就可以了。)
Case 1. w是红色。这时我们根据红黑树的性质可以肯定x->parent是黑色、w->lchild是黑色。我们把x->parent与w的颜色互换,然后做一次Left-Rotate(x->parent)。做完之后x就有了一个新的兄弟:原w->lchild,前面说过它一定是黑色的。那么我们就在不破坏红黑树性质的前提下,把Case 1转换成了Case2、3、4中的一个,也就是w是黑色的情况。如下图:
Case 2. w是黑色,并且w的两个子节点都是黑色。这时我们只要把w设成红色。然后把x移到x->parent,开始下一轮迭代(注意,那“超载”的1单位的黑色始终是跟着指针x走的,直到x走到了一个红色节点上才能把它“卸下”)。思考一下,这一次操作不会破坏红黑树的性质。这如下图(图中节点B不一定是红色,也可能是黑色):
Case 3. w是黑色,并且w的右子节点也是黑色。这时我们把w与w->lchild的颜色互换,然后做Right-Rotate(w)。思考一下,这样做之后不会破坏红黑树的性质。这时x的新的兄弟就是原w->lchild。而Case 3被转化成了Case 4。
Case 4. w是黑色,并且w的右子节点是红色。一但遇到Case 4,就胜利在望了。我看下面一张图。先把w与x->parent的颜色互换,再做Left-Rotate(x->parent)。这时图中节点E(也就是原w->rchild)所在的路径就肯定少了一个黑色,而x所在的路径则多了一个黑色。那么我们就把x上多余的1个单位的黑色丢给E就可以了。至此,Delete-Fixup就顺利完成了。
以下是我的红黑树代码,其中的所有函数都不包含递归(当然,由于树的层数不会很大,要写成递归也可以)。可以用它来解决UVA #544(http://online-judge.uva.es/p/v5/544.html)和UVA #10909(http://online-judge.uva.es/p/v109/10909.html)。
//Key中的内容可能更复杂,比如字符串等。 struct RBTNode{ class RBT{ void clear() { void delFixup(RBTNode* delNode) { void insertFixup(RBTNode* insertNode) { //比较两个Key的大小。这里可能有更复杂的比较,如字符串比较等。 //把一个节点向左下方移一格,并让他原来的右子节点代替它的位置。 //把一个节点向右下方移一格,并让他原来的左子节点代替它的位置。 //找到子树中最大的一个节点 //找到子树中最小的一个节点 public: ~RBT() { //找到从小到大排序后下标为i的节点。i从0开始。 //删除一个节点 RBTNode* temp = toDel; RBTNode* replace = toDel->lchild != m_null? toDel->lchild: toDel->rchild; void init() { //插入一个节点 RBTNode* p = m_root; int nodeCount() { //按照key查找一个节点。 //一个节点在中序遍列中的下一个节点。 //一个节点在中序遍列中的前一个节点。 |