BST初探
定义
BST(二叉搜索树)是一种树形结构,有如下性质。
- 空树是二叉搜索树。
- 若左子树非空,那么左子树上所有点的权值均小于其根节点的值。
- 若左子树非空,那么其右子树上所有点的权值均大于其根节点的值。
- 二叉搜索树的左右子树均为二叉搜索树。
操作
BST 的单次操作最坏为
节点定义
struct node{
int val, ch[2], size;//节点权值,左/右儿子,子树大小
}d[N];
#define ls(x) d[x].ch[0]
#define rs(x) d[x].ch[1]
int tot, root;//第几个节点没用,根节点
int newnode(int val){//返回新建节点的编号
int w = ++tot;
d[w].val = val, ls(w) = rs(w) = 0, d[w].size = 1;
return w;
}
void pushup(int x){//更新子树大小。
d[x].size = d[ls(x)].size + d[rs(x)].size + 1;
}
插入
我们设插入节点权值为
从根节点开始,我们需要满足 BST 的第
如果当前是空节点那么就直接新建。
那么如果
否则根据第
void insert(int&now, int val){
if(!now)return void(now = newnode(val));
if(d[now].val < val)insert(rs(now), val);
else insert(ls(now), val);
pushup(now);
}
删除
我们设删除节点权值为
如果当前节点为空,说明没有。
否则如果
否则目标节点在左子树。
我们现在找到了要删除的节点。
但是如果要删除的节点有儿子节点呢?
如果它有一个儿子,我们可以直接用它的儿子顶替它的位置。
如果它有两个儿子,那么我们找到它的后缀,将它的权值变成其后缀的权值,然后继续向下删除后继即可。
我们只需要先向右走一步,然后一直往左走,最后一个就是后继了。
void del(int&now, int val){
if(!now)return;//空节点返回
if(d[now].val == val){//找到目标节点
int w = now;
if(ls(now) && (w = rs(now))){
while(ls(w))w = ls(w);//和后缀交换。
d[now].val = d[w].val, del(rs(now), d[w].val);//删除后缀
}
else now = ls(now) ? ls(now) : rs(now);//被儿子顶替
}
else if(d[now].val < val)del(rs(now), val);
else del(ls(now), val);
if(now)pushup(now);
}
查 x 排名
如果
否则,进入左子树。
平衡树内可能有好几个权值为
int query_rank(int val){
int ans = 1, now = root;
while(now)
if(d[now].val < val)ans += d[d[now].ls].size + 1, now = d[now].rs;
else now = d[now].ls;
return ans;
}
第 k 小
如果
如果
否则我们减去左儿子的大小,进入右子树。
int kth(int x){
int now = root, siz = 0, z = x;
while(now){
if((siz = d[ls(now)].size + 1) == x)return d[now].val;//找到节点
else now = ((siz > x) ? ls(now) : (x -= siz, rs(now)));
}
return -1;
}
前驱
第一种:
我们可以先找到
int ask_pre(int val){return kth(query_rank(val) - 1);}
第二种:
若当前节点是叶子节点,如果
否则,如果
否则,用左儿子更新答案,进入右儿子。
int ask_pre(int val){
int now = root, ans = -1e9;
while(now)
if(d[now].val >= val)now = ls(now);
else ans = d[now].val, now = rs(now);
return ans;
}
后继
询问
第一种:
int ask_next(int val){return kth(query_rank(val + 1));}
第二种:
和前驱相差不大。
int ask_next(int val){
int now = root, ans = 1e9;
while(now){
if(d[now].val <= val)now = rs(now);
else ans = d[now].val, now = ls(now);
}
return ans;
}
优化
树的形态是链的时候,BST 的时间复杂的会退化成
那么我们可以尽可能的让树尽可能的不变成链。
能解决这个问题的就是平衡树。
平衡树大多会定义一个平衡状态,在树不满足平衡时,会通过旋转来改变树的形态。
接下来我会列举几个比较常见的平衡树:
- AVL:最早发明的自平衡二叉树,要使左右子树的高度差不大于
。 - RBT(红黑树):由 B 树改良,需要满足自身的
个性质。 - 旋转 treap,无旋treap:将 tree 和 heap 结合,来保证树的形态。
- splay:将要操作的点旋转到根,做到均摊复杂度。
- WBLT:由线段树和加权平衡树结合,常数优秀。
- B 树:不再是二叉树,而是多叉树,由于信息紧贴,在不区分堆栈的磁盘会更优秀。
- 替罪羊树:加权平衡树的另一种实现,在树不平衡时,直接推倒重建。
旋转
rotate
操作是把某个给定节点上移一个位置,并保证二叉搜索树的性质不改变。
旋转操作分为左旋和右旋(图上节点是编号)。
我们来模拟一下右旋的操作(红色是要删除的,蓝色是更改后的)。
这样就完成了一次旋转。
而在实现中,我会把左右旋写在一起。
这里的 rotate(x, 0)
表示将
这里的 rotate(x, 1)
表示将
void rotate(int&now, int dir){
int t = d[now].ch[dir];
d[now].ch[dir] = d[t].ch[!dir];
d[t].ch[!dir] = now;
pushup(now), pushup(t), now = t;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!