搜索二叉树是接下来要学习的map和set的基础,它和我们之前学习的priority_queue的二叉树结构相同;当搜索二叉树进化为平衡搜索二叉树的时候,时间复杂度可以优化为O(logn);效率非常高;
搜索二叉树的结构
由于每个节点都有两个棵子树,左子树中都是比根节点小的数据,而右子树中数据都比根节点大,而左右子树又有自己的根节点,都是如此存放数据,于是搜索树就这样形成了;而只要是新的数据存入树中就可以通过不断和根不断的比较不断的向树的深处寻找自己的位置,直到到底树的某个叶子节点处,成为这个叶子节点的节点;
了解了搜索二叉树的结构我们接下来实现一下它;
实现搜索二叉树
如何实现呢?首先我们可以从操作系统那里学到的管理数据的知识来实现管理我们的搜索二叉树——先描述在组织;我们需要把二叉树的基本节点描述出来,告诉编译器,这棵树的节点需要装载什么数据,以及怎么和其他节点联系的数据;
描述节点
template<class K>
struct treeNode {
typedef treeNode<K> node;
这些是与其他节点的联系
node* _left;
node* _right;
这就是我们装载的数据_key
K _key;
treeNode(K key)
:_left(nullptr)
, _right(nullptr)
, _key(key)
{}
};
描述好了节点之后,我们需要把这些节点组织起来管理好,使得其形成相应的数据结构,如何组织起来呢?我们将节点的集合创建成为一个类,而又因为节点之间是有指针联系的,所以我们这个类的成员变量只需要放置根节点的指针,即可访问整棵树;
上面描述了节点
我们接下来将我们的节点组织起来,形成这一棵树,而这棵树的内容就是一个个的节点
所以成员变量也就是一个节点的指针(指针节省空间)
template<class K>
class BSTree
{
public:
typedef treeNode<K> node;
private:
node* _root=nullptr;
};
那是如何通过根节点访问的呢?这就是通过我们在节点中存储的节点和其他节点联系的数据_left,_right指针了;_left指针指向key值小于当前节点的节点,_right指针指向大于当前节点的节点;清楚要如何组织之后,我们开始将数据一个一个插入,形成这个数据结构;
插入数据——增
循环法:
我们协同下面的代码边看,边理解;
首先,我们得查看我们插入节点时,树中是否有数据,如果树为空,那么我们现在插入的节点,插入后就会直接成为根节点;如果树中有数据,我们就得开始比较,开始向树的深处寻找我们插入节点的位置了,数据小于根则向根节点的_left指针处移动,大于则向_right指针处移动;值得注意的是我们因为指针的指向是单项的在我们移动至下一个节点的时候我们需要记录上一个节点的指针我们将其命名为parent指针;当找到我们插入节点位置时就使得parent指针指向我们使得新创立的节点和树产生联系,成为树的叶子节点;
循环法
bool insert(const K& key)
{
node* cur = _root;
if (_root == nullptr)
{
node* newnode = new node(key);
_root = newnode;
return true;
}
node* parent = _root;
while (cur)
{
if (key < cur->_key)//key小了
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)//key大了
{
parent = cur;
cur = cur->_right;
}
else//当key在树中有了相同是值时无法插入;
{
//cur->_left = new node(key);//测试
//return true;
return false;
}
}
node* newnode = new node(key);
if (newnode->_key < parent->_key)//判断我们的节点在原节点的左边还是右边
{
parent->_left = newnode;
}
else if (newnode->_key > parent->_key)
{
parent->_right = newnode;
}
return true;
}
递归法:
递归时一定伴随着根节点的变化(树的子树也会有子树的根节点);所以函数传参时,得需要一个root指针来接收当前需要操作的节点的指针,而我们的根节点就是第一个需要操作的节点(因为类中存储的只有一个根节点成员);可是我们写的函数是接口函数,是和成员_root不同的,它是开放出来的函数,我们在传参的时候无法直接传递_root根节点给这个递归函数;于是乎我们需要在函数中套一层函数,这个接口中的函数才是递归函数,而又因为这个函数实现是在类中的所以_root根节点是可以成功传递的;
递归首先需要的就是结束条件;当我们递归到的节点为空的时候就说明已经找到我们插入树中需要的叶子节点位置了;实现完结束条件后,我们实现一下递归条件;我们如果key值大于当前节点则向右递归小于则向左;不断向深处递归直到寻找到空成功插入,或者遇到key值相同数据无法插入;
递归法
bool res_insert(K key)
{
return _res_insert(_root, key);
}
递归
bool _res_insert(node*& root, K key)
{
if (root == nullptr)
{
node* newnode = new node(key);
root = newnode;
return true;
}
if (key < root->_key)
{
return _res_insert(root->_left, key);
}
else if (key > root->_key)
{
return _res_insert(root->_right, key);
}
else
{
return false;
}
}
查看数据是否存在——查
查找数据还是可以用上面类似的循环法进入while循环中比较,也可以使用下面的递归方式,采用递归遇到相同数据时使得返回值+1从而获得相应数据出现在树中的次数,因为当前树是搜索二叉树,数据出现次数最多为1所有只有0,1两种情况出现;
int count(K key)//统计个数(但是在搜索二叉树的时候是寻找key值是否在树中,在则为1不在则为0)
{
return _count(_root, key);
}
int _count(node* root, K key)
{
if (root == nullptr)
return 0;
int count = 0;
if (key == root->_key)
count++;
return count + _count(root->_left, key) + _count(root->_right, key);
}
删除节点——删
循环法:
其实删除数据的方式和插入时差不多,插入时是在循环退出时,找到了空的位置来增加新的节点,而删除时,则是找到那个key值相同的节点,来删除这个节点;所以我们删除的位置是会在循环中的;而我们删除节点会有这几种情况:
1.左子树为空
2.右子树为空
3.左右子树都为空
4.左右子树都不为空
我们而第3点可以和上面两种的任意一点相结合;
所以,当我们寻找到了要删除的节点时,我们需要做的是,分三种情况来删除,1,2两种情况时,我们需要记住要删除的节点的父亲节点并循环到要删除节点处,然后把将删除节点的子树连接到父节点原来连接删除节点的指针处即可;我们这里实现1,2情况的时候,一定得把3情况加入,否则,在这棵树为歪脖子树只有一边有节点,删除根的时候,会发生数据丢失情况(根被delete),导致程序崩溃;所以我们需要加入cur==_root这一判断;
第4种情况时,我们需要找到删除节点的左子树的最大节点或者是右子树的最小节点来替换删除节点的数据,从而删除那个最大或者最小节点(因为这种节点是最左或者最右节点),它们最多只有一个子树,只需要将子树连接到删除节点的父亲节点上就好了;
这里需要注意的是:因为有特殊情况,删除节点为根节点的时候,我们根节点没有父亲节点了,所以我们得让根的父亲节点等于它自己,这样即使要删除根,也是将根的子节点链接成为自己的子节点不会出现空指针解引用问题;
循环法
bool erase(K key)
{
if (_root == nullptr)
{
cout << "该树为空无可删除节点" << endl;
return false;
}
node* cur = _root;
node* parent = _root;
while (cur)
{
if (key < cur->_key)//向左找
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)//向右找
{
parent = cur;
cur = cur->_right;
}
else//找到并删除1.左子树为空,右子树为空,左右子树都为空2.左右子树都不为空
{
node* del = cur;
if (cur->_left == nullptr)//左为空或左右为空的情况
{
if (cur == _root)//如果要删除的就是根节点时
{
_root = cur->_right;
}
if (cur->_key > parent->_key)
{
parent->_right = cur->_right;
}
else
{
parent->_left = cur->_right;
}
}
else if (cur->_right == nullptr)
{
if (cur == _root)//如果要删除的就是根节点时
{
_root = cur->_left;
}
else if (cur->_key > parent->_key)
{
parent->_right = cur->_left;
}
else
{
parent->_left = cur->_left;
}
}
else//左右都不为空
{
node* prightmin = cur;
node* rightmin = cur->_right;//寻找右子树的最小节点与当前节点值互换,然后删除那个最小节点;
while (rightmin->_left)
{
prightmin = rightmin;
rightmin = rightmin->_left;
}
if (rightmin->_key > prightmin->_key)
{
prightmin->_right = rightmin->_right;
}
else
{
prightmin->_left = rightmin->_right;
}
del->_key = rightmin->_key;
del = rightmin;
}
delete (del);
return true;
}
}
cout << "没有找到该节点:"<<key << endl;
return false;
}
递归法:
递归法传递的是root指针的引用,所以是不需要记录当前指针的父亲节点的,我们直接修改当前节点就可以做到连接的效果;
bool res_erase(K key)
{
return _res_erase(_root,key);
}
bool _res_erase(node*& root, K key)
{
if (root == nullptr)
return false;
if (key < root->_key)
{
return _res_erase(root->_left, key);
}
else if (key > root->_key)
{
return _res_erase(root->_right, key);
}
else
{
node* del = root;
if (root->_left == nullptr)//左为空or左右都为空
{
root = root->_right;
delete del;
return true;
}
else if (root->_right == nullptr)//右为空
{
root = root->_left;
delete del;
return true;
}
else//左右都不为空
{
node* rightmin = root->_right;
while (rightmin->_left)
{
rightmin = rightmin->_left;
}
swap(root->_key, rightmin->_key);
return _res_erase(root->_right, key);
}
}
}
中序打印
中序打印可以直接获得打印好的数据;
void print()
{
_print(_root);
cout << endl;
}
void _print(node* root)//中序遍历
{
if (root == nullptr)
return;
_print(root->_left);
cout << root->_key << " ";
_print(root->_right);
}
上面就是通过增删查来管理我们搜索二叉树的代码;
下面是二叉树代码的完整版,可以通过这个链接查看,本文章是第二个实现;
在BSTree.cpp中
C++代码/搜素二叉树 · future/my_road - 码云 - 开源中国 (gitee.com)
二叉搜索树的作用
K模型
当我们的二叉搜索树只有一个K key数据的时候,我们可以称其为K模型;这个K模型可以用来匹配树中的数据,如果检索到树中有相同数据时可以实现某些功能;
例如:
门禁系统,门禁系统中存储了很多的人的人脸信息;我们录入人脸之后,我们通过门禁时,门禁就可以从搜索树中快速寻找我们的匹配数据,如果数据不存在则无法通过,存在则会开门;
词典查单词,停车场停车扫车牌,都可以使用类似的K模型来处理;
KV模型
当我们树中数据变为两个的时候K key,V value;这个时候一个节点中有两个数据,这样可以形成键值对,我们可以完成统计,或者翻译类似的功能;
看下面就是通过kv模型实现的翻译功能:
kv模型实现:
在BSTree.cpp代码中