二叉搜索树(BST)-BINARY SEARCH TREE

DHWJHY

2024-04-12 17:44:58

Technology

## 前言-INTRODUCTION 我们曾经听说过很多 $\texttt{逆天、优雅}$ 的**平衡树**数据结构,包括 $\texttt{splay}$、$\texttt{treap}$、**红黑树** 等,然而,他们共同的基础是**二叉搜索树($\texttt{Binary Search Tree-BST}$)**,于是,我们先从 $\texttt{BST}$ 开始讲起…… ## 一、$\texttt{BST}$的定义-THE DIFNITION OF $\texttt{BST}$ 给定一棵二叉树,树上的每个节点带有一个数值,即权值或关键码。其 $\texttt{BST}$ 性质指,对于任意一个节点: 1. 该节点的权值严格大于它的左子树中任意节点的权值。 2. 该节点的权值严格小于它的右子树中任意节点的权值。 如此的二叉树就是一棵**二叉搜索树($\texttt{BST}$)**。 当然,它还有一个小小的性质是,其中序遍历为从小到大的排序。 ## 二、$\texttt{BST}$ 的实现与维护-THE PRACTICE AND MAINTENANCE OF $\texttt{BST}$ 本人因为比较懒,所以这里直接引用博客,感谢 Brilliant11001 的巨著,希望他能重新找到归属。 然而,小小蒟蒻觉得大佬的博客还是太 $\texttt{高端}$ 了,于是,这里有几个小小的补充,结合着看,应该不至于完全看不懂。 感谢大佬!“上帝保佑,但愿人人都能”看见这篇博客:[《二叉搜索树(BST)》](https://www.luogu.com/article/zqha49ef) 注意,以下代码**满足例题**,即输入应**受到题目限制**,否则**代码不保证正确性**。 - ### 建立-BUILDING 这里,我们用类似**前向星**的方式进行“指向”表示根叶关系,用**结构体**来把一个节点表示出来,于是便有定义如下: ```cpp const int N = 1e6,inf = 0x7ffffff; //一个代表无穷的inf struct BST{ int ls,rs;//左右儿子的下标,全部初始化为0 int val;//权值或关键码 int num;//val出现的次数 int siz;//当前子树的节点个数 }a[N]; int cnt,root; //与前向星类似的cnt //root是根节点 ``` 大家可能注意到了一个`num`,一方面,BST 中不会出现权值相同的节点,另一方面,`num`的实际含义表示了节点的“虚实”,这将在后面的排名中起重大作用。 (主要是由于 同机房数学大佬 ~~就是下面那位~~ 被这玩意儿迷惑了,加以细说) 然而,为了方便查找、修改操作,我们初始化时需要两个边界——$+\infty$、$-\infty$ ,我们把 $-\infty$ 作为根节点,于是在最开始 $+\infty$ 便是根节点的右儿子, ```cpp int renew(int u){//加入新节点 a[++cnt].val=u; a[cnt].num++; a[cnt].siz++; return cnt; } void build(){ renew(-inf); a[1].num--;a[1].siz--;//边界不需要计入数量 renew(inf); a[2].num--;a[2].siz--;//边界不需要计入数量 root=1;//根节点更新 a[1].rs=2;//右儿子下标 } ``` - ### 查询-QUERY 查找 $x$ ,有可能有这个数,也可能没这个数。 从根节点开始,按如下操作遍历节点: 1. 当 $val$ == $x$ 时,返回当前节点下标; 2. 当 $val$ < $x$ 时,向右儿子 $\texttt{query}$ 。 3. 当 $val$ > $x$ 时,向做儿子 $\texttt{query}$ 。 4. 当当节点下标无意义时,返回 `0` 。 ```cpp int query(int p,int val){ if(!p) return 0; if(val==a[p].val) return p; if(val<a[p].val) return query(a[p].ls,val); else return query(a[p].rs,val); } ``` - ### 插入-INSERT 设要插入一个数 $x$ 。那么如下。 从根节点开始,按 $\texttt{BST}$ 的性质,一点一点往下遍历, 在 $\texttt{BST}$ 有可能已经有一个 $x$ ,所以当查找到 $x$ 的时候,直接返回;如果没有,那么比较大小,当当前节点 $val$ 大于 $x$ 时,向左儿子走,不管是否为空,反之向右儿子走,不管是否为空。 代码如下: ```cpp void inserten(int &p,int val){ if(!p){//先看看下面的,再回来看 p=renew(val); //上面的int &p是引用,当当前p为0时,就是此位置为空 //那么就更新当前空节点,引用修改父节点的子节点下标 return ; } if(val==a[p].val) return ;//假如有这个val,则无为返回 if(val<a[p].val) inserten(a[p].ls,val);//向左查找 else inserten(a[p].rs,val);//向右查找 } ``` - ### 求前驱/后继-ASK FOR PREMAXN OR SUFMINN 例如求 $x$ 的前驱后继。 $\bold{前驱}\texttt{pre-small bigger or lower-bound}$:在 $\texttt{BST}$ 中权值小于 $x$ 的前提下,权值最大的节点。 $\bold{后继}\texttt{suf-big smaller or upper-bound}$:在 $\texttt{BST}$ 中权值大于 $x$ 的前提下,权值最小的节点。 搜索查找嘛,有一种情况是,当 $\texttt{BST}$ 中有这个节点,那么只需要在它的左子树中一直往右节点找(查前驱)或在其右子树中一直往右节点找(查后继)。 ```cpp int get_pre(int p,int val){ int ans=1;//先假设答案为inf p=root;//从根节点开始 while(p){ if(val==a[p].val){//假如有这个节点 if(a[p].l>0){//他有左儿子 p=a[p].l; while(a[p].r) p=a[p].r;//使劲找右儿子 ans=p; } break; } if(a[p].val<val && a[ans].val<a[p].val) ans=p;//使劲更新 p=val<a[p].val?a[p].l:a[p].r;//左右进入 } return ans; } int get_suf(int p,int val){ int ans=2; p=root; while(p){ if(val==a[p].val){ if(a[p].r>0){ p=a[p].r; while(a[p].l) p=a[p].l; ans=p; } break; } if(val<a[p].val && a[p].val<a[ans].val) ans=p; p=val<a[p].val?a[p].l:a[p].r; } return ans; } ``` 注意,这么写无论 $\texttt{BST}$ 中有没有 $x$ ,都可以得到合理答案。 - ### 查询排名-QUERY FOR CERTAIN ONE'S RANKING 和查询类似,但要注意,小于 $x$ 的节点,不一定就在 $x$ 的左子树中,有可能 $x$ 作为某节点的右儿子(大于该节点),所以出现这种情况时还要加上这“某节点”的左子树及其本身。 策略是从根节点开始: 1. 当当前节点等于 $x$ 时,返回当前节点左子树节点个数。 2. 当当前节点小于 $x$ 时,若有右子树,则向右子树询问,并返回询问与当前节点的左子树节点个数、当前节点的和;若无右子树,则直接返回当前节点子树节点个数+1。 3. 当当前节点大于 $x$ 时,若有左子树,则向左子树询问;若无,说明 $x$ 为当前子树的最小权值,返回 1。 在 3 中,我们返回的 1 就恰好是“小于 $x$ 的个数+1”中的 1 ,2 中同理。 此时,前面形同虚设的`a[].num`就派上用场了,由于左右边界$\texttt{-inf}$、$\texttt{inf}$不计入 $\texttt{BST}$ ,所以他们的`a[1].num`、`a[2].num`为 $0$ ,而其他的作为 $1$,此时就很好处理了。 ```cpp int query_rank(int p,int val){ printf("query(%d,%d)\n",p,val); if(val==a[p].val) return a[a[p].l].siz+1; //如果有 x ,那么小于 x 的必定包含 x 的左子树 if(val>a[p].val){ //当 x 大于当前节点时,当前节点的左子树也是小于 x 的 if(a[p].r>0)//有右子树,节点及其左子树都小于 x return a[a[p].l].siz+a[p].num+query_rank(a[p].r,val); else//没有,当前节点所剩其下的节点都小于 x ,加 size return a[p].siz+1; }else{ //当 x 小于当前节点时,往左走, if(a[p].l>0) return query_rank(a[p].l,val); else return 1;//x 比子树中所有数都小,排名 0+1 } } ``` - ### 排名查询-QUERY FOR SOMEONE RANKED CERTAINLY 查询排名为 $x$ 的节点权值为多少。 类似于上一个操作,我们尝试得到当前节点的排名,并不断比较,从而更进,然而,有一点特殊的是,我们需要判断当前节点是否为边界,因为当查找最后排名时,会先到边界,所以还要判断。 ```cpp int query(int p,int rank,int rate){ if(rank==rate+a[a[p].l].siz+a[p].num){ if(a[p].num) return a[p].val; else return query(a[p].l,rank,rate); } if(rank<rate+a[a[p].l].siz+a[p].num){ return query(a[p].l,rank,rate); }else{ return query(a[p].r,rank,rate+a[a[p].l].siz+a[p].num); } } ``` - ### 删除-DELETE 删除已有的 $x$ 。 先要找到 $x$ ,其次,当失去 $x$ 时,我们可以让 $x$ 的前驱或者后继来代替 $x$ 的位置,但我们仍需处理其前驱或后继的子树。 以后继替代 $x$ 为例。 当找到 $x$ 时,向其右子树寻找后继 $suf$ ,然而 $suf$ 一定没有左子树,所以用其右子树替代它,并且,用它把 $x$ 替换掉。 ```cpp void deleten(int &p,int val){ if(!p) return ; if(val==a[p].val){ //无左无右下仍可 if(!a[p].l) p=a[p].r;//无左用右替 else if(!a[p].r) p=a[p].l;//无右用左替 else{ //有左有右 int suf=a[p].r;//找后继 while(a[suf].l) suf=a[suf].l;//直到无左子树 deleten(a[suf].r,a[suf].val);//用其右子树替其 //用suf替x a[suf].l=a[p].l,a[suf].r=a[p].r; p=suf; } return ; } //检索 if(val<a[p].val) deleten(a[p].l,val); else deleten(a[p].r,val); } ``` ### 例题-EXAMPLE 有了上述操作(不包括删除、查询操作),我们就可以做这道题:[P5076 【深基16.例7】普通二叉树(简化版)](https://www.luogu.com.cn/problem/P5076) ```cpp #include<bits/stdc++.h> using namespace std; const int N = 1e4+10,inf=0x7fffffff; struct BST{ int l,r; int val; int num; int siz; }a[N]; int cnt,root; int q; void couten(); int renew(int u){ a[++cnt].val=u; a[cnt].num++; a[cnt].siz++; return cnt; } void build(){ renew(-inf); a[1].num--;a[1].siz--; renew(inf); a[2].num--;a[2].siz--; root=1,a[1].r=2; } int query_rank(int p,int val){ // printf("query(%d,%d)\n",p,val); if(val==a[p].val) return a[a[p].l].siz+1; //如果有 x ,那么小于 x 的必定包含 x 的左子树 if(val>a[p].val){ //当 x 大于当前节点时,当前节点的左子树也是小于 x 的 if(a[p].r>0)//有右子树,节点及其左子树都小于 x return a[a[p].l].siz+a[p].num+query_rank(a[p].r,val); else return a[p].siz+1; }else{ if(a[p].l>0) return query_rank(a[p].l,val); else return 1; } } void inserten(int &p,int val){ if(!p){//先看看下面的,再回来看 p=renew(val); //上面的int &p是引用,当当前p为0时,就是此位置为空 //那么就更新当前空节点,引用修改父节点的子节点下标 return ; } a[p].siz++; if(val==a[p].val) return ;//假如有这个val,则无为返回 if(val<a[p].val) inserten(a[p].l,val);//向左查找 else inserten(a[p].r,val);//向右查找 } int get_suf(int p,int val){ int ans=2; p=root; while(p){ if(val==a[p].val){ if(a[p].r>0){ p=a[p].r; while(a[p].l) p=a[p].l; ans=p; } break; } if(val<a[p].val && a[p].val<a[ans].val) ans=p; p=val<a[p].val?a[p].l:a[p].r; } return ans; } int get_pre(int p,int val){ int ans=1;//先假设答案为inf p=root;//从根节点开始 while(p){ if(val==a[p].val){//假如有这个节点 if(a[p].l>0){//他有左儿子 p=a[p].l; while(a[p].r) p=a[p].r;//使劲找右儿子 ans=p; } break; } if(a[p].val<val && a[ans].val<a[p].val) ans=p;//使劲更新 p=val<a[p].val?a[p].l:a[p].r;//左右进入 } return ans; } int query(int p,int rank,int rate){ if(rank==rate+a[a[p].l].siz+a[p].num){ if(a[p].num) return a[p].val; else return query(a[p].l,rank,rate); } if(rank<rate+a[a[p].l].siz+a[p].num){ return query(a[p].l,rank,rate); }else{ return query(a[p].r,rank,rate+a[a[p].l].siz+a[p].num); } } int main(){ build(); scanf("%d",&q); while(q--){ int op,x; scanf("%d%d",&op,&x); if(op==5){ inserten(root,x); }else if(op==4){ printf("%d\n",a[get_suf(root,x)].val); }else if(op==3){ printf("%d\n",a[get_pre(root,x)].val); }else if(op==1){ printf("%d\n",query_rank(root,x)); }else{ printf("%d\n",query(root,x,0)); } } return 0; } void couten(){ vector<int> s; s.push_back(root); while(!s.empty()){ vector<int> g; for(int i=0;i<s.size();i++){ BST j=a[s[i]]; printf("%d:%d(%d)[%d,%d] ",s[i],j.val,j.siz,j.l,j.r); if(j.l) g.push_back(j.l); if(j.r) g.push_back(j.r); } s=g; puts(""); } } ``` 再打了一遍:[模板(最新款——马蜂优良、定义明确)](https://www.luogu.com.cn/article/67o1dbix)。 ## 不如STL……-$\texttt{STL}$ BETTER [大佬救我](https://www.luogu.com/article/t00def0c) $\bold{谨记}$,使用 $\texttt{STL}$ 时一定要先初始化两个哨兵($-\infty$、$+\infty$),防止在只有一个数时找一个小于这个数的数的前继时迭代器越界。 ## 三、特异性进化-EVOLUTION FOR SPECIFICTY 显然,满足 $\texttt{BST}$ 性质且中序遍历相同的二叉查找树不唯一,如此,有可能树的深度达到 $O(\frac{1}{2}n)$ ,从而被卡成 $O(n^2)$ 的时间复杂度,所以,我们想要把中序遍历相同的二叉树确定下来,确定成深度维持在 $O(\log n)$ 的级别。 于是,下一代数据结构登场——[树堆-TREAP](https://www.luogu.com.cn/article/ugcagegy)。