普通平衡树

ouyu69 发布于 2025-02-18 30 次阅读


二叉查找树(BST) 是一种能存储特定数据类型的容器。二叉查找树允许快速查找,插入或者删除一节点。重要性质:左小右大,中序遍历是有序的。

Splay(伸展树) 是一种平衡二叉树,它通过将一个节点旋转到根结点,使得整个棵树仍然满足 BST 的性质,并且保持平衡而不至于退化成链。


1.旋转

image-20250218104648961

struct node{
    int s[2] ;//左右儿子
    int p ; //父亲
    int v ;//节点权值
    int cnt;//权值出现次数
    int size;// 子树大小
    void init(int p1, int v1){
        p = p1, v = v1 ;
        cnt = size = 1 ;
    }
}tr[N];
int root ;//根节点编号
int idx ;//节点个数
void rotate(int x){//旋转,x为旋转后往上走的节点
    //y 为当前节点的 父节点, z 为原先当前节点父节点的父节点
    int y = tr[x].p , z = tr[y].p ; 
    int k = tr[y].s[1] == x ;//k = 1 说明左旋 反之说明右旋
    //以左旋为例
    //旋转后, 当前节点的左儿子节点变成原先当前节点父节点的右儿子节点
    tr[y].s[k] = tr[x].s[k^1] ;
    //同时当前节点的左儿子节点的父节点变成原先当前节点的父节点
    tr[tr[x].s[k^1]].p = y ;
    //当前节点的左儿子节点变成原先当前节点父节点
    tr[x].s[k^1] = y ;
    //当前节点的父节点的父节点变成原先当前节点
    tr[y].p = x ;
    //当前父节点的父节点的儿子节点变成原先当前节点,替换原先父亲节点的位置
    tr[z].s[tr[z].s[1]==y] = x ;
    //当前节点的父节点变成原先父节点的父节点
    tr[x].p = z ;
    pushup(x) ;
    pushup(y) ; 
}
void pushup(x){
    tr[x].size = tr[tr[x].s[0]].size + tr[tr[x].s[1]].size + tr[x].cnt ;
}

2. 伸展

访问一个节点 就要把 旋转到根结点。

image-20250218105111073

//k >0 时,把 x 转到 k 下面
//k = 0 时,把 x 转到根
void splay(int x,int k){
    while(tr[x].p != k){
        int y = tr[x].p, z = tr[y].p ;
        if(z!= k){//直线型先转y后转x。折线形转两次x
            ((tr[z].s[0] == y) ^ (tr[y].s[0] == x)) : rotate(x) ? rotate(y) ;
        }
        rotate(x) ;
    }
    if(k == 0) root = x ;
}

3. 查找 find

找到 v 所在节点并把其转到根上

void find(int v){
    int x = root ;
    while(tr[x].s[v > tr[x].v] && v! = tr[x].v){//两个条件, 第一个是找不到目标节点, 第二个时,找到目标节点
        x = tr[x].s[v > tr[x].v] ;
    splay(x,0) ;
    }
}

4. 前驱 get_pre

的前驱, 返回其节点编号

int get_pre(int v){
    find(v) ;
    int x = root ;
    if(tr[x].v < v) retrun x ; //说明当前根节点就是 v 的前驱
    //如果 tr[x].v = v,那么v 的前驱就一定在树伸展后的左子树的右下角
    //如果 tr[x].v > v,因为此时的根节点与v一定是最接近的,所以此时v 大于当前根节点左儿子的v,也就是说其前驱也一样是当前伸展后根节点左子树的右下角
    x = tr[x].s[0] ;//得到左子树右下角
    while(tr[x].s[1]) x = tr[x].s[1] ;
    return x ;
}

5. 后继 get_suc

的后继, 返回其节点编号

//和前驱同理
int get_suc(int v){
    find(v) ;
    int x = root ;
    if(tr[x].v > v) retrun x ; //说明当前根节点就是 v 的后继驱
    x = tr[x].s[1] ;//得到右子树左下角
    while(tr[x].s[0]) x = tr[x].s[0] ;
    return x ;
}

6. 删除

删除 (如果有多个相同的,只删除一个)

void del(int v){
    int pre = get_pre(v) ;//把前驱节点放在根节点
    int suc = get_suc(v) ;//后继节点放在根节点之下
    splay(pre,0), splay(suc,pre) ;
    //伸展后,比我们要删除的节点大的节点在suc的右子树上, 比其小的在 pre的左子树上,
    int del = tr[suc].s[0] ;//也就是说我们现在要删除的节点没有子节点,为叶子节点
    if(tr[del].cnt > 1){
        tr[del].cnt --, splay(del,0) ;
    }else{
        tr[suc].s[0] = 0, splay(suc,0) ;
    }
}

7. 插入

void insert(int v){
    int x = root, p = 0 ;
    while(x && tr[x],v !=v ){
         p = x , x = tr[p].s[v>tr[x].v]
    }
    if(x) tr[x].cnt++ ;//该值第一次出现
    else{//新出现的值
        x = ++ idx ;
        tr[p].s[v>tr[p].v] = x ;//记录新的节点是谁的哪个儿子节点
        tr[x].init(p,v) ;
    }
    splay(x, 0);
}

 

8. 查询数据排名(该数也有可能不存在)

int get_rank(int v){
    insert(v) ;//先添加一下这个值,可能不存在
    int res = tr[tr[root].s[0]].size ;//为什么不用+1是因为还有哨兵节点无穷小
    del(v);//最后删除,因为是辅助用,本不应该存在
    return res ;
}

9. 查询排名为 的数值

int get_val(int k){
    int x = root ;
    while(1){
        int y = tr[x].s[0] ;
        if(tr[y].size() + tr[x].cnt < k){//说明目标在右子树上
            k -= tr[y].size() + tr[x].cnt ;
            x = tr[x].s[1] ;
        }else{
            if(tr[y].size() >= k) x = tr[x].s[0] ;
            else break ;
        }
    }
    splay(x,0) ;//避免变成链
    return tr[x].v ;
}

完整代码

#include<bits/stdc++.h>
#define int long long 
#define lc(x) tr[x].s[0] ;
#define rc(x) tr[x].s[1] ;
using namespace std;
const int N = 1e6 + 10;
const int INF = 0x3f3f3f3f3f3f3f3f ;
struct node
{
    int v ;//权值大小
    int p ;//父亲节点
    int cnt ; //权值数量
    int siz ; //树的大小
    int s[2] ; //儿子节点
    void init(int p1, int v1){
        p = p1 , v = v1 ;
        cnt = siz = 1 ;

    }
}tr[N];
int root ; //表示根节点的编号
int idx ; //表示新的节点的编号
void pushup(int x){
    tr[x].siz = tr[tr[x].s[0]].siz + tr[tr[x].s[1]].siz + tr[x].cnt ;
}
void rotate(int x){//旋转节点
    int y = tr[x].p, z = tr[y].p ;
    int k = tr[y].s[1] == x ;//1说明要左旋
    tr[y].s[k] = tr[x].s[k^1] ;//原来节点的左儿子,变成父节点的右儿子节点
    tr[tr[x].s[k^1]].p = y ;//原来节点的左儿子节点的父亲节点变成原先节点的父亲节点
    tr[x].s[k^1] = y ;//原来节点的左儿子变成其父亲节点
    tr[y].p = x ;
    tr[z].s[tr[z].s[1] == y] = x ;
    tr[x].p = z ;
    //更新树 大小
    pushup(x) ;
    pushup(y) ;
}
void splay(int x, int k){//伸展 两个参数, 第一个参数所代表的节点要变成 第二个参数的节点的儿子节点 
    while(tr[x].p != k){
        int y = tr[x].p , z = tr[y].p ;
        if(z != k){
            (tr[z].s[0] == y) ^ (tr[y].s[0] == x) ? rotate(x) : rotate(y) ;
        }
        rotate(x) ;
    }
    if(k == 0) root = x ;
}
void insert(int v){
    int x = root , p = 0 ;
    while(x && tr[x].v != v){
        p = x , x = tr[p].s[v > tr[x].v] ;
    }

    if(x){//说明不是新的点
        tr[x].cnt ++ ;
    }else{
        x = ++idx ;
        tr[p].s[v > tr[p].v] = x ;
        tr[x].init(p,v) ;
    }
    splay(x,0) ;
}
void find(int v){//把v做权值所在节点转移到根节点上
    int x = root ;
    while(tr[x].s[v > tr[x].v] && tr[x].v != v){//还有可能目标节点不存在
        x = tr[x].s[v > tr[x].v] ;
    }
    splay(x,0) ;
}
int get_pre(int v){
    find(v) ;
    int x = root ;
    if(tr[x].v < v) return x ;
    x = tr[x].s[0] ;
    while(tr[x].s[1]){
        x = tr[x].s[1] ;
    }
    return x ;
}

int get_suc(int v){
    find(v) ;
    int x = root ;
    if(tr[x].v > v) return x ;
    x = tr[x].s[1] ;
    while(tr[x].s[0]){
        x = tr[x].s[0] ;
    }
    return x ;
}

void del(int v){//要把目标节点放在叶子节点上
    //让后继变成前驱的右儿子, 让当前变成后继的左儿子
    int pre = get_pre(v) ;
    int suc = get_suc(v) ;
    splay(pre,0), splay(suc, pre) ;
    int del = tr[suc].s[0] ;//这就是目标删除的节点
    if(tr[del].cnt > 1){
        tr[del].cnt -- ;
        splay(del,0) ;
    }else{
        tr[suc].s[0] = 0 ;
        splay(suc,0) ;
    }
}

int get_rank(int v){
    insert(v) ;
    int res = tr[tr[root].s[0]].siz ;
    del(v) ;
    return res ;
}
int get_val(int k){
    int x = root ;
    while(1){
        if(k <= tr[tr[x].s[0]].siz + tr[x].cnt){
            if(k > tr[tr[x].s[0]].siz){
                break ;
            }else{
                x = tr[x].s[0] ;
            }
        }else{
            k -= tr[tr[x].s[0]].siz + tr[x].cnt ;
            x = tr[x].s[1] ; 
        }
    }
    return x ;

}
int n ;
void solve(){
    cin >> n ;
    int op ;
    int v ;
    //两个哨兵
    insert(INF) ;
    insert(-INF) ;
    while(n -- ){
        cin >> op ;
        cin >> v ;
        if(op == 1){
            insert(v) ;
        }else if(op == 2){
            del(v) ;
        }else if(op == 3){
            int res = get_rank(v) ;
            cout << res << endl ;
        }else if(op == 4){
            int res = get_val(v + 1) ;
            res = tr[res].v ;
            cout << res << endl ;
        }else if(op == 5){
            int res = get_pre(v) ;
            res = tr[res].v ;
            cout << res << endl ;
        }else{
            int res = get_suc(v) ;
            res = tr[res].v ;
            cout << res << endl ;
        }
    }

}

signed main()
{
    solve() ;
}

 

我打算法竞赛,真的假的。
最后更新于 2025-02-18