樊梓慕:个人主页
个人专栏:《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C++》《Linux》《算法》
每一个不曾起舞的日子,都是对生命的辜负
本篇文章博主会对二叉搜索树的一些特性进行讲解,并且进行模拟实现。
欢迎大家收藏以便未来做题时可以快速找到思路,巧妙的方法可以事半功倍。
=========================================================================
GITEE相关代码:樊飞 (fanfei_c) - Gitee.com
=========================================================================
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
一般像这种链式结构,我们都要实现一个节点类出来用来构建联系。
template
struct BSTreeNode
{
BSTreeNode* _left;
BSTreeNode* _right;
K _key;
BSTreeNode(const K& key = 0)
:_left(nullptr)
, _right(nullptr)
, _key(key)
{}
};
因为这里的构造不需要特定的要求,只需要构造一棵空树,即给root赋值为nullptr即可。
//构造函数
BSTree()
:_root(nullptr)
{}
或者你可以让系统自己生成一个默认的构造。
BSTree() = default;
注意这里完成的是深拷贝,不能是值拷贝,防止二次析构发生。
//拷贝构造函数
BSTree(const BSTree& t)
{
_root = Copy(t._root);
}
//设置private限定,不要暴露该接口
Node* Copy(Node* root)
{
if (root == nullptr)
return nullptr;
Node* newRoot = new Node(root->_key);
newRoot->_left = Copy(root->_left);
newRoot->_right = Copy(root->_right);
return newRoot;
}
在之前数据结构的模拟实现中,一般对于赋值运算符这块我们已经学习了现代写法,直接swap即可。
//赋值运算符重载函数
BSTree& operator=(BSTree t)
{
swap(_root, t._root);
return *this;
}
原理:=的右值由于参数传递的不是引用,所以会调用自身的拷贝构造形成一个临时对象,交换临时对象与左值根节点后,此时左值根节点已经是之前的右值根节点了,然后返回左值根节点完成赋值,结束后右值根节点会被析构(即之前的左值)。
当然还有以下这种传统写法。
//传统写法
const BSTree& operator=(const BSTree& t)
{
if (this != &t) //防止自己给自己赋值
{
_Destory(_root); //先将当前的二叉搜索树中的结点释放
_root = _Copy(t._root); //拷贝t对象的二叉搜索树
}
return *this; //支持连续赋值
}
二叉树的析构一定采用『 后序』的方式。
//析构函数
~BSTree()
{
Destroy(_root);
}
void Destroy(Node* root)
{
if (root == nullptr)
return;
Destroy(root->_left);
Destroy(root->_right);
delete root;
}
根据二叉搜索树的特性(左子树都小于根,右子树都大于根),我们在二叉搜索树当中查找指定值的结点的方式如下:
非递归方式:
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 true;
}
}
return false;
}
递归方式:
bool FindR(const K& key)
{
return _FindR(_root, key);
}
//设置为private,不要暴露该接口
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;
}
}
思考:为什么要设计成子函数这种形式呢?
因为当外部调用FindR时,我们没法直接传递根节点『 根节点是private域』,所以我们需要通过FindR获取到*this从而获取到_root,然后再调用_FindR,_FindR设计的参数为root和key就可以实现逻辑了,包括后面的递归方式都是这个思路。
插入的具体过程如下:
非递归方式:
使用非递归方式实现二叉搜索树的插入函数时,找到插入位置后我们需要new新节点,然后将该节点与对应的父节点进行连接,所以我们需要定义一个parent指针,该指针用于标记待插入结点的父结点。
注意:连接parent和cur时,需要判断应该将cur连接到parent的左边还是右边。
//插入函数
bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
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
{
return false;
}
}
cur = new Node(key);
if (parent->_key < key) //key值大于当前parent结点的值
{
parent->_right = cur; //将结点连接到parent的右边
}
else //key值小于当前parent结点的值
{
parent->_left = cur; //将结点连接到parent的左边
}
return true;
}
递归方式:
同样的,对于递归方式来讲,我们如何将新节点与父节点进行连接呢?
这里只需要给参数上加一个『 引用』即可。
bool _InsertR(Node*& root, const K& key)
{
if (root == nullptr)
{
root = new Node(key);
return true;
}
if (root->_key < key)
{
return _InsertR(root->_right, key);
}
else if (root->_key > key)
{
return _InsertR(root->_left, key);
}
else
{
return false;
}
}
二叉搜索树的删除函数情况比较复杂,场景比较多。
若是在二叉树当中没有找到待删除结点,则直接返回false表示删除失败即可,但若是找到了待删除结点,此时就有以下三种情况:
分情况进行讨论:
(1)待删除结点的左子树为空(待删除结点的左右子树均为空包含在内)。
若待删除结点的左子树为空,那么找到待删除结点后,只需先让其父结点指向该结点的右孩子结点,然后再将该结点释放。
(2)待删除结点的右子树为空。
若待删除结点的右子树为空,那么找到待删除结点后,只需先让其父结点指向该结点的左孩子结点,然后再将该结点释放。
(3)待删除结点的左右子树均不为空。
比如以下场景,要删除节点『 7』。
利用替换法:
step1:
step2:
然后将该替换后的节点删除,该替换后的节点必然左右子树当中至少有一个为空树,因此删除该结点的方法与前面说到的情况一和情况二的方法相同。
注意:只能是待删除结点左子树当中值最大的结点,或是待删除结点右子树当中值最小的结点代替待删除结点被删除,因为只有这样才能使得进行删除操作后的二叉树仍保持二叉搜索树的特性。
非递归方式:
左子树的最大一定是左子树中最右面的节点;
右子树的最小一定是右子树中最左面的节点。
当找到待删除结点右子树当中值最小的结点时,先将待删除结点的值改为minRight的值,之后直接判断此时minRight是minParent的左孩子还是右孩子,然后对应让minParent的左指针或是右指针转而指向minRight的右孩子(注意:minRight的左孩子为空),最后将minRight结点进行释放即可。
bool Erase(const K& key)
{
Node* parent = nullptr; //记录待删除结点的父结点
Node* cur = _root; //记录待删除结点
while (cur)
{
if (key < cur->_key) //key值小于当前结点的值
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key) //key值大于当前结点的值
{
parent = cur;
cur = cur->_right;
}
else //找到了待删除结点
{
if (cur->_left == nullptr) //待删除结点的左子树为空
{
if (cur == _root) //待删除结点是根结点,此时parent为nullptr
{
_root = cur->_right; //二叉搜索树的根结点改为根结点的右孩子即可
}
else //待删除结点不是根结点,此时parent不为nullptr
{
if (cur == parent->_left) //待删除结点是其父结点的左孩子
{
parent->_left = cur->_right; //父结点的左指针指向待删除结点的右子树即可
}
else //待删除结点是其父结点的右孩子
{
parent->_right = cur->_right; //父结点的右指针指向待删除结点的右子树即可
}
}
delete cur; //释放待删除结点
return true; //删除成功,返回true
}
else if (cur->_right == nullptr) //待删除结点的右子树为空
{
if (cur == _root) //待删除结点是根结点,此时parent为nullptr
{
_root = cur->_left; //二叉搜索树的根结点改为根结点的左孩子即可
}
else //待删除结点不是根结点,此时parent不为nullptr
{
if (cur == parent->_left) //待删除结点是其父结点的左孩子
{
parent->_left = cur->_left; //父结点的左指针指向待删除结点的左子树即可
}
else //待删除结点是其父结点的右孩子
{
parent->_right = cur->_left; //父结点的右指针指向待删除结点的左子树即可
}
}
delete cur; //释放待删除结点
return true; //删除成功,返回true
}
else //待删除结点的左右子树均不为空
{
//替换法删除
Node* minParent = cur; //标记待删除结点右子树当中值最小结点的父结点
Node* minRight = cur->_right; //标记待删除结点右子树当中值最小的结点
//寻找待删除结点右子树当中值最小的结点
while (minRight->_left)
{
//右子树中最小一定在最左面
minParent = minRight;
minRight = minRight->_left;
}
cur->_key = minRight->_key; //将待删除结点的值改为minRight的值
//注意一个隐含条件:此时minRight的_left为空
if (minRight == minParent->_left) //minRight是其父结点的左孩子
{
minParent->_left = minRight->_right; //父结点的左指针指向minRight的右子树即可
}
else //minRight是其父结点的右孩子
{
minParent->_right = minRight->_right; //父结点的右指针指向minRight的右子树即可
}
delete minRight; //释放minRight
return true; //删除成功,返回true
}
}
}
return false; //没有找到待删除结点,删除失败,返回false
}
递归方式:
在找到了待删除节点后的思路与操作一样,未找到之前改换为递归方式即可。
bool _EraseR(Node*& root, const K& key)
{
if (root == nullptr)
return false;
if (key < root->_key) //key值小于根结点的值
return _EraseR(root->_left, key); //待删除结点在根的左子树当中
else if (key > root->_key) //key值大于根结点的值
return _EraseR(root->_right, key); //待删除结点在根的右子树当中
else //找到了待删除结点
{
if (root->_left == nullptr) //待删除结点的左子树为空
{
Node* del = root; //保存根结点
root = root->_right; //根的右子树作为二叉树新的根结点
delete del; //释放根结点
}
else if (root->_right == nullptr) //待删除结点的右子树为空
{
Node* del = root; //保存根结点
root = root->_left; //根的左子树作为二叉树新的根结点
delete del; //释放根结点
}
else //待删除结点的左右子树均不为空
{
Node* minParent = root; //标记根结点右子树当中值最小结点的父结点
Node* minRight = root->_right; //标记根结点右子树当中值最小的结点
//寻找根结点右子树当中值最小的结点
while (minRight->_left)
{
//右子树中最小一定在最左面
minParent = minRight;
minRight = minRight->_left;
}
root->_key = minRight->_key; //将根结点的值改为minRight的值
//注意一个隐含条件:此时minRight的_left为空
if (minRight == minParent->_left) //minRight是其父结点的左孩子
{
minParent->_left = minRight->_right; //父结点的左指针指向minRight的右子树即可
}
else //minRight是其父结点的右孩子
{
minParent->_right = minRight->_right; //父结点的右指针指向minRight的右子树即可
}
delete minRight; //释放minRight
}
return true; //删除成功,返回true
}
}
bool EraseR(const K& key)
{
return _EraseR(_root, key); //删除_root当中值为key的结点
}
K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:
给一个单词word,判断该单词是否拼写正确,具体方式如下:
每一个关键码key,都有与之对应的值Value,即
namespace key_value
{
template
struct BSTreeNode
{
typedef BSTreeNode Node;
Node* _left;
Node* _right;
K _key;
V _value;
BSTreeNode(const K& key, const V& value)
:_left(nullptr)
,_right(nullptr)
,_key(key)
,_value(value)
{}
};
template
class BSTree
{
typedef BSTreeNode Node;
public:
bool Insert(const K& key, const V& value)
{
if (_root == nullptr)
{
_root = new Node(key, value);
return true;
}
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
{
return false;
}
}
cur = new Node(key, value);
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
Node* 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 cur;
}
}
return nullptr;
}
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 (cur == parent->_right)
{
parent->_right = cur->_right;
}
else
{
parent->_left = cur->_right;
}
}
delete cur;
return true;
}
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
{
// 替换法
Node* rightMinParent = cur;
Node* rightMin = cur->_right;
while (rightMin->_left)
{
rightMinParent = rightMin;
rightMin = rightMin->_left;
}
cur->_key = rightMin->_key;
if (rightMin == rightMinParent->_left)
rightMinParent->_left = rightMin->_right;
else
rightMinParent->_right = rightMin->_right;
delete rightMin;
return true;
}
}
}
return 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);
}
private:
Node* _root = nullptr;
};
}
『 查找效率』代表了二叉搜索树中各个操作的性能,因为插入和删除操作都必须先查找。
对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
时间复杂度描述的是最坏情况下算法的效率,因此普通二叉搜索树各个操作的时间复杂度都是O(N)。
如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?(即log(N))
我们后面会学习AVL树与红黑树,他们对二叉搜索树进行了一定的优化,使得二叉搜索树的性能都能达到最优。
=========================================================================
如果你对该系列文章有兴趣的话,欢迎持续关注博主动态,博主会持续输出优质内容
博主很需要大家的支持,你的支持是我创作的不竭动力
~ 点赞收藏+关注 ~
=========================================================================