- Remove all "## Java 解法" sections and Java code blocks - Replace "## Go 解法" with "## 解法" - Remove "### Go 代码要点" and "### Java 代码要点" sections - Keep all Go code sections intact - Maintain complete documentation structure and content - Update 22 markdown files in the LeetCode Hot 100 directory
11 KiB
11 KiB
删除链表的倒数第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 <= 300 <= Node.val <= 1001 <= n <= sz
进阶
你能尝试使用一趟扫描实现吗?
解题思路
方法一:双指针法(推荐)
**核心思想:**使用两个指针 fast 和 slow,fast 先移动 n 步,然后 fast 和 slow 一起移动,直到 fast 到达链表末尾。此时 slow 指向要删除结点的前一个结点。
算法步骤:
- 创建哑结点
dummy,指向链表头 - 初始化
fast和slow指针都指向dummy fast先移动n + 1步fast和slow同时移动,直到fast为nil- 此时
slow.next就是要删除的结点,执行slow.next = slow.next.next - 返回
dummy.next
为什么移动 n + 1 步?
- 这样
slow最终会停在要删除结点的前一个结点 - 方便删除操作:
slow.next = slow.next.next
方法二:计算长度法
**核心思想:**先遍历链表计算长度,然后计算要删除的正数位置,再遍历到该位置删除结点。
算法步骤:
- 遍历链表,计算长度
length - 要删除的正数位置为
length - n - 创建哑结点
dummy,指向链表头 - 遍历到第
length - n - 1个结点 - 删除下一个结点
- 返回
dummy.next
方法三:栈法
**核心思想:**将所有结点压入栈中,然后弹出 n 个结点,栈顶就是要删除结点的前一个结点。
算法步骤:
- 创建哑结点
dummy - 将所有结点压入栈
- 弹出
n个结点 - 栈顶结点的
next指向要删除结点的next - 返回
dummy.next
解法
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 实现(计算长度法)
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 实现(栈法)
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: 如果链表是循环链表,应该如何处理?
方法:检测循环,计算长度,然后调整删除位置
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 个结点
func removeFirstNNodes(head *ListNode, n int) *ListNode {
for i := 0; i < n && head != nil; i++ {
head = head.Next
}
return head
}
Q3: 如果链表很长,如何优化内存使用?
方法:
- 使用固定大小的滑动窗口
- 避免存储整个链表
- 使用递归(但会增加栈空间)
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. 深度理解:为什么需要哑结点?
关键点:
- 处理删除头结点的特殊情况
- 统一处理逻辑,减少边界条件判断
- 简化代码,提高可读性
哑结点的作用:
// 没有哑结点的情况
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. 实战扩展:链表操作的通用技巧
技巧总结:
- 使用哑结点简化边界处理
- 双指针技巧:快慢指针、前后指针
- 递归处理链表问题
- 栈辅助解决链表问题
通用模板:
func solveLinkedListProblem(head *ListNode) *ListNode {
dummy := &ListNode{0, head}
// ... 使用双指针或其他技巧
return dummy.Next
}
3. 变形题目
- 19. 删除链表的倒数第N个节点 - 原题
- 82. 删除排序链表中的重复元素 II - 删除所有重复元素
- 83. 删除排序链表中的重复元素 - 删除重复元素保留一个
- 203. 移除链表元素 - 删除指定值的节点
4. 优化技巧
空间优化:
- 原地操作,不使用额外空间
- 递归改为迭代
时间优化:
- 一次遍历完成
- 提前终止条件
代码优化:
- 合并重复逻辑
- 减少不必要的变量
5. 实际应用场景
应用场景:
- 缓存淘汰策略(LRU)
- 音乐播放列表管理
- 浏览器历史记录
- 撤销/重做功能
面试问题:
- 如何处理并发访问的链表?
- 如何实现线程安全的链表操作?
6. 面试技巧
常见面试问题:
- 时间/空间复杂度分析
- 边界条件处理
- 优化思路
- 相关题目变体
回答技巧:
- 先给出暴力解法
- 逐步优化
- 说明权衡取舍
7. 相关题目推荐
相关题目:
总结
这道题的核心是:
- 双指针法:一次遍历,快慢指针配合
- 边界处理:使用哑结点简化删除头结点的处理
- 多种解法:双指针、计算长度、栈法各有优劣
易错点:
- 忘记处理删除头结点的情况
- 快指针移动步数错误(应该是 n+1)
- 空链表的特殊情况处理
- 循环链表的特殊情况
最优解法:双指针法,时间 O(L),空间 O(1)