1. 二叉搜索树的概念
二叉搜索树又叫二叉排序树,它或者是一颗空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上的所有结点都小于根节点上的值
- 若它的右子树不为空,则右子树上的所有结点都大于根节点上的值
- 它的左右子树也分别是二叉搜索树
例:int a [ ]={5,3,4,1,7,8,2,6,0,9}构成的二叉搜索树如下:
二叉搜索树的特点:中序遍历后会得到一个升序排列的数组。
2. 二叉搜索树的操作
2.1 查找 key
其中:右树的值 > 根节点的值 > 左树的值
若根节点不为空:
若根节点的值value==key 则返回当前节点
若根节点的值value > key 则在其左树继续查找
若根节点的值value < key 则在其右树继续查找
否则返回空值 nullptr
Node* Find(const K& value)//查找一个元素,时间复杂度为O(logN)
{
Node* cur = _root;
while (cur)
{
if (value > cur->_val)
cur = cur->_right;
else if (value < cur->_val)
cur = cur->_left;
else
return cur; //找到返回当前结点
}
return nullptr; //找不到返回空值
}
2.2 插入
若树为空则直接插入
若树不为空,则按二叉搜索树的性质确定插入位置,再进行插入
插入成功返回true,插入失败(插入相同的值)返回false
bool Insert(const K& value)//插入一个元素
{
if (_root == nullptr)//若树为空时,可直接插入
{
_root = new Node(value);
return true;
}
//树不空时,先确定位置再插入
Node* cur = _root;
Node* parent = nullptr;//记录cur的双亲节点,方便插入新节点
while (cur)
{
parent = cur;
if (val > cur->_data) //val大于当前节点的值就向右找
cur = cur->_right;
else if (val < cur->_data) //val 小于当前节点的值就向左找
cur = cur->_left;
else //val 等于当前节点的值,不允许插入
return false;
}
//找到对应位置,插入元素并与树连接
cur = new Node(value);
if (value > parent->_val)
parent->_right = cur;
else
parent->_left = cur;
return true;
}
2.3 删除
这里无法使用std::find确定位置,因为需要同时知道当前节点的双亲节点
我们应该首先查找元素是否在二叉搜索树中,若不存在则直接返回,若存在则删除。删除时会有以下几种情况:
1)要删除的结点是叶子节点或只有左孩子
则将被删除结点的双亲节点指向被删除节点的左孩子
2)要删除的结点只有右孩子
则将被删除结点的双亲节点指向被删除节点的右孩子
3)要删除的结点既有左孩子也有右孩子
则在被删除结点的左子树中找最右结点(或者在右子树中最左结点)与要删除的结点交换值,再删除最值结点。
bool Erase(const K& value)//删除一个元素
{
if (_root == nullptr)//如果树为空,删除失败
return false;
//查找元素在二叉搜索树中应该删除的位置
Node* cur = _root;
Node* parent = nullptr;//记录cur的双亲节点,方便插入新节点
while (cur)
{
if (value > cur->_val)//若是查找的元素大于cur的值,到cur的右树中找
{
parent = cur;
cur = cur->_right;
}
else if (value < cur->_val)
{
parent = cur;
cur = cur->_left;
}
else//已经找到,跳出循环
break;
}
if (nullptr == cur) return false; //找不到,无法删除
Node* del = cur;
if (cur->_right == nullptr) //1. 是叶子或只有左孩子
{
if (parent == nullptr) //要删节点是根节点,则直接返回
_root = cur->_left;
else{
if (parent->_left == cur) //如果cur是父亲节点的左孩子
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
}else if (cur->_left == nullptr) //2. 只有右孩子
{
if (parent == nullptr)
_root = _root->_right; //要删节点是根节点
else{
if (parent->_right == cur)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
}else //既有左孩子又有右孩子
{
Node* replace = cur->_right; //用右树最左进行替代
Node* pre = cur; //替代节点的双亲节点,关键!!
while (replace->_left) //找到右树最左结点
{
pre = replace;
replace = replace->_left;
}
cur->_data = replace->_data;
if (pre->_left==replace) //替代节点的双亲节点与它的下个结点连接
pre->_left = replace->_right;
else pre->_right = replace->_right;
del = replace;
}
delete del;
return true;
}
3.完整代码
在下篇博客:二叉搜索树的模拟实现中我分享了完整代码,敬请查看!
4.二叉树的性能分析
二叉搜索树的插入和删除都必须先查找,所以查找的效率也就是二叉树操作的性能。
对于n个节点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点再二叉搜索树的深度的函数,即节点越深,比较次数越多。
但对于同一关键码的集合,如果各关键码插入次序不同,可能得到不同结构的二叉树:最优的情况下,这棵树为完全二叉树;最差的情况下,这棵树为单支树。
例如,{3,4,5,6,7,8,9}这个集合
若它按{6,4,3,5,8,7,9}插入结点时,就能构成完全二叉树,使得查找效率最高
若它按{3,4,5,6,7,8,9}插入结点时,只能构成右单支,查找效率最低
那么,我们有没有什么办法,能保证无论按照什么次序插入关键码,二叉树的性能都是最佳的呢?
正好在下篇博客,我讲到的AVL(平衡)二叉树,就能很好的完成这件事情。