C++ map 底层、AVL 树与 LeetCode 692 高频单词 ------ 学习大纲
本文基于手写笔记整理:std::map 接口与插入语义、LeetCode 692 前 K 个高频单词、AVL 树的定义/旋转/失衡修复。
可作为 Feynman 复习入口:先看"核心知识链",再用"主动回忆清单"自测。
摘要
核心结论一览
模块 核心结论 一句话要点 std::map 底层为有序容器(平衡 BST); operator[]缺键时自动插入默认值insert返回pair<it, bool>:成功指向新元素,失败指向等效元素且不覆盖旧值LeetCode 692 哈希表统计词频 + 小顶堆取 top-k 比较器:频次小者堆顶,频次相同字典序大者堆顶,堆大小恒为 k AVL 树 最早的自平衡 BST(1962),任意节点 |bf| ≤ 1 失衡分 LL/LR/RR/RL 四类,插入修复最多 2 次旋转;查找/插入/删除均 O(log n) 关键点
mymap[str]++是最简词频统计写法:不存在则插默认值 0,再自增。- AVL(1962,高度差 ≤ 1,读多写少场景更快)与 RB(1972,颜色弱平衡,C++11 起 std::map/std::set 实际采用)是两类最主要的平衡 BST。
- LC 692 中"字典序"只约束频次相同的词;堆内保留前 k 后逆序弹出即得答案。
易错点
insert失败时不覆盖 已有 value,与operator[]的赋值语义相反;- LR/RL 情形必须两步旋转(先子节点、再父节点),不能只旋一次;
- 每次旋转后必须回溯更新路径上的高度/平衡因子,手写 AVL 最常漏掉;
- LC 692 堆 size 超过 k 后必须立刻 pop,否则堆中保留的不是前 k。
1. std::map 的接口与实现概览
1.1 成员变量与成员函数
cpp
// map 的核心成员(示意)
// 底层:有序容器(C++11 起标准实现为红黑树,早期/教学场景常用 AVL 树)
public:
int map_count(int n); // 统计接口示例
public member functions:
void: insert, remove, find, ...
1.2 operator\[\] 的行为
operator[]是 map 特有的"按键访问":- 找到 key 对应的元素并返回其引用;
- 找不到则自动插入一个
key -> 默认值的键值对,再返回引用。
- 统计场景常用写法(笔记示例):
cpp
map<K, V> mymap;
for (const auto& str : arr) {
mymap[str]++; // 等价于:不存在则插入默认值 0,然后 ++
}
- 有序性与插入的关系(笔记要点):
- 插入后容器保持字典序有序 (按 key 的
<比较); - 已有 key 重复插入不会改变原值(配合
operator[]是自增/覆盖语义)。
- 插入后容器保持字典序有序 (按 key 的
1.3 map::insert 的返回值
insert 返回 pair<iterator, bool>:
| 情形 | first(迭代器) | second(bool) |
|---|---|---|
| 插入成功 | 指向新元素的迭代器 | true |
| 插入失败(key 已存在) | 指向等效元素的迭代器 | false |
cpp
map<K, V> mymap;
std::pair<map<K,V>::iterator, bool> ret;
ret = mymap.insert(pair<K, V>(key, value));
if (ret.second) {
// 新插入成功,ret.first 指向新元素
} else {
// key 已存在,ret.first 指向已有元素,value 未被修改
}
易错点:
insert失败时不会覆盖 已有 value(区别于operator[]的赋值)。想要"有则更新、无则插入"用
mymap[key] = value;想要"只在没有时插入"用insert_or_assign/ 检查ret.second。
2. LeetCode 692. 前 K 个高频单词
- Tags:哈希表、堆、排序、字符串、Trie、桶排序
- 题目 :给定字符串数组
words和整数k,返回前 k 个高频单词;答案按字典序排序(频次相同时按字典序)。
2.1 核心思路
- 用
unordered_map<string, int>统计词频; - 用小顶堆保留前 k 个高频词(堆大小恒为 k);
- 堆的比较器:频次小的在堆顶;频次相同则字典序大的在堆顶(这样字典序更优的先被弹出,留下的就是字典序较小的 k 个);
- 弹出堆中全部元素,逆序得到答案。
2.2 参考代码
cpp
class Solution {
public:
vector<string> topKFrequent(vector<string>& words, int k) {
unordered_map<string, int> countMap;
for (auto& s : words) countMap[s]++;
auto cmp = [](const string& a, const string& b,
const unordered_map<string,int>& m) {
if (m[a] == m[b]) return a < b; // 频次相同,字典序小的优先级低
return m[a] > m[b]; // 频次小的堆顶
};
priority_queue<string, vector<string>,
decltype(cmp)> pq(cmp, vector<string>(), countMap);
for (auto& s : countMap) {
pq.push(s);
if (pq.size() > k) pq.pop();
}
vector<string> ans;
while (!pq.empty()) { ans.push_back(pq.top()); pq.pop(); }
reverse(ans.begin(), ans.end());
return ans;
}
};
笔记中出现了自定义结构体比较器(
MyString带count、重载operator<)的写法,本质相同:把"频次 + 字典序"编码进一个可排序的类型,交给堆完成 top-k 选择。
2.3 复杂度
- 时间:
O(n log m + m log k)(n 为 words 长度,m 为不同词数); - 空间:
O(m)。
3. AVL 树(Adaptive Balance Tree / 自平衡二叉搜索树)
3.1 定义与历史
- AVL 树是最早发明的自平衡二叉搜索树 ,由苏联科学家 Adelson-Velskii 和 Landis 于 1962 年论文《An algorithm for the organization of information》中提出;1973 年 Knuth 在《The Art of Computer Programming》第 3 卷中再次提及。
- 为什么叫"最早":
- 它定义的平衡因子概念早于红黑树(AVL 1962 vs RB 1972);
- 最早的 AVL 定义:节点数不超过 2^h − 1(h 为高度);
- 最早的平衡约束:左右子树高度差不超过 1。
3.2 核心性质
- 是二叉搜索树(BST):左子树 < 节点 < 右子树;
- 每个节点的左右子树高度差(平衡因子)绝对值不超过 1;
- 左右子树本身也必须是 AVL 树(递归定义)。
平衡因子(Balance Factor) 定义:节点左子树高度 − 右子树高度。
允许值 ∈ {−1, 0, 1};|bf| > 1 即失衡,需要旋转修复。
3.3 四种失衡与旋转
| 情形 | 触发位置(相对失衡节点) | 修复旋转 |
|---|---|---|
| LL | 左子树的左子树插入 | 对失衡节点右旋 |
| LR | 左子树的右子树插入 | 先对左子节点左旋 ,再对失衡节点右旋 |
| RR | 右子树的右子树插入 | 对失衡节点左旋 |
| RL | 右子树的左子树插入 | 先对右子节点右旋 ,再对失衡节点左旋 |
左旋(以 x 为轴,其右孩子 y 上移)
x y
\ --左旋--> /
y x
/ \ \
β γ γ
步骤(笔记 2.2.1):
- 设 y = x->right;
- y 的左子树 β 挂到 x 的右子树;
- x 整体成为 y 的左子树;
- 更新 x、y 的高度,回溯路径上的平衡因子。
右旋(左旋的镜像)
x y
/ --右旋--> \
y x
/ \ /
α β β
3.4 插入修复实例(笔记图示推演)
- 基础树:Root(10),左 6,右 14;再插入 8、20、4、12、18、16 等节点,逐层检查平衡因子;
- 插入 2 的过程(2.3.2 图):
- 在节点 10 的左子树方向插入 2;
- 节点 4 的左子树插入 2 后,节点 4 平衡因子 = 1;
- 节点 10 平衡因子仍 = 0;
- 若某节点 |bf| > 1,按上表执行对应旋转(示例中对节点 10 / 节点 4 做左旋恢复)。
主动回忆:插入导致失衡时,旋转发生在从新节点往上第一个 |bf|>1 的节点 ,且只需一次或两次旋转即可恢复整棵树的 AVL 性质。
3.5 查找过程(笔记 2.3.4 图)
搜索从根节点(如 a)开始:
- 在 a 处比较目标;
- 按 BST 性质决定进左子或右子,在 b 处继续比较;
- 在 c 处继续;
- 结果:找到返回节点,找不到返回空。
- AVL 树高度 = O(log n),查找/插入/删除均为 O(log n)。
3.6 AVL vs 红黑树(补充,可展开)
- AVL:严格平衡(高度差 ≤ 1),查找更快;旋转次数更多,插入/删除稍慢;
- RB 树(1972):弱平衡,增删更稳定,std::map / std::set / C++11 起 std 容器实际采用;
- 应用场景:读多写少选 AVL;写多或对最坏情况敏感选 RB。
4. 知识链条串联(Feynman 视角)
operator[] 自动插入 → map 的有序性来自底层平衡 BST →
平衡 BST 的平衡约束(AVL:高度差≤1) → 失衡检测(平衡因子) →
旋转修复(LL/LR/RR/RL 四种) → top-k 场景用堆替代遍历排序(LC 692)。
5. 主动回忆清单
map::insert返回的 pair 中,bool 为 false 时迭代器指向谁?value 会被改吗?mymap[str]++对"不存在的 key"会发生什么?- AVL 树和 RB 树谁最早?分别在哪一年?
- 平衡因子的定义式是什么?允许取值?
- LL 情形应该做左旋还是右旋?LR 情形分几步?
- 插入后需要旋转的位置如何确定?为什么旋转后整棵树恢复平衡?
- LC 692 堆比较器为什么要求"频次相同时字典序小的优先级低"?
- 为什么旋转只影响 O(1) 个节点(每次最多 2 次旋转)?
6. 易错点总结
- 混淆
insert与operator[]的覆盖语义; - 把 AVL 记成 RB:AVL 是 1962、高度差 ≤ 1;RB 是 1972、颜色弱平衡;
- LR/RL 情形只做一次旋转(必须先对子节点旋一次再对父节点旋一次);
- 旋转后忘记回溯更新高度(这是手写 AVL 最容易漏的一步);
- LC 692 堆大小超过 k 不及时 pop,导致答案不是前 k。
任何的批评和建议欢迎指出,我们共同进步!