🧠 思路
先写一个节点的逻辑,确保返回的数据符合预期
再考虑其他节点,根据返回值做处理
正/反,数据从上(前)往下(后)推,或者从下(后)往上(前)推
题 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());
};