Tiefensuche in Java

1. Übersicht

In diesem Tutorial werden wir die Tiefensuche in Java untersuchen.

Die Tiefensuche (DFS) ist ein Traversal-Algorithmus, der sowohl für Tree- als auch für Graph-Datenstrukturen verwendet wird. Die Tiefensuche geht tief in jeden Zweig, bevor Sie einen anderen Zweig erkunden .

In den nächsten Abschnitten sehen wir uns zuerst die Implementierung eines Baums und dann eines Diagramms an.

Informationen zum Implementieren dieser Strukturen in Java finden Sie in unseren vorherigen Tutorials zu Binary Tree and Graph.

2. Baumtiefe zuerst suchen

Es gibt drei verschiedene Befehle zum Durchlaufen eines Baums mit DFS:

  1. Traversal vorbestellen
  2. Inorder Traversal
  3. Nachbestellungsdurchquerung

2.1. Traversal vorbestellen

Beim Vorbestellungsdurchlauf durchlaufen wir zuerst die Wurzel, dann den linken und den rechten Teilbaum.

Wir können die Vorbestellungsdurchquerung einfach mithilfe der Rekursion implementieren :

  • Besuchen Sie den aktuellen Knoten
  • Traverse links subtree
  • Durchqueren Sie den rechten Teilbaum
public void traversePreOrder(Node node) { if (node != null) { visit(node.value); traversePreOrder(node.left); traversePreOrder(node.right); } }

Wir können die Vorbestellungsüberquerung auch ohne Rekursion implementieren.

Um eine iterative Vorbestellungsdurchquerung zu implementieren, benötigen wir einen Stapel und führen die folgenden Schritte aus:

  • Drücken Wurzel in unserem s tack
  • Während der Stapel nicht leer ist
    • Pop aktuellen Knoten
    • Besuchen Sie den aktuellen Knoten
    • Schieben Sie das rechte Kind und dann das linke Kind zum Stapeln
public void traversePreOrderWithoutRecursion() { Stack stack = new Stack(); Node current = root; stack.push(root); while(!stack.isEmpty()) { current = stack.pop(); visit(current.value); if(current.right != null) { stack.push(current.right); } if(current.left != null) { stack.push(current.left); } } }

2.2. Inorder Traversal

Bei der Inorder Traversal durchlaufen wir zuerst den linken Teilbaum, dann die Wurzel und schließlich den rechten Teilbaum .

Inorder Traversal für einen binären Suchbaum bedeutet, die Knoten in aufsteigender Reihenfolge ihrer Werte zu durchlaufen.

Wir können die Inorder Traversal einfach mithilfe der Rekursion implementieren:

public void traverseInOrder(Node node) { if (node != null) { traverseInOrder(node.left); visit(node.value); traverseInOrder(node.right); } }

Wir können Inorder Traversal auch ohne Rekursion implementieren :

  • Schieben Sie den Wurzelknoten auf s Tack
  • Während s Tack nicht leer ist
    • Schieben Sie das linke Kind weiter auf den Stapel, bis wir das am weitesten links stehende Kind des aktuellen Knotens erreichen
    • Besuchen Sie den aktuellen Knoten
    • Schieben Sie das rechte Kind auf den Stapel
public void traverseInOrderWithoutRecursion() { Stack stack = new Stack(); Node current = root; stack.push(root); while(! stack.isEmpty()) { while(current.left != null) { current = current.left; stack.push(current); } current = stack.pop(); visit(current.value); if(current.right != null) { current = current.right; stack.push(current); } } }

2.3. Nachbestellungsdurchquerung

Schließlich durchlaufen wir bei der Nachbestellungsdurchquerung den linken und rechten Teilbaum, bevor wir die Wurzel durchqueren .

Wir können unserer vorherigen rekursiven Lösung folgen :

public void traversePostOrder(Node node) { if (node != null) { traversePostOrder(node.left); traversePostOrder(node.right); visit(node.value); } }

Oder wir können die Nachbestellungsüberquerung auch ohne Rekursion implementieren :

  • Push Wurzelknoten in s tack
  • Während s Tack nicht leer ist
    • Überprüfen Sie, ob wir den linken und rechten Teilbaum bereits durchlaufen haben
    • Wenn nicht, schieben Sie das rechte und das linke Kind auf den Stapel
public void traversePostOrderWithoutRecursion() { Stack stack = new Stack(); Node prev = root; Node current = root; stack.push(root); while (!stack.isEmpty()) { current = stack.peek(); boolean hasChild = (current.left != null || current.right != null); boolean isPrevLastChild = (prev == current.right || (prev == current.left && current.right == null)); if (!hasChild || isPrevLastChild) { current = stack.pop(); visit(current.value); prev = current; } else { if (current.right != null) { stack.push(current.right); } if (current.left != null) { stack.push(current.left); } } } }

3. Graphentiefe zuerst suchen

Der Hauptunterschied zwischen Diagrammen und Bäumen besteht darin, dass Diagramme Zyklen enthalten können .

Um eine Suche in Zyklen zu vermeiden, markieren wir jeden Knoten, wenn wir ihn besuchen.

Wir werden zwei Implementierungen für Graph DFS sehen, mit Rekursion und ohne Rekursion.

3.1. Graph DFS mit Rekursion

Beginnen wir zunächst einfach mit der Rekursion:

  • Wir beginnen an einem bestimmten Knoten
  • Markieren Sie den aktuellen Knoten als besucht
  • Besuchen Sie den aktuellen Knoten
  • Überqueren Sie nicht besuchte benachbarte Eckpunkte
public void dfs(int start) { boolean[] isVisited = new boolean[adjVertices.size()]; dfsRecursive(start, isVisited); } private void dfsRecursive(int current, boolean[] isVisited) { isVisited[current] = true; visit(current); for (int dest : adjVertices.get(current)) { if (!isVisited[dest]) dfsRecursive(dest, isVisited); } }

3.2. Diagramm DFS ohne Rekursion

Wir können Graph DFS auch ohne Rekursion implementieren. Wir werden einfach einen Stapel verwenden :

  • Wir beginnen an einem bestimmten Knoten
  • Schieben Sie den Startknoten in den Stapel
  • Während Stapel nicht leer
    • Markieren Sie den aktuellen Knoten als besucht
    • Besuchen Sie den aktuellen Knoten
    • Schieben Sie nicht besuchte benachbarte Eckpunkte
public void dfsWithoutRecursion(int start) { Stack stack = new Stack(); boolean[] isVisited = new boolean[adjVertices.size()]; stack.push(start); while (!stack.isEmpty()) { int current = stack.pop(); isVisited[current] = true; visit(current); for (int dest : adjVertices.get(current)) { if (!isVisited[dest]) stack.push(dest); } } }

3.4. Topologische Sortierung

Es gibt viele Anwendungen für die Tiefensuche in Diagrammen. Eine der bekanntesten Anwendungen für DFS ist die topologische Sortierung.

Die topologische Sortierung für einen gerichteten Graphen ist eine lineare Reihenfolge seiner Scheitelpunkte, sodass für jede Kante der Quellknoten vor dem Ziel steht.

Um topologisch sortiert zu werden, benötigen wir eine einfache Ergänzung zu der gerade implementierten DFS:

  • Wir müssen die besuchten Scheitelpunkte in einem Stapel halten, da die topologische Sortierung die besuchten Scheitelpunkte in umgekehrter Reihenfolge ist
  • Wir schieben den besuchten Knoten erst auf den Stapel, nachdem wir alle seine Nachbarn durchlaufen haben
public List topologicalSort(int start) { LinkedList result = new LinkedList(); boolean[] isVisited = new boolean[adjVertices.size()]; topologicalSortRecursive(start, isVisited, result); return result; } private void topologicalSortRecursive(int current, boolean[] isVisited, LinkedList result) { isVisited[current] = true; for (int dest : adjVertices.get(current)) { if (!isVisited[dest]) topologicalSortRecursive(dest, isVisited, result); } result.addFirst(current); }

4. Fazit

In diesem Artikel haben wir die Tiefensuche sowohl für die Tree- als auch für die Graph-Datenstruktur erörtert.

Der vollständige Quellcode ist auf GitHub verfügbar.