# 字典与集合:词频统计与去重
学习目标:通过词频统计与标签去重场景,掌握哈希表、集合结构在各语言中的初始化、缺失键查询与增量更新模式。
前置知识与范围:基本类型与循环。演示在内存中对离散标签的频次统计与哈希去重。
# 问题与边界
- 词频统计(Map 映射):
- 输入词列表:
["apple", "banana", "apple", "orange", "banana", "apple"]。 - 预期结果:
"apple"出现 3 次,"banana"出现 2 次,"orange"出现 1 次。 - 缺失键契约:查询不存在的词(如
"pear")时,必须明确返回缺失(如None/null/false)或安全默认值0,不得发生不可控崩溃。
- 输入词列表:
- 标签去重(Set 集合):
- 输入标签列表:
["web", "rust", "web", "cli"]。 - 预期结果:集合总大小为 3,且包含
"rust",不包含"go"。
- 输入标签列表:
# 关键差异
| 语言 | 映射(Map)类型 | 集合(Set)类型 | 增量计数惯用法 | 缺失键行为 |
|---|---|---|---|---|
| Rust | HashMap<K, V> |
HashSet<T> |
*counts.entry(w).or_insert(0) += 1 |
get() 返回 Option<&V> |
| Go | map[K]V |
无内置 Set,惯用 map[T]struct{} |
counts[w]++(利用零值自动为 0) |
val, ok := m[k] 区分零值与缺失 |
| Java | HashMap<K, V> |
HashSet<T> |
counts.merge(w, 1, Integer::sum) |
get() 未命中返回 null |
| JavaScript | Map |
Set |
counts.set(w, (counts.get(w) ?? 0) + 1) |
get() 未命中返回 undefined |
| TypeScript | 沿用 JS Map<K, V> |
Set<T> |
counts.set(w, (counts.get(w) ?? 0) + 1) |
严格空安全约束 |
| Kotlin | mutableMapOf |
mutableSetOf |
counts[w] = (counts[w] ?: 0) + 1 |
get() 返回可空类型 V? |
| Python | 原生 dict |
原生 set |
counts[w] = counts.get(w, 0) + 1 |
m[k] 抛出 KeyError;get() 返回 None |
| C++ | std::unordered_map |
std::unordered_set |
counts[w]++(下标操作自动插入默认值) |
find() 返回 end() 迭代器 |
# 各语言实现
# Rust
Rustuse std::collections::{HashMap, HashSet};
fn main() {
let words = ["apple", "banana", "apple", "orange", "banana", "apple"];
let mut counts: HashMap<&str, usize> = HashMap::new();
for &w in &words {
*counts.entry(w).or_insert(0) += 1;
}
assert_eq!(counts.get("apple"), Some(&3));
assert_eq!(counts.get("banana"), Some(&2));
assert_eq!(counts.get("orange"), Some(&1));
assert_eq!(counts.get("pear"), None);
let tags = ["web", "rust", "web", "cli"];
let tag_set: HashSet<&str> = tags.into_iter().collect();
assert_eq!(tag_set.len(), 3);
assert!(tag_set.contains("rust"));
assert!(!tag_set.contains("go"));
println!("apple: {}, tags: {}", counts["apple"], tag_set.len());
}
Rust 的 entry API 避免了多余的二次键查找。
# Go
Gopackage main
import "fmt"
func main() {
words := []string{"apple", "banana", "apple", "orange", "banana", "apple"}
counts := make(map[string]int)
for _, w := range words {
counts[w]++ // Go 的零值机制:未初始化的 int 自动为 0
}
if counts["apple"] != 3 || counts["banana"] != 2 || counts["orange"] != 1 {
panic("count mismatch")
}
if _, ok := counts["pear"]; ok {
panic("expected pear to be absent")
}
tags := []string{"web", "rust", "web", "cli"}
tagSet := make(map[string]struct{})
for _, t := range tags {
tagSet[t] = struct{}{} // 空结构体不占用内存
}
if len(tagSet) != 3 {
panic("set size mismatch")
}
if _, ok := tagSet["rust"]; !ok {
panic("expected rust")
}
fmt.Printf("apple: %d, tags: %d\n", counts["apple"], len(tagSet))
}
Go 没有泛型内置 Set,惯用 map[T]struct{} 达到零额外内存消耗的集合效果。
# Java
Javaimport java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class Main {
public static void main(String[] args) {
List<String> words = List.of("apple", "banana", "apple", "orange", "banana", "apple");
Map<String, Integer> counts = new HashMap<>();
for (String w : words) {
counts.merge(w, 1, Integer::sum);
}
assert counts.get("apple") == 3;
assert counts.get("banana") == 2;
assert counts.get("orange") == 1;
assert !counts.containsKey("pear");
Set<String> tagSet = new HashSet<>(List.of("web", "rust", "web", "cli"));
assert tagSet.size() == 3;
assert tagSet.contains("rust");
assert !tagSet.contains("go");
System.out.println("apple: " + counts.get("apple") + ", tags: " + tagSet.size());
}
}
Java 8 提供的 Map.merge() 是简洁更新哈希映射的标准范式。
# JavaScript
JavaScriptconst assert = {
equal: (a, b) => { if (a !== b) throw new Error(`${a} !== ${b}`); },
deepEqual: (a, b) => { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(`${JSON.stringify(a)} !== ${JSON.stringify(b)}`); },
throws: (fn, errType) => {
try { fn(); throw new Error("expected throw"); } catch (e) {
if (e.message === "expected throw") throw e;
if (errType && !(e instanceof errType)) throw e;
}
}
};
const words = ["apple", "banana", "apple", "orange", "banana", "apple"];
const counts = new Map();
for (const w of words) {
counts.set(w, (counts.get(w) ?? 0) + 1);
}
assert.equal(counts.get("apple"), 3);
assert.equal(counts.get("banana"), 2);
assert.equal(counts.get("orange"), 1);
assert.equal(counts.get("pear"), undefined);
const tagSet = new Set(["web", "rust", "web", "cli"]);
assert.equal(tagSet.size, 3);
assert.equal(tagSet.has("rust"), true);
assert.equal(tagSet.has("go"), false);
console.log(`apple: ${counts.get("apple")}, tags: ${tagSet.size}`);
现代 JS 首选 Map 和 Set,避免在普通对象上受原型链污染影响。
# TypeScript
TypeScriptconst assert = {
equal: (a: unknown, b: unknown) => { if (a !== b) throw new Error(`${a} !== ${b}`); },
deepEqual: (a: unknown, b: unknown) => { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(`${JSON.stringify(a)} !== ${JSON.stringify(b)}`); },
throws: (fn: () => void, errType?: unknown) => {
try { fn(); throw new Error("expected throw"); } catch (e: unknown) {
if (e instanceof Error && e.message === "expected throw") throw e;
if (typeof errType === "function" && !(e instanceof errType)) throw e;
}
}
};
const words: readonly string[] = ["apple", "banana", "apple", "orange", "banana", "apple"];
const counts = new Map<string, number>();
for (const w of words) {
counts.set(w, (counts.get(w) ?? 0) + 1);
}
assert.equal(counts.get("apple"), 3);
assert.equal(counts.get("banana"), 2);
assert.equal(counts.get("orange"), 1);
const tagSet = new Set<string>(["web", "rust", "web", "cli"]);
assert.equal(tagSet.size, 3);
assert.equal(tagSet.has("rust"), true);
console.log(`apple: ${counts.get("apple")}, tags: ${tagSet.size}`);
TypeScript 对泛型键值对强类型检查,杜绝向 Set 写入不匹配类型的元素。
# Kotlin
Kotlinfun main() {
val words = listOf("apple", "banana", "apple", "orange", "banana", "apple")
val counts = mutableMapOf<String, Int>()
for (w in words) {
counts[w] = (counts[w] ?: 0) + 1
}
check(counts["apple"] == 3)
check(counts["banana"] == 2)
check(counts["orange"] == 1)
check(!counts.containsKey("pear"))
val tagSet = mutableSetOf("web", "rust", "web", "cli")
check(tagSet.size == 3)
check(tagSet.contains("rust"))
println("apple: ${counts["apple"]}, tags: ${tagSet.size}")
}
Kotlin 的下标读写语法 counts[w] = ... 提供了直观的操作接口。
# Python
Pythonif __name__ == "__main__":
words = ["apple", "banana", "apple", "orange", "banana", "apple"]
counts = {}
for w in words:
counts[w] = counts.get(w, 0) + 1
assert counts["apple"] == 3
assert counts["banana"] == 2
assert counts["orange"] == 1
assert "pear" not in counts
tag_set = set(["web", "rust", "web", "cli"])
assert len(tag_set) == 3
assert "rust" in tag_set
assert "go" not in tag_set
print(f"apple: {counts['apple']}, tags: {len(tag_set)}")
Python 字典的 get(k, 0) 是处理未见键的标准防御写法。
# C++
C++#include <iostream>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <string>
#include <cassert>
int main() {
std::vector<std::string> words = {"apple", "banana", "apple", "orange", "banana", "apple"};
std::unordered_map<std::string, int> counts;
for (const auto& w : words) {
counts[w]++; // 下标访问未命中时自动插入值初值 0
}
assert(counts["apple"] == 3);
assert(counts["banana"] == 2);
assert(counts["orange"] == 1);
assert(counts.find("pear") == counts.end());
std::unordered_set<std::string> tag_set = {"web", "rust", "web", "cli"};
assert(tag_set.size() == 3);
assert(tag_set.count("rust") == 1);
assert(tag_set.count("go") == 0);
std::cout << "apple: " << counts["apple"] << ", tags: " << tag_set.size() << "\n";
}
C++ 中的 std::unordered_map 的 operator[] 会有副作用:若键不存在会自动创建默认构造的值;纯查询应使用 find()。
# 迁移误区与适用限制
- C++ 下标查询副作用:在只读检查时使用
m["unknown"]会意外向 map 插入一个默认键值对,破坏集合只读性,改变其大小。 - 哈希键与可变性:若将可变对象用作哈希键(如在 Python 中将列表作为键),会因为不可哈希直接抛错;若在 Java 中修改已存入 Map 的对象键的属性,会导致哈希码变更从而在哈希表中“永久丢失”。
# 运行与验证
语言版本基线见示例基线。 所有程序运行后控制台输出均为:
纯文本apple: 3, tags: 3
词频计数与去重大小校验全部通过。