接上篇Java集合系列-HashMap 1.8(一)
3.5 红黑树
3.5.1 树形化操作
3.5.1.1 操作描述
参照源码
3.5.1.2 源码解析
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
//...
// 树形化准备
final void treeifyBin(Node[] tab, int hash) {
int n, index; Node e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
// 对于触发了树形化操作,但是桶容量还没达到64的情况下优先去做扩容处理,扩容也会分拆链表
resize();
// 定位要做树形下的桶位置,获取桶位元素e
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode hd = null, tl = null;
// 循环遍历链表中的元素,将其改造成为双向链表结构,表头元素为hd
do {
// 将e元素封装成为树节点TreeNode
TreeNode p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
// 执行树形化
hd.treeify(tab);
}
}
// 将Node节点封装成树节点
TreeNode replacementTreeNode(Node p, Node next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
static final class TreeNode extends LinkedHashMap.Entry {
//...
// 树形化操作
final void treeify(Node[] tab) {
TreeNode root = null;//代表根节点
// 此处循环将this赋值给x,this代表的是当前树节点,这个类是HashMap的内部类用于标识树节点,
// this就是当前类的实例,也就是一个树节点,但是是哪个树节点,就要依靠之间的代码上下文来判
// 断了,看看调用该方法的地方有这样的代码:hd.treeify(tab);这就表示当前节点就是那额hd节
// 点,而这个hd节点就是之前改造好的双向链表的表头结点
// 这里循环的是双向链表中的元素
for (TreeNode x = this, next; x != null; x = next) {
next = (TreeNode)x.next;
x.left = x.right = null;
if (root == null) {
// root == null的情况是链表头结点的时候才会出现,这时候将这个头结点作为树根节点
x.parent = null;//根节点无父节点
x.red = false;//黑色
root = x;//赋值
}
else {
// 这里只有非链表头节点才能进来
K k = x.key;
int h = x.hash;
Class> kc = null;
// 此处循环的是已构建的红黑树的节点,从根节点开始,遍历比较当前链表节点与当前红黑树节点的
// hash值,dir用于保存比较结果,如果当前链表节点小,则dir为-1,否则为1,实际情况却是,能
// 拨到同一个桶位的所有元素的hash值那是一样的呀,所以dir的值是无法依靠hash值比较得出结果
// 的,那么重点就靠最后一个条件判断来得出结果了,
for (TreeNode p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);// 最后需要依靠这个方法来决定dir的值
TreeNode xp = p;
// 根据dir的值来决定将当前链表节点保存到当前树节点的左边还是右边,
// 或者当前链表节点需要与当前树节点的左节点还是右节点接着比较
// 主要寻找子节点为null的情况,将节点保存到null位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
// dir<=0,将链表节点保存到当前树节点的左边子节点位置
xp.left = x;
else
// dir<=0,将链表节点保存到当前树节点的右边子节点位置
xp.right = x;
// 一旦添加的一个新节点,就要进行树平衡操作,以此保证红黑树结构
// 树的平衡操作依靠的就是其左右旋转操作
root = balanceInsertion(root, x);
break;
}
}
}
}
// 最后将组装好的树的根节点保存到桶下标位
moveRootToFront(tab, root);
}
static void moveRootToFront(Node[] tab, TreeNode root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) {
// 首先定位桶下标位
int index = (n - 1) & root.hash;
TreeNode first = (TreeNode)tab[index];
// 校验当前桶下标位的值是否为根节点的值,可能会存在不同的原因是树的平衡操作将原本的根节点挪移了
// 如果相同,那么不作任何处理,如果不同,就需要替换桶位元素为树根节点元素,然后改变双向链表结构
// 将root根节点作为双向链表表头元素,为何要替换呢,因为在判断桶位元素类型时会对链表进行遍历,如
// 果桶位置放的不是链表头或者尾元素,遍历将变得非常麻烦
if (root != first) {
Node rn;
tab[index] = root;
TreeNode rp = root.prev;
if ((rn = root.next) != null)
((TreeNode)rn).prev = rp;
if (rp != null)
rp.next = rn;
if (first != null)
first.prev = root;
root.next = first;
root.prev = null;
}
// 校验链表和树的结构
assert checkInvariants(root);
}
}
//...
}
//...
}
3.5.2 红黑树分拆操作
3.5.2.1 操作描述
很简单,看源码
3.5.2.2 源码解析
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
//...
static final class TreeNode extends LinkedHashMap.Entry {
//...
// 将一颗大树分拆为两颗小树,如果树太小,退化为单向链表
final void split(HashMap map, Node[] tab, int index, int bit) {
// this代表当前节点,也就是树的根节点,桶位节点
// map代表当前集合
// tab代表新桶数组
// index代表当前节点的桶位下标
// bit为旧桶容量
TreeNode b = this;
// Relink into lo and hi lists, preserving order
TreeNode loHead = null, loTail = null;
TreeNode hiHead = null, hiTail = null;
int lc = 0, hc = 0;// lc表示低位树容量,hc表示高位树容量
for (TreeNode e = b, next; e != null; e = next) {
next = (TreeNode)e.next;
e.next = null;
// 分拆树节点的依据,结果为0的一组(低位组),结果不为0的一组(高位组)
if ((e.hash & bit) == 0) {
// 组装低位组双向链表
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
// 组装高位组双向链表
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
// 针对低位组进行树形化处理,如果该组元素数量少于6个则退化为单向链表
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
// 针对高位组进行树形化处理,如果该组元素少于6个则退化为单向链表
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
//...
}
//...
}
3.5.3 红黑树添加元素操作
3.5.3.1 操作描述
参照源码
3.5.3.2 源码解析
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
//...
static final class TreeNode extends LinkedHashMap.Entry {
//...
// 红黑树的添加元素,map为当前HashMap,tab为当前桶数组,h为新增元素的key的hash值,k为新增元素的key,v为新增元素的value
final TreeNode putTreeVal(HashMap map, Node[] tab,
int h, K k, V v) {
Class> kc = null;
boolean searched = false;
// 当前节点是已定位的桶位元素,其实就是树结构的根节点元素
TreeNode root = (parent != null) ? root() : this;
for (TreeNode p = root;;) {
// dir代表当前树节点与待添加节点的hash比较结果
// ph代表当前树节点的hash值
// pk代表当前树节点的key
// 由于一个桶位的所有元素hash值相等,所以最后得出结果需要依靠
int dir, ph; K pk;
if ((ph = p.hash) > h)
// 如果当前节点的hash值大,dir为-1
dir = -1;
else if (ph < h)
// 如果当前节点的hash值小,dir为1
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
// hash值相等的情况下,如果key也一样直接返回当前节点,返回去之后会执行value的替换操作
return p;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode 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))
// 这个找到的q也是与待添加元素key相同的元素,执行替换
return q;
}
// 最终需要依靠这个方法来得出dir值的结果
dir = tieBreakOrder(k, pk);
}
TreeNode xp = p;
// 根据dir的值来决定是当前节点的左侧还是右侧,如果该侧右子节点则继续循环寻找位置,否则直接将新元素添加到该侧子节点位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node xpn = xp.next;
TreeNode x = map.newTreeNode(h, k, v, xpn);//封装树节点
if (dir <= 0)
// dir<=0,将新节点添加到当前节点左侧
xp.left = x;
else
// 否则将新节点添加到当前节点右侧
xp.right = x;
// 设置新节点的链表位置,将其作为xp的下级节点
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
// 如果xp节点原本有下级节点xpn,则要将新节点插入到xp和xpn之间(指双向链表中)
((TreeNode)xpn).prev = x;
// 插入了新节点之后,要进行树平衡操作,平衡操作完成,将根节点设置为桶位节点
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
final TreeNode root() {
for (TreeNode r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
// 一般我们在HashMap中保存的键值对的类型都是不变的,这一般用泛型控制,
// 那么就意味着,两个元素的key的类型时一样的,所以才需要靠其hashCode来决定大小
// System.identityHashCode(parameter)是本地方法,用于获取和hashCode一样的结果,
// 这里的hashCode指的是默认的hashCode方法,与某些类重写的无关
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
//...
}
//...
}
3.5.4 红黑树添加元素平衡操作
3.5.4.1 操作描述
3.5.4.1.1 左旋操作描述
绕A节点左旋,等于将A的右子节点B甩上来替换自己的位置,而自己顺势下沉成为其左子节点,这时你会发现,B有三个子节点,明显结构不对,将B的原来的左子节点C转移到下沉的A上,成为其右子节点,旋转结束
其实,要保证左子树节点值小于其根节点,右子树节点值大于其根节点,那么在替换AB节点之后,C节点的值就出现了问题,只有将其挪到A节点右边才能继续保证上面的结构。
首先我们知道B节点为A的右节点,那么B>A,而C为B的左节点,则CA,因此:A
3.5.4.1.2 右旋操作描述
绕A几点右旋,等于将A的左子节点B甩上来替换自己的位置,而自己顺势下沉成为其右子节点,这是你会发现,B有三个子节点,明显结构不对,将B的原来的右子节点C转移到下沉的A上,成为其左子节点,旋转结束 新增节点全部初始化为红色节点,然后分以下几种情况: 红黑树的节点删除操作主要分为这么三种: 针对第一种情况,真的好简单,待删节点即为叶子节点,直接删除即可; 针对第二种情况,也不难,将那个子节点替换待删节点即可; 至于第三种情况,那就麻烦了,但通过变换,可以将其转化为第一种或者第二种情况:处理方式是,找到待删节点的右子树中的最左节点(或者左子树中的最右节点),将其与待删节点互换位置,然后就将情况转换为第一种或者第二种了。 针对第三种情况转换方法的解析:为什么要找到待删节点的右子树最左节点呢,因为红黑树是二叉搜索树,这个二叉搜索树中满足"左子节点<其父节点<其父节点的右子节点"的规则,那么找到的右子树的最左节点,就是整颗树中大于待删节点值的最小值节点了,为了保证二叉搜索树的搜索结构(也就是刚刚那个公式),我们只能找最接近待删节点值的节点值来接替它的位置,如此能保证二叉搜索的结构,但是可能会破坏红黑树的结构,因为如果待删节点为红色,而替换节点为黑色的话,那岂不是在待删节点分支多加了一个黑色节点嘛,还有其他各种情况,种种,需要进行删除节点后的树平衡操作来保证红黑树的结构完整。 下面重点说说删除后的平衡问题: 其实只要待删节点是黑色节点,一旦删除必然会导致分支中黑色节点缺一(红黑树不再平衡),具体情况又分为以下几种:(基础条件:待删节点p为黑色,其只有一个子节点x,操作在待删节点被删除之后,子节点替换其位置之后) 貌似有点难...大家要看进去思考才能理解,光看没用! HashMap中涉及到了数组操作,单向链表操作,双向链表操作,红黑树操作: 数组操作: 单向链表操作: 双向链表操作: 红黑树操作: 参考:
首先我们知道B为A的左子节点,所以BB,而C又位于A的左子树,则CC>B。要保证这个结果成立,那么再B替换A的位置之后,A下沉为B的右子节点,因为A>B,所以往右走,
这时C和A均位于B的右侧,比较二者发现C
3.5.4.1.3 添加平衡操作描述
3.5.4.2 源码解析
public class HashMap
3.5.5 红黑树删除元素操作
3.5.5.1 操作描述
解析:开始情况是x分支删除了一个黑色节点,即x分支缺少一个黑色几点,而x的兄弟节点xpr为红色节点,xp为黑色节点,我们将xp和xpr颜色互换,那么在xpr分支黑色节点数量是不变的(只是位置变了),然后我么以红色的xp为基准执行左旋,将黑色的xpr甩上去替换xp的位置,xp作为xpr的左子节点,那么x端分支便多出了xpr这个黑色节点来补足不够的数量,而兄弟分支黑色节点数量还是不变的。
解析:新的sr(即原来的xpr)的原因是因为右旋操作之前执行了颜色替换,兄弟节点右侧分支少了一个黑色节点,右旋之后变为黑色的sl补充了这个黑色节点,但是现在我们要用sl[新xpr]来替换xp节点[置为xp节点的颜色],那么右侧分支原本用来补充之前缺少的黑色节点又消失了,所以将已知的红色节点sr置为黑色来进行补充)3.5.5.2 源码解析
public class HashMap
四、总结
【Java入门提高篇】Day25 史上最详细的HashMap红黑树解析
红黑树(一)之 原理和算法详细介绍