PHP算法 《樹形結構》 之 伸展樹(1) - 基本概念

伸展樹的介紹

一、出處:http://dongxicheng.org/structure/splay-tree/html

A、 概述 算法

 

二叉查找樹(Binary Search Tree,也叫二叉排序樹,即Binary Sort Tree)可以支持多種動態集合操做,它能夠用來表示有序集合、創建索引等,於是在實際應用中,二叉排序樹是一種很是重要的數據結構。編程

 

從算法複雜度角度考慮,咱們知道,做用於二叉查找樹上的基本操做(如查找,插入等)的時間複雜度與樹的高度成正比。對一個含n個節點的徹底二叉樹,這些操做的最壞狀況運行時間爲O(log n)。但若是由於頻繁的刪除和插入操做,致使樹退化成一個n個節點的線性鏈(此時即爲一個單鏈表),則這些操做的最壞狀況運行時間爲O(n)。爲了克服以上缺點,不少二叉查找樹的變形出現了,如紅黑樹、AVL樹,Treap樹等。網絡

 

本文介紹了二叉查找樹的一種改進數據結構–伸展樹(Splay Tree)。它的主要特色是不會保證樹一直是平衡的,但各類操做的平攤時間複雜度是O(log n),於是,從平攤複雜度上看,二叉查找樹也是一種平衡二叉樹。另外,相比於其餘樹狀數據結構(如紅黑樹,AVL樹等),伸展樹的空間要求與編程複雜度要小得多。數據結構

 

B、 基本操做性能

 

伸展樹的出發點是這樣的:考慮到局部性原理(剛被訪問的內容下次可能仍會被訪問,查找次數多的內容可能下一次會被訪問),爲了使整個查找時間更小,被查頻率高的那些節點應當常常處於靠近樹根的位置。這樣,很容易得想到如下這個方案:每次查找節點以後對樹進行重構,把被查找的節點搬移到樹根,這種自調整形式的二叉查找樹就是伸展樹。每次對伸展樹進行操做後,它均會經過旋轉的方法把被訪問節點旋轉到樹根的位置。學習

 

爲了將當前被訪問節點旋轉到樹根,咱們一般將節點自底向上旋轉,直至該節點成爲樹根爲止。「旋轉」的巧妙之處就是在不打亂數列中數據大小關係(指中序遍歷結果是全序的)狀況下,全部基本操做的平攤複雜度仍爲O(log n)。spa

 

伸展樹主要有三種旋轉操做,分別爲單旋轉,一字形旋轉和之字形旋轉。爲了便於解釋,咱們假設當前被訪問節點爲X,X的父親節點爲Y(若是X的父親節點存在),X的祖父節點爲Z(若是X的祖父節點存在)。設計

 

(1)    單旋轉3d

 

節點X的父節點Y是根節點。這時,若是X是Y的左孩子,咱們進行一次右旋操做;若是X 是Y 的右孩子,則咱們進行一次左旋操做。通過旋轉,X成爲二叉查找樹T的根節點,調整結束。

 

 

(2)    一字型旋轉

 

節點X 的父節點Y不是根節點,Y 的父節點爲Z,且X與Y同時是各自父節點的左孩子或者同時是各自父節點的右孩子。這時,咱們進行一次左左旋轉操做或者右右旋轉操做。

 

 

(3)    之字形旋轉

 

節點X的父節點Y不是根節點,Y的父節點爲Z,X與Y中一個是其父節點的左孩子而另外一個是其父節點的右孩子。這時,咱們進行一次左右旋轉操做或者右左旋轉操做。

 

 

C、伸展樹區間操做

 

在實際應用中,伸展樹的中序遍歷即爲咱們維護的數列,這就引出一個問題,怎麼在伸展樹中表示某個區間?好比咱們要提取區間[a,b],那麼咱們將a前面一個數對應的結點轉到樹根,將b 後面一個結點對應的結點轉到樹根的右邊,那麼根右邊的左子樹就對應了區間[a,b]。緣由很簡單,將a 前面一個數對應的結點轉到樹根後, a 及a 後面的數就在根的右子樹上,而後又將b後面一個結點對應的結點轉到樹根的右邊,那麼[a,b]這個區間就是下圖中B所示的子樹。

 

 

利用區間操做咱們能夠實現線段樹的一些功能,好比回答對區間的詢問(最大值,最小值等)。具體能夠這樣實現,在每一個結點記錄關於以這個結點爲根的子樹的信息,而後詢問時先提取區間,再直接讀取子樹的相關信息。還能夠對區間進行總體修改,這也要用到與線段樹相似的延遲標記技術,即對於每一個結點,額外記錄一個或多個標記,表示以這個結點爲根的子樹是否被進行了某種操做,而且這種操做影響其子結點的信息值,當進行旋轉和其餘一些操做時相應地將標記向下傳遞。

 

與線段樹相比,伸展樹功能更強大,它能解決如下兩個線段樹不能解決的問題:

 

(1) 在a後面插入一些數。方法是:首先利用要插入的數構造一棵伸展樹,接着,將a 轉到根,並將a 後面一個數對應的結點轉到根結點的右邊,最後將這棵新的子樹掛到根右子結點的左子結點上。

 

(2)  刪除區間[a,b]內的數。首先提取[a,b]區間,直接刪除便可。

 

二、出處:http://www.cnblogs.com/skywang12345/p/3604238.html

伸展樹(Splay Tree)是一種二叉排序樹,它能在O(log n)內完成插入、查找和刪除操做。它由Daniel Sleator和Robert Tarjan創造。
(01) 伸展樹屬於二叉查找樹,即它具備和二叉查找樹同樣的性質:假設x爲樹中的任意一個結點,x節點包含關鍵字key,節點x的key值記爲key[x]。若是y是x的左子樹中的一個結點,則key[y] <= key[x];若是y是x的右子樹的一個結點,則key[y] >= key[x]。
(02) 除了擁有二叉查找樹的性質以外,伸展樹還具備的一個特色是:當某個節點被訪問時,伸展樹會經過旋轉使該節點成爲樹根。這樣作的好處是,下次要訪問該節點時,可以迅速的訪問到該節點。

假設想要對一個二叉查找樹執行一系列的查找操做。爲了使整個查找時間更小,被查頻率高的那些條目就應當常常處於靠近樹根的位置。因而想到設計一個簡單方法,在每次查找以後對樹進行重構,把被查找的條目搬移到離樹根近一些的地方。伸展樹應運而生,它是一種自調整形式的二叉查找樹,它會沿着從某個節點到樹根之間的路徑,經過一系列的旋轉把這個節點搬移到樹根去。

相比於"二叉查找樹"和"AVL樹",學習伸展樹時須要重點關注是"伸展樹的旋轉算法"。

旋轉

旋轉的代碼:

/* 
 * 旋轉key對應的節點爲根節點,並返回根節點。
 */
Node* splaytree_splay(SplayTree tree, Type key)
{
    Node N, *l, *r, *c;

    if (tree == NULL) 
        return tree;

    N.left = N.right = NULL;
    l = r = &N;

    for (;;)
    {
        if (key < tree->key)
        {
            if (tree->left == NULL)
                break;
            if (key < tree->left->key)
            {
                c = tree->left;                           /* 01, rotate right */
                tree->left = c->right;
                c->right = tree;
                tree = c;
                if (tree->left == NULL) 
                    break;
            }
            r->left = tree;                               /* 02, link right */
            r = tree;
            tree = tree->left;
        }
        else if (key > tree->key)
        {
            if (tree->right == NULL) 
                break;
            if (key > tree->right->key) 
            {
                c = tree->right;                          /* 03, rotate left */
                tree->right = c->left;
                c->left = tree;
                tree = c;
                if (tree->right == NULL) 
                    break;
            }
            l->right = tree;                              /* 04, link left */
            l = tree;
            tree = tree->right;
        }
        else
        {
            break;
        }
    }

    l->right = tree->left;                                /* 05, assemble */
    r->left = tree->right;
    tree->left = N.right;
    tree->right = N.left;

    return tree;
}

上面的代碼的做用:將"鍵值爲key的節點"旋轉爲根節點,並返回根節點。它的處理狀況共包括:

(a):伸展樹中存在"鍵值爲key的節點"。
        將"鍵值爲key的節點"旋轉爲根節點。
(b):伸展樹中不存在"鍵值爲key的節點",而且key < tree->key。
        b-1) "鍵值爲key的節點"的前驅節點存在的話,將"鍵值爲key的節點"的前驅節點旋轉爲根節點。
        b-2) "鍵值爲key的節點"的前驅節點存在的話,則意味着,key比樹中任何鍵值都小,那麼此時,將最小節點旋轉爲根節點。
(c):伸展樹中不存在"鍵值爲key的節點",而且key > tree->key。
        c-1) "鍵值爲key的節點"的後繼節點存在的話,將"鍵值爲key的節點"的後繼節點旋轉爲根節點。
        c-2) "鍵值爲key的節點"的後繼節點不存在的話,則意味着,key比樹中任何鍵值都大,那麼此時,將最大節點旋轉爲根節點。 

下面列舉個例子分別對a進行說明。

在下面的伸展樹中查找10,共包括"右旋"  --> "右連接"  --> "組合"這3步。

 

第一步: 右旋
對應代碼中的"rotate right"部分

 

第二步: 右連接
對應代碼中的"link right"部分

 

第三步: 組合
對應代碼中的"assemble"部分

提示:若是在上面的伸展樹中查找"70",則正好與"示例1"對稱,而對應的操做則分別是"rotate left", "link left"和"assemble"。
其它的狀況,例如"查找15是b-1的狀況,查找5是b-2的狀況"等等,這些都比較簡單,你們能夠本身分析。

例子:

新建伸展樹,而後向伸展樹中依次插入10,50,40,30,20,60。插入完畢這些數據以後,伸展樹的節點是60;此時,再旋轉節點,使得30成爲根節點。
依次插入10,50,40,30,20,60示意圖以下:

將30旋轉爲根節點的示意圖以下:

三、出處:http://www.cnblogs.com/vamei/archive/2013/03/24/2976545.html

樹的搜索效率與樹的深度有關。二叉搜索樹的深度可能爲n,這種狀況下,每次搜索的複雜度爲n的量級。AVL樹經過動態平衡樹的深度,單次搜索的複雜度爲log(n) 。咱們下面看伸展樹(splay tree),它對於m次連續搜索操做有很好的效率。 

伸展樹會在一次搜索後,對樹進行一些特殊的操做。這些操做的理念與AVL樹有些相似,即經過旋轉,來改變樹節點的分佈,並減少樹的深度。但伸展樹並無AVL的平衡要求,任意節點的左右子樹能夠相差任意深度。與二叉搜索樹相似,伸展樹的單次搜索也可能須要n次操做。但伸展樹能夠保證,m次的連續搜索操做的複雜度爲mlog(n)的量級,而不是mn量級。 

具體來講,在查詢到目標節點後,伸展樹會不斷進行下面三種操做中的一個,直到目標節點成爲根節點 (注意,祖父節點是指父節點的父節點)

1. zig: 當目標節點是根節點的左子節點或右子節點時,進行一次單旋轉,將目標節點調整到根節點的位置。

zig

2. zig-zag: 當目標節點、父節點和祖父節點成"zig-zag"構型時,進行一次雙旋轉,將目標節點調整到祖父節點的位置。

zig-zag

3. zig-zig:當目標節點、父節點和祖父節點成"zig-zig"構型時,進行一次zig-zig操做,將目標節點調整到祖父節點的位置。

zig-zig

單旋轉操做和雙旋轉操做見AVL樹。下面是zig-zig操做的示意圖:

zig-zig operation

在伸展樹中,zig-zig操做(基本上)取代了AVL樹中的單旋轉。一般來講,若是上面的樹是失衡的,那麼A、B子樹極可能深度比較大。相對於單旋轉(想一下單旋轉的效果),zig-zig能夠將A、B子樹放在比較高的位置,從而減少樹總的深度。

 

下面咱們用一個具體的例子示範。咱們將從樹中搜索節點2:

Original

zig-zag (double rotation)

zig-zig

zig (single rotation at root)

上面的第一次查詢須要n次操做。然而通過一次查詢後,2節點成爲了根節點,樹的深度大減少。總體上看,樹的大部分節點深度都減少。此後對各個節點的查詢將更有效率。

伸展樹的另外一個好處是將最近搜索的節點放在最容易搜索的根節點的位置。在許多應用環境中,好比網絡應用中,某些固定內容會被大量重複訪問(好比江南style的MV)。伸展樹可讓這種重複搜索以很高的效率完成。

四、出處:http://www.cnblogs.com/kernel_hcy/archive/2010/03/17/1688360.html

1、簡介:
伸展樹,或者叫自適應查找樹,是一種用於保存有序集合的簡單高效的數據結構。伸展樹實質上是一個二叉查找樹。容許查找,插入,刪除,刪除最小,刪除最大,分割,合併等許多操做,這些操做的時間複雜度爲O(logN)。因爲伸展樹能夠適應需求序列,所以他們的性能在實際應用中更優秀。
伸展樹支持全部的二叉樹操做。伸展樹不保證最壞狀況下的時間複雜度爲O(logN)。伸展樹的時間複雜度邊界是均攤的。儘管一個單獨的操做可能很耗時,但對於一個任意的操做序列,時間複雜度能夠保證爲O(logN)。
2、自調整和均攤分析:
    平衡查找樹的一些限制:
一、平衡查找樹每一個節點都須要保存額外的信息。
二、難於實現,所以插入和刪除操做複雜度高,且是潛在的錯誤點。
三、對於簡單的輸入,性能並無什麼提升。
    平衡查找樹能夠考慮提升性能的地方:
一、平衡查找樹在最差、平均和最壞狀況下的時間複雜度在本質上是相同的。
二、對一個節點的訪問,若是第二次訪問的時間小於第一次訪問,將是很是好的事情。
三、90-10法則。在實際狀況中,90%的訪問發生在10%的數據上。
四、處理好那90%的狀況就很好了。
3、均攤時間邊界:
在一顆二叉樹中訪問一個節點的時間複雜度是這個節點的深度。所以,咱們能夠重構樹的結構,使得被常常訪問的節點朝樹根的方向移動。儘管這會引入額外的操做,可是常常被訪問的節點被移動到了靠近根的位置,所以,對於這部分節點,咱們能夠很快的訪問。根據上面的90-10法則,這樣作能夠提升性能。
爲了達到上面的目的,咱們須要使用一種策略──旋轉到根(rotate-to-root)。具體實現以下:
旋轉分爲左旋和右旋,這兩個是對稱的。圖示:
 
爲了敘述的方便,上圖的右旋叫作X繞Y右旋,左旋叫作Y繞X左旋。
下圖展現了將節點3旋轉到根:
 
                            圖1
首先節點3繞2左旋,而後3繞節點4右旋。
注意:所查找的數據必須符合上面的90-10法則,不然性能上不升反降!!
4、基本的自底向上伸展樹:
    應用伸展(splaying)技術,能夠獲得對數均攤邊界的時間複雜度。
    在旋轉的時候,能夠分爲三種狀況:
一、zig狀況。
    X是查找路徑上咱們須要旋轉的一個非根節點。
    若是X的父節點是根,那麼咱們用下圖所示的方法旋轉X到根:
     
                                圖2
    這和一個普通的單旋轉相同。
二、zig-zag狀況。
在這種狀況中,X有一個父節點P和祖父節點G(P的父節點)。X是右子節點,P是左子節點,或者反過來。這個就是雙旋轉。
先是X繞P左旋轉,再接着X繞G右旋轉。
如圖所示:
 
                            圖三
三、zig-zig狀況。
    這和前一個旋轉不一樣。在這種狀況中,X和P都是左子節點或右子節點。
    先是P繞G右旋轉,接着X繞P右旋轉。
    如圖所示:
     
                                    圖四
    下面是splay的僞代碼:   

    P(X) : 得到X的父節點,G(X) : 得到X的祖父節點(=P(P(X)))。
    Function Buttom-up-splay:
        Do
            If X 是 P(X) 的左子結點 Then
                If G(X) 爲空 Then
                    X 繞 P(X)右旋
                Else If P(X)是G(X)的左子結點
                    P(X) 繞G(X)右旋 X 繞P(X)右旋
                Else
                    X繞P(X)右旋 X繞P(X)左旋 (P(X)和上面一句的不一樣,是原來的G(X))
                Endif
            Else If X 是 P(X) 的右子結點 Then
                If G(X) 爲空 Then
                    X 繞 P(X)左旋
                Else If P(X)是G(X)的右子結點 P(X) 繞G(X)左旋 X 繞P(X)左旋
                Else
                    X繞P(X)左旋 X繞P(X)右旋 (P(X)和上面一句的不一樣,是原來的G(X))
                Endif Endif While (P(X) != NULL)
    EndFunction

    仔細分析zig-zag,能夠發現,其實zig-zag就是兩次zig。所以上面的代碼能夠簡化:
    

 Function Buttom-up-splay:
        Do
            If X 是 P(X) 的左子結點 Then
                If P(X)是G(X)的左子結點
                    P(X) 繞G(X)右旋
                Endif
                X 繞P(X)右旋
            Else If X 是 P(X) 的右子結點 Then
                If P(X)是G(X)的右子結點
                    P(X) 繞G(X)左旋
                Endif 
                X 繞P(X)左旋
            Endif While (P(X) != NULL)
    EndFunction

    下面是一個例子,旋轉節點c到根上。 
 
                                    圖五
5、基本伸展樹操做:
一、插入
    當一個節點插入時,伸展操做將執行。所以,新插入的節點在根上。
二、查找
    若是查找成功(找到),那麼因爲伸展操做,被查找的節點成爲樹的新根。
若是查找失敗(沒有),那麼在查找遇到NULL以前的那個節點成爲新的根。也就是,若是查找的節點在樹中,那麼,此時根上的節點就是距離這個節點最近的節點。
三、查找最大最小
        查找以後執行伸展。
四、刪除最大最小
a)刪除最小:
    首先執行查找最小的操做。
這時,要刪除的節點就在根上。根據二叉查找樹的特色,根沒有左子節點。
使用根的右子結點做爲新的根,刪除舊的包含最小值的根。
b)刪除最大:
首先執行查找最大的操做。
刪除根,並把被刪除的根的左子結點做爲新的根。
五、刪除
        將要刪除的節點移至根。
        刪除根,剩下兩個子樹L(左子樹)和R(右子樹)。
        使用DeleteMax查找L的最大節點,此時,L的根沒有右子樹。
        使R成爲L的根的右子樹。
        以下圖示:
         
                                圖六
6、自頂向下的伸展樹:
    在自底向上的伸展樹中,咱們須要求一個節點的父節點和祖父節點,所以這種伸展樹難以實現。所以,咱們能夠構建自頂向下的伸展樹。
    當咱們沿着樹向下搜索某個節點X的時候,咱們將搜索路徑上的節點及其子樹移走。咱們構建兩棵臨時的樹──左樹和右樹。沒有被移走的節點構成的樹稱做中樹。在伸展操做的過程當中:
   一、當前節點X是中樹的根。
   二、左樹L保存小於X的節點。
   三、右樹R保存大於X的節點。
開始時候,X是樹T的根,左右樹L和R都是空的。和前面的自下而上相同,自上而下也分三種狀況:
一、zig:
 
                                圖七
    如上圖,在搜索到X的時候,所查找的節點比X小,將Y旋轉到中樹的樹根。旋轉以後,X及其右子樹被移動到右樹上。很顯然,右樹上的節點都大於所要查找的節點。注意X被放置在右樹的最小的位置,也就是X及其子樹比原先的右樹中全部的節點都要小。這是因爲越是在路徑前面被移動到右樹的節點,其值越大。讀者能夠分析一下樹的結構,緣由很簡單。
二、zig-zig:
 
                                圖八
    在這種狀況下,所查找的節點在Z的子樹中,也就是,所查找的節點比X和Y都小。因此要將X,Y及其右子樹都移動到右樹中。首先是Y繞X右旋,而後Z繞Y右旋,最後將Z的右子樹(此時Z的右子節點爲Y)移動到右樹中。注意右樹中掛載點的位置。
三、zig-zag:
 
                            圖九
    在這種狀況中,首先將Y右旋到根。這和Zig的狀況是同樣的。而後變成上圖右邊所示的形狀。接着,對Z進行左旋,將Y及其左子樹移動到左樹上。這樣,這種狀況就被分紅了兩個Zig狀況。這樣,在編程的時候就會簡化,可是操做的數目增長(至關於兩次Zig狀況)。
    最後,在查找到節點後,將三棵樹合併。如圖:
 
                                圖十
    將中樹的左右子樹分別鏈接到左樹的右子樹和右樹的左子樹上。將左右樹做爲X的左右子樹。從新最成了一所查找的節點爲根的樹。
    下面給出僞代碼:    

 右鏈接:將當前根及其右子樹鏈接到右樹上。左子結點做爲新根。 左鏈接:將當前根及其左子樹鏈接到左樹上。右子結點做爲新根。 T : 當前的根節點。
    Function Top-Down-Splay Do 
            If X 小於 T Then 
               If X 等於 T 的左子結點 Then 右鏈接 
               ElseIf X 小於 T 的左子結點 Then 
                 T的左子節點繞T右旋 
                 右鏈接 
               Else X大於 T 的左子結點 Then 右鏈接 左鏈接 
               EndIf ElseIf X大於 T Then 
               IF X 等於 T 的右子結點 Then 左鏈接 
               ElseIf X 大於 T 的右子結點 Then T的右子節點繞T左旋 左鏈接 
               Else X小於 T 的右子結點 Then 左鏈接 右鏈接 
               EndIf EndIf While  (找到 X或遇到空節點) 組合左中右樹 
 EndFunction 

    一樣,上面的三種狀況也能夠簡化:   

 Function Top-Down-Splay Do 
            If X 小於 T Then 
                If X 小於 T 的左孩子 Then T的左子節點繞T右旋 
                EndIf    
                右鏈接 Else If X大於 T Then 
                If X 大於 T 的右孩子 Then T的右子節點繞T左旋
                EndIf 
                左鏈接 
            EndIf While  !(找到 X或遇到空節點) 組合左中右樹 
    EndFuntion

    下面是一個查找節點19的例子:
    在例子中,樹中並無節點19,最後,距離節點最近的節點18被旋轉到了根做爲新的根。節點20也是距離節點19最近的節點,可是節點20沒有成爲新根,這和節點20在原來樹中的位置有關係。
 
    這個例子是查找節點c:
 

相關文章
相關標籤/搜索