1.二叉搜索树简介
什么是二叉搜索树?顾名思义,二叉搜索树是一颗二叉树,但是它却有搜索的功能,它是如何做到的呢?二叉搜索树的条件如下:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
- 二叉搜索树可以是一棵空树
二叉搜索树如下图所示:
二叉搜索树其实还有一个隐藏功能:二叉搜索树走一个中序遍历,你会发现,遍历出来的数据是有序的。
二叉搜索树的查找效率非常高,最多只需要查找高度次。
2.二叉搜索树的实现
对于二叉搜索树的实现,无非就是实现二叉搜索树的增、删、查、改;但是如果在二叉搜索树中修改一个值,很有可能会破坏二叉搜索树的结构,导致这棵二叉搜索树不再是二叉搜索树。所以我们不能修改二叉搜索树中的值。
2.1二叉搜索树节点的定义
二叉搜索树的结点中,我们需要定义左孩子和右孩子指针,分别指向左孩子和右孩子;当然还需要一个存储数据的_data;
template<class K>
struct BStreeNode
{
typedef BStreeNode<K> Node;
Node* _left;
Node* _right;
K _data;
BStreeNode(const K& val)
:_left(nullptr)
,_right(nullptr)
,_data(val)
{}
};
2.2二叉搜索树的查找
二叉搜索树的查找功能,根据二叉搜索树的性质实现,需要查找的值,比当前结点大,就去右边查找,比当前结点小,就去左边查找;找到了就返回true,如果找到空了,说明没找到,返回false。
bool Find(const K& val)
{
Node* temp = _root;
while (temp)
{
if (val > temp->_data)
temp = temp->_right;
else if (val < temp->_data)
temp = temp->_left;
else
return true;
}
return false;
}
2.3二叉搜索树的插入
插入一个结点的时候,肯定要将该节点链接到它的父节点上去,所以查找插入位置的时候,需要记录其父节点,如果找到相等的说明不能插入了,返回false;如果找到空,说明找到插入位置了,但是不知道连接到父节点的那一边?通过判断 插入的值和父节点的值的大小决定链接到父节点的左边还是右边。
bool Insert(const K& val)
{
if (_root == nullptr)
{
_root = new Node(val);
return true;
}
Node* temp = _root;
Node* parent = nullptr;
while (temp)
{
if (val > temp->_data)
{
parent = temp;
temp = parent->_right;
}
else if (val < temp->_data)
{
parent = temp;
temp = parent->_left;
}
else
{
return false;
}
}
temp = new Node(val);
if (val < parent->_data)
{
parent->_left = temp;
}
else if (val > parent->_data)
{
parent->_right = temp;
}
return true;
}
2.4二叉搜索树的删除
二叉搜索树的删除其实就是删除节点,删除的结点可以划分为一下几种情况:
- 该节点没有孩子
- 该节点有一个孩子
- 该节点有两个孩子
如下图所示:
如何实现二叉搜索树的删除呢?当删除的是叶子节点的时候,挺好的,直接删除即可,但是,如果删除的是有孩子的结点呢?这个时候就需要用到替换法删除。替换法删除就是找一个能替换当前结点的值,交换值,转换删除这个被替换的结点。
既然要替换,那我们用哪一个结点替换这个要删除的节点呢?选择的结点替换之后,不能改变当前二叉搜索树的结构,我们有以下两种选择:
- 左子树的最大结点;左子树的最大结点其实就是左子树的最右边的结点,这个结点有个特点就是,右孩子为空,所以我们可以利用这个特性找这个结点。
- 右子树的最小节点;右子树的最小结点其实就是右子树的最左边的结点,这个结点有个特点就是,左孩子为空,所以我们可以利用这个特性找这个结点。
代码实现如下:
bool Erase(const K& val)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (val > cur->_data)
{
parent = cur;
cur = cur->_right;
}
else if (val < cur->_data)
{
parent = cur;
cur = cur->_left;
}
else//相等 要删除了
{
if (cur->_left == nullptr)//左边为空
{
if (cur == _root)
{
_root = cur->_right;
}
else
{
if (cur == parent->_left)
{
//如果cur是根结点的话,没有parent
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
return true;
}
else if (cur->_right == nullptr) //右边为空
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
//如果cur是根结点的话,没有parent
if (cur == parent->_left)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
delete cur;
return true;
}
else // 左右都不为空 替换法删除
{
Node* r_min_parent = cur;
Node* r_min = cur->_right;
while (r_min->_left) //当该循环停下来的时候说明找到了右边的最小结点
{
r_min_parent = r_min;
r_min = r_min->_left;
}
cur->_data = r_min->_data;
//交换完值之后,要删除的结点变成了r_min
if (r_min == r_min_parent->_left) //当删除的是根结点的时候也适用
r_min_parent->_left = r_min->_right;
else
r_min_parent->_right = r_min->_right;
delete r_min;
return true;
}
}
}
return false;
}
3.二叉搜索树的应用
二叉搜索树是一种用来搜索的结构;搜索模型有两种,一种是Key搜索模型,一种是Key-Value搜索模型;
- Key搜索模型主要用来 快速查找一个值在不在。如:门禁系统。
- Key-Value搜索模型主要用来 通过一个值查找另一个值。如:字典查询。
4.性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:O(log_2 N)。
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:O(N)。
为了避免二叉搜索树退化成单支树,可以通过一些限制条件限制二叉搜索树的结构,比较有名的有AVL树 和 红黑树。