C++ map 底层与AVL 树

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)

关键点

  1. mymap[str]++ 是最简词频统计写法:不存在则插默认值 0,再自增。
  2. AVL(1962,高度差 ≤ 1,读多写少场景更快)与 RB(1972,颜色弱平衡,C++11 起 std::map/std::set 实际采用)是两类最主要的平衡 BST。
  3. 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 特有的"按键访问":
    1. 找到 key 对应的元素并返回其引用;
    2. 找不到则自动插入一个 key -> 默认值 的键值对,再返回引用。
  • 统计场景常用写法(笔记示例):
cpp 复制代码
map<K, V> mymap;
for (const auto& str : arr) {
    mymap[str]++;   // 等价于:不存在则插入默认值 0,然后 ++
}
  • 有序性与插入的关系(笔记要点):
    1. 插入后容器保持字典序有序 (按 key 的 < 比较);
    2. 已有 key 重复插入不会改变原值(配合 operator[] 是自增/覆盖语义)。

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 核心思路

  1. 用 unordered_map<string, int> 统计词频;
  2. 用小顶堆保留前 k 个高频词(堆大小恒为 k);
  3. 堆的比较器:频次小的在堆顶;频次相同则字典序大的在堆顶(这样字典序更优的先被弹出,留下的就是字典序较小的 k 个);
  4. 弹出堆中全部元素,逆序得到答案。

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 卷中再次提及。
  • 为什么叫"最早":
    1. 它定义的平衡因子概念早于红黑树(AVL 1962 vs RB 1972);
    2. 最早的 AVL 定义:节点数不超过 2^h − 1(h 为高度);
    3. 最早的平衡约束:左右子树高度差不超过 1。

3.2 核心性质

  1. 是二叉搜索树(BST):左子树 < 节点 < 右子树;
  2. 每个节点的左右子树高度差(平衡因子)绝对值不超过 1;
  3. 左右子树本身也必须是 AVL 树(递归定义)。

平衡因子(Balance Factor) 定义:节点左子树高度 − 右子树高度。

允许值 ∈ {−1, 0, 1};|bf| > 1 即失衡,需要旋转修复。

3.3 四种失衡与旋转

情形 触发位置(相对失衡节点) 修复旋转
LL 左子树的左子树插入 对失衡节点右旋
LR 左子树的右子树插入 先对左子节点左旋 ,再对失衡节点右旋
RR 右子树的右子树插入 对失衡节点左旋
RL 右子树的左子树插入 先对右子节点右旋 ,再对失衡节点左旋
左旋(以 x 为轴,其右孩子 y 上移)
复制代码
    x                 y
     \      --左旋-->  /
      y               x
     / \               \
    β   γ               γ

步骤(笔记 2.2.1):

  1. 设 y = x->right;
  2. y 的左子树 β 挂到 x 的右子树;
  3. x 整体成为 y 的左子树;
  4. 更新 x、y 的高度,回溯路径上的平衡因子。
右旋(左旋的镜像)
复制代码
      x               y
     /      --右旋-->  \
    y                   x
   / \                   /
  α   β                 β

3.4 插入修复实例(笔记图示推演)

  • 基础树:Root(10),左 6,右 14;再插入 8、20、4、12、18、16 等节点,逐层检查平衡因子;
  • 插入 2 的过程(2.3.2 图):
    1. 在节点 10 的左子树方向插入 2;
    2. 节点 4 的左子树插入 2 后,节点 4 平衡因子 = 1;
    3. 节点 10 平衡因子仍 = 0;
    4. 若某节点 |bf| > 1,按上表执行对应旋转(示例中对节点 10 / 节点 4 做左旋恢复)。

主动回忆:插入导致失衡时,旋转发生在从新节点往上第一个 |bf|>1 的节点 ,且只需一次或两次旋转即可恢复整棵树的 AVL 性质。

3.5 查找过程(笔记 2.3.4 图)

搜索从根节点(如 a)开始:

  1. 在 a 处比较目标;
  2. 按 BST 性质决定进左子或右子,在 b 处继续比较;
  3. 在 c 处继续;
  4. 结果:找到返回节点,找不到返回空。
  • 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. 主动回忆清单

  1. map::insert 返回的 pair 中,bool 为 false 时迭代器指向谁?value 会被改吗?
  2. mymap[str]++ 对"不存在的 key"会发生什么?
  3. AVL 树和 RB 树谁最早?分别在哪一年?
  4. 平衡因子的定义式是什么?允许取值?
  5. LL 情形应该做左旋还是右旋?LR 情形分几步?
  6. 插入后需要旋转的位置如何确定?为什么旋转后整棵树恢复平衡?
  7. LC 692 堆比较器为什么要求"频次相同时字典序小的优先级低"?
  8. 为什么旋转只影响 O(1) 个节点(每次最多 2 次旋转)?

6. 易错点总结

  • 混淆 insert 与 operator[] 的覆盖语义;
  • 把 AVL 记成 RB:AVL 是 1962、高度差 ≤ 1;RB 是 1972、颜色弱平衡;
  • LR/RL 情形只做一次旋转(必须先对子节点旋一次再对父节点旋一次);
  • 旋转后忘记回溯更新高度(这是手写 AVL 最容易漏的一步);
  • LC 692 堆大小超过 k 不及时 pop,导致答案不是前 k。

任何的批评和建议欢迎指出,我们共同进步!

相关推荐
喆星时瑜1 小时前
R语言向量语法的详细教程
开发语言·r语言
释厄6231 小时前
物理发展史·量子力学=节点2·牛顿力学=节点3·宇宙的语言算法AI
人工智能·算法·microsoft
乃嘿仔1 小时前
AI 热点日报 · 2026-10-01
开发语言·人工智能·php
CQU_JIAKE1 小时前
9.30【A】
算法
INGNIGHT1 小时前
1897 · 会议室 3(扫描线)
数据结构·算法
All for pursuit.1 小时前
【贪心-2】253.会议室 II
数据结构·c++·算法·leetcode
垆边人似月.1 小时前
华为机考题(一):质数因子
算法·华为
重生之小比特2 小时前
【C++进阶】异常
开发语言·c++
朝朝辞暮i2 小时前
C++ 第 15 课:指针 * 和地址 &
开发语言·c++