ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LeetCode刷题第10天:算法模板与刷题方法,搭稳基础的分水岭

LeetCode刷题第10天:算法模板与刷题方法,搭稳基础的分水岭

1. 第10天为什么是刷题路上第一道分水岭

1.1 前10天隐藏的节奏规律

先说实话,LeetCode Day10这个节点,放到整条刷题时间线里其实很尴尬。它不像Day1那样有新鲜感,也不像Day30那样能看到明显的题量积累,它恰恰处在一段“热情退潮、正反馈还没到位”的真空期。我自己当年刷到第10天的时候,正好碰到一个尴尬的场景:简单题开始觉得重复,中等题又经常卡住,一打开题解觉得自己是个废物,合上题解又什么都写不出来。

但后来我才意识到,第10天根本不用急着追求“秒杀题目”,它真正的意义在于完成一次节奏校准。如果你只刷了几天,你会觉得算法题靠灵光一现;刷到第10天,应该开始意识到算法题是一门可以拆解的技能,有套路、有模板、有固定的分析路径。这个认知转变,比多刷几题重要得多。

观察那些能坚持过百天的人,前10天几乎都做对了一件事:他们没把刷题当成每天必须完成的KPI,而是当成一个“建立肌肉记忆”的过程。第1到第7天,练的是手感,每天2到3道题,把数组、字符串、链表这些基础结构写熟练;第8到第10天,才开始尝试整理题型、归纳套路。如果你从第1天就开始死磕困难题,大概率在第5天就崩溃了。如果你到第10天还在每个类型之间乱跳,那后面很容易陷入“刷了就忘”的循环。

1.2 第10天应该掌握的最低能力线

我给自己定过一条判断标准:Day10结束的时候,如果还不能独立写出这几类代码,就先不要急着开新题。

第一,数组遍历与原地操作的常规写法,包括循环里维护索引、交换元素、覆盖写入。像“把数组里的0移动到末尾”这种题,即使不熟悉双指针,也要能写出一个能跑的版本。第二,哈希表的基本用法,知道什么时候用字典存索引、什么时候用集合去重。第三,简单递归,能理解二叉树的先序、中序、后序遍历代码是怎么一层层展开的。第四,会算时间复杂度和空间复杂度,至少能说出“这个双层循环是O(n²)”“用哈希表能降到O(n)”这种量级的判断。

如果这四条里超过两条是虚的,那说明前面10天的题基本是“抄题解抄过去的”。第10天最值得做的一件事,就是回头把做过的简单题重新写一遍,尤其是那些当初看了题解才做出来的题。你会发现,能独立写出来和能看懂是两种完全不同的掌握程度。

1.3 第10天最容易崩盘的三种状态

根据我身边的刷题搭子的普遍经历,第10天放弃基本逃不出三种状态。

第一种是“贪多型”,每天恨不得刷四到五道新题,结果每道题都只停留在一知半解。这种状态持续到第10天,大脑里已经塞满了零散的题解片段,遇到新的题目时根本调不出来,挫败感瞬间爆棚。

第二种是“死磕型”,一道中等题卡了三个小时还不肯看题解。不是说死磕不对,但第10天还没有足够的套路储备,死磕的效率极低。更合理的策略是20到30分钟没有有效思路就去看题解,看完之后关掉题解自己重新写一遍,把“看不懂”变成“能复现”,这才是有效学习。

第三种是“飘了型”,连续几天把“简单题秒过”当成自己水平的证明,没有留出时间复盘,也没有尝试写更系统的解法。简单题秒过很多时候只是因为你之前见过类似的题,真的遇到变体就露馅。第10天是个很好的自检时间点,先解决这三种状态,再谈刷题数量。

2. 把每次做题变成一条可复制的流水线

2.1 审题阶段:先别急着码代码

很多人刷题有一个毛病,打开题目看到示例,大概知道要干嘛就开始写,写完发现有两三个边界情况没考虑,改来改去,一个多小时就过去了。我刷到第10天时最明显的进步,就是开始学会“用手推题目”,而不是“用眼睛猜题目”。

一个实用的做法是,在写任何代码之前,把题目提供的示例在纸上手动跑一遍,并额外构造两个自己的用例。比如题目要求返回数组下标,我会先想想:如果数组为空怎么办?如果没有答案怎么办?如果答案可以有多个,题目要求返回哪一个?这些问题在编码前想清楚,能省掉后面大量调试时间。

审题阶段还有一个容易被忽略的细节:确认输入规模。题目里通常会给数组长度、数值范围等约束,这些约束直接决定了你能用的解法复杂度。如果输入规模是10的4次方,那O(n²)的算法很可能超时;如果输入规模只有100,那暴力解法完全可行。第10天起,我会强制自己养成一个习惯:看完题目先看一眼数据范围,再决定写什么复杂度级别的算法。

2.2 暴力解不是浪费时间,是分析的起点

我在第10天之前也犯过一个错误,总觉得写暴力解很丢人,巴不得一上来就写出最优解。后来踩了坑才发现,最优解从来不是凭空想出来的,它是对暴力解进行分析之后逐步改进得到的。

暴力解的真正作用,是帮你验证对题目的理解。先把最直白的方法写出来,哪怕时间复杂度是O(n²)或者O(2ⁿ),只要逻辑正确,就能作为基线。有了这个基线,你才能直观感受到优化的空间在哪里。以“两数之和”为例,暴力版本就是两层循环检查每一对数字,看到这题的第一反应是先写出这个版本,再分析“内层循环到底在做什么”,发现内层循环在做的事是“查找一个值是否存在于数组”,于是很自然地想到用哈希表来优化查找。

如果直接跳到哈希表的解法,你可能只知道“这题用哈希表”,却不理解为什么能用哈希表。一旦题目变成“三数之和”“和可被K整除的子数组”,你照样不知道从哪里下手。所以我建议第10天之后,凡是没思路的题,先逼自己写一个能跑的暴力解,哪怕是TLE也先提交一次。这一步对建立解题直觉特别重要。

2.3 优化方向从哪里找

暴力解写完之后,优化的方向其实有规律可循,最常见的是下面这几类。

第一,减少重复计算。同一个值可能被反复访问或计算,用哈希表、前缀和、缓存等方式存储中间结果。这是最通用的优化方向。第二,减少不必要的遍历。如果数组已经有序,或者你只需要维护某一端的指针,可以用双指针把O(n²)降到O(n)。第三,换一种处理顺序。比如倒序遍历、先排序再处理、用堆维护当前最大值等,这些都属于“改变数据结构或顺序”的优化。

判断优化是否有效的标准很简单:时间复杂度是否降了一个量级,或者空间复杂度是否在可接受范围内。你要记住,空间换时间是算法题里最常见的交易,绝大多数哈希表方案都属于这一类。第10天的时候,能熟练做好这个分析就足够了,不需要追求所有题都秒出最优解。

2.4 复盘比做题更值钱

我做题做了一段时间后,把大部分时间从“做新题”挪到了“复盘旧题”上,正确率提升反而更快。复盘不是简单看一遍题解,而是要把自己的思路和标准解法进行对比,找到分岔点在哪里。

我会用固定模板做记录,大概是四行内容:题目编号和一句话描述;我的第一反应是什么;正解是什么思路;差距出在哪里(是没想到数据结构,还是边界没处理,还是理解错了题意)。这个记录不要写成抄题解,一定要用自己的话重写一遍。你看,第10天如果认真留了这10份记录,后面再复习的效率会翻倍。

另外,复盘时我习惯把一个题目的多个解法都写一遍。很多时候你只写了最优解,却没写过暴力解,导致你对“为什么最优”没有直观感知。两种版本都写一写,复杂度对比才会刻进脑子里。这个动作看起来很费时间,但对长期刷题而言非常划算。

3. 第10天最值得吃透的三套基础模板

3.1 哈希表:空间换时间的第一利器

如果你在Day10前后还不知道什么时候用哈希表,那这10天基本是在无效刷题。哈希表的核心作用就一句话:把查找某个值是否存在的时间从O(n)降到O(1)。在数组相关题目里,它几乎是“对时间复杂度不满时的第一反应”。

我们来写一个最简单也最有代表性的代码。题目是“如果数组中有两个数的和等于目标值,返回这两个数的下标”,暴力解法是两层循环,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 []

这个解法的核心要点是:先查表再入表,这样可以正确处理“每个元素只能用一次”的约束。如果你先把所有数都存入哈希表,再遍历查找,就会遇到“同一元素被使用两次”的问题,比如nums = [3, 3],target = 6时,直接存表也没问题,但nums = [1, 2, 3],target = 2时,先把所有数存进去再查找,可能返回[0, 0],这就是错解。

Day10阶段的哈希表题,不需要做得太偏,把“两数之和”“是否存在重复元素”“最长连续序列”这几种经典场景吃透,就足够支撑你往后继续刷了。

3.2 双指针:数组和链表上的高效遍历

双指针是第10天最容易给人“哇”的套路,因为它能把很多O(n²)的问题降到O(n)。最常见的应用场景有两个:有序数组上的相向双指针,以及数组原地操作的快慢双指针。

以“将数组中的0移动到末尾并保持非0元素的相对顺序”为例,快慢双指针的写法非常简洁。

def move_zeroes(nums): slow = 0 for fast in range(len(nums)): if nums[fast] != 0: nums[slow], nums[fast] = nums[fast], nums[slow] slow += 1 return nums

这里的slow指针指向“下一个非0元素应该放的位置”,fast指针负责扫描所有元素。当fast遇到非0元素,就把它交换到slow位置,然后slow向后移动。交换这一步非常巧妙,它能保留之前已经处理过的非0元素,也能顺带把0被动交换到后面。整个过程只遍历一次数组,时间复杂度O(n),空间复杂度O(1)。

第10天容易犯的错误是把两个指针的语义搞混,比如不知道slow到底指向哪里、什么时候该移动slow。这里分享一个我自己的记忆方法:快指针负责“找”,慢指针负责“存”,“找”到目标之后把值“存”到慢指针位置,慢指针再往前走一步。想清楚这个语义,绝大多数快慢指针题都不会写错。

3.3 递归与栈:二叉树问题的起点

有些人到第10天还在刻意回避二叉树,这是战略失误。二叉树在算法题里的地位非常高,而它的基础无非是递归和栈的配合。前序遍历的递归写法几乎是所有二叉树题目的地基,看下面这段代码:

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

递归的理解难点在于“不要试图展开太多层,要相信函数定义”。你只需要相信preorder(root)返回的就是以root为根的前序遍历结果,那么当前层的任务就只是处理root,然后分别调用preorder(root.left)和preorder(root.right)。很多人在Day10卡住,是因为总想着在脑子里逐层展开,一展开就乱了。

如果递归写得不稳,可以配合栈写迭代版本,它们本质是一样的,只是把系统帮你压的递归栈换成手动维护的栈而已。

def preorder(root): stack, res = [root], [] while stack: node = stack.pop() if node: res.append(node.val) stack.append(node.right) stack.append(node.left) return res

注意栈是先入后出,所以我们要先压入右节点、再压入左节点,这样左节点会先弹出,保证遍历顺序是“根-左-右”。第10天不要求你把各种遍历倒背如流,但至少要知道递归和迭代是可以互相转换的,这为后面做更复杂的二叉树题目打了很好的底子。

4. 从一道题看完整解题链路:以“某些求和类题目”为例

4.1 第一反应:暴力求解

为了把前面讲的方法串起来,我用一道很经典的题来做完整演示。题目大概是这样的:给定一个整数数组和一个目标值,请返回两个数的下标,使这两个数之和等于目标值。你可以假称这是一道模拟项目X中的需求。我先写出暴力版本:

def two_sum_brute(nums, target): n = len(nums) for i in range(n): for j in range(i + 1, n): if nums[i] + nums[j] == target: return [i, j] return []

这个版本很好理解,就是穷举所有下标组合。时间复杂度O(n²),空间复杂度O(1)。在输入规模很小的时候完全没问题,但一旦数组长度到10的4次方量级,两个循环就是10的8次方次操作,大概率超时。

这里要补一个关键认知:判断一道题能不能用暴力解,不是看题面,而是看数据范围。很多第10天的朋友会因为TLE而怀疑人生,其实他们只是没养成先看数据范围的习惯。

4.2 优化分析:内循环到底在做什么

暴力版本的第二层循环,做的事情本质上是在“寻找一个数等于target - nums[i]”。这个“寻找”操作放在循环里就是O(n),如果能把每次查找都变成O(1),整个算法就会从O(n²)变成O(n)。这正好是哈希表的用武之地。

优化的过程不是玄学,是顺着“查找慢就加哈希表”这条线推出来的。我先建立一个空字典,键是数组元素的值,值是它对应的下标。然后从左到右遍历数组,每遇到一个数num,先检查target - num是否已经在字典里。如果在,说明之前遇到过一个数和当前这个数能凑成target,直接返回两个下标;如果不在,就把当前这个数存进字典。这样一遍遍历就完成,不需要回头再找。

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

为什么必须先“查”再“存”?如果先把所有元素都存进去,再遍历查询,会导致一个元素被匹配到它自己。我用一个很极端的例子说明:nums = [2, 2], target = 4。如果先全部存入字典,多次遍历时第1次取到nums[0]=2,在字典里找到2对应的下标为0,于是返回[0, 0],这显然是不对的。先查后存就能天然避免用自己的下标匹配自己。

4.3 复杂度对比和边界条件

暴力版是O(n²)时间、O(1)空间;哈希版是O(n)时间、O(n)空间。这是非常典型的空间换时间。很多初学者一看到O(n)空间就开始担心“空间会不会爆”,实际上常见的内存限制下,O(n)空间远没有O(n²)时间那么致命,尤其是n在10的5次方以内时,哈希表存几万个键值对完全没问题。

边界条件也值得聊两句。题目如果明确说“一定存在一个答案”,那代码里不需要专门处理找不到的情况;如果不能确定,建议返回空列表。另外,如果数组长度小于2,也要直接返回空列表,虽然两数之和这种题通常不会出现这么短的数组,但养成习惯总没坏处。

这道题的完整链路看起来简单,但它真正展示了方法论:先暴力,再分析哪里慢,然后用合适的结构优化。第10天需要的不是几千道题的广度,而是把每道题都走完这个链路,直到它成为本能。

5. 实操中踩过的坑与排查技巧

5.1 超时问题:先看复杂度,再看常数

第10天最容易遇到的报错就是超时。很多人第一反应是“是不是我的代码循环太多导致太慢”,但实际上,超时的根子绝大多数在最开始就埋下了,跟你循环细节关系不大。

我会按这个顺序排查:第一步,看一眼题目的数据范围,估算自己算法的时间复杂度。比如数组长度是10的5次方,而你的算法是O(n²),那基本可以确定是复杂度问题,不是常数优化能救的。第二步,确认你的代码在最坏情况下确实会跑满整个复杂度。有些题在平均情况下表现还行,但测试用例会故意构造最坏情况,比如所有元素都相等、所有数都不满足条件。第三步,如果复杂度本身没问题,才去检查循环里有没有重复计算、有没有不必要的函数调用、有没有频繁创建对象。

这里也有个经验之谈:不要为了优化常数而牺牲代码可读性,第10天阶段更重要的是学会判断“该不该优化”。有些题你用了哈希表但还是很慢,可能是你的哈希表里存了太多无用的中间状态,这时候应该回头检查你的更新逻辑,而不是盲目换成更复杂的数据结构。

5.2 边界条件:自己造用例,别等测试出报告

很多刷题新手习惯写完代码直接提交,等平台告诉你答案错误,再回去猜哪儿错了。这种模式效率太低,我在第10天时把习惯改成了“提交前先自测”,效果好了很多。

自测时我会构造几类经典边界:空输入,比如空数组、空字符串;单元素输入;全相同元素;递增或递减序列;目标值在两端的情况;包含负数或零的情况。以“两数之和”为例,我会自己跑一遍nums = [3, 3], target = 6,确保不会返回相同的下标;再跑一遍nums = [1, 2, 3], target = 5,确保返回的是[1, 2]而不是[2, 1]。

还有一个很隐蔽的边界问题:整数溢出。不过Python没有这烦恼,刷C++或Java的同学就要特别小心了,在做加法比较时,可以把类型转成更大的范围,或者用减法替代加法来避免溢出。Day10阶段不需要过多关注溢出问题,但至少要知道有这个坑。

5.3 一看就会、一写就废:如何破局

这个现象太常见了,尤其是看题解的时候觉得自己全懂了,关上题解自己写又卡在原地。问题出在你只是“认得”了解法,但没有真正在脑中重建过它。破解办法只有一个:把题解扔到一边,凭记忆和思路重写。

重写也不是让你背代码,而是让你用自己的理解去表达。比如你看懂了双指针解法,那就用自己的话描述:快指针找非0值,慢指针指向待替换位置,遇到非0值就交换。然后你写代码的过程中自然会遇到一些细节问题,比如交换之后要不要主动把原来的位置置0,这些细节恰恰是你看题解时不会注意到的。多来两次这种“重写练习”,你才会真正吸收一个解法。

第10天阶段,“一写就废”根本不是能力问题,而是练习方式问题。你不需要做更多新题,你需要把做过的题再重写两三遍,直到不需要刻意回忆就能写出来为止。

5.4 错题本到底该记什么

很多人建了错题本,但记得像流水账:题目编号、错误代码、正确代码。这样的错题本是没有灵魂的。有效错题本的核心应该是记录“当时的思考过程”和“错误发生的地方”,而不是记录答案。

我自己的格式是四个部分:第一,这道题我第一眼想用的方法是什么;第二,为什么那个方法行不通或者不够好;第三,最终解法里最关键的洞察是什么,比如“用哈希表存之前出现过的数,来加速查找”;第四,如果下次遇到类似题,我应该优先想到哪个套路。

这样的错题本看起来不像笔记,更像是一份给自己的“思维提示卡”。第10天时,试着把前面10天的错题按这个格式整理一遍,你会发现自己对算法的理解已经从“散点”开始连成“线”了。

6. 一些关于Day10之后的话

6.1 保持固定节奏比追求速度更重要

刷题这件事,最怕的不是慢,而是断。哪怕是每天只做一道简单题,也比连续休息三天再一口气刷五道要强。算法思路的建立依赖于持续的输入,一旦中断几天,你对那些模板的熟练度会明显下降,重新捡起来又要花额外的时间。

我自己到了第10天的时候,最大的收获不是会了多少题,而是摸清了自己每天能投入多少时间。比如今天状态好,可以刷两道新题加一道复盘;状态差,就只做一道复用模板的题,甚至只做复盘。不要因为某天刷得少就产生负罪感,第10天最忌讳的是拿刷题数量衡量自己。

6.2 从第10天开始,试着给题目分类

第10天之前,你可能只是按照题号顺序刷,或者每天随机从推荐列表里挑题。到了这个节点,建议开始有意识地给做过的题贴标签:数组、字符串、哈希表、双指针、二叉树的遍历、栈与队列。每做一道题,就在记录里加一个标签。

这么做的好处会在后面逐步显现。当你刷到第30天、第50天时,你会发现很多所谓的新题不过是旧题换了层包装。比如“移动零”考的是双指针,“移除元素”考的是同一个模板,“删除有序数组中的重复项”本质还是双指针。题目分类得越清楚,你的迁移能力就越强,下次遇到新题时调取套路的速度就越快。

6.3 最后分享一个提升复现率的小习惯

第10天这个节点,我特别建议你尝试做一件事:把你做过的每道题的暴力解和最优解都写到同一个文件里,并在注释中写清复杂度和关键思路。我当时用的就是本地的一个文档,按日期和标签排列。这样做有两个好处:一是复习时能快速看到自己的进步轨迹,这对信心维系很有帮助;二是当你想不起来某个套路时,翻自己的文档比翻题解更快,因为那上面记的是你已经消化的领域表达。

到Day10那天,我把前面积累的十几份记录重新过了一遍,才真正有一种“上道了”的感觉。后续的刷题,就是在这些模板和套路之上做增量积累。希望这份Day10的经验能帮正在这个节点徘徊的朋友稳住节奏,别急着追赶别人的进度,先把自己的地基打扎实。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进