無限極分類(adjacency list)的三種方式(迭代、遞歸、引用)


一般的分類樹狀結構有兩種方式:

  • 一種是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 = '&nbsp;&nbsp;&nbsp;';
 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 }
View Code

以上記錄下,如轉載請標明來源地址

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM