有篇Splay入門必看文章 —— CSDN連接node
今天開始本身動手寫Splay。身邊的小夥伴大可能是用下標和數組來維繫各個結點的聯繫,可是我仍是一如既往的喜歡C++的指針(❤ ω ❤)。數組
結點以一個struct結構體的形式存在。安全
struct node { int value; node *father; node *son[2]; node (int v = 0, node *f = NULL) { value = v; father = f; son[0] = NULL; son[1] = NULL; } };
其中,son[0]表明左兒子,son[1]表明右兒子。框架
用一個函數來判斷子節點是父節點的哪一個兒子。函數
inline bool son(node *f, node *s) { return f->son[1] == s; }
返回值就是son[]數組的下標,這個函數很方便。spa
最關鍵的是旋轉操做,有別於常見的zig,zag旋轉,我喜歡用一個函數實現其二者的功能,即rotate(x)表明將x旋轉到其父節點的位置上。.net
inline void rotate(node *t) { node *f = t->father; node *g = f->father; bool a = son(f, t), b = !a; f->son[a] = t->son[b]; if (t->son[b] != NULL) t->son[b]->father = f; t->son[b] = f; f->father = t; t->father = g; if (g != NULL) g->son[son(g, f)] = t; else root = t; }
函數會自行判斷x實在父節點的左兒子上仍是右兒子上,並自動左旋或右旋。這裏要注意改變祖父結點的兒子指針,以及結點的父親指針,切忌馬虎漏掉。同時還要事先作好特判,放止訪問非法地址。這裏用指針相較於用下標的一個好處就是,若是你不當心訪問了NULL即空指針,也是下標黨經常使用的0下標,指針寫法必定會RE,而下標寫法可能就不會崩潰,於是不易發現錯誤,致使一些較爲複雜而智障的錯誤。指針
而後是核心函數——Splay函數,貌似也有人叫Spaly的樣子,然而我並無考證什麼。Splay(x,y)用於將x結點旋轉到y結點的某個兒子上。特別地,Splay(x,NIL)表明將x旋轉到根節點的位置上。根節點的父親通常是NIL或0。code
inline void spaly(node *t, node *p) { while (t->father != p) { node *f = t->father; node *g = f->father; if (g == p) rotate(t); else { if (son(g, f) ^ son(f, t)) rotate(t), rotate(t); else rotate(f), rotate(t); } } }
這裏值得注意的是兩種雙旋。若是t(該節點),f(父親節點),g(祖父節點)造成了一條單向的鏈,即[右→右]或[左→左]這樣子,那麼就先對父親結點進行rotate操做,再對該節點進行rotate操做;不然就對該節點連續進行兩次rotate操做。據稱單旋無神犇,雙旋O(logN),這句話我也沒有考證,我的表示不想作什麼太多的探究,畢竟Splay的複雜度原本就挺玄學的了,並且專門卡單旋Splay的題也沒怎麼據說過。對了,這個雙旋操做和AVL的雙旋是否是有那麼幾分類似啊,雖然仍是不太同樣的吧,好吧其實也不怎麼像╮(╯-╰)╭。blog
接下來談談插入操做。插入操做就和普通的二叉搜索樹相似,先找到合適的葉子結點,而後在空着的son[]上新建結點,把值放入。不一樣的是須要把新建的結點Splay到根節點位置,複雜度須要,不要問爲何。
inline void insert(int val) { if (root == NULL) root = new node(val, NULL); for (node *t = root; t; t = t->son[val >= t->value]) { if (t->value == val) { spaly(t, NULL); return; } if (t->son[val >= t->value] == NULL) t->son[val >= t->value] = new node(val, t); } }
注意,這個插入函數實現的是非重集合。
與之對應的就是刪除操做,相對的複雜一些。刪除一個元素,須要先在樹中找到這個結點,而後把這個結點Splay到根節點位置,開始分類討論。若是這個結點沒有左兒子(左子樹),直接把右兒子放在根的位置上便可;不然的話就須要千方百計合併左右子樹:在左子樹種找到最靠右(最大)的結點,把它旋轉到根節點的兒子上,此時它必定沒有右兒子,由於根節點的左子樹中不存在任何一個元素比它更大,那麼把根節點的右子樹接在這個結點的右兒子上便可。
inline void erase(int val) { node *t = root; for ( ; t; ) { if (t->value == val) break; t = t->son[val > t->value]; } if (t != NULL) { spaly(t, NULL); if (t->son[0] == NULL) { root = t->son[1]; if (root != NULL) root->father = NULL; } else { node *p = t->son[0]; while (p->son[1] != NULL) p = p->son[1]; spaly(p, t); root = p; root->father = NULL; p->son[1] = t->son[1]; if (p->son[1] != NULL) p->son[1]->father = p; } } }
相較於insert()確實複雜了很多。
以上就是Splay的框架了,是Splay必不可少的部分,在此基礎上能夠加入許多新的功能。
例如,手動實現垃圾回收,這樣新建結點的常數會小不少,畢竟C++的new是很慢的。
node tree[siz], *stk[siz]; int top; inline node *newnode(int v, node *f) { node *ret = stk[--top]; ret->size = 1; ret->value = v; ret->father = f; ret->son[0] = NULL; ret->son[1] = NULL; ret->reverse = false; return ret; } inline void freenode(node *t) { stk[top++] = t; }
有的時候須要咱們維護子樹大小。
inline int size(node *t) { return t == NULL ? 0 : t->size; } inline void update(node *t) { t->size = 1; t->size += size(t->son[0]); t->size += size(t->son[1]); }
簡單,安全。
維護區間翻轉的時候須要用到打標記的方式。
inline bool tag(node *t) { return t == NULL ? false : t->reverse; } inline void reverse(node *t) { if (t != NULL) t->reverse ^= true; } inline void pushdown(node *t) { if (tag(t)) { std::swap(t->son[0], t->son[1]); reverse(t->son[0]); reverse(t->son[1]); t->reverse ^= true; } }
還有更爲簡潔的rotate函數。
inline void connect(node *f, node *s, bool k) { if (f == NULL) root = s; else f->son[k] = s; if (s != NULL) s->father = f; } inline void rotate(node *t) { node *f = t->father; node *g = f->father; bool a = son(f, t), b = !a; connect(f, t->son[b], a); connect(g, t, son(g, f)); connect(t, f, b); update(f); update(t); }
@Author: YouSiki