將扁平的分類陣列 (含 id, pid) 按層級順序排序輸出 (父節點在前、子節點緊跟),是選單渲染、麵包屑、分類管理常見需求。本文提供標準遞歸函數 order(),分析其呼叫堆疊深度風險,並介紹非遞歸迭代法、資料庫層面解決方案 (遞歸 CTE、巢狀集合、閉包表) 及效能對比。
1, 'pid'=>0, 'name'=>'電子產品'],
['id'=>2, 'pid'=>1, 'name'=>'手機'],
['id'=>3, 'pid'=>2, 'name'=>'蘋果'],
['id'=>4, 'pid'=>2, 'name'=>'安卓'],
['id'=>5, 'pid'=>1, 'name'=>'筆電'],
['id'=>6, 'pid'=>0, 'name'=>'家電'],
];
$sorted = order($cats);
/*
輸出順序:
1 電子產品 (pid=0)
2 手機 (pid=1)
3 蘋果 (pid=2)
4 安卓 (pid=2)
5 筆電 (pid=1)
6 家電 (pid=0)
*/
xdebug.max_nesting_level、記憶體限制)。分類超過 100 層 (罕見) 會導致 Maximum function nesting level exceeded。
function orderOptimized(array $data, int $pid = 0): array {
// 建立 pid => 子節點列表 索引 O(N)
$children = [];
foreach ($data as $item) {
$children[$item['pid']][] = $item;
}
$result = [];
// 使用棧模擬遞歸 (深度優先)
$stack = [[$pid, 0]]; // [當前pid, 子節點索引]
$iterators = []; // 每層的迭代器狀態
while ($stack) {
[$currentPid, $index] = array_pop($stack);
$kids = $children[$currentPid] ?? [];
if ($index < count($kids)) {
$item = $kids[$index];
$result[] = $item;
// 將下一個同層節點壓回棧
$stack[] = [$currentPid, $index + 1];
// 將第一個子節點壓入棧 (深度優先)
$stack[] = [$item['id'], 0];
}
}
return $result;
}
function orderGenerator(array $children, int $pid = 0): Generator {
foreach ($children[$pid] ?? [] as $item) {
yield $item;
yield from orderGenerator($children, $item['id']);
}
}
// 使用
$children = []; foreach ($data as $d) $children[$d['pid']][] = $d;
foreach (orderGenerator($children) as $item) {
echo $item['name'] . "\n"; // 逐行處理,不佔用結果陣列記憶體
}
| 模型 | 原理 | 查詢子樹 | 移動節點 | 適用場景 |
|---|---|---|---|---|
| 鄰接表 (Adjacency List) | pid 欄位 | 遞歸 CTE / PHP 遞歸 | 簡單 | 讀多寫少、層級淺 |
| 路徑枚舉 (Path Enumeration) | path: '1/2/3/' | LIKE '1/2/%' | 更新路徑 | 讀多、需快速祖先/後代 |
| 巢狀集合 (Nested Set) | lft, rgt 編號 | lft BETWEEN ... | 複雜 (重新編號) | 極少變動、大量讀取 |
| 閉包表 (Closure Table) | 單獨表存所有祖先後代關係 | JOIN closure | 插入/刪除關係行 | 讀寫平衡、需遞歸聚合 |
WITH RECURSIVE category_tree AS (
-- 錨點:頂層分類
SELECT id, pid, name, 0 AS depth, CAST(name AS CHAR(1000)) AS path
FROM categories WHERE pid = 0
UNION ALL
-- 遞歸:子分類
SELECT c.id, c.pid, c.name, ct.depth + 1, CONCAT(ct.path, ' > ', c.name)
FROM categories c
INNER JOIN category_tree ct ON c.pid = ct.id
)
SELECT * FROM category_tree ORDER BY path;
<ul><li>。$tree = []; foreach($data as $d) $tree[$d['pid']][] = $d; function build($pid){ foreach($tree[$pid]??[] as $c){ $c['children']=build($c['id']); $res[]=$c; } return $res??[]; }PHP 端遞歸排序適合中小型分類 (千筆內、層級 < 10)。大型系統請下推至資料庫 (遞歸 CTE) 或採用閉包表/巢狀集合,配合前端樹狀組件實現高效能分類管理。