数组转树与树转数组

扁平数组转树形结构 (Array To Tree)

核心痛点

处理"数组转树"最直观的思路是使用递归配合双重循环:遍历数组中的每一项,再次遍历数组寻找其子节点。

这种做法的时间复杂度为

scss 复制代码
O(n2)O(n2)

。当数据量

复制代码
nn

较小时(如几十条菜单),性能损耗尚可忽略。但当数据量达到数千条(如组织架构、行政区划)时,计算次数呈指数级增长,会导致浏览器主线程阻塞,页面卡顿。

解决方案:Map 映射法

为了解决性能瓶颈,我们需要将时间复杂度降低至

scss 复制代码
O(n)O(n)

核心思路利用 JavaScript 对象是**引用类型(Reference Type)**的特性,结合 空间换时间 的策略:

  1. 第一次遍历:构建一个以 id 为键,节点对象为值的 Map(或对象字典)。这一步可以让我们在

    scss 复制代码
    O(1)O(1)

    的时间内获取任意节点的引用。

  2. 第二次遍历:通过 parentId 直接从 Map 中查找父节点引用,并将当前节点挂载到父节点的 children 属性下。

代码实现

JavaScript

ini 复制代码
/**
 * 扁平数组转树形结构 - O(n)
 * @param {Array} items 扁平数组
 * @param {Object} config 配置项,兼容不同的字段名
 * @returns {Array} 树形结构
 */
function arrayToTree(items, config = {}) {
  const { id = 'id', pid = 'parentId', children = 'children' } = config;
  
  const result = [];
  const itemMap = new Map();

  // 1. 初始化 Map,并处理数据深浅拷贝问题
  // 这一步确保了后续操作的是新对象,不污染原数据
  for (const item of items) {
    // 扩展运算符实现浅拷贝,初始化 children 数组
    itemMap.set(item[id], { ...item, [children]: [] });
  }

  // 2. 二次遍历,利用引用地址组装树
  for (const item of items) {
    const idValue = item[id];
    const pidValue = item[pid];
    const treeItem = itemMap.get(idValue);

    // 尝试获取父节点引用
    const parent = itemMap.get(pidValue);

    if (parent) {
      // 如果父节点存在,利用引用特性,直接 push 到父节点的 children 中
      // 此时 Map 中的父节点对象和 result 中的对象指向同一块内存地址
      parent[children].push(treeItem);
    } else {
      // 如果找不到父节点,说明是根节点(Root)
      // 注意:这里兼容了 parentId 为 null、0 或不存在的情况
      result.push(treeItem);
    }
  }

  return result;
}

深度解析

该算法仅对数组进行了两次遍历,总操作次数为

复制代码
2n2n

,因此时间复杂度稳定在

scss 复制代码
O(n)O(n)

关键在于 parent[children].push(treeItem) 这一步。在 JavaScript 堆内存中,treeItem 和 parent 都是独立的对象。当我们把 treeItem 放入 parent 的数组时,实际上是存储了 treeItem 的内存地址。

无论层级多深,我们都无需递归查找,因为 Map 充当了"索引表",让我们能直接定位到任何节点的内存地址并进行挂载。


树形结构转扁平数组 (Tree To Array)

核心场景

  • 全量搜索:在树中查找符合条件的节点。
  • 表格渲染:将树形数据展示为平铺的表格。
  • 数据清洗:去除树结构中的空 children 或无效字段。

解决方案:迭代法 (Iterative Approach)

虽然递归(Recursion)写法代码简洁,但在极端层级深度下(如深度超过 10000 层),递归调用栈(Call Stack)可能溢出。

为了代码的健壮性,推荐使用广度优先遍历 (BFS)深度优先遍历 (DFS) 的迭代写法。这里展示基于栈(Stack)的 DFS 写法,因为它逻辑清晰且性能优异。

代码实现

JavaScript

arduino 复制代码
/**
 * 树形结构转扁平数组 - 迭代法 (DFS)
 * @param {Array} tree 树形数据
 * @returns {Array} 扁平数组
 */
function treeToArray(tree) {
  const result = [];
  // 使用扩展运算符浅拷贝,避免修改原数组引用
  // stack 作为任务栈,模拟递归调用的过程
  const stack = [...tree];

  while (stack.length > 0) {
    // 弹出栈顶元素(后进先出,模拟深度优先)
    const node = stack.pop();
    
    // 解构分离 children 和其他属性
    // 目的:扁平化后的节点通常不需要 children 字段,且防止循环引用
    const { children, ...rest } = node;
    
    // 将处理后的纯净节点推入结果集
    result.push(rest);

    // 如果存在子节点,将它们推入栈中待后续处理
    if (children && children.length > 0) {
      // 注意:为了保证顺序(如果需要),可以反转 children 后入栈,或者使用 shift() 做 BFS
      // 这里使用 push + pop 实现 DFS
      stack.push(...children);
    }
  }

  return result;
}

注意点:Immutability (不可变性)

在上述代码中,const { children, ...rest } = node 是至关重要的一步。

  1. 断开引用:我们不希望扁平化后的对象依然保留庞大的 children 树状结构,这会造成不必要的内存占用。
  2. 数据安全:如果直接 delete node.children,会修改原始传入的树数据。在 Vue/React 等响应式框架中,这种副作用(Side Effect)会导致不可预知的视图更新异常。通过解构赋值产生新对象,保证了函数的纯度(Pure Function)。

总结

在前端工程化开发中,处理复杂数据结构是检验工程师基本功的试金石。

  1. 数组转树 :核心在于利用 Hash Map 建立索引,将

    scss 复制代码
    O(n2)O(n2)

    的嵌套查找优化为

    scss 复制代码
    O(n)O(n)

    的引用挂载。这是典型的"空间换时间"算法思想。

  2. 树转数组 :核心在于利用 栈(Stack)队列(Queue) 将递归逻辑转化为迭代循环,避免栈溢出风险,并严格遵守数据不可变原则。

理解内存引用(Memory Reference)和算法复杂度,是写出高性能、强健壮性代码的关键。

相关推荐
We་ct1 小时前
浏览器 Reflow(重排)与Repaint(重绘)全解析
前端·面试·edge·edge浏览器
笨笨狗吞噬者1 小时前
【uniapp】小程序端解决分包的uni_modules打包后产物进入主包中的问题
前端·微信小程序·uni-app
WebInfra1 小时前
Modern.js 3.0 发布:聚焦 Web 框架,拥抱生态发展
前端·javascript·前端框架
AngelPP1 小时前
OpenClaw Memory 模块完整分析
前端·aigc·ai编程
刘琦沛在进步1 小时前
【数据结构】学习数据结构的第一课——顺序表(静态)
数据结构·c++·学习
ID_180079054732 小时前
淘宝商品详情 API 接口 item_get: 高效获取商品数据的技术方案
java·前端·数据库
We་ct2 小时前
LeetCode 637. 二叉树的层平均值:BFS层序遍历实战解析
前端·数据结构·算法·leetcode·typescript·宽度优先
敲敲了个代码2 小时前
浏览器时间管理大师:深度拆解 5 大核心调度 API
前端·javascript·学习·web
ssshooter2 小时前
看完就懂 useLayoutEffect
前端·react.js·面试