Hello 算法:哈希表从 O(1) 查询到冲突与扩容的完整解析 📅 发布时间:2026/9/7 4:55:30 👁 浏览次数: Hello 算法哈希表从 O(1) 查询到冲突与扩容的完整解析【免费下载链接】hello-algo《Hello 算法》动画图解、一键运行的数据结构与算法教程。支持简中、繁中、English、日本語提供 Python, Java, C, C, C#, JS, Go, Swift, Rust, Ruby, Kotlin, TS, Dart 等代码实现项目地址: https://gitcode.com/GitHub_Trending/he/hello-algo本文基于《Hello 算法》hello-algo仓库的 哈希表文档展开系统讲解哈希表的核心概念、14 种语言的常用操作 API、基于数组的简单实现原理以及哈希冲突与扩容机制。读完后你将掌握用 Python、Java、C、Go 等语言使用内置哈希表的全部基本操作理解index hash(key) % capacity这行核心公式背后的原理并能读懂仓库中从 Python 到 C 的完整ArrayHashMap源码实现。什么是哈希表哈希表hash table又称散列表通过建立键key与值value之间的映射实现高效的元素查询向哈希表中输入一个键key即可在 $O(1)$ 时间内获取对应的值value。文档用一个贴近生活的例子说明其动机给定 $n$ 个学生每个学生有“姓名”和“学号”两项数据。若希望实现“输入学号返回姓名”的查询数组和链表都需要遍历所有元素而哈希表可以把学号直接映射到存储位置一步命中。三种结构的查询效率对比如下原文档表元素查询效率对比| | 数组 | 链表 | 哈希表 | | -- | -- | -- | -- | | 查找元素 | $O(n)$ | $O(n)$ | $O(1)$ | | 添加元素 | $O(1)$ | $O(1)$ | $O(1)$ | | 删除元素 | $O(n)$ | $O(n)$ | $O(1)$ |添加元素数组链表只需把元素加到尾部$O(1)$查询元素数组链表是乱序的需要遍历所有元素$O(n)$删除元素需先查询到元素再删除$O(n)$。结论很直接在哈希表中进行增删查改的时间复杂度都是 $O(1)$这就是它成为最常用数据结构之一的根本原因。常用操作初始化、添加、查询、删除哈希表的常见操作包括初始化、添加键值对、查询键对应的值、删除键值对。以文档中“学号 → 姓名”的数据为例各语言的标准写法如下与 codes/java/chapter_hashing/hash_map.java、codes/cpp/chapter_hashing/hash_map.cpp 等示例源码一致。Python# 初始化哈希表 hmap: dict {} # 添加操作在哈希表中添加键值对 (key, value) hmap[12836] 小哈 hmap[15937] 小啰 hmap[16750] 小算 hmap[13276] 小法 hmap[10583] 小鸭 # 查询操作向哈希表中输入键 key得到值 value name: str hmap[15937] # 删除操作在哈希表中删除键值对 (key, value) hmap.pop(10583)Java/* 初始化哈希表 */ MapInteger, String map new HashMap(); /* 添加操作 */ map.put(12836, 小哈); map.put(15937, 小啰); map.put(16750, 小算); map.put(13276, 小法); map.put(10583, 小鸭); /* 查询操作 */ String name map.get(15937); /* 删除操作 */ map.remove(10583);C/* 初始化哈希表 */ unordered_mapint, string map; /* 添加操作 */ map[12836] 小哈; map[15937] 小啰; map[16750] 小算; map[13276] 小法; map[10583] 小鸭; /* 查询操作 */ string name map[15937]; /* 删除操作 */ map.erase(10583);Go/* 初始化哈希表 */ hmap : make(map[int]string) /* 添加操作 */ hmap[12836] 小哈 hmap[15937] 小啰 hmap[16750] 小算 hmap[13276] 小法 hmap[10583] 小鸭 /* 查询操作 */ name : hmap[15937] /* 删除操作 */ delete(hmap, 10583)JavaScript/* 初始化哈希表 */ const map new Map(); /* 添加操作 */ map.set(12836, 小哈); map.set(15937, 小啰); map.set(16750, 小算); map.set(13276, 小法); map.set(10583, 小鸭); /* 查询操作 */ let name map.get(15937); /* 删除操作 */ map.delete(10583);Rust注意其get/remove返回Option来表达“键可能不存在”use std::collections::HashMap; /* 初始化哈希表 */ let mut map: HashMapi32, String HashMap::new(); /* 添加操作 */ map.insert(12836, 小哈.to_string()); map.insert(15937, 小啰.to_string()); map.insert(16750, 小算.to_string()); map.insert(13279, 小法.to_string()); map.insert(10583, 小鸭.to_string()); /* 查询操作 */ let _name: OptionString map.get(15937); /* 删除操作 */ let _removed_value: OptionString map.remove(10583);其余语言在仓库 codes 目录下均有对应示例文件四种基本操作可按下表速查| 语言 | 初始化 | 添加 | 查询 | 删除 | | -- | -- | -- | -- | -- | | C# |Dictionaryint, string map new();|map[12836] 小哈;|map[15937]|map.Remove(10583);| | Swift |var map: [Int: String] [:]|map[12836] 小哈|map[15937]!|map.removeValue(forKey: 10583)| | Dart |Mapint, String map {};|map[12836] 小哈;|map[15937]|map.remove(10583);| | TypeScript |new Mapnumber, string()|map.set(12836, 小哈)|map.get(15937)|map.delete(10583)| | Kotlin |HashMapInt, String()|map[12836] 小哈|map[15937]|map.remove(10583)| | Ruby |hmap {}|hmap[12836] 小哈|hmap[15937]|hmap.delete(10583)| | C | 无内置哈希表需自行实现 | — | — | — |值得注意的细节C 语言未提供内置哈希表因此仓库 codes/c/chapter_hashing 目录下的示例如 array_hash_map.c全部基于数组手工实现。C# 的Dictionary还允许在初始化时直接写字面量{ 12836, 小哈 }, ...TS/JS 示例中还会用console.info打印添加、删除前后的表内容以便观察。三种遍历方式键值对、键、值哈希表有三种常用的遍历方式遍历键值对、单独遍历键、单独遍历值。以 Java 为例与 hash_map.java 中的“遍历哈希表”段一致/* 遍历键值对 key-value */ for (Map.EntryInteger, String kv : map.entrySet()) { System.out.println(kv.getKey() - kv.getValue()); } /* 单独遍历键 key */ for (int key : map.keySet()) { System.out.println(key); } /* 单独遍历值 value */ for (String val : map.values()) { System.out.println(val); }其他语言的等价写法# Pythonitems() / keys() / values() for key, value in hmap.items(): print(key, -, value) for key in hmap.keys(): print(key) for value in hmap.values(): print(value)// Gorange 直接解包键值对 for key, value : range hmap { fmt.Println(key, -, value) } for key : range hmap { fmt.Println(key) } for _, value : range hmap { fmt.Println(value) }// JS / TypeScriptentries() / keys() / values() for (const [k, v] of map.entries()) { console.info(k - v); } for (const k of map.keys()) { console.info(k); } for (const v of map.values()) { console.info(v); }// Rust对 map 迭代得到 (键引用, 值引用) for (key, value) in map { println!({key} - {value}); } for key in map.keys() { println!({key}); } for value in map.values() { println!({value}); }C 支持范围 for 与迭代器两种写法kv.first/kv.second分别取键和值Dart 用map.forEach((key, value) {...})及map.keys/map.valuesRuby 用hmap.entries.each/hmap.keys.each/hmap.values.each。完整多语言版本见原文档 hash_map.md 中的代码选项卡。基于数组的简单实现桶与哈希函数理解哈希表最快的方式是看它最简单的形态仅用一个数组来实现。在哈希表中数组中的每个空位称为桶bucket每个桶可存储一个键值对。查询操作就是找到key对应的桶并在桶中获取value。那么如何基于key定位对应的桶这由**哈希函数hash function**完成。哈希函数的作用是把一个较大的输入空间映射到一个较小的输出空间输入空间是所有key输出空间是所有桶数组索引。也就是说输入一个key通过哈希函数就能得到该键值对在数组中的存储位置。其计算分两步通过某种哈希算法hash()计算得到哈希值将哈希值对桶数量数组长度capacity取模得到对应的桶索引index。index hash(key) % capacity设capacity 100、hash(key) key恒等哈希则哈希函数就是key % 100。以学号为key、姓名为value12836 % 100 36即存入第 36 号桶。仓库中给出了 Python、Java、C、C 等多语言实现。Python 版 array_hash_map.py 是理解原理的最佳入口其核心代码将key和value封装成Pair类哈希表本体ArrayHashMap只持有一个 100 长度的桶数组class Pair: 键值对 def __init__(self, key: int, val: str): self.key key self.val val class ArrayHashMap: 基于数组实现的哈希表 def __init__(self): # 初始化数组包含 100 个桶 self.buckets: list[Pair | None] [None] * 100 def hash_func(self, key: int) - int: 哈希函数 index key % 100 return index def get(self, key: int) - str | None: 查询操作 index: int self.hash_func(key) pair: Pair self.buckets[index] if pair is None: return None return pair.val def put(self, key: int, val: str): 添加和更新操作 pair Pair(key, val) index: int self.hash_func(key) self.buckets[index] pair def remove(self, key: int): 删除操作 index: int self.hash_func(key) # 置为 None代表删除 self.buckets[index] None def entry_set(self) - list[Pair]: 获取所有键值对 result: list[Pair] [] for pair in self.buckets: if pair is not None: result.append(pair) return result几个值得注意的实现细节对应源码 array_hash_map.pyhash_func就是文档公式的直接落地key % 100get与put的开销只是“算一次取模 一次数组下标访问”这正是 $O(1)$ 查询的来源remove是把桶置空None而非“擦除数组元素”——数组长度不可变桶位只是被标记为可用entry_set/key_set/value_set需要线性扫描 100 个桶遍历复杂度为 $O(n)$与单次 $O(1)$ 查询并不矛盾。同样的设计在 C 语言中体现为显式内存管理array_hash_map.c 中hashFunc(key)同样返回key % MAX_SIZEMAX_SIZE为 100put用malloc为新Pair及其字符串值分配内存removeItem与析构函数delArrayHashMap负责逐桶free。Java 版 array_hash_map.java 则用ListPair初始化 100 个null桶接口与 Python 版一一对应hashFunc/get/put/remove/pairSet/keySet/valueSet。对比这几份源码可以清楚看到各语言 API 形态不同但“桶数组 取模定位”的内核完全一致。哈希冲突与扩容从本质上看哈希函数把“所有key构成的输入空间”映射到“数组所有索引构成的输出空间”而输入空间往往远大于输出空间。因此理论上一定存在“多个输入对应相同输出”的情况。以上述key % 100为例只要两个key的后两位相同输出就相同12836 % 100 36 20336 % 100 36两个学号指向了同一个桶同一个姓名这显然是错的。这种“多个输入对应同一输出”的情况称为哈希冲突hash collision。缓解冲突最直观的手段是扩容哈希表容量 $n$ 越大多个key被分配到同一个桶的概率就越低。扩容前键值对(136, A)和(236, D)发生冲突扩容到更大容量后冲突即消失。但扩容代价不菲原因有二类似数组扩容需把所有键值对从原哈希表迁移至新哈希表由于capacity改变index hash(key) % capacity的结果全部变化必须重新计算每个键值对的存储位置rehash。为此编程语言通常会预留足够大的哈希表容量防止频繁扩容。衡量“该不该扩容”的指标是负载因子load factor元素数量除以桶数量用于衡量哈希冲突的严重程度也常作为扩容触发条件。例如在 Java 中当负载因子超过 $0.75$ 时系统会把哈希表扩容至原先的 $2$ 倍。小结与延伸阅读本文完整覆盖了哈希表文档的核心内容哈希表相对数组/链表的 $O(1)$ 增删查优势、14 种语言的初始化/添加/查询/删除/遍历写法、桶 index hash(key) % capacity的数组式实现以及冲突产生与扩容机制。可以继续从以下仓库文件深入内置哈希表完整示例codes/python/chapter_hashing/built_in_hash.py、codes/java/chapter_hashing/built_in_hash.java、codes/cpp/chapter_hashing/built_in_hash.cpp冲突解决拉链法/开放寻址法同章文档 hash_collision.md 及源码 hash_map_chaining.java、hash_map_open_addressing.java哈希函数设计除留数法、直接定址法、乘法散列法等同章文档 hash_algorithm.md 及 simple_hash.py本章习题与总结exercises.md、summary.md。【免费下载链接】hello-algo《Hello 算法》动画图解、一键运行的数据结构与算法教程。支持简中、繁中、English、日本語提供 Python, Java, C, C, C#, JS, Go, Swift, Rust, Ruby, Kotlin, TS, Dart 等代码实现项目地址: https://gitcode.com/GitHub_Trending/he/hello-algo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考