二叉搜索树详解:概念、增删查与 key/value 应用(C++ 实现)
1. 引言:为什么需要二叉搜索树
先看一组朴素的数据结构对比:
- 有序数组:二分查找很快,O(log₂N)。但插入、删除需要搬移大量数据,效率极低。
- 链表:插入、删除很快,但查找要 O(N),无法使用"二分"思想。
有没有一种结构,既能高效查找,又能高效插入删除?
二叉搜索树(Binary Search Tree,BST)应运而生。它把"二分思想"迁移到链式结构上,是后续平衡树(AVL、红黑树)以及 STL 中 map / set / multimap / multiset 的底层基础。
2. 二叉搜索树的概念与性质
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有结点的值都 ≤ 根结点的值;
- 若它的右子树不为空,则右子树上所有结点的值都 ≥ 根结点的值;
- 它的左右子树也分别为二叉搜索树。
由性质可知:对二叉搜索树进行中序遍历,得到的一定是有序序列,所以它也常被叫做"二叉排序树"。
关于"相等值":二叉搜索树中可以支持插入相等的值,也可以不支持,具体看使用场景定义。这正是 STL 四兄弟的差异所在:
| 容器 | 是否允许重复 key |
|---|---|
map / set | 不允许,去重 |
multimap / multiset | 允许,不去重 |
int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};
按这个序列构建出的二叉搜索树如下(中序遍历结果为 1 3 4 6 7 8 10 13 14,恰好有序):
8
/ \
3 10
/ \ \
1 6 14
/ \ /
4 7 13
3. 性能分析
查找、插入、删除的效率都取决于树的高度:
- 最优情况:树为完全二叉树(或接近完全二叉树),高度为 log₂N,此时增删查效率为 O(log₂N)。
- 最差情况:树退化成单支树(或类似单支),高度为 N,此时增删查退化为 O(N)。
最优:完全二叉树 最差:单支树
8 8
/ \ \
3 10 10
/ \ \ \
1 6 14 14
...
因此二叉搜索树增删查改的综合时间复杂度为 O(N)。这样的效率显然无法满足海量数据存储搜索的需求,所以后续课程还会继续讲解二叉搜索树的变形 —— 平衡二叉搜索树(AVL 树)和红黑树,才能可靠地用于在内存中存储和搜索数据。
顺便聊聊二分查找的缺陷:二分查找虽然也能达到 O(log₂N) 的查找效率,但它有两个致命缺陷:
- 需要存储在支持下标随机访问的结构中,并且数据有序;
- 插入和删除效率很低,因为在这种结构中增删一般需要挪动数据。
平衡二叉搜索树既保留了二分查找的效率,又解决了插入删除低效的问题,这正是它的价值所在。
4. 查找
查找过程非常直观:
- 从根结点开始比较,查找 x:
- x 比根的值大,则往右边走;
- x 比根的值小,则往左边走。
- 最多查找高度次;走到空还没找到,说明该值不存在。
- 若不支持插入相等值,找到 x 即可返回。
- 若支持插入相等值,意味着可能有多个 x,一般要求查找中序遍历的第一个 x。例如下图查找 3,要返回左子树中那一个 3(而非更早到达的 3)。
5. 插入
插入的具体过程:
- 树为空:直接新增结点,赋值给
root指针。 - 树不为空:按二叉搜索树性质走 —— 插入值比当前结点大往右走,小往左走,找到空位置插入新结点。
- 支持相等值的情况:插入值跟当前结点相等时,可以往右走也可以往左走(要保持逻辑一致,不要一会往右一会往左)。
以 {8, 3, 1, 10, 6, 4, 7, 14, 13} 为例,一步步构建(最终形态见第 2 节):
8 → 3(左) → 1(3左) → 10(8右) → 6(3右) → 4(6左) → 7(6右) → 14(10右) → 13(14左)
6. 删除(难点)
删除是二叉搜索树最复杂的操作。首先查找元素是否在树中,不存在则返回 false;若存在,假设要删除的结点为 N,分四种情况处理:
| 情况 | 描述 |
|---|---|
| 1 | N 的左右孩子均为空 |
| 2 | N 的左孩子为空,右孩子不为空 |
| 3 | N 的右孩子为空,左孩子不为空 |
| 4 | N 的左右孩子均不为空 |
对应解决方案:
- 情况 1:把 N 的父亲对应孩子指针置空,直接删除 N(实际可当作情况 2 或 3 处理,效果一样)。
- 情况 2:把 N 的父亲对应孩子指针指向 N 的右孩子,直接删除 N。
- 情况 3:把 N 的父亲对应孩子指针指向 N 的左孩子,直接删除 N。
- 情况 4:无法直接删除 N,因为它的两个儿子无处安放,只能使用替换法:
- 找 N 左子树的最大结点(最右结点)或 N 右子树的最小结点(最左结点)R 替代 N —— 这两个结点中的任意一个放到 N 的位置,都仍满足二叉搜索树规则;
- "替代"即交换 N 与 R 的值,转而删除 R 结点;
- 由于 R 必然符合情况 2 或情况 3,可以"直接删除"。
易错点提示:当右子树的根结点本身就是右子树最小结点时(例如删除根结点 8,其右子树最小结点就是 10,此时 rightMinP 必须初始化为 cur),处理时要格外小心,否则会出现指针悬空报错。代码注释里也专门标注了这一点。
7. 完整代码实现(key 版)
template<class K>
struct BSTNode
{
K _key;
BSTNode<K>* _left;
BSTNode<K>* _right;
BSTNode(const K& key)
: _key(key)
, _left(nullptr)
, _right(nullptr)
{}
};
// Binary Search Tree
template<class K>
class BSTree
{
typedef BSTNode<K> Node;
public:
bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else
{
return false; // 默认不支持插入相等值
}
}
cur = new Node(key);
if (parent->_key < key)
parent->_right = cur;
else
parent->_left = cur;
return true;
}
bool Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
cur = cur->_right;
else if (cur->_key > key)
cur = cur->_left;
else
return true;
}
return false;
}
bool Erase(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else
{
// 0~1 个孩子的情况:情况 1/2/3 均可直接删除,改变父亲对应孩子指针指向即可
if (cur->_left == nullptr)
{
if (parent == nullptr)
_root = cur->_right;
else
{
if (parent->_left == cur)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
delete cur;
return true;
}
else if (cur->_right == nullptr)
{
if (parent == nullptr)
_root = cur->_left;
else
{
if (parent->_left == cur)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
delete cur;
return true;
}
else
{
// 两个孩子的情况:替换法删除
// 这里取右子树的最小结点作为替代结点
// 注意:右子树根就是最小结点的情况,rightMinP 一定要初始化为 cur
Node* rightMinP = cur;
Node* rightMin = cur->_right;
while (rightMin->_left)
{
rightMinP = rightMin;
rightMin = rightMin->_left;
}
cur->_key = rightMin->_key;
if (rightMinP->_left == rightMin)
rightMinP->_left = rightMin->_right;
else
rightMinP->_right = rightMin->_right;
delete rightMin;
return true;
}
}
}
return false;
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
private:
Node* _root = nullptr;
};
提示:实际工程中还应补全拷贝构造、赋值运算符重载和析构函数(后文 key/value 版给出了完整写法,可参考)。
8. key 与 key/value 使用场景
8.1 key 搜索场景
只有 key 作为关键码,结构中只存储 key,搜索场景只需判断 key 在不在。key 搜索树支持增删查,不支持修改(修改 key 会破坏搜索树结构)。
场景 1:小区无人值守车库。物业把购买车位的业主车牌号录入后台系统,车辆进入时扫描车牌,在系统中则抬杆放行,不在则提示非本小区车辆。
场景 2:英文文章拼写检查。将词库中所有单词放入二叉搜索树,读取文章中的单词逐一查找,不在树中的单词就用波浪线标红提示。
8.2 key/value 搜索场景
每个关键码 key 都有对应的 value,value 可以是任意类型对象。结点中除了 key 还要存 value,增删查仍以 key 为关键字走二叉搜索树规则。key/value 树支持修改,但只允许修改 value,不能修改 key。
场景 1:简单中英互译字典。结点存 key(英文)和 value(中文),输入英文即可查到对应中文。
场景 2:商场无人值守车库。入场扫描车牌,记录车牌和入场时间;出场扫描车牌,查找入场时间,用当前时间减入场时间算出停车时长与费用,缴费后抬杆。
场景 3:统计一篇文章中单词出现次数。读到一个单词先查找:不存在说明首次出现,插入 (单词, 1);已存在则对应次数 ++。
8.3 key/value 二叉搜索树代码实现
template<class K, class V>
struct BSTNode
{
K _key;
V _value;
BSTNode<K, V>* _left;
BSTNode<K, V>* _right;
BSTNode(const K& key, const V& value)
: _key(key)
, _value(value)
, _left(nullptr)
, _right(nullptr)
{}
};
template<class K, class V>
class BSTree
{
typedef BSTNode<K, V> Node;
public:
BSTree() = default;
BSTree(const BSTree<K, V>& t)
{
_root = Copy(t._root);
}
BSTree<K, V>& operator=(BSTree<K, V> t)
{
swap(_root, t._root);
return *this;
}
~BSTree()
{
Destroy(_root);
_root = nullptr;
}
bool Insert(const K& key, const V& value)
{
if (_root == nullptr)
{
_root = new Node(key, value);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(key, value);
if (parent->_key < key)
parent->_right = cur;
else
parent->_left = cur;
return true;
}
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
cur = cur->_right;
else if (cur->_key > key)
cur = cur->_left;
else
return cur;
}
return nullptr;
}
bool Erase(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else
{
if (cur->_left == nullptr)
{
if (parent == nullptr)
_root = cur->_right;
else
{
if (parent->_left == cur)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
delete cur;
return true;
}
else if (cur->_right == nullptr)
{
if (parent == nullptr)
_root = cur->_left;
else
{
if (parent->_left == cur)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
delete cur;
return true;
}
else
{
Node* rightMinP = cur;
Node* rightMin = cur->_right;
while (rightMin->_left)
{
rightMinP = rightMin;
rightMin = rightMin->_left;
}
cur->_key = rightMin->_key;
if (rightMinP->_left == rightMin)
rightMinP->_left = rightMin->_right;
else
rightMinP->_right = rightMin->_right;
delete rightMin;
return true;
}
}
}
return false;
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << ":" << root->_value << endl;
_InOrder(root->_right);
}
void Destroy(Node* root)
{
if (root == nullptr)
return;
Destroy(root->_left);
Destroy(root->_right);
delete root;
}
Node* Copy(Node* root)
{
if (root == nullptr)
return nullptr;
Node* newRoot = new Node(root->_key, root->_value);
newRoot->_left = Copy(root->_left);
newRoot->_right = Copy(root->_right);
return newRoot;
}
private:
Node* _root = nullptr;
};
测试一:中英互译字典
int main()
{
BSTree<string, string> dict;
dict.Insert("left", "左边");
dict.Insert("right", "右边");
dict.Insert("insert", "插入");
dict.Insert("string", "字符串");
string str;
while (cin >> str)
{
auto ret = dict.Find(str);
if (ret)
cout << "->" << ret->_value << endl;
else
cout << "无此单词,请重新输入" << endl;
}
return 0;
}
测试二:统计水果出现次数
int main()
{
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果",
"苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
BSTree<string, int> countTree;
for (const auto& str : arr)
{
// 1. 不在树中:说明第一次出现,插入 <水果, 1>
// 2. 在树中:查找到的结点次数++
auto ret = countTree.Find(str);
if (ret == nullptr)
countTree.Insert(str, 1);
else
ret->_value++;
}
countTree.InOrder(); // 按键有序输出,且带出现次数
return 0;
}
9. 总结
- 二叉搜索树把二分思想迁移到链式结构,查找、插入、删除平均 O(log₂N),但最坏会退化为 O(N) 的单支树。
- 中序遍历即得有序序列,这是它叫"排序树"的由来。
- 不支持相等值 → 对应
map/set;支持相等值 → 对应multimap/multiset。 - key 搜索解决"在不在";key/value 搜索解决"key 对应的 value 是什么",两者都不允许修改 key。

1150

被折叠的 条评论
为什么被折叠?



