面试官:为什么默认初始容量为2次幂?不是2次幂会怎样?讲讲 HashMap 扰动函数?

本文深入探讨了HashMap为何选择2的幂次方作为初始容量,通过分析源码揭示了2次幂能确保数据均匀插入并降低哈希冲突。同时,解释了扰动函数的作用,它是通过右移并异或操作减少冲突,提高HashMap性能。结论是,容量为2次幂和扰动函数的设计都是为了优化哈希冲突,提升HashMap效率。

前言

关于HashMap的详解文章请移步:深度剖析HashMap一篇文章就够了

关于面试官的其他问题请移步:

为什么初始容量是 2次幂?

jdk1.8 的 putVal()

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 如果没有hash碰撞则直接插入元素
    if ((p = tab[i = (n - 1) & hash]) == null) 
        tab[i] = newNode(hash, key, value, null);
    else {
    	......
	}
}

通过看源码,我们发现,判断桶的索引的实现是 i = ( n - 1 ) & hash,其中 n 是 map 的容量。

任何 2 的整数幂 - 1 得到的二进制都是 1,如:16 - 1 = 15(1111);32 - 1 = 31(11111)

而 n-1 与 hash 做的是与运算(&),与运算是 两个都为1,才为1

既然我们的 n-1 永远都是 1,那 ( n - 1 ) & hash 的计算结果就是 低位的hash 值。如:

    00100100 10100101 11000100 00100101    // Hash 值 
&   00000000 00000000 00000000 00001111    // 16 - 1 = 15
----------------------------------
    00000000 00000000 00000000 00000101    // 高位全部归零,只保留末四位。

那容量不是 2次幂会怎么样?我们来做个试验。

2次幂的情况:

hash(n-1)& hash结果
01111 & 00
11111 & 11
21111 & 102
31111 & 113
41111 & 1004
51111 & 1015
61111 & 1106
… …… …… …

非2次幂的情况,假设 n = 10

hash(n-1)& hash结果
01100 & 00
11100 & 10
21100 & 100
31100 & 110
41100 & 1004
51100 & 1014
61100 & 1104
… …… …… …

对比来看,哪种发生哈希碰撞的概率更低一目了然,如果 n 为 2次幂,可以保证数据的均匀插入,降低哈希冲突的概率,毕竟冲突越大,代表数组中的链表/红黑树越大,从而降低Hashmap 的性能。

扰动函数

HashMap 中的扰动函数是一个通过对 key 值类型自带的哈希函数生成的散列值进行位移计算来扰乱散列值,以达到降低哈希碰撞的概率的方法。源码中对应的是 hash(),但具体是如何进行移位和降低碰撞概率的??

// jdk 8
static final int hash(Object key) {
   int h;
   return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

我们分析一下hash(),key.hash() 调用的是key类型自带的哈希函数,返回的是 int 类型的散列值。

如果没有扰动函数的情况下,我们拿着散列值作为下标找到 hashmap 中对应的桶位存下即可(不发送哈希冲突的情况下),但 int 类型是 32 位,很少有Hashmap的数组有40亿这么大,所以, key 类型自带的哈希函数返回的散列值不能拿来直接用。如果我们取低几位的 hash 值来做数组映射行不行,但是如果低位相同,高位不同的 hash 值就碰撞了,如:

// Hash 碰撞示例:
00000000 00000000 00000000 00000101 & 1111 = 0101 // H1
00000000 11111111 00000000 00000101 & 1111 = 0101 // H2

为了解决这个问题,HashMap 想了个办法,用扰动函数降低碰撞的概率。将 hash 值右移16位(hash值的高16位)与 原 hash 值做异或运算(^),从而得到一个新的散列值。如:

00000000 00000000 00000000 00000101 // H1
00000000 00000000 00000000 00000000 // H1 >>> 16
00000000 00000000 00000000 00000101 // hash = H1 ^ (H1 >>> 16) = 5

00000000 11111111 00000000 00000101 // H2
00000000 00000000 00000000 11111111 // H2 >>> 16
00000000 00000000 00000000 11111010 // hash = H2 ^ (H2 >>> 16) = 250

H1,H2 两个 hash 值经过扰动后,很明显不会发生碰撞。

总结

总的来说,不管是规定 Hashmap 的 n 为 2次幂,还是扰动函数,都是为了一个目标,降低哈希冲突的概率,从而使 HashMap 性能得到优化。而规定 n 为 2次幂,是在新建 Hashmap对象初始化时,规定其容量大小的角度来优化。而扰动函数是插入 key 值时改变 key 的散列值来达到优化效果。

<think>好的,我现在需要帮助用户准备一个关于如何在面试中实现哈希表(HashMap)的问题。首先,我得回想一下哈希表的基本结构和关键点。哈希表通常由数组和链表(或红黑树)组成,用于处理哈希冲突。核心点包括哈希函数、冲突解决、动态扩容等。 用户可能是在准备技术面试,需要展示对数据结构的理解以及编码能力。面试官可能会期望看到清晰的代码结构,处理边界情况的能力,以及对时间复杂度的分析。因此,我需要确保回答涵盖这些方面。 首先,用户需要知道哈希表的基本组成部分:一个存储桶的数组,每个桶里可能是一个链表,用于处理哈希冲突。哈希函数的设计也很重要,通常可以使用内置的哈希函数然后取模。动态扩容是另一个关键点,当元素数量超过负载因子时,需要重新哈希,扩大数组大小,并重新分配元素。 接下来,考虑用户可能的疑问,比如如何处理冲突,开放寻址和链地址法的区别,负载因子的影响,以及扩容的具体步骤。同时,代码的实现要简洁,有良好的注释,并且能够通过测试用例。 可能需要比较Java中的HashMap实现,比如Java 8之后当链表长度超过阈值时会转为红黑树,但面试中可能不需要这么复杂,保持链表即可。但用户可能想知道这些优化点,可以提到但不需要深入。 另外,代码的可读性很重要,方法如put、get、remove需要清晰。测试用例部分要展示不同情况,如插入、删除、冲突处理、扩容等。 最后,总结关键点,强调哈希函数、冲突解决、扩容机制,以及时间和空间复杂度。这样用户能够全面展示他们的知识,并在面试中表现出色。</think>### 手写HashMap核心实现(Java版) --- #### **1. 数据结构设计** ```java class MyHashMap<K, V> { private static final int INIT_CAPACITY = 16; // 初始容量 private static final float LOAD_FACTOR = 0.75f; // 负载因子 // 哈希桶数组(链表节点) static class Node<K, V> { final K key; V value; Node<K, V> next; Node(K key, V value) { this.key = key; this.value = value; } } private Node<K, V>[] table; // 存储桶数组 private int size; // 当前元素数量 } ``` --- #### **2. 核心方法实现** **哈希函数设计:** ```java private int hash(K key) { if (key == null) return 0; int h = key.hashCode(); // 扰动函数:减少哈希碰撞 return h ^ (h >>> 16); } private int getIndex(K key, int length) { return (length - 1) & hash(key); // 代替取模运算 } ``` **PUT方法实现:** ```java public void put(K key, V value) { // 延迟初始化 if (table == null) { table = (Node<K, V>[]) new Node[INIT_CAPACITY]; } int index = getIndex(key, table.length); Node<K, V> head = table[index]; // 遍历链表 while (head != null) { if (head.key.equals(key)) { // 键已存在 head.value = value; // 更新值 return; } head = head.next; } // 创建新节点(头插法) Node<K, V> newNode = new Node<>(key, value); newNode.next = table[index]; table[index] = newNode; size++; // 扩容检查 if (size > table.length * LOAD_FACTOR) { resize(); } } ``` **GET方法实现:** ```java public V get(K key) { if (table == null) return null; int index = getIndex(key, table.length); Node<K, V> node = table[index]; while (node != null) { if (node.key.equals(key)) { return node.value; } node = node.next; } return null; } ``` --- #### **3. 动态扩容机制** ```java private void resize() { Node<K, V>[] oldTable = table; int newCap = oldTable.length << 1; // 容量翻倍 Node<K, V>[] newTable = (Node<K, V>[]) new Node[newCap]; // 重新哈希所有元素 for (Node<K, V> head : oldTable) { while (head != null) { Node<K, V> next = head.next; int newIndex = getIndex(head.key, newCap); // 头插法迁移节点 head.next = newTable[newIndex]; newTable[newIndex] = head; head = next; } } table = newTable; } ``` --- #### **4. 删除操作实现** ```java public V remove(K key) { if (table == null) return null; int index = getIndex(key, table.length); Node<K, V> prev = null; Node<K, V> curr = table[index]; while (curr != null) { if (curr.key.equals(key)) { if (prev == null) { // 删除头节点 table[index] = curr.next; } else { prev.next = curr.next; } size--; return curr.value; } prev = curr; curr = curr.next; } return null; } ``` --- ### **5. 关键问题解答(面试高频考点)** **Q1:如何处理哈希冲突?** - 链地址法(当前实现方案):将冲突元素组织成链表 - 开放寻址法:线性探测/二探测(适合小数据量场景) **Q2为什么选择2容量?** - 位运算替代取模运算:`hash & (n-1)` 比 `hash % n` 快10倍 - 保证哈希值分布均匀性 **Q3:时间复杂度分析** | 操作 | 平均情况 | 最坏情况 | |--------|---------|---------| | put/get | $O(1)$ | $O(n)$ | | remove | $O(1)$ | $O(n)$ | --- ### **6. 测试用例示例** ```java public static void main(String[] args) { MyHashMap<String, Integer> map = new MyHashMap<>(); // 基础功能测试 map.put("apple", 10); map.put("banana", 20); System.out.println(map.get("apple")); // 10 // 冲突测试 map.put("Aa", 100); // 与"BB"哈希值相同 map.put("BB", 200); System.out.println(map.get("Aa")); // 100 // 扩容测试 for (int i = 0; i < 1000; i++) { map.put("key" + i, i); } System.out.println(map.get("key999")); // 999 // 删除测试 map.remove("apple"); System.out.println(map.get("apple")); // null } ``` --- ### **7. 优化方向建议** 1. **树化优化**:当链表长度>8时转为红黑树(参考Java8 HashMap2. **空键处理**:单独处理key=null的情况 3. **并发控制**:添加分段锁实现线程安全 4. **迭代器实现**:实现EntrySet的遍历功能 5. **哈希优化**:使用更好的扰动函数,如`hash = 31 * hash + key.hashCode()` --- **面试要点总结**: 1. 清晰解释哈希函数设计原理 2. 准确处理链表操作(插入/删除节点) 3. 理解扩容机制的时间成本 4. 分析不同场景下的时间复杂度 5. 能讨论各种优化方案的取舍
评论 6
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值