(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. 定义两个指针 fast 和 slow,初始都指向 dummy。
  3. 先让 fast 指针向前走 n + 1 步(注意是 n+1,因为我们要让 slow 停在待删除节点的前一个位置)。
  4. 然后 fast 和 slow 同时向前移动,直到 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 → 5,n = 2

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

✅ 答案有效性证明

  • 边界情况覆盖:

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

    • fast 走 n+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

相关推荐
Escalating_xu1 小时前
【C 语言】数据在内存中的存储:补码、大小端、整型陷阱与 IEEE 754 全解析
java·c语言·网络
灯澜忆梦2 小时前
【面向对象编程C++】| 基础语法
java·c++·算法
小蒋观天下2 小时前
专项方案:大场景港口AI安防、多干扰环境下的算法调优与落地实操
人工智能·深度学习·算法·安全·机器学习·计算机视觉·ai大模型
一个风轻云淡2 小时前
GCC 和 GDB命令简单解读
java·linux·前端
省钱兄--zs2 小时前
西安24小时自助健身房解决方案实战:系统开发与部署全流程指南
java·spring boot·系统架构·intellij-idea·需求分析
Evand J2 小时前
【电机滤波例程5】渐消因子自适应扩展卡尔曼滤波(EKF),MATLAB。PMSM负载突变下的状态估计。附下载链接
算法·matlab
H.莓飛2 小时前
【数据结构】二叉树_OJ题
linux·开发语言·数据结构·算法
资料库012 小时前
Linux 8个常见运维故障排查
java·linux·运维
要开心吖ZSH2 小时前
MySQL 慢 SQL 排查操作手册-个人笔记版
java·笔记·sql·mysql·慢查询