红黑树(Red Black Tree) 是一种自平衡二叉查找树。红黑树和AVL树类似,都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。红黑树可以在O(log n)时间内完成查找,插入和删除操作。
二叉搜索树可以看 二叉搜索树
AVL树可以看 AVL树的插入与删除
1. 红黑树的性质
红黑树的自平衡依赖于它的以下性质:
性质1. 结点是红色或黑色。
性质2. 根结点是黑色。
性质3. 每个结点节点(NIL结点,空结点,与其它二叉搜索树不同,红黑树将叶子结点的孩子连接到NIL结点,以简化边界条件)是黑色的。
性质4. 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)
性质5. 从任一结点到其每个叶子结点的所有路径都包含相同数目的黑色结点。
这里给出一个定义,一个结点到其叶子结点的路径上的黑色结点的个数,称为该结点的黑高,根据性质5可以知道,一个结点的左子树和右子树的黑高应当相同。
根据性质5,我们还可以知道红黑树的平衡度:一个结点的左右两颗子树的高度相差最多一倍。极端情况下,一颗子树a中全部都是黑色结点,另一颗子树b中则是红黑相间,由于它们的黑高相同,所以b中的红色结点与黑色结点的数量相同,因此b的高度是a的两倍。
一颗红黑树如下图所示。
红黑树的所有“叶子结点”都指向同一个NIL哨兵结点,这个NIL哨兵结点是红黑树T的一个成员变量,此时这些“叶子结点”就不再是叶子结点了,所以它们可以使黑色的也可以是红色的,哨兵NIL结点一定是黑色的。此外,T.Root即红黑树的根结点的父结点也指向哨兵NIL结点。我们只需要记住一点,NIL结点只是用来简化插入和删除后调整树的平衡的边界条件而已,对于树本身的性质没有任何影响。这一点在后续的算法介绍和示例代码中会体现到。
先给出旋转的程序,这与AVL树的旋转是一样的,只是不需要调整结点的高度而已。
void leftRotate(Tree *tree, Node *x) { Node *y = x->right; x->right = y->left; if (y->left != tree->nil && y->left != NULL) { y->left->parent = x; } y->parent = x->parent; if (x->parent == tree->nil || x->parent == NULL) { tree->root = y; } else if (x == x->parent->left) { x->parent->left = y; } else { x->parent->right = y; } y->left = x; x->parent = y; } void rightRotate(Tree *tree, Node *x) { Node *y = x->left; x->left = y->right; if (y->right != tree->nil && y->right != NULL) { y->right->parent = x; } y->parent = x->parent; if (x->parent == tree->nil || x->parent == NULL) { tree->root = y; } else if (x == x->parent->left) { x->parent->left = y; } else { x->parent->right = y; } y->right = x; x->parent = y; }
上述代码分别是对tree中的结点x进行左旋和右旋的操作。旋转操作说到底就是一个修改指针指向的操作,因此它可以在常数时间内完成。
2. 红黑树的插入
和AVL树一样,在插入和删除结点之后,红黑树也是通过旋转来调整树的平衡的。红黑树插入结点z的方法和普通二叉搜索树一样,都是将新结点z作为一个叶子结点插入到树的底部。不同的是,红黑树将新结点z作为一个红色结点,将其孩子指针指向NIL结点,然后当新结点z的父结点为红色时,由于违反了性质4,因此需要对其进行调整(如果新结点z的父结点为黑色,则不会违反任何性质,尤其是因为z是红色,所以不会违反性质5,即黑高不变)。
红黑树调整算法的设计要遵循一个原则:同一时刻红黑树只能违反至多一条性质。
红黑树插入结点z后的调整有3种情况。
情况1. z的叔结点y是红色的。
左图中插入的新结点z是一个红色结点,其父结点A是红色的,违反了性质4,所以需要进行调整(由于结点A是红色的,根据性质4,由于树本身是平衡的,所以结点C必然是黑色的)。因为其叔结点y是红色的,于是可以修改结点A,结点B为黑色,此时结点C的黑高就会发生变化,从原来的0(忽略子树a、b、r、d、e的黑高)变成了1,因此,还需要将结点C变成红色以保持其黑高不变。此时,由于结点C由黑色变成了红色,如果结点C的父结点是红色的,那么就会违反性质4,于是结点C变成了新的结点z,从这里开始向上回溯调整树。
对于新插入的结点z是结点A的左子树的情况与上述一致。
对于新插入的结点z是结点C的右子树的结点的情况与上述对称。
情况1是一种比较简单的情况。
情况2. z的叔结点y是红色的且z是一个右孩子
情况2不能像情况1一样通过修改z的父结点的颜色来维持性质4,因为如果将z的父结点变成了黑色,那么树的黑高就会发生变化,必然会引起对性质5的违反。以上面情况1的图为例,假设此时结点y为黑色,那么结点C的右子树高为1(忽略子树d和e),左子树高也相同,如果简单的修改结点A为黑色,那么结点C的左子树的黑高会比右子树大1,此时即使将结点C修改为红色也于事无补。
此时可以通过旋转结点z的父结点使情况2转变成情况3进行处理。
情况3. z的叔结点y是红色的且z是一个左孩子
情况2转变成情况3然后针对情况3进行处理的流程可以看下图。
情况2通过对结点A进行一次左旋转变成情况3,此时结点z不再是原来的B,而是结点A了,此时树依然只是违反性质4。情况3通过对结点C进行一次右旋,然后改变结点B和结点C的颜色,得到右图。
先来证明这一操作的正确性:
对于左图,由于刚插入结点z的时候,只违反了性质4,性质5依然满足,假设子树a的黑高为ha,子树b的黑高为hb,依次类推,可以知道 hb==hr==ha==hd,hC=hd+1,对结点A进行左旋转变成情况3,即中图时,树依然只违反性质4,新的结点z为结点A,之后再对结点C右旋并修改颜色得到右图,此时结点A和结点C均为平衡的,结点B也是平衡的,而且结点B的黑高为hd+1。由此可知,整个操作后,该树的黑高不变,且满足所有红黑树的性质。
在红黑树的调整过程中,z始终指向一个红色的结点,因此z永远不会影响其所在树的黑高,于是我们始终关注结点z的父结点是否为红色,如果是,则意味着违反了性质4,需要调整,否则就可以退出循环了。在算法的最后,我们还需要关注性质2,将根结点的颜色改为黑色,根结点的颜色改变也是绝对不会引起树的不平衡的,而将其改为黑色也是不会引起对性质4的违反的。
下面是红黑树及其结点的定义
typedef struct Node { int value; int color; // 结点颜色 struct Node *parent; struct Node *left; struct Node *right; } Node; typedef struct Tree { Node *root; Node *nil; // 哨兵 } Tree;
下面是C实现的红黑树插入的程序:
void insertRBTree(Tree *tree, int value) { if (tree->root == NULL) { tree->root = createRBNode(tree, value); RBInsertFixup(tree, tree->root); return; } Node *node = createRBNode(tree, value); Node *n = tree->root; while (1) { if (value < n->value) { if (n->left == tree->nil) { n->left = node; node->parent = n; break; } n = n->left; } else { if (n->right == tree->nil) { n->right = node; node->parent = n; break; } n = n->right; } } RBInsertFixup(tree, node); }
总的来说,该程序与一般二叉搜索树的插入式类似的,只是在插入完成后需要对新插入的结点node调用RBInsertFixup方法来调整。
void RBInsertFixup(Tree *tree, Node *z) { while (z->parent->color == RED) { if (z->parent == z->parent->parent->left) { Node *y = z->parent->parent->right; if (y->color == RED) { // 情况1 z->parent->color = BLACK; y->color = BLACK; z->parent->parent->color = RED; z = z->parent->parent; } else { if (z == z->parent->right) { // 情况2 z = z->parent; leftRotate(tree, z); } // 情况3 z->parent->color = BLACK; z->parent->parent->color = RED; rightRotate(tree, z->parent->parent); } } else if (z->parent == z->parent->parent->right) { // 与上面对称 Node *y = z->parent->parent->left; if (y->color == RED) { z->parent->color = BLACK; y->color = BLACK; z->parent->parent->color = RED; z = z->parent->parent; } else { if (z == z->parent->left) { z = z->parent; rightRotate(tree, z); } z->parent->color = BLACK; z->parent->parent->color = RED; leftRotate(tree, z->parent->parent); } } } tree->root->color = BLACK; }
RBInsertFixup方法循环检测结点z的父结点是否为红色,如果不是则退出循环。当结点z指向根结点时,由于根结点的父结点指针指向NIL结点,而NIL结点是黑色的,因此也会退出循环。这就是上述所说的“设置NIL结点简化边界问题”的具体体现。
3. 红黑树的删除
红黑树只有在黑色结点被删除的时候才需要进行调整,因为只有这种情况才会引起对性质的违反(性质5,或许还有性质4)。
和插入结点一样,红黑树删除结点也要先执行二叉搜索树的删除过程。
void deleteFromRBTree(Tree *tree, Node *node) { if (node == tree->nil) return; int node_original_color = node->color; Node *changeNode = node; if (node->left == tree->nil) { changeNode = node->right; transplant(tree, node, node->right); } else if (node->right == tree->nil) { changeNode = node->left; transplant(tree, node, node->left); } else { Tree t; t.root = node->right; t.nil = tree->nil; Node *min = RBMinimum(&t); Node *end = min->right; node_original_color = min->color; changeNode = end; min->size = node->size - 1; if (node == tree->root) tree->root = min; if (node->right != min) { RBTransplant(tree, min, min->right); min->right = node->right; node->right->parent = min; } min->left = node->left; node->left->parent = min; RBTransplant(tree, node, min); min->color = node->color; } free(node); if (node_original_color == BLACK) { RBDeleteFixup(tree, changeNode); } }
我们主要关注deleteFromRBTree方法的最后,这里做了一个判断,当node_original_color == BLACK,则对changNode结点执行RBDeleteFixup方法。node_original_color记录的是实际被删除的结点的颜色。changeNode指向被移动的结点。针对二叉搜索树删除结点的3种情况,可以有以下结论:
情况1. 被删除结点x的左子树为空,changeNode指向右孩子,node_original_color为x的颜色;
情况2. 被删除结点x的右子树为空,changeNode指向左孩子,node_original_color为x的颜色;
情况3. 被删除结点x的左右子树均不为空,那么实际被删除的结点其实是x的后驱结点,changeNode指向x的后驱结点y的右孩子,node_original_color为y的颜色。因为这里y才是实际被删除的结点,其可能会引起从y向上回溯的路径的黑高产生变化。
无论是哪一种情况,得到的changeNode都是平衡的,因为它到达的叶子结点的路径都不经过结点x。
注意,上述3种情况都有可能使changeNode指向一个NIL结点,此时我么你需要先将NIL结点的父结点指针做出相应修改,对应上述3种情况,有如下2种修改:
情况1,2:NIL(changeNode)的父结点指针指向被删除结点的父结点;
情况3:NIL(changeNode)的父结点指针指向被删除结点的后驱结点的父结点。
其实上面这修改的论述有点多余,因为它其实就跟二叉搜索树的删除操作对指针指向的修改是完全一样的。在这里,我只是要强调,我们应该将NIL结点视为一个普通的叶子结点,虽然我们只是使用它来简化边界操作的。至此,在插入和删除,我们都已经说明了NIL结点对简化边界操作的作用。总的来说就是,因为有了NIL结点,我们再也不用考虑根结点的父结点为NULL,或者是叶子结点的孩子为NULL这种边界问题了。
先给出RBDeleteFixup方法的代码,然后再针对代码解释。
void RBDeleteFixup(Tree *tree, Node *x) { while (x != tree->root && x->color == BLACK) { if (x == x->parent->left) { Node *w = x->parent->right; if (w->color == RED) { // 情况1 w->color = BLACK; x->parent->color = RED; leftRotate(tree,x->parent); w = x->parent->right; } if (w->left->color == BLACK && w->right->color == BLACK) { // 情况2 w->color = RED; x = x->parent; } else { if (w->right->color == BLACK) { // 情况3 w->left->color = BLACK; w->color = RED; rightRotate(tree, w); w = x->parent->right; } // 情况4 w->color = x->parent->color; x->parent->color = BLACK; w->right->color = BLACK; leftRotate(tree, x->parent); x = tree->root; } } else if (x == x->parent->right) { // 与上面对称 Node *w = x->parent->left; if (w->color == RED) { w->color = BLACK; x->parent->color = RED; rightRotate(tree, x->parent); w = x->parent->left; } if (w->left->color == BLACK && w->right->color == BLACK) { w->color = RED; x = x->parent; } else { if (w->left->color == BLACK) { w->right->color = BLACK; w->color = RED; leftRotate(tree, w); w = x->parent->left; } w->color = x->parent->color; x->parent->color = BLACK; w->left->color = BLACK; rightRotate(tree, x->parent); x = tree->root; } } } x->color = BLACK; }
针对删除结点的3种情况,对changeNode的调整又有不同的情况:
(1). 被删除结点x只有一个孩子
此时x的孩子changeNode就会带着自己的孩子们替换掉x,假如changeNode是黑色的,那么changeNode往上的路径的黑高会因为x的删除而少1,引起树的不平衡,对这种情况的处理后面会讲述,假如changeNode是红色的,我们只需要简单地修改changeNode为黑色即可解决这个问题;
(2). 被删除结点x有两个孩子
此时x会被x的后驱结点y所替换,y原来的位置变成了y原来的右孩子changeNode。此后,我们只需要将y的颜色修改成黑色就可以使y不会引起树的不平衡,然后再集中关注changeNode的颜色,针对changeNode,就可以像上面的(1)那样处理了。
当changeNode为黑色时,对changeNode的处理有4种情况,下面将changeNode称为结点x,注意,x始终是黑色而且平衡的:
情况1. x的兄弟结点w是红色的
删除结点后,x的父结点xp经过x到达叶子结点的路径的黑高产生变化,以xp为根的树不再平衡。
针对情况1,x是平衡的,但是因为结点B的左子树被删除了一个黑色的结点,导致结点B的左子树的黑高少了1,所以结点B是不平衡的。此时,ha==hb==hr-1,hr==hd==he==hf。可以对结点B进行一次左旋,然后修改结点B和结点D的颜色得到右图,转变成情况2、3、4进行处理。
情况2. x的兄弟结点w是黑色的而且w的两个子结点都是黑色的
与情况1一样,在删除结点后,结点B不平衡,其中ha==hb==hr-1,hr==hd==he==hf。于是我们可以直接修改结点D为黑色,就可使得结点B达到平衡,但是这又会使得结点B的黑高比原来少1,会引起结点B往上的树不平衡。此时,如果结点B为红色,那么RBDeleteFixup的循环就会结束,然后将结点B修改为黑色,此时结点B的黑高就又恢复如初,不影响其它树的平衡。如果结点B为黑色,则从该结点开始继续向上回溯调整树的黑高。
情况3. x的兄弟结点w是黑色的而且w的左孩子是红色的,w的右孩子是黑色的
这个跟插入结点的情况2类似,可以通过旋转将其转变成情况4进行处理。
简单证明一下对结点w右旋后结点w的红黑性质不会被破坏。旋转前,结点w是平衡的,所以hr==hd==he+1==hf+1。旋转后,结点w指向了结点C,此时结点w的左子树高为hr,右子树高为he+1==hr,所以结点w依然是平衡的,再看结点D,hd==he+1,所以结点D也是平衡的。综上所述,这一旋转操作不会影响结点B的右子树的红黑性质,仅仅是将其转变成请款4进行处理而已。
情况4. x的兄弟结点w是黑色的,而且w的右孩子是红色的
考察这种情况,首先和上面一样,因为删除结点导致结点B不平衡,其中hr==hd==he==hf,ha==hb==hr-1。对结点B进行一次左旋并修改结点B、D、E的颜色可以得到右图。此时结点B达到平衡结点D的左子树黑高为ha+2,右子树黑高为he+1==ha+2,所以结点D也达到平衡,该树从根开始的黑高在删除前和删除并旋转操作后不变,因此不会影响到其它树的平衡。也就是说,执行完情况4的操作之后,整棵树应当已经平衡了,除非旋转后的结点x是根结点,违反了性质2。根据上面给出的RBDeleteFixup程序,当x为根结点时就要退出循环,最后将x染成黑色,此时,整棵树就达到了平衡了。
上述4种情况是针对结点x是一颗左子树而言的,当x是一颗右子树时其操作与上述操作完全对称。
4. 红黑树的插入和旋转操作的时间复杂度
红黑树插入需要O(lg(n))次,对插入结点后的调整所做的旋转操作不会超过2次。删除结点后的调整所做的旋转操作不会操作3次,沿树回溯至多O(lg(n))次。总而言之,红黑树的插入和删除的时间复杂度均为O(lg(n))。
C实现的代码可以参考我的github项目,里面还有其它一些数据结构和算法的实现。该项目持续更新哦~