You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;
}

解决方案

核心思路

  1. 预处理数据:把扁平数组转换成parent -> 所有子依赖的映射,反向构建父到子的关系,方便后续遍历
  2. DFS遍历+路径追踪:用DFS遍历每个节点的依赖链,同时记录当前遍历路径,以此检测循环依赖
  3. 缓存结果:针对大规模数据集,缓存已处理节点的结果,避免重复计算,大幅提升效率
  4. 结果合并:递归处理子节点后,将子节点的依赖列表、树结构、循环信息合并到当前节点的结果中,确保信息完整

完整代码

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存储已处理
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.22 14:28:09