目录
[一 二叉搜索树的概念](#一 二叉搜索树的概念)
[二 二叉搜索树的性能分析](#二 二叉搜索树的性能分析)
[三 二叉搜索树的插入](#三 二叉搜索树的插入)
[四 二叉搜索树的查找](#四 二叉搜索树的查找)
[五 二叉搜索树的删除(重点)](#五 二叉搜索树的删除(重点))
[六 二叉搜索树key何key/value使用场景](#六 二叉搜索树key何key/value使用场景)
[1 key搜索场景](#1 key搜索场景)
[2 key/value搜索场景](#2 key/value搜索场景)
[3 key/value⼆叉搜索树代码实现](#3 key/value⼆叉搜索树代码实现)
[4 水果类示例](#4 水果类示例)
一 二叉搜索树的概念
二叉搜索树又称二叉排序树,它要么是一棵空树,要么是具有以下性质的二叉树:
- 若其左子树不为空,则左子树上所有结点的值都小于等于根结点的值。
- 若其右子树不为空,则右子树上所有结点的值都大于等于根结点的值。
- 它的左、右子树也分别为二叉搜索树。
二叉搜索树是否支持插入相等的值,需根据具体使用场景定义。后续学习的 map、set、multimap、multiset 系列容器,其底层实现均基于二叉搜索树。其中,map 和 set 不支持插入相等值,而 multimap 和 multiset 支持插入相等值。

1 存储
2 高效搜索 查找次数:二叉树的高度
中序查找:有序的
二 二叉搜索树的性能分析
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其高度为:⌊log₂N⌋。
最差情况下,二叉搜索树退化为单支树(或者类似单支),其高度为:N。
所以综合而言,二叉搜索树增删查改的时间复杂度为:O (N) 。这样的效率显然无法满足需求,后续课程需要继续讲解二叉搜索树的变形 ------ 平衡二叉搜索树,包括 AVL 树和红黑树,它们才能适用于在内存中存储和搜索数据。
另外需要说明的是,二分查找也能实现 O (log₂N) 级别的查找效率,但二分查找有两大缺陷:
- 需要存储在支持下标随机访问的结构中,且数据必须有序。
- 插入和删除数据的效率很低,因为这类结构中插入或删除数据,通常需要挪动其他数据。
这一点也正体现出了平衡二叉搜索树的价值。

如果是最优的情况,那么搜索二叉树的时间复杂度为log(N),但是如果像上述右图一样,那么时间复杂度是O(N),但是时间复杂度一般考虑最差情况
三 二叉搜索树的插入
插入过程如下:
-
若树为空,直接新增结点,并将该结点赋值给 root 指针。
-
若树不为空,按照二叉搜索树的性质查找插入位置:插入值小于当前结点值则向左遍历,插入值大于当前结点值则向右遍历,直至找到空位置后插入新结点。
-
若支持插入相等的值,需保持逻辑一致性:插入值与当前结点值相等时,需固定选择向左或向右遍历(避免有时向左、有时向右),找到空位置后插入新结点。

先定义搜索二叉树的结构和初始化
cpp
template<class K>
struct BSTreeNode
{
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
BSTreeNode(const K& key)
:_left(nullptr)
,_right(nullptr)
,_key(key)
{}
};
cpp
// 不允许相等的值插入
template<class K>
class BSTree
{
typedef BSTreeNode<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;
}
| 步骤 | 逻辑描述 | 代码对应片段 |
|---|---|---|
| 1. 空树处理 | 若根节点为nullptr(树空),直接创建新节点作为根节点,插入成功 |
if (_root == nullptr) { _root = new Node(key); return true; } |
| 2. 查找插入位置 | 用cur指针遍历树,parent指针记录cur的父节点,通过键值比较确定遍历方向:- 若cur->_key < key:目标位置在右子树,parent更新为cur,cur移至右子节点;- 若cur->_key > key:目标位置在左子树,parent更新为cur,cur移至左子节点;- 若cur->_key == key:键值重复,返回false |
while (cur) { ... } 中的分支判断 |
| 3. 插入新节点 | 遍历结束后,cur为nullptr,parent为新节点的父节点:- 若parent->_key < key:新节点作为parent的右子节点;- 若parent->_key > key:新节点作为parent的左子节点 |
cur = new Node(key); 后的分支判断 |
| 4. 返回结果 | 插入完成,返回true |
四 二叉搜索树的查找
遍历步骤:
-
从根节点开始比较,查找目标值x:若x大于当前节点值,则向右遍历查找;若x小于当前节点值,则向左遍历查找。
-
最多查找树的高度次,若遍历至空节点仍未找到x,则说明该值不存在。
-
若不支持插入相等的值,一旦找到与x相等的节点,直接返回该节点即可。
-
若支持插入相等的值(即可能存在多个值为x的节点),通常要求查找中序遍历中的第一个x。例如:查找值为3时,需返回中序遍历中第一个出现的3(如示意图中1的右孩子对应的3)。
cpp
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;
}
五 二叉搜索树的删除(重点)
⾸先查找元素是否在⼆叉搜索树中,如果不存在,则返回false。
如果查找元素存在则分以下四种情况分别处理:(假设要删除的结点为N)
1. 要删除结点N左右孩子均为空
2. 要删除的结点N左孩子位空,右孩子结点不为空
3. 要删除的结点N右孩子位空,左孩子结点不为空
4. 要删除的结点N左右孩子结点均不为空
对应以上四种情况的解决方案:
- 把N结点的父亲对应孩子指针指向空,直接删除N结点(情况1可以当成2或者3处理,效果是⼀样
的)
把N结点的父亲对应孩子指针指向N的右孩子,直接删除N结点
把N结点的父亲对应孩子指针指向N的左孩子,直接删除N结点
无法直接删除N结点,因为N的两个孩子无处安放,只能用替换法删除。找N左子树的值最大结点
R(最右结点)或者N右子树的值最小结点R(最左结点)替代N,因为这两个结点中任意⼀个,放到N的
位置,都满足二叉搜索树的规则。替代N的意思就是N和R的两个结点的值交换,转而变成删除R结
点,R结点符合情况2或情况3,可以直接删除。


情况一:这是最简单的删除。因为1既没有左孩子又没有右孩子,所以可以直接删除不用考虑孩子节点
情况二 三:这两种都是只有一边孩子的。把父亲节点指向有的那边孩子节点,然后删除节点
情况四:最复杂(面试可能会考)删除根节点或者删除的节点左右孩子都有。
1 左右孩子都有:需要用到替换法。找左子树的最右节点(左子树的最大值)或右子树的最左节点(右子树的最小值)来替换当前的节点
2删除根节点:如上图是一种特殊情况(1),当使用替换法删除的时候,发现根节点的右子树的左孩子根本不存在,如果不额外列出这种情况,就会报错。还有一种特殊情况(2):删除根节点时,它的某一子树不存在,就可以直接把存在的孩子节点设为根节点
特殊情况(1):

注意:我们删除这个值必须要先找到对应的节点
代码实现:
cpp
// 从二叉搜索树中删除键值为key的节点,成功返回true,失败返回false
bool Erase(const K& key)
{
Node* parent = nullptr; // 记录当前节点的父节点
Node* cur = _root; // 从根节点开始查找目标节点
// 查找目标节点key
while (cur)
{
if (cur->_key < key) // 当前节点值小于key,向右子树查找
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key) // 当前节点值大于key,向左子树查找
{
parent = cur;
cur = cur->_left;
}
else // 找到目标节点cur,执行删除操作
{
// 情况1:目标节点左子树为空(包括左空右空和左空右非空)
if (cur->_left == nullptr)
{
// 若目标节点是根节点,直接让根指向其右子树
if (cur == _root)
{
_root = cur->_right;
}
else // 非根节点:更新父节点的指针,指向目标节点的右子树
{
// 判断目标节点是父节点的左孩子还是右孩子
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
delete cur; // 释放目标节点内存
return true; // 删除成功
}
// 情况2:目标节点右子树为空(左子树非空)
else if (cur->_right == nullptr)
{
// 若目标节点是根节点,直接让根指向其左子树
if (cur == _root)
{
_root = cur->_left;
}
else // 非根节点:更新父节点的指针,指向目标节点的左子树
{
// 判断目标节点是父节点的左孩子还是右孩子
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
delete cur; // 释放目标节点内存
return true; // 删除成功
}
else // 情况3:目标节点左右子树均非空(使用替换法删除)
{
// 找目标节点右子树中值最小的节点(最左节点)作为替换节点
Node* replaceParent = cur; // 替换节点的父节点(初始为目标节点)
Node* replace = cur->_right; // 从右子树开始查找
while (replace->_left) // 循环找到最左节点(值最小)
{
replaceParent = replace;
replace = replace->_left;
}
// 将替换节点的值赋给目标节点(完成值替换)
cur->_key = replace->_key;
// 删除替换节点(替换节点必为左空或左右均空,符合情况1或2)
// 判断替换节点是其父节点的左孩子还是右孩子
if (replaceParent->_left == replace)
{
replaceParent->_left = replace->_right; // 替换节点的右子树补位
}
else
{
replaceParent->_right = replace->_right; // 替换节点的右子树补位
}
delete replace; // 释放替换节点内存
return true; // 删除成功
}
}
}
// 循环结束仍未找到目标节点,删除失败
return false;
}
六 二叉搜索树key何key/value使用场景
1 key搜索场景
只有key作为关键码,结构中只需要存储key即可。关键码即为需要搜索到的值,搜索场景只需要判断key在不在。 基于key的搜索场景,实现的二叉树搜索树支持增、删、查操作,但不支持修改操作。修改key会破坏搜索树的结构。
场景1:小区无人值守车库 小区车库中,仅购买了车位的业主车辆可进入小区。物业会将这些业主的车牌号录入后台系统,当车辆进入时,系统扫描车牌并查询其是否在系统内。若在,则抬杆放行;若不在,则提示"非本小区车辆",禁止进入。
场景2:英文文章单词拼写检查 将词库中所有单词存入二叉搜索树,读取文章中的每个单词后,在二叉搜索树中查找该单词。若未找到,则用波浪线标红该单词,提示拼写可能错误。
2 key/value搜索场景
每个关键码key都对应一个值value,value可以是任意类型的对象。树的结点结构中,除了存储key,还需存储对应的value。增、删、查操作仍以key为关键字,遵循二叉搜索树的规则进行比较,从而快速找到key对应的value。 基于key/value的搜索场景,实现的二叉搜索树支持修改操作,但仅能修改value,不能修改key。修改key会破坏二叉搜索树的性质。
场景1:简单中英互译字典 树的结点中存储key(英文单词)和value(对应的中文含义)。使用时输入英文单词,通过二叉搜索树查找该key,找到后即可获取其对应的中文含义,完成翻译
场景2:商场无人值守车库计费 车辆在入口进场时,系统扫描车牌并记录,此时树中存储的key为车牌,value为入场时间。车辆在出口离场时,系统再次扫描车牌并查找对应的value(入场时间)。用当前时间减去入场时间计算出停车时长,再根据时长计算停车费用,用户缴费后系统抬杆,车辆即可离场。
场景3:文章单词出现次数统计 读取文章中的每个单词,在二叉搜索树中查找该单词(key)。 - 若单词不存在,说明是首次出现,在树中新增结点,存储key(该单词)和value(次数1)。 - 若单词已存在,直接将该key对应的value(次数)加1,完成统计更新。
3 key/value⼆叉搜索树代码实现
key/value和上面写的搜索二叉树本质区别不大,需要在插入时增加value参数
cpp
#include <iostream>
using namespace std;
// 命名空间:封装key-value型二叉搜索树相关结构
namespace key_value
{
// 二叉搜索树节点结构体:存储key、value及左右孩子指针
template<class K, class V>
struct BSTreeNode
{
BSTreeNode<K, V>* _left; // 左孩子指针
BSTreeNode<K, V>* _right; // 右孩子指针
K _key; // 关键码:搜索的关键字(不可重复、不可修改)
V _value; // 对应的值:可任意类型,支持修改
// 节点构造函数:初始化左右孩子为nullptr,赋值key和value
BSTreeNode(const K& key, const V& value)
:_left(nullptr)
, _right(nullptr)
, _key(key)
, _value(value)
{}
};
// 二叉搜索树类:key不可重复,支持增、删、查,不支持修改key(仅能通过Find间接改value)
template<class K, class V>
class BSTree
{
typedef BSTreeNode<K, V> Node; // 类型别名:简化节点类型书写
public:
// 插入操作:向树中插入(key, value)键值对,key不可重复
// 参数:key-插入的关键码,value-对应的关联值
// 返回值:插入成功返回true,key已存在返回false
bool Insert(const K& key, const V& value)
{
// 情况1:树为空(根节点为nullptr),直接创建新节点作为根
if (_root == nullptr)
{
_root = new Node(key, value);
return true;
}
Node* parent = nullptr; // 记录当前节点的父节点(用于后续挂接新节点)
Node* cur = _root; // 遍历指针:从根节点开始查找插入位置
// 循环查找插入位置:根据BST性质(左小右大)遍历
while (cur)
{
if (cur->_key < key) // 当前key小于目标key,去右子树找
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key) // 当前key大于目标key,去左子树找
{
parent = cur;
cur = cur->_left;
}
else // 找到相同key,不允许重复插入,返回false
{
return false;
}
}
// 循环结束:cur为nullptr,parent指向插入位置的父节点,创建新节点
cur = new Node(key, value);
// 根据父节点key与目标key的大小关系,挂接新节点到父节点的左/右孩子
if (parent->_key < key)
{
parent->_right = cur; // 父key小,新节点挂在右子树
}
else
{
parent->_left = cur; // 父key大,新节点挂在左子树
}
return true; // 插入成功
}
// 查找操作:根据key查找对应的节点
// 参数:key-要查找的关键码
// 返回值:找到返回对应节点指针,未找到返回nullptr
Node* Find(const K& key)
{
Node* cur = _root; // 遍历指针:从根节点开始查找
while (cur)
{
if (cur->_key < key) // 当前key小,去右子树找
{
cur = cur->_right;
}
else if (cur->_key > key) // 当前key大,去左子树找
{
cur = cur->_left;
}
else // 找到匹配key,返回当前节点(可通过节点指针修改value)
{
return cur;
}
}
return nullptr; // 遍历完未找到,返回nullptr
}
// 删除操作:根据key删除对应的节点,维护BST树性质
// 参数:key-要删除的关键码
// 返回值:删除成功返回true,key不存在返回false
bool Erase(const K& key)
{
Node* parent = nullptr; // 记录待删除节点的父节点
Node* cur = _root; // 遍历指针:查找待删除节点
// 第一步:查找待删除节点cur及其父节点parent
while (cur)
{
if (cur->_key < key) // 当前key小,去右子树找
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key) // 当前key大,去左子树找
{
parent = cur;
cur = cur->_left;
}
else // 找到待删除节点cur,进入删除逻辑
{
// 情况1:待删除节点无左孩子(左子树为空)
if (cur->_left == nullptr)
{
// 子情况1.1:待删除节点是根节点(无父节点)
if (cur == _root)
{
_root = cur->_right; // 根节点更新为右孩子(可能为nullptr)
}
else // 子情况1.2:待删除节点是父节点的左/右孩子
{
if (cur == parent->_left) // cur是父节点的左孩子
{
parent->_left = cur->_right; // 父节点左孩子指向cur的右孩子
}
else // cur是父节点的右孩子
{
parent->_right = cur->_right; // 父节点右孩子指向cur的右孩子
}
}
delete cur; // 释放待删除节点内存
return true;
}
// 情况2:待删除节点无右孩子(右子树为空)
else if (cur->_right == nullptr)
{
// 子情况2.1:待删除节点是根节点
if (cur == _root)
{
_root = cur->_left; // 根节点更新为左孩子(可能为nullptr)
}
else // 子情况2.2:待删除节点是父节点的左/右孩子
{
if (cur == parent->_left) // cur是父节点的左孩子
{
parent->_left = cur->_left; // 父节点左孩子指向cur的左孩子
}
else // cur是父节点的右孩子
{
parent->_right = cur->_left; // 父节点右孩子指向cur的左孩子
}
}
delete cur; // 释放待删除节点内存
return true;
}
// 情况3:待删除节点左右子树均不为空
else
{
// 解决方案:用cur右子树的最小节点(最左节点)替代cur
Node* replaceParent = cur; // 替代节点的父节点(初始为cur)
Node* replace = cur->_right; // 替代节点(从cur右子树开始找)
// 查找右子树的最左节点(即右子树的最小节点)
while (replace->_left)
{
replaceParent = replace;
replace = replace->_left;
}
// 用替代节点的key和value覆盖待删除节点cur(仅覆盖值,不改变指针关系)
cur->_key = replace->_key;
cur->_value = replace->_value;
// 处理替代节点的后续指针(替代节点无左孩子,只需处理右孩子)
if (replaceParent->_left == replace) // 替代节点是其父节点的左孩子
{
replaceParent->_left = replace->_right;
}
else // 替代节点是其父节点的右孩子(cur右子树无左节点时)
{
replaceParent->_right = replace->_right;
}
delete replace; // 释放替代节点内存(原cur节点已被"间接删除")
return true;
}
}
}
// 遍历完未找到key,删除失败
return false;
}
// 中序遍历:输出树中所有(key:value),BST中序遍历结果为key升序排列
void InOrder()
{
_InOrder(_root); // 调用私有递归遍历函数,从根节点开始
cout << endl; // 遍历结束后换行
}
private:
// 私有递归中序遍历函数:供公有InOrder调用,隐藏递归实现细节
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; // 树的根节点指针,初始化为空树
};
}
4 水果类示例
cpp
int main()
{
string arr[] = { "苹果", "西⽠", "苹果", "西⽠", "苹果", "苹果", "西⽠", "苹
果", "⾹蕉", "苹果", "⾹蕉" };
BSTree<string, int> countTree;
for (const auto& str : arr)
{
// 先查找⽔果在不在搜索树中
// 1、不在,说明⽔果第⼀次出现,则插⼊<⽔果, 1>
// 2、在,则查找到的结点中⽔果对应的次数++
//BSTreeNode<string, int>* ret = countTree.Find(str);
auto ret = countTree.Find(str);
if (ret == NULL)
{
countTree.Insert(str, 1);
}
else
{
ret->_value++;
}
}
countTree.InOrder();
return 0;
}
countTree是实例化对象
疑问点:为什么是ret->_value++,而不是str->_value++
countTree,find(str)的功能是:根据str,根据二叉搜索树中查找对应得节点,找到后返回该节点的指针(BSTreeNode<string,int>*)
因为ret指向的是存储键值对的树节点,而str只是单纯的的字符串,只有节点才能访问------value这个成员,字符串不能访问