leader:来,做道算法题:把这个数组转换成Tree。我直接用三种解法回答

707 阅读5分钟

在认真改bug的时候,微信咚咚一响,看到leader给我发了一条信息,点开一看

Snipaste_2024-08-19_15-49-46.png

当我看到这道题的时候,我突然想起来前一阵子和他聊过他当面试官都喜欢考察一些什么东西,他说考察基础比较多,比如把一个对象数组转成树结构。

好家伙,可能是突然想起了,这不就来了。

之前也写过一些转树形的代码,所以看到这个并不是很陌生。

我们来看回这道题,这个对象数组每个都有三个属性,分别是自己的id,名字name以及父级的id-pid

这种在真实的业务场景还是挺多的,比如说菜单的渲染和权限列表等。

在这里,我将尝试用三种解法来回答这个问题,这三种解法也是我思考和解答这道题的一个思路历程。

先放上例子和目标结果:

  • list

pid为0表示顶层父级对象

var list = [
  {
    id: 1,
    name: 'jack',
    pid: 0,
  },
  {
    id: 2,
    name: 'jack',
    pid: 1,
  },
  {
    id: 3,
    name: 'jack',
    pid: 1,
  },
  {
    id: 4,
    name: 'jack',
    pid: 2,
  },
];
  • 目标结果
[
  {
    "id": 1,
    "name": "jack",
    "pid": 0,
    "children": [
      {
        "id": 2,
        "name": "jack",
        "pid": 1,
        "children": [{ "id": 4, "name": "jack", "pid": 2, "children": [] }]
      },
      { "id": 3, "name": "jack", "pid": 1, "children": [] }
    ]
  }
]

借助Map

一开始想的当然是迭代的方法,想法很直接,就是遍历每个对象,然后去找他的父对象,如果能找到,则把他加到父对象的子数组中。

如果每次找父对象都去遍历一遍数组的话,算法的时间复杂度就会上去,这里可以借助一个映射map来记录每个对象,key是每个对象的id,value则是其本身。

在遍历之前,我们的准备工作就是把所有对象在map中做好映射。

list.forEach((item) => {
    map[item.id] = { ...item, children: [] };
});

在这里,给每个对象都加一个children属性。

然后就是遍历list中的每个对象了。

在遍历对象的时候,需要考虑两点:

  • 此对象是父级对象。如果是父级对象,则直接加入就好了。
  • 此对象不是父级父级对象。如果不是父级对象,那么它一定会有一个父级对象,这时候我们需要去找到这个父级对象。由于我们使用了map,所以我们很容易的就能够根据此对象的pid去找到其父级对对象。找到父级对象后,在其子对象数组中加入即可。

来看完整的代码:

const getTree = (list) => {
  let map = {};
  let res = [];
  // 映射对象
  list.forEach((item) => {
    map[item.id] = { ...item, children: [] };
  });
  
  // 遍历对象
  list.forEach((item) => {
    // 如果是父级对象,对应上述第一点
    if (item.pid === 0) {
      res.push(map[item.id]);
    } else {
      // 对应上述第二点,从映射中获取父级对象
      let parent = map[item.pid];
      if (parent) {
        parent.children.push(map[item.id]);
      }
    }
  });
  return res;
}

其实这里还藏着一个知识点,仔细看上面,我们实际上需要的是res数组,在一开始确定一个对象是父级对象的时候,就已经通过res.push(map[item.id])将其加入到res数组中。在后面其子对象寻找父级对象时,通过的时let parent = map[item.pid]获取父级对象,并且parent.children.push(map[item.id])这样子修改就能同步修改了res数组中的值。

说的很绕,但是仔细看完的同学或许已经知道答案了,这里其实是数组中存储的是这些对象的引用,在其他地方修改,所有引用的地方都会修改。

我做出来之后,立马发给了leader,很如愿得到赞赏

Snipaste_2024-08-19_15-51-25.png

但是他在后面加了一句:“我以为你会用递归”

是啊,我其实在做题的时候也想过了递归,树形构建其实也很适合用递归去实现,代码看起来非常优雅,但是时间复杂度没有上面的解法好。

舍弃map,拥抱递归

我们可以在上面的解法基础上实现递归,做法是舍弃mapmap是用来方便我们快速确定父级对象的,如果我们不用map,就要通过递归去找到父级对象。

我们需要一个函数去找到父级对象,具体是对已经加入到res数组的对象一一遍历,如果此对象的id等于子对象的pid,则说明找到,如果找不到,则需要去其子数组中去找,直到找到,如果找不到则说明null。

const findParent = (item, result) => {
  for (const r_item of result) {
    if (r_item.id === item.pid) {
      return r_item;
    }
    if (r_item.children) {
      const parent = findParent(item, r_item.children);
      if (parent) {
        return parent;
      }
    }
  }
  return null;
}

而主函数则是需要去遍历每个对象,然后做法跟第一种就类似了

const getTreeRecur = (list) => {
  let result = [];
  list.forEach((item) => {
    if (item.pid === 0) {
      result.push({ ...item, children: [] });
    } else {
      let parent = findParent(item, result);
      if (parent) {
        parent.children.push({ ...item, children: [] });
      }
    }
  });
  return result;
}

写完后我就发现问题了,这种做法其实是不太行的,我们传入findParent的是结果数组,如果某个父级对象还没有加入的话,而先去找其子对象,会发现找不到,这就要求我们的原始数组需要按序排列,这其实不符合真实的业务需求,可以说有部分测试案例会通过不了,所以这种做法仅供参考

正确的递归

最后,还是看一下做这道题递归的正确解法应该怎么做。

这里的递归可以理解为自顶向下,先确定一个父级对象,然后根据父级对象的id,去找所有其所有的子对象,即某个对象的pid满足pid===id,如此再通过子对象的id,去递归寻找子对象的子对象,直到遍历完成。

这其实是非常耗时的一种做法,但是理解起来并不困难。

实现代码如下:

const getTreeRecur2 = (root, result, pid) => {
    for(const item of root) {
      if(item.pid === pid) {
        let tmp = {...item, children: []};
        result.push(tmp);
        getTreeRecur(root, tmp.children, tmp.id);
      }
    }
}

到此,这道题就先做到这里了,继续改bug(摸鱼。