LeetCode 第19题:删除链表的倒数第N个结点

LeetCode 第19题:删除链表的倒数第N个结点

题目描述

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

难度

中等

题目链接

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

示例

示例 1:

输入: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. 使用快慢指针,快指针先走n步
  2. 使用虚拟头节点简化边界情况
  3. 当快指针到达末尾时,慢指针的下一个节点就是要删除的节点

具体步骤:

  1. 创建虚拟头节点,简化边界情况
  2. 快指针先走n步
  3. 快慢指针同时移动,直到快指针到达末尾
  4. 删除慢指针的下一个节点
  5. 返回虚拟头节点的下一个节点

时间复杂度:O(n),其中n是链表长度
空间复杂度:O(1)

方法二:计算长度(两趟扫描)

先遍历一遍计算链表长度,再遍历一遍删除指定节点。

具体步骤:

  1. 第一次遍历计算链表长度length
  2. 计算正向位置:pos = length - n
  3. 第二次遍历到pos位置删除节点

代码实现

C# 实现(双指针)

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     public int val;
 *     public ListNode next;
 *     public ListNode(int val=0, ListNode next=null) {
 *         this.val = val;
 *         this.next = next;
 *     }
 * }
 */
public class Solution {
    public ListNode RemoveNthFromEnd(ListNode head, int n) {
        // 创建虚拟头节点
        ListNode dummy = new ListNode(0, head);
        ListNode fast = dummy;
        ListNode slow = dummy;
      
        // 快指针先走n步
        for (int i = 0; i < n; i++) {
            fast = fast.next;
        }
      
        // 快慢指针同时移动
        while (fast.next != null) {
            fast = fast.next;
            slow = slow.next;
        }
      
        // 删除目标节点
        slow.next = slow.next.next;
      
        return dummy.next;
    }
}

C# 实现(两趟扫描)

public class Solution {
    public ListNode RemoveNthFromEnd(ListNode head, int n) {
        // 创建虚拟头节点
        ListNode dummy = new ListNode(0, head);
      
        // 计算链表长度
        int length = 0;
        ListNode current = head;
        while (current != null) {
            length++;
            current = current.next;
        }
      
        // 找到要删除节点的前一个节点
        current = dummy;
        for (int i = 0; i < length - n; i++) {
            current = current.next;
        }
      
        // 删除目标节点
        current.next = current.next.next;
      
        return dummy.next;
    }
}

代码详解

双指针版本:

  1. 虚拟头节点:
    • 使用dummy节点简化边界情况
    • 特别是当需要删除第一个节点时
  2. 快指针移动:
    • 先走n步建立距离
    • 保持这个距离同时移动
  3. 删除节点:
    • slow.next指向slow.next.next
    • 完成节点删除操作

两趟扫描版本:

  1. 计算长度:
    • 遍历一遍得到总长度
    • 计算正向位置
  2. 删除节点:
    • 移动到目标位置的前一个节点
    • 修改next指针完成删除

执行结果

双指针版本:

  • 执行用时:84 ms
  • 内存消耗:37.9 MB

两趟扫描版本:

  • 执行用时:88 ms
  • 内存消耗:38.1 MB

总结与反思

  1. 这道题的关键点:
    • 使用虚拟头节点简化操作
    • 双指针技巧的应用
    • 边界情况的处理
  2. 两种解法比较:
    • 双指针:一趟扫描,更优雅
    • 两趟扫描:思路直观,但效率较低
  3. 优化思路:
    • 使用虚拟头节点
    • 一趟扫描实现
    • 注意边界条件

相关题目

posted @   旧厂街小江  阅读(4)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示