前言
在前面我们已经学习了二叉树的基础操作,但是,仅仅是二叉树,没有太大的作用啊,存数据效果没有顺序表和链表好,那为啥还要学二叉树呢?
这不就来了嘛,给二叉树增加一些性质,作用不就出来了嘛。
本篇文章将介绍二叉树的进阶版本,给二叉树增加了搜索特性,搜索二叉树
搜索二叉树的概念和性质
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
1. 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
2. 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
3.它的左右子树也分别为二叉搜索树
注意:要求整棵左子树的值都小于根节点的值,而不仅仅是要求左节点的值小于根节点的值,对于右子树也是如此
ps:当然也可以处理成左子树大,右子树小,不过普遍来说,都是构成左子树小,右子树大的。
搜索二叉树的操作
这里我们先给一个数组,用这个数组的数据构成搜索二叉树
搜索二叉树的遍历
根据搜索二叉树的性质,我们知道,左子树的值全部都小于根节点的值,右子树的值全部都大于根节点的值。
所以,当我们采用中序遍历就可以得到有=升序的序列。
void INORDER(Node* root)
{
if (root == nullptr)
return;
INORDER(root->_left);
cout << root->_val << " ";
INORDER(root->_right);
}
二叉搜索树的查找
假设要寻找的值是key
牢记搜索二叉树的性质,从根节点开始向下寻找,
当key > 当前节点的时候,去右子树寻找即可
当key < 当前节点的时候,去左子树寻找即可
找到了就返回true,没找到就返回false
bool find(const DataType& key)
{
Node* cur = _root;
while (cur)
{
if (key > cur->_val)
{
cur = cur->_right;//大了就去右子树找
}
else if (key < cur->_val)//小了就去左子树找
{
cur = cur->_left;
}
else
{
return true;
}
}
return false;
}
注意:思考搜索二叉树查找的时间复杂度
是不是很多同学认为是O(logN)?
实际上并不是,应该查找高度次,但是高度就是logN了吗?
事实上,存在极端情况,二叉树会退化成单边二叉树,此时树的高度就是n - 1,
此时,时间复杂度是O(N)。
搜索二叉树的插入
记要插入值key
要插入首先要找到该在何处插入。
显然,这里的思路和查找类似
当key大了,就往右子树走
当key小了,就往左子树走
直到走到空的位置,可以插入。
找到位置后,用key构建一个新的节点,链接到到搜索树上去即可。
那么这里就需要寻找父节点了,所以,我们需要引入一个parent指针来标记父亲节点。
但是?究竟是插在父亲节点的左子树还是右子树呢?
去和父节点的值进行比较即可。
大于父节点的值,插在右边,小于父节点的值插在左边。
对于插入,如果搜索二叉树里面已经存在了该key值,那么就不重复插入,所以搜索二叉树就有了去重的特性
bool insert(const DataType& 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->_val)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;//已经有了,就不重复插入
}
}
搜索二叉树的删除
搜索二叉树最难的部分就是删除,这里需要仔细体会
对于删除节点,我们会发现树里面有三种节点
- 当前节点没有子节点
- 当前节点只有一个子节点
- 当前节点有两个子节点
对于这三种情况我们都需要进行思考。
- 当前节点没有子节点
此时我们只需要释放该节点,将父亲节点指向空即可 - 当前节点只有一个子节点
a. 左子树为空
将父亲节点指向右子树,释放当前节点即可
b. 右子树为空
将父亲节点指向左子树,释放当前节点即可 - 当前节点有两个子节点(最复杂的情况)
此时,我们就需要去找到一个合适的值,来替代当前节点。
很明显,这个值一定要大于左子树,小于右子树
那么,也就是,我们需要找到左子树的最大值,或者右子树的最小值来替代当前节点
这里我们以寻找右子树的最小值为例。
如何寻找右子树的最小值?
右子树的最小值一定在右子树的最左边,否则就会有更小的值(这里读者理解不了的话,可以画个图理解一下)
所以这里只需要设置一个指针rightMin,从cur->right开始一路向左,直到左子树为空停止。
此时rightMin就是右子树的最小值。
这是将rightMin的值给cur即可,然后删除rightMin节点
这里删除非常简单,只需要让rightMin的父亲的左指向rightMin的右即可。(所以需要引入rightMinP指针记录rightMin的父亲)
算法实现过程中的一些细节:
(1)首先对于第三种情况,我们去寻找右子树的最小节点,就是寻找右子树的最左边,但是如果右子树没有左节点怎么办?
比如这里要删除8,右子树没有左节点,此时要进行特判,让cur的右指向rightMin的右。
(2)对于1,2两种情况,需要父亲节点指向子节点的左或者右
但是,对于根节点来说,根节点没有父亲,就会出现空指针问题。
对于10这个节点就没有父亲节点,
那么此时就需要将root 指向 cur的右
右子树为空类似。
bool erase(const DataType& key)
{
if (_root == nullptr)
return false;
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (key > cur->_val)
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_val)
{
parent = cur;
cur = cur->_left;
}
else
{
//找到了,开始删除
if (cur->_left == nullptr)
{
//左边为空
if (parent == nullptr)//特判一下
{
_root = cur->_right;
delete cur;
return true;
}
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
delete cur;
return true;
}
else if (cur->_right == nullptr)
{
//右边为空
if (parent == nullptr)
{
_root = cur->_left;
delete cur;
return true;
}
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
delete cur;
return true;
}
else
{
//两个节点都为空
Node* rightMin = cur->_right;
Node* rightMinP = cur;
//找右子树最小的节点
while (rightMin->_left)
{
rightMinP = rightMin;
rightMin = rightMin->_left;
}
cur->_val = rightMin->_val;
if (rightMin == rightMinP->_right)
{
cur->_right = rightMin->_right;
}
else
{
rightMinP->_left = rightMin->_right;
}
delete rightMin;//这里不删cur
return true;
}
}
}
return false;//找不到返回false;
}
搜索二叉树的应用
搜索二叉树分为k型和kv型,上述就是以k型为例,当然,kv型类似,聪明的读者肯定能举一反三。
1、k型
K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
上面的例子就是k型,就不举例子了。
2、kv型
KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见:
比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。
void TestBSTree3()
{
// 输入单词,查找单词对应的中文翻译
BSTree<string, string> dict;
dict.Insert("string", "字符串");
dict.Insert("tree", "树");
dict.Insert("left", "左边、剩余");
dict.Insert("right", "右边");
dict.Insert("sort", "排序");
// 插入词库中所有单词
string str;
while (cin>>str)
{
BSTreeNode<string, string>* ret = dict.Find(str);
if (ret == nullptr)
{
cout << "单词拼写错误,词库中没有这个单词:" <<str <<endl;
}
else
{
cout << str << "中文翻译:" << ret->_value << endl;
}
}
}
剩下的构造 ,析构,拷贝构造比较简单,这里就不讲解了。