本文介绍了二叉搜索树的基本概念及详细实现方法。此外还探讨了二叉搜索树在日常生活场景中的应用,例如在小区门禁系统中用于车牌号的快速识别,以及实现单词翻译器或者统计次数的功能
目录
二叉搜索树的概念
二叉搜索树(Binary Search Tree, BST是一种特殊的二叉树数据结构又称⼆叉排序树 具有以下性质
对于每一个节点 如果它的左子树不为空那么左子树上所有结点的值都小于等于该结点的值
如果它的右子树不为空那么右子树上所有结点的值都大于等于该结点的值
⼆叉搜索树中可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义,之后学习的map/set/multimap/multiset这些容器 底层就是⼆叉搜索树,其中map/set不支持插入相等值,multimap/multiset支持插入相等值
二叉搜索树是之后学习map/set等容器及红黑树AVL树的基础 需要我们掌握清楚
二叉搜索树的性能分析
最好的情况 该树为完全二叉树也就是每一层节点都是满的(或者和这样差不多的) 那么时间复杂度为O(logN)
最坏的情况 每一层只有一个节点(或者类似) 时间复杂度为O(N)
时间复杂度按最坏的情况算 所以最终时间复杂度为O(N)
可以看到 在坏的情况下时间复杂度还是很一般的 所以之后学习的红黑树 AVL数就通过一些方式来解决这样的问题 让时间复杂度变为O(logN)
另外二分查找时间复杂度也是O(logN) 但是二分查找有很大的限制
1.首先 二分查找需要数据是有序的
2.结构上必须要求是连续存储的物理结构 如vector 但是这样的话 插入删除的效率就低了(需要挪动数据)
接下来我们就来实现一个不支持插入相等值的二叉树
二叉搜索树实现
先把基本的结构给搭建好
template <class K>
class BSnode
{
public:
BSnode(const K& key)
:_key(key)
, _left(nullptr)
, _right(nullptr)
{}
K _key;
BSnode<K>* _left;
BSnode<K>* _right;
};
template<class K>
class BStree
{
public:
typedef BSnode<K> Node;
private:
Node* _root=nullptr;
};
插入
因为我们实现的不支持插入重复元素 所以返回值为bool类型 如果插入成功返回true 如果要插入的值是重复值插入失败返回false
插入很简单 如果插入的值比跟节点大就到右节点 小于跟节点就到左节点 等于此时节点值直接返回false 直到为空节点 此时就是要插入的位置
另外需要存一下它的父节点 让它的父节点指向它 指向之前还需要判断一下父节点的右节点是它还是左节点是它
bool insert(const K& x)
{
if (_root == nullptr) //对空树的处理
_root=new Node(x);
Node* cur = _root;
Node* curparent = cur;
while (cur!=nullptr) //直到为空了 就是要插入的位置
{
if (x > cur->_key) //要插入的数大于根的值就往右
{
curparent = cur; //cur里面存的是cur的上一个位置 cur改变之前先把它的位置存到curparent中
cur = cur->_right;
}
else if (x < cur->_key) //小于根的值就往左
{
curparent = cur;
cur = cur->_left;
}
else
return false; //不支持插入重复的元素 插入失败 返回false
}
//如果正常出了循环 那么此时cur的位置就是新插入节点的位置 那么此时为新节点开空间 然后让它的父节点指向它
if (curparent->_key<x)
curparent->_right = new Node(x); //此时还需要判断 cur位置的节点是父节点的右孩子还是左孩子
else
curparent->_left = new Node(x);
return true; //插入成功 返回true
}
查找
查找和插入逻辑差不多 比插入还要简单
Node* find(const K& x)
{
Node* cur=_root;
while (cur != nullptr)
{
if (x == cur->_key)
return cur;
else if (x > cur->_key)
{
cur = cur->_right;
}
else
cur = cur->_left;
}
return nullptr; //如过出了while循环说明没有找到要查找的值
}
删除
删除要处理的地方比较多
首先我们需要先找到要删除的节点 和find差不多 还需要存要删除节点cur的父节点curparent 因为在删除节点之后要处理cur父节点和它左子点的连接关系
在找到要删除的节点之后
对于这个要删除的节点可以分为三种情况
1.它没有子节点
2.它有一个子节点
3它有两个子节点
对于第一二种情况其实可以归为一类处理
我们先看要删除的节点cur的左节点是否为空 为空的话让它的父节点curparent指向它的右节点 这样就对cur左节点为空右不为空和两个节点都为空的情况处理了 此时还需要判断父节点的右节点是cur还是左节点是cur 判断好后直接让父节点连接cur的右节点


如果这个情况不满足 再看右节点是否为空 右节点为空的话 此时就是左节点不为空右节点为空的情况了 此时同样看一下父节点的左节点是它还是右节点是它 然后让父节点连接cur的左节点

但是如果要删除的节点是根节点的话 此时这样的处理就出问题了 对于要删除节点是根节点的情况需要特殊处理一下 直接更新根节点为它的子节点

如果这两种情况都不满足的话 剩下的情况就是cur的两个子节点都不为空的情况了 此时cur有两个子节点 还用之前的方法处理父节点和子节点关系就很麻烦
这时候可以用到替换的方法(就像之前二叉树那里删除头节点时候的方法) 找到一个合适的节点和这个要删除的节点值进行交换 然后删掉这个和cur交换值的节点
那么怎样的节点是合适的节点呢 又怎么找到呢
和cur节点交换值的后需要让这个二叉树仍为搜索二叉树的结构 所以这个节点要大于此时cur左树的所有值 小于cur右树的所有值 并符合和父节点的关系 符合这些要求的有左树中的最大值和右树中的最小值这两个 就像下图 假设要删除的节点是8 那么此时符合要求的就是左树最大值7和右树最小值9

这两种都是可以的 我们这里选择取右树中最小值的方法 要找到也很简单直接从cur的右节点开始 如果它的左节点为空了 那么此时的节点就是又树中最小的了 否则就一直等于它的左节点来找
在找到之后 让cur位置的值更新为这个节点的值 也就是上图的9 然后删除这个节点 删除之前需要让它的父节点10的左节点(因为找这个最小值的方式就是一直向左找 所以除了刚开始cur的右节点就是右树最小值之外 找到的右树最小值的节点一定是它父节点的左节点)为它的右节点(上面的图是右节点为空了 此时右节点可能不为空)
此外还需要处理一种特殊情况:刚开始cur的右节点就是右树最小值 如下cur的右节点10直接就是右树最小值了 此时特殊处理一下直接让cur的右节点指向被删除位置的右节点

bool erase(const K& x)
{
//先找到要删除的节点
Node* cur = _root;
Node* curparent = cur; //要出了父节点和删除节点的子节点直接关系 需要存一下父节点
while (cur != nullptr)
{
if (x > cur->_key)
{
curparent = cur;
cur = cur->_right;
}
else if(x < cur->_key)
{
curparent = cur;
cur = cur->_left;
}
else //找到了 进行删除的操作
{
//先处理删除的这个节点有一个子节点或者没有子节点
if (cur->_left == nullptr) //处理删除节点左节点为空的情况 同时处理左空右存在和两个子节点都为空的情况
{
//还需要处理如果要删的节点是父节点的情况
if (cur == _root)
{
_root = cur->_right; //此时要删除根节点 根的左节点为空 直接让根节点为此时根节点的右节点
}
if (curparent->_right == cur) //同样需要找到删除的节点是父节点的左孩子还是右孩子
{
curparent->_right = cur->_right;
}
else
curparent->_left = cur->_right;
delete[] cur;
}
else if(cur->_right==nullptr) // 处理左不为空右为空的情况
{
if (cur == _root) //同样处理要删除的节点是根节点的特殊情况
{
_root = cur->_left;
}
if (curparent->_right == cur) //同样需要找到删除的节点是父节点的左孩子还是右孩子
{
curparent->_right = cur->_left;
}
else
curparent->_left = cur->_left;
delete[] cur;
}
else //剩下的就是两个子节点都为不为空的情况 都不为为空的话就不能直接进行删除
{ // 否则很难处理 可以找到一个符合的节点和要删除的节点交换值 然后把和根节点交换的节点给删除了
Node* fi = cur->_right;
Node* fiparent=cur; //这个合适的值要求满足大于左树中的值但是小于右数值 所以可以是在左树中找最大值或者是在右树中找最小值
while (fi->_left!= nullptr) //这里我用在右树中找最小值的方法 //fi初始就为根节点的右节点在右树中找到最小值只需要不断向左
{
fiparent = fi;
fi = fi->_left;
}
cur->_key = fi->_key; //更新要删除位置的值
if (fiparent == cur) //有肯刚开始fi初始节点的左节点就已经为空了 此时这种情况需要特殊处理
{
fiparent->_right = cur->_right;
}
fiparent->_left = fi->_right; //因为是在根节点的右数中找最小值 最终的节点一定是某一节点的左边且他的左节点也一定为空
//除了刚开始fi左节点就是空的情况 所以直接让父节点的左节点指向这个节点的右节点
delete [] fi;
}
return true;
}
}
//如果出了循环说明没有找到 直接返回false
return false;
}
正序打印(也就是中序遍历)
因为二叉搜索树左树小于根节点右树大于根节点这样的结构 二叉树的中序遍历直接就是从小到大的顺序打印
这里注意中序遍历需要形参为节点类型 到时候使用时候需要传根节点 而我们创建的对象为BStree类型 BStree类内的根节点root又是私有的不能直接访问
所以我们可以通过在类内提供一个返回根节点的函数来解决 但是这样使用肯定看着不舒服 我们还可以使用像之前实现归并排序非递归方式那样 再封装一层的方式
void Midbl() //中序遍历的形参类型需要为节点 但是我们创建的对象是BStree类型 且里面的root根节点为私有
{ //所以 我们可以提供一个返回根节点的函数 或者像之前实现归并非递归那样做一层封装
Midbl1(_root);
}
void Midbl1(Node* root) //中序遍历 先左再中再右 对搜索二叉树来说也就是从小到大的顺序打印
{
if (root == nullptr)
{
return;
}
Midbl1(root->_left);
cout << root->_key<<" ";
Midbl1(root->_right);
}
⼆叉搜索树key和key/value在生活中的使用情景
key
key就是我们实现的二叉搜索树里面节点存的值 它不只能存整形 还能存string类型 比较的时候就按string的方式比较
1.在生活中可以用在小区大门前检查车辆是否是小区里面居民的车辆 居民入住的时候会把他车的车牌号录入到二叉搜索树里面 在进小区大门时候会检测这个车牌号在二叉搜索树里面是否存在 存在就放行
检查的时候不是通过中序遍历查找这样时间复杂度为O(N) 而是使用从根节点开始的方式 这样时间复杂度为O(logN)(在结合之后学习的红黑树 AVL树让这个二叉树平衡)
2.检查⼀篇英文 文章单词拼写是否正确,将词库中所有单词放⼊⼆叉搜索树,读取文章中的单词,查找是否在⼆叉搜索树中,
key/value搜索场景
在节点里面再加一个模板类型的value 这样每⼀个key都有与之对应的值value 里面的增/删/查的功能在进入⼆叉搜索树后还是以key为关键字进行比较,可以快速查找到key对应value(因为比较是按照key进行的 不能对key改 但是可以改value)
简单中英互译字典,树的结构中的结点存储key(英文)和vlaue(中文),搜索时输入英文,则由这个英文通过find找到那个节点 然后打印里面的中文
如下在实现之后我们就可以实现一个单词翻译器的功能

或者如下是统计次数的功能

整体逻辑不需要更改 只需要加上一个模版 给节点增加一个模板V类型的参数value 然后insert函数里面在创建节点的地方也加上value就可以了
这样实现之后我们就可以通过往这个二叉搜索树里面插入一下对应英文匹配的中文 之后我们用find来在二叉搜索树里面找到对应英文单词的节点 然后将这个节点的第二个参数value也就是中文打印出来

template <class K,class V>
class BSnode
{
public:
BSnode(const K& key, const V& value)
:_value(value)
, _key(key)
, _left(nullptr)
, _right(nullptr)
{}
K _key;
BSnode<K,V>* _left;
BSnode<K,V>* _right;
V _value;
};
template<class K,class V>
class BStree
{
public:
typedef BSnode<K,V> Node;
bool insert(const K& x,const V& value)
{
if (_root == nullptr) //对空树的处理
_root = new Node(x,value);
Node* cur = _root;
Node* curparent = cur;
while (cur != nullptr) //直到为空了 就是要插入的位置
{
if (x > cur->_key) //要插入的数大于根的值就往右
{
curparent = cur; //cur里面存的是cur的上一个位置 cur改变之前先把它的位置存到curparent中
cur = cur->_right;
}
else if (x < cur->_key) //小于根的值就往左
{
curparent = cur;
cur = cur->_left;
}
else
return false; //不支持插入重复的元素 插入失败 返回false
}
//如果正常出了循环 那么此时cur的位置就是新插入节点的位置 那么此时为新节点开空间 然后让它的父节点指向它
if (curparent->_key < x)
curparent->_right = new Node(x,value); //此时还需要判断 cur位置的节点是父节点的右孩子还是左孩子
else
curparent->_left = new Node(x,value);
return true; //插入成功 返回true
}
Node* find(const K& x)
{
Node* cur = _root;
while (cur != nullptr)
{
if (x == cur->_key)
return cur;
else if (x > cur->_key)
{
cur = cur->_right;
}
else
cur = cur->_left;
}
return nullptr; //如过出了while循环说明没有找到要查找的值
}
bool erase(const K& x)
{
//先找到要删除的节点
Node* cur = _root;
Node* curparent = cur; //要出了父节点和删除节点的子节点直接关系 需要存一下父节点
while (cur != nullptr)
{
if (x > cur->_key)
{
curparent = cur;
cur = cur->_right;
}
else if (x < cur->_key)
{
curparent = cur;
cur = cur->_left;
}
else //找到了 进行删除的操作
{
//先处理删除的这个节点有一个子节点或者没有子节点
if (cur->_left == nullptr) //处理删除节点左节点为空的情况 同时处理左空右存在和两个子节点都为空的情况
{
//还需要处理如果要删的节点是父节点的情况
if (cur == _root)
{
_root = cur->_right; //此时要删除根节点 根的左节点为空 直接让根节点为此时根节点的右节点
}
if (curparent->_right == cur) //同样需要找到删除的节点是父节点的左孩子还是右孩子
{
curparent->_right = cur->_right;
}
else
curparent->_left = cur->_right;
delete[] cur;
}
else if (cur->_right == nullptr) // 处理左不为空右为空的情况
{
if (cur == _root) //同样处理要删除的节点是根节点的特殊情况
{
_root = cur->_left;
}
if (curparent->_right == cur) //同样需要找到删除的节点是父节点的左孩子还是右孩子
{
curparent->_right = cur->_left;
}
else
curparent->_left = cur->_left;
delete[] cur;
}
else //剩下的就是两个子节点都为不为空的情况 都不为为空的话就不能直接进行删除
{ // 否则很难处理 可以找到一个符合的节点和要删除的节点交换值 然后把和根节点交换的节点给删除了
Node* fi = cur->_right;
Node* fiparent = cur; //这个合适的值要求满足大于左树中的值但是小于右数值 所以可以是在左树中找最大值或者是在右树中找最小值
while (fi->_left != nullptr) //这里我用在右树中找最小值的方法 //fi初始就为根节点的右节点在右树中找到最小值只需要不断向左
{
fiparent = fi;
fi = fi->_left;
}
cur->_key = fi->_key; //更新要删除位置的值
if (fiparent == cur) //有肯刚开始fi初始节点的左节点就已经为空了 此时这种情况需要特殊处理
{
fiparent->_right = cur->_right;
}
fiparent->_left = fi->_right; //因为是在根节点的右数中找最小值 最终的节点一定是某一节点的左边且他的左节点也一定为空
//除了刚开始fi左节点就是空的情况 所以直接让父节点的左节点指向这个节点的右节点
delete[] fi;
}
return true;
}
}
//如果出了循环说明没有找到 直接返回false
return false;
}
void Midbl() //中序遍历的形参类型需要为节点 但是我们创建的对象是BStree类型 且里面的root根节点为私有
{ //所以 我们可以提供一个返回根节点的函数 或者像之前实现归并非递归那样做一层封装
Midbl1(_root);
}
void Midbl1(Node* root) //中序遍历 先左再中再右 对搜索二叉树来说也就是从小到大的顺序打印
{
if (root == nullptr)
{
return;
}
Midbl1(root->_left);
cout << root->_value << " ";
Midbl1(root->_right);
}
private:
Node* _root = nullptr;
};
void keyvaluetest()
{
BStree<string,string> b1;
b1.insert("run", "跑");
b1.insert("jump", "跳");
b1.insert("fly", "飞");
b1.insert("climb", "爬");
int tm = 1;
do
{
cout << "请选择你要翻译的单词:";
string s1;
cin >> s1;
auto x = b1.find(s1);
if (x)
{
cout << "中文为:" << x->_value << endl;
}
cout << "如果要退出请输入0,继续使用请输入1";
cin >> tm;
} while (tm != 0);
}
也可以用来统计出现物品次数
void keyvaluetest2()
{
BStree<string, int>b1;
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉" };
for (const auto& str : arr)
{
auto ret = b1.find(str);
if (ret ==nullptr) //find如果没有找到会返回nullptr 说明第一次出现
{ //直接插入<水果,1>
b1.insert(str, 1);
}
else
{
ret->_value++; //find不为空说明找到了 此时只需要改变value的值
}
}
b1.Midbl();
}

3681

被折叠的 条评论
为什么被折叠?



