资讯动态

算法刷题核心模板:双指针、动态规划、回溯与二分实战

发布时间:2026/10/15 22:19:45 来源:尧图企业网站定制
核心关键词背后的算法思维高频题刷多了你会发现一个现象真正让人卡住的往往不是题目本身而是不知道该往哪个方向想。LeetCode 和牛客上的题虽然成千上万但出题套路其实高度集中。我整理过自己刷过的几百道题发现大概有这几类核心思维模型把每个模型吃透比题海战术管用得多。第一个是贪心思维。这类题的特征是“每一步都选当前最优最后就是全局最优”。听起来简单但难点在于证明“局部最优等于全局最优”。很多同学看到贪心题就想用动态规划硬刚反而把问题搞复杂。典型如“跳跃游戏”“任务调度器”“分发饼干”这类题的关键在于找到正确的贪心策略而不是证明它证明的工作就交给数学直觉和测试用例。第二个是双指针思维。排序数组找两数之和、判断回文链表、链表找环这类题用双指针能把 O(n^2) 砍到 O(n)。双指针的精髓在于“让两个指针保持特定的距离或移动规则”这需要你对题目条件足够敏感。比如看到“有序数组”四个字第一反就应该想到双指针或者二分。第三个是动态规划思维。这是大多数人的噩梦但也是最高频的考点。动态规划的核心不是套公式而是搞清楚两件事状态怎么定义转移方程怎么写。我刷动态规划题总结出来的经验是先画表格把状态转移的过程用表格模拟一遍思路清晰了再写代码。后面的章节我会用一个具体的例子拆解这个过程。第四个是哈希表思维。空间换时间看到“找出现次数”“去重”“两数之和”这类关键词第一反应该是哈希表。在 Python 里就是字典或集合在 Java 里就是 HashMap 和 HashSet。明确了核心思维模型接下来就进入实践环节。高频题不能盲目刷需要抓重点。LeetCode 的 Top Interview Questions 列表、牛客的热题榜都是很好的起点。我的建议是每天 3 道题按标签刷而不是按难度刷先搞定双指针和哈希表这两个性价比最高的标签再过渡到动态规划。基础攻克数组与字符串的高频题型与模板数组和字符串是算法题里最常见的载体也是很多复杂题型的基础。搞定这一块后面刷链表、树、动态规划都会轻松很多。这一部分题目的特点是看起来简单但边界条件极其容易出错需要你养成“先写注释再写代码”的习惯。暴力解法到双指针优化的完整思路先看一个最常见的题三数之和。题目要求找出数组中所有和为 0 的三元组。新手第一反应是三重循环这个写法思路没错但时间复杂度是 O(n^3)对于数据量稍大的用例直接超时。def three_sum(nums): nums.sort() result [] n len(nums) for i in range(n - 2): if i 0 and nums[i] nums[i - 1]: continue left, right i 1, n - 1 target -nums[i] while left right: current_sum nums[left] nums[right] if current_sum target: result.append([nums[i], nums[left], nums[right]]) while left right and nums[left] nums[left 1]: left 1 while left right and nums[right] nums[right - 1]: right - 1 left 1 right - 1 elif current_sum target: left 1 else: right - 1 return result排序是这道题的关键一步。排序之后固定一个数剩下的两个数用双指针从两端往中间逼近。这个套路非常标准刷熟之后可以迁移到好几道题上。特别要注意去重的写法外层的 i 跳过重复值内层找到答案后左右指针都要跳过重复值。这个细节写到烂熟面试手写不会卡壳。边界条件分析的隐藏陷阱数组题最容易在边界条件上翻车。以这道题为例需要注意n 3时直接返回空列表这是最容易被忽略的判断。排序后第一个元素就已大于 0可以直接剪枝返回因为三个正数之和不可能为 0。固定元素 i 时nums[i] nums[i-1]的去重逻辑不能写成nums[i] nums[i1]否则会跳过正确答案的三元组组合形式。很多人忽略排序这一步在整体方案中的价值。排序是 O(n log n)后面双指针是 O(n)对每个 i 都要跑一遍双指针所以总复杂度是 O(n^2)。这个复杂度级别在面试中是可以接受的最优解。字符串高频题型滑动窗口模板直接用字符串当之无愧的高频区间是“子串问题”。最长不重复子串、最小覆盖子串、重复 DNA 序列几乎都能用滑动窗口模板解决。滑动窗口本质上是一种“在线维护区间信息”的双指针变体。def length_of_longest_substring(s): char_set set() left, max_len 0, 0 for right in range(len(s)): while s[right] in char_set: char_set.remove(s[left]) left 1 char_set.add(s[right]) max_len max(max_len, right - left 1) return max_len核心机制是右指针负责扩张左指针负责收缩。等发现当前字符已经在集合里时就收缩左边界直到集合里不再有重复字符。这个过程每个字符最多进集合一次、出集合一次所以时间复杂度是 O(n)空间复杂度 O(min(m, n))m 是字符集大小。模板的迁移从字符串到其他场景这套滑动窗口模板可以迁移到很多场景。比如“长度为 k 的滑动窗口内的最大值”换成维护双端队列“最小覆盖子串”换成维护窗口内各字符出现次数的字典。不要死记模板理解每个变量的作用后套用到新题上才不会被“变形”卡住。常见误区数组越界与索引逻辑数组题最经典的报错就是 IndexError。举一个我刷题时真实踩过的坑“寻找旋转排序数组中的最小值”这道题用二分法时我当时判断的是nums[mid] nums[mid1]当 mid 恰好是最后一个索引时直接越界。后来养成习惯凡是要访问mid1或mid-1先确认 mid 不会落在边界上。二分法处理这种题时循环条件建议写成left right这样退出时 left 和 right 相等直接返回nums[left]省去一堆边界判断。数据结构必考链表、栈与队列的实战模板链表题在面试中非常高频尤其在需要考察“指针操作基本功”的场景。链表题的难点不在于算法思维而在于指针操作的细节。你需要在纸上画出每个节点的 next 指向变化从图上推演代码才会真的理解。链表反转类问题的核心模板链表反转是很多题的基础操作回文链表判断、K 个一组翻转链表等都会用到。迭代反转是最基础也最通用的写法def reverse_linked_list(head): prev None cur head while cur: next_node cur.next cur.next prev prev cur cur next_node return prev核心动作是保存 next、反转当前节点的指向、移动 prev 和 cur。这三步的顺序一定不能乱。先保存 next 是为了防止断链后找不到下一个节点然后让 cur.next 指向 prev最后把 prev 和 cur 都往后移动。这套模板在面试中大概率会用到背熟写出只是及格水平真正加分的是你能解释清楚每一步的作用和边界场景比如链表只有一个节点时这套代码也能正常工作。链表环检测快慢指针的代码细节链表找环用龟兔赛跑算法也就是快慢指针。快指针每次走两步慢指针每次走一步如果链表有环两个指针必然在某一刻相遇。这里一个很容易被忽略的点是快指针必须先判断自身是否为 None再判断fast.next是否为 None。否则快指针已经走到链尾时访问fast.next.next会抛空指针异常。正确的写法def has_cycle(head): slow fast head while fast and fast.next: slow slow.next fast fast.next.next if slow fast: return True return False需要留意的是初始判断。如果head为空或只有一个节点循环不会进入直接返回 False。这个写法不依赖链表长度也不会无限循环因为没环的链表 fast 终会遇到 None有环的链表快慢指针一定会相遇。链表题的边界思维空节点与单节点链表题我踩过最多的坑就是空指针对 None 调用 .next 或 .val。所以拿到链表题我习惯先写一个保护性的判断if not head or not head.next: return head # 空链表或单节点链表无需处理这行代码能解决链表反转、删除倒数第 N 个节点、交换链表节点中“处理单节点”的全部边界问题。再一个细节是虚拟头节点的使用。凡是头节点可能被删除的题比如“删除链表的倒数第 N 个节点”都建议创建一个虚拟头节点指向 head。这样删除操作就不需要单独讨论“删除的是头节点”这个分支代码更统一。栈与队列单调栈的思维破局栈和队列的题集中在括号匹配、表达式求值、单调栈三类。括号匹配是栈的入门应用左括号入栈右括号时检查栈顶是否匹配。单调栈的思维更隐蔽但也是高频点。“接雨水”这道题就可以用单调递减栈解决。遇到比栈顶矮或相等的柱子就入栈遇到更高的柱子说明栈顶柱子右边出现了一个“右边界”此时可以计算一次积水面积。这里的关键思维是积水区域不是一次性计算的而是由“左右边界之间的较小值”决定每个横向小块。这种“把大问题拆成若干横向小块”的思路就是单调栈的精华。def trap(height): stack [] water 0 for i, h in enumerate(height): while stack and h height[stack[-1]]: top stack.pop() if not stack: break left stack[-1] width i - left - 1 high min(height[left], h) - height[top] water width * high stack.append(i) return water这段代码的难点在于理解“为什么 popped 之后再检查栈是否为空”。栈顶元素被弹出后如果栈里没有元素说明左边没有更高的柱子挡着积水会从这里流走所以不能计算面积。这个细节是很多初学者卡住的地方。另外打双端队列的两个方向都要熟练。滑动窗口最大值那道题核心也是单调队列维护一个队头到队尾递减的序列每次窗口滑动时先从队头移除已离开窗口的元素再加入新元素时从队尾弹出所有更小的元素。理解了两个方向的弹出规则就能解决一大类“窗口极值”问题。动态规划从表格法到状态压缩的全面拆解动态规划这块我见过太多人直接卡死在入门阶段。其实只要掌握正确的思考顺序动态规划并没有传说中那么可怕。我认为最关键的一步是先画表格再写代码。经典零一背包问题的逐步推导以“零一背包”为例有 n 个物品每个物品有重量 w[i] 和价值 v[i]背包容量为 C每个物品只能取一次求能装下的最大价值。二维状态的转移方程是这样定义的dp[i][j]表示前 i 个物品中在容量为 j 时能装下的最大价值。注意状态定义中“前 i 个物品”这个约束这让转移方程变得清晰要么不选第 i 个物品价值等于dp[i-1][j]要么选第 i 个物品价值等于dp[i-1][j - w[i]] v[i]。def knapsack(weights, values, capacity): n len(weights) dp [[0] * (capacity 1) for _ in range(n 1)] for i in range(1, n 1): for j in range(capacity 1): if weights[i-1] j: dp[i][j] dp[i-1][j] else: dp[i][j] max(dp[i-1][j], dp[i-1][j-weights[i-1]] values[i-1]) return dp[n][capacity]空间优化的核心思路上面二维数组的写法空间复杂度是 O(n*C)。但观察状态转移第 i 行只依赖第 i-1 行所以可以用一维数组滚动更新。关键在于遍历容量时必须倒序def knapsack_optimized(weights, values, capacity): dp [0] * (capacity 1) for i in range(len(weights)): for j in range(capacity, weights[i]-1, -1): dp[j] max(dp[j], dp[j-weights[i]] values[i]) return dp[capacity]为什么要倒序因为一维 dp 复用同一个数组如果正序遍历dp[j - weights[i]]可能已经包含了第 i 个物品导致同一个物品被多次选择这就变成了完全背包而非零一背包。倒序遍历保证dp[j - weights[i]]是上一轮的状态还没有被当前物品污染。这个细节是零一背包和完全背包的唯一区别。完全背包只改一行方向定乾坤完全背包的物品可以选无限次。只需要将内层循环改为正序for j in range(weights[i], capacity 1): dp[j] max(dp[j], dp[j - weights[i]] values[i])因为正序保证可以用到本次迭代同一物品更新过的状态相当于允许重复选择。与零一背包对比记忆动态规划的空间优化就不再是死记硬背而是一个自然推导的结果。最长上升子序列的常见考察点最长上升子序列是面试和笔试都极高频的题暴力解法是 O(n^2) 的动态规划进阶解法是用“贪心 二分”做到 O(n log n)。我这里先讲 O(n^2) 版本因为它最容易理解也是后续优化的基础。def length_of_lis(nums): if not nums: return 0 dp [1] * len(nums) for i in range(len(nums)): for j in range(i): if nums[i] nums[j]: dp[i] max(dp[i], dp[j] 1) return max(dp)状态定义是dp[i]表示以第 i 个元素结尾的最长上升子序列长度。每个位置初始化 1表示子序列长度至少为 1该元素自身。在 O(n^2) 版本中dp[i]是在扫描所有前面的元素 j 后更新得来的。O(n log n) 版本的核心是维护一个数组 tailtail[k] 表示长度为 k1 的上升子序列末尾元素的最小值。处理每个新元素时在 tail 中二分查找第一个大于等于它的位置替换掉。这个技巧的精髓在于“用后续更小的元素替换前面的较大元素保持插入可能性最大化”。关于状态转移的循环顺序有个很容易犯的错在 O(n^2) 写法里第二层 j 循环必须从 0 到 i不能从 i-1 到 0。否则虽然大部分情况结果相同但在有相等元素和特殊序列时会出错。为了方便记忆动态规划的循环顺序一概保持“从小到大、从前往后”这也是最利于排查问题的写法。动态规划的通用思考路径状态、转移、边界动态规划题目千变万化但思考路径是固定的。我总结了四步法明确状态这个问题的子问题是什么维度有哪些比如背包问题的状态是“物品数”和“容量”两个维度。定义 dp 数组dp[i][j]通项公式是什么写清楚下标含义。推导转移方程当前状态可以从哪些状态转移过来列出所有可能取最大值或最小值。确定边界与遍历顺序初始值是多少循环从小到大还是从大到小零一背包倒序、完全背包正序就是典型的遍历顺序问题。这套路径用得顺了遇到新题不会慌。面试时哪怕最终没有写出最优解只要把状态定义和转移方程讲清楚也能拿到大部分分数。高频考察的字符串处理与哈希技巧字符串和哈希往往是组合题。前面讲过的滑动窗口就用到了哈希表记录窗口内元素。哈希表还有一个非常经典的用法两数之和系列。从两数之和到三数之和的技术演进两数之和是大多数人的算法入门第一题。暴力法是 O(n^2)用哈希表可以做到 O(n)def two_sum(nums, target): seen {} for i, num in enumerate(nums): complement target - num if complement in seen: return [seen[complement], i] seen[num] i return []这里的核心技巧是“边遍历边建表”。先检查补数是否已经在哈希表中如果没有就把当前数存入表里。这样省去了一次单独建表的循环也让代码更简洁。这个写法在后面的“判断两数之和是否存在”类题目中可以直接复用。三数之和的哈希版本需要先去重处理逻辑比排序 双指针复杂所以更推荐双指针方案。但从两数到三数你看到的是哈希表思维如何在更复杂的题目中优雅地迁移。它不再适合时往往会转成排序 双指针这套方案。字符串高频哈希题字母异位词分组“字母异位词分组”是一道极具代表性的哈希表题。考察点是把异位词归到一组其中“如何表示一个字符串的指纹”是设计重点。def group_anagrams(strs): from collections import defaultdict groups defaultdict(list) for s in strs: key .join(sorted(s)) groups[key].append(s) return list(groups.values())排序后的字符串作为哈希键。这个方案的时间复杂度是 O(n * k log k)其中 n 是字符串数量k 是最长字符串的字母个数。空间复杂度 O(n*k)。这个思路也广泛用于判断“两个字符串是否互为异位词”的题目。一个提升性能的变体是“用字母出现次数作为 key”即把每个字符出现次数拼成字符串例如a1b2c0这样的形式。这个做法把排序降到 O(k)但构建 key 也需要 O(k)。实测来看在字符串较短时普通排序足够快在字符串很长时计数法的收益才明显。两种写法都建议掌握面试时可以展示你对比两者的能力。最长不含重复字符的子串两种解法的对比上一节已经给过滑动窗口解法。这里补一个更贴近“面试加分”的优化版本用字典记录每个字符最后一次出现的下标从而把左指针直接跳到重复字符的下一个位置。def length_of_longest_substring(s): pos {} left max_len 0 for right, ch in enumerate(s): if ch in pos and pos[ch] left: left pos[ch] 1 pos[ch] right max_len max(max_len, right - left 1) return max_len如果不小心把条件写漏了pos[ch] left后果很微妙当 ch 曾经出现过但位置在 left 左边时这个信息其实已经无效因为我们当前窗口并不包含它。加这个条件就是为了避免被旧位置干扰。这版代码虽然不如前面 set 版直观但熟练后代码更短、思考更精准。哈希映射的冲突与扩容原理简述理解了哈希表能用 O(1) 时间查找的前提你会更敢在面试里用哈希解决看起来与“哈希”无关的问题。哈希表本质是数组加哈希函数把 key 映射到数组下标。冲突时用链地址法或开放寻址法解决。Python 的 dict 采用开放寻址Java 的 HashMap 在链表长度超过阈值时转化为红黑树。这些知识本身不一定直接考但在“为什么你的算法是 O(1)”的追问环节可以自然回答。回溯与搜索从排列组合到递归剪枝回溯算法在牛客和 LeetCode 中一直处于“面试高频、但代码容易写乱”的地位。高点在于题型多全排列、子集、组合总和、岛屿数量、单词搜索全是回溯和搜索。低点是如果递归参数设计不合理代码会陷入混乱。全排列问题的回溯模板全排列题最适合讲清楚回溯的通用模板。核心思想是“已经用过哪个数、当前路径有哪些元素、是否收集到足够长度”。用 Python 写def permute(nums): result [] path [] used [False] * len(nums) def backtrack(): if len(path) len(nums): result.append(path[:]) return for i in range(len(nums)): if used[i]: continue used[i] True path.append(nums[i]) backtrack() path.pop() used[i] False backtrack() return resultpath[:]拷贝是必须的。如果不拷贝直接 append(path)后续的 path 变化会直接影响已经加入结果的对象导致结果数组里全是同一个最终状态的路径。这个问题在 Python 新手里出现的频率极高。剪枝的两种情况可行性剪枝与去重剪枝剪枝是回溯的提速关键。第一种是可行性剪枝当前选择明显不可能得到合法解时直接跳过。比如“组合总和”里当前的数已经大于剩余目标值不再往下走。“括号生成”里右括号数量已经大于左括号数量时不继续递归。这类剪枝极大缩小了搜索空间。第二种是去重剪枝在含重复元素的序列中排序后跳过与前一个元素相同的选择前提是前一个元素没有被使用。比如含重复元素的全排列若不剪枝会生成重复结果。if i 0 and nums[i] nums[i-1] and not used[i-1]: continue这个条件非常经典。它确保重复元素只有在前一个相同元素已经被使用的情况下才能继续排列下去。这样就把同值元素之间的相对顺序固定为一种避免重复排列。很多人背不下这个条件建议手动模拟两次含重复元素的全排列过程内在逻辑会非常清晰。岛屿问题深度优先搜索的边界处理岛屿问题是搜索类的高频代表判断网格中陆地连成几块。解法可以是 DFS 或 BFS核心是标记已访问的陆地防止重复计数。DFS 写法def num_islands(grid): if not grid: return 0 count 0 rows, cols len(grid), len(grid[0]) def dfs(r, c): if r 0 or r rows or c 0 or c cols or grid[r][c] 0: return grid[r][c] 0 dfs(r1, c) dfs(r-1, c) dfs(r, c1) dfs(r, c-1) for r in range(rows): for c in range(cols): if grid[r][c] 1: count 1 dfs(r, c) return count这里的核心是“访问过就置为 0”防止后续重复搜索。这种标记手法被称为“原地标记法”。注意递归边界要先判断越界再判断字符值顺序反了会在越界时访问到不存在的行或列。如果你觉得网格类的递归深度可能超过 Python 默认递归限制可以加sys.setrecursionlimit()或者改 BFS。牛客的递归限制一般能支撑常见用例但谨慎起见知道这个风险点即可。递归回溯的复杂度分析与状态重置回溯算法的复杂度通常是 O(答案数量 * 构造单个答案的开销)。以全排列为例答案是 n! 个每个答案拷贝需要 O(n)所以时间复杂度是 O(n * n!)。空间复杂度取决于递归深度最坏是 O(n)。这个复杂度在数据规模 n 8 或 9 时是可接受的。一旦 n 超过 12全排列类型的试题基本不会出现除非用数学公式化简。这个经验可以帮助你判断回溯题的数据范围。状态重置指的是递归返回前把路径和标记恢复原状。很多人漏掉这一步导致同一层的状态被污染。我在刷题初期的教训是在递归函数里先记录状态比如 visited 数组的修改再在递归返回时恢复。凡是用到全局状态或可变对象的回溯都要成对出现“修改 / 恢复”。建议在写递归前先写一个状态修改清单然后像 checklist 一样逐步执行。排序、二分与边界思维排序和二分看似基础却是面试中容易“阴沟翻船”的点。排序在于选择哪种策略、稳定性。二分则是考察你对区间不变量的理解。快速排序与归并排序的两种模板对比面试手写排序最快的写法是快速排序def quick_sort(nums): if len(nums) 1: return nums pivot nums[len(nums) // 2] left [x for x in nums if x pivot] mid [x for x in nums if x pivot] right [x for x in nums if x pivot] return quick_sort(left) mid quick_sort(right)这个写法简单直接但不高效因为它会反复生成新数组空间开销比较大。它在面试中拿来演示思路可以真正工程里更推荐原地划分版本的快排以避免额外空间。归并排序的优势则在于稳定性和适合链表及外部排序。时间复杂度都是 O(n log n)但快排平均性能最好归并排序则可保证最坏 O(n log n)。如果面试官要你手写“数组中的逆序对”就需要归并排序思维。统计逆序对的思路是在合并前后两个有序子数组时发现右侧元素小于左侧元素时就可以直接累计右侧元素之前跨过一个左侧元素的逆序数。标准二分的循环不变量二分查找的边界问题最让人头痛。只需要牢牢抓住一个原则保持left指向“满足条件区间的左边一个位置”right指向“满足条件区间的右边一个位置”循环条件用left right最后返回 left 或 right。不同写法会有差异但关键是先想清楚“你要查找的到底是什么性质”。def binary_search(nums, target): left, right 0, len(nums) - 1 while left right: mid (left right) // 2 if nums[mid] target: return mid elif nums[mid] target: left mid 1 else: right mid - 1 return -1这套写法的循环不变量是“target 如果存在一定在 [left, right] 这个闭区间里”。每次排除一半区间不会出现遗漏。换一种写法左闭右开的区间则对应不同的循环条件切忌几种写法混用导致 mid 更新错误。二分答案的实战使用场景二分法不只能用于“在有序数组中找值”还能用于“在可行域中找最优解”。典型的场景包括“把数组分成 m 份求最大值最小化”“在 D 天内送达包裹的能力”。这类题的特征是解空间是单调的求最小值或最大值的可行判断可以用贪心快速验证。做法是 binary search over answer对答案进行二分然后写 check 函数判断当前答案是否可行。def ship_within_days(weights, days): def can_ship(capacity): current_sum, count 0, 1 for w in weights: if current_sum w capacity: count 1 current_sum 0 current_sum w return count days left, right max(weights), sum(weights) while left right: mid (left right) // 2 if can_ship(mid): right mid else: left mid 1 return left这套“二分答案 贪心 check”的组合在高频题中反复出现。掌握了它等于掌握一类问题的通用解法。很多看起来与二分无关的题只要解空间单调且可快速验证都可以套这套模板。我在牛客上看到不少公司笔试题就是这种类型实用性非常高。实战经验牛客笔试与 LeetCode 刷题节奏这一部分说说我踩过坑之后的节奏安排包括时间分配、复盘方式和心态管理。很多同学刷题刷了很久却感觉进步缓慢基本都是节奏问题。笔试环境的特殊性与踩坑记录牛客笔试和 LeetCode 有一个显著区别牛客通常是 ACM 模式需要自己处理输入输出。有些同学在 LeetCode 上刷得很顺一到牛客笔试连输入格式都读不明白。剑指 Offer 模式的题目通常已经封装好函数但牛客的热门模拟卷经常是 ACM 模式。你需要掌握sys.stdin.read()、input().split()、map(int, ...)这些基础输入技巧。另一个常见问题是类名和函数名。牛客的题目有些平台要求类名必须是 Mainpublic static void main 入口。已经编译报错过的人会懂这种无语。建议首次使用某个平台时先做一道最简单的“AB 问题”把输入输出、依赖库、包名、类名这些环境因素全部踩通再开始正式刷题。时间分配的黄金比例我试过两种节奏。第一种是每天刷 10 题但不总结月底回头一看全忘了。第二种是每天 3 题加 1 道复习月底能清晰记得每类题的模板。个人经验是后者的效率远超前者。推荐节奏比较简单每天 2 道新题 1 道旧题回顾。按标签刷一轮专门做“数组 双指针”一轮“动态规划”一轮“回溯 搜索”。每周挑 2 道做过的题从零开始写一遍模拟面试白板状态。每天复盘时间控制在 30 分钟以内写下“这题的题型关键词”和“核心模板的链接”。复盘模板怎么用最有效我会用一个简单的复盘表格。字段包含日期、题目名、核心思维、模板难度、首次 AC 耗时、是否需要重刷。如果一道题首次 AC 耗时超过 40 分钟就标记为“需要重刷”。重刷时间间隔是 3 天后、7 天后、21 天后。这样能对抗遗忘曲线。在“核心思维”这一栏我不写题目描述只写一句抽象的“这类题的做法”。比如“看到有序数组找目标 → 二分”“看到字符串子串 → 滑动窗口”“看到最优解 重叠子问题 → 动态规划”。这些抽象描述才是复盘的真正价值比抄一遍题解有用得多。面试现场的算法表达技巧面试和笔试的考察点不太一样。笔试考察结果面试更考察思考过程。我见过不少同学一上来就写代码结果被追问两个细节就卡住了。正确的顺序建议是先口述思路、说复杂度再开始写代码。写的时候边写边讲变量作用。写完主动跑一个测试用例验证。这一套流程走完面试官对你的工程意识和沟通能力的印象会远高于“闷头写完 bug-free 但说不出为什么”的候选人。需要避免的现场答题误区第一误区为了展示自己会更多解法开篇先说一个复杂的、不常见的解法。面试官每题可用的时间有限建议先答稳妥的解法再补充优化思路。第二误区不会的时候沉默。面试最常见的考察方式就是“遇到问题时的应对方法”如果完全没有头绪可以先把暴力解法说出来再分析哪里能优化。暴力解法也有效表明你在工作。哪怕你没优化出来也比一声不吭强很多。第三误区写完不验证。手写代码不算运行很难发现问题但主动走一个简单的例子能帮你发现很多低级错误同时也向面试官证明你有测试意识。结语与限时模板分享这套刷题体系用了大半年我的 LeetCode AC 从 80 题涨到 300 题以上牛客模拟卷的通过率也从 30% 提升到 70% 以上。我没有特别好的天赋只是把刷题变成了一件有反馈、有节奏的事。最后给出一份浓缩模板清单题型核心方法时间复杂度数组/字符串子串滑动窗口O(n)两数之和类哈希表O(n)链表反转/环检测迭代/快慢指针O(n)多重循环优化排序 双指针O(n log n) O(n)背包问题一维 DP 滚动数组O(n*C)最长上升子序列贪心 二分O(n log n)全排列/组合回溯 剪枝依题意最值可行性问题二分答案 贪心 checkO(log(区间) * check)我个人在实际操作中的体会是最笨的办法往往最稳。把算法模板理解到位之后先用最直白的思路把题 AC 掉再考虑如何优化。这样不仅能保证正确性还能在优化的过程中学到更多东西。刷题不是为了炫技而是为了在真正的面试中能稳定输出这比什么都重要。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑