最近花了些时间重拾数据结构的基础知识,先尝试了红黑树,花了大半个月的时间研究其原理和实现,下面是学习到的知识和一些笔记的分享。望各位多多指教。本次代码的实现请点击:红黑树实现代码 – gisthtml
红黑树是带有 color 属性的二叉搜索树,color 的值为红色或黑色,所以叫作红黑树。node
对红黑树的每一个结点的结构体定义以下:git
1github 2算法 3数据结构 4函数 5学习 6网站 7spa 8 |
struct RBNode { int color; void *key; void *value; struct RBNode *left; struct RBNode *right; struct RBNode *parent; }; |
设根结点的 parent 指针指向 NULL,新结点的左右孩子 left 和 right 指向 NULL。叶子结点是 NULL。
定义判断红黑树颜色的宏为
1 |
#define ISRED(x) ((x) != NULL && (x)->color == RED) |
所以,叶子结点 NULL 的颜色为非红色,在红黑树中,它就是黑色,包括黑色的叶子结点。
黑高的定义,从某个结点 x 触发(不含该结点)到达一个叶结点的任意一条简单路径上的黑色结点个数称为该结点的黑高(black-height),记做 bh(x)。
下面是一个红黑树的例子
旋转操做在树的数据结构里面很常常出现,好比 AVL 树,红黑树等等。不少人都了解旋转的操做是怎么进行的(HOW),在网上能找到不少资料描述旋转的步骤,可是却没有人告诉我为何要进行旋转(WHY)?为何要这样旋转?经过与朋友交流,对于红黑树来讲,之因此要旋转是由于左右子树的高度不平衡,即左子树比右子树高或者右子树比左子树高。那么,以左旋为例,经过左旋转,就能够将左子树的黑高 +1,同时右子树的黑高 -1,从而恢复左右子树黑高平衡。
以右旋为例,α 和 β 为 x 的左右孩子,γ 为 y 的右孩子,由于 y 的左子树比右子树高度多一,所以以 y 为根的子树左右高度不平衡,那么以 y-x 为轴左旋使其左右高度平衡,左旋以后 y 和 β 同时成为 x 的右孩子,然而由于要旋转的是 x 和 y 结点,所以就让 β 成为 y 的左孩子便可。
旋转的算法复杂度:从图示可知,旋转的操做只是作了修改指针的操做,所以算法复杂度是 O(1)。
红黑树的全部操做的算法复杂度都是 O(lgn)。这是由于红黑树的最大高度是 2lg(n+1)。
证实以下:
设每一个路径的黑色节点的数量为 bh(x),要证实红黑树的最大高度是 2lg(n+1),首先证实任何子树包含 2^bh(x) - 1 个内部节点。
下面使用数学概括法证实。
当 bh(x) 等于 0 时,即有 0 个节点,那么子树包含 2^0 - 1 = 0 个内部节点,得证。
对于其余节点,其黑高为 bh(x) 或 bh(x) - 1,当 x 是红节点时,黑高为 bh(x),不然,为 bh(x) - 1。对于下一个节点,由于每一个孩子节点都比父节点的高度低,所以概括假设每一个子节点至少有 2^bh(x)-1 - 1 个内部节点,所以,以 x 为根的子树至少有 2^(bh(x)-1) - 1 + 2^(bh(x)-1) - 1 = 2^bh(x) - 1个内部节点。
设 h 是树高,根据性质 4 可知道,每一条路径至少有一半的节点是黑的,所以 bh(x) - 1 = h/2。
那么红黑树节点个数就为 n >= 2^h/2 - 1。
可得 n + 1 >= 2^h/2。两边取对数得:
|
|
由上面的证实可得,红黑树的高度最大值是 2log(n+1),所以红黑树查找的复杂度为 O(lgn)。对于红黑树的插入和删除操做,算法复杂度也是 O(lgn),所以红黑树的全部操做都是 O(lgn)
的复杂度。
红黑树的插入操做,先找到要新节点插入的位置,将节点赋予红色,而后插入新节点。最后作红黑树性质的修复。
由于插入操做只可能会违反性质 二、四、5,对于性质 2,只须要直接将根节点变黑便可;那么须要处理的就有性质 4 和性质 5,若是插入的是黑节点,那么就会影响新节点所在子树的黑高,这样一来就会违反性质 5,若是新节点是红色,那么新插入的节点就不会违反性质 5,只须要处理违反性质 2 或性质 4 的状况。即根节点为红色或者存在两个连续的红节点。简而言之,就是减小修复红黑性质被破坏的状况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
RB-INSERT(T, node) walk = T.root prev = NULL while (walk != NULL) prev = walk if (node.key < walk.key) walk = walk.left else walk = walk.right node.parent = walk if (walk == NULL) T.root = node else if (node.key < walk.key) walk.left = node else walk.right = node RB-INSERT-FIXUP(T, node) |
插入以后,若是新结点(node)的父结点(parent)或者根节点(root)是红色,那么就会违反了红黑树的性质 4 或性质 2。对于后者,只须要直接将 root 变黑便可。
而前者,违反了性质 4 的,即红黑树出现了连续两个红结点的状况。修复的变化还要看父结点是祖父结点的左孩子仍是右孩子,左右两种状况是对称的,此处看父结点是祖父结点的左孩子的状况。要恢复红黑树的性质,那么就须要将 parent 的其中一个变黑,这样的话,该结点所在的子树的黑高 +1,这样就会破坏了性质 5,违背了初衷。所以须要将 parent->parent(grandparent)的另外一个结点(uncle 结点)的黑高也 +1 来维持红黑树的性质。
若是 uncle 是红色,那么直接将 uncle 变为黑色,同时 parent 也变黑。可是这样一来,以 grandparent 为根所在的子树的黑高就 +1,所以将 grandparent 变红使其黑高减一,而后将 node 指向 grandparent,让修复结点上升两个 level,直到遇到根结点为止。
若是 uncle 是黑色,那么就不能将 uncle 变黑了。那么只能将红节点上升给祖父节点,即将祖父结点变红,而后将父结点变黑,这样一来,以父结点为根的子树的左右子树就不平衡了,此时左子树比右子树的黑高多 1,那么就须要经过将祖父结点右旋以调整左右平衡。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
RB-INSERT-FIXUP(T, node) while IS_RED(node) parent = node->parent if !IS_RED(parent) break grandparent = parent->parent if parent == grandparent.left uncle = grandparent.right if IS_RED(uncle) parent.color = BLACK uncle.color = BLACK grandparent.color = RED node = grandparent elseif node == parent.right LEFT_ROTATE(T, parent) swap(node, parent) else parent.color = BLACK grandparent.color = RED RIGHT_ROTATE(T, grandparent) else same as then clause with "right" and "left" exchanged
T.root.color = BLACK |
插入的步骤主要有两步
a. 找到新结点的插入位置 b. 进行插入修复。而插入修复包括旋转和使修复结点上升。
对于 a,从上面可知,查找的算法复杂度是 O(lgn)。
对于 b,插入修复中,每一次修复结点上升 2 个 level,直到遇到根结点,走过的路径最大值是树的高度,算法复杂度是 O(lgn);由旋转的描述可得其算法复杂度是 O(1),所以插入修复的算法复杂度是 O(lgn)。
综上所述,插入的算法复杂度 O(INSERT) = O(lgn) + O(lgn) = O(lgn)。
红黑树的删除操做,先找到要删除的结点,而后找到要删除结点的后继,用其后继替换要删除的结点的位置,最后再作红黑树性质的修复。
红黑树的删除操做比插入操做更复杂一些。
要删除一个结点(node),首先要找到该结点所在的位置,接着,判断 node 的子树状况。
- 若是 node 只有一个子树,那么将其后继(successor)替换掉 node 便可;
- 若是 node 有两个子树,那么就找到 node 的 successor 替换掉 node;
- 若是 successor 是 node 的右孩子,那么直接将 successor 替换掉 node 便可,可是须要将 successor 的颜色变为 node 的颜色;
- 若是 successor 不是 node 的右孩子,而由于 node 的后继是没有左孩子的(这个能够查看相关证实),因此删除掉 node 的后继 successor 以后,须要将 successor 的右孩子 successor.right 补上 successor 的位置。
删除过程当中须要保存 successor 的颜色 color,由于删除操做可能会致使红黑树的性质被破坏,而删除操做删除的是 successor。所以,每一次改变 successor 的时候,都要更新 color。
TRANSPLANT(T, u, v) 是移植结点的操做,此函数的功能是使结点 v 替换结点 u 的位置。在删除操做中用来将后继结点替换到要删除结点的位置。
用 x 表示有非空左右孩子的结点。在树的中序遍历中,在 x 的左子树的结点在 x 的前面,在 x 的右子树的结点都在 x 的后面。所以,x 的前驱在其左子数,后继在其右子树。
假设 s 是 x 的后继。那么 s 不能有左子树,由于在中序遍历中,s 的左子树会在 x 和 s 的中间。(在 x 的后面是由于其在 x 的右子树中,在 s 的前面是由于其在 x 的左子树中。)在中序遍历中,与前面的假设同样,若是任何结点在 x 和 s 之间,那么该结点就不是 x 的后继。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
RB-DELETE(T, node) color = node.color walk_node = node if IS_NULL(node.left) need_fixup_node = node.right transplant(T, node, need_fixup_node) elseif IS_NULL(node.right) need_fixup_node = node.left transplant(T, node, need_fixup_node) else walk_node = minimum(node.right) color = walk_node.color need_fixup_node = walk_node.right if walk_node.parent != node transplant(T, walk_node, walk_node.right) walk_node.right = node.right walk_node.right.parent = walk_node transplant(T, node, walk_node) walk_node.left = node.left walk_node.left.parent = walk_node walk_node.color = node.color
if color == BLACK RB-DELETE-FIXUP(T, need_fixup_node) |
注:笔者参考的是算法导论的伪代码,可是在实现的时候,由于用 NULL 表示空结点,若是须要修复的结点 need_fixup_node为空时没法拿到其父结点,所以保存了其父结点 need_fixup_node_parent 及其所在方向 direction,为删除修复时访问其父结点及其方向时作调整。
删除过程当中须要保存 successor 的颜色 color,由于删除操做可能会致使红黑树的性质被破坏,而删除操做删除的是 successor。所以,每一次改变 successor 的时候,都要更新 color。
会致使红黑树性质被破坏的状况就是 successor 的颜色是黑色,当 successor 的颜色是红色的时候,不会破坏红黑树性质,理由以下:
- 性质 1,删除的是红结点,不会改变其余结点颜色,所以不会破坏。
- 性质 2,若是删除的是红结点,那么该结点不多是根结点,所以根结点的性质不会被破坏。
- 性质 3,叶子结点的颜色保持不变。
- 性质 4,删除的是红结点,由于原来的树是红黑树,因此不可能出现连续两个结点为红色的状况。由于删除是 successor 只是替换 node 的位置,可是颜色被改成 node 的颜色。另外,若是 successor 不是node 的右孩子,那么就须要先将 successor 的右孩子 successor->right 替换掉 successor,若是 successor 是红色,那么 successor->right 确定是黑色,所以也不会形成两个连续红结点的状况。性质 4 不被破坏。
- 性质 5,删除的是红结点,不会影响黑高,所以性质 5 不被破坏。
若是删除的是黑结点,可能破坏的性质是 二、四、5。理由及恢复方法以下:
- 若是 node 是黑,其孩子是红,且 node 是 root,那么就会违反性质 2;(修复此性质只须要将 root 直接变黑便可)
- 若是删除后 successor 和 successor->right 都是红,那么会违反性质 4;(直接将 successor->right 变黑就能够恢复性质)
- 若是黑结点被删除,会致使路径上的黑结点 -1,违反性质 5。
那么剩下性质 5 较难恢复,不妨假设 successor->right 有一层额外黑色,那么性质 5 就得以维持,而这样作就会破坏了性质 1。由于此时 new_successor 就为 double black(BB)或 red-black(RB)。那么就须要修复new_successor 的颜色,将其“额外黑”上移,使其红黑树性质完整恢复。
注意:该假设只是加在 new_successor 的结点上,而不是该结点的颜色属性。
若是是 R-B 状况,那么只须要将 new_successor 直接变黑,那么“额外黑”就上移到 new_successor 了,修复结束。
若是是 BB 状况,就须要将多余的一层“额外黑”继续上移。此处还要看 new_successor 是原父结点的左孩子仍是右孩子,这里设其为左孩子,左右孩子的状况是对称的。
若是直接将额外黑上移给父结点,那么以 new_successor 的父结点为根的子树就会失去平衡,由于左子树的黑高 -1 了。所以须要根据 new_successor 的兄弟结点 brother 的颜色来考虑调整。
若是 brother 是红色,那么 brother 的两个孩子和 parent 都是黑色,此时额外黑就没法上移给父结点了,那么就须要作一些操做,将 brother 和 parent 的颜色交换,使得 brother 变黑, parent 变红,这样的话,brother 所在的子树黑高就 +1 了,以 parent 为根作一次左旋恢复黑高平衡。旋转以后,parent 是红色的,且 brother 的其中一个孩子成为了 parent 的新的右孩子结点,将 brother 从新指向新的兄弟结点,而后接着考虑其余状况。
若是 brother 是黑色,那么就须要经过将 brother 的黑色和 successor 的额外黑组成的一重黑色上移达到目的,而要上移 brother 的黑色,还须要考虑其孩子结点的颜色。
若是 brother->right 和 brother->right 都是黑色,那么好办,直接将黑色上移,即 brother->color = RED。此时包含额外黑的结点就变成了 parent。parent 为 RB 或 BB,循环继续。
若是 brother->left->color =RED,brother->right->color = BLACK,将其转为最后一种状况一块儿考虑。即将 brother->right 变红。转换步骤为:将 brother->left->color = BLACK; brother->color = RED。这样的话 brother 的左子树多了一层黑,右旋 brother,恢复属性。而后将 brother 指向如今的 parent 的右结点,那么如今的 brother->right 就是红色。转为最后一种状况考虑。
若是 brother->right->color = RED。那么就要将 brother->right 变黑,使得 brother 的黑色能够上移而不破坏红黑树属性,上移步骤是使 brother 变成 brother->parent 的颜色,brother->parent 变黑这样一来,黑色就上移了。而后左旋 parent,这样 successor 的额外黑就经过左旋加进来的黑色抵消了。可是 parent 的右子树的黑高就 -1 了,而经过刚刚将 brother->right 变黑就弥补了右子树减去的黑高。如今就不存在额外黑了,结束修复,而后让 successor 指向 root,判断 root 是否为红色。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
while node != root && node.color == BLACK) parent = node.parent if node = parent.left brother = parent.right if IS_RED(brother) brother.color = BLACK parent.color = RED LEFT_ROTATE(T, parent) brother = parent.right
if brother.left.color == BLACK and brother.right.color == BLACK brother.color = RED node = parent elseif brother.right.color = BLACK brother.left.color = BLACK brother.color = RED RIGHT_ROTATE(T, brother) brother = parent.right else brother.color = parent.color parent.color = BLACK brother.right.color = BLACK LEFT_ROTATE(T, parent) node = root else (same as then clause with “right” and “left” exchanged) node.color = BLACK |
删除的操做主要有查找要删除的结点,删除以后的修复。
修复红黑树性质主要是旋转和结点上移。对于查找来讲,查找的算法复杂度是O(lgn),旋转的复杂度是O(1),结点上移,走过的路径最大值就是红黑树的高,所以上移结点的复杂度就是O(lgn)。
综上所述,删除算法的复杂度是 O(DELETE) = O(lgn) + O(1) + O(lgn) = O(lgn)
。
若是对部分步骤不理解,能够到这个网站看看红黑树每一步操做的可视化过程:红黑树可视化网站。
本次代码的实现请点击:红黑树实现代码
由于基础知识比较薄弱,因此想补一下本身的基础,无奈悟性较低,花了大半个月时间才把红黑树给理解和实现出来。中途跟朋友讨论了不少次,所以有以上的这些总结。以前一直不敢去实现红黑树,由于以为本身根本没法理解和实现,心里的恐惧一直压抑着本身,但通过几回挣扎以后,终于鼓起勇气去研究一番,发现,只要用心去研究,就没有解决不了的问题。纠结了好久要不要发这篇博文,这只是一篇知识笔记的记录,并不敢说指导任何人,只想把本身在理解过程当中记录下来的笔记分享出来,给有须要的人。但其实想一想,纠结个蛋,让笔记做为半成品躺在印象笔记里沉睡,还不如花时间完善好发布出来,而后有兴趣的继续探讨一下。
若是真的要问我红黑树有什么用?为何要学它?我真的回答不上,可是我以为,基础的东西,多学一些也无妨。只有学了,有个思路在脑海里,之后才能用得上,否则等真正要用才来学的话,彷佛会浪费了不少学习成本。
http://blog.jobbole.com/103045/