你理解的hashmap是什么
你好,hashmap是我们日常生活中每天都要用到的一个集合类,它是以键值对的形式进行存储,
(1.7和1.8对比)一、在jdk1.7和1.8之间,hashmap的实现略有区别,在jdk1.7的 时候,hashmap采用的数据结构是数组加链表,到jdk1.8之后采用的是数组加链表加红黑树,红黑树的引用是为了提高它的查询效率,因为链表查询的时间复杂度是O(n),而红黑树的查询效率是O(logN)。二、还有在1.7之前,当我们遇到hash碰撞,在链表上插入数据的时候用的是头插法,在1.8之后改用了尾插法,因为头插法有一些优点,比如插入的效率更高,不需要遍历链表,并且作者也认为后插入的数据人们会先用到,但是在实际当中会出现问题,比如在高并发的情况下会有可能变成循环链表,耗尽我们cpu的性能,为了解决这个问题,在jdk1.8之后改成了尾插法,当然jdk1.7和1.8之间还有很多优化的细节,但是我记得不是太清了,比如要对他的哈希算法进行一个简化,需要在源码中才能看的更透,当然这个源码我是读过的,只是这些东西我可能记得不是太清了。
(创建-初始化)接下来我就用jdk1.8和您聊一聊jdk的一些基本原理,首先我们创建hashmap的时候,阿里规约里面要求我们需要传入一个初始化容量,在我们预知我们将来要插入多少数据的前提下,我们最好传入一个初始化容量,而且这个容量最好是一个2的次幂,我接下会和您接着去聊为什么是二的次幂,一、首先呢当我们去往hashmap中put值的时候,当put第一个值的时候,不管数组加链表还是数组加红黑树加链表,数组会不会去初始化,并且会按照大于等于我们传入的初始化容量离得最近的一个二次幂这么一个值给我们进行初始化数组,二、初始化之后他会使用key的hash值与上他刚才计算(离得最近的二次幂减一)的容量算出它的下标,因为我们的容量都是二的次幂,二的次幂减一它的所有的低位都是1,高位都是0,和我们的hash与过之后,一定会出现一个在咱们容量范围之内的一个下标,因为与运算在我们的计算机当中效率非常高,所以它采用的是与运算不是取余运算,取余运算的效率非常的高。三、并且为了解决hash值底位变化不频繁而导致的插入节点不均匀问题,将hash值右移16位,这样高16位和低16位就都能参与运算了。
(添加数据)当然在我们添加数据的时候,会出现两个问题,一个是扩容的问题,一个是树化的问题,关于扩容的问题,在hashmap当中有一个成员变量,他叫加载因子,当我们插入节点的数量大于等于容量乘以加载因子,也就是16乘以0.75,就是当咱们的size大于12的时候他就会进行一次扩容,当然当我们的链表上悬挂的节点足够多的时候,他还会进行树化,当然树化和扩容都是一个很耗性能的一个操作,树化的前提就是我们悬挂的节点要大于等于8,并且在源码当中还有一个成员变量,是最小的树化的一个容量,意思就是数组的容量达不到默认的64,他会优先选择扩容,而不是对链表进行树化,所以树化有两个先决条件,第一个就是数组的容量要大于等于64,第二个就是链表的长度要大于等于8,所以阿里规约里面要求我们传入初始化容量,根本目的就是为了少扩容,我们也可以在阿里规约里面找到初始化容量的一些公式,我记得应该是你将来要存入数据的数量除以扩容因子加一,应该是这么一个算法。关于hashmap我跟您能聊的只有这么多,如果您还有什么问题可以继续问我
扩容的时机:1. size >= 容量*扩容因子
扩容的大小:原来的两倍,左移一位
树化的时机:1.容量大于等于64 2.链表长度大于等于8
树化的过程:1.把Node变成TreeNode 2. 调用treeify进行树化
数据结构:
1.7 头插 1.头插不需要遍历 2.设计者认为后插入的一般会先查找 缺点 :多线程循环链表的问题
1.8 尾插
哈希碰撞(Hash Collision) 哈希碰撞是指不同的键(Key)通过哈希函数计算后得到相同的哈希值。由于哈希函数输出值的范围通常小于键的实际取值范围,当键的数量足够多时,哈希碰撞不可避免。在HashMap
中,哈希碰撞会导致原本应该分布在哈希表不同位置的键值对被迫存放在同一个位置上。为了解决哈希碰撞,HashMap
采用了链地址法(Separate Chaining):在每个哈希桶(数组元素)中存储一个链表(或树,取决于Java版本和哈希冲突严重程度),链表中的节点存储发生哈希碰撞的键值对。
负载因子(Load Factor) 负载因子是衡量HashMap
填充程度的一个重要参数,通常定义为已存储键值对数量与哈希表容量(数组长度)的比值。在Java中,HashMap
的默认负载因子为0.75。负载因子过高,意味着哈希表接近饱和,哈希碰撞的概率增大,查找、插入和删除等操作的效率降低。反之,过低的负载因子会导致哈希表过于稀疏,浪费存储空间。合理设置负载因子可以在空间利用率和时间效率之间找到平衡。
插入方式 插入键值对时,HashMap
首先使用键的hashCode()
方法计算哈希码,然后通过哈希码与数组长度进行取模运算(hashCode() % capacity
)得到对应哈希桶的索引。如果该桶为空,直接将新键值对作为单节点链表插入;如果已有其他键值对(即发生哈希碰撞),则将新节点添加到链表末尾(Java 8之前)或根据键的自然顺序(或自定义比较器)插入到链表适当位置(Java 8及以后引入红黑树结构时)。插入过程中,如果负载因子超过阈值且哈希表未达到最大容量(capacity * MAXIMUM_CAPACITY
),会触发扩容操作。
扩容倍数 当HashMap
的负载因子超过阈值时,会触发扩容操作以维持合理的哈希表填充程度。扩容时,HashMap
会创建一个新的、容量为原来两倍的哈希表,并将所有原有键值对重新哈希到新表中。扩容倍数为2的设计基于以下考虑:
-
简化索引计算:扩容后,键的索引计算公式为
index = (old_hash & old_capacity) | (new_hash & (new_capacity - 1))
。由于容量总是2的幂次方,old_capacity
的低位全为0,new_capacity - 1
的高位全为1,因此扩容后索引仅与原索引的低位有关,高位不受影响。这意味着扩容后,大部分键值对无需改变索引,只需将原链表复制到新表对应位置即可。 -
减少哈希碰撞:扩容后,键值对分布到更大的哈希表中,哈希冲突概率减小,有利于提高查找、插入和删除的效率。
-
高效内存分配:容量翻倍可以更好地利用JVM的内存分配机制,减少内存碎片。
综上,HashMap
通过处理哈希碰撞、动态调整负载因子、采用特定的插入方式以及按2的倍数进行扩容,实现了高效、动态的键值存储。这些设计确保了在大多数情况下,HashMap
能保持接近常数时间复杂度的查找、插入和删除操作。
hashmap碰撞解决方法
- 链式地址法。也称为“拉链法”。这种方法是将具有相同哈希值的元素存储在单向链表中。当发生冲突时,新元素会添加到链表的末尾。1234567
- 再哈希法。这种方法是通过计算元素的另一个哈希值来解决冲突。如果新的哈希值与原哈希值不同,则可以避免冲突,否则会继续使用原哈希值的方法。13456
HashMap
解决哈希碰撞的方法主要是采用链地址法(Separate Chaining),并在Java 8及更高版本中引入了**红黑树(Red-Black Tree)**优化以应对极端情况下大量的哈希碰撞。以下是详细说明:
链地址法(Separate Chaining)
-
基本原理:
HashMap
内部使用一个数组作为基础数据结构,数组的每个元素称为一个哈希桶(Bucket)。每个桶可以容纳一个或多个键值对,这些键值对以链表的形式链接在一起。当插入一个键值对时,首先计算键的哈希码,然后通过哈希码与数组长度进行取模运算(hashCode() % capacity
)得到对应桶的索引。如果该桶为空,直接将新键值对作为单节点链表插入;如果已有其他键值对(即发生哈希碰撞),则将新节点添加到链表末尾(Java 8之前)或根据键的自然顺序(或自定义比较器)插入到链表适当位置(Java 8及以后引入红黑树结构时)。 -
优点:链地址法将原本应该分布在不同位置但由于哈希碰撞而不得不放在一起的键值对组织成链表,有效地分散了冲突,使得即使发生碰撞,也能通过遍历链表找到正确的键值对。这种方式简单易行,对于小规模的哈希碰撞效果良好。
红黑树优化(Java 8及以上版本)
-
引入背景:在极端情况下,如大量哈希碰撞导致某些桶内的链表过长,查找、插入和删除的时间复杂度会退化为O(n),影响性能。为了解决这个问题,Java 8开始,当某个桶内链表长度达到阈值(默认为8)时,
HashMap
会自动将该链表转换为红黑树结构。 -
红黑树特性:红黑树是一种自平衡二叉查找树,保证了任意节点的左右子树高度差不超过1,从而确保查找、插入、删除等操作的时间复杂度维持在O(log n)。将过长的链表转换为红黑树,显著提高了在大量哈希碰撞时的操作效率。
-
转换与回退:当链表长度降到阈值一半(默认为6)以下时,
HashMap
会自动将红黑树再转换回链表,以节省空间并简化操作。
综上所述,HashMap
通过链地址法(使用链表存储哈希碰撞的键值对)和引入红黑树优化(在链表过长时转换为红黑树)两种方式来解决哈希碰撞问题,确保在各种情况下都能维持较高的查找、插入和删除效率。