题目
给你一个链表,删除链表的倒数第 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
进阶: 你能尝试使用一趟扫描实现吗?
题目解析
思路:
本题的解题关键是如何在一次遍历中定位到倒数第 N 个结点。传统的方法是先遍历一次链表得到链表的总长度 L,然后再遍历到第 L-N 个结点进行删除操作。然而,这需要两次遍历,效率较低。为了实现一次遍历,我们可以使用双指针法。
具体算法如下:
- 初始化两个指针 fast 和 slow,并将它们都指向一个哑结点,哑结点的 next 指针指向链表的头结点。
- 将 fast 指针向前移动 n+1 步,达到倒数第 n 个结点的前一个位置。
- 同时移动 fast 和 slow 指针,直到 fast 指针到达链表末尾。此时 slow 指针将指向倒数第 n+1 个结点。
- 删除操作:将 slow 指针的 next 指向 next.next,这样就跳过了倒数第 n 个结点,达到了删除的目的。
- 返回哑结点的 next 指针,即链表的新头结点。
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
dummy = ListNode(0, head)
fast = slow = dummy
# 快指针先前进 n+1 步
for _ in range(n + 1):
fast = fast.next
# 同时移动快慢指针,直到快指针到达链表末尾
while fast:
fast = fast.next
slow = slow.next
# 删除倒数第 n 个节点
slow.next = slow.next.next
return dummy.next
执行:
总结
删除链表的倒数第 N 个节点是一个链表操作问题,这类问题通常要求对链表的结构进行修改,同时保持链表的其他部分不变。这个特定的问题要求在一趟扫描中完成任务,这就排除了使用两次遍历来解决问题的方法。适用于这种情况的算法需要能够在不完全遍历整个链表的情况下定位到目标节点。
这类问题适用的方法是双指针法,它在处理需要同时访问链表中多个位置的节点时特别有效。具体到删除倒数第 N 个节点的问题,双指针法可以用以下步骤实现:
- 哑节点:创建一个哑节点作为辅助,它的
next指针指向链表的头节点。这样做的好处是可以简化对头节点进行操作的逻辑,尤其是在头节点需要被删除时,哑节点使得我们不必特殊处理头节点的情况。 - 快慢指针初始化:初始化两个指针,快指针(fast)和慢指针(slow),并将它们都置于哑节点。
- 快指针先行:将快指针向前移动 N+1 步,这样快指针和慢指针之间就保持了 N 个节点的间隔。
- 同步移动指针:同时移动快指针和慢指针,直至快指针到达链表的末尾。此时慢指针指向倒数第 N+1 个节点,即目标节点的前一个节点。
- 删除目标节点:修改慢指针的
next指针,使其跳过下一个节点(即倒数第 N 个节点),从而实现删除操作。 - 返回结果:返回哑节点的
next指针,即新的链表头节点。
这种方法的算法复杂度是高效的,时间复杂度为 O(L),其中 L 是链表的长度,因为我们只需要遍历链表一次。空间复杂度为 O(1),因为我们只需要常数级的额外空间。
使用场景:
双指针法非常适合解决与链表相关的问题,尤其是当我们需要定位链表的特定位置或是在没有给出链表长度的情况下完成操作时。除了删除倒数第 N 个节点的问题,双指针法也可以用于:
- 寻找链表的中间节点
- 判断链表是否有环
- 寻找链表环的入口
- 寻找两个链表的交点
掌握了双指针法,我们不仅能够解决特定的链表问题,还能够拓展到其他相关问题的解决方案。这种方法是链表问题中的一种基础且强大的技巧。