使用场景举例
在web开发过程中,菜单数据的树形结构是必不可少的,如果后端开发人员只是将相关的菜单数据查询出来而未做转换就需要自行转换。
需转化数组示例:
// 数据项中,parentId指当前菜单的父菜单项的id。
const data = [
{ id: 1, title: '系统管理', parentId: 0 },
{ id: 2, title: '用户管理', parentId: 1 },
{ id: 3, title: '角色管理', parentId: 1 },
{ id: 4, title: '菜单管理', parentId: 1 },
{ id: 5, title: '字典管理', parentId: 1 },
{ id: 6, title: '编码规则管理', parentId: 1 },
{ id: 7, title: '个人中心', parentId: 0 },
{ id: 8, title: '个人资料', parentId: 7 },
{ id: 9, title: '我的消息', parentId: 7 },
{ id: 10, title: '菜单1', parentId: 0 },
{ id: 11, title: '菜单1-1', parentId: 10 },
{ id: 12, title: '菜单1-2', parentId: 10 },
{ id: 13, title: '菜单1-2-1', parentId: 12 },
{ id: 14, title: '菜单1-2-2', parentId: 12 },
{ id: 15, title: '菜单1-2-3', parentId: 12 }
]
转换结果:
[
{
"id": 1,
"title": "系统管理",
"parentId": 0,
"children": [
{
"id": 2,
"title": "用户管理",
"parentId": 1,
"children": []
},
{
"id": 3,
"title": "角色管理",
"parentId": 1,
"children": []
},
{
"id": 4,
"title": "菜单管理",
"parentId": 1,
"children": []
},
{
"id": 5,
"title": "字典管理",
"parentId": 1,
"children": []
},
{
"id": 6,
"title": "编码规则管理",
"parentId": 1,
"children": []
}
]
},
{
"id": 7,
"title": "个人中心",
"parentId": 0,
"children": [
{
"id": 8,
"title": "个人资料",
"parentId": 7,
"children": []
},
{
"id": 9,
"title": "我的消息",
"parentId": 7,
"children": []
}
]
},
{
"id": 10,
"title": "菜单1",
"parentId": 0,
"children": [
{
"id": 11,
"title": "菜单1-1",
"parentId": 10,
"children": []
},
{
"id": 12,
"title": "菜单1-2",
"parentId": 10,
"children": [
{
"id": 13,
"title": "菜单1-2-1",
"parentId": 12,
"children": []
},
{
"id": 14,
"title": "菜单1-2-2",
"parentId": 12,
"children": []
},
{
"id": 15,
"title": "菜单1-2-3",
"parentId": 12,
"children": []
}
]
}
]
}
]
实现思路
思路1:递归方式
- 首先定义一个 children 数组,用于存储所有与指定父节点 ID 匹配的子节点;
- 然后遍历整个数组,查找与指定父节点 ID 匹配的所有子节点;
- 对于每个匹配的子节点,递归调用 toTree 函数,传递当前子节点的 ID 作为新的父节点 ID,以获取当前子节点的所有子节点;
- 最后将所有子节点添加到 children 数组中,并返回结果数组。
function toTree(arr, parentId) {
if (!arr.length) {
return [];
}
// 定义一个 children 数组
const children = [];
for (const node of arr) {
if (node.parentId === parentId) {
const child = { ...node };
// 对于每个匹配的子节点,递归调用 toTree 函数,传递当前子节点的 ID 作为新的父节点 ID,以获取当前子节点的所有子节点。
child.children = toTree(arr, node.id);
children.push(child);
}
}
return children;
}
// 测试代码
const tree = toTree(data, 0);
console.log(JSON.stringify(tree));
思路2:非递归方式
- 首先创建一个空数组 tree 用于存储树形结构,同时创建一个 Map 对象 map 用于存储每个节点的子节点信息;
- 遍历数组中的每个节点,将其作为 map 中的一个数据项存储。此时每个节点的 children 属性被初始化为空数组;
- 再次遍历 map 中的每个节点。对于每个节点,获取其父节点 ID 并从 map 中查找父节点。如果找到父节点,则将该节点添加为父节点的子节点;否则将该节点添加到根节点数组 tree 中;
- 完成所有节点的处理后,返回 tree 数组作为结果。
function toTree(arr) {
const tree = [];
const map = new Map();
arr.forEach(node => {
map.set(node.id, { ...node, children: [] });
});
map.forEach((node, _, map) => {
const parentId = node.parentId;
const parent = map.get(parentId);
if (parent) {
parent.children.push(node);
} else {
tree.push(node);
}
});
return tree;
}
// 测试代码
const tree = toTree(data);
console.log(JSON.stringify(tree));
时间复杂度和空间复杂度分析
递归方式
时间复杂度:
递归方式的时间复杂度为 O(n2) ,其中 n 是一维数组中元素的个数。对于每个节点,递归函数需要遍历整个数组以查找其子节点。在最坏情况下,即每个节点都没有子节点,递归函数需要遍历整个数组 n 次,因此时间复杂度为 O(n2)。
空间复杂度:
递归方式的空间复杂度取决于递归深度,即树的深度。在最坏情况下,即树的深度为 n,递归函数将被调用 n 次,空间复杂度为 O(n)。
非递归方式
时间复杂度:
非递归方式的时间复杂度为 O(n),其中 n 是一维数组中元素的个数。非递归方式只需要遍历一遍一维数组即可将其转换为树形结构,因此时间复杂度为 O(n)。
空间复杂度:
在上面的非递归方式示例中,使用了一个 Map 对象存储中间结果,因此空间复杂度为 O(n)。
如何选择
虽然递归方式实现简单,但时间复杂度和空间复杂度都较高,不适用于处理大规模的数据集。相比之下,非递归方式具有更好的性能和可扩展性,适合处理大规模的数据集。