目录
1、二叉搜索树概念
2、二叉搜索树操作
3、二叉搜索树的实现
4、二叉搜索树的应用
5、二叉搜索树的性能分析
注:本文的代码都是在VS2022下测试的。
1、二叉搜索树概念
二叉搜索树又称二叉排序树,它要么是一棵空树,要么是具有以下性质的二叉树:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值。
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值。
它的左右子树也分别为二叉搜索树。
补充:对二叉搜索树进行中序遍历得到的就是升序。
2、二叉搜索树操作
2.1 二叉搜索树的查找
a、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
b、最多查找高度次,走到到空,还没找到,这个值不存在。
2.2二叉搜索树的插入
插入的具体过程如下:
a.树为空,则直接新增节点,赋值给root指针。
b.树不空,按二叉搜索树性质查找插入位置,插入新节点。
2.3 二叉搜索树的删除
首先查找元素是否在二叉搜索树中,如果不存在,则返回,
否则要删除的结点可能分下面四种情况:
a.要删除的结点无孩子结点
b.要删除的结点只有左孩子结点
c.要删除的结点只有右孩子结点
d.要删除的结点有左、右孩子结点
看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程如下:
情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点--直接删除
情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点--直接删除
情况d:替换法,找一个结点的值替代这个要删除的值,这个用来替代的值就是 左子树的最大节点 或者 右子树的最小节点,这样才能保证删除之后,这还是一颗搜索树。
注:如果找的是 左子树的最大结点(也就是左子树的最右结点) 来替换的话,那么在交换完这两个结点的值,删除掉这个最右结点后,还要记得让这个最右结点的双亲结点指向这个最右结点的左子树(如果这个最右结点有左子树的话)。如果找的是 右子树的最小结点(也就是右子树的最左结点) 来替换的话,同理。
补充:一颗搜索树的最左结点就是最小值,最右结点就是最大值。
3、二叉搜索树的实现
#include<iostream>
template<class K> //K模型
//struct BinarySearchTree //这个名字太长了,简写一下
struct BSTreeNode //注:二叉搜索树不叫搜索二叉树的原因是,搜索二叉树的英文是这样的:SearchBinaryTree,简写就变成了SBTree,这不太友好。
{
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
BSTreeNode(const K& key = K())
:_left(nullptr)
,_right(nullptr)
,_key(key)
{}
};
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
bool insert(const K& key)
//补充:默认定义,搜索树不允许冗余(即不能有重复数据)。(但在一些扩展版本下,允许冗余,因为有些场景有用,总的来说90%的场景搜索树都不允许冗余)
{
if (_root == nullptr) //单独判断,如果_root==nullptr,直接插入就行了,无需走下面的判断
{
_root = new Node(key);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
parent = cur;
if (cur->_key < key)
{
cur = cur->_right;
}
else if (cur->_key > key)
{
cur = cur->_left;
}
else
{
return false; //不允许数据冗余
}
}
cur = new Node(key);
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
Node* Find(const K& key)
{
Node* cur=_root;
while (cur)
{
if (cur->_key < key)
{
cur = cur->_right;
}
else if (cur->_key > key)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
}
//注:当前结构的搜索树不允许修改(modify)
bool Erase(const K& key) //比较难的一点
{
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
// parent = cur; //这个赋值要在里面进行,不能在这里一上来就赋值,如果遇到这种情况,ex:1 3 5,你要删除 1,这样写的话就会崩溃
//因为把这个写在这外面的话,当 cur->_key==key 的时候,也会把cur赋值给parent,这是不对的。
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else
{
if (cur->_left && cur->_right) //如果该结点有左右子树的话,先做一下替换
{
//这边统一选择去左边找最右结点(最大值)来替换(选右边也类似)
parent = cur;
Node* tmp = cur->_left;
while (tmp->_right)
{
parent = tmp;
tmp = tmp->_right;
}
std::swap(tmp->_key, cur->_key);
cur = tmp;
}
//开始删除
if (cur->_right == nullptr) //如果该结点的右子树为空的话
{
if (cur == _root) //删根的情况需要单独判断一下,因为根没有父亲
{
_root = _root->_left;
}
else
{
//注意:这里还需要先判断一下到底是让 父亲的左指向我的左,还是 父亲的右指向我的左
if (parent->_right == cur)
{
parent->_right = cur->_left;
}
else
{
parent->_left = cur->_left;
}
}
}
else //如果该结点的左子树为空
{
if (cur == _root)
{
_root = _root->_right;
}
else
{
if (parent->_right == cur)
{
parent->_right = cur->_right;
}
else
{
parent->_left = cur->_right;
}
}
}
delete cur;
return true;
}
}
return false;
}
void InOrder()
{
_InOrder(_root);
std::cout << std::endl;
}
private:
void _InOrder(Node* root /* = _root */) //注意:这里不能给缺省值 _root,因为 _root 需要this指针调用,但是this指针本身就是形参,这样写玩不了。
{
if (root == nullptr)
return;
_InOrder(root->_left); //左
std::cout << root->_key << " "; //根
_InOrder(root->_right); //右
}
private:
Node* _root = nullptr;
};
void TestInsert()
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
BSTree<int> t;
for (auto& x : a)
{
t.insert(x);
}
t.InOrder(); //这里有个问题,没法给 InOrder() 这个函数传参,因为 _root 是私有函数,你在这里调不动。
//那么该怎么解决呢?
//给个缺省值吗? 这是不行的,给不了
//那该怎么办?
//三种方法:1、把这个测试函数定义成友元。(这个方法很不好,就一个测试函数又不是要经常用,定义成友元有点太没边界感了)
// 2、学Java,弄一个 Get() 函数,把 _root 拿出来。
// 3、看上面的操作。(封装一下,套一层)
for (auto x : a)
{
t.Erase(x);
t.InOrder();
}
}
int main()
{
TestBST();
return 0;
}
补充:
1、默认定义,搜索树不允许冗余(即不能有重复数据)。(但在一些扩展版本下,允许冗余,因为有些场景有用,总的来说90%的场景搜索树都不允许冗余)
2、搜素二叉树的实现还能用递归来解决,但是不建议这样,写代码遵循能用循环搞定,就别递归(毕竟递归的开销挺大的)。
注: KV模型 的二叉搜索树这里不写了,跟 K模型 的差不多,就是多个 Value 参数罢了。
3、K模型 不支持修改 key,KV模型 也不支持修改 key,但支持修改 value。
4、二叉搜索树的应用
1、K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
第一步,以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树。
第二步,在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
总结:K模型就是用来快速判断 key 在不在的。(就像 set)
2、KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。
该种方式在现实生活中非常常见:
比如:英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对。
再比如:统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。
总结:KV模型就是通过 关键码key 来快速判断 Value 在不在的。(就像map)
5、二叉搜索树的性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,
如果是一颗 满二叉树/完全二叉树(接近满/完全二叉树) 的树,则查找效率为:logN
如果是一颗 单支树(接近单支树),则查找效率为:N
注:插入时的顺序越接近有序,那么二叉搜索树就退化的越厉害,查找效率直线下降。
问题:如果退化成单支树,二叉搜索树失去了性能优势。那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?
答:AVL树和红黑树。
本文内容到此结束,感谢阅读,谢谢。