前言
学习二叉搜索树是为了给后面学习 AVL树 和 红黑树 打基础的,因此还是比较重要的。
一、二叉搜索树的概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有结点的值都小于等于根结点的值
- 若它的右子树不为空,则右子树上所有结点的值都大于等于根结点的值
- 它的左右子树也分别为二叉搜索树
- 二叉搜索树中可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义,后续我们学习map/set/multimap/multiset系列容器底层就是二叉搜索树,其中map/set不支持插入相等值,multimap/multiset支持插入相等值
因为这些性质,使得其在查找数据时效率较高
二叉搜索树示意图:
左:不支持相等值 右:支持相等值
简单来说:二叉搜索树的每一个节点,它的左子树节点全是小于等于它的,它的右子树节点全是大于等于它的。关于等于,取决于该二叉搜索树是否支持相等值。
关于排序:
前面所说,二叉搜索树又称二叉排序树,这是因为二叉搜索树输出时一般按照中序遍历输出
- 如上图左边树按照中序遍历:1->3->4->6->7->8->10->13->14
- 右边树按照中序遍历:1->3->3->6->7->8->10->10->13
很明显,二叉搜索树中的数据按照中序输出就是有序的。
二、二叉搜索树的效率
最优情况下:二叉搜索树为完全二叉树(或者接近完全二叉树),其高度为:
。效率最好。
如:此时可以通过与根节点大小比较快速锁定目标节点
最差情况下:二叉搜索树退化为单支树(或者类似单支),其高度为: N
如:此时效率最低,假如查找1,基本遍历了所有数据
总结:
- 所以综合而言二叉搜索树增删查改时间复杂度为: O(N)
- 根据上图很容易发现二叉搜索树的缺陷,这样的效率显然是无法满足我们需求的,我们后续课程需要继续讲解二叉搜索树的变形,平衡二叉搜索树AVL树和红黑树,才能适用于我们在内存中存储和搜索数据。
补充说明:
需要说明的是,二分查找也可以实现
级别的查找效率,但是二分查找有两大缺陷:
- 需要存储在支持下标随机访问的结构中,并且有序。
- 插入和删除数据效率很低,因为存储在下标随机访问的结构中,插入和删除数据一般需要挪动数据。
这里也就体现出了平衡二叉搜索树的价值。
三、二叉搜索树的模拟实现
注:我们先主要实现不支持相等数据的二叉搜索树
1.基本框架
//定义节点
template<class K>
struct BSNode
{
K _key;
BSNode<K>* _left;
BSNode<K>* _right;
BSNode(const K& key)//构造
:_key(key)
, _left(nullptr)
, _right(nullptr)
{
}
};
//二叉搜索树
template<class K>
class BSTree
{
typedef BSNode<K> Node;//重命名方便书写
public:
private:
Node* _root = nullptr;
};
- 关于节点,_key用于存储数据, _left 和 _right 分别是左子树和右子树的节点指针。
- 搜索二叉树的成员变量只要一个,就是根节点。
- 注意上面模版参数都是 K,因为该模板参数主要控制 _key 的类型。
2.二叉搜索树的插入
插入的具体过程如下:
- 树为空,则直接新增结点,赋值给_root指针
- 树不为空,按二叉搜索树性质,插入值比当前结点大往右走,插入值比当前结点小往左走,找到空位置,插入新结点。
- 如果支持插入相等的值,插入值跟当前结点相等的值可以往右走,也可以往左走,找到空位置,插入新结点。(要注意的是要保持逻辑一致性,插入相等的值不要⼀会往右走,一会往左走)
比如:在下面二叉搜索树中插入 9
9比8大,所以往8的右子树走,9比10小,所以往10的左子树走,10的左子树为空,所以9应该插入这里。
代码实现:
bool Insert(const K& key)
{
//树为空,直接作为根节点
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
//树不为空,需要查找插入位置
//查找需要两个指针,cur用于找到目标位置,perent用于标记cur的父节点
//这样就可以链接新节点
Node* perent = nullptr;
Node* cur = _root;
while (cur)
{
if (key > cur->_key)//比当前值大,往右子树找
{
perent = cur;//先更新父节点
cur = cur->_right;//走到右孩子节点
}
else if (key < cur->_key)//比当前值小,往左子树找
{
perent = cur;//先更新父节点
cur = cur->_left;//走到左孩子节点
}
else
{
return false;//如果是相等值,则插入失败,因为我们是实现不支持重复值的
}
}
//走到这里,说明cur找到了
cur = new Node(key);//先创建结点
//使用父节点进行链接,因为不确定是父节点的左孩子还是右孩子
//所以需要单独判断,比较插入值与父节点值的大小即可,大插右,小插左
if (key > perent->_key)
{
perent->_right = cur;
}
else
{
perent->_left = cur;
}
return true;//插入成功返回true
}
- 代码实现中给出了详细注释,注意查看。
- 插入操作相对较简单
3.二叉搜索树的查找
不知道你有没有发现,其实在插入操作中,我们同样进行了查找操作,我们需要根据参数找到其在二叉树中的位置。作业关于二叉树的查找操作,可以直接复制插入操作。
代码实现:
//查找
bool Find(const K& key)
{
Node* cur = _root;//cur还是用于遍历查找
while (cur)//查找策略与插入查找一致,不再赘述
{
if (key > cur->_key)
{
cur = cur->_right;
}
else if (key < cur->_key)
{
cur = cur->_left;
}
else
{
return true;
}
}
return false;
}
- 查找简单点说就是,大往右,小往左
- 因为查找就是寻找相等值,以上是不支持数据重复。如果支持重复数据,那么返回的就是重复数据在中序遍历中的第一个。
4.二叉搜索树的删除
注:删除应该是二叉搜索树中最复杂的一部分了
删除操作主要复杂在,当我们删除一个节点后,我们需要继续保持搜索二叉树的性质,那么删除一个节点后就需要对树进行调整。
示例1:
- 如上图,如果我们删除1,那么结构是不受影响的
- 当我们删除10时,我们就需要进行调整,此时我们可以将10的右子树替代10原来的位置
删除后:
根据以上两种情况,可以总结两条情况:
- 如果删除节点的左右孩子都为空,那么该节点可以直接删除
- 如果删除节点的左孩子为空,那么可以使用该节点的右孩子代替原位置
示例2:
- 删除14,我们可以用14的左子树代替原来位置
- 所以,当删除节点的右子树为空时,我们可以将其左孩子代替原节点
示例3:
- 当删除节点的左右子树都不为空时,这时候就比较棘手了
- 解决方法就是,找出删除节点左子树中最大值,或者找出删除节点右子树中最小的值,只有满足这两种情况的值,才能替换删除节点,保持搜索二叉树的性质
如:找出 3 右子树中最小的值进行替换
总结:
- 将以上情况的解决方法总结一下,情况1和2,3其实可以合并在一起
- 左子树为空:把N结点的父亲对应孩子指针指向N的右孩子,直接删除N结点
- 右子树为空:把N结点的父亲对应孩子指针指向N的左孩子,直接删除N结点
- 左右子树都不为空:无法直接删除N结点,因为N的两个孩子无处安放,只能用替换法删除。找N左子树的值最大结点 R(最右结点)或者N右子树的值最小结点R(最左结点)替代N,因为这两个结点中任意⼀个,放到N的位置,都满足二叉搜索树的规则。替代N的意思就是N和R的两个结点的值交换,转而变成删除R结点,R结点符合情况2或情况3,可以直接删除。
左右子树都为空的情况没有单独讨论,因为它可以在左子树为空或者右子树为空的情况中被解决。左右孩子都为空,那么也可以说用其左右任意空指针代替原位置然后删除。
代码实现:
//删除
bool Erase(const K& key)
{
Node* cur = _root;//cur遍历查找目标位置
Node* perent = nullptr;//跟踪记录cur的父节点
while (cur)
{
if (key > cur->_key)
{
perent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
perent = cur;
cur = cur->_left;
}
else
{
//找到了
//1.左子树为空
if (cur->_left == nullptr)
{
if (cur == _root)//如果删除节点为根节点
{
_root = cur->_right;//用不为空的右子树替代
}
else//不是根节点
{
//需要单独判断,确认cur是其父节点左右哪个孩子节点
if (perent->_left == cur)
{
perent->_left = cur->_right;//更新父节点的左孩子,变相删除了cur
}
else if (perent->_right == cur)
{
perent->_right = cur->_right;
}
}
delete cur;//替换后,释放节点
return true;
}
else if (cur->_right == nullptr)//2.右子树为空
{
if (cur == _root)//根节点依旧要单独判断
{
_root = cur->_left;
}
else
{
if (perent->_left == cur)
{
perent->_left = cur->_left;
}
else if (perent->_right == cur)
{
perent->_right = cur->_left;
}
}
delete cur;
return true;
}
else//3.左右子树都不为空
{
//找右子树最小节点
Node* minRight = cur->_right;//minRight查找最小节点
Node* minRightPerent = cur;//跟踪记录minRight的父节点
while (minRight->_left)//一直遍历左孩子就能找到最小节点
{
minRightPerent = minRight;
minRight = minRight->_left;
}
cur->_key = minRight->_key;//找到节点后先替换目标值
//依旧需要确认是父节点的哪个孩子
if (minRightPerent->_left == minRight)
{
minRightPerent->_left = minRight->_right;//替换,变相删除
}
else
{
minRightPerent->_right = minRight->_right;
}
delete minRight;//释放节点
return true;
}
}
}
return false;//没有找到,返回false
}
注释解释了大部分代码,但还有一些问题在这里解释:
- 1.为什么在左子树为空的情况下,或者右子树为空的情况下,如果查找到的是根节点需要单独判断,这是因为此时父节点perent=nullptr,因为cur是根节点所以导致perent无法更新,因此需要单独判断这种情况。
- 2.为什么左右子树都不为空的情况下,又不需要单独判读根节点情况,因为我们在初始化时,让 minRightPerent=cur,所以不存在父节点为空的情况。
- 3.那为什么 perent 不初始化为 _root,你可以自己代入查看一下,行不通。
- 4.perent 和 minRightPerent 都是为了记录要删除节点的父节点,这样方便删除和链接节点,但是我们并不能确认究竟是父节点的哪一个节点是要删除的节点,即便有一个节点为空,我们也不能提前得知,所以在涉及更新父节点的孩子节点时,都需要判断一下。
5.二叉搜索树的输出打印
- 因为二叉搜索树又称二叉排序树嘛,所以输出是按照中序输出。
- 关于中序遍历有任何问题可以查看我以前关于二叉树的文章。
问题:
- 中序遍历是需要根节点的,而根节点 _root 属于私有成员,外界不能访问。
解决方案:
- 为了确保类的封装性,我们不能将 _root 公开,这里有两种方法解决:
- 增加一个 GetRoot 函数获取根节点。
- 将中序打印函数写在私有限定符里,并在公有限定符中再封装一层,利用类成员函数可以直接访问私有成员变量,这样外面调用中序打印时,就可以不用传参直接使用了。
很明显,第二种方案更有优势。
代码实现:
//二叉搜索树
template<class K>
class BSTree
{
typedef BSNode<K> Node;
public:
//中序遍历
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
//中序打印
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
Node* _root = nullptr;
};
- 经过一层封装解决,就是这么巧妙。
四、完整代码和使用演示
SerchBinaryTree.h
#pragma once
//定义节点
template<class K>
struct BSNode
{
K _key;
BSNode<K>* _left;
BSNode<K>* _right;
BSNode(const K& key)//构造
:_key(key)
, _left(nullptr)
, _right(nullptr)
{
}
};
//二叉搜索树
template<class K>
class BSTree
{
typedef BSNode<K> Node;//重命名方便书写
public:
bool Insert(const K& key)
{
//树为空,直接作为根节点
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
//树不为空,需要查找插入位置
//查找需要两个指针,cur用于找到目标位置,perent用于标记cur的父节点
//这样就可以链接新节点
Node* perent = nullptr;
Node* cur = _root;
while (cur)
{
if (key > cur->_key)//比当前值大,往右子树找
{
perent = cur;//先更新父节点
cur = cur->_right;//走到右孩子节点
}
else if (key < cur->_key)//比当前值小,往左子树找
{
perent = cur;//先更新父节点
cur = cur->_left;//走到左孩子节点
}
else
{
return false;//如果是相等值,则插入失败,因为我们是实现不支持重复值的
}
}
//走到这里,说明cur找到了
cur = new Node(key);//先创建结点
//使用父节点进行链接,因为不确定是父节点的左孩子还是右孩子
//所以需要单独判断,比较插入值与父节点值的大小即可,大插右,小插左
if (key > perent->_key)
{
perent->_right = cur;
}
else
{
perent->_left = cur;
}
return true;//插入成功返回true
}
//查找
bool Find(const K& key)
{
Node* cur = _root;//cur还是用于遍历查找
while (cur)//查找策略与插入查找一致,不再赘述
{
if (key > cur->_key)
{
cur = cur->_right;
}
else if (key < cur->_key)
{
cur = cur->_left;
}
else
{
return true;
}
}
return false;
}
//删除
bool Erase(const K& key)
{
Node* cur = _root;//cur遍历查找目标位置
Node* perent = nullptr;//跟踪记录cur的父节点
while (cur)
{
if (key > cur->_key)
{
perent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
perent = cur;
cur = cur->_left;
}
else
{
//找到了
//1.左子树为空
if (cur->_left == nullptr)
{
if (cur == _root)//如果删除节点为根节点
{
_root = cur->_right;//用不为空的右子树替代
}
else//不是根节点
{
//需要单独判断,确认cur是其父节点左右哪个孩子节点
if (perent->_left == cur)
{
perent->_left = cur->_right;//更新父节点的左孩子,变相删除了cur
}
else if (perent->_right == cur)
{
perent->_right = cur->_right;
}
}
delete cur;//替换后,释放节点
return true;
}
else if (cur->_right == nullptr)//2.右子树为空
{
if (cur == _root)//根节点依旧要单独判断
{
_root = cur->_left;
}
else
{
if (perent->_left == cur)
{
perent->_left = cur->_left;
}
else if (perent->_right == cur)
{
perent->_right = cur->_left;
}
}
delete cur;
return true;
}
else//3.左右子树都不为空
{
//找右子树最小节点
Node* minRight = cur->_right;//minRight查找最小节点
Node* minRightPerent = cur;//跟踪记录minRight的父节点
while (minRight->_left)//一直遍历左孩子就能找到最小节点
{
minRightPerent = minRight;
minRight = minRight->_left;
}
cur->_key = minRight->_key;//找到节点后先替换目标值
//依旧需要确认是父节点的哪个孩子
if (minRightPerent->_left == minRight)
{
minRightPerent->_left = minRight->_right;//替换,变相删除
}
else
{
minRightPerent->_right = minRight->_right;
}
delete minRight;//释放节点
return true;
}
}
}
return false;//没有找到,返回false
}
//中序遍历
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
//中序打印
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
Node* _root = nullptr;
};
使用演示:
#include <iostream>
using namespace std;
#include "SerchBinaryTree.h"
int main()
{
BSTree<int> b;
b.Insert(6);
b.Insert(9);
b.Insert(1);
b.Insert(3);
b.InOrder();
b.Erase(1);
b.InOrder();
return 0;
}
运行结果:
总结
以上就是本文的全部内容了,感谢你的支持!