伸展樹(Splay Tree)樹平衡二叉查找樹的一種,具有二叉查找樹的所有性質。在性能上又比普通的二叉查找樹有所改進:普通的二叉查找樹在最壞情況下的查找操作的時間復雜度為O(n)(當二叉樹退化成一條鏈的時候),而伸展樹在任何情況下的平攤時間復雜度均為 O(log2n).
特性
- 和普通的二叉查找樹相比,具有任何情況下、任何操作的平攤O(log2n)的復雜度,時間性能上更好
- 和一般的平衡二叉樹比如 紅黑樹、AVL樹相比,維護更少的節點額外信息,空間性能更優,同時編程復雜度更低
- 在很多情況下,對於查找操作,后面的查詢和之前的查詢有很大的相關性。這樣每次查詢操作將被查到的節點旋轉到樹的根節點位置,這樣下次查詢操作可以很快的完成
- 可以完成對區間的查詢、修改、刪除等操作,可以實現線段樹和樹狀數組的所有功能
旋轉
伸展樹實現O(log2n)量級的平攤復雜度依靠每次對伸展樹進行查詢、修改、刪除操作之后,都進行旋轉操作 Splay(x, root),該操作將節點x旋轉到樹的根部。
伸展樹的旋轉有六種類型,如果去掉鏡像的重復,則為三種:zig(zag)、zig-zig(zag-zag)、zig-zag(zag-zig)。
1 自底向上的方式進行旋轉
這種方式需要每個節點存放其父節點的指
1.1 zig旋轉
如圖所示,x節點的父節點為y,x為y的左子節點,且y節點為根。則只需要對x節點進行一次右旋(zig操作),使之成為y的父節點,就可以使x成為伸展樹的根節點。
1.2 zig-zig旋轉
如上圖所示,x節點的父節點y,y的父節點z,三者在一字型鏈上。此時,先對y節點和z節點進行zig旋轉,然后再對x節點和y節點進行zig旋轉,最后變為右圖所示,x成為y和z的祖先節點。
1.3 zig-zag旋轉
如上圖所示,x節點的父節點y,y的父節點z,三者在之字型鏈上。此時,先對x節點和y節點進行zig旋轉,然后再對x節點和y節點進行zag旋轉,最后變為右圖所示,x成為y和z的祖先節點。
2 自頂向下的方式進行旋轉
這種方式不需要節點存儲其父節點的指針。當我們沿着樹向下搜索某個節點x時,將搜索路徑上的節點及其子樹移走。構建兩棵臨時的樹——左樹和右樹。沒有被移走的節點構成的樹稱為中樹。
(1) 當前節點x是中樹的根
(2) 左樹L保存小於x的節點
(3) 右樹R保存大於x的節點
開始時候,x是樹T的根,左樹L和右樹R都為空。三種旋轉操作:
2.1 zig旋轉
如圖所示,x節點的子節點y就是我們要找的節點,則只需要對y節點進行一次右旋(zig操作),使之成為x的父節點,就可以使y成為伸展樹的根節點。將y作為中樹的根,同時,x節點移動到右樹R中,顯然右樹上的節點都大於所要查找的節點。
2.2 zig-zig旋轉
如上圖所示,x節點的左子節點y,y的左子節點z,三者在一字型鏈上,且要查找的節點位於z節點為根的子樹中。此時,對x節點和y節點進行zig,然后對z和y進行zig,使z成為中樹的根,同時將y及其子樹掛載到右樹R上。
2.3 zig-zag旋轉
如上圖所示,x節點的左子節點y,y的右子節點z,三者在之字型鏈上,且需要查找的元素位於以z為根的子樹上。此時,先對x節點和y節點進行zig旋轉,將x及其右子樹掛載到右樹R上,此時y成為中樹的根節點;然后再對z節點和y節點進行zag旋轉,使得z成為中樹的根節點。
2.4 合並
最后,找到節點或者遇到空節點之后,需要對左、中、右樹進行合並。如圖所示,將左樹掛載到中樹的最左下方(滿足遍歷順序要求),將右樹掛載到中樹的最右下方(滿足遍歷順序要求)。
父節點向左到左子節點-> zig
父節點向右到右子節點->zag
舉例說明旋轉操作
Original
zig-zag (double rotation)
zig-zig
zig (single rotation at root)
伸展樹的基本操作
利用Splay操作,可以在伸展樹上進行如下操作:
(1) Find(x, S) 判斷x是否在伸展樹S表示的有序集中
首先按照普通的二叉查找樹查找算法進行查找,如果找到元素x,則執行Splay(x, S)操作將x旋轉到樹根的位置。
(2) Insert(x, S) 將元素x插入到樹中
首先按照普通的二叉查找樹插入算法進行插入,然后執行Splay(x, S)
(3) Delete(x, S) 將元素x從伸展樹S所表示的有序集中刪除
首先按照普通的二叉查找樹查找算法找到x的位置。如果x沒有孩子或只有一個孩子,則直接將x刪除,並通過Splay操作,將x節點的父節點調整到伸展樹的根節點處。否則,向下查找x的后繼節點y,用y替代x的位置,然后執行Splay(y, S),將y調整為伸展樹的根
(4) Join(S1, S2) 將兩個伸展樹S1, S2合並為一個伸展樹。其中S1的所有元素小於S2中的所有元素。
首先按照普通的二叉查找樹查找算法找到S1中最大元素x,然后執行Splay(x, S1)將x旋轉到S1的根部,此時S1中的所有元素必然在x的左子樹上,x的右子樹為空,則可以將S2掛載到x的右子樹位置。
(5) Split(x, S) 以x為界,將伸展樹S分離為兩棵伸展樹S1和S2,其中S1中所有元素都小於x,S2中所有元素都大於x。
首先執行Find(x, S),將元素x調整為伸展樹的根節點,則x的左子樹就是S1,右子樹就是S2.
伸展樹Splay(x,S)實現(c++)
1 自底向上的旋轉方式
struct TreeNode{ int data; TreeNode* left; TreeNode* right; TreeNode* parent; TreeNode(int d) : data(d), left(NULL), right(NULL), parent(NULL){}; }; TreeNode* gTreeRoot; void Rotate(TreeNode* x, bool left_rotate){ //旋轉x節點(將x節點 按照 left_rotate 指示 繞着其父節點y 進行左旋或者右旋 TreeNode* y = x->parent; if (y == NULL){ return; } if (left_rotate){ y->right = x->left; if (!x->left){ x->left->parent = y; } } else{ y->left = x->right; if (!x->right){ x->right->parent = y; } } x->parent = y->parent; if (!y->parent){ if (y == y->parent->left){ y->parent->left = x; } else{ y->parent->right = x; } } if (y == gTreeRoot){ //全局的根節點 gTreeRoot = x; } } //將節點x通過不斷的Rotate操作,直到x成為f的子節點 void Splay(TreeNode* x, TreeNode* f){ TreeNode* y = x->parent, *z = NULL; while (y != f){ z = y->parent; if (z == f){ Rotate(x, x == y->right); } else{ if (!(x == y->left ^ y == z->left)){ //一字型 旋轉 zig-zig Rotate(y, y == z->right); Rotate(x, x == y->right); } else{ //之字型旋轉 zig-zag Rotate(x, x == y->right); //注意,上一步的rotate操作,x的地址沒有發生改變,但是x地址指向的結構體中的各個域被修改為經過旋轉之后的結構 //所有,這里直接使用x即可 Rotate(x, x == z->right); } } } }
2 自頂向下的方式旋轉
struct TreeNode{ int data; TreeNode* left; TreeNode* right; TreeNode(int d = 0) : data(d), left(NULL), right(NULL){}; }; TreeNode* Splay(int i, TreeNode* t){ TreeNode N, *l, *r, *y; // l, t, r 分別為左樹、中樹、右樹 if (t == NULL){ return; } l = r = &N; while (true){ if (i < t->data){ if (t->left == NULL){ //碰到空節點,結束 break; } if (i < t->left->data){ //需要進行右旋 y = t->left; t->left = y->right; y->right = t; t = y; if (t->left == NULL){ break; } } r->left = t; //掛載到右樹,最小的位置 r = t; t = t->left; //將 z 升為中樹的根節點 } else if (i > t->data){ if (t->right == NULL){ break; } if (i > t->right->data){ //需要進行左旋 y = t->right; t->right = y->left; y->left = t; } l->right = t; //掛載到左樹,最大的位置 l = t; t = t->right; //將z升為中樹的根節點 } else{ break; } } l->right = t->left; //將左、中、右樹進行合並 r->left = t->right; t->left = N.right; t->right = N.left; return t; }