Skip to content

Lec1 AVL,Splay

Lec1: AVL+Splay

AVL

定义

\(h(x)\): 以\(x\)为根的子树的最大深度 - 空树\(h=-1\) 单个点\(h=0\)

平衡因子 \(BF(x)=h(lson(x))-h(rson(x))\) 一棵满足条件的AVL树 \(\forall x, BF(x) \in \{-1,0,1 \}\)

复杂度分析

设最大深度为k的树的最少节点数为\(h_k\),则\(h_{k+1}=h_{k}+h_{k-1}+1\) 类似斐波那契数列可以得到\(h_n=O(c^n)\)

也就是说树高是\(O(\log n)\)

操作

旋转

旋转是AVL和splay的基础,它能在不改变一棵合法 BST 中序遍历结果的情况下改变局部节点的深度。

以根节点为视角,旋转分为左旋和右旋:(图片来自OI-wiki)

rbtree-rotations

以儿子节点为视角,就可以不用区分左旋和右旋。实际上旋转是把孩子和父节点交换但保持BST性质的操作,左儿子交换上去叫右旋,右儿子交换上去叫左旋。

这样我们可以统一写出旋转操作的代码:

int chk(int x){return rson(fa(x))==x;}
void rotate(int x){
    //把x换到父亲的位置
    int y=fa(x),z=fa(y),k=chk(x),w=tree[x].ch[k^1];
    tree[y].ch[k]=w;
    fa(w)=y;
    if(z) tree[z].ch[chk(y)]=x;
    fa(x)=z;
    tree[x].ch[k^1]=y;
    fa(y)=x;
    pushup(y);
    pushup(x);
}

插入

插入一个点后,可能会破坏关系。称插入的点为Trouble maker,递归向上找到的第一个不满足关系的点为trouble finder

LL Rotation: 把B换到A的位置

image.png

trouble finder是A, 插入点在B的左儿子子树中(也有可能是那个点)

LR Rotation:CB互换,CA互换

image.png

void maintain(int x){
    if(abs(h(lson(x))-h(rson(x)))>=2){//如果>=2
        if(h(lson(x))>h(rson(x))){
            if(h(lson(lson(x)))>h(rson(lson(x)))){
                rotate(lson(x));//LL single rotation
            }else{
                rotate(rson(lson(x)));//LR double rotation
                rotate(lson(x));
            }
        }
        //... 右节点的情况对称

插入: 先按照BST插入节点,接着向上,找到第一个不满足AVL操作的点,消除这个点即可。因此AVL插入的rotate次数≤2

Splay

定义

操作

核心思想:操作某个点时(查询、插入、删除),就通过旋转将目标点转移到根部

splay

两种情况:

void splay(int x,int goal=0) { //把x转到goal的子节点,goal默认=0的时候转到根
        while(tree[x].fa!=goal) {
            int y=tree[x].fa;
            int z=tree[y].fa;
            if(z!=goal) {
                if(chk(x)==chk(y)) rotate(y);//zig-zig
                else rotate(x);//zig-zag
            }
            rotate(x);//如果z就是根,那么只需要zig一次
        }
        if(!goal) root=x;
    }
}

势能分析

势能分析的数学语言

让消耗大的那一步操作势能减小

multipop

有一个栈,两种操作 - Push 进去1个元素,消耗1时间 - 每次可以选择 Pop k个元素(只要栈里有),消耗k时间。 现在一共有n 个元素,问经过一系列合法操作之后总共消耗多少时间。

让消耗大的操作势能减少, 那么pop k的消耗比较大, 势能应该变小. 所以定义\(\Phi(S_k)\)代表此时栈中的元素个数。 最后栈中元素可能有0~n个,故\(\Phi(S_n) \geq \Phi(S_0)\)满足势能分析的条件

  • push \(a_i=1, \Phi_i-\Phi_{i-1}=1\)
  • pop \(a_i=k, \Phi_i-\Phi_{i-1}=-k\)

所以总时间\(\sum( a_i+\Phi_i-\Phi_{i-1}) \leq 2n\)

std::vector

如果有空位,就插入。否则新建一个大小为原来2倍的表,把原有元素复制进去. 假设最后表是满的

复制的操作对应势能减小。可以设势能为表中的空位个数的相反数 开始时为-1,最后为0,满足放缩条件。 - 情况1 \(a_i=1, \Phi_i-\Phi_{i-1}=1\) - 情况2 \(a_i=k, \Phi_i-\Phi_{i-1}=(-k)-0=-k\)

总时间\(O(n)\), 平均下来每次是\(O(1)\)

Splay

定义势函数 $$\Phi(T)=\sum_{i \in T} \log Size(i)

$$ \(Size(i)\)为i子树的大小. 为了方便,定义\(rank(i)\)为子树大小取对数

Comments