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

一,知识串烧

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(未达阈值)
    }
}
相关推荐
裤裤兔1 小时前
利用matlab进行FDR校正的实现方式
数据结构·算法·matlab·多重比较矫正·校正·fdr
野蛮人6号1 小时前
力扣热题100道之31下一个排列
算法·leetcode·职场和发展
敲代码的嘎仔2 小时前
LeetCode面试HOT100——160. 相交链表
java·学习·算法·leetcode·链表·面试·职场和发展
吃着火锅x唱着歌2 小时前
LeetCode 454.四数相加II
算法·leetcode·职场和发展
敲代码的嘎仔2 小时前
LeetCode面试HOT100—— 206. 反转链表
java·数据结构·学习·算法·leetcode·链表·面试
自然语2 小时前
深度学习时代结束了,2025年开始只剩下轮廓
数据结构·人工智能·深度学习·学习·算法
海天一色y2 小时前
Leetcode07-整数反转
算法
im_AMBER2 小时前
Leetcode 66 几乎唯一子数组的最大和
数据结构·笔记·学习·算法·leetcode
繁华似锦respect2 小时前
C++ 自定义 String 类
服务器·开发语言·c++·哈希算法·visual studio