ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++ STL 常用 API 实战指南:刷题与面试必备的容器与算法技巧

C++ STL 常用 API 实战指南:刷题与面试必备的容器与算法技巧 先聊个现象。很多准备蓝桥杯、力扣周赛或算法面试的人卡点往往不是“没想到算法”而是“想到了算法但代码写不出来”。明明知道这道题该用前缀和结果循环里把索引写错知道要用单调队列却搞不清 deque 的 front 和 back 方向。说白了菜谱背得再熟刀工跟不上炒出来的菜就是夹生。C 刷算法题的大部分战斗力来自标准库 STL 和 algorithm 这套现成的 API这也是我想系统谈透的内容。这篇文章不是什么官方手册更像是我从刷题、面试和反复复盘里攒下来的一份 C 算法题常用 API 实战笔记。适合刚入门想提升刷题效率的人也适合准备算法岗面试前想快速过一遍 STL 常见操作的老手。每个 API 我都会尽量说清楚复杂度、容易翻车的细节以及它真正有用的刷题场景。篇幅不短但保证每一节都能直接拿来用。1. 入场准备刷题前必须搞定的输入输出基础设施1.1 头文件该带多少万能头文件与标准头文件的取舍最开始刷题时我习惯直接写#include bits/stdc.h因为蓝桥杯、牛客这类环境基本都支持一个头文件把所有标准库全拉进来写惯了之后确实省心。但随着面试和工程实践增多我的建议是比赛和在线评测可以用万能头日常工作面试手撕代码尽量写标准头文件组合。原因很简单。第一bits/stdc.h在部分企业自研编译环境和线上笔试平台里不一定可用如果你在面试现场手写代码编译器不认识这个头文件会很尴尬。第二它会把大量用不到的模板实例化进来编译时间明显变长虽然刷题时体感不明显但对工程习惯的养成有副作用。我的做法是本地刷题、打比赛用万能头面试准备和工程代码里用下面这组标准头#include iostream #include string #include vector #include algorithm #include numeric #include map #include set #include unordered_map #include unordered_set #include queue #include stack #include deque #include limits这套组合能覆盖九成以上刷题场景。需要处理格式化输出时再补一个#include iomanip需要用字符串流时补#include sstream。宁可多写一行也比突然发现某个环境没有bits/stdc.h而整个代码跑不起来强。1.2 输入输出加速与格式化输出的隐藏坑很多新手刚用cin/cout刷题时会疑惑为什么同一份逻辑用scanf就过用cin就超时原因在于cin默认与 C 标准 I/O 同步每次读取都要同步缓冲区状态性能损耗非常大。解决办法是两行代码ios::sync_with_stdio(false); cin.tie(nullptr);第一行关闭与 stdio 的同步第二行解除cin与cout的绑定避免每次交替输入输出都刷新缓冲区。实测下来这样配置后的cin/cout速度基本能追平scanf/printf。这里有个很多人踩过的坑关闭同步后不要再混用cin和scanf也不要混用cout和printf。我曾经在一份代码里先用cin读了几个整数再用scanf读字符结果字符读出来完全不对。原因就是关闭同步后两个流的内部状态不再保持一致顺序已经乱了。格式化输出方面如果题目要求保留两位小数我一般直接用cout fixed setprecision(2) ans endl;fixed表示以小数形式输出setprecision(2)控制小数点后的位数。如果忘了fixedsetprecision(2)只会控制总有效数字位数结果可能完全出乎意料。建议把这个组合拳直接封装成一个输出函数或者宏避免每次都写错。还有一个做题常遇到的格式问题输出数组时要求每个元素用空格隔开、末尾不能有额外空格。我一般用这个模式for (int i 0; i n; i) { if (i) cout ; cout nums[i]; }这种写法省掉了判断i n-1的分支是老手们默认的格式处理方式。2. 容器类 APIvector 与 string 的高频用法2.1 vector 的初始化玄机、扩容机制与 erase-remove 惯用法vector是刷题出现频率最高的容器没有之一。先看一个我亲眼见过无数次的新手错误vectorint a(5); // 5 个元素全部初始化为 0 vectorint b{5}; // 1 个元素值为 5()和{}的区别在算法题里非常容易踩。vectorint v(n)是建一个包含n个默认值元素的数组vectorint v{1, 2, 3}才是列表初始化。如果n本身是一个变量不小心写成花括号代码连编译都可能不过或者产生完全不符合预期的结果。二维数组初始化也是高频考点vectorvectorint matrix(n, vectorint(m, 0));这行代码创建了n行m列的矩阵所有元素初始化为 0。注意内层vectorint(m, 0)不能省略否则每个元素是空数组。vector的扩容机制也是一个面试常客。push_back的均摊复杂度是 O(1)原因是容量不够时会按倍数扩容通常是 1.5 倍或 2 倍把旧元素搬过去。理解这一点有助于解释为什么频繁push_back性能还可以但如果你提前知道元素数量最好先调用reservevectorint v; v.reserve(100000);reserve只增加容量不改变元素个数。而resize会真正改变size并把新增元素默认初始化。很多人分不清这两个记住一句话reserve管的是“能装多少”resize管的是“现在有多少”。后者可以配合v[i] x直接用下标赋值前者之后仍然只能push_back。删除方面有个大坑。vector的erase删除中间元素的复杂度是 O(n)因为要搬移后面的元素。如果你在循环里逐个删除符合条件的元素代码会慢到离谱。标准做法是 erase-remove 惯用法v.erase(remove_if(v.begin(), v.end(), [](int x) { return x % 2 0; }), v.end());remove_if会把不满足条件的元素挪到前面返回新的逻辑末尾迭代器然后再用erase把这个位置到原末尾之间的元素一次性删掉。这比循环erase的 O(n²) 快得多。最后补一个和迭代器失效相关的重要提醒vector在插入扩容时所有指向旧内存的迭代器、指针和引用都会失效删除非末尾元素时指向删除位置之后元素的迭代器也会失效。刷题时如果发现“迭代器越界”或“空指针崩溃”优先检查是不是这里出了问题。我自己的习惯是需要频繁从头部删除的题目直接改用deque或者用下标维护左右指针而不是在vector上反复erase。2.2 string 的匹配、截取与字符串数字转换string是处理文本类算法题的命脉。最常用的是substrstring s hello; string sub s.substr(1, 3); // ell string tail s.substr(2); // llo从位置 2 到末尾substr的时间复杂度是 O(n)在大量截取的场景下要注意总复杂度。find用来找子串位置找不到时返回string::npos。正确判断写法是if (s.find(abc) ! string::npos) { // 找到了 }注意npos是一个size_t类型的大数不是 -1。如果把find的返回值直接当作int判断是否会遇到npos会有类型隐患。另外find_first_of和find_last_of是找字符串中任意一个目标字符第一次出现的位置比如s.find_first_of(aeiou)能快速找到第一个元音字母的位置适合处理字符集合类问题。字符串和数字转换也是高频操作。stoi、stol、stoll可以把字符串转成整数to_string把整数转回字符串。这里有个隐藏功能很多人不知道stoi可以指定进制。int val stoi(ff, nullptr, 16); // 255按分隔符拆分字符串也是常见需求。如果分隔符是空格直接上stringstream最省事string line; getline(cin, line); stringstream ss(line); string word; while (ss word) { // 处理每个单词 }这里有个细节上面用getline(cin, line)之前如果已经有cin n这种输入n输入后会留下一个换行符在缓冲区直接getline会把空行读进来。解决办法是在cin n后先cin.ignore();或getchar();吞掉换行。这个坑我在实习生代码里见过不下十次。排序字符串时直接sort(s.begin(), s.end());string的迭代器就是普通随机访问迭代器完全可以直接喂给sort。当题目要求按字典序重新排列字符时这行代码就是最标准的答案。3. 栈、队列与优先队列适配器容器的实战选择3.1 stack、queue 与 deque 的应用细节stack和queue只是容器适配器它们默认基于deque实现功能被严格限制栈只能看到栈顶队列只能看到队头和队尾。这种限制是好事它迫使你使用正确的操作方式不容易写出歧义代码。但在算法题中我经常用vector模拟栈vectorint st; st.push_back(x); // 入栈 int top st.back(); // 取栈顶 st.pop_back(); // 出栈为什么不用stack因为vector能遍历栈内所有元素在调试时方便很多。有时候题目需要同时知道栈顶信息和栈内某个位置的元素stack就无能为力了。queue在 BFS广度优先搜索里几乎是固定搭配queuepairint, int q; q.push({0, 0}); while (!q.empty()) { auto [x, y] q.front(); // C17 结构化绑定 q.pop(); }不过如果 BFS 的每一层需要按“层”来处理直接用queue配合当前层大小的写法更稳int size q.size(); for (int i 0; i size; i) { // 处理同一层的节点 }deque是双端队列两端插入删除都是 O(1)。它最大的用武之地是滑动窗口类题目尤其是求滑动窗口最大值。标准做法是维护一个单调递减的双端队列dequeint dq; for (int i 0; i n; i) { // 去掉超出窗口范围的队头 if (!dq.empty() dq.front() i - k 1) dq.pop_front(); // 从队尾弹出比当前值小的元素保持单调性 while (!dq.empty() nums[dq.back()] nums[i]) dq.pop_back(); dq.push_back(i); if (i k - 1) ans.push_back(nums[dq.front()]); }这段代码是“滑动窗口最大值”的标准解队列里存的是数组下标而不是元素值。这样可以通过下标判断元素是否已经滑出窗口。deque的front对应窗口内的最大值候选back是插入新元素时用来维持单调性的位置。不熟悉的时候很容易把front和back的操作写反建议自己手推一遍。3.2 priority_queue 的堆操作与自定义比较器priority_queue是算法题里的另一个高频工具它封装了堆结构。默认是大顶堆也就是队头是最大元素priority_queueint pq; pq.push(5); pq.push(1); pq.top(); // 5小顶堆则需要三个模板参数priority_queueint, vectorint, greaterint pq;这里第二个参数是底层容器第三个是比较器。很多新手只写priority_queueint, greaterint编译直接报错原因就是少了底层容器参数。priority_queue最实用的场景是 TopK 问题。要求前 K 个最大元素时维护一个大小为 K 的小顶堆逻辑是priority_queueint, vectorint, greaterint pq; for (int x : nums) { if (pq.size() k) pq.push(x); else if (x pq.top()) { pq.pop(); pq.push(x); } } // 堆里剩下的就是最大的 K 个元素这样做的时间复杂度是 O(n log k)比全量排序的 O(n log n) 好空间复杂度 O(k)。自定义比较器是这里最大的坑。sort的比较器返回true表示“第一个参数应该排在第二个前面”而priority_queue的比较器返回true表示“第一个参数的优先级比第二个低”也就是应该往后放。这个语义刚好相反。举个例子我们想实现从小到大出队的小顶堆如果用自定义结构体struct Cmp { bool operator()(int a, int b) { return a b; // 注意这里用大于号 } }; priority_queueint, vectorint, Cmp pq;为什么是a b因为当a b时比较器返回true它认为a的优先级更低所以b会排在前面。这正好实现了小顶堆。新手常犯的错误是把sort的a b抄过来结果得到一个不按预期工作的堆。我实际刷题时很少写结构体更喜欢用auto加 lambda 表达式auto cmp [](const vectorint a, const vectorint b) { return a[1] b[1]; }; priority_queuevectorint, vectorvectorint, decltype(cmp) pq(cmp);注意构造函数必须传入cmp这个 lambda 对象。这种写法在处理复杂结构排序时比写仿函数直观很多但代码量略大。还有一个小技巧当题目需要同时维护最大值和最小值时可以开两个堆然后用一个延迟删除的哈希表处理“已经知道不该在堆里但还没弹出的元素”。具体做法是准备删除某元素时把它记录到unordered_map的计数里真正从堆顶弹出元素时先检查它是否被标记删除如果是就丢弃直到堆顶是合法元素。这个技巧在动态中位数、双堆问题里很实用比手动维护删除标记高效得多。4. 关联容器set、map 与无序哈希的选择题4.1 set/map 的有序性与 lower_bound 联动set和map底层都是红黑树元素按 key 有序存储插入、删除、查找都是 O(log n)。刷题时它们最大的价值在于“动态维护有序集合”。比如一个不断插入和删除元素、还要反复查找某个值是否存在的问题set就是天然的工具。map的[]运算符是个典型的“温柔陷阱”mapstring, int cnt; cnt[apple]; // 没问题key 不存在时会先插入value 默认 0再自增但如果只是想判断某个 key 是否存在千万别用[]if (cnt[banana]) { ... } // 错误banana 不存在时会被插入到 map 中正确的存在性判断是if (cnt.find(banana) ! cnt.end()) { ... }顺序遍历map天然按 key 升序输出。需要降序时可以指定greaterstringmapstring, int, greaterstring mp;set在刷题中的另一个高频用法是查找元素位置。注意区分全局的std::lower_bound和容器成员函数set::lower_bound。对于vector这类随机访问容器用全局版本没问题但对于set全局std::lower_bound的复杂度是 O(n)因为set的迭代器不是随机访问迭代器需要线性移动。set自带的lower_bound成员函数能利用红黑树结构做到 O(log n)。auto it st.lower_bound(target);这个操作在很多动态二分题里非常关键。例如维护一个有序集合每次询问大于等于target的最小元素直接用set::lower_bound就好不要手写循环去遍历。multiset允许重复元素但在算法题里我尽量少用因为它的接口不够丰富很多操作需要手动配合find和erase完成。一个经验是如果需要对一堆可重复数据做“动态取最小 删除指定元素”的操作multiset很合适如果只是静态排序直接vector sort反而更简单。4.2 unordered_map/unordered_set 的哈希世界与防卡哈希unordered_map的平均复杂度是 O(1)比map在频繁查找插入的场景下快得多但它的底层是哈希表存在退化风险。理论上当所有元素哈希冲突到同一个桶时操作复杂度会退化到 O(n)。在一些比赛和在线评测平台上有人会故意构造数据去卡默认哈希函数让unordered_map超时。这就是所谓的“卡哈希”。不信你可以搜一下很多打 Codeforces 的选手的经验几乎每个人都被哈希卡过至少一次。解决方案是自定义一个哈希函数并把它传给unordered_map作为第三个模板参数。我自己常用的写法是struct CustomHash { static uint64_t splitmix64(uint64_t x) { x 0x9e3779b97f4a7c15; x (x ^ (x 30)) * 0xbf58476d1ce4e5b9; x (x ^ (x 27)) * 0x94d049bb133111eb; return x ^ (x 31); } size_t operator()(uint64_t x) const { static const uint64_t FIXED_RANDOM chrono::steady_clock::now().time_since_epoch().count(); return splitmix64(x FIXED_RANDOM); } }; unordered_maplong long, int, CustomHash mp;这段代码的精髓在加入了一个程序运行时的随机种子攻击者无法预知哈希分布就难以构造碰撞数据。对于字符串键可以把string转换成 64 位整数再哈希或者写一个专门针对字符串的版本。虽然平时刷力扣、蓝桥杯不太会遇到恶意卡哈希但在对抗性强的比赛中这是个非常重要的保命技巧。另一个值得注意的点是unordered_map的内存占用远大于map因为哈希表需要预分配桶数组。如果数据量极大且内存紧张可能需要考虑改用map。另外pairint, int这类复合类型默认没有哈希函数使用unordered_mappairint, int, int直接编译不过。解决办法有两个要么改用mappairint, int, int让红黑树帮你排序要么自己写一个把pair编码成long long的哈希函数。如果坐标范围已知且不大把两个 int 合并成一个 long long 再放进unordered_maplong long, int是最简单的方案。4.3 迭代器失效的统一认知与删除操作的正确姿势迭代器失效这个问题我在面试里问过不少人能完整说清楚的不多。这里统一整理一下vector插入扩容时所有迭代器失效删除非末尾元素时指向被删位置及之后元素的迭代器失效。deque两端操作不影响已有迭代器但在中间插入或删除会影响。map/set只有指向被删除元素的迭代器失效其他位置的迭代器仍然有效。所以可以放心地边遍历边删除。unordered_map/unordered_set插入可能导致 rehash此时所有迭代器失效删除时只有指向被删元素的迭代器失效。在容器遍历过程中删除元素时正确写法很重要。对关联容器C11 之后erase会返回下一个迭代器auto it mp.begin(); while (it ! mp.end()) { if (shouldDelete(it-second)) { it mp.erase(it); } else { it; } }对vector则不建议在遍历中逐个erase优先用前面介绍的erase remove_if。记住一句话先标记、后统一删除是避免迭代器失效最稳妥的思想。5. 算法库排序、二分、排列一键调用5.1 sort 家族的玄机与自定义比较器的“严格弱序”sort是刷题最常用的算法没有之一。它底层是内省排序综合了快排、堆排和插入排序最坏复杂度 O(n log n)实际性能非常稳。用法sort(v.begin(), v.end()); // 降序 sort(v.begin(), v.end(), greaterint()); // 自定义排序 sort(v.begin(), v.end(), [](const pairint, int a, const pairint, int b) { if (a.first ! b.first) return a.first b.first; return a.second b.second; });自定义比较器最大的坑是“必须满足严格弱序”。简单理解如果a和b是等价的既不小于对方也不大于对方比较器必须返回false不允许在a b和b a上都返回true。否则排序结果不确定甚至有触发未定义行为导致程序崩溃的可能。最常见的错误写法是把当成比较器sort(v.begin(), v.end(), [](int a, int b) { return a b; // 错误等价元素时返回 true破坏严格弱序 });另一个高频需求是稳定排序。stable_sort会保留相等元素的原始相对顺序底层是归并排序时间 O(n log n)但会额外申请空间。当你先按次要字段排序、再按主要字段排序时如果第一次用普通sort第二次排序就会把第一次的相对顺序打乱必须改用stable_sort。不过我的做法是直接在最终比较器里依次比较多个字段一个sort搞定就不需要依赖稳定性。partial_sort和nth_element是容易被忽视的两个工具。partial_sort会把前 K 个元素排成有序后面元素不管适合“只要最小的 K 个值”。nth_element更极端它只保证第 N 个元素就位左边的元素都不大于它右边的都不小于它平均 O(n)。求中位数、第 K 大元素时nth_element比完整排序快得多。5.2 lower_bound/upper_bound 的边界艺术与统计频率有序数组上的二分不要自己手写用lower_bound和upper_bound是效率最高、最不容易出错的方案。vectorint v {1, 3, 5, 5, 5, 7}; auto it1 lower_bound(v.begin(), v.end(), 5); // 第一个 5 的位置 auto it2 upper_bound(v.begin(), v.end(), 5); // 第一个 5 的位置lower_bound返回指向第一个不小于目标值的迭代器upper_bound返回第一个大于目标值的迭代器。当它们同时作用于有序数组时it2 - it1刚好等于目标值在数组中出现的次数。这个技巧在第 5.2 章里太常用了比如统计数组里有多少个元素等于target不需要手写两次二分。auto range equal_range(v.begin(), v.end(), 5); int count range.second - range.first;equal_range一次性返回左闭右开区间比分别调用lower_bound和upper_bound更优雅。还有一个常见操作是找到第一个大于等于目标值的位置下标。用it - v.begin()就能拿到索引。注意如果目标值比所有元素都大返回值是end()这时访问*it会越界。刷题时必须在取得迭代器后先判断it ! v.end()。binary_search只判断元素是否存在返回布尔值。其实它内部就是用lower_bound实现的如果你还需要位置和个数请直接使用lower_bound或equal_range别再用binary_search多折腾一次。5.3 排列、去重、翻转与最值工具全排列是搜索题的常客。next_permutation能原地生成字典序的下一个排列并返回布尔值表示是否还有下一个排列。配合do-while可以列出所有排列sort(p.begin(), p.end()); do { // 处理当前排列 } while (next_permutation(p.begin(), p.end()));注意使用前要先排序才能从最小字典序开始遍历全部排列。prev_permutation则是生成上一个排列用得相对少一些。去重的标准三步走是刷题中反复出现的经典组合sort(v.begin(), v.end()); v.erase(unique(v.begin(), v.end()), v.end());为什么必须先排序因为unique只移除相邻的重复元素不排序的话重复元素可能散布在不同位置unique就失效了。很多新手忘了排序直接unique结果发现重复项还在。unique本身也不是真正删除元素而是把不重复的元素前移返回新的逻辑末尾所以最后必须配合erase收缩容器。最值方面int mx *max_element(v.begin(), v.end()); int mn *min_element(v.begin(), v.end()); auto res minmax_element(v.begin(), v.end()); // res.first 指向最小值res.second 指向最大值如果只需要最大最小值用minmax_element一次遍历搞定比调用两次max_element和min_element更高效。在需要知道最大最小值对应下标时也可以通过res.first - v.begin()获得索引。reverse是另一个容易被忽视的便捷操作翻转容器reverse(v.begin(), v.end()); reverse(s.begin(), s.end());字符串反转、数组反转、配合next_permutation取逆向排列时都很常用。swap交换两个变量或两个容器的内容复杂度都是 O(1)内部只是交换了数据指针。6. 数值与累积类 API前缀和与数学处理的加速器6.1 accumulate、iota 与 partial_sum 的一行式初始化accumulate是求和利器但有个极其常见的坑第三个初始值参数直接决定返回类型。如果初值写0int int的结果还是int求和过程中一旦溢出结果就错了。求大数组和时一定要写0LLlong long total accumulate(v.begin(), v.end(), 0LL);如果是浮点数数组求和初值写0.0。accumulate的第四个参数是自定义二元运算可以做到求乘积、求最大最小值等但刷题场景用得少。iota可以生成一段连续整数序列vectorint idx(n); iota(idx.begin(), idx.end(), 0); // idx {0, 1, 2, ..., n-1}这在对数组排序后仍需要保留原下标信息的题目中非常好用。比如按元素值排序但输出时要输出原位置最稳的写法就是用一个下标数组idx然后按nums[idx[i]]作为键来排序。partial_sum用于生成前缀和数组vectorint prefix; partial_sum(v.begin(), v.end(), back_inserter(prefix)); // prefix[i] 是 v[0] 到 v[i] 的和前缀和是算法题的高频思路它能将区间求和从 O(n) 降到 O(1)。一维前缀和的核心公式是[l, r]区间和等于prefix[r] - prefix[l-1]注意当l 0时prefix[l-1]不存在通常会在前缀和数组最前面补一个 0。二维前缀和则需要在行列上分别累加原理相同。adjacent_difference用于生成差分数组也就是相邻元素的差值vectorint diff; adjacent_difference(v.begin(), v.end(), back_inserter(diff));差分数组的操作逻辑是在区间[l, r]上统一加上k只需要在diff[l]加k在diff[r1]减k。这个技巧在处理多次区间更新时能显著降低复杂度。虽然很多选手更喜欢手写差分循环但理解adjacent_difference背后的思路对你掌握差分思想有好处。6.2 数学函数取整与取模的精度陷阱std::gcd和std::lcm是 C17 标准库提供的最大公约数和最小公倍数函数需要包含numeric头文件。在 C14 或更老版本里只能使用 GCC 扩展的__gcd或者自己手写辗转相除。刷题前先确认你面向的编译器标准支持到 C17std::gcd确实方便很多。#include numeric int g gcd(a, b); int l lcm(a, b);向上取整是另一个常见场景。有人习惯int ans ceil((double)a / b);但这里有浮点精度隐患。当a和b非常大时(double)a / b可能会因为精度损失导致向上取整结果差 1。更稳妥的办法是用整数公式int ans (a b - 1) / b;这个公式的原理是把余数补满一整个除数。注意a b - 1可能溢出所以题目数据很大时先用long long计算。pow求整数幂时我几乎不用。因为pow基于浮点实现大整数场景下可能因为精度误差得到错误结果。求 2 的k次幂直接写1LL k一般整数幂用快速幂模板。开方在判断完全平方数时也要小心int x sqrt(n); // 可能差 1 while ((x 1) * (x 1) n) x; while (x * x n) --x;取模运算是数学题的另一大坑。C 里负数取模的结果是负数比如-5 % 3的结果是-2而在数学意义上我们通常希望是1。处理方式是int mod (x % m m) % m;先把x % m的结果转换到非负再模一次确保结果落在[0, m-1]区间。涉及幂运算的取模比如(a * b) % m如果a和b都是int相乘可能溢出必须把其中一个转成long long或者用乘法安全性更高的模板函数。7. 实战整合与进阶建议7.1 一套 API 组合拳TopK 高频单词的完整实现讲了这么多零散的 API最后用一个真实题目把它们串起来。题目场景给定一个字符串数组返回出现频率最高的 K 个单词频率相同的情况下按字典序排序。这是 LeetCode 上的经典题。完整实现如下vectorstring topKFrequent(vectorstring words, int k) { unordered_mapstring, int freq; for (const string w : words) { freq[w]; } vectorpairstring, int items(freq.begin(), freq.end()); sort(items.begin(), items.end(), [](const pairstring, int a, const pairstring, int b) { if (a.second ! b.second) return a.second b.second; return a.first b.first; }); vectorstring ans; for (int i 0; i k; i) { ans.push_back(items[i].first); } return ans; }这个解法把所有关键技巧都用了进去unordered_map做哈希计数vectorpairstring, int把 map 转成可排序的结构lambda 比较器先按频率降序、再按字典序升序最后用push_back收集答案。整体复杂度 O(n log n)简洁清晰。如果数据量大到sort全排代价太高可以改造成大小为 K 的小根堆auto cmp [](const pairstring, int a, const pairstring, int b) { if (a.second ! b.second) return a.second b.second; return a.first b.first; }; priority_queuepairstring, int, vectorpairstring, int, decltype(cmp) pq(cmp);这里比较器的语义要特别注意因为堆顶是“优先级最低”的元素所以比较器内部的符号与sort完全相反。写完后你自己会感受到熟练的人三分钟写完全部 API不熟的人可能卡在 lambda 的语法上。7.2 算法面试中的 API 熟练度要求与训练建议算法面试的手撕环节通常要求在 30 到 45 分钟内完成一道中等难度的题。这时候能不能快速写出干净代码很大程度取决于 API 是否形成条件反射。比如看到“去重”就应该立刻想到sort unique erase看到“区间更新”就该想到差分数组看到“动态求极值”就该想到priority_queue。这些反射不是靠背出来的是靠大量刷题练出来的。我自己的训练方法是准备初期专门花两周时间把上面提到的所有 API 各刷 20 遍以上。不用记复杂题目就写最小化的示例代码比如手动实现一个数组的sort、两次lower_bound、一次priority_queue自定义排序确保语法闭着眼睛都能写对。之后正式刷题时把这些 API 当成积木专注算法思路。还有一点很多人忽略练题时关掉编辑器的自动补全。在线笔试和面试白板大多没有智能提示你要是连std::greaterint都拼不出来心态会瞬间崩掉。平时就把numeric、algorithm、unordered_map这些头文件和 API 名记牢考试时就能节省大量查找时间。如果你用 VS Code 刷题建议先把 C/C 编译运行环境配好至少保证一键编译、一键运行否则调试过程就够你喝一壶。这不是核心内容但环境不顺非常影响学习节奏。最后分享一个我反复踩坑之后的体会刷题不是比谁记住的 API 多而是比谁能把有限的 API 组合出最优解。sort、lower_bound、priority_queue、unordered_map、vector、string这六样东西已经能解决大部分算法题。把它们的复杂度、边界条件和特殊语义研究透比贪多去背冷门函数有用得多。我后来写了几年 C最常用的还是这些老朋友反而不太需要那些花哨特性。代码量上去之后你会发现这些 API 用顺手了之后思路和实现之间再也没有断层那种“脑子里有解法但手跟不上”的难受感就会彻底消失。
返回列表