目录
前言
本文介绍了什么是二叉搜索树并实现流一些基本二叉搜索树的操作(具体见目录)
——2024.3.12
一、啥是二叉搜索树
二叉搜索树本质上就是一种二叉树,它具备二叉树的性质,同时,其还有这这样一个性质:对于每一个根节点,它左子树的任一节点键值小于根节点的键值,它任意右子树节点的键值均大于根节点的键值。对于拥有该性质的二叉树我们称其为二叉搜索树。
由于二叉搜索树是一种基于二叉树的数据结构,这实际上并不是一种“新”的数据结构,因而对于二叉树的学习,本文使用学习数据结构的方式来学习这一数据结构,对于某一数据结构的学习我们可以先从这一数据结构的“增删查改”来观察。
二、二叉搜索树的实现
1.二叉树节点的定义
在定义二叉树节点时,我们需注意,我们无法确定这一树结构在不同的用户使用时存入何种数据,所以我们在定义键值类型时可以考虑将二叉树节点写成模板的方式来解决
template<class K>
struct BSTreeNode
{
typedef BSTreeNode<K> Node;
Node* left;
Node* right;
K key; //这里key是有意义的数据,我们将其称为键值
//稍后的操作将围绕这一值展开
BSTreeNode(const K& _key)
:left(nullptr)
, right(nullptr)
, key(_key)
{}
};
2.二叉搜索树的“插入”
注意:由于二叉搜索树的性质,插入时应符合二叉搜索树这一树结构的存储逻辑,即:对于每一个根节点,它左子树的任一节点键值小于根节点的键值,它任意右子树节点的键值均大于根节点的键值。(二叉搜索树中每一节点的键值唯一)
当二叉树不为空时:
当二叉树为空时,插入的节点就直接作为根节点即可。
bool insert_node(const k& key)
{
if (_root == nullptr) //二叉树为空时
{
_root = new Node(key);
return true;
}
Node* cur = _root; //二叉树不为空时
Node* parent = nullptr;
while (cur) //当cur不是空节点的时候循环直到找到插入的位置
{ //当key大于正在检索的键值的时候,则插入目的地在此节点的右树中
if (key > cur->key)
{
parent = cur;
cur = cur->right;
}
else if (key < cur->key) //当key小于正在检索的键值的时候,则插入目的地在此节点的左树中
{
parent = cur;
cur = cur->left;
}
else //当key等于正在检索的键值的时候,则插入键值不合法
{
return false;
}
}
if (key < parent->key) //判断键值插入是在上一层根节点的左树还是右树
{
parent->left = new Node(key);
}
else
{
parent->right = new Node(key);
}
return true;
}
3.二叉搜索树的“查找”
查找二叉搜索树的节点时,只需比对cur的键值是否等于目标键值,如果等于则返回true,如果小于目标键值则去该节点的右树去查找,如果大于目标值则去该节点的左树去查找。(这里不返回目标节点的地址,可以返回,但是要考虑,如果返回地址后,根据地址改变键值后,二叉搜索树乱序的情况,这一情况我们稍后讨论)
bool find_node(const k& key)
{
Node* cur = _root;
while (cur)
{
if (key > cur->key)
{
cur = cur->right;
}
else if (key < cur->key)
{
cur = cur->left;
}
else
{
return true;
}
}
return false;
}
4.二叉搜索树的“遍历”
对于二叉搜索树的遍历有一个有意思的点就是,二叉搜索树的中序遍历一定是一个有序的递增序列。(这里就不做动图演示中序便利的过程了)。
对于二叉树的遍历,本文采用递归的方式:
void _inorder(const Node* tmp)
{
if (tmp == nullptr)
{
return;
}
_inorder(tmp->left);
cout << tmp->key << " ";
_inorder(tmp->right);
}
void inorder()
{
_inorder(_root);
}
5.二叉搜索树的“删除”
二叉搜索树的删除较为复杂,我们先梳理一下整体逻辑:
首先我们要找到删除的节点也就是要先找到键值与目标键值相匹配的节点,然后对节点进行删除,如果删除的节点无左右子树,那么直接删除即可,如果删除的节点只有左子树或右子树,只需要将对应的子树接到cur的父节点上即可 ,如果删除的节点左右子树都存在,那么此时再挪动子树是比较难实现的,我们换一种思路:我们可以以该节点为根节点,对其左右子树进行检索,找到左子树的最大值或右子树的最小值来替换cur节点的键值,而后删除替换后的节点即可(不是cur节点)
此外,还需要注意,倘若删除的是整个树的根节点(只有左子树或右子树的情况下),无法将删除节点的左或右子树插入到删除节点的父节点,因为删除节点是第一个父节点。所以该情况需要特殊处理一下。
bool earase(const k& key)
{
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (key > cur->key) //查找目标键值的节点
{
parent = cur;
cur = cur->right;
}
else if (key < cur->key)
{
parent = cur;
cur = cur->left;
}
else
{
if (cur->left==nullptr) //查找目标键值的节点结束
{
if (cur == _root) //处理只含右子树时,删除根节点时的特殊情况
{
_root = cur->right;
return true;
}
if (key < parent->key)//删除目标键值节点
{
parent->left = cur->right;
}
else
{
parent->right = cur->right;
}
delete (cur);
cur = nullptr;
}
else if (cur->right == nullptr)
{
if (cur == _root) //处理只含左子树时,删除根节点时的特殊情况
{
_root = cur->left;
return true;
}
if (key < parent->key)//删除目标键值节点
{
parent->left = cur->left;
}
else
{
parent->right = cur->left;
}
delete (cur);
cur = nullptr;
}
else //处理删除节点同时含有左右子树的情况
{
Node* rightmin = cur->right;
Node* rightminparent = cur;
while (rightmin->left) //查找右子树的最小值
{
rightminparent = rightmin;
rightmin = rightmin->left;
}
cur->key = rightmin->key; //删除节点
if (rightmin==rightminparent->left)
{
rightminparent->left = rightmin->right;
}
else
{
rightminparent->right = rightmin->right;
}
delete rightmin;
rightmin = nullptr;
return true;
}
}
}
三.有多个键值的结构咋办嘞
实际上,当有多个键值的时候,我们需要找到可以代表本节点的键值,根据该节点的键值来处理其他键值,举个例子,当我们查英汉字典的时候,是否就是根据英语找汉语,或者根据汉语找英语。据此本文下面定义的结构体设置了两个键值来模仿这一过程(当然键值的数量可能远远超过本例)。当我们查找键值的时候并对其进行操作时仍只需查找一个键值。(但是在删除函数中,我们要注意删除含有左右子树节点这一情况时,交换cur与righmin键值的操作时,要同时交换两个键值)
template <typename k, typename v>
struct BSTreeNode
{
typedef BSTreeNode<k, v> node;
node* _left;
node* _right;
k _key;
v _value;
BSTreeNode<k, v>(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:
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 (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(key, value);
if (key < parent->_key)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
return true;
}
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;
}
bool Erase(const K& key)
{
if (_root == nullptr)
{
return false;
}
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
{
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;
cur = nullptr;
return true;
}
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;
cur = nullptr;
return true;
}
else
{
Node* rightmin = cur->_right;
Node* rightminparent = cur;
while (rightmin->_left)
{
rightminparent = rightmin;
rightmin = rightmin->_left;
}
cur->_key = rightmin->_key;
cur->_value = rightmin->_value; //要注意同时交换两个键值
if (rightmin == rightminparent->_right)
{
rightminparent->_right = rightmin->_right;
}
else if(rightmin==rightminparent->_left)
{
rightminparent->_left = rightmin->_right;
}
delete rightmin;
rightmin = nullptr;
return true;
}
}
}
return false;
}
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root ->_left);
cout << root->_key << " " << root->_value << endl;
_InOrder(root->_right);
}
void InOrder()
{
_InOrder(_root);
}
private:
Node* _root = nullptr;
};
void TestBSTree()
{
BSTree<string, string> dict;
dict.Insert("insert", "插入");
dict.Insert("erase", "删除");
dict.Insert("left", "左边");
dict.Insert("string", "字符串");
dict.InOrder();
cout << endl;
dict.Erase("insert");
cout << endl;
dict.InOrder();
dict.Erase("erase");
cout << endl;
dict.InOrder();
dict.Erase("left");
cout << endl;
dict.InOrder();
dict.Erase("string");
cout << endl;
dict.InOrder();
}
int main()
{
TestBSTree();
return 0;
}
补充:
如果你看到这一部分,可能是还疑惑博主说过查找函数为什么返回布尔值而不是节点的指针,其实就像博主在上文说的:如果返回的是改节点的指针,那么就允许了用户对已有节点进行更改,这样的操作会破坏原有的二叉搜索树的存储顺序,
解决方案:如果要改变已有节点的值的话,首先要查找在二叉树中是否含有相同键值的节点,如果有那么修改就是不合理的,如果修改后的节点键值不在二叉搜索树中存在,那么应该先将该节点删除,而后重新插入更改值后的节点,这两个过程可以复用我们写的erase()、insert()函数进行实现,这里博主希望感兴趣的小伙伴自行实现如果对自己实现的不自信,可将自己实现后的代码置于评论区,供大家讨论。
——本文【完】