(LeetCode-Hot100)19. 删除链表的倒数第 N 个结点

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

🔗 LeetCode 中文链接

📌 问题简介

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

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

题目描述

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

提示

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

📌 示例说明

示例 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]

💡 解题思路

方法一:双指针(快慢指针)✅(推荐)

这是最经典、高效的方法,满足"一趟扫描"的要求。

步骤如下

  1. 创建一个虚拟头节点 dummy,指向原链表头。这样可以统一处理删除头节点的情况。
  2. 定义两个指针 fastslow,初始都指向 dummy
  3. 先让 fast 指针向前走 n + 1 步(注意是 n+1,因为我们要让 slow 停在待删除节点的前一个位置)。
  4. 然后 fastslow 同时向前移动,直到 fast 到达链表末尾(即 fast == null)。
  5. 此时 slow 指向的是倒数第 n+1 个节点(即待删除节点的前驱),执行 slow.next = slow.next.next 即可删除目标节点。
  6. 返回 dummy.next

为什么是 n+1 步?

因为我们希望 slow 最终停在要删除节点的前一个节点,这样才能修改其 next 指针。


方法二:先计算长度再删除 ❌(不满足进阶要求)

  1. 第一次遍历:计算链表总长度 L
  2. 第二次遍历:从头走到第 L - n 个节点(即待删除节点的前一个),然后删除。
  3. 时间复杂度 O(2L) ≈ O(L),但需要两趟扫描。

虽然可行,但不符合"一趟扫描"的进阶要求,不推荐。


方法三:栈(辅助空间)⚠️

  1. 将所有节点依次入栈。
  2. 弹出 n 个节点,此时栈顶即为待删除节点的前驱。
  3. 修改指针并返回。

空间复杂度 O(L),不如双指针优雅。


💻 代码实现

java 复制代码
// Java 实现:双指针法
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        
        ListNode fast = dummy;
        ListNode slow = dummy;
        
        // fast 先走 n+1 步
        for (int i = 0; i <= n; i++) {
            fast = fast.next;
        }
        
        // fast 和 slow 同时前进,直到 fast 为 null
        while (fast != null) {
            fast = fast.next;
            slow = slow.next;
        }
        
        // 删除倒数第 n 个节点
        slow.next = slow.next.next;
        
        return dummy.next;
    }
}
go 复制代码
// Go 实现:双指针法
func removeNthFromEnd(head *ListNode, n int) *ListNode {
    dummy := &ListNode{Val: 0, Next: head}
    fast, slow := dummy, dummy

    // fast 先走 n+1 步
    for i := 0; i <= n; i++ {
        fast = fast.Next
    }

    // fast 和 slow 同时前进
    for fast != nil {
        fast = fast.Next
        slow = slow.Next
    }

    // 删除节点
    slow.Next = slow.Next.Next

    return dummy.Next
}

🧪 示例演示(以示例1为例)

原始链表:1 → 2 → 3 → 4 → 5n = 2

  1. 构造虚拟头:dummy → 1 → 2 → 3 → 4 → 5
  2. fast 先走 3 步(n+1=3):fast 指向 4
  3. slowdummyfast4
  4. 同步移动:
    • fast=5, slow=1
    • fast=null, slow=3
  5. 此时 slow 指向 3slow.next4(即倒数第2个)
  6. 执行 slow.next = slow.next.next3 → 5
  7. 结果:1 → 2 → 3 → 5

✅ 答案有效性证明

  • 边界情况覆盖

    • 删除头节点(如 [1,2], n=2):dummy 机制确保正确处理。
    • 删除唯一节点(如 [1], n=1):slow 指向 dummydummy.next = null,返回 null
    • 删除尾节点(如 n=1):slow 停在倒数第二个,正确删除最后一个。
  • 指针逻辑正确性

    • fastn+1 步后,与 slow 的距离恒为 n+1
    • fast 到达 null(链表尾后一位),slow 必在倒数第 n+1 位。

因此,算法在所有合法输入下均正确。


📊 复杂度分析

方法 时间复杂度 空间复杂度 是否一趟扫描
双指针(推荐) O(L) O(1) ✅ 是
计算长度 O(L) O(1) ❌ 否
O(L) O(L) ✅ 是

L 为链表长度。


📌 问题总结

  • 核心技巧 :使用虚拟头节点避免对头节点的特殊处理。
  • 关键洞察 :通过控制两个指针的距离(n+1),实现"定位倒数第 n 个节点的前驱"。
  • 最佳实践:双指针法时间空间最优,且满足进阶要求。
  • 易错点
    • 忘记 n+1 步,导致 slow 停在待删除节点而非其前驱。
    • 未使用虚拟头,导致删除头节点时逻辑复杂。

💡 一句话口诀快指针先走 n+1,慢指针随后跟;快到终点时,慢删下一结。

github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions

相关推荐
树码小子1 小时前
Mybatis(14)Mybatis-Plus入门 & 简单使用
java·mybatis-plus
人道领域1 小时前
Maven配置加载:动态替换的艺术
java·数据库·后端
MX_93591 小时前
@Import整合第三方框架原理
java·开发语言·后端·spring
就不掉头发1 小时前
动态规划算法 --积小流以成江海
算法·动态规划
坚持就完事了2 小时前
Java实现数据结构中的链表
java·数据结构·链表
写代码的小球2 小时前
C++ 标准库 <numbers>
开发语言·c++·算法
拳里剑气2 小时前
C++:哈希
开发语言·数据结构·c++·算法·哈希算法·学习方法
玩具猴_wjh2 小时前
JWT优化方案
java·服务器·数据库
闻缺陷则喜何志丹2 小时前
【高等数学】导数与微分
c++·线性代数·算法·矩阵·概率论