【轉】【伸展樹Splay Tree】


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  *  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;
}

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  < stdio.h >
#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);
  }
 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM