目录
01.二叉搜索树的概念
有一种查找方法,具有极高的查找效率,时间复杂度仅为 O(logn)。在每一步查找过程中都会将搜索范围减半,这就是二分查找。
但是在实际应用中,我们并不经常使用二分查找。这是因为它只能在有序数组或有序数据结构上工作,如果数据是无序的,则需要先进行排序;而且它只适用于数组或连续存储的数据结构中。
在实际运用中,我们更常用的是二叉搜索树(Binary Search Tree, BST),其查找时间复杂度也是 O(logn),并且拥有高效的插入和删除操作。
二叉搜索树又称二叉排序树,它的定义如下:
1.若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
2.若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
3.它的左右子树也分别为二叉搜索树
检验一个二叉树是否是二叉搜索树,不仅要检查每个节点的左右子节点的值是否满足条件,还要确保整个左子树的所有节点的值都小于根节点的值,整个右子树的所有节点的值都大于根节点的值。
02.二叉搜索树的操作过程
1.二叉搜索树的查找
step1:从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
step2:如果最终走到节点为空还没找到,则这个值不存在。
2.二叉树搜索树的插入
二叉搜索树的插入都是在叶子节点进行插入,第一个插入的元素就是根节点,后面插入的元素根据性质判断插入位置
3.二叉搜索树的删除
二叉搜索树的删除除了会在叶子结点进行删除外,还可能在中间结点进行删除,在中间结点进行删除时,就需要考虑它的孩子结点的情况,因此要分情况讨论:
case1.删除结点无孩子结点
case2.删除结点只有左孩子结点
case3.删除结点只有右孩子结点
case4.删除结点有左、右孩子结点
其实case1和case2或3可以结合起来,实际删除过程如下:
case2:删除该结点后,该父结点指向该左孩子结点
case3:删除该结点后,该父结点指向该右孩子结点
case4:需要在其子树中寻找一个结点替代该结点(左子树的值最大结点/右子树的值最小结点),替换后仍满足搜索树要求
比如上述例子中,可以用36(左子树的值最大结点)或50(右子树的值最小结点)对41进行替换,然后将替换的结点位置进行删除。
03.二叉搜索树的代码实现
(1)基本框架
身为一个二叉树,其结点内部包含左孩子结点指针、右孩子结点指针、数据,在二叉搜索树中,我们需要实现它的查找、插入、删除以及中序遍历操作。
为什么特意要实现中序遍历呢,因为根据二叉搜索树的特性(任意一个结点,其左子树的所有结点都小于该结点,右子树所有结点都大于该结点)中序遍历会以递增的方式访问树中的所有结点(先访问左子树-->根节点-->右子树),此时就可以按顺序输出所有节点,实现了排序的效果。
template<class T>
struct BSTNode
{
BSTNode(const T& data = T())
:
_pLeft(nullptr), _pRight(nullptr), _data(data)
{}
BSTNode<T>* _pLeft;
BSTNode<T>* _pRight;
T _data;
};
template<class T>
class BSTree
{
typedef BSTNode<T> Node;
typedef Node* PNode;
public:
BSTree();//默认构造
~BSTree();//析构
PNode Find(const T& data);//查找元素
bool Insert(const T& data);//插入元素
bool Erase(const T& data);//删除元素
void InOrder();//中序遍历
private:
PNode _pRoot;
};
(2)树的创建与销毁
树的默认构造只需要在初始化列表中将成员变量根结点赋值为空即可。
析构过程与其他二叉树相同,需要递归地销毁每一个结点:对于每一个结点,首先递归销毁它的左子树,然后递归销毁销毁它的右子树,最后销毁该结点本身。这种顺序保证了我们在销毁父结点前已经销毁了它的所有子结点,防止内存泄漏。
BSTree() : _pRoot(nullptr){}
~BSTree()
{
destory(_pRoot);
_pRoot = nullptr;
}
void destory(const PNode& node)
{
if (node == nullptr)
return;
destory(node->_pLeft);
destory(node->_pRight);
delete node;
}
(3)元素的查找
首先将要查找的值与当前节点进行比较,根据比较结果选择路径:
- 比当前节点小,则移向当前节点的左子节点。
- 比当前节点大,则移向当前节点的右子节点。
- 等于当前节点,则查找成功,返回当前节点。
- 遍历到结点为空还没找到,则该值不存在,返回空。
PNode Find(const T& data)
{
Node* cur = _pRoot;
while (cur != nullptr)
{
if (data < cur->_data)
cur = cur->_pLeft;
else if (data > cur->_data)
cur = cur->_pRight;
else
return cur;
}
return nullptr;
}
(4)元素的插入
将待插入的值与当前节点进行比较:
- 比当前节点小,则移向当前节点的左子节点,。
- 比当前节点的值大,则移向当前节点的右子节点。
- 等于当前节点的值,则不能进行插入,返回false。
- 遍历到节点为空,则当前位置就是要插入位置
我们可以定义两个指针,一个记录当前遍历节点位置,一个记录父节点位置,这样在找到插入位置时就可以直接进行插入。
bool Insert(const T& data)
{
// 如果树为空,直接插入
if (nullptr == _pRoot)
{
_pRoot = new Node(data);
return true;
}
// 按照二叉搜索树的性质查找data在树中的插入位置
PNode pCur = _pRoot;
// 记录pCur的双亲,因为新元素最终插入在pCur双亲左右孩子的位置
PNode pParent = nullptr;
while (pCur)
{
pParent = pCur;
if (data < pCur->_data)
pCur = pCur->_pLeft;
else if (data > pCur->_data)
pCur = pCur->_pRight;
else
return false; // 元素已经在树中存在
}
// 插入元素
pCur = new Node(data);
if (data < pParent->_data)
pParent->_pLeft = pCur;
else
pParent->_pRight = pCur;
return true;
}
以上方法我们需要显式的管理父节点指针,过程略显繁琐,有没有什么办法可以直接修改当前节点指针呢,我们观察以下这段代码:
bool Insert(const T& data)
{
InsertR(_pRoot,data);
return true;
}
void InsertR(PNode& node, const T& data)
{
if (nullptr == node) {
node = new Node(data);
return;
}
if (node->_data == data)
return;
if (data < node->_data)
InsertR(node->_pLeft, data);
else
InsertR(node->_pRight, data);
return;
}
在这段代码中,我们依旧将data值与当前节点进行比较,比较之后,我们选择递归地将data插入到左或右子树当中,并且在递归过程中通过引用传参,可以直接修改节点指针,就不需要用到父节点指针了。
这种引用传参的递归方法,巧妙地化简了插入过程,并且在每个递归调用中自然地处理了边界情况,逻辑更为直观。
(5)元素的删除
根据上面的分析过程,分情况进行讨论
a:删除该结点后,该父结点指向该左孩子结点
b:删除该结点后,该父结点指向该右孩子结点
在a和b情况中,我们直接将父节点中指向当前节点的指针改为指向该节点的下一节点,然后对该节点进行销毁即可。
c:需要在其子树中寻找一个结点替代该结点(左子树的值最大结点/右子树的值最小结点),替换后仍满足搜索树要求
第三种情况处理起来就复杂许多,我们需要先找到替代节点(左子树最大或右子树最小,这里我们选择左子树最大这种情况),找到后还需要考虑替代节点是否含有左子树(不需要考虑右子树,因为最大值肯定没有右子树),如果有,我们需要将替代节点的左子树接到替代节点的父节点上,因此我们还需要定义一个父节点指针变量。
代码呈现如下:
bool Erase(const T& data)
{
// 如果树为空,删除失败
if (nullptr == _pRoot)
return false;
// 查找在data在树中的位置
PNode pCur = _pRoot;
PNode pParent = nullptr;
while (pCur)
{
if (data == pCur->_data)
break;
else if (data < pCur->_data)
{
pParent = pCur;
pCur = pCur->_pLeft;
}
else
{
pParent = pCur;
pCur = pCur->_pRight;
}
}
// data不在二叉搜索树中,无法删除
if (nullptr == pCur)
return false;
// 分以下情况进行删除
if (nullptr == pCur->_pRight)
{
// 当前节点只有左孩子或者左孩子为空---可直接删除
if (pParent->_pLeft == pCur)
pParent->_pLeft = pCur->_pLeft;
else
pParent->_pRight = pCur->_pLeft;
delete pCur;
}
else if (nullptr == pCur->_pLeft)
{
// 当前节点只有右孩子---可直接删除
if (pParent->_pLeft == pCur)
pParent->_pLeft = pCur->_pRight;
else
pParent->_pRight = pCur->_pRight;
delete pCur;
}
else
{
// 当前节点同时具有左右孩子
Node* leftmax = pCur->_pLeft;
Node* pre = pCur;
while (leftmax->_pRight) {
pre = leftmax;
leftmax = leftmax->_pRight;
}
pCur->_data = leftmax->_data;
if (leftmax->_pLeft)
pre->_pRight = leftmax->_pLeft;
else if (pre == pCur)
pCur->_pLeft = nullptr;
else
pre->_pRight = nullptr;
delete leftmax;
leftmax = nullptr;
}
return true;
}
这里我们还是可以考虑用引用传参递归的方法简化过程
但要注意的是,与插入操作不同,删除操作后,返回值是更新后的子树的根节点,这是因为在删除某些节点时,子树的根节点可能会发生变化,比如左子树或右子树的新根节点替代了被删除的节点。而插入操作则不需要,因为树的结构是自然地扩展的。
PNode EraseR(PNode& node, const T& data)
{
if (nullptr == node)
return node;
if (data == node->_data)
{
if (!node->_pRight)
return node->_pLeft;
else if (!node->_pLeft)
return node->_pRight;
else {
if (!node->_pRight) {
PNode temp = node->_pLeft;
delete node;
return temp;
}
else if (!node->_pLeft) {
PNode temp = node->_pRight;
delete node;
return temp;
}
else {
PNode minNode = FindMin(node->_pRight);
node->_data = minNode->_data;
node->_pRight = EraseR(node->_pRight, minNode->_data);
}
}
}
if (data < node->_data)
node->_pLeft = EraseR(node->_pLeft, data);
else if (data > node->_data)
node->_pRight = EraseR(node->_pRight, data);
return node;
}
PNode FindMin(PNode node) const {
while (node && node->_pLeft != nullptr)
node = node->_pLeft;
return node;
}
(6)中序遍历
中序遍历的顺序是:左子树 -> 根节点 -> 右子树,我们使用递归方法来实现:
1.递归左子树:首先递归访问左子树,确保左子树的所有节点值被访问和打印。
2.打印当前节点:然后打印当前节点的值。
3.递归右子树:最后递归访问右子树,确保右子树的所有节点值被访问和打印。
void InOrder()
{
print(_pRoot);
cout << endl;
}
void print(PNode node)
{
if (node == nullptr)
return;
print(node->_pLeft);
cout << node->_data << " ";
print(node->_pRight);
}
最后附上完整代码:
#include<iostream>
using namespace std;
template<class T>
struct BSTNode
{
BSTNode(const T& data = T())
:
_pLeft(nullptr), _pRight(nullptr), _data(data)
{}
BSTNode<T>* _pLeft;
BSTNode<T>* _pRight;
T _data;
};
template<class T>
class BSTree
{
typedef BSTNode<T> Node;
typedef Node* PNode;
public:
BSTree() : _pRoot(nullptr)
{}
~BSTree(){
destory(_pRoot);
_pRoot = nullptr;
}
PNode Find(const T& data)
{
Node* cur = _pRoot;
while (cur != nullptr)
{
if (data < cur->_data)
cur = cur->_pLeft;
else if (data > cur->_data)
cur = cur->_pRight;
else
return cur;
}
return nullptr;
}
bool Insert(const T& data)
{
// 如果树为空,直接插入
if (nullptr == _pRoot)
{
_pRoot = new Node(data);
return true;
}
// 按照二叉搜索树的性质查找data在树中的插入位置
PNode pCur = _pRoot;
// 记录pCur的双亲,因为新元素最终插入在pCur双亲左右孩子的位置
PNode pParent = nullptr;
while (pCur)
{
pParent = pCur;
if (data < pCur->_data)
pCur = pCur->_pLeft;
else if (data > pCur->_data)
pCur = pCur->_pRight;
else
return false; // 元素已经在树中存在
}
// 插入元素
pCur = new Node(data);
if (data < pParent->_data)
pParent->_pLeft = pCur;
else
pParent->_pRight = pCur;
return true;
/*InsertR(_pRoot,data);
return true;*/
}
bool Erase(const T& data)
{
// 如果树为空,删除失败
if (nullptr == _pRoot)
return false;
// 查找在data在树中的位置
PNode pCur = _pRoot;
PNode pParent = nullptr;
while (pCur)
{
if (data == pCur->_data)
break;
else if (data < pCur->_data)
{
pParent = pCur;
pCur = pCur->_pLeft;
}
else
{
pParent = pCur;
pCur = pCur->_pRight;
}
}
// data不在二叉搜索树中,无法删除
if (nullptr == pCur)
return false;
// 分以下情况进行删除
if (nullptr == pCur->_pRight)
{
// 当前节点只有左孩子或者左孩子为空---可直接删除
if (pParent->_pLeft == pCur)
pParent->_pLeft = pCur->_pLeft;
else
pParent->_pRight = pCur->_pLeft;
delete pCur;
}
else if (nullptr == pCur->_pLeft)
{
// 当前节点只有右孩子---可直接删除
if (pParent->_pLeft == pCur)
pParent->_pLeft = pCur->_pRight;
else
pParent->_pRight = pCur->_pRight;
delete pCur;
}
else
{
Node* leftmax = pCur->_pLeft;
Node* pre = pCur;
while (leftmax->_pRight) {
pre = leftmax;
leftmax = leftmax->_pRight;
}
pCur->_data = leftmax->_data;
if (leftmax->_pLeft)
pre->_pRight = leftmax->_pLeft;
else if (pre == pCur)
pCur->_pLeft = nullptr;
else
pre->_pRight = nullptr;
delete leftmax;
leftmax = nullptr;
}
return true;
//EraseR(_pRoot, data);
//return true;
}
void InOrder()
{
print(_pRoot);
cout << endl;
}
private:
PNode _pRoot;
void InsertR(PNode& node, const T& data)
{
if (nullptr == node) {
node = new Node(data);
return;
}
if (node->_data == data)
return;
if (data < node->_data)
InsertR(node->_pLeft, data);
else
InsertR(node->_pRight, data);
return;
}
PNode EraseR(PNode& node, const T& data)
{
if (nullptr == node)
return node;
if (data == node->_data)
{
if (!node->_pRight)
return node->_pLeft;
else if (!node->_pLeft)
return node->_pRight;
else {
if (!node->_pRight) {
PNode temp = node->_pLeft;
delete node;
return temp;
}
else if (!node->_pLeft) {
PNode temp = node->_pRight;
delete node;
return temp;
}
else {
PNode minNode = FindMin(node->_pRight);
node->_data = minNode->_data;
node->_pRight = EraseR(node->_pRight, minNode->_data);
}
}
}
if (data < node->_data)
node->_pLeft = EraseR(node->_pLeft, data);
else if (data > node->_data)
node->_pRight = EraseR(node->_pRight, data);
return node;
}
PNode FindMin(PNode node) const {
while (node && node->_pLeft != nullptr)
node = node->_pLeft;
return node;
}
void destory(const PNode& node)
{
if (node == nullptr)
return;
destory(node->_pLeft);
destory(node->_pRight);
delete node;
}
void print(PNode node)
{
if (node == nullptr)
return;
print(node->_pLeft);
cout << node->_data << " ";
print(node->_pRight);
}
};
以上就是二叉搜索树的图解与完整实现过程,欢迎在评论区留言,觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~😉