欢迎来到我的:世界
希望作者的文章对你有所帮助,有不足的地方还请指正,大家一起学习交流 !
前言
内容
一、什么是二叉搜索树?
二叉搜索树(Binary Search Tree, BST)是一种特殊的二叉树数据结构,满足以下性质:
左子树所有节点的值 < 根节点的值
右子树所有节点的值 > 根节点的值
左右子树也分别是二叉搜索树
这种结构使得查找、插入、删除操作的时间复杂度可以达到O(h),其中h为树的高度
如图所示:这是一棵标准的二叉搜索树:
二、BST基本操作
2.1 节点结构
这段代码使用 C++ 语言定义了一个模板结构体 BsNode,通常用于表示二叉搜索树(Binary Search Tree,BST)中的节点。
template <class K>
struct BsNode
{
K _key;
BsNode<K>* _left;
BsNode<K>* _right;
BsNode(const K& k)
:_key(k)
, _left(nullptr)
, _right(nullptr)
{}
};
解释:
template <class K>
template
是 C++ 中的模板关键字,用于定义模板。模板允许我们编写通用的代码,这些代码可以处理不同的数据类型。class K
表示定义了一个模板参数K
,class
在这里等同于typename
,意味着K
可以是任何类型,例如int
、double
、std::string
等。
struct
是 C++ 中用于定义结构体的关键字。在这里我就不详细解释这个了;
K _key;
BsNode<K>* _left;
BsNode<K>* _right;
K _key
:定义了一个类型为 K
的成员变量 _key
,用于存储节点的键值。由于使用了模板,_key
可以是任何类型,具体类型取决于在使用 BsNode
时指定的模板参数。
• BsNode<K>* _left
;:定义了一个指向 BsNode<K>
类型的指针 _left
,用于指向当前节点的左子节点。如果当前节点没有左子节点,_left
为 nullptr
。
• BsNode<K>* _right;
:定义了一个指向 BsNode<K>
类型的指针 _right
,用于指向当前节点的右子节点。如果当前节点没有右子节点,_right
为 nullptr
。
BsNode(const K& k)
:_key(k)
, _left(nullptr)
, _right(nullptr)
{}
BsNode(const K& k)
:这是 BsNode
结构体的构造函数,用于创建 BsNode
对象。构造函数接受一个类型为 const K&
的参数 k
,使用常量引用可以避免不必要的拷贝,提高效率。
:_key(k), _left(nullptr), _right(nullptr)
:这是构造函数的初始化列表,用于在对象创建时初始化成员变量。_key
被初始化为传入的参数 k
,_left
和 _right
被初始化为 nullptr
,表示该节点初始时没有左子节点和右子节点。
2.2 插入操作
通过比较节点值选择插入方向,保持BST性质,在这里我就不让具有相同键值插入进来了!!!,保持每只值都是单一值
// 该函数用于向二叉搜索树中插入一个新的键值
// 参数 key 是要插入的键值,类型为 const K&,使用常量引用避免不必要的拷贝
bool INSERT(const K& key)
{
// 检查二叉搜索树的根节点是否为空
// 如果为空,说明这是一棵空树,直接创建一个新节点作为根节点
if (_root == nullptr)
{
// 使用 new 操作符动态分配一个新的 Node 对象,将 key 作为参数传递给构造函数
_root = new Node(key);
// 插入成功,返回 true
return true;
}
// 定义一个指针 front,用于记录 cur 的父节点
// 初始化为 nullptr,在后续遍历中会被更新
Node* front = nullptr;
// 定义一个指针 cur,用于遍历二叉搜索树
// 初始化为根节点,从根节点开始查找插入位置
Node* cur = _root;
// 开始遍历二叉搜索树,直到 cur 为 nullptr
while (cur)
{
// 如果当前节点的键值大于要插入的键值
if (cur->_key > key)
{
// 更新 front 为当前节点
front = cur;
// 移动 cur 到当前节点的左子节点
cur = cur->_left;
}
// 如果当前节点的键值小于要插入的键值
else if (cur->_key < key)
{
// 更新 front 为当前节点
front = cur;
// 移动 cur 到当前节点的右子节点
cur = cur->_right;
}
// 如果当前节点的键值等于要插入的键值
else
{
// 输出错误信息,提示不允许插入相同元素
perror("不允许相同元素插入");
// 插入失败,返回 false
return false;
}
}
// 当 cur 为 nullptr 时,说明已经找到了插入位置
// 根据 front 的键值和要插入的键值的大小关系,决定将新节点插入到 front 的左子节点还是右子节点
if (front->_key > key)
// 如果 front 的键值大于要插入的键值,将新节点插入到 front 的左子节点
front->_left = new Node(key);
else
// 如果 front 的键值小于要插入的键值,将新节点插入到 front 的右子节点
front->_right = new Node(key);
// 插入成功,返回 true
return true;
}
2.3 查找操作
利用BST性质进行二分查找
// 该函数用于在二叉搜索树中查找指定的键值
// 参数 key 是要查找的键值,类型为 const K&,使用常量引用避免不必要的拷贝
bool FIND(const K& key) {
// 定义一个指针 cur,用于遍历二叉搜索树
// 初始化为根节点,从根节点开始查找指定的键值
Node* cur = _root;
// 开始遍历二叉搜索树,只要 cur 不为 nullptr 就继续循环
while (cur)
{
// 如果当前节点的键值大于要查找的键值
if (cur->_key > key)
{
// 根据二叉搜索树的性质,要查找的键值可能在当前节点的左子树中
// 因此将 cur 移动到当前节点的左子节点,继续查找
cur = cur->_left;
}
// 如果当前节点的键值小于要查找的键值
else if (cur->_key < key)
{
// 根据二叉搜索树的性质,要查找的键值可能在当前节点的右子树中
// 因此将 cur 移动到当前节点的右子节点,继续查找
cur = cur->_right;
}
// 如果当前节点的键值等于要查找的键值
else if (cur->_key == key)
{
// 表示已经找到了指定的键值,返回 true
return true;
}
}
// 当 cur 为 nullptr 时,说明已经遍历完整个二叉搜索树,没有找到指定的键值
// 返回 false 表示查找失败
return false;
}
三、BST删除操作详解(重点)
⾸先查找元素是否在⼆叉搜索树中,如果不存在,则返回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,可以直接删除。
非递归方式:
// 该函数用于在二叉搜索树中删除指定键值的节点
// 参数 key 是要删除的节点的键值,使用常量引用避免不必要的拷贝
bool Erase(const K& key)
{
// 定义一个指针 front,用于记录 cur 的父节点,初始化为 nullptr
Node* front = nullptr;
// 定义一个指针 cur,用于遍历二叉搜索树,初始化为根节点
Node* cur = _root;
while (cur)
{
// 如果当前节点的键值小于要删除的键值
if (cur->_key < key)
{
front = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
front = cur;
cur = cur->_left;
}
else
{
if (cur->_left == nullptr)
{
//if (cur->_right == nullptr)
//{
// //第一种情况:删除的是叶子节点
// if (front->_left == cur)
// front->_left = nullptr;
// else
// front->_right = nullptr;
//}
//else
//{
// //第二种情况:删除的的节点只用右节点
// if (front->_left == cur)
// front->_left = cur->_right;
// else
// front->_right = cur->_right;
//}
/
//第一种情况和第二种情况可以合并
if (cur == _root)
{
//忘了,还有删除根的一种情况
_root = cur->_right;
}
else
{
if (front->_left == cur)
front->_left = cur->_right;
else
front->_right = cur->_right;
}
delete cur;
return true;
}
else
{
if (cur->_right == nullptr)
{
if (cur == _root)
{
//还有删除根的一种情况
_root = cur->_left;
}
else
{
//第三种情况:删除的结点只有左节点
if (front->_left == cur)
front->_left = cur->_left;
else
front->_right = cur->_left;
}
delete cur;
return true;
}
else
{
//第四种情况:删除的结点有左右节点
//这种情况比较特殊:
//要么找其左子树的最大节点,替代他/或者找最右子树的最小节点,代替他,就可以
//在这里我来找右子树的最小节点
Node* replace = cur->_right;
Node* replacefront = cur;
while (replace->_left)
{
replacefront = replace;
replace = replace->_left;
}
cur->_key = replace->_key;
if (replacefront->_left == replace)
replacefront->_left = replace->_left;
else
replacefront->_right = replace->_right;
delete replace;
return true;
}
}
}
}
return false;
}
递归方式:
TreeNode* deleteNode(TreeNode* root, int key) {
if (!root) return nullptr;
if (key < root->val) {
root->left = deleteNode(root->left, key);
} else if (key > root->val) {
root->right = deleteNode(root->right, key);
} else {
// Case 1 & 2: 无子节点或只有一个子节点
if (!root->left) {
TreeNode* temp = root->right;
delete root;
return temp;
} else if (!root->right) {
TreeNode* temp = root->left;
delete root;
return temp;
}
// Case 3: 两个子节点都存在
TreeNode* temp = findMin(root->right);
root->val = temp->val;
root->right = deleteNode(root->right, temp->val);
}
return root;
}
TreeNode* findMin(TreeNode* node) {
while (node->left) {
node = node->left;
}
return node;
}
以删除节点3为例:
- 找到右子树最小节点4
- 将节点3的值替换为4
- 递归删除原节点4
删除前:
5
/ \
3 6
/ \ \
2 4 7
删除后:
5
/ \
4 6
/ \
2 7
总结
到了最后:感谢支持
------------对过程全力以赴,对结果淡然处之