算法刷题系列(五十九):二叉搜索树——查找、插入与删除
算法刷题系列(五十九):二叉搜索树——查找、插入与删除
算法刷题系列(五十九):二叉搜索树——查找、插入与删除
目录里的早期“二叉排序树”笔记只留下了定义和一张插图。本篇把定义扩展成完整操作:在二叉搜索树(BST)中,左子树的键小于根,右子树的键大于根,并且左右子树本身也满足这一性质。
这条顺序性质让查找可以像二分一样沿一条根到叶路径前进;但普通 BST 不保证平衡,输入顺序可能让它退化成链表。
一、查找为什么只走一边?
要查找键 x,在当前节点比较:
x小于当前键,只可能在左子树;x大于当前键,只可能在右子树;- 相等则命中。
每次比较都丢弃另一棵子树。运行时间与树高 h 成正比:平均平衡时约 O(log n);极端偏斜时为 O(n)。
二、插入与重复键策略
从根开始按查找规则走,遇到空孩子时插入新节点。下面的实现约定不保存重复键;若业务需要重复键,可让节点保存计数或将相等键统一放在一侧,但查询、删除和旋转都必须遵循同一规则。
三、删除分三种情况
删除目标节点时:
- 叶子节点:让父节点链接置空。
- 只有一个孩子:用唯一孩子替代该节点。
- 有两个孩子:用右子树最小节点(中序后继)替换键值,再删除后继原位置。
第三种情况保持了左边都小于替代键、右边都大于替代键的顺序关系。
四、Java 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class BinarySearchTree {
private static final class Node {
int key;
Node left;
Node right;
Node(int key) {
this.key = key;
}
}
private Node root;
public boolean contains(int key) {
Node current = root;
while (current != null) {
if (key == current.key) {
return true;
}
current = key < current.key ? current.left : current.right;
}
return false;
}
public void insert(int key) {
root = insert(root, key);
}
private Node insert(Node node, int key) {
if (node == null) {
return new Node(key);
}
if (key < node.key) {
node.left = insert(node.left, key);
} else if (key > node.key) {
node.right = insert(node.right, key);
}
return node;
}
public void delete(int key) {
root = delete(root, key);
}
private Node delete(Node node, int key) {
if (node == null) {
return null;
}
if (key < node.key) {
node.left = delete(node.left, key);
} else if (key > node.key) {
node.right = delete(node.right, key);
} else if (node.left == null) {
return node.right;
} else if (node.right == null) {
return node.left;
} else {
Node successor = node.right;
while (successor.left != null) {
successor = successor.left;
}
node.key = successor.key;
node.right = delete(node.right, successor.key);
}
return node;
}
}
五、Python 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
from __future__ import annotations
class Node:
def __init__(self, key: int) -> None:
self.key = key
self.left: Node | None = None
self.right: Node | None = None
class BinarySearchTree:
def __init__(self) -> None:
self.root: Node | None = None
def contains(self, key: int) -> bool:
current = self.root
while current is not None:
if key == current.key:
return True
current = current.left if key < current.key else current.right
return False
def insert(self, key: int) -> None:
self.root = self._insert(self.root, key)
def _insert(self, node: Node | None, key: int) -> Node:
if node is None:
return Node(key)
if key < node.key:
node.left = self._insert(node.left, key)
elif key > node.key:
node.right = self._insert(node.right, key)
return node
def delete(self, key: int) -> None:
self.root = self._delete(self.root, key)
def _delete(self, node: Node | None, key: int) -> Node | None:
if node is None:
return None
if key < node.key:
node.left = self._delete(node.left, key)
elif key > node.key:
node.right = self._delete(node.right, key)
elif node.left is None:
return node.right
elif node.right is None:
return node.left
else:
successor = node.right
while successor.left is not None:
successor = successor.left
node.key = successor.key
node.right = self._delete(node.right, successor.key)
return node
tree = BinarySearchTree()
for value in (8, 3, 10, 1, 6, 14, 4, 7, 13):
tree.insert(value)
assert tree.contains(6) and not tree.contains(2)
tree.delete(3)
assert not tree.contains(3) and tree.contains(4) and tree.contains(6)
低于 Python 3.10 的解释器可在代码顶部添加 from __future__ import annotations。
六、中序遍历与有序输出
BST 的中序遍历顺序是“左子树、当前节点、右子树”,因此输出键按升序排列。反过来,若树的中序结果不是非递减序列,输入就不满足 BST 的定义。
验证 BST 时,不能只比较节点与直接孩子;必须检查整棵左子树都小于祖先、整棵右子树都大于祖先。可递归传递每个子树允许的上下界。
七、边界与复杂度
- 空树查找失败、插入后成为根节点。
- 删除不存在的键不改变树。
- 删除根节点要把新的子树根返回给调用者。
- 若按升序依次插入,普通 BST 会变成高度为
n的链,操作退化为O(n)。 - 递归操作调用栈为
O(h);偏斜树可能导致递归深度过大。
因此,BST 的顺序性质并不等于它自动平衡。下一篇对比 AVL、B 树和 B+ 树如何控制高度,及它们为何适用于不同的内存层次。
小结
BST 用一条比较规则把查找范围缩到一个孩子;插入沿同一路径寻找空位,删除则需处理零、一、两个孩子三类结构。操作复杂度由树高决定,想获得稳定的对数级操作还需要平衡机制。
本文由作者按照 CC BY 4.0 进行授权