一、题目
给你一个链表,删除链表的倒数第 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 <= 300 <= Node.val <= 1001 <= n <= sz
二、解答
题目分析
题目要求
给定一个链表的头节点 head 和一个整数 n,需删除链表的倒数第 n 个节点,并返回删除后的链表头节点。
关键条件
-
倒数定位:需通过链表的倒数位置确定待删除节点,而非正数位置。
-
边界处理:
- 当链表仅有一个节点(如示例 2)时,删除后返回空链表。
- 当删除头节点(如示例 3 中
n=1时),需直接修改头节点引用。
-
一次遍历要求:理想解法需在单次遍历内完成定位和删除,避免两次遍历的额外时间开销。
核心思路
双指针法(快慢指针) :通过维护两个指针的固定间隔,在一次遍历中定位目标节点的前驱节点,从而实现高效删除。
-
哑节点辅助:引入哑节点(dummy node)作为头节点的前驱,统一处理头节点删除的边界情况。
-
指针间隔控制:
- 快指针(first)先移动
n步,与慢指针(second)形成n步的间隔。 - 随后两指针同步移动,当快指针到达链表末尾时,慢指针恰好指向倒数第
n+1个节点(即待删除节点的前驱)。
- 快指针(first)先移动
-
删除操作:通过修改慢指针的
next指针,跳过待删除节点,完成删除。
示例分析
示例 1
输入:head = [1,2,3,4,5], n = 2
分析:
-
链表长度为 5,倒数第 2 个节点是值为 4 的节点(正数第 4 个节点)。
-
使用双指针:
- 快指针先移动 2 步,指向节点 3(值为 3),慢指针指向哑节点。
- 同步移动两指针至快指针到达末尾(null),此时慢指针指向节点 3(倒数第 3 个节点,即待删除节点的前驱)。
- 删除慢指针的下一个节点(值为 4),链表变为 [1,2,3,5]。
输出:[1,2,3,5]
示例 2
输入:head = [1], n = 1
分析:
- 链表仅有一个节点,倒数第 1 个节点即头节点。
- 哑节点的
next指向头节点,快指针移动 1 步后指向 null,慢指针仍指向哑节点。 - 删除慢指针的下一个节点(头节点),返回哑节点的
next(null)。
输出:[]
示例 3
输入:head = [1,2], n = 1
分析:
- 倒数第 1 个节点是值为 2 的节点(正数第 2 个节点)。
- 快指针移动 1 步后指向节点 2,慢指针指向哑节点。
- 快指针继续移动至 null,慢指针仍指向哑节点,此时删除慢指针的下一个节点(值为 1 的头节点?不,注意:n=1 时,快指针初始移动 1 步后指向节点 2,同步移动后快指针变为 null,慢指针指向哑节点。待删除节点是慢指针的下一个节点的下一个节点(哑节点.next 是头节点 1,头节点的 next 是 2,待删除的是 2,因此慢指针(哑节点)的 next 应指向 2 的 next(null),最终头节点仍为 1)。
输出:[1]
算法思路
预处理
- 创建哑节点:
dummy = ListNode(0),其next指向头节点head,便于统一处理头节点删除。 - 初始化双指针:快指针
first和慢指针second均指向哑节点。
双指针移动
- 快指针先行:将
first移动n步,使其与second间隔n个节点。 - 同步移动:同时移动
first和second,直至first到达链表末尾(null)。此时,second指向倒数第n+1个节点(即待删除节点的前驱)。
删除节点
修改 second 的 next 指针,使其跳过待删除节点(second.next = second.next.next),完成删除。
复杂度分析
- 时间复杂度:O (L),其中 L 为链表长度。仅需一次遍历(快指针移动 L+1 步,慢指针移动 L+1 - n 步)。
- 空间复杂度:O (1),仅使用常数额外空间(哑节点和双指针)。
代码(python)
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
class Solution:
def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
def getLength(head: ListNode) -> int:
length = 0
while head:
length += 1
head = head.next
return length
dummy = ListNode(0, head)
length = getLength(head)
cur = dummy
for i in range(1, length - n + 1):
cur = cur.next
cur.next = cur.next.next
return dummy.next
这段代码使用两次遍历来解决问题:第一次遍历计算链表长度,第二次遍历定位并删除目标节点。下面是对代码的详细分析:
方法思路
- 计算链表长度:通过辅助函数
getLength遍历链表,统计节点总数length。 - 确定删除位置:倒数第
n个节点的正数位置为length - n + 1(链表索引从 1 开始)。 - 哑节点辅助:创建哑节点
dummy指向头节点,处理删除头节点的特殊情况。 - 定位前驱节点:从哑节点开始,移动
length - n步,到达目标节点的前驱节点。 - 删除操作:修改前驱节点的
next指针,跳过目标节点。
代码解释
-
辅助函数
getLength:- 遍历链表,累加计数器
length,直到链表末尾(head为None)。 - 时间复杂度为 O (L),L 为链表长度。
- 遍历链表,累加计数器
-
哑节点的作用:
- 创建哑节点
dummy,其next指向头节点head。 - 当需要删除头节点时(如
n=length),哑节点确保操作的一致性,避免单独处理头节点的边界情况。
- 创建哑节点
-
定位前驱节点:
- 计算正数位置
length - n + 1,但由于从哑节点开始移动,实际需移动length - n步。 - 循环
for i in range(1, length - n + 1)执行length - n次,使cur指向目标节点的前驱。
- 计算正数位置
-
删除操作:
- 通过
cur.next = cur.next.next直接跳过目标节点,完成删除。
- 通过
复杂度分析
- 时间复杂度:O (L),其中 L 为链表长度。需两次遍历,每次遍历时间为 O (L)。
- 空间复杂度:O (1),仅使用常数级额外空间(哑节点和指针)。