# 删除链表的倒数第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) } ``` ### Java 实现(双指针法) ```java public class RemoveNthFromEnd { // 链表结点定义 public static class ListNode { int val; ListNode next; ListNode() {} ListNode(int val) { this.val = val; } ListNode(int val, ListNode next) { this.val = val; this.next = next; } } public ListNode removeNthFromEnd(ListNode head, int n) { // 创建哑结点,处理删除头结点的特殊情况 ListNode dummy = new ListNode(0, head); ListNode fast = dummy; ListNode slow = dummy; // fast 先移动 n + 1 步 for (int i = 0; i <= n; i++) { fast = fast.next; } // fast 和 slow 一起移动,直到 fast 为 null while (fast != null) { fast = fast.next; slow = slow.next; } // 删除 slow 的下一个结点 slow.next = slow.next.next; return dummy.next; } // 辅助函数:创建链表 private ListNode createList(int[] nums) { ListNode dummy = new ListNode(); ListNode current = dummy; for (int num : nums) { current.next = new ListNode(num); current = current.next; } return dummy.next; } // 辅助函数:打印链表 private void printList(ListNode head) { ListNode current = head; while (current != null) { System.out.print(current.val); if (current.next != null) { System.out.print(" -> "); } current = current.next; } System.out.println(); } // 测试用例 public static void main(String[] args) { RemoveNthFromEnd solution = new RemoveNthFromEnd(); // 测试用例1 ListNode head1 = solution.createList(new int[]{1, 2, 3, 4, 5}); System.out.print("输入: "); solution.printList(head1); System.out.println("n = 2"); ListNode result1 = solution.removeNthFromEnd(head1, 2); System.out.print("输出: "); solution.printList(result1); // 测试用例2: 删除头结点 ListNode head2 = solution.createList(new int[]{1}); System.out.print("\n输入: "); solution.printList(head2); System.out.println("n = 1"); ListNode result2 = solution.removeNthFromEnd(head2, 1); System.out.print("输出: "); solution.printList(result2); // 测试用例3: 删除最后一个结点 ListNode head3 = solution.createList(new int[]{1, 2}); System.out.print("\n输入: "); solution.printList(head3); System.out.println("n = 1"); ListNode result3 = solution.removeNthFromEnd(head3, 1); System.out.print("输出: "); solution.printList(result3); // 测试用例4: 长链表 ListNode head4 = solution.createList(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); System.out.print("\n输入: "); solution.printList(head4); System.out.println("n = 5"); ListNode result4 = solution.removeNthFromEnd(head4, 5); System.out.print("输出: "); solution.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 } ``` ### Java 实现(栈法) ```java import java.util.Stack; public ListNode removeNthFromEndByStack(ListNode head, int n) { // 创建哑结点 ListNode dummy = new ListNode(0, head); // 将所有结点压入栈 Stack stack = new Stack<>(); ListNode current = dummy; while (current != null) { stack.push(current); current = current.next; } // 弹出 n 个结点 for (int i = 0; i < n; i++) { stack.pop(); } // 栈顶就是要删除结点的前一个结点 ListNode prev = stack.peek(); 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: 如果链表是循环链表,应该如何处理? **A:** 需要先判断是否为循环链表,如果是,需要找到尾结点并断开循环。 ```go func removeNthFromEndCircular(head *ListNode, n int) *ListNode { if head == nil { return nil } // 检测是否有环 hasCycle := detectCycle(head) if !hasCycle { return removeNthFromEnd(head, n) } // 如果有环,需要先找到环的入口和长度 // 然后根据 n 的值决定如何删除 // 这是一个复杂的问题,需要更多边界条件处理 return head } func detectCycle(head *ListNode) bool { slow, fast := head, head for fast != nil && fast.Next != nil { slow = slow.Next fast = fast.Next.Next if slow == fast { return true } } return false } ``` ### Q2: 如果要求删除前 n 个结点,应该如何修改? **A:** 直接遍历到第 n-1 个结点,然后删除后续所有结点。 ```go func removeFirstN(head *ListNode, n int) *ListNode { if n <= 0 { return head } dummy := &ListNode{0, head} current := dummy // 移动到第 n 个结点的前一个结点 for i := 0; i < n && current != nil; i++ { current = current.Next } if current != nil { current.Next = nil } return dummy.Next } ``` ### Q3: 如果链表很长,如何优化内存使用? **A:** 使用双指针法是最优的,因为它不需要额外的空间。另外,可以考虑使用尾递归优化(如果语言支持)。 ## P7 加分项 ### 1. 深度理解:为什么需要哑结点? **哑结点的作用:** 1. **统一处理:** 避免单独处理删除头结点的特殊情况 2. **简化边界条件:** 当要删除的是头结点时,普通方法需要特殊处理 3. **代码简洁:** 使用哑结点后,删除操作统一为 `prev.next = prev.next.next` **没有哑结点的问题:** ```go // 没有哑结点的版本(需要特殊处理删除头结点) func removeNthFromEndWithoutDummy(head *ListNode, n int) *ListNode { length := 0 current := head for current != nil { length++ current = current.Next } if n == length { // 要删除的是头结点,特殊处理 return head.Next } pos := length - n current = head for i := 0; i < pos-1; i++ { current = current.Next } current.Next = current.Next.Next return head } ``` ### 2. 实战扩展:链表操作的通用技巧 #### 技巧1:快慢指针的应用 - **找中点:** fast 移动 2 步,slow 移动 1 步 - **找倒数第 k 个:** fast 先移动 k 步 - **检测环:** fast 移动 2 步,slow 移动 1 步 ```go // 找链表中点 func findMiddle(head *ListNode) *ListNode { slow, fast := head, head for fast != nil && fast.Next != nil { slow = slow.Next fast = fast.Next.Next } return slow } // 检测环 func hasCycle(head *ListNode) bool { slow, fast := head, head for fast != nil && fast.Next != nil { slow = slow.Next fast = fast.Next.Next if slow == fast { return true } } return false } ``` #### 技巧2:虚拟头结点的使用 - **统一操作:** 避免边界条件判断 - **简化代码:** 使删除、插入操作更简洁 - **常见场景:** 删除操作、插入操作 ### 3. 变形题目 #### 变形1:删除链表中的重复元素 **LeetCode 83:** 删除排序链表中的重复元素,使得每个元素只出现一次。 ```go func deleteDuplicates(head *ListNode) *ListNode { if head == nil { return nil } current := head for current.Next != nil { if current.Val == current.Next.Val { current.Next = current.Next.Next } else { current = current.Next } } return head } ``` #### 变形2:删除链表中的所有重复元素 **LeetCode 82:** 删除排序链表中所有重复的元素,只保留原始链表中没有重复出现的数字。 ```go func deleteDuplicatesAll(head *ListNode) *ListNode { dummy := &ListNode{0, head} prev := dummy for prev.Next != nil { curr := prev.Next // 检查是否有重复 if curr.Next != nil && curr.Val == curr.Next.Val { // 跳过所有重复的值 val := curr.Val for curr != nil && curr.Val == val { curr = curr.Next } prev.Next = curr } else { prev = prev.Next } } return dummy.Next } ``` #### 变形3:旋转链表 **LeetCode 61:** 将链表每个节点向右移动 k 个位置。 ```go func rotateRight(head *ListNode, k int) *ListNode { if head == nil || k == 0 { return head } // 计算链表长度并连接成环 length := 1 tail := head for tail.Next != nil { tail = tail.Next length++ } tail.Next = head // 计算新的尾结点位置 k = k % length stepsToNewTail := length - k newTail := head for i := 1; i < stepsToNewTail; i++ { newTail = newTail.Next } newHead := newTail.Next newTail.Next = nil return newHead } ``` ### 4. 优化技巧 #### 优化1:一次遍历删除多个结点 如果需要删除多个位置的结点,可以在一次遍历中完成。 ```go func removeNodes(head *ListNode, positions []int) *ListNode { dummy := &ListNode{0, head} posMap := make(map[int]bool) for _, pos := range positions { posMap[pos] = true } prev := dummy curr := head index := 1 for curr != nil { if posMap[index] { prev.Next = curr.Next } else { prev = curr } curr = curr.Next index++ } return dummy.Next } ``` #### 优化2:递归解法(优雅但可能栈溢出) ```go func removeNthFromEndRecursive(head *ListNode, n int) *ListNode { counter := 0 return removeHelper(head, &counter, n) } func removeHelper(node *ListNode, counter *int, n int) *ListNode { if node == nil { return nil } node.Next = removeHelper(node.Next, counter, n) *counter++ if *counter == n { return node.Next } return node } ``` ### 5. 实际应用场景 - **LRU 缓存:** 删除最近最少使用的数据 - **浏览器历史记录:** 删除特定位置的历史记录 - **文本编辑器:** 撤销操作(删除最近的修改) - **任务队列:** 删除超时或取消的任务 ### 6. 面试技巧 **面试官可能会问:** 1. "为什么选择双指针法而不是计算长度法?" 2. "如果链表很长,递归解法会有什么问题?" 3. "如何证明你的算法是正确的?" **回答要点:** 1. 双指针法只需一次遍历,代码简洁,空间复杂度低 2. 递归可能导致栈溢出,对于长链表不推荐 3. 可以通过画图、举例、边界条件分析来证明正确性 ### 7. 相关题目推荐 - LeetCode 19: 删除链表的倒数第 N 个结点(本题) - LeetCode 61: 旋转链表 - LeetCode 83: 删除排序链表中的重复元素 - LeetCode 82: 删除排序链表中的所有重复元素 - LeetCode 206: 反转链表 - LeetCode 142: 环形链表 II