数组转树与树转数组

扁平数组转树形结构 (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)和算法复杂度,是写出高性能、强健壮性代码的关键。

相关推荐
剑锋所指,所向披靡!7 小时前
数据结构之线性表
数据结构·算法
蜡台7 小时前
element-ui 2 el-tree 内容超长滚动条不显示问题
前端·vue.js·elementui·el-tree·v-deep
哈里谢顿7 小时前
agnes0317面试总结
面试
哈里谢顿8 小时前
golang常见面试题总结
面试·go
小小小小宇8 小时前
软键盘常见问题(二)
前端
m0_672703318 小时前
上机练习第49天
数据结构·算法
样例过了就是过了9 小时前
LeetCode热题100 N 皇后
数据结构·c++·算法·leetcode·dfs·深度优先遍历
小小小小宇9 小时前
软键盘常见问题
前端
小小小小宇9 小时前
富文本编辑器知识体系(三)
前端
小小小小宇9 小时前
富文本编辑器知识体系(二)
前端