操作
由于每个红黑树也是一个特化的二叉查找树,因此红黑树上的仅仅读操作与普通二叉查找树上的仅仅读操作同样。然而,在红黑树上进行插入操作和删除操作会导致不再符合红黑树的性质。
恢复红黑树的属性须要少量(O(log n))的颜色变更(实际是非常高速的)和不超过三次树旋转(对于插入操作是两次)。尽管插入和删除非常复杂。但操作时间仍能够保持为 O(log n) 次。
插入
我们首先以二叉查找树的方法添加节点并标记它为红色。(假设设为黑色,就会导致根到叶子的路径上有一条路上。多一个额外的黑节点,这个是非常难调整的。可是设为红色节点后,可能会导致出现两个连续红色节点的冲突。那么能够通过颜色调换(color flips)和树旋转来调整。
) 以下要进行什么操作取决于其它临近节点的颜色。同人类的家族树中一样,我们将使用术语叔父节点来指一个节点的父节点的兄弟节点。注意:
在以下的示意图中,将要插入的节点标为N,N的父节点标为P。N的祖父节点标为G,N的叔父节点标为U。在图中展示的不论什么颜色要么是由它所处情形所作的假定。要么是这些假定所暗含 (imply) 的。
对于每一种情况,我们将使用 C 演示样例代码来展示。
通过下列函数。能够找到一个节点的叔父和祖父节点:
node grandparent(node n) {
return n->parent->parent;
}
node uncle(node n) {
if (n->parent == grandparent(n)->left)
return grandparent(n)->right;
else
return grandparent(n)->left;
}
情形1: 新节点N位于树的根上,没有父节点。
在这样的情形下,我们把它重绘为黑色以满足性质2[5]。
由于它在每一个路径上对黑节点数目添加一,性质5[4]符合。
void insert_case1(node n) {
if (n->parent == NULL)
n->color = BLACK;
else
insert_case2(n);
}
情形2: 新节点的父节点P是黑色。所以性质4[3]没有失效(新节点是红色的)。在这样的情形下。树仍是有效的。性质5[4]受到威胁。由于新节点N有两个黑色叶子儿子;可是由于新节点N是红色,通过它的每一个子节点的路径就都有同通过它所代替的黑色的叶子的路径相同数目的黑色节点。所以这个性质依旧满足。
void insert_case2(node n) {
if (n->parent->color == BLACK)
return; /* 树仍旧有效 */
else
insert_case3(n);
}
注意: 在下列情形下我们假定新节点有祖父节点,由于父节点是红色;而且假设它是根。它就应当是黑色。
所以新节点总有一个叔父节点。虽然在情形4和5下它可能是叶子。
情形3: 假设父节点P和叔父节点U二者都是红色,(此时新插入节点N做为P的左子节点或右子节点都属于情形3,这里右图仅显示N做为P左子的情形)则我们能够将它们两个重绘为黑色并重绘祖父节点G为红色(用来保持性质5[4])。如今我们的新节点N有了一个黑色的父节点P。 由于通过父节点P或叔父节点U的不论什么路径都必然通过祖父节点G。在这些路径上的黑节点数目没有改变。 可是,红色的祖父节点G的父节点也有可能是红色的,这就违反了性质4[3]。 为了解决问题,我们在祖父节点G上递归地进行情形1的整个过程。 (把G当成是新增加的节点进行各种情况的检查) |
void insert_case3(node n) {
if (uncle(n) != NULL && uncle(n)->color == RED) {
n->parent->color = BLACK;
uncle(n)->color = BLACK;
grandparent(n)->color = RED;
insert_case1(grandparent(n));
}
else
insert_case4(n);
}
注意: 在余下的情形下,我们假定父节点P 是其父亲G 的左子节点。
假设它是右子节点。情形4和情形5中的左和右应当对调。
情形4: 父节点P是红色而叔父节点U是黑色或缺少; 还有,新节点N是其父节点P的右子节点,而父节点P又是其父节点的左子节点。在这样的情形下。我们进行一次左旋转调换新节点和其父节点的角色; 接着。我们按情形5处理曾经的父节点P。 这导致某些路径通过它们曾经不通过的新节点N或父节点P中的一个,可是这两个节点都是红色的。所以性质5[4]没有失效。 |
void insert_case4(node n) {
if (n == n->parent->right && n->parent == grandparent(n)->left) {
rotate_left(n->parent);
n = n->left;
} else if (n == n->parent->left && n->parent == grandparent(n)->right) {
rotate_right(n->parent);
n = n->right;
}
insert_case5(n);
}
情形5: 父节点P是红色而叔父节点U 是黑色或缺少,新节点N 是其父节点的左子节点。而父节点P又是其父节点G的左子节点。在这样的情形下,我们进行针对祖父节点G 的一次右旋转; 在旋转产生的树中,曾经的父节点P如今是新节点N和曾经的祖父节点G 的父节点。我们知道曾经的祖父节点G是黑色,否则父节点P就不可能是红色 (假设 P 和 G 都是紅色就違反了性質4,所以 G 必須是黑色)。我们切换曾经的父节点P和祖父节点G的颜色,结果的树满足性质4[3]。性质5[4]也仍然保持满足。由于通过这三个节点中不论什么一个的全部路径曾经都通过祖父节点G 。如今它们都通过曾经的父节点P。 在各自的情形下,这都是三个节点中唯一的黑色节点。 |
void insert_case5(node n) {
n->parent->color = BLACK;
grandparent(n)->color = RED;
if (n == n->parent->left && n->parent == grandparent(n)->left) {
rotate_right(grandparent(n));
} else {
/* Here, n == n->parent->right && n->parent == grandparent(n)->right */
rotate_left(grandparent(n));
}
}
注意插入实际上是原地算法,由于上述全部调用都使用了尾部递归。
删除
假设须要删除的节点有两个儿子,那么问题能够被转化成删除还有一个仅仅有一个儿子的节点的问题(为了表述方便。这里所指的儿子。为非叶子节点的儿子)。对于二叉查找树,在删除带有两个非叶子儿子的节点的时候。我们找到要么在它的左子树中的最大元素、要么在它的右子树中的最小元素。并把它的值转移到要删除的节点中(如在这里所展示的那样)。我们接着删除我们从中复制出值的那个节点,它必然有少于两个非叶子的儿子。
由于仅仅是复制了一个值而不违反不论什么属性。这就把问题简化为怎样删除最多有一个儿子的节点的问题。它不关心这个节点是最初要删除的节点还是我们从中复制出值的那个节点。
在本文余下的部分中,我们仅仅须要讨论删除仅仅有一个儿子的节点(假设它两个儿子都为空。即均为叶子,我们随意将当中一个看作它的儿子)。
假设我们删除一个红色节点,它的父亲和儿子一定是黑色的。所以我们能够简单的用它的黑色儿子替换它。并不会破坏属性3和4。
通过被删除节点的全部路径仅仅是少了一个红色节点,这样能够继续保证属性5。还有一种简单情况是在被删除节点是黑色而它的儿子是红色的时候。
假设仅仅是去除这个黑色节点,用它的红色儿子顶替上来的话。会破坏属性5,可是假设我们重绘它的儿子为黑色,则以前通过它的全部路径将通过它的黑色儿子,这样能够继续保持属性5。
须要进一步讨论的是在要删除的节点和它的儿子二者都是黑色的时候,这是一种复杂的情况。我们首先把要删除的节点替换为它的儿子。出于方便。称呼这个儿子为N,称呼它的兄弟(它父亲的还有一个儿子)为S。
在以下的示意图中。我们还是使用P称呼N的父亲。SL称呼S的左儿子,SR称呼S的右儿子。我们将使用下述函数找到兄弟节点:
struct node *
sibling(struct node *n)
{
if (n == n->parent->left)
return n->parent->right;
else
return n->parent->left;
}
我们能够使用下列代码进行上述的概要步骤,这里的函数 replace_node
替换 child
到 n
在树中的位置。出于方便,在本章节中的代码将假定空叶子被用不是 NULL 的实际节点对象来表示(在插入章节中的代码能够同不论什么一种表示一起工作)。
void
delete_one_child(struct node *n)
{
/*
* Precondition: n has at most one non-null child.
*/
struct node *child = is_leaf(n->right) ? n->left : n->right;
replace_node(n, child);
if (n->color == BLACK) {
if (child->color == RED)
child->color = BLACK;
else
delete_case1(child);
}
free(n);
}
假设 N 和它初始的父亲是黑色,则删除它的父亲导致通过 N 的路径都比不通过它的路径少了一个黑色节点。由于这违反了属性 4,树须要被又一次平衡。有几种情况须要考虑:
情况 1: N 是新的根。在这样的情况下,我们就做完了。我们从全部路径去除了一个黑色节点。而新根是黑色的,所以属性都保持着。
void
delete_case1(struct node *n)
{
if (n->parent != NULL)
delete_case2(n);
}
注意: 在情况2、5和6下,我们假定 N 是它父亲的左儿子。
假设它是右儿子,则在这些情况下的左和右应当对调。
情况 2: S 是红色。在这样的情况下我们在N的父亲上做左旋转,把红色兄弟转换成N的祖父。我们接着对调 N 的父亲和祖父的颜色。 虽然全部的路径仍然有同样数目的黑色节点,如今 N 有了一个黑色的兄弟和一个红色的父亲,所以我们能够接下去按 4、5或6情况来处理。 (它的新兄弟是黑色由于它是红色S的一个儿子。) |
void
delete_case2(struct node *n)
{
struct node *s = sibling(n);
if (s->color == RED) {
n->parent->color = RED;
s->color = BLACK;
if (n == n->parent->left)
rotate_left(n->parent);
else
rotate_right(n->parent);
}
delete_case3(n);
}
情况 3: N 的父亲、S 和 S 的儿子都是黑色的。在这样的情况下。我们简单的重绘 S 为红色。结果是通过S的全部路径,它们就是曾经不通过 N 的那些路径。都少了一个黑色节点。由于删除 N 的初始的父亲使通过 N 的全部路径少了一个黑色节点,这使事情都平衡了起来。可是。通过 P 的全部路径如今比不通过 P 的路径少了一个黑色节点。所以仍然违反属性4。要修正这个问题,我们要从情况 1 開始。在 P 上做又一次平衡处理。 |
void
delete_case3(struct node *n)
{
struct node *s = sibling(n);
if ((n->parent->color == BLACK) &&
(s->color == BLACK) &&
(s->left->color == BLACK) &&
(s->right->color == BLACK)) {
s->color = RED;
delete_case1(n->parent);
} else
delete_case4(n);
}
情况 4: S 和 S 的儿子都是黑色,可是 N 的父亲是红色。在这样的情况下,我们简单的交换 N 的兄弟和父亲的颜色。这不影响不通过N 的路径的黑色节点的数目,可是它在通过 N 的路径上对黑色节点数目添加了一,添补了在这些路径上删除的黑色节点。 |
void
delete_case4(struct node *n)
{
struct node *s = sibling(n);
if ((n->parent->color == RED) &&
(s->color == BLACK) &&
(s->left->color == BLACK) &&
(s->right->color == BLACK)) {
s->color = RED;
n->parent->color = BLACK;
} else
delete_case5(n);
}
情况 5: S 是黑色。S 的左儿子是红色。S 的右儿子是黑色。而 N 是它父亲的左儿子。 在这样的情况下我们在 S 上做右旋转。这样 S的左儿子成为 S 的父亲和 N 的新兄弟。我们接着交换 S 和它的新父亲的颜色。全部路径仍有相同数目的黑色节点。可是如今 N 有了一个右儿子是红色的黑色兄弟,所以我们进入了情况 6。N 和它的父亲都不受这个变换的影响。 |
void
delete_case5(struct node *n)
{
struct node *s = sibling(n);
if (s->color == BLACK) /* this if statement is trivial,
due to Case 2 (even though Case two changed the sibling to a sibling's child,
the sibling's child can't be red, since no red parent can have a red child). */
// the following statements just force the red to be on the left of the left of the parent,
// or right of the right, so case six will rotate correctly.
if ((n == n->parent->left) &&
(s->right->color == BLACK) &&
(s->left->color == RED)) { // this last test is trivial too due to cases 2-4.
s->color = RED;
s->left->color = BLACK;
rotate_right(s);
} else if ((n == n->parent->right) &&
(s->left->color == BLACK) &&
(s->right->color == RED)) {// this last test is trivial too due to cases 2-4.
s->color = RED;
s->right->color = BLACK;
rotate_left(s);
}
}
delete_case6(n);
}
情况 6: S 是黑色,S 的右儿子是红色。而 N 是它父亲的左儿子。在这样的情况下我们在 N 的父亲上做左旋转,这样 S 成为 N 的父亲和 S 的右儿子的父亲。 我们接着交换 N 的父亲和 S 的颜色,并使 S 的右儿子为黑色。 子树在它的根上的仍是相同的颜色,所以属性 3 没有被违反。 可是,N 如今添加了一个黑色祖先: 要么 N 的父亲变成黑色,要么它是黑色而 S 被添加为一个黑色祖父。所以。通过 N 的路径都添加了一个黑色节点。 此时,假设一个路径不通过 N,则有两种可能性:
在不论什么情况下,在这些路径上的黑色节点数目都没有改变。所以我们恢复了属性 4。在示意图中的白色节点能够是红色或黑色。可是在变换前后都必须指定同样的颜色。 |
void
delete_case6(struct node *n)
{
struct node *s = sibling(n);
s->color = n->parent->color;
n->parent->color = BLACK;
if (n == n->parent->left) {
s->right->color = BLACK;
rotate_left(n->parent);
} else {
s->left->color = BLACK;
rotate_right(n->parent);
}
}
相同的。函数调用都使用了尾部递归,所以算法是就地的。此外,在旋转之后不再做递归调用。所以进行了恒定数目(最多 3 次)的旋转。
渐进边界的证明
包括n个内部节点的红黑树的高度是 O(log(n))。
定义:
- h(v) = 以节点v为根的子树的高度。
- bh(v) = 从v到子树中不论什么叶子的黑色节点的数目(假设v是黑色则不计数它)(也叫做黑色高度)。
引理: 以节点v为根的子树有至少2bh(v) − 1个内部节点。
引理的证明(通过归纳高度):
基础: h(v) = 0
假设v的高度是零则它必然是 nil,因此 bh(v) = 0。所以:
2bh(v) − 1 = 20 − 1 = 1 − 1 = 0
归纳如果: h(v) = k 的v有 2bh(v) − 1 − 1 个内部节点暗示了 h(v') = k+1 的 v'有2bh(v') − 1 个内部节点。
由于 v' 有 h(v') > 0 所以它是个内部节点。相同的它有黑色高度要么是 bh(v') 要么是 bh(v')-1 (根据v'是红色还是黑色)的两个儿子。通过归纳如果每一个儿子都有至少 2bh(v') − 1 − 1 个内部接点,所以 v' 有:
2bh(v') − 1 − 1 + 2bh(v') − 1 − 1 + 1 = 2bh(v') − 1
个内部节点。
使用这个引理我们如今能够展示出树的高度是对数性的。由于在从根到叶子的不论什么路径上至少有一半的节点是黑色(依据红黑树属性4),根的黑色高度至少是h(root)/2。
通过引理我们得到:
因此根的高度是O(log(n))。