如何处理数组扁平化,这确实是开发中可能经常遇到的问题,比如服务端给返会来一大坨数据是一个树状结构,当数组嵌套层次较深时,处理起来会比较复杂,例如遍历、搜索、过滤或排序等,扁平化数组可以使这些操作更加简单和直观。
flat
这里我们可以先提一下 js 中的 flat() 方法, 这个方法就是用来处理多维数组扁平化的方法。
flat() 方法创建一个新的数组,并根据指定深度递归地将所有子数组元素拼接到新的数组中。
看上面这些例子,就可以得出结论,他接受一个参数 depth (深度) 如果默认不传递,depth 的默认深度是1, 如果传递 0 那就是相当于没有做特殊的处理,只是仅仅将原本的数组浅拷贝了一下返回过来,如果传递 Infinity (无穷), 那不管是几维数组,也会通过处理后返回一个扁平后的一维数组。那么知道这些概念之后,还是去封装一下这个方法,这样才可以深刻理解扁平化的原理。
Array.prototype.myFlat = function (deep = 1) {
if (deep === Infinity) {
return this.reduce((pre, cur) => {
if (Array.isArray(cur)) {
console.log(cur);
return pre.concat(cur.myFlat(Infinity));
}
return pre.concat(cur);
}, []);
}
if (deep === 0) {
return [...this];
}
const result = [];
this.forEach(ele => {
if (Array.isArray(ele)) {
result.push(...ele);
} else {
result.push(ele);
}
});
return result.myFlat(deep - 1);
}
思路: 我们还是在 Array 原型上去创建 myFlat 方法,当我不传递的时候,deep 为 1,进行遍历,如果里面的每一项有数组的化打平,装进一个新数组,如果不是数组,直接装进新数组,最后返回的结果做一个递归。如果传递的是 0,那我们只是将数组进行一层浅拷贝。如果是传递 Infinity 这个属性,也会进行递归的操作。
当然,这只是我们剖析扁平化 flat() 这个数组方法的原理,在实际开发中一般不会有像 [1,[2,[3,[4,[5]....]]]] 这种类似的数据,更贴切的是树状的结构。
假设是这样的数据:
const tree = [{
id: '1',
children: [
{
id: '1-1',
},
{
id: '1-2',
asdf: '123',
children: [
{
id: '1-2-1'
}, {
id: '1-2-2',
children: [{
id: '1-2-2-1'
}]
}]
}
]
},
{
id: '2'
}
];
现在需要将这个数据做一个扁平化处理,期望的结果是
在操作完之后,我的数据是扁平的,增加了一个 pId 属性,这个 pId 是原本父节点的id,如果没有父节点的话为 null。
深度优先 DFS (Depth-First Search)
在操作之前我们需要先了解深度优先的概念,深度优先遍历,尽可能深的搜索树的分支。
序号代表节点遍历的顺序,由此可见深度优先其实就是从根节点开始遍历,中间只要碰到子节点的children,就会开始遍历子节点一直往里深入,直到没有子节点了才会继续顺着树往下遍历。
function flat(tree, pId = null) {
// 余项
return tree.reduce((pre, { children, ...cur }) => {
cur.pId = pId;
pre.push(cur);
if (children) {
pre.push(...flat(children, cur.id));
}
return pre;
}, []);
}
console.log(flat(tree))
思路: 我们可以用 reduce() 方法进行遍历,reduce 方法(另一篇文章js常用的遍历方法有讲到),参数 pre 是上一次的值,我预期是个数组,于是 initalValue 传递为数组,第二个参数用余项,拿到除了 children 以外的所有数据,接着将将 pId 这个属性添加到每一项,这时候的 pId 为 null,将每一项除了 children 这个属性都添加的新数组中,判断只要有 children 我们就进行递归,将这次的调用结果转为对象的形式装的数组里, 最终返回 pre。
广度优先 BFS (Breadth-First Search)
序号表示被搜索的顺序,先把同层的节点给遍历完,再去遍历子节点。
function flat(tree) {
const list = tree.map(cur => ({...cur, pId: null}));
const result = [];
while (list.length) {
const {children, ...cur} = list.shift();
result.push(cur);
if (children) {
list.push(...children.map(item => ({...item, pId: cur.id})));
}
}
return result;
}
思路: 我们现将每个节点都添加属性 pId,默认为空,循环 list 的长度,拿上面的数据为例,list 的长度是 2,满足循环条件开启循环,我们用结构的方式,将 list 的第一项删除的数据给 cur 这个变量,接着将删除的数据添加的创建的 result 这个新数组里面, 此时判断有没有 children 如果有 children 就将当前的 id 赋值给 pId,之后将数组打散成对象,装到 list 数组后面(为了便于接下来循环),这个时候 result 里面已经有我们的一项数据了,而 list 里面此时是三条数据, 以此类推,每一轮的循环开始,都将当前 list 的第一项元素放到 reuslt 里,然后遇到 children 就打散成对象装到 list 里继续循环,当 list 里面没有数据了,也代表我们已经处理完所有的数据,此时循环就停了,返回最终的 result。
我们可以看到,这两种方案虽然都是将树形结构扁平化,但是得到结果的顺序是不一样的。最终深度优先结果的顺序是 1 1-1 1-2 1-2-1 1-2-2... 而广度优先结果的顺序是 1 2 1-1 1-2 1-2-1 1-2-2.
如果只是因为最终处理完的结果不一样,而去考虑两者到底用哪种方案吗?
具体的话还是看场景和需求
深度优先优缺点
优点
- 能找出所有解决方案
- 优先搜索一棵子树,然后是另一棵,所以和广度对比,有着内存需要相对较少的优点
缺点
- 要多次遍历,搜索所有可能路径,标识做了之后还要取消。
- 在深度很大的情况下效率不高
广度优先优缺点
优点
- 对于解决最短或最少问题特别有效,而且寻找深度小
- 每个结点只访问一遍,结点总是以最短路径被访问,所以第二次路径确定不会比第一次短
缺点
- 内存耗费量大(需要开大量的数组单元用来存储状态)
小结
深度优先和广度优先并没有想象中的那么复杂,而且在平时项目中的应用非常广泛,因此需要我们重点掌握。