几道常见的链表算法题
链表没有随机访问,所有操作最终都归结为改写 next 指针,而指针一改错,后半段链表就可能永久丢失。本篇挑五道面试中出现频率最高的题:反转(迭代与递归两种)、判圈与找入口、合并两个有序链表、一趟删除倒数第 N 个节点、两两交换,每题按”一句话思路、关键代码、易错点”组织。
反转链表
思路:迭代法维护 prev、cur、next 三个指针,每轮把 cur 的指向掉头;递归法则先反转后半段,再把当前节点挂到末尾。
迭代的关键是守住一条不变量:循环开始时,prev 左侧是已经反转好的部分,cur 右侧是待处理部分。每轮先做 next = cur.next 把后继存下来,再执行 cur.next = prev 完成掉头,最后 prev、cur 整体右移一格。循环结束时 cur 为 null,prev 正好停在新头节点上。时间 O(n),额外空间 O(1)。
// 迭代写法 |
递归版好写但要付出 O(n) 的栈空间,链表很长时有栈溢出风险,工程代码里更推荐迭代。
易错点:掉头前不保存后继,链表直接断成两截;递归漏写 head.next = null 会让最后两个节点互相指向、形成环;返回值是 prev 而不是已经走到 null 的 cur。
环形链表:判圈与找入口
思路:快慢指针从头出发,快的一次两步、慢的一次一步,若能相遇则有环;之后让一个指针回到头部,与慢指针同速前进,再次相遇的位置就是环入口。
判圈的直觉是”操场套圈”:环形跑道上快的人总会从背后追上慢的人,且相对速度为一格,不会跳过。无环时快指针先到达 null,循环自然退出。
找入口为什么正确?设头到入口 a 步,入口到首次相遇点 b 步,相遇点沿环回到入口 c 步。相遇时慢指针走了 a+b,快指针多绕了 k 圈、走了 a+b+k(b+c)。由”快指针路程是慢指针的两倍”得 a+b = k(b+c),整理出 a = (k-1)(b+c) + c——从头走 a 步,等价于从相遇点绕 k-1 圈后再走 c 步,二者必然停在同一位置,正是入口。
public ListNode detectCycle(ListNode head) { |
易错点:循环条件必须写成 fast != null && fast.next != null,只判 fast 会在 fast.next.next 处空指针;要先移动再判断相等,否则起点就被误认为相遇;只判圈不求入口时,相遇即可返回 true,不需要第二阶段。
合并两个有序链表
思路:借助哑节点和尾指针,每轮比较两条链表的当前头,把较小的接到结果尾部;一条耗尽后,另一条剩余部分整体挂上。
public ListNode mergeTwoLists(ListNode l1, ListNode l2) { |
这道题也有简洁的递归写法:较小的节点作为结果头,它的 next 指向”剩余部分合并”的递归结果。递归版代码短,但同样要承担 O(n) 的调用栈开销。迭代版时间 O(n+m)、空间 O(1),且复用原节点、不产生新分配。
易错点:循环结束后直接把整条剩余链接上即可,不要逐节点新建;返回值是 dummy.next,写成 return dummy 会把值为 0 的哑节点也带出去。
删除倒数第 N 个节点
思路:快慢指针都从哑节点出发,快指针先走 n+1 步,然后两者同步前进;快指针到尾时,慢指针恰好停在待删节点的前驱,一趟扫描完成删除。
朴素做法是先遍历一遍数出长度 L,把”倒数第 N 个”换算成”正数第 L-N+1 个”,再遍历第二遍删除,要走两趟。快慢指针技巧的本质是让两个指针始终保持 N+1 的间距,这样快指针撞线时,慢指针自然落在目标前驱上,把两趟压缩成一趟。
public ListNode removeNthFromEnd(ListNode head, int n) { |
易错点:先走的是 n+1 步而不是 n 步——只有停在”前驱”位置,删除头节点(即倒数第 L 个)这种边界才能被哑节点统一兜住;题目不保证 n 合法时,先走阶段要判空,否则快指针越界。
两两交换相邻节点
思路:哑节点之后每轮处理一对节点 a、b,按固定顺序改三条边——a.next = b.next、b.next = a、prev.next = b,然后把 prev 移到 a 进入下一轮。
public ListNode swapPairs(ListNode head) { |
每轮操作前 prev 指向待交换对的前一个节点,三条边改完后,这一对的顺序变成 b、a,且与后文衔接完好。节点数为奇数时最后一个节点自然落单、保持不动。
易错点:三条边的改写顺序不能乱,必须先把 b.next 交给 a.next 再动 b.next,否则后半段丢失;循环条件是后面至少剩两个节点;prev 每轮前进两格(移动到 a),只前进一格会把已交换的节点再卷进来。
小结
五道题其实共用三个套路:哑节点抹平”头节点被删或被换”的边界、快慢指针处理环检测与”倒数第 N”这类相对距离问题、改指针之前先把后继存好。复杂度上都是 O(n) 时间,迭代版 O(1) 额外空间,递归版多付出 O(n) 栈空间。写完代码用空表、单节点、两节点三组最小用例手跑一遍,比背模板更能拦住指针错误。

