在数字时代,我们很少再看到用剪贴杂志制作便签或勒索信的场景,但这个经典的概念却是一个极好的编程练习题目。今天,我们将通过 Rust 实现一个"杂志剪贴"检测器,来学习 Rust 中集合类型和算法思维的强大功能。
问题场景
想象一下,你是一名侦探,正在调查一起神秘案件。现场发现了一张手工制作的便签,而你需要确定嫌疑人在案发时是否接触过某本特定的杂志。你的任务是判断这本杂志中的单词是否足够制作出这张便签。
这个问题看似简单,但背后涉及了重要的计算机科学概念------字符频率统计和资源分配问题。
核心实现
rust
// This stub file contains items which aren't used yet; feel free to remove this module attribute
// to enable stricter warnings.
#![allow(unused)]
use std::collections::HashMap;
pub fn can_construct_note(magazine: &[&str], note: &[&str]) -> bool {
let mut word_count: HashMap<&str, u32> = HashMap::new();
// 统计杂志中每个单词的数量
for &word in magazine {
*word_count.entry(word).or_insert(0) += 1;
}
// 检查便签中的每个单词是否都能从杂志中找到
for &word in note {
let count = word_count.entry(word).or_insert(0);
if *count == 0 {
return false;
}
*count -= 1;
}
true
}
代码解析
HashMap 的使用
rust
use std::collections::HashMap;
let mut word_count: HashMap<&str, u32> = HashMap::new();
我们使用 HashMap 来存储每个单词及其出现次数。键是单词(&str),值是计数(u32)。这是解决频率统计问题的经典数据结构。
entry API 的妙用
rust
*word_count.entry(word).or_insert(0) += 1;
Rust 的 entry API 是处理 HashMap 的优雅方式。它避免了多次查找同一个键的开销:
- 如果键存在,
entry()返回一个OccupiedEntry - 如果键不存在,
or_insert(0)插入默认值 0
然后通过解引用 * 直接修改值。
引用和解引用
rust
for &word in magazine {
// ...
}
这里使用了模式匹配来解构引用。因为 magazine 是 &[&str] 类型,所以其中的元素是 &&str。通过 &word 模式,我们直接获取了 &str 类型的 [word](file:///Users/zacksleo/projects/github/zacksleo/exercism-rust/exercises/concept/semi-structured-logs/src/lib.rs#L25-L25)。
算法逻辑
rust
// 统计杂志中每个单词的数量
for &word in magazine {
*word_count.entry(word).or_insert(0) += 1;
}
// 检查便签中的每个单词是否都能从杂志中找到
for &word in note {
let count = word_count.entry(word).or_insert(0);
if *count == 0 {
return false;
}
*count -= 1;
}
算法分为两个步骤:
- 统计杂志中每个单词的出现次数
- 遍历便签中的单词,检查是否有足够的单词可用,同时减少相应的计数
测试用例分析
通过测试用例我们可以看到各种场景:
rust
use magazine_cutout::*;
#[test]
fn test_example_works() {
let magazine = "two times three is not four"
.split_whitespace()
.collect::<Vec<&str>>();
let note = "two times two is four"
.split_whitespace()
.collect::<Vec<&str>>();
assert!(!can_construct_note(&magazine, ¬e));
}
#[test]
fn test_fn_returns_true_for_good_input() {
let magazine = "The metro orchestra unveiled its new grand piano today. Its donor paraphrased Nathn Hale: \"I only regret that I have but one to give \"".split_whitespace().collect::<Vec<&str>>();
let note = "give one grand today."
.split_whitespace()
.collect::<Vec<&str>>();
assert!(can_construct_note(&magazine, ¬e));
}
#[test]
fn test_fn_returns_false_for_bad_input() {
let magazine = "I've got a lovely bunch of coconuts."
.split_whitespace()
.collect::<Vec<&str>>();
let note = "I've got som coconuts"
.split_whitespace()
.collect::<Vec<&str>>();
assert!(!can_construct_note(&magazine, ¬e));
}
#[test]
fn test_case_sensitivity() {
let magazine = "i've got some lovely coconuts"
.split_whitespace()
.collect::<Vec<&str>>();
let note = "I've got some coconuts"
.split_whitespace()
.collect::<Vec<&str>>();
assert!(!can_construct_note(&magazine, ¬e));
let magazine = "I've got some lovely coconuts"
.split_whitespace()
.collect::<Vec<&str>>();
let note = "i've got some coconuts"
.split_whitespace()
.collect::<Vec<&str>>();
assert!(!can_construct_note(&magazine, ¬e));
}
这些测试用例覆盖了多种情况:
- 基本功能测试
- 正面案例(可以构造)
- 负面案例(无法构造)
- 大小写敏感性测试
Rust 特性的体现
1. 零成本集合类型
rust
use std::collections::HashMap;
Rust 标准库提供了高性能的集合类型,这些实现在保证安全的同时,性能与 C++ 的标准库相当。
2. 模式匹配解构
rust
for &word in magazine {
// ...
}
通过模式匹配,我们可以直接获取需要的值,避免了额外的解引用操作。
3. 所有权和借用系统
rust
pub fn can_construct_note(magazine: &[&str], note: &[&str]) -> bool
函数参数使用切片引用,避免了不必要的数据拷贝,同时保证了内存安全。
4. 错误处理和提前返回
rust
if *count == 0 {
return false;
}
一旦发现无法满足需求,立即返回 false,避免了不必要的计算。
实际应用场景
这个算法在现实世界中有许多应用:
- 文本分析:检查文档相似性,检测抄袭
- 资源分配:验证系统是否有足够的资源执行任务
- 库存管理:检查仓库是否有足够的商品满足订单
- 游戏开发:检查玩家是否有足够的材料制作物品
- 编译器优化:寄存器分配和资源调度
性能优化考虑
对于大规模数据,我们可以进一步优化:
rust
pub fn can_construct_note(magazine: &[&str], note: &[&str]) -> bool {
// 如果便签比杂志还长,直接返回false
if note.len() > magazine.len() {
return false;
}
let mut word_count: HashMap<&str, u32> = HashMap::new();
// ... 其余逻辑保持不变
}
扩展思考
这个问题还可以有多种变化:
- 大小写不敏感:统一转换为小写再比较
- 标点符号处理:去除标点符号
- 词干提取:将单词还原为词根进行比较
- 多语言支持:处理不同语言的文本
总结
通过这个练习,我们学习了:
- HashMap 的基本使用和 entry API
- 模式匹配在解构中的应用
- 引用和解引用的操作
- 算法思维在实际问题中的应用
- Rust 在文本处理方面的优势
这个看似简单的"杂志剪贴"问题,实际上展示了 Rust 在处理复杂数据结构时的优雅和高效。通过合理使用标准库提供的工具,我们可以用很少的代码解决实际问题。
在下一篇文章中,我们将继续探索 Rust 的更多强大功能!