2 条题解
-
0
“std::map<string, int>(基于红黑树,有序)或 std::unordered_map<string, int>(基于哈希表,无序)是最佳选择”请说明这两个的区别?你好,我是阿西莫夫。
这是一个非常棒的计算机科学基础问题。选择
map还是unordered_map,往往决定了你的程序是“优雅地慢”还是“狂野地快”。简单来说:
std::map是一个一丝不苟的图书管理员,而std::unordered_map是一个拥有瞬间移动魔法的快递员。下面我从原理、性能、特性三个维度为你详细拆解它们的区别:
1. 核心区别一览表
特性 std::map (有序映射) std::unordered_map (无序映射) 底层实现 红黑树 (Red-Black Tree) 哈希表 (Hash Table) 内部顺序 有序 (按 Key 的大小自动排序) 无序 (杂乱无章,看似随机) 查找速度 (稳定,较快) (平均极快,最坏 ) 插入/删除 (平均) 内存占用 较高 (每个节点存父/子指针和颜色) 更高 (需预分配桶数组,利用率低) Key的要求 需要定义 <运算符 (可比较)需要定义 hash函数和==运算符引入标准 C++98 (老牌经典) C++11 (现代标准)
2. 深度原理解析
🤖 std::map:严谨的红黑树
想象你在查一本纸质字典。
- 原理:字典里的单词是按 A-Z 严格排序的。
- 查找:你想找 "Science",你不会从第一页翻到最后一页。你会先翻到中间,看是 "M",知道 "S" 在后面,再往后翻一半……这就是二分查找的逻辑。
- 特点:无论数据怎么插入,它永远保持有序。
- 优点:
- 有序性:如果你需要按顺序输出所有汉字(如“按拼音排序”),
map是唯一选择。 - 稳定性:查询时间非常稳定,永远是 。不会因为运气不好而变慢。
- 有序性:如果你需要按顺序输出所有汉字(如“按拼音排序”),
⚡ std::unordered_map:魔法的哈希表
想象你有 1000 个标了号的魔法桶。
- 原理:你有一个数学公式(哈希函数)。给它一个词 "Science",公式算出
886。你就直接把数据扔进 886 号桶里。 - 查找:下次你要找 "Science",公式一算还是
886,你直接把手伸进 886 号桶就能拿出来。不需要翻找,一步到位。 - 特点:桶里数据的物理位置和字母顺序无关,完全取决于那个数学公式。
- 缺点(哈希冲突):
- 万一 "Science" 算出 886,"Math" 也算出 886 怎么办?它们会挤在同一个桶里,排成一条小链表。
- 最坏情况:如果运气极差,所有词都算出了 886,哈希表就退化成了链表,查询速度变成最慢的 。但在 OI 竞赛中,除非出题人刻意构造数据攻击(卡哈希),一般情况都非常快。
3. 代码直观对比
为了让你看到“有序”和“无序”的直接区别,请看这段代码:
#include <iostream> #include <map> #include <unordered_map> #include <string> using namespace std; int main() { // 1. 使用 std::map (红黑树) map<string, int> ordered; ordered["Zeus"] = 1; ordered["Apollo"] = 2; ordered["Athena"] = 3; cout << "--- std::map (自动排序) ---" << endl; for (auto const& [key, val] : ordered) { cout << key << ": " << val << endl; } // 2. 使用 std::unordered_map (哈希表) unordered_map<string, int> unordered; unordered["Zeus"] = 1; unordered["Apollo"] = 2; unordered["Athena"] = 3; cout << "\n--- std::unordered_map (乱序) ---" << endl; for (auto const& [key, val] : unordered) { cout << key << ": " << val << endl; } return 0; }运行结果可能如下:
--- std::map (自动排序) --- Apollo: 2 <-- A 开头排第一 Athena: 3 Zeus: 1 <-- Z 开头排最后 --- std::unordered_map (乱序) --- Zeus: 1 <-- 顺序完全不可预测 Athena: 3 Apollo: 24. 什么时候用哪个?(做题经验)
作为一名正在准备信息学竞赛的学生,你应该遵循以下**“阿西莫夫法则”**:
- 首选
unordered_map:- 如果题目只要求你**“统计次数”、“判断是否存在”**(如刚才的“虚词账本”题、或者“两数之和”)。
- 因为它的速度通常比
map快 2-3 倍,能帮你避免 TLE(超时)。
- 必须用
map的情况:- 如果题目要求**“按字典序输出结果”**。
- 如果题目需要用到**“二分查找”相关功能**(如
lower_bound/upper_bound,比如查找“第一个大于等于某值的键”)。 - 如果你的键(Key)是
pair<int, int>或vector<int>这种复杂类型,且你不想自己手写复杂的 Hash 函数(map自带对 pair 的比较,直接能用)。
总结
map:优雅、有序、稳定,但慢一点。unordered_map:暴力、杂乱、极快,但有被“卡”的风险。
在“虚词账本”这道题中,因为我们只关心“字出现了几次”,而不关心“字按拼音怎么排”,所以
unordered_map是理论上的最佳选择。 -
0
C++ 标准解答 (NOIP C++14)
这份代码使用了
std::unordered_map,这是 C++11 引入的标准哈希表,查询效率理论上为 。/** * 题目: 书童的“虚词账本” (Word Frequency Counter) * 作者: Isaac Asimov (AI) * 知识点: std::unordered_map 的使用 (Hash Map) */ #include <iostream> #include <string> #include <unordered_map> // 引入哈希表头文件 (C++11) // 如果编译器较老不支持 C++11,可以使用 <map>,用法完全一样,只是底层是树 using namespace std; int main() { // 1. IO 优化 ios_base::sync_with_stdio(false); cin.tie(NULL); int n; if (!(cin >> n)) return 0; // 2. 定义哈希表 (账本) // Key (键): string -> 汉字 // Value (值): int -> 出现的次数 unordered_map<string, int> word_counts; // 3. 读取文章并统计 for (int i = 0; i < n; i++) { string word; cin >> word; // map 的神奇操作符 []: // 如果 word 存在,返回对应的 value 的引用 // 如果 word 不存在,自动插入 {word, 0} 并返回 value 的引用 word_counts[word]++; } // 4. 处理查询 int m; cin >> m; for (int i = 0; i < m; i++) { string query; cin >> query; // 直接输出 map 中记录的次数 // 如果 query 从未出现过,word_counts[query] 会自动创建并返回 0 // (注:在某些严格场景下,若不想污染 map,应用 .find() 或 .count(), // 但本题只求输出,直接用 [] 最简洁) cout << word_counts[query] << endl; } return 0; }
数据生成器 (Generator)
生成器会从一个预定义的“文言文常用字库”中随机抽取字符生成文章,并生成包含“存在”和“不存在”的查询。
请保存为
gen_hashmap.cpp并运行。/** * Project: The Scholar's Particle Counter (HashMap Data Generator) * Author: Isaac Asimov (AI) */ #include <iostream> #include <fstream> #include <random> #include <string> #include <vector> #include <unordered_map> using namespace std; // ========================================== // Part 1: 标准解答逻辑 // ========================================== class Solution { public: void solve(string in_file, string out_file) { ifstream cin(in_file); ofstream cout(out_file); if (!cin.is_open()) return; int n; cin >> n; unordered_map<string, int> counts; for(int i=0; i<n; i++) { string s; cin >> s; counts[s]++; } int m; cin >> m; for(int i=0; i<m; i++) { string s; cin >> s; cout << counts[s] << endl; } cin.close(); cout.close(); cout << "Generated: " << out_file << endl; } }; // ========================================== // Part 2: 数据生成逻辑 // ========================================== mt19937 rng(2025); int rand_int(int min, int max) { uniform_int_distribution<int> dist(min, max); return dist(rng); } // 文言文常用词库 vector<string> dictionary = { "之", "乎", "者", "也", "矣", "焉", "哉", "曰", "云", "子", "君", "臣", "民", "国", "仁", "义", "礼", "智", "信", "上", "下", "古", "今", "山", "川", "遂", "乃", "复", "安", "耳", "耶" }; void generate_input(int case_id) { string filename = to_string(case_id) + ".in"; ofstream fout(filename); int n, m; if (case_id <= 3) { n = 20; m = 5; } else if (case_id <= 7) { n = 1000; m = 100; } else { // 压力测试: 10万数据量 n = 100000; m = 100000; } fout << n << endl; // 生成文章 // 为了制造大量重复(让计数有意义),我们只从较小的字典中采样 // 同时也生成一些随机字符串作为“生僻字” vector<string> article_words; for (int i = 0; i < n; i++) { string word; if (rand_int(1, 100) <= 80) { // 80% 概率使用常用虚词 word = dictionary[rand_int(0, dictionary.size() - 1)]; } else { // 20% 概率生成随机生僻字 (用字母模拟,便于处理) // 实际汉字是多字节,string 都能处理,这里为了生成器简单用字母代替部分生僻词 // 题目描述中说了是字符串,所以混用汉字和字母是合法的 word = "Word" + to_string(rand_int(1, 100)); } fout << word << (i == n-1 ? "" : " "); article_words.push_back(word); } fout << endl; fout << m << endl; // 生成查询 for (int i = 0; i < m; i++) { string query; int dice = rand_int(1, 100); if (dice <= 60) { // 60% 查询文章中存在的词 (从字典里选) query = dictionary[rand_int(0, dictionary.size() - 1)]; } else if (dice <= 90) { // 30% 查询肯定不存在的词 query = "MobilePhone"; } else { // 10% 查询随机生成的生僻字 query = "Word" + to_string(rand_int(1, 100)); } fout << query << endl; } fout.close(); } int main() { ios_base::sync_with_stdio(false); cout << "--- Generating HashMap Data ---" << endl; Solution solver; for (int i = 1; i <= 10; i++) { generate_input(i); string in = to_string(i) + ".in"; string out = to_string(i) + ".out"; solver.solve(in, out); } cout << "--- Done! ---" << endl; return 0; }阿西莫夫的点评
- 汉字与 String:
对于 C++ 的
std::string来说,一个汉字(UTF-8 编码下占 3 字节)和一个英文字母并没有本质区别,它们都只是一串char数据。map可以完美处理它们。这是对**“数据抽象”**概念的最好演示。 - 文言文的韵律: 通过统计“之乎者也”的频率,我们其实是在做最原始的自然语言处理(NLP)。你会发现,越是经典的古文,这些虚词的分布越有规律。这道题不仅考了算法,还稍微触碰了一下计算语言学的大门。
- 汉字与 String:
对于 C++ 的
- 1
信息
- ID
- 19249
- 时间
- 1000ms
- 内存
- 32MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者