news 2026/10/7 16:55:31

Java链表面试题攻略:反转链表、快慢指针与边界避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java链表面试题攻略:反转链表、快慢指针与边界避坑指南

1. 为什么面试官总拿链表说事——先说清楚链表的价值

但凡你准备过Java后端面试,肯定绕不开链表这套题。说实话,链表在业务代码里直接用的机会真不多,日常开发大部分时候都在跟ArrayList、HashMap打交道,面试官为什么偏偏盯上链表不放?我个人的理解是:链表考的不是你背没背过API,而是你懂不懂“引用”和“指针操作”的本质。Java里没有C/C++那种显式指针,但对象引用本质上就是一种指针,链表题恰恰是检验你对引用操作、内存指向、边界条件这三件事掌握程度的最好工具。

还有一个原因,链表的题目变化多、坑深。一道反转链表能玩出迭代、递归、头插法好几个版本,每版还都能延伸出“部分反转”“K个一组反转”这些变体。面试官用一道链表题,基本就能判断出你是背过答案还是真的理解了代码在内存里怎么动。这也是我把链表题整理成系列的原因,希望你能通过这一套题建立起“指针操作”的直觉,而不是死记代码。

另一个现实因素是,链表的操作涉及大量边界判断和空值防护,这些恰恰是实际工程里最容易出bug的地方。你写一个简易的循环单链表、合并两个有序链表,表面上在练兵,实际上是在练“防御式编程”的习惯——先判空、再操作、最后复位。这个习惯放到任何生产代码里都是加分项。所以这篇文章不是单纯给你背题的,我会把每道题背后的“为什么”拆开讲清楚。

1.1 链表的核心考点:引用操作和边界思维

先统一一下认知:链表的每个节点是一个对象,节点里存一个data字段和一个next字段,next就是指向下一个节点的引用。你把链表题做错,绝大多数不是因为逻辑想不明白,而是因为“引用赋值”这一步没想清楚。

举个例子,你写node.next = prev;和prev = node;这两行的顺序一旦写反,指向就丢了。很多新手写反转链表,卡在“丢节点”上,就是因为没有意识到:node.next还没被保存下来就被覆盖了。这个问题的本质是“你只有一个引用,但你需要同时记住当前节点、下一个节点、上一个节点三个位置”,所以迭代反转才需要三个指针变量。

边界思维就更直白了。链表为空怎么办?链表只有一个节点怎么办?操作头节点时需不需要特殊处理?这两个“怎么办”几乎贯穿了所有链表面试题。你去看网上各种题解,评论区问得最多的永远是“如果链表只有一个节点会不会空指针”“如果删除的是头节点怎么返回”。这类问题没有技巧,唯一的办法就是养成“先画图、列用例、再写代码”的习惯。

1.2 面试前必会的链表基本功清单

结合这几年我看到的面经和真实面试反馈,我整理了下面这个基本功清单,按优先级排序:

序号基本功对应面试题掌握程度
1遍历链表求链表长度、打印链表熟练
2反转链表反转整个链表、反转部分区间熟练
3快慢指针找中间节点、判断是否有环熟练
4双指针删除倒数第N个节点掌握
5有序链表合并合并两个有序链表掌握
6链表节点删除删除指定节点、去重掌握
7概念题数组和链表的区别、循环链表的特性熟练

第一项“遍历”是地基,其他所有操作都是在遍历的基础上加条件、加判断。很多人刷题上来就啃反转、啃环检测,结果连打印一个链表都要想半天,这就不太行了。我建议你把遍历代码写到“不加思考就能默写”的程度,后面所有题目都会顺畅很多。

快慢指针这个技巧尤其值得重视。判断链表是否有环、找环的入口、找链表中点、找倒数第K个节点,全都能用快慢指针解。说白了它就是让两个指针以不同速度移动,利用“路程差”来找位置。这个思路理解了,一套题就都通了。

2. 必会面试题逐题拆解:从反转链表到环检测

先说明一下,这套题是面向面试的手写代码场景,所以我不光给解法,还会告诉你每种解法在面试官眼里的加分点和减分点。毕竟面试跟做开发不一样,代码要能讲出思路、经得起追问。

2.1 单链表反转——迭代法和递归法都要会

反转链表是所有链表题里出现频率最高的一道,没有之一。你要说“不会反转链表就去面试”,那基本等于白送。题目要求很简单:输入一个链表的头节点,反转后返回新的头节点。

迭代法是基础版本,核心思路是维护三个指针:prev、curr、nextTemp。每一步做三件事:先保存当前节点的下一个节点,再让当前节点的next指向前一个节点,最后移动prev和curr指针。完整代码如下:

public ListNode reverseList(ListNode head) { ListNode prev = null; ListNode curr = head; while (curr != null) { ListNode nextTemp = curr.next; // 第一步:先保存下一个节点 curr.next = prev; // 第二步:反转指向 prev = curr; // 第三步:prev 前移 curr = nextTemp; // 第四步:curr 前移 } return prev; }

这个代码里最关键的注释就是第一步那个“先保存下一个节点”。你想想,如果没保存,curr.next被改掉之后,循环里就拿不到下一个节点了,整个链表就断了。这个坑几乎所有写链表的人都会踩,面试官盯着看你写的时候,也会特别留意你有没有先保存后操作的习惯。

递归法的代码更短,但理解门槛高一些:

public ListNode reverseListRecursive(ListNode head) { if (head == null || head.next == null) { return head; } ListNode newHead = reverseListRecursive(head.next); head.next.next = head; head.next = null; return newHead; }

递归的思维是“假设后面的都已经反转好了,只需要处理当前节点”。head.next.next = head这行是最难理解的:它让当前节点的下一个节点反过来指向自己。画个图会清晰很多:链表1 -> 2 -> 3,递归到3时返回,然后2.next.next = 2就把3.next从null改成了2,接着2.next = null,于是子链表变成了1 -> 2 <- 3的形态,一路向上完成反转。

面试时我建议你优先写迭代法,因为好讲、好排查、空间复杂度是O(1)。但如果面试官问你“除了迭代还有没有别的方法”,你能把递归法写出来,是一个明确的加分项。不过要注意Java的递归深度问题,链表特别长的时候递归可能导致栈溢出,这个点最好主动提一句,显得你有工程意识。

2.2 判断链表是否有环——快慢指针的标准姿势

判断一个单链表里是否存在环,这道题在面试里出现的频率同样非常高。经典的解法是快慢指针:快指针每次走两步,慢指针每次走一步。如果链表有环,快指针最终会跟慢指针相遇;如果无环,快指针会先到达链表的末尾。

public boolean hasCycle(ListNode head) { if (head == null || head.next == null) { return false; } ListNode slow = head; ListNode fast = head.next; while (slow != fast) { if (fast == null || fast.next == null) { return false; } slow = slow.next; fast = fast.next.next; } return true; }

为什么快指针走两步、慢指针走一步,两指针就一定能相遇?原因在于:当慢指针进入环之后,快指针已经在环里了。假设它们之间的距离是差N个节点,每走一次,快指针相对慢指针靠近一步,所以最多走N次就能追上。你把这个逻辑讲给面试官听,比干巴巴背代码有说服力得多。

这里有个容易忽略的细节:初始化时slow = head、fast = head.next是一种写法,也可以都从head开始,用 do-while 循环。两种写法都能过,但要注意判空的位置。我习惯从head和head.next开始,循环里先判fast是否为空,逻辑比较清晰。

延伸题型里还有“返回环的入口节点”。这个需要一点数学推导:快慢指针相遇时,把一个指针移回头部,另一个留在相遇点,然后两个指针都改成每次走一步,再次相遇的位置就是入口节点。这个推导过程面试官问到的概率很高,建议提前准备好。我当时是拿纸画了三四遍才真正理解,核心就是“相遇点到入口的距离等于头节点到入口的距离”这条性质。

2.3 合并两个有序链表——递归简洁但要注意栈深度

合并两个有序链表这个题在平时业务代码里其实很有用,比如合并两个排序好的日志列表、合并两个有序数据源。面试考这道题,主要是看你能不能把“两个指针逐个比较”这个过程写干净。

迭代法用到一个很实用的技巧:虚拟头节点(dummy node)。你可以把它理解成一个“占位”的空节点,它的next最终指向合并后的链表头,这样就不需要单独处理“第一个节点是哪个”的问题。

public ListNode mergeTwoLists(ListNode l1, ListNode l2) { ListNode dummy = new ListNode(0); ListNode cur = dummy; while (l1 != null && l2 != null) { if (l1.val <= l2.val) { cur.next = l1; l1 = l1.next; } else { cur.next = l2; l2 = l2.next; } cur = cur.next; } // 处理剩余部分 if (l1 != null) { cur.next = l1; } if (l2 != null) { cur.next = l2; } return dummy.next; }

这个写法里最妙的地方就是 dummy 节点。你想想,如果没有 dummy,合并后的头节点到底是 l1 还是 l2 的第一个节点,需要先比较一次再确定,代码就会多一层分支。有了 dummy,所有节点都统一按“cur.next 指向谁”来处理,最后直接返回 dummy.next 就行。这就是我常说的“用结构消除分支”。

递归版本的写法很漂亮,但理解起来需要一点抽象思维:

public ListNode mergeTwoListsRecursive(ListNode l1, ListNode l2) { if (l1 == null) return l2; if (l2 == null) return l1; if (l1.val <= l2.val) { l1.next = mergeTwoListsRecursive(l1.next, l2); return l1; } else { l2.next = mergeTwoListsRecursive(l1, l2.next); return l2; } }

递归的视角是:我只关心当前两个节点谁更小,剩下的交给递归去处理。这是分治思想的雏形。面试时能写出递归版并说清楚“递”和“归”的过程,会显得你对递归的理解很扎实。不过同样的,递归版在链表很长时也会栈溢出,实际工程我更推荐迭代版,面试时可以两个都提一下,说明你懂权衡。

2.4 找链表的中间节点和删除倒数第N个节点

这两个题都是快慢指针的经典应用,放一起说是因为思路高度一致:让一个指针先“多走几步”,再两个指针一起走。

找中间节点是让快指针每次走两步、慢指针每次走一步,当快指针走到末尾时,慢指针就是中间节点。如果链表长度是偶数,你可以选择返回靠左还是靠右的那个,跟面试官确认一下即可。

删除倒数第N个节点的做法是:先让一个指针从头走N步,然后另一个指针从头开始,两个指针一起走。当前一个指针走到末尾时,后一个指针正好在倒数第N个节点的前一个位置。这一步要特别注意:删除的是头节点的情况。处理方式是再加一个 dummy 节点,让两个指针都从 dummy 出发,这样即使删除的是头节点,也能用统一逻辑处理。

public ListNode removeNthFromEnd(ListNode head, int n) { ListNode dummy = new ListNode(0); dummy.next = head; ListNode first = dummy; ListNode second = dummy; // first 先走 n+1 步,因为是从 dummy 开始的 for (int i = 0; i <= n; i++) { first = first.next; } // 两个指针一起走 while (first != null) { first = first.next; second = second.next; } // second 现在指向待删除节点的前一个 second.next = second.next.next; return dummy.next; }

这个代码里有几个细节我想专门强调一下。第一,为什么 first 要先走 n+1 步而不是 n 步?因为 second 从 dummy 出发,如果 first 走 n+1 步,那么当 first 走到 null 时,second 正好在倒数第 n+1 个节点,也就是待删除节点的前一个。第二,删除节点不需要手动让被删除节点的 next 指向 null,Java 的 GC 会处理,你只需要把前一个节点的 next 跳过它就行。第三,返回的是 dummy.next 而不是 head,因为删除的可能是头节点。

3. 现场手写代码的实操过程与避坑要点

面试手写代码跟坐在 IDE 里写业务逻辑完全是两码事。没有自动补全,没法跑测试,只能靠眼睛和心理模拟。我见过很多代码写得不错的人,面试一写就乱,主要原因不是不会,而是“手写流程”不对。下面我把我自己的实操流程整理出来,按照这个流程走,能少踩很多坑。

3.1 手写链表题的标准流程:画图、列用例、写代码

我每次拿到链表题,不管多简单,都会在脑子里过这三个步骤。第一,画图。在草稿纸上画出链表的形态,标出每个节点的 next 指向。这不是浪费时间,而是强迫自己把抽象的引用关系具象化。很多错误在画图阶段就能暴露出来。

第二,列用例。至少列出三种情况:空链表、单节点链表、正常多节点链表。如果是删除类题目,加一个“删除头节点”的用例;如果是反转类题目,加一个“两个节点”的用例。这一步能帮你提前想清楚边界判断怎么写。

第三,写代码。写的时候注意几点:所有“访问 next 之前”的习惯性判空、循环终止条件的检查、返回值是 head 还是 dummy.next。写完之后不要急着交,用手里的用例在脑子里“跑”一遍。这就是俗称的“脑跑”,我发现很多人跳过了这一步,导致一些明显的越界错误没被发现。

这三个步骤看起来繁琐,但实际上链表题写多了之后,第二步可以压缩到几秒钟——你的大脑会自然建立起边界条件的条件反射。不过在练习阶段,我建议你老老实实走完,养成习惯比追求速度重要。

3.2 链表代码的几个典型坏习惯

代码风格在面试里会被暗中观察,尽管面试官不会直接说。说说我见过的几个典型坏习惯,大家引以为戒。

第一个是变量命名随意。有人用a、b、c来命名节点指针,代码短的时候还好,稍微长一点就看不懂了。我建议使用prev、curr、nextTemp、slow、fast这类表意明确的命名,既方便自己写,也方便给面试官讲。

第二个是嵌套判断过多,逻辑混乱。链表题最多两层循环加一层 if 就差不多了,如果你写出了三层嵌套,大概率是某个边界条件没想清楚,可以停下来重新画图,而不是继续堆代码。

第三个是忽略返回值。这个错误特别隐蔽。链表操作经常要修改链表的头节点,比如删除头节点、反转链表,这些操作之后头节点变了。很多新手写删除头节点时,函数返回的还是原来的 head,结果整个链表就丢了。所以每道题的返回值是 dummy.next 还是 head,必须想清楚,面试时我会习惯性地在函数最后一行注释“return 新头节点”来提醒自己。

4. 链表面试中容易翻车的常见问题与排查思路

链表题的 bug 其实高度规律化。我总结了这么几个高频问题,每个都是我或身边同事真实踩过的坑,你提前知道这些,现场排查会快很多。

4.1 三个高频翻车点:空指针、死循环、丢节点

空指针是所有链表题最经典的坑。Java 里访问node.next时如果node是 null,直接抛 NullPointerException。典型场景:反转链表时没有判空,直接对head.next操作;遍历时循环条件写了while (node.next != null)而 node 本身可能为 null。解决办法就一条:凡是“取 next”之前,先确认这个节点不是 null。这句话我在代码里反反复复强调,因为真的太多人栽在这上面了。

死循环的本质是链表里出现了环。反转链表时如果最后忘了把原头节点的 next 设为 null,链表就变成一个环。合并链表时如果两个指针没有同时推进,也可能造成原地打转。排查看两个地方:循环条件是否写得过宽,以及某个节点的 next 是否被错误地指向了之前的节点。最简单的验证方式是拿一个两节点或三节点的例子,手动模拟几轮循环。

丢节点是第三种经典问题,也是最隐蔽的。丢节点的本质是:你修改了某个节点的 next,但它的原本指向没有被保存下来,导致后续访问时拿不到那个节点了。前面反转链表里讲的nextTemp就是为了解决这个问题。还有一种丢节点的情况出现在删除时:你要删除节点 B,正确做法是A.next = B.next,写成了B = A.next,结果只是移动了局部变量指针,链表本身没有一点变化。

4.2 构建一个自测用例列表,把风险提前干掉

我强烈建议每个刷链表题的人都维护一个“自测用例模板”,不管是写在代码注释里还是记在笔记里。以下是我常用的用例列表:

用例场景链表形态你该检查什么
空链表null代码是否不报错直接返回
单节点1 -> null返回是否正确,是否会空指针
双节点1 -> 2 -> null反转后 2 -> 1,是否丢节点
正常链表1 -> 2 -> 3 -> 4功能结果是否正确
带环链表1 -> 2 -> 3 -> 2环检测是否返回 true
删除头节点1 -> 2 -> 3,删除第3个(倒数第1个)2返回的头节点是否更新

这套用例表基本覆盖了链表题 90% 的边界情况。每次写完代码,拿这几个列表过一遍,用最快的速度在脑子里模拟一下,能帮你避免绝大多数低级错误。面试官看你花三十秒做这个自我检查,印象分绝对比直接交卷高不少。

4.3 面试时被追问“还有别的方法吗”怎么办

在面试的场景里,写完第一版代码后,面试官十有八九会追问一句:“还有没有别的解法?”这句话听着有点压力,但其实是个展示机会。我建议你提前准备每个题目的两个解法,最常见的组合是“迭代 + 递归”,或者“双指针 + 哈希集合”。

比如判断链表是否有环,除了快慢指针,你还可以用哈希集合:遍历链表,把每个节点放进 HashSet,如果某个节点已经存在,说明有环。这个方案的优点是直观、时间复杂度同样是 O(n),缺点是空间复杂度 O(n)。面试时你可以主动说:“快慢指针是 O(1) 空间,如果允许空间换时间,用 HashSet 也可以做,逻辑更直白。”这种回答既展示了你的知识广度,也体现了对时空复杂度的敏感。

我记得有一次面试官追问反转链表的迭代法理解,我直接说“把链表想成一排手拉手的人,反转就是把每个手的方向换个边,但是换的时候要一只手先拉住下一个人的手再松开当前的手”——口头说的可比画图快多了,面试官听完还笑了。把复杂概念类比成生活场景,表达会顺畅很多。

5. 链表题的后续延伸:从单人挑战到组合应用

这一节不算面试必须,但我觉得价值很高。链表题刷顺了之后,你会发现很多“更高级”的题目其实就是基础题的组合。我举几个例子,帮你看清楚整个知识网络。

5.1 从反转链表到K个一组反转

K个一组反转链表是反转链表的高阶变体。它要求每K个节点一组反转,最后一组不够K个就不动。思路是:先写出一个“反转区间”的函数,再在主函数里分组调用。这个题如果能独立写出来,说明你对反转的理解不是背代码,而是真正掌握了“局部反转”的操作逻辑。

核心难点有两处。第一,每组反转后,要把上一组的结尾跟本组的开头连接起来,也就是需要记录每一组的 prev 和 next。第二,处理最后一组“不够K个”时,要把它反转回去。这两个问题本质上是“区间边界维护”,跟处理普通链表的边界是同一类思维。

这个题在业界和面试中都是常客,作为“必会题01”的延伸,很适合在刷完基础后再挑战。能把 K 个一组反转写明白的人,写其他链表题都会比较有底气。

5.2 从有序合并到归并排序

归并排序的链表版本是一个更综合的题目。它的基本流程是:找到链表中间节点,把链表分成两半,递归排序两半,最后用“合并两个有序链表”的方式把结果拼起来。你发现没有?这中间用到的技巧全是上面那些基础题:快慢指针找中间节点、递归分割、合并两个有序链表。

我第一次写出链表的归并排序时有一种“豁然开朗”的感觉,因为之前学的所有碎片技巧在这一刻全部串联了起来。链表版的归并排序时间复杂度是 O(n log n),空间复杂度是 O(log n)(递归栈),对比数组版有天然优势。这种题目如果面试问到了,你前面那些基础题打下的底子,正好可以全部发挥出来。

5.3 从循环单链表到约瑟夫环

如果你还准备蓝桥杯或者其他编程竞赛,循环单链表几乎是必考模型。约瑟夫环就是经典场景:N个人围成一圈,从某个位置开始报数,报数的人出列,直到只剩一人。这个问题的朴素做法之一就是用循环单链表模拟报数过程。链表在这个场景比数组自然,因为删除出列者只需要改动相邻节点的 next 指向,数组则需要移动后续元素。

顺带一提,热词里有“蓝桥杯数字题目”和“b3631 单向链表”这类词,如果你是冲着竞赛去刷题的,链表这些基础操作更是绕不开的起点。把单链表、循环单链表的插入、删除、遍历练熟之后,很多模拟类题目会轻松很多。竞赛题往往不会直接考一个“反转链表”,但会在更复杂的题目里要求你“操作链表”,不熟练的话很容易卡在那一步。

最后再讲一点我自己的实操体会

刷链表题这件事,我个人的感觉是:别追求数量,追求“一题多解”和“能讲清楚”。同一道反转链表,迭代写一遍、递归写一遍、头插法再写一遍,你写三遍的理解深度,远大于刷三道不同的题。面试官问“还有别的方法”,本质上就是想看你有没有做过这个层面的思考。

另外,我发现用一个小本子记录“自己写错的点”特别有效。比如我当时记过:反转链表忘记保存 nextTemp、合并链表忘记移动 cur、删除倒数第N个忘记 first 走 n+1 步。每次面试前翻一遍,比临时刷题管用得多。这些错误是属于自己的,跟从题解里抄来的笔记完全不同,记忆也会深刻得多。

最后给你一个非常实用的小技巧:链表题写完之后,自己在草稿纸上画一个两节点的例子,手动走一遍循环。这个过程不会超过三十秒,但能帮你发现 80% 的潜在问题。读着这篇文章的你,如果正好在准备 Java 面试,希望这一套“链表必会题”的第一篇能帮你建立起信心。先掌握基础,再谈延伸,链表的坑就那么几个,踩过了就通透了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 16:55:30

源码虚拟物品自动发货系统:支付回调、卡密池与授权绑定实战

简介&#xff1a;一套面向源码、素材等虚拟物品在线销售的PHPMySQL商城系统&#xff0c;适合个人站长、自由职业者用来搭建付费下载/内容变现平台。系统内置文章内容收费、资源下载收费、VIP每日下载额度、游客限时购买等模式&#xff0c;并支持免签收款、三级分销、佣金提现、…

作者头像 李华
网站建设 2026/10/7 16:54:29

OpenShell:让命令行效率回归的开源终端环境组合方案

1. OpenShell到底解决什么问题先说个直白的结论&#xff1a;OpenShell不是某个单一软件&#xff0c;而是一整套“让命令行回归效率”的开源组合方案。如果你每天都跟终端打交道&#xff0c;一定会有这种感觉——装了一堆工具&#xff0c;快捷键记不清&#xff0c;配置改乱了也不…

作者头像 李华
网站建设 2026/10/7 16:54:02

我的世界原版生存服运营实战:离线登录、生电稳定与社区生态

1. 你为什么还在找一个“看起来没什么特别”的原版生存服&#xff1f;说实话&#xff0c;我玩我的世界也有小十年了&#xff0c;从1.7.10一路玩到现在的JAVA 26.X时代&#xff0c;开过服、跑过图、炸过存档、也见过路人从萌新变成红石大神。每次看到《CloudRain》这种招新帖&am…

作者头像 李华
网站建设 2026/10/7 16:52:53

VSCode插件离线导出与迁移:清单备份到VSIX分发完整指南

换电脑、重装系统、或者被分配了一台只能连内网的开发机&#xff0c;这些场景下“VSCode 插件怎么搬过去”几乎是每个用 VSCode 的人都绕不开的问题。所谓“导出 VSCode 插件到本地”&#xff0c;简单说就是把已经装好的扩展变成一份可以带走、可以分发、可以离线安装的东西。它…

作者头像 李华
网站建设 2026/10/7 16:52:28

UPX脱壳与base62解码:BUUCTF逆向题[GUET-CTF2019]re解析

刷完几道简单题就兴冲冲点开 BUUCTF 的 reverse 列表&#xff0c;看到[GUET-CTF2019]re这种短名字&#xff0c;我第一反应是&#xff1a;又来一道送分题。结果从晚上八点折腾到凌晨一点&#xff0c;中途一度怀疑自己是不是真的适合搞逆向。卡住我的不是复杂算法&#xff0c;而是…

作者头像 李华