文章目录
1. 二叉搜索树的介绍
1.1 概念
二叉搜索树又称二叉排序树(中序遍历是升序),它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有结点的值都小于等于根结点的值
- 若它的右子树不为空,则右子树上所有结点的值都大于等于根结点的值
- 它的左右子树也分别为二叉搜索树
- 二叉搜索树中可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义,STL容器中的map/set/multimap/multiset系列容器底层就是二叉搜索树的变形,其中map/set不支持插入相等值,multimap/multiset支持插入相等值
下图都为二叉搜索树:
1.2 二叉搜索树的性能分析
- 最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其高度为:O(log2N)
- 最差情况下,二叉搜索树退化为单支树(或者类似单支),其高度为:O(N/2)
- 所以综合而言二叉搜索树增删查改时间复杂度为:O(N)
那么这样的效率显然是无法满足我们需求的,后面几篇文章会介绍二叉搜索树的变形如:平衡二叉搜索树、AVL树和红黑树,这些才能适用于我们在内存中存储和搜索数据。
另外需要说明的是,二分查找也可以实现O(logN) 级别的查找效率,但是二分查找有两大缺陷:
- 1.需要存储在支持下标随机访问的结构中,并且有序。
- 2.插入和删除数据效率很低,因为存储在下标随机访问的结构中,插入和删除数据一般需要挪动数据。
这也从侧面体现出了平衡二叉搜索树的价值。
2. 二叉搜索树的增删查改
2.1 插入
插入的树结点结构为:
template<class K, class V>
struct BSTreeNode// key/value结构
{
K _key;// _key是关键字,增删查改都按关键字进行,不允许修改_key值
V _val;// _val是另存储的值,可支持修改
struct BSTreeNode<K, V>* _left;
struct BSTreeNode<K, V>* _right;
BSTreeNode(const K& key, const V& val)
:_key(key), _val(val)
, _left(nullptr), _right(nullptr) {}
};
插入的具体过程如下:
- 1.树为空,则直接新增结点,赋值给root指针
- 2.树不空,按二叉搜索树性质,插入值比当前结点大往右走,插入值比当前结点小往左走,找到空位置,插入新结点。
- 3.如果支持插入相等的值,插入值跟当前结点相等的值可以往右走,也可以往左走,找到空位置,插入新结点。(要注意的是要保持逻辑一致性,插入相等的值不要一会往右走,⼀会往左走)
下图是一颗二叉搜索树的插入步骤图(都以没有重复值的为例):
代码如下:
//规定按key值比较,比根小插入至左子树,比根大插入至右子树,key值相同不插入并返回false
bool Insert(const K& key, const V& value)
{
if (_root == nullptr)//空树直接插入
{
Node* newnode = new Node(key, value);
_root = newnode;
return true;
}
Node* cur = _root; //遍历
Node* parent = nullptr;//记录父亲
while (cur)//循环从根遍历找合适空位
{
if (key < cur->_key)//比根小
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)//比根大
{
parent = cur;
cur = cur->_right;
}
else
return false;//跟根相等
}
//cur为空,说明找到空位,直接用parent链接
Node* newnode = new Node(key, value);
if (key < parent->_key)//链接时再进行判断
parent->_left = newnode;
else
parent->_right = newnode;
return true;
}
2.2 查找
查找显然有两种方式:
- 1.遍历查找
- 2.利用搜索树性质查找
- 第一种,通过遍历进行查找,时间复杂度最好为O(1),最坏为O(N),平均为O(N)
- 第二种,利用搜索树特性查找,时间复杂度最好为O(1),近似单支树时是最坏:为O(N),近似完全二叉树时是平均:为O(logN)
这里以第二种实现为例:
- 从根开始比较,查找key,key比根的值大则往右边走查找,key比根值小则往左边走查找。
- 最多查找高度次,走到为空,还没找到,这个值不存在。
- 如果不支持插入相等的值,找到x即可返回。
Node* Find(const K& key)
{
if (_root == nullptr)//空树返回空
return nullptr;
Node* cur = _root;
while (cur)
{
if (key < cur->_key)
cur = cur->_left;
else if (key > cur->_key)
cur = cur->_right;
else
return cur;
}
return nullptr;//找完了没找到,就返回空
}
2.3 删除(重点)
- 讨论:
Ⅰ.首先查找元素是否在二叉搜索树中,如果不存在,则返回false。
Ⅱ.如果查找元素存在则分以下四种情况分别处理:(假设要删除的结点为N)
- 要删除结点N左右孩子均为空
- 要删除的结点N左孩子为空,右孩子结点不为空
- 要删除的结点N右孩子为空,左孩子结点不为空
- 要删除的结点N左右孩子结点均不为空
- 解决:
- 把N结点的父亲对应孩子指针指向空,直接删除N结点(情况1可以当成2或者3处理,效果是一样的)
- 把N结点的父亲对应孩子指针指向N的右孩子,直接删除N结点
- 把N结点的父亲对应孩子指针指向N的左孩子,直接删除N结点
- 无法直接删除N结点,因为N的两个孩子无处安放,只能用替换法删除。找N的左子树的最右结点R(左子树最大结点)或者N的右子树的最左结点L(右子树最小结点)替代N,因为这两个结点中任意一个,放到N的位置,都满足二叉搜索树的规则。替代N的意思就是N和L(或R)的两个结点的值交换,转而变成删除L(或R)结点,替换后L(或R)结点符合情况2或情况3,可以直接删除。
对于为什么情况1可以看成2或3来处理,解释如下:
- (1)N的左直接看成空,把N的右孩子特殊,看成是个空的孩子,就变成情况2
- (2)或者把N的左孩子特殊,看成是个空的孩子,右直接看成空,就变成情况3
下图是一颗二叉搜索树的删除步骤图(涉及替换时,红色结点为删除结点,绿色结点为替换结点):
附上代码(替换结点为N的左子树的最右结点):
bool Erase(const K& key)//特别考验逻辑与思考全面性
{
if (_root == nullptr)//空树删除不了
return false;
Node* cur = _root;
Node* parent = cur;
while (cur)//循环去找要删除的结点
{
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else //找到要删除的结点
{
//有四种情况---对应解决法
//1.删除结点N左右都为空---N的父亲对应N的指针指向为空,直接删除N,
// 这种可以看成一种特殊情况的2或3,即:
// (1)N的左直接看成空,把N的右孩子特殊,看成是个空的孩子,
// (2)或者把N的左孩子特殊,看成是个空的孩子,右直接看成空
// 故这里可以不用实现情况1
//2.删除结点N左为空,右不为空---N的父亲对应N的指针指向N的右孩子,直接删除N
if (cur->_left == nullptr)
{
if (_root == cur)// 删除根时
_root = cur->_right;
else
{
if (parent->_left == cur)//这里还要判断要删除的N对应是其父亲的左还是右孩子
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
delete cur;
}
//3.删除结点N左不为空,右为空---N的父亲对应N的指针指向N的左孩子,直接删除N
else if (cur->_right == nullptr)
{
if (_root == cur)
_root = cur->_left;
else
{
if (parent->_left == cur)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
delete cur;
}
//4.删除结点N左右均不为空---不能直接删除N,删除会破坏结构,只能用对应结点替换法,
// 替换不是直接交换结点,而是交换结点内的key和val值,替换规则如下:
// (1) 若N的左子树不为空,则找N的左子树的最大结点,也就是N的左子树的最右结点
// (2) 若N的右子树不为空,则找N的右子树的最小结点,也就是N的右子树的最左结点
// 然后执行替换结点内的值,此时要删除结点就变成了交换后的结点,删除情况就变为情况2或3了,这里实现(1)
else //左右都不为空
{
Node* find = cur->_left;//找替换结点
Node* find_parent = cur;//记录替换结点的父亲
while (find->_right)//一直在N的左子树中找右结点,直至最右结点(也就是左子树最大结点)
{
find_parent = find;
find = find->_right;
}
std::swap(find->_key, cur->_key);
std::swap(find->_val, cur->_val);
if (find->_left == nullptr)
{
if (find_parent->_left == find)//这里还要判断替换的结点对应是其父亲的左还是右孩子
find_parent->_left = find->_right;
else
find_parent->_right = find->_right;
}
else
{
if (find_parent->_left == find)
find_parent->_left = find->_left;
else
find_parent->_right = find->_left;
}
delete find;
}
return true;
}
}
return false;//出了循环,说明没找到要删除的结点
}
- 代码优化:
基于情况4的特性,发现用的是删除结点N的左子树的最右结点作替换,也就是说这个替换结点的右孩子一定为空,于是上述代码就不要再逐一判断替换后它是属于情况2还是情况3。简单说,经过上述替换操作后,删除结点按情况3来直接进行删除就行。 其次,swap操作直接改为赋值操作效率也会更高些。
类似下图:
改正代码如下:
bool Erase(const K& key)
{
if (_root == nullptr)
return false;
Node* cur = _root;
Node* parent = cur;
while (cur)
{
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else
{
if (cur->_left == nullptr)
{
if (_root == cur)
_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 (_root == cur)
_root = cur->_left;
else
{
if (parent->_left == cur)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
delete cur;
}
else
{
Node* find = cur->_left;
Node* find_parent = cur;
while (find->_right)
{
find_parent = find;
find = find->_right;
}
cur->_key = find->_key;
cur->_val = find->_val;
if (find_parent->_left == find)
find_parent->_left = find->_left;
else
find_parent->_right = find->_left;
delete find;
}
return true;
}
}
return false;
}
扩展:感兴趣的读者可以尝试用递归删除
3. 二叉搜索树的应用(了解)
3.1 key搜索场景
只有key作为关键码,结构中只需要存储key即可,关键码即为需要搜索到的值,搜索场景只需要判断key在不在。key的搜索场景实现的⼆叉树搜索树支持增删查,但是不支持修改,修改key破坏搜索树结构了。
- 场景1:小区无人值守车库,小区车库买了车位的业主车才能进小区,那么物业会把买了车位的业主的⻋牌号录⼊后台系统,⻋辆进入时扫描车牌在不在系统中,在则抬杆,不在则提示非本小区⻋辆,无法进入。
- 场景2:检查⼀篇英文文章单词拼写是否正确,将词库中所有单词放入二叉搜索树,读取文章中的单词,查找是否在二叉搜索树中,不在则波浪线标红提示。
3.2 key/value搜索场景
每⼀个关键码key,都有与之对应的值value,value可以任意类型对象。树的结构中(结点)除了需要存储key还要存储对应的value,增/删/查还是以key为关键字走二叉搜索树的规则进行比较,可以快速查找到key对应的value。key/value的搜索场景实现的⼆叉树搜索树支持修改,但是不支持修改key,修改key破坏搜索树结构了,修改value则可以。
- 场景1:简单中英互译字典,树的结构中(结点)存储key(英文)和vlaue(中文),搜索时输入英文,则同时查找到了英文对应的中文。
- 场景2:商场无人值守车库,入口进场时扫描车牌,记录车牌和入场时间,出口离场时,扫描车牌,查找入场时间,用当前时间-入场时间计算出停车时长,计算出停车费用,缴费后抬杆,车辆离场。
- 场景3:统计一篇文章中单词出现的次数,读取一个单词,查找单词是否存在,不存在这个说明第一次出现,(单词,1),单词存在,则++单词对应的次数。