HashMap 的理解

HashMap 的理解

HashMap 的实现思路

即将key-value 存在一个列表中,然后根据key 的计算出的hash值(散列值)作为这个其在列表中的index。
这样就可以通过 list[index] 直接读取这个key-value 了,而不需要列表所有元素。

这里有一个问题就是,如果多个key 计算出来的hash 值一样,那么就存在位置重复的问题。 像这样,不同的数据拥有相同散列值的情况,被称为“冲突”。 解决的方法一般有链地址法(chaining)和开放地址法 (open addressing)两种。
链地址法是将拥有相同散列值的元素存放在链表中,因此随着元素个数的增加,散列冲突和查询链表的时间也跟着增加,就造成了性能的损失。 不过作为优化,当链表很长可以转为 红黑树,改善查询效率。
开放地址法则是在遇到冲突时,再继续寻找一个新的数据存放空间(一般称为槽)。寻找空闲槽最简单的方法,就是按顺序遍历,直到找到空闲槽为止。

JAVA 中是采用 链地址法 解决冲突的。

无论如何,当冲突对查找效率产生的不利影响超过某一程度时,就会对表的大小进行修改,从而努力在平均水平上保持 O(1) 的查找效率。

public class MyHashMap<K, V> {
    private static class Node<K, V> {
        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 capacity = 16;
    private int size = 0;
    private final float loadFactor = 0.75f;

    @SuppressWarnings("unchecked")
    public MyHashMap() {
        table = (Node<K, V>[]) new Node[capacity];
    }

    private int hash(K key) {
        return (key == null) ? 0 : key.hashCode() & (capacity - 1);
    }

    public void put(K key, V value) {
        if (size >= capacity * loadFactor) {
            resize();
        }
        int index = hash(key);
        Node<K, V> newNode = new Node<>(key, value);

        if (table[index] == null) {
            table[index] = newNode;
        } else {
            Node<K, V> current = table[index];
            Node<K, V> prev = null;

            while (current != null) {
                if (current.key.equals(key)) {
                    current.value = value; // 更新
                    return;
                }
                prev = current;
                current = current.next;
            }

            // 未找到 key,插入到链表末尾
            prev.next = newNode;
        }
        size++;
    }

    public V get(K key) {
        int index = hash(key);
        Node<K, V> current = table[index];
        while (current != null) {
            if (current.key.equals(key)) {
                return current.value;
            }
            current = current.next;
        }
        return null;
    }

    private void resize() {
	    int oldCapacity = capacity;
	    // 检查是否达到最大容量
	    if (oldCapacity >= MAXIMUM_CAPACITY) {
	        threshold = Integer.MAX_VALUE;
	        return;
	    }
	    
	    // 计算新容量
	    int newCapacity = oldCapacity << 1;  // 双倍扩容
	    capacity = newCapacity;
	    
	    // 创建新表
	    Node<K, V>[] newTable = (Node<K, V>[]) new Node[newCapacity];
	    
	    // 重新哈希所有节点
	    for (int i = 0; i < oldCapacity; i++) {
	        Node<K, V> node = table[i];
	        if (node == null) continue;
	        
	        // 拆分为两个链表
	        Node<K, V> loHead = null, loTail = null;
	        Node<K, V> hiHead = null, hiTail = null;
	        
	        do {
	            Node<K, V> next = node.next;
	            // 判断节点应该留在原位置还是移动到新位置
	            if ((hash(node.key) & oldCapacity) == 0) {
	                if (loTail == null)
	                    loHead = node;
	                else
	                    loTail.next = node;
	                loTail = node;
	            } else {
	                if (hiTail == null)
	                    hiHead = node;
	                else
	                    hiTail.next = node;
	                hiTail = node;
	            }
	            node = next;
	        } while (node != null);
	        
	        // 将链表放入新表
	        if (loTail != null) {
	            loTail.next = null;
	            newTable[i] = loHead;  // 原位置
	        }
	        if (hiTail != null) {
	            hiTail.next = null;
	            newTable[i + oldCapacity] = hiHead;  // 新位置
	        }
	    }
	    table = newTable;
	}

}

Q&A

为什么计算key索引时还需要用hash值执行 & (capacity - 1)?

这个是因为hash 值很大,需要取模, 如 int index = hash(key) % capacity;
不过 & (capacity - 1) 的作用和 % capacity 在 capacity 是 2 的幂时是等价的,而且&运算更快。

假设 capacity = 16(10000 二进制),则:

hash(key) % 16 
= hash(key) & (16 - 1) 
= hash(key) & 0b1111

效果:只保留 hash(key) 的低 4 位,相当于取模 16。

为什么 HashMap 的容量必须是 2 的幂?

  1. & 运算替代%, 提高计算速度。

  2. 减少哈希冲突

如果容量不是 2 的幂,hash % capacity 可能导致某些位从未被使用,增加冲突概率。

  1. 扩容优化

16 -> 32 扩容的索引计算对比

旧索引:hash & (oldCap - 1) = hash & 0b01111(取低 4 位)。

新索引:hash & (newCap - 1) = hash & 0b11111(取低 5 位)。

关键发现:

新索引的二进制比旧索引 多 1 位(第 5 位)。

因此:

如果 hash 的第 5 位是 0,新索引 = 旧索引。

如果 hash 的第 5 位是 1,新索引 = 旧索引 + oldCap。

实现如下


if ((e.hash & oldCap) == 0) {
    // 保持原索引
    newTab[j] = e;
} else {
    // 新索引 = 原索引 + oldCap
    newTab[j + oldCap] = e;
}

这样的优点有

  1. 可以直接复用 hash 值,无需重新调用 hashCode()。

  2. 位运算代替取模

  3. 均匀分布:
    扩容后,原桶中的元素会 均匀分散到新桶(一半留在原位置,一半移动到 oldCap + 原位置)。

<think>好的,我现在需要给用户介绍哈希表(Hash table)。首先,我要确定用户的基础,可能是一个刚开始学习数据结构的新手,所以得从基本概念讲起,避免使用太专业的术语。 哈希表是什么?它是一种用于存储键值对的数据结构,通过哈希函数将键映射到存储位置,这样可以快速访问数据。需要解释清楚哈希函数的作用,比如把任意大小的数据转换成固定大小的值,这个值作为数组的索引。 然后要提到哈希表的优点,比如平均情况下O(1)的时间复杂度,这样查找、插入和删除都很快。但也要讲缺点,比如哈希冲突的可能性,以及如何解决冲突。常见的解决冲突的方法有两种:开放寻址法和链地址法。需要分别简单说明这两种方法的原理,比如开放寻址法是在冲突时寻找下一个空槽,而链地址法则是在每个位置维护一个链表。 接下来,可以举一个例子,比如用哈希表存储学生信息,键是学号,值是姓名或者其他信息。这样用户更容易理解应用场景。同时,可能需要提到哈希函数的设计很重要,不好的哈希函数会导致冲突增多,影响性能。 另外,负载因子(load factor)也是一个关键点,当负载因子超过一定阈值时,哈希表需要进行扩容,重新哈希,这会带来一定的开销。需要解释为什么扩容是必要的,以及扩容的过程。 还要考虑实际应用中的哈希表实现,比如在Python中的字典,Java的HashMap,这些都是哈希表的例子。这些实际例子能帮助用户联系到他们学过的知识。 最后,总结哈希表的适用场景和注意事项,比如适合快速查找,但内存消耗可能较大,设计好的哈希函数和处理冲突的方法很重要。可能用户还会想知道如何选择解决冲突的方法,可以简单比较两种方法的优缺点,比如链地址法在负载因子高时更有效,而开放寻址法可能更节省空间。 需要检查是否有遗漏的关键点,比如哈希表的时间复杂度在最好和最坏情况下的差异,比如最坏情况下是O(n),这时候需要平衡结构和算法设计。确保语言通俗易懂,结构清晰,分点说明,适当使用例子和比喻,帮助用户建立直观的理解。</think>哈希表(Hash Table)是一种高效的数据结构,用于存储键值对(Key-Value Pair)。它的核心思想是通过**哈希函数**将键(Key)快速映射到存储位置,从而实现快速的数据插入、删除和查询操作。 --- ### **核心组成** 1. **哈希函数(Hash Function)** 将任意大小的输入(如字符串、对象)转换为固定大小的整数值(哈希值)。例如: $$ \text{hash}(key) = \text{index} $$ 理想情况下,不同的键应映射到不同的索引,但实际可能存在冲突(不同键生成相同索引)。 2. **数组(Array)** 哈希表底层通常是一个数组,每个位置称为一个**桶(Bucket)**,存储键值对或链表的头节点(用于解决冲突)。 3. **冲突解决机制** - **开放寻址法(Open Addressing)**:冲突时,按某种规则(如线性探测)寻找下一个空桶。 - **链地址法(Chaining)**:每个桶指向一个链表,冲突的键值对存储在同一位置的链表中。 --- ### **工作流程** 1. **插入数据** - 计算键的哈希值 → 找到对应桶 → 若无冲突,直接存入;若有冲突,按冲突解决机制处理。 2. **查询数据** - 计算键的哈希值 → 定位到桶 → 若存在冲突,遍历链表或探测后续桶。 3. **删除数据** - 类似查询,找到键的位置后删除,并处理冲突结构的更新(如链表断开)。 --- ### **关键特性** - **时间复杂度** 平均情况下:插入、删除、查询均为 $O(1)$;最坏情况(所有键冲突)退化为 $O(n)$。 - **负载因子(Load Factor)** 定义:$\text{负载因子} = \frac{\text{已存键值对数量}}{\text{数组长度}}$ 通常设定阈值(如0.75),超过时触发扩容(Rehashing),即新建更大数组并重新映射所有键。 --- ### **优缺点** - **优点** - 高效的平均时间复杂度。 - 灵活适应动态数据规模(通过扩容)。 - 适合精确查找(如数据库索引、缓存系统)。 - **缺点** - 哈希函数设计不当会导致冲突频繁,性能下降。 - 遍历数据时效率低(需扫描整个数组)。 - 内存占用较高(需预留空桶减少冲突)。 --- ### **应用场景** 1. **字典(如Python的`dict`、Java的`HashMap`)** 2. **数据库索引** 3. **缓存系统(如Redis)** 4. **唯一性检测(如防止重复提交)** --- ### **示例** 假设用哈希表存储学生信息(学号为键,姓名为值): 1. 插入键`2023001`,哈希函数计算得到索引`3` → 存入数组位置3。 2. 查询键`2023001`时,直接定位到索引3获取值。 3. 若两个键映射到同一索引(冲突),使用链表或探测法处理。 --- ### **总结** 哈希表通过空间换时间的策略实现高效操作,核心在于哈希函数的设计和冲突解决机制。合理选择负载因子和扩容策略能显著提升性能,是实际开发中最常用的数据结构之一。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值