Rust: Rust 集合:HashMap 与 HashSet 详解
最后更新:2026-08-26
HashMap 和 HashSet 是 Rust 标准库中最常用的基于哈希的集合——HashMap 存储键值对映射,HashSet 存储不重复的元素集合。
如果说 Vec 是"按顺序存东西",那 HashMap 就是"按名字找东西"——你不需要记住索引,只需要知道键。而 HashSet 则是"有没有这个东西"的终极答案。
1. 你将学到
- 使用
HashMap::new()、insert、get操作键值对 - 使用
entryAPI 优雅地处理"插入或更新"逻辑 - 理解 HashMap 的所有权规则(哪些类型可以做 key/value)
- 使用 HashSet 去重以及执行交集、并集、差集运算
- 遍历 HashMap 的多种方式
- 根据场景选择合适的集合类型(Vec vs HashMap vs HashSet)
2. 一个投票系统的故事
(1) 痛苦:用两个 Vec 存票数
Anna 正在开发一个班级投票系统,需要统计每位候选人的得票数。
最开始她用两个 Vec:
let mut candidates = Vec::new();
let mut votes = Vec::new();
candidates.push("Alice");
votes.push(0);
candidates.push("Bob");
votes.push(0);
// 给 Alice 投票
let pos = candidates.iter().position(|&c| c == "Alice").unwrap();
votes[pos] += 1;
// 查询 Bob 的票数
let pos = candidates.iter().position(|&c| c == "Bob").unwrap();
println!("Bob 的票数: {}", votes[pos]);
用两个并行的 Vec 管理数据,问题显而易见:维护两个 Vec 的同步很脆弱——增删候选人时很容易忘记更新另一个 Vec。而且查找候选人需要 O(n) 的线性搜索,候选人越多越慢。从代码可读性上看,
candidates[i]和votes[i]的关联关系是隐式的,新人很难理解。
(2) Rust HashMap 的方案
use std::collections::HashMap;
fn main() {
let mut votes = HashMap::new();
// 给候选人投票
*votes.entry("Alice").or_insert(0) += 1;
*votes.entry("Bob").or_insert(0) += 1;
*votes.entry("Alice").or_insert(0) += 1; // 再投 Alice
*votes.entry("Charlie").or_insert(0) += 1;
// 查询票数
for (candidate, count) in &votes {
println!("{}: {} 票", candidate, count);
}
// 查询特定候选人
println!("Alice 的票数: {}", votes.get("Alice").unwrap());
}
输出:
Alice: 2 票
Bob: 1 票
Charlie: 1 票
Alice 的票数: 2
HashMap 是一个键值对映射表:
key -> value。entryAPI 优雅地处理"如果不存在就插入默认值,如果存在就更新"的场景。get方法在 O(1) 时间复杂度内查找键。不用再维护两个同步的 Vec 了。
3. HashMap 与 HashSet 概览
(1) 概念图
graph TB
A[基于哈希的集合] --> B[HashMap<K, V>]
A --> C[HashSet<T>]
B --> B1[insert: 插入键值对]
B --> B2[get: 通过键取值]
B --> B3[entry: 优雅插入/更新]
B --> B4[remove: 删除键值对]
B --> B5[contains_key: 判断键是否存在]
B --> B6[iter: 遍历所有键值对]
C --> C1[insert: 添加元素]
C --> C2[contains: 判断是否包含元素]
C --> C3[union: 并集运算]
C --> C4[intersection: 交集运算]
C --> C5[difference: 差集运算]
C --> C6[symmetric_difference: 对称差集]
(2) 集合类型对比
| 特性 | Vec<T> | HashMap<K, V> | HashSet<T> |
|---|---|---|---|
| 存储 | 有序序列 | 无序键值对 | 无序不重复元素 |
| 查找 | O(n) 线性搜索 | O(1) 哈希查找 | O(1) 哈希查找 |
| 插入 | O(1) 末尾追加 | O(1) 平均 | O(1) 平均 |
| 去重 | 手动检查 | 键自动去重 | 元素自动去重 |
| 内存 | 低(连续存储) | 中(哈希表开销) | 中(哈希表开销) |
| 适用场景 | 按序访问、小数据量 | 键值映射、快速查找 | 集合运算、去重检查 |
(3) HashMap 常用方法速查
| 方法 | 返回类型 | 说明 |
|---|---|---|
insert(k, v) |
Option<V> |
插入键值对,返回旧值 |
get(&k) |
Option<&V> |
按键查找值 |
get_mut(&k) |
Option<&mut V> |
按键查找可变引用 |
remove(&k) |
Option<V> |
删除键值对,返回被删值 |
contains_key(&k) |
bool |
键是否存在 |
entry(k) |
Entry<K,V> |
获取 entry 做插入/更新 |
keys() |
Keys<K,V> |
遍历所有键 |
values() |
Values<K,V> |
遍历所有值 |
len() |
usize |
键值对数量 |
is_empty() |
bool |
是否为空 |
clear() |
() |
清空所有键值对 |
drain() |
Drain<K,V> |
移除并返回所有键值对 |
(4) HashSet 集合运算
| 运算 | 方法 | 数学符号 | 说明 |
|---|---|---|---|
| 并集 | union(&other) |
A ∪ B | 两集合所有元素 |
| 交集 | intersection(&other) |
A ∩ B | 两集合共有元素 |
| 差集 | difference(&other) |
A - B | 在 A 不在 B 的元素 |
| 对称差 | symmetric_difference(&other) |
A △ B | 只在一个集合中的元素 |
| 子集 | is_subset(&other) |
A ⊆ B | A 的元素全在 B 中 |
| 超集 | is_superset(&other) |
A ⊇ B | A 包含 B 的所有元素 |
4. HashMap 与 HashSet 示例
▶ 示例 1:HashMap 基础 API——投票统计系统(难度 ⭐⭐)
// ============================================
// 投票统计系统:展示 HashMap 基本 API
// ============================================
use std::collections::HashMap;
fn main() {
// Create a new empty HashMap
let mut vote_counts: HashMap<String, u32> = HashMap::new();
// --- insert ---
// Insert key-value pairs (overwrites existing value)
vote_counts.insert(String::from("Alice"), 0);
vote_counts.insert(String::from("Bob"), 0);
vote_counts.insert(String::from("Charlie"), 0);
println!("After initial insert:");
print_votes(&vote_counts);
// --- get ---
// Get a value by key (returns Option<&V>)
let alice_votes = vote_counts.get("Alice");
match alice_votes {
Some(count) => println!("Alice's votes (via get): {}", count),
None => println!("Alice not found"),
}
// --- entry API ---
// The idiomatic way: insert or update
// entry() returns an Entry enum, or_insert() inserts default if missing
println!("\n--- Voting round ---");
let candidates = ["Alice", "Bob", "Alice", "Charlie", "Alice", "Bob", "David"];
for name in &candidates {
let count = vote_counts.entry(String::from(*name)).or_insert(0);
*count += 1;
println!("Voted for {} (total: {})", name, count);
}
// --- contains_key ---
println!("\n--- Checking candidates ---");
for name in &["Alice", "David", "Eve"] {
if vote_counts.contains_key(*name) {
println!("{} is a candidate with {} votes", name, vote_counts.get(*name).unwrap());
} else {
println!("{} is NOT a candidate", name);
}
}
// --- len and is_empty ---
println!("\nTotal candidates: {}", vote_counts.len());
println!("Is empty: {}", vote_counts.is_empty());
// --- Final results ---
println!("\n--- Final Results ---");
print_votes(&vote_counts);
}
fn print_votes(votes: &HashMap<String, u32>) {
// Note: HashMap iteration order is NOT guaranteed
for (name, count) in votes {
println!(" {}: {} votes", name, count);
}
}
输出:
After initial insert:
Alice: 0 votes
Charlie: 0 votes
Bob: 0 votes
Alice's votes (via get): 0
--- Voting round ---
Voted for Alice (total: 1)
Voted for Bob (total: 1)
Voted for Alice (total: 2)
Voted for Charlie (total: 1)
Voted for Alice (total: 3)
Voted for Bob (total: 2)
Voted for David (total: 1)
--- Checking candidates ---
Alice is a candidate with 3 votes
David is a candidate with 1 votes
Eve is NOT a candidate
Total candidates: 4
Is empty: false
--- Final Results ---
Alice: 3 votes
Charlie: 1 votes
David: 1 votes
Bob: 2 votes
entry(key).or_insert(default)是 HashMap 最常用的惯用写法——如果键不存在,插入默认值并返回其引用;如果键存在,直接返回其引用。配合*count += 1可以一行完成"插入或累加"操作。get返回Option<&V>,永远不会 panic。
▶ 示例 2:HashMap 所有权规则与值类型(难度 ⭐⭐⭐)
// ============================================
// HashMap 所有权规则:什么类型能做 key/value
// ============================================
use std::collections::HashMap;
#[derive(Debug, Hash, Eq, PartialEq)]
struct ProductId(u32);
#[derive(Debug, Clone)]
struct Product {
name: String,
price: f64,
stock: u32,
}
fn main() {
// --- Rule 1: Owned types as keys ---
// String (owned) can be a key; &str (borrowed) needs lifetime management
let mut inventory: HashMap<String, Product> = HashMap::new();
let product = Product {
name: String::from("Rust Book"),
price: 29.99,
stock: 100,
};
// insert takes ownership of key and value
inventory.insert(String::from("RB-001"), product);
// println!("{:?}", product); // ❌ product was moved into the HashMap
// --- Rule 2: Inserting a reference ---
// Borrowed keys need lifetime annotations on the HashMap
// This works because the string literals have 'static lifetime
let mut lookup: HashMap<&str, u32> = HashMap::new();
lookup.insert("apple", 5);
lookup.insert("banana", 3);
println!("Lookup table: {:?}", lookup);
// --- Rule 3: Getting values returns references ---
// get() returns Option<&V>, not V
let stock_ref = inventory.get("RB-001");
match stock_ref {
Some(p) => println!("Product: {}, price: {}", p.name, p.price),
None => println!("Not found"),
}
// inventory is still valid (we only borrowed)
// --- Rule 4: Custom types as keys ---
// Keys must implement Eq + Hash
let mut product_map: HashMap<ProductId, String> = HashMap::new();
product_map.insert(ProductId(1), String::from("Laptop"));
product_map.insert(ProductId(2), String::from("Mouse"));
// --- Rule 5: Updating values with get_mut ---
// get_mut() returns Option<&mut V> for mutable access
if let Some(product) = inventory.get_mut("RB-001") {
product.stock -= 1; // Sell one unit
println!("Updated stock: {}", product.stock);
}
// --- Rule 6: The entry API for sophisticated updates ---
let mut word_count: HashMap<String, u32> = HashMap::new();
let text = "hello world hello rust hello again";
for word in text.split_whitespace() {
// or_insert returns &mut V, which we dereference and increment
let counter = word_count.entry(String::from(word)).or_insert(0);
*counter += 1;
}
println!("\nWord count: {:?}", word_count);
// Advanced: modify entry with and_modify + or_insert
let mut scores: HashMap<String, u32> = HashMap::new();
for team in &["red", "blue", "red", "green", "blue", "red"] {
scores.entry(String::from(*team))
.and_modify(|count| *count += 1) // if exists, increment
.or_insert(1); // if not, insert 1
}
println!("Scores: {:?}", scores);
}
输出:
Lookup table: {"banana": 3, "apple": 5}
Product: Rust Book, price: 29.99
Updated stock: 99
Word count: {"again": 1, "hello": 2, "rust": 1, "world": 1}
Scores: {"green": 1, "blue": 2, "red": 3}
HashMap 的所有权规则:插入时 key 和 value 的所有权转移给 HashMap。
get返回引用(&V),不会转移所有权。key 类型必须实现Eq + Hashtrait(基本类型和 String 默认实现了)。entry+and_modify+or_insert链式调用是 Rust 特有的优雅模式。
▶ 示例 3:HashSet 去重与集合运算(难度 ⭐⭐)
// ============================================
// HashSet:去重、交集、并集、差集运算
// ============================================
use std::collections::HashSet;
fn main() {
// --- Basic HashSet: deduplication ---
println!("--- HashSet Deduplication ---");
let mut unique_numbers: HashSet<i32> = HashSet::new();
let numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5];
for &n in &numbers {
unique_numbers.insert(n);
}
println!("Original: {:?}", &numbers[..]);
println!("Unique: {:?}", unique_numbers);
println!("Count: {} (original: {})", unique_numbers.len(), numbers.len());
// --- contains ---
println!("\n--- Contains Check ---");
for &n in &[1, 7, 9] {
if unique_numbers.contains(&n) {
println!("{} is in the set", n);
} else {
println!("{} is NOT in the set", n);
}
}
// --- Set operations ---
println!("\n--- Set Operations ---");
let set_a: HashSet<i32> = [1, 2, 3, 4, 5].iter().cloned().collect();
let set_b: HashSet<i32> = [4, 5, 6, 7, 8].iter().cloned().collect();
println!("Set A: {:?}", set_a);
println!("Set B: {:?}", set_b);
// Union: elements in A OR B
let union: HashSet<&i32> = set_a.union(&set_b).collect();
println!("Union (A ∪ B): {:?}", union);
// Intersection: elements in A AND B
let intersection: HashSet<&i32> = set_a.intersection(&set_b).collect();
println!("Intersection (A ∩ B): {:?}", intersection);
// Difference: elements in A but NOT in B
let diff_ab: HashSet<&i32> = set_a.difference(&set_b).collect();
println!("Difference (A - B): {:?}", diff_ab);
let diff_ba: HashSet<&i32> = set_b.difference(&set_a).collect();
println!("Difference (B - A): {:?}", diff_ba);
// Symmetric difference: elements in A or B but NOT both
let sym_diff: HashSet<&i32> = set_a.symmetric_difference(&set_b).collect();
println!("Symmetric Difference: {:?}", sym_diff);
// --- Practical example: finding common friends ---
println!("\n--- Practical: Common Friends ---");
let alice_friends: HashSet<&str> =
["Bob", "Charlie", "David", "Eve"].iter().cloned().collect();
let bob_friends: HashSet<&str> =
["Alice", "Charlie", "Eve", "Frank"].iter().cloned().collect();
println!("Alice's friends: {:?}", alice_friends);
println!("Bob's friends: {:?}", bob_friends);
// Mutual friends (intersection)
let mutual: HashSet<&&str> = alice_friends.intersection(&bob_friends).collect();
println!("Mutual friends: {:?}", mutual);
// Friends only Alice knows (difference)
let alice_only: HashSet<&&str> = alice_friends.difference(&bob_friends).collect();
println!("Only Alice knows: {:?}", alice_only);
// All unique friends (union)
let all_friends: HashSet<&&str> = alice_friends.union(&bob_friends).collect();
println!("All unique friends: {:?}", all_friends);
}
输出:
--- HashSet Deduplication ---
Original: [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
Unique: {3, 2, 1, 6, 4, 9, 5}
Count: 7 (original: 11)
--- Contains Check ---
1 is in the set
7 is NOT in the set
9 is in the set
--- Set Operations ---
Set A: {2, 3, 4, 5, 1}
Set B: {4, 7, 6, 5, 8}
Union (A ∪ B): {7, 2, 3, 6, 4, 5, 1, 8}
Intersection (A ∩ B): {4, 5}
Difference (A - B): {2, 3, 1}
Difference (B - A): {6, 7, 8}
Symmetric Difference: {1, 2, 3, 6, 7, 8}
--- Practical: Common Friends ---
Alice's friends: {"Charlie", "David", "Eve", "Bob"}
Bob's friends: {"Charlie", "Frank", "Alice", "Eve"}
Mutual friends: {"Charlie", "Eve"}
Only Alice knows: {"David", "Bob"}
All unique friends: {"Charlie", "David", "Frank", "Alice", "Eve", "Bob"}
HashSet 的四大集合运算:
union(并集——所有元素)、intersection(交集——共同元素)、difference(差集——A有B没有)、symmetric_difference(对称差集——不同时属于两者的元素)。这些方法返回迭代器,需要.collect()收集到新的 HashSet 中。
▶ 示例 4:遍历 HashMap 与集合选择策略(难度 ⭐⭐)
// ============================================
// 遍历 HashMap + 集合选择策略对比
// ============================================
use std::collections::HashMap;
fn main() {
// --- Build a sample dataset ---
let mut sales: HashMap<String, f64> = HashMap::new();
sales.insert(String::from("Laptop"), 1200.0);
sales.insert(String::from("Mouse"), 25.0);
sales.insert(String::from("Keyboard"), 80.0);
sales.insert(String::from("Monitor"), 350.0);
sales.insert(String::from("Headphones"), 150.0);
// --- Method 1: Iterate over key-value pairs ---
println!("--- All Products (iter) ---");
for (product, revenue) in &sales {
println!(" {}: ${:.2}", product, revenue);
}
// --- Method 2: Iterate over keys only ---
println!("\n--- Product Names (keys) ---");
for product in sales.keys() {
println!(" - {}", product);
}
// --- Method 3: Iterate over values only ---
println!("\n--- Revenue Values (values) ---");
let total: f64 = sales.values().sum();
println!(" Total revenue: ${:.2}", total);
println!(" Average: ${:.2}", total / sales.len() as f64);
// --- Method 4: Mutable iteration over values ---
println!("\n--- Apply 10% Discount (values_mut) ---");
for revenue in sales.values_mut() {
*revenue *= 0.9; // Apply 10% discount
}
for (product, revenue) in &sales {
println!(" {}: ${:.2}", product, revenue);
}
// --- Method 5: drain to consume the HashMap ---
let mut backup = sales.clone();
println!("\n--- Drain (consumes HashMap) ---");
while let Some((product, revenue)) = backup.drain().next() {
println!(" Removed: {} (${:.2})", product, revenue);
}
println!(" backup is empty: {}", backup.is_empty());
// --- When to use what: Collection selection guide ---
println!("\n--- Collection Selection Guide ---");
// Scenario 1: Vec (ordered, indexed access)
let mut todo_list: Vec<&str> = Vec::new();
todo_list.push("Buy milk");
todo_list.push("Write report");
todo_list.push("Call mom");
println!("Vec (ordered todo list):");
for (i, item) in todo_list.iter().enumerate() {
println!(" {}. {}", i + 1, item);
}
// Scenario 2: HashMap (key-value lookup)
let mut phone_book: HashMap<&str, &str> = HashMap::new();
phone_book.insert("Alice", "123-4567");
phone_book.insert("Bob", "987-6543");
println!("HashMap (phone book):");
println!(" Alice's number: {}", phone_book.get("Alice").unwrap());
// Scenario 3: HashSet (membership check)
let mut admin_users: HashSet<&str> = HashSet::new();
admin_users.insert("admin");
admin_users.insert("root");
let user = "admin";
println!("HashSet (admin check):");
println!(" Is '{}' admin? {}", user, admin_users.contains(user));
}
// Import HashSet for the last scenario
use std::collections::HashSet;
输出:
--- All Products (iter) ---
Laptop: $1200.00
Mouse: $25.00
Keyboard: $80.00
Monitor: $350.00
Headphones: $150.00
--- Product Names (keys) ---
- Laptop
- Mouse
- Keyboard
- Monitor
- Headphones
--- Revenue Values (values) ---
Total revenue: $1805.00
Average: $361.00
--- Apply 10% Discount (values_mut) ---
Laptop: $1080.00
Mouse: $22.50
Keyboard: $72.00
Monitor: $315.00
Headphones: $135.00
--- Drain (consumes HashMap) ---
Removed: Laptop ($1080.00)
Removed: Mouse ($22.50)
Removed: Keyboard ($72.00)
Removed: Monitor ($315.00)
Removed: Headphones ($135.00)
backup is empty: true
--- Collection Selection Guide ---
Vec (ordered todo list):
1. Buy milk
2. Write report
3. Call mom
HashMap (phone book):
Alice's number: 123-4567
HashSet (admin check):
Is 'admin' admin? true
HashMap 的遍历方式:
iter()遍历所有键值对,keys()只遍历键,values()只遍历值,values_mut()可变遍历值,drain()消费并移除所有元素。选择集合类型时:需要有序、可重复、按索引访问 → Vec;需要键值映射、快速查找 → HashMap;需要去重、集合运算、成员检查 → HashSet。
▶ 示例 5:综合练习——词频统计与文本分析(难度 ⭐⭐⭐)
// ============================================
// 综合示例:HashMap + HashSet 文本分析
// ============================================
use std::collections::{HashMap, HashSet};
fn word_frequency(text: &str) -> HashMap<String, u32> {
let mut freq: HashMap<String, u32> = HashMap::new();
for word in text.split_whitespace() {
let clean: String = word.chars()
.filter(|c| c.is_alphabetic())
.map(|c| c.to_lowercase().next().unwrap())
.collect();
if !clean.is_empty() {
*freq.entry(clean).or_insert(0) += 1;
}
}
freq
}
fn unique_words(text: &str) -> HashSet<String> {
text.split_whitespace()
.map(|w| w.to_lowercase())
.collect()
}
fn top_n(freq: &HashMap<String, u32>, n: usize) -> Vec<(&str, u32)> {
let mut entries: Vec<_> = freq.iter().map(|(k, &v)| (k.as_str(), v)).collect();
entries.sort_by(|a, b| b.1.cmp(&a.1));
entries.into_iter().take(n).collect()
}
fn main() {
let text1 = "the cat sat on the mat and the cat slept on the mat";
let text2 = "the dog ran on the grass and the dog slept on the rug";
println!("=== 文本 1 词频 ===");
let freq1 = word_frequency(text1);
for (word, count) in top_n(&freq1, 5) {
println!(" '{}': {} 次", word, count);
}
println!("\n=== 文本 2 词频 ===");
let freq2 = word_frequency(text2);
for (word, count) in top_n(&freq2, 5) {
println!(" '{}': {} 次", word, count);
}
let words1 = unique_words(text1);
let words2 = unique_words(text2);
let common: HashSet<_> = words1.intersection(&words2).collect();
println!("\n共同词汇: {:?}", common);
let only1: HashSet<_> = words1.difference(&words2).collect();
println!("文本1独有: {:?}", only1);
let only2: HashSet<_> = words2.difference(&words1).collect();
println!("文本2独有: {:?}", only2);
let all: HashSet<_> = words1.union(&words2).collect();
println!("所有词汇数: {}", all.len());
}
输出:
=== 文本 1 词频 ===
'the': 3 次
'cat': 2 次
'on': 2 次
'mat': 2 次
'sat': 1 次
=== 文本 2 词频 ===
'the': 3 次
'dog': 2 次
'on': 2 次
'grass': 1 次
'ran': 1 次
共同词汇: {"the", "and", "on", "slept"}
文本1独有: {"mat", "cat", "sat"}
文本2独有: {"ran", "rug", "grass", "dog"}
所有词汇数: 11
word_frequency用entry().or_insert()模式优雅地计数;unique_words用 HashSet 自动去重;intersection/difference/union实现集合运算。HashMap + HashSet 是文本分析的黄金组合。
❓ 常见问题
Eq + Hash trait。Entry 枚举,有两个变体:Occupied(Entry) 和 Vacant(Entry)。📖 小节
HashMap<K, V>存储键值对映射,提供 O(1) 平均时间复杂度的查找- entry API 是 Rust 特有的"插入或更新"惯用写法(
entry(k).or_insert(v)) - HashMap 插入时转移所有权,key 必须实现
Eq + Hashtrait HashSet<T>本质上是HashMap<T, ()>,用于去重和集合运算- HashSet 支持 union(并集)、intersection(交集)、difference(差集)、symmetric_difference(对称差集)
- 集合选择策略:有序/索引 → Vec,键值查找 → HashMap,去重/成员检查 → HashSet
📝 作业
-
难度 ⭐:创建一个
HashMap<String, u32>存储水果价格("apple"=5, "banana"=3, "orange"=4)。写一个函数fn total_cost(items: &[&str], prices: &HashMap<String, u32>) -> u32计算购物车总价。在 main 中测试["apple", "banana", "apple"]购物车。 -
难度 ⭐⭐:写一个函数
fn word_frequency(text: &str) -> HashMap<String, u32>统计文本中每个单词出现的次数。使用entryAPI。在 main 中测试 "the quick brown fox jumps over the lazy dog the fox" 并打印结果。 -
难度 ⭐⭐⭐:创建两个班级的学生名单(
HashSet<&str>),Class A 有 ["Alice", "Bob", "Charlie", "David"],Class B 有 ["Charlie", "David", "Eve", "Frank"]。编写函数fn analyze_classes(a: &HashSet<&str>, b: &HashSet<&str>)打印:两班都有的学生(交集),只在 A 班的学生(差集),所有不重复的学生(并集),以及只在一个班的学生(对称差集)。在 main 中调用并输出结果。