全栈开发知识库

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

题目

给你一个链表,删除链表的倒数第 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

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

思路

难点在于"倒数"

如果题目说的是"删除正数第 n 个",那太简单了——从头走 n-1 步就到了。

但这里是倒数第 n 个。链表是单向的,我们没法从尾巴往回走。最朴素的办法是:先遍历一遍数出总长度 L,那么倒数第 n 个就是正数第 L - n + 1 个,再走一遍去删。这需要走两遍。

有没有办法只走一遍就找到倒数第 n 个?有,用快慢双指针。

快慢指针:制造一个"固定的间距"

核心思想非常巧妙:

  • 让两个指针 fast 和 slow,先让 fast 领先 slow 恰好 n 步。然后两个指针一起同速往前走,当 fast 走到末尾时,slow 正好停在倒数第 n 个节点的位置。

为什么?因为两个指针始终保持 n 的间距。当 fast 到达终点(末尾)时,slow 距离终点也正好是 n,那 slow 指向的不就是倒数第 n 个吗?

打个比方:两个人在跑道上,前面的人(fast)领先后面的人(slow)恰好 n 米,两人同速跑。当前面的人冲过终点线时,后面的人离终点线正好还剩 n 米。

一个细节:要删除节点,得站在它"前面" 在单链表里删除一个节点,需要拿到它的前一个节点 prev,然后执行 prev.next = prev.next.next 把它跳过去。

所以我们真正想让 slow 停的位置,不是"倒数第 n 个"本身,而是"倒数第 n 个的前一个"(也就是倒数第 n+1 个)。

实现上有个小技巧:让 fast 领先 slow 走 n+1 步,这样 fast 到末尾(null)时,slow 就停在待删节点的前一个了。

老朋友 dummy 又来了

如果要删的是头节点(比如 [1], n=1,删的是第一个也是唯一一个节点),那"前一个节点"就不存在了。老规矩,在头节点前面加一个 dummy 哨兵节点,让 slow 从 dummy 出发,这样即使删头节点也有"前一个"可用,不用特判。

图解整个过程(head = [1,2,3,4,5], n=2)

  • 初始,加dummy,fast和slow都从dummy出发:
fast = dummy
slow = dummy
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> null
  • 让fast先走n+1 = 3步
slow = dummy
fast = 3
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> null

现在fast和slow的间距是3.

  • 两个指针一起走,直到fast变成null
slow = 3
fast = null (到null了,停)
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> null

现在slow停在节点3,它正好是待删除节点4(倒数第2个)的前一个。

  • 删除
slow.next = slow.next.next  // 3的next从4改成5
dummy -> 1 -> 2 -> 3 -> 5 -> null

返回dummy.next,结果是 1 -> 2 -> 3 -> 5


为什么是n+1步,而不是n步

如果fast先走n步,当fast到null的时候slow的位置正好是倒数第n个节点本身,这个时候只能拿到节点,删不了它。

因为要删除单链表节点必须站在它前面,所以我们要多走1步,让slow停在前一个的位置。


class ListNode(var `val`: Int) {
    var next: ListNode? = null
}

class Solution {
    fun removeNthFromEnd(head: ListNode?, n: Int): ListNode? {
        // 1. 哨兵节点,指向 head,统一处理"删除头节点"的边界情况
        val dummy = ListNode(0)
        dummy.next = head

        // 2. 快慢指针都从 dummy 出发
        var fast: ListNode? = dummy
        var slow: ListNode? = dummy

        // 3. 让 fast 先走 n+1 步,制造出 n+1 的间距
        //    这样 fast 到达 null 时,slow 恰好停在"待删节点的前一个"
        for (i in 0 until n + 1) {
            fast = fast?.next
        }

        // 4. 两指针同速前进,直到 fast 走到末尾 (null)
        while (fast != null) {
            fast = fast.next
            slow = slow?.next
        }

        // 5. 此时 slow 是待删节点的前一个,跳过待删节点完成删除
        slow?.next = slow?.next?.next

        // 6. 返回真正的头节点
        return dummy.next
    }
}

复杂度

  • 时间复杂度: O(n)(这里 n 指链表长度):fast 从头走到尾,一共只遍历一遍链表。相比"先数长度再删"的两遍遍历,双指针只需一遍。
  • 空间复杂度: O(1):只用了 dummy、fast、slow 几个指针,没有额外空间。


  • 邮箱 :charon.chui@gmail.com
  • Good Luck!
本页由 Markdown 自动渲染 · 共 628 篇笔记