☰
高频算法题刷题攻略:题型识别、模板记忆与边界条件
2026/10/15 16:27:34 网站建设 项目流程

1. 刷题前必须想清楚的几件事

很多刚开始刷算法题的朋友,最容易犯的一个错误就是拿到题目就闷头写代码,写完提交报错再改,改了再交,几次之后虽然AC了,但回头一看,啥也没记住。我在带过不少新人之后发现,真正能把牛客和LeetCode高频题刷明白的人,靠的不是题海战术,而是先建立一套自己的“解题操作系统”。这套系统包括三部分:题型识别、模板记忆、边界条件意识。

先说题型识别。你拿到一道题,第一眼不是看它讲了个什么故事,而是看它属于哪一大类:是数组操作、链表翻转、二叉树遍历,还是动态规划、回溯搜索、单调栈?大类定下来,你的思路框架就有了。比如看到“连续子数组”“滑动窗口”这类关键词,基本就往双指针或前缀和上靠;看到“最近”“下一个更大”“单调”这类词,优先想单调栈。这不是死记硬背,而是大量高频题确实有非常强的规律性,先识别模式,再动手写,效率会翻好几倍。

再说模板记忆。我不建议背题,但强烈建议背模板。区别在哪?背题是把一道题的具体代码背下来,题目稍微换个包装就不会了;背模板是把一类问题的核心骨架提炼出来,比如二分查找的while写法、回溯的递归模板、链表的哑节点技巧,这些东西才是能迁移的。后面我会把牛客和LeetCode上最高频的几类题,全部整理成可以直接抄的模板,配合原理解释,你照着练几遍就能内化成自己的。

最后说边界条件。这可能是普通人和高手之间差距最大的一环。同一个模板,边界条件处理得好不好,直接决定你的代码能不能通过全部测试用例。空数组、单元素、负数、溢出、指针是否越界,这些“脏数据”才是真正考验水平的地方。我会在后面的章节里专门拿出一节来聊边界条件和常见坑,这些都是实战里踩出来的经验。

这篇内容适合谁?如果你是刚准备刷题的小白,建议先把基础语法过一遍,然后按文中的顺序逐类攻克;如果你已经刷过一两百道但总是感觉没体系,那你可以直接跳到自己薄弱的大类看模板和查错表;哪怕你是面试前临时抱佛脚,文末的模板速查表也够你在路上背一背了。

2. 整体思路:高频题的“二八定律”

2.1 为什么高频题永远逃不开那几类

我先给一个数据感:在牛客的校招算法题库和LeetCode热题100之间,重合度最高的题型是什么?大概是这么几类:数组与哈希表、链表、二叉树、回溯、动态规划、贪心、双指针和滑动窗口、栈与单调栈、排序与二分查找。注意,这里面的顺序不是随便排的,它其实就是按出现频率从高到低在走。

为什么会这样?因为面试官出算法题的核心目的,不是考察你会不会背某个偏门算法,而是考察你的代码能力、逻辑清晰度和对基础数据结构的掌握程度。数组和哈希表是所有算法的基础,链表是高频手写题,二叉树能考察递归思维,回溯和动态规划能考察状态抽象能力。这跟学校的教材顺序也基本一致,所以你按这个顺序刷,就是在用最小的题目量覆盖最大范围的考点。

我在实际带人刷题时,喜欢让大家先做一个动作:把LeetCode热题100和牛客的“高频常考”榜单拉出来,扫一遍每道题的标签,统计一下标签出现次数。你会发现,排序和二分并没有想象中那么多,但哈希表、双指针、递归这三个标签几乎无处不在。这个统计动作,比你看一百篇“刷题攻略”都管用,因为它会让你心里有数:真正该花大块时间的,是哪几个板块。

2.2 刷题节奏与时间分配的建议

我知道很多人会关心“一天刷几道合适”,但说实话,这个数字并没有标准答案,关键看你的周期和目标。如果还有一个月面试,那每天认真研究三道题,彻底吃透,比囫囵吞枣刷十道题有用得多。如果把周期拉长到三个月,那前两个月打基础,最后一个月专项突破,节奏会更好。

我更推荐的做法是“按模块刷,每周一个主题”。比如第一周只做数组和哈希表,第二周只做链表,第三周只做二叉树,第四周集中攻克动态规划。每个模块内部,先做两三道简单题找手感,再做中等题加深理解,最后回到模板上做总结。这样一轮下来,你脑子里会形成清晰的“题型 -> 模板 -> 边界”三层结构,而不是一团浆糊。

还有一个很多人忽略的点:复盘的时间应该不小于刷题的时间。一道题做完,AC了,不代表结束了。你要问自己三个问题:这题的模板是什么?边界条件有哪些?如果题目改一个条件,我的代码要动哪里?能回答上来,才算真的吸收了。刷题效率高的人,不是因为脑子快,而是因为复盘做得到位。

2.3 牛客和LeetCode的差异怎么利用

这两个平台我都刷了不少,各有所长。LeetCode的题解社区质量很高,discuss里能看到各种语言的版本,特别适合学习一题多解和优化思路;牛客的题库更贴近国内面试风格,尤其是那些“ACM模式”的输入输出题,能锻炼你处理OOM、越界等实际问题的能力。

我的建议是:以LeetCode为主学思路,以牛客为主练手感。具体来说,先在LeetCode上把模板题做明白,理解了原理之后,去牛客找同类型的题目用ACM模式写一遍。ACM模式要求你自己处理输入输出,这对细节的把控能力提升非常明显,很多人在LeetCode上写得很顺,一到牛客格式就崩,就是因为缺乏这方面的练习。

当然,这两者也有共同点:高频题就那么一百多道,反复刷个三轮,你的水平绝对超过大多数临时刷题的人。第一轮按类型刷,弄懂模板;第二轮打乱顺序刷,训练题型识别;第三轮限时刷,模拟真实面试的时间压力。三轮下来,你会明显感到拿到新题不再慌,能很快定位到熟悉的知识点。

3. 数组与哈希:高频中的高频

3.1 两数之和的模板化思考

两数之和大概是所有算法题里知名度最高的一道,也是无数人算法之路的起点。题目本身很简单:给定一个数组和一个目标值,找到两个数使它们的和等于目标值,返回下标。但是这道题的价值一点都不简单,它背后藏着“空间换时间”这个核心思想。

最直观的想法是双重循环枚举所有组合,时间复杂度O(n²)。这个解法没有问题,但面试官一定会追问一句:能不能优化?这时候你要想到哈希表。我们遍历数组时,每看到一个数字nums[i],就算出它需要的另一半target - nums[i],然后去哈希表里查这个值是否已经出现过。如果出现过,直接返回结果;如果没有,就把当前数字和下标存进哈希表。这样一趟遍历就能解决,时间复杂度降到O(n)。

这道题的模板可以写成下面这个样子:

def twoSum(nums, target): hash_map = {} for i, num in enumerate(nums): complement = target - num if complement in hash_map: return [hash_map[complement], i] hash_map[num] = i return []

注意几点:一定要先查哈希表再插入当前值,否则会出现用同一个元素自己匹配自己的问题;哈希表里存的key是元素值,value是下标;循环结束后如果没找到,按题目要求返回空结果或者特殊值。这套“遍历 -> 查需求 -> 存当前”的思路,后面在滑动窗口、前缀和里还会反复出现,所以说它是数组题的祖师爷模板,一点不夸张。

3.2 连续子数组问题:前缀和与哈希的组合拳

两数之和是“两个元素相加等于目标值”,再升级一步,就有了一道重量级高频题:连续子数组的和等于目标值,问一共有多少种情况。比如给定数组[1, 1, 1]和目标值2,答案是2,因为[1,1]出现两次。

这题的暴力解法是枚举左右端点,复杂度O(n²)。但如果用前缀和,就能把问题转化成:前缀和数组中,任意两个前缀和之差就是某段连续子数组的和。所以我们要找的,其实是“有多少对前缀和,它们的差值等于k”。这又变成了一个类似两数之和的问题,只不过这次哈希表里存的不再是下标,而是前缀和出现的次数。

def subarraySum(nums, k): prefix_sum = 0 hash_map = {0: 1} count = 0 for num in nums: prefix_sum += num if prefix_sum - k in hash_map: count += hash_map[prefix_sum - k] hash_map[prefix_sum] = hash_map.get(prefix_sum, 0) + 1 return count

这段代码里有几个细节一定要反复品。第一,哈希表初始要放入{0: 1},这代表前缀和为0的情况出现过一次,不然从开头到某个位置的子数组就会被漏掉。第二,每次是先查再更新次数,顺序颠倒会导致重复计数。第三,前缀和可能非常大,不要用数组,直接塞进哈希表就行。

这类题的变种很多,比如“和可被K整除的子数组”“乘积小于K的子数组”等,但核心思想都一样:遇到连续子数组的统计类问题,优先想前缀和,再考虑用哈希表加速查询。掌握了这套组合拳,你基本就拿下了数组题里最硬的一块骨头。

3.3 滑动窗口的边界收缩技巧

还有一个数组题的重头戏是滑动窗口。它的出现场景非常有辨识度:求连续子数组在某种约束下的最长或最短长度,且这种约束可以随着窗口移动而调整。典型的例子是“无重复字符的最长子串”和“最小覆盖子串”。

滑动窗口的模板并不复杂,核心是维护两个指针left和right,以及一个窗口内的状态信息(比如字符出现次数的字典)。右指针负责把新元素纳入窗口,左指针负责在条件不满足时收缩窗口。每次收缩,都要相应更新状态信息,最后在合适的位置更新答案。

def lengthOfLongestSubstring(s: str) -> int: window = set() left = 0 ans = 0 for right in range(len(s)): while s[right] in window: window.remove(s[left]) left += 1 window.add(s[right]) ans = max(ans, right - left + 1) return ans

无重复字符这道题用集合就行,因为窗口里每个字符最多出现一次。但如果题目变成“最多包含K种字符的最长子串”,状态信息就要换成字典来统计每个字符出现的次数。模板的本质不变:右指针 expand,不满足约束时用 while 循环 shrink,每次循环维护答案。至于 “先判断后收缩” 还是 “先收缩后判断”,每个题略有差异,建议你亲手写几遍,形成肌肉记忆。

滑动窗口的复杂度是O(n),因为每个元素最多被left和right各访问一次。相比暴力解法的O(n²),这是一个质的提升。面试时如果你能主动指出这一点,会是非常加分的表现。

4. 链表与栈:指针的体操

4.1 哑节点为啥是链表题的救命稻草

链表题是很多人刷题路上的一道坎,因为指针操作抽象,稍不留神就断链或者进入死循环。但我告诉你,链表题有一招可以覆盖八成以上的应用场景:哑节点(dummy node)。

哑节点是一个虚拟的链表头,它不存储任何有效数据,只是为了统一代码逻辑。举个例子,删除链表的倒数第N个节点,如果用常规思路,需要先遍历一遍求链表长度,再找到待删除节点的前一个节点。但有了哑节点,我们可以让快指针先走N步,然后快慢指针一起走,快指针到底时,慢指针正好停在待删除节点的前一个位置。这样一次遍历就能搞定,还不需要特判“删除的是头节点”这种边界情况。

def removeNthFromEnd(head, n): dummy = ListNode(0, head) fast = dummy slow = dummy while n + 1 > 0: fast = fast.next n -= 1 while fast: fast = fast.next slow = slow.next slow.next = slow.next.next return dummy.next

我特别想强调一下return dummy.next而不是return head。因为如果删除的就是原来的头节点,head还指着那个被删掉的节点,直接返回head就出错了。这个坑我见过很多人踩,而用哑节点基本可以免疫一大部分边界问题。所以说,哑节点的价值不只是逻辑统一,更是边界简化。

4.2 链表反转的迭代写法

反转链表是链表题的基础款,面试现场手写率极高。它的迭代写法核心是三个指针的配合:cur表示当前节点,prev表示前驱节点,temp保存后继节点。每一步操作都是:先用temp暂存cur.next,再把cur.next指向prev,接着prev和cur各自前进一位。循环结束后,prev就是新的链表头。

def reverseList(head): prev = None cur = head while cur: temp = cur.next cur.next = prev prev = cur cur = temp return prev

为什么需要一个temp?因为当你把cur.next指向prev之后,原来的后继节点就找不到了。不提前存一份,循环根本走不下去。这个”先暂存、再修改、后移动“的顺序,就是迭代反转的节奏,也是很多指针题目通用的操作哲学。

在此基础上,衍生题就多了:反转链表的前N个节点、反转区间内的节点、K个一组反转链表、回文链表判断等。如果你能把基础反转写得滚瓜烂熟,再理解区间反转要保留哪些前置节点的线索,后面这些题其实都是同一套思想的排列组合。我个人建议,反转链表一定要背着写十遍以上,因为它真的是链表题的分水岭。

4.3 单调栈:处理“下一个更大元素”的利器

栈的题里,最容易被忽略、但面试常考的,就是单调栈。它专门用来处理一类问题:当前位置的下一个更大元素、左侧第一个比当前小的元素,或者类似的高度/温度问题。暴力做法是两层循环,但单调栈能用O(n)的时间解决。

单调栈的思路其实很好理解:维护一个栈,栈内元素按照某种单调性排列(从栈底到栈顶递增或递减)。当新元素入栈时,不断弹出破坏单调性的栈顶元素,被弹出的元素,其“下一个更大元素”往往就是这个新元素。这个特性,正好命中我们要找的答案。

def nextGreaterElement(nums): stack = [] ans = [-1] * len(nums) for i in range(len(nums) - 1, -1, -1): while stack and nums[i] >= stack[-1]: stack.pop() if stack: ans[i] = stack[-1] stack.append(nums[i]) return ans

这里选择从右往左遍历,原因是单调栈维护的是右侧元素的信息,从右遍历时,栈内天然就只包含当前元素右侧的候选值。弹栈的条件是当前元素比栈顶大,这样留在栈里的始终是单调递减序列,栈顶永远是离当前元素最近的更大值。你要是觉得从右往左不好理解,也可以从左往右遍历,用栈存下标,弹出时记录答案,效果等价,只是写法稍绕。我建议你先死磕一种写法,把它背下来,另一种理解即可。

单调栈的变种题包括:每日温度、接雨水、柱状图中最大的矩形、去除重复字母等。这些题看似千差万别,但只要你能识别出“找左右边界/找下一个更大更小”这类特征,就用单调栈往里套。

5. 二叉树:递归就是天然的模板

5.1 二叉树遍历模板

二叉树的三种深度优先遍历(前序、中序、后序)和一个层序遍历,简直可以说是递归模板的教科书。前序遍历的递归写法只有几行:

def preorderTraversal(root): if not root: return [] return [root.val] + preorderTraversal(root.left) + preorderTraversal(root.right)

中序和后序,只需要调整一下拼接顺序。很多同学觉得这太简单了,没必要练,但真正面试时会发现,考得多的不是遍历本身,而是基于遍历模板的升级题:最大深度、路径总和、最近公共祖先、二叉树展开为链表。而且这些题的解法,往往就是在递归遍历的骨架中加几个状态变量。

在使用递归模板时,有两个点必须养成习惯。第一,递归出口的写法一定是先判空,再处理子树,否则你会在空节点上访问属性直接报错。第二,要区分“处理根节点”和“处理子树结果”的顺序,这直接决定了你是前序、中序还是后序逻辑。

5.2 递归改迭代:手动维护栈

递归虽然好写,但某些场景下会爆栈,或者面试官会刻意考察你能否用迭代实现遍历。前序遍历的迭代版,核心是显式维护一个栈。先把根节点入栈,每次弹出栈顶节点时访问,再把右孩子和左孩子依次入栈。注意顺序是先右后左,因为栈是先进后出,右孩子先入栈会最后才被访问,这样就保证了左孩子先被处理。

def preorderTraversalIterative(root): if not root: return [] stack = [root] ans = [] while stack: node = stack.pop() ans.append(node.val) if node.right: stack.append(node.right) if node.left: stack.append(node.left) return ans

为什么要手动写迭代版?因为很多二叉树难题,比如中序遍历的迭代版,是后面研究树的线索化、二叉搜索树迭代器的基础。你能不能用显式栈模拟递归,很大程度上反映了你对递归调用过程的理解深度。如果你递归版已经写得很顺,建议把前序、中序、后序三个版本的迭代写法都过一遍,这比多刷五道新题更有价值。

5.3 回溯算法:拿树当扩展示例

回溯算法是递归的另一种表现形式,专门用来解决排列、组合、子集这类搜索问题。它本质上是一棵“决策树”的深度优先遍历:每一层递归对应一个决策点,选择了一个分支后,递归进入下一层,关键是递归结束后要撤销选择,也就是“回溯”这个词的来源。

def subsets(nums): res = [] path = [] def backtrack(start): res.append(path[:]) for i in range(start, len(nums)): path.append(nums[i]) backtrack(i + 1) path.pop() backtrack(0) return res

子集问题的这个模板非常经典。每次进入回溯函数,都把当前path的副本加入结果集。循环里做选择、递归、撤销选择。为什么用path[:]而不是path?因为append存入的是引用,之后path一变,结果集里所有副本都会跟着变,这是新手最容易踩的坑。必须拷贝一份快照,这个细节我在很多人的代码里都见过。

如果把子集问题改成全排列,那循环的起始位置不再是从i+1开始,而是每次递归都要从头扫描,用visited数组标记哪些元素已经用过。改成组合总和,则需要加入排序和剪枝。代码骨架不变,变的只是约束条件。这也是为什么我说回溯题千万不要去背具体题目,而要把这个“选择-递归-撤销”的铁三角模板背下来。

6. 动态规划:从模板到状态设计

6.1 动态规划五步法

动态规划大概是所有题型里劝退率最高的。但真正拆解开来,它确实有相对固定的分析流程,我习惯叫作“动规五步法”:确定dp数组的含义、确定递推公式、初始化dp数组、确定遍历顺序、打印dp数组验证。五步走下来,大部分简单的DP题都能被按在地上摩擦。

拿最经典的爬楼梯来说,dp[i]表示到达第i阶的方法数。递推关系是dp[i] = dp[i-1] + dp[i-2],因为最后一步要么跨一阶,要么跨两阶。初始化dp[0] = 1,dp[1] = 1。遍历顺序自然是从小到大。这个dp定义一旦建立,代码几乎就是照抄:

def climbStairs(n): dp = [0] * (n + 1) dp[0] = 1 dp[1] = 1 for i in range(2, n + 1): dp[i] = dp[i - 1] + dp[i - 2] return dp[n]

动归题最大的难点不是写代码,而是“状态怎么设计”。同一个问题,用一维dp还是二维dp,直接决定了代码的复杂度和能不能AC。我的经验是:先看题目是求最大/最小还是方案数,再看有几个变量在变化。一个变量就用一维dp,两个变量就用二维dp。比如背包问题是典型的二维状态dp[i][j],i表示考虑前i件物品,j表示当前容量。

6.2 二维DP与背包模板

背包问题的模板是动态规划里的常青树,面试考它的频率相当高,尤其是01背包。所谓01背包,就是每件物品要么拿要么不拿,最多拿一次。dp[i][j]表示前i件物品在容量为j时能装下的最大价值。递推公式是dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]),意思就是:不拿第i件,或者拿了第i件但腾出它的重量。

我们可以把二维dp优化成一维dp,把容量维度倒过来遍历,防止同一个物品被多次放入:

def zeroOnePack(capacity, weight, value, n): dp = [0] * (capacity + 1) for i in range(n): for j in range(capacity, weight[i] - 1, -1): dp[j] = max(dp[j], dp[j - weight[i]] + value[i]) return dp[capacity]

请注意,这里内层循环是倒序的,这是01背包的精髓所在。如果你正序遍历,那dp[j - weight[i]]可能已经包含了当前物品,就变成完全背包了。很多新手在背包问题上翻车,就是对这一行循环顺序的理解不够深刻。建议自己手推一遍dp数组的变化过程,彻底搞明白为什么倒序不会重复取物品。

滑动窗口式优化不是必须掌握的,但至少要知道dp数组的维度可以压缩,这在你处理超大容量时会很有用。面试中能把空间复杂度从O(n*m)优化到O(m),本身就是一道很好的加分题。

6.3 打家劫舍、最长递增子序列的变体

打家劫舍系列是DP里比较轻松的一类,它的核心约束是“相邻房子不能同时偷”。dp[i]表示偷到第i个房子时的最大金额,递推公式为dp[i] = max(dp[i-1], dp[i-2] + nums[i])。如果你能把这个递推公式的语义理解清楚,后面改成环形打家劫舍,只是把数组拆成两条子串分别DP,本质上解法完全一致。

最长递增子序列则是一个典型的“结尾定义法”DP题。dp[i]定义为以nums[i]结尾的最长递增子序列长度。对每个i,我们遍历它前面的所有j,如果nums[i] > nums[j],就可以把nums[i]接到以nums[j]结尾的子序列后面,dp[i] = max(dp[i], dp[j] + 1)。这个题还有一个非常重要的进阶版:最长递增子序列的O(n log n)优化,使用patience sorting,用二分维护一个单调序列。面试中如果能写出这个优化,档次一下就上去了。

其实DP没有你想象的那么难,它的核心就是“状态定义 -> 递推 -> 边界 -> 优化”。只要你能静下心来把五步法走全,DP题其实是所有题型里最容易举一反三的,因为递推关系一旦想清楚,代码本身往往只有十几行。

7. 常见问题排查与边界条件

7.1 最容易翻车的几个边界

我总结了一下,刷题提交后报错的情况,90%以上都出在下面这些边界条件上:空数组或空链表、只有一个元素、数字全是负数或零、目标值比所有数都大、要求返回下标时从0开始还是从1开始、递归没有出口或出口条件写错。这些听着容易,实际写代码时经常被忽略。

一个很典型的例子是二分查找。它的标准模板是左闭右开区间[l, r),循环条件写成while (l < r),每次更新是r = mid或l = mid + 1。如果你写成while (l <= r)还用同一个公式,很大概率会死循环。这个细节我在大量候选人代码里反复看到。所以二分查找我建议不要凭印象写,每次都要在心里默念“区间开闭是否一致”和“循环不变量是什么”。

还有滑动窗口题里面的“左指针是否需要回退”。有些场景中,比如找最长子串,left只会向右移动,不需要回退;但有些场景,比如求窗口平均值,用累积和而不是每次重算,否则复杂度就会退化到O(n²)。懂得使用前缀和来维护窗口和,是一个性价比很高的优化习惯。

7.2 超时与递归爆栈的排查思路

如果你在牛客上提交后显示超时,先别急着怀疑是Python太慢。第一步应该检查自己的时间复杂度是不是高于O(n log n)。暴力双重循环在n=10^5级别时基本必炸,这时候你要想的是怎么用哈希表、双指针、前缀和来优化。

递归爆栈则另有原因。Python默认的递归深度大约是1000,如果树的深度比较深或递归调用次数多,就会触发RecursionError。解决方法有三个:一是把递归改成显式栈的迭代写法;二是用sys.setrecursionlimit(10000)调高限制;三是检查是否真的需要递归,很多图论题的DFS完全可以用栈改写。我个人更推荐前两个方案结合使用,毕竟迭代写法也能帮你更透彻地理解递归结构。

但有一点我必须提醒:调高递归深度只是权宜之计,如果题目的数据范围很大,递归本身的空间复杂度可能已经不符合要求。所以遇到爆栈,除了改写法,也要想是不是应该用BFS或者压缩状态来替代DFS。

7.3 常见问题速查表

为了你刷题时方便排查,我把最常见的问题和解决方向整理成一个速查表:

报错/现象可能原因排查方向
IndexError或者指针溢出数组索引越界,或者链表指针走到了None检查循环边界条件和while判断,先判空再访问
结果错误但逻辑看似正确初始化值不对,或dp初始状态错误检查dp[0]、dp[1]等初始值,测试最小用例
超时暴力O(n²)解法在长输入下挂掉换哈希表、双指针、前缀和、滑动窗口优化
递归深度超出递归出口缺失或调用层级过深补出口,或改成迭代栈
同一结果输出多次回溯时忘记撤销选择,或使用了可变对象引用添加path.pop(),使用path[:]拷贝快照
滑动窗口答案偏大/偏小窗口收缩时机不对确认是先收缩后统计还是先统计后收缩
二分死循环区间开闭与mid更新不一致固定一种模板,循环不变量保持一致

这张表不是让你死记,而是当你的代码出现问题时,先有一个大概的排查方向。多年的刷题经验告诉我,最快的提升方式并不是写更多新题,而是认真对待每一次报错。把报错原因搞清楚,在下一次写代码时主动避免,你的代码质量就会指数级上升。

8. 模板速查:可以直接抄的高频解法骨架

这一节,我把前面讲过的模板集中整理一下,方便你面试前快速翻阅。每一块我都尽量精简成“填进去就能跑”的骨架,但请注意,你自己的理解才是灵魂,背模板最多只是应急。

8.1 数组与字符串类

哈希表查补数模板、前缀和统计模板、滑动窗口最长无重复模板,这三块是数组题的核心。

两数之和的哈希模板,核心是先查需求再存当前。 前缀和统计子数组个数,核心是维护出现次数的哈希表并初始化{0:1}。 滑动窗口最长无重复子串,核心是右指针扩展、while收缩、每次更新答案。

这三套代码我在前面的章节已经给出完整写法,你可以直接抄进自己的笔记。面试的时候如果碰到和它们类似的题,先把模板默写出来,再根据题目要求调整条件判断。

8.2 链表与栈类

链表题最值得背的模板有三个:反转链表、快慢指针找中点、哑节点删除节点。反转链表的迭代写法是三个指针轮转,找中点的快慢指针则是一个步进一、一个步进二,哑节点主要用于删除头节点或边界统一。

栈类模板最核心的是单调栈。不管是从右往左维护候选,还是从左往右弹出记录答案,核心都是维护栈内单调性。遇到“下一个更大”“上一个更小”这类关键词,直接用。

8.3 二叉树与动归类

二叉树的前中后序遍历模板、层序遍历队列模板,值得手写到形成肌肉记忆。尤其是前序和后序在递归中的差别,很多人会搞混,建议用一道最简单的题反复确认。

回溯模板自带共性,选择 -> 递归 -> 撤销选择。子集、组合、全排列都是这个骨架的变体。做选择前排序可用来去重,递归结束后pop可用来撤销状态。

动态规划的五步法不需要代码模板,因为它更像一种思维方式。但01背包的倒序遍历、最长递增子序列的双层循环递推,这两个代码骨架可以背熟。我再次强调,背代码是为了理解,不是为了应付,理解透彻后写出来的代码才是你自己的。

9. 我在实际刷题中的一点体会

聊了这么多模板和细节,最后说点掏心窝的话。我带过不少从零开始刷题的人,也见过很多刷了三四百道题却依然没底气的案例。差别出在哪里?我发现他们大多数是“刷题机器”,刷完就丢,没有总结,没有模板,没有边界意识。而真正能在大厂面试和技术面中稳定发挥的人,往往只刷了两百道左右,但每一道都扎扎实实拆解过,能讲出为什么要这么写,不能那么写。

所以,我给所有准备刷题的人一个最真诚的建议:别追求数量,追求质量。拿两数之和这道看似最简单的题举例,你要是能把它跟“连续子数组和”“三数之和”“四数之和”“和为K的连续子数组”串起来,形成一个知识网络,那这道题在你手里就发挥出了十倍的价值。这种串联能力,才是刷题真正带给你的东西。

我在实操中还有一个很私人的习惯:每周末拿出一个小时,不写新题,只把本周做过的题的模板重新默写一遍,再把自己写错的边界条件单独抄到一个本子上反复看。坚持几周之后,你会明显感觉到写代码的手感和思路都清晰了很多。这个“错题本思维”,比多刷十道题管用得多。

如果你非要问我最快进步的捷径是什么,我的回答是:把一道题从暴力 → 优化 → 边界 → 变体全部吃透,再开始下一道。先慢后快,才是效率最高的路。

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

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

立即咨询