☰
LeetCode刷题指南:C++ STL容器选型与高频算法模板
2026/10/8 8:36:35 网站建设 项目流程

从第一次点开 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++,编译参数直接带上-std=c++17和-O2,刷题代码不需要额外的库,所以参数越多反而越容易出问题:

{ "version": "2.0.0", "tasks": [ { "type": "cppbuild", "label": "C/C++: g++ 编译当前文件", "command": "C:/msys64/mingw64/bin/g++.exe", "args": [ "-fdiagnostics-color=always", "-g", "${file}", "-o", "${fileDirname}/${fileBasenameNoExtension}.exe", "-std=c++17", "-O2" ], "options": { "cwd": "${fileDirname}" }, "problemMatcher": ["$gcc"], "group": { "kind": "build", "isDefault": true } } ] }

launch.json负责调试,只要保证program指向刚才生成的 exe,miDebuggerPath指向 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,并且会在知道规模时提前调用reserve:

vector<int> ans; ans.reserve(n); // 避免反复扩容导致的拷贝开销 for (int i = 0; i < n; ++i) { ans.push_back(nums[i]); }

这个习惯特别重要。假如你在循环里持续push_back而不reserve,vector 会按 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_map<int, int> cnt; for (int x : nums) cnt[x]++;

set 和 unordered_set 同理:只需要判重时,unordered_set就够。还有一个高频出错的点:STL 的内置哈希不覆盖所有类型。比如pair<int, int>作为 key 时,很多版本的 STL 没有默认哈希函数,直接编译报错。我通常的解法是把它编码成long long:

long 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异常,在做“字符串转数组”类题目时,一定要先确认格式。

字符串数组初始化也是热词里反复出现的需求。C++11 之后最舒服的写法是直接列表初始化:

vector<string> words = {"apple", "banana", "cherry"};

如果是固定大小的字符数组,可以这样:

char grid[3][10] = {"abc", "def", "ghi"};

这里有个小坑:char grid[3][10]每个字符串最多 9 个字符,因为末尾要留\0,超了会编译报错。vector<string>没有这个限制,所以我优先推荐它。

3. 高频算法模板:排序、单调栈与二分答案

3.1 排序:冒泡、插入、快排和 std::sort 的分工

排序是 LeetCode 万题之基。手写冒泡排序虽然实战中没人用,但笔试偶尔会考,它也是理解稳定排序的入门例子:

void bubbleSort(vector<int>& 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 pair<int,int>& a, const pair<int,int>& b) { if (a.first != b.first) return a.first > b.first; // 按第一字段降序 return a.second < b.second; // 第一字段相同时升序 });

这个写法的好处是:比较逻辑完全内联在读代码的人眼前,不用去翻一个全局函数或函数对象定义。LeetCode 官方题解里百分之八九十的自定义排序都是这么写的。

3.2 单调栈:一类题型的通解框架

单调栈是我最想推荐给大家的“性价比”算法模板,因为它一旦学会了,一大类“找下一个更大/更小元素”的题目全部秒杀。它的核心思想很简单:维护一个栈,从栈底到栈顶保持单调递增或递减,在出栈的时候结算答案。

最经典的通解模板如下,这里以“找每个元素右边第一个比它大的元素”为例:

vector<int> dailyTemperatures(vector<int>& temperatures) { int n = temperatures.size(); vector<int> ans(n, 0); // 栈里存的是下标,不是温度值 stack<int> 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。所以循环可以每次加 6:

bool 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(vector<int>& 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(vector<int>& data, const std::function<bool(int,int)>& cmp) { sort(data.begin(), data.end(), cmp); }

实际写代码时,lambda 更是到处都在用。它本质上是匿名函数对象,捕获外部变量也方便。把“指定顺序输出”的需求交给 lambda 比较器,比任何回调机制都直观,这也是前面 3.1 里那个排序例子的底层原理。

4.2 随机数、等待时间和文件 I/O 的坑

C++ 的随机数有一个经典问题:rand()生成的随机数质量差,而且rand() % n会引入模偏差。想要“真正的随机数”,C++11 之后要用<random>:

std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<int> dist(1, 100); int randNum = dist(gen); // 1 到 100 之间的均匀随机整数

如果你需要“等待一会儿再继续”,用<thread>里的sleep_for:

std::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_s:

FILE* 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 容器组织好要写入的批量数据,比如vector<Row>,然后逐条绑定参数、批量执行。这里 STL 的价值在于:你不用自己手写动态数组、不用管理字符串的重新分配,容器的生命周期还能帮你把内存释放问题降到最低。我见过不少从纯 C 转 C++ 的同事,第一次看到taos_stmt_bind_param的回调接口时束手无策,本质上是缺少“容器组织 + 指针传递 + 生命周期”这套训练。这正是刷题带来的隐形成长——STL 用得熟了,面对任何陌生 API 都能更快地理解它想让你怎么管理数据。

5. 常见问题排查和避坑实录

5.1 VSCode 跳转失败和智能提示失效怎么办

如果你发现 VSCode 里所有函数、变量都不能 Ctrl+点击跳转,通常是三个原因:c_cpp_properties.json的includePath配错、IntelliSense 引擎卡在了 Tag Parser 模式、或者某个.vscode配置没有被真正加载。

我推荐的最快排查步骤:先按Ctrl+Shift+P,执行“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": "c++17", "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++,或链接时加-lstdc++
fopen 报 C4996 安全错误MSVC 要求使用_s安全函数使用fopen_s,或定义_CRT_SECURE_NO_WARNINGS
s.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++ 的世界里找到自己的节奏。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询