跳到正文
第 19 题中等

删除链表的倒数第 N 个结点

Remove Nth Node From End of List

删除链表倒数第 n 个节点并返回头节点,一趟扫描完成。

在力扣中练习

题目描述

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

 

示例 1:

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

示例 2:

输入:head = [1], n = 1
输出:[]

示例 3:

输入:head = [1,2], n = 1
输出:[1]

 

提示:

  • 链表中结点的数目为 sz
  • 1 <= sz <= 30
  • 0 <= Node.val <= 100
  • 1 <= n <= sz

 

进阶:你能尝试使用一趟扫描实现吗?

解题思路

主解法

快慢指针 · 一趟扫描

两人隔开 n+1 步,fast 到底时 slow 正好停在待删节点的前驱

先数长度再删除需要走两遍。快慢指针通过固定间隔,把“倒数第 n 个”转成“fast 走到空时,slow 正好停在目标前驱”。

自拟主例 head=[8,3,8,-1,5,2],n=3,删除 -1,结果 [8,3,8,5,2]。两个 8 是不同节点,图中用稳定的位置编号区分。

分步图解

fast 和 slow 都从哑节点出发;需要让 fast 先走 n+1 步。

节点身份按位置区分,两个 8 不会混淆值 · next 引用
null哑dummy节点 dummy,值 哑,next → node-0fastslow8#0节点 #0,值 8,next → node-13#1节点 #1,值 3,next → node-28#2节点 #2,值 8,next → node-3-1#3节点 #3,值 -1,next → node-45#4节点 #4,值 5,next → node-52#5节点 #5,值 2,next → null
1 / 3
从哑节点出发先拉开 n+1 步,slow 最终才会停在前驱。

两指针从哑节点出发,fast 先走 n+1 步,再一起移动。这里多走的一步是为了让 slow 停在前驱,而不是待删节点本身。

分步图解

先行第 4/4 步,fast=-1。

节点身份按位置区分,两个 8 不会混淆值 · next 引用
null哑dummy节点 dummy,值 哑,next → node-0slow8#0节点 #0,值 8,next → node-13#1节点 #1,值 3,next → node-28#2节点 #2,值 8,next → node-3-1#3节点 #3,值 -1,next → node-4fast5#4节点 #4,值 5,next → node-52#5节点 #5,值 2,next → null
1 / 5
同步移动直到 fast 为空,然后只改前驱的一条 next。

保存 victim=slow->next,让前驱指向 victim 的后继,再释放 victim。返回 dummy.next,不能仍返回旧 head;哑节点让删除头节点也走同一套逻辑。只改变一条引用,其余节点身份与相对顺序保持不变。

时间 O(n)空间 O(1)

易错点

  • fast 只走 n 步:slow 会停在待删节点本身,拿不到前驱,删不掉。
  • 删完返回 head:如果删的正是头节点,head 已经不在链表里了。
  • 把同步前进写成先移 slow 再判断 fast,会多走一步,删错节点。
对照解法

两次遍历 · 数长度

先数出总长 L,再走 L−n 步找前驱

正确的两次遍历,先数长度再找前驱;思路直观,主解法则用固定间隔只走一遍。

最朴素的思路,先确认它为什么也对:

  1. 第一趟 for (ListNode* p = head; p; p = p->next) ++len 得到总长度 L;
  2. 要删的是正数第 L - n + 1 个,它的前驱是正数第 L - n 个;
  3. 从 dummy 出发走 L - n 步,就停在那个前驱上,改一次 next 即可。

这里仍然保留 dummy,原因和一趟版一样:L - n == 0(要删头节点)时前驱就是 dummy,代码不需要分支。

两趟扫描的时间也是 O(n),只是常数大一点;空间同为 O(1)。面试时如果一趟版写不顺,两趟版至少保证正确,再往一趟优化。

  1. 1第一趟遍历,数出链表长度 len。
  2. 2建 dummy,从 dummy 出发走 len - n 步。
  3. 3prev->next = prev->next->next,跳过目标节点。
  4. 4返回 dummy.next。
时间 O(n)空间 O(1)

易错点

  • 把 len - n 写成 len - n + 1,会停在待删节点上,删掉的是它的后继。
  • 忘了 dummy,删头节点时 prev 为空,直接崩溃。

代码与演示

主解法配有图解;对照解法用于比较思路、复杂度或实现方式。

演示输入head = [8,3,8,-1,5,2], n = 3

fast 和 slow 都从哑节点出发;需要让 fast 先走 n+1 步。

节点身份按位置区分,两个 8 不会混淆值 · next 引用
null哑dummy节点 dummy,值 哑,next → node-0fastslow8#0节点 #0,值 8,next → node-13#1节点 #1,值 3,next → node-28#2节点 #2,值 8,next → node-3-1#3节点 #3,值 -1,next → node-45#4节点 #4,值 5,next → node-52#5节点 #5,值 2,next → null
1 / 10
查看 C++ 代码

延伸阅读

  • 快慢指针的「间距」就是倒数序号的度量衡:这类题的关键是让间距等于 k,而不是背具体步数。
  • dummy 哨兵在链表题里几乎万能:删除、插入、合并、拆分都可以先加一个假头,省掉边界特判。
  • 同源题:环形链表(141)用快慢指针判相遇,环形链表 II(142)再加一次从头同步走来定位入口。