一般的分類樹狀結構有兩種方式:
- 一種是adjacency list,也就是是id,parent id這中形式。
- 另一種是nested set,即左右值的形式。
左右值形式查詢起來比較高效,無需遞歸等,推薦使用,但是沒有pid形式簡單直觀,而且有些舊的數據庫類似地區等結構設計一直是pid這種形式(貌似也有算法可以將兩者轉換,不做深入了解),所以。。。
下面所說的都為adjacency list的形式,數據表格式類似id,pid,name這種格式。
通常來說是將數據全部從數據庫讀取后,然后再組裝數組來實現,當然也可以每次遞歸等都查詢數據庫,但是會造成數據庫壓力,且不容易封裝成方法,不建議這樣做。
目前來說常用的有三種方式,我們來實現select下拉菜單展示的樣式:
1、首先是最常用最普通,同樣也是效率最低的遞歸方法:就是不停的foreach循環遞歸。
function getTreeOptions3($list, $pid = 0) { $options = []; foreach ($list as $key => $value) { if ($value['id'] == $pid) {//查看是否為子元素,如果是則遞歸繼續查詢 $options[$value['id']] = $value['name']; unset($list[$key]);//銷毀已查詢的,減輕下次遞歸時查詢數量 $optionsTmp = $this->getTreeOptions3($list, $value['id']);//遞歸 if (!empty($optionsTmp)) { //$options = array_merge($options, $optionsTmp);//銷毀已查詢的,減輕下次遞歸時查詢數量 $options =$options+ $optionsTmp;//用array_merge會導致索引重排 } } } return $options; }
2、第二種是利用入棧、出棧的遞歸來計算,效率比上個好點,但是也挺慢的。流程是先反轉數組,然后取出頂級數組入棧,開始while循環,先出棧一個查找其下有沒有子節點,如果有子節點,則將此子節點也入棧,下次while循環就會查子節點的,依次類推:
function getTreeOptions2($list, $pid = 0) { $tree = []; if (!empty($list)) { //先將數組反轉,因為后期出棧時會優先出最上面的 $list = array_reverse($list); //先取出頂級的來壓入數組$stack中,並將在$list中的刪除掉 $stack = []; foreach ($list as $key => $value) { if ($value['pid'] == $pid) { array_push($stack,$value); unset($list[$key]); } } while (count($stack)) { //先從棧中取出第一項 $info = array_pop($stack); //查詢剩余的$list中pid與其id相等的,也就是查找其子節點 foreach ($list as $key => $child) { if ($child[pid] == $info['id']) { //如果有子節點則入棧,while循環中會繼續查找子節點的下級 array_push($stack, $child); unset($list[$key]); } } //組裝成下拉菜單格式 $tree[$info['id']] = $info['name']; } } return $tree; }
3、利用引用來處理,這個真的很巧妙,而且效率最高,可參照這里,如果想看再詳細的解釋,可以查看這個問答。
/** * 先生成類似下面的形式的數據 * [ * 'id'=>1, * 'pid'=>0, * 'items'=>[ * 'id'=>2, * 'pid'=>'1' * 。。。 * ] * ] */ function getTree($list, $pid = 0) { $tree = []; if (!empty($list)) { //先修改為以id為下標的列表 $newList = []; foreach ($list as $k => $v) { $newList[$v['id']] = $v; } //然后開始組裝成特殊格式 foreach ($newList as $value) { if ($pid == $value['pid']) {//先取出頂級 $tree[] = &$newList[$value['id']]; } elseif (isset($newList[$value['pid']])) {//再判定非頂級的pid是否存在,如果存在,則再pid所在的數組下面加入一個字段items,來將本身存進去 $newList[$value['pid']]['items'][] = &$newList[$value['id']]; } } } return $tree; }
然后再遞歸生成select下拉菜單所需要的,由於上方的特殊格式,導致遞歸起來非常快:
function formatTree($tree) { $options = []; if (!empty($tree)) { foreach ($tree as $key => $value) { $options[$value['id']] = $value['name']; if (isset($value['items'])) {//查詢是否有子節點 $optionsTmp = $this->formatTree($value['items']); if (!empty($optionsTmp)) { //$options = array_merge($options, $optionsTmp); $options =$options+ $optionsTmp;//用array_merge會導致索引重排 } } } } return $options; }
以上三種,對於數據量小的來說,無所謂用哪種,但是對於數據量大的來說就非常明顯了,用4000條地區數據測試結果效率對比:
- 第一種方法(遞歸)耗時:8.9441471099854左右
- 第二種方法(迭代)耗時:6.7250330448151左右
- 第三種方法(引用)耗時:0.028863906860352左右
我去,這差距,第三種方法真是逆天了。但是再次提醒,這只是一次性讀取多的數據的時候,當數據量很小的時候,相差無幾,不一定非要用最高效率的,還可以通過懶加載等其他方式來實現。
順便封裝個類,可以增加一些填充什么的。更多的細節可以查看下面的類:

1 /** 2 * parent_id類型樹結構相關 3 * 沒必要非要寫成靜態的方法,靜態方法參數太多,所以用實例在構造函數中修改參數更合適 4 * 需要首先將所有數據取出,然后再用此方法重新規划數組,其它的邊取邊查詢數據庫的方法不推薦 5 * 經測試第一種方法要快很多,建議使用 6 * @author vishun <nadirvishun@gmail.com> 7 */ 8 9 class Tree 10 { 11 /** 12 * 圖標 13 */ 14 public $icon = '└'; 15 /** 16 * 填充 17 */ 18 public $blank = ' '; 19 /** 20 * 默認ID字段名稱 21 */ 22 public $idName = 'id'; 23 /** 24 * 默認PID字段名稱 25 */ 26 public $pidName = 'pid'; 27 /** 28 * 默認名稱字段名稱 29 */ 30 public $titleName = 'name'; 31 /** 32 * 默認子元素字段名稱 33 */ 34 public $childrenName = 'items'; 35 36 /** 37 * 構造函數,可覆蓋默認字段值 38 * @param array $config 39 */ 40 function __construct($config = []) 41 { 42 if (!empty($config)) { 43 foreach ($config as $name => $value) { 44 $this->$name = $value; 45 } 46 } 47 } 48 49 /** 50 * 生成下拉菜單可用樹列表的方法 51 * 經測試4000條地區數據耗時0.02左右,比另外兩種方法快超級多 52 * 流程是先通過引用方法來生成一種特殊樹結構,再通過遞歸來解析這種特殊的結構 53 * @param array $list 54 * @param int $pid 55 * @param int $level 56 * @return array 57 */ 58 public function getTreeOptions($list, $pid = 0, $level = 0) 59 { 60 //先生成特殊規格的樹 61 $tree = $this->getTree($list, $pid); 62 //再組裝成select需要的形式 63 return $this->formatTree($tree, $level); 64 } 65 66 /** 67 * 通過遞歸來解析特殊的樹結構來組裝成下拉菜單所需要的樣式 68 * @param array $tree 特殊規格的數組 69 * @param int $level 70 * @return array 71 */ 72 protected function formatTree($tree, $level = 0) 73 { 74 $options = []; 75 if (!empty($tree)) { 76 $blankStr = str_repeat($this->blank, $level) . $this->icon; 77 if ($level == 0) {//第一次無需有圖標及空格 78 $blankStr = ''; 79 } 80 foreach ($tree as $key => $value) { 81 $options[$value[$this->idName]] = $blankStr . $value[$this->titleName]; 82 if (isset($value[$this->childrenName])) {//查詢是否有子節點 83 $optionsTmp = $this->formatTree($value[$this->childrenName], $level + 1); 84 if (!empty($optionsTmp)) { 85 //$options = array_merge($options, $optionsTmp);//發現一個問題,這里直接用array_merge會導致key重排 86 $options = $options+$optionsTmp; 87 //$options = ArrayHelper::merge($options, $optionsTmp);//如果是用yii2帶話可以用助手類,需要use其命名空間 88 } 89 } 90 } 91 } 92 return $options; 93 } 94 95 /** 96 * 生成類似下種格式的樹結構 97 * 利用了引用&來實現,參照:http://blog.csdn.net/gxdvip/article/details/24434801 98 * [ 99 * 'id'=>1, 100 * 'pid'=>0, 101 * 'items'=>[ 102 * 'id'=>2, 103 * 'pid'=>'1' 104 * 。。。 105 * ] 106 * ] 107 * @param array $list 108 * @param int $pid 109 * @return array 110 */ 111 protected function getTree($list, $pid = 0) 112 { 113 $tree = []; 114 if (!empty($list)) { 115 //先修改為以id為下標的列表 116 $newList = []; 117 foreach ($list as $k => $v) { 118 $newList[$v[$this->idName]] = $v; 119 } 120 //然后開始組裝成特殊格式 121 foreach ($newList as $value) { 122 if ($pid == $value[$this->pidName]) { 123 $tree[] = &$newList[$value[$this->idName]]; 124 } elseif (isset($newList[$value[$this->pidName]])) { 125 $newList[$value[$this->pidName]][$this->childrenName][] = &$newList[$value[$this->idName]]; 126 } 127 } 128 } 129 return $tree; 130 } 131 132 /** 133 * 第二種方法,利用出入棧迭代來實現 134 * 經測試4000條地區數據耗時6.5s左右,比較慢 135 * @param $list 136 * @param int $pid 137 * @param int $level 138 * @return array 139 */ 140 public function getTreeOptions2($list, $pid = 0, $level = 0) 141 { 142 $tree = []; 143 if (!empty($list)) { 144 145 //先將數組反轉,因為后期出棧時會有限出最上面的 146 $list = array_reverse($list); 147 //先取出頂級的來壓入數組$stack中,並將在$list中的刪除掉 148 $stack = []; 149 foreach ($list as $key => $value) { 150 if ($value[$this->pidName] == $pid) { 151 array_push($stack, ['data' => $value, 'level' => $level]);//將層級記錄下來,方便填充空格 152 unset($list[$key]); 153 } 154 } 155 while (count($stack)) { 156 //先從棧中取出第一項 157 $info = array_pop($stack); 158 //查詢剩余的$list中pid與其id相等的,也就是查找其子節點 159 foreach ($list as $key => $child) { 160 if ($child[$this->pidName] == $info['data'][$this->idName]) { 161 //如果有子節點則入棧,while循環中會繼續查找子節點的下級 162 array_push($stack, ['data' => $child, 'level' => $info['level'] + 1]); 163 unset($list[$key]); 164 } 165 } 166 //組裝成下拉菜單格式 167 $blankStr = str_repeat($this->blank, $info['level']) . $this->icon; 168 if ($info['level'] == 0) {//第一次無需有圖標及空格 169 $blankStr = ''; 170 } 171 $tree[$info['data'][$this->idName]] = $blankStr . $info['data'][$this->titleName]; 172 } 173 } 174 return $tree; 175 } 176 177 /** 178 * 第三種普通列表轉為下拉菜單可用的樹列表 179 * 經測試4000條地區數據耗時8.7s左右,最慢 180 * @param array $list 原數組 181 * @param int $pid 起始pid 182 * @param int $level 起始層級 183 * @return array 184 */ 185 public function getTreeOptions3($list, $pid = 0, $level = 0) 186 { 187 $options = []; 188 if (!empty($list)) { 189 $blankStr = str_repeat($this->blank, $level) . $this->icon; 190 if ($level == 0) {//第一次無需有圖標及空格 191 $blankStr = ''; 192 } 193 foreach ($list as $key => $value) { 194 if ($value[$this->pidName] == $pid) { 195 $options[$value[$this->idName]] = $blankStr . $value[$this->titleName]; 196 unset($list[$key]);//銷毀已查詢的,減輕下次遞歸時查詢數量 197 $optionsTmp = $this->getTreeOptions3($list, $value[$this->idName], $level + 1);//遞歸 198 if (!empty($optionsTmp)) { 199 //$options = array_merge($options, $optionsTmp);//發現一個問題,這里直接用array_merge會導致key重排 200 $options = $options+$optionsTmp; 201 //$options = ArrayHelper::merge($options, $optionsTmp);//如果是用yii2帶話可以用助手類,需要use其命名空間 202 } 203 } 204 } 205 } 206 return $options; 207 } 208 }
以上記錄下,如轉載請標明來源地址