LeetCode 19 - 删除链表的倒数第N个节点

题目信息

  • 题目编号: 19
  • 题目名称: 删除链表的倒数第N个节点
  • 标签: 链表、双指针
  • 难度: 中等
  • 题目链接 : LeetCode 链接

题目描述

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

示例

复制代码
示例 1:
输入: head = [1,2,3,4,5], n = 2
输出: [1,2,3,5]
解释: 链表为 1->2->3->4->5,删除倒数第2个节点(值为4),结果为 1->2->3->5

示例 2:
输入: head = [1], n = 1
输出: []
解释: 链表只有一个节点,删除后为空链表

示例 3:
输入: head = [1,2], n = 1
输出: [1]
解释: 链表为 1->2,删除倒数第1个节点(值为2),结果为 1

解题思路

初步思考

第一眼看到这个题目,可能会有同学想到先遍历一遍链表获取总长度,然后再遍历一次删除倒数第n个节点。这种方法虽然可行,但需要遍历两遍链表。有没有更优雅的解法呢?

答案是肯定的!我们可以使用双指针技巧,只需一次遍历就能完成任务。这种技巧在链表问题中非常常见,值得深入理解。

方法分析

方法一:双指针法(快慢指针)

思路 :

双指针法的核心思想是使用两个指针,让它们之间保持固定的距离。具体来说,我们让快指针先走n步,然后快慢指针一起移动,当快指针到达链表末尾时,慢指针就恰好指向待删除节点的前一个节点。

这种方法之所以高效,是因为:

  • 只需遍历一次链表
  • 不需要预先知道链表长度
  • 代码简洁优雅

根据一个简单示例,通过图示展示思路:

以链表 [1,2,3,4,5], n=2 为例:

复制代码
初始状态:快慢指针都指向头节点
head -> 1 -> 2 -> 3 -> 4 -> 5 -> null
        ↑    
        slow, fast

第一步:快指针先走n步(n=2)
head -> 1 -> 2 -> 3 -> 4 -> 5 -> null
        ↑         ↑
       slow      fast

第二步:快慢指针一起移动
head -> 1 -> 2 -> 3 -> 4 -> 5 -> null
              ↑             ↑
            slow          fast

第三步:继续移动,直到fast到达末尾
head -> 1 -> 2 -> 3 -> 4 -> 5 -> null
                  ↑         ↑
                slow       fast

此时slow指向待删除节点(4)的前一个节点(3)

算法步骤:

  1. 创建哑节点(dummy node),使其指向链表头节点,用于处理边界情况
  2. 初始化快慢指针都指向哑节点
  3. 快指针先向前移动n步
  4. 快慢指针同时向前移动,直到快指针到达链表末尾
  5. 此时慢指针指向待删除节点的前一个节点,执行删除操作
  6. 返回哑节点的下一个节点

采用上面图示的示例,通过文字一步步的讲解该方法的实现过程:

[1,2,3,4,5], n=2 为例:

  1. 创建哑节点 dummy,指向头节点1

    • dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> null
    • slow = fast = dummy
  2. 快指针先走2步

    • fast 移动到节点3的位置
    • slow = dummy, fast = 3
  3. 快慢指针同时移动,fast 先到达节点5

    • slow 移动到节点3的位置
    • 此时 fast.next = null,循环结束
  4. slow 指向节点3,执行删除

    • slow.next = slow.next.next3.next = 4.next
    • 链表变为 dummy -> 1 -> 2 -> 3 -> 5 -> null
  5. 返回 dummy.next 即 1

复杂度分析:

  • 时间复杂度: O(L),其中L是链表长度,只需遍历一次
  • 空间复杂度: O(1),只使用了常数个额外指针

代码实现

Python 实现:

python 复制代码
# Definition for singly-linked list.
# 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]:
        """
        删除链表中倒数第n个节点
        
        Args:
            head: 链表头节点
            n: 倒数第n个节点
            
        Returns:
            删除节点后的链表头节点
        """
        # 创建哑节点,简化边界情况处理
        dummy = ListNode(0, head)
        slow = fast = dummy
        
        # 快指针先走n步
        for _ in range(n):
            fast = fast.next
        
        # 快慢指针一起移动,直到快指针到达末尾
        while fast.next:
            slow = slow.next
            fast = fast.next
        
        # 删除倒数第n个节点
        slow.next = slow.next.next
        
        return dummy.next

Java 实现:

java 复制代码
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(0, head);
        ListNode slow = dummy;
        ListNode fast = dummy;
        
        for (int i = 0; i < n; i++) {
            fast = fast.next;
        }
        
        while (fast.next != null) {
            slow = slow.next;
            fast = fast.next;
        }
        
        slow.next = slow.next.next;
        
        return dummy.next;
    }
}

总结与收获

知识点

  1. 哑节点(Dummy Node): 在链表头部添加一个哑节点,可以统一处理头节点被删除的边界情况,避免额外的条件判断
  2. 双指针技巧: 使用快慢两个指针,通过控制它们之间的距离差来定位目标位置,是链表问题中的常用技巧
  3. 一次遍历: 通过巧妙的指针移动,实现一次遍历完成所有操作

易错点

  1. 边界情况处理: 当要删除的是头节点时,如果没有哑节点,需要额外判断。本题使用哑节点统一处理
  2. 指针移动范围: 快指针只需要移动到倒数第n个节点,不需要移动到null,否则慢指针会少走一步
  3. 内存泄漏: 在Java中不需要手动释放,但在C++中需要注意删除的节点需要手动释放

优化思路

  • 本方法已经是最优解,时间复杂度O(n),空间复杂度O(1)
  • 可以考虑使用递归方式,但递归需要O(n)的栈空间,不如迭代方法高效

相似题目

相关推荐
sunfove2 小时前
麦克斯韦方程组 (Maxwell‘s Equations) 的完整推导
线性代数·算法·矩阵
一路向北·重庆分伦2 小时前
03-01:MQ常见问题梳理
java·开发语言
一 乐2 小时前
绿色农产品销售|基于springboot + vue绿色农产品销售系统(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·后端·宠物
txinyu的博客2 小时前
结合游戏场景理解,互斥锁,读写锁,自旋锁,CAS / 原子变量,分段锁
开发语言·c++·游戏
lhrimperial2 小时前
企业智能知识库助手落地实践:从RAG到Multi-Agent
java·spring cloud·微服务·系统架构·知识图谱
Rui_Freely2 小时前
Vins-Fusion之 SFM准备篇(十二)
人工智能·算法·计算机视觉
3***68842 小时前
Spring Boot中使用Server-Sent Events (SSE) 实现实时数据推送教程
java·spring boot·后端
阿里嘎多学长2 小时前
2026-01-11 GitHub 热点项目精选
开发语言·程序员·github·代码托管
yuanyikangkang2 小时前
STM32 lin控制盒
开发语言