Sortowanie topologiczne
Ostatnia aktualizacja
Sortowanie topologiczne skierowanego grafu acyklicznego (DAG) to liniowe uporządkowanie jego wierzchołków takie, że dla każdej krawędzi u → v wierzchołek u występuje przed v. Odpowiada na pytania typu „w jakiej kolejności uruchomić te zadania, żeby każde zadanie wymagane zakończyło się wcześniej?”. Naciśnij Odtwórz powyżej i zobacz, jak algorytm Kahna zdejmuje wierzchołki w poprawnej kolejności.
Algorytm Kahna wielokrotnie bierze wierzchołek bez pozostałych krawędzi wchodzących (stopień wejściowy 0), dopisuje go do wyniku i usuwa jego krawędzie wychodzące, co może zwolnić kolejne wierzchołki o stopniu wejściowym 0. Działa tylko na grafie DAG: jeśli istnieje cykl, niektóre wierzchołki nigdy nie osiągną stopnia wejściowego 0 i poprawna kolejność nie istnieje. Złożoność czasowa to O(V + E).
Złożoność czasowa i pamięciowa
| Miara | Złożoność | Uwagi |
|---|---|---|
| Czas | O(V + E) | Każdy wierzchołek wypisany raz, każda krawędź usunięta raz |
| Pamięć | O(V) | Stopnie wejściowe + zbiór gotowych + wynik |
| Wymaga | Grafu DAG | Graf z cyklami nie ma porządku topologicznego |
| Wynik | Nie jest jednoznaczny | Może istnieć wiele poprawnych kolejności |
Krok po kroku (algorytm Kahna)
| Krok | Co się dzieje |
|---|---|
| 1 | Oblicz stopień wejściowy (liczbę krawędzi wchodzących) każdego wierzchołka. |
| 2 | Zbierz wszystkie wierzchołki o stopniu wejściowym 0 w zbiór gotowych. |
| 3 | Weź gotowy wierzchołek i dopisz go do wyniku. |
| 4 | Zmniejsz o jeden stopień wejściowy każdego z jego następników. |
| 5 | Każdy następnik, którego stopień wejściowy spadnie do 0, trafia do zbioru gotowych. |
| 6 | Powtarzaj, dopóki zbiór gotowych nie będzie pusty. |
Przykład krok po kroku
Sortujemy DAG z krawędziami A→C, B→C, C→D, C→E, D→F, E→F (początkowe stopnie wejściowe A:0 B:0 C:2 D:1 E:1 F:2):
| Krok | Zbiór gotowych | Kolejność | Działanie |
|---|---|---|---|
| 0 | {A, B} | [] | A i B mają na starcie stopień wejściowy 0, więc oba są gotowe. |
| 1 | {B} | [A] | Wypisz A; jego krawędź A→C obniża stopień wejściowy C z 2 → 1. |
| 2 | {C} | [A, B] | Wypisz B; jego krawędź B→C obniża C z 1 → 0, więc C staje się gotowy. |
| 3 | {D, E} | [A, B, C] | Wypisz C; krawędzie C→D i C→E obniżają D i E do 0, oba stają się gotowe. |
| 4 | {E} | [A, B, C, D] | Wypisz D; jego krawędź D→F obniża stopień wejściowy F z 2 → 1. |
| 5 | {F} | [A, B, C, D, E] | Wypisz E; jego krawędź E→F obniża F z 1 → 0, więc F staje się gotowy. |
| 6 | {} | [A, B, C, D, E, F] | Wypisz F; zbiór gotowych jest pusty, a wszystkie 6 wierzchołków ma już swoje miejsce: koniec. |
Kiedy używać sortowania topologicznego
| Używaj, gdy | Unikaj, gdy |
|---|---|
| Potrzebujesz kolejności, która respektuje zależności (etapy budowania, instalacja pakietów, wymagania wstępne kursów). | Graf jest nieskierowany: porządek topologiczny jest zdefiniowany tylko dla grafów skierowanych. |
| Graf jest grafem DAG i wystarczy ci dowolna poprawna kolejność liniowa. | Graf może zawierać cykle, a mimo to potrzebujesz porządku liniowego (taki nie istnieje). |
| Chcesz tanio wykrywać cykle: nieudane sortowanie topologiczne dowodzi, że cykl istnieje. | Potrzebujesz najkrótszej lub optymalnej kolejności według jakiejś wagi; zwykłe sortowanie topologiczne ignoruje wagi. |
Przetwarzasz kolejność jednorazowo w O(V + E). | Krawędzie ciągle się zmieniają i trzeba sortować od nowa po każdej zmianie, wtedy lepiej sprawdzi się struktura przyrostowa. |
Topological Sort: kod
Przejrzysta, gotowa do uruchomienia implementacja algorytmu Topological Sort w językach: Python, JavaScript, Java, C++, C. Wybierz język, skopiuj kod albo otwórz go od razu w edytorze online Coddy.
Topological Sort: kod (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: kod (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: kod (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: kod (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: kod (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}Sortowanie topologiczne: najczęstsze pytania
Do czego służy sortowanie topologiczne?
Układa zadania tak, aby każda zależność znalazła się przed tym, co jej potrzebuje. Praktyczne zastosowania to systemy budowania i menedżery pakietów (najpierw kompilacja zależności), planowanie kursów z wymaganiami wstępnymi oraz kolejność obliczania formuł w arkuszach kalkulacyjnych.
Jaka jest złożoność czasowa sortowania topologicznego?
O(V + E), ponieważ każdy wierzchołek jest przetwarzany raz, a każda krawędź sprawdzana raz. Potrzebują O(V) dodatkowej pamięci.Dlaczego sortowanie topologiczne wymaga grafu DAG?
a musi być przed b, a b musi być przed a, żadna kolejność liniowa nie spełni obu warunków. Dlatego porządek topologiczny istnieje wtedy i tylko wtedy, gdy graf jest skierowanym grafem acyklicznym. Algorytm Kahna wykrywa cykl, gdy kończy pracę przed wypisaniem wszystkich wierzchołków.Czym różni się algorytm Kahna od sortowania topologicznego przez DFS?
O(V + E); algorytm Kahna unika głębokiej rekurencji i naturalnie udostępnia zbiór gotowych, a DFS zwykle wymaga mniej kodu.Kiedy użyć sortowania topologicznego zamiast zwykłego sortowania?
O(n log n), porządkuje według wartości; sortowanie topologiczne porządkuje według krawędzi „musi być przed” i w przeciwieństwie do sortowania przez porównania może dać wiele poprawnych wyników dla tych samych danych.Czy wynik sortowania topologicznego jest jednoznaczny?
Zwykle nie. Gdy dwa lub więcej wierzchołków jest gotowych (stopień wejściowy 0) jednocześnie, można je wypisać w dowolnej kolejności, więc większość grafów DAG ma kilka poprawnych porządków topologicznych. Kolejność jest jednoznaczna tylko wtedy, gdy na każdym kroku gotowy jest dokładnie jeden wierzchołek, czyli gdy DAG tworzy pojedynczy łańcuch (ścieżkę Hamiltona).