前言
由于TreeMap的实现原理就是以红黑树为基础数据结构的,所以基本也是红黑树的原理解读。
红黑树
红黑树是一种自平衡的二叉查找树。是一种复杂但高效的数据结构,它可以在O(log n)时间内做查找,插入和删除。
红黑树的规定:
1.一个节点只能是红色或者黑色
2.根节点是黑色
3.每个叶节点(null节点/空节点)为黑色
4.如果一个节点为红色,则他们的2个子节点都为黑色
5.从任意节点到其每个叶节点
红黑树结构java代码示例:(TreeMap中的内部类Entry)
|
|
红黑树的平衡
在介绍红黑树的插入前,先弄清楚红黑树如何保持平衡。因为一般在对红黑树插入后,需要对红黑树做平衡化处理。
红黑树的左旋
红黑树的左旋操作如下图:
代码如下所示:(TreeMap.rotateLeft)
|
|
红黑树的右旋
红黑树的右旋操作如下图:
代码如下所示:(TreeMap.rotateRight)
|
|
红黑树插入
首先红黑树是一棵二叉查找树,所以新增一个节点,首先根据二叉树的性质找到相应的节点位置。然后根据红黑树的特点进行调整和平衡。
红黑树新增节点需要注意以下三点:
1.新增节点默认为红色。
2.如果新增的节点的父节点为黑色,那么能维持红黑树的性质。
3.如果新增的节点的父节点为红色,那么会破坏红黑树的性质。需要通过重新着色、旋转等手段来维持红黑树的性质。
红黑树的节点新增有以下5种情况:
(以下约定,新增节点为N,父节点为P,叔父节点为U,祖父节点为G)
1.新增节点没有父节点,即为跟节点,直接设置为黑色。
2.新增节点的父节点为黑色,则直接插入。
3.新增节点(N)的父节点(P)和叔父节点(U)都为红色->将父节点(P)和叔父节点(U)设置为黑,祖父节点(G)设置为红。这时候,由于经由父节点(P)和叔父节点(U)的路径必经过祖父节点(G),所以这些路径上的黑色节点数目还是相同的。但是祖父节点(G)变为红色之后,祖父节点(G)的父节点可能也是红色,这时候就要将祖父节点(G)当做新增节点递归处理。如下图所示:
4.新增节点(N)的父节点(P)为红色,父节点(P)为祖父节点(G)的左子节点,叔父节点(U)都为黑色或者缺少,且新增节点(N)为父节点(P)的右子节点->将节点N,P左旋(如下图所所示)。这里注意,如果父节点(P)为祖父节点(G)的右子节点时是要进行右旋。然后产生的结果其实还没有完成,以为违反了规则4,将P节点作为新增节点进行情况5的操作。
5.新增节点(N)的父节点(P)为红色,父节点(P)为祖父节点(G)的左子节点,叔父节点(U)都为黑色或者缺少,且新增节点(N)为父节点(P)的左子节点->将祖父节(G)与父节点(P)进行右旋(如果父节点(P)为祖父节点(G)的右子节点,进行左旋),然而还没有完成,因为节点P,N都为红色,违反了规则4。将P,G的颜色进行交换。如下图所示:
TreeMap中元素的插入
TreeMap的put()
方法其实只是找到新增节点插入的位置,而插入之后的红黑树平黑调整调用了fixAfterInsertion()
方法进行。put()
方法代码如下:
|
|
再来看看fixAfterInsertion()
如何进行红黑树的平衡的,代码如下:
|
|
红黑树的删除
相比较红黑树的插入,红黑树的删除更加复杂。删除一个节点,一般要寻找一个真正的删除点,替代删除点,然后根据删除点做红黑树的平衡。
如何寻找到真正的删除点呢?其实就是寻找待删除点的中序遍历(LDR)的前继节点或者后继节点。即待删除点的最左父树的右父节点或者右子树的最左节点。
所以可以推断出,真正的删除点必定是一个只有一个孩子或者没有孩子的节点,而根据红黑树的性质,可以得出以下2个结论:
- 真正的删除点必定只有一个红色孩子节点或者没有孩子节点
- 如果真正的删除点是一个红色节点,那它必定是个叶子节点
所以,红黑树的删除步骤如下:
1.寻找真正的删除点,将真正删除点的元素赋值为待删除点
2.删除真正的删除点,如果删除点有子节点,以子节点代替其位置
3.以删除点开始判定红黑树的平衡性质
4.如有必要做相应的平衡操作
以下开始讨论红黑树删除的几种情况。我们约定,真正的删除点使用“旧”标记,旧点所在位置将被他的子节点取代(最多只会有一个子节点),我们使用“新”标记旧点的孩子节点。删除操作会有以下集中情况:
1.旧点为红色节点
若旧点为红色节点,则它必定为叶子节点,直接删除即可。
2.一黑一红
当旧点为黑色结点,新点为红色结点时,将新点取代旧点位置后,将新点染成黑色即可(如下图所示)。这里需要注意:旧点为红色,新点为黑色的情况不可能存在。
3.双黑
当旧点和新点都为黑色是(新点为空节点也属于这种情况),情况比较复杂,需要根据旧点兄弟结点的颜色来决定进行什么样的操作。我们使用“兄”来表示旧点的兄弟结点。
3.1 红兄
由于兄弟结点为红色,所以父结点必定为黑色,而旧点被删除后,新点取代了它的位置。下图演示了两种可能的情况:
红兄的情况需要进行RR或LL型旋转,然后将父结点染成红色,兄结点染成黑色。然后重新以新点为判定点进行平衡操作。我们可以观察到,旋转操作完成后,判定点没有向上回溯,而是降低了一层,此时变成了黑兄的情况(可能会是3.2.1、3.2.2、3.2.3)。
3.2 黑兄
黑兄的情况最为复杂,需要根据黑兄孩子结点(这里用“侄”表示)和父亲结点的颜色来决定做什么样的操作。
3.2.1 黑兄二黑侄
这种情况比较简单,只需将兄结点变为红色即可,然后根据父节点继续平衡。(其实这时候如果父节点为红色,将父节点设置为黑色,删除操作就结束了)如下图所示:
3.2.2 黑兄右黑侄
黑兄,左侄红色,右侄黑色这种情况需要区分新点是起父节点的左子节点还是右子节点。
3.2.2-1 新点是其父的左子节点
将左侄设置为黑,兄节点设置为红色,然后以兄节点右旋(将情况转为了3.2.3-1)。如下图所示:
3.2.2-2 新点为其父节点的右子节点
将兄节点的颜色设置为父节点颜色,父节点和左侄节点设置为黑色,然后根据父节点右旋。删除操作结束。如下图所示:
3.2.3 黑兄左黑侄
黑兄,左黑侄,右红侄的情况也是需要区分新点是起父节点的左子节点还是右子节点.
3.2.3-1 新点是左子节点
将兄节点的颜色设置为父节点的颜色,父节点和右侄节点设置为黑色,然后根据父节点左旋。删除结束。如下图所示:
3.2.3-2 新点是右子节点
将右侄设置为黑色,兄节点设置为红色,然后以兄节点左旋(将情况转为3.2.2-2)。如下图说示:
TreeMap删除代码
TreeMap.remove
代码如下:
|
|
remove其实只是根据key找出对应的节点。真正的删除在deleteEntry
方法中,代码如下所示:
|
|
在删除节点后,可能需要调用fixAfterDeletion
方法来平衡红黑树。fixAfterDeletion
方法代码如下所示:
|
|
后记
这篇笔记写了4天时间。参考了大量大牛的文章,遇到不太懂的时候就拿viso自己画图帮助理解。这次的学习过程收获良多,让我不得不再次感叹数据结构的精妙与算法的魅力。