更多面经和面试题目解答欢迎访问https://github.com/darkmoon233/GreenPills-CSNotes
“叶结点” 或"NULL结点",它不包含数据而只充当树在此结束的指示,这些结点以及它们的父结点,在绘图中都会经常被省略。
当树的出现深度不平衡时,就需要进行部分子树的旋转。
在二叉平衡树的外侧插入(插入到X节点左子树的左节点,或是插入到X节点右子树的右节点)引起的平衡被破坏,通过单旋转(左旋或是右旋)可以解决
伪代码:
//T为二叉搜索树,x为要进行旋转的节点
Left-Rotate(T,x)
y <- x.right // 对y赋值
x.right <- y.left // 将y的左节点赋给x做右节点(一开始y是x的右节点,最终目标是将y的左节点给x做右节点,y和x的地位互换)
y.left.p <- x // 修改y的左节点的父节点为x(双(三)向赋值才算是一次完整的节点换位
y.p <- x.p // 将x的父节点赋给y的父节点
// 根据x所处的位置不同进行分类处理,x如果是根节点则根节点改为y,x如果在父节点的左(右)节点,将父节点的左(右)节点换为y节点的
if x.p = null
else if x=x.p.left
then x.p.left <- y
else x.p.right <- y
y.left <- x
x.p <- y
右旋的过程是非常像的,不多赘述。
在二叉平衡树的内侧插入(插入到X节点左子树的右节点,或是插入到X节点左子树的右节点)引起的平衡被破坏,通过双旋转(单旋转两次)可以解决
二叉查找树插入就是左右比较一下,找到位置直接插就行。(不存在插入的值已经存在的情况)
伪代码:
Btree-Insert(T,z)
y <- null
x <- T.root
while x!= null
do y <- x
if x.key > z.key
then x <- x.left
else x <- right
z.p <- y
if y=null
then z <- T.root
else if y.key > z.key
then y.left <- z
else y.right <- z
由于红黑树除了引入二叉平衡树的基础外,还引入了颜色等一系列规则,因此在调整之后需要修改。
红黑树的插入(和普通的二叉搜索树一样,只是增加了最后的涂色操作和修正操作):
Rb-Insert(T,z)
y <- null
x <- T.root
while x != null
do y <- x
if z.key < x.key
then x <-x.left
else x <- x.right
z.p <- y
if y = null
then T.root <- z // 空树
else if z.key < y.key
then y.left <- z
else y.right <- z
z.left = null
z.right = null
z.color = red
Rb-Insert-Fixup(T,z) // 进行修正
分析一下插入的几种情况:
对于情况1,直接将节点涂黑就可以了(因为不涉及到其他的规则)
对于情况2,红黑树没有被破坏,不需要进行额外的工作
对于情况3,单纯的变色不能完全解决问题,需要进行更进一步的分类。
修复红黑树的伪代码:
Rb-Insert-Fixup(T,z)
while z.p.color = red
do if z.p = z.p.p.left
then y <- z.p.p.right
if y.color = red // 情况1 :uncle节点为红色
then z.p.color <- black // 父节点重染色为黑色
y.color <- black // uncle节点重染色为黑色
z.p.p.color <- red // 祖父节点重染色为红色
z <- z.p.p // z指向祖父节点,重新进行染色流程(因为将祖父变色可能会带来违反红黑树规则的连锁反应)
else if z = z.p.right // 情况2 :uncle节点为黑色,且自己为父节点的右子树
then z <- z.p // z指向z的父节点,以当前节点进行左旋
Left-Rotate(T,z)
z.p.color <- black // 情况3 :uncle节点为黑色,自己为父节点的左子树(如果是经历了左旋的话,那么z已经是左节点了)
z.p.p.color <- red // 父节点染色为黑色,祖父节点染色为红色。
Right-Rotate(T,z.p.p) // 以祖父节点进行右旋
else // 交换一下旋转方向
T.root.color <- black
下面用一个例子表现一下插入一个节点之后的过程(图片来源:julycoding)
这时候大功告成,没有问题了。
为了避免父子节点皆为红色的情况持续性的向上发展导致时间上的瓶颈,可以用以下的方法解决:新增节点为A,就沿着A的路径,只要看到某节点X的两个节点都是红色,就把X改为红色,两个子节点改为黑色,但是如果X的父节点也是红色,就使用之前的策略进行调整,进行单旋转或是双旋转并改变颜色。这样就可以避免不断地向上回溯(因为已经把回溯的问题解决了)
过程和如下图示是一致的。
待删除节点有三种情况:
红黑树的删除节点的过程实际上是先用二叉查找树(BST)的方法进行删除,再用红黑树特有的方法进行调整。
二叉查找树的删除节点的情况如下:
红黑树删除节点的过程如下
Rb-Tree-Delete(T,z)
if z.left = null or z.right = null // 至少一个子节点为空
then y <- z // y用来表示待删除的节点
else y <- Tree-Successor(z) // 将z的后继节点赋值给y(后继节点是中序遍历后的集合每个元素的下一个元素)
// 在至少一个子节点为空的情况下用x代替那个节点
if y.left != null
then x <- y.left
else x <- y.right
// 删除的如果是根节点,则用子节点代替父节点成为根节点,否则就是用子节点替换删除节点的位置
x.p <- y.p // 删除操作
if y.p = null
then T.root <- x
else if y = y.p.left
then y.p.left <- x
else y.p.right <- y
// 有两个儿子时候用儿子的value替换当前节点的值
if y!=z
then z.key <- y.key
copy y's satellite data into z
if y.color = Black // 删除红色节点不会带来规则的违反,删除黑色可能会带来各种问题
then Rb-Delete-Fixup(T,x)
return y // 返回被删除节点
删除后恢复红黑树特性:
Rb-Delete-Fixup(T,x)
while x != T.root and x.color = black
do if x = x.p.left // 若x是父节点的左孩子,令w是他的兄弟节点
then w <- x.p.right
if w.color = red // 兄弟是红色的
then w.color <- black // case1:将兄弟节点X设置为黑色(这里导致后边w的颜色总是黑色的)
x.p.color <- red // case1:将父节点置为红色
Left-Rotate(T,x.p) // case1:以x的父节点为支点进行左旋
w <- x.p.right // case1: w定义为x的新的兄弟
if w.left.color = black and w.right.color = black // w的两个儿子都是黑色
then w.color <- red // case2: 兄弟节点设为红色
x <- x.p // case2: 当前节点变为父节点
else if w.right.color = black // 兄弟节点的右儿子是黑色,左儿子红色(根据之前的if来的)
then w.left.color <- black // case3: 兄弟节点的左儿子的颜色变为黑色
w.color <- red // case3: 兄弟节点变红色
Right-Rotate(T,w) // case3: 右旋
w <- x.p.right // case3: w指向新的兄弟节点
w.color <- x.p.color // case4:兄弟节点用他的父亲的颜色染色
x.p.color <- black // case4: 父节点染成黑色
w.right.color <- black // case4: 兄弟节点的右儿子染成黑色
Left-Rotate(T,x.p) // case4: 以x的父节点进行左旋
x <- T.root // case4: 设置x为根节点
else (same as then clause with "right" and "left" exchanged)
x.color <- black
上面的修复情况看起来有些复杂,下面我们用一个分析技巧:我们从被删结点后来顶替它的那个结点开始调整,并认为它有额外的一重黑色。这里额外一重黑色是什么意思呢,我们不是把红黑树的结点加上除红与黑的另一种颜色,这里只是一种假设,我们认为我们当前指向它,因此空有额外一种黑色,可以认为它的黑色是从它的父结点被删除后继承给它的,它现在可以容纳两种颜色,如果它原来是红色,那么现在是红+黑,如果原来是黑色,那么它现在的颜色是黑+黑。有了这重额外的黑色,原红黑树性质5就能保持不变。现在只要恢复其它性质就可以了,做法还是尽量向根移动和穷举所有可能性。"--saturnman。
对不起我没看懂saturnman的话。 --greenpill
对于恢复红黑树的性质有如下的几种情况:
前两种情况实际上已经解决了,现在把后面四种情况处理一下。
case1:当前节点是黑+黑,且兄弟节点是红色的(因为兄弟是红色,所以他的子节点都是黑的)
兄弟节点染成黑色,父节点染成红色,然后左旋。
还要记得重设兄弟节点方便后续操作。
case2:当前节点是黑+黑,且兄弟节点是黑色的,且兄弟节点的子节点都是黑色的
兄弟节点染色为红色,当前节点变为父节点
case3: 当前节点是黑+黑,且兄弟节点是黑色的,且兄弟节点的左子是红色的,右子是黑色的
把兄弟节点染红,之后以兄弟节点未支点进行右旋,更新w为新的兄弟节点
case4:当前节点是黑+黑,且兄弟节点是黑色的,且兄弟节点的右子是红色的,左子是任意的
兄弟节点用他的父亲的颜色染色,父节点染成黑色,兄弟节点的右儿子染成黑色,以x的父节点进行左旋,设置x为根节点
待添加删除节点的示例。
后继节点指的是中序遍历中的后一个节点
若一个节点有右子树,那么该节点的后继节点是其右子树中val值最小的节点(也就是右子树中所谓的leftMostNode)
若一个节点没有右子树,那么判断该节点和其父节点的关系
2.1 若该节点是其父节点的左边孩子,那么该节点的后继结点即为其父节点
2.2 若该节点是其父节点的右边孩子,那么需要沿着其父亲节点一直向树的顶端寻找,直到找到一个节点P,P节点是其父节点Q的左边孩子(可参考例子2的前驱结点是1),那么Q就是该节点的后继节点
ps.ps 我也看到有使用前驱节点来考虑红黑树的,我觉得这两种没什么太大区别
本文github地址:https://github.com/darkmoon233/GreenPill-Notes/blob/master/网易面试/红黑树.md 会介绍我的面试和笔试的内容。