Zykluserkennung in gerichteten und ungerichteten Graphen
Erkennen Sie Zyklen in ungerichteten Graphen mit Elternverfolgung und in gerichteten Graphen mit DFS-Farbmarkierung (drei Zustände: weiß/grau/schwarz).
Zykluserkennung in gerichteten und ungerichteten Graphen ist eine kostenlose Coding Interview Prep-Lektion auf CoddyKit. Dies ist Lektion 4 von 4. Du kannst die komplette Lektion unten kostenlos lesen – dann übst du sie direkt im Browser mit einem integrierten Code-Editor und einem KI-Tutor rund um die Uhr. Sie ist Teil des Coding Interview Prep-Lernpfads, und dein Fortschritt wird über Web und CoddyKit-App synchronisiert. Der Coding Interview Prep-Kurs umfasst insgesamt 4 Lektionen.
Warum Zykluserkennung wichtig ist
Ein Zyklus in einem Graphen ist ein Pfad, der am selben Knoten beginnt und endet. Die Zykluserkennung ist für viele Algorithmen entscheidend: Eine topologische Sortierung schlägt bei zyklischen Graphen fehl, bei der Auflösung von Abhängigkeiten müssen zirkuläre Abhängigkeiten erkannt werden, und die Deadlock-Erkennung bei der Betriebssystemplanung erfordert das Finden von Zyklen in Ressourcenzuweisungsgraphen. Der Ansatz unterscheidet sich bei ungerichteten und gerichteten Graphen – dafür sind grundsätzlich unterschiedliche Algorithmen erforderlich.
from collections import defaultdict
# Undirected cycle: A-B-C-A (triangle)
undirected = defaultdict(list)
for u, v in [('A','B'),('B','C'),('C','A')]:
undirected[u].append(v)
undirected[v].append(u)
# Directed cycle: A->B->C->A
directed = defaultdict(list)
for u, v in [('A','B'),('B','C'),('C','A')]:
directed[u].append(v) # one direction only
# Key difference:
# Undirected: edge A-B appears as both A->B and B->A
# Must track parent to distinguish cycle from back-edge to parent
print('Undirected and directed cycles need different detection')Zykluserkennung in ungerichteten Graphen mit DFS
In einem ungerichteten Graphen existiert ein Zyklus, wenn die DFS einen Knoten besucht, der sich bereits im aktuellen Pfad befindet (nicht nur besucht wurde). Die Herausforderung besteht darin, dass jede Kante in beide Richtungen vorkommt. Wenn wir einen Kindknoten besuchen, enthält seine Nachbarliste daher unseren aktuellen Knoten (den Elternknoten). Wir müssen den Elternknoten jedes Knotens verfolgen, um die Kante zurück zum Elternknoten nicht fälschlicherweise als Zyklus zu kennzeichnen. Wenn wir auf einen besuchten Knoten stoßen, der nicht unser Elternknoten ist, haben wir einen Zyklus gefunden.
def has_cycle_undirected(n, edges):
from collections import defaultdict
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
visited = set()
def dfs(node, parent):
visited.add(node)
for nb in graph[node]:
if nb not in visited:
if dfs(nb, node): # recurse with current as parent
return True
elif nb != parent: # visited and not parent = CYCLE
return True
return False
for node in range(n):
if node not in visited:
if dfs(node, -1): # -1 = no parent for root
return True
return False
print(has_cycle_undirected(4, [(0,1),(1,2),(2,3),(3,1)])) # True
print(has_cycle_undirected(3, [(0,1),(1,2)])) # FalseZyklus in ungerichteten Graphen mit BFS
Die Zykluserkennung mit BFS in einem ungerichteten Graphen verfolgt ebenfalls den Elternknoten jedes besuchten Knotens. Beim Verarbeiten der Nachbarn eines Knotens gilt: Ist ein Nachbar bereits besucht und nicht der Elternknoten des aktuellen Knotens, existiert ein Zyklus. Verwenden Sie ein Dictionary zum Speichern der Elternknoten. Dieser Ansatz mit O(V + E) vermeidet das Rekursionslimit und ist die bevorzugte iterative Alternative für große Graphen.
from collections import deque, defaultdict
def has_cycle_bfs_undirected(n, edges):
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
visited = set()
for start in range(n):
if start in visited:
continue
visited.add(start)
parent = {start: -1}
queue = deque([start])
while queue:
node = queue.popleft()
for nb in graph[node]:
if nb not in visited:
visited.add(nb)
parent[nb] = node
queue.append(nb)
elif parent[node] != nb: # visited and not parent = CYCLE
return True
return False
print(has_cycle_bfs_undirected(4, [(0,1),(1,2),(2,0)])) # TrueZyklus in gerichteten Graphen: Warum die Verfolgung von Elternknoten nicht funktioniert
In einem gerichteten Graphen reicht die Verfolgung von Elternknoten nicht aus. Betrachten Sie A→C und B→C: Knoten C hat zwei 'Eltern', aber keinen Zyklus. Der richtige Ansatz verwendet eine Färbung mit drei Zuständen: weiß (unbesucht), grau (im aktuellen DFS-Pfad/Stack) und schwarz (vollständig verarbeitet). Ein Zyklus existiert, wenn wir während der DFS auf einen grauen Knoten stoßen – das bedeutet, dass wir eine Rückwärtskante zu einem Vorfahren im aktuellen Pfad gefunden haben.
# Three-state DFS coloring:
# WHITE (0): not yet visited
# GRAY (1): currently being visited (in DFS stack)
# BLACK (2): fully visited (all descendants processed)
# Why parent fails for directed graphs:
# A -> C (no cycle)
# B -> C (no cycle)
# If we DFS from A, mark C gray
# Then DFS from B finds C is gray -- but this is NOT a cycle!
# C is gray from A's path, not B's path.
# Parent tracking only works when the back-edge goes to the IMMEDIATE parent.
print('Directed graph: use 3-state coloring (white/gray/black)')Zykluserkennung in gerichteten Graphen mit 3-Zustände-DFS
Verwenden Sie ein Array state[] mit den Werten 0 (weiß/unbesucht), 1 (grau/im Stack) und 2 (schwarz/fertig). Starten Sie die DFS, markieren Sie den Knoten beim Betreten grau und beim Verlassen schwarz. Wenn die DFS einen grauen Knoten erreicht, wurde eine Rückwärtskante gefunden – es existiert ein Zyklus. Erreicht sie einen schwarzen Knoten, wurde dieser Pfad bereits vollständig und zyklusfrei untersucht, sodass Sie ihn überspringen können.
def has_cycle_directed(n, edges):
from collections import defaultdict
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
state = [0] * n # 0=white, 1=gray, 2=black
def dfs(node):
state[node] = 1 # mark gray (in stack)
for nb in graph[node]:
if state[nb] == 1: # gray = back edge = CYCLE
return True
if state[nb] == 0: # white = unvisited
if dfs(nb):
return True
state[node] = 2 # mark black (fully processed)
return False
for node in range(n):
if state[node] == 0:
if dfs(node):
return True
return False
print(has_cycle_directed(4, [(0,1),(1,2),(2,0),(2,3)])) # True (0->1->2->0)
print(has_cycle_directed(3, [(0,1),(1,2)])) # FalseCourse Schedule: Zyklus in einem DAG
Course Schedule (LeetCode #207) fragt, ob alle Kurse unter Berücksichtigung der Voraussetzungen abgeschlossen werden können. Modellieren Sie die Kurse als Knoten und die Voraussetzungen als gerichtete Kanten. Alle Kurse können genau dann abgeschlossen werden, wenn der Graph ein DAG ist (keine Zyklen enthält). Verwenden Sie die Zykluserkennung mit einer 3-Zustände-DFS – wird ein Zyklus gefunden, geben Sie False zurück, andernfalls True.
from collections import defaultdict
def can_finish(num_courses, prerequisites):
graph = defaultdict(list)
for a, b in prerequisites:
graph[b].append(a) # b is prerequisite for a: b -> a
state = [0] * num_courses
def dfs(course):
if state[course] == 1: return False # cycle!
if state[course] == 2: return True # already verified
state[course] = 1 # mark as in-progress
for next_course in graph[course]:
if not dfs(next_course):
return False
state[course] = 2 # mark as done
return True
return all(dfs(i) for i in range(num_courses) if state[i] == 0)
print(can_finish(2, [[1,0]])) # True: take 0 then 1
print(can_finish(2, [[1,0],[0,1]])) # False: circular dependencyZykluserkennung mit Kahn's Algorithmus (BFS)
Eine alternative Zykluserkennung für gerichtete Graphen verwendet Kahns BFS für die topologische Sortierung. Zählen Sie die Eingangsgrade aller Knoten. Legen Sie Knoten mit Eingangsgrad 0 in eine Queue. Verarbeiten Sie jeden Knoten: Verringern Sie die Eingangsgrade seiner Nachbarn und fügen Sie diejenigen zur Queue hinzu, deren Eingangsgrad 0 erreicht. Entspricht die Anzahl der verarbeiteten Knoten V, gibt es keinen Zyklus; andernfalls existiert ein Zyklus (die unverarbeiteten Knoten bilden Zyklen). Dieser Ansatz mit O(V + E) ist intuitiv und leichter zu merken als eine 3-Zustände-DFS.
from collections import defaultdict, deque
def has_cycle_kahn(n, edges):
graph = defaultdict(list)
in_degree = [0] * n
for u, v in edges:
graph[u].append(v)
in_degree[v] += 1
# Start with all zero in-degree nodes
queue = deque(i for i in range(n) if in_degree[i] == 0)
processed = 0
while queue:
node = queue.popleft()
processed += 1
for nb in graph[node]:
in_degree[nb] -= 1
if in_degree[nb] == 0:
queue.append(nb)
return processed != n # if not all processed, cycle exists
print(has_cycle_kahn(4, [(0,1),(1,2),(2,0),(2,3)])) # True
print(has_cycle_kahn(3, [(0,1),(1,2)])) # FalseDen Zyklus finden: Zyklusknoten sammeln
Manchmal müssen Sie ermitteln, welche Knoten zu einem Zyklus gehören, statt nur dessen Existenz festzustellen. Wenn während einer 3-Zustände-DFS eine Rückwärtskante gefunden wird, verfolgen Sie den Aufruf-Stack (oder einen Pfad-Stack) zurück, um alle Knoten zwischen dem Vorfahren und dem aktuellen Knoten zu sammeln. Ein Pfad-Stack, der parallel zum Zustandsarray verwaltet wird, erfasst den aktuellen DFS-Pfad und ermöglicht die Rekonstruktion des Zyklus in O(Zykluslänge).
def find_cycle_nodes(n, edges):
from collections import defaultdict
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
state = [0] * n
path = [] # current DFS path
cycle = []
def dfs(node):
state[node] = 1
path.append(node)
for nb in graph[node]:
if state[nb] == 1: # back edge -> found cycle
start = path.index(nb)
cycle.extend(path[start:])
return True
if state[nb] == 0 and dfs(nb):
return True
path.pop()
state[node] = 2
return False
for i in range(n):
if state[i] == 0 and dfs(i):
break
return cycle
print(find_cycle_nodes(4, [(0,1),(1,2),(2,0),(2,3)])) # [0, 1, 2]Eventuell sichere Zustände finden
Eventuell sichere Zustände finden (LeetCode #802) fragt, welche Knoten schließlich zu einem Endknoten (ohne ausgehende Kanten) führen, ohne in einem Zyklus stecken zu bleiben. Ein Knoten ist 'sicher', wenn alle von ihm ausgehenden Pfade zu Endknoten führen. Verwenden Sie eine 3-Zustände-DFS: Schwarze Knoten (vollständig verarbeitet, ohne einen Zyklus zu erkennen) sind sicher. Knoten, die Teil eines Zyklus sind oder zu einem Zyklus führen, sind nicht sicher.
def eventual_safe_nodes(graph):
n = len(graph)
state = [0] * n # 0=unvisited, 1=visiting, 2=safe
def dfs(node):
if state[node] == 1: # currently visiting = cycle
return False
if state[node] == 2: # already verified safe
return True
state[node] = 1 # mark as visiting
for nb in graph[node]:
if not dfs(nb):
return False # leads to cycle, not safe
state[node] = 2 # mark as safe
return True
return [i for i in range(n) if dfs(i)]
# [[1,2],[2,3],[5],[0],[5],[],[]] means:
# 0->[1,2], 1->[2,3], 2->[5], 3->[0] (cycle!), 4->[5], 5->[], 6->[]
print(eventual_safe_nodes([[1,2],[2,3],[5],[0],[5],[],[]]))
# [2, 4, 5, 6]Redundante Kante in einem ungerichteten Graphen
Redundant Connection (LeetCode #684) findet die Kante, die beim Hinzufügen zu einem ansonsten azyklischen ungerichteten Graphen einen Zyklus erzeugt. Dies lässt sich zwar mit einer DFS-Zykluserkennung lösen, die klarste Lösung verwendet jedoch Union-Find (DSU): Verarbeiten Sie die Kanten nacheinander. Sind beide Endpunkte bereits verbunden (gehören sie zur selben Komponente), erzeugt die aktuelle Kante einen Zyklus und ist die gesuchte Antwort. DSU benötigt O(alpha(n)) pro Operation – effektiv O(1).
def find_redundant_connection(edges):
n = len(edges)
parent = list(range(n + 1))
rank = [0] * (n + 1)
def find(x):
if parent[x] != x:
parent[x] = find(parent[x]) # path compression
return parent[x]
def union(x, y):
px, py = find(x), find(y)
if px == py:
return False # already connected = cycle!
if rank[px] < rank[py]: px, py = py, px
parent[py] = px
if rank[px] == rank[py]: rank[px] += 1
return True
for u, v in edges:
if not union(u, v):
return [u, v] # this edge creates the cycle
return []
print(find_redundant_connection([[1,2],[1,3],[2,3]])) # [2,3]
print(find_redundant_connection([[1,2],[2,3],[3,4],[1,4],[1,5]])) # [1,4]Zusammenfassung: Strategien zur Zykluserkennung
Fassen wir die Werkzeuge zur Zykluserkennung zusammen: Für ungerichtete Graphen verwenden Sie DFS mit Verfolgung der Elternknoten oder Union-Find. Für gerichtete Graphen verwenden Sie eine 3-Zustände-DFS (weiß/grau/schwarz) oder Kahns BFS für die topologische Sortierung. Wählen Sie Union-Find, wenn Sie Kanten nacheinander hinzufügen (online). Wählen Sie Kahn's Algorithmus, wenn Sie zusätzlich die topologische Reihenfolge benötigen. Wählen Sie eine 3-Zustände-DFS, wenn Sie die konkreten Zyklusknoten identifizieren müssen. Stellen Sie in Interviews zur Zykluserkennung immer den Unterschied zwischen gerichteten und ungerichteten Graphen heraus.
# Cycle detection summary:
# Graph type | Algorithm | Complexity
# ------------|----------------------|-----------
# Undirected | DFS + parent track | O(V + E)
# Undirected | Union-Find (DSU) | O(E * alpha(V))
# Directed | DFS 3-state (W/G/B) | O(V + E)
# Directed | Kahn's BFS topo sort | O(V + E)
# When to choose:
# Online (edges added one at a time): Union-Find
# Need topological order too: Kahn's BFS
# Need cycle nodes identified: 3-state DFS with path stack
# Simple existence check: any of the above
print('Always clarify directed vs undirected before coding')Kurzer Test
Testen Sie Ihr Verständnis der Konzepte aus Data Structures & Algorithms — Coding Interview Prep in dieser Lektion.
Zusammenfassung der Lektion
In dieser Lektion haben Sie Folgendes gelernt: die Zykluserkennung in ungerichteten Graphen mit einer DFS und Verfolgung der Elternknoten, die Zykluserkennung in gerichteten Graphen mit einer 3-Zustände-Färbung in Weiß/Grau/Schwarz, Kahns BFS-Alternative für gerichtete Graphen sowie Anwendungen wie Course Schedule, Redundant Connection und Eventual Safe States. Als Nächstes beschäftigen wir uns mit den Grundlagen der dynamischen Programmierung.
Häufig gestellte Fragen
Ist die Lektion „Zykluserkennung in gerichteten und ungerichteten Graphen“ kostenlos?
Ja — der vollständige Text von „Zykluserkennung in gerichteten und ungerichteten Graphen“ ist hier im Web kostenlos zu lesen. Um sie interaktiv zu üben (integrierter Code-Editor und 24/7 KI-Tutor) und den Rest des Coding Interview Prep-Kurses freizuschalten, upgrade auf CoddyKit PRO. Der Coding Interview Prep-Kurs umfasst insgesamt 4 Lektionen.
Was lerne ich in „Zykluserkennung in gerichteten und ungerichteten Graphen“?
Erkennen Sie Zyklen in ungerichteten Graphen mit Elternverfolgung und in gerichteten Graphen mit DFS-Farbmarkierung (drei Zustände: weiß/grau/schwarz). Du übst Coding Interview Prep mit praktischem Code, den du direkt im Browser ausführst, und ein 24/7 KI-Tutor beantwortet deine Fragen während du die Lektion bearbeitest.
Brauche ich Erfahrung, um Coding Interview Prep zu starten?
Keine Vorkenntnisse erforderlich. Coding Interview Prep auf CoddyKit ist für Anfänger bis fortgeschrittene Lernende strukturiert, sodass du hier starten oder von Anfang an beginnen und in deinem eigenen Tempo voranschreiten kannst. Dies ist Lektion 4 von 4.
Wie lange dauert die Lektion „Zykluserkennung in gerichteten und ungerichteten Graphen“?
Die meisten CoddyKit-Lektionen dauern etwa 5–10 Minuten. Jede ist kompakt und interaktiv, sodass du stetig Fortschritte machst und genau dort weitermachst, wo du aufgehört hast – im Web und in der App.
Kann ich in dieser Coding Interview Prep-Lektion Code schreiben und ausführen?
Ja. Jede Coding Interview Prep-Lektion enthält einen integrierten Code-Editor, sodass du echten Code direkt in deinem Browser schreibst und ausführst und sofort KI-Feedback erhältst — ohne lokale Einrichtung erforderlich.
Alle Lektionen in diesem Kurs
- Graphdarstellungen und Vorbereitung der Traversierung
- BFS: kürzester Pfad und Ebenendurchlauf
- DFS: Zusammenhangskomponenten und Flood Fill
- Zykluserkennung in gerichteten und ungerichteten Graphen