文章目录

  • TreeNode结构
  • HashMap1.8(插入)中的树化
    • treeify函数
    • balanceInsertion函数
    • 左旋右旋rotateLeft函数和rotateRight函数

TreeNode结构

上面我们陆续讲解了二叉查找树和红黑树理论知识,接着我们讲一下代码层面。下面是HashMap1.8中的TreeNode结构:

 /*** 用于Tree bins 的Entry。 扩展LinkedHashMap.Entry(进而扩展Node),因此可以用作常规节点或链接节点的扩展。*/static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent;  // 红黑树父节点TreeNode<K,V> left;    //左子节点TreeNode<K,V> right;   //右子节点  TreeNode<K,V> prev;    // 删除后需要取消链接,指向前一个节点(原链表中的前一个节点)boolean red;         //红黑树精髓 redTreeNode(int hash, K key, V val, Node<K,V> next) {super(hash, key, val, next);}     //省略后续代码
    /*** HashMap.Node subclass for normal LinkedHashMap entries.*/static class Entry<K,V> extends HashMap.Node<K,V> {Entry<K,V> before, after;Entry(int hash, K key, V value, Node<K,V> next) {super(hash, key, value, next);}}

TreeNode继承了LinkedHashMap内部类LinkedHashMap.Entry,LinkedHashMap.Entry又继承自HashMap.Node。这些字段跟Entry,Node中的字段一样,是使用默认访问权限的,所以子类可以直接使用父类的属性。

这里提一点为什么在1.7中我们是没有红黑树的,在1.8中要引入红黑树呢???
主要还是因为在1.7中hash冲突导致slot链化严重,影响get查询效率,
本来散链表在最理想状态的查询效率是O(1),但是在链化特别严重后会导致查询退化为O(n)。

HashMap1.8(插入)中的树化

在HashMap1.8中新增了链表树化红黑树的代码,但是要同时满足两个条件才会对链表进行树化。
条件一:链表长度达到8
条件二:散列表数组长度达到64
如果没有同时满足两个条件的话只有链表长度达到8而数组长度没有达到64的话,只会触发resize
进行扩容。
这里提一句,树化以后还是可能转回链表的,当长度小于6的时候就会转回链表了,大于6就依然保持树化结构。
    public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {//省略部分代码else {for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);//当桶中元素个数超过阈值(8)时就进行树化if (binCount >= TREEIFY_THRESHOLD - 1)treeifyBin(tab, hash);break;}//省略部分代码}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(); //数组大小在64以下的话会进行扩容不会去树化else if ((e = tab[index = (n - 1) & hash]) != null) {TreeNode<K,V> hd = null, tl = null;do {//将节点替换为TreeNodeTreeNode<K,V> p = replacementTreeNode(e, null);if (tl == null)            hd = p;                       //hd指向头结点else {/*这里其实是将单链表转化成了双向链表,tl是p的前驱,每次循环更新指向双链表的最后一个元素,用来和p相连,p是当前节点*/p.prev = tl;tl.next = p;}tl = p;} while ((e = e.next) != null);if ((tab[index] = hd) != null)//将链表进行树化hd.treeify(tab);}}

从上方中的代码中可以看到,树化操作是在put操作里面执行的,在putVal函数中有if判断桶中元素个数是否超过8,符合条件调用treeifyBin函数,在treeifBin函数中是先if判断了散列表数组大小是否低于64,低于64不会进行树化会去扩容,当符合我上文说的两个条件链表大于8,数组大于64的时候灰先将所有节点替换为TreeNode,然后再将单链表转为双链表,方便之后的遍历和移动操作。而最终的操作,实际上是调用TreeNode的方法treeify进行的。

treeify函数

上面对putVal到treeifyBin函数做了分析,继续往下走我们看treeify函数是如何进行树化操作的
final void treeify(Node<K,V>[] tab) {//树的根节点TreeNode<K,V> root = null;//x是当前节点,next是后面往下遍历的节点for (TreeNode<K,V> x = this, next; x != null; x = next) {next = (TreeNode<K,V>)x.next;x.left = x.right = null;//如果根节点为null,把当前节点设置为根节点if (root == null) {x.parent = null;x.red = false;root = x;}else {K k = x.key;int h = x.hash;Class<?> kc = null;//这里循环遍历,进行二叉查找树的插入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);TreeNode<K,V> xp = p;/*这里进行左右子节点判定*/if ((p = (dir <= 0) ? p.left : p.right) == null) {x.parent = xp;/*上面有根据hash大小的判断去给定dir的值,这里拿dir的值来设置left和right*/if (dir <= 0) xp.left = x;elsexp.right = x;     root = balanceInsertion(root, x);  //进行插入平衡处理break;}}}}       moveRootToFront(tab, root);  //确保给定节点是桶中的第一个元素}    //这里不是为了整体排序,而是为了在插入中保持一致的顺序static int tieBreakOrder(Object a, Object b) {int d;//用两者的类名进行比较,如果相同则使用对象默认的hashcode进行比较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;}

上面在treeify的代码还是比较简单的,我们可以看到,循环遍历当前树,先会进行判断是否有根节点,如果没有的话现在插入的这个节点就作为根节点,如果存在的话,会去找到可以给该节点插入的位置,依次和遍历节点比较hash值的大小,比它大则跟其右子树节点比较,小则与其左子树节点比较,依次遍历,直到找到左子树节点或者右子树节点为null的位置进行插入。我在本章刚开始的时候讲的二叉查找树就提过这类树的一个很重要的特点,左子树节点的值小于根节点,右子树节点的值大于根节点。

balanceInsertion函数

真正复杂一点的地方在于balanceInsertion函数,这个函数中,将红黑树进行插入
平衡处理(左旋,右旋和变色操作),保证插入节点后仍保持红黑树的性质。
现在我们直接进入balanceInsertion的代码
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,TreeNode<K,V> x) {x.red = true;/*x:插入的节点,xp:父节点,xpp:祖父节点,xppl:祖父左子节点(叔叔节点),xppr:祖父右子节点(叔叔节点)*/for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {    //死循环,直到找到根节点才结束。       //情景1:父节点为null,说明当前节点就是根节点,直接return if ((xp = x.parent) == null) {x.red = false;  //染色为黑色(根节点规定为黑色)return x;}   //情景2:父节点是黑色节点或者祖父节点为null,插入后没有影响黑色完美平衡,直接返回else if (!xp.red || (xpp = xp.parent) == null)return root;//情景3:插入的节点父节点和祖父节点都存在,并且其父节点是祖父节点的左节点,父节点为红色,也有两种情况(LR 或者 LL)         if (xp == (xppl = xpp.left)) {            //情景3-1:插入节点的叔叔节点存在且是红色if ((xppr = xpp.right) != null && xppr.red) {xppr.red = false;//将叔叔节点染色成黑xp.red = false;  //将父节点染色成黑xpp.red = true;//将祖父节点染色成红x = xpp; //最后将爷爷节点设置为当前节点进行下一轮操作}            //情景3-2:插入节点的叔叔节点是黑色或不存在else {              //情景3-2-1:当前插入节点是父节点的右子节点(LR的情景)if (x == xp.right) {root = rotateLeft(root, x = xp);//以父节点为旋转节点进行左旋,变成LL的情景xpp = (xp = x.parent) == null ? null : xp.parent;//设置祖父节点}//情景3-2-2:插入节点是其父节点的左子节点              /*左旋完了之后,就回到了LL的情景进行右旋(父节点是祖父节点的左子节点,当前节点是父节点的左子节点),然后父节点又是红色,当前插入节点也是红色,违反了红黑色的性质,红色不能两两相连,所以接下来需要进行染色;*/if (xp != null) {xp.red = false; //将父节点染色为黑if (xpp != null) {xpp.red = true; //将祖父节点染色为红root = rotateRight(root, xpp); //然后再对祖父节点右旋。}}}}          //情景4:插入的节点父节点和祖父节点都存在,并且其父节点是祖父节点的右节点,父节点为红色,也有两种情况(RL 或者 RR)else {            //情景4-1:插入节点的叔叔节点不为空且是红色if (xppl != null && xppl.red) {xppl.red = false; //将叔叔节点染色成黑xp.red = false;  //将父节点染色成黑xpp.red = true; //将祖父节点染色成红x = xpp; //并且祖父节点设置为当前节点进行下一轮操作}            //情景4-2:插入节点的叔叔节点是黑色或不存在else {              //情景4-2-1:插入节点是其父节点的左子节点(RL的情景) if (x == xp.left) {root = rotateRight(root, x = xp); 以父节点为旋转节点进行右旋,变成RR的情景xpp = (xp = x.parent) == null ? null : xp.parent; //设置祖父节点}              //情景4-2-2:插入节点是其父节点的右子节点,这个时候已经变成了RR的情况,需要对祖父节点左旋来维持平衡if (xp != null) {xp.red = false; //将父节点染色为黑if (xpp != null) {xpp.red = true;  //将祖父节点染色为红root = rotateLeft(root, xpp); //再对祖父节点进行左旋}}}}}}

从上面我写的注释来深入分析balanceInsertion函数后会发现插入的节点x可能存在有父节点xp,祖父节点xpp和叔叔节点xppr和xppl。这里是存在四种场景的。

场景1:父节点为null,直接return插入的节点x场景2:父节点是黑色节点或者祖父节点为null的时候,balanceInsertion有root和x两个参数,在上层的treeify函数中已经对root节点插入了x这个子节点,所以在这里可以直接return root。场景3:插入的节点父节点和祖父节点都存在,并且其父节点是祖父节点的左节点的时候,父节点为红色,也有两种情况(LR 或者 LL) 这个时候我们先去判定祖父节点的右节点是叔叔节点且是红色在代码中的判断//情景3-1:插入节点的叔叔节点存在且是红色if ((xppr = xpp.right) != null && xppr.red)这个时候就会去变色,会去将叔叔节点xppr和父节点xp都变成黑色,然后祖父节点xpp变为红色,然后这里会进行x = xpp的赋值,讲祖父节点设置为当前节点进行下一轮操作xppr.red = false;xp.red = false;xpp.red = true;x = xpp;//情景3-2:插入节点的叔叔节点是黑色或不存在这个时候会存在两个子情况//情景3-2-1:当前插入节点是父节点的右子节点(LR的情景)因为我们在上面判定过当前的父节点是祖父节点的左子节点,所以这个时候插入节点是父节点的右子节点的话对应的就是LR情况。//情景3-2-2:插入节点是其父节点的左子节(LL的情景)这里的判定因为上面已经判断过了是父节点右子节点的情况,这里就只能是左子节点了,所以对应的是LL情景
场景4:插入的节点父节点和祖父节点都存在,并且其父节点是祖父节点的右节点,父节点为红色,也有两种情况(RR 或者 RL)
先去判定祖父节点的右节点是叔叔节点且是红色在代码中的判断//情景4-1:插入节点的叔叔节点不为空且是红色if ((xppr = xpp.right) != null && xppr.red)这个时候就会去变色,会去将叔叔节点xppr和父节点xp都变成黑色,然后祖父节点xpp变为红色,然后这里会进行x = xpp的赋值,讲祖父节点设置为当前节点进行下一轮操作xppr.red = false;xp.red = false;xpp.red = true;x = xpp;//情景4-2:插入节点的叔叔节点是黑色或不存在这个时候会存在两个子情况//情景4-2-1:插入节点是其父节点的左子节点(RL的情景) 因为我们在上面判定过当前的父节点是祖父节点的右子节点,所以这个时候插入节点是父节点的左子节点的话对应的就是LR情况。//情景4-2-2:插入节点是其父节点的右子节点,这个时候已经变成了RR的情况,需要对祖父节点左旋来维持平衡这里的判定因为上面已经判断过了是父节点左子节点的情况,这里就只能是右子节点了,所以对应的是RR情景

这里彻底分析了HashMap红黑树底层的平衡逻辑(变色+左旋右旋),相信各位同学也理解了,对左旋右旋的情况 LL RR LR RL 还不太清晰的可以去看下本文开头讲解的二叉树旋转方面的知识点。

左旋右旋rotateLeft函数和rotateRight函数

接着我们继续看一下TreeNode左旋和右旋的代码 rotateLeft函数和rotateRight函数
/*** 左旋* @param root 当前根节点* @param p 指定的旋转节点* @return 返回根节点(平衡涉及左旋右旋会将根节点改变,所以需要返回最新的根节点) */
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,TreeNode<K,V> p) {// r:旋转节点的右子节点;    pp:旋转节点的父节点, rl:旋转节点的右子节点的左子节点TreeNode<K,V> r, pp, rl;if (p != null && (r = p.right) != null) { //旋转节点非空并且旋转节点的右子节点非空if ((rl = p.right = r.left) != null)  //将p节点的右子节点设置为右子节点的左子节点rl.parent = p; //将rl的父节点设置为pif ((pp = r.parent = p.parent) == null)//将r的父节点设置为p的父节点,如果是空的话(root = r).red = false;//染色成黑else if (pp.left == p) //判断父节点是祖父节点的左子节点还是右子节点pp.left = r; //如果是左子节点,那么就把祖父节点的左子节点设置为relse  pp.right = r; //如果是右子节点,就把祖父节点的右子节点设置为rr.left = p; //最后将r的左子节点设置为pp.parent = r; //将p的父节点设置为r}return root;}
左旋示意图:左旋p节点pp                  pp|                   |p                   r/ \         ---->   / \l   r               p   rr/ \             / \rl  rr          l   rl
  • 左旋做了几件事?
    

    1、将rl设置为p的右子节点,将rl的父节点设置为p
    2、将r的父节点设置pp,将pp的左子节点或者右子节点设置为r
    3、将r的左子节点设置为p,将p的父节点设置为r

/*** 右旋* @param root 当前根节点* @param p 指定的旋转节点* @return  返回根节点(平衡涉及左旋右旋会将根节点改变,所以需要返回最新的根节点) */
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,TreeNode<K,V> p) {//l:p节点的左子节点 pp:p节点的父节点 lr:p节点的左子节点的右子节点TreeNode<K,V> l, pp, lr;if (p != null && (l = p.left) != null) { //旋转节点p非空并且p节点的左子节点非空if ((lr = p.left = l.right) != null) //将p节点的左子节点设置为左子节点的右子节点lr.parent = p; //然后将p节点的左子节点的右子节点的父节点设置为pif ((pp = l.parent = p.parent) == null) //将p节点的左子节点的父节点设置为p的父节点,如果为空的话,说明l就是根节点了(root = l).red = false; //染色成黑else if (pp.right == p) //判断p节点是pp节点的左子节点还是右子节点,pp.right = l; //如果p节点是pp节点的右子节点的话,将祖父节点pp的右子节点设置为lelse //如果p节点是pp节点的左子节点的话,将祖父节点pp的左子节点设置为lpp.left = l;l.right = p; //最后将l节点的右子节点设置为pp.parent = l; //将p节点的父节点设置为l}return root;}
右旋示意图:右旋p节点pp                      pp|                       |p                       l/ \          ---->      / \l   r                   ll  p/ \                         / \ll  lr                      lr  r
  • 右旋都做了几件事?
    

    1.将lr设置为p节点的左子节点,将lr的父节点设置为p
    2.将l的父节点设置为pp,将pp的左子节点或者右子节点设置为l
    3.将l的右子节点设置为p,将p的父节点设置为l

      上面关于hashmap底层的树化过程已经讲得很详细了,接着我们继续把treeify函数中的moveRootToFront函数讲完。
    

跟多内容,进入原文查看 :

  • https://blog.csdn.net/weixin_42236165/article/details/110044943

缘灭--HashMap底层原理之1.8put源码篇(三)相关推荐

  1. 缘灭--HashMap系列之1.8put源码篇(三)

    小宋本篇文章接着带大家深入了解HashMap,这次我们来聊一聊1.8put源码关于树化的部分. 文章目录 TreeNode结构 HashMap1.8(插入)中的树化 treeify函数 balance ...

  2. HashMap底层原理分析(put、get方法)

    1.HashMap底层原理分析(put.get方法) HashMap底层是通过数组加链表的结构来实现的.HashMap通过计算key的hashCode来计算hash值,只要hashCode一样,那ha ...

  3. java map原理_Java HashMap底层原理分析

    前两天面试的时候,被面试官问到HashMap底层原理,之前只会用,底层实现完全没看过,这两天补了补功课,写篇文章记录一下,好记性不如烂笔头啊,毕竟这年头脑子它记不住东西了哈哈哈.好了,言归正传,今天我 ...

  4. HashMap底层原理(当你put,get时内部会发生什么呢?)

    HashMap底层原理解析(一) 接触过HashMap的小伙伴都会经常使用put和get这些方法,那接下来就对HashMap的内部存储进行详解.(以初学者的角度进行分析)-(小白篇) 当程序试图将多个 ...

  5. 深度解剖HashMap底层原理

    HashMap底层原理 写在前面 JDK1.7版本--HashMap java.1.7源码分析 new一个HashMap实例的存储流程图如下: API常用方法 API中重要的变量 第一步:申明一个Ha ...

  6. 我向面试官讲解了hashmap底层原理,他对我竖起了大拇指

    前言: 正值金九银十的黄金招聘期,大家都准备好了吗?HashMap是程序员面试必问的一个知识点,其内部的基本实现原理是每一位面试者都应该掌握的,只有真正地掌握了 HashMap的内部实现原理,面对面试 ...

  7. 没人比我更懂系列之--HashMap底层原理及相关问题

    HashMap的设计 map(k,v)这样的结构设计,对于很多存储逻辑异常适用.简单来说,就是数组+链表/红黑树(jdk1.8). HashMap如何确定存储位置? 对于HashMap来说,首先要做的 ...

  8. 字节跳动Android三面视频解析:framework+MVP架构+HashMap原理+性能优化+Flutter+源码分析等

    前言 对于字节跳动的二面三面而言,Framework+MVP架构+HashMap原理+性能优化+Flutter+源码分析等问题都成高频问点!然而很多的朋友在面试时却答不上或者答不全!今天在这分享下这些 ...

  9. mvcc原理_MVCC原理探究及MySQL源码实现分析

    沃趣科技数据库专家  董红禹 MVCC原理探究及MySQL源码实现分析 数据库多版本读场景 session 1 session 2 select a from test; return a = 10 ...

最新文章

  1. Windows SharePoint Services 3.0 应用程序模板
  2. UICollectionView自定义布局(二)
  3. oracle 64位客户端_oracle的管理工具toad如何设置命令补全
  4. Firefox 扩展开发 install.rdf和chrome.manifest
  5. 算法 排序 python 实现--快速排序
  6. sql查table,VIEW,sp, function 及 trigger 物件
  7. Mac 技术篇-设置Finder文件管理显示文件路径
  8. wxWidgets:wxMoveEvent类用法
  9. 微软推出的Pylance,随着VS Code的更新,性能又前进了一步
  10. vs2003打开项目错误
  11. 怎么在java上运行服务器,用java做了一个简单的定时任务工程,不知道如何让它在服务器上运行起来?应该怎样做???...
  12. javafx 表格列拖拉_JavaFX技巧22:“自动调整大小(树)”表列
  13. VS2013 生成sqlite3动态连接库
  14. UDP实时图像传输进阶篇——1080P视频传输
  15. 设计原则SOLD之 —— 单一职责原则SRP
  16. php dingo和jwt,laravel dingo/api添加jwt-auth认证
  17. 支付宝配置沙箱测试android,个人开发者使用支付宝沙箱环境进行代码调试
  18. 教程:客制化您的输入法
  19. 如何跳出令人窒息的职场死循环
  20. 如何使用爬虫与JieBa库制作词云

热门文章

  1. springboot整合JPA+MYSQL+queryDSL数据增删改查
  2. 入门html网页,网页设计第一天:HTML入门
  3. Java 应用程序的 CPU 使用率飙升原因分析
  4. 快手短视频去水印API接口源码
  5. playbook变量
  6. 概要设计说明书(一)
  7. 一款好用的开源的 macOS 压缩工具
  8. /tmp/ccY3hmyr.o(.eh_frame+0x11): undefined reference to `__gxx_personality_v0'
  9. 2022年11月23日——jQuery——T1(基础选择器与表单选择器)
  10. IDEA插件系列(76):Active Tab Highlighter插件——高亮Tab标签