無限級分類是電商、CMS、OA 等系統的核心模型。傳統做法要么遞歸查詢數據庫(N+1 問題),要么在 PHP 中遞歸構建樹(代碼冗長)。本文介紹一種基於 引用賦值(&) 的單次遍歷算法:將扁平數組轉為巢狀樹結構,代碼不足 30 行,性能極高,邏輯清晰。
標準 id / pid / name 結構:
| id | pid | name |
|---|---|---|
| 1 | 0 | 四川 |
| 2 | 0 | 重慶 |
| 3 | 1 | 成都 |
| 4 | 1 | 綿陽 |
| 5 | 3 | 高新區 |
<?php
/**
* 扁平數組轉無限級分類樹(引用賦值版)
* @param array $orig 原始數組,每項需含 id、pid 字段
* @return array 樹形結構數組
*/
public static function get_tree($orig) {
// 1. 重建索引:以 id 為鍵,便於 O(1) 查找父節點
$items = array();
foreach ($orig as $key => $value) {
$items[$value['id']] = $value;
}
// 2. 單次遍歷組裝樹
$tree = array();
foreach ($items as $key => $item) {
if ($item['pid'] == 0) {
// 頂級分類:直接掛入樹根,使用引用賦值
$tree[] = &$items[$key];
} else {
// 非頂級分類
if (isset($items[$item['pid']])) {
// 父節點存在:將當前節點以引用方式加入父節點的 child 數組
$items[$item['pid']]['child'][] = &$items[$key];
} else {
// 父節點不存在(數據異常或非第一層):作為孤兒節點掛入樹根
$tree[] = &$items[$key];
}
}
}
return $tree;
}
id,使 $items[$pid] 能直接定位父節點,無需遞歸查找。$items[$pid]['child'][] = &$items[$key] 不是複製數據,而是將子節點的 內存地址 存入父節點。因此,當後續處理到該子節點時,它已經掛在父節點下;同理,子節點的子節點也會自動掛在正確位置。Array
(
[0] => Array
(
[id] => 1
[pid] => 0
[name] => 四川
[child] => Array
(
[0] => Array
(
[id] => 3
[pid] => 1
[name] => 成都
[child] => Array
(
[0] => Array
(
[id] => 5
[pid] => 3
[name] => 高新區
)
)
)
[1] => Array
(
[id] => 4
[pid] => 1
[name] => 綿陽
)
)
)
[1] => Array
(
[id] => 2
[pid] => 0
[name] => 重慶
)
)
category_id/parent_id,建議增加參數映射或預處理統一鍵名。$visited 集合檢測並拋出異常。path 欄位(如 0/1/3/5)存儲路徑,查詢時 LIKE '0/1/%' 獲取子樹,再由 PHP 組裝。json_encode 輸出,PHP 5.4+ 自動處理引用;舊版本可用 json_encode($tree, JSON_UNESCAPED_UNICODE) 或先 unset($tree) 斷開引用。TreeUtil::build($list, 'id', 'pid', 'child') 通用工具類,提高復用性。| 方案 | 時間複雜度 | 空間複雜度 | 代碼行數 | 適用場景 |
|---|---|---|---|---|
| 遞歸查詢 DB | O(N) 查詢次數 | O(1) | ~10 | 極小數據、實時性要求極高 |
| PHP 遞歸構建 | O(N²) 最壞 | O(N) | ~20 | 中小數據、邏輯簡單 |
| 引用賦值單遍歷 | O(N) | O(N) | ~25 | 大數據、高性能、通用推薦 |
| Closure Table / Path | O(log N) 查詢 | O(N log N) | DB 結構變更 | 超大規模、頻繁移動節點 |
掌握「引用賦值 + 索引映射」這一模式,不僅能優雅解決無限級分類,同樣適用於部門組織架構、評論回復巢狀、菜單權限樹等所有「扁平轉樹形」場景。建議收藏入庫,下次遇到樹形結構直接套用。