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 成员变量与成员函数
// map 的核心成员(示意)// 底层:有序容器(C++11 起标准实现为红黑树,早期/教学场景常用 AVL 树)public:intmap_count(intn);// 统计接口示例publicmember functions:void:insert,remove,find,...1.2 operator[] 的行为
operator[]是 map 特有的"按键访问":- 找到 key 对应的元素并返回其引用;
- 找不到则自动插入一个
key -> 默认值的键值对,再返回引用。
- 统计场景常用写法(笔记示例):
map<K,V>mymap;for(constauto&str:arr){mymap[str]++;// 等价于:不存在则插入默认值 0,然后 ++}- 有序性与插入的关系(笔记要点):
- 插入后容器保持字典序有序(按 key 的
<比较); - 已有 key 重复插入不会改变原值(配合
operator[]是自增/覆盖语义)。
- 插入后容器保持字典序有序(按 key 的
1.3 map::insert 的返回值
insert返回pair<iterator, bool>:
| 情形 | first(迭代器) | second(bool) |
|---|---|---|
| 插入成功 | 指向新元素的迭代器 | true |
| 插入失败(key 已存在) | 指向等效元素的迭代器 | false |
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 参考代码
classSolution{public:vector<string>topKFrequent(vector<string>&words,intk){unordered_map<string,int>countMap;for(auto&s:words)countMap[s]++;autocmp=[](conststring&a,conststring&b,constunordered_map<string,int>&m){if(m[a]==m[b])returna<b;// 频次相同,字典序小的优先级低returnm[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());returnans;}};笔记中出现了自定义结构体比较器(
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。
任何的批评和建议欢迎指出,我们共同进步!