面试官问我:如何把扁平列表变成树?我反手写了四种解法,还顺手优化了性能!

46 阅读4分钟

“前端不就是切图仔吗?怎么还要懂数据结构?”
——直到你遇到这道大厂高频面试题。


🌳 一道题,看穿你的“内功”

假如面试官只问了一个问题:给你一个扁平数组,如何转成树形结构? 我当场写了两种方法,还分析了时间复杂度,面试官直接点头说‘可以了’。”

听起来简单?别急,这题背后藏着 递归思维、算法优化、工程实践 三大考点。今天我们就来拆解这道经典题,顺便看看它在真实项目中是怎么“活”起来的。


📋 题目描述

给定一个扁平列表,每个元素包含 idparentIDname,其中 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 优化?有点东西!”


🧠 面试官真正想考什么?

别以为这只是个“写代码”题,其实他在考察:

  1. 数据结构敏感度
    能否从 parentID 联想到树?能否识别出“层级关系”?
  2. 算法思维
    是否意识到 O(n²) 的问题?有没有优化意识?
  3. 工程落地能力
    这种结构在哪儿用?怎么处理异常?是否考虑过大数据量?

🌍 真实场景:这玩意儿真有用!

你以为这只是面试造火箭?错!它天天在你项目里跑:

场景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 报警)

甚至可以封装成通用工具函数,支持自定义字段名(比如 pidparentId)。


🏁 总结:从小题看大局

方法时间复杂度可读性性能推荐场景
递归法O(n²)⭐⭐⭐⭐小数据、快速原型
Map 优化法O(n)⭐⭐⭐生产环境首选

记住:面试不是秀代码,而是展示你的思考过程。

下次面试官再问这题,你不仅能写出两种解法,还能聊性能、谈场景、讲边界——offer 不就稳了?