哈希映射(Hash Map)
最后更新
哈希映射存储键值对,让你平均以 O(1) 按键查到对应的值。它的工作方式和哈希表完全一样,只是每个桶中的条目同时存着键和它对应的值。存入或取出一个键值对时,先对键做哈希得到桶的下标,再在这个桶的链上查找匹配的键。点击上方的“播放”,观察键值对如何按哈希桶放置,以及如何按键取出一个值。
Python 的 dict、Java 的 HashMap 以及 JavaScript 的 Map 和对象背后都是这种结构。冲突的处理方式和哈希表相同(这里用的是拉链法),所以性能取决于好的哈希函数和较低的负载因子。
时间复杂度
| 操作 | 平均 | 最坏情况 |
|---|---|---|
| Put(插入或更新) | O(1) | O(n) |
| Get(查找) | O(1) | O(n) |
| 删除 | O(1) | O(n) |
| 空间 | O(n) | O(n) |
常用语言中的哈希映射
| 语言 | 类型 |
|---|---|
| Python | dict |
| Java | HashMap |
| JavaScript | Map / 对象 |
| C++ | std::unordered_map |
| Go | map |
示例演算
用 hash(key) % 8 把键值对 ("cat", 3)、("dog", 5)、("cat", 9)、("emu", 7) 插入一个有 8 个桶的映射。假设 hash("cat") % 8 = 2、hash("dog") % 8 = 5、hash("emu") % 8 = 2:
| 步骤 | 结构 | 操作 |
|---|---|---|
Put ("cat", 3) | 桶 2:[("cat", 3)] | 哈希到桶 2;链为空,所以追加这个键值对。 |
Put ("dog", 5) | 桶 2:[("cat", 3)],桶 5:[("dog", 5)] | 哈希到桶 5;链为空,所以追加这个键值对。 |
Put ("cat", 9) | 桶 2:[("cat", 9)],桶 5:[("dog", 5)] | 哈希到桶 2;键 "cat" 已在链中,所以把它的值更新为 9。 |
Put ("emu", 7) | 桶 2:[("cat", 9), ("emu", 7)],桶 5:[("dog", 5)] | 哈希到桶 2;与 "cat" 冲突,没有找到这个键,所以追加这个键值对。 |
Get "emu" | 桶 2:[("cat", 9), ("emu", 7)] | 哈希到桶 2;扫描链,跳过 "cat",匹配到 "emu",返回 7。 |
什么时候使用哈希映射
| 适合使用的情况 | 不适合使用的情况 |
|---|---|
需要按精确的键进行平均 O(1) 的查找、插入和删除。 | 需要让键保持有序或做范围查询,应使用平衡 BST 或有序映射。 |
| 键可哈希,并且相等的定义清晰(字符串、整数、元组)。 | 键是可变的,插入后可能改变,这会让它落在错误的桶里。 |
| 要计数、去重、做缓存,或按 ID 建立索引。 | 需要最坏情况 O(log n) 的保证,而不是均摊的平均上界。 |
| 不关心迭代顺序,或者按插入顺序保存的映射就够用。 | 内存非常紧张,桶、负载因子的预留空间和指针都会增加开销。 |
Hash Map 代码
简洁、可运行的 Hash Map 实现,提供 Python, JavaScript, Java, C++, C 版本。选择一种语言,复制代码,或在 Coddy 在线编辑器中直接打开。
Hash Map 的 Python 代码
1# Python's built-in dict is a hash map: O(1) average2# insert, lookup, and delete.3inventory = {"apple": 3, "banana": 7}4
5# Insert and update6inventory["cherry"] = 57inventory["apple"] += 28
9# Lookup, with .get for a safe default on missing keys10print("apple: ", inventory["apple"])11print("mango: ", inventory.get("mango", 0))12
13# Membership test and delete14print("banana in stock:", "banana" in inventory)15del inventory["banana"]16print("banana in stock:", "banana" in inventory)17
18# Iterate over key-value pairs19for fruit, count in sorted(inventory.items()):20 print(f"{fruit}: {count}")21
22# Classic hash map use case: counting frequencies23words = "the quick brown fox jumps over the lazy dog the end".split()24freq = {}25for word in words:26 freq[word] = freq.get(word, 0) + 127
28print("Occurrences of the:", freq["the"])29print("Most common word: ", max(freq, key=freq.get))Hash Map 的 JavaScript 代码
1// The built-in Map is the idiomatic hash map in JavaScript:2// any key type, insertion-ordered iteration, O(1) average lookups.3const ages = new Map();4
5ages.set("Alice", 30);6ages.set("Bob", 25);7ages.set("Carol", 35);8ages.set("Bob", 26); // set on an existing key updates the value9
10console.log("Bob:", ages.get("Bob"));11console.log("Has Dave?", ages.has("Dave"));12console.log("Size:", ages.size);13
14ages.delete("Carol");15console.log("Size after delete:", ages.size);16
17for (const [name, age] of ages) {18 console.log(`${name} is ${age}`);19}20
21console.log("Keys:", [...ages.keys()].join(", "));Hash Map 的 Java 代码
1import java.util.HashMap;2import java.util.Map;3
4public class Main {5 public static void main(String[] args) {6 Map<String, Integer> ages = new HashMap<>();7 ages.put("Alice", 30);8 ages.put("Bob", 25);9 ages.put("Carol", 35);10
11 System.out.println("Bob -> " + ages.get("Bob"));12 System.out.println("contains Carol: " + ages.containsKey("Carol"));13 System.out.println("Dave or default: " + ages.getOrDefault("Dave", -1));14
15 // getOrDefault makes frequency counting a one-liner16 String[] words = {"apple", "banana", "apple", "cherry", "apple"};17 Map<String, Integer> counts = new HashMap<>();18 for (String w : words) {19 counts.put(w, counts.getOrDefault(w, 0) + 1);20 }21 System.out.println("apple count: " + counts.get("apple"));22
23 ages.remove("Bob");24 System.out.println("size after remove: " + ages.size());25
26 for (Map.Entry<String, Integer> entry : ages.entrySet()) {27 System.out.println(entry.getKey() + " is " + entry.getValue());28 }29 }30}Hash Map 的 C++ 代码
1#include <algorithm>2#include <iostream>3#include <string>4#include <unordered_map>5#include <vector>6
7int main() {8 std::unordered_map<std::string, int> ages;9
10 // Insert and update11 ages["alice"] = 30; // operator[] inserts or overwrites12 ages.insert({"bob", 25}); // insert keeps an existing value13 ages.emplace("carol", 41);14 ages["alice"] = 31;15
16 // Lookup17 auto it = ages.find("bob");18 if (it != ages.end()) {19 std::cout << "bob is " << it->second << "\n";20 }21 std::cout << "has dave: " << ages.count("dave") << "\n";22
23 // Remove24 ages.erase("carol");25
26 // Iterate (sort keys first: unordered_map has no defined order)27 std::vector<std::string> keys;28 for (const auto& entry : ages) keys.push_back(entry.first);29 std::sort(keys.begin(), keys.end());30 for (const std::string& name : keys) {31 std::cout << name << " = " << ages[name] << "\n";32 }33 std::cout << "size: " << ages.size() << "\n";34 return 0;35}Hash Map 的 C 代码
1#include <stdbool.h>2#include <stdio.h>3#include <stdlib.h>4#include <string.h>5
6#define BUCKETS 167
8typedef struct Node {9 char key[24];10 int value;11 struct Node* next;12} Node;13
14// A small string -> int hash map with a reusable put/get/remove API15typedef struct {16 Node* buckets[BUCKETS];17 int size;18} HashMap;19
20unsigned int hashKey(const char* key) {21 unsigned int h = 5381;22 for (; *key; key++) h = h * 33 + (unsigned char)*key;23 return h % BUCKETS;24}25
26void mapPut(HashMap* map, const char* key, int value) {27 unsigned int i = hashKey(key);28 for (Node* n = map->buckets[i]; n != NULL; n = n->next) {29 if (strcmp(n->key, key) == 0) {30 n->value = value;31 return;32 }33 }34 Node* n = malloc(sizeof(Node));35 strcpy(n->key, key);36 n->value = value;37 n->next = map->buckets[i];38 map->buckets[i] = n;39 map->size++;40}41
42bool mapGet(const HashMap* map, const char* key, int* out) {43 for (Node* n = map->buckets[hashKey(key)]; n != NULL; n = n->next) {44 if (strcmp(n->key, key) == 0) {45 *out = n->value;46 return true;47 }48 }49 return false;50}51
52void mapRemove(HashMap* map, const char* key) {53 Node** link = &map->buckets[hashKey(key)];54 while (*link != NULL) {55 if (strcmp((*link)->key, key) == 0) {56 Node* old = *link;57 *link = old->next;58 free(old);59 map->size--;60 return;61 }62 link = &(*link)->next;63 }64}65
66int main(void) {67 HashMap map = {0};68 mapPut(&map, "alice", 30);69 mapPut(&map, "bob", 25);70 mapPut(&map, "carol", 41);71 mapPut(&map, "alice", 31); // updates the existing key72 int age;73 if (mapGet(&map, "bob", &age)) printf("bob is %d\n", age);74 printf("has dave: %s\n", mapGet(&map, "dave", &age) ? "yes" : "no");75 mapRemove(&map, "carol");76 printf("size: %d\n", map.size);77 return 0;78}哈希映射常见问题
哈希映射和哈希表有什么区别?
它们本质上是同一种结构:一个用键的哈希值来定位的桶数组。在日常用法中,“哈希表”常指一组键或这种技术本身,而“哈希映射”强调存储的是键值对。有些语言还会在线程安全上加以区分(例如 Java 的 Hashtable 和 HashMap),但核心算法完全相同。
哈希映射的时间复杂度是多少?
O(1)。在所有键都冲突到同一个桶的极端情况下,它们会退化为 O(n),所以扩容和好的哈希函数很重要。两个键哈希到同一个桶时,哈希映射怎么处理?
它会解决冲突。这个可视化使用拉链法:每个桶存一个小列表,新的键值对如果哈希到这里,就追加到这个列表中。查找时,映射会在这条短链上扫描匹配的键。另一种技术是开放寻址法,它会在数组中探测另一个空位。
哈希映射和二叉搜索树,该用哪个?
O(1),就用哈希映射。如果需要键有序、范围查询,或查找前驱和后继,就用平衡二叉搜索树(如 TreeMap 或 std::map),这些操作的代价是 O(log n)。BST 牺牲一点速度,换来哈希映射无法提供的顺序保证。什么是负载因子?为什么它会触发扩容?
0.75)时扩容,通常把桶数翻倍,并重新哈希所有条目。扩容是 O(n),但很少发生,均摊下来,平均操作仍保持 O(1)。可以用列表这样的可变对象作为哈希映射的键吗?
list 做键时 Python 会抛出 TypeError: unhashable type。如果在插入后修改了键,它的哈希值就会变化,映射再也无法在正确的桶里找到它,这个条目就悄无声息地丢失了。请使用字符串、数字或元组等不可变的键。