
力扣第1题“两数之和Two Sum”是很多C刷题人注册账号后点开的第一个算法题。我第一次认真写这道题时已经工作一年多平时写C业务代码没问题但“算法题”三个字还是让我心里打鼓。真正做完回头看这题最大的价值不是“简单”而是把三件事一次性串了起来暴力思路为什么会被淘汰、哈希表到底解决什么问题、以及C STL里那几个容器在算法题里该怎么用。这篇就围绕这三点展开适合刚开始刷力扣的C新人也适合想系统梳理STL用法的人。1. 两数之和的题目还原与“暴力直觉”的代价1.1 原题到底在问什么先把题目完整还原一遍很多人刷到后面会忘掉原题的长相。给定一个整数数组 nums 和一个整数目标值 target请你在该数组中找出和为目标值 target 的那两个整数并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是数组中同一个元素在答案里不能重复出现。 你可以按任意顺序返回答案。示例1输入 nums [2,7,11,15], target 9输出 [0,1]因为 nums[0] nums[1] 9。 示例2输入 nums [3,2,4], target 6输出 [1,2]。 示例3输入 nums [3,3], target 6输出 [0,1]。这里有个看似不起眼但极其重要的信息数组中同一个元素在答案里不能重复出现。这句话直接决定了哈希表解法里“先查再插”的顺序后面第2章会细说。题目还假设每种输入只对应一个答案所以不存在“多个答案返回哪个”的纠结。力扣绝大多数题目都会给这样的约束目的就是让你把注意力放在算法本身而不是答案歧义上。还有一个细节容易被忽略返回值是下标不是值。这意味着不能先把数组排序再用二分——排序会打乱下标。如果你下意识用了“排序双指针”的思路会发现下标全乱套了。这一点很多新手会踩雷我也是踩过之后才记住的。1.2 第一版双重循环解法能过样例但过不了复杂度这关绝大多数人的第一版会写成这样class Solution { public: vectorint twoSum(vectorint nums, int target) { int n nums.size(); for (int i 0; i n; i) { for (int j i 1; j n; j) { if (nums[i] nums[j] target) { return {i, j}; } } } return {}; } };这段代码的正确性没有任何问题。内层循环从i 1开始天然规避了“同一个元素用两次”的问题返回的{i, j}也天然有序。空间复杂度是 O(1)只用了两个循环变量。但它的问题在于时间复杂度是 O(n²)。当数组长度是 10 万时最坏情况下要执行约 50 亿次比较。力扣的评测数据虽然不至于到 10 万但到 10^4 量级时O(n²) 已经会在超时边缘徘徊。有个常见误解是“力扣第一题很简单暴力也能过”。确实这道题在数据量较小时暴力能过但如果你一直停留在暴力层后面遇到“两数之和”的变体题比如“和为 k 的连续子数组”“三数之和”会立刻卡住。暴力习惯最大的问题不是这一题过不过而是让你错过了思考“怎么少做无用功”的机会。1.3 复杂度数字背后的真实差距来算一笔账。假设机器每秒能执行 10^8 次简单操作数据规模 n双重循环 O(n²)哈希表 O(n)10^3约 10^6 次毫秒级约 10^3 次可忽略10^4约 10^8 次1 秒上下约 10^4 次可忽略10^5约 10^10 次100 秒约 10^5 次毫秒级“空间换时间”不是玄学是数学。哈希表解法用 O(n) 的空间把每次查找的耗时从 O(n) 压到平均 O(1)。这个思路会贯穿后续大量中等难度题目所以第1题虽然简单却是理解“为什么需要哈希表”的最佳入口。2. 哈希表解法拆解C里unordered_map的正确打开方式2.1 核心思想把“找另一半”变成“查字典”暴力法是“凑对子”我在人群里找一个人只能一个一个问。哈希表的思路是先把见过的每个人登记到花名册上每遇到一个新的人只需要在花名册里翻一下有没有我要找的“另一半”。对应到题目里对于数组里的每一个数nums[i]我们要找的目标是target - nums[i]。如果这个目标值已经在自己维护的“登记表”里出现过那答案就是“登记表里存的下标” 当前下标i。所以问题被转化成一个很朴素的逻辑维护一个从“数值”到“数组下标”的映射。C 里最顺手的容器是unordered_map它的键存数值、值存下标。2.2 完整代码逐行拆解从find到emplaceclass Solution { public: vectorint twoSum(vectorint nums, int target) { unordered_mapint, int hash; for (int i 0; i nums.size(); i) { int need target - nums[i]; auto it hash.find(need); if (it ! hash.end()) { return {it-second, i}; } hash.emplace(nums[i], i); } return {}; } };逐行来看unordered_mapint, int hash;创建空哈希表键是数组元素的值值是该值首次出现时的数组下标。int need target - nums[i];计算当前元素需要的“另一半”。auto it hash.find(need);在哈希表里查找这个目标值。find返回迭代器找不到时等于hash.end()。if (it ! hash.end()) { return {it-second, i}; }找到了it-first是目标值it-second是目标值出现时的下标直接和当前下标构成答案。hash.emplace(nums[i], i);没找到就把当前值登记进去供后续元素查询。这里有个初学者高频问题为什么用emplace而不是insert或operator[]operator[]在键不存在时会插入“值初始化的元素”。一旦你用hash[nums[i]] i键不存在时先构造一个默认值再赋值多一次构造如果代码逻辑不当还可能意外插入脏数据。insert和emplace的区别在构造方式。insert通常先构造一个pair再拷贝进入容器emplace(nums[i], i)直接利用参数在节点内存里就地构造。对这道题的int类型来说性能差异几乎为零但养成用emplace的习惯是好的以后遇到vectorpair、mapstring, vectorint这类场景会明显受益。2.3 三个隐藏细节刷题面试都容易栽细节一为什么必须先查再插不能先插再查。如果先把当前元素插入哈希表再查找need当target 2 * nums[i]时会产生错误答案。举个例子nums [3, 2, 4], target 6。i 0时nums[0] 3先插入{3, 0}再查need 6 - 3 3find会命中自己返回[0, 0]。但题目明确要求同一个元素不能重复使用。所以循环里的顺序必须是先找找不到再插入。细节二重复元素怎么处理。看nums [3, 3], target 6。i 0时哈希表为空查不到need 3插入{3, 0}。i 1时能查到键3命中并返回{0, 1}正确。这里有一个值得注意的点我在第2个3到达时已经return了所以哈希表不需要处理“键已存在时的更新策略”。但如果哪天你遇到变体题要求返回所有可能答案就得想清楚哈希表里存的到底是“最左边的下标”还是“最新的下标”这会导致完全不同的结果。细节三为什么用 unordered_map 而不是 map。面试里追问这个问题的概率很高。map底层是红黑树查找复杂度 O(log n)元素按键有序unordered_map底层是哈希表查找平均 O(1)元素无序。在只有一次查找时二者差距不明显但题目在循环里反复find、数据规模一大差距就会放大。力扣刷题默认优先unordered_map除非题目明确要求按键有序输出才转用map。表格对比容器底层结构查找复杂度是否有序典型场景unordered_map哈希表平均 O(1)无序快速查找、计数、存下标map红黑树O(log n)按键有序需要按 key 遍历、取最大最小3. 从代码到运行VS Code配置C与本地调试全记录3.1 环境准备很多人的第一个坑不是算法是编译器力扣题目的函数签名已经预设好理论上你在网页编辑器里也能写完提交。但当你需要调试、打印中间结果、测试多组用例时本地环境几乎是必需的。我做过一段时间之后发现本地能跑通再提交成功率远高于直接网页硬写。我常用的本地组合是 VS Code MinGW-w64 C/C 扩展。安装步骤不多但容易错的位置却不少安装 MinGW-w64。注意选 x86_64 架构、posix 线程模型Windows 下兼容性更好。安装后把bin目录比如C:\mingw64\bin加到系统 PATH 环境变量。安装 VS Code 的 C/C 扩展ms-vscode.cpptools。这个扩展提供语法高亮、代码补全、IntelliSense 和调试能力。在项目根目录建.vscode文件夹写tasks.json编译任务和launch.json调试配置。很多新人卡在第3步。还有一部分人运行程序时弹窗报“VCRUNTIME140.dll 找不到”这是系统缺少 Visual C Redistributable 运行库。这个运行库属于微软官方的系统组件装了 Visual Studio 或多数 IDE 后会自动带上如果你只用 g 编译跑程序在干净机器上经常要单独装一次否则程序运行到一半崩溃跟代码一点关系都没有。3.2 一套直接可抄的配置tasks.json与launch.json先放tasks.json{ version: 2.0.0, tasks: [ { type: cppbuild, label: C: g.exe 生成活动文件, command: C:/mingw64/bin/g.exe, args: [ -fdiagnostics-coloralways, -g, ${file}, -o, ${fileDirname}\\${fileBasenameNoExtension}.exe ], options: { cwd: ${fileDirname} }, problemMatcher: [$gcc], group: { kind: build, isDefault: true } } ] }再放launch.json{ version: 0.2.0, configurations: [ { name: C 调试, type: cppdbg, request: launch, program: ${fileDirname}\\${fileBasenameNoExtension}.exe, args: [], stopAtEntry: false, cwd: ${fileDirname}, environment: [], externalConsole: false, MIMode: gdb, miDebuggerPath: C:/mingw64/bin/gdb.exe, preLaunchTask: C: g.exe 生成活动文件, setupCommands: [ { description: 为 gdb 启用整齐打印, text: -enable-pretty-printing, ignoreFailures: true } ] } ] }三个最容易出错的地方command和miDebuggerPath里的路径必须指向你自己的 MinGW 安装位置。只加 PATH 不写全路径配置会失效。preLaunchTask的值必须和tasks.json里的label完全一致大小写、空格、冒号都不能差。我第一次配的时候就是差了个冒号折腾了半小时。建议在args里加-stdc17这样unordered_map、auto等语法的行为和现代标准一致测试代码里也敢直接用结构化绑定这些新语法。3.3 踩坑实录本地能跑但提交不过的几种情况按出现频率排序我见过最多的报错是这几种g 不是内部或外部命令。PATH 没加对或者加了但没重启 VS Code / 终端。Windows 修改环境变量后已经打开的终端不会自动刷新这个最隐形。能编译但函数跳转不了。在 VS Code 里 ctrl点击跳不到unordered_map的定义是因为 IntelliSense 没有找到标准库头文件路径。用 C/C 扩展命令“C/C: 编辑配置(JSON)”打开c_cpp_properties.json把includePath加上 MinGW 的include目录比如C:/mingw64/include。更多时候只要把编译器路径切换到 gcc 就能解决。64 位代码里用fopen报安全错误提示考虑用fopen_s。这是 Windows 上 MSVC 风格的警告走 MinGW 一般不太会遇到。万一碰到在编译器参数里加-D_CRT_SECURE_NO_WARNINGS或源码顶部写宏定义即可。运行时报“缺少 VCRUNTIME140.dll”。这是 VC 运行库缺失装对应版本 Redistributable跟你的代码没有关系。3.4 用完整main做本地回归测试力扣的输入是数组和 target输出是下标。本地写一个带main的测试代码非常实用#include iostream #include vector #include unordered_map using namespace std; class Solution { public: vectorint twoSum(vectorint nums, int target) { unordered_mapint, int hash; for (int i 0; i nums.size(); i) { int need target - nums[i]; auto it hash.find(need); if (it ! hash.end()) { return {it-second, i}; } hash.emplace(nums[i], i); } return {}; } }; int main() { Solution s; vectorint nums {3, 2, 4}; int target 6; vectorint ans s.twoSum(nums, target); for (int x : ans) { cout x ; } cout endl; return 0; }跑完会输出1 2。建议把示例1、2、3和几个边界场景空数组、只有一对有效答案、重复元素都塞进main里循环测一遍。这里有个小提醒提交到力扣时只粘贴class Solution那一部分不要把#include、main带上去。有些新手把 main 也贴进提交框结果编译器报一堆奇怪错误。4. 刷完第1题之后这道题的延伸空间与C STL进阶清单4.1 从两数之和到三数之和为什么突然要换武器两数之和的哈希表解法很好但到了“三数之和”力扣15题再用同样的思路就会很痛苦因为去重是最大的敌人。三个数组合起来会产生大量重复三元组用哈希表返回下标、再去重逻辑很容易写乱。这时候排序 双指针才是更优解先排序固定一个数i用左右双指针在剩余区间里找两个数使它们的和等于-nums[i]。这个转变很有价值它说明一个道理哈希表不是万能的。两数之和适合哈希是因为题目只要求返回一个答案、且返回下标三数之和要求所有不重复的三元组双指针的枚举方式天然支持去重。所以刷题不能背模板要理解每种数据结构适合什么输出形式。4.2 前缀和换个包装的“两数之和”推荐你在刷完第1题后立刻做一道变体题“和为 k 的连续子数组”力扣560。它看起来和两数之和完全不一样但底层思路一模一样。先算前缀和pre[i] sum(nums[0..i-1])那么子数组nums[j..i]的和等于pre[i1] - pre[j]。要快速判断某个pre[j]是否等于pre[i1] - k就会用到unordered_map记录前缀和出现的次数。这就是第1题“查另一半”的逻辑在区间和问题里的复刻。我后来总结出一个关键认知哈希表的应用场景不是“两个数相加”而是把过去计算过的信息存起来供当前查询。一旦想通这点后面看单调栈、前缀和、快速幂这些题目本质上都是在“缓存历史结果”。4.3 C刷题常用STL容器快速盘点从第1题开始C刷题路上使用频率最高的就是 STL。我整理成了一张清单容器底层结构核心用途典型题vector动态数组顺序遍历、随机访问、返回结果几乎所有题目unordered_map哈希表查找、计数、存下标两数之和、字母异位词分组map红黑树有序键值对需要按 key 遍历的场景set / unordered_set平衡树 / 哈希表去重、判断存在性最长连续序列stack栈后进先出、最近配对有效括号、单调栈queue队列先进先出、逐层扩展广搜模板priority_queue堆动态取最大/最小前 K 个高频元素每类容器都有对应的高频题。第1题用到了unordered_map之后建议按这条线刷有效的括号stack、三数之和双指针 排序、和为 k 的子数组前缀和 unordered_map、滑动窗口最大值priority_queue 或单调队列、岛屿数量queue 广搜模板。4.4 刷题节奏建议第一题不是终点而是起点我的建议是第1题完成后先把“哈希表四件套”刷完两数之和、四数相加II、最长连续序列、和为 k 的子数组。这四题能把unordered_map的三种用法摸透存下标、计次数、判存在。之后再碰链表结构体链表的基本语法是很多人的第一个坎、二叉树递归 层次遍历、动态规划路线会顺畅很多。如果担心光看题解不会写本地代码就从这道题开始每道题都用同一套 VS Code 配置标准库头文件、Solution 类、main 里放多组测试用例。这样每次刷题都在同时练 C 基本功不只是练算法。最后说点个人体会。第一次做这题时我花了三分钟写暴力解法然后用了半小时看题解才弄明白哈希表那几行为什么能快这么多。后来又过了几周才真正理解“先查再插”为什么能避开同一个元素重复使用。力扣第1题不难但它是我把 C 从“写业务”切换到“写算法”的转折点。现在每次带新人刷题我都会让他们先在本地把main跑起来、多打印几次中间状态再提交到网页。环境顺手了思路顺畅了后面那几百题的路才会越走越宽。