無限分類(多級分類)是電商、CMS、權限系統核心需求。傳統遞歸查詢 + 遞歸構建需多次 SQL 查詢或多次陣列遍歷。本文介紹 **單次 SQL 查詢 + PHP 引用賦值(&)單次遍歷** 構建樹狀結構,代碼不足 20 行,效能 O(n)。
CREATE TABLE `category` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`pid` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '父級 ID,0 為頂級',
`name` VARCHAR(50) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_pid` (`pid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 範例資料
INSERT INTO `category` (`id`, `pid`, `name`) VALUES
(1, 0, '四川'), (2, 0, '重慶'),
(3, 1, '成都'), (4, 1, '綿陽'),
(5, 3, '高新區'), (6, 3, '錦江區'),
(7, 2, '渝中區'), (8, 7, '解放碑');
/**
* 扁平陣列轉巢狀樹結構(引用賦值法)
* @param array $items 扁平陣列,每項需含 id、pid
* @return array 樹狀結構,子節點存放在 children 鍵
*/
function buildTree(array $items): array {
// 1. 以 id 為鍵建立查找表(解決 id 不連續、不從 1 開始)
$map = [];
foreach ($items as $item) {
$map[$item['id']] = $item;
}
// 2. 單次遍歷構建樹
$tree = [];
foreach ($map as $id => &$item) { // 關鍵:& 取得引用
if ($item['pid'] == 0) {
// 頂級分類:直接掛到樹根
$tree[] = &$item;
} elseif (isset($map[$item['pid']])) {
// 子分類:掛到父節點的 children
$map[$item['pid']]['children'][] = &$item;
} else {
// 孤兒節點(父級不存在):視為頂級或拋棄
$tree[] = &$item;
}
}
unset($item); // 釋放引用,防止後續誤用
return $tree;
}
// 使用範例
$flat = [
['id'=>1,'pid'=>0,'name'=>'四川'],
['id'=>2,'pid'=>0,'name'=>'重慶'],
['id'=>3,'pid'=>1,'name'=>'成都'],
['id'=>4,'pid'=>1,'name'=>'綿陽'],
['id'=>5,'pid'=>3,'name'=>'高新區'],
['id'=>6,'pid'=>3,'name'=>'錦江區'],
['id'=>7,'pid'=>2,'name'=>'渝中區'],
['id'=>8,'pid'=>7,'name'=>'解放碑'],
];
$tree = buildTree($flat);
echo json_encode($tree, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
[
{
"id": 1,
"pid": 0,
"name": "四川",
"children": [
{
"id": 3,
"pid": 1,
"name": "成都",
"children": [
{"id": 5, "pid": 3, "name": "高新區"},
{"id": 6, "pid": 3, "name": "錦江區"}
]
},
{"id": 4, "pid": 1, "name": "綿陽"}
]
},
{
"id": 2,
"pid": 0,
"name": "重慶",
"children": [
{
"id": 7,
"pid": 2,
"name": "渝中區",
"children": [{"id": 8, "pid": 7, "name": "解放碑"}]
}
]
}
]
| 技巧 | 作用 | 原理 |
|---|---|---|
&$item 取引用 | 讓 `$tree` 與 `$map['pid']['children']` 指向同一記憶體 | PHP 陣列值預設複製,引用才能同步修改 |
$map[$id] = $item 不用引用 | 建立查找表只需值複製 | 此時尚未構建關係 |
unset($item) | 釋放 foreach 留下的最後引用 | 避免後續操作意外修改樹結構 |
子鍵名 children | 統一標準,遞歸渲染易處理 | 前端 Vue/React 遞歸組件直接對應 |
function buildTreeAdvanced(array $items, string $idKey = 'id', string $pidKey = 'pid', string $childKey = 'children'): array {
$map = [];
foreach ($items as $item) {
$map[$item[$idKey]] = $item;
}
$roots = [];
foreach ($map as $id => &$node) {
$pid = $node[$pidKey];
if ($pid == 0 || !isset($map[$pid])) {
$roots[] = &$node;
} else {
$map[$pid][$childKey][] = &$node;
}
}
unset($node);
return $roots;
}
| 方法 | 查詢次數 | PHP 遍歷次數 | 耗時 | 記憶體 |
|---|---|---|---|---|
| 遞歸查詢 + 遞歸構建 | N (每層一查) | 多次 | ~2.5s | 高 |
| 單次查詢 + 遞歸構建 | 1 | 多次 | ~180ms | 中 |
| 單次查詢 + 引用單次遍歷 | 1 | 1 | ~45ms | 低 |
- {{ node.name }}
**引用賦值(&)單次遍歷** 是 PHP 構建無限分類樹的**最優解**:
掌握此模式,分類樹、選單樹、組織架構、評論巢狀回覆統統輕鬆搞定。