Go中BST实现关键在于指针赋值:Insert必须返回新根并显式赋值(如t.root = t.insert(t.root, val)),避免局部指针修改无效;Search需先判nil再解引用;中序遍历传切片地址或返回新slice;删除双子节点时用中序后继替换并递归删除。

Go 里写一个能用的二叉搜索树(BST),关键不是“能不能跑”,而是**指针改了但没传回去**——这是 90% 新手 panic 或逻辑错乱的根源。
Insert 必须返回新根,不能只改局部指针
常见错误是写成 func (n *Node) Insert(val int) 然后直接操作 n.Left 或 n.Right,结果递归调用后父节点的指针还是旧值,树结构断裂。
- 正确做法:插入函数必须返回
*Node,上层用赋值接收,例如node.Left = insert(node.Left, val) - 根节点插入要显式重赋值:
t.root = t.insert(t.root, val),否则t.root永远不会变 - 递归终止时新建节点:
if node == nil { return &Node{Val: val} },不能漏判空 - 重复值不插入(BST 定义要求唯一),所以
val == node.Val时直接return node,不往下走
Search 要先判 nil 再解引用
Go 中对 nil 指针调用方法或访问字段会 panic,而 BST 查找天然可能走到空子树。很多人写 if val == node.Val 就 panic 了。
- 必须第一行检查:
if node == nil { return false }或if node == nil { return nil } - 不要用
node != nil && node.Val == val这种短路写法——Go 不保证左操作数求值后才求右,且可读性差 - 返回
*Node时,调用方必须自己判空;返回bool更安全,适合存在性判断 - 非递归版本可用 for 循环 + 指针移动,避免栈溢出,但要注意循环条件是
node != nil
中序遍历输出有序序列,但切片传参容易丢数据
递归中序遍历常用 append 累加结果,但 Go 的 slice 是值传递,如果只传 []int,底层 array 扩容后原变量收不到新底层数组。
立即学习“go语言免费学习笔记(深入)”;
- 正确方式:传
*[]int或返回新 slice,例如inorder(node.Left, &res) - 更推荐函数式写法:
return append(append(inorder(node.Left), node.Val), inorder(node.Right)...),清晰但有拷贝开销 - 如果只想要打印,直接
fmt.Println(node.Val)即可,无需收集 - 注意:中序遍历结果有序,是 BST 的核心性质验证点,建议写个测试比对
sort.Ints后是否一致
删除节点时,双子节点场景必须找中序后继
删掉有左右孩子的节点,不能简单拿左或右子树顶上——会破坏 BST 性质。必须用中序后继(右子树最小值)或中序前驱(左子树最大值)替换。
- 中序后继一定在右子树最左端,用循环找:
for succ := node.Right; succ.Left != nil; succ = succ.Left { } - 找到后,把后继值复制到当前节点,再递归删掉那个后继节点(它最多只有一个孩子)
- 别忘了处理后继节点被删后,其父节点的指针更新——递归删除会自动处理,前提是删除函数也返回新子树根
- 如果只删叶节点或单孩子节点,直接返回非空子树即可:
if node.Left == nil { return node.Right }
BST 表面简单,但指针、递归、边界判断三者叠加,稍不留神就漏掉一层 nil 检查或少一次赋值。真正卡住人的从来不是算法逻辑,而是 Go 里那几个星号和等号的位置。


















