الفرز الطوبولوجي
آخر تحديث
الفرز الطوبولوجي لرسم بياني موجّه لا دوري (DAG) هو ترتيب خطي لعُقده بحيث تأتي u قبل v لكل حافة u → v. يجيب على أسئلة مثل «بأي ترتيب يمكنني تشغيل هذه المهام بحيث ينتهي كل شرط مسبق أولاً؟». اضغط على تشغيل أعلاه لمشاهدة خوارزمية كان تستخرج العُقد بترتيب صحيح.
تأخذ خوارزمية كان بشكل متكرر عقدة لا تملك حواف داخلة متبقية (درجة دخول 0)، وتضيفها إلى الترتيب وتزيل حوافها الخارجة - ما قد يحرر عُقدًا جديدة بدرجة دخول 0. تعمل على DAG فقط: إذا وُجدت دورة فلن تصل بعض العُقد أبدًا إلى درجة دخول 0 ولا يوجد ترتيب صحيح. تعمل في زمن O(V + E).
التعقيد الزمني والمكاني
| المقياس | التعقيد | ملاحظات |
|---|---|---|
| الزمن | O(V + E) | كل عقدة تُخرَج مرة واحدة، وكل حافة تُزال مرة واحدة |
| المكان | O(V) | عدادات درجة الدخول + المجموعة الجاهزة + الترتيب |
| يتطلب | رسمًا بيانيًا DAG | الدورات ليس لها فرز طوبولوجي |
| النتيجة | غير وحيدة | قد توجد عدة ترتيبات صحيحة |
خطوة بخطوة (خوارزمية كان)
| الخطوة | ما يحدث |
|---|---|
| 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؛ المجموعة الجاهزة فارغة وكل العُقد الست مرتبة - انتهى. |
متى تستخدم الفرز الطوبولوجي
| استخدمه عندما | تجنّبه عندما |
|---|---|
| تحتاج إلى ترتيب يحترم التبعيات (خطوات البناء، تثبيت الحزم، المتطلبات المسبقة للدورات). | يكون الرسم البياني غير موجّه - الترتيب الطوبولوجي معرّف فقط للرسوم الموجّهة. |
| يكون الرسم البياني 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، فلا يوجد ترتيب خطي يحقق كليهما. لذا يوجد فرز طوبولوجي إذا وفقط إذا كان الرسم البياني موجّهًا لا دوريًا. تكتشف خوارزمية كان دورة عندما تنتهي قبل إخراج جميع العُقد.ما الفرق بين خوارزمية كان والفرز الطوبولوجي المبني على DFS؟
O(V + E)؛ يتجنب كان التعاود العميق ويكشف المجموعة الجاهزة بشكل طبيعي، بينما DFS غالبًا أقصر في الكتابة.متى ينبغي أن أستخدم الفرز الطوبولوجي بدلاً من الفرز العادي؟
O(n log n) حسب القيمة؛ أما الفرز الطوبولوجي فيرتّب حسب حواف «يجب أن يأتي قبل»، وخلافًا لفرز المقارنة يمكنه إنتاج عدة إجابات صحيحة للمدخل نفسه.