由浅入深递归练习

🧠 思路
复制代码
先写一个节点的逻辑,确保返回的数据符合预期
再考虑其他节点,根据返回值做处理
正/反,数据从上(前)往下(后)推,或者从下(后)往上(前)推
题 1:文件系统树展开(最经典入门)
📌 目标

输出所有节点的 name

📦 数据结构
TypeScript 复制代码
type Node = {
  id: string;
  name: string;
  children?: Node[];
};

const root: Node = {
  id: "1",
  name: "root",
  children: [
    {
      id: "2",
      name: "child2",
      children: [
        { id: "4", name: "child4" },
        {
          id: "5",
          name: "child5",
          children: [
            { id: "8", name: "child8" },
            { id: "9", name: "child9" },
          ],
        },
      ],
    },
    {
      id: "3",
      name: "child3",
      children: [
        { id: "6", name: "child6" },
        { id: "7", name: "child7" },
      ],
    },
  

🧩 任务

实现函数:

TypeScript 复制代码
function flattenTree(root: Node): string[]
📌 答案
TypeScript 复制代码
type NestedArray<T> = (T | NestedArray<T>)[];

const flatArr = <T,>(arr: NestedArray<T>): T[] => {
  const result: T[] = [];
  for (const item of arr) {
    if (Array.isArray(item)) {
      result.push(...flatArr(item));
    } else {
      result.push(item);
    }
  }
  return result;
};

const flattenTree = (root: Node): string[] => {
  // const result = new Map<string, Node>();
  const result: string[] = [];
  result.push(root.name);
  if (root.children) {
    const names = root.children.map((child_node) => flattenTree(child_node));

    const flatNames = flatArr(names);
    result.push(...flatNames);
  }
  return result;
};

],
};
const result = flattenTree(root);
console.log(result);
🔥 第二阶段:递归进阶(业务结构处理)
题 2:查找某个节点
📌 要求
  • 找到即停止递归
  • 不能遍历完整树(优化意识)
💡 提升点

👉 "提前 return" 是递归性能关键点

📌 答案

TypeScript 复制代码
function findNode(root: Node, id: string): Node | null {
  let target: Node | null = null;
  if (root.id === id) {
    target = root;
  } else {
    if (root.children) {
      for (const child of root.children) {
        target = findNode(child, id);
        if (target) break;
      }
    }
  }
  return target;
}
题 3:统计树节点数量(带过滤条件)
📌 任务

统计 name 长度 > 3 的节点数

💡 提升点
  • 高阶递归(带 predicate)
  • 业务过滤逻辑嵌入递归

📌 答案

TypeScript 复制代码
const filterFn = (node: Node): boolean => node.id === "9";

function countNodes(root: Node, predicate?: (node: Node) => boolean): number {
  // let count = 0;
  const match = predicate?.(root) ?? true;
  return (
    (match ? 1 : 0) +
    (root.children?.reduce(
      (sum, childNode) => sum + countNodes(childNode, predicate),
      0,
    ) ?? 0)
  );
  // if (match) {
  //   count++;
  // }
  // if (root.children) {
  //   // for (const childNode of root.children) {
  //   //   count += countNodes(childNode, predicate);
  //   // }
  //   count += root?.children?.reduce((sum, childNode) => sum + countNodes(childNode, predicate), 0) ?? 0;
  // }
  // return count;
}
题 4:ReactFlow 节点依赖计算(重点🔥)

📌 结构

TypeScript 复制代码
type Node = {
  id: string;
  type: string;
};

type Edge = {
  source: string;
  target: string;
};

📌 任务

找到所有"上游依赖节点"

📌 示例

TypeScript 复制代码
A → B → D
A → C

如果 target = D

输出:

TypeScript 复制代码
[A, B]

⚠️ 难点
  • 图结构不是树(可能多路径)
  • 要防止死循环
  • 要去重

💡 提升点
  • visited 集合(图遍历核心)
  • DFS on graph(递归版)

📌 答案
TypeScript 复制代码
function getUpstreamNodes(
  nodeId: string,
  nodes: Node[],
  edges: Edge[],
): Node[] {
  const nodeMap = new Map(nodes.map((n) => [n.id, n]));

  const targetToSources = new Map<string, string[]>();

  for (const { source, target } of edges) {
    if (!targetToSources.has(target)) targetToSources.set(target, []);
    targetToSources.get(target)!.push(source);
  }

  const visited = new Set<string>(); // 防环
  const collected = new Set<string>(); // 去重

  function dfs(currId: string) {
    const sources = targetToSources.get(currId) ?? [];
    for (const sourceId of sources) {
      if (visited.has(sourceId)) continue;
      visited.add(sourceId);
      collected.add(sourceId);
      dfs(sourceId);
    }
  }
  dfs(nodeId);
  return [...collected]
    .map((id) => nodeMap.get(id))
    .filter((n): n is Node => Boolean(n));
}
const nodes: Node[] = [
  { id: "1", type: "start" },
  { id: "2", type: "detect" },
  { id: "3", type: "classify" },
  { id: "4", type: "feature" },
  { id: "5", type: "logic" },
  { id: "6", type: "end" },
];

const edges: Edge[] = [
  { source: "1", target: "2" },
  { source: "1", target: "3" },
  { source: "2", target: "5" },
  { source: "3", target: "5" },
  { source: "5", target: "6" },
];
// 1=>2=>5=>6
// 1=>3=>5=>6
const result = getUpstreamNodes("6", nodes, edges);
console.log(result);

题 5:递归缓存优化
📌 任务

要求:

  • 如果 cache 命中直接返回
  • 否则递归计算依赖节点结果
  • 存入 cache
📌 答案

后面的节点输出依赖前面的节点中的数据

TypeScript 复制代码
function computeNodeValue(nodeId, graph, cache) {
  // 1. 命中缓存
  if (cache.has(nodeId)) {
    return cache.get(nodeId);
  }

  // 2. 递归依赖(graph 需提供:依赖列表 + 如何由依赖算当前值)
  const upstreamNodes= graph.getUpstreamNodes(nodeId);
  const upstreamUOutputs= upstreamNodes.map((node_id) =>
    computeNodeValue(node_id, graph, cache)
  );

  const value = graph.compute(nodeId, depValues);

  // 3. 写入缓存
  cache.set(nodeId, value);
  return value;
}

题 6:检测循环依赖(非常重要🔥)
1. 什么叫「循环依赖」?
  • 沿着边的方向走一圈能回到自己,就是环:
  • 合法(DAG,无环):
  • 开始 → 检测 → 分类 → 结束
  • 非法(有环):
  • 检测A → 分类B → 逻辑C → 检测A
📌 答案
TypeScript 复制代码
function hasCycle(nodes: { id: string }[], edges: { source: string; target: string }[]): boolean {
  // 邻接表:source → [targets]
  const adj = new Map<string, string[]>();
  for (const n of nodes) adj.set(n.id, []);
  for (const e of edges) {
    adj.get(e.source)?.push(e.target);
  }

  const visited = new Set<string>();   // 这个点是否已经被 DFS 扫过(可选优化)
  const onStack = new Set<string>();   // 当前递归路径上的点 = recursion stack

  function dfs(nodeId: string): boolean {
    if (onStack.has(nodeId)) return true;   // 在当前路径上又遇到了 → 环
    if (visited.has(nodeId)) return false;  // 已扫过且无环,不用再扫

    visited.add(nodeId);
    onStack.add(nodeId);

    for (const next of adj.get(nodeId) ?? []) {
      if (dfs(next)) return true;
    }

    onStack.delete(nodeId);  // 回溯:离开当前路径
    return false;
  }

  // 图可能不连通,每个节点都要作为起点试一次
  for (const n of nodes) {
    if (!visited.has(n.id) && dfs(n.id)) return true;
  }
  return false;
}
💡 本质
  • 图 DFS + recursion stack
题 8:递归结构合并
📌 任务

两个树结构合并:

规则:

  • 相同 id 合并 children
  • 不同 id 直接拼接

📌 答案

TypeScript 复制代码
const mergeNode = (a: Node, b: Node) => {
  return { ...a, ...b, children: mergeChildrenNode(a.children??[], b.children??[]) };
};

const mergeChildrenNode = (aChildren: Node[], bChildren: Node[]): Node[] => {
  const maps = new Map<string, Node>();
  for (const aChild of aChildren) {
    maps.set(aChild.id, aChild);
  }
  for (const bChild of bChildren) {
    if (maps.has(bChild.id)) {
      const aChild = maps.get(bChild.id)!;
      maps.set(aChild.id, mergeNode(aChild, bChild));
    } else {
      maps.set(bChild.id, bChild);
    }
  }

  return Array.from(maps.values());
};
相关推荐
tedcloud1232 小时前
ai-engineering-from-scratch部署教程:从零搭建AI应用环境
服务器·前端·人工智能·系统架构·edge
Kurisu5752 小时前
全面战争:战锤3修改器下载2026最新
前端
丷丩2 小时前
MapLibre GL JS第21课:绘制GeoJSON点图标、注记
前端·javascript·gis·mapbox·maplibre gl js
LCG元2 小时前
现代Web应用高可用架构设计与性能调优实战
前端·wpf
丷丩2 小时前
MapLibre GL JS第20课:更新GeoJSON多边形
前端·javascript·gis·mapbox·maplibre gl js
swipe3 小时前
DeepAgents middleware 工程实战:把复杂 Agent 的运行时基建交给可组合中间件
前端·面试·llm
丷丩3 小时前
MapLibre GL JS第33课:渲染世界副本
javascript·gis·map·mapbox·maplibre gl js
前端环境观察室3 小时前
别让 Agent 浏览器任务无限重试:失败分类、RetryPolicy 与人工复核
前端
bonechips3 小时前
深入理解 JavaScript的历史包袱——变量提升(Hoisting)
javascript·深度学习