跳转至

十大经典链表题(Go · ACM 模式)

💡 一句话概述

精选 10 道由易到难的 LeetCode 经典链表题,每道题重点讲解**核心思路、指针操作和边界处理**,并给出完整的 Go ACM 模式(可编译运行的含 main 输入输出)题解。


🔑 核心概念

  1. 虚拟头节点(dummy node) — 链表操作的第一法宝:在头节点前加一个哨兵节点,统一处理头节点被删除/替换的情况,避免大量 if 判断
  2. 快慢指针 — 链表操作的第二法宝:两个指针以不同速度遍历,常用于找中间节点、检测环、找倒数第 k 个节点
  3. 原地修改 vs 新建链表 — 明确题目要求:是修改原链表的 Next 指针,还是创建新节点组成新链表
  4. 指针断链前先保存 — 修改 Next 指针前,务必用临时变量保存原来的 Next,否则链表一断就找不回来了
  5. 递归 vs 迭代 — 链表天然具有递归结构(头 + 剩余链表),很多问题递归写法更简洁,但要注意栈深度

📝 详细说明

链表节点定义

本文所有题目使用以下链表节点定义:

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

链表操作通用技巧

技巧 适用场景 说明
dummy node 头节点可能变化 返回 dummy.Next 即可
快慢指针 中间节点、倒数第 k 个 快指针先走 k 步,或快指针速度是慢指针两倍
三指针(prev, curr, next) 反转、插入、删除 保存三个状态,防止断链
哈希表辅助 检测环、相交 空间换时间
递归 反转、合并、回文 利用链表递归结构

第 1 题:反转链表

题目

给你单链表的头节点 head,请你反转链表,并返回反转后的链表头。(LeetCode 206)

思路与操作

这是链表最基础的操作,核心是**三指针迭代**:prev(已反转部分的头)、curr(当前节点)、next(暂存下一个节点)。

  • 初始:prev = nil,curr = head
  • 每次循环:保存 next = curr.Next,将 curr.Next 指向 prev,然后 prev = curr,curr = next
  • 结束条件:curr == nil,此时 prev 就是新的头节点
graph LR
    subgraph 初始状态
        A["1"] --> B["2"] --> C["3"] --> D["4"] --> E["5"]
    end
    subgraph 反转后
        E2["5"] --> D2["4"] --> C2["3"] --> B2["2"] --> A2["1"]
    end

ACM 题解(Go)

package main

import (
    "bufio"
    "fmt"
    "os"
)

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

// 反转链表 - 迭代法
func reverseList(head *ListNode) *ListNode {
    var prev *ListNode
    curr := head
    for curr != nil {
        next := curr.Next // 暂存下一个节点
        curr.Next = prev  // 当前节点指向前一个节点
        prev = curr       // prev 前进
        curr = next       // curr 前进
    }
    return prev
}

func main() {
    var n int
    fmt.Scan(&n)
    sc := bufio.NewScanner(os.Stdin)
    sc.Split(bufio.ScanWords)

    // 构建链表
    dummy := &ListNode{}
    curr := dummy
    for i := 0; i < n; i++ {
        sc.Scan()
        var val int
        fmt.Sscan(sc.Text(), &val)
        curr.Next = &ListNode{Val: val}
        curr = curr.Next
    }

    // 反转链表
    head := reverseList(dummy.Next)

    // 输出结果
    for head != nil {
        if head.Next != nil {
            fmt.Printf("%d ", head.Val)
        } else {
            fmt.Printf("%d", head.Val)
        }
        head = head.Next
    }
    fmt.Println()
}

递归解法

递归解法更简洁,但要注意递归深度。func reverseList(head *ListNode) *ListNode { if head == nil || head.Next == nil { return head } newHead := reverseList(head.Next) head.Next.Next = head; head.Next = nil; return newHead }


第 2 题:检测环形链表

题目

给你一个链表的头节点 head,判断链表中是否有环。如果链表中存在环,则返回 true;否则返回 false。(LeetCode 141)

思路与操作

快慢指针法:快指针每次走 2 步,慢指针每次走 1 步。如果链表有环,快慢指针一定会在环内相遇(就像在操场跑步,跑得快的人一定会追上跑得慢的人)。

  • 慢指针 slow:每次走 1 步
  • 快指针 fast:每次走 2 步
  • 如果 fast 或 fast.Next 变成 nil,说明无环
  • 如果 fast == slow,说明有环
graph LR
    subgraph 有环链表
        A["1"] --> B["2"] --> C["3"] --> D["4"] --> E["5"]
        E --> C
    end

ACM 题解(Go)

package main

import (
    "bufio"
    "fmt"
    "os"
)

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

// 检测环形链表 - 快慢指针
func hasCycle(head *ListNode) bool {
    if head == nil || head.Next == nil {
        return false
    }
    slow := head      // 慢指针:每次走 1 步
    fast := head.Next // 快指针:每次走 2 步

    for slow != fast {
        if fast == nil || fast.Next == nil {
            return false // 快指针到头了,说明无环
        }
        slow = slow.Next
        fast = fast.Next.Next
    }
    return true // 快慢指针相遇,有环
}

func main() {
    var n, pos int
    fmt.Scan(&n, &n, &pos) // n 为节点数,pos 为环入口位置(-1 表示无环)

    sc := bufio.NewScanner(os.Stdin)
    sc.Split(bufio.ScanWords)

    // 构建链表
    dummy := &ListNode{}
    curr := dummy
    nodes := make([]*ListNode, n)
    for i := 0; i < n; i++ {
        sc.Scan()
        var val int
        fmt.Sscan(sc.Text(), &val)
        curr.Next = &ListNode{Val: val}
        curr = curr.Next
        nodes[i] = curr
    }

    // 创建环(如果 pos != -1)
    if pos != -1 && pos < n {
        nodes[n-1].Next = nodes[pos]
    }

    fmt.Println(hasCycle(dummy.Next))
}

第 3 题:合并两个有序链表

题目

将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。(LeetCode 21)

思路与操作

双指针比较法:用两个指针分别遍历两个链表,每次选择较小的节点接到结果链表后面。

  • 创建 dummy 节点作为结果链表的头
  • 比较 l1.Val 和 l2.Val,将较小的节点接到 curr 后面
  • 当一个链表遍历完,直接将另一个链表剩余部分接到 curr 后面
graph TD
    subgraph 输入
        A1["1"] --> A2["2"] --> A3["4"]
        B1["1"] --> B2["3"] --> B3["4"]
    end
    subgraph 输出
        C1["1"] --> C2["1"] --> C3["2"] --> C4["3"] --> C5["4"] --> C6["4"]
    end

ACM 题解(Go)

package main

import (
    "bufio"
    "fmt"
    "os"
)

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

// 合并两个有序链表
func mergeTwoLists(l1, l2 *ListNode) *ListNode {
    dummy := &ListNode{} // 虚拟头节点
    curr := dummy

    for l1 != nil && l2 != nil {
        if l1.Val <= l2.Val {
            curr.Next = l1
            l1 = l1.Next
        } else {
            curr.Next = l2
            l2 = l2.Next
        }
        curr = curr.Next
    }

    // 接上剩余部分
    if l1 != nil {
        curr.Next = l1
    }
    if l2 != nil {
        curr.Next = l2
    }

    return dummy.Next
}

func main() {
    sc := bufio.NewScanner(os.Stdin)
    sc.Split(bufio.ScanWords)

    // 读取第一个链表
    sc.Scan()
    var n1 int
    fmt.Sscan(sc.Text(), &n1)
    dummy1 := &ListNode{}
    curr1 := dummy1
    for i := 0; i < n1; i++ {
        sc.Scan()
        var val int
        fmt.Sscan(sc.Text(), &val)
        curr1.Next = &ListNode{Val: val}
        curr1 = curr1.Next
    }

    // 读取第二个链表
    sc.Scan()
    var n2 int
    fmt.Sscan(sc.Text(), &n2)
    dummy2 := &ListNode{}
    curr2 := dummy2
    for i := 0; i < n2; i++ {
        sc.Scan()
        var val int
        fmt.Sscan(sc.Text(), &val)
        curr2.Next = &ListNode{Val: val}
        curr2 = curr2.Next
    }

    // 合并并输出
    head := mergeTwoLists(dummy1.Next, dummy2.Next)
    for head != nil {
        if head.Next != nil {
            fmt.Printf("%d ", head.Val)
        } else {
            fmt.Printf("%d", head.Val)
        }
        head = head.Next
    }
    fmt.Println()
}

第 4 题:删除链表倒数第 N 个节点

题目

给你一个链表,删除链表的倒数第 n 个节点,并且返回链表的头节点。(LeetCode 19)

思路与操作

快慢指针法:让快指针先走 n 步,然后快慢指针一起走,当快指针到达末尾时,慢指针正好指向倒数第 n 个节点的前一个节点。

  • 快指针 fast 先走 n 步
  • 快慢指针一起走,直到 fast.Next == nil
  • 此时 slow.Next 就是倒数第 n 个节点,执行删除操作
graph LR
    subgraph 示例
        A["1"] --> B["2"] --> C["3"] --> D["4"] --> E["5"]
    end
    subgraph 删除倒数第2个
        A2["1"] --> B2["2"] --> C2["3"] --> E2["5"]
    end

ACM 题解(Go)

package main

import (
    "bufio"
    "fmt"
    "os"
)

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

// 删除链表倒数第N个节点
func removeNthFromEnd(head *ListNode, n int) *ListNode {
    dummy := &ListNode{Next: head} // 虚拟头节点,处理删除头节点的情况
    fast := dummy
    slow := dummy

    // 快指针先走 n+1 步(因为要找到倒数第 n 个的前一个)
    for i := 0; i <= n; i++ {
        fast = fast.Next
    }

    // 快慢指针一起走
    for fast != nil {
        fast = fast.Next
        slow = slow.Next
    }

    // 删除 slow.Next 节点
    slow.Next = slow.Next.Next

    return dummy.Next
}

func main() {
    var n, k int
    fmt.Scan(&n)
    sc := bufio.NewScanner(os.Stdin)
    sc.Split(bufio.ScanWords)

    // 构建链表
    dummy := &ListNode{}
    curr := dummy
    for i := 0; i < n; i++ {
        sc.Scan()
        var val int
        fmt.Sscan(sc.Text(), &val)
        curr.Next = &ListNode{Val: val}
        curr = curr.Next
    }
    fmt.Scan(&k) // 读取要删除的倒数第 k 个

    // 删除并输出
    head := removeNthFromEnd(dummy.Next, k)
    for head != nil {
        if head.Next != nil {
            fmt.Printf("%d ", head.Val)
        } else {
            fmt.Printf("%d", head.Val)
        }
        head = head.Next
    }
    fmt.Println()
}

第 5 题:链表的中间节点

题目

给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。(LeetCode 876)

思路与操作

快慢指针法:快指针每次走 2 步,慢指针每次走 1 步。当快指针到达末尾时,慢指针正好在中间。

  • 快指针 fast:每次走 2 步
  • 慢指针 slow:每次走 1 步
  • 当 fast == nil || fast.Next == nil 时,slow 就是中间节点
graph LR
    subgraph 奇数个节点
        A["1"] --> B["2"] --> C["3"] --> D["4"] --> E["5"]
    end
    subgraph 中间节点
        C2["3"]
    end

ACM 题解(Go)

package main

import (
    "bufio"
    "fmt"
    "os"
)

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

// 链表的中间节点
func middleNode(head *ListNode) *ListNode {
    slow := head // 慢指针:每次走 1 步
    fast := head // 快指针:每次走 2 步

    for fast != nil && fast.Next != nil {
        slow = slow.Next
        fast = fast.Next.Next
    }

    return slow
}

func main() {
    var n int
    fmt.Scan(&n)
    sc := bufio.NewScanner(os.Stdin)
    sc.Split(bufio.ScanWords)

    // 构建链表
    dummy := &ListNode{}
    curr := dummy
    for i := 0; i < n; i++ {
        sc.Scan()
        var val int
        fmt.Sscan(sc.Text(), &val)
        curr.Next = &ListNode{Val: val}
        curr = curr.Next
    }

    // 找中间节点并输出
    mid := middleNode(dummy.Next)
    fmt.Println(mid.Val)
}

第 6 题:相交链表

题目

给你两个单链表的头节点 headA 和 headB,请你找出并返回两个单链表相交的起始节点。如果不存在相交节点,返回 null。(LeetCode 160)

思路与操作

双指针法:让两个指针分别从两个链表头开始走,当走到末尾时切换到另一个链表的头。这样两个指针走过的总路程相等,如果有交点,一定会在交点相遇。

  • 指针 pA 从 headA 开始,指针 pB 从 headB 开始
  • 当 pA 走到末尾,切换到 headB;当 pB 走到末尾,切换到 headA
  • 如果有交点,pA 和 pB 会在交点相遇;如果无交点,最终都会变成 nil
graph LR
    subgraph 链表A
        A1["4"] --> A2["1"]
    end
    subgraph 链表B
        B1["5"] --> B2["0"] --> B3["1"]
    end
    subgraph 公共部分
        C1["8"] --> C2["4"] --> C3["5"]
    end
    A2 --> C1
    B3 --> C1

ACM 题解(Go)

package main

import (
    "bufio"
    "fmt"
    "os"
)

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

// 相交链表
func getIntersectionNode(headA, headB *ListNode) *ListNode {
    if headA == nil || headB == nil {
        return nil
    }

    pA := headA
    pB := headB

    for pA != pB {
        if pA == nil {
            pA = headB // 走到末尾,切换到 headB
        } else {
            pA = pA.Next
        }
        if pB == nil {
            pB = headA // 走到末尾,切换到 headA
        } else {
            pB = pB.Next
        }
    }

    return pA
}

func main() {
    // 读取两个链表和相交点
    // 为了简化输入,这里假设输入格式为:
    // 链表A长度 链表B长度 相交点索引(-1表示不相交)
    var nA, nB, intersectIdx int
    fmt.Scan(&nA, &nB, &intersectIdx)

    sc := bufio.NewScanner(os.Stdin)
    sc.Split(bufio.ScanWords)

    // 构建公共部分(如果相交)
    var intersectNode *ListNode
    if intersectIdx >= 0 {
        // 读取相交部分
        var commonLen int
        fmt.Scan(&commonLen)
        dummy := &ListNode{}
        curr := dummy
        nodes := make([]*ListNode, commonLen)
        for i := 0; i < commonLen; i++ {
            sc.Scan()
            var val int
            fmt.Sscan(sc.Text(), &val)
            curr.Next = &ListNode{Val: val}
            curr = curr.Next
            nodes[i] = curr
        }
        intersectNode = nodes[intersectIdx]
    }

    // 构建链表A
    dummyA := &ListNode{}
    currA := dummyA
    for i := 0; i < nA; i++ {
        sc.Scan()
        var val int
        fmt.Sscan(sc.Text(), &val)
        currA.Next = &ListNode{Val: val}
        currA = currA.Next
    }
    if intersectNode != nil {
        currA.Next = intersectNode
    }

    // 构建链表B
    dummyB := &ListNode{}
    currB := dummyB
    for i := 0; i < nB; i++ {
        sc.Scan()
        var val int
        fmt.Sscan(sc.Text(), &val)
        currB.Next = &ListNode{Val: val}
        currB = currB.Next
    }
    if intersectNode != nil {
        currB.Next = intersectNode
    }

    // 查找交点
    result := getIntersectionNode(dummyA.Next, dummyB.Next)
    if result != nil {
        fmt.Println(result.Val)
    } else {
        fmt.Println("null")
    }
}

第 7 题:回文链表

题目

给你一个单链表的头节点 head,请你判断该链表是否为回文链表。如果是,返回 true;否则,返回 false。(LeetCode 234)

思路与操作

快慢指针 + 反转后半部分:找到中间节点,反转后半部分链表,然后比较前半部分和后半部分。

  • 快慢指针找中间节点
  • 反转后半部分链表
  • 比较前半部分和反转后的后半部分
  • 恢复链表(可选)
graph LR
    subgraph 原始链表
        A["1"] --> B["2"] --> C["3"] --> D["2"] --> E["1"]
    end
    subgraph 反转后半部分
        A2["1"] --> B2["2"] --> C2["3"] --> D2["2"] --> E2["1"]
    end

ACM 题解(Go)

package main

import (
    "bufio"
    "fmt"
    "os"
)

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

// 反转链表
func reverseList(head *ListNode) *ListNode {
    var prev *ListNode
    curr := head
    for curr != nil {
        next := curr.Next
        curr.Next = prev
        prev = curr
        curr = next
    }
    return prev
}

// 找中间节点
func findMiddle(head *ListNode) *ListNode {
    slow := head
    fast := head
    for fast != nil && fast.Next != nil {
        slow = slow.Next
        fast = fast.Next.Next
    }
    return slow
}

// 回文链表
func isPalindrome(head *ListNode) bool {
    if head == nil || head.Next == nil {
        return true
    }

    // 1. 找中间节点
    mid := findMiddle(head)

    // 2. 反转后半部分
    secondHalf := reverseList(mid)

    // 3. 比较前半部分和后半部分
    p1 := head
    p2 := secondHalf
    result := true
    for p2 != nil {
        if p1.Val != p2.Val {
            result = false
            break
        }
        p1 = p1.Next
        p2 = p2.Next
    }

    // 4. 恢复链表(可选,但面试中可能加分)
    reverseList(secondHalf)

    return result
}

func main() {
    var n int
    fmt.Scan(&n)
    sc := bufio.NewScanner(os.Stdin)
    sc.Split(bufio.ScanWords)

    // 构建链表
    dummy := &ListNode{}
    curr := dummy
    for i := 0; i < n; i++ {
        sc.Scan()
        var val int
        fmt.Sscan(sc.Text(), &val)
        curr.Next = &ListNode{Val: val}
        curr = curr.Next
    }

    fmt.Println(isPalindrome(dummy.Next))
}

第 8 题:环形链表 II(找环入口)

!!! note "题目** 给定一个链表的头节点 head,返回链表开始入环的第一个节点。如果链表无环,则返回 null。(LeetCode 142)

思路与操作

快慢指针 + 数学推导:

设: - a = 头节点到环入口的距离 - b = 环入口到相遇点的距离 - c = 环的长度 - b

快指针走过的距离:a + b + c 慢指针走过的距离:a + b

因为快指针速度是慢指针的 2 倍,所以:2(a + b) = a + b + c → a = c

这意味着:从头节点出发走 a 步,从相遇点出发走 c 步,两者会在环入口相遇。

graph TD
    A["头节点"] --> B["环入口"]
    B --> C["相遇点"]
    C --> B

ACM 题解(Go)

package main

import (
    "bufio"
    "fmt"
    "os"
)

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

// 环形链表 II - 找环入口
func detectCycle(head *ListNode) *ListNode {
    if head == nil || head.Next == nil {
        return nil
    }

    // 1. 快慢指针找相遇点
    slow := head
    fast := head
    for fast != nil && fast.Next != nil {
        slow = slow.Next
        fast = fast.Next.Next
        if slow == fast {
            break
        }
    }

    // 2. 如果无环,返回 nil
    if fast == nil || fast.Next == nil {
        return nil
    }

    // 3. 从头节点和相遇点同时出发,相遇点即为环入口
    p1 := head
    p2 := slow // 相遇点
    for p1 != p2 {
        p1 = p1.Next
        p2 = p2.Next
    }

    return p1
}

func main() {
    var n, pos int
    fmt.Scan(&n, &pos)

    sc := bufio.NewScanner(os.Stdin)
    sc.Split(bufio.ScanWords)

    // 构建链表
    dummy := &ListNode{}
    curr := dummy
    nodes := make([]*ListNode, n)
    for i := 0; i < n; i++ {
        sc.Scan()
        var val int
        fmt.Sscan(sc.Text(), &val)
        curr.Next = &ListNode{Val: val}
        curr = curr.Next
        nodes[i] = curr
    }

    // 创建环(如果 pos != -1)
    if pos != -1 && pos < n {
        nodes[n-1].Next = nodes[pos]
    }

    // 查找环入口
    entry := detectCycle(dummy.Next)
    if entry != nil {
        fmt.Println(entry.Val)
    } else {
        fmt.Println("null")
    }
}

第 9 题:两数相加

!!! note "题目** 给你两个非空链表,代表两个非负整数。它们每位数字都是按逆序存储的,每个节点只能存储一位数字。请你将两个数相加,并以相同形式返回一个表示和的链表。(LeetCode 2)

思路与操作

模拟加法:从个位开始逐位相加,处理进位。

  • 两个指针分别遍历两个链表
  • 每次将两个节点的值加上进位,计算当前位和新进位
  • 如果一个链表遍历完,用 0 代替
  • 最后如果还有进位,需要新增一个节点
graph LR
    subgraph 输入
        A1["2"] --> A2["4"] --> A3["3"]
        B1["5"] --> B2["6"] --> B3["4"]
    end
    subgraph 输出
        C1["7"] --> C2["0"] --> C3["8"]
    end

ACM 题解(Go)

package main

import (
    "bufio"
    "fmt"
    "os"
)

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

// 两数相加
func addTwoNumbers(l1, l2 *ListNode) *ListNode {
    dummy := &ListNode{}
    curr := dummy
    carry := 0 // 进位

    for l1 != nil || l2 != nil || carry > 0 {
        sum := carry
        if l1 != nil {
            sum += l1.Val
            l1 = l1.Next
        }
        if l2 != nil {
            sum += l2.Val
            l2 = l2.Next
        }
        carry = sum / 10
        curr.Next = &ListNode{Val: sum % 10}
        curr = curr.Next
    }

    return dummy.Next
}

func main() {
    sc := bufio.NewScanner(os.Stdin)
    sc.Split(bufio.ScanWords)

    // 读取第一个链表
    sc.Scan()
    var n1 int
    fmt.Sscan(sc.Text(), &n1)
    dummy1 := &ListNode{}
    curr1 := dummy1
    for i := 0; i < n1; i++ {
        sc.Scan()
        var val int
        fmt.Sscan(sc.Text(), &val)
        curr1.Next = &ListNode{Val: val}
        curr1 = curr1.Next
    }

    // 读取第二个链表
    sc.Scan()
    var n2 int
    fmt.Sscan(sc.Text(), &n2)
    dummy2 := &ListNode{}
    curr2 := dummy2
    for i := 0; i < n2; i++ {
        sc.Scan()
        var val int
        fmt.Sscan(sc.Text(), &val)
        curr2.Next = &ListNode{Val: val}
        curr2 = curr2.Next
    }

    // 相加并输出
    head := addTwoNumbers(dummy1.Next, dummy2.Next)
    for head != nil {
        if head.Next != nil {
            fmt.Printf("%d ", head.Val)
        } else {
            fmt.Printf("%d", head.Val)
        }
        head = head.Next
    }
    fmt.Println()
}

第 10 题:K 个一组翻转链表

!!! note "题目** 给你链表的头节点 head,每 k 个节点一组进行翻转,请你返回修改后的链表。(LeetCode 25)

思路与操作

分组翻转:每 k 个节点为一组进行翻转,不足 k 个的保持原样。

  • 用一个指针 tail 找到当前组的第 k 个节点
  • 如果不足 k 个,直接返回
  • 翻转当前组的 k 个节点
  • 递归处理剩余部分
  • 连接翻转后的部分和递归处理后的部分
graph LR
    subgraph Input
        A["1"] --> B["2"] --> C["3"] --> D["4"] --> E["5"]
    end
    subgraph "Output k=2"
        A2["2"] --> B2["1"] --> C2["4"] --> D2["3"] --> E2["5"]
    end

ACM 题解(Go)

package main

import (
    "bufio"
    "fmt"
    "os"
)

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

// K个一组翻转链表
func reverseKGroup(head *ListNode, k int) *ListNode {
    if head == nil || k == 1 {
        return head
    }

    // 1. 检查是否有 k 个节点
    tail := head
    for i := 0; i < k; i++ {
        if tail == nil {
            return head // 不足 k 个,保持原样
        }
        tail = tail.Next
    }

    // 2. 翻转前 k 个节点
    var prev *ListNode
    curr := head
    for i := 0; i < k; i++ {
        next := curr.Next
        curr.Next = prev
        prev = curr
        curr = next
    }

    // 3. 递归处理剩余部分,并连接
    head.Next = reverseKGroup(curr, k)

    return prev // prev 是翻转后的头节点
}

func main() {
    var n, k int
    fmt.Scan(&n, &k)
    sc := bufio.NewScanner(os.Stdin)
    sc.Split(bufio.ScanWords)

    // 构建链表
    dummy := &ListNode{}
    curr := dummy
    for i := 0; i < n; i++ {
        sc.Scan()
        var val int
        fmt.Sscan(sc.Text(), &val)
        curr.Next = &ListNode{Val: val}
        curr = curr.Next
    }

    // K个一组翻转并输出
    head := reverseKGroup(dummy.Next, k)
    for head != nil {
        if head.Next != nil {
            fmt.Printf("%d ", head.Val)
        } else {
            fmt.Printf("%d", head.Val)
        }
        head = head.Next
    }
    fmt.Println()
}

⚠️ 常见陷阱

忘记处理空链表或单节点链表

很多链表题目在头节点为空或只有一个节点时需要特殊处理。写代码前先考虑边界情况。

断链前忘记保存 Next 指针

在修改 curr.Next 之前,必须用临时变量保存原来的 Next,否则链表一断就找不回来了。这是链表操作最常见的 bug。

虚拟头节点(dummy)使用不当

使用 dummy 节点时,最后要返回 dummy.Next 而不是 head。因为头节点可能已经改变。

快慢指针初始化错误

  • 检测环时:slow 和 fast 都从 head 开始,但循环条件是 fast != nil && fast.Next != nil
  • 找中间节点时:如果要找第二个中间节点(偶数个节点时),fast 从 head 开始;如果要找第一个,fast 从 head.Next 开始

🏋️ 练习题

练习 1:LeetCode 24 — 两两交换链表节点

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即只能进行节点交换)。

答案

用递归或迭代。迭代:dummy -> 1 -> 2 -> 3 -> 4,交换 1 和 2 时,让 dummy.Next = 2,2.Next = 1,1.Next = 递归处理 3->4。关键是画图理清指针指向。

练习 2:LeetCode 148 — 排序链表

给你链表的头结点 head,请将其按升序排列并返回排序后的链表。要求时间复杂度 O(n log n),空间复杂度 O(1)。

答案

归并排序:找中间节点,递归排序左右两半,合并两个有序链表。时间 O(n log n),空间 O(log n)(递归栈)。如果要求 O(1) 空间,需要用自底向上的迭代归并。

练习 3:LeetCode 23 — 合并 K 个升序链表

给你一个链表数组,每个链表都已按升序排列。请将所有链表合并到一个升序链表中,返回合并后的链表。

答案

方法一:分治法,两两合并,时间 O(N log k),N 是所有节点总数。方法二:最小堆,每次取 k 个链表头节点中最小的,时间 O(N log k)。面试推荐分治法,代码简洁。


🔗 相关链接