📱
哈希表应用:C++ 自带的「超级字典」,拿来就用
不用自己造抽屉——unordered_map 是「词→词数」的超级字典,unordered_set 是「见过没见过」的 O(1) 雷达。
💡 打个比方:手机里的超级字典
手机通讯录不用一格格翻:输入名字直接弹出来。C++ 的 unordered_map 就是这样的超级字典:左边存 key(单词),右边存 value(次数),查一个、记一个都是「一步到位」;unordered_set 是它的简化版——只记「见过谁」,用来 O(1) 判断「是否出现过」。
📖 术语小抄:unordered_map = 无序的键值字典(key→value),平均 O(1);unordered_set = 无序集合,只存 key,平均 O(1) 判「在不在」;mp[key]++ = key 不存在时自动新建(初始 0)再 +1;set.count(x) = x 在不在集合里(1/0);set.insert(x) = 把 x 放进去(已有就不重复放);Two Sum = 两数之和(找两数相加 = 目标值)。
🖼️ 大图看明白:超级字典 + 词频 + Two Sum
查「apple」:不用翻页,一步直达
超级字典:左边单词,右边次数
apple
banana
cherry
3
2
1
划正字对账:apple ||| banana || cherry |
查一个词、记一个词,都是一步 O(1)
freq[w]++:6 个词逐一手推
apple → 字典里没有 → 新建条目,次数 = 1
banana → 字典里没有 → 新建条目,次数 = 1
apple → 字典里有 → 1 + 1 = 2
cherry → 字典里没有 → 新建条目,次数 = 1
banana → 字典里有 → 1 + 1 = 2
apple → 字典里有 → 2 + 1 = 3
Two Sum:need = 9 - x,seen = 已见过的数
x = 2:need = 7,seen 里没有 → seen = {2}
x = 7:need = 2,2 在 seen → 2 + 7 = 9 ✓
x = 4:need = 5,seen 里没有 → seen = {2,7,4}
x = 5:need = 4,4 在 seen → 4 + 5 = 9 ✓
蓝字 = 命中(次数 +1 / 找到一对) ✓ = 一对解(每对恰好找到一次)
🧩 子任务 1:unordered_map 入门——统计单词词频
🎯 小目标:会用 mp[w]++ 统计词频,能手推字典每一步的变化,知道「key 不存在时自动新建」这个魔法。
// 任务:统计一串单词里每个词出现几次
// 工具:unordered_map<string, int> freq;(左:单词,右:次数)
// 魔法:freq[w]++ —— w 不在字典里?自动新建(次数从 0 开始),再 +1
//
// 输入单词:apple banana apple cherry banana apple
// 手推字典的变化:
// apple → {} → {apple:1}
// banana → {apple:1} → {apple:1, banana:1}
// apple → {apple:1, banana:1} → {apple:2, banana:1}
// banana → {apple:2, banana:1, cherry:1} → {apple:2, banana:2, cherry:1}
// apple → {apple:2, banana:2, cherry:1} → {apple:3, banana:2, cherry:1}
// 最终:apple 3 次、banana 2 次、cherry 1 次(共 6 个词)
✅ 小检查:① freq[w]++ 对「第一次出现的 w」做了什么?② 如果单词换成「cat dog cat cat」,最终字典是什么?③ 怎么输出每个词和它的次数?
看看答案
① 自动新建条目(次数 = 0)再 +1 ——所以第一次出现就是 1。这正是 mp[key] 的「访问即创建」行为。
② {cat:3 , dog:1 }。
③ 用 for 遍历字典:for (auto &p : freq) cout << p.first << " " << p.second << endl;(p.first 是 key,p.second 是 value)。
⚠️ 易错:① unordered_map 的遍历顺序是乱的 (不是按输入顺序、也不是按字母序)——题目要求「按出现顺序/按字母序输出」时,要另想招(记下出现顺序,或换 map);② key 用 string 时别忘 #include 和 using namespace std。
🧩 子任务 2:unordered_set——去重和「见过没见过」O(1) 雷达
🎯 小目标:会用 set 做去重(保持第一次出现的顺序输出)和成员判断。
// unordered_set:只存 key、不存 value 的「名单」
// 两个超能力:
// seen.count(x) → x 在名单里吗?返回 1/0(O(1))
// seen.insert(x) → 把 x 加进名单(已经有了就不重复加)
//
// 应用 1:去重(保留每个数第一次出现的位置)
// 输入:3 1 4 1 5 9 2 6 5
// seen 空
// 3 → count=0 → 输出 3,insert 3 seen={3}
// 1 → count=0 → 输出 1,insert 1 seen={3,1}
// 4 → count=0 → 输出 4,insert 4 seen={3,1,4}
// 1 → count=1 → 跳过(见过)
// 5 → count=0 → 输出 5,insert 5 seen={3,1,4,5}
// 9 → 输出 9,insert 2 → 输出 2,insert 6 → 输出 6,insert
// 5 → count=1 → 跳过
// 结果:3 1 4 5 9 2 6(重复的 1、5 只留第一次)
✅ 小检查:① 「判断 x 是否出现过」用数组下标、链表、unordered_set 分别多快?② 去重输出为什么是 3 1 4 5 9 2 6 而不是 1 2 3 4 5 6 9?③ seen.count(x) 的返回值是什么?
看看答案
① 数组:O(1) 但要求 x 是范围小的非负整数;链表:O(n) 逐个比;unordered_set:平均 O(1) ,x 多大都行。
② 去重代码是「按输入顺序扫一遍 ,谁第一次出现就先输出谁」——set 只负责判重,不改变输出顺序(set 自己的遍历顺序才是乱的,这里没遍历 set)。
③ 返回 1(在)或 0(不在) ,可以直接写 if (seen.count(x))。
⚠️ 易错:① 把「set 遍历顺序是乱的」当成「按 set 输出就是去重后的有序结果」——要按原顺序去重,必须扫原数组 + set 判重 ;② insert 之后再用 count 判断——顺序反了,重复的就被放出去了(先 count 后 insert)。
🧩 子任务 3:Two Sum——一次遍历 + set 找补数
🎯 小目标:掌握「边扫边存」的 Two Sum 套路:对每个 x,问 seen 里有没有 9-x;能手推找出全部解。
// 两数之和:nums 里找两数相加 = target(这里 target = 9)
// 笨办法:两重循环 O(n²),n = 10 万 → 100 亿次,太慢
// 快办法:一次遍历 + seen 集合
// 扫到 x 时,补数 need = target - x
// 问:seen(前面扫过的数)里有 need 吗?
// 有 → 找到一对 (need, x)!
// 没有 → 把 x 存进 seen,继续扫
//
// nums = [2, 7, 4, 5, 11, 15],target = 9,手推:
// x=2:need=7,seen={} → 没有 → seen={2}
// x=7:need=2,2 ∈ seen → 命中!(2, 7) → seen={2,7}
// x=4:need=5,seen={2,7} → 没有 → seen={2,7,4}
// x=5:need=4,4 ∈ seen → 命中!(4, 5) → seen={2,7,4,5}
// x=11:need=-2 → 没有 → seen={2,7,4,5,11}
// x=15:need=-6 → 没有 → seen={2,7,4,5,11,15}
// 全部解:(2, 7) 和 (4, 5) —— 共 2 对,一次遍历 O(n) 找完
// 为什么每对只被找到一次?后面那个数问「need 在前面吗」,
// 前面的数不会回头再问——每个有序对恰好问一次。
✅ 小检查:① 这个办法为什么是 O(n)?② 如果 nums = [1, 2, 3, 4, 5],target = 6,有哪些解?③ 为什么不能「先把所有数 insert 进 set 再遍历」?(提示:x = 3, target = 6 时)
看看答案
① 只扫一遍 :每个 x 做 O(1) 的 count + O(1) 的 insert,共 n 次 → O(n) 。
② 扫 1:need=5 不在 → {1};扫 2:need=4 不在 → {1,2};扫 3:need=3 不在 → {1,2,3};扫 4:need=2 在 → (2,4);扫 5:need=1 在 → (1,5)。解:(2,4)、(1,5) 两对。(3+3=6 但只有一个 3,不算。)
③ 先全存再遍历,扫到 x=3 时 seen 里有 3(自己),会误报「3+3=6」——但数组里只有一个 3。边扫边存保证 need 一定在 x 前面 ,不会拿自己凑对。
⚠️ 易错:① 顺序必须是「先查 need,再 insert x 」——反过来就可能出现 x + x 的假解(只有一个 x 时);② 要「全部解」就继续扫完整个数组,找到一对不要 break;③ 输出顺序:先 found 的先输出(本题 2+7 在 4+5 前面)。
🧩 子任务 4:unordered_map vs map——O(1) 还是 O(log n)?要不要「有序」
🎯 小目标:分清 unordered_map 和 map 的速度与顺序差异,会按需求选。
// 两个「字典」兄弟:
// unordered_map:底层是哈希表 → 查/插平均 O(1),但 key 顺序是乱的
// map:底层是二叉搜索树(红黑树)→ 查/插 O(log n),但 key 永远从小到大排好
//
// 怎么选?看题目要不要「按 key 有序输出」:
// 只要快(判重、计数、Two Sum) → unordered_map / unordered_set
// 要按 key 从小到大输出 / 要「最大 key」「前驱后继」→ map / set
//
// 账怎么算(n = 10 万):
// 10 万次查找:
// unordered_map ≈ 10 万 × O(1) ≈ 10 万次比较
// map ≈ 10 万 × O(log n) ≈ 10 万 × 17 ≈ 170 万次比较
// 平时 unordered 快 10 几倍;但 map 给你「有序」这个 unordered 没有的本事
// (map 的 lower_bound / upper_bound 也能 O(log n) 找位置)
// 一句话:要速度选 unordered,要顺序选 map,两者 API 几乎一样,换个头就能用
✅ 小检查:① 统计词频后要求「按单词字母序输出」,用哪个?② 只做「这个数见过没」判断,用哪个?③ n = 10 万时,map 一次查找大约比较多少次?为什么是 17?
看看答案
① map (或 unordered_map 存完 + 把结果排序输出,两种都行;直接 map 最省事)。
② unordered_set ——平均 O(1),不需要顺序。
③ log₂(100000) ≈ 17 次 :树高约 log n 层,每层比较一次 key 走左还是走右,17 层就到。
⚠️ 易错:① 看到「有序输出」需求还在用 unordered_map 遍历——顺序是乱的,WA;② 反过来,只要速度却用了 map,n 大时可能 TLE(超时);③ 两者 API 几乎相同(mp[key]、mp.count、mp.size、for 遍历),切换 = 换一行声明 。
💻 完整代码:词频统计 + Two Sum(输出所有解)
🎯 小目标:读懂「freq[w]++ 自动建条目」和「先查 need 再 insert」两段代码;对照输出。
// ① 词频统计 + ② Two Sum(输出所有解)
#include <bits/stdc++.h>
using namespace std;
int main() {
// ① 词频:freq[w]++ —— w 不在就自动新建(0 起步)再 +1
vector<string> words = {"apple","banana","apple","cherry","banana","apple"};
unordered_map<string,int> freq;
for (auto &w : words) freq[w]++;
for (auto &p : freq) // 遍历:p.first=词,p.second=次数
cout << p.first << " " << p.second << endl;
// 输出(顺序随机):apple 3 / banana 2 / cherry 1
// ② Two Sum:target = 9,边扫边存,输出所有解
vector<int> nums = {2, 7, 4, 5, 11, 15};
int target = 9;
unordered_set<int> seen; // 「前面扫过的数」名单
for (int x : nums) {
int need = target - x; // 补数
if (seen.count(need)) // 前面有人和 x 凑成 target
cout << need << " + " << x << " = " << target << endl;
seen.insert(x); // 先查后存(顺序不能反!)
}
// 输出:2 + 7 = 9
// 4 + 5 = 9
return 0;
}
// 运行对照(②):
// x=2:seen 无 7 → 存 2
// x=7:seen 有 2 → 输出 2 + 7 = 9 → 存 7
// x=4:seen 无 5 → 存 4
// x=5:seen 有 4 → 输出 4 + 5 = 9 → 存 5
// x=11、15:need = -2、-6,seen 都没有 → 只存不输出
✋ 你来试试
第 1 题:用 freq[w]++ 统计 ["cat","dog","cat","fish","cat"],最终字典是什么?
{cat:3 , dog:1 , fish:1 }——cat 出现 3 次(每次 +1),dog 和 fish 各 1 次。输出顺序随机,但「词→次数」的对应是准的。
第 2 题:对 5 3 5 8 3 8 5 做「按原顺序去重」,输出什么?seen 最后是什么?
输出 5 3 8 (每个数只留第一次出现的位置);seen 最后 = {5, 3, 8}(3 个元素,重复的 insert 不生效)。
第 3 题:nums = [3, 3, 4, 7],target = 6。「边扫边存」能找到几对?是哪对?
1 对:(3, 3) 。手推:x=3 时 need=3,seen 空 → 没找到,存 3;第二个 x=3 时 need=3,seen 里有 3 → 命中 (3,3)。两个 3 一前一后,「先查后存」恰好能凑对;如果只有一个 3,就不会误报。
第 4 题(加餐):nums 有 200000 个数,找所有和为 target 的数对。两重循环最坏大约多少次?「边扫边存」多少次?
两重循环:200000 × 200000 / 2 ≈ 2×10¹⁰(200 亿)次 ,普通评测会超时;边扫边存:每个数 1 次 count + 1 次 insert,共 200000 次 (O(n)),飞快。这就是「哈希表换掉一重循环」的典型用法。
🎯 一句话记住:C++ 自带超级字典 ——unordered_map[key]++ 自动建条目、O(1) 计数;unordered_set 做 O(1) 去重和「见过没见过」雷达(先 count 后 insert 保原序去重);Two Sum = 边扫边存 :每个 x 先问 seen 里有没有 target-x、再把自己存进去,一次 O(n) 遍历找出所有 解;要速度选 unordered(O(1)),要「按 key 有序输出」换 map (O(log n),key 自动排好)。