Splay入門
目錄
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保持平衡 的
,避免冗餘的比較,讓那些低頻節點訪問次數降低。
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; }
刪除
根據前驅後驅的性質可得
\[ MIN,\cdots,pre(x),x,nxt(x),\cdots,MAX \]
(即同時滿足
\(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大一定在右子樹中,進入左子樹,但是要同時減去左子樹的個數 } }
參考
個人覺得寫的很好的部落格: