ALBERT-LUDWIGS-UNIVERSITÄT FREIBURG IM BREISGAU Animation komplexer Datenstrukturen und der dazugehörigen Algorithmen am Beispiel der Fibonacci-Heaps Wissenschaftliche Arbeit im Rahmen der Staatsexamensprüfung für das Lehramt an Gymnasien im Fach Mathematik vorgelegt von Tobias Lauer betreut von Prof. Dr. Thomas Ottmann Freiburg, im August 1999 Ich erkläre, daß ich die Arbeit selbständig und nur mit den angegebenen Hilfsmitteln angefertigt habe und daß alle Stellen, die dem Wortlaut oder dem Sinne nach anderen Werken entnommen sind, durch Angabe der Quellen als Entlehnungen kenntlich gemacht worden sind. Freiburg, 6. August 1999 Inhaltsverzeichnis: 1 Einleitung 1 2 Anforderungen 3 2.1 Allgemeine Vorgaben 3 2.2 Nützliche Zusatzfunktionen 4 3 Die Datenstruktur Fibonacci-Heap 5 3.1 Priority Queues 5 3.1.1 Begriffe und Definitionen 5 3.1.2 Anwendungen 6 3.1.3 Möglichkeiten der Implementation von Priority Queues 7 3.2 Die Struktur von Fibonacci-Heaps 7 3.2.1 Allgemeine Merkmale 7 3.2.2 Knotenstruktur 9 3.2.3 Manipulation von Bäumen in einem F-Heap 3.3 Die Heapoperationen 10 12 3.3.1 Einfache Operationen 12 3.3.2 Zusammengesetzte Operationen 12 3.3.3 Einfluß der Operationen auf die Struktur eines F-Heaps 14 3.4 Laufzeitanalyse 15 3.4.1 Vergleich verschiedener Implementationen von Priority Queues 15 3.4.2 Die Laufzeiten der zusammengesetzten Operationen 16 3.4.3 Amortisierte Analyse 17 4 Animation 4.1 Visualisierung von Datenstrukturen 20 20 4.1.1 An einer Visualisierung beteiligte Parteien 20 4.1.2 Einzelrechner oder verteiltes System 21 4.2 Algorithmenanimation 22 4.2.1 Anwendungen 22 4.2.2 Das Pfad-Transitions-Paradigma 23 4.2.3 Interaktion zwischen Algorithmus Animation 23 4.2.4 Einordnung des vorliegenden Ansatzes in Browns Taxonomie 25 4.3. Ausgewählte Aspekte und ihre Umsetzung 26 4.3.1 Repräsentation der Bäume 26 4.3.2 Verwendung von Farben 29 4.3.3 Animation der Manipulationen an F-Heaps 30 4.3.4 Darstellung der Laufzeitanalyse 32 4.3.5 Interaktivität und Benutzerfreundlichkeit 34 5 Implementation 5.1 Unterschiedliche Implementationsmöglichkeiten bei F-Heaps 36 36 5.1.1 Die delete-Operation 36 5.1.2 Abtrennen des Knotens bei decreasekey 37 5.1.3 Einfügen von Knoten in die Wurzelliste 37 5.1.4 Das Löschen von Markierungen 38 5.2 Die Programmiersprache Java 39 5.2.1 Plattformunabhängigkeit 39 5.2.2 Programmieren von Datenstrukturen 39 5.2.3 Geschwindigkeit 40 5.2.4 Modularer Aufbau von Programmen 40 5.2.5 Verwendung vorhandener Komponenten 40 5.3 Die Animationsbibliothek JEDAS 41 5.3.1 Aufbau und Benutzung der Bibliothek 41 5.3.2 Erweiterung von JEDAS um die Klasse CompObj 42 5.4 Beschreibung der programmierten Java-Klassen 43 5.4.1 Implementation von F-Heaps durch die Klassen Node und Fheap 43 5.4.2 Die Klasse der animierten Fibonacci-Heaps AnimFHeap 46 5.4.3 Codierung und Übermittlung der Animationsinformationen 47 5.4.4 Das Abspielmodul Anima 48 5.4.5 Das Hauptprogramm HeapSim 49 5.4.6 Das Statistikmodul StatWin 50 5.4.7 Das Dateiformat fhp 51 5.4.8 Menüs und Dialoge 52 6 Handhabung des Programmpakets 6.1 Installation 52 52 6.1.1 Systemvoraussetzungen 52 6.1.2 Installation unter UNIX und Linux 53 6.1.3 Installation unter Windows 95/98 53 6.2 Bedienung 53 6.2.1 Starten der Anwendung 53 6.2.2 Das Hauptfenster 54 6.2.3 Steuerung über die Menüleiste 55 6.2.4 Der Animationsbereich 56 6.2.5 Die Kontrollfläche 57 6.2.6 Ausführen der Heapoperationen 58 6.2.7 Das Statistikfenster 58 6.2.8 Beenden des Programms 59 6.3 Beispieldateien im .fhp-Format 60 6.3.1 Verwendung der beigefügten Beispiele 60 6.3.2 Erstellen eigener Beispieldateien 60 6.4 Verwendung einzelner Klassen in eigenen Java-Programmen 61 7 Zusammenfassung und Ausblick 62 Literaturverzeichnis 64 1 Kapitel 1 Einleitung Der Einsatz von Computertechnik zur Wissensvermittlung im Bildungsbereich hat in den letzten Jahren immer mehr zugenommen. Es gibt zahlreiche Autorensysteme zur Erstellung von computerunterstützten Unterrichtseinheiten, multimediale Lehrvorträge werden aufgezeichnet und sind über das Internet unabhängig von Ort und Zeit für jeden abrufbar. Auch wenn herkömmliche Lehrmethoden dadurch wohl nie ganz ersetzbar sein werden, eröffnet die Computertechnik doch Möglichkeiten, die ohne ihren Einsatz schwierig oder gar nicht umzusetzen wären. Zwei Beispiele dafür sind die Animation und die Simulation von Lerninhalten. Gerade im Bereich der Informatik, wo die nötige Technologie an den Bildungseinrichtungen in der Regel ausreichend zur Verfügung steht, bietet es sich an, von diesen Möglichkeiten Gebrauch zu machen. Algorithmen und Datenstrukturen waren seit Beginn der Computeranimation für Lehrzwecke der Hauptgegenstand und Inhalt vieler Visualisierungssysteme. Es gibt für Animationen dieser Art zahlreiche Beispiele, die oft auch über das Internet verfügbar sind.1 Ein großer Nachteil vieler dieser Systeme ist allerdings, daß die Animationen nur unter einem bestimmten Betriebssystem lauffähig sind und daher vielen potentiellen Benutzern vorenthalten bleiben. Mit der Entwicklung der Programmiersprache Java hat sich in den letzten Jahren ein plattformunabhängiger Standard herausgebildet, der die Möglichkeit bietet, Anwendungen zu schreiben, die auf allen gängigen Betriebssystemen lauffähig sind. Für die vorliegende Arbeit wird diese Möglichkeit genutzt und eine Algorithmenanimation in Java realisiert. Zudem soll dabei eine komplexere Datenstruktur erläutert werden, als dies in der Regel bei den bisherigen Demonstrationsprogrammen der Fall war. Ziel der Arbeit ist die Visualisierung der Datenstruktur „Fibonacci-Heap“ und die Animation der dazugehörigen Algorithmen. 1 Beispiele für solche Animationssysteme sind: XTango und Polka, online verfügbar unter http://www.cc.gatech.edu/gvu/softviz/algoanim/algoanim.html; Pavane, zu finden unter http://swarm.cs.wustl.edu/pavane.html; Zeus, unter http://www.research.digital.com/SRC/zeus/home.html. 2 Das folgende Kapitel nennt zunächst die Anforderungen, die an eine solches Programmpaket zu stellen sind. Dabei werden sowohl allgemeine Vorgaben bezüglich Funktionalität und Bedienung als auch hilfreiche zusätzliche Elemente erwähnt. Kapitel 3 beschreibt ausführlich die zu visualisierende Datenstruktur. Das Verständnis der Fibonacci-Heaps und der für sie definierten Operationen ist die Grundlage, um den hier verfolgten Ansatz der Animation nachzuvollziehen. Auch eine Analyse der Laufzeiten für die einzelnen Algorithmen wird dabei durchgeführt. In Kapitel 4 werden zunächst allgemeine Grundlagen und ein Klassifikationsmodell der Softwarevisualisierung beschrieben. Der Ansatz der vorliegenden Arbeit wird in diesen Kontext eingeordnet. Am Beispiel ausgewählter Aspekte des Programms werden wichtige Grundsätze der Algorithmenanimation dargestellt. Die konkrete Implementation der Datenstruktur sowie deren Animation ist Gegenstand des fünften Kapitels. Zunächst werden verschiedene Vorschläge zur Implementation der Fibonacci-Heaps diskutiert und die Wahl der eigenen Umsetzung begründet. Nach einer kurzen Beschreibung der verwendeten Programmiersprache Java und der ebenfalls benutzten Animationsbibliothek JEDAS werden die für die vorliegende Arbeit programmierten Klassen aufgezählt und erläutert. Kapitel 6 beschreibt die Handhabung des Programmpakets. Hier wird neben Installationshinweisen und der konkreten Bedienung der Animation auch die Verwendung der mitgelieferten Beispieldateien erklärt. Eine abschließende Zusammenfassung wird ergänzt durch einen Ausblick auf die Möglichkeiten, die die Algorithmenanimation und das hier erstellte Programm für die Lehre bieten. Einige Punkte zur Schreibweise sind noch anzumerken. Neu eingeführte und wichtige Begriffe, die auf Fibonacci-Heaps erklärten Operationen und Java-Methoden sind kursiv gedruckt. Namen von Java-Klassen und Variablen sowie Menüs und Menüpunkte des Programms erscheinen in jeweils anderen Schriftarten. 3 Kapitel 2 Anforderungen Ein Animationssystem, das zu Lehrzwecken eingesetzt werden soll, muß unterschiedliche Voraussetzungen erfüllen. Die folgenden Abschnitte beschreiben sowohl allgemeine Anforderungen, die für jegliche Art von Lernsoftware und Animation gelten, als auch spezifische Vorgaben, die für die vorliegende Arbeit relevant sind. 2.1 Allgemeine Vorgaben Das Programm soll die Datenstruktur Fibonacci-Heap visualisieren und die darauf operierenden Algorithmen mit Hilfe von Animationen darstellen. Dabei sollten nicht nur vorgegebene Beispiele unterstützt werden; vielmehr muß es dem Benutzer möglich sein, interaktiv die Datenstruktur zu verändern und zu beeinflussen, indem er die auszuführenden Heapoperationen und ihre Parameter frei wählen kann. Die Bedienung muß intuitiv und einfach zu erlernen sein, damit der Benutzer seine Konzentration auf die Animation richten kann. Die Animation selbst sollte in fließenden Bewegungen die Übergänge zwischen aufeinanderfolgenden Zuständen der FibonacciHeaps darstellen, ohne dabei den Betrachter mit zu vielen Details zu „überladen“. Wesentliches Kriterium muß immer die Verständlichkeit bleiben. Es ist interessant, das in der Theorie bewiesene Laufzeitverhalten von Algorithmen auch in der Praxis nachzuvollziehen. Insbesondere für Fibonacci-Heaps, bei denen sich die tatsächliche Laufzeit einer einzelnen Operation von den theoretischen amortisierten Kosten erheblich unterscheiden kann, müßte solch eine praktische Überprüfung sehr hilfreich sein. Um die relativ komplexe amortisierte Laufzeitanalyse verständlich zu machen, ist es nützlich, dem Benutzer statistische Informationen über den Fibonacci-Heap zusammen mit den Laufzeiten der vorangegangenen Operationen zur Verfügung zu stellen. Auch diese Informationen sind – wenn möglich – visuell aufzubereiten. Außerdem soll es möglich sein, die vom Programm erzeugten Animationen in Lehrvorträge einzubinden. Insbesondere multimediale Vorträge, wie sie beispielsweise mit 4 dem Whiteboard-System an der Universität Freiburg seit längerer Zeit erstellt werden, sind zu unterstützen. Gleichzeitig muß das Programm aber auch von den Lernenden zum Selbststudium genutzt werden können, da Animationen – wie neuere Studien [Kehoe 1999] belegen – vor allem das Lernen in einem offenen, informellen Kontext zu unterstützen scheinen. 2.2 Nützliche Zusatzfunktionen Damit auch eine Darstellung von Fibonacci-Heaps mit einer großen Anzahl von Elementen möglich wird, ist es nützlich, die Anzeige verkleinern bzw. vergrößern zu können. Diese „Zoomfunktion“ sollte automatisch die Größe verändern, wenn zum Beispiel durch Einfügen eines neuen Elements die Darstellung aller Knoten nicht mehr gegeben ist. Schließlich müssen im Paket auch Beispiele für Fibonacci-Heaps integriert sein. Dabei sollten sowohl Standard- als auch Extremfälle berücksichtigt werden. Brown und Hershberger [Brown 1998b] betonen, daß gerade für Lehrzwecke sehr wohl konstruierte Beispiele verwendet werden können oder sogar sollen, um die Animationen interessant und instruktiv zu gestalten. Außerdem ist es wünschenswert, daß der Benutzer die Animation zu einem beliebigen Zeitpunkt stoppen, pausieren, unterbrechen und wieder fortsetzen kann. Das Programm sollte schließlich die Möglichkeit bieten, einen Fibonacci-Heap in seinem aktuellen Zustand abzuspeichern und diesen später wieder einzulesen. Dies erlaubt es, beim Einsatz des Programms zu Unterrichtszwecken speziell vorbereitete Beispiele einzuladen, um bestimmte Vorgänge zu demonstrieren. 5 Kapitel 3 Die Datenstruktur Fibonacci-Heap Fibonacci-Heaps – kurz F-Heaps – wurden erstmals von Fredman und Tarjan beschrieben [Fredman 1987]. Es handelt sich bei dieser Datenstruktur um eine Möglichkeit der Implementation von Priority Queues (Vorrangswarteschlangen). Diese abstrakte Datenstruktur wird im folgenden Abschnitt beschrieben und bildet die Voraussetzung für das Verständnis von Fibonacci-Heaps. Danach folgt eine Beschreibung des Aufbaus von F-Heaps und – damit eng verbunden – der Operationen, die auf ihnen erklärt sind. Eine amortisierte Laufzeitanalyse der wichtigsten Algorithmen schließt das Kapitel ab. 3.1 Priority Queues 3.1.1 Begriffe und Definitionen Fredman und Tarjan bezeichnen Vorrangswarteschlangen als „Heaps“ [Fredman 1997]. Da dieser Begriff aber oft auch anderweitig (üblicherweise für die beim Sortieralgorithmus HeapSort eingesetzte Struktur der Halde) verwendet wird, benutzt man meist die von Knuth [Knuth 1973] eingeführte Bezeichnung Priority Queue (Vorrangswarteschlange). In einer Priority Queue wird eine Menge von Elementen gespeichert, auf der eine Ordnung definiert ist. Jedem Element ist hierbei ein Schlüssel (seine „Priorität“), üblicherweise in Form einer ganzen Zahl, zugeordnet. Natürlich kann als Schlüsselmenge ebenso jede andere Menge mit einer Ordnung gewählt werden. Der Einfachheit halber werden zur Erklärung der Datenstruktur häufig die Elemente selbst mit ihren Schlüsseln identifiziert. Da vorausgesetzt wird, daß kein Element mehrmals gespeichert wird, haben in diesem Fall alle Elemente verschiedene Schlüssel, was im allgemeinen aber nicht zutreffen muß. Die abstrakte Datenstruktur Priority Queue ist gegeben durch die Operationen, die auf ihr ausgeführt werden können. Dazu gehört zunächst das Initialisieren einer neuen leeren Struktur (init). Ebenso muß es möglich sein, ein neues Element einzufügen (insert). 6 Schließlich muß das Element mit der höchsten Priorität (dem kleinsten Schlüssel) gesucht (accessmin) und entfernt (deletemin) werden können. Daneben sind weitere Operationen wünschenswert, nämlich das Entfernen eines beliebigen Elements aus der Struktur (delete) und das Erhöhen der Priorität eines Elements, was dem Herabsetzen eines Schlüssels um oder auf einen vorgegebenen Wert entspricht (decreasekey). Auch ist es oft nützlich, zwei elementfremde Priority Queues zusammenfügen zu können (meld). Die effiziente Suche nach einem Element wird in einer Priority Queue nicht unterstützt. Daher muß zum Entfernen eines beliebigen Elements und zum Herabsetzen eines Schlüssels Zugriff auf das jeweiligen Element bestehen. Eine Möglichkeit, wie dies realisiert werden kann, wird im Abschnitt 5.4.5 beschrieben. 3.1.2 Anwendungen Der Begriff Vorrangswarteschlange erinnert an Situationen wie Warteschlangen an Supermarktkassen oder persönliche To-do-Listen mit zu erledigenden Aufgaben. Tatsächlich werden F-Heaps auch in Betriebssystemen zum job scheduling, der Verwaltung der anstehenden Jobs eingesetzt. Die typischen Anwendungen für Priority Queues sind allerdings Optimierungsalgorithmen für Graphen, darunter insbesondere Dijkstras Algorithmus zur Berechnung der kürzesten Wege von einem Knoten im Graphen zu allen anderen („single-source shortest path problem“). Die Laufzeit dieses Algorithmus zu verbessern war das Ziel von Fredman und Tarjan bei der Entwicklung von Fibonacci-Heaps. Die Kanten des Graphen werden bei diesem Verfahren in einer Priority Queue verwaltet, mit den Kantenlängen als Schlüsselwerten. Weiterhin werden Priority Queues in Algorithmen zur Ermittlung minimal spannender Bäume („minimum spanning trees“) von Graphen verwendet. Es ist klar, daß die Laufzeit solcher Algorithmen entscheidend von den Laufzeiten der Operationen der Priority Queue und damit von deren Implementation abhängen. 7 3.1.3 Möglichkeiten der Implementation von Priority Queues Es gibt vielfältige Möglichkeiten, Priority Queues konkret zu implementieren. Sie reichen vom einfachen Array über lineare Listen, Heaps2 bis hin zu Binomial Queues und Relaxed Heaps. Das wichtigste Kriterium für die Wahl einer Implementation ist die Laufzeit der einzelnen Operationen. Am besten werden Priority Queues wird mit einer Datenstruktur implementiert, die speziell für die relevanten Operationen optimiert ist. Ein Beispiel hierfür sind Fibonacci-Heaps. 3.2 Die Struktur von Fibonacci-Heaps Eine Beschreibung der Datenstruktur ist sehr eng mit den darauf definierten Operationen verbunden. Eine direkte Definition ist deshalb nur schwer anzugeben. Im folgenden werden daher zunächst einige allgemeine Merkmale von Fibonacci-Heaps aufgezeigt, dann die Knotenstruktur erläutert und schließlich die Manipulationen an Bäumen beschrieben. 3.2.1 Allgemeine Merkmale Grundsätzlich kann man einen Fibonacci-Heap als eine „Kollektion heapgeordneter Bäume“ [Ottmann 1996] beschreiben. Heapgeordnet bedeutet in diesem Fall: der Schlüssel eines jeden Knotens x ist nicht kleiner als der Schlüssel seines Vaterknotens p(x), wenn x einen Vaterknoten besitzt. Daher enthält der Wurzelknoten eines heapgeordneten Baums immer ein Element mit minimalem Schlüssel. Die Wurzeln der Bäume sind dabei in einer ungeordneten, doppelt verketteten, zyklischen Liste verbunden, und es gibt einen Zeiger auf den Knoten mit dem kleinsten Schlüssel (der aufgrund der Heapordnung immer die Wurzel eines der Bäume sein muß). Dieser Knoten heißt der Minimalknoten des F-Heaps. Abbildung 3.1 zeigt ein Beispiel für einen F-Heap. 2 Der Begriff „Heap“ wird hier gebraucht im Sinne einer heapgeordneten Liste von Schlüsseln, wie sie zum Beispiel beschrieben wird in [Ottmann 1996], S. 89ff. 8 2 5 9 3 10 7 12 6 Abbildung 3.1: Grobdarstellung des Aufbaus eines Fibonacci-Heaps. Die Verkettung der Knoten ist vereinfacht dargestellt. Der Pfeil zeigt auf den Knoten mit minimalem Schlüssel. Eine präzisere direkte Definition der Struktur gibt es nicht; die Struktur eines FibonacciHeaps ist nämlich keineswegs starr, sondern hängt immer von den bisher ausgeführten Operationen sowie von deren Reihenfolge ab. Daher wird der Fibonacci-Heap üblicherweise durch die Beschreibung der auf ihm durchführbaren Operationen definiert. (Die Struktur eines Fibonacci-Heaps nach einer gegebenen Folge von Operationen auf einem anfangs leeren F-Heap ist nämlich immer eindeutig.) Die Fibonacci-Heaps bilden somit die kleinste Klasse von heapgeordneten Bäumen, die unter den auf ihr definierten Operationen abgeschlossen ist. Dadurch erhalten F-Heaps jedoch implizit eine Struktur, die für die Laufzeiten der Algorithmen von entscheidender Bedeutung ist. So ist beispielsweise die Gesamtzahl der Nachkommen jedes Knotens exponentiell in der Anzahl seiner Söhne. Genauer gilt, daß die Größe eines jeden Baums in einem F-Heap größer oder gleich der (r+2)-ten Fibonacci-Zahl3 ist, wenn r den Rang der Wurzel des Baums bezeichnet. Diesem Lemma verdankt die Datenstruktur den Namen „Fibonacci-Heap“. Die Heap-Operationen sind die bereits in Abschnitt 3.1.1 für Priority Queues beschriebenen Aktionen init, insert, accessmin, deletemin, decreasekey, delete und meld. Sie können die Struktur des F-Heaps verändern, indem neue Bäume zur Liste hinzugefügt oder bestehende Bäume verändert werden, beispielsweise durch das Abschneiden von Teilbäumen (cut), das Entfernen von Wurzelknoten (remove) oder das Verbinden zweier Bäume zu einem neuen (link). Zur genaueren Beschreibung dieser Manipulationen von Bäumen ist es notwendig, den Aufbau eines Knotens näher zu betrachten. 3 Die Folge der Fibonacci-Zahlen ist definiert durch F0 = 0; F1 = 1; Fn = Fn-1 + Fn-2 für alle n > 1. 9 3.2.2 Knotenstruktur Jeder Knoten besteht aus dem Inhaltsfeld (das auch den Schlüssel enthält), dem Rang, der die Zahl seiner Söhne angibt, und einem Markierungsbit, das auf true gesetzt ist, wenn der Knoten irgendwann einen seiner Söhne verloren hat, seitdem er selbst zuletzt Sohn eines anderen Knotens geworden ist. Außerdem enthält jeder Knoten Zeiger auf seinen Vater, einen seiner Söhne sowie auf seinen linken und rechten Bruder im Baum oder der Wurzelliste. Abbildung 3.2 zeigt den schematischen Aufbau eines Knotens mit diesen 7 Vater Inhaltsfeld mit Schlüssel Rang Markierung Sohn Rechter Bruder Linker Bruder Feldern bzw. Zeigern. Abbildung 3.2: Schematische Darstellung des Knotenformats.4 Der Vaterzeiger eines Knotens in der Wurzelliste ist auf null gesetzt5, ebenso der Sohnzeiger eines Knotens ohne Söhne. Hat ein Knoten keine Brüder, so weisen die Zeiger zum linken und rechten Bruder auf den Knoten selbst. Jeder Knoten ist mit seinen Brüdern in einer doppelt verketteten, zirkulären Liste verbunden. Dadurch genügt es, daß jeder Knoten nur einen Sohnzeiger besitzt, weil mittels der Verkettung auf jeden der Söhne zugegriffen werden kann. Abbildung 3.3 zeigt den F-Heap aus Abbildung 3.1 mit allen Zeigern. 2 5 9 3 10 7 12 6 Abbildung 3.3: Detaillierte Darstellung eines Fibonacci-Heaps mit allen Zeigern zwischen den Knoten. 4 Die Darstellung erfolgte in Anlehnung an ein Handout von Sven Schuierer über Fibonacci-Heaps aus der Vorlesung „Algorithmentheorie“ im Sommersemester 1998. 5 Es gibt in der vorliegenden Implementation einen Ausnahmefall, in dem der Vaterzeiger eines Wurzelknotens nicht null ist. Dies betrifft den im nächsten Abschnitt beschriebenen remove-Schritt. 10 3.2.3 Manipulation von Bäumen in einem F-Heap Die eigentlichen Heapoperationen verändern die Struktur eines F-Heaps in unterschiedlicher Weise. Alle Veränderungen an einzelnen heapgeordneten Bäumen lassen sich jedoch aus drei Grundschritten, link, cut und remove6, zusammensetzen. Dieser Ansatz – das Beschreiben der Operationen durch Unterteilen in kleine Einzelschritte – ist motiviert durch das Ziel der Animation, wo diese „Baukastenmethode“ nützlich ist, weil sie sehr viel Aufwand spart. Zur Manipulation von heapgeordneten Bäumen gibt es die folgenden drei Möglichkeiten. Diese mit Hilfe von Animationen anschaulich zu machen ist ein wesentlicher Bestandteil der später beschriebenen Visualisierung. (i) link Zwei Bäume von gleichem Rang r (d.h. mit derselben Anzahl r von Söhnen der Wurzel) werden verbunden, indem die Schlüssel der Wurzelknoten verglichen werden und der Knoten mit kleinerem Schlüssel zu einem Sohn des Knotens mit größerem Schlüssel gemacht wird. Dadurch entsteht ein neuer Baum vom Rang r+1. Die Gesamtzahl der Bäume verringert sich um 1. (a) 2 5 (b) 3 9 10 6 2 5 12 9 12 3 10 6 Abbildung 3.4: Die link-Operation verbindet zwei Bäume von gleichem Rang miteinander. Bild (a) zeigt die Bäume vor, (b) nach dem link-Schritt. (ii) cut Ein Teilbaum wird aus einem Baum herausgetrennt, indem die Wurzel des Teilbaums aus der Liste der Söhne seines Vaters entfernt und neben dem Minimalknoten in die Wurzelliste eingehängt wird. Die Zahl der Bäume erhöht sich um 1. 6 Die Namen link und cut sind häufig in der Literatur verwendete Bezeichnungen, die auf Fredman und Tarjan [Fredman 1987] zurückgehen. Der Name remove dagegen wird lediglich im Rahmen dieser Arbeit benutzt. 11 (a) 2 5 9 (b) 3 12 10 2 9 5 6 3 10 12 6 Abbildung 3.5: Abtrennen des Knotens mit Schlüssel 9, (a) vor dem cut, (b) nach dem Einfügen des abgetrennten Teilbaums in die Wurzelliste. (iii) remove Ein Knoten x der Wurzelliste wird entfernt, indem man den Knoten selbst löscht und die Liste seiner Söhne in die Wurzelliste einfügt, falls der Knoten Söhne besitzt. Die Gesamtzahl der Bäume erhöht sich somit um Rang(x)-1. 7 2 5 (a) 4 9 12 7 5 3 10 9 12 6 3 10 4 6 (b) Abbildung 3.6: Der remove-Schritt entfernt den Knoten mit Schlüssel 2 aus der Wurzelliste und fügt die Liste der Söhne des Knotens in die Wurzelliste ein. Es ist festzuhalten, daß keiner der beschriebenen Schritte die Heapordnung der Knoten zerstört. Außerdem soll im Vorgriff auf die in Abschnitt 3.4.2 beschriebene Laufzeitanalyse hier bemerkt werden, daß jede dieser Methoden in konstanter Zeit ausführbar ist. Denn wie man leicht nachprüft, werden bei keinem dieser Schritte Zeigeraktualisierungen an mehr als 6 Knoten vorgenommen. Außerdem kann durch die doppelte Verkettung auf alle diese Knoten direkt zugegriffen werden, insbesondere auf beide Nachbarn eines Knotens, wenn dieser aus der Liste seiner Brüder herausgetrennt wird. Allerdings bleiben bei remove die Vaterzeiger der neu in die Wurzelliste aufgenommenen Söhne des entfernten Knotens bestehen, sonst wäre diese Aktion nicht in konstanter Zeit möglich, da alle Söhne durchlaufen werden müßten. Aus diesen drei Manipulationsschritten und der meld-Operation (die leicht modifiziert auch Teil von remove ist) lassen sich im Prinzip alle komplexeren Heap-Operationen zusammensetzen. Der folgende Abschnitt beschreibt, wie diese realisiert werden. 12 3.3 Die Heapoperationen Die in Abschnitt 3.1.1 für Priority Queues geforderten Operationen müssen für FibonacciHeaps konkret implementiert werden. Auch bei deren Beschreibung wird die Laufzeit im Hinblick auf die spätere Analyse erwähnt werden. Zunächst werden die einfachen Operationen init, accessmin und meld erläutert, danach die komplexeren, aus anderen zusammengesetzten Operationen insert, deletemin, decreasekey und delete. Die Unterscheidung anhand dieses Kriteriums – „einfach“ im Gegensatz zu „zusammengesetzt“ – wurde deshalb gewählt, weil sie für die Animation der Operationen von Bedeutung ist. Dort lassen sich die komplexeren Animationsschritte in analoger Weise aus einfacheren Teilen zusammensetzen. 3.3.1 Einfache Operationen Das Erzeugen eines neuen, leeren F-Heaps (init) gibt lediglich einen Zeiger auf null zurück. Zum Bestimmen des Minimalknotens (accessmin) wird das Element zurückgegeben, auf das der Minimumzeiger des F-Heaps weist. Beide Vorgänge sind natürlich in konstanter Zeit durchführbar. Um zwei F-Heaps h1 und h2 zu verschmelzen, werden die Wurzellisten aneinandergehängt und der Minimumzeiger auf das kleinere der beiden Minima gesetzt. Auch diese Operation benötigt konstante Zeit, weil zum Aneinanderhängen der Wurzellisten aufgrund der zirkulären Verkettung kein Durchlaufen der Knoten erforderlich ist. 3.3.2 Zusammengesetzte Operationen Die insert-Operation, also das Einfügen eines neuen Knotens x in den F-Heap h1, setzt sich zusammen aus dem Erzeugen eines neuen F-Heaps h2, der nur den Knoten x enthält, und dem anschließenden Verschmelzen von h1 und h2. Es ist klar, daß diese Operation in konstanter Zeit ausführbar ist. Zum Entfernen des Minimalknotens min aus dem F-Heap h wird min mit dem oben beschriebenen remove-Schritt entfernt. Das heißt, die Liste der Söhne von min (falls vorhanden) wird in die Wurzelliste aufgenommen. Danach erfolgt das „Konsolidieren“ (consolidate) der Wurzelliste. Hierbei werden jeweils zwei Knoten gleichen Ranges mittels 13 link verbunden, und zwar so lange, bis keine Bäume von gleichem Rang mehr vorhanden sind. Um alle Bäume in einem Durchlauf der Wurzelliste zu erfassen und gegebenenfalls zu verbinden, wird ein Rang-Array eingerichtet. Jede Position des Arrays beinhaltet einen Zeiger auf einen Wurzelknoten, dessen Rang dem Index dieser Position entspricht. Daraus ergibt sich, daß der höchste Index im Array gleich dem maximal möglichen Rang eines Knotens im vorliegenden F-Heap ist. Zu Beginn ist das Array leer, alle Zeiger weisen auf null. Beim Durchlaufen der Wurzelliste werden nun die Knoten nacheinander in die ihrem Rang entsprechenden Positionen des Arrays eingetragen. Ist ein Platz schon belegt, so weiß man, daß es einen weiteren Wurzelknoten von diesem Rang gibt, und verbindet diese beiden mittels link. Der Platz im Array wird wieder freigegeben, und die Wurzel des resultierenden Baums wird nun in die nächsthöhere Position eingetragen, da der Rang sich beim link-Schritt um eins erhöht hat. Falls diese Array-Position ebenfalls belegt ist, führt man einen weitere link-Operation durch, usw., bis man auf einen freien Platz im Array trifft. Dann geht man zum nächsten Knoten und trägt diesen ein, bis alle Knoten der Wurzelliste durchlaufen sind. Zum Schluß zeigt jede Position des Arrays auf maximal eine Wurzel; damit befinden sich nur Knoten von unterschiedlichem Rang in der Wurzelliste. Zusätzlich werden beim Durchlaufen die Vaterzeiger aller Wurzelknoten auf null gesetzt, der Minimumzeiger auf den neuen Minimalknoten aktualisiert und alle eventuell vorhandenen Markierungsbits der Wurzelknoten gelöscht. Die Laufzeit der deletemin-Operation hängt von der Gesamtgröße des F-Heaps sowie der Anzahl der Knoten in der Wurzelliste (vor dem Konsolidieren) ab. Die Zahl der durchzuführenden Schritte ist proportional zur Länge des Rang-Arrays, also dem maximal möglichen Rang eines Knotens, zuzüglich der Anzahl der durchgeführten link-Schritte. Beim Herabsetzen eines Schlüssels (decreasekey) auf einen kleineren Wert wird im allgemeinen die Heapordnung eines Baums zerstört, denn der herabgesetzte Schlüssel ist möglicherweise kleiner als der seines Vaterknotens. In diesem Fall wird der Knoten nach dem Herabsetzen des Schlüssels mittels cut von seinem Vater abgetrennt und (zusammen mit seinen Nachkommen) als neuer Baum in die Wurzelliste eingefügt. Zusätzlich wird beim Abtrennen das Markierungsbit des Vaterknotens gesetzt, außer wenn der Vater ein Wurzelknoten ist. Die Markierung eines Knotens gibt somit an, ob dieser Knoten bereits einen seiner Söhne verloren hat, seitdem er selbst zuletzt Sohn eines anderen Knotens geworden ist. Falls nun beim Abtrennen eines Knotens der Vater bereits markiert ist (dieser also seinen zweiten Sohn verliert), trennt man den Vater ebenfalls ab und fügt ihn in die Wurzelliste 14 ein. Falls dessen Vater auch schon markiert war, geschieht mit diesem dasselbe, usw., so daß man unter Umständen eine ganze Serie von Abtrennungen, sogenannte cascading cuts, erhält. Durch diese Prozedur wird gewährleistet, daß ein Baum nicht unkontrolliert „beschnitten“ wird, sondern die Anzahl der Nachkommen eines Knotens immer exponentiell in der Anzahl seiner Söhne bleibt. Dies ist von entscheidender Bedeutung für die Laufzeiten der Algorithmen. Durch die möglicherweise erforderlichen cascading cuts bleiben die tatsächlichen Kosten einer decreasekey-Operation nicht immer konstant, sondern hängen davon ab, wie viele Abtrennungen durchgeführt werden müssen. Zum Entfernen eines beliebigen Knotens p aus dem F-Heap h gibt es zwei Möglichkeiten, von denen eine einfacher zu implementieren ist, die andere aber Laufzeitvorteile bringt. Der erste Vorschlag ist folgender: „Zunächst wird der Schlüssel von p auf einen Wert herabgesetzt, der kleiner als alle übrigen Schlüsselwerte in h ist. Anschließend wird die Operation Delete Min ausgeführt“ [Ottmann 1996]. Ebenso verfahren [Cormen 1990] und [Boyer 1997]. Bei dieser Variante wird am Schluß jeder delete-Operation die Wurzelliste konsolidiert. Dies ist jedoch gar nicht nötig; nur wenn das entfernte Element dasjenige mit minimalem Schlüssel ist, muß die Wurzelliste durchlaufen und das neue Minimum gefunden werden. Ansonsten verursacht das Konsolidieren in der Regel nur längere tatsächliche Laufzeiten. Die andere Alternative [Fredman 1987] kommt in fast allen Fällen ohne Konsolidierung der Wurzelliste aus. Nur falls der zu entfernende Knoten p der Minimalknoten ist, wird die deletemin-Operation ausgeführt. Ansonsten wird p zunächst mittels cut von seinem Vaterknoten abgetrennt (falls er einen Vater hat). Dabei sind eventuell weitere cascading cuts notwendig. Danach wird p mit remove aus der Wurzelliste entfernt. Die tatsächliche Laufzeit zum Entfernen eines Knotens, der nicht Minimalknoten ist, ist im ersten Fall die Summe der Laufzeiten von decreasekey und deletemin. Im zweiten Fall entspricht sie im wesentlichen der Laufzeit von decreasekey, hängt also nur von der Anzahl der cascading cuts ab, die durchgeführt werden müssen. 3.3.3 Einfluß der Operationen auf die Struktur eines F-Heaps Wie man aus dem letzten Abschnitt sieht, wird lediglich beim Entfernen des Minimums die sonst eher lose Struktur eines F-Heaps wieder konsolidiert. Nach jedem Konsolidierungsvorgang ähnelt der Aufbau eines Fibonacci-Heap stark dem einer 15 Binomial Queue7. So kommen beispielsweise nur Bäume von unterschiedlichem Rang in der Wurzelliste vor. Wird kein decreasekey oder delete ausgeführt, entspricht ein F-Heap sogar nach jeder deletemin-Operation genau einer Binomial Queue. Wenn in einer Folge von Operationen längere Zeit keine deletemin-Operation vorkommt, kann es jedoch passieren, daß die Struktur immer mehr „degeneriert“. Abbildung 3.7 zeigt drei Beispiele. So können Fibonacci-Heaps, die dieselben Elemente enthalten, im einen Extremfall die Gestalt (a) einer ungeordneten linearen Liste, im anderen Extrem (b) die eines einzelnen heapgeordneten Binomialbaums oder gar (c) die Gestalt eines wie eine geordnete Liste aussehenden „Unärbaums“ haben. 2 12 3 8 10 6 2 9 3 5 5 (a) 6 2 8 5 9 3 9 12 (b) 10 6 8 10 (c) 12 Abbildung 3.7: Drei unterschiedlich strukturierte F-Heaps mit denselben Elementen 3.4 Laufzeitanalyse Der wichtigste Grund für die Verwendung von Fibonacci-Heaps sind die vorteilhaften Laufzeiten für die einzelnen Operationen im Vergleich zu anderen Implementationen von Priority Queues. Dieser Abschnitt analysiert die Kosten der Heapmanipulationen und erklärt insbesondere die amortisierte Analyse, die für die Bestimmung der Laufzeiten auf F-Heaps relevant ist. 3.4.1 Vergleich verschiedener Implementationen von Priority Queues In Tabelle 3.8 sind die Laufzeiten für lineare Listen, Heaps, Binomial Queues und Fibonacci-Heaps als Implementationen von Priority Queues zusammengestellt. 7 Für eine Beschreibung von Binomial Queues siehe z.B. [Ottmann 1996], S.387-394. 16 init insert accessmin deletemin meld (m ≤ n) decreasekey delete Liste O(1) O(1) O(1) O(n) O(1) O(1) O(n) Heap O(1) O(log n) O(1) O(log n) O(n)/O(m log n) O(log n) O(log n) Binomial Queue O(1) O(log n) O(log n) O(log n) O(log n) O(log n) O(log n) Fibonacci-Heap O(1) O(1) O(1) O(log n)* O(1) O(1)* O(log n)* Tabelle 3.8: Vergleich der Laufzeiten verschiedener Implementationen von Priority Queues. Die Variablen n und m stehen für die Anzahl der Elemente in den Priority Queues. Bei den mit (*) gekennzeichneten Schranken handelt es sich um die amortisierte Laufzeit.8 Wie man sieht, schneiden Fibonacci-Heaps im Vergleich mit anderen Implementierungen deutlich besser ab. Darüber hinaus eignet sich diese Datenstruktur trotz ihrer komplexen Struktur, die durch die vierfache Verkettung das Adjustieren vieler Zeiger erforderlich macht, auch für „einfache“ Aufgaben wie Sortieren, wo sie laut [Boyer 1997] zum bekannten HeapSort durchaus kompetitiv ist. Die Ursache hierfür liegt insbesondere am sogenannten „lazy melding“, dem Aufschieben des zeitaufwendigen Konsolidierungsprozesses bis zu einem geeigneten Zeitpunkt, in diesem Fall dem Entfernen des Minimums. Da nach dem Entfernen das neue Minimum gefunden werden muß, was ein Durchgehen der Wurzelliste erfordert, bietet es sich an, das Konsolidieren parallel dazu durchzuführen. So können Operationen wie insert oder decreasekey in konstanter amortisierter Zeit durchgeführt werden, wie in den folgenden Abschnitten erläutert wird. 3.4.2 Die Laufzeiten der zusammengesetzten Operationen Die tatsächlichen Laufzeiten der komplexeren, zusammengesetzten Operationen hängen im wesentlichen davon ab, wie oft die jeweiligen Einzelschritte, die ja jeweils nur konstante Kosten verursachen, ausgeführt werden müssen. Beim Entfernen des Minimums ist dies die Anzahl der link-Schritte, beim Herabsetzen eines Schlüssels oder Entfernen eines Knotens die Anzahl der cascading cuts. Diese Zahlen können von Fall zu Fall beträchtlich variieren. So müssen beim Entfernen des Minimums aus dem F-Heap in Abbildung 3.9 (a) 8 Diese Zusammenstellung wurde aus einem Handout von Sven Schuierer über Fibonacci-Heaps aus der Vorlesung „Algorithmentheorie“ im Sommersemester 1998 übernommen . Die Laufzeit von delete bei der Listenimplementation wurde von O(1) auf O(n) korrigiert, da es sich beim Entfernen eines beliebigen Elements auch um das Minimum handeln kann. 17 insgesamt 7 links vorgenommen werden, beim F-Heap in (b) jedoch überhaupt keine, obwohl beide Heaps dieselbe Anzahl von Elementen haben. Ebenso müssen beim Herabsetzen des Schlüssels 15 auf 1 im Fall von (a) kein cut, bei (b) jedoch 8 cuts ausgeführt werden. 2 3 5 6 12 3 8 6 10 2 9 5 15 8 (a) 9 10 12 (b) 15 Abbildung 3.9: Worst-case-Beispiele für (a) deletemin und (b) decreasekey[15,1]. Bei den grau hinterlegten Knoten ist das Markierungsbit gesetzt. Eine einfache Worst-case-Analyse würde also für beide Operationen eine Laufzeit in der Größenordnung O(n) ergeben, wobei n die Anzahl der Elemente im F-Heap ist.9 Diese Schranken sind jedoch für eine durchschnittliche deletemin- oder decreasekey-Operation nicht realistisch. Vor allem beim Herabsetzen eines Schlüssels kann man in den meisten Fällen eine konstante Zahl von Schritten erwarten. Insbesondere berücksichtigt die einfache Worst-case-Analyse nicht die Folge von Operationen, die einer dieser Operationen vorausgeht. Daher ist es sinnvoll, bei Fibonacci-Heaps eine amortisierte Worst-case-Analyse vorzunehmen, was im nächsten Abschnitt geschehen soll. 3.4.3 Amortisierte Analyse Bei der amortisierten Laufzeitanalyse werden nicht primär die Kosten einer einzelnen Operation betrachtet, sondern die einer ganzen Folge von Operationen auf einem anfangs 9 In diesem Punkt irrt übrigens Boyer [Boyer 1997], der O(log n) als Schranke für die tatsächliche Laufzeit von decreasekey nennt mit der Begründung, die maximale Tiefe eines Baums sei beschränkt durch O(log n). Dies ist nicht der Fall. Da Wurzelknoten beliebig viele Söhne verlieren können, lassen sich F-Heaps wie in Abbildung 3.9 (b), bei denen die Tiefe des Baums n ist, für jede Heapgröße n leicht konstruieren. 18 leeren F-Heap. Die Gesamtlaufzeit für diese Folge kann dann so auf die Einzeloperationen „verteilt“ werden, daß die Kosten für jede Operation innerhalb der in Tabelle 3.8 angegebenen Größenordnung bleiben. Man kann sich diesen Ansatz folgendermaßen veranschaulichen. Bei Operationen mit konstanter Laufzeit wird zusätzlich zu den regulären Kosten noch eine gewisse Anzahl von Kosteneinheiten „dazubezahlt“, die als Reserve für spätere, „teurere“ Operationen bestimmt sind. Die amortisierten Kosten bestehen dann aus den tatsächlichen Kosten abzüglich des bei der Operation verwendeten oder zuzüglich des dabei gesammelten „Guthabens“. Als Beispiel soll das Einfügen eines neuen Knotens dienen. Wenn wir die konstanten Kosten für insert mit 1 KE (Kosteneinheit) veranschlagen und bei jeder Einfügeoperation noch 1 KE dazubezahlen, steigen zwar die amortisierten Kosten von insert auf 2 KE, bleiben aber in O(1). Bei n eingefügten Knoten haben wir nun ein Guthaben von n KE zur Verfügung, mit Hilfe derer sich beim Entfernen des Minimums sämtliche link-Schritte bezahlen lassen (wenn man für einen link-Schritt ebenfalls 1 KE ansetzt); denn jeder der noch im F-Heap verbleibenden Knoten wird höchstens einmal an einen anderen angehängt. Die Kosten von deletemin reduzieren sich somit auf das Erstellen des Rang-Arrays sowie das Übernehmen der Söhne des Minimus in die Wurzelliste. Für jeden dieser Söhne bezahlt man nämlich nochmals 1 KE, so daß für jeden Knoten in der Wurzelliste immer 1 KE Guthaben vorhanden ist, um eine spätere link-Operation bezahlen zu können. Da sowohl die Länge des Rang-Arrays als auch die maximale Anzahl der Söhne eines Knotens in O(log n) liegen10, ist auch die amortisierte Laufzeit von deletemin durch O(log n) beschränkt. Ebenso verfährt man beim Markieren von Knoten nach einem cut. Bei jedem Markieren bezahlt man noch zusätzliche 2 KE Guthaben. Mit dieser Reserve kann, falls der markierte Knoten nochmals einen Sohn verliert, der Knoten selbst ebenfalls abgetrennt und in die Wurzelliste eingehängt werden (1 KE benötigt man für das Abtrennen, 1 KE bleibt als Guthaben für den neu hinzugekommenen Wurzelknoten). Auf diese Weise bleiben die amortisierten Kosten für decreasekey in O(1), da sämtliche cascading cuts vom angesammelten Guthaben bezahlt werden können. 10 Der höchste Index des Rang-Arrays ist ja gerade die maximale Anzahl von Söhnen, die ein Knoten haben kann. Die Schranke von O(log n) ergibt sich, weil die Anzahl der Knoten in einem Baum stets exponentiell bezogen auf den Rang der Wurzel ist (siehe 3.2.1) und damit die Anzahl der Söhne eines (Wurzel-)Knotens immer höchstens logarithmisch in der Gesamtzahl der Knoten sein kann. Beweise hierzu findet der interessierte Leser z.B. in [Fredman 1987], [Cormen 1990] oder [Ottmann 1996]. Vgl. dazu auch die Funktion maxRank in Abschnitt 5.3.1. 19 Die amortisierten Kosten für das Entfernen eines beliebigen Knotens setzen sich aus denen von decreasekey und deletemin zusammen und liegen somit in O(log n). Dies ist bei beiden beschriebenen Varianten der Implementation der Fall, da – auch wenn keine Konsolidierung stattfindet – die Söhne des entfernten Knotens in die Wurzelliste aufgenommen werden und für sie Guthaben bezahlt wird. Zur Berechnung der tatsächlichen und der amortisierten Kosten wird jedem Zustand i des F-Heaps eine natürliche Zahl πi, das „Potential“, zugeordnet. Dabei bezeichnet i den Zustand nach der i-ten Operation auf einem zu Beginn leeren F-Heap. Dieses Potential wird definiert als die Anzahl der Knoten in der Wurzelliste plus zweimal die Anzahl der markierten Knoten, die nicht in der Wurzelliste stehen. Wie man sieht, gibt πi immer genau das oben beschriebene Guthaben an. Für einen leeren F-Heap ist das Potential offensichtlich 0, bei jedem Einfügen erhöht es sich um 1, beim Markieren eines Knotens um 2. Beim Verbinden zweier Bäume erniedrigt sich das Potential um 1, bei einem cascading cut um 2, was jeweils genau dem bei diesen Schritten „aufgebrauchten“ Guthaben entspricht. Die amortisierten Kosten ai der i-ten Operation werden definiert als die tatsächlichen Kosten ti zuzüglich der Erhöhung des Potentials, also ai = ti + (πi - πi-1). Die gesamten amortisierten Kosten nach einer Folge von n Operationen sind dann n ∑ i =1 ai = n ∑ i =1 ti + π n und bilden somit eine obere Schranke für die tatsächlichen Kosten, da das Potential immer eine nichtnegative Zahl ist. Anschaulich kann man sich dies dadurch klarmachen, daß die amortisierten Kosten immer noch das Restguthaben mit einschließen. Ein Teil der Zielsetzung der vorliegenden Arbeit war es, diese Veranschaulichung der amortisierten Kosten im Vergleich zur tatsächlichen Laufzeit ebenfalls in einer Visualisierung umzusetzen. Das folgende Kapitel beschreibt verschiedene Ansätze der Visualisierung, insbesondere der Animation von Algorithmen. 20 Kapitel 4 Animation Die Animation von Algorithmen ist eine Möglichkeit der Visualisierung von Sachverhalten. In diesem Kapitel werden zunächst einige Grundlagen und Begriffe der Softwarevisualisierung vorgestellt. Daran schließt sich eine Einordnung des vorliegenden Ansatzes in den Kontext der Algorithmenanimation an. Dabei werden wichtige Konzepte und Aspekte der Animation behandelt und am Fall des vorliegenden Programms exemplifiziert. 4.1 Visualisierung von Datenstrukturen Die Visualisierung von Sachverhalten, insbesondere von Lerninhalten, hat oft eine entscheidende Bedeutung für den Lernerfolg. Studien wie [Lewalter 1997] belegen, daß „insbesondere Animationen zu einer bedeutsamen Steigerung der Lernleistung im Vergleich zu einer reinen Textdarbietung führen“. Dies gilt ganz besonders für die Vermittlung von Datenstrukturen. Die Beschreibung in Worten mag zwar die einzig exakte Definition sein, gerade bei den Fibonacci-Heaps ist diese für sich allein genommen jedoch nicht einfach zu begreifen. So finden sich in praktisch allen Büchern und Aufsätzen zu diesem Thema unterstützende Grafiken, die das Verständnis in der Regel entscheidend erleichtern [Fredman 1987] [Cormen 1990] [Ottmann 1996]. 4.1.1 An einer Visualisierung beteiligte Parteien Bei jeder Art der Softwarevisualisierung kann man vier beteiligte Personen oder Gruppen unterscheiden, die sich teilweise überschneiden können. Zum einen ist dies der Programmierer des zu visualisierenden Programms oder Algorithmus. Eine wichtige Rolle kommt dem Entwickler des Animationssystems zu, mit dem Algorithmen dargestellt werden können. Weiterhin gibt es den „Visualisierer“, die Person, die zum ausgewählten 21 Algorithmus oder Programm mit Hilfe des Visualisierungssystems eine Darstellung spezifiziert. Am Ende steht schließlich der Benutzer oder Betrachter, für den die Visualisierung geschrieben wurde. Bei der vorliegenden Arbeit fielen die Rollen von Programmierer, Entwickler und Visualisierer zusammen. Allerdings wurde für die Entwicklung des Visualisierungssystems die Animationsbibliothek JEDAS verwendet, so daß alle Personen, die bei der Entstehung von JEDAS beteiligt waren, mit zu den Entwicklern zu zählen sind. 4.1.2 Einzelrechner oder verteiltes System Ein Aspekt, der beim Erstellen einer Visualisierung entschieden werden muß, ist die Frage, ob Algorithmus und Animation auf ein und derselben Maschine oder auf einem verteilten System, d.h. in einem Netzwerk mit Client und Server ablaufen soll. Beide dieser Varianten haben Vor- und Nachteile, die im folgenden kurz beschrieben werden sollen. In einem verteilten System laufen Algorithmus und Animation auf verschiedenen Rechnern ab. Der Benutzer, der am Client-Rechner die Animation betrachten möchte, startet über das Netzwerk den Algorithmus auf dem Server. Dieser schickt Animationsinformationen zurück an den Client, der diese mit Hilfe eines dafür vorgesehenen Programms abspielt. Solch eine Abspielroutine könnte zum Beispiel durch ein Plugin für einen Webbrowser realisiert werden. Der Vorteil eines solchen Systems besteht darin, daß auf dem Rechner des Benutzers der Algorithmus selbst gar nicht ablaufen muß. Dadurch, daß auf dem Client nur das Abspielprogramm installiert sein muß, spart man Speicherplatz und Rechnerkapazität. Ein weitaus größerer Vorteil ist jedoch, daß ein derartiges System sich sehr leicht erweitern läßt. Neue Animationen (zum Beispiel für andere Algorithmen) können auf dem Client-Rechner abgespielt werden, ohne daß der Benutzer zusätzlichen Aufwand hat. Der Algorithmus selbst muß nur auf dem Server bereitgestellt werden. Demgegenüber steht der Nachteil der ständig notwendigen Datenübertragung. Es müssen sowohl Animationsinformationen vom Server zum Client als auch Benutzereingaben vom Client zum Server übermittelt werden. Dies kann vor allem bei Übertragungen mit niedrigerer Bandbreite (beispielsweise über eine Modemverbindung) Verzögerungen mit sich führen, was einen flüssigen Ablauf stört. Dieses Problem stellt sich nicht beim Ablaufen von Algorithmus und Animation auf einem Rechner. Hier wird das ganze Programm auf dem Rechner des Benutzers installiert oder 22 vor Ablauf über eine Netzverbindung dahin übertragen. Der Nachteil eines solchen Systems ist die größere Datenmenge, der Vorteil der in der Regel reibungslose Ablauf, da während des Programmablaufs keine Datenübertragung stattfinden muß. Daraus ergibt sich insbesondere eine Verbesserung in der Interaktivität. Benutzereingaben müssen nicht an einen Server übermittelt werden, sondern wirken sich sofort und direkt aus. Aus diesem Grund wurde für die vorliegende Arbeit das Modell eines autonomen Systems, das auf einem Rechner abläuft, gewählt. 4.2 Algorithmenanimation Da Fibonacci-Heaps im wesentlichen durch die auf ihnen erklärten Operationen definiert sind, ist es natürlich wichtig, gerade diese durch Visualisierung verständlich zu machen. Die Operationen sind dynamische Prozesse, sie verändern die Struktur des Heaps. Zur Visualisierung eignet sich dafür optimal die Animation, da so die gesamte Operation als Bewegung dargestellt werden kann. In Textbüchern hingegen kann nur mit „VorherNachher“-Darstellungen oder bestenfalls mit Serien von Einzelbildern gearbeitet werden. Auch Stasko [Stasko 1998b] sieht in der Animation das geeignete Instrument zur Visualisierung von Algorithmen und Prozessen. Selbst wenn Veränderungen, beispielsweise Zeigeraktualisierungen, im Algorithmus punktuell ablaufen und damit eine Animation, die einen Pfeil kontinuierlich verändert, nicht die „Programmwirklichkeit“ wiedergibt, ist dieses Vorgehen dennoch verständlich und führt den Betrachter nicht zu der falschen Annahme, der Algorithmus nehme die Änderungen ebenso graduell vor. Es ist im Gegenteil gerade die kontinuierliche Bewegung, die den Benutzer die Übersicht behalten läßt. Es herrscht allerdings große Einigkeit darüber, daß sich die Algorithmenanimation nur für genügend kleine Datenmengen eignet. Bei zu großem Umfang dauern die Bewegungen zu lange, und der Benutzer wird ungeduldig. 4.2.1 Anwendungen Stasko und Lawrence [Stasko 1998c] nennen im Bereich der Lehre vor allem drei Einsatzmöglichkeiten der Animation von Algorithmen. Sie kann zum einen im Lehrvortrag vom Dozierenden als ergänzendes Hilfsmittel zur Erklärung von Sachverhalten dienen. 23 Eine zweite Möglichkeit ist es, die Animationen von den Lernenden in einer (z.B. vorlesungsbegleitenden) praktischen Übung verwenden zu lassen. Drittens können Studierende die Animationen unabhängig bzw. außerhalb von Lehrveranstaltungen zum eigenständigen Lernen benutzen. Alle drei Verwendungsmöglichkeiten werden vom hier entworfenen Programmpaket unterstützt. Vor allem erlaubt die plattformunabhängige Implementation, die Simulation auch auf dem privaten Rechner auszuführen 4.2.2 Das Pfad-Transitions-Paradigma Ebenfalls von Stasko stammt das Pfad-Transitions-Paradigma („path-transition paradigm“), ein Konzept, auf dem die Animationssysteme XTango, Polka und letzten Endes auch die für die vorliegende Arbeit verwendete Bibliothek JEDAS basieren. Dieser Ansatz sieht vier abstrakte Grundtypen vor, aus denen sich alle Animationen zusammensetzen: grafische Objekte, Orte, Pfade und Transitionen. Die Objekte bewegen sich entlang eines Pfades, der aus einer Kette von Orten besteht. Eine Transition ist gegeben durch ein Objekt, einen Pfad und die Art der Transition (Bewegung, Farbwechsel, Größenänderung, etc.). Auf diesen Grundbausteinen aufbauend lassen sich komplexe Animationen realisieren [Stasko 1998b]. 4.2.3 Interaktion zwischen Algorithmus und Animation Grundsätzlich basiert die vorliegende Implementation auf dem von Brown und Sedgewick vorgestellten Konzept der „interesting events“ [Brown 1998c]. Hierbei werden im Algorithmus an den „interessanten“ Stellen Anweisungen eingefügt, die dem Abspielmodul Befehle zur Animation übermitteln. Die Abspielroutine interpretiert diese dann und setzt die entsprechenden Animationen aus den „primitiven“ Schritten zusammen. Zu den „interesting events“ zählen hier insbesondere die Baummanipulationen cut, link und remove, aber auch die dafür notwendigen Aktionen wie das Zentrieren von Bäumen oder das Schließen von Lücken in der Wurzelliste. Bei jeder Heapoperation werden daher während des Ablaufs eine ganze Reihe von Animationsinformationen erstellt, die in einer Warteschlange gepuffert werden. Die Abspielroutine liest ständig die Informationen vom Puffer und führt die Animationen aus. Dabei werden die einzelnen Schritte solange simultan ausgeführt, bis ein STOP-Signal gelesen wird. Danach wartet die Routine, bis alle anstehenden Animationsschritte beendet sind, und liest dann die nächste Information. 24 Dieses Verfahren beschleunigt trotz der zusätzlichen Zwischenpufferung die Animation, weil die Heapoperationen nicht auf die Animation „warten“ müssen. So kann zum Beispiel noch während des Ablaufs einer Animation die nächste Operation am F-Heap durchgeführt werden. Allerdings hat diese Methode auch zur Konsequenz, daß gewisse Knoteninformationen, die eigentlich nur für die Animation von Bedeutung sind, zusätzlich im F-Heap selbst gespeichert werden müssen. Ein typisches Beispiel dafür ist die geänderte Position eines Knotens, für den zusätzlich eine Kante mit aktualisiert werden muß. Da dies zwei eigenständige Animationsschritte sind (obwohl sie simultan ablaufen), muß z.B. für die Animation der Kante bereits die neue Position des Knotens bekannt sein, während sich dieser noch darauf zubewegt. Es stellt sich jedoch heraus, daß außer der Position jedes Knotens die einzige doppelt benötigte Information der Zeiger auf das aktuelle Minimum ist.11 Das Problem, wie und wo für jeden Knoten seine aktuelle Position gespeichert werden soll, löst sich durch die Knotenstruktur praktischerweise von selbst; wir haben nämlich in jedem Knoten noch ein freies Feld item für dessen „Inhalt“ zur Verfügung. Dieses wird nicht benötigt, weil ja bei der Animation keine „echten“ Daten mit dem F-Heap verwaltet werden müssen, und bietet sich deshalb ideal zur Speicherung der Position an. Bei jeder Berechnung einer Bewegung wird diese Position aktualisiert, noch bevor der Knoten im Animationsfenster sie tatsächlich erreicht. Dadurch können bereits weitere Animationsschritte und weitere Heap-Operationen berechnet werden, während die Animation noch abläuft. Die Knoteninformationen werden im Animationsmodul durch zwei Objekte repräsentiert. Der Knoten selbst wird dargestellt durch ein zusammengesetztes Graphikobjekt: es besteht aus einem Kreis, der den Knoten symbolisiert, und einem Textobjekt, das den Schlüsselwert als Zahl innerhalb des Kreises darstellt. Das zweite Objekt ist eine Linie, die entweder die Verbindungskante zum Vater des Knotens symbolisiert oder die Kante zum linken Bruder, falls es sich um einen Knoten in der Wurzelliste handelt. Diese beiden Objekte sind in zwei Arrays abgespeichert und zwar an der Position, deren Index dem Schlüssel des repräsentierten Knotens entspricht. Der Knoten mit Schlüssel 32 beispielsweise wird also dargestellt durch ein zusammengesetztes Objekt, das an Position 11 Die doppelte Speicherung von Informationen erscheint insgesamt unökonomisch und sollte in der Regel vermieden werden. Sie kann aber auch sinnvoll sein, weil sie eine strikte Trennung von Algorithmus und Animation erlaubt. Bei Staskos und Turners XTango-Animation von Paired Heaps [Stasko 1992] wird in der Animationskomponente sogar fast die gesamte Datenstruktur „gespiegelt“, was in der vorliegenden Arbeit glücklicherweise nicht nötig ist. 25 32 des Knotenarrays liegt, und ein Linienobjekt, das an Position 32 des Linienarrays gespeichert ist. So ist ein einfacher Zugriff auf Knoten und Kanten über den Schlüsselwert möglich, und ein zusätzliches Speichern des Schlüssels entfällt. 4.2.4 Einordnung des vorliegenden Ansatzes in Browns Taxonomie Brown [Brown 1998a] charakterisiert Darstellungen von Algorithmenanimationen entlang der drei Dimensionen Inhalt, Persistenz und Transformation. Der Inhalt bewegt sich dabei auf einer Skala, dessen Endpunkte mit direkt und synthetisch bezeichnet werden können. Direkte Darstellungen zeigen Bilder, die isomorph zu den Strukturen des animierten Algorithmus sind. Das heißt, die dargestellten Objekte entsprechen Programmobjekten (Variablen, Zeigern, o.ä.). Von der Darstellung kann auf den aktuellen Zustand der Datenstruktur geschlossen werden und umgekehrt. Synthetische Darstellungen dagegen haben keine Entsprechung in Programmvariablen. Sie zeigen oft Abstraktionen der Daten oder Zusatzinformationen an. Das vorliegende Programmpaket stellt im Animationsfenster eine relativ direkte Abbildung der Datenstruktur dar. Mit dem Zusatzwissen der zyklischen Verkettung von Brüdern und der Konvention, daß der Sohnzeiger eines Knotens immer auf den am weitesten links stehenden Sohn weist, kann die gesamte Datenstruktur aus dem Bild rekonstruiert werden. Das Statistikfenster hingegen gibt eher synthetische Informationen wieder. Die Laufzeiten der Operationen haben keine Entsprechung in der Datenstruktur; außerdem sind die Informationen „künstlich“ aufbereitet, weil sie eine starke Vereinfachung der wirklichen Laufzeiten (z.B. „eine Einfügeoperation benötigt 1 Schritt“) repräsentieren. Die Persistenz gibt an, ob eine Darstellung Informationen über einen augenblicklichen Zustand zeigt oder rückblickend auch vorherige Zustände und die Veränderungen bis zum gegenwärtigen Zeitpunkt beschreibt. Entsprechend kann man die Pole dieses Kontinuums mit Augenblick und Vorgeschichte bezeichnen. Auch hier unterscheiden sich in der vorliegenden Animation die beiden Anzeigefenster voneinander. Die Animationsfläche zeigt zu jedem Zeitpunkt nur einen augenblicklichen Zustand an, während das Statistikdiagramm einen Überblick über die letzten Operationen gibt, also auch die Vorgeschichte mit visualisiert. Die Dimension der Transformation bewegt sich zwischen den Endpunkten diskret und inkrementell. Während inkrementelle Transformationen fließende Übergänge zwischen Zuständen darstellen, zeigen diskrete Transformationen lediglich Einzelbilder an. Es ist 26 klar, daß es sich hier ebenfalls um ein Kontinuum handelt, denn auch fließende Animationen setzen sich letztendlich aus Einzelbildern zusammen. Im Falle der vorliegenden Arbeit finden im Animationsfenster inkrementelle Transformationen statt, im Statistikfenster dagegen diskrete. Es findet dort nämlich nur einmal pro ausgeführter Operation eine Aktualisierung statt. Brown unterscheidet zusätzlich noch, ob eine Darstellung generisch, das heißt vielseitig verwendbar, oder an einen speziellen Algorithmus angepaßt ist. Die Implementation im Falle dieser Arbeit ist relativ stark angepaßt an die spezielle Struktur der Fibonacci-Heaps. Andererseits kann der Animationsabspieler mit wenigen Modifikationen auch zur Darstellung und Animation von Binomial Queues, Paired Heaps oder für beliebige andere Graphen verwendet werden. 4.3 Ausgewählte Aspekte und ihre Umsetzung Die Darstellung und Animation der F-Heaps sollte möglichst genau den Aufbau dieser Datenstruktur und die Funktionsweise der Algorithmen wiedergeben, gleichzeitig aber übersichtlich und verständlich bleiben. [Baecker 1998] beschreibt diesen Sachverhalt folgerndermaßen: Animating programs for pedagogical purposes is not a trivial endeavor. [...] To be effective, algorithm animation must abstract or highlight only the essential aspects of an algorithm. We must decide [...] which data to represent, how they are to be visualized, and when to update their representations during the execution of a program. Most importantly, we must try to enhance relevant features and suppress extraneous detail [...]. Dieser Grundsatz und weitere Anforderungen an Algorithmenanimationen sollen im folgenden am Beispiel ausgewählter Aspekte des vorliegenden Programms beleuchtet werden. 4.3.1 Repräsentation der Bäume Das obige Zitat erhebt die Frage, ob es zweckmäßig ist, alle Details der Datenstruktur in der Visualisierung wiederzugeben. Dies betrifft insbesondere die zahlreichen Felder und Zeiger eines Knotens. So ist es beispielsweise unnötig, den Rang eines Knotens extra anzugeben, da bei einer geeigneten Darstellung (wie zum Beispiel der in Abbildung 3.1 gegebenen) die Anzahl der Söhne immer klar zu erkennen ist. Andere Informationen wie 27 der Schlüsselwert sind dagegen unabdingbar. Fraglich ist die Darstellung der Zeiger auf die Brüder eines Knotens. Die doppelte Verkettung wiederzugeben scheint unnötig, selbst bei einfachen Kanten zwischen Söhnen eines Knotens ist die Baumstruktur immer schwieriger zu erkennen. Andererseits sollte die Brüderbeziehung zwischen Knoten in der Wurzelliste erkennbar sein. Hier ist also ein geeigneter Kompromiß zwischen detailgetreuer Wiedergabe und Übersichtlichkeit zu finden. Die Darstellungsweise der vorliegenden Arbeit lehnt sich an eine Konvention an, die häufig in der Literatur zu finden ist und die auch in den Abbildungen des vorigen Kapitels verwendet wurde. Dabei werden lediglich die Schlüsselwerte und die Vaterzeiger der Knoten angezeigt. Der einzige Unterschied in der vorliegenden Darstellung ist die zusätzliche Verwendung von Kanten in der Wurzelliste, die die Bäume untereinander verbinden. Für Bäume selbst gibt es eine Anzahl verschiedener Visualisierungstechniken. Beispiele dafür finden sich bei [Jeffery 1998]. Die meisten dieser Ansätze, wie zum Beispiel die Repräsentation als ineinander geschachtelte Rechtecke, sind für den hier verfolgten Zweck jedoch eher ungeeignet, so daß letztendlich die konventionelle Darstellung als Knoten und Kanten, der die Bezeichnung „Baum“ zu verdanken ist, gewählt wurde. Lediglich eine Variante hiervon wurde noch in Erwägung gezogen: eine Darstellung in Kreisform könnte die zyklische Verkettung der Wurzelliste optimal verdeutlichen, die einzelnen heapgeordneten Bäume würden dann strahlenförmig in alle Richtungen ausgehen, wobei die Listen der Söhne wieder als Kreise repräsentiert werden könnten. Diese Variante mußte aber mangels Übersichtlichkeit wieder verworfen werden. Bei der Darstellung einzelner Bäume gilt es zu entscheiden, wie die Knoten angeordnet werden sollen. Da die Liste der Söhne eines Knotens zyklisch ist, könnte prinzipiell der am weitesten links stehende Sohn willkürlich unter allen Söhnen gewählt werden. Um aber ein einheitliches Modell zu erhalten, wurde für die vorliegende Arbeit beschlossen, immer den „ersten“ Sohn – also den, auf den der Sohnzeiger des Vaters weist – ganz links anzuzeigen. Alle weiteren Söhne werden zum Zeitpunkt ihres Einfügens rechts davon dargestellt, so daß die Ordnung der Söhne von links nach rechts die Reihenfolge ihres Einfügens widerspiegelt. Des weiteren muß entschieden werden, wie ein Vaterknoten relativ zu seinen Söhnen angezeigt werden soll. Hier spielen vor allem ästhetische Gesichtspunkte eine Rolle. Abbildung 4.1 zeigt verschiedene Möglichkeiten der Darstellung. 28 2 2 5 9 3 12 10 5 6 2 9 12 3 10 8 5 6 9 12 3 10 8 linksbündig 6 8 zentriert (i) zentriert (ii) Abbildung 4.1: Verschiedene Möglichkeiten für das Layout der Bäume von Fibonacci-Heaps. Die linksbündige Variante läßt die Liste der Söhne an der x-Position beginnen, an der der Vater steht. Dieses Layout ist programmtechnisch relativ einfach zu realisieren. Obwohl die Darstellungsweise zum Beispiel bei der Visualisierung von Binomial Queues sehr sinnvoll ist und auch für F-Heaps des öfteren in der Literatur zu finden ist, wirkt ein solcher Baum mit größerem Verzweigungsfaktor der Wurzel schnell sehr unübersichtlich.12 Eine zentrierte Variante erscheint daher geeigneter. Mit „zentriert“ ist hier gemeint, daß die Wurzel jedes Teilbaums mittig über ihren Söhnen steht. Zum Festlegen der „Mitte“ gibt es grundsätzlich zwei Ansätze, die in gewisser Weise analog sind zur Unterscheidung zwischen arithmetischem Mittel und Median. So kann man einerseits die Wurzel genau in die Mitte der Grenzen ihres Baums stellen, wie in Abbildung 4.1 (i) dargestellt wird; die Position der Wurzel wäre dann (l + r)/2 (arithmetisches Mittel), wobei l und r für den linken und rechten Rand des Baums stehen. Dieser Ansatz ist optisch jedoch nicht immer befriedigend, weil die Kanten von der Wurzel zu den Söhnen sehr „unruhig“ verlaufen können. Deshalb wurde als Mitte hier die x-Position des „mittleren“ Sohns in der Liste der Söhne gewählt (Median). Genauer heißt dies, bei einer ungeraden Anzahl n von Söhnen steht die zentrierte Wurzel genau über dem (n + 1)/2-ten Sohn, bei einer geraden Anzahl zwischen dem (n/2)-ten und dem (n/2 + 1)-ten Sohn. Diese Variante, die in Abbildung 4.1 (ii) zu sehen ist, ist optisch ansprechender, weil sie einen geordneteren Gesamteindruck vermittelt.13 12 13 Dasselbe gilt selbstverständlich analog für die rechtsbündige Darstellung. Dieser Eindruck ist zwangsläufig subjektiv, wurde aber von mehreren befragten Personen bestätigt. 29 Allerdings ist der ästhetische Gewinn durch das Zentrieren nicht ganz frei von Kosten. Manipulierte Bäume müssen neu zentriert werden, immer wenn sie einen Teilbaum verlieren oder hinzubekommen. Dies ist zum einen mit hohem programmtechnischem Aufwand verbunden, zum anderen mit längerer Rechenzeit. Diese fällt zwar beim Ablauf der Animation nicht ins Gewicht, muß aber deshalb erwähnt werden, weil die in Abschnitt 3.4.3 beschriebenen Laufzeitschranken nicht mehr gültig sind, wenn man die Animationsberechnungen mit einbezieht. 4.3.2 Verwendung von Farben Mit der immer weiter steigenden und inzwischen fast flächendeckenden Verbreitung von Farbdisplays kommt im Bereich der Animation auch der Farbgestaltung eine besondere Bedeutung zu. Farbe birgt ein großes Potential, da sie als eine „natürliche“ Form der Unterscheidung Informationen ohne zu große kognitive Belastung für den Benutzer darstellen kann. Allerdings darf Farbe auch nicht übertrieben eingesetzt werden, da dies auf Kosten der Übersichtlichkeit gehen kann. Brown und Hershberger [Brown 1998b] identifizieren fünf Möglichkeiten, Farbe als Visualisierungsinstrument zu verwenden: zur Kennzeichnung von Zuständen der Datenstrukturen, zum Hervorheben von Aktivitäten, zur optische Verknüpfung verschiedener Ansichten, zum Betonen von Mustern sowie zum Sichtbarmachen der Vergangenheit. Insbesondere die beiden erstgenannten Punkte wurden auch in der vorliegenden Animation verwendet. Wichtige Zustände von Knoten werden in der Animation durch Farbe kontrastiert. So haben markierte Knoten eine andere Hintergrundfarbe (in der Standardeinstellung ist dies magenta) als die nichtmarkierten (hellgrau), der Minimalknoten hat eine eigene Farbe (Standard: gelb).14 Bei einigen Operationen wird Farbe außerdem zur Hervorhebung einer Aktivität eingesetzt. Ein neu eingefügter Knoten wechselt beim Erscheinen die Farbe und geht von einem dunklen Blauton zur hellgrauen Standardfarbe über. Dasselbe geschieht beim Herabsetzen eines Schlüssels auf einen neuen Wert. Auf diese Weise wird die Aufmerksamkeit des Betrachters auf die entsprechende Stelle im Animationsfenster 14 Diese Strategie nutzt den Umstand aus, daß der Minimalknoten nicht selbst markiert ist. Im einzig möglichen Fall, dem Herabsetzen eines markierten Knotens auf einen neuen minimalen Schlüssel, endet der Knoten in der Wurzelliste, und die Markierung wird ohnehin bedeutungslos, so daß sie nicht mehr angezeigt werden muß. 30 gelenkt. Aus einem ähnlichen Grund wird ein Farbwechsel auch beim link-Schritt verwendet. Hier werden zwei Bäume verbunden, die möglicherweise weit voneinander entfernt dargestellt sind. Damit dies für den Benutzer nicht ganz unvermutet vor sich geht, blinken die beiden Wurzelknoten vor dem Verbinden kurz auf (Standardeinstellung: dreimalig in rot), was auch symbolisch für das Vergleichen der Schlüssel gedeutet werden kann. Eine Doppelrolle kommt der Farbe bei der delete-Operation zu. Der zu entfernende Knoten wird nämlich ebenfalls eingefärbt, um ihn für die Löschung zu kennzeichnen. Dies ist deshalb notwendig, weil der Knoten nach dem Abtrennen von seinem Vater zuerst in die Wurzelliste integriert wird (falls er nicht schon dort war), bevor er daraus wieder (mittels remove) entfernt wird. Zwischen diesen beiden Schritten müssen jedoch eventuell noch anfallende cascading cuts ausgeführt werden, so daß leicht der Blick vom eigentlich zu entfernenden Knoten abgelenkt werden kann. Das Einfärben ist also gleichzeitig die Kennzeichnung eines Zustandes – der Knoten wird als „zu löschend“ markiert – und das Hervorheben einer noch anstehenden Aktivität. 4.3.3 Animation der Manipulationen an F-Heaps Die wichtigste Aufgabe der vorliegenden Arbeit besteht darin, die Operationen auf Fibonacci-Heaps in ihrer Dynamik darzustellen. Dies bedeutet konkret, daß zwei aufeinanderfolgende Zustände durch Animation ineinander übergehen und die optische Kohärenz gewahrt bleibt. Dazu müssen die Einzelschritte link, cut und remove animiert werden, die in 3.2.3 erläutert wurden und aus denen sich die übrigen Operationen im wesentlichen zusammensetzen. Die Abbildungen 3.4, 3.5 und 3.6 zeigten bereits die Zustände des F-Heaps vor und nach den jeweiligen Schritten. Die Überlegung ist nun, die Übergänge möglichst fließend und damit für den Betrachter gut nachvollziehbar zu präsentieren. Dabei ergeben sich eine Reihe miteinander konkurrierender Anforderungen, die in Einklang zu bringen von entscheidender Bedeutung ist. So sollte einerseits die Animation fließend dargestellt werden, was das Synchronisieren möglichst vieler Bewegungen erfordert. Andererseits darf darunter auf keinen Fall die Verständlichkeit für den Benutzer leiden, der durch zu viele synchron ablaufende Bewegungen leicht den Überblick verlieren kann. Als Beispiel hierfür soll das Abtrennen eines Teilbaums (cut) genannt werden. Bei diesem Schritt muß zusätzlich zum Trennen des Knotens von seinem Vater und dem 31 Bewegen zum Zielort rechts neben dem Minimalknoten der „beschnittene“ Baum wieder zentriert und noch Platz zum Einfügen in der Wurzelliste geschaffen werden. In der vorliegenden Animation werden nicht alle dieser Vorgänge gleichzeitig angezeigt, sondern in zwei Teilschritten. Zunächst wird das Abtrennen des Knotens, das Zentrieren des Restbaums und das Markieren des Vaters synchron dargestellt. Danach wird in der Wurzelliste Platz geschaffen, eventuelle Lücken geschlossen und der abgetrennte Teilbaum rechts vom Minimalknoten eingefügt. So behält der Betrachter einen besseren Überblick über die Operation, während gleichzeitig eine fließende und zügige Bewegung dafür sorgt, daß die Aufmerksamkeit nicht durch Langeweile abgelenkt wird. Weitere Anforderungen, die oft schwierig zu vereinbaren sind, sind zwei von Gloors [Gloor 1998a] „zehn Geboten“ der Algorithmenanimation, nämlich Einheitlichkeit („Be consistent“) und Übersichtlichkeit („Be clear and concise“). Einerseits ist es also wünschenswert, ähnliche Prozeduren auch immer gleich zu behandeln. Ein Beispiel hierfür ist das Einfügen von einem oder mehreren Knoten in die Wurzelliste. Dies geschieht im allgemeinen mit der meld-Operation; dabei werden die neuen Knoten grundsätzlich rechts neben dem Minimum in die Wurzelliste eingehängt. Es erscheint daher konsequent, dies genauso nach dem Entfernen eines beliebigen Knotens (remove) aus der Wurzelliste durchzuführen. In diesem Fall würde die Liste der Söhne des entfernten Knotens ebenfalls rechts neben dem Minimum eingefügt werden. Eine Animation dieses Vorgangs wäre jedoch höchst unübersichtlich, weil die Söhne möglicherweise über eine lange Distanz im Animationsbereich über andere Knoten hinweg bis zum Ziel bewegt werden müßten. Dort muß außerdem Platz geschaffen sowie die Lücke am früheren Standort geschlossen werden. 7 2 5 1 9 12 3 10 7 1 5 9 12 3 10 6 6 Abbildung 4.2: Das Entfernen des Knotens mit Schlüssel 2 unter Verwendung der normalen meldOperation. Die (grau hinterlegten) Söhne des entfernten Knotens werden rechts neben dem Minimalknoten (s. Pfeil) in die Wurzelliste eingefügt. Sehr viel übersichtlicher ist es für den Betrachter, wenn die Söhne einfach anstelle des entfernten Vaters in die Wurzelliste eingefügt werden (wie es in Abbildung 3.6 dargestellt wurde). Es müssen keine langwierigen Verschiebungen stattfinden, und die anfallenden 32 Zeigeradjustierungen können ebenfalls auf einfache und verständliche Weise dargestellt werden. Auch hier wurde der besseren Übersichtlichkeit Vorrang vor absoluter Konsistenz eingeräumt und für die remove-Methode eine eigene Verschmelzeprozedur geschrieben, anstatt – was programmtechnisch ökonomischer gewesen wäre – die vorhandene meldOperation zu verwenden. Ein weiteres Beispiel für Überlegungen dieser Art taucht auch beim Verbinden zweier Bäume (link) auf. Hier wurde jedoch der Einheitlichkeit Rechnung getragen, obwohl dadurch gelegentlich Bäume über weite Distanzen bewegt werden müssen. So wird der anzuhängende Knoten immer ganz rechts in die Liste der bereits vorhandenen Söhne das anderen Knotens eingefügt, auch wenn der anzuhängende Baum vorher weiter links stand und damit über weitere Knoten einschließlich alle anderen Söhne hinweg bewegt werden muß. Dafür sieht der Benutzer, daß der neue Teilbaum nicht willkürlich angehängt wird sondern nach klaren Vorgaben. 4.3.4 Darstellung der Laufzeitanalyse Im Einklang mit Gloors „neuntem Gebot“ der Algorithmenanimation, „Include analysis and comparisons“ [Gloor 1998a], enthält das vorliegende Programm auch eine Darstellung der Laufzeitanalyse. Einerseits werden die Laufzeiten der unterschiedlichen Operationen bereits durch die Animation vermittelt. Da nahezu jede Zeigeraktualisierung durch eine Bewegung realisiert wird, wirkt die Animation sogar als „Hebel“ und verstärkt noch den Gegensatz zwischen Operationen mit konstanter und nicht konstanter Laufzeit. Andererseits kann die Animation allein freilich keine exakten Informationen zur Größenordnung der Laufzeiten geben und auch nicht das Konzept der amortisierten Analyse vermitteln. Ein gesonderte Visualisierung der Laufzeitanalyse wurde deshalb implementiert. Allerdings ergeben sich bei der Umsetzung eines solchen Statistikmoduls eine Reihe von Schwierigkeiten. Die entscheidende Frage beim Zählen der Kosten ist, was man als einen Schritt ansehen soll. Die exakteste Methode, nämlich jede Zeigeraktualisierung als einen Schritt zu betrachten, wirft das Problem der Unübersichtlichkeit auf. Schon die einzelnen Operationen mit konstanter Laufzeit bestehen ja aus einer höchst unterschiedlichen Anzahl von Pointerupdates, was Vergleiche sehr schwierig macht. Durch die vierfache Verkettung der Einzelknoten (mit Vater, Sohn, linkem und rechtem Bruder) sind in Fibonacci-Heaps 33 ohnehin schon sehr viele Aktualisierungen notwendig. Zusammengesetzte Operationen verstärken dieses Problem noch. Eine Vereinfachung der Zählweise erscheint – insbesondere im Kontext der Vermittlung von Lerninhalten – dringend notwendig, darf aber die tatsächlichen Laufzeiten nicht verschleiern. Zudem muß beachtet werden, daß die ermittelte Laufzeit mit der Potentialfunktion zur Berechnung der amortisierten Kosten in Einklang steht. In Anlehnung an die Definition der Potentialfunktion in [Fredman 1987] wurde folgende Definition der Laufzeiten zugrundegelegt: Die Methode insert benötigt stets genau einen Schritt. Dabei spielt es keine Rolle, ob der eingefügte Knoten zum neuen Minimum wird (und damit ein zusätzliches Pointerupdate erforderlich ist) oder nicht. Auch das Lesen des aktuellen Minimums, accessmin, und das Verschmelzen zweier Heaps, meld, kosten jeweils eine Einheit. Komplizierter wird es bei den übrigen Operationen. Die deleteminOperation benötigt zunächst einen Schritt für das Entfernen des Knotens. Zusätzlich fallen dann für das Erstellen des Arrays Kosten der maximal benötigten Länge an; diese Zahl entspricht dem maximalen Rang eines Knotens im F-Heap. Schließlich kommt noch für jedes Verschmelzen zweier Bäume gleichen Ranges (link) eine Kosteneinheit hinzu, so daß der Gesamtaufwand für das Entfernen des Minimums bei 1 + maxRank(n) + #links liegt. Die Operation decreasekey verursacht zunächst eine Kosteneinheit für das Herabsetzen des Schlüssels und das eventuell notwendige Abtrennen und Einfügen in die Wurzelliste. Dazu kommt ein weiterer Schritt für jeden cascading cut, also das zusätzliche Abtrennen markierter Vorfahren. Dieselben Kosten entstehen auch für das Entfernen eines beliebigen Schlüssels, da dieser Vorgang praktisch identisch abläuft. Bei der amortisierten Laufzeit wird zu den tatsächlichen Kosten noch die Erhöhung des Potentials durch die jeweilige Operation addiert. Wird das Potential verringert, zählt dies als negative Erhöhung, und die amortisierten Kosten sind niedriger als die tatsächliche Laufzeit. Tabelle 4.3 zeigt die sich nach dieser Definition ergebenden tatsächlichen Kosten und die höchstmöglichen amortisierten Kosten sowie deren Größenordnung. Operation insert accessmin meld deletemin decreasekey delete Tatsächliche Zeit 1 1 1 1 + maxRank(n) + #links 1 + #cascading cuts 1 + #cascading cuts Amortisierte Zeit 1+1 1 1 ≤ 1 + 2⋅maxRank(n) ≤1+3 ≤ 1 + maxRank(n) Schranke O(1) O(1) O(1) O(log n) O(1) O(log n) Tabelle 4.3: Laufzeiten der verschiedenen Heap-Operationen, wie sie vom Statistikmodul errechnet werden. Die Schranken beziehen sich auf die amortisierten Laufzeiten. 34 Wie man sieht, liegen die amortisierten Kosten bei allen Operationen außer deletemin und delete in O(1), sind also unabhängig von der Größe des F-Heaps. Nur das Entfernen des Minimums oder eines beliebigen Knotens erfordert eine Laufzeit, die – von der Größenordnung her – durch den höchstmöglichen Rang eines Knotens in einem F-Heap der Größe n beschränkt ist. Dieser maximale Rang liegt in O(log n), da die Zahl aller Nachkommen eines Knotens exponentiell in der Anzahl seiner Söhne ist. Die Ausgabe der Informationen über die tatsächliche und amortisierte Laufzeit erfolgt einmal als Text mit detaillierten Angaben zur tatsächlichen Laufzeit jeder Operation, der Potentialänderung, der sich daraus ergebenden amortisierten Kosten und der zugehörigen Laufzeitbeschränkung. DELETE-MIN: Deleted nodes: 1 Array length: 4 Number of links: 8 -------------------Actual time: 13 Actual time adds up to 13 Net increase: -9 Potential is now 2 -----------------------------------------------Amortized time: 4 Amortized time adds up to 15 Amortized time bounded by 2*[log(10)] + 1 = 9 Abbildung 4.3: Textausgabe der Statistikinformationen zur deletemin-Operation Zum anderen werden die tatsächliche und die amortisierte Laufzeit in einem Diagramm in einem separaten Statistikfenster aufgetragen. Diese visuelle Darstellung ermöglicht insbesondere die einfache Gegenüberstellung und den Vergleich von tatsächlicher und amortisierter Laufzeit, die als zwei verschiedenfarbige Kurven dargestellt werden. Auf diese Weise sieht kann Benutzer am Verlauf der Kurven schnell erkennen, daß die bisherige amortisierte Laufzeit immer eine obere Schranke für die bisherige tatsächliche Laufzeit ist. 4.3.5 Interaktivität und Benutzerfreundlichkeit Interaktivität bezeichnet die Möglichkeit der Beeinflussung der Animation durch den Benutzer. Dazu gehören neben grundlegenden Dingen wie dem Verkleinern und Vergrößern der Ansicht oder der Möglichkeit, die Animation zu stoppen und fortzusetzen auch die Wahl des Inputs. Das vorliegende Programmpaket besteht nicht nur aus der 35 Animation von festgelegten Abläufen, sondern soll es dem Betrachter ermöglichen, durch Interaktion den Algorithmus zu steuern. Die Simulation erlaubt es, in jedem Zustand des F-Heaps die nächste Heapoperation frei zu wählen. Gloor empfiehlt, den Benutzer wenigstens alle 45 Sekunden zur Interaktion mit dem System zu „zwingen“ [Gloor 1998a], was hier durch die ständige Wahl der nächsten Operation gewährleistet wird. Dabei sollte das Programm so tolerant wie möglich gegenüber fehlerhaften Eingaben sein oder solche möglichst von vornherein verhindern. Im Falle der vorliegenden Arbeit sind z.B. Operationen, die im aktuellen Zustand nicht durchführbar sind, in den Menüs nicht anwählbar. Solche Operationen wären beispielsweise das Entfernen des Minimums aus einem leeren F-Heap oder das Einfügen eines bereits bestehenden Schlüssels. Die Bedienung des Programmpakets sollte für den Benutzer so komfortabel wie möglich gestaltet werden. Dazu gehört unter anderem eine intuitive Benutzbarkeit. Dies bedeutet, daß sich die Grundzüge der Bedienung im wesentlichen an die Konventionen des Betriebssystems und gängiger Anwendungen halten sollte. Ein mit anderen Programmen vertrauter Benutzer wird das Paket dann ohne allzu große Schwierigkeiten bedienen können. Bei den meisten Betriebssystemen und Benutzeroberfächen ist heute die Maus das gängige Eingabegerät. Auch das vorliegende Programm ist darum für die Steuerung mit der Maus konzipiert. Unterschiedliche Menüs erlauben eine flexible Kontrolle, die den individuellen Gewohnheiten des Benutzers angepaßt ist. Es wird sowohl die konventionelle Bedienung mit Doppelklicks, Dialogfenstern und Menüleiste als auch das direkte Steuern mittels Kontextmenüs unterstützt. Ebenso ist das Eingeben von Schlüsselwerten mit oder ohne Tastatur möglich. Um dem Benutzer das Auswählen von Schlüsselwerten beim Einfügen eines neuen Knotens oder beim Herabsetzen eines Schlüssels zu ersparen – denn dabei darf kein Wert genommen werden, der schon im F-Heap vorkommt –, kann vom Programm zufällig ein Wert ausgesucht werden lassen. Ebenso ist es möglich, die gesamte nächste Operation zufällig wählen zu lassen. Die Angaben über die Laufzeit werden der Übersichtlichkeit halber in einem separaten Fenster dargestellt. Dieses kann jederzeit aus- und eingeblendet werden. Stasko [Stasko 1992] berichtet in seiner Studie über die Animation von Paired Heaps, daß von Benutzern häufig die Möglichkeit gewünscht wird, die Animation „zurückzuspulen“, um die Datenstruktur nochmals vor einer gerade ausgeführten Operation sehen zu können. Die Schwierigkeit bei der Umsetzung ist allerdings, daß die meisten Operationen im 36 Algorithmus nicht einfach umkehrbar sind. Eine Möglichkeit, trotzdem ein „undo“ zumindest des letzten Schritts zu realisieren, besteht darin, den F-Heap nach jeder Operation automatisch zu speichern und bei Bedarf diesen alten Zustand wiederherzustellen. Kapitel 5 Implementation Die konkrete Umsetzung der Animation und Simulation erfordert die Wahl einer Programmiersprache und gegebenenfalls vorhandener Hilfsmittel bzw. bereits vorhandener Programmbausteine. Dieses Kapitel nennt die Vor- und Nachteile der verwendeten Sprache Java, beschreibt das an der Universität Freiburg entwickelte Animationspaket JEDAS, welches zur Umsetzung der Animationen benutzt wurde, und erläutert die implementierten Klassen. Zunächst werden jedoch Fragen zu Unterschieden in konkreten Implementationen von FibonacciHeaps angesprochen. 5.1 Unterschiedliche Implementationsmöglichkeiten bei F-Heaps Konkrete Implementationen von Fibonacci-Heaps können im Detail variieren. Dies wirkt sich in den meisten Fällen lediglich auf die graphische Darstellung des F-Heaps aus, manchmal wird aber auch dessen Struktur oder sogar die Laufzeit einer Operation dadurch verändert. 5.1.1 Die delete-Operation Wie bereits in Abschnitt 3.3.2 beschrieben, kann die delete-Operation, die einen beliebigen Knoten aus dem F-Heap entfernt, auf zweierlei Art und Weise realisiert werden. Die vorliegende Implementation folgt dem Vorschlag aus [Fredman 1987], die delete- 37 Operation so zu auszuführen, daß die Wurzelliste nicht konsolidiert wird, wenn der entfernte Knoten nicht der mit minimalem Schlüssel ist. Zusätzlich zur Veränderung der Struktur des F-Heaps wird durch das Weglassen der Konsolidierung auch die Laufzeit beeinflußt. Die amortisierte Laufzeit von delete bleibt zwar auch bei dieser Variante in O(log n), weil das Potential sich dementsprechend erhöht (siehe 3.4.3), aber im konkreten Fall wird durch das Weglassen der zeitaufwendigen Konsolidierung das Verfahren erheblich beschleunigt, da die tatsächlichen Kosten für das Entfernen eines Knotens, der nicht Minimum ist, konstant sind (wenn man einmal von möglichen cascading cuts absieht). Diese Tatsache wird in der Animation noch verstärkt sichtbar, weil die Darstellung des Konsolidierens mit zahlreichen Bewegungen verbunden ist. 5.1.2 Abtrennen des Knotens bei decreasekey Ein weiteres Detail hat ebenfalls Einfluß auf die Struktur eines F-Heaps. Fredman und Tarjan trennen bei jeder decreasekey-Operation den Knoten mit herabgesetztem Schlüssel von seinem Vater (falls vorhanden) ab. Dies ist jedoch nicht immer notwendig. Wenn der Wert des herabgesetzten Schlüssels nicht kleiner als der des Vaters ist, muß kein cut (und damit auch keine Markierung des Vaters) vorgenommen werden. Der Algorithmus bei Schuierer15 und [Cormen 1990] berücksichtigt dies und führt die cut-Operation nur dann aus, wenn sie tatsächlich notwendig ist. Die vorliegende Implementation folgt dieser Variante. 5.1.3 Einfügen von Knoten in die Wurzelliste Anderes Details, in denen konkrete Implementationen variieren können, wirken sich weniger auf die Struktur des F-Heaps, wohl aber auf seine graphische Darstellung aus und sind für die vorliegende Arbeit deshalb ebenfalls von Belang. Zum einen ist dies die prinzipiell willkürliche Wahl, ob beim Einhängen in die Wurzelliste das einzufügende Element rechts oder links vom Minimum integriert werden soll. Dasselbe gilt für das Verbinden zweier Bäume, wo der neue Teilbaum entweder rechts oder links vom ersten Sohn (falls vorhanden) in die Liste der Söhne eingefügt werden kann. 15 Folien und Handouts zu Fibonacci-Heaps (Vorlesung „Algorithmentheorie“ im Sommersemester 1998). 38 Die vorliegende Implementation fügt neue sowie abgetrennte Knoten immer rechts vom Minimum in die Wurzelliste ein. Beim Verbinden zweier Bäume gleichen Ranges wird der Baum mit größerer Wurzel links von child in die Liste der Söhne eingefügt. Dieses Vorgehen erscheint auf den ersten Blick inkonsequent, dient aber der Anschaulichkeit der Animation. Da der child-Knoten einer Wurzel immer am weitesten links in der Liste der Söhne dargestellt wird und diese Liste zyklisch ist, wird der links neu eingefügte Knoten rechts von allen anderen Söhnen angezeigt. Dies gewährleistet die in 4.3.1 beschriebene Ordnung der Söhne und vermeidet gleichzeitig ein „Zerreißen“ der Liste der Söhne, wenn ein neuer Knoten eingefügt wird. 5.1.4 Das Löschen von Markierungen Ebenfalls Einfluß auf die graphische Darstellung hat die Entscheidung, ob die Markierungen von Knoten aufgehoben werden, wenn diese nach einem cut in die Wurzelliste eingefügt werden. In [Fredman 1987] ist dies nicht der Fall, der Algorithmus bei Schuierer löscht die Markierung sowohl in der cut-Operation als auch beim Eintragen in das Array während der consolidate-Operation. In [Cormen 1990] wird die Markierung eines abgetrennten Knotens ebenfalls entfernt, beim Konsolidieren werden allerdings nur die Knoten, die bei einem link an einen anderen Knoten angehängt werden, von ihrer Markierung befreit. Es wäre natürlich wünschenswert, in der Wurzelliste keine markierten Knoten zu haben, da deren Markierungen ohnehin redundant sind; diese Knoten werden nämlich weder bei cascading cuts berücksichtigt, noch erhöhen die Markierungen von Knoten der Wurzelliste das Potential des F-Heaps. Daher läge es nahe, einfach für jeden Knoten, der in die Wurzelliste gelangt, die Markierung zu löschen. Dies ist jedoch nicht immer möglich: beim Entfernen eines beliebigen Knotens wird die Liste seiner Söhne in die Wurzelliste integriert, ohne daß diese Söhne einzeln durchlaufen werden. Das müßte jedoch geschehen, um die Markierung zu entfernen, und würde eine zusätzliche Laufzeit von O(log n) erfordern. In der vorliegenden Implementation wird daher die Markierung nur beim Konsolidieren gelöscht. Dabei verliert jeder betrachtete Knoten in der Wurzelliste seine Markierung. Markierte Knoten, die durch cuts oder das Entfernen ihres Vaterknotens in die Wurzelliste 39 gelangen, behalten ihre Markierung bis zum nächsten Konsolidieren. Folglich befinden sich am Ende einer deletemin-Operation nur unmarkierte Knoten in der Wurzelliste. 5.2 Die Programmiersprache Java Das vorliegende Animationspaket wurde in der Programmiersprache Java realisiert. Im folgenden werden die Vor- und Nachteile dieser Wahl beschrieben. 5.2.1 Plattformunabhängigkeit Die Programmiersprache Java hat den großen Vorteil der Plattformunabhängigkeit. Der Programmcode wird nicht bis auf Betriebssystemlevel compiliert, sondern in den sogenannten Java byte code übersetzt, der von einem plattformspezifischen Interpreter ausgeführt wird. Auf diese Weise ist ein einmal compilierter Code auf allen Plattformen mit Java-Interpreter lauffähig. Allerdings muß der Begriff der Plattformunabhängigkeit relativiert werden. Auf unterschiedlichen Betriebssystemen kann nämlich insbesondere das Aussehen dargestellter Komponenten stark variieren. Sogar auf verschiedenen Rechnen, die dasselbe Betriebssystem verwenden, kann es Unterschiede geben, insbesondere in der Wahl der Fonts zur Darstellung von Text. Zusätzlich kann es in manchen Fällen vorkommen, daß Java byte code auf einem System fehlerfrei läuft, auf einem anderen jedoch Ausnahmen oder Abstürze verursacht. Zwei Beispiele seien hier genannt. Die vorliegende Anwendung bricht unter IRIX (einem UNIXDerivat, das auf SGI-Workstations eingesetzt wird) ab, wenn das Programm Menüpunkte deaktiviert, die nicht mehr anwählbar sein sollen. Unter Windows kommt es vereinzelt zu nicht reproduzierbaren Fehlermeldungen während der Animation. 5.2.2 Programmieren von Datenstrukturen Die Programmierung von Datenstrukturen erweist sich in Java als sehr komfortabel, da nicht zwischen Zeigern und Variablen unterschieden werden muß. Alle Variablen (mit Ausnahme der „Grundtypen“ boolean, byte, char, double, float, int, long und short) sind implizit Zeiger auf Objekte. Wenn also ein Node-Objekt Zeiger auf seinen linken und 40 rechten Bruder enthält, müssen diese nicht (wie in anderen Sprachen, beispielsweise C) als Zeiger deklariert werden. Jede Instanz der Klasse Node enthält einfach zwei weitere Node-Objekte left und right. 5.2.3 Geschwindigkeit Ein Nachteil von Java, der direkt mit dem Vorzug der Plattformunabhängigkeit zusammenhängt, ist die relativ langsame Laufgeschwindigkeit. Dadurch, daß der Java byte code während der Ausführung noch interpretiert (d.h. in für das Betriebssystem ausführbare Befehle übersetzt) werden muß, haben Java-Programme eine längere Laufzeit als beispielsweise in der Sprache C geschriebener und compilierter Code. Dieser Nachteil macht sich auch im vorliegenden Programm bemerkbar. Wenn eine größere Anzahl von Objekten im Animationsbereich dargestellt wird, nimmt die Geschwindigkeit der Animationen merklich ab. Auch die vergleichsweise hohen Systemvoraussetzungen sind eine Folge dieses Problems. 5.2.4 Modularer Aufbau von Programmen Ein entscheidender Vorteil von Java wiederum ist die objektorientierte Philosophie mit der Möglichkeit zum modularen Aufbau von Programmen. Dies erlaubt es im vorliegenden Fall, Datenstruktur und Animation praktisch unabhängig voneinander zu programmieren und diese nur durch ein weiteres Programmodul zu verknüpfen. So ist zum Beispiel die Klasse FHeap als Implementation der Fibonacci-Heaps universell in jedem JavaProgramm einsetzbar. Ebenso kann der Abspieler Anima – mit minimalen Veränderungen – auch zur Animation beliebiger anderer Datenstrukturen oder Graphen verwendet werden. 5.2.5 Verwendung vorhandener Komponenten Eine praktische Motivation für die Implementation der vorliegenden Arbeit in Java ist die Animationsbibliothek JEDAS von Rainer Müller, die im Sommersemester 1999 im Softwarepraktikum am Lehrstuhl für Algorithmen und Datenstrukturen ausgebaut wurde. Dieses Paket wurde ebenfalls mit Java realisiert und bildet die Grundlage für die Animationen dieser Arbeit. 41 5.3 Die Animationsbibliothek JEDAS Die Programmbibliothek JEDAS (Java EDucational Animation System) umfaßt alle für die Bildschirmanimation von Objekten notwendigen Werkzeuge. Sie ist insbesondere gedacht für die Programmierung von Animationen zu Unterrichtszwecken. Das Paket baut auf dem Abstract Window Toolkit (AWT) der Sprache Java auf und unterstützt daher noch nicht die seit dem JDK (Java Developer‘s Kit) 1.2 vorhandenen Swing-Klassen, mit deren Hilfe graphische Bausteine unabhängig vom Betriebssystem immer dasselbe Aussehen haben. Aus diesem Grund variiert auch das Erscheinungsbild dieser Animation von Betriebssystem zu Betriebssystem, in manchen Fällen sogar zwischen verschiedenen Rechnern, die unter demselben Betriebssystem laufen. JEDAS lag zum Zeitpunkt der Fertigstellung dieser Arbeit in der Alphaversion v0.908 vor.16 5.3.1 Aufbau und Benutzung der Bibliothek Die Bibliothek basiert im wesentlichen auf dem in 4.2.2 beschriebenen Pfad-TransitionsParadigma. Die animierten Objekte werden realisiert durch Unterklassen der JEDASKlasse Obj. Die gängigen geometrischen Figuren wie Rechteck, Oval, Linie sowie Textobjekte sind bereits implementiert. Orte werden codiert als Paar von reellen Zahlen in der Klasse DPair. Die Zahlen entsprechen den x- und y-Koordinaten im Animationspanel, wobei (0,0) der linken oberen und (1,1) der rechten unteren Ecke entspricht. Pfade sind implementiert durch die Klasse Path. Sie bestehen aus einem Array von DTupleObjekten, in der Regel Instanzen der Klasse DPair. Eine Transition (Trans-Objekt) setzt sich im wesentlichen zusammen aus einem animierbaren Objekt, einem Pfad, einem Transitionstyp – zum Beispiel Bewegung, Farbänderung oder Veränderung der Größe – sowie dem Animationspanel, in dem die Animation stattfinden soll. Diese Transitionen werden einem Scheduler-Objekt übergeben, das die Frame-Updates kontrolliert und alle anstehenden Transitionen ausführt. 16 Unter http://www.informatik.uni-freiburg.de/~rmueller/jedas/index.html ist das Paket samt Dokumentation zum Download verfügbar. Dort finden sich neben einer detaillierten Beschreibung des Animationszyklus auch diverse Beispielanimationen. 42 Ein Vorteil von JEDAS gegenüber anderen Systemen wie XTango ist, daß die Transitionen parallel zueinander ausgeführt werden können, ohne daß diese vorher zusammengesetzt werden müssen. Das heißt insbesondere, daß während des Ablaufs einer Transition eine neue erstellt und hinzugefügt werden kann. Dies vereinfacht interaktive Anwendungen, bei denen eine neue Bewegung starten kann, während eine andere noch abläuft. Zusätzlich soll in naher Zukunft mit JEDAS die Möglichkeit realisiert werden, auch interaktive Animationen aufzuzeichnen und in multimediale Lehrvorträge einzubinden. 5.3.2 Erweiterung von JEDAS um die Klasse CompObj Für die vorliegende Arbeit wurde JEDAS um die Klasse CompObj erweitert, die in der Hierarchie eine Unterklasse der JEDAS-Klasse GraphObj darstellt. CompObj ermöglicht es, eine beliebige Anzahl von animierbaren Objekten zu einem Objekt zusammenzufassen. Dadurch wird die Handhabung zusammengesetzter Objekte stark vereinfacht. Beispielsweise ist jeder Knoten im Animationsbereich aus zwei Objekten zusammengesetzt: einem Kreis (vom Typ OvalObj) und einer Instanz der Klasse TextObj, die den Schlüssel als Zahl innerhalb des Kreises anzeigt. Durch das Zusammenfassen kann jeder Knoten als ein einziges Animationsobjekt der Klasse CompObj behandelt werden. Noch vorteilhafter erweist sich diese Klasse jedoch bei der Implementation der Baummanipulationen. Insbesondere das Verschieben ganzer Bäume und Teilbäume kann so erledigt werden, ohne daß jeder Knoten gesondert bewegt werden muß. Statt dessen faßt man alle Knoten und Kanten des Baums zu einem Objekt von Typ CompObj zusammen und führt die Animationssequenz für dieses Objekt aus. Danach kann das CompObjObjekt wieder gelöscht werden. Eine Instanz der Klasse CompObj speichert die enthaltenen Objekte in einem VectorObjekt ab. Die Methode add erlaubt es, einzelne oder mehrere in einem Array verwaltete Animationsobjekte hinzuzufügen. Die übrigen Methoden, die im wesentlichen die Methoden der direkten Oberklasse GraphObj überschreiben, führen die Anweisungen für alle Elemente des Vektors aus. Die weiteren Klassen, aus denen sich das vorliegende Paket zusammensetzt, werden im folgenden Abschnitt erläutert. 43 5.4 Beschreibung der programmierten Java-Klassen Das Programmpaket kann grob unterteilt werden in die Implementation der Datenstruktur Fibonacci-Heap selbst, die Animationswerkzeuge, die Klassen zur Kommunikation dieser beiden Teile und alle restlichen Klassen, die im wesentlichen Hilfsmittel darstellen. Der vollständige kommentierte Programmcode sämtlicher im Rahmen der vorliegenden Arbeit programmierter Klassen findet sich im Verzeichnis Source auf der beigefügten Diskette. Die .java-Dateien können mit jedem Editor eingesehen werden. Für einige dieser Klassen wurden mit Hilfe des Programms javadoc Dokumentationen im Java-üblichen Format erstellt. Diese befinden sich als HTML-Dateien im Verzeichnis javadoc auf der Diskette und können mit jedem Webbrowser betrachtet werden. 5.4.1 Implementation von F-Heaps durch die Klassen Node und FHeap Die Datenstruktur Fibonacci-Heap wurde durch die Java-Klassen FHeap und Node implementiert. Node beschreibt den Aufbau eines Knotens. Eine Instanz dieser Klasse enthält die folgenden Felder: public class Node { public Object item; Node left, right, parent, child; int key, rank; boolean mark; ... } Diese Implementation folgt der von Fredman und Tarjan vorgeschlagenen, wie sie in Abbildung 3.2 dargestellt wurde, abgesehen von einer Ausnahme. Das Inhaltsfeld, das zusätzlich zum Inhalt auch den Schlüssel speichert, wurde getrennt in zwei Felder, nämlich die ganze Zahl key zur Speicherung des Schlüssels und das Objekt item zum Speichern des Inhalts. Auf diese Weise müssen an den inneren Aufbau von item keine zusätzlichen Anforderungen wie Name und Format des Schlüsselfeldes gestellt werden. Dies würde vor allem beim Herabsetzen des Schlüssels Probleme bereiten. Node benötigt dadurch zwar ein Feld mehr, die Klasse FHeap ist damit aber universeller einsetzbar, weil das Feld item die Speicherung beliebiger Objekte zuläßt. 44 Zusätzlich zu den Feldern enthält die Klasse Node die öffentlichen Methoden isRoot, separate und das geschützte setParameters. Die Methode isRoot gibt einen Boole’schen Wert zurück, der anzeigt, ob es sich bei dem jeweiligen Objekt um einen Knoten der Wurzelliste handelt; separate trennt den Knoten von seinen Brüdern ab; setParameters setzt die Werte key, rank, mark, parent, child, left und right des Knotens und wird intern benutzt, um einen aus einer Datei importierten F-Heap zu rekonstruieren. Ein Problem ergibt sich bei der Implementation der Methode isRoot. Das Überprüfen, ob es sich bei einem Knoten um die Wurzel eines Baums handelt, ist mit dem bloßen Betrachten des Vaterzeigers nämlich nicht getan. Auch für einen Knoten in der Wurzelliste kann der Vaterzeiger noch gesetzt sein, und zwar immer dann, wenn der Knoten durch das Entfernen des Vaters (remove) zur Wurzel wurde und seitdem noch keine Konsolidierung ausgeführt wurde. Würde man beim remove-Schritt die Vaterzeiger aller in die Wurzelliste gelangender Söhne auf null setzen, so bliebe die Laufzeit dieser Operation nicht mehr in O(1), da alle Söhne betrachtet werden müßten. Andererseits muß auch die isRoot-Methode in konstanter Zeit ausführbar sein. Man kann also nicht die Liste der Brüder durchlaufen und prüfen, ob der Minimalknoten darunter ist. Die Lösung des Problems, die hier gewählt wurde, ist das Löschen des Sohnzeigers nach dem Entfernen eines Knotens aus der Wurzelliste. Dadurch genügt es, bei isRoot zusätzlich zu überprüfen, ob bei einem vorhandenen Vaterknoten der Sohnzeiger auf null gesetzt ist. Damit sieht die Methode folgendermaßen aus: public boolean isRoot() { return ((parent == null) || (parent.child == null)); } Die Klasse FHeap beschreibt den Aufbau und die Operationen eines Fibonacci-Heaps. Sie besteht aus den zwei (nicht-öffentlichen) Feldern min, das auf den Knoten mit minimalem Schlüssel zeigt, und size, das die Gesamtzahl der Knoten in diesem F-Heap angibt. Daneben finden sich sämtliche Heap-Operationen als öffentliche Methoden meld, insert, accessmin, deletemin, decreasekey und delete sowie die internen Methoden consolidate, enter, link, cut, remove und maxRank. Ein neuer, leerer Fibonacci-Heap wird durch den Aufruf des Konstruktors FHeap erzeugt, der „objektorientierten“ Variante der in Abschnitt 3.1.1 beschriebenen Operation init: FHeap q = new FHeap(); 45 Die Methode insert(key) erzeugt einen neuen F-Heap, der nur aus einem Knoten mit Schlüssel key besteht, fügt diesen dann mittels meld in die Wurzelliste des bestehenden F-Heaps ein und gibt den Knoten zurück. Mit dem Aufruf q.insert(key).item = object; kann also das Objekt object mit der Priorität key in den F-Heap q aufgenommen werden. Werden die Knoteninhalte mit den Schlüsselwerten identifiziert (ist also das item-Feld überflüssig), so genügt das Kommando q.insert(key); Die Methode meld(otherHeap) verschmilzt den F-Heap q, für den die Methode aufgerufen wird, mit dem F-Heap otherHeap. Ist einer der beiden Heaps leer, so ist das Verschmelzungsprodukt gleich dem jeweils anderen. Die Methode gibt keinen Wert zurück, sondern verändert q, indem otherHeap hinzugefügt wird. Dies hat den Vorteil, daß meld auch für heap-interne Prozeduren wie das Einfügen eines neuen Schlüssels, das Einfügen der Liste der Söhne bei deletemin sowie das Einhängen eines Knotens nach einem cut genutzt werden kann. In der Animation bzw. Simulation kann meld allein allerdings nicht genutzt werden, da nur ein F-Heap pro Animationsfenster dargestellt wird. Die Größe von otherHeap wird zu der von q addiert, die beiden Wurzellisten werden zusammengefügt und zwar derart, daß das Minimum von otherHeap rechts an das Minimum von q angehängt wird. Daraus ergibt sich, daß auch ein neuer Knoten sowie ein durch cut abgetrennter stets rechts vom Minimum in die Wurzelliste eingefügt wird. Das Entfernen des Knotens mit minimalem Schlüssel erfolgt durch die Methode deletemin. Sie verschmilzt die Liste der Söhne des Minimalknotens (falls vorhanden) mit der Wurzelliste und löscht anschließend den Knoten aus dem F-Heap. Danach wird die Wurzelliste konsolidiert und schließlich der minimale Schlüssel zurückgegeben. Die Methode maxRank berechnet für die aktuelle Größe des F-Heaps eine obere Schranke für den maximalen Rang eines beliebigen Knotens. Der berechnete Wert ist in den meisten Fällen tatsächlich der größtmögliche Rang, in manchen Fällen liegt er aber um eins darüber. Dies liegt daran, daß die Funktion maxRank im wesentlichen die Umkehrung der Fibonacci-Folge ist. Wenn die Anzahl n der Knoten im F-Heap zwischen Fi (der i-ten 46 Fibonacci-Zahl) und Fi+1 ist, so ist der maximale Rang eines Knotens gleich i-2.17 Die Funktion maxRank „interpoliert“ diese Umkehrung der Fibonacci-Folge für alle natürlichen Zahlen durch Approximation mit dem ganzzahligen Anteil von 1.4404⋅log2(n). Dies kann zu leichten Abweichungen führen, wie Tabelle 5.1 zeigt. Heapgröße n 1 2 3 4 5 6 7 8 9 10 11 12 13 Maximaler Rang maxRank(n) 0 0 1 1 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 Tabelle 5.1.: Werte für den maximalen Rang eines Knotens und die Funktion maxRank in Abhängigkeit von der Anzahl n der Knoten in einem F-Heap. FibonacciZahlen in der linken Spalte sind fettgedruckt. 5.4.2 Die Klasse der animierten Fibonacci-Heaps AnimFHeap Zur Animation der Fibonacci-Heaps wurde die Unterklasse AnimFHeap programmiert. Sie erweitert die Klasse FHeap um die Animationsfunktionen sowie um die zusätzlichen Zählvariablen count und potential, die es erlauben, die tatsächliche Laufzeit jeder Operation sowie das aktuelle Potential des F-Heaps zu einem beliebigen Zeitpunkt zu bestimmen. Diese Variablen werden im Statistikteil des Hauptprogramms verwendet. Zum Initialisieren eines neuen AnimFHeap-Objekts muß außerdem ein Parameter vom Typ Queue (siehe 5.4.3) übergeben werden. AnimFHeap erbt die Methoden von FHeap und ergänzt bzw. überschreibt diese. Die Ergänzungen bestehen darin, daß für alle relevanten Schritte Animationsinformationen aufbereitet und „verschickt“ werden, die dann vom Abspielmodul ausgeführt werden können. Außerdem wird potential aktualisiert und count um die Anzahl der 17 Dies folgt unmittelbar aus dem in 3.2.1 erwähnten Lemma, welches besagt, daß die Anzahl der Nachkommen eines Knotens mit Rang r mindestens Fr+2 ist. 47 durchgeführten Schritte erhöht. Jede konstante Teiloperation entspricht dabei einem Schritt. Als Beispiel sei hier die Operation delete aufgeführt: public int delete(Node node) { if (node != min) buffer.put(newAnimInfo("MARK_DELETE",node.key)); if (node.isRoot()) count++; return super.delete(node); } Die zweite und dritte Zeile erstellen – sofern es sich beim entfernten Knoten nicht um das Minimum handelt – eine Information, die bewirkt, daß der Knoten zum Löschen markiert wird. Die nächsten beiden Zeilen erhöhen die Laufzeit um eins, falls es sich um einen Wurzelknoten handelt.18 Danach wird die delete-Operation der Oberklasse FHeap ausgeführt. AnimFHeap enthält zahlreiche zusätzliche Methoden. Zum einen sind dies moveSubtree, moveRight, moveUp, moveDown, moveCenter, die komplexere Animationsschritte wie das Verschieben und Zentrieren von Bäumen durchführen sowie die Hilfsfunktionen pos, xpos, treePos, centerTreePos, subtreeWidth, nextTo, include, die im wesentlichen die Positionen berechnen, an die bestimmte Knoten bei der Animation bewegt werden sollen. Des weiteren sind die Methoden rescale, drawSubtree und draw enthalten, mit deren Hilfe der Inhalt des Animationsfensters in veränderter Größe neu dargestellt werden kann und die damit die Zoomfunktion implementieren. Die Größe der Darstellung wird durch die Variablen hstep und vstep bestimmt, die die horizontalen und vertikalen Abstände zwischen den Knoten festlegen, sowie durch eine Variable nodeSize im Abspielmodul, das die Knoten zeichnet. 5.4.3 Codierung und Übermittlung der Animationsinformationen Zur Übermittlung der Animationsinformationen wurden die Klassen Queue und AnimInfo programmiert. Queue implementiert eine FIFO-Schlange ("first in first out") mit den Methoden put und get, die synchronisiert sind, damit verschiedene Threads gleichzeitig auf die Schlange zugreifen können, ohne sich gegenseitig zu stören. Die 18 Der Grund hierfür ist, daß beim Entfernen eines Wurzelknotens kein cut-Schritt durchgeführt wird, der den Zähler erhöht. Da das Entfernen jedoch in jedem Fall einen Schritt erfordert (zuzüglich eventueller cascading cuts), muß der Zähler hier erhöht werden. 48 Elemente der Schlange sind Instanzen der Klasse AnimInfo. Ein AnimInfo-Objekt codiert Informationen über einen Animationsschritt. Es besteht aus einem String, der die Bezeichnung des Animationsschritts trägt, aus einem Array ganzer Zahlen, das die Knoten identifiziert, die manipuliert werden sollen, einer weiteren ganzen Zahl für Zusatzinformationen sowie zwei Objekten der JEDAS-Klasse DPair, die Positionen im Animationsbereich angeben. class AnimInfo { String kindOfAnim; int[] key; int parent; DPair position; DPair pos2; ... } Die Bezeichner für die Art der Animation werden im Abspielmodul Anima abgefragt und können jederzeit ergänzt werden, wenn neue Animationsschritte implementiert werden. Eine AnimFHeap-Instanz gibt für jede Operation die als AnimInfo-Objekte codierten Schritte der Animation an die Warteschlange weiter. 5.4.4 Das Abspielmodul Anima Die Animation wird von einem Abspielmodul, einer Instanz der Klasse Anima, ausgeführt. Diese Klasse stellt die Kontrollschaltfläche und den Animationsbereich des Hauptfensters bereit sowie die Befehle, die zum Darstellen und zur Animation von Knoten und Kanten notwendig sind. Dazu benutzt Anima die Klassen der Animationsbibliothek JEDAS. Nach dem Initialisieren des Animationsfensters versucht ein Anima-Objekt, Informationen von der als Parameter übergebenen Warteschlange zu lesen, und setzt diese in Animationen um. Dabei werden alle Schritte sofort nach dem Lesen ausgeführt; dies bedeutet, daß Animationen, die direkt nacheinander von der Warteschlange gelesen werden, für den Betrachter gleichzeitig ausgeführt werden. Dies ist relevant für die Darstellung der meisten Heap-Operationen, da diese sich aus mehreren gleichzeitig ablaufenden Bewegungen zusammensetzen, beispielsweise das Verschieben eines Knotens und das gleichzeitige Drehen der zugehörigen Kante. Sollen Schritte nacheinander ausgeführt werden, so muß dazwischen ein STOP-Signal in der Schlange abgelegt werden. 49 Wird diese Information gelesen, werden zunächst alle noch laufenden Bewegungen beendet, bevor das nächste Element vom Puffer gelesen wird. Dies ermöglicht zwar ein sehr flexibles Abspielen der Animationen, erfordert jedoch auch große Sorgfalt beim Programmieren. Es muß nämlich beachtet werden, daß zwei Animationsschritte, manipulieren die dürfen, gleichzeitig weil es ablaufen, sonst zu niemals dasselbe Konflikten Animationsobjekt kommen kann. Eine Überwachungsfunktion, die dies verhindert, ist in der vorliegenden Version der Klasse Anima noch nicht integriert. Die implementierten Methoden zur Darstellung von Animationen sind createNode, remove, clear, mark, decrease, update, updateLine, moveLine, moveSubtree, flash und draw. 5.4.5 Das Hauptprogramm HeapSim Das Modul HeapSim verbindet alle Programmteile und bildet somit das „Herzstück“ der Anwendung. Es erzeugt je eine Instanz von AnimFHeap, Anima und StatWin. Zusätzlich stellt HeapSim sämtliche Menüs und Dialoge bereit und implementiert Funktionen wie Maus- und Tastaturabfrage. Für den schnellen Zugriff auf einzelne Knoten im Fibonacci-Heap werden außerdem alle Knoten in einem Array key[] an der Position ihres Schlüssels verwaltet. Dies ist notwendig, um die Operationen decreasekey und delete in den genannten Laufzeiten ausführen zu können. Diese verlangen als Input nämlich jeweils einen Knoten, den zu finden sehr lange dauern kann, da die Suche nach Schlüsseln in F-Heaps (und in Priority Queues im allgemeinen) nicht unterstützt wird. Um beispielsweise aus dem F-Heap Q den Knoten mit Schlüssel k zu löschen, benutzt man also den Befehl Q.delete(key[k]); Selbstverständlich muß auch dieses Array bei Löschungen, Einfügeoperationen oder dem Herabsetzen von Schlüsseln ständig aktualisiert werden. Das geschieht in den Methoden insert, delmin, decrease und delete, nachdem von dort die eigentlichen Heapoperationen aufgerufen wurden. Außerdem sorgen diese Methoden dafür, daß die im Heap auftauchenden Schlüsselwerte in den Auswahlmenüs deaktiviert sind. 50 Die Prozeduren statBefore und statAfter bearbeiten die Statistikinformationen über die Laufzeiten; statBefore wird vor jeder Operation aufgerufen und überprüft Zählvariable und Potential des F-Heaps, statAfter wertet nach der Operation die Änderungen aus und übermittelt diese an das Statistikfenster. Weiterhin werden sämtliche Zufallsoperationen und Zufallszahlen durch die Methoden rndOp, randomIns, randomDec und randomDel bestimmt. Beim Wählen einer zufälligen Operation mit rndOp sind die einzelnen Möglichkeiten nicht gleichverteilt. Vielmehr werden die Wahrscheinlichkeiten für bestimmte Operationen beeinflußt von der Größe des F-Heaps. Bei einem Heap mit wenigen Knoten ist beispielsweise ein insert sehr viel wahrscheinlicher als ein deletemin. Je größer der F-Heap wird, desto stärker kehren sich diese Wahrscheinlichkeiten um, damit die durchschnittliche Knotenanzahl eingegrenzt bleibt. Die Methoden saveHeap und openHeap sorgen durch Instantiieren von fhp-Objekten dafür, daß bei Bedarf der aktuelle Zustand in einer .fhp-Datei abgespeichert oder aus einer solchen eingelesen wird. 5.4.6 Das Statistikmodul StatWin Die Klasse StatWin stellt ein JEDAS-Panel zur Verfügung, in dem die Laufzeiten der Operationen in einem Diagramm dargestellt werden. StatWin enthält die beiden Methoden init und update. Mit init werden alle vorhandenen Graphen gelöscht und die Beschriftungen der Achsen auf die aktuellen Werte gesetzt. Die x-Achse bekommt dann als Anfangswert die Nummer der aktuellen Operation, die y-Achse die bisher angefallenen tatsächlichen Kosten. Auf diese Weise „wandert“ das Koordinatenkreuz bei jedem Aufruf von init weiter, so daß die Graphen nicht aus dem angezeigten Bereich hinauswachsen können. Die Methode update wird mit zwei ganzzahligen Parametern aufgerufen, die die tatsächliche und amortisierte Laufzeit der letzten Operation angeben. Daraus errechnet das Modul die neuen Endpositionen für die beiden Graphen. Falls diese Positionen außerhalb des angezeigten Koordinatensystems liegen, wird mit init das Achsenkreuz neu gezeichnet. Schließlich wird das der letzten Operation entsprechende Teilstück beider Graphen dargestellt. 51 5.4.7 Das Dateiformat fhp Um die Speicherung eines F-Heaps in einem bestimmten Zustand sowie ein späteres Weiterbearbeiten zu ermöglichen, wurde das Dateiformat .fhp konzipiert. Es faßt auf einfache und platzsparende Weise alle Informationen über den aktuellen Zustand eines animierten F-Heaps zusammen. Der Dateikopf besteht aus zwei Bytes, die die Anzahl der Knoten im Heap und das augenblickliche Potential angeben. Danach folgen für jeden Knoten 6 Bytes, die sämtliche Knoteninformationen folgendermaßen codieren: Das erste Byte enthält in den Bits 0 bis 6 den Schlüsselwert des Knotens (diese sieben Bits reichen aus, da der Schlüssel immer kleiner ist als 100), Bit 7 wird genau dann gesetzt, wenn der Knoten markiert ist. Byte zwei enthält in den Bits 0 bis 3 den Rang des Knotens. Das dritte Byte enthält den Schlüsselwert des Vaterknotens und gibt außerdem zwei wichtige Zustände an. Für Knoten in der Wurzelliste ist hat dieses Byte den Wert 0, es sei denn, es handelt sich bei dem Knoten um das Minimum oder den Knoten, der sich in der Darstellung am weitesten links in der Wurzelliste befindet. Im ersten Fall hat Byte 3 den Wert 255, im letzteren 254, und falls für einen Knoten beides zutrifft, den Wert 253. Die letzten drei Bytes enthalten die Schlüssel von Sohn, linkem und rechtem Bruder. Die Anzahl der Knoten im F-Heap sowie das Potential müssen prinzipiell nicht im Dateikopf gespeichert werden, da sich beide Werte beim Einlesen der Knoten bestimmen lassen. Dies wird auch zusätzlich durchgeführt und die Ergebnisse mit den gespeicherten Werten verglichen. Auf diese Weise erhält man ein einfaches Verfahren um zu prüfen, ob es sich bei einer eingelesenen .fhp-Datei auch tatsächlich um einen korrekt codierten FHeap handelt. Das .fhp-Format wurde durch die Java-Klasse fhp realisiert. Diese Klasse regelt das Codieren und Decodieren und enthält die zum Laden und Speichern der Dateien notwendigen Methoden. Um einen F-Heap zu speichern, muß lediglich ein fhp-Objekt mit dem Dateinamen und dem AnimFHeap-Objekt als Parametern erzeugt und die write-Methode mit dem Knotenarray als Input aufgerufen werden. Zum Laden einer .fhp-Datei wird ebenfalls ein fhp-Objekt mit dem Dateinamen und einem F-Heap instantiiert. Die Methode read gibt dann als Wert ein Knotenarray zurück und belegt die Zeiger des F-Heaps (min, size und potential) mit den entsprechenden Werten. 52 5.4.8 Menüs und Dialoge Die zur Interaktion des Benutzers mit dem Programm notwendigen Dialogfenster und Menüs wurden mit Hilfe der Klassen InputDialog, HeapMenuBar und HeapOpMenu implementiert. InputDialog ist eine Unterklasse von Dialog und stellt in seinem Fenster – abhängig von den übergebenen Parametern – Eingabefelder für die verschiedenen Heapoperationen zur Verfügung. HeapMenuBar erbt von MenuBar und stellt die Menüleiste im Hauptfenster mit den drei Menüs File, F-Heap und Options dar. HeapOpMenu erweitert die Klasse PopupMenu und liefert die Kontextmenüs, die durch Rechtsklick aufgerufen werden. Kapitel 6 Handhabung des Programmpakets Die gesamte Animation und Simulation ist auf der beiliegenden Diskette verfügbar. Dieses Kapitel erklärt die Installation und Benutzung sowie die eventuelle Weiterbenutzung einzelnen Klassen in anderen Java-Programmen. 6.1 Installation 6.1.1 Systemvoraussetzungen Zur Darstellung der Animationen wird ein Rechner mit VGA-Grafikkarte benötigt. Damit eine vernünftige Geschwindigkeit gewährleistet ist, sollte im PC-Bereich mindestens ein Pentium MMX oder ein vergleichbarer Prozessor und 32 MB Hauptspeicher vorhanden sein. Das Java Runtime Environment ab Version 1.1.4 des JDK muß installiert sein. Das Programm läuft prinzipiell unter allen Betriebssystemen, die Java unterstützen. Getestet wurde es bisher allerdings nur unter UNIX (Solaris, IRIX), Linux und Windows 9x. 53 6.1.2 Installation unter UNIX und Linux Zuerst muß das Verzeichnis F-Heap von der Diskette in ein Verzeichnis des Benutzers kopiert werden. Anschließend wird der Java-Klassenpfad auf das JEDAS-Archiv gesetzt. Dies geschieht mit der Anweisung setenv CLASSPATH /.../F-Heap/jedas0908.jar:. Die Zeichenkette /.../ steht für den Pfad des Verzeichnisses, in das F-Heap kopiert wurde. Unter UNIX (nicht Linux) muß – falls noch nicht geschehen – das Java Runtime Environment mit setup java aktiviert werden, bevor das Programm gestartet werden kann. 6.1.3 Installation unter Windows 95/98 Zunächst wird das Verzeichnis F-Heap von der Diskette auf die Festplatte kopiert. Danach muß in der Datei C:\autoexec.bat die CLASSPATH-Variable auf das JEDAS-Archiv gesetzt werden. Dies geschieht durch Einfügen der Zeile SET CLASSPATH=C:\...\F-Heap\jedas0908.jar;. Die Zeichenfolge C:\...\ steht für den Pfad, in den das Verzeichnis F-Heap kopiert wurde. Der Rechner muß nun neu gestartet werden, damit die Änderung wirksam wird. 6.2 Bedienung 6.2.1 Starten der Anwendung Die Animationsanwendung wird unter Windows (in einer DOS-Shell) oder unter UNIX im Verzeichnis F-Heap mit der Kommandozeile 54 java HeapSim gestartet. Nach dem Aufruf meldet sich die Anwendung mit dem Hauptfenster. Hier muß zuerst die Play-Taste links unten im Kontrollbereich gewählt werden, damit die Animation aktiviert wird. Sollte das Programm unter IRIX nicht stabil laufen, so kann mit java HeapSimLite eine Version mit minimal eingeschränkter Funktionalität gestartet werden. 6.2.2 Das Hauptfenster Das Hauptfenster setzt sich zusammen aus der Titelleiste, der Menüleiste, dem Animationsbereich und der Kontrollfläche. Abbildung 6.1: Das Hauptfenster, bestehend aus Titelleiste, Menüleiste, Animationsbereich und Kontrollfläche (dargestellt unter Windows 98). Die Titelleiste gibt den Namen der aktuell geöffneten .fhp-Datei an. Der beim Programmstart neu initialisierte F-Heap trägt den Namen [untitled]. Ein Name kann beim Speichern der Datei vergeben werden. 55 Die Menüleiste umfaßt die Auswahlmenüs File, F-Heap und Options, in denen alle Kommandos zur Heapsimulation und zu den Zusatzfunktionen enthalten sind. Im Animationsbereich, der den größten Teil des Hauptfensters in Anspruch nimmt, werden die Fibonacci-Heaps und die Animationen dargestellt. Alle Kommandos, die zur Manipulation der F-Heaps notwendig sind, können von hier auch über Kontextmenüs erreicht werden. Die Kontrollfläche erlaubt die Steuerung der Animation über Schaltflächen, die an die Bedienelemente eines Videorecorders erinnern. 6.2.3 Steuerung über die Menüleiste Das Menü File bietet die Optionen New, Open, Save und Exit zur Auswahl. Mit New wird ein neuer, leerer F-Heap im Animationsbereich erzeugt. Der möglicherweise vorher dort dargestellte F-Heap wird dabei gelöscht und sollte bei Bedarf vorher abgespeichert werden. Die Option Open erlaubt das Öffnen eines in einer .fhp-Datei abgespeicherten F-Heaps. Es erscheint ein der jeweiligen Oberfläche entsprechendes Dialogfeld19, in dem mit Hilfe der Maus oder per Tastatureingabe die gewünschte Datei gewählt werden kann. Existiert die angegebene Datei, so wird sie geöffnet und der zugehörige F-Heap im Animationsbereich dargestellt. Dieser kann dann wie gewohnt weiter bearbeitet werden. Mit Save kann der aktuell dargestellte F-Heap in eine Datei gespeichert werden. Auch hier öffnet sich dem Benutzer ein plattformspezifisches Dialogfenster, in dem der Dateiname und der Verzeichnispfad bestimmt werden können. Existiert die angegebene Datei bereits, so erscheint vor dem Speichern eine Warnmeldung, die dem Benutzer das Überschreiben der Datei oder den Abbruch des Speichervorgangs zur Auswahl stellt. Beim Speichern wird dem Dateinamen automatisch das Suffix .fhp angefügt, falls dieses noch nicht vom Benutzer selbst angegeben wurde. Die Option Exit beendet die Anwendung. Der im Animationsbereich dargestellte F-Heap geht dabei verloren und sollte daher bei Bedarf vorher gespeichert werden. Das Menü F-Heap dient zum Bearbeiten des F-Heaps, der im Animationsbereich dargestellt wird. Es bietet als Optionen die Standard-Heapoperationen Insert node, 19 Dieser Dialog wird realisiert durch die Java-Klasse FileDialog, die ein plattformspezifisches Fenster aufruft. Daher kann das Aussehen des Dialogs je nach Betriebssystem variieren. 56 Delete minimum, Decrease key und Delete node. Zusätzlich läßt sich über den Menüpunkt Random operation eine zufällig gewählte Heapoperation ausführen. Die Menüpunkte Delete minimum, Decrease key und Delete node können nicht immer ausgewählt werden, was im gegebenen Falle durch eine andere Farbdarstellung dieser Optionen im Menü angezeigt wird. So kann der Minimalknoten nur aus einem nichtleeren F-Heap entfernt werden; die Befehle Decrease key sowie Delete node können nur gewählt werden, wenn vorher ein Knoten im Animationsbereich durch Anklicken mit der linken Maustaste markiert wurde. Im Menü Options kann durch Auswahl des Menüpunkts Show statistics das Statistikfenster für den aktuellen F-Heap ein- bzw. ausgeblendet werden, in dem die tatsächliche und amortisierte Laufzeit der zuletzt ausgeführten Heapoperationen in einem Diagramm ausgegeben werden. Das Untermenü Zoom... erlaubt es dem Benutzer, die Größe der Darstellung im Animationsbereich zu verändern. Das Auswählen von Zoom in bewirkt eine Vergrößerung des Bildes um den Faktor 1,25, mit Zoom out erhält man eine Verkleinerung um Faktor 0,8. Diese Faktoren beziehen sich auf Länge und Breite der Knoten sowie auf die Abstände zwischen den Knoten. Der Auswahlpunkt Auto zoom aktiviert bzw. deaktiviert die automatische Verkleinerung des Bildes bei zu langer Wurzelliste. Ist diese Option deaktiviert, so werden beim Einfügen neuer Knoten die Bäume rechts in der Wurzelliste möglicherweise aus dem Animationsbereich hinausgeschoben. Dies beeinträchtigt zwar nicht die Funktionalität der Animation, jedoch deren Überschaubarkeit. Daher wird empfohlen, diese Funktion aktiviert zu lassen. 6.2.4 Der Animationsbereich Der Animationsbereich stellt den F-Heap und alle an ihm durchgeführten Operationen graphisch dar. Beim Programmstart ist die Fläche leer (bis auf einen Pfeil, der den Zeiger auf den Minimalknoten darstellt). Zur Bearbeitung des F-Heaps können – als Alternative zur Menüleiste – direkt die Animationsfläche und die darin dargestellten Objekte verwendet werden. Zum Bearbeiten eines Knotens genügt es, diesen mit der rechten Maustaste20 anzuklicken und im daraufhin erscheinenden Kontextmenü die gewünschte Operation auszuwählen (siehe 6.2.6). Nach 20 Die Begriffe „rechte“ und „linke“ Maustaste beziehen sich hier auf eine für Rechtshänder eingestellte Standardmaus. Je nach Gerät und individuellen Benutzereinstellungen kann damit auch eine andere Taste gemeint sein. 57 einem Rechtsklick auf eine leere Stelle in der Animationsfläche erscheinen im Kontextmenü nur diejenigen Operationen, die sich nicht auf einen bestimmten Knoten beziehen. Eine weitere Alternative zur Heap-Bearbeitung ist das Doppelklicken auf einen Knotens oder die Animationsfläche mit der linken Maustaste. Dadurch öffnet sich ein Benutzerdialog, der die jeweils relevanten Operationen zur Auswahl anbietet. Wird ein Knoten mit der linken Maustaste einmal angeklickt, so wird er farbig markiert. Dadurch werden im Menü F-Heap die Optionen Decrease key und Delete node wählbar. Diese beziehen sich dann auf den markierten Knoten. 6.2.5 Die Kontrollfläche Die Kontrollfläche dient zum Steuern des Animationsablaufs. Nach dem Programmstart sollte zuerst die Play-Taste aktiviert werden. Erst danach kann die Animation laufen. Die Kontrollfläche ist eine Instanz der JEDAS-Klasse CtrlPanel, die in das vorliegende Programm übernommen wurde. Die Schaltflächen sind den Tasten eines Videorecorders nachempfunden. Diese Klasse ist vor allem gedacht für das Abspielen von Animationen, die nicht interaktiv vom Benutzer manipuliert werden können. In diesem Fall sollte nämlich die Möglichkeit zum Stoppen bzw. Pausieren und Fortsetzen der Animation bestehen. Im vorliegenden Fall, wo die Animation ohnehin nach jeder Operation stehenbleibt und auf eine neue Eingabe wartet, ist dies weniger wichtig. Dennoch wurde die Schaltfläche beibehalten, da sie für die Aufzeichnung der Animationen relevant sein wird, sobald diese Option in JEDAS möglich ist. Außerdem soll die Möglichkeit bestehen, daß beispielsweise bei einer länger dauernden deleteminDarstellung die Animation vom Betrachter unterbrochen werden kann. Dies geschieht entweder mit der Pause-Taste (die Animationszeit läuft weiter) oder mit der Stop-Taste (die Uhr wird angehalten). Die Animation kann durch erneutes Wählen der Pause-Taste bzw. der Play-Taste fortgesetzt werden. Das Drücken der Auswurftaste beendet die gesamte Anwendung. F-Heaps, die später weiterverwendet werden sollen, müssen vorher gespeichert werden, ansonsten gehen sie verloren. 58 6.2.6 Ausführen der Heapoperationen Zum Einfügen eines neuen Knotens gibt es drei Möglichkeiten. Der einfachste Weg ist der rechte Mausklick auf die Animationsfläche. Im dann erscheinenden Kontextmenü wird das Untermenü Insert new node angewählt. Danach kann der Schlüsselwert aus den weiteren Auswahlen bestimmt werden. Alternativ kann im Menü F-Heap mit dem Befehl Insert oder mit einem Doppelklick in die Animationsfläche ein Dialog geöffnet werden, der die Eingabe des Schlüssels über die Tastatur erlaubt. Hier werden nur Werte von 1 bis 99 angenommen, die noch nicht im Heap vorkommen. Random bzw. Random key bedeutet in jedem der drei Fälle die zufällige Wahl eines Schlüsselwerts. Um den Minimalknoten zu entfernen, ist im Kontextmenü der Animationsfläche Delete minimum node zu wählen. Dieser Punkt ist auch im Menü F-Heap zu finden sowie in der Dialogbox, die sich durch Doppelklick auf eine freie Stelle im Animationsbereich öffnet. Eine weitere Möglichkeit ist der Menüpunkt Delete node im Kontextmenü des Minimalknotens. Zum Herabsetzen eines Schlüssels wird der betreffende Knoten mit der rechten Maustaste angewählt. Im Kontextmenü bietet das Untermenü Decrease key to... die Auswahl eines Schlüssels. Eine Dialogbox zum Eingeben des Schlüssels mittels der Tastatur erhält man durch Doppelklicken des Knotens oder durch einfaches Anklicken mit der linken Maustaste (der Knoten färbt sich blau) und anschließendes Wählen von Decrease key im Menü F-Heap. Es muß dann ein Schlüsselwert eingegeben werden, der kleiner ist als der ursprüngliche und außerdem noch nicht im Heap vorkommt. Zum Entfernen eines beliebigen Knotens verfährt man ganz analog, nur daß statt Decrease key der Menüpunkt Delete bzw. Delete node gewählt wird. Um eine Operation zufällig vom Programm bestimmen zu lassen, wählt man aus dem Menü F-Heap oder einem Kontextmenü den Punkt Random operation. 6.2.7 Das Statistikfenster Das Statistikfenster gibt Auskunft über die Laufzeiten der zuletzt ausgeführten Operationen. Es kann im Menü Options des Hauptfensters mit dem Auswahlpunkt Show statistics ein- und ausgeblendet werden. 59 Abbildung 6.2: Das Statistikfenster mit Darstellung der tatsächlichen und der amortisierten Laufzeit. In der Titelleiste des Statistikfensters wird die zuletzt ausgeführte Operation und die aktuelle Anzahl der Elemente im F-Heap angezeigt. Die Laufzeiten werden in einem zweidimensionalen Koordinatensystem angezeigt. Dabei ist in x-Richtung die Anzahl der Operationen aufgetragen, die y-Achse stellt die Laufzeit in Schritten (wie sie in 4.3.4 definiert wurden) dar. Die tatsächlichen Kosten werden blau, die amortisierten Kosten rot dargestellt. Da die gesamte amortisierte Laufzeit immer eine obere Schranke für die tatsächlichen Kosten ist, verläuft die rote Kurve stets oberhalb der blauen. Der Abstand der beiden Graphen gibt das Potential zum jeweiligen Zeitpunkt an. Immer wenn sich die Kurven voneinander entfernen, bedeutet dies, daß Guthaben angesammelt wird; nähern sie sich einander an, so wird Guthaben verbraucht. 6.2.8 Beenden des Programms Zum Beenden des Programms kann entweder die Option Exit im Menü File gewählt oder die einer Auswurftaste nachempfundene Schaltfläche im Kontrollbereich gedrückt werden. Eine weitere Alternative ist das jeweils plattformübliche Schließen des Hauptfensters. Sollte sich das Programm einmal wegen eines Fehlers oder Absturzes nicht auf diese Weise beenden lassen, so gibt es die Möglichkeit, in der Shell, aus der die Anwendung 60 gestartet wurde, die Tastenkombination Ctrl(Strg)-c zu drücken und damit die Ausführung des Java-Programms zu beenden. 6.3 Beispieldateien im .fhp-Format Zur Erleichterung für den Benutzer sind dem Paket unterschiedliche Beispiele für Fibonacci-Heaps beigefügt. Dabei handelt es sich sowohl um „normale“ als auch um extrem degenerierte F-Heaps. Diese können eingeladen und dann bearbeitet werden. Außerdem können selbst eigene Beispiel-Heaps erstellt und gespeichert werden. 6.3.1 Verwendung der beigefügten Beispiele Die Beispiele für Fibonacci-Heaps sind als .fhp-Dateien im Verzeichnis F-Heap zu finden. Mit der Option Open im Menü File können diese eingelesen werden. Nur gültige .fhp-Dateien werden vom Programm akzeptiert. Danach können diese F-Heaps wie in Abschnitt 6.2 beschrieben weiter bearbeitet werden. Ein späterer Zustand kann mit File - Save abgespeichert werden. Allerdings sollte im dabei erscheinenden Dialogfenster ein neuer Dateiname gewählt werden, ansonsten wird das ursprüngliche Beispiel überschrieben. 6.3.2 Erstellen eigener Beispieldateien Jeder angezeigte Fibonacci-Heap kann im aktuellen Zustand als .fhp-Datei abgespeichert werden. Um aussagekräftige Beispiel-Heaps zu erstellen, sollte der Benutzer mit der Struktur von F-Heaps vertraut sein. Zur Erklärung einer wichtigen Heapoperation eignen sich als instruktive Beispiele F-Heaps, die sich vor der Ausführung dieser Operation in einem Zustand befinden, der sich durch das Ausführen entscheidend verändert. In diesem Zustand muß der F-Heap gespeichert werden. Zur Veranschaulichung der cascading cuts empfiehlt es sich beispielsweise, mehrere markierte Knoten in einer Kette zu haben, die dann beim Herabsetzen eines Schlüssels alle abgetrennt werden. 61 6.4 Verwendung einzelner Klassen in eigenen Java-Programmen Es ist möglich, Java-Klassen aus dem Paket in anderen Anwendungen weiterzuverwenden. Aus diesem Grund wurden die Klassen einzeln gespeichert und nicht zu einem Archiv zusammengefaßt. Am interessantesten dürfte die Implementation der F-Heaps selbst sein. Dazu müssen lediglich die Klassendateien FHeap.class und Node.class in das Verzeichnis der neuen Anwendung kopiert werden. Die Verwendung von FHeap wurde bereits geschildert und kann auch im entsprechenden javadoc-File nachgesehen werden. Auch die FIFO-Schlange, die durch Queue.class und Cell.class implementiert wurde, ist oftmals nützlich. Insbesondere erlaubt sie durch die Synchronisation der Methoden den Zugriff aus verschiedenen Threads. Für Entwickler von JEDAS-Animationen ist das Verwenden von zusammengesetzten Objekten mit Hilfe von CompObj.class sicher vorteilhaft. In dieser Klasse wurden jedoch nur die für die vorliegende Animation notwendigen Methoden implementiert; für einige Fälle muß gegebenenfalls der Programmcode erweitert werden. Das Source-File CompObj.java befindet sich – wie der gesamte Quellcode – ebenfalls auf der Diskette. Alle übrigen Klassen können selbstverständlich ebenfalls verwendet werden, sind aber eher speziell für die Anwendung konzipiert und daher weniger ausführlich dokumentiert. 62 Kapitel 7 Zusammenfassung und Ausblick Ziel dieser Arbeit war es, als Beispiel für die Visualisierung komplexer Datenstrukturen eine Animation und Simulation von Fibonacci-Heaps und der darauf operierenden Algorithmen zu erstellen. Das vorliegende Programm setzt diese Vorgaben um und erlaubt eine benutzerfreundliche Interaktion mit dem Algorithmus über einfach zu bedienende Elemente und Hilfsfunktionen. Es ist anzumerken, daß sich ein Programm wie das vorliegende ständig weiter verbessern ließe. So könnte die Verbindung zwischen Datenstruktur und Animation direkt – das heißt ohne Pufferung auf einer Warteschlange – erfolgen, was bei geeigneter Implementation die Animationen vermutlich beschleunigen würde. Die parallele Darstellung mehrerer F-Heaps in verschiedenen Fenstern wäre ein ebenso nützlicher Zusatz wie die Möglichkeit, die Darstellung im Statistikfenster zu beeinflussen. Einige dieser Punkte werden sicherlich in späteren Versionen des Programms umgesetzt werden. Entscheidender sind jedoch die Einsatzmöglichkeiten des Pakets. Der nächste Schritt ist eine Einbindung der Animation in einen multimedialen Lehrvortrag. Es ist geplant dies zu verwirklichen, sobald die dazu notwendige Technologie vorhanden ist, nämlich die Möglichkeit, die Animationssequenzen von JEDAS-Animationen aufzuzeichnen und später synchron mit dem ebenfalls aufgezeichneten Vortrag abzuspielen. Im Rahmen des Hochschulprojekts VIROR soll die Animation außerdem auf einem zentralen Server zum Download für die angeschlossenen Hochschulen bereitgestellt werden. So können interessierte Lehrende und Studierende das Programm zu Unterrichtszwecken oder zum Selbststudium nutzen. Hierbei wäre es zudem höchst interessant, Studien zum Lernerfolg vorzunehmen. Es wird sehr oft beklagt, daß es zu wenige empirische Untersuchungen über die Effektivität von Algorithmenanimation in der Lehre gibt. In jüngerer Zeit hatten die wenigen durchgeführten Experimente teilweise eher Anlaß zu Zweifeln an dem von Lernenden und Lehrenden spontan geäußerten positiven Einfluß auf das Verständnis gegeben [Kehoe 1999]. Es wäre zu überprüfen, ob diese Zweifel berechtigt sind. Dies könnte im Rahmen des Einsatzes in einer Lehrveranstaltung mit der Möglichkeit zum selbständigen Üben und 63 einem abschließenden Test geschehen, wie es beispielsweise in [Stasko 1998c] beschrieben wird. Es ist klar, daß im Bereich der Lehre Softwarevisualisierung und Algorithmenanimation bei weitem noch nicht die Möglichkeiten ausschöpfen, die vorhanden wären, was auch Jim Foley im Vorwort zu [Stasko 1998a] bedauert: My only disappointment with the field is that Software Visualization has not yet had a major impact on the way we teach algorithms [...]. While I continue to believe in the promise and potential of SV, it is at the same time the case that SV has not yet had the impact that many have predicted and hoped for. Wenn das hier erstellte Programm einen kleinen Beitrag dazu leisten kann, die Entwicklung in diesem Bereich weiter voranzubringen, ist ein wesentliches Ziel der vorliegenden Arbeit erreicht. 64 Literaturverzeichnis: [Baecker 1998] Ronald Baecker. „Sorting Out Sorting: A Case Study of Software Visualization for Teaching Computer Science.“ In: [Stasko 1998a], S. 369-381. [Boyer 1997] John Boyer. „The Fibonacci Heap.“ In: Dr. Dobb’s Journal, Januar 1997. (Auch online verfügbar unter http://www.ddj.com/articles/1997/9701/9701o/ 9701o.htm) [Brown 1998a] Marc H. Brown. „A Taxonomy of Algorithm Animation Displays.“ In: [Stasko 1998a], S. 35-42. [Brown 1998b] Marc H. Brown und John Hershberger. „Fundamental Techniques for Algorithm Animation Displays.“ In: [Stasko 1998a], S. 82-101. [Brown 1998c] Marc H. Brown und Robert Sedgewick. „Interesting Events.“ In: [Stasko 1998a], S. 155-159. [Campione 1997] Mary Campione und Kathy Walrath. Das JavaTM Tutorial: Objektorientierte Programmierung für das Internet. Bonn: Addison-WesleyLongman, 1997. [Cormen 1990] Thomas H. Cormen, Charles E.. Leiserson und Ronald L. Rivest. Introduction to Algorithms. Cambridge, MA: MIT Press, 1990. [Fredman 1987] Michael L. Fredman und Robert Endre Tarjan. „Fibonacci Heaps and Their Uses in Improved Network Optimization Algorithms.“ In: Journal of the Association for Computing Machinery, 34: 596-615, 1987. [Gloor 1998a] Peter A. Gloor. „User Interface Issues for Algorithm Animation.“ In: [Stasko 1998a], S. 145-152. [Gloor 1998b] Peter A. Gloor. „Animated Algorithms.“ In: [Stasko 1998a], S. 409-416. [Jeffery 1998] Clinton L. Jeffery. „A Menagerie of Program Visualization Techniques.“ In: [Stasko 1998a], S. 73-79. [Kehoe 1999] Colleen Kehoe, John Stasko und Ashley Taylor. „Rethinking the Evaluation of Algorithm Animations as Learning Aids: An Observational Study.“ Graphics, Visualization, and Usability Center, Georgia Institute of Technology, Atlanta, GA. Technical Report GIT-GVU-99-10, March 1999. [Knuth 1973] Donald E. Knuth. The Art of Computer Programming. Volume 3: Sorting and Searching. Reading, MA: Addison-Wesley, 1973. [Lewalter 1997] Doris Lewalter. Lernen mit Bildern und Animationen. Münster: Waxmann, 1997. [Linden 1996] Peter van der Linden. Just JAVA. Upper Saddle River: Prentice Hall, 1996. 65 [Ottmann 1996] Thomas Ottmann und Peter Widmayer. Algorithmen und Datenstrukturen. Heidelberg, Berlin, Oxford: Spektrum Akademischer Verlag, ³1996. [Stasko 1998a] John Stasko, John Domingue, Marc H. Brown und Blain A. Price (Hg.). Software Visualization. Programming as a Multimedia Experience. Cambridge, MA: MIT Press, 1998. [Stasko 1998b] John Stasko. „Smooth Continuous Animation for Portraying Algorithms and Processes.“ In: [Stasko 1998a], S. 103-118. [Stasko 1998c] John Stasko und Andrea Lawrence. „Empirically Assessing Algorithm Animations as Learning Aids.“ In: [Stasko 1998a], S. 419-438. [Stasko 1992] John Stasko und Carlton Turner. „Tidy Animations of Tree Algorithms.“ Graphics, Visualization, and Usability Center, Georgia Institute of Technology, Atlanta, GA. Technical Report GIT-GVU-92-11, 1992.