BST是什么?
二叉搜索树BST(Binary Search Tree),又称为二叉排序树。
该树要么是一个空树,要么具有以下性质:
序号 | BST的性质 |
---|---|
1 | 左树不空,则左树中所有节点中的值都小于根节点的值 |
2 | 右树不空,则右树中所有节点中的值都大于根节点的值 |
3 | 左右子树也是BST |
下图中的树便是一棵BST:
BST的节点插入
创建二叉搜索树比较简单,首先我们定义出二叉搜索树的节点类模板BSTNode
,该类应具有三个成员,分别是左孩子指针leftchild
,右孩子指针rightchild
,节点值val
。如下:
template<Type>
class BSTNode {
friend class BSTree<Type>;
public:
BSTNode() :val(Type()), leftchild(nullptr), rightchild(nullptr) {}
BSTNode(Type _val, BSTNode* _left = nullptr, BSTNode* _right = nullptr) :
val(_val),leftchild(_left),rightchild(_right){}
~BSTNode(){}
private:
BSTNode* leftchild;
BSTNode* rightchild;
Type val;
};
这仅仅是一个二叉搜索树的节点类,我们的目的是创建二叉搜索树。
二叉搜索树的类模板BSTree
应该要能访问BSTNode
的private
成员,所以要将BSTree
添加为BSTNode
的友元类。
不想添加友元类也行,有两种做法:
1.用struct
构造BSTNode
2.将BSTNode
的private
成员改为public
接下来就要创建BSTree
类,我们继续用模板写:
template<class Type>
class BSTree {
public:
BSTree() :root(nullptr){}
BSTree(vector<Type>& v):root(nullptr) {
for (const auto& e : v)
Insert(e);
}
public:
//此处只暴露对外调用接口,真正的方法被保护
bool Insert(const Type& data){ return Insert(root,data); }//新增节点
bool Remove(const Type& key){ return Remove(root, key); }//删除节点
protected:
bool Insert(BSTNode<Type>*& root, const Type& data);
bool Remove(BSTNode<Type>*& root, const Type& key);
}
private:
BSTNode<Type>* root;
};
用一个栗子,简单解释一下二叉搜索树的插入操作。
现需要用{50,30,40,10,70,80,2,60,90}
创建二叉搜索树,过程如下:
按照上面的思路,可以轻易写出真正的插入接口(递归):
bool Insert(BSTNode<Type>*& root, const Type& data) {
if (!root) {//节点为空
root = new BSTNode<Type>(data);
return true;
}
if (data > root->val)//插入值大于本节点值,说明应插入右树
Insert(root->rightchild, data);
else if (data < root->val)//插入值小于本节点值,说明应插入右树
Insert(root->leftchild, data);
return false;//能运行到此处,说明插入值与本节点值相同,不接受插入重复值,返回插入失败
}
BST的节点删除
单纯删除二叉搜索树的节点,而不进行后续调整,有可能会破坏树的结构。
如下图,
直接删除50,会使下面的树散架,直接丢失整棵树;
直接删除10,会丢失2;
直接删除80,会丢失90;
直接删除60,谁也不会丢,符合预期。
因此要是直接删除,不做调整,仅仅适合删除叶子节点。
我们编写代码,要尽量高内聚,低耦合,也就是说,要编写删除节点的函数,你就把删除的各种情况都写在这一个函数里,让一个函数可以完成各类节点删除的工作。
所以删除节点就有以下四类情况:
1. 删除目标左右均有
2. 删除目标有左无右
3. 删除目标有右无左
4. 删除目标无左无右
删除工作完成后,树依然还要是二叉搜索树
删除目标无左无右
设我们现在删除目标是90,那我们直接删除即可。
二叉搜索树的结构没被破坏,符合预期
删除目标无左有右
删除目标无左有右的情况下,我们只需要把删除目标的右孩子迁至删除目标处即可,如下:
注意!注意!注意!此处的说的“迁移”是迁移节点(也就是修改指针)。并不是把节点的值赋值进删除目标节点!!!
具体实现时就如下图,修改70的右指针rightchild
指向90,再删除80即可。
如果采用赋值方式,倒也不是不可以,就是需要将80的右树所有节点进行迁移才行,比较麻烦。
二叉搜索树的结构没被破坏,符合预期
删除目标无右有左
情况与目标无左有右很类似,我们只需要把删除目标的左孩子迁至删除目标处即可,如下:
二叉搜索树的结构没被破坏,符合预期
删除目标左右均有
这种情况下有两种解决方案:
1.找到删除目标左树中的最大值,进行值覆盖(绿色)
2.找到删除目标右树中的最小值,进行值覆盖(蓝色)
当值覆盖完毕后,这种类型的删除就转化成了删除叶子节点!!
一下简单了很多有没有?
按这种思路,我们也就能写出真正的删除接口了(递归)
bool Remove(BSTNode<Type>*& root, const Type& key) {
//先找到节点
if (!root)//不存在
return false;
if (key > root->val)//进右树找
return Remove(root->rightchild, key);
else if (key < root->val)//进左树找
return Remove(root->leftchild, key);
else {//找到
BSTNode<Type>* p = nullptr;
if (root->leftchild && root->rightchild) {//存在左右子树
p = root->leftchild;//进左树
while (p->rightchild) //找最大值
p = p->rightchild;
root->val = p->val;//值覆盖
Remove(root->leftchild, p->val);//删除左树的最大值
}
else {//有左无右,有右无左,无左无右
p = root;//保存删除目标
if (root->leftchild)//将左树迁移到本节点
root = root->leftchild;
else//将右树迁移到本节点
root = root->rightchild;
delete p;//删除目标
}
return true;
}
}
结语
最后,要是想要用迭代(非递归)方法写删除,会略微麻烦一些,我们需要找到删除目标的父节点pr
去修改父节点的指针指向(leftchild,rightchild
),实现如下:
bool Remove(BSTNode<Type>*& t, const Type& key) {
if (!t)//空树
return false;
BSTNode<Type>* p = t, * pr = nullptr;//pr为p的父节点
while (p)//寻找删除目标
{
if (key == p->val)//找到
break;
pr = p;//父节点更新
if (key < p->val)//进左树寻找
p = p->leftchild;
else//进右树寻找
p = p->rightchild;
}
if (p->leftchild && p->rightchild) {//左右均有
BSTNode<Type>* q = p->leftchild;//进左树
while (q->rightchild) {//找左树的最大值
pr = q;
q = q->rightchild;
}
p->val = q->val;//值覆盖
p = q;//转化为删除叶子节点
}
if (!pr)//没有父节点,说明删除目标是整棵树的根节点
{
if (p->leftchild)//左树存在
t = p->leftchild;//左树迁移
else
t = p->rightchild;//右树迁移
}
else {
if (pr->leftchild == p) {//删除目标是父节点的左孩子
if (p->leftchild)//有左无右
pr->leftchild = p->leftchild;
else//有右无左
pr->leftchild = p->rightchild;
}
else {//删除节点是父节点的右孩子
if (p->leftchild)//有左无右
pr->rightchild = p->leftchild;
else//有右无左
pr->rightchild = p->rightchild;
}
}
delete p;//删除目标
return true;
}