上一篇我们已经很详细的介绍了红黑树的具体结构以及插入和删除操作了,那么红黑树在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;//将新结点的父节点和前置指