如何用递归在 Go 中寻链表尾 K 个节点之根?

更新于
2026-08-20 21:32:34
4阅读来源:SEO资源
  • 内容介绍
  • 文章标签
  • 相关推荐

:为何递归查找链表倒数第 K 个节点会让人头疼?

在实际开发中,常见的痛点包括:

  • 忘记在递归入口检查 nil导致空指针异常。
  • 递归终止条件写得不明确,容易出现无限递归或提前返回。
  • 计数器的传递方式不当,使得最终返回的节点不是期望的倒数第 K 个。
  • 对 Go 中指针传值机制不了解,导致对链表结构的修改未生效。老实说,
如何用递归在 Go 中寻链表尾 K 个节点之根?

问题描述

给定一个单向链表的头指针 head还有正整数 K返回链表中倒数第 K 个节点。如果链表长度小于 K则返回 nil

示例

// 链表: 1 → 2 → 3 → 4 → 5
// K = 2
// 返回值应为节点值 4

链表节点结构体定义

type ListNode struct {
Val int
Next *ListNode
}

递归解法主要思路

思路概述:

  1. 从头结点开始递归遍历至链表末尾。
  2. 每次函数返回时携带当前已经遍历过的节点计数。
  3. 当计数等于 K 时即可确定当前节点为倒数第 K 个。
  4. 利用 Go 的多返回值。把计数和目标节点一起返回,避免全局变量带来的副作用。

递归函数实现

func findKthFromEnd {
// 基础情况:到达链表末尾
if node == nil {
return nil,0
}
// 向后递归,获取子调用的结果和计数
foundNode,cnt := findKthFromEnd
// 当前层级计数加一
cnt++
// 当计数恰好等于 k 时这个 node 就是目标节点
if cnt == k && foundNode == nil {
return node,cnt
}
// 否则继续把子调用得到的结果往上传
return foundNode,cnt
}

从对外包装函数来看。隐藏计数细节

func FindKthToTail *ListNode {
if head == nil || k <= 0 {
        return nil // 参数非法或空链表直接返回
    }
    node, _ := findKthFromEnd
    return node
}

完整示例代码

package main
import (
"fmt"
)
// ---------- 链表结构 ----------
type ListNode struct {
Val int
Next *ListNode
}
// ---------- 递归主要 ----------
func findKthFromEnd {
if node == nil {
return nil,0
}
foundNode,cnt := findKthFromEnd
cnt++
if cnt == k && foundNode == nil {
return node,cnt
}
return foundNode,cnt
}
// ---------- 对外接口 ----------
func FindKthToTail *ListNode {
if head == nil || k <= 0 {
        return nil
    }
    node, _ := findKthFromEnd
   \treturn node
}
// ---------- 辅助函数:构造链表 ----------
func buildList *ListNode {
\tif len == 0 {
\t\treturn nil
\t}
\ad := &ListNode{Val: vals}
\tcur := head
\tfor _, v := range vals {
\t\tcur.Next = &ListNode{Val: v}
\t\tcur = cur.Next
\t}
\treturn head
}
// ---------- 主函数演示 ----------
func main {
\ad := buildList
\tk := 2
\ttarget := FindKthToTail
\tif target != nil {
\t\tfmt.Printf
\t} else {
\t\tfmt.Printf
\t}
}

常见错误与避免策略

  • NIL 检查遗漏:在任何对指针进行解引用前,都必须先判断是否为 nil。说起来,这是防止运行时 panic 的第一步先。
  • 终止条件写错:a)若把 K==0) 当作合法输入,会导致永远无法匹配;b)若忘记在到达末尾时返回 (nil。0)),上层调用将得到错误计数。
  • 全局变量计数:a)使用全局变量会在并发或多次调用时产生竞争;b)本实现,
  • K 超出范围:a)如果 K 大于链表长度,需要在包装函数里直接返回 null);b)上述实现自然满足,因为递归结束后计数永远不到 K。
  • Pointers vs Values:a)Go 中参数是按值传递,但指针本身也是一个值;b)务必传入 **指向**结构体的指针,而不是结构体拷贝。否则对 Next 的修改不会反映到原链表上。

时间与空间复杂度分析

时间复杂度 空间复杂度
递归实现 OO
迭代双指针实现 OO

虽然迭代版拥有更低空间占用,但递归版代码更简洁、易读。若链表极长且担心栈溢出,可改用双指针迭代方案。

Troubleshooting 小贴士

  1. Panic: runtime error: invalid memory address or nil pointer dereference - 检查入口处是否忘记了 If head == nil { return nil }.
  2. K 为负或零 - 在包装函数里统一过滤掉非正整数输入。按理说,
  3. 返回值总是 nil - 确认计数器是否在每层都正确 +1。而且只在第一次达到 K 时返回当前节点,而不是继续覆盖。
  4. Cruel infinite recursion - 确保每次递归都向更深层移动(即传入 ),否则永远停留在同一层导致堆栈溢出。

掌握递归,你也能轻松解决 “倒数第 K 个” 类题目!不过,

看完这篇。你已经了解:

  • 如何安全地检查空指针避免 panic;
  • 如何设计明确且唯一的基准情形;
  • 如何利用多返回值携带计数,从而保持函数纯粹、无副作用;
  • 常见坑点及对应防御措施。

只要牢记「先检查再操作」「确保每层都有进展」这两条原则,你就能自信地使用递归解决差不多问题。祝编码愉快,老实说,

如何用递归在 Go 中寻链表尾 K 个节点之根?

标签:递归

:为何递归查找链表倒数第 K 个节点会让人头疼?

在实际开发中,常见的痛点包括:

  • 忘记在递归入口检查 nil导致空指针异常。
  • 递归终止条件写得不明确,容易出现无限递归或提前返回。
  • 计数器的传递方式不当,使得最终返回的节点不是期望的倒数第 K 个。
  • 对 Go 中指针传值机制不了解,导致对链表结构的修改未生效。老实说,
如何用递归在 Go 中寻链表尾 K 个节点之根?

问题描述

给定一个单向链表的头指针 head还有正整数 K返回链表中倒数第 K 个节点。如果链表长度小于 K则返回 nil

示例

// 链表: 1 → 2 → 3 → 4 → 5
// K = 2
// 返回值应为节点值 4

链表节点结构体定义

type ListNode struct {
Val int
Next *ListNode
}

递归解法主要思路

思路概述:

  1. 从头结点开始递归遍历至链表末尾。
  2. 每次函数返回时携带当前已经遍历过的节点计数。
  3. 当计数等于 K 时即可确定当前节点为倒数第 K 个。
  4. 利用 Go 的多返回值。把计数和目标节点一起返回,避免全局变量带来的副作用。

递归函数实现

func findKthFromEnd {
// 基础情况:到达链表末尾
if node == nil {
return nil,0
}
// 向后递归,获取子调用的结果和计数
foundNode,cnt := findKthFromEnd
// 当前层级计数加一
cnt++
// 当计数恰好等于 k 时这个 node 就是目标节点
if cnt == k && foundNode == nil {
return node,cnt
}
// 否则继续把子调用得到的结果往上传
return foundNode,cnt
}

从对外包装函数来看。隐藏计数细节

func FindKthToTail *ListNode {
if head == nil || k <= 0 {
        return nil // 参数非法或空链表直接返回
    }
    node, _ := findKthFromEnd
    return node
}

完整示例代码

package main
import (
"fmt"
)
// ---------- 链表结构 ----------
type ListNode struct {
Val int
Next *ListNode
}
// ---------- 递归主要 ----------
func findKthFromEnd {
if node == nil {
return nil,0
}
foundNode,cnt := findKthFromEnd
cnt++
if cnt == k && foundNode == nil {
return node,cnt
}
return foundNode,cnt
}
// ---------- 对外接口 ----------
func FindKthToTail *ListNode {
if head == nil || k <= 0 {
        return nil
    }
    node, _ := findKthFromEnd
   \treturn node
}
// ---------- 辅助函数:构造链表 ----------
func buildList *ListNode {
\tif len == 0 {
\t\treturn nil
\t}
\ad := &ListNode{Val: vals}
\tcur := head
\tfor _, v := range vals {
\t\tcur.Next = &ListNode{Val: v}
\t\tcur = cur.Next
\t}
\treturn head
}
// ---------- 主函数演示 ----------
func main {
\ad := buildList
\tk := 2
\ttarget := FindKthToTail
\tif target != nil {
\t\tfmt.Printf
\t} else {
\t\tfmt.Printf
\t}
}

常见错误与避免策略

  • NIL 检查遗漏:在任何对指针进行解引用前,都必须先判断是否为 nil。说起来,这是防止运行时 panic 的第一步先。
  • 终止条件写错:a)若把 K==0) 当作合法输入,会导致永远无法匹配;b)若忘记在到达末尾时返回 (nil。0)),上层调用将得到错误计数。
  • 全局变量计数:a)使用全局变量会在并发或多次调用时产生竞争;b)本实现,
  • K 超出范围:a)如果 K 大于链表长度,需要在包装函数里直接返回 null);b)上述实现自然满足,因为递归结束后计数永远不到 K。
  • Pointers vs Values:a)Go 中参数是按值传递,但指针本身也是一个值;b)务必传入 **指向**结构体的指针,而不是结构体拷贝。否则对 Next 的修改不会反映到原链表上。

时间与空间复杂度分析

时间复杂度 空间复杂度
递归实现 OO
迭代双指针实现 OO

虽然迭代版拥有更低空间占用,但递归版代码更简洁、易读。若链表极长且担心栈溢出,可改用双指针迭代方案。

Troubleshooting 小贴士

  1. Panic: runtime error: invalid memory address or nil pointer dereference - 检查入口处是否忘记了 If head == nil { return nil }.
  2. K 为负或零 - 在包装函数里统一过滤掉非正整数输入。按理说,
  3. 返回值总是 nil - 确认计数器是否在每层都正确 +1。而且只在第一次达到 K 时返回当前节点,而不是继续覆盖。
  4. Cruel infinite recursion - 确保每次递归都向更深层移动(即传入 ),否则永远停留在同一层导致堆栈溢出。

掌握递归,你也能轻松解决 “倒数第 K 个” 类题目!不过,

看完这篇。你已经了解:

  • 如何安全地检查空指针避免 panic;
  • 如何设计明确且唯一的基准情形;
  • 如何利用多返回值携带计数,从而保持函数纯粹、无副作用;
  • 常见坑点及对应防御措施。

只要牢记「先检查再操作」「确保每层都有进展」这两条原则,你就能自信地使用递归解决差不多问题。祝编码愉快,老实说,

如何用递归在 Go 中寻链表尾 K 个节点之根?

标签:递归