装东西的容器:集合
学习目标
学完本章后,你将能够:
- 使用
Vec存储动态数组 - 使用
String进行文本操作 - 使用
HashMap存储键值对 - 根据需求选择合适的集合类型
- 遍历和处理集合中的数据
一、为什么需要集合?
之前我们学的变量一次只能存一个值:
let x = 5; // 一个数字
let s = "hello"; // 一段文字但现实生活中,我们经常需要存一堆东西:
- 班级里所有学生的名字
- 购物车里的所有商品
- 单词表中每个单词对应的翻译
集合(Collections)就是用来存”一堆东西”的数据类型。
二、Vec:可以变长的数组
2.1 最简单的 Vec
fn main() {
let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);
v.push(3);
println!("{:?}", v);
}输出:
[1, 2, 3]
Vec 和数组不同:
- 数组大小固定,Vec 可以增长
- 数组用
[1, 2, 3]字面量,Vec 用Vec::new()或vec![]
2.2 用 vec! 宏快速创建
fn main() {
let v = vec![1, 2, 3, 4, 5];
println!("{:?}", v);
println!("长度:{}", v.len());
}vec![...] 是最常用的创建 Vec 的方式,就像 println! 一样是宏。
2.3 访问元素
fn main() {
let v = vec![10, 20, 30, 40, 50];
// 下标访问(越界会 panic)
println!("第三个元素:{}", v[2]);
// get 方法(越界返回 None)
match v.get(10) {
Some(value) => println!("值:{}", value),
None => println!("索引 10 不存在!"),
}
}| 访问方式 | 越界行为 | 返回类型 |
|---|---|---|
v[index] | panic(程序崩溃) | 元素类型 T |
v.get(index) | 返回 None | Option<&T> |
建议:如果你不确定索引是否有效,用 get。确定索引有效时用下标。
2.4 遍历 Vec
fn main() {
let v = vec![10, 20, 30, 40, 50];
// 只读遍历
println!("所有元素:");
for num in &v {
println!(" {}", num);
}
// 带索引的遍历
for (i, num) in v.iter().enumerate() {
println!("v[{}] = {}", i, num);
}
}2.5 修改 Vec 中的元素
fn main() {
let mut v = vec![10, 20, 30];
// 通过可变引用遍历并修改
for num in &mut v {
*num += 5; // * 解引用,修改原值
}
println!("{:?}", v); // [15, 25, 35]
}2.6 Vec 的常用操作
fn main() {
let mut v = vec![1, 2, 3];
v.push(4); // 在末尾添加
v.pop(); // 移除最后一个,返回 Option<T>
v.insert(1, 99); // 在索引1处插入
v.remove(0); // 移除索引0的元素
println!("{:?}", v); // [99, 2, 3]
println!("长度:{}", v.len()); // 3
println!("是否为空:{}", v.is_empty()); // false
}| 操作 | 方法 | 说明 |
|---|---|---|
| 末尾添加 | push(value) | 加到最后 |
| 末尾弹出 | pop() | 返回 Option,拿掉最后一个 |
| 插入 | insert(index, value) | 在指定位置插入 |
| 删除 | remove(index) | 删掉并返回 |
| 长度 | len() | 有多少元素 |
| 判空 | is_empty() | 是否为空 |
| 清空 | clear() | 全部删掉 |
三、String:会变长的字符串
之前我们经常用 "Hello" 这样的字符串字面量,但它是不可变的。String 是可变的、拥有所有权的字符串。
3.1 创建 String
fn main() {
let s1 = String::new(); // 空字符串
let s2 = String::from("你好"); // 从字面量创建
let s3 = "你好".to_string(); // 另一种方式
println!("s2: {}, s3: {}", s2, s3);
}3.2 修改 String
fn main() {
let mut s = String::from("你好");
s.push_str("世界"); // 追加字符串
s.push('!'); // 追加单个字符
println!("{}", s); // 你好世界!
}3.3 拼接 String
fn main() {
let s1 = String::from("Hello");
let s2 = String::from("World");
// 方式1:+ 运算符(会把 s1 的所有权移走)
let s3 = s1 + " " + &s2;
// println!("{}", s1); // s1 已被移走
// 方式2:format! 宏(不移动所有权)
let s4 = String::from("Rust");
let s5 = String::from("很棒");
let s6 = format!("{} {}", s4, s5);
println!("{}", s6); // Rust 很棒
println!("s4 还能用:{}", s4); // format! 不移动所有权
}| 拼接方式 | 是否转移所有权 | 推荐度 |
|---|---|---|
+ | 第一个 String 会移走 | 适合简单拼接 |
format! | 不转移 | 推荐,更灵活 |
3.4 取子串和长度
fn main() {
let s = String::from("你好世界!");
println!("长度(字节):{}", s.len()); // UTF-8 字节数
println!("是否为空:{}", s.is_empty());
// 取子串(注意 UTF-8 边界问题)
let hello = &s[0..6]; // "你好" 在 UTF-8 中占 6 个字节
println!("{}", hello); // 你好
}四、HashMap:键值对字典
HashMap 就像一本电话簿:你通过名字找电话号码。名字是”键”,号码是”值”。
4.1 创建和插入
use std::collections::HashMap;
fn main() {
let mut phone_book = HashMap::new();
phone_book.insert("小明", "13800138000");
phone_book.insert("小红", "13900139000");
phone_book.insert("小刚", "13700137000");
}use std::collections::HashMap; 是”导入”声明,因为 HashMap 不像 Vec 和 String 那样在”预导入”中。
4.2 查找
fn main() {
let mut phone_book = HashMap::new();
phone_book.insert("小明", "13800138000");
// 用 get 查找
match phone_book.get("小明") {
Some(number) => println!("小明的电话:{}", number),
None => println!("没找到小明"),
}
// 用 [] 查找(不存在会 panic)
println!("{}", phone_book["小明"]);
}4.3 遍历
fn main() {
let mut scores = HashMap::new();
scores.insert("小明", 90);
scores.insert("小红", 85);
scores.insert("小刚", 78);
for (name, score) in &scores {
println!("{}:{}分", name, score);
}
}注意:HashMap 的遍历顺序不固定。
4.4 更新和插入规则
fn main() {
let mut map = HashMap::new();
// 直接覆盖
map.insert("a", 1);
map.insert("a", 100); // 覆盖为 100
// 只在没有键的时候插入
map.entry("a").or_insert(999); // a 已有,不插入
map.entry("b").or_insert(200); // b 没有,插入 200
println!("a: {}, b: {}", map["a"], map["b"]); // a: 100, b: 200
}entry().or_insert() 是”字典里有没有这个词?没有就补上”的意思。
4.5 根据旧值更新
fn main() {
let text = "你好世界你好";
let mut word_count = HashMap::new();
for word in text.chars() {
let count = word_count.entry(word).or_insert(0);
*count += 1;
}
println!("{:?}", word_count);
// {'你': 2, '好': 2, '世': 1, '界': 1}
}这是”统计词频”的经典写法!
五、三大集合对比
| 特性 | Vec | String | HashMap<K, V> |
|---|---|---|---|
| 存储内容 | 同类型的多个值 | 文字 | 键 → 值 映射 |
| 访问方式 | 索引 v[0] | 切片 &s[..] | 键 map[“key”] |
| 是否有序 | 有序 | 有序 | 无序 |
| 是否可变长 | 是 | 是 | 是 |
| 典型用途 | 列表、队列 | 文字处理 | 字典、缓存、统计 |
| 比喻 | 铅笔盒 | 便签纸 | 电话簿 |
六、综合示例:学生成绩管理
use std::collections::HashMap;
fn main() {
// 用 Vec 存学生名字列表
let names = vec![
String::from("小明"),
String::from("小红"),
String::from("小刚"),
];
// 用 HashMap 存成绩
let mut scores = HashMap::new();
scores.insert(String::from("小明"), 90);
scores.insert(String::from("小红"), 85);
scores.insert(String::from("小刚"), 78);
// 遍历并打印
let mut total = 0;
for name in &names {
if let Some(score) = scores.get(name) {
println!("{}:{}分", name, score);
total += score;
}
}
println!("平均分:{:.1}", total as f64 / names.len() as f64);
}本章小结
这一章你学习了三种最常用的集合:
Vec<T>:动态数组,像”可以变长的列表”。vec![]创建,push添加,get安全访问String:可变的文本。push_str追加,format!安全拼接HashMap<K,V>:键值对,像电话簿。insert存入,get查找,entry().or_insert()按需插入
这三个集合能覆盖你大部分的日常编程需求。下一章,我们学习如何处理”错误”。
章节考查
总分100分:概念考查40分 + 判断正误20分 + 代码分析15分 + 编程大题15分 + 填空题5分 + 代码补全5分
一、概念考查(每题4分,共40分)
1. Vec<T> 和数组的主要区别是?
- A. Vec 更快
- B. Vec 可以动态增长,数组大小固定
- C. 数组可以存不同类型
- D. Vec 不需要类型标注
点击查看答案
B。Vec 是动态的,可以在运行时增长或收缩。数组的大小在编译时就固定了。
2. vec![1, 2, 3] 创建的是什么?
- A. 数组
- B. Vec
- C. 元组
- D. 切片
点击查看答案
B。vec! 宏创建一个 Vec,类型自动推断为 Vec<i32>。
3. v.get(100) 在索引越界时返回什么?
- A. 0
- B. panic
- C. None
- D. 编译错误
点击查看答案
C。get 方法安全,越界时返回 None。
4. String::from("你好") 和 "你好" 的区别是?
- A. 完全相同
- B.
String拥有数据且可变,字面量&str不可变 - C.
"你好"是 String 类型的 - D. 长度不同
点击查看答案
B。String 拥有所有权且可修改,"你好" 是 &str 字符串字面量,不可变。
5. format! 和 println! 的区别?
- A. 没有区别
- B.
format!返回字符串,不打印 - C.
println!返回字符串 - D.
format!需要 3 个参数
点击查看答案
B。format! 拼接字符串并返回 String,不打印到屏幕。
6. HashMap 中 entry().or_insert() 的作用是?
- A. 总是插入新值
- B. 如果键存在则替换,否则插入
- C. 如果键不存在则插入默认值
- D. 删除该键
点击查看答案
C。or_insert 只在键不存在时才插入默认值,存在时什么都不做。
7. for num in &mut v 中为什么需要 *num?
- A.
num是引用,需要解引用才能修改原值 - B. Rust 的语法规则
- C. 不需要
*,直接改就行 - D.
*是删除的意思
点击查看答案
A。&mut v 遍历得到的是可变引用 &mut i32,需要用 * 解引用才能修改实际值。
8. v.push(42) 做了什么?
- A. 在开头插入 42
- B. 在末尾添加 42
- C. 替换第一个元素为 42
- D. 删除值为 42 的元素
点击查看答案
B。push 在 Vec 的末尾添加元素。
9. HashMap 的遍历顺序是?
- A. 按插入顺序
- B. 按键的字母顺序
- C. 不保证顺序
- D. 按值的升序
点击查看答案
C。HashMap 不保证遍历顺序,每次运行可能不同。
10. String 的 len() 返回的是?
- A. 字符数
- B. 字节数
- C. 单词数
- D. 行数
点击查看答案
B。len() 返回字符串占用的字节数(UTF-8 编码),不是字符数。
二、判断正误(每题2分,共20分)
1. Vec::new() 需要指定类型,如 Vec::<i32>::new() 或带类型标注。
点击查看答案
正确。如果编译器无法推断类型,需要显式标注:let v: Vec<i32> = Vec::new();。
2. let s = String::new(); 创建了包含 “new” 的字符串。
点击查看答案
错误。String::new() 创建的是空字符串 ""。
3. String 可以用 + 直接和 &str 拼接。
点击查看答案
正确。let s = s1 + &s2; 是合法的,但要注意 + 会借用左边的所有权。
4. format! 不会移动任何参数的所有权。
点击查看答案
正确。format! 使用引用,不会转移所有权。
5. HashMap 在标准库的预导入中,不需要 use。
点击查看答案
错误。HashMap 需要 use std::collections::HashMap; 导入。
6. v.pop() 删除并返回 Vec 的第一个元素。
点击查看答案
错误。pop() 删除并返回的是最后一个元素。
7. for (key, value) in &map 遍历的是键值对的引用。
点击查看答案
正确。&map 遍历不可变引用,不会移动 HashMap。
8. vec![0; 10] 和 vec![0, 10] 是等价的。
点击查看答案
错误。vec![0; 10] 创建 10 个 0,vec![0, 10] 创建两个元素 [0, 10]。
9. let v = vec![1, 2]; v[5]; 编译不通过。
点击查看答案
错误。编译可以通过,但运行时会 panic。
10. map.entry(key).or_insert(value) 返回的是值的可变引用。
点击查看答案
正确。or_insert 返回 &mut V,可以通过它修改值。
三、代码分析(每题3分,共15分)
1. 下面代码的输出是什么?
fn main() {
let mut v = vec![1, 2, 3];
v.push(4);
v.pop();
println!("{:?}", v);
}- A. [1, 2, 3]
- B. [1, 2, 3, 4]
- C. [2, 3, 4]
- D. [1, 2, 3, 4] 然后 panic
点击查看答案
A。push(4) 后是 [1,2,3,4],pop() 移除最后一个,变回 [1,2,3]。
2. 下面代码的输出是什么?
fn main() {
let mut s = String::from("Rust");
s.push_str("入门");
println!("{}", s);
}- A. Rust
- B. 入门
- C. Rust入门
- D. 编译错误
点击查看答案
C。push_str 追加字符串,“Rust” + “入门” = “Rust入门”。
3. 下面代码的输出是什么?
use std::collections::HashMap;
fn main() {
let text = "hello world hello";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
println!("{}", map["hello"]);
}- A. 0
- B. 1
- C. 2
- D. 编译错误
点击查看答案
C。“hello” 出现了 2 次,所以 count 为 2。
4. 下面代码有什么问题?
fn main() {
let v = vec![1, 2, 3];
for n in v {
println!("{}", n);
}
println!("{:?}", v); // 又用了一次
}- A. Vec 不能遍历
- B.
for循环会消耗 v 的所有权 - C. 打印语法不对
- D. 没有错误
点击查看答案
B。for n in v 会消耗 v,之后不能再使用。改用 for n in &v 即可。
5. 下面代码的输出是什么?
fn main() {
let mut v = vec![10, 20, 30];
for n in &mut v {
*n += 1;
}
let sum: i32 = v.iter().sum();
println!("{}", sum);
}- A. 60
- B. 63
- C. 66
- D. 编译错误
点击查看答案
B。每个元素 +1 后变成 [11, 21, 31],总和 = 63。
四、编程大题(15分)
题目: 编写一个”单词计数器”程序:
- 定义一个字符串
text,包含一段英文或中文句子 - 用 HashMap 统计每个单词出现的次数(中文按单个汉字统计,英文按空格分词)
- 用 Vec 存储单词和次数的配对(按任意顺序)
- 遍历打印每个单词和它的次数
- 找出出现次数最多的单词并打印
点击查看答案
use std::collections::HashMap;
fn main() {
let text = "你好世界你好Rust学习RustRust";
let mut word_count: HashMap<&str, u32> = HashMap::new();
// 中文字符按单个字统计,英文单词按空格分词
// 简化版:按字符统计
for ch in text.chars() {
// 因为 HashMap 的 key 是 &str,这里我们用简单的办法
// 实际应用中可能需要更复杂的分词逻辑
}
// 改进版本:全部按字符串中的单词(按空格分)
let text_en = "hello world hello rust world rust rust";
let mut word_count = HashMap::new();
for word in text_en.split_whitespace() {
let count = word_count.entry(word).or_insert(0);
*count += 1;
}
// 打印所有统计
println!("单词统计:");
let mut pairs: Vec<(&&str, &u32)> = word_count.iter().collect();
for (word, count) in &pairs {
println!(" {}: {} 次", word, count);
}
// 找出最多
let max_word = word_count.iter()
.max_by_key(|&(_, count)| count)
.unwrap();
println!("出现最多:\"{}\",共 {} 次", max_word.0, max_word.1);
}评分标准:
- HashMap 创建和导入(2分)
- 分词逻辑(4分)
- 统计计数(4分)
- 遍历打印(3分)
- 找最大值(2分)
五、填空题(每题1分,共5分)
1. 用 vec! 创建 Vec:let v = vec![______];(三个数字 1, 2, 3)。
点击查看答案
1, 2, 3。vec![1, 2, 3] 创建包含三个整数的 Vec。
2. 在 Vec 末尾添加元素用 ______,移除末尾元素用 ______。
点击查看答案
push 和 pop。push 添加,pop 移除并返回 Option。
3. String::______("你好") 从字面量创建 String。
点击查看答案
from。String::from("...") 是最常用的创建 String 的方式。
4. HashMap 的查找方法 ______ 返回 Option<&V>。
点击查看答案
get。map.get(key) 返回 Option<&V>。
5. map.entry(key).______(default) 在键不存在时插入默认值。
点击查看答案
or_insert。or_insert 返回值的可变引用,存在则不动,不存在则插入默认值。
六、代码补全(共5分)
1. 补全 Vec 操作(2分)
fn main() {
let mut v = vec![1, 2];
v.______(3); // 添加 3
println!("{:?}", v); // 输出 [1, 2, 3]
}点击查看答案
v.push(3);2. 补全 HashMap 查找(2分)
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert("key", 42);
match map.______("key") {
Some(val) => println!("{}", val),
None => println!("没找到"),
}
}点击查看答案
match map.get("key") {3. 补全字符串拼接(1分)
fn main() {
let s1 = String::from("Hello");
let s2 = String::from("World");
let s3 = ______("{} {}", s1, s2);
println!("{}", s3);
}点击查看答案
let s3 = format!("{} {}", s1, s2);计分:概念40 + 判断20 + 代码分析15 + 编程15 + 填空5 + 补全5 = 总分100分
练习
练习
| 题号 | 题目 | 链接 | 知识点 |
|---|---|---|---|
| P1001 | A+B Problem | https://www.luogu.com.cn/problem/P1001 | 输入输出 |
| P1008 | 全排列 | https://www.luogu.com.cn/problem/P1008 | 递归、枚举 |
| P1010 | 幂次方 | https://www.luogu.com.cn/problem/P1010 | 函数、递归 |
| P1012 | 拼数 | https://www.luogu.com.cn/problem/P1012 | 字符串、排序 |