HashMap 哈希映射
本教程共 78 篇 · 第 38 篇 · 更新于 2026-08-08 · 约 9 分钟阅读
本节目标:用
HashMap<K, V>存键值对,学会创建、查询、遍历,并理解entry+or_insert在”有则改、无则插”场景下的妙用。
数组靠下标找值,HashMap 靠”键”找值。当你想用队伍名查积分、用用户名查资料时,它比数组顺手得多,平均查找速度是 O(1)。别的语言里它叫 map、字典、object,换汤不换药。
1-1 创建 HashMap
和 Vec 类似,用 new 创建再 insert 键值对。但有个不同:用 HashMap 得手动 use 引入,因为它没在 Rust 的 prelude(预导入)里——String 和 Vec 因为太常用,被自动引入了,HashMap 得自己写。所谓 prelude,就是不用写 use 就自动能用的那一小撮名字;HashMap 不在其中,所以你每次都得自己从 std::collections 请它出来。
use std::collections::HashMap;
let mut my_gems = HashMap::new();
my_gems.insert("红宝石", 1);
my_gems.insert("蓝宝石", 2);
编译器能推出类型是 HashMap<&str, i32>。和别的集合一样,HashMap 也活在堆上,且所有键必须同类型、所有值必须同类型。
Tip预先知道大概要存多少对,用
HashMap::with_capacity(n)一次性预留空间,少做几次内存搬移,性能更好。
1-2 用迭代器 collect 创建
有时数据本来在别的结构里,比如一张积分表读进来是 Vec<(String, u32)>。笨办法是循环 insert,更 Rust 的写法是用迭代器加 collect:
use std::collections::HashMap;
let teams_list = vec![
("中国队".to_string(), 100),
("美国队".to_string(), 10),
("日本队".to_string(), 50),
];
let teams_map: HashMap<_, _> = teams_list.into_iter().collect();
println!("{:?}", teams_map);
into_iter 把列表变成迭代器,collect 把里面的元组 (K, V) 收集成 HashMap。注意类型标注 HashMap<_, _> 不能省——collect 能生成多种集合,编译器得靠这个标注才知道你想要 HashMap,具体的 K、V 交给它推导。
1-3 所有权会转移
HashMap 遵守普通的所有权规则:键或值若实现了 Copy(如整数),就复制进去;若没实现(如 String),所有权就被转移到 HashMap 里,原变量不能再用了。
use std::collections::HashMap;
let name = String::from("Sunface");
let age = 18;
let mut boys = HashMap::new();
boys.insert(name, age);
// println!("{name}"); // 报错:name 的所有权已转移
Warning如果把引用放进
HashMap,要保证人引用的那个值活得比HashMap久。一旦原值被释放,里面的引用就成了悬垂引用,编译器会在编译期拦下你。
1-4 查询与遍历
用 get 按 key 取值,返回 Option<&V>:查不到是 None,查到是 Some(&值)。
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
let team_name = String::from("Blue");
let score: Option<&i32> = scores.get(&team_name);
get 的参数是引用(&team_name),因为写在签名里的是 k: &Q。得益于 String 实现了 Borrow<str>,你传 &String 或 &str 都行。
想直接拿到值而不是引用,可以:
let score: i32 = scores.get(&team_name).copied().unwrap_or(0);
copied 把 &i32 复制成 i32,unwrap_or(0) 在查不到时给个默认值 0。
遍历所有键值对用 for:
for (key, value) in &scores {
println!("{key}: {value}");
}
Note
HashMap内部不保证顺序,遍历出来的先后和插入顺序未必一致,别依赖它。
1-5 更新值:覆盖与 entry
insert 同一个 key,新值会覆盖旧值,并返回旧值的 Option:
let old = scores.insert("Blue", 20);
assert_eq!(old, Some(10)); // 返回被覆盖的旧值
更常见的需求是”有就更新、没有就插入”。entry + or_insert 一句话搞定:
let v = scores.entry("Yellow").or_insert(5);
assert_eq!(*v, 5); // 没有,插入 5
let v = scores.entry("Yellow").or_insert(50);
assert_eq!(*v, 5); // 已经有了,50 没插进去
or_insert 返回的是指向值的可变引用 &mut V。拿到它就能直接改,不用先查再插。
1-6 一个综合例子:词频统计
统计一句话里每个词出现几次,是 entry 的经典用法:
use std::collections::HashMap;
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1; // 解引用后才能改
}
println!("{:?}", map);
每遇到一个词:若没见过,用这个词作 key、插入计数 0;若见过,取出已有的计数加一。or_insert 给的可变引用让我们就地修改,干净利落。
Warning初学者常漏掉
*count += 1里的解引用。这里count是&mut i32,直接count += 1类型对不上,必须先*count解引用再改。
1-7 什么样的类型能当键
想当 key,类型必须能比较相等,也就是实现 Eq 和 Hash 这两个 trait。常见整数、字符串都没问题。但 f32、f64 浮点数没实现 Eq(因为存在 NaN,它和谁都不等),所以不能用浮点数作 HashMap 的 key。
底层原理是:哈希函数把 key 算成一个哈希值,再用这个值来存和查。Rust 默认用 SipHash,安全性高但速度一般。若性能测试显示不够快,可去 crates.io 找 ahash 等更快的哈希实现替换。
1-8 HashMap 的两个坑:顺序与键
用 HashMap 时有两点必须记牢。第一,遍历顺序是不确定的。Rust 出于安全考虑,哈希表使用了随机化的哈希(SipHash),所以你每次运行程序、甚至同一程序不同进程里,迭代出来的键顺序都可能不一样,也绝对不是插入顺序、更不是排序顺序。任何”依赖顺序”的逻辑都是错的。
第二,键类型必须同时实现 Hash 和 Eq(可比较相等)。像 i32、String、bool 这些基础类型都满足;你自己定义的结构体要当键,就得手动 derive(Hash, Eq, PartialEq)。此外,键在被放进 HashMap 之后就不该再被修改——一旦改了参与哈希的字段,之后就找不回它了。
use std::collections::HashMap;
let mut m: HashMap<String, i32> = HashMap::new();
m.insert(String::from("a"), 1);
// 遍历顺序不保证是 a 先、b 后
for (k, v) in &m {
println!("{k}: {v}");
}
Warning别指望
HashMap有序。需要有序请用BTreeMap(按键排序,但键要能比较大小)。另外频繁插入前可用HashMap::with_capacity(n)预留容量,减少扩容。
1-9 entry API 再深入:计数与默认值
entry 是 HashMap 最高频的利器,尤其做”计数”和”找不到就给默认值”时。它的 or_insert 返回”这个值的可变引用”,如果键不存在就先插入默认值再返回:
use std::collections::HashMap;
let mut counts: HashMap<String, i32> = HashMap::new();
for word in ["a", "b", "a", "a", "b"] {
let key = word.to_string();
*counts.entry(key).or_insert(0) += 1; // 首次出现给 0,然后 +1
}
// counts: a->3, b->2
如果默认值的构造比较贵,用 or_insert_with(|| 贵的计算) 做”惰性”插入——只有真正需要默认值时才计算,避免白白浪费。
Tip做词频、做分组统计、做缓存,
entry().or_insert(...)几乎都是最干净写法。记住它返回的是&mut V,所以配合*解引用去修改值。
1-10 用 HashMap 做分组与倒排
除了计数,HashMap 还特别适合”按某个键把数据分堆”。比如你有一批学生,想按”班级”分组,就以学生号作值、班级名作键,用 entry(班级).or_insert_with(Vec::new) 拿到该班的列表再 push。这样一次遍历就得到”班级 → 学生列表”的映射。
另一个经典用法是”倒排索引”:正常是”文档 → 包含的词”,有时你需要反过来查”某个词出现在哪些文档”。做法是遍历每个文档的每个词,把词当键、文档编号收进 Vec 当值。这种”一对多”的关系,用 HashMap<K, Vec<V>> 表达最自然。
做这类聚合时要记住两点:键必须可哈希且相等比较稳定;往 Vec 里 push 不会改变键本身,所以不会影响查找。聚合类的需求(分组、计数、分类、索引),基本都能用 HashMap 加 entry 一把梭搞定。
Tip看到”按 X 归拢 Y""统计每个 X 有多少 Y""从 Y 反查 X”这类描述,第一时间就想到
HashMap配合entry().or_insert(...),几乎不会错。
1-11 更多常用操作
HashMap 还有一些高频方法,先认个脸熟:
use std::collections::HashMap;
let mut m: HashMap<String, i32> = HashMap::new();
m.insert("a".into(), 1);
println!("{}", m.contains_key("a")); // true:有没有这个键
println!("{}", m.len()); // 1:有多少对
println!("{}", m.is_empty()); // false
m.remove("a"); // 删除某个键及其值
println!("{}", m.contains_key("a")); // false
除了 entry(key).or_insert(默认),“有就改、没有就插”还有个更精致的写法——and_modify 配合 or_insert:
use std::collections::HashMap;
let mut m: HashMap<&str, i32> = HashMap::new();
m.entry("a")
.and_modify(|v| *v += 1) // 键存在时执行
.or_insert(1); // 键不存在时插入 1
第一次执行时 a 不存在,and_modify 里的闭包不触发,最终插入 1;第二次 a 已存在,and_modify 把它加一变成 2。一行把”存在则更新、不存在则初始化”写全了。
另外,当默认值就是类型的”零值”时,可以用 or_default() 偷懒,它会调用该类型的 Default 实现(比如 i32 的默认是 0、Vec 的默认是空向量):
let mut m: HashMap<&str, i32> = HashMap::new();
*m.entry("x").or_default() += 5; // 默认 0,加 5 后变成 5
Note初始化时若数据已经是”键-值”元组的集合,也可用
HashMap::from([(k1, v1), (k2, v2)])直接造,比一个个insert更紧凑。
1-12 小结
HashMap<K, V>存键值对,需use std::collections::HashMap,查找约 O(1);- 创建用
new+insert,或从迭代器collect而来(标注HashMap<_, _>); - 没实现
Copy的键/值,所有权会转移进HashMap; - 查询用
get返回Option<&V>,遍历用for (k, v); - 有则改无则插,用
entry(key).or_insert(默认); - 能当 key 的类型需实现
Eq+Hash,浮点数不行。
集合类型(Vec、String、HashMap)告一段落。下一章进入错误处理,先看不可恢复的 panic!。