哈希表结构存储过程 原理及实例详解(Java) - 集合

本文详细解析了HashMap底层数据结构,包括哈希表的组成(数组+链表/红黑树),哈希值计算,扩容机制,以及HashMap的构造方法和put方法的工作原理。重点介绍了如何处理哈希冲突和何时转换为红黑树以优化查询性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

哈希表结构存储过程

1.HashMap底层数据数据结构:哈希表
2.jdk7:哈希表 = 数组+链表
  jdk8:哈希表 = 数组+链表+红黑树
3.
  先算哈希值,此哈希值在HashMap底层经过了特殊的计算得出
  如果哈希值不一样,直接存
  如果哈希值一样,再去比较内容,如果内容不一样,也存
  如果哈希值一样,内容也一样,直接去重复(后面的value将前面的value覆盖)
  
  哈希值一样,内容不一样->哈希冲突(哈希碰撞)
4.要知道的点:
  a.在不指定长度时,哈希表中的数组默认长度为16,HashMap创建出来,一开始没有创建长度为16的数组
  b.什么时候创建的长度为16的数组呢?在第一次put的时候,底层会创建长度为16的数组
  c.哈希表中有一个数据加[加载因子]->默认为0.75(加载因子)->代表当元素存储到百分之75的时候要扩容了->2倍
  d.如果对个元素出现了哈希值一样,内容不一样时,就会在同一个索引上以链表的形式存储,当链表长度达到8并且当前数组长度>=64,链表就会改成使用红黑树存储
    如果后续删除元素,那么在同一个索引位置上的元素个数小于6,红黑树会变回链表
  e.加入红黑树目的:查询快
外面笔试时可能会问到的变量
default_initial_capacity:HashMap默认容量  16
default_load_factor:HashMap默认加载因子   0.75f
threshold:扩容的临界值   等于   容量*0.75 = 12  第一次扩容
treeify_threshold:链表长度默认值,转为红黑树:8
min_treeify_capacity:链表被树化时最小的数组容量:64

1.问题:哈希表中有数组的存在,但是为啥说没有索引呢?

​ 哈希表中虽然有数组,但是set和map却没有索引,因为存数据的时候有可能在同一个索引下形成链表,如果2索引上有一条链表,那么我们要是按照索引2获取,咱们获取哪个元素呢?所以就取消了按照索引操作的机制

2.问题:为啥说HashMap是无序的,LinkedHashMap是有序的呢?

​ 原因:HashMap底层哈希表为单向链表

​ LinkedHashMap底层在哈希表的基础上加了一条双向链表

1.HashMap无参数构造方法的分析

//HashMap中的静态成员变量
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

解析:使用无参数构造方法创建HashMap对象,将加载因子设置为默认的加载因子,loadFactor=0.75F。

2.HashMap有参数构造方法分析

HashMap(int initialCapacity, float loadFactor) ->创建Map集合的时候指定底层数组长度以及加载因子
    
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
    	throw new IllegalArgumentException("Illegal initial capacity: " +
    initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
    	initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
    	throw new IllegalArgumentException("Illegal load factor: " +
    loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);//10
}

解析:带有参数构造方法,传递哈希表的初始化容量和加载因子

  • 如果initialCapacity(初始化容量)小于0,直接抛出异常。
  • 如果initialCapacity大于最大容器,initialCapacity直接等于最大容器
    • MAXIMUM_CAPACITY = 1 << 30 是最大容量 (1073741824)
  • 如果loadFactor(加载因子)小于等于0,直接抛出异常
  • tableSizeFor(initialCapacity)方法计算哈希表的初始化容量。
    • 注意:哈希表是进行计算得出的容量,而初始化容量不直接等于我们传递的参数。

3.tableSizeFor方法分析

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

8  4  2  1规则->无论指定了多少容量,最终经过tableSizeFor这个方法计算之后,都会遵循8421规则去初始化列表容量为了存取高效,尽量较少碰撞

解析:该方法对我们传递的初始化容量进行位移运算,位移的结果是 8 4 2 1 码

  • 例如传递2,结果还是2,传递的是4,结果还是4。
  • 例如传递3,结果是4,传递5,结果是8,传递20,结果是32。

4.Node 内部类分析

哈希表是采用数组+链表的实现方法,HashMap中的内部类Node非常重要,证明HashSet是一个单向链表

 static class Node<K,V> implements Map.Entry<K,V> {
     final int hash;
     final K key;
     V value;
     Node<K,V> next;
 Node(int hash, K key, V value, Node<K,V> next) {
     this.hash = hash;
     this.key = key;
     this.value = value;
     this.next = next;
}

解析:内部类Node中具有4个成员变量

  • hash,对象的哈希值
  • key,作为键的对象
  • value,作为值得对象(讲解Set集合,不牵扯值得问题)
  • next,下一个节点对象

5.存储元素的put方法源码

public V put(K key, V value) {
	return putVal(hash(key), key, value, false, true);
}

解析:put方法中调研putVal方法,putVal方法中调用hash方法。

  • hash(key)方法:传递要存储的元素,获取对象的哈希值
  • putVal方法,传递对象哈希值和要存储的对象key

6.putVal方法源码

Node<K,V>[] tab; Node<K,V> p; int n, i;
	if ((tab = table) == null || (n = tab.length) == 0)
		n = (tab = resize()).length;

解析:方法中进行Node对象数组的判断,如果数组是null或者长度等于0,那么就会调研resize()方法进行数组的扩容。

7.resize方法的扩容计算

if (oldCap > 0) {
     if (oldCap >= MAXIMUM_CAPACITY) {
         threshold = Integer.MAX_VALUE;
         return oldTab;
     }
     else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
              oldCap >= DEFAULT_INITIAL_CAPACITY)
         newThr = oldThr << 1; // double threshold
}

解析:计算结果,新的数组容量=原始数组容量<<1,也就是乘以2。

8.确定元素存储的索引

if ((p = tab[i = (n - 1) & hash]) == null)
	 tab[i] = newNode(hash, key, value, null);

解析:i = (数组长度 - 1) & 对象的哈希值,会得到一个索引,然后在此索引下tab[i],创建链表对象。

不同哈希值的对象,也是有可能存储在同一个数组索引下。

其中resize()扩容的方法,默认是16
 tab[i] = newNode(hash, key, value, null);->将元素放在数组中  i就是索引

 i = (n - 1) & hash
     0000 0000 0000 0000 0000 0000 0000 1111->15
                                                    &   0&0=0 0&1=0 1&1=1
     0000 0000 0000 0001 0111 1000 0110 0011->96355
--------------------------------------------------------
     0000 0000 0000 0000 0000 0000 0000 0011->3
     0000 0000 0000 0000 0000 0000 0000 1111->15
                                                    &   0&0=0 0&1=0 1&1=1
     0000 0000 0001 0001 1111 1111 0001 0010->1179410
--------------------------------------------------------
     0000 0000 0000 0000 0000 0000 0000 0010->2

9.遇到重复哈希值的对象

 Node<K,V> e; K k;
 if (p.hash == hash &&
 	((k = p.key) == key || (key != null && key.equals(k))))
		 e = p;

解析:如果对象的哈希值相同,对象的equals方法返回true,判断为一个对象,进行覆盖操作。

else {
     for (int binCount = 0; ; ++binCount) {
     	if ((e = p.next) == null) {
     		p.next = newNode(hash, key, value, null);
     	if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
     		treeifyBin(tab, hash);
     	break;
 }

解析:如果对象哈希值相同,但是对象的equals方法返回false,将对此链表进行遍历,当链表没有下一个节点的时候,创建下一个节点存储对象.

<think>嗯,用户想了解哈希表的底层实现原理,特别是数据结构以及解决哈希冲突的方法。首先,我需要回忆一下哈希表的基本概念。哈希表是一种通过哈希函数将键映射到存储位置的数据结构,这样可以快速访问数据。根据引用[2],哈希表通过哈希函数将关键字转换为数组下标,比如使用取模运算hash(key) = key % capacity。 接下来,底层数据结构方面,哈希表通常是一个数组,每个位置称为桶。但如何处理冲突呢?用户提到了哈希冲突的解决方法,这里可能需要详细说明开放寻址法和链地址法。引用[1]和[2]都提到了哈希冲突是不可避免的,所以设计好的解决方法是关键。 开放寻址法,比如线性探测,当发生冲突时,按顺序查找下一个空槽。而链地址法则是在每个数组元素处维护一个链表,所有哈希值相同的元素都放在同一个链表中。引用[3]中的C++版哈希表可能用的是链地址法,因为提到负载因子和数组大小调整。 然后需要考虑哈希函数的设计原则,引用[1]提到哈希函数需要让地址均匀分布,简单高效。比如除留余数法,或者更复杂的如MD5的一部分。同时,负载因子过高会导致冲突增加,所以需要扩容,引用[3]提到负载因子通常控制在0.75左右,超过时会扩容并重新哈希。 还有,如何处理哈希冲突时的比较问题?引用[5]指出,当哈希码相同时,必须用equals方法判断对象是否真正相等,所以在Java中需要同时重写hashCode和equals方法。 用户可能还想知道哈希表的优缺点,比如快速查找但空间浪费,这在引用[3]里有提到。应用场景如缓存、字典结构,可能也需要简单说明。 总结起来,回答的结构应该是先解释哈希表的基本结构,再详细说明哈希函数,接着两种解决冲突的方法,最后讨论性能优化和相关问题。需要确保引用正确,比如引用[2]的例子和引用[1]的冲突避免方法。还要注意使用中文,数学公式用$...$,独立公式用$$...$$,比如哈希函数的例子。</think>### 哈希表底层实现原理详解 #### 一、哈希表基本结构 哈希表(Hash Table)是一种通过**哈希函数**将键(Key)映射到存储位置的数据结构,核心由以下两部分组成: 1. **哈希函数**:将任意长度的输入(如字符串、对象)转换为固定范围的整数值,例如: $$ \text{index} = \text{hash}(key) \% \text{capacity} $$ 其中`capacity`是底层数组的容量[^2]。 2. **数组(桶)**:存储实际数据的容器,每个数组位置称为一个“桶”(Bucket)。 #### 二、哈希冲突解决方法 由于哈希函数可能将不同键映射到同一位置(即$ \text{hash}(k_i) = \text{hash}(k_j) $),需通过以下方法解决冲突[^1]: ##### 1. **开放寻址法(Open Addressing)** - **原理**:冲突发生时,按某种规则(如线性探测、二次探测)寻找下一个空闲位置。 - **示例**: - 线性探测:若位置$i$被占用,尝试$i+1, i+2, \dots$直到找到空位。 - 公式: $$ \text{index} = (\text{hash}(key) + \text{step}) \% \text{capacity} $$ - **缺点**:容易产生“聚集现象”,导致查找效率下降[^3]。 ##### 2. **链地址法(Separate Chaining)** - **原理**:每个数组元素维护一个链表(或红黑树),所有哈希值相同的元素存入同一链表。 - **示例**:Java的`HashMap`在JDK8后,链表长度超过8时转为红黑树以提高性能[^1]。 - **优点**:简单高效,适合高冲突场景。 #### 三、关键设计与优化 1. **哈希函数设计原则**: - **均匀性**:输出值应均匀分布在数组范围内。 - **高效性**:计算速度快,如除留余数法、平方取中法[^2]。 2. **负载因子(Load Factor)**: - 定义: $$ \text{负载因子} = \frac{\text{已存元素数量}}{\text{数组容量}} $$ - 作用:当负载因子超过阈值(如0.75)时,触发扩容(Rehashing)以降低冲突率[^3]。 3. **扩容机制**: - 通常将数组容量翻倍,重新计算所有元素的哈希位置。 #### 四、性能分析 - **理想情况**:插入、查找、删除操作的时间复杂度为$O(1)$[^3]。 - **最坏情况**:所有元素哈希冲突,时间复杂度退化为$O(n)$(如链表遍历)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值