本篇要讲的就是红黑树的删除操作
红黑树插入操作请参考 数据结构 - 红黑树(Red Black Tree)插入详解与实现(Java)
红黑树的删除是红黑树操作中比较麻烦且比较有意思的一部分。
在此之前,重申一遍红黑树的五个定义:
1. 红黑树的节点不是黑色的就是红色的
2. 红黑树的根节点一定是黑色的
3. 红黑树的所有叶子节点都是黑色的(注意:红黑树的叶子节点指Nil节点)
4. 红黑树任何路径上不允许出现相邻两个红色节点
5. 从红黑树的任一节点开始向下到任意叶子节点所经过的黑色节点数目相同
接着,请大家谨记你操作的对象都是一颗标准的红黑树,所以不要脑补过多不能存在的情况,如果你考虑的情况不在本文的讨论范围之内,可以往上看看是不是你的情况违反了五条规则其中某一条,若还有疑问,欢迎留言讨论。
D(Delete)表示待删除节点
P(Parent)表示待删除节点的父节点
S(Sibling)表示待删除节点的兄弟节点
U(Uncle)表示带删除节点的叔叔节点
GP(Grandparent)表示待删除节点的祖父节点
XL(Left child of X)表示节点X的左子树节点
XR(Right child of X)表示节点X的右子树节点
删除一个新的节点有以下四种情况:
1. 删除的节点是叶子节点(非Nil)
2. 删除的节点只有左子树
3. 删除的节点只有右子树
*4. 删除的节点同时拥有左子树和右子树
其实只有上面前三种情况,对于第四种情况,可以找到待删除节点的直接后继节点,用这个节点的值代替待删除节点,接着情况转变为删除这个直接后继节点,情况也变为前三种之一。
因为有很多情况是不存在,待删除节点是叶子节点(非Nil)的情况稍微复杂一些,
我们下面先考虑待删除的节点只有左子树或只有右子树的情况。
不存在的情况包括
①
②
☂ (san)
④
⑤
⑥
请读者分析一下上面不可能情况的原因,不复杂但一定要知道为什么。两个节点的颜色红黑情况加上左右子树情况,总共八种情况,上面已经排除了六种,剩下以下两种可能的的情况。
①
DL表示DL节点原本的值
②
DR表示DR节点原本的值
这两种情况的调整操作比较简单,直接用DL/DR的元素值代替D的元素,再把DL/DR直接删去就好,操作过后不违反红黑树定义,删除结束。
删除节点的四种情况已经解决了三种,剩下最后一种了。
待删除的节点是叶子节点的情况:
因为待删除的节点有可能是红色也可能是黑色。
如果待删除节点是红色的,那直接删去这个节点,删除结束。
如果待删除节点是黑色的,根据父节点P和兄弟节点S的情况,可分为以下五种情况。
情况1:父节点P是红色节点
或者
这两种情况是一样的,我们讨论第一个图就好,当把D删去后,从P的左子树下来的黑色节点数目少了一,对应的调整做法为,把P染成黑色,此时P左子树的黑色结点数目恢复,但此时右子树黑色结点数目多了一,再把S对应染成红色即可。
图例:
情况2:兄弟节点S是红色节点
或者
只能是这两种情形,做法是把P染成红色,S染成黑色,然后以P为轴做相应的旋转操作(如果D为P的左子树节点则以P为轴做左旋操作,如果D为P的右子树节点则以P为轴做右旋操作)
图例(以第一种情形为例):
到这里就把情况二变成了情况一(父节点为红色)的情况,接着按照情况一的处理方式进行操作。
情况3:结点D的远亲侄子为红色节点的情况
此时父节点P的颜色可红可黑,这种情况的调整做法是,交换P和S的颜色,然后把远侄子节点SR/SL设置为黑色,再以P为轴做相应的旋转操作(如果D为P的左子树则左旋,如果D为P的右子树则右旋)
图例(以第一种情形为例):
调整前后从P点下来的所有路径黑色节点数目没有发生变化,删除节点D后结束。(注意此处S的左子树SL可以为Nil节点或者红色节点,但依然是按照上面的规则进行调整,对结果没有影响)
情况4:节点D的近亲侄子为红色节点的情况
注意此处节点D的远侄子节点必须为Nil节点,否则就变成情况3了。这种情况的调整方式是,把S染成红色,把近侄子节点SR/SL染成黑色,然后以节点S为轴做相应的旋转操作(如果D为P的左子树则以S为轴做右旋操作,如果D为P的右子树则以S为轴做左旋操作)。
图例(以第一种情形为例)
然后就真的变成情况3了......接着按照情况3的处理方式进行处理。
情况5:节点D,P,S均为黑色节点
以第一种情形为例,这种情况删除D之后,从P的左子树下来的黑色节点数目少了一,且没有周围也没有红节点来补全这个黑节点,做法就是把D删去,然后把节点S染成红色,这样一来节点P的左右子树路径的黑色节点路径就一样了,但导致节点P整棵子树的任意路径的黑色节点数比其他路径少了一,此时我们再从P开始(即把P当成D),但不再删除P,向上继续调整,直到根节点(一直是情况5)或者遇到情况1~4并调整后结束。
我看过几篇文章,最后一种情况基本讲到我这里就已经结束了,所以我在这种情况上也因此多话了一点时间去理解。若此处有更详细的例子,会更能帮助理解,所以我决定举两个例子,来说明什么叫从P节点开始向上调整,哪种情况就是要直到根节点, 哪种情况就是遇到情况1~4,然后调整后结束。
从节点P往上依然是全黑的情况(父节点,兄弟节点均为黑色)
从节点P往上是其他情况
这里只是举个例子,无论是变成情况1~4的哪种,经过调整之后都无需再继续上溯,因为此时黑色节点数目已经恢复,且例子里面GP不是根节点,因为根节点不可能为红色。
下面倒序总结一下
待删除的节点是黑色叶子(非Nil)节点的情况
待删除的节点是红色叶子节点的情况
情况6 直接删除该节点
待删除的节点只拥有左子树或只拥有右子树的情况
待删除的节点同时拥有左子树和右子树的情况
情况9 找出直接后继节点并转变为情况1~8
至此,关于红黑树删除的所有情况均讨论完毕,以上的每个字以及每个图都是自己写自己画的,花了不少时间,希望大家多看看,结合图理解比较形象,彻底搞懂红黑树的操作,代码却是次要的,因为同一种思路也有不同的代码风格和实现方式。同时也希望这篇文章能对大家有帮助。
下面是删除的代码:
总的公共方法是这样的,找到该元素对应的节点,然后删除该节点:
public boolean delete(int elem) { if (null == this.root) { return false; } else { TreeNode node = this.root; // find out the node need to be deleted while (null != node) { if (node.getElem() == elem) { deleteNode(node); return true; } else if (node.getElem() > elem) { node = node.getLeft(); } else { node = node.getRight(); } } return false; } }
删除节点的方法为私有方法,包含了同时拥有左右子树,只拥有左子树以及只拥有右子树的操作
private void deleteNode(TreeNode node) { if(null == node.getLeft() && null == node.getRight()) { if (node.getColor() == NodeColor.RED) { delete_red_leaf(node, true); } else { delete_black_leaf(node, true); } } else if (null == node.getLeft()) { // the node color must be black and the right child must be red node // replace the element of node with its right child's // cut off the the link between node and its right child node.setElem(node.getRight().getElem()); node.setRight(null); } else if (null == node.getRight()) { node.setElem(node.getLeft().getElem()); node.setLeft(null); } else { // both children are not null TreeNode next = node.getRight(); while (null != next.getLeft()) { next = next.getLeft(); } TreeUtils.swapTreeElem(node, next); deleteNode(next); } }
由大及小,删除的节点是红色叶子节点的情况,注意此处待删除的节点肯定不是根节点,所以不需要考虑该节点为根节点的情况
private void delete_red_leaf(TreeNode node, boolean needDel) { TreeNode parent = node.getParent(); if (node == parent.getLeft()) { parent.setLeft(null); } else { parent.setRight(null); } }
最后就是最麻烦的删除的删除黑色叶子(非Nil)节点的情况,找出兄弟节点,找出远侄子节点,找出近侄子节点。
private void delete_black_leaf(TreeNode node, boolean needDel) { TreeNode parent = node.getParent(); if (null != parent) { boolean nodeInLeft = parent.getLeft() == node; TreeNode sibling = nodeInLeft ? parent.getRight() : parent.getLeft(); TreeNode remoteNephew = null == sibling ? null : (nodeInLeft ? sibling.getRight() : sibling.getLeft()); TreeNode nearNephew = null == sibling ? null : (nodeInLeft ? sibling.getLeft() : sibling.getRight()); if (sibling.getColor() == NodeColor.RED) { delete_sibling_red(node); } else if (null != remoteNephew && remoteNephew.getColor() == NodeColor.RED) { delete_remote_nephew_red(node); } else if (null != nearNephew && remoteNephew.getColor() == NodeColor.RED) { delete_near_nephew_red(node); } else { // the sibling is also a leaf if (parent.getColor() == NodeColor.RED) { delete_parent_red(node); } else { sibling.setColor(NodeColor.RED); delete_black_leaf(parent, false); } } } if (needDel) { if (null == parent) { this.root = null; } else if (node.getParent().getLeft() == node) { parent.setLeft(null); } else { parent.setRight(null); } } }
删除叶子节点包含了另外一个参数 boolean needDel ,因为上面提到的有些情况需要继续上溯,所以有些节点不能被删除。
红黑树所有操作大功告成,希望对大家的学习有所帮助。
PS:请问大家有可以画好看的二叉树的软件推荐吗
请尊重知识产权,引用转载请通知作者!