
从第一次点开 LeetCode 到现在我大概刷了上千道题但真正让我从“刷了忘、忘了刷”里走出来的反而不是题量而是两样东西一套固定的刷题方法以及把 C STL 用得足够熟。很多刷题的人会有同感——思路没问题代码也写得出来但要么总是编译报错要么总是超时要么就是容器选错导致代码写得又长又慢。这篇《LeetCode 刷题指南与 C STL 使用手册》就是把我这些年的刷题方法论、STL 容器和算法的实战取舍、以及日常踩坑记录整理出来新手照着做能少走三个月弯路有基础的人也能在容器选型、算法模板、环境排障几个环节里找到一些资料里不细写的老实话。先说明一个容易混淆的点这篇里的 STL 指的是 C 的 Standard Template Library标准模板库是容器、迭代器、算法、函数对象那一整套东西不是 3D 打印里用来描述模型表面的 .stl 文件格式。两者除了缩写一样没有任何关系。下面进入正题。1. 开刷前的准备环境搭建和选题策略1.1 VSCode 配置 C/C 环境的三个关键点工欲善其事必先利其器。LeetCode 官方网页版做题很方便但只要你开始按专题刷题、写一些超过单文件规模的测试代码本地编辑器还是绕不开。VSCode 是绝大多数人的选择配置 C/C 环境的核心其实只有三个文件tasks.json、launch.json、c_cpp_properties.json。tasks.json负责编译我一般用一个最简配置把编译器指向g编译参数直接带上-stdc17和-O2刷题代码不需要额外的库所以参数越多反而越容易出问题{ version: 2.0.0, tasks: [ { type: cppbuild, label: C/C: g 编译当前文件, command: C:/msys64/mingw64/bin/g.exe, args: [ -fdiagnostics-coloralways, -g, ${file}, -o, ${fileDirname}/${fileBasenameNoExtension}.exe, -stdc17, -O2 ], options: { cwd: ${fileDirname} }, problemMatcher: [$gcc], group: { kind: build, isDefault: true } } ] }launch.json负责调试只要保证program指向刚才生成的 exemiDebuggerPath指向 gdb 就行这里不展开因为刷 LeetCode 真正反复用的其实不是调试器而是编译输出里的报错信息。第三个c_cpp_properties.json主要用于智能提示和跳转后面第 5 节我会专门讲“跳转失效”的坑。这里只提醒一件事Windows 下如果 C/C 扩展自动检测不到 MinGW 路径手写includePath的时候一定要把编译器自带的 include 目录加全最常见的现象就是#include algorithm都标红。另一个和 Windows 用户关系很大的细节是 Microsoft Visual C Redistributable。如果你用 MSVC 的cl.exe编译或者从网上下载的某个 exe 是 MSVC 编译的运行时经常报“缺少 MSVCP140.dll”。这个 dll 不是某个第三方库而是 VC 运行库的一部分解决办法就是安装“Microsoft Visual C 2015-2022 Redistributable (x64)”。我推荐本地刷题直接统一用 MinGW 的 g把这类系统级麻烦降到最低。1.2 刷题节奏和选题策略热门 100 题不是拿来背的选题比埋头刷重要得多。LeetCode 官方有“热门 100 题”这个列表很多人把它当成题库一顿乱刷其实它更像一张考点地图。我建议把热门 100 题按标签拆开数组、哈希表、双指针、滑动窗口、二叉树、回溯、动态规划、贪心每个标签先挑 5 到 8 道最典型的题集中突破而不是按题号顺序刷。按专题刷的好处非常明显同类题目的套路是相似的你今天做 3 道滑动窗口明天遇到第 4 道时思路迁移几乎是零成本的。节奏方面普通人没必要每天刷十道然后累到退坑。我自己的节奏是工作日每天 2 道周末集中补 3 到 4 道。题目做完以后当天晚上花十分钟复盘一次周末再把本周卡住过的题重新手写一遍不需要打开编辑器——直接纸上写思路。这种“短期重复 间隔回顾”的方法比盲目堆量有效得多。另外我很推荐参加 LeetCode 周赛哪怕次次只过一两道。周赛有一个日常刷题给不了的约束时间。在 60 分钟内逼自己快速识别题型、快速写代码能非常精准地暴露你的薄弱点。最近几场周赛比如周赛 430 这类场次难度梯度都很标准适合用来检验自己进入刷题中期之后的真实水平。有人会问C 语言这么复杂为什么刷题必选它其实 LeetCode 官方支持十几种语言但 C 有两个别人替代不了的优势一是运行时最快同样的 O(n log n) 算法C 基本不会因为语言层面的额外开销超时二是 STL 提供了一整套高质量容器和算法写代码就像搭积木。C 为什么没有“普遍流行”主要是学习曲线陡、语法细节多但如果你目标明确就是算法和面试C 恰好是把这些成本转化到能力上最實惠的语言。2. STL 容器怎么选底层原理和实战取舍2.1 vector、list、deque底层逻辑决定你用谁容器选型是 STL 使用手册里最核心的部分因为选错容器代码的正确性和性能都会出问题。三个线性容器各有各的底层逻辑vector连续内存支持 O(1) 随机访问尾部插入均摊 O(1)但头部或中间插入删除是 O(n)。扩容时会发生整体拷贝这是新手最容易忽略的性能黑洞。list双向链表任意位置插入删除都是 O(1)但不支持随机访问遍历一个节点要沿着指针跳缓存局部性差刷题场景 90% 用不上。deque分段连续内存头尾插入删除都是 O(1)也支持 O(1) 随机访问。写滑动窗口求最大值时它是一个好选择因为需要在两头操作。你可以把 vector 想象成一排固定门牌号的酒店房间走廊连续走到哪间都很快但要在中间拆墙加房间就很麻烦list 则像一群人手拉手排成的队伍不用连续但要找到队伍中间某个人的话你得从队头一个个数过去。LeetCode 刷题最常见的情况是“先知道要处理的数据规模再一次性装入数据”所以我几乎只用 vector并且会在知道规模时提前调用reservevectorint ans; ans.reserve(n); // 避免反复扩容导致的拷贝开销 for (int i 0; i n; i) { ans.push_back(nums[i]); }这个习惯特别重要。假如你在循环里持续push_back而不reservevector 会按 1、2、4、8 的倍数扩容最坏情况下拷贝总代价会接近 O(n^2)。提前 reserve 一下整个过程就从 O(n^2) 变成了 O(n)。我见过很多人代码逻辑完全正确只因为少了这一行就反复超时非常可惜。2.2 map、unordered_map、set有序和无序的本质差别哈希表和有序树的区别很多新手背了“红黑树 vs 哈希表”还是不会选。我给你一个更实用的判断标准如果只是做“这个值出现过没有”“这个值出现了几次”这类查找统计无脑用unordered_map平均 O(1)如果题目要求按 key 的顺序输出结果或者需要调用lower_bound/upper_bound做范围查询才用map它的内部是红黑树有序但查找是 O(log n)。刷题时我统计词频的标准写法是这样unordered_mapint, int cnt; for (int x : nums) cnt[x];set 和 unordered_set 同理只需要判重时unordered_set就够。还有一个高频出错的点STL 的内置哈希不覆盖所有类型。比如pairint, int作为 key 时很多版本的 STL 没有默认哈希函数直接编译报错。我通常的解法是把它编码成long longlong long key (long long)a * 1000003 b;只要数据范围确定不会溢出这种方式既简单又高效能避开自定义哈希结构体的麻烦。2.3 string 操作和数组初始化细节里翻车最多的地方字符串在 LeetCode 里几乎场场出现。string提供了、、push_back、substr、find、stoi、to_string这些成员函数大部分情况下够用。但有几个细节很容易踩坑第一substr返回的是一个新字符串如果只是取一个字符比如s[i]不要写成s.substr(i, 1)性能差而且没有意义。第二find找不到时返回string::npos这个值等于-1转换成的巨大无符号数很多人写if (s.find(c) ! -1)虽然能过编译但正确写法是if (s.find(c) ! string::npos)。第三stoi转数字时如果字符串不是合法数字会抛出invalid_argument异常在做“字符串转数组”类题目时一定要先确认格式。字符串数组初始化也是热词里反复出现的需求。C11 之后最舒服的写法是直接列表初始化vectorstring words {apple, banana, cherry};如果是固定大小的字符数组可以这样char grid[3][10] {abc, def, ghi};这里有个小坑char grid[3][10]每个字符串最多 9 个字符因为末尾要留\0超了会编译报错。vectorstring没有这个限制所以我优先推荐它。3. 高频算法模板排序、单调栈与二分答案3.1 排序冒泡、插入、快排和 std::sort 的分工排序是 LeetCode 万题之基。手写冒泡排序虽然实战中没人用但笔试偶尔会考它也是理解稳定排序的入门例子void bubbleSort(vectorint a) { int n a.size(); for (int i 0; i n - 1; i) { bool swapped false; for (int j 0; j n - i - 1; j) { if (a[j] a[j 1]) { swap(a[j], a[j 1]); swapped true; } } if (!swapped) break; // 提前结束优化 } }插入排序则在“近乎有序”的数组上表现异常好接近 O(n)。它也是 std::sort 内部优化的一部分当递归到小数组时introsort 会切到插入排序。这也是为什么了解底层能让你的优化方向更清晰。实际刷题时直接std::sort就行。它的内部实现是 introspection sort简单说就是快排 堆排 插入排序的组合大部分情况用快排递归深度太深就切堆排规避最坏 O(n^2)小数组切插入排序充分利用局部性。平均 O(n log n)。但要注意std::sort不是稳定排序需要“相同 key 保持原相对顺序”时用std::stable_sort。配合“指定顺序输出”的需求STL 算法 lambda 是刷题神器sort(people.begin(), people.end(), [](const pairint,int a, const pairint,int b) { if (a.first ! b.first) return a.first b.first; // 按第一字段降序 return a.second b.second; // 第一字段相同时升序 });这个写法的好处是比较逻辑完全内联在读代码的人眼前不用去翻一个全局函数或函数对象定义。LeetCode 官方题解里百分之八九十的自定义排序都是这么写的。3.2 单调栈一类题型的通解框架单调栈是我最想推荐给大家的“性价比”算法模板因为它一旦学会了一大类“找下一个更大/更小元素”的题目全部秒杀。它的核心思想很简单维护一个栈从栈底到栈顶保持单调递增或递减在出栈的时候结算答案。最经典的通解模板如下这里以“找每个元素右边第一个比它大的元素”为例vectorint dailyTemperatures(vectorint temperatures) { int n temperatures.size(); vectorint ans(n, 0); // 栈里存的是下标不是温度值 stackint st; for (int i 0; i n; i) { while (!st.empty() temperatures[st.top()] temperatures[i]) { int idx st.top(); st.pop(); ans[idx] i - idx; } st.push(i); } return ans; }为什么栈里要存下标而不是直接存值因为答案经常需要计算距离。这也是新手最容易犯的错——把值入栈等出栈的时候根本不知道它对应的位置在哪。这道题就是 LeetCode 的“每日温度”你如果去翻评论区就会发现几乎所有高效做法都是这个模板的变体。单调栈的时间复杂度是 O(n)因为每个下标最多入栈一次、出栈一次。用生活里的例子理解就像排队买奶茶每个人只关心前面第一个比自己高的背影一旦看到那个人自己就离开队伍去追他。理解这个场景后再看“柱状图中最大的矩形”“接雨水”这些变形题思路会非常顺。3.3 快速幂、质数判断和二分答案三个高频小技巧快速幂是用来在 O(log n) 时间内计算 a 的 n 次方的刷题时经常配合取模使用比如 (10^9 7) 取模的场景。迭代写法比递归更省心long long fastPow(long long a, long long n, long long mod) { long long res 1; while (n 0) { if (n 1) res res * a % mod; a a * a % mod; n 1; } return res; }质数判断看似简单写不好性能差一个量级。最基础的优化是只遍历到 sqrt(n)再进阶一点是 6k±1 判定除了 2、3 以外所有质数都满足 n % 6 1 或 n % 6 5。所以循环可以每次加 6bool isPrime(int n) { if (n 1) return false; if (n 3) return true; if (n % 2 0 || n % 3 0) return false; for (int i 5; (long long)i * i n; i 6) { if (n % i 0 || n % (i 2) 0) return false; } return true; }另一个高频技巧是二分答案。它解决的不是“在一个有序数组里查找某个数”而是“在一个单调的答案区间里找最合适的那个答案”。LeetCode 875“爱吃香蕉的狒狒”就是经典例子给定狒狒每小时最多吃的香蕉数求能在 H 小时内吃完所有堆的最小速度。速度 k 是一个单调量——k 越大耗时越小所以可以二分int minEatingSpeed(vectorint piles, int h) { auto needHours [](int speed) { long long hours 0; for (int p : piles) hours (p speed - 1) / speed; return hours; }; int lo 1, hi *max_element(piles.begin(), piles.end()); while (lo hi) { int mid lo (hi - lo) / 2; if (needHours(mid) h) hi mid; else lo mid 1; } return lo; }这里(p speed - 1) / speed是很实用的向上取整写法刷题时比ceil函数可靠不涉及浮点误差。整个模板记住之后遇到“最小化最大值”“最大化最小值”一类问题基本都可以套。4. 进阶技巧和易错点从链表到随机数4.1 链表结构体、回调函数和自定义排序LeetCode 的链表题会给你现成的结构体定义但实际工程里你也得会自己写。最基础的链表节点结构体长这样struct ListNode { int val; ListNode* next; ListNode(int x) : val(x), next(nullptr) {} };构造函数里的next(nullptr)是很多新手忽略的——不初始化指针它就是野指针后面遍历时会崩得莫名其妙。所有涉及链表的题都建议养成“节点创建即初始化”的习惯。回调函数也是 C 里一个高频考点。最原始的方式是函数指针但刷题和工程里更常用的是std::function和 lambda。比如你要把一个比较规则作为参数传递给另一个函数可以这样void process(vectorint data, const std::functionbool(int,int) cmp) { sort(data.begin(), data.end(), cmp); }实际写代码时lambda 更是到处都在用。它本质上是匿名函数对象捕获外部变量也方便。把“指定顺序输出”的需求交给 lambda 比较器比任何回调机制都直观这也是前面 3.1 里那个排序例子的底层原理。4.2 随机数、等待时间和文件 I/O 的坑C 的随机数有一个经典问题rand()生成的随机数质量差而且rand() % n会引入模偏差。想要“真正的随机数”C11 之后要用randomstd::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distributionint dist(1, 100); int randNum dist(gen); // 1 到 100 之间的均匀随机整数如果你需要“等待一会儿再继续”用thread里的sleep_forstd::this_thread::sleep_for(std::chrono::milliseconds(500));别看这行简单很多人写Sleep(500)那是在 Windows API 下才有的函数换到 Linux 上直接编译失败用std::chrono是跨平台的标准解法。文件 I/O 这里有一个几乎每个用 MSVC 的 C/C 开发者都会撞上的坑fopen报安全错误提示 C4996。因为微软的 CRT 把所有fopen、strcpy这类函数标记为不安全要求你改用带_s后缀的版本。解决方式有两种一是直接用fopen_sFILE* fp nullptr; errno_t err fopen_s(fp, data.txt, r); if (err ! 0) { /* 打开失败 */ }二是在代码开头定义_CRT_SECURE_NO_WARNINGS或者直接关掉警告。我个人更推荐前者因为_s版本会强制你检查错误码这个习惯在 64 位环境下尤其重要——文件路径、缓冲区长度这类问题在 64 位下更隐蔽。至于流式 I/O刷题提交代码时我习惯在main开头加两行ios::sync_with_stdio(false); cin.tie(nullptr);这两行能让cin/cout的输入输出速度大幅提升原理是切断了与 C 标准 I/O 的同步以及解除了 cin 和 cout 的绑定关系。竞赛场景里它们是“救命”的日常工程里则无所谓。4.3 性能细节reserve、迭代器失效和传参前面提到过reserve的重要性这里再补充几个同样容易被忽略的性能细节。第一遍历容器时尽量用常量引用for (const auto x : vec) { ... }如果写成for (auto x : vec)每个元素都会被拷贝一遍当元素是 string 或自定义对象时浪费非常明显。第二vector 的迭代器失效问题。最典型的错误是在循环里删除元素for (auto it vec.begin(); it ! vec.end(); it) { if (*it % 2 0) vec.erase(it); // 危险erase 后迭代器失效 }正确的策略是使用“erase-remove”惯用法vec.erase(std::remove_if(vec.begin(), vec.end(), [](int x) { return x % 2 0; }), vec.end());remove_if先把要删除的元素挪到容器末尾返回新的逻辑结尾再统一 erase这样既不会出现迭代器失效性能也远优于单个erase。第三字符串拼接。s s x和s x表面相似实际上前者会生成临时对象反复使用时发生多次拷贝。刷题时碰到大量拼接的题要么s.reserve()预留空间要么尽量使用或push_back。4.4 从刷题到工程STL 底子怎么用在真实 C 接口上有人觉得刷题和工程是两回事其实不是。就拿 TDengine 这类时序数据库的 C/C 绑定来举例写入数据时用到的taos_stmt_prepare、taos_stmt_bind_param、taos_stmt_execute这套预处理接口核心考验的就是“数据怎么组织、内存谁管理”。实际工程里常见做法是先用 STL 容器组织好要写入的批量数据比如vectorRow然后逐条绑定参数、批量执行。这里 STL 的价值在于你不用自己手写动态数组、不用管理字符串的重新分配容器的生命周期还能帮你把内存释放问题降到最低。我见过不少从纯 C 转 C 的同事第一次看到taos_stmt_bind_param的回调接口时束手无策本质上是缺少“容器组织 指针传递 生命周期”这套训练。这正是刷题带来的隐形成长——STL 用得熟了面对任何陌生 API 都能更快地理解它想让你怎么管理数据。5. 常见问题排查和避坑实录5.1 VSCode 跳转失败和智能提示失效怎么办如果你发现 VSCode 里所有函数、变量都不能 Ctrl点击跳转通常是三个原因c_cpp_properties.json的includePath配错、IntelliSense 引擎卡在了 Tag Parser 模式、或者某个.vscode配置没有被真正加载。我推荐的最快排查步骤先按CtrlShiftP执行“C/C: Reset IntelliSense Database”重启 VSCode。如果还不行打开c_cpp_properties.json把编译器路径和 include 路径显式写好{ configurations: [ { name: Win64, includePath: [ ${workspaceFolder}/**, C:/msys64/mingw64/include/c/**, C:/msys64/mingw64/include/** ], defines: [_DEBUG, UNICODE], cStandard: c17, cppStandard: c17, intelliSenseMode: windows-gcc-x64, compilerPath: C:/msys64/mingw64/bin/g.exe } ], version: 4 }配完之后再把设置里C_Cpp.intelliSenseEngine改成Default保存重启。90% 的“全部跳转失效”问题都能解决。5.2 编译和运行错误速查表我把刷题两年里高频撞见的错误整理成一张表每次报错先对号入座别慌报错现象原因解决办法程序启动提示缺少 MSVCP140.dll缺少 VC 运行库安装 Microsoft Visual C 2015-2022 Redistributable (x64)g 编译报 undefined reference tostd::cout用 gcc 编 C 程序改用 g或链接时加-lstdcfopen 报 C4996 安全错误MSVC 要求使用_s安全函数使用fopen_s或定义_CRT_SECURE_NO_WARNINGSs.at(i)抛 out of range 异常下标越界检查循环边界改用s[i]前先判 i s.size()vector 遍历同时 erase 后运行异常迭代器失效改用 erase-remove 惯用法哈希表存储 pair 时编译失败pair 无默认哈希用(long long)a * N b编码cl 不是内部或外部命令MSVC 环境变量未配置使用 MinGW g或打开 x64 Native Tools 命令行5.3 我踩过的一些坑和刷题建议聊几个我印象最深的坑。第一个是“只刷题不复盘”。我前期刷了三百多道回头一看很多题完全没印象等于白刷。后来改成每周末抽 2 小时把本周错题重写一遍记忆牢固程度明显上了一个台阶。第二个是“死磕一道题”。一道题卡两小时以上边际收益就趋近于零了直接看题解、理解思路、隔天合上题解重写效率高得多。第三个是“迷信手写一切”。有人觉得用 STL 会“废掉基本功”但真实情况是std::sort、unordered_map、priority_queue这些组件本身就是在表达算法思想把这些用熟之后手写基础算法反而更清楚它们解决什么问题。另外刷题之外我特别建议做点“小玩具”项目。比如拿 C 写个小游戏、实现一个键盘映射工具、做一套字符串处理的小工具这些项目不需要多复杂但它们会逼你用上文件 I/O、STL 容器、回调函数和随机数。等你把刷题学到的容器和工程里的场景真正串起来你会发现 LeetCode 那套训练完全没白费它给你的是在任何 C 代码面前都不怵的底气。我个人至今还保留着一个习惯每次遇到一个新容器或新算法都会主动去翻一下 C 标准库文档里它的复杂度保证。就像出门旅游前先看地图STL 的复杂度表就是那张地图。希望这份指南也能让看到这里的你少走些弯路早点在 LeetCode 和 C 的世界里找到自己的节奏。