Files
interview/16-LeetCode Hot 100/删除链表的倒数第N个结点.md
yasinshaw 15dbd75004 refactor: remove Java code sections from all LeetCode Hot 100 markdown files
- 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
2026-03-05 12:31:48 +08:00

11 KiB
Raw Blame History

删除链表的倒数第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

进阶

你能尝试使用一趟扫描实现吗?

解题思路

方法一:双指针法(推荐)

**核心思想:**使用两个指针 fastslowfast 先移动 n 步,然后 fastslow 一起移动,直到 fast 到达链表末尾。此时 slow 指向要删除结点的前一个结点。

算法步骤:

  1. 创建哑结点 dummy,指向链表头
  2. 初始化 fastslow 指针都指向 dummy
  3. fast 先移动 n + 1
  4. fastslow 同时移动,直到 fastnil
  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 实现(双指针法)

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: 如果链表很长,如何优化内存使用?

方法

  1. 使用固定大小的滑动窗口
  2. 避免存储整个链表
  3. 使用递归(但会增加栈空间)
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. 变形题目

  1. 19. 删除链表的倒数第N个节点 - 原题
  2. 82. 删除排序链表中的重复元素 II - 删除所有重复元素
  3. 83. 删除排序链表中的重复元素 - 删除重复元素保留一个
  4. 203. 移除链表元素 - 删除指定值的节点

4. 优化技巧

空间优化:

  • 原地操作,不使用额外空间
  • 递归改为迭代

时间优化:

  • 一次遍历完成
  • 提前终止条件

代码优化:

  • 合并重复逻辑
  • 减少不必要的变量

5. 实际应用场景

应用场景:

  • 缓存淘汰策略LRU
  • 音乐播放列表管理
  • 浏览器历史记录
  • 撤销/重做功能

面试问题:

  • 如何处理并发访问的链表?
  • 如何实现线程安全的链表操作?

6. 面试技巧

常见面试问题:

  1. 时间/空间复杂度分析
  2. 边界条件处理
  3. 优化思路
  4. 相关题目变体

回答技巧:

  • 先给出暴力解法
  • 逐步优化
  • 说明权衡取舍

7. 相关题目推荐

相关题目:

  1. 206. 反转链表
  2. 21. 合并两个有序链表
  3. 141. 环形链表
  4. 142. 环形链表 II

总结

这道题的核心是:

  1. 双指针法:一次遍历,快慢指针配合
  2. 边界处理:使用哑结点简化删除头结点的处理
  3. 多种解法:双指针、计算长度、栈法各有优劣

易错点

  • 忘记处理删除头结点的情况
  • 快指针移动步数错误(应该是 n+1
  • 空链表的特殊情况处理
  • 循环链表的特殊情况

最优解法:双指针法,时间 O(L),空间 O(1)