資料結構之伸展樹(二)
阿新 • • 發佈:2018-10-31
之前寫了一篇Splay的部落格【資料結構之伸展樹(一)】,只是說了一下了它的原理及核心的伸展操作,後來發現具體在哪裡應用splay我還是分不大清。
事實上,Splay常常用於實現可分裂與合併的序列,舉個板栗,比如給你一個數組,將陣列從某一個地方分成倆陣列,或者給你倆陣列,將他們直接連線成一個數組——這就是Splay的強項
就像上次寫的伸展樹部落格裡面說的,要處理一個區間[L,R]只需要將L-1伸展到根,R+1伸展到根的右兒子,然後就好操作了。先說分裂操作:將前k個結點從原來的splay裡面分離出來,只需要將第k個結點伸展到根,然後讓根與右子樹斷開連線,就分好了~然後再說合並,將合併後放左邊的的Splay的最大的結點伸展到根,然後另外一棵Splay作為它的右子樹~
上面操作裡,"第k個結點","最大的結點",怎麼求這倆玩意兒是關鍵~在實現中,我們用一個結構體來存結點,在結構體里加一個s,以儲存以這個結點為根的子樹有多少個結,這樣如果一棵樹的左子樹的s是k-1,那麼它自己就是第k個結點了,至於最大的結點,從根向右兒子遞迴下去,直到沒有右兒子為止,就像普通BST一樣
附上程式碼(順便加上了splay翻轉的程式碼)
// Splay Tree #include<bits/stdc++.h> struct Node // Splay Tree結點的定義 { Node* ch[2]; //左右子樹 int v; //鍵值(1~n),表示這個結點是第v大的,鍵值成BST int s; //以它為根的子樹的總結點數 int flip; //延遲標記————是否需要翻轉,如果不需要區間反轉就不需要這個變數 Node(int v=0):v(v) { ch[0] = ch[1] = NULL; s = 1;flip = 0;} int cmp(const int& x) const // 第x大的元素在左子樹還是右子樹(或者是其本身?) { int t = ch[0] == NULL ? 0 : ch[0]->s; if(x == t+1) return -1; return x <= t ? 0 : 1; } void maintain() { s = 1; if(ch[0] != NULL) s += ch[0]->s; if(ch[1] != NULL) s += ch[1]->s; } void pushdown() // 延遲標記的下沉函式,如果不需要區間反轉就不需要這個函式 { if(flip) { Node* p = ch[0]; ch[0] = ch[1]; ch[1] = p; flip = 0; if(ch[0] != NULL) ch[0]->flip ^= 1; if(ch[1] != NULL) ch[1]->flip ^= 1; } } }; void rotate(Node* &o, int d)//d=0代表左旋,d=1代表右旋,最終o仍然指向根 { Node* k = o->ch[d^1]; o->ch[d^1] = k->ch[d]; k->ch[d] = o; o->maintain(); k->maintain(); o = k; } void insert(Node* &o, int x)//在以o為根的子樹插入鍵值x,修改o繼續為根節點(假設沒有x) { if(o == NULL) o = new Node(x); else { int d = (x < o->v ? 0 : 1); insert(o->ch[d], x); } o->maintain(); } // 將int陣列a[n]轉化成伸展樹,中序遍歷出來仍然是a[](如果原來無序,建樹後仍然無序) void build(Node* &rt, int a[], int n) { if(n <= 2) { for(register int i = 0; i < n; ++ i) insert(rt, a[i]); return; } insert(rt, a[n/2]); build(rt, a, n/2); build(rt, a+n/2+1, n-n/2-1); } void remove(Node* &o, int x)//在以o為根的子樹中刪去元素第x大的元素,修改o繼續為根節點 { int d = o->cmp(x);//如果需要刪除元素x(x存在SPT裡的話),只需要修改這一句就好 if(d == -1) { if(o->ch[0] == NULL) o = o->ch[1]; else if(o->ch[1] = NULL) o = o->ch[0]; else { int d2 = (o->ch[0]->s > o->ch[1]->s ? 1 : 0); rotate(o, 0); remove(o->ch[0], x); } } else remove(o->ch[d], x); if(o != NULL) o->maintain(); } void splay(Node* &o, int k)//找到第k大的元素並伸展到根 { o->pushdown();//如果沒有區間反轉就不需要這一句 int d = o->cmp(k);//看第k小的數是在左子樹還是右子樹 int t = o->ch[0] == NULL ? 0 : o->ch[0]->s; if(d == 1) k -= t + 1;//如果在右子樹,那麼o的第k小數就是是右子樹的第 (k - (o->ch[0]->s + 1)) 小數(也就是減去左子樹節點數以及o結點) if(d != -1)//只需要考慮d!=-1,因為當d==-1,第k個元素就在根上 { Node* p = o->ch[d];//直接找那棵子樹 p->pushdown();//如果沒有區間反轉就不需要這一句 int d2 = p->cmp(k);//同上面的d t = p->ch[0] == NULL ? 0 : p->ch[0]->s; int k2 = (d2 == 0 ? k : k - t - 1);//同上面的if(d == 1) if(d2 != -1) { splay(p->ch[d2], k2); if(d == d2) //加上最後的旋轉構成一字雙旋 rotate(o, d^1); else //加上最後的旋轉構成之字雙旋 rotate(o->ch[d], d); } rotate(o, d^1);//配合if(d2!=-1)裡面內容構成雙旋,或者if條件不成立,即單旋 } } Node* merge(Node* left, Node* right)//合併left和right。假定left的所有元素比right小。注意right可以是null,但left不可以 { splay(left, left->s); left->ch[1] = right; left->maintain(); return left; } // 把o的前k小結點放在left裡,其它的放在right裡。1<=k<=o->s。當k=o->s時,right=null void split(Node* o, int k, Node* &left, Node* &right) { splay(o, k); left = o; right = o->ch[1]; o->ch[1] = NULL; left->maintain(); } void print(Node* o) // 中序遍歷輸出splayTree { o->pushdown();//如果沒有區間反轉就不需要這一句 if(o->ch[0] != NULL) print(o->ch[0]); printf("%d\n", o->v); if(o->ch[1] != NULL) print(o->ch[1]); }
之前做一個Splay的題,給一個序列,然後中間截一段翻轉一下,放到後面,輸出新的序列。當時就在想,splay是一棵BST,為什麼BST這樣弄完還會保持BST的性質。不知道有沒有人和我想的一樣~
後來我想明白了,與其說Splay的鍵值構成BST,不如說Splay的索引值構成BST,也就是說a[1]恆在a[2]前面,a[m]恆在a[m+1]前面~~這樣想上面的問題就好理解了,翻轉放到後面,實際上是對索引值的修改,修改了索引值,然後通過伸展操作來維護其BST特性