19. 删除链表的倒数第 N 个节点 (Medium)

专题归类: 04-链表 LeetCode 链接: https://leetcode.cn/problems/remove-nth-node-from-end-of-list/


在线做题: 写 Python 代码并运行 · Java 跳转 LeetCode

题目描述

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

示例:

  • 输入:head = [1,2,3,4,5], n = 2
  • 输出:[1,2,3,5]
  • 解释:删除倒数第 2 个节点(值为 4)

题目详细分析

  • 数据范围: 链表中节点数在 [1, 30] 范围内,n 保证是有效的(1 ≤ n ≤ 链表长度)
  • 输入特征: 单链表,只能从头到尾遍历。n 从 1 开始计数(倒数第 1 个是尾节点)
  • 核心约束: 只能一次遍历完成(进阶要求),不能先遍历算长度再删
  • 隐藏条件: 删除节点需要找到待删节点的前一个节点。删除头节点时没有前驱,需要特殊处理
  • 边界条件: 删除头节点(n = 链表长度)、删除尾节点(n = 1)、链表只有一个节点

小白版直白理解

就像你不知道一列火车有多长,但想知道从车尾往前数的第 n 节车厢是哪个,然后把它去掉。

一个聪明的办法:找两个朋友,让第一个朋友先往前多走 n 步(提前出发),然后两个朋友以相同速度往前走。当第一个朋友走到车尾时,第二个朋友正好站在待删车厢的前一节。因为第一个朋友提前走了 n 步,所以第二个朋友距离车尾正好还有 n 步。

为了防止删的是车头(第一节车厢),我们在火车前面加一个虚拟的「第 0 节车厢」,这样不管删哪节都有前驱。


解题思路

思路一:快慢指针 + 虚拟头节点(推荐)

为什么这样想: 要找到倒数第 n 个节点,本质是找到距离末尾 n 步的节点。但单链表不知道末尾在哪里。让快指针先走 n+1 步(指向倒数第 n 个节点的前一个),然后两个指针同步走直到快指针到末尾,此时慢指针就停在待删节点的前一个位置。

关键洞察: 快指针先走 n+1 步而不是 n 步,是为了让慢指针指向待删节点的前驱,这样才能执行删除操作。

def removeNthFromEnd(head, n):
    """快慢指针 + 虚拟头节点"""
    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
    
    # 此时 slow 指向待删节点的前驱
    slow.next = slow.next.next
    return dummy.next

思路二:两次遍历法

思路讲解: 先遍历一次链表计算出总长度 L,则倒数第 n 个节点就是正数第 L-n+1 个。第二次遍历到第 L-n 个节点(待删节点的前驱)执行删除。思路简单但需要两次遍历。

def removeNthFromEnd(head, n):
    """两次遍历法"""
    # 第一次遍历:计算长度
    length = 0
    cur = head
    while cur:
        length += 1
        cur = cur.next
    
    # 虚拟头节点
    dummy = ListNode(0, head)
    cur = dummy
    # 第二次遍历:走到待删节点的前驱
    for _ in range(length - n):
        cur = cur.next
    
    cur.next = cur.next.next
    return dummy.next

思路三:栈辅助法

思路讲解: 遍历链表将所有节点入栈,然后弹出 n 个节点,此时栈顶就是待删节点的前驱。用虚拟头节点统一处理删除头节点的情况。

def removeNthFromEnd(head, n):
    """栈辅助法"""
    dummy = ListNode(0, head)
    cur = dummy
    stack = []
    while cur:
        stack.append(cur)
        cur = cur.next
    
    # 弹出 n 个节点
    for _ in range(n):
        stack.pop()
    
    # 栈顶就是待删节点的前驱
    prev = stack[-1]
    prev.next = prev.next.next
    return dummy.next

易错点

  • 快指针先走 n+1 步: 如果走了 n 步,慢指针就指向待删节点本身而不是前驱,无法执行删除
  • 虚拟头节点: 删除头节点时,如果没有 dummy,head = head.next 可以,但代码需要分支处理。用 dummy 可以统一逻辑
  • n 的合法性: 题目保证 n 有效,但通用代码应考虑 n 是否超过链表长度
  • dummy 初始连接: dummy = ListNode(0, head) 不能漏掉 head 参数,否则 dummy 与链表断开
  • 返回值: 始终返回 dummy.next,而不是 head(head 可能已被删除)

框架提炼

快慢指针找倒数第 N 个模板: 适用于查找或删除链表倒数第 N 个节点的场景。

def find_nth_from_end(head, n):
    """找到倒数第 n 个节点"""
    fast = slow = head
    for _ in range(n):
        fast = fast.next
    while fast:
        fast = fast.next
        slow = slow.next
    return slow

删除操作加强版(带虚拟头节点):

def remove_nth_from_end(head, n):
    dummy = ListNode(0, head)
    fast = slow = dummy
    for _ in range(n + 1):
        fast = fast.next
    while fast:
        fast = fast.next
        slow = slow.next
    slow.next = slow.next.next
    return dummy.next

快慢指针固定偏移量这一技巧可推广到更多需要「定位到距离末尾固定距离」的场景。


关联题目