二叉树搜索树(上)
概念
二叉搜索树又称二叉排序树,它或者是一颗空树,或者是具有以下性质的二叉树:
• 若它的左子树不为空,则左子树上所有结点的值都⼩于等于根结点的值
• 若它的右子树不为空,则右子树上所有结点的值都大于等于根结点的值
• 它的左右子树也分别为二叉搜索树
• 二叉搜索树中可以支持插⼊相等的值,也可以不支持插⼊相等的值,具体看使⽤场景定义,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;}elsereturn 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;
}
那么,增删查改我们现在已经完成了增、删、查了,但是普通二叉树是不允许修改的。
本文到此结束。