二叉树搜索树(上)
二叉树搜索树(上)
概念
二叉搜索树又称二叉排序树,它或者是一颗空树,或者是具有以下性质的二叉树:
• 若它的左子树不为空,则左子树上所有结点的值都⼩于等于根结点的值
• 若它的右子树不为空,则右子树上所有结点的值都大于等于根结点的值
• 它的左右子树也分别为二叉搜索树
• 二叉搜索树中可以支持插⼊相等的值,也可以不支持插⼊相等的值,具体看使⽤场景定义,map/set/multimap/multiset系列容器底层就是二叉搜索树,其中map/set不支持插⼊相等值,multimap/multiset支持插⼊相等值
二叉搜索树的性能分析
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其⾼度为: O ( l o g 2 N ) O(log_2N) O(log2N)
最差情况下,二叉搜索树退化为单支树(或者类似单支),其⾼度为: O ( N 2 ) O(\frac{N}{2}) O(2N)
所以综合而言二叉搜索树增删查改时间复杂度为: O(N)
那么这样的效率显然是无法满⾜我们需求的,二叉搜索树的变形:平衡二叉搜索树AVL树和红⿊树,才能适用于我们在内存中存储和搜索数据。
另外需要说明的是,二分查找也可以实现 O(logN) 级别的查找效率,但是二分查找有两⼤缺陷:
- 需要存储在支持下标随机访问的结构中,并且有序。
- 插⼊和删除数据效率很低,因为存储在下标随机访问的结构中,插⼊和删除数据⼀般需要挪动数据。
这⾥也就体现出了平衡二叉搜索树的价值。
二叉树的插入
插⼊的具体过程如下:
- 树为空,则直接新增结点,赋值给root指针
- 树不空,按二叉搜索树性质,插⼊值⽐当前结点大往右走,插⼊值⽐当前结点⼩往左⾛,找到空位置,插⼊新结点。
- 如果支持插⼊相等的值,插⼊值跟当前结点相等的值可以往右⾛,也可以往左⾛,找到空位置,插⼊新结点。(要注意的是要保持逻辑⼀致性,插⼊相等的值不要⼀会往右走,⼀会往左走)
参考代码:
template<class K>
class BSTNode
{
public:
BSTNode(const K& key)
:_key(key)
,_left(nullptr)
,_right(nullptr)
{}
private:
K _key;
BTSNode<K>* _left;
BTSNode<K>* _right;
};
template<class K>
class BSTree
{
using Node = BSTNode<k>;//用using替代typedef的作用
public:
bool Insert(const K& key)
{
//如果是空树
if (_root == nullptr)
{
_root = new Node(key);
return true;//这时就结束了
}
//如果不是空树
Node* parent = nullptr;
Node* cur = _root;
while (cur)//当cur不为空
{
if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else
return false;//避免冗余
}
//到这里说明cur已经找到空位置了,但是不知道是从右走的还是往左走的,得再比一次
cur = new Node(key);
if (key < parent->_key)
{
parent->left = cur;
}
else
{
parent->right = cur;
}
return true;
}
private:
Node* _root = nullptr;//是结点指针类型,不是结点类型
};
如果允许已存在的值插入(或者说运行冗余):
我们会发现,如果走中序遍历(中序遍历顺序是先遍历左子树,然后访问根节点,最后遍历右子树),就是有序的。即使允许冗余的情况下,中序遍历也是有序的。
中序遍历代码参考:
template<class K>
class BSTree
{
//……
private:
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key <<" ";
_InOrder(root->_right);
}
Node* _root = nullptr;//是结点指针类型,不是结点类型
};
可以发现一个问题,我们需要传入根节点才能进行中序遍历,但是根节点在类中贝private修饰无法访问。
一种方法是提供GetRoot接口,但还有别的方法:
在C++的类中要写递归时,公开的方法都要套一层。
public:
void InOrder()//类外面拿不到_root,但是类里面是可以的
{
_InOrder(_root);
}
类里面二叉树的递归基本都需要像这样套一层。
二叉树搜索树的查找
- 从根开始⽐较,查找x,x⽐根的值⼤则往右边⾛查找,x⽐根值⼩则往左边⾛查找。
- 最多查找⾼度次,⾛到到空,还没找到,这个值不存在。
- 如果不⽀持插⼊相等的值,找到x即可返回
- 如果⽀持插⼊相等的值,意味着有多个x存在,⼀般要求查找中序的第⼀个x。如下图,查找3,要找到1的右孩⼦的那个3返回
参考代码:
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;
}
二叉搜索树的删除
⾸先查找元素是否在二叉搜索树中,如果不存在,则返回false。
如果查找元素存在则分以下四种情况分别处理:(假设要删除的结点为N)
- 要删除结点N左右孩⼦均为空
- 要删除的结点N左孩⼦位空,右孩⼦结点不为空
- 要删除的结点N右孩⼦位空,左孩⼦结点不为空
- 要删除的结点N左右孩⼦结点均不为空
对应以上四种情况的解决⽅案:
-
把N结点的⽗亲对应孩⼦指针指向空,直接删除N结点(情况1可以当成2或者3处理,效果是⼀样的)
-
把N结点的⽗亲对应孩⼦指针指向N的右孩⼦,直接删除N结点
-
把N结点的⽗亲对应孩⼦指针指向N的左孩⼦,直接删除N结点
-
⽆法直接删除N结点,因为N的两个孩⼦⽆处安放,只能⽤替换法删除。
找N左⼦树的值最⼤结点R(最右结点)或者N右⼦树的值最⼩结点R(最左结点)替代N,因为这两个结点中任意⼀个,放到N的位置,都满⾜⼆叉搜索树的规则。
替代N的意思就是N和R的两个结点的值交换,转⽽变成删除R结点,R结点符合情况2或情况3,可以直接删除。
参考代码(比较多要注意的细节,写在注释里了):
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
{
//找到了要删除的数据,接下来进行删除,但是要判断情况
//cur的左孩子为空(左右孩子都为空也被包含在内了),然后把cur的右孩子给给父(父可能为空)
if (cur->_left == nullptr)
{
//父为空,也就是说要删的是头结点
if (parent==nullptr)
{
_root = cur->_right;
}
else
{
//要判断父节点的左还是右指针指向cur的孩子
if(parent->_left==cur)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
return true;//别忘了
}
//cur的右孩子为空,把cur的左孩子给父
else if (cur->_right == nullptr)
{
if (parent == nullptr)
{
_root = cur->_left;
}
else
{
//判断父节点的左还是右指针指向cur孩子
if (parent->_left == cur)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
delete cur;
return true;//别忘了
}
else//现在是cur的左右孩子都不为空
{
//第一步,找R(这里采用cur的右子树的最小结点/也就是最左结点)
Node* p = cur;//这里如果初始给空,后面如果不进循环,会造成对空指针的解引用
Node* r = cur->_right;
while (r->_left)
{
p = r;
r = r->_left;
}
//第二步,进行交换(但不用真的把cur的值再去给r)
cur->_key = r->_key;
//第三步,删除R——很容易考虑不全面!!删除R又要把删除的问题考虑全面,但这里不能用递归,会找不到
//在删除R时要把R的右孩子(只可能是右孩子)给给父,但是父的左还是右指针不确定
if (p->_right == r)
{
p->_right = r->_right;
}
else
{
p->_left = r->_right;
}
delete r;
return true;
}
}
}
return false;
}
那么,增删查改我们现在已经完成了增、删、查了,但是普通二叉树是不允许修改的。
本文到此结束。