概念
在 LeetCode 中,当题目描述中提到“倒数第 n 个结点”,通常是从 1 开始计数的。也就是说,倒数第 1 个结点是链表的最后一个结点。
例如,对于链表 [1,2,3,4,5],倒数第 1 个结点是 5,倒数第 2 个结点是 4,依此类推。
易错点
核心概念
双指针技巧: 这是一个在链表问题中常见的技巧,特别是与链表长度或位置相关的题目。通过使用两个指针间隔一定的距离移动,我们可以避免对链表的多次遍历。在这个问题中,我们使用两个指针,一个先行指针和一个后行指针,先行指针领先后行指针 n 步。当先行指针到达链表的末尾时,后行指针指向的节点就是我们需要删除的节点的前一个节点。
2. 代码中的错误
错误:second 指针始终指向头结点,没有随着 first 指针的移动而移动。 原因:当 first 指针向前移动时,second 指针应该也要开始移动,以保持它们之间的距离。但在您的代码中,second 只在 first 到达链表尾部时开始移动,这导致了错误。
您当时可能是怎么想的:您可能想让 first 指针先行,然后再移动 second 指针。但您可能忘记了,一旦 first 指针移动了 n 步,second 指针就应该开始和 first 指针一起移动。
如何避免:
- 清晰地描述算法逻辑:在开始编码之前,先在纸上或脑中清晰地描述算法逻辑。
- 绘制示意图:对于链表问题,通过绘制示意图来表示节点和指针的关系,这有助于更好地理解和避免错误。
- 测试代码:使用多个测试用例,特别是边界条件来测试代码,确保其正确性。
为什么要使用 var first: ListNode? = dummy
var dummy = ListNode(0)
dummy.next = head
var first: ListNode? = dummy
var second: ListNode? = dummy
如果 second 从 head 开始,并且我们要删除的是链表的第一个节点,那么当 first 遍历到链表末尾时,second 并不会停留在 head。它会移动到需要删除的节点的前一个节点。但问题在于,当我们需要删除的是链表的第一个节点时,second 本应该停留在一个“虚拟节点”上,这样我们才能删除 head。但由于没有这样的“虚拟节点”,所以我们无法正确地删除 head。
因此,为了统一处理所有情况(包括删除链表的第一个节点),我们使用 dummy 作为起始节点。这样,无论我们要删除的节点是链表中的任何一个节点,second 都会正确地指向它的前一个节点。
题目
19. 删除链表的倒数第 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 <= 300 <= Node.val <= 1001 <= n <= sz
解题思路🙋🏻 ♀️
边界思考🤔
代码
class Solution {
func removeNthFromEnd(_ head: ListNode?, _ n: Int) -> ListNode? {
if n < 1 {
return head
}
var dummy = ListNode(0)
dummy.next = head
var first: ListNode? = dummy
var second: ListNode? = dummy
var count = 0
while count < n {
first = first?.next
count += 1
// 检查first是否为nil
if first == nil && count < n {
return head
}
}
while first?.next != nil {
first = first?.next
second = second?.next
}
second?.next = second?.next?.next
return dummy.next
}
}
时空复杂度分析
O(n)
引用
本系列文章部分概念内容引用 www.hello-algo.com/
解题思路参考了 abuladong 的算法小抄, 代码随想录... 等等
Youtube 博主: huahua 酱, 山景城一姐,