删除倒数第 N 个结点 图解题解
不知道链表多长,怎么找倒数第 N 个?fast 先走 N 步,然后两个指针一起走,fast 到尾 slow 就到位——一遍搞定。
删倒数第 N 个节点:fast 先走 N 步,然后 fast 和 slow 一起走,fast 到链表末尾时 slow 正好停在待删节点的前一个——两者始终差 N 格,fast 到头 slow 就到位了。前面加一个 dummy 虚拟头节点,这样就算要删的是第一个节点,slow 也有左邻居可以直接改 next 断链,不用单独处理边界。
这道题到底在问什么
- 输入
- head=[1,2,3,4,5], n=2
- 输出
- [1,2,3,5]
最优解:为什么这么做
一句话答案:LeetCode 19 删除链表的倒数第 N 个结点,一次遍历的解法是快慢指针加哑结点:fast 先从哑结点出发走 n+1 步,然后与 slow 保持这个间距同速前进,fast 走到 null 时 slow 恰好停在待删结点的前驱上,改一次 next 即完成删除。时间 O(L)、空间 O(1)。
这道题真正在问什么
给单链表的头节点和一个数 n,删掉倒数第 n 个结点后返回头。难点全在「倒数」两个字上:单链表只能从头往后走、不能回头,而且事先不知道总长度——站在任何一个节点上,你都说不清它是倒数第几个。
两遍扫描为什么能做但不够好
最直接的思路是先扫一遍数出链表总长 L,「倒数第 n 个」就换算成「正数第 L-n+1 个」,再从头走到它的前一个节点做删除。完全正确,但要把链表扫两遍。面试的经典追问是:能不能只走一遍?
关键观察是:「倒数第 n」这个信息虽然单个指针拿不到,但可以翻译成一个相对关系——待删结点与链表末尾之间恰好隔着固定的距离。距离是可以用两个指针「保持队形」来维护的:先把两个指针拉开固定间距,再让它们同速前进,前面的到达终点时,后面的位置就被这个间距唯一确定了。
为什么 fast 要先走 n+1 步而不是 n 步
删除链表节点必须拿到它的前驱,因为删除动作是「前驱.next 跳过待删点」。所以我们真正想让 slow 停的位置不是待删结点本身,而是它前面一个。
把间距设为 n+1:fast 走到 null(越过末尾一格)时,slow 距离末尾 n+1 格,正好落在倒数第 n 个结点的前驱上。如果只拉开 n 步,slow 会停在待删结点身上——单链表到了这里已经拿不到前驱,只能用「复制后继值再删后继」之类的别扭补救。多走的那一步,本质是给「删除需要前驱」这个约束预留的。
哑结点是怎么把删头特判消掉的
有一种情况会让「找前驱」失效:n 恰好等于链表长度,待删的就是头结点,而头结点天然没有前驱。在头前面垫一个不存数据的哑结点 dummy,让 fast 和 slow 都从 dummy 出发,头结点就有了统一的前驱,删头和删中间、删尾用同一段代码,最后返回 dummy.next 就是新头。
这也是链表题的通用经验:凡是「可能对头结点做修改」的操作(删除、插入、反转),加个哑结点几乎总能把边界分支压成零。
复杂度与正确性小结
时间 O(L):fast 从头到尾恰好走一遍链表,slow 走的路程更短,总步数是线性的。空间 O(1):只有两个指针加一个哑结点。
正确性可以一句话说清:两个指针同速移动时间距永远不变,所以「fast 到 null」与「slow 在倒数第 n+1 个」是同一时刻发生的同一件事,不依赖链表内容,也不需要知道 L 的具体值。题目保证 n 不超过链表长度,所以 fast 先走 n+1 步时最多走到 null,不会越界。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3记住这条间距:fast 先冲 N+1 步,slow 永远落后 N+1 个身位。
- 4哑结点 D 接在头前。fast、slow 都从 D 出发——D 不是真实结点,只为统一删头的写法。
- 5fast 单独往前走一格(现在第 1/3 步)。slow 还按兵不动,间距正一格一格拉开。
- 6fast 单独往前走一格(现在第 2/3 步)。slow 还按兵不动,间距正一格一格拉开。
- 7fast 单独往前走一格(现在第 3/3 步)。slow 还按兵不动,间距正一格一格拉开。
- 8fast 走满 3 步停下。此刻 fast 与 slow 之间恰好隔 3 格——这就是「倒数第 N」的秘密:保持这个间距一起走即可。
- 9现在 fast、slow 同速前进。先看 slow 挪一格——间距始终保持不变。
- 10fast 也挪一格跟上。两个指针像被一根长度固定的杆连着,一起往末尾滑。
- 11现在 fast、slow 同速前进。先看 slow 挪一格——间距始终保持不变。
- 12fast 也挪一格跟上。两个指针像被一根长度固定的杆连着,一起往末尾滑。
- 13现在 fast、slow 同速前进。先看 slow 挪一格——间距始终保持不变。
- 14fast 也挪一格跟上。两个指针像被一根长度固定的杆连着,一起往末尾滑。
- 15现在 fast、slow 同速前进。先看 slow 挪一格——间距始终保持不变。
- 16fast 也挪一格跟上。两个指针像被一根长度固定的杆连着,一起往末尾滑。
- 17现在 fast、slow 同速前进。先看 slow 挪一格——间距始终保持不变。
- 18fast 也挪一格跟上。两个指针像被一根长度固定的杆连着,一起往末尾滑。
- 19现在 fast、slow 同速前进。先看 slow 挪一格——间距始终保持不变。
- 20fast 再走一步就越过了末尾(指向 null)。停!此刻 slow 落在哪?——正好是「待删结点的前驱」。
- 21fast 到 null,slow 停在值 6——它正是待删结点(值 7)的前驱。下一步让前驱跳过待删点。
- 22把前驱的 next 跨过灰掉的值 7,直接接到它的下一个(值 8)。值 7 就此从链上脱钩。
- 23删除完成。返回哑结点的 next(跳过 D 本身),得到结果链表 1→2→3→4→5→6→8。一次遍历搞定,没有先数长度。
⚠️ 容易写错的地方
✗ 错:fast 先走 n 步
✓ 对:fast 先走 n+1 步
多走一步才能让 slow 停在「前驱」而非待删点本身
✗ 错:不用哑结点
✓ 对:加哑结点 D
删头结点时 slow 没有前驱会特判,哑结点统一写法
✗ 错:直接 head 当 slow 起点
✓ 对:slow 从哑结点起
保证 slow 落在待删点前一个
完整代码(Python / C++ / Java)
Python
def removeNthFromEnd(head, n):
dummy = ListNode(0, head) # 哑结点统一删头/删中
fast = slow = dummy
for _ in range(n + 1): # fast 先走 n+1 步
fast = fast.next
while fast: # 一起走到 fast 出末尾
fast = fast.next
slow = slow.next
slow.next = slow.next.next # 前驱跳过待删点
return dummy.nextC++
ListNode* removeNthFromEnd(ListNode* head, int n){
ListNode dummy(0, head);
ListNode *fast = &dummy, *slow = &dummy;
for(int i = 0; i < n + 1; i++) fast = fast->next;
while(fast){ fast = fast->next; slow = slow->next; }
slow->next = slow->next->next;
return dummy.next;
}Java
ListNode removeNthFromEnd(ListNode head, int n){
ListNode dummy = new ListNode(0, head);
ListNode fast = dummy, slow = dummy;
for(int i = 0; i < n + 1; i++) fast = fast.next;
while(fast != null){ fast = fast.next; slow = slow.next; }
slow.next = slow.next.next;
return dummy.next;
}复杂度
时间
O(L)
指针只走一遍链表
空间
O(1)
仅两个指针 + 哑结点
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 删除倒数第 N 个结点 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么用哑结点而不直接判 head?+
当待删的是头结点时,slow 需要一个「头之前的前驱」才能改 next;哑结点正好充当它,避免单独写删头分支。
能不能不用双指针?+
可以先遍历一遍求长度 L,再走 L-n 步到前驱——但要扫两遍。双指针一遍完成、更优。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 删除倒数第 N 个结点 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。