ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

剑指Offer C++刷题指南:从环境配置到手写代码避坑

剑指Offer C++刷题指南:从环境配置到手写代码避坑 简介这是一份面向求职者和C开发者的《剑指Offer》编程题源码合集配套博文整理。压缩包共2098个文件、约44.2MB核心为242个cpp源文件与226个h头文件另有vcxproj/sln等工程文件便于直接打开调试以及pdb、exe等编译产出和辅助说明文档可对照工程结构按专题学习。内容覆盖数组、链表、树、栈与队列、动态规划、递归、字符串匹配、排序搜索、面向对象、模板、内存管理、异常处理等经典面试知识点能够帮助读者理解题目背后的解题思路并用C落地实现。已有373人学习适合准备技术面试、想要提升算法和C编码能力的程序员系统研读。1. 剑指offer源代码C别把“看懂了”当成“写得出来”面试官在共享文档里贴出一个函数签名让你手写“反转链表”。你脑子里明明有递归解法手指却先卡在struct ListNode的定义上写到中间忘记把next指针先存下来整个链表当场断成两截。这是很多刷《剑指offer》的人最熟悉的一幕——题解看得懂C 代码一合上书就写不顺。“剑指offer源代码C”这个方向本质不是去找一份现成代码背下来而是解决一件事把这两百来道题变成你自己的手写肌肉记忆。这篇文章会从环境搭建、代码模板、调试方法到高频翻车点讲一套照着做就能跑通的做法。适合两类人准备算法面试但 C 不熟的在校生以及转 C 后需要补数据结构手写能力的在职开发。2. 为什么用C刷剑指offer先用vscode把工程骨架立起来2.1 C在面试手写环节的真实优势面试手写算法题选 C 不是因为它难而是因为它把内存和类型都暴露在你面前写错了能当场暴露面官也更买账。剑指offer的题解在 GitHub 上有 C、Java、Python 各种版本但用 C 刷有一个别的语言给不了的好处STL 的vector、unordered_map、stack这些容器能让你把精力放在算法逻辑上不用像写 C 那样手动维护数组长度和扩容逻辑。我见过不少从 C 转 C 刷题的人一开始总忍不住写int a[100]这种定长数组其实剑指offer里绝大多数数组题用vector就能覆盖还自带越界检查debug 模式下。另一个优势是引用传参。链表题里经常要“修改头节点”用 C 写就得传二级指针ListNode**心智负担很重C 里直接ListNode*就解决了函数内对头指针的修改能直接带出来。这个差异在写树的递归时尤其明显——递归函数要往vector里累加结果传引用比返回新数组高效得多代码也干净。后面第 5 章我会专门讲“指针值传递”这个坑很多人翻车就翻在这里。有人会问那我直接用 Python 刷不更省事吗省事是真但面试环境经常指定语言而且 C 的手写代码更能体现对内存布局的理解。剑指offer很多题的标准解就是 C 写的面试官看你的代码如果有nullptr、const、引用这些现代 C 习惯印象分会好不少。我给你的建议是主刷语言定为 C但每个题的思路用一句话写清楚这样面官追问时你脑子里有两条线可以切换。2.2 用vscode配置一个能一键运行题解的C环境刷题场景不需要搞完整 CMake 工程一个能编译、能运行、能断点调试的最小环境就够了。我一般会建这样一个目录结构offer/ ├── build.sh # 一键编译并运行指定源文件 ├── include/ │ └── common.h # 公共头文件放 ListNode/TreeNode 定义 └── solutions/ ├── 03_reverse_list.cpp ├── 04_rebuild_binary_tree.cpp └── ...build.sh是核心它长这样#!/bin/bash # 用法: ./build.sh 03_reverse_list # 不要带 .cpp 后缀 # -stdc17 启用现代C语法 # -g 保留调试信息给 gdb 或 vscode 调试用 # -Wall 打开常见警告防止 signed/unsigned 比较这类小毛病 g -stdc17 -g -Wall $1.cpp -o $1 ./$1逻辑说明表示编译成功才运行这样你不会盯着一个编译报错还在找运行结果。-g是给调试器用的如果你在 vscode 里按 F5 启动调试没有这个参数就没法断点。-Wall不是把所有警告都打开只是打开一组最常见的刷题阶段建议一直开着能让很多潜在 bug 在运行前暴露。参数调整如果你的编译器版本较老不支持 C17可以把-stdc17改成-stdc11剑指offer的题解用到的新特性基本到 C11 就够。Windows 用户如果没有 git bash把这个脚本改造成build.bat一样用echo off g -stdc17 -g -Wall %1.cpp -o %1.exe %1.exevscode 配置 c/c 环境时有几个关键点安装 C/C 扩展后按CtrlShiftP搜索“C/C: Edit Configurations (UI)”在“编译器路径”里填 MinGW 的g.exe绝对路径tasks.json里的command填gargs按上面 build.sh 的参数写。很多新手卡在 “g 不是内部或外部命令”那是因为 MinGW 的bin目录没加进系统 PATH不是 vscode 的问题。我把ListNode和TreeNode的定义放在include/common.h里这样每道题就不用重复写结构体了// common.h所有链表/二叉树题共用的节点定义 #pragma once // 防止同一个头文件被 include 多次 struct ListNode { int val; ListNode* next; ListNode(int x) : val(x), next(nullptr) {} }; struct TreeNode { int val; TreeNode* left; TreeNode* right; TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} };逻辑说明#pragma once是头文件保护的一种写法比#ifndef宏更省事主流编译器都支持。构造函数里初始化列表写val(x), next(nullptr)是 C 的推荐习惯不写的话next是野指针链表遍历会访问到随机地址然后崩溃——这个坑我见过太多次全是血泪经验。3. 把高频考点拆成C代码模板链表、二叉树、数组字符串各一套3.1 链表题反转链表的三指针模板剑指offer里的链表题不管题目怎么包装判断环、找倒数第 k 个节点、合并两个有序链表核心都是指针操作的稳健性。反转链表是最基础的也是我面试时必考的手写题。理解它等于理解所有链表指针操作的套路// 反转单链表三指针迭代法 // 输入: 1-2-3-4-5 // 输出: 5-4-3-2-1 ListNode* reverseList(ListNode* head) { ListNode* prev nullptr; // 前一个节点初始为 null对应新链表的尾 ListNode* cur head; // 当前正在处理的节点 while (cur ! nullptr) { ListNode* nextNode cur-next; // 先存住下一个节点不然指针一改就找不到了 cur-next prev; // 掉转方向当前节点指向前一个 prev cur; // prev 前移 cur nextNode; // cur 前移 } return prev; // 原链表的尾节点就是新链表的头 }逻辑说明nextNode必须在cur-next被修改之前保存。很多人第一次写会漏掉这一行结果cur前进时已经找不到next链表后半截直接丢在黑匣子里。整个过程是“先保存、再断链、双指针前移”三步循环顺序不能乱。参数说明head是原链表头指针返回的是新链表头也就是原链表的尾节点。如果head是空指针循环不执行直接返回nullptr这个空指针行为是面试官最喜欢追问的考点之一。还有递归写法base case 是head nullptr || head-next nullptr递归体是“先反转后面再让 head-next-next 指向 head最后断掉 head-next”但那属于进阶迭代先写稳。3.2 二叉树题递归遍历与重建树的统一框架二叉树题在剑指offer里占比很高而且几乎都建立在三种遍历之上。前序、中序、后序的递归模板是同一套架子只换三行代码的位置// 前序遍历模板根 - 左 - 右 // 中序和后序只需要调整下面三行的顺序 void preorder(TreeNode* root, vectorint result) { if (root nullptr) return; // 空节点是递归出口 result.push_back(root-val); // 第1行访问根 preorder(root-left, result); // 第2行左子树 preorder(root-right, result); // 第3行右子树 }逻辑说明result用引用传递是为了避免每层递归都拷贝整个 vector。如果把vectorint result按值传进去每次递归都会复制复杂度直接退化成 O(n²)而且外层根本拿不到结果。参数说明root是当前子树的根节点result是输出参数调用前先vectorint res; preorder(root, res);。剑指offer里“重建二叉树”这道题给前序和中序让你还原整棵树。核心思路是前序第一个元素一定是根在中序里找到根的位置左边是左子树、右边是右子树然后递归建左右子树。实现时用哈希表把中序的“值到下标”先存起来避免每次递归都 O(n) 线性查找。我一般会写一个辅助函数接收(preorder, preL, preR, inL)而不是每次都创建新的子数组这样内存开销小很多。TreeNode*的空指针判断必须是递归函数的第一行放在push_back之前。很多新手把if (root nullptr)写在访问之后等 root 为空还去访问root-left就是野指针访问程序直接崩。还有一个常见误用递归里写了if (root-left ! nullptr) preorder(root-left, result)其实完全没必要——递归函数自己会处理空节点外层判断是多余的代码还变丑。3.3 数组字符串题双指针、滑动窗口与字符串切分的STL写法数组和字符串是剑指offer里最杂的一类从二分查找到滑动窗口都有。我挑一个最高频的场景讲找一个数组里连续子序列满足某个条件这类题用双指针维护一个“窗口”时间复杂度 O(n)空间 O(1)。比如“找出和为 target 的连续正整数序列”// 找出所有和为 target 的连续正整数序列至少两个数 // 用滑动窗口窗口和小于 target 右边界右移大于 target 左边界右移 vectorvectorint findContinuousSeq(int target) { vectorvectorint res; int left 1, right 1; // 左闭右开区间 [left, right) int sum 0; while (left target / 2) { // 序列至少两个数左边界不用超过 target/2 if (sum target) { sum right; // 窗口扩张 right; } else if (sum target) { sum - left; // 窗口收缩 left; } else { vectorint oneSeq; for (int i left; i right; i) { oneSeq.push_back(i); } res.push_back(oneSeq); sum - left; // 找到一个答案后左边界继续右移找下一个 left; } } return res; }逻辑说明[left, right)左闭右开是关键约定初始left1, right1, sum0。sum target时扩张窗口sum target时收缩窗口相等时记录结果然后左移继续找。注意记录完一个结果后不要停把left从窗口里拿走、右移才能继续搜下一个。参数说明target是目标和返回值是vectorvectorint每个元素是一个连续序列。字符串类的题C 的 STL 有几个趁手工具istringstream可以按空格切分字符串getline可以按自定义分隔符切分。比如实现字符串转整数面试官常考手写 atoi要点是跳过前导空格、处理正负号、用long long累加避免 int 溢出、最后判断是否超出INT_MIN/INT_MAX范围。这里要特别注意C 的int溢出是未定义行为不是“自动截断”编译器优化后可能给你完全不可预测的结果所以中间计算一律用long long。4. 让题解可复现给每道题配一个测试骨架和可视化打印4.1 用assert给每题搭一个最小回归测试很多人刷题只写一个函数在 main 里随便构造一个输入打印一眼觉得“对”就过了。这种做法的问题很明显你验证的只是那个特定 case边界根本没测到。我给自己定的规矩是每道题写完main 函数里至少三个测试用例——普通场景、空输入、单元素边界。用assert而不是打印来验证跑完没有输出就代表全过。// 03_reverse_list.cpp 的 main 函数 // 三个用例普通链表、空链表、单节点链表 #include common.h #include cassert ListNode* reverseList(ListNode* head); // 前面实现的题解 int main() { // case 1普通链表 1-2-3-4-5 ListNode n1(1), n2(2), n3(3); n1.next n2; n2.next n3; ListNode* head reverseList(n1); assert(head-val 3); // 新头是原来的尾 assert(head-next-val 2); // 下一个是原来的倒数第二个 assert(head-next-next-val 1); assert(head-next-next-next nullptr); // 原头变成了新尾 // case 2空链表必须返回 nullptr 而不是崩掉 assert(reverseList(nullptr) nullptr); // case 3单节点链表 7反转后还是自己 ListNode single(7); assert(reverseList(single) single); printf(all reverseList test cases passed\n); return 0; }逻辑说明assert在 debug 模式下有效条件为假就终止并输出失败的行号这是最快的定位方式。release模式下assert会被预处理器整个删掉所以不要依赖它做运行期逻辑判断。参数说明每个 case 前写一句注释是给面试官看的也是给自己复盘看的我很推荐保持这个习惯。测试骨架配合第 2 章的build.sh改完代码直接./build.sh 03_reverse_list几秒钟内知道有没有改坏。这比你在网上找在线刷题网站贴代码再点提交要快得多因为本地能断点、能加打印在线编辑器很多时候只给你一个输出框。4.2 链表和二叉树怎么打印两种可视化调试土办法链表和二叉树的结构在调试器里看很费劲尤其树的层数一深监视窗口里全是节点地址。我一般给链表和树各写一个打印函数出问题时直接打印整条链或整棵树几秒钟就能看出结构对不对// 打印链表1 - 2 - 3 - nullptr void printList(ListNode* head) { while (head ! nullptr) { std::cout head-val - ; head head-next; } std::cout nullptr std::endl; } // 层序打印二叉树空节点打印 #一眼看出左右子树挂没挂对 void printTree(TreeNode* root) { if (root nullptr) return; std::queueTreeNode* q; q.push(root); while (!q.empty()) { TreeNode* node q.front(); q.pop(); if (node ! nullptr) { std::cout node-val ; q.push(node-left); // 左右孩子都入队空节点也入队 q.push(node-right); } else { std::cout # ; } } std::cout std::endl; }逻辑说明打印二叉树的这个写法用了 BFS 层序遍历空节点也打印#这样你能直观看到“挂到左还是挂到右”。调试“重建二叉树”这类题时非常有用重建完打一层和前序遍历的结果对一下立刻知道哪一步递归错了。参数说明printTree用queueTreeNode*每层节点出队时把左右孩子入队即使孩子是空指针也入队这样#才能打出来。这两个函数放在common.h里所有题都能用。我的习惯是函数写完、用例跑通之后把打印函数删掉保持题解代码干净因为面试时你不可能递一份带一堆调试输出的代码上去。5. 剑指offer C刷题避坑五个高频翻车点5.1 空指针与空数组函数签名里的隐藏炸弹现象链表反转函数跑普通用例全对一传nullptr就段错误或者返回了一个垃圾指针。原因很多题解代码只在循环里判断cur ! nullptr但函数入口没有处理head nullptr的情况。还有一种更隐蔽的题目说“输入可能为空数组”但 C 里vector为空时v.size()返回 0v[0]是未定义行为不是返回 0。解决函数第一行永远做空判断。链表的返回统一nullptr数组的返回统一空vector。我的经验是写题解前先在注释里列出“输入为空”的分支再写主逻辑这样面官追问边界时你也能背出来。5.2 STL迭代器失效和vector元素引用悬垂现象vectorint里存着某个元素的引用继续push_back之后引用指向的地址变成乱值。原因vector扩容时会重新分配一整块连续内存把旧数据拷贝过去旧地址就失效了。如果你提前取了int ref vec[0]push_back触发扩容后ref就是悬垂引用读出来的是野值。解决刷题阶段遵守一条纪律——不要长期持有vector内部元素的外部引用。需要用索引就存size_t下标等push_back全部结束后再通过下标访问。剑指offer里很多题用哈希表unordered_mapint, int存“值到下标”这里同样只存下标不存迭代器因为哈希表在 rehash 时也会让迭代器失效。5.3 整数溢出和字符串边界测试用例测不出来的问题现象手写 atoi、快速幂、求数值的整数次方这类题自测时传小数值全过一测2147483647或-2147483648就翻车。原因int类型在 32 位系统下范围是[-2147483648, 2147483647]求绝对值时-2147483648的正数形式2147483648超出了范围产生未定义行为。快速幂里做乘法mid * mid在mid 46340时直接溢出。这属于 C 最典型的隐蔽坑编译器不会报错运行结果还是错的。解决所有中间计算一律用long long。比如快速幂// 计算 a 的 n 次幂对 1000000007 取模 // 用快速幂把 O(n) 降到 O(log n) long long quickPow(long long a, long long n, long long mod) { long long res 1; while (n 0) { if (n 1) res res * a % mod; // n 的当前位是 1 才乘 a a * a % mod; // a 每次平方 n 1; // n 右移一位 } return res; }逻辑说明快速幂的核心是“把指数拆成二进制”a每次自乘得到a^2, a^4, a^8...n的当前最低位是 1 时才把结果乘进去。参数说明a和mod都声明成long long就是为了防止a*a溢出 int 范围函数返回long long而不是int最后需要转int时再做一次范围判断。5.4 Visual C的fopen安全报错和CRT的历史包袱现象在 Windows 上用 Visual Studio 写剑指offerfopen一写就报C4996: fopen was declared deprecated旁边还跟一句说建议用fopen_s。原因Visual C 从 VS2005 开始把fopen、strcpy这一类不做缓冲区检查的 CRT 函数标记为“不安全”。这不是说你的代码真有漏洞而是编译器的安全策略更激进。用 MinGW 的 g 编译时没有这套机制所以同样的代码在 vscode 里跑得很高兴一拿到 VS 里就报错。解决两个选择。一是改用fopen_s注意它的参数顺序和fopen不同第一个参数是文件指针的地址第二个才是文件名。二是简单粗暴在源文件第一行加#define _CRT_SECURE_NO_WARNINGS或者在 VS 项目属性里“预处理器定义”中加上它告诉编译器“我确认这个 API 是安全的”。刷题阶段我推荐第二种节省时间不要把注意力耗在和编译器搏斗上。5.5 按值传参与指针的指针函数形参的三种写法现象写了一个void reverse(ListNode* head)函数里执行了head newHead回到 main 里一打印head 还是原来的地址整个反转白做了。原因C 的指针参数也是值传递。形参ListNode* head是实参地址的一份拷贝你在函数里修改head本身只改了这份拷贝实参不受影响。这个和 Java 的行为完全不一样是 C/C 新手最容易懵的地方。解决如果需要修改头指针本身有两条路函数返回新头指针推荐清晰或者形参写成ListNode* head引用传参这样函数内对head的赋值会同步到实参。// 正确写法1返回新头指针 ListNode* reverseList(ListNode* head); // 正确写法2引用传参直接改掉外部指针 void reverseList(ListNode* head) { ListNode* prev nullptr; ListNode* cur head; while (cur ! nullptr) { ListNode* nextNode cur-next; cur-next prev; prev cur; cur nextNode; } head prev; // 直接修改实参 }逻辑说明引用传参相当于给实参起了个别名函数内head prev就是把外部那个指针变量改掉了。两种写法面试都认可但注意别混用——一会儿返回新头、一会儿引用传参代码读起来会晕。我个人的习惯是链表翻转、合并这类“产出新链表头”的操作全部用返回值这样调用方的head reverseList(head)一眼就能看懂。6. 把刷题成果变成面试手写能力三个容易忽略的习惯面试现场写代码和在家里对着屏幕慢慢调是两回事。限时、有人盯着、还要边写边讲这时候决定成败的不是你会不会这道题而是你平常有没有固定一套“写完立刻自查”的流程。我自己遇到过最尴尬的一次题解思路完全正确但忘了检查链表为空的分支面官给了我一个空输入代码当场段错误。从那以后我每道题合上编辑器之前必须做三件事。第一件人肉过一遍测试用例。不管题目本身有没有给示例自己列五个空输入、单元素、全部相同、升序、降序。链表题再加一个“头节点就是目标”的场景二叉树题加一个“只有左子树”的场景。这不需要花很多时间但是能挡住大多数边界 bug。你甚至可以把这个习惯变成肌肉记忆写完函数先看有没有nullptr判断再看循环退出条件最后看返回值的类型对不对。第二件想想题解能不能用 STL 简化。剑指offer有不少题标准答案用双指针或哈希表但你用手写的循环很容易出偏移错误。比如判断括号匹配直接用stackchar找重复数字先想能不能用unordered_set。STL 不是笨重的黑匣子是你手写代码的“后悔药”。当然面试官有时候会问“你能不能用 O(1) 空间实现”这时候你就得把手写的双指针方案拿出来两种都准备着。第三件注释只写“为什么”不写“是什么”。注释是给面试官讲思路时的提词板也是你自己复盘时的线索。比如// 这里必须先保存 next不然改完指针就丢了下个节点比// cur 指向当前节点有用得多。我见过很多人的代码注释写得比代码还长全是“给 i 加 1”这种废话面官不会觉得你认真只会觉得你冗余。这三件事加起来每道题多花五分钟但坚持十道题之后你会明显感觉到手写速度和准确率一起上去了。剑指offer源代码C这件事价值不在于你收集了多少份题解源码而在于你把每道题都亲手拆开、装上、跑通、测过。希望这些方法能让你刷题少走点弯路面试时多一分底气——希望帮到你。本文还有配套的精品资源点击获取
返回列表