AI Library · 知识库

# 字典与集合:词频统计与去重

学习目标:通过词频统计与标签去重场景,掌握哈希表、集合结构在各语言中的初始化、缺失键查询与增量更新模式。

前置知识与范围:基本类型与循环。演示在内存中对离散标签的频次统计与哈希去重。

# 问题与边界

  1. 词频统计(Map 映射):
    • 输入词列表:["apple", "banana", "apple", "orange", "banana", "apple"]。
    • 预期结果:"apple" 出现 3 次,"banana" 出现 2 次,"orange" 出现 1 次。
    • 缺失键契约:查询不存在的词(如 "pear")时,必须明确返回缺失(如 None / null / false)或安全默认值 0,不得发生不可控崩溃。
  2. 标签去重(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()。

# 迁移误区与适用限制

# 运行与验证

语言版本基线见示例基线。 所有程序运行后控制台输出均为:

纯文本apple: 3, tags: 3

词频计数与去重大小校验全部通过。

# 相关阅读