用拉链法实现哈希表

简介: 本文详解哈希表中拉链法的实现原理,通过简化版到完整版代码,介绍如何用链表解决哈希冲突。支持泛型、动态扩容缩容、键值增删查改及遍历所有key,结合Java内置LinkedList优化实现,直观展示哈希表核心机制。

前文 哈希表核心原理 中我介绍了哈希表的核心原理和几个关键概念,其中提到了解决哈希冲突的方法主要有两种,分别是拉链法和开放寻址法(也常叫做线性探查法):
本文就来具体介绍一下拉链法的实现原理和代码。
拉链法的简化版实现
哈希表核心原理 已经介绍过哈希函数和 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 数组的大小在创建哈希表时就固定,不考虑负载因子和动态扩缩容的问题。
简化版代码
这里直接给出简化版的代码实现,你可以先看看,后面将通过可视化面板展示增删查改的过程
有了上面的铺垫,我们现在来看一个比较完善的 Java 代码实现,主要新增了以下几个功能:
1、使用了泛型,可以存储任意类型的 key 和 value。
2、底层的 table 数组会根据负载因子动态扩缩容。
3、使用了 哈希表基础 中提到的 hash 函数,利用 key 的 hashCode() 方法和 table.length 来计算哈希值。
4、实现了 keys() 方法,可以返回哈希表中所有的 key。
为了方便,我直接使用 Java 标准库的 LinkedList 作为链表,这样就不用自己手动处理增删链表节点的操作了。当然如果你愿意的话,可以参照前文 单/双链表代码实现 自己编写操作 KVNode 的逻辑。
Java
运行代码
复制代码
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import java.util.LinkedList;
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);
相关文章
|
4天前
|
Java API
用链表实现队列/栈
本文介绍如何用链表实现栈和队列,利用双链表头尾操作均为O(1)的特性,通过调用LinkedList API高效实现。栈可选头部或尾部作栈顶,队列同理,只需调整增删位置。文末引出数组实现队列的性能问题,启发优化思考。
|
4天前
|
存储 API 索引
队列/栈基本原理 ❗前置知识
本文介绍队列和栈两种“操作受限”的数据结构:队列遵循先进先出(FIFO),只能队尾入、队头出;栈遵循先进后出(FILO),仅在栈顶进行增删操作。二者底层多由数组或链表实现,核心API包括push、pop、peek和size,是后续复杂数据结构的基础。
|
4天前
|
Java 索引 容器
单/双链表代码实现
本文详解双链表与单链表的 MyLinkedList 实现,重点介绍三个关键优化:1)同时持有头尾节点引用,提升尾部操作效率;2)使用虚拟头尾节点简化边界处理;3)解析链表删除中的内存泄露误区,并强调指针置空的良好编程习惯。
|
3天前
|
存储 缓存 算法
学习数据结构和算法的框架思维
本文系统总结数据结构与算法本质:所有数据结构皆源于数组和链表,核心操作为遍历与访问;算法本质是穷举,关键在于无遗漏、无冗余。文章提炼出通用框架,帮助读者建立计算机思维,掌握高效解题方法,适合初学者建立全局观,也适合进阶者温故知新。
|
3天前
|
缓存 网络协议 算法
核心原理:能否画张图解释下 RPC 的通信流程?
RPC(远程过程调用)是一种实现分布式系统间通信的技术,它让调用远程服务像调用本地方法一样简单。本文深入浅出地讲解了RPC的定义、核心目标、通信流程及在微服务架构中的关键作用,帮助开发者理解其底层原理,掌握如何通过动态代理、序列化、协议设计等机制屏蔽网络复杂性,提升开发效率与系统可维护性。
|
3天前
|
消息中间件 Kubernetes 网络协议
别老想着怎么用好 RPC 框架,你得多花时间琢磨原理
2011年加入京东,亲历技术演进,现任技术架构部首席架构师。主导微服务、消息中间件等核心系统研发,深耕分布式架构。课程涵盖RPC基础、进阶与高级实战,带你掌握网络通信核心,构建高效可靠分布式系统。(238字)
|
3天前
|
算法 Java 索引
双指针技巧秒杀七道数组题目
本文介绍双指针技巧在数组和链表中的应用,重点解析快慢指针如何实现原地修改。通过LeetCode经典题如删除有序数组/链表重复项,展示如何用慢指针记录结果、快指针遍历数据,高效完成去重,时间复杂度O(N),避免频繁数据搬移。
|
3天前
|
算法
双指针技巧秒杀七道链表题目
本文总结单链表七大技巧:合并有序链表、链表分解、合并K个有序链表、找倒数第k个节点、找中点、判断环及起点、判断相交及交点,均基于双指针思想,涵盖LeetCode多道经典题目,助你系统掌握链表算法核心。
|
3天前
|
JSON 前端开发 Java
另外几个接口文档
提供班级与学员信息管理功能,支持班级列表分页查询、添加、修改、删除及详情查看,同时支持学员信息条件查询,涵盖基本信息、班级关联、学历等字段,便于高效管理教学资源。
|
3天前
|
Java
多叉树的递归/层序遍历
多叉树是二叉树的扩展,节点可有多个子节点。遍历方式与二叉树类似,DFS无中序位置,BFS通过队列实现,支持按层遍历并记录深度,代码结构清晰,易于扩展。