文章目录
- [1. 二叉搜索树的概念](#1. 二叉搜索树的概念)
- [2. 二叉搜索树的性能分析](#2. 二叉搜索树的性能分析)
- [3. 二叉搜索树的插入](#3. 二叉搜索树的插入)
- 4.二叉搜索树的常见功能
- [5. 二叉搜索树key和key/value使用场景](#5. 二叉搜索树key和key/value使用场景)
-
- [5.1 key搜索场景](#5.1 key搜索场景)
- [5.2 key/value搜索场景:](#5.2 key/value搜索场景:)
- [5.3 key/value二叉搜索树代码实现](#5.3 key/value二叉搜索树代码实现)
1. 二叉搜索树的概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有结点的值都小于等于根结点的值
- 若它的右子树不为空,则右子树上所有结点的值都大于等于根结点的值
- 它的左右子树也分别为二叉搜索树
- 二叉搜索树中可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义,后续我们学习
map/set/multimap/multiset系列容器底层就是二叉搜索树,其中map/set不支持插入相等值,multimap/multiset支持插入相等值

cpp
//节点
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;
private:
Node* root=nullptr;
};
2. 二叉搜索树的性能分析
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其高度为: log 2 N \log_2 N log2N
最差情况下,二叉搜索树退化为单支树(或者类似单支),其高度为: N N N
所以综合而言二叉搜索树增删查改时间复杂度为: O ( N ) O(N) O(N)
那么这样的效率显然是无法满足我们需求的,我们后期需要二叉搜索树的变形,平衡二叉搜索树AVL树 和红黑树,才能适用于我们在内存中存储和搜索数据。
另外需要说明的是,二分查找也可以实现 O ( log 2 N ) O(\log_2 N) O(log2N)级别的查找效率,但是二分查找有两大缺陷:
- 需要存储在支持下标随机访问的结构中,并且有序。
- 插入和删除数据效率很低,因为存储在下标随机访问的结构中,插入和删除数据一般需要挪动数据。
这里也就体现出了平衡二叉搜索树的价值。

3. 二叉搜索树的插入
插入的具体过程如下:
- 树为空,则直接新增结点,赋值给
root指针 - 树不空,按二叉搜索树性质,插入值比当前结点大往右走,插入值比当前结点小往左走,找到空位置,插入新结点。
- 如果支持插入相等的值,插入值跟当前结点相等的值可以往右走,也可以往左走,找到空位置,插入新结点。(要注意的是要保持逻辑一致性,插入相等的值不要一会往右走,一会往左走)

int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};
我们这里实现不支持重复数据的版本:
cpp
bool Insert(const K& key)
{
//根节点单独考虑
if(_root==nullptr)
{
_root=new Node(key);
return true;
}
Node* parent = nullptr;//记录cur的父节点
Node* cur = _root;//临时节点 用于遍历的
while (cur)
{
//当节点数据大的时候,转移到左子树,parent用于记录节点
if (cur->_key > key)
{
parent=cur;
cur=cur->_left;
}
//当节点数据小的时候,转移到右子树,parent用于记录节点
else if (cur->_key < key)
{
parent=cur;
cur=cur->_right;
}
else
{
return false //当数据相同,插入失败
}
}
//创建新节点
cur=new BSTNode(key);
//用新节点和parent节点数据比较,判断是在parent节点左边还是右边
if (cur->_key>parent->_key)
{
parent->_right=cur;
}
else
{
parent->_left=cur;
}
return true;
}
4.二叉搜索树的常见功能
4.1 遍历
4.1.1 非递归实现
遍历我们选择中序遍历,因为可以正好让树的数据以升序的形式遍历。
但是这里我们传统的写法会有一些问题,因为我们的_root是private成员,无法访问,所以就没法传根节点 。对此,我们可以通过函数套用函数的形式实现(因为_root在类里面可以调用。)
cpp
void InOrder()
{
_InOrder(_root);
}
//中序遍历
void _InOrder(Node* root)
{
if (root==nullptr)
{
return;
}
_InOrder(root->_left);
cout<<root->_key<<" ";
_InOrder(root->_right);
}
4.1.2 递归实现
cpp
private:
bool _Insert(Node*& root, const k& x)//Node*& root:使用指针引用,确保递归过程中对root的修改(如新建结点赋值)能同步到上层父结点的孩子指针;
{
if (root == nullptr)
{
root = new Node(x);
return true;
}
if (root->_key < x)
return _Insert(root->_right, x);
else if (root->_key > x)
return _Insert(root->_left, x);
else
return false;
}
public:
bool InsertR(const K& key)
{
return _Insert(_root, key);
}
- _Insert(私有递归函数):核心插入逻辑,接收当前子树根节点指针引用 + 待插入键值,递归查找插入位置;
- InsertR(公有接口):对外暴露的插入接口,调用私有递归函数,传入整棵树的根节点_root。
4.2 二叉搜索树的查找
- 从根开始比较,查找
x,x比根的值大 则往右边走查找,x比根值小则往左边走查找。 - 最多查找高度次,走到到空,还没找到,这个值不存在。
- 如果不支持插入相等的值,找到x即可返回
- 如果支持插入相等的值,意味着有多个
x存在,一般要求查找中序的第一个x。如下图,查找3,要找到1的右孩子的那个3返回。
cpp
//查找
bool 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 true;
}
}
return false;
}
4.3 二叉搜索树的删除
首先查找元素是否在二叉搜索树中,如果不存在,则返回false。
如果查找元素存在则分以下四种情况分别处理:(假设要删除的结点为N)
- 要删除结点N左右孩子均为空
- 要删除的结点N左孩子位空,右孩子结点不为空
- 要删除的结点N右孩子位空,左孩子结点不为空
- 要删除的结点N左右孩子结点均不为空
对应以上四种情况的解决方案:
- 把N结点的父亲对应孩子指针指向空,直接删除N结点(情况1可以当成2或者3处理,效果是一样的)
示例 :删除结点
1(左右孩子均空)
原树结构(简化):
8
/ \
3 10
/ \ \
1 6 14
/ \ /
4 7 13
处理流程:
1. 找到结点1的父结点(3);
2. 将父结点3的左孩子指针置空;
3. 删除结点1。
删除后结构:
8
/ \
3 10
\ \
6 14
/ \ /
4 7 13
- 把N结点的父亲对应孩子指针指向N的右孩子,直接删除N结点
示例 :删除结点
10(左空、右孩子是14)
原树结构(简化):
8
/ \
3 10
\ \
6 14
/ \ /
4 7 13
处理流程:
1. 找到结点10的父结点(8);
2. 将父结点8的右孩子指针指向10的右孩子(14);
3. 删除结点10。
删除后结构:
8
/ \
3 14
\ /
6 13
/ \
4 7
- 把N结点的父亲对应孩子指针指向N的左孩子,直接删除N结点
示例 :删除结点
6(右空、左孩子是4)
原树结构(简化):
8
/ \
3 10
/ \ \
1 6 14
/ /
4 13
处理流程:
1. 找到结点6的父结点(3);
2. 将父结点3的右孩子指针指向6的左孩子(4);
3. 删除结点6。
删除后结构:
8
/ \
3 10
/ \ \
1 4 14
/
13
- 无法直接删除N结点,因为N的两个孩子无处安放,只能用替换法删除。找N左子树的值最大结点R(最右结点)或者N右子树的值最小结点R(最左结点)替代N,因为这两个结点中任意一个,放到N的位置,都满足二叉搜索树的规则。替代N的意思就是N和R的两个结点的值交换,转而变成删除R结点,R结点符合情况2或情况3,可以直接删除。
示例 :删除根结点
8(左右孩子均非空,以交换左树最大值为例)
8 (待删除结点N)
/ \
3 10
/ \ \
1 5 14
/ \ /
4 7 13
/
6
3 的右孩子是 5 → 5 的右孩子是 7 → 7 的右孩子为空,因此 R=7(左子树最大值结点)
仅交换值,结点的父子关系不变,交换后树结构变为:
7 (原8的位置,值替换为7)
/ \
3 10
/ \ \
1 5 14
/ \ /
4 8 13 (原7的位置,值替换为8)
/
6
此时 R 结点(值 8)的特征:左孩子为 6、右孩子为空(符合 "右空、左非空" 的情况 3):
1. 找到 R 的父结点:5;
2. 将父结点 5 的右孩子指针指向 R 的左孩子 6;
3. 删除 R 结点(值 8)。
7
/ \
3 10
/ \ \
1 5 14
/ \ /
4 6 13
按照我们的逻辑给出的代码就是(有误):
cpp
bool Erase(const k& key)
{
Node* parent = nullptr;//记录cur父节点的
Node* cur = _root;//临时节点 用于遍历的 寻找key值位置
while (cur)
{
//当节点数据大的时候,转移到左子树,parent用于记录节点
if (cur->_key > key)
{
parent=cur;
cur=cur->_left;
}
//当节点数据小的时候,转移到右子树,parent用于记录节点
else if (cur->_key < key)
{
parent=cur;
cur=cur->_right;
}
else
{
//寻找成功开始删除
//左为空
if (cur->_left==nullptr)
{ //排出
//看看cur在父亲节点的哪一边
if (parent->_left==cur)
{
parent->_left=cur->_right;
}
else
{
parent->_right=cur->_right;
}
delete cur;
}
//右为空
else if (cur->_right==nullptr)
{ //排出
//看看cur在父亲节点的哪一边
if (parent->_left==cur)
{
parent->_left=cur->_left;
}
else
{
parent->_right=cur->_left;
}
delete cur;
}
else //左右都不为空,找子树中合适的节点代替 这里以替换左树最大值为例子
{
Node* maxleftparent=cur;//记录最大节点父亲节点 因为左子树最大节点 有一个左子树 需要删除
Node* maxleft=cur->_left;
while(maxleft->_right)
{
maxleftparent=maxleft;
maxleft=maxleft->_right;
}
//删除节点值于左树最大值 交换
swap(maxleft->_key, cur->_key);
//
maxleftparent->_right=maxleft->_left;
delete maxleft;
}
}
return true;
}
return false;
}
删除操作的易错点
但是这个代码有几个致命bug:
bug1:
cpp
Node* maxleftparent=cur;//记录最大节点父亲节点 因为左子树最大节点 有一个左子树 需要删除
Node* maxleft=cur->_left;
while(maxleft->_right)
{
maxleftparent=maxleft;
maxleft=maxleft->_right;
}
//删除节点值于左树最大值 交换
swap(maxleft->_key, cur->_key);
//
maxleftparent->_right=maxleft->_left;
delete maxleft;
用这个树 删除8 用交换右树最小值的方法 会出现以下情况:(这次以交换右边最小值为例子)
当我们 8和10交换后 我们
minrightparent->_left=minright->_right就出事了所以我们要判断
minright是minrightparent的左树还是右树(右树就说明minrightparent是cur)修改后代码:
我们换成以交换左树最大修改后代码:
cpp
Node* maxleftparent=cur;//记录最大节点父亲节点 因为左子树最大节点 有一个左子树 需要删除
Node* maxleft=cur->_left;
while(maxleft->_right)
{
maxleftparent=maxleft;
maxleft=maxleft->_right;
}
//删除节点值于左树最大值 交换
swap(maxleft->_key, cur->_key);
//判断maxleft节点是不是cur
if(maxleftparent->_right==maxleft)
maxleftparent->_right=maxleft->_left;
else
maxleftparent->_left=maxleft->_left;
delete maxleft;
bug2:
cpp
if (cur->_left==nullptr)
{ //排出
//看看cur在父亲节点的哪一边
if (parent->_left==cur)
{
parent->_left=cur->_right;
}
else
{
parent->_right=cur->_right;
}
delete cur;
}
//右为空
else if (cur->_right==nullptr)
{ //排出
//看看cur在父亲节点的哪一边
if (parent->_left==cur)
{
parent->_left=cur->_left;
}
else
{
parent->_right=cur->_left;
}
delete cur;
}
很好我们解决了一个 其实还有一个容易忽略 当我们删除根节点时候
我们惊奇的发现 特喵根节点父亲节点为空啊 所以也得单独考虑
修改后:
cpp
//寻找成功开始删除
//左为空
//root单独判断
if (cur->_left==nullptr)
{ //排出
//root单独判断
if(cur!=_root)
{
//看看cur在父亲节点的哪一边
if (parent->_left==cur)
{
parent->_left=cur->_right;
}
else
{
parent->_right=cur->_right;
}
}
else
{
//更改_root指向
_root=cur->_right;
}
delete cur;
}
//右为空
else if (cur->_right==nullptr)
{ //排出
//root单独判断
if(cur!=_root)
{
//看看cur在父亲节点的哪一边
if (parent->_left==cur)
{
parent->_left=cur->_left;
}
else
{
parent->_right=cur->_left;
}
}
else
{
//更改_root指向
_root=cur->_left;
}
delete cur;
}
删除操作正确代码
cpp
bool Erase(const k& key)
{
Node* parent = nullptr;//记录cur父节点的
Node* cur = _root;//临时节点 用于遍历的 寻找key值位置
while (cur)
{
//当节点数据大的时候,转移到左子树,parent用于记录节点
if (cur->_key > key)
{
parent=cur;
cur=cur->_left;
}
//当节点数据小的时候,转移到右子树,parent用于记录节点
else if (cur->_key < key)
{
parent=cur;
cur=cur->_right;
}
else
{
//寻找成功开始删除
//左为空
//root单独判断
if (cur->_left==nullptr)
{ //排出
//root单独判断
if(cur!=_root)
{
//看看cur在父亲节点的哪一边
if (parent->_left==cur)
{
parent->_left=cur->_right;
}
else
{
parent->_right=cur->_right;
}
}
else
{
//更改_root指向
_root=cur->_right;
}
delete cur;
}
//右为空
else if (cur->_right==nullptr)
{ //排出
//root单独判断
if(cur!=_root)
{
//看看cur在父亲节点的哪一边
if (parent->_left==cur)
{
parent->_left=cur->_left;
}
else
{
parent->_right=cur->_left;
}
}
else
{
//更改_root指向
_root=cur->_left;
}
delete cur;
}
else //左右都不为空,找子树中合适的节点代替 这里以替换左树最大值为例子
{
Node* maxleftparent=cur;//记录最大节点父亲节点 因为左子树最大节点 有一个左子树 需要删除
Node* maxleft=cur->_left;
while(maxleft->_right)
{
maxleftparent=maxleft;
maxleft=maxleft->_right;
}
//删除节点值于左树最大值 交换
swap(maxleft->_key, cur->_key);
//判断maxleft节点是不是cur
if(maxleftparent->_right==maxleft)
maxleftparent->_right=maxleft->_left;
else
maxleftparent->_left=maxleft->_left;
delete maxleft;
}
}
return true;
}
return false;
}
5. 二叉搜索树key和key/value使用场景
5.1 key搜索场景
只有key作为关键码,结构中只需要存储key即可,关键码即为需要搜索到的值,搜索场景只需要判断key在不在。key的搜索场景实现的二叉树搜索树支持增删查,但是不支持修改,修改key破坏搜索树结构了。
场景1: 小区无人值守车库,小区车库买了车位的业主车才能进小区,那么物业会把买了车位的业主的车牌号录入后台系统,车辆进入时扫描车牌在不在系统中,在则抬杆,不在则提示非本小区车辆,无法进入。
场景2: 检查一篇英文文章单词拼写是否正确,将词库中所有单词放入二叉搜索树,读取文章中的单词,查找是否在二叉搜索树中,不在则波浪线标红提示。
5.2 key/value搜索场景:
每一个关键码key,都有与之对应的值value,value可以任意类型对象。树的结构中(结点)除了需要存储key还要存储对应的value,增/删/查还是以key为关键字走二叉搜索树的规则进行比较,可以快速查找到key对应的value。key/value的搜索场景实现的二叉树搜索树支持修改,但是不支持修改key,修改key破坏搜索树性质了,可以修改value。
场景1: 简单中英互译字典,树的结构中(结点)存储key(英文)和vlaue(中文),搜索时输入英文,则同时查找到了英文对应的中文。
场景2: 商场无人值守车库,入口进场时扫描车牌,记录车牌和入场时间,出口离场时,扫描车牌,查找入场时间,用当前时间-入场时间计算出停车时长,计算出停车费用,缴费后抬杆,车辆离场,删除入场时间和车牌。
场景3: 统计一篇文章中单词出现的次数,读取一个单词,查找单词是否存在,不存在这个说明第一次出现,(单词加入树),单词存在,则++单词对应的次数。
5.3 key/value二叉搜索树代码实现
其实整体更改并不是很大
cpp
namespace key_value
{
template<class K, class V>
struct BSTNode
{
K _key;
V _value;
// pair<K, V> _kv;
BSTNode<K, V>* _left;
BSTNode<K, V>* _right;
BSTNode(const K& key, const V& value)
:_key(key)
,_value(value)
, _left(nullptr)
, _right(nullptr)
{}
};
// key
template<class K, class V>
class BSTree
{
typedef BSTNode<K, V> Node;
public:
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)//不能只查找在不在了 要返回查找节点对应的value
{
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 (cur != _root)
{
// 左为空,父亲指向我的右
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
else
{
_root = cur->_right;
}
delete cur;
}
else if (cur->_right == nullptr)
{
if (cur != _root)
{
// 右为空,父亲指向我的左
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
else
{
_root = cur->_left;
}
delete cur;
}
else
{
// 左右都不为空,找子树中适合的节点替代我
Node* minRightParent = cur;
Node* minRight = cur->_right;
while (minRight->_left)
{
minRightParent = minRight;
minRight = minRight->_left;
}
swap(cur->_key, minRight->_key);
if (minRight == minRightParent->_left)
minRightParent->_left = minRight->_right;
else
minRightParent->_right = minRight->_right;
delete minRight;
}
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;//遍历的时候 key 和 value都输出
_InOrder(root->_right);
}
private:
Node* _root = nullptr;
};
}
例子1:实现一个小字典
效果如下:
例子2:查找水果出现次数代码和效果展示:








