19. 删除链表的倒数第 N 个结点(双指针)

821 阅读3分钟

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。

每日刷题第23天 2021.1.18

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

题目

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

示例

  • 示例1 image.png
输入: head = [1,2,3,4,5], n = 2
输出: [1,2,3,5]
  • 示例2
输入: head = [1], n = 1
输出: []
  • 示例3
输入: head = [1,2], n = 1
输出: [1]

提示

  • 链表中结点的数目为 sz
  • 1 <= sz <= 30
  • 0 <= Node.val <= 100
  • 1 <= n <= sz 进阶: 你能尝试使用一趟扫描实现吗?

解法

解法1: 常规做法

  • 第一步:循环遍历一遍链表,记录下链表的总长度len
  • 第二步:用总长度len减去n得到从前往后需要遍历的个数m
    • m为0:当前链表从前往后找的节点数为0,即:需要删除头节点
    • m不为0:遍历m个节点,找到需要删除的节点的前一个节点
  • 第三步:链表删除节点的操作,将当前节点的前一节点的next指向当前节点的后一个节点
    • 即:prev.next = prev.next.next
var removeNthFromEnd = function(head, n) { 
  let h = head; 
  // 记录链表的长度 
  let len = 0; 
  while (h) { 
    len++; 
    h = h.next; 
  } 
  // console.log('链表长度',len); 
  len = len - n; // 1 1 
  // 删除头节点 len2 n2 
  if (len == 0){ 
    head = head.next; 
    return head; 
  }
  // 删除尾节点 
  // 删除中间节点 
  h = head; 
  while (len > 1 && h) {
    // 遍历到n-1节点 
    len--; 
    h = h.next; 
  }
  h.next = h.next.next; 
  return head; 
};

解法2: 自己写的双指针做法

  • 第一步:slow指针指向链表的头节点,fast 指针和slow开始时都指向链表的头节点。
  • 第二步:while循环fast指针为真的时候执行(第三种解法有优化🆕)
    • fast指针向前遍历n个节点后,判断fast指针是否为空(不是有效的节点),如果为空,则表示需要删除的节点就是slow节点,即头节点,执行slow = slow.next输出即可
    • fast.next为空表示,fast指针已经找到链表的最后一个节点(有效节点),那么slow指针指向的就是需要删除的节点的前一个节点。此时跳出循环break
    • fast && fast.next均为真时,表示当前还没有找到要删除的节点,因此将slow指针向前移动一个节点,fast指针指向slow指针所指的节点,重新往前遍历n个节点,重复循环♻️,指到找到需要删除的节点为止。
  • 第三步:因为只删除一个节点,因此直接将需要删除的节点的前一个节点的next指向,需要删除的后一个节点即可。
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/

/**
* @param {ListNode} head
* @param {number} n
* @return {ListNode}
*/

var removeNthFromEnd = function(head, n) {
  let slow = head;
  let fast = slow;
  // 记录需要循环的次数
  let nn = n;

  while (fast) {
    // 一遍循环遍历
    while (nn > 0) {
      nn--;
      fast = fast.next;
    }
    // fast.next 空 找到尾节点
    if (!fast) {
      // 不为有效节点
      slow = slow.next;
      return slow;
    }
    if (!fast.next) break;
    slow = slow.next;
    fast = slow;
    nn = n;
  }
  // 连接
  // 只删除一个
  slow.next = slow.next.next;
  return head;
};

解法3: (优化)参考leetcode官方的解题文章后,写的双指针做法

  • 第一步:创建新的表头,让slow指针指向新创建的表头
  • 第二步:将fast指针指向真正的表头head,并往后遍历n个节点,此时slowfast指针之间的距离,刚好是n
  • 第三步:(⚠️也是自己写双指针的时候没有考虑到的做法,做了很多重复的遍历)这里不需要将slow指针赋值给fast指针,重新遍历n个节点;而是在前面第二步的时候已经确定好slowfast指针之间的距离的基础上,移动slow往后一个节点,同时也将fast往后移动一个节点,始终确保两指针之间的间距为n个节点即可。
  • 第四步:当fast指针指向null,slow刚好指向需要删除的前一个节点,此时将slow.next = slow.next.next
  • 🏷️总结:这种方法,就不需要单独判断是否删除的是头节点
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/

/**
* @param {ListNode} head
* @param {number} n
* @return {ListNode}
*/

var removeNthFromEnd = function(head, n) {
  // 还可以优化
  // 1. 第一次fast移动n后,slow和fast之间的距离就固定了
  // 之后slow移动1,fast移动1即可
  // 2. 创建新的表头使用
  
  // 创建新的头指针
  let dummy = new ListNode(0,head); 
  let slow = dummy;
  let fast = head;
  
  for (let i = 0; i < n; i++) {
    // 现将fast指针前移n步
    fast = fast.next;
  }
  
  // 双指针依次往前变换
  while (fast) {
    slow = slow.next;
    fast = fast.next;
  }
  slow.next = slow.next.next;
  // 返回真正的链表头节点
  return dummy.next;
};

附录

  • 链表的注意事项⚠️
    • 在调用 next 字段之前,始终检查节点是否为空。
    • 获取空节点的下一个节点将导致空指针错误。例如,在我们运行 fast = fast.next.next 之前,需要检查 fast 和 fast.next 不为空。
    • 仔细定义循环的结束条件。