19. 删除链表的倒数第 N 个结点

294 阅读2分钟

题目介绍

力扣19题:leetcode-cn.com/problems/re…

image.png

分析

链表中删除某个节点,其实就是将之前一个节点next,直接指向当前节点的后一个节点,相当于“跳过”了这个节点。

当然,真正意义上的删除,还应该回收节点本身占用的空间,进行内存管理。这一点在java中我们可以不考虑,直接由JVM的GC帮我们实现。

方法一:计算链表长度(二次遍历)

最简单的想法是,我们首先从头节点开始对链表进行一次遍历,得到链表的长度 L。

然后,我们再从头节点开始对链表进行一次遍历,当遍历到第 L-N+1 个节点时,它就是我们需要删除的倒数第N个节点。 这样,总共做两次遍历,我们就可以得到结果。

代码演示如下:

// 方法一:计算链表长度
public ListNode removeNthFromEnd1(ListNode head, int n){
    // 1. 遍历链表,得到长度l
    int l = getLength(head);

    // 2. 从前到后继续遍历,找到正数第l-n+1个元素
    // 定义一个哨兵节点,next指向头节点
    ListNode sentinel = new ListNode(-1, head);

    ListNode curr = sentinel;
    for (int i = 0; i < l - n; i++){
        curr = curr.next;
    }
    // 找到第l-n个节点

    // 跳过第l-n+1个节点
    curr.next = curr.next.next;

    return sentinel.next;

}

// 定义一个获取链表长度的方法
public static int getLength(ListNode head){
     int length = 0;
     while ( head != null ){
         length ++;
         head = head.next;
     }
     return length;
 }

复杂度分析

  • 时间复杂度:O(L),其中 L 是链表的长度。只用了两次遍历,是线性时间复杂度。
  • 空间复杂度:O(1)。

方法二:利用栈

另一个思路是利用栈数据结构。因为栈是“先进后出”的,所以我们可以在遍历链表的同时将所有节点依次入栈,然后再依次弹出。

这样,弹出栈的第 n 个节点就是需要删除的节点,并且目前栈顶的节点就是待删除节点的前驱节点。这样一来,删除操作就变得十分方便了。 代码演示如下:

// 方法二:使用栈
public ListNode removeNthFromEnd2(ListNode head, int n){
    // 定义一个哨兵节点,next指向头节点
    ListNode sentinel = new ListNode(-1, head);

    ListNode curr = sentinel;

    // 定义一个栈
    Stack<ListNode> stack = new Stack<>();

    // 1. 将所有节点入栈
    while (curr != null){
        stack.push(curr);
        curr = curr.next;
    }

    // 2. 弹栈n次
    for (int i = 0; i < n; i++)
        stack.pop();

    stack.peek().next = stack.peek().next.next;

    return sentinel.next;
}

复杂度分析

  • 时间复杂度:O(L),其中 L是链表的长度。我们压栈遍历了一次链表,弹栈遍历了N个节点,所以应该耗费O(L+N)时间。N <= L,所以时间复杂度依然是O(L),而且我们可以看出,遍历次数比两次要少,但依然没有达到“一次遍历”的要求。
  • 空间复杂度:O(L),其中 L 是链表的长度。主要为栈的开销。

方法三:双指针(一次遍历)

我们可以使用两个指针 first 和 second 同时对链表进行遍历,要求 first 比 second 超前 N 个节点。

这样,它们总是保持着N的距离,当 first 遍历到链表的末尾(null)时,second 就恰好处于第L-N+1,也就是倒数第 N 个节点了。 代码演示如下:

// 方法三:双指针
public ListNode removeNthFromEnd(ListNode head, int n){
    // 定义一个哨兵节点,next指向头节点
    ListNode sentinel = new ListNode(-1, head);

    // 定义前后双指针
    ListNode first = sentinel, second = sentinel;

    // 1. first先走n+1步
    for (int i = 0; i < n + 1; i++)
        first = first.next;

    // 2. first、second同时前进,当first变为null,second就是倒数第n+1个节点
    while (first != null){
        first = first.next;
        second = second.next;
    }

    // 3. 删除倒数第n个节点
    second.next = second.next.next;

    return sentinel.next;
}

复杂度分析

  • 时间复杂度:O(L),其中 L是链表的长度。这次真正实现了一次遍历。
  • 空间复杂度:O(1)。