【部落格】Splay

zysssss發表於2024-03-19

Splay

前言

學到Splay 感覺網上的資料多了好多

果然是常用的 那得好好學

Splay樹是一種平衡二叉查詢樹

它透過旋轉操作保持樹的平衡而不至於退化成鏈

能夠在均攤複雜度\(O(log N)\)時間內完成插入,查詢和刪除操作

提一句

Splay是Tarjan發明的

Tarjan好強


演算法介紹

二叉查詢樹

既然Splay是一種平衡的二叉查詢樹,我們得先知道普通的二叉查詢樹

二叉查詢樹(BST) 是具有下列性質的二叉樹

若它的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;

若它的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值;

它的左、右子樹也分別為二叉查詢樹

它的中序遍歷是有序的

附個圖

image

它能在最優\(O(log\) $ n)$的時間複雜度內完成查詢,修改等操作

但是 如果出題人不講武德

那就勸他好自為之

從小到大給資料 BST就退化成了一條鏈

時間複雜度達到了\(O(n)\)

於是我們要用Splay的旋轉操作避免退化


Splay

splay維護以下資訊

image

在每次操作時,Splay會透過一次次旋轉,將樹的結構改變(也就是伸展操作splay),把當前結點轉到樹根上

這個操作使得Splay的均攤時間複雜度為\(O(log\) $ n)$

所以到底怎麼旋轉呢


操作

旋轉Rotate(x)

旋轉操作:

將指定的結點x向上移動一級

將原有的父節點作為自己的兒子

並且維持BST的性質

旋轉分為左旋右旋 但是我們把他們結合在一起

詳情看程式碼 配合圖片食用更佳

image

#define lc(x) tree[x].ch[0]
#define rc(x) tree[x].ch[1]
#define fa(x) tree[x].fa
int root,cnt;
bool Check(int x)
//查詢x是在左子樹還是右子樹
//右子樹返回1 左子樹返回0
{
    return rc(fa(x))==x;
}
void Pushup(int now)
{
    tree[now].siz=tree[lc(now)].siz+tree[rc(now)].siz+tree[now].cnt;
}
void Rotate(int x)
{
    //y為x的父節點
    //z為y的父節點
    //選取不與x同側的x的子節點w 以維持BST性質
    int y=fa(x);
    int z=fa(y);
    int k=Check(x),w=tree[x].ch[k^1];
    tree[z].ch[Check(y)]=x,fa(x)=z;//把x接在z下
    tree[y].ch[k]=w,fa(w)=y;//把w接在y下
    tree[x].ch[k^1]=y,fa(y)=x;//把y接在x下
    Pushup(y),Pushup(x);
}

伸展Splay(x,target)

這個操作將x調整為target子節點

它分四種情況 需要分別處理

  1. target是x的父節點 結束
  2. target是x的父親的父親 rotate(x)

image

  1. 直線型(x的父親的父親不是target) rotate(y),rotate(x) 先父後兒

image

  1. 折線型(x的父親的父親不是target) rotate(x),rotate(x) 先兒後兒

image

如果我們想將x轉到根節點

只需要讓target等於0即可

Splay的程式碼如下

void Splay(int x,int target=0)
//如果呼叫的時候寫Splay(x) 預設target=0
//如果呼叫的時候寫Splay(x,target) target為傳入的值
{
    while(fa(x)!=tarjat)
    {
        int y=fa(x),z=fa(y);
        if(z!=target)
        {
            if(Check(x)==Check(y)) //直線型
                Rotate(y);
            else//折線形
                Rotate(x);
        }
        Rotate(x);
    }
    if(!target)
        root=x;
}

查詢Find(v)

判斷元素v是否在伸展樹表示的有序集中
並把它轉到根

設當前結點為x

若v大於x 向右子樹

若v小於x 向左子樹

若v等於x 找到了 把x轉到根

若訪問到空節點 表示沒找到

void Find(int val)
{
    int now=root;
    while(tree[now].ch[val>tree[now].val] && tree[now].val!=val)
        now=tree[now].ch[val>tree[now].val];
    Splay(now);
}

插入Insert(v)

跟查詢差不多

設當前結點為x

若v大於x 向右子樹

若v小於x 向左子樹

若v等於x 找到了 cnt++

訪問到空節點 新建一個點

void Insert(int val)
{
    int now=root,las=0;
    while(now && tree[now].val!=val)
    {
        las=now;
        now=tree[now].ch[val>tree[now].val];
    }
    if(now)//找到了 個數加1
        ++tree[now].cnt;
    else//沒找到 新建點
    {
        now=++cnt;
        if(las)
            tree[las].ch[val>tree[las].val]=now;
        lc(now)=rc(now)=0;
        tree[now].val=val;
        fa(now)=las;
        tree[now].siz=tree[now].cnt=1;
    }
    Splay(now);
}

其他操作

合併Join(u,v)

u,v分別是需要合併的子樹S1,S2的根節點

需要滿足S1的所有元素都小於S2的所有元素

首先找到S1的最大點x 轉到根

然後把S2當作x的右子樹即可

刪除Del(x)

先找到x 轉到根

然後把左右子樹合併

查詢排名Getrank(v)

先找到v所在的結點x 轉到根

排名為左子樹結點個數+1

分裂Split(v)

先找到v所在的結點x 轉到根

取出左右子樹即可


到此Splay就寫差不多了

附例題

P3369 【模板】普通平衡樹

P3391 【模板】文藝平衡樹


引用來源

Splay入門_splay zigzag-CSDN部落格

Splay 樹 - OI Wiki (oi-wiki.org)

二叉搜尋樹_百度百科 (baidu.com)

還有書