树结构、扁平化数组相互转换

12,908 阅读3分钟

前段时间看到了一篇文章,面试了十几个高级前端,竟然连(扁平数据结构转Tree)都写不出来,提到了根据数组转化为树结构,这种需求确实在开发中也有遇到,尤其是一些树图一类的处理。于是思考并整理了几个相关的算法,包括常见的递归和效率高一点的循环。

测试数据

const arr = [
  {id: 1, name: '部门1', pid: 0},
  {id: 2, name: '部门2', pid: 1},
  {id: 3, name: '部门3', pid: 1},
  {id: 4, name: '部门4', pid: 3},
  {id: 5, name: '部门5', pid: 4},
]

const obj = {
  id: 1,
  name: "部门1",
  pid: 0,
  children: [
    {
      id: 2,
      name: "部门2",
      pid: 1,
      children: []
    },
    {
      id: 3,
      name: "部门3",
      pid: 1,
      children: [
        {
          id: 4,
          name: "部门4",
          pid: 3,
          children: [
            {
              id: 5,
              name: "部门5",
              pid:4,
              children: []
            }
          ]
        }
      ]
    }
  ]
}

tips:以下算法均以上面的数据结构实现,主要体现思路,注意注释

array to tree 【生成树结构】

递归

定义方法获取指定 id 的所有 children,每个 children 递归调用该方法获取 children.

/**
 * @param {arr: array 原数组数组, id: number 父节点id} 
 * @return {children: array 子数组}
 */
function getChildren(arr, id) {
  const res = [];
  for (const item of arr) {
    if (item.pid === id) { // 找到当前id的子元素
      // 插入子元素,每个子元素的children通过回调生成
      res.push({...item, children: getChildren(arr, item.id)});
    }
  }
  return res;
}

getChildren(arr, 0);

定义方法获取指定 idnodenode.children 的每个元素 调用该方法获取新的子元素

/**
 * @param {arr: array 原数组数组, id: number 当前元素id} 
 * @return {children: array 子数组}
 */
function getNode(arr, id){
  const node = arr.find(i => i.id === id); // 找到当前元素
  node.children = [];
  for(const item of arr) {
    if (item.pid === id) {
      // 给当前元素添加子节点,每个子节点递归调用
      node.children.push(getNode(arr, item.id));
    }
  }
  return node;
}

getChildren(arr, 1);

循环

利用 map 存储数组数据,并且每次循环处理对应的元素,利用指针指向内存空间的原理

/**
 * @param {arr: array, pid: number}
 * @return {obj: object}
 */
function fn(arr, pid) {
  const map = new Map(); // 生成map存储元素
  for(const item of arr) {
    if (!map.has(item.id)) { // 若map中没有当前元素,添加并初始化children
      map.set(item.id, {...item, children: []});
    } else {
      // 感谢 @[八股文吟诵大师](https://juejin.cn/user/1750078243741591) 指出问题;
      // 若不存在父元素 pid,后续会构造一个空的父元素,就会出现当前元素 item 已经存在的情况;此时需要将当前元素与已存在的元素(创建的父元素)进行合并
      map.set(item.id, {...map.get(item.id), ...item});
    }
    if (map.has(item.pid)){ // 查找父元素,存在则将该元素插入到children
      map.get(item.pid).children.push(map.get(item.id));
    } else { // 否则初始化父元素,并插入children
      map.set(item.pid, {children: [map.get(item.id)]});
    }
  }
  return map.get(pid);
}

fn(arr, 0);

tree to array 【扁平化数据】

递归

/**
 * @param {obj: object, res: array}
 * @return {arr: array}
 */
function fn(obj, res = []) { // 默认初始结果数组为[]
  res.push(obj); // 当前元素入栈
  // 若元素包含children,则遍历children并递归调用使每一个子元素入栈
  if (obj.children && obj.children.length) {
    for(const item of obj.children) {
      fn(item, res);
    }
  }
  return res;
}

fn(arr);
/**
 * @param { arr: array } 接收子数组,调用时可以构造 `{children: [...]}` 开始调用
 * @return { arr: array }
 */
function fn(arr) {
  const res = [];
  res.push(...arr); // chilren插入结果数组
  for(const item of arr) { // 遍历子元素,若包含children则递归调用
    if (item.children && item.children.length) {
      res.push(...fn(item.children))
    }
  }
  return res;
}

fn([children: [obj]]);

循环

基本大部分递归都可以用栈的思想改为循环,将每次需要处理的元素压入栈中,处理后弹出,至栈为空

/**
 * @param {obj: object} 
 * @return {arr: array}
 */
function fn(obj) {
  const stack = [];         // 声明栈,用来存储待处理元素
  const res = [];           // 接收结果
  stack.push(obj);          // 将初始元素压入栈
  while(stack.length) {     // 栈不为空则循环执行
    const item = stack[0];  // 取出栈顶元素
    res.push(item);         // 元素本身压入结果数组
    stack.shift();          // 将当前元素弹出栈
    // 逻辑处理,如果当前元素包含子元素,则将子元素压入栈
    if (item.children && item.children.length) {
      stack.push(...item.children);
    }
  }
  return res;
}

fn(obj);

参考文献

面试了十几个高级前端,竟然连(扁平数据结构转Tree)都写不出来