上一篇我们已经很详细的介绍了红黑树的具体结构以及插入和删除操作了,那么红黑树在HashMap中是如何具体实现的呢?
首先我们来熟悉一下里面对左旋和右旋操作的实现,毕竟这两个操作在插入和删除里面是最基本的操作。
首先来看左旋:
p-->25,r-->35,pp->50
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root, TreeNode<K,V> p){
//左旋操作,左旋操作的具体内容是将p结点向左下方旋转,把p的右孩子r向左上方
//旋转,代替p结点,p结点则作为r的左孩子,r继承p结点的父结点,且r结点如果有
//左孩子,则将其拆下来作为p结点的右孩子
TreeNode<K,V> r, pp, rl;
//传入参数为p,此操作是以p结点为旋点向左旋转,r为p的右右孩子,pp为p结点
//的父结点,rl为r的左孩子
if(p != null && (r = p.right) != null){
//首先判断两个结点是否为空,如果为空就转不了了
if((rl = p.right = r.left) != null)
//再将r的左孩子作为p的右孩子,如果r没有左孩子,则不做操作
rl.parent = p;
//否则再将p设置为r的左孩子的父结点
if((pp = r.parent = p.parent) == null)
//再将p的父结点作为r的父结点,如果p没有父结点,则说明p为根结点
(root = r).red = false;
//则将r作为根节点且将其颜色染黑
else if(pp.left == p)
//否则判断p是左孩子还是右孩子
pp.left = r;
//如果是左孩子,则将r作为pp的左孩子
else
pp.right = r;
//否则将r作为pp的右孩子
r.left = p;
//再将p作为r的左孩子
p.parent = r;
//将r作为p的父结点
}
return root;
//调整完后返回根结点
}
注意传入的结点即左旋操作的旋点。
右旋操作,和左旋相反:
p-->25,l-->20,pp-->50
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root, TreeNode<K,V> p){
//右旋操作,与左旋操作相反,将p结点向右下方向右旋转,将p的左孩子l向右上方
//向右旋转,代替p结点,p结点作为l结点的右结点,l作p结点的父结点,且l结点
//如果有右孩子,则将其拆下来作为p结点的左孩子
TreeNode<K,V> l, pp, lr;
//l为p结点的左孩子,pp为p结点的父结点,lr为l的右孩子
if(p != null && (l = p.left) != null){
//如果p不为空且p的左孩子不为空,开始右旋
if((lr = p.left = l.right) != null)
//如果l的右孩子不为空,则将其作为p的左孩子
lr.parent = p;
//将p作为lr的父结点
if((pp = l.parent = p.parent) == null)
//将p的父结点作为l的父结点,如果为空
(root = l).red = false;
//则将l作为根节点,并且将其染为黑色
else if(pp.right == p)
//判断p为左孩子还是右孩子
pp.right = l;
//如果为右孩子,则将l作为pp的右孩子
else
pp.left = l;
//否则将l作为左孩子
l.right = p;
//将p设置为l的右孩子
p.parent = l;
//将l设置为p的父结点
}
return root;
//返回根结点
}
同样,传入的p结点也是右旋操作的旋点。
接下来介绍插入操作,在HashMap中红黑树的插入操作分为两部分实现,第一部分调用putTreeVal方法找到结点需要插入的位置并将结点插入,第二部分调用balanceInsertion方法将红黑树调整为平衡状态。
插入操作
首先介绍第一个方法,寻找插入点其实和在红黑树中查找某一个结点类似,只不过这里还要新设立一个指向标dir。而后通过循环来查找,每一次循环下来dir都要记录一个值,如果为-1,表示下一次循环从前一次循环的左孩子开始查找。如果为1,表示下一次循环从前一次循环的右孩子开始查找。然后从根节点开始寻找,取当前结点与传入结点的hash值进行比较,如果当前结点比传入结点小,则dir = 1,反之,则dir = -1。两者哈希值相等时,再比较两者的key值是否一样,如果一样则说明此结点已经在红黑树中了,所以返回红黑树中的结点,结束插入操作。如果不一样,则再比较谁的key值大,如果key值无法比较,则比较key值的内存地址的哈希值。最后根据dir进行下一轮循环,直到找到结点所要插入的位置。找到位置之后再判断应该插在父结点的左侧还是右侧,然后创建TreeNode进行插入,此时的插入就不止是红黑树的插入操作了,还有双向链表的插入,并且两个体系的插入没有交集。插入成功后调用balanceInsertion方法对红黑树进行调整,调整后返回根结点作为双向链表的头结点以及哈希表槽内的结点(moveRootToFront方法)。最后返回Null表示插入成功。
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab, int h, K k, V v){
//将结点插入到红黑树中,即先找到插入点,再创建新结点,插入进去,最后调整红黑树,将
//红黑树的根节点作为链表首节点并且放入哈希表槽内
Class<?> kc = null;//Class类,用作compareable比较器
boolean searched = false;//查询标记
TreeNode<K,V> root = (parent != null) ? root() : this;//首先找到根节点
for(TreeNode<K,V> p = root; ;){
//从根结点开始查找,找到适合插入的地方再进行插入操作
int dir, ph; K pk;
//dir为指示标,每一次循环根据此指示标进行向左深入还是向右深入
//ph为当前结点的哈希值,pk为当前结点的key值
if((ph = p.hash) > h)
//首先比较hash值,大则dir等于-1,指示下一次向左子树遍历,大于
//则dir等于1,指示下一次向右子树遍历
dir = -1;
else if(ph < h)
dir = 1;
else if((pk = p.key) == k || (k != null && k.equals(pk)))
//如果hash值相等,则比较key值,如果相等,说明红黑树中
//已有此节点,不能插入,返回当前结点
return p;
else if((kc == null) && (kc = compareClassFor(k)) == null || (dir = compareComparables(kc,k,pk)) == 0){
//否则即hash值相等,但是key值不等,故先比较key值,如果小于则dir等于-1,大于则dir等于1,相等则进入if语句
if(!searched){
//此处search表示先向下寻找有没有同样的结点,有则直接返回该结点,不用插入了,没有才继续进行
//而且每次插入操作只做一次此操作,这样的好处是防止向下寻找的时候忽然找到一个一样的结点,浪费资源。所以在第一次
//寻找的时候就先确定该结点是否不存在树中
TreeNode<K,V> q,ch;
searched = true;
if(((ch = p.left) != null && (q = ch.find(h,k,kc)) != null) || ((ch = p.right) != null && (q = ch.find(h,k,kc)) != null))
return q;
}
dir = tieBreakOrder(k,pk);//如果key值也一样,则比较内存地址中的hash值
}
TreeNode<K,V> xp = p;//先记录p结点
if((p = (dir <= 0) ? p.left : p.right) == null){
//根据dir选择进入左孩子还是右孩子,再判断是否为空,不为空继续循环,为空开始插入
Node<K,V> xpn = xp.next;//记录结点的下一个结点
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);//创建一个新的树结点
if(dir <= 0)//根据dir判断新结点应该插入左边还是右边
xp.left = x;
else
xp.right = x;
xp.next = x;//将xp结点的后置指针指向新节点x
x.parent = x.prev = xp;//将新结点的父节点和前置指针都设置为xp节点
if(xpn != null)//如果xp不是尾结点
((TreeNode<K,V>)xpn).prev = x;
//则设置xp的后一个结点的前置指针指向x新节点
moveRootToFront(tab,balanceInsertion(root,x));
//先调整,再将根结点作为链表的首节点并放入哈希槽中
return null;//插入成功,则返回null
}
}
}
插入调整
调整操作基本是按照我的上一篇博客那五种情况分别处理的,详情请移步(红黑树详解)。首先将新插入的结点染成红色,然后通过循环操作不断调整树的整体结构,从下向上调整,直到根节点返回根结点,结束调整操作。
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x){
//插入结点后对红黑树进行调整
x.red = true;
//先将插入结点染成红色
for(TreeNode<K,V> xp, xpp, xppl, xppr;;){
//开始进行循环向上调整,xp为x的父结点,xpp为x的祖父结点
//xppl为xpp的左孩子,xppr为xpp的右孩子
if((xp = x.parent) == null){
//如果x的父结点为空,则说明x结点是根结点
x.red = false;
//将x的颜色染黑
return x;
//返回根结点x
}else if(!xp.red || (xpp = xp.parent) == null)
//如果x的父结点为黑色或者x的祖父结点不存在,即x的父结点为根
return root;
//不需做任何处理,直接返回根
//上面是对x父结点为黑色进行讨论,下面则默认x的父结点为红色
if(xp == (xppl = xpp.left)){
//如果xp为xpp的左孩子
if((xppr = xpp.right) != null && xppr.red){
//如果xpp的右孩子不为空且是红色的
xppr.red = false;
//将xpp的右孩子染成黑色
xp.red = false;
//将xp染成黑色
xpp.red = true;
//将xpp染成红色
x = xpp;
//将x移动到xpp位置,向上调整
}else{
//否则,说明xpp的右孩子为黑色
if(x == xp.right){
//如果新插入的结点是右孩子
root = rotateLeft(root, x = xp);
//则以x的父结点为旋点进行左旋转
xpp = (xp = x.parent) == null ? null : xp.parent;
//旋转后,x位于原来的xp的位置,xp转到了原来的x的位置,
//因此需要重新调整,调整后,xp变为x的父结点,xpp还是原来的位置
//也就是说只有x和xp互换了位置,但是xp还是x的父结点
}
if(xp != null){
//无论第一次插在左孩子的位置上还是插在右孩子的位置上,
//都要进行进行右旋转,区别是如果插在左孩子位置上
//只要进行一次右旋转就行,而插在右孩子位置上,则先
//进行左旋转,再进行右旋转,因此只需要判断xp是否为空就够了
xp.red = false;
//先将xp染黑
if(xpp != null){
xpp.red = true;
//再将xpp染红
root = rotateRight(root,xpp);
//以xpp为旋点进行右旋转
}
}
}
}else {
//否则,xp为xpp的右孩子,步骤基本一致
if(xppl != null && xppl.red){
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}else{
if(x == xp.left){
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if(xp != null){
xp.red = false;
if(xpp != null){
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
删除操作
和插入类似,删除也包括两个部分,第一个部分是寻找删除的结点所在的位置,判断该结点是不是边缘结点(即孩子至多只有一个),如果不是边缘结点则寻找中序遍历中的下一个结点和该结点互换(真正意义上的置换,并不是只换key值或value值)。第二个部分是删除后的调节,使得整棵树保持平衡。
第一部分又分为几个阶段,首先判断能不能删除,如果哈希表内都是空的,则说明里面没有元素,删除不了,直接返回。否则开始在双向链表中寻找该结点,找到了的话在链表层面上将其删除,再看还有没有其他结点,如果没有,说明哈希表槽内就一个结点,直接返回。否则再判断剩余的结点有多少个,我们知道,当结点数量少于八个的时候,会拆树建表,因此也就不需要进行红黑树的调整了。而结点数量多余八个则老老实实调整树,这是第一个阶段。然后开始寻找该结点真正应该在的位置(可能就是原来的位置,也可能是别的位置),如果位置变换了,则将其与另外一个边缘结点互换并且获得该结点被删除后替代它位置的结点(可能是叶子结点也可能是它的孩子,当然孩子最多只有一个),这是第二结点。而第三阶段是删除结点,并且判断要不要进行删除调节,最后结束删除操作。
final void removeTreeNode(HashMap<K,V> map,Node<K,V>[] tab, boolean movable){
//删除一个结点,首先在链表中删除,然后在红黑树中删除,链表中删除很好删,主要是红黑树中如何删
//首先判断被删除的结点是否有左右孩子,没有直接删除就够了,有一个孩子也好办,用那个孩子来顶替
//它就够了,最难的是有两个孩子,此处给的处理方法是寻找中序遍历中的下一个结点,将要删除的结点
//和该结点替换,因为它的有两个孩子的话,那么它中序遍历的下一个结点最多有一个孩子,替换了之后
//就变成了上面几种情况之一了。
int n;
if(tab == null || (n = tab.length) == 0)
//首先判断哈希表是否为空,空则删除不了,直接返回
return;
int index = (n - 1) & hash;
//否则就先获得要删除结点在哈希表中的位置,因为是被删除结点引用
//的这个方法,所以hash就是该结点的hash值。
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl,;
//获取哈希表内的结点,此结点基本就是红黑树的根结点
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
//获取要删除结点的前置结点以及后置结点,用于在链表中删除
if(pred == null)
tab[index] = first = succ;
//如果前置结点为空,则说明要删除的结点是首节点,因此只要
//把其后置结点作为首节点放入哈希表内
else
pred.next = succ;//否则将其前置结点的后置指针指向其后置结点
if(succ != null)
succ.prev = pred;//如果后置结点不为空,则将其后置结点的前置指针指向其前置结点
//以上就完成了在链表层面上的删除
if(first == null)//如果哈希表内为空,说明该槽内已经没有元素了,直接返回
return;
if(root.parent != null)//如果哈希表内有元素,则先找到红黑树的根结点
root = root.root();
if(root == null || root.right == null || (rl = root.left) == null || rl.left == null){
//如果根结点为空或者根结点的左孩子为空或者根节点的右孩子为空或者根结点的左孩子的左孩子为空
//说白了就是所有结点的数量加起来不足八个,那就拆红黑树,变链表并返回
tab[index] = first.untreeify(map);
return;
}
//上面基本是在链表与红黑树之间进行操作,现在开始是在红黑树上面进行操作了
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
//p为要删除的结点,pl为要删除的结点的左孩子,pr为要删除的结点的右孩子,replacement为替代结点
//这里要明白一个概念:在链表中删除不代表在红黑树中删除,这是两个体系,因为每一个TreeNode都有
//五个指针,即指向父节点,指向左孩子,指向右孩子,指向前置结点,指向后置结点
if(pl != null && pr != null){
//如果要删除的结点的左孩子和右孩子都不为空,即为最难搞定的一种,开始找替换的结点
TreeNode<K,V> s = pr, sl;
//首先设置s为p的右孩子,sl为s的左孩子
while((sl = s.left) != null)
//首先s是p的右孩子,然后一直向s的左边往下寻找,一直到s的左孩子
//是空的时候取s节点。想象一下,这个s的值是大于p并且最接近p的那
//一个结点,也就是中序遍历的p的下一个结点
s = sl;
boolean c = s.red; s.red = p.red; p.red = c;
//将p结点与s的结点颜色互换
TreeNode<K,V> sr = s.right;
//取s结点的右孩子sr
TreeNode<K,V> pp = p.parent;
//取p结点的父节点pp
if(s == pr){
//如果s就是p的右孩子,也就是说s和p是直接相关联的
p.parent = s;
//那就将s作为p的父节点
s.right = p;
//将p作为s的右结点
}else {
//否则说明s与p是隔开的
TreeNode<K,V> sp = s.parent;
//取s的父节点sp
if((p.parent = sp) != null){
//将sp作为p的父节点
if(s == sp.left)
//如果s是sp的左结点
sp.left = p;
//则将p也设置为sp的左结点
else
sp.right = p;
//否则就将p设置为sp的右结点
}
if((s.right = pr) != null)
//将p的右结点设置为s的右结点
pr.parent = s;
//将s设置为pr的父结点
}
//以上就是把s和p的基本定位定好,如果s和p相邻,直接替换
//如果不相邻,则将s的父结点作为p的父结点,将p的右孩子结点
//作为s的右孩子结点,只做了两步,下面就是其它几步
p.left = null;
//先将p的左孩子设置为空,因为原s的左孩子必为空
if((p.right = sr) != null)
//将s的右孩子设置为p的右孩子,如果s没有右孩子就不做处理
sr.parent = p;
//将p设置为s右孩子的父结点
if((s.left = pl) != null)
//将p的左孩子设置为s的左孩子,如果p没有左孩子就不做处理
pl.parent = s;
//将s设置为p的左孩子的父结点
if((s.parent = pp) == null)
//将p的父结点设置为s的父结点,如果p的父结点为空,则说明p是根
root = s;
//则将s作为根结点
else if(p == pp.left)
//否则判断p是左孩子还是右孩子
pp.left = s;
//如果是左孩子,就将s作为左孩子
else
pp.right = s;
//否则就将s作为右孩子
//此上就将p和s完全替换完全了,接下来寻找替换p的结点
if(sr != null)
//如果s的右孩子不为空
replacement = sr;
//则将其作为替换结点
else
replacement = p;
//否则说明s既没有左孩子也没有右孩子,则将p自己作为替换元素
}
//此上是在要删除的结点左右孩子都有的情况下进行的操作,如果不符合
//则说明要删除的结点最多有一个孩子
else if(pl != null)
//如果p结点不是两个孩子都有,则判断它是否有左孩子
replacement = pl;
//如果有左孩子,则将其作为替换结点
else if(pr != null)
//否则判断它是否有右孩子
replacement = pr;
//如果有右孩子,则将其作为替换结点
else
replacement = p;
//否则说明p两个孩子都没有,则将其自己作为替换元素
if(replacement != p){
//先判断替换结点是否为它自己,如果不是
TreeNode<K,V> pp = replacement.parent = p.parent;
//取现在的p结点的父结点(注意是现在,如果p和s替换了,则相当于取s的原本的父结点)
//并将pp设置为替换结点的父结点
if(pp == null)
root = replacement;//如果父结点为空,则将替换结点作为根
else if(p == pp.left)//否则判断p是左孩子还是右孩子
pp.left = replacement;
//如果p是左孩子,则将替换结点作为左孩子
else
pp.right = replacement;
//否则将替换结点作为右孩子
p.left = p.right = p.parent = null;
//将p的左孩子指针、右孩子指针和父结点指针都置为空
}
TreeNode<K,V> r = p.red ? root : blanceDeletion(root,replacement);
//如果p为红色,则不需要调整,如果为黑色,则需要进行调整
if(replacement == p){
//如果p自己就是替换元素,说明p既没有左孩子也没有右孩子
TreeNode<K,V> pp = p.parent;
//取p的父结点
p.parent = null;
//将p的父指针置为空
if(pp != null){
//如果父结点不为空
if(p == pp.left)
//如果p是左孩子
pp.left = null;
//则将pp的左孩子指针置为空
else if(p == pp.right)
pp.right = null;
//否则将pp的右孩子指针置为空
}
}
if(movable)//如果是可调整的,则将根调整到链表的首节点,放入哈希表中
moveRootToFront(tab,r);
}
删除调节
删除调节部分的代码写的比较难懂,有的地方将很多种情况杂揉在一起进行处理了,需要对删除有着比较深的理解才能真正理的清(博主感觉自己也很吃力),其中传入的x结点有两种可能,一种是被删除结点本身,另外一种是替代被删除结点的它的子结点,倘若是子结点,那么一定是红色的,因为被删除结点只有一个孩子结点,如果是黑色结点那么一定导致以他为根的子树不平衡(第五条性质:从任何一个结点开始到它的叶子结点所有路径中所经过的黑色结点数量一定相等)。剩下的就只要牢记五种情况,慢慢分析了。其中五种情况分别为:
当被删除结点是左孩子时:
1.当删除的结点是树中的唯一结点或者该结点是红色结点的话,直接删除。
2.当删除的结点是黑色结点,则需要看其兄弟结点的情况,此时又分为两种情况。
1)当其兄弟结点是红色的话,以父结点为旋点向左旋转,将父结点染红,将兄弟结点染黑。
2)当其兄弟结点是黑色的话,再分三种情况讨论
a)当兄弟结点的孩子结点都是黑色的,则将兄弟结点染红,再将父结点染黑。
b)当兄弟结点的左孩子为红色的,则以兄弟结点为旋点进行右转,以父结点为旋点进行左旋,再将父结点染黑。
c)当兄弟结点的右孩子为红色的,则以父结点为旋点进行左旋,将兄弟结点染红,将父结点和兄弟结点的右孩子染黑。
static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root, TreeNode<K,V> x){
//删除之后对红黑树进行调整,这个x有两种可能,如果删除的结点没有孩子,那么这个x
//就是将要被删除的结点,但是还没有删除,如果删除的结点有孩子,那么这个孩子一定
//是红色的,因为它只能有一个孩子,如果是黑色的,那么树就不平衡了。所以当传入的
//是被删除结点的孩子的话,直接将它转成黑色就结束了。
for(TreeNode<K,V> xp, xpl, xpr;;){
//循环调整,xp是x的父结点,xpl是xp的左孩子,xpr是xp的右孩子
if(x == null || x == root)
//如果x是空值或者x是根,直接返回
return root;
else if((xp = x.parent) == null){
//如果xp是空值,说明x是根,将x染黑并返回
x.red = false;
return x;
}
else if(x.red){
//如果x是将要删除的结点,那么它一定不是红色,如果是红色
//那么一定是要删除的结点的孩子,而且只能是红色,将其染成黑色
//整棵树就平衡了。
x.red = false;
return root;
}
//如果第一轮到这里还没有结束,那么说明被删除的结点没有孩子,传进来的是他自己
else if((xpl = xp.left) == x){
//如果x是左孩子
if((xpr = xp.right) != null && xpr.red){
//首先判断x的兄弟结点,即xp的右孩子是否为红色,如果为红色,则为第二种情况
xpr.red = false;
//如果是则先将xpr染黑
xp.red = true;
//再将xp染红
root = rotateLeft(root,xp);
//以xp为旋点向左旋转
xpr = (xp = x.parent) == null ? null : xp.right;
//旋转完再恢复结构,令xp为x的父结点,xpr为xp的右孩子
}
//如果上面的if语句没有被处理,则说明xpr为黑色结点,xpr不会为空,如果为空则树开始就不平衡
if(xpr == null)
//如果xp的右结点为空,则直接将x移到xp位置,结束此次循环
//这是属于第二种情况,删除结点的兄弟结点是红色的,且其孩子
//都是黑色的(叶子结点都是黑色的)
x = xp;
//下一次循环开始时判断x为红色,转成黑色,这样这颗以原来xpr
//左转上去为根的左子树增加了一个黑色结点(xp转下来变成黑色)
//右子树没增加也没减少黑色结点(原来的xpr为红色转上去染黑)
//而且这颗树的整体黑色结点也没有变化
else{
//1.运行到此处,有一种情况是前面两个if语句都运行了,则说明被删除结
//点的兄弟结点是红色的,那么它的子结点只能是两个黑色结点,而进行左转后
//xp的右孩子则是原来的xpr的左孩子,也一定是黑色的且没有左右孩子.
//2.运行到此处,还有一种可能是前面两个if语句都没有运行,则说明被删除结点
//的兄弟结点是黑色的,那么开始来判断它的子结点是什么情况的了
TreeNode<K,V> sl = xpr.left, sr = xpr.right;
//sl为xpr的左孩子,sr为xpr的右孩子
if((sr == null || !sr.red) && (sl == null || !sr.red)){
//1.如果xpr没有左右孩子,则将xpr染成红色,那么xp为根节点的子树达到
//平衡了(左子树少一个黑色结点,为空,右子树只有一个红色结点)
//2.如果xpr的两个孩子都是黑色的,那么只需要将它本身染红,就可以达到
//子树的平衡了(左子树少了一个黑色结点,右子树一个黑色结点变成了红色)
//这是第三种情况
xpr.red = true;
x = xp;
//1.再将x移动到xp位置上,第二轮循环将其染黑,保证树的平衡,返回结束
//2.此时将x移动到xp位置,如果xp是红色的,那直接染黑就可以保证整棵树
//平衡,如果是黑色的,那继续向上调整。
}else{
//否则说明xpr的孩子中必有红色结点,运行到此时,说明是属于2这个情况
if(sr == null || !sr.red){
//先判断xpr的左孩子是否为红,如果右孩子为黑则左孩子必为红
//而左孩子为红说明是属于第四种情况
if(sl != null)
//如果sl不为空,则将sl先染黑
sl.red = false;
xpr.red = true;
//再将xpr染红
root = rotateRight(root, xpr);
//以xpr为旋点向右旋转
xpr = (xp = x.parent) == null ? null : xp.right;
//重新调整结构,使xp为x的父结点,xpr为xp的右孩子
}
//不管xpr的孩子中谁为红,都必须做以下操作,只是左孩子为红的话需要先
//进行右旋转再做以下操作
//如果上一个if语句处理了,那么说明为第四种情况
//如果上一个if语句没有处理,则说明为第五种情况
if(xpr != null){
//先判断xpr是否为空
xpr.red = (xp == null) ? false : xp.red;
//先将xpr设置成xp一样的颜色,防止整棵树的黑色结点有变化
if((sr = xpr.right) != null)
//如果xpr的右孩子不为空
sr.red = false;
//将sr染为黑色,代替xpr作为右子树的黑色结点
}
if(xp != null){
//如果xp不为空
xp.red = false;
//将xp染为黑色并向左下旋转,代替被删除的结点当黑色结点
root = rotateLeft(root, xp);
//以xp为旋点向左旋转
}
x = root;
//局部调整完毕,可以直接返回
}
}
}else{
//否则,说明x是xp的右孩子,镜像调整,基本类似
if(xpl != null && xpl.red){
xpl.red = false;
xp.red = true;
root = rotateRight(root,xp);
xpl = (xp = x.parent) == null ? null : xp.left;
}
if(xpl == null)
x = xp;
else{
TreeNode<K,V> sl = xpl.left, sr = xpl.right;
if((sl == null || !sl.red) && (sr == null || !sr.red)){
xpl.red = true;
x = xp;
}else{
if(sl == null || !sl.red){
if(sr != null)
sr.red = false;
xpl.red = true;
root = rotateLeft(root,xpl);
xpl = (xp = x.parent) == null ? null : xp.left;
}
if(xpl != null){
xpl.red = (xp == null) ? false : xp.red;
if((sl = xpl.left) != null)
sl.red = false;
}
if(xp != null){
xp.red = false;
root = rotateRight(root,xp);
}
x = root
}
}
}
}
}
其他操作
重置根结点:
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root){
//由于红黑树的性质,要求查找时必须从根节点开始,因此必须要保证根节点在
//哈希表槽内并且是链表的头结点
int n;
if(root != null && tab != null && (n = tab.length) > 0){
//先判断根节点是否为空并且哈希表是否为空,如果为空就没必要设置了
int index = (n - 1) & root.hash;//如果不为空,则先获取根结点在数组上的位置
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];//将数组槽内的结点取出
if(root != first){//如果根节点不是槽内结点
Node<K,V> rn;
tab[index] = root;//将根结点放入数组槽内
TreeNode<K,V> rp = root.prev;//获取根结点在双向链表中的前置结点
if((rn = root.next) != null)//如果根结点不是链表中的尾结点
((TreeNode<K,V>)rn).prev = rp;//则将根的后置结点的前置指针指向根结点的前置结点
if(rp != null)//如果根不是链表中的头结点
rp.next = rn;//则将根的前置结点的后置指针指向根的前置结点,这两步就是将根节点从链表中删除
if(first != null)//如果数组槽内的原结点不是空
first.prev = root;//则将原结点的前置指针指向根结点
root.next = first;//再将根结点的后置指针指向数组槽内的原结点
root.prev = null;//根节点的前置指针指向空
}
//上面的操作简单来说就是将红黑树中的根节点从双向链表中取出作为双向链表的头结点并放入数组槽内
assert checkInvariants(root);
//防御性编程,检查是否满足红黑树和双向链表的条件
}
}
从红黑树中查找结点:
final TreeNode<K,V> find(int h, Object k, class<?> kc){
//在红黑树中寻找某个结点
TreeNode<K,V> p = this;
//本方法应该是TreeNode节点自己调用的,所以this为调用此方法的结点
//将该结点作为根节点来寻找某一个符合条件的结点
do{
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
if((ph = p.hash) > h)//如果结点的hash值大于传进来的hash值,则向左子树查
p = pl;
else if(ph < h)//如果结点的hash值小于传进来的hash值,则向右子树查
p = pr;
else if((pk == p.key) == k || (k != null && k.equals(pk)))
//如果相等,则查看key值是否相等,相等则直接返回该结点
return p;
//以上就是判断hash值是否相等,当hash值相等之后,再来下面判断key是否相等
else if(pl == null)
//如果左孩子为空,结束此次循环,下一次循环进入右子树进行查找
p = pr;
else if(pr == null)
//如果右孩子为空,结束此次循环,下一次循环进入左子树进行查找
p = pl;
else if((kc != null || (kc = comparableClassFor(k)) != null) && (dir = compareComparables(kc,k,pk)) != 0)
//否则说明左右孩子都不为空,则使用compareComparables来比较左右结点的key值
p = (dir < 0) ? pl : pr;
//否则比较的值小于0,下一次循环进入左孩子,大于0,下一次循环进入右孩子
else if((q = pr.find(h, k, kc)) != null)
//如果上面的判断还是无法判断,则递归查找右孩子,找到了则返回
return q;
else
//如果右孩子递归也没有找到,则开始找左孩子
p = pl;
}while(p != null);
return null;//如果还是没有找到,则返回空值
}
将双向链表变为红黑树:
final void treeify(Node<K,V>[] tab){
//如果哈希表内的某个槽内的元素所构建的双向链表结点太多了,则会影响操作速度,因此就
//开始把双向链表改造为红黑树,便于操作,此方法就是将双向链表改造为红黑树+双向链表
//主要在插入结点的时候调用
TreeNode<K,V> root = null;//先设立一个根结点
for(TreeNode<K,V> x = this, next; x != null; x = next){//对双向链表进行遍历
next = (TreeNode<K,V>)x.next;//存储当前结点的下一个结点
x.left = x.right = null;//先将当前结点的左孩子和右孩子设置为空
if(root == null){//如果根为空,说明刚开始建树
x.parent = null;//将x节点的父节点设置为空,说明x节点为根结点
x.red = false;//x节点颜色设置为黑(符合红黑树的第二特征:根必须是黑色结点)
root = x;//将x节点设置为根结点
}else {
K k = x.key;//否则,说明根不是空,则相当于向红黑树中插入结点,获得当前结点的key值
int h = x.hash;//获得当前结点的hash值
Class<?> kc = null;//设置一个Class类,用于后面的比较器
for(TreeNode<K,V> p = root; ;){//对红黑树进行遍历
int dir, ph;//dir作为标记,指示结点向什么地方插入,ph为当前结点的哈希值
K pk = p.key;//获得当前结点的key值
if((ph = p.hash) > h)//新插入的结点和当前结点比较hash值,大的向右,小的向左
dir = -1;
else if(ph < h)
dir = 1;
else if((kc == null) && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc,k,pk)) == 0)
//如果hash值相等,则比较key值,如果key值也相等,则比较内存地址的hash值,此处两行代码实际上比较了两种情况
//注意if条件中的还有一个对dir中的比较
dir = tieBreakOrder(k,pk);
TreeNode<K,V> xp = p;//先将p存储到xp,方便后面插入
if((p = (dir <= 0) ? p.left : p.right) == null){
//如果dir小于等于0,则p移向p的左孩子,如果dir大于0,则p移向p的右孩子
//知道p等于空时,才进行插入操作
x.parent = xp;//此时p所指的位置即x所要插入的位置,xp所指的结点即p的父节点.因此将x的父节点设置为xp
if(dir <= 0)
xp.left = x;//如果dir小于等于0,则将x设置为xp的左孩子
else
xp.right = x;//如果dir大于0,则将x设置为xp的右孩子
root = balanceInsertion(root, x);//将插入结点后的红黑树调整,并返回根结点
break;//结束红黑树的循环,继续双向链表的循环
}
}
}
}
moveRootToFront(tab,root);
//链表循环结束,说明链表结点全都转换进入红黑树中,再将红黑树的根节点设置为链表首节点并且放入哈希表中
}
将红黑树拆为双向链表:
final Node<K,V> untreeify(HashMap<K,V> map){
//将红黑树插成链表,当数据量少的时候,就没必要建树,因为插入和删除也有一定的消耗,所以拆成链表
//主要在删除结点的时候调用,由于有红黑树的时候也是红黑树与双向链表并存的,所以拆树的时候也好拆
Node<K,V> hd = null, tl = null;//记录头结点与当前结点
for(Node<K,V> q = this; q != null; q = q.next){//遍历双向链表
Node<K,V> p = map.replacementNode(q,null);
//将每个结点从TreeNode转换为Node,也就是新建一个Node结点,将必要的信息放进去再返回新结点即可
if(tl == null)//如果当前结点为空,说明链表为空,设置头结点
hd = p;
else
tl.next = p;//否则当前结点的后置指针指向新节点
tl = p;//将新结点设置为当前结点
}
return hd;//返回头结点
}
其他方法:
final TreeNode<K,V> getTreeNode(int h, Object k){
//获得树中的结点,首先判断调用的是否为根结点,不是则找到根节点
//再调用find方法寻找
return ((parent != null) ? root() : this).find(h, k, null);
}
static int tieBreakOrder(Object a, Object b){
//当哈希值相等并且key值也相等,则比较两者的内存地址的哈希码
int d;
if(a == null || b == null || (d = a.getClass().getName().compareTo(b.getClass().getName())) == 0)
//如果两者都不为空且两者都是同一个类,则比较两者在内存地址的哈希码,identityHashCode方法即获得对象在内存地址的哈希码
d = (System.identityHashCode(a) <= System.identityHashCode(b) ? -1 : 1);
return d;
}
static <K,V> boolean checkInvariants(TreeNode<K,V> t){
//递归不变检查
TreeNode<K,V> tp = t.parent, tl = t.left, tr = t.right, tb = t.prev, tn = (TreeNode<K,V>)t.next;
//tp为t的父结点,tl为t的左孩子,tr为t的右孩子,tb为t的前置结点,tn为t的后置结点
if(tb != null && tb.next != t)
//检查t的前置结点的后置指针是否指向自己
return false;
if(tn != null && tn.prev != t)
//检查t的后置结点的前置指针是否指向自己
return false;
if(tp != null && t != tp.left && t != tp.right)
//检查t是否为自己父结点的左孩子或者右孩子
return false;
if(tl != null && (tl.parent != t || tl.hash > t.hash))
//检查t是否是自己左孩子的父结点且自己左孩子的hash值是否比自己小
return false;
if(tr != null && (tr.parent != t || tr.hash < t.hash))
//检查t是否是自己右孩子的父结点且自己右孩子的hash值是否比自己大
return false;
if(t.red && tl != null && tl.red && tr != null && tr.red)
//检查如果叶子节点是否都为黑色
return false;
if(tl != null && !checkInvariants(tl))
//向左下递归检查
return false;
if(tr != null && !checkInvariants(tr))
//向右下递归检查
return false;
return true;
}
至此就基本解析完了红黑树以及它的实现了。接下来就开始分析HashMap的主体源码了。
参考资料:
https://blog.csdn.net/qq_40803710/article/details/80342262
https://blog.csdn.net/skyxmstar/article/details/70140512
https://blog.csdn.net/v_JULY_v/article/details/6109153
https://blog.csdn.net/weixin_42340670/article/details/80555860
https://blog.csdn.net/weixin_42340670/article/details/80753826
https://blog.csdn.net/qq_41174684/article/details/89041973
https://baike.baidu.com/item/%E7%BA%A2%E9%BB%91%E6%A0%91/2413209?fr=aladdin