其实刷LeetCode这件事,我一直觉得最大的门槛不是“算法难”,而是“坚持难”。Hot100这套题单能在面试圈里经久不衰,不是因为题目本身有多炫技,而是它精准覆盖了面试中最高频的考点模型——链表操作、二叉树遍历、双指针、滑动窗口、动态规划、BFS/DFS、栈与单调栈,几乎每一道都能在真实面试题里找到影子。我自己在准备面试期间,把这100题来回刷了两轮,第一轮按标签归类刷,第二轮直接随机打乱重刷,每一轮都会有新的收获。
在开始分享具体笔记之前,先聊聊我对Hot100这套题单的整体看法和使用策略。这篇文章会持续更新,我把自己在刷题过程中的思路拆解、Java实现时踩过的坑、以及零散但高频的语法点全部沉淀在这里,希望能给正在刷题的朋友一些参考。
1. 刷题前的准备:Hot100 的正确打开方式
1.1 为什么首选Hot100而不是按题库顺序刷
很多人一开始刷LeetCode喜欢从第1题开始按顺序往下做,我试过这个方法,大概坚持到50题左右就放弃了。原因很简单:题库顺序完全是按题目编号排的,难度不会线性增长,知识点也是东一榔头西一棒槌,你刚把链表搞明白,下一道题直接跳到动态规划,挫败感很强。
Hot100这套题单的价值在于它按“高频”筛选了题目,而且这些题分布在各类核心知识点上。刷它的正确姿势不是按编号刷,而是按数据结构和算法标签分类刷。比如这周只刷链表类,下周只刷二叉树类,让同一种解题模式在脑子里形成肌肉记忆,比每天换花样要有效得多。
我在第一轮刷题时给自己定的规则是:
- 按照数组、链表、二叉树、回溯、DP、图论等标签分批刷,每批5-7天
- 每道题先独立思考30分钟,想不出来再看题解,绝不直接看答案
- 每道题做完后写一个简单的思路备注,哪怕只有两三句话
- 每个周末把本周刷的题重新快速过一遍,重点看备注和易错点
这样走完一轮大概需要2到3个月,节奏是每天1到2道题。不要贪快,质量永远优先于数量。
1.2 刷题用的核心工具与模板
工欲善其事,必先利其器。我先说下我的Java刷题环境:本地IDE用IntelliJ IDEA,配合一个Github仓库管理刷题代码,写完后直接粘贴到LeetCode编辑器里提交。这样做的理由是LeetCode自带的编辑器对调试不友好,尤其是涉及到链表、树这类需要可视化结构的数据类型,本地IDE配合断点调试效率高得多。
还有一个小建议:建立自己的模板代码库。刷题过程中你会发现很多代码是可以复用的,比如二叉树的前序/中序/后序遍历、二分查找的边界模板、滑动窗口的固定框架、背不过的DP状态转移公式等等。我把这些模板单独存成一个Java文件放在项目里,像是在建一个自己的算法工具箱。这是一个很大的效率提升点,因为面试场景下时间紧迫,你不可能现场去推导一个二分查找的边界条件,肯定是直接用熟悉得不能再熟悉的模板。
在我的模板库里,必存的东西包括:
- 二分查找的标准模板和“寻找左右边界”变体
- 链表反转的迭代与递归两版
- 二叉树遍历的递归与非递归实现
- 拓扑排序的BFS框架
- 并查集模板
- 快速幂算法
- 前缀和与差分数组模板
有了这些基础操作模板,面对中期难度的题目时,你的大脑CPU主要用来思考“这道题该套哪个模板”,而不是纠结“这个模板怎么实现的”。
2. 数据结构类题目的Java实现要点与易错点
2.1 链表题的迭代思维:从反转链表到K个一组翻转
链表在Hot100里出现的频率极高,因为链表能考察指针操作的熟练度和对引用关系的理解。Java的链表节点定义通常是ListNode,包含val和next两个字段,刷题时不需要引入新的依赖包,直接用题目给定的类即可。
我刷链表题最大的体会是:链表题的解题核心不是代码量,而是把每一步的指针变动在纸上画清楚。以反转链表为例,最经典的pre, cur, next三步走,很多人在代码里写了十几行但依然出错,原因是没想清楚三个指针在每一轮循环结束后的状态。反转链表有个很好用的规律:“改指针前先备份后继”,因为一旦改变了一个节点的next方向,原本的后继节点就找不到了。
Hot100里关于链表的题目分散在各个难度区间。简单的如“相交链表”、“环形链表”,核心思路都是快慢指针或者双指针。难一点的如“合并K个升序链表”,这个题最优雅的做法是优先队列(PriorityQueue),每次取出最小的那个头结点加入结果,再把该节点的后继放回队列中。Java的PriorityQueue默认是小顶堆,刚好符合这道题的需求。
链表类题目里我特别想说一个细节:很多题如果直接遍历会超时,需要用到“快慢指针”来优化成O(n)。比如找链表中倒数第K个节点,先让快指针走K步,然后快慢指针同步走,快指针到达尾部时,慢指针正好在倒数第K个位置上。这个技巧看着简单,但实际写代码时很容易被K的边界条件(K为0、K等于链表长度)整晕,建议在本地多跑几个边界用例。
2.2 二叉树遍历的递归与非递归:前中后序的模板记忆术
二叉树的题目在Hot100里是重头戏,不只是因为它涉及递归思想,更因为很多复杂题都是在遍历框架上做文章。递归遍历是最容易写的,三步法:退出条件、递归左、递归右。但很多面试官会追问“不用递归怎么写”,这时候非递归遍历就显得非常重要。
我整理了一个记忆非递归遍历的方法:前序和层序用栈或队列处理时顺序是相对直观的,中序和后序则需要利用栈的迭代替换递归过程中的回溯节点。具体来说,中序非递归的模板是:不断把左孩子压栈,出栈时访问节点,然后转向右孩子继续这个过程。后序非递归稍微绕一点,最简洁的方式是使用“反转前序”的思路——先按“根右左”的顺序遍历,最后整体反转结果。
这份整理是不是觉得复杂?其实写代码不需要背,理解了栈模拟递归的“上下文恢复”逻辑就水到渠成了。我建议想做对二叉树类题目,先能手写非递归的前序和中序,这是最低门槛。后序可以直接在面试时表示“这里我用递归实现”,一般不会扣分。
二叉树另一个高频模型是层序遍历(BFS),用队列实现,每轮循环记录当前队列长度size,然后弹出size个节点处理。这个模式在“二叉树的右视图”、“二叉树的层序遍历”以及图论里的BFS最短路径中都用得上。我把这个模板固定在了自己的模板库里,遇到相关问题直接粘贴改造。
2.3 数组与矩阵:双指针和前缀和的灵活应用
数组类是Hot100里数量最多的,题目的“套路化”程度也最高。这里说的双指针不是简单的首尾指针,而是一个抽象概念:在遍历过程中维护两个“哨兵”位置,通过它们的移动缩小问题规模。经典例子是“盛最多水的容器”和“三数之和”。
以“三数之和”为例,这道题给我留下的教训是去重操作必须在循环内实时做,而不是最后靠Set去重。如果你把所有结果存在HashSet<List<Integer>>里再去重,会面临两个问题:一是内存浪费,二是List的哈希逻辑基于元素顺序,如果两个List元素顺序不同但内容相同,哈希值也会不同,去重效果就不稳定。正确做法是排序后固定一个数,再用双指针找另外两个数,同时跳过重复元素。
前缀和也是数组题中隐藏的常用技巧。Hot100里的“和为K的子数组”和“区域检索”都用到了前缀和思想。我第一次做“和为K的子数组”时直接用了双重循环,时间复杂度O(n²),数据量一大就超时。后来才意识到这种“连续子数组之和比较”的问题,本质上是把sum[j] - sum[i]转化为在哈希表中查找sum[i] - k是否出现过。这个题我建议仔细体会,它是从暴力到优化的一个典型思维跃迁。
3. 高频题型拆解:从暴力到最优解
3.1 滑动窗口的框架与套路
滑动窗口是Hot100中“字符串”和“数组”题型的常客,它解决的是一类固定模式的问题:“在一个序列中找到满足某种条件的最短/最长连续子数组(子串)”。这类题如果用暴力枚举,时间复杂度很容易到O(n²),但是用滑动窗口可以降到O(n)。
滑动窗口的核心是四个变量:left、right(窗口左右边界)、count/map(用于记录窗口内元素状态)、result(暂存最优解)。我去刷题时给自己规定了一个固定动作:每次移动right扩展窗口,检查窗口是否满足条件,不满足则移动left收缩窗口,直到窗口恢复合法状态。这个“扩展-收缩”的过程非常模式化。
Java在这个场景中常用HashMap记录字符出现的次数(当题目涉及重复字符时),或者直接使用int[256]这样的字符计数数组,后者速度更快但可读性稍差。我有次在“无重复字符的最长子串”这道题上WA了几次,原因就是我先更新了left再更新了maxLength,导致漏掉了一种边界情况。后来我把“先保证窗口合法,再计算当前最优解”这个顺序写成了固定步骤,每次都先移动窗口、再统计最优值,这个错误就再也没有出现过。
3.2 动态规划与状态机:二维DP的实现细节
动态规划是Hot100里最容易让人心态爆炸的题型。不是因为它考察的数学思维有多强,而是很多时候你确实想破了脑袋也想不出状态转移方程。这里我给三条可执行的经验:
第一,DP题先不要直接想代码,先在纸上定义出状态的含义。比如“编辑距离”这道题,状态定义为dp[i][j]表示word1的前i个字符转成word2的前j个字符所需的最少操作数。一旦这个定义清晰了,转移方程基本就是照着这个定义去讨论“最后一个字符是否相等”的两种情况。
第二,二维DP的边界条件极容易出错。我吃过很多亏的地方是二维数组的dp[0][j]和dp[i][0]的初始化。这类“哨兵行/列”的初始值往往不是0,而是根据实际业务意义推导出来的,比如编辑距离里dp[i][0] = i就代表“把一个长度为i的字符串删空需要i次操作”。所以我建议每次写完二维DP后,先用小数据跑一遍,逐个检查边界行和列的值是否合理。
第三,滚动数组优化要谨慎用。很多二维DP可以用滚动数组把空间复杂度从O(n²)降到O(n),但这样做会让代码的可读性变差,而且如果转移方程里同时依赖i-1行和i行左侧的元素,滚动数组的更新顺序就需要注意,否则会覆盖旧数据。“最小路径和”和“不同路径”这类可以用一维数组优化,但编辑距离这类依赖“左上角”的题,滚动数组就需要额外用一个变量保存左上角的旧值。建议先把O(n²)版本的代码跑通,再考虑优化。
3.3 回溯与剪枝:Java集合引用的坑
回溯算法是年轻程序员最容易写错又好几天找不出问题的一类题目。核心思想是“尝试-递归-撤销”,这个“撤销”动作是绝大多数人的易错点。
拿“全排列”来说,如果每次递归都新建一个List传入下一层,那就不存在“撤销”问题,但代价是内存消耗大。更常见的写法是使用一个“路径”List,递归完成后做path.remove(path.size()-1)来撤销刚才的选择。这个撤销操作很容易忘,一旦忘掉,你会发现结果里混入了非常奇怪的重复排列。
在Java里还有一个隐藏的坑:递归中如果把当前路径直接加入结果集,必须做拷贝。我有一次写“子集”这道题,直接result.add(path),结果最后result里全是空列表。这是因为path是同一个对象的引用,后续的递归修改会直接改动已经存入result中的内容。正确做法是result.add(new ArrayList<>(path)),先拷贝一份当前路径的快照再存入结果集。这个坑我用了一次就记到了死,希望看到这篇笔记的你能直接避开。
回溯的剪枝也很重要。适合用used数组或boolean[] visited来标记哪些元素已经被选取过,如果题目要求结果不重复(比如包含重复元素的数组求子集),还需要先排序,再通过判断i > 0 && nums[i] == nums[i-1] && !visited[i-1]来跳过同一层级的重复分支。
3.4 BFS与图的遍历:队列解最短路径
Hot100里的“腐烂的橘子”、“单词接龙”、“岛屿数量”都是BFS的绝佳练习素材。BFS的关键在于“逐层向外扩展”,而这一特征用队列实现时,每次需要先记录当前队列的大小,再一次性消耗完这一批元素,这样就能把“层”的概念从队列中显式剥离出来。
关于“腐烂的橘子”,我看到网上很多讨论都在纠结如何设计visited数组或如何保存坐标。其实这道题有个更简洁的思路:先把所有初始腐烂的橘子入队,同时统计新鲜橘子的数量。接着BFS扩散,每一轮扩散后更新新鲜橘子的数量,当队列为空时,如果新鲜橘子总数大于0,则说明有些橘子永远不会被腐烂,返回-1。如果都为0,则返回扩散的轮数。
BFS在Java中实现时,二维坐标的矩阵输入通常用int[]表示,即x = point[0],y = point[1]。方向数组int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}}几乎可以说是所有网格图BFS的标准配置,建议直接写熟。
4. Java语法与易错点:刷题之外的隐形分数
4.1 集合框架的选用时机与性能差异
Java刷题有个区别于Python/C++的特点——集合类的选择对性能影响极大。我最早刷题时喜欢一切皆ArrayList+HashSet,但后来发现有些场景用LinkedList或ArrayDeque会更合适。
这里给出我常用的选型经验:
| 场景 | 推荐集合 | 理由 |
|---|---|---|
| 频繁头尾增删 | ArrayDeque | 双向队列,O(1)的头尾操作 |
| 需要按下标随机访问 | ArrayList | 底层数组,随机访问O(1) |
| 频繁在中间插入/删除 | LinkedList | 链表结构,不需要搬移元素 |
| 元素去重且无序 | HashSet | 哈希表实现,O(1)查找 |
| 保持插入顺序的去重集合 | LinkedHashSet | 底层再维护一条链表 |
| 需要有序的输出 | TreeSet | 红黑树实现,按自然序迭代 |
另外还有一个日常刷题中容易忽视的性能点:ArrayList初始化时就指定容量。很多人在Hot100里处理较大的测试用例时突然遇到TLE,不是算法复杂度不对,而是反复扩容导致常数过大。建议提前估算容量,比如new ArrayList<>(nums.length),尤其当你已知结果规模的大概上限时,这一行代码能省下不少时间。
Java的String相关的题也有大坑。刷字符串类的题如果涉及大量拼接,千万不要用String +=,因为字符串是不可变对象,底层每一次拼接都新建一个对象,复杂度很可能退化成O(n²)。正确做法是用StringBuilder的append方法。面试场景下你熟练使用StringBuilder这本身也是一个加分项,它说明你了解Java字符串的基本机制。
4.2 排序、比较器与lambda表达式
Java的Arrays.sort()和Collections.sort()是刷题时的必备工具,但默认的自然顺序往往不能满足题目要求,你需要自定义比较器。Java 8以后最顺手的方式就是lambda表达式,例如:
// 按区间起点升序排序 Arrays.sort(intervals, (a, b) -> a[0] - b[0]);这里要特别提醒:lambda里直接返回两个数的差值,在处理极限数值时可能出现整数溢出问题。比如按分数降序排列(a, b) -> b[1] - a[1],如果b[1]是Integer.MAX_VALUE,a[1]是负数,减法直接溢出了。稳妥写法是使用Integer.compare(b[1], a[1])或者Comparator.comparingInt(...)链式调用。
对二维数组按第二列排序、对List<int[]>排序这类需求,我在Hot100的“合并区间”、“会议室”这类题中反复用到。可以提前把这种自定义排序lambda写法练到闭着眼睛都能写出来,现场面试时是非常好的“印象分”。
4.3 位运算与特殊进制转换
Hot100里有些题目用位运算解法非常优雅,比如“只出现一次的数字”利用异或运算的交换律和结合律,把成对出现的数字全部抵消掉,剩下的就是那个唯一出现一次的数字。用Java写这题只需一个循环异或即可。
“基本计算器”这类题虽然主体是栈,但解析数字时也常需要用到进制累积技巧:遍历字符串中的数字字符时,用num = num * 10 + (ch - '0')逐步把字符转换成真正的数值。这个写法看似简单,却经常在“数字后面跟着运算符”的边界处理时出错,需要配合判断当前字符是否已经到达表达式的末尾来做收尾操作。
关于二进制和位运算,Java中&,|,^,~,<<,>>,>>>这几个运算符含义和优先级与C系语言一致,刷题需要用到时留意一下无符号右移和普通右移的区别,比如处理负数时>>保留符号位,而>>>不保留。在第一轮刷题时,可以刻意挑几道位运算题练手,你会慢慢发现二进制直觉的培养对很多题都有帮助。
4.4 经典易错点清单(实时更新)
把我在刷题过程中反复踩过的坑汇总成一个清单,每次代码提交前我都会快速过一遍这些点,真的能拦截掉不少WA:
- 数组越界:循环写
i <= nums.length的经典手误;二维数组访问时的行列搞混 - 空值处理:链表题不判断head为null,二叉树题不判断root为null,字符串题不处理空串
- 负数取模:Java
%的符号与左操作数一致,-5 % 3结果是-2而不是1,部分数学题会遇到 - Integer缓存:“
Integer比较用==”这个误区,面试八股常考,刷题时如果要用HashMap做计数,键永远用基本类型包装类而要谨慎比较 - 栈的peek和pop混淆:
peek()只看不取,pop()取走并删除,很多人写BFS/DFS的代码时一时手滑 - 递归的返回值类型不匹配:有的递归函数应该有返回值,你却把它设计成了void,导致父层拿不到子层的结果
- 字符与数字互转:
'0'的ASCII码是48,用ch - '0'而不是Integer.parseInt(ch + "") - String不可变性:字符串做内层循环修改时还是用
char[]或者先转为StringBuilder - List去重的哈希冲突:前面提到过的
add(new ArrayList<>(path)),切忌直接加入引用 - Comparator返回值为0时的稳定性:排序结果可能不稳定,如果题目要求多关键字排序,要用
thenComparing链式指定
这些都写在了我的笔记置顶位置,刷题两轮之后,对所有问题点基本都有印象了。
5. 两道Hot100经典题的完整手撕过程
5.1 “爱吃香蕉的狒狒”:二分查找里的边界细节
“爱吃香蕉的狒狒”这道题在热搜里被很多人关注,是二分查找在“最小化最大值”场景下的经典应用。题目核心是:给定一堆香蕉堆和总时间H,求最小速度K,使得在H小时内吃完所有香蕉。每堆香蕉如果你用速度K去吃,如果数量小于K也要花1小时整。
这个问题的关键词是“最小速度K”,并且满足单调性:速度越大,吃完所需的总时间越短。这种“某种条件随参数增加而单调变化”的场景,是非常典型的二分查找信号。
我第一次做这题时,用了二分模板的“左闭右闭”形式,但卡在了两个点上:
第一个是上界选多少。一开始我直接选了max(piles)作为上界,后来发现在某些特殊测试用例中可能不够,因为虽然有香蕉堆的数量上限,但总时间H可以很小,这时K可能需要大于最大堆的香蕉数吗?仔细分析:如果K已经大于最大堆的香蕉数,那么每堆香蕉都只花1小时吃完,总时间等于堆数。若H大于堆数,那K确实不必超过最大堆;但若H小于堆数,则没有解,题目也保证有解存在。因此上界设置成max(piles)是安全的,但从工程角度我一般习惯取max(piles),因为这已经是真实边界。
第二个是时间计算函数。对每堆香蕉pile,吃得时间(pile + K - 1) / K这个向上取整的写法就是“除以K并向上取整”的经典表达式。我第一次用Math.ceil((double)pile / K)来处理,结果在小数据上没问题,但数据量变大时浮点数精度会产生偏差,提交偶尔WA。换成整数运算(pile + K - 1) / K之后,问题彻底消失。这也是一个小经验:刷题时能用整数运算替代浮点就直接用整数运算,浮点的不精确性本来就是算法题里的一个坑。
贴一下核心代码(Java版):
public int minEatingSpeed(int[] piles, int h) { int left = 1, right = 0; for (int p : piles) { right = Math.max(right, p); } while (left < right) { int mid = left + (right - left) / 2; if (canFinish(piles, mid, h)) { right = mid; // 尝试更小的速度 } else { left = mid + 1; } } return left; } private boolean canFinish(int[] piles, int speed, int h) { int time = 0; for (int p : piles) { time += (p + speed - 1) / speed; if (time > h) return false; // 提前退出,避免int溢出 } return time <= h; }注意这里canFinish提前退出的判断很重要,不仅能加快速度,还能防止time累加溢出。left + (right - left) / 2的写法是为了防止两个大整数相加时的溢出问题,也是面试官比较喜欢看到的细节。
5.2 “腐烂的橘子”的BFS轮数计算技巧
这题我在开头提过基本思路,这里完整展开说一下。给定一个m x n的网格,0代表空单元格,1代表新鲜橘子,2代表腐烂橘子。每分钟腐烂的橘子会污染上下左右相邻的新鲜橘子,问多少分钟后网格中所有橘子都腐烂,或者返回 -1。
这题最容易出错的不是BFS本身,而是轮数(分钟数)的统计方式。如果你在每层出队时记录steps++,那你要很小心初始状态的处理:当初始腐烂橘子已经存在但还需要1分钟才能感染邻居时,你是要用steps还是steps-1?这里很容易多算一分钟或者少算一分钟。
推荐一个干净的思路:BFS开始前把所有的腐烂橘子入队,记录fresh的数量。每一轮循环中先把当前队列的size固定下来,然后只处理size个节点,这就是“这一分钟内会发生的腐烂传播”。处理完后minutes++。当队列为空时,检查fresh是否为0,如果不为0说明有孤岛橘子,返回 -1;否则返回minutes。
这个写法的好处是“层数”和“时间”天然对应,代码不容易出错。核心代码如下:
public int orangesRotting(int[][] grid) { int m = grid.length, n = grid[0].length; Queue<int[]> queue = new LinkedList<>(); int fresh = 0; for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (grid[i][j] == 2) { queue.offer(new int[]{i, j}); } else if (grid[i][j] == 1) { fresh++; } } } if (fresh == 0) return 0; int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}}; int minutes = 0; while (!queue.isEmpty()) { int size = queue.size(); boolean changed = false; for (int i = 0; i < size; i++) { int[] cur = queue.poll(); for (int[] d : dirs) { int nx = cur[0] + d[0]; int ny = cur[1] + d[1]; if (nx < 0 || nx >= m || ny < 0 || ny >= n || grid[nx][ny] != 1) { continue; } grid[nx][ny] = 2; fresh--; queue.offer(new int[]{nx, ny}); changed = true; } } if (changed) minutes++; } return fresh == 0 ? minutes : -1; }这里的changed布尔变量是可选的,它的作用是防止最后一轮BFS没有实际感染却依然增加分钟数。如果你用“分层BFS+while队列非空”的结构,有的参考写法里会在每次外层循环结束直接minutes++,这时候最后一轮队列为空的情况会多算1分钟,所以我更推荐加一个changed判断。
BFS还有一个前置注意点:入队时就标记grid[nx][ny] = 2,而不是出队时再标记。否则会发生同一个新鲜橘子被多个腐烂橘子重复入队的情况,导致结果偏大或者死循环。这一点在“岛屿数量”这类题中也同样适用,标记访问的动作必须放在入队时。
6. 刷题节奏与面试准备心得
6.1 刷完Hot100一轮后如何自测
一轮刷完不等于万事大吉。我自己在一轮结束后做的第一件事是把所有题目标题列成一个表格,标出每道题当时的AC时间、WA次数、关键词标签。然后随机抽出10道曾经WA过的题,不看自己的备注直接重写。这个自测方式非常有效,比按顺序重刷更能检验真实的记忆水平,因为随机抽题切断了“因为上一题是XX类型,所以这题大概是XX类型”的惯性联想。
如果随机抽出的题能在30分钟内AC且没有查看参考代码,说明基本掌握了。如果卡住了,就把这道题标记为“二轮重点题”,几天内再刷一遍。这样循环两轮之后,Hot100的核心题基本都能形成稳定的解题反射。
我也建议大家保留每个版本的提交记录。LeetCode的提交历史能回看每一次WA和AC的代码差异,这是非常好的复盘素材。我有次发现自己的某个题WA了5次,翻看提交记录才意识到每次都是同一个边界没处理好,于是把这个边界抽象成了模板思想写进笔记。复盘的深度决定了刷题的收获上限。
6.2 结合八股文与Java面试准备
刷Hot100和准备Java面试可以并行进行,两者并不冲突,而且互相成就。热搜词里反复出现的“Java八股文”和“Java基础面试题”,本质上和算法题在面试考核中占据的权重是并行的。面试中算法题做得好,极大缓解了后续八股问答的紧张感。
我的做法是每天前半段时间刷算法题保持手感,后半段复习Java基础(集合源码、并发、JVM、Spring等)。特别是刷题过程中频繁用到的HashMap、PriorityQueue、ArrayDeque这些类,我会顺手去翻一下源码,理解底层是数组还是链表、扩容机制是什么。这样做的好处是“以用带学”,你对这些集合的理解能落在地上,回答“HashMap底层原理”这类八股题时,视角会比较实际而不是背诵。
比如很多人在面试中回答“HashMap的put流程”时,如果刷题时经常用size、getOrDefault这些API,你会在理解层面自然更倾向于用“键值对存储和查找”的视角去分析,而不是反复背那几个步骤。这种结合让我在面试时,对于“算法实现里为什么选用这个集合”这类追问非常从容,因为确实是自己天天在用的东西。
6.3 给新手的刷题避坑建议
这一小节的内容想给刚开始刷题的朋友几条实用建议,不浇鸡汤,直接说可执行的方向:
第一,不要在一道题上死磕太久。独立思考30分钟是一个标准的上限。超过这个时间,如果依然没有任何思路,直接看题解并理解其思路,再合上题解自己重新写一遍。这个过程比死磕一道题一天的价值高得多。所谓“刷题”的目的不是证明自己能独立做出来,而是通过反复训练建立解题直觉。
第二,一定要手动跑小样列子。哪怕你已经觉得代码逻辑天衣无缝了,提交前也应该在本地用一个非常小的输入(比如3个节点、4个元素的数组)手动模拟一遍。算法题里大多数WA都发生在边界条件上,而小样本输入是发现边界问题最快的方式。
第三,建立错题重刷周期。我用的是一个很简单的办法:每周末随机抽5道本周做过的题,每道题给20分钟,不AC就下周继续抽。这种做法让错题不会变成“做过就忘”,而是真正地被内化。
第四,重视代码风格和命名。虽然算法题核心是思路,但面试过程中,面试官会直接看你现场写的代码。变量命名清晰(比如left、right、slow、fast、cur、prev)、逻辑结构紧凑、关键步骤有注释,这些习惯需要平时练,真到面试现场你很难临时改变书写习惯。我的经验是:把代码当成写给同事Review的作品来写,思路不只是让自己看懂,还要让阅卷者看懂。
7. 持续更新的维护机制
我会持续把Hot100中更多题目的思路和易错点补充进这篇笔记,形成一个真正能随刷随查的更新型文档。为了维护方便,我的实际做法是:每次用LeetCode的Cookie下载自己的提交记录,导出一个JSON文件,配合脚本统计哪些题目WA次数最多、哪些题的AC时间最长,然后优先把这些题目整理成笔记。
我用了一个简单的维护原则:不只是记录“这道题怎么做”,而是记录“这道题让我明白了什么”。比如做“基本计算器”时我学到的核心是“用栈保存符号状态+延迟计算”,做“柱状图中最大的矩形”时学到“单调栈的本质是寻找左右第一个比当前元素小/大的位置”。当每道题都能浓缩为一两句话的核心启发时,这份笔记就会慢慢变成一本自己专属的算法思维词典,价值远大于只收藏别人的题解。
同时我也会把Hot100之外但和这些题目同类型的高频题(比如周赛中的新题、大厂面试中出现过的变体)顺手补充进来,当作练习延伸。保持这个笔记和题单同步更新,让每一次打开它都有新鲜的参考内容。
最后分享一个小习惯:我会把Hot100的题单导到备忘录里,每完成一道题,就在题目旁边标记当天日期和WA次数。三个月后回头看这个列表,上面记录的不是简单的对号,而是一条清晰的从“易错”到“熟练”的曲线,这种进步感本身就是支撑我持续刷下去的最大动力。希望这篇笔记也能成为你的刷题路程上的陪伴文档,欢迎随时回来查阅更新。