普通的二叉搜索树在最坏的情况下,可能退化成一个链表。而又因为二叉搜索树的所有操作的性能(添加,删除,查找等),与二叉搜索树的高度有关。在最坏的情况下,二叉搜索树的高度和元素个数相同,此时二叉搜索树的效率降为了O(n)级别。
所以为了防止我们的二叉搜索树退化成一个链表,就产生了平衡二叉树。平衡二叉树可以保证它的左右两个子树的高度差不会超过1。平衡二叉树有很多实现,一个经典实现就是红黑树。
红黑树
在红黑树中将树中的节点划分为两种状态,分别用黑色和红色来表示。
红黑树为了保证自己能够平衡子树,所以制订以下五个规则:
1、每个节点必须有颜色,要么黑色,要么红色,没有别的颜色。
2、根节点必须是黑色
3、所有的空节点(nil节点)都认为是黑色节点。
4、红色的节点不能连续,即一个红色的节点,它的父节点和子节点不能也是红色的,
5、无论从哪一个节点起始,到它每个叶子节点的路径中,黑色节点数量必须相同。
在对红黑树进行添加、删除等操作之后,必须使红黑树符合这5个规则。
那么问题来了,在添加删除操作之后,树中节点的数量都变了,是怎么保证整个树满足上述这些规则呢?
这里涉及到3种操作,变色、左旋和右旋。通过这个三种操作,在增删节点之后调整树的形状结构,使它满足上述5个规则。这也是红黑树能保持平衡的原因。
变色操作我们在下文的添加、删除节点的实际操作中,再进行在描述。
先来说一下左右旋。
左旋
左旋是指以某节点为支点,进行逆时针旋转。如下图,是以2为支点进行的左旋:文字描述一下就是,2的右孩子节点4,变为了2的父节点,2由父节点变为4的左孩子。同时,4原来的左孩子变为2的右孩子。
右旋
右旋与左旋相反,即以某节点为支点进行顺时针旋转。同样,我们看下图,是以5为支点进行的右旋:
文字描述同样反过来,5的左孩子节点3,变为5的父节点,5由父节点变为3的右孩子。3原来的右孩子变为5的左孩子。
插入新节点
首先是在树中找到新节点正确的位置,寻找位置的过程与普通的二叉搜索树相同,只是将新插入的节点默认为红色节点。为什么默认为红色?因为如果你将新节点默认为黑色,则插入后肯定会打破原本符合规则的红黑树(上述第5条规则)。但是,如果你将新节点定为红色,则有可能不用任何操作就符合红黑树规则,如下图,当新插入的红色节点,它的父亲节点为黑色时候,此时已经满足红黑树规则了。所以用红色比黑色好。
如果很不巧,新插入的节点的父亲节点也为红色,因为红色节点不能连续,所以我们需要调整红黑树的结构使其满足规则。在调整的过程中我们会遇到3种需要处理的情况,我们来一一进行说明。
情况1:
插入新节点40,此时它的父节点为红色,并且它的叔叔节点(即51)也为红色。此时我们需要进行变色操作。将该节点的父亲节点、叔叔节点都变为黑色,祖父节点变为红色。
此时上图已经满足红黑树的规则。但有的时候我们经过了变色操作后,仍不满足红黑树的规则,会遇到下面的情况。
情况2:
如图,我们插入新的节点53,在按情况1的操作变色后,变成了这样:
此时,49与44为两个连续的红色,显然不符合规则。此时的操作为:我们将49看做当前节点,将当前节点的父节点44变为黑色,祖父节点35置为红色,以祖父节点为支点进行左旋。
此时情况2就处理完成了。
最后我们说一下情况3的情景,如下图:
我们向树中插入新节点37,在按情况1的操作变色后,变成了这样:
情况3:
此时,49与44为两个连续的红色,显然不符合规则。而我们只需以49为支点,进行一次右旋,就变成了情况2。如下图。
再按情况2进行一次操作就符合规则了。
3种情况我们说明完了,但是你可能还会有这样的疑问,什么时候进行左旋,什么时候进行右旋;什么时候以父节点为支点旋转,什么时候又以祖父节点为支点旋转?
那么我们可以总结一下,当遇到连续的红色节点应该怎么办:当前节点我们叫它X,如果X相对于父节点的左右位置和父节点相对于祖父节点的左右位置相同,此时,就以祖父节点为支点,进行反向旋转。例如:X为父节点的左孩子,X的父节点同样也是其祖父节点的左孩子,此时以祖父节点为支点进行右旋;
如果X相对于父节点的左右位置和父节点相对于祖父节点的左右位置不同,则以X的父节点为支点,进行旋转,旋转方向与X相对于父节点左右位置相反。例如:X为其父节点的左孩子,X的父节点为祖父节点的右孩子,此时以X的父节点为支点进行一次右旋。
删除节点
在红黑树中删除节点,肯定要涉及到要删的这个节点是红色的还是黑色的。删除红色比较简单,我们先说一下删除红色节点。
删除节点要考虑这个节点所处的位置,所以我们罗列一下红色的节点所有可能的位置情况。
- 它是一个叶子节点。
- 它既有左子树也有右子树。
你可能会发现为什么少了一种情况?它不能只有左子树或者只有右子树吗?我们可以看下图:
很明显,这四种情况都不符合红黑树的规则,所以根本不会出现这种情况。
而对于既有左子树也有右子树的情况。我们可以先和普通的二叉搜索树的删除操作一样,将它与前驱或者后继交换一下。它就又变成第一种情况——成为了一个叶子节点。所以我们只需考虑当它是叶子节点的情况。
很简单,直接删除红色叶子节点。
接下来我们看一下当要删除的节点是黑色的时候应该怎么办。
同样我们列一下节点位置可能的情况:
- 它是一个叶子节点。
- 它只有左子树,或只有右子树。
- 它既有左子树也有右子树。
第三种情况和删除红色节点时的处理方法一样,可以转换成第一种或第二种情况,所以我们只关心前两种情况。
当要删除的黑色节点只有一个子树时:
它的左孩子或者右孩子一定是红色的,因为如果是黑色的就不符合红黑树的规则了。
操作方法为:我们只需要将它的子节点变黑,然后代替它的位置就完成了。
最后我们看一下最难处理的一种情况。
要删除的黑色节点是叶子节点时:
情况1:待删除黑色节点20,它的兄弟节点为红色。
此时的操作为,将兄弟节点和父节点颜色交换,即父亲变红,兄弟变黑。然后以父节点为支点进行左旋。(旋转方向同样是与待删除节点的左右位置相同)
此时会变成了下述的情况4,再按情况4进行操作就可以了。
情况2:待删除黑色节点20,它的兄弟节点为黑色,并且它拥有红色的远侄子节点,近侄子节点有没有都可以。(侄子节点即兄弟节点的子节点,远侄子节点就是,当前节点如果是其父节点的左孩子,那么它的远侄子节点就是兄弟节点的右孩子,近侄子同理)
操作方法为:将远侄子节点变黑,兄弟节点与父亲节点互换颜色,最后以父节点为支点进行左旋。(为什么是左旋?因为待删除的20是左孩子,我们要将左子树长度拉长,将它沉下来,使它变成多余的节点好删除它,如果它是右孩子,则进行右旋)
操作后如下图就完成了。
情况3:待删除黑色节点20,它的兄弟节点为黑色,但它没有红色的远侄子节点(即nil点,记住,nil点算黑色),只有红色的近侄子节点。
操作方法为:将兄弟节点与近侄子节点交换颜色,再以兄弟节点进行右旋。(旋转方向很好记,因为此处旋转的目的是为了创造远侄子,近侄子节点是左节点,所以就只能右旋了,如果近侄子是右节点则进行左旋。)
操作后如下图:
此时有了红色的远侄子,就满足了情况2,再按情况2进行一次操作就完成了。
情况4:待删除黑色节点20,它的兄弟节点为黑色,远侄子、近侄子节点都没有。(即两个nil节点,nil节点算黑色)
操作方法为:将兄弟节点变为红色,同时最关键的是,将当前节点20的父节点50,看做当前节点,继续递归的进行这四种情况的判断,直到当前节点为红色,或者当前节点是根节点才停止。最后将当前节点变为黑色!!
我们将上图红黑树按流程演示一下:
第一步按情况4操作,将55变红。并将父节点50看做当前节点,继续操作。
此时当前节点61为红色,满足停止递归条件,将61变为黑色,停止。整个操作完成。
此时有关红黑树的知识就说完了。
以上所有内容都为自己查阅资料学习理解之后手敲的。尽量得采用通俗易懂的描述和解释让读者更明白。27张图都是自己亲自画的,花费了四天才写完,如果觉得写的还可以,麻烦点亮喜欢支持一下,如果还是不懂,可以下方留言QQ等联系方式,我亲自告诉你。