在《算法导论 之 红黑树 - 插入》中已经对红黑树的5个性质做了较详细的分析,同时也给出了insert操作的C语言实现。首先我们再回顾一下红黑树的5个性质:
①、每个节点要么是红色的,要么是黑色的;
②、根结点是黑色的;
③、所有叶子结点(NIL)都是黑色的;
④、如果一个结点是红色,则它的两个儿子都是黑色的;
⑤、对任何一个结点,从该结点通过其子孙结点到达叶子结点(NIL)的所有路径上包含相同数目的黑结点。
和插入操作一样,结点的删除操作的时间复杂度也是O(log2@N)[注:以2为底数,N为对数],但删除操作的处理更复杂一些。
/****************************************************************************** **函数名称: rbt_delete **功 能: 删除结点(外部接口) **输入参数: ** tree: 红黑树 ** key: 关键字 **输出参数: NONE **返 回: RBT_SUCCESS:成功 RBT_FAILED:失败 **实现描述: ** 1. 如果key的结点不存在,则无需进行任何的处理; ** 2. 如果key的结点存在,则调用_rbt_delete()删除结点。 **注意事项: **作 者: # Qifeng.zou # 2013.12.27 # ******************************************************************************/ int rbt_delete(rbt_tree_t *tree, int key) { rbt_node_t *node = tree->root; while(tree->sentinel != node) { if(key == node->key) { return _rb_delete(tree, node); /* 找到:执行删除处理 */ } else if(key < node->key) { node = node->lchild; } else { node = node->rchild; } } return RBT_SUCCESS; /* 未找到 */ }
假如需要删除结点D,则删除操作的过程有如下几种情况:[注:在以下所有绘制的红黑树中,均未绘制叶子结点]
情况1:被删结点D的左孩子为叶子结点,右孩子无限制(可为叶子结点,也可为非叶子结点)
处理过程:
①、删除结点D,并用右孩子结点替代结点D的位置;
②、如果被删结点D为红色,则红黑树性质未被破坏,因此无需做其他调整;
③、如果被删结点D为黑色,则需进一步做调整处理。
图1 情况1-1:左右孩子均为叶子结点
[叶子结点取代了结点D的位置]
图2 情况1-2:左孩子为叶子结点 右孩子不为叶子结点
[结点DR取代了叶子结点的位置]
情况2: 被删结点D的右孩子为叶子结点,左孩子不为叶子结点
处理过程:
①、删除结点D,并用左孩子节点替代结点D的位置;
②、如果被删结点D为红色,则红黑树性质未被破坏,因此不需做其他调整;
③、如果被删结点D为黑色,则需进一步做调整处理。
图3 情况2:右孩子为叶子结点 左孩子不为叶子结点
[结点DL取代结点D的位置]
情况3: 被删结点D的左右孩子均不为叶子节点
处理过程:
①、找到结点D的后继结点S
②、将结点S的key值赋给结点D;
③、再将结点S从树中删除,并用结点S的右孩子替代结点S的位置;[注:从前面的描述可以看出,其实被删的是结点D的后继结点S]
④、如果被删结点S为红色,则红黑树性质未被破坏,因此不需做其他调整;
⑤、如果被删结点S为黑色,则需进一步做调整处理。
图4 情况3:左右孩子均不为叶子结点
[后继结点的右孩子SR取代后继结点S的位置]
综合情况1、2、3可知,当实际被删的结点为黑色时,才需进一步做调整处理 —— 实际被删的结点为红色时,并不会破坏红黑树的5点性质,其实现的过程如下:[注:代码中出现的数据类型、宏、枚举或函数定义可以参考《算法导论 之 红黑树 - 插入》]
/****************************************************************************** **函数名称: _rbt_delete **功 能: 删除结点(内部接口) **输入参数: ** tree: 红黑树 ** dnode: 将被删除的结点 **输出参数: NONE **返 回: RBT_SUCCESS:成功 RBT_FAILED:失败 **实现描述: ** 1. 如果将被删除的结点dnode无后继结点,则直接被删除,并被其左孩子或右孩子替代其位置 ** 2. 如果将被删除的结点dnode有后继结点,则将后继结点的其赋给dnode,并删除后继结点, ** 再将后继结点的右孩子取代后继结点的位置 ** 3. 完成1、2的处理之后,如果红黑树的性质被破坏,则调用rbt_delete_fixup()进行调整 **注意事项: **作 者: # Qifeng.zou # 2013.12.28 # ******************************************************************************/ int _rb_delete(rbt_tree_t *tree, rbt_node_t *dnode) { rbt_node_t *parent = NULL, *next = NULL, *refer = NULL; /* Case 1: 被删结点D的左孩子为叶子结点, 右孩子无限制(可为叶子结点,也可为非叶子结点) */ if(tree->sentinel == dnode->lchild) { parent = dnode->parent; refer = dnode->rchild; refer->parent = parent; if(tree->sentinel == parent) { tree->root = refer; } else if(dnode == parent->lchild) { parent->lchild = refer; } else /* dnode == parent->rchild */ { parent->rchild = refer; } if(rbt_is_red(dnode)) { free(dnode); return RBT_SUCCESS; } free(dnode); return rbt_delete_fixup(tree, refer); } /* Case 2: 被删结点D的右孩子为叶子结点, 左孩子不为叶子结点 */ else if(tree->sentinel == dnode->rchild) { parent = dnode->parent; refer = dnode->lchild; refer->parent = parent; if(tree->sentinel == parent) { tree->root = refer; } else if(dnode == parent->lchild) { parent->lchild = refer; } else /* dnode == parent->rchild */ { parent->rchild = refer; } if(rbt_is_red(dnode)) { free(dnode); return RBT_SUCCESS; } free(dnode); return rbt_delete_fixup(tree, refer); } /* Case 3: 被删结点D的左右孩子均不为叶子节点 */ /* 查找dnode的后继结点next */ next = dnode->rchild; while(tree->sentinel != next->lchild) { next = next->lchild; } parent = next->parent; refer = next->rchild; refer->parent = parent; if(next == parent->lchild) { parent->lchild = refer; } else /* next == parent->rchild */ { parent->rchild = refer; } dnode->key = next->key; if(rbt_is_red(next)) /* Not black */ { free(next); return RBT_SUCCESS; } free(next); return rbt_delete_fixup(tree, refer); }
当红黑树中实际被删除的结点为黑色时,则可能破坏红黑树的5个性质。经过分析总结,破坏红黑树性质的情况有如下几种:
============================================================================
情况1:参照结点N的兄弟B是红色的
处理过程:
①、将父结点P的颜色改为红色,兄弟结点的颜色改为黑色;
②、以父结点P为支点进行左旋处理;
③、情况1转变为情况2或3、4,后续需要依次判断处理。
如下图所示:[注意:请注意图中处理前后node、brother指针的变化,这将是后续处理的参照]
图5 调整情况1
情况2:参照结点N的兄弟B是黑色的,且B的两个孩子都是黑色的
处理过程:
①、将兄弟结点B的颜色改为红色
②、情况2处理完成后,不必再进行情况3、4的判断,但需重新循环判断前提1、2。
如下图所示:[注意:请注意图中处理前后node、brother指针的变化,这将是后续处理的参照]
图6 调整情况2
情况3:参照结点N的兄弟B是黑色的,且B的左孩子是红色的,右孩子是黑色的
处理过程:
①、将兄弟结点B的颜色改为红色,结点B的左孩子改为黑色;
②、以结点B为支点进行右旋处理;
③、情况3转化为情况4,后续必须进行情况4的处理。
如下图所示:[注意:请注意图中处理前后node、brother指针的变化,这将是后续处理的参照]
图7 调整情况3
情况4:参照结点N的兄弟B是黑色的,且B的左孩子是黑色的,右孩子是红色的
处理过程:
①、将父结点P的颜色拷贝给兄弟结点B,再将父结点P和兄弟结点的右孩子BR的颜色改为黑色;
②、以父结点P为支点,进行左旋处理;
③、将node改为树的根结点,也意味着调整结束。
如下图所示:[注意:请注意图中处理前后node指针的变化,这将是后续处理的参照]
图8 调整情况4
[注:蓝色表示结点颜色可能为红,也可能为黑,在此也更能突出复制结点P的颜色给结点B]
============================================================================
情况5:参照结点N的兄弟B是红色的
处理过程:
①、将父结点P的颜色改为红色,兄弟结点的颜色改为黑色;
②、以父结点P为支点进行右旋处理;
③、情况5转变为情况6或7、8,后续需要依次判断处理。
如下图所示:[注意:请注意图中处理前后node、brother指针的变化,这将是后续处理的参照]
图9 调整情况5
情况6:参照结点N的兄弟B是黑色的,且B的两个孩子都是黑色的
处理过程:
①、将兄弟结点B的颜色改为红色;
②、情况6处理完成后,不必再进行情况7、8的判断,但需要重新循环判断前提1、2。
如下图所示:[注意:请注意图中处理前后node、brother指针的变化,这将是后续处理的参照]
图10 调整情况6
情况7:参照结点N的兄弟B是黑色的,且B的右孩子是红色的,左孩子是黑色的
处理过程:
①、将兄弟结点B的颜色改为红色,结点B的右孩子改为黑色;
②、以结点B为支点进行左旋处理;
③、情况7转化为情况8,后续必须进行情况8的处理。
如下图所示:[注意:请注意图中处理前后node、brother指针的变化,这将是后续处理的参照]
图11 调整情况7
情况8:参照结点N的兄弟B是黑色的,且B的右孩子是黑色的,左孩子是红色的
处理过程:
①、将父结点P的颜色拷贝给兄弟结点B,再将父结点P和兄弟结点的左结点BL颜色改为黑色;
②、以父结点P为支点,进行右旋处理;
③、将node改为树的根结点,也意味着调整结束。
如下图所示:[注意:请注意图中处理前后node指针的变化,这将是后续处理的参照]
图12 调整情况8
[注:蓝色表示结点颜色可能为红,也可能为黑,在此也更能突出复制结点P的颜色给结点B]
综合以上情况的分析,删除结点后的调整过程的实现代码如下所示:[注:代码中出现的数据类型、宏、枚举或函数定义可以参考《算法导论 之 红黑树 - 插入》]
/****************************************************************************** **函数名称: rbt_delete_fixup **功 能: 修复删除操作造成的黑红树性质的破坏(内部接口) **输入参数: ** tree: 红黑树 ** node: 实际被删结点的替代结点(注: node有可能是叶子结点) **输出参数: NONE **返 回: RBT_SUCCESS:成功 RBT_FAILED:失败 **实现描述: **注意事项: ** 注意: 被删结点为黑色结点,才能调用此函数进行性质调整 **作 者: # Qifeng.zou # 2013.12.28 # ******************************************************************************/ int rbt_delete_fixup(rbt_tree_t *tree, rbt_node_t *node) { rbt_node_t *parent = NULL, *brother = NULL; while(rbt_is_black(node) && (tree->root != node)) { /* Set parent and brother */ parent = node->parent; /* 前提1:node为parent的左孩子 */ if(node == parent->lchild) { brother = parent->rchild; /* Case 1: 兄弟结点为红色: 以parent为支点, 左旋处理 */ if(rbt_is_red(brother)) { rbt_set_red(parent); rbt_set_black(brother); rbt_left_rotate(tree, parent); /* 参照结点node不变, 兄弟结点改为parent->rchild */ brother = parent->rchild; /* 注意: 此时处理还没有结束,还需要做后续的调整处理 */ } /* Case 2: 兄弟结点为黑色(默认), 且兄弟结点的2个子结点都为黑色 */ if(rbt_is_black(brother->lchild) && rbt_is_black(brother->rchild)) { rbt_set_red(brother); node = parent; } else { /* Case 3: 兄弟结点为黑色(默认), 兄弟节点的左子结点为红色, 右子结点为黑色: 以brother为支点, 右旋处理 */ if(rbt_is_black(brother->rchild)) { rbt_set_black(brother->lchild); rbt_set_red(brother); rbt_right_rotate(tree, brother); /* 参照结点node不变 */ brother = parent->rchild; } /* Case 4: 兄弟结点为黑色(默认), 兄弟结点右孩子结点为红色: 以parent为支点, 左旋处理 */ rbt_copy_color(brother, parent); rbt_set_black(brother->rchild); rbt_set_black(parent); rbt_left_rotate(tree, parent); node = tree->root; } } /* 前提2:node为parent的右孩子 */ else { brother = parent->lchild; /* Case 5: 兄弟结点为红色: 以parent为支点, 右旋处理 */ if(rbt_is_red(brother)) { rbt_set_red(parent); rbt_set_black(brother); rbt_right_rotate(tree, parent); /* 参照结点node不变 */ brother = parent->lchild; /* 注意: 此时处理还没有结束,还需要做后续的调整处理 */ } /* Case 6: 兄弟结点为黑色(默认), 且兄弟结点的2个子结点都为黑色 */ if(rbt_is_black(brother->lchild) && rbt_is_black(brother->rchild)) { rbt_set_red(brother); node = parent; } else { /* Case 7: 兄弟结点为黑色(默认), 兄弟节点的右子结点为红色, 左子结点为黑色: 以brother为支点, 左旋处理 */ if(rbt_is_black(brother->lchild)) { rbt_set_red(brother); rbt_set_black(brother->rchild); rbt_left_rotate(tree, brother); /* 参照结点node不变 */ brother = parent->lchild; } /* Case 8: 兄弟结点为黑色(默认), 兄弟结点左孩子结点为红色: 以parent为支点, 右旋处理 */ rbt_copy_color(brother, parent); rbt_set_black(brother->lchild); rbt_set_black(parent); rbt_right_rotate(tree, parent); node = tree->root; } } } rbt_set_black(node); return RBT_SUCCESS; }
代码3 删除调整
首先,随机输入多个key生成左图树,再随机删除任意key后,得到右图树。经过分析可以发现:右图也是一个红黑树。经过反复验证后,可以判断以上代码的处理是正确的。[注:红黑树的打印可以参考博文《算法导论 之 红黑树 - 打印、销毁》]
图13 结果展示