【C++】——二叉搜索树(详解)
HackerElite 2024-07-14 12:05:04 阅读 54
一 二叉搜索树概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
✨若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
✨若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
✨它的左右子树也分别为二叉搜索树
二 二叉搜索树的操作
和其他结构差不多,一般都为增删查改
2.1 二叉搜索树的查找
✨从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
✨最多查找高度次,走到到空,还没找到,这个值不存在
比如我们要查找6,那么就会先跟8比较,然后往左边走,比3大,就往右边走,然后就找到了所对应的值。
2.2 二叉搜索树的插入
插入的具体过程如下:
a. 树为空,则直接新增节点,赋值给root指针
b. 树不空,按二叉搜索树性质查找插入位置,插入新节点
对于插入来说,也就是比查找多了一个插入的过程
2.3 二叉搜索树的删除
对于删除来说就很有讲究了,因为你要保证它删除完以后还是一个二叉搜索树,不然就扯蛋了
✨首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:
1. 要删除的结点无孩子结点
2. 要删除的结点只有左孩子结点
3. 要删除的结点只有右孩子结点
4. 要删除的结点有左、右孩子结点
看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程
如下:
情况2:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点--直接删除
情况3:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点--直接删除
情况4:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点
中,再来处理该结点的删除问题--替换法删除
2.3 二叉搜索树的应用
K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到
的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
在二叉搜索树中检存在则拼写正确,不存在则拼写错误。
2. KV模型:每一个关键码key,都有与之对应索该单词是否存在,的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见:
比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英
文单词与其对应的中文<word, chinese>就构成一种键值对;
再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出
现次数就是<word, count>就构成一种键值对。
对于容器map和set的底层就是二叉搜索树的优化,所以二叉搜索的应用是非常广泛的。
2.4 二叉搜索树的性能
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
其实从表面上看二叉搜索树的效率是很高的,但是它也有极端情况
最优情况是第一个,类似完全二叉树,平均比较次数为lon(N),
最坏情况是第二个图,二叉搜索树退化为单支树(或者类似单支)
如果是第二个那么就让搜索二叉树没有了优势,但是后面会有对这个的改良。
三 二叉搜索树的实现
3.1 二叉搜索树结构
首先我们肯定得有一个结点用来存储内部结构
<code>template<class T>
class BSTreeNode
{
BSTreeNode<T> *left;//左指针
BSTreeNode<T> *right;//右指针
T val;
BSTreeNode(const T&key):val(key),left(nullptr),right(nullptr)
{
}
};
其次我们需要的一颗树,对于二叉搜索树来说,我们主要是删除和插入的操作,其他的其实都大同小于
template <class T>
class BSTree
{
typedef BSTreeNode<T> Node;
public:
bool insert(const T&key);
bool Find(const T&key);
bool Erase(const T&key);
private:
Node* root;
};
3.2 插入元素
对于插入元素,我们需要的是先找到那个可以插入的值,如果要插入的值已经存在,那么就插入失败,如果不存在,那么我就必须按照二叉搜索树的规则进行插入
如果不懂,那看看代码就应该会清晰了
bool insert(const T& key)
{
if (root == nullptr)//特判,如果是跟,那么直接插入不需要去找
{
root = new Node(key);
return true;
}
Node* cur = root;
Node* parent = nullptr;//以便我们找到父亲结点
while (cur)
{
if (key>cur->val)//如果插入值大于当前值,往右走
{
parent = cur;
cur = cur->right;
}
else if (key < cur->key)//同理,往左走
{
parent = cur;
cur = cur->left;
}
else//相等,说明已经存在了,插入失败
{
return false;
}
}
//这里说明找到位置了进行插入
cur = new Node(key);
if (parent -> val<key)//插入之前也要判断是插入到左边还是右边
{
parent->right = cur;
}
else
{
parent->left = cur;
}
return true;
}
以上就是插入的代码,总结下来就是,先特判,然后去找位置,找到位置进行插入,插入的时候也要判断是插左边还是右边
3.3 寻找元素
寻找元素就是插入元素的简单版本,思路就是插入元素上面的,下面给出代码
bool Find(const T& key)
{
if (root == nullptr) return false;
Node* cur = root;
Node* parent = nullptr;
while (cur)
{
if (key > cur->val)
{
cur = cur->right;
}
else if (key < cur->val)
{
cur = cur->left;
}
else
{
return true;
}
}
return false;
}
3.4 删除元素
对于删除元素,里面的细节就很多了
情况1:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点--直接删除
情况2:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点--直接删除
情况3:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点
中,再来处理该结点的删除问题--替换法删除
下面我们就根据这些操作一步一步去完成
首先我们肯定是先去寻找要删除的元素在哪里
bool Erase(const T& key)
{
if (root == nullptr) return false;
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;
}
else
{
if (parent->left == cur)
{
parent->left = cur->right;
}
else
{
parent->right = cur->right;
}
}
delete cur;
}
判断是不是左为空,如果是左为空,那么我就需要去把他的右孩子链接起来
同时我们还需要去判断他是在父亲的左还是右
第二种情况就是如果是有为空的情况,和左为空是对称的
else if (cur->right == nullptr)
{
if (cur == root)
{
root = cur->left;
}
else
{
if (parent->left == cur)
{
parent->left = cur->left;
}
else
{
parent->right = cur->left;
}
}
delete cur;
}
第三种情况也 是最为复杂的一种就是,左右都不为空
else//都不为空,替代法
{
Node* parent = cur;//这里需要把parent置成cur
Node* subLeft = cur->right;//这里我们去右边找最小的,也就是右边最左值
while (subLeft->left)
{
parent = subLeft;
subLeft = subLeft->left;
}
swap(cur->val, subLeft->val);//找到了交换
if (parent->left == subLeft)//然后判断是在父亲的左边还是右边
{
parent->left = subLeft->right;//因为是最左边,所以他只会有右子树
}
else if (parent->right = subLeft)
{
parent->right = subLeft - right;
}
delete subLeft;//链接上了,就直接删除
}
return true;
}
}
return false;
}
以上就是二叉搜索树的最关键的操作函数实现,其实不难,注意细节就行了
完整代码
#pragma once
template<class T>
class BSTreeNode
{
BSTreeNode<T> *left;
BSTreeNode<T>* right;
T val;
BSTreeNode(const T&key):val(key),left(nullptr),right(nullptr)
{
}
};
template <class T>
class BSTree
{
typedef BSTreeNode<T> Node;
public:
bool insert(const T& key)
{
if (root == nullptr)
{
root = new Node(key);
return true;
}
Node* cur = root;
Node* parent = nullptr;
while (cur)
{
if (key>cur->val)
{
parent = cur;
cur = cur->right;
}
else if (key < cur->key)
{
parent = cur;
cur = cur->left;
}
else
{
return false;
}
}
cur = new Node(key);
if (parent -> val<key)
{
parent->right = cur;
}
else
{
parent->left = cur;
}
return true;
}
bool Find(const T& key)
{
if (root == nullptr) return false;
Node* cur = root;
Node* parent = nullptr;
while (cur)
{
if (key > cur->val)
{
cur = cur->right;
}
else if (key < cur->val)
{
cur = cur->left;
}
else
{
return true;
}
}
return false;
}
bool Erase(const T& key)
{
if (root == nullptr) return false;
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;
}
else
{
if (parent->left == cur)
{
parent->left = cur->right;
}
else
{
parent->right = cur->right;
}
}
delete cur;
}
else if (cur->right == nullptr)
{
if (cur == root)
{
root = cur->left;
}
else
{
if (parent->left == cur)
{
parent->left = cur->left;
}
else
{
parent->right = cur->left;
}
}
delete cur;
}
else//都不为空,替代法
{
Node* parent = cur;
Node* subLeft = cur->right;
while (subLeft->left)
{
parent = subLeft;
subLeft = subLeft->left;
}
swap(cur->val, subLeft->val);
if (parent->left == subLeft)
{
parent->left = subLeft->right;
}
else if (parent->right = subLeft)
{
parent->right = subLeft - right;
}
delete subLeft;
}
return true;
}
}
return false;
}
void InOder()
{
_InOder(root);
cout << endl;
}
private:
bool _EraseR(Node*& root, const K& key)
{
if (root == nullptr) return;
if (root->val < key)
{
return _EraseR(root->right, key);
}
else if (root->val > key)
{
return _EraseR(root->left, key);
}
else
{
//删除
if (root->left == nullptr)
{
Node* del = root;
root = root->right;
delete del;
return true;
}
else if (root->right == nullptr)
{
Node* del = root;
root = root->left;
delete del;
return true;
}
else
{
Node* subLeft = root->right;
while (subLeft->left)
{
subLeft = subLeft->left;
}
swap(subLeft->val, root->val);
return _EraseR(root->right, key);
}
}
}
void _InOder(Node*root)
{
if (root == nullptr)
return;
_InOder(root->left);
cout << root->val << ' ';
_InOder(root->right);
}
Node* root=nullptr;
};
四 二叉搜索树递归实现
对于二叉搜索树的递归实现,这里着重去解释删除操作的递归
bool _EraseR(Node*& root, const K& key)
{
if (root == nullptr) return;
if (root->val < key)
{
return _EraseR(root->right, key);/递归去找删除的数
}
else if (root->val > key)
{
return _EraseR(root->left, key);
}
else
{
//删除
if (root->left == nullptr)//这里不需要特判根结点,因为用了引用,不需要父亲结点
//自然也不会出现空的情况
{
Node* del = root;
root = root->right;
delete del;
return true;
}
else if (root->right == nullptr)
{
Node* del = root;
root = root->left;
delete del;
return true;
}
else
{
Node* subLeft = root->right;
while (subLeft->left)
{
subLeft = subLeft->left;
}
swap(subLeft->val, root->val);
return _EraseR(root->right, key);//这里递归去右树删除,这样就省去很多麻烦
}
}
从上面代码中我们可以看出,其实少了很多的判断,尤其是父亲结点的链接判断,之所以会这样,就是因为我们用了引用,这样可以直接改变指针指向的位置,在连接的时候,就直接连接上了,自然就不需要父亲结点。
有了上面删除的递归版本,那么其他的理解起来就很容易了
bool _InsertR(Node*& root, const K& key)
{
if (root == nullptr)
{
root = new Node(key);
return true;
}
if (root->val < key)
return _InsertR(root->right, key);
else if (root->val > key)
return _InsertR(root->left, key);
else
return false;
}
bool _FindR(Node* root, const K& key)
{
if (root == nullptr)
{
return false;
}
if (root->val < key)
{
return _FindR(root->right, key);
}
else if (root->val > key)
{
return _FindR(root->left, key);
}
else
{
return true;
}
}
五 总结
以上就是搜索二叉树的大概内容了,希望对你有用
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。