今天还是冯哲老师的讲授~~
今日内容:简单数据结构(没看出来简单qaq)
1.搜索二叉树
前置技能
一道入门题
在初学OI的时候,总会遇到这么一道题。
给出N次操作,每次加入一个数,或者询问当前所有数的最大值。
维护一个最大值Max,每次加入和最大值进行比较。
时间复杂度O(N).
EX:入门题
给出N次操作,每次加入一个数,删除一个之前加入过的数,或者询问当前所有数的最大值。
N ≤ 100000.
引入二叉搜索树(BST):
特征:
二叉搜索树的key值是决定树形态的标准。
每个点的左子树中,节点的key值都小于这个点。
每个点的右子树中,节点的key值都大于这个点。
一个好例子:
我们可以发现:每个结点的左儿子一定小于该结点,右儿子一定大于该结点!
进一步可以推出:每一层从左往右都是按从大到小的顺序拍好的(虽然没啥用)
示例:
基本操作:
查询最大/最小值
注意到BST左边的值都比右边小,所以如果一个点有左儿子,就往左儿子走,否则这个点就是最小值啦。
代码(最小值):
int Findmin() { int x = root; //x记录当前结点,当然从根节点开始找 while (ls[x]) x=ls[x]; //如果Is[x]不为0,说明有左儿子,让x等于它的左儿子 return key[x]; //返回最小权值 }
插入一个值
现在我们要插入一个权值为x的节点。
为了方便,我们插入的方式要能不改变之前整棵树的形态。
首先找到根,比较一下key[root]和x,如果key[root] < x,节点应该插在root右侧,否则再左侧。
看看root有没有右儿子,如果没有,那么直接把root的右儿子赋成x就完事了。
否则,为了不改变树的形态,我们要去右儿子所在的子树里继续这一操作,直到可以插入为止。
删除一个值
现在我们要删除一个权值为x的点
之前增加一个点我们能够不改变之前的形态。
定位一个节点
要删掉一个权值,首先要知道这个点在哪。
从root开始,像是插入一样找权值为x的点在哪。
int Find(int x) //在搜索二叉堆里定位一个数 { int now=root; //从根节点开始找 while(key[now]!=x) //如果不相等就一直往下找 if (key[now]<x) now=rs[now]; //如果大于当前结点就直接从右边找,二分思想减少复杂度 else now=ls[now]; //否则从左边找 return now; //如果没找到会返回空值 }
方案一
直接把这个点赋成一种空的状态.
但是这样的话查询起来不太方便。
所以还是稍微麻烦一点吧。
方案二
对这个节点x的儿子情况进行考虑。
如果x没有儿子,删掉x就行了。
如果x有一个儿子,直接把x的儿子接到x的父亲下面就行了。
x如果是x父亲的左儿子,那么x的儿子直接接在x父亲的左下面就好了,否则接在右下面
如果x有两个儿子,这种情况就比较麻烦了。
定义x的后继y,是x右子树中所有点里,权值最小的点。
找这个点可以x先走一次右儿子,再不停走左儿子。(因为y肯定在x右子树中的最左侧)
如果y是x的右儿子,那么直接把y的左儿子赋成原来x的左儿子,然后用y代替x的位置。
原理:
就算y是x右儿子中最小的一个,但一定比x的任何一个左儿子都大,所以换到x的位置其他的点不用动,因为左边的点都比他小,右边的点都比他大,而x小于任何一个右子树的点,所以换到右子树里就是最小的那个,也就是之前y所在的位置,又因为y没有左儿子,但可能有右儿子(y的右儿子代替的是y他本来在的位置),所以x换过去就成了上面只有一个孩子的情况,这样删除就方便啦!(不得不说这也太强了吧!)
#include<cstdio> #include<algorithm> #include<cstring> #include<iostream> #include<cstring> #include<string> #include<cmath> #include<ctime> #include<set> #include<vector> #include<map> #include<queue> #define N 300005 #define M 8000005 #define mid ((l+r)>>1) #define mk make_pair #define pb push_back #define fi first #define se second using namespace std; int i,j,m,n,p,k,ls[N],rs[N],sum[N],size[N],a[N],root,tot,fa[N]; void ins(int x)//插入一个权值为x的数字 { sum[++tot]=x; //用tot来表示二叉树里的节点个数,sum数组存第tot个结点的权值 size[tot]=1; //它能遍历到的点只有它自己 if (!root) root=tot;//如果一开始一个节点都没有,就要找一个节点当根 else { int now=root; //从根开始 for (;;) { ++size[now]; if (sum[now]>sum[tot]) //判断和当前节点的大小,如果tot小于当前结点now,说明应该插在now左边 { if (!ls[now]) //如果now没左儿子,直接插入 { ls[now]=tot;fa[tot]=now; //标记now的左儿子为tot,tot的父亲为now break; //跳出循环,进入下次插入 } else now=ls[now];//如果now有左儿子,那么继续从now的左儿子Is[now]继续往下找 } else { if (!rs[now]) //与上面同理 { rs[now]=tot; fa[tot]=now; break; } else now=rs[now]; } } } } int FindMin() //找最小值,肯定在左子树里 { int now=root; //从根节点开始找 while (ls[now]) now=ls[now]; //一直往下找,直到没有左孩子 return sum[now]; //返回最小值 } void build1()//暴力build的方法,每次插入一个值 { for (i=1;i<=n;++i) ins(a[i]); } int Divide(int l,int r) { if (l>r) return 0; ls[mid]=Divide(l,mid-1); rs[mid]=Divide(mid+1,r); fa[ls[mid]]=fa[rs[mid]]=mid; fa[0]=0; sum[mid]=a[mid]; size[mid]=size[ls[mid]]+size[rs[mid]]+1; return mid; } void build2()//精巧的构造,使得树高是log N的 { sort(a+1,a+n+1); root=Divide(1,n); tot=n; } int Find(int x)//查询值为x的数的节点编号 { int now=root; while (sum[now]!=x&&now) if (sum[now]<x) now=rs[now]; else now=ls[now]; return now; } int Findkth(int now,int k) { if (size[rs[now]]>=k) return Findkth(rs[now],k); //因为右子树的数都大于左子树中的数,所以如果当前点右子树的size值大于k,那么第k大的值一定在右子树里,递归右子树继续往下找 else if (size[rs[now]]+1==k) return sum[now]; //因为右子树一共有size[rs[now]]个,若size[rs[now]]+1==k,说明第k大的值就是当前结点,直接返回,因为左边的数都比当前结点的数小 else Findkth(ls[now],k-size[rs[now]]-1);//注意到递归下去之后右侧的部分都比它要大,第k大的数只能在左子树里了,此时我们已经找了前size[rs[now]]+1位大数了,所以要用k减去它 } void del(int x) //删除一个值为x的点 { int id=Find(x),t=fa[id];//找到这个点的编号id,t位id的父亲 if (!ls[id]&&!rs[id]) //如果这个结点没有儿子 { if (ls[t]==id) ls[t]=0; //是左儿子就将ls[t]置空 else rs[t]=0; //否则就将rs[t]置空 for (i=id;i;i=fa[i]) size[i]--; //将结点id删去后,他及他的祖先能遍历到的点都减少了一,所以要减去一 } else if (!ls[id]||!rs[id]) //只有一个儿子 { int child=ls[id]+rs[id];//找存在的儿子的编号,因为其中肯定有一个儿子的编号为0表示没有该儿子,加起来就是存在的那个儿子的编号 if (ls[t]==id) ls[t]=child; //是左儿子就将ls[t]接上id的孩子child else rs[t]=child; //否则就将rs[t]接上id的孩子child fa[child]=t; //标记child的新父亲 for (i=id;i;i=fa[i]) size[i]--; //将结点id删去后,他及他的祖先能遍历到的点都减少了一,所以要减去一 } else { int y=rs[id]; while (ls[y]) y=ls[y]; //找后继 if (rs[id]==y) { if (ls[t]==id) ls[t]=y; else rs[t]=y; fa[y]=t; ls[y]=ls[id]; fa[ls[id]]=y; for (i=id;i;i=fa[i]) size[i]--; size[y]=size[ls[y]]+size[rs[y]];//y的子树大小需要更新 } else //最复杂的情况 { for (i=fa[y];i;i=fa[i]) size[i]--;//注意到变换完之后y到root路径上每个点的size都减少了1 int tt=fa[y]; //先把y提出来 if (ls[tt]==y) { ls[tt]=rs[y]; fa[rs[y]]=tt; } else { rs[tt]=rs[y]; fa[rs[y]]=tt; } //再来提出x if (ls[t]==x) { ls[t]=y; fa[y]=t; ls[y]=ls[id]; rs[y]=rs[id]; } else { rs[t]=y; fa[y]=t; ls[y]=ls[id]; rs[y]=rs[id]; } size[y]=size[ls[y]]+size[rs[y]]+1;//更新一下size } } } int main() { scanf("%d",&n); for (i=1;i<=n;++i) scanf("%d",&a[i]); build1(); printf("%d\n",Findkth(root,2));//查询第k大的权值是什么,这里k==2 del(4); printf("%d\n",Findkth(root,2));
return 0; }
又是未完待续qaq~