⭐博客主页:️CS semi主页
⭐欢迎关注:点赞收藏+留言
⭐系列专栏:C++进阶
⭐代码仓库:C++进阶
家人们更新不易,你们的点赞和关注对我而言十分重要,友友们麻烦多多点赞+关注,你们的支持是我创作最大的动力,欢迎友友们私信提问,家人们不要忘记点赞收藏+关注哦!!!
二叉树搜索树
- 一、二叉搜索树
- 1、概念
- 2、搜索二叉树的实现(非递归版本和递归版本)
- (1)搭个框架
- (2)查找
- i、非递归版本
- ii、递归版本
- (3)插入
- i、非递归版本
- ii、递归版本
- (4)删除
- i、非递归版本
- ii、递归版本
- (5)中序遍历
- (7)拷贝
- (8)赋值
- (9)销毁
- 二、二叉树的性能分析
- 三、二叉搜索树的应用
- Key模型
- Key-Value模型
一、二叉搜索树
1、概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树
以下均是搜索二叉树,简而言之就是左小右大。
2、搜索二叉树的实现(非递归版本和递归版本)
(1)搭个框架
要实现二叉搜索树,我们需要搭建一个结点类的框架:
##1、结点类当中包含三个成员变量:节点值,左指针,右指针。
##2、结点类只需要一个构造函数即可,用于构造指定节点值的结点。
// 先来个结点的定义
template<class K>
// struct为了默认是开放的
struct BSTreeNode
{
// 左树右树和值
BSTreeNode<K>* _right;
BSTreeNode<K>* _left;
K _key;
// 构造一下左右结点和值(构造函数)
BSTreeNode(const K& key)
:_right(nullptr)
, _left(nullptr)
, _key(key)
{}
};
// 二叉搜索树的定义
template<class K>
class BSTree
{
public:
// 结点重命名为Node
typedef BSTreeNode<K> Node;
// 构造函数
BSTree()
:_root(nullptr)
{}
private:
Node* _root;
};
(2)查找
i、非递归版本
从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。最多查找高度次,走到到空,还没找到,这个值不存在。
// 查找
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;
}
// 找到则返回true
else
{
return true;
}
}
// 找不到返回false
return false;
}
ii、递归版本
代码:
// 查找 -- 递归版本
bool FindR(const K& key)
{
return _FindR(_root, key);
}
// 查找的子函数
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;
}
}
解析:
(3)插入
i、非递归版本
- 树为空,则直接新增节点,赋值给root指针
- 树不空,按二叉搜索树性质(左小右大)查找插入位置,插入新节点
(1)若待插入结点的值小于根节点的值,则需要将结点插入到左子树当中。
(2)若待插入结点的值大于根节点的值,则需要将结点插入到右子树当中。
(3)若待插入结点的值等于根结点的值,则插入结点失败。
往后如此进行下去,直到找到与待插入结点的值相同的结点判定为插入失败,或者是最终插入到某叶子结点的左右子树中(即空树当中)。
// 插入
bool Insert(const K& key)
{
// _root为空
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
// _root不为空
Node* cur = _root; // 记录一下当前节点从根节点开始
Node* parent = nullptr; // 父亲节点用来记录要插入的父节点
while (cur)
{
// 插入值比当前节点大则往右树走
if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
// 插入值比当前节点小则往左树走
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
// 值相等则返回false
else
{
return false;
}
}
// 创建一个cur的带值的信结点
cur = new Node(key);
// 判断一层parent的值与key的值的大小
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
ii、递归版本
神之一手:& – const K& key – 我下个递归版本是你的引用,我就是你,就不需要传参了。
// 查找
bool FindR(const K& key)
{
return _FindR(_root, key);
}
// 插入的子函数
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;
}
}
(4)删除
相对来讲会比较难一些,首先查找元素是否在二叉搜索树中,如果不存在,则返回false删除失败即可, 否则要删除的结点可能分下面三种情况:
&&&无儿无女型:也只有一种情况就是叶子结点,删除叶子结点,直接将其父亲节点指向空,把这个结点置空即可。
&&&独生家庭型:只有一个子节点,删除自己本身,并链接子节点和父节点。
&&&俩孩子型:可以让待删除的结点找其左子树中最大的结点保存值,然后将待删结点的值用左子树最大节点的值替代,最后将左子树最大结点删掉。或者是找其待删结点右子树当中最小的值的结点保存值,然后将待删结点的值用右子树最小结点的值代替,最后将右子树最小结点删掉。
俩孩子替换结点还有两种情况:倘若替换结点刚好是叶子结点没有孩子,直接删除置空即可。
倘若替换节点有一个孩子(不管左右孩子)就跟独生家庭的模式一模一样了!
解释为什么替换结点要么没孩子要么只有一个孩子:因为左小右大,倘若俩孩子都有,那这个替换节点一定不是左子树最大/右子树最小的结点!
i、非递归版本
代码层面解析:
实际上有四种:
1、左子树为空
2、右子树为空
3、左右子树都为空
4、要删的刚好是根节点
// 删除左子树的最右结点
bool Erase(const K& key)
{
Node* parent = nullptr; // 刚开始定义parent为NULL
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 (parent->_right == cur) // 前面确保parent已经往后找过结点了
{
parent->_right = cur->_right; // 直接连接parent的右和cur的右
}
else
{
parent->_left = cur->_right; // 连接左对右
}
}
}
// 右为空
else if (cur->_right == nullptr)
{
// 要删除的结点刚好是根节点
if (cur == _root)
{
_root = cur->_left;
}
else
{
if (parent->_right == cur)
{
parent->_right = cur->_left;
}
else
{
parent->_left = cur->_left;
}
}
}
else // 都不为空
{
// 找替换节点
Node* parent = cur; // 精妙部分就是parent定义为当前节点,因为保证parent节点不为空
Node* LeftMax = cur->_left; // 法一:用的是左树的最右节点
while (LeftMax->_right)
{
parent = LeftMax;// parent永远比cur节点往前走一步
LeftMax = LeftMax->_right;
}
swap(cur->_key, LeftMax->_key); // 交换待删结点的值和
// 判断LeftMax在哪里的问题
if (parent->_left == LeftMax)
{
parent->_left = LeftMax->_left;
}
else
{
parent->_right = LeftMax->_left;
}
cur = LeftMax; // 当前节点就是最右节点
}
delete cur;
return true;
}
}
return false;
}
// 删除右子树的最左节点
// 删除
bool Erase(const K& key)
{
Node* parent = nullptr; // 刚开始定义parent为NULL
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 (parent->_right == cur) // 前面确保parent已经往后找过结点了
{
parent->_right = cur->_right; // 直接连接parent的右和cur的右
}
else
{
parent->_left = cur->_right; // 连接左对右
}
}
}
// 右为空
else if (cur->_right == nullptr)
{
// 要删除的结点刚好是根节点
if (cur == _root)
{
_root = cur->_left;
}
else
{
if (parent->_right == cur)
{
parent->_right = cur->_left;
}
else
{
parent->_left = cur->_left;
}
}
}
//else // 都不为空
//{
// // 找替换节点
// Node* parent = cur; // 精妙部分就是parent定义为当前节点,因为保证parent节点不为空
// Node* LeftMax = cur->_left; // 法一:用的是左树的最右节点
// while (LeftMax->_right)
// {
// parent = LeftMax;// parent永远比cur节点往前走一步
// LeftMax = LeftMax->_right;
// }
// swap(cur->_key, LeftMax->_key); // 交换待删结点的值和
// // 判断LeftMax在哪里的问题
// if (parent->_left == LeftMax)
// {
// parent->_left = LeftMax->_left;
// }
// else
// {
// parent->_right = LeftMax->_left;
// }
// cur = LeftMax; // 当前节点就是最右节点
//}
else
{
Node* parent = cur;
Node* RightMin = cur->_right;
while (RightMin->_left)
{
parent = RightMin;
RightMin = RightMin->_left;
}
swap(cur->_key, RightMin->_key);
if (parent->_right = RightMin)
{
parent->_right = RightMin->_right;
}
else
{
parent->_right = cur->_left;
}
cur = RightMin;
}
delete cur;
return true;
}
}
return false;
}
ii、递归版本
递归版本好理解,
1、若树为空树。则结点删除失败,返回false。
2、若所给的key值小于树的根节点的值,则问题变为删除左子树中值为key的结点。
3、若所给的key值大于树的根节点的值,则问题变为删除右子树中值为key的结点。
4、若所给的key值为根节点的值,则还是找左子树的最小结点或者右子树的最右结点按照下面的步骤进行即可。
根的左子树为空,新结点为根的右
根的右子树为空,新节点为根的左
根的左右子树都不为空,则可以找左子树的最大或者是右子树的最小值,交换然后删除当前节点即可。
// 删除
bool EraseR(const K& key)
{
return _EraseR(_root, key);
}
// 删除的子函数
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;
// 1、左为空
// 2、右为空
// 3、左右都不为空
if (root->_left == nullptr)
{
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_left;
}
else
{
Node* leftmax = root->_left;
while (leftmax->_right)
{
leftmax = leftmax->_right;
}
swap(leftmax->_key, root->_key);
return _EraseR(root->_left, key);
}
delete del;
return true;
}
}
(5)中序遍历
思路就是左子树-根-右子树,递归左子树打印再递归右子树。
// 中序遍历
void InOrder()
{
_InOrder(_root);
cout << endl;
}
// 中序遍历的子函数
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
(7)拷贝
拷贝是深拷贝,创建一个copyroot的树然后递归拷贝左子树拷贝右子树,最后返回copyroot的树即可。
// 拷贝构造 -- 深拷贝
BSTree(const BSTree<K>& t)
{
_root = Copy(t._root);
}
// 拷贝构造子函数
Node* Copy(Node* root)
{
if (root == nullptr)
return nullptr;
Node* copyroot = new Node(root->_key);
root->_left = Copy(root->_left); // 拷贝左子树
root->_right = Copy(root->_right); // 拷贝右子树
return copyroot;
}
(8)赋值
利用swap库函数直接赋值。
// 赋值
BSTree<K>& operator=(BSTree<K> t)
{
swap(_root, t._root);
return *this;
}
(9)销毁
// 销毁
void Destroy(Node*& root)
{
if (root == nullptr)
return;
Destroy(root->_left);
Destroy(root->_right);
delete root;
root = nullptr;
}
二、二叉树的性能分析
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:log2N。
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:N/2。
三、二叉搜索树的应用
Key模型
key的搜索模型,判断关键字在不在
比如我们刷卡进宿舍,链接终端找到这个人的信息即可。
检查一篇英文文章有没有拼写出错。
Key-Value模型
KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。
比如:中英文的转换,通过英文单词的输入能够找到相对应的中文翻译< word, chinese > 就构成键对值。
统计单词个数,统计这个单词的出现频次,< word, count >。
// 改造二叉搜索树为KV结构
template<class K, class V>
struct BSTNode
{
BSTNode(const K& key = K(), const V& value = V())
: _PLeft(nullptr), _PRight(nullptr), _key(key), _Value(value)
{}
BSTNode<T>* _PLeft;
BSTNode<T>* _PRight;
K _key;
V _value
};
template<class K, class V>
class BSTree
{
typedef BSTNode<K, V> Node;
typedef Node* PNode;
public:
BSTree()
: _PRoot(nullptr)
{}
PNode Find(const K& key);
bool Insert(const K& key, const V& value);
bool Erase(const K& key);
private:
PNode _PRoot;
}
void TestBSTree()
{
// 输入单词,查找单词对应的中文翻译
BSTree<string, string> dict;
dict.Insert("string", "字符串");
dict.Insert("tree", "树");
dict.Insert("left", "左边、剩余");
dict.Insert("right", "右边");
dict.Insert("sort", "排序");
// 插入词库中所有单词
string str;
while (cin >> str)
{
BSTreeNode<string, string>* ret = dict.Find(str);
if (ret == nullptr)
{
cout << "单词拼写错误,词库中没有这个单词:" << str << endl;
}
else
{
cout << str << "中文翻译:" << ret->_value << endl;
}
}
}