“前端不就是切图仔吗?怎么还要懂数据结构?”
——直到你遇到这道大厂高频面试题。
🌳 一道题,看穿你的“内功”
假如面试官只问了一个问题:给你一个扁平数组,如何转成树形结构? 我当场写了两种方法,还分析了时间复杂度,面试官直接点头说‘可以了’。”
听起来简单?别急,这题背后藏着 递归思维、算法优化、工程实践 三大考点。今天我们就来拆解这道经典题,顺便看看它在真实项目中是怎么“活”起来的。
📋 题目描述
给定一个扁平列表,每个元素包含 id、parentID 和 name,其中 parentID === 0 表示根节点。要求将其转换为树形结构。
const list = [
{ id: 1, parentID: 0, name: 'A' },
{ id: 2, parentID: 1, name: 'B' },
{ id: 3, parentID: 2, name: 'C' },
{ id: 4, parentID: 3, name: 'D' }
]
期望输出:
[
{
id: 1,
parentID: 0,
name: 'A',
children: [
{
id: 2,
parentID: 1,
name: 'B',
children: [
{
id: 3,
parentID: 2,
name: 'C',
children: [
{ id: 4, parentID: 3, name: 'D', children: [] }
]
}
]
}
]
}
]
🔁 解法一:朴素递归(O(n²))
这是大多数人第一反应想到的方法——递归 + 筛选。
function list2tree(list, parentID = 0) {
return list
.filter(item => item.parentID === parentID)
.map(item => ({
...item,
children: list2tree(list, item.id)
}));
}
✅ 优点:
- 代码简洁,逻辑清晰,符合人类直觉。
- 一眼看出“父子关系”是通过
parentID建立的。
❌ 缺点:
- 时间复杂度 O(n²) :每次递归都要遍历整个列表找子节点。
- 数据量一大(比如省市区 3000+ 条),性能直接“雪崩”。
💡 面试官心里OS:嗯,基础还行,但能不能再想想?
⚡ 解法二:Map 优化(O(n))
既然递归慢是因为重复遍历,那我们就用 空间换时间!
核心思想:先建映射表,再挂载父子关系。
function listToTree(list) {
const map = new Map();
const result = [];
// 第一步:把所有节点放进 Map,初始化 children
for (const item of list) {
map.set(item.id, { ...item, children: [] });
}
// 第二步:遍历一次,挂载到父节点 or 根数组
for (const item of list) {
if (item.parentID === 0) {
result.push(map.get(item.id));
} else {
const parent = map.get(item.parentID);
if (parent) parent.children.push(map.get(item.id));
}
}
return result;
}
✅ 优点:
- 时间复杂度 O(n) ,性能飞跃!
- 利用了
Map的 O(1) 查找,比对象更规范(尤其 key 是数字时)。 - 实际项目中推荐使用!
❌ 注意点:
- 要确保数据合法(比如不能有循环引用,否则会死循环)。
- 如果
parentID指向不存在的节点,需要做容错处理(比如忽略或报错)。
💡 面试官眼睛一亮:“哦?还知道用 Map 优化?有点东西!”
🧠 面试官真正想考什么?
别以为这只是个“写代码”题,其实他在考察:
- 数据结构敏感度
能否从parentID联想到树?能否识别出“层级关系”? - 算法思维
是否意识到 O(n²) 的问题?有没有优化意识? - 工程落地能力
这种结构在哪儿用?怎么处理异常?是否考虑过大数据量?
🌍 真实场景:这玩意儿真有用!
你以为这只是面试造火箭?错!它天天在你项目里跑:
场景1:省市区三级联动
数据库通常这样存:
id | parentID | name
1 | 0 | 北京市
2 | 1 | 朝阳区
3 | 1 | 海淀区
4 | 0 | 上海市
5 | 4 | 浦东新区
前端拿到后必须转成树,才能渲染下拉菜单!
场景2:后台管理系统 - 菜单权限
菜单配置也是扁平存储,但侧边栏需要树形结构:
{ id: 101, parentID: 0, title: '用户管理' }
{ id: 102, parentID: 101, title: '用户列表' }
{ id: 103, parentID: 101, title: '角色管理' }
场景3:评论嵌套(如掘金评论区)
每条评论有 parentId,要渲染成“楼中楼”效果,也得靠它!
🎯 加分项:边界处理 & 健壮性
真正的大厂代码,不会假设数据完美。你可以补充:
// 处理空输入
if (!list || !Array.isArray(list)) return [];
// 处理循环引用(可选:用 Set 记录访问路径)
// 处理 parentID 不存在的情况(跳过 or 报警)
甚至可以封装成通用工具函数,支持自定义字段名(比如 pid、parentId)。
🏁 总结:从小题看大局
| 方法 | 时间复杂度 | 可读性 | 性能 | 推荐场景 |
|---|---|---|---|---|
| 递归法 | O(n²) | ⭐⭐⭐⭐ | ❌ | 小数据、快速原型 |
| Map 优化法 | O(n) | ⭐⭐⭐ | ✅ | 生产环境首选 |
记住:面试不是秀代码,而是展示你的思考过程。
下次面试官再问这题,你不仅能写出两种解法,还能聊性能、谈场景、讲边界——offer 不就稳了?