news 2026/10/2 7:04:09

C++ map 底层与AVL 树

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 成员变量与成员函数

// map 的核心成员(示意)// 底层:有序容器(C++11 起标准实现为红黑树,早期/教学场景常用 AVL 树)public:intmap_count(intn);// 统计接口示例publicmember functions:void:insert,remove,find,...

1.2 operator[] 的行为

  • operator[]是 map 特有的"按键访问":
    1. 找到 key 对应的元素并返回其引用;
    2. 找不到则自动插入一个key -> 默认值的键值对,再返回引用。
  • 统计场景常用写法(笔记示例):
map<K,V>mymap;for(constauto&str:arr){mymap[str]++;// 等价于:不存在则插入默认值 0,然后 ++}
  • 有序性与插入的关系(笔记要点):
    1. 插入后容器保持字典序有序(按 key 的<比较);
    2. 已有 key 重复插入不会改变原值(配合operator[]是自增/覆盖语义)。

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

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

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 卷中再次提及。
  • 为什么叫"最早":
    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。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 7:02:09

可降解纸袋专用热封胶采购成本偏高

可降解纸袋专用热封胶的采购成本问题引起了越来越多企业的关注。随着环保法规日益严格、使用可降解材料成为了一项重要的行业趋势。这种热封胶用于粘合可降解纸袋、确保运输过程中防止泄漏等破损。然而、原材料的高成本以及生产工艺的复杂程度采购价格不断攀升。另外&#xff0…

作者头像 李华
网站建设 2026/10/2 7:00:19

用 Ace Data Cloud 快速接入 Kling 服装视频复刻:电商短视频生产的新选择

用 Ace Data Cloud 快速接入 Kling 服装视频复刻&#xff1a;电商短视频生产的新选择 如果你正在做电商、内容营销、品牌种草或素材自动化生产&#xff0c;最耗时的环节往往不是“想一个创意”&#xff0c;而是把商品图、模特图、参考视频、配音和成片流程真正串起来。传统方式…

作者头像 李华
网站建设 2026/10/2 7:00:09

数据库的盘搬不上对象存储?三种存储的分界线,一篇说清

评审存储方案的时候&#xff0c;有三类需求最常见&#xff1a;把 MySQL 的数据目录挪过去、给虚拟机提供磁盘、给全公司当一个共享网盘。这三类需求听着都像"存东西"&#xff0c;但对象存储对其中两类是接不住的。先分清三种存储各自承诺了什么&#xff0c;再决定要不…

作者头像 李华