算法修炼之路——【链表】Leetcode19 删除链表倒数第n个节点

问题描述

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

示例:

输入: head = [1, 2, 3, 4, 5], n = 2
输出: head = [1, 2, 3, 5]

说明:
给定的n保证是有效的

问题分析

这道题目的难度,力扣官网给出的难度为“中等”。通常中等难度的问题需要将问题拆解为两部分,或者通常为简单程度题目的进一步综合。

我们通过题干可以了解到,这一题是删除倒数第n个节点,这里的重点为删除,那么我们需要确定删除的对象或具体删除的位置,这样可以将题干具体为以下问题:

  1. 如何定位删除的节点?(这里是倒数第n个节点)
  2. 如何删除此节点?

关于问题1,我们知道,对于一个链表,我们只知道其首节点,其他任何信息如长度均不可知,此时我们需要有单独的实现定位节点功能的代码。当我们不知道长度等具体情况的时候,我们需要有一个“探路兵”,告诉我们这条路具体多长l,然后我们需要往前走l-n的距离,此时停止,位置恰好就为倒数的n处。

边界条件考虑

题目中并没有完全说明n的取值范围,只指明了有效,则我们必须把边界条件考虑在内:

  1. 当需要删除的节点为首节点时
    我们易知此时采用哨兵机制(具体可参照此篇)
  2. 当需要删除的节点为尾节点时
    当需要删除的节点为尾节点时,我们的规则保持统一,故不需要进一步的调整。

步骤罗列

  1. 初始化“探路兵”,即一个指针scoutP和哨兵节点dummyHead;
  2. scoutP遍历一遍链表,给出确定的链表长度l;
  3. 再对链表进行遍历,此时给定遍历的长度为l-n
  4. 给定的长度用完,则为倒数第n个节点,删除此节点(删除操作可参照文章12)。

解题代码1

    public static ListNode solutionWithP(ListNode head, int n) {
        if (head == null) {
            return head;
        }

        //1. init pointers and dummyHead
        ListNode dummyHead = new ListNode(0);
        dummyHead.next = head;
        ListNode scoutP = dummyHead;
        int len = 0;
        
        // 2. go through linkedlist to ensure it's length
        while (scoutP.next != null) {
            len++;
            scoutP = scoutP.next;
        }
        
        int step = len - n;
        scoutP = dummyHead; //re-use of scoutP
        // 3. go through linkedlist for len-n step
        while(step-- > 0)
            scoutP = scoutP.next;
        
        // 4. deleted specified node
        scoutP.next = scoutP.next.next;
        
        return dummyHead.next;
    }

复杂度分析

  1. 时间复杂度:
    这里我们对链表进行了不止一次的遍历,第一次为确定链表长度O(N);
    第二次为定位需要删除的节点位置O(N - n));最坏情况下为删除链表最后一个节点,此时n为1,遍历了2N-n个节点;最好情况下为删除链表第一个节点,此时n为N,遍历了N+1个节点。
    时间复杂度均为O(N)。

  2. 空间复杂度:
    我们没有初始化或设置额外的辅助容器,所以这里空间复杂度为O(1);

进阶

这里可不可以对数据遍历的次数减少??
在解题代码1中我们分析,其遍历次数不止一次,且最坏情况下需要遍历两次,当我们一个指针需要遍历两次的时候,这时候可以思考,能不能增加指针数量来使得遍历次数减少呢?

进阶解题思路

我们需要找出倒数第n节点的位置,这里可以通过控制两个指针间距来定义这个n长度,这里我们设fastPslowP, 当快指针到达链表尾部时,此时slowP所在的位置就是应该删去节点的前一个节点。
这时候,我们再进一步梳理思路则有:

步骤罗列

  1. 定义两个指针和哨兵节点;
  2. 使得快节点与慢节点间隔为n;
  3. 两个指针同时开始遍历,快指针到达链表尾部时,停止;
  4. 慢指针更新其后续节点,返回哨兵节点的下一个节点。

解题代码2

    public static ListNode solutionWithTwoP(ListNode head, int n) {
        if (head == null) {
            return head;
        }

        //1. init pointers and dummyHead
        ListNode dummyHead = new ListNode(-1);
        dummyHead.next = head;
        ListNode slowP = dummyHead;
        ListNode fastP = dummyHead;

        //2. init the fixed gap of n
        for(int i = 1; i < n+1; i++)
            fastP = fastP.next;

        //3. slowP start to move
        while (fastP.next != null) {
            slowP = slowP.next;
            fastP = fastP.next;
        }

        //3. delete specified node
        slowP.next = slowP.next.next;

        return dummyHead.next;
    }

复杂度分析

  1. 时间复杂度:
    这里只对倒数第n个节点之前的数据进行了遍历,故其时间复杂度为O(L),这里 L=N-n

  2. 空间复杂度
    没有额外的容器要求,即只要求常量级的额外空间,故空间复杂度为O(1)。

GitHub代码

具体完整可运行文件参见GitHub

发布了47 篇原创文章 · 获赞 55 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/u011106767/article/details/105262722