Splay
前言
學到Splay 感覺網上的資料多了好多
果然是常用的 那得好好學
Splay樹是一種平衡二叉查詢樹
它透過旋轉操作保持樹的平衡而不至於退化成鏈
能夠在均攤複雜度\(O(log N)\)時間內完成插入,查詢和刪除操作
提一句
Splay是Tarjan發明的
Tarjan好強
演算法介紹
二叉查詢樹
既然Splay是一種平衡的二叉查詢樹,我們得先知道普通的二叉查詢樹
二叉查詢樹(BST) 是具有下列性質的二叉樹
若它的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;
若它的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值;
它的左、右子樹也分別為二叉查詢樹
它的中序遍歷是有序的
附個圖
它能在最優\(O(log\) $ n)$的時間複雜度內完成查詢,修改等操作
但是 如果出題人不講武德
那就勸他好自為之
從小到大給資料 BST就退化成了一條鏈
時間複雜度達到了\(O(n)\)
於是我們要用Splay的旋轉操作避免退化
Splay
splay維護以下資訊
在每次操作時,Splay會透過一次次旋轉,將樹的結構改變(也就是伸展操作splay),把當前結點轉到樹根上
這個操作使得Splay的均攤時間複雜度為\(O(log\) $ n)$
所以到底怎麼旋轉呢
操作
旋轉Rotate(x)
旋轉操作:
將指定的結點x向上移動一級
將原有的父節點作為自己的兒子
並且維持BST的性質
旋轉分為左旋和右旋 但是我們把他們結合在一起
詳情看程式碼 配合圖片食用更佳
#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的子節點
它分四種情況 需要分別處理
- target是x的父節點 結束
- target是x的父親的父親 rotate(x)
- 直線型(x的父親的父親不是target) rotate(y),rotate(x) 先父後兒
- 折線型(x的父親的父親不是target) rotate(x),rotate(x) 先兒後兒
如果我們想將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)
還有書