JavaScript中基于子ID引用从扁平数组构建依赖树并检测循环依赖
JavaScript中基于子ID引用从扁平数组构建依赖树并检测循环依赖
问题描述
我现在需要处理一个扁平的项目列表,每个项目包含id和dep字段(dep是当前项目的子依赖ID)。核心需求有三个:
- 为每个项目生成所有依赖的扁平去重列表
- 构建层级化的依赖树结构
- 检测并标记任意位置的循环依赖,还要明确指出循环发生的节点对
实际场景中每个ID可能对应多条记录(比如示例里的id=8),且数据集规模达到几十万条,需要保证处理效率。我自己尝试用DFS实现了部分逻辑,能生成依赖列表,但搞不定循环检测和树结构的构建,希望能得到帮助。
输入示例
const input = [ { id: '1' }, { id: '2' }, { id: '3' }, { id: '4', dep: '1' }, { id: '5', dep: '2' }, { id: '6', dep: '3' }, { id: '7', dep: '4' }, { id: '8', dep: '5' }, { id: '8', dep: '1' }, { id: '8', dep: '4' }, { id: '9', dep: '8' }, { id: '10', dep: '8' }, { id: '11', dep: '10' }, { id: '12', dep: '11' }, { id: '14', dep: '13' }, // 循环引用 { id: '13', dep: '17' }, // 循环引用 { id: '15', dep: '13' }, // 循环引用 { id: '16', dep: '13' }, // 循环引用 { id: '17', dep: '16' }, // 循环引用 { id: '18', dep: '13' }, // 循环引用 { id: '19', dep: '20' }, // 循环引用 { id: '20', dep: '14' }, // 循环引用 { id: '21', dep: '22' }, { id: '17', dep: '21' }, { id: '16', dep: '14' } ];
预期输出
预期输出需要包含每个项目的完整依赖信息:
- 无依赖的项目值为
null - 有依赖的项目包含:
allDeps:所有依赖的扁平去重数组tree:层级化的依赖树,循环节点标记为'circular'hasCircular:布尔值,标记是否存在循环依赖circularIds:循环路径的数组,记录发生循环的节点对
const output = { '1': null, '2': null, '3': null, '4': { allDeps: [ '1' ], tree: { '1': null }}, '5': { allDeps: [ '2' ], tree: { '2': null }}, '6': { allDeps: [ '3' ], tree: { '3': null }}, '7': { allDeps: [ '4', '1' ], tree: { '4': { '1': null }} }, '8': { allDeps: [ '5', '2', '1', '4' ], tree: { '5': { '2': null }, '4': { '1': null }, '1': null } }, '9': { allDeps: [ '8', '5', '2', '1', '4' ], tree: { '8': { '5': { '2': null }, '4': { '1': null }, '1': null }} }, '10': { allDeps: [ '8', '5', '2', '1', '4' ], tree: { '8': { '5': { '2': null }, '4': { '1': null }, '1': null }} }, '11': { allDeps: [ '10', '8', '5', '2', '1', '4' ], tree: { '10': { '8': { '5': { '2': null }, '4': { '1': null }, '1': null }}} }, '12': { allDeps: [ '11', '10', '8', '5', '2', '1', '4' ], tree: { '11': { '10': { '8': { '5': { '2': null }, '4': { '1': null }, '1': null }}}} }, '13': { allDeps: [ '17', '16', '13', '22', '21', '14' ], hasCircular: true, circularIds: [[ '13', '16' ], [ '13', '14' ]], tree: { '17': { '16': { '13': 'circular', '14': { '13': 'circular' } }, '21': { '22': null } } } }, '14': { allDeps: [ '13', '17', '16', '22', '21', '13', '14' ], hasCircular: true, circularIds: [[ '13', '16' ], [ '14', '16' ]], tree: { '13': { '17': { '16': { '13': 'circular', '14': 'circular' }, '21': { '22': null } } } } }, '15': { allDeps: [ '13', '17', '16', '22', '21', '13', '14' ], hasCircular: true, circularIds: [[ '13', '16' ], [ '13', '14' ]], tree: { '13': { '17': { '16': { '13': 'circular', '14': { '13': 'circular' } }, '21': { '22': null } } } } }, '16': { allDeps: [ '13', '17', '16', '14', '22', '21' ], hasCircular: true, circularIds: [[ '17', '16' ], [ '17', '16' ]], tree: { '13': { '17': { '16': 'circular', '21': { '22': null } } }, '14': { '13': { '17': { '16': 'circular', '21': { '22': null } }, '21': { '22': null } } } } }, '17': { allDeps: [ '16', '13', '17', '21', '22', '14' ], hasCircular: true, circularIds: [[ '13', '17' ], [ '13', '17' ]], tree: { '16': { '13': { '17': 'circular' }, '14': { '13': { '17': 'circular' } } }, '21': { '22': null } } }, '18': { allDeps: [ '13', '17', '16', '21', '22', '14' ], hasCircular: true, circularIds: [[ '13', '16' ], [ '14', '13' ]], tree: { '13': { '17': { '16': { '13': 'circular', '14': { '13': 'circular' } }, '21': { '22': null } } } } }, '19': { allDeps: [ '20', '14', '13', '17', '16', '22', '21' ], hasCircular: true, circularIds: [[ '13', '16' ], [ '14', '16' ]], tree: { '20': { '14': { '13': { '17': { '16': { '13': 'circular', '14': 'circular' }, '21': { '22': null } } } } } } }, '20': { allDeps: [ '14', '13', '17', '16', '21', '22' ], hasCircular: true, circularIds: [[ '13', '16' ], [ '16', '14' ]], tree: { '14': { '13': { '17': { '16': { '13': 'circular', '14': 'circular' }, '21': { '22': null } } } } } }, '21': { allDeps: [ '22' ], tree: { '22': null } }, '22': null };
现有代码
这是我目前实现的进度,能生成allDeps列表,但缺少循环检测和树结构的构建逻辑:
function getMap(fullList) { const portifolioMap = {}; const itemsWithDependencies = new Set(); // 优化:先处理无依赖的项 const portfolioCache = {}; // 缓存每个ID对应的所有记录,避免重复遍历 const emptyRow = { allDeps: new Set() }; // 获取所有唯一ID const uniqueIds = fullList.reduce((acc, item) => { acc.add(item.id); if(item.dep) { acc.add(item.dep); itemsWithDependencies.add(item.id); } if(!portfolioCache[item.id]) { portfolioCache[item.id] = []; } if(item.dep && !portfolioCache[item.dep]) { portfolioCache[item.dep] = []; } portfolioCache[item.id].push(item); return acc; }, new Set()); function visitNode(firstNodeId, node) { if(!node) return; if(!portifolioMap[firstNodeId]) { portifolioMap[firstNodeId] = Object.assign({}, emptyRow); } if(node.dep) { portifolioMap[firstNodeId].allDeps.add(node.dep); } const allItemsFromThisDep = portfolioCache[node.dep] || []; allItemsFromThisDep.forEach(thisNode => visitNode(firstNodeId, thisNode)); return; } function buildTree(id) { const allItemsFromThisCnpj = portfolioCache[id]; if(!allItemsFromThisCnpj.length) return null; if(!portifolioMap[id]) { portifolioMap[id] = Object.assign({}, emptyRow); } allItemsFromThisCnpj.forEach((item) => visitNode(id, item)); return portifolioMap[id]; } // 遍历所有唯一ID uniqueIds.forEach((id) => { if(!itemsWithDependencies.has(id)) { portifolioMap[id] = null; return; } portifolioMap[id] = buildTree(id); }); return portifolioMap; }
解决方案
核心思路
- 预处理数据:把扁平数组转换成
parent -> 所有子依赖的映射,反向构建父到子的关系,方便后续遍历 - DFS遍历+路径追踪:用DFS遍历每个节点的依赖链,同时记录当前遍历路径,以此检测循环依赖
- 缓存结果:针对大规模数据集,缓存已处理节点的结果,避免重复计算,大幅提升效率
- 结果合并:递归处理子节点后,将子节点的依赖列表、树结构、循环信息合并到当前节点的结果中,确保信息完整
完整代码
function buildDependencyTree(input) { // 第一步:构建 parent -> 所有子依赖的映射 const depMap = new Map(); const allIds = new Set(); input.forEach(item => { allIds.add(item.id); if (item.dep) { allIds.add(item.dep); if (!depMap.has(item.id)) { depMap.set(item.id, new Set()); } depMap.get(item.id).add(item.dep); } }); // 缓存已处理的节点结果,避免重复计算 const cache = new Map(); // 处理单个节点的核心函数 function processNode(nodeId, path = new Set()) { // 如果已经缓存过,直接返回 if (cache.has(nodeId)) { return cache.get(nodeId); } // 检测循环:当前节点已经在路径中 if (path.has(nodeId)) { const result = { allDeps: [nodeId], tree: 'circular', hasCircular: true, circularIds: [[Array.from(path)[0], nodeId]] // 记录循环路径的首尾节点 }; cache.set(nodeId, result); return result; } // 获取当前节点的所有子依赖 const children = depMap.get(nodeId) || new Set(); if (children.size === 0) { // 无依赖节点 const result = null; cache.set(nodeId, result); return result; } // 初始化当前节点的结果 const currentResult = { allDeps: new Set(), tree: {}, hasCircular: false, circularIds: [] }; // 复制当前路径,加入当前节点,用于递归检测 const newPath = new Set(path); newPath.add(nodeId); // 遍历所有子依赖 for (const childId of children) { const childResult = processNode(childId, newPath); if (childResult === null) { // 子节点无依赖 currentResult.allDeps.add(childId); currentResult.tree[childId] = null; } else { // 合并子节点的所有依赖 childResult.allDeps.forEach(dep => currentResult.allDeps.add(dep)); currentResult.allDeps.add(childId); // 构建树结构 currentResult.tree[childId] = childResult.tree; // 合并循环信息 if (childResult.hasCircular) { currentResult.hasCircular = true; currentResult.circularIds.push(...childResult.circularIds); } } } // 去重循环路径(避免重复记录) currentResult.circularIds = [...new Set(currentResult.circularIds.map(arr => JSON.stringify(arr)))].map(str => JSON.parse(str)); // 把allDeps转换成数组 currentResult.allDeps = Array.from(currentResult.allDeps); // 缓存结果 cache.set(nodeId, currentResult); return currentResult; } // 处理所有节点,生成最终输出 const output = {}; allIds.forEach(id => { output[id] = processNode(id); }); return output; } // 使用示例 const input = [/* 你的输入数组 */]; const result = buildDependencyTree(input); console.log(result);
代码说明
- depMap构建:将原数据转换为
id -> 子依赖ID集合的映射,避免每次遍历都去原数组查找,提升效率 - 循环检测:通过
path集合追踪当前遍历路径,若递归中遇到已在路径中的节点,直接判定为循环并记录循环节点对 - 缓存机制:
cache存储已处理
相关产品推荐
相关产品推荐

