目录
- 1. 二叉搜索树的概念
- 2. 二叉搜索树的性能分析
- 3. 二叉搜索树的插入
- 4. 二叉搜索树的查找
- 5. 二叉搜索树的删除
- 6. 二叉搜索树的实现代码
- 7. 二叉搜索树 key 和 key/value 使用场景
- 7.1 key 搜索场景
- 7.2 key/value 搜索场景
- 7.3 key/value 二叉搜索树代码实现
1. 二叉搜索树的概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有结点的值都小于等于根结点的值。
- 若它的右子树不为空,则右子树上所有结点的值都大于等于根结点的值。
- 它的左右子树也分别为二叉搜索树。
- 二叉搜索树中可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义。后续学习的
map、set、multimap、multiset系列容器底层就是二叉搜索树,其中map/set不支持插入相等值,multimap/multiset支持插入相等值。
2. 二叉搜索树的性能分析
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其高度为:log_2(N)。
最差情况下,二叉搜索树退化为单支树(或者类似单支),其高度为:N NN。
所以综合而言,二叉搜索树增删查改时间复杂度为:O ( N ) O(N)O(N)。
那么这样的效率显然是无法满足我们需求的,我们后续课程需要继续讲解二叉搜索树的变形,平衡二叉搜索树 AVL 树和红黑树,才能适用于我们在内存中存储和搜索数据。
另外需要说明的是,二分查找也可以实现O ( log 2 N ) O(\log_2 N)O(log2N)级别的查找效率,但是二分查找有两大缺陷:
- 需要存储在支持下标随机访问的结构中,并且有序。
- 插入和删除数据效率很低,因为存储在下标随机访问的结构中,插入和删除数据一般需要挪动数据。
这里也就体现出了平衡二叉搜索树的价值。
3. 二叉搜索树的插入
插入的具体过程如下:
- 树为空,则直接新增结点,赋值给
root指针。 - 树不空,按二叉搜索树性质,插入值比当前结点大往右走,插入值比当前结点小往左走,找到空位置,插入新结点。
- 如果支持插入相等的值,插入值跟当前结点相等的值可以往右走,也可以往左走,找到空位置,插入新结点。(要注意的是要保持逻辑一致性,插入相等的值不要一会往右走,一会往左走。)
inta[]={8,3,1,10,6,4,7,14,13};4. 二叉搜索树的查找
- 从根开始比较,查找
x。x比根的值大则往右边走查找,x比根值小则往左边走查找。 - 最多查找高度次,走到空还没找到,这个值不存在。
- 如果不支持插入相等的值,找到
x即可返回。 - 如果支持插入相等的值,意味着有多个
x存在,一般要求查找中序的第一个x。如下图,查找3,要找到1的右孩子的那个3返回。
5. 二叉搜索树的删除
首先查找元素是否在二叉搜索树中,如果不存在,则返回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,可以直接删除。
6. 二叉搜索树的实现代码
template<classK>structBSTNode{K _key;BSTNode<K>*_left;BSTNode<K>*_right;BSTNode(constK&key):_key(key),_left(nullptr),_right(nullptr){}};// Binary Search Treetemplate<classK>classBSTree{typedefBSTNode<K>Node;public:boolInsert(constK&key){if(_root==nullptr){_root=newNode(key);returntrue;}Node*parent=nullptr;Node*cur=_root;while(cur){if(cur->_key<key){parent=cur;cur=cur->_right;}elseif(cur->_key>key){parent=cur;cur=cur->_left;}else{returnfalse;}}cur=newNode(key);if(parent->_key<key){parent->_right=cur;}else{parent->_left=cur;}returntrue;}boolFind(constK&key){Node*cur=_root;while(cur){if(cur->_key<key){cur=cur->_right;}elseif(cur->_key>key){cur=cur->_left;}else{returntrue;}}returnfalse;}boolErase(constK&key){Node*parent=nullptr;Node*cur=_root;while(cur){if(cur->_key<key){parent=cur;cur=cur->_right;}elseif(cur->_key>key){parent=cur;cur=cur->_left;}else{// 0-1个孩子的情况// 删除情况1、2、3均可以直接删除,改变父亲对应孩子指针指向即可if(cur->_left==nullptr){if(parent==nullptr){_root=cur->_right;}else{if(parent->_left==cur)parent->_left=cur->_right;elseparent->_right=cur->_right;}deletecur;returntrue;}elseif(cur->_right==nullptr){if(parent==nullptr){_root=cur->_left;}else{if(parent->_left==cur)parent->_left=cur->_left;elseparent->_right=cur->_left;}deletecur;returntrue;}else{// 2个孩子的情况// 删除情况4,替换法删除// 假设这里我们取右子树的最小结点作为替代结点去删除// 这里尤其要注意右子树的根就是最小结点时的处理,对应课件图中删除8的情况// 一定要把cur给rightMinP,否则会报错。Node*rightMinP=cur;Node*rightMin=cur->_right;while(rightMin->_left){rightMinP=rightMin;rightMin=rightMin->_left;}cur->_key=rightMin->_key;if(rightMinP->_left==rightMin)rightMinP->_left=rightMin->_right;elserightMinP->_right=rightMin->_right;deleterightMin;returntrue;}}}returnfalse;}voidInOrder(){_InOrder(_root);cout<<endl;}private:void_InOrder(Node*root){if(root==nullptr){return;}_InOrder(root->_left);cout<<root->_key<<" ";_InOrder(root->_right);}private:Node*_root=nullptr;};7. 二叉搜索树 key 和 key/value 使用场景
7.1 key 搜索场景
只有key作为关键码,结构中只需要存储key即可,关键码即为需要搜索到的值,搜索场景只需要判断key在不在。key的搜索场景实现的二叉树搜索树支持增删查,但是不支持修改,修改key破坏搜索树结构了。
场景 1:小区无人值守车库,小区车库买了车位的业主车才能进小区,那么物业会把买了车位的业主的车牌号录入后台系统,车辆进入时扫描车牌在不在系统中,在则抬杆,不在则提示非本小区车辆,无法进入。
场景 2:检查一篇英文文章单词拼写是否正确,将词库中所有单词放入二叉搜索树,读取文章中的单词,查找是否在二叉搜索树中,不在则波浪线标红提示。
7.2 key/value 搜索场景
每一个关键码key,都有与之对应的值value,value可以任意类型对象。树的结构中(结点)除了需要存储key还要存储对应的value,增/删/查还是以key为关键字走二叉搜索树的规则进行比较,可以快速查找到key对应的value。key/value的搜索场景实现的二叉树搜索树支持修改,但是不支持修改key,修改key破坏搜索树性质了,可以修改value。
场景 1:简单中英互译字典,树的结构中(结点)存储key(英文)和value(中文),搜索时输入英文,则同时查找到了英文对应的中文。
场景 2:商场无人值守车库,入口进场时扫描车牌,记录车牌和入场时间,出口离场时,扫描车牌,查找入场时间,用当前时间 - 入场时间计算出停车时长,计算出停车费用,缴费后抬杆,车辆离场。
场景 3:统计一篇文章中单词出现的次数,读取一个单词,查找单词是否存在,不存在这个说明第一次出现,(单词,1),单词存在,则++单词对应的次数。
7.3 key/value 二叉搜索树代码实现
template<classK,classV>structBSTNode{// pair<K, V> _kv;K _key;V _value;BSTNode<K,V>*_left;BSTNode<K,V>*_right;BSTNode(constK&key,constV&value):_key(key),_value(value),_left(nullptr),_right(nullptr){}};template<classK,classV>classBSTree{typedefBSTNode<K,V>Node;public:BSTree()=default;BSTree(constBSTree<K,V>&t){_root=Copy(t._root);}BSTree<K,V>&operator=(BSTree<K,V>t){swap(_root,t._root);return*this;}~BSTree(){Destroy(_root);_root=nullptr;}boolInsert(constK&key,constV&value){if(_root==nullptr){_root=newNode(key,value);returntrue;}Node*parent=nullptr;Node*cur=_root;while(cur){if(cur->_key<key){parent=cur;cur=cur->_right;}elseif(cur->_key>key){parent=cur;cur=cur->_left;}else{returnfalse;}}cur=newNode(key,value);if(parent->_key<key){parent->_right=cur;}else{parent->_left=cur;}returntrue;}Node*Find(constK&key){Node*cur=_root;while(cur){if(cur->_key<key){cur=cur->_right;}elseif(cur->_key>key){cur=cur->_left;}else{returncur;}}returnnullptr;}boolErase(constK&key){Node*parent=nullptr;Node*cur=_root;while(cur){if(cur->_key<key){parent=cur;cur=cur->_right;}elseif(cur->_key>key){parent=cur;cur=cur->_left;}else{if(cur->_left==nullptr){if(parent==nullptr){_root=cur->_right;}else{if(parent->_left==cur)parent->_left=cur->_right;elseparent->_right=cur->_right;}deletecur;returntrue;}elseif(cur->_right==nullptr){if(parent==nullptr){_root=cur->_left;}else{if(parent->_left==cur)parent->_left=cur->_left;elseparent->_right=cur->_left;}deletecur;returntrue;}else{Node*rightMinP=cur;Node*rightMin=cur->_right;while(rightMin->_left){rightMinP=rightMin;rightMin=rightMin->_left;}cur->_key=rightMin->_key;if(rightMinP->_left==rightMin)rightMinP->_left=rightMin->_right;elserightMinP->_right=rightMin->_right;deleterightMin;returntrue;}}}returnfalse;}voidInOrder(){_InOrder(_root);cout<<endl;}private:void_InOrder(Node*root){if(root==nullptr){return;}_InOrder(root->_left);cout<<root->_key<<":"<<root->_value<<endl;_InOrder(root->_right);}voidDestroy(Node*root){if(root==nullptr)return;Destroy(root->_left);Destroy(root->_right);deleteroot;}Node*Copy(Node*root){if(root==nullptr)returnnullptr;Node*newRoot=newNode(root->_key,root->_value);newRoot->_left=Copy(root->_left);newRoot->_right=Copy(root->_right);returnnewRoot;}private:Node*_root=nullptr;};字典示例:
intmain(){BSTree<string,string>dict;//BSTree<string, string> copy = dict;dict.Insert("left","左边");dict.Insert("right","右边");dict.Insert("insert","插入");dict.Insert("string","字符串");string str;while(cin>>str){autoret=dict.Find(str);if(ret){cout<<"->"<<ret->_value<<endl;}else{cout<<"无此单词,请重新输入"<<endl;}}return0;}统计单词出现次数示例:
intmain(){string arr[]={"苹果","西瓜","苹果","西瓜","苹果","苹果","西瓜","苹果","香蕉","苹果","香蕉"};BSTree<string,int>countTree;for(constauto&str:arr){// 先查找水果在不在搜索树中// 1、不在,说明水果第一次出现,则插入<水果,1>// 2、在,则查找到的结点中水果对应的次数++//BSTreeNode<string, int>* ret = countTree.Find(str);autoret=countTree.Find(str);if(ret==NULL){countTree.Insert(str,1);}else{ret->_value++;}}countTree.InOrder();return0;}