Files
leetcode-go/笔试/微派 Test1/02-删除链表倒数第N个节点.md
T

6.3 KiB
Raw Blame History

tags, create time
tags create time
笔试
微派
链表
双指针
Go
2026-05-16 14:40

02 - 删除链表倒数第 n 个节点

题面

输入:

  1. 一个单向链表的头结点 head
  2. 整数 n(1 ≤ n ≤ 链表长度)

输出: 删除倒数第 n 个节点后的链表头结点

要求:

  • ACM 模式:从 stdin 读取,stdout 输出结果
  • 一次遍历完成(O(1) 额外空间)
  • 被删除节点一定存在

示例 1:

输入: head = [1, 2, 3, 4, 5], n = 2
输出: [1, 2, 3, 5]
解释: 倒数第 2 个是 4,删除后得到 1->2->3->5

示例 2:

输入: head = [1], n = 1
输出: []
解释: 只有一个节点,删除它后链表为空

示例 3:

输入: head = [1, 2], n = 1
输出: [1]
解释: 倒数第 1 个(即尾节点 2),删除后剩下 1

[!question] 💡 思考一下 如果题目说"只允许遍历一次",为什么不能先走一遍求长度再走一遍删除?


思路

方法一:快慢指针(双指针)⭐ 最优

核心思想

flowchart LR
    A["dummy -> 1 -> 2 -> 3 -> 4 -> 5<br/>n = 2"] --> B["fast 和 slow 都从 dummy 出发"]
    B --> C["第一步: fast 先走 n+1 = 3 步"]
    C --> D["此时 fast=dummy, slow=dummy<br/>fast 在空位, slow 在 dummy"]
    D --> E["第二步: 一起走直到 fast == nil"]
    E --> F["slow 停在 3 的位置"]
    F --> G["slow.Next = slow.Next.Next<br/>跳过节点 4"]
    G --> H["删除完成: 1->2->3->5"]

关键步骤

  1. 创建虚拟头结点 dummy,指向 head

    • 为什么要 dummy?因为可能删除头结点本身(如示例 2),用 dummy 可以避免特殊判断。
  2. 快指针 fast 先走 n+1 步

    • 走到 n+1 而不是 n,是为了让慢指针最终停在要删除节点的前驱。
  3. 快慢指针同步前进,直到 fast == nil

  4. 执行删除: slow.Next = slow.Next.Next

为什么是 n+1 步?

链表: dummy → 1 → 2 → 3 → 4 → 5
                          ↑
                    删除倒数第 2 个(节点 4)
                    
fast 先走 3 步 (n+1):
dummy → 1 → 2 → 3 → 4 → 5
^       ^               ^
dummy   dummy(fast)     ...继续...

不对,让我重新演示:
dummy → 1 → 2 → 3 → 4 → 5 → nil
^
fast 走1步 → 1
fast 走2步 → 2  
fast 走3步 → 3 (n+1=3 步)

现在 fast/slow 同时走:
slow→1, fast→4         (step 1)
slow→2, fast→5         (step 2)
slow→3, fast→nil       (step 3, 停止)

slow 停在 3,slow.Next = 4(要删除的节点)✓

[!tip] 规律总结

  • 快指针先走 n+1 步
  • 当快指针到达 nil 时,慢指针恰好停在被删节点的前驱
  • 时间复杂度 O(n),空间复杂度 O(1)

方法二:两次遍历(备选)

虽然题目通常要求一次遍历,但了解这个方法有助于面试中展示全面性:

  1. 第一次遍历计算链表长度 L
  2. 第二次遍历走到正数第 (L-n) 个节点,执行删除

时间 O(n),空间 O(1),但需要两次遍历。


方法三:栈(备选)

  1. 将所有节点入栈
  2. 弹出 n 次,第 n 次弹出的就是要删除的节点
  3. 栈顶元素即为前驱节点,执行删除

时间 O(n),空间 O(n)(不满足 O(1) 空间要求)。


代码提示

// 1. 定义 ListNode 结构体
// 2. 创建 dummy := &ListNode{Next: head}
// 3. fast, slow := dummy, dummy
// 4. for i := 0; i <= n; i++ { fast = fast.Next }  // 先走 n+1 步
// 5. for fast != nil { fast = fast.Next; slow = slow.Next }
// 6. slow.Next = slow.Next.Next  // 删除
// 7. return dummy.Next

技巧

[!note] ACM 模式的辅助函数 在 ACM 模式下,通常需要写 helper 函数来将数组转为链表,以及将链表输出为数组格式。这些虽然不是算法核心,但必不可少。

// 常用模板:数组转链表
func arrToList(arr []int) *ListNode {
    dummy := &ListNode{}
    cur := dummy
    for _, v := range arr {
        cur.Next = &ListNode{Val: v}
        cur = cur.Next
    }
    return dummy.Next
}

// 常用模板:链表转数组
func listToArray(head *ListNode) []int {
    var result []int
    for head != nil {
        result = append(result, head.Val)
        head = head.Next
    }
    return result
}

[!warning] ⚠️ 边界检查 删除首节点的情况由 dummy 自动处理,不需要 if head == nil 判断。但要注意 n 超过链表长度的情况——题目保证合法时可跳过。


代码

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

// ListNode 单链表节点定义
type ListNode struct {
	Val  int
	Next *ListNode
}

func main() {
	scanner := bufio.NewScanner(os.Stdin)

	// 第一行: 数组形式的链表 "1 2 3 4 5"
	if !scanner.Scan() {
		return
	}
	parts := strings.Fields(scanner.Text())
	n := len(parts)
	
	// 第二行: 目标 n
	if !scanner.Scan() {
		return
	}
	removeN, _ := strconv.Atoi(strings.TrimSpace(scanner.Text()))

	// 数组转链表
	head := arrToList(parts)
	
	// 删除倒数第 n 个节点
	newHead := removeNthFromEnd(head, removeN)
	
	// 输出: 逗号分隔或空格分隔均可
	output := listToArray(newHead)
	if len(output) == 0 {
		fmt.Println("[]")
	} else {
		fmt.Print("[")
		for i, v := range output {
			if i > 0 {
				fmt.Print(" ")
			}
			fmt.Print(v)
		}
		fmt.Println("]")
	}
}

// removeNthFromEnd 一次遍历删除倒数第 n 个节点
func removeNthFromEnd(head *ListNode, n int) *ListNode {
	// 创建虚拟头结点,简化头节点删除的处理
	dummy := &ListNode{Next: head}
	fast, slow := dummy, dummy

	// 快指针先走 n+1 步
	for i := 0; i <= n; i++ {
		fast = fast.Next
	}

	// 快慢指针同步前进,直到 fast 到达末尾
	for fast != nil {
		fast = fast.Next
		slow = slow.Next
	}

	// slow 现在停在要删除节点的前驱
	slow.Next = slow.Next.Next

	return dummy.Next
}

// arrToList 将字符串数组转换为链表
func arrToList(parts []string) *ListNode {
	dummy := &ListNode{}
	cur := dummy
	for _, p := range parts {
		v, _ := strconv.Atoi(p)
		cur.Next = &ListNode{Val: v}
		cur = cur.Next
	}
	return dummy.Next
}

// listToArray 将链表转换为切片
func listToArray(head *ListNode) []int {
	var result []int
	for head != nil {
		result = append(result, head.Val)
		head = head.Next
	}
	return result
}