关注Java领域相关技术 记录有趣的事情

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

US-B.Ralph
US-B.Ralph
2020-10-18

问题地址

LeetCode每日一题/2020-10-18

LeetCode19. 删除链表的倒数第N个节点


问题描述

规则

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

示例

给定一个链表: 1->2->3->4->5, 和 n = 2.

当删除了倒数第二个节点后,链表变为 1->2->3->5.

说明

  • 给定的 n 保证是有效的。

进阶

  • 你能尝试使用一趟扫描实现吗?

解析

解题思路

  • 定义两个指针p1和p2,同时指向head,p2先移动n个指针;
    • 如果p2不为null,则同时移动p1、p2;
    • 如果p2为空,则返回p1.next

数据操作分析

  • 见思路分析

复杂度分析

  1. 时间复杂度
  2. 空间复杂度

编码实现

public class LeetCode0019_RemoveNthNodeFromEndOfList {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode p1 = head;
        ListNode p2 = head;
        while (n > 0) {
            p2 = p2.next;
            n--;
        }

        if (p2 == null) return p1.next;
        while (p2.next != null) {
            p1 = p1.next;
            p2 = p2.next;
        }

        p1.next = p1.next.next;
        return head;
    }
}

官方解法

前言

在对链表进行操作时,一种常用的技巧是添加一个哑节点(dummy node),它的 \textit{next} 指针指向链表的头节点。这样一来,我们就不需要对头节点进行特殊的判断了。

例如,在本题中,如果我们要删除节点 y,我们需要知道节点 y 的前驱节点 x,并将 x 的指针指向 y 的后继节点。但由于头节点不存在前驱节点,因此我们需要在删除头节点时进行特殊判断。但如果我们添加了哑节点,那么头节点的前驱节点就是哑节点本身,此时我们就只需要考虑通用的情况即可。

特别地,在某些语言中,由于需要自行对内存进行管理。因此在实际的面试中,对于「是否需要释放被删除节点对应的空间」这一问题,我们需要和面试官进行积极的沟通以达成一致。下面的代码中默认不释放空间。

方法一 计算链表长度

思路与算法:

  • 一种容易想到的方法是,我们首先从头节点开始对链表进行一次遍历,得到链表的长度 L。随后我们再从头节点开始对链表进行一次遍历,当遍历到第 L-n+1 个节点时,它就是我们需要删除的节点。

    为了与题目中的 n 保持一致,节点的编号从 1 开始,头节点为编号 1 的节点。

  • 为了方便删除操作,我们可以从哑节点开始遍历 L-n+1 个节点。当遍历到第 L−n+1 个节点时,它的下一个节点就是我们需要删除的节点,这样我们只需要修改一次指针,就能完成删除操作。

复杂度分析:

  1. 时间复杂度:O(L),其中 L 是链表的长度。
  2. 空间复杂度:O(1)

编码实现

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(0, head);
        int length = getLength(head);
        ListNode cur = dummy;
        for (int i = 1; i < length - n + 1; ++i) {
            cur = cur.next;
        }
        cur.next = cur.next.next;
        ListNode ans = dummy.next;
        return ans;
    }

    public int getLength(ListNode head) {
        int length = 0;
        while (head != null) {
            ++length;
            head = head.next;
        }
        return length;
    }
}

方法二:栈

思路及算法:

  • 我们也可以在遍历链表的同时将所有节点依次入栈。根据栈「先进后出」的原则,我们弹出栈的第 nn 个节点就是需要删除的节点,并且目前栈顶的节点就是待删除节点的前驱节点。这样一来,删除操作就变得十分方便了。

复杂度分析:

  1. 时间复杂度:O(L),其中 L 是链表的长度。
  2. 空间复杂度:O(L),其中 L 是链表的长度。主要为栈的开销。

编码实现

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(0, head);
        Deque<ListNode> stack = new LinkedList<ListNode>();
        ListNode cur = dummy;
        while (cur != null) {
            stack.push(cur);
            cur = cur.next;
        }
        for (int i = 0; i < n; ++i) {
            stack.pop();
        }
        ListNode prev = stack.peek();
        prev.next = prev.next.next;
        ListNode ans = dummy.next;
        return ans;
    }
}

方法三:一次遍历

思路及算法:

我们也可以在不预处理出链表的长度,以及使用常数空间的前提下解决本题。

由于我们需要找到倒数第 n 个节点,因此我们可以使用两个指针 \textit{first}\textit{second} 同时对链表进行遍历,并且 \textit{first}\textit{second} 超前 n 个节点。当 \textit{first} 遍历到链表的末尾时,\textit{second} 就恰好处于倒数第 n 个节点。

具体地,初始时 \textit{first}\textit{second} 均指向头节点。我们首先使用 \textit{first} 对链表进行遍历,遍历的次数为 n。此时,\textit{first}\textit{second} 之间间隔了 n-1 个节点,即 \textit{first}\textit{second} 超前了 n 个节点。

在这之后,我们同时使用 \textit{first}\textit{second} 对链表进行遍历。当 \textit{first} 遍历到链表的末尾(即 \textit{first} 为空指针)时,\textit{second} 恰好指向倒数第 n 个节点。

根据方法一和方法二,如果我们能够得到的是倒数第 n 个节点的前驱节点而不是倒数第 n 个节点的话,删除操作会更加方便。因此我们可以考虑在初始时将 \textit{second} 指向哑节点,其余的操作步骤不变。这样一来,当 \textit{first} 遍历到链表的末尾时,\textit{second} 的下一个节点就是我们需要删除的节点。

复杂度分析:

  1. 时间复杂度:O(L),其中 L 是链表的长度。
  2. 空间复杂度:O(1)

编码实现

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

精彩评论

跳转地址1:动画图解 LeetCode 第 19 号问题:删除链表的倒数第 N 个节点

题目解析

  • 采取双重遍历肯定是可以解决问题的,但题目要求我们一次遍历解决问题,那我们的思路得发散一下。

  • 我们可以设想假设设定了双指针 p 和 q 的话,当 q 指向末尾的 NULL,p 与 q 之间相隔的元素个数为 n 时,那么删除掉 p 的下一个指针就完成了要求。

    • 设置虚拟节点 dummyHead 指向 head
    • 设定双指针 p 和 q,初始都指向虚拟节点 dummyHead
    • 移动 q,直到 p 与 q 之间相隔的元素个数为 n
    • 同时移动 p 与 q,直到 q 指向的为 NULL
    • 将 p 的下一个节点指向下下个节点

动画

编码实现

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
     ListNode* dummyHead = new ListNode(0);
        dummyHead->next = head;

        ListNode* p = dummyHead;
        ListNode* q = dummyHead;
        for( int i = 0 ; i < n + 1 ; i ++ ){
            q = q->next;
        }

        while(q){
            p = p->next;
            q = q->next;
        }

        ListNode* delNode = p->next;
        p->next = delNode->next;
        delete delNode;

        ListNode* retNode = dummyHead->next;
        delete dummyHead;

        return retNode;

    }
};

跳转地址2:画解算法:19. 删除链表的倒数第N个节点

解题思路

  • 标签:链表
  • 整体思路是让前面的指针先移动n步,之后前后指针共同移动直到前面的指针到尾部为止
  • 首先设立预先指针 pre,预先指针是一个小技巧,在第2题中进行了讲解
  • 设预先指针 pre 的下一个节点指向 head,设前指针为 start,后指针为 end,二者都等于 pre
  • start 先向前移动n步
  • 之后 start 和 end 共同向前移动,此时二者的距离为 n,当 start 到尾部时,end 的位置恰好为倒数第 n 个节点
  • 因为要删除该节点,所以要移动到该节点的前一个才能删除,所以循环结束条件为 start.next != null
  • 删除后返回 pre.next,为什么不直接返回 head 呢,因为 head 有可能是被删掉的点
  • 时间复杂度:O(n)

画解

编码实现

/**
 * 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 pre = new ListNode(0);
        pre.next = head;
        ListNode start = pre, end = pre;
        while(n != 0) {
            start = start.next;
            n--;
        }
        while(start.next != null) {
            start = start.next;
            end = end.next;
        }
        end.next = end.next.next;
        return pre.next;
    }
}
US-B.Ralph
LeetCode数据结构与算法算法

Leave a Comment

邮箱地址不会被公开。 必填项已用*标注

6 − 2 =