無限分類演算法

來源:互聯網
上載者:User

一、引言
產品分類,多級的樹狀結構的論壇,郵件清單等許多地方我們都會遇到這樣的問題:如何儲存多級結構的資料?在PHP的應用中,提供後台資料存放區的通常是關係
型資料庫,它能夠儲存大量的資料,提供高效的資料檢索和更新服務。然而關係型資料的基本形式是縱橫交錯的表,是一個平面的結構,如果要將多級樹狀結構儲存
在關係型資料庫裡就需要進行合理的翻譯工作。接下來我會將自己的所見所聞和一些實用的經驗和大家探討一下:
層級結構的資料儲存在平面的資料庫中基本上有兩種常用設計
方法:

    * 毗鄰目錄模式(adjacency list model)
    * 預排序遍曆樹演算法(modified preorder tree traversal algorithm)

我不是電腦專業的,也沒有學過什麼資料結構的東西,所以這兩個名字都是我自己按照字面的意思翻的,如果說錯了還請多多指教。這兩個東西聽著好像很嚇人,
其實非常容易理解。

二、模型
這裡我用一個簡單食品目錄作為我們的樣本資料。
我們的資料結構是這樣的,以下是代碼:

  1. Food
  2. |
  3. |---Fruit
  4. |    |
  5. |    |---Red
  6. |    |    |
  7. |    |    |--Cherry
  8. |    |
  9. |    +---Yellow
  10. |          |
  11. |          +--Banana
  12. |
  13. +---Meat
  14.       |--Beef
  15.       +--Pork

複製代碼

為了照顧那些英文一塌糊塗的PHP愛好者

  1. Food  : 食物
  2. Fruit : 水果
  3. Red   : 紅色
  4. Cherry: 櫻桃
  5. Yellow: 黃色
  6. Banana: 香蕉
  7. Meat  : 肉類
  8. Beef  : 牛肉
  9. Pork  : 豬肉


制代碼

三、實現

1、毗鄰目錄模式(adjacency list model)

這種模式我們經常用到,很多的教程
和書中也介紹過。我們通過給每個節點增加一個屬性
parent 來表示這個節點的父節點從而將整個樹狀結構通過平面的表描述出來。根據這個原則,例子中的資料可以轉化成如下的表:
以下是代碼:

  1. +-----------------------+
  2. |   parent |    name    |
  3. +-----------------------+
  4. |          |    Food    |
  5. |   Food   |   Fruit    |
  6. |   Fruit  |    Green   |
  7. |   Green  |    Pear    |
  8. |   Fruit  |    Red     |
  9. |   Red    |    Cherry  |
  10. |   Fruit  |    Yellow  |
  11. |   Yellow |    Banana  |
  12. |   Food   |    Meat    |
  13. |   Meat   |    Beef    |
  14. |   Meat   |    Pork    |
  15. +-----------------------+

複製代碼

我們看到 Pear
是Green的一個子節點,Green是Fruit的一個子節點。而根節點'Food'沒有父節點。
為了簡單地描述這個問題,這個例子中只用了name來表示一個記錄。
在實際的資料庫中,你需要用數位id來標示每個節點,資料庫的表結構大概應該像這樣:id, parent_id, name,
descrīption。
有了這樣的表我們就可以通過資料庫儲存整個多級樹狀結構了。

顯示多級樹,如果我們需要顯示這樣的一個多級結構需要一個遞迴函式。
以下是代碼:

  1. <?php
  2. // $parent is the parent of the children we want to see
  3. // $level is increased when we go deeper into the tree,
  4. //        used to display a nice indented tree
  5. function display_children($parent, $level) {
  6.     // 獲得一個 父節點 $parent 的所有子節點
  7.     $result = mysql_query("
  8.         SELECT name
  9.         FROM tree
  10.         WHERE parent = '" . $parent . "'
  11.         ;"
  12.     );
  13.     // 顯示每個子節點
  14.     while ($row = mysql_fetch_array($result)) {
  15.         // 縮排顯示節點名稱
  16.         echo str_repeat('  ', $level) . $row['name'] . "/n";
  17.         //再次調用這個函數顯示子節點的子節點
  18.         display_children($row['name'], $level+1);
  19.     }
  20. }
  21. ?>

複製代碼


整個結構的根節點(Food)使用這個函數就可以列印出整個多級樹結構,由於Food是根節點它的父節點是空的,所以這樣調用:
display_children('',0)。將顯示整個樹的內容:

  1. Food
  2.     Fruit
  3.         Red
  4.             Cherry
  5.         Yellow
  6.             Banana
  7.     Meat
  8.         Beef
  9.         Pork


制代碼

如果你只想顯示整個結構中的一部分,比如說水果部分,就可以這樣調
用:display_children('Fruit',0);

幾乎使用同樣的方法我們可以知道從根節點到任意節點的路徑。比如 Cherry 的路徑是 "Food >; Fruit >; Red"。
為了得到這樣的一個路徑我們需要從最深的一級"Cherry"開始,
查詢得到它的父節點"Red"把它添加到路徑中,然後我們再查詢Red的父節點並把它也添加到路徑中,以此類推直到最高層的"Food",以下是代碼:

  1. <?php
  2. // $node 是那個最深的節點
  3. function get_path($node) {
  4.     // 查詢這個節點的父節點
  5.     $result = mysql_query("
  6.         SELECT parent
  7.         FROM tree
  8.         WHERE name = '" . $node ."'
  9.         ;"
  10.     );
  11.     $row = mysql_fetch_array($result);
  12.     // 用一個數組儲存路徑
  13.     $path = array();
  14.     // 如果不是根節點則繼續向上查詢
  15.     // (根節點沒有父節點)
  16.     if ($row['parent'] != '') {
  17.         // the last part of the path to $node, is the name
  18.         // of the parent of $node
  19.         $path[] = $row['parent'];
  20.         // we should add the path to the parent of this node
  21.         // to the path
  22.         $path = array_merge(get_path($row['parent']), $path);
  23.     }
  24.     // return the path
  25.     return $path;
  26. }
  27. ?>;

複製代碼


果對"Cherry"使用這個函數:print_r(get_path('Cherry')),就會得到這樣的一個數組了:

  1. Array (
  2.     [0] => Food
  3.     [1] => Fruit
  4.     [2] => Red
  5. )

複製代碼


下來如何把它列印成你希望的格式
,就是你的事情了。

缺點:
這種方法很簡單,容易理解,好上手。但是也有一些缺點。主要是因為運行速度很慢,由於得到每個節點都需要進行資料庫查詢,資料量大的時候要進行很多查詢才
能完成一個樹。另外由於要進行遞迴運算,遞迴的每一級都需要佔用一些記憶體所以在空間利用上效率也比較低。

2、預排序遍曆樹演算法

現在讓我們看一看另外一種不使用遞迴計算,更加快速的方法,這就是預排序遍曆樹演算法(modified preorder tree traversal
algorithm)
這種方法大家可能接觸的比較少,初次使用也不像上面的方法容易理解,但是由於這種方法不使用遞迴查詢演算法,有更高的查詢效率。

我們首先將多級資料按照下面的方式畫在紙上,在根節點Food的左側寫上 1 然後沿著這個樹繼續向下 在 Fruit 的左側寫上 2
然後繼續前進,沿著整個樹的邊緣給每一個節點都標上左側和右側的數字。最後一個數字是標在Food 右側的
18。在下面的這張圖中你可以看到整個標好了數位多級結構。(沒有看懂?用你的手指指著數字從1數到18就明白怎麼回事了。還不明白,再數一遍,注意移
動你的手指)。
這些數字標明了各個節點之間的關係,"Red"的號是3和6,它是 "Food" 1-18 的子孫節點。 同樣,我們可以看到
所有左值大於2和右值小於11的節點 都是"Fruit" 2-11 的子孫節點

以下是代碼:

  1.                    
         1 Food 18
  2.                              |
  3.             +------------------------------+
  4.             |                              |
  5.         2 Fruit 11                     12 Meat 17
  6.             |                              |
  7.     +-------------+                 +------------+
  8.     |             |                 |            |
  9. 3 Red 6      7 Yellow 10       13 Beef 14   15 Pork 16
  10.     |             |
  11. 4 Cherry 5    8 Banana 9

複製代碼

這樣整個樹狀結構可以通過左右值來儲存到資料庫中。繼
續之前,我們看一看下面整理過的資料表。

以下是代碼:

  1. +----------+------------+-----+-----+
  2. |  parent  |    name    | lft | rgt |
  3. +----------+------------+-----+-----+
  4. |          |    Food    | 1   | 18  |
  5. |   Food   |   Fruit    | 2   | 11  |
  6. |   Fruit  |    Red     | 3   |  6  |
  7. |   Red    |    Cherry  | 4   |  5  |
  8. |   Fruit  |    Yellow  | 7   | 10  |
  9. |   Yellow |    Banana  | 8   |  9  |
  10. |   Food   |    Meat    | 12  | 17  |
  11. |   Meat   |    Beef    | 13  | 14  |
  12. |   Meat   |    Pork    | 15  | 16  |
  13. +----------+------------+-----+-----+

複製代碼

注意:由於"left"和"right"在
SQL中有特殊的意義,所以我們需要用"lft"和"rgt"來表示左右欄位。 另外這種結構中不再需要"parent"欄位來表示樹狀結構。也就是
說下面這樣的表結構就足夠了。

以下是代碼:

  1. +------------+-----+-----+
  2. |    name    | lft | rgt |
  3. +------------+-----+-----+
  4. |    Food    | 1   | 18  |
  5. |    Fruit   | 2   | 11  |
  6. |    Red     | 3   |  6  |
  7. |    Cherry  | 4   |  5  |
  8. |    Yellow  | 7   | 10  |
  9. |    Banana  | 8   |  9  |
  10. |    Meat    | 12  | 17  |
  11. |    Beef    | 13  | 14  |
  12. |    Pork    | 15  | 16  |
  13. +------------+-----+-----+

複製代碼

好了我們現在可以從資料庫中擷取資料了,例如我們需要
得到"Fruit"項下的所有所有節點就可以這樣寫查詢語句:

  1. SELECT
    * FROM tree WHERE lft BETWEEN 2 AND 11;

複製代碼

這個查詢得到了以下的結果。

以下是代碼:

  1. +------------+-----+-----+
  2. |    name    | lft | rgt |
  3. +------------+-----+-----+
  4. |    Fruit   | 2   | 11  |
  5. |    Red     | 3   |  6  |
  6. |    Cherry  | 4   |  5  |
  7. |    Yellow  | 7   | 10  |
  8. |    Banana  | 8   |  9  |
  9. +------------+-----+-----+

複製代碼

看到了吧,只要一個查詢就可以得到所有這些節點。為
了能夠像上面的遞迴函式那樣顯示整個樹狀結構,我們還需要對這樣的查詢進行排序。用節點的左值進行排序:

  1. SELECT * FROM tree WHERE lft BETWEEN 2 AND 11 ORDER
    BY lft ASC;

複製代碼


下的問題如何顯示層級的縮排了。
以下是代碼:

  1. <?php
  2. function display_tree($root) {
  3.     // 得到根節點的左右值
  4.     $result = mysql_query("
  5.         SELECT lft, rgt
  6.         FROM tree
  7.         WHERE name = '" . $root . "'
  8.         ;"
  9.     );
  10.     $row = mysql_fetch_array($result);
  11.     // 準備一個空的右值堆棧
  12.     $right = array();
  13.     // 獲得根基點的所有子孫節點
  14.     $result = mysql_query("
  15.         SELECT name, lft, rgt
  16.         FROM tree
  17.         WHERE lft BETWEEN '" . $row['lft'] . "' AND '" .
    $row['rgt'] ."'
  18.         ORDER BY lft ASC
  19.         ;"
  20.     );
  21.     // 顯示每一行
  22.     while ($row = mysql_fetch_array($result)) {
  23.         // only check stack if there is one
  24.         if (count($right) > 0) {
  25.             // 檢查我們是否應該將節點移出堆棧
  26.             while ($right[count($right) - 1] < $row['rgt']) {
  27.                 array_pop($right);
  28.             }
  29.         }
  30.         // 縮排顯示節點的名稱
  31.         echo str_repeat('  ',count($right)) . $row['name'] .
    "/n";
  32.         // 將這個節點加入到堆棧中
  33.         $right[] = $row['rgt'];
  34.     }
  35. }
  36. ?>

複製代碼


果你運行一下以上的函數就會得到和遞迴函式一樣的結果。只是我們的這個新的函數可能會更快一些,因為只有2次資料庫查詢。
要獲知一個節點的路徑就更簡單了,如果我們想知道Cherry 的路徑就利用它的左右值4和5來做一個查詢。

  1. SELECT name FROM tree WHERE
    lft < 4 AND rgt >; 5 ORDER BY lft ASC;

複製代碼

這樣就會得到以下的結果:

以下是代碼:

  1. +------------+
  2. |    name    |
  3. +------------+
  4. |    Food    |
  5. |    Fruit   |
  6. |    Red     |
  7. +------------+

複製代碼

那麼某個節點到底有多少子孫節點呢?很簡單,子孫總
數=(右值-左值-1)/2

  1. descendants
    = (right – left - 1) / 2

複製代碼

不相信?自己算一算啦。
用這個簡單的公式,我們可以很快的算出"Fruit 2-11"節點有4個子孫節點,而"Banana
8-9"節點沒有子孫節點,也就是說它不是一個父節點了。
很神奇吧?雖然我已經多次用過這個方法,但是每次這樣做的時候還是感到很神奇。
這的確是個很好的辦法,但是有什麼辦法能夠幫我們建立這樣有左右值的資料表呢?這裡再介紹一個函數給大家,這個函數可以將name和parent結構的表
自動轉換成帶有左右值的資料表。
以下是代碼:

  1. <?php
  2. function rebuild_tree($parent, $left) {
  3.     // the right value of this node is the left value + 1
  4.     $right = $left+1;
  5.     // get all children of this node
  6.     $result = mysql_query("
  7.         SELECT name
  8.         FROM tree
  9.         WHERE parent = '" . $parent . "'
  10.         ;"
  11.     );
  12.     while ($row = mysql_fetch_array($result)) {
  13.         // recursive execution of this function for each
  14.         // child of this node
  15.         // $right is the current right value, which is
  16.         // incremented by the rebuild_tree function
  17.         $right = rebuild_tree($row['name'], $right);
  18.     }
  19.     // we've got the left value, and now that we've processed
  20.     // the children of this node we also know the right value
  21.     mysql_query("
  22.         UPDATE tree
  23.         SET
  24.             lft = '" . $left . "',
  25.             rgt= '" . $right . "'
  26.         WHERE name = '" . $parent . "'
  27.         ;"
  28.     );
  29.     // return the right value of this node + 1
  30.     return $right + 1;
  31. }
  32. ?>

複製代碼


然這個函數是一個遞迴函式,我們需要從根節點開始運行這個函數來重建一個帶有左右值的樹

  1. rebuild_tree('Food',1);

複製代碼

這個函數看上去有些複雜,但是它的作用和手工對錶進
行編號一樣,就是將立體多層結構的轉換成一個帶有左右值的資料表。

那麼對於這樣的結構我們該如何增加,更新和刪除一個節點呢?
增加一個節點一般有兩種方法:
第一種,保留原有的name
和parent結構,用老方法向資料中添加資料,每增加一條資料以後使用rebuild_tree函數對整個結構重新進行一次編號。
第二種,效率更高的辦法是改變所有位於新節點右側的數值。舉例來說:我們想增加一種新的水果"Strawberry"(草莓)它將成為"Red"節點的最
後一個子節點。首先我們需要為它騰出一些空間。"Red"的右值應當從6改成8,"Yellow 7-10 "的左右值則應當改成
9-12。依次類推我們可以得知,如果要給新的值騰出空間需要給所有左右值大於5的節點 (5 是"Red"最後一個子節點的右值)
加上2。所以我們這樣進行資料庫操作:

  1. UPDATE
    tree SET rgt = rgt + 2 WHERE rgt > 5;
  2. UPDATE tree SET lft = lft + 2 WHERE lft > 5;

複製代碼

這樣就為新插入的值騰出了空間,現在可以在騰出的
空間裡建立一個新的資料節點了, 它的左右值分別是6和7

  1. INSERT
    INTO tree SET lft=6, rgt=7, name='Strawberry';

複製代碼

再做一次查詢看看吧!怎麼樣?很快吧。

四、結語
好了,現在你可以用兩種不同的方法設計你的多級資料庫結構了,採用何種方式完全取決於你個人的判斷,但是對於層次多數量大的結構我更喜歡第二種方法。如果
查詢量較小但是需要頻繁添加和更新的資料,則第一種方法更為簡便。
另外,如果資料庫支援的話 你還可以將rebuild_tree()和 騰出空間的操作寫成資料庫端的觸發器函數, 在插入和更新的時候自動執行,
這樣可以得到更好的運行效率, 而且你添加新節點的SQL語句會變得更加簡單。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.