Graphen

Graphen sind Teil der Graphentheorie und dienen als Datenstruktur zur Speicherung von Beziehungen zwischen Objekten.

allgemeine Definition

Graphen bestehen aus Knoten und Kanten. Dabei verbinden die Kanten einen Knoten mit einem anderem. Jeder Knoten kann eine beliebige Anzahl an Kanten haben, mit welchen er verbunden ist.

Das Königsberger Brückproblem

Eines der ersten Probleme im Hinblick auf Graphen stellt das Königsberger Brückenproblem dar. Dieses wurde von Leonard Euler aufgestellt und beschäftigt sich mit der Frage, ob man alle Brücken in der Stadt Königsberg in einer Tour überqueren kann, ohne dabei eine Brücke zwei mal zu überqueren.

Map-Koenigsberg
Von Bogdan Giuşcă - Public domain (PD),based on the image, CC BY-SA 3.0, Link

In der obigen Karte sind die Brücken mit Grün kenntlich gemacht. Um nun das Problem in einem Graphen zu veranschaulichen, werden die verschiedenen Stadtteile als Knoten, und die Brücken als Kanten definiert.

Graph-Koenigsberg
CC BY-SA 3.0, Link

Es stellt sich raus das es keine sogenannte Euler-Tour gibt, welche das vorhin genannte Kriterium erfüllt. Dies hängt mit dem Grad der Knoten zusammen. Also der Anzahl an Kanten, mit denen ein Knoten immer verbunden ist.

Dabei gilt: Eine Euler-Tour ist dann möglich, wenn jeder Knoten in dem Graph eine gerade Anzahl an Kanten hat. Jeder Grad also gerade ist.

Hamiltonkreisproblem

Das Hamiltonkreisproblem beruht auf dem von Sir William Rowan Hamilton erfundenen Brettspiel „Traveller’s Dodecahedron or A Voyage Round The World“.

Hamiltonkreis
Von Christoph Sommer - Eigenes Werk, CC BY-SA 3.0, Link

Bei dem Hamiltonproblem geht es darum, einen Weg über alle Knoten in dem Graph zu finden. Dabei darf allerdings nur jeder Knoten nur einmal in dem Weg vorkommen. Außerdem muss der Weg dort aufhören, wo er auch angefangen hat. Eine mögliche Lösung für das Problem ist ebenfalls im obigen Bild dargestellt.

Im Gegensatz um Königsberger Brückenproblem sind bei diesem Problem die Knoten im Fokus. Außerdem gehört das Hamiltonkreisproblem zur Klasse der NP-Probleme (Probleme, welche sich nur in nicht-polynomialer Zeit lösen lassen). Damit ist es ein Problem, welches sich nur sehr schwer lösen lässt.

Umsetzung

Grundsätzlich gibt es zwei Möglichkeiten, einen Graphen zu speichern:

1. Adjazenzliste

Eine solche Liste wird durch die List-Datenstruktur realisiert. Dabei wird in eine Liste jeder Knoten gespeichert. Für jeden Knoten wird dann in einer weiteren Liste gespeichert, mit welchen Knoten er verbunden ist.

Adjazenzlist-Implementation
Von Wdvorak - Eigenes Werk, CC0, Link

Die obige Liste ist folgendem Graphen zuzuordnen:

Adjazenzlist-Implementation-Graph
Von Wdvorak - Eigenes Werk, CC0, Link

Dieser Graph ist ungerichtet, da man erkennen kann, dass bei zwei Verbunden Knoten beide Knoten ihre Verbindung zum jeweiligen anderen speichern:

a -> e
e -> a

2. Adjazenzmatrix

Die Adjazenzmatrix speichert einen Graphen in Form eines zweidimensionalen Arrays.

Adjazenzmatrix-Implementation-Matrix

Die obige Matrix resultiert in folgendem Graph:

Adjazenzmatrix-Implementation-Graph
Von Tfr.didi - Eigenes Werk, CC BY-SA 3.0, Link

Dabei hat jeder Knoten jeweils eine Spalte und Zeile. Eine Verbindung zwischen zwei Knoten ensteht dadurch, dass in der Spalte des Knoten 1 und in der Zeile von Knoten 2 eine 1 steht. Ist dort eine 0 vorhanden, so verbindet keine Kante diese beiden Knoten. Sollte eine Gewichtung der Kanten vorliegen, so werden die Zahlen in der Matrix durch die entsprechenden Gewichtungen ersetzt. Sollte dann keine Verbindung vorhanden sein, so bleibt das entsprechende Kästchen leer.

Vor- und Nachteile

Der auffälligste Unterschied zwischen Matrix und Liste ist, dass bei einer Matrix durch das zweidimensionale Array keine Möglichkeit besteht, effektiv einen neuen Knoten einzufügen. Allerdings muss auch bedacht werden, dass eine Liste immer komplett durchlaufen werden muss, um zu überprüfen, ob eine Kante vorhanden ist. Dadurch ergibt sich:

Eine Adjazenzliste sollte bei einem Graphen mit vielen Schreiboperationen gewählt werden, während eine Ajdazenzmatrix bei schnellen Leseoperationen gewählt werden sollte.

Formale Definition

Ähnlich wie bei Automaten kann auch ein Graph durch eine formale Definition beschrieben werden. Dabei werden die Kanten als Verbindungen zwischen zwei Knoten angegeben:

Beispiel-Graph
Quelle: Moodle

Für den obigen Graph ergibt sich also folgende Definition:

ist die Menge aus Knoten:

ist die Menge aus Kanten und wird zweidimensional angegeben. Jede Kante ist dabei eine Menge aus Anfangs- und Endknoten.

Der Grad eines Knoten wird mit angegeben: bedeutet, dass der Knoten 1 einen Grad von 3 besitzt. Die maximale Anzahl an Kanten eines Knoten in einem ungerichteten Graphen wird durch die Gleichung beschrieben. Dabei ist die maximale Anzahl an Kanten, und die Anzahl an Knoten in dem Graph.

Wenn zwei Knoten miteinander verbunden sind, so werden sie als adjazent bezeichnet. Das Gegenteil dazu heißt inzident.

Kombinationen aus Kanten oder Knoten werden wie folgt beschrieben:

  • eine Kantenfolge ist ein Weg über Kanten, bei dem Kanten auch mehrfach vorkommen können.
  • ein Kantenzug ist eine Kantenfolge ohne Wiederholungen.
  • ein Weg ist ein Kantenzug ohne Wiederholungen von Knoten.
  • ein Kreis ist ein abgschlossener Weg.

Ein Graph wird dabei als zusammenhängend bezeichnet, wenn jeder Knoten von jedem anderem Knoten erreicht werden kann.

Implementierung

Eine Implementierung stellt das Land NRW bereit. Dabei werden der Graph, sowie die Knoten und Kanten jeweils als Objekt dargestellt. Im folgenden werden die Implementierung in einem Implementationsdiagramm visualisiert.

Graph

Graphen durchsuchen

Ähnlich wie bei den Binärbäumen, gibt es auch bei einem Graphen Möglichkeiten diesen zu durchsuchen. Dabei wird ein Startknoten festgelegt, bei dem die Suche beginnt. Im folgenden werden die Algorithmen für das komplett Dursuchen eines Graphen vorgestellt. Sie könnten aber sehr leicht so abgewandelt werden, dass überprüft wird, ob ein Weg von einem Knoten zu einem anderen besteht.

Breitendurchlauf

Bei dem Breitendurchlauf werden erste alle Nachbarknoten des aktuellen Knoten gespeichert. Für jeden dieser Knoten werden dann wieder alle Nachbarknoten gespeichert.

Pseudocode

MARKIERE Startknoten v
FÜGE v in eine Schlange D EIN

SOLANGE D nicht leer:
    NEHME einen Knoten w aus D
    LÖSCHE w aus D

    FÜR jeden mit w verbundenen Knoten u:
        WENN u nicht markiert:
            MARKIERE u
            FÜGE u in D EIN
Breath-Search-GIF
From tutorialhorizon

Implementierung

Für die Implementierung werden die Klassen des Ministeriums verwendet. Neben der Graph,Vertex und Edge Klasse wird auch die Klasse Queue benötigt, da sie die Schlange D aus dem Pseudocode repräsentiert.

public String breadthSearch(Graph graph, String start) {
    graph.setAllVertexMarks(false);

    String result = "";
    Queue<Vertex> d = new Queue<>();
    Vertex startVertex = graph.getVertex(start);

    if (startVertex == null) {
        return "ERROR: Wrong start vertex.";
    }

    startVertex.setMark(true);

    d.enqueue(startVertex);

    while (!d.isEmpty()) {
        Vertex current = d.front();
        result += " " + current.getID();
        d.dequeue();

        List<Vertex> neighbors = graph.getNeighbours(current);

        for (neighbors.toFirst(); neighbors.hasAccess(); neighbors.next()) {
            Vertex neighbor = neighbors.getContent();

            if (!neighbor.isMarked()) {
                neighbor.setMark(true);
                d.enqueue(neighbor);
            }
        }
    }
    return result;
}

Tiefendurchlauf

Beim Tiefendurchlauf wird ein Pfad solange entlangegangen, bis ein Knoten erreicht wird, der keine weiteren noch nicht besichtigen Nachbarknoten hat. In diesem Fall geht man einen Knoten zurück, und geht einen anderen Pfad entlang, bis alle erreichbaren Knoten markiert wurden.

Pseudocode

MARKIERE Startknoten v
FÜGE v in eine Stapel D EIN

SOLANGE D nicht leer:
    NEHME einen Knoten w aus D
    LÖSCHE w aus D

    FÜR jeden mit w verbundenen Knoten u:
        WENN u nicht markiert:
            MARKIERE u
            FÜGE u zu D hinzu
Depthth-Search-GIF
From codeabbey

Implementierung

Bei der Implementierung wird auch die Klasse Stack für den Stapel D benötigt.

public String depthSearch(Graph graph, String start) {
    graph.setAllVertexMarks(false);

    String result = "";

    Stack<Vertex> d = new Stack<>();
    Vertex startVertex = graph.getVertex(start);

    if (startVertex == null) {
        return "ERROR: Wrong start vertex.";
    }

    startVertex.setMark(true);

    d.push(startVertex);

    while (!d.isEmpty()) {
        Vertex current = d.top();

        List<Vertex> neighbors = graph.getNeighbours(current);

        for (neighbors.toFirst(); neighbors.hasAccess(); neighbors.next()) {
            Vertex neighbor = neighbors.getContent();

            if (neighbor.isMarked()) continue;

            neighbor.setMark(true);
            d.push(neighbor);
        }

        // dem Stack wurde nichts hinzugefügt. Daher hat der aktuelle Knoten keine Nachbarn
        if (d.top().getID().equals(current.getID())) {
            result += " " + current.getID();
            d.pop();
        }

    }
    return result;
}