二叉查找树 (Binary Search Tree, 简称 BST) 是一种基本的数据结构,其设计核心在于每个节点的值都满足以下性质:
- 左子树的所有节点值均小于当前节点值。
- 右子树的所有节点值均大于当前节点值。
这使得二叉查找树能够高效地支持一系列查找相关操作,包括普通查找、前驱后继查询、基于排名的查询以及基于值的排名计算。在树形合理时,bst 可以以 log 的时间快速进行集合查找。
本文将结合代码和理论讲解这些操作,帮助读者全面掌握 BST 的查找机制。
假定我们这样构造二叉树:
struct TreeNode {
int value; // 节点的值
int size; // 以该节点为根的子树大小
TreeNode* left; // 指向左子树
TreeNode* right; // 指向右子树
TreeNode(int v) : value(v), size(1), left(nullptr), right(nullptr) {}
};
1. 普通查找
问题描述
给定一个值 $ x $,我们希望在 BST 中找到值等于 $ x $ 的节点。
查找思路
从根节点开始递归或迭代:
- 如果当前节点值等于 $ x $,则查找成功。
- 如果 $ x $ 小于当前节点值,则进入左子树继续查找。
- 如果 $ x $ 大于当前节点值,则进入右子树继续查找。
- 如果到达空节点,查找失败。
代码实现
cpp
TreeNode* find(TreeNode* root, int value) {
if (!root || root->value == value) return root;
if (value < root->value) return find(root->left, value);
return find(root->right, value);
}
时间复杂度
- 平衡树:$ O(\log n) $
- 非平衡树(退化为链表):$ O(n) $
即效率取决于树高。
2. 查找前驱和后继
前驱与后继的定义
- 前驱:小于给定值的最大节点。
- 后继:大于给定值的最小节点。
前驱查找
- 如果节点有左子树,前驱是左子树中值最大的节点。
- 如果节点没有左子树,沿着父节点回溯,找到第一个右子树包含当前节点的祖先节点。
后继查找
- 如果节点有右子树,后继是右子树中值最小的节点。
- 如果节点没有右子树,沿着父节点回溯,找到第一个左子树包含当前节点的祖先节点。
代码实现
cpp
TreeNode* findPredecessor(TreeNode* root, int value) {
TreeNode* predecessor = nullptr;
while (root) {
if (value > root->value) {
predecessor = root;
root = root->right;
} else {
root = root->left;
}
}
return predecessor;
}
TreeNode* findSuccessor(TreeNode* root, int value) {
TreeNode* successor = nullptr;
while (root) {
if (value < root->value) {
successor = root;
root = root->left;
} else {
root = root->right;
}
}
return successor;
}
3. 以排名查询值
问题描述
给定一个排名 $ k $,找到 BST 中第 $ k $ 小的节点值。
额外信息:子树大小
为了保存排名信息,我们在每个节点存储一个额外的属性 size
,表示以当前节点为根的子树大小。
- 根节点的子树大小为其左子树大小加右子树大小再加 1。
- 在插入节点时动态更新
size
属性。
查询思路
- 计算左子树大小 $ \text{size}_{\text{left}} $。
- 如果 $ k = \text{size}_{\text{left}} + 1 $,当前节点即为所求。
- 如果 $ k \leq \text{size}_{\text{left}} $,在左子树中递归查找。
- 如果 $ k > \text{size}{\text{left}} + 1 $,在右子树中递归查找,更新 $ k = k - \text{size}{\text{left}} - 1 $。
代码实现
cpp
TreeNode* findByRank(TreeNode* root, int k) {
if (!root) return nullptr;
int leftSize = root->left ? root->left->size : 0;
if (k == leftSize + 1) return root;
if (k <= leftSize) return findByRank(root->left, k);
return findByRank(root->right, k - leftSize - 1);
}
4. 以值查询排名
问题描述
给定一个值 $ x $,求其在 BST 中的排名(即有多少节点值小于 $ x $)。
查询思路
- 初始化排名计数器 $ \text{rank} = 0 $。
- 遍历树路径:
- 如果 $ x $ 小于当前节点值,进入左子树。
- 如果 $ x $ 大于当前节点值,更新 $ \text{rank} += \text{size}_{\text{left}} + 1 $,进入右子树。
- 如果找到值等于 $ x $,返回当前排名。
- 如果到达空节点,说明值不存在。
代码实现
cpp
int findRankByValue(TreeNode* root, int value) {
int rank = 0;
while (root) {
if (value < root->value) {
root = root->left;
} else {
rank += (root->left ? root->left->size : 0) + 1;
if (value == root->value) return rank;
root = root->right;
}
}
return -1; // 值不存在
}
总结
操作 | 方法描述 | 时间复杂度 |
---|---|---|
查找值 | 从根节点沿路径递归查找。 | O(h) |
查找前驱/后继 | 利用树的结构特性,寻找最接近的节点值。 | O(h) |
以排名查询值 | 利用子树大小属性,从根递归寻找第 ( k ) 小节点。 | O(h) |
以值查询排名 | 统计路径中左子树大小,计算排名。 | O(h) |
通过本文的讲解和代码示例,希望读者能够掌握 BST 的查找操作,并能在实际开发中灵活运用这些知识。
然而,需要注意的是,查找操作的效率高度依赖于树的平衡性。当 BST 不平衡时,其性能可能退化到与链表类似,导致时间复杂度变为 $ O(n) $。因此,在进行插入和删除操作时,保持树的平衡是至关重要的。
为了解决这一问题,平衡二叉树(如 AVL 树、红黑树)通过特定的旋转操作,保证了树的高度始终保持在 $ O(\log n) $ 的级别。作为延伸思考,你可以研究平衡树的原理及其在保持效率上的作用。