二叉查找樹(BST),保證任意節點的左兒子小於其父親,任意節點的右兒子大於其父親的二叉樹。可是當出現毒瘤數據時,BST會退化爲鏈,從而影響效率。而Splay是其中的一種比較萬能的填坑方法。git
Splay基本旋轉操做。在不破壞二叉查找樹(BST)結構的前提下,將一個節點向上旋轉一層,使其曾經的父親成爲他如今的兒子(圖中x節點)github
這種旋轉模式能夠找出廣泛規律的,這裏很少闡述,引用一下yyb神犇總結的優化
1.X變到原來Y的位置
2.Y變成了 X原來在Y的 相對的那個兒子
3.Y的非X的兒子不變 X的 X原來在Y的 那個兒子不變
4.X的 X原來在Y的 相對的 那個兒子 變成了 Y原來是X的那個兒子spa
請結合圖和代碼理解一下code
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的平衡。htm
Splay爲何能讓BST保持平衡的玄學原理不少博客未說起。本身yy了一天,搞出了個理由,表述不嚴謹,意會一下。粗略證實:對於隨機生成的數據,裸BST原本就能夠平衡,而Splay這種旋轉行爲的自己對於數據也是隨機性的,因此最後仍是能夠平衡;對於毒瘤單調遞增或遞減的數據,裸BST不能平衡,效率低的緣由能夠看作是由於樹退化成鏈,也能夠看作是由於每個新節點在插入時都須要比較一些嚴重脫離當前插入數據範圍(趨勢)的數據(如插入1,2,3……10,1000,1001,1002……1010時,每次插入大於等於1000的數時,裸BST每次都要先和前10個比較大小,可是其實這是沒必要要的,由於前10個數遠小於插入的數,若是像這樣每次都要訪問這些低頻節點,會大大增長其複雜度),而每次的Splay操做就是使根節點儘可能符合當前插入數據的趨勢,避免冗餘的比較,讓那些低頻節點訪問次數下降。證畢blog
然而單純的Rotate操做仍是不夠,有些狀況須要考慮,同上,記y爲當前須要旋轉的節點x的父親,z爲y的父親(也是x的祖父),\(k(x,y)\)表示節點x,y的關係(x爲y的右兒子仍是爲y的左兒子),特別的,當\(k(x,y)=k(y,z)\)(或者即x,y,z三點共線)時,兩次單旋對於複雜度沒有優化,如圖:遞歸
咱們必需要先將其父節點向上旋轉一次,再將要旋轉的節點向上旋轉一次,如圖:get
其餘狀況則直接作兩次旋轉便可
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); }
根據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; //乾淨地幹掉 }
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大必定在右子樹中,進入左子樹,可是要同時減去左子樹的個數 } }
我的以爲寫的很好的博客:
本文采用 知識共享 署名-非商業性使用-相同方式共享 3.0 中國大陸 許可協議進行許可。歡迎轉載,請註明出處: 轉載自:Santiego的博客