用字典缓存节点实现O(n)建树:先遍历构建id→node映射,再按parent_id挂载子节点;需统一处理parent_id为空/0/None为哨兵值,缺失父节点时在map中占位,利用Python 3.7+ dict顺序性保证子节点顺序。

用字典缓存节点比递归重建快得多
直接遍历列表反复查找父节点,时间复杂度会到 O(n²),尤其数据量过千就明显卡顿。核心思路是:**先扫一遍建好所有节点的 id → node 映射,再按 parent_id 一次性挂载子节点**。这样只需两次线性遍历,稳定 O(n)。
关键约束:每个元素必须含 id 和 parent_id 字段(parent_id 可为 None、0 或空字符串,需统一处理)。
- 避免用
list.index()或next(filter(...))查父节点——每次调用都重扫列表 - 初始化时把所有节点放进
node_map = {},键用str(item['id'])更安全(防 int/str 混用) -
parent_id值若为None或0,建议提前归一化为统一哨兵值(如'ROOT'),避免后续判断分支过多
处理 parent_id 为空或不存在的情况
真实数据里常有 parent_id 缺失、为 0、None 或根本不在列表中。不处理会导致子节点丢失或 KeyError。
推荐做法:扫第一遍时,把所有 parent_id 对应的父节点也纳入 node_map 占位(即使它没出现在原始列表中),但只给它一个最小骨架(如 {'id': pid, 'children': []})。这样挂载时不会报错,且不影响已有节点数据。
立即学习“Python免费学习笔记(深入)”;
- 如果明确知道根节点
parent_id是0,可先过滤出所有parent_id == 0的项作为 roots,其余再挂载——但要小心有些数据用None表示根 - 挂载循环中,用
node_map.setdefault(parent_id, {'id': parent_id, 'children': []})安全获取父节点 - 最终返回的是所有
parent_id不在node_map中的节点(即真正根节点),不是硬写parent_id == 0
Python 3.7+ 直接用 dict 保持插入顺序
构建树时,子节点顺序往往要和原始列表中出现顺序一致(比如菜单排序)。Python 3.7+ 的 dict 默认保持插入顺序,正好利用这点:遍历时按列表顺序往 parent['children'] 中 .append(),结果天然有序。
如果用 defaultdict(list) 或预分配空列表,顺序不受影响;但若中间用了 sorted() 或字典推导式打乱遍历顺序,就会出问题。
- 不要对原始列表做
sorted(items, key=lambda x: x['parent_id'])—— 这会破坏业务要求的顺序 - 子节点列表必须用
.append(),别用+=或extend()拼接其他结构 - 如果需要按某字段(如
sort_order)二次排序,那是构建完树之后的独立步骤,不应混入构建逻辑
递归深度超限?改用栈模拟递归
当树深度超过 1000 层(比如组织架构嵌套极深),Python 默认递归限制会触发 RecursionError。此时不能靠 sys.setrecursionlimit() 硬扛——可能引发解释器崩溃或内存溢出。
正确解法:用显式栈(list)替代函数调用栈。把待处理节点(含其目标父节点引用)压栈,循环弹出并挂载,完全规避递归。
- 栈中每个元素是元组:
(current_node, parent_id),初始压入所有节点(parent_id来自原数据) - 主循环中,取一个节点,查
node_map得父节点,append进去;若父节点还不存在,则暂存到等待队列,下一轮再试 - 实际中更常用两遍扫描(前文所述),只有在极端深度 + 动态父节点缺失场景才需栈方案
parent_id 的语义是否全局一致——同一份数据里混用 0、None、'' 表示根,又没文档说明,比写代码花的时间多十倍。


















