1. 理论推导过程
二叉搜索树的基本定义
二叉搜索树(Binary Search Tree, BST)是一种特殊的二叉树,其每个节点都满足以下性质:
-
左子树所有节点的键值均小于根节点的键值;
-
右子树所有节点的键值均大于根节点的键值;
-
左右子树本身也是二叉搜索树。
这一性质保证了在树中查找一个元素时,可以利用二分查找的思想,从根节点出发,根据键值大小选择左子树或右子树,从而大大减少查找次数。
理论推导
-
查找操作:
对于给定的键值x
:-
从根节点开始比较,如果
x
等于当前节点的键值,则查找成功; -
如果
x
小于当前节点的键值,则递归地在左子树中查找; -
如果
x
大于当前节点的键值,则递归地在右子树中查找。
-
-
插入操作:
插入一个新键时,利用查找操作定位到应插入的位置(即找到一个空子树),然后将新键作为该空位置的节点插入,同时保证新树依然满足 BST 的定义。 -
删除操作:
删除一个节点时需要考虑三种情况:-
叶子节点:直接删除即可;
-
只有一个子树的节点:用子树替代该节点;
-
有两个子树的节点:通常找该节点右子树中最小的节点(或左子树中最大的节点)来替代,然后删除该节点,保证树的有序性。
-
2. 使用步骤
以常见操作为例,说明 BST 的使用步骤:
查找操作
-
从根节点开始;
-
若当前节点为空,则返回“未找到”;
-
比较
x
与当前节点的键值:-
若相等,则查找成功;
-
若
x
小于当前节点键值,则移动到左子树; -
若
x
大于当前节点键值,则移动到右子树;
-
-
重复步骤 2-3,直到查找结束。
插入操作
-
从根节点开始;
-
若当前节点为空,则插入新节点;
-
比较新键
x
与当前节点键值:-
若
x
小于当前节点键值,则递归插入到左子树; -
若
x
大于当前节点键值,则递归插入到右子树;
-
-
插入后返回根节点,保证树结构不变。
删除操作
-
查找要删除的节点;
-
根据该节点的子树情况分类讨论:
-
叶子节点:直接删除;
-
一个子树:用子树代替该节点;
-
两个子树:找右子树中最小节点(或左子树中最大节点)替换当前节点,然后删除替换节点;
-
-
调整链接,保证 BST 性质不变。
3. 时间复杂度推导
-
平均情况:
在一棵平衡的 BST 中,每次操作沿着树的高度进行查找、插入或删除。
平均情况下,树的高度为 O(logn) (n 为节点数),因此平均时间复杂度为:-
查找:O(logn)
-
插入:O(logn)
-
删除:O(logn)
-
-
最坏情况:
当 BST 退化为链表(例如插入的键已经有序),树的高度为 O(n) ;
此时,操作的时间复杂度退化为:-
查找:O(n)
-
插入:O(n)
-
删除:O(n)
-
4. 应用场景
二叉搜索树因其高效的查找、插入和删除操作,在以下场景中被广泛应用:
-
字典/映射数据结构: 用于存储键值对,实现快速查找和动态更新。
-
集合操作: 用于集合的存储和去重,支持高效的范围查询。
-
数据库索引: 在数据库中,通过 BST 或其平衡变种(如 AVL 树、红黑树)加速数据检索。
-
符号表: 在编译器和解释器中维护标识符与其属性的映射。
5. 示例
假设我们有一组待插入的键:[50, 30, 70, 20, 40, 60, 80],我们一步步构造 BST。
插入过程
-
插入 50
-
当前树为空,50 成为根节点。
当前树结构:
50
-
-
插入 30
-
比较 30 与根 50,30 < 50,插入到左子树。
当前树结构:
50/30
-
-
插入 70
-
比较 70 与根 50,70 > 50,插入到右子树。
当前树结构:
50/ \30 70
-
-
插入 20
-
20 < 50,进入左子树;20 < 30,插入到 30 的左侧。
当前树结构:
50/ \30 70/ 20
-
-
插入 40
-
40 < 50,进入左子树;40 > 30,插入到 30 的右侧。
当前树结构:
50/ \30 70/ \ 20 40
-
-
插入 60
-
60 > 50,进入右子树;60 < 70,插入到 70 的左侧。
当前树结构:
50/ \30 70/ \ / 20 40 60
-
-
插入 80
-
80 > 50,进入右子树;80 > 70,插入到 70 的右侧。
最终 BST 结构:
50/ \30 70/ \ / \ 20 40 60 80
-
查找操作示例
例如查找 60:
-
从根 50 开始:60 > 50,进入右子树;
-
在节点 70:60 < 70,进入左子树;
-
在节点 60:找到目标。
删除操作示例
例如删除 70:
-
节点 70 有两个子树,常见做法是找右子树中最小的节点,此处为 80 的左子树为空,则 80 为最小;
-
或者找左子树中最大的节点(这里为 60)。
假设我们选择使用 60 替换 70: -
将 60 替换到 70 的位置,然后删除原来的 60 节点(由于 60 为叶子节点,直接删除)。
最终树结构变为:50/ \30 60/ \ \ 20 40 80