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

PHP 實現無限分類遞迴演算法:從基礎遞迴到非遞迴樹構建完整指南

     編輯:PHP 相關知識

PHP 實現無限分類遞迴演算法:從基礎遞迴到非遞迴樹構建完整指南

無限分類(無限級分類)是電商、CMS、權限系統的核心資料結構。本文系統講解遞迴與非遞迴兩大流派的樹構建演算法,並提供生產級優化方案。

資料結構定義

$categories = [
    ['id' => 1, 'name' => '電子產品', 'pid' => 0, 'sort' => 1],
    ['id' => 2, 'name' => '手機通訊', 'pid' => 1, 'sort' => 1],
    ['id' => 3, 'name' => '電腦辦公', 'pid' => 1, 'sort' => 2],
    ['id' => 4, 'name' => '智能手機', 'pid' => 2, 'sort' => 1],
    ['id' => 5, 'name' => '筆記本電腦', 'pid' => 3, 'sort' => 1],
    ['id' => 6, 'name' => '台式機', 'pid' => 3, 'sort' => 2],
    ['id' => 7, 'name' => '配件外設', 'pid' => 1, 'sort' => 3],
    ['id' => 8, 'name' => '手機殼', 'pid' => 4, 'sort' => 1],
    ['id' => 9, 'name' => '滑鼠鍵盤', 'pid' => 7, 'sort' => 1],
];

方案一:遞迴構建樹結構(經典、直觀)

function buildTreeRecursive(array $items, int $pid = 0): array
{
    $tree = [];
    foreach ($items as $item) {
        if ($item['pid'] === $pid) {
            $children = buildTreeRecursive($items, $item['id']);
            if ($children) {
                $item['children'] = $children;
            }
            $tree[] = $item;
        }
    }
    return $tree;
}

$tree = buildTreeRecursive($categories);
print_r($tree);

輸出結構:

Array
(
    [0] => Array
        (
            [id] => 1
            [name] => 電子產品
            [pid] => 0
            [children] => Array
                (
                    [0] => Array
                        (
                            [id] => 2
                            [name] => 手機通訊
                            [children] => Array
                                (
                                    [0] => Array
                                        (
                                            [id] => 4
                                            [name] => 智能手機
                                            [children] => Array
                                                (
                                                    [0] => Array
                                                        (
                                                            [id] => 8
                                                            [name] => 手機殼
                                                        )
                                                )
                                        )
                                )
                        )
                    ...
                )
        )
)

缺陷:每次遞迴遍歷整個陣列,時間複雜度 O(n²),資料量大時效能急劇下降。

方案二:引用法單次遍歷構建樹(O(n) 時間複雜度,推薦)

function buildTreeOptimized(array $items): array
{
    // 1. 建立 ID 索引映射
    $map = [];
    foreach ($items as $item) {
        $map[$item['id']] = $item;
    }
    
    // 2. 單次遍歷建立父子關係(利用引用)
    $tree = [];
    foreach ($items as $item) {
        $id = $item['id'];
        $pid = $item['pid'];
        
        // 確保 map 中有該節點的引用
        if (!isset($map[$id]['children'])) {
            $map[$id]['children'] = [];
        }
        
        if ($pid === 0) {
            // 根節點
            $tree[] = &$map[$id];
        } elseif (isset($map[$pid])) {
            // 掛載到父節點
            $map[$pid]['children'][] = &$map[$id];
        }
        // 孤兒節點(父節點不存在)可選擇忽略或掛載到特殊容器
    }
    
    return $tree;
}

$tree = buildTreeOptimized($categories);

核心技巧:使用 &$map[$id] 引用,確保父節點的 children 陣列指向同一記憶體地址,單次遍歷即可完成樹構建。

方案三:非遞迴層級輸出(適用於選單渲染、不需完整樹結構)

function printTreeLevel(array $items, int $pid = 0, int $level = 0): void
{
    foreach ($items as $item) {
        if ($item['pid'] === $pid) {
            echo str_repeat('│  ', $level) . '├─ ' . $item['name'] . "\n";
            printTreeLevel($items, $item['id'], $level + 1);
        }
    }
}

printTreeLevel($categories);

輸出:

├─ 電子產品
│  ├─ 手機通訊
│  │  ├─ 智能手機
│  │  │  └─ 手機殼
│  ├─ 電腦辦公
│  │  ├─ 筆記本電腦
│  │  └─ 台式機
│  └─ 配件外設
│     └─ 滑鼠鍵盤

方案四:資料庫層面優化(大規模資料必備)

1. 閉包表 - 最強查詢效能

-- 閉包表結構
CREATE TABLE category_closure (
    ancestor INT NOT NULL,
    descendant INT NOT NULL,
    depth INT NOT NULL,
    PRIMARY KEY (ancestor, descendant)
);

-- 查詢某節點所有子孫(含自身)
SELECT c.* FROM categories c
JOIN category_closure cc ON c.id = cc.descendant
WHERE cc.ancestor = 1 AND cc.depth > 0;

-- 查詢某節點所有祖先
SELECT c.* FROM categories c
JOIN category_closure cc ON c.id = cc.ancestor
WHERE cc.descendant = 8 AND cc.depth > 0;

2. 路徑枚舉 - 簡單實用

-- 增加 path 欄位:/1/2/4/8/
ALTER TABLE categories ADD COLUMN path VARCHAR(500) DEFAULT '';

-- 查詢子樹
SELECT * FROM categories WHERE path LIKE '/1/2/4/%';

-- 查詢祖先鏈
SELECT * FROM categories WHERE '/1/2/4/8/' LIKE CONCAT(path, '%') AND id != 8;

3. 巢狀集合 - 適合靜態樹

-- left/right 編號
-- 查詢子樹:left > parent.left AND right < parent.right
-- 移動子樹需重新編號,寫入成本高

生產級工具類封裝

<?php
declare(strict_types=1);

namespace App\Utils;

class TreeBuilder
{
    public static function build(array $items, string $idKey = 'id', string $pidKey = 'pid', string $childrenKey = 'children'): array
    {
        $map = [];
        foreach ($items as $item) {
            $id = $item[$idKey];
            $map[$id] = $item;
            $map[$id][$childrenKey] = [];
        }
        
        $tree = [];
        foreach ($items as $item) {
            $id = $item[$idKey];
            $pid = $item[$pidKey];
            
            if ($pid === 0 || $pid === '0' || $pid === null) {
                $tree[] = &$map[$id];
            } elseif (isset($map[$pid])) {
                $map[$pid][$childrenKey][] = &$map[$id];
            }
        }
        return $tree;
    }
    
    public static function getChildren(array $tree, int $targetId, string $idKey = 'id', string $childrenKey = 'children'): array
    {
        foreach ($tree as $node) {
            if ($node[$idKey] === $targetId) {
                return $node[$childrenKey] ?? [];
            }
            if (!empty($node[$childrenKey])) {
                $result = self::getChildren($node[$childrenKey], $targetId, $idKey, $childrenKey);
                if ($result) return $result;
            }
        }
        return [];
    }
    
    public static function getPath(array $tree, int $targetId, string $idKey = 'id', string $childrenKey = 'children'): array
    {
        $path = [];
        self::findPath($tree, $targetId, $path, $idKey, $childrenKey);
        return $path;
    }
    
    private static function findPath(array $tree, int $targetId, array &$path, string $idKey, string $childrenKey): bool
    {
        foreach ($tree as $node) {
            $path[] = $node;
            if ($node[$idKey] === $targetId) {
                return true;
            }
            if (!empty($node[$childrenKey]) && self::findPath($node[$childrenKey], $targetId, $path, $idKey, $childrenKey)) {
                return true;
            }
            array_pop($path);
        }
        return false;
    }
    
    public static function toFlat(array $tree, string $childrenKey = 'children'): array
    {
        $flat = [];
        foreach ($tree as $node) {
            $children = $node[$childrenKey] ?? [];
            unset($node[$childrenKey]);
            $flat[] = $node;
            if ($children) {
                $flat = array_merge($flat, self::toFlat($children, $childrenKey));
            }
        }
        return $flat;
    }
}

// 使用
$tree = TreeBuilder::build($categories);
$path = TreeBuilder::getPath($tree, 8); // 獲取手機殼的完整路徑

效能對比與選型建議

方案時間複雜度空間複雜度適用場景資料量建議
遞迴遍歷O(n²)O(h)教學、極小資料集< 100 條
引用法單次遍歷O(n)O(n)通用 Web 應用、API 回傳< 10,000 條
閉包表O(log n) 查詢O(n log n)高頻讀取、複雜祖先/子孫查詢> 10,000 條
路徑枚舉O(log n) 查詢O(n)中等規模、簡單部署< 50,000 條

常見陷阱與解決方案

  1. 循環引用:資料臟導致 A→B→A,構建前檢測 pid === id 或使用拓撲排序驗證
  2. 孤兒節點:父節點被刪除,子節點 pid 指向不存在 ID,定時清理或軟刪除標記
  3. 深度過大:PHP 預設遞迴限制 100 層,超深樹需非遞迴實現或調整 xdebug.max_nesting_level
  4. 併發修改:移動節點時鎖定整棵樹或使用資料庫行鎖,防止樹結構破損

掌握引用法單次遍歷構建樹是 PHP 開發者必備技能;大規模場景請務必在資料庫層面佈局閉包表或路徑枚舉,將複雜度前置到寫入端,換取讀取端的極致效能。

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