Files
interview/16-LeetCode Hot 100/删除链表的倒数第N个结点.md

692 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 删除链表的倒数第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<ListNode> 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