Rust练习册之字母异位词与字符串处理方法技巧
作者:一缕清烟在人间
前言
在日常生活中,我们经常会遇到一些单词,它们由相同的字母组成,但顺序不同,这种词被称为"字母异位词"(Anagram)。比如 “listen” 和 “silent” 就是一对字母异位词。在 Exercism 的 “anagram” 练习中,我们将实现一个字母异位词查找器,这不仅能帮助我们理解字符串处理的基本技巧,还能深入学习 Rust 中的集合操作和字符处理。
问题背景
字母异位词是指由相同字母重新排列组成的不同单词。判断两个单词是否为字母异位词的核心思想是:如果两个单词包含完全相同的字母,且每个字母出现的次数也相同,那么它们就是字母异位词。
让我们先看看练习提供的实现:
use std::collections::HashSet;
fn sort(word: &str) -> String {
let mut chars: Vec<char> = word.chars().collect();
chars.sort_unstable();
chars.into_iter().collect()
}
pub fn anagrams_for<'a>(word: &'a str, possible_anagrams: &'a [&str]) -> HashSet<&'a str> {
let word = word.to_lowercase();
let sorted = sort(&word);
possible_anagrams
.iter()
.filter(|e| {
let x = e.to_lowercase();
x != word && sorted == sort(&x)
})
.cloned()
.collect()
}
这个实现采用了非常优雅的方法:将单词中的字符排序,如果两个单词排序后相同,那么它们就是字母异位词。
算法解析
1. 字符排序方法
fn sort(word: &str) -> String {
let mut chars: Vec<char> = word.chars().collect();
chars.sort_unstable();
chars.into_iter().collect()
}
这个函数是整个算法的核心:
- 将字符串转换为字符向量
- 对字符进行排序
- 将排序后的字符重新组合成字符串
使用 sort_unstable 而不是 sort 是因为不需要稳定排序,这样可以获得更好的性能。
2. 主要逻辑
pub fn anagrams_for<'a>(word: &'a str, possible_anagrams: &'a [&str]) -> HashSet<&'a str> {
let word = word.to_lowercase();
let sorted = sort(&word);
possible_anagrams
.iter()
.filter(|e| {
let x = e.to_lowercase();
x != word && sorted == sort(&x)
})
.cloned()
.collect()
}
主函数的逻辑非常清晰:
- 将目标单词转为小写并排序作为基准
- 遍历所有候选词
- 过滤条件:
- 候选词不能与目标词相同(即使大小写不同)
- 候选词排序后必须与目标词排序后相同
- 收集结果到 HashSet 中
测试用例分析
通过查看测试用例,我们可以更好地理解需求:
#[test]
fn test_no_matches() {
let word = "diaper";
let inputs = ["hello", "world", "zombies", "pants"];
let outputs = vec![];
process_anagram_case(word, &inputs, &outputs);
}
最基本的情况,没有任何匹配的字母异位词。
#[test]
fn test_detect_simple_anagram() {
let word = "ant";
let inputs = ["tan", "stand", "at"];
let outputs = vec!["tan"];
process_anagram_case(word, &inputs, &outputs);
}
简单情况,“ant” 和 “tan” 是字母异位词。
#[test]
fn test_case_insensitive_anagrams() {
let word = "Orchestra";
let inputs = ["cashregister", "Carthorse", "radishes"];
let outputs = vec!["Carthorse"];
process_anagram_case(word, &inputs, &outputs);
}
大小写不敏感的匹配,“Orchestra” 和 “Carthorse” 是字母异位词。
#[test]
fn test_does_not_detect_a_word_as_its_own_anagram() {
let word = "banana";
let inputs = ["banana"];
let outputs = vec![];
process_anagram_case(word, &inputs, &outputs);
}
一个词不能是它自己的字母异位词,即使大小写不同。
#[test]
fn test_unicode_anagrams() {
let word = "ΑΒΓ";
// These words don't make sense, they're just greek letters cobbled together.
let inputs = ["ΒΓΑ", "ΒΓΔ", "γβα"];
let outputs = vec!["ΒΓΑ", "γβα"];
process_anagram_case(word, &inputs, &outputs);
}
支持 Unicode 字符,包括希腊字母。
替代实现方法
除了字符排序方法,还有其他几种判断字母异位词的方式:
1. 字符计数方法
use std::collections::HashMap;
use std::collections::HashSet;
fn char_count(word: &str) -> HashMap<char, usize> {
let mut counts = HashMap::new();
for c in word.chars() {
*counts.entry(c).or_insert(0) += 1;
}
counts
}
pub fn anagrams_for<'a>(word: &'a str, possible_anagrams: &'a [&str]) -> HashSet<&'a str> {
let word = word.to_lowercase();
let word_counts = char_count(&word);
possible_anagrams
.iter()
.filter(|&candidate| {
let candidate_lower = candidate.to_lowercase();
candidate_lower != word && char_count(&candidate_lower) == word_counts
})
.cloned()
.collect()
}
这种方法通过统计每个字符出现的次数来判断是否为字母异位词。
2. 排序优化版本
use std::collections::HashSet;
fn normalize(word: &str) -> String {
let mut chars: Vec<char> = word.to_lowercase().chars().collect();
chars.sort_unstable();
chars.into_iter().collect()
}
pub fn anagrams_for<'a>(word: &'a str, possible_anagrams: &'a [&str]) -> HashSet<&'a str> {
let normalized_target = normalize(word);
let target_lower = word.to_lowercase();
possible_anagrams
.iter()
.filter(|&&candidate| {
let candidate_lower = candidate.to_lowercase();
candidate_lower != target_lower && normalize(candidate) == normalized_target
})
.cloned()
.collect()
}
这个版本在 normalize 函数中就进行了小写转换,避免了重复转换。
性能比较
让我们分析一下不同方法的性能特点:
字符排序方法:
- 时间复杂度:O(n log n),其中 n 是单词长度
- 空间复杂度:O(n)
- 优点:实现简单,易于理解
- 缺点:排序操作相对较慢
字符计数方法:
- 时间复杂度:O(n),其中 n 是单词长度
- 空间复杂度:O(k),其中 k 是不同字符的数量
- 优点:时间复杂度更优
- 缺点:需要额外的 HashMap 存储
对于大多数实际应用,字符排序方法已经足够快,而且代码更简洁。
边界情况处理
在实现中需要特别注意以下边界情况:
#[test]
fn test_misleading_unicode_anagrams() {
// Despite what a human might think these words different letters, the input uses Greek A and B
// while the list of potential anagrams uses Latin A and B.
let word = "ΑΒΓ"; // 希腊字母
let inputs = ["ABΓ"]; // 拉丁字母 + 希腊字母
let outputs = vec![];
process_anagram_case(word, &inputs, &outputs);
}
Unicode 字符的处理需要特别小心,因为看起来相似的字符可能有不同的编码。
#[test]
fn test_same_bytes_different_chars() {
let word = "a⬂"; // 61 E2 AC 82
let inputs = ["€a"]; // E2 82 AC 61
let outputs = vec![];
process_anagram_case(word, &inputs, &outputs);
}
即使字节相同但字符顺序不同也不能算作字母异位词。
实际应用场景
字母异位词在实际开发中有多种应用:
- 文本处理工具:查找文档中的字母异位词
- 游戏开发:拼字游戏中的单词匹配
- 教育软件:语言学习应用中的练习题
- 数据清洗:识别重复但拼写不同的数据项
扩展功能
基于这个基础实现,我们可以添加更多功能:
use std::collections::HashSet;
pub struct AnagramSolver {
word: String,
sorted_chars: String,
}
impl AnagramSolver {
pub fn new(word: &str) -> Self {
let word = word.to_lowercase();
let sorted_chars = Self::sort_chars(&word);
AnagramSolver { word, sorted_chars }
}
fn sort_chars(word: &str) -> String {
let mut chars: Vec<char> = word.chars().collect();
chars.sort_unstable();
chars.into_iter().collect()
}
pub fn is_anagram(&self, candidate: &str) -> bool {
let candidate_lower = candidate.to_lowercase();
candidate_lower != self.word && Self::sort_chars(&candidate_lower) == self.sorted_chars
}
pub fn find_anagrams<'a>(&self, candidates: &'a [&str]) -> HashSet<&'a str> {
candidates
.iter()
.filter(|&&candidate| self.is_anagram(candidate))
.cloned()
.collect()
}
}
这种面向对象的方式可以避免重复计算目标词的排序结果。
总结
通过 anagram 练习,我们学到了:
- 字符串处理:掌握了 Rust 中字符串和字符的基本操作
- 算法思维:学会了用排序和字符统计两种方法解决同一问题
- 集合操作:熟练使用 HashSet 进行数据收集和去重
- 生命周期:理解了 Rust 中的生命周期注解
- Unicode 处理:了解了 Unicode 字符的复杂性
- 测试驱动:通过丰富的测试用例确保实现的正确性
这些技能在实际开发中非常有用,特别是在处理文本数据、实现搜索功能和构建语言相关应用时。字母异位词虽然看起来简单,但它涉及到了字符串处理的许多核心概念,是学习 Rust 字符串操作的良好起点。
到此这篇关于Rust练习册之字母异位词与字符串处理方法技巧的文章就介绍到这了,更多相关Rust字母异位词与字符串内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
