- 前言:本文会详细介绍二叉搜索树的概念,对二叉搜索树的操作进行了画图讲解,对二叉搜索树的两种模型以及应用作了介绍,最终对二叉搜索树的性能进行了分析.
🏝️1.二叉搜索树概念
在学习二叉搜索树之前,我们先温习一下二叉树的概念:所谓二叉树,其意义是:“任何节点最多只允许有两个子节点”,这两个子节点称为左子节点和右子节点.如果以递归方式来定义二叉树,我们可以说:“一个二叉树如果不为空,便是由一个根节点和左右两子树构成,左右两子树都可能为空”.
那么,由此我们引入二叉搜索树(又称二叉排序树),它要么是一棵空树,要么是一棵具有以下性质的二叉树:
- 若它的左子树不为空,那么它左子树任意一个节点的值都小于根结点的值
- 若它的右子树不为空,那么它右子树任意一个节点的值都大于根节点的值
- 它的左右子树分别为一棵二叉搜索树
比如,如图这样一棵树,就是一棵二叉搜索树:
它的左右子树分别也是二叉搜索树
仔细观察一下我们会发现,对于一棵二叉搜索树,它的中序遍历的结果恰好是有序的.所以对于二叉搜索树,它不仅具有搜索的功能,也是能够做到排序的.
🏠2.二叉搜索树的操作
⛰️2.1 二叉搜索树的查找
对于二叉搜索树,我们最先需要学习的操作就是二叉搜索树的查找,因为对于更复杂的插入和删除操作都是需要用到查找的思想的.
例如,对于这样一棵二叉搜索树,我们怎样去实现它的查找呢?
由上述二叉搜索树的性质,查找操作其实是比较容易的,对于我们要查找的值,从根结点开始,进行比较,当前要查找的值大于当前节点的值,就往当前节点的右子树走,若查找的值小于当前节点的值,就往当前节点的左子树走.,如果相等,就是找到了.
那么,如果走到了nullptr
还没有返回,那就是没有找到.
下面我们用代码实现一下:
//节点定义
template<class K>
struct BSTreeNode
{
BSTreeNode(const K& key = K())
: _key(key)
, _left(nullptr)
, _right(nullptr)
{}
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
};
//树的基本框架
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
protected:
Node* _root = nullptr;
};
//查找操作
bool find(const K& val)
{
Node* cur = _root;
while (cur)
{
//查找的值 < 当前节点的值 -> 向当前节点的左子树走
if (cur->_key > val)
{
cur = cur->_left;
}
else if (cur->_key < val) //查找的值 > 当前节点的值 -> 向当前节点的右子树走
{
cur = cur->_right;
}
else
{
//找到了
return true;
}
}
return false;
}
🏞️2.2 二叉搜索树的插入
要对一棵二叉搜索树进行插入,有许多种情况:
- 当这棵树是空树
当这棵树还是一棵空树时,也就是_root
为nullptr
,那我们只需要创建一个新的节点,与_root
进行链接就可以
- 这棵树不为空树
当这棵树不为空树时,我们就需要根据二叉搜索树的性质,从根结点开始,如果要插入的值大于当前节点的值,就往当前节点的右子树走,若要插入的值小于当前节点的值,就往当前节点的左子树走.直到指向当前节点的指针为nullptr
,就停止搜索.
注意:由于最终指向节点的指针会走到空,所以我们还需要定义一个
parent
指针来记录当前节点的父节点
最终我们只需要将新节点链接在搜索完成后的parent
指针的左边或者右边即可.
当最终插入节点时,我们可以将parent
指向的节点的值与需要插入的值
相比较,如果parent->_key
> 要插入的值
就链接在parent
的左边,反之链接在右边.
可以看到,我们在进行处理时,如果插入节点的值与搜索树中其中一个节点的值相同,我们会返回false
,这是因为,二叉搜索树一般是用来查找的,所以我们一般不插入重复值.
代码如下:
bool insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
Node* newnode = new Node(key);
while (cur)
{
if (cur->_key > key)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;
}
}
if (parent->_key > key)
{
parent->_left = newnode;
}
else
{
parent->_right = newnode;
}
return true;
}
为了观察我们插入后的结果是否正确,由于我们知道二叉搜索树的中序遍历是有序的.所以我们可以写一个中序遍历来观察结果是否为有序从而判断插入的正确性.
public:
//给用户的接口
void InOrder()
{
_InOrder(_root);
}
protected:
//内部实现递归的接口
void _InOrder(Node* root)
{
if (root)
{
_InOrder(root->_left);
std::cout << root->_key << " ";
_InOrder(root->_right);
}
}
🏜️2.3 二叉搜索树的删除
二叉搜索树的删除是这些操作中最为复杂的一个,因为它有多种情况.
首先,我们要删除这个节点,那第一步我们得先找到这个节点,然后再谈及对它的删除,如果这个节点不存在,返回false
,如果找到了这个节点,那我们删除时会有下面四种情况:
-
要删除的节点无孩子节点
-
要删除的节点只有左孩子节点
-
要删除的节点只有右孩子节点
-
要删除的节点的左孩子和右孩子节点都存在
但实际上,对于第一种情况,我们可以合并到第二种或第三种情况中,因为第一种情况时左右孩子都为nullptr
,所以可以当作第二种或者第三种来处理,所以最终总结起来就只有三种情况:
- 删除该节点且使被删除节点的父节点指向被删除节点的左孩子节点
- 删除该节点且使被删除节点的父节点指向被删除节点的右孩子节点
- 找到该节点的右子树中最小值的节点,将它的值与找到的需要删除的节点的值进行交换,然后转换为删除这个最小值节点.
我们先来看第2,3种情况:
接下来我们进行删除:
第三种情况与这个类似.
我们着重来讲解第四种情况:
代码如下:
bool erase(const K& val)
{
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (cur->_key < val)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > val)
{
parent = cur;
cur = cur->_left;
}
else
{
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = cur->_right;
}
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;
}
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
delete cur;
}
else
{
Node* minParent = cur;
Node* minRight = cur->_right;
while (minRight->_left)
{
minParent = minRight;
minRight = minRight->_left;
}
cur->_key = minParent->_key;
if (minRight == minParent->_left)
{
minParent->_left = minParent->_right;
}
else
{
minParent->_right = minParent->_right;
}
delete minRight;
minRight = nullptr;
}
return true;
}
}
return false;
}
if (cur == _root)
{
_root = cur->_right;
}
上面有这样几句代码,我来解释一下它是为了防止这种情况的发生:
这就是二叉搜索树的删除操作.
🏖️3. 二叉搜索树的应用
- K模型:K模型即只有
key
作为关键码,结构中只需要存储Key
即可,关键码即为需要搜索到的值。
比如:给一个单词word
,判断该单词是否拼写正确,具体方式如下:以单词集合中的每个单词作为key
,构建一棵二叉搜索树在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。 - KV模型:每一个关键码
key
,都有与之对应的值Value
,即<Key, Value>
的键值对。该种方式在现实生活中非常常见:比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>
就构成一种键值对;再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>
就构成一种键值对。
下面,我用KV
模型来写代码来举个例子:
//KV模型的节点定义
template<class K, class V>
struct BSTreeNode
{
BSTreeNode(const K& key = K(), const V& val = V())
: _key(key)
, _val(val)
, _left(nullptr)
, _right(nullptr)
{}
BSTreeNode<K,V>* _left;
BSTreeNode<K,V>* _right;
K _key;
V _val;
};
#include"BSTree.h"
using namespace std;
int main()
{
//KV模型的二叉搜索树,存两个值
stl::BSTree<string, string> dict;
dict.insert("钥匙", "key");
dict.insert("月亮", "moon");
dict.insert("团队", "team");
dict.insert("工人", "worker");
string _str;
while (cin >> _str)
{
//KV,模型中,由于我们可能需要修改value,所以返回节点的指针
auto ret = dict.find(_str);
if (ret != nullptr)
{
cout << ret->_val << endl;
}
else
{
cout << "查询不到此单词" << endl;
}
}
return 0;
}
运行这段代码就可以实现我们英汉词典的功能.
运行结果如下:
🗻4. 二叉搜索树的性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
最优情况下,二叉搜索树为像左边这样的完全二叉树,其平均比较次数为:
log
2
N
\log_2^N
log2N
最差情况下,二叉搜索树退化为像右边这样的单支树,其平均比较次数为: N 2 \frac{N}{2} 2N
所以二叉搜索树查找的时间复杂度也就是O(N)
本文对二叉搜索树的操作均为非递归实现,递归实现我会在下一篇文章中讲解.随后会附上链接.
KV
模型完整代码:
//这是文章中提到的KV模型的完整代码,文章中是以K模型
#include<iostream>
namespace stl
{
template<class K>
struct BSTreeNode
{
BSTreeNode(const K& key)
: _key(key)
, _left(nullptr)
, _right(nullptr)
{}
BSTreeNode<T>* _left;
BSTreeNode<T>* _right;
K _key;
};
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
Node* CopyTree(Node* root)
{
if(root == nullptr)
{
return nullptr;
}
Node* CopyNode = new Node(root->_key);
CopyNode->_left = CopyTree(root->_left);
CopyNode->_right = CopyTree(root->_right);
return CopyNode;
}
void Destroy(Node* root)
{
if(root != nullptr)
{
Destroy(root->_left);
Destroy(root->_right);
delete root;
}
}
public:
//因为提供了拷贝构造后,编译器不再生成默认构造
BSTree() = default;
BSTree(const BSTree<K>& bst)
{
_root = CopyTree(bst._root);
}
BSTree<K>& operator=(BSTree<K> bst)
{
std::swap(_root, bst._root);
return *this;
}
~BSTree()
{
Destroy(_root);
_root = nullptr;
}
bool insert(const K& val)
{
if(_root == nullptr)
{
_root = new Node(val);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
Node* newnode = new Node(val);
while(cur)
{
if(cur->_key > val)
{
parent = cur;
cur = cur->_left;
}
else if(cur->_key < val)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;
}
}
if(parent->_key > val)
{
parent->_left = newnode;
}
else
{
parent->_right = newnode;
}
return true;
}
void InOrder()
{
_InOrder(_root);
std::cout<<std::endl;
}
bool find(const K& val)
{
Node* cur = _root;
while(cur)
{
if(cur->_key > val)
{
cur = cur->_left;
}
else if(cur->_key < val)
{
cur = cur->_right;
}
else
{
return true;
}
return false;
}
}
bool erase(const K& val)
{
Node* cur = _root;
Node* parent = nullptr;
while(cur)
{
if(cur->_key < val)
{
parent = cur;
cur = cur->_right;
}
else if(cur->_key > val)
{
parent = cur;
cur = cur->_left;
}
else
{
if(cur->_left == nullptr)
{
if(cur == _root)
{
_root = cur->_right;
}
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;
}
if(cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
delete cur;
}
else
{
Node* minParent = cur;
Node* minRight = cur->_right;
while(minRight->_left)
{
minParent = minRight;
minRight = minRight->_left;
}
cur->_key = minParent->_key;
if(minRight == minParent->_left)
{
minParent->_left = minParent->_right;
}
else
{
minParent->_right = minParent->_right;
}
delete minRight;
minRight = nullptr;
}
return true;
}
}
return false;
}
//递归版本
//本文中还没有讲解递归的代码实现,但先将代码附上,大家可以先看一看
bool FindR(const K& val)
{
return _FindR(_root, val);
}
bool InsertR(const K& val)
{
return _InsertR(_root, val);
}
bool EraseR(const K& val)
{
return _EraseR(_root, val);
}
protected:
bool _FindR(Node* root, const K& val)
{
if(root == nullptr)
{
return false;
}
if(root->_key == val)
{
return true;
}
if(root->_key < val)
{
return _FindR(root->_right, val);
}
else if(root->_key > val)
{
return _FindR(root->_left, val);
}
}
bool _InsertR(Node*& root, const K& val)
{
if(root == nullptr)
{
root = new Node(val);
return true;
}
if(root->_key > val)
{
return _InsertR(root->_left, val);
}
else if(root->_key < val)
{
return _InsertR(root->_right, val);
}
else
{
return false;
}
}
bool _EraseR(Node*& root, const K& val)
{
if(root == nullptr)
{
return false;
}
if(root->_key > val)
{
return _EraseR(root->_left, val);
}
else if(root->_key < val)
{
return _EraseR(root->_right, val);
}
else
{
//记录要删除的节点
Node* del = root;
if(root->_left == nullptr)
{
root = root->_right;
}
else if(root->_right == nullptr)
{
root = root->_left;
}
else
{
Node* minRight = root->_right;
while(minRight->_left)
{
minRight = minRight->_left;
}
std::swap(root->_key, minRight->_key);
return _EraseR(root->_right, val);
}
delete del;
del = nullptr;
return true;
}
}
void _InOrder(Node* root)
{
if(root)
{
_InOrder(root->_left);
std::cout<<root->_key<<" ";
_InOrder(root->_right);
}
}
Node* _root = nullptr;
};
}