浅谈*衡树
吐槽
*代表ping或jin,为什么要屏蔽掉ping和jin
前置知识
二叉排序树没学过的先去学习一下
正文
简介
*衡树是一种可以在O(log n)的时间复杂度上动态插入、删除、求前驱后继、求排名等的树形结构
*衡树的实现方法很多,有Splay、Treap、AVL树、红黑树等
推荐写Treap,因为Treap的常数小,代码复杂度低(相对而言)
具体实现
二叉排序树在形为一条链时,有退化至O(n^2)的可能
为了使其稳定在O(n log n),需要使其趋*于*衡状态,如下图
可是如何使链变得*衡呢?
可以进行旋转操作,将子节点转动到父节点,同时保持二叉排序树的性质,如下图
像这样旋转可以在保证性质不变的前提下使其趋*于*衡
旋转分两种,上图为右旋,即将左儿子旋上来,另一种为左旋,即将有儿子旋上来
但是,我们应该在什么时候进行旋转呢?
这就是Treap的精髓,对每一个点附一个随机值,再让随机值保持堆的性质,如果堆的性质被打破,那么旋转
也许你可能很倒霉,随机的值链上就保持堆的性质,然后退化,但因为它是随机的,所以退化的可能性几乎可以忽略不记
实现
P3369 【模板】普通*衡树
#include<bits/stdc++.h> using namespace std; struct node{ long long val,s,m; long long x; node *l,*r; }*root; //val是值,s是子树大小,m是有几个大小为val的点,x是随机值,l和r是左右子节点 long long s(){ long long x=rand(); x*=rand(); x*=rand(); x*=rand(); return x; } //求随机值 void rx(node* &r){ node *k=r->l; r->l=k->r; k->r=r; r->s=r->s-k->s; if(r->l!=NULL) r->s+=r->l->s; k->s+=r->s; if(r->l!=NULL) k->s-=r->l->s; r=k; } //右旋,看不懂可以画下图 void lx(node* &r){ node *k=r->r; r->r=k->l; k->l=r; r->s=r->s-k->s; if(r->r!=NULL) r->s+=r->r->s; k->s+=r->s; if(r->r!=NULL) k->s-=r->r->s; r=k; } //左旋 void insert(node* &r,long long x){ if(r==NULL){ r=new node; r->x=s(); r->val=x; r->s=r->m=1; r->l=r->r=NULL; return; } //新建节点 r->s++; //子树变大 if(r->val>x){ //去右儿子 insert(r->l,x); if(r->l->x<r->x) rx(r); //违背堆的性质,将左儿子右旋上来 }else if(r->val==x){ r->m++; //值相同,数量增加 return; }else{ insert(r->r,x); if(r->r->x<r->x) lx(r); //与去右儿子同理,只是反过来 } } void cl(node* &r,long long x){ r->s--; //子树大小减少 if(r->val>x){ cl(r->l,x); if(r->l!=NULL && r->l->x<r->x) rx(r); //要特判空节点 }else if(r->val==x){ //到达需删除的节点 if(r->m>1){ //数量大于1,只减少数量 r->m--; return; } if(r->l==NULL || r->r==NULL){ //一个子节点或没有子节点,直接删 if(r->l==NULL && r->r==NULL) r=NULL; else if(r->l==NULL) r=r->r; else r=r->l; return; }else{ //两个子节点不能直接删,要旋下去再删 r->s++; //小坑,要对旋上来的节点子树大小减少 if(r->l->x<r->r->x){ rx(r); r->s--; cl(r->r,x); }else{ lx(r); r->s--; cl(r->l,x); } } }else{ cl(r->r,x); if(r->r!=NULL && r->r->x<r->x) lx(r); } } long long findp(node* r,long long x){ if(x<r->val) return findp(r->l,x); //在左边就与当前结点和右儿子无关 else if(x==r->val){ if(r->l==NULL) return 1; else return 1+r->l->s; } //找到后特判左子节点为空,注意不是m而是1,因为是严格排名,相同的不算 else{ if(r->l==NULL) return findp(r->r,x)+r->m; else return findp(r->r,x)+r->l->s+r->m; } //右子节点把当前节点和左子树算上 } long long findx(node* r,long long x){ if(r==NULL) return -1; if(x>r->s) return 0; long long ls; if(r->l==NULL) ls=0; else ls=r->l->s; if(x<=ls) return findx(r->l,x); else if(x<=ls+r->m) return r->val; else return findx(r->r,x-ls-r->m); //跟求排名原理一致,只是倒着做 } long long findq(node* r,long long x){ if(r==NULL) return 0; if(r->val<x){ if(r->r!=NULL) return max(r->val,findq(r->r,x)); else return r->val; //已经比x小,看能否更接*于x,即找右子树 }else{ if(r->l!=NULL) return findq(r->l,x); else return 0; } //找x的前驱,当前节点更大,就找左子树 } long long findh(node* r,long long x){ if(r==NULL) return 1e9; if(r->val>x){ if(r->l!=NULL) return min(r->val,findh(r->l,x)); else return r->val; }else{ if(r->r!=NULL) return findh(r->r,x); else return 1e9; } } //与前驱同理,只是操作反过来 int main(){ long long q,opt,x; srand(time(0)); cin>>q; while(q--){ cin>>opt>>x; if(opt==1){ insert(root,x); //插入 }else if(opt==2){ cl(root,x); //删除 }else if(opt==3){ cout<<findp(root,x)<<endl; //输出排名 }else if(opt==4){ cout<<findx(root,x)<<endl; //输出排名为x的数 }else if(opt==5){ cout<<findq(root,x)<<endl; //求后继 }else{ cout<<findh(root,x)<<endl; //求前驱 } } return 0; }
代码有亿点点长,主要是因为操作类型较多,实际用不着写六种操作
如果用指针写,记得多判空