用拉链法实现哈希表

简介: 本文详解哈希表中拉链法的实现原理,通过简化版与完整版Java代码,介绍如何用链表解决哈希冲突,支持泛型、动态扩容及增删查改操作,帮助深入理解哈希表底层机制。

前文 哈希表核心原理 中我介绍了哈希表的核心原理和几个关键概念,其中提到了解决哈希冲突的方法主要有两种,分别是拉链法和开放寻址法(也常叫做线性探查法):
本文就来具体介绍一下拉链法的实现原理和代码。
拉链法的简化版实现
哈希表核心原理 已经介绍过哈希函数和 key 的类型的关系,其中 hash 函数的作用是在 O(1)O(1) 的时间把 key 转化成数组的索引,而 key 可以是任意不可变的类型。
但是这里为了方便诸位理解,我先做如下简化:
1、我们实现的哈希表只支持 key 类型为 int,value 类型为 int 的情况,如果 key 不存在,就返回 -1。
2、我们实现的 hash 函数就是简单地取模,即 hash(key) = key % table.length。这样也方便模拟出哈希冲突的情况,比如当 table.length = 10 时,hash(1) 和 hash(11) 的值都是 1。
3、底层的 table 数组的大小在创建哈希表时就固定,不考虑负载因子和动态扩缩容的问题。
简化版代码
这里直接给出简化版的代码实现,你可以先看看,后面将通过可视化面板展示增删查改的过程
import java.util.LinkedList;

// 用拉链法解决哈希冲突的简化实现
public class ExampleChainingHashMap {

// 链表节点,存储 key-value 对儿
// 注意这里必须存储同时存储 key 和 value
// 因为要通过 key 找到对应的 value
static class KVNode {
    int key;
    int value;

    // 为了简化,我这里直接用标准库的 LinkedList 链表
    // 所以这里就不添加 next 指针了
    // 你当然可以给 KVNode 添加 next 指针,自己实现链表操作
    public KVNode(int key, int value) {
        this.key = key;
        this.value = value;
    }
}


// 底层 table 数组中的每个元素是一个链表
private LinkedList<KVNode>[] table;

public ExampleChainingHashMap(int capacity) {
    table = new LinkedList[capacity];
}

private int hash(int key) {
    return key % table.length;
}

// 查
public int get(int key) {
    int index = hash(key);

    if (table[index] == null) {
        // 链表为空,说明 key 不存在
        return -1;
    }

    LinkedList<KVNode> list = table[index];
    // 遍历链表,尝试查找目标 key,返回对应的 value
    for (KVNode node : list) {
        if (node.key == key) {
            return node.value;
        }
    }

    // 链表中没有目标 key
    return -1;
}

// 增/改
public void put(int key, int value) {
    int index = hash(key);

    if (table[index] == null) {
        // 链表为空,新建一个链表,插入 key-value
        table[index] = new LinkedList<>();
        table[index].add(new KVNode(key, value));
        return;
    }

    // 链表不为空,要遍历一遍看看 key 是否已经存在
    // 如果存在,更新 value
    // 如果不存在,插入新节点
    LinkedList<KVNode> list = table[index];
    for (KVNode node : list) {
        if (node.key == key) {
            // key 已经存在,更新 value
            node.value = value;
            return;
        }
    }

    // 链表中没有目标 key,添加新节点
    // 这里使用 addFirst 添加到链表头部或者 addLast 添加到链表尾部都可以
    // 因为 Java LinkedList 的底层实现是双链表,头尾操作都是 O(1) 的
    // https://labuladong.online/algo/data-structure-basic/linkedlist-implement/
    list.addLast(new KVNode(key, value));
}

// 删
public void remove(int key) {
    LinkedList<KVNode> list = table[hash(key)];
    if (list == null) {
        return;
    }

    // 如果 key 存在,则删除
    // 这个 removeIf 方法是 Java LinkedList 的方法,可以删除满足条件的元素,时间复杂度 O(N)
    list.removeIf(node -> node.key == key);
}

}
有了上面的铺垫,我们现在来看一个比较完善的 Java 代码实现,主要新增了以下几个功能:
1、使用了泛型,可以存储任意类型的 key 和 value。
2、底层的 table 数组会根据负载因子动态扩缩容。
3、使用了 哈希表基础 中提到的 hash 函数,利用 key 的 hashCode() 方法和 table.length 来计算哈希值。
4、实现了 keys() 方法,可以返回哈希表中所有的 key。
为了方便,我直接使用 Java 标准库的 LinkedList 作为链表,这样就不用自己手动处理增删链表节点的操作了。当然如果你愿意的话,可以参照前文 单/双链表代码实现 自己编写操作 KVNode 的逻辑。
import java.util.LinkedList;

public class MyChainingHashMap {

// 拉链法使用的单链表节点,存储 key-value 对
private static class KVNode<K, V> {
    K key;
    V value;
    // 因为我们使用了内置的 LinkedList 类,所以不用 next 指针
    // 不用我们自己实现链表的逻辑

    KVNode(K key, V val) {
        this.key = key;
        this.value = val;
    }
}

// 哈希表的底层数组,每个数组元素是一个链表,链表中每个节点是 KVNode 存储键值对
private LinkedList<KVNode<K, V>>[] table;

// 哈希表中存入的键值对个数
private int size;
// 底层数组的初始容量
private static final int INIT_CAP = 4;

public MyChainingHashMap() {
    this(INIT_CAP);
}

public MyChainingHashMap(int initCapacity) {
    size = 0;
    // 初始化哈希表
    table = (LinkedList<KVNode<K, V>>[]) new LinkedList[initCapacity];
    for (int i = 0; i < table.length; i++) {
        table[i] = new LinkedList<>();
    }
}

/***** 增/改 *****/

// 添加 key -> val 键值对
// 如果键 key 已存在,则将值修改为 val
public void put(K key, V val) {
    if (key == null) {
        throw new IllegalArgumentException("key is null");
    }
    LinkedList<KVNode<K, V>> list = table[hash(key)];
    // 如果 key 之前存在,则修改对应的 val
    for (KVNode<K, V> node : list) {
        if (node.key.equals(key)) {
            node.value = val;
            return;
        }
    }
    // 如果 key 之前不存在,则插入,size 增加
    list.add(new KVNode<>(key, val));
    size++;

    // 如果元素数量超过了负载因子,进行扩容2N
    if (size >= table.length * 0.75) {
        resize(table.length * 2);
    }
}

/***** 删 *****/

// 删除 key 和对应的 val
public void remove(K key) {
    if (key == null) {
        throw new IllegalArgumentException("key is null");
    }
    LinkedList<KVNode<K, V>> list = table[hash(key)];
    // 如果 key 存在,则删除,size 减少
    for (KVNode<K, V> node : list) {
        if (node.key.equals(key)) {
            list.remove(node);
            size--;

            // 缩容,当负载因子小于 0.125 时,缩容
            if (size <= table.length / 8) {
                resize(table.length / 4);
            }
            return;
        }
    }
}

/***** 查 *****/

// 返回 key 对应的 val,如果 key 不存在,则返回 null
public V get(K key) {
    if (key == null) {
        throw new IllegalArgumentException("key is null");
    }
    LinkedList<KVNode<K, V>> list = table[hash(key)];
    for (KVNode<K, V> node : list) {
        if (node.key.equals(key)) {
            return node.value;
        }
    }
    return null;
}

// 返回所有 key
public List<K> keys() {
    List<K> keys = new LinkedList<>();
    for (LinkedList<KVNode<K, V>> list : table) {
        for (KVNode<K, V> node : list) {
            keys.add(node.key);
        }
    }
    return keys;
}

/***** 其他工具函数 *****/

public int size() {
    return size;
}

// 哈希函数,将键映射到 table 的索引
private int hash(K key) {
    return (key.hashCode() & 0x7fffffff) % table.length;
}

private void resize(int newCap) {
    // 构造一个更大容量的 HashMap
    MyChainingHashMap<K, V> newMap = new MyChainingHashMap<>(newCap);
    // 穷举当前 HashMap 中的所有键值对
    for (LinkedList<KVNode<K, V>> list : table) {
        for (KVNode<K, V> node : list) {
            // 将键值对转移到新的 HashMap 中
            newMap.put(node.key, node.value);
        }
    }
    // 将当前 HashMap 的底层 table 换掉
    this.table = newMap.table;
}

public static void main(String[] args) {
    MyChainingHashMap<Integer, Integer> map = new MyChainingHashMap<>();
    map.put(1, 1);
    map.put(2, 2);
    map.put(3, 3);
    System.out.println(map.get(1)); // 1
    System.out.println(map.get(2)); // 2

    map.put(1, 100);
    System.out.println(map.get(1)); // 100

    map.remove(2);
    System.out.println(map.get(2)); // null

    System.out.println(map.keys()); // [1, 3](顺序可能不同)
}

}

相关文章
|
23天前
|
监控 应用服务中间件 API
Agentic 应用时代,Dify 全链路可观测最佳实践
本文讲述 Dify 平台在 Agentic 应用开发中面临的可观测性挑战,从开发者与运维方双重视角出发,系统分析了当前 Dify 可观测能力的现状、局限与改进方向
334 18
Agentic 应用时代,Dify 全链路可观测最佳实践
|
1月前
|
存储 人工智能 自然语言处理
阿里云 Elasticsearch 的 AI 革新:高性能、低成本、智能化的搜索新纪元
本文介绍了数智化浪潮下, 阿里云 Elasticsearch 打通了 云原生内核优化、RAG 闭环方案、云原生推理平台 三大能力模块,实现了从底层到应用的全链路升级,助力企业构建面向未来的智能搜索中枢。
367 22
|
26天前
|
SQL JSON 分布式计算
【跨国数仓迁移最佳实践6】MaxCompute SQL语法及函数功能增强,10万条SQL转写顺利迁移
本系列文章将围绕东南亚头部科技集团的真实迁移历程展开,逐步拆解 BigQuery 迁移至 MaxCompute 过程中的关键挑战与技术创新。本篇为第六篇,MaxCompute SQL语法及函数功能增强。 注:客户背景为东南亚头部科技集团,文中用 GoTerra 表示。
235 20
|
10天前
|
人工智能 安全 开发者
解构AI时代的“深圳答案”:以硬实力构建“护城河”
2025年,深圳以“昇腾+光明实验室+华为”协同模式,打造国产AI算力生态。不同于追逐应用热点,深圳聚焦底层突破,构建从芯片到应用的全栈自主链条,通过政企联动、产学研协同,形成“技术攻关—场景验证—迭代优化”闭环,推动算力高效利用与产业深度融合,为全球AI发展提供安全可控的“中国方案”。
79 15
|
26天前
|
机器学习/深度学习 人工智能 算法
PAIFuser:面向图像视频的训练推理加速框架
阿里云PAI推出PAIFuser框架,专为视频生成模型设计,通过模型并行、量化优化、稀疏运算等技术,显著提升DiT架构的训练与推理效率。实测显示,推理耗时最高降低82.96%,训练时间减少28.13%,助力高效低成本AI视频生成。
191 22
|
19天前
|
SQL 关系型数据库 MySQL
MySQL从入门到精通:系统性学习路径
“MySQL从入门到精通”系统梳理了从基础到高阶的完整学习路径,涵盖安装配置、SQL语法、数据库设计、事务锁机制、性能优化、主从复制及分库分表等核心内容,结合实战任务帮助开发者由浅入深掌握MySQL,助力成为数据库高手。
145 13
|
19天前
|
前端开发 数据挖掘
精准类目+关键词布局,让1688商品快速获得曝光!
本文详解1688商品曝光提升策略,涵盖精准类目选择、关键词优化、流量获取及展现位竞争。通过科学布局关键词、完善商品信息、提升服务质量,助力商家精准触达客户,实现曝光与转化双增长。
|
8天前
|
JavaScript 定位技术 API
Trilium Notes:构建个人知识库的开源神器
Trilium Notes 是一款开源、免费的个人知识管理系统,支持树状结构、笔记克隆、富文本与 Markdown、代码高亮、加密笔记、思维导图及 REST API。可本地部署,跨平台同步,助力构建“第二大脑”,适合学习、研发与创意写作。
145 8
 Trilium Notes:构建个人知识库的开源神器
|
14天前
|
人工智能 安全 调度
一文详解容器服务面向大模型和 AI Agent 的技术变革
在生成式人工智能迅猛发展的浪潮下,企业应用正加速从模型研究走向业务落地。无论是大规模的数据处理、超大参数模型的训练与推理,还是部署能够自动完成任务的 AI Agent,这些场景都需要稳定、高效且可弹性伸缩的资源调度与管理能力。容器凭借环境一致性、跨平台部署和高效调度等优势,天然契合 AI 场景对多样化算力、快速迭代和规模化分发的要求,成为 AI 时代事实上的原生基石。然而,要满足在生产规模下的需求,产品及技术形态需随之演进。
169 3
|
26天前
|
SQL 分布式计算 大数据
【跨国数仓迁移最佳实践8】MaxCompute Streaming Insert:大数据数据流写业务迁移的实践与突破
本系列文章将围绕东南亚头部科技集团的真实迁移历程展开,逐步拆解 BigQuery 迁移至 MaxCompute 过程中的关键挑战与技术创新。本篇为第八篇,MaxCompute Streaming Insert:大数据数据流写业务迁移的实践与突破。 注:客户背景为东南亚头部科技集团,文中用 GoTerra 表示。
266 38