力扣解题-19. 删除链表的倒数第 N 个结点

0 阅读5分钟

力扣解题-19. 删除链表的倒数第 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

进阶:你能尝试使用一趟扫描实现吗?

Related Topics

链表、双指针


第一次解答

解题思路

核心方法:快慢双指针法(一趟扫描版),通过让快指针先移动n步,再让快慢指针同步移动,当快指针到达链表尾部时,慢指针恰好指向“倒数第n个节点的前驱节点”,直接删除目标节点,时间复杂度O(n)、空间复杂度O(1),满足进阶的“一趟扫描”要求。

核心逻辑拆解

删除倒数第n个节点的核心难点是“如何一次遍历找到倒数第n个节点的前驱”:

  1. 哑节点初始化:创建dummy哑节点(dummy.next = head),解决“删除头节点”的边界问题(如示例2中删除唯一节点1);
  2. 双指针初始化:定义fastDummyslowDummy均指向dummy(避免单独处理头节点);
  3. 快指针先行n步
    • 遍历n次,将fastDummy向后移动n步,使快慢指针之间保持n个节点的距离;
    • (示例1中n=2,fastDummy从dummy→1→2,与slowDummy(dummy)间距2);
  4. 快慢指针同步移动
    • 循环条件:fastDummy.next != null(快指针未到链表尾部);
    • 每次循环,fastDummyslowDummy各向后移动一步;
    • 当快指针到达尾部(fastDummy.next = null),慢指针slowDummy恰好指向“倒数第n个节点的前驱节点”;
    • (示例1中最终slowDummy指向3,其next是4(倒数第2个节点));
  5. 删除目标节点slowDummy.next = slowDummy.next.next(跳过倒数第n个节点,完成删除);
  6. 返回结果:返回dummy.next(跳过哑节点,得到删除后的链表头)。
具体步骤(以示例1 head=[1,2,3,4,5]、n=2为例)
步骤操作fastDummy指向slowDummy指向说明
1初始化dummy→1→2→3→4→5dummydummy哑节点避免头节点边界问题
2快指针移动1步(i=1)1dummy保持间距第一步
3快指针移动2步(i=2)2dummy快指针完成n步先行
4同步移动1次31快指针未到尾部
5同步移动2次42快指针未到尾部
6同步移动3次53fastDummy.next=null,循环终止
7删除节点53slowDummy.next=5(跳过4)
最终链表:1→2→3→5,与示例结果一致。
性能说明
  • 时间复杂度:O(n)(仅一趟扫描,快指针移动n步 + 快慢指针同步移动(sz-n)步,总步数=sz ≤30);
  • 空间复杂度:O(1)(仅使用几个指针变量,无额外存储);
  • 优势:
    1. 一趟扫描完成所有操作,满足进阶要求,执行效率最高;
    2. 哑节点完美解决删除头节点的边界问题(如示例2、3);
    3. 双指针间距固定,逻辑直观,无冗余计算。
    public ListNode removeNthFromEnd(ListNode head, int n) {
        //初始化
        ListNode dummy=new ListNode(0);
        dummy.next=head;
        //定义一个快,一个慢的栈
        ListNode fastDummy=dummy;
        ListNode slowDummy=dummy;
        //定位到比slow快n个的栈
        for(int i=1;i<=n;i++){
            fastDummy=fastDummy.next;
        }
        //开始循环
        while(fastDummy.next!=null) {
            fastDummy = fastDummy.next;
            slowDummy = slowDummy.next;
        }
        slowDummy.next=slowDummy.next.next;
        return dummy.next;
        }
    }

示例解答

解题思路

解法1:两次遍历法(基础思路,对比参考)

核心方法:先统计长度再定位删除,第一次遍历统计链表总长度sz,第二次遍历到“sz-n”位置(倒数第n个节点的前驱),删除目标节点,逻辑更简单但需要两次扫描,时间复杂度仍为O(n),空间复杂度O(1)。

代码实现
public ListNode removeNthFromEnd(ListNode head, int n) {
    // 哑节点处理边界
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    ListNode curr = dummy;
    int sz = 0;
    
    // 第一次遍历:统计链表长度
    while (curr.next != null) {
        sz++;
        curr = curr.next;
    }
    
    // 第二次遍历:定位到倒数第n个节点的前驱(sz-n位置)
    curr = dummy;
    for (int i = 0; i < sz - n; i++) {
        curr = curr.next;
    }
    
    // 删除目标节点
    curr.next = curr.next.next;
    
    return dummy.next;
}
适用场景说明
  • 时间复杂度:O(n)(两次遍历,总步数=2sz);
  • 空间复杂度:O(1);
  • 优势:逻辑极简,新手易理解,无需理解双指针间距的逻辑;
  • 劣势:需要两次扫描链表,效率略低于双指针法,不满足进阶要求。
解法2:栈辅助法(思路拓展)

核心方法:利用栈的“后进先出”特性,将所有节点入栈,再出栈n次,栈顶元素即为倒数第n个节点的前驱,删除目标节点,逻辑直观但需要额外空间。

代码实现
import java.util.Stack;

public ListNode removeNthFromEnd(ListNode head, int n) {
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    Stack<ListNode> stack = new Stack<>();
    ListNode curr = dummy;
    
    // 所有节点入栈
    while (curr != null) {
        stack.push(curr);
        curr = curr.next;
    }
    
    // 出栈n次,找到倒数第n个节点的前驱
    for (int i = 0; i < n; i++) {
        stack.pop();
    }
    
    // 栈顶是前驱节点,删除目标节点
    ListNode prev = stack.peek();
    prev.next = prev.next.next;
    
    return dummy.next;
}
性能说明
  • 时间复杂度:O(n)(入栈+出栈共2n步);
  • 空间复杂度:O(n)(栈存储所有节点);
  • 优势:无需计算指针间距,利用栈的特性直接定位前驱,逻辑易理解;
  • 劣势:额外使用栈存储节点,空间开销高于双指针法,不推荐在工程中使用。

总结

  1. 快慢双指针法(第一次解答):O(n)时间+O(1)空间,一趟扫描完成,满足进阶要求,是工程首选的最优解法;
  2. 两次遍历法:O(n)时间+O(1)空间,逻辑极简,适合新手理解核心思路但效率略低;
  3. 栈辅助法:O(n)时间+O(n)空间,思路拓展但有额外空间开销,仅作参考;
  4. 关键技巧:
    • 核心思想:双指针间距固定n步,一次遍历定位目标节点前驱,避免多次扫描;
    • 边界处理:哑节点是解决“删除头节点”的关键,所有解法均需优先使用;
    • 进阶优化:双指针法是链表“倒数第k个节点”类问题的通用最优解法,需重点掌握。