如何用递归在 Go 中寻链表尾 K 个节点之根?
- 内容介绍
- 文章标签
- 相关推荐
:为何递归查找链表倒数第 K 个节点会让人头疼?
在实际开发中,常见的痛点包括:
-
忘记在递归入口检查
nil导致空指针异常。 - 递归终止条件写得不明确,容易出现无限递归或提前返回。
- 计数器的传递方式不当,使得最终返回的节点不是期望的倒数第 K 个。
- 对 Go 中指针传值机制不了解,导致对链表结构的修改未生效。老实说,
问题描述
给定一个单向链表的头指针 head还有正整数 K返回链表中倒数第 K 个节点。如果链表长度小于 K则返回 nil。
示例
// 链表: 1 → 2 → 3 → 4 → 5
// K = 2
// 返回值应为节点值 4
链表节点结构体定义
type ListNode struct {
Val int
Next *ListNode
}
递归解法主要思路
思路概述:
- 从头结点开始递归遍历至链表末尾。
- 每次函数返回时携带当前已经遍历过的节点计数。
-
当计数等于
K时即可确定当前节点为倒数第 K 个。 - 利用 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 的修改不会反映到原链表上。
时间与空间复杂度分析
| 时间复杂度 | 空间复杂度 | |
|---|---|---|
| 递归实现 | O | O |
| 迭代双指针实现 | O | O |
虽然迭代版拥有更低空间占用,但递归版代码更简洁、易读。若链表极长且担心栈溢出,可改用双指针迭代方案。
Troubleshooting 小贴士
-
Panic: runtime error: invalid memory address or nil pointer dereference - 检查入口处是否忘记了
If head == nil { return nil }. - K 为负或零 - 在包装函数里统一过滤掉非正整数输入。按理说,
- 返回值总是 nil - 确认计数器是否在每层都正确 +1。而且只在第一次达到 K 时返回当前节点,而不是继续覆盖。
-
Cruel infinite recursion - 确保每次递归都向更深层移动(即传入
),否则永远停留在同一层导致堆栈溢出。
掌握递归,你也能轻松解决 “倒数第 K 个” 类题目!不过,
看完这篇。你已经了解:
- 如何安全地检查空指针避免 panic;
- 如何设计明确且唯一的基准情形;
- 如何利用多返回值携带计数,从而保持函数纯粹、无副作用;
- 常见坑点及对应防御措施。
只要牢记「先检查再操作」「确保每层都有进展」这两条原则,你就能自信地使用递归解决差不多问题。祝编码愉快,老实说,
:为何递归查找链表倒数第 K 个节点会让人头疼?
在实际开发中,常见的痛点包括:
-
忘记在递归入口检查
nil导致空指针异常。 - 递归终止条件写得不明确,容易出现无限递归或提前返回。
- 计数器的传递方式不当,使得最终返回的节点不是期望的倒数第 K 个。
- 对 Go 中指针传值机制不了解,导致对链表结构的修改未生效。老实说,
问题描述
给定一个单向链表的头指针 head还有正整数 K返回链表中倒数第 K 个节点。如果链表长度小于 K则返回 nil。
示例
// 链表: 1 → 2 → 3 → 4 → 5
// K = 2
// 返回值应为节点值 4
链表节点结构体定义
type ListNode struct {
Val int
Next *ListNode
}
递归解法主要思路
思路概述:
- 从头结点开始递归遍历至链表末尾。
- 每次函数返回时携带当前已经遍历过的节点计数。
-
当计数等于
K时即可确定当前节点为倒数第 K 个。 - 利用 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 的修改不会反映到原链表上。
时间与空间复杂度分析
| 时间复杂度 | 空间复杂度 | |
|---|---|---|
| 递归实现 | O | O |
| 迭代双指针实现 | O | O |
虽然迭代版拥有更低空间占用,但递归版代码更简洁、易读。若链表极长且担心栈溢出,可改用双指针迭代方案。
Troubleshooting 小贴士
-
Panic: runtime error: invalid memory address or nil pointer dereference - 检查入口处是否忘记了
If head == nil { return nil }. - K 为负或零 - 在包装函数里统一过滤掉非正整数输入。按理说,
- 返回值总是 nil - 确认计数器是否在每层都正确 +1。而且只在第一次达到 K 时返回当前节点,而不是继续覆盖。
-
Cruel infinite recursion - 确保每次递归都向更深层移动(即传入
),否则永远停留在同一层导致堆栈溢出。
掌握递归,你也能轻松解决 “倒数第 K 个” 类题目!不过,
看完这篇。你已经了解:
- 如何安全地检查空指针避免 panic;
- 如何设计明确且唯一的基准情形;
- 如何利用多返回值携带计数,从而保持函数纯粹、无副作用;
- 常见坑点及对应防御措施。
只要牢记「先检查再操作」「确保每层都有进展」这两条原则,你就能自信地使用递归解决差不多问题。祝编码愉快,老实说,

