递归构建树结构应避免每次遍历全量数组,正确做法是先建ID映射表实现O(n)时间复杂度,注意引用赋值、根节点多态判断、环检测、深度限制及挂载后统一排序。

递归函数里别反复遍历全量数组
常见错误是写一个 buildTree() 函数,每次递归都用 foreach 扫一遍原始数据找子节点。1000 条数据、5 层深,最坏情况要遍历 5000 次,时间复杂度 O(n²)。真实项目里这会拖慢接口几百毫秒。
正确做法是先建索引映射表:$map[$node['id']] = $node,再用 isset($map[$pid]) 快速定位父节点。这样两轮遍历搞定,总时间复杂度压到 O(n)。
- 第一遍:把每个节点存进
$map,并初始化children为空数组 - 第二遍:对每个节点,查它的
parent_id是否在$map中;在就挂到对应父节点的children里,不在就归入根数组 - 必须用
&$map[$node['id']]加引用,否则children里存的是副本,嵌套结构不生效
根节点判断不能只写 parent_id == 0
数据库里根节点的 parent_id 可能是 NULL、空字符串、0,甚至 'root'。硬写 == 0 会导致部分根节点被漏掉,前端菜单直接缺一级。
判断逻辑得覆盖所有可能:
立即学习“PHP免费学习笔记(深入)”;
- 用
!isset($node['parent_id']) || $node['parent_id'] === null || $node['parent_id'] === '' - 如果约定根为
0,也要加类型严格判断:$node['parent_id'] === 0,避免'0'或false被误判 - 更稳妥的做法是显式传入根标识符,比如
$rootPid = null,然后统一用$node['parent_id'] === $rootPid
深层递归容易栈溢出或死循环
组织架构数据若存在环形引用(A→B→C→A)或脏数据(parent_id 指向不存在的 ID),纯递归会一直调用直到 Fatal error: Maximum function nesting level of '256' reached。
必须加深度控制和防环机制:
- 递归函数签名带上
$depth = 0参数,入口处加if ($depth > 20) return []; - 维护一个已访问 ID 集合(如
$visited = []),每次递归前检查当前$id是否已在其中,有则立即返回空数组 - 不要依赖数据库字段值做唯一判定——有些系统用
uuid当id,但parent_id存的是数字,类型不一致会导致isset($map[$pid])失效
排序必须挂载完再做,不能在递归里 usort
很多人想在每层递归里对 $children 排序,结果发现只有顶层生效。原因是:挂载时用的是引用,但 usort() 会重排数组键顺序,破坏引用关系,子节点实际没进对的地方。
正确时机是全部挂载完成之后,逐层后序处理:
- 先跑完两轮映射挂载,得到完整树
- 写一个独立的递归函数,参数是节点(含
children),在里面调用usort($node['children'], fn($a, $b) => $a['sort_order'] $b['sort_order']) - 确保排序字段名一致,比如有的表叫
weight、有的叫listorder,别写死成sort_order



















