💁♂️个人主页:进击的荆棘
👇作者其它专栏:
目录
1.二叉搜索树的概念
2.二叉搜索树的性能分析
3.二叉搜索树的插入
4.二叉搜索树的查找
5.二叉搜索树的删除
6.二叉搜索树的实现代码
7.二叉搜索树key和key/value的使用场景
1.二叉搜索树的概念
二叉搜索树又称二叉排序树,它或者是一颗空树,或者是具有以下性质的二叉树:
●若它的左子树不为空,则左子树上所有节点的值都小于等于根节点的值
●若它的右子树不为空,则右子树上所有节点的值都大于等于根节点的值
●它的左右子树也分别为二叉搜索树
●二叉搜索树可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义,当我们使用map/set/multimap/multiset系列容器时,它们的底层就是二叉搜索树,其中map/set不支持插入相等的值,multimap/multiset支持插入相等值

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

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

int a[]={8,3,1,10,6,4,7,14,13};


4.二叉搜索树的查找
1.从跟开始比较,查找x,x比根的值大就往右边查找,比它小就往左边查找。
2.最多查找高度次,走到空,若还没找到,则这个值不存在。
3.若不支持插入相等的值,找到x即可返回。
4.若支持插入相等的值,意味着有多个x存在,一般要求查找中序的第一个x。如下图,查找3,要找到1的右孩子的那个3返回。

5.二叉搜索树的删除
首先查找元素是否在二叉搜索树中,若不存在,则返回false。
若查找元素存在则分以下四种情况分别处理:(假设要删除的节点为N)
1.要删除节点N左右孩子均为空
2.要删除的节点N左孩子为空,右孩子节点不为空
3.要删除的节点N右孩子为空,左孩子节点不为空
4.要删除的节点左右孩子节点均不为空
对应以上四种情况的解决方案:
1.把N节点的父亲对应孩子指针指向空,直接删除N节点(情况1可以当成2或3处理,效果是一样的)
2.把N节点的父亲对应孩子指针指向N的右孩子,直接删除N节点
3.把N节点的父亲对应孩子指针指向N的左孩子,直接删除N节点
4.无法直接删除N节点,因为N的两个孩子无处安放,只能用替换法删除。找到左子树的值最大节点R(最右节点)或N右子树的值最小节点R(最左节点)代替N,因为这两个节点中任意一个,放到N的位置,都满足二叉搜索树的规则。代替N的意思就是N和R的两个节点的值交换,转而变成删除R节点,R节点符合情况2或情况3,可以直接删除。



6.二叉搜索树的实现代码
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;
//C++11新用法,意同上面
using Node=BSTNode<K>;
public:
bool Insert(const K& key){
if(!_root) {
_root=new Node(key);
return true;
}
Node* cur=_root;
Node* parent=nullptr;
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;
}
bool Find(const K& key){
if(!_root) return false;
Node* cur=_root;
while(cur){
if(cur->_key<key){
cur=cur->_right;
}
else if(cur->_key>key){
cur=cur->_left;
}
else break;
}
return true;
}
bool Erase(const K& key){
Node* cur=_root;
Node* parent=nullptr;
while(cur){
if(cur->_key<key){
parent=cur;
cur=cur->_right;
}
else if(cur->_key>key){
parent=cur;
cur=cur->_left;
}
//找到该值,有四种情况:1.无左右孩子 2.有左无右 3.有右无左 4.有两孩子
//总结为两种情况,1.无左将右赋给对应的父节点的左右节点,无右将左赋给对应的父结点的左右节点(无孩子不影响,因为会赋空)
//2.无法直接将节点删去,因为它的两孩子无处安放,应使用替代法,让它左子树的最大节点或右子树的最小节点与它替换,然后删去
else{
//左为空时
if(!cur->_left){
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){
if(cur==_root) _root=cur->_left;
else{
if(parent->_left==cur) parent->_left=cur->_left;
else parent->_right=cur->_left;
}
delete cur;
}
//左右都不为空,右子树最左节点
else{
Node* replaceParent=cur;
Node* replace=cur->_right;
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;
}
void InOrder(){
_InOrder(_root);
cout<<endl;
}
private:
void _InOrder(const Node* root){
if(!root) return ;
_InOrder(root->_left);
cout<<root->_key<<' ';
_InOrder(root->_right);
}
private:
Node* _root=nullptr;
};
7.二叉搜索树key和key/value的使用场景
7.1key搜索场景:
只有key作为关键码,结构中只需要存储key即可,关键码即为需要搜索到的只,搜索场景只需要判断key在不在。key的搜索场景实现的二叉搜索树支持增删查,但是不支持修改,修改key破坏搜索树结构了。
场景1:小区无人值守车库,小区车库买了车位的业主车才能进小区,那么物业会把买了车位的业主的车牌号录入后台系统,车辆进入时扫描车牌在不在系统中,在则抬杆,不在则提示非本小区车辆,无法进入。
场景2:检查一篇英文文章单词拼写是否正确,将词库中所有单词放入二叉搜索树,读取文章中的单词查找是否在二叉搜索树中,不在则波浪线标红提示。

7.2key/value搜索场景:
每一个关键码key,都有与之对应的值value,value可以任意类型对象。树的结构中(节点)除了需要存储还要存储对应的value,增/删/查还是以key为关键字走二叉搜索树的规则进行比较,可以快速查找到key对应的value,key/value的搜索场景实现的二叉搜索树支持修改,但是不支持修改key,修改key破环搜索树性质了,可以修改value。
场景1:简单中英互译字典,树的结构中(节点)存储key(英文)和value(中文),搜索时输入英文,则同时查找到了英文对应的中文。
场景2:商场无人值守车库,入口进场时扫描车牌,记录车牌和入场时间,出口离场时,扫描车牌,查找入场时间,用当前时间-入场时间计算出停车时长,计算出停车费,缴费后抬杆,车辆离场。
场景3:统计一篇文章中单词出现的次数,读取一个单词,查找单词是否存在,不存在这个说明第一次出现,(单词,1)单词存在,则++单词对应的次数。
7.3key/value二叉搜索树代码实现
cpp
template<class K,class V>
struct BSTNode{
K _key;
V _value;
BSTNode<K,V>* _left;
BSTNode<K,V>* _right;
BSTNode(const K& key,const V& value)
:_key(key)
,_value(value)
,_left(nullptr)
,_right(nullptr)
{}
};
template<class K,class V>
class BSTree{
//typedef BSTNode<K> Node;
//C++11新用法,意同上面
using Node=BSTNode<K,V>;
//C++11,强制生成构造
BSTree()=default;
BSTree(const BSTree& t){
_root=Copy(t._root);
}
BSTree& operator=(BSTree tmp){
swap(_root,tmp._root);
return *this;
}
~BSTree(){
Destroy(_root);
_root=nullptr;
}
public:
bool Insert(const K& key,const V& value){
if(!_root) {
_root=new Node(key,value);
return true;
}
Node* cur=_root;
Node* parent=nullptr;
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;
}
bool Find(const K& key){
if(!_root) return false;
Node* cur=_root;
while(cur){
if(cur->_key<key){
cur=cur->_right;
}
else if(cur->_key>key){
cur=cur->_left;
}
else break;
}
return true;
}
bool Erase(const K& key){
Node* cur=_root;
Node* parent=nullptr;
while(cur){
if(cur->_key<key){
parent=cur;
cur=cur->_right;
}
else if(cur->_key>key){
parent=cur;
cur=cur->_left;
}
//找到该值,有四种情况:1.无左右孩子 2.有左无右 3.有右无左 4.有两孩子
//总结为两种情况,1.无左将右赋给对应的父节点的左右节点,无右将左赋给对应的父结点的左右节点(无孩子不影响,因为会赋空)
//2.无法直接将节点删去,因为它的两孩子无处安放,应使用替代法,让它左子树的最大节点或右子树的最小节点与它替换,然后删去
else{
//左为空时
if(!cur->_left){
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){
if(cur==_root) _root=cur->_left;
else{
if(parent->_left==cur) parent->_left=cur->_left;
else parent->_right=cur->_left;
}
delete cur;
}
//左右都不为空,右子树最左节点
else{
Node* replaceParent=cur;
Node* replace=cur->_right;
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;
}
void InOrder(){
_InOrder(_root);
cout<<endl;
}
private:
void _InOrder(const Node* root){
if(!root) return ;
_InOrder(root->_left);
cout<<root->_key<<' ';
_InOrder(root->_right);
}
void Destroy(Node* root){
if(!root) return ;
Destroy(root->_left);
Destory(root->right);
delete root;
}
Node* Copy(Node* root){
if(!root){
return nullptr;
}
Node* newRoot=new Node(root->_key,root->_value);
newRoot->left=Copy(root->left);
newRoot->right=Copy(root->_right);
return newRoot;
}
private:
Node* _root=nullptr;
};
int main(){
BSTree<string,string> dict;
//BSTree<string,string> copy=dict;
dict.Insert("left","左边");
dict.Insert("right","右边");
dict.Insert("insert","插入");
dict.Insert("string","字符串");
string str;
while(cin>>str){
auto ret=dict.Find(str);
if(ret){
cout<<"->"<<ret->_value<<endl;
}
else cout<<"查不到次单词,请重新输入"<<endl;
}
return 0;
}
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;
}