• Keine Ergebnisse gefunden

Implementierung von effizienten Sortieralgorithmen f¨ur SLP-komprimierte Strings in der funktionalen Programmiersprache Haskell

N/A
N/A
Protected

Academic year: 2022

Aktie "Implementierung von effizienten Sortieralgorithmen f¨ur SLP-komprimierte Strings in der funktionalen Programmiersprache Haskell"

Copied!
66
0
0

Wird geladen.... (Jetzt Volltext ansehen)

Volltext

(1)Implementierung von effizienten Sortieralgorithmen für SLP-komprimierte Strings in der funktionalen Programmiersprache Haskell Bachelorarbeit. 24. Mai 2013 Sadik Yel. Eingereicht bei Prof. Dr. Manfred Schmidt-Schauß Künstliche Intelligenz und Softwaretechnologie Fachbereich Informatik und Mathematik Institut für Informatik.

(2)

(3) Danksagung Zu erst möchte ich mich bei allen bedanken, die mich während der Erstellung dieser Bachelorarbeit unterstützt und motiviert haben. Einen ganz herzlichen Dank widme ich Herrn Dr. David Sabel, der mir sehr gute Korrekturtipps und Hilfestellungen gab, die nicht nur zu einer wesentlichen Qualitätsbesserung dieser Thesis beigetragen haben, nein, auch der Spaß an der Arbeit und die Motivation, die er mir übermittelt hat, waren unschlagbar. Natürlich gilt der herzliche Dank auch dem Herrn Prof. Dr. Manfred Schmidt-Schauß ohne ihn gebe es wahrscheinlich diese Thesis nicht. Danke, denn ich bin sehr erfreut darüber, dass ich ein Thema bearbeitet habe, welches mir Spaß gemacht hat und meinen Interessen entsprach. Einen besonderen Dank sei auch dem Herrn Prof. Dr.-Ing. Lars Hedrich gewidmet, der sich die Zeit nimmt, um Zweitkorrektor zu sein. Weiterhin möchte ich mich bei meiner Familie dafür bedanken, dass sie immer an mich geglaubt haben und dass es sie gibt. Auch dem Herrn Ahmet Sinci gilt mein Dank, der mich motiviert und meinen Zeitplan mit organisiert hat. Zu guter Letzt bedanke ich mich bei Melanie Jacksties, die sich die Zeit genommen hat, um Korrektur zu lesen und mir mit Verbesserungsvorschlägen den letzten Feinschliff gegeben hat.. i.

(4) Erklärung Erklärung gemäß Bachelor-Ordnung Informatik 2007 §24 Abs. 11 Hiermit bestätige ich, dass ich die vorliegende Arbeit selbstständig verfasst habe und keine anderen Quellen oder Hilfsmittel als die in dieser Arbeit angegebenen verwendet habe. Frankfurt am Main, den 24. Mai 2013. Sadik Yel. ii.

(5) Inhaltsverzeichnis Abbildungsverzeichnis. 1. I. Einleitung. 2. 1. Motivation und Überblick 1.1. Motivation . . . . . . . . . . . . . 1.2. Ziele und Aufgabenstellung . . . 1.2.1. Spezifikation der Aufgabe 1.3. Übersicht über die Kapitel . . . .. 3 3 3 5 5. . . . .. . . . .. . . . .. . . . .. . . . .. . . . .. . . . .. . . . .. . . . .. . . . .. . . . .. . . . .. . . . .. . . . .. . . . .. . . . .. . . . .. . . . .. . . . .. . . . .. . . . .. . . . .. II. GRUNDLAGEN. 7. 2. Sortieren 2.1. Quicksort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2. Mergesort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. 8 8 10. 3. Komprimieren mit kontextfreien Grammatiken 3.1. Chomsky-Normalform . . . . . . . . . . . . 3.2. Straight-Line-Program (SLP) . . . . . . . . 3.2.1. Eigenschaften von SLP . . . . . . . . 3.3. Bekannte Algorithmen auf SLPs . . . . . . . 3.3.1. Algorithmus von Plandowski . . . . . 3.3.2. Das Erzeugen von Wortlängen . . . .. . . . . . .. 12 15 15 16 17 18 19. . . . . . . . . . .. 22 22 22 24 24 24 25 27 27 28 29. . . . . . .. . . . . . .. . . . . . .. . . . . . .. . . . . . .. 4. Haskell 4.1. Eigenschaften von funktionalen Programmiersprachen 4.2. Auswertungsreihenfolgen . . . . . . . . . . . . . . . . 4.3. Programmieren mit Haskell . . . . . . . . . . . . . . 4.3.1. Funktionen . . . . . . . . . . . . . . . . . . . 4.3.2. Einfache Datentypen . . . . . . . . . . . . . . 4.3.3. Listen . . . . . . . . . . . . . . . . . . . . . . 4.3.4. Eingebaute Funktionen . . . . . . . . . . . . . 4.3.5. Eigene Datentypen . . . . . . . . . . . . . . . 4.3.6. Quicksort Implementierung in Haskell . . . . . 4.4. Module . . . . . . . . . . . . . . . . . . . . . . . . . .. iii. . . . . . .. . . . . . . . . . .. . . . . . .. . . . . . . . . . .. . . . . . .. . . . . . . . . . .. . . . . . .. . . . . . . . . . .. . . . . . .. . . . . . . . . . .. . . . . . .. . . . . . . . . . .. . . . . . .. . . . . . . . . . .. . . . . . .. . . . . . . . . . .. . . . . . .. . . . . . . . . . .. . . . . . .. . . . . . . . . . ..

(6) III. SCFG-SORT ALGORITHMUS. 30. 5. Sortieren von SLP-komprimierten Strings 5.1. Genaue Problembeschreibung . . . . . . . . . . . . . . . . . . 5.2. Zerlegung des Problems . . . . . . . . . . . . . . . . . . . . . 5.3. Ein effizienter Algorithmus für das Vergleichsproblem auf SLPs 5.4. Extrahieren des i. Symbols . . . . . . . . . . . . . . . . . . . . 5.5. Finden des längsten gemeinsamen Präfixes . . . . . . . . . . .. . . . . .. . . . . .. . . . . .. . . . . .. . . . . .. . . . . .. IV. IMPLEMENTIERUNG UND TESTS. 31 31 32 32 33 34. 39. 6. Repräsentation des SCFG-Sort-Algorithmus in Haskell 40 6.1. Benutzerschnittstellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 6.2. Datentypen des SLPs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 6.3. Hilfreiche Funktionen aus dem Modul GBC . . . . . . . . . . . . . . . . 43 6.4. Implementierung der Hauptalgorithmen des SLP-Sorts . . . . . . . . . . 43 6.4.1. Die Quicksort Implementierung . . . . . . . . . . . . . . . . . . . 43 6.4.2. Implementierung zum Lösen des Vergleichsproblems für SLPs . . 44 6.4.3. Finde das Terminal im SLP . . . . . . . . . . . . . . . . . . . . . 45 6.4.4. Berechnung des längsten gemeinsamen Präfixes . . . . . . . . . . 45 6.4.5. Suchen des Nichtterminals und Erweiterung der SLP um Produktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 7. Testfälle zum SLP-Sort 7.1. Eingabegrammatik . . . . . . . . . . . . . . . . 7.1.1. Die erzeugten Wörter der Nichtterminale 7.2. Sortieren der Nichtterminale . . . . . . . . . . . 7.3. Quicksort Versus Mergesort . . . . . . . . . . . 7.4. SLP-Sort Versus Alternativsort . . . . . . . . . 7.5. Extreme Testfälle . . . . . . . . . . . . . . . . .. V. Schluss. . . . . . .. . . . . . .. . . . . . .. . . . . . .. . . . . . .. . . . . . .. . . . . . .. . . . . . .. . . . . . .. . . . . . .. . . . . . .. . . . . . .. . . . . . .. . . . . . .. 48 48 48 49 50 53 55. 57. 8. Zusammenfassung und Ausblick 58 8.1. Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 8.2. Fazit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 8.3. Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 Literatur. 60. iv.

(7) Abbildungsverzeichnis 1. 2. 3. 4.. Mögliche Wege um den Präfix zu finden . . . . . . . Beispielbaum zu dem SLP aus Beispiel 5.8 . . . . . Aufrufhierarchie des SLP-Sort-Algorithmus . . . . SLP-Mergesort-Algorithmus Versus Alternativ Sort. 1. . . . .. . . . .. . . . .. . . . .. . . . .. . . . .. . . . .. . . . .. . . . .. . . . .. . . . .. . . . .. 35 36 41 54.

(8) Teil I. Einleitung Dieser Abschnitt gibt eine Orientierung darüber, was uns in dieser Arbeit erwarten wird. Er dient zur Motivation, gibt die Aufgabenstellung/Ziele wieder und wird mit einer kurzen Zusammenfassung über die Kapitel abgeschlossen.. 2.

(9) 1. Motivation und Überblick Diese Arbeit beschäftigt sich mit dem Sortieren von Grammatik-komprimierten Strings.. 1.1. Motivation Ein anhaltender Trend in der Informatik und der zunehmenden Digitalisierung des alltäglichen Lebens ist, dass immer mehr Daten anfallen, die gesammelt, verarbeitet und analysiert werden müssen. Daraus entstehen neue Herausforderungen an Programme und Algorithmen, die dadurch mit sehr großen Daten umgehen müssen. Das Gebiet der Algorithmen für Grammatik-komprimierte Daten (oder spezieller: Strings) beschäftigt sich mit der Verarbeitung von großen und gut komprimierbaren Daten mit dem Ziel diese zu Optimieren. Indem Algorithmen direkt mit den komprimierten Daten arbeiten, ohne diese zu entpacken, werden die Arbeitsschritte beschleunigt. Um dies zu erreichen, müssen immer neue bzw. angepasste Varianten bestehender Algorithmen entworfen werden, z.B. solche die komprimierte Daten vergleichen, durchsuchen, usw. Zur Darstellung der komprimierten Daten werden spezielle kontextfreie Grammatiken verwendet, die sich als mathematischer Formalismus sehr gut zur Analyse und zum Entwurf entsprechender Algorithmen eignen. Für Strings eignen sich sogenannte StraightLine-Programs (SLPs): Ein SLP ist eine eingeschränkte, kontextfreie Grammatik in Chomsky-Normalform. Im Bestfall können durch die Kompression mit SLPs, exponentielle Kompressionsraten erzielt werden, d.h. dass eine Grammatik mit n Produktionen im Bestfall einen String der Länge O(2n ) darstellen kann. Es gibt viele Algorithmen, die auf SLPs arbeiten. Ein bekannter und wichtiger Algorithmus ist der von Plandowski [Pla94], der die Gleichheit zweier Strings (gegeben durch zwei Nichtterminale der SLP) in polynomieller Zeit (polynomiell in der Größe der Grammatik und nicht in der Größe der Strings) entscheiden kann. Einen Überblick über weitere Algorithmen auf SLPs findet man z.B. in [Loh12]. Um polynomielle Zeit zu erreichen, ist das wichtigste Entwurfsprinzip all dieser Algorithmen, dass die Wörter der Grammatiken niemals vollständig erzeugt werden, denn eine solche Dekompression würde für gut komprimierte Worte direkt zu einer exponentiellen Laufzeit führen.. 1.2. Ziele und Aufgabenstellung In dieser Bachelorarbeit wollen wir das in der Informatik wohlbekannte Problem des Sortierens von Strings betrachten und analog zu den oben genannten Algorithmen für. 3.

(10) andere Probleme, einen Algorithmus zum Sortieren von SLP-komprimierten Strings entwerfen, erläutern, analysieren und schließlich in der funktionalen Programmiersprache Haskell implementieren. Das Sortieren an sich ist ein altbekanntes und auch gut untersuchtes Problem der Informatik. Es gibt sehr viele gute Sortierimplementierungen wie zum Beispiel Quicksort oder Mergesort, welche eine n-elementige Liste in Zeit O(n log n) sortieren können1 . Diese Laufzeit ist ebenso eine untere Schranke für allgemeine vergleichsbasierte Sortierverfahren (vgl. [OW97] S’ 138 Satz 2.4). Auch das Sortieren von Strings (bezüglich der lexikographischen Ordnung2 ) ist gut untersucht. So handelt es sich zum Beispiel bei Radix-Sort, um ein Standartverfahren zum Sortieren von Kurzen Strings, wobei es sich am meisten lohnt, wenn die Strings ungefähr gleich lang sind3 . Das Sortieren von kurzen Strings mit Mergesort und Quicksort funktioniert auch gut, jedoch entstehen Probleme bei langen Strings. Die obige Laufzeit von O(n log n) ist nur übertragbar, wenn die zu sortierenden Strings eine konstante Länge haben. Bei nicht konstanter Länge4 ist ein einzelner Vergleich der Strings nicht konstant, wodurch die Laufzeit von der Länge der Strings abhängig ist. Es ergibt sich daher eine worst-case Laufzeit von O(nm(log nm)) zum Sortieren von n Strings der Länge maximal m, bei Verwendung von Mergesort. Bei besonders langen und vielen Strings kann bereits die oben erwähnte Laufzeit von O(nm(log nm)) in der Praxis problematisch werden. Es ist daher sinnvoll Sortieralgorithmen für komprimierte Strings zu entwerfen und zu betrachten, in der Hoffnung, dass bei guter Kompression auch das Sortierproblem schneller gelöst werden kann, d.h. eine Frage, die diese Bachelorarbeit beantworten soll ist: Wie ist die Laufzeit, wenn man besonders lange Strings sortieren will? Kann man Zeit gewinnen, indem man diese großen Strings in Grammatiken komprimiert und dann diese Grammatiken sortiert? Gegen Ende dieser Arbeit, werden all diese Fragen beantwortet sein. Das Ziel ist es daher eine Haskell-Implementierung zum Sortieren von SLP-komprimierten 1. Bei Quicksort müsste man eigentlich von einer randomisierten Laufzeit sprechen, um an dieser Stelle genauer zu sein. 2 Die lexikographische Ordnung funktioniert wie die Ordnung im Lexikon. Man Vergleicht erst den ersten Buchstaben, anschließend den zweiten Buchstaben usw. 3 Der Unterschied ist kurz ausgedrückt: Sortieren von n Strings w1 , . . . , wn , wobei das Alphabet konstant ist: • Radix-Sort hat im worst-case die Laufzeit O(mn), wobei m die maximale Länge der Strings also m = maxi {(|wi |)} • Mergesort hat die Laufzeit O(m0 n log(m0 n)), wobei m0 die mittlere Länge der Strings also m0 = sumi {|wi |}/n (liegt daran, dass ein Vergleich von wi , wj , Zeit min{(|wi |, |wj |)} kostet) 4. Grob geschätzt lohnt sich somit Mergesort wenn m0 log(m0 n) < m gdw. m/m0 > log(m0 n). Zum Beispiel ist die Länge der beiden Wörter Hallo“ und Sauerstoffflasche“ nicht Konstant. ” ”. 4.

(11) Strings anzufertigen, die polynomielle Laufzeit in der Größe der SLP haben soll. Dabei kann (und soll) die bestehende Programmbibliothek Data.GBC (vgl. [gbc13]) verwendet werden, die neben der Repräsentation von SLPs in Haskell auch einige Algorithmen zur Verfügung stellt. Das Vorgehen zum Entwurf des Sortieralgorithmus auf SLPs lässt sich vereinfacht wie folgt beschreiben: Verwende ein beliebiges vergleichsbasiertes Sortierverfahren und implementiere den Vergleich zweier SLP-komprimierten Strings bezüglich der lexikographischen Ordnung effizient.. 1.2.1. Spezifikation der Aufgabe Das Ziel dieser Arbeit ist es, eine Implementierung in Haskell anzufertigen, die es erlaubt Grammatik-komprimierte Strings, welche als Liste von Nichtterminalen eingegeben werden, lexikographisch zu sortieren. Dieser Sortieralgorithmus soll eine polynomielle Laufzeit in der Größe der Grammatik haben und an die Bibliothek Data.GBC angebunden werden. Der wesentliche Schritt im Sortieralgorithmus ist der Vergleich zweier Nichtterminale A, B bezüglich ihrer dekomprimierten Strings und der lexikographischen Ordnung. 1. Hierbei muss zunächst die Länge n des längsten gemeinsamen Präfix von A und B berechnet werden. Dies kann durch die Methode der Intervallhalbierung (Binäre Suche) und dem Plandowski-Gleichheitstest effizient (d.h. polynomiell in der Größe der Grammatik) durchgeführt werden. 2. Berechne effizient das (n + 1)-te Symbol der zu A und B zugehörigen dekomprimierten Strings 3. Vergleiche beide Symbole. Dieser Vergleich legt die Ordnung von A und B fest Anschließend kann mit einem beliebigen Sortierverfahren sortiert werden.. 1.3. Übersicht über die Kapitel In Kapitel II, wohl einer der wichtigsten Kapitel, werden wir uns mit den Grundlagen beschäftigen, damit die Funktionsweise des Sortieralgorithmus für SLPs besser zu verstehen ist. Wir lernen hier die Sortiermethoden Quicksort und Mergesort kennen, gehen auf die kontextfreie Grammatiken ein und runden das mit einem kurzen Einblick in die verwendete Programmiersprache Haskell ab.. 5.

(12) Der SLP-Sort Algorithmus wird in Kapitel III erläutert. Hier geben wir den SLP-Sort Algorithmus mittels vielen kleinen Algorithmen wieder und besprechen dessen Laufzeit. In den letzten beiden Kapiteln betrachten wir einige Codeauszüge aus dem SLP-Sort Haskell Programm, gehen auf Testfälle ein und schließen mit einer Zusammenfassung ab.. 6.

(13) Teil II. GRUNDLAGEN In diesem Abschnitt widmen wir uns den Grundlagen für das Verständnis des SCFGSort-Algorithmus. Wir gehen auf die Sortieralgorithmen Quicksort und Mergesort ein, besprechen die Eigenschaften davon und betrachten die Funktionsweisen mit Beispielen. Weiterhin werden wir die kontextfreien Grammatiken kennenlernen, mit den darauf anwendbaren Algorithmen, wie dem Plandowski. Zu guter Letzt gibt es einen kurzen Einblick in die Programmiersprache Haskell mit ihren Eigenschaften und Modulen. Außerdem sehen wir auch eine Quicksort Implementierung.. 7.

(14) 2. Sortieren Wir werden in diesem Kapitel den Quicksort und Mergesort Algorithmus kennenlernen, deren Eigenschaften besprechen und sie anhand von Beispielen besser verstehen. Der Inhalt orientiert sich im Wesentlichen an ([BN11] S’ 146-149).. 2.1. Quicksort Der Quicksort-Algorithmus folgt dem Divide and Conquer Prinzip. Er ist kein stabiles Sortierverfahren. Das heißt, wenn Elemente mit der gleichen Ordnung eine Reihenfolge haben, dann haben sie nach dem Sortieren diese Reihenfolge nicht behalten. In den meisten Fällen ist der Quicksort ein Inplace-Algorithmus, was wiederum bedeutet, dass er kaum zusätzlichen Speicher benötigt. Bei dem Quicksort-Algorithmus, den ich im Rahmen meiner Bachelorarbeit verwende, handelt es sich nicht um einen InplaceAlgorithmus. Daher folgt: Bemerkung 2.1. Der in dieser Arbeit verwendete Quicksort-Algorithmus ist kein Stabiles Sortierverfahren und ein Inplace-Algorithmus. Die Laufzeit von Quicksort beträgt im Worst-Case Fall Θ(n2 ), im Average-Case und Best-Case Fall Θ(n log n). Aufgrund der guten Laufzeit wird Quicksort in der Praxis oft genutzt. Da wir aber in dieser Bachelorarbeit Strings vergleichen werden und der Vergleich zwischen Strings nicht konstant ist, ist es besser hier die Vergleiche als Maß für die Geschwindigkeit zu nehmen. Somit lässt sich sagen: Bemerkung 2.2. Quicksort benötigt zum Vergleichen von Strings im Best-Case und Average-Case Fall Θ(n log n) Vergleiche und im Worst-Case Fall beträgt die Anzahl der Vergleiche somit Θ(n2 ). Quicksort nutzt das Teile-und-Beherrsche-Prinzip zum Sortieren, welches gleich illustriert wird. In der Phase des Teilens wird ein Element als Pivot gewählt und die Liste wird in zwei Teillisten aufgeteilt. Hierbei gilt die Invariante das alle Elemente der linken Teilliste kleiner gleich als das Pivot und alle Elemente in der rechten Teilliste größer als das Pivot sind. Jetzt ruft sich Quicksort rekursiv für die linke und rechte Teilmenge auf (Beherrschen). Dies wiederholt sich solange bis jedes Element eine Mitte darstellt. Anschließend werden die Teilmengen wieder zu einer Menge zusammengefasst. Im folgenden Beispiel soll das Vorgehen des Algorithmus verdeutlicht werden:. 8.

(15) Beispiel 2.3 (Quicksort). Gegeben sei folgende Liste, die wir mittels eines DeklarativenQuicksort-Algorithmus5 sortieren wollen: E6 — E8 — E4 — E1 — E3 — E7 — E2 — E5 Hier stehen E1 . . . E8 stellvertretend für Zahlen, Strings und Chars. D.h., die Elemente die wir sortieren wollen sind unbestimmt. Im folgenden wird angenommen, dass die Ordnung E1 ≤ E2 ≤ . . . ≤ E8 ist. Eine einfache Möglichkeit der Pivotwahl ist es immer das linkeste Element der Liste als Pivot zu wählen6 . D.h. das erste Pivot ist Element E6. E6. E8 — E4 — E1 — E3 — E7 — E2 — E5. Als nächstes wird jedes Elemente das kleiner-gleich ist als das Pivot selbst, links vom Pivot stehen und jedes Element das größer als das Pivot ist, rechts davon. E4 — E1 — E3 — E2 — E5. E6. E8 — E7. Wir haben unser erstes Endelement gefunden, d.h. E6 befindet sich an der richtigen Position. Nun wird rekursiv die linke Teilliste (links von E6) mit dem gleichen Verfahren aufgerufen und dadurch sortiert. Und im Anschluss ebenso für die rechte Teilliste. Der nächste rekursive Aufruf ist die Liste mit den Elementen E4, E1, E3, E2, E5. Hier wählen wir wieder das erste Element als Pivot, also E4. Wir stellen auch schon die Invariante her, das jedes Element links vom Pivot kleiner-gleich ist als das Pivot selbst und jedes Element auf der rechten Seite größer ist als das Pivot. E1 — E3 — E2. E4. E5. E6. E8 — E7. Da das Element E5 nun auch schon seine endgültige Position erreicht hat, können wir uns den rekursiven Aufruf sparen und haben E5 auch als abgeschlossen gekennzeichnet. Es folgt der Rekursive Aufruf für die Elemente E1, E3, E2. E1. E3 — E2. E4. E5. E6. E8 — E7. Das Element E1 war schon an seiner richtigen Position, somit hat sich hier nicht viel getan. Nun rufen wir die letzten beiden Teillisten noch rekursiv auf.. 5. Wir sagen dass ein Algorithmus Deklarativ ist, wenn die Beschreibung des Problems im Vordergrund steht und nicht der Berechnungsablauf. 6 Es ist effizienter, wenn man das Pivot zufällig wählt, doch soll das uns nun nicht weiter beschäftigen.. 9.

(16) E1 E1. E2 E2. E3 E3. E4. E5. E4. E5. E6 E6. E8 — E7 E7. E8. Im letzten Schritt werden die Teillisten wieder zu einer ganzen Liste zusammengefügt. E1 — E2 — E3 — E4 — E5 — E6 — E7 — E8 Einen Algorithmus, der diese Prozedur implementiert, lernen wir in Kapitel 4.3.6 kennen.. 2.2. Mergesort Betrachten wir einen anderen Divide and Conquer Algorithmus den Mergesort. Er ist stabil und in der Regel nicht In-Place. Bemerkung 2.4. Der in dieser Arbeit verwendete Mergesort-Algorithmus ist ein Stabiles Sortierverfahren und kein Inplace-Algorithmus. Der Mergesort-Algorithmus hat eine Worst, Average und Best-Case-Laufzeit von O(n log n), weshalb er um einiges schneller ist als sein Kontrahent Quicksort. Hier sei auch bemerkt, dass diese Laufzeit für unsere Zwecke wieder wenig aussagekräftig ist und deswegen gilt: Bemerkung 2.5. Der in dieser Bachelorarbeit verwendete Mergesort-Algorithmus benötigt im Worst, Average und Best-Case Fall O(n log n) Vergleiche. Das Verfahren des Mergesort-Algorithmus besteht darin, die Listen solange in zwei Hälften zu teilen bis jede Teilliste nur noch aus einem Element besteht. Im Anschluss mischt der Mergesort-Algorithmus immer zwei Elemente zu einer Teilliste. Bei dem Mischen vergleicht er die zwei Elemente und fügt sie dann in der richtigen Reihenfolge in eine neue Teilliste ein. Dieses vorgehen wird solange wiederholt, bis alle Elemente verglichen und zu einer neuen Liste gemischt wurden. Da die Liste manchmal eine ungerade Anzahl von Elementen hat, kann es vorkommen, dass ein Element im ersten Schritt nach dem Mischen übrig bleibt. Dieses Element bleibt unverändert und wird erst im nächsten Schritt verarbeitet. Wir betrachten ein Beispiel: Beispiel 2.6 (Mergesort). Gegeben sei wie in Beispiel 2.3 folgende Liste, die wir mittels eines Mergesort-Algorithmus sortieren wollen: E6 — E8 — E4 — E1 — E3 — E7 — E2 — E5. 10.

(17) Wobei E1 . . . E8 stellvertretend für Zahlen, Strings und Chars stehen. D.h. die Elemente die wir sortieren wollen sind unbestimmt. Wir nehmen an, dass die Ordnung E1 ≤ E2 ≤ . . . ≤ E8 gilt. Zuerst rufen wir den Mergesort-Algorithmus immer solange rekursiv auf und teilen dabei die Listen immer in zwei Teillisten bis wir alle Elemente in einer eigenen Teilliste haben.. E6. E6 — E8 — E4 — E1. E3 — E7 — E2 — E5. E6 — E8. E3 — E7. E8. E4 — E1 E4. E1. E3. E7. E2 — E5 E2. E5. Es folgt der rekursive Aufstieg, d.h. wir fangen an jeweils die ersten zwei Teillisten zu einer Teilliste zusammenzufassen. Dabei gilt, dass die Elemente nach der Größe in die neue Teilliste eingefügt werden. E6 — E8. E1 — E4. E3 — E7. E2 — E5. Beim Zusammenfügen zu der 2er-Liste hat sich nur die Reihenfolge der 2en Teilliste geändert, d.h. E4 wurde mit E1 verglichen und E1 durfte zuerst in die neue Liste eingefügt werden. Anschließend haben wir auch E4 eingefügt. Der Vorgang wird wiederholt und hinterher mischen wir wieder. E1 — E4 — E6 — E8. E2 — E3 — E5 — E7. Schauen wir uns den Vorgang des Mischens nochmal genauer an. Zuerst wurden die ersten beiden Linken-Teillisten gemischt, d.h. E6, E8 und E1, E4. Hier wurde nun E1 mit E6 verglichen, weil E1 kleiner als E6 ist, kam E1 zuerst in die neue Teilliste. Als nächstes wurde E6 mit E4 verglichen, es stellt sich heraus, dass E4 auch kleiner ist als E6, somit wird E4 in die neue Teilliste eingefügt. Die rechte Teilliste ist somit leer, deswegen fügen wir E6,E8 an das Ende der Teilliste (da wir E6, E8 im vorherigen Schritt schon sortiert haben, geht die Ordnung durch das Anhängen nicht verloren). Dasselbe machen wir auch mit den rechten Teillisten. Es folgt der letzte Schritt, das Mischen der Teillisten der Größe 4, der sortierten Listen. E1 — E2 — E3 — E4 — E5 — E6 — E7 — E8. Somit ist die Liste sortiert und wir sind fertig.. 11.

(18) 3. Komprimieren mit kontextfreien Grammatiken In diesem Abschnitt erläutern wir zunächst kontextfreie Grammatiken, im Anschluss werden die zur komprimierten Darstellung verwendeten speziellen kontextfreien Grammatiken (SLPs) veranschaulicht und abschließend geben wir bekannte Verfahren auf SLPs wieder, die im späteren Verlauf der vorliegenden Arbeit benötigt werden. Kontextfreie Grammatiken gehören zu dem Teilgebiet der formalen Sprachen. Sie werden unter anderem in Programmiersprachen eingesetzt um effizient zu parsen. Weiterhin sind sie von praktischer Bedeutung: ”. . . vor allem bei der Definition von Programmiersprachen, bei der Formalisierung der Syntaxanalyse, beim Vereinfachen der Übersetzung von Programmiersprachen und in anderen Prozessen, bei denen Zeichenketten verarbeitet werden. Z.B. sind kontextfreie Grammatiken nützlich zur Beschreibung korrekt geklammerter arithmetischer Ausdrücke und der Block-Struktur in Programmiersprachen (d.h. korrekte Klammerung der begins und ends).“ [HU94]. Definition 3.1 (Kontextfreie Grammatik). Eine kontextfreie Grammatik G besteht aus einem Tupel (N, Σ, S, P ) wobei: • N eine endliche Menge von sogenannten Nichtterminalen ist, also solche die noch weiter ersetzt werden können • Σ eine endliche Menge mit Σ ∩ N = ∅, den sogenannten Terminalen, ist, wobei diese nicht weiter ersetzt werden können • S ∈ N das Startsymbol ist und • P eine endliche Menge von Produktionen ist, wobei eine Produktion eine Regel der Form A → w ist, mit A ∈ N und w ist eine sogenannte Satzform: Eine Satzform ist ein Wort aus (N ∪ Σ)* Hierbei bezeichnet * den Kleenschen Abschluss. Mit  bezeichnen wir im folgenden das leere Wort. Die kontextfreie Grammatik wird dem Typ 2 der Chomsky Hierarchie (vgl. [Lan11a]) zugeordnet. Schauen wir uns als nächstes ein Beispiel zur kontextfreien Grammatik an. Beispiel 3.2 (kontextfreie Grammatik). Gegeben sei die Menge der Nichtterminale {S, A, B, C, Z, L} mit Startsymbol S, die Menge der Terminale {a, b, 1, 2, 3, } und folgende Produktionen:. 12.

(19) S S C A B Z Z Z L. −→ −→ −→ −→ −→ −→ −→ −→ −→. ABCS  AZL a b 1 2 3. (1) (2) (3) (4) (5) (6) (7) (8) (9). Dadurch ist eine kontextfreie Grammatik definiert und wir können damit Wörter erzeugen. Schauen wir uns doch mal an, welche Wörter unsere kontextfreie Grammatik erzeugen kann. Der wesentliche Schritt bei einer Herleitung ist das Ersetzen eines Nichtterminals A durch w, wobei A → w eine Produktion ist. Ausgehend von dem Startsymbol S können wir uns am Anfang entscheiden, ob wir irgendwas erzeugen wollen oder direkt ein  wählen. Wir entscheiden uns für den einfacheren Fall und wählen ein  (2). Demzufolge wäre unser Wort, bestehend nur aus dem leeren Wort, das leere Wort. Das erste Wort unserer oben definierten Sprache steht nun fest. Wie sieht das zweite Wort aus? Wir starten mit (1) und erhalten A B C S. Das A ersetzen wir direkt durch a (4). Jetzt haben wir den ersten Buchstaben unseres Wortes. Es folgt das B, welches sich zu b auflöst (5). Jetzt wird es ein wenig interessanter, denn das Nichtterminal C erzeugt die Nichtterminale A Z L. Fassen wir kurz nochmal anschaulich zusammen was wir gerade aufgelöst haben: (1). (4). (5). S −→ A B C S −→ a B C S, −→ a b C S Wir führen die Produktion fort, lösen das C auf und erhalten: (3). a b C S −→ a b A Z L S Denn das C wird zu A Z L und der Rest bleibt unverändert. Führen wir unser Beispiel fort: (4). a b A Z L S −→ a b a Z L S. 13.

(20) Wir Lösen das Z auf und können nun aus den Zahlen 1, 2 oder 3 auswählen. Wählen wir die 2 und führen unser Beispiel fort. (7). (9). a b a Z L S −→ a b a 2 L S −→ a b a 2. S. Es ist etwas interessantes eingetreten, deshalb stoppen wir an dieser Stelle. Jetzt dürfen wir das S wieder auflösen, doch ist es nun entscheidend, ob wir uns für das  entscheiden und somit die Ableitung beenden oder, ob wir wieder von vorne anfangen und das Wort verlängern. Wir wählen das Endsymbol  und terminieren. Das erzeugte Wort sieht folgendermaßen aus: aba2. . Umgangssprachlich ist die oben definierte Sprache folgendermaßen beschrieben, erzeuge Null mal oder unendlich oft das Wort: a, b, a, Zahl zwischen 1 und 3, Unterstrich und epsilon Somit haben wir eine Möglichkeit kennengelernt Wörter zu erzeugen und Sprachen zu definieren. Bei unserer Beispielgrammatik haben sich einige Merkmale kenntlich gemacht. Eines dieser Merkmale ist, dass auf der linken Seite der Produktion stets ein Nichtterminal war. Desweiteren haben wir drei Produktionen benötigt um die Zahlen 1,2 oder 3 zu erzeugen. Die Andeutung macht nun neugierig auf die Möglichkeiten der formalen Grammatiken. Alle Möglichkeiten durchzuspielen würde aber den Rahmen dieser Bachelor Arbeit sprengen, daher kann zur Vertiefung die Chomsky-Hierarchie in [HU94] (S’ 237-253) nachgelesen werden. Als Nächstes geben wir eine Definition über die von der kontextfreien Grammatik erzeugten Sprache. Definition 3.3 (Von der kontextfreien Grammatik erzeugten Sprache). Sei G = (N, Σ, S, P ) eine kontextfreie Grammatik, u, v, w Satzformen über (N ∪ Σ), A ein Nichtterminal aus N und A → w eine Produktion aus P Dann kann uwv direkt aus uAv hergeleitet werden. Wir schreiben dies als uAv →G uwv. Die Herleitungsrelation mit beliebig (aber endlich) vielen Schritten notieren wir als w1 →∗G wn , die genau dann erfüllt ist, wenn wi →G wi+1 für i = 1, . . . , n − 1 und n ≥ 1. Für ein Nichtterminal A ∈ N bezeichnet LG (A) die von A erzeugte Sprache, die durch LG (A) := {w ∈ Σ∗ | A →∗G w} definiert ist.. 14.

(21) Die von der Grammatik G erzeugte Sprache ist LG (S).. 3.1. Chomsky-Normalform In diesem Abschnitt wird die Chomsky-Normalform kurz dargestellt. Die Darstellung richtet sich im Wesentlichen nach [Lan11b]. Die Chomsky-Normalform hat seinen Namen von dem Linguisten Noam Chomsky und ist eine kontextfreie Grammatik. Definition 3.4 (Chomsky-Normalform). Gegeben sei eine kontextfreie Grammatik G = (N, Σ, S, P ) mit Startsymbol S. Die Grammatik G ist dann in Chomsky-Normalform, wenn jede Produktion P eine der folgenden Formen erfüllt: • C −→ A B • C −→ a • (S −→ ) Wobei A, B, C ∈ N und a ∈ Σ. Wenn die Produktion S −→  erlaubt ist, darf auf der rechten Seite vom Pfeil kein S stehen. Um die Definition nochmal umgangssprachlich zu formulieren, besteht jede Produktion von der Chomsky-Normalform aus nur einem Nichtterminal auf der linken Seite vom Pfeil und Zwei Nichtterminalen oder einem Terminal auf der rechten Seite vom Pfeil. Zu jeder kontextfreien Grammatik gibt es eine äquivalente kontextfreie Grammatik ” in Chomsky-Normalform“ [Lan11b]. Wir wollen die kontextfreie Grammatik noch weiter einschränken und kommen somit zum SLP.. 3.2. Straight-Line-Program (SLP) Wird nun die Definition der Chomsky-Normalform genommen und die Produktion S →  entfernt, erhält man schon fast die Definition des SLPs. Es wird lediglich hinzugefügt, dass es für jedes Nichtterminal N genau eine Produktion gibt, dass es kein Startsymbol gibt und dass die Grammatik azyklisch ist. Somit ist die Definition vom SLP vollständig. Das Programm zu dieser Bachelor Arbeit, wird als Eingabe den SLP nutzen!. 15.

(22) Definition 3.5 (Ein SLP). Ein SLP ist ein 3-Tupel (N, Σ, P ) wobei N eine Menge von Nichtterminalen, Σ eine Menge von Terminalen, P eine Menge von Produktionen ist (analog zu kontextfreien Grammatiken und der Chomsky-Normalform) und zusätzlich gilt: • Für jedes Nichtterminal A ∈ N enthält P genau eine Produktion A → w • Die Grammatik ist nicht rekursiv, d.h. es gibt kein Nichtterminal A ∈ N , sodass A erreichbar von A ist, wobei: – Wir sagen A2 ist direkt von A1 erreichbar (notiert als A1 < A2 ), wenn A1 → w ∈ P und A2 kommt in w vor. – Wir sagen An ist von A1 erreichbar (A1 <+ An ), wenn es A2 , . . . , An−1 ∈ N gibt, sodass Ai < Ai+1 für i = 1, . . . , n − 1 und n > 1.. Es lässt sich leicht nachrechnen, dass für jedes Nichtterminal A eines SLPs gilt: L(A) ist eine einelementige Menge. Daher werden SLPs auch manchmal als SCFG (Singleton Context Free Grammar) bezeichnet. Im Folgenden schreiben wir daher für Nichtterminale eines SLPs G: wortG (A) = w, wenn LG (A) = {w} gilt. 3.2.1. Eigenschaften von SLP Ein wesentlicher Unterschied zu der kontextfreien Grammatik ist, dass jedes Nichtterminal der SLP genau ein Wort generiert. Weiterhin kann das SLP einen String, im Best Case, in logarithmischer Größe darstellen. Beispiel 3.6 (SLP). Gegeben sei die Menge der Nichtterminale {S, B, C, D, H, A, L, O} mit Startsymbol S, die Menge der Terminale {h, a, l, o } und den dazugehörigen Produktionen: S B C D H A L O. −→ −→ −→ −→ −→ −→ −→ −→. H B AC DO LL h a l o. (1) (2) (3) (4) (5) (6) (7) (8). 16.

(23) Beginnend vom Startsymbol leiten wir uns das folgende Wort her: (1). (5). (2). (6). (3). (4). (7). S −→ H B −→ h B −→ h A C −→ h a C −→ h a D O −→ h a L L O −→ (7) (8) h a l L O −→ h a l l O −→ h a l l o An dieser Stelle kann die Frage aufkommen, wozu das Ganze gut sein soll, immerhin haben wir abgesehen von dem Wort hallo“, welches nur aus 5 Zeichen besteht, eine ganze ” Menge an Zeichen zu speichern, inklusive der Produktionen. Um Klarheit in diesem Paradoxon zu erhalten, führe man sich vor Augen, wie oft sich ein Vorname in einem Telefonbuch wiederholt. Hierbei müsse der Vornamen nicht mehr 100-mal gespeichert werden, sondern er würde nur einmal gespeichert. Schauen wir uns nun ein Beispiel an, der den Best-Case präsentiert. Beispiel 3.7 (SLP Best Case). Gegeben sei wieder die Menge der Nichtterminale {S, F, E, D, C, B, A, X } mit Startsymbol S, dem Terminal x und den dazugehörigen Produktionen: S A B C D E F X. −→ −→ −→ −→ −→ −→ −→ −→. AA BB CC DD EE F F X X x. (1) (2) (3) (4) (5) (6) (7) (8). Das erzeugte Wort lautet 128-mal x“. Jetzt sehen wir, dass 8 Produktionen und 9 ” Zeichen zu speichern sind, statt 128 Zeichen. Das ist ein wesentlicher Unterschied! Abschließend sei noch erwähnt, dass beim Herleiten der Wörter die Linksherleitung verwendet wurde. Es gibt auch die Rechtsherleitung (vgl. [AB02] S’ 280), wobei man hier von rechts beginnt das Wort herzuleiten.. 3.3. Bekannte Algorithmen auf SLPs Es gibt viele gute und bekannte Algorithmen, die auf Grammatik-komprimierten Strings arbeiten. Zwei von diesen werden wir in diesem Abschnitt kennenlernen, denn sie werden später in unseren Sortierverfahren als Unterprozedur verwendet. Zuerst ist es aber notwendig zwei Definitionen einzuführen. Definition 3.8 (Größe der SLP). Gegeben sei ein SLP G = (Σ, N, P ). Die Größe der SLP G definieren wir durch die Anzahl der Produktionen und schreiben dafür |G|.. 17.

(24) Definition 3.9 (Länge des Wortes). Wir bezeichnen für ein Wort w mit |w| die Länge des Wortes w. Diese Definitionen veranschaulichen wir uns an einem Beispiel. Beispiel 3.10. Sei G1 die Grammatik aus Beispiel 3.6 und G2 die Grammatik aus Beispiel 3.7, dann erhält man durch |G1 | = 8 und |G2 | = 8 die jeweilige Größe. Sei weiterhin w1 das von G1 erzeugte Wort und w2 das von G2 erzeugte Wort, dann ist die Größe von |w1 | = 5 und die Größe von |w2 | = 128. 3.3.1. Algorithmus von Plandowski Wojciech Plandowski [Pla94] schrieb den Algorithmus zum Lösen des Wortproblems, den ich in dieser Bachelorarbeit verwende. Das Wortproblem für SLPs ist wie folgt definiert: Definition 3.11 (Wortproblem). Gegeben sei ein SLP G = (N, Σ, P ) und zwei Nichtterminale A1 , A2 ∈ N . Das Wortproblem besteht darin zu entscheiden, ob wort(A1 ) = wort(A2 ) gilt. D.h. ob die beiden Nichtterminale A1 , A2 das gleiche Wort erzeugen. Das Besondere an Plandowskis-Algorithmus ist, dass der Vergleich der SLPs erfolgt, ohne die Wörter erzeugen zu müssen. Da das Erzeugen der Wörter der Grammatik, exponentiell viel Zeit, in der Größe der Grammatik, in Anspruch nehmen könnte. Die Eingabe für den Algorithmus von Plandowski besteht aus zwei SLPs (in Beispiel 3.13 sehen wir, dass wir in Wirklichkeit nur einen SLP eingeben) und zwei Nichtterminale. D.h. Plandowski’s Algorithmus vergleicht die beiden Nichtterminale der SLPs bezüglich ihrer dekomprimierten Strings. Ein weiterer Vorteil besteht darin, dass der Vergleich zwischen zwei Wörtern mit einem beliebigen Nichtterminal durchgeführt werden kann, da wir die Wörter nicht erzeugen. Veranschaulichen wir dies an einem Beispiel: Beispiel 3.12 (Plandowski triviale Entscheidbarkeit). Sei G1 die Grammatik aus Beispiel 3.6 und G2 die Grammatik aus Beispiel 3.7. Wir nutzen G1 und G2 als Eingaben für den Algorithmus von Plandowski. Um die Voraussetzung des Algorithmus vom Plandowski zu erfüllen, geben wir noch zwei Nichtterminale ein. Einfachheitshalber wählen wir H und X als Nichtterminale. Bekanntlich erzeugt das Nichtterminal H in G1 das Wort h und das Nichtterminal X erzeugt das Wort x in G2 . Wir nehmen also an, dass das Nichtterminal X das erste Symbol von G2 erzeugt.. 18.

(25) Wir wollen die ersten Zeichen der beiden erzeugten Wörter vergleichen. Zur Erinnerung: das Nichtterminal S in G1 erzeugte das Wort hallo“ und in G2 erzeugte es das ” Wort x“*128 (128-mal x). ” Geben wir die Grammatiken G1 und G2 sowie die dazugehörigen Nichtterminale H und X in den Algorithmus von Plandowski ein und wir erhalten als Ausgabe False. Das bedeutet, dass die Wörter nicht gleich sind. Im Folgenden werden wir ein weiteres Beispiel betrachten, bei dem man nicht auf Anhieb erkennen kann, ob die gleichen Wörter erzeugt werden: Beispiel 3.13 (Plandowski nichttriviale Entscheidbarkeit). Gegeben sei eine Grammatik G mit Nichtterminalen {A, A1 , A2 , . . . , An , B, B1 , B2 , . . . , Bn , S1 , S2 }, n ∈ N und n > 0, Startsymbolen S1 und S2 , den Terminalen {a, b} und folgenden Produktionen: A B A1 A2 .. .. −→ −→ −→ −→. a b AB A1 A1. An B1 B2 .. .. −→ −→ −→. An−1 An−1 BA B1 B1. Bn S1 S2. −→ −→ −→. Bn−1 Bn−1 An A A Bn. Das Wortproblem für S1 , S2 zu entscheiden ist nicht trivial, da zwar beide Nichtterminale das gleiche Wort (ab)n a erzeugen, der Aufbau der dazugehörigen Produktionen jedoch sehr unterschiedlich ist. Man kann sich leicht überzeugen, dass |W ort(S1 )| = 2n + 1 gilt. Aus Platzgründen wird hier nicht weiter auf den Algorithmus von Plandowski eingegangen. Es ist lediglich noch nennenswert, dass der Algorithmus von Plandowski, das Wortproblem für SLPs in Zeit O(|G|3 ) löst (vgl. [Pla94]), wobei |G| die Größe der Grammatik bezeichnet. Satz 3.14. Der Algorithmus von Plandowski löst das Wortproblem für SLPs in O(|G|3 ).. 3.3.2. Das Erzeugen von Wortlängen Ein ganz wesentlicher Algorithmus auf den Grammatiken ist die Berechnung der Wortlängen. Ziel hierbei ist es, jedes Nichtterminal mit der Länge seines Wortes zu markieren. Im fol-. 19.

(26) genden betrachten wir hier lediglich eine kurze informelle Beschreibung des Algorithmus. Gegeben sei ein SLP G = (Σ, N, P ). A, B, C bezeichnen Nichtterminale aus N und a ein Terminal aus Σ. Führe mit der dynamischen Programmierung folgende Schritte auf dem SLP aus, bis alle Nichtterminale markiert sind: 1. Für A → a ∈ P setze |wort(A)| = 1 2. A → BC ∈ P berechne |wort(A)| = |wort(B)| + |wort(C)| Wir schreiben einfach Ai um kenntlich zu machen, dass für das Nichtterminal A |wort(A)| = i gilt. Beispiel 3.15 verdeutlicht dieses Vorgehen. Beispiel 3.15 (Wortlängen berechnen). Gegeben sei wieder die Grammatik aus Beispiel 3.6 mit der Menge der Nichtterminale {S, B, C, D, H, A, L, O }, Terminale {h, a, l, o } dem Startsymbol S und den dazugehörigen Produktionen mit den Wortlängen: S5 B4 C3 D2 H1 A1 L1 O1. −→ −→ −→ −→ −→ −→ −→ −→. H 1 B4 A1 C 3 D2 O1 L1 L1 h a l o. (1) (2) (3) (4) (5) (6) (7) (8). Folgende Tabelle verdeutlicht die Berechnung der Wortlängen. Nt. Wortlängenberechnung vom Nt. O→o L→l A→a H→h D → LL C → DO B → AC S → HB. |wort(O)| = 1 |wort(L)| = 1 |wort(A)| = 1 |wort(H)| = 1 |wort(D)| = |wort(L)| + |wort(L)| = 1 + 1 = 2 |wort(C)| = |wort(D)| + |wort(O)| = 2 + 1 = 3 |wort(B)| = |wort(A)| + |wort(C)| = 1 + 3 = 4 |wort(S)| = |wort(H)| + |wort(B)| = 1 + 4 = 5. Wir hatten das Glück, dass wir schon eine sortierte Reihenfolge der Produktionen hatten. Allgemein müssten die Produktionen in einem Vorverarbeitungsschritt topologisch sortiert werden. Weiterhin haben wir die dynamische Programmierung verwendet und somit Doppelauswertungen vermieden. Satz 3.16. Das berechnen der Wortlängen kann in Zeit O(|G|) mit topologischem Sortieren und dynamischer Programmierung erfolgen.. 20.

(27) Welches Wort würde nun das Nichtterminal C erzeugen? Wir springen mitten in die Grammatik und fangen an das Wort ab dem Nichtterminal C zu erzeugen: (3). (4). (7). (7). (8). C −→ D O −→ L L O −→ l L O −→ l l O −→ l l o Wir haben das Wort llo erhalten und stellen fest, dass die Wortlänge drei ist. In Kapitel 5 werden wir sehen, dass die erzeugten Wortlängen von großer Bedeutung sind.. 21.

(28) 4. Haskell In diesem Abschnitt behandeln wir die funktionale Programmiersprache Haskell, da die Implementierung des in dieser Arbeit verwendeten Programms in der Programmiersprache Haskell realisiert wurden ist. Haskell ist eine funktionale Programmiersprache, welche sich an mathematischen Denkstrukturen anlehnt. Das Besondere an Haskell ist, dass man Algorithmen meistens so implementieren kann, wie sie gelesen werden. In diesem Kapitel schauen wir uns die Programmiersprache Haskell in Fluggeschwindigkeit an. Die Inhalte dieses Kapitels orientieren sich im Wesentlichen an [BN11], deshalb wäre es als ergänzendes Literaturmaterial empfehlenswert.. 4.1. Eigenschaften von funktionalen Programmiersprachen Als erstes schauen wir uns die Eigenschaften der funktionalen Programmiersprachen an • Wenn wir von Funktionen im mathematischen Sinne sprechen, dann wissen wir welche Eingaben wir einer Funktion übergeben dürfen (Definitionsbereich), ebenso wissen wir auch welche Ausgaben wir erhalten werden (Wertebereich). • Eine funktionale Programmiersprache besteht also aus Funktionen, die zu komplexen Algorithmen geformt werden können. • Ein wesentliches Merkmal besteht darin, dass der Speicherplatz nicht direkt manipuliert werden kann, wie es in Java oder C++ der Fall ist. Eine solche Sprache nennt man auch reine funktionale Programmiersprache. • Eine funktionale Programmiersprache heißt stark typisiert, wenn jeder Ausdruck einen Typ hat. Bemerkung 4.1. Da die Ein- und Ausgabe in funktionalen Programmiersprachen klar definiert sein muss, folgt der Vorteil, dass weniger Fehler zur Laufzeit des Programms auftreten (Typsicherheit). Anders als in der Mathematik, ist es bei funktionalen Programmiersprachen wichtig zu wissen, in welcher Reihenfolge die Parameter auszuwerten sind, deshalb behandeln wir im nächsten Abschnitt Auswertungsstrategien.. 4.2. Auswertungsreihenfolgen Im Folgenden werden wir 3 Auswertungsstrategien kennenlernen. Auch Haskell benutzt eine dieser Auswertungsstrategien.. 22.

(29) • call-by-name Auswertung (Definitionsauswertung vor Argumentauswertung) In dieser Strategie sind die Argumente erst dann auszuwerten, wenn sie benötigt werden. • call-by-value Auswertung (Argumentauswertung vor Definitionsauswertung) Diese Auswertungsstrategie wertet erst die Argumente aus und arbeitet anschließend mit diesen weiter. • call-by-need Auswertung (call-by-name + sharing der Argumentauswertung) Hier wird die call-by-name Auswertung dahingehend erweitert, dass wenn etwas auszuwerten ist, dass dann alle gleichen Argumente auf einmal ausgewertet werden. Machen wir uns die Auswertungsstrategien mit einem Beispiel verständlicher. Beispiel 4.2 (Auswertungsreihenfolgen). Gegeben sei folgende Funktion, welche zwei Parameter a und b als Eingabe erwartet: Addiere (a, b) = a + b Wir können der Funktion Addiere zwei Argumente übergeben und sie würde uns ein Ergebnis liefern. Rufen wir die Funktion beispielsweise mit folgenden Argumenten auf: a=3+4 b=3+4 Den Aufruf pflanzen wir direkt in die Auswertungsstrategien ein: call-by-name. call-by-value. call-by-need. Addiere (3 + 4, 3 + 4) → (3 + 4) + (3 + 4) → 7 + (3 + 4) →7+7 → 14. Addiere (3 + 4, 3 + 4) → Addiere (7, 3 + 4) → Addiere (7, 7) →7+7 → 14. Addiere (3 + 4, 3 + 4) → (3 + 4) + (3 + 4) →7+7 → 14. Wichtig ist das pro Schritt immer nur eine Auswertung bzw. Definitionsersetzung ausgeführt werden kann. Es wird ersichtlich, dass die call-by-need Strategie wohl die wenigsten Schritte benötigt7 , da so spät wie möglich ausgewertet wird und Mehrfachauswertungen vermieden werden. Deshalb nutzt die funktionale Programmiersprache Haskell die call-by-need Strategie. Wir können festhalten, dass Haskell eine starke typisierte reine funktionale Programmiersprache ist, welche die call-by-need Strategie nutzt.. 7. Sei jeder Pfeile in der obigen Tabelle ein Auswertungs oder Definitionsersetzungs-Schritt, dann benötigt die call-by-name und call-by-value Strategie 4 Schritte und die call-by-need benötigt 3 Schritte.. 23.

(30) 4.3. Programmieren mit Haskell In diesem Abschnitt betrachten wir die Programmiersprache Haskell.. 4.3.1. Funktionen Wir werden sehen, dass die Programmierung von Funktionen in Haskell genauso leicht ist, wie sie in der Mathematik definiert wird. Es gibt lediglich nur ein paar kleine Abweichungen. Unser Beispiel 10 könnte in Haskell folgendermaßen aussehen: addiere a b = a + b. Diese Funktion würde wieder nur die beiden Werte a und b zusammenaddieren. Wie sieht nun die Funktion zum Multiplizieren zweier Zahlen aus? multipliziere a b = a * b. Es hat sich nicht viel verändert. Wir schauen uns noch eine rekursive Funktion in Haskell an. fakultaet n z = if z <= 1 then. 1 else n * (fakultaet n (z - 1)). Was macht diese Funktion? Sie berechnet die uns bekannte Fakultätsfunktion. Das if-then-else Konstrukt kann man folgendermaßen verstehen: 1. wenn z kleiner gleich 1 ist, dann sind wir fertig und wir geben 1 zurück. 2. wenn z > 1 ist, dann multipliziere n mit dem rekursiven Aufruf. Wir haben nun Zahlen verwendet und werden auch kurz auf Wahrheitswerte und Symbole eingehen.. 4.3.2. Einfache Datentypen In den meisten Programmiersprachen werden Datentypen wie Zahlen, Wahrheitswerte und Symbole verwendet. Deshalb geben wir hier nur eine kurze Idee der Datentypen an. Zahlentypen In Haskell gibt es folgende vier Datentypen für Zahlen: Diese sind Int, Integer, Float und Double. Int kann eine bestimmte Menge von Zahlen darstellen und Integer kann beliebig. 24.

(31) große/kleine Zahlen darstellen. Float und Double repräsentieren dann die Bruchzahlen, wobei Float 32 Bit und Double 64 Bit Genauigkeit verwendet. Symbole Der Datentyp der Symbole wird als Char bezeichnet. Haskell nutzt den Unicode, in dem alle Zeichen definiert sind. Wahrheitswerte Wahrheitswerte werden in Haskell durch den Typ Bool mit den beiden Werten True und False repräsentiert. Haskell unterstützt die bekannten Regeln zum Auswerten von booleschen Operationen.. 4.3.3. Listen Wie in jeder Programmiersprache üblich, gibt es auch in Haskell Listen. Da Haskell stark typisiert ist, haben die Elemente in den Listen immer den gleichen Typen. Betrachten wir ein paar Listenimplementationen in Haskell. liste1 = [1,2,3,4,5]. Intern wird diese Liste folgendermaßen dargestellt: 1:(2:(3:(4:(5:[])))). Durch diese Darstellung wird ersichtlich das Listen in Haskell rekursiv definiert sind. Diese Liste besteht aus den Zahlen von 1 bis 5. Alternativ könnte man dies auch so darstellen: liste2 = [1..5]. Wenn wir liste2 ausgeben, erscheinen die Zahlen 1 bis 5. Die Elemente der nächsten Liste bestehen nur aus dem Datentyp Char. liste3 = ["1","2","3","4","5"]. An dieser Stelle wird nochmal betont, dass die liste2 gleich der liste1 ist, jedoch ist die liste1 ungleich der liste3, somit ist auch die liste2 ungleich der liste3. Listen sind ein mächtiges Konstrukt in Haskell, doch aus Platzgründen werden wir uns nur noch die list comprehensions anschauen.. 25.

(32) list comprehensions Angenommen man möchte die Ersten zehn natürlichen Zahlen quadrieren, dann haben wir in Haskell viele Möglichkeiten dies zu implementieren. Eine der elegantesten Möglichkeiten ist es, list comprehensions einzusetzen. Hierbei wird eine Liste in eine andere Liste umgewandelt. Die Implementation hierfür schaut folgendermaßen aus: zumQuadrat = [x*x | x <- [1..10]]. Wenn wir nun diese Funktion im Haskell Interpreter aufrufen, dann erhalten wir folgendes Ergebnis: *Main> zumQuadrat [1,4,9,16,25,36,49,64,81,100]. Was wäre, wenn wir die Zahl 10 hinter den Punkten weggelassen hätten? Der Haskell Interpreter würde eine unendliche Liste erzeugen, jedoch wäre der Platzverbrauch irgendwann beendet und der Haskell Interpreter würde nach einer kurzen Zeit eine Fehlermeldung zurückgeben. Dennoch ist es sinnvoll in Haskell mit unendlichen Listen zu arbeiten. Da die Elemente Schritt für Schritt erzeugt werden und Haskell die call-by-need-Strategie verwendet, ist die Liste zwar unendlich, aber wir können endlich viele Elemente nehmen und den unendlichen Aufruf beenden. Veranschaulichen wir uns diese Mächtigkeit und lassen wir die Liste ins unendliche laufen. zumQuadratUnendlich = [x*x | x <- [1..]]. Nachdem wir die Funktion zumQuadratUnendlich aufrufen, werden sehr viele Zahlen angezeigt, jedoch erhalten wir am Ende eine Fehlermeldung, wie bereits oben geschildert. Um diese Fehlermeldung zu vermeiden, können wir die eingebaute Funktion take verwenden. Die eingebaute Funktion take tut nichts anderes als die Anzahl der gewollten Elemente zurückzugeben. *Main> take 10 zumQuadratUnendlich [1,4,9,16,25,36,49,64,81,100]. Somit haben wir die ersten zehn Elemente aus einer unendlichen Liste genommen. Wir haben nun zum ersten Mal eine eingebaute Funktion in Haskell genutzt, Haskell stellt viele solcher eingebauter Funktionen zur Verfügung.. 26.

(33) 4.3.4. Eingebaute Funktionen In diesem Abschnitt sind ein paar Funktionen, die in dieser Bachelorarbeit verwendet wurden aufgelistet. • div • insert • compare • lookup • find Ohne auf die Funktionen näher einzugehen, betrachten wir nur die Ein und Ausgaben und erläutern das Verhalten anschließend. *Main> div 20 2 10 *Main> insert 5 [1,2,4,6,9] [1,2,4,5,6,9] *Main> compare 3 4 LT *Main> lookup ’a’ [(’a’,1),(’b’,2),(’c’,3)] Just 2 *Main> find (>3) [1..10] Just 4. In der ersten Zeile des Codes kann man schon vermuten, dass div für Division steht, d.h. die Zahl 20 wurde durch 2 geteilt. Die Funktion insert fügt die Zahl 5 in die Liste ein. Die 5. Zeile vergleicht die 3 mit der 4 und gibt dann das entsprechende Resultat LT zurück. LT steht für lower term (zu Deutsch: kleinerer Term), was bedeutet, dass die 3 kleiner als die 4 ist. Lookup sucht nach dem ’a’ in der Liste mit Tupeln und gibt dann den dazugehörigen Partner aus. Die letzte Funktion gibt das erste Element aus, welches größer als die Zahl 3 ist.. 4.3.5. Eigene Datentypen Die Programmiersprache Haskell erlaubt es, eigene Datentypen zu definieren. Ein Datentyp besteht aus einer Sammlung von Typen. Möchte man nun zum Beispiel einen Datentyp Wochentage haben, so könnte dies folgendermaßen aussehen: data Wochentage = Montag | Dienstag | Mittwoch | Donnerstag | Freitag | Samstag deriving Show. 27.

(34) Wir können nun Montag ganz normal in Haskell verwenden. *Main> Montag Montag *Main> Dienstag Dienstag. Hätten wir den Datentyp Wochentage nicht definiert, würde uns der Interpreter eine Fehlermeldung zurückgeben. Die in dieser Arbeit verwendeten Grammatiken werden mittels Datentypen realisiert.. 4.3.6. Quicksort Implementierung in Haskell Wir haben schon die Funktionsweise des Quicksort-Algorithmus in Kapitel 2.1 gesehen, jetzt wollen wir uns die genaue Implementierung anschauen. quickSort :: Ord a => [a] -> [a] quickSort [] = [] quickSort (x:xs) = quickSort [y | y <- xs, y <= x] ++ [x] ++ quickSort [y | y <- xs, y > x]. Zum besseren Verständnis schneiden wir den Code (vgl. [BN11] S’ 146.) in Einzelteile. Zu Beginn betrachten wir die Implementierug in der ersten Zeile, die uns noch nicht bekannt ist. Hier wird lediglich der Typ der quickSort Funktion definiert. Was bedeutet also der Typ? Wir schauen uns den rechten Teil des Typs an [a] → [a]. Daraus ergibt sich, dass wir eine Liste mit Elementen eingeben und eine Liste mit Elementen ausgegeben bekommen. Das Wort Elemente“ ist nun viel zu allgemein, deshalb gibt es noch das Ord ” a =>. Das Ord a ist eine Klasse, in der Methoden definiert sind, die beschreiben, wie sich die Elemente dieser Klasse zu verhalten haben. Also ist unser a Mitglied von der Klasse Ord. Ord steht für Order und definiert Ordnungsrelationen. Demzufolge verstehen wir aus der ersten Zeile, dass wir Elemente eingeben wollen, die eine Ordnung haben können. In der zweiten Zeile sehen wir wieder etwas neues. Der Grundgedanke dahinter nennt sich pattern matching. Hier wird der Aufruf der Funktion auf die Fälle abgefragt. D.h. er prüft, ob die Eingabe eine leere Liste ist und wenn dies so ist, dann gibt er die leere Liste zurück. Weiterhin wird in der dritten Zeile die Liste in ein Anfangselement x und Restliste xs aufgesplittet. Anschließend ruft er sich rekursiv mit allen Elementen die kleiner gleich sind als das x auf, hängt sie an das x an und dann wird noch der rekursive Aufruf für alle Elemente, die größer sind als das x angehängt. Bespiel 2.3 kann zu einem genaueren Verständnis herangezogen werden.. 28.

(35) 4.4. Module Haskell-Programme können aus mehreren Modulen bestehen. Wobei ein Modul aus einer Sammlung von Datentypen und Funktionen besteht sowie Typklassen. Weiterhin gibt es in Haskell schon viele vordefinierte Module, die importiert werden können. Diese Module sind sehr effizient implementiert und erleichtern das Programmieren. Es bestehen sehr viele Module, wir werden doch nur kurz auf das Modul Data.Map eingehen. Wir haben weiter oben Listen kennengelernt. Zudem haben wir auch die Möglichkeit in Betracht gezogen, mit der Funktion lookup einen Schlüssel einzugeben, um den zugehörigen Wert zurückzuerhalten. Im Data.Map gibt es auch eine Funktion, die lookup heißt. Der Unterschied hier ist, dass der lookup aus dem Modul Data.Map wesentlich schneller ist. Da die Listen intern als Bäume gespeichert sind.. 29.

(36) Teil III. SCFG-SORT ALGORITHMUS In diesem Abschnitt widmen wir uns der Funktionalität des SCFG-Sort Algorithmus. Wir werden anhand von kleinen aufeinander aufbauenden Algorithmen, das Sortierproblem lösen. Am Ende dieses Abschnitts wird dann eine Laufzeitanalyse zum SCFG-Sort Algorithmus durchgeführt.. 30.

(37) 5. Sortieren von SLP-komprimierten Strings In diesem Kapitel wird das Kernstück dieser Bachelorarbeit vorgestellt, der SCFG-Sort Algorithmus. Wir werden in diesem Kapitel die wichtigsten Schritte des SCFG-SortAlgorithmus betrachten.. 5.1. Genaue Problembeschreibung Bevor das Problem definiert wird, werden noch ein paar Konventionen angegeben mit deren Hilfe es sich leichter arbeiten lässt. 1. Wir schreiben prefix(w, i) für i ∈ {0, . . . , |w|} um auszudrücken, dass der Präfix von w die Länge i hat. 2. Weiterhin bezeichnet w[i] für i ∈ {1, . . . , |w|} das Zeichen an Position i in w. 3. Sei Σ ein Alphabet bestehend aus Terminalen und sei weiterhin ≤ die Ordnung auf Σ. Seien w1 , w2 Wörter aus Σ∗ dann ist die Ordnung ≤ auf Worten über Σ∗ definiert als: w1 ≤ w2 gdw. ∃i: prefix(w1 , i) = prefix(w2 , i) und es gilt einer der beiden folgenden Fälle: a) w1 [i + 1] < w2 [i + 1] (wobei |w1 | > i und |w2 | > i) b) |w1 | = i und |w2 | ≥ i (d.h. w1 ist ein Präfix von w2 ) Beispiel 5.1 veranschaulicht uns die eingeführten Konventionen. Beispiel 5.1 (Konventionen). Gegeben seien die Wörter w1 = aa, w2 = aaa, w3 = aab, w4 = abcdef gh. Dann ist prefix(w4 , 6) = abcdef und w[6] = f . Weiterhin ist w1 ≤lex w2 und w1 ≤lex w3 aber w4 6≤lex w3 . Kommen wir nun zu dem Hauptproblem. Gesucht ist ein Algorithmus für das folgende Sortierproblem auf SLPs: Definition 5.2. Gegeben sei ein SLP G = (Σ, N, P ) und eine Folge [A1 , . . . , An ] von Nichtterminalen, wobei für i = 1, . . . , n : Ai ∈ N . Das Sortierproblem auf SLPs besteht darin, eine Permutation [B1 , . . . , Bn ] der Folge [A1 , . . . , An ] zu finden, sodass für 1 ≤ i ≤ n: wortG (Bi ) ≤lex wortG (Bi+1 ). Das heißt ein passendes Sortierverfahren sortiert Nichtterminale einer SLP anhand der lexikographischen Ordnung der erzeugten Worte. Eine naive Vorgehensweise wäre es, die Folge [wortG (A1 ), . . . , wortG (An )] zu berechnen und anschließend ein Sortierverfahren (für Strings) auf die Folge anzuwenden. Dieses Vorgehen ist jedoch sehr ineffizient, da die Berechnung von wortG (Ai ) unter Umständen exponentielle Zeit (in der Größe der Grammatik) in Anspruch nimmt.. 31.

(38) Für die Lösung des obigen Problems wird daher ein Algorithmus gesucht, der eine polynomielle Laufzeit in O(|G|) (vgl. Definition 3.8) und n (wenn n die Länge der Folge ist) besitzt.. 5.2. Zerlegung des Problems Ein Unterproblem beim (vergleichsbasierten) Sortieren, ist der Vergleich zweier Elemente. Für unser konkretes Problem lässt sich dieser Vergleich ähnlich zum Wortproblem (vgl. Definition 3.11) für SLPs formulieren. Definition 5.3. Das Vergleichsproblem für SLPs sei wie folgt definiert: Gegeben ein SLP G = (Σ, N, P ) und Nichtterminale A1 , A2 ∈ N : Entscheide ob wortG (A1 ) ≤lex wortG (A2 ) gilt. Es lässt sich leicht verifizieren, dass es ausreicht ein effizientes Verfahren für das Vergleichsproblem zu entwickeln, um auch das Sortierproblem effizient zu lösen: Sobald wir das Vergleichsproblem in polynomieller Zeit in O(|G|) lösen können, können wir ein beliebiges, effizientes vergleichsbasiertes Sortierverfahren verwenden (wie beispielsweise die in Kapitel 2 behandelten Quick- oder Mergesort-Algorithmen). Das so zusammengesetzte Verfahren ist dann polynomiell in O(|G|) und der Anzahl der zu sortierenden Elemente. Daher werden wir im Folgenden ausschließlich ein Algorithmus zur Lösung des Vergleichsproblems erläutern.. 5.3. Ein effizienter Algorithmus für das Vergleichsproblem auf SLPs Das Grundgerüst unseres Algorithmus für das Vergleichsproblem lässt sich zunächst wie folgt beschreiben: Algorithmus 5.4 (Lösen des Vergleichsproblems für SLPs). Sei G = (Σ, N, P ) und A1 , A2 ∈ N . Der folgende Algorithmus gibt True aus, wenn wortG (A1 ) ≤lex wortG (A2 ) und anderenfalls False. 1. Finde den längsten gemeinsamen Präfix wortG (A1 ) und wortG (A2 ), oder genauer: Finde i ∈ N0 mit prefix(wortG (A1 ), i) = prefix(wortG (A2 ), i) so dass i maximal ist. 2. Es sind drei Fälle zu betrachten: a) Wenn i = |wortG (A1 )| und i ≥ |wortG (A2 )|, dann gebe True aus.. 32.

(39) b) Wenn i < |wortG (A1 )| und |wortG (A2 )| = i, dann gebe False aus. c) Wenn i < |wortG (A1 )| und i < |wortG (A2 )|, dann sind wir im allgemeinen Fall: Berechne a1 = wortG (A1 )[i+1] und a2 = wortG (A2 )[i+1]. Wenn a1 < a2 dann gebe True aus, anderenfalls False Es lässt sich leicht verifizieren, dass dieses Verfahren das Vergleichsproblem für SLPs löst. Jedoch sind noch zwei Schritte genauer zu spezifizieren: • Das Finden des längsten gemeinsamen Präfixes in Schritt (1) und • der Vergleich an Position i + 1 in Schritt (2c). Im nächsten Abschnitt werden wir zunächst den Vergleich an Position i + 1 erläutern und im Anschluss das Finden des längsten gemeinsamen Präfixes erörtern. Beachte, dass die benötigten Wortlängen, wie in Kapitel 3.3.2 vorgeführt wurde, effizient für ein SLP berechnet werden können (vgl. Satz 3.16).. 5.4. Extrahieren des i. Symbols Algorithmus 5.5 (Berechnen des i. Symbols eines SLP-komprimierten Strings). Sei ein SLP G = (Σ, N, P ), ein Nichtterminal A ∈ N und eine Zahl i (mit i ≤ |wortG (A)|) gegeben. Die folgende (rekursive) Funktion pos(G, A, i) berechnet wortG (A)[i]: pos(G, A, i) = 1. Wenn |wortG (A)| = 1, i = 1 und A → a ∈ P , dann gebe a zurück 2. Wenn A → BC ∈ P . Sei lB = |wortG (B)|. a) Wenn i ≤ lB dann mache rekursiv weiter mit pos(G, B, i) b) Wenn i > lB , dann mache rekursiv weiter mit pos(G, C, i − lB ) Informell sucht der Algorithmus anhand der Wortlängen die richtige Stelle. Die Wortlängen können in einer Initialisierungsphase in O(|G|) mit topologischem Sortieren und dynamischem Programmieren berechnet werden (vgl. Satz 3.16). Da jede Produnktion von G maximal einmal inspiziert wird, kann die anschließende Berechnung von pos(G, A, i) ebenfalls in O(|G|) durchgeführt werden. Daher gilt: Satz 5.6. Algorithmus 5.5 hat Worst-Case Laufzeit O(|G|) für ein SLP G.. 33.

(40) 5.5. Finden des längsten gemeinsamen Präfixes Sei G = (Σ, N, P ), A1 , A2 ∈ N und l = min{(|wortG (A1 )|, |wortG (A2 )|)}. Dann muss der längste gemeinsame Präfix eine Länge kleiner gleich l haben. Die Idee zum Finden des längsten gemeinsamen Präfixes ist ähnlich zu der Intervallhalbierung bei der binären Suche: Algorithmus 5.7 (Berechnung des längsten gemeinsamen Präfixes). Sei G = (Σ, N, P ), A1 , A2 ∈ N und l = min{(|wortG (A1 )|, |wortG (A2 )|)}. Sei i die Position, für die sicher ist, dass prefix(wortG (A1 ), i − 1) und prefix(wortG (A2 ), i − 1) identisch sind. Initial sei i = 1. Zudem sei j mit j ≥ i die Positionen, für die stets gilt: prefix(wortG (A1 , j + 1)) und prefix(wortG (A2 , j + 1)) müssen wir nicht mehr vergleichen. Initial sei j = l. 1. Abbruchkriterium: • Wenn i = j ist, dann beende und – gebe i zurück falls prefix(wortG (A1 , i)) und prefix(wortG (A2 , i)) gleich sind – gebe i − 1 zurück falls prefix(wortG (A1 , i)) und prefix(wortG (A2 , i)) nicht gleich sind 2. Setze m = max{(b(i + j)/2c, 1)} 3. Prüfe ob die m-langen Präfixe der Worte wortG (A1 ), wortG (A2 ) identisch sind, d.h. ob prefix(wortG (A1 ), m) = prefix(wortG (A2 ), m) gilt. • Gilt Gleichheit, dann muss der gesuchte Präfix eine Länge zwischen m und j haben. In diesem Fall setze i := m + 1. • Sind die Präfixe nicht gleich, dann muss der gesuchte Präfix eine Länge zwischen 0 und m haben, setze j := m. Fahre mit Schritt (2) fort. Die Vorgehensweise des Algorithmus kann man sich anhand von Abbildung 1 veranschaulichen. Dabei wird der rechte Pfad genommen, wenn die Präfixe gleich sind. Ansonsten wird der linke Pfad in Erwägung gezogen. Die Anzahl der rekursiven Aufrufe (und ebenso die Anzahl der auszuführenden Gleichheitstests von Präfixen) ist von der Ordnung O(log l). Dies lässt sich weiter abschätzen durch O(log 2|G| ) = O(|G|), da bei bester Komprimierung |G| einen exponentiell großen String erzeugt. Es bleibt noch zu klären, wie der Gleichheitstest der Präfixe prefix(wortG (A1 ), m) und prefix(wortG (A2 ), m) effizient durchgeführt werden kann. Hier greifen wir auf den Algorithmus von Plandowski zurück (siehe Kapitel 3.3.1), der ein solches Wortproblem. 34.

(41) 35 Abbildung 1: Mögliche Wege um den Präfix zu finden.

(42) S11 E7. H4. D6 C4 B. a. B2. 2. 2. B. A1. A1. A1. A1. a. a. a. a. F2. A1. A1. A1. a. a. F2. G1. G1. G1. G1. b. b. b. b. Abbildung 2: Beispielbaum zu dem SLP aus Beispiel 5.8 lösen kann. Allerdings müssen wir als Eingabe für Plandowski’s Algorithmus Nichtterminale übergeben, die die m-langen Präfixe von wortG (A1 ) und wortG (A2 ) erzeugen. Unglücklicherweise existieren diese Nichtterminale nicht notwendigerweise in jeder denkbaren Grammatik G. Beispiel 5.8 (Nichtterminal nicht gefunden). Gegeben sei folgende SLP mit zugehörigen Wortlängen: S 11 E7 D6 C4 B2 A1 H4 F2 G1. −→ −→ −→ −→ −→ −→ −→ −→ −→. E7 H 4 D6 A1 C 4 B2 B2 B2 A1 A1 a 2 F F2 G1 G1 b. (1) (2) (3) (4) (5) (6) (7) (8) (9). Welches Wort w würde diese SLP erzeugen? Schauen wir uns das Ganze noch einmal mit einem Baum an. Hier wird die Verteilung der Längen klarer (vgl. Abbildung 2). Also erzeugt unsere SLP das Wort w: aaaaaaabbbb Wenn wir w[1] erreichen wollen laufen wir immer links im Baum d.h. wir gehen von S → E → D → C → B → A → a.. 36.

(43) Für w[6] wäre unser Weg S → E → D → B → A → a. Als nächstes betrachten wir den Weg genauer, der das Präfix von wort(5) erzeugt. Die folgende Wegfindung basiert auf dem Algorithmus zum Extrahieren des i. Symbols (vgl. Algorithmus 5.5). Beginnend bei S wird erkannt, dass die 6 ungleich als die 11 ist und wir noch nicht das richtige Nichtterminal gefunden haben. Also werfen wir als nächstes ein Blick auf E und H. Hier erkennen wir, dass die 6 < |wort(E)| ist, deshalb laufen wir nach links (sonst müssten wir nach rechts und 6 von 11 (11 = |wort(S)|) abziehen und dann weiter suchen). Wir befinden uns im Baum beim Nichtterminal E. Der linke Pfad wird anvisiert, da die 6 kleiner ist als die 7. Bei D angekommen können wir aufhören zu suchen, denn wir haben ein Nichtterminal erreicht, welches ein Wort der Größe 6 erzeugt. Kommen wir zum Hauptproblem. Plandowski möchte eine Grammatik und zwei Nichtterminale als Eingabe. Wir könnten ihm nun die Nichtterminale E und H geben. E erzeugt das Wort aaaaaaa und H erzeugt das Wort bbbb. Der Vergleich ist hier eindeutig, denn die Wörter sind nicht gleich. Wie schaut es aber nun aus wenn, man das Wort aaaaaaab mit bbbb vergleichen will? Das sollte eigentlich kein Problem sein, doch liegt der Mangel darin, das wir kein Nichtterminal haben, welches das Wort aaaaaaab erzeugt. Daher muss die Grammatik erweitert werden. Den passenden Algorithmus hierfür werden wir nun beschreiben. Algorithmus 5.9 (Finden und Erzeugen von Nichtterminalen). Sei ein SLP G = (Σ, N, P ), ein Nichtterminal A ∈ N und eine Zahl i (mit i ≤ |wortG (A)|) gegeben. Die folgende (rekursive) Funktion prefixNT(G, A, i) erweitert die Grammatik G zu G0 und berechnet ein Nichtterminal A0 mit wortG0 (A0 ) = prefix(wortG (A), i): prefixNT(G, A, i) = 1. Wenn |wortG (A)| = i, dann gebe (A, G) zurück 2. Wenn A → BC die Produktion zu A in G ist, dann sei lB = |wortG (B)|. a) Wenn i = lB , dann gebe (B, G) zurück. b) Wenn i < lB , dann mache rekursiv weiter mit prefixNT(G, B, i) c) Wenn i > lB , dann • Sei (C 0 , G0 ) = prefixNT(G, C, i − lB ) • Erzeuge ein neues Nichtterminal A0 und eine neue Produktion A0 → BC 0 und erweitere G0 um beide Komponenten. Sei G00 die so erweiterte Grammatik • Gebe (A0 , G00 ) zurück. 37.

(44) Wir setzen unser Beispiel von oben fort. Beispiel 5.10 (SLP um eine Produktion erweitern). Ziel ist es das Wort aaaaaaab mit bbbb zu vergleichen, dazu müssen wir ein neues Nichtterminal erzeugen, welches das Wort der Länge 8 generieren kann. Nach Algorithmus 5.9 (2c) erzeugen wir die Produktion H 0 → EG. Anschließend wird die neue Produktion der Grammatik hinzugefügt: S 11 E7 D6 C4 B2 A1 H4 F2 G1 H’8. −→ −→ −→ −→ −→ −→ −→ −→ −→ −→. E7 H 4 D 6 A1 C 4 B2 B2 B2 A1 A1 a 2 F F2 G1 G1 b 7 E G1. (1) (2) (3) (4) (5) (6) (7) (8) (9) (8). Vollständigkeitshalber folgt der Vergleich zwischen wortG (H 0 ) und wortG (H) mit dem Resultat das wortG (H 0 ) <lex wortG (H) ist. Die Laufzeit des Algorithmus lässt sich durch O(|G|) abschätzen, da jede Produktion höchstens einmal benutzt wird. Die Größe der Grammatik verdoppelt sich höchstens. Daher ergibt sich: Ein Gleichheitstest für Präfixe kann in O(|G|) + O(|G|3 ) = O(|G|3 ) Zeit durchgeführt werden, d.h. Plandowski’s Algorithmus dominiert die Laufzeit hierbei. Für Algorithmus 5.7 ergibt sich daher eine Laufzeit der Größenordnung O(|G| · |G|3 ) = O(|G|4 ). Satz 5.11. Für Algorithmus 5.7 beträgt die Laufzeit O(|G|4 ). Algorithmus 5.4 löst daher das Vergleichsproblem für SLPs ebenfalls in Zeit O(|G|4 ), da nur eine Ausführung von Algorithmus 5.5 hinzukommt, die O(|G|) Zeit benötigt. Bei Verwendung eines optimalen vergleichsbasierten Sortierverfahrens können wir daher schließen: Das Sortierproblem für SLPs mit einer SLP G und n Nichtterminalen kann in Zeit O((n · |G|4 ) log(n · |G|4 )) gelöst werden. Da |G| nicht mehr als |G| Nichtterminale besitzen kann folgt daraus O(|G|5 log |G|5 ). Wir vereinfachen und erhalten daher: Satz 5.12. Das Sortierproblem für SLPs mit einer SLP G und paarweise verschiedenen Nichtterminalen kann in Zeit O(|G|5 log |G|) gelöst werden.. 38.

(45) Teil IV. IMPLEMENTIERUNG UND TESTS Nachdem die Funktionsweise des SCFG-Sort Algorithmus betrachtet wurden, beschäftigen wir uns mit der Implementierung des SCFG-Sort Algorithmus in Haskell. Somit betrachten wir in diesem Kapitel die Ein- und Ausgabe in Haskell, geben Codeausschnitte an und testen anschließend das Ganze. Beim Testen werden wir erneut die Vor- und Nachteile der Sortiermethode von Quicksort und Mergesort beleuchten und hoffentlich auch die im Kapitel 2 besprochenen Eigenschaften wiedererkennen.. 39.

(46) 6. Repräsentation des SCFG-Sort-Algorithmus in Haskell In diesem Kapitel werden wir einige Codeauszüge aus dem SCFG-Sort-Modul betrachten und somit ein kleines Verständnis über die Implementierung des Quelcodes gewinnen. Zunächst sollten wir verstehen, wie die Grammatiken in Haskell implementiert werden.. 6.1. Benutzerschnittstellen Es existieren zwei Möglichkeiten dem SCFG-Sort-Programm die Eingabe zu übergeben. Entweder man gibt die Grammatik direkt als eine große Grammatik ein, wobei die Nichtterminale aus Integern bestehen oder man gibt eine Liste von Grammatiken ein. Im zweiten Fall werden alle Nichtterminale der Grammatiken umbenannt, d.h. sie werden eindeutig von den anderen unterscheidbar gemacht und danach zu einer großen Grammatik zusammengefasst. Anschließend wird die Grammatik zum Sortieren der dazugehörigen Nichtterminale verwendet. Der Hauptaufruf erfolgt mit dem Aufruf der Funktion main. main :: IO () main = mainQuicksortSlpC grammarAsChar :: [SCFG [Char] [Char]] grammarAsChar = [grammatikAlsMap44,grammatikAlsMap33,grammatikAlsMap22, grammatikAlsMapBsp,grammatikAlsMapS,grammatikAlsMapI, grammatikAlsMapA,grammatikAlsMap1] mainQuicksortSlpC :: [Integer] mainQuicksortSlpC = let scfg = Map.unions (prepareToRename grammarAsChar 0) in quicksortSlp (wordLength scfg) (Map.keys scfg). Unsere Eingabegrammatiken sind alle in der Litse grammarAsChar gespeichert. Der eigentliche Aufruf erfolgt mit mainQuicksortSlpC. Wie oben besprochen, erfolgt hier der Teil des Umbenennens und des Vereinigens der Grammatiken. Natürlich wird auch quicksortSlp aufgerufen. Die Nichtterminale werden alle mittels (Map.keys scfg) erzeugt und der Funktion quicksortSlp übergeben. Einen Überblick über die Aufrufhierarchie des SLP-Sort-Algorithmus ist in Abbildung 3 gegeben. Das Ergebnis ist eine Liste der sortierten Nichtterminale. Man erkennt nun nicht unbedingt anhand dieser Liste, dass die Sortierung erfolgreich war, deshalb existiert am Ende des Quellcodes noch eine kleine Hilfsfunktion, die alle Wörter in der Reihenfolge der sortierten Nichtterminale ausgibt. Danach kann man mit dem Auge überprüfen, ob richtig sortiert wurde.. 40.

(47) Abbildung 3: Aufrufhierarchie des SLP-Sort-Algorithmus. 41.

(48) 6.2. Datentypen des SLPs Am Anfang des Codes findet man ein paar Beispielgrammatiken. Um diese Grammatiken zu erzeugen, verwende man die Bibliothek GBC [gbc13]. Zuerst gibt man ein paar Produktionen an und setzt diese dann in eine Liste. Anschließend gibt es den Befehl listToSCFG, welcher aus den Listen die Grammatik erzeugt. -- S -> FC beispiel_ProduktionA beispiel_ProduktionA -- F -> AA beispiel_ProduktionB -- A -> a beispiel_ProduktionC -- C -> c beispiel_ProduktionD. :: Prod a [Char] = Production (N 0 "S") [(N 0 "F"), (N 0 "C")] = Production (N 0 "F") [(N 0 "A"), (N 0 "A")] = Production (N 0 "A") [(T "a")] = Production (N 0 "C") [(T "c")]. grammatikAlsListeA = [beispiel_ProduktionA,beispiel_ProduktionB, beispiel_ProduktionC,beispiel_ProduktionD] grammatikAlsMapA = listToSCFG grammatikAlsListeA. Die intern dargestellte Grammatik heißt grammatikAlsMapA. Der Datentyp der Nichtterminale/Terminale und der Produktion ist in der Bibliothek GBC zu finden und sieht folgendermaßen aus: data Symbol a b = T a | N Integer b deriving(Show,Read) data Prod a b = Production (Symbol a b) [Symbol a b] deriving (Eq,Ord,Show,Read). Das T a“ steht für das Terminal und das N Integer b“ steht für das Nichtterminal. ” ” Nun wird der Datentyp Symbol“ in den Datentyp Prod“ eingepflanzt und wir können ” ” Produktionen erzeugen. Der Datentyp für die Grammatik ist dann die Liste von Produktionen oder die Map von Produktionen.. 42.

(49) 6.3. Hilfreiche Funktionen aus dem Modul GBC In der Bibliothek GBC gibt es einige hilfreiche Funktionen, worin sich auch der Algorithmus von Plandowski befindet. Wir betrachten kurz die Funktionen, die sich nützlich erwiesen haben. Die Funktion printSCFG präsentiert die Grammatik und die Funktion val erzeugt das Wort zum gegebenen Nichtterminal. *Main> printSCFG grammatikAlsMapA "A" ::= "a" "C" ::= "c" "F" ::= "A" "A" "S" ::= "F" "C" *Main> val grammatikAlsMapA (N 0 "S2") ["a","a","c"]. Auch gibt es eine Funktion, um die Wortlängen zu erzeugen, so wie wir es in Kapitel 3.3.2 kennengelernt haben. *Main> wordLength grammatikAlsMapA [Production (N 1 "A") [T "a"],Production (N 2 "F") [N 1 "A" (N 1 "C") [T "c"],Production (N 3 "S2") [N 2 "F",N 1 "C"]]. Wir sehen, dass wir eine Liste von Produktionen mit den dazugehörigen Längen erhalten haben.. 6.4. Implementierung der Hauptalgorithmen des SLP-Sorts Wir betrachten hier die Implementierungen zu den Algorithmen, die in Kapitel 5 besprochen wurden. 6.4.1. Die Quicksort Implementierung Zuerst betrachten wir noch einmal den an das SLP angepasste Quicksort Algorithmus: quicksortSlp :: (Enum a, Num a, Ord a1, Ord a, Show a) => SCFGList a1 a -> [a] -> [a] quicksortSlp _ [] = [] quicksortSlp scfg (nt:rest_nt) = (quicksortSlp scfg lT_ore_EQ) ++ [nt] ++ (quicksortSlp scfg gT ) where lT_ore_EQ = [nt’ | nt’ <- rest_nt, (binsearchPlandowski scfg nt’ nt fv) < GT ] gT = [nt’ | nt’ <- rest_nt, (binsearchPlandowski scfg nt’ nt fv) == GT] fv = let -- hier werden die freshnames erzeugt (k,a) = Map.findMax (listToSCFG scfg) in [k+1..]. 43.

Referenzen

ÄHNLICHE DOKUMENTE

Black-Box-Tests sind Methoden zum Testen von Software, bei denen keine Kenntnisse ¨uber den inneren Aufbau des Pr¨uflings benutzt werden, wie z.B.. Die Betrachtung er- folgt nur

Zusammenfassend kann erkannt werden, dass eine Transformation, beziehungsweise auch eine Standardreduktion, durch folgenden Typ repräsentiert

Dort gibt es ausführliche Musterlösungen, Proben, Lernzielkontrollen, Schulaufgaben und Klassenarbeiten für alle Schularten, Klassen und Fächer, passend zum aktuellen

Dort gibt es ausführliche Musterlösungen, Proben, Lernzielkontrollen, Schulaufgaben und Klassenarbeiten für alle Schularten, Klassen und Fächer, passend zum aktuellen

• Wenn die hier entwickelten Methoden zur Analyse der lokal-dichteoptimierten Strukturen der Dreiecke erster Generation in einer amorphen bin¨ aren 2D Kolloidmischung mit repulsi-

Mit dem Winkel y und einem Mittelwert für den Krümmungshalbmesser kann man eine gute Annäherung f ür die Länge dieses Schnittes zwischen A und B finden, wenn y

Karlsruher Institut f¨ ur Technologie Institut f¨ ur Theorie der Kondensierten Materie Ubungen zur Klassischen Theoretischen Physik I ¨ WS

(c) Begr¨ unde ¨ uber das Wachs- tumsverhalten, warum die beiden Kurven nebst W noch einen weiteren ge- meinsamen Punkte auf- weisen m¨ ussen!. In der unteren Figur sind