6 Algorithmenanalyse und Grenzen der Berechenbarkeit 6.1 Sortieralgorithmen Gegeben ist im Folgenden ein 20-spaltiges Feld mit natürlichen Zahlen als Einträgen. Bsp: 55 7 23 14 2 5 4 27 15 0 8 1 36 3 18 45 9 10 19 6 Diese Zahlen sollen nun vom Computer beginnend mit der kleinsten Zahl der Größe nach geordnet werden. Doch zunächst bringen Sie bitte in möglichst kurzer Zeit alle Zahlen in die richtige Reihenfolge. Diese relativ "einfache" Aufgabe kann auf ganz verschiedene Arten gelöst werden. Wir unterscheiden im Folgenden verschiedene Sortierverfahren, die sich hinsichtlich ihrer Arbeitsweise, Effektivität und Komplexität unterscheiden. Sortieren durch direktes Auswählen Sortieren durch direktes Einfügen Sortieren mit Bubble-Sort Sortieren mit Quick-Sort Sortieren mit Merge-Sort 6.1.1 Sortieren durch direktes Auswählen („selection sort“) Dieses Sortierverfahren ist recht nahe liegend. An der ersten Position muss die kleinste Zahl stehen. Also wird alles durchsucht, bis die kleinste Zahl gefunden wurde. Diese wird mit der Zahl an der ersten Position vertauscht. Nun wird zur nächsten Position vorgegangen und diese wird zur ersten Position des restlichen Feldes. Bsp.: 16 7 12 14 2 11 4 13 15 0 8 1 17 3 18 5 9 10 19 6 Die kleinste Zahl ist die 0, also werden 0 und 16 miteinander vertauscht. Nun ist die kleinste Zahl die 1, also werden 1 und 7 miteinander vertauscht. Dieses Verfahren wiederholt sich, bis das ganze Feld abgearbeitet wurde, dann ist es zwangsläufig sortiert. 16 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 7 12 14 2 11 4 13 15 0 8 1 17 3 18 5 9 10 19 6 7 12 14 2 11 4 13 15 16 8 1 17 3 18 5 9 10 19 6 1 12 14 2 11 4 13 15 16 8 7 17 3 18 5 9 10 19 6 1 2 14 12 11 4 13 15 16 8 7 17 3 18 5 9 10 19 6 1 2 3 12 11 4 13 15 16 8 7 17 14 18 5 9 10 19 6 1 2 3 4 11 12 13 15 16 8 7 17 14 18 5 9 10 19 6 1 2 3 4 5 12 13 15 16 8 7 17 14 18 11 9 10 19 6 1 2 3 4 5 6 13 15 16 8 7 17 14 18 11 9 10 19 12 1 2 3 4 5 6 7 15 16 8 13 17 14 18 11 9 10 19 12 1 2 3 4 5 6 7 8 16 15 13 17 14 18 11 9 10 19 12 1 2 3 4 5 6 7 8 9 15 13 17 14 18 11 16 10 19 12 1 2 3 4 5 6 7 8 9 10 13 17 14 18 11 16 15 19 12 1 2 3 4 5 6 7 8 9 10 11 17 14 18 13 16 15 19 12 1 2 3 4 5 6 7 8 9 10 11 12 14 18 13 16 15 19 17 1 2 3 4 5 6 7 8 9 10 11 12 13 18 14 16 15 19 17 1 2 3 4 5 6 7 8 9 10 11 12 13 14 18 16 15 19 17 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 18 19 17 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 18 19 17 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 19 18 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Arbeitsauftrag: Programmieren Sie das Sortierverfahren durch Auswahl. Dabei soll jede Zwischenzeile in eine Listbox oder Memo ausgegeben werden (eigene Prozedur). Gestalten Sie dabei die Formularoberfläche so, dass Sie später noch andere Sortierverfahren ergänzen können. 6.1.2 Sortieren durch direktes Einfügen („insertion sort“) Gegeben ist wieder eine unsortierte Liste: 3 8 10 19 16 1 0 12 13 18 7 9 5 2 6 14 11 17 15 4 Nun geht man die Liste von Anfang bis Ende durch. Irgendwann findet man ein Element, dass hinter einer größeren Zahl steht, in obigen Beispiel die 16. Also nimmt man die 16 heraus, schiebt alle größeren Zahlen (hier nur die 19) nach hinten und fügt die herausgenommene 16 wieder ein. Ausführlich: 3 8 10 19 16 1 0 12 13 18 3 8 10 19 1 0 12 13 18 3 8 10 19 1 0 12 13 18 16 Die neue Liste sieht so aus: 3 8 10 16 19 1 0 12 13 18 7 9 5 2 6 14 11 17 15 4 7 9 5 2 6 14 11 17 15 4 7 9 5 2 6 14 11 17 15 4 7 9 5 2 6 14 11 17 15 4 Nun wiederholt man den Vorgang. Die erste Zahl, die vor sich größere stehen hat ist die 1. 3 8 10 16 19 1 0 12 13 18 7 9 5 2 6 14 11 17 15 4 3 8 10 16 19 0 12 13 18 7 9 5 2 6 14 11 17 15 4 3 8 10 16 19 0 12 13 18 7 9 5 2 6 14 11 17 15 4 1 Der ganze Sortierungsprozess sieht dann in unserem Beispiel so aus: 3 3 1 0 0 0 0 0 0 0 0 0 0 0 0 0 8 10 19 16 1 0 12 13 18 7 9 5 2 6 14 11 17 8 10 16 19 1 0 12 13 18 7 9 5 2 6 14 11 17 3 8 10 16 19 0 12 13 18 7 9 5 2 6 14 11 17 1 3 8 10 16 19 12 13 18 7 9 5 2 6 14 11 17 1 3 8 10 12 16 19 13 18 7 9 5 2 6 14 11 17 1 3 8 10 12 13 16 19 18 7 9 5 2 6 14 11 17 1 3 8 10 12 13 16 18 19 7 9 5 2 6 14 11 17 1 3 7 8 10 12 13 16 18 19 9 5 2 6 14 11 17 1 3 7 8 9 10 12 13 16 18 19 5 2 6 14 11 17 1 3 5 7 8 9 10 12 13 16 18 19 2 6 14 11 17 1 2 3 5 7 8 9 10 12 13 16 18 19 6 14 11 17 1 2 3 5 6 7 8 9 10 12 13 16 18 19 14 11 17 1 2 3 5 6 7 8 9 10 12 13 14 16 18 19 11 17 1 2 3 5 6 7 8 9 10 11 12 13 14 16 18 19 17 1 2 3 5 6 7 8 9 10 11 12 13 14 16 17 18 19 1 2 3 5 6 7 8 9 10 11 12 13 14 15 16 17 18 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 19 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Arbeitsauftrag: Programmieren Sie das Sortierverfahren durch Auswahl. 6.1.3 Sortieren durch Vertauschen (Bubble-Sort) Das einfachste Sortierverfahren überhaupt ist Bubble-Sort: Steht eine größere Zahl vor einer kleineren Zahl, so werden die beiden Zahlen miteinander vertauscht. Dies wird solange wiederholt, bis bei einem Durchlauf durch alle Zahlen keine Vertauschung mehr erforderlich war. 19 11 18 17 5 1 9 10 6 8 2 16 15 12 4 7 3 14 13 0 11 18 17 5 1 9 10 6 8 2 16 15 12 4 7 3 14 13 0 19 11 17 5 1 9 10 6 8 2 16 15 12 4 7 3 14 13 0 18 19 11 5 1 9 10 6 8 2 16 15 12 4 7 3 14 13 0 17 18 19 5 1 9 10 6 8 2 11 15 12 4 7 3 14 13 0 16 17 18 19 1 5 9 6 8 2 10 11 12 4 7 3 14 13 0 15 16 17 18 19 1 5 6 8 2 9 10 11 4 7 3 12 13 0 14 15 16 17 18 19 1 5 6 2 8 9 10 4 7 3 11 12 0 13 14 15 16 17 18 19 1 5 2 6 8 9 4 7 3 10 11 0 12 13 14 15 16 17 18 19 1 2 5 6 8 4 7 3 9 10 0 11 12 13 14 15 16 17 18 19 1 2 5 6 4 7 3 8 9 0 10 11 12 13 14 15 16 17 18 19 1 2 5 4 6 3 7 8 0 9 10 11 12 13 14 15 16 17 18 19 1 2 4 5 3 6 7 0 8 9 10 11 12 13 14 15 16 17 18 19 1 2 4 3 5 6 0 7 8 9 10 11 12 13 14 15 16 17 18 19 1 2 3 4 5 0 6 7 8 9 10 11 12 13 14 15 16 17 18 19 1 2 3 4 0 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 1 2 3 0 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 1 2 0 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 1 0 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Die Zahl an der ersten Stelle, die 19, wird solange mit ihrem Nachfolger vertauscht, solange dieser Nachfolger kleiner ist. Damit rutscht die 19 ans Ende, alle anderen Zahlen dabei um eine Position nach vorne. In der zweiten Zeile bleibt die 11 vorne stehen, da ihr Nachfolger größer ist. Dieser, die 18 rutscht dann wieder bis nach hinten auf die vorletzte Position. Dort wird nicht mehr vertauscht, da ja schon die größte Zahl am Ende steht. In der dritten Zeile geschieht Vergleichbares mit der 17. In der vierten Zeile wird zunächst die 11 bis vor die 16 geschoben, dort bleibt sie hängen. Dafür wird von dort die 16 bis auf ihre endgültige Position Arbeitsauftrag: Programmieren Sie das Sortierverfahren durch Vertauschen. 6.1.4 Sortieren durch Quicksort Das Grundprinzip dieses Algorithmus ist "Teile und Herrsche". Die Datei wird in zwei Teile zerlegt, die dann unabhängig voneinander sortiert werden. Für die beiden Teile wird dieses Verfahren erneut angewandt, es ist also rekursiv. 12 12 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 13 11 17 16 2 13 11 1 3 2 6 8 1 3 2 3 1 4 6 1 3 2 4 6 1 3 2 4 6 1 2 3 4 6 1 2 3 4 6 1 2 3 4 6 1 2 3 4 6 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 7 10 7 10 7 5 7 5 7 5 7 5 7 5 7 5 7 5 7 5 7 6 7 6 6 7 6 7 6 7 6 7 6 7 6 7 6 7 6 7 6 7 6 7 6 7 6 7 6 7 6 7 6 7 4 4 4 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 5 5 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 19 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 10 10 10 10 10 10 10 10 10 10 10 8 8 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 6 6 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 0 0 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 14 14 14 14 14 14 14 14 14 14 14 18 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 17 16 16 16 3 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 1 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 15 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 Hier wird die 15 als das zerlegende Element gewählt. Nun wird von links her die erste Zahl gesucht, die größer ist, dies ist die 17. Von rechts her gesucht ist die 1 die erste kleinere Zahl. Die 17 und die 1 werden nun miteinander vertauscht, denn wenn 15 das zerlegende Element ist, dürfen links davon nur kleinere, rechts davon nur größere Zahlen stehen. Dieser Vorgang wird wiederholt, dabei werden die 16 und die 3 miteinander vertauscht, im nächsten Durchgang die 19 und die 14. Die Suche von links nach größeren Zahlen als der 15 endet nun bei der 18, dort steht aber auch gerade die Suche von rechts. Daher werden die 18 und die 15 miteinander vertauscht. Nun kommt die Rekursion ins Spiel. Dasselbe Verfahren wie oben beschrieben wird nun erneut angewandt, zunächst auf den linken Teil, dann auf den rechten Teil. In der zweiten Zeile wird nun die 9 als das zerlegende Element gewählt. Bei der Wiederholung des oben beschriebenen Vorganges werden die 12 und die 0, die 13 und die 6, die 11 und die 8 miteinander vertauscht, schließlich noch die 10 und die 5. Im letzten Schritt werden dann 10 und 9 miteinander vertauscht. Nun wieder Rekursion: Dasselbe Verfahren wird nun wieder zuerst auf den linken Teil, dann auf den rechten Teil angewandt. Dies geschieht solange, bis alles sortiert ist. Programmieren Sie dieses Verfahren! 6.1.5 Sortieren mit Merge-Sort Mergesort (engl. merge für verschmelzen und sort für sortieren) ist ein stabiler Sortieralgorithmus. Er arbeitet nach dem Prinzip Teile und herrsche. Der Algorithmus kann sowohl rekursiv als auch iterativ realisiert werden.[1] Er wurde erstmals 1945 durch John von Neumann vorgestellt. Mergesort betrachtet die zu sortierenden Daten als Liste und zerlegt sie in kleinere Listen, die jede für sich sortiert werden. Die sortierten kleinen Listen werden dann im Reißverschlussverfahren zu größeren Listen zusammengefügt (engl.: (to) merge), bis wieder eine sortierte Gesamtliste erreicht ist. Das Verfahren arbeitet bei Arrays in der Regel nicht inplace, es sind dafür aber (trickreiche) Implementierungen bekannt. Verkettete Listen sind besonders geeignet zur Implementierung von Mergesort, dabei ergibt sich die in-placeSortierung fast von selbst. Das Bild veranschaulicht die drei wesentlichen Schritte eines Teile-und-herrsche-Verfahrens, wie sie im Rahmen von Mergesort umgesetzt werden. Der Teile-Schritt ist ersichtlich trivial (die Daten werden einfach in zwei Hälften aufgeteilt). Die wesentliche Arbeit wird beim Verschmelzen (merge) geleistet - daher rührt auch der Name des Algorithmus. Bei Quicksort ist hingegen der Teile-Schritt aufwendig und der Merge-Schritt einfacher (nämlich eine Konkatenierung). Bei der Betrachtung des in der Grafik dargestellten Verfahrens sollte man sich allerdings bewusst machen, dass es sich hier nur um eine von mehreren Rekursionsebenen handelt. So könnte etwa die Sortierfunktion, welche die beiden Teile 1 und 2 sortieren soll, zu dem Ergebnis kommen, dass diese Teile immer noch zu groß für die Sortierung sind. Beide Teile würden dann wiederum aufgeteilt und der Sortierfunktion rekursiv übergeben, so dass eine weitere Rekursionsebene geöffnet wird, welche dieselben Schritte abarbeitet. Im Extremfall (der bei Mergesort sogar der Regelfall ist) wird das Aufteilen so weit fortgesetzt, bis die beiden Teile nur noch aus einzelnen Datenelementen bestehen. Die folgende Grafik illustriert die Arbeitsweise des Algorithmus: unit UMergeSort; interface procedure MergeSort(var A: array of Integer); Mergesort ist im Gegensatz zu Quicksort stabil, wenn der Merge-Schritt richtig implementiert ist. Seine Komplexität (Worst-, Best- und Average-Case-Verhalten) beträgt in der LandauNotation ausgedrückt stets Θ(nlog(n)). Damit ist Mergesort hinsichtlich der Komplexität dem Quicksort gewissermaßen überlegen, da der Quicksort (ohne besondere Vorkehrungen) ein Worst-Case-Verhalten von Θ(n2) besitzt. Die Wahrscheinlichkeit, dass dieser Fall auftritt, ist jedoch so gering, dass Quicksort in der Praxis bessere Ergebnisse erzielt. Implemetieren Sie Merge-Sort! 6.2 Algorithmenanalyse ZUVERLÄSSIGKEIT VON ALGORITHMEN Den Bubble hab´ ich im Kopf, Quicksort schreibe ich ab. (Bill Gates) Wichtigste Forderung an ein Programm ist, dass es zuverlässig ist, d.h. die gewünschte Leistung tatsächlich erbringt. Häufig liefert aber Software, die seit Jahren ( scheinbar ) zuverlässig arbeitet, plötzlich fehlerhafte Resultate. Beispiel 1. Abrechnungspanne der Deutschen Telekom am 1.1.1996 : Durch ein fehlerhaftes Programm wurde fast allen Kunden bei der Umstellung der Tarifstruktur eine zu hohe Abrechnung erstellt, obwohl vorher umfangreiche Probeläufe absolviert worden waren. 2. Startprogramm der BOEING 737 : unerklärliche Startabbrüche bei v = 60 Knoten (Ursache : im Programm waren nur die Fälle v < 60 Knoten bzw. v > 60 Knoten berücksichtigt. Bei Übermittlung von v = 60 Knoten durch die Sensoren erkannte das Programm einen undefinierten Zustand und brach daher ab ) 3. Jahr-2000-Problem ( Ursache : Um den damals sehr teuren Speicherplatz zu sparen, wurde die Jahreszahl in den Datumsangaben anfänglich meist nur mit zwei Ziffern dargestellt. Daher kann ein solches Programm nicht zwischen 1900 und 2000 unterscheiden. Vergleiche von Datumsangaben führen auf falsche Resultate ) Je komplexer ein Programm ist, um so sicherer enthält es Fehler. ( Schätzung : ≥ 1 Fehler je 10 KByte Programmtext ) Definition Der Nachweis, dass ein Informatiksystem zuverlässig arbeitet, heißt Validierung. Bemerkung In der Regel handelt es sich nicht um einen exakten Beweis, sondern um eine empirische Bestätigung auf Widerruf. Bei den Prüfmethoden unterscheidet man formale Beweise ( Verifikation ), Tests und weitere Methoden (z.B. Inspektionen ). Bei der formalen Methode ( Verifikation ) wird die Einhaltung der Bedingungen an die Zwischen- und Endergebnisse des Algorithmus bzw. Programms mittels logischer Herleitungen nachgewiesen. Bei der empirischen Methode ( Testen ) wird der Algorithmus bzw. das Programm mit bestimmten ausgesuchten Daten erprobt, zu denen die Ergebnisse bekannt sind. Dabei kann lediglich das Vorhandensein von Fehlern entdeckt, aber nicht die Fehlerfreiheit nachgewiesen werden. Definition Ein Algorithmus heißt korrekt, wenn er seiner Spezifikation genügt, d.h. wenn er zu allen Eingabedaten, die einer bestimmten Vorbedingung genügen, diejenigen Ausgabedaten erzeugt, welche eine bestimmte Nachbedingung erfüllen. Die Überprüfung erfolgt hauptsächlich durch die beiden o.g. Verfahren. EFFIZIENZ VON ALGORITHMEN Definition Zwei Algorithmen heißen äquivalent, wenn sie gleichen Eingabedaten gleiche Ausgabedaten zuordnen, also derselben Spezifikation genügen. Von zwei äquivalenten Algorithmen heißt derjenige effizienter, der mit weniger Laufzeit und / oder weniger Speicherplatz auskommt. Bemerkung Man kann einen Algorithmus nicht in beiderlei Hinsicht beliebig verbessern. Ab einer gewissen Schwelle wird die Beschleunigung eines Algorithmus durch erhöhten Speicherbedarf erkauft ( Claus VOLCKER : Informations-Unschärferelation ) EINFACHE MAßNAHMEN ZUR STEIGERUNG DER EFFIZIENZ - Vermeiden unnötiger Berechnungen (Beispiel: Mehrfache Berechnungen einer Größe bei verschiedenen Schleifendurchläufen ) Ausnutzen mathematischer Eigenschaften Neue Lösungsidee bei gleicher Datenstruktur Änderung der Datenstruktur GRÖSSENORDNUNG Bezeichnung Ein Problem der Größe n ist ein Spezialfall eines Problems, bei dem die Eingabedaten aus n Elementen bestehen. Die Zahl O( n ) der Rechenschritte, die ein Algorithmus zur Lösung eines Problems der Größe n benötigt, heißt Zeitkomplexität des Algorithmus. Zur Ermittlung der Zeitkomplexität nimmt man folgende Vereinfachungen vor : 1. Es wird nur die dominante Operation betrachtet, von der die Laufzeit wesentlich abhängt ( z.B. eine zentrale Schleife ). 2. Man betrachtet Spezialfälle : günstigster ( + ) / ungünstigster ( - ) / mittlerer ( o ) Fall. 3. Durch Weglassen additiver oder multiplikativer Konstanten geht man zur Größenordnung der Laufzeit O( n ) über. Wir analysieren unsere bisher erarbeiteten Sortieralgorithmen hinsichtlich ihres Zeitbedarfs und ihres Speicherplatzbedarfs. http://www.matheprisma.de/Module/Sortieren/Vergleich.htm Sortierverfahren Zeitkomplexität (worst case) Zeitkomplexität (average case) Zeitkomplexität (best case) stabil inplace Selection O(n2) O(n2) O(n2) nein ja Insertion O(n2) O(n2) O(n) ja ja Bubble O(n2) O(n2) O(n) ja ja Quick O(n2) O(n*log(n)) O(n*log(n)) nein ja Merge O(n*log(n)) O(n*log(n)) O(n*log(n)) ja nein Auschlaggebend für die Zeitkomplexität ist stets der Aufbau der zu sortierenden Datenstruktur. So sind Insertion – und Bubblesort sogar linear schnell, wenn eine bereits sortierte Liste vorliegt. Dagegen verlangsamt sich Quicksort auf quadratische Größenordnung, wenn sich eine ungünstige Wahl des Pivotelementes (z.B. größtes oder kleinstes Element durch die Anordnung der Daten ergibt. Die Teilliste besteht dann stets nur aus einem Element, wodurch sich keine logarithmische Komplexität mehr erreichen lässt. Stabil ist ein Verfahren dann, wenn es die Reihenfolge von Datensätzen mit gleichem Schlüsselwert nicht verändert. Wenn beispielsweise eine Liste alphabetisch sortierter Personendateien nach dem Geburtsdatum neu sortiert wird, dann bleiben unter einem stabilen Sortierverfahren alle Personen mit gleichem Geburtsdatum alphabetisch sortiert. Bubblesort kann somit durchaus eine sinnvolle Wahl darstellen, wenn zum Beispiel wenige neue Daten in einen bereits sortierten Bestand eingefügt werden müssen. Hier besteht sein Vorteil – und damit seine Geschwindigkeitsgewinn – darin, dass andere Verfahren, z.B. Quicksort, die bereits sortierten Daten wieder völlig durcheinanderwirbeln, während die neuen Daten bei Bubblesort einfach an ihren Platz innerhalb des Bestands „blubbern“ können, ohne diesen neu zu strukturieren. Merke: Die Komplexität des besten Sortierverfahrens ist von der Größenordnung n*log(n). Es gibt keinen Algorithmus, der weniger Vergleiche benötigt! Anmerkung: In der Informatik meint “log” stets den Logarithmus zur Basis 2. Mathematische Beweise: vgl. z.B.: http://de.wikipedia.org/wiki/Sortierverfahren 6.3 Grenzen der Berechenbarkeit 6.3.1 Praktische Grenzen der Berechenbarkeit 6.3.1.1 Algorithmen mit exponentieller Laufzeit 6.3.1.1.1 Das Rundreiseproblem Das Problem des Handlungsreisenden (auch Rundreiseproblem, engl. Traveling Salesman Problem oder Traveling Salesperson Problem kurz TSP) ist ein kombinatorisches Optimierungsproblem des Operations Research und der theoretischen Informatik. Die Aufgabe besteht darin, eine Reihenfolge für den Besuch mehrerer Orte so zu wählen, dass die gesamte Reisestrecke des Handlungsreisenden nach der Rückkehr zum Ausgangsort möglichst kurz ist. Seit seiner ersten Erwähnung als mathematisches Problem im Jahre 1930 haben sich viele Forscher damit befasst und neue Optimierungsverfahren daran entwickelt und erprobt, die momentan auch für andere Optimierungsprobleme eingesetzt werden. Heute steht eine Vielzahl von heuristischen und exakten Methoden zur Verfügung, mit denen auch schwierige Fälle mit mehreren tausend Städten optimal gelöst wurden. Das Problem des Handlungsreisenden tritt schon in seiner Reinform in vielen praktischen Anwendungen auf, beispielsweise in der Tourenplanung, in der Logistik oder im Design von Mikrochips. Noch häufiger tritt es allerdings als Unterproblem auf, wie zum Beispiel bei der Verteilung von Waren, bei der Planung von Touren eines Kunden- oder Pannendienstes oder bei der Genom-Sequenzierung. Dabei sind die Begriffe „Stadt“ und „Entfernung“ nicht wörtlich zu nehmen, vielmehr repräsentieren die Städte beispielsweise zu besuchende Kunden, Bohrlöcher oder DNA-Teilstränge, während Entfernung für Reisezeit, Kosten oder den Grad der Übereinstimmung zweier DNA-Stränge steht. In vielen praktischen Anwendungen müssen zudem Zusatzbedingungen wie Zeitfenster oder eingeschränkte Ressourcen beachtet werden, was die Lösung des Problems erheblich erschwert. Komplexitätstheoretisch gehört das TSP zur Klasse der NP-äquivalenten (nicht in polynomialer Zeitkomplexität lösbar) Probleme. Es wird daher sehr stark angenommen, dass die Worst-case-Laufzeit jedes deterministischen Algorithmus, der für dieses Problem stets optimale Lösungen liefert, im besten Fall exponentiell von der Anzahl der Städte abhängt. Schon für wenige Städte kann die benötigte Laufzeit eines solchen Algorithmus also unpraktikabel viel Zeit beanspruchen. Das Problem des Handlungsreisenden kann exakt gelöst werden, indem man die Weglängen aller möglichen Rundreisen berechnet und dann eine mit der kleinsten Weglänge auswählt. Das ist aber schon bei einer kleinen Zahl von Städten nicht mehr praktisch durchführbar. Bei der einfachsten Variante, dem symmetrischen TSP mit n Städten, gibt es (n − 1)! / 2 verschiedene Rundreisen, das sind bei 15 Städten über 43 Milliarden und bei 18 Städten bereits über 177 Billionen. Wie schnell die Rechenzeit mit wachsender Anzahl von Städten wächst, zeigt das folgende Beispiel: hat man einen Rechner, der die Lösung für 30 Städte in einer Stunde berechnet, dann braucht dieser für zwei zusätzliche Städte annähernd die tausendfache Zeit; das sind mehr als 40 Tage. Dem intuitiven Vorgehen eines Handlungsreisenden entspricht wohl am ehesten die NearestNeighbor-Heuristik (nächster Nachbar). Von einer Stadt ausgehend wählt diese jeweils die nächstgelegene als folgenden Ort aus. Dieses wird sukzessive fortgesetzt, bis alle Städte bereist wurden und der Handlungsreisende zum Ausgangsort zurückgekehrt ist. Die hiermit verwandte Farthest-Neighbor-Heuristik besucht in jedem Schritt die am entferntesten gelegene Stadt. In jeder Stadt muss also der kürzeste bzw. weiteste ausgehende Weg gesucht werden. Maximal kann es pro Stadt nur so viele ausgehende Kanten geben, wie Knoten im Graphen vorhanden sind. Daraus ergibt sich eine algorithmische Komplexität von O(n²), die Anzahl der Rechenschritte hängt also quadratisch von der Zahl der Städte ab. Dass diese Heuristik im Allgemeinen jedoch nicht die beste Lösung liefert, liegt daran, dass die Distanz zwischen der Ausgangsstadt und der letzten besuchten Stadt bis zuletzt nicht berücksichtigt wird. Die Nearest- und die Farthest-Neighbor-Heuristik können beliebig schlechte Ergebnisse liefern, das heißt, es gibt keinen konstanten, instanzunabhängigen Approximationsfaktor für den Lösungswert im Vergleich zum Optimalwert. Wissenschaftler des Queen Mary and Westfield College der Universität London fanden heraus, dass Hummeln bei der Nahrungssuche in der Lage sind, das Problem des Handlungsreisenden besser und effizienter zu lösen, als dies mit den bislang bekannten mathematischen Computerverfahren möglich ist. Wie Hummeln diese Ergebnisse erzielen können, ist dabei noch nicht geklärt. 6.3.2 Theoretische Grenzen der Berechenbarkeit Lange Zeit haben Philosophen und Mathematiker geglaubt, dass jedes mathematische Problem algorithmisch lösbar sei. So vertrat z.B. David Hilbert (1862- 1943) die Auffassung, “dass ein jedes bestimmte mathematische Problem einer strengen Erledigung notwendig fähig sein müsse, sei es, dass es gelingt, die Beantwortung der gestellten Frage zu geben, sei es, dass die Unmöglichkeit seiner Lösung und damit die Notwendigkeit des Misslingens aller Versuche dargetan wird.“ Erst in unserem Jahrhundert erkannte man, dass nicht alle Probleme, die sich computergerecht spezifizieren lassen, auch algorithmisch lösbar sind. Ernüchternd war vor allem die Erkenntnis, dass die Lösung dieser Probleme nicht an den technischen Unzulänglichkeiten der verfügbaren Geräte scheitert, sondern dass dem menschlichen Denken selbst offenbar Grenzen gesetzt sind Beispiel: Das Goldbach-Problem (C. Goldbach, 1742) Eines der bekanntesten bis heute ungelösten Probleme der Mathematik ist die sogenannte Goldbachsche Vermutung: Jede gerade Zahl >= 4 lässt sich als Summe zweier Primzahlen darstellen. 4=2+2 6=3+3 8=5+3 10 = 7 + 3 Bezeichnung: Eine gerade Zahl n >= 4 hat die Goldbach-Eigenschaft, wenn es eine Zahl k < n gibt mit : k und n-k sind Primzahlen Für die Informatik reduzieren wir das Goldbach-Problem auf die Fragestellung: Gibt es einen Algorithmus, der für eine gegebene gerade Zahl n >= 4 entscheidet, ob sie die Goldbach-Eigenschaft hat oder nicht ? Es gibt ihn: Algorithmus: Test auf Goldbach-Eigenschaft Eingabe : n {gerade Zahl >= 4} FÜR k := 2 BIS n DIV 2 WIEDERHOLE WENN Prim(k) UND Prim(n-k) DANN Ausgabe : "ja" SONST Ausgabe : "Nein" FUNKTION Prim (Zahl : integer) : boolean WENN Zahl = 2 DANN Prim := TRUE SONST {Zahl ist Primzahl genau dann ,wenn Zahl zwischen 2 und Wurzel aus Zahl keine Teiler besitzt} P := TRUE i := 2 WIEDERHOLE WENN Zahl MOD i = 0 DANN P := FALSE inc( i) BIS (NOT P) ODER i > Wurzel aus Zahl ENDE_SONST Prim := P Die Goldbach-Eigenschaft ist also entscheidbar. Damit ist das Goldbach-Problem für den Informatiker gelöst, aber die Goldbachsche Vermutung lässt sich damit weder beweisen noch widerlegen. Aufgabe: Entwickeln Sie ein Programm, das gerade Zahlen größer gleich 4 auf ihre GoldbachEigenschaft testet. Ein anderes Beispiel stellen die „wundersamen Zahlen“ dar. Definition: Eine Menge M natürlicher Zahlen heißt entscheidbar, wenn es einen Algorithmus gibt, der für jede natürliche Zahl feststellt, ob sie Element von M ist oder nicht. Dieser Algorithmus heißt dann ein zweiseitiges Entscheidungsverfahren. Ist die Menge M durch eine Eigenschaft gegeben, so nennen wir diese Eigenschaft entscheidbar. Bezeichnung: Die Funktion g : N ---> N, g( n) := 1/2 n , falls n gerade und g( n) := 3 n + 1 , falls n ungerade heißt Collatz-Funktion(L. Collatz, um 1930). Bezeichnung: Eine natürliche Zahl n heißt wundersam, wenn es eine natürliche Zahl k, so dass die CollatzFunktion k mal hintereinander ausgeführt 1 ergibt. : Beispiele: n = 5: ________________________________________________________________ n = 15: ________________________________________________________________ (auch Collatz-Folge genannt) Lothar Collatz stellte die Vermutung auf, daß jede natürliche Zahl wundersam ist. Auch diese Vermutung ist bis heute weder bewiesen noch widerlegt. Analog zum Goldbach-Problem schwächen wir die Fragestellung für die Informatik ab: Gibt es einen Algorithmus, der für eine gegebene natürliche Zahl n entscheidet, ob sie wundersam ist oder nicht ? Naheliegend ist folgender Algorithmus: Test auf Wundersamkeit Eingabe : n SOLANGE n > 1 WIEDERHOLE WENN n gerade DANN n := n DIV 2 SONST n := 3 * n + 1 ENDE_SOLANGE BIS n=1 Ausgabe : "Wundersam" Dieser Algorithmus stellt aber kein zweiseitiges Entscheidungsverfahren dar: Ist n wundersam, so liefert dieser Algorithmus nach endlich vielen Schritten die Ausgabe "Wundersam". Gibt man aber eine "unwundersame" Zahl ein, kommt der Algorithmus zu keinem Ende und damit zu keiner Entscheidung. Allerdings hat bis heute noch niemand eine unwundersame Zahl gefunden. Definition: Eine Menge M natürlicher Zahlen heißt partiell entscheidbar, wenn es einen Algorithmus gibt, der für jede natürliche Zahl feststellt, ob sie Element von M ist. Dieser Algorithmus heißt dann ein einseitiges Entscheidungsverfahren. Ist die Menge M durch eine Eigenschaft gegeben, so nennen wir diese Eigenschaft partiell entscheidbar. Ein zweiseitiges Entscheidungsverfahren für die Eigenschaft "Wundersam" ist bisher nicht bekannt. Beispiel: Game of Life (J. Conway) Gegeben ist ein zweidimensionales Feld aus n Zeilen und m Spalten, dessen Komponenten Zellen genannt werden. Jede Zelle besitzt zwei Zustände (lebend, tot). Die zu Beginn lebenden Zellen bilden die Anfangsgeneration. Eine Folgegeneration entsteht nach folgenden Regeln : Eine tote Zelle wird lebendig, wenn drei ihrer acht Nachbarn leben. Eine Zelle stirbt, wenn mindestens vier Nachbarn leben (Übervölkerung). Eine Zelle stirbt, wenn höchstens ein Nachbar lebt (Vereinsamung). Gesucht ist ein Algorithmus, der für eine beliebige gegebene Anfangsgeneration des Lebensspiels entscheidet, ob diese unsterblich ist oder nicht. Der Algorithmus könnte so aussehen, dass er den Lebenslauf der Anfangsgeneration Schritt für Schritt nachvollzieht. Stirbt sie, so ist die Sterblichkeit festgestellt. Stirbt sie aber nicht, gelangt der Algorithmus zu keinem Ende und damit auch zu keiner Entscheidung. Der Algorithmus stellt also nur ein einseitiges Entscheidungsverfahren dar. Im Gegensatz zum Problem der Wundersamkeit ist jedoch für das Game of Life inzwischen bewiesen, dass kein zweiseitiges Entscheidungsverfahren existiert. Die Sterblichkeit einer Anfangsgeneration im Game of Life ist also zwar partiell entscheidbar, aber nachgewiesenermaßen nicht entscheidbar. Wie man solche Nachweise führt, soll an folgendem Satz gezeigt werden: SATZ VON TURING (eingeschränkt für Pascal-Programme): Es gibt kein Pascal-Programm, das für ein beliebiges Pascal-Programm P und eine beliebige Eingabe x die Frage beantwortet, ob P auf x angewendet nach endlich vielen Schritten terminiert oder nicht, d.h. die Halte-Eigenschaft für Pascal-Programme ist unentscheidbar und nur partiell entscheidbar. Beweis: Annahme: Es gibt es eine boolesche Funktion "Haelt", die folgendes leistet: Ist P ein Pascal-Programm und x eine Zeichenfolge, so liefert Haelt nach Eingabe von P und x den Wert TRUE, wenn P auf x angewendet nach endlich vielen Schritten hält, andernfalls den Wert FALSE. FUNCTION Haelt( P, x : Text) : boolean; BEGIN IF {P terminiert bei Eingabe von x} THEN Haelt := TRUE ELSE Haelt := FALSE; END; Diese Funktion wird nun in ein Programm "Seltsam" eingebettet: PROGRAM Seltsam; FUNCTION Haelt(P, x : Text) : boolean; {wie oben } BEGIN {Lies Programm P} WHILE Haelt(P, P) DO; {Endlosschleife} writeln('Fertig !'); END. Das heißt: Hält das Programm P, wenn ihm sein eigener Quelltext eingegeben wird, so begibt sich das Programm "Seltsam" in eine Endlosschleife, andernfalls hält es an. Da P beliebig ist, kann insbesondere auch P = "Seltsam" sein. In diesem Fall gilt : Ist Haelt( Seltsam, Seltsam) = TRUE, so begibt sich "Seltsam" in eine Endlosschleife. Ist Haelt( Seltsam, Seltsam) = FALSE, so terminiert "Seltsam". D.h. Das Programm "Seltsam" terminiert bei der Eingabe seines eigenen Quelltextes genau dann, wenn es nicht terminiert. Die obige Annahme der Existenz der Funktion "Haelt" führt demnach zu einem WIDERSPRUCH ! Was heißt das für die Praxis? Dies bedeutet zum Beispiel, dass es kein universelles Testwerkzeug für Endloszyklen in einem beliebigen zu untersuchenden Programm geben kann und niemals geben wird. Für spezielle Programme sind dagegen Verifikationstechniken bekannt, die solche Prüfungen vornehmen können. Gödelscher Unvollständigkeitssatz: In jedem widerspruchsfreien Axiomensystem gibt es Sätze, die nicht mit den Mitteln dieses Systems bewiesen werden können.