拓撲排序 — Kahn's BFS 與 DFS 雙解法完整教學 | 資料結構與演算法

2026/07/11
拓撲排序 — Kahn's BFS 與 DFS 雙解法完整教學 | 資料結構與演算法

拓撲排序(Topological Sort) 是對 有向無環圖(DAG) 節點進行線性排序的技術,確保每條有向邊 u → v 中,u 永遠出現在 v 之前。從大學選課的先修規劃,到 npm 套件的依賴安裝順序,背後都是拓撲排序在默默運作。本文完整介紹 Kahn’s Algorithm(BFS)DFS 後序反轉法 雙解法,並搭配環偵測與 LeetCode 實戰題解。

前言

想像你是一位大學新生,面對一份課程目錄,上面標注了各課程的先修要求:

  • 修「演算法」之前必須先修「資料結構」
  • 修「資料結構」之前必須先修「程式設計概論」
  • 修「機器學習」之前必須先修「線性代數」與「機率統計」

你需要規劃一份修課順序,讓每門課在修習時,其所有先修課程都已完成。這就是拓撲排序要解決的問題——在一個有先後依賴關係的系統中,找到一個合法的處理順序。

拓撲排序應用無所不在:

  • Build 系統:Make / Gradle / Bazel 編譯前先做拓撲排序,確保模組依賴順序正確
  • 套件管理器:npm / pip / cargo 按依賴拓撲順序安裝套件,並偵測循環依賴
  • CI/CD Pipeline:GitHub Actions / Jenkins 的 Job 依賴圖,決定哪些 Job 可並行
  • Excel 公式計算:儲存格依賴關係按拓撲順序計算,確保引用的儲存格已先計算

讀完本文,你將能夠:

  • 理解 DAG(有向無環圖)入度(In-degree) 的核心概念
  • 掌握 Kahn’s Algorithm(BFS 入度移除法) 的完整流程
  • 學會 DFS 後序反轉法 與三色標記環偵測
  • 用 TypeScript / JavaScript 與 C++ 實作兩種解法
  • 解析 5 道 LeetCode 精選題(207、210、269、802、1136)

核心概念

有向無環圖(DAG)與入度

有向無環圖(Directed Acyclic Graph,DAG) 是拓撲排序的必要條件。「有向」指邊有方向性(u → v 不等於 v → u);「無環」指圖中不存在從某節點出發、沿有向邊走最終回到自身的路徑。

入度(In-degree) 是拓撲排序的關鍵數據——某節點的入度,就是「有幾條有向邊指向它」的數量。

先修課程範例:

        A ──→ C ──→ F
        │     │
        ↓     ↓
        B ──→ D ──→ E

  入度計算:
    A: 0  (起點,無人指向它)
    B: 1  (A→B)
    C: 1  (A→C)
    D: 2  (B→D, C→D)
    E: 1  (D→E)
    F: 1  (C→F)

  合法的拓撲排序(多種):
    A → B → C → D → E → F
    A → C → B → D → F → E
    A → C → F → B → D → E

  規則:若有邊 u → v,則 u 必須出現在 v 之前

入度為 0 的節點沒有前置依賴,可以最先處理。這就是 Kahn’s Algorithm 的靈感來源。


Kahn’s Algorithm(BFS 入度移除法)

Kahn’s Algorithm 是最直觀的拓撲排序實作,流程如下:

  1. 計算所有節點的入度
  2. 將所有入度為 0 的節點加入 Queue
  3. 從 Queue 取出節點,加入結果序列
  4. 將其所有鄰居的入度減 1
  5. 若鄰居的入度變為 0,加入 Queue
  6. 重複直到 Queue 空

環偵測:若最終結果序列長度 < 節點總數,代表圖中有環(有些節點的入度永遠無法降為 0)。

DAG 步驟圖(6 個節點:0~5,邊:0→2, 0→3, 1→3, 1→4, 2→5, 3→5, 4→5)

初始入度:
  節點: [0,  1,  2,  3,  4,  5]
  入度: [0,  0,  1,  2,  1,  3]

Step 1:入度為 0 → Queue = [0, 1],Result = []

Step 2:取出 0 → 移除邊 0→2(入度[2]: 1→0)、0→3(入度[3]: 2→1)
        Queue = [1, 2],Result = [0]

Step 3:取出 1 → 移除邊 1→3(入度[3]: 1→0)、1→4(入度[4]: 1→0)
        Queue = [2, 3, 4],Result = [0, 1]

Step 4:取出 2 → 移除邊 2→5(入度[5]: 3→2)
        Queue = [3, 4],Result = [0, 1, 2]

Step 5:取出 3 → 移除邊 3→5(入度[5]: 2→1)
        Queue = [4],Result = [0, 1, 2, 3]

Step 6:取出 4 → 移除邊 4→5(入度[5]: 1→0)
        Queue = [5],Result = [0, 1, 2, 3, 4]

Step 7:取出 5 → Queue = []
        Result = [0, 1, 2, 3, 4, 5]

Result 長度(6) == 節點數(6) → 無環!

DFS 後序反轉法

DFS 版本的核心思想:對每個節點做深度優先搜尋,在所有鄰居都處理完之後,才將自己推入 Stack。最終 Stack 反轉即為拓撲順序。

為什麼後序反轉能得到正確順序?因為在 DFS 中,節點 v 的所有後繼節點(及其遞迴後繼)都會先被推入 Stack,v 最後推入。反轉後,v 就排在所有它指向的節點之前,滿足拓撲排序的定義。

環偵測:使用三色標記法:

  • 0(白色):尚未訪問
  • 1(灰色):正在 DFS 路徑中
  • 2(黑色):已完全處理完

若 DFS 途中遇到「灰色」節點,代表發現了回邊(Back Edge),即存在環。


JS/TS 實作

Kahn’s Algorithm

interface TopologicalResult {
  order: number[];
  hasCycle: boolean;
}

function kahnTopologicalSort(n: number, edges: [number, number][]): TopologicalResult {
  // 建立鄰接表與入度陣列
  const graph: number[][] = Array.from({ length: n }, () => []);
  const inDegree = new Array(n).fill(0);

  for (const [u, v] of edges) {
    graph[u].push(v);
    inDegree[v]++;
  }

  // 將所有入度為 0 的節點加入 Queue
  const queue: number[] = [];
  for (let i = 0; i < n; i++) {
    if (inDegree[i] === 0) queue.push(i);
  }

  const order: number[] = [];

  while (queue.length > 0) {
    const u = queue.shift()!;
    order.push(u);

    for (const v of graph[u]) {
      inDegree[v]--;
      if (inDegree[v] === 0) queue.push(v);
    }
  }

  // 若結果長度 < n,代表有環存在
  return {
    order,
    hasCycle: order.length < n,
  };
}

// 使用範例
const result = kahnTopologicalSort(6, [[0,2],[0,3],[1,3],[1,4],[2,5],[3,5],[4,5]]);
console.log(result.order);    // 輸出:[0, 1, 2, 3, 4, 5]
console.log(result.hasCycle); // 輸出:false

帶層級的 Kahn(並行任務排程)

當題目問「最少需要幾個學期/輪次」時,可以用帶層級的 Kahn 版本——每一層的節點可以並行執行:

interface LayeredResult {
  layers: number[][];
  hasCycle: boolean;
}

function kahnWithLayers(n: number, edges: [number, number][]): LayeredResult {
  const graph: number[][] = Array.from({ length: n }, () => []);
  const inDegree = new Array(n).fill(0);

  for (const [u, v] of edges) {
    graph[u].push(v);
    inDegree[v]++;
  }

  // 初始層:所有入度為 0 的節點
  let currentLayer: number[] = [];
  for (let i = 0; i < n; i++) {
    if (inDegree[i] === 0) currentLayer.push(i);
  }

  const layers: number[][] = [];
  let processed = 0;

  while (currentLayer.length > 0) {
    layers.push([...currentLayer]);
    processed += currentLayer.length;
    const nextLayer: number[] = [];

    for (const u of currentLayer) {
      for (const v of graph[u]) {
        inDegree[v]--;
        if (inDegree[v] === 0) nextLayer.push(v);
      }
    }
    currentLayer = nextLayer;
  }

  return {
    layers,
    hasCycle: processed < n,
  };
}

// 使用範例
const { layers } = kahnWithLayers(6, [[0,2],[0,3],[1,3],[1,4],[2,5],[3,5],[4,5]]);
// layers[0] = [0, 1]  → 第一學期可修
// layers[1] = [2, 3, 4]  → 第二學期可修
// layers[2] = [5]  → 第三學期可修
// 最少需要 3 個學期(layers.length)

DFS 後序反轉版

function dfsTopologicalSort(n: number, edges: [number, number][]): TopologicalResult {
  const graph: number[][] = Array.from({ length: n }, () => []);
  for (const [u, v] of edges) graph[u].push(v);

  // 三色標記:0=未訪問, 1=訪問中(在當前 DFS 路徑上), 2=已完成
  const state = new Array(n).fill(0);
  const stack: number[] = [];
  let hasCycle = false;

  function dfs(u: number): void {
    if (hasCycle) return;
    state[u] = 1; // 標記為「訪問中」

    for (const v of graph[u]) {
      if (state[v] === 1) {
        // 遇到「訪問中」的節點 → Back Edge → 有環!
        hasCycle = true;
        return;
      }
      if (state[v] === 0) {
        dfs(v);
      }
    }

    state[u] = 2; // 後序:標記為「已完成」,推入 Stack
    stack.push(u);
  }

  for (let i = 0; i < n; i++) {
    if (state[i] === 0) dfs(i);
  }

  return {
    order: hasCycle ? [] : stack.reverse(),
    hasCycle,
  };
}

課程排程應用(整合版)

class CourseScheduler {
  private n: number;
  private prerequisites: [number, number][];

  constructor(numCourses: number, prerequisites: [number, number][]) {
    this.n = numCourses;
    this.prerequisites = prerequisites;
  }

  // 是否可完成所有課程(環偵測)
  canFinish(): boolean {
    const { hasCycle } = kahnTopologicalSort(this.n, this.prerequisites);
    return !hasCycle;
  }

  // 回傳一個合法的修課順序
  findOrder(): number[] {
    const { order, hasCycle } = kahnTopologicalSort(this.n, this.prerequisites);
    return hasCycle ? [] : order;
  }

  // 回傳每個學期可以修的課(最少學期數)
  findParallelSchedule(): number[][] {
    const { layers, hasCycle } = kahnWithLayers(this.n, this.prerequisites);
    return hasCycle ? [] : layers;
  }
}

// 使用範例
const scheduler = new CourseScheduler(4, [[1,0],[2,0],[3,1],[3,2]]);
console.log(scheduler.canFinish());         // 輸出:true
console.log(scheduler.findOrder());          // 輸出:[0, 1, 2, 3](其中一種合法順序)
console.log(scheduler.findParallelSchedule()); // 輸出:[[0], [1, 2], [3]]

C++ 對照

Kahn’s Algorithm(BFS 版)

#include <vector>
#include <queue>
using namespace std;

struct TopologicalResult {
    vector<int> order;
    bool hasCycle;
};

TopologicalResult kahnTopologicalSort(int n, vector<pair<int,int>>& edges) {
    vector<vector<int>> graph(n);
    vector<int> inDegree(n, 0);

    for (auto [u, v] : edges) {
        graph[u].push_back(v);
        inDegree[v]++;
    }

    queue<int> q;
    for (int i = 0; i < n; i++)
        if (inDegree[i] == 0) q.push(i);

    vector<int> order;
    while (!q.empty()) {
        int u = q.front(); q.pop();
        order.push_back(u);
        for (int v : graph[u]) {
            if (--inDegree[v] == 0) q.push(v);
        }
    }

    return { order, (int)order.size() < n };
}

DFS 後序反轉版(含三色標記)

#include <vector>
#include <stack>
using namespace std;

class TopoSortDFS {
    int n;
    vector<vector<int>>& graph;
    vector<int> state; // 0=未訪 1=訪問中 2=完成
    stack<int> stk;
    bool hasCycle = false;

    void dfs(int u) {
        if (hasCycle) return;
        state[u] = 1;
        for (int v : graph[u]) {
            if (state[v] == 1) { hasCycle = true; return; }
            if (state[v] == 0) dfs(v);
        }
        state[u] = 2;
        stk.push(u);
    }

public:
    TopoSortDFS(int n, vector<vector<int>>& g) : n(n), graph(g), state(n, 0) {}

    vector<int> sort() {
        for (int i = 0; i < n; i++)
            if (state[i] == 0) dfs(i);

        if (hasCycle) return {};
        vector<int> result;
        while (!stk.empty()) { result.push_back(stk.top()); stk.pop(); }
        return result; // 已是正確拓撲順序(Stack 反轉)
    }
};

帶層級的 Kahn(並行任務計算)

#include <vector>
#include <queue>
using namespace std;

// 回傳每層可並行執行的節點,若有環則回傳空
vector<vector<int>> kahnLayers(int n, vector<pair<int,int>>& edges) {
    vector<vector<int>> graph(n);
    vector<int> inDegree(n, 0);

    for (auto [u, v] : edges) {
        graph[u].push_back(v);
        inDegree[v]++;
    }

    vector<vector<int>> layers;
    vector<int> current;
    for (int i = 0; i < n; i++)
        if (inDegree[i] == 0) current.push_back(i);

    int processed = 0;
    while (!current.empty()) {
        layers.push_back(current);
        processed += (int)current.size();
        vector<int> next;
        for (int u : current)
            for (int v : graph[u])
                if (--inDegree[v] == 0) next.push_back(v);
        current = next;
    }

    return processed == n ? layers : vector<vector<int>>{};
}

複雜度分析

方法時間複雜度空間複雜度環偵測特色
Kahn’s BFSO(V + E)O(V + E)自然偵測(count < n)直觀、支援層級計算
DFS 後序反轉O(V + E)O(V + E)(遞迴棧)需三色標記程式碼更精簡
帶層級 KahnO(V + E)O(V + E)自然偵測可直接得到最短並行輪數

兩種主要演算法的時間與空間複雜度相同,皆為 O(V + E),但實務上各有優劣:

  • Kahn’s BFS:程式流程清晰、易於偵錯;結合最小堆可輸出字典序最小的拓撲排序;天然支援層級(並行任務)資訊
  • DFS 後序反轉:程式碼更短;但遞迴深度可能超過 Stack 限制,需考慮改用迭代版本

變體與延伸

字典序最小的拓撲排序

標準 Kahn’s Algorithm 用普通 Queue,取出順序取決於加入順序。若要輸出字典序最小的拓撲排序,只需把 Queue 換成最小堆(Min-Heap / Priority Queue)

// 用排序陣列模擬 Min-Heap(面試可接受)
function lexSmallestTopoSort(n: number, edges: [number, number][]): number[] {
  const graph: number[][] = Array.from({ length: n }, () => []);
  const inDegree = new Array(n).fill(0);

  for (const [u, v] of edges) {
    graph[u].push(v);
    inDegree[v]++;
  }

  // 初始化:保持升序排列以模擬 Min-Heap
  const minHeap: number[] = [];
  for (let i = 0; i < n; i++) if (inDegree[i] === 0) minHeap.push(i);
  minHeap.sort((a, b) => a - b);

  const result: number[] = [];
  while (minHeap.length > 0) {
    const u = minHeap.shift()!; // 取出最小值
    result.push(u);
    for (const v of graph[u]) {
      if (--inDegree[v] === 0) {
        // 二分搜尋找插入位置,維持有序
        let lo = 0, hi = minHeap.length;
        while (lo < hi) {
          const mid = (lo + hi) >> 1;
          if (minHeap[mid] < v) lo = mid + 1;
          else hi = mid;
        }
        minHeap.splice(lo, 0, v);
      }
    }
  }

  return result.length === n ? result : []; // 空陣列代表有環
}

最長路徑(DAG 上的動態規劃)

在 DAG 上求最長路徑是拓撲排序最重要的延伸應用。先做拓撲排序,再按拓撲順序做 DP:

// 在 DAG 上求最長路徑(可代表最多串連任務數)
function longestPathInDAG(n: number, edges: [number, number][]): number {
  const graph: number[][] = Array.from({ length: n }, () => []);
  const inDegree = new Array(n).fill(0);

  for (const [u, v] of edges) {
    graph[u].push(v);
    inDegree[v]++;
  }

  // Step 1:Kahn's Algorithm 得到拓撲順序
  const queue: number[] = [];
  for (let i = 0; i < n; i++) if (inDegree[i] === 0) queue.push(i);

  // dp[i] = 以節點 i 為終點的最長路徑長度
  const dp = new Array(n).fill(1);

  while (queue.length > 0) {
    const u = queue.shift()!;
    for (const v of graph[u]) {
      // Step 2:按拓撲順序做 DP 轉移
      dp[v] = Math.max(dp[v], dp[u] + 1);
      if (--inDegree[v] === 0) queue.push(v);
    }
  }

  return Math.max(...dp);
}

// 使用範例
console.log(longestPathInDAG(6, [[0,2],[0,3],[1,3],[1,4],[2,5],[3,5],[4,5]]));
// 輸出:4(最長路徑:0→2→5 或 1→3→5 等)

面試考點

拓撲排序在面試中有兩種核心考法,必須熟練:

考法一:環偵測(Can I finish?)

題目通常說「判斷是否可以完成所有任務」。本質就是判斷圖是否為 DAG。使用 Kahn’s Algorithm,若最終 count < n 則有環。

考法二:求排序順序(What is the order?)

題目要求輸出一個合法的完成順序。直接回傳 Kahn’s Algorithm 的結果陣列即可。若有環則回傳空陣列。

考法三:最少輪次(How many rounds?)

題目問「最少需要幾個學期/批次可以完成」。使用帶層級的 Kahn,答案就是 layers.length

建圖方向要特別注意:題目通常說「修 A 前需先修 B」,代表邊是 B → A(先修 B,後修 A)。建圖時方向容易搞錯。


LeetCode 練習

207. Course Schedule(環偵測)

// 給定 numCourses 門課與 prerequisites 陣列
// prerequisites[i] = [a, b] 表示修 a 之前必須先修 b(邊:b → a)
// 判斷能否完成所有課程
function canFinish(numCourses: number, prerequisites: number[][]): boolean {
  const graph: number[][] = Array.from({ length: numCourses }, () => []);
  const inDegree = new Array(numCourses).fill(0);

  for (const [course, pre] of prerequisites) {
    graph[pre].push(course); // 邊:pre → course
    inDegree[course]++;
  }

  const queue: number[] = [];
  for (let i = 0; i < numCourses; i++) {
    if (inDegree[i] === 0) queue.push(i);
  }

  let completed = 0;
  while (queue.length > 0) {
    const course = queue.shift()!;
    completed++;
    for (const next of graph[course]) {
      inDegree[next]--;
      if (inDegree[next] === 0) queue.push(next);
    }
  }

  return completed === numCourses; // 全部處理完 → 無環
}
// 時間:O(V+E),空間:O(V+E)

210. Course Schedule II(求拓撲順序)

// 在 207 基礎上,回傳一個合法修課順序;若有環則回傳空陣列
function findOrder(numCourses: number, prerequisites: number[][]): number[] {
  const graph: number[][] = Array.from({ length: numCourses }, () => []);
  const inDegree = new Array(numCourses).fill(0);

  for (const [course, pre] of prerequisites) {
    graph[pre].push(course);
    inDegree[course]++;
  }

  const queue: number[] = [];
  for (let i = 0; i < numCourses; i++) {
    if (inDegree[i] === 0) queue.push(i);
  }

  const order: number[] = [];
  while (queue.length > 0) {
    const course = queue.shift()!;
    order.push(course);
    for (const next of graph[course]) {
      inDegree[next]--;
      if (inDegree[next] === 0) queue.push(next);
    }
  }

  return order.length === numCourses ? order : [];
}
// 時間:O(V+E),空間:O(V+E)

269. Alien Dictionary(外星字典)

外星字典是拓撲排序的經典難題:給定按外星語字典序排列的單詞列表,推導出字母的相對順序。

// 給定排好序的單字列表,推導出字母之間的相對大小順序
function alienOrder(words: string[]): string {
  // 收集所有出現過的字母
  const chars = new Set(words.join(''));
  const n = chars.size;
  const charIndex = new Map<string, number>();
  let idx = 0;
  for (const c of chars) charIndex.set(c, idx++);

  const graph: number[][] = Array.from({ length: n }, () => []);
  const inDegree = new Array(n).fill(0);

  // 相鄰單字兩兩比較,找出字母順序關係
  for (let i = 0; i < words.length - 1; i++) {
    const w1 = words[i], w2 = words[i + 1];
    const len = Math.min(w1.length, w2.length);
    let found = false;
    for (let j = 0; j < len; j++) {
      if (w1[j] !== w2[j]) {
        // w1[j] 排在 w2[j] 之前(邊:w1[j] → w2[j])
        const u = charIndex.get(w1[j])!;
        const v = charIndex.get(w2[j])!;
        graph[u].push(v);
        inDegree[v]++;
        found = true;
        break;
      }
    }
    // 若 w1 是 w2 的前綴且 w1 更長,則字典序無效(如 ["abc", "ab"])
    if (!found && w1.length > w2.length) return '';
  }

  // Kahn's Algorithm
  const queue: number[] = [];
  for (let i = 0; i < n; i++) if (inDegree[i] === 0) queue.push(i);

  const indexChar = new Map<number, string>();
  for (const [c, i] of charIndex) indexChar.set(i, c);

  let result = '';
  while (queue.length > 0) {
    const u = queue.shift()!;
    result += indexChar.get(u);
    for (const v of graph[u]) {
      inDegree[v]--;
      if (inDegree[v] === 0) queue.push(v);
    }
  }

  return result.length === n ? result : ''; // 有環則字典序矛盾
}
// 時間:O(C),C 為所有單字的字母總數;空間:O(1)(最多 26 個字母)

802. Find Eventual Safe States(反向圖 + 拓撲排序)

// 安全節點:從該節點出發的所有路徑,最終都終止於終端節點(無出邊)
// 等同於:不在任何環上,且不指向環中節點的節點
//
// 關鍵洞見:在反向圖中,終端節點(原圖出度=0)的入度=0
// 對反向圖做拓撲排序,能被排入的節點即為安全節點
function eventualSafeNodes(graph: number[][]): number[] {
  const n = graph.length;
  const reverseGraph: number[][] = Array.from({ length: n }, () => []);
  const outDegree = new Array(n).fill(0); // 原圖出度 = 反向圖「入度」

  for (let u = 0; u < n; u++) {
    for (const v of graph[u]) {
      reverseGraph[v].push(u); // 反向邊 v → u
      outDegree[u]++;
    }
  }

  // 原圖終端節點(出度=0)在反向圖中入度=0,最先處理
  const queue: number[] = [];
  for (let i = 0; i < n; i++) {
    if (outDegree[i] === 0) queue.push(i);
  }

  const safe = new Array(n).fill(false);
  while (queue.length > 0) {
    const u = queue.shift()!;
    safe[u] = true;
    for (const prev of reverseGraph[u]) {
      outDegree[prev]--;
      if (outDegree[prev] === 0) queue.push(prev);
    }
  }

  const result: number[] = [];
  for (let i = 0; i < n; i++) {
    if (safe[i]) result.push(i); // 結果自然按升序排列
  }
  return result;
}
// 時間:O(V+E),空間:O(V+E)

1136. Parallel Courses(最少學期數)

// 每個學期只能修入度為 0 的課程,求修完所有課程的最少學期數
// 若有環(某些課程永遠無法修),回傳 -1
function minimumSemesters(n: number, relations: number[][]): number {
  const graph: number[][] = Array.from({ length: n + 1 }, () => []);
  const inDegree = new Array(n + 1).fill(0);

  for (const [prev, next] of relations) {
    graph[prev].push(next);
    inDegree[next]++;
  }

  // 初始層:所有入度為 0 的課程(課程編號從 1 開始)
  let currentLayer: number[] = [];
  for (let i = 1; i <= n; i++) {
    if (inDegree[i] === 0) currentLayer.push(i);
  }

  let semesters = 0;
  let completed = 0;

  while (currentLayer.length > 0) {
    semesters++;
    completed += currentLayer.length;
    const nextLayer: number[] = [];

    for (const course of currentLayer) {
      for (const next of graph[course]) {
        inDegree[next]--;
        if (inDegree[next] === 0) nextLayer.push(next);
      }
    }
    currentLayer = nextLayer;
  }

  return completed === n ? semesters : -1; // 若有環,部分課程無法修完
}
// 時間:O(V+E),空間:O(V+E)

LeetCode 題目總覽

題號題目難度核心技巧
207Course ScheduleMediumKahn’s Algorithm 環偵測
210Course Schedule IIMediumKahn’s Algorithm 求拓撲順序
269Alien DictionaryHard建圖 + Kahn’s Algorithm
802Find Eventual Safe StatesMedium反向圖 + 拓撲排序
1136Parallel CoursesMedium帶層級的 Kahn(最少學期)

總結

拓撲排序是圖論中最實用的演算法之一,核心在於處理有方向性的依賴關係。本文介紹的兩種實作各有適用場景:

  • Kahn’s Algorithm(BFS):直觀、易理解,自然偵測環,支援層級計算和字典序最小排序;是面試與實務的首選
  • DFS 後序反轉法:程式碼精簡,但需要額外的三色標記才能偵測環;適合在已知無環的 DAG 上快速取得排序

解題時,記住三個關鍵問題:

  1. 能不能完成?→ 環偵測(count < n 即有環)
  2. 完成的順序?→ Kahn’s Algorithm 結果序列
  3. 最少幾輪?→ 帶層級的 Kahn,答案為 layers.length

下一篇將進入 最小生成樹(Minimum Spanning Tree),介紹 Kruskal 與 Prim 演算法——同樣是圖論中不可或缺的核心主題。

如有任何問題或疑惑,歡迎在下方留言,或透過

聯絡頁面

與我聯繫,希望這篇文章能幫助你深刻理解拓撲排序的精髓!

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。