1. 导读
本次主要分享HashMap中红黑树插入后再平衡的过程, 主要内容如下:
.1 什么是红黑树;
.2 左旋与右旋;
.3 HashMap红黑树的再平衡过程;
2. 什么是红黑树
为后面内容做铺垫(如果对此部分内容熟悉可直接跳过), 先简单介绍下红黑树:
红黑树是一棵二叉树, 且是一棵自平衡二叉树, 到这里, 我们可以知道红黑树具有:
.1 除根节点外, 每个节点都有其父节点, 可以有两个子节点;
.2 比当前节点大是右节点, 小的是左节点;
除了上面的特性以外, 红黑树还有其自身的特性:
.1 节点是红色或黑色;
.2 根是黑色;
.3 所有叶子都是黑色(叶子是NIL节点;
.4 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点;)
.5 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点;
下面是一棵典型的红黑树:
那我们以上图为基准, 来看红黑树插入的情况:
.1 插入节点是12:
这种情况是比较理想的情况, 完全符合红黑树的特性, 就不需要再平衡;
我们来看下插入节点是7的情况:
.2 插入节点是7:
case 1: 7刚插入为红, 违反第四点特性;
case 2: 将父节点置黑, 违反第五点特性;
case 3: 将祖父节点置红, 然后绕祖父节点左旋, 红黑树即平衡;
.3 插入的节点是23:
case 1: 23刚插入, 违反特性4;
case 2: 绕22左旋, 22变成23的左子节点;
case 3: 将23置黑, 25置红, 绕25右旋, 红黑树重新平衡;
通过上面3个例子, 我们可以看到需要重新平衡红黑树的特征颜色:
case 1:
.1 父节点是红色, 那么把新节点置为黑色, 然后绕父节点左旋;
.2 这时因为祖父节点也是黑色的, 违反特性5, 我们还要做处理;
.3 我们把祖父节点置为红色, 然后绕祖父节点右旋, 红黑树重新平衡;
case 2:
case1有种特殊情况:存在叔叔节点;
.1 这时候只要把祖父节点置为红色, 父节点和叔叔节点置为黑色;
.2 用祖父节点再往上判断即可;
case 3:
这种情况就是case1的镜像, 那么操作也是按case1的镜像操作来即可:
.1 新节点置黑, 绕父节点右旋;
.2 祖父节点置红, 绕新节点左旋, 红黑树重新平衡;
3. 左旋与右旋
前面一直在用到左旋与右旋操作, 那么什么是左旋与右旋呢?
左旋是把父节点变成右子节点的左子节点的过程;
右旋是左旋的镜像操作: 父节点变成左子节点的右子节点的过程;
我们来看下HashMap.TreeNode::rotateLeft是如何处理的:
.1 要进行左旋的节点p必须要有右子节点r;
.2 如果r有左子节点rl, 那么rl要变为p的右子节点;
.3 如果p是根节点了, 那么r需要变为黑色, 这也是左旋过程中唯一的变色动作;
右旋其实是左旋的镜像动作, 这里就不做赘述了;
4. HashMap红黑树的再平衡
HashMap.TreeNode::balanceInsertion代码较长, 我们用流程图来说明:
HashMap.TreeNode::balanceInsertion的主要流程是:
.1 先把待插入节点x置为红色;
.2 遍历当前红黑树, 如果待插入节点已经是根节点了, x置黑, 直接返回;
.3 x的父节点xp是黑色节点, 直接返回;
.4 x的父节点是根节点, 直接返回;
.5 xp是xp的父节点xpp的左子节点, 且叔叔节点xppr存在, xppr为红色, 将xp和xppr置为黑色, xpp置为红色, 然后再次循环(循环时x变成了当前的xpp);
.6 xppr是黑色的, 且x是右节点, 绕xp左旋, 把xp变成x的左子节点;
.7 x经过上一步变化后变成了根节点(此时x已经变为黑色), 直接返回;
.8 x不是根节点, 先把x变黑, 再把xpp置红, 再以xpp进行右旋;
.9 x位于右子树的平衡过程是左子树的镜像过程, 请参考上面的5-8步;
可以看到红黑树的再平衡过程会变更根节点, 而HashMap的特性要求table[i]必须是二叉树的根节点, 那么HashMap是如何保证再次平衡过后的红黑树的root一定位于table[i]的呢?
5. HashMap.TreeNode::moveRootToFront
HashMap.TreeNode::moveRootToFront这个方法保证了平衡过后的红黑树的根节点一定位于首节点;
看示意图可得知, moveRootToFront()的作用就是将root摘出来, 将其放到首节点;
本期的分享就是这些了, 上面内容如有不足之处, 欢迎指正;
如果看了对你有帮助, 烦请点赞转发, 感谢;