尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
面试常问:如何将扁平列表转换为树形结构?深入剖析两种优雅实现
后台管理系统中地址三级联动、多级菜单、组织架构树……这些常见功能都离不开一个核心操作——列表转树。今天我们就来彻底搞懂这个经典算法并对比两种优雅的 JavaScript 实现。一、为什么需要“列表转树”在关系型数据库如 MySQL中我们通常会用一张表存储具有层级关系的数据每条记录通过parentId指向父级。例如idnameparentId1一级菜单A02一级菜单B03一级菜单A-114一级菜单A-1-135一级菜单B-12当我们执行SELECT * FROM menu时得到的就是一个扁平列表一维数组。但前端渲染树形组件如 el-tree、z-tree或级联选择器时需要的是嵌套的树形结构[ { id: 1, name: 一级菜单A, children: [ { id: 3, name: 一级菜单A-1, children: [ { id: 4, name: 一级菜单A-1-1, children: [] } ] } ] }, { id: 2, name: 一级菜单B, children: [ { id: 5, name: 一级菜单B-1, children: [] } ] } ]所以列表转树是前后端协作中的基础技能也是面试中的高频手写题。二、原始数据我们沿用上面的菜单数据const flatList [ { id: 1, name: 一级菜单A, parentId: 0 }, { id: 2, name: 一级菜单B, parentId: 0 }, { id: 3, name: 一级菜单A-1, parentId: 1 }, { id: 4, name: 一级菜单A-1-1, parentId: 3 }, { id: 5, name: 一级菜单B-1, parentId: 2 }, ];核心逻辑每个节点通过parentId指向父节点parentId: 0表示根节点顶级。三、方法一利用Map建立索引经典易读思路第一次遍历将所有节点存入Map以id为键同时为每个节点初始化children空数组不影响原对象。第二次遍历取出当前节点根据其parentId找到父节点。若父节点存在将当前节点加入父节点的children中若父节点不存在即parentId为 0 或找不到说明当前节点是根节点直接放入tree数组。代码实现function listToTree(list) { const map new Map(); const tree []; // 第一步构建 id - node 映射并为每个节点准备 children list.forEach((item) { map.set(item.id, { ...item, // 展开原属性 children: [] // 新增子节点容器 }); }); // 第二步组装父子关系 list.forEach((item) { const current map.get(item.id); // 当前节点含 children const parent map.get(item.parentId); // 父节点含 children if (parent) { parent.children.push(current); // 挂到父节点下 } else { tree.push(current); // 没有父节点 → 根节点 } }); return tree; }逐步解析发生了什么map的妙用以id为键将每个节点平铺存储。注意我们使用了{ ...item, children: [] }这样新对象不会影响原flatList中的对象浅拷贝一层。为什么先存再组装因为flatList是乱序的比如子节点可能出现在父节点之前如果边遍历边挂载可能父节点尚未创建。但这里我们预先全部存入map所以无论如何都能找到父节点。根节点判断parentId 0时map.get(0)返回undefined因此该节点被直接推入tree数组。测试console.log(JSON.stringify(listToTree(flatList), null, 2));输出结果即为上面期望的树形结构。四、方法二一招reduce搞定函数式风格有些开发者偏爱reduce因为它能将遍历和累积过程浓缩在一个表达式里。我们可以用reduce同时完成构建映射和组装树。代码实现function listToTree(list) { // 用 reduce 构建 id - node 映射 const nodeMap list.reduce((map, item) { map[item.id] { ...item, children: [] }; return map; }, {}); // 初始化为空对象 // 再用 reduce 组装树 return list.reduce((tree, item) { const cur nodeMap[item.id]; const parent nodeMap[item.parentId]; if (parent) { parent.children.push(cur); } else { tree.push(cur); } return tree; }, []); // 初始化为空数组 }解析reduce的每一步reduce 记忆口诀通用语法arr.reduce((累加器, 当前元素, 当前索引, 原数组) { // 必须 return 累加器作为下一次迭代的“累加器” }, 初始值);两个关键点初始值决定了累加器的类型写{}最终得到对象写[]最终得到数组写0最终得到数字。必须return如果不return下一次累加器就会变成undefined导致程序报错。reduce是数组的归并方法arr.reduce(callback, initialValue)每次迭代将上次的累积结果accumulator与当前元素一起传入回调。第一步reduce累积器map初始为{}。每次迭代把item.id作为 key值为{ ...item, children: [] }。最后返回一个完整的映射对象nodeMap类似{ 1: {...}, 2: {...}, ... }。第二步reduce累积器tree初始为[]。每次迭代取出当前节点cur和父节点parent。如果父节点存在parent.children.push(cur)否则tree.push(cur)。最后返回tree即根节点数组。与 Map 版本的对比数据结构Map比普通对象更适合频繁查找因为Map的键可以是任意类型且查找效率稳定。不过这里id是数字对象也能胜任。代码风格reduce更“函数式”但嵌套两层reduce可读性稍逊于两次forEach。实际开发中两者都常见看团队偏好。性能两者都是 O(n) 时间复杂度只遍历两次性能优秀。五、扩展处理边界情况实际业务中可能会遇到以下问题我们可以优化数据顺序混乱我们的方法已经通过提前构建映射解决了父节点不必出现在子节点之前。循环引用如果数据错误出现parentId形成环如 A.parentId B, B.parentId A递归会导致死循环。但我们的方法只构建父子关系不会无限递归因为只遍历一次不会追踪子子孙孙。根节点标识有的系统用null或0表示根节点parentId不存在则视为根。我们可以将parentId做类型统一例如if (item.parentId null || item.parentId 0)。原地修改 vs 拷贝我们使用{ ...item }复制了对象避免污染原始数据。如果不需要保留原数据也可以直接修改原对象但为了安全建议复制。六、总结应用场景后台管理系统的菜单树、组织架构、评论楼中楼、地区级联等。核心思想利用id和parentId建立映射通过两次线性遍历完成组装。两种实现MapforEach清晰直观reduce更简洁但需要理解归并过程。面试亮点手写时注意先映射再组装避免顺序依赖提到空间换时间用 Map 或对象存储索引考虑递归转迭代减少栈风险但这里无需递归。掌握了这个算法你再也不用写复杂的递归去遍历了而且效率更高。下次面试官问起不妨自信地写出这两种实现并解释其背后的原理。
RELATED

相关推荐

DeepInsightTheorem:基于技巧识别的LLM数学推理增强框架

DeepInsightTheorem:基于技巧识别的LLM数学推理增强框架

1. 项目概述:从“解题”到“解构”的思维跃迁最近在折腾大语言模型(LLM)的应用开发,特别是数学推理这块,发现一个挺有意思的现象。很多模型,无论是开源的还是闭源的,在解决复杂数学问题时&#…

📅 2026/11/14 22:10:26
Go数学计算避坑指南:精度、溢出与类型安全实战

Go数学计算避坑指南:精度、溢出与类型安全实战

1. 项目概述:用 Go 做数学计算,不是写计算器,而是构建可验证、可扩展、可嵌入的数值处理能力“Cmo hacer clculos matemticos en Go con operadores”——这个西班牙语标题直译是“如何在 Go 中使用运算符进行数学计算”,乍看像入…

📅 2026/11/14 22:10:26
Ubuntu 14.04 Droplet自动扩缩容实战:Shell级轻量方案

Ubuntu 14.04 Droplet自动扩缩容实战:Shell级轻量方案

1. 为什么在 Ubuntu 14.04 Droplet 上做自动扩缩容,本质上是在和时间赛跑你点开 DigitalOcean 控制台,新建一台 Ubuntu 14.04 的 Droplet,选了 1GB 内存、1 核 CPU,装上 Nginx、Passenger、PostgreSQL,再部署一个 Ruby…

📅 2026/11/5 11:35:55
MORE NEWS

更多资讯

📰

怎么考水电工证的,外省证能转过来吗?报名流程全解析

怎么考水电工证的,外省证能转过来吗?报名流程全解析 之前考的证在外省能不能转过来?这个问题,不少从事水电安装、维修、施工的同行都问过,特别是南阳的劳务班组负责人,经常遇到工人拿着外地的电工证,担心不能在当地使用。其实,国家早就有政策规定,…

📰

新疆电工证在哪里查询多少钱?学历不高也能报名的实操指南

新疆电工证在哪里查询多少钱?学历不高也能报名的实操指南 你是不是也担心自己学历不高,担心报名电工证会因为学历被拒?其实不然,新疆电工证的报考门槛并没有你想象的那么高,关键是要了解流程、准备材料,以及知道在哪里查询。本文将一步步告诉你【新疆电…

📰

电工证掉了只要有编号怎么补救?官方报名入口在这儿

电工证掉了只要有编号怎么补救?官方报名入口在这儿 怕考不过白交培训费,现在很多人拿着电工证掉了只要有编号的旧证来咨询,到底还能不能用?能不能补办?其实,这背后藏着不少人的焦虑,尤其是像你我这样的中小型施工企业负责人,手里握着十几个工种的证,…

📰

电工证多久能查 考在外省的证千万别踩坑

电工证多久能查 考在外省的证千万别踩坑 之前考的证在外省能不能转过来?这几乎是每个准备报考电工证的人心里最怕的问题。特别是像许昌这样的中小施工企业负责人,手里握着一堆证,但一到外地就发现“卡壳”, 千万别踩坑…

📰

电工证的其他用途你知道还能干啥吗?通过率揭秘

电工证的其他用途你知道还能干啥吗?通过率揭秘 之前考的证在外省能不能转过来?这个问题其实很多人都问过,特别是那些在外地打工、或者刚从学校毕业的应届生。你可能觉得电工证就是个上岗证,但它的用途可不止这些。今天咱们就聊聊电工证的其他用途,顺便揭…

📰

弱电有电工证吗保姆级教程:工地太忙根本没时间复习考试怎么办

弱电有电工证吗保姆级教程:工地太忙根本没时间复习考试怎么办 工地太忙根本没时间复习考试,你不是一个人。在河南鹤壁,很多从事弱电工程的工人也常遇到这个困惑:弱电有电工证吗?能不能用低压电工证去接活?这不仅关系到能否顺利接工程,还影响着你的职业…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬