19 删除链表的倒数第 N 个结点(2021-07-15)

本文介绍了四种方法解决LeetCode第19题——删除链表的倒数第N个结点。包括两次遍历计算链表长度、两次遍历加哑结点、使用栈以及快慢指针的方法。对于每种解法,详细说明了思路、时间复杂度和空间复杂度,并给出了JavaScript实现的执行效率。
摘要由CSDN通过智能技术生成

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

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

题目描述见链接内容。

解法1:两次遍历计算链表长度

可以通过两次遍历来实现,第一次遍历计算出链表长度,这样就知道倒数第n个对应的正数是第几个了,然后在第二次遍历时,将对应的节点的『前驱节点』的next指向next.next

问题是,当删除的是正数第一个节点时,由于第一个节点不存在『前驱节点』所以需要特殊处理

var removeNthFromEnd = function (head, n) {
  let cur1 = head,
    len = 0;
  while (cur1) {
    len += 1;
    cur1 = cur1.next;
  }

  const targetIndex = len - n;

  // 处理去除首节点的情况
  if (targetIndex === 0) {
    head = head.next;
  }

  let cur2 = head,
    index = 0;
  while (cur2) {
    if (index + 1 === targetIndex) {
      cur2.next = cur2.next ? cur2.next.next : null;
    }
    cur2 = cur2.next;
    index += 1;
  }

  return head;
};
  • 时间复杂度:${O(N)}$
  • 空间复杂度:${O(1)}$
  • 执行用时:64ms, 在所有JavaScript提交中击败了100%的用户,内存消耗:39.5MB,在所有JavaScript提交中击败了9%的用户

解法2:两次遍历+哑结点

因为头结点没有『前驱节点』,所以需要特殊处理。对链表进行操作时,一种常用的技巧是添加一个哑结点(Dummy Node),它的next指针指向链表的头节点,这样就不需要对头节点进行特殊处理了,只考虑通用情况既可以。

要注意,最后返回的应该是哑结点的next节点。

利用哑结点对上面的解法进行改造:

var removeNthFromEnd = function (head, n) {
  let cur1 = head,
    len = 0;
  while (cur1) {
    len += 1;
    cur1 = cur1.next;
  }

  const targetIndex = len - n;
  // 添加哑结点
  const dummyNode = new ListNode(0, head);

  let cur2 = dummyNode,
    index = 0;

  while (cur2) {
    if (index === targetIndex) {
      cur2.next = cur2.next ? cur2.next.next : null;
    }
    cur2 = cur2.next;
    index += 1;
  }

  return dummyNode.next;
};
  • 时间复杂度:${O(N)}$
  • 空间复杂度:${O(1)}$
  • 执行用时:80ms, 在所有JavaScript提交中击败了86%的用户,内存消耗:39.1MB,在所有JavaScript提交中击败了71%的用户

解法3:栈

首先沿用上面的小技巧,仍然声明一个哑结点,然后将所有节点依次入栈,完成后,然后再依次出栈,当出栈到第n个后,就是我们要找到的节点,当前栈顶的节点就是要删除节点的前置节点了

var removeNthFromEnd = function (head, n) {
  const dummyNode = new ListNode(0, head),
    stack = [];
  let cur1 = dummyNode;

  // 入栈
  while (cur1) {
    stack.push(cur1);
    cur1 = cur1.next;
  }

  let count = 0;

  while (count < n) {
    stack.pop();
    count += 1;
  }

  // 前置节点
  const target = stack.pop();
  target.next = target.next ? target.next.next : null;

  return dummyNode.next;
};
  • 时间复杂度:${O(N)}$
  • 空间复杂度:${O(1)}$
  • 执行用时:68ms, 在所有JavaScript提交中击败了98%的用户,内存消耗:39MB,在所有JavaScript提交中击败了86%的用户

解法4:快慢指针

上面的解法都使用了两次遍历,如果要缩减为一次遍历的话,无非是利用空间换时间,或者增加指针数量。可以把上面的单指针变为双指针,一个慢指针,一个快指针,快指针先走n步,慢指针再走,一起走到最后,这时慢指针就指向了倒数第n个节点

我们需要找到倒数第n个节点的前置节点,也就是倒数第n + 1个节点,所以可以让快指针先走一个节点,即快指针指向head,慢指针指向哑结点

var removeNthFromEnd = function (head, n) {
  const dummyNode = new ListNode(0, head);

  // 快指针先走一个
  let fast = head,
    slow = dummyNode,
    count = 0;

  // 快指针先走 n - 1 步
  while (count < n) {
    fast = fast.next;
    count += 1;
  }

  // 一起走到最后
  while (fast) {
    fast = fast.next;
    slow = slow.next;
  }

  // slow 就是倒数第 n 个节点的前置节点
  slow.next = slow.next ? slow.next.next : null;
  return dummyNode.next;
};
  • 时间复杂度:${O(N)}$
  • 空间复杂度:${O(1)}$
  • 执行用时:64ms, 在所有JavaScript提交中击败了100%的用户,内存消耗:39.2MB,在所有JavaScript提交中击败了60%的用户
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值