文章目录
- 概念
- 实现
- 架构
- BSTreeNodea(节点)
- BSTree
- 框架
- 增删查 -- 循环写法
- insert(尾插)
- inOrder(遍历)
- Find(查找)
- Erase(删除)
- 默认成员函数
- 构造
- 拷贝构造
- 析构函数
- 赋值运算符重载
- 增删查 -- 递归写法
- _InsertR(递归尾插)
- _FindR(查)
- _EraseR(删除)
概念
二叉搜索树(Binary Search Tree,BST),也称为二叉查找树或二叉排序树,是一种特殊的二叉树,其中节点的值按照一定规律排列。具体来说 ,对于任意一个节点,其左子树的节点键值均小于根节点的键值;右子树的节点键值均大于根节点的键值。
即满足以下性质:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
实现
架构
这里将二叉搜索树要实现的大致内容贴出;
#pragma once
namespace aiyimu
{
template<class K>
// 二叉树节点
struct BSTreeNode
{};
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
// 尾插
bool insert(const K& key)
{}
// 中序遍历
void inOrder()
{}
bool Find(const K& key)
{}
// 删除key
bool Erase(const K& key)
{}
// ---------------------------------------- 递归写法 ---------------------------------------
// 查找
bool FindR(const K& key)
{}
bool InsertR(const K& key)
{}
bool EraseR(const K& key)
{}
// 默认成员函数
// 构造
BSTree() = default;//C++: 强制编译器生成默认构造
// 拷贝构造
BSTree(const BSTree<K>& bst)
{}
// 析构
~BSTree()
{}
// 赋值重载
BSTree<K>& operator=(BSTree<K> bst)
{}
// 不能访问_root
private:
// 拷贝
Node* _Copy(Node* root)
{}
// 销毁BST
void _Destory(Node*& root)
{}
bool _FindR(Node* root, const K& key)
{}
bool _InsertR(Node* root, const K& key)
{}
bool _EraseR(Node* root, const K& key)
{}
void _inOrder(Node* root)
{}
private:
Node* _root = nullptr;
};
}
BSTreeNodea(节点)
template<class K>
// 二叉树节点
struct BSTreeNode
{
// 成员变量
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
// 构造函数
BSTreeNode(const K& key)
:_left(nullptr)
,_right(nullptr)
,_key(key)
{}
};
- _left 表示指向当前节点的左子节点的指针;
- _right 表示指向当前节点的右子节点的指针;
- _key 表示当前节点所存储的键值(key),即二叉搜索树中的排序关键字。
_left 和 _right 成员变量的类型均为 BSTreeNode*,即指向某个类型为 K 的节点的指针。这是因为二叉树节点本身也是一种自定义数据类型,其成员变量也可以是指向其他节点的指针。
构造函数
- 首先将 _left 和 _right 成员变量初始化为 nullptr,表示当前节点暂时没有左子节点和右子节点。
- 然后将 _key 成员变量初始化为传入的参数 key,表示当前节点存储的键值就是 key。
BSTree
框架
增删查 – 循环写法
insert(尾插)
- 插入时如果此时无节点,直接创建新节点作为_root,返回true
- 首先查找key值需要插入的位置,如果key值大于节点值,向右继续查找;如果key值小于节点值,向左继续查找。
- 如果插入的key值已存在,则直接返回false(二叉搜索树没有重复的元素,这一步去重),最后的cur即为待插入的位置。
- 当找到要插入的cur时,最后将key作为cur的子节点插入
- 具体代码都有相应的注释做解释
// 尾插
bool insert(const K& key)
{
// 如果此时无节点,直接创建新节点作为_root,返回true
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
// 查找要插入的位置
while (cur)
{
// 如果key大,则向右插入
// 反之key小向左插入
if (key > cur->_key)
{
//parent存cur:代表改变后的cur的父节点
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
// 元素重复则返回false
else
{
return false;
}
}
// 此时找到了待插入的位置cur
// 将key插入作为cur的左/右子节点
//创建节点
cur = new Node(key);
if (key > parent->_key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true; //插入成功返回true
}
inOrder(遍历)
在上文框架重可以知道,inOrder()
是public访问,而_inOrder()
是private访问
好处:
- 封装性: 将具体实现细节封装在私有函数中,可以保证外部用户无法直接访问和修改数据结构,提高程序的封装性和安全性。
- 灵活性: 将遍历操作和遍历入口分离开来,可以在不暴露内部实现的前提下对外提供更多的功能接口,比如后序遍历、层序遍历等。
- 可读性: 通过将不同的功能或逻辑分别实现在不同的函数中,可以大大提高代码的可读性和可维护性。
- 在inOrder()中,直接调用_inOrder()并传入_root成员函数
public:
void inOrder()
{
_inOrder(_root);
cout << endl;
}
- 中序遍历符合 左 中 右 的顺序,当root为空时返回,按照顺序递归即可
private:
void _inOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_inOrder(root->_left);
cout << root->_key << " ";
_inOrder(root->_right);
}
Find(查找)
- 查找所执行的操作和插入时一致,遍历二叉搜索树,如果key大向右找,key小想左找即可。
bool Find(const K& key)
{
Node* cur = _root;
// 遍历二叉搜索树查找key
while (cur)
{
// 如果key大,则向右找
if (key > cur->_key)
{
cur = cur->_right;
}
// key小,向左找
else if (key < cur->_key)
{
cur = cur->_left;
}
// 找到了
else
{
return true;
}
}
// 退出循环,找不到
return false;
}
Erase(删除)
首先查找待删除的节点,如果找不到,直接返回false
找到节点后,执行删除操作,一共有三种情况:
- 待删除节点左树为空
- 待删除节点右树为空
- 待删除节点左右都不为空
下图做解释:
当左树为空 且 待删除的节点是根节点
当左树为空 且 待删除的节点不是根节点
当右树为空
当右树为空的操作思路和左树为空时一致,按照左树的思路写即可。
当左右树都不为空
另外:在这段代码中,由于是在查找 cur 节点并删除其子树的过程中,无论 cur 是否是根节点都不会影响查找和删除的过程。
bool Erase(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
// 找到要删除的节点
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
// 执行删除操作
else
{
// 三种情况
// 1、左为空
// 2、右为空
// 3、左右都不为空
// 1. 左为空时
if (cur->_left == nullptr)
{
// 当要删除的为根时,parent会出问题,单独写这种情况
if (cur == _root)
{
_root = cur->_right; // 直接将根改为右节点(左为空)
}
// 判断 cur 在 parent 的左侧还是右侧,然后将 parent 对应的子节点指向 cur 的右子节点。
else
{
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
// 清理掉 cur 的内存空间
delete cur;
cur = nullptr;
}
// 2. 右为空时
else if (cur->_right == nullptr)
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
delete cur;
cur = nullptr;
}
// 3. 左右都不为空
else
{
// 找到右子树的最小节点替换
Node* minParent = cur;
Node* min = cur->_right;
while (min->_left)
{
minParent = min;
min = min->_left;
}
swap(cur->_key, min->_key);
if (minParent->_left == min)
minParent->_left = min->_right;
else
minParent->_right = min->_right;
delete min;
}
return true;
}
}
return false;
}
默认成员函数
构造
这里使用了 C++11 中的新特性:= default
它表示对于该函数,我们采用编译器默认生成的实现方式,而不需要手写构造函数的实现代码。
直接用默认构造进行成员变量的初始化
BSTree() = default;//C++: 强制编译器生成默认构造
拷贝构造
拷贝构造依然调用_Copy()私有函数,传入bst._root
BSTree(const BSTree<K>& bst)
{
_root = _Copy(bst._root);
}
_Copy()
- 如果root为空,直接返回空
- 先创建新节点拷贝root的key值
- 然后拷贝左右指针,最后返回新节点
// 拷贝
Node* _Copy(Node* root)
{
// root为空返回空
if (root == nullptr)
{
return nullptr;
}
// 拷贝节点和指向关系
Node* copyRoot = new Node(root->_key);
copyRoot->_left = _Copy(root->_left);
copyRoot->_right = _Copy(root->_right);
return copyRoot;
}
析构函数
- 同理拷贝构造,共有函数调用
_Destory()
,传入_root
public:
~BSTree()
{
_Destory(_root);
}
- _Destory()利用递归删除左右子树,最后销毁根节点
private:
void _Destory(Node*& root)
{
if (root == nullptr)
return;
// 递归销毁根节点所有左右节点
_Destory(root->_left);
_Destory(root->_right);
// 销毁根节点
delete root;
root = nullptr;
}
赋值运算符重载
函数体中调用了 swap
函数,将当前对象的 根节点 _root 和新建的对象 bst 的根节点 bst._root 进行交换 ,从而实现了二叉搜索树的值拷贝和赋值操作
BSTree<K>& operator=(BSTree<K> bst)
{
swap(_root, bst._root); // 交换根节点
return *this;
}
增删查 – 递归写法
_InsertR(递归尾插)
- 首先判断 root 是否为空,如果为空,则直接创建一个新的节点并将 key 值赋值给新节点的键值 _key,然后将新节点作为根节点返回,表示插入操作成功。
- 若root不为空则查找要插入的位置,若key值大则向右递归 ,key值小则向左递归
- 如果key值等于某一个节点的值,不再插入,返回false
bool _InsertR(Node* root, const K& key)
{
// 为空,直接创建新节点
if (root == nullptr)
{
root = new Node(key);
return true;
}
// 查找位置并插入节点
if (key > root->_key)
{
return _InsertR(root->_right, key);
}
else if (key < root->_key)
{
return _InsertR(root->_left, key);
}
else
return false;
}
_FindR(查)
相同的思路,递归查找
bool _FindR(Node* root, const K& key)
{
// 遇空返回
if (root == nullptr)
{
return;
}
// 如果key大向右找
if (key > root->_key)
return _FindR(root->_right, key);
else if (key < root->_key)
return _FindR(root->_left, key);
else
return true;
}
_EraseR(删除)
bool _EraseR(Node* root, const K& key)
{
// 为空返回false
if (root == nullptr)
{
return false;
}
// 寻找要删除的节点
if (key > root->_key)
{
return _EraseR(root->_right, key);
}
else if (key < root->_left)
{
return _EraseR(root->_left, key);
}
else
{
// 执行删除操作
Node* del = root;
if (root->_left == nullptr)
{
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_right;
}
else
{
// 找到右树的最 小/左 节点并替换
Node* min = root->_right;
while (min->_left)
{
min = min->_left;
}
swap(root->_key, min->_key);
return _EraseR(root->_right, key);
}
// 删除节点
delete del;
return true;
}
}