C++ STL高频函数速查:序列容器、关联容器与排序二分 写数据结构与算法题、准备面试手撕代码、打竞赛C选手最该先搞明白的事情不是背模板而是把手边STL的常用函数用熟。我见过太多人拿着教材啃红黑树实现结果真到了做题的时候连priority_queue的自定义比较器都写不利索最后只能用一堆循环和临时变量硬凑既慢又容易出错。这套“数据结构与算法C常用函数”系列总结就是帮你把这些高频函数一次性理顺。这篇是上篇聚焦最核心的序列容器、关联容器以及排序、二分这几个逃不掉的算法函数。内容不堆砌冷门API只讲刷题和工程里真正高频的那一批每个都会给到语义、复杂度、使用姿势和容易踩的坑。适合正在刷题准备面试的同学也适合已经工作但平时写业务代码多、STL用得比较浅的朋友查漏补缺。1. 整体设计思路用STL到底在用什么先把底层逻辑讲清楚。数据结构与算法题里选C图的不是语言本身有多炫而是STL这套标准库把最常用的数据结构都封装好了并且有严格的复杂度保证。vector的随机访问是O(1)map的插入查找是O(log n)unordered_map平均O(1)这些都是标准里写死的契约你可以放心地用它们做复杂度分析不需要担心某个实现版本突然变慢。很多人有一个误区觉得用STL是“投机取巧”面试官会让你手写。实际上面试官考察的是你知不知道什么时候该用哪个结构、复杂度是多少、边界条件怎么处理。STL的容器就是你的工具箱你用得越熟算法思路就能越快地落地成代码。真正需要手写的是那些STL里没有直接对应的结构比如并查集、线段树、字典树而不是让你从零写一个红黑树。认清这一点你的学习重心才是对的。还有一点工程经验和刷题经验在STL的使用上有区别。工程里讲究代码可读性和稳定性刷题场景讲究写码速度和边界覆盖。这篇总结兼顾两边但会更侧重算法场景因为函数的使用边界、迭代器的失效规则这些刷题时最容易踩。看整个STL的体系核心就三块容器存数据、算法处理数据、迭代器沟通两者。本篇总结的“常用函数”主要集中在容器的成员函数和algorithm头文件里的通用算法函数这两块覆盖了算法题里九成以上的代码量。至于仿函数、适配器这些概念我不会单独展开讲但会在priority_queue和sort的比较器部分带出来因为那是实际写代码绕不开的。2. 序列容器常用函数vector、stack、queue、deque、priority_queue序列容器是算法题里出场率最高的一类。它们的共同特点是元素之间有明确的先后位置关系增删改查的语义直观。这里按使用频率从高到低逐个过。2.1 vector不只是动态数组vector是算法题里的绝对主角绝大多数需要“数组”的场景直接用vector就对了。别用C风格数组那玩意儿的缺陷你比谁都清楚不能动态扩容、传参退化成指针、没有边界检查。vector把这些问题都解决了。#include vector using namespace std; vectorint v; // 空vector vectorint v(10, 0); // 10个元素每个都是0 vectorint v {3, 1, 4}; v.push_back(2); // 尾部插入均摊O(1) v.emplace_back(5); // 尾部原地构造比push_back省一次拷贝 v.back(); // 最后一个元素 v.front(); // 第一个元素 v.pop_back(); // 删除最后一个元素O(1) v.size(); // 元素个数size_t类型 v.empty(); // 判空 v.clear(); // 清空所有元素O(n) v.resize(20); // 重新设置大小 v.reserve(100); // 预先分配容量不改变size v.capacity(); // 当前容量这里有几个值得说透的点。size()和capacity()的区别经常有人搞混。size()是当前有多少个元素capacity()是vector底层已经申请了多少空间。两者之间的差值就是“免费”的扩张空间。当你push_back导致元素个数超过capacity时vector会重新分配一块更大的内存通常是原来两倍把旧元素搬过去。这个重新分配的过程是O(n)的所以频繁push_back的综合代价是均摊O(1)。提前用reserve(n)把容量开好就能避免反复搬家的开销。刷题时如果预先知道数据规模上来reserve一下是很好的习惯。emplace_back和push_back的区别在于push_back接收的是已经构造好的元素内部会做一次拷贝或移动emplace_back接收的是构造参数直接在vector内部的内存上构造元素。对于简单的int两者没差别如果存的是pairstring, vectorint这种复杂对象emplace_back(s, vec)就可以省掉一次临时对象构造加移动的开销。这属于工程上的性能细节刷题时纯int数据无所谓但写复杂结构时养成用emplace_back的习惯是值得的。erase是vector里为数不多的“危险”操作复杂度O(n)因为它需要把被删元素后面的所有元素往前搬。删除单个元素用v.erase(pos)删除区间用v.erase(v.begin() l, v.begin() r)。头文件里的remove配合erase可以按值删除但那个组合叫“erase-remove惯用法”单独使用时容易忘记remove并不真的删元素只是把要保留的元素往前提。很多新手在这里栽过跟头。2.2 stack和queue这俩其实很“懒”stack和queue在C里是容器适配器意思是在底层容器的基础上限制了接口。stack默认用deque做底层queue默认也是deque。它们的成员函数极其简单简单到很多人会忽视它们的存在感。#include stack #include queue stackint st; st.push(1); st.top(); // 返回栈顶元素 st.pop(); // 弹出栈顶元素注意pop不返回元素 st.size(); st.empty(); queueint q; q.push(1); q.front(); // 队首元素 q.back(); // 队尾元素 q.pop(); // 弹出队首元素 q.size(); q.empty();两个容易记混的地方。stack只有top()没有front()和back()queue有front()和back()但没有top()。另一个坑是pop()都不返回被弹出的元素所以弹出加读取要写成两行int x st.top(); st.pop();。这个设计是为了避免异常安全问题和拷贝开销C里就是这么设计的别和Python混为一谈。什么时候用stack、什么时候用queue判断标准是处理的先后顺序。需要“后进先出”的地方比如括号匹配、DFS非递归遍历、单调栈用stack需要“先进先出”的地方比如BFS层序遍历、拓扑排序用queue。这两个容器在算法题里没有性能问题因为底层deque的两端操作都是O(1)放心用。2.3 deque两头都能操作但别小看它deque是双端队列支持两端的插入删除全部O(1)。它的存在意义就是当你需要在头部操作时不用像vector那样把所有元素搬一遍。#include deque dequeint dq; dq.push_back(1); dq.push_front(2); // O(1) dq.pop_back(); dq.pop_front(); // O(1) dq.front(); dq.back(); dq[3]; // 也支持随机访问O(1)deque的随机访问也是O(1)但它不像vector那样是一整块连续内存而是分段连续的所以capacity的概念在deque上不适用。刷题时用deque的地方不多最典型的是滑动窗口最大值你需要维护一个双端队列存下标队首过期就pop_front队尾出不和谐的就pop_back。那道题如果你用vector硬删头元素复杂度会退化到O(n)每次直接超时。另一个典型场景是需要“两边都能添加数据”的模拟题比如回文构造。2.4 priority_queue优先队列的C形态priority_queue在算法题里的出现频率极高TopK问题、合并K个有序链表、哈夫曼编码、贪心取最大/最小值全都靠它。它默认是个大根堆根节点是最大值top()返回最大值。#include queue priority_queueint pq; // 默认大根堆 pq.push(3); pq.push(1); pq.push(4); int x pq.top(); // 4 pq.pop(); // 小根堆要传入 greaterint priority_queueint, vectorint, greaterint min_pq; // 自定义比较器注意返回值语义和sort相反 struct Node { int dist; int id; }; struct Cmp { bool operator()(const Node a, const Node b) const { return a.dist b.dist; // dist小的优先级高 - 小根堆 } }; priority_queueNode, vectorNode, Cmp heap;这里的“坑点”值得好好说。sort里写return a b是升序但在priority_queue的比较器里return a b会得到大根堆你要小根堆就得写return a b。原因是priority_queue的Compare参数语义是“优先级低于”的关系如果comp(a, b)为true说明a的优先级低于bb会浮到上面。写了几次不习惯的话我的建议是别背规则每次写的时候头脑里过一遍“这个比较函数返回true代表a排在b底下”。另一个坑自定义类型用priority_queue时比较器必须是结构体并重载operator()而且需要用const修饰。很多人试图直接传Lambda表达式写成priority_queueNode, vectorNode, [](autoa, autob){...}这是编译不过的因为priority_queue的模板参数要求的是类型而不是对象。要用Lambda得配合decltypeauto cmp [](const Node a, const Node b) { return a.dist b.dist; }; priority_queueNode, vectorNode, decltype(cmp) heap(cmp);这套写法的记忆负担比结构体版本高我个人建议刷题时固定用结构体版本一次写对。还有个容易被忽视的细节priority_queue不提供迭代器也没有clear()之外的快速重建方式。如果你需要频繁地把堆清空重建可以考虑直接用vector加make_heap系列操作但那套接口更底层刷题时普遍用得少。真到了要多次重建的题把priority_queue放在循环体内部声明就能“自动重置”也是常见解法。容器尾部插入头部插入随机访问典型场景vectorO(1)均摊O(n)O(1)动态数组、排序、二分查找dequeO(1)O(1)O(1)滑动窗口、双端操作stack无push为尾部不支持不支持表达式求值、DFS、单调栈queuepush O(1)不支持不支持BFS、逐层处理priority_queuepush O(log n)不支持不支持TopK、贪心、哈夫曼3. 关联容器与键值工具map、set、pair、string关联容器的使用逻辑和序列容器完全不同它们的核心是根据“键”来组织数据不是为了维护“位置”。这一节把map、set以及它们和pair、string配合时的常见函数讲清楚。3.1 map和unordered_map键值对的正确打开方式map底层是红黑树元素按键升序排列查找、插入、删除都是O(log n)unordered_map底层是哈希表平均O(1)但不保证顺序。算法题里优先用哪个我的经验是默认用map只有当明确需要O(1)查找且不care顺序时才用unordered_map。原因很实际面试时讲复杂度map的红黑树复杂度分析好讲而且unordered_map在实现里要处理哈希冲突某些极端数据下反而会退化。#include map #include unordered_map mapstring, int mp; mp[apple] 3; mp.insert({banana, 5}); mp.emplace(orange, 7); // 查找 auto it mp.find(apple); if (it ! mp.end()) { // it-first是键it-second是值 } if (mp.count(apple)) { // 存在性检查count返回0或1map里键唯一 } // 遍历 for (auto [k, v] : mp) { cout k v endl; // 按key升序 } // 删除 mp.erase(apple); mp.erase(it); // 传迭代器map的operator[]是个双刃剑。mp[key]当key不存在时会自动插入一个默认值然后返回引用。所以你不能用mp[key]来判断键是否存在一旦你执行了if (mp[foo] 0)这种判断foo就已经被插进map了值为类型默认值。这就是经典的“查询改状态”陷阱。要判断存在性用find()或count()。map里存的值访问也有讲究。mp.at(key)能拿到值的引用但key不存在时抛异常mp[key]不抛异常但会插入默认值。刷题时用mp[key]这种自增操作很常见它是安全的因为即使键不存在也会先插入0然后自增到1。这属于有意的“懒初始化”写法。unordered_map的函数接口和map一致你不必改任何调用代码只需换类型名。但要注意如果你的键是自定义结构体unordered_map需要你提供哈希函数而map只需要比较函数。平时写题时键基本是int或string不需要关心这个一旦键变成pairint,int你就得写自定义哈希而mappairint,int, int直接用默认的比较逻辑就行了。从这个角度看map在快速做题时更省心。3.2 set和unordered_set去重与有序集合set是只存键的map键唯一且有序unordered_set是哈希版。主要场景是去重、快速判断存在性、维护一个有序值集合。#include set setint s; s.insert(5); s.insert(3); s.insert(5); // 第二次插入无效 auto it s.find(3); if (it ! s.end()) { } if (s.count(3)) { } s.erase(3); // 按值删 s.erase(it); // 按位置删 // 有序遍历 for (int x : s) cout x ; // 3 5set里还有两个特别重要的函数lower_bound和upper_bound。它们返回的是迭代器语义和数组版二分一致lower_bound(x)返回第一个 x的元素的迭代器upper_bound(x)返回第一个 x的元素的迭代器。由于set底层是红黑树这两个操作是O(log n)不是O(n)这一点和vector上lower_bound的O(n)线性遍历完全不同。auto it s.lower_bound(4); // 第一个4的元素迭代器 auto it2 s.upper_bound(4); // 第一个4的元素迭代器 if (it s.end()) { // 所有元素都小于4 } int prev *prev(it); // 严格小于4的最大元素“找第一个大于等于某个值的最小元素”“找前驱后继”这类问题set的这两个函数就是神器。区间覆盖、日程安排、天际线问题这些经典题里你用set维护有序端点配合lower_bound判断重叠代码能短得非常优雅。3.3 pair和string的日常小技巧pair和string严格来说不是容器但它们是算法题里最基本的“零件”配合容器时有一些高频函数值得单独记。pair的典型用法是存坐标、区间、键值对。C17开始推荐用结构化绑定直接拆值好用得很#include utility pairint, int p {1, 2}; auto [x, y] p; // C17结构化绑定 vectorpairint, string vp; vp.push_back({1, one}); vp.emplace_back(2, two); mapstring, pairint,int mp; mp[s].first 5;string的高频操作集中在拼接、查找、截取和类型转换#include string string s hello; s world; s.size(); s.length(); // 和size()等价 s.substr(0, 3); // 子串参数起始位置长度 s.find(lo); // 返回第一个匹配的位置没找到返回string::npos s.find(l, 3); // 从下标3开始查找 if (s.find(x) string::npos) { // 没找到 } int num stoi(123); // 字符串转int string str to_string(456); // 数字转字符串find返回的是size_t比较要用string::npos而不是-1。很多新手写成if (s.find(x) -1)这在某些编译器上也能蒙混过关但严格来说是错误的因为npos是size_t的最大值把它隐式转成int的-1是未定义行为。写string::npos才是跨平台安全的。substr第二个参数是“长度”不是“结束位置”要截到字符串尾部就直接省略第二个参数s.substr(pos)。这个细节在切割ip字符串、解析路径时经常用。4. 通用算法函数sort、reverse、lower_bound、max/minalgorithm头文件里装着一批与具体容器无关的通用算法。它们的工作对象是迭代器区间所以能作用于vector、deque、string甚至在原始数组上也能用。这一节讲的都是笔试面试里最常用到的。4.1 sort排序的终局方案sort是性能最强的通用排序平均O(n log n)内部实现是混合排序通常是快排插入排序优化官方要求不比快排慢。凡是需要对vector、string、数组排序的场景直接调用它。#include algorithm vectorint v {5, 2, 8, 1}; sort(v.begin(), v.end()); // 升序 sort(v.begin(), v.end(), greaterint()); // 降序 sort(v.begin(), v.end(), [](int a, int b) { return a b; // lambda比较器降序 }); string s cba; sort(s.begin(), s.end()); // abc int a[] {3, 1, 4}; sort(a, a 3);sort接受的第三个参数是“比较器”返回true表示第一个参数排在第二个参数前面。这是整个STL里最需要记住的语义比较器表达的是“a应该排在b之前”。升序就是a b降序就是a b。普通类型洗个菜没问题自定义结构体才是考验struct Interval { int l, r; }; sort(intervals.begin(), intervals.end(), [](const Interval a, const Interval b) { if (a.l ! b.l) return a.l b.l; return a.r b.r; });比较器有几个硬性要求必须满足严格弱序即对任意acomp(a, a)必须为false如果comp(a, b)和comp(b, a)都为false就认为两者等价。如果你在比较器里对“相等”的元素返回true比如写return a.l b.l那么排序行为未定义可能崩溃也可能顺序错乱。这是sort使用中最高频的Bug来源排查时先检查比较器有没有写反边界。sort的迭代器参数要求随机访问迭代器所以它能作用于vector、array、deque、string但不能作用于list和map。list自带sort()成员函数map天然有序根本不需要排序。如果需要对map按value排序标准做法是把pair拷进vector再sort。stable_sort是稳定排序相等元素的相对顺序保持不变适合多关键字段轮转排序的场景比如先按时间排再按优先级排。代价是要额外分配内存一般刷题用不到知道存在性即可。4.2 reverse、swap两个不起眼的杂技reverse把区间逆序swap交换两个同类型对象的值。极简但用得好能省很多思考。reverse(v.begin(), v.end()); swap(a, b); // 基本类型 swap(v1, v2); // 两个vector交换O(1) swap(mp1, mp2); // 两个map交换O(1)swap两个vector之所以是O(1)是因为它只交换内部指针、大小和容量不搬动元素。这个性质在需要“两阶段处理”的时候特别好用比如分层BFS时交换当前层和下一层的vector或者滚动数组换引用。swap对于所有STL容器都是O(1)的除非特殊情况比如array这是标准里明确保证的可以放心。4.3 lower_bound、upper_bound、binary_search二分查找的标准姿势手写二分是每个算法学习者都经历过的痛边界条件、死循环、越界。好消息是algorithm已经给了封装好的二分函数前提是数据必须有序。它们的返回值是迭代器复杂度O(log n)。vectorint v {1, 3, 5, 7, 9}; auto it1 lower_bound(v.begin(), v.end(), 5); // 第一个5的元素 - 指向5 auto it2 upper_bound(v.begin(), v.end(), 5); // 第一个5的元素 - 指向7 // 转下标 int idx lower_bound(v.begin(), v.end(), 5) - v.begin(); // 判断是否存在 bool found binary_search(v.begin(), v.end(), 5);记住一个原则lower_bound是左闭右开思想的体现它找的是“插入点”。如果想把x插入有序数组且保持有序插入位置就是lower_bound(x)。upper_bound与之相比只是在重复元素时跳到相同元素段的末尾。这几个函数的实际价值特别大很多难题的“最后一步”就是二分查找。最长递增子序列的O(n log n)解法里lower_bound直接定位替换位置区间查找问题里lower_bound找到左边界、upper_bound找到右边界两个下标相减就是区间内元素个数。你如果手写二分这些场景的边界条件够你调半小时用函数一次搞定。针对set和map它们各自有成员函数版本的lower_bound复杂度O(log n)而通用版本的lower_bound在set的迭代器上是O(n)因为set的迭代器不是随机访问迭代器不能直接跳转。这是一个暗坑我会在第五节展开。4.4 max、min、abs、max_element、min_element基础但必要的小函数。max和min比较两个值也可以接受初始化列表比较多个值。max_element和min_element接受迭代器区间返回指向最大/最小元素的迭代器。int a max(3, 5); int b max({3, 7, 2, 9}); // C11初始化列表版本 vectorint v {4, 2, 8, 6}; auto it max_element(v.begin(), v.end()); // 指向8 auto it2 min_element(v.begin(), v.end()); // 指向2 int maxIdx max_element(v.begin(), v.end()) - v.begin(); int c abs(-5); // cstdlib或cmathabs对int求绝对值fabs对double求。刷题时不注意会把int最小值abs出问题abs(INT_MIN)未定义因为正数表示范围不够。遇到这种场景换成long long再处理。max_element的返回值也是迭代器转下标要减去begin()转值要*it别搞混。5. 常见问题与排查技巧实录这一节是我在带新人、写题、看代码评审中被问得最多的问题合集。很多问题看着小但每一次出现都会浪费大量调试时间。5.1 迭代器失效头号隐形杀手vector的insert和erase会使插入/删除位置之后的迭代器失效push_back在扩容时会使所有迭代器失效。clear之后所有迭代器失效。map和set的插入删除不会使其他迭代器失效除了被删的那个本身这是它们的结构优势。// 错误的循环删除 for (auto it v.begin(); it ! v.end(); it) { if (*it 0) { v.erase(it); // 失效it之后行为未定义 } } // 正确的写法C11及以后 for (auto it v.begin(); it ! v.end(); ) { if (*it 0) { it v.erase(it); // erase返回下一个有效迭代器 } else { it; } }C11之前没有erase返回值流行写法是v.erase(it)现在这招在新代码里已经没必要了直接用返回值就好。map和set的erase虽然不使其他迭代器失效但从C11开始也返回下一个迭代器所以统一用it container.erase(it)是最省心的。还有一个容易被忽略的场景你在用unordered_map时持有迭代器随后又插入新元素导致哈希表rehash那这个迭代器会失效。哈希表的扩容和vector扩容一样都会搬动元素迭代器自然失效。遇到这种情况先完成对已有元素的操作再执行插入。5.2 priority_queue的比较器为什么反直觉很多同学在自定义类型小根堆上反复试错。前面的结构体写法已经给出这里补充一个万能公式想让优先级最高的是“值越小越优先”比较器里就写return lhs rhs想让“值越大越优先”写return lhs rhs。这个规则不要在写的时候才推导它会消耗你的短期记忆直接背下来。另一个相关的坑是Lambda版本的写法用decltype(cmp)作为模板参数后构造priority_queue时要传入cmp对象auto cmp [](int a, int b) { return a b; }; priority_queueint, vectorint, decltype(cmp) pq(cmp);如果漏了最后的(cmp)会编译错误让人一头雾水。原理是priority_queue会默认构造一个比较器但如果比较器是Lambda类型它是没有默认构造函数必须显式传入。这种细节问题现场写码时最容易卡住。5.3 map还是unordered_map不只是性能问题刷题时遇到“统计频率”“快速查询”这类需求很多人默认用unordered_map理由是平均O(1)。但要注意两个陷阱。第一unordered_map的常数因子比较大。哈希计算、冲突处理、扩容重哈希都有额外开销在元素量只有几千、几万的算法题里它和map的实际耗时差距非常小。反倒是map的红黑树实现操作稳定、可按序遍历、不用写哈希函数写码时的心理负担小很多。第二unordered_map对键类型有要求。基本类型和string有默认哈希但pairint,int就没有标准哈希函数。你要存一个坐标到值的映射如果硬要用unordered_map得自己拼一个哈希struct PairHash { size_t operator()(const pairint,int p) const { return ((size_t)p.first 32) ^ p.second; } }; unordered_mappairint,int, int, PairHash mp;而mappairint,int, int直接就能编译因为它只需要operator而pair自带字典序比较。所以我个人在快速刷题时遇到键是复合类型就无脑map基本类型才考虑unordered_map。这个习惯帮我省下了大量编译报错时间。5.4 sort自定义比较器的边界陷阱前面提过严格弱序的要求这里展开讲一个具体的崩溃案例。// 错误示例 sort(v.begin(), v.end(), [](int a, int b) { return a b; // 相等两个元素时返回true违反严格弱序 });当a b时这个比较器返回true意味着a排在b前面同时还认为b排在a前面排序算法内部的一致性检查会挂掉直接导致内存越界崩溃。排查方法也很直接打印比较器里a b和b a两个方向的结果如果都返回了不该返回的true就是边界问题。正确写法是return a b等于的情况返回false。另一个陷阱是Lambda捕获导致的性能问题。比较器如果捕获了大对象比如[vec]捕获整个vector每次比较都要访问外部状态虽然正确性没问题但性能会差。更好的做法是只捕获需要的外部值或者干脆不捕获。刷题场景下这个优化不是必须的但工程代码里这是评审重点。5.5 综合案例一道题的STL组合打法光列函数太干巴巴用一道典型题把这一篇的函数串一遍。题目是这样给定一个字符串数组统计每个字符串出现的次数按出现次数降序排列次数相同按字典序升序。#include bits/stdc.h using namespace std; vectorpairstring,int solve(const vectorstring words) { unordered_mapstring,int cnt; for (const string w : words) { cnt[w]; } vectorpairstring,int res(cnt.begin(), cnt.end()); sort(res.begin(), res.end(), [](const auto a, const auto b) { if (a.second ! b.second) return a.second b.second; // 次数降序 return a.first b.first; // 字典序升序 }); return res; }这道题覆盖的东西unordered_map做统计operator[]惰性插入、vector从map初始化、sort自定义比较器lambda 多条件排序、pair存储。如果你对这篇的每个函数都烂熟这套组合拳写出来不超过两分钟。如果你试图手写哈希表加快排序大概率要写错。这也是STL的核心价值把经典结构做到最优让你把脑力留给算法本身。另外一个经常考的组合是需要动态有序时用set需要快速查询时用unordered_map同时维护两个结构。经典题的解法是“双向索引”用map做有序索引内部查unordered_map找位置。这两个结构的配合在工程里也很常用先掌握单独用法组合时注意增删必须同步更新两个结构防止数据不一致。写在最后一些实际体会这篇总结写得比较细是因为我知道STL函数的学习很容易“一看就会、一写就废”。很多函数你以为自己会用真到了压力面试或竞赛现场细节决定成败。我个人的建议是不要对着文档背函数列表而是把这篇里的每个函数都手写一遍到自己的常用代码模板里然后拿十道经典算法题去练——括号匹配、滑动窗口最大值、TopK、合并区间、最长不重复子串、翻转链表这类——用完一遍函数就变成了你的肌肉记忆。还有一个小技巧分享写题时把#include bits/stdc.h当成万能头文件用比赛和刷题平台都支持可以把注意力从“头文件记不全”里解放出来。但正式工程代码不要这么干按需包含才是规范。另外刷题时尽量用vector替代C数组用auto推测迭代器类型能明显减少口头和书面代码之间的转换损耗。这篇“上篇”把序列容器、关联容器和通用算法函数讲完了。下一篇我会整理字符串处理、位运算、二分答案、自定义结构体哈希等进阶部分包括std::string_view、std::function、std::sort在特殊场景的应用以及一些更隐蔽的容器陷阱。先把这篇里的函数用熟下篇的内容你会吸收得更快。