你了解HashMap吗?

你了解HashMap吗?

你对hash的理解?

hash的基本概念就是把任意长度的输入通过一个hash算法之后,映射成固定长度的输出。这算是hash的概念吧

但是把任意长度的输入转化为固定长度的输出,会不会有问题啊?

这个肯定会有问题,在程序中可能碰到两个value值经过hash算法之后算出同样的hash值,也就是会发生hash冲突。

hash冲突可以避免吗?

理论上是没办法避免的,就比如抽屉原理,我们一个有10个苹果,但是一共就9个抽屉,就是最终一定会有一个抽屉里的数量是大于1的,所以hash冲突没有办法避免,只能尽量避免。

你认为一个好一点的hash算法,它考虑的点,应该是哪些呢?

首先,这个hash算法,①它的效率得高,要做到长文本也能高效计算出hash值。②hash值不能让它逆推出原文。③两次输入,只要有一点不同,它也得保证这个hash值是不同的。④其次就是尽可能的要分散吧,因为,在table中slot大部分都处于空闲状态时,要尽可能降低hash冲突。

HashMap中存储数据的结构,长什么样啊?

按照jdk8来说吧,jdk8中HashMap现在是数组+链表+红黑数构成的,然后每个数据单元都是一个Node结构,Node结构中有key字段、有value字段、next字段、hash字段。
next字段就是发送hash冲突的时候,当前桶位中的node与冲突node连成一个链表要用的字段。

如果创建HashMap的时候,你没有去指定这个hashmap中的这个散列表数组长度,那初始长度是多少啊?

初始长度默认是16。

那这个散列表是new HashMap()的时候就创建了,还是在什么时候创建的?

散列表是懒加载机制,只有第一次put数据的时候,它才创建。

这个默认的负载因子是多少啊?并且有啥用啊?

默认是0.75,负载因子它的作用就是计算扩容阈值用的,比如使用无参构造方法创建的hashmap对象,它默认情况下扩容阈值就是16*0.75,也就是12。

链表转化为红黑树需要达到什么条件?

有两个指标,其中一个就是链表长度达到8,另一个是当前散列表数组长度已经达到64,否则的话,就算slot内部链表长度到了8,它也不会转为树,它仅仅会发生一次resize,散列表扩容。

Node对象内部有一个hash字段,这个hash字段的值是key对象的hashcode()返回值么?

这个不是的,这个hash值的key.hashcode二次加工得到的,加工原则是key的hashcode高16位^低16位得到的一个新值。

那为什么要这样搞?

主要还是因为hash寻址算法的缘故,第一点,散列表数组的长度必须是2的次方数嘛,比如说就是16,32,64,寻址算法是hash&(table.length - 1),就已经知道了,table.length一定是2的次方数,length转化为二进制后,一定是1是高位,然后低位全部是0,就比如说,16转化为二进制就是1加上四个0,然后32转化为二进制就是1加五个0。这种数值它减1之后得到的数值转换为二进制它也很特殊,比如16 - 1 = 15,然后15转化为二进制就是四个1,任何与这种高位全部为0,低位都是一串1的数,按位与之后,它得到的这个数值一定是>=0且<=这个二进制数的,然后带上数组的下标为0的slot,加起来正好为2的次方数。

那说下为什么采用这个高低位异或吧?

采用高低位异或主要是优化hash算法,因为hashmap内部散列表,它大多数场景下,它不会特别大,也就是说这个table.length - 1得到的这个二进制数,实际有效位很有限,一般都在16位以内,这样的话,key的hash值高16位就等于完全浪费了,没起到作用,所以,node的hash字段才采用了高16位^(异或)低16位这种方式来搞它。

hashmap的put写数据的具体流程,尽可能的详细点去说?

这个写数据,主要是为了分为几种情况吧,一共是四种情况,前面这个寻址算法是一样的,都是根据key#hashcode经过高低位异或之后的值,再按位与&(table.length - 1),得到一个槽位下标。然后根据这个槽内的状况,状况不同,然后情况也不同,大概就是四中状态,第一种是slot == null 第二种就是slot != null,并且它引用的node还没有链化;第三种就是slot内的node已经链化了;第四种就是冲突很严重的情况下,就是那个链已经转化为红黑数了,一共就是这四种情况。

那你先聊聊前面三种情况吧,等下我们再聊聊红黑数。

  1. slot==null的时候,这个其实没什么好说的,直接占用这个slot就可以了,然后把当前put方法传进来的key和value包装成一个node对象,放到这个slot中就可以了。
  2. 当slot!=null,但尚未成链,这种情况,我们需要先对比一下,这个node#key与当前put对象的key是否完全相等,如果完全相等,那这个操作就是relace操作,就是替换操作,把新的value替换当前slot->node#value就可以了,否则的话,这个put操作就是一个正儿八经的hash冲突了,然后在slot->node后面追加一个node就可以了,采用尾插法
  3. slot已经链化了,和第二种情况处理很相似,首先也是迭代查找node,看看链表上的元素的key,与当前传来的key是不是完全一致,如果有一致的话,还是replace操作,替换当前node#value,否则的话就是,我们迭代到链表尾节点也没有匹配到完全一致的node,就把数据包装成node追加到链表尾部。还需需要检查一下当前链表长度有没有达到树化阈值,如果达到阈值的话,就调用一个树化方法,树化操作都在这个方法里面完成。

那你简单说一下红黑树的写入操作吧?也比较复杂,这样吧,你先说一下,红黑树写入操作的第一部分吧,就是说找到它的父节点的流程吧?

嗯,这个问题比较复杂,我得先整理一下。。这样吧,我觉得要回答好这个问题还得从TreeNode说起,要不咱先说一下TreeNode这个结构?TreeNode就是继承了Node结构,在Node基础上加了几个字段,分别是指向父节点parent,然后指向左子节点left,指向右节点right,然后还有表示颜色的red,然后这个就是TreeNode的基本结构。
然后红黑树的插入操作,首先是找到一个合适的插入点,就是找到插入节点的父节点,然后这个红黑树它又满足二叉树的所有排序特性(PS:满足二叉排序树的所有特征),这个找父节点的操作和二叉排序树是完全一致的,说一下这个二叉排序树,其实就是二分查找算法映射出来的结构,每个节点都可以有子节点,并且左子节点小于当前节点,右节点大于当前节点,然后每一次向下查找一一层就可以排除掉一半的数据,查找效率非常的牛逼。但是查找的过程也是分情况的,首先第一种情况就是一直向下探测,直到查询到左子树或者右子树为null,说明整个树中,它没有发现node#key与当前put的key一致的这个TreeNode,此时探测节点就是插入父节点所在了,然后就是当前插入节点插入到父节点的左子树或者是右子树就完事了。根据这个插入节点的hash值和父节点hash值大小决定左右的,插入会打破平衡,还需要一个红黑数的平衡算法。
第二种情况就是,根节点向下探测的过程中,发现这个TreeNode#key与当前put#key完全一致,说明它也是一次replace操作,直接替换这个TreeNode#value字段就可以了。

非常的详细了,那说一下红黑树的那几个原则?

构成数的节点拥有一个颜色属性,颜色要么是黑,要么是红。第二个规则就是根节点必须是黑色的,第三个就是,从叶节点到根节点的路径上,它每条路径黑色节点它一定是一致的,俗称的黑高。第四个是就是不能有两个红色节点相连,也就是红色节点的两个字节点它一定是黑色节点。还有就是叶子节点一定是黑色的。然后就是红插,就是新的节点一定是红色的(规则里没有这一条,这是一个经验)。因为红插的话有优势,碰到父节点是黑色的话,树不会失衡,如果是黑插的话,就一定会失衡了。

那这个左旋或者右旋是怎么回事

jdk8中hashmap为什么要引入红黑数呢?

其实主要就是解决hash冲突导致链化严重的问题,在jdk7的时候,当这个slot链化非常严重的时候,就是严重影响get查询效率,本身散列表最理想的查询效率为O(1),但是链化特别严重后,就会导致查询退化为O(n),严重影响查询性能了,为了解决这个问题,它才引入的红黑树,红黑树其实就是一颗特别的二叉排序树,嗯,时间复杂度。。。比链表强很多(忘了)。

那为什么链化之后性能就变低了呢?

因为链表它毕竟不是数组,它从内存角度来看,它没有连续着,如果我们要往后查询的话,如果要查询的数据它在链表的末尾,那只能一个节点一个节点next跳跃过期,非常耗费性能。

那你说一说什么情况下会触发这个扩容呢?

就是在写数据之后可能会触发扩容,在hashmap结构内,我记得有一个记录当前数据量的字段,这个数据量字段达到扩容阈值的话,它就会触发扩容操作。

那扩容后会扩容多大呢?这块算法是咋样的呢?

扩容的规则是这样的,因为table数组长度必须是2的次方数嘛,扩容其实每次都是按照上一次tableSize位移运算得到的,做一次左移1位运算,假设当前tableSize是16的话,16<<1=32

先打断一下,tableSize为什么要采用位移运算呢?咋不直接乘以2?

主要是因为性能吧,因为CPU毕竟它不支持乘法运算,所有的乘法运算,它最终都是在指令层面转化为了加法实现的,效率很低,如果用位运算的话对CPU来说就非常简洁高效,效率很高。

那创建这个扩容后的table数组,老数组中的数据怎么迁移呢?

迁移其实就是挨个桶位推进迁移,就是一个桶位一个桶位的处理,主要还是看当前处理桶位的数据状态吧,也是分了大概四种状态吧,第一种是这个slot存储的是null,第二种就是存储的是个node,node没有链化,第三种是slot存储了一个链化的node,第四种就是存储了一个红黑树的根节点TreeNode对象,这四种的迁移规则都不太一样。

第一种情况不用处理,就不用说了,你说下第二和第三种情况,slot内存储的是单个node或者存储的是一个链化的一个链表,这种怎么处理?

当迁移的时候发现,这个slot中存储的这个node节点,它的next是null的时候,说明这个slot它没有发生过hash冲突,直接迁移就好了,根据新表的tableSize计算出它在新表的位置,然后存放过去就ok了,第二种就是迁移时发现它的这个next字段,不为null,说明这个slot发生过hash冲突,这个时候需要把当前slot中保存的这个链表,拆成两个链表,分别是这个高位链和低位链。
这里我说一下高位链和低位链的意思,我们都知道老表的桶位已经链化了,这样就推理出这个链表中,所有的node#hash字段转化二进制以后,低位都是相同的,低位指的就是老表的tableSize - 1转化出来的二进制数的有效位,打个比方吧,table数组长度是16;16 - 1 就是15,然后15转化出来的二进制数就是四个1嘛,说明低位是低四位,高位是第5位(PS:01111),当链表低位,就是。。目前这个链表的低位一定是相同的,但是高位不一定,有点node高位可能是0,有点node高位可能就是1,这块对应的node迁移到新表中,它所存放的slot位置也是不一样的,低位链因为高位是0,所以说,迁移到node新表的时候,这个slot下标和老的是一样的,那高位链因为高位是1,所以说到扩容,就是到了新表以后,是老表的位置+老表的size。

那最后咱再聊下红黑树的扩容吧,咋处理的?

大概是这样啊,红黑树节点对象,这个TreeNode结构,它依然保留了这个next字段,也就是说,红黑树,它内部其实它还维护这一个链表,只不过咱查询的时候,咱现在不用它,但是你新增或者删除节点的时候,你仍然需要维护这个聊表,这个链表有啥用呢?就是说,方便split拆分这个红黑树时去用的,其实处理就和上边说的这个普通的这个node链表,没啥区别,也就是根据这个高位和低位拆分成高位链和低位链,然后高位链这个数据啊,最终要存放到新表的,下标就是老表的位置+老表的数组长度,然后就是出来的这个位置,低位链存放的这个位置和老表呢,是同一个位置。就不同的点,你拆分出来的这个链表(高低位链表),需要看一下它的长度,如果这个长度是<=6的话,直接将这个TreeNode转化为node链表,然后放到这个扩容后的新表中就可以了,如果你拆分出来的这个链表,长度仍然是>6的话,还是需要把这个链表给升级成红黑树(其实就是重建红黑树),大概就是这样。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值