

❤️@燃于AC之乐 来自重庆 计算机专业的一枚大学生
✨专注 C/C++ Linux 数据结构 算法竞赛 AI
🏞️志同道合的人会看见同一片风景!
👇点击进入作者专栏:
🌟《算法画解》算法相关题目点击即可进入实操🌟
感兴趣的可以先收藏起来,请多多支持,还有大家有相关问题都可以给我留言咨询,希望希望共同交流心得,一起进步,你我陪伴,学习路上不孤单!
文章目录
- 前言(整理学习蓝图)
- 一、概念
- 二、性能分析
- 三、基础接口实现:
-
- 模板定义与中序遍历
- [3.1 二叉树的插入(insert)](#3.1 二叉树的插入(insert))
- [3.2 二叉搜索树的查找(find)](#3.2 二叉搜索树的查找(find))
- 四、进阶接口------删除(erase)实现
-
- [4.1 对4种情况分析](#4.1 对4种情况分析)
- [4.2 代码实现](#4.2 代码实现)
- 五、测试用例及结果
- 六、二叉搜索树key和key/value使用场景
-
- [5.1 基于关键码 key 的搜索场景](#5.1 基于关键码 key 的搜索场景)
- [5.2 基于关键码 key/value 的搜索场景:](#5.2 基于关键码 key/value 的搜索场景:)
前言(整理学习蓝图)
容器,置物之所也,根据"数据在容器中的排列特性",容器可分为序列式(sequence) 和 关联式(associative) 两种:

前几章的学习,已完成了大部分对序列式容器的深入讲解,现在将进入到关联式容器的学习。
关联式容器:数据(每个元素)都有一个键值(key)和一个实值(value)。
当元素被插入到关联式容器中时,容器内部结构(可能是RB-tree,也可能是hash-table)便依照其键值大小,以某种特定规则将这个元素放置于适当位置。关联式容器没有所谓头尾(只有最大元素和最小元素) ,所以不会有所谓
push_back()、push_front()、pop_back()、pop_front()、begin()、end() 这样的操作行为。一般而言,关联式容器的内部结构是一个balanced binary tree(平衡二叉树),以便获得良好的搜寻效率。balanced
binary tree有许多种类型,包括AVL-tree、RB-tree,AA-tree.
其中,被STL使用得最广泛的就是红黑树(RB-tree)。
由于后面要学的map,set都是要以一种平衡二叉树(blanced binary tree),红黑树为轮子,我们先引入最基础的二叉搜索树(binary search tree),先了解大的框架,循序渐进,由浅入深的学习是一个比较好的过程。
一、概念
⼆叉搜索树(binary search tree)又称⼆叉排序树,它可以是空树,也可以是具有以下性质的⼆叉树:
若它的左子树不为空,则左子树上所有结点的值都小于等于根结点的值
若它的右子树不为空,则右子树上所有结点的值都大于等于根结点的值
它的左右子树也分别为⼆叉搜索树
⼆叉搜索树中可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义,后续我们学习map/set/multimap/multiset系列容器底层就是⼆叉搜索树,其中map/set不支持插入相等值,multimap/multiset支持插入相等值。

二、性能分析
查找:

二叉搜索树可以提供对数时间的元素插入和访问,但是会受到节点大小的分布限制,如下:

这样的效率显然无法满足实际需求。因此,后续课程将继续讲解二叉搜索
树的优化结构------平衡二叉搜索树AVL树和红黑树,它们适用于在内存中
高效地存储和查询数据。
需要补充的是,二分查找虽然也能达到 O(log₂N) 的查找效率,但其存在两
个显著缺陷:
数据必须存储在支持随机访问且有序的结构中;
插入和删除效率低,由于依赖顺序存储,增删数据往往需要移动大量元
素。
这也正是平衡二叉搜索树的重要价值所在。
三、基础接口实现:
模板定义与中序遍历
cpp
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 key
template<class K>
class BSTree
{
//typedef BSTNode<K> Node;
//C++11
using Node = BSTNode<K>;
public:
//类里面递归的写法:
//root私有,外部无,公开在成员函数中暴露
void InOrder()
{
_InOrder(_root); //*this
cout << endl;
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
private:
Node* _root = nullptr;
};
3.1 二叉树的插入(insert)
1.树为空,新增节点赋值给root指针;
2.树不空,按⼆叉搜索树性质,插入值比当前节点大往右走,插入值比当前节点小往左走,找到空位置,插入新节点。
3.支持插入等值(左右走规则要一致)或者不支持(这里实现)。
cpp
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;
}
3.2 二叉搜索树的查找(find)
1.从根开始比较,查找x,x比根的值大则往右边走查找,x比根值小则往左边走查找。
2.最多查找高度N次,走到到空,还没找到,这个值不存在。
3.如果不支持插入相等的值,找到x即可返回
4.如果支持插入相等的值,意味着有多个x存在,⼀般要求查找中序的第⼀个x。(这里实现的是不支持相等值)
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;
}
四、进阶接口------删除(erase)实现
对于二叉搜索树的erase接口实现才是一大难点,在力扣的题目中也考察过,非常的重要。
4.1 对4种情况分析
首先查找元素是否纯在,不存在返回false,其次大致要分为四种情况:
-
要删除结点 N 左右孩子均为空
解决方案:
把 N 结点的父亲对应孩子指针指向空,直接删除 N 结点(情况 1 可以当成 2 或者 3 处理,效果是一样的)
-
要删除的结点 N 左孩子为空,右孩子结点不为空
解决方案:
把 N 结点的父亲对应孩子指针指向 N 的右孩子,直接删除 N 结点 。

-
要删除的结点 N 右孩子为空,左孩子结点不为空
解决方案:
把 N 结点的父亲对应孩子指针指向 N 的左孩子,直接删除 N 结点 。

-
要删除的结点 N 左右孩子结点均不为空
解决方案:
无法直接删除 N 结点,因为 N 的两个孩子无处安放,只能用替换法删除 。找 N 左子树的值最大结点 R(最右结点)或者 N 右子树的值最小结点 R(最左结点)替代 N(这里使用 N 右子树的值最小结点 R最左结点),因为这两个结点中任意一个,放到 N 的位置,都满足二叉搜索树的规则。替代 N 的意思就是 N 和 R 的两个结点的值交换,转而变成删除 R 结点,R 结点符合情况 2 或情况 3,可以直接删除。

4.2 代码实现
cpp
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
{
//情况123(0-1个孩子)
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->_right;
//Node* replaceParent = nullptr; 这里不能给空,有右边第一个就是
//右边最左的情况,就不会进入循环更新父节点,而父节点不为空
Node* replaceParent = cur;
while (replace->_left)
{
replaceParent = replace;
replace = replace->_left;
}
cur->_key = replace->_key;
if (replaceParent->_left == replace)
replaceParent->_left = replace->_right;
else
replaceParent->_right = replace->_right;
delete replace;
}
return true;
}
}
return false;
}
五、测试用例及结果
cpp
#include "BST.h"
int main()
{
key::BSTree<int> t;
int a[] = { 8, 3, 1, 10, 1, 6, 4, 7, 14, 13 };
for (auto e : a)
{
t.Insert(e);
}
t.InOrder();
t.Insert(16);
t.InOrder();
t.Insert(3);
t.InOrder();
t.Erase(3);
t.InOrder();
t.Erase(8);
t.InOrder();
for (auto e : a)
{
t.Erase(e);
t.InOrder();
}
return 0;
}

六、二叉搜索树key和key/value使用场景
5.1 基于关键码 key 的搜索场景
当搜索仅依赖关键码 key,且只需判断 key 是否存在时,数据结构中仅存
储 key 即可。此时关键码即为搜索目标,搜索过程只关注 key 是否存在于
结构中。在该场景下,二叉搜索树支持插入、删除与查找,但不支持修改
操作,因为修改 key 会破坏搜索树的有序结构。
场景示例:
场景1:
小区无人值守车库
业主购买车位后,物业会将其车牌号录入后台系统。车辆进入时扫描车牌,若系统中有记录则抬杆放行;若无记录则提示"非本小区车辆,禁止进入"。

场景2:
英文单词拼写检查
将词典中所有单词存入二叉搜索树。读取文章中的每一个单词,若单词存在于二叉搜索树中,则拼写正确;否则以波浪线标红提示拼写错误。

5.2 基于关键码 key/value 的搜索场景:
在 key/value 搜索场景中,每个关键码 key 都对应一个值 value,value 可
以是任意类型的对象。树的结点中除了存储 key,还需存储对应的
value。数据的增、删、查操作仍基于 key 按二叉搜索树规则进行比较,
并借此快速定位并获取对应的 value。
在此类场景中,二叉搜索树支持修改 value,但不能修改 key,否则会破坏搜索树的有序性质。
场景示例:
场景1:
简单中英互译词典
树结点中存储 key(英文单词)与 value(中文释义)。搜索时输入英文单词,系统即可同时获取对应的中文翻译。

场景2:
商场无人值守车库
车辆入场时扫描车牌,系统记录车牌号(key)与入场时间(value)。出场时再次扫描车牌,根据 key 查找入场时间,用当前时间减去入场时间计算停车时长,并生成停车费用。缴费后抬杆放行。

场景3:
统计文章中单词出现次数
依次读取文章中的单词,若单词未存在于树中,则插入该单词并初始化次数为 1;若单词已存在,则将其对应次数加一,从而完成词频统计。


加油!志同道合的人会看到同一片风景。
看到这里请点个赞 ,关注 ,如果觉得有用就收藏一下吧。后续还会持续更新的。 创作不易,还请多多支持!