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

PHP 無限級分類兩大經典實現:遞歸樹狀輸出與表格式 TreeTable 算法詳解

     編輯:PHP 相關知識

PHP 無限級分類完整解決方案:遞歸輸出、TreeTable 表格渲染、數據庫設計、前端交互

無限級分類是 CMS、電商、OA 系統核心需求。本文深度解析兩種經典實現:遞歸樹狀輸出 (簡單直觀) 與 TreeTable 表格算法 (複雜佈局),含數據庫設計、PHP 核心代碼、前端渲染、性能優化、非遞歸替代方案。

一、數據庫設計 (邻接表模型)

CREATE TABLE `category` (
  `id` int UNSIGNED NOT NULL AUTO_INCREMENT,
  `parent_id` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '父級ID,0為頂級',
  `name` varchar(50) NOT NULL,
  `sort` int NOT NULL DEFAULT 0 COMMENT '同級排序',
  `level` tinyint NOT NULL DEFAULT 1 COMMENT '冗余層級,便於查詢',
  `path` varchar(255) NOT NULL DEFAULT '' COMMENT '路徑:0/1/5/ 便於 Like 查詢子樹',
  PRIMARY KEY (`id`),
  KEY `idx_parent` (`parent_id`),
  KEY `idx_path` (`path`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

關鍵冗余字段:level 避免遞歸計算層級,path 支援 LIKE '0/1/%' 一次查出整棵子樹。

二、方案一:遞歸樹狀輸出 (適合選單、下拉框)

<?php
function buildTree(array $items, int $parentId = 0, int $level = 0): array {
    $tree = [];
    foreach ($items as $item) {
        if ($item['parent_id'] == $parentId) {
            $item['level'] = $level;
            $item['children'] = buildTree($items, $item['id'], $level + 1);
            $tree[] = $item;
        }
    }
    return $tree;
}

// 渲染 HTML 選單
function renderSelect(array $tree, string $prefix = '') {
    foreach ($tree as $node) {
        echo "";
        if ($node['children']) {
            renderSelect($node['children'], $prefix . '├─ ');
        }
    }
}

// 使用
$all = $pdo->query("SELECT * FROM category ORDER BY sort")->fetchAll(PDO::FETCH_ASSOC);
$tree = buildTree($all);
echo "";
?>

三、方案二:TreeTable 表格算法 (適合後台管理列表)

核心思想:將樹結構映射為二維表格座標系,計算每個節點的 row (行)、col (列)、rowspan (跨行)、colspan (跨列),最終輸出標準 <table>。

<?php
class TreeTable {
    private array $nodes = []; // id => node data
    private int $maxCol = 0;
    private int $totalRows = 0;

    public function init(array $flatData): void {
        // 1. 建立索引
        foreach ($flatData as $row) {
            $this->nodes[$row['id']] = $row + [
                'children_ids' => [],
                'parent_ids'   => [],
            ];
        }
        // 2. 建立父子關系鏈
        foreach ($this->nodes as $id => $node) {
            // 子孫 IDs (遞歸收集)
            $this->nodes[$id]['children_ids'] = $this->getDescendantIds($id);
            // 祖先 IDs
            $this->nodes[$id]['parent_ids'] = $this->getAncestorIds($id);
            // 層級 (列號)
            $this->nodes[$id]['col'] = count($this->nodes[$id]['parent_ids']) + 1;
            $this->maxCol = max($this->maxCol, $this->nodes[$id]['col']);
        }
        // 3. 計算 rowspan (葉子節點數)
        foreach ($this->nodes as $id => $node) {
            $leafCount = $this->countLeaves($id);
            $this->nodes[$id]['rowspan'] = $leafCount ?: 1;
        }
        // 4. 排序:按祖先路徑字典序,保證父節點在子節點前
        uasort($this->nodes, function($a, $b) {
            $ap = implode(',', $a['parent_ids']);
            $bp = implode(',', $b['parent_ids']);
            return strcmp($ap, $bp) ?: ($a['id'] - $b['id']);
        });
        // 5. 分配行號
        $row = 1;
        foreach ($this->nodes as $id => $node) {
            $this->nodes[$id]['row'] = $row;
            $row += $node['rowspan'];
        }
        $this->totalRows = $row - 1;
        // 6. 計算 colspan
        foreach ($this->nodes as $id => $node) {
            $node['colspan'] = ($node['rowspan'] == 1) ? ($this->maxCol - $node['col'] + 1) : 0;
        }
    }

    private function getDescendantIds(int $id): array {
        $ids = [];
        foreach ($this->nodes as $nid => $node) {
            if (in_array($id, $node['parent_ids'])) $ids[] = $nid;
        }
        return $ids;
    }
    private function getAncestorIds(int $id): array {
        $ids = [];
        $pid = $this->nodes[$id]['parent_id'] ?? 0;
        while ($pid && isset($this->nodes[$pid])) {
            $ids[] = $pid;
            $pid = $this->nodes[$pid]['parent_id'] ?? 0;
        }
        return array_reverse($ids); // 從根到父
    }
    private function countLeaves(int $id): int {
        if (empty($this->nodes[$id]['children_ids'])) return 1;
        $count = 0;
        foreach ($this->nodes[$id]['children_ids'] as $cid) {
            $count += $this->countLeaves($cid);
        }
        return $count;
    }

    public function render(): string {
        $html = '';
        for ($r = 1; $r <= $this->totalRows; $r++) {
            $html .= '';
            for ($c = 1; $c <= $this->maxCol; $c++) {
                // 找到該坐標的節點
                $node = null;
                foreach ($this->nodes as $n) {
                    if ($n['row'] == $r && $n['col'] == $c) {
                        $node = $n; break;
                    }
                }
                if ($node) {
                    $rs = $node['rowspan'] > 1 ? " rowspan='{$node['rowspan']}'" : '';
                    $cs = $node['colspan'] > 1 ? " colspan='{$node['colspan']}'" : '';
                    $indent = str_repeat('  ', $node['col'] - 1);
                    $html .= "{$indent}{$node['name']}";
                }
            }
            $html .= '';
        }
        $html .= '
'; return $html; } } // 使用 $data = $pdo->query("SELECT * FROM category ORDER BY sort")->fetchAll(PDO::FETCH_ASSOC); $tt = new TreeTable(); $tt->init($data); echo $tt->render(); ?>

四、非遞歸優化:一次查詢 + 內存構建 (大數據量)

<?php
// 1. 一次性讀取所有數據 (按 sort 排序)
$rows = $pdo->query("SELECT * FROM category ORDER BY path, sort")->fetchAll(PDO::FETCH_ASSOC);

// 2. 內存構建樹 (O(N))
$tree = [];
$map = [];
foreach ($rows as $row) {
    $map[$row['id']] = $row + ['children' => []];
}
foreach ($map as $id => &$node) {
    if ($node['parent_id'] == 0) {
        $tree[] = &$node;
    } else {
        $map[$node['parent_id']]['children'][] = &$node;
    }
}
unset($node); // 釋放引用
?>

五、前端交互增強 (jQuery/Vue 示例)

<!-- TreeTable 支持展開/折疊 -->
<style>
.tree-table td[rowspan] { cursor: pointer; }
.tree-table tr.collapsed + tr { display: none; } /* 簡單隱藏邏輯需 JS 配合 */
</style>
<script>
// Vue 3 組件化 TreeTable (概念)
const TreeTable = {
  props: ['data'],
  template: `
    
▼ {{ cell.indent }}{{ cell.name }}
`, // computedRows 計算行數據... } ?>

六、替代方案:閉包表 / 路徑枚舉 / 嵌套集合

模型查詢子樹移動子樹插入/刪除適用場景
鄰接表 (親ID)遞歸/路徑 Like簡單 (改 parent_id)簡單通用、讀多寫少
路徑枚舉 (path)LIKE '0/1/%' 極快需更新所有後代 path簡單讀極多、層級固定
嵌套集合 (lft/rgt)BETWEEN 極快極複雜 (重排序號)複雜幾乎不變動的目錄
閉包表 (ancestor/descendant)JOIN 即時批量 INSERT/DELETE觸發器維護高頻移動、複雜查詢

推薦:中小項目用 鄰接表 + path 冗余,大型高併發用 閉包表 或 物化路徑 + 觸發器。

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