【经典手写题】数组转树的三种解法

大家好,我是雷布斯,今天来给大家讲解一道很基础但是又很常见的手写题:实现数组转成树形结构。

假设我们有这样的原始数据:

[
  { "id"12"parent_id"1"name""朝阳区" },
  { "id"241"parent_id"24"name""田林街道" },
  { "id"31"parent_id"3"name""广州市" },
  { "id"13"parent_id"1"name""昌平区" },
  { "id"2421"parent_id"242"name""上海科技绿洲" },
  { "id"21"parent_id"2"name""静安区" },
  { "id"242"parent_id"24"name""漕河泾街道" },
  { "id"22"parent_id"2"name""黄浦区" },
  { "id"11"parent_id"1"name""顺义区" },
  { "id"2"parent_id"0"name""上海市" },
  { "id"24"parent_id"2"name""徐汇区" },
  { "id"1"parent_id"0"name""北京市" },
  { "id"2422"parent_id"242"name""漕河泾开发区" },
  { "id"32"parent_id"3"name""深圳市" },
  { "id"33"parent_id"3"name""东莞市" },
  { "id"3"parent_id"0"name""广东省" }
]

需要根据每一项的 parent_idid,将上面的数组转成树形结构,以上面的原始数据为例,转换后的数据是:

[{
 "id"2,
 "parent_id"0,
 "name""上海市",
 "children": [{
  "id"21,
  "parent_id"2,
  "name""静安区",
  "children": []
 }, {
  "id"22,
  "parent_id"2,
  "name""黄浦区",
  "children": []
 }, {
  "id"24,
  "parent_id"2,
  "name""徐汇区",
  "children": [{
   "id"241,
   "parent_id"24,
   "name""田林街道",
   "children": []
  },
        ...
  }]
 }]
}, ...]

由于篇幅有限,上面的转换后数据有部分删减。

这种数据和嵌套对象相互转换的题目,在手写题环节很常见,今天我们就来介绍几种解法。

方法一

很容易想到的一个方法就是利用递归:每次遍历时,找到将本次遍历的根节点作为父节点的所有子节点,直至找不到有子节点的。

/**
 * 数组转树形结构
 * @param {array} list 被转换的数组
 * @param {number|string} root 根节点(最外层节点)
 * @returns array
 */

function arrayToTree(list, root{
  return list
    .filter(item => item.parent_id === root)
    .map(item => ({ ...item, children: arrayToTree(list, item.id) }))
}

代码很简洁,filtermap 方法也是数组中很常见的方法,相信大家也很好理解。

方法二

可以利用浅拷贝是拷贝对象的内存地址的特性,我们修改拷贝后,所有引用都会同步修改。利用这个特点,我们将子节点依次放入父节点,最后将最外层父节点返回即可。

/**
 * 数组转树形结构
 * @param {array} list 被转换的数组
 * @param {number|string} root 根节点(最外层节点)的 id
 * @return array
 */

function arrayToTree(list, root{
  const result = [] // 用于存放结果
  const map = {} // 用于存放 list 下的节点

  // 1. 遍历 list,将 list 下的所有节点以 id 作为索引存入 map
  for (const item of list) {
    map[item.id] = { ...item } // 浅拷贝
  }

  // 2. 再次遍历,将根节点放入最外层,子节点放入父节点
  for (const item of list) {
    // 3. 获取节点的 id 和 父 id
    const { id, parent_id } = item // ES6 解构赋值
    // 4. 如果是根节点,存入 result
    if (item.parent_id === root) {
      result.push(map[id])
    } else {
      // 5. 反之,存入到父节点
      map[parent_id].children
        ? map[parent_id].children.push(map[id])
        : (map[parent_id].children = [map[id]])
    }
  }

  // 将结果返回
  return result
}

代码中有注释,相信大家多读几遍也能理解。

方法三

针对方法二,我们也可以有些优化,只需要一次遍历,就能获取到结果

/**
 * 数组转树形结构
 * @param {array} list 被转换的数组
 * @param {number|string} root 根节点(最外层节点)
 * @returns array
 */

function arrayToTree(list, root{
  const result = [] // 用于存放结果
  const map = {} // 用于存放 list 下的节点

  // 遍历 list
  for (const item of list) {
    // 1. 获取节点的 id 和 父 id
    const { id, parent_id } = item // ES6 解构赋值

    // 2. 将节点存入 map
    if (!map[id]) map[id] = {}

    // 3. 根据 id,将节点与之前存入的子节点合并
    map[id] = map[id].children
      ? { ...item, children: map[id].children }
      : { ...item }

    // 4. 如果是根节点,存入 result
    if (parent_id === root) {
      result.push(map[id])
    } else {
      // 5. 反之,存入父节点
      if (!map[parent_id]) map[parent_id] = {}
      if (!map[parent_id].children) map[parent_id].children = []
      map[parent_id].children.push(map[id])
    }
  }

  // 将结果返回
  return result
}

最后

相信大家在看完解答后,会觉得这道题难度不高,如果自己遇到类似的题目肯定能写出来。

但从我最近作为面试官的角度来看,90% 的同学无法写出正确的答案,其中的原因也值得大家思考。

顺便也给我们的交流群和辅导服务打个广告: