目录
一、二叉搜索树概念
如果一个二叉树具有如下性质,那么就叫它二叉搜索树:
1.左子树上的所有节点值都小于根节点
2.右子树上的所有节点值都小于根节点
3.左右子树也为二叉搜索树
二叉搜索树(Binary Search Tree,BST)又叫二叉排序树或者二叉查找树
叫二叉排序树是因为:二叉搜索树中序遍历时得到的数据为升序排序
(通常来说,二叉搜索树不允许数据冗余,即数据重复)
二、二叉搜索树的操作
二叉搜索树节点的结构:
template<class T>
struct BSTreeNode {
T _val;
struct BSTreeNode* _left;
struct BSTreeNode* _right;
//构造函数
BSTreeNode(T val)
:_val(val),
_left(nullptr),
_right(nullptr)
{}
};
1.查找操作
从根节点开始找,值比根节点小则向左查找,值比根节点大则向右查找。最多查找高度次
//查找
bool Find(const K& key)
{
Node* cur = _root;
else
{
while (cur)
{
if (key < cur->_key)
cur = cur->_left;
else if (key > cur->_key)
cur = cur->_right;
else
return true;
}
}
return false;
}
2.插入操作
二叉搜索树只能插入可以比较大小的数据
//插入
bool Insert(const K& key)
{
Node* cur = _root;
Node* parent = nullptr;
if (cur == nullptr)//树为空
{
_root = new Node(key);
return true;
}
else//树不为空
{
while (cur)
{
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;//二叉搜索树不允许数据冗余
}
}
Node* newnode = new Node(key);
if (key < parent->_key)
parent->_left = newnode;
else if (key > parent->_key)
parent->_right = newnode;
return true;
}
}
3.中序遍历
中序遍历时一个递归操作,所以一定会有根节点作为参数。但是二叉搜索树对象调用中序遍历函数时,无法将其根节点不方便传入函数(除非再设计一个根节点的接口),所以给中序遍历函数套壳,将根节点传入实现函数,再给实现函数套壳,二叉搜索树调用套壳函数,即调用实现函数
//中序遍历(排序)(递归套壳)
void InOrder()
{
_InOrder(_root);
cout << endl;
}
//中序遍历(排序)(实现原理)
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
4.删除操作
首先查找要删除的数据是否在二叉树中,如果不在则返回false
如果在,则有以下四种情况(将要删除的节点设为delete):
1.delete没有孩子节点:delete为左节点,则将父亲节点的左孩子置为空;delete为右节点,则将父亲节点的右孩子置为空
2.delete只有左孩子节点:delete为左节点,则将父亲节点的左孩子指向delete的左孩子;delete为右节点,则将父亲节点的右孩子指向delete的左孩子
3.delete只有右孩子节点:delete为右节点,则将父亲节点的左孩子指向delete的右孩子;delete为右节点,则将父亲节点的右孩子指向delete的右孩子
4.delete左右孩子节点都有:将左子树的最大节点(左子树中最右边的节点)或者右子树的最小节点(右子树中最左边的节点)的值与delete的值进行交换,再删除delete
(如何寻找左子树的最大节点?左子树的最大节点就是左子树中最右边的节点)
(如何寻找右子树的最小节点?右子树的最小节点就是右子树中最左边的节点)
经过分析,发现情况1与情况2或者3可以合并(以情况1和情况2合并为例),因为情况2中将父亲节点的孩子指向delete的孩子,中情况1中同样适用,只不过情况1中delete的孩子为空,正好符合将父亲节点的孩子置空
实际写代码过程中,会发现有两个坑:
第一个坑是情况1、2、3,删除节点时,如果该节点是根节点,根节点没有父亲节点,代码出错,所以需要加一个判断条件
第二个坑是情况4,如果出现右子树的最小节点刚好是根节点的右孩子的情况(左子树的最大节点刚好是根节点的左孩子的情况),那么RightMin就是右孩子节点,而其他情况都是左孩子节点,这就需要我们单独判断一下RightMin的节点情况,再进行删除
//删除
bool Erase(const K& key)
{
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else//找到值
{
if (cur->_right == nullptr)//情况1和情况2合并(没有孩子节点或者只有左孩子节点)
{
if (cur == _root)//特殊情况:删除根节点
_root = _root->_left;
else
{
if (cur == parent->_left)//左结点
parent->_left = cur->_left;
else if (cur == parent->_right)//右结点
parent->_right = cur->_left;
}
delete cur;
}
else if (cur->_left == nullptr && cur->_right != nullptr)//情况3(只有右孩子节点)
{
if (cur == _root)//特殊情况:删除根节点
_root = _root->_left;
else
{
if (cur == parent->_left)//左结点
parent->_left = cur->_right;
else if (cur == parent->_right)//右结点
parent->_right = cur->_right;
}
delete cur;
}
else//情况4(左右孩子节点都有)(用右子树的最小节点替换)
{
Node* RightMinParent = cur;
Node* RightMin = cur->_right;//先确定在右子树中寻找
while (RightMin->_left)
{
RightMinParent = RightMin;
RightMin = RightMin->_left;
}
swap(cur->_key, RightMin->_key);//交换
if (RightMin == RightMinParent->_left)//RightMin是左孩子
RightMinParent->_left = RightMin->_right;//删除节点,将其父亲节点连接RightMin的右孩子(因为RightMin是最左节点,所以它一定是父亲节点的左孩子,且一定没有左孩子)
else//RightMin是右孩子
RightMinParent->_right = RightMin->_right;//删除节点,将其父亲节点连接RightMin的右孩子(因为RightMin是最左节点,所以它一定是父亲节点的左孩子,且一定没有左孩子)
delete(RightMin);
}
return true;
}
}
return false;
}
5.完整代码
namespace key {
template<class K>
struct BSTreeNode {
K _key;
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
//构造函数
BSTreeNode(K key)
:_key(key),
_left(nullptr),
_right(nullptr)
{}
};
template<class K>
class BSTree {
typedef struct BSTreeNode<K> Node;
public:
//构造函数
BSTree()
:_root(nullptr)
{}
//插入
bool Insert(const K& key)
{
Node* cur = _root;
Node* parent = nullptr;
if (cur == nullptr)//树为空
{
_root = new Node(key);
return true;
}
else//树不为空
{
while (cur)
{
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;//二叉搜索树不允许数据冗余
}
}
Node* newnode = new Node(key);
if (key < parent->_key)
parent->_left = newnode;
else if (key > parent->_key)
parent->_right = newnode;
return true;
}
}
//查找
bool Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (key < cur->_key)
cur = cur->_left;
else if (key > cur->_key)
cur = cur->_right;
else
return true;
}
return false;
}
//中序遍历(排序)(递归套壳)
void InOrder()
{
_InOrder(_root);
cout << endl;
}
//删除
bool Erase(const K& key)
{
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else//找到值
{
if (cur->_right == nullptr)//情况1和情况2合并(没有孩子节点或者只有左孩子节点)
{
if (cur == _root)//特殊情况:删除根节点
_root = _root->_left;
else
{
if (cur == parent->_left)//左结点
parent->_left = cur->_left;
else if (cur == parent->_right)//右结点
parent->_right = cur->_left;
}
delete cur;
}
else if (cur->_left == nullptr && cur->_right != nullptr)//情况3(只有右孩子节点)
{
if (cur == _root)//特殊情况:删除根节点
_root = _root->_left;
else
{
if (cur == parent->_left)//左结点
parent->_left = cur->_right;
else if (cur == parent->_right)//右结点
parent->_right = cur->_right;
}
delete cur;
}
else//情况4(左右孩子节点都有)(用右子树的最小节点替换)
{
Node* RightMinParent = cur;
Node* RightMin = cur->_right;//先确定在右子树中寻找
while (RightMin->_left)
{
RightMinParent = RightMin;
RightMin = RightMin->_left;
}
swap(cur->_key, RightMin->_key);//交换
if (RightMin == RightMinParent->_left)//RightMin是左孩子
RightMinParent->_left = RightMin->_right;//删除节点,将其父亲节点连接RightMin的右孩子(因为RightMin是最左节点,所以它一定是父亲节点的左孩子,且一定没有左孩子)
else//RightMin是右孩子
RightMinParent->_right = RightMin->_right;//删除节点,将其父亲节点连接RightMin的右孩子(因为RightMin是最左节点,所以它一定是父亲节点的左孩子,且一定没有左孩子)
delete(RightMin);
}
return true;
}
}
return false;
}
private:
Node* _root;
//中序遍历(排序)(实现原理)
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
};
}
三、二叉搜索树的K模型和KV模型
1.K模型
K模型中只有key作为关键码,结构中只需要存储key即可
应用场景:给定一个单词,判断该单词是否拼写正确
将词库中的每个单词作为一个key,构建一棵二叉树搜索树;在二叉搜索树中查找给定单词是否存在,存在则拼写正确,不存在则拼写错误
2.KV模型
KV模型中没有个关键码key都有一个与之对应的值Value,即<Key,Value>键值对
(KV模型是支持修改的,可以修改value值;但是K模型不能修改,修改后就不能保证是二叉搜索树了)
应用场景1:英汉词典中,英文与中文的对应关系,通过英文单词快速找到与其对应的中文,<word,chinese>就是一种键值对
应用场景2:统计单词出现次数,给定一个单词就可以知道它的出现次数,<word,count>也是一种键值对
二叉搜索树的KV模型源代码(在K模型基础上稍加改动即可):
//源代码
namespace key_value {
template<class K, class V>
struct BSTreeNode {
K _key;
V _value;
BSTreeNode<K,V>* _left;
BSTreeNode<K,V>* _right;
//构造函数
BSTreeNode(K key, V value)
:_key(key),
_value(value),
_left(nullptr),
_right(nullptr)
{}
};
template<class K, class V>
class BSTree {
typedef struct BSTreeNode<K,V> Node;
public:
//构造函数
BSTree()
:_root(nullptr)
{}
//插入
bool Insert(const K& key, const V& value)
{
Node* cur = _root;
Node* parent = nullptr;
if (cur == nullptr)//树为空
{
_root = new Node(key, value);
return true;
}
else//树不为空
{
while (cur)
{
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;//二叉搜索树不允许数据冗余
}
}
Node* newnode = new Node(key, value);
if (key < parent->_key)
parent->_left = newnode;
else if (key > parent->_key)
parent->_right = newnode;
return true;
}
}
//查找
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (key < cur->_key)
cur = cur->_left;
else if (key > cur->_key)
cur = cur->_right;
else
return cur;
}
return nullptr;
}
//中序遍历(排序)(递归套壳)
void InOrder()
{
_InOrder(_root);
cout << endl;
}
//删除
bool Erase(const K& key)
{
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else//找到值
{
if (cur->_right == nullptr)//情况1和情况2合并(没有孩子节点或者只有左孩子节点)
{
if (cur == _root)//特殊情况:删除根节点
_root = _root->_left;
else
{
if (cur == parent->_left)//左结点
parent->_left = cur->_left;
else if (cur == parent->_right)//右结点
parent->_right = cur->_left;
}
delete cur;
}
else if (cur->_left == nullptr && cur->_right != nullptr)//情况3(只有右孩子节点)
{
if (cur == _root)//特殊情况:删除根节点
_root = _root->_left;
else
{
if (cur == parent->_left)//左结点
parent->_left = cur->_right;
else if (cur == parent->_right)//右结点
parent->_right = cur->_right;
}
delete cur;
}
else//情况4(左右孩子节点都有)(用右子树的最小节点替换)
{
Node* RightMinParent = cur;
Node* RightMin = cur->_right;//先确定在右子树中寻找
while (RightMin->_left)
{
RightMinParent = RightMin;
RightMin = RightMin->_left;
}
swap(cur->_key, RightMin->_key);//交换
if (RightMin == RightMinParent->_left)//RightMin是左孩子
RightMinParent->_left = RightMin->_right;//删除节点,将其父亲节点连接RightMin的右孩子(因为RightMin是最左节点,所以它一定是父亲节点的左孩子,且一定没有左孩子)
else//RightMin是右孩子
RightMinParent->_right = RightMin->_right;//删除节点,将其父亲节点连接RightMin的右孩子(因为RightMin是最左节点,所以它一定是父亲节点的左孩子,且一定没有左孩子)
delete(RightMin);
}
return true;
}
}
return false;
}
private:
Node* _root;
//中序遍历(排序)(实现原理)
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << ":" << root->_value << endl;
_InOrder(root->_right);
}
};
}
//测试代码
void test1()//英汉翻译
{
key_value::BSTree<string, string> dict;
dict.Insert("string", "字符串");
dict.Insert("tree", "树");
dict.Insert("left", "左边、剩余");
dict.Insert("right", "右边");
dict.Insert("sort", "排序");
string str;
while (cin >> str)
{
key_value::BSTreeNode<string, string>* ret = dict.Find(str);
if (ret)
cout << ret->_value << endl;
else
cout << "无此单词,请重新输入" << endl;
}
}
void test2()//统计单词
{
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜","苹果", "香蕉", "苹果", "香蕉" };
key_value::BSTree<string, int> countTree;
for (auto e : arr)
{
auto ret = countTree.Find(e);
if (ret)
ret->_value++;
else
countTree.Insert(e, 1);
}
countTree.InOrder();
}
int main()
{
test1();
test2();
return 0;
}
四、二叉搜索树的查找的时间复杂度
二叉搜索树的时间复杂度通常为logN,最多查找高度次
但是当插入的数据接近有序时,查找效率会直线下降,所以才出现了AVL树和红黑树来解决这个问题