Animation komplexer Datenstrukturen und der dazugehörigen

Werbung
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.
Herunterladen