装东西的容器:集合

学习目标

学完本章后,你将能够:

  • 使用 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)返回 NoneOption<&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}
}

这是”统计词频”的经典写法!


五、三大集合对比

特性VecStringHashMap<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() 按需插入

这三个集合能覆盖你大部分的日常编程需求。下一章,我们学习如何处理”错误”。

10-出错了怎么办:错误处理


章节考查

总分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. 切片
点击查看答案

Bvec! 宏创建一个 Vec,类型自动推断为 Vec<i32>

3. v.get(100) 在索引越界时返回什么?

  • A. 0
  • B. panic
  • C. None
  • D. 编译错误
点击查看答案

Cget 方法安全,越界时返回 None

4. String::from("你好")"你好" 的区别是?

  • A. 完全相同
  • B. String 拥有数据且可变,字面量 &str 不可变
  • C. "你好" 是 String 类型的
  • D. 长度不同
点击查看答案

BString 拥有所有权且可修改,"你好"&str 字符串字面量,不可变。

5. format!println! 的区别?

  • A. 没有区别
  • B. format! 返回字符串,不打印
  • C. println! 返回字符串
  • D. format! 需要 3 个参数
点击查看答案

Bformat! 拼接字符串并返回 String,不打印到屏幕。

6. HashMap 中 entry().or_insert() 的作用是?

  • A. 总是插入新值
  • B. 如果键存在则替换,否则插入
  • C. 如果键不存在则插入默认值
  • D. 删除该键
点击查看答案

Cor_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 的元素
点击查看答案

Bpush 在 Vec 的末尾添加元素。

9. HashMap 的遍历顺序是?

  • A. 按插入顺序
  • B. 按键的字母顺序
  • C. 不保证顺序
  • D. 按值的升序
点击查看答案

C。HashMap 不保证遍历顺序,每次运行可能不同。

10. String 的 len() 返回的是?

  • A. 字符数
  • B. 字节数
  • C. 单词数
  • D. 行数
点击查看答案

Blen() 返回字符串占用的字节数(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. 编译错误
点击查看答案

Cpush_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. 没有错误
点击查看答案

Bfor 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分)

题目: 编写一个”单词计数器”程序:

  1. 定义一个字符串 text,包含一段英文或中文句子
  2. 用 HashMap 统计每个单词出现的次数(中文按单个汉字统计,英文按空格分词)
  3. 用 Vec 存储单词和次数的配对(按任意顺序)
  4. 遍历打印每个单词和它的次数
  5. 找出出现次数最多的单词并打印
点击查看答案
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, 3vec![1, 2, 3] 创建包含三个整数的 Vec。

2. 在 Vec 末尾添加元素用 ______,移除末尾元素用 ______

点击查看答案

pushpoppush 添加,pop 移除并返回 Option。

3. String::______("你好") 从字面量创建 String。

点击查看答案

fromString::from("...") 是最常用的创建 String 的方式。

4. HashMap 的查找方法 ______ 返回 Option<&V>

点击查看答案

getmap.get(key) 返回 Option<&V>

5. map.entry(key).______(default) 在键不存在时插入默认值。

点击查看答案

or_insertor_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分


练习

练习

题号题目链接知识点
P1001A+B Problemhttps://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字符串、排序