「力扣」第 19 题:删除链表的倒数第 N 个节点(双指针)

本文详细解析了如何在链表中删除倒数第N个节点的问题,介绍了两种方法:通过两次遍历获取链表长度后再定位,以及使用快慢指针技巧一次性定位目标节点。文中还探讨了虚拟头节点的概念,以及其在处理链表问题中的作用。

地址:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/

分析:

  • 要删除一个结点,我们知道,需要来到待删除结点的上一个结点。
  • 要删除倒数第 N 个结点,因此我们就得站在倒数第 N + 1 个结点,然后修改它的 next 指针指向。

(假装这里有动画,我太懒了)

  • 为此,我需要遍历一遍链表,得到链表的额长度 len ,找到倒数第 N + 1 个结点,就有得从链表的起始结点开始遍历,那么这里要走多少步呢?我们不是靠猜。画出一个具体的图,就很清晰。

一共 6 个结点,N = 2 ,倒数第 2 个结点,就是正数第 5 个结点,从起始结点开始,需要走 3 步;

一共 6 个结点,N = 1 ,倒数第 1 个结点,就是正数第 6 个结点,从起始结点开始,需要走 4 步;

从起始结点开始,需要走 len - N - 1 步。

那么问题又来了。

一共 6 个结点,N = 6 ,倒数第 6 个结点,就是正数第 0 个结点,第 0 个结点,从起始结点开始,需要走 -1 步;,是没有意义的,我们要删掉的就是起始结点本身。当然我们可以对这种情况单独做判断,但事实上,处理链表起始结点的边界问题,有一个经典的技巧,那就是设置虚拟头结点。

虚拟头结点

  • 虚拟头结点的存在非必要,但是可以简化我们对链表问题的讨论,这个技巧其实我们在插入排序的时候用过,也叫「哨兵」,只不过在链表中的哨兵是我们认为引入的,它不参与链表的业务,只起到占位和回避边界条件讨论的作用;
  • 在链表的实现中,我们一般也都会初始化“虚拟头结点”。这是非常常见的技巧。仍然是在删除结点的任务中,设想一下,如果链表删到都没有结点了,我怎么还能引用到链表呢,又怎么在链表的末尾添加结点呢。

有了虚拟头结点以后,这道题的解决方案就可以变成:

1、先遍历输出链表的长度;

2、创建虚拟头结点,接在原来链表的头部(这一步如果大家看教科书的话,也叫做“头插法”);

3、然后从虚拟头结点开始遍历,走 len - N 步,这时候,就不要减 1 了,需要多走一步,同样,对走几步想不清楚的朋友,还是建议在纸上写写画画,遍不难得出正确答案。

  • 注意:这里我们从虚拟头结点开始走,就是为了避免那个最极端的情况。还是 6 个结点,删除倒数第 6 个,也就是第 1 个,我们走 0 步,也就是站在虚拟头结点处,修改结点的 next 指针指向即可。怎么样,是不是很神奇。

在这里再次强调:

1、虚拟头结点是几乎所有链表实现里的实现技巧,如果要求你实现一个链表,一般我们都会实现带有虚拟头结点的链表;

不管是单链表、双链表、循环链表、循环双链表,均是如此;

2、做「力扣」里的问题,因为「力扣」是以结点为视角看待链表的,因此虚拟头结点有的时候非必要,但我们常常需要,当你觉得需要分类讨论的时候,这个分类讨论的时机往往就在边界,不妨考虑一下,设置一个虚拟头结点可能就能避免分类讨论。

在上面的步骤中,有一点不太爽的地方是,我们需要遍历一次链表,数出链表的长度。然后,再从头开始遍历。如果这个链表比较长,待删除的结点正正好在末尾,此时就相当于得跑两遍链表。而影响时间性能的原因是,我必须得等第一次链表遍历完成以后,才能开始第 2 次遍历。

下面再介绍一个在链表问题中,也是比较常见的技巧。那就是“快慢指针”。事实上也是双指针,只不过它们是同向移动的,不过不是在数组中,也不是滑动窗口问题。

快慢指针

对于这道题,我们就让一个指针先走 N + 1步,注意是仍然是从虚拟头结点开始走,然后再让一个指针从虚拟头结点开始,和之前的那个指针同步移动,直到之前的指针走到了链表的末尾,慢指针就刚刚好来到了倒数第 N + 1 个结点。

(至于为什么是从虚拟头结点开始走 N + 1 步,这一个操作没有想明白的朋友依然是建议自己画一个图,举几个简单的例子帮助分析,找出规律,一点都不难。)

这样做的好处是,两个指针可以一起走,相当于我们实现了一个并行的操作,节约了一些时间。

下面我们来看一下代码。

  • 注意:快指针可以移动的条件。

Java 代码:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummyNode = new ListNode(-1);
        dummyNode.next = head;

        ListNode fastNode = dummyNode;
        for (int i = 0; i < n + 1; i++) {
            fastNode = fastNode.next;
        }

        ListNode slowNode = dummyNode;
        while (fastNode != null) {
            fastNode = fastNode.next;
            slowNode = slowNode.next;
        }

        // 此时 slowNode 来到了待删除的结点的上一个结点
        ListNode deleteNode = slowNode.next;
        slowNode.next = deleteNode.next;
        deleteNode.next = null;
        return dummyNode.next;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值