二叉搜索树 — 插入、查找、删除与平衡概念
二叉搜索树 — 插入、查找、删除与平衡概念
一、什么是二叉搜索树
二叉搜索树(Binary Search Tree,BST)是一种特殊的二叉树,它在二叉树的基础上加了一条"排序规则":
对于任意节点,左子树中所有节点的值 < 该节点的值 < 右子树中所有节点的值。
这个规则看似简单,但它让查找变得极其高效——每一步你都可以排除掉一半的候选节点,就像翻字典一样:要找"苹果",你知道它在前面,绝对不会翻到"西瓜"那部分去。
BST 的核心性质
- 有序性:中序遍历 BST 会得到一个严格升序的序列。
- 查找效率:在平衡状态下,查找、插入、删除都是 O(log n)。
- 无重复值:标准 BST 不允许重复值(遇到重复值时要么忽略,要么用计数或链表处理)。
二、节点结构与基本操作
type BSTNode struct {
Val int
Left *BSTNode
Right *BSTNode
}
2.1 查找(Search)
查找是 BST 最核心的操作。从根节点出发,比较目标值与当前节点的大小关系,决定向左还是向右走。
查找 7:
10
/ \
5 15 10 > 7 → 往左
/ \ / \
3 7 12 18 5 < 7 → 往右 → 找到了!
func (node *BSTNode) Search(val int) *BSTNode {
curr := node
for curr != nil {
if val == curr.Val {
return curr
} else if val < curr.Val {
curr = curr.Left
} else {
curr = curr.Right
}
}
return nil
}
迭代版本比递归版本好在不需要额外的调用栈开销。如果树是平衡的,每次比较排除一半节点,查找效率就是 O(log n)。
2.2 插入(Insert)
插入的逻辑和查找很相似:先"假装"要查找这个值,走到一个空位(nil)时,那里就是新节点该待的地方。
func (node *BSTNode) Insert(val int) *BSTNode {
if node == nil {
return &BSTNode{Val: val}
}
if val < node.Val {
node.Left = node.Left.Insert(val)
} else if val > node.Val {
node.Right = node.Right.Insert(val)
}
// val == node.Val 时什么都不做,BST 不允许重复
return node
}
这里返回 *BSTNode 是因为插入可能改变树的根(空树插入第一个节点时)。递归写法简洁优雅,但要注意栈深度问题。也可以写成迭代版本:
func InsertIterative(root *BSTNode, val int) *BSTNode {
newNode := &BSTNode{Val: val}
if root == nil {
return newNode
}
curr := root
for {
if val < curr.Val {
if curr.Left == nil {
curr.Left = newNode
break
}
curr = curr.Left
} else if val > curr.Val {
if curr.Right == nil {
curr.Right = newNode
break
}
curr = curr.Right
} else {
// 重复值,不插入
break
}
}
return root
}
2.3 删除(Delete)
删除是 BST 操作中最复杂的一个,因为删除一个节点后还要维持 BST 的有序性质。分三种情况:
情况一:删除叶子节点
最简单,直接删掉即可。让父节点指向该节点的指针变为 nil。
删除 7: 删除后:
10 10
/ \ / \
5 15 → 5 15
\
7
情况二:删除只有一个子节点的节点
用它的唯一子节点"顶替"它的位置,就像公司里某个员工离职了,他带的唯一一个下属直接向他的上级汇报。
删除 5(只有右孩子): 删除后:
10 10
/ \ / \
5 15 → 7 15
\
7
情况三:删除有两个子节点的节点
这是最棘手的。不能直接删,因为两个孩子没法同时顶替一个位置。策略是:找一个"替身"来占据这个位置,然后删除替身原来的位置。
替身有两个候选:
- 后继节点(successor):右子树中的最小值(一路向左走到底)
- 前驱节点(predecessor):左子树中的最大值(一路向右走到底)
通常用后继节点。后继节点要么是叶子,要么只有右孩子(因为它是"最左"的),所以删它很简单。
删除 10(有两个孩子):
10 12 ← 后继顶上来
/ \ / \
5 15 → 5 15
/ \ / \
12 18 nil 18
func (node *BSTNode) Delete(val int) *BSTNode {
if node == nil {
return nil
}
if val < node.Val {
node.Left = node.Left.Delete(val)
} else if val > node.Val {
node.Right = node.Right.Delete(val)
} else {
// 找到了要删除的节点
// 情况一 & 二:0或1个子节点
if node.Left == nil {
return node.Right
}
if node.Right == nil {
return node.Left
}
// 情况三:2个子节点,找后继
successor := node.Right.minNode()
node.Val = successor.Val
node.Right = node.Right.Delete(successor.Val)
}
return node
}
// minNode 找子树中的最小节点
func (node *BSTNode) minNode() *BSTNode {
curr := node
for curr.Left != nil {
curr = curr.Left
}
return curr
}
三、BST 的退化问题
BST 的性能高度依赖树的形状。在理想情况下(平衡),高度是 log₂n,所有操作都是 O(log n)。
但如果按照有序序列(比如 1, 2, 3, 4, 5)依次插入,树会退化成一棵"右斜树"——每个节点都只有右孩子,本质上就是一条链表。
插入顺序: 1, 2, 3, 4, 5
结果:
1
\
2
\
3
\
4
\
5
高度 = 5,查找复杂度 = O(n),退化为链表!
这就是为什么我们需要平衡二叉搜索树——AVL 树和红黑树通过旋转操作保证树的高度始终是 O(log n)。它们的核心思想是:当插入或删除导致某棵子树"偏重"时,通过旋转来重新平衡。
四、复杂度总结
| 操作 | 平均情况 | 最坏情况(退化为链表) |
|---|---|---|
| 查找 | O(log n) | O(n) |
| 插入 | O(log n) | O(n) |
| 删除 | O(log n) | O(n) |
| 最小值/最大值 | O(log n) | O(n) |
| 中序遍历 | O(n) | O(n) |
五、BST 的典型应用
- 数据库索引:B+ 树是 BST 的推广,几乎所有关系数据库的索引底层都是 B+ 树。
- 符号表:编译器用 BST 存储变量名到类型的映射。
- 自动补全:结合前缀搜索,快速找到以某字符串开头的所有词条。
- 区间查询:查找落在 [low, high] 范围内的所有值。
六、关键要点
- BST 的核心是"左小右大",这个简单规则带来了高效的查找能力。
- 中序遍历 = 升序输出,这是 BST 区别于普通二叉树的关键特性。
- 删除有两个子节点的节点时,找后继(或前驱)来顶替,保持结构的有序性。
- 插入顺序决定树的形状,有序插入会导致退化。实际工程中直接用平衡树(如 Go 没有内置平衡树,但可以用第三方库或自己实现红黑树)。
- Go 标准库没有 BST,这一点和 Java(TreeMap)、C++(std::map)不同。Go 的
sort包提供了排序切片,但没有内置有序映射,需要时自己实现或用第三方。

浙公网安备 33010602011771号