算法:删除链表的倒数第 N 个结点
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
示例 1:
1 | 输入:head = [1,2,3,4,5], n = 2 |
示例 2:
1 | 输入:head = [1], n = 1 |
示例 3:
1 | 输入:head = [1,2], n = 1 |
提示:
- 链表中结点的数目为
sz
1 <= sz <= 30
0 <= Node.val <= 100
1 <= n <= sz
进阶:你能尝试使用一趟扫描实现吗?
解法
1 | func removeNthFromEnd(_ head: ListNode?, _ n: Int) -> ListNode? { |
算法使用了“双指针”技术,通过设置一个“快指针”和一个“慢指针”来高效地完成这个任务。
1. 空链表检查
1 | if head == nil { return head } |
与上一个版本一样,首先我们检查链表是否为空。如果链表为空,直接返回原链表。
2. 初始化两个指针
1 | var first: ListNode? = head |
first
是快指针,最初指向链表的头节点。last
是慢指针,最初也指向链表的头节点。m
是一个计数器,帮助我们控制last
指针移动的步数。
3. 移动快指针
1 | while first?.next != nil { |
这里我们开始循环,直到
first
指针走到链表的最后一个节点。在每次循环中,
first
指针都会向前移动一步。m
变量用于控制last
指针的移动。如果m
达到n
,意味着first
指针已经走过了n
步,此时我们开始让last
指针也移动,这样last
和first
之间的距离就始终保持为n
。
4. 删除倒数第 N 个节点的特殊情况
1 | if m < n { |
这一部分是处理特殊情况。如果链表的长度小于 n
(例如 n
比链表长度还大),我们直接返回 head?.next
,相当于删除头节点。
5. 删除倒数第 N 个节点
1 | last?.next = last?.next?.next |
一旦 first
指针到达链表的最后一个节点时,last
指针就会指向倒数第 n+1
个节点。此时,last?.next
就是倒数第 N
个节点。我们通过将 last?.next
指向 last?.next?.next
,成功删除了倒数第 N
个节点。
6. 返回修改后的链表头
1 | return head |
最后,我们返回链表的头节点。即使删除了倒数第 N
个节点,链表的头节点仍然是原始头节点。
总结:
这段代码的关键思想与前一种解法类似:通过快慢指针的技巧来找出倒数第 N
个节点。主要不同点在于:
该实现使用了
m
作为计数器来控制last
指针的移动,直到first
指针走了n
步,last
指针才开始移动。处理链表长度小于
n
的情况时,通过返回head?.next
来直接删除链表的头节点。
这种方式具有 O(L)
的时间复杂度和 O(1)
的空间复杂度,其中 L
是链表的长度。
优化后
1 | func removeNthFromEnd(_ head: ListNode?, _ n: Int) -> ListNode? { |
与上一种解法相比,这种写法实现方式有所不同,依然采用了类似的“双指针”技巧,但在处理快慢指针时的细节和逻辑略有不同。让我们逐步讲解这个算法的每一部分:
1. 初始化和空链表检查:
1 | if head == nil { return nil } |
首先,我们检查链表是否为空。如果是空链表,直接返回 nil
。
2. 设置快指针:
1 | var fast: ListNode? = head |
我们定义了一个 fast
指针,并将其初始化为链表的头节点。接下来,我们让 fast
指针先走 n
步。这样,fast
指针在执行完这个循环后,会离链表的尾部有 n
个节点的距离。
需要特别注意的是,如果链表长度小于 n
,我们直接返回 head?.next
,这是因为倒数第 N
个节点不存在,我们只需要删除链表中的第一个节点。
3. 设置慢指针:
1 | var low: ListNode? = head |
现在,fast
指针已经向前走了 n
步,因此接下来,我们定义一个 low
指针,初始时指向头节点。然后,我们同时让 fast
和 low
一起向前移动,直到 fast
指针到达链表的最后一个节点。由于 fast
和 low
之间的距离保持为 n
,当 fast
到达链表的尾部时,low
就会指向倒数第 n+1
个节点。
4. 删除倒数第 N 个节点:
1 | low?.next = low?.next?.next |
此时,low
指针指向的是倒数第 n+1
个节点,而 low?.next
就是我们要删除的倒数第 N
个节点。我们通过将 low?.next
的指针指向 low?.next?.next
来删除这个节点。
5. 返回结果:
1 | return head |
最后,我们返回修改后的链表头节点 head
,这样链表中的节点已经删除了倒数第 N
个节点。
总结:
这个算法的关键点在于利用快慢指针的技巧。通过先让快指针走 n
步,然后快慢指针一起前进,直到快指针到达链表末尾,这样慢指针就能精确地定位到倒数第 n+1
个节点的位置。然后通过调整指针来删除倒数第 N
个节点,完成删除操作。这个算法的时间复杂度是 O(L)
,其中 L
是链表的长度,空间复杂度是 O(1)
,因为我们只使用了常数空间。