二叉搜索树
前置芝士
中序遍历,二叉树
(本蒟蒻即将要学平衡树,所以先来学搜索树总结一下)
定义
- 空树是二叉搜索树。
- 若二叉搜索树的左子树不为空,则其左子树上所有点的附加权值均小于其根节点的值。
- 若二叉搜索树的右子树不为空,则其右子树上所有点的附加权值均大于其根节点的值。
- 二叉搜索树的左右子树均为二叉搜索树。
(人话就是:右儿子大于根节点大于左儿子)
作用
如果你想找第n大的数,你明显需要冒泡排序或快速排序,最坏复杂度分别为: O ( n 2 / 2 ) , O ( n log n ) O(n^2/2),O(n\log n) O(n2/2),O(nlogn)
但如果你用上图这棵树,根据他(右儿子>自己>左儿子)的特性,可以在期望复杂度 O ( log n ) O(\log n) O(logn) 的时间内查出一个数据
同时修改也变得简单, O ( log n ) O(\log n) O(logn) 的时间查找位置, O ( 1 ) O(1) O(1) 的时间连边
性质
对一个二叉树进行中序遍历,如果是单调递增的,则可以说明这个树是二叉搜索树
操作及模板
(在接下来的代码中, n n n 为节点个数, h h h 为树的高度, s i z [ x ] siz[x] siz[x] 为 x x x 及子树的大小, v a l [ x ] val[x] val[x] 为 x x x 点数值, c n t [ x ] cnt[x] cnt[x] 为 x x x 节点的出现次数, l s [ x ] , r s [ x ] ls[x],rs[x] ls[x],rs[x] 为 x x x 的左儿子和右儿子)
遍历
时间复杂度: O ( n ) O(n) O(n)
void print(int rt){
// 遍历以 o 为根节点的二叉搜索树
if(!rt) return ;// 遇到空树,返回
print(ls[rt]); // 递归遍历左子树
for(int i=1;i<=cnt[rt];i++) printf("%d\n",val[rt]); // 输出根节点信息
print(rs[rt]); // 递归遍历右子树
}
查找最大/最小值
由性质可得,最小值为左链顶点,最大值为右链顶点。时间复杂度: O ( h ) O(h) O(h)
int findmin(int rt){
if(!ls[rt]) return rt;
return findmin(ls[rt]);//一直向左儿子跳
}
int findmax(int rt){
if(!rs[rt]) return rt;
return findmax(rs[rt]);//一直向右儿子跳
}
findmax和findmin函数返回的是最大/小值的编号,如果需要值,用 v a l [ r t ] val[rt] val[rt] 来获得最大/小值
插入
时间复杂度: O ( h ) O(h) O(h)
void insert(int &rt,int v){
if(!rt){ //若o为空,直接返回一个值为v的新节点。
val[rt=++sum]=v;
cnt[rt]=siz[rt]=1;
return ;
}
siz[rt]++;
if(val[rt]==v){
cnt[rt]++;
return ;
}
if(val[rt]>v) insert(ls[rt],v);//若o的权值大于v,在o的左子树中插入权值为v的节点。
if(val[rt]<v) insert(rs[rt],v);//若o的权值小于v,在o的右子树中插入权值为v的节点。
}
删除
时间复杂度: O ( h ) O(h) O(h)
int deletemin(int &rt){
if(!ls[rt]){//若没有左儿子
int u=rt;
rt=rs[rt];//用右儿子替换父节点
return u;
} else{
int u=deletemin(ls[rt]);
siz[rt]-=cnt[u];//删掉了u,还需要将它的siz删掉
return u;
}
}
//返回删除的编号
void del(int &rt,int v){//在以rt为根节点的二叉搜索树中删除一个值为v的节点
siz[rt]--;//不管怎么着,都是删它的子树
if(val[rt]==v){
if(cnt[rt]>1){//若附加cnt大于1
cnt[rt]--;//减去即可
return ;
}
if(ls[rt]&&rs[rt]) rt=deletemin(rs[rt]);//一般是用它左子树的最大值或右子树的最小值代替它,然后将它删除。
//这里以右子树最小值代替为例
else
rt=ls[rt]+rs[rt];//只有一个儿子的节点,返回这个儿子
return ;
}
if(val[rt]>v) del(ls[rt],v);
if(val[rt]<v) del(rs[rt],v);
}
求元素的排名
维护每个根节点的子树大小 siz。查找一个元素的排名,首先从根节点跳到这个元素,若向右跳,答案加上左儿子节点个数加当前节点重复的数个数,最后答案加上终点的左儿子子树大小 +1。
int queryrhk(int rt,int v){
if(val[rt]==v) return siz[ls[rt]]+1;
if(val[rt]>v) return queryrnk(ls[rt],v);
if(val[rt]<v) return queryrnk(rs[rt],v)+siz[ls[rt]]+cnt[rt];
}
查找排名为k的元素
在一棵子树中,根节点的排名取决于其左子树的大小。
若其左子树的大小大于等于 k k k,则该元素在左子树中;
若其左子树的大小在区间 [ k − c n t , k − 1 ] [k-cnt,k-1] [k−cnt,k−1]( c n t cnt cnt为当前结点的值的出现次数)中,则该元素为子树的根节点;
若其左子树的大小小于 k − c n t k-cnt k−cnt ,则该元素在右子树中。
时间复杂度: O ( h ) O(h) O(h) 。
int querykth(int rt,int k){
if(siz[ls[rt]]>=k) return querykth(ls[rt],k);
if(siz[ls[rt]]<k-cnt[rt]) return querykth(rs[rt],k-siz[ls[rt]]-cnt[rt]);
// 如要找排名为 k 的元素所对应的结点,直接 return rt 即可
}
找前驱和后缀
时间复杂度: O ( h ) O(h) O(h)
//找前驱
int getpre(int rt,int val,int ans){
if(val[rt]>=val){
if(!ls[rt]) return ans;
else getpre(ls[rt],val,ans);
}
else{
if(!rs[rt]){
if(val[rt]<val) return val[rt];
else return ans;
}
if(cnt[rt]) return getpre(rs[rt],val,val[rt]);
else return getpre(rs[rt],val,ans);
}
}
//找后缀
int getnext(int rt,int val,int ans){
if(val[rt]<=val){
if(!rs[rt]) return ans;
else getnext(rs[rt],val,ans);
}
else{
if(!ls[rt]){
if(val[rt]>val) return val[rt];
else return ans;
}
if(cnt[rt]) return getnext(ls[rt],val,val[rt]);
else return getnext(ls[rt],val,ans);
}
}