Splay入門
BST與Splay
二叉查找樹(BST),保證任意節點的左兒子小於其父親,任意節點的右兒子大於其父親的二叉樹。但是當出現毒瘤數據時,BST會退化為鏈,從而影響效率。而Splay是其中的一種比較萬能的填坑方法。
Rotate
Splay基本旋轉操作。在不破壞二叉查找樹(BST)結構的前提下,將一個節點向上旋轉一層,使其曾經的父親成為他現在的兒子(圖中x節點)
這種旋轉模式可以找出普遍規律的,這里不多闡述,引用一下yyb神犇總結的
1.X變到原來Y的位置
2.Y變成了 X原來在Y的 相對的那個兒子
3.Y的非X的兒子不變 X的 X原來在Y的 那個兒子不變
4.X的 X原來在Y的 相對的 那個兒子 變成了 Y原來是X的那個兒子
請結合圖和代碼理解一下
void Rotate(int x){//旋轉節點x
//k表示x是否為y的右節點;y即圖中y節點,x即圖中x節點,z即圖中A節點
int y=ff[x],z=ff[y],k=(ch[y][1]==x);
//將x與y位置互換,並更新其父親
ch[z][ch[z][1]==y]=x;
ff[x]=z;
//將圖中D節點從x的右兒子變為y的左二子,k^1表示0,1取反(0^1=1,1^1=0)
ch[y][k]=ch[x][k^1];
ff[ch[x][k^1]]=y;
//將y更新
ch[x][k^1]=y;
ff[y]=x;
}
/*
ff[x]表示x的父親
ch[x][1]表示x的右節點
ch[x][0]表示x的左節點
1^1=0 1^0=1
*/
這樣,每次有新節點加入、刪除或查詢時,都將其旋轉至根節點,這樣可以保持BST的平衡。
Splay為什么能讓BST保持平衡的玄學原理很多博客未提及。自己yy了一天,搞出了個理由,表述不嚴謹,意會一下。粗略證明:對於隨機生成的數據,裸BST本來就可以平衡,而Splay這種旋轉行為的本身對於數據也是隨機性的,所以最后還是可以平衡;對於毒瘤單調遞增或遞減的數據,裸BST不能平衡,效率低的原因可以看做是因為樹退化成鏈,也可以看做是因為每一個新節點在插入時都需要比較一些嚴重脫離當前插入數據范圍(趨勢)的數據(如插入1,2,3……10,1000,1001,1002……1010時,每次插入大於等於1000的數時,裸BST每次都要先和前10個比較大小,但是其實這是不必要的,因為前10個數遠小於插入的數,如果像這樣每次都要訪問這些低頻節點,會大大增加其復雜度),而每次的Splay操作就是使根節點盡量符合當前插入數據的趨勢,避免冗余的比較,讓那些低頻節點訪問次數降低。證畢
Splay
然而單純的Rotate操作還是不夠,有些情況需要考慮,同上,記y為當前需要旋轉的節點x的父親,z為y的父親(也是x的祖父),\(k(x,y)\)表示節點x,y的關系(x為y的右兒子還是為y的左兒子),特別的,當\(k(x,y)=k(y,z)\)(或者即x,y,z三點共線)時,兩次單旋對於復雜度沒有優化,如圖:
我們必須要先將其父節點向上旋轉一次,再將要旋轉的節點向上旋轉一次,如圖:
其他情況則直接做兩次旋轉即可
inline void splay(int x, int goal){ //將x旋轉直至成為goal的兒子
while(ff[x]!=goal){
int y=ff[x],z=ff[y];
if(z!=goal) //如果y已經是根節點的兒子了,那么只需要將x向上旋轉一次就好了,不需要兩次旋轉
((ch[z][0]==y)^(ch[y][0]==x))?rotate(x):rotate(y); //x,y,z三點共線是否三點一線
rotate(x);//再旋轉一下
}
if(goal==0) rot=x; //更新樹根(0是樹根的父親)
}
查找操作
非遞歸,比較簡單,查找后,平衡樹的根(rot)就是查找到的節點
/*
rot維護了這棵平衡樹的樹根
val[x]獲取節點x的值
*/
inline void find(int x){
int u=rot; //rot為樹根
if(u==0) return; //樹空
while(ch[u][x>val[u]]!=0&&x!=val[u]) //節點存在(即不為0)並且不是x,才進入到下一層
u=ch[u][x>val[u]]; //進入到相應的子樹中
splay(u,0); //每次查詢都要將節點旋轉至樹根,原理前文已提
}
插入
inline void insert(int x){
int fa=0,u=rot;
while(u!=0&&x!=val[u]){
fa=u;
u=ch[u][x>val[u]];
}
if(u!=0) //x存在
cnt[u]++; //已有x,那么增加其個數
else{ //沒有x存在
u=tot++; //分配一個新的節點編號
if(fa==0) //新建一個樹根
rot=u; //更新樹根
else //新建葉節點
ch[fa][x>val[fa]]=u; //更新其父親的信息
//維護節點的其他信息
val[u]=x;
ff[u]=fa;
cnt[u]=1;
size[u]=1;
//ch[u][0]=ch[u][1]=0;
}
splay(u,0);
}
Update
根據Splay自底向上旋轉的性質,根據左右兒子的節點大小(size)以維護當前節點大小(用於求第k小問題)
void update(int x){
size[x]=size[ch[x][0]]+sizep[ch[x][1]]; //左右兒子
}
每次Rotate改變樹形狀時調用
NEW Rotate
void Rotate(int x){
//代碼不變
int y=ff[x],z=ff[y],k=(ch[y][1]==x);
ch[z][ch[z][1]==y]=x;
ff[x]=z;
ch[y][k]=ch[x][k^1];
ff[ch[x][k^1]]=y;
ch[x][k^1]=y;
ff[y]=x;
//只有節點x,y的大小發生了變化(看圖)
update(y),update(x);
}
前驅/后驅
前驅:比x小的最大節點;后驅:比x大的最小節點
先找到該節點,根據BST性質,其前驅即其左子樹最右邊的節點(進入其左兒子之后一直向右轉),其后驅即其右子樹最左邊的節點(進入其右兒子之后一直向左轉)
前驅
inline int pre(int x){
find(x); //查找后,此時樹根即為查詢節點
int u=ch[rot][0]; //進入左子樹
if(u==0) return -1; // 沒有比x小的數
while(ch[u][1]!=0) u=ch[u][1]; //一路向右
return u;
}
后驅
inline int nxt(int x){
find(x); //查找后,此時樹根即為查詢節點
int u=ch[rot][1]; //進入右子樹
if(u==0) return -1; // 沒有比x大的數
while(ch[u][0]!=0) u=ch[u][0]; //一路向左
return u;
}
刪除
根據前驅后驅的性質可得
(即同時滿足\(pre(x) < x < nxt(x)\)的x只有一個)那么我們可以根據這個性質x這一個節點夾逼到某個確定的位置,然后干凈地干掉(無需維護其他信息)
具體先將x的前驅旋至樹根,再旋轉x的后驅,使x的后驅成為樹根的兒子,這時我們會發現x被夾逼到樹根的右兒子的左兒子(或者后驅節點的左兒子)
inline void delete(int x){
int xp=pre(x),xn=nxt(x);
splay(xp, 0); //將x的前驅旋至樹根
splay(xn, rot); //旋轉x的后驅,使x的后驅成為樹根的兒子
int u=ch[xn][0]; //即將被刪除的節點
if(cnt[u]>1){ //如果不止一個節點
cnt[u]--; //那么將其個數減一即可
splay(u,0); //記得Splay!
}else
ch[xn][0]=0; //干凈地干掉
}
第k大
inline int findk(int x){
int u=rot;
if(size[u]<x) return -1; //不存在
while(1){
if(x<=size[ch[u][0]]+cnt[u]) u=ch[u][0]; //如果左子樹大小加節點副本數(cnt)大於x,那么第k大一定在左子樹中,進入左子樹
else if(x==size[ch[u][0]]+cnt[u]) return u; //如果左子樹大小加節點副本數(cnt)恰等於x,那么第k大就是當前節點
else u=ch[u][1], x-=size[ch[u][0]]+cnt[u]; //如果左子樹大小加節點副本數(cnt)小於x,那么第k大一定在右子樹中,進入左子樹,但是要同時減去左子樹的個數
}
}
參考
個人覺得寫的很好的博客:
- [Splay入門解析【保證讓你看不懂(滑稽)】](https://www.cnblogs.com/cjyyb/p/7499020.html)
- Splay入門詳解
- More Senior Data Structure · 特別淺地淺談Splay
本文采用 知識共享 署名-非商業性使用-相同方式共享 3.0 中國大陸 許可協議進行許可。歡迎轉載,請注明出處: 轉載自:Santiego的博客