前言
JDK1.8后的HashMap引入了红黑树,在学习HashMap源码之前,了解了红黑树原理,及其如何通过代码进行实现后,在整体的看HashMap的源码就会简单很多。
概述
红黑树的特性
- 根节点必须是黑色节点。
- 节点是红色或黑色。
- 所有叶子都是黑色。(叶子是Null节点)
- 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
- 从任一结点到其每个叶子的所有路径都包含相同数目的黑色节点。
展示叶子节点
在HashMap源码中红黑树也是需要满足以上条件的,当我们在插入时可能会不满足以上特性,这时候就需要进行位置的调整了,如变色、左旋、右旋等操作来保持红黑树的特性。HashMap中红黑树每次插入节点都是红色节点,如果是插入节点是黑色的,则不满足特性。
如:不满足特性5,不同路径的黑色节点数量不一致。
变色平衡
以上结构不符合红黑树特性,插入节点为红色,且父亲节点也为红色,这时需要调整树形结构。我们可以知道在插入红色节点35之前,红黑树肯定是平衡的(不平衡则会进行平衡调节),由于插入节点的父亲是红色,可以判断其爷爷节点50肯定是黑色的,这时候我们判断叔叔节点70是红色的,如果父亲节点跟叔叔节点都是红色的,此时对父亲节点和叔叔节点进行变色调整,由黑色调整为红色,爷爷节点需要从黑色变为空色,但由于爷爷节点是根节点,必须为黑色,所以在变红色后,需要判断是否根节点,如果是根节点则变为黑色,此时红黑S树就平衡了。如果爷爷节点不是根节点,此时就还是红色,有可能爷爷的父亲节点存在红色的情况,所以我们需要将爷爷节点作为插入节点,重复进行平衡操作,直接平衡。
右旋平衡
插入红色节点34时,由于其父亲也是红色,不满足红黑树特性。这时候由于其叔叔节点为空,非红色节点,所以不能直接通过变色解决平衡问题。由于插入节点34在父节点35的左边,父亲节点35在爷爷节点40左右,这时候我们可以以爷爷节点40作为旋转点进行右旋。
- 将父节点35的父亲设置为太爷爷节点50
- 判断爷爷节点40是太爷爷节点50的左边还是右边,如果是左边,则将太爷爷50的左边设置为父亲节点35,右边则设置右边为父亲节点35。
- 将父亲节点35的右边设置为爷爷节点
- 将爷爷节点40的父亲设置为父亲夜店35
- 将爷爷节点40的左边设置为父亲节点35的右边,图中由于父亲节点35的右边是一个NULL节点,所以爷爷节点40的左边也是一个NULL节点。
通过以上步骤,我们发现树形结构还是不平衡,根节点到每个叶子节点黑色数量不是一致的。所以我们还需要调整父亲节点35的颜色为黑色,爷爷节点40颜色为红色,这样达到平衡效果。
左旋平衡
上图中存在两个连续红色的子父节点,不满足红黑色特性,需要通过平衡处理。
- 判断其叔叔节点是否存在且为红色,如果不满足条件,则无法直接通过变色解决平衡问题。
- 判断子节点34在父节点35的左边还是右边,如果在右边,则以父亲节点35作为旋转节点进行左旋。
- 将插入节点38的父亲指向爷爷节点40。
- 将父亲节点35的父亲指向插入节点38。
- 将父亲节点35的右边指向插入节点38的左边节点,这里由于插入节点的左边节点为NULL,所以父亲节点35的右边节点也会为NULL。
- 判断父亲节点35位于爷爷节点的左边还是右边,并将对应的指向插入节点38。
由以上步骤,可以发现此时的树形结构跟我们右旋时所遇到的树形情况一样,这时候再将父亲节点35作为插入节点进行平衡,这里参考本文章的右旋平衡章节。
正文
通过红黑树的特性,我们了解其原理,现在看看HashMap中的平衡是如何进行代码实现的。
红黑树平衡方法:balanceInsertion
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
x.red = true;
//X为插入节点,将其颜色设置为红色
//xp为插入节点的父亲
//xpp为插入节点的爷爷
//xppl、xxpl为其叔叔节点
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
//如果插入节点的父亲为null,证明是其跟节点,此时设置为黑色即可
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
//如果父亲节点为黑色或者爷爷节点为空,此时证明,父亲节点为根节点。根节点又是黑色,所以插入一个红色节点不会破坏平衡
else if (!xp.red || (xpp = xp.parent) == null)
return root;
if (xp == (xppl = xpp.left)) {
//如果父亲节点是爷爷节点的左边时
//获取位于右边的叔叔节点,如果叔叔节点不为空且是红色节点。
if ((xppr = xpp.right) != null && xppr.red) {
//此时进行变色,将父亲跟叔叔节点变为黑色,爷爷节点变为黑色
xppr.red = false;
xp.red = false;
xpp.red = true;
//将爷爷节点设置为插入节点,因为爷爷节点变成了红色,可能会破坏平衡,所以需要重新走一边平衡流程
x = xpp;
}
else {
//进入这边条件,则证明其右边的叔叔节点为空
//判断插入节点是否是父节点的右边
if (x == xp.right) {
//这种情况跟本文“左旋平衡”中图示情况一致
//以父亲作为旋转节点进行左旋,左旋之后此时的树还不是平衡状态,此时还是存在两个连续的红色节点
root = rotateLeft(root, x = xp);
//重新定义父亲节点,并给爷爷节点赋值
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
//将父亲节点设置为黑色
xp.red = false;
if (xpp != null) {
//将爷爷节点设置为红色,并进行右旋,此时红黑树就平衡了。
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
}
else {
//进入这里,证明父亲节点是爷爷节点的右边。这里的操作跟上面条件里面是操作是反过来的。
//判断是否存在红色的叔叔节点
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);
}
}
}
}
}
}
rotateLeft方法,左旋见方法2详解
rotateRight方法,右旋见方法3详解
红黑树左旋方法:rotateLeft
上图中的旋转节点为:节点35
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> r, pp, rl;
//r为旋转节点的右边节点 简称:旋转右节点
//pp为旋转节点的父亲节点 简称:旋转父节点
//rl旋转右节点的左边节点 简称:旋右左节点
//判断旋转节点和旋转右节点不为null,并对旋转右节点进行赋值
if (p != null && (r = p.right) != null) {
//判断旋右左节点不为空,并将旋转节点的右边设置为旋右的左节点,并对旋右左节点rl进行赋值
if ((rl = p.right = r.left) != null)
//将旋右左节点的父亲设置为旋转节点
rl.parent = p;
//旋转右节点的父亲设置为其爷爷节点,并判断其爷爷节点是否为空
if ((pp = r.parent = p.parent) == null)
//为空则证明所处位置为根节点,将其设置为黑色
(root = r).red = false;
//判断旋转节点是在其父亲节点左边还是右边
else if (pp.left == p)
//将旋转节点父亲的左边设置为旋转右节点
pp.left = r;
else
//将旋转节点父亲的右边设置为旋转右节点
pp.right = r;
//将旋转右节点的左边设置为旋转节点
r.left = p;
//将旋转节点的父亲设置为旋转右节点
p.parent = r;
}
return root;
}
红黑树右旋方法:rotateRight
右旋跟左旋是对应反着来的。
上图中的旋转节点为:节点40
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> l, pp, lr;
//l为旋转节点的右边节点 简称:旋转左节点
//pp为旋转节点的父亲节点 简称:旋转父节点
//lr旋转右节点的右边节点 简称:旋左右节点
//判断旋转节点和旋转左节点不为null,并对旋转左节点进行赋值
if (p != null && (l = p.left) != null) {
//判断旋左左节点不为空,并将旋转节点的左边设置为旋左的右节点,并对旋左右节点lr进行赋值
if ((lr = p.left = l.right) != null)
//将旋左右节点的父亲设置为旋转节点
lr.parent = p;
//旋转左节点的父亲设置为其爷爷节点,并判断其爷爷节点是否为空
if ((pp = l.parent = p.parent) == null)
//为空则证明所处位置为根节点,将其设置为黑色
(root = l).red = false;
else if (pp.right == p)
//将旋转节点父亲的右边设置为旋转左节点
pp.right = l;
else
//将旋转节点父亲的右边设置为旋转右节点
pp.left = l;
//将旋转左节点的右边设置为旋转节点
l.right = p;
//将旋转节点的父亲设置为旋转左节点
p.parent = l;
}
return root;
}
红黑树添加方法:putTreeVal
上面讲解了红黑树的平衡操作,现在进入红黑树的添加节点方法中。
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
//获取其根节点
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) {
//p:根节点
//dir:-1代表左边节点方向 1代表右边节点方向
//hash:插入节点的key hash值
//key:插入节点的key值
//pk:当前所在节点的key值
int dir, ph; K pk;
//判断插入节点key的hash值与当前节点比较大小,确定走向
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
//判断当前节点的key是否相等,相等则查到了位置
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//有可能存入的key实现了比较器,所以这里会尝试获取比较器来重新计算下一节点树的方向
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
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);
}
TreeNode<K,V> xp = p;
//判断此时的方向是往哪边查找
if ((p = (dir <= 0) ? p.left : p.right) == null) {
//获取当前节点的下个节点值
Node<K,V> xpn = xp.next;
//如果不存在该相同的key信息,则创建新的节点,
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
//判断新插入节点放在左边还是右边
if (dir <= 0)
xp.left = x;
else
xp.right = x;
//当前节点的链表下个节点指向插入节点
xp.next = x;
//将插入节点的红黑树父节点,及其链表的上个节点指向当前节点。
x.parent = x.prev = xp;
//如果当前节点原先的链表下个节点不为空,则将其链表的上个节点指向新插入的节点。
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
//将ROOT节点的位置放到数组的第一个位置中
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
到最后这块的代码涉及了链表的赋值,有读者可能有疑问,明明是红黑树怎么又对链表进行操作了?
HashMap1.8引入了红黑树,当链表节点个人>=8个时,会转为红黑树。当节点个数<=6个时,会转为链表。所以我们在操作红黑树的插入操作时,需要记录节点的上个节点,及其下个节点的指向,以便后续转为链表。
我们可以看到红黑树节点都有prev属性,而TreeNode继承了Node,所以也有了next属性。
红黑树查询方法:find
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
//将红黑树所在的当前节点赋值给p
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
//比较当前节点的hash值大小,决定查找方向
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
//能走到这里证明hash值是一样的,如果key值一样,则是查找到了
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//能走到这里证明hash值是一样的(hash冲突情况),如果左边节点为null,则赋值为右边,从右边进行查询,否则从左边查询
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
//走到这里,证明还没办法确定走向,所以尝试获取key是否实现Comparable,并重新计算出其方向
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
//递归,从右边节点进行查找,如果查不到则从左边进行查找
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
//找不到返回null
return null;
}
红黑树删除方法:removeTreeNode
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;
//获取当前下标中的第一个值,也就是跟节点
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
//将上个节点的next指向移除节点的下个节点
pred.next = succ;
//下个节点不为空,则将下个节点的prev 指向移除节点的prev节点。
if (succ != null)
succ.prev = pred;
if (first == null)
return;
//如果第一个节点的父节点有值,则获取当前红黑树的根节点
if (root.parent != null)
root = root.root();
//如果根节点不存在 ,节点树为0
//movable默认为true&&根节点的右边为空,节点数小于3
//根节点的左节点为空,节点数小于3
//左节点的左节点为空时,节点数小于等于6
if (root == null
|| (movable
&& (root.right == null
|| (rl = root.left) == null
|| rl.left == null))) {
//链表化,因为前面对链表节点完成了删除操作,故在这里完成之后直接返回,即可完成节点的删除
tab[index] = first.untreeify(map); // too small
return;
}
//p为删除节点
//pl为删除节点的左节点
//pr为删除节点的右节点
//replacement 为删除节点的替换节点
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
if (pl != null && pr != null) {
//这里的目的是获取删除节点的右边中最小值来替换当前被删除节点,这样才能保证树形特性。也可以查询删除节点的左边中的最大值节点来替换
TreeNode<K,V> s = pr, sl;
while ((sl = s.left) != null) // find successor
s = sl;
//获取待替换节点的颜色C,将待替换节点颜色与删除节点保持一致,将删除节点颜色替换为待替换节点颜色。这里就是将待替换节点与删除节点进行替换工作
boolean c = s.red; s.red = p.red; p.red = c; // swap colors
//获取待替换节点的右节点
TreeNode<K,V> sr = s.right;
//获取删除节点的父节点
TreeNode<K,V> pp = p.parent;
//如果待替换节点的父节点为删除节点的右边
if (s == pr) { // p was s's direct parent
//交换两个节点的位置,父节点变子节点,子节点变父节点
p.parent = s;
s.right = p;
}
else {
//这里整块逻辑就是将删除节点与待替换节点进行互换,但是删除节点的左边节点,其父亲节点还未改变
//获取待替换节点的父节点
TreeNode<K,V> sp = s.parent;
//将删除节点的父节点指向待替换节点的父节点,这里将删除节点的位置替换到待替换节点中
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
//将删除节点的右边节点,其父节点替换为待替换节点
if ((s.right = pr) != null)
pr.parent = s;
}
//将删除节点的左节点设置为空
p.left = null;
//将删除节点右边设置为待替换节点的右节点,并将替换节点的右节点,其父亲替换为删除节点
if ((p.right = sr) != null)
sr.parent = p;
//待替换节点的左节点设置为删除节点的左节点,并且将该左节点的父节点设置为待替换节点
if ((s.left = pl) != null)
pl.parent = s;
//待替换节点的父节点设置为删除节点的父节点,如果为空则设置待替换节点为根节点,否则将删除节点的父节点,其对应的左节点或右节点进行替换。
if ((s.parent = pp) == null)
root = s;
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
//设置待移除的节点,这里主要判断删除节点替换位置后,是否是最后一个节点,如果是则后面将其删除,不是则移除节点设置为其右边的节点,后面再把移除的右边节点赋值给替换后的删除节点,达到删除的效果
if (sr != null)
replacement = sr;
else
replacement = p;
}
//走到这里证明删除节点的右边为Null,此时其左边节点作为替换节点,否则为将其右边节点作为替换节点
else if (pl != null)
replacement = pl;
else if (pr != null)
replacement = pr;
else
//到这里,证明删除节点没有子节点,所以替换节点设置为本身
replacement = p;
//由于替换节点是根删除节点相邻,所以将替换节点顶替其删除节点,这样删除节点就从树中被移除了,再将其属性都置为null
if (replacement != p) {
TreeNode<K,V> pp = replacement.parent = p.parent;
if (pp == null)
root = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null;
}
//如果删除节点是红色的,替换者肯定是黑色的,所以不需要进行平衡操作,否则需要进行平衡
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
//到这里证明删除节点处于最后一个节点时
if (replacement == p) { // detach
//将删除节点从树中移除
TreeNode<K,V> pp = p.parent;
p.parent = null;
if (pp != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
//保持根节点处于 所在数组index位置中的第一个节点
if (movable)
moveRootToFront(tab, r);
}
总结
为了保持红黑树的特性,在插入或者删除时,可能破坏其平衡结构,所以通过变色、左旋、右旋等方式来保持红黑树的平衡。
红黑树特性:
- 根节点必须是黑色节点。
- 结点是红色或黑色。
- 所有叶子都是黑色。(叶子是Null节点)
- 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
- 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点。