目录
一、概念:
二叉搜索树:二叉搜索树又称二叉排序树,其具有以下特点:
1.可能为空树
2.其左子树的节点值均小于根节点的值
3.其右子树的节点值均大于根节点的值
4.其左右子树均构成二叉排序树
示例:
二、代码实现二叉搜索树
1. 二叉搜索树的查找
假设我们要查找的值为6,假设要查找的值为key,由二叉搜索树的特点可知
a. 从根节点开始进行查找,如果要查询的key值小于根节点,就进入左子树进行查找,如果要查询的key值大于根节点,就进入右子树进行查找。
b.最多查找的次数为二叉搜索树的高度次,如果走到空的位置,则未查到,这个值不存在。
根据以上描述,我们根据上图对查找的过程如下:
我们可以看出当key=6时,此时从根节点开始,key小于根节点的值,因此,要从根节点的左子树进行查找,此时到节点3处,可以得知key的值大于3,因此要在节点3的右子树进行查找。此时找到等于key的节点。
上述过程用代码进行描述:
首先定义一个二叉搜索树节点的结构体:
template<class K>
struct BSTreeNode
{
typedef BSTreeNode<K> Node;
BSTreeNode(const K& val)
:_left(nullptr)
,_right(nullptr)
,_val(val)
{}
Node* _left;
Node* _right;
K _val;
};
2. 二叉搜索树类的实现
template<class K>
class BSTree {
public:
typedef BSTreeNode<K> Node;
private:
Node* _root = nullptr; //将根节点初始化为空
};
接下来根据上述描述实现查找功能函数:
bool Find(const K& val) // 传入要查找的key的值 val
{
Node* cur = _root; //定义一个当前二叉搜索树的指针指向root
if (cur == nullptr) //如果当前指向root节点的指针为nullptr,表示当前树为空 返回false
{
return false;
}
while (cur) //对树进行查找,如果为空,则表示未找到,退出循环
{
if (cur->_val < val) //如果当前节点的值小于要查询的key值
{
cur = cur->_right; //向当前节点的右子树进行查找
}
else if (cur->_val > val) //如果当前节点的值大于要查询的key值
{
cur = cur->_left; //向当前节点的左子树进行查找
}
else
{
return true; //此时找到相等的节点
}
}
return false; //未找到返回false
}
3. 二叉搜索树的插入
a. 当前树如果为空树,即根节点为空(root==nullptr),则申请一个新的节点,并赋值给根节点
b.如果树不为空,则根据二叉搜索树的性质找到合适的插入位置,插入新的节点。
假设我们要插入的节点为15,那么根据图我们可以表示为:
根据图示可以看出,如果要插入15这个节点,首先从根节点进行查找其合适的插入位置,根据左子树均小于根节点以及右子树大于根节点的性质,可以找到15的插入位置为节点14的右子树。
据图所示,当遍历二叉搜索树找合适的插入位置过程当中,结束条件为找到节点为空的位置,而当插入新节点的过程中,需要找到空节点的上一个位置链接新的节点,因此我们需要保存相关的节点。
代码实现如下:
bool insert(const K& val) //传入要插入的节点值
{
if (_root == nullptr) //如果根节点为空,即空树,那么申请一个新的节点,并返回true
{
_root = new Node(val);
return true;
}
Node* cur = _root; //定义一个节点cur指向根节点
Node* parent = nullptr; //保存父节点,链接新的节点
while (cur) //如果cur为空,则代表找到了插入的位置
{
if (cur->_val < val) //如果当前节点的值小于要查询的key值
{
parent = cur; //更新父节点为当前节点的值
cur = cur->_right; //向右查找插入位置
}
else if (cur->_val > val)//如果当前节点的值大于要查询的key值
{
parent = cur; //更新父节点为当前节点的值
cur = cur->_left; //向左查找插入位置
}
else
{
return false; //如果找到了与要插入值相同的节点,那么就无法插入,返回false
}
}
cur = new Node(val); //这里已经找到了合适的位置,此刻申请一个新的节点赋值给cur
if (parent->_val < val) //此刻parent指向要插入的节点的父节点位置
{
parent->_right = cur; //如果父节点的值小于要插入的值,则将其链接到右子树
}
else
{
parent->_left = cur; //如果父节点的值小于要插入的值,则将其链接到左子树
}
return true; //插入成功
}
4. 二叉搜索树的删除
a. 首先查找当前二叉搜索树中是否存在要删除的节点,如果不存在则返回
b. 根据二叉搜索树的性质可知:二叉搜索树的左子树和右子树分别都是二叉搜索树,所以在删除节点的过程中应当始终保证二叉搜索树这一性质,因此删除节点分为以下几种情况:
(1) 删除叶子节点(即无孩子节点):直接进行删除,删除叶子节点并不会改变二叉搜索树其他节点的位置。
如图所示,如果要删除的节点为叶子节点时,只需找到其所在位置对其进行删除即可,二叉搜索树其他节点依然构成二叉搜索树。只需将删除位置节点的父节点的指向置空即可。
(2)要删除的节点只有一个孩子节点:
此处我们以14这个节点为例,其只有13这一个左孩子节点。由图可知,如果我们将14这个节点删除,那么我们需要将其左孩子节点13与其父节点10链接在一起,根据搜索树的性质,我们可以很清楚的看出应当将13链接到10的右孩子处。如下图所示:
接下来我们看下一种情况,如图所示,如果我们此刻要删除的是3这个节点,那么就要将3的孩子节点连接到节点3的父节点,那么根据搜索树性质可知,左子树小于根节点,右子树大于根节点。那么节点3的右孩子节点6需要链接到父节点8的左孩子处。
因此会得到如下情况:
由以上两张图我们可以看出,同样都是删除只有一个孩子的节点,其操作是相同的。
只需将要删除节点的孩子节点链接至要删除节点的父节点处即可。
但是父节点在链接节点的时候,应该链接到左边还是右边呢?
根据上图我们可以看出,如果我们要删除的节点是父节点的右节点,那么就将要删除节点的孩子连接到父节点的右孩子处,相反,如果我们要删除的节点是父节点的左孩子,那么就将要删除节点的孩子连接到父节点的左孩子处。
同时我们也可以看到,我们可以将(1)和(2)归类为同一种情况:
在(1)中删除叶子节点的情况,最终都是将要删除节点的父节点连接至空,而(2)中情况是将要删除节点的父节点连接至其左孩子或右孩子处,此时(1)中的情况便会成为一种特殊情况。
(3)要删除的节点有两个孩子节点:
如果要删除的节点既有左孩子又有右孩子,那么就无法使用上述的方式来进行删除,因为要始终保持二叉搜索树的左右子树均为二叉搜索树,即左子树均小于根节点,右子树均大于根节点。
因此,我们可以采取如下方式进行删除此类节点,假设我们要删除的节点为3这个节点,其既有左孩子又有右孩子:
我们可以找到其右子树中最左侧的值,即其右子树中的最小值与其进行交换值:
此时我们只需删除4这个节点即可。方法如下:
a. 首先找到右子树最小的节点,我们定义为rightMin
b. 此时我们只需删除右子树中最小值这个节点即可
c. 我们所找到的是右子树中的最小节点,根据性质我们可以知道最小值节点应是右子树中最左的节点,因此最小值节点不会存在左孩子,但是有可能存在右孩子。
d.根据c描述,我们可以知道删除这个节点的时候遵循的内容是(2)中所述删除只有一个孩子节点时的情形,因此不做过多描述。
最终删除代码实现如下:
bool erase(const K& val)
{
if (_root == nullptr)
{
return false;
}
Node* cur = _root; //当前节点指向根节点
Node* parent = nullptr; //定义一个父节点
while (cur) //首先是查找要删除的节点,如果未找到则返回false
{
if (cur->_val < val) //如果当前节点的值小于要删除的结点的值
{
parent = cur; //保留父节点的位置
cur = cur->_right; //向右子树进行查找
}
else if (cur->_val > val)//如果当前节点的值大于要删除的结点的值
{
parent = cur; //保留父节点的位置
cur = cur->_left; //向左子树进行查找
}
else { //此时代表找到了要删除的节点
if (cur->_right == nullptr) //如果当前节点的右孩子为空,则表示此时是要删除的节点只有一个孩子或者无孩子
{
if (cur == _root) //如果此时节点为根节点
{
_root = cur->_left; //要将根节点更新至左孩子处
}
else
{
if (cur == parent->_right) //如果当前要删除节点为父节点的右孩子
{
parent->_right = cur->_left; //将要删除节点的孩子链接至父节点的右孩子处
}
else { //如果当前要删除节点为父节点的左孩子
parent->_left = cur->_left; //将要删除节点的孩子链接至父节点的左孩子处
}
}
delete cur; //删除节点
return true;
}
else if (cur->_left == nullptr) //此处与上述思路相同,逻辑相反
{
if (cur == _root)
{
cur = cur->_right;
}
else
{
if (cur == parent->_right)
{
parent->_right = cur->_right;
}
else
{
parent->_left = cur->_right;
}
}
delete cur;
return true;
}
else // 此处代表要删除的节点不仅有左孩子也有右孩子,无法直接删除
{
Node* rightMin = cur->_right; //定义一个右子树最小节点的指针
Node* parent = _root; //定义父节点
while (rightMin->_left) //查找右子树中值最小的节点
{
parent = rightMin; //更新父节点
rightMin = rightMin->_left;
}
cur->_val = rightMin->_val; //将右子树中最小节点的值赋值给要删除的节点
if (rightMin == parent->_left) // 此时只需删除右子树中最小值的节点即可,此时要删除的节点变为cur
parent->_left = rightMin->_right;// 如果当前节点的值是父节点的左孩子,将当前节点cur的右孩子链接至父节点的左孩子
else
parent->_right = rightMin->_right;//如果当前节点的值是父节点的右孩子,将当前节点cur的右孩子链接至父节点右孩子
delete rightMin; //删除节点
return true;
}
}
}
return false;
}