Topological Sort (מיון טופולוגי)
עודכן לאחרונה
מיון טופולוגי של גרף מכוון חסר מעגלים (DAG) הוא סידור ליניארי של הצמתים כך שלכל קשת u → v, הצומת u מופיע לפני v. הוא עונה על שאלות כמו "באיזה סדר אפשר להריץ את המשימות האלה כך שכל דרישה מקדימה תסתיים קודם?" לחצו על הפעלה למעלה כדי לראות את האלגוריתם של Kahn מקלף צמתים בסדר תקין.
האלגוריתם של Kahn לוקח שוב ושוב צומת שלא נשארו לו קשתות נכנסות (דרגת כניסה 0), מוסיף אותו לסדר ומסיר את הקשתות היוצאות ממנו, מה שעשוי לשחרר צמתים חדשים עם דרגת כניסה 0. הוא עובד רק על DAG: אם יש מעגל, חלק מהצמתים לעולם לא יגיעו לדרגת כניסה 0 ולא קיים סדר תקין. זמן הריצה שלו הוא O(V + E).
סיבוכיות זמן וזיכרון
| מדד | סיבוכיות | הערות |
|---|---|---|
| זמן | O(V + E) | כל צומת נפלט פעם אחת, כל קשת מוסרת פעם אחת |
| זיכרון | O(V) | ספירות דרגת כניסה + קבוצת המוכנים + הסדר |
| דורש | DAG | לגרף עם מעגלים אין סדר טופולוגי |
| תוצאה | לא יחידה | יכולים להתקיים סדרים תקינים רבים |
צעד אחר צעד (האלגוריתם של Kahn)
| צעד | מה קורה |
|---|---|
| 1 | מחשבים את דרגת הכניסה (מספר הקשתות הנכנסות) של כל צומת. |
| 2 | אוספים את כל הצמתים עם דרגת כניסה 0 לקבוצת המוכנים. |
| 3 | לוקחים צומת מוכן ומוסיפים אותו לסדר הפלט. |
| 4 | מקטינים באחד את דרגת הכניסה של כל אחד מהצמתים העוקבים שלו. |
| 5 | כל עוקב שמגיע לדרגת כניסה 0 מצטרף לקבוצת המוכנים. |
| 6 | חוזרים על התהליך עד שקבוצת המוכנים ריקה. |
דוגמה מפורטת
מיון ה-DAG עם הקשתות A→C, B→C, C→D, C→E, D→F, E→F (דרגות כניסה התחלתיות A:0 B:0 C:2 D:1 E:1 F:2):
| צעד | קבוצת המוכנים | סדר | פעולה |
|---|---|---|---|
| 0 | {A, B} | [] | ל-A ול-B יש דרגת כניסה 0 בהתחלה, ולכן שניהם מוכנים. |
| 1 | {B} | [A] | פולטים את A; הקשת A→C מורידה את דרגת הכניסה של C מ-2 ל-1. |
| 2 | {C} | [A, B] | פולטים את B; הקשת B→C מורידה את C מ-1 ל-0, ולכן C נעשה מוכן. |
| 3 | {D, E} | [A, B, C] | פולטים את C; הקשתות C→D ו-C→E מורידות את D ואת E ל-0, ושניהם נעשים מוכנים. |
| 4 | {E} | [A, B, C, D] | פולטים את D; הקשת D→F מורידה את דרגת הכניסה של F מ-2 ל-1. |
| 5 | {F} | [A, B, C, D, E] | פולטים את E; הקשת E→F מורידה את F מ-1 ל-0, ולכן F נעשה מוכן. |
| 6 | {} | [A, B, C, D, E, F] | פולטים את F; קבוצת המוכנים ריקה וכל 6 הצמתים מסודרים. סיימנו. |
מתי להשתמש במיון טופולוגי
| כדאי כאשר | עדיף להימנע כאשר |
|---|---|
| צריך סדר שמכבד תלויות (שלבי בנייה, התקנת חבילות, דרישות קדם של קורסים). | הגרף לא מכוון: סדר טופולוגי מוגדר רק לגרפים מכוונים. |
| הגרף הוא DAG ורוצים סידור ליניארי תקין כלשהו. | הגרף עלול להכיל מעגלים ובכל זאת צריך סדר מלא (אין כזה). |
| רוצים לזהות מעגלים בזול: מיון טופולוגי שנכשל מוכיח שקיים מעגל. | צריך את הסדר הקצר ביותר או האופטימלי לפי משקל כלשהו; מיון טופולוגי רגיל מתעלם ממשקלים. |
מעבדים את הסדר פעם אחת ב-O(V + E). | הקשתות משתנות כל הזמן וצריך למיין מחדש בכל עדכון, ושם מבנה אינקרמנטלי מתאים יותר. |
קוד Topological Sort
מימוש נקי של Topological Sort שאפשר להריץ, ב-Python, JavaScript, Java, C++, C. בחרו שפה, העתיקו את הקוד, או פתחו אותו טעון מראש בעורך האונליין של Coddy.
קוד Topological Sort ב-Python
1from collections import deque2
3
4def topological_sort(graph):5 # Kahn's algorithm: repeatedly remove nodes with no incoming edges6 in_degree = {node: 0 for node in graph}7 for node in graph:8 for neighbor in graph[node]:9 in_degree[neighbor] += 110 queue = deque(node for node in graph if in_degree[node] == 0)11 order = []12 while queue:13 node = queue.popleft()14 order.append(node)15 for neighbor in graph[node]:16 in_degree[neighbor] -= 117 if in_degree[neighbor] == 0:18 queue.append(neighbor)19 if len(order) != len(graph):20 raise ValueError("Graph has a cycle, no topological order")21 return order22
23
24graph = {25 "shirt": ["tie", "jacket"],26 "tie": ["jacket"],27 "pants": ["shoes", "jacket"],28 "socks": ["shoes"],29 "shoes": [],30 "jacket": [],31}32
33print(" -> ".join(topological_sort(graph)))קוד Topological Sort ב-JavaScript
1const graph = {2 A: ["C"],3 B: ["C", "D"],4 C: ["E"],5 D: ["F"],6 E: ["F"],7 F: [],8};9
10// Kahn algorithm: repeatedly take a node with no incoming edges11function topologicalSort() {12 const inDegree = {};13 for (const node in graph) inDegree[node] = 0;14 for (const node in graph) {15 for (const next of graph[node]) inDegree[next]++;16 }17 const queue = Object.keys(inDegree).filter((n) => inDegree[n] === 0);18 const order = [];19 while (queue.length > 0) {20 const node = queue.shift();21 order.push(node);22 for (const next of graph[node]) {23 if (--inDegree[next] === 0) queue.push(next);24 }25 }26 if (order.length !== Object.keys(graph).length) {27 throw new Error("Graph has a cycle");28 }29 return order;30}31
32console.log("Topological order:", topologicalSort().join(" -> "));קוד Topological Sort ב-Java
1import java.util.ArrayDeque;2import java.util.List;3import java.util.Queue;4
5public class Main {6 public static void main(String[] args) {7 int n = 6;8 // DAG: an edge u -> v means u must come before v9 List<List<Integer>> adj = List.of(10 List.of(2),11 List.of(2, 3),12 List.of(4),13 List.of(4),14 List.of(5),15 List.of()16 );17 int[] inDegree = new int[n];18 for (List<Integer> next : adj) {19 for (int v : next) inDegree[v]++;20 }21
22 // Kahn: start from every node with no prerequisites23 Queue<Integer> queue = new ArrayDeque<>();24 for (int i = 0; i < n; i++) {25 if (inDegree[i] == 0) queue.add(i);26 }27
28 StringBuilder order = new StringBuilder("Topological order:");29 while (!queue.isEmpty()) {30 int node = queue.poll();31 order.append(" ").append(node);32 for (int next : adj.get(node)) {33 if (--inDegree[next] == 0) queue.add(next);34 }35 }36 System.out.println(order);37 }38}קוד Topological Sort ב-C++
1#include <iostream>2#include <queue>3#include <vector>4
5int main() {6 // Directed acyclic graph: adj[u] lists nodes that depend on u7 std::vector<std::vector<int>> adj = {8 {}, // 09 {}, // 110 {3}, // 2 -> 311 {1}, // 3 -> 112 {0, 1}, // 4 -> 0, 113 {0, 2}, // 5 -> 0, 214 };15 int n = static_cast<int>(adj.size());16 std::vector<int> inDegree(n, 0);17 for (const auto& neighbors : adj) {18 for (int v : neighbors) ++inDegree[v];19 }20 // Kahn's algorithm: start from nodes with no incoming edges21 std::queue<int> ready;22 for (int u = 0; u < n; ++u) {23 if (inDegree[u] == 0) ready.push(u);24 }25 std::cout << "Topological order: ";26 while (!ready.empty()) {27 int u = ready.front();28 ready.pop();29 std::cout << u << " ";30 // Removing u unlocks neighbors whose in-degree drops to 031 for (int v : adj[u]) {32 if (--inDegree[v] == 0) ready.push(v);33 }34 }35 std::cout << "\n";36 return 0;37}קוד Topological Sort ב-C
1#include <stdio.h>2
3#define N 64
5int main(void) {6 // Directed acyclic graph: adj[u][v] = 1 means an edge u -> v7 int adj[N][N] = {0};8 adj[2][3] = 1;9 adj[3][1] = 1;10 adj[4][0] = 1;11 adj[4][1] = 1;12 adj[5][0] = 1;13 adj[5][2] = 1;14 int inDegree[N] = {0};15 for (int u = 0; u < N; u++) {16 for (int v = 0; v < N; v++) inDegree[v] += adj[u][v];17 }18 // Kahn's algorithm: start from nodes with no incoming edges19 int queue[N];20 int head = 0, tail = 0;21 for (int u = 0; u < N; u++) {22 if (inDegree[u] == 0) queue[tail++] = u;23 }24 printf("Topological order: ");25 while (head < tail) {26 int u = queue[head++];27 printf("%d ", u);28 // Removing u unlocks neighbors whose in-degree drops to 029 for (int v = 0; v < N; v++) {30 if (adj[u][v] && --inDegree[v] == 0) queue[tail++] = v;31 }32 }33 printf("\n");34 return 0;35}שאלות נפוצות על מיון טופולוגי
למה משמשים מיון טופולוגי?
מהי סיבוכיות הזמן של מיון טופולוגי?
O(V + E), כי כל צומת מעובד פעם אחת וכל קשת נבדקת פעם אחת. הם משתמשים בזיכרון נוסף של O(V).למה מיון טופולוגי דורש DAG?
a חייב לבוא לפני b ו-b חייב לבוא לפני a, אף סדר ליניארי לא מקיים את שני התנאים. לכן סדר טופולוגי קיים אם ורק אם הגרף הוא גרף מכוון חסר מעגלים. האלגוריתם של Kahn מזהה מעגל כשהוא מסיים לפני שפלט את כל הצמתים.מה ההבדל בין האלגוריתם של Kahn למיון טופולוגי עם DFS?
O(V + E); Kahn נמנע מרקורסיה עמוקה ומציג את קבוצת המוכנים באופן טבעי, ואילו DFS קצר יותר לכתיבה פעמים רבות.מתי להשתמש במיון טופולוגי במקום במיון רגיל?
O(n log n) מסדר לפי ערך; מיון טופולוגי מסדר לפי קשתות של "חייב לבוא לפני", ובניגוד למיון השוואה הוא יכול להפיק תשובות תקינות רבות לאותו קלט.