这是搬运自自己一年前的写的数据结构解析。
诸君姑且一看,希望这篇文章对朋友有帮助。如果有什么问题随时跟我反映,我会尽快做出修正。 : )
红黑树
我们都知道平衡二叉查找树(排序树)在极端情况下,查询效率为O(N)。因此需要一个自平衡的数据结构保证查找效率。本片主角红黑树树就这样诞生了。
红黑树(英语:Red–black tree)是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。它在1972年由鲁道夫·贝尔发明,被称为"对称二叉B树",它现代的名字源于Leo J. Guibas和Robert Sedgewick于1978年写的一篇论文。红黑树的结构复杂,但它的操作有着良好的最坏情况运行时间,并且在实践中高效:它可以在log n时间内完成查找,插入和删除,这里的n是树中元素的数目.
—— 摘自wiki
性质
红黑树是一颗查询复杂度保持在O(log N) 的自平衡二叉查找树。在原有的二叉查找树的性质下,通过给节点上色满足如下性质实现:
- 节点是红色或黑色。
- 根是黑色。
- 所有叶子都是黑色(叶子节点指NIL节点)。
- 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
- 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。
具体的某个红黑树入大概入下图所示。 注意任何红黑树都要满足上面五条性质。
操作
红黑树的操作包括新增、删除、和查询操作。 查询操作与之前的二叉树查找树雷同,这里略过。额外地,需要说明一下二叉查找树中的旋转操作
旋转
二叉排序树的旋转指的是不破坏二叉树性质的情况下,改变树的结构的一种操作。具体分为如下两种操作:
- 左旋, 将树进行逆时针旋转。如图所示,节点E进行左旋,原本E的右子树接替E的位置称为父亲,E成为S节点的左孩子。注意如果S原本有左子树(左子树的值应该在E到S之间),将该树称为E的新右儿子。仍然满足二叉排序树的规则:
- 右旋,将树进行顺时针旋转。左旋的镜像操作。如图所示:
注意,旋转是满足二叉排序树的结构变化操作。 旋转后的节点高度可能会发生变化。 红黑树而言,可能会导致与其性质冲突。 因此需要再对红黑树进行重染色。
新增
红黑树新增时,默认新增的是红色节点。一种情况是,如果节点是根结点,则将树染红即可;一种简单的情况是,当节点的父亲是黑色节点时显然不会违反之前的规则。当父亲节点是红色节点时。需要分情况讨论。
-
当节点的叔叔节点(父亲节点的兄弟节点)是红色时。 将父亲节点和叔叔节点染黑,将爷爷节点染红。这样操作后,所有经过父亲节点和叔叔节点黑色节点数不变,满足性质5,但爷爷节点染红后可能会导致它其与上层节点的冲突。因此当前指针指向爷爷节点,重新进行一次判断。
-
当前节点的叔叔节点为黑色时,分如下两种情况来讨论(这里以父亲节点为左子树来判断,若是右子树则镜像操作即可)
-
当前节点是父亲节点的左子树。 则将父亲节点染黑,爷爷节点染红。 再对爷爷进行右旋操作。 此时经过父亲节点的节点和黑节点数不变(满足性质5)。
-
当前节点是父亲节点的右子树。则将父亲节点进行左旋,当前指针指向原来的父节点。此时当前节点变为父亲节点的左子树。 按照 2.1 的方式处理。
-
删除
删除操作比较复杂。 大概跟味两部分:删除流程和删除节点后平衡操作。
删除流程
首先通过二分操作找到需要删除的节点。根据当前节点判断是否能删除:
- 如果被删除节点有两个孩子:直接寻找该节点的中序后继(即右子树中最小的节点)。后继节点的值覆盖当前节点值。指针指向后继重新进行判断。
- 如果被删除节点有一个孩子或孩子都为 NIL:则继续进行判断
- 如果被删除节点是红色节点:直接将孩子节点顶替当前节点。不会影响到性质。
- 如果被删除节点是黑色节点,且其孩子节点为红色:将孩子节点顶替当前节点,再讲孩子节点染黑即可。
- 如果被删除节点是黑色节点,且孩子节点都为黑色(此时孩子节点必为NIL节点,否则单独节点为黑色不满足性质5):当时情况较为复杂,则进行以下删除后平衡操作。
删除后平衡操作
首先是如果当前节点已经是根结点了,则将根结点染黑。
我们将删除前的节点定义为N(先平衡,再删除替换节点),节点的父节点为P, 兄弟节点为S,兄弟节点的左子树为SL,右子树为SR。我们遍历N、P、S、SL、SR可能有的情况有如下几种,我们将主意进行讨论。(下文将由N节点为P节点的左子树进行讨论。若为右子树,镜像操作即可)
N | P | S | SL | SR | case |
---|---|---|---|---|---|
B | S | B | B(NIL) | B(NIL) | (1) |
B | B | B | B(NIL) | B(NIL) | (2) |
B | B | S | B | B | (3) |
B | B(or S) | B | S | B(NIL) | (4) |
B | B(or S) | B | S(or B(NIL)) | S | (5) |
-
删除节点为黑色, 父亲节点为红色, S节点为黑色, 且S节点的孩子SL、SR都为黑色(NIL节点)。 则将P节点染黑,S节点染红。则经过S节点的黑色节点数不变。经过N的黑色节点数补齐。满足相关性质。
-
删除节点为黑色, 父亲节点为黑色, S节点为黑色, 且S节点的孩子SL、SR都为黑色(NIL节点)。将S节点染红。此时通过N节点和通过S节节点,即通过P的比删除前少1。将当前指针指向P节点,重新执行平衡操作。
-
删除节点为黑色,父亲节点为黑色,S节点为红色,则S节点的孩子SL、SR都为黑色。将P节点染红,S节点染黑,再对叶子节点进行左旋。此时经过S节点的数量不变。经过N的节点仍然少1,此时P为红色。按照情况4、5进行判断。
-
删除节点为黑色,父亲节点为红色或者为黑色,S节点为黑色,其左SL节点为红色,右节点为黑色(为NIL)。将SL染黑,S染红,S节点右旋。这样S和SL角色互换,可以按照情况5判断。
-
删除节点为黑色,父亲节点为红色或者黑色,S节点为黑色。其右子树SR为红色(SL为红色或黑(NIL))。S节点染为P节点的颜色,P节点染黑,SR节点染黑,P节点左旋。 此时SR节点不变, SL节点和N节点恢复。满足规律5。恢复平衡。
证明
我们在之前说过红黑树的查询效率是log2(n)。但具体为什么根据前面5条性质即确定红黑树的具体查询效率的呢?需要进行证明:
首先定义 BH(v) 为节点黑色高度( 红黑树的节点v上到他的叶子节点的黑色节点的个数,不计v本身)、TOTAL[ BH(v) ] 为 当黑高为 BH(v 时,树可能有的总叶子节点个数 可知
-
BH(v) = 0 时 v 此时可能为空节点或只有v本身 、TOTAL[ BH(v) ] 最小为 0
-
当 BH(v) > 0 时: 根据 性质5 黑高为 BH(V) 的 红黑树, 其子树的黑高至少为 BH(V) - 1 (或子树的高度仍为 BH(V) )即数的节点总数
T O T A L [ B H ( v ) ] ≥ 2 ⋅ T O T A L [ B H ( v ) − 1 ] + 1 TOTAL[BH(v)] \geq 2 \cdot TOTAL[BH(v) - 1] + 1 TOTAL[BH(v)]≥2⋅TOTAL[BH(v)−1]+1
由此可得
T O T A L [ B H ( v ) ] = 2 B H ( v ) − 1 TOTAL[BH(v)] = 2^{BH(v)} - 1 TOTAL[BH(v)]=2BH(v)−1
因此BH(V) 跟 TOTAL[V] 之间满足指数关系。又由性质4可知,树的黑高至少为这棵树的二分之一。 即 BH(V) >= H(V) / 2,可得,对于根结点root而言。有
n ≥ 2 B H ( r o o t ) − 1 n ≥ 2 H ( r o o t ) ÷ 2 − 1 n + 1 ≥ 2 H ( r o o t ) ÷ 2 l o g 2 ( n + 1 ) ≥ H ( r o o t ) ÷ 2 H ( r o o t ) ≤ 2 ⋅ l o g 2 ( n + 1 ) n \geq 2^{BH(root)} - 1 \\ n \geq 2^{H(root) \div 2} - 1 \\ n + 1 \geq 2^{H(root) \div 2} \\ log_2(n + 1) \geq H(root) \div 2 \\ H(root) \leq 2 \cdot log_2(n + 1) \\ n≥2BH(root)−1n≥2H(root)÷2−1n+1≥2H(root)÷2log2(n+1)≥H(root)÷2H(root)≤2⋅log2(n+1)
与 avl 的比较
两者都是经过优化的自平衡二叉树。有各自的自平衡算法,增删查效率都是log(n)但有一些区别。但从平衡性而言,AVL的平衡性更加严格。
红黑树允许较低的平衡性(但复杂度仍为log(n))换取更少的自平衡操作。由于avl数平衡性高于红黑树,查询效率上会高于红黑树(但复杂度仍为log(n))。红黑树算是比较「折衷」的平衡树方案,C++的STL中和JAVA的TreeMap & HashMap都实现了红黑树以达到O(log(n)) 的插入删除的效果