二叉搜索树的概念
二叉搜索树 又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
核心性质
- 若它的左子树不为空,则左子树上所有结点的值都小于等于根结点的值。
- 若它的右子树不为空,则右子树上所有结点的值都大于等于根结点的值。
- 它的左右子树也分别为二叉搜索树。
关于重复值的说明
二叉搜索树中可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义。
后续我们学习 map/set/multimap/multiset 系列容器底层就是二叉搜索树:
- map / set:不支持插入相等值。
- multimap / multiset :支持插入相等值。

二叉搜索树的性能分析
高度与时间复杂度
-
最优情况
二叉搜索树为完全二叉树(或接近完全二叉树),其高度为:
log2Nlog_2Nlog2N此时,增删查改的时间复杂度为 O(log₂N)。
-
最差情况
二叉搜索树退化为单支树(或类似单支),其高度为:
NNN此时,增删查改的时间复杂度退化为 O(N) 。

综合效率与优化方向
综合而言,普通二叉搜索树的增删查改时间复杂度为 O(N)。这样的效率显然无法满足我们的需求。
因此,我们需要学习二叉搜索树的变形------平衡二叉搜索树 ,例如 AVL树 和 红黑树,它们才能适用于我们在内存中存储和搜索数据。
与二分查找的对比
另外需要说明的是,二分查找也可以实现 O(log₂N) 级别的查找效率,但它有两大缺陷:
- 存储限制:需要存储在支持下标随机访问的结构中,并且数据必须有序。
- 增删效率低:插入和删除数据效率很低,因为在此类结构中操作通常需要挪动大量数据。
这也恰恰体现了平衡二叉搜索树的价值:它既能保持高效的查找性能,又能相对高效地进行插入和删除操作。
二叉搜索树的插入
插入的具体过程
-
树为空
若树为空,则直接新增结点,赋值给
root指针。 -
树不空
若树不空,按二叉搜索树的性质进行查找:
- 插入值 大于 当前结点的值:向 右 走。
- 插入值 小于 当前结点的值:向 左 走。
- 找到空位置后,插入新结点。
-
处理相等的值
如果支持插入相等的值,当插入值与当前结点的值相等时,可以选择向 右 走,也可以选择向 左 走,找到空位置后插入新结点。
注意:要保持逻辑一致性,即在整个插入过程中,对于相等值的处理方向必须统一(不要一会儿往右走,一会儿往左走)。
二叉搜索树的删除
首先查找元素是否在二叉搜索树中,如果不存在,则返回 false。
如果查找元素存在,则分以下四种情况分别处理(假设要删除的结点为 N):
四种情况
- 要删除结点 N 左右孩子均为空。
- 要删除的结点 N 左孩子为空,右孩子结点不为空。
- 要删除的结点 N 右孩子为空,左孩子结点不为空。
- 要删除的结点 N 左右孩子结点均不为空。
解决方案
-
情况一
把 N 结点的父结点对应孩子指针指向空,直接删除 N 结点。
注:情况 1 可以当成情况 2 或者情况 3 处理,效果是一样的。
-
情况二
把 N 结点的父结点对应孩子指针指向 N 的右孩子,直接删除 N 结点。
-
情况三
把 N 结点的父结点对应孩子指针指向 N 的左孩子,直接删除 N 结点。
-
情况四
无法直接删除 N 结点,因为 N 的两个孩子无处安放,只能用替换法删除。
- 步骤 :
- 找 N 左子树的值最大结点 R(最右结点),或者 N 右子树的值最小结点 R(最左结点)。
- 用 R 替代 N。因为这两个结点中任意一个,放到 N 的位置,都满足二叉搜索树的规则。
- "替代 N"的意思就是将 N 和 R 两个结点的值交换。
- 转而变成删除 R 结点,此时 R 结点符合情况 2 或情况 3,可以直接删除。
- 步骤 :
cpp
#include<iostream>
using namespace std;
template<class K, class V>
struct BSTreeNode
{
K _key;
V _value;
BSTreeNode* _right;
BSTreeNode* _left;
BSTreeNode(const K& key, const V& value) :_key(key), _value(value), _right(nullptr), _left(nullptr){ }
};
template<class K, class V>
class BSTree
{
typedef BSTreeNode<K, V> Node;
public:
BSTree(BSTreeNode<K, V>* root=nullptr):_root(root)
{
}
bool Insert(const K& key, const V& value)
{
if (_root == nullptr)
{
_root = new Node(key,value);
return true;
}
else
{
Node* cur = _root;
Node* parent = cur;
while (cur)
{
parent = cur;
if (key > cur->_key)
{
cur = cur->_right;
}
else if (key < cur->_key)
{
cur = cur->_left;
}
else return false;
}
Node* newnode = new Node(key, value);
if (key > parent->_key)
{
parent->_right = newnode;
}
else
{
parent->_left = newnode;
}
}
return true;
}
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (key > cur->_key)
{
cur = cur->_right;
}
else if (key < cur->_key)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
}
bool Erase(const K& key)//函数还没实现完,并且现在逻辑有问题
{
Node* cur = _root;
Node* parent = nullptr;
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 (cur == _root)
{
_root = cur->_right;
}
else
{
if (parent->_left == cur)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
}
//如果右为空
else if (cur->_right == nullptr)
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
if (parent->_left == cur)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
delete cur;
}
//如果要删除节点左右都不为空,此处统一使用被删除节点的左边最大节点来代替该节点(替换值和关键词,而不是直接替换节点)。
else
{
Node* replace = cur;
Node* replaceparent = cur;//replaceparent 必须初始化为 cur,而不是 nullptr。因为如果没进循环,说明 replace 就是 cur 的右孩子,其父节点就是 cur。
replace = replace->_right;//寻找右子树的最左节点(比当前节点值大,但是比当前右树剩下节点值都要小,才能代替)
while (replace->_left)
{
replaceparent = replace;
replace = replace->_left;
}
cur->_key = replace->_key;
cur->_value = replace->_value;
if (replaceparent == cur)
{
replaceparent->_right = replace->_right;
}
else
{
replaceparent->_left = replace->_right;//把替代节点的右子树挂在其父节点的左子树,因为替代节点无左节点,不检查replace有无子节点是因为可以直接挂空.
}
delete replace;
}
return true;
}
}
return false;
}
void _InOrder(Node* root)
{
if (root == nullptr)return;
_InOrder(root->_left);
cout << "key:" << root->_key << " " << "value:" << root->_value;
_InOrder(root->_right);
}
void InOrder()//外部接口
{
_InOrder(_root);
}
private:
Node* _root = nullptr;
};
void TestBSTree()
{
BSTree<string, string> dict;
dict.Insert("insert", "插入");
dict.Insert("erase", "删除");
dict.Insert("left", "左边");
dict.Insert("string", "字符串");
string str;
while (cin >> str)
{
auto ret = dict.Find(str);
if (ret)
{
cout << str << ":" << ret->_value << endl;
}
else
{
cout << "单词拼写错误" << endl;
}
}
string strs[] = { "苹果", "西瓜", "苹果", "樱桃", "苹果", "樱桃", "苹果", "樱桃", "苹果" };
// 统计水果出现的次
BSTree<string, int> countTree;
for (auto str : strs)
{
auto ret = countTree.Find(str);
if (ret == NULL)
{
countTree.Insert(str, 1);
}
else
{
ret->_value++;
}
}
countTree.InOrder();
}