一、二叉搜索树的核心概念与性质
二叉搜索树,亦称二叉排序树,它是一棵具有严格顺序约束的二叉树。其定义遵循递归规则:一棵树要么为空,要么满足以下三条性质:
-
左子树约束 :若左子树不为空,则左子树上所有 节点的值均小于或等于根节点的值。
-
右子树约束 :若右子树不为空,则右子树上所有 节点的值均大于或等于根节点的值。
-
递归性质:其左子树和右子树本身也必须各自是一棵二叉搜索树。
关于重复值的处理策略 :
二叉搜索树对相等值的插入策略是灵活的,取决于具体应用场景。
唯一键场景 :不允许插入相等的值。对应 C++ STL 中的
std::map和std::set,它们依赖红黑树实现,保证了键的唯一性。多重键场景 :允许插入相等的值。对应 C++ STL 中的
std::multimap和std::multiset,允许同一键值多次出现。
二、二叉搜索树的性能深度剖析
二叉搜索树的查找、插入和删除效率与树的高度(h) 直接相关,而高度又取决于数据的插入顺序。
-
最优情况 :
当数据分布均匀,构建出的树形结构为完全二叉树(或接近完全二叉树)时,树的高度约为 h=log2Nh=log2N。
- 时间复杂度:O(logN)O(logN)
-
最差情况 :
当数据以近乎有序的顺序(如严格递增或递减)插入时,二叉搜索树会严重失衡,退化为单支树(形似链表)。
-
树的高度:h=Nh=N
-
时间复杂度:O(N)O(N)(此时查找效率与遍历链表无异)
-
-
综合评价 :
综合最好与最坏情况,普通二叉搜索树的增删查改平均时间复杂度通常表述为 O(N),因为最坏情况下的线性退化是不可忽视的风险。
三、为什么需要平衡二叉搜索树?
显然,O(N)O(N) 的效率在处理海量内存数据时是无法接受的。对比传统的二分查找算法,两者之间的权衡更能体现平衡树的独特价值:
| 对比项 | 二分查找(有序数组) | 普通二叉搜索树 | 平衡二叉搜索树 |
|---|---|---|---|
| 查找效率 | O(logN)O(logN) | O(N)O(N) | O(logN)O(logN) |
| 插入/删除 | O(N)O(N) (需挪动数据) | O(N)O(N) (可能退化为链表) | O(logN)O(logN) |
| 存储要求 | 必须支持随机访问且连续存储 | 链式存储,内存离散 | 链式存储,内存离散 |
结论与展望 :
二分查找虽查找快,但插入删除代价高昂;普通 BST 虽插入删除灵活,但查找稳定性差。为了兼顾 高效的查找 与 高效的动态修改(插入/删除) ,我们需要一种能够自我调节、防止退化的特殊 BST------平衡二叉搜索树。后续我们将深入探讨 AVL 树和红黑树,它们是解决这一痛点、支撑起现代软件底层数据存储的关键技术。
四、二叉搜索树核心操作
1. 插入操作
插入新结点的过程如下:
-
若树为空,则直接创建新结点作为根结点。
-
若树不为空,则从根结点开始:
-
若插入值小于当前结点值,则向左子树移动;
-
若插入值大于当前结点值,则向右子树移动;
-
若支持插入相等值,可以统一规定向左或向右(保持逻辑一致性),直到找到空位插入。
-
cpp
int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};
依次插入后得到的 BST 结构如下:
cpp
8
/ \
3 10
/ \ \
1 6 14
/ \ /
4 7 13
C++ 实现(插入):
cpp
Node* insert(Node* root, int val) {
if (!root) return new Node(val);
if (val < root->val)
root->left = insert(root->left, val);
else if (val > root->val)
root->right = insert(root->right, val);
// 若支持相等值,可在此处处理
return root;
}
2. 查找操作
查找是否包含某个值:
-
从根开始,若等于当前结点则返回;
-
小于则向左,大于则向右;
-
若走到空结点,则不存在。
cpp
bool search(Node* root, int val) {
if (!root) return false;
if (val == root->val) return true;
if (val < root->val)
return search(root->left, val);
else
return search(root->right, val);
}
3. 删除操作(重点)
删除操作相对复杂,需分三种情况:
-
叶子结点:直接删除。
-
只有一个子结点:用子结点替换当前结点。
-
有两个子结点:
-
找到右子树中的最小结点(或左子树中的最大结点);
-
用该结点值替换当前结点值;
-
递归删除那个替换结点。
-
cpp
Node* remove(Node* root, int val) {
if (!root) return nullptr;
if (val < root->val)
root->left = remove(root->left, val);
else if (val > root->val)
root->right = remove(root->right, val);
else {
// 找到要删除的结点
if (!root->left) {
Node* rightChild = root->right;
delete root;
return rightChild;
}
if (!root->right) {
Node* leftChild = root->left;
delete root;
return leftChild;
}
// 有两个孩子:找右子树最小结点
Node* minNode = findMin(root->right);
root->val = minNode->val;
root->right = remove(root->right, minNode->val);
}
return root;
}