LeetCode 19 删除链表的倒数第 N 个结点(dummy + 快慢指针,一次遍历)

0 阅读3分钟

image.png

一、题目大意

给定一个链表,删除链表的倒数第 n 个结点,返回链表的头结点。 示例: 输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5],删除倒数第 2 个节点 4。 特殊边界:

  1. 链表只有一个节点 [1],n=1 →输出空链表;
  2. 删除链表第一个节点。

要求:最好一趟遍历完成;如果不使用 dummy,删除头节点要单独 if 分支判断。

二、解题思路

核心思想:虚拟头结点 + 快慢指针

  1. 虚拟头结点 dummydummy.next = head。统一删除逻辑,不用单独处理删除头结点的特殊情况。
  2. 快慢指针slowfast全部初始指向dummy
  3. fast 指针先走 n 步,制造快慢指针之间 n 个节点的间隔。
  4. 循环条件:while(fast.next != null),fast 和 slow 同步向后移动。
  5. 循环结束,slow 停在待删除节点的前驱节点

删除链表节点,必须拿到前驱节点,执行:slow.next = slow.next.next

  1. 返回dummy.next
  • 时间复杂度:O (L),L 链表长度,只遍历一次链表
  • 空间复杂度:O (1),仅使用几个指针变量

三、完整 Java 代码

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(-1);
        dummy.next = head; //虚拟节点接上原链表
        ListNode slow = dummy;
        ListNode fast = dummy;

        //fast先走n步
        for(int i = 0; i < n; i++){
            fast = fast.next;
        }
        //fast后面还有节点,快慢同步向后移动
        while(fast.next != null){
            slow = slow.next;
            fast = fast.next;
        }
        //slow为待删节点前驱,执行删除
        slow.next = slow.next.next;
        return dummy.next;
    }
}

四、实例演示

样例 1:head=[1,2,3,4,5],n=2

链表结构:dummy(-1) →1→2→3→4→5

步骤fast 位置fast.nextslow 位置操作说明
初始-1(dummy)1-1(dummy)快慢指针指向 dummy
fast 第 1 次先走12-1(dummy)i=0
fast 第 2 次先走23-1(dummy)i=1,n 步走完
同步移动①341fast.next≠null,一起后移
同步移动②452fast.next≠null,一起后移
同步移动③5null3fast.next==null,退出循环

循环结束,slow = 节点 3(待删节点 4 的前驱);执行slow.next = slow.next.next,节点 3 直接指向 5,删除节点 4。 输出链表:[1,2,3,5]

边界样例 2:head=[1], n=1

链表:dummy(-1) →1

步骤fast 位置fast.nextslow 位置操作说明
初始-1(dummy)1-1(dummy)快慢指针指向 dummy
fast 先走 1 步1null-1(dummy)n=1 步完成
循环判断nullnull-1(dummy)fast.next 为 null,循环不进入

slow 仍然在 dummy;执行slow.next = slow.next.nextdummy.next = null,返回空链表。

五、语法坑与边界总结

  1. 必须写dummy.next = head,虚拟头结点要接上原链表,否则链表为空,直接空指针。

  2. 循环条件和 fast 先走步数必须配对

    • ❌错误组合:fast 先走 n 步,写while(fast != null);遇到删除头节点样例,slow 跑到待删节点本身,slow.next为 null,访问slow.next.next报空指针。
  3. 删除语句:slow.next = slow.next.next

    • ❌错误写法:slow = slow.next.next,只移动 slow 指针,没有修改链表连接,节点不会被删除
  4. slow 最终位置要求:待删除节点的前驱节点,不是待删除节点本身。

  5. 返回值永远返回dummy.next,不能返回 head。

  6. 边界测试用例:

    • 链表只有单个节点;
    • 删除链表第一个节点;
    • 删除链表末尾节点。

六、个人感悟

  1. 遇到链表可能删除头节点的场景,优先考虑虚拟头结点dummy,省去大量 if 分支判断。
  2. 快慢指针做题,重点不是单纯移动指针,要思考循环结束之后指针停在哪一个位置。
  3. 链表删除节点的核心逻辑:一定要拿到待删节点的前一个节点,通过修改 next 引用实现删除。
  4. 两套快慢指针写法都可以 AC,优先选用fast先走n步 + while(fast.next != null),边界样例不容易出错。