哈希桶,元素插入逻辑实现

一,知识串烧

1,哈希桶的核心结构:数组加链表(数组负责快速定位,链表负责解决哈希冲突);

2,节点设计:封装key,value,和next指针,支持链表式存储

3,插入流程:(put方法)计算下标➡️检查重复key➡️插入/更新节点

4,核心知识:通过哈希运算实现O(1)级别的查找/插入效率,用链表地址解决哈希冲突

二、完整代码实现(Java 版,可直接运行)

java 复制代码
import java.util.Objects;

// 哈希桶核心类(存储key-value键值对)
class HashBucket<K, V> {
    // 1. 内部节点类:封装key、value和下一个节点的引用
    static class Node<K, V> {
        K key;       // 键(唯一)
        V value;     // 值
        Node<K, V> next; // 指向下一个节点的指针

        // 节点构造方法:创建节点时初始化key和value
        public Node(K key, V value) {
            this.key = key;
            this.value = value;
            this.next = null; // 初始化为空,后续通过next关联其他节点
        }
    }

    // 2. 哈希桶成员变量
    private Node<K, V>[] array; // 核心结构:数组(每个元素是链表的头节点)
    private int usedSize;       // 记录当前有效元素个数(不含冲突节点的重复计数)
    private static final int DEFAULT_CAPACITY = 10; // 数组默认初始容量

    // 3. 构造方法:初始化哈希桶(创建数组)
    @SuppressWarnings("unchecked") // 抑制泛型数组转换警告
    public HashBucket() {
        this.array = new Node[DEFAULT_CAPACITY]; // 初始化数组,默认容量10
        this.usedSize = 0;
    }

    // 4. 核心方法:插入/更新元素(put操作)
    public void put(K key, V value) {
        // 步骤1:计算当前key对应的数组下标(哈希运算核心)
        int index = hash(key);

        // 步骤2:检查该下标对应的链表中是否存在重复key
        Node<K, V> cur = array[index]; // 从链表头节点开始遍历
        while (cur != null) {
            // 若key已存在:更新value(保证key唯一性)
            if (Objects.equals(cur.key, key)) { // 用Objects.equals避免空指针
                cur.value = value;
                return; // 更新完成,直接返回
            }
            cur = cur.next; // 未找到重复key,继续遍历下一个节点
        }

        // 步骤3:不存在重复key,插入新节点(头插法,效率更高)
        Node<K, V> newNode = new Node<>(key, value);
        newNode.next = array[index]; // 新节点的next指向原链表头
        array[index] = newNode;      // 数组该位置的头节点更新为新节点
        usedSize++; // 有效元素个数+1
    }

    // 辅助方法:计算key对应的数组下标(哈希函数)
    private int hash(K key) {
        if (key == null) {
            return 0; // 若key为null,默认存放在下标0位置
        }
        // 核心逻辑:key的哈希值 % 数组长度(会议中"k除以数组长度"实际为取模,保证下标在数组范围内)
        // 哈希值可能为负,用& Integer.MAX_VALUE转为正数
        return (key.hashCode() & Integer.MAX_VALUE) % array.length;
    }

    // 辅助方法:获取元素(用于测试验证)
    public V get(K key) {
        int index = hash(key);
        Node<K, V> cur = array[index];
        while (cur != null) {
            if (Objects.equals(cur.key, key)) {
                return cur.value;
            }
            cur = cur.next;
        }
        return null; // 未找到返回null
    }

    // 辅助方法:获取有效元素个数(用于测试验证)
    public int getUsedSize() {
        return usedSize;
    }

    // 测试主方法
    public static void main(String[] args) {
        HashBucket<String, Integer> hashBucket = new HashBucket<>();
        // 插入元素(包含重复key测试)
        hashBucket.put("语文", 90);
        hashBucket.put("数学", 95);
        hashBucket.put("英语", 88);
        hashBucket.put("数学", 98); // 重复key,更新value

        // 验证结果
        System.out.println("有效元素个数:" + hashBucket.getUsedSize()); // 输出3
        System.out.println("数学成绩:" + hashBucket.get("数学")); // 输出98(已更新)
        System.out.println("英语成绩:" + hashBucket.get("英语")); // 输出88
    }
}

三、重点讲解(必须掌握)

1. 哈希桶结构设计重点

  • 数组 + 链表的组合原因
    • 数组:支持通过下标快速定位(时间复杂度 O (1)),是哈希桶高效的核心;
    • 链表:解决「哈希冲突」(不同 key 计算出同一下标),无需移动元素,只需在链表末尾 / 头部插入。
  • 节点类设计
    • 必须包含key(唯一标识)、value(存储数据)、next(链表关联);
    • 用泛型<K, V>实现通用性,支持任意引用类型的 key 和 value。

2. 插入流程(put 方法)重点

  • 下标计算(hash 方法)
    • 核心公式:(key的哈希值 & 正数掩码) % 数组长度
    • 为什么不用直接除法?会议中 "k 除以数组长度" 实际是简化表述,取模(%)才能保证下标在[0, 数组长度-1]范围内,避免数组越界;
    • 处理 null key:单独判断,默认存放在下标 0,符合 HashMap 等框架的设计逻辑。
  • 重复 key 处理
    • 遍历对应下标链表,用Objects.equals(key1, key2)比较(避免空指针,且支持自定义 key 的 equals 重写);
    • 若存在重复 key,直接更新 value(而非新增节点),保证 key 的唯一性。
  • 节点插入方式
    • 采用「头插法」:新节点先指向原链表头,再更新数组的头节点引用;
    • 优势:无需遍历到链表末尾,插入效率 O (1)(尾插法需遍历链表,效率 O (n))。

3. 核心变量作用

  • usedSize:仅记录有效元素个数(重复 key 更新时不递增),为后续「扩容机制」提供依据(如负载因子达到阈值时扩容);
  • DEFAULT_CAPACITY:数组默认容量,可根据实际需求调整(通常设为 2 的幂,优化哈希运算)。

四、难点解析(易混淆 / 出错点)

1. 哈希冲突的理解与解决

  • 为什么会冲突?:不同 key 的哈希值经过取模后,可能得到相同的数组下标(如 key1 哈希值 15,key2 哈希值 25,数组长度 10,均得到下标 5);
  • 链地址法的优势:相比 "开放地址法"(如线性探测),链地址法不会导致元素扎堆,且插入 / 删除时无需移动其他元素,实现简单;
  • 易错点:忘记处理冲突,直接覆盖数组原有节点,导致数据丢失。

2. 链表遍历与边界处理

  • 空链表插入 :当数组下标对应的节点为null(该位置无元素),直接将新节点作为头节点存入数组;
  • 非空链表遍历 :必须遍历到cur == null才能确认无重复 key,否则可能遗漏中间节点的重复判断;
  • 易错点 :遍历链表时未判断cur.next == null,导致空指针异常。

3. key 的比较逻辑

  • 默认 equals 的局限性 :若 key 是自定义对象(如 User、Student),未重写equals()hashCode(),会默认比较对象地址,导致重复 key 无法被识别;
  • 解决方法 :自定义 key 类必须重写equals()(判断内容相等)和hashCode()(保证内容相等的对象哈希值相同),否则哈希桶无法正常工作。

4. 哈希函数的优化

  • 原始哈希值的问题 :key 的hashCode()可能为负数,直接取模会得到负下标,导致数组越界;
  • 解决方案 :用key.hashCode() & Integer.MAX_VALUE将哈希值转为正数(Integer.MAX_VALUE 是 0x7fffffff,二进制最高位为 0,与运算后最高位变为 0,即正数)。

五、了解部分(扩展知识)

1. 扩容机制

  • 触发条件 :当usedSize / 数组长度 ≥ 负载因子(默认负载因子 0.75),数组扩容为原来的 2 倍(通常设为 2 的幂,优化哈希取模运算);
  • 扩容流程:创建新数组→遍历原数组所有链表→重新计算每个节点的新下标→插入新数组(该过程称为 rehash);
  • 作用:减少哈希冲突,保证哈希桶的查询 / 插入效率(负载因子过高会导致链表过长,效率退化到 O (n))。

2. 其他冲突解决方式

  • 开放地址法:冲突时,按一定规则(如线性探测、二次探测)寻找数组中的下一个空位置;
  • 哈希再哈希法:冲突时,使用第二个哈希函数重新计算下标;
  • 公共溢出区法:将所有冲突的元素存入单独的溢出链表。

3. 实际应用场景

  • 哈希桶是HashMap(Java)、dict(Python)、unordered_map(C++)等容器的底层核心结构;
  • 适用于需要快速查找、插入、删除的场景(如缓存、数据库索引、用户信息存储等)。

4. 进阶优化

  • 红黑树转换:当链表长度超过阈值(如 HashMap 中默认 8),将链表转为红黑树,将查询效率从 O (n) 优化到 O (log n);
  • 扰动函数 :对 key 的哈希值进行多次异或和移位运算,减少哈希冲突(如 HashMap 的hash()方法)。

六, 扩容部分的代码

java 复制代码
import java.util.Objects;

// 哈希桶核心类(支持自动扩容,解决链表过长导致的效率退化问题)
class HashBucket<K, V> {
    // 内部节点类(不变)
    static class Node<K, V> {
        K key;
        V value;
        Node<K, V> next;

        public Node(K key, V value) {
            this.key = key;
            this.value = value;
            this.next = null;
        }
    }

    // 成员变量(新增负载因子常量)
    private Node<K, V>[] array;
    private int usedSize;
    private static final int DEFAULT_CAPACITY = 10; // 默认初始容量
    private static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认负载因子(扩容阈值)

    // 构造方法(不变)
    @SuppressWarnings("unchecked")
    public HashBucket() {
        this.array = new Node[DEFAULT_CAPACITY];
        this.usedSize = 0;
    }

    // 核心方法:插入/更新元素(新增扩容触发逻辑)
    public void put(K key, V value) {
        // 步骤1:计算下标
        int index = hash(key);

        // 步骤2:检查重复key,存在则更新
        Node<K, V> cur = array[index];
        while (cur != null) {
            if (Objects.equals(cur.key, key)) {
                cur.value = value;
                return;
            }
            cur = cur.next;
        }

        // 步骤3:头插法插入新节点
        Node<K, V> newNode = new Node<>(key, value);
        newNode.next = array[index];
        array[index] = newNode;
        usedSize++;

        // 步骤4:判断是否需要扩容(核心新增逻辑)
        // 负载因子 = 有效元素个数 / 数组长度,达到阈值则扩容
        if (loadFactor() >= DEFAULT_LOAD_FACTOR) {
            resize(); // 扩容为原容量的2倍
        }
    }

    // 辅助方法:计算负载因子(新增)
    private float loadFactor() {
        // 强制转为float,避免整数除法(如7/10=0,而非0.7)
        return (float) usedSize / array.length;
    }

    // 核心扩容方法(新增)
    @SuppressWarnings("unchecked")
    private void resize() {
        // 1. 创建新数组:容量为原数组的2倍(通常设为2的幂,优化哈希取模)
        Node<K, V>[] newArray = new Node[array.length * 2];

        // 2. 重新哈希(rehash):遍历原数组所有节点,迁移到新数组
        for (int i = 0; i < array.length; i++) {
            Node<K, V> cur = array[i]; // 原数组当前下标对应的链表头

            // 遍历当前链表的所有节点
            while (cur != null) {
                // 关键:保存当前节点的下一个节点(避免迁移时链表断裂)
                Node<K, V> nextNode = cur.next;

                // 3. 计算当前节点在新数组中的新下标(基于新数组长度取模)
                int newIndex = hash(cur.key, newArray.length);

                // 4. 头插法将当前节点插入新数组对应的链表
                cur.next = newArray[newIndex];
                newArray[newIndex] = cur;

                // 5. 移动到下一个节点继续迁移
                cur = nextNode;
            }
        }

        // 6. 替换原数组引用:新数组成为哈希桶的核心存储结构
        this.array = newArray;
    }

    // 重载hash方法:支持传入自定义数组长度(用于扩容时计算新下标)
    private int hash(K key, int newArrayLength) {
        if (key == null) {
            return 0;
        }
        // 基于新数组长度取模,保证下标在新数组范围内
        return (key.hashCode() & Integer.MAX_VALUE) % newArrayLength;
    }

    // 原hash方法(用于普通插入/查询时计算下标)
    private int hash(K key) {
        return hash(key, array.length); // 复用重载方法,默认传入当前数组长度
    }

    // 辅助方法:获取元素(不变)
    public V get(K key) {
        int index = hash(key);
        Node<K, V> cur = array[index];
        while (cur != null) {
            if (Objects.equals(cur.key, key)) {
                return cur.value;
            }
            cur = cur.next;
        }
        return null;
    }

    // 辅助方法:获取有效元素个数(不变)
    public int getUsedSize() {
        return usedSize;
    }

    // 辅助方法:获取当前数组容量(新增,用于测试验证扩容结果)
    public int getCurrentCapacity() {
        return array.length;
    }

    // 测试主方法(新增扩容验证逻辑)
    public static void main(String[] args) {
        HashBucket<String, Integer> hashBucket = new HashBucket<>();

        // 测试1:初始状态验证
        System.out.println("初始容量:" + hashBucket.getCurrentCapacity()); // 输出10
        System.out.println("初始负载因子:" + (float) hashBucket.getUsedSize() / hashBucket.getCurrentCapacity()); // 0.0

        // 测试2:插入元素触发扩容(默认负载因子0.75,10*0.75=7,插入第8个元素时扩容)
        hashBucket.put("语文", 90);
        hashBucket.put("数学", 95);
        hashBucket.put("英语", 88);
        hashBucket.put("物理", 92);
        hashBucket.put("化学", 89);
        hashBucket.put("生物", 91);
        hashBucket.put("历史", 85); // 第7个元素,负载因子7/10=0.7(未达阈值)
        System.out.println("插入7个元素后容量:" + hashBucket.getCurrentCapacity()); // 仍为10

        hashBucket.put("地理", 87); // 第8个元素,负载因子8/10=0.8(超过0.75,触发扩容)
        System.out.println("插入8个元素后容量:" + hashBucket.getCurrentCapacity()); // 输出20(扩容为2倍)

        // 测试3:验证扩容后元素可正常获取(rehash成功)
        System.out.println("地理成绩:" + hashBucket.get("地理")); // 输出87
        System.out.println("数学成绩:" + hashBucket.get("数学")); // 输出95
        System.out.println("有效元素个数:" + hashBucket.getUsedSize()); // 输出8(无数据丢失)

        // 测试4:扩容后继续插入,验证负载因子控制
        hashBucket.put("政治", 86);
        hashBucket.put("信息技术", 93);
        System.out.println("扩容后插入2个元素,负载因子:" + (float) hashBucket.getUsedSize() / hashBucket.getCurrentCapacity()); // 10/20=0.5(未达阈值)
    }
}
相关推荐
LYFlied7 小时前
【每日算法】LeetCode 153. 寻找旋转排序数组中的最小值
数据结构·算法·leetcode·面试·职场和发展
唐装鼠7 小时前
rust自动调用Deref(deepseek)
开发语言·算法·rust
ytttr8738 小时前
MATLAB基于LDA的人脸识别算法实现(ORL数据库)
数据库·算法·matlab
jianfeng_zhu9 小时前
整数数组匹配
数据结构·c++·算法
smj2302_796826529 小时前
解决leetcode第3782题交替删除操作后最后剩下的整数
python·算法·leetcode
LYFlied10 小时前
【每日算法】LeetCode 136. 只出现一次的数字
前端·算法·leetcode·面试·职场和发展
唯唯qwe-11 小时前
Day23:动态规划 | 爬楼梯,不同路径,拆分
算法·leetcode·动态规划
做科研的周师兄11 小时前
中国土壤有机质数据集
人工智能·算法·机器学习·分类·数据挖掘
来深圳11 小时前
leetcode 739. 每日温度
java·算法·leetcode
yaoh.wang11 小时前
力扣(LeetCode) 104: 二叉树的最大深度 - 解法思路
python·程序人生·算法·leetcode·面试·职场和发展·跳槽