目录
二叉搜索树的概念
二叉搜索数也称为二叉排序数或者二叉查找树
二叉搜索树:一棵二叉树,可以为空;如果不为空,要满足以下性质
1. 非空左子树的所有键值小于其根节点的键值
2. 非空右子树的所有键值大于其根节点的键值
3. 左右子树都是二叉搜索树
特征:
这样的二叉搜索树通过中序遍历后是升序,因为左子树都小于根节点,右子树都大于根节点,所以也称之为二叉排序树
单个节点的结构
struct BSTreeNode
{
BSTreeNode(const K& key)
:_key(key)
,_left(nullptr)
,_right(nullptr)
{}
K _key;
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
};
左子树的值要小于根节点的值,右子树的值要大于根节点的值
二叉搜索树的结构
template<class K>
struct BSTree
{
typedef BSTreeNode<K> Node;
public:
// 函数实现...
private:
Node* root = nullptr;
};
中序遍历
private:
void _InOrder(Node* root)
{
if (root == nullptr)
return;
// 左子树 - 根 - 右子树
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
public:
void InOrder()
{
_InOrder(_root);
cout << endl;
}
因为在类外面不能访问私有变量 _root,所以在类里面实现递归时需要配合子函数来实现
插入
bool Insert(const K& key)
{
// 判断二叉树是否为空,为空时直接插入链接
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
// 不为空时,需要找到合适的插入位置
Node* parent = nullptr;
Node* cur = _root;
while (cur != nullptr)
{
parent = cur;
if (cur->_key < key)
{
// 当前节点的值小于要插入的值,那么就往右节点走
cur = cur->_right;
}
else if(cur->_key > key)
{
// 否则就往左节点走
cur = cur->_left;
}
else
{
// 相等时就结束
return false;
}
}
// 找到合适的位置时就链接
cur = new Node(key);
if (parent->_key > key)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
return true;
}
因为规定二叉搜索树不能出现一样的值,所有在判断是否相等时直接返回 false ,证明这个值已经存在了
parent 指针指向 cur 的前一个节点,方便最后 cur 找到合适的位置时链接,在链接时,parent 指向的节点也不知道 cur 该链接在左或是右,所以需要再次比较得出结果
测试代码:
查找
bool Find(const K& kay)
{
Node* cur = _root;
while (cur != nullptr)
{
if (cur->_key > kay)
{
// 当前节点大于 key 时就往左节点找
cur = cur->_left;
}
else if (cur->_key < kay)
{
// 当前节点小于 key 时就往左节点找
cur = cur->_right;
}
else
{
// 相等就是找到了,返回 true
return true;
}
}
// 一直没有找到就返回 false
return false;
}
同样是根据左小右大的规则来查找
删除
bool Erase(const K& key)
{
// 先找到要删除的节点
Node* cur = _root;
Node* parent = nullptr;
while (cur != nullptr)
{
if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else
{
// 找到了,进行删除
if (cur->_left == nullptr) //当要删除的节点左为空时
{
if (cur == _root)
{
// 当要删除的节点为根节点时要单独处理
_root = cur->_right;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
}
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;
}
}
}
else //当要删除的节点左右都不为空时
{
// 先找到右树的最小节点(最左节点)
Node* subLeft = cur->_right;
Node* subParent = cur;
while (subLeft->_left != nullptr)
{
subLeft = subLeft->_left;
}
// 交换
std::swap(cur->_key, subLeft->_key);
// 链接
if (subLeft == subParent->_left)
subParent->_left = subLeft->_right;
else
subParent->_right = subLeft->_right;
}
// 删除完成,最后返回 true
return true;
}
}
// 没有找到要删除的节点
return false;
}
如图所示:
先通过左小右大的原则来找要删除的节点,找到后开始删除
在删除的节点里面有三种情况:
1. 要删除的节点只有左子树没有右子树
2. 要删除的节点只有右子树没有左子树
3. 要删除的节点有左右子树
叶子节点可以归纳为1、2点内
那么先拿删除 14 节点举例:
那么 14 节点就是右子树为空,创建一个 parent 节点指针指向 14 节点的父亲,也就是 10 节点,并且直接判断 14 节点是 10 节点的左还是右,因为 14 节点的右为空,所以判断后 10 节点直接链接 14 节点的左即可
需要注意的是,当要删除的节点是根节点时,就是根节点的右或者左子树为空时,需要作特殊处理,详情请见代码
删除 10 节点,10 节点是左子树为空的情况也是一样的,这里就不过多赘述
删除叶子节点也和上面的代码逻辑一样
删除 8 节点举例:
8 节点既是根节点也是左右子树都不为空的情况,那么删除的话就需要使用交换法
找出左子树中最大的节点,或者找出右子树最小的节点进行交换,这样才能成功交换,并且不影响二叉树
以上代码中我实现的是找出右子树最小的节点进行交换,所以 subLeft 就是找到右子树中最小的那个节点,也就是最左边的那个节点,subParent 指向 subLeft 的父亲节点
然后 subLeft 和要删除的节点 cur 进行交换,交换后再通过 subParent 进行链接,因为 subLeft 是右子树的最左节点,所以 subLeft 一定没有左子树,所以只需要判断 subLeft 在 subParent 的左还是右,再通过 subParent 的左或者右链接 subLeft 的右即可
析构
private:
void Destroy(Node*& root)
{
// 后续递归
if (root == nullptr)
{
return;
}
// 左子树 - 右子树 - 根
Destroy(root->_left);
Destroy(root->_right);
delete root;
}
public:
~BSTree()
{
_~BSTree(_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;
}
public:
BSTree(const BSTree<K>& t)
{
_root = copy(t._root);
}
重载赋值运算符
BSTree<K>& operator=(BSTree<K> t)
{
swap(_root, t._root);
return *this;
}