二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树
二叉搜索树又具有排序的特性,当中序遍历之后,我们会发现是顺序排列的,并且二叉搜索树不允许重复,所以可以说二叉搜索树是天生去重的。
int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};
我们将上边数组内容逐一插入到二叉搜索树中,就可以得到下边这幅图,通过这幅图来探究二叉搜索树的相关操作:
要实现搜索二叉树,就必须实现二叉树的结点类,在前边数据结构中,我们学习过,一个结点类中有该结点的值,左子树的指针,右子树的指针。
并且必须实现拷贝构造函数,在后边使用new来开辟一个对象时用到,来开辟一个指定值的结点。
template
struct node
{
k _key;
node* _left;
node* _right;
//使用初始化列表初始化
node(const k& key)
:_key(key)
,_left(nullptr)
,_right(nullptr)
{}
};
我们使用模版来实现二叉搜索树,就可以实现泛型,在BStree中创建一个node* 类型的头结点。
template
struct node
{
k _key;
node* _left;
node* _right;
node(const k& key)
:_key(key)
,_left(nullptr)
,_right(nullptr)
{
}
};
template
class BStree
{
public:
typedef node node;
//在中间实现各类接口
private:
//给一个默认值,可以不用写构造函数
node* _root = nullptr;
};
构造函数比较简单,如果给了默认值,就可以不实现构造函数,如果没有给默认值,就直接构造一颗空树。
//构造函数
BSTree()
:_root(nullptr)
{}
//析构函数
~BStree()
{
_destory(_root);
}
//进行递归销毁
void _destory(node*& root)
{
if (root == nullptr)
return;
//后序进行销毁
_destory(root->_left);
_destory(root->_right);
delete root;
root = nullptr;
}
注意拷贝构造函数的参数必须引用传参,否则就会造成递归拷贝。若不实现拷贝构造,默认生成的实现的是浅拷贝。
//拷贝构造函数
BStree(const BStree& copy)
{
_root = Copy(copy._root);
}
//递归拷贝一颗树
node* Copy(node* root)
{
if (root == nullptr)
return nullptr;
node* copyRoot = new node(root->_key);
copyRoot->_left = Copy(root->_left);
copyRoot->_right = Copy(root->_right);
return copyRoot;
}
在实现了拷贝构造之后,就可以通过现代写法来直接进行赋值重载
BStree operator=(BStree t)
{
swap(t._root, _root);
return *this;
}
此时的形参t就是实参的拷贝,将t的头结点指针与this的头结点指针交换,就可以实现赋值重载。
根据二叉搜索树的性质,当树为空或者没找到时返回false,若树不为空进入循环,分以下三种情况:
1.当前结点的值小于key值,二叉树在右子树继续查找。
2.当前结点的值大于key值,二叉树在左子树继续查找。
3.当前结点的值等于key值,已经找到,返回true。
bool find(const k& key)
{
node* cur = _root;
while (cur)
{
if (cur->_key > key)
{
cur = cur->_left;
}
else if (cur->_key < key)
{
cur = cur->_right;
}
else
{
return true;
}
}
return false;
}
//主函数
bool FindR(const k& key)
{
return _FindR(_root, key);
}
//递归子函数
bool _FindR(node* root, const k& key)
{
if (root == nullptr)
{
return false;
}
if (root->_key > key)
{
return _FindR(root->_left, key);
}
else if(root->_key < key)
{
return _FindR(root->_right, key);
}
else
{
return true;
}
}
在插入时,首先判断二叉树是否为空树,若为空树,直接领根结点为插入结点即可,若不为空树,分为以下几种情况:
1.当key大于当前结点的值,在二叉树的右子树继续插入。
2.当key小于当前结点的值,在二叉树的左子树继续插入。
3.当key等于当前结点的值,返回false,二叉搜索树不允许出现重复的值。
一直往下遍历,除非出现重复的值,否则一定可以在合适的地方插入。
由于最后要进行插入,所以得定义一个parent变量来保存父结点的值.
//循环插入
bool Insert(const k& key)
{
//如果为空树,就直接进行插入
if (_root == nullptr)
{
_root = new node(key);
return true;
}
//否则不为空树
node* cur = _root;
node* parent = nullptr;
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
//此处的cur是一个临时变量,所以要插入必须令cur等于parent的左右子树。
cur = new node(key);
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
在子函数递归时,我们的参数是很有讲究的,此时参数要传引用,在传引用之后,在递归时,下一个递归函数中的root就是上一个递归函数中的root->_left的别名,所以直接修改是可以的,不需要使用循环版本的父结点来指向了。
//递归插入
bool InsertR(const k& key)
{
return _InsertR(_root, key);
}
//子函数递归
bool _InsertR(node*& root, const k& key)
{
if (root == nullptr)
{
root = new node(key);
return true;
}
if (root->_key < key)
{
return _InsertR(root->_right, key);
}
else if (root->_key > key)
{
return _InsertR(root->_left, key);
}
//如果已经有了这个数,就返回false,因为搜索二叉树不允许重复
else
{
return false;
}
}
在删除某一结点时,首先和插入和查找类似,先找到要删除的结点,在找到要删除的结点之后,也应该分为以下三种情况:
1.要删除的结点的左孩子结点为空,那么直接让该结点的父结点指向该结点的左孩子结点。
2.要删除的结点的右孩子结点为空,直接让该结点的父结点指向该结点的右孩子结点。
3.要删除的结点左右孩子结点都不为空,我们使用替换法,将要删除的结点和该结点左子树中最大的结点,或者右子树中最小的结点互换,然后再去删除。
与查找类似,循环版本必须保存父结点的值,在最后进行插入。
bool erase(const k& key)
{
node* parent = nullptr;
node* cur = _root;
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 (_root == cur)
{
_root = cur->_right;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
cur = nullptr;
}
else if (cur->_right == nullptr)
{
if (_root == cur)
{
_root = cur->_left;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
delete cur;
cur = nullptr;
}
else
{
node* min = cur->_right;
node* prev = cur;
while (min->_left)
{
prev = min;
min = min->_left;
}
swap(min->_key, cur->_key);
if (prev->_left == min)
{
prev->_left = min->_right;
}
else
{
prev->_right = min->_right;
}
delete min;
}
return true;
}
}
return false;
}
和插入相同,子函数传参时使用引用传参,此时去root就是上次递归中root->_left的别名,所以修改root就是修改上个结点的左子树或右子树。
bool EraseR(const k& key)
{
return _EraseR(_root, key);
}
bool _EraseR(node*& root,const k& key)
{
if (root->_key > key)
{
return _EraseR(root->_left, key);
}
else if (root->_key < key)
{
return _EraseR(root->_right, key);
}
else
{
node* del = root;
if (root->_left == nullptr)
{
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_left;
}
else
{
node* min = root->_right;
while (min->_left)
{
min = min->_left;
}
swap(min->_key, root->_key);
return _EraseR(root->_right, key);
}
delete del;
del = nullptr;
}
}