1. ⼆叉搜索树的概念
⼆叉搜索树⼜称⼆叉排序树,它或者是⼀棵空树,或者是具有以下性质的⼆叉树:
• 若它的左⼦树不为空,则左⼦树上所有结点的值都⼩于等于根结点的值
• 若它的右⼦树不为空,则右⼦树上所有结点的值都⼤于等于根结点的值
• 它的左右⼦树也分别为⼆叉搜索树
• ⼆叉搜索树中可以⽀持插⼊相等的值,也可以不⽀持插⼊相等的值,具体看使⽤场景定义,后续我们学习map/set/multimap/multiset系列容器底层就是⼆叉搜索树,其中map/set不⽀持插⼊相等值,multimap/multiset⽀持插⼊相等值
称为二叉排序树的原因:这颗树是严格遵守左边小右边大的方式.当我们去按中序遍历去走一边,它就会排好升序,所以叫二叉排序树。
2. ⼆叉搜索树的性能分析
最优情况下,⼆叉搜索树为完全⼆叉树(或者接近完全⼆叉树),其⾼度为: log2 N最差情况下,⼆叉搜索树退化为单⽀树(或者类似单⽀),其⾼度为: N
所以综合⽽⾔⼆叉搜索树增删查改时间复杂度为: O(N)
那么这样的效率显然是⽆法满⾜我们需求的,我们后续课程需要继续讲解⼆叉搜索树的变形,平衡⼆叉搜索树AVL树和红⿊树,才能适⽤于我们在内存中存储和搜索数据。
另外需要说明的是,⼆分查找也可以实现 O(log2 N) 级别的查找效率,但是⼆分查找有两⼤缺陷:
3. 需要存储在⽀持下标随机访问的结构中,并且有序。
4. 插⼊和删除数据效率很低,因为存储在下标随机访问的结构中,插⼊和删除数据⼀般需要挪动数据。
这⾥也就体现出了平衡⼆叉搜索树的价值。
3.二叉搜索树相关功能实现
初始化,insert(插入),Find(查找),Erase(删除),析构,打印(中序遍历),构造(深拷贝)
初始化
template<class K>
struct BSTreeNode
{
//二叉搜索树节点
BSTreeNode * left;
BSTreeNode* right;
K _key;
//构造函数
BSTreeNode(const K& key)
:left(nullptr)
, right(nullptr)
, _key(key)
{}
};
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
private:
Node* _root=nullptr;
insert(插入)
//不带重复的搜索二叉树
//插入
bool Insert(const K& key)
{
//如果为第一个节点,直接创建新节点赋予_root
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所在位置即为插入点
//创建出节点+真确链接(判断出为parent左边还是右边)
cur = new Node(key);
if (parent->_key < key)
{
parent->right = cur;
}
else
{
parent->left = cur;
}
return true;
}
其中while那部分代码为核心,利用二叉搜索树左边小·右边大特性找到插入节点位置。
Find(查找)
关键代码(while利用二叉搜索树特性寻找)
bool Find(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
{
//已存在这种值到达此处
return true;
}
}
return false;
}
Erase(删除)
⾸先查找元素是否在⼆叉搜索树中,如果不存在,则返回false。
如果查找元素存在则分以下四种情况分别处理:(假设要删除的结点为N)
- 要删除结点N左右孩⼦均为空
- 要删除的结点N左孩⼦位空,右孩⼦结点不为空
- 要删除的结点N右孩⼦位空,左孩⼦结点不为空
- 要删除的结点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,2,3进行操作
细节:删除节点要判断为,parent的左,还是parent的右。经过分析为parent左为一般情况,parent的右删除节点为根节点。
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)
{
_root = cur->right;
}
else
{
if (cur == parent->left)
{
parent->left = cur->right;
}
else
{
parent->right = cur->right;
}
}
delete cur;
}
//孩子右为空
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;
}
else//到达这里为左右孩子都存在
{
//替换法
Node* pMinright = cur;
Node* minRight = cur->right;
while (minRight->left)
{
pMinright = minRight;
minRight = minRight->left;
}
swap(cur->_key, minRight->_key);
//删除节点要判断为,parent的左,还是parent的右
if (pMinright->left == minRight)
{
pMinright->left = minRight->right;
}
else
{
pMinright->right = minRight->right;
}
delete minRight;
}
return true;
}
}
return false;
}
打印(中序遍历),构造(深拷贝),析构
1.分析知道,递归遍历需要传入根节点参数,如果由使用者传入根节点是不方便的,因为_root为private不便于访问,解决方案1.成员函数GetRoot()将遍历包裹一层,具体实现看代码
2.深拷贝利用前序遍历一个个取值构造
3.析构利用后序遍历
public:
//默认构造函数
//方法一
/*BSTree()
{
}*/
//方法二 强制生成
BSTree() = default;
//拷贝构造
BSTree(const BSTree<K>& t)
{
//这种写法肯定存在问题这是一种浅拷贝,通过析构来观察
//_root = t._root;
//完成深拷贝就要利用t给*this构造
_root = Copy(t._root);
}
//赋值运算符重载
BSTree<K>& operator=(const BSTree<K> t)
{
swap(_root, t._root);
return *this;
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
~BSTree()
{
Destory(_root);
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->left);
cout << root->_key << " ";
_InOrder(root->right);
}
Node* Copy(Node* root)
{
if (root == nullptr)
return nullptr;
Node* copy = new Node(root->_key);
copy->left = Copy(root->left);
copy->right = Copy(root->right);
return copy;
}
void Destory(Node* root)
{
if (root == nullptr)
{
return;
}
Destory(root->left);
Destory(root->right);
delete root;
}
二叉搜索树的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的搜索场景实现的⼆叉树搜索树⽀持修改,但是不⽀持修改key,修改key破坏搜索树性质了,可以修改value。
场景1:简单中英互译字典,树的结构中(结点)存储key(英⽂)和vlaue(中⽂),搜索时输⼊英⽂,则同时查找到了英⽂对应的中⽂。
场景2:商场⽆⼈值守⻋库,⼊⼝进场时扫描⻋牌,记录⻋牌和⼊场时间,出⼝离场时,扫描⻋牌,查找⼊场时间,⽤当前时间-⼊场时间计算出停⻋时⻓,计算出停⻋费⽤,缴费后抬杆,⻋辆离场。
场景3:统计⼀篇⽂章中单词出现的次数,读取⼀个单词,查找单词是否存在,不存在这个说明第⼀次出现,(单词,1),单词存在,则++单词对应的次数。
代码实现
namespace key_value
{
template<class K, class V>
struct BSTreeNode
{
BSTreeNode<K, V>* _left;
BSTreeNode<K, V>* _right;
K _key; // 中文
V _value; // 英文
BSTreeNode(const K& key, const V& value)
:_left(nullptr)
, _right(nullptr)
, _key(key)
, _value(value)
{}
};
template<class K, class V>
class BSTree
{
typedef BSTreeNode<K, V> Node;
public:
// 强制生成
BSTree() = default;
// BSTree(const BSTree& t)
BSTree(const BSTree<K, V>& t)
{
_root = Copy(t._root);
}
// t1 = t3
// BSTree& operator=(BSTree t)
BSTree<K, V>& operator=(BSTree<K, V> t)
{
swap(_root, t._root);
return *this;
}
~BSTree()
{
Destory(_root);
_root = nullptr;
}
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)
{
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 (parent == nullptr)
if (cur == _root)
{
_root = cur->_right;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
}
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;
}
else // 两个孩子
{
Node* pMinRight = cur;
Node* minRight = cur->_right;
while (minRight->_left)
{
pMinRight = minRight;
minRight = minRight->_left;
}
swap(cur->_key, minRight->_key);
if (pMinRight->_left == minRight)
{
pMinRight->_left = minRight->_right;
}
else
{
pMinRight->_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;
_InOrder(root->_right);
}
void Destory(Node* root)
{
if (root == nullptr)
{
return;
}
Destory(root->_left);
Destory(root->_right);
delete root;
}
Node* Copy(Node* root)
{
if (root == nullptr)
return nullptr;
Node* copy = new Node(root->_key, root->_value);
copy->_left = Copy(root->_left);
copy->_right = Copy(root->_right);
return copy;
}
private:
Node* _root = nullptr;
};
}