列表转树结构:从扁平列表到层级森林

列表转树结构:从扁平列表到层级森林的魔法变身🔄

一、题目详细描述:扁平列表的 "树形梦想"

想象一下,你手里有一串平平无奇的列表数据,每个元素都带着idparentId------ 就像一群知道自己 "爸爸是谁" 的小精灵。比如这样:

javascript

复制代码
const list = [
  { id: 1, parentId: 0, name: 'A' }, // 老大,没有爸爸(parentId=0)
  { id: 2, parentId: 1, name: 'B' }, // 老二,爸爸是1号
  { id: 3, parentId: 1, name: 'C' }, // 老三,爸爸也是1号
  { id: 4, parentId: 2, name: 'D' }  // 老四,爸爸是2号
];

而你的任务,是把它们变成一棵枝繁叶茂的 "树"🌳,让每个节点都带着自己的 "孩子":

javascript

复制代码
[
  {
    id: 1,
    parentId: 0,
    name: 'A',
    children: [ // A的孩子
      {
        id: 2,
        parentId: 1,
        name: 'B',
        children: [ { id: 4, parentId: 2, name: 'D' } ] // B的孩子
      },
      { id: 3, parentId: 1, name: 'C' } // A的另一个孩子
    ]
  }
]

简单说,就是让扁平的 "线性关系" 变成有层级的 "父子关系",这就是列表转树结构的核心任务!

二、面试官想考这些知识点

1.树 🌳:数据结构的 "家族图谱"

树是一种经典的层级结构,有几个关键概念和这道题死死绑定:

  • 根节点 :没有爸爸的节点(parentId=0),是树的 "老祖宗";
  • 子节点:被其他节点包含的节点,比如上例中 B 是 A 的子节点;
  • 层级关系 :通过parentIdid关联,形成 "爷爷→爸爸→儿子" 的链条;
  • children 属性:树结构的标志,每个节点用它收纳自己的 "后代"。

面试官就是想看看你对 "如何用代码表达层级关系" 的理解 ------ 毕竟树结构在前端太常见了!

2.递归 🔄:树结构的 "天然搭档"

一提到树,递归简直是 "条件反射" 般的存在。递归的核心是 "自己调用自己",刚好匹配树的 "每个节点的子节点也是一棵树" 的特性:

  • 递归公式 :要构建一个节点的子树,只需找到所有parentId等于该节点id的元素,再为这些元素递归构建它们的子树;
  • 退出条件 :当某个节点没有子节点时(找不到parentId等于它id的元素),递归就可以 "刹车" 了。

说白了,递归就是让每个节点自己搞定 "找孩子" 的工作,非常省心~

三、解法一:递归法 ------ 简单直接的 "笨办法"

1.两层循环:暴力但有效

递归法的核心思路是 "先找根,再找子,子再找孙"。外层循环找当前层级的节点,内层循环帮这些节点找 "孩子",找不到就收手。

代码实现

javascript

复制代码
// 列表转树的递归函数
// list:原始扁平列表;parentId:当前要找的父节点ID(默认0,即根节点)
function list2tree(list, parentId = 0) {
  const result = []; // 存储当前层级的节点

  // 外层循环:遍历所有节点,找"爸爸"是parentId的节点
  list.forEach(item => { 
    // 如果当前节点的parentId等于目标parentId,说明它是当前层级的节点
    if (item.parentId === parentId) { 
      // 递归调用:帮这个节点找它的子节点(子节点的parentId等于当前节点的id)
      const children = list2tree(list, item.id); 
      // 如果有子节点,就给当前节点加个children属性存起来
      if (children.length) { 
        item.children = children; 
      }
      // 把处理好的节点放进结果数组
      result.push(item); 
    }
  });

  return result; // 返回当前层级的节点(可能带children)
}

2.ES6 语法优化:代码瘦身术✨

ES6 的filtermap简直是为这种场景量身定做的!用它们可以把循环和判断 "浓缩" 成更优雅的代码。

ES6 API 详解

  • filter:遍历数组,返回满足条件的元素组成的新数组(相当于 "筛选");
  • map:遍历数组,对每个元素做处理后返回新数组(相当于 "改造")。

优化代码

javascript

复制代码
function list2tree2(list, parentId = 0) {
  // 1. 先用filter筛选出当前parentId的直接子节点
  return list.filter(item => item.parentId === parentId)
    // 2. 用map给每个子节点"装孩子"
    .map(item => ({
      ...item, // 保留原始属性(id、parentId、name等)
      children: list2tree2(list, item.id) // 递归找子节点,挂到children上
    }));
}

3.时间复杂度:O (n²)------ 有点费时间的 "老实人"

为什么是 O (n²)?假设列表有 n 个节点,每个节点都要遍历一次列表找子节点(最坏情况下每个节点都要找 n 次),所以总操作次数是 n×n,即 O (n²)。

能不能优化?当然能!这种方法虽然简单,但数据量大的时候会很慢(比如有 10000 个节点,就要做 1 亿次操作)。这时候就得请出 "空间换时间" 的思路啦~

四、解法二:空间换时间 ------ 用 HashMap 加速⚡

1.用对象字面量代替 HashMap:给节点办 "身份证"

思路是先给每个节点 "拍个照" 存起来(存在对象里),需要找父节点时直接 "刷身份证" 调取,不用再遍历整个列表。

代码实现

javascript

复制代码
function listToTree(list) {
  const map = {}; // 用对象当"通讯录",key是节点id,value是带children的节点
  const result = []; // 最终的树结构

  // 第一步:给每个节点"办身份证",并初始化children
  list.forEach(item => {
    map[item.id] = {
      ...item, // 复制原始属性
      children: [] // 先给每个节点空的children数组
    };
  });

  // 第二步:给每个节点"找爸爸",挂到正确的位置
  list.forEach(item => {
    const node = map[item.id]; // 从通讯录里取出当前节点
    if (item.parentId === 0) { 
      // 如果是根节点,直接放进结果数组
      result.push(node); 
    } else {
      // 不是根节点?查通讯录找到爸爸,把自己放进爸爸的children里
      // 可选链?.避免爸爸不存在的情况(防止报错)
      map[item.parentId]?.children.push(node); 
    }
  });

  return result;
}

2.ES6 的 Map 结构:更专业的 "通讯录"

ES6 的Map是专门做键值对存储的,比普通对象更灵活(键可以是任意类型),用它来实现更规范。

代码实现

javascript

复制代码
function list2treeWithMap(list) {
  const nodeMap = new Map(); // 用Map当通讯录
  const tree = []; // 最终的树

  // 第一步:给每个节点办身份证(存在Map里)
  list.forEach(item => {
    nodeMap.set(item.id, { // 用id当key
      ...item,
      children: [] // 初始化children
    });
  });

  // 第二步:认亲,挂到爸爸名下
  list.forEach(item => {
    const node = nodeMap.get(item.id); // 从Map里取节点
    if (item.parentId === 0) {
      tree.push(node); // 根节点进结果
    } else {
      // 找爸爸,把自己加进children
      nodeMap.get(item.parentId)?.children.push(node);
    }
  });

  return tree;
}

3.时间复杂度:O (n)------ 飞一般的速度

为什么这么快?因为只遍历了两次列表:第一次存节点(O (n)),第二次挂节点(O (n)),每次操作(存、取、加 children)都是 O (1)。总操作次数是 2n,忽略常数后就是 O (n)。大数据量下,这可比递归法快太多了!

五、面试官会问什么? 🤔

1.实际开发中哪里会用到列表转树?

太多啦!比如省市区三级联动(数据库里省、市、区存在一张表,用parentId关联)、后台管理系统的树状菜单(菜单父子层级)、评论区的嵌套回复(评论和子评论)等。

复制代码
id parentId name
1  0        北京
2  1        东城区
3  1        朝阳区
...
12 0        江西
32 12       赣州
...

2.为什么列表要扁平化存储,而不是直接存成树?

扁平化列表(带parentId)在数据库中存储更方便,查询、增删节点时不用处理复杂的嵌套结构;需要展示层级关系时,再转成树结构就行("存的时候 flat ,用的时候 tree")。

3.递归法和 HashMap 法各有什么优缺点?

递归法代码简洁、容易理解,但数据量大时效率低(O (n²));HashMap 法效率高(O (n)),但需要额外空间存 map(空间复杂度 O (n))。实际开发中,数据量大选 HashMap,数据量小递归更直观。

六、结语:选对方法,事半功倍!

列表转树结构看似简单,却藏着数据结构(树)、算法思想(递归、空间换时间)和 JS API 的综合考察。递归法像 "笨鸟先飞",简单易懂但效率一般;HashMap 法则像 "聪明的懒汉",用空间换时间,适合大数据场景。

下次面试官再问这个问题,你可以先笑着说:"这题我会两种解法~" 😎 然后从递归讲到 Map 优化,再结合实际场景分析,保证让面试官眼前一亮!

相关推荐
代码游侠2 小时前
复习——线程(pthread)
linux·运维·开发语言·网络·学习·算法
小oo呆2 小时前
【自然语言处理与大模型】LangChainV1.0入门指南:核心组件Agent
前端·javascript·easyui
papaofdoudou2 小时前
基于QEMU 模拟intel-iommu的sva/svm demo环境搭建和验证
算法·机器学习·支持向量机
再__努力1点2 小时前
【78】HOG+SVM行人检测实践指南:从算法原理到python实现
开发语言·人工智能·python·算法·机器学习·支持向量机·计算机视觉
scx201310042 小时前
20251214 字典树总结
算法·字典树
leiming62 小时前
MobileNetV4 (MNv4)
开发语言·算法
BD_Marathon3 小时前
关于JS和TS选择的问题
开发语言·javascript·ecmascript
YGGP3 小时前
【Golang】LeetCode 136. 只出现一次的数字
算法·leetcode
Hao_Harrision3 小时前
50天50个小项目 (React19 + Tailwindcss V4) ✨ | DrawingApp(画板组件)
前端·react.js·typescript·tailwindcss·vite7