LeetCode和C++ STL这两样东西,放在一块学,效率是最高的。纯刷题不碰STL,很多代码写出来又臭又长,明明十行能解决的事非要手撸一个红黑树;光学STL不刷题,又容易陷入“容器都会用、算法全不会”的尴尬。这篇内容就围绕这两条线展开,从环境配置到容器实战,从高频算法到工程迁移,一次性把该踩的坑、该背的模板、该理解的原理都讲透。无论你是刚开始刷LeetCode的C++新手,还是想系统梳理STL用法的老手,都能从这里拿到可以直接抄作业的方案。
1. 整体设计与思路拆解
1.1 为什么刷LeetCode一定要搭配STL
我见过不少刷题的同学,明明已经写到第五六十题了,代码里还在自己写链表反转、自己实现哈希表,甚至排序都要手写快排。这个方向不能说错,只能说效率太低。LeetCode考的是算法思维和代码组织能力,不是让你在面试时证明自己能徒手写出一个RBTree。STL里的容器和算法,本身就是工程验证过无数次的实现,刷题时直接拿来用,能帮你把注意力集中在“这题怎么解”而不是“数组怎么扩容”上。
但反过来说,如果你只会用STL,不了解底层原理,也会出问题。比如有人用unordered_map存pair做键,编译半天都过不了,还不明白为什么;有人用vector存数据后一边遍历一边erase,直接踩进迭代器失效的坑里。所以正确的姿势是:STL做武器,算法做战术,遇到容器之间的差异、迭代器的行为这些细节,必须停下来搞明白。这才是“刷题指南”和“使用手册”合并成一篇文章的底层逻辑。
1.2 从零到周赛的完整进阶路线
不管你现在在哪个阶段,我都推荐把刷题路径分成四段走,而不是按题库顺序硬刷。
第一段是“线性结构打底”。数组、字符串、链表、栈、队列,配合vector、string、stack、queue、deque这些容器,把双指针、滑动窗口、单调栈这几类基础题型过一遍。第二段是“哈希与查找”。用unordered_map、unordered_set解决计数、去重、映射类问题,配套掌握lower_bound、upper_bound、二分查找的精髓。第三段是“排序与堆”。sort、stable_sort、partial_sort、priority_queue全部练熟,解决TopK、区间合并、贪心调度类问题。第四段才是“进阶结构”。树、图、并查集、Trie、线段树,这时候STL能帮的忙变少了,但map、set依然能解决很多树形问题。
这条路线对应到LeetCode上,大概就是前200题热门的覆盖范围。建议每天3道新题加1道旧题复盘,每周参加一次周赛检验训练成果。周赛的意义不是让你拿名次,而是逼迫你在限时状态下快速选择容器、设计算法,这种能力是慢慢刷题练不出来的。
1.3 C++标准版本与编译器的选择
刷题时用哪个C++标准,很多人不重视,实际上影响很大。我现在日常都是用C++17,LeetCode默认的g++环境也支持C++17,这意味着结构化绑定、if constexpr、optional这些特性都能直接用。老一点的代码会默认C++11,但C++11缺少很多便捷语法,比如结构化绑定、string_view,写出来的代码就容易啰嗦。
编译器方面,Windows上刷题最常见的搭配是VSCode加MinGW-w64,Mac上用clangd或Xcode自带的clang,Linux上就是g++。不管你用哪个,有一点是一致的:你要知道自己当前用的编译器是什么版本、支持哪个C++标准。不然写完代码在本机能编译,提交到LeetCode却报编译错误,多半就是标准版本或编译器扩展的问题。
2. 环境准备:VSCode搭配C++的完整配置
2.1 VSCode下配置C++开发环境
先说结论,VSCode写LeetCode完全够用,前提是配置不折腾。很多新人把时间浪费在折腾编辑器上,今天装这个插件,明天配那个路径,最后题没刷几道,环境倒是重装了五六遍。跟着下面的步骤走,正常情况下半小时内能跑通第一个C++程序。
第一步,下载并安装MinGW-w64。我建议用MSYS2来管理工具链,因为MSYS2的包更新更及时,安装完还能顺便拿到gdb调试器。安装后在Windows的环境变量Path里加入MinGW的bin目录,然后在终端里执行g++ --version确认能输出版本信息。
第二步,在VSCode里安装三个插件:C/C++(微软官方那个)、Code Runner、C++ Intellisense。注意,别装一堆看起来功能相似的插件,插件装多了反而会互相干扰,比如代码提示就容易被两个插件同时接管。
第三步,配置c_cpp_properties.json。这个文件是整个环境配置的核心,它决定了IntelliSense能不能正常工作。你需要手动指定compilerPath指向g++.exe,然后在includePath里加上MinGW自带的include目录。如果你不配这个,就会出现“所有函数和变量都没办法跳转”的经典问题,这个我后面专门讲。
第四步,配置tasks.json和launch.json用于编译和调试。tasks.json里写好编译命令,比如:g++ -g main.cpp -o main,launch.json配好gdb调试器路径。这一步做完,F5就能一键调试。
2.2 Visual C++ Redistributable缺失的坑
还有一个环境问题天天有人问:明明代码在VSCode里能跑,双击exe却提示缺少VCRUNTIME140.dll。这不是你代码的问题,是目标机器上没有装Visual C++ Redistributable运行库。这个运行库不是Visual Studio本体,它只是把C++程序运行所需的DLL打包分发,文件不大,装起来也很快。
问题是很多人不知道装哪个版本。如果你用的是MinGW编译,那么这个报错一般不会出现,因为MinGW依赖的是libgcc、libstdc++等,而不是VC运行库。报这个错的多半是用Visual Studio或cl.exe编译的程序,或者别人给你的exe。遇到这情况,直接去微软官网下载最新的Visual C++ Redistributable,注意区分x64和x86,现在绝大多数程序都是64位,但你装一个x86的版本也不亏,很多老程序还在依赖它。
我自己的建议是:x64和x86都装,版本选2015-2022合集那个,它会同时安装2015、2017、2019、2022四个版本的运行库。装完基本一劳永逸,以后不会再遇到DLL缺失问题。
2.3 64位下的编译与运行细节
热词里有一个“c++ 64位 fopen报安全错误”,这其实是Visual C++编译器对fopen等函数的安全检查闹的。VC编译器默认把fopen标记为废弃的,要求你用更安全的fopen_s。而g++没有这个限制。如果你在Windows上用Visual C++写代码,不想改用fopen_s,可以在文件开头定义宏_CRT_SECURE_NO_WARNINGS,或者直接在项目属性里关掉SDL检查。
另外,64位环境下有两个细节容易踩坑。第一个是类型尺寸:指针变成8字节了,int还是4字节,size_t是8字节,遍历容器时用int i = 0; i < vec.size(); i++这种写法,编译器会警告有符号与无符号不匹配。刷题时为了省事,直接写for(int i = 0; i < vec.size(); i++)一般不报错,但严谨一点应该用size_t或直接范围for。第二个是栈空间:64位程序默认栈大小通常还是1MB到8MB,深度递归依然可能爆栈。所以DFS这种递归算法,如果递归深度超过一万层,优先考虑改成迭代写法或增加栈大小。
3. STL核心容器在刷题中的实战用法
3.1 string与vector的基础操作细节
string是刷题最高频的容器,没有之一。这里先把几个容易出问题的点说清楚。
第一个是字符串数组初始化。很多人写vector<string> strs = {"abc", "def"};没问题,但想初始化一个字符数组时就容易卡住:vector<char> chars = {'a', 'b', 'c'};注意这里是花括号,不是圆括号。还有string s(5, 'a')意思是生成"aaaaa",而string s = 'a'是会编译报错的。
第二个是字符串转数组。LeetCode里经常遇到把"1,2,3"这种字符串拆出来。最稳妥的写法是用istringstream加getline按分隔符逐个取出,std::getline(ss, token, ',')。这个方法比手写循环找逗号清楚得多,也不用担心边界判断漏掉最后一个元素。转数字用stoi、stol、stoll,注意如果是超长整数字符串,得用stoll,否则溢出。如果数值可能超过long long范围,那就要自己实现大数处理了。
第三个是vector的resize和reserve。reserve只预留容量不改变size,resize直接改变size并默认初始化元素。刷DP题时我习惯先vector<vector<int>> dp(n, vector<int>(m, 0));,这样后来访问dp[i][j]不会踩到未初始化的内存。还有一个细节:二维vector传参时尽量用引用,避免拷贝开销。LeetCode的函数签名里很多都直接传vector的引用,不需要你手动加&。
3.2 哈希容器:unordered_map与unordered_set
哈希容器是解决“查找”类题目的利器。两数之和、字母异位词分组、最长连续序列,都是它们的经典应用场景。
先说unordered_map的基本用法。统计字符频率时,最简洁的写法是for(char c : s) mp[c]++;不必先判断键在不在,operator[]会在键不存在时自动插入默认值。这是operator[]最大的便利。但要小心:如果你只是想查键存不存在,用count(key)或find(key),不要用mp[key],因为后者会在键不存在时插入一个默认值,污染数据。这个坑刷题时经常遇到,后面排查章节我再展开。
再谈自定义哈希的问题。LeetCode里有些题目要用pair<int,int>或vector<int>做键,默认的哈希函数不支持这些类型。遇到这种情况,最简单的方案是转换成string,比如把pair转成to_string(a) + "," + to_string(b);高效一点是自定义结构体哈希,模板特化std::hash或自定义仿函数。但刷题求快,能转字符串就转字符串,省得调试半天。
注意区分map和unordered_map。刷题时90%的场景用unordered_map就行,因为我们是单次查询,不关注有序输出。只有当你需要按键的自然顺序遍历,或者需要在查找时快速获取最小/最大键时,才用map(底层红黑树)。不要下意识地全用map,性能差不少。
3.3 栈与单调栈的模板化用法
stack本身用法简单,push、pop、top,但单调栈是一个非常值得背模板的思想。热词里专门有“单调栈算法c++”,说明这是高频考点。
单调栈的典型框架是这样的:遍历一个数组,维护一个栈,栈内元素按某种单调性排列。比如求每个元素右边第一个比它大的元素,可以维护一个从栈底到栈顶递减的栈。每次遇到一个新元素,如果它比栈顶大,那么栈顶元素的“右边更大元素”就是当前元素,弹出栈顶并记录答案,然后继续比较新栈顶,直到新元素不大于栈顶,再把新元素入栈。
这段逻辑我建议你亲手敲一遍,然后封装成自己熟悉的样子。因为单调栈的题变形非常多,接雨水、柱状图中最大矩形、每日温度、去除重复字母,全是从这个核心框架变出来的。你理解了“栈里存的是下标”和“什么时候弹栈”这两个关键点,任何变形题都能套。
3.4 优先级队列与TopK问题
priority_queue默认是大顶堆,最大元素在堆顶。TopK问题用大顶堆就要先弹出再保留小的,比较绕;大多数时候你要的是前K个最小或前K个最大的元素,这时候就要自定义比较器。
小顶堆的写法很多人忘了,记住这个模板:priority_queue<int, vector<int>, greater<int>> pq;。这个greater不是算法里的greater,它是functional头文件里的仿函数,作用是比较两个值。如果要存的是自定义结构体,比如pair<int,int>,需要自己写一个比较结构体:
struct Compare { bool operator()(const pair<int,int>& a, const pair<int,int>& b) { return a.second > b.second; // 小顶堆,按second排序 } }; priority_queue<pair<int,int>, vector<pair<int,int>>, Compare> pq;刷题时有一个更省事的替代方案:用vector存数据,配合std::push_heap和std::pop_heap,但操作起来还是容易出错。我建议就老老实实用priority_queue,写熟了之后,合并K个升序链表、数组中的第K个最大元素、前K个高频元素这些题都能在几分钟内搞定。
3.5 结构体链表与迭代器注意点
LeetCode的链表题是需要自己定义结构体的,典型写法:
struct ListNode { int val; ListNode* next; ListNode() : val(0), next(nullptr) {} ListNode(int x) : val(x), next(nullptr) {} ListNode(int x, ListNode* next) : val(x), next(next) {} };这个结构体的三个构造函数是LeetCode默认带的,你不需要修改它。链表题的核心是别丢节点:先保存下一个节点的指针,再改当前节点的next,不然白回改。初学者最容易犯的错是用一个临时指针遍历时,把head也搞丢了。记住原则:头节点单独用一个指针指向,遍历指针随便动,但如果你后面要返回head,就不要拿head去遍历。
另外,list容器刷题时用得不多。链表题之所以要手写结构体而不是直接用STL的list,是因为LeetCode的节点是它自定义的,我们改的是节点之间的指针关系。STL的list把内部结构封装了,根本不给你操作next的机会。这两种链表不是一回事,别混。
迭代器失效问题也在这一块讲掉。vector和deque在插入、删除元素后,之后的迭代器都可能失效;map、set、list在删除元素时,只有指向被删元素的迭代器失效,其他迭代器还活着。所以在遍历中erase,正确的姿势是it = vec.erase(it);而不要it++,或者把需要删除的元素先用另一个容器收集起来,遍历完了再统一删。这个规则刷题时经常踩到,尤其是“删除有序数组中的重复项”这类需要原地操作的题。
4. 高频算法的STL实现模板
4.1 快速幂的优雅实现
快速幂在LeetCode里是“数值的整数次方”这类题的主角。核心思想是二分幂:把指数拆成二进制,看每一位是否为1,决定要不要乘上相应的基数。C++里有一个非常干净的递归写法,也有一个迭代写法。
long long fastPow(long long a, long long n) { long long res = 1; while (n > 0) { if (n & 1) res *= a; a *= a; n >>= 1; } return res; }注意几个细节。第一,n要取非负,如果题目允许负指数,需要先取倒数再算正幂,或者直接用double类型。第二,乘法可能溢出,尤其是底数a很大时,所以参数类型用long long。如果题目要求对MOD取模,每次乘完之后都要res %= MOD; a %= MOD;,不然中间结果爆炸。第三,这个模板不只用于整数幂,还能扩展到矩阵快速幂,用来求解斐波那契数列的O(log n)算法,思路完全一样。
4.2 二分查找:lower_bound与upper_bound的最佳实践
LeetCode上有大量二分查找的题,很多题目一眼看过去不是二分,但其实答案是单调的,这就是“二分答案”。经典例子就是“爱吃香蕉的狒狒”这类题目。题目说狒狒每小时要吃一定数量的香蕉,需要多少时间才能吃完所有堆,寻找一个最小的速度。这里的速度是单调的:速度越大,吃完所需时间越短。于是你对速度做二分,每次检查在当前速度下能否在限制时间内吃完,最后收敛到最小的可行速度。
写二分时,用STL的lower_bound和upper_bound能省很多事。lower_bound返回第一个大于等于某个值的元素位置,upper_bound返回第一个大于某个值的元素位置。它们不只是用来查数组,还能用来做区间计数——比如统计有序数组中小于等于x的元素个数,直接upper_bound(v.begin(), v.end(), x) - v.begin()。对于“搜索旋转排序数组”这类题目,STL的lower_bound不适用,那就要自己写一个基于区间的二分。但是可以先用lower_bound判断是否旋转点,再决定在哪部分继续二分。
自己写二分时要注意的是:区间闭开的选择,以及退出条件。我推荐统一用左闭右开[l, r)写法,退出条件是while (l < r),更新时l = mid + 1或r = mid,这样不容易死循环。网上很多教程用闭区间[l, r],容易在l和r相邻时陷入无限循环。选一种你习惯且不出错的,用到烂熟。
4.3 排序算法:从手写到sort的进阶
冒泡和插入排序是面试的入门考点,也是理解稳定排序的起点。但在LeetCode上,你几乎不需要自己写这些基础排序算法,因为STL的sort就是优化的快速排序,或者叫内省排序,处理绝大多数数据都是O(n log n),且在数据接近有序时表现比纯快排更好。直接用就好。
需要知道的是sort的进阶用法。sort(v.begin(), v.end(), greater<int>())降序排;stable_sort在需要保持相等元素相对顺序时用,比如“按频率对单词排序”。partial_sort求TopK时比全排序更快,比如求最大的3个数,partial_sort(v.begin(), v.begin()+3, v.end(), greater<int>())。还有nth_element,可以线性时间内找到数组中第K大的元素,不求全部有序,能省不少计算。如果你刷TopK题时不想到用priority_queue,那nth_element是你的第二选择。
关于自定义排序,最常用的是lambda表达式:
sort(people.begin(), people.end(), [](const vector<int>& a, const vector<int>& b) { if (a[0] != b[0]) return a[0] > b[0]; return a[1] < b[1]; });写lambda时注意严格弱排序规则:如果两个元素相等,比较器必须返回false,否则sort可能崩溃。还有一点,lambda捕获参数时要小心引用捕获的临时变量,容器在排序期间不能修改元素值,否则结果不可预期。
4.4 单调栈与判断质数的优化思路
判断质数是很多数学类题目的基础操作。最简单的是试除法,从2遍历到sqrt(n),但复杂度O(sqrt(n))在多次调用时会很吃力。更好的是预处理质数表,用埃氏筛。用C++写埃氏筛时可以配合vector<bool>或bitset,注意vector<bool>是特化版本,会压缩存储,但访问速度稍慢。如果你既要快又要省内存,bitset也很顺手,只是长度需要编译期常量,刷题时一般用vector 或vector 更利索。
vector<int> sieve(int n) { vector<bool> isPrime(n + 1, true); isPrime[0] = isPrime[1] = false; for (int i = 2; i * i <= n; ++i) { if (isPrime[i]) { for (int j = i * i; j <= n; j += i) isPrime[j] = false; } } vector<int> primes; for (int i = 2; i <= n; ++i) if (isPrime[i]) primes.push_back(i); return primes; }埃氏筛的时间复杂度是O(n log log n),在n为一百万以内时非常快。如果你只需要判断单个大数是否为质数,试除法到sqrt(n)就够了,不要过度设计。
4.5 周赛视角:如何用STL提速
LeetCode周赛热词经常出现,说明关注周赛的人越来越多。周赛的题目一般是四道题,前两道考察基础容器和哈希表,后两道涉及DP、图论或贪心,有时还需要计算几何。参加过几次周赛的用户会发现:前三道题完全用STL解决绰绰有余,关键问题不是“STL会不会用”,而是“用哪个容器、怎么组织数据”的决策速度。
对比一下,同样是统计一组单词的频率再做排序输出,新手可能用vector嵌套pair再手写排序比较器,熟练的人直接用unordered_map计数、sort排序,三分钟实现完。这就是STL使用熟练度的差距。我建议每周周赛之后不要只看排名,把四道题都重新做一遍,分别记录自己第一次提交的用时和最终优化后的用时,这样能明显看到自己决策速度的变化。
5. 刷题之外的工程迁移能力
5.1 回调函数的本质与STL中的实际应用
热词里有一个“c++回调函数例子”。很多人刷题刷到一定程度,觉得算法题就是数组和字符串,跟真实工程没关系。其实回调函数就是存在于STL各处的一个核心概念。sort的比较器、priority_queue的比较器、for_each里的函数对象,本质都是回调。
最直接的写法是lambda表达式,它是C++11引入的语法糖,能够就地书写函数逻辑。再解释一层,lambda在编译时会生成一个匿名函数对象,所以它能被当作“可调用对象”传入算法。回调在工程上最常见的使用场景就是注册事件处理,比如某个网络库收到数据后调用你的处理函数。你在刷题时熟练掌握lambda,后面看工作代码时也会轻松很多。
5.2 面向数据库的C++绑定:从STL到参数化写入
热词里有个特别工程化的案例:“tdengine, c++绑定写入数据库”。TDengine是一个时序数据库,它提供了C/C++接口。很多人第一次接触这类接口时有点懵,因为这不是STL容器,而是C风格的函数调用。
TDengine的写入流程大致是这样:先用taos_stmt_init创建一个预处理语句对象,再用taos_stmt_prepare准备SQL语句,SQL里用?占位符,然后逐个用taos_stmt_bind_param绑定参数,最后taos_stmt_execute执行。这个过程和你在刷题时“先准备数据结构,再填充数据”的思维方式完全一致,只不过对象换成了数据库。
举个例子,往TDengine写入一条设备温度记录,代码骨架大概是:
// 伪代码示意 TAOS_STMT* stmt = taos_stmt_init(conn); const char* sql = "INSERT INTO meters VALUES (?, ?, ?)"; taos_stmt_prepare(stmt, sql, strlen(sql)); TAOS_BIND params[3]; // 分别绑定时间戳、设备ID、温度值 for (int i = 0; i < 3; i++) { taos_stmt_bind_param(stmt, ¶ms[i]); } taos_stmt_execute(stmt); taos_stmt_close(stmt);刷题练出来的能力在这里直接体现:你会关注参数对齐、类型匹配、内存生命周期,因为这些本质和vector下标管理没什么区别。如果你刷题时就能处理好“结构体里的指针指向哪里”这种问题,数据库参数绑定自然上手更快。
5.3 时间等待、随机数、键盘映射等小工具
工程中还有很多需要写的小逻辑:等待一定时间、生成随机数、监听键盘输入。C++11之后,std::this_thread::sleep_for配合std::chrono::seconds就能实现时间等待,而老式写法是sleep或Sleep。
#include <thread> #include <chrono> using namespace std; this_thread::sleep_for(chrono::milliseconds(500));随机数方面,不要用老的rand()配srand了,那东西质量差还容易踩坑。C++11提供了<random>库,用random_device配合mt19937生成高质量随机序列。如果是刷题或者简单脚本用,rand()问题不大,但工程上请使用<random>。
键盘映射这类需求,一般要调用操作系统API,比如Windows下用RegisterHotKey或SetWindowsHookEx,这已经超出STL范畴。但它的思想仍然是“事件注册+回调处理”,和前面讲回调函数是一致的。不用被操作系统API吓住,底层逻辑和刷题是共通的。
5.4 一个经典误会:STL库和STL三维模型文件
热词里出现了“qopengl 加载stl,3dsmax2012修复stl模型的uv”这类词。这里必须澄清一个让人头痛的命名冲突:C++ STL库(Standard Template Library)和3D打印领域里的STL文件格式(STereoLithography)是完全不同的东西,只是缩写恰好一样。
如果你在QOpenGL程序里要加载STL模型,你读的是一堆三角形面片数据,和C++的vector、map没有任何关系。同样,3ds Max修复STL模型的UV,是在处理网格贴图坐标,也不涉及C++标准库。很多初期开发者会被这两个同名的概念搞混,在一个问题上搜索半天,结果发现搜出来全是另一个领域的内容。遇到这类情况,搜索时建议带上上下文词,比如“C++ STL容器”和“OpenGL STL模型”就完全走两套路线了。
5.5 为什么C++看起来没那么“普遍”
热词里有个问题很有意思:“c++为什么没有普遍”。这个问题其实反映了新手的困惑:好像周围人都在学Python和Java,C++是不是没人用了?事实恰恰相反,C++在操作系统、游戏引擎、高性能计算、数据库内核、嵌入式、量化交易等领域依然是绝对主力。它之所以在“泛程序员”群体里显得不那么普遍,是因为学习曲线陡峭,几乎无法速成。
Python两三天就能实现一个爬虫,C++三天可能还在和指针、编译错误搏斗。很多人学到指针之后就放弃了,所以你在社交媒体上看到的C++内容也比Python少。但如果你把LeetCode和STL吃透,你会发现C++表达算法题特别清晰,没有GC的垃圾回收干扰,也没有动态类型的隐式转换,每一步都要你自己负责。正是这种“显式”的风格,才让C++成为吃性能的系统和算法的最佳表达语言。
6. 常见问题与排查技巧实录
6.1 VSCode所有函数变量都没办法跳转
这是配置问题,不是代码问题。最常见原因是没装C/C++插件,或者装了插件但没配置c_cpp_properties.json。打开VSCode命令面板,输入C/C++: Edit Configurations,在弹出的json里把compilerPath设成实际g++.exe的路径,然后重启VSCode。还有一种情况是项目里有多个源文件,IntelliSense需要知道每个文件的编译参数,这时建议开启compileCommands配置,从compile_commands.json读取。
如果配置了还不跳转,多半是插件版本或工作区缓存问题。把C/C++插件禁用再启用,或者删除.vscode目录重新生成,基本能解决。切记每次切换编译器或更新工具链后,要重新生成IntelliSense索引,不然它还在用旧的索引数据。
6.2 一边遍历一边删除容器元素,程序崩溃
这个问题我前面提过,现在完整展开。vector遍历中执行erase后,当前迭代器失效,如果你继续用它自增,就是未定义行为,轻则跳过元素,重则崩溃。正确写法是:
for (auto it = v.begin(); it != v.end(); ) { if (*it % 2 == 0) it = v.erase(it); else ++it; }但对于刷题场景,更优雅的做法是先记住要删除的下标或元素,遍历结束后再删除。比如先统计每个元素出现次数,再对所有出现次数大于1的键执行erase。相比一边遍历一边删除,这种“先收集后处理”的方式能避免多数迭代器失效问题。
6.3 string转数字和数字放大溢出问题
字符串转数字用stoi、stol、stoll,但也可能抛异常。如果字符串包含非数字字符,stoi会抛出std::invalid_argument;如果超出范围,会抛出std::out_of_range。刷题时可以不管异常,但工程上最好用std::from_chars,它是C++17引入的高性能无异常转换函数,在GCC 11以上才稳定支持。
数字放大溢出这个更常见。LeetCode里经常让求数组的连续子数组乘积、两个大数相加、大数乘法,如果题目允许的范围超过int,就要使用long long;再大就要考虑字符串模拟大数运算。有一个小习惯:凡涉及乘积、累加、求幂,先想想会不会超过2^31 - 1,就基本能规避大部分溢出问题。
6.4 错误速查表
我整理了一个高频错误速查表,你们收藏下来,遇到问题直接查:
| 症状 | 常见原因 | 解决方法 |
|---|---|---|
| 缺少VCRUNTIME140.dll | 未安装VC运行库 | 安装Visual C++ Redistributable 2015-2022 x64 |
| C4996: fopen 不安全 | MSVC安全检查 | 定义_CRT_SECURE_NO_WARNINGS或用fopen_s |
| 变量无法跳转/IntelliSense无提示 | includePath或compilerPath配置错误 | 编辑c_cpp_properties.json后重启 |
| 编译报错未声明标识符 | 头文件忘了include或命名空间冲突 | 检查using namespace std是否与局部变量重名 |
| 容器遍历时崩溃 | 迭代器失效 | 用it = erase(it)配合重新赋值 |
| 运行结果出现超大负数 | int溢出 | 换成long long并检查中间结果范围 |
| 递归深度过大系统栈溢出 | 递归调用过深 | 改写成迭代栈或使用std::stack |
| string转int抛异常 | 字符串不是纯数字 | 使用从字符串解析或from_chars |
6.5 几个容易忽略的实战小技巧
最后写几个我平时刷题和写工程代码时沉淀下来的小技巧,都属于“没人告诉你但你迟早要踩”的类别。
第一个,调试时善用prlong longf和条件断点。很多人调试C++代码喜欢打无数断点,然后一直按下一步,效率低得吓人。我更推荐在关键位置打cout <<输出中间变量,或者只打一两个断点,配合watch窗口观察特定值的走向。刷题时尤其如此,因为你通常只需要看一两轮循环里的状态变化。
第二个,MinGW用户提交LeetCode前,注意本地编译器和LeetCode的编译器差异。最典型的是#include <bits/stdc++.h>在本地能用,但有些平台不支持或表达很慢。为了保险,最好养成显式include需要的头文件的习惯,这也能帮助你理清依赖关系。
第三个,数据结构尽量在栈上创建。刷题时写vector<vector<int>> dp(n, vector<int>(m, 0));,这个dp在函数返回时会自动释放。如果你用new创建了vector,记得delete。没有内存泄漏的良好习惯,在写LeetCode这种短时程序时看不出问题,但迁移到真实工程时就是灾难。
第四个,善用std::numeric_limits<int>::max()获取整数最大值,不要手写成0x3f3f3f3f。虽然那个鬼数在竞赛圈很流行,但LeetCode刷题写代码时,可读性比那一点性能重要得多。
6.6 刷题三个月后的进阶建议
当你把前200题热门题目刷完一遍,STL常见容器都能熟练使用之后,会有一种“什么题都能写但写不优雅”的感觉。这是正常的瓶颈期。这时候建议做三件事:第一,把之前AC过的题重新做一遍,要求每道题比第一次提交少用一半时间;第二,开始限时模拟,按30分钟、45分钟、60分钟随机抽题做;第三,把你自己的代码模板整理成“个人手册”,比如二分模板、单调栈模板、DFS模板、BFS模板各存一份,形成肌肉记忆。
到这个阶段,你已经不需要再关注STL容器的基本用法了,而是逐渐开始思考“这个容器在这个场景下是否最优”。比如用deque实现滑动窗口求最大值明显优于vector加priority_queue的复杂维护,用unordered_map做记忆化搜索的缓存时,如何选择键的类型使哈希效率最高。这些思考才是从“会用STL”到“用好STL”的分水岭,也是LeetCode刷到两百题以后还能继续变强的关键。
我个人在实际操作中的体会是:LeetCode题解看十遍,不如自己动手写两遍。有一段时间我热衷于收藏各种“万题模板”,收藏夹里堆了几十篇,真正到用的时候脑子一片空白。后来改成每学一个模板,就在当周周赛里故意找一道能套用的题去实践,只有被题目“虐”过一遍,那些模板才算真正长在你身上。写代码这件事没有捷径,但把手里的工具用好、把错误记录好,确实是能让人少走很多弯路的。