如何利用unordered_map高效统计文本单词频率?
- 内容介绍
- 文章标签
- 相关推荐
本文共计1061个文字,预计阅读时间需要5分钟。
直接使用 `std::unordered_map` 存储单词和频率,是 C++ 中实现简洁性与性能首选。它不排序、平均插入/查找时间复杂度为 O(1),比 `std::map` 快得多,尤其在文本包含数千个不同单词时差异明显。
但别光图快——std::unordered_map 的键必须可哈希,std::string 已内置支持,不用额外写哈希函数;若你用自定义结构体当键,就得自己提供 std::hash 特化或传入哈希仿函数。
- 头文件必须加:
#include <unordered_map> - 声明方式:
std::unordered_map<:string int> word_count;</:string> - 计数只需一行:
word_count[cleaned_word]++;(自动初始化为 0 再加 1) - 注意:空字符串
""也会被当作有效键存进去,后续遍历时得跳过
清洗单词比选容器更关键
绝大多数词频不准,不是因为用了 map 还是 unordered_map,而是单词没洗干净。比如 "Hello!"、"hello"、"HELLO" 默认会被算作三个不同单词。
清洗要分两步做,且顺序不能反:
立即学习“C++免费学习笔记(深入)”;
- 先过滤非字母数字字符:用
std::isalnum()判断每个字符,只保留字母和数字(保留don't中的撇号?那就得改逻辑,但标准清洗通常不要) - 再统一转小写:用
std::tolower()配合std::transform(),别手写循环拼接,容易漏掉 locale 问题 - 清洗后检查是否为空:
if (word.empty()) continue;,否则标点全被滤掉就剩空串,word_count[""]++会悄悄多计一次
按频次排序输出必须中转 std::vector
std::unordered_map 本身不支持按 value 排序,强行遍历然后在循环里找最大值是 O(n²),不可取。
正确做法是把所有键值对拷进 std::vector<std::pair<std::string, int>>,再用 std::sort() 自定义比较:
std::vector<std::pair<std::string, int>> vec(word_count.begin(), word_count.end()); std::sort(vec.begin(), vec.end(), [](const auto& a, const auto& b) { return a.second > b.second; // 频次高在前 });
如果频次相同还想按字典序排单词,改成:a.second != b.second ? a.second > b.second : a.first < b.first。
注意:a.second >= b.second 是错的——std::sort 要求严格弱序(strict weak ordering),相等时必须返回 false,否则行为未定义,可能崩溃或乱序。
从文件读取时别用 operator>> 直接切
std::cin >> word 或 iss >> word 看似方便,但它按空白分割,遇到 "C++"、"it's"、"well-known" 就会切歪:前者变成 "C++"(带加号),后者可能被拆成 "it's" 或 "it" + "s",取决于标点是否紧贴。
更鲁棒的做法是逐字符扫描,用状态机识别“字母/数字连续段”作为单词边界:
- 初始化一个空
std::string word - 遍历每个字符:
if (std::isalnum(c)) word += std::tolower(c); - 遇到非字母数字时,若
word非空,就计数并清空:if (!word.empty()) { word_count[word]++; word.clear(); } - 文件末尾别忘处理最后一个单词(
word可能还留着)
这个逻辑看似啰嗦,但能稳住 "a-b-c"、"'quoted'"、"C++17" 这类边界情况。真正跑线上文本时,清洗比容器选择花的时间多得多。
本文共计1061个文字,预计阅读时间需要5分钟。
直接使用 `std::unordered_map` 存储单词和频率,是 C++ 中实现简洁性与性能首选。它不排序、平均插入/查找时间复杂度为 O(1),比 `std::map` 快得多,尤其在文本包含数千个不同单词时差异明显。
但别光图快——std::unordered_map 的键必须可哈希,std::string 已内置支持,不用额外写哈希函数;若你用自定义结构体当键,就得自己提供 std::hash 特化或传入哈希仿函数。
- 头文件必须加:
#include <unordered_map> - 声明方式:
std::unordered_map<:string int> word_count;</:string> - 计数只需一行:
word_count[cleaned_word]++;(自动初始化为 0 再加 1) - 注意:空字符串
""也会被当作有效键存进去,后续遍历时得跳过
清洗单词比选容器更关键
绝大多数词频不准,不是因为用了 map 还是 unordered_map,而是单词没洗干净。比如 "Hello!"、"hello"、"HELLO" 默认会被算作三个不同单词。
清洗要分两步做,且顺序不能反:
立即学习“C++免费学习笔记(深入)”;
- 先过滤非字母数字字符:用
std::isalnum()判断每个字符,只保留字母和数字(保留don't中的撇号?那就得改逻辑,但标准清洗通常不要) - 再统一转小写:用
std::tolower()配合std::transform(),别手写循环拼接,容易漏掉 locale 问题 - 清洗后检查是否为空:
if (word.empty()) continue;,否则标点全被滤掉就剩空串,word_count[""]++会悄悄多计一次
按频次排序输出必须中转 std::vector
std::unordered_map 本身不支持按 value 排序,强行遍历然后在循环里找最大值是 O(n²),不可取。
正确做法是把所有键值对拷进 std::vector<std::pair<std::string, int>>,再用 std::sort() 自定义比较:
std::vector<std::pair<std::string, int>> vec(word_count.begin(), word_count.end()); std::sort(vec.begin(), vec.end(), [](const auto& a, const auto& b) { return a.second > b.second; // 频次高在前 });
如果频次相同还想按字典序排单词,改成:a.second != b.second ? a.second > b.second : a.first < b.first。
注意:a.second >= b.second 是错的——std::sort 要求严格弱序(strict weak ordering),相等时必须返回 false,否则行为未定义,可能崩溃或乱序。
从文件读取时别用 operator>> 直接切
std::cin >> word 或 iss >> word 看似方便,但它按空白分割,遇到 "C++"、"it's"、"well-known" 就会切歪:前者变成 "C++"(带加号),后者可能被拆成 "it's" 或 "it" + "s",取决于标点是否紧贴。
更鲁棒的做法是逐字符扫描,用状态机识别“字母/数字连续段”作为单词边界:
- 初始化一个空
std::string word - 遍历每个字符:
if (std::isalnum(c)) word += std::tolower(c); - 遇到非字母数字时,若
word非空,就计数并清空:if (!word.empty()) { word_count[word]++; word.clear(); } - 文件末尾别忘处理最后一个单词(
word可能还留着)
这个逻辑看似啰嗦,但能稳住 "a-b-c"、"'quoted'"、"C++17" 这类边界情况。真正跑线上文本时,清洗比容器选择花的时间多得多。

