国产成人精品久久免费动漫-国产成人精品天堂-国产成人精品区在线观看-国产成人精品日本-a级毛片无码免费真人-a级毛片毛片免费观看久潮喷

您的位置:首頁技術文章
文章詳情頁

php:樹形結構的算法

瀏覽:76日期:2024-02-12 16:48:15

從喜悅村上轉載,以前也讀過此文,講述得還是比較清楚的。產品分類,多級的樹狀結構的論壇,郵件列表等許多地方我們都會遇到這樣的問題:如何存儲多級結構的數據?

在PHP的應用中,提供后臺數據存儲的通常是關系型數據庫,它能夠保存大量的數據,提供高效的數據檢索和更新服務。然而關系型數據的基本形式是縱橫交錯的表,是一個平面的結構,如果要將多級樹狀結構存儲在關系型數據庫里就需要進行合理的翻譯工作。接下來我會將自己的所見所聞和一些實用的經驗和大家探討一下。

層級結構的數據保存在平面的數據庫中基本上有兩種常用設計方法:

毗鄰目錄模式(adjacency list model) 預排序遍歷樹算法(modified preorder tree traversal algorithm) 我不是計算機專業的,也沒有學過什么數據結構的東西,所以這兩個名字都是我自己按照字面的意思翻的,如果說錯了還請多多指教。

這兩個東西聽著好像很嚇人,其實非常容易理解。這里我用一個簡單食品目錄作為我們的示例數據。 我們的數據結構是這樣的:

Food||---Fruit| || |---Red| | || | |--Cherry| | | |---Yellow | || |--Banana||---Meat| |--Beef||--Pork為了照顧那些英文一塌糊涂的PHP愛好者

Food:食物Fruit:水果Red:紅色Cherry:櫻桃Yellow:黃色Banana:香蕉Meat:肉類Beef:牛肉Pork:豬肉

毗鄰目錄模式(adjacency list model)

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

+-----------------------+| parent | name |+-----------------------+| | Food || Food | Fruit || Fruit | Green || Green | Pear || Fruit | Red || Red | Cherry || Fruit | Yellow || Yellow | Banana || Food | Meat || Meat | Beef || Meat | Pork |+-----------------------+我們看到 Pear 是Green的一個子節點,Green是Fruit的一個子節點。而根節點'Food'沒有父節點。 為了簡單地描述這個問題, 這個例子中只用了name來表示一個記錄。 在實際的數據庫中,你需要用數字的id來標示每個節點,數據庫的表結構大概應該像這樣:id, parent_id, name, description。有了這樣的表我們就可以通過數據庫保存整個多級樹狀結構了。

顯示多級樹如果我們需要顯示這樣的一個多級結構需要一個遞歸函數。

<?php// $parent is the parent of the children we want to see // $level is increased when we go deeper into the tree, // used to display a nice indented tree

function display_children($parent, $level) { // 獲得一個 父節點 $parent 的所有子節點 $result = mysql_query('SELECT name FROM tree '. 'WHERE parent=''.$parent.'';');

// 顯示每個子節點 while ($row = mysql_fetch_array($result)) { // 縮進顯示節點名稱 echo str_repeat(' ',$level).$row['name'].'n';

//再次調用這個函數顯示子節點的子節點

display_children($row['name'], $level+1); } }?> 對整個結構的根節點(Food)使用這個函數就可以打印出整個多級樹結構,由于Food是根節點它的父節點是空的,所以這樣調用: display_children('',0)。將顯示整個樹的內容:

FoodFruitRedCherryYellowBananaMeatBeefPork如果你只想顯示整個結構中的一部分,比如說水果部分,就可以這樣調用:

display_children('Fruit',0);

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

<?php// $node 是那個最深的節點 function get_path($node) { // 查詢這個節點的父節點 $result = mysql_query('SELECT parent FROM tree '. 'WHERE name=''.$node.'';'); $row = mysql_fetch_array($result);

// 用一個數組保存路徑 $path = array();

// 如果不是根節點則繼續向上查詢 // (根節點沒有父節點) if ($row['parent']!='') { // the last part of the path to $node, is the name // of the parent of $node $path[] = $row['parent'];

// we should add the path to the parent of this node // to the path $path = array_merge(get_path($row['parent']), $path); }

// return the path return $path; }?> 如果對'Cherry'使用這個函數:print_r(get_path('Cherry')),就會得到這樣的一個數組了:

Array([0] => Food[1] => Fruit[2] => Red)接下來如何把它打印成你希望的格式,就是你的事情了。 缺點:這種方法很簡單,容易理解,好上手。但是也有一些缺點。主要是因為運行速度很慢,由于得到每個節點都需要進行數據庫查詢,數據量大的時候要進行很多查詢才能完成一個樹。另外由于要進行遞歸運算,遞歸的每一級都需要占用一些內存所以在空間利用上效率也比較低。

現在讓我們看一看另外一種不使用遞歸計算,更加快速的方法,這就是預排序遍歷樹算法(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 Food 18|+---------------------------------------+| |2 Fruit 11 12 Meat 17| |+------------------------+ +---------------------+| | | |3 Red 6 7 Yellow 10 13 Beef 14 15 Pork 16| |4 Cherry 5 8 Banana 9

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

+-----------------------+-----+-----+| parent | name | lft | rgt |+-----------------------+-----+-----+| | Food | 1 | 18 || Food | Fruit | 2 | 11 || Fruit | Red | 3 | 6 || Red | Cherry | 4 | 5 || Fruit | Yellow | 7 | 10 || Yellow | Banana | 8 | 9 || Food | Meat | 12 | 17 || Meat | Beef | 13 | 14 || Meat | Pork | 15 | 16 |+-----------------------+-----+-----+注意:由于'left'和'right'在 SQL中有特殊的意義,所以我們需要用'lft'和'rgt'來表示左右字段。 另外這種結構中不再需要'parent'字段來表示樹狀結構。也就是 說下面這樣的表結構就足夠了。

+------------+-----+-----+| name | lft | rgt |+------------+-----+-----+| Food | 1 | 18 || Fruit | 2 | 11 || Red | 3 | 6 || Cherry | 4 | 5 || Yellow | 7 | 10 || Banana | 8 | 9 || Meat | 12 | 17 || Beef | 13 | 14 || Pork | 15 | 16 |+------------+-----+-----+好了我們現在可以從數據庫中獲取數據了,例如我們需要得到'Fruit'項下的所有所有節點就可以這樣寫查詢語句: SELECT * FROM tree WHERE lft BETWEEN 2 AND 11; 這個查詢得到了以下的結果。

+------------+-----+-----+| name | lft | rgt |+------------+-----+-----+| Fruit | 2 | 11 || Red | 3 | 6 || Cherry | 4 | 5 || Yellow | 7 | 10 || Banana | 8 | 9 |+------------+-----+-----+看到了吧,只要一個查詢就可以得到所有這些節點。為了能夠像上面的遞歸函數那樣顯示整個樹狀結構,我們還需要對這樣的查詢進行排序。用節點的左值進行排序:

SELECT * FROM tree WHERE lft BETWEEN 2 AND 11 ORDER BY lft ASC;剩下的問題如何顯示層級的縮進了。

<?phpfunction display_tree($root) { // 得到根節點的左右值 $result = mysql_query('SELECT lft, rgt FROM tree '.'WHERE name=''.$root.'';'); $row = mysql_fetch_array($result);

// 準備一個空的右值堆棧 $right = array();

// 獲得根基點的所有子孫節點 $result = mysql_query('SELECT name, lft, rgt FROM tree '. 'WHERE lft BETWEEN '.$row['lft'].' AND '. $row['rgt'].' ORDER BY lft ASC;');

// 顯示每一行 while ($row = mysql_fetch_array($result)) { // only check stack if there is one if (count($right)>0) { // 檢查我們是否應該將節點移出堆棧 while ($right[count($right)-1]<$row['rgt']) { array_pop($right); } }

// 縮進顯示節點的名稱 echo str_repeat(' ',count($right)).$row['name'].'n';

// 將這個節點加入到堆棧中 $right[] = $row['rgt']; } }?> 如果你運行一下以上的函數就會得到和遞歸函數一樣的結果。只是我們的這個新的函數可能會更快一些,因為只有2次數據庫查詢。 要獲知一個節點的路徑就更簡單了,如果我們想知道Cherry 的路徑就利用它的左右值4和5來做一個查詢。

SELECT name FROM tree WHERE lft < 4 AND rgt > 5 ORDER BY lft ASC;這樣就會得到以下的結果:

+------------+| name |+------------+| Food || Fruit || Red |+------------+那么某個節點到底有多少子孫節點呢?很簡單,子孫總數=(右值-左值-1)/2 descendants = (right – left - 1) / 2 不相信?自己算一算啦。用這個簡單的公式,我們可以很快的算出'Fruit 2-11'節點有4個子孫節點,而'Banana 8-9'節點沒有子孫節點,也就是說它不是一個父節點了。 很神奇吧?雖然我已經多次用過這個方法,但是每次這樣做的時候還是感到很神奇。

這的確是個很好的辦法,但是有什么辦法能夠幫我們建立這樣有左右值的數據表呢?這里再介紹一個函數給大家,這個函數可以將name和parent結構的表自動轉換成帶有左右值的數據表。

<?phpfunction rebuild_tree($parent, $left) { // the right value of this node is the left value + 1 $right = $left+1;

// get all children of this node $result = mysql_query('SELECT name FROM tree '. 'WHERE parent=''.$parent.'';'); while ($row = mysql_fetch_array($result)) { // recursive execution of this function for each // child of this node // $right is the current right value, which is // incremented by the rebuild_tree function $right = rebuild_tree($row['name'], $right); }

// we've got the left value, and now that we've processed // the children of this node we also know the right value mysql_query('UPDATE tree SET lft='.$left.', rgt='. $right.' WHERE name=''.$parent.'';');

// return the right value of this node + 1 return $right+1; }?> 當然這個函數是一個遞歸函數,我們需要從根節點開始運行這個函數來重建一個帶有左右值的樹

rebuild_tree('Food',1);這個函數看上去有些復雜,但是它的作用和手工對表進行編號一樣,就是將立體多層結構的轉換成一個帶有左右值的數據表。

那么對于這樣的結構我們該如何增加,更新和刪除一個節點呢? 增加一個節點一般有兩種方法:

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

UPDATE tree SET rgt=rgt+2 WHERE rgt>5;UPDATE tree SET lft=lft+2 WHERE lft>5;這樣就為新插入的值騰出了空間,現在可以在騰出的空間里建立一個新的數據節點了, 它的左右值分別是6和7

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

再做一次查詢看看吧!怎么樣?很快吧。 好了,現在你可以用兩種不同的方法設計你的多級數據庫結構了,采用何種方式完全取決于你個人的判斷,但是對于層次多數量大的結構我更喜歡第二種方法。如果查詢量較小但是需要頻繁添加和更新的數據,則第一種方法更為簡便。

另外,如果數據庫支持的話 你還可以將 rebuild_tree() 和 騰出空間的操作寫成數據庫端的觸發器函數, 在插入和更新的時候自動執行, 這樣可以得到更好的運行效率, 而且你添加新節點的SQL語句會變得更加簡單。類遞歸法Posted by 訪客 on 2004, May 31 - 9:18am.我用類 遞歸法 寫了段程序,跟文章中的遞歸不完全一樣正準備移植到 xoops 中: http://dev.xoops.org/modules/xfmod/project/?ulink

已經出現內存溢出現象不過準備繼續采用遞歸法,只是需要繼續改進

希望有機會跟各位討論cms» reply to this comment還是兩種方法之比較Posted by 訪客 on 2004, March 17 - 8:30pm.仔細研究了一下這篇文章,覺得受益非淺,但后來又想了想,覺得有一下問題(為了好記憶,毗鄰目錄模式我稱為遞歸的方法,預排序遍歷樹算法我稱為預排序樹的方法):

1、兩種方法比較大的區別是遞歸是在查詢的時候要用到堆棧進行遞歸,預排序樹則是在更新節點時要進行半數(指所插入節點的后半部分)節點的更新。雖然您也說了,如果節點多了,更新又頻繁,預排序樹效率會降低,采用遞歸會好些,而如果節點層次較多的話,首先遞歸會導致堆棧溢出,再者遞歸本身效率就不高,加上每一層遞歸都要操作數據庫,總體效果也不會理想。我目前的做法是一次性把數據全取出來,然后對數組進行遞歸操作,會好一些;再進一步改進的話,可以為每行記錄增加一個ROOT根節點(目前是只記錄相鄰的父節點),這樣在查找分支樹時效率就會比較高了,更新樹的時候也是十分便捷的,應該是一種比較好的方式。

2、改進遞歸的方式,文章中在計算預排序樹節點的左右值的時候其實也用到了一種遍歷方式,通過數組替代堆棧,手工實現壓棧和彈出;這種方法如果引用到遞歸算法中,在進行遞歸的時候也用數組替代堆棧的話,也可以提高遞歸的效率的。

3、并發,如果考慮到并發的情況,尤其是更新樹的時候,預排序樹大面積更新節點信息的方法需要額外注意采用加鎖和事務的機制保證數據一致性。

4、多根節點或者多父節點的情況,在這種情況下,顯然就不是一個標準的二叉樹或者多叉樹了,預排序樹算法需要進行比較大的改進才能適應,而遞歸的方法則應用自如,所以在這種情況下,遞歸的適應性較強。這是當然的了,因為遞歸的方法就是鏈表的一種形式,樹、圖都可以用鏈表來表達,當然適應力強了。

5、直觀,如果不用程序操作,直接觀察數據庫中存儲的數據的話,顯然遞歸方式下存儲的數據比較直觀,而預排序樹的數據很難直接閱讀(針對層次關系來說),這在數據交換中是不是會有影響呢?

總體來說,我個人比較喜歡用遞歸的方法,但一直擔心遞歸對效率的影響,所幸還沒有接觸過規模較大的分類層次,遞歸用數組替代堆棧會是一種比較好的改進方法。而預排序樹不失為一種解決簡單樹的高效方法,用習慣了,也應該是非常出色的,尤其是它從葉子節點到根節點的反向查找非常方便。

Fwolfwww.fwolf.com» reply to this comment非常高興看到你的回復Posted by shuke on 2004, March 18 - 5:47am.非常高興你這么認真的讀完這篇文章。這篇文章其實是原來發表在sitepoint.com上的,我把它翻譯了一下,希望給希望初學入門的朋友介紹一些方法,拋磚引玉。你的方法也很好,有機會我會試一下的。(如果你有興趣的話,何不就上面的例子把你的方法和具體實現的代碼也寫成教程發出來吧,這樣大家就用更加實際的例子來模仿了)如果你對數據庫中保存多級結構有興趣研究的話,這里還有兩個連接也很不錯可以作為參考: 介紹了常見的4中方法 一次查詢,數組排序的腳本我想你的腳本肯定比這個強。 另外我看到你也用drupal,它還有一個高級功能叫分布式用戶驗證系統,只要在任何一個drupal的站點注冊以后就可以登錄訪問其它的drupal站點了。挺有意思的。祝好!» reply to this comment 用循環來建樹已經實現了Posted by 訪客 on 2004, March 25 - 10:10pm.你上次提供的資料我已經都看過了,不過老實說,第一篇文章里沒有太多新東西,或許是我沒看太明白吧,第二個居然是PHP3寫的,程序結構沒有細看,用到太多的函數交叉。正好我在一個系統中用戶角色要用到分級,按照數組的思路就把遍歷寫了下來,沒有時間整理,先放到這里你看看吧,數據庫用的是ADODB,程序是直接從系統中摘出來的,希望能夠描述得清楚,主要是利用了PHP強大的數組操作,用循環來進行遞歸。注釋里是一種相近的方法,只是處理結果的時機不同而已。

<?php/** * 顯示列表 * @access public */ function DispList() { //不縮進的顯示方式 // $this->mIsDispListIndex = true; // echo('<p align='right'><a href='http://www.cgvv.com.cn/bcjs/?action=new&part=role'>增加新角色</a></p>'); _fcksavedurl=''?action=new&part=role'>增加新角色</a></p>');' // // $this->mListTitle = '用戶角色列表'; // $this->SetDataOption('list'); // // $this->SetQueryTable( array($this->mTableUserRole) ); // // //查詢順序 // $this->SetQueryOrder( 'asc', $this->mTableUserRole, 'sequence' ); // // $this->Query('list'); // parent::DispList();

// //另外一種顯示方式,用數組作為堆棧,A: 壓棧時存role,壓完就刪除source // $this->CheckProperty('mrDb'); // $this->CheckProperty('mrSql'); // $this->mrSql->Select('role, title, parent'); // $this->mrSql->From($this->mTableUserRole); // $this->mrSql->Orderby('parent, sequence'); // $this->mRs = $this->mrDb->Execute($this->mrSql->Sql()); // if (0 < count($this->mRs)) // { // $source = & $this->mRs->GetArray(); //數字索引 // $stack = array(''); //堆棧 // $stacki = array(-1); //和堆棧對應,記錄堆棧中數據在樹中的層次 // $target = array(); // while (0 < count($stack)) // { // $item = array_shift($stack); // $lev = array_shift($stacki); // if (!empty($item)) // { // //在這里把加工過的數據放到target數組 // array_push($target, str_repeat(' ', $lev) . $item); // //$s1 = str_repeat(' ', $lev) . $item; // } // $del = array(); //要從$source中刪除的節點 // $ar = array(); //需要添加到堆棧中的節點 // foreach ($source as $key=>$val) // { // //尋找匹配的子節點 // if (empty($item)) // { // $find = empty($source[$key]['parent']); // } // else // { // $find = ($item == $source[$key]['parent']); // } // if ($find) // { // array_unshift($ar, $source[$key]['role']); // $del[] = $key; // } // } // foreach ($ar as $val) // { // array_unshift($stack, $val); // array_unshift($stacki, $lev + 1); // } // foreach ($del as $val) // { // unset($source[$val]); // } // echo(implode(', ', $stack) . '<br />' . implode(', ', $stacki) . '<br />' . implode(', ', $target) . '<br /><br />'); // } // debug_array(); // } // else // { // echo('<center>沒有檢索到數據</center>'); // }

//另外一種顯示方式,用數組作為堆棧,B: 壓棧時存數組索引,出棧并使用完后再刪除source $this->CheckProperty('mrDb'); $this->CheckProperty('mrSql'); $this->mrSql->Select('role, title, parent'); $this->mrSql->From($this->mTableUserRole); $this->mrSql->Orderby('parent, sequence'); $this->mRs = $this->mrDb->Execute($this->mrSql->Sql()); if (!empty($this->mRs) && !$this->mRs->EOF) { $source = & $this->mRs->GetArray(); //數字索引 $stack = array(-1); //堆棧 $stacki = array(-1); //和堆棧對應,記錄堆棧中數據在樹中的層次 $target = array(); while (0 < count($stack)) { $item = array_shift($stack); $lev = array_shift($stacki); if (-1 != $item) { //在這里把加工過的數據放到target數組 $s1 = str_repeat('; ', $lev) . '<a href='http://www.cgvv.com.cn/bcjs/?action=disp&part=role&role=' . $source[$item]['role'] . ''>' . $source[$item]['title'] . '</a>'; $s2 = '<a href='http://www.cgvv.com.cn/bcjs/?action=edit&part=role&role=' . $source[$item]['role'] . ''>編輯</a> <a href='http://www.cgvv.com.cn/bcjs/?action=delete&part=role&role=' . $source[$item]['role'] . ''>刪除</a>'; array_push($target, array($s1, $s2)); } $del = array(); //要從$source中刪除的節點 $ar = array(); //需要添加到堆棧中的節點 foreach ($source as $key=>$val) { //尋找匹配的子節點 if (-1 == $item) { $find = empty($source[$key]['parent']); } else { $find = ($source[$item]['role'] == $source[$key]['parent']); } if ($find) { array_unshift($ar, $key); } } foreach ($ar as $val) { array_unshift($stack, $val); array_unshift($stacki, $lev + 1); } //從source中刪除 unset($source[$item]); //echo(implode(', ', $stack) . '<br />' . implode(', ', $stacki) . '<br />' . implode(', ', $target) . '<br /><br />'); } //輸出 echo('<p align='right'><a href='http://www.cgvv.com.cn/bcjs/?action=new&part=role'>增加新角色</a></p>'); array_unshift($target, array('角色', '操作')); $this->CheckProperty('mrLt'); $this->mrLt->SetData($target); $this->mrLt->mListTitle = '用戶角色列表'; $this->mrLt->mIsDispIndex = false; $this->mrLt->Disp(); } else { echo('<center>沒有檢索到數據</center>'); } } // end of function DispList?>

標簽: PHP
主站蜘蛛池模板: 欧美成人a级在线视频 | 欧美日比视频 | 亚洲视频免费播放 | 欧美日韩精品国产一区在线 | 久久视屏这里只有精品6国产 | 国产精品毛片va一区二区三区 | 国产成人亚洲综合91精品555 | 久久精品视频一区二区三区 | 欧美福利一区二区三区 | 欧美日韩一区二区三区高清不卡 | 免费人成黄页网站在线观看 | 私人玩物福利视频 | 日韩中文字幕在线观看视频 | 国产成人精品亚洲日本语音 | 日韩精品a | 日韩视频一区二区 | 亚洲视频精选 | 免费播放欧美毛片 | 欧美成人a| 国产精品极品美女自在线看免费一区二区 | 国产欧美成人免费观看视频 | 中文字幕一区二区三 | 手机在线免费看毛片 | 国产三级日本三级在线播放 | 在线观看国产情趣免费视频 | 国产欧美日韩一区二区三区 | 久久成人免费观看草草影院 | 九九精品久久久久久噜噜 | 精品午夜寂寞影院在线观看 | 一级毛片在播放免费 | 久久99精品久久久久久三级 | 波多野结衣视频免费观看 | 亚洲精品大片 | 国产成人一级片 | 日本一区二区三区高清在线观看 | 香蕉依依精品视频在线播放 | 91看片淫黄大片欧美看国产片 | 能看毛片的网址 | 国产精品久久久久999 | 在线播放日韩 | 国产免费人成在线看视频 |