欢迎各位大佬光临本文章!!!
还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正。
本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬、帅哥、美女点点支持,您的每一分关心都是我坚持的动力。
我的博客地址:bingbing~bang的博客_CSDN博客
https://blog.csdn.net/bingbing_bang?type=blog
我的gitee:冰冰棒 (bingbingsupercool) - Gitee.com
https://gitee.com/bingbingsurercool
系列文章推荐
冰冰学习笔记:《二叉树的功能函数与OJ练习题》
冰冰学习笔记:《多态》
目录
系列文章推荐
前言
1.二叉搜索树的概念
2.二叉搜索树的实现
2.1二叉搜索树的插入
2.3二叉搜索树的查找
2.3二叉搜索树的删除
2.4其他函数
2.5二叉搜索树的性能
3.二叉搜索树的应用
3.1K模型
3.2K-V模型
前言
二叉搜索树是二叉树的一种实际应用,二叉树的特性和功能函数在前面的数据结构章节中进行了详细的介绍,忘记的可以自行前去复习查看。想要理解map和set这两个容器,就必须了解二叉搜索树。
1.二叉搜索树的概念
什么是二叉搜索树呢?二叉搜索树又称二叉排序树,它或者是一颗空树,或者是一颗具备以下条件的二叉树。
(1)若左子树不为空,则左子树上所有的节点的值都小于根节点的值
(2)若右子树不为空,则右子树上所有的节点的值都大于根节点的值
(3)左右子树也满足前两个条件,偶分别为二叉搜索树。
2.二叉搜索树的实现
二叉搜索树也是二叉树,二叉树的功能函数其都应该具备。但是二叉搜索树在一些函数上具备不同的性质。
二叉搜索树和二叉树具备相同的节点结构:
template<class K>
struct TreeNode
{
TreeNode<K>* _left;
TreeNode<K>* _right;
K _key;
TreeNode(const K& data)
:_left(nullptr), _right(nullptr), _key(data)
{}
};
2.1二叉搜索树的插入
在学习二叉树的插入操作时我们只是将新节点直接链接即可,但是二叉搜索树需要满足条件才能链接插入。二叉搜索树中不允许存在相同的节点,因此二叉搜索树的插入操作是一个bool类型的返回函数。当插入成功返回true,失败返回false。
(1)非递归插入
调用插入函数后,我们需要先判断插入节点是否为第一次插入,如果为第一次插入,那么新节点就是的根节点。如果不是根节点,那我们就需要找到新插入节点的位置,然后进行链接。
如何找到根节点的位置呢?这就需要利用二叉搜索树的特性进行查找。新插入节点的值比根节点大,我们需要向根节点的右子树查询,比根节点小就需要向左子树进行查询。当遇到与插入节点的值相同的节点时,跳出查找并返回false,插入失败。当查找到nullptr指针时,说明该位置极为新插入节点的位置。
这里需要注意一点,我们在链接新节点的时候需要有指向父亲节点的指针才能链接,否则无法链接。 因此我们在每次查找之后都需要记录父亲节点,然后再更新查找节点。
bool Insert(const K& val)
{
if (_root == nullptr)//第一次插入
{
_root = new Node(val);
return true;
}
else
{
Node* cur = _root;
Node* parent = _root;
while (cur)
{
if (cur->_key < val)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > val)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(val);
if (parent->_key > val)
parent->_left = cur;
else
parent->_right = cur;
}
return true;
}
(2)递归插入
我们知道二叉树天然适应递归操作,因此二叉搜索树也可以使用递归来进行插入操作。
这里我们需要注意,递归调用的时候我们的函数需要显示的接受root节点,但是在外面调用的时候我们又不能显示的传递_root,因此我们需要封装一层函数。在类里面就可以使用_root。
递归写法与之前循环的写法类似。当我们遇到空节点,意味着找到了正确的插入位置,此时我们需要将新节点链接到原有的结构中。如果没有遇到空节点我们就需要对节点的_key和新插入的val进行比对,val>_key则需要递归到根节点的右子树中进行插入,反之则去左子树进行插入操作。当遇到相同的节点的时候就直接层层返回false。
但是我们要注意,如果我们递归使用的root是传值调用,那么在进行节点链接时并不会影响上一层调用的结果,因为这一层的root节点是仅属于此次调用的局部变量,即便链接到root的后面也不会影响到全局的结构。
如果我们采用的是引用传参,在最后一层调用时,当前的root还是上一层子树节点的别名,链接将会直接连接到上一层子树的后面,从而完成树的结构链接。引用传参在前面的递归中并没有起到实际作用,但是在最后进行连接的时候才真正做到了点睛之笔。
bool InsertR(const K& val)
{
return _InsertR(_root, val);
}
bool _InsertR(Node*& root, const K& val)
{
if (root == nullptr)
{
root = new Node(val);
return true;
}
if (root->_key < val)
{
return _InsertR(root->_right, val);
}
else if (root->_key > val)
{
return _InsertR(root->_left, val);
}
else
return false;
}
2.3二叉搜索树的查找
在有了插入函数的基础后,查找函数就比较简单,我们只需要沿用插入的思路进行改进即可。
bool Find(const K& val)
{
if (_root == nullptr)
{
return false;
}
else
{
Node* cur = _root;
while (cur)
{
if (cur->_key < val)
{
cur = cur->_right;
}
else if (cur->_key > val)
{
cur = cur->_left;
}
else
return true;
}
}
return false;
}
bool FindR(const K& val)
{
return _FindR(_root, val);
}
bool _FindR(Node* root, const K& val)
{
if (root == nullptr)
{
return false;
}
if (root->_key < val)
{
return _FindR(root->_right, val);
}
else if (root->_key > val)
{
return _FindR(root->_left, val);
}
else
return true;
}
2.3二叉搜索树的删除
二叉树的删除需要考虑多种情况。
(1)删除节点没有孩子,可以直接删除。
(2)删除节点有一个孩子,需要将孩子托管之后在进行删除。
(3)删除节点左右孩子都有,需要替换节点,将替换后的节点进行删除。
在删除节点之前我们还是需要先找到该节点,如果没有删除的节点,那么就返回false。在找到节点后,我们需要考虑一些特殊情况,例如当前节点是否为根节点,如果为根节点我们需要找到替换根的新节点来充当树的根。
仔细分析会发现,我们在删除叶子节点的时候,删除后也需要对删除节点的父亲节点的指针进行置空处理避免出现野指针问题,所以我们将父节点原先指向删除节点的指针指向删除节点的孩子就可以完成置空处理,此时就将此种删除转化为删除节点只有一个孩子的情况了。
情况一:待删除节点的左子树为空
如果此时删除的节点为根节点,那么我们只需要将根节点更新为该节点的右子树即可,然后将此节点进行释放。
如果待删除节点并不是根节点,我们需要将待删除节点的右子树托管给删除节点的父亲节点,此时就有两种情况,如果待删除节点是父节点的左子树,那么删除节点的右子树就需要托管给父节点的左子树,反之就是托管给父节点的右子树。这就需要我们在前面进行查找的时候将父节点进行保存。
情况二:待删除节点右子树为空
此种情况与上面的情况正好相反,如果待删除节点是根节点,那么我们就让左子树做新的根节点。如果待删除节点不是根节点,那么我们就需要将待删除节点的左子树托管给待删除节点的父亲节点,也需要区分两种情况。
情况三:删除节点左右子树都不为空
此种情况最为复杂,我们的做法是找到一个叶子或者只含有一个孩子的节点来替换当前需要删除的节点,替换完毕后,转换成了将替换后的位置进行删除,此时就是上面两种情况的删除。
那么什么样的节点满足替换条件呢?替换后,新节点同样需要满足左子树小于根节点,右子树大于根节点。通过画图分析,我们发现,左子树中最大的节点和右子树中最小的节点都满足替换条件,因此我们任选一个就可以。
左子树的最大节点一定是最右边的节点,右子树的最小节点一定是最左边的节点,但是这两个节点都有可能具备子树。因此我们再替换后,需要将子树连接在删除节点的父节点上,然后再将节点删除。这就需要我们在寻找替换节点的时候也需要将替换节点的父亲节点进行保存。
这里我们以找右子树中最小的节点来做替换节点,因此我们需要两个节点指针,一个为rightmin指向右边的最小节点,一个为minpraent指向最小节点的父亲节点。在找到后我们将rightmin指向的数据与删除节点替换,然后判断minparent的左边指向的是rightmin还是右边指向的是rightmin,将rightmin的孩子节点托给minparent节点。
//替换法删除代码
Node* minParent = cur;
Node* rightMin = cur->_right;
while (rightMin->_left)
{
minParent = rightMin;
rightMin = rightMin->_left;
}
cur->_key = rightMin->_key;
//判断极端情况,删除根节点
if (minParent->_left == rightMin)
minParent->_left = rightMin->_right;
else
minParent->_right = rightMin->_right;
delete rightMin;
这里难免会有些人出现疑问,既然都知道4一定是右子树中最左边的节点,那么替换后,4一定在他的父节点的左子树中,我们直接将4的右孩子连接在父亲节点的左侧不行吗?为什么还需要保存父亲节点的指针判断是在左边还是右边呢?常规情况下确实是这样,但是不排除我们遇到下面情况的删除。
当我们想删除根节点8时,发现左右子树都不为空,此时我们需要去右边子树中找到最小的节点,经过循环我们找到了10这个节点,我们需要将10与8进行替换,此时我们发现rightmin指向8,minparent指向10,此时如果我们简单的将rightmin的孩子连接到minparent的左边会出现错误。因为10的左边有子树,并且原本rightmin不再是minparent的左孩子而是右孩子。
在上面的情况都考虑后我们的代码就实现成下面的状况:
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
{
// 开始删除
// 1、左为空
// 2、右为空
// 3、左右都不为空
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = cur->_right;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
cur = nullptr;
}
else if (cur->_right == nullptr)
{
if (_root == cur)
{
_root = cur->_left;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
delete cur;
cur = nullptr;
}
else
{
// 替换法删除 -- 找右边的最小值
Node* minParent = cur;
Node* rightMin = cur->_right;
while (rightMin->_left)
{
minParent = rightMin;
rightMin = rightMin->_left;
}
cur->_key = rightMin->_key;
//判断极端情况,删除根节点
if (minParent->_left == rightMin)
minParent->_left = rightMin->_right;
else
minParent->_right = rightMin->_right;
delete rightMin;
}
}
}
return false;
递归形式的删除代码和上面的逻辑基本一致,只不过查找方式不是循环而是递归查找,找到后进行删除的时候如果是左右子树都不为空的时候我们就直接利用替换删除将两个节点替换,然后将删除问题转化为递归删除替换后的子树中的删除节点。删除时,节点的传参依旧时引用传参。
bool EraseR(const K& val)
{
return _EraseR(_root, val);
}
bool _EraseR(Node*& root, const K& val)
{
if (root == nullptr)
{
return false;
}
if (root->_key < val)
{
return _EraseR(root->_right, val);
}
else if (root->_key > val)
{
return _EraseR(root->_left, val);
}
else
{
//找到了,开始删除
Node* del = root;
if (root->_left == nullptr)
root = root->_right;
else if (root->_right == nullptr)
root = root->_left;
else
{
//左右都不为空
Node* min = root->_right;
while (min->_left)
{
min = min->_left;
}
swap(min->_key, root->_key);
_EraseR(root->_right, val);
}
}
return true;
}
2.4其他函数
二叉搜索树的中序遍历也是一个常被的函数,由于二叉树的性质吗,在进行中序调用后我们会得到一组有序且去重的递增数列。因为二叉树永远满足左子树小于根节点小于右子树。
另外二叉搜索树的拷贝构造和赋值重载都需要完成深拷贝,因此我们的拷贝和赋值需要一个节点一个节点的进行拷贝构造。在析构函数中我们还需要将节点一一释放,因此我们采用后续遍历的方式进行析构,我们先析构左子树,在析构右子树,最后析构根节点。
在完成拷贝构造后,我们还需要自己实现默认构造函数,当然我们也可以使用C++11的语法,强制编译器生成默认的构造函数。
void InOrder()//中序遍历
{
_InOrder(_root);
cout << endl;
}
BSTree() = default;//强迫生成默认构造函数C++11
BSTree<K>& operator=(BSTree<K> t)//赋值重载
{
swap(_root, t._root);
return *this;
}
BSTree(const BSTree<K>& t)//拷贝构造
{
_root = _copy(t._root);
}
~BSTree()//析构
{
_Destory(_root);
}
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
Node* _copy(Node* root)
{
if (root == nullptr)
return nullptr;
Node* CopyRoot = new Node(root->_key);
CopyRoot->_left = _copy(root->_left);
CopyRoot->_right = _copy(root->_right);
return CopyRoot;
}
void _Destory(Node*& root)
{
if (root == nullptr)
{
return;
}
_Destory(root->_left);
_Destory(root->_right);
delete root;
root = nullptr;
}
2.5二叉搜索树的性能
我们发现在对二叉搜索树进行操作时都需要先进行搜索,因此搜索的性能就可以等效为搜索树的性能。理想情况下二叉搜索树的效率可以达到O(logN)的性能,当然这是在这颗二叉树是完全或近似完全二叉树的情况下。但是当我们插入一组有序数据时,二叉搜索树的查找性能就会降低到O(N),二叉树将会退化为单枝树。插入和删除只会在一边进行操作。
基于这种情况,大佬们又提出了AVL树和红黑树。这两个结构将通过某种特殊的机制维持二叉树的平衡,使得搜索二叉树近似一种完全二叉树,这样在进行插入,删除,查找的操作就会基本维持在O(logN)左右。
3.二叉搜索树的应用
3.1K模型
K模型就是二叉搜索树中存储的只有一个关键字,set就是K模型的一种应用。我们使用这种模型可以判断某个数据存不存在,每次通过给定的key进行搜索,找得到就是存在,找不到就不存在。
3.2K-V模型
K-V模型就是通过key来寻找val,每个key都对应一个val,map就是K-V的应用。这种模型我们可以记录某种数据出现的次数,数据类型作为key独立存在,次数作为val进行计算,首次出现则将key插入,再次出现只需要更改key对应的val的数值即可。
下面是对K-V模型的简单实现,与K类似,只不过多了一个参数V。
namespace KEY_VAL
{
template<class K,class V>
struct TreeNode
{
TreeNode<K,V>* _left;
TreeNode<K,V>* _right;
K _key;
V _val;
TreeNode(const K& key,const V& val)
:_left(nullptr), _right(nullptr),_key(key), _val(val)
{}
};
template<class K,class V>
class BSTree_KV
{
typedef TreeNode<K,V> Node;
public:
//BSTree_KV() {}
BSTree_KV() = default;//强迫生成默认构造函数C++11
void InOrder()
{
_InOrder(_root);
cout << endl;
}
//
//递归
Node* FindR(const K& key)
{
return _FindR(_root, key);
}
bool InsertR(const K& key,const V& val)
{
return _InsertR(_root, key,val);
}
bool EraseR(const K& key)
{
return _EraseR(_root, key);
}
~BSTree_KV()
{
_Destory(_root);
}
BSTree_KV<K,V>& operator=(BSTree_KV<K,V> t)
{
swap(_root, t._root);
return *this;
}
BSTree_KV(const BSTree_KV<K,V>& t)
{
_root = _copy(t._root);
}
private:
Node* _copy(Node* root)
{
if (root == nullptr)
return nullptr;
Node* CopyRoot = new Node(root->_key,root->_val);
CopyRoot->_left = _copy(root->_left);
CopyRoot->_right = _copy(root->_right);
return CopyRoot;
}
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 = root->_right;
while (min->_left)
{
min = min->_left;
}
swap(min->_key, root->_key);
swap(min->_val, root->_val);
_EraseR(root->_right, key);
}
}
return true;
}
bool _InsertR(Node*& root, const K& key,const V& val)
{
if (root == nullptr)
{
root = new Node(key,val);
return true;
}
if (root->_key < key)
{
return _InsertR(root->_right, key,val);
}
else if (root->_key > key)
{
return _InsertR(root->_left, key,val);
}
else
return false;
}
void _Destory(Node*& root)
{
if (root == nullptr)
{
return;
}
_Destory(root->_left);
_Destory(root->_right);
delete root;
root = nullptr;
}
Node* _FindR(Node* root, const K& key)
{
if (root == nullptr)
{
return nullptr;
}
if (root->_key < key)
{
return _FindR(root->_right, key);
}
else if (root->_key > key)
{
return _FindR(root->_left, key);
}
else
return root;
}
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << ":"<<root->_val<<" ";
_InOrder(root->_right);
}
Node* _root = nullptr;
};
}