在 Web 開發中,無限級分類(如商品分類、評論回覆、組織架構)是常見需求。本文將介紹兩種主流實現方式:遞歸法與非遞歸(迭代)法,並附上完整代碼示例與性能對比。
核心欄位包含:id(主鍵)、pid(父級 ID)、name(分類名稱)、deep(層級深度,冗餘字段用於優化查詢)。
<?php // 建表示例 SQL CREATE TABLE `category` ( `id` int(11) NOT NULL AUTO_INCREMENT, `pid` int(11) DEFAULT '0' COMMENT '父級ID', `deep` tinyint(4) DEFAULT '0' COMMENT '層級深度', `name` varchar(50) NOT NULL COMMENT '分類名稱', PRIMARY KEY (`id`), KEY `pid` (`pid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ?>
遞歸代碼簡潔,邏輯清晰,適合數據量較小(< 1000 條)的場景。
<?php
/**
* 遞歸獲取樹狀結構
* @param array $list 扁平化數據
* @param int $pid 父級ID
* @return array
*/
function getTreeRecursive(array $list, int $pid = 0): array
{
$tree = [];
foreach ($list as $row) {
if ($row['pid'] == $pid) {
$children = getTreeRecursive($list, $row['id']);
if ($children) {
$row['children'] = $children;
}
$tree[$row['id']] = $row;
}
}
return $tree;
}
// 使用示例
$list = [
['id'=>1, 'pid'=>0, 'name'=>'電子產品'],
['id'=>2, 'pid'=>1, 'name'=>'手機'],
['id'=>3, 'pid'=>2, 'name'=>'蘋果'],
['id'=>4, 'pid'=>0, 'name'=>'圖書'],
];
$tree = getTreeRecursive($list);
print_r($tree);
?>
利用 PHP 變量引用特性,一次遍歷即可構建樹形結構,性能顯著優於遞歸,適合大數據量場景。
<?php
/**
* 非遞歸構建無限級分類
* @param array $list 扁平化數據(需包含 id、pid)
* @return array 樹形結構數組
*/
function buildTreeIterative(array $list): array
{
// 1. 建立 ID 索引映射
$map = [];
foreach ($list as $item) {
$map[$item['id']] = $item;
}
// 2. 利用引用構建父子關係
$tree = [];
foreach ($map as $id => &$node) {
$pid = $node['pid'];
if ($pid == 0 || !isset($map[$pid])) {
// 頂級分類
$tree[$id] = &$node;
} else {
// 子分類,掛載到父節點 children
if (!isset($map[$pid]['children'])) {
$map[$pid]['children'] = [];
}
$map[$pid]['children'][$id] = &$node;
}
}
unset($node); // 釋放引用
return $tree;
}
// 測試數據
$list = [
['id'=>1, 'pid'=>0, 'name'=>'電子產品'],
['id'=>2, 'pid'=>1, 'name'=>'手機'],
['id'=>3, 'pid'=>2, 'name'=>'蘋果'],
['id'=>4, 'pid'=>1, 'name'=>'電腦'],
['id'=>5, 'pid'=>0, 'name'=>'圖書'],
];
$tree = buildTreeIterative($list);
echo json_encode($tree, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
?>
結合遞歸生成 <select> 選項,使用 實現視覺縮進。
<?php
function renderSelectOptions(array $tree, int $level = 0): string
{
$html = '';
$indent = str_repeat(' |--', $level);
foreach ($tree as $node) {
$html .= "<option value='{$node['id']}'>{$indent}{$node['name']}</option>";
if (!empty($node['children'])) {
$html .= renderSelectOptions($node['children'], $level + 1);
}
}
return $html;
}
// 輸出完整下拉框
echo "<select name='category_id'>";
echo renderSelectOptions($tree);
echo "</select>";
?>
| 方案 | 時間複雜度 | 適用場景 | 優點 | 缺點 |
|---|---|---|---|---|
| 遞歸 | O(n²) 最壞情況 | 數據量小、層級淺 | 代碼簡單、易理解 | 大數據量時棧溢出風險、性能差 |
| 非遞歸(引用法) | O(n) | 數據量大、高併發 | 性能極佳、單次遍歷 | 代碼稍複雜、需理解引用機制 |
path 字段(如 0/1/2/3),查詢子孫節點只需 LIKE '0/1/2/%',極大提升讀取性能。總結:中小型項目首選非遞歸引用法,兼顧性能與可維護性;大型系統可結合路徑枚舉+緩存方案。