1.二叉搜索树简介
二叉搜索树又称二叉排序树,它或者是一棵空树 ,或者是具有以下性质的二叉树 :
若它的左子树不为空,则左子树上 所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上 所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树
并且二叉搜索树的中序遍历之后是一个有序数组,就是因为先走左边的,但是左边的一定更小;最后再右边的,但是右边的一定最大。
二叉搜索树也叫BinarySearchTree 即 BST
二叉搜索树的功能:
在C语言阶段对树的学习中我们了解到,二叉树用于储存数据或者用于排序并非最优解。
二叉树的主要功能是查找:
理论上,二叉搜索树中不允许有冗余、重复的数据(这里一个4、那里一个4)
默认情况下,搜索二叉树也不支持修改节点中的数据,但是变形之后BST支持冗余或者修改。
2. 二叉树的基本接口
先完成基本结构:
cpp
#pragma once
#include <iostream>
#include <assert.h>
#include <vector>
using namespace std;
template<typename K>
class BSTNode {
public:
typedef BSTNode<K> Node;
Node* _left;
Node* _right;
K _key;//作为数据
};
template<typename K>
class BinarySerachTree {
public:
typedef BSTNode<K> Node;
protected:
Node* _root = nullptr;
};
2.1 插入
空容器的第一步是加入数据, 但是因为标准BST不允许冗余元素,
所以插入之前需要先查一下有没有这个元素,先完成一个Find函数:
写完Find之后,再进行插入。
插入的逻辑同Find,找到合适的位置之后插入:
cpp
bool Insert(const K& key) {
Node* newnode = new Node(key);
//为空单独判断
if (_root == nullptr) {
_root = newnode;
return true;
}
//非空树时,找到合适的位置再加入
Node* cur = _root;
Node* parent = _root;
while (cur) {
if (cur->_key > key) {
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key) {
parent = cur;
cur = cur->_right;
}
else {
return false;
}
//此时已经找到了合适的父节点,但至于向左插入还是向右插入还需要再判断一次
}
if (parent->_key > key) parent->_left = newnode;
if (parent->_key < key) parent->_right = newnode;
return true;
}
因为最后一遍cur已经走到nullptr了,所以需要再使用一次分支语句判断一下往哪走。
为了检查是否插入,我们还需要一个函数打印一下树,采取中序遍历能直接有序打印:
但是_root作为私有成员,想直接Inorder(_root)是不行的:
还需要套一层封装处理一下:
因为_root是不能被外部访问的,只能套一个内部套一个外部了。
在写删除节点之前,我们再来观察一下搜索二叉树:
搜索二叉树有查找和去重的作用
也可以排序+查重
查找效率并不是O(logN)
树也有可能退化成:
毕竟没有要求搜索二叉树必须是完全二叉树
所以按照最差的情况,最坏的情况的时间复杂度是O(lN)
可以用平衡二叉搜索树来解决,也就是传说中的AVL树和红黑树
2.2 删除
我们以这棵树为例:
1.叶子节点很好解决,直接删除即可。比如1和7
2.有一个孩子的,直接托孤即可,把你的孩子交给你的父节点。比如6和14
3.有两个孩子的,要找人替代。左子树的最大到右子树的最小这个区间的数据都能替代。
但是我们的实际方法就是将这两个节点其中选一个拿去替代。
至于如何找最大或者最小,把根传进去,找小就一直往左走,找大就往右走。
先将二叉树再补充的复杂一点,以删去3为例:
比方说我们用右子树的最左节点去替换(右子树的最左节点和左子树的最右节点一定满足情况或者2,也就是说最多有一个节点或者本身就是叶子节点),替换之后可以直接删除这个 右子树的最左节点 或者****左子树的最右节点
并且还有以下规律:
bst中,最左侧的节点最小,最右侧的节点最大。
这一规律在子树上同样适用。
想要删除,需先找到数据,找到了开始删除:
开始删除时,先讨论第一二种情况,两种情况可以合并,可以把第一种删除叶子节点的情况想象成将nullptr托孤给父节点:
注意:
1.为了保证能找到父节点,所以还是要用双指针法
2.分类讨论的逻辑:如果要被删除的cur的左为空,那就意味着cur要把右边托孤给父节点;但是该让parent的left去接受还是right去接受呢?所以必须再判断一次cur是parent的左还是右。
最后看两个孩子的情况:
我们先假设都用右子树的最小(左)节点来替换,当然也可以用左子树的最大(右)节点。
先去右子树找小(左d)节点:
只有left还存在,就一直往左走:
然后交换数据,将替代者的数据给到cur的位置去:
完成这一步之后就希望删除rightMin节点了。
不过想删除rightMin,必须要用到他的parent
所以还是必须双指针跟着走,所以我们创造了变量rightMinP
我们还是使用之前的加强树作为测试用例:
假设我们要删除的是3:
没有问题,那我们执行一下全部删除:
结果在删除第一个8的时候就出错了
上述代码只能解决上述场景的问题(想用4替代3)
假设我们希望:左图中删除8,或者右图中删除3,以上代码都还是存在一定的逻辑漏洞。
3右数中的最小值就是右数的根,所以最后一句rightMinP->left=rightMin->_right就不正确;
并且rightMinP是空,所以会运行错误。
8的错误就是因为我们自己将rigthMinParent设置成了nullptr。删除8的时候,cur指向的是8,但是rightMinParent指向的是10,10没有左节点,所以就不会进入while循环,rigthMinParent还是保持初始值nullptr
最后发现,要删除最后一个数据13的时候又报错了,其原因是:
我们解决了删除有两个子节点的节点的父节点的空指针情况(赋值成cur解决了),但是没有考虑至多只有一个子节点的节点的父节点是空指针的情况。
这样的情况想要删除8,就会报错。
因为parent只有进入了查找的循环才会有值:
如果根节点就是我们要删除的_root , 就会因为parent为nullptr而报错。
解决方案:单独判断即可。
这种情况只可能是:左子树为空并且cur就是_root
或者右子树为空并且cur就是_root , 所以此时parent一定是空。
cpp
if (cur->_left == nullptr) {
if (parent == nullptr)
{
_root = cur->_right;
}else
if (parent->_left==cur) {
parent->_left = cur->_right;
}else
if (parent->_right==cur) {
parent->_right = cur->_right;
}
delete cur;
return true;
}
if (cur->_right == nullptr) {
if (parent == nullptr)
{
_root = cur->_left;
}else
if (parent->_left == cur) {
parent->_left = cur->_left;
}else
if (parent->_right == cur) {
parent->_right = cur->_left;
}
delete cur;
return true;
}
完整的删除代码:
cpp
bool Erase(const K& key) {
Node* parent = nullptr;
Node* cur = _root;
//首先要去找到希望被删除的key
while (cur) {
if (cur->_key > key) {
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key) {
parent = cur;
cur = cur->_right;
}
else {
//此时找到了,准备进行删除
//先处理最多只有一个孩子的节点
if (cur->_left == nullptr) {
if (parent == nullptr)
{
_root = cur->_right;
}else
if (parent->_left==cur) {
parent->_left = cur->_right;
}else
if (parent->_right==cur) {
parent->_right = cur->_right;
}
delete cur;
return true;
}
if (cur->_right == nullptr) {
if (parent == nullptr)
{
_root = cur->_left;
}else
if (parent->_left == cur) {
parent->_left = cur->_left;
}else
if (parent->_right == cur) {
parent->_right = cur->_left;
}
delete cur;
return true;
}
else {
//删除有两个孩子的双节点
//这次我们全部都用右树的最小节点
Node* rightMin = cur->_right;
Node* rightMinParent = cur;
//先去找右节点最小的
while (rightMin->_left) {
rightMinParent = rightMin;
rightMin = rightMin->_left;
}
//找到合适的替换值了就开始交换
cur->_key = rightMin->_key;
//希望被删除的位置一定是没有左节点的。
if(rightMinParent->_left==rightMin)
rightMinParent->_left = rightMin->_right;
else
rightMinParent->_right = rightMin->_right;
delete rightMin;
return true;
}
}
}
return false;
}
3. 搜索二叉树的实践运用
关于查找,目前为止我们有四种方法搜寻:
关于搜索树的使用:
场景1:在不在 key模型(set)
场景2:通过一个值找另外一个值 key/value模型 (map)
通过一个值找另一个值,在节点中存两个数据,可以通过一个找另外一个。
直接在原模版上直接改:
可以由此实现一个小字典:
至于cin>>str是如何被判断对错的:
string的流提取是被重载了的,返回值中的istream又去重载了内置类型bool
最后判断的其实是bool的真假。
至于结束这样输入的方法:
1 . CTRL+C 但这样是杀进程,报错结束。
2 . CTRL+Z+换行 输入CTRL+Z+换行的时候让标志变为false , 退出循环
key-value的运用:
将Node节点中的数据设置成:string类型的水果名,和int类型的value用于计数