Java集合系列---红黑树(基于HashMap 超详细!!!)
1 平衡因子:
左右子树 高度之差
LL型 右旋
LR型 -->LL 右旋
RR -->左旋
RL -->RR 左旋
左旋:逆时针旋转红黑树的两个节点,使得父节点被自己的右孩子取代,而自己成为自己的左孩子。
右旋:顺时针旋转红黑树的两个节点,使得父节点被自己的左孩子取代,而自己成为自己的右孩子
左旋和右旋的代码
/*** 左旋*/static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,TreeNode<K,V> p) {//这里的p即上图的A节点,r指向右孩子即C,rl指向右孩子的左孩子即D,pp为p的父节点TreeNode<K,V> r, pp, rl;if (p != null && (r = p.right) != null) {if ((rl = p.right = r.left) != null)rl.parent = p;//将p的父节点的孩子节点指向rif ((pp = r.parent = p.parent) == null)(root = r).red = false;else if (pp.left == p)pp.left = r;elsepp.right = r;//将p置为r的左节点r.left = p;p.parent = r;}return root;}/*** 右旋*/static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,TreeNode<K,V> p) {//这里的p即上图的A节点,l指向左孩子即C,lr指向左孩子的右孩子即E,pp为p的父节点TreeNode<K,V> l, pp, lr;if (p != null && (l = p.left) != null) {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;elsepp.left = l;l.right = p;p.parent = l;}return root;}
2 红黑树的性质:
1)节点是黑色或者红色
2)根节点是黑色的
3)每个叶结点是黑色的
4)每个红色节点的两个子节点都是黑色的
5)从任一节点到其他每个叶子节点的所有路径都包含相同数目的黑色节点
只要考虑到每个节点都遵循左小右大就很容易判断出是左旋还是右旋
3 红黑树调整
a 插入的节点为红色
因为插入的节点如果为黑色,就可能违背红黑树特性的最后一条,所以每次插入的都是红色节点
b 具体情况
根据被插入节点的父节点的情况,可以将"当节点z被着色为红色节点,并插入二叉树"划分为三种情况来处理。
① 情况说明:被插入的节点是根节点。
处理方法:直接把此节点涂为黑色。
② 情况说明:被插入的节点的父节点是黑色。
处理方法:什么也不需要做。节点被插入后,仍然是红黑树。
③ 情况说明:被插入的节点的父节点是红色。
处理方法:那么,该情况与红黑树的“特性(5)”相冲突。这种情况下,被插入节点是一定存在非空祖父节点的;进一步的讲,被插入节点也一定存在叔叔节点(即使叔叔节点为空,我们也视之为存在,空节点本身就是黑色节点)。理解这点之后,我们依据"叔叔节点的情况",将这种情况进一步划分为3种情况(Case)。
现象说明 | 处理策略 | |
---|---|---|
Case 1 | 当前节点的父节点是红色,且当前节点的祖父节点的另一个子节点(叔叔节点)也是红色 | (01) 将“父节点”设为黑色。(02) 将“叔叔节点”设为黑色。(03) 将“祖父节点”设为“红色”。(04) 将“祖父节点”设为“当前节点”(红色节点);即,之后继续对“当前节点”进行操作。 |
Case 2 | 当前节点的父节点是红色,叔叔节点是黑色,且当前节点是其父节点的右孩子 | (01) 将“父节点”作为“新的当前节点”。(02) 以“新的当前节点”为支点进行左旋。 |
Case 3 | 当前节点的父节点是红色,叔叔节点是黑色,且当前节点是其父节点的左孩子 | (01) 将“父节点”设为“黑色”。(02) 将“祖父节点”设为“红色”。(03) 以“祖父节点”为支点进行右旋 |
c 删除
第一步:将红黑树当作一颗二叉查找树,将节点删除。
这和"删除常规二叉查找树中删除节点的方法是一样的"。分3种情况:
① 被删除节点没有儿子,即为叶节点。那么,直接将该节点删除就OK了。
② 被删除节点只有一个儿子。那么,直接删除该节点,并用该节点的唯一子节点顶替它的位置。
③ 被删除节点有两个儿子。那么,先找出它的后继节点;然后把“它的后继节点的内容”复制给“该节点的内容”;之后,删除“它的后继节点”。在这里,后继节点相当于替身,在将后继节点的内容复制给"被删除节点"之后,再将后继节点删除。这样就巧妙的将问题转换为"删除后继节点"的情况了,下面就考虑后继节点。 在"被删除节点"有两个非空子节点的情况下,它的后继节点不可能是双子非空。既然"的后继节点"不可能双子都非空,就意味着"该节点的后继节点"要么没有儿子,要么只有一个儿子。若没有儿子,则按"情况① "进行处理;若只有一个儿子,则按"情况② "进行处理。
后继节点就是删除节点比它大的最小子节点
第二步:通过"旋转和重新着色"等一系列来修正该树,使之重新成为一棵红黑树。
因为"第一步"中删除节点之后,可能会违背红黑树的特性。所以需要通过"旋转和重新着色"来修正该树,使之重新成为一棵红黑树。
源码详解
1)TreeNode
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent; // red-black tree links 父节点TreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev; // needed to unlink next upon deletion 和next都是用来构建双向链表boolean red;TreeNode(int hash, K key, V val, Node<K,V> next) {super(hash, key, val, next);}
treeifyBin将普通链表转成为由 TreeNode 型节点组成的链表,并在最后调用 treeify 是将该链表转为红黑树
树化要满足两个条件:
1.链表长度大于等于 TREEIFY_THRESHOLD 8
2.桶数组容量大于等于 MIN_TREEIFY_CAPACITY 64
当桶数组比较小时,键值对节点hash的碰撞率会比较高,进而导致链表长度较长。这个时候应该扩容,而不是树化,而不是立马树化。毕竟高碰撞率是因为桶数组容量较小引起的,这个是主因。同时,扩容时需要拆分红黑树并重新映射,耗费时间。
final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)resize();//符合树化条件else if ((e = tab[index = (n - 1) & hash]) != null) {TreeNode<K,V> hd = null, tl = null;// hd 为头节点(head),tl 为尾节点(tail)do {//把普通节点转化为树节点TreeNode<K,V> 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);}}
将链表转化为红黑树
final void treeify(Node<K, V>[] tab){ //树的根节点TreeNode<K, V> root = null;// 以for循环的方式遍历刚才我们创建的链表。x是当前节点,next是后继节点for (TreeNode<K, V> x = this, next; x != null; x = next){// next向前推进。next = (TreeNode<K, V>) x.next;x.left = x.right = null;// 为树根节点赋值。if (root == null){x.parent = null;x.red = false;root = x;} else{K k = x.key;int h = x.hash;Class<?> kc = null;// 此时红黑树已经有了根节点,上面获取了当前加入红黑树的项的key和hash值进入核心循环。// 这里从root开始,是以一个自顶向下的方式遍历添加。// for循环没有控制条件,由代码内break跳出循环。for (TreeNode<K, V> p = root;;){//p指向遍历中的当前节点,x为待插入节点,k是x的key,h是x的hash值,ph是p的hash值,dir用来指示x节点与p的比较,-1表示比p小,1表示比p大,不存在相等情况,因为HashMap中是不存在两个key完全一致的情况。int dir, ph;K pk = p.key;if ((ph = p.hash) > h)dir = -1;else if (ph < h)dir = 1;//如果hash值相等,那么判断k是否实现了comparable接口,如果实现了comparable接口就使用compareTo进行进行比较,如果仍旧相等或者没有实现comparable接口,则在tieBreakOrder中比较else if ((kc == null && (kc = comparableClassFor(k)) == null)|| (dir = compareComparables(kc, k, pk)) == 0)dir = tieBreakOrder(k, pk);// xp:x parent。TreeNode<K, V> xp = p;// 找到符合x添加条件的节点。if ((p = (dir <= 0) ? p.left : p.right) == null){x.parent = xp;// 如果xp的hash值大于x的hash值,将x添加在xp的左边。if (dir <= 0)xp.left = x;// 反之添加在xp的右边。elsexp.right = x;// 维护添加后红黑树的红黑结构。root = balanceInsertion(root, x);// 跳出循环当前链表中的项成功的添加到了红黑树中。break;}}}}// Ensures that the given root is the first node of its bin,自己翻译一下。moveRootToFront(tab, 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;//first指向链表第一个节点TreeNode<K,V> first = (TreeNode<K,V>)tab[index];if (root != first) {//如果root不是第一个节点,则将root放到第一个首节点位置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;}//这里是防御性编程,校验更改后的结构是否满足红黑树和双链表的特性//因为HashMap并没有做并发安全处理,可能在并发场景中意外破坏了结构assert checkInvariants(root);}}
调整树的结构
static <K, V> TreeNode<K, V> balanceInsertion(TreeNode<K, V> root, TreeNode<K, V> x){// 正如开头所说,新加入树节点默认都是红色的,不会破坏树的结构。x.red = true;// xp:x parent,代表x的父节点。// xpp:x parent parent,代表x的祖父节点// xppl:x parent parent left,代表x的祖父的左节点。// xppr:x parent parent right,代表x的祖父的右节点。for (TreeNode<K, V> xp, xpp, xppl, xppr;;){// 如果x的父节点为null说明只有一个节点,该节点为根节点,根节点为黑色,red = false。if ((xp = x.parent) == null){x.red = false;return x;} // 进入else说明不是根节点。// 如果父节点是黑色,红色的x节点可以直接添加到黑色节点后面,返回根就行了不需要任何多余的操作。// 如果父节点是红色的,但祖父节点为空的话也可以直接返回根此时父节点就是根节点,因为根必须是黑色 的,添加在后面没有任何问题。else if (!xp.red || (xpp = xp.parent) == null)return root;// 一旦我们进入到这里就说明了两件是情// 1.x的父节点xp是红色的,这样就遇到两个红色节点相连的问题,所以必须经过旋转变换。// 2.x的祖父节点xpp不为空。// 判断如果父节点是否是祖父节点的左节点if (xp == (xppl = xpp.left)){// 父节点xp是祖父的左节点xppr// 判断祖父节点的右节点不为空并且是否是红色的// 此时xpp的左右节点都是红的,所以直接进行上面所说的第三种变换,将两个子节点变成黑色,将xpp变成红色,然后将红色节点x顺利的添加到了xp的后面。// 这里大家有疑问为什么将x = xpp?// 这是由于将xpp变成红色以后可能与xpp的父节点发生两个相连红色节点的冲突,这就又构成了第二种旋转变换,所以必须从底向上的进行变换,直到根。// 所以令x = xpp,然后进行下下一层循环,接着往上走。if ((xppr = xpp.right) != null && xppr.red){xppr.red = false;xp.red = false;xpp.red = true;x = xpp;}// 进入到这个else里面说明。// 父节点xp是祖父的左节点xppr。// 祖父节点xpp的右节点xppr是黑色节点或者为空,默认规定空节点也是黑色的。// 下面要判断x是xp的左节点还是右节点。else{// x是xp的右节点,此时的结构是:xpp左->xp右->x。这明显是第二中变换需要进行两次旋转,这里先进行一次旋转。// 下面是第一次旋转。if (x == xp.right){root = rotateLeft(root, x = xp);xpp = (xp = x.parent) == null ? null : xp.parent;}// 针对本身就是xpp左->xp左->x的结构或者由于上面的旋转造成的这种结构进行一次旋转。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);}}}}}}
将红黑树转化为链表
HashMap 通过两个额外的引用 next 和 prev 保留了原链表的节点顺序。这样再对红黑树进行重新映射时,完全可以按照映射链表的方式进行。这样就避免了将红黑树转成链表后再进行映射,无形中提高了效率。
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {TreeNode<K,V> b = this;// Relink into lo and hi lists, preserving orderTreeNode<K,V> loHead = null, loTail = null;TreeNode<K,V> hiHead = null, hiTail = null;int lc = 0, hc = 0;/* * 红黑树节点仍然保留了 next 引用,故仍可以按链表方式遍历红黑树。* 下面的循环是对红黑树节点进行分组,与上面类似*/for (TreeNode<K,V> e = b, next; e != null; e = next) {next = (TreeNode<K,V>)e.next;e.next = null;if ((e.hash & bit) == 0) {if ((e.prev = loTail) == null)loHead = e;elseloTail.next = e;loTail = e;++lc;}else {if ((e.prev = hiTail) == null)hiHead = e;elsehiTail.next = e;hiTail = e;++hc;}}if (loHead != null) {// 如果 loHead 不为空,且链表长度小于等于 6,则将红黑树转成链表if (lc <= UNTREEIFY_THRESHOLD)tab[index] = loHead.untreeify(map);else {tab[index] = loHead;/* * hiHead == null 时,表明扩容后,* 所有节点仍在原位置,树结构不变,无需重新树化*/if (hiHead != null) // (else is already treeified)loHead.treeify(tab);}}if (hiHead != null) {if (hc <= UNTREEIFY_THRESHOLD)tab[index + bit] = hiHead.untreeify(map);else {tab[index + bit] = hiHead;if (loHead != null)hiHead.treeify(tab);}}}
删除节点
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab, boolean movable) {......//p是待删除节点,replacement用于后续的红黑树调整,指向的是p或者p的继承者。//如果p是叶子节点,p==replacement,否则replacement为p的右子树中最左节点if (replacement != p) {//若p不是叶子节点,则让replacement的父节点指向p的父节点TreeNode<K,V> pp = replacement.parent = p.parent;if (pp == null)root = replacement;else if (p == pp.left)pp.left = replacement;elsepp.right = replacement;p.left = p.right = p.parent = null;}//若待删除的节点p时红色的,则树平衡未被破坏,无需进行调整。//否则删除节点后需要进行调整TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);//p为叶子节点,则直接将p从树中清除if (replacement == p) { // detachTreeNode<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;}}
}
删除后的调整
static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root, TreeNode<K,V> x) {for (TreeNode<K,V> xp, xpl, xpr;;) {//x为空或x为根节点,直接返回if (x == null || x == root)return root; //x为根节点,染成黑色,直接返回(因为调整过后,root并不一定指向删除操作过后的根节点,如果之前删除的是root节点,则x将成为新的根节点)else if ((xp = x.parent) == null) {x.red = false; return x;}//如果x为红色,则无需调整,返回else if (x.red) {x.red = false;return root; }//x为其父节点的左孩子else if ((xpl = xp.left) == x) {//如果它有红色的兄弟节点xpr,那么它的父亲节点xp一定是黑色节点if ((xpr = xp.right) != null && xpr.red) { xpr.red = false;xp.red = true; //对父节点xp做左旋转root = rotateLeft(root, xp); //重新将xp指向x的父节点,xpr指向xp新的右孩子xpr = (xp = x.parent) == null ? null : xp.right; }//如果xpr为空,则向上继续调整,将x的父节点xp作为新的x继续循环if (xpr == null)x = xp; else {//sl和sr分别为其近侄子和远侄子TreeNode<K,V> sl = xpr.left, sr = xpr.right;if ((sr == null || !sr.red) &&(sl == null || !sl.red)) {xpr.red = true; //若sl和sr都为黑色或者不存在,即xpr没有红色孩子,则将xpr染红x = xp; //本轮结束,继续向上循环}else {//否则的话,就需要进一步调整if (sr == null || !sr.red) { if (sl != null) //若左孩子为红,右孩子不存在或为黑sl.red = false; //左孩子染黑xpr.red = true; //将xpr染红root = rotateRight(root, xpr); //右旋xpr = (xp = x.parent) == null ?null : xp.right; //右旋后,xpr指向xp的新右孩子,即上一步中的sl}if (xpr != null) {xpr.red = (xp == null) ? false : xp.red; //xpr染成跟父节点一致的颜色,为后面父节点xp的左旋做准备if ((sr = xpr.right) != null)sr.red = false; //xpr新的右孩子染黑,防止出现两个红色相连}if (xp != null) {xp.red = false; //将xp染黑,并对其左旋,这样就能保证被删除的X所在的路径又多了一个黑色节点,从而达到恢复平衡的目的root = rotateLeft(root, xp);}//到此调整已经完毕,进入下一次循环后将直接退出x = root;}}}//x为其父节点的右孩子,跟上面类似else { // symmetricif (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;}}}}
}
参考博客:https://www.cnblogs.com/xuxinstyle/p/9556998.html
https://www.cnblogs.com/qingergege/p/7351659.html
https://www.cnblogs.com/mfrank/p/9227097.html
Java集合系列---红黑树(基于HashMap 超详细!!!)相关推荐
- HashMap底层红黑树原理(超详细图解)+手写红黑树代码
在看完了小刘老师和黑马的源码视频之后,我整理了一篇HashMap的底层源码文章,学海无涯,这几天看了对红黑树的讲解,故将其整理出来 HashMap底层源码解析上 HashMap底层源码解析下 视频链接 ...
- Java集合系列之四大常用集合(ArrayList、LinkedList、HashSet、HashMap)的用法
Java集合系列之四大常用集合(ArrayList.LinkedList.HashSet.HashMap)的用法 ArrayList ArrayList就是传说中的动态数组,用MSDN中的说法,就是A ...
- hashmap删除指定key_「集合系列」- 深入浅出分析HashMap
最近几天,一直在学习HashMap的底层实现,发现关于HashMap实现的博客文章还是很多的,对比了一些,都没有一个很全面的文章来做总结,本篇文章也断断续续结合源码写了一下,如果有理解不当之处,欢迎指 ...
- Java 8系列之重新认识HashMap(转载自美团点评技术团队)
Java 8系列之重新认识HashMap(转载自美团点评技术团队) 摘要 HashMap是Java程序员使用频率最高的用于映射(键值对)处理的数据类型.随着JDK(Java Developmet Ki ...
- 红黑树 原理和算法详细介绍(Java)
R-B Tree简介 R-B Tree,全称是Red-Black Tree,又称为"红黑树",它一种特殊的二叉查找树.红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red) ...
- Java 集合系列06: Vector深入解析
戳上面的蓝字关注我们哦! 精彩内容 精选java等全套视频教程 精选java电子图书 大数据视频教程精选 java项目练习精选 概论 这是接着以前的文章分享的,这里给出以前的文章的连接,供小伙伴们回顾 ...
- 深入Java集合系列之五:PriorityQueue
转载自 深入Java集合系列之五:PriorityQueue 前言 今天继续来分析一下PriorityQueue的源码实现,实际上在Java集合框架中,还有ArrayDeque(一种双端队列),这里 ...
- Java 集合系列03之 ArrayList详细介绍(源码解析)和使用示例
转载自 Java 集合系列03之 ArrayList详细介绍(源码解析)和使用示例 第1部分 ArrayList介绍 ArrayList简介 ArrayList 是一个数组队列,相当于 动态数组.与 ...
- Java 集合系列目录(Category)
Java 集合系列目录(Category) 转自:Java 集合系列目录(Category) 01. Java 集合系列01之 总体框架 02. Java 集合系列02之 Collection架构 0 ...
最新文章
- swfheader 0.10 Released(已更正下载地址)
- 五大新品+两大黑科技,看华为云如何升级基础设施让用户“躺平”
- 五种开源协议的比较(BSD_Apache_GPL_LGPL_MIT)
- div iframe html5,深入理解iframe
- Typora本地图片上传
- 微商人赚钱的4个关键点
- 快捷方便的对js文件进行语法检查。
- wamp无法访问php,wamp无法访问phpmyadmin怎么办
- Flutter学习之事件循环机制、数据库、网络请求
- 非递归求解N皇后问题(回溯法)
- Angularjs在初始化未完毕时出现闪烁的解决办法
- mysql 的下划线搜索转义
- Loadrunner11安装
- 手把手教你用ppc手机远程控制电脑(摘自网络)
- bzoj·入门OJ·统计损失
- 网易微专业 前端工程师 学习笔记
- 小码哥crm学习笔记
- 硬盘主引导记录,分区引导记录(MBR,PBR)
- 斯坦福21秋季:实用机器学习-李沐课程笔记
- Cisco Packet Tracer 交换机的VLAN划分