Example 3 Architectural Design EINFACHER ROUTENPLANER – DESIGNDOKUMENT Einführung in die strukturierte Programmierung – Übungsbeispiel 3 Gruppenmitglieder Gruppe 18 Thomas Bernat [0530058] Andreas Hechenblaickner [0430217] Daniela Kejzar [0310129] Christoph Zehentner [0430513] Gruppe 18 Seite 1 Architectural Design 1 Example 3 Architectural Design Das zweite Übungsbeispiel „Gerichteter Graph“ wird um Funktionen zur Ermittlung eines gültigen Pfades zwischen zwei Knoten erweitert. 1.1 Beschreibung eines Pfades Ein Pfad zwischen zwei Knoten wird von einer Liste von Knoten beschrieben, zwischen denen Kanten mit entsprechender Richtung existieren. Abbildung 1.1.1: Gerichteten Graph mit mehreren Pfaden In der Abbildung sind Pfade zwischen den Knoten (a) und (e) möglich, die durch folgende Listen beschrieben werden: [ (a), (b), (c), (e) ] [ (a), (d), (e) ] [ (a), (b), (d), (e) ] Alle drei genannten Listen beschreiben gültige Pfade um von (a) nach (e) zu gelangen. Die Namen der Kanten spielen dabei keine Rolle und wurden deshalb in der Abbildung nicht eingezeichnet. Vorsicht besteht bei Schleifen im Graphen, wie etwa zwischen den Knoten (b) und (c). So würde zB auch [ (a), (b), (c), (b), (c), (b), (c), ..., (e) ] eine unendliche Menge von gültigen Pfaden von (a) nach (e) beschreiben. Seite 2 Gruppe 18 Example 3 1.2 Architectural Design Ermitteln des Pfades Es gilt einen beliebigen gültigen Pfad im Graphen zwischen gegebenem Startund Zielknoten zu finden. Diese Aufgabe wird rekursiv gelöst, indem zuerst der Startknoten in die Pfad-Liste aufgenommen wird, und dann für jede Kante die vom Startknoten ausgeht, eine neuer Startknoten ermittelt wird. Zwischen diesen neu gefunden Startknoten und dem alten Zielknoten wird wiederum versucht ein Pfad zu ermitteln, bis einer der zwei folgenden Kriterien eintrifft: 1. Start- und Zielknoten sind identisch. In diesem Fall wurde ein gültiger Pfad gefunden. Die Pfad-Liste enthält nun eine gültige Liste von Knoten um vom ursprünglichen Startknoten zum Zielknoten zu gelangen. 2. Vom Startknoten führen keine weiteren Kanten mehr weg. In diesem Fall befindet man sich in einer „Sackgasse“ und muss einen Schritt zurückgehen und es mit der nächsten Kante im vorherigen Aufruf probieren (Backtracking). Dazu wird der eben betrachtete Knoten wieder aus der Pfad-Liste entfernt (er befindet sich immer an der letzten Stelle). Ein gültiger Pfad wurde gefunden, wenn Fall (1) eintritt. Kommt dieser Fall niemals vor, gibt es keinen gültigen Pfad zwischen Start- und Zielknoten in dem Graphen. Anmerkung: Es wird dabei nicht der kürzeste, sondern der als erstes eingefügte Pfad gefunden! Besondere Vorsicht besteht bei Schleifen im Graphen (siehe oben). Um diese im Pfad von vornherein auszuschließen, muss zunächst überprüft werden, ob sich der aktuelle Startknoten bereits in der Pfad-Liste befindet. Ist dies der Fall, würden weitere rekursive Aufrufe zu einer endlosen Schleife führen. Es ist daher notwendig das beschriebene Backtracking sofort durchzuführen. Gruppe 18 Seite 3 Architectural Design Example 3 Struktogramm Aus den obigen Überlegungen ergibt sich folgendes Struktogramm als Beschreibung eines möglichen Lösungsweges: ErmittlePfad (Startknoten, Zielknoten) Startknoten noch nicht in Pfad enthalten? Ja Nein Startknoten zu Pfad hinzufügen Startkonten = Zielknoten? Ja Nein Startknoten zu Pfad hinzufügen für alle Knoten zu den Kanten von Startknoten existieren Rückgabe TRUE ErmittlePfad (Knoten, Zielknoten)? Ja Rückgabe TRUE Nein Letzten Knoten in Pfad entfernen Rückgabe FALSE Rückgabe FALSE Abbildung 1.2.1: Rekursives ermitteln eines Pfades – Struktogramm 1.3 Verwendete Datenstruktur Ein Pfad wird in einem dynamischen Array von Zeigern auf Knoten gespeichert. Beim ermitteln eines Pfades muss bei jedem Funktionsaufruf ein Element an letzter Stelle hinzugefügt werden können. Beim Backtracking wird jeweils das zuletzt eingefügte Element wieder entfernt. Die Datenstruktur entspricht daher einem Stapel. Structure 1 typedef struct Path_ * Path; 2 3 struct Path_ { 4 int length; // aktuelle Anzahl der Elemente des Pfades 5 Node * nodeList; // Array von Knoten 6 }; Seite 4 Gruppe 18 Example 3 Modul Path 2 Modul Path 2.1 Funktionalität Erstellen eines neuen Pfades Speicher für eine neue leere Pfad-Liste wird angelegt und initialisiert. Diese Funktion entspricht dem Prinzip eines Konstruktors. Kann der notwendige Speicher nicht angefordert werden, tritt der Fehler ERROR_NOT_ENOUGH_MEMORY auf. Funktionsheader Path newPath(); Pfad zerstören Gibt den verwendeten Speicher des übergebenen Pfades frei. Diese Funktion entspricht dem Prinzip eines Destruktors. Funktionsheader void freePath(Path path); Knoten an letzter Stelle zum Pfad hinzufügen Fügt einen Knoten am Ende der Pfad-Liste (als oberstes Element im Stapel) hinzu. Es muss zunächst der für die Pfad-Liste angeforderte Speicherblock vergrößert werden. Kann der Block nicht vergrößert werden, tritt der Fehler ERROR_NOT_ENOUGH_MEMORY auf. Weiters muss der Wert length entsprechend angepasst werden. Funktionsheader void addNodeToPath(Path path, Node node); Letzten Knoten vom Pfad entfernen Löscht den letzten Knoten aus der Pfad-Liste (das oberste Element am Stapel). Der für die Pfad-Liste angeforderte Speicherblock muss entsprechend verkleinert und der Wert length verringert werden. Gruppe 18 Seite 5 Modul Path Example 3 Funktionsheader void removeLastNodeFromPath(Path path); Überprüfen ob der Knoten bereits im Pfad ist Überprüft ob der übergebene Knoten bereits in der Pfad-Liste enthalten ist. Wird der Knoten gefunden, wird der Wert TRUE zurückgegeben, sonst FALSE. Funktionsheader int isNodeInPath(Path path, Node node); Pfad am Bildschirm ausgeben Gibt einen Pfad in der geforderten Schreibweise am Bildschirm aus. Die Namen der einzelnen Knoten in der Pfad-Liste werden dabei durch '->' getrennt. Funktionsheader void printPath(Path path); Verwendet getNameFromNode (Modul Knoten) Pfad ermitteln Ermittelt einen beliebigen gültigen Pfad zwischen src_node und dst_node im Graphen graph. Es muss anfangs ein leerer Pfad übergeben werden. Falls ein Pfad gefunden wird, wird der Wert TRUE zurückgegeben und der Parameter path enthält einen gültigen Pfad um von src_node nach dst_node zu gelangen. Gibt es keinen gültigen Pfad im Graphen, wird FALSE zurückgegeben. Der Parameter path sollte in diesem Fall nicht ausgewertet werden. Seite 6 Gruppe 18 Example 3 Modul Path Funktionsheader int searchPath(Graph graph, Node src_node, Node dst_node, Path path); Verwendet searchPath isNodeInPath addNodeToPath removeLastNodeFromPath getFirstEdgeBySrc getNextEdgeBySrc getDstFromEdge 2.2 Testprogramm – Befehl sp (Show Path) Das Modul Testprogramm wird um den Befehl sp zur Anzeige eines Pfades zwischen zwei Knoten erweitert. Die Namen des Start- und des Zielknoten werden abgefragt. Wird ein Pfad gefunden, wird dieser in folgendem Format ausgegeben: a->b->c->d Wird kein Pfad gefunden, wird folgende Meldung angezeigt: Sorry, no path! Anmerkung: Diese Meldung wird nicht über die Fehlerbehandlungroutine abgehandelt, da der Aufbau der Meldung leider nicht ins Schema passt. Ist einer der beiden eingegebenen Knoten nicht im Graphen vorhanden, tritt der Fehler ERROR_NODE_NOT_EXISTS auf. Funktionsheader void commandShowPath(Graph graph); Verwendet readLine newPath searchPath printPath freePath errorHandler Gruppe 18 Seite 7 Modul Path 2.3 Example 3 Fehlerbehandlung Zur Anzeige von Fehlermeldungen wird das eigenständige Modul Fehlerbehandlung (siehe Übungsbeispiel 2) verwendet. Fehlerkonstanten Es werden folgende Konstanten als error_code für die Fehlerfälle definiert: Seite 8 Fehlerkonstante ERROR_NULL_POINTER Fehlermeldung Error: Null pointer! ERROR_NOT_ENOUGH_MEMORY Error: Not enough memory! ERROR_NODE_NOT_EXISTS Error: Node does not exist! Gruppe 18 Example 3 3 Aufteilung in mehrere Dateien Aufteilung in mehrere Dateien Zur besseren Übersicht wird der Quellcode in mehrere Source- und Headerdateien aufgeteilt. Die Aufteilung entspricht dabei weitestgehend der getroffenen Einteilung in die einzelnen Module. Zusätzlich wird ein Makefile erstellt. Datei main.c, main.h Beschreibung aus Hauptprogramm Example 2 common.c, common.h Allgemein Funktionen und Konstanten, Fehlerbehandlungsroutine Example 2 graph.c, graph.h Operationen am Graphen Example 2 node.c, node.h Knotenliste; Operationen auf Knoten Example 2 edge.c, edge.h Kantenliste; Operationen auf Kanten Example 2 path.c, path.h Pfad zwischen zwei Knoten Example 3 testsuite.c, testsuite.h Testprogramm (CLI) Example 2, Example 3 Makefile GNU-Makefile Example 3 Gruppe 18 Seite 9