文章目录
1. 二叉搜索树的概念
🐧① 二叉搜索树(BST,Binary Search Tree),也称二叉排序树或者二叉查找树;
🍎② 左子树的值都 小于 根结点,右子树的值都大于
根结点;
✌③ 一颗搜索树如下所示:
2. 二叉搜索树 K 模型的代码实现
2.1 Find ( ) 查找的实现
🐧① 从根结点开始查找🔍,key 比根结点大则往根结点右边走,比根结点小则往根结点的左边走;
🐧② 最多查找高度次
// 二叉树的查找
bool Find(const K& key)
{
if (_root == nullptr)
{
return false;
}
Node* cur = _root;
while (cur)
{
if (cur->_key > key)
{
cur = cur->_left;
}
else if (cur->_key < key)
{
cur = cur->_right;
}
else
{
return true;
}
}
return false;
}
2.2 Insert () 插入的实现
🐧① 树为空,直接在根结点插入数据;
🐧② 树不为空,则先查找要插入结点的位置,然后再插入数据;
🐧③ 我们需要用一个指针来记录被插入结点位置的 父结点;
// 插入的实现
bool Insert(const K& key)
{
// 1.可能刚开始的时候是一颗 空树 的情况
if (_root == nullptr)
{
// 这是 new 的特性:new对自定义类型自动调用构造函数并完成初始化
_root = new Node(key);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else
{
// 二叉搜索树一般不允许数据冗余的情况
return false;
}
}
cur = new Node(key);
if (parent->_key > key)
{
parent->_left = cur;
}
else if (parent->_key < key)
{
parent->_right = cur;
}
return true;
}
2.3 InOrder ( ) 中序遍历的实现
🐧① 因为中序遍历要用到 递归
实现,因为类外面的测试函数要用到 BSTree
这个类的 _root
私有的成员变量,在类外面无法获取,所以我们该怎么获取呢?
a. 将其设置为友元函数(不可取,因为这只是一个测试函数,不能随意的将测试函数作为友元);
b. 采取缺省函数,给默认值( 不可取) , 原因如下:
-root
是非静态成员变量
c.将递归函数嵌套定义,将
_InOrder
定义成私有的成员函数就好啦🐧
2.4 Erase ( ) 删除的实现
二叉搜索树删除的 细节比较多,可大致分成以下三种情况;
🐧① 要删除的结点只有左孩子结点
,在删除结点的时候,我们也要考虑被删除的结点是 父亲的左孩子还是右孩子;
注意:左右孩子都为空的情况可以归纳为这一类哦❗
还要特殊考虑当前删除的结点是根结点哦❗
🍎同理:要删除的结点 只有右孩子结点
,这种情况类似,所以就不讲述了。
🐧② 如果要删除的结点有左右孩子结点
采取替换法,用待删除结点的左子树最大结点或者右子树的最小结点来替换待删除的结点;
细节:
🍎Ⅰ、找到了右子树的最小结点,然后将它与待删除的结点替换之后,然后删除rightMin
,但是删除之前一定要考虑该删除的结点是否还有孩子
;
🍎Ⅱ、要记得考虑右子树的最小结点是其父结点的左孩子
还是右孩子
;(为什么要考虑这个情况呢? — 因为假如我们要删除的结点时根结点
,此时右子树的最小结点的父结点可能为根结点
,造成了rightMin
是父结点rightMinParent
右孩子的特殊情况)。
下面这两张图极其重要,可以完美的解释这段代码:
// 二叉搜索树的删除
bool Erase(const K& key)
{
if (_root == nullptr)
return false;
Node* cur = _root;
Node* parent = cur;
// 1. 首先找到要删除的结点
while (cur)
{
if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else
{
// ① 如果要删除的结点没有 右孩子
if (cur->_right == nullptr)
{
// 如果要删除的结点是根结点
if (parent == cur)
{
_root = cur->_left;
}
else
{
if (parent->_key > key)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
delete cur;
}
// ② 如果要删除的结点没有 左孩子
else if (cur->_left == nullptr)
{
// 如果要删除的结点是根结点
if (parent == cur)
{
_root = cur->_right;
}
else
{
if (parent->_key > key)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
}
// ③ 如果要删除的结点有 左右孩子结点
else
{
// 先找到右子树的最小结点
Node* rightMin = cur->_right;
Node* rightMinParent = cur;
while (rightMin->_left)
{
rightMinParent = rightMin;
rightMin = rightMin->_left;
}
swap(cur->_key, rightMin->_key);
// 判断 rightMin 在 rightMinParent的左边还是右边
if (rightMinParent->_left == rightMin)
rightMinParent->_left = rightMin->_right;
else
rightMinParent->_right = rightMin->_right;
delete rightMin;
}
return true;
}
}
return false;
}
3. 二叉搜索树的 KV 模型
🐧① K模型:K模型即只有key作为关键码
,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
在二叉搜索树中检索该单词是否存在
,存在则拼写正确,不存在则拼写错误。
🐧②KV模型:每一个关键码 key,都有与之对应的值 Value 即<Key, Value>的键值对。
统计单词次数,统计成功后,
给定单词就可快速找到其出现的次数
,单词与其出现次数就是<word, count>就构成一种键值对;
- 以下是改造成 KV 模型的代码:
🍎 如何改造呢 ? ------ 其实只需要在K模型
的基础上面稍加改动即可;
🍎Node* Find(const K& key)
函数的编写,返回找到的结点的指针,这个时候就不是返回bool
值了;因为要涉及对value
的修改操作,所以必须返回指针。
namespace key_value {
template<class K, class V>
struct BSTreeNode {
BSTreeNode<K, V>* _left;
BSTreeNode<K, V>* _right;
K _key;
V _value;
// 初始化
BSTreeNode(const K& key, const V& value)
:_left(nullptr)
, _right(nullptr)
, _key(key)
, _value(value)
{}
};
template<class K, class V>
class BSTree {
typedef BSTreeNode<K, V> Node;
public:
// 插入
bool Insert(const K& key, const V& value)
{
if (_root == nullptr)
{
_root = new Node(key, value);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;
}
}
cur = new Node(key, value);
if (parent->_key > key)
{
parent->_left = cur;
}
else if (parent->_key < key)
{
parent->_right = cur;
}
return true;
}
// 查找
Node* Find(const K& key)
{
if (_root == nullptr)
return nullptr;
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)
{
// 1.先找到要删除的结点
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
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;
}
}
delete cur;
}
// 如果要删除的结点右孩子为空
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;
}
// 左右孩子都不为空的情况
// 替换法:寻找右子树的最小结点来替换
else
{
Node* rightMinParent = cur;
Node* rightMin = cur->_right;
// 找到右子树的最小结点
while (rightMin->_left)
{
rightMinParent = rightMin;
rightMin = rightMin->_left;
}
swap(cur->_key, rightMin->_key);
// 最左节点不一定是叶子节点
if (rightMin == rightMinParent->_right)
{
rightMinParent->_right = rightMin->_right;
}
else
{
rightMinParent->_left = rightMin->_right;
}
delete rightMin;
}
return true;
}
}
return false;
}
// 中序遍历
void Inorder()
{
_Inorder(_root);
}
private:
void _Inorder(Node* root)
{
if (root == nullptr)
{
return;
}
_Inorder(root->_left);
cout << root->_key << " " << root->_value;
_Inorder(root->_right);
}
Node* _root = nullptr;
};
// 查找是否有某种水果
void TestBSTreeKeyValue()
{
BSTree<string, string> tr2;
tr2.Insert("apple", "苹果");
tr2.Insert("orange", "橘子");
tr2.Insert("pear", "梨");
tr2.Insert("strawberry", "草莓");
string str;
while (cin >> str)
{
BSTreeNode<string, string>* ptr = tr2.Find(str);
if (ptr != nullptr)
{
cout << ptr->_key << " " << ptr->_value << endl;
}
else
{
cout << "Sorry,查无此水果,请重新输入!" << endl;
}
}
}
// 统计某种水果的数量
void TestBSTreeCount()
{
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜","苹果", "香蕉", "苹果", "香蕉" };
BSTree<string, int> countTree;
for (auto str : arr)
{
BSTreeNode<string, int>* ptr = countTree.Find(str);
if (ptr == nullptr)
{
countTree.Insert(str, 1);
}
else
{
ptr->_value++;
}
}
countTree.Inorder();
}
}
4.二叉搜索树的性能分析
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:
l
o
g
2
N
log_2 N
log2N
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:
N
2
\frac{N}{2}
2N
🍎为了解决上面这个最差情况下的二叉搜索树问题,所以引出了AVL
树和红黑树,后面的博客我将会谈到!