
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 先走 n1 步因为是从 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 要先走 n1 步而不是 n 步因为 second 从 dummy 出发如果 first 走 n1 步那么当 first 走到 null 时second 正好在倒数第 n1 个节点也就是待删除节点的前一个。第二删除节点不需要手动让被删除节点的 next 指向 nullJava 的 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 走 n1 步。每次面试前翻一遍比临时刷题管用得多。这些错误是属于自己的跟从题解里抄来的笔记完全不同记忆也会深刻得多。最后给你一个非常实用的小技巧链表题写完之后自己在草稿纸上画一个两节点的例子手动走一遍循环。这个过程不会超过三十秒但能帮你发现 80% 的潜在问题。读着这篇文章的你如果正好在准备 Java 面试希望这一套“链表必会题”的第一篇能帮你建立起信心。先掌握基础再谈延伸链表的坑就那么几个踩过了就通透了。