Arian Soleimanzadeh
  • 首页
  • 博客
  • 播客
  • 视频
  • 联系
العربيةArabic
DeutschGerman
EnglishEnglish
فارسیPersian
한국어Korean
中文Chinese
面板•快速联系

Languages

Choose your interface locale

ar

العربية

Arabic

de

Deutsch

German

en

English

English

fa

فارسی

Persian

ko

한국어

Korean

zh

中文

Chinese

预约咨询

发送一条简短消息——我会尽快回复。

LinkedIn快速回复
首页/文章/使用 JavaScript 和 TypeScript 实现 Kosaraju 算法
Kosaraju文章

使用 JavaScript 和 TypeScript 实现 Kosaraju 算法

从零实现 Kosaraju 算法:使用 JavaScript 和 TypeScript 完成图表示、DFS 完成顺序、转置图、SCC 提取、复杂度分析以及实际工程中的常见问题。

2026年8月20日9 分钟阅读1 浏览
#Kosaraju#JavaScript#TypeScript#Graph Algorithms#SCC#DFS#Software Engineering

Arian Soleimanzadeh

Software Engineer & Researcher

使用 JavaScript 和 TypeScript 实现 Kosaraju 算法,包括 DFS、转置图和强连通分量

Arian Soleimanzadeh

AI · 代码 · 产品

研究 + 工程
本页目录
引言使用 Adjacency List 表示图第一次 DFS:记录完成顺序构造 Transpose Graph完整 JavaScript 实现TypeScript 泛型版本微服务依赖示例时间复杂度JavaScript 的递归深度问题常见错误总结

引言

Kosaraju 算法用于寻找有向图中的 Strongly Connected Components(SCC,强连通分量)。

理解理论之后,下一步就是把算法真正实现出来。

本文将使用 JavaScript 和 TypeScript 完成完整实现。

整体流程为:

Code
123456789
原图 DFS
   ↓
Finish Order
   ↓
反转所有边
   ↓
Transpose Graph DFS
   ↓
SCCs

使用 Adjacency List 表示图

JavaScript
1234567
const graph = new Map([
  [0, [1]],
  [1, [2, 3]],
  [2, [0]],
  [3, [4]],
  [4, [3]]
]);

对于稀疏图,邻接表通常比邻接矩阵更节省空间。


第一次 DFS:记录完成顺序

Kosaraju 的关键之一是必须在所有邻居处理完成之后,才把顶点加入 Stack。

JavaScript
1234567891011
function fillOrder(node, graph, visited, stack) {
  visited.add(node);

  for (const neighbor of graph.get(node) ?? []) {
    if (!visited.has(neighbor)) {
      fillOrder(neighbor, graph, visited, stack);
    }
  }

  stack.push(node);
}

这里保存的是 Finish Order,而不是 Discovery Order。


构造 Transpose Graph

将:

Code
1
A → B

反转为:

Code
1
B → A
JavaScript
12345678910111213141516171819
function transposeGraph(graph) {
  const transpose = new Map();

  for (const node of graph.keys()) {
    transpose.set(node, []);
  }

  for (const [node, neighbors] of graph.entries()) {
    for (const neighbor of neighbors) {
      if (!transpose.has(neighbor)) {
        transpose.set(neighbor, []);
      }

      transpose.get(neighbor).push(node);
    }
  }

  return transpose;
}

完整 JavaScript 实现

JavaScript
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
function kosaraju(graph) {
  const visited = new Set();
  const stack = [];

  function fillOrder(node) {
    visited.add(node);

    for (const neighbor of graph.get(node) ?? []) {
      if (!visited.has(neighbor)) {
        fillOrder(neighbor);
      }
    }

    stack.push(node);
  }

  for (const node of graph.keys()) {
    if (!visited.has(node)) {
      fillOrder(node);
    }
  }

  const transpose = new Map();

  for (const node of graph.keys()) {
    transpose.set(node, []);
  }

  for (const [node, neighbors] of graph.entries()) {
    for (const neighbor of neighbors) {
      if (!transpose.has(neighbor)) {
        transpose.set(neighbor, []);
      }

      transpose.get(neighbor).push(node);
    }
  }

  visited.clear();
  const components = [];

  function collect(node, component) {
    visited.add(node);
    component.push(node);

    for (const neighbor of transpose.get(node) ?? []) {
      if (!visited.has(neighbor)) {
        collect(neighbor, component);
      }
    }
  }

  while (stack.length > 0) {
    const node = stack.pop();

    if (!visited.has(node)) {
      const component = [];
      collect(node, component);
      components.push(component);
    }
  }

  return components;
}

TypeScript 泛型版本

TypeScript
1
type Graph<T> = Map<T, T[]>;

这样顶点可以是数字,也可以是字符串,例如服务名称。

TypeScript
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
function kosaraju<T>(graph: Graph<T>): T[][] {
  const visited = new Set<T>();
  const stack: T[] = [];

  const fillOrder = (node: T): void => {
    visited.add(node);

    for (const neighbor of graph.get(node) ?? []) {
      if (!visited.has(neighbor)) {
        fillOrder(neighbor);
      }
    }

    stack.push(node);
  };

  for (const node of graph.keys()) {
    if (!visited.has(node)) {
      fillOrder(node);
    }
  }

  const transpose: Graph<T> = new Map();

  for (const node of graph.keys()) {
    transpose.set(node, []);
  }

  for (const [node, neighbors] of graph.entries()) {
    for (const neighbor of neighbors) {
      if (!transpose.has(neighbor)) {
        transpose.set(neighbor, []);
      }

      transpose.get(neighbor)!.push(node);
    }
  }

  visited.clear();
  const components: T[][] = [];

  const collect = (node: T, component: T[]): void => {
    visited.add(node);
    component.push(node);

    for (const neighbor of transpose.get(node) ?? []) {
      if (!visited.has(neighbor)) {
        collect(neighbor, component);
      }
    }
  };

  while (stack.length > 0) {
    const node = stack.pop()!;

    if (!visited.has(node)) {
      const component: T[] = [];
      collect(node, component);
      components.push(component);
    }
  }

  return components;
}

微服务依赖示例

TypeScript
1234567
const services: Graph<string> = new Map([
  ["user", ["payment"]],
  ["payment", ["notification"]],
  ["notification", ["user"]],
  ["report", ["analytics"]],
  ["analytics", ["report"]]
]);

算法可以找到:

Code
12
{user, payment, notification}
{report, analytics}

这两个 SCC 都代表潜在的循环依赖关系。


时间复杂度

Code
123
第一次 DFS       O(V + E)
构造 Transpose   O(V + E)
第二次 DFS       O(V + E)

因此总复杂度为:

Code
1
O(V + E)

空间复杂度同样保持在线性级别。


JavaScript 的递归深度问题

对于非常深的图,递归 DFS 可能出现:

Code
1
RangeError: Maximum call stack size exceeded

因此在超大规模或 Production 图处理中,通常值得考虑 iterative DFS。


常见错误

实现 Kosaraju 时常见的问题包括:

  • 在处理完邻居之前就把顶点加入 Stack。
  • 第二次 DFS 前忘记清空 visited。
  • 第二次 DFS 仍然使用原图。
  • 忽略没有出边的顶点。
  • 测试时错误地假设 SCC 内部顺序固定。

总结

Kosaraju 的实现可以浓缩为:

Code
123456789
DFS
↓
Finish Order
↓
Transpose
↓
DFS
↓
SCCs

使用邻接表时,时间复杂度为 O(V + E)。

TypeScript 的 Generic 和类型安全能力使这种实现很适合扩展到模块依赖、包依赖以及微服务架构分析等实际工程场景。

最值得记住的是:

第一次 DFS 负责确定正确的处理顺序,Transpose Graph 反转组件之间的可达方向,而第二次 DFS 最终揭示每一个 SCC 的边界。

本页目录
引言使用 Adjacency List 表示图第一次 DFS:记录完成顺序构造 Transpose Graph完整 JavaScript 实现TypeScript 泛型版本微服务依赖示例时间复杂度JavaScript 的递归深度问题常见错误总结

文章信息

发布时间、阅读时长和浏览数据。

发布

2026年8月20日

更新

2026年8月19日

阅读时长

9 分钟阅读

浏览

1

作者

Arian Soleimanzadeh

下一篇

B2B CRM 与 B2C CRM 有什么区别?从销售流程到软件架构

让我们构建清晰、快速而优雅的作品。

用于合作、咨询或产品工作的快速联系入口。

快速联系给我发邮件
Arian Soleimanzadeh

个人作品集,聚焦现代 Web 工程、UI 系统与实用型 AI 产品——干净的代码,清晰的设计。

快速链接

  • 关于
  • 博客
  • 项目
  • 联系

联系

  • info@ariansoleimanzadeh.site
  • soleimanzadeh.a.work@gmail.com

可联系时间: 工作日

通常回复时间在 24小时内。

订阅通讯

获取文章、项目与新版本发布的更新。

© 2026 ariansoleimanzadeh.site — 保留所有权利。

LinkedIn