前端高频面试手写题——扁平化数组转树
题目背景:
后端经常返回扁平列表,如部⻔、评论、菜单等,需要在前端转成树形结构渲染。
任务:
请写⼀个通⽤函数arrayToTree,把平铺数组转换成树。
输入:
type Item = { id: number; parentId: number | null; [key: string]: any
}
例如:
const list: Item[] = [{ id: 1, parentId: null, name: 'Root' },{ id: 2, parentId: 1, name: 'Child 1' },{ id: 3, parentId: 1, name: 'Child 2' },{ id: 4, parentId: 2, name: 'GrandChild 1' },
];
// 输出格式参考:
interface TreeNode extends Item {children: TreeNode[];
}// 签名函数:
export function arrayToTree<T extends { id: number;parentId: number | null }>(items: T[]
): (T & { children: T[] })[] { ... }// 示例调用
const tree = arrayToTree(list);
console.log(tree);
// =>
// [{
// id: 1,
// parentId: null,
// name: 'Root',
// children: [
// { id: 2, parentId: 1, name: 'Child 1', children: [...] },
// ...
初始定义:
type Item = {id: number;parentId: number | null;[key: string]: any
}
interface TreeNode extends Item {children: TreeNode[];
}
const list: Item[] = [{ id: 1, parentId: null, name: 'Root' },{ id: 2, parentId: 1, name: 'Child 1' },{ id: 3, parentId: 1, name: 'Child 2' },{ id: 4, parentId: 2, name: 'GrandChild 1' },
];
方法1(暴力):
分析数组中的信息:
- item:本身的id,父节点id,自身name
- 根节点父节点为null
- 父节点id值一致的item,属于同一个父节点,互为兄弟节点,在同一级
原始的list像是一条一条的记录,转换成树结构后,变成像是竖着的一级一级的思维导图式的形式
如何转换?
遍历list
如何处理list中的item?
- 将item生成一个TreeNode节点
- 通过第一个节点的parentId ,找到对应的item
- 将这些item转化成TreeNode节点,添加到第一个节点中
- 重复此操作,依此处理第一个节点的children数组
- 记录对应的parentId,遍历list,将item添加到对应节点的children数组中,
const arr: TreeNode[] = [];
arr.push({...list[0],children: []
})
function arrayToTree(arr){for(let i = 0; i < arr.length; i++){let id = arr[i].id;for(let j = 0; j < list.length ; j++){if(list[j].parentId === id){arr[i].children.push({...list[j],children: []});}}arrayToTree(arr[i].children);}
}
// 使用
arrayToTree(arr);
缺陷
- 这个方法手动指定了一个根节点,如果有多个根节点,这个写法就不适配了,因此我们需要自动将根节点加入进去
function initArr() {list.filter(item => item.parentId === null).map(item => {arr.push({...item,children: []})})
}
initArr();
- 虽然好理解,但是效率实在是太低,退退退🤺,out。
方法1的简化版:
function arrayToTree(list, parentId = null) {return list.filter(item => item.parentId === parentId).map(item => ({...item,children: arrayToTree(list, item.id)}));
}
arrayToTree(list, null);
思路:
- 先将parentId一致的节点过滤,再依此处理符合条件的item,将item转换成TreeNode,通过return的方式添加到父节点中
- 递归处理每个节点
缺陷:
- 虽然写法简单,通俗易懂,但效率还是太低了,时间复杂度 O(n²),数据一大就不行了, out。
方法2(map)
- 将list中所有的item,都转换为TreeNode节点并存在map中
- 遍历list,处理每个item:
- 遇到根节点(parentId === null),加入roots中
- 遇到其他节点,通过map找到该item的parentId对应的映射(父节点),将该item加入到该父节点的chrilren中
function arrayToTree(list) {const map = new Map();const roots = [];// 初始化 maplist.forEach(item => {map.set(item.id, { ...item, children: [] });});// 构建父子关系list.forEach(item => {const node = map.get(item.id);if (item.parentId === null) {roots.push(node);} else {const parent = map.get(item.parentId);if (parent) {parent.children.push(node);}}});return roots;
}
arrayToTree(list);
- 时间复杂度O(n), 推荐。
终极版(ts泛型 + 类型推导):
这个版本我们加上了ts泛型,进行了类型约束
首先我们需要明白类型约束的含义:
export function arrayToTree<T extends { id: number;parentId: number | null }>(items: T[]
): (T & { children: T[] })[] { ... }
// 解释:
// arrayToTree<泛型约束>(参数):(返回值){}
// <T extends { id: number; parentId: number | null }>: T为泛型约束,T中必须包含两个字段:id和parentId
// (items: T[]): 函数参数items, 类型是一个T类型的数组
// :(T & { children: T[] })[] : 函数返回值,返回值是一个数组,数组中的元素必须包含:
// 1. T中的id和parentId
// 2. 新增了children属性,children属性是T类型的数组
完整代码:
function arrayToTree<T extends {id: number;parentId: number | null
}>(items: T[]
): (T & { children: T[] })[] {const map = new Map();const result: (T & { children: T[]})[] = [];// 初始化maplist.forEach(item => {map.set(item.parentId, { ...item, children: [] });})// 转化list.forEach(item => {const node = map.get(item.id);if (item.parentId === null) {result.push(node)} else {const parent = map.get(item.parentId);if (parent) {parent.children.push(node);}}})return result;
}arrayToTree(list);
它和方法2的思路一致,都是使用map的方式,先建立id映射,在遍历list,找到对应的映射,进行处理,perfect!
