尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C++算法精进指南:从数据结构到动态规划的LeetCode高效刷题路线

C++算法精进指南:从数据结构到动态规划的LeetCode高效刷题路线 1. 项目概述一份为C选手量身定制的算法精进地图如果你是一名正在用C刷LeetCode的开发者无论是为了面试冲刺还是为了系统性提升算法能力你大概率都经历过这样的迷茫题库里两千多道题从何刷起是跟着官方列表顺序还是看哪个“热题100”榜单刷完一道题除了“AC”的短暂快感似乎并没有形成深刻的理解和体系化的记忆。更让人头疼的是很多题解虽然提供了答案但背后的解题思想、代码优化技巧、以及如何将这道题的经验迁移到其他问题上往往语焉不详。这份笔记正是为了解决这些问题而生。它不是简单的题目答案合集而是一份以C为核心实现语言以构建完整算法知识体系为目标经过精心排序和深度解析的刷题路线图。其核心价值在于“顺序”和“详解”。顺序决定了你学习路径的效率避免在知识孤岛间跳跃详解则确保你每刷一题都能吃透其背后的思想、写出一段高效优雅的C代码并建立起与其他题目的联系。我会持续更新这份笔记力求覆盖核心算法与数据结构让你用最少的时间获得最扎实的成长。2. 刷题顺序设计的核心逻辑与路线图盲目刷题是效率最低的学习方式。一个科学的顺序应该符合认知规律即由浅入深、由点及面、前后关联。我设计的这个顺序主要基于以下几个原则2.1 原则一数据结构先行算法随后这是构建大厦的基石。你必须先熟悉“砖瓦”数据结构的特性才能学会如何用它们“盖房子”设计算法。因此路线会从最基础的数组、字符串、链表开始逐步过渡到栈、队列、哈希表再到复杂的树、图最后是高级数据结构如堆、并查集、前缀树等。在每个数据结构模块内再融入相关的算法思想。2.2 原则二同类型题目集中突破这是形成肌肉记忆和思维模式的关键。将相同解法或相同数据结构的题目放在一起连续练习能让你快速掌握这类问题的“套路”。例如在“链表”模块我会把涉及虚拟头节点、快慢指针、反转链表、合并链表的题目集中讲解让你一次吃透。2.3 原则三难度螺旋式上升在每个小模块内题目难度会从Easy到Medium偶尔穿插Hard。这保证了学习的平滑性。你不会在还没掌握基础遍历时就去挑战复杂的树形DP。整个大路线也是从基础数据结构到基础算法排序、二分、双指针再到高级算法回溯、动规、贪心、图论。2.4 原则四强调前后关联与知识迁移在讲解一道题时我会明确指出它和之前哪道题的思想一脉相承或者它能为后面哪类难题打下基础。例如学会了“两数之和”哈希表那么“三数之和”排序双指针的解法虽然不同但你可以对比思考为何此处不用哈希表从而加深对算法适用场景的理解。注意这份顺序并非LeetCode题号的顺序也不同于任何单一的“热题”列表。它是基于我个人和众多上岸者的经验重新组织的一个学习路径。你可以把它看作一门精心编排的“算法课程”大纲。基于以上原则我规划的初始核心路线图如下第一阶段编程基础与线性结构目标熟悉C STL基础容器操作掌握数组、字符串、链表的常见处理方法。核心题目类型数组基本操作、字符串处理、链表增删改查、双指针技巧快慢指针、左右指针。第二阶段基础数据结构与简单算法目标掌握栈、队列、哈希表的应用理解递归入门二叉树。核心题目类型栈实现表达式求值/括号匹配、队列应用、哈希表解决查找问题、二叉树遍历递归/迭代。第三阶段中级算法思想目标攻克排序、二分查找、滑动窗口、回溯算法、基础动态规划。核心题目类型各种排序算法的应用场景、二分查找的变体、滑动窗口解决子串/子数组问题、排列组合类回溯、经典一维/二维DP问题。第四阶段高级数据结构与复杂算法目标掌握堆、并查集、图论算法、复杂动态规划与贪心策略。核心题目类型堆解决TopK问题、并查集处理连通性、图的DFS/BFS及最短路径、背包问题、区间DP、贪心选择证明。这个路线是动态的我会在每个阶段的详解中插入必须掌握的经典题目和具有代表性的新题。3. C刷题详解的核心方法论不止于AC刷题的目标不是提交通过而是“掌握”。对于每一道入选的题目我的详解笔记会包含以下几个层次这也是你自查是否真正掌握一道题的标准3.1 题意理解与边界条件分析这是所有步骤的基础却最容易被忽视。我会带你仔细审题识别出所有可能的边界情况空输入、单个元素、极大/极小值、负数等并在思路分析阶段就考虑进去。例如链表题目常需考虑头节点被修改或删除的情况这通常引入“虚拟头节点”技巧。3.2 多解法对比与时空复杂度分析一道题往往有多种解法。我会从最直观的暴力法开始分析其缺点然后逐步优化引出更高效的算法。对于每一种解法都会明确给出时间复杂度和空间复杂度并解释为什么。这能训练你评估算法优劣的能力。示例对于“两数之和”我们会对比暴力O(n²)和哈希表O(n)解法并讨论为何哈希表在此处更优频繁查找。3.3 C实现细节与STL技巧这是本笔记的特色所在。我会提供可直接运行的C代码并重点讲解其中的关键点容器选择为什么用vector而不是deque用unordered_map还是map迭代器与索引在遍历时何种情况下用索引访问更安全清晰何种情况下用迭代器或范围for循环更现代函数参数传递何时用值传递、引用传递、常量引用const 这直接影响效率。内存与拷贝注意不必要的临时对象拷贝特别是在递归或循环中。STL算法应用巧妙使用sort,lower_bound,next_permutation等算法能极大简化代码。3.4 代码注释与可读性提供的代码将包含关键步骤的注释说明“为什么这么做”。良好的变量命名和代码结构本身就是面试的加分项。3.5 关联题目与举一反三在题目最后我会列出与之强相关的题目编号并简要说明关联点。鼓励你立即去尝试巩固刚学到的模式。4. 第一阶段详解数组、字符串与链表实战入门让我们正式进入第一阶段的实战。这是培养代码感觉和掌握基础操作的关键时期。4.1 数组篇从简单操作到双指针思想数组是连续的内存空间支持随机访问。LeetCode上很多题目本质是数组操作。经典入门27. 移除元素题意原地移除数组中所有值等于val的元素返回新数组长度。核心解法快慢指针双指针。这是必须掌握的经典范式。C详解class Solution { public: int removeElement(vectorint nums, int val) { int slowIndex 0; // 慢指针指向下一个待填充的位置即新数组的末尾 for (int fastIndex 0; fastIndex nums.size(); fastIndex) { // 快指针遍历原数组 if (nums[fastIndex] ! val) { // 当快指针找到不需要删除的元素时 nums[slowIndex] nums[fastIndex]; // 将其赋值给慢指针位置 slowIndex; // 慢指针向前移动新数组长度1 } // 如果等于val快指针继续走慢指针不动相当于“跳过”了这个元素 } return slowIndex; // 慢指针最终的位置就是新数组的长度 } };为什么是O(n)时间复杂度快指针遍历一次数组每个元素只被处理一次。关联题目26.删除有序数组中的重复项快慢指针变体283.移动零本质相同。双指针进阶977. 有序数组的平方题意非递减顺序排序的整数数组返回每个数字平方后按非递减顺序排序的新数组。核心解法数组本身有序但平方后最大值在两端。使用左右指针向中间遍历比较平方值从后向前填充新数组。C实现要点vectorint sortedSquares(vectorint nums) { int n nums.size(); vectorint result(n); // 预先分配好空间避免push_back int left 0, right n - 1, pos n - 1; // pos指向结果数组当前待填充的位置从后往前 while (left right) { // 注意等号要处理最后一个元素 int leftSquare nums[left] * nums[left]; int rightSquare nums[right] * nums[right]; if (leftSquare rightSquare) { result[pos--] leftSquare; left; } else { result[pos--] rightSquare; right--; } } return result; }心得对于需要反向填充或从两端向中间收敛的问题左右指针是利器。预先分配vector大小比动态push_back在性能上更优。4.2 字符串篇理解不可变性与常用操作在C中string是可变的这比某些语言更方便。重点掌握子串、翻转、匹配等操作。经典例题344. 反转字符串题意原地反转字符串必须使用O(1)额外空间。解法左右指针交换。C细节使用swap函数或直接使用异或操作进行交换。注意循环条件是left right。void reverseString(vectorchar s) { for (int i 0, j s.size() - 1; i j; i, j--) { swap(s[i], s[j]); // 标准库swap // 或者手动交换: char temp s[i]; s[i] s[j]; s[j] temp; } }核心挑战151. 翻转字符串里的单词题意翻转字符串中单词的顺序并去除多余空格。解题思路这是一道综合题。可以分为三步去除多余空格使用快慢指针原地去除首尾和中间多余空格类似数组移除元素。反转整个字符串。反转每个单词在反转后的字符串中找到每个单词的起止位置分别进行反转。C实现关键函数void removeExtraSpaces(string s) { int slow 0; // 慢指针 for (int fast 0; fast s.size(); fast) { if (s[fast] ! ) { // 遇到非空格就处理即删除所有空格 if (slow ! 0) s[slow] ; // 在单词前手动添加空格第一个单词除外 while (fast s.size() s[fast] ! ) { s[slow] s[fast]; // 拷贝整个单词 } } } s.resize(slow); // slow的大小即为去除多余空格后的大小 }关联题目剑指 Offer 58 - II. 左旋转字符串局部反转整体反转技巧。4.3 链表篇掌握指针操作与虚拟头节点链表题目是面试高频点核心是理解指针引用的指向关系。建议在纸上画图分析。基础操作203. 移除链表元素题意删除链表中所有满足node.val val的节点。难点头节点可能被删除。这是引入虚拟头节点dummy node的经典场景。C详解ListNode* removeElements(ListNode* head, int val) { ListNode* dummyHead new ListNode(0); // 创建一个虚拟头节点其next指向真实头节点 dummyHead-next head; ListNode* cur dummyHead; // 当前检查的节点从虚拟头开始 while (cur-next ! nullptr) { if (cur-next-val val) { // 找到需要删除的节点 ListNode* tmp cur-next; // 保存待删除节点 cur-next cur-next-next; // 跳过该节点 delete tmp; // C需要手动释放内存面试中需注意 } else { cur cur-next; // 否则当前节点向后移动 } } head dummyHead-next; // 新的头节点可能是原来的下一个节点 delete dummyHead; // 删除虚拟头节点 return head; }重要心得使用虚拟头节点可以统一删除逻辑无需单独处理头节点。在C中操作链表时一定要注意内存管理如果删除了节点要用delete释放除非题目说明不需要。快慢指针应用142. 环形链表 II题意判断链表是否有环并返回环的入口节点。Floyd判圈算法这是必须掌握的数学结论。设置快指针每次两步和慢指针每次一步。如果快指针遇到nullptr则无环。如果有环快慢指针必在环内某点相遇。此时将其中一个指针移回链表头然后两个指针都每次走一步再次相遇的点即为环的入口。C代码框架ListNode *detectCycle(ListNode *head) { ListNode* fast head; ListNode* slow head; while (fast ! nullptr fast-next ! nullptr) { slow slow-next; fast fast-next-next; if (slow fast) { // 相遇有环 ListNode* index1 head; ListNode* index2 fast; // 或slow while (index1 ! index2) { index1 index1-next; index2 index2-next; } return index1; // 环的入口 } } return nullptr; // 无环 }为什么可行这涉及到数学推导。设头到入口距离为a入口到相遇点距离为b相遇点再到入口距离为c。第一次相遇时慢指针走了ab快指针走了an(bc)b。由于快指针速度是慢指针两倍可得a (n-1)(bc)c。这个等式意味着从head走a步和从相遇点走c步再绕n-1圈会到达同一点入口。所以第二次相遇点就是入口。第一阶段的核心是建立对基础数据结构的熟练度并初步掌握双指针这一强大工具。务必做到每道题都能手写无误并理解其所有变种。5. 第二阶段详解哈希表、栈、队列与二叉树基础掌握了线性结构后我们进入更抽象的数据结构。它们能帮你解决更复杂的问题。5.1 哈希表以空间换时间的利器C中常用unordered_set集合和unordered_map映射。其查找、插入的平均时间复杂度为O(1)。经典入门1. 两数之和题意在数组中找出和为目标值的两个数返回其索引。暴力法两层循环O(n²)。哈希表优化在遍历数组时对于当前元素nums[i]我们检查target - nums[i]是否在之前遍历过的元素集合中。为了同时保存值和索引我们使用unordered_mapint, intkey是数值value是对应索引。C实现vectorint twoSum(vectorint nums, int target) { unordered_mapint, int hashmap; // value - index for (int i 0; i nums.size(); i) { auto it hashmap.find(target - nums[i]); if (it ! hashmap.end()) { return {it-second, i}; // 找到返回之前存的索引和当前索引 } hashmap[nums[i]] i; // 没找到将当前值存入哈希表 } return {}; // 题目保证有解这里为了完整性返回空 }思考为什么边遍历边存而不是先全部存入因为要避免同一个元素被使用两次。例如target6, nums[3]如果先全存进去就会找到自己。哈希集合应用202. 快乐数题意判断一个数是否是快乐数各位平方和最终变为1。关键如果不是快乐数平方和会进入一个循环。如何检测循环——哈希集合。C思路计算平方和如果等于1则返回true如果这个和已经在集合中出现过说明进入了循环返回false否则将和加入集合并继续。bool isHappy(int n) { unordered_setint seen; while (n ! 1 !seen.count(n)) { seen.insert(n); n getNext(n); // 计算下一个平方和 } return n 1; } int getNext(int n) { int sum 0; while (n 0) { int digit n % 10; sum digit * digit; n / 10; } return sum; }5.2 栈与队列理解后进先出与先进先出栈非常适合处理对称性、递归转迭代、路径回溯等问题。队列则用于BFS广度优先搜索。栈的经典应用20. 有效的括号题意判断一个只包含括号的字符串是否有效。解法遍历字符串遇到左括号就压栈遇到右括号就检查栈顶是否匹配的左括号匹配则弹出不匹配或栈空则无效。最后栈空才有效。C实现技巧使用unordered_map来映射右括号到左括号使代码更简洁。bool isValid(string s) { stackchar st; unordered_mapchar, char pairs {{), (}, {], [}, {}, {}}; for (char ch : s) { if (pairs.count(ch)) { // 当前字符是右括号 if (st.empty() || st.top() ! pairs[ch]) { return false; } st.pop(); // 匹配成功弹出栈顶左括号 } else { // 当前字符是左括号 st.push(ch); } } return st.empty(); // 最后栈必须为空 }队列与BFS入门102. 二叉树的层序遍历题意按层返回二叉树节点的值。BFS标准模板使用队列。将根节点入队然后循环队列不空时记录当前队列大小即本层节点数循环处理该大小的所有节点出队、记录值、将其左右子节点入队。C代码vectorvectorint levelOrder(TreeNode* root) { vectorvectorint result; if (!root) return result; queueTreeNode* q; q.push(root); while (!q.empty()) { int levelSize q.size(); // 关键记录当前层的节点数 vectorint level; for (int i 0; i levelSize; i) { TreeNode* node q.front(); q.pop(); level.push_back(node-val); if (node-left) q.push(node-left); if (node-right) q.push(node-right); } result.push_back(level); } return result; }心得levelSize的获取必须在for循环之外因为q.size()在循环中是变化的。这是BFS层序遍历的固定写法务必熟记。5.3 二叉树基础递归与迭代遍历二叉树是理解递归和后续复杂树形DP的基础。必须熟练掌握三种深度优先遍历前序、中序、后序的递归和迭代写法。递归遍历以前序为例void preorder(TreeNode* root, vectorint res) { if (!root) return; res.push_back(root-val); // 前序根左右 preorder(root-left, res); preorder(root-right, res); }递归非常直观但需要理解函数调用栈。面试时可能会要求写迭代法。迭代遍历使用栈模拟递归前序迭代由于访问顺序是“根左右”我们可以先将根节点压栈然后循环栈不空出栈访问然后先右后左压栈保证出栈时是左先于右。vectorint preorderTraversal(TreeNode* root) { vectorint result; if (!root) return result; stackTreeNode* st; st.push(root); while (!st.empty()) { TreeNode* node st.top(); st.pop(); result.push_back(node-val); if (node-right) st.push(node-right); // 右先入栈 if (node-left) st.push(node-left); // 左后入栈 } return result; }中序迭代中序是“左根右”需要借助指针来帮助访问。思路是指针指向当前节点只要节点不为空就压栈并向左走cur cur-left节点为空时弹出栈顶此时栈顶是最左侧的节点访问它然后指针指向其右子树。vectorint inorderTraversal(TreeNode* root) { vectorint result; stackTreeNode* st; TreeNode* cur root; while (cur ! nullptr || !st.empty()) { if (cur ! nullptr) { // 指针来访问节点访问到最底层 st.push(cur); // 将访问的节点放进栈 cur cur-left; // 左 } else { cur st.top(); st.pop(); // 从栈里弹出的数据就是要处理的数据 result.push_back(cur-val); // 中 cur cur-right; // 右 } } return result; }后序迭代后序是“左右根”可以看作是“根右左”的前序遍历的逆序。所以可以按照类似前序但“先左后右”的顺序遍历最后反转结果。vectorint postorderTraversal(TreeNode* root) { vectorint result; if (!root) return result; stackTreeNode* st; st.push(root); while (!st.empty()) { TreeNode* node st.top(); st.pop(); result.push_back(node-val); if (node-left) st.push(node-left); // 相对于前序这里顺序调换 if (node-right) st.push(node-right); } reverse(result.begin(), result.end()); // 将结果反转 return result; }掌握二叉树的遍历是解决所有树问题的基础。很多问题例如求深度、找路径、判断对称等都是遍历的变体。6. 第三阶段详解回溯、动规与贪心算法精讲这是算法学习的核心难点也是面试中的重头戏。理解其思想比背诵模板更重要。6.1 回溯算法枚举所有可能性的艺术回溯本质是深度优先搜索DFS用于解决组合、排列、分割、子集等问题。其核心是“尝试-回溯”的递归过程。模板与核心思想递归函数通常叫backtracking参数包含当前路径path、当前选择位置startIndex等。终止条件当满足题目要求如路径长度等于k时将当前路径加入结果集。遍历选择在当前层遍历所有可能的选择。做出选择将选择加入路径。递归进入下一层。撤销选择回溯将刚才加入路径的选择移除恢复到之前的状态以进行下一次尝试。经典例题77. 组合题意从1到n中任选k个数的所有组合。C详解class Solution { private: vectorvectorint result; vectorint path; void backtracking(int n, int k, int startIndex) { if (path.size() k) { // 终止条件路径长度等于k result.push_back(path); return; } // 遍历选择从startIndex开始到 n - (k - path.size()) 1 进行剪枝 for (int i startIndex; i n - (k - path.size()) 1; i) { path.push_back(i); // 做出选择 backtracking(n, k, i 1); // 递归下一层从i1开始避免重复 path.pop_back(); // 撤销选择回溯 } } public: vectorvectorint combine(int n, int k) { result.clear(); path.clear(); backtracking(n, k, 1); return result; } };关键点startIndex控制下一层递归的起始位置保证组合内元素不重复且有序避免出现[2,1]这样的重复组合。剪枝优化循环条件i n - (k - path.size()) 1。当前还需要k - path.size()个元素从i开始最多还能选n - i 1个元素。如果n - i 1 k - path.size()即剩下的元素不够了就没必要继续了。这是回溯算法性能优化的关键。排列问题46. 全排列与组合的区别排列关注顺序[1,2]和[2,1]是不同的。因此不需要startIndex但需要used数组记录哪些元素已经被使用过。C实现void backtrack(vectorint nums, vectorbool used) { if (path.size() nums.size()) { result.push_back(path); return; } for (int i 0; i nums.size(); i) { if (used[i]) continue; // 当前数字已使用跳过 used[i] true; path.push_back(nums[i]); backtrack(nums, used); path.pop_back(); used[i] false; } }6.2 动态规划从记忆化搜索到状态转移动态规划是解决具有重叠子问题和最优子结构问题的强大工具。其核心是定义状态和状态转移方程。解题步骤确定dp数组及下标的含义。确定递推公式状态转移方程。dp数组如何初始化。确定遍历顺序。举例推导dp数组用于验证和调试。经典入门70. 爬楼梯题意每次可以爬1或2阶到n阶有多少种方法。思路dp[i]爬到第i阶楼梯的方法数。要想到达第i阶可以从第i-1阶爬1步上来也可以从第i-2阶爬2步上来。所以dp[i] dp[i-1] dp[i-2]。初始化dp[1]1,dp[2]2或dp[0]1作为起点。C实现空间优化版int climbStairs(int n) { if (n 2) return n; int dp_i_2 1; // dp[i-2] int dp_i_1 2; // dp[i-1] int dp_i; for (int i 3; i n; i) { dp_i dp_i_1 dp_i_2; dp_i_2 dp_i_1; dp_i_1 dp_i; } return dp_i_1; // 循环结束时dp_i_1就是dp[n] }关联这就是斐波那契数列。很多简单DP问题都是斐波那契的变体。背包问题基础416. 分割等和子集0-1背包题意判断数组是否能分成两个和相等的子集。转化为背包问题数组总和为sum目标就是找一些数其和为target sum/2。每个数只能选一次这就是0-1背包。DP定义dp[j]容量为j的背包能装的最大价值这里价值重量即数字本身。但本题是“能否装满”所以可以定义dp[j]为容量为j的背包能否恰好装满布尔值。状态转移对于当前数字nums[i]如果j nums[i]那么dp[j] dp[j] || dp[j - nums[i]]。即不选nums[i]保持dp[j]或选nums[i]看j-nums[i]能否装满。C实现bool canPartition(vectorint nums) { int sum accumulate(nums.begin(), nums.end(), 0); if (sum % 2 ! 0) return false; // 和为奇数不可能平分 int target sum / 2; vectorbool dp(target 1, false); dp[0] true; // 容量为0的背包不装任何东西就是满的 for (int num : nums) { for (int j target; j num; j--) { // 必须倒序遍历保证每个物品只使用一次 dp[j] dp[j] || dp[j - num]; } } return dp[target]; }关键心得0-1背包的一维DP数组实现内层循环必须倒序遍历容量。这是因为dp[j]依赖于上一轮i-1的dp[j-num]。正序遍历会覆盖掉上一轮的值导致一个物品被重复使用变成完全背包。6.3 贪心算法局部最优与全局最优贪心算法的核心是每一步都做出当前看起来最优的选择希望导致全局最优解。它不像动规有固定的公式更考验对问题性质的洞察和证明。简单贪心455. 分发饼干题意每个孩子有胃口值g[i]每块饼干有尺寸s[j]一块饼干最多满足一个胃口值小于等于它的孩子。求最多满足的孩子数。贪心策略为了不浪费饼干大饼干优先满足胃口大的孩子或者小饼干优先满足胃口小的孩子。这里采用“小饼干喂饱小胃口”。步骤将g和s排序。用指针i遍历孩子指针j遍历饼干。如果s[j] g[i]则满足两个指针都后移否则只移动饼干指针j尝试更大的饼干。C实现int findContentChildren(vectorint g, vectorint s) { sort(g.begin(), g.end()); sort(s.begin(), s.end()); int i 0, j 0; while (i g.size() j s.size()) { if (s[j] g[i]) { i; // 满足一个孩子 } j; // 无论是否满足饼干都被尝试过了 } return i; // i就是被满足的孩子数量 }为什么贪心有效可以反证如果最优解中有一块小饼干满足了一个大胃口的孩子那么交换一下用这块小饼干去满足一个更小的胃口如果存在不会使结果变差。所以排序后贪心匹配可以得到最优解。贪心算法通常需要证明但在面试中能清晰阐述“为什么这样贪心”的思路往往比严格证明更重要。对于更复杂的贪心问题如“区间调度”、“跳跃游戏”需要多做练习来培养直觉。7. 常见问题与排查技巧实录在刷题和面试过程中一些常见错误和调试技巧能帮你节省大量时间。7.1 编译与语法错误vector下标越界这是最常见的运行时错误。访问前务必检查索引i是否满足0 i vec.size()。在循环中注意边界条件。空指针访问对于指针或可能为nullptr的节点如TreeNode*,ListNode*在访问其成员-val,-next前必须判空。使用未初始化的变量局部变量不会自动初始化使用前请赋值。特别是int,bool等基本类型。函数返回值确保所有控制路径都有返回值。编译器可能会报错“control reaches end of non-void function”。7.2 逻辑与算法错误无限递归递归函数没有正确的终止条件或终止条件永远达不到。检查递归基base case是否正确递归参数是否向基 case 收敛。死循环while或for循环的终止条件写错导致循环变量不更新或更新错误。在循环开始和结束时打印关键变量值有助于调试。状态未回溯在回溯算法中忘记在递归返回后pop_back()或重置used数组导致状态污染。DP数组初始化错误dp[0]或边界条件的初始化至关重要。例如在背包问题中dp[0]0和dp[0]1代表完全不同的含义。务必结合题意和递推公式推导初始化值。整数溢出当题目涉及大数运算如阶乘、指数或使用int进行累加时注意结果可能超出int范围约±21亿。考虑使用long long。7.3 调试与性能优化技巧打印调试法在关键位置如循环开始/结束、递归入口/出口打印变量状态。对于复杂数据结构链表、树可以编写简单的打印函数。小数据测试不要一上来就用复杂用例。先用题目给的示例甚至自己构造更小的、边界的情况空输入、单个元素进行测试。对比暴力法如果你的优化算法结果不对可以写一个简单但正确的暴力解法如双重循环在小数据上对比结果定位错误。复杂度分析提交前预估算法的时间和空间复杂度。如果超时TLE考虑是否存在更优算法如用哈希表O(n)替代暴力O(n²)或者递归/回溯中是否可以进行剪枝。利用STL特性unordered_map的[]运算符在key不存在时会插入默认值而find方法不会。根据场景选择使用避免意外插入。容器选择频繁在头部插入/删除用deque或list随机访问用vector查找用unordered_set/map无序遍历或set/map有序遍历。7.4 面试实战技巧先沟通再动笔拿到题目先和面试官确认理解是否正确阐述你的初步思路暴力法、可能的优化方向获得反馈后再开始写代码。边写边讲写代码时解释你在做什么为什么这么做。这展示了你的沟通能力和思维过程。考虑边界写完代码主动提出测试一些边界情况空、单元素、极大值、负数等。分析复杂度代码完成后主动分析时间复杂度和空间复杂度。代码风格使用有意义的变量名适当添加注释保持代码整洁。在C中注意const的正确使用以及指针/引用的选择。刷题是一个持续积累和反思的过程。这份笔记会随着我的学习和实践不断更新补充更多经典的题目和更深入的解析。记住目标不是刷完所有题而是通过每一道题掌握一类方法构建起自己的算法知识网络。当你拿到一个新题能快速将其归类到某个已知的模型或模式中时你就真正入门了。
返回列表