首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 陣列轉樹狀結構演算法大全:遞歸、引用、迭代三種實現與前端渲染範例

     編輯:PHP 相關知識

前言

將扁平陣列(如資料庫查詢結果)轉換為樹狀結構,是選單、分類、組織架構等功能的核心需求。本文整理三種主流演算法,並附上完整前端渲染 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;
}

完整前端渲染 Demo (jQuery + CSS)

以下程式碼展示如何將 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²) 最壞資料量中等、程式碼可讀性優先直觀易懂、易除錯深層資料易爆堆疊

建議: 一般專案首選引用法;資料量小或原型開發用標準遞歸;資料來源無序且層級關係複雜可考慮遞歸分組法。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved