將扁平陣列(如資料庫查詢結果)轉換為樹狀結構,是選單、分類、組織架構等功能的核心需求。本文整理三種主流演算法,並附上完整前端渲染 Demo。
核心思想:先建立 id => node 的雜湊表,再反向遍歷將子節點掛載到父節點的 children 屬性,最後返回根節點。
function arrayToTree($sourceArr, $key, $parentKey, $childrenKey) {
$temp = [];
// 1. 建立 id 索引
foreach ($sourceArr as $v) {
$temp[$v[$key]] = $v;
}
// 2. 反向遍歷掛載子節點
for ($i = count($sourceArr) - 1; $i >= 0; $i--) {
$pid = $sourceArr[$i][$parentKey];
if (isset($temp[$pid])) {
$node = $temp[$sourceArr[$i][$key]];
// 初始化父節點 children 陣列
$temp[$pid][$childrenKey] = isset($temp[$pid][$childrenKey]) && is_array($temp[$pid][$childrenKey])
? $temp[$pid][$childrenKey] : [];
// 將當前節點推入父節點 children
array_push($temp[$pid][$childrenKey], $node);
}
}
return $temp; // 返回包含所有節點的陣列,根節點在頂層
}
先找出所有葉子節點,從底向上遞歸組裝,直到所有節點都有父節點或成為根節點。
function arrayToTreeRecursive($sourceArr, $key, $parentKey, $childrenKey) {
$temp = [];
$allRoot = true;
$leafArr = [];
foreach ($sourceArr as $v) {
$isLeaf = true;
foreach ($sourceArr as $cv) {
if ($v[$key] != $cv[$key]) {
if ($v[$key] == $cv[$parentKey]) $isLeaf = false;
if ($v[$parentKey] == $cv[$key]) $allRoot = false;
}
}
if ($isLeaf) $leafArr[$v[$key]] = $v;
$temp[$v[$key]] = $v;
}
if ($allRoot) return $temp; // 無父子關係,直接返回
unset($v, $cv, $sourceArr, $isLeaf);
foreach ($leafArr as $v) {
if (isset($temp[$v[$parentKey]])) {
$temp[$v[$parentKey]][$childrenKey] = isset($temp[$v[$parentKey]][$childrenKey]) && is_array($temp[$v[$parentKey]][$childrenKey])
? $temp[$v[$parentKey]][$childrenKey] : [];
array_push($temp[$v[$parentKey]][$childrenKey], $v);
unset($temp[$v[$key]]); // 從頂層移除,避免重複
}
}
return arrayToTreeRecursive($temp, $key, $parentKey, $childrenKey); // 遞歸處理上一層
}
定義 getTree($data, $pid),每次查找父 ID 為 $pid 的節點,遞歸建立其 child 屬性。
function getTree($data, $pid = 0, $key = 'id', $pKey = 'parentId', $childKey = 'child', $maxDepth = 0) {
static $depth = 0;
$depth++;
if ($maxDepth <= 0) $maxDepth = count($data) * count($data); // 防無限遞歸
if ($depth > $maxDepth) exit("error recursion: max depth $maxDepth");
$tree = [];
foreach ($data as $rk => $rv) {
if ($rv[$pKey] == $pid) {
$rv[$childKey] = getTree($data, $rv[$key], $key, $pKey, $childKey, $maxDepth);
$tree[] = $rv;
}
}
return $tree;
}
以下程式碼展示如何將 PHP 生成的樹陣列輸出為巢狀 <dl><dt><dd> HTML,並搭配 jQuery 實現摺疊/展開互動。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>樹狀選單 Demo</title>
<style>
.kyp_tree { font: 12px/2.5 verdana; float:left; }
.kyp_tree dd { margin-left:20px; }
.kyp_tree dl { border-bottom:1px dashed #ccc; }
.kyp_tree dl dl, .kyp_tree dl.last { border:none; }
.kyp_tree dt { background:url(tree_top.gif) no-repeat 2px -57px; padding-left:15px; cursor:pointer; height:30px; line-height:27px; }
.kyp_tree dd dt { background:url(tree_arrow.gif) no-repeat 2px 10px; font-size:12px; }
.kyp_tree dt.open { background-position: 2px 12px; }
.kyp_tree dd dt.open { background-position: 0 -25px; }
.kyp_tree dt.nosub { background:none; }
.kyp_tree a { color:#333; text-decoration:none; }
.kyp_tree a:hover { color:#AE0002; }
</style>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
jQuery.fn.createTree = function(fn, ini) {
var $ = jQuery, ini = Object(ini);
this.find('dd').hide();
this.children('dl:last').addClass('last');
this.find('dt').each(function() {
var nosub = $(this).next('dd').size() == 0;
if (nosub) $(this).addClass('nosub');
if (ini.id && ini.id == $(this).attr('classify')) {
$(this).parents('dd').show().prev('dt').addClass('open');
$(this).addClass('red_sub');
if (nosub) $(this).addClass('currentClass');
else { $(this).next('dd').show(); $(this).addClass('open'); }
}
}).click(function(e) {
var dd = $(this).next('dd'), isClose = dd.css('display') == 'none';
if (dd.size()) {
if (isClose) { dd.show(); $(this).addClass('open'); }
else { dd.hide(); $(this).removeClass('open'); }
}
return fn && fn.call(this, e, dd);
});
if (ini.mx) {
this.find('dt').click(function(e) {
var J = $(this);
if (J.next('dd').size() && J.hasClass('open')) {
J.parent().siblings('dl').children('dd').hide();
J.parent().siblings('dl').children('dt').removeClass('open');
J.next('dd').show(); J.addClass('open');
}
});
}
};
$(function() {
$('#tree_wrap').createTree(function(e, dd) {
$('#show').html(this.innerHTML + dd.size());
}, {mx: 1, id: 200});
});
</script>
</head>
<body>
<?php
// 測試資料
$tree = [
1 => ['id'=>1, 'cname'=>'一級分類', 'pid'=>0],
100 => ['id'=>100, 'cname'=>'二級分類A', 'pid'=>1],
101 => ['id'=>101, 'cname'=>'二級分類B', 'pid'=>1],
2 => ['id'=>2, 'cname'=>'二級分類', 'pid'=>1],
3 => ['id'=>3, 'cname'=>'三級分類', 'pid'=>2],
4 => ['id'=>4, 'cname'=>'四級分類', 'pid'=>3],
5 => ['id'=>5, 'cname'=>'四級分類2', 'pid'=>3],
200 => ['id'=>200, 'cname'=>'五級分類', 'pid'=>5],
6 => ['id'=>6, 'cname'=>'另一頂層', 'pid'=>0],
7 => ['id'=>7, 'cname'=>'First', 'pid'=>0],
8 => ['id'=>8, 'cname'=>'First Child', 'pid'=>7],
];
// 輔助函數:取得直接子節點
function getChildArray($id) {
global $tree;
$arr = [];
foreach ($tree as $k => $v) if ($v['pid'] == $id) $arr[$k] = $v;
return $arr;
}
function getChildTotal($id) {
global $tree;
$total = 0;
foreach ($tree as $v) if ($v['pid'] == $id) $total++;
return $total;
}
// 遞歸生成 HTML
function getTreeHTML($tree, $level = 0) {
$html = '';
if ($tree) {
$level++;
foreach ($tree as $id => $node) {
$html .= "<dl>";
$html .= '<dt classify="'.$node['id'].'"><a href="#">'.$node['cname'].'</a></dt>';
if (getChildTotal($node['id'])) {
$html .= '<dd>';
$html .= getTreeHTML(getChildArray($node['id']), $level);
$html .= '</dd>';
}
$html .= '</dl>';
}
}
return $html;
}
$html = getTreeHTML(getChildArray(0));
echo '<div id="tree_wrap" class="kyp_tree">';
echo $html;
echo '</div><div id="show" style="clear:both;border-top:1px solid red"></div>';
?>
</body>
</html>
| 演算法 | 時間複雜度 | 適用場景 | 優點 | 缺點 |
|---|---|---|---|---|
| 引用法 | O(n) | 大量資料、高效能需求 | 單次遍歷、無遞歸、記憶體友善 | 邏輯稍抽象 |
| 遞歸分組 | O(n²) 最壞 | 資料層級深、需自動處理多根 | 自動處理無序資料 | 多層遞歸、效能較差 |
| 標準遞歸 | O(n²) 最壞 | 資料量中等、程式碼可讀性優先 | 直觀易懂、易除錯 | 深層資料易爆堆疊 |
建議: 一般專案首選引用法;資料量小或原型開發用標準遞歸;資料來源無序且層級關係複雜可考慮遞歸分組法。