如果你已经刷过几十道链表题,回头看 LeetCode 19 删除链表的倒数第N个节点,你可能会觉得它并不难。但这道题偏偏是我见过面试翻车率最高的链表题之一:有人上来就两次遍历,有人能写出快慢指针却处理不好删除头节点,还有人把“倒数第 N 个”硬生生理解成“正数第 N 个”。
这道题表面问的是“删除一个节点”,实际考的是三件事:链表只能单向遍历的约束、删除动作必须找到前驱节点、以及各种边界条件下的指针处理。无论你刚入门数据结构,还是准备算法面试,把 LeetCode 19 吃透,等于把链表题里最常用的一组基本功打了个底。这篇内容我会从最朴素的遍历思路讲起,重点拆解快慢指针为什么是“一趟扫描搞定”的关键,也用真实测试样例带你把边界情况走一遍。
1. 题目在考什么:拆穿“倒数第 N 个”这层窗户纸
1.1 “倒数”在单向链表里是一道翻译题
题目本身一句话就能说清:给你一个单链表的头节点 head,再给你一个整数 n,删除链表的倒数第 n 个节点,返回链表的头节点。题目给出的链表结构基本长这样:
python复制class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
很多初学者第一次看到“倒数”就懵了。数组里想找倒数第 N 个元素,直接用 arr[length - N] 就能定位,因为数组支持随机访问。但单链表每个节点只保存了一个后继指针,你只能从 head 开始,通过 next 一路往前走,永远不知道终点在哪里,更没法从尾部向回退。
所以“删除倒数第 N 个节点”这个问题,本质上是一个翻译题:你没法直接对“倒数”做下标操作,只能先把“倒数”翻译成“从某个位置开始走多少步”。最直观的翻译方式就是先遍历一遍,数出链表长度 L,那么倒数第 N 个节点就是正数第 L - N + 1 个节点。这个思路没问题,也是很多参考解法里“两次遍历”的由来。
但 LeetCode 19 有一个进阶要求:能不能用一趟扫描完成?这个“一趟扫描”的要求就引出了双指针,也是面试官真正想看到的点。两次遍历只是做了翻译,双指针才是对链表结构更深的理解。
1.2 链表删除的本质:先找到待删节点的前驱
写这道题时很多新手会有一个下意识错误:他们想用一个指针 cur 从头遍历,等 cur 指向待删除节点时,再想办法把 cur 自己删掉。但单链表做不到这一点,因为一个节点并不知道它的前一个节点是谁。
正确的删除操作其实是“跳过”:
python复制pre.next = pre.next.next
意思就是:让待删节点前一个节点 pre 的 next 指针,直接越过待删节点,指向它后面的节点。至于被跳过的节点,在 Python 里由垃圾回收处理;在 C++ 这类语言里,还得手动释放内存。这个细节对 LeetCode 19 尤其关键,因为如果待删节点恰好是头节点,那么它根本没有前驱,所以我们的操作会退化成一个特殊分支:head = head.next。
一个链表删除动作背后,其实藏着三处基本功:一是遍历链表,二是定位前驱,三是处理边界。LeetCode 19 巧妙地把这三样东西缝在了一道题里,所以它才适合作为链表面试的第一道经典题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 一次遍历的钥匙:快慢指针与哑节点的配合
2.1 哑节点的作用:让删除头节点的场景不再特殊
先说说哑节点,英文里叫 dummy node。做法是在真正的头节点前面额外加一个虚拟节点,它不存储业务数据,只是为了让代码统一处理删除操作。
为什么需要它?我前面说过,删除节点必须找到前驱。当你想删除的是整个链表的头节点时,从逻辑上来讲,head 之前并没有节点,所以你必须写一条单独的逻辑去处理“删头节点”的情况。加入 dummy 之后,dummy 变成 head 的前驱,删除头节点就变成普通的“让 dummy.next 跳过 head”的操作,不再需要 if-else 分支。
在 LeetCode 19 的代码里,我们通常这样建:
python复制dummy = ListNode(0)
dummy.next = head
最后返回 dummy.next,而不是直接返回 head。原因是如果删掉的是原头节点,head 变量已经被跳过,直接返回 head 就会出错;而 dummy.next 永远指向删除后的真正头节点。
我见过不少人在这一步偷懒:函数参数里有 head,就直接拿 head 当链表的头去操作。表面省了一个节点,实际会在头节点删除和返回值两个地方埋雷。面试时能主动提出“用哑节点统一头节点与普通节点的删除逻辑”,通常是一个加分项。
2.2 快慢指针的间隔:先走 n 步,还是先走 n+1 步?
快慢指针的思路说起来很形象:两个指针从同一起点出发,快指针先走 n 步,慢指针原地不动。此时快指针领先慢指针 n 个节点。接下来
