二叉搜索树
1、二叉搜索树的概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有结点的值都小于等于根结点的值
- 若它的右子树不为空,则右子树上所有结点的值都大于等于根结点的值
- 它的左右子树也分别为二叉搜索树
2、二叉搜索树的操作
int a = {8, 3, 1 ,10, 6, 4, 7, 14, 13}

2.1 二叉搜索树结点的创建
cpp
template<class K>
struct BSTNode
{
K _key;
BSTNode<K>* _left;
BSTNode<K>* _right;
BSTNode(const K& key)
:_key(key)
, _left(nullptr)
, _right(nullptr)
{
}
};
2.2 二叉搜索树的框架
cpp
template<class K>
class BSTree
{
typedef BSTNode<K> Node;
public:
private:
Node* _root = nullptr;
};
2.3 Insert(插入)
cpp
bool Insert(const K& key)
{
//如果根节点是空指针,那么就把新节点直接赋值给根节点_root
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
//如果不为空查找
//定义父节点指针,作用是在遍历时记住cur上一层,最后新节点要挂在parent的左或右
Node* parent = nullptr;
//从根节点开始遍历整棵树
Node* cur = _root;
while (cur) // cur不为空,说明当前还有节点可以比较
{
// 情况1:当前节点键值 < 要插入的key
if (cur->_key < key)
{
parent = cur; // 把当前节点记为候选父节点
cur = cur->_right; // key更大,去右子树继续找
}
// 情况2:当前节点键值 > 要插入的key
else if (cur->_key > key)
{
parent = cur; // 记为父节点
cur = cur->_left; // key更小,去左子树继续找
}
// 情况3:相等,树中已有相同key
else
{
return false; // 插入失败,重复元素不允许
}
}
//插入节点,和父节点连接
cur = new Node(key);
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
2.4 Find(查找)
cpp
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;
}
else
{
return true;
}
}
return false;
}
从根开始查找,比根大则去右边查找,比根小则去左边查找,最多查找次数是树的高度
2.5 InOrder(中序遍历)
cpp
public:
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
return;
//递归访问左子树
_InOrder(root->_left);
//打印当前节点(访问根)
cout << root->_key << " ";
//递归访问右子树
_InOrder(root->_right);
}
这里的中序遍历是用递归的方式来实现的,如果想中序遍历二叉树,就要把根结点_root传给这个函数,但根结点_root是私有成员变量,用户访问不到。但类里面可以访问私有成员,所以可以在类里面可以实现一个中序遍历的子函数_InOrder,在子函数中实现中序遍历的逻辑,然后在提供一个中序遍历的函数接口InOrder,由它调用_InOrder。这样用户就可以使用中序遍历
2.6 Erase(删除)
删除大体分四种情况:
- 要删除的节点N没有子节点
- 要删除的节点N只有右孩子
- 要删除的节点N只有左孩子
- 要删除的节点N既有左孩子也有右孩子
解决方案:
情况1:把N节点的父亲对应孩子指针指向空,直接删除N结点(情况1可以当成2或者3处理,效果是一样的)
情况2:把N节点的父亲对应孩子指针指向N的右孩子,直接删除N节点
情况3:把N节点的父亲对应孩子指针指向N的左孩子,直接删除N节点
情况4:用替代法删除,删除一个节点,可以找另一个节点继承当前的位置,找的节点一定要满足一个条件:左子树的最大节点或右子树的最小节点
cpp
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:cur左孩子为空,覆盖两种场景
//场景1:无任何子节点 场景2:只有右孩子
if (cur->_left == nullptr)
{
if (cur != _root)
{
//判断cur是父亲节点的左孩子
if (cur == parent->_left)
{
// 左为空,父亲节点左指针直接指向cur的右子树
// 场景1叶子:cur->_right是nullptr,等价父左指针置空,断开叶子连接
// 场景2只有右孩子:父节点直接接上cur的右子树
parent->_left = cur->_right;
}
//cur是父节点的右孩子
else
{
//父节点右指针直接指向cur的右子树
parent->_right = cur->_right;
}
}
// cur是整棵树唯一根节点,没有父节点
else
{
// 根节点更新为cur的右孩子
// 场景1单根叶子:cur->_right=nullptr,整棵树直接置空
//场景2只有右孩子:直接让右孩子作为根节点
_root = cur->_right;
}
// 释放待删除节点内存,防止内存泄漏
delete cur;
}
//分支2:cur右孩子为空
//场景3:只有只有左孩子
else if (cur->_right == nullptr)
{
//判断:待删节点cur不是根,有父节点
if (cur != _root)
{
// cur是父的左孩子
if (cur == parent->_left)
{
//父节点左指针直接接上cur的左子树,跳过待删节点
parent->_left = cur->_left;
}
//cur是父的右孩子
else
{
//父右指针直接接上cur的左子树
parent->_right = cur->_left;
}
}
//cur是根节点,只有左子树
else
{
//树根直接更新为cur的左孩子
_root = cur->_left;
}
//释放待删节点内存
delete cur;
}
//分支3:cur左右孩子都不为空
//场景4:既有左孩子也有右孩子
else
{
//思路:不能直接删cur,交换cur和右子树最小值节点的key,再删除最小值节点
Node* minRightParent = cur; //记录右子树最小值节点的父节点,初始为cur
//这里minRightParent不能设置为空,因为minRight可能为空
Node* minRight = cur->_right; //进入cur的右子树,寻找最小值
//循环一路向左,找到右子树最左节点
while (minRight->_left)
{
minRightParent = minRight; //更新最小值节点的父
minRight = minRight->_left; //持续向左遍历
}
//交换两个节点
swap(cur->_key, minRight->_key);
//删除最小值节点minRight
if (minRight == minRightParent->_left)
//最小值是父的左孩子,父左指针接上最小值的右子树
minRightParent->_left = minRight->_right;
else
//最小值是父的右孩子,父右指针接上最小值的右子树
minRightParent->_right = minRight->_right;
delete minRight;
}
return true;
}
}
return false;
}
3、总代码
cpp
#pragma once
template<class K>
struct BSTNode
{
K _key;
BSTNode<K>* _left;
BSTNode<K>* _right;
BSTNode(const K& key)
:_key(key)
, _left(nullptr)
, _right(nullptr)
{
}
};
// key
template<class K>
class BSTree
{
typedef BSTNode<K> Node;
public:
bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
//如果不为空查找
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
{
return false;
}
}
cur = new Node(key);
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
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;
}
else
{
return true;
}
}
return false;
}
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
{
// 准备删除
if (cur->_left == nullptr)
{
if (cur != _root)
{
// 左为空,父亲指向我的右
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
else
{
_root = cur->_right;
}
delete cur;
}
else if (cur->_right == nullptr)
{
if (cur != _root)
{
// 右为空,父亲指向我的左
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
else
{
_root = cur->_left;
}
delete cur;
}
else
{
// 左右都不为空,找子树中适合的节点替代我
Node* minRightParent = cur;
Node* minRight = cur->_right;
while (minRight->_left)
{
minRightParent = minRight;
minRight = minRight->_left;
}
swap(cur->_key, minRight->_key);
if (minRight == minRightParent->_left)
minRightParent->_left = minRight->_right;
else
minRightParent->_right = minRight->_right;
delete minRight;
}
return true;
}
}
return false;
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
private:
Node* _root = nullptr;
};
4、两种模型
4.1 K模型
只有key作为关键码,结构中只需要存储key即可,关键码即为需要搜索到的值。例如:
- 小区无人值守车库,车辆进入时扫描车牌在不在系统中,在则抬杆,不在则提示非本小区车辆,无法进入
- 检查⼀篇英文章单词拼写是否正确,将词库中所有单词放入二叉搜索树,读取文章中的单词,查找是否在二叉搜索树中,不在则波浪线标红提示
4.2 key/value搜索场景
每一个关键码key,都有与之对应的值value,value可以任意类型对象。即<key,value>的键相对。例如:
- 简单中英互译字典,树的结构中(结点)存储key(英文)和vlaue(中文),搜索时输入英文,则同时查找到了英文对应的中文
- 商场无人值守车库,入口进场时扫描车牌,记录车牌和入场时间,出口离场时,扫描车牌,查找入场时间,用当前时间-入场时间计算出停车时长,计算出停车费用
- 统计一篇文章中单词出现的次数,读取⼀个单词,查找单词是否存在,不存在这个说明第⼀次出现,(单词,1),单词存在,则++单词对应的次数。
4.2.1 key/value二叉搜索树代码实现
将节点改成<key,value>类型,即键值对模型
cpp
template<class K, class V>
struct BSTNode
{
K _key;
V _value;
// pair<K, V> _kv;
BSTNode<K, V>* _left;
BSTNode<K, V>* _right;
BSTNode(const K& key, const V& value)
:_key(key)
, _value(value)
, _left(nullptr)
, _right(nullptr)
{ }
};
二叉搜索树类模板
cpp
template<class K, class V>
class BSTree
{
typedef BSTNode<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->_right;
}
else if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(key, value);
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
//查找
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_key < key)
{
cur = cur->_right;
}
else if (cur->_key > key)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
}
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
{
// 准备删除
if (cur->_left == nullptr)
{
if (cur != _root)
{
// 左为空,父亲指向我的右
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
else
{
_root = cur->_right;
}
delete cur;
}
else if (cur->_right == nullptr)
{
if (cur != _root)
{
// 右为空,父亲指向我的左
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
else
{
_root = cur->_left;
}
delete cur;
}
else
{
// 左右都不为空,找子树中适合的节点替代我
Node* minRightParent = cur;
Node* minRight = cur->_right;
while (minRight->_left)
{
minRightParent = minRight;
minRight = minRight->_left;
}
swap(cur->_key, minRight->_key);
if (minRight == minRightParent->_left)
minRightParent->_left = minRight->_right;
else
minRightParent->_right = minRight->_right;
delete minRight;
}
return true;
}
}
return false;
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << ":" << root->_value << endl;
_InOrder(root->_right);
}
private:
Node* _root = nullptr;
};
}
int main()
{
string arr[] = { "苹果", "西瓜", "苹果","苹果","苹果","苹果", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
key_value::BSTree<string, int> t;
for (auto& str : arr)
{
auto pNode = t.Find(str);
if (pNode == nullptr)
{
t.Insert(str, 1);
}
else
{
pNode->_value++;
}
}
t.InOrder();
return 0;
}
输出结果:
香蕉:2
苹果:9
西瓜:2
5、二叉搜索树的性能分析

最优情况下:二叉搜索树为完全二叉树(或者接近完全二叉树),其高度为:㏒₂N
最差情况下:二叉搜索树退化为单支树(或者类似单支),其高度为: N
所以综合而言二叉搜索树增删查改时间复杂度为: O(N)