快速入门Splay
\(splay\) :伸展樹(\(Splay Tree\)),也叫分裂樹,是一種二叉排序樹,它能在\(O(log n)\)內完成插入、查找和刪除操作。它由\(Daniel Sleator\)和\(Robert Tarjan\)創造,后勃剛對其進行了改進。它的優勢在于不需要記錄用于平衡樹的冗余信息。在伸展樹上的一般操作都基于伸展操作。
先讓我們看一下一棵二叉搜索樹(\(Binary\) \(Search\) \(Tree\))是什么樣子的。
如圖所示,對任意一棵\(BST\),它有以下性質:
- 是一棵空樹,或者是具有下列性質的二叉樹
- 若它的左子樹不空,則左子樹上所有結點的值均小于它的根結點的值;
- 若它的右子樹不空,則右子樹上所有結點的值均大于它的根結點的值;
根據定義,我們會發現:
- 這棵樹的中序遍歷,與其壓成數組的升序排序等效
在這樣一棵樹中,我們可以很容易地維護以下信息:
- 查詢\(x\)數的排名
- 查詢排名為\(x\)的數
- 求\(x\)的前驅(前驅定義為小于\(x\),且最大的數)
- 求\(x\)的后繼(后繼定義為大于\(x\),且最小的數)
同樣的我們會發現,對于一個固定的數列,它可以形成很多種不同類型的\(BST\)。如果這棵樹恰好不太優美,每次維護的復雜度可能會被卡到\(O(N)\)(一條鏈)。
所以,平衡樹這種偉大的數據結構就誕生啦!
顧名思義,平衡樹就是一棵可以保持全樹平衡的二叉搜索樹,以此避免復雜度退化為\(O(N)\)。比較經典的一種平衡樹是\(Treap\),它基于的是對一個有序數列,隨機出的\(BST\)期望復雜度是\(O(logN)\),通過利用堆的性質來維護其隨機性,這個東西我的上一篇博客已經介紹過,不再展開介紹。今天我們要介紹的是另一種經典的平衡樹——\(Splay\)。
既然是平衡樹,\(Splay\)是如何實現其樹體平衡的呢?
在\(Splay\)的每一個維護操作中,維護結束后當前被維護的點都會被旋轉成為樹的根節點,這個過程叫做樹的伸展。\((Splay)\)。伸展是\(Splay\)的核心操作。與\(Treap\)利用隨機出來的優先級進行堆的維護不同,\(Splay\)的大多數操作都要基于伸展操作,這也決定了\(Splay\)相比前者具有更廣泛的適用性。
那\(Splay\)是怎么保證其復雜度不退化成\(O(N)\)的呢?來看個例子。
在這一棵已經退化成鏈的\(BST\)中,我們對最底下那個節點進行了一次維護。在這之后,這個節點就開始了向根節點的漫漫伸展之路~
所以在伸展過程結束后,這棵樹就再次自發地進化回了一棵正常的樹。如果深度更深會更加明顯,在一次\(Splay\)以后,它會從\(N\)級別的深度進化為\(logN\)級別。
接著讓我們貪心地想一想,假如現在這棵樹非常的不優秀。我想要把它卡掉,就應該總是訪問它最不優秀的節點。如果最開始它還有很多超級長的鏈,那么經過幾次貪心的訪問之后,它的所有鏈中的最大深度就已經回到\(logN\)了。不管常數怎么樣,均攤一下復雜度是沒有問題了。
既然這些操作\(Treap\)也能做,為什么不用又快又好寫的\(Treap\)呢?因為\(Splay\)在區間操作和\(LCT\)中有其不可替代的作用。具體是什么作用,我也沒有學到,等到學了在拿出來講吧QwQ
講過了原理,我們可以來看一下代碼實現了Qw
inline void push_up (int u) {t[u].sz = t[u].cnt;t[u].sz += t[t[u].ch[0]].sz;t[u].sz += t[t[u].ch[1]].sz; }inline void rotate (int x) {int y = t[x].fa;int z = t[y].fa;int d1 = t[y].ch[1] == x;int d2 = t[z].ch[1] == y;connect (z, x, d2);connect (y, t[x].ch[!d1], d1);connect (x, y , !d1); push_up (y);push_up (x); }這里\(connect\)是一個連邊的函數,旋轉的原理和\(Treap\)一樣,都是要保證其\(BST\)的性質,可以手畫一下示意圖就明白啦~
inline void splay (int x, int goal) {if (x == 0) return;while (t[x].fa != goal) {int y = t[x].fa;int z = t[y].fa;int d1 = t[y].ch[1] == x;int d2 = t[z].ch[1] == y;if (z != goal) {if (d1 == d2) {rotate (y);} else {rotate (x);}}rotate (x);}if (goal == 0) {root = x;} }核心操作——伸展,可以思考一下:為什么是把\(x\)旋轉為\(goal\)的子節點?
剩下的操作,作者很懶,就只貼上代碼啦~
inline void find (int key) {int u = root;if (u == 0) return;while (t[u].key != key && t[u].ch[key > t[u].key]) {u = t[u].ch[key > t[u].key];}//找到key對應的節點,并把它旋轉到根。splay (u, 0); }inline void Insert (int key) {int u = root, fa = 0;while (u != 0 && t[u].key != key) {fa = u;//記得記錄父親if (key > t[u].key) {u = t[u].ch[1];} else {u = t[u].ch[0];}}if (u != 0) {//已有(能查到)++t[u].sz;++t[u].cnt;} else {//新增u = ++max_size;t[u].sz = 1;t[u].cnt = 1;t[u].key = key;connect (fa, u, key > t[fa].key); }splay (u, 0); } inline int Next (int key, int dir) {//dir = 0 -> 前驅//dir = 1 -> 后繼find (key);int u = root;if (dir == 0 && t[u].key < key) return u;if (dir == 1 && t[u].key > key) return u; //如果key值并沒有存在于樹中:u = t[u].ch[dir];while (t[u].ch[!dir]) {u = t[u].ch[!dir];}//e.g 如果要找前驅,就先往左一步(保證一定比當前值更小),再一直向右(最大的那個)。return u; }inline void Delete (int key) {int _pre = Next (key, 0);int _nxt = Next (key, 1);splay (_pre, 0000);splay (_nxt, _pre);//當前鍵值key的前驅是_pre, 后繼是_nxt//_pre被旋轉到根節點,_nxt成為_pre的子節點(顯然是右)//那么當前點一定在_nxt的左邊,而且底下沒有任何一個點。int u = t[_nxt].ch[0];if (t[u].cnt > 1) {--t[u].cnt;splay (u, 0);} else {t[_nxt].ch[0] = 0;} }inline int kth (int k) {int u = root;if (u == 0) return 0;while (u != 0) {int ls = t[u].ch[0];int rs = t[u].ch[1];if (k > t[ls].sz + t[u].cnt) {k -= t[ls].sz + t[u].cnt;u = rs;//格外注意不要寫反順序} else if (k <= t[ls].sz) {u = ls;} else {return t[u].key;}}return false; }inline int get_rnk (int key) {find (key);return t[t[root].ch[0]].sz; }還有一點需要注意的,\(splay\)在使用前要先\(insert\)一個極大值和一個極小值。否則在\(Next\)函數的查找中,比如只有一個點的話,會出現找不到前驅和后繼的情況,也就會導致出莫名其妙的鍋。當然,加上極大極小值之后要格外注意對答案的處理。下面給出完整代碼,題目P3369 【模板】普通平衡樹。
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> #define N 100010 #define INF 0x7fffffff using namespace std;struct Splay_Tree {int root, max_size;struct Splay_Node {int sz, fa, cnt, key, ch[2];}t[N];Splay_Tree () {root = max_size = 0;memset (t, 0, sizeof (t));}inline void connect (int u, int v, int dir) {t[u].ch[dir] = v;t[v].fa = u;}inline void push_up (int u) {t[u].sz = t[u].cnt;t[u].sz += t[t[u].ch[0]].sz;t[u].sz += t[t[u].ch[1]].sz;}inline void rotate (int x) {int y = t[x].fa;int z = t[y].fa;int d1 = t[y].ch[1] == x;int d2 = t[z].ch[1] == y;connect (z, x, d2);connect (y, t[x].ch[!d1], d1);connect (x, y , !d1); push_up (y);push_up (x);} inline void splay (int x, int goal) {if (x == 0) return;while (t[x].fa != goal) {int y = t[x].fa;int z = t[y].fa;int d1 = t[y].ch[1] == x;int d2 = t[z].ch[1] == y;if (z != goal) {if (d1 == d2) {rotate (y);} else {rotate (x);}}rotate (x);}if (goal == 0) {root = x;} }inline void find (int key) {int u = root;if (u == 0) return;while (t[u].key != key && t[u].ch[key > t[u].key]) {u = t[u].ch[key > t[u].key];}splay (u, 0);}inline void Insert (int key) {int u = root, fa = 0;while (u != 0 && t[u].key != key) {fa = u;if (key > t[u].key) {u = t[u].ch[1];} else {u = t[u].ch[0];}}if (u != 0) {++t[u].sz;++t[u].cnt;} else {u = ++max_size;t[u].sz = 1;t[u].cnt = 1;t[u].key = key;connect (fa, u, key > t[fa].key); }splay (u, 0);} inline int Next (int key, int dir) {find (key);int u = root;if (dir == 0 && t[u].key < key) return u;if (dir == 1 && t[u].key > key) return u; u = t[u].ch[dir];while (t[u].ch[!dir]) {u = t[u].ch[!dir];}return u;}inline void Delete (int key) {int _pre = Next (key, 0);int _nxt = Next (key, 1);splay (_pre, 0000);splay (_nxt, _pre);int u = t[_nxt].ch[0];if (t[u].cnt > 1) {--t[u].cnt;splay (u, 0);} else {t[_nxt].ch[0] = 0;}}inline int kth (int k) {int u = root;if (u == 0) return 0;while (u != 0) {int ls = t[u].ch[0];int rs = t[u].ch[1];if (k > t[ls].sz + t[u].cnt) {k -= t[ls].sz + t[u].cnt;u = rs;//格外注意 } else if (k <= t[ls].sz) {u = ls;} else {return t[u].key;}}return false;}inline int get_rnk (int key) {find (key);return t[t[root].ch[0]].sz;} }st;int n, x, opt;int main () { // freopen ("splay.in", "r", stdin);scanf ("%d", &n);st.Insert (+INF);st.Insert (-INF);for (int i = 1; i <= n; ++i) {scanf ("%d %d", &opt, &x);if (opt == 1) {st.Insert (x);} if (opt == 2) {st.Delete (x);}if (opt == 3) {printf ("%d\n", st.get_rnk (x));}if (opt == 4) {printf ("%d\n", st.kth (x + 1));}if (opt == 5) {printf ("%d\n", st.t[st.Next (x, 0)].key);}if (opt == 6) {printf ("%d\n", st.t[st.Next (x, 1)].key);}} }轉載于:https://www.cnblogs.com/maomao9173/p/10297014.html
總結
- 上一篇: MySQL的库表详细操作
- 下一篇: Gitlab常用命令