小编在学习完二叉搜索树(SearchBinaryTree)之后觉得虽然二叉搜索树不是很难,但是它对于后面学习C++中的AVL树和红黑树及map和set的封装都有重要的作用,因此小编今天带给大家二叉搜索树的原理及实现,话不多说,开始学习!~~~
一、二叉搜索树的底层原理
1、二叉搜索树的图片(回顾二叉树)
通过上面一张图片大家可能不能得出二叉搜索树的原理,话不多说,直接告诉大家
原理:任意一个节点的左子树的值小于这个节点的值,并且这个节点的右子树的值大于这个节点的值。通俗理解也就是小于这个节点的值放到这个节点的左边,大于这个节点的值放到这个节点的右边即可。
大家通过在C语言数据结构中学习的二叉树的基础,肯定可以明白这个二叉搜索树的结构,因为二叉搜索树和二叉树的树形结构一样,但是稍微不一样的点就是二叉搜索树有自己的原理,不只是简单的储存数据,二叉搜索树储存的数据都是有特定的意义,并且都符合二叉搜索树的原理才可以。
二、二叉搜索树的实现
1、在了解了二叉树的原理内容后,大家先跟小编写一个二叉搜索树的基础结构。
// 搜索二叉树 左边节点小于父节点,右边节点大于父节点
template<class T>
struct BSTreeNode
{
T _data;
struct BSTreeNode<T>* _left;
struct BSTreeNode<T>* _right;
BSTreeNode(const T& data)
:_data(data)
, _left(nullptr)
, _right(nullptr)
{}
};
上面就是一个二叉树的基本结构,相信大家并不陌生,并且每次创建的节点让左右节点的指针都置为空。
2、第二步,大家跟着小编实现一个二叉树的插入功能,在这里,大家应该已经明白了二叉搜索树的原理,左节点的值小于父节点,右节点的值大于根节点,因此大家可以根据插入的值的大小来判断这个这个插入的值应该插入到哪块。接下来,画图给大家介绍一个例子,大家肯定秒懂:
每次插入的值,直到比较到空节点的位置就是该值插入的位置,再明白了这个插入原理之后相信大家应该可以实现一个插入的模块了,小编已经实现完啦,将代码和注释放到下面,供大家参考:
typedef struct BSTreeNode<T> Node; // 由于节点的名字太长,所以在这里我重命名一下
bool Insert(const T& x)
{
if (_root == nullptr)
{
_root = new Node(x);
return true;
}
Node* parent = nullptr; // 用 parent 的二叉树的指针记录当前节点的父节点的指针,为插入节点做准备
Node* cur = _root; // cur 节点用来记录二叉树根节点的指针
while (cur)
{
if (cur->_data < x) // 插入的值比当前节点的值大,走到右边
{
parent = cur;
cur = cur->_right;
}
else if (cur->_data > x) // 插入的值比当前节点的值小,走到左边
{
parent = cur;
cur = cur->_left;
}
else
{
// 走到else说明搜索二叉树里面有这个值,所以就不用插入
return false;
}
}
// 走到这块表达已经找到了插入的位置
// 在插入时还应该判断当前节点应该插入到父节点的左还是右,判断完直接插入
if (parent->_left == cur && parent->_data > x)
parent->_left = new Node(x);
else
parent->_right = new Node(x);
}
3、第三步大家请跟着我学习二叉树的节点删除操作,这个可就有点难啦,不过不影响,我来告诉大家原理和操作,画图及注释,大家肯定可以很清楚的学会这个删除节点的操作。不过在删除一个节点的时候需要明白这个节点是否存在,如果这个节点存在才有删除的必要,那这里就需要大家先实现一个查找模块,查找操作和插入操作有点相同,在这里小编就不仔细讲了,直接将代码及注释放到下面供大家参考及学习。
查找操作:
bool Find(const T& x)
{
// 在这里查找二叉搜索树中是否存在 x 这个值
Node* cur = _root; // 先记录根节点方便遍历这颗二叉搜索树
while (cur)
{
if (cur->_data < x) // 如果这个 x 的值大于当前节点的值,就走向右侧
{
cur = cur->_right;
}
else if (cur->_data > x) // 如果这个 x 的值小于当前节点的值,就走向左侧
{
cur = cur->_left;
}
else
{
return true; // 如果当前节点的值等于 x 及说明已经找到 返回true
}
}
return false; // 走到这块说明没有找到 ,返回false
}
大家在实现删除节点的操作的时候需要先来理解下删除的原理,删除操作有四种情况,下面我分别画图来告诉大家这四种情况,还有这四种情况的处理方式,难度由易到难,放在下面:
情况一(删除的节点为叶子节点):
情况二(删除的节点不为叶子节点,并且存在一个子节点):
情况三(删除中间节点,并且当前节点有两个子节点):
情况四(删除中间节点,并且当前节点有两个子节点(不同于情况三)):
好了看到这块相信大家已经明白了删除操作的所有流程,那我把实现的过程代码及注释放到下面供大家参考:
bool erase(const T& x)
{
assert(_root != nullptr);
// 当只有一个根节点是特殊处理
if (_root->_left == nullptr && _root->_right == nullptr)
{
delete _root;
_root = nullptr;
return true;
}
// 删除有两种情况
// 1、删除的节点只有一个或者没有子节点
// 2、删除的节点有两个节点
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
// 第一种情况
if (cur->_data > x)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_data < x)
{
parent = cur;
cur = cur->_right;
}
else
{
// 处理的特殊情况 根节点是需要消除的节点 并且只有一个子节点
if (cur == _root && cur->_left == nullptr)
{
_root = _root->_right;
delete cur;
return true;
}
if (cur == _root && cur->_right == nullptr)
{
_root = _root->_left;
delete cur;
return true;
}
int flag = 0;
// 这里是处理第一种情况和第二种情况,把他们放在一块处理,这里有一个空指针说明已经找到了叶子节点
// 或者叶子节点的上一个节点,但是只有一个子节点
if (parent->_right == cur && cur->_left == nullptr)
{
parent->_right = cur->_right;
flag = 1;
}
if (parent->_left == cur && cur->_left == nullptr)
{
parent->_left = cur->_right;
flag = 1;
}
if (parent->_right == cur && cur->_right == nullptr)
{
parent->_right = cur->_left;
flag = 1;
}
if (parent->_left == cur && cur->_right == nullptr)
{
parent->_left = cur->_left;
flag = 1;
}
if (flag == 1)
{
delete cur;
return true;
}
break;
}
}
// 如果是正常情况下退出循环,说明没有找到要删除的数据
if (cur == nullptr)
return false;
// 第二种情况
// 可以选择在左边找一个最大值,或者在右边找一个最小值来替代删除的元素
// 这里实现的是从右边找最小的
Node* pt = nullptr;
Node* rt = cur->_right;
// 这种情况是第四种情况
if (rt->_left == nullptr)
{
// 说明这个rt的节点就是右边最小的节点
swap(rt->_data, cur->_data);
cur->_right = rt->_right;
delete rt;
return true;
}
// 走到这块说明rt不是右边最小的节点
// 这个是第三种情况
while (rt->_left)
{
pt = rt;
rt = rt->_left;
}
swap(rt->_data, cur->_data);
delete rt;
pt->_left = nullptr;
return true;
}
4、第四步就只需要处理的是按照大小顺序打印这棵树的每个节点的值,先讲解打印操作的原理
操作原理:因为这棵树的排序方式是根节点大于左子树,根节点小于右子树,所以使用二叉树的中序遍历就可以把这颗树按照大小顺序打印出来。
具体操作和数据结构中二叉树的中序遍历相同,这里我就直接向大家展示代码及注释即可:
// 查找二叉树的中序遍历刚好是顺序排序
void InOrder()
{
// 中序遍历需要递归来解决,所以这个 _root 不好传 如果直接用_root 的话 _root 就会被改变
Node* cur = _root;
_InOrder(cur);
cout << endl;
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_data << " ";
_InOrder(root->_right);
}
Node* _root = nullptr;
5、搜索二叉树的分析
如上图搜索二叉树可能会出现第二种情况,所以时间复杂度会大大提到,第二种情况的时间复杂度最后为O(N),但如果是接近平衡的搜索二叉树,时间复杂度就会接近O(nlgn),大大提高效率,所以以搜索二叉树衍生除了AVL树和红黑树后面带给大家学习,就会解决这种一边倒的情况,让搜索二叉树接近平衡。
好啦,今天的内容就到这啦,搜索二叉树的内容相信大家一定会有所收获,我们下期再见!~~~