692 lines
16 KiB
Markdown
692 lines
16 KiB
Markdown
# 删除链表的倒数第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
|