二叉搜索树
一,二叉搜索树的概念
🚀二叉搜索树又叫二叉排序树和二叉查找树,它主要有如下几个性质:
- 1,若它的左子树不为空,那么左子树的结点的值一定小于根结点的值。
- 2,若它的右子树不为空,那么右子树的结点的值一定小于根结点的值。
- 3,它的左右子树都是二叉搜索树。
🚀二叉搜索树的查找性能并不高,例如它的左右子树高度差距很大,那么查找效率接近O(N),平衡二叉搜索树的搜索性能很高达到O(logN)。
二,代码实现
🚀主要完成下面的接口:
#pragma once
namespace gy
{
template<class K>
struct TreeNode
{
K _val;
TreeNode<K>* _left = nullptr;
TreeNode<K>* _right = nullptr;
TreeNode(int val = 0)
:_val(val)
{}
};
template<class K>
class BSTree
{
public:
typedef TreeNode<K> node;
BSTree(const BSTree<K>& t)
{}
BSTree& operator==(BSTree<K> t)
{}
bool Insert(const K& k)
{}
bool find(const K& k)
{}
bool erase(const K& k)
{}
~BSTree()
{}
private:
node* _root = nullptr;
};
}
插入
🚀如果根结点为空的话直接创建结点返回true。否则,创建两个指针变量cur和prev从根节点开始遍历,如果cur的结点的值大于要插入的值那么cur = cur->_left;如果小于要插入的值那么cur = cur->_right,如果等于的话直接返回false。找到相应的位置后还要判断插入的位置是prev的左子树还是右子树。
bool Insert(const K& k)
{
if (_root == nullptr)
{
_root = new node(k);
return true;
}
node* prev = nullptr;
node* cur = _root;
while (cur)
{
if (cur->_val > k)
{
prev = cur;
cur = cur->_left;
}
else if (cur->_val < k)
{
prev = cur;
cur = cur->_right;
}
else
return false; //二叉搜索树中不存在相等的结点
}
if (prev->_val > k)
prev->_left = new node(k);
else
prev->_right = new node(k);
return true;
}
🚀插入的递归实现
bool InsertR(const K& k)
{
return _InsertNonR(_root, k);
}
bool _InsertR(node*& root, const K& k)
{
if (root == nullptr)
{
root = new node(k);
return true;
}
if (root->_val == k)
return false;
if (root->_val > k)
return _InsertNonR(root->_left, k);
if (root->_val < k)
return _InsertNonR(root->_right, k);
}
🚀这个引用使用的非常巧妙,如果使用的一级指针的话,形参的改变不影响实参,当这层栈帧销毁返回上一层栈帧时达不到想要的效果。二级指针又有些繁琐,所以这里用应用恰到好处。
查找
🚀插入的过程就已经做了一遍查找的工作了,如果当前结点大于要查找的值,那么就去它左子树查找,如果小于就到它的右子树去查找,如果相等那么就找到了。
bool find(const K& k)
{
node* cur = _root;
while (cur)
{
if (cur->_val > k)
cur = cur->_left;
else if (cur->_val < k)
cur = cur->_right;
else
return true;
}
return false;
}
删除
🚀删除操作是二叉搜索树的所有操作中最麻烦的一个,主要分为一下几种情况:
- 1,要删除的结点是叶子结点,可直接删除。
- 2,要删除的结点只有左子树或者只有右子树,将其左子树或右子树连接到其父节点。
- 3,要删除的结点既有左子树又有右子树,那么就需要找此结点右子树的最小结点或者此节点左子树的最大结点来替换掉此结点,然后删除替换结点。
注意: 替换的结点有两种情况:叶子结点/有左子树或者右子树,如果用其右子树的最小结点替代,那么替代节点有可能有右子树,反之有可能有左子树。如果是叶子结点那么可以直接删除,如果替换节点有子节点那么就需要将其子节点链接到其父节点(这里值得注意的是,其父节点有可能就是要删除的结点)。 - 4,还有要注意的一点是,上面提到的要删除的结点只有左子树或者只有右子树的时候,如果恰好此节点是根结点,那么prev结点为空,如不不加处理就会出现解引用空指针的问题。
bool erase(const K& k)
{
node* cur = _root;
node* prev = nullptr;
while (cur)
{
if (cur->_val > k)
{
prev = cur;
cur = cur->_left;
}
else if(cur->_val < k)
{
prev = cur;
cur = cur->_right;
}
else
{
//找到了要删除的结点
//1.左为空
if (cur->_left == nullptr)
{
//判断是否为根结点
if (cur == _root)
{
node* tmp = _root->_right;
delete _root;
_root = tmp;
return true;
}
else
{
if (prev->_left == cur)
{
prev->_left = cur->_right;
delete cur;
return true;
}
else
{
prev->_right = cur->_right;
delete cur;
return true;
}
}
}
//2.右为空
else if (cur->_right == nullptr)
{
//判断是否为根结点
if (cur == _root)
{
node* tmp = _root->_left;
delete _root;
_root = tmp;
return true;
}
else
{
if (prev->_left == cur)
{
prev->_left = cur->_left;
delete cur;
return true;
}
else
{
prev->_right = cur->_left;
delete cur;
return true;
}
}
}
else
{
//3.有左右子树,那么就找替代节点--右子树最小的结点
node* min_right = cur->_right;
node* prev_min = cur;
while (min_right->_left)
{
prev_min = min_right;
min_right = min_right->_left;
}
cur->_val = min_right->_val;
if (prev_min == cur)
{
prev_min->_right = min_right->_right;
}
else
{
prev_min->_left = min_right->_right;
}
delete min_right;
return true;
}
}
}
return false;
}
🚀删除的递归实现:
bool eraseR(const K& k)
{
return _EraseR(_root, k);
}
bool _EraseR(node*& root, const K& key)
{
if (root == nullptr)
return false;
if (root->_key < key)
{
return _EraseR(root->_right, key);
}
else if (root->_key > key)
{
return _EraseR(root->_left, key);
}
else
{
node* del = root;
// 开始准备删除
if (root->_left == nullptr)
{
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_left;
}
else
{
//有两个孩子结点
node* min_right = root->_right;
while (min_right->_left)
{
min_right = min_right->_left;
}
swap(root->_key, min_right->_key);
return _EraseR(root->_right, key);
}
delete del;
return true;
}
}
🚀删除递归实现应注意的是,要删除的结点有左右子树的时候,我们采用的是交换替换结点与要删除结点的key值,这样就转化成了删除的结点只有一个子树的问题,但是这里注意的是,我们交换完之后是从要删除结点的又子树开始查找的,而不是从替换结点开始查找的,这是因为我们的参数是指针的引用,如果从替换结点开始删除的话,我们无法用到这个引用,因为这个引用是要删除结点的上一层栈帧中的->_left或者是->_right。
拷贝构造
🚀这里的拷贝构造肯定是深拷贝,就是把这棵树复制一份,所以我们的拷贝构造就转化成调用一个copy函数。
BSTree(const BSTree<K>& t)
{
_root = copy(t._root);
}
node* copy(node* root)
{
if (root == nullptr)
return nullptr;
node* newnode = new node(root->_val);
newnode->_left = copy(root->_left);
newnode->_right = copy(root->_right);
return newnode;
}
赋值运算符重载
🚀采用一种现代写法,形参为同类对象,那么调用此函数的时候会发生拷贝构造(一次深拷贝),然后交换根节点的指针,形参是个局部变量出了这个函数它所指向的二叉搜索树就会被析构。
BSTree& operator==(const BSTree<K>& t)
{
std::swap(_root, t._root);
return *this;
}
析构
🚀析构函数就是在销毁这颗二叉树,我们可以调用destroy这个递归函数来完成析构的操作。
~BSTree()
{
destroy(_root);
}
void destroy(node* root)
{
if (root == nullptr)
return;
destroy(root->_left);
destroy(root->_right);
delete root;
}
三,改造出K-V结构的二叉搜索树
🚀搜索树的一个重要作用就是查找,现实生活中通常有两类查找的方式:
1,看某个事物在不在一个集合中。
- 门禁系统
- 车库系统
2,通过一个事物去找另一个事物。 - 通过手机号查询快递
- 在线词典
上面实现的二叉搜索树可以完成在不在的操作,下面我们在实现一下通过一个事物去找另一个事物。其实就是多了一个模板参数V。
namespace _gy
{
template<class K,class V>
struct BSTNode
{
BSTNode* _left;
BSTNode* _right;
K _key;
V _val;
BSTNode(const K& x,const V& val)
:_left(nullptr)
, _right(nullptr)
, _key(x)
,_val(val)
{}
};
template<class K,class V>
class BSTree
{
public:
typedef BSTNode<K,V> node;
BSTree<K, V>() = default;
BSTree<K, V>(const BSTree<K,V>& t)
{
_root = copy(t._root);
}
BSTree<K,V>& operator=(const BSTree<K,V> t)
{
std::swap(_root, t._root);
return *this;
}
~BSTree()
{
Destroy(_root);
}
/*BSTree()
:_root(nullptr)
{}*/
bool Insert(const K& key,const V& val)
{
if (_root == nullptr)
{
_root = new node(key,val);
return true;
}
node* prev = _root;
node* cur = _root;
while (cur)
{
if (cur->_key > key)
{
prev = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
prev = cur;
cur = cur->_right;
}
else
return false;
}
//找到合适的位置
if (key > prev->_key)
prev->_right = new node(key,val);
else if (key < prev->_key)
prev->_left = new node(key,val);
return true;
}
void Inorder()
{
_Inorder(_root);
cout << endl;
}
node* Find(const K& key)
{
node* cur = _root;
while (cur)
{
if (cur->_key > key)
cur = cur->_left;
else if (cur->_key < key)
cur = cur->_right;
else
return cur;
}
return nullptr;
}
bool Erase(const K& key)
{
node* cur = _root;
node* prev = _root;
while (cur)
{
if (cur->_key > key)
{
prev = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
prev = cur;
cur = cur->_right;
}
else if (cur->_key == key)
{
//找到要删除的结点
// 如果这个结点是根节点并且没有左右孩子
if (cur == _root && cur->_left == nullptr)
{
node* tmp = cur->_right;
delete cur;
_root = tmp;
return true;
}
else if (cur == _root && cur->_right == nullptr)
{
node* tmp = cur->_left;
delete cur;
_root = tmp;
return true;
}
//1.叶子结点或者只有左结点or右结点
if (cur->_left == nullptr)
{
if (prev->_left == cur)
{
prev->_left = cur->_right;
delete cur;
return true;
}
if (prev->_right == cur)
{
prev->_right = cur->_right;
delete cur;
return true;
}
}
else if (cur->_right == nullptr)
{
if (prev->_left == cur)
{
prev->_left = cur->_left;
delete cur;
return true;
}
if (prev->_right == cur)
{
prev->_right = cur->_left;
delete cur;
return true;
}
}
else
{
//2.要删除的结点有两个子结点
//找到其左子树的最右边的结点或者
//其右子树的最左结点替代其位置
node* min_right = cur->_right;
node* prev_min = cur;
while (min_right->_left)
{
prev_min = min_right;
min_right = min_right->_left;
}
cur->_key = min_right->_key;
if (prev_min == cur)
{
cur->_right = min_right->_right;
}
else
{
prev_min->_left = min_right->_right;
}
delete min_right;
return true;
}
}
}
return false;
}
protected:
void _Inorder(node* root)
{
if (root == nullptr)
return;
_Inorder(root->_left);
cout << "(" << root->_key << "," << root->_val << ")" << " ";
_Inorder(root->_right);
}
void Destroy(node* root)
{
if (root == nullptr)
return;
Destroy(root->_left);
Destroy(root->_right);
delete root;
}
private:
node* _root = nullptr;
};
}
🚀这种K-V结构其实与K结构没有多大区别,在插入操作上形参有做不同,在查找操作上K-V结构的二叉搜索树时返回结点的指针,K-V结构的查找和插入都是按照K来进行的,并且它们的析构,拷贝构造等没有任何区别。二者并无本质区别。