开篇:刷力扣,先啃链表、二叉树、栈这三块硬骨头
如果你正在准备大厂面试,或者刚入行想系统地补数据结构,大概率会打开力扣刷题。但看一眼题库,两千多道题直接头晕——从哪刷起?我见过太多人一上来就怼动态规划,结果被教做人,两周后弃坑。但如果你把矛头指向链表、二叉树、栈这三块,情况会完全不一样。
为什么?因为这三个结构几乎是所有“套路题”的载体。链表练的是引用操作和边界意识,二叉树练的是递归建模和遍历框架,栈练的是状态维护和线性思维。它们彼此独立,又能互相串联——比如用栈迭代做二叉树遍历,用栈辅助反转链表,用双端队列做层序遍历。而且力扣热题100里,链表、二叉树、栈相关的题目大概能占到四成左右,是实打实的高频区块。
这篇文章不打算给你堆一套“大全”,而是想以过来人的视角,把这三个结构在力扣里最常见的题型、最本质的解法框架、以及我实际刷题过程中踩过的坑,一次讲透。你会看到具体的题目举例、代码模板、复杂度分析,还有一些只有真正动手写过才会知道的细节。不管你是刚开始刷力扣的算法新手,还是已经刷了一阵子、想查漏补缺的进阶选手,这篇内容应该都能帮你省下不少弯路。
顺便说一句,刷这些题不是背答案。我见过很多同学把热门题的解法背得滚瓜烂熟,但面试官稍微换个问法就卡壳。关键在于理解每种结构背后的“思维模型”——链表是引用关系,二叉树是递归结构,栈是后进先出。模型对了,题目怎么变都不怕。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
1. 三个数据结构,为什么必须放在一起练
1.1 它们是算法题的最小“基础设施”
我经常把力扣的知识点分成三层:基础语法层、数据结构层、算法思想层。链表、二叉树、栈正好处在中间层——它们既不像数组和哈希表那样“白给”,也不像动态规划和图论那样需要大量经验和数学直觉。这一层是性价比最高的投资:学会一个结构,就能打通一大批题。
但更关键的是,这三个结构彼此之间高度纠缠。比如,递归是二叉树最自然的解法,但递归本质上是系统栈帮你维护状态;迭代遍历二叉树时,你又得自己显式地维护一个栈。再比如,反转链表可以用迭代也可以用递归,你如果对栈的“后进先出”有直觉,理解递归的“回溯”就会快得多。所以把它们放在一起练,不是简单的“平行学习”,而是互相强化。
我在带新人刷题的时候,发现一个规律:凡是链表画图熟练、二叉树递归写得多、栈的进出时机拿捏得准的人,后面学回溯、学DFS、学单调栈、学图论,都会顺畅很多。反过来,基础不牢的人,即使背下了一堆高级模板,一遇变形题还是露馅。
1.2 三个结构的“思维模型”分别是什么
拿生活里的例子来类比。
链表像一条铁路线。每节车厢知道下一节在哪,但如果你身在中间某个位置,你是没法直接跳到最后一节的——只能一节一节往后走。所以链表类题目最核心的思维是“引用追踪”:你要清楚地知道每一步操作之后,哪个指针指向哪,哪个节点的next变成了什么。几乎所有链表难题,本质都是指针操作题。
二叉树像一份公司组织结构图。CEO下面是部门负责人,部门负责人下面是小组长,小组长下面是组员。你想统计全公司有多少人,自然会让每个管理者去统计自己部门的人数,然后一级一级上报。这就是递归的直觉——二叉树题目天然适合“把大问题拆成同构的小问题”。
栈像一个堆盘子用的弹簧柱。你总是只能先取最上面的盘子,想拿下面的必须先拿走上面的。所以栈类题目核心是“状态和时间顺序”:最近发生的事最先被处理。表达式求值、括号匹配、函数调用,全是这种“后进先出”的场景。
这三句话听起来简单,但它们就是解题的总纲。我刷题到现在,遇到陌生的难题,第一件事永远是问自己:这题的逻辑结构更像哪一类模型?是链式的引用访问,是层级递归的树,还是需要按顺序消解的栈?模型定下来,解法就八九不离十了。
2. 链表类题目:先画图再写码,别上来就玩双指针
2.1 链表题型的“全家福”清单
链表在力扣里的题目,我大致给它们分了个类。你可以对照着看自己刷到哪一层了:
-
基础操作类:反转链表(206)、删除倒数第N个节点(19)、合并两个有序链表(21)、两两交换链表中的节点(24),这类题考察的是最基础的指针操作。
-
双指针与快慢指针类:环形链表(141)、环形链表 II(142)、链表的中间节点(876)、相交链表(160)。这类题的核心是“快慢指针”,也是链表题里最容易玩出花的地方。
-
特殊链表类:LRU缓存(146)、复制带随机指针的链表(138)、排序链表(148)。这类题往往混合了哈希表、归并排序等技巧,是链表的综合应用题。
-
虚拟头节点类:移除链表元素(203)、两数相加(2)、分隔链表(86)。这类题统一套路是先造一个“哑节点”,避免处理头节点的特殊情况。
你会发现,链表的题目花样虽然多,但核心能力就四个字:引用操作。你如果对“引用就是对象的别名”这个概念不够敏感,写链表题就是灾难现场。
2.2 我最想强调的:虚拟头节点和指针丢失问题
刷链表题最崩溃的瞬间是什么?写着写着发现头节点丢了,或者链表绕成了环,程序直接死循环。这两个问题我都踩过无数次,后来总结出一个保命原则:“动手改指针之前,先想清楚哪个节点会被甩掉,先拿临时指针把它接住。”
举个例子,反转链表。很多初学者的第一反应是“我把每个节点的next指向前一个不就完了吗?”逻辑没错,但写起来问题就来了:当你把当前节点的next改成前一个节点时,下一轮要访问的原next就已经丢了。所以你必须先用一个临时指针存住它。这就是经典的三指针法:prev、cur、nextTemp。
另外一个能省掉大量麻烦的技巧是虚拟头节点。比如删除链表中的某个节点,如果删除的是头节点,处理逻辑和非头节点不一样,又是一堆分支判断。但如果我在最前面加一个虚拟节点dummy,让dummy.next指向真正的头节点,那么“删除任意节点”就统一成了“让前一个节点的next跳过目标节点”这个动作。这个思路在多个场景里都能用,属于两分钟学会、受益终身的技巧。
再表演一个经验:快慢指针不只是在链表里找中点、判断环,它还能解决“不知道链表长度的情况下找到倒数第K个节点”这种问题。实现方式就是让快指针先走K步,然后两个指针同步前进,快指针走到头时,慢指针刚好停在目标位置。理解了这个逻辑,你就不需要再“先遍历一遍求长度,再遍历一遍找位置”了,时间复杂度也从两趟遍历降到了一趟。
3. 二叉树:递归是根基,迭代是进阶,一定要两手抓
3.1 遍历框架:你用递归写三行,用迭代写十行,但两手都要会
二叉树这章,最核心的就是遍历。前序、中序、后序、层序,基本上是所有二叉树题目的母体。很多题目,比如求最大深度、判断对称、路径总和,本质上就是在某种遍历过程中“顺便处理一下当前节点”。
递归版本的四种遍历非常好写,核心就是一个模板换顺序:
python复制def dfs(root):
if not root:
return
# 前序:在这里处理 root
dfs(root.left)
# 中序:在这里处理 root
dfs(root.right)
# 后序:在这里处理 root
但光会递归不够。面试官特别喜欢让你写一个非递归版本,目的是考察你是否真的理解遍历过程,还是在背模板。迭代版本用栈模拟系统栈的进出,前序和中序写法还算好懂,后序最恶心——因为根节点得最后访问,你得额外标记节点是被“左子树访问完返回”还是“右子树访问完返回”。
我自己学习迭代遍历时的一个心得:不要死记代码,而是去手动模拟一张栈的进出表。比如中序遍历,整个过程的逻辑就是“一路向左入栈,无左可走就出栈访问,然后转向右孩子,继续一路向左”。我建议你在草稿纸上画一棵三层二叉树,把每个节点入栈、出栈、访问的顺序手动推一遍。这个动作看起来笨,但效果奇好——推完一遍你就再也不会忘记迭代中序遍历了。
3.2 递归思维:让函数自己相信它能解决问题
二叉树的递归题有一个心理门槛:很多人总觉得“我先得把递归怎么走通的细节想清楚才行”,其实不必。递归的核心是“信任”——你信任递归函数能解决规模更小的子问题,然后你只需要解决“当前层”和“子问题之间如何拼接”。
比如求二叉树的最大深度:
python复制def maxDepth(root):
if not root:
return 0
left = maxDepth(root.left)
right = maxDepth(root.right)
return max(left, right) + 1
你不需要去模拟left和right是怎么算出来的,你只需要知道这个函数定义就是“返回树的高度”,那当前节点的深度自然是“左右子树较高者加1”。这种思考方式,你一旦习惯,就会上瘾——因为很多看起来很麻烦的题,拆成递归之后代码短得吓人。
我见过太多人写递归时纠结“我用的是前序还是后序”,其实这个区分在这个场景里并不重要。重要的是你要搞清楚数据从哪里来、到哪里去。如果答案来自左右子树的返回值,那一定是后序——你要先把子问题算完再计算当前节点;如果答案是一个需要从上往下传递的条件判断,那你可能要在前序位置检查,把状态作为参数传给递归函数。这个“数据流向”的意识,比背“递归三要素”有用得多。
4. 栈:一道题记一个场景,单调栈是隐藏的提分点
4.1 栈的经典场景:括号、表达式、函数调用
栈的入门题非常友好,最典型的就是有效括号(20)。这个题的思路是:遇到左括号就入栈,遇到右括号就弹出栈顶检查是否匹配。这几乎是对栈“后进先出”最直观的诠释了。
比有效括号更进一步的是表达式求值类题目,比如逆波兰表达式求值(150)、基本计算器(224)。这类题需要你把数字和运算符分别或混合压栈,在合适的时机弹出运算。我第一次写基本计算器的时候被加减乘除和括号整得头皮发麻,后来发现只要掌握两个原则就顺了:一是每个数字进来时,前面必定已经决定好它的符号或运算符;二是遇到左括号时把当前状态保存到栈里,遇到右括号时再恢复现场。这个“保存现场 / 恢复现场”的思路,和函数递归调用里系统栈做的事一模一样。
还有一类高频题是“用两个栈实现队列”(232)和“最小栈”(155)。前者考察的是两个栈的配合:一个栈负责入队,一个栈负责出队,出队栈为空时把入队栈全部倒过来。后者要求O(1)时间内拿到栈中最小值,常规做法是额外维护一个单调递减的辅助栈。这两道都是面试高频,建议没事就默写一遍,属于肌肉记忆级别的基础题。
4.2 单调栈:看起来高级,其实是“下一个更大元素”的万能模板
如果要选一个“学会了能吊打一片题目”的栈技巧,我觉得首推单调栈。单调栈的核心思想是:维护一个栈,让栈内元素保持单调递增或单调递减,每次有元素要入栈时,把破坏单调性的栈顶元素弹出去,而这个“弹出”的瞬间,往往就是计算答案的时机。
最经典的是每日温度(739)和下一个更大元素(496)。你如果第一次看到单调栈写法,一定会觉得特别神奇——竟然可以线性时间内解决“往右找第一个更大的”这类问题。我自己当时的感觉是,这玩意儿到底是怎么想出来的?后来想明白了才发现,它本质上是在维护一个“未解决的等待列表”——栈底的元素在等一个比它大的数,一旦出现了这个数,它就出栈“领奖”了。
用单调栈还能解接雨水(42)和柱状图中最大的矩形(84),这两题算是栈类题的巅峰之作,很考校理解和应用能力。建议你先把每日温度逐行吃透,然后再用同样的模板去套“下一个更大元素”,最后再挑战接雨水。这个过程比直接看十道题的题解有用得多。
还有一个比较精妙的点:单调栈不仅适用于数组,也可用于链表。比如力扣1019题“链表中的下一个更大节点”,做法就是先遍历链表把值存到数组,然后跑单调栈模板。你看,链表和栈又串起来了——这也是我为什么主张把这三个结构放在一起学的原因。
5. 常见问题与排查技巧实录
刷题过程中遇到问题,不能光靠“看题解”。我把自己以及带过的同学踩过的坑整理了一下,做成一个速查表,希望能帮你少走弯路。
| 问题现象 | 根本原因 | 排查与解决思路 |
|---|---|---|
| 链表反转后头节点丢失 | 修改next指针前没有保存后继节点 | 检查每个修改指针的操作前,是否已经用临时变量保存了被甩掉的节点 |
| 链表题目运行出现死循环 | 链表成环,往往是某处next指回了前驱 | 在草稿纸上画出操作前后的指针状态,确认每个节点只被“跳过”而不是被“绕回” |
| 递归二叉树时栈溢出 | 递归深度过大,或base case写错 | 先检查退出条件是否为null判断;若树特别深,考虑迭代遍历 |
| 中序迭代遍历输出顺序错乱 | 入栈和出栈时机有问题 | 手动模拟一遍“一路向左入栈 → 出栈访问 → 转向右孩子”的流程 |
| 栈题“下一更大元素”结果全部为-1 | 单调栈维护的方式不对 | 确认是“从左到右遍历”还是“从右到左遍历”,两种写法逻辑相反但结果一致 |
| 最小栈取min时报错 | 辅助栈没有同步出栈 | 辅助栈必须和主栈在pop时保持同步,否则min会停留在已弹出的元素上 |
关于如何定位问题,我的实践是:如果提交没通过,第一时间不要看题解,先自己打印中间过程。链表题打印每个节点的值,二叉树题打印遍历顺序,栈题打印每次入栈出栈后的栈内容。把执行过程可视化出来,八成以上的问题你一眼就能发现。
还有一个平时训练的小技巧:每道题提交通过之后,不要马上做下一道,花五分钟想一想“我的解法最坏情况是什么复杂度?空间呢?”以及“如果数据规模扩大十倍,这个代码还能跑吗?”——这两个问题就是面试时不自觉暴露功底的地方。我见过太多人能AC题,但被问一句“时间复杂度是多少”就开始支支吾吾。所以平时就养成习惯,面试才不会慌。
在基本功训练方面,我强烈建议你手写而不是用IDE补全。自动补全会让你忽略代码结构,而算法题的笔试环境往往没有这种便利。我早期刷题时就是太依赖IDE提示,直到有次面试要求在白板手写反转链表,结果现场写出一堆低级语法错误。从那以后,我每周至少手写三到五道核心题,尤其是链表和二叉树这类对指针和结构要求高的题目,写顺手之后,面试的手感会完全不一样。
6. 刷题路径与时间安排的实用建议
6.1 三条分阶段路径,总有一条适合你
刷题最忌讳的就是没有节奏。每个人基础不一样,我用经验帮你把路径拆成三条,你可以按实际情况选:
如果你是完全零基础,我的建议是:先把链表、二叉树、栈各自的“基础操作题”刷完,大概各十道左右,目标是能无压力地写出来,然后立刻做力扣热题100里对应的简单题。这个阶段不追求速度,但每道题都要做到不看题解能自己写出来。我第一次刷反转链表,愣是背了三遍才完全理解指针操作,但一旦理解,后面很多链表题都变得顺了。
如果你大学学过数据结构、但是刷题少,我建议直接按专题刷。比如第一周只刷链表,第二周只刷二叉树,第三周只刷栈。每个专题内按题型细分,比如链表先刷基础操作,再刷快慢指针,最后刷特殊链表。这个阶段的核心是建立“分类模板感”:看到一个题的描述,我能自动归类到某个专题和某个套路,思路就能顺下来。
如果你已经刷了三百道以上、准备冲刺面试,那就不用再按专题走了,直接刷力扣热题100和高频面试题。此时重点不是“会不会做”,而是“能不能又快又稳地写出来”,每道题都要做到10到15分钟内手写出来,并能在写的同时说出思路和复杂度。这个阶段我会给自己卡时间,写不出来就看题解,但是要标记成错题,一周后重刷。
6.2 时间安排:每天一小时比周末死磕一天强
我接触过很多成功的刷题案例,发现一个共性:他们不是每天刷十小时,而是每天固定一到两小时,持续三个月以上。算法是个“手熟”的功夫,保持手感比突击重要得多。
我个人的实操是:早上花20分钟手写一道前一天的旧题,保持肌肉记忆;晚上花30到40分钟做一道新题,然后花5分钟写解题笔记。解题笔记不要抄题解,而是写“我为什么没想到”、“卡在哪里了”、“下次再遇到类似题目我应该先想什么”。这个笔记不用很长,但它的价值极高——因为刷题的本质不是AC,而是把“不会”变成“会”。
复盘时还有一个好用的方法:关掉题解,只看题目的文字描述,然后在脑海里过一遍思路。如果描述完思路之后发现自己能顺理成章地说出复杂度、边界条件和优化方向,那就说明这道题是真的掌握了,否则建议标记为“待重刷”。我用这个方法筛掉了大量“假掌握”的题目,面试前只复习标记过的题,效率能提升一大截。
6.3 全栈视角下的算法:为什么面试官问的是链表而不是框架
现在前端后端的生态都这么成熟了,大家日常都在用React、Vue、Spring Boot、MySQL这些,不少同学会问:面试官为什么还抓着链表和二叉树不放?直接问项目经验不好吗?
这个问题的答案是:算法题考的不是算法本身,而是三种底层素养:第一是抽象能力——面对一个复杂问题,你能不能把它拆解成清晰的结构;第二是边界意识——链表题尤其考验你思考“空的、一个元素、两个元素、尾部操作”这种边界情况;第三是复杂度意识——你写的代码在数据量大时会不会崩,这是全栈工程师上线前必须考虑的问题。
事实上,做全栈项目时,树和栈也无处不在。浏览器的DOM本身是一棵树,前端组件树也是一棵树;后端的调用链是一棵树,递归解析JSON也是一棵树。函数调用栈是所有编程语言的运行基础,浏览器回退就是栈行为,事务回滚也是栈行为。而链表呢——文件系统的目录可以用链表组织,消息队列的底层实现、操作系统的任务调度也离不开类似链表的逻辑。所以,刷这三类题,其实是在补全你对计算机系统底层的认知拼图。
7. 写在最后:一些不那么“技术”的体会
走到这一步,文章的主体内容基本讲完了。但我还想再多说几句,不是技术,而是刷题过程中慢慢琢磨出来的体会。
第一句:不要因为一道题卡了几个小时就怀疑自己。几乎所有刷题人都有过这种挫败时刻。我刷二叉树路径总和那道题时,递归理解不到位,卡了整整一个下午。后来发现只是我在递归函数里忘记把“当前路径”通过参数传下去了。这种卡顿不是笨,而是大脑在建立新连接的必经之路。
第二句:建立你自己的“高频套路手账”。比如链表的虚拟头节点、二叉树的递归返回值习惯、栈的单调模板——每掌握一个套路,就记一个“触发条件”。什么叫触发条件?就是“看到什么样的题干特征,就应该往模板上想”。我自己的做法很简单,拿一个Notion页面或者普通的markdown文档,按数据结构分类,每类列一页模板题和对应的题干关键词。面试前翻一遍这个文档,比临时翻题解有效得多。
第三句:也是我最想强调的——刷题不要孤军奋战。找一个搭子,两个人互相讲解题目,或者加入一个刷题打卡群。为什么?因为“会做”和“会讲”是两码事,而面试本质上是“讲题”。你能把一个复杂思路讲得让一个陌生人听懂,这个能力在面试里是王炸级别的存在。我当年就是在模拟面试群里被一个同学指出“你讲题的语速太快、跳步太多”,后来刻意训练两周之后,面试官反馈明显好了很多。
最后,如果你正处在一个“被题折磨到怀疑人生”的节点,我想说:刷题是一场典型的长期主义游戏,它的回报曲线是滞后的。你可能连续刷了几十道题都没有感觉,然后在某一天回头做两周前卡住的题,突然发现自己不费吹灰之力就写出来了。那个瞬间,你会对“练习的意义”有真实的感受。链表、二叉树、栈,这是你刷题路上的第一批朋友,把它们的脾气摸透,后面无论多难的题,都不至于无路可走。
