哈希表(HashMap)技术学习笔记

一、哈希表核心概念

1. 时间复杂度对比

  • 哈希表:查询、增删时间复杂度 O(1)(常数级)// 核心优势,依靠哈希直接定位
  • 普通数组/链表:查询时间复杂度 O(n) // 需要逐个遍历,数据量越大效率越低

2. 底层存储结构

哈希表底层采用 数组 + 链表 组合结构:

  1. 主体为节点数组,数组每个位置可存放K-V节点
  2. 存储形式:以键值对(K-V) 存储数据,数据统一封装为独立节点
  3. 核心流程:数据key → 计算hash值 → 映射数组下标 → 完成数据存储/查询

3. Hash算法与下标映射

  1. Hash算法规则
    • 相同key经过哈希算法,必然得到相同hash值
    • 不同key大概率得到不同hash值,存在哈希冲突(不同key算出相同hash值)// 哈希表必须解决的问题
  2. 下标计算规则
    通过 hash值 & 数组最大下标 运算得到数组存储下标 // 位运算效率远高于取模运算
  3. 冲突分类
    • 哈希冲突:不同key生成相同hash值
    • 下标映射冲突:不同hash值,经位运算后得到同一个数组下标

4. 冲突解决方案:拉链法

也叫链地址法,是本案例采用的冲突解决方式:

  1. 数组对应下标位置存入第一个节点
  2. 后续冲突节点,挂载到前一个节点的next引用上,形成单向链表
  3. 进阶优化:当链表长度达到 8 时,链表转换为红黑树(平衡二叉树) // 避免链表过长导致查询效率退化

5. 数组扩容机制

  1. 扩容触发条件:数组已使用容量达到总容量的 75%(负载因子0.75)
  2. 扩容规则:数组容量扩大为原长度2倍 ,数组长度始终保持 2^n // 保证位运算下标计算正常
  3. 扩容逻辑:扩容后所有原有节点需要重新计算下标,迁移至新数组

二、核心实体类:节点类 HNode

用于封装单个键值对数据,同时维护链表指针与hash值。

java 复制代码
package com05.dc.zyf0531;

/**
 * 哈希表节点类:封装K-V键值对,实现链表结构
 * @param <K> 键类型
 * @param <V> 值类型
 */
public class HNode<K, V> {
    K key;         // 键
    V value;       // 值
    HNode<K, V> next; // 链表下一个节点 // 实现拉链法的核心引用
    int hash;      // 当前key对应的hash值 // 提前存储,避免重复计算

    // 构造方法:初始化节点数据
    public HNode(K key, V value, int hash) {
        this.key = key;
        this.value = value;
        this.hash = hash;
    }
}

三、自定义哈希表实现类 ZHashMap

基于数组+链表实现简易HashMap,包含初始化、添加、扩容、查询核心方法。

java 复制代码
package com05.dc.zyf0531;

import java.util.Random;

/**
 * 自定义哈希表实现 底层:数组+单向链表
 * @param <K> 键泛型
 * @param <V> 值泛型
 */
public class ZHashMap<K, V> {
    HNode<K, V>[] table;  // 存储节点的底层数组 // 哈希表主体
    int length;           // 底层数组总长度
    int size;             // 哈希表中总元素个数
    int useArrSize;       // 数组中已被占用的下标位置数量
    // 数组默认初始容量 16,遵循 2^n 规则
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    int count;            // 重复key计数

    // 无参构造:初始化哈希表
    public ZHashMap() {
        length = DEFAULT_INITIAL_CAPACITY;
        table = new HNode[length];
        size = 0;
        useArrSize = 0;
    }

    /**
     * 根据key查询对应value
     * @param key 查找的键
     * @return 对应value
     */
    public V get(K key) {
        // 1. 计算hash值
        int hash = key.hashCode();
        // 2. 计算数组下标
        int index = hash & (length - 1);
        HNode<K, V> node = table[index];

        // 遍历当前下标下的链表
        while (node != null) {
            // 先比hash,再比key,避免哈希冲突误判
            if (node.hash == hash && (node.key == key || node.key.equals(key))) {
                return node.value;
            }
            node = node.next;
        }
        return null; // 未找到返回空
    }

    /**
     * 添加/修改键值对 核心方法
     * @param key 键
     * @param value 值
     */
    public void put(K key, V value) {
        // 1. 计算key的hash值
        int hash = key.hashCode();
        // 2. 位运算计算存储下标 hash & (数组长度-1)
        int index = hash & (length - 1);
        HNode<K, V> first = table[index];

        // 情况1:当前下标无节点,直接存入新节点
        if (first == null) {
            HNode<K, V> node = new HNode<>(key, value, hash);
            table[index] = node;
            size++;
            useArrSize++;
        } else {
            // 情况2:下标存在节点,遍历链表判断是否重复key
            // 先判断头节点是否为重复key
            if (first.hash == hash && (first.key == key || first.key.equals(key))) {
                first.value = value; // 覆盖旧值
                count++;
                System.out.println("相同数据计数:" + count);
            } else {
                HNode<K, V> temp = first;
                int flag = 0; // 标记是否找到重复key
                // 遍历链表后续节点
                while (temp.next != null) {
                    temp = temp.next;
                    if (temp.hash == hash && (temp.key == key || temp.key.equals(key))) {
                        temp.value = value;
                        count++;
                        System.out.println("相同数据计数:" + count);
                        flag = 1;
                        break;
                    }
                }
                // 链表遍历完毕,无重复key,追加新节点到链表尾部
                if (flag == 0) {
                    temp.next = new HNode<>(key, value, hash);
                    size++;
                }
            }
        }

        // 判断是否满足扩容条件:已用容量 >= 总容量*0.75
        if (length * 0.75 <= useArrSize) {
            resize();
            System.out.println("扩容 len:" + length);
        }
    }

    /**
     * 数组扩容方法:容量翻倍,重新迁移所有节点
     */
    public void resize() {
        // 位运算 <<1 等价于 *2,数组扩容2倍
        int newLength = length << 1;
        HNode<K, V>[] newTable = new HNode[newLength];
        int newUseArrSize = 0;
        int newSize = 0;

        // 遍历原数组所有下标
        for (int i = 0; i < length; i++) {
            HNode<K, V> first = table[i];
            if (first != null) {
                // 遍历当前下标下的整条链表
                while (first != null) {
                    // 记录原节点下一引用,防止断链
                    HNode<K, V> oldNext = first.next;
                    // 重新计算新数组下标
                    int nindex = first.hash & (newLength - 1);
                    HNode<K, V> temp = newTable[nindex];

                    if (temp == null) {
                        // 新下标无节点,直接放入
                        newTable[nindex] = new HNode<>(first.key, first.value, first.hash);
                        newUseArrSize++;
                    } else {
                        // 新下标存在节点,遍历到链表尾部追加
                        while (temp.next != null) {
                            temp = temp.next;
                        }
                        temp.next = new HNode<>(first.key, first.value, first.hash);
                        newSize++;
                    }
                    // 遍历原链表下一个节点
                    first = oldNext;
                }
            }
        }
        // 替换底层数组,更新属性
        length = newLength;
        useArrSize = newUseArrSize;
        table = newTable;
        size = newSize;
        System.out.println("扩容后: 数组长度:" + length + " 数组使用长度:" + useArrSize + " 元素个数:" + size);
    }

    // 测试主方法
    public static void main(String[] args) {
        Random ran = new Random();
        ZHashMap<String, Integer> map = new ZHashMap<>();
        // 批量插入10万条数据测试
        for (int i = 0; i < 100000; i++) {
            map.put("hello" + i, i);
        }
        System.out.println(map.size);
        System.out.println(map.useArrSize);
        System.out.println(map.count);
    }
}

四、关键流程总结

  1. 新增数据流程

    计算key哈希值 → 位运算求数组下标 → 判断下标是否为空 → 空则直接存节点;非空则遍历链表 → 存在重复key则更新value,无则链表尾部追加节点 → 判断是否触发扩容。

  2. 扩容流程

    负载因子达到0.75触发扩容 → 数组容量翻倍 → 遍历原数组及所有链表节点 → 每个节点重新计算下标并迁移至新数组 → 替换底层数组。

  3. 冲突处理流程

    出现下标冲突时,使用拉链法构建链表;链表长度过长(≥8)会转为红黑树,保证查询效率。

五、重点知识点备注

  1. 下标计算使用 hash & (length - 1),仅当数组长度为 2^n 时运算等效于取模,效率更高。
  2. 判断key是否相同时,先比较hash值,再比较key ,减少equals调用次数,提升效率。
  3. 负载因子0.75是时间与空间的平衡值:过小浪费空间,过大加剧哈希冲突。
  4. 扩容是耗时操作,会重新迁移所有数据,合理初始容量可减少扩容次数。
相关推荐
生而为虫1 小时前
[学习记录] 幼儿学习拼音html游戏
学习·游戏
AOwhisky1 小时前
MySQL 学习笔记(第四期):SQL 语言之多表查询
linux·运维·网络·数据库·笔记·学习·mysql
xian_wwq2 小时前
【学习笔记】「大模型安全:攻击面演化史」第 07 篇-安全左移
人工智能·笔记·学习
秋雨梧桐叶落莳2 小时前
iOS——NSUserDefaults学习
学习·macos·ios·objective-c·cocoa
易小染3 小时前
AI-Agent学习-LangChain-01
学习·langchain
nnsix4 小时前
Unity 贴图压缩格式 笔记
笔记·unity·贴图
xian_wwq5 小时前
【学习笔记】「大模型安全:攻击面演化史」第 03 篇-数据投毒
笔记·学习·ai安全
sheeta19985 小时前
LeetCode 每日一题笔记 日期:2026.06.06 题目:2196. 根据描述创建二叉树
笔记·算法·leetcode
Chase_______6 小时前
【Java基础 | 15】集合框架(中):Set、HashSet、TreeSet 与哈希表
java·windows·散列表