二叉搜索树必须用指针(Node)定义,因插入、删除需动态修改父子关系,值类型传参为副本无法影响原树;Insert/Delete 必须返回 Node 以支持子树重接,中序遍历天然有序,判断合法性应递归传递上下界而非收集后比对。
二叉搜索树的结构定义为什么必须用指针?
因为 BST 的插入、删除操作会动态改变节点的父子关系,如果用值类型(如
而非
),每次传参都是副本,修改无法反映到原树上。Go 中切片、map、channel 本身是引用类型,但自定义结构体不是——所以
是刚需。
典型错误:写成
,调用后 root 完全没变。
正确做法是:
如何避免递归插入时的空指针 panic?
常见崩溃场景:调用
前没检查
,而方法内部又对
做了字段访问。其实更稳妥的方式是把递归逻辑收在函数入口做判空,让递归体只处理非空节点。
立即学习
“
go语言免费学习笔记(深入)
”;
不要在
内部直接写
,而是先判断
再赋新节点
或者统一由调用方保证:让
总返回新子树根,由上层重新赋值(如上例),这样空节点的处理被提前收束
注意:
必须返回
,否则无法把新创建的叶子节点“挂回去”
查找和删除为什么不能只靠递归返回 bool 或 int?
查找可以只返回
或
,但删除必须重构局部树结构——它可能把子节点提上来,也可能用中序后继替换,这些都会导致原节点的父指针需要更新。如果只写
,就丧失了重连能力。
实操建议:
删除函数签名应为
,和
保持一致
遇到要删的节点时,若它有两个子节点,找到右子树最小值(中序后继),用其值覆盖当前节点,再递归删右子树里的那个重复值——这样避免多次结构调整
特别注意:删除后若节点变成叶子或单子节点,要返回非空子节点(或
),让父节点能正确重接
BST 遍历中,中序遍历为什么最常用?
因为 BST 的定义决定了中序遍历天然输出升序序列,这是它区别于普通二叉树的核心价值。其他遍历(前序、后序)在 BST 场景下极少单独使用。
实现要点:
中序递归最简写法是
,但要注意栈深度——极端左斜树可能导致栈溢出
生产环境建议用迭代+栈模拟,或改用 Morris 遍历(O(1) 空间),不过后者会临时修改树结构,需谨慎
如果只是想“判断是否合法 BST”,别直接中序收集再比对,而是用递归传递上下界:
,初始调用传
表示无界
边界容易被忽略的是:节点值可能为
或
,所以传入的
最好用指针或封装成可空类型,而不是硬写极值。
Node*Noderoot *Nodefunc (n Node) Insert(val int)type Node struct {
Val int
Left *Node
Right *Node
}
func (n *Node) Insert(val int) *Node {
if n == nil {
return &Node{Val: val}
}
if val < n.Val {
n.Left = n.Left.Insert(val)
} else if val > n.Val {
n.Right = n.Right.Insert(val)
}
return n
}n.Left.Insert()n.Left == niln(*Node).Insertn.Left.Insert()n.Left == nilInsertInsert*Nodebool*Nodefunc Delete(val int) boolfunc (n *Node) Delete(val int) *NodeInsertnilinorder(n.Left); visit(n); inorder(n.Right)isValid(n, min, max)nilmath.MinInt64math.MaxInt64min/max