大数跨境
0
0

LeetCode 19删除链表的倒数第N个结点详解:双指针与栈解法全解析

LeetCode 19删除链表的倒数第N个结点详解:双指针与栈解法全解析 点点技术屋
2025-11-27
0

题目解析:为什么这道题是链表操作的经典必刷题

LeetCode 19题要求我们删除链表的倒数第 N 个结点,并且只能遍历一次链表。这个"一次遍历"的限制直接否定了先遍历求长度再定位删除的暴力做法,逼着我们思考更高效的解法。题目看似简单,但隐藏着三个容易踩坑的关键点:如何准确定位倒数第N个节点、如何处理删除头节点的特殊情况、如何保证操作的时间复杂度为 O(n)。

举个例子,给定链表 1→2→3→4→5,并且 N=2,最终要返回的链表应该是 1→2→3→5。这个过程中我们需要找到倒数第2个节点(也就是4)并删除它,但关键在于我们并不知道链表的长度,这就像在黑暗中数台阶,必须用巧劲而不是蛮力。

链表基础:理解这道题的前置知识

在开始解题前,我们需要先明确链表的基本结构。链表是由节点组成的线性数据结构,每个节点包含两部分:存储数据的数据域和指向下一个节点的指针域。与数组不同,链表的元素在内存中不是连续存储的,这意味着我们无法像数组那样通过下标直接访问元素,必须从头节点开始逐个遍历。

在 Python 中,链表节点通常定义为:

class ListNode:    def __init__(self, val=0next=None):        self.val = val        self.next = next

这种结构决定了链表操作的特殊性:要删除某个节点,必须找到它的前一个节点,然后修改前一个节点的 next 指针。这也是本题的核心难点所在——如何在不知道链表长度的情况下,高效找到倒数第 N 个节点的前一个节点。

双指针法:一次遍历解决问题的最优解

双指针法是解决这道题的最优方案,它的核心思想是通过两个指针之间的距离差来定位倒数第 N 个节点。具体步骤如下:

  1. 创建两个指针 fast 和 slow,初始时都指向虚拟头节点

  2. 让 fast 指针先向前移动 N+1 步(为什么是 N+1?因为我们要找到倒数第 N 个节点的前一个节点)

  3. 然后让 fast 和 slow 指针同时向前移动,直到 fast 指针到达链表末尾(指向 null)

  4. 此时 slow 指针指向的就是倒数第 N 个节点的前一个节点,修改 slow.next 即可删除目标节点

为什么这种方法能奏效?因为当 fast 指针比 slow 指针超前 N+1 步时,当 fast 到达终点,slow 与终点的距离就是 N+1 步,也就是说 slow 指向的是倒数第 N+1 个节点,正好是我们要删除的倒数第 N 个节点的前一个节点。这种方法只需要遍历一次链表,时间复杂度为 O(n),空间复杂度为 O(1)。

栈解法:利用后进先出特性的巧妙思路

除了双指针法,栈也是解决这道题的常用方法。栈的后进先出(LIFO)特性可以帮助我们反向访问链表节点:

  1. 遍历整个链表,将所有节点依次入栈

  2. 然后连续弹出 N 个节点,此时栈顶元素就是倒数第 N+1 个节点

  3. 修改栈顶节点的 next 指针,指向倒数第 N-1 个节点,完成删除操作

栈解法的时间复杂度同样是 O(n),但空间复杂度为 O(n),因为需要额外存储所有节点。虽然空间复杂度不如双指针法优秀,但栈解法的思路更加直观,容易理解和实现。

多语言实现:Python/Java/JavaScript代码对比

Python实现

class Solution:    def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:        # 创建虚拟头节点        dummy = ListNode(0, head)        fast = slow = dummy
        # fast指针先移动n+1步        for _ in range(n + 1):            fast = fast.next
        # 同时移动fast和slow        while fast:            fast = fast.next            slow = slow.next
        # 删除节点        slow.next = slow.next.next
        return dummy.next

Java实现

class Solution {    public ListNode removeNthFromEnd(ListNode head, int n) {        // 创建虚拟头节点        ListNode dummy = new ListNode(0);        dummy.next = head;        ListNode fast = dummy, slow = dummy;
        // fast指针先移动n+1步        for (int i = 0; i <= n; i++) {            fast = fast.next;        }
        // 同时移动fast和slow        while (fast != null) {            fast = fast.next;            slow = slow.next;        }
        // 删除节点        slow.next = slow.next.next;
        return dummy.next;    }}

JavaScript实现

var removeNthFromEnd = function(head, n) {    // 创建虚拟头节点    const dummy = new ListNode(0, head);    let fast = dummy, slow = dummy;
    // fast指针先移动n+1步    for (let i = 0; i <= n; i++) {        fast = fast.next;    }
    // 同时移动fast和slow    while (fast) {        fast = fast.next;        slow = slow.next;    }
    // 删除节点    slow.next = slow.next.next;
    return dummy.next;};

三种语言的实现思路完全一致,都是采用双指针法结合虚拟头节点的方式。代码结构清晰,核心逻辑都在15行左右,充分体现了双指针法的简洁高效。

虚拟头节点:解决头节点删除难题的关键技巧

虚拟头节点(Dummy Node)是链表操作中的一个重要技巧,它可以统一处理各种边界情况,避免对头节点进行特殊判断。

没有虚拟头节点时,删除头节点和删除其他节点需要不同的处理逻辑:

  • 删除其他节点:找到前一个节点,修改其 next 指针

  • 删除头节点:直接返回 head.next

而有了虚拟头节点后,无论删除哪个节点,都可以采用相同的处理逻辑:找到前一个节点,修改其 next 指针。虚拟头节点就像是一个哨兵,让所有节点都拥有了前一个节点,从而简化了代码逻辑。

边界情况处理:这些坑你必须知道

在解决这道题时,有几个边界情况需要特别注意:

  1. 删除头节点:当 N 等于链表长度时,要删除的就是头节点。没有虚拟头节点时,需要特殊处理这种情况。

  2. 链表长度为1:当链表只有一个节点且 N=1 时,删除后应该返回 null。

  3. N 等于 0:虽然题目中说明 N 是正整数,但实际编程中还是应该考虑这种异常输入。

  4. N 大于链表长度:同样需要考虑这种异常情况的处理。

双指针法结合虚拟头节点的方案已经考虑了这些边界情况,能够优雅地处理各种极端场景。

拓展思路:从删除倒数节点到删除中间节点

解决了删除倒数第 N 个节点的问题后,我们可以思考如何将这种思路拓展到其他链表操作问题:

  1. 删除链表中间节点:如果要求删除链表的中间节点(只允许遍历一次),可以使用快慢指针法:快指针每次走两步,慢指针每次走一步,当快指针到达终点时,慢指针正好指向中间节点。

  2. 删除链表的第 N 个节点:这是删除倒数第 N 个节点的正向版本,可以直接遍历找到第 N-1 个节点进行删除。

  3. 删除链表中等于给定值的所有节点:需要遍历整个链表,逐个判断并删除符合条件的节点。

这些问题虽然具体要求不同,但核心思路都是通过指针操作来定位和删除目标节点,体现了链表操作的共性。

总结:这道题教会我们什么

LeetCode 19题虽然是一道中等难度的题目,但它涵盖了链表操作的多个核心知识点:

  1. 双指针技巧:这是解决很多链表、数组问题的高效方法,通过两个指针之间的关系来定位目标元素。

  2. 虚拟头节点:简化边界情况处理的常用技巧,在很多链表操作题中都能用到。

  3. 边界情况处理:编程不仅要处理正常情况,还要考虑各种极端场景。

  4. 空间与时间复杂度权衡:双指针法(O(1)空间)和栈解法(O(n)空间)的对比,体现了算法设计中的取舍思想。

掌握这道题的解法,不仅能应对类似的链表操作问题,更能培养我们解决复杂问题的思维能力。很多时候,优秀的算法不是凭空出现的,而是通过不断优化和改进得到的。从暴力解法到双指针法,体现的正是这种优化思维。

最后记住,解决算法问题的关键不是记住答案,而是理解其中的思想和方法,并能将其应用到新的问题中。这道题中用到的双指针技巧和虚拟头节点思想,在很多其他链表问题中都能发挥重要作用。

【声明】内容源于网络
0
0
点点技术屋
写自己擅长的Nginx,Linux,Redis,JVM和Golang 写自己学习的算法,运维总结 写一些散文随笔 写我最爱的大熊猫,发大熊猫的图片
内容 120
粉丝 0
点点技术屋 写自己擅长的Nginx,Linux,Redis,JVM和Golang 写自己学习的算法,运维总结 写一些散文随笔 写我最爱的大熊猫,发大熊猫的图片
总阅读10
粉丝0
内容120