二叉搜索树(Binary Search Tree,简称 BST)是一种兼具 “有序性” 和 “高效检索” 特性的二叉树结构,它通过特定的节点值分布规则,实现了插入、查找、删除操作的高效执行(最优时间复杂度 O (log₂N))。在 STL 中,map、set、multimap、multiset 等容器的底层均基于二叉搜索树的变形(平衡二叉树)实现。本文将从 “概念→性能→核心操作→实战实现” 的路径,系统讲解二叉搜索树的核心原理,帮你彻底掌握这一基础数据结构。
一、二叉搜索树的概念:有序性是核心
二叉搜索树(也称二叉排序树)是一种满足以下规则的二叉树:
- 若左子树非空,则左子树中所有节点的值 ≤ 根节点的值;
- 若右子树非空,则右子树中所有节点的值 ≥ 根节点的值;
- 左、右子树也分别是二叉搜索树(递归定义)。
关键说明:
相等值的处理:根据场景需求,相等值可插入左子树或右子树(需保持逻辑一致)。例如 STL 的 set/map 不允许插入相等值,而 multiset/multimap 允许;
中序遍历特性:二叉搜索树的中序遍历结果是严格递增的(若相等值插入右子树)或非递减的(若相等值插入左子树)—— 这是判断一棵二叉树是否为 BST 的重要依据。
示例(中序遍历验证):
8
/ \
3 10
/ \ \
1 6 14
/ \ /
4 7 13
中序遍历结果:1 3 4 6 7 8 10 13 14(严格递增),符合 BST 特性。
二、二叉搜索树的性能分析:最优与最差场景
二叉搜索树的性能取决于树的 “高度”—— 高度越小,操作效率越高。
2.1 最优情况:接近完全二叉树
当插入的节点值随机时,BST 会形成接近完全二叉树的结构,树的高度为 log₂N(N 为节点总数)。此时插入、查找、删除操作的时间复杂度均为 O(log₂N),与二分查找效率相当。
2.2 最差情况:退化为单支树
当插入的节点值有序(如递增或递减)时,BST 会退化为 “单支树”(类似链表),树的高度为 N。此时所有操作的时间复杂度退化为 O(N),效率极低。
示例(有序插入退化为单支树):插入序列 1,2,3,4,5,BST 退化为右单支树:
1
\
2
\
3
\
4
\
5
2.3 BST vs 二分查找:各有优劣
二分查找虽能实现 O (log₂N) 的查找效率,但存在明显缺陷:
- 依赖支持随机访问的结构(如数组),且数据必须有序;
- 插入 / 删除效率低(需挪动大量元素,时间复杂度 O (N))。
BST 的优势在于:
无需依赖连续空间,插入 / 删除无需挪动元素;
兼顾有序性与动态操作效率(最优场景下)。
注意:普通 BST 的最差性能无法满足工业需求,因此实际应用中会使用其变形 —— 平衡二叉树(如 AVL 树、红黑树),通过自平衡机制保证树的高度始终为 O (log₂N)。
三、二叉搜索树的核心操作:插入、查找、删除
3.1 插入操作:按规则找到空位
插入的核心是 “遵循 BST 规则,找到合适的空位插入新节点”,步骤如下:
- 若树为空,直接创建根节点;
- 若树非空,从根节点开始比较:
- 插入值 > 当前节点值:向右子树移动;
- 插入值 < 当前节点值:向左子树移动;
- 插入值 == 当前节点值:根据需求决定是否插入(本文默认不允许重复,直接返回 false);
- 找到空位(当前节点为 nullptr),创建新节点,并让父节点的左 / 右指针指向新节点。
代码实现(插入):
template <class K>
struct BSTNode {
K _key;
BSTNode<K>* _left;
BSTNode<K>* _right;
BSTNode(const K& key) : _key(key), _left(nullptr), _right(nullptr) {}
};
template <class K>
class BSTree {
typedef BSTNode<K> Node;
public:
bool Insert(const K& key) {
// 情况1:树为空,创建根节点
if (_root == nullptr) {
_root = new Node(key);
return true;
}
// 情况2:树非空,找到插入位置
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 {
// 不允许重复插入,返回false
return false;
}
}
// 创建新节点,链接到父节点
cur = new Node(key);
if (parent->_key < key) {
parent->_right = cur;
} else {
parent->_left = cur;
}
return true;
}
private:
Node* _root = nullptr;
};
3.2 查找操作:按规则遍历树
查找的核心是 “根据 BST 的有序性,从根节点开始定向查找”,步骤如下:
- 从根节点开始比较;
- 查找值 > 当前节点值:向右子树移动;
- 查找值 < 当前节点值:向左子树移动;
- 查找值 == 当前节点值:返回 true(找到);
- 遍历到 nullptr 仍未找到:返回 false。
代码实现(查找):
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;
}
特殊场景:支持重复值的查找
若 BST 允许插入重复值(如 multiset),查找时需返回 “中序遍历的第一个目标值”(确保有序性)。例如查找值 3,需返回左子树中最靠右的 3:
8
/ \
3 10
/ \ \
1 6 14
\ / \ /
3 4 7 13
查找 3 时,需返回 1 的右孩子(中序第一个 3)。
3.3 删除操作:分四种情况处理
删除是 BST 最复杂的操作,需保证删除节点后,树仍满足 BST 规则。根据删除节点(N)的子节点数量,分为四种情况:
情况 1:N 的左、右子树均为空(叶子节点)
处理方式:直接删除 N,让 N 的父节点对应指针指向 nullptr。
示例:删除节点 1(叶子节点),父节点 3 的左指针改为 nullptr。
情况 2:N 的左子树为空,右子树非空
处理方式:让 N 的父节点对应指针指向 N 的右子树,直接删除 N。
示例:删除节点 10(右子树为 14),父节点 8 的右指针指向 14,删除 10。
情况 3:N 的右子树为空,左子树非空
处理方式:让 N 的父节点对应指针指向 N 的左子树,直接删除 N。
示例:删除节点 14(左子树为 13),父节点 10 的右指针指向 13,删除 14。
情况 4:N 的左、右子树均非空(核心难点)
问题:直接删除 N 会导致左、右子树无处安放,破坏 BST 结构;
解决方案:替换法删除—— 找到 N 的 “替代节点”,用替代节点的值覆盖 N 的值,再删除替代节点(替代节点必为情况 2 或 3,可直接删除)。
替代节点的选择(二选一):
N 左子树的最大值节点:左子树中最靠右的节点(如删除 3,左子树最大值为 1 的右孩子 3);
N 右子树的最小值节点:右子树中最靠左的节点(如删除 8,右子树最小值为 10 的左孩子 13)。
代码实现(删除):
bool Erase(const K& key) {
Node* parent = nullptr;
Node* cur = _root;
// 步骤1:找到要删除的节点cur
while (cur) {
if (cur->_key < key) {
parent = cur;
cur = cur->_right;
} else if (cur->_key > key) {
parent = cur;
cur = cur->_left;
} else {
// 步骤2:分情况删除cur
// 情况2:左子树为空,右子树继位
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;
}
// 情况3:右子树为空,左子树继位
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;
}
// 情况4:左右子树均非空,替换法删除(选右子树最小值)
else {
// 找到右子树的最小值节点(最靠左的节点)
Node* rightMinP = cur; // 替代节点的父节点
Node* rightMin = cur->_right;
while (rightMin->_left) {
rightMinP = rightMin;
rightMin = rightMin->_left;
}
// 用替代节点的值覆盖cur的值
cur->_key = rightMin->_key;
// 删除替代节点(rightMin必为情况2或3)
if (rightMinP->_left == rightMin) {
rightMinP->_left = rightMin->_right;
} else {
rightMinP->_right = rightMin->_right;
}
delete rightMin;
return true;
}
}
}
// 未找到要删除的节点
return false;
}
四、二叉搜索树的两种应用场景:Key 与 Key-Value
BST 根据存储数据的类型,分为 “仅存储 Key” 和 “存储 Key-Value 键值对” 两种场景,分别对应不同的业务需求。
4.1 Key 场景:仅需判断 “存在性”
核心需求:仅需存储关键码(Key),判断某个 Key 是否存在(无关联值)。特点:不支持修改 Key(修改会破坏 BST 结构),仅支持增、删、查。
典型场景:
- 小区车库车牌验证:将业主车牌作为 Key 存入 BST,车辆进入时扫描车牌,查找是否存在(存在则抬杆);
- 单词拼写检查:将词库单词作为 Key 存入 BST,读取文章单词时查找是否存在(不存在则标红)。
4.2 Key-Value 场景:需关联 “数据与值”
核心需求:存储 Key 与关联值 Value(如英文单词 - 中文释义、车牌 - 入场时间),通过 Key 快速查找 Value。特点:不支持修改 Key(破坏结构),但支持修改 Value;增、删、查均以 Key 为依据。
典型场景:
- 中英字典:Key 为英文单词,Value 为中文释义,输入英文可快速查找中文;
- 停车费计算:Key 为车牌,Value 为入场时间,出场时查找 Value,计算停车时长与费用;
- 单词频次统计:Key 为单词,Value 为出现次数,读取单词时若存在则 Value++,否则插入 <单词,1>。
代码实现(Key-Value 版 BST):
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:
// 插入Key-Value
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 {
// 不允许重复Key
return false;
}
}
cur = new Node(key, value);
if (parent->_key < key) {
parent->_right = cur;
} else {
parent->_left = cur;
}
return true;
}
// 查找Key对应的节点(返回节点指针,可修改Value)
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;
}
// 中序遍历(输出Key-Value,验证有序性)
void InOrder() {
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node* root) {
if (root == nullptr) return;
_InOrder(root->_left);
cout << root->_key << ":" << root->_value << " ";
_InOrder(root->_right);
}
private:
Node* _root = nullptr;
};
实战示例:单词频次统计
int main() {
string arr[] = {"苹果", "西瓜", "苹果", "西瓜", "苹果", "香蕉"};
BSTree<string, int> countTree;
for (const auto& str : arr) {
Node* ret = countTree.Find(str);
if (ret == nullptr) {
// 第一次出现,插入<单词, 1>
countTree.Insert(str, 1);
} else {
// 已出现,频次+1
ret->_value++;
}
}
// 中序遍历输出(有序且带频次)
countTree.InOrder(); // 输出:苹果:3 西瓜:2 香蕉:1
return 0;
}
五、总结
二叉搜索树是一种基于 “有序性” 设计的基础数据结构,其核心要点如下:
- 概念:左子树值≤根≤右子树,中序遍历有序;
- 性能:最优 O (log₂N)(接近完全二叉树),最差 O (N)(单支树);
- 操作:
- 插入:按规则找空位,链接新节点;
- 查找:按规则定向遍历,判断存在性;
- 删除:分四种情况,左右子树均非空时用替代法;
- 场景:Key 场景(判断存在)、Key-Value 场景(关联数据)。
177万+

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



