红黑树

红黑树（Red–black tree）是一种自平衡二叉查找树，是在计算机科学中用到的一种数据结构，典型用途是实现关联数组。它在1972年由鲁道夫·贝尔发明，被称为「对称二叉B树」，它现代的名字源于利奥尼达斯·J·吉巴斯和罗伯特·塞奇威克于1978年写的一篇论文。红黑树的结构复杂，但它的操作有着良好的最坏情况运行时间，并且在实践中高效：它可以在formula_1时间内完成查找、插入和删除，这里的formula_2是树中元素的数目。
历史.
1972年，鲁道夫·拜尔发明了一种资料结构，那是一个B树的四阶特殊情况。此结构会维持所有从根到树叶的路径都具有相同数量的节点，创造了一个完美的平衡树。然而，他们并不是二元搜寻树。拜尔在他的论文中称呼他们为「对称二元B树」（Symmetric binary B-tree），后来他们以2-3-4树或是2-4树的形式开始流行。
在一篇1972年的论文〈A Dichromatic Framework for Balanced Trees〉中，利奥尼达斯·J·吉巴斯和罗伯特·塞奇威克从对称二元B树+中推导出了红黑树。之所以选择「红色」是因为这是作者在帕罗奥多研究中心公司（Xerox PARC）工作时用彩色雷射列印机可以产生的最好看的颜色。另一种说法来自吉巴斯，是因为他们当时能用来绘制树的笔有红色和黑色。
用途和好处.
红黑树和AVL树一样都对插入时间、删除时间和查找时间提供了最好可能的最坏情况担保。这不只是使它们在时间敏感的应用，如实时应用中有价值，而且使它们有在提供最坏情况担保的其他数据结构中作为基础模板的价值；例如，在计算几何中使用的很多数据结构都可以基于红黑树实现。
红黑树在函数式编程中也特别有用，在这里它们是最常用的持久数据结构之一，它们用来构造关联数组和集合，每次插入、删除之后它们能保持为以前的版本。除了formula_1的时间之外，红黑树的持久版本对每次插入或删除需要formula_1的空间。
红黑树是2-3-4树的一种等同。换句话说，对于每个2-3-4树，都存在至少一个数据元素是同样次序的红黑树。在2-3-4树上的插入和删除操作也等同于在红黑树中颜色翻转和旋转。这使得2-3-4树成为理解红黑树背后的逻辑的重要工具，这也是很多介绍算法的教科书在红黑树之前介绍2-3-4树的原因，尽管2-3-4树在实践中不经常使用。
红黑树相对于AVL树来说，牺牲了部分平衡性以换取插入和删除操作时少量的旋转操作，整体来说性能要优于AVL树。
性质.
红黑树是每个节点都带有颜色属性的二叉查找树，颜色为红色或黑色。在二叉查找树强制一般要求以外，对于任何有效的红黑树我们增加了如下的额外要求：
下面是一个具体的红黑树的图例：
这些约束确保了红黑树的关键特性：从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例，这个在高度上的理论上限允许红黑树在最坏情况下都是高效的，而不同于普通的二叉查找树。
要知道为什么这些性质确保了这个结果，注意到性质4导致了路径不能有两个毗连的红色节点就足够了。最短的可能路径都是黑色节点，最长的可能路径有交替的红色和黑色节点。因为根据性质5所有最长的路径都有相同数目的黑色节点，这就表明了没有路径能多于任何其他路径的两倍长。
在很多树数据结构的表示中，一个节点有可能只有一个子节点，而叶子节点包含数据。用这种范例表示红黑树是可能的，但是这会改变一些性质并使算法复杂。为此，本文中我们使用「nil叶子」，如上图所示，它不包含数据而只充当树在此结束的指示。这些节点在绘图中经常被省略，导致了这些树好像同上述原则相矛盾，而实际上不是这样。与此有关的结论是所有节点都有两个子节点，尽管其中的一个或两个可能是空叶子。
操作.
因为每一个红黑树也是一个特化的二叉查找树，因此红黑树上的只读操作与普通二叉查找树上的只读操作相同。然而，在红黑树上进行插入操作和删除操作会导致不再符合红黑树的性质。恢复红黑树的性质需要少量（formula_1）的颜色变更（实际是非常快速的）和不超过三次树旋转（对于插入操作是两次）。虽然插入和删除很复杂，但操作时间仍可以保持为formula_1次。
插入.
我们首先以二叉查找树的方法增加节点并标记它为红色。（如果设为黑色，就会导致根到叶子的路径上有一条路上，多一个额外的黑节点，这个是很难调整的。但是设为红色节点后，可能会导致出现两个连续红色节点的冲突，那么可以通过颜色调换（color flips）和树旋转来调整。）下面要进行什么操作取决于其他临近节点的颜色。同人类的家族树中一样，我们将使用术语"叔父节点"来指一个节点的父节点的兄弟节点。注意：
在下面的示意图中，将要插入的节点标为N，N的父节点标为P，N的祖父节点标为G，N的叔父节点标为U。在图中展示的任何颜色要么是由它所处情形这些所作的假定，要么是假定所暗含（imply）的。
对于每一种情形，我们将使用C示例代码来展示。通过下列函数，可以找到一个节点的叔父和祖父节点：
<syntaxhighlight lang="C">
</syntaxhighlight>
情形1:新节点N位于树的根上，没有父节点。在这种情形下，我们把它重绘为黑色以满足性质2。因为它在每个路径上对黑节点数目增加一，性质5符合。
<syntaxhighlight lang="C">
</syntaxhighlight>
情形2:新节点的父节点P是黑色，所以性质4没有失效（新节点是红色的）。在这种情形下，树仍是有效的。性质5也未受到威胁，尽管新节点N有两个黑色叶子子节点；但由于新节点N是红色，通过它的每个子节点的路径就都有同通过它所取代的黑色的叶子的路径同样数目的黑色节点，所以依然满足这个性质。
<syntaxhighlight lang="C">
</syntaxhighlight>
注意：在下列情形下（情形3、4和5）我们假定新节点的父节点为红色，所以它有祖父节点；因为如果父节点是根节点，那父节点就应当是黑色。所以新节点总有一个叔父节点，尽管在情形4和5下它可能是叶子节点。
<syntaxhighlight lang="C">
</syntaxhighlight>
注意：在余下的情形下，我们假定父节点P是其祖父G的左子节点。如果它是右子节点，情形4和情形5中的"左"和"右"应当对调。
<syntaxhighlight lang="C">
</syntaxhighlight>
<syntaxhighlight lang="C">
</syntaxhighlight>
注意插入实际上是原地算法，因为上述所有调用都使用了尾部递归。
删除.
如果需要删除的节点有两个儿子，那么问题可以被转化成删除另一个只有一个儿子的节点的问题（为了表述方便，这里所指的儿子，为非叶子节点的儿子）。对于二叉查找树，在删除带有两个非叶子儿子的节点的时候，我们要么找到它左子树中的最大元素、要么找到它右子树中的最小元素，并把它的值转移到要删除的节点中（如在这里所展示的那样）。我们接着删除我们从中复制出值的那个节点，它必定有少于两个非叶子的儿子。因为只是复制了一个值（没有复制颜色），不违反任何性质，这就把问题简化为如何删除最多有一个儿子的节点的问题。它不关心这个节点是最初要删除的节点还是我们从中复制出值的那个节点。
在本文余下的部分中，我们只需要讨论删除只有一个儿子的节点（如果它两个儿子都为空，即均为叶子，我们任意将其中一个看作它的儿子）。如果我们删除一个红色节点（此时该节点的儿子将都为叶子节点），它的父亲和儿子一定是黑色的。所以我们可以简单的用它的黑色儿子替换它，并不会破坏性质3和性质4。通过被删除节点的所有路径只是少了一个红色节点，这样可以继续保证性质5。另一种简单情况是在被删除节点是黑色而它的儿子是红色的时候。如果只是去除这个黑色节点，用它的红色儿子顶替上来的话，会破坏性质5，但是如果我们重绘它的儿子为黑色，则曾经通过它的所有路径将通过它的黑色儿子，这样可以继续保持性质5。
需要进一步讨论的是在要删除的节点和它的儿子二者都是黑色的时候，这是一种复杂的情况（这种情况下该节点的两个儿子都是叶子节点，否则若其中一个儿子是黑色非叶子节点，另一个儿子是叶子节点，那么从该节点通过非叶子节点儿子的路径上的黑色节点数最小为2，而从该节点到另一个叶子节点儿子的路径上的黑色节点数为1，违反了性质5）。我们首先把要删除的节点替换为它的儿子。出于方便，称呼这个儿子为N（在新的位置上），称呼它的兄弟（它父亲的另一个儿子）为S。在下面的示意图中，我们还是使用P称呼N的父亲，SL称呼S的左儿子，SR称呼S的右儿子。我们将使用下述函数找到兄弟节点：
<syntaxhighlight lang="C">
struct node *
sibling(struct node *n)
</syntaxhighlight>
我们可以使用下列代码进行上述的概要步骤，这里的函数codice_1替换codice_2到codice_3在树中的位置。出于方便，在本章节中的代码将假定空叶子被用不是NULL的实际节点对象来表示（在"插入"章节中的代码可以同任何一种表示一起工作）。
<syntaxhighlight lang="C">
void
delete_one_child(struct node *n)
</syntaxhighlight>
如果N和它初始的父亲是黑色，则删除它的父亲导致通过N的路径都比不通过它的路径少了一个黑色节点。因为这违反了性质5，树需要被重新平衡。有几种情形需要考虑：
情形1: N是新的根。在这种情形下，我们就做完了。我们从所有路径去除了一个黑色节点，而新根是黑色的，所以性质都保持着。
<syntaxhighlight lang="C">
void
delete_case1(struct node *n)
</syntaxhighlight>
注意：在情形2、5和6下，我们假定N是它父亲的左儿子。如果它是右儿子，则在这些情形下的"左"和"右"应当对调。
<syntaxhighlight lang="C">
void
delete_case2(struct node *n)
</syntaxhighlight>
<syntaxhighlight lang="C">
void
delete_case3(struct node *n)
(s->color == BLACK)&&
(s->left->color == BLACK)&&
(s->right->color == BLACK)) {
</syntaxhighlight>
<syntaxhighlight lang="C">
void
delete_case4(struct node *n)
(s->color == BLACK)&&
(s->left->color == BLACK)&&
(s->right->color == BLACK)) {
</syntaxhighlight>
<syntaxhighlight lang="C">
void
delete_case5(struct node *n)
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.
(s->right->color == BLACK)&&
(s->left->color == RED)) { // this last test is trivial too due to cases 2-4.
(s->left->color == BLACK)&&
(s->right->color == RED)) {// this last test is trivial too due to cases 2-4.
</syntaxhighlight>
<syntaxhighlight lang="C">
void
delete_case6(struct node *n)
</syntaxhighlight>
同样的，函数调用都使用了尾部递归，所以算法是原地算法。此外，在旋转之后不再做递归调用，所以进行了恒定数目（最多3次）的旋转。
C++示例代码.
<syntaxhighlight lang="cpp">
using namespace std;
class bst {
private:
public:
private:
</syntaxhighlight>
渐近边界的证明.
包含"n"个内部节点的红黑树的高度是formula_1。
定义：
引理：以节点formula_9为根的子树有至少formula_14个内部节点。
引理的证明（通过归纳高度）：
基础：formula_15
如果formula_9的高度是零则它必定是NIL，因此formula_17。所以：
formula_18
归纳假设：formula_19的formula_9有formula_21个内部节点暗示了h(formula_22) = k+1的formula_22有formula_24个内部节点。
因为formula_22有h(formula_22)> 0所以它是个内部节点。同样的它有黑色高度要么是bh(formula_22)要么是bh(formula_22)-1（依据formula_9是红色还是黑色）的两个儿子。通过归纳假设每个儿子都有至少formula_30个内部接点，所以formula_22有：
formula_32
个内部节点。
使用这个引理我们现在可以展示出树的高度是对数性的。因为在从根到叶子的任何路径上至少有一半的节点是黑色（根据红黑树性质4），根的黑色高度至少是formula_33。通过引理我们得到：
formula_34
因此根的高度是formula_1。