# 删除链表的倒数第N个结点 (Remove Nth Node From End of List) ## 题目描述 给你一个链表,删除链表的倒数第 `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` ### 进阶 你能尝试使用一趟扫描实现吗? ## 解题思路 ### 方法一:双指针法(推荐) **核心思想:**使用两个指针 `fast` 和 `slow`,`fast` 先移动 `n` 步,然后 `fast` 和 `slow` 一起移动,直到 `fast` 到达链表末尾。此时 `slow` 指向要删除结点的前一个结点。 **算法步骤:** 1. 创建哑结点 `dummy`,指向链表头 2. 初始化 `fast` 和 `slow` 指针都指向 `dummy` 3. `fast` 先移动 `n + 1` 步 4. `fast` 和 `slow` 同时移动,直到 `fast` 为 `nil` 5. 此时 `slow.next` 就是要删除的结点,执行 `slow.next = slow.next.next` 6. 返回 `dummy.next` **为什么移动 n + 1 步?** - 这样 `slow` 最终会停在要删除结点的前一个结点 - 方便删除操作:`slow.next = slow.next.next` ### 方法二:计算长度法 **核心思想:**先遍历链表计算长度,然后计算要删除的正数位置,再遍历到该位置删除结点。 **算法步骤:** 1. 遍历链表,计算长度 `length` 2. 要删除的正数位置为 `length - n` 3. 创建哑结点 `dummy`,指向链表头 4. 遍历到第 `length - n - 1` 个结点 5. 删除下一个结点 6. 返回 `dummy.next` ### 方法三:栈法 **核心思想:**将所有结点压入栈中,然后弹出 `n` 个结点,栈顶就是要删除结点的前一个结点。 **算法步骤:** 1. 创建哑结点 `dummy` 2. 将所有结点压入栈 3. 弹出 `n` 个结点 4. 栈顶结点的 `next` 指向要删除结点的 `next` 5. 返回 `dummy.next` ## 解法 ### Go 实现(双指针法) ```go package main import "fmt" // ListNode 链表结点定义 type ListNode struct { Val int Next *ListNode } func removeNthFromEnd(head *ListNode, n int) *ListNode { // 创建哑结点,处理删除头结点的特殊情况 dummy := &ListNode{0, head} fast, slow := dummy, dummy // fast 先移动 n + 1 步 for i := 0; i <= n; i++ { fast = fast.Next } // fast 和 slow 一起移动,直到 fast 为 nil for fast != nil { fast = fast.Next slow = slow.Next } // 删除 slow 的下一个结点 slow.Next = slow.Next.Next return dummy.Next } // 辅助函数:创建链表 func createList(nums []int) *ListNode { dummy := &ListNode{} current := dummy for _, num := range nums { current.Next = &ListNode{num, nil} current = current.Next } return dummy.Next } // 辅助函数:打印链表 func printList(head *ListNode) { current := head for current != nil { fmt.Printf("%d", current.Val) if current.Next != nil { fmt.Printf(" -> ") } current = current.Next } fmt.Println() } // 测试用例 func main() { // 测试用例1 head1 := createList([]int{1, 2, 3, 4, 5}) fmt.Print("输入: ") printList(head1) fmt.Printf("n = 2\n") result1 := removeNthFromEnd(head1, 2) fmt.Print("输出: ") printList(result1) // 测试用例2: 删除头结点 head2 := createList([]int{1}) fmt.Print("\n输入: ") printList(head2) fmt.Printf("n = 1\n") result2 := removeNthFromEnd(head2, 1) fmt.Print("输出: ") printList(result2) // 测试用例3: 删除最后一个结点 head3 := createList([]int{1, 2}) fmt.Print("\n输入: ") printList(head3) fmt.Printf("n = 1\n") result3 := removeNthFromEnd(head3, 1) fmt.Print("输出: ") printList(result3) // 测试用例4: 长链表 head4 := createList([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) fmt.Print("\n输入: ") printList(head4) fmt.Printf("n = 5\n") result4 := removeNthFromEnd(head4, 5) fmt.Print("输出: ") printList(result4) } ``` ### Go 实现(计算长度法) ```go func removeNthFromEndByLength(head *ListNode, n int) *ListNode { if head == nil { return nil } // 计算链表长度 length := 0 current := head for current != nil { length++ current = current.Next } // 要删除的正数位置 pos := length - n // 创建哑结点 dummy := &ListNode{0, head} current = dummy // 移动到要删除结点的前一个结点 for i := 0; i < pos; i++ { current = current.Next } // 删除结点 current.Next = current.Next.Next return dummy.Next } ``` ### Go 实现(栈法) ```go func removeNthFromEndByStack(head *ListNode, n int) *ListNode { if head == nil { return nil } // 创建哑结点 dummy := &ListNode{0, head} // 将所有结点压入栈 var stack []*ListNode current := dummy for current != nil { stack = append(stack, current) current = current.Next } // 弹出 n 个结点 for i := 0; i < n; i++ { stack = stack[:len(stack)-1] } // 栈顶就是要删除结点的前一个结点 prev := stack[len(stack)-1] prev.Next = prev.Next.Next return dummy.Next } ``` ## 复杂度分析 ### 双指针法 - **时间复杂度:** O(L) - 其中 L 是链表长度 - 只需遍历链表一次 - **空间复杂度:** O(1) - 只使用了常数级别的额外空间 - 只需要几个指针变量 ### 计算长度法 - **时间复杂度:** O(L) - 第一次遍历计算长度:O(L) - 第二次遍历删除结点:O(L) - 总时间复杂度:O(2L) = O(L) - **空间复杂度:** O(1) - 只使用了常数级别的额外空间 ### 栈法 - **时间复杂度:** O(L) - 需要遍历链表一次 - **空间复杂度:** O(L) - 需要额外的栈空间存储所有结点 ## 进阶问题 ### Q1: 如果链表是循环链表,应该如何处理? **方法**:检测循环,计算长度,然后调整删除位置 ```go func removeNthFromEndCircular(head *ListNode, n int) *ListNode { if head == nil { return nil } // 计算链表长度并检测循环 length := 1 slow, fast := head, head.Next for fast != nil && fast.Next != nil && slow != fast { slow = slow.Next fast = fast.Next.Next length++ } // 如果有循环 if slow == fast { // 计算循环长度 cycleLength := 1 slow = slow.Next for slow != fast { slow = slow.Next cycleLength++ } // 总长度 totalLength := length + cycleLength - 1 pos := totalLength - n // 处理位置调整 if pos < 0 { pos += totalLength } // 执行删除 return removeNthFromEndByPosition(head, pos) } // 没有循环,使用原有方法 return removeNthFromEnd(head, n) } ``` ### Q2: 如果要求删除前 n 个结点,应该如何修改? **方法**:直接删除前 n 个结点 ```go func removeFirstNNodes(head *ListNode, n int) *ListNode { for i := 0; i < n && head != nil; i++ { head = head.Next } return head } ``` ### Q3: 如果链表很长,如何优化内存使用? **方法**: 1. 使用固定大小的滑动窗口 2. 避免存储整个链表 3. 使用递归(但会增加栈空间) ```go func removeNthFromEndOptimized(head *ListNode, n int) *ListNode { // 使用固定大小的窗口 dummy := &ListNode{0, head} slow, fast := dummy, dummy // fast 先移动 n + 1 步 for i := 0; i <= n; i++ { if fast == nil { return head // n > 链表长度 } fast = fast.Next } // 移动窗口 for fast != nil { slow = slow.Next fast = fast.Next } // 删除结点 slow.Next = slow.Next.Next return dummy.Next } ``` ## P7 加分项 ### 1. 深度理解:为什么需要哑结点? **关键点:** - 处理删除头结点的特殊情况 - 统一处理逻辑,减少边界条件判断 - 简化代码,提高可读性 **哑结点的作用:** ```go // 没有哑结点的情况 func removeNthFromEndWithoutDummy(head *ListNode, n int) *ListNode { // 需要特殊处理删除头结点的情况 length := 0 current := head for current != nil { length++ current = current.Next } if length == n { return head.Next // 删除头结点 } // ... 其他逻辑 return head } ``` ### 2. 实战扩展:链表操作的通用技巧 **技巧总结:** - 使用哑结点简化边界处理 - 双指针技巧:快慢指针、前后指针 - 递归处理链表问题 - 栈辅助解决链表问题 **通用模板:** ```go func solveLinkedListProblem(head *ListNode) *ListNode { dummy := &ListNode{0, head} // ... 使用双指针或其他技巧 return dummy.Next } ``` ### 3. 变形题目 1. [19. 删除链表的倒数第N个节点](https://leetcode.cn/problems/remove-nth-node-from-end-of-list/) - 原题 2. [82. 删除排序链表中的重复元素 II](https://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/) - 删除所有重复元素 3. [83. 删除排序链表中的重复元素](https://leetcode.cn/problems/remove-duplicates-from-sorted-list/) - 删除重复元素保留一个 4. [203. 移除链表元素](https://leetcode.cn/problems/remove-linked-list-elements/) - 删除指定值的节点 ### 4. 优化技巧 **空间优化:** - 原地操作,不使用额外空间 - 递归改为迭代 **时间优化:** - 一次遍历完成 - 提前终止条件 **代码优化:** - 合并重复逻辑 - 减少不必要的变量 ### 5. 实际应用场景 **应用场景:** - 缓存淘汰策略(LRU) - 音乐播放列表管理 - 浏览器历史记录 - 撤销/重做功能 **面试问题:** - 如何处理并发访问的链表? - 如何实现线程安全的链表操作? ### 6. 面试技巧 **常见面试问题:** 1. 时间/空间复杂度分析 2. 边界条件处理 3. 优化思路 4. 相关题目变体 **回答技巧:** - 先给出暴力解法 - 逐步优化 - 说明权衡取舍 ### 7. 相关题目推荐 **相关题目:** 1. [206. 反转链表](https://leetcode.cn/problems/reverse-linked-list/) 2. [21. 合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/) 3. [141. 环形链表](https://leetcode.cn/problems/linked-list-cycle/) 4. [142. 环形链表 II](https://leetcode.cn/problems/linked-list-cycle-ii/) ## 总结 这道题的核心是: 1. **双指针法**:一次遍历,快慢指针配合 2. **边界处理**:使用哑结点简化删除头结点的处理 3. **多种解法**:双指针、计算长度、栈法各有优劣 **易错点**: - 忘记处理删除头结点的情况 - 快指针移动步数错误(应该是 n+1) - 空链表的特殊情况处理 - 循环链表的特殊情况 **最优解法**:双指针法,时间 O(L),空间 O(1)