1、 概述
二叉查找樹(Binary Search Tree,也叫二叉排序樹,即Binary Sort Tree)能夠支持多種動態集合操作,它可以用來表示有序集合、建立索引等,因而在實際應用中,二叉排序樹是一種非常重要的數據結構。
從算法復雜度角度考慮,我們知道,作用於二叉查找樹上的基本操作(如查找,插入等)的時間復雜度與樹的高度成正比。對一個含n個節點的完全二叉樹,這些操作的最壞情況運行時間為O(log n)。但如果因為頻繁的刪除和插入操作,導致樹退化成一個n個節點的線性鏈(此時即為一個單鏈表),則這些操作的最壞情況運行時間為O(n)。為了克服以上缺點,很多二叉查找樹的變形出現了,如紅黑樹、AVL樹,Treap樹等。
本文介紹了二叉查找樹的一種改進數據結構–伸展樹(Splay Tree)。它的主要特點是不會保證樹一直是平衡的,但各種操作的平攤時間復雜度是O(log n),因而,從平攤復雜度上看,二叉查找樹也是一種平衡二叉樹。另外,相比於其他樹狀數據結構(如紅黑樹,AVL樹等),伸展樹的空間要求與編程復雜度要小得多。
2、 基本操作
伸展樹的出發點是這樣的:考慮到局部性原理(剛被訪問的內容下次可能仍會被訪問,查找次數多的內容可能下一次會被訪問),為了使整個查找時間更小,被查頻率高的那些節點應當經常處於靠近樹根的位置。這樣,很容易得想到以下這個方案:每次查找節點之后對樹進行重構,把被查找的節點搬移到樹根,這種自調整形式的二叉查找樹就是伸展樹。每次對伸展樹進行操作后,它均會通過旋轉的方法把被訪問節點旋轉到樹根的位置。
為了將當前被訪問節點旋轉到樹根,我們通常將節點自底向上旋轉,直至該節點成為樹根為止。“旋轉”的巧妙之處就是在不打亂數列中數據大小關系(指中序遍歷結果是全序的)情況下,所有基本操作的平攤復雜度仍為O(log n)。
伸展樹主要有三種旋轉操作,分別為單旋轉,一字形旋轉和之字形旋轉。為了便於解釋,我們假設當前被訪問節點為X,X的父親節點為Y(如果X的父親節點存在),X的祖父節點為Z(如果X的祖父節點存在)。
(1) 單旋轉
節點X的父節點Y是根節點。這時,如果X是Y的左孩子,我們進行一次右旋操作;如果X 是Y 的右孩子,則我們進行一次左旋操作。經過旋轉,X成為二叉查找樹T的根節點,調整結束。
(2) 一字型旋轉
節點X 的父節點Y不是根節點,Y 的父節點為Z,且X與Y同時是各自父節點的左孩子或者同時是各自父節點的右孩子。這時,我們進行一次左左旋轉操作或者右右旋轉操作。
(3) 之字形旋轉
節點X的父節點Y不是根節點,Y的父節點為Z,X與Y中一個是其父節點的左孩子而另一個是其父節點的右孩子。這時,我們進行一次左右旋轉操作或者右左旋轉操作。
3、伸展樹區間操作
在實際應用中,伸展樹的中序遍歷即為我們維護的數列,這就引出一個問題,怎么在伸展樹中表示某個區間?比如我們要提取區間[a,b],那么我們將a前面一個數對應的結點轉到樹根,將b 后面一個結點對應的結點轉到樹根的右邊,那么根右邊的左子樹就對應了區間[a,b]。原因很簡單,將a 前面一個數對應的結點轉到樹根后, a 及a 后面的數就在根的右子樹上,然后又將b后面一個結點對應的結點轉到樹根的右邊,那么[a,b]這個區間就是下圖中B所示的子樹。
利用區間操作我們可以實現線段樹的一些功能,比如回答對區間的詢問(最大值,最小值等)。具體可以這樣實現,在每個結點記錄關於以這個結點為根的子樹的信息,然后詢問時先提取區間,再直接讀取子樹的相關信息。還可以對區間進行整體修改,這也要用到與線段樹類似的延遲標記技術,即對於每個結點,額外記錄一個或多個標記,表示以這個結點為根的子樹是否被進行了某種操作,並且這種操作影響其子結點的信息值,當進行旋轉和其他一些操作時相應地將標記向下傳遞。
與線段樹相比,伸展樹功能更強大,它能解決以下兩個線段樹不能解決的問題:
(1) 在a后面插入一些數。方法是:首先利用要插入的數構造一棵伸展樹,接着,將a 轉到根,並將a 后面一個數對應的結點轉到根結點的右邊,最后將這棵新的子樹掛到根右子結點的左子結點上。
(2) 刪除區間[a,b]內的數。首先提取[a,b]區間,直接刪除即可。
4、實現
代碼全部來自【參考資料2】。
(1)旋轉操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
// node 為結點類型,其中ch[0]表示左結點指針,ch[1]表示右結點指針
// pre 表示指向父親的指針
// Rotate函數用於(左/右)旋轉x->pre
void
Rotate(node *x,
int
d)
// 旋轉操作,d=0 表示左旋,d=1 表示右旋
{
node *y = x->pre;
Push_Down(y), Push_Down(x);
// 先將Y 結點的標記向下傳遞(因為Y 在上面),再把X 的標記向下傳遞
y->ch[! d] = x->ch[d];
if
(x->ch[d] != Null) x->ch[d]->pre = y;
x->pre = y->pre;
if
(y->pre != Null)
if
(y->pre->ch[0] == y) y->pre->ch[0] = x;
else
y->pre->ch[1] = x;
x->ch[r] = y, y->pre = x, Update(y);
// 維護Y 結點
if
(y == root) root = x;
// root 表示整棵樹的根結點
}
|
(2)splay操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
void
Splay(node *x, node *f)
// Splay 操作,表示把結點x 轉到結點f 的下面
{
for
(Push_Down(x) ; x->pre != f; )
// 一開始就將X 的標記下傳
if
(x->pre->pre == f)
// 父結點的父親即為f,執行單旋轉
if
(x->pre->ch[0] == x) Rotate(x, 1);
else
Rotate(x, 0);
else
{
node *y = x->pre, *z = y->pre;
if
(z->ch[0] == y)
if
(y->ch[0] == x)
Rotate(y, 1), Rotate(x, 1);
// 一字形旋轉
else
Rotate(x, 0), Rotate(x, 1);
// 之字形旋轉
else
if
(y->ch[1] == x)
Rotate(y, 0), Rotate(x, 0);
// 一字形旋轉
else
Rotate(x, 1), Rotate(x, 0);
// 之字形旋轉
}
Update(x);
// 最后再維護X 結點
}
|
(3)將第k個數轉到要求的位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
// 找到處在中序遍歷第k 個結點,並將其旋轉到結點f 的下面
void
Select(
int
k, node *f)
{
int
tmp;
node *t;
for
(t = root; ; )
// 從根結點開始
{
Push_Down(t);
// 由於要訪問t 的子結點,將標記下傳
tmp = t->ch[0]->size;
// 得到t 左子樹的大小
if
(k == tmp + 1)
break
;
// 得出t 即為查找結點,退出循環
if
(k <= tmp)
// 第k 個結點在t 左邊,向左走
t = t->ch[0];
else
// 否則在右邊,而且在右子樹中,這個結點不再是第k 個
k -= tmp + 1, t = t->ch[1];
}
Splay(t, f);
// 執行旋轉
}
|
5、 應用
(1) 數列維護問題
題目:維護一個數列,支持以下幾種操作:
1. 插入:在當前數列第posi 個數字后面插入tot 個數字;若在數列首位插入,則posi 為0。
2. 刪除:從當前數列第posi 個數字開始連續刪除tot 個數字。
3. 修改:從當前數列第posi 個數字開始連續tot 個數字統一修改為c 。
4. 翻轉:取出從當前數列第posi 個數字開始的tot 個數字,翻轉后放入原來的位置。
5. 求和:計算從當前數列第posi 個數字開始連續tot 個數字的和並輸出。
6. 求和最大子序列:求出當前數列中和最大的一段子序列,並輸出最大和。
(2) 輕量級web服務器lighttpd中用到數據結構splay tree.
6、 參考資料
(1) 楊思雨《伸展樹的基本操作與應用》
(2) Crash《運用伸展樹解決數列維護問題》
MiYu原創, 轉帖請注明 : 轉載自 ______________白白の屋
伸展樹(Splay Tree)是AVL樹不錯的替代,它有以下幾個特點:
(1)它是二叉查找樹的改進,所以具有二叉查找樹的有序性。
(2)對伸展樹的操作的平攤復雜度是O(log2n)。
(3)伸展樹的空間要求、編程難度非常低。
提到伸展樹,就不得不提到AVL樹和Read-Black樹,雖然這兩種樹能夠保證各種操作在最壞情況下都為logN,但是兩都實現都比較復雜。而在實際情況中,90%的訪問發生在10%的數據上。因此,我們可以重構樹的結構,使得被經常訪問的節點朝樹根的方向移動。盡管這會引入額外的操作,但是經常被訪問的節點被移動到了靠近根的位置,因此,對於這部分節點,我們可以很快的訪問。這樣,就能使得平攤復雜度為logN。
1、自底向上的伸展樹
伸展操作Splay(x,S)是在保持伸展樹有序性的前提下,通過一系列旋轉操作將伸展樹S中的元素x調整至樹的根部的操作。
在旋轉的過程中,要分三種情況分別處理:
(1)Zig 或 Zag
(2)Zig-Zig 或 Zag-Zag
(3)Zig-Zag 或 Zag-Zig
1.1、Zig或Zag操作
節點x的父節點y是根節點。
1.2、Zig-Zig或Zag-Zag操作
節點x的父節點y不是根節點,且x與y同時是各自父節點的左孩子或者同時是各自父節點的右孩子。
1.3、Zig-Zag或Zag-Zig操作
節點x的父節點y不是根節點,x與y中一個是其父節點的左孩子而另一個是其父節點的右孩子。
2、自頂向下的伸展樹
在自底向上的伸展樹中,我們需要求一個節點的父節點和祖父節點,因此這種伸展樹難以實現。因此,我們可以構建自頂向下的伸展樹。
當我們沿着樹向下搜索某個節點X的時候,我們將搜索路徑上的節點及其子樹移走。我們構建兩棵臨時的樹──左樹和右樹。沒有被移走的節點構成的樹稱作中樹。在伸展操作的過程中:
(1)當前節點X是中樹的根。
(2)左樹L保存小於X的節點。
(3)右樹R保存大於X的節點。
開始時候,X是樹T的根,左右樹L和R都是空的。和前面的自下而上相同,自上而下也分三種情況:
2.1、Zig操作
如上圖,在搜索到X的時候,所查找的節點比X小,將Y旋轉到中樹的樹根。旋轉之后,X及其右子樹被移動到右樹上。很顯然,右樹上的節點都大於所要查找的節點。注意X被放置在右樹的最小的位置,也就是X及其子樹比原先的右樹中所有的節點都要小。這是由於越是在路徑前面被移動到右樹的節點,其值越大。
2.2、Zig-Zig操作
這種情況下,所查找的節點在Z的子樹中,也就是,所查找的節點比X和Y都小。所以要將X,Y及其右子樹都移動到右樹中。首先是Y繞X右旋,然后Z繞Y右旋,最后將Z的右子樹(此時Z的右子節點為Y)移動到右樹中。
2.3、Zig-Zag操作
這種情況中,首先將Y右旋到根。這和Zig的情況是一樣的,然后變成上圖右邊所示的形狀。此時,就與Zag(與Zig相反)的情況一樣了。
最后,在查找到節點后,將三棵樹合並。如圖:
2.4、示例:
下面是一個查找節點19的例子。在例子中,樹中並沒有節點19,最后,距離節點最近的節點18被旋轉到了根作為新的根。節點20也是距離節點19最近的節點,但是節點20沒有成為新根,這和節點20在原來樹中的位置有關系。
3、實現
3.1、splay操作

tree_node N, * l, * r, * y;
if (t == NULL)
return t;
N.left = N.right = NULL;
l = r = & N;
for (;;)
{
if (i < t -> item)
{
if (t -> left == NULL)
break ;
if (i < t -> left -> item)
{
y = t -> left; /* rotate right */
t -> left = y -> right;
y -> right = t;
t = y;
if (t -> left == NULL)
break ;
}
r -> left = t; /* link right */
r = t;
t = t -> left;
} else if (i > t -> item)
{
if (t -> right == NULL)
break ;
if (i > t -> right -> item)
{
y = t -> right; /* rotate left */
t -> right = y -> left;
y -> left = t;
t = y;
if (t -> right == NULL)
break ;
}
l -> right = t; /* link left */
l = t;
t = t -> right;
} else {
break ;
}
}
l -> right = t -> left; /* assemble */
r -> left = t -> right;
t -> left = N.right;
t -> right = N.left;
return t;
}
Rotate right(查找10):
Link right:
Assemble:
Rotate left(查找20):
Link left:
3.2、插入操作

**將i插入樹t中,返回樹的根結點(item值==i)
*/
tree_node * ST_insert( int i, tree_node * t) {
/* Insert i into the tree t, unless it's already there. */
/* Return a pointer to the resulting tree. */
tree_node * node;
node = (tree_node * ) malloc ( sizeof (tree_node));
if (node == NULL){
printf( " Ran out of space\n " );
exit( 1 );
}
node -> item = i;
if (t == NULL) {
node -> left = node -> right = NULL;
size = 1 ;
return node;
}
t = splay(i,t);
if (i < t -> item) { // 令t為i的右子樹
node -> left = t -> left;
node -> right = t;
t -> left = NULL;
size ++ ;
return node;
} else if (i > t -> item) { // 令t為i的左子樹
node -> right = t -> right;
node -> left = t;
t -> right = NULL;
size ++ ;
return node;
} else {
free(node); // i值已經存在於樹t中
return t;
}
}
3.3、刪除操作
**從樹中刪除i,返回樹的根結點
*/
tree_node * ST_delete( int i, tree_node * t) {
/* Deletes i from the tree if it's there. */
/* Return a pointer to the resulting tree. */
tree_node * x;
if (t == NULL)
return NULL;
t = splay(i,t);
if (i == t -> item) { /* found it */
if (t -> left == NULL) { // 左子樹為空,則x指向右子樹即可
x = t -> right;
} else {
x = splay(i, t -> left); // 查找左子樹中最大結點max,令右子樹為max的右子樹
x -> right = t -> right;
}
size -- ;
free(t);
return x;
}
return t; /* It wasn't there */
}
完整代碼:

#include < stdlib.h >
int size; // 結點數量
#define NUM 20
typedef struct tree_node{
struct tree_node * left;
struct tree_node * right;
int item;
}tree_node;
tree_node * splay ( int i, tree_node * t) {
tree_node N, * l, * r, * y;
if (t == NULL)
return t;
N.left = N.right = NULL;
l = r = & N;
for (;;)
{
if (i < t -> item)
{
if (t -> left == NULL)
break ;
if (i < t -> left -> item)
{
y = t -> left; /* rotate right */
t -> left = y -> right;
y -> right = t;
t = y;
if (t -> left == NULL)
break ;
}
r -> left = t; /* link right */
r = t;
t = t -> left;
} else if (i > t -> item)
{
if (t -> right == NULL)
break ;
if (i > t -> right -> item)
{
y = t -> right; /* rotate left */
t -> right = y -> left;
y -> left = t;
t = y;
if (t -> right == NULL)
break ;
}
l -> right = t; /* link left */
l = t;
t = t -> right;
} else {
break ;
}
}
l -> right = t -> left; /* assemble */
r -> left = t -> right;
t -> left = N.right;
t -> right = N.left;
return t;
}
/*
**將i插入樹t中,返回樹的根結點(item值==i)
*/
tree_node * ST_insert( int i, tree_node * t) {
/* Insert i into the tree t, unless it's already there. */
/* Return a pointer to the resulting tree. */
tree_node * node;
node = (tree_node * ) malloc ( sizeof (tree_node));
if (node == NULL){
printf( " Ran out of space\n " );
exit( 1 );
}
node -> item = i;
if (t == NULL) {
node -> left = node -> right = NULL;
size = 1 ;
return node;
}
t = splay(i,t);
if (i < t -> item) { // 令t為i的右子樹
node -> left = t -> left;
node -> right = t;
t -> left = NULL;
size ++ ;
return node;
} else if (i > t -> item) { // 令t為i的左子樹
node -> right = t -> right;
node -> left = t;
t -> right = NULL;
size ++ ;
return node;
} else {
free(node); // i值已經存在於樹t中
return t;
}
}
/*
**從樹中刪除i,返回樹的根結點
*/
tree_node * ST_delete( int i, tree_node * t) {
/* Deletes i from the tree if it's there. */
/* Return a pointer to the resulting tree. */
tree_node * x;
if (t == NULL)
return NULL;
t = splay(i,t);
if (i == t -> item) { /* found it */
if (t -> left == NULL) { // 左子樹為空,則x指向右子樹即可
x = t -> right;
} else {
x = splay(i, t -> left); // 查找左子樹中最大結點max,令右子樹為max的右子樹
x -> right = t -> right;
}
size -- ;
free(t);
return x;
}
return t; /* It wasn't there */
}
void ST_inoder_traverse(tree_node * node)
{
if (node != NULL)
{
ST_inoder_traverse(node -> left);
printf( " %d " , node -> item);
ST_inoder_traverse(node -> right);
}
}
void ST_pre_traverse(tree_node * node)
{
if (node != NULL)
{
printf( " %d " , node -> item);
ST_pre_traverse(node -> left);
ST_pre_traverse(node -> right);
}
}
void main() {
/* A sample use of these functions. Start with the empty tree, */
/* insert some stuff into it, and then delete it */
tree_node * root;
int i;
root = NULL; /* the empty tree */
size = 0 ;
for (i = 0 ; i < NUM; i ++ )
root = ST_insert(rand() % NUM, root);
ST_pre_traverse(root);
printf( " \n " );
ST_inoder_traverse(root);
for (i = 0 ; i < NUM; i ++ )
root = ST_delete(i, root);
printf( " \nsize = %d\n " , size);
}