mikespook 回復于:2004-01-08 15:28:19 |
收獲頗多~~~
子孫總數=(左-右-1)/2 雖然一直知道這個事實可是一直都不知道這玩意有什么用~~ 打印出來,值得慢慢讀! |
tonera 回復于:2004-01-08 15:28:26 |
收了,支持你寫書! :D |
夜貓子 回復于:2004-01-08 22:22:22 |
產品分類,多級的樹狀結構的論壇,郵件列表等許多地方我們都會遇到這樣的問題:如何存儲多級結構的數據?在PHP的應用中,提供后臺數據存儲的通常是關系型數據庫,它能夠保存大量的數據,提供高效的數據檢索和更新服務。然而關系型數據的基本形式是縱橫交錯的表,是一個平面的結構,如果要將多級樹狀結構存儲在關系型數據庫里就需要進行合理的翻譯工作。接下來我會將自己的所見所聞和一些實用的經驗和大家探討一下:
層級結構的數據保存在平面的數據庫中基本上有兩種常用設計方法: 1、毗鄰目錄模式(adjacency list model) 2、預排序遍歷樹算法(modified preorder tree traversal algorithm) 我不是計算機專業的,也沒有學過什么數據結構的東西,所以這兩個名字都是我自己按照字面的意思翻的,如果說錯了還請多多指教。 這兩個東西聽著好像很嚇人,其實非常容易理解。這里我用一個簡單食品目錄作為我們的示例數據。 我們的數據結構是這樣的 以下是代碼: [code:1:86c003e7bb] Food | |---Fruit | | | |---Red | | | | | |--Cherry | | | |---Yellow | | | |--Banana | |---Meat | |--Beef | |--Pork [/code:1:86c003e7bb] 為了照顧那些英文一塌糊涂的PHP愛好者 Food:食物 Fruit:水果 Red:紅色 Cherry:櫻桃 Yellow:黃色 Banana:香蕉 Meat:肉類 Beef:牛肉 Pork:豬肉 毗鄰目錄模式(adjacency list model) 這種模式我們經常用到,很多的教程和書中也介紹過。我們通過給每個節點增加一個屬性 parent 來表示這個節點的父節點從而將整個樹狀結構通過平面的表描述出來。根據這個原則,例子中的數據可以轉化成如下的表: 以下是代碼: [code:1:86c003e7bb] +-----------------------+ | parent | name | +-----------------------+ | | Food | | Food | Fruit | | Fruit | Green | | Green | Pear | | Fruit | Red | | Red | Cherry | | Fruit | Yellow | | Yellow | Banana | | Food | Meat | | Meat | Beef | | Meat | Pork | +-----------------------+ [/code:1:86c003e7bb] 我們看到 Pear 是Green的一個子節點,Green是Fruit的一個子節點。而根節點'Food'沒有父節點。 為了簡單地描述這個問題, 這個例子中只用了name來表示一個記錄。 在實際的數據庫中,你需要用數字的id來標示每個節點,數據庫的表結構大概應該像這樣:id, parent_id, name, description。 有了這樣的表我們就可以通過數據庫保存整個多級樹狀結構了。 顯示多級樹 如果我們需要顯示這樣的一個多級結構需要一個遞歸函數。 以下是代碼: [code:1:86c003e7bb] <?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); } } ?> [/code:1:86c003e7bb] 對整個結構的根節點(Food)使用這個函數就可以打印出整個多級樹結構,由于Food是根節點它的父節點是空的,所以這樣調用: display_children('',0)。將顯示整個樹的內容: [code:1:86c003e7bb] Food Fruit Red Cherry Yellow Banana Meat Beef Pork [/code:1:86c003e7bb] 如果你只想顯示整個結構中的一部分,比如說水果部分,就可以這樣調用:display_children('Fruit',0); 幾乎使用同樣的方法我們可以知道從根節點到任意節點的路徑。比如 Cherry 的路徑是 "Food > Fruit > Red"。 為了得到這樣的一個路徑我們需要從最深的一級"Cherry"開始, 查詢得到它的父節點"Red"把它添加到路徑中, 然后我們再查詢Red的父節點并把它也添加到路徑中,以此類推直到最高層的"Food" 以下是代碼: [code:1:86c003e7bb] <?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; } ?> [/code:1:86c003e7bb] 如果對"Cherry"使用這個函數:print_r(get_path('Cherry')),就會得到這樣的一個數組了: [code:1:86c003e7bb] Array ( [0] => Food [1] => Fruit [2] => Red ) [/code:1:86c003e7bb] 接下來如何把它打印成你希望的格式,就是你的事情了。 缺點: 這種方法很簡單,容易理解,好上手。但是也有一些缺點。主要是因為運行速度很慢,由于得到每個節點都需要進行數據庫查詢,數據量大的時候要進行很多查詢才能完成一個樹。另外由于要進行遞歸運算,遞歸的每一級都需要占用一些內存所以在空間利用上效率也比較低。 現在讓我們看一看另外一種不使用遞歸計算,更加快速的方法,這就是預排序遍歷樹算法(modified preorder tree traversal algorithm) 這種方法大家可能接觸的比較少,初次使用也不像上面的方法容易理解,但是由于這種方法不使用遞歸查詢算法,有更高的查詢效率。 我們首先將多級數據按照下面的方式畫在紙上,在根節點Food的左側寫上 1 然后沿著這個樹繼續向下 在 Fruit 的左側寫上 2 然后繼續前進,沿著整個樹的邊緣給每一個節點都標上左側和右側的數字。最后一個數字是標在Food 右側的 18。 在下面的這張圖中你可以看到整個標好了數字的多級結構。(沒有看懂?用你的手指指著數字從1數到18就明白怎么回事了。還不明白,再數一遍,注意移動你的手指)。 這些數字標明了各個節點之間的關系,"Red"的號是3和6,它是 "Food" 1-18 的子孫節點。 同樣,我們可以看到 所有左值大于2和右值小于11的節點 都是"Fruit" 2-11 的子孫節點 以下是代碼: [code:1:86c003e7bb] 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 [/code:1:86c003e7bb] 這樣整個樹狀結構可以通過左右值來存儲到數據庫中。繼續之前,我們看一看下面整理過的數據表。 以下是代碼: [code:1:86c003e7bb] +-----------------------+-----+-----+ | 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 | +-----------------------+-----+-----+ [/code:1:86c003e7bb] 注意:由于"left"和"right"在 SQL中有特殊的意義,所以我們需要用"lft"和"rgt"來表示左右字段。 另外這種結構中不再需要"parent"字段來表示樹狀結構。也就是 說下面這樣的表結構就足夠了。 以下是代碼: [code:1:86c003e7bb] +------------+-----+-----+ | 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 | +------------+-----+-----+ [/code:1:86c003e7bb] 好了我們現在可以從數據庫中獲取數據了,例如我們需要得到"Fruit"項下的所有所有節點就可以這樣寫查詢語句: SELECT * FROM tree WHERE lft BETWEEN 2 AND 11; 這個查詢得到了以下的結果。 以下是代碼: [code:1:86c003e7bb] +------------+-----+-----+ | name | lft | rgt | +------------+-----+-----+ | Fruit | 2 | 11 | | Red | 3 | 6 | | Cherry | 4 | 5 | | Yellow | 7 | 10 | | Banana | 8 | 9 | +------------+-----+-----+ [/code:1:86c003e7bb] 看到了吧,只要一個查詢就可以得到所有這些節點。為了能夠像上面的遞歸函數那樣顯示整個樹狀結構,我們還需要對這樣的查詢進行排序。用節點的左值進行排序: SELECT * FROM tree WHERE lft BETWEEN 2 AND 11 ORDER BY lft ASC; 剩下的問題如何顯示層級的縮進了。 以下是代碼: [code:1:86c003e7bb] <?php function 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']; } } ?> [/code:1:86c003e7bb] 如果你運行一下以上的函數就會得到和遞歸函數一樣的結果。只是我們的這個新的函數可能會更快一些,因為只有2次數據庫查詢。 要獲知一個節點的路徑就更簡單了,如果我們想知道Cherry 的路徑就利用它的左右值4和5來做一個查詢。 SELECT name FROM tree WHERE lft < 4 AND rgt > 5 ORDER BY lft ASC; 這樣就會得到以下的結果: 以下是代碼: [code:1:86c003e7bb] +------------+ | name | +------------+ | Food | | Fruit | | Red | +------------+ [/code:1:86c003e7bb] 那么某個節點到底有多少子孫節點呢?很簡單,子孫總數=(右值-左值-1)/2 descendants = (right – left - 1) / 2 不相信?自己算一算啦。 用這個簡單的公式,我們可以很快的算出"Fruit 2-11"節點有4個子孫節點,而"Banana 8-9"節點沒有子孫節點,也就是說它不是一個父節點了。 很神奇吧?雖然我已經多次用過這個方法,但是每次這樣做的時候還是感到很神奇。 這的確是個很好的辦法,但是有什么辦法能夠幫我們建立這樣有左右值的數據表呢?這里再介紹一個函數給大家,這個函數可以將name和parent結構的表自動轉換成帶有左右值的數據表。 以下是代碼: [code:1:86c003e7bb] <?php function 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; } ?> [/code:1:86c003e7bb] 當然這個函數是一個遞歸函數,我們需要從根節點開始運行這個函數來重建一個帶有左右值的樹 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語句會變得更加簡單。 |
shukebeita 回復于:2004-01-08 22:30:18 |
能不能把那些圖表對對齊,尤其是那個代標號的樹狀圖,否則大家看不懂了。 |
longnetpro 回復于:2004-01-09 03:38:53 |
好!shukebeita這個寫得好!代碼都不用看,就看你那個表和查詢就知道思路了,可能比原來那個樹型解決方案還要好!不過看起來暫時只是二叉樹,還不是真正的任意形狀的樹,還得研究一陣子。
另外:shukebeita的兩個翻譯不準,現在我來翻譯一下(主要對英文欠缺或是無專業背景人士) 1、adjacency list model - 鄰接表模式 注:adjacent list 術語為鄰接表,好象是(好多年了,忘了很多)指主干節點拖的一個尾巴,與鄰接矩陣有關??傊孟笫敲總€節點都橫豎兩條雙向鏈表中間,總體上被鏈成一個矩陣,每個節點是矩陣中的一個元素。 2、modified preorder tree traversal algorithm - 修改過的先序遍歷樹算法(亦即先根遍歷樹算法) 注:modified 意為“修改過的” preorder tree traversal 專業術語“先序遍歷樹”,preorder是“先于...的順序”的意思,pre- 是詞前綴,意為“先”,order 意為“順序”;traversal是“遍歷的”的意思。 整個算法的原則是: 從樹根開始處理該樹的所有節點,處理方法為—— 先處理當前的節點,然后遞歸處理當前節點的左子節點,然后再遞歸處理當前節點的右子節點,如果子節點為NULL(沒有節點)的話即返回。寫成代碼的話,一般形式為: [code:1:202257c133] function preorder(&$node, $level = 0){ // 注意是傳遞引用,C中也就是傳遞指針 process_root($node); // 處理本節點 $n =& $node->left; // 引用賦值,以便作為參數傳遞 if ($n != null){ // 如果左子節點不為空 preorder($n, level + 1); // 遞歸處理左子節點,同時層數加一 } $n =& $node->right; // 引用賦值,以便作為參數傳遞 if ($n != null){ // 如果右子節點不為空 preorder($n, level + 1); // 遞歸處理右子節點,同時層數加一 } } [/code:1:202257c133] 以上之所以加了幾個判斷是為了防止多一次無意義的遞歸,引用賦值主要是減少PHP中的復制開銷(PHP5是會好一些)。 而外部調用一般要先定義一個入口函數,再調用遞歸函數,形式如下: [code:1:202257c133] function call_preorder(){ $root =& $tree->root; // 引用賦值,將樹的根作為遍歷的入口點 $level = 0; // 樹根是0級 preorder($root, $level); // 然后從樹根開始遍歷 } [/code:1:202257c133] 遞歸函數中有一個函數是process(&$node),用于處理該節點$node,可以實現任何你想實現的功能,如打印此節點的關鍵字值等。 |
longnetpro 回復于:2004-01-09 03:59:47 |
剛才突然發現一個問題,就是這個水果順列是已插入好的順列,所以查找起來用這個方法可以,但是如果是隨機插入節點的話,雖然在查詢的時候不用遞歸了,但建樹的時候還要用到遞歸,而且每次插入預留兩個空不太靈活,有時也會影響到計算所有子節點的數目,因為數據庫操作寫入可能會寫錯。
最主要的是目前暫只適用于二叉樹,不適合于多叉樹。 不過這個問題值得研究一番。 |
shukebeita 回復于:2004-01-09 09:33:28 |
[quote:65de5c20c5="longnetpro"]2、modified preorder tree traversal algorithm - 修改過的先序遍歷樹算法(亦即先根遍歷樹算法)
注:modified 意為“修改過的” preorder tree traversal 專業術語“先序遍歷樹”,preorder是“先于...的順序”的意思,pre- 是詞前綴,意為“先”,order 意為“順序”;[/quote:65de5c20c5] 多謝支持,這些方法也是我從外國的書里學到的,我的確不太懂這些術語,不過我對 modified preorder tree traversal algorithm 這一句還是有我的想法。 1、我不是計算級或者數學專業的,單是從字面上理解我想大概有一種算法叫做preorder tree traversal algorithm ,而這里用到的是它的一個修改版本,所以稱其為modified。 2、pre是先的意思沒錯,但是order在這里更準確地說應該是排序,排定順序的意思。所以就是說這個樹是[b:65de5c20c5]“預先排好序號“[/b:65de5c20c5]的樹。 [quote:65de5c20c5] 最主要的是目前暫只適用于二叉樹,不適合于多叉樹。 [/quote:65de5c20c5] 不知道你說的多叉樹是不是說像下面的樣子 [code:1:65de5c20c5] 1 Food 20 | +-------------------------------------------+ | | | 2 Fruit 11 12 Vegetable 13 14 Meat 19 | | +------------------------+ +-----------------------+ | | | | 3 Red 6 7 Yellow 10 15 Beef 16 17 Pork 18 | | 4 Cherry 5 8 Banana 9 [/code:1:65de5c20c5] 如果是這樣的話,你完全可以放心,這種辦法沒有問題,一樣適用。我已經試過多次。 [quote:65de5c20c5]但建樹的時候還要用到遞歸[/quote:65de5c20c5] 所以這種方法對于經常需要查詢,但是很少增加和更新的數據(比如產品,書籍的多級分類)更為合適,一旦建立很少變動。 [quote:65de5c20c5]有時也會影響到計算所有子節點的數目,因為數據庫操作寫入可能會寫錯。 [/quote:65de5c20c5] 這就是需要transaction(事務處理)的地方了。 |
longnetpro 回復于:2004-01-09 11:56:36 |
別的你說的都能理解,也很容易理解。
多叉樹從排序情況看是一樣的,但是的確插入和更新就比較麻煩了,雖說只用到少數幾個UPDATE,但是改動卻是大范圍的??磥硎裁捶椒ǘ疾荒苁赖?。 有一點必須指出,preorder肯定是你理解錯誤,“預先排好序號”這個意思完全與遍歷無關,與它的算法原理也一點都不相合。 首先要搞清楚遍歷的意思。遍歷是將一個拓樸結構上所有的節點根據一種算法按順序全都處理到——注意是所有的節點。遍歷是一個過程,在遍歷的過程中,按一定的順序處理各個節點。 preorder 遍歷就是“先序遍歷”,也叫“先根遍歷”,之所以這樣說是因為先序遍歷在對子節點進行遞歸遍歷之前先處理根節點。inorder遍歷是“中序遍歷”,也叫“中根遍歷”,因為中序遍歷是先對左子節點遞歸遍歷,然后處理根節點,然后再對右子節點進行遞歸遍歷。postorder遍歷是“后序遍歷”,也叫“后根遍歷”,是左右子節點都遍歷排完了最后再處理根節點。 其實我對這個英文也理解有點誤,但是術語肯定是“先序”這個意思,現在想起來可能中文意思與英文原意有差別。當時學的時候就是“先序”“前序”這種說法,但到底表示什么也不是很清楚?,F在想來應該是這樣的:preorder應該是“在排序之前”,inorder是“在排序之中”,postorder是“在排序之后”,就和“戰前”,“戰后”的拼詞方法類似??赡芸傮w上理解就是“當前節點的處理在排序(即遞歸遍歷)之前的遍歷算法”,“當前節點的處理在排序(即遞歸遍歷)之中的遍歷算法”,“當前節點的處理在排序(即遞歸遍歷)之后的遍歷算法”。 |
tonera 回復于:2004-04-25 15:35:37 |
今天終于遇到關于多級分類查詢和數據設計的問題了。再次認真看了一遍樓主的文章,真是恍然大悟、茅塞頓開啊。我沒讀多少書,沒學過離散數學,呵呵。我覺得這個問題實在討論得太有意義了。
這個問題其實并不只是PHP的問題,更多的是數據設計的問題,精巧的數據結構實在太重要了。 我開始也只想到用遞歸來處理,但效率實在太差。層次越多,效率越低。如果用先序遍歷樹算法(modified preorder tree traversal algorithm) 。則相對來說,層次越多,表現越好。呵呵呵呵。 :D 唯一不爽的就是在編輯節點時比較麻煩。 shuke,能不能抽空寫個樹構造類,以后大家都用這個算啦。呵呵。 :D 我試試看能不能完成,等我搞好了再貼。 |
raffles 回復于:2005-01-14 13:50:03 |
http://php.linuxpack.net/show.php?type=studyphp&id=1073542478
看不了!????????????????????? |
夜貓子 回復于:2005-01-14 14:26:56 |
我已經轉貼過來了,上邊我發的那個帖子就是 |
aspbiz 回復于:2005-01-14 15:15:00 |
樹形結構不難吧。
一些基本的東東。 |
夜貓子 回復于:2005-01-14 15:33:53 |
那是因為你知道得太少 |
jhsea3do 回復于:2005-01-14 22:16:11 |
實用性文章,頂! |
SATAND 回復于:2005-01-15 01:33:48 |
關于標記任意的級別數據,我還是贊同采取編碼標記法
比如,每級數據為四位數,飽合值9999,在編號存取方面,采用VChar值 “Num.”四位作為前綴,之后是一級編碼,二級編碼,三級編碼。。。比如一條信息在第四級13號,那其編號就是“Num.0001000100010013” 遞歸列樹則基礎于檢索時的條件限制,例如用“Num.xxxx%”作為關鍵字檢索第一級的所有數據量,如果要把某一類置于另一個父類,只需要檢索“Num.xxxxxxxx...”將前面的“Num.xxxx...”替換為父類的ID就OK了 另,有兩點需要注意: 第一,這張數據表要有一個Max值表輔助,也就是要通過一個Max值,來記錄每一個類已經存儲的最大值; 第二,刪除數據時不能真的刪除,而是打上刪除標記,在插入新紀錄時update,有效的節約資源 本標記看似采用大數標記,其實屬字串操作,不會耗太多的CPU資源,而設定索引后,配合左端定制右邊通配符的條件檢索,可以獲得很高的效率 以上是偶在做某集團財務應用時采用的分公司標記法,這樣,當某家分公司被調整時,可以很方便的實現大挪移。 聲明,上面的討論沒有細讀,是否文不對題的嫌疑...請指正 http://newsy.org/news/news/SATAND/tech/2005-01-15/1105724453.html |
lichen 回復于:2005-01-17 15:15:19 |
老大是不是連接更改了,怎么打不開吶???? |
原文轉自:http://www.anti-gravitydesign.com