二叉搜索树(简介)
定义和性质
二叉查找树或是空树,或是满足如下三个性质的二叉树:
1.若其左子树非空,则左子树上所有节点的值都小于根节点的值 若其右子树非空,则右子树上所有节点的值都大于根节点的值
2.其左右子树都是一棵二叉查找树
3.二叉查找树的特性:左子树<根<右子树,即二叉查找树的中序遍历是一个递增序列
二叉查找树的查询
算法步骤:
- 若二叉查找树为空,则查找失败,返回空指针
- 若二叉查找树非空,则将待查找关键字key与根节点的关键字T−>data进行比较:
- 如果x=T->data,则查找成功,返回查询到的当前节点T
- 如果x<T−>data,则递归查找左子树
- 如果x>T−>data,则递归查找右子树
时间复杂度:最好情况是O(logn),最坏情况是O(n) 空间复杂度:O(n)
查找最小值和最大值
由二叉搜索树的性质可得,二叉搜索树上的最小值为二叉搜索树左链的顶点,最大值为二叉搜索树右链的顶点。时间复杂度为O(h) 。
inline int findmin(int o){//查找最小值
if(!lc[o]) return o;//找到第一个没有左子树的节点,由二叉查找树的性质知,此时该节点值为最小值,返回节点编号
return findmin(lc[o]);//一直向左子树找
}
inline int findmax(int o){//查找最大值
if(!rc[o]) return o;//找到第一个没有右子树的节点,由二叉查找树的性质知,此时节点值大于左子树所有节点值,该节点值即为最大值,返回节点编号
return findmax(rc[o]);//一直向右子树找
}
插入一个元素
定义 insert(o,v) 为在以 o 为根节点的二叉搜索树中插入一个值为 v 的新节点。 分类讨论如下:
- 若 o 为空,直接返回一个值为 v 的新节点。
- 若 o 的权值等于v ,该节点的附加域该值出现的次数自增 。
- 若 o 的权值大于v ,在 o 的左子树中插入权值为 v 的节点。
- 若 o 的权值小于 v,在 o 的右子树中插入权值为 v 的节点。
inline void insert(int &o,int v){//在以o为根节点的二叉搜索树中插入一个值为v的节点
if(!o){//如果o没有在树上
val[o=++num]=v;//加入一个值为v,编号为++n的节点
cnt[o]=siz[o]=1;//初始化节点o的出现次数和子树大小
lc[o]=rc[o]=0;//将左右节点初始值赋为0
return;
}
siz[o]++;//o还是在树上,更新最终得到的o点的所有祖先节点的子树大小
if(val[o]>v) insert(lc[o],v);//如果节点o的值>v,说明v在此时的节点的左子树上,向左子树插入v
if(val[o]<v) insert(rc[o],v);//如果节点o的值<v,说明v在此时的节点的右子树上,向右子树插入v
}
删除一个元素
定义 del(o,v) 为在以 o 为根节点的二叉搜索树中删除一个值为 v 的节点。 先在二叉搜索树中找到权值为 v 的节点,分类讨论如下:
- 若该节点的附加 cnt 大于 1,只需要减少 cnt。
- 若该节点的附加 cnt 为 1:
- 若 o 为叶子节点,直接删除该节点即可。
- 若 o 为链节点,即只有一个儿子的节点,返回这个儿子。
- 若 o 有两个非空子节点,一般是用它左子树的最大值或右子树的最小值代替它,然后将它删除。
时间复杂度 O(h)。
inline int deletemin(int &o){//查询最小值
if(!lc[o]){//如果左子树为空
int u=o;
o=rc[o];//找到最小值,更新节点,原本o点的值更新为最小值的右节点
return u;//返回最小值
}else{//如果左子树不为空
int u=deletemin(lc[o]);//继续查找
siz[o]-=cnt[u];//沿途的节点的子树大小-=u的个数
return u;
}
}
inline void del(int &o,int v){//在以o为根节点的二叉查找树中删除一个值为v的节点
siz[o]--;//o节点的子树大小减一
if(val[o]==v){//如果o的值和v相同(找到了)
if(cnt[o]>1){//如果o点不止一个
cnt[o]--;//直接把cnt[o]-1
return;
}
if(lc[o]&&rc[o]) o=deletemin(rc[o]);//如果o节点左右子树均不为空,用左子树的最大值或右子树的最小值来替换o(此处用的是右子树的最小值)
else o=lc[o]+rc[o];//如果左子树为空或右子树为空或左右子树均为空(此处代码浓缩):当左子树为空时,lc[o]为0,o点被右节点替换;当右子树为空时,rc[o]为0,o被左节点替换;当左右子树均为空时lc[o]=rc[o]=0,o点变为0;
return;
}
if(val[o]>v) del(lc[o],v);//如果节点o的值>v,说明v在此时的节点的左子树上,向左子树搜索v
if(val[o]<v) del(rc[o],v);//如果节点o的值<v,说明v在此时的节点的右子树上,向右子树搜索v
}
求元素的排名
- 排名定义为将数组元素排序后第一个相同元素之前的数的个数加一。
- 查找一个元素的排名,首先从根节点跳到这个元素,若向右跳,答案加上左儿子节点个数加当前节点重复的数个数,最后答案加上终点的左儿子子树大小加一。 时间复杂度 O(h)。
inline int queryrnk(int o,int v){//求一个值为v的元素在以o为根节点的二叉搜索树中的排名
if(val[o]==v) return siz[lc[o]]+1;//如果val[o]==v,找到v了,返回该节点左节点的排名+1
if(val[o]>v) return queryrnk(lc[o],v);//如果节点o的值>v,说明v在此时的节点的左子树上,向左子树搜索v
if(val[o]<v) return queryrnk(rc[o],v)+siz[lc[o]]+cnt[o];//如果节点o的值<v,说明v在此时的节点的右子树上,向右子树搜索v,答案+=该节点左节点的排名+该节点重复的个数
}
查找排名为k的元素
- 在一棵子树中,根节点的排名取决于其左子树的大小。
- 若其左子树的大小大于等于 k,则该元素在左子树中;
- 若其左子树的大小在区间[k-cnt,k-1] ( cnt为当前结点的值的出现次数)中,则该元素为子树的根节点;
- 若其左子树的大小小于 k-cnt,则该元素在右子树中。
- 时间复杂度 O(h)。
inline int querykth(int o,int k){//查找在以o为根节点的二叉搜索树中排名为k的元素的值
if(siz[lc[o]]>=k) return querykth(lc[o],k);//如果o的左节点的子树大小>=k,k在o的左子树中
if(siz[lc[o]]<k-cnt[o]) return querykth(rc[o],k-siz[lc[o]]-cnt[o]);//如果o的左节点的子树的大小+o节点重复的个数(就是o的左子树的大小+cnt[o]-1)<k,说明该元素的排名>o节点排名,该元素在o右子树上,查询时k-=siz[lc[o]]+cnt[o],相当于在以rc[o]为根节点的二叉搜索数上重新查询
return val[o];//排名为k的节点是一颗树的根节点或叶子节点(此时o在它所在的树中的排名为k),返回val[o],即o的值(如果要求排名为k的节点的编号)
}
二叉搜索树模板
#include<bits/stdc++.h>
using namespace std;
//二叉搜索树:左子树所有值小于根节点,右子树所有值大于根节点,且左子树和右子树均为根节点
int lc[100005],rc[100005];//lc为左子树 rc为右子树,lc[i]表示i节点的左节点编号 rc[i] 表示i节点的右节点编号
inline int findmin(int o){//查找最小值
if(!lc[o]) return o;//找到第一个没有左子树的节点,由二叉查找树的性质知,此时该节点值为最小值,返回节点编号
return findmin(lc[o]);//一直向左子树找
}
inline int findmax(int o){//查找最大值
if(!rc[o]) return o;//找到第一个没有右子树的节点,由二叉查找树的性质知,此时节点值大于左子树所有节点值,该节点值即为最大值,返回节点编号
return findmax(rc[o]);//一直向右子树找
}
int val[100005],cnt[100005],siz[100005];//val[i]表示节点i的值,cnt[i]表示节点i出现的次数,siz[i]表示节点i的子树的大小
int num=0,o=1;
inline void insert(int &o,int v){//在以o为根节点的二叉搜索树中插入一个值为v的节点
if(!o){//如果o没有在树上
val[o=++num]=v;//加入一个值为v,编号为++n的节点
cnt[o]=siz[o]=1;//初始化节点o的出现次数和子树大小
lc[o]=rc[o]=0;//将左右节点初始值赋为0
return;
}
siz[o]++;//o还是在树上,更新最终得到的o点的所有祖先节点的子树大小
if(val[o]>v) insert(lc[o],v);//如果节点o的值>v,说明v在此时的节点的左子树上,向左子树插入v
if(val[o]<v) insert(rc[o],v);//如果节点o的值<v,说明v在此时的节点的右子树上,向右子树插入v
}
inline int deletemin(int &o){//查询最小值
if(!lc[o]){//如果左子树为空
int u=o;
o=rc[o];//找到最小值,更新节点,原本o点的值更新为最小值的右节点
return u;//返回最小值
}else{//如果左子树不为空
int u=deletemin(lc[o]);//继续查找
siz[o]-=cnt[u];//沿途的节点的子树大小-=u的个数
return u;
}
}
inline void del(int &o,int v){//在以o为根节点的二叉查找树中删除一个值为v的节点
siz[o]--;//o节点的子树大小减一
if(val[o]==v){//如果o的值和v相同(找到了)
if(cnt[o]>1){//如果o点不止一个
cnt[o]--;//直接把cnt[o]-1
return;
}
if(lc[o]&&rc[o]) o=deletemin(rc[o]);//如果o节点左右子树均不为空,用左子树的最大值或右子树的最小值来替换o(此处用的是右子树的最小值)
else o=lc[o]+rc[o];//如果左子树为空或右子树为空或左右子树均为空(此处代码浓缩):当左子树为空时,lc[o]为0,o点被右节点替换;当右子树为空时,rc[o]为0,o被左节点替换;当左右子树均为空时lc[o]=rc[o]=0,o点变为0;
return;
}
if(val[o]>v) del(lc[o],v);//如果节点o的值>v,说明v在此时的节点的左子树上,向左子树搜索v
if(val[o]<v) del(rc[o],v);//如果节点o的值<v,说明v在此时的节点的右子树上,向右子树搜索v
}
inline int queryrnk(int o,int v){//求一个值为v的元素在以o为根节点的二叉搜索树中的排名
if(val[o]==v) return siz[lc[o]]+1;//如果val[o]==v,找到v了,返回该节点左节点的排名+1
if(val[o]>v) return queryrnk(lc[o],v);//如果节点o的值>v,说明v在此时的节点的左子树上,向左子树搜索v
if(val[o]<v) return queryrnk(rc[o],v)+siz[lc[o]]+cnt[o];//如果节点o的值<v,说明v在此时的节点的右子树上,向右子树搜索v,答案+=该节点左节点的排名+该节点重复的个数
}
inline int querykth(int o,int k){//查找在以o为根节点的二叉搜索树中排名为k的元素的值
if(siz[lc[o]]>=k) return querykth(lc[o],k);//如果o的左节点的子树大小>=k,k在o的左子树中
if(siz[lc[o]]<k-cnt[o]) return querykth(rc[o],k-siz[lc[o]]-cnt[o]);//如果o的左节点的子树的大小+o节点重复的个数(就是o的左子树的大小+cnt[o]-1)<k,说明该元素的排名>o节点排名,该元素在o右子树上,查询时k-=siz[lc[o]]+cnt[o],相当于在以rc[o]为根节点的二叉搜索数上重新查询
return val[o];//排名为k的节点是一颗树的根节点或叶子节点(此时o在它所在的树中的排名为k),返回val[o],即o的值(如果要求排名为k的节点的编号)
}
int n,m;
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
int x;
cin>>x;
insert(o,x);
}
for(int i=1;i<=m;i++){
int q;
cin>>q;
if(q==1){
cout<<findmin(1)<<endl;
}
else if(q==2) cout<<findmax(1)<<endl;
else if(q==3){
int v;
cin>>v;
del(o,v);
}else if(q==4){
int v;
cin>>v;
cout<<queryrnk(1,v);
}else if(q==5){
int v;
cin>>v;
cout<<querykth(1,v);
}
}
return 0;
}
练习推荐:
https://www.luogu.com.cn/problem/P1864
萌新的第一篇文章,不喜勿喷。
给个免费的赞和关注再走吧求求了ヾ(≧▽≦*)o
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下