作者:几冬雪来
时间:2023年11月7日
内容:C++的搜索二叉树讲解
目录
前言:
在上一篇博客中我们艰难的结束了C++部分多态知识的讲解,多态属于C++的一个重要的知识。而在多态结束之后,下来要讲解的就是C语言时期二叉树进阶部分的知识,那在讲解二叉树的进阶之前这里要先了解一下搜索二叉树。
什么是搜索二叉树:
在学习搜索二叉树之前我们先要了解什么是搜索二叉树。
在C语言学习时期我们就有学习过普通的二叉树,而搜索二叉树就是在普通二叉树中再添加一些新的特征。
那么它有什么不同的特征就成为了讨论的话题。
在这个地方如果左子树所有的节点都小于根,右子树的所有结点都大于根,每一棵子树都满足这个特征,这里这棵二叉树就是搜索二叉树。
那为什么这样的树被称为搜索二叉树?
这里的搜索二叉树就是字面意思,方便搜索,因为这里左树的节点都小于根,右树的结点都大于根,加入我们要寻找的一个值比根大,这个地方我们就可以直接访问右树不用理会左树节点的值。
那么在这个地方如果要从搜索二叉树中找到一个值,那么它最多是不是找高度次(O(N))。
其实不然,通常我们的二叉树都是左边这种形式,都是如果将左边这棵二叉树换成根节点非常大的话就会变成右边这样的树。
这个时候我们就不能简单的去分析它们搜索所用的时间。
那么这个地方就会涉及到另外两棵树——红黑树和AVL树。
与此同时在这个地方搜索二叉树也可以被叫做二叉查找树和二叉排序树。
那么它为什么又被叫为二叉排序树呢,这里就要谈一谈这棵树的中序或者中序遍历是一个什么样子的情况了。
如果搜索二叉树在这里走的是中序的话,那么它最后出来的结果就是有序的,因为搜索二叉树是左小右大的形式,中序排序的结果就是——左->中->右,所以说它的结果是有序的。
那么接下来我们就来书写搜索二叉树的代码了。
搜索二叉树的增删查改:
再接下来我们就要讲解搜索二叉树的增删查改了。
在C语言时期我们并没有书写普通二叉树的增删查改,这并不意味着二叉树的增删查改没有书写的必要,知识因为在普通二叉树中,因为插入的地方没有限制哪里都可以进行插入操作,所以使得这些操作没有意义。
但是类似搜索二叉树这种特殊二叉树,对比起普通二叉树它就有限制插入的地方,所以特殊二叉树的增删查改是有意义的。
搜索二叉树的定义初始化:
还是一样写二叉树的时候我们通常都会通过定义一个类来存储节点的左节点右节点和它自己本身的值 。
这个地方的第一个模板我们用其来构造左右节点。
第二个模板在私有处定义一个_root来作为头节点。
最后的在第二个模板之中我们要typedef第一个模板将它重新命名,然后再对我们的头节点进行初始化。
搜索二叉树增操作:
定义和初始化搜索二叉树之后,接下来我们就要根据搜索二叉树来书写它的增的操作。
在这个地方我们定义一个bool函数,再然后就是进行判断。
这里进行判断,如果一开始我们的树里面没有值,也就是树为空。这个时候就需要我们new一个节点作为我们的头节点使用。
如果这棵树不是空的话这里就要定义一个cur和parent指针,然后进入循环判断以cur为条件,如果cur为空就意味着到了空节点。
然后就是判断,如果父节点存放的值小于要插入的值这个地方就放在左边,反之则放在右边,这里注意有一个特殊情况,那就是要存的值和节点的值相同,这种情况我们的数据就不能进行存储,这里就需要返回false。
但是上边的代码只是单纯的进行比较确定位置而已,这个地方要完成最终的插入操作的话,这里还要创建一个新的节点给cur。
然后进行第二次比较,因为parent这个时候因为没有进行重新的赋值初始化,因此这里只需要判断一次然后将节点插入即可。
这里的最后就是书写二叉树中序遍历的代码,先走左边然后走中间最后走右边,三者都是以递归的形式。
搜索二叉树找操作:
然后就是搜索二叉树中一个比较简单的操作,也就是查找操作。
那么搜索二叉树的查找操作的代码要如何书写呢?
这里就是我们查找操作的代码,它的代码甚至可以沿用搜索二叉树中增操作的代码, 因为查找操作比较简单,因此也不过多的讲解。
搜索二叉树删操作:
与搜索二叉树查找操作的难度不同,删除可谓是搜索二叉树中增删查改里面最为困难的一个操作,删除操作才是搜索二叉树的重点。
这个地方的删除操作分为几种情况。
第一种情况就是要删去这棵树值为7的节点,这里因为值为7的节点刚刚好是我们的叶子节点,因此这个地方只需要将值为6节点的右子树置空即可。
接下来就是第二种情况,要删除的节点是值为14的节点。这里值为14的节点只有一个叶子节点,这里也是十分的容易,只需要将值为10的节点右子树链接上值为13的节点即可。
当然以上两种删除都是较为简单的问题,接下来就是搜索二叉树难操作的地方了。
最后一种情况就是该节点有两个子节点,这里拿头节点举例子,假如这个地方我们将头节点删去,那么这棵树要怎么进行调整。
这个地方我们不能顺便的拉一个节点来作为这棵树的头节点。
这里解决问题的方法就是从这一棵树中找出左树最大值的节点又或者是右树最小值的节点来作为我们的头节点,这样才能真正的解决问题。
接下来我们就来书写搜索二叉树删除操作的代码。
这个地方要实现删除操作,首先还是要定义一个自身节点和父节点,然后对其进行初始化的操作。
如果然后就是判断节点值与左右子树的大小,如果我们要查找的值小于父节点的值就像左走,反之向右走。
然后就是找到了的情况,这里先处理两个简单的情况,那就是这个地方删除的节点刚好左右子树右一边为空。
然后对要删除的值之后就需要进一步的进行判断,最后进行链接,然后将cur的值进行删除即可。但是这个地方还有一种特殊的情况需要处理。
如这里的这张图,这棵树只有右树,如果这里要删除的节点是数据为8的节点,这会导致程序的崩溃。
而且应对这个问题的方法就是将根节点进行转移,从值为8节点转移到下一个节点中去。
解决这种情况的方法就是在判断是否为空之后,对根节点进行判断,如果根节点就是我们要删除的值,这里就将下一个值定为新的根节点。
接下来还有一种最复杂的情况,那就是左右都不为空的情况。
这个地方如果左右都不为空的话,就需要我们去找左树的最大值又或者右树的最小值和这里要删除的值进行一个替换操作。
这里我们就找左树的最大值,这里一开始就走节点的左边,接下来就是进入循环,因为我们这棵树是搜索二叉树,因此条件就是节点一直走右边,直到空为止。
但是这里的代码还有许多坑的地方存在。
首先就是替换的问题,因为在图中7和8节点替换之后,这里我们就要重新编译一遍,所以为了防止找不到要替换的值,这里我们需要找到父节点。
然后就是它的代码。
但是如果这样子书写的话又会有特殊情况的出现。
特殊情况也会导致最后代码的崩溃。
就如上图,原本值为3的节点有左树无右树,同时它也是根节点的下一个节点。这个地方如果要3和8的节点进行调换的话。
如果是这种情况的话,判断条件的leftMax->_right指向的刚刚好是空,这会导致循环进不去有间接的让父节点依旧为空。
这里经过一系列的修改之后,我们的代码呈现这个样子。首先为了防止父节点为空的情况发生,一开始我们就不将它初始化为空。
然后就是判断,如果父节点的下一个结点正好是左树最大节点(要删除的节点),同时这个它的右树为空。
这里我们就要将父节点的左给leftMax的左,反之则是父节点的将leftMax的左给父节点的右。最后将leftMax给给cur,然后将cur进行delete操作即可删除这个值的节点。
搜索二叉树增删查改递归版本:
上边讲解了搜索二叉树的增删查,这个地方我们并没有再写搜索二叉树修改数据的代码,因为在这之前我们要实现搜索二叉树增删查递归方式的书写。
这里就要查找操作来举例。
这里要注意一个点,那就是在C++当中凡是要递归走树型结构我们都需要写一个东西。
这个地方要想写搜索二叉树增删查改递归版本的话,首要的就是要在私有类中书写一个子函数,因为这里我们要实行递归操作就必须有数的变化。(有变化才能递归)
因此如果要书写搜索二叉树递归部分的代码的话,我们就要将可能会发生变化的root加上。
搜索二叉树查操作(递归版本):
知道了这一个知识点之后,接下来我们就来书写搜索二叉树查操作的代码。
这里的查操作的递归相较于删和增的递归版本要容易一些。
这个地方我们的代码这样书写即可。如果值大于要找的值,这个地方就进入递归它的右边,如果小的话就递归它的左边,如果找到的话,这里我们就只需要返回true即可。
这就是查找操作递归方法的书写。
搜索二叉树增操作(递归版本):
在讲解完了二叉树递归版本的查找数据后,接下来就来讲解搜索二叉树的增操作代码要如何去书写。
这就是我们插入时候的代码了,中间查找比较大小的操作,这段代码可以直接将查操作的代码拿过来使用,这里就不过多的讲解了。
但是这个地方的代码有些许的不同,可以看见在函数对象中我们为root加入了一个引用操作。
这是因为要在递归判断之后,顺便将我们要插入的节点插入进去。
对象中引用操作也就是在这里进行使用,这里root如果为空的话,也就是找到了要插入的位置。这个地方就不需要像以前一样重新遍历一遍找到父节点进行插入。
因为有引用的存在,这个地方如果root为空找到插入的位置,因为是引用因为我们就能直接new一个新节点出来,将值放进去,这样就能完成需求。
搜索二叉树删操作(递归版本):
在这个地方搜索二叉树的删除操作递归形式可以算我们增删查改中最为困难的一个操作了。
在平时考试,问答搜索二叉树的问题,一般都是提问搜索二叉树增删查改中删除操作,那么下面我们就来看看删除操作要怎么书写?
这个地方要进行删除操作的话,首先还是免不了使用查找操作来找到我们想要删掉值的节点,这里简单来说就是进行比较。
如果大于就走左边,小于就走右边。
最后也是当我们找到要删除值的节点了,这里的代码是最具有难度的,因为删除操作要兼顾三种情况,也就是左为空,右为空和左右都不为空的情况。
这里如果两边有一边为空的情况我们就像上图一样书写,这个地方在一开始要为root加一个引用,这样子就可以通过引用来改变指向,从而做到删除的作用。
类似有一个值为14的节点,它有一个父节点和一个右边的子节点,这里判断后将root->_right赋给root,这样就使得递归回去14的父节点会指向14的子节点。
然后就是两边都不为空的情况。
这里先定义一个del,在else中我们先找左树的最大节点,找到那个节点之后在将根节点和这个左树最大节点替换。
然后再走一遍EraseR进行删除操作,这个地方要记得传的参数为root->_left而不是leftMax,这是因为leftMax为临时变量,如果传临时变量的话再怎么修改递归中的root,它都没有实际效果。
其余操作:
在讲解完了两个版本的增删查改后,接下来我们就简单将剩下的一些操作的代码给它写上和补齐。
析构函数:
有一开始有构造函数,那么结束的时候就有析构函数,在这里我们就来书写搜索二叉树的析构函数,用递归的形式。
书写析构函数之前需要我们去写出它的一个子函数,因为我们析构递归没有参数。
这里使用递归去析构函数而不是循环,是因为通常析构二叉树或者多叉树都是从数的叶子结点开始往回析构,都是后序遍历删除比较容易,循环析构比较麻烦。
这里我们就只需要递归一下左边再递归一下右边,然后将root节点删除,最后要记得参数加上引用符合后将删除的节点进行置空操作。
深拷贝:
然后接下来因为上面的析构为递归式的析构,因此在这里会发生浅拷贝的问题,这里就需要我们写一个深拷贝的代码来解决这个问题。
这里我们就需要拷贝一棵新树出来,先将这棵树传出去,然后判断是否为空树,如果为空树那么就返回空。
如果不为空树那就先创建一个头节点出来,然后就是前序遍历拷贝,递归拷贝先拷贝左在拷贝右。将这棵树拷贝出来最后返回这棵树的根节点即可。
赋值:
最后的一个常用操作就是赋值操作,这里就直接写代码即可。
这里就是简单的交换操作。
最后返回this指针即可。
搜索二叉树的应用:
在了解完了搜索二叉树后,我们就要来了解搜索二叉树会在哪些地方被应用到。
如上图,搜索二叉树通常都是用来解决K(key),V(value)问题。
代码:
#pragma once
template<class K>
struct BSTreeNode
{
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
BSTreeNode(const K& key)
:_left(nullptr),
_right(nullptr),
_key(key)
{
}
};
template<class K>
class BSTree
{
typedef BSTrssNode<K> Node;
public:
BSTree()
:_root(nullptr)
{
}
BSTree(const BSTree<K>& t)
{
_root = Copy(t._root);
}
BSTree<K>& operator=(BSTree<K> t)
{
swap(_root, t._root);
return *this;
}
~BSTree()
{
Destory(_root);
}
bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* cur = _root;
Node* parent = cur;
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 = new Node(key);
if (parent->_key < key)
{
parent->right = cur;
}
else
{
parent->left = cur;
}
return true;
}
bool Find(const K& key)
{
Node* cur = _root;
while (cur)
{
{
if (cur->_key < key)
{
cur = cur->_right;
}
else if (cur->_key > key)
{
cur = cur->_left;
}
else
{
return false;
}
}
}
}
bool Erase(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else
{
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = cur->_right;
}
else
{
if (parent->_right == cur)
{
parent->_right = cur->_right;
}
else
{
parent->_left = cur->_right;
}
}
}
else if(cur->_right == nullptr)
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
if (parent->_right == cur)
{
parent->_right = cur->_left;
}
else
{
parent->_left = cur->_left;
}
}
}
else
{
Node* parent = cur;
Node* leftMax = cur->_left;
while (leftMax->_right)
{
parent = leftMax;
leftMax = leftMax->_right;
}
swap(cur->_key, leftMax->_key);
if (parent->_left == leftMax)
{
parent->_left = leftMax->_left;
}
else
{
parent->_right = leftMax->_left;
}
cur = leftMax;
}
delete cur;
return true;
}
}
return 0;
}
void InOrder()
{
_InOrder(_root);
}
void _InOrder(Node* root)
{
if (root == NUll)
{
return;
}
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
bool FindR(const K& key)
{
return _FindR(_root, key);
}
bool InsertR(const K& key)
{
return _InsertR(_root, key);
}
bool EraseR(const K& key)
{
_EraseR(_root, key);
}
private:
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;
}
void Destory(Node*& root)
{
if (root == nullptr)
{
return;
}
Destory(root->_left);
Destory(root->_right);
delete root;
root = nullptr;
}
bool _EraseR(Node*& root, const K& key)
{
if(root == nullptr)
{
return false;
}
if (root->_key < key)
{
return _EraseR(root->_right, key);
}
else if (root->_key > key)
{
return _EraseR(root->_left, key);
}
else
{
Node* del = root;
if (root->_left == nullptr)
{
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_left;
}
else
{
Node* leftMax = root->_left;
while (leftMax->_right)
{
leftMax = leftMax->_right;
}
swap(root->_key, leftMax->_key);
return EraseR(root->_left, key);
}
delete del;
return true;
}
}
bool _InsertR(Node*& root, const K& key)
{
if (root == nullptr)
{
root = new Node(key);
return true;
}
if (root->_key < key)
{
return _FindR(root->_right, key);
}
else if (root->_key > key)
{
return _FindR(root->_left, key);
}
else
{
return true
}
}
bool _FindR(Node* root,const K& key)
{
if (root == nullptr)
{
return false;
}
if (root->_key < key)
{
return _FindR(root->_right, key);
}
else if (root->_key > key)
{
return _FindR(root->_left, key);
}
else
{
return true
}
}
private:
Node* _root;
};
void TestBSTree1()
{
}
结尾:
到这里我们的搜索二叉树的基本内容就讲解完了,但是这并不意味着它的结束,在上文我们有说过。搜索二叉树还涉及到我们C++中的AVL树和红黑树,同时K,V问题也是后面我们要学习好了解的问题,最后希望之篇博客能带来帮助。