问题关键点
在需要存储树结构的情况下,一般由于使用的关系型数据库(如 MySQL),是以类似表格的扁平化方式存储数据。因此不会直接将树结构存储在数据库中,通常是通过邻接表、路径枚举、嵌套集或闭包表来存储。
扁平列表通常包含id和parentId字段,通过parentId建立层级关系。
使用递归或循环方法构建树结构。
其中,邻接表是最常用的方案之一,其存储模型如下:
| id |
pid |
data |
| 1 |
0 |
a |
| 2 |
1 |
b |
| 3 |
1 |
c |
该模型代表了如下的树状结构:
1 2 3 4 5 6 7 8 9
| { id: 1, pid: 0, data: 'a', children: [ {id: 2, pid: 1, data: 'b'}, {id: 3, pid: 1, data: 'c'}, ] }
|
大部分情况下,会交给应用程序来构造树结构。
典型题目
1 2 3 4 5 6 7 8
| const list = [ { pid: null, id: 1, data: "1" }, { pid: 1, id: 2, data: "2-1" }, { pid: 1, id: 3, data: "2-2" }, { pid: 2, id: 4, data: "3-1" }, { pid: 3, id: 5, data: "3-2" }, { pid: 4, id: 6, data: "4-1" }, ];
|
M1
递归解法:该方法简单易懂,从根节点出发,每一轮迭代找到 pid 为当前节点 id 的节点,作为当前节点的 children,递归进行。
1 2 3 4 5 6 7 8 9 10 11 12
| function listToTree(list,pid=null,{idName="id", pidName = "pid", childName = "children"} = {}){ return list.reduce((root, item) => { if(item[pidName] === pid){ const children = listToTree(list, item[idName]); if(children.length){ item[childName] = children; } return [...root,item]; } return root },[]) }
|
时间复杂度分析:最坏的情况下,这棵树退化为链表,且倒序排列。每一轮迭代需要在最后面才找到目标节点。假设有n个元素,那么总迭代次数为 n+(n-1) + (n-2) + … + 1,时间复杂度为 O(n^2)。
M2
迭代法:利用对象在 js 中是引用类型的原理。第一轮遍历将所有的项,将项的 id 与项自身在字典中建立映射,为后面的立即访问做好准备。 由于操作的每一项都是对象,结果集 root 中的每一项和字典中相同 id 对应的项实际上指向的是同一块数据。后续的遍历中,直接对字典进行操作,操作同时会反应到 root 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function listToTree(list, rootId=null,{idNmae="id", pidName="pid",childName="children"}={}){ const record = {} const root = []
list.forEach((item) => { record[item[idName]] = item; item[childName] = []; });
list.forEach((item) => { if(item[pidName] == rootId){ root.push(item) } else { record[item[pidName]][childName].push(item) } })
return root; }
|
时间复杂度分析:经历了两轮迭代,假设有 n 个元素,那么总迭代次数为 n + n,时间复杂度为 O(n)。
M2(1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
| function listToTree(list, rootId=null,{idName="id", pidName="pid", childName="children"} ={}){ const record = {}; const root = []
list.forEach((item) => { const id = item[idName] const parentId = item[pidName];
record[id] = !record[id] ? item : {...item, ...record[id]}; const treeItem = record[id] if(parentId === rootId){ root.push(treeItem) } else { if(!record[parentId]){ record[parentId] = {} } if(!record[parentId][childName]){ record[parentId][childName] = []; }
record[parentId][childName].push(treeItem); } }) return root; }
function listToTree1(list){ const map = {}; const rooots = []
list.forEach((item) => { map[item.id] = {...item, children: []}; })
list.forEach((item) => { if(item.parentId === null){ roots.push(map[item.id]) } else { const parent = map[item.parentId] if(parent){ parent.children.push(map[item.id]) } } })
return roots; }
function listToTreeOptimized(list, {idNmae="id", pidName="pid", childName="children"} = {}){ const map = new Map(); cosnt roots = [];
list.forEach((item) => { map.set(item[idName], {...item, [childName] : []}) })
list.forEach((item) => { const node = map.get(item[idName]) if(item[pidName] == null){ roots.push(node) } else { const parent = map.get(item[pidName]) if(parent){ parent[childName].push(node) } } }) return roots }
function renderTree(nodes, container, level = 0) { nodes.forEach(node => { const nodeElement = document.createElement('div'); nodeElement.className = 'tree-node'; const contentElement = document.createElement('div'); contentElement.className = 'node-content'; contentElement.textContent = `${node.name} (ID: ${node.id})`; nodeElement.appendChild(contentElement); container.appendChild(nodeElement); if (node.children && node.children.length > 0) { renderTree(node.children, nodeElement, level + 1); } }); }
|
时间复杂度分析:经历了一轮迭代,假设有 n 个元素,那么时间复杂度为 O(n)。
M2(2)
record 字典仅记录 id 与 children 的映射关系,代码更精简:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function listToTree(list, rootId=null, {idName="id",pidName="pid",childName="children"} = {}){ const record = {}; const root = [];
list.forEach((item) => { const newItem = Object.assign({}, item); cosnt id = newItem[idName]; const parentId = newItem[pidName];
newItem[childName] = record[id] ? record[id] : (record[id]=[])
if(parentId === rootId){ root.push(newItem); }else { if(!record[parentId]){ record[parentId] = [] } record[parentId].push(newItem) } }) return root }
|
- 时间复杂度分析:经历了一轮迭代,假设有 n 个元素,那么时间复杂度为 O(n)。
- 递归法:在数据量增大的时候,性能会急剧下降。好处是可以在构建树的过程中,给节点添加层级信息。
- 迭代法:速度快。但如果想要不影响源数据,需要在 record 中存储一份复制的数据,且无法在构建的过程中得知节点的层级信息,需要构建完后再次深度优先遍历获取。
- 迭代法变体一:按需创建 children,可以避免空的 children 列表。
- 小数据量 (<1000条):使用您的递归实现,代码更简洁
- 大数据量或性能敏感:使用Map优化版本
- 需要处理循环引用:添加循环检测逻辑
- 需要保持原数据不变:使用深拷贝
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
|
function deepClone(obj) { if (obj === null || typeof obj !== 'object') return obj; if (obj instanceof Date) return new Date(obj); if (obj instanceof Array) return obj.map(item => deepClone(item)); const cloned = {}; for (let key in obj) { if (obj.hasOwnProperty(key)) { cloned[key] = deepClone(obj[key]); } } return cloned; }
function listToTreeWithDeepClone( list, { idName = "id", pidName = "pid", childName = "children" } = {} ) { const clonedList = deepClone(list); const map = new Map(); const roots = []; clonedList.forEach(item => { map.set(item[idName], { ...item, [childName]: [] }); }); clonedList.forEach(item => { const node = map.get(item[idName]); if (item[pidName] === null) { roots.push(node); } else { const parent = map.get(item[pidName]); if (parent) { parent[childName].push(node); } } }); return roots; }
|
参考资料:
- 剑指前端offer