2-3樹

前言:2-3樹的資料比較少,國內的某本參考書關於2-3樹的介紹和國外的還不一樣,網上搜了一下,發現這一篇翻譯的介紹比較詳細,不錯,於是轉貼一下。

 

資料來源於:http://blog.donews.com/sowen/

 

前言

 

備註:文中可能偶爾多用了英文,倒不是賣弄,很多時候只是習慣性的,因爲如果你平時接觸的東西都是英文的,你寫下來的時候自然想到的是英文字眼,而不是多一層先翻譯成中文。還有一些是我只記得英文資料上的定義,比如我寫之前想到 balanced tree,卻不知道中文應該是什麼,查google才知道應該翻譯成平衡樹。還有的原因是可能英文解釋能更清晰表達就直接使用英文了。反正這篇東西的對象都是同行,我相信大部分人都有閱讀英文資料的習慣,因此一定能夠了解。如果閣下不喜歡中文夾着英文的東西,請不必閱讀了,謝謝。

 

tree 在計算機數據結構裏是一種非常重要的東西,介紹性的東西就不多講了,樹多數用於檢索量比較大的地方,比如數據庫和硬盤文件排列之類。樹分很多種,最簡單的當然是 BST (binary search tree) 中文應該是 二叉檢索樹,這個東西雖然簡單,可是毛病很多,比如很容易出現 worse case,就是檢索中最不願意看到的線性檢索(一棵只有右子樹或者左子樹的樹);而且BST不是balanced的。所以,BST一般很少應用,前人發明了很多其他的樹,比如AVLB-TREE2-3 tree, 2-3-4 tree 等等。這篇文章要介紹的就是2-3 tree,主要是因爲它的中文資料……似乎沒有(反正我沒有在google上找到)。而 2-3-4 tree 只是一個變種,最後會簡單介紹一下。

 

本文的實現代碼我用了JAVA,本來這種需要訪問指針的東西,我更想用c++,但考慮易讀性還是JAVA比較好點,比較適合介紹性質的代碼。

 

特性

 

2-3 tree 嚴格來說只有兩個要求:

 1.所有的結點有2個或者3個子樹

 2.所有的leaves(葉)都在同一個級別上。

 

不知道這樣的中文解釋是否夠清楚,解釋一下就是:葉是樹中最後一個結點,它們的高度(path from root)都必須一樣;每個結點最多只能有3個子樹。

 

比如下面這棵樹就是一個合法的2-3 tree

 

<10 20>

/  
|  
/

/   
|   
/

<5> <15> <30 40>

 

其實2-3 tree也是從BST演變過來的,小值就在左邊,大值在右邊。每個結點最多隻有兩個值,一個小,一個大。如果一個結點有兩個值,這個結點稱爲full,那麼如果它有子樹,那麼它一定有3個子樹。如上圖所示,小值(<10) 在左子樹,大值 (>20) 在右子樹,而中間值( 10<x<20 ),就在中間的子樹。

 

爲什麼要這樣排列?

 因爲它是balanced的!一棵高度爲k2-3 tree2k – 1 3k – 1 之間的葉;一棵有n elements 2-3 tree 高度最小爲 1+log3n 最大爲 1+log2n。它的檢索時間爲O(logn)

 

檢索的pseudo code 

LOOP until curr = nil


IF ( data = curr.small OR data = curr.large )


    Return curr


ELSE IF ( data < curr.small )


  curr = curr.left


ELSE IF ( data > curr.small AND data < curr.large )


  curr = curr.middle


ELSE


  curr = curr.large

 也可以通過遞歸來做,跟BST差不多,檢索並不是本文介紹的目的,因爲實在沒什麼好說的。


2-3 tree的結點結構

 

最簡單的結構如下

 

struct tree23Node {

     int small,

     int large,

     tree23Node left,

     tree23Node middle,

     tree23Node large

 }

 

插入

 

對於2-3 tree,所有的插入(新值)都必須發生在leaf。所以,首先找到新值應該所在的leaf,然後根據這個leaf的情況做出判斷:

 1.  該點只有一個元素。直接加入就可以了,判斷是small還是large。

 2.  該點full(small和large都有值),其父結點不是full。那就split該結點,並把中間值推上去。

 3.  該點和其父結點都full。如果父結點full,表示父結點已經有3個子樹。那就需要一直split 並一直往上推,直到發生以下情況:

         1)     父結點最多隻有3個子樹

         2)     父結點是根,創建一個新root,保存中間值。樹的高度增加1

 

最好的解釋其實還是例子,請先看一棵合法的2-3 tree如下: 

 

<53>

 

         /         /

 

        /           /

 

<27>        <65, 78>

 

/  /        /     |     /

 

  <12> <39,43> <60> <69,74>  <93>

 

        樹一

 

 1)  加入 15

 

新加入一個值的時候,要記住,所有的新增一定發生在leaf。所以第一步我們需要找到15的位置。不錯,就是在<12>那裏。這個 leaf 只有一個元素,就是說不是full,這是最簡單的一種情況,直接把15加在那個leaf就可以了。因爲15>12,所以樹會變成

 

<53>

 

         /         /

 

        /           /

 

<27>        <65, 78>

 

/  /        /     |     /

 

<12,15> <39,43> <60> <69,74>  <93>

  

     樹二

 

我們可以再檢查一下,是否滿足2-3 tree的兩個要求。(1) 所有結點只有2個或者3個子樹,這個滿足了。(2)所有的leaves都在同一個級別(高度一樣),這個也滿足了。

 

2)  加入22

 

這次好像有點麻煩了,22似乎應該加在<12,15>這個leaf上,可是它已經full了。如果加入後,應該變成

  

  <53>

 

             /         /

 

            /           /

 

<27>        <65, 78>

 

/  /        /     |     /

 

<12,15,22> <39,43> <60> <69,74>  <93>

  

    樹三 (not valid yet)

 

按照前面提到的插入要求,需要split這個結點,split之後,該結點應該變成<12> <15> <22>三個結點,然後把中間那個推上去。(爲什麼呢?因爲只有中值上去變成父樹裏的值,原來的large變成的那個結點才能變成中子樹的結點,還記得它的特性嗎?所以,最後的樹應該變成

 

   <53>

 

             /         /

 

            /           /

 

<15,27>        <65, 78>

 

/   |   /        /     |     /

 

<12> <22> <39,43> <60> <69,74>  <93>

  

     樹四

 

    請自行檢驗是否合法2-3 tree

 

3)  現在讓我們試試新加入 32

 首先,大家都知道32應該在那個leaf開始動作了吧。不錯,就是<39,43>,加入後,這個leaf應該變成<32,39,43>;full了,要split,變成<32><39><43>,再把中值推上去,變成以下的一棵樹

  

   <53>

 

             /         /

 

            /           /

 

<15,27,39>   <65, 78>

 

/  |  |   /    /     |     /

 

<12><22><32><43> <60> <69,74>  <93>

  

     樹五(not valid yet)

 

很顯然,這不是一個合法的樹。因爲有結點有超過3個子樹,而且有結點超過兩個元素(full)。那麼就需要繼續split,然後又是中值往上推,直到見到只有兩個子樹的結點<53>。所以,再split一次後的樹應該變成:

  

  <27,53>

 

             /     |       /

 

            /      |        /

 

<15>     <39>      <65, 78>

 

/   /      /   /    /    |    /

 

<12>  <22> <32> <43> <60> <69,74>  <93>

 

        樹六

 

4)  在【樹六】的基礎上加入 34,35,37,後,2-3 tree應該變成如下:

 

<27,53>

 

             /       |       /

 

            /        |        /

 

<15>     <34,39>     <65, 78>

 

/   /    /   |    /     /    |    /

 

<12> <22> <32><35,37><43> <60> <69,74>  <93>

  

    樹七

  

 

你對了嗎?

 

 

1)  最後,讓我們看看最複雜的一種情況,如果在【樹七】中,加入36,應該變成怎樣?

  

我們可以一步一步來,首先,找到36應該在的leaf,<35,37>,加入後變成<35,36,37>,該leaf變成full了。Split,然後推中值上去。變成如下:

 

      <34,36,39>

 

    /    |   |   /

 

  <32> <35> <37> <43>

  

好,父樹也變成full了,這種情況我們見過,再split和再推中值上去。

 

     <27,36,53>

 

  /    |      |     /

 

 <15> <34>    <39>  <65,78>

 

 //   /  /    /  /     / | /

 

  <32><35><37><43> 

 

嗯,現在我們的root也變成full了。那就在split一次,然後新建一個root,保存中值。 變成

 

         <36>

 

       /       /

 

    <27>       <53>

 

   /   /     /     /

 

<15>  <34> <39>   <65,78>

 

………………………………… (以下省略,大家都知道下面怎麼連接了吧)

 

這個時候,我們的2-3 tree高度增加了1

 

到這裏,已經列舉了所有增加新值的例子。如果還不是太清晰,那我就在此總結一下吧:

 

l         新值的增加總是發生在leaf。(它最後是否在leaf上就很難說,但增加這個動作永遠是發生在leaf上,所以第一步永遠都是找到新值應該位於的leaf)

 l         如果一個結點有三個值,我們說它是full了,需要split,把中值推到父結點上去。

 l         如果一個結點需要split而它又剛好是root,那麼就新創建一個root,保存那個中值。樹的高度增加1。

 

接下來我們要做什麼呢?大家是想看刪除的特性嗎?嗯,我只能說,刪除相當複雜,即使只是BST的刪除也很麻煩,大家都還記得BST的刪除吧。我現在可以說的2-3 tree的刪除大概是和新增反過來的,新增是往上推一個值,刪除是往下找一個子樹合併。

 

我不想立刻就開始講刪除,因爲我想趁大家還記得新增的特性時讓大家看看代碼,加深大家對新增的印象。

 

我的代碼- 新增部分


前面我給大家看過一個最簡單的structureof 2-3 treenode,那個結構其實也可以做,但是比較麻煩。因爲我們需要往上讀父結點,如果沒有parent指針,一般都是兩種方法,一是用臨時變量保存,這種方法在2-3tree裏不是太現實,因爲你不知道要往上查多少次;另外一種方法是用回溯,把所有順序訪問過的結點放到stack裏,而我前面也說過,2-3tree的高度最大爲log2n +1,所以stack的實現用數組都可以,比較方便。如果stack爲空,表示我們已經到root了。但是用stack的話,我們就只能用循環。(大家都明白爲什麼吧)

 

如果在c++裏,我們還可以用遞歸回到父結點上;但是java裏參數傳遞沒有引用傳遞(也有人說JAVA只有引用傳遞,都是說法一個,這裏不追究)這個概念,所以我們很難用遞歸一步一步往上走。

 

不過既然寫程序的是我們,我們可以對那個結構稍做修改,以適應我們的需要。

 

請參照上面那些樹的圖,有沒有發現很多時候新增動作發生之後,一個結點會出現4個子樹,3個值。這是最大的可能發生的情況,那麼我們就給結構3個值,4個子樹。同時爲了方便,也給它一個父結點的引用,以及一個itemCount的值標誌該結點有多少個值。

 

因此,新的tree23Node結構如下:

 


public class tree23Node


{


        int small; // the small item in this node


        int large; // the large item in this node


        int temp; // stores a temporary item


               


        tree23Node left;        // specifies left child


        tree23Node middle;    // specifies middle child


        tree23Node right;     // specifies right child


        int itemCount;       // how many items in this node


 


        // 以下一個結點用來儲存臨時子樹,爲什麼設置成爲private,是因爲我打算將讀取它們的動作都放在這個class裏,就自然不需要讓別人看到它們。


        private tree23Node rtNode;


        private tree23Node parent;


}


 

  

下面是tree23Node的constructor,一共有兩個,很容易看到,這裏就不註釋了。

 

需要注意的是,我設置了default value是 -1,其實也是最小值。所以我是假定後面的2-3 tree 加入的值都是正數。當然你也可做相應修改以至能夠接受負數。

 


    tree23Node()


    {


        itemCount=0;


        small = large = temp = -1;


        left = middle = right = parent = null;


    } // end tree23Node constructor


               


    tree23Node(int newVal)


    {


         itemCount = 1;


         small = newVal;


         large = temp = -1;


         left = middle = right = parent = null;


    } // end tree23Node constructor

  

同時tree23Node還提供兩個方法,一個是isLeaf,返回真如果該node是一個leaf;代碼如下(對不起,註釋我一般習慣用英文):

 


/****************************************************************


* method isLeft


* purpose: tell whether this node is a leaf or not.


*       (a leaf is a node that has no child)


* return true if it is a leaf


***************************************************************/


public boolean isLeaf()


{


     boolean retval = false;


      if (left==null && middle ==null && right==null)


      {


           retval = true;


       }


       return retval;


} // end method isLeaf

 

另外一個重要的方法是 split,這個代碼會放到最後。

 

下一篇,我們會看到tree23 class 裏面的insert方法

 

對於 class tree23,我提供了一個public的insert方法,如下:

 


public void insert(int data)


{


    // if the root is empty, which means the tree is emtpy.


    // create the tree. otherwise, call a private method


   


    if (root==null)


    {


        root = new tree23Node(data);


    }


    else


    {


        insert(root, data);


    }


} // end insert

  

同時,我也提供了一個private的insert方法,其實也可以放到同一個地方,主要就是免得一個方法太長而已。該方法代碼如下:

 


private void insert(tree23Node curr, int data)


{


    // stores the node in a temporary variable


   


    tree23Node ptr = curr;


 


// for 2-3 tree, all new item must be inserted in a leaf.


     // find the correct leaf firstly


     // 還記得我前面說過的吧,第一步永遠都是先找到一個leaf


               


     while (!ptr.isLeaf())


     {


         if (data <= ptr.small && ptr.left!=null)


         {


              ptr = ptr.left;


          }


          else if (data>=ptr.large && ptr.right!=null)


          {


               ptr = ptr.right;


          }


          else if (data<ptr.large && ptr.middle!=null)


          {


              ptr =  ptr.middle;


          }


      }


               


// 下面首先判斷兩種情況,如果當前的leaf只有一個元素,直接放進去就是了


               


      if (ptr.itemCount==1)


      {


           if (ptr.small <= data)


           {


                ptr.large = data;


                ptr.itemCount ++;


            }


            else


            {


                ptr.large = ptr.small;


                ptr.small = data;


                ptr.itemCount ++;


            }


       } // end outer if


               


       //否則的話,這個leaf已經full了,因爲它有兩個值。那我就split它好了。但是在split之前,我先做一件事,就是先還是照加入新值,並按順序排列好。就是說,如果新值最小,就放到small,把原來的small放到large,把原來的large放到temp。如果新值最大,就放到temp;如果它是中間值,就放到large,而原來的large就放到temp。


               


       else


       {


             if (ptr.large <= data)


             {


                 ptr.temp = data;


              }


              else if (ptr.small <= data)


              {


                  ptr.temp = ptr.large;


                  ptr.large = data;


              }


              else


              {


                  ptr.temp = ptr.large;


                  ptr.large = ptr.small;


                  ptr.small = data;


              }


                   


              // increase the item count of this node, which


              // should be 3 now


                   


              ptr.itemCount++;


                   


              // call split method and overwrite root


 


              root=ptr.split(); // split 方法返回的是一棵完整的樹


 


         } // end else


} // end method insert

 

 

終於到了最激動人心的時候,就是 class tree23Node 的split方法。

 

爲什麼要把這個方法放到tree23Node裏面呢?一開始我只是覺得會方便一些,因爲比較split的時候,對每個結點的各種屬性都需要訪問和改變,如果在this裏面訪問,可以不需要寫 “[對象].” 這樣。呵呵,很無聊的原因吧。

 

其實,放到tree23Node還是有原因的,從OO的概念上看,tree23.split()沒有意義,無論它是否private。Split這個操作是屬於一個node的。另外,從可讀性和操作性上,放在tree23裏都相當麻煩,我也看過不少這樣做的代碼,大家也不妨去google查查看。

  

在顯示代碼之前,先做一些說明。

 

 

Split方法只有當一個node是full的時候才被調用,而且返回值是一棵完整的2-3tree。

 

該結點其中small是最小值,large是中間值,temp包含最大值。我們要做的是:

 1.     將最小值和最大值分開,變成獨立的兩個node,讓我們叫一個是小結點,一個是大結點

 2.     把中間值推到父結點。 

1)如果父結點是空的,那麼表示我們已經到了root,創建一個新root包含那個中間值,並連接小結點到left,大結點到right,返回新創建的root 

2) 看看this是屬於父結點的哪一個子樹 

    a) 屬於左子樹,推上去的值一定是一個父結點的最小值,需要保存在small裏

 

如果父結點是full的,先把原來的right連接到rtNode裏做個備份,原來的large放到temp裏,small放到large,推上去的值放在small裏。現在輪到父結點需要split了,請在這裏記住rtNode不爲null。

  

如果父結點不是full,那大家都知道怎麼做了。

  

b)屬於右子樹,推上去的一定是父結點的最大值,需要保存在large(也可能在temp裏,要看父結點是否full)

  

   然後如上,分析父結點是否爲full

 

c) 屬於中子樹,如果父子樹有中子樹,那麼表示它一定已經full了。

 

   最後,看看父結點的 rtNode是否爲空,如果不是,表示需要繼續往上split。遞歸調用。否則就一直循環推上去直到root,返回。

 

可能我上面解釋的不太清楚,還是讓我們看代碼吧

 


tree23Node split()


{


tree23Node retval, node1, node2;


     int s, m, l;


                   


                   


     s = small;


     m = large;


     l = temp;


                   


     // firstly, prepare two new nodes that are splited


     // in this node. That means, I have two new nodes that


     // one has the smallest item, another has the largest item.


     // Then, I will pass the middle item up.


    


     node1 = new tree23Node(s)                    ;


     node2 = new tree23Node(l);


                   


     // there are two cases


     // 1. this is the root, I just need to create a new root


     //    that contains the middle item and connect two new


     //    nodes to it


     // 2. this is not the root, I need to do the following things


     //      1) consider this node is parent’s left, middle, or


     //         right, to do different connections


     //      2) if parent is still full, recursively split


               


     if (parent==null)


     {


           // create a new node contains the middle value


          


           retval = new tree23Node(m);


           retval.left = node1;


           retval.right = node2;


      }


      else


      {


         // get the parent node


                  


         retval = parent;


             


         // if this node is parent’s left child


                 


         if (retval.left==this)


         {


              // if parent is full already, now parent should


              // have four children(1 is in rtNode), three items


         


              if (retval.itemCount>1)


              {


                    retval.rtNode = retval.right;


                    retval.right = retval.middle;


                   retval.temp = retval.large;


               }


 


               retval.left = node1;


               retval.middle = node2;


               retval.large = retval.small;


               retval.small = m;


         }


         else if (retval.right==this)


         {


                // if parent of this is not full, now parent


                // should have 3 children, otherwise 4, 1 is in


                // rtNode


                           


                if (retval.itemCount==1)


                {


                     retval.middle = node1;


                     retval.right = node2;


                     retval.large = m;


                 }


                 else


                 {


                      retval.right = node1;


                      retval.rtNode = node2;


                      retval.temp = m;


                  }


         }


         else if (retval.middle == this)


         {


                  // if this is the parent’s middle child


                  // it means parent must have 2 items, it


                  // has been full, I need to store one in rtNode


                           


              retval.middle = node1;


              retval.rtNode = retval.right;


              retval.right = node2;


              retval.temp = retval.large;


              retval.large = m;


         }


         else


         {


              //should never happen


                    


              System.out.println(”pointer error!”);


              System.exit(0);


         }


                       


         // 我已經把新元素加到父結點去了,它是否合法我暫時不管,總之我先增父結點的總數,減掉當前結點的總數


                       


          retval.itemCount++;


          itemCount–;


                 


      } // end else parent != null


                   


      // fix the new nodes


               


      node1.parent = node2.parent = retval;


                


      // if this node is not a leaf, I also need to fix


      // new nodes’ children and those parents


               


      if (!isLeaf())


      {


            node1.left = left;


            node1.right = middle;


            node2.left = right;


            node2.right = rtNode;


        


            if (node1.left!=null)


            {


                node1.left.parent = node1;


             }


                       


            if (node1.right!=null)


            {


                node1.right.parent = node1;


             }


                       


             if (node2.left!=null)


             {


                 node2.left.parent = node2;


             }


                     


             if (node2.right!=null)


             {


                 node2.right.parent = node2;


              }


      } // end if


                   


      // now I can clean the resources


                   


      left = middle = right = rtNode = parent = null;


                   


      // if parent still has an rtNode, it means parent is full


      // now, it needs to be splited too.


                   


      if (retval.rtNode!=null)


      {


            return retval.split();


      }


                   


      // if I am here, that means I have splited everything


      // now I move to the top, return the root


                   


      while (retval.parent!=null)


      {


            retval = retval.parent;


      }


                   


      return retval;


               


} // end method split

 

 


到這裏,新增就全部講完了。真是累得可以,明天繼續。

 

刪除

 

 

這次,讓我們直接看例子,請回顧之前看到的【樹一】

 

1.  最簡單的情況就是,刪除一個leaf結點的值後,改結點仍然不會空。比如刪除【樹一】的39,43,69,73(是說一次刪除一個,不是指依次刪除)。提到的值都是位於leaf上,而且這些結點都是已經有兩個值;不過要記得移除了large值的話,不需要多做什麼;但移除small值,需要將large值設置爲-1,然後把原來的large移動到small上。

 

2.  在【樹一】上移除12

 

如果移走了12,樹會變成如下:

 

<53>

 

         /         /

 

        /           /

 

<27>        <65, 78>

 

/  /        /     |     /

 

   <>  <39,43> <60> <69,74>  <93>

 

            樹八 (not valid yet)

  

    <27> 的左子樹空了,這不合法。這時候,我們需要向<12>的“親戚”(sibling)借一個值,也就是<27>的右子樹,它剛好有兩個值。將小值39推上去,再將27拉下來,最後如下

  

<53>

 

         /         /

 

        /           /

 

<39>        <65, 78>

 

/  /        /     |     /

 

   <27> <43> <60> <69,74>  <93>

 

            樹九

 

    這裏使用的策略是,當刪除一個結點的值會識得該結點變空,就嘗試向它的親戚結點借一個值,如果可能的話。

 

3.  刪除非leaf結點的值

 

這次讓我們刪除【樹九】的65,這是一個在非leaf結點上的值。

 

因爲當一個結點非leaf,它一定有子樹,因此也必定存在有“inorder successor”(這個東西真的不知道怎麼翻譯,這個跟BST的刪除一樣,找到一個最小的比65大的元素)

 

所以,我們可以找到69,然後跟65替換,最後的樹變成

 

<53>

 

         /         /

 

        /           /

 

<39>        <69, 78>

 

/  /        /     |     /

 

   <27> <43>   <60>   <74>  <93>

 

     樹十

 

4.  刪除一個元素,其親戚結點無值可借

 

比如【樹十】我們要刪除74,它右邊的親戚結點只有一個值,無法借過來。還記得我之前曾經提過嗎,對,我們把父結點的值拉下來,就剛好如同我們新增時候所做的反過來。我們可以將69拖下來,並與60合併。最後樹變成

 

<53>

 

         /         /

 

        /           /

 

<39>         <78>

 

/  /        /        /

 

   <27> <43>   <60,69>    <93>

 

     樹十一

 

請自己嘗試在【樹十一】上增加74,看看是否和【樹十】一樣

 

5.  刪除一個元素,其親戚結點無值可借,而且父結點只有一個元素,無法下推

 

比如,我們要從【樹十】中刪除43,如果我們像剛纔所說的那樣做,把父結點拖下來合併,樹就會變成這樣:

 

<53>

 

         /         /

 

        /           /

 

<>         <69, 78>

 

/  /        /     |     /

 

 <27,39> <>   <60>   <74>  <93>

 

     樹十二 (not valid yet)

 

這明顯不合法。我們需要做的時候重複一次,再把父結點(39的父結點是53)拖下來合併,原來39的位置變成53了,53的位置又變空了。可是這次原來39的位置有親戚子樹<69,78>可以借。請參照情況2,把小值69推上去,因爲53去了左子樹那邊,需要找它的inordersuccessor(找到60),把它移到53的右子樹去。最終,完整的樹變成

 

<69>

 

         /         /

 

        /           /

 

<53>          <78>

 

/   /        /     /

 

 <27,39> <60>   <74>   <93>

 

     樹十三

 

現在是一棵合法的2-3樹了,但是這次你不能用將43重新插入來檢驗是否正確,因爲如果重新插入43會變成

 

<69>

 

         /         /

 

        /           /

 

<39,53>        <78>

 

/  |  /        /   /

 

 <27> <43> <60>   <74>   <93>

 

     樹十四

 

6.  刪除一個元素,其親戚結點無法借值,其父結點只有一個元素,其父結點的親戚結點也無法借值……

 

這是最複雜的情況,其實也就是需要不斷的重複步驟,直到出現以下情況

 

1)  當一個結點有兩個元素 

2)  當一個結點的親戚結點可以借值 

3)  當已經到達root

 

當最後一種情況發生的時候,root的兩個子樹就合併,原來的root就被刪除,新的root出現,樹的高度減1

 

總結

 

刪除元素i的方法

 

1)  找到包含元素i的結點n 

2)  如果n是一個leaf,先刪除i 

如果n不是一個leaf,找到i的inorder successor,與i交換,然後刪除i 

3)  如果這個時候n不是空,那麼刪除完成 

如果n變空了,惡夢開始(^^),我們需要修復樹 

a)       檢查n的親戚結點,如果有兩個元素,借一個過來(要注意借過來的元素和父結點元素的安排)比如你從左子樹借一個過來,那當然是借一個左子樹的large,然後原來的父結點元素拖下來;如果是從右子樹借一個,那當然是借small,還要同時把small變成large,再把small變成新父結點,原來的父結點放到空結點去。(如果覺得比較混亂請自己畫個圖就清楚了) 

b)       如果沒有親戚結點有兩個元素,那就直接把父結點拖下來合併,讓父結點變成空。 

c)       遞歸的往上重複,直到遇到在情況6中所述的三種情形。

 

 

代碼

 

首先我要給大家看的是 tree23裏面的delete方法

 

    public void delete(int data)

 

    {

 

        tree23Node ptr, found;

 

        int tmp;

 

       

 

        found = locate(data); // 首先我找到該元素所在的結點

 

       

 

        if (found==null)

 

        {

 

            return;

 

        }

 

        else

 

        {

 

            if (!found.isLeaf()) // if it is not a leaf

 

            {

 

                if (found.small==data && found.itemCount>1)

 

                {

 

                    ptr = found.middle;

 

                }

 

                else

 

                {

 

                    ptr = found.right;

 

                }

 

           

 

                // 這裏我是找 inorder successor

 

                while(ptr.left!=null) ptr = ptr.left;

 

               

 

                // 然後交換需要刪除的那個值

 

                if (found.small==data)

 

                {

 

                    tmp = found.large;

 

                    found.small = ptr.small;

 

                    ptr.large = tmp;

 

                }

 

                else

 

                {

 

                    tmp = found.large;

 

                    found.large = ptr.smll;

 

                    ptr.large = tmp;

 

                }

 

            }

 

            else  // found is a leaf

 

            {

 

                ptr = found;

 

            }

 

           

 

            // 交換完畢了就刪除元素

 

            if (ptr.large == data)

 

{

 

    ptr.large = -1;

 

 ptr.itemCount–;

 

            }

 

            else if (ptr.small==data)

 

            {

 

                ptr.small = ptr.large;

 

                ptr.large = -1;

 

                ptr.itemCount–;

 

            }

 

           

 

            // if it is empty, fix the root

 

            // 這裏我用了跟新增一樣的技巧,調用fix方法修復,然後返回整棵樹

 

            if (ptr.itemCount == 0)

 

                root = ptr.fix();

 

               

 

        } // end if found==null

 

    } // end delete

 

剩下的工作,就是在 tree23Node裏增加一個方法fix,不需要任何參數,該方法調用的時候this一定是一個空結點,然後就往上找就是。具體實現的步驟已經說得很清楚了,而方法也類似split那樣。

 

 

如果感興趣的朋友不妨自己試試,倒的確是一個鍛鍊的好機會。

 

 

後記

 

 

這篇東西純粹是介紹性質的文章,因爲2-3樹在國外是很受重視的,可惜似乎國內倒不太關注。我在google上找到有人問,卻沒有什麼比較詳細的中文資料。於是才決定寫這篇東西。寫得比較潦草,也沒有非常專業的口吻,不妥的地方還請指教。非常歡迎轉載,但是請不要自己修改後發表到盈利的報刊雜誌上,只限網上轉載,轉載的時候也請聲明出處,謝謝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章