力扣题目-删除链表的倒数第n个节点

84 阅读5分钟

一、题目

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

 

示例 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

二、解答

题目分析

题目要求

给定一个链表的头节点 head 和一个整数 n,需删除链表的倒数第 n 个节点,并返回删除后的链表头节点。

关键条件
  1. 倒数定位:需通过链表的倒数位置确定待删除节点,而非正数位置。

  2. 边界处理

    • 当链表仅有一个节点(如示例 2)时,删除后返回空链表。
    • 当删除头节点(如示例 3 中 n=1 时),需直接修改头节点引用。
  3. 一次遍历要求:理想解法需在单次遍历内完成定位和删除,避免两次遍历的额外时间开销。

核心思路

双指针法(快慢指针) :通过维护两个指针的固定间隔,在一次遍历中定位目标节点的前驱节点,从而实现高效删除。

  1. 哑节点辅助:引入哑节点(dummy node)作为头节点的前驱,统一处理头节点删除的边界情况。

  2. 指针间隔控制

    • 快指针(first)先移动 n 步,与慢指针(second)形成 n 步的间隔。
    • 随后两指针同步移动,当快指针到达链表末尾时,慢指针恰好指向倒数第 n+1 个节点(即待删除节点的前驱)。
  3. 删除操作:通过修改慢指针的 next 指针,跳过待删除节点,完成删除。

示例分析

示例 1

输入:head = [1,2,3,4,5], n = 2
分析

  • 链表长度为 5,倒数第 2 个节点是值为 4 的节点(正数第 4 个节点)。

  • 使用双指针:

    • 快指针先移动 2 步,指向节点 3(值为 3),慢指针指向哑节点。
    • 同步移动两指针至快指针到达末尾(null),此时慢指针指向节点 3(倒数第 3 个节点,即待删除节点的前驱)。
    • 删除慢指针的下一个节点(值为 4),链表变为 [1,2,3,5]。
      输出:[1,2,3,5]
示例 2

输入:head = [1], n = 1
分析

  • 链表仅有一个节点,倒数第 1 个节点即头节点。
  • 哑节点的 next 指向头节点,快指针移动 1 步后指向 null,慢指针仍指向哑节点。
  • 删除慢指针的下一个节点(头节点),返回哑节点的 next(null)。
    输出:[]
示例 3

输入:head = [1,2], n = 1
分析

  • 倒数第 1 个节点是值为 2 的节点(正数第 2 个节点)。
  • 快指针移动 1 步后指向节点 2,慢指针指向哑节点。
  • 快指针继续移动至 null,慢指针仍指向哑节点,此时删除慢指针的下一个节点(值为 1 的头节点?不,注意:n=1 时,快指针初始移动 1 步后指向节点 2,同步移动后快指针变为 null,慢指针指向哑节点。待删除节点是慢指针的下一个节点的下一个节点(哑节点.next 是头节点 1,头节点的 next 是 2,待删除的是 2,因此慢指针(哑节点)的 next 应指向 2 的 next(null),最终头节点仍为 1)。
    输出:[1]

算法思路

预处理
  1. 创建哑节点dummy = ListNode(0),其 next 指向头节点 head,便于统一处理头节点删除。
  2. 初始化双指针:快指针 first 和慢指针 second 均指向哑节点。
双指针移动
  1. 快指针先行:将 first 移动 n 步,使其与 second 间隔 n 个节点。
  2. 同步移动:同时移动 first 和 second,直至 first 到达链表末尾(null)。此时,second 指向倒数第 n+1 个节点(即待删除节点的前驱)。
删除节点

修改 second 的 next 指针,使其跳过待删除节点(second.next = second.next.next),完成删除。

复杂度分析
  • 时间复杂度:O (L),其中 L 为链表长度。仅需一次遍历(快指针移动 L+1 步,慢指针移动 L+1 - n 步)。
  • 空间复杂度:O (1),仅使用常数额外空间(哑节点和双指针)。

代码(python)

class Solution:
    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        class Solution:
    def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
        def getLength(head: ListNode) -> int:
            length = 0
            while head:
                length += 1
                head = head.next
            return length
        
        dummy = ListNode(0, head)
        length = getLength(head)
        cur = dummy
        for i in range(1, length - n + 1):
            cur = cur.next
        cur.next = cur.next.next
        return dummy.next

这段代码使用两次遍历来解决问题:第一次遍历计算链表长度,第二次遍历定位并删除目标节点。下面是对代码的详细分析:

方法思路

  1. 计算链表长度:通过辅助函数getLength遍历链表,统计节点总数length
  2. 确定删除位置:倒数第n个节点的正数位置为length - n + 1(链表索引从 1 开始)。
  3. 哑节点辅助:创建哑节点dummy指向头节点,处理删除头节点的特殊情况。
  4. 定位前驱节点:从哑节点开始,移动length - n步,到达目标节点的前驱节点。
  5. 删除操作:修改前驱节点的next指针,跳过目标节点。

代码解释

  1. 辅助函数getLength

    • 遍历链表,累加计数器length,直到链表末尾(headNone)。
    • 时间复杂度为 O (L),L 为链表长度。
  2. 哑节点的作用

    • 创建哑节点dummy,其next指向头节点head
    • 当需要删除头节点时(如n=length),哑节点确保操作的一致性,避免单独处理头节点的边界情况。
  3. 定位前驱节点

    • 计算正数位置length - n + 1,但由于从哑节点开始移动,实际需移动length - n步。
    • 循环for i in range(1, length - n + 1)执行length - n次,使cur指向目标节点的前驱。
  4. 删除操作

    • 通过cur.next = cur.next.next直接跳过目标节点,完成删除。

复杂度分析

  • 时间复杂度:O (L),其中 L 为链表长度。需两次遍历,每次遍历时间为 O (L)。
  • 空间复杂度:O (1),仅使用常数级额外空间(哑节点和指针)。