接触编程的人大多经历过这样的阶段:语法书翻了两三遍,跟着教程写过几个小项目,可真到打开刷题网站的那一刻,面对第一道题脑子里依然是空白。别急,这不是能力问题,而是缺少一套系统化的热身流程。不少同学的刷题记录都能看出一个规律:能把入门题稳稳吃透的人,后面进阶普遍顺畅;而一上来就直奔困难题的那部分人,几周后基本就安静了。LintCode领扣的入门题库恰好提供了这样一条合理路径,从基础数据类型到经典算法套路,难度曲线平缓,很适合作为算法起点的第一站。
这篇内容我整理了入门前最值得做的几类题目,每道题都给出完整的思考路径和代码实现,同时补充了我实际刷题时踩过的坑和总结的排查方法。文章面向的是刚接触算法题、或者刷了一阵子但感觉进步缓慢的同学。如果你已经能稳定解决中等难度题目,可以直接跳到后面看复杂度分析和进阶建议;如果刚开始,建议按顺序来,把基础打扎实再谈技巧。
1. 入门之前:先把"刷题"这件事想明白
很多人对刷题存在误解,觉得刷题就是背答案、拼题量。实际上入门阶段的核心目标只有一个:把大脑从"能读懂代码"切换到"能设计算法"的状态。这个转换需要刻意练习,也需要合理的选题策略。
1.1 为什么选择领扣作为起点而不是其他平台
市面上的刷题平台有不少,我推荐入门阶段优先考虑这个平台,原因是三个字:梯度好。它的入门题覆盖了最基础的数据结构和算法思想,不会出现"标着简单却要求优先队列加动态规划"这种劝退情况。同时题解区提供了多语言版本,方便你对照自己熟悉的语言去理解思路。
平台还有一个很实用的功能:题目难度标签和章节分类。这意味着你可以按照"数组、字符串、链表、二叉树"这样的知识模块去集中训练,而不是东一题西一题地乱刷。对新手来说,带分类的练习远比随机刷题效率高,因为同一知识点的反复刺激才能形成肌肉记忆。
当然,平台不是唯一选择,但作为入门入口,它的提示机制值得一说。遇到卡点可以付费查看提示,对自己实在想不出来的题目,看提示后继续思考,比自己死磕半小时然后放弃要有效得多。这个机制能帮你保持学习节奏。
1.2 入门阶段最容易踩的三大误区
第一个误区是追求题量。有些人一周刷了五六十道题,问他"两数之和"有几种解法,他只能说出暴力法。这就是典型的刷题刷成了抄代码,没有真正内化。入门阶段我更建议一道题至少思考三种方案,把时间花在"为什么"上,而不是花在"下一题"上。
第二个误区是跳过数据结构直接上算法。没搞懂链表指针的操作就去做链表反转题,代码全凭感觉写,能过就是运气好,不能过就反复试错。正确的做法是先花几天时间把每种基础数据结构的增删改查写熟练,再做对应的算法题目,你会发现理解门槛立刻降了一大截。
第三个误区是不看复杂度就提交。写出的代码能跑通案例就觉得完成了,这种心态会让你在真正的大数据量测试面前措手不及。入门阶段就开始养成分析时间复杂度和空间复杂度的习惯,后面才会走得更远。
1.3 刷题顺序:别被"简单"标签骗了
"入门"分类下的题目并不完全等价于"容易"。有些题目看起来代码量不大,但背后的数学推导却相当精巧,比如整数反转和回文数判断这类数字处理题。我会建议你先从纯数组和字符串操作的题目开始,这类题逻辑直观,代码路径短,能快速建立信心。
接下来再练习数学类题目,这类题能培养你对边界条件和特殊值的敏感度。最后才是链表和二叉树这种涉及引用和递归的题目,此时你已经有了足够的编码经验,理解递归调用栈会顺畅许多。按这个顺序刷完二十道左右的题目,基本就具备了进入中等难度区域的底气。
2. 入门选题清单:真正值得优先做的几类题目
入门题库少说也有上百道,不可能全部刷完,也没必要。我把真正值得优先做的题目按类型划分,每类说明的练习目标和代表性题目的思考方向。
2.1 数组与字符串:最基础的"查表"功夫
数组和字符串是所有算法题的地基,几乎每个复杂的解决方案里都能看到它们的身影。入门阶段要重点练习的操作包括:遍历、查找、反转、拼接、去重,以及双指针技巧的初体验。
这类题的核心价值在于让你熟悉索引操作的边界。比如反转数组,门面功夫就是双指针一前一后交换元素,但真正容易出错的是循环终止条件:是left < right还是left <= right?用奇数长度和偶数长度的数组各测一遍,你才能彻底搞明白这个细节。
关于字符串,很多题目会衍生出大小写转换、字母频率统计这类操作。我建议你提前把常用字符判断函数熟悉一遍,比如判断是否为字母或数字、转大小写的方法,这样做题的时候能少查好几次文档。
2.2 数学类题目:找回写代码的手感
数学类题目是入门题库里最友好的一批,因为它们不需要你掌握复杂的数据结构,核心考验的是逻辑分解能力。常见套路包括取模、整除、数位分离、最大公约数等操作。
这类题目的示范意义在于:让你习惯把数学问题转化为程序逻辑。比如判断一个数是否是回文数,数学上的定义是人脑直接感知的,但程序需要你拆解成"取出每一位数字再反转比较"这样的可执行步骤。这个拆解过程恰恰是程序员的核心能力。
建议优先做整数反转、回文数判断、丑数检测这类题目。它们代码量短,但每道题都藏着边界条件,练一遍就能快速提升思维严谨度。
2.3 链表与二叉树:理解"引用"与"递归"
我见过不少同学在这个阶段突然卡住,不是因为题目难度,而是链表、二叉树涉及的概念跳出了简单的"值操作"范畴,引入了"引用"这个关键概念。链表不是数组,你不能用下标随机访问;二叉树遍历则天然适合递归实现。
链表类题目入门阶段练两件事:遍历和反转。反转链表看似简单,却同时考验了三个指针的协作:当前节点、前驱节点、后继节点。动手画图是解决这类题的最好方法,画着画着你就会发现指针赋值的逻辑自然浮出水面。
二叉树题目从最大深度、前序遍历开始,这类题就是递归的标准模板:递归终止条件是什么、当前层要做什么操作、递归返回值如何使用。把这三件事想清楚,二叉树题目就只剩下套模板的问题了。
3. 核心题目拆解:从零到一写出AC代码
接下来进入正题,我用五道最具代表性的入门题目做完整拆解,每道题都会讲清楚思路推导、代码实现、复杂度分析,以及我实际提交中遇到的疑难杂症。
3.1 两数之和的三种写法
题目描述很简单:给定一个整数数组和一个目标值,找到数组中两个数,使得它们的和等于目标值,返回这两个数的下标。
暴力解法是双重循环,逐一检查每对组合。代码量最少,但时间复杂度是O(n²)。如果数组长度是10000,意味着要执行约5000万次比较,这在严格的时间限制下是危险的。
优化的突破口在于"空间换时间"。遍历数组时,对每个元素x,我们要找的是target - x。与其在数组中线性搜索target - x,不如用哈希表记录已经出现过的元素和它的下标。这样每次查找只需O(1)时间,整体降到O(n)。
def two_sum(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 []我最初写这段代码时犯过一个低级错误:先把所有元素放进哈希表再遍历查找。这在数组存在重复值时会导致同一个下标被返回两次。正确的做法是边遍历边存,这样可以保证只有遇到互补数时才记录答案。
还有一种思路是先排序再用双指针从两端逼近,但排序会打乱原始下标,需要额外保存索引信息。这道题里哈希表方案最优雅,但双指针思想在其他题目里非常有用,值得理解。
3.2 回文数判断的边界陷阱
题目要求判断一个整数是否为回文数,即正序和倒序读相同。看到回文这两个字,第一反应是转成字符串然后反转比较。这种方式能通过,但面试时如果没做额外说明,容易被追问"能否不用字符串完成"。
整数反转法不涉及字符串:将数字后半段反转,与前半段比较。关键在于处理奇数位数的情况,比如数字12321,反转后半段得到123,与前半段123相等,就是回文数。
def is_palindrome(x): if x < 0 or (x % 10 == 0 and x != 0): return False reversed_half = 0 while x > reversed_half: reversed_half = reversed_half * 10 + x % 10 x //= 10 return x == reversed_half or x == reversed_half // 10这段代码的巧妙之处在于只反转一半数字,避免了整数溢出问题。我第一次做这道题时反转整个数字,在Python里没问题,但换成其他语言就可能溢出。提前养成写"最小可行方案"的习惯,会让你在更多场景受益。
另外,别忘了判断负数:-121倒过来是121-,显然不是回文数。还有末尾为0的数,比如10,倒过来是01,也不是回文数,如果不处理会出错。
3.3 反转整数的溢出处理
题目要求给定一个32位有符号整数,将数字部分反转。基本思路很直接:不断取模取余,将数字反向拼接。但核心考点在于溢出判断:反转后的结果可能超出32位整数的范围。
假设环境不允许存储64位整数,这意味着你在拼接过程中就要判断是否溢出。一种稳妥的方法是:用一个新的变量记录反转结果,每次拼接前检查是否大于INT_MAX/10,或小于INT_MIN/10。
def reverse_integer(x): INT_MAX = 2**31 - 1 INT_MIN = -2**31 result = 0 sign = 1 if x >= 0 else -1 x = abs(x) while x != 0: digit = x % 10 if result > (INT_MAX - digit) // 10: return 0 result = result * 10 + digit x //= 10 return sign * result这里用了一个小技巧:判断result > (INT_MAX - digit) // 10而不是result > INT_MAX // 10,因为后者在正数场景下可行,但在负数场景下容易漏判。更简洁的写法是直接判断result > (INT_MAX - digit) // 10,当反转为负数时同理。
实验时容易忽略的是数字0,反转后还是0,两种边界情况都能覆盖。这道题给我的最大收获不是会做了,而是开始养成"写代码前先想清楚输入范围"的习惯。算法题里的刁钻用例,几乎都藏在边界条件里。
3.4 斐波那契数列的递归与迭代之争
求第n个斐波那契数,n从0开始计数。绝大多数人的第一版代码是递归实现:
def fib_recursive(n): if n <= 1: return n return fib_recursive(n - 1) + fib_recursive(n - 2)代码无比简洁,但递归树会重复计算大量子问题,时间复杂度是O(2^n)。n=40时就已经明显卡顿,n=50直接超时。如果面试官追问优化方案,这版代码大概率被淘汰。
引入记忆化递归(memoization),用字典记录已计算的值,可以降到O(n)。但这还不是最推荐的解法,因为迭代法不仅不需要额外空间,还能避免递归栈溢出风险:
def fib_iterative(n): if n <= 1: return n prev, curr = 0, 1 for _ in range(2, n + 1): prev, curr = curr, prev + curr return curr迭代法的思路类似滚动数组,只维护当前值和前一个值,空间复杂度O(1)。这道题的训练价值不在于斐波那契本身,而在于让你体会同一个问题从"正确"到"高效"的演进路径,这也是刷题的核心乐趣。
3.5 合并两个有序数组的双指针法
题目要求将两个有序数组合并为一个有序数组,要求不额外开辟新数组,直接修改第一个数组。这道题我第一次做时从头开始合并,发现前几个元素会覆盖掉还没比较过的值,调试了好半天才缓过来。
正确的做法是从后往前填充。因为第一个数组末尾预留了额外空间,从后往前遍历时,较大值可以直接放在末尾,不会覆盖还没处理的元素。
def merge(nums1, m, nums2, n): p1, p2 = m - 1, n - 1 tail = m + n - 1 while p1 >= 0 and p2 >= 0: if nums1[p1] > nums2[p2]: nums1[tail] = nums1[p1] p1 -= 1 else: nums1[tail] = nums2[p2] p2 -= 1 tail -= 1 while p2 >= 0: nums1[tail] = nums2[p2] p2 -= 1 tail -= 1这段代码的边界条件值得反复咀嚼。如果nums2先被消耗完,说明数组1剩余的已经是正确顺序,无需继续操作;如果nums1先被消耗完,需要把nums2剩余部分拷贝过去。很多实现会遗漏第二个while循环,导致错误结果。
4. 从AC到优秀:复杂度分析与代码规范化
具备解题能力只是第一步。入门阶段如果能同时养成两大习惯——分析复杂度和规范化代码风格,后续进阶将会顺滑很多。
4.1 认识大O表示法:入门必须过的一道坎
大O表示法描述的是算法运行时间随输入规模增长的趋势。O(1)表示常数时间,无论输入多大,执行次数保持不变;O(n)表示线性增长;O(n²)表示平方级增长。
入门阶段不必深究严格定义,但每写完一道题,我建议你花三十秒思考三个问题:我的算法遍历了几层?额外使用了多少存储空间?有没有可以合并的循环?思考完你会发现很多代码优化的空间。
举个例子,判断一个字符串里每个字符出现的次数,你可以用双重循环,也可以遍历一次并用哈希表记录。前者的复杂度O(n²),后者O(n)。同样是正确解法,放在大规模数据上表现天差地别。
4.2 空间换时间的经典套路
入门题里最常出现的优化策略就是空间换时间,典型代表是哈希表。它的本质是:把需要反复查找的信息预先存好,用O(1)查找代替O(n)遍历。
以"两数之和"为例,暴力解法是每到一个元素就去剩余部分寻找互补值,使用了O(1)空间但付出O(n²)时间。哈希表解法使用O(n)空间,但把时间降到O(n)。当数据量大时,多花一些内存换取运行时间的减少,通常是值得的。
我见过有人一听说空间换时间就盲目套哈希表,遇到统计元素出现次数的问题也上哈希表,这没问题;但遇到要求空间复杂度O(1)的题目,就完全懵了。入门阶段要训练的是"知道有哪些套路以及每个套路的代价",而不是背套路。
4.3 规范命名与代码可读性
刷题平台对变量名没有强制要求,但代码写给自己看,也可能会被将来的同事看。我合作的几个竞赛选手有个共同习惯:变量名一定起得有意义。i、j、k这种单字母命名在简单循环里可以接受,一旦逻辑稍复杂,读起来极其痛苦。
我的建议是三个原则:循环计数用i、j可以,但语义性变量一定要用全称;临时变量加前缀说明作用,比如tempNode、nextNode;布尔变量用is_开头,比如is_palindrome、is_valid,读起来像一句话,这让代码的意图一目了然。
代码可读性不只是给别人看,更重要的是给未来的自己看。两周后回看自己写的题解,如果还要花十分钟才看懂当时的思路,说明命名和注释都有改进空间。
4.4 把题目答案变成自己的知识库
很多人刷题最大的遗憾是刷完就忘。我自己的做法是维护一份个人题库笔记,每道题记录三件事:核心思路、复杂度分析、踩坑点。不要复制题解,而是用自己的话重新表达一遍,这样记忆会深得多。
遇到同一道题有多个解法的,我会把所有解法都记录进去,并标注每种解法的适用场景。比如排序相关的问题,数据量小可以选插入排序,数据量大应该选快速排序或归并排序。这类对比总结,只有在做题时认真体会过才会留下深刻印象。
5. 新手问得最多的几个问题
整理这篇文章时,我回顾了自己和身边朋友刷题初期反复问过的问题。下面把最有代表性的几个拉出来集中回答,希望能帮你少走绕路。
5.1 题目看了二十分钟毫无思路怎么办
我的经验是:不要在毫无思路的情况下死磕超过二十分钟。入门题的考察点就那么几类,如果你卡住了,说明对某一类套路还不熟悉。这时候最有效的方法是直接看题解,看懂思路后合上题解自己重新写一遍,写不出来再回看,直到能独立写出AC代码。
但这里有个关键条件:看题解后必须复盘。找出自己刚才卡住的原因是什么?是没理解数据结构,还是没掌握算法思想?把原因记下来,下次遇到类似题目就会产生条件反射。
5.2 代码在本地能跑但提交就是报错
这类问题绝大多数集中在输入输出格式或边界条件上。平台判题时对输出格式有严格限制,多一个空格少一个换行都可能导致错误。我建议每次提交前先自己构造两三组特殊测试用例:空输入、单个元素输入、最大数值输入。
还有一种隐蔽情况:全局变量污染。如果你在函数外定义了变量,多次调用函数时会残留上次的结果。解决方案很简单:所有变量只在函数内部定义,不要依赖外部状态。
5.3 题型标签那么多,哪些要重点攻克
入门阶段我的选题优先级是:数组、字符串、哈希表、链表、二叉树、递归。这六类题目覆盖率极高,练熟之后至少一半的中等难度题都有了下手思路。排序和二分查找可以等基础扎实后再加入,动态规划和贪心算法则属于进阶内容,建议不要过早碰。
一个可量化的目标是:每类题型稳定刷十题左右,每一题都能独立写出至少一种AC方案,并说清时间空间复杂度。达到这个标准后,再去做综合性的中等题会顺畅很多。
5.4 刷题频率与复盘节奏
有不少人给自己定下"每天刷十题"的目标,但坚持一周就崩盘了。我身边真正能坚持下来的人,节奏通常是每天一到两题,周末集中复盘当周的错题和笔记。刷题不是短跑,更像一场马拉松,稳定输出比爆发式刷题重要得多。
复盘时重点关注的就是错题和超时题。把自己当时的思路和正确思路放在一起对比,找出思维断点在哪里,然后归纳成一句教训。我自己就有一个文档专门记录这类教训,比如"注意负数取模行为"、"递归一定要有终止条件"、"边界条件先想清楚再写循环"。遇到新题时先回忆教训,正确率明显提升。
算法学习是一个持续积累的过程,入门阶段最重要的不是掌握多少技巧,而是培养正确的思维方式和解题习惯。每次提交AC后别急着庆祝,多想一想有没有更优的解法;每次看到题解时也别急着抄,先停下来自己尝试推导。我个人体会最深的一点是:刷题不是比谁刷得多,而是比谁思考得深。把一道入门题吃透,收获往往胜过盲目做十道新题。慢慢来,反而更快。