Günther Stiege Einführung in die Informatik November 2012 i Vorwort Sie wartet im Park bei den Tannen. Er hat noch viel zu tun: Den Back gepätscht. Pettern gemetscht. An der Platine den Finger gequetscht. Igemehlt. Einen Apfel geschält. Die Datei abgedätet. Er hat sich arg verspätet. Nun geht sie verärgert von dannen. chg escalera 1999 Das vorliegende Buch ist aus einem Skript zu den Vorlesungen „Programmierung“ und „Datenstrukturen“ entstanden, die ich im Wintersemester 1998/99 und im Sommersemester 1999 an der Universität Oldenburg gehalten habe. Diese beiden Vorlesungen bildeten einen einführenden, in erster Linie für Anfänger im Dimplomstudiengang Informatik bestimmten Kurs. Den Diplomstudiengang Informatik gibt es nicht mehr. Eine anspruchsvolle einführende Darstellung der Informatik ist jedoch auch für den universitären Bachelor-Studiengang in Informatik notwendig. Sie wird auch in naturwissenschaftlichen und technischen universitären Studiengängen gebraucht. Für diesen Leserkreis ist das Buch gedacht. Ich habe den Stoff in die Teile „Grundlagen“, „Algorithmen“, „Einfache Datenstrukturen“, „Allgemeine Graphen“ und „Parallelität“ eingeteilt. Das soll kurz erläutert werden. In den Grundlagen wird mit einführenden Beispielen im 1. Kapitel ein Einstieg in den Stoff gefunden. Ich habe es für zweckmäßig gehalten, Knuth [Knut1997] sowie einer Vorlesung meines Kollegen R. Vollmar zu folgen und mit dem guten, alten euklidischen Algorithmus zu beginnen. Die dazugehörenden Effizienzbetrachtungen sind zugegebenermaßen für Anfänger in den ersten Vorlesungstunden nicht ganz einfach. Hier wie auch im Rest des Buches habe ich jedoch Wert darauf gelegt, mathematischen Schwierigkeiten, insbesondere auch Beweisen, nicht auszuweichen. Die leider in der Informatikausbildung zunehmend zu beobachtende Tendenz, Schulung in formalem Denken zugunsten praktischer Fertigkeiten und Kenntnisse zu reduzieren, halte ich für fatal. Das ausführliche 2. Kapitel erläutert anhand der Sprache C die Grundlagen der Pro- ii grammierung. Auch hierzu einige Anmerkungen. Es ist üblich, in der universitären Informatikausbildung außer dem Informatik-Grundkurs einen getrennten Programmierkurs vorzusehen. Meistens wird dort eine „moderne“ Sprache, objektorientiert oder funktional, zu Grunde gelegt. Ich bin bewußt davon abgewichen, da ich eine explizite und intensive Beschäftigung mit Konzepten wie Zeigern und Unterprogrammen in der Informatikausbildung für sehr wichtig und prägend halte. Die Kapitel 3 „Darstellung von Daten durch Bitmuster“ und 4 „Rechensysteme“ vermitteln so viel über „reale“ Rechner, wie für eine Einführung nötig ist. Der Kern des Kurses sind die Teile II, III und IV. In Teil II werden in zwei Kapiteln Algorithmen allgemein untersucht. Es wird der naive Algorithmus-Begriff behandelt und es werden Markov-Methoden als Beispiel für eine Formalisierung des Algorithmus-Begriffs vorgestellt. Die Churchsche These wird erwähnt und die Unlösbarkeit des Halteproblems gezeigt. Es wird die Zeiteffizienz von Algorithmen betrachtet und dazu die O-Notation eingeführt. Zum Schluß wird das Problem P = N P erläutert. In zwei ergänzenden Abschnitten wird in Form eines kurzen Überblicks auf Turingmaschinen und Formale Sprachen und auf die näherungsweise Lösung schwieriger Probleme eingegangen. Teil III ist einfachen Datenstrukturen, nämlich Listen, Suchbäumen, Hashing und Sortieren gewidmet. Die entsprechenden Datenstrukturen und Algorithmen werden vorgestellt und untersucht. Teil IV des Buches handelt von allgemeinen Graphen. Das sind Graphen, in denen ungerichtete Kanten und gerichtete Bögen beliebig gemischt auftreten. Diese vereinheitlichende Sicht ist neu und war mir zu der Zeit, in der der Kurs durchgeführt wurde, noch nicht bekannt. Aus diesem Grund ist Teil IV des Buches ganz anders, insbesondere auch umfangreicher, als die graphentheoretischen Kapitel des ursprünglichen Skripts. Den Kern des vierten Teils bilden die Kapitel „Grundlagen“, „Darstellungen“, „Wege und Zusammenhang“, „Tiefensuche und Breitensuche“ sowie „Die Biblockzerlegung“. In der Informatik und anderen Anwendungsgebieten der Graphentheorie sind Netzwerke, also Graphen, deren Knoten und/oder Linien zusätzliche Attribute aufweisen, von besonderer Bedeutung. Als wichtiges Beispiel werden „Kürzeste Wegen“ in allgemeinen Graphen untersucht. Parallelität ist ein sehr wichiges Gebiet der Informatik. In Einführungen und Grundkursen bleibt oft keine Zeit, es zu behandeln. Einen ersten Einblick geben die zwei Kapitel von Teil V. Das Kapitel „Parallelität in Rechensystemen und Netzen“ beschreibt, wo und in welcher Form parallele Abläufe auftreten. Das Kapitel „Programmieren II: Parallele Programme“ behandelt am Beispiel von Leichtgewichtsprozessen in C einige einführende Fragen der Parallelität. Ich habe mich bemüht, die nach meiner Meinung wesentlichen Dinge eines jeden angesprochenen Gebietes hinreichend ausführlich darzustellen. Eine Reihe von Dingen, die ich auch für wichtig halte, die man bei Zeitmangel aber überspringen kann, habe ich explizit mit iii einem * als Ergänzung gekennzeichnet. Es sind einzelne Abschnitte, in der Graphentheorie auch ganze Kapitel. Einige Ergänzungen haben nur Überblickscharakter. In Ergänzungen wird durchaus auf andere Ergänzungen zurückgegriffen, an anderer Stelle nicht. Zu den meisten Kapiteln gibt es Übungen. Zur Mehrzahl der Übungen sind im Anhang Lösungen oder Lösungshinweise angegeben. Die Übungen reichen für einen ergänzenden Übungskurs nicht aus, denn reine Lernaufgaben zum Trainieren des Stoffes habe ich nicht aufgenommen. Statt dessen habe ich für die Übungen Erweiterungen und Ergänzungen ausgewählt. An mehr als einer Stelle wird der Leser auch aufgefordert, einfache Aussagen des Stoffteils selber zu beweisen. In den Anhängen zum Buch befinden sich eine Zusammenfassung mathematischer Hilfsmittel allgemeiner Art, eine kurze Darstellung der benötigten Begriffe und Ergebnisse aus der Mengenlehre, ein Anhang über wichtige Grundlagen der Wahrscheinlichkeitstheorie und einiges mehr.. Für die Vorlesung, das Skript und jetzt das Buch habe ich eine Vielzahl von Lehrbüchern und andere schriftliche Quellen benutzt. Sie sind als Literaturzitate im Text der einzelnen Kapitel, speziell in den abschließenden Abschnitten „Literatur“ angegeben. Kurs und Skript wurden in besonderem Maße haben von den Büchern von Aho/Ullman [AhoU1995], Cormen/Leiserson/Rivest [CormLR1990], Knuth ([Knut1997], [Knut1998], [Knut1998a]) sowie Kowalk [Kowa1996] beeinflußt. Eine Reihe von Verzeichnissen ergänzt das Buch. Besonderen Wert habe ich auf ein ausführliches Stichwortverzeichnis gelegt. Insgesamt ergibt sich ein ziemlich dickes Buch. Es würde mich freuen, wenn es als ganzens oder in Teilen zu intensivem Lesen anregte. Besonders erfeut wäre ich, wenn es Neulingen in der Informatik etwas von der Faszination und der Schönheit dieser Wissenschaft vermittelte. Das Buch ist in LATEX geschrieben und nach amerikanischem Brauch wird ein “chapter” in “sections” und diese in “subsections” unterteilt. Nur widerstrebend habe ich mich durchgerungen, deshalb bei der Kapitelgliederung von „Abschnitten“ (statt Unterkapitel) und „Unterabschnitten“ zu sprechen. Vielen Leuten gilt es zu danken: Meine Mitarbeiter Björn Briel, Olaf Maibaum und Ingo Stierand haben mir bei der Erstellung des Skripts nicht nur durch intensives Korrekturlesen, sondern auch durch viele fachliche Diskussionen und Anregungen immer wieder geholfen. Auch Anregungen von Studierenden sind in die endgültige Fassung eingflossen. Etliche Ungenauigkeiten und eine Vielzahl von Schreibfehlern konnten dadurch ausgemerzt werden. Auch Herr Michael Uelschen und Frau Susanne Steiner haben durch Korrekturlesen zur Verbesserung beigetragen. Meinem Kollegen Hermann Luttermann danke ich für die Durchsicht des Kapitels über Parallelität in Rechensystemen und Netzen. Der nach der Vorlesung und der ersten Skriptversion entstandene umfangreiche Teil über allgemeine Graphen, wäre ohne die mehrjährige intensive Zuasmmenarbeit mit Ingo Stie- iv rand und Sergej Alekseev nicht möglich gewesen. Leider sind die interessanten und wichtigen Ergebnisse zur Ablaufanalyse aus der Dissertation Alekseev [Alek2006] wegen Platzmangels nicht mehr in das Buch eingeflossen. Natürlich braucht es für die Enstehung eines Buches wie dieses eine vorherige Reifezeit und sowie eine anregende Arbeitsumgebung. Beides habe ich im Kreis der Kollegen und meiner Mitarbeiter an meinen Wirkungsstätten, der TU Braunschweig, der Universität Hildesheim und der Universität Oldenburg gehabt. Ein besonderer Dank gilt meinem Sohn Harold, der mit viel Zähigkeit den für ihn schwierigen Text Korrektur gelesen hat. Und natürlich meiner Frau, ohne deren Unterstützung ich das Buch gar nicht hätte schreiben können. Hannover, im November 2012 G. Stiege Inhaltsverzeichnis i Vorwort I Grundlagen 1 1 Einführende Beispiele 1.1 Der euklidische Algorithmus . . . . . . . . . . . . . . . 1.1.1 Entwicklung und Untersuchung des Algorithmus 1.1.2 Ein Programm für den euklidischen Algorithmus 1.1.3 Effizienz des euklidischen Algorithmus . . . . . 1.2 Sortieren durch Einfügen . . . . . . . . . . . . . . . . . 1.2.1 Allgemeines zum Sortieren . . . . . . . . . . . . 1.2.2 Insertion Sort: Algorithmus und Programm . . . 1.2.3 Effizienz von Sortieren durch Einfügen . . . . . 1.3 Ein Kochrezept . . . . . . . . . . . . . . . . . . . . . . Aufgaben . . . . . . . . . . . . . . . . . . . . . . . . . Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 3 3 6 10 18 18 18 19 25 27 28 2 Programmieren I: Sequentielle Programme 2.1 Schichtenaufbau eines Rechensystems . . . . 2.2 Dateien und Datenbanken . . . . . . . . . . 2.3 Grundbegriffe . . . . . . . . . . . . . . . . . 2.4 Werte, Variable, Zeiger, Namen . . . . . . . 2.5 Wertebereiche, Operationen, Ausdrücke . . . 2.5.1 Ganze Zahlen . . . . . . . . . . . . . 2.5.2 Rationale Zahlen . . . . . . . . . . . 2.5.3 Zeichen . . . . . . . . . . . . . . . . 2.5.4 Wahrheitswerte . . . . . . . . . . . . 2.5.5 Adressen . . . . . . . . . . . . . . . . 2.5.6 Aufzählungstypen . . . . . . . . . . . 2.6 Reihungen, Zeichenreihen, Sätze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 29 33 34 37 45 45 48 52 54 57 59 61 v . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . vi INHALTSVERZEICHNIS 2.7 2.8 2.6.1 Reihungen . . . . . . 2.6.2 Zeigerarithmetik in C 2.6.3 Zeichenreihen . . . . 2.6.4 Sätze . . . . . . . . . Programmsteuerung . . . . Unterprogramme . . . . . . Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 Darstellung von Daten durch Bitmuster 3.1 Bits und Bitmuster . . . . . . . . . . . . . 3.2 Darstellung natürlicher Zahlen . . . . . . . 3.3 Darstellung ganzer Zahlen . . . . . . . . . 3.4 Darstellung rationaler Zahlen . . . . . . . 3.5 Hexadezimaldarstellung von Bitmustern . 3.6 Darstellung von Zeichenreihen . . . . . . . 3.7 Eigentliche Bitmuster und Wahrheitswerte 3.8 Bitmuster in C . . . . . . . . . . . . . . . Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 65 67 68 78 83 91 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93 94 96 101 105 109 112 117 118 119 4 Rechensysteme 4.1 Grobschema der konventionellen Maschine . . . 4.1.1 Prozessor . . . . . . . . . . . . . . . . . 4.1.2 Hauptspeicher . . . . . . . . . . . . . . . 4.1.3 Ein-/Ausgabeschnittstellen . . . . . . . . 4.1.4 Übertragungsmedium . . . . . . . . . . . 4.1.5 Periphere Geräte . . . . . . . . . . . . . 4.2 Prozessor und Maschinenbefehle . . . . . . . . . 4.2.1 Register des Prozessors . . . . . . . . . . 4.2.2 Maschinenbefehle und Unterbrechungen . 4.2.3 Befehlsklassen . . . . . . . . . . . . . . . 4.2.4 Adressierungsarten . . . . . . . . . . . . 4.3 Grundsoftware . . . . . . . . . . . . . . . . . . . 4.3.1 Betriebssystem . . . . . . . . . . . . . . 4.3.2 Weitere Programme der Grundsoftware . 4.4 Virtuelle Adressierung . . . . . . . . . . . . . . Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121 121 121 122 126 127 127 129 129 133 135 137 138 139 144 145 147 II Algorithmen 5 Algorithmen I: Naiv und formalisiert . . . . . . . . . . . . . . . . . . 149 151 INHALTSVERZEICHNIS 5.1 5.2 Der naive Algorithmus-Begriff . . . . . . . . . . . 5.1.1 Eigenschaften . . . . . . . . . . . . . . . . 5.1.2 Schrittweise Verfeinerung . . . . . . . . . . 5.1.3 Entwurfstechniken für Algorithmen . . . . 5.1.4 Algorithmen in applikativer Darstellung . Der formalisierte Algorithmusbegriff . . . . . . . . 5.2.1 Markov-Algorithmen . . . . . . . . . . . . 5.2.2 Churchsche These . . . . . . . . . . . . . . 5.2.3 Algorithmisch unlösbare Probleme . . . . 5.2.4 Turing-Maschinen und formale Sprachen* Aufgaben . . . . . . . . . . . . . . . . . . . . . . Literatur . . . . . . . . . . . . . . . . . . . . . . . vii . . . . . . . . . . . . 6 Algorithmen II: Effizienz und Komplexität 6.1 Laufzeitanalyse von Algorithmen und Programmen 6.1.1 Vorbemerkungen . . . . . . . . . . . . . . . 6.1.2 Beispiel: Mischsortieren . . . . . . . . . . . . 6.1.3 Boden und Decke . . . . . . . . . . . . . . . 6.1.4 Modulo . . . . . . . . . . . . . . . . . . . . 6.1.5 Abschätzungen und Größenordnungen . . . 6.1.6 Rekurrenzen und erzeugende Funktionen* . 6.2 Die Komplexität von Problemen . . . . . . . . . . . 6.2.1 Überblick . . . . . . . . . . . . . . . . . . . 6.2.2 Komplexität von Algorithmen . . . . . . . . 6.2.3 Beispiel: Hamiltonkreise . . . . . . . . . . . 6.2.4 Die Problemklassen P, N P und weitere . . 6.3 Näherungslösungen schwieriger Probleme* . . . . . Aufgaben . . . . . . . . . . . . . . . . . . . . . . . Literatur . . . . . . . . . . . . . . . . . . . . . . . . III Einfache Datenstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152 152 155 159 162 166 166 169 170 172 175 175 . . . . . . . . . . . . . . . 177 . 177 . 177 . 178 . 189 . 193 . 195 . 199 . 199 . 199 . 200 . 201 . 210 . 216 . 223 . 223 225 7 Allgemeines zu Datenstrukturen 227 7.1 Sätze und Vetretersätze . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227 7.2 Schlüssel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 228 8 Listen 231 8.1 Terminologie und Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . . 231 8.1.1 Definition und Beispiele . . . . . . . . . . . . . . . . . . . . . . . . 231 8.1.2 Operationen mit Listen . . . . . . . . . . . . . . . . . . . . . . . . . 232 viii INHALTSVERZEICHNIS 8.2 8.3 8.4 Binärsuche . . . . . . . . . . . . . . . . . . . . . Realisierung von Listen . . . . . . . . . . . . . . 8.3.1 Verkettete Listen . . . . . . . . . . . . . 8.3.2 Realisierung von Listen durch Reihungen Keller, Schlangen, Halden . . . . . . . . . . . . 8.4.1 Keller . . . . . . . . . . . . . . . . . . . 8.4.2 Keller: Beispiele und Anwendungen . . . 8.4.3 Schlangen . . . . . . . . . . . . . . . . . 8.4.4 Halden und Prioritätswarteschlangen . . Aufgaben . . . . . . . . . . . . . . . . . . . . . Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 Suchbäume 9.1 Binäre Suchbäume . . . . . . . . . . . . . . . . . . . . . . . . 9.1.1 Allgemeines zum Suchen . . . . . . . . . . . . . . . . . 9.1.2 Binärbäume . . . . . . . . . . . . . . . . . . . . . . . . 9.1.3 Binäre Suchbäume und Operationen auf ihnen . . . . . 9.2 Rot-Schwarz-Bäume . . . . . . . . . . . . . . . . . . . . . . . 9.2.1 Definition und Eigenschaften von Rot-Schwarz-Bäumen 9.2.2 Operationen auf Rot-Schwarz-Bäumen . . . . . . . . . 9.3 Zufällige binäre Suchbäume . . . . . . . . . . . . . . . . . . . 9.4 B-Bäume und externe Datenspeicherung . . . . . . . . . . . . 9.4.1 Mehrweg-Suchbäume . . . . . . . . . . . . . . . . . . . 9.4.2 Speicherung bei Dateien und Datenbanken . . . . . . . 9.4.3 B-Bäume . . . . . . . . . . . . . . . . . . . . . . . . . 9.5 Digitale Bäume . . . . . . . . . . . . . . . . . . . . . . . . . . Aufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 Suchen mit Schlüsseltransformation 10.1 Direkte Speicherung . . . . . . . . . . . . . . . . 10.2 Grundlagen des Hashings . . . . . . . . . . . . . . 10.2.1 Hashingalgorithmen . . . . . . . . . . . . . 10.2.2 Kollisionsauflösung durch Verkettung . . . 10.2.3 Kollisionsauflösung durch offenes Hashing 10.2.4 Universelles Hashing . . . . . . . . . . . . 10.2.5 Dynamisches Hashing . . . . . . . . . . . . Aufgaben . . . . . . . . . . . . . . . . . . . . . . Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235 239 239 243 245 245 247 251 252 255 255 . . . . . . . . . . . . . . . 257 . 257 . 257 . 258 . 266 . 277 . 277 . 280 . 293 . 296 . 296 . 298 . 299 . 303 . 303 . 305 . . . . . . . . . 307 . 308 . 308 . 309 . 311 . 312 . 316 . 316 . 317 . 317 INHALTSVERZEICHNIS ix 11 Sortieren 11.1 Allgemeines zum Sortieren . . . . . . . . . . . 11.2 Quicksort . . . . . . . . . . . . . . . . . . . . 11.2.1 Algorithmus und Programm . . . . . . 11.2.2 Komplexität von Quicksort . . . . . . . 11.2.3 Randomisiertes Quicksort . . . . . . . 11.2.4 Qicksort und Mischsortieren . . . . . . 11.3 Halden, Heapsort und Prioritätswartesclangen 11.3.1 Halden (als Datenstruktur) . . . . . . 11.3.2 Sortieren mit Halden (Heapsort) . . . . 11.3.3 Prioritätswarteschlangen (nach Knuth) 11.4 Mindestkomplexität beim Sortieren . . . . . . 11.5 Lineares Sortieren . . . . . . . . . . . . . . . . Aufgaben . . . . . . . . . . . . . . . . . . . . Literatur . . . . . . . . . . . . . . . . . . . . . IV . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Allgemeine Graphen 319 . 319 . 320 . 320 . 326 . 328 . 329 . 329 . 329 . 332 . 333 . 337 . 339 . 342 . 342 345 12 Grundlagen allgemeiner Graphen 12.1 Definitionen und Beispiele . . . . . . . . . . . . 12.2 Orientierungsklassen und Untergraphen . . . . . 12.3 Bipartite Graphen . . . . . . . . . . . . . . . . 12.4 Der Grad eines Knotens . . . . . . . . . . . . . 12.5 Anmerkung: Ersetzung von Kanten durch Bögen 12.6 Gleichheit und Isomorphie von Graphen* . . . . Aufgaben . . . . . . . . . . . . . . . . . . . . . Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347 347 351 353 355 356 357 359 360 13 Darstellungen von Graphen 13.1 Graphische Darstellung . . . . . . . . . . . 13.2 Darstellung durch Matrizen . . . . . . . . 13.3 Darstellung durch Listen . . . . . . . . . . 13.4 Externe Darstellungen und Basiswerkzeuge Aufgaben . . . . . . . . . . . . . . . . . . Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 361 361 362 365 369 371 371 . . . . . . . . . . . . . . . . . . 14 Wege und Zusammenhang 373 14.1 Wege: Definitionen und elementare Eigenschaften . . . . . . . . . . . . . . 373 14.2 Erreichbarkeit und Zusammenhang . . . . . . . . . . . . . . . . . . . . . . 378 14.3 Brücken und Schnittpunkte . . . . . . . . . . . . . . . . . . . . . . . . . . 381 x INHALTSVERZEICHNIS 14.4 Kreisfreiheit und Bäume . . . . . . . . . . . . . . 14.4.1 Kreisfreiheit und Zusammenhang . . . . . 14.4.2 a-Bäume . . . . . . . . . . . . . . . . . . . 14.4.3 f-Bäume . . . . . . . . . . . . . . . . . . . 14.5 Partielle Ordnung und Schichtennumerierung . . . 14.6 Klassifizierung von Zusammenhangskomponenten 14.7 Abgeleitete Graphen . . . . . . . . . . . . . . . . 14.8 Eulersche und hamiltonsche Wege . . . . . . . . . 14.8.1 Eulerwege . . . . . . . . . . . . . . . . . . 14.8.1.1 a-Eulerkreise . . . . . . . . . . . 14.8.1.2 f-Eulerkreise . . . . . . . . . . . 14.8.2 Hamiltonwege . . . . . . . . . . . . . . . . 14.9 Datenstrukturen für Wege und Kreiszerlegung . . 14.9.1 Wege als verkettete Listen . . . . . . . . . 14.9.2 Kreiszerlegung* . . . . . . . . . . . . . . . Aufgaben . . . . . . . . . . . . . . . . . . . . . . Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 Tiefensuche und Breitensuche 15.1 Tiefensuche . . . . . . . . . . . . . . . . . . . . . . . 15.2 Tiefensuchbäume . . . . . . . . . . . . . . . . . . . . 15.3 Bestimmung schwacher Zusammenhangskomponenten 15.4 Bestimmung starker Zusammenhangskomponenten . . 15.5 Breitensuche . . . . . . . . . . . . . . . . . . . . . . . Aufgaben . . . . . . . . . . . . . . . . . . . . . . . . Literatur . . . . . . . . . . . . . . . . . . . . . . . . . 16 Die 16.1 16.2 16.3 16.4 16.5 16.6 16.7 Biblockzerlegung Klassen geschlossener a-Wege und Kantenzerlegungen Die Biblockzerlegung allgemeiner Graphen . . . . . . Eigenschaften der Komponenten der Biblockzerlegung Klassifikation von Linien und Knoten . . . . . . . . . Der Biblockgraph . . . . . . . . . . . . . . . . . . . . Algorithmen zur Bestimmung der Biblockzerlegung . Digraphen und vollständige Orientierungen . . . . . . Aufgaben . . . . . . . . . . . . . . . . . . . . . . . . Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382 382 384 386 387 392 394 399 399 399 400 404 405 405 406 408 411 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 413 . 413 . 419 . 423 . 423 . 430 . 433 . 434 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 437 437 439 443 448 449 451 458 461 462 17 Perioden* 465 17.1 a-Periode und f-Periode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 465 17.2 Periodizitätsklassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 468 INHALTSVERZEICHNIS xi 17.3 Ein Algorithmus zur Bestimmung der Periode . . . . . . . . . . . . . . . . 470 Aufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473 Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 475 18 Ergänzungen zur Graphentheorie* 18.1 Mengertheorie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18.1.1 Trennende Mengen und disjunkte Wege . . . . . . . . . . . . . 18.1.2 Die Mengersätze . . . . . . . . . . . . . . . . . . . . . . . . . 18.1.3 Erweiterungen zu den Mengersätzen . . . . . . . . . . . . . . . 18.1.4 Die Struktur von Mengertrennmengen . . . . . . . . . . . . . Literatur zu Abschnitt „Mengertheorie“ . . . . . . . . . . . . . 18.2 Korrespondenzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18.2.1 Überdeckungen und Unabhängigkeit . . . . . . . . . . . . . . 18.2.2 Korrespondenzen (Matchings) . . . . . . . . . . . . . . . . . . 18.2.3 Maximale Korrespondenzen und alternierende Wege . . . . . . 18.2.4 Ein Algorithmus zum Finden maximaler Korrepondenzen . . . 18.2.5 Korrektheit und Effizienz des Algorithmus . . . . . . . . . . . 18.2.6 Korrespondenzen in bipartiten Graphen . . . . . . . . . . . . Literatur zu Abschnitt „Korrespondenzen“ . . . . . . . . . . . 18.3 Höhere Zusammenhangszerlegungen . . . . . . . . . . . . . . . . . . . 18.3.1 k-a-Zusammenhang . . . . . . . . . . . . . . . . . . . . . . . . 18.3.2 k-a-Linienzusammenhang . . . . . . . . . . . . . . . . . . . . . 18.3.3 Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Aufgaben zu Abschnitt „Höhere Zusammenhangszerlegungen“ Literatur zu Abschnitt „Höhere Zusammenhangszerlegungen“ . 18.4 Partitionen und starke Transitivität . . . . . . . . . . . . . . . . . . . 18.4.1 Partitionen in allgemeinen Graphen . . . . . . . . . . . . . . . 18.4.2 Starke Transitivität . . . . . . . . . . . . . . . . . . . . . . . . Aufgaben zu Abschnitt “Partitionen und starke Transitivität“ Literatur zu Abschnitt “Partitionen und starke Transitivität“ . 18.5 Knotenfärbungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Aufgaben zu Abschnitt „Färbungen“ . . . . . . . . . . . . . . Literatur zu Abschnitt „Färbungen“ . . . . . . . . . . . . . . . 18.6 Planarität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Literatur zu Abschnitt „Planarität“ . . . . . . . . . . . . . . . 19 Kürzeste Wege in Netzwerken 19.1 Gewichtete Wege . . . . . . . . . . . . . . . . . . . . . . . 19.2 Kürzeste Wege mit nichtnegativen Gewichten . . . . . . . 19.3 Kürzeste Wege mit beliebigen Gewichten . . . . . . . . . . 19.4 Kürzeste Wege unter Berücksichtigung der Graphstruktur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 477 . 477 . 477 . 478 . 480 . 481 . 482 . 482 . 482 . 483 . 484 . 487 . 492 . 494 . 495 . 495 . 496 . 499 . 500 . 500 . 500 . 501 . 501 . 504 . 505 . 505 . 505 . 508 . 508 . 509 . 510 . . . . 511 . 511 . 513 . 516 . 518 xii INHALTSVERZEICHNIS 19.4.1 Kürzeste 19.4.2 Kürzeste Aufgaben . . . Literatur . . . . a-Entfernungen f-Entfernungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 Flüsse in Netzwerken* 20.1 Definition und Zielsetzung . . . . . . . . 20.2 Verbessernde Wege und Schnitte . . . . . 20.3 Algorithmus nach Ford und Fulkerson . . 20.4 Der Algoritmus von Edmonds und Karp 20.4.1 Beschreibung des Algorithmus . . 20.4.2 Korrektheit und Komplexität des Karp . . . . . . . . . . . . . . . . 20.4.3 Vereinfachung des Netzes . . . . . 20.5 Nicht ganzzahlige Flüsse . . . . . . . . . 20.6 Existenz eines maximalen Flusses . . . . Aufgaben . . . . . . . . . . . . . . . . . Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Algorithmus von . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Edmonds und . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 518 520 523 523 525 . 525 . 527 . 531 . 532 . 532 . . . . . . 534 542 543 543 544 544 21 Weitere Ergänzungen zu Netzwerken* 21.1 Minimale erzeugende Bäume . . . . . . . . . . . . . . . . . . . . . . . . . Literatur zu Abschnitt „Minimale erzeugende Bäume“ . . . . . . . 21.2 Gewichtsoptimale Korrespondenzen . . . . . . . . . . . . . . . . . . . . . Literatur zu Abschnitt „Gewichtsoptimale Korrespondenzen“ . . . 21.3 Minimale Rundwege . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21.3.1 Das Problem des chinesischen Briefträgers . . . . . . . . . . . . . 21.3.2 Das Problem des Handlungsreisenden . . . . . . . . . . . . . . . . Aufgaben zu Abschnitt “Minimale Rundwege“ . . . . . . . . . . . Literatur zu Abschnitt „Minimale Rundwege“ . . . . . . . . . . . 21.4 Endliche Markovketten . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21.4.1 Endliche Markovketten mit stationären Übergangswahrscheinlichkeiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21.4.2 Markovgraphen (Beispiel) . . . . . . . . . . . . . . . . . . . . . . 21.4.3 Zustandsklassifizierung . . . . . . . . . . . . . . . . . . . . . . . . Literatur zu Abschnitt „Endliche Markovketten“ . . . . . . . . . . . . . . . . . . . . 547 547 549 549 551 552 552 555 555 556 556 . . . . 556 557 559 562 V 563 Parallelität 22 Parallelität in Rechensystemen und Netzen 565 22.1 Parallelität auf der Hardware-Ebene . . . . . . . . . . . . . . . . . . . . . . 565 INHALTSVERZEICHNIS 22.2 Parallelität auf der Betriebssystem-Ebene 22.3 Vernetzte Rechner . . . . . . . . . . . . 22.4 Hochleistungsrechner . . . . . . . . . . . Literatur . . . . . . . . . . . . . . . . . . xiii . . . . . . . . . . . . . . . . . . . . 23 Programmieren II: Parallele Programme 23.1 Programmierbeispiele für Leichtgewichtsprozesse . 23.1.1 Beispiel: Paralleles Zählen . . . . . . . . . 23.1.2 Beispiel: Matrixmultiplikation . . . . . . . 23.1.3 Verallgemeinerter Speedup . . . . . . . . . 23.2 Erzeuger/Verbraucher . . . . . . . . . . . . . . . 23.2.1 Lösung mit Leichtgewichtsprozessen . . . . 23.2.1.1 Programme . . . . . . . . . . . . 23.2.1.2 Semaphore, Mutexe und Signale . 23.2.2 Lösung durch Simulation . . . . . . . . . . 23.2.3 Bediensysteme* . . . . . . . . . . . . . . . 23.3 Beispiel: Zeigerspringen . . . . . . . . . . . . . . . Aufgaben . . . . . . . . . . . . . . . . . . . . . . Literatur . . . . . . . . . . . . . . . . . . . . . . . VI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 581 . 581 . 582 . 585 . 589 . 590 . 592 . 592 . 595 . 597 . 600 . 604 . 607 . 608 Anhänge A Mengenlehre A.1 Mengen . . . . . . . . . . . . . . . . . . A.2 Geordnete Paare und Relationen . . . . A.3 Abbildungen und Familien . . . . . . . . A.4 Allgemeine Vereinigungen, Durchschnitte A.5 Relationen über einer Menge . . . . . . . A.6 Mächtigkeit einer Menge . . . . . . . . . 567 576 577 579 609 . . . . . . . . . . . . . . . . . . . . . . . . und Produkte . . . . . . . . . . . . . . . . B Wahrscheinlichkeitstheorie B.1 Allgemeines zu Wahrscheinlichkeitsräumen B.2 Diskrete Wahrscheinlichkeitsräume: . . . . B.3 Stetige Wahrscheinlichkeitsräume: . . . . . B.4 Stochastische Prozesse . . . . . . . . . . . B.5 Zufallszahlen . . . . . . . . . . . . . . . . Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 611 . 611 . 613 . 614 . 615 . 616 . 616 . . . . . . 619 . 620 . 622 . 624 . 626 . 626 . 627 xiv INHALTSVERZEICHNIS C Hilfsmittel aus der Analysis und Zahlentheorie 629 C.1 Exponentialfunktion und Logarithmusfunktion . . . . . . . . . . . . . . . . 629 C.2 Ungleichung von Cauchy-Schwarz-Bunjakowski . . . . . . . . . . . . . . . . 631 C.3 Zahlentheoretische Hilfssätze . . . . . . . . . . . . . . . . . . . . . . . . . . 632 D Ergänzungen zu Erzeuger/Verbraucher 635 D.1 Erzeuger/Verbraucher in Realzeit . . . . . . . . . . . . . . . . . . . . . . . 635 D.2 Erzeuger/Verbraucher in der Simulation . . . . . . . . . . . . . . . . . . . 648 E Das E.1 E.2 E.3 E.4 E.5 System GHS zur Bearbeitung von Graphen Allgemeine Beschreibung von GHS . . . . . . . . . . . . . . Beispiel: Einlesen eines Graphen . . . . . . . . . . . . . . . . Beispiel: Schwache und starke Zusammenhangskomponenten Beispiel: Biblockzerlegung . . . . . . . . . . . . . . . . . . . Beispiel: Finden von Hamiltonkreisen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 659 659 659 660 667 667 F Verzeichnis der Algorithmen 677 G Namensliste 683 L Lösungen ausgewählter Aufgaben L.1 Kapitel 5: „Algorithmen I“ . . . . . . . . . . . . . . . . L.2 Kapitel 6: „Algorithmen II“ . . . . . . . . . . . . . . . L.3 Kapitel 9: „Suchbäume“ . . . . . . . . . . . . . . . . . . L.4 Kapitel 10: „Hashing“ . . . . . . . . . . . . . . . . . . . L.5 Kapitel 11: „Sortieren“ . . . . . . . . . . . . . . . . . . L.6 Kapitel 12: „Grundlagen allgemeiner Graphen“ . . . . . L.7 Kapitel 13: „Darstellung allgemeiner Graphen“ . . . . . L.8 Kapitel 14: „Wege und Zusammenhang“ . . . . . . . . . L.9 Kapitel 15: „Tiefen- und Breitensuche“ . . . . . . . . . L.10 Kapitel 16: „Die Biblockzerlegung“ . . . . . . . . . . . L.11 Kapitel 17: „Ergänzung: Perioden“ . . . . . . . . . . . . L.12 Kapitel 18: “Ergänzungen zur Graphentheorie“ . . . . . L.13 Kapitel 19: “Kürzeste Wege in Netzwerken“ . . . . . . . L.14 Kapitel 23 : „Programmieren II: Parallele Programme“ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 687 687 688 692 693 693 694 696 697 701 703 705 707 708 708 Abbildungsverzeichnis 709 Tabellenverzeichnis 715 Literaturverzeichnis 723 INHALTSVERZEICHNIS Stichwortverzeichnis xv 743 xvi INHALTSVERZEICHNIS Teil I Grundlagen 1 Kapitel 1 Einführende Beispiele 1.1 Der euklidische Algorithmus Der Algorithmus ist nach Euklid 1 benannt, obwohl er mit großer Wahrscheinlichkeit schon Eudoxos 2 bekannt war. 1.1.1 Entwicklung und Untersuchung des Algorithmus Gesucht ist der größte gemeinsame Teiler zweier positiver natürlicher Zahlen m und n. Beispiel: m = 98 und n = 21 98 = 4 · 21 + 14 21 = 1 · 14 + 7 14 = 2 · 7 + 0 ggt(98, 21) = 7 Der in diesem Beispiel benutzte Algorithmus ist in Tabelle 1.1 angegeben. Er ist auf eine Art formuliert, die man „natürliche Sprache mit Regeln“ nennen kann. Der Algorithmus hat eine Kurzbezeichnung (E) und einen Namen (Euklid). Danach folgt in Kursivschrift eine Kurzbeschreibung. Den Rest der Beschreibung bilden die einzelnen Schritte (E1, E2, E3). Auf jeden Schritt folgt der in der Numerierung nächste, wenn nicht explizit (wie z. B. Euklid, ∗um 300 v.Chr. Ausbildung vermutlich an der Platonischen Akademie in Athen, wirkte unter Ptolemaios I am Museion in Alexandria. Hat in den „Elementen“ das mathematische Wissen seiner Zeit umgeformt und in eine axiomatisch-deduktive Form gebracht. Die Elemente beeinflußten die Entwicklung der Mathematik entscheidend, teilweise bis in die Gegenwart. Sie waren im christlichen Kulturkreis zeitweise das nach der Bibel meistgelesene Buch. Der euklidische Algorithmus – in seiner subtraktiven Form – steht im 7. Buch, Propositionen 1 und 2. Von Euklid stammen außerdem Werke zur Optik („Katoptrik“) und zu den Kegelschnitten („Konika“) sowie Arbeiten zur Planimetrie, Astronomie und zur Musiktheorie. 2 Eudoxos von Knidos, ∗408 v.Chr. in Knidos, †355 (?) in Athen. Vielseitiger griechischer Mathematiker, Naturforscher und Philosoph. Proportionen- und Ähnlichkeitslehre sowie Lehre von den Kegelschnitten, die in die Elemente und Konika Euklids eingegangen sind. Astronomische und geographische Arbeiten. 1 3 4 KAPITEL 1. EINFÜHRENDE BEISPIELE ' Algorithmus E (Euklid) Es ist der größte gemeinsame Teiler zweier positiver natürlicher Zahlen m und n zu berechnen. $ E1 [Restbildung] Es wird m durch n geteilt; der Rest sei r. (Es gilt 0 ≤ r < n) E2 [Ist Rest Null?] Wenn r = 0, endet der Algorithmus; n ist das Ergebnis. & E3 [Vertauschen] Setze m :← n und n :← r und gehe zu Schritt E1 zurück. Tabelle 1.1: Euklidischer Algorithmus % bei Schritt E3) etwas anderes angegeben ist. In Abhängigkeit von einer Prüfung (einem Test) können in einem Algorithmusschritt auch unterschiedliche Fortsetzungen bestimmt werden. In Schritt E2 z. B. wird der Algorithmus beendet, wenn der Rest Null wird. Andernfalls wird mit Schritt E3 fortgefahren. Beispiel 1.1 Als Beispiel zum euklidischen Algorithmus soll der größte gemeinsame Teiler von 119 und 544 berechnet werden Anfangswerte m :← 119 n ← 544 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 119 = 0 · 544 + 119 r :← 119 r 6= 0 m :← 544 n :← 119 544 = 4 · 119 + 68 r :← 68 r 6= 0 m :← 119 n :← 68 119 = 1 · 68 + 51 r :← 51 r 6= 0 m :← 68 n :← 51 68 = 1 · 51 + 17 r :← 17 r 6= 0 m :← 51 n :← 17 51 = 3 · 17 + 0 r :← 0 r = 0; n = 17 (E1) (E2) (E3) (E1) (E2) (E3) (E1) (E2) (E3) (E1) (E2) (E3) (E1) (E2) 1.1. DER EUKLIDISCHE ALGORITHMUS ggt(119, 544) = 17. 5 23 Bei jedem Algorithmus muß man nachweisen, daß er auch wirklich das leistet, was er laut Beschreibung leisten soll. Es soll gezeigt werden, daß bei korrekten Eingabedaten, d. h. positiven natürlichen Zahlen 1. der Algorithmus hält und 2. das richtige Ergebnis berechnet. Der euklidische Algorithmus hält. Für die Eingabewerte m und n ergibt sich der folgenden Ablauf des euklidischen Algorithmus. m1 = q1 · n1 + r1 Dabei gilt: m2 = q2 · n2 + r2 .. m1 = m und n1 = n . 0 ≤ ri < ni (Rest ist kleiner als Divisor!) mi = qi · ni + ri mi+1 = ni mi+1 = qi+1 · ni+1 + ri+1 ni+1 = ri .. . Also ri = ni+1 > ri+1 für i = 1, 2, . . . . D. h. die ri bilden eine streng abnehmende Folge natürlicher Zahlen. Jede streng abnehmende Folge natürlicher Zahlen ist endlich, hat also eine letztes Element. Das letzte Element der Folge ri muß Null sein, weil der euklidische Algorithmus vorher nicht hält. Es gibt demnach ein erstes k mit rk = 0 und mk = qk · nk . 2 Der euklidische Algorithmus berechnet ggt(n, m). mi = qi · ni + ri ⇒ jeder gemeinsame Teiler von ni und ri teilt mi ⇒ ggt(ni , ri ) ist gemeinsamer Teiler von mi und ni ⇒ ggt(ni , ri ) ≤ ggt(mi , ni ) ri = mi − qi · ni ⇒ jeder gemeinsame Teiler von mi und ni teilt ri ⇒ ggt(mi , ni ) ist gemeinsamer Teiler von ni und ri ⇒ ggt(mi , ni ) ≤ ggt(ni , ri ) Das heißt ggt(mi , ni ) = ggt(ni , ri ) = ggt(mi−1 , ni−1 ). Es ist also ggt(m, n) = ggt(mk , nk ). 2 Anmerkung 1.1 Man kann im Algorithmus von Tabelle 1.1 die ganzzahlige Division (Schritt E1) durch eine fortgesetzte Subtraktion des Divisors vom Dividenden ersetzen, bis das Ergebnis zum ersten Mal kleiner wird als der Divisor. Man spricht dann von der subtraktiven Form des euklidischen Algorithmus. 2 3 Ende eines Beweises, eines Beispiels oder einer Anmerkung 6 KAPITEL 1. EINFÜHRENDE BEISPIELE Anmerkung 1.2 Ist m = q · n + r mit q ≥ 2, so gilt m − (q − 1) · n = 1 · n + r. Nach der ersten Division sind die Abläufe für (m, n) und (m − (q − 1) · n, n) identisch. Sie liefern den gleichen größten gemeinsamen Teiler und benötigen die gleiche Anzahl von Schritten. 2 1.1.2 Ein Programm für den euklidischen Algorithmus In der Informatik sind Programmierfertigkeiten unerläßlich und der Zusammenhang zwischen Algorithmen und Programmen von zentraler Bedeutung. Daher soll schon in diesem Unterabschnitt ein Programm für den euklidischen Algorithmus vorgestellt werden. Es ist als eine erste Einführung in die Programmiersprache C gedacht. Doch zunächst die folgende Überlegung: Im Algorithmus von Tabelle 1.1 sind ganz allgemein m und n als Anfangswerte angegeben. In einem Programm muß jedoch konkretisiert werden, wo diese Werte herkommen. Dafür gibt es verschiedene Möglichkeiten: • Im Programm als feste Werte angegeben. • Von einem anderen Stück des gleichen Programms. • Von einem anderen, getrennt ablaufenden Programm. • Aus einer Datei. • Von einem menschlichen Benutzer am Dialoggerät. Ebenso gibt es verschieden Möglichkeiten für die Entscheidung, was mit dem berechneten ggt(m, n) geschehen soll. Für das zu entwickelnde Programm soll festgelegt werden, daß ein menschlicher Benutzer am Dialoggerät die Werte m und n über die Tastatur eingibt und den berechneten größten gemeinsamen Teiler auf dem Bildschirm als Ausgabe erhält. Tabelle 1.2 zeigt, wie das durch eine einfache Erweiterung des ursprünglichen euklidischen Algorithmus erreicht werden kann. Ein graphische Darstellung des erweiterten Algorithmus als Flußdiagramm ist auf Seite 156 zu finden. Es soll nun ein C-Programm angegeben und untersucht werden, das den euklidischen Algorithmus mit Ein- und Ausgabe realisiert. Tabelle 1.3 zeigt das Programm. Die Zeichen zwischen den Zeichenfolgen /* und */ sind Kommentare und dienen nur der besseren Lesbarkeit. Sie haben auf das Programm keinen Einfluß. Die erste wirksame Programmzeile ist #include<stdio.h> Mit der Steueranweisung include wird der Inhalt einer Datei, in diesem Fall der Datei stdio.h, dem Programmtext hinzugefügt. Die Ergänzungen aus stdio.h werden gebraucht, weil im folgenden Anweisungen zur Ein- und Ausgabe benutzt werden. Das eigentliche Programm beginnt mit der Anweisung main und ist in geschweifte Klammern eingeschlossen. Mit der Anweisung 1.1. DER EUKLIDISCHE ALGORITHMUS ' Algorithmus EEA (Euklid mit Ein- und Ausgabe) Es ist der größte gemeinsame Teiler zweier positiver natürlicher Zahlen m und n zu berechnen. 7 $ EEA1 [Eingabe Anfangswerte] Lies die Anfangswerte von m und n. (Eingabe und Wertzuweisung) EEA2 [Restbildung] Es wird m durch n geteilt; der Rest sei r. (Es gilt 0 ≤ r ≤ n) EEA3 [Ist Rest Null?] Wenn r = 0, gib den Wert von n als größten gemeinsamen Teiler aus; der Algorithmus endet. & EEA4 [Vertauschen] Setze m :← n und n :← r und gehe zu Schritt EEA2 zurück. % Tabelle 1.2: Euklidischer Algorithmus mit Ein- und Ausgabe int m, n, rest; werden die Variablen m, n und rest definiert. Diese Variablen können ganzzahlige Werte annehmen. Das Ende einer Anweisung wird durch ein Semikolon angegeben. Es folgen die ausführbaren Anweisungen des Programms. Mit printf("Bitte m eingeben: "); wird der in Anführungsstriche angegebene Text auf dem Bildschirm ausgegeben. Die Anweisung scanf("%d",&m); bewirkt das Einlesen des ersten Wertes von der Tastatur. In Anführungsstrichen wird das Format der Eingabe spezifiziert, %d bedeutet, daß die eingelesene Zeichenfolge als ganze Zahl zu interpretieren ist. Mit dem zweiten Parameter – &m – wird festgelegt, daß die Variable m diese Zahl als Wert erhält. In Abschnitt 2.8 wird erläutert, warum hier nicht die Variable m direkt, sondern die Adresse &m dieser Variablen angegeben werden muß. Es folgt eine if-Anweisung. Die Bedingung in runden Klammern wird geprüft und, falls sie zutrifft, also in diesem Fall m einen nicht positiven Wert hat, werden die in geschweiften Klammern stehenden nachfolgenden Anweisungen ausgeführt. Sie bewirken, daß eine Fehlermeldung auf dem Bildschirm erscheint und dann – mit exit(0) – das Programm beendet wird. Die nächsten sieben Zeilen des Programms bewirken auf die gleiche Weise die Eingabe und Prüfung des zweiten Wertes (n). Im Rest des Programms ist der eigentliche euklidische Algorithmus enthalten. Mit der Anweisung 8 KAPITEL 1. EINFÜHRENDE BEISPIELE rest = m % n; wird der Wert von m durch den Wert von n ganzzahlig geteilt und der Variablen rest der Rest als Wert zugewiesen. Es folgt eine while-Anweisung. Die Bedingung in runden Klammern – Rest ungleich Null – wird geprüft und solange sie wahr ist, werden die in geschweiften Klammern stehenden nachfolgenden Anweisungen ausgeführt: m erhält den gleichen Wert wie n, n erhält den aktuellen Rest als Wert und es wird ein neuer aktueller Rest gebildet und in rest festgehalten. Diese Anweisungen bilden die while-Schleife. Sie wird erst verlassen, wenn der Rest Null ist, also der Algorithmus endet. Zum Schluß wird mit der Anweisung printf("ggt(m,n) = %d\n",n) der gefundene größte gemeinsame Teiler ausgegeben. Im Format, das auch hier wieder zwischen den Anführungsstrichen steht, wird mit %d angegeben, daß an dieser Stelle ein ganzahliger Wert ausgegeben werden soll. Der Rest des Formats wird so ausgegeben, wie er im Programm steht. Mit ,n wird spezifiziert, daß der Wert der Variablen n die auszugebende ganze Zahl ist. Für weitere Einzelheiten des Programmierens in C wird auf Kapitel 2 „Programmieren I: Sequentielle Programme“ verwiesen. 1.1. DER EUKLIDISCHE ALGORITHMUS 9 ' /***************************************************************/ /* Programm EUKLID */ /* */ /* Liest zwei natuerliche Zahlen ein, berechnet den */ /* groessten gemeinsamen Teiler und gibt diesen aus. */ /***************************************************************/ #include <stdio.h> $ main() { int /* /* } & m, n, rest; Eingabe und Prüfung der Ausgangswerte printf("Bitte m eingeben: "); scanf("%d", &m); if (m <= 0) { printf(" m ist nicht positiv\n"); exit(0); }; printf("Bitte n eingeben: "); scanf("%d", &n); if (n <= 0) { printf(" n ist nicht positiv\n"); exit(0); }; Berechnung ggt und Ausgabe rest = m % n; /* 1. Divisionsrest berechnen while (rest != 0) { m = n; /* Vertauschen und n = rest; rest = m % n; /* Rest bilden }; printf("ggt(m,n) = %d\n", n); /* Ausgabe Ergebnis */ */ */ */ */ */ Tabelle 1.3: Programm EUKLID (Berechnung des größten gemeinsamen Teilers) % 10 KAPITEL 1. EINFÜHRENDE BEISPIELE 1.1.3 Effizienz des euklidischen Algorithmus Hier soll Effizienz gleichgesetzt werden mit Ausführungszeit. Andere Beurteilungskriterien von Algorithmen wie Speicherbedarf, Verständlichkeit u. a. sollen nicht berücksichtigt werden. Die Ausführungszeit wird nicht in Sekunden oder anderen Zeiteinheiten gemessen, sondern durch die Anzahl Divisonsoperationen in Abhängigkeit von m und n. Es sei d(m, n) := Anzahl Divisionsoperationen bei der Berechnung von ggt(m, n) mittels Algorithmus E d(m, n) gibt auch die Anahl Schleifendurchläufe des Algorithmus an und wir fassen einen Schleifendurchlauf als einen Berechnungsschritt konstanten und von m und n unabhängigen Aufwands auf. Für das folgende nehmen wir m, n ∈ N+ an. BEC-Analyse (Best Case) Welches ist der bestmögliche Fall? Für alle m, n gilt d(m, n) = 1 ⇔ m = a · n mit a ∈ N d. h. 1. Für alle m und alle n gilt 1 ≤ d(m, n). 2. Für jedes m und alle n, die Teiler von m sind, gilt d(m, n) = 1. (1.1) WOC-Analyse (Worst Case) Welches ist der schlechtestmögliche Fall? Es seien m und n positive natürliche Zahlen. Ist m < n, so führt der erste Schleifendurchlauf zur Vertauschung von m und n. Es soll daher m ≥ n angenommen werden und der euklidische Algorithmus benötige k Divisionen, d. h. d(m, n) = k. Mit m1 = m und n1 = n ergeben sich die k Schritte, die zur Bestimmung von ggt(m, n) (m ≥ n) nötig sind, folgendermaßen: m1 m2 = q1 · n1 + r1 = q2 · n2 + r2 .. . mi = qi · ni + ri mi+1 = qi+1 · ni+1 + ri+1 .. . mk = qk · nk + rk ni > ri mi+1 = ni dabei gilt ni+1 = ri rk = 0 für i = 1, . . . , k − 1 für i = 1, . . . , k − 1 1.1. DER EUKLIDISCHE ALGORITHMUS 11 Zu gegebenem k wollen wir Zahlen mi(k) und ni(k) so bestimmen, daß für alle (m, n) mit d(m, n) = k gilt mi(k) ≤ m und ni(k) ≤ n, also (mi(k), ni(k)) := min{(m, n) | d(m, n) = k} für k = 1, 2, 3, . . . . Man mache sich klar, daß die hier benutzte komponentenweise Ordnung der Paare natürlicher Zahlen eine partielle Ordnung ist (siehe Seite 616) und demzufolge das gewünschte Minimum möglicherweise gar nicht existiert! Wenn es jedoch existiert, ist es eindeutig (warum?). Wenn die Zahlen mi(k) und ni(k) existieren, muß nach Anmerkung 1.2 außerdem gelten mi(k) = 1 · ni(k) + ri(k) mit ri(k) < ni(k). Im folgenden wird gezeigt, daß die gewünschten Zahlen mi(k) und ni(k) existieren und mit einer Rekursionsformel berechnet werden können. Dazu werden die Fälle k = 1 und k ≥ 2 unterschieden. Fall k = 1: Offenbar ist 1 = 1 · 1 + 0, also mi(1) = ni(1) = 1. Fall k ≥ 2: Satz 1.1 Für k = 2, 3, . . . existieren mi(k) und ni(k) und sind gegeben durch 1. 2. mi(2) = 3 und ni(2) = 2 sowie mi(k + 1) = mi(k) + ni(k) und ni(k + 1) = ni(k) + ri(k) . (1.2) (1.3) Beweis: Durch vollständige Induktion über k. Induktionsanfang: Es muß gelten r1 > r2 = 0 und n1 > r1 ≥ 1. Die kleinstmöglichen Werte sind r1 = 1 und n1 = 2, und es ergibt sich mi(2) = 3 und ni(2) = 2. Induktionsschritt (Schluß von k auf k + 1): Unter der Annahme, daß für κ = 2, 3, . . . , k mi(κ) und ni(κ) existieren und mi(κ) > ni(κ) gilt, existieren auch mi(k +1) und ni(k +1) und es gilt mi(k + 1) = mi(k) + ni(k) und ni(k + 1) = ni(k) + ri(k) (1.4) In der Tat: Es sei d(m, n) = k + 1 und m = q · n + r. Dann ist d(n, r) = k, also n ≥ mi(k) und r ≥ ni(k). D. h. m ≥ 1 · n + r ≥ mi(k) + ni(k) und n ≥ mi(k) = ni(k) + ri(k). Außerdem ist d(mi(k + 1), ni(k + 1)) = d(mi(k) + ni(k), ni(k) + ri(k)) = d(mi(k) + ni(k), mi(k)) = 1 + d(mi(k), ni(k)) = 1 + k. 2 Zusammenhang mit Fibonacci-Zahlen: Die Fibonacci-Zahlen4 sind definiert durch F0 := 0, F1 := 1 und Fk := Fk−1 + Fk−2 für 2 ≤ k . (1.5) Fibonacci, Leonardo (Leonardo von Pisa, Leonardo Pisano) ∗um 1170 in Pisa, †1240 ebd. Italienischer Mathematiker. Erster bedeutender Mathematiker des (mittelalterlichen) Abendlandes. Lernte auf Sizilien sowie auf Reisen nach Afrika, Byzanz und Syrien die indischen und arabischen Rechenmethoden kennen, die er weiterentwickelte und in dem umfangreichen Rechenbuch „Liber abbaci“ (1202) beschrieb. Kleinere Schriften behandeln geometrische und zahlentheoretische Fragen. Die Fibonacci-Zahlen werden von ihm im Liber abbaci bei einer Aufgabe zur Vermehrung von Hasen eingeführt. 4 12 KAPITEL 1. EINFÜHRENDE BEISPIELE Die Fibonacci-Zahlen sind ein Beispiel für eine rekursiv definierte Zahlenfolge: Es gibt Anfangswerte und eine Rekursionsvorschrift (Rekursionsformel), mit der ein Glied der Folge aus den Werten der vorangehenden Folgenglieder berechnet werden kann. Rekursive Definitionen beruhen auf dem gleichen Grundprinzip wie Beweise durch vollständige Induktion (siehe Beweis zu Satz 1.1) und werden daher manchmal auch induktive Definitionen genannt. Zur Berechnung der Werte einer rekursiv definierten Zahlenfolge und auch für theoretische Untersuchungen, ist es natürlich sehr wünschenswert, die Folgenglieder auch durch einen geschlossenen Ausdruck darstellen zu können. Für die Fibonacci-Zahlen ist das möglich, und in Gleichung 1.6 ist eine geschlossene Darstellung angegeben. Die Ungleichung 1.7 hängt eng damit zusammen und liefert eine wichtige Abschätzung. √ 1 + 5 1 b i ) mit Φ := b := 1 − Φ und Φ (1.6) Fi = √ · (Φi − Φ 2 5 √ i Φ 5 1 + Fi − √ < 1 mit Φ = ≈ 1.618 2 5 (1.7) Zum Beweis und zu weiteren Einzelheiten über Fibonancci-Zahlen siehe Knuth [Knut1997]. Tabelle 1.4 zeigt die exakten und die Näherungswerte der Fibonacci-Zahlen bis 30. Nun ist aber ni(2) = 2, ni(3) = ni(2) + ri(2) = 3 und ni(k) = ni(k − 1) + ri(k − 1) = ni(k − 1) + ni(k − 2) für 4 ≤ k. Daraus und aus mi(k) = ni(k + 1) sowie ri(k) = ni(k − 1) folgt für k = 2, 3, . . . # ri(k) = Fk ni(k) = Fk+1 (1.8) mi(k) = Fk+2 " ! Was bedeutet dieser Zusammenhang für die WOC-Analyse des euklidischen Algorithmus? Zur Vereinfachung soll für das folgende m ≥ n ≥ 2 angenommen werden. Zu jedem m existiert ein eindeutig bestimmtes k mit mi(k) ≤ m < mi(k+1) und es gilt d(m, n) < k+1, also d(m, n) ≤ k. Aus mi(k) ≤ m folgt nach Gleichung 1.8 Fk+2 ≤ mi(k) und nach Ungleichung 1.7 Φk+2 √ + α ≤ m mit |α| < 1 . 5 das heißt Φk+2 < √ 5 · (m + 1) . 1.1. DER EUKLIDISCHE ALGORITHMUS F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 F13 F14 F15 F16 F17 F18 F19 F20 F21 F22 F23 F24 F25 F26 F27 F28 F29 F30 = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 10946 17711 28657 46368 75025 121393 196418 317811 514229 832040 13 ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ 0.447 0.724 1.171 1.894 3.065 4.960 8.025 12.985 21.010 33.994 55.004 88.998 144.001 232.999 377.001 610.000 987.000 1597.000 2584.000 4181.000 6765.001 10946.002 17711.004 28657.006 46368.012 75025.016 121393.031 196418.047 317811.094 514229.156 832040.250 Tabelle 1.4: Fibonacci-Zahlen und Näherungswerte bis 30 Also5 d(m, n) ≤ k < ln √ 5 · (m + 1) − 2. ln Φ (1.9) Die Anzahl Schritte, die der euklidische Algorithmus zur Berechnung des größten gemein5 Zu Bezeichnungen und Eigenschaften von Logarithmen siehe Abschnitt C.1, Seite 629. im Anhang. 14 KAPITEL 1. EINFÜHRENDE BEISPIELE samen Teilers von m und n (n < m) braucht, ist stets durch eine Größe, die nicht schneller wächst als der Logarithmus von m, beschränkt. Mit der in Unterabschnitt 6.1.5, Seite 195, einzuführenden Notation läßt sich das als d(m, n) = O(ln m)) schreiben. Wenn n langsamer wächst als m – stets unter der Annahme n < m – ist die Anzahl Schritte O(ln n), denn analog zu 1.9 läßt sich zeigen √ ln 5 · (n + 1) 0 d(m, n) ≤ k < − 1. (1.10) ln Φ AVC-Analyse (Average Case) Es soll untersucht werden, wie das Verhalten des Euklidischen Algorithmus im Mittel ist. Um die Fragestellung zu erläutern und zu präzisieren, werde die Tabelle 1.5 betrachtet. Die Zeilen geben die Werte für m = 1, 2, . . . 15 und die Spalten die Werte für n (n < m) an. Die Tabellenelemente zeigen die Schrittanzahl zur Berechnung von ggt(m, n). Man sieht, daß d(13, 8) = 5 und d(m, n) < 5 in allen anderen Fällen. Was kann man als den Mittelwert der Werte in der Tabelle 1.5 ansehen? Es ist üblich, die Summe der Werte geteilt durch die Anzahl Werte als Mittelwert zu nehmen AV C(15) = 15 P m P d(m, n) m=1 n=1 120 ≈ 2.496774 . Um zu allgemeinen Aussagen über den Mittelwert zu kommen, müßte man das Verhalten von AV C(m) in Abhängigkeit von m untersuchen. Das soll hier nicht geschehen. Statt dessen sollen für Werte m = 1, 2, . . . , 100 die Größen BEC(m), W OC(m) und AV C(m) numerisch berechnet werden. Das Ergebnis ist in Abbildung 1.1 zu sehen. Wie schon hergeleitet, ist BEC(m) = 1 und W OC(m) eine Sprungfunktion mit logarithmischem Wachstum. Die Kurve für AV C(m) läßt logarithmisches Wachstum vermuten6 . Wie in der vergrößerten Darstellung von Abbildung 1.2 zu erkennen, wächst AV C(m) nicht monoton, sondern oszilliert. Die Oszillationen werden allerdings rasch geglättet, wie die Funktionswerte für den Bereich m = 101, . . . , 200 zeigen. 6 Für einen etwas anders definierten Mittelwert, nämlich Tm = 1 · [d(m, 1) + d(m, 2) + · · · + d(m, m)] , m ist logarithmisches Wachstum bekannt [Knut1998]. Tm ist der Mittelwert einer Zeile in der Matrix. 1.1. DER EUKLIDISCHE ALGORITHMUS 1 2 3 4 5 6 7 8 15 9 10 11 12 1 1 2 1 1 3 1 2 1 4 1 1 2 1 5 1 2 3 2 1 6 1 1 1 2 2 1 7 1 2 2 3 3 2 1 8 1 1 3 1 4 2 2 1 9 1 2 1 2 3 2 3 2 1 10 1 1 2 2 1 3 3 2 2 1 11 1 2 3 3 2 3 4 4 3 2 1 12 1 1 1 1 3 1 4 2 2 2 2 1 13 1 2 2 2 4 2 3 14 1 1 3 2 3 2 1 15 1 2 1 3 1 2 2 13 14 5 3 3 3 2 1 3 4 3 4 2 2 1 3 3 2 4 2 3 2 Tabelle 1.5: Schrittanzahl beim euklidischen Algorithmus 15 1 16 k 10 9 8 7 6 5 4 3 2 1 0 KAPITEL 1. EINFÜHRENDE BEISPIELE .. ........ .......... ... .... .. ... ... ... ... ... ... ... ... ... ... ... ... ... .... .. ... ... .... .. ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... .... .. ... ... .... .. ... ... ... ... ... ... ... ... ... ... ............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................ ...... k = W OC(m) +++++++++++ ++++++++++++++++++++++++++++++++++ +++++++++++++++++++++ +++++++++++++ ++++++++ k = AV C(m) +++++ +++ ·· · · ·· · · ·· · · ·· ·· · ··· · · ·· ··· · ·· ·· ·· ·· ·· ·· ·· ··· · ·· ·· ·· ·· ··· ··· ·· ··· · ··· ··· ···· ·· ··· ··· ++ ·· · · · · k = BEC(m) ··· · ·· · · ·· + + 1 10 20 30 40 050 060 070 080 Abbildung 1.1: BEC, WOC und AVC in Abhängigkeit von m 090 m 100 1.1. DER EUKLIDISCHE ALGORITHMUS 17 AV C(m) 5 4 .. ....... .......... ... ... .. .... .. ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... .... .. ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... .... .. ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... .... .. ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... .. ............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................ .. ·· ··· · ·· ·· 3 2 1 0 ·· ·· ·· · · ·· ·· ·· · ·· · · ·· ··· ·· · ·· · ··· · · ··· 1 10 101 110 · ·· ·· · · · ·· · · 20 120 ··· · ··· ·· ·· · · ··· ·· · · · · ·· 30 130 · ·· ·· · ·· ·· · ···· 40 140 · ··· ·· · ··· · · ·· ·· ·· · · ··· · ·· · · · · ·· 050 150 ·· · · · · ·· · ·· · 060 160 · · ·· ·· · · 070 170 Abbildung 1.2: AVC in Abhängigkeit von m · ·· ·· ·· ·· ·· ·· ·· · ·· · · ·· · · ·· ·· · · 080 180 ··· ·· ·· ··· · · · ·· · 090 190 ·· · m 100 200 18 KAPITEL 1. EINFÜHRENDE BEISPIELE 1.2 Sortieren durch Einfügen 1.2.1 Allgemeines zum Sortieren Sortieren: Die Elemente einer endlichen, nichtleeren Menge, der Sortiermenge, sollen der Reihe nach angeordnet werden! Sortieren bedeutet nicht: Nach Sorten einteilen. Die Elemente der Sortiermenge besitzen einen Sortierwert. Sie sind nach diesem Sortierwert anzuordnen, zu sortieren. Der Wertebereich, aus dem die Sortierwerte sind, bildet das Sortierkriterium, auch Sortierwertemenge genannt. Damit man sortieren kann, muß auf der Sortierwertemenge eine lineare Ordnung (siehe Anhang A „Mengenlehre“, Abschnitt A.5) definiert sein. Man kann nach dieser Ordnung aufsteigend oder absteigend sortieren. Der Sortierwert eines Elementes der Sortiermenge muß nicht eindeutig sein, d. h. unterschiedliche Elemente der zu sortierenden Menge dürfen den gleichen Sortierwert haben. Die wichtigsten Beispiele für Sortierwertemengen sind: • Ganze Zahlen • Reelle Zahlen • Wörter mit lexikographischer Ordnung (siehe Seite 114) Unter einem Sortierverfahren versteht man einen Algorithmus oder ein Programm, mit dem eine Menge sortiert werden kann. 1.2.2 Insertion Sort: Algorithmus und Programm Es gibt eine große Anzahl von Sortieralgorithmen. Ein naheliegendes Verfahren ist „Sortieren durch Einfügen“. Es ist mit dem Einordnen der Karten eines Spielers beim Kartenspiel zu vergleichen. Abbildung 1.3 zeigt ein Beispiel für die Arbeitsweise des Verfahrens. Die zu sortierende Menge ist {47, 10, 50, 48, 35, 3} und es wird angenommen, daß sie anfangs im Speicher in der Reihenfolge 47, 10, 50, 48, 35, 3 steht (1. Zeile, linke Spalte in Abbildung 1.3). Es wird ein sortiertes Anfangsstück der Länge 1 gebildet (1. Zeile, rechte Spalte, kursiv geschriebenes Anfangsstück). In den weiteren Schritten wird jeweils das Element, das unmittelbar hinter dem sortierten Anfangsstück steht, so weit nach links in das Anfangsstück heineingeschoben, bis es den Platz erreicht hat, in den es eingefügt werden muß. Die größeren Werte des Anfangsstücks werden nach rechts verschoben und das sortierte Anfangsstück so um eine Stelle verlängert. Der Algorithmus endet, wenn das Anfangsstück die ganze Sortiermenge umfaßt. In Tabelle 1.6 ist der Algorithmus IS für das Sortieren durch Einfügen angegeben. Er ist in 1.2. SORTIEREN DURCH EINFÜGEN 47 10 50 48 35 19 3 47 10 50 48 35 3 3 10 47 50 48 35 3 48 35 3 10 47 50 48 35 3 35 3 10 47 48 50 35 3 3 10 35 47 48 50 3 10 50 48 35 47 50 10 47 48 10 47 50 35 10 47 48 50 3 10 35 47 48 50 3 10 35 47 48 50 Abbildung 1.3: Beispiel für Sortieren durch Einfügen anderer Form als der euklidische Algorithmus (Tabelle 1.1) formuliert, und zwar in einem an C anglehnten Pseudocode. Die in Zeile 1 aufgeführte for-Schleife wird für die Werte n = 2, 3, . . . , m ausgeführt. Sie besteht aus den nachfolgenden, in geschweifte Klammern eingeschlossenen Anweisungen. Der Algorithmus ist als vollständiges C-Programm INSERTSORT in den Tabellen 1.7 und 1.8 zu sehen. Die zu sortierenden Werte werden in die Reihung sortierfeld eingelesen, darin durch Einfügen sortiert und zum Schluß ausgegeben. Eine graphische Darstellung des Programms als Struktogramm ist auf Seite 158 zu finden. 1.2.3 Effizienz von Sortieren durch Einfügen Was soll den Aufwand des Sortierens durch Einfügen messen? Im Algorithmus IS (Tabelle 1.6) sind eine äußere Schleife (for-Schleife) und eine innere Schleife (while-Schleife) zu erkennen. Die äußere Schleife wird immer m − 1 Mal durchlaufen. Das gilt auch für den Fall m = 1, in dem sie gar nicht durchlaufen wird! Der Aufwand für einen Durchlauf 20 KAPITEL 1. EINFÜHRENDE BEISPIELE ' Algorithmus IS (InsertionSort) Die Werte in den Plätzen P [1], P [2], . . . , P [m] werden durch Einfügen sortiert. Am Ende stehen die Werte in den Plätzen in aufsteigender Reihenfolge. & 1 for (n = 2; n ≤ m; n = n + 1) 2 { 3 j = n − 1; 4 aktuellerwert = P [n]; 5 while (j ≥ 1 ∧ P [j] > aktuellerwert) 6 { 7 P [j + 1] = P [j]; 8 j = j − 1; 9 }; 10 P [j + 1] = aktuellerwert; 11 }; % Tabelle 1.6: Algorithmus Sortieren durch Einfügen der äußeren Schleife besteht aus einem konstanten Teil und dem Aufwand für die innere Schleife. Die innere Schleife führt bei jedem Durchlauf der äußeren Schleife zu mindestens einem und höchsten n − 1 Tests (Zeile 5). Die Zahl der Vertauschungen (Zeile 7) und der Subtraktionen (Zeile 8) ist um 1 geringer als die Zahl der Tests. Es ist demnach sinnvoll, den Aufwand des Algorithmus IS durch die Anzahl T (m) der Tests für die innere Schleifen bei m zu sortierenden Werten zu messen. BEC-Analyse: Für jeden Durchlauf der äußeren Schleife wird der Test der inneren Schleife immer nur einmal ausgeführt. Das ist genau dann der Fall, wenn die P [1], P [2], . . . , P [m] von Anfang an aufsteigend sortiert sind. (1.11) T (m) = m − 1 WOC-Analyse: Im schlechtesten Fall wird der Test der inneren Schleife immer n − 1 Mal ausgeführt. Das ist genau dann der Fall, wenn jedes neue Element, das in das sortierte Anfangsstück eingefügt werden soll, ganz an den Anfang geschoben werden muß, also kleiner als alle Werte des sortierten Anfansgstücks ist. Das ist also genau dann der Fall, wenn die P [1], P [2], . . . , P [m] (2 ≤ m) von Anfang an absteigend sortiert sind. T (m) = m X n=2 (n − 1) = m−1 X n=1 n $ 1.2. SORTIEREN DURCH EINFÜGEN T (m) = 21 m · (m − 1) 2 (1.12) AVC-Analyse: Es wird nur eine Plausibilitätsbetrachtung durchgeführt: Bei „zufälliger“ Sortierreihenfolge der P [1], . . . , P [m] am Anfang muß jeder Wert mit der Hälfte der vor ihm stehenden und schon sortierten (dem sortierten Anfangsstück) verglichen werden. Dann ergibt sich m m−1 X n−1 X n T (m) = = 2 2 n=2 n=1 T (m) = m · (m − 1) 4 (1.13) Auch der Durchschnittswert der Sortierzeit wächst quadratisch mit der Anzahl der Sortierelemente. Beispiel 1.2 Abbildung 1.4 zeigt einen Vergleich der Größen BEC(m), W OC(m) und AV C(m) für das Programm INSERTSORT bei 100 zu sortierenden Elementen. 2 22 KAPITEL 1. EINFÜHRENDE BEISPIELE ' /***************************************************************/ /* Programm INSERTSORT */ /* */ /* Liest bis zu 10000 natuerliche Zahlen ein und gibt sie */ /* in aufsteigend sortierter Reihenfolge aus. */ /* Die erste negative ganze Zahl beendet die Eingabe */ /* Zum Sortieren wird "Sortieren durch Einfuegen" */ /* benutzt. */ /***************************************************************/ #include <stdio.h> main() { int int sortierfeld[10001]; m, n, j, k; /* Setzen Anfangswerte for (m=0; m< 10000; m=m+1) { sortierfeld[m] = 0; }; */ /* /* Eingabe Sortierwerte printf("Bitte Sortierwert eingeben: "); */ scanf("%d", &k); sortierfeld[0] = k; if (sortierfeld[0] < 0) { printf("Keine Sortierung\n"); exit(0); }; m = 0; while (sortierfeld[m] >= 0 && m < 10000) { m = m+1; printf("Bitte Sortierwert eingeben: "); scanf("%d", &k); sortierfeld[m]=k; }; */ /* & $ */ Tabelle 1.7: Programm INSERTSORT (Sortieren durch Einfügen) – Teil 1 % 1.2. SORTIEREN DURCH EINFÜGEN ' /* Sortieren durch Einfuegen m = m-1; for (n=1; n<=m; n=n+1) { j = n-1; k = sortierfeld[n]; while (j >= 0 && sortierfeld[j] > k) { sortierfeld[j+1] = sortierfeld[j]; j = j-1; }; sortierfeld[j+1] = k; }; Ausgabe for (n=0; n<=m; n=n+1) { printf("%4d\n", sortierfeld[n]); }; /* 23 */ $ */ & } Tabelle 1.8: Programm INSERTSORT (Sortieren durch Einfügen) – Teil 2 % 24 T (m) 5000 4800 4600 4400 4200 4000 3800 3600 3400 3200 3000 2800 2600 2400 2200 2000 1800 1600 1400 1200 1000 800 600 400 200 0 KAPITEL 1. EINFÜHRENDE BEISPIELE T (m) = W OC(m) ... ....... ......... .... ... ... ... ... ... ... ... ... ... ... ... .... .. ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... .... .. ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... .... .. ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... .... .. ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... .... .. ... ... ... ... ... ... ... ... . .............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................. ... + + + + + + + + + + + + + + + + + + + + + + + + + + T (m) = AV C(m) + + + + ·· + · + · + ·· + · + · + ·· + · + ·· + · + · + ·· + · · ++ ·· · + · ++ ·· · + · ·· ++ · + ·· ++ ·· + · · ++ ·· · + · + · ·· ++ · + · · ++ ··· · ++ · + ·· + ··· ++ · + · + ·· +++ ···· + · T (m) = BEC(m) · + · +++ · · · · · · · + + + ··· ++ + + + · · · · · · m + · + · + · · + · + · + ·· + ·· + ··· ···· ++++ + 1 10 20 30 40 050 060 070 080 090 100 Abbildung 1.4: BEC, WOC, AVC für Sortieren durch Einfügen 1.3. EIN KOCHREZEPT 1.3 25 Ein Kochrezept Die bisher behandelten Beispiele betrafen klare sequentielle Algorithmen ohne parallele Abläufe und ohne zeitliche Nebenbedingungen. Das folgende Beispiel eines Kochrezepts zeigt, daß diese Bedingungen nicht immer gegeben sind. Das Rezept lautet: Zutaten: 3 l kalte MilĚ, 1 PŁĘĚen Puddingpulver, zwei gut gehŁufte EğlŽĎel ZuĘer, 1 Ei. 4 Zubereitung: Man nehme 6 EğlŽĎel von der MilĚ und verquirle damit daŊ Puddingpulver, den ZuĘer und daŊ Eigelb deŊ EiŊ. Man bringe die MilĚ zum KoĚen, gebe dann daŊ verquirlte Puddingpulver hinzu und laĄe alleŊ unter R§hren kurz aufkoĚen. DaŊ zu Ćeifem SĚnee gesĚlagene Eiweiğ wird sofort naĚ dem KoĚen unter die Speise gehoben und in die GlŁser gef§llt.7 In Tabelle 1.9 ist das Rezept als Algorithmus nach dem Muster des euklidischen Algorithmus auf Seite 4 formuliert. Dabei fallen verschiedene Dinge auf. Zunächst einmal wird deutlich, daß in der ursprünglichen Rezeptbeschreibung ein hohes Maß an implizitem Wissen steckt. Ein Roboter könnte Schritt CP7 des Algorithmus so verstehen, daß umzurühren ist, aber möglicherweise nicht wissen, daß die Kochplatte einzuschalten ist. In diesem Fall ist der Algorithmus zu verfeinern und die einzelnen Schritte sind in detailliertere Anweisungen aufzuteilen. Auf diese schrittweise Verfeinerung wird in Kapitel 5, Unterabschnitt 5.1.2, näher eingegangen. Weiter ist in dem Algorithmus nicht zu erkennen, daß Zeitbedingungen einzuhalten sind. So muß z. B. Schritt CP6 unmittelbar auf Schritt CP5 folgen, da sonst die Milch wieder kalt wird. Schließlich ist es möglich, bestimmte Schritte gleichzeitig auszuführen. Z. B. kann man die Milch zum Kochen aufsetzen, wenn man die Verquirl-Milch abgenommen hat, und, während sie aufkocht, das Ei trennen. Diese mögliche Gleichzeitigkeit – in der Informatik spricht man von Parallelität – wird durchaus beim Kochen ausgenutzt. Abbildung 1.5 zeigt, wie der Algorithmus CP „Cremepudding“ zu ergänzen ist, wenn Zeitbedingungen und Parallelitätseigenschaften berücksichtigt werden sollen. In der Abbildung sind die Schritte CP1 - CP9 angegeben und durch Pfeile verbunden. Die Bedeutung der Pfeile ist: Die Aktion am Pfeilanfang muß beendet sein, bevor die Aktion am Pfeilende begonnen werden darf. Diese Relation zwischen Aktionen ist transitiv, z. B. darf CP8 nicht begonnen werden, ehe CP3 beendet ist. Parallel dürfen die Aktionen ausgeführt werden, von denen keine ein (direkter oder indirekter) Vorgänger der anderen ist, z. B. Milch aufkochen (CP5), Verquirlen (CP3) und Eischnee schlagen (CP4). Da Frakturschrift nicht mehr als allgemein bekannt vorausgesetzt werden kann, hier die Transskription: Zutaten: 3 l kalte Milch, 1 Päckchen Puddingpulver, zwei gut gehäufte Eßlöffel Zucker, 1 Ei. 7 4 Zubereitung: Man nehme 6 Eßlöffel von der Milch und verquirle damit das Puddingpulver, den Zucker und das Eigelb des Eis. Man bringe die Milch zum Kochen, gebe dann das verquirlte Puddingpulver hinzu und lasse alles unter Rühren kurz aufkochen. Das zu steifem Schnee geschlagene Eiweiß wird sofort nach dem Kochen unter die Speise gehoben und in die Gläser gefüllt. 26 ' KAPITEL 1. EINFÜHRENDE BEISPIELE Algorithmus CP (Cremepudding) Aus 34 l kalter Milch, einem Päckchen Puddingpulver, 2 gut gehäuften Eßlöffeln Zucker und einem Ei soll ein Cremepudding für 4 Personen zubereitet werden. $ CP1 [Verquirl-Milch] Es sind 6 Eßlöffel von der Milch abzunehmen. CP2 [Ei trennen] Ei in Eigelb und Eiweiß trennen. CP3 [Verquirlen] Die 6 Eßlöffel Milch (CP1), das Puddingpulver, den Zucker und das Eigelb (CP2) verquirlen. CP4 [Eischnee] Das Eiweiß (CP2) zu steifem Schnee schlagen. CP5 [Milch aufkochen] Die restliche Milch auf den Herd stellen und dort stehen lassen, bis sie kocht. CP6 [Puddingpulver zur Milch] Das verquirlte Puddingpulver (CP3) in Milch (CP5) geben. CP7 [Mischung aufkochen] Die mit dem Puddingpulver vermischte Milch (CP6) solange umrühren, bis sie gerade aufkocht. CP8 [Eiweiß hinzufügen] Das geschlagene Eiweiß (CP4) sofort unter die Speise (CP7) heben. CP9 [Abfüllen] Den fertigen Cremepudding (CP8) in Gläser geben. Der Algorithums &endet. Tabelle 1.9: Algorithmus Cremepudding % Eine Darstellung wie die von Abbildung 1.5 heißt Präzedenzgraph (precedence graph). Es ist ein gerichteter Graph (Digraph). In diesem Digraphen darf es keine Kreise geben, da es sonst Aktionen gäbe, die beendet sein müssen, bevor sie begonnen werden dürfen. Daß einige Aktionen unmittelbar aufeinanderfolgen müssen, ist im Präzedenzgraphen der Abbildung 1.5 durch die Angabe R! gekennzeichnet, die an einigen Pfeilen steht. R! steht für „Realzeitbedingung“. Realzeitbedingungen dieser und anderer Art spielen in der Informatik und ihren Anwendungen eine wichtige Rolle, werden aber in diesem Buch nicht weiter behandelt. 1.3. EIN KOCHREZEPT 27 Algorithmus PCP [Paralleler Cremepudding] Aus 34 l kalter Milch, einem Päckchen Puddingpulver, 2 gut gehäuften Eßlöffeln Zucker und einem Ei soll ein Cremepudding für 4 Personen zubereitet werden. PCP1 [CP mit Reihenfolgeangaben] Man führe die in Algorithmus CP angegebenen Schritte nach dem folgenden Zeitdiagramm aus: CP2 [Ei trennen] CP1 [Verquirl-Milch] ... .............. .. ............... ....... ... .............. ....... ... ............... ....... ............... ....... ... . . . . . . . . . ............... . ... ............... ....... ... ............... ........ .............. ....... ... ............... ....... ... ............... ........ . . . . . . ... ............... ....... ............... ....... ....... .. .............. ............... ... . ........ ........ .................. ...................... ... ........... .. CP3 [Verquirlen] CP5 [Milch aufkochen] ... ............ ... .............. ............... ... ............... ... .............. . . . . . . . . . . . . . ... .... ............... ... ............... ... .............. ............... ... ............... . . . . . . ... . . . . . . . . ... ............... .......... ............... ....... . ............... ... .................................. .. R! CP6 [Puddingpulver zur Milch] ... ... ... ... ... ... ... ... ... ......... ....... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... .. .......... ....... ... CP4 [Eischnee] ...... ...... ..... ...... ..... . . . . ..... ..... ...... ...... ..... . . . . . ...... ...... ...... ...... ..... . . . . ..... ..... ...... ...... ..... . . . . . . ...... ..... .......... ..... .......... ...... .......... ..... . . .......... . . .......... ..... .......... ..... .......... ...... .......... ..... .......... ...... . . .......... . . . .......... ..... .......... ...... .......... ...... .......... ..... .......... .. ............. . ............... . . . . . . . ............ ....... R! CP7 [Mischung aufkochen] R! CP8 [Eiweiß hinzufügen] ... .. ... ... ... ... ... ... ... . .......... ....... ... CP9 [Abfüllen] Abbildung 1.5: Algorithmus Paralleler Cremepudding Aufgaben Aufgabe 1.1 Erweitern und modifizieren Sie das Programm EUKLID (Tabelle 1.3, Seite 9) so, daß es am Schluß außer ggt(m, n) auch d(m, n) ausgibt und zusätzlich für alle gül- 28 KAPITEL 1. EINFÜHRENDE BEISPIELE tigen Eingaben d(m, n) = d(n, m) gilt. d(m, n) ist die Anzahl Divisionsoperationen des Programmlaufs. Literatur Literatur zum Thema „Sortieren“ ist in Abschnitt 11.5 angegeben. Zum euklidischen Algorithmus siehe Cormen/Leiserson/Rivest [CormLR1990], Abschnitt 33.2; Knuth [Knut1997], Abschnitt 1.1, und Knuth [Knut1998], Abschnitte 4.5.2 und 4.5.3 (mit interessanten historischen Anmerkungen); Weiss [Weis1995], Abschnitt 2.4 . Der euklidische Algorithmus ist der Einstieg in das reizvolle und schwierige Gebiet der Algorithmen für ganzzahlige Probleme. Einführende Betrachtungen zu Primzahlen und zur Bestimmung ganzzahliger Nullstellen (diophantische Gleichungen) findet man bei Kowalk ([Kowa1996], S. 549-587). Ausführlicher wird das Gebiet in Cormen/Leiserson/Rivest [CormLR1990], Kapitel 33, behandelt. Kapitel 2 Programmieren I: Sequentielle Programme 2.1 Schichtenaufbau eines Rechensystems Unter einem Rechensystem soll die Gesamtheit des Rechners mit seinen verschiedenen elektronischen, elektrischen und mechanischen Komponenten und Geräten (die Hardware) und seinen Programmen und Daten (die Software) verstanden werden. Moderne Rechensysteme sind sehr komplex. Übersicht und Ordnung läßt sich durch eine Einteilung in Schichten erreichen. Dazu ist es zweckmäßig (siehe auch [Tane1990]), den Begriff einer Hierarchie virtueller Maschinen einzuführen und zu benutzen. Eine Maschine führt Anweisungen aus einer wohldefinierten, die Maschine charakterisierenden Sprache aus. Eine korrekte Folge von Anweisungen der Sprache heißt Programm für die Maschine, die Ausführung dieser Anweisungen auf der Maschine ist ein Programmablauf. Eine Maschine heißt virtuell, wenn sie durch ein Programm, das auf einer anderen Maschine abläuft, realisiert wird. Virtuelle Maschinen können auf zwei Arten realisiert werden: Durch Interpretation oder durch Übersetzung. Übersetzung wird auch Compilierung genannt. Seien M, M’ und M” Maschinen und L, L’ und L” die zugehörigen Sprachen. Man spricht von Interpretation, wenn Programme in der Sprache L durch ein auf M’ ablaufendes und in L’ formuliertes Programm (Interpreter) ausgeführt (interpretiert) werden (Abbildung 2.1). Man spricht von Übersetzung, wenn Programme der Sprache L von einem auf M” laufenden und in L” formulierten Programm (Übersetzer, Compiler) in ein Programm der Sprache L’ umgewandelt (übersetzt) werden. Dieses Programm wird (eventuell zeitlich verzögert) auf M’ ausgeführt (Abbildung 2.2). Ein zu übersetzendes (interpretierendes) Programm wird Quellprogramm (source program) genannt. Übersetzungen und Interpretationen müssen genau das bewirken, was 29 30 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME Maschine M Sprache L - Maschine M’ Sprache L’ Abbildung 2.1: Interpretation Maschine M Sprache L Maschine M” Sprache L” Maschine M’ Sprache L’ Abbildung 2.2: Übersetzung in dem Quellprogramm beabsichtigt war, sie müssen „semantisch treu“ sein. Das Konzept der virtuellen Maschine soll jetzt auf Schichten von Rechensystemen angewendet werden. Tabelle 2.1 zeigt den Aufbau eines Rechensystems als Hierarchie von Maschinen und Sprachen. Die Basisschicht, auf der alle anderen aufbauen, ist die Hardwaremaschine, auch reale Maschine genannt. Auf ihr werden Mikroprogramme ausgeführt, die Maschinenprogramme der zweiten Schicht, auch konventionellen Maschine genannt, interpretieren. Die konventionelle Maschine ist virtuell. Sie ist das, was in der Systemprogrammierung als „Hardware“ angesehen und in den Handbüchern über „Maschinenbefehle“ beschrieben wird. Es ist möglich und auch getan worden, durch Mikroprogrammierung einer Hardwaremaschine unterschiedliche konventionelle Maschinen zu realisieren. Auf einer konventionellen Maschine werden Programme einer Betriebssystem-Maschine Betriebsart Interpretation Interpretation Interpretation Übersetzung Übersetzung .. ............... Interpretation 2.1. SCHICHTENAUFBAU EINES RECHENSYSTEMS Maschine Sprache läuft auf Hardwaremaschine (Register, Mikroprogrammsprache ——— Werk(e) für Rechenoperationen, Busse, Speicher, Ein/Ausgabeschnittstellen, periphere Geräte usw.) Konventionelle Maschine Maschinenprogrammsprache Hardwaremaschine Betriebssystem-Maschine Eingeschränkte MaschinenKonventionelle Maschine programmsprache mit Betriebsbefehlen Assembler-Maschinen Assemblersprachen Hardwaremaschine (Mikroassembler), Konventionelle Maschine, Betriebssystemmaschine Programmiersprachenhöhere Programmiersprachen: Betriebssystem-Maschine Maschinen COBOL, FORTRAN, C, Modula, Pascal, PL/1 usw. .. . . . . . . . . . . . . . . . . . . . . . . . . . . . .. .. . . . . . . . . . . . . . . . . . . . . . . . . . . . .. .. . . . . . . . . . . . . . . . . . . . . . . . . . . . .. Lisp, Prolog, APL, Basic u. a. ProgrammierprachenMaschinen Benutzerorientierte Maschinen Kommandosprachen, Parame- Programmiersprachentersprachen, graphische Spra- Maschinen chen (Fenstertechnik), Spezialsprachen z. B. Datenbanksprachen oder Sprachen für Expertensysteme Interpretation (Übersetzung) Tabelle 2.1: Sprach- und Maschinenschichten von Rechensystemen 31 32 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME interpretiert. Diese Programme bestehen aus Maschinenbefehlen der konventionellen Maschine und speziellen Anweisungen an das Betriebssystem, den Betriebsbefehlen. Auf einer konventionellen Maschine können mehrere, unterschiedliche Betriebsystem-Maschinen realisiert sein. Zum Betriebssystem siehe auch Unterabschnitt 4.3.1. Programme von Assembler-Maschinen sind in einer dem Menschen angepaßten Art formuliert und werden in eine maschinenangepaßte Form übersetzt. Die Anweisungen von Assembler-Programmen entsprechen im wesentlichen eins-zu-eins den Anweisungen der Maschinenprogramme, in die sie übersetzt werden. Assemblerprogramme werden kaum noch geschrieben; jedoch ist es für Informatiker wichtig, ein Verständnis für maschinennahe Programmierung zu haben. Deshalb wird in Beispiel 4.1, Seite 130, eine einfache konventionelle Maschine vorgestellt, mit der Assemblerprogrammierung geübt werden kann. Für Programmieraufgaben aller Art werden heutzutage fast ausschließlich höhere Programmiersprachen benutzt. Davon werden die meisten übersetzt, wie z. B. die in Tabelle 2.1 genannten. Auch C++ ist ein Beispiel für eine Sprache, die übersetzt wird. Einige höhere Programmiersprachen werden interpretiert, Beispiele sind in Tabelle 2.1 genannt. Gelegentlich gibt es für eine Programmiersprache sowohl Interpreter als auch Übersetzer. Für Anwendungsprobleme, um deren Lösung es letztlich beim Einsatz von Rechensystemen geht, werden überwiegend benutzerorientierte Maschinen eingesetzt. Ein interessanter Sonderfall ist Java, an dem auch die Übersetzung höherer Programmiersprachen allgemein etwas genauer untersucht werden soll. In den meisten Fällen wird ein Programm einer höheren Programmiersprache nicht direkt, sondern in zwei Schritten übersetzt. Siehe hierzu Abbildung 2.3. Der erste Schritt übersetzt das Quellprogramm in einen Zwischencode, der von dem Rechensystem, auf dem das übersetzte Programm letztlich ablaufen soll (der Plattform), unabhängig ist. Im zweiten Schritt wird das Zwischencode-Programm in die endgültige plattformabhängige, ablauffähige Form, das Binärprogramm übersetzt. Der Vorteil dieser Zweistufigkeit liegt darin, daß der Teil des Compilers, der aus Quellcode Zwischencode erzeugt, häufig Front End genannt, nur einmal gebaut werden muß und dann für jede Plattform nur der Teil, der Zwischencode in Binärcode übersetzt (Back End), hinzugefügt werden muß. Soll andererseits ein Compiler für eine neue Programmiersprache gebaut werden, genügt es einen Front End zu bauen und mit Back Ends, die schon existieren, an die einzelnen Plattformen anzupassen. Im allgemeinen geschehen die beiden Teilübersetzungen in einem Gesamtübersetzungslauf unmittelbar hintereinander und werden nach außen nicht sichtbar. Bei Java hat man nun dieses Vorgehen etwas modifiziert. Es wird zunächst durch Übersetzung ein Zwischencode erzeugt, der bei Java Bytecode heißt. Dieser wird nicht noch einmal übersetzt, sondern von einem Programm, daß man virtuelle Java-Maschine (Java Virtual Machine, JVM) nennt, direkt interpretiert. Solche Java-Interpreter gibt es inzwischen auf den meisten Plattformen. Die Interpretation entspricht dem Ablauf eines Binärprogramms bei konventioneller Übersetzung. Sie kann dementsprechend zu unter- 2.2. DATEIEN UND DATENBANKEN 33 .............................. .............................. .............................. ............. ............. ............. ........ ........ ........ ........ ........ ........ ...... ...... ...... ...... ...... ...... ...... ...... ...... ..... ..... ..... . . . . ..... . . . . . . . . . . . . . . . . .... .... .. .... .. . . . .... . . . . . . . . ... ... ... .. .. .. . . . . . . ... . . . . ... ... .. .. ... . ... . . . . . . ... ... ... . . . .. . . . . . ... ... ... . . . . . . .... . ... ... ... . . . ... ... .... ... .. .. . ... .. .. . ... . . . .. . . .. . ... . . . .................................................................................................................... ....................................................................................................................... ... .. ... . ...... .... ...... .... . . .. ... . . ... ... . .. ... . .. . . . . ... ... . . ... . .. . . . . ... ... ... ... ... ... ... ... ... ... .. ... .. ... ... .. .. ... ... .. ... ... ... ... ... . ... . ... . . . . . . ... ... .... .. .. .. .... .... ..... .... ... ... ..... ..... ..... ..... ..... ..... ...... ...... ...... ...... ...... ...... ...... ....... ....... ...... ...... ...... ........ . . . . . . . . . . . . . . . ......... . . . . . . . . . . . . ............ . ............ . ....................................... ................................ ............................... Übersetzung Quellprogramm Programm in Zwischencode Übersetzung Binärprogramm ............................................ ............................................ ....... ....... ......... ......... ...... ...... ....... ....... ...... ..... ...... ..... . . . . . . . . .... .... ... .... .... .... .... ... ... ... ... . . . ... ... . . . . . . ... ... . ... . . . . ... ... . .. . ... . . . ... . . . . .. . . .. .. ..... .... .. .. .. . .. .. . .. . .. .. ..... ......................................................................................................................... ...................................................................................................................... .. .. .. ... ... ... ... .. ... ... . ... .. ... ... . ... ... .. ... ... ... ... ... ... ... .. .. ... ... ... ... ... . . ... . . ... .. .... ... .... .... .... ... .... ..... .... .... ...... ...... ..... ..... ...... ....... ...... ...... . . . . . . . . . . . . . . . ......... . . . .......... . .......................................... ....................................... Übersetzung Quellprogramm (Java) virtuelle JavaMaschine Interpretierung JavaBytecodeMaschine Abbildung 2.3: Zwischencode und Bytecode schiedlichen späteren Zeitpunkten und an verschiedenen Orten auf allen Rechensystemen erfolgen, die einen Java-Interpreter aufweisen. Aus diesem Grund eignet sich Java in besonderem Maße für den Einsatz in Netzen. Ein Nachteil ist der Effizienzverlust durch die Interpretation des Bytecodes. Im folgenden wird in einem kurzen Abschnitt erläutert, was Dateien und Datenbanken sind. Im Rest des Kapitels werden Aufbau und Benutzung höherer Programmiersprachen beispielhaft an der Programmiersprache C untersucht. Programmkonstrukte und Datentypen, die es in C nicht gibt, werden nur teilweise erläutert. 2.2 Dateien und Datenbanken Dateien (file) sind benannte Sammlungen von Daten, die über Programmläufe hinweg existieren. Man spricht von permanenter Speicherung (permanent storage). Es wird auch die Bezeichnung nicht flüchtig (persistent) benutzt. Daten, die nur während eines Programmlaufs existieren, heißen temporär (flüchtig, temporary). Dateien werden auf Medien gespeichert, die dauerhafte Lagerung zulassen (siehe Unterabschnitt 4.1.5, Seite 127). Sie werden in Verzeichnissen (directory), die oft hierarchisch aufgebaut sind, verwaltet. In dem Verzeichniseintrag einer Datei wird der Dateiname und weitere Daten wie Lage auf dem Träger, Zugriffsberechtigungen, Datum der letzten Änderung vermerkt. Oft ist es üblich, durch Endungen im Dateinamen etwas über Nutzung und Struktur der Datei aus- 34 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME zusagen. Zum Beispiel bezeichnet program.c im Allgemeinen eine C-Quelldatei, die aus abdruckbaren Zeichen besteht. Vor dem Aufkommen allgemeiner Vernetzung und der explosionsartigen Zunahme von Rechneranwendungen im privaten Bereich (Musik, Bilder, Texte u.ä.) wurden in erster Linie Programme in Quell-oder Objektformat und kommerziell-administrative Daten gespeichert. Programmdateien waren Bibliotheken in speziellen Formaten. Dateien für kommerzielladministrative Daten bestanden aus Sätzen (record): Kundensätze, Artikelsätze, Personalsätze, Patientensätze usw. Der Datenverwaltung des Bertriebssystems war der Aufbau aus Sätzen und deren Typ bekannt und allgemeine Zugriffsmethoden (access method) erlaubten das Suchen, Lesen und Speichern der Sätze. Das hat sich in der Zeit danach rasch sehr deutlich geändert. Zum einen reicht ein einfacher Aufbau aus Sätzen für die Vielzahl unterschiedlicher neuer Anwendungen nicht aus. Für Transport und Speicherung benutzt man statt dessen einfache Folgen von Bits oder Zeichen. Alles was an Struktur darüber hinaus gebraucht wird, ist nur für die entsprechenden Anwendungssysteme sichtbar. Zum anderen sind die Sätze der kommerziell-administrativen Datenverarbeitung geblieben. Mit je einer Datei für jede Klasse von Sätzen konnten jedoch die vielfältigen Verflechtungen und Beziehungen nur umständlich und schwierig bearbeitet werden. Deshalb hat man schon recht früh damit begonnen, Datenbestände aufzubauen, die Sätze unterschiedlicher Klassen und die Beziehungen zwischen diesen abbilden. Solche Datenbestände werden Datenbanken (data base) genannt. Das Programmsystem, das Datenbanken verwaltet, heißt Datenbanksystem (data base system). 2.3 Grundbegriffe Allgemeines: Die Programmiersprache C wurde 1972 bei den AT&T Bell Laboratories („Bell Labs“) von Ritchie 1 entwickelt. Es gibt eine Vielzahl von Lehrbüchern über C. Das Standardwerk ist Kernighan/Ritchie [KernR1988]. Die folgenden Ausführungen stützen sich an mehreren Stellen auch auf Lowes/Paulik [LoweP1995]. C-Programme: C-Programme und ihre Wirkung kann man sich im Grundsatz so vorstellen, daß es eine (virtuelle) C-Maschine gibt, auf der C-Programme direkt ablaufen (siehe Abbildung 2.4). Die wesentlichen Teile eine C-Programms wurden auf Seite 6 besprochen und sollen anhand des kleinen Programms in Tabelle 2.2 noch einmal wiederholt werden. • Kommentar. Kommentare sind zwischen /* und */ eingeschlossen. Ritchie, Dennis M. Amerikanischer Informatiker. Entwickelte zusammen mit Thompson (siehe Seite 142) das Betriebssystem Unix. Entwarf für die Entwicklung von Unix die Programmiersprache C, für die er den ersten Compiler baute. 1 2.3. GRUNDBEGRIFFE 35 .................................... ........ ...... ...... ..... ..... .... ... ... . . ... ... . ... .. . ... ... .... .. ... .. ... ........................................................................................................... ... ... .... ... ... ... .. . ... . . . ... . ... ... .... ... ..... ... ...... ..... . . . . . ....... .............. ................... ........ C-Programm ' & C-Maschine Abbildung 2.4: Grundschema für C-Programme /****************************************/ /* */ /* Programm ZEICHENZAHL */ /* */ /* Zaehlt die eingegebenen Zeichen */ /* und gibt die Anzahl aus */ /* */ /****************************************/ #include <stdio.h> main() { int zahl; zahl = 0; while (getchar() != ’\n’) { zahl = zahl + 1; }; printf("%d\n", zahl); } $ % Tabelle 2.2: Programm ZEICHENZAHL • Makro. Makros dienen zur Erweiterung des Programms durch Textersetzung oder auch zur Erweiterung des Programms durch Einfügen des Inhalts zusätzlicher Dateien, wie z. B. im Programm ZEICHENZAHL die Zeile #include <stdio.h>. • Funktionsdeklaration (auch Funktionsdefinition genannt)2 . 2 Streng genommen, sind Definitionen und Deklarationen in C zu unterscheiden, Deklarationen legen 36 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME Funktionen sind (in C) Unterprogramme (siehe Abschnitt 2.8). Sie müssen deklariert werden, bevor sie benutzt werden können. Im Programm ZEICHENZAHL wird z. B die Funktion getchar benutzt, mit der einelne Zeichen von der Tastatur eingelesen werden. Die Deklaration von getchar steht nicht explizit im Programm, sondern wird mit der Datei stdio.h hinzugefügt. • Variablendeklaration (auch Variablendefinition genannt).2 Auch Variablen (siehe Abschnitt 2.5) müssen deklariert werden, ehe sie benutzt werden. Im Program ZEICHENZAHL wird mit int zahl eine Variable für ganzzahlige Werte deklariert. • Anweisungen an die C-Maschine. Dies sind die ausführbaren Anweisungen, also die, die von der C-Maschine bearbeitet werden. Im Programm ZEICHENZAHL ist die erste ausführbare Anweisung zahl = 0. Mit ihr wird der Variablen zahl der Wert 0 zugewiesen. Es ist zweckmäßig, sich vorzustellen, daß die C-Maschine vor der Bearbeitung einer ausführbaren Anweisung in einem bestimmten Zustand ist und durch die Bearbeitung in einen neuen (im allgemeinen verschiedenen) Zustand überführt wird. Vom Programmieren zum Programmablauf: Programmieren, edieren ......................... .......... ....... ....... ..... ..... .... ... ... . . ... ... . ... ... .... ... .... .. . . .. . .......................... ................................ .. .. .. ....... ... .. . . ... . . . ... . ... ... ... ... .... ... ...... ..... . . ....... . . . .............. ................. ...... Quellprogramm in C C-Compiler, Binder u. a. Abbildung 2.5 zeigt die einzelnen .............................. ......... ...... ...... ..... ..... .... ... . ... . ... ... . ... .... ... .. .... .. . . .. . ........................... ................................ .. .. .. ...... ... .. . . ... . . . ... . ... ... ... ... .... .... ...... .... . . ....... . . . .............. ................. ...... Binärprogramm Hardware und Betriebssystem Abbildung 2.5: Vom Programmieren zum Programmablauf Schritte von der Programmierung über die Übersetzung bis zum Ablauf eines C-Programms. Sie gelten ganz ähnlich auch für Programme in den meisten anderen höheren Programmiersprachen. Ein (C-)Quellprogramm ist ein Text (Zeichenreihe). Er wird von einem Menschen (manchmal auch von einem Programm, einem Programmgenerator) erzeugt und in einer Quelldatei abgelegt. Durch die Übersetzung mit einem (C-)Compiler und die Bearbeitung mit einem Binder (vergl. Seite 144) wird aus dem Quellprogramm ein ablauffähiges Binärprogramm. Ein Binärprogramm ist ein Bitmuster. Es besteht aus Maschineneinen Typ fest, während Definitionen auch Speicherplatz reservieren [KernR1988]. In diesem Buch soll diese Unterscheidung jedoch nicht getroffen werden. 2.4. WERTE, VARIABLE, ZEIGER, NAMEN 37 befehlen (der konventionelle Maschine) und Betriebsbefehlen (siehe auch Unterabschnitt 4.3.1). Syntax für (C-)Programme: Unter der Syntax einer Programmiersprache versteht man einen Satz von Regeln, denen der Aufbau des Programmtextes genügen muß, damit der Übersetzer (Compiler) es als korrekt erkennt und ein Binärprogramm erzeugt. Fragen der Syntax sind im Compilerbau sehr wichtig. Wir wollen uns im folgenden mit Fragen der Syntax nicht weiter befassen. Semantik für (C-)Programme: Unter der Semantik (von C) versteht man die Regeln, die die Zustandsänderungen der (C-)Maschine steuern, wenn die Anweisungen eines (C-)Programms ausgeführt werden. „Semantik beschreibt die Wirkung der Anweisungen.“ Eine wichtige Frage in der Informatik ist: Welche semantischen Eigenschaften eines Programms kann man automatisch (d. h. mit Hilfe eines anderen Programms) aus dem Programmtext ableiten? Leider kann man die wichtigsten, z. B. ob ein Programm bei korrekten Eingabewerten immer hält (Halteproblem, siehe Abschnitt 5.2) nicht erkennen. Mit der Semantik von C wollen wir uns im folgenden ausführlich befassen, jedoch formlos (d. h. nicht formal). 2.4 Werte, Variable, Zeiger, Namen Eine Übersicht über Datentypen: In der Datenverarbeitung werden Werte (value) gleicher Art, d. h. Daten gleicher Art, zu einem Datentyp zusammengefaßt. Man spricht von Werten (Daten) des gleichen Typs. Es folgt eine Übersicht über verschiedene Arten von Datentypen. • Datentypen, die direkt in der konventionellen Maschine, in der „Hardware“, gegeben sind. Werden in Kapitel 3 „Darstellung von Daten durch Bitmuster“ behandelt. • Datentypen, die direkt in der Programmiersprache gegeben sind. Werden in diesem Abschnitt behandelt. Es sind ◦ Elementare Datentypen ∗ ∗ ∗ ∗ Ganze Zahlen Rationale Zahlen Zeichen und Zeichenreihen Wahrheitswerte ◦ Zeiger 38 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME ◦ Zusammengesetzte Datentypen ∗ Reihungen (array) ∗ Sätze (record) • Datentypen, die nicht direkt in der Programmiersprache gegeben sind ◦ Datentypen im Programm Diese Datentypen werden oft Datenstrukturen genannt. Sie werden in den Teilen III „Einfache Datenstrukturen“ sowie IV „Allgemeine Graphen“ behandelt. ∗ Listen ∗ Bäume ∗ Graphen ◦ Datentypen für Dateien und Datenbanken Hierauf wird in diesem Buch nur kurz eingegangen (siehe Abschnitt 9.4, Seite 296). Im folgenden werden Datentypen besprochen, die direkt in der Programmiersprache C gegeben sind. Auf andere Programmiersprachen wird nur am Rande eingegangen. Bei diesen Datentypen beziehen sich sowohl Zeiger als auch zusammengesetzte Datentypen auf schon bekannte Datentypen. Zeiger und zusammengesetzte Datentypen sind abgeleitete Datentypen. Die Werte der einzelnen Datentypen haben in einem Programmlauf unterschiedliche Ursprünge: • Sie werden im Programm direkt angegeben (Konstanten). • Sie werden während des Programmlaufs von außen eingegeben (Eingabegeräte, Dateien, Netz, Interprozeßkommunikation, Betriebssystemaufruf, Bibliotheksaufruf, Signalaufnahme usw.). • Sie werden während des Programmlaufs erzeugt („berechnet“), im allgemeinen aus anderen, schon vorhandenen Werten. Einige während des Programmlaufs berechnete Werte werden (auf unterschiedliche Weise) ausgegeben3 . Beispiel 2.1 (Programm DYNREIHUNG) Zur Erläuterung der Betrachtungen über Werte, Variable, Zeiger und Namen soll als Beispiel das Programm DYNREIHUNG vorgestellt werden. Es ist in den Tabellen 2.3 und 2.4 wiedergegeben. Das Programm liest Für einfache und häufig benutzte Formen der Eingabe und Ausgabe von Werten gibt es in C Standardfunktionen in der Standardbibliothek. Diese enthält noch weitere nützliche Klassen von Funktionen, zum Beispiel mathematische Funktionen. Eine vollständige Beschreibung der Standardbibliothek ist in Lowes/Paulik [LoweP1995] zu finden. 3 2.4. WERTE, VARIABLE, ZEIGER, NAMEN 39 eine Zahlenfolge n, a1 , a2 , . . . an ein. Dabei ist n die Anzahl der anschließend einzulesenden ganzen Zahlen ai . Von diesen werden Mittelwert, Varianz und Streuung berechnet E = V = σ = 1 n · 1 n · √ n P ai Mittelwert (E − ai )2 Varianz i=1 n P i=1 V Streuung Das Programm ermittelt auch die Folgenglieder, für die der Abstand zum Mittelwert maximal ist. Alle berechneten Werte werden ausgegeben. Die Zahlen a1 , a2 , . . . , an werden vom Programm eingelesen und in aufeinanderfolgenden Plätzen, in einer Reihung, gespeichert. Diese Plätze stehen nicht von Anfang an zur Verfügung, sondern werden während des Programmlaufes mit der Funktion malloc dynamisch angefordert und dem Programm zugewiesen. Die Adresse des ersten dieser Plätze wird in der Variablen zeiger gespeichert. Platz i der Reihung wird dann mit zeiger+(i-1) angesprochen. Mit der Anweisung free kann dynamisch zugewiesener Speicher wieder freigegeben werden. Weitere Einzelheiten zu Reihungen sind in Unterabschnitt 2.6.1 zu finden. 2 Variablen: Bei der Eingabe von Werten und bei der Berechnung neuer Werte während eines Programmlaufes muß es möglich sein, diese Werte „aufzubewahren“. Das geschieht mit Hilfe von Variablen. Eine Variable ist ein Speicherplatz (ein Behälter), der während des Programmlaufs Werte eines bestimmten Datentyps aufnehmen kann. Eine Variable hat einen Wert und eine Adresse und kann einen Namen haben. Siehe Abbildung 2.6. 40 ' KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME /**********************************************************/ /* */ /* Programm DYNREIHUNG */ /* */ /* Liest die Anzahl einzugebender */ /* int-Werte ein. */ /* */ /* Legt dynamisch eine Reihung */ /* entsprechnder Groesse ein. */ /* */ /* Liest die Werte in die Reihung ein. */ /* */ /* Berechnet Mittelwert, Varianz und */ /* Streuung. */ /* */ /* Gibt alle Werte mit maximalem */ /* Abstand zum Mittelwert aus. */ /* */ /**********************************************************/ #include <stdio.h> #include <malloc.h> #include <math.h> main() { int zahl, i; int *zeiger; int summe = 0; float mittel, streuung, varianz, max, r; float fsumme = 0.; & scanf("%d", &zahl); if (zahl <= 0) {printf("Anzahl ungueltig\n"); exit(0);}; zeiger = (int *) malloc(zahl * sizeof(int)); if (zeiger == NULL) {printf("Nicht genuegend Speicher vorhanden\n"); exit(0);}; for (i = 0; i < zahl; i=i+1) { scanf("%d", zeiger+i); summe = summe + *(zeiger+i); }; mittel = ((float)summe)/zahl; printf("Mittelwert = %f \n", mittel); $ % 2.4. WERTE, VARIABLE, ZEIGER, NAMEN 41 Wert einer Variablen: Der Wert wird auch Inhalt genannt. Er kann während eines Programmlaufs für verschiedene Dinge benutzt werden (Berechnungen, Tests, Ausgaben, Weiterleitung an andere Programmteile u. a.), ohne daß er dabei geändert wird. Man sagt, die Variable wird gelesen. Durch Berechnungen, Eingaben u. a. kann der Wert einer Variablen auch ' } & max = 0.; for (i = 0; i < zahl; i=i+1) { r = *(zeiger+i) - mittel; if (r < 0.) r = -r; fsumme = fsumme + r*r; if (r > max) max = r; }; varianz = fsumme/zahl; streuung = sqrt(varianz); printf("Varianz = %f \n", varianz); printf("Streuung = %f \n", streuung); for (i = 0; i < zahl; i=i+1) { r = *(zeiger+i) - mittel; if (r < 0.) r = -r; if (r == max) { printf("Zahl[%d] = %d |Zahl[%d] - Mittelwert| = %f\n", i+1, *(zeiger+i), i+1, max); }; }; $ % Tabelle 2.4: Programm DYNREIHUNG (Dynamischer Aufbau einer Reihung) – Teil 2 geändert werden. Man sagt, die Variable wird geschrieben. Die wichtigste Form der Wertänderung einer Variablen ist die Zuweisung (Wertzuweisung, assignment). Diese wird in C in der Form <variable> = <Ausdruck für den zuzuweisenden Wert>4 Es soll hier nicht allgemein festgelegt werden, was ein solcher Ausdruck ist. Oft ist es ein arithmetischer Ausdruck, der eine Zahl liefert. 4 42 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME ..... .... ... ....... .. ... ........ .. ........ .. ... . ..... .... ... ....... . .... . ... ....... .. . ........ ... ...... ... ... ...... ... 16708 ... .. ... ....... ..... .... .......................... ....... ... ..... . .. ..... .............................. ..... ...... ....... .. .... ... .. .... .. .... .. ... .... .. ... ... .. 113 ........ .. ........ .. ... . 113 .............................. ..... .. ..... .. ...... ............................. .... . . . . . . .......... ... .. ... .... .. ... ... ... ... ... ... ... .. 507 .. .. ....... .. .. ....... einwohner einwohner_zgr A. Variable einwohner hat den int-Wert 16708. B. Variable einwohner_zgr hat als Wert die Adresse der intVariablen einwohner. Abbildung 2.6: Werte, Variable, Zeiger, Namen geschrieben. Das Gleichheitszeichen heißt Zuweisungsoperator. Die Schreibweise ist in vielen anderen Programmiersprachen analog, nur wird oft ein anderes Zeichen (oder eine Kombination von Zeichen) als Zuweisungsoperator benutzt, z. B. := oder :←. Wertänderungen durch Zuweisungen sind in (sauber strukturierten) Programmen (für Menschen) gut erkennbar. Wertänderungen auf anderem Wege sind schwerer zu erkennen und sollten nach Möglichkeit vermieden werden. Wenn das nicht möglich ist, wie z. B. in C bei der Eingabe von Werten mittels der Funktion scanf, sollte der Programmtext auf die Wertänderung durch einen Kommentar oder auf andere Weise besonders hinweisen. Alle Werte, die eine Variable annimmt, sind vom gleichen Datentyp. Zu einem Zeitpunkt hat sie nur einen Wert. Dieser Wert kann unbestimmt sein. Im Beispielprogramm DYNREIHUNG sind z. B. zahl, streuung und fsumme Variable. zahl kann Werte vom Typ int annehmen, streuung und fsumme sind Variable für Werte vom Typ float. Alle drei Variablen werden durch eine Variablendeklaration (Variablendefinition) am Anfang des Programmtextes bereitgestellt. Zu Beginn des Programmlaufes haben zahl und streuung einen unbestimmten Wert. Die Variable zahl erhält während des Programmlaufs bei dem Eingabebefehl scanf einen definierten Wert. Bei der Variablen streuung passiert das erst, wenn ihr die Quadratwurzel der Varianz zugewiesen wird. Der Variablen fsumme ist bei der Deklaration ein Anfangswert (Vorbesetzungswert, Initialisierungswert), nämlich 0, zugewiesen worden und diesen Wert hat die Variable beim Beginn des Programmlaufes. Hat eine Variable (in C) einmal einen bestimmten Wert, dann wird ihr Wert während des Programmlaufs nicht mehr unbestimmt (siehe jedoch Abschnitt 2.8). 2.4. WERTE, VARIABLE, ZEIGER, NAMEN 43 Adresse und Name einer Variablen: Jede Variable, die durch eine Deklaration eingeführt wird, erhält bei der Deklaration einen Namen. Keine andere Variable kann den gleichen Namen haben (siehe jedoch Abschnitt 2.8). In einigen Programmiersprachen (nicht C) kann eine Variable mehrere Namen haben. Variable, die nicht über eine Deklaration eingeführt werden (s. u.), haben keinen Namen. Jede Variable hat eine eindeutig bestimmte Adresse, eine Art „Hausnummer“. In Abbildung 2.6 wird das durch Ellipsen angedeutet. Die Adresse der Variablen einwohner ist 113. Dieser Wert ist jedoch nur symbolisch zu sehen und nicht als wirkliche Zahl aufzufassen. Adressen können in einigen Programmiersprachen im Programm benutzt werden. Man spricht dann allgemein von Zeigern (pointer) oder Referenzen. Adressen weisen in den Programmiersprachen, in denen sie benutzt werden können, recht unterschiedliche Eigenschaften auf. Das folgende gilt speziell für C. Rechnerintern sind Adressen in C-Programmen natürliche Zahlen, die jedoch nicht nur vom Programm, sondern auch von der Rechnerarchitektur abhängen. Sie haben für den Programmierer keine explizite Bedeutung. Nur für Spezialisten und für Testzwecke macht es Sinn, Adressen auszugeben. Es macht jedoch Sinn und ist oft nützlich, Adressen als Verweise auf Variablen und damit auch auf die Werte, die diese enthalten, zu benutzen. Das ist in C möglich und gängige Programmierpraxis. In C gilt: Alle Werte einer Variablen gehören zum gleichen Datentyp (Ausnahme: union). Der Typ einer Variable bestimmt sich durch den Datentyp ihrer Werte. Der Typ einer Adresse bestimmt sich durch den Typ der Variablen, die zu der Adresse gehört. Ein Zeiger (pointer) ist (in C) eine Variable, deren Werte Adressen eines bestimmten Typs sind (Ausnahme void). In Abbildung 2.6 ist in Teil B ein Zeiger zu sehen. Sein Name ist einwohner_zgr, seine Adresse 507. Der Zeiger enthält als aktuellen Wert die Adresse der Variablen einwohner, nämlich 113. Generierung von Variablen: Es kommt in der Programmierung oft vor, daß in einem Programm Speicherplätze eines bestimmten Typs gebraucht werden, ihre Anzahl aber erst während des Ablaufs des Programms festgestellt wird. Einige Programmiersprachen, darunter C, kennen Anweisungen, mit denen zur Ablaufzeit Speicherplätze für Variablen eines Datentyps bereitgestellt werden können. Man spricht auch von dynamischer Erzeugung von Variablen. Solche Variablen haben eine Adresse, aber keinen Namen. Ein Beispiel in C ist in Tabelle 2.3 zu sehen. In der Programmzeile 44 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME zeiger = (int *) malloc(zahl * sizeof(int)); wird Speicherplatz mit der Funktion malloc angefordert. Die Größe des angeforderten Speicherbereichs wird durch zahl * sizeof(int) spezifiziert und die Anfangsadresse als Adresse vom Datentyp int der Zeigervariablen zeiger zugewiesen. Konstanten: Wie oben erwähnt, werden Werte von Datentypen in einem Programm oftmals explizit als Konstanten aufgeführt, z. B. bei Anfangswerten oder Wertzuweisungen im Programmlauf. Auch als Bestandteile von Ausdrücken treten sie oft auf. So kann zum Beispiel die Berechnung eines Kreisinhalts in Abhängigkeit vom Radius innerhalb eines Programms mehrfach durch die Anweisung inhalt = (radius*radius)*3.1416 erfolgen. Die konstanten Datenwerte müssen auch zur Ablaufzeit des Programms, bei compilierten Programmen also auch nach der Übersetzung, zur Verfügung stehen. Das kann so geschehen, als wäre die Konstante eine Variable, deren Wert sich nicht ändert, ein Literal. Konstanten können während des Programmlaufs aber durchaus auch anders zur Verfügung stehen, z. B. durch Maschinenbefehle erzeugt werden. Mit der 1 in der Zuweisung n = n + 1 ist das z. B. häufig der Fall. In einigen Programmiersprachen können Konstanten auf die gleiche Art wie Variablen deklariert werden. Wenn das der Fall ist, haben so eingeführte Konstanten analog zu Variablen einen (konstanten) Wert, eine Adresse und einen Namen. Der Name ist eindeutig (siehe jedoch Abschnitt 2.8). Die Adresse ist die (eindeutige) Hausnummer eines Speicherplatzes und kann von Zeigern als Wert angenommen werden. In C gibt es hierfür das Sprachelement const und mit der Anweisung const float pi=3.1416; kann die Konstante pi deklariert werden. Mit &pi steht im Programm dann auch die Adresse des Speicherplatzes, in dem der Wert 3.1416 steht, zur Verfügung. Zu konstanten Adressen siehe Unterabschnitt 2.5.5, speziell Tabelle 2.14. In C werden Konstanten oft mit der Textersetzung #define angegeben. So würde z. B. die Zeile #define PI 3.1416 im Kopfteil des Programms bewirken, daß bei der Übersetzung der Text PI überall im Programm durch den Text 3.1416 ersetzt wird. Entsprechend würde mit der Angabe #define TELOL 0441 der Text TELOL durch den Text 0441 ersetzt. Diese Form der Konstantenangabe ist nützlich. Man beachte jedoch, daß sie nicht einer Konstantendeklaration mittels const entspricht. 2.5. WERTEBEREICHE, OPERATIONEN, AUSDRÜCKE 2.5 2.5.1 45 Wertebereiche, Operationen, Ausdrücke Ganze Zahlen Datentypen für ganze Zahlen: Es gibt 4 Standardtypen mit Vorzeichen und 4 Standardtypen ohne Vorzeichen sowie zwei allgemeine Typen (siehe Tabelle 2.5). Es ist instal' & • • • • signed signed signed signed • • char int char short int int long int • • • • unsigned unsigned unsigned unsigned char short int int long int $ % Tabelle 2.5: Standardtypen ganzer Zahlen lationsabhängig, ob char = signed char oder char = unsigned char. Ebenso ist die Festlegung von int installationsabhängig, jedoch muß stets gelten signed short int ⊆ int ⊆ signed long int. Es gibt Mindestwertebereiche. Reale Compiler dürfen größere Wertebereiche aufweisen. Tabelle 2.6 zeigt im Vergleich die Mindestwertbereiche und die Typ signed char unsigned char signed short int unsigned short int signed int unsigned int signed long int unsigned long int Minimum Maximum größtes donar kleinstes donar −127 −128 127 127 0 0 255 255 −32767 −32768 32767 32767 0 0 65535 65535 −32767 −32768 32767 32767 0 0 65535 65535 −2147483647 −2147483648 2147483647 2147483647 0 0 4294967295 4294967295 donar: Rechner Sparc IPX mit GNU-C-Compiler (gcc, v2.7). Tabelle 2.6: Mindestwertebereiche ganzer Zahlen des Rechners „donar“, einer Sparc IPX mit GNU-C-Compiler (gcc, v2.7). Wo kommen die Werte für donar her? Es gibt installationsspezifische Parameter in der Datei limits.h. Tabelle 2.7 zeigt einen Auszug aus dieser Datei. 46 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME ' & CHAR_BIT CHAR_MAX CHAR_MIN UCHAR_MAX SCHAR_MAX SCHAR_MIN INT_MAX INT_MIN LONG_MAX LONG_MIN SHRT_MAX SHRT_MIN UINT_MAX ULONG_MAX USHRT_MAX = = = = = = = = = = = = = = = 8 127 -128 255 127 -128 2147483647 -2147483648 2147483647 -2147483648 32767 -32768 4294967295 4294967295 65535 $ % Tabelle 2.7: Ganze Zahlen bei donar (Auszug aus limits.h) Auch Zeichen sind in C ganze Zahlen, jedoch: Es wird empfohlen, in C Zeichen, d. h. Werte vom Typ char, nicht als ganze Zahlen aufzufassen, sondern sie als eigenen, nichtnumerischen Datentyp zu behandeln. Einzelheiten dazu sind in Unterabschnitt „Zeichen“, Seite 52, zu finden. Ganzzahlige Konstanten und ganzzahlige Ein-/Ausgabedaten: Konstanten werden als ganzzahlige Dezimalzahlen mit oder ohne Vorzeichen geschrieben. Zusätze können den Typ näher festlegen. Beispiele: -327519, 327519, +327519, 0, -1213456798l, 3111222333UL Führende Nullen dürfen, außer für den Wert 0, nicht geschrieben werden. Eine führende Null legt Oktalschreibweise statt Dezimalschreibweise fest. Bei der Eingabefunktion scanf und der Ausgabefunktion printf werden die Formatierungsanweisungen %d, %i und %u benutzt. Für Ergänzungen (Längenangaben, Typpräzisierungen a. a.) sei auf Kernighan/Ritchie [KernR1988] und Lowes/Paulik [LoweP1995] verwiesen. C läßt irritierende Merkwürdigkeiten zu, wie das Beispiel 2.2 zeigt. 2.5. WERTEBEREICHE, OPERATIONEN, AUSDRÜCKE 47 Beispiel 2.2 unsigned long int L; L = -1; printf("L (int) = %d\n", L); printf("L (unsigned) = %u\n", L); L = -1ul; printf("L (int) = %d\n", L); printf("L (unsigned) = %u\n", L); L L L L (int) = -1 (unsigned) = 4294967295 (int) = -1 (unsigned) = 4294967295 2 Anmerkung 2.1 # In C können ganzzahlige Werte auch in oktaler oder hexadezimaler Schreibweise angegeben werden. Es wird empfohlen, das nicht zu tun. Die oktale Schreibweise sollte überhaupt nicht und die hexadezimale Schreibweise ausschließlich für die Spezifizierung von Steuerzeichen und Bitmustern benutzt werden. " ! Steuerzeichen werden in Unterabschnitt 2.5.3 besprochen, zu Bitmustern in C siehe Abschnitt 3.8 im Kapitel 3 „Darstellungen von Daten durch Bitmuster“. C kennt ganzzahlige Werte auch als Aufzählungswerte (vom Typ enum). Siehe hierzu Unterabschnitt 2.5.6, Seite 59. 2 Ganzzahlige Operationen: • Ganze Zahlen (signed). Addition (+), Subtraktion (−) und Multiplikation (∗) sowie Vorzeichenwechsel sind ganzzahlig. Verhalten und Ergebnis bei Bereichsüberschreitungen sind unbestimmt. Ganzzahlige Division (/) und Rest (%) genügen der Bedingung m = (m/n)·n+m%n mit 0 ≤ |m%n| < |n|. Treten negative Operanden auf, darf der Rest negativ oder positiv sein und der Quotient ist nicht eindeutig bestimmt. Beispiel: 48 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME ◦ −8 = (−1) · 5 + (−3) ◦ −8 = (−2) · 5 + (+2) Bei donar gilt zusätzlich |(m/n) · n| ≤ |m| und damit sind ganzzahliger Quotient und Rest eindeutig bestimmt und der Betrag des Quotienten nicht vom Vorzeichen abhängig. Zu ganzahliger Division und Restbildung siehe auch Unterabschnitt 6.1.4, Seite 193. Das Verhalten bei Division durch 0 ist in C nicht festgelegt. Es führt bei vielen Rechnern (z. B. donar) zu Programmabbruch. Vergleichsoperationen sind <, <=, >, >=, == und !=. Sie sind zwischen allen Werten des zulässigen Bereichs definiert und liefern wahr oder falsch (siehe Unterabschnitt 2.5.4). • Natürliche Zahlen (unsigned). Die Operationen +, −, ∗ sowie die Vorzeichenwechsel werden grundsätzlich in modulob-Arithmetik ausgeführt (b ist die Größe des Zahlenbereiches). Es gibt daher keine Bereichsüberschreitungen. Zur Modulorechnung siehe Unterabschnitt 6.1.4, Seite 193. Division mit Divisor ungleich 0 und Rest sind eindeutig definiert. Das Verhalten bei Division durch 0 ist nicht festgelegt, führt in der Regel jedoch zu Programmabbruch. Die Vergleichsoperationen gelten wie für ganzzahlige Werte. Anmerkung 2.2 In C gibt es für die Potenzbildung weder bei ganzen noch bei rationalen Zahlen einen elementaren Operator. Statt dessen kann die Funktion pow (siehe [KernR1988] oder [LoweP1995]) benutzt werden. 2 Ganzzahlige Ausdrücke: Ganzzahlige Ausdrücke werden auf die übliche Art gebildet. Zu Vorrangregeln siehe die Literatur, [KernR1988] und [LoweP1995]. Ebenso zu eventuell notwendigen expliziten Typumwandlungen. Gemischte Ausdrücke, also Ausdrücke mit Operationen zwischen Werten verschiedener Datentypen, werden nach den folgenden Regeln ausgewertet 1. Kurze Operanden (short) werden stets in int bzw. unsigned int umgewandelt (integral promotion). 2. Jeder Ausdruck hat den höchstwertigen Typ seiner Operanden (siehe Tabelle 2.8). 2.5.2 Rationale Zahlen Datentypen für rationale Zahlen: Zum Arbeiten mit Zahlen, die keine ganzen Zahlen sind, gibt es in höheren Programmiersprachen eigene Datentypen. Sie heißen manchmal 2.5. WERTEBEREICHE, OPERATIONEN, AUSDRÜCKE 5 4 3 2 1 long und int gleich long double double float unsigned int / unsigned long int / long long 7 6 5 4 3 2 1 und int ungleich long double double float unsigned long long unsigned int int Tabelle 2.8: Prioritäten der Datentypen • float • double 49 • long double Tabelle 2.9: Standardtypen rationaler Zahlen real. In C gibt es drei Standard-Datentypen, sie sind in Tabelle 2.9 angegeben. Die Wertemengen dieser Datentypen sind durch Bereichsgrenzen und Genauigkeiten charakterisiert. Auf die rechnerinterne Darstellung als Gleitpunktzahl (daher kommt die Bezeichnung float) wird in Abschnitt 3.4 eingegangen. C schreibt vor, daß sowohl für Bereichsgrenzen als auch für Genauigkeiten stets gelten muß float ⊆ double ⊆ long double Eine Übersicht über Bereichsgrenzen nach der Norm und auf dem Rechner donar ist in Tabelle 2.10. zu sehen. Diese und andere installationsspezifischen Werte sind in der Datei float.h (Tabelle 2.11) zu finden. Rationalzahlige Konstanten und rationalzahlige Ein-/Ausgabedaten: Konstanten für rationale Zahlen können (in C) wahlweise mit oder ohne Exponentenangabe geschrieben werden. Ohne Exponentenangabe z. B. 0. + .37 3.1416 − 99.99 Der Dezimalpunkt muß angegeben werden. Mit Exponentenangabe z. B. 3.0e − 5 − 4711e + 23 Der Buchstabe e muß angegeben werden. 4711.e2 50 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME Typ float double long double Typ float double long double Größter Wert Norm donar 1E + 37 3.402823466E + 38F 1E + 37 1.7976931348623157E + 308 1E + 37 1.189731495357231765085759326628007016E + 4932L Kleinster (positiver) Wert Norm donar 1E − 37 1.175494351E − 38F 1E − 37 2.2250738585072014E − 308 1E − 37 3.362103143112093506262677817321752603E − 4932L Typ float double long double Norm 1E − 5 1E − 9 1E − 9 Genauigkeit donar 1.192092896E − 07F 2.2204460492503131E − 16 1.925929944387235853055977942584927319E − 34L Typ Anzahl gültiger Dezimalstellen donar Norm float double long double 6 9 9 6 15 33 Tabelle 2.10: Mindestwertebereiche und Genauigkeiten rationaler Zahlen Voreinstellung für die Genauigkeit ist double. Will man als Genauigkeit float oder long double haben, so ist das durch nachgestelltes f bzw. L explizit anzugeben. Bei der Eingabefunktion scanf und der Ausgabefunktion printf werden die Formatierungsanweisungen %f und %e benutzt. Für Ergänzungen (Längenangaben, Typpräzisierungen u. a.) sei wieder auf Kernighan/ Ritchie [KernR1988] und Lowes/Paulik [LoweP1995] verwiesen. Operationen und Ausdrücke mit rationalen Zahlen: Zwischen rationalen Zahlen sind Addition, Subtraktion, Multiplikation und Division zulässig. Das Verhalten bei Überschreiten oder Unterschreiten des zulässigen Wertebeiches (Überlauf, overflow, Unterlauf, 2.5. WERTEBEREICHE, OPERATIONEN, AUSDRÜCKE ' 51 $ FLT_RADIX FLT_ROUNDS FLT_DIG FLT_EPSILON FLT_MANT_DIG FLT_MAX FLT_MAX_EXP FLT_MIN FLT_MIN_EXP = = = = = = = = = 2 1 6 1.192093e-07 24 3.402823e+38 128 1.175494e-38 -125 DBL_DIG DBL_EPSILON DBL_MANT_DIG DBL_MAX DBL_MAX_EXP DBL_MIN DBL_MIN_EXP = = = = = = = 15 2.220446e-16 53 1.797693e+308 1024 2.225074e-308 -1021 = = = = = = = 33 1.925929944387235853055977942584927319e-34 113 1.189731495357231765085759326628007016e+4932 16384 3.362103143112093506262677817321752603e-4932 -16381 % LDBL_DIG LDBL_EPSILON LDBL_MANT_DIG LDBL_MAX LDBL_MAX_EXP LDBL_MIN LDBL_MIN_EXP & Tabelle 2.11: Gleitpunktzahlen auf donar (Auszug aus float.h) underflow) legt die Norm nicht fest und ist installationsabhängig. Das gleiche gilt für Division durch Null, die jedoch meistens zu Fehlerabbruch führt. Das exakte Ergebnis einer Operation zwischen rationalen Zahlen kann auch dann zu einem nicht darstellbaren Wert führen, wenn es nicht außerhalb der Grenzen des darstellbaren Bereiches liegt. In einem solchen Fall muß gerundet werden. Die Art der Rundung ist installationsabhängig und ist (in C) in der Datei float.h vermerkt und über FLT_ROUNDS abfragbar. Siehe hierzu Lowes/Paulik [LoweP1995], Abschnitt 8.6 . Tabelle 2.11 zeigt, daß auf donar nach Modus 1 gerundet wird, also zur nächsten darstellbaren Zahl. Vergleichsoperationen sind <, <=, >, >=, == und !=. Sie sind zwischen allen darstellbaren rationalen Zahlen definiert und liefern wahr oder falsch (siehe Unterabschnitt 2.5.4). Arithmetische Ausdrücke mit rationalen Zahlen werden auf die übliche Art gebildet. Zu 52 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME Vorrangregeln siehe die Literatur, [KernR1988] und [LoweP1995]. Ebenso zu eventuell notwendigen expliziten Typumwandlungen. Da in der Mathematik ganze Zahlen auch rationale Zahlen sind, treten auch in C ganzzahlige Werte in Ausdrücken mit rationalen Zahlen häufig auf. Dabei erfolgt eine implizite Typkonversion der ganzzahligen Werte in rationale Werte (Tabelle 2.8). Gelegentlich ist eine explizite Typumwandlung sinnvoll oder nötig. Dazu wird vor den zu konvertierenden Wert der Datentyp, in den umgewandelt werden soll, in Klammern geschrieben. Beispiel: float x; x = 3/2; x = (float)3/2; x = 3/(float)2; x = (float)(3/2); In der ersten und in der letzten Zuweisung erhält x wegen der ganzzahligen Division den Wert 1.0; in den mittleren Zuweisungen den Wert 1.5 . Weitere (von C unabhängige) Einzelheiten zu Gleitpunktzahlen sind in Abschnitt 3.4 zu finden. 2.5.3 Zeichen Datentypen für Zeichen: Es gibt die Datentypen • signed char • unsigned char • char Tabelle 2.12: Standardtypen für Zeichen Zeichen sind in C ganze Zahlen (vergleiche Seite 46). Sie sollten jedoch stets als eigener, nichtnumerischer Datentyp behandelt werden! Installationsabhängig ist char = signed char oder char = unsigned char. Zu den Mindestbereichen siehe Tabelle 2.6. Die Werte für ein spezielles System stehen in limits.h. Ebenso, ob char negative Werte annehmen kann. Die Werte für donar sind in Tabelle 2.7 aufgeführt. Zeichenkonstanten und Ein-/Ausgabedaten von Zeichen: Zeichenkonstanten werden als einzelne Zeichen, eingeschlossen in Apostrophe, geschrieben, z. B. ’K’, ’7’ oder ’-’. Welche Zeichen es gibt und welchen internen Zahlenwert sie haben, hängt vom jeweiligen Rechensystem ab und ist meistens für den Programmierer nicht von Bedeutung. 2.5. WERTEBEREICHE, OPERATIONEN, AUSDRÜCKE 53 In der Regel umfaßt der Datentyp char die abdruckbaren Zeichen (Groß- und Kleinbuchstaben, Ziffern, Satz- und Sonderzeichen) eines der beiden gängigen Codes ASCII (siehe Tabelle 3.4, Seite 115) oder EBCDIC (siehe Tabelle 3.5, Seite 116). Für einige Zeichen, die nicht darstellbare Steuerzeichen sind (z. B. „Zeilenvorschub“) oder die (wie z. B. „Apostroph“) in der Syntax schon anderweitig belegt sind, sieht C besondere Schreibweisen, die mit einem Fluchtsymbol beginnen („Escape-Sequenzen“), vor. Diese Zeichen sind in Tabelle 2.13 aufgeführt. \a \b \f \n \r \t \v \’ \” \? \\ Piepen (alert) Versetzen um eine Position nach links (backspace) Seitenvorschub (formfeed) Zeilenvorschub oder Zeilenende (linefeed bzw. new line) Positionierung auf Zeilenanfang (carriage return) Horizontaler Tabulator (horizontal tab) Vertikaler Tabulator (vertical tab) Apostroph (nicht Begrenzung einer Zeichenkonstante) Anführungszeichen (nicht Begrenzung einer Konstante für eine Zeichenreihe) Fragezeichen (nicht Bestandteil eines Trigraphen5 ) Backslash (nicht Einleitung einer Escape-Sequenz) 5 Trigraphen sind Zeichenfolgen, die mit 2 Fragezeichen beginnen und die auf Rechnern mit reduziertem Zeichensatz den vollen für C benötigten Zeichensatz ermöglichen. Siehe auch Lowes/Paulik [LoweP1995], Abschnitt 1.7. Tabelle 2.13: Tabelle der Escape-Sequenzen in C Wenn man den numerischen Wert eines Zeichens (für eine gegebene Rechenanlage!) kennt, kann das Zeichen auch in hexadezimaler Schreibweise angegeben werden. Das ist gelegentlich für Steuerzeichen, die nicht standardmäßig vorgesehen sind, notwendig. Zum Beispiel gehört das Fluchtsymbol (escape) nicht zu den Standardsteuerzeichen von Tabelle 2.13. Es hat auf donar den numerischen Wert 27 und kann im Programm in der hexadezimalen Schreibeweise ’\x1B’ angegeben werden. Zu Hexadezimaldarstellungen allgemein siehe auch Abschnitt 3.5. Es gibt auch eine oktale Schreibweise. Mit Ausnahme des Wertes Null (manchmal auch Binärnull genannt), der üblicherweise \0 geschrieben wird, sollte die oktale Schreibweise nicht benutzt werden. Der Standard erlaubt auch Zeichenkonstanten mit mehr als einem Zeichen. Diese sind im allgemeinen jedoch nicht sinnvoll. Für künftige Codeerweiterungen, die möglicherweise mehr als 256 Zeichen brauchen, ist der Datentyp wchar_t vorgesehen. Angaben zu diesem Typ stehen in der Datei stddef.h. Bei der Eingabefunktion scanf und der Ausgabefunktion printf wird die Formatierungsanweisung %c benutzt. Soll in printf ein %-Zeichen als Konstante ausgegeben werden, so 54 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME ist %% zu schreiben. Operationen mit Zeichen: Arithmetische Operationen mit Zeichen sind möglich und beziehen sich auf die numerischen Werte der Zeichen. Diese Operationen sollten und können vermieden werden. Vergleichsoperationen sind <, <=, >, >=, == und !=. Sie sind zwischen allen Zeichendarstellungen definiert und liefern wahr oder falsch (siehe Unterabschnitt 2.5.4). Die Vergleichsoperationen beziehen sich auf die numerischen Werte der Zeichen! Das macht bei == und != keine Schwierigkeiten, wohl aber bei den anderen Vergleichsoperationen. Es gibt nämlich keine akzeptierte „natürliche Ordnung“ der darstellbaren Zeichen. Zwar ist z. B. stets a < b oder 9 > 5, aber ob alle Großbuchstaben kleiner sind als alle Kleinbuchstaben, also z. B. Z < m gilt oder nicht, wird in den einzelnen Codes unterschiedlich festgelegt. Ähnliches gilt für die Frage, ob Ziffern kleiner oder größer sind als Buchstaben, für die Anordnung der Satz- und Sonderzeichen, für die Einordnung der Umlaute und ß usw. Kommt es bei einer Anwendung auf eine bestimmte Ordnung der Zeichen, d. h. auf eine bestimmte Sortierreihenfolge, an, so sollte man nicht die im Rechensystem gegebene Ordnung zugrundelegen, sondern über zusätzliche Datenstrukturen und Funktionen die gewünschte Ordnung explizit festlegen. 2.5.4 Wahrheitswerte Allgemeines: Wir gehen aus von einer zweielementigen Menge B = {T, F } . Die Elemente werden oft auch mit 0 und 1 oder mit 0 und L bezeichnet. Diese Menge kann man aus unterschiedlicher Sicht betrachten: • Algebraische Sicht. Mit den Verknüpfungen x F F T T y x+y F F T T F T T F x F F T T y x·y F F T F F F T T bildet B einen Körper, mit den Verknüpfungen x F F T T y xty F F T T F T T T x F F T T y xuy F F T F F F T T 2.5. WERTEBEREICHE, OPERATIONEN, AUSDRÜCKE 55 einen komplementären, distributiven Verband, eine boolesche Algebra. In beiden Fällen werden die Elemente meist mit 0 und 1 bezeichnet. • Logische Sicht. T unf F werden als Wahrheitswerte „wahr“ und „falsch“ betrachtet, und es werden Aussagenregeln untersucht. Die Begründer der modernen algebraischen und logischen Sicht der Strukturen über B = {T, F } sind Boole 6 und De Morgan 7 . Sie schufen die Grundlagen der Schaltalgebra und damit die theoretischen Voraussetzungen für die moderne Computertechnik. Datentyp für Wahrheitswerte: Wir wollen uns mit der logischen Sicht befassen und Wahrheitswerte als Datentyp in C untersuchen. Wahrheitswerte dienen in Programmiersprachen der Ablaufsteuerung von Programmen (siehe Abschnitt 2.7). In C gibt es im Gegensatz zu vielen anderen Programmiersprachen keinen eigenen Datentyp für Wahrheitswerte. „Wahr“ und „falsch“ werden durch ganze Zahlen dargestellt, und zwar gilt Wert = 0 Wert 6= 0 ' bedeutet bedeutet „falsch“ „wahr“. Einer sauberen Programmstrukturierung sind die folgenden Festlegungen sehr dienlich #define #define typedef TRUE FALSE int $ 1 0 BOOLEAN; Es wird dringend empfohlen, den Datentyp BOOLEAN durchgehend zu benutzen and &Konstanten für Wahrheitswerte als TRUE bzw. FALSE zu schreiben. % Operationen mit Wahrheitswerten: Werden auch boolesche Operationen oder logische Verknüpfungen genannt. Es seien x und y (Variable für abstrakte) Wahrheitswerte. In C seien die Variablen a, b und c als BOOLEAN definiert. Im folgenden werden wichtige Operationen mit Wahrheitswerten betrachtet und ihre Realisierung in C angegeben. Einstellige Operationen. Es gibt 22 = 4 Operationen, d. h. Abbildungen B → B. x f(x) F T T T f (x) = T (Konstante T ). In C: c = TRUE; Boole, George, ∗1815 Lincoln (England), †1864 Ballintemple bei Cork (Irland). Englischer Mathematiker und Logiker. Professor für Mathematik in Cork. 7 De Morgan, Augustus, ∗1806 Madura (Indien), †1871 London. Englischer Mathematiker und Logiker. Professor für Mathematik am Londoner University College. 6 56 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME x f(x) F F T F f (x) = F (Konstante F ). In C: c = FALSE; x f(x) F F T T f (x) = x (Identität). In C: c = a; x f(x) F T T F f (x) = x (Negation). In C: c = !a; Zweistellige Operationen. Es gibt 24 = 16 Operationen, d. h. Abbildungen B × B → B. Dei beiden wichtigsten sind x y f(x,y) F F F F T T T F T T T T f (x, y) = x ∨ y (ODER, Disjunktion). In C: c = a || b; x y f(x,y) F F F F T F T F F T T T f (x, y) = x ∧ y (UND, Konjunktion). In C: c = a && b; Als (nichttriviale) boolesche Operationen zwischen Wahrheitswerten bietet C nur !, || und &&. Es gilt jedoch: Alle n-stelligen (n ≥ 2) Verknüpfungen über B lassen sich auf diese drei Operationen zurückführen (ohne Beweis). Zum Beispiel x y f(x,y) F F T F T T T F F T T T f (x, y) = x ⇒ y (WENN-DANN, Implikation). In C: c = (!a) || (a && b); 2.5. WERTEBEREICHE, OPERATIONEN, AUSDRÜCKE 57 Prädikate: Unter einem n-stelligen Prädikat versteht man eine Abbildung, die jedem n-Tupel eines Datenbereichs einen Wahrheitswert zuordnet. In C heißen Prädikate relationale (oder logische) Operationen. Bis auf ! sind sie in C alle zweistellig. • Für Zahlen: ==, !=, <, <=, >, >=. • Für Zeichen: ==, !=, <, <=, >, >=. Siehe jedoch Seite 54. • Für Adressen: ==, !=. • Für Wahrheitswerte: Die oben behandelten Operationen !, || und &&. Hinzu kommen die Vergleiche == und !=. Achtung: In C sind zwei Wahrheitswerte, die als Zahlen verschieden sind, auch als Wahrheitswerte verschieden, selbst wenn sie beide „wahr“ darstellen! Siehe hierzu die folgende Anmerkung 2.3. Anmerkung 2.3 Die Mehrdeutigkeit des Wahrheitswertes „wahr“ als Zahlenwert macht in C gelegentlich Schwierigkeiten. In C gilt jedoch: Alle oben genannten Prädikate liefern als Wahrheitsswert „wahr“ stets den Zahlenwert 1. Das gleiche gilt für Standardfunktionen, zum Beispiel für den Vergleich von Zeichenreihen (siehe Unterabschnitt 2.6.3). Wenn zusätzlich durchgehend die Konstante TRUE (Seite 55) benutzt wird und Wahrheitswerte auf keine andere Art erzeugt werden, hat „wahr“ die eindeutige Darstellung 1. In diesem Fall können einige weitere logische Funktionen einfach dargestellt werden. Zum Beispiel x y f(x,y) F F T F T F T F F T T T f (x, y) = x ⇔ y (Äquivalenz). In C: c = (a == b); x y f(x,y) F F F F T T T F T T T F f (x, y) = xXORy (XOR, exklusives ODER). In C: c = (a != b); 2 2.5.5 Adressen Adressen und Zeiger wurden in Abschnitt 2.4 eingeführt. Adressen identifizieren Speicherplätze für Variablen oder Konstanten (siehe Abbildung 2.6). Variable, die Adressen als Werte annehmen, heißen Zeiger. 58 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME Datentypen für Adressen: Ist <typ> ein zulässiger Datentyp in C, so ist <typ> * der Datentyp der Adressen von Variablen, die Werte vom Datentyp <typ> enthalten. Rechnerintern sind Adressen in C natürliche Zahlen, deren explizite Werte jedoch nicht von Interesse sind (siehe Seite 43). Wertebereich: Es gibt keinen festen Wertebereich, jedoch für jede Variable eines Programmlaufs eine eindeutige Adresse. Zeiger können außer Adressen von Variablen (und Konstanten) in C auch den Wert NULL – in anderen Programmiersprachen oft NIL – annehmen. Dieser Wert ist von allen anderen Adressen verschieden und besagt, daß der Zeiger nicht auf eine Variable zeigt. Konstante: Adressen können in C-Programmen nicht explizit (wie z. B. Zahlen) hingeschrieben werden. Es ist aber durchaus möglich, mit dem Sprachelement const eine bestimmte Adresse als Konstante zu deklarieren und ihr einen Namen zu geben, wie z. B. ' float const float x; y =13.02; /*Variable*/ /*Konstante*/ float const float *x1; *x2; float const float *const x3 = &u; *const x4 = &u; & $ /*Variabler Zeiger auf Variable*/ /*Variabler Zeiger auf Konstante*/ /*Konst. Zeiger auf Variable*/ /*Konst. Zeiger auf Konstante*/ % Tabelle 2.14: Konstante und Variable in C in den letzten beiden Deklarationen von Tabelle 2.14. Ein-/Ausgabe: Es ist nicht möglich und macht auch keinen Sinn, in C Adressen einzugeben. Es ist möglich und gelegentlich sinnvoll, Adressen eines C-Programms auszugeben. Die Adressen sollten als natürliche Zahlen oder in Hexadezimaldarstellung z. B. mit printf ausgegeben werden. Operationen mit Adressen: Mit dem Operator & wird von einer benannten Variable (oder einer benannten Konstante, siehe Seite 44) die Adresse gewonnen und kann z. B. einem Zeiger zugewiesen werden. Man spricht von Adreßbildung oder Referenzierung. Mit dem Operator * gelangt man von einer Adresse zu dem Wert an dem entsprechenden Speicherplatz. Man spricht von Wertbildung oder Dereferenzierung. Das Programmbeispiel in Tabelle 2.15 führt zur Ausgabe der Zahlen 111 und 112. Adressen können mit dem Operator & auch von Variablen, die keinen Namen haben (siehe Seite 43), gebildet werden. Die Bezeichnungen Referenzierung und Dereferenzierung werden (unabhängig von C) allgemein benutzt, um von einem „Objekt“ auf einen „Verweis auf das Objekt“ überzugehen und umgekehrt. Allerdings ist manchmal nicht unmittelbar und eindeutig klar, was ein 2.5. WERTEBEREICHE, OPERATIONEN, AUSDRÜCKE ' & #include <stdio.h> main() { int x = 111; int *xz; printf("x = %d\n", *(&x)); xz = &x; *xz = 112; printf("x = %d\n", x); } 59 $ % Tabelle 2.15: Verwendung von Referenzierung & und Dereferenzierung * in C Objekt und was ein Verweis auf ein Objekt ist. Im Einzelfall sind dazu klare Festlegungen zu treffen. Zwischen Adressen gibt es die Vergleichsopertionen == und !=, die Wahrheitswerte liefern. C erlaubt auch bestimmte Rechenoperationen mit Adressen. Diese sind jedoch nur im Zusammenhang mit Reihungen sinnvoll und werden daher in Unterabschnitt 2.6.1 behandelt. 2.5.6 Aufzählungstypen In Programmen sind oft auch Daten zu bearbeiten, die nicht numerischer Natur und auch nicht als Zeichenreihen anzusehen sind. Ein typisches Beispiel sind Monate oder Wochentage. Sie sind keine Zahlen und, da sie in verschiedenen Sprachen unterschiedlich benannt sind, auch keine Zeichenreihen. Man kann sie in Programmen mittels Zahlen oder Zeichenreihen darstellen, aber das ist unnatürlich und kann zu Fehlern führen, z. B. wenn man die Summe Februar + März bildet. Einige Programmiersprachen kennen den Datentyp Aufzählung (enumeration). In C kann man, wie in dem Programmausschnitt in Tabelle 2.16 dargestellt, das Sprachelement enum <name> benutzen. Damit werden in dem Beispiel die neuen elementaren Datentypen enum Wochentage und enum Ostseeanrainer im Programm definiert. Die Elemente dieser Datentypen werden jeweils explizit benannt und aufgezählt. Die Namen der Elemente müssen untereinander und von allen anderen Namen von Variablen und Konstanten des Programms verschieden sein (siehe jedoch Abschnitt 2.8). Wertebereich, Konstanten, Variablen: Die Namen bezeichnen die (konstanten) Werte eines neuen Datentyps. Weitere Werte hat der Datentyp nicht. Die Namen werden in Program- 60 ' & KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME #include <stdio.h> main() { int h; int *ph; enum Ostseeanrainer { Daenemark, Schweden, Finnland, Russland, Estland, Lettland, Litauen, Polen, Deutschland }; enum Wochentag { Mo, Di, Mi, Do, Fr, Sa, So }; enum Ostseeanrainer land1, land2; enum Wochentag tag1 = So, tag2; land1 = Polen; land2 = Daenemark; if (land1 == land2) . . . $ % } Tabelle 2.16: Aufzählungstypen in C (Programmausschnitt) men zur direkten Angabe von Werten des Datentyps benutzt. Variable werden auf die übliche Art erklärt, z. B. tag1 im Programm von Tabelle 2.16. Mit expliziten Konstantendeklarationen wie z. B. const enum Wochentag tag3=Do; können weitere Namen für die Werte eingeführt werden. Für diese explizit erklärten Konstanten ist auch eine Adreßbildung möglich (z. B. &tag3), nicht jedoch für die Namen, die bei der Deklaration des Datentyps benutzt werden (&Do wird bei der Übersetzung nicht akzeptiert). Ein-/Ausgabe: Eine direkte Ein- oder Ausgabe über die Namen der Werte einer Aufzählung ist nicht möglich. Operationen mit Aufzählungswerten: Es sind Zuweisungen zu Variablen und Vergleiche 2.6. REIHUNGEN, ZEICHENREIHEN, SÄTZE 61 auf Gleichheit und Ungleichheit möglich. Wichtig ist, daß Aufzählungstypen als Indexbereiche von Reihungen verwandt werden können (siehe Seite 64). Anmerkung 2.4 C-intern sind Aufzählungswerte ganze Zahlen vom Typ int. Den Namen eines Aufzählungstyps werden bei der Deklaration beginnend bei 0 in aufsteigender Reihenfolge natürliche Zahlen als Werte zugeordnet. Im Beispiel von Tabelle 2.16 hat Deutschland den Wert 8. Durch explizite Wertfestlegung in der Definition kann davon abgewichen werden. Würde in Tabelle 2.16 z. B. Do = -5 geschrieben werden, so hätten Mo, Di, Mi die Werte 0, 1, 2 und Do, Fr, Sa, So die Werte -5, -4, -3, -2. Auch mehrere Namen mit dem gleichen numerischen Wert sind möglich. Mit Aufzählungswerten kann in C wie mit ganzen Zahlen gerechnet werden. Dadurch ist es möglich, beliebig unsinnige Dinge zu tun. Zum Beispiel kann Finnland + Do ausgerechnet oder geprüft werden, ob So kleiner als Schweden ist. In einigen Programmiersprachen, z. B. Pascal oder Modula-2, sind Aufzählungstypen echte, von ganzen Zahlen und untereinander verschiedene Datentypen. Siehe zum Beispiel Wirth [Wirt1996], Seite 22. Es wird empfohlen, die numerischen Eigenschaften von Aufzählungstypen nach Möglichkeit nicht zu benutzen. Einige reale Wertemengen wie z. B. Wochentage oder Monate tragen eine natürliche Anordnung, bei anderen wie z. B. den Staaten der Europäischen Union ist keine natürliche Anordnung gegeben. Selbst bei einer natürlicher Anordnung ist die Benutzung der numerischen Ordnung oft nicht sinnvoll, da die natürliche Anordung in der Regel zyklisch8 ist (Dezember liegt vor Januar). 2 2.6 Reihungen, Zeichenreihen, Sätze In den meisten Programmiersprachen, auch in C, stehen zwei Arten von zusammengesetzten Datentypen zur Verfügung – Reihungen und Sätze. Siehe auch die Übersicht auf Seite 37. 2.6.1 Reihungen Allgemeines: Reihungen bestehen aus Werten des gleichen (elementaren oder abgeleiteten) Datentyps. Die einzelnen Bestandteile einer Reihung werden über einen Index angesprochen. Die englische Bezeichnung für Reihung ist array. Im Deutschen spricht man oft auch von einem Feld, manchmal auch von einem Vektor. 8 Sie ist damit gar keine Ordnung im mathematischen Sinne. Siehe Seite 616. 62 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME Die Bezeichnung Feld wird in diesem Buch ausschließlich für die Bestandteile eines Satzes benutzt (siehe Unterabschnitt 2.6.4, Seite 68). Die Bezeichnung Vektor sollte man nur im Zusammenhang mit Vektorräumen benutzen. Abstrakt gesehen ist der Bereich, aus dem die Indizes gewählt werden, der Indexbereich, eine beliebige nichtleere endliche Menge. Sind die Indizes die n-Tupel eines Produkts endlicher Mengen (siehe Abschnitt A.4, Seite 615, im Anhang), so spricht man von einer n-dimensionalen Reihung, im Fall n ≥ 2 von einer mehrdimensionalen Reihung. In Programmiersprachen werden Indexbereiche aus Intervallen ganzer Zahlen oder Wertebereichen von Aufzählungstypen oder Produkten hieraus gebildet. Reihungen in C: In C gibt es für Reihungen das Sprachmittel <datentyp> <name>[<größe>]; Dabei ist <datentyp> ein in C zugelassener Datentyp, <name> der Bezeichner der Reihung und <größe> eine positive natürliche Zahl, die die Anzahl der Elemente der Reihung angibt. Im Beispiel von Tabelle 2.17 wird die Reihung ir deklariert. Sie besteht aus 4 Ele' & $ #include <stdio.h> #define LAENGE 4 main() { int ir[LAENGE]; ir[0] = 9; ir[1] = 10; ir[2] = 11; ir[3] = 12; printf("ir[0] = %d ir[3] = %d\n", ir[0], ir[3]); } % Tabelle 2.17: Reihung in C menten, die ganze Zahlen als Werte aufnehmen. Vorbesetzungswerte sind zulässig. Man könnte im Beispielprogramm statt der vier Einzelzuweisungen auch die Deklaration erweitern int ir[ ] = {9,10,11,12}; Die Größe braucht dann auch nicht angegeben zu werden, sie ergibt sich aus der Zahl der zugewiesenen Vorbesetzungswerte. 2.6. REIHUNGEN, ZEICHENREIHEN, SÄTZE 63 In C ist der Indexbereich einer Reihung von n Elementen (n ≥ 1) stets die Menge der natürlichen Zahlen {0, 1, 2, · · · , n − 1}. Abbildung 2.7 zeigt die Variablen, die durch das Programm von Tabelle 2.17 eingerichtet werden. Vergleiche auch Abbildung 2.6. ..... ... ... ... ..... .... 9 α ................... ........ ... ..... ......... ............ .......... ... ......... . . .. . .. ... ... ... ... .... . ir[0] ........ ....... ...... .. . ...... ....... ........ 10 β ................... ........ ... ..... ......... ........... ........... .. .......... . . .. .. ... ... ... ... .... . ir[1] ........ ...... ...... . .. ..... ........ ....... 11 γ ................... ........ ... ..... ......... ........... ........... .......... . . . .. . .. ... ... ... ... .... . ........ ....... ..... . . ...... ........ ....... ir[2] 12 δ ................... ........ ... ..... ......... ............ .......... ... ......... . . .. . .. ... ... ... ... .... . ........ ... . . ... ....... ir[3] Abbildung 2.7: Variablen einer Reihung Sie werden im Programm über die indizierten Namen ir[0], ir[1], ir[2], ir[3] angesprochen. Durch den Ablauf des Programms erhalten sie die Werte 9, 10, 11, 12. Die Adressen dieser Variablen sind α, β, γ und δ. Die Größe einer Reihung, d. h. die Anzahl ihrer Elemente, wird in C bei der Übersetzung des Programms festgelegt und muß eine positive natürliche Zahl sein. Sie kann im Programm direkt als numerischer Wert geschrieben werden, z. B. ir[4]. Zweckmäßiger ist, sie wie im Beispiel durch eine Textersetzungsdefinition #define festzulegen. In C gibt es auch die Möglichkeit, die Größe durch eine Variable (oder Konstante) mit Vorbesetzungswert anzugeben. Z. B. kann im Programm von Tabelle 2.17 statt #define LAENGE 4 auch int LAENGE = 4; geschrieben werden. Es wird jedoch dringend geraten, diese Möglichkeit nicht zu benutzen, da sie einerseits bei falschen (z. B. negativen) Angaben zu sehr unübersichtlichem Fehlerverhalten führen kann und andererseits gegenüber der Größenangabe durch #define keine Vorteile bietet. Indexwerte zur Adressierung von Elementen einer Reihung können durchaus dynamisch zur Ablaufzeit des Programms bestimmt werden. Darin liegt ja gerade der Vorteil von Reihungen! Zur Berechnung eines Indexwertes kann jeder Ausdruck, der eine passende natürliche Zahl liefert, genommen werden. Zum Beispiel könnte man im Programm von Tabelle 2.17 statt ir[2] = 11; auch ir[(ir[1]-ir[0]+1)] = 11; schreiben (was allerdings in diesem Fall nicht sehr sinnvoll wäre). Der Index, der für die Adressierung eines Feldes einer Reihung benutzt wird, muß eine natürliche Zahl aus dem zulässigen Bereich sein. Unzulässige Indexwerte können zu sehr schwer zu findenden Fehlern führen. Mehrdimensionale Reihungen in C: C kennt keine mehrdimensionalen Reihungen. Da jedoch in C Reihungen von beliebigen zulässigen Datentytpen gebildet werden können, kann man mehrdimensionale Reihungen durch Schachtelung eindimensionaler Reihungen gewinnen. Das soll am Beispiel der Matrixmultiplikation erläutert werden. In Tabelle 2.18 64 ' & KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME #include <stdio.h> #define L 15 main() { int i,m,n; float A[L][L]; float B[L][L]; float C[L][L]; . . . for (m=0; m<L; m=m+1) { for (n=0; n<L; n=n+1) { A[m][n] = 0.; for (i=0; i<L; i=i+1) { A[m][n] = A[m][n] + (B[m][i])*(C[i][n]); }; }; }; . . . } $ % Tabelle 2.18: Matrixmultiplikation in C ist ein Programmausschnitt angeben, der die Matrixmultiplikation zweier quadratischer Matrizen der Ordnung L zeigt. Reihungen über Aufzählungen in C: Die Gleichsetzung von Aufzählungstypen mit natürlichen, bei 0 beginnenden Zahlen (siehe Anmerkung 2.4, Seite 61) hat auch Vorteile. Sie erlaubt es, Aufzählungstypen unmittelbar als Indexbereiche zu nutzen. So kann man z. B. mit Hilfe der Deklarationen float enum wochenumsatz[7]; {Mo, Di, Mi, Do, Fr. Sa, So}; 2.6. REIHUNGEN, ZEICHENREIHEN, SÄTZE 65 mit wochenumsatz[Sa] den Umsatz einer Gastwirtschaft am Sonnabend der laufenden Woche bezeichnen. Auch mehrdimensionale Reihungen sind möglich. Die Deklarationen float enum enum jmumsatz[12][10]; {Jan, Feb, Mar, Apr, Mai, Jun, Jul, Aug, Sep, Okt, Nov, Dez}; {j1990, j1991, j1992, j1993, j1994, j1995, j1996, j1997, j1998, j1999}; gestatten es, den Umsatz im Mai 1995 über jmumsatz[Mai][j1995] anzusprechen. 2.6.2 Zeigerarithmetik in C Im Programm DYNREIHUNG (Tabellen 2.3 und 2.4, Beispiel 2.1, Seite 38) wird auch eine Reihung benutzt, allerdings eine, deren Größe von Programmlauf zu Programmlauf verschieden ist und deren Speicherplatz am Beginn des Programmlaufs durch einen Funktionsaufruf malloc bereitgestellt wird. Es werden zahl Speicherplätze, die jeweils einen Wert vom Typ int aufnehmen können, angelegt, also Variablen vom Typ int. Die Adresse der ersten dieser Variablen wird als Wert einer Variablen vom Typ int*, einem Zeiger, mit Namen zeiger zugewiesen. Mit zeiger+1 wird die zweite Variable adressiert, mit zeiger+2 die dritte usw. Der Inhalt der ersten Variablen wird durch *(zeiger), der der zweiten durch *(zeiger+1) usw angegeben. Auf diese Weise kann man so rechnen, als hätte man eine Reihung mit Indexbereich 0, 1, 2, · · · , zahl − 1, und das wird im Programm DYNREIHUNG auch so gemacht. Dabei wird allerdings eine von [ ] verschiedene Notation benutzt. Generell gilt in C das folgende. Ist α eine Adresse einer Variablen für Werte vom Typ <typ>, so ist α + i auch wieder eine Adresse dieses Typs. i ist dabei ein Ausdruck für eine ganze, durchaus auch negative Zahl. Dabei wird so getan, als sei die durch α gegebene Variable Element einer hinreichend großen Reihung von Variablen vom Typ <typ>, zu der auch das Element mit der Adresse α + i gehört. In Abbildung 2.7 zeigen sowohl α + 1 als auch δ − 2 auf die Variable mit Adresse β. Im Programm ließe sich das (etwas umständlich) als &(ir[0])+1 und &(ir[3])-2 formulieren. Es liegt in der Verantwortung des Programmierers, sicherzustellen, daß mit Zeigerarithmetik gebildete Adressen auch wirklich zu Variablen des Typs <typ> gehören. C führt keine Überprüfung aus. Wie dargelegt, kann man in C die Adressierung der Elemente einer im Programm deklarierten Reihung wahlweise durch die Indexangabe mit [ ] oder durch Zeigerarithmetik ausdrücken. Diese Wahlmöglichkeit hat man auch, wenn die Reihung gar nicht explizit deklariert wird, sondern wie im Programm DYNREIHUNG (Seite 41) implizit durch eine Variablengenerierung mit malloc entsteht. Im Programm DYNREIHUNG kann man z. B. ohne weiteres statt summe = summe + *(zeiger+i); auch summe = summe + zeiger[i]; schreiben. Die Möglichkeiten 66 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME • Die Reihung wird im Programmtext mit fester Größe deklariert. • Die Reihung wird im Programmlauf durch Variablengenerierung und Speicherung der Anfangsadresse in einem Zeiger realisiert. sowie • Die Elemente der Reihung werden über Indexangabe [ ] angesprochen. • Die Elemente der Reihung werden über Zeigerarithmetik angesprochen. können beliebig kombiniert werden. Ob man feste Reihungsdeklaration oder Variablengenerierung zum Beginn eines Programlaufes wählt, hängt vom zu lösenden Problem ab. In welcher Form man die Elemente der Reihung adressiert, ist eine Frage des Programmierstils, also eigentlich Geschmackssache. Anmerkung 2.5 Es gibt Aufgaben, bei denen Variablen des gleichen Datentyps bearbeitet werden, aber der dafür benötigte Gesamtplatz weder zum Zeitpunkt der Programmerstellung noch zu Beginn eines Programmlaufes bekannt ist, sondern sich erst während des Programmlaufes ergibt. Für diese Aufgaben kann man Reihungen nicht benutzen, sondern muß auf andere Datentrukturen und Zugriffstechniken zurückgreifen, wie z. B. Listen (Kapitel 8) oder Bäume (Kapitel 9). 2 Anmerkung 2.6 Obwohl mit Reihungen, die im Programm deklariert werden, und mit Reihungen, die man durch Variablengenerierung erzeugt, auf die gleiche Weise gearbeitet werden kann, gibt es zwischen den beiden Arten von Reihungen auch einen wesentlichen Unterschied. Das soll am Beispiel der Programme in den Tabellen 2.17 und 2.19 erläutert werden. ..... .... ... .. .... ...... α ............................. ..... .. ........ ..................... .. ......... . . .. . .. ... ... ... ... .... . ........ ... . ..... .... ... . ... ........ ... .... ..... 9 α ........ ......... ............ .... .. ...... ....................... ........ ....... ...... . .. ..... ........ ....... 10 β ........ ......... ........... . ..... .. ........ ................... ........ ....... ...... .. . ...... ........ ........ 11 γ .................. ........ ... ..... . ......... ........... ........ ........ ....... ...... . .. ..... ........ ....... 12 δ .................. ........ ... ..... . ............................ ........ ... . .. .. ........ ar Abbildung 2.8: Variablen einer dynamisch generierten Reihung Die Abbildungen 2.7 und 2.8 zeigen die zugehörigen Variablen mit ihren Werten, Adressen und Namen. ar ist der Name eines Zeigers, der die Adresse und den Wert α hat. Über Dereferenzierung dieser Variablen und anschließender Adreßmodifikation über [ ] (oder Zeigerarithmetik) kommt man zu den unbenannten Variablen mit den Adressen α, β, γ, und δ. 2.6. REIHUNGEN, ZEICHENREIHEN, SÄTZE ' & #include <stdio.h> #define LAENGE 4 main() { int *ar; ar = (int *)malloc(sizeof(int)*LAENGE); ar[0] = 9; ar[1] = 10; ar[2] = 11; ar[3] = 12; printf("ar[0] = %d ar[3] = %d\n", ar[0], ar[3]); } 67 $ % Tabelle 2.19: Durch Variablengenerierung erzeugte Reihung in C Der im Programm von Tabelle 2.17 (Abbildung 2.7) auftretende Name ir (ohne eckige Klammern) hat eine besondere Bedeutung. Er bezeichnet keine Variable, ist aber ein Name des Programms. Er bezeichnet direkt eine Adresse, in unserem Fall α. ir kann nicht wie ar ein Wert zugewiesen werden. Wird das Programm in Tabelle 2.17 um die Deklaration int *zi erweitert, so haben die Anweisungen zi zi = = ir; &(ir[0]); die gleiche Wirkung. Sie weisen zi den Wert α zu. 2.6.3 2 Zeichenreihen Zeichenreihen sind endliche Folgen von Zeichen. Die Zeichen werden aus einer nicht leeren, endlichen Menge, dem Alphabet, genommen. Ist auf dem Alphabet eine lineare Ordnung (siehe Abschnitt A.5, Seite 616) gegeben, so ergibt sich daraus eine lexikographische Ordnung auf den Zeichenketten. Zeichenreihen werden auch Zeichenketten genannt, die englische Bezeichnung ist string. Zu weiteren Einzelheiten über Zeichenreihen siehe die allgemeinen Erläuterungen in Abschnitt 3.6, Seite 112. In Programmiersprachen bilden Zeichenreihen einen wichtigen Datentyp. Als Operationen mit Zeichenreihen benötigt man Vergleichsoperationen und Operationen mit Teilzeichenreihen (Suchen, Entfernen, Einfügen). Ein wichtiger Sonderfall des Einfügens von Teilzeichenreihen ist das Zusammensetzen von Zeichenreihen (Konkatenation). 68 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME In C sind Zeichenreihen, anders als in einigen anderen Programmiersprachen, kein elementarer Datentyp, sondern Reihungen von Elementen vom Typ char. Allerdings gibt es einige Besonderheiten: • Da Zeichenreihen ihrer Natur nach von variabler Länge sind, Reihungen in C jedoch stets eine feste Länge haben, wurde die Festlegung getroffen, jede Zeichenreihe mit dem zusätzlichen Zeichen Binärnull (siehe Seite 53) abzuschließen. Dieses Zeichen belegt zwar ein Element in der Reihung, wird aber nicht zur Zeichenreihe gezählt. Die leere Zeichenreihe besteht demnach nur aus dem Zeichen Binärnull. • Es gibt in der Standardbibliothek (siehe Fußnote 3 auf Seite 38) von C eine Reihe leistungsfähiger Funktionen zur Bearbeitung von Zeichenreihen [LoweP1995]. • Zeichenreihen lassen sich in C direkt, also als Konstanten angeben. Dazu wird die Zeichenreihe in Anführungstriche gesetzt. Man beachte, insbesondere bei Größenangaben, daß im Programm stets auch noch die abschließende Binärnull gespeichert wird. Beispiele: char char zr1[ ] *zr2 = = ”Zeichenreihe1”; ”Zeichenreihe2”; Die beiden Deklarationen haben jedoch nicht die gleiche Bedeutung. Vergleiche Anmerkung 2.6. • Für die Ein-/Ausgabe von Zeichenreihen mit den Standardfunktionen printf und scanf gibt es das Format %s. 2.6.4 Sätze Allgemeines: Sätze (record) bestehen aus mehreren Werten, die i. a. zu unterschiedlichen (elementaren oder abgeleiteten) Datentypen gehören. Die einzelnen Bestandteile eines Satzes heißen Felder (field). Alle Sätze des gleichen Datentyps sind auf die gleiche Art aus Feldern aufgebaut. Die Felder werden über Feldnamen angesprochen. Zusammen mit dem Feldnamen ist auch der Datentyp eines Feldes festgelegt. Statt Satz wird im Deutschen auch Verbund gesagt. Für Feldname gibt es auch die Bezeichnung Selektor (selector). In C heißen Sätze Strukturen (structure) und Felder (member). Die Bezeichnung Struktur ist sehr allgemein und wird in diesem Buch nicht für Datentypen benutzt. Benutzt wird natürlich das entsprechende Sprachelement struct aus C. Zu den Begriffen Satz und record ist anzumerken, daß sie auch noch eine andere wichtige Bedeutung haben: Sie bezeichen die elementaren Komponenten einer Datei oder einer Datenbank. 2.6. REIHUNGEN, ZEICHENREIHEN, SÄTZE 69 Sätze in C: Mit dem Sprachkonstrukt struct kann man in C einen abgeleiteten Datentyp definieren, d. h den Aufbau seiner Sätze festlegen. Für diesen Datentyp können Variablen und Konstanten deklariert und wahlweise die Felder analog zu Reihungen (Seite 62) mit Vorbesetzungswerten versehen werden. Wichtiger als die Deklaration von Variablen im Programm ist bei Satz-Datentypen die dynamische Variablengenerierung zur Ablaufzeit des Programms. Satz-Datentypen bilden die Basis für die Konstruktion von Datenstrukturen (siehe Übersicht Seite 38 und Kapitel 7 „Allgemeines zu Datenstrukturen“). Wie Sätze in C-Programmen benutzt werden können, soll am Beispiel des Programms LVA gezeigt werden. Beispiel 2.3 (Programm LVA) Aufgabenstellung, Lehrverantaltungsdatei, Kommandos: Es sei eine Textdatei gegeben, in der zeilenweise Lehrveranstaltungseinträge gespeichert sind. Siehe Tabelle 2.20. Sie enthält pro Lehrveranstaltung die Nummer, den Namen, den Namen des Dozenten ' 80003 80009 90093 10913 90067 90055 90017 80013 80037 80113 60009 -1 & Markovketten Analysis Graphalgorithmen abcd Programmierung Java Modellierung Zahlentheorie Codierungstheorie Stochastik Niederlaendisch * Markov Euler Scala efg Stiege Boles Soluz Mueller Mayr-Helm Bernoulli Tenhoff * Di Mi Di Di Di Mi Mo Mo Fr Di Do * 18 10 18 10 10 10 10 14 08 10 20 * 18 12 20 12 12 12 12 16 10 12 22 * A321 H1 H5 H5 G F H5 H5 H3 H5 A408 * * Do Do * Do * Mi Do * Do * * * 08 10 * 10 * 08 10 * 10 * * * 10 12 * 12 * 10 12 * 12 * * * H2 A209 * F * H1 A315 * A209 * * $ % Tabelle 2.20: Lehrveranstaltungsdatei und Angaben zu Zeit und Ort. Es ist ein C-Programm zu schreiben, das die Lehrveranstaltungsdatei einliest und dann mit dem Benutzer einen Dialog beginnt. In diesem Dialog gibt der Benutzer Anweisungen, die das Programm bearbeitet und dann beantwortet. Es sollen zunächst nur die Kommandos hilfe, help, ende und lvanamen realisiert werden. In Tabelle 2.21 ist die Wirkung der Kommandos hilfe und help zu sehen. Kommando lvanamen bewirkt, daß eine Tabelle der Lehrveranstaltungen, aufsteigend sortiert nach Lehrveranstaltungsnamen, ausgegeben wird (siehe Tabelle 2.22) Das Programm LVA: In den Tabellen 2.23, 2.24 und 2.25 sind die ersten drei Teile des Programms LVA zu sehen. Teil I ist der Programmkopf. In Teil II werden globale Größen deklariert: Strukturen, Routinen, Variable. Bei den Strukturen wird der Satztyp lvstz (Lehrveranstaltungssatz) eingeführt. Die Sätze dieses Typs enthalten Felder für die 70 ' KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME $ LVA: Name der Eingabedatei? lvadat <LVA-Anweisung>: hilfe LVA: Die zulaessigen Anweisungen sind zur Zeit (November 1998): ende Beendet den Programmlauf. hilfe Diese Anweisung. help Diese Anweisung. lvanamen Ausgabe einer Liste aller Lehrveranstaltungen <LVA-Anweisung>: help LVA: Die zulaessigen Anweisungen sind zur Zeit (November 1998): ende Beendet den Programmlauf. hilfe Diese Anweisung. help Diese Anweisung. lvanamen Ausgabe einer Liste aller Lehrveranstaltungen & ' Tabelle 2.21: Anweisungen des Programms LVA <LVA-Anweisung>: lvanamen 80009 Analysis 80037 Codierungstheorie 90093 Graphalgorithmen 90055 Java 80003 Markovketten 90017 Modellierung 60009 Niederlaendisch 90067 Programmierung 80113 Stochastik 80013 Zahlentheorie 10913 abcd <LVA-Anweisung>: ende & % Euler Mayr-Helm Scala Boles Markov Soluz Tenhoff Stiege Bernoulli Mueller efg Mi Fr Di Mi Di Mo Do Di Di Mo Di 10 08 18 10 18 10 20 10 10 14 10 12 10 20 12 18 12 22 12 12 16 12 H1 H3 H5 F A321 H5 A408 G H5 H5 H5 Do * Do * * Mi * Do Do Do * $ 08 * 10 * * 08 * 10 10 10 * 10 * 12 * * 10 * 12 12 12 * H2 * A209 * * H1 * F A209 A315 * % Tabelle 2.22: Tabelle der Lehrveranstaltungsnamen Lehrveranstaltungsnummer, den Lehrveranstaltungsnamen, den Namen des Dozenten und Stundenplanangaben. Sie enthalten außerdem zwei Verweisfelder (*links und *rechts), die für den Aufbau eines Binärbaumes9 benutzt werden. Bei den Routinen sind die Unterprogramme hilfe, aufbau, lvanamen und lvaneu, in die das Gesamtprogramm zerlegt worden ist, aufgeführt. Als globale Variable wird schließlich noch der Zeiger namensbaum deklariert. Er enthält die Adresse der Wurzel des aufzubauenden Binärbaums. 9 Binärbäume werden ausführlich in Abschnitt 9.1 behandelt. 2.6. REIHUNGEN, ZEICHENREIHEN, SÄTZE ' /****************************************************************/ /* Programm LVA */ /* Liest eine Liste von Lehrveranstaltungsangaben ein und baut */ /* daraus eine programminterne baumartige Datenstruktur auf. */ /* Es wird vorausgesetzt, dass die Eingabeliste korrekt ist. */ /* Nach dem Aufbau der Datenstruktur geht das Programm in eine */ /* Abfrageschleife, in der es Anweisungen erwartet. */ /* */ /* Zulaessige Anweisungen sind: ende, hilfe, help, lvanamen */ /* (hier sollen weitere folgen) */ /****************************************************************/ #include <stdio.h> #include <malloc.h> #define TRUE 1 #define FALSE 0 typedef int boolean; & 71 $ % Tabelle 2.23: Programm LVA (Teil I) In Teil III (Tabelle 2.25) ist das Hauptprogram main wiedergegeben. Es besetzt einen Puffer vor und ruft dann das Unterprogramm aufbau auf, mit dem die Lehrveranstaltungsdatei eingelesen und die programminterne Datenstruktur, der Binärbaum, aufgebaut wird. Anschließend wird in einer Schleife das nächste Kommando abgefragt und ausgeführt. Das Unterprogramm aufbau ist als Teil IV und Teil V des Programms LVA in den Tabellen 2.26 und 2.27 zu sehen. Es erfragt den Namen der Eingabedatei, öffnet diese und legt mit dem ersten Eintrag der Eingabedatei als Wurzel einen Binärbaum an. In einer Schleife werden dann die weiteren Einträge der Eingabedatei an der Stelle, die in alphabetischer Anordnung dem Lehrveranstaltungsnamen entspricht, in den Binärbaum eingefügt. Der von aufbau angelegte Binärbaum ist in Abbildung 2.9 dargestellt. Die Abbildung zeigt nur die Lehrveranstaltungsnamen und die Verweise, die übrigen Felder der Sätze sind nicht zu sehen. Jeder Satz ist ein Knoten in dem Binärbaum und hat Verweise auf einen linken und einen rechten Nachfolger. Ist der Verweis NULL, gibt es keinen Nachfolger, andernfalls zeigt der Verweis auf einen Knoten, der Wurzel des linken (rechten) Unterbaumes ist. Die lexikographische Ordnung der Lehrveranstaltungsnamen10 ist in dem Binärbaum auf folgende Art festgehalten: Im linken Unterbaum eines jeden Knotens sind alle Namen kleiner/gleich dem Namen des Knotens, im rechten Unterbaum sind sie größer. In aufbau wird das Unterprogramm lvaneu, das in Tabelle 2.28 zu sehen ist, aufgerufen. Dieses fordert Speicher für den nächsten Satz an (malloc) und liest den Eintrag aus der Eingabedatei. 10 Man beachte, daß das kleine a in abcd kleiner ist als jeder Großbuchstabe. 72 Markov.......... ◦...................... ............ ◦ ............ ....... ............ . . . . ketten . ....... . . . . . ....... ......... .. ............ Analysis ....... ....... ....... ....... ....... ....... ....... ....... ....... ....... ....... ... . .................... • ........◦... abcd .... ... ... .... ... . . . ... ... ... ... .... . . . . ..... ........... ......... Graph... ◦........ ......◦ ......... ........ ... algorithmen ........ . . . . . . . ... . ........ .. ........ ........ ......... ........ . . . . . . . . ......... ........ ......... ........ ........ . . . . . . . .. .............. ................ Codierungstheorie • • Program... ◦......... ......◦ ......... ........ ... mierung ........ . . . . ... . . . . . ........ .. ........ ........ ......... ........ . . . . . . . . ......... ........ ......... ........ ........ . . . . . . . . .. .............. ................ ... ... ... ... ... ... ... . .......... ....... .. Java ◦ • . ... ... ... ... . . .. ... ... ... ... . . ... ... ... . .... ................ ... • • Modellierung • ........◦... Zahlentheorie ◦ • • • Stochastik • • .... ... ... .... ... . . .... ... ... ... ... . . ... .. ... ............. ....... Niederländisch ... ... ... ... ... ... ... ... ... .. . .............. ..... Abbildung 2.9: Binärer Suchbaum der Lehrveranstaltungen .. ... ... ... ... . . .. ... ... ... ... . . ... ... ... . ... ............ . . ...... KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME ............ ........... ........... ............ ............ . . . . . . . . . . . ... ............ ........... ........... ............ . ...................... . . . . ..................... 2.6. REIHUNGEN, ZEICHENREIHEN, SÄTZE ' /***********************************************************/ /* Strukturen */ /***********************************************************/ struct lvstz /* Satz fuer eine Lehrveranstaltung */ { int lvanr; char lvaname[51]; char lvadozent[26]; struct lvstz *links; struct lvstz *rechts; /* Stundenplanangaben 1.Tag */ char tag1[3]; char anf1[3]; char ende1[3]; char raum1[11]; /* Stundenplanangaben 2.Tag */ char tag2[3]; char anf2[3]; char ende2[3]; char raum2[11]; }; typedef struct lvstz lvasatz; /***********************************************************/ /* Routinen */ /***********************************************************/ void hilfe(void); void aufbau(void); void lvanamen(lvasatz *lvalauf); lvasatz *lvaneu(FILE *fd); /***********************************************************/ /* Globale Variable und Konstanten */ /***********************************************************/ lvasatz *namensbaum=NULL; /* Zeiger auf */ /* Wurzel des Namensbaumes */ & /***********************************************************/ 73 $ % Tabelle 2.24: Das Programm LVA (Teil II) Tabelle 2.29 zeigt die Routine hilfe, die beim Kommando gleichen Namens aufgerufen wird, als Teil VII des Programms LVA. Die Routine lvanamen, die das Kommando gleichen Namens ausführt und eine nach Lehrveranstaltungsnamen aufsteigend sortierte Liste ausgibt, ist in Tabelle 2.30 zu sehen. Sie 74 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME ' /**************************************************************/ /* Hauptprogramm */ /* Ruft Routine aufbau zum Anlegen der Datenstruktur auf, */ /* erwarten dann Anweisungen und ruft die entprechenden */ /* den Routinen auf. */ /**************************************************************/ main() { int i; boolean gueltig; char anwpuffer[256]; /* Eingabepuffer fuer */ /* aktuelle Anweisung */ for (i = 0; i < 256; i=i+1) anwpuffer[i] = ’\0’; aufbau(); & /* Einlesen Liste und Aufbau $ Datenstruktur */ while (TRUE) /* Einlesen, Erkennen und Ausfuehren */ { /* der Anweisungen */ gueltig = FALSE; printf("<LVA-Anweisung>: "); scanf("%s", anwpuffer); if (strcmp(anwpuffer, "ende") == 0) {exit(0);}; /* Anweisung "ende" */ if (strcmp(anwpuffer, "hilfe") == 0) {gueltig = TRUE; hilfe();}; if (strcmp(anwpuffer, "help") == 0) {gueltig = TRUE; hilfe();}; if (strcmp(anwpuffer, "lvanamen") == 0) {gueltig = TRUE; lvanamen(namensbaum);}; if (!gueltig) {printf("Unzulaessige LVA-Anweisung: %s\n", anwpuffer);}; }; } % Tabelle 2.25: Das Programm LVA (Teil III) startet bei der Wurzel und bearbeitet jeden Knoten folgendermaßen: Zuerst werden die Knoten des linken Unterbaumes bearbeitet, dann wird der Lehrveranstaltungsname des Knotens ausgegeben und als letztes die Knoten des rechten Unterbaumes bearbeitet. Dazu ruft sich das Unterprogramm selber auf, man spricht von rekursiven Aufrufen (siehe 2.6. REIHUNGEN, ZEICHENREIHEN, SÄTZE ' /**************************************************************/ /* Routinen aufbau */ /* */ /* Liest die Lehrveranstaltungsdatei ein und baut eine */ /* programminterne Datenstruktur in Form eines binaeren */ /* Suchbaums auf. */ /* Ruft Routine lvaneu auf. */ /**************************************************************/ void aufbau(void) { char dateiname[256]; lvasatz *lvaaktuell, *lvasuch, *lvasuch1; FILE *fd; printf("LVA: Name der Eingabedatei? "); scanf("%s", dateiname); fd = fopen(dateiname, "r"); if (fd == NULL) { printf("LVA: Eingabedatei kann nicht geoeffnet werden\n"); exit(0); }; lvaaktuell = lvaneu(fd); if (lvaaktuell->lvanr < 0) {printf("LVA: Keine Eingabedaten\n"); exit(0);}; namensbaum = lvaaktuell; lvaaktuell = lvaneu(fd); & Tabelle 2.26: Das Programm LVA (Teil IV) 75 $ % Abschnitt 2.8). Der rekursive Ablauf der Prozedur lvanamen ist in Abbildung 2.10 dargestellt. Man sieht in Tabelle 2.30, daß bei jedem Knoten vier Arbeitsschritte ausgeführt werden 1. links: Der linke Unterbaum wird bearbeitet (durch den rekursiven Aufruf von lvanamen mit der Wurzel des linken Unterbaumes als Parameter). 2. ausgeben: Der Lehrveranstaltungsname des aktuellen Knotens wird ausgegeben. 3. rechts: Der rechte Unterbaum wird bearbeitet (durch den rekursiven Aufruf von lvanamen mit der Wurzel des rechten Unterbaumes als Parameter). 76 ' } & KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME while (lvaaktuell->lvanr >=0 ) { lvasuch = namensbaum; while (lvasuch != NULL) { lvasuch1 = lvasuch; if (strcmp(lvasuch->lvaname, lvaaktuell->lvaname) >= 0) { if (lvasuch->links == NULL) { lvasuch->links = lvaaktuell; lvasuch = NULL; } else {lvasuch = lvasuch->links;}; } else { if (lvasuch->rechts == NULL) { lvasuch->rechts = lvaaktuell; lvasuch = NULL; } else {lvasuch = lvasuch->rechts;}; } }; lvaaktuell = lvaneu(fd); }; $ % Tabelle 2.27: Das Programm LVA (Teil V) 4. zurück: Nach Bearbeitung des rechten Unterbaumes ist die Bearbeitung des aktuellen Knotens beendet und es geht zum Aufrufer zurück. Das ist im Programmtext allerdings nicht explizt erkennbar, die Routine lvanamen benutzt die Anweisung return nicht. Für jeden Knoten ist in Abbildung 2.10 die Aufrufstufe angegeben. Weiter sind in Rechtecken die Arbeitsschritte für einen Knoten, die nicht durch Arbeitsschritte auf tieferer 2.6. REIHUNGEN, ZEICHENREIHEN, SÄTZE ' /**************************************************************/ /* Routine lvaneu */ /* */ /* Erhaelt als Eingabe den Dateideskriptor der Eingabedatei.*/ /* Fordert Speicherplatz an, liest die Daten der naechsten */ /* Lehrveranstaltung aus der Datei und baut einen Lehrver- */ /* anstaltungssatz auf. */ /* Liefert die Adresse des aufgebauten Satzes zurueck. */ /**************************************************************/ lvasatz *lvaneu(FILE *fd) { lvasatz *lvaaktuell; lvaaktuell = (lvasatz *) malloc(sizeof(lvasatz)); if (lvaaktuell == NULL) { printf("LVA Einlesen Daten: Nicht genuegend Speicher vorhanden\n"); exit(0); }; fscanf(fd, " %d %s %s %s %s %s %s %s %s %s %s", &lvaaktuell->lvanr, &lvaaktuell->lvaname, &lvaaktuell->lvadozent, &lvaaktuell->tag1, &lvaaktuell->anf1, &lvaaktuell->ende1, &lvaaktuell->raum1, &lvaaktuell->tag2, &lvaaktuell->anf2, &lvaaktuell->ende2, &lvaaktuell->raum2); lvaaktuell->links = NULL; lvaaktuell->rechts = NULL; return(lvaaktuell); } & 77 $ % Tabelle 2.28: Das Programm LVA (Teil VI) Stufe11 unterbrochen werden, zusammengefaßt. Markovketten wird auf Stufe 1 bearbeitet und führt beim ersten Schritt zur Bearbeitung von Analysis auf Stufe 2. Dieser Knoten hat keinen linken Unterbaum, so daß Schritt links nicht zur nächsten Stufe führt. Es wird der Name Analysis ausgegeben. Danach führt Schritt rechts zur Bearbeitung des Knotens Graphalgorithmen. Von diesem geht es nach der Bearbeitung von Codierungstheorie, Ausgabe des Namens und der Bearbeitung von Java zurück und danach kehrt auch Analysis zurück. Anschließend geht es auf Stufe 1 weiter mit der 11 3 ist tiefer als 1 ! 78 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME ' /**************************************************************/ /* Routine hilfe */ /* */ /* Gibt die aktuell gueltigen Anweisungen des Programms */ /* zusammen mit einer kurzen Beschreibung aus. */ /**************************************************************/ void hilfe(void) { printf("\n"); $ printf("LVA: Die zulaessigen Anweisungen sind zur Zeit (November 1998):\n"); printf(" ende Beendet den Programmlauf.\n"); printf(" hilfe Diese Anweisung.\n"); printf(" help Diese Anweisung.\n"); printf(" lvanamen Ausgabe einer Liste aller Lehrveranstaltungen\n"); printf("\n"); } & % Tabelle 2.29: Das Programm LVA (Teil VII) Ausgabe des Namens Markovketten usw. 2.7 2 Programmsteuerung Vorbemerkungen: Elektronische Rechner werden heutzutage für eine große, kaum noch zu überblickende Vielfalt von Aufgaben eingesetzt. Das ist nur möglich, weil die Ablaufsteuerung der Rechner, also die ablaufenden Programme, in Abhängigkeit von den bearbeiteten Daten unterschiedliche Programmteile durchlaufen. Auf der Hardwareebene (sowohl Hardwaremaschine als auch konventionelle Maschine, Tabelle 2.1) sind dafür Sprungbefehle, insbesondere bedingte Sprungbefehle, die Voraussetzung. Siehe hierzu Unterabschnitt 4.2.2, Seite 133. Auf der Ebene der Programmiersprachen hat man daraus eine Reihe von Sprachkonstrukten entwickelt, die zur Ablaufsteuerung (Programmsteuerung 12 , Es wird auch oft die Bezeichnung Programmkontrolle benutzt. Das ist jedoch schlechtes Deutsch, da das Wort Kontrolle anders als das englische Wort control nicht Steuerung bedeutet. 12 2.7. PROGRAMMSTEUERUNG ' 79 /**************************************************************/ /* Routine lvanamen */ /* */ /* Gibt die vollstaendige Liste der Lehrveranstaltungen */ /* in alphabetisch aufsteigender Ordnung der */ /* Lehrveranstaltungsbezeichnungen aus. */ /**************************************************************/ void lvanamen(lvasatz *lvalauf) { if (lvalauf->links != NULL) {lvanamen(lvalauf->links);}; $ printf("%6d %20s %10s %2s %2s %2s %4s %2s %2s %2s %4s\n", lvalauf->lvanr, lvalauf->lvaname, lvalauf->lvadozent, lvalauf->tag1, lvalauf->anf1, lvalauf->ende1,lvalauf->raum1, lvalauf->tag2, lvalauf->anf2, lvalauf->ende2,lvalauf->raum2); if (lvalauf->rechts != NULL) {lvanamen(lvalauf->rechts);}; } & % Tabelle 2.30: Das Programm LVA (Teil VIII) program control) von Programmen dienen und in diesem Abschnitt dargestellt werden. In alten Programmiersprachen, z. B. frühe Versionen von FORTRAN, war in der Struktur der Programme die Verwandtschaft zur Assemblersprache noch zu erkennen. Einige Anweisungen eines Programms wurden mit einer Marke (label) versehen und mit der Anweisung goto wurden bedingte und unbedingte Sprünge zu Marken realisiert. Auch das Ende von Schleifen wurde über Marken festgelegt. Seit dem Aufkommen moderner Programmiersprachen – Algol60, C und Pascal gehörten zu den ersten – sind Marken und goto-Anweisungen nicht mehr nötig. Zwar sind sie in den meisten Programmiersprachen, auch C, noch erlaubt, werden aber kaum noch benutzt. Im folgenden wird auf diese Sprachkonstrukte nicht mehr eingegangen. Allgemeiner Aufbau von Programmen: Bei allen Unterschieden im einzelnen haben Programme in modernen Programmiersprachen den folgenden allgemeinen Aufbau: • Definition von Konstanten, Variablen, Funktionen, Datentypen. • Folge ausführbarer Anweisungen (executable statement). 80 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME Markovketten 1 links Analysis 2 links ausgeben rechts Niederländisch 5 links ausgeben rechts zurück Graphalgorithmen 3 links Modellierung 4 zurück Codierungstheorie links ausgeben Programmierung 3 ausgeben rechts rechts Zahlentheorie 4 links zurück Stochastik 5 links 4 Graphalgorithmen 3 ausgeben rechts ausgeben rechts Java links zurück 4 ausgeben rechts zurück Zahlentheorie 4 ausgeben rechts zurück Graphalgorithmen 3 zurück Programmierung 3 zurück Analysis 2 zurück abcd 2 ausgeben Markovketten 1 ausgeben rechts abcd 2 links Programmierung 3 links Modellierung 4 links ausgeben rechts rechts zurück Markovketten 1 zurück Abbildung 2.10: Rekursive Aufrufe der Prozedur lvanamen Man kann von dem Definitionsteil und dem Anweisungsteil eines Programms sprechen. In den meisten Programmiersprachen, darunter auch C, sind Definitionsteil und Anweisungsteil eines Programms getrennt und der Anweisungsteil folgt dem Definitionsteil. In einigen Programmiersprachen können in einem Programm Definitionen und ausführbare Anweisungen gemischt werden. Datendefinitionen und Datentypen wurden in den Abschnitten 2.4 und 2.5 behandelt. Ausführbare Anweisungen – kurz: Anweisungen (statement) – sind 2.7. PROGRAMMSTEUERUNG 81 • Anweisungsblöcke • Elementare Anweisungen • Alternativen • Schleifen • Unterprogrammaufrufe Eine Folge von Anweisungen, die durch Blockklammern eingeschlossen wird, bildet einen Anweisungsblock. Blockklammern sind oft begin und end. In C werden geschweifte Klammern verwendet. Ein Anweisungsblock wird wie eine einzige Anweisung behandelt und kann überall dort im Programm auftreten, wo eine Anweisung stehen darf, also z. B auch in einem übergeordneten Anweisungblock. Die Programme haben eine Blockstruktur. Anmerkung 2.7 Einige Programmiersprachen, darunter auch C, lassen zu, daß ein Anweisungsblock nicht nur aus ausführbaren Anweisungen besteht, sondern auch einen Definitionsteil enthält. Die dort definierten Variablen, Konstanten, Datentypen, Funktionen gelten nur innerhalb des Anweisungsblocks. Ihr Gültigkeitsbereich (scope) ist der entsprechende Block. Man sagt, die Variable (Konstante etc) sei in dem entsprechenden Block lokal. Es wird empfohlen, innerhalb von Anweisungsblöcken Definitionen zu vermeiden. In Funktionen jedoch sind lokale Gültigkeitsbereiche nicht nur erlaubt, sondern für die Programmierung von Unterprogrammen wesentlich. Das wird in Abschnitt 2.8 im einzelnen erläutert. 2 Anmerkung 2.8 In manchen Programmiersprachen, z. B. Algol68, liefert jede Anweisung auch einen Wert. Dieser kann für Zuweisungen, Abfragen, in Ausdrücken usw. benutzt werden oder unberücksichtigt bleiben. In C liefern Anweisungen keine Werte. Zu Unterprogrammaufrufen in C siehe Abschnitt 2.8. 2 Elementare Anweisungen: Die wichtigste elementare Anweisung ist die Zuweisung. Dabei wird einer Variablen durch einen Ausdruck ein Wert zugewiesen. Anweisungen, Werte und Ausdrücke wurden in den Abschnitten 2.4 und 2.5 behandelt. Beispiele für weitere elementare Anweisungen sind in C break (z. B. zum vorzeitigen Verlassen einer Schleife) oder return (Übergabe eines Funktionswertes am Ende eines Funktionsaufrufes; siehe Abschnitt 2.8). Zu den elementaren Anweisungen gehört auch die leere Anweisung (empty statement, null statement), die es in den meisten Programiersprachen, auch C, gibt. 82 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME Alternativen: Alternativen werden auch Auswahlanweisungen genannt. Alternativen prüfen Bedingungen, die Wahrheitswerte liefern, und entscheiden in Abhängigkeit von dem Wert der Bedingungen, welche Anweisungen ausgeführt werden und welche nicht. Die wichtigste Alternative ist die if-Anweisung. Sie hat die allgemeine Form if <bedingung> then <anweisung1> else <anweisung2> <bedingung> liefert einen Wahrheitswert. Ist dieser true, so wird <anweisung1> ausgeführt, andernfalls <anweisung2>. Der else-Teil darf fehlen. In C wird die Bedingung in runde Klammern gesetzt und das Wort then weggelassen. <anweisung1> und <anweisung2> heißen Programmzweige und sind häufig Anweisungsblöcke. In Programmen treten oft geschachtelte if-Anweisungen auf. Tabelle 2.31 zeigt eine schematische Schachtelung in C-Schreibweise. Um diese umständliche und aufwendige Schreibif (...) {........} else { if (...) {........} else { if (...) {........} else { ..... } } } Tabelle 2.31: Schachtelung von if-Anweisungen weise zu vereinfachen, gibt es in C die Sprachmittel else if und switch (siehe Kernighan/Ritchie [KernR1988]). Die entsprechenden und zum Teil weiterreichenden Sprachmittel anderer Programmiersprachen, z. B. case-Anweisung, sind in Kowalk [Kowa1996], Abschnitt 5.3, beschrieben. Schleifen: Schleifen (loop) werden auch Wiederholungsanweisungen oder Iterationen genannt. Schleifen haben die allgemeine Form Solange <bedingung> tue <anweisung> oder 2.8. UNTERPROGRAMME 83 Tue <anweisung> bis <bedingung> <bedingung> ist die Schleifenbedingung, <anweisung> der Schleifenrumpf und im Allgemeinen ein Anweisungsblock. Bei der ersten Form spricht man auch von Schleifenkopf und Schleifenkörper. Die erste Form der Schleifenanweisung wird generell, auch in C, mit dem Sprachkonstrukt while ausgedrückt. Dabei wird der Schleifenrumpf nicht ausgeführt, wenn <bedingung> schon beim ersten Test false liefert. Für die zweite, weniger häufig benutzte Form von Schleifen gibt es das Sprachkonstrukt until, in C do ... while. Bei dieser Schleifenform wird der Schleifenrumpf stets mindestens einmal ausgeführt. Oft kommen in Programmen Schleifen vor, bei denen ein Zähler erhöht oder erniedrigt werden muß, bis ein Zielwert erreicht ist. Es ist dann zweckmäßig, die Prüfung, ob der Endzählerstand schon erreicht ist, mit der Inkrementierung oder Dekrementierung des Zählers zu verbinden. Viele Programmiersprachen, auch C, verwenden für Schleifen dieser Art das Sprachkonstrukt for, man spricht von for-Schleifen. FORTRAN verwendet DO. Unterprogrammaufrufe: 2.8 Werden im nächsten Abschnitt (2.8) behandelt. Unterprogramme Allgemeines: Schon sehr früh merkte man bei der Programmierung (in Assembler), daß es sehr nützlich ist, bestimmte Programmstücke in einem Programm so zur Verfügung zu stellen, daß sie unter einem identifizierenden Namen an unterschiedlichen Stellen des Programms ablaufen. Das Konzept wurde mit Erfolg in höhere Programmiersprachen übernommen. Der allgemeine Name für solche Programmstücke ist Unterprogramm (subprogram, subroutine) 13 . Das Ausführen eines Unterprogramms heißt Unterprogrammaufruf (subroutine call). Unterprogramme dienen der flexiblen Erweiterung der benutzten Programmiersprache. Man kann (und sollte) mit Unterprogrammen ein Programm sauber gestalten und klar gliedern. Allerdings kann man mit einem Übermaß an Unterprogrammen des Guten auch zuviel tun. Ein weiterer Vorteil von Unterprogrammen ist, daß der von ihnen benötigte Platz im Arbeitsspeicher nur einmal gebraucht wird. Speicherplatzersparnis war früher ein wichtiger Gesichtspunkt und ist für sehr große Unterprogramme immer noch von Bedeutung. Da heutzutage jedoch Geschwindigkeit wichtiger als Speicherplatzersparnis ist, werden Unterprogramme, bei denen das möglich ist, von den Compilern zunehmend expandiert, d. h die sich aus ihrer Übersetzung ergebenden Maschinenbefehle werden an allen Aufrufstellen (in-line), in das übersetzte Programm eingefügt. Machmal werden Unterprogramme auch Routinen genannt. Diese Bezeichnung wird jedoch oft auch gleichbedeutend mit Programm benutzt. 13 84 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME Ein Unterprogrammaufruf bedeutet, daß im aufrufenden Programm von dem Unterprogramm eine bestimmte Arbeit erwartet wird. Im allgemeinen werden dafür die zu bearbeitenden Daten im aufrufenden Programm bereitgestellt. Geschieht das durch explizite Angabe dieser Daten, so spricht man von Parametern (auch Eingabeparameter) des Unterprogrammaufrufs. Die zu bearbeitenden Daten können auch implizit bereitgestellt werden, indem das Unterprogramm von vornherein ihre Namen oder Adressen im aufrufenden Programm kennt. Entsprechend kann ein Unterprogramm einen berechneten Wert – das Ergebnis (result) – an das aufrufende Programm zurückliefern. Es ist auch möglich, daß ein aufgerufenes Unterprogramm bestimmte Arbeiten durchführt, aber kein Ergebnis zurückliefert. Unterprogramme, die kein Ergebnis zurückliefern, werden oft Prozeduren (procedure) genannt und Unterprogramme mit Ergebnis heißen i. a. Funktionen (function). In diesem Buch wird der Bezeichnungsweise von C gefolgt. Daher bedeutet Funktion14 stets Unterprogramm, unabhängig davon, ob ein Ergebnis geliefert wird oder nicht. Anmerkung 2.9 (Makros) Unterprogramme werden manchmal auch durch Makros (macro) in ein Programm eingefügt. Allgemein sind Makros Anweisungen zur Textersetzung, die der Compiler oder ein davor ablaufendes Vorbereitungsprogramm (Präprozessor) ausführt, ehe mit der eigentlichen Übersetzung begonnen wird. #define LAENGE 4 ist z. B. ein typischer Makro in C. Durch Makros definierte Programmteile sind keine echten Unterprogramme, da die Unterprogrammeigenschaft nach der Textersetzung nicht mehr sichtbar ist. 2 Aufrufstruktur und Gültigkeitsbereiche Das Programmstück eines Gesamtprogramms, in dem der Programmablauf beginnt, heißt Hauptprogramm (main program). In C erhält es den Namen main. In diesem Programm wird es i. a. Unterprogrammaufrufe geben. Diese Unterprogramme können während ihres Ablaufs selber wieder Unterprogramme aufrufen. Bei normalem Programmablauf wird jedes aufgerufene Unterprogramm zum aufrufenden Programm zurückkehren und schließlich wird sich das Hauptprogramm irgendwann beenden15 . Fehler- und Sondersituationen können dazu führen, daß von dieser Aufrufstruktur abgewichen wird. In C, wie in vielen anderen Programmiersprachen, haben sowohl das Hauptprogramm als auch jedes Unterprogramm den auf Seite 79 beschriebenen allgemeinen Aufbau. Das heißt, in jedem dieser (Teil-)Programme gibt es einen Definitionsteil und einen Anweisungsteil, und dieser ist aus Anweisungsblöcken, elementaren Anweisungen, Alternativen, Schleifen 14 Falls keine Funktion im mathematischen Sinne (siehe Abschnitt A.3, Seite 614, im Anhang) gemeint ist. 15 Zu reaktiven Systemen siehe Abschnitt 5.1, Seite 152. 2.8. UNTERPROGRAMME 85 und Funktionsaufrufen (also Unterprogrammaufrufen) zusammengesetzt. Die in einem Haupt- oder Unterprogramm definierten Variablen, Konstanten und Datentypen sind dort lokal (siehe auch Anmerkung 2.7). Ihr Gültigkeitsbereich ist das entsprechende Hauptoder Unterprogramm. Außerhalb sind sie nicht sichtbar und ihre Namen können dort ganz andere Objekte bezeichnen. Diese Zusammenhänge sollen an einem Beispiel verdeutlicht werden. Es seien ein Hauptprogramm H und zwei Unterprogramme U1 und U2 (in C oder einer anderen Programmiersprache) gegeben. U1 wird nur aus dem Hauptprogramm H und U2 nur aus dem Unterprogramm U1 aufgerufen. Wie oft und an welchen Stellen die Unterprogramme aufgerufen werden, ist programmlaufabhängig. In Abbildung 2.11. wird ein Programmlauf H . . . . . . . . . U1 U2 0 t1 . . . . . . . . . . . . . . . . . . t2 t3 . . . . . . . . . t4 t5 t .. ...................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................... . Abbildung 2.11: Nichtrekursive Unterprogrammaufrufe gezeigt, in dem jedes Unterprogramm genau einmal aufgerufen wird. In keinem Programmlauf ist die Schachtelungstiefe größer als 2, und in jedem Fall wird ein Unterprogramm erst bis zum Schluß durchlaufen, ehe es erneut aufgerufen wird. Es seien DH, DU1 , DU2 die lokalen Definitionsteile von H, U1 bzw U2 . Dann sind bei dem Ablauf in Abbildung 2.11 die Größen von DH in den Intervallen [0, t1 ] und [t4 , t5 ] gültig. Die von DU1 gelten in den Intervallen [t1 , t2 ] und [t3 , t4 ] und die von DU2 im Intervall [t2 , t3 ]. Wenn U1 im Intervall [t1 , t2 ] abläuft, können weder die Größen von DH noch die von DU2 angesprochen werden. Es gibt jedoch einen wesentlichen Unterschied: DH war im Intervall [0, t1 ] gültig und der zum Zeitpunkt t1 gültige Zustand muß auch zum Zeitpunkt t4 gelten. DH ist in [t1 , t2 ] vorhanden, jedoch inaktiv. Im Gegensatz dazu wird DU2 außerhalb des Intervalls gar nicht benötigt und in der Tat wird bei vielen Compilern (in der Regel auch bei C-Compilern) der zu DU2 gehörende Speicherplatz freigegeben, d. h. außerhalb von [t2 , t3 ] existert DU2 gar nicht. Man kann daher, wenn man ganz exakt sein will, zwischen Gültigkeitsbereich, Lebensdauer und Aktivitätsintervall der Daten eines Haupt- oder Unterprogramms unterscheiden. Z. B. ist DU1 im Unterprogramm U1 gültig, lebt bei dem Programmablauf von Abbildung 2.11 von t1 bis t4 und ist in ihm von t1 bis t2 und von t3 bis t4 aktiv. 86 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME Parameter- und Ergebnisübergabe: Obwohl, wie dargelegt, ein aufrufendes und ein aufgerufenes Programm den jeweils anderen Definitionsteil nicht sehen, muß es natürlich zwischen beiden einen Datenaustausch geben. Dazu muß eine beiden Programmen bekannte Beschreibung der auszutauschenden Daten vorhanden sein. In Analogie zu Datentypen nennt man diese oft den Typ des Unterprogramms. Der Typ eines Unterprogramms legt fest, von welchem Datentyp die an das Unterprogramm zu übergebenden Parameter und das zurückgelieferte Ergebnis sind. Als Sonderfall ist dabei zugelassen, daß es keine Parameter gibt und/oder daß kein Ergebnis zurückgeliefert wird. In C gibt es hierfür das Sprachelement void. Am saubersten sind aufrufendes und aufgerufenes Programm getrennt, wenn bei einem Aufruf nur Werte an das Unterprogramm übergeben werden, die Variablen des Aufrufers aber nicht bekannt sind. Für Parameter, bei denen nur Werte übergeben werden, spricht man von Wertübergabe (call by value). C z. B. kennt nur Wertübergabe. Der zu übergebende Wert kann an der Aufrufstelle im aufrufenden Programm direkt als Konstante, über eine zu dereferenzierende Variable oder einen komplizierteren Ausdruck bereitgestellt werden. Im Unterprogramm wird der Wert benutzt, z. B. für Berechnungen. Da es sich nicht um eine Variable handelt, kann er nicht geändert werden. In einigen Programmiersprachen gibt es auch die Möglichkeit, einen Variablennamen als Parameter anzugeben, und dem Unterprogramm wird die Adresse (und nicht der Wert) der Variablen übergeben. Man spricht von Adreßübergabe (call by reference). In diesem Fall kann im Unterprogramm der Wert der Variablen natürlich geändert werden. Wertübergabe von Parametern sichert eine saubere Trennung zwischen aufrufendem und aufgerufenem Programm. Es gibt jedoch auch Fälle, in denen es zweckmäßig ist, einem Unterprogramm die Adresse von Variablen des Aufrufers bekanntzugeben und ihm zu erlauben, die Werte dieser Variablen zu ändern. Zum Beispiel ist es nicht sinnvoll, die Werte einer großen Reihung in den Datenbereich eines Unterprogramms zu kopieren, um sie dort zu bearbeiten und dann zurückzuliefern. In C kann man das Problem dadurch lösen, daß man explizit festlegt, daß der übergebene Parameterwert nicht vom Typ <typ>, sondern vom Typ „Adresse (einer Variablen oder Konstanten) vom Typ <typ>“ ist. Zu Einzelheiten siehe Lowes/Paulik [LoweP1995]. „Zurückliefern eines Ergebnisses“ bedeutet, daß im aufrufenden Programm an der Aufrufstelle nach der Ausführung des Unterprogrammaufrufs ein Wert vom Typ des Ergebnisses zur Verfügung steht. Dieser Wert kann einer passenden Variablen zugewiesen oder zur Berechnung eines Ausdrucks genutzt werden. Wie dargelegt, kann ein Unterprogramm, das Adressen von Variablen des Aufrufers kennt, im aufrufenden Programm Änderungen vornehmen. Man spricht dann manchmal von Nebeneffekten (Seiteneffekt, side effect). Wenn möglich, sollen Nebeneffekte vermieden werden. Das ist jedoch nicht immer sinnvoll oder möglich. Zum Beispiel macht es in C Sinn, Zeichenreihen von einer Variablen in eine andere zu kopieren und dabei zu zählen, wie lang die kopierte Zeichenreihe ist. Ein entsprechendes Unterprogramm könnte als Eingabeparameter die Adressen der Quell- 2.8. UNTERPROGRAMME 87 und der Zielvariablen bekommen, als „Nebeneffekt“ den Inhalt der Zielvariablen ändern und als Ergebnis die Zahl der kopierten Zeichen zurückliefern. Globale Variable: Für den Datenaustausch zwischen aufrufendem und aufgerufenem Programm sind Parameterübergabe und Ergebnisrücklieferung der „offizielle“ Weg. Daß Adressen, die an das aufgerufene Programm übergeben werden, zu Nebeneffekten führen, ist eine leichte, aber oft notwendige Abweichung von der offiziellen Linie. Bei manchen Programmieraufgaben ist es zweckmäßig, noch weiter von dieser Linie abzuweichen. Als Beispiel sei die Bearbeitung einer Baumstruktur (siehe Kapitel 9) genannt, an der eine Reihe verschiedener Unterprogramme beteiligt sind. Man könnte bei jedem Unterprogrammaufruf dem Unterprogramm die Adresse des Wurzelknotens und andere benötigte allgemeine Informationen über den Baum als Parameter übermitteln. Das wäre jedoch unpraktisch und aufwendig. Zweckmäßiger ist es, die entsprechenden Variablen einheitlich und unter gleichem Namen in allen Unterprogrammen und dem Hauptprogramm zur Verfügung zu haben. In den meisten Programmiersprachen, auch C, kann das durch geeignete Sprachkonstrukte festgelegt werden. Man spricht dann von globalen Variablen. Auch Unterprogramme selbst müssen als solche definiert werden. Zur vollständigen Konstruktion eines Unterprogramms gehört die Festlegung des Namens, des Typs der Parameter und des Typs des Ergebnisses. Außerdem muß es programmiert werden, d. h. sein Definitionsteil und sein Anweisungsteil müssen spezifiziert werden. In Abhängigkeit von der Stelle im Programm, an der das Unterprogramm definiert wird und von anderen Angaben hat auch jedes Unterprogramm einen Gültigkeitsbereich. In der Praxis ist es gelegentlich nützlich, für Unterprogramme einen eingeschränkten Gültigkeitsbereich zu haben. Oft ist jedoch ein globaler Gültigkeitsbereich für alle Unterprogramme eines Gesamtprogramms vorzuziehen. In diesem Fall kann jedes Unterprogramm an jeder Stelle aufgerufen werden. Rekursive Aufrufe: Die meisten Programmiersprachen lassen zu, daß ein Unterprogramm sich selbst aufruft. Man spricht von einem rekursiven Unterprogrammaufruf (rekursiver Funktionsaufruf, recursive function call) oder auch von Rekursion (recursion). Ein erstes Beispiel für die Anwendung rekursiver Unterprogramme ist die Routine lvaname für die Ausgabe von Lehrveranstaltungsnamen. Siehe Tabelle 2.29. Sie benutzt die in Abbildung 2.9 gezeigte Baumstruktur. Die einzelnen rekursiven Aufrufe sind in Abbildung 2.10 wiedergegeben. Rekursion ist eine sehr wichtige Technik bei der Formulierung von Programmen und Algorithmen und wird in Teil II „Algorithmen“, Teil III „Einfache Datenstrukturen“ und in Teil IV „Allgemeine Graphen“ intensiv benutzt. Die Bezeichnungen Rekursion und rekursiver Unterprogrammaufruf werden auch benutzt, wenn ein Unterprogramm sich nicht direkt, sondern indirekt aufruft. Man spricht auch von gekoppelter Rekursion oder indirekten rekursiven Unterprogrammaufrufen. Abbildung 2.12 zeigt einige Beispiele für Aufrufschemata. Darin stellen Rechtecke Haupt- oder Unterpro- 88 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME H H H H ... ... ... ... ... ......... ......... .. ... ... ... ... ... ......... ......... .. ... ... ... ... ... ......... ........ ... ... ... ... ... ... ......... ......... .. U1 U1 U1 U1 ... ... ... ... ... .......... ........ .. ... ... ... ... ... .......... ....... ... U2 A ......... ... .. . .. . ... . ................................. .. B ... ........ ........ ... .... .. ... ... ... ... ... ... ... .......... ........ .. ......... ... .. . .. . ... . ................................ . .. ........ ........ ... .... .. ... ... U2 U2 C D Abbildung 2.12: Aufrufschemata für nichtrekursive, direkt rekursive und indirekt rekursive Unterprogramme gramme dar. Ein Pfeil bedeutet, daß bei einem Ablauf das Haupt- oder Unterprogramm am Pfeilanfang das Unterprogramm am Pfeilende aufrufen kann, aber nicht muß. Teil A des Bildes zeigt ein nichtrekursives Aufrufschema. Es entspricht dem Beispiel in Abbildung 2.11. Teil B stellt ein einfaches rekursives Aufrufschema dar. Das Unterprogramm U1 kann sich selber aufrufen. In Teil C ist eine gekoppelte Rekursion zu sehen, bei der die Unterprogramme U1 und U2 sich gegenseitig aufrufen können. Teil D schließlich dient als Beispiel für eine Mischung von einfacher und gekoppelter Rekursion. Ein Programm ist genau dann nichtrekursiv, wenn sein Aufrufschema (als gerichteter Graph aufgefaßt) keine Kreise enthält. Bei rekursiven Unterprogrammen sind Gültigkeitsbereich, Lebensdauer und Aktivitätsintervall komplizierter als bei nichtrekursiven. Sie hängen von der Aufrufstufe eines Unterprogramms ab. Das soll am Beispiel der Abbildung 2.13 erläuter werden. Es gibt ein Hauptprogramm H und zwei Unterprogramme, U1 und U2 . U1 kann in H und U2 in U1 aufgerufen werden. Darüber hinaus kann U2 seinerseits U1 aufrufen. Es liegt das Aufrufschema von Teil C, Abbildung 2.12 vor. Es kann jetzt vorkommen, daß ein Unterprogramm schon dann erneut aufgerufen wird, wenn es noch nicht zum Schluß gekommen ist. Die Schachtelungstiefe der Abläufe ist nicht mehr begrenzt; bei einem fehlerhaften Programmablauf kann sie im Prinzip über alle Grenzen wachsen. Abbildung 2.13 zeigt einen Ablauf mit zwei Aufrufen eines jeden Unterprogramms (U1 (1), U1 (2), U2 (1), U2 (2)). Jedes der beiden Unterprogramme läuft in zwei Aufrufstufen mit einem eigenen Definitionsteil ab. Für DU1 (1), den Definitionsteil der ersten Aufrufstufe von U1 , gilt z. B.: Er ist in U1 (1) gültig, lebt von t1 bis t8 und ist von t1 bis t2 und von t7 bis t8 aktiv. 2.8. UNTERPROGRAMME H 89 . . . . . . . . . U1 (1) . . . . . . . . . . . . . . . . . . U2 (1) . . . . . . . . . . . . . . . . . . U1 (2) . . . . . . . . . U2 (2) 0 t1 t2 t3 t4 . . . . . . . . . t5 . . . . . . . . . t6 t7 t8 t9 t .. ..................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................... .. Abbildung 2.13: Rekursive Unterprogrammaufrufe Externe Prozeduren: Den bisherigen Betrachtungen über Unterprogramme lag die Annahme zugrunde, daß ein Gesamtprogramm mit seinem Hauptprogramm und seinen Unterprogrammen als eine Einheit programmiert und übersetzt wird. Dem Compiler sind dann zur Übersetzungszeit alle Definitionsteile, insbesondere auch die Typen aller Unterprogramme bekannt (siehe Seite 86). Es kommt aber häufig vor, daß in einem Programm Unterprogramme aufgerufen werden, die nicht zusammen mit dem Hauptprogramm übersetzt werden. Diese Unterprogramme sollen externe Prozeduren genannt werden. Um die Aufrufe von externen Unterprogrammen übersetzen zu können, muß der Compiler allerdings ihren Typ kennen. Es müssen bei der Übersetzung Typbeschreibungen der externen Unterprogramme vorhanden sein. In C werden .h-Dateien (z. B. stdio.h) dafür benutzt. Bei externen Prozeduren ist es besonders zweckmäßig, den Unterprogrammaufruf als Erweiterung der benutzten Programmiersprache aufzufassen (siehe Seite 83). Man ruft mit dem Unterprogramm einen Dienst auf. Externe Prozeduren lassen sich einteilen in • Unterprogramme, die zum Gesamtprogramm gehören, aber aus besonderen Gründen getrennt übersetzt und später durch Binden (siehe Unterabschnitt 4.3.2, Seite 144) hinzugefügt werden. Gründe für eine getrennte Übersetzung können z. B sein: Das Programm wird für eine Übersetzung zu groß, mehrere Programmierer arbeiten am gleichen Programm, man will das Unterprogramm in einer anderen Programmiersprache schreiben als das Hauptprogramm. • Systemdienste. Hierunter versteht man Unterprogramme, die wesentliche und not- 90 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME wendige Dienste bieten; Dienste, die aber nicht als direkte Konstrukte in der Programmiersprache vorhanden sind. In C spricht man von Standardfunktionen. Systemdienste sind zum Teil normale, getrennt übersetzte Unterprogramme, die häufig anfallende Aufgaben übernehmen, wie z. B. Formatumsetzungen, Bearbeitung und Vergleiche von Zeichenreihen, mathematische Berechnungen elementarer Funktionen. Zum Teil sind es Unterprogramme, über die ein Zugang zu den Diensten des Betriebssystems (siehe Unterabschnitt 4.3.1, Seite 139) bereitgestellt wird. Hierzu sind zum Beispiel die Unterprogramme, mit denen Ein- und Ausgaben durchgeführt werden, zu zählen. Systemdienste für eine Programmiersprache gehören i. a. dazu, wenn man einen Compiler für diese Sprache bezieht. Man spricht vom Laufzeitsystem (run time system) des Compilers16 . Die Programme eines Laufzeitsystems werden i. a. in einer Laufzeitbibliothek zusammengefaßt (siehe Seite 144). Etwas Ähnliches wie Systemdienste sind Unterprogramme aus Unterprogrammbibliotheken, auch kurz Bibliotheken (library) genannt. Hierbei handelt es sich um Sammlungen von Unterprogrammen für spezielle Aufgaben, z. B. für Matrizenrechnung oder Computergrafik. Diese Sammlungen gehören in der Regel nicht zum Leistungsumfang einer Compilerlieferung. • Prozedurfernaufrufe (remote procedure call, RPC). In modernen Rechensystemen werden viele Aufgaben durch Arbeitsteilung gelöst. Eine Form der Arbeitsteilung, die sich als sehr zweckmäßig erwiesen hat, ist Client-Server-Betrieb. Dabei läuft für eine zusammengehörende Klasse von Aufgaben ein eigenes, im allgemeinen umfangreiches Programm ab, der Server (Dienstleister). So gibt es z. B. für Datenbankaufgaben einen Datenbankserver, für rechenintensive Aufgaben eine Rechenserver, auch Compute-Server genannt, für den Zugang zum Netz einen Netzserver usw. Programmläufe, die einen dieser Dienste von einem Server anfordern, heißen Kunden (client). Kunde und Server können Programmabläufe auf dem gleichen Rechner sein. Häufiger ist es, daß Kunde und Server auf verschiedenen Rechnern, die vernetzt sind (siehe Abschnitt 22.3, Seite 576), ablaufen17 . Kunde und Server können auf unterschiedliche Art miteinander kommunizieren. Eine bequeme und hierfür häufig benutzte Form der Interprozeßkommunikation ist der Prozedurfernaufruf. Der Kunde ruft den benötigten Dienst so auf, als wäre es ein internes Unterprogramm. Wie dabei Daten zwischen Kunden und Server ausgetauscht werden, ist für Prozedurfernaufrufe normiert. Welche Bedeutung die Daten haben und wie der Server arbeitet, ist natürlich anwendungsabhängig. Auf einem gegebenen Rechner sind die Laufzeitsysteme verschiedener Compiler der gleichen Programmiersprache nicht notwendigerweise in allen Punkten identisch, denn nicht für alle Systemdienste gibt es normierte Festlegungen. 17 Ist für ein (eventuell auch für mehrere) Serverprogramm ein eigener Rechner reserviert, so ist es üblich auch diesen als Server zu bezeichnen. 16 2.8. UNTERPROGRAMME 91 Literatur Zum Schichtenaufbau eines Rechensystems siehe das Buch von Tanenbaum [Tane1990]. Das Standardwerk für C ist Kernighan/Ritchie [KernR1988]. Es gibt eine deutsche Übersetzung [KernR1990] sowie ein Lösungsbuch für die Aufgaben [TondG1989]. Es sei auch auf das Buch Lowes/Paulik [LoweP1995] hingewiesen. Eine breit angelegte Einführung in die Informatik mit C ist in Impagliazzo/Nagin [ImpaN1995] zu finden. Zu den Grundbegriffen der Programmierung, wie sie in diesem Kapitel dargestellt wird, siehe auch Goos [Goos1996], Abschnitt 8.1 . In Appelrath/Ludewig ([AppeL1995], Kapitel 2) werden die Grundbegriffe der Programmierung am Beispiel der Programmiersprache Modula-2 erläutert. Ein weiterführendes Buch, das Programmiersprachen allgemein behandelt, ist Sethi [Seth1996]. Darstellung und Bedeutung von Werten werden ausführlich und aus allgemeinerer Sicht bei Kowalk ([Kowa1996], S.99-126) behandelt. Das gleiche gilt für Ausdrücke ([Kowa1996], S.126-142). In dem Buch von Langsam/Augenstein/Tenenbaum [LangAT1996], Kapitel 1, werden Datentypen von C und Klassen von C++ behandelt. Dabei wird auch auf abstrakte Datentypen eingegangen. Unterabschnitt 2.5.4 „Wahrheitswerte“ gibt einen allerersten Einblick in die (mathematische) Logik. Ausführlichere, aber immer noch als Einführung für Informatiker gedachte Darstellungen findet man bei Goos [Goos1997], Kapitel 4, Kowalk [Kowa1996], Anhang A und Aho/Ullman [AhoU1995], Kapitel 12, 13, 14, 15. Anweisungen, Anweisungsfolgen und Programmsteuerung sind ausführlich in Kowalk ([Kowa1996], S.143-172) beschrieben. In Kowalk ([Kowa1996], S.211-249) findet man auch eine ausführliche Darstellung von Unterprogrammen. Zu diesem Punkt siehe auch Sethi [Seth1996]. Rekursion wird ausführlich in Kapitel 2 von Aho/Ullman [AhoU1995] behandelt. Literatur zu ergänzenden Gebieten: Es sollen hier einige wichtige, zur Programmierung gehörende Gebiete, die in diesem Kapitel nicht angesprochen werden konnten, erwähnt werden. Zeiten sind wichtige Werte, auf deren Darstellung und Bearbeitung in diesem Kapitel nicht eingegangen wurde. Für C wird auf das Kapitel „Termine und Zeiten“ in Lowes/Paulik [LoweP1995] verwiesen. Große Programme werden nur dann sauber und erfolgreich sein, wenn man eine systematische Programmierung einhält. Ein Hilfsmittel dazu sind Module. Ein Programmodul ist eine Zusammenfassung von Daten und Operatoren, für die Zugriffe und Aufrufe nicht beliebig, sondern nur in kontrollierter Form möglich sind. Zu Modulen siehe Kowalk ([Kowa1996], S.255-263). 92 KAPITEL 2. PROGRAMMIEREN I: SEQUENTIELLE PROGRAMME Eine nützliche Methode, systematisch zu programmieren, ist strukturierte Programmierung. Dabei wird ein Programm schrittweise verfeinert. Für die Programmsteuerung sind nur Sequenzen, Alternativen und Schleifen, aber nicht rekursive Aufrufe zugelassen. Auf strukturierte Programmierung wird kurz in Abschnitt 5.1.2, Seite 155 eingegangen. Eine ausführlichere Darstellung findet man in Goos [Goos1996], Kapitel 9. In neuerer Zeit hat sich objektorientiertes Programmieren als besonders erfolgreich für systematisches Programmieren erwiesen und starke Akzeptanz gefunden. Charakteristisch für diese Art des Programmierens ist die Zusammenfassung von Objekten zu Klassen. Bearbeitung von Objekten ist ausschließlich über vorgegebene Methoden möglich. Besonders wichtig ist die Möglichkeit, auf einfache Art aus gegebenen Klassen neue zu erzeugen sowie Eigenschaften und Methoden auf diese zu übertragen, zu vererben. In allgemeiner Form wird objektorientiertes Programmieren bei Kowalk [Kowa1996], Abschnitt 8.2, und Goos [Goos1996], Kapitel 10, behandelt. Spezielle sprachspezifische Darstellungen findet man in den Lehrbüchern über objektorientierte Programmiersprachen, z. B. C++ (Stroustrup [Stro1991], Lippman [Lipp1992], RRZN [RRZN1993]), Java (Arnold/Gosling [ArnoG1996a], Doberkat/Dißmann, [DobeD1999], Flanagan [Flan1996]), Smalltalk (Bothner/Kähler [BothK1998]). Die objektorientierte Programmiersprache C++ liegt auch einigen allgemeinen Darstellungen von Algorithmen und Datenstrukturen zugrunde, z. B. Sedgewick [Sedg1998], Schaerer [Scha1994] oder das schon erwähnte Buch Langsam/ Augenstein/Tenenbaum [LangAT1996]. Programme sollen fehlerfrei arbeiten. Das wichtigste Hilfsmittel hierfür ist das Testen. Es sei auf die Bücher von Appelrath/Ludewig [AppeL1995], Kapitel 4, und Kowalk [Kowa1996], Kapitel 12, hingewiesen. Unter Softwaretechnik versteht man Methoden, mit denen komplexe Programmsysteme erstellt und in Betrieb genommen werden. Es sei auf Kowalk [Kowa1996], Kapitel 13, und auf das umfangreiche zweibändige Werk von Balzert ([Balz1996] und [Balz1998]) hingewiesen. Einen wichtigen Teilbereich der Softwaretechnik bilden objektorientierte Analyse und objektorientierter Entwurf. Siehe hierzu Kowalk [Kowa1996], Kapitel 18. Spezielles Lehrbücher sind Rumbough et al. [RumbBPEL1991] sowie Coad/Yourdon [CoadY1991] und [CoadY1991a]. Schließlich soll auch das Gebiet der Programmverifikation erwähnt werden. In diesem Gebiet bemüht man sich, die Korrektheit von Programmen automatisch und mit formalen Methoden nachzuweisen. Die Korrektheit eines Programms wird nachgewiesen, indem bewiesen wird, daß es genau das tut, was seine Spezifikation verlangt. Trotz vieler Erkenntnisse und jahrelanger Erfahrung ist Programmverifikation immer noch nicht von großer praktischer Bedeutung. Zur Einführung in die Programmverifikation siehe Appelrath/Ludewig [AppeL1995], Abschnitt 4.2, Goos [Goos1996], Abschnitt 8.2, oder Kowalk [Kowa1996], Kapitel 11. Bei Kowalk sind auch Ausführungen zur Semantik von Programmiersprachen zu finden. Kapitel 3 Darstellung von Daten durch Bitmuster Rechner werden gebaut, um Aufgaben aus der realen Welt zu bearbeiten und zu lösen. Einige Beispiele: 1. Numerische Berechnungen aller Art, z. B. Wettervorhersage. 2. Verwaltungsaufgaben, z.B. das Einwohnermeldewesen einer Stadt. 3. Steuerung technischer Prozesse wie z.B. Straßenverkehr, Raumflugkörper, chemische Prozesse, Walzstraßen, Intensivstationen. Um solche Aufgabenstellungen in Rechnern nachbilden und bearbeiten zu können, muß für die Daten und die Bearbeitungsvorschriften der Daten eine Darstellung gefunden werden, die ein Rechner versteht und verarbeiten kann. Man braucht ein rechnergerechtes Modell der realen Aufgabenstellung. Die Gewinnung eines solchen Modells geht über mehrere Stufen entsprechend den verschiedenen Maschinenschichten von Abschnitt 2.1. Im Inneren moderner Rechner werden alle Informationen durch binäre Elemente, d.h. durch Elemente, die zwei Zustände annehmen, dargestellt. Es ist also nötig, die Daten und Bearbeitungsvorschriften der realen Aufgabenstellung auf binäre Informationsdarstellungen zurückzuführen. Die Darstellung von Daten, insbesondere Zahlen, durch binäre Elemente ist sehr viel älter als Computer und geht auf Leibniz 1 zurück. Leibniz, Gottfried Wilhelm, ∗1646 Leipzig, †1716 Hannover. Deutscher Philosoph und Universalgelehrter. Diplomat des Mainzer Kurfürsten, später Hofrat und Bibliothekar in Hannover. Schuf die Philosophie der Monaden. In seiner Dyadik legte er die Gundlagen für binäre Datendarstellungen. Er entdeckte die Grundlagen der Differential- und Integralrechnung etwas früher als Newton .2 1 Newton, Sir Isaac, ∗1643 Woolsthorpe bei Grantham (England), †1727 Kensington (heute zu London). Englischer Mathematiker, Physiker und Astronom. Begründer der klassischen theoretischen Physik, insbesondere der Mechanik. Schuf etwas später als Leibniz, aber unabhängig von ihm, die Grundlagen der Differential- und Integralrechnung. 2 93 94 KAPITEL 3. DARSTELLUNG VON DATEN DURCH BITMUSTER 3.1 Bits und Bitmuster Definition 3.1 Ein Element, das zwei Zustände annehmen kann und auch stets genau einen der beiden Zustände enthält, soll Bitspeicherstelle genannt werden. Der Wert, den es enthält, heißt Bit. Die beiden Zustände für Bits können technisch unterschiedlich realisiert sein. Zum Beispiel: • Strom fließt / Strom fließt nicht • große Spannung / kleine Spannung • positive Spannung / negative Spannung • zwei verschiedene Magnetisierungsrichtungen Die Zustände sollen unabhängig von der technischen Realisierung mit 0 und 1 bezeichnet werden. Die Symbole 0 und 1 stellen (zunächst) nur die beiden Zustände und keine Zahlen dar. Die Terminologie ist ungenau: Mit „Bit“ wird oft auch die Speicherstelle bezeichnet, die einen der Werte 0 oder 1 enthält. Ob ein Bit als Speicherstelle oder Wert der Speicherstelle gemeint ist, wird i. a. durch den Zusammenhang klar. Unter einem Bitmuster soll eine endliche Folge von Bits verstanden werden. Abbildung 3.1 zeigt eine Folgen von sieben Bitspeicherstellen, die als aktuellen Wert das Bitmuster 0 1 1 0 1 1 0 Abbildung 3.1: Bitspeicherstellen mit Bitmuster 0110110 enthält. Bitmuster der Länge 8 heißen Bytes. Vom Zusammenhang abhängend kann Byte auch Speicherstelle für 8 Bits bedeuten. Bitmuster haben in Rechnern (in der Schicht der konventionellen Maschine) zwei verschiedene Bedeutungen: • Maschinenbefehle Maschinenbefehle führen Operationen in Rechnern aus. Sie ◦ verändern Bitmuster und ◦ steuern die Ablauffolge von Operationen. Maschinenbefehle werden in Abschnitt 4.2 behandelt. 3.2. DARSTELLUNG NATÜRLICHER ZAHLEN 95 • Daten Daten lassen sich zum einen unterteilen in ◦ Daten des realen Problems und ◦ Daten für die interne Verwaltung im Rechner. Zum anderen sind Daten ◦ elementare Daten (in der konventionellen Maschine verfügbar) oder ◦ zusammengesetzte Daten (in der konventionellen Maschine nicht verfügbar). Im folgenden wird nur auf die Darstellung elementarer Daten durch Bitmuster eingegangen. Elementare Daten sind: a. Zahlen i. Natürliche Zahlen ii. Ganze Zahlen iii. Rationale Zahlen b. Zeichenreihen c. Wahrheitswerte d. Bitmuster im eigentlichen Sinne Wir wollen den Abschnitt mit einem einfachen, aber wichtigen Satz abschließen. Satz 3.1 Eine Folge von k Bitstellen kann genau 2k verschiedene Bitmuster annehmen. Beweis: Durch vollständige Induktion. (i) Die Behauptung ist richtig für k = 1. Es gibt 21 = 2 Werte, die Werte 0 und 1. (ii) Die Behauptung sei richtig für k. Bei k + 1 Bitsstellen kommt zu jedem Bitmuster der Länge k entweder eine 0 oder eine 1 hinzu. Also gibt es 2 · 2k = 2k+1 Bitmuster. 2 96 KAPITEL 3. DARSTELLUNG VON DATEN DURCH BITMUSTER h h h h h h h h h h h h h h X XXX Tabelle 3.1: Absolute Darstellungen natürlicher Zahlen XXIX 753 573 Tabelle 3.2: Positionsabhängige Darstellungen natürlicher Zahlen 3.2 Darstellung natürlicher Zahlen Zunächst einige allgemeine Überlegungen. Natürliche Zahlen werden durch Zeichenreihen oder Bilder dargestellt. Tabelle 3.1 zeigt einige absolute Darstellungen natürlicher Zahlen, Tabelle 3.2 zeigt Beispiele für positionsabhängige Darstellungen. Besonders wichtig sind q-adische Darstellungen, zu denen auch die übliche Dezimaldarstellung gehört. Als Grundlage dafür zunächst zwei Sätze. Für beide Sätze soll gelten: k ∈ N (k ≥ 1); q ∈ N (q ≥ 2); bn , b0n ∈ N (0 ≤ bn , b0n < q). Satz 3.2 Jedes m ∈ N mit 0 ≤ m ≤ q k − 1 besitzt eine eindeutige Darstellung m= k−1 X bn q k−1−n n=0 Satz 3.3 Ist m1 = b0 q k−1 + b1 q k−2 + · · · + bk−1 q 0 und m2 = b00 q k−1 + b01 q k−2 + · · · + b0k−1 q 0 so folgt aus b0 > b00 stets m1 > m2 . Beim Beweisen wird gebraucht: also q k − 1 = (q − 1)q k−1 + (q − 1)q k−2 + · · · + (q − 1)q 0 q k > (q − 1)q k−1 + (q − 1)q k−2 + · · · + (q − 1)q 0 (?) 3.2. DARSTELLUNG NATÜRLICHER ZAHLEN 97 Beweis Satz 3.3: Der Satz ist richtig für k = 1. Sei k ≥ 2. Ist b0 > b00 , so ist b0 − b00 ≥ 1, d. h. m1 − b00 q k−1 = ≥ > ≥ = (b0 − b00 )q k−1 + b1 q k−2 + · · · + bk−1 q 0 q k−1 und wegen (?) k−2 k−3 0 (q − 1)q + (q − 1)q + · · · + (q − 1)q 0 k−2 0 k−3 0 b1 q + b2 q + · · · + bk−1 q 0 m2 − b00 q k−1 . Also m1 > m2 . 2 Beweis Satz 3.2: a. Existenz einer Darstellung Es gilt m = b0 q k−1 + r0 mit 0 ≤ r0 ≤ q k−1 − 1 r0 = b1 q k−2 + r1 mit 0 ≤ r1 ≤ q k−2 − 1 · · · rk−3 = bk−2 q + rk−2 mit 0 ≤ rk−2 ≤ q − 1 Dabei gilt zusätzlich b0 b1 bk−2 < q, denn sonst wäre m = b0 q k−1 + r0 ≥ q k + r0 ≥ q k < q, denn sonst wäre r0 ≥ q k−1 · · · < q, denn sonst wäre rk−3 ≥ q 2 , wobei m < q k nach Voraussetzung des Satzes gelten soll. Mit bk−1 := rk−2 ergibt sich schließlich die Darstellung m = b0 q k−1 + b1 q k−2 + · · · + bk−2 q + bk−1 . b. Eindeutigkeit Es habe m zwei verschiedene Darstellungen m = b0 q k−1 + · · · + bk−1 q 0 m = b00 q k−1 + · · · + b0k−1 q 0 . Sei ν der erste Index von links, bei dem bν 6= b0ν , und bν > b0ν . Dann gilt nach Satz 3.3 bν q k−ν−1 + bν+1 q k−ν−2 + · · · + bk−1 q 0 > b0ν q k−ν−1 + b0ν+1 q k−ν−2 + · · · + b0k−1 q 0 . 98 KAPITEL 3. DARSTELLUNG VON DATEN DURCH BITMUSTER Daraus folgt m − m = bν q k−ν−1 + bν+1 q k−ν−2 + · · · + bk−1 q 0 − b0ν q k−ν−1 b0ν+1 q k−ν−2 + · · · + b0k−1 q 0 > 0. Dieser Widerspruch zeigt, daß es nur eine Darstellung geben kann. 2 Satz 3.2 liefert die q-adische Darstellung natürlicher Zahlen. Es ist die von der Dezimaldarstellung bekannte Kurzdarstellung durch Positionsschreibweise üblich (Abbildung 3.2). Mit wachsendem k hat jedes m unendlich viele Darstellungen. Falls m > 0, gibt es q k−1 q k−2 b0 b1 q q q q q q q2 q1 q0 bk−3 bk−2 bk−1 Abbildung 3.2: Kurzdarstellung durch Positionsschreibweise darunter aber nur eine ohne führende Nullen. Bei Dezimaldarstellungen schreibt man keine führenden Nullen und die Null einstellig. In Rechnern ist k festgelegt, und man stellt stets alle k Ziffern dar. Aus Informatiksicht ist noch die folgende Bemerkung wichtig: Bisher haben wir natürliche Zahlen durch (i. a. andere) natürliche Zahlen ausgedrückt, jedoch noch nicht festgelegt, daß die natürlichen Zahlen 0, 1, · · · , (q − 1) durch q verschiedene Zeichen (q-adische Ziffern) dargestellt werden und welches diese Zeichen sind. Beispiel 3.1 Im folgenden sind drei übliche und ein ungebräuchlicher Satz von q-adische Ziffern angegeben. 10-adische (dezimal): 2-adische (dual, binär): 5-adische: 5-adische (unkonventionell): 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 0, 1 0, 1, 2, 3, 4, 2, , 4, #, $ Es muß klar sein, welches Zeichen zu welcher Zahl gehört. Das ist insbesondere für den letzten Ziffernsatz wichtig. 2 Nach den vorangehenden allgemeinen Betrachtungen soll im folgenden der für Rechner wesentliche Fall q = 2 und k fest betrachtet werden. Es ergibt sich die Dualdarstellung natürlicher Zahlen durch Bitmuster der Länge k: b0 b1 b2 · · · bk−1 = b0 · 2k−1 + b1 · 2k−2 + · · · + bk−1 · 20 3.2. DARSTELLUNG NATÜRLICHER ZAHLEN 99 mit der (erst jetzt zu treffenden) Festlegung: Zeichen 0 entspricht Zahl 0 und Zeichen 1 entspricht Zahl 1. Es wird auch die Bezeichnung Binärdarstellung benutzt. Der darstellbare Bereich ist: 0, 1, · · · , 2k − 1 . Die folgenden Bitmusterlängen und Zahlbereiche sind in Rechnern üblich: k k k = 8 = 16 = 32 0, 1, · · · , 28 − 1 = 255 0, 1, · · · , 216 − 1 = 65.535 0, 1, · · · , 232 − 1 = 4.294.967.295 Beispiel 3.2 Zwei Beispiele für die Dualdarstellung natürlicher Zahlen: 1. k=8 10010011 = 1 · 27 + 0 · 26 + 0 · 25 + 1 · 24 + 0 · 23 + 0 · 21 + 1 · 21 + 1 · 20 = 128 + 16 + 2 + 1 = 147 2. k = 10 0111001000 = 0 · 29 + 1 · 28 + 1 · 27 + 1 · 26 + 0 · 25 + 0 · 2 · 24 + 1 · 23 + 0 · 22 + 0 · 21 + 0 · 20 = 256 + 128 + 64 + 8 = 456 2 Wenn auf die Basis der Zahldarstellung ausdrücklich hingewiesen werden soll, wird die Schreibweise 10010011b = 147d angewandt. Falls die Bedeutungen aus dem Zusammenhang klar werden, kann der Index b und/oder der Index d fortgelassen werden: 10010011 = 147 . Ein vorgegebenes Bitmuster kann jedoch auch eine andere Bedeutung haben und eine andere Zahl darstellen. Zum Beispiel gilt bei Zweierkomplementdarstellung der Länge k = 8 (wird in Abschnitt 3.3 eingeführt): 10010011 = −109 . Merke: Das Gleichheitszeichen zwischen Bitmustern und dargestellten Objekten ist stets im Zusammenhang als „stellt dar“ zu interpretieren. Abschließende Bemerkung: So nützlich, natürlich und häufig die Interpretation von Bitmustern als Dualdarstellung natürlicher Zahlen auch ist, man kann die Zahlen 0, 1, · · · , 2k−1 auch anders durch Bitmuster der Länge k darstellen. 100 KAPITEL 3. DARSTELLUNG VON DATEN DURCH BITMUSTER Beispiel 3.3 Darstellung durch Vertauschen von Nullen und Einsen 0 1 9 13 127 128 255 11111111 11111110 11110110 11110010 10000000 01111111 00000000 2 Wieviele Abbildungen von Bitmustern der Länge k auf die natürlichen Zahlen 0, 1, · · · , 2k−1 gibt es (Abbildung 3.3)? Es gibt so viele Darstellungen der ersten 2k natürlichen Zahlen ............................................ ........... ........ ........ ...... ...... ...... ...... ..... . . . . .... ... . .... . .. ... . . ... ... . ... .. ... . .. ... . ... .. . ... .... ... ... .. .. ... .. .. ... ... .. ... .. .... ... ... ... ... .. ... ... ... .. ... .. . . ... . ... ... .... .... .... .... ..... .... . ...... . . . ...... ... ........ ...... ....... .......... ............................................... Bitmuster der Länge k f (bijektiv) ............................................ ........... ........ ........ ...... ...... ...... ...... ..... . . . . .... .. . . .... . ... ... . . ... ... ... . . . ... . ... ... . ... .. . ... . . . ... .... .. .. ... .. .. ... ... ... ... ... .. . ... . . k . ... ... ... ... ... .. ... .. ... . . ... ... ... .. ... ... .... ... ..... ..... . ...... . . . ....... .... ........ ...... ........... ........ .......................................... - natürliche Zahlen 0, 1, · · · , 2 − 1 Abbildung 3.3: Darstellung natürlicher Zahlen durch Bitmuster durch Bitmuster der Länge k wie es bijektive Selbstabbildungen der Menge der k-stelligen Bitmuster auf sich selbst gibt, also 2k ! : f | f bijektive Selbstabbildung von {0, 1}k = (Anzahl Bitmuster der Länge k)! = 2k ! . Für k = 8 ergibt das 2k ! = 256! > 100157 > 10300 Möglichkeiten. Zur Darstellung eines Bereichs natürlicher Zahlen werden nicht immer alle Bitmuster einer festen Länge k benutzt (Abbildung 3.4). Beispiel 3.4 Natürliche Zahlen werden zum Teil in Dezimaldarstellung durch binärcodierte Dezimalziffern dargestellt. Zur Darstellung der Zahlen 0, 1, · · · , 9 werden Bitmuster 3.3. DARSTELLUNG GANZER ZAHLEN ..... ................... ............................ ......... ....... ....... ...... ...... ...... . . . . . ..... . ..... ... .... .... . . ... .. . . ... ... ... . ... .. . ......................... . . .. . . . . . . ...... .... ... . . .. . . . . . ... .... ... ... . . ... .. .. . . ... .. ... . .. ... ... .... .... .. . .. ... .. .. ... .. .... ... . . ... ... . . ... . ... ... .... .... .. ... ....... .... .... ... ............................. ... .. . . ... ... ... ... ... .... ... ... ... . . . ..... ..... ..... ..... ...... ...... ....... ....... ......... ........ ................ . . . . . . . . . . . . . . . . ............. Bitmuster der Länge k 101 ................................... ............. ......... ........ ....... ....... ...... ..... ..... . . . . ..... ... . . .... . ... .... . . .. ... . . ... ... . ... .. ... . ....................... . . . . ... . . .... . ...... ..... ... . . . ... . . ... ... ... ... ... . .. ... ... .... ... ... ... ... .. ... ... ... ... .. ... .. . . ... .. . ... . . . . ... ... . . . . . . ... ... . . . . ..... . ... . . ....... .... ... ... ............................. ... ... ... .. ... ... . ... . .. .... ... .... .... ..... ..... ...... ..... . ...... . . . . .. ....... ....... ......... ................................................... f partiell - Natürliche Zahlen Abbildung 3.4: Darstellung natürlicher Zahlen durch partielle Abbildungen der Länge k = 4 verwendet. 0000 → 7 0 0001 → 7 1 0010 → 7 2 .. . 1000 → 7 8 1001 → 7 9 1010 1011 1100 1101 1110 1111 Diesen Bitmustern werden keine Zahlen, jedoch manchmal Vorzeichen zugeordnet. 2 Anmerkung 3.1 Alle modernen Rechner benutzen zur Darstellung natürlicher Zahlen die auf Seite 98 eingeführte Dualdarstellung. Die Anordnung der Bits innerhalb eines Speicherwortes variiert jedoch bei den verschiedenen Rechnertypen. Es sind zwei unterschiedliche Anordnungen in der Praxis üblich, bekannt unter den Namen „big endean“ und „little endean“. Weitere Einzelheiten sind in Anmerkung 4.1, Seite 124, zu finden. 2 3.3 Darstellung ganzer Zahlen Mit Bitmustern der Länge k können maximal 2k ganze Zahlen dargestellt werden. Welche Zahlen soll man nehmen und wie darstellen? Es ist sinnvoll, möglichst so viele positive wie negative zu haben. Wählt man als nichtnegative Zahlen 0, 1, · · · , 2k−1 − 2, 2k−1 − 1 , so sind 2k−1 Bitmuster verbraucht. Es bleiben 2k−1 Bitmuster frei. Welche Bitmuster sind verbraucht? Für die nichtnegativen Zahlen wird naheliegenderweise die Dualdarstellung natürlicher Zahlen benutzt, und damit haben die nichtnegativen Zahlen in der Darstellung links eine Null. Es bleiben die Bitmuster übrig, die dort eine Eins haben. Eine 102 KAPITEL 3. DARSTELLUNG VON DATEN DURCH BITMUSTER naheliegende Lösung zur Darstellung der negativen Zahlen wäre, das erste (linke) Bit als Vorzeichenstelle mit der Zuordnung und 0 = ˆ + 1 = ˆ − zu nehmen. Die übrigen Bits geben den Betrag an, man hat die Vorzeichen - Betrag Darstellung. Beispiel 3.5 k=4 Es können 24 = 16 Zahlen dargestellt werden, davon sind 23 = 8 nicht negativ: 0, 1, 2, 3, 4, 5, 6, 7 Ihre Dualdarstellung ist: 0 1 2 3 = = = = 0000 0001 0010 0011 4 5 6 7 = = = = 0100 0101 0110 0111 Für die negativen Zahlen ergeben sich bei Vorzeichen-Betrag-Darstellung die Zuordnungen −0 = 0 −1 −2 −3 = = = = 1000 1001 1010 1011 −4 −5 −6 −7 = = = = 1100 1101 1110 1111 2 Die Vorzeichen-Betrag-Darstellung wird zur Darstellung ganzer Zahlen in Rechnern nicht (mehr) benutzt, wohl aber zur Darstellung anderer Zahlen. Ein wesentlicher Nachteil ist die doppelte Darstellung der Null. In modernen Rechnern benutzt man statt dessen für ganze Zahlen die Zweierkomplementdarstellung. Abbildung 3.5 zeigt die Zuordnung der Bitmuster einmal für den Fall, daß nur natürliche Zahlen dargestellt werden und zum anderen für den Fall, daß ganze Zahlen im Zweierkomplement dargestellt werden. Man sieht: Ganze Zahlen im Zweierkomplement (k Bits) m≥0 m<0 Natürliche Zahlen in Dualdarstellung (k Bits) 7 → m k 7→ 2 − |m| . 3.3. DARSTELLUNG GANZER ZAHLEN 103 2k−1 − 1 2k−1 0 ? z }| −2k−1 { −1 | {z } z }| { 0 ? | 2k − 1 {z } 2k−1 − 1 Abbildung 3.5: Zweierkomplementdarstellung ganzer Zahlen Die darstellbaren nichtnegativen Zahlen behalten die Darstellung, die sie haben, wenn nur natürliche und nicht auch negative Zahlen darzustellen wären. Eine darstellbare negative Zahl m hat die gleiche Darstellung, wie sie die natürliche Zahl 2k − |m| hätte, wenn nur natürliche Zahlen dargestellt würden. Wie erhält man nun die Bitmuster für die negativen Zahlen? Das soll nur als Algorithmus und ohne weitere Begründungen erläutert werden. Um das Zweierkomplement eines Bitmusters der Länge k zu gewinnen, werden die folgenden Operationen mit dem Bitmuster ausgeführt: 1. Logisch invertieren 2. Ergebnis als natürliche Zahl in k Bits auffassen und 1 addieren 3. Überlauf (über 2k − 1) vernachlässigen Damit hat man a. die Festlegung der Darstellung einer negativen Zahl m über die Dualdarstellung der positiven Zahl 2k − |m| und b. einen Algorithmus, der zu einem Bitmuster das Zweierkomplement bildet. Es ist nicht selbstverständlich, daß beides das gleiche ergibt. Für −(2k−1 − 1) ≤ m ≤ 2k−1 − 1 gilt jedoch: Darstellung von (−m) = Zweierkomplement der Darstellung von m Beispiel 3.6 (Fortsetzung von Beispiel 3.5) (k = 4) Die Darstellungen der negativen Zahlen ergeben sich als Zweierkomplemente der Beträge: 104 KAPITEL 3. DARSTELLUNG VON DATEN DURCH BITMUSTER −1 = −2 −3 −4 −5 −6 −7 = = = = = = Zweierkomplement von 0001 = 1110 + 1 = 1111 1111 = = = = = = 1110 1101 1100 1011 1010 1001 Einige zusätzliche Tests: = Zweierkomplement von 0000 = −0 1111 + 1 = 0000 = 0101 1|0000 = Zweierkomplement von 1011 = −(−5) 0100 + 1 0101 Anmerkung: Das Zweierkomplement von 1000 = 0111 + 1 = 1000 1000 kann nicht als Zweierkomplement einer darstellbaren nichtnegativen Zahl gewonnen werden. Es gibt zwei Möglichkeiten: a. 1000 = −8 . b. Bitmuster 1000 wird zur Darstellung von Zahlen nicht benutzt. 2 k-mal z }| { Die Frage, ob das Bitmuster 1 0 · · · 0 zur Darstellung der Zahl −2k−1 genommen werden oder keine Zahl darstellen soll, ist für allgemeines k zu beantworten. Es wird in allen modernen Rechnern die erste Möglichkeit gewählt. Man hat dann jedoch im negativen Bereich eine darstellbare Zahl mehr als im positiven Bereich, und das muß bei Berechnungen und Maschinenbefehlen berücksichtigt werden. 3.4. DARSTELLUNG RATIONALER ZAHLEN 105 Abschließende Bemerkung: Außer der Vorzeichen-Betrag-Darstellung und der Darstellung im Zweierkomplement gab es früher auch die Einerkomplementdarstellung ganzer Zahlen, bei der der negative Wert einer Zahl durch logisches Invertieren gewonnen wird und die Null zwei Darstellungen besitzt. Wenn ganze Zahlen bei Gleitpunktdarstellungen (siehe Abschnitt 3.4) als Exponenten benutzt werden, wird oft die m-Exzeß-Darstellung verwendet. Dabei ist m = 2k−1 und wird auf den Wert der darzustellenden Zahl addiert. Das (nichtnegative) Ergebnis wird als natürliche Zahl in Dualdarstellung angegeben. Abbildung 3.6 zeigt die Zurordnung z ? −2k−1 }| | { −1 z 0 2k − 1 2k−1 − 1 2k−1 0 {z }|? } | {z } { 2k−1 − 1 Abbildung 3.6: m-Exzeßdarstellung ganzer Zahlen von Bitmustern zu ganzen Zahlen (vergleiche auch Abbildung 3.5). Manchmal ist m auch von 2k−1 verschieden, z. B. m = 2k−1 −1. Der Bereich darzustellender Zahlen ist dann so zu wählen, daß auch weiterhin m + a > 0 für jede Zahl a des Bereichs gilt. 3.4 Darstellung rationaler Zahlen Für Aufgaben der realen Welt werden außer natürlichen und ganzen Zahlen auch reelle und komplexe Zahlen benötigt. Komplexe Zahlen werden als Paare reeller Zahlen dargestellt und sind keine elementaren Daten. Reelle Zahlen können rationale Zahlen (z. B. 31 ; 0, 78 ; 0, 55 · · ·), irrational algebraische √ 1 Zahlen (z. B. 2 ; 3− 5 ) oder irrational transzendente Zahlen (z. B. e ; π ; 3eπ ) sein. Sie werden jedoch alle beim bisherigen Rechnen (ohne Computer) durch endliche Dezimalbrüche approximiert. Dabei sind die Approximationsgenauigkeiten meistens höher als die Meßgenauigkeiten der realen Welt. Auch in Rechnern werden rationale Zahlen zur Näherung von reellen Zahlen eingesetzt, wobei man möglichst gute Annäherungen haben will. 106 KAPITEL 3. DARSTELLUNG VON DATEN DURCH BITMUSTER Es gibt verschiedene Arten der Darstellung rationaler Zahlen in Rechnern. Besonders wichtig sind Gleitpunktdarstellungen (Gleitkomma, Fließkomma, floating point), und nur diese sollen im folgenden betrachtet werden. Sie sollen zunächst anhand eines einfachen Beispiels erläutert werden. Beispiel 3.7 Es werden Bitmuster der Länge k = 12 betrachtet. Sie werden, wie in Abbildung 3.7 angegeben, in die Teilbitmuster V, CH und MAN unterteilt. Diesen TeilV CH MAN Abbildung 3.7: Gleitpunktdarstellung in 12 Bits bitmustern werden Zahlen bzw. Vorzeichen zugeordnet. Die zugeordneten Werte sollen entsprechend mit v, ch und man bezeichnet werden. Im Teilbitmuster MAN wird die Mantisse angegeben. Sie stellt in 8 Bits einen Dualbruch dar. Die Bits sind die Ziffern für die wachsenden Potenzen von 12 . Für die Mantisse MAN = 10011001 ergibt z. B. man = 1 · 12 + 0 · 14 + 0 · 1 8 +1· 1 16 +1· 1 32 +0· 1 64 +0· 1 128 +1· 1 256 = 0, 59765625 . Entsprechend ergibt die Mantisse MAN = 001000000 man = 1 · 1 8 = 0, 125 . Das Bit V ist das Vorzeichen. Es bezieht sich auf die Mantisse. 1 = ˆ −. 0 = ˆ + Das Teilfeld CH ist die Charakteristik. Sie legt den Exponenten zur Basis 2 fest. Man will möglichst gleich viele negative wie positive Exponenten haben und kann z. B. die Zuordnung nach der 4-Exzeß-Darstellung (siehe Abschnit 3.3) treffen: CH CH CH CH CH CH CH CH = = = = = = = = 000 001 010 011 100 101 110 111 : : : : : : : : ch ch ch ch ch ch ch ch = = = = = = = = −4 −3 −2 −1 0 1 2 3 . Die dargestellte rationale Zahl ergibt sich schließlich nach der Regel: 3.4. DARSTELLUNG RATIONALER ZAHLEN Dargestellte Zahl = 107 v · 2ch · man . Beispiele: 1 110 1000 1001 = −22 · ( 21 + = 0 000 1111 1111 = 1 32 − 2, 140625 + 1 ) 256 + 2−4 · ( 21 + 41 + 1 8 + 1 16 + 1 32 + 1 64 + 1 128 + 1 ) 256 = 0, 062255859 Ohne Zusatzfestlegungen ist die eingeführte Darstellung von rationalen Zahlen nicht eindeutig: 011010000000 = 22 · 21 = 2 011101000000 = 23 · 1 4 = 2. Um Eindeutigkeit zu erreichen, soll die erste Stelle der Mantisse stets 1 sein. Solche Mantissen heißen normalisiert. Die erste der obigen Darstellungen von 2 ist normalisiert, die zweite nicht. Für eine normalisierte Mantisse m gilt stets 1 > m ≥ 0,5 . Mit normalisierten Mantissen kann man allerdings die 0 nicht darstellen. Es soll daher zuätzlich festgelegt werden: 0 000 00000000 = 0 . 2 Der Zahlenbereich, der mit Gleitpunktzahlen nach Beispiel 3.7 dargestellt werden kann, ist für reale Rechner viel zu klein. In modernen Rechner wird meistens eine Gleitpunktdarstellung nach dem Standard IEEE 754 gewählt. Die dafür gültige Norm ist ANSI/IEEE 8543 [ANSI1987]. Diese soll im folgenden kurz dargestellt werden. Gleitpunktzahlen nach IEEE 754 gibt es in einfacher Genauigkeit (single precision) und doppelter Genauigkeit (double precision). Es gibt auch Gleitpunktzahlen in erweiterter Genauigkeit (extended precision), auf die aber nicht weiter eingegangen werden soll. Abbildung 3.8 zeigt den Aufbau von Zahlen einfacher und doppelter Genauigkeit. Für das Vorzeichen V gilt 0= b + 1= b −. Von den Werten 0, · · · , 255 (bzw. 0, · · · , 2047) der Charakteristik CH haben 0 und 255 (bzw. 0 und 2047) eine besondere Bedeutung und werden nicht als Exponenten betrachtet. Die übrigen werden in 127-Exzeß-Darstellung (bzw. in 1023-Exzeß-Darstellung) als Exponent ch zur Basis 2 genommen. Damit ergibt sich der Exponentenbereich −126, · · · , 127 (bzw. −1022, · · · , 1023). 3 ANSI (American National Standards Institute). Zu IEEE siehe Fußnote auf Seite 597. 108 KAPITEL 3. DARSTELLUNG VON DATEN DURCH BITMUSTER 1 8 23 V CH MAN a. Einfache Genauigkeit (32 Bits) 1 11 52 V CH MAN b. Doppelte Genauigkeit (64 Bits) Abbildung 3.8: Gleitpunktzahlen nach IEEE 754 Der Mantisse MAN = b1 b2 · · · b23 (bzw. MAN = b1 b2 · · · b52 ) wird der Dualbruch b1 · ( 12 )1 + b2 · ( 12 )2 + · · · + b23 ( 21 )23 (bzw. b1 · ( 12 )1 + b2 · ( 21 )2 + · · · + b52 · ( 21 )52 ) zugeordnet. Es sind für MAN alle Bitmuster zulässig. Normalisierung wird dadurch erreicht, daß ein weiteres, nicht wirklich gespeichertes Bit in der Mantisse ganz vorn angenommen wird. Dieses Bit ist stets 1 und es folgt ihm stets ein (auch nur implizit vorhandenes) Komma. Das ergibt 1 1 man = 1 + b1 · ( )1 + · · · + b23 · ( )23 2 2 1 1 (bzw. man = 1 + b1 · ( )1 + · · · + b52 · ( )52 ) , 2 2 und es gilt stets 1 1 ≤ man ≤ 2 − ( )23 2 1 (bzw. 1 ≤ man ≤ 2 − ( )52 ) . 2 Die betragsmäßig kleinsten darstellbaren (normalisierten) Zahlen sind somit ±1, 0 · 2−126 (bzw. ±1, 0·2−1022 ). Rechenergebnisse, die auf betragsmäßig kleinere Zahlen führen, wären bei früheren Gleitpunktdarstellungen zu 0 gesetzt worden oder hätten eine UnterlaufUnterbrechung (Alarme siehe Unterabschnitt 4.2.2) ergeben. Bei Gleitpunktzahlen nach IEEE 754 ist mit der Zulassung denormalisierter Zahlen (denormalized number) unter Verlust gültiger Stellen ein gleitender Übergang zu 0 möglich. Dazu werden der Wert CH = 0 und beliebige nichtnull Mantissenwerte benutzt. Der Exponent ist stets 2−127 (bzw. 2−1023 ). Die Mantisse MAN ist wieder ein Dualbruch, allerdings ohne ein implizit vorangestelltes Bit 1. Normalisierte Zahlen haben 24 (bzw. 53) gültige (Binär-)Stellen. Bei denormalisierten Zahlen bestimmt die am weitesten links stehende 1 der Mantisse die Anzahl gültiger Stellen. 3.5. HEXADEZIMALDARSTELLUNG VON BITMUSTERN 109 Beispiel 3.8 a = 0 00000001 0000 · · · 0 = 2−126 · 1, 0 b = 0 00000000 1111 · · · 1 = 2−127 · (1 − 2−23 ) c = 1 00000000 0110 · · · 0 = −2−127 · ( 14 + 81 ) = −2−127 · 0, 375 d = 0 00000000 000 · · · 01 = 2−127 · 2−23 = 2−150 a ist die kleinste positive normalisierte Zahl. b ist die größte und d die kleinste positive denormalisierte Zahl. a hat 24, b hat 23, c hat 22 und d hat 1 gültige Stelle. 2 Die Zahl Null wird durch die beiden Bitmuster V=0 und V = 1 CH = 0 CH = 0 MAN = 0 MAN = 0 dargestellt. Bei Überlauf über die betragsmäßig größten Zahlen hinweg, sieht IEEE 754 keine denormalisierten Zahlen, sondern die Werte +∞ und −∞ vor. Bei k = 32 ist die Darstellung +∞ = 0 11111111 00 · · · 0 −∞ = 1 11111111 00 · · · 0 Bei k = 64 ist die Darstellung entsprechend. Mit diesen Werten und normalen Zahlen kann nach den üblichen Regeln gerechnet werden. Einige Kombinationen wie z.B. +∞ − ∞, +∞ · 0 oder 00 ergeben unbestimmte Ergebnisse. Diese werden mit dem Format NaN (Not a Number) dargestellt. Tabelle 3.3 zeigt die verschiedenen Formate in einer Zusammenstellung. Für eine Reihe weiterer wichtiger Fragen (Rundung, Formatumwandlung, Fehlersituationen u. a.), die hier nicht behandelt werden können, siehe die Norm ANSI/IEEE 854 [ANSI1987]. 3.5 Hexadezimaldarstellung von Bitmustern Lange Bitmuster sind – für Menschen, nicht für Rechner! – unbequem. Daher wird eine abkürzende Schreibweise durch Hexadezimalzeichen (Sedezimalzeichen) eingeführt: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F Manchmal werden auch die Kleinbuchstaben a, b, c, d, e, f zugelassen. Die Hexadezimalzeichen sind Abkürzungen für Bitmuster der Länge 4, und es wird die folgende Zuordnung getroffen: 110 KAPITEL 3. DARSTELLUNG VON DATEN DURCH BITMUSTER normalisiert ± 0 < CH < Max beliebiges Bitmuster denormalisiert ± 00 · · · 0 nichtnull Bitmuster Null ± 00 · · · 0 00 · · · 0 unendlich ± 11 · · · 1 00 · · · 0 keine Zahl ± 11 · · · 1 nichtnull Bitmuster Tabelle 3.3: Formate der Gleitpunktdarstellung IEEE 754 0000 = 0 0001 = 1 .. . 0111 = 7 1000 = 8 1001 = 9 1010 = 1011 = 1100 1101 1110 1111 A B = C = D = E = F . Dabei soll vereinbart werden, daß hexadezimal geschriebene Bitmuster immer ein Vielfaches von 4 als Länge haben. Beispiele: 1001 0010 1000 = 928 1111 1111 = FF Auch mit Hexadezimalzeichen können natürliche Zahlen dargestellt werden. Beispiel 3.9 AF = 1010 1111 = 1 · 27 + 1 · 25 + 1 · 23 + 1 · 22 + 1 · 21 + 1 · 20 = 175 2 Die Hexadezimalzeichen können mit der Festlegung 3.5. HEXADEZIMALDARSTELLUNG VON BITMUSTERN 0 = ˆ .. . 9 10 11 12 13 14 15 = ˆ = ˆ = ˆ = ˆ = ˆ = ˆ = ˆ 111 0 9 A B C D E F auch als Ziffern zur Basis 16 (Hexadezimalziffern) aufgefaßt werden. Hexadezimalzeichenreihen sind dann natürliche Zahlen in 16-adischer Darstellung. Beispiel 3.10 AF = A · 161 + F · 160 = 160 + 15 = 175 2 Es ist kein Zufall, daß in den Beispielen 3.9 und 3.10 die gleiche natürliche Zahl dargestellt wird, denn 1. Die Hexadezimalzeichen stellen in beiden Darstellungen die gleiche Zahl dar, z. B. A = 10d A = 1010 = 1 · 23 + 1 · 21 = 8 + 2 = 10d Hexadezimalziffern Dualziffern. 2. Auch für beliebige Zeichenreihen von Hexadezimalziffern ergeben beide Darstellungen die gleiche Zahl (a) Mit Hexadezimalziffern h0 h1 h2 · · · hp−1 = h0 · 16p−1 + h1 · 16p−2 + · · · + hp−1 · 160 (b) Mit Dualziffern (k = 4p) b00 · 2k−1 + b01 · 2k−2 + b02 · 2k−3 + b03 · 2k−4+ b10 · 2k−5 + b11 · 2k−6 + b12 · 2k−7 + b13 · 2k−8+ .. .. . . b(p−1)0 · 23 + b(p−1)1 · 22 + b(p−1)2 · 21 + b(p−1)3 · 20 = (b00 · 23 + b01 · 22 + b02 · 21 + b03 · 20) · 24p−4 + (b10 · 23 + b11 · 22 + b12 · 21 + b13 · 20 ) · 24p−8+ .. .. . . (b(p−1)0 · 23 + b(p−1)1 · 22 + b(p−1)2 · 21 + b(p−1)3 · 20 ) · 24p−4p = h0 · (24 )p−1 + h1 · (24 )p−2 + · · · + hp−1 (24 )0 . 112 KAPITEL 3. DARSTELLUNG VON DATEN DURCH BITMUSTER Früher waren auch Oktalzeichen für Bitmuster der Länge 3 üblich: 0 1 2 3 = = = = 000 001 010 011 4 5 6 7 = = = = 100 101 110 111 Sie werden kaum noch benutzt. 3.6 Darstellung von Zeichenreihen Definition 3.2 Ein Alphabet (Zeichenvorrat) ist eine endliche, nicht leere Menge von Zeichen. Eine Zeichenreihe (Zeichenkette, string) ist eine endliche Folge von Zeichen aus einem Alphabet. Beispiel 3.11 Alphabet1 Alphabet2 Alphabet3 Alphabet4 7038 XAAAAEN a A<B = = = = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9} Dezimalziffern {A, B, C, · · · , X, Y, Z} Großbuchstaben {6=, <, >, |, \, @, $, %, !} {0, · · · , 9, A, · · · , Z, a, · · · , z} Zeichenreihe über Alphabet1 (auch über Alphabet4) Zeichenreihe über Alphabet2 (auch über Alphabet4) Zeichenreihe über Alphabet4 Zeichenreihe über keinem der Alphabete, wohl aber über Alphabet5 := Alphabet2 ∪ Alphabet3 2 Unter der Länge einer Zeichenreihe wird die Anzahl Zeichen der Zeichenreihe verstanden. Vereinbarung: Über jedem Alphabet gibt es auch die leere Zeichenreihe λ. Sie hat die Länge 0. Beispiel 3.12 Das Alphabet sei A := Alphabet1 = {0, 1, · · · , 9}. Dann sind X = 000137; Beispiele für Zeichenreihen über A. X = 5; X=λ 2 3.6. DARSTELLUNG VON ZEICHENREIHEN 113 Operationen mit Zeichenreihen: 1. Zusammensetzen von Zeichenreihen (Konkatenation). Binäre Operation, bei der die beiden Zeichenreihen hintereinander geschrieben werden. Als Operationszeichen wird ◦ („Kuller“) verwendet. Beispiel: A = {0, 1, 2, · · · 9}. X = 310 X = 111 Y = 0777 Y =λ X ◦ Y = 3100777 Y ◦ X = X ◦ Y = 111 A∗ bildet mit der Verknüpfung ◦ ein (i. a. nicht kommutatives) Monoid, das freie Monoid über A. 2. Zerlegen einer Zeichenreihe. abcHHdDxXr kann beispielsweise zerlegt werden in a bc HHdD xXr Teilzeichenreihe (substring) einer Zeichenreihe: Zeichenreihe, die aus der gegebenen Zeichenreihe durch Zerlegung gewonnen werden kann. 3. Entfernen einer Teilzeichenreihe aus einer Zeichenreihe. 4. Einfügen einer Zeichenreihe in eine Zeichenreihe. Wörter und formale Sprachen: Ist A ein Alphabet, so wird mit A∗ die Menge der Zeichenreihen über A bezeichnet. A∗ heißt auch Menge der Wörter über A. Man sagt, B ist eine formale Sprache über A, wenn B eine Teilmenge von A∗ ist. Beispiel 3.13 A = {0, 1, 2, · · · 9}. Beispiele für formale Sprachen über A B = {0, 1117, 33333} C = {X ∈ A∗ | X beginnt mit 1} . 2 Diese Begriffe sind in der Informatik an anderer Stelle wichtig (siehe Seiten 174 und 211), jedoch weniger für die Darstellung von Zeichenreihen durch Bitmuster. 114 KAPITEL 3. DARSTELLUNG VON DATEN DURCH BITMUSTER Lexikographische Ordnung: Ist auf dem Alphabet A eine lineare Ordnung gegeben, so wird daraus eine lineare Ordnung auf der Menge der Wörter über A, die lexikographische Ordnung. Dazu legt man fest: 1. Zwei Wörter sind gleich, wenn sie gleich lang sind und an allen Stellen übereinstimmen. 2. Von zwei Wörtern, die nicht gleich sind, ist dasjenige kleiner, das an der ersten Ungleichheitsstelle den kleineren Wert aufweist. Gibt es keine Ungleichheitsstelle, so ist das kürzere Wort das kleinere. Diese Festlegung stimmt mit der „alphabetischen Ordnung“ im umgangsprachlichen Sinne überein. Codes: In Rechnern heißen die Alphabete Codes. Sie bestehen im allgemeinen aus Großund Kleinbuchstaben, Dezimalziffern sowie Satz- und Sonderzeichen. Besonders wichtig sind die in den Tabellen 3.4 und 3.5 gegebenen Codes ASCII (American Standard Code for Information Interchange) und EBCDIC (Extended Binary Coded Decimal Interchange Code). Die Zeichen beider Codes werden in 8 Bits dargestellt, obwohl ASCII eigentlich ein 7-Bit-Code ist. Beispiel 3.14 X1a = 11100111 11110001 10000001 (EBCDIC) (a-b) = 00101000 01100001 00101101 01100010 00101001 (ASCII) 2 Anmerkung: Manche Zeichenreihen stellen Zahlen dar. Bitmuster für Zeichenreihen, die Zahlen darstellen, sind i. a. verschieden von den Bitmustern, die die Zahlen direkt darstellen. Beispiel 3.15 Als natürliche Zahlen: +073 = 73 Als Zeichenreihen: +073 = 6 73 Darstellung als Dualzahl in 16 Bits: +073 = 00000000 01001001 73 = 00000000 01001001 Als Zeichenreihen in EBCDIC-Darstellung: +073 = 73 = 01001110 11110000 11110111 11110011 11110111 11110011 2 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111 sp 0 @ P ‘ p ! 1 A Q a q Erläuterungen: " 2 B R b r a. b. c. # 3 C S c s $ 4 D T d t % 5 E U e u & 6 F V f v ’ 7 G W g w ( 8 H X h x ) 9 I Y i y * : J Z j z + ; K [ k { , < L \ l | – = M ] m } . > N b n e / ? O _ o 3.6. DARSTELLUNG VON ZEICHENREIHEN 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111 sp bedeutet Leerzeichen In den Zeilen stehen die linken, in den Spalten die rechten 4 Bits des Bitmusters Es gibt auch eine deutsche Variante des Codes. Dabei gilt: Deutsch: Ä Ö Ü ä ö ü ß § International: [ \ ] { | } ˜ @ Tabelle 3.4: ASCII (American Standard Code for Information Interchange) 115 116 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111 sp & – { } \ 0 / a j e A J 1 Erläuterungen: a. b. [ ] b k s c l t d m u e n v f o w g p x h q y i r z B K S 2 C L T 3 D M U 4 E N V 5 F O W 6 G P X 7 H Q Y 8 I R Z 9 ! b : . $ , # < * % @ sp bedeutet Leerzeichen In den Zeilen stehen die linken, in den Spalten die rechten 4 Bits des Bitmusters Tabelle 3.5: EBCDIC (Extended Binary Coded Decimal Interchange Code) ( ) _ ’ + ; > = | ? ” KAPITEL 3. DARSTELLUNG VON DATEN DURCH BITMUSTER 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111 3.7. EIGENTLICHE BITMUSTER UND WAHRHEITSWERTE 3.7 117 Eigentliche Bitmuster und Wahrheitswerte Bitmuster im eigentlichen Sinne treten in Anwendungsproblemen selten direkt als Daten auf. Sie werden zur Konstruktion komplexer Daten gebraucht. Für rechnerinterne Verwaltungsaufgaben werden sie häufig benutzt. Tabelle 3.6 zeigt die wichtigsten Operationen, die mit Einzelbitwerten ausgeführt werden können. NICHT, NOT Negation UND, AND Konjunktion ¬ ∧ ∨ ODER, OR Disjunktion 6≡ Ausschließliches ODER exclusive OR Operanden 0 1 0 0 1 0 0 1 1 1 0 0 1 0 0 1 1 1 0 0 0 1 1 0 1 1 Ergebnis 1 0 0 0 0 1 0 1 1 1 0 1 1 0 Tabelle 3.6: Logische Operationen Operationen auf Bitmustern mit mehr als 1 Stelle werden stellenweise ausgeführt, z. B. ¬(11101010 ∧ 11000111) = ¬11000010 = 00111101 . Negation wird oft auch durch Überstreichen gekennzeichnet: ¬(X ∨ Y ) = X ∨ Y . Die Reihenfolge der Operationen wird durch Klammerung bestimmt. Regeln zur Vereinfachung sind: 1. ¬ hat Vorrang vor ∨ und ∧ 2. ∨ und ∧ sind gleichberechtigt. 118 KAPITEL 3. DARSTELLUNG VON DATEN DURCH BITMUSTER Wahrheitswerte sind Daten, die nur die Werte wahr und falsch (bzw. TRUE und FALSE) annehmen. Sie werden für die Ablaufsteuerung in höheren Programmiersprachen benutzt. Wahrheitswerte können im Prinzip durch ein Bit dargestellt werden: wahr = 1 falsch = 0 . In der Praxis wird häufig jedoch ein ganzes Byte benutzt. Z. B. falsch = 00000000 wahr = X mit X 6= 00000000 . Für Wahrheitswerte gelten die logischen Operationen wie für Bitwerte. In der Tat kommt die Bezeichnung „logische Operationen“ von der Aussagenlogik (siehe auch Unterabschnitt 2.5.4). 3.8 Bitmuster in C In der Programmiersprache C gibt es, anders als in vielen anderen Programmiersprachen, die Möglichkeit, Bitmuster unmittelbar zu bearbeiten. Diese Bearbeitung hängt jedoch stark von der jeweiligen Rechnerstruktur ab und wird deshalb in diesem Kapitel und nicht in Kapitel 2 „Programmieren I“ behandelt. Für die Bitbearbeitung sind alle ganzzahligen Datentypen (siehe Unterabschnitt 2.5.1, Seite 45) und nur diese zugelassen. Es wird empfohlen, sich für die Bitbearbeitung auf die vorzeichenlosen Datentypen unsigned char, unsigned short int, unsigned int, unsigned long int zu beschränken. Empfehlenswert ist auch die Definition eines eigenen Datentyps für jede Länge, z. B. typedef typedef typedef unsigned char unsigned int unsigned long int KURZBIT BIT LANGBIT Für die Bitbearbeitung gibt es in C die im folgenden beschriebenen Operationen. Verschiebung (shift): x < < <wert> bedeutet, daß die Bits der Variablen x um <wert> Stellen nach links verschoben werden. Von rechts werden Nullbits nachgezogen. Entsprechend bewirkt > > eine Verschiebung nach rechts. Bei Beschränkung auf vorzeichenlose Datentypen werden von links Nullbits nachgezogen. Für andere Datentypen ist unbestimmt, ob Nullbits oder Einsbits nachgezogen werden. <wert> ist ein Ausdruck, der einen ganzzahligen Wert liefert. Ist dieser negativ oder größer als die Anzahl Bits in x, so ist das Ergebnis unbestimmt. Negation: ∼x bedeutet, daß jedes Bit von x invertiert wird. Auf keinen Fall sollte der Negationsoperator ∼ zur Vorzeicheninvertierung von Zahlen benutzt werden. 3.8. BITMUSTER IN C 119 Logische Verknüpfungen: x | y x & y x ^ y Der Operator | verknüpft die Bits der Operanden x und y stellenweise (d. h. bitweise) durch ODER, der Operator & verknüpft stellenweise durch UND und der Operator ^ verknüpft stellenweise durch ausschließliches ODER (d. h. die Verknüpfgung zweier Bits ergibt genau dann 1, wenn die Bits verschieden sind). Trotz der Bezeichnung gilt: Bitweise logische Verknüpfungen ergeben keinen Wahrheitswert, sondern ein Bitmuster. Für Bitmuster sind auch die Vergleichsoperationen == und != zugelassen, die natürlich einen Wahrheitswert als Resulat ergeben. Beispiel 3.16 Es soll der Unterschied zwischen logischen Operationen für Wahrheitswerte und logischen Verknüpfungen für Bitmuster an einem kleinen Programm verdeutlicht werden. #include <stdio.h> typedef unsigned int main() { BIT x,y; BIT; x = 0x00000001; y = 0x00000010; printf("x = %x y = %x x|y = %x x||y = %x\n", x, y, x|y, x||y ); } x = 1 y = 10 x|y = 11 x||y = 1 2 Für weitere Einzelheiten zur Bitbearbeitung in C sei auf Lowes/Paulik [LoweP1995] verwiesen. Literatur Die Ausführungen dieses Kapitels sind bis auf Abschnitt 3.8 dem Skript Stiege [Stie1995b] entnommen. 120 KAPITEL 3. DARSTELLUNG VON DATEN DURCH BITMUSTER Kapitel 4 Rechensysteme 4.1 Grobschema der konventionellen Maschine Die konventionelle Maschine ist das, was ein Systemprogrammierer - jemand, der hardwarenahe Software entwickelt, z. B. ein Betriebssystem - als „Hardware“ sieht. Es ist jedoch eine virtuelle Maschine, die über der (realen) Hardwaremaschine läuft (vergl. Abschnitt 2.1). Die Bezeichnung „konventionelle Maschine“ ist nicht allgemein üblich, jedoch zweckmäßig und zutreffend. Sie wurde von Tanenbaum [Tane1990] eingeführt. In Abbildung 4.1 sind die wichtigsten Komponenten einer konventionellen Maschine zu sehen. Der Rechner im engeren Sinne besteht aus Prozessor, Hauptspeicher und Ein/Ausgabeschnittstellen. Diese sind durch ein Übertragungsmedium miteinander verbunden. Die Ein-/Ausgabeschnittstellen verbinden den Rechner mit verschiedenen Geräten (Bildschirm, Tastatur, Drucker usw.). Man spricht von den peripheren Geräten. In den folgenden Unterabschnitten wird auf die Komponenten der konventionellen Maschine überblicksartig eingegangen. Anmerkung: Der Rechner im engeren Sinne wird auch Zentraleinheit (central processing unit) genannt. Heutzutage ist diese Bezeichnung aber eher für den Prozessor gebräuchlich, und so wird sie auch in diesem Buch benutzt. 4.1.1 Prozessor Der Prozessor wird auch Zentralprozessor, CPU, central processing unit, processor, Rechenprozessor oder Rechnerkern genannt. Für ihn ist charakteristisch: • Er führt Maschinenbefehle aus. • Er führt Unterbrechungen aus. • Er besteht aus Registern. 121 122 KAPITEL 4. RECHENSYSTEME Hauptspeicher Prozessor 6 6 ? ? Übertragungsmedium 6 6 6 ? ? ? EAS EAS EAS 6 6 6 ? ? ? | {z zu den peripheren Geräten } EAS: Ein-/Ausgabeschnittstelle Abbildung 4.1: Grobschema der konventionellen Maschine Es gibt Rechner mit mehreren Rechenprozessoren (Multiprozessorsystem, multiprocessor system). Im folgenden werden, wenn nicht explizit anders gesagt, nur Einprozessorsysteme (Monoprozessorsystem) betrachtet. Auf die Arbeitsweise von Rechenprozessoren wird genauer in Abschnitt 4.2 eingegangen. 4.1.2 Hauptspeicher Andere Bezeichnungen für den Hauptspeicher sind Arbeitsspeicher oder main memory. Die Bezeichnung Kernspeicher (engl. core) bezieht sich auf die früher übliche Realisierung als Magnetkernspeicher und ist veraltet. Der Hauptspeicher besteht aus Speicherzellen (Wörtern) einer festen Anzahl Bits, d.h. fester Länge k. k heißt die Wortlänge des Speichers. Abbildung 4.2 zeigt schematisch den Aufbau des Hauptspeichers. Die Speicherzellen werden von Null aufwärts adressiert, d.h. numeriert. Der Prozessor und die Ein-/Ausgabeschnittstellen schreiben in den Hauptspeicher und lesen aus dem Hauptspeicher. Typische Wortlängen des Hauptspeichers sind: k = 8, k = 16, k = 32, k = 64 4.1. GROBSCHEMA DER KONVENTIONELLEN MASCHINE .. . .. . 0 1 ··· 0 ··· 1 .. . .. . .. . ··· k−2 ··· k−1 n−2 2 n−1 {z Adressen der Wörter | 123 Nummern der Bits eines Wortes } Abbildung 4.2: Schema des Hauptspeichers Im Fall k = 8 spricht man von Byteadressierung. Es kommt durchaus vor, daß die Wortlänge k nicht mit der Speicherzugriffsbreite übereinstimmt. Ist zum Beispiel k = 8, so werden beim Zugriff zum Speicher abhängig vom Rechnertyp 8, 16, 32 oder 64 Bits übertragen. Das geschieht aus Effizienzgründen. Davon werden jedoch nur die adressierten Anteile bearbeitet. Wird z. B. bei einem Maschinenbefehl als Operand 1 Byte benötigt, so wird nur diese eine Byte im Prozessor bei der Befehlsausführung benutzt, auch wenn insgesamt 4 Bytes aus dem Speicher übertragen wurden. Zur Angabe von Hauptspeichergrößen und anderen Größen in Rechnern werden die folgenden Zahlenbezeichnungen benutzt: 1 1 1 1 Kilo Mega Giga Tera = = = = 1 1 1 1 K M G T = = = = 1K·1K 1K·1M 1K·1G 1024 = 210 = 1024 K = 220 = 1024 M = 230 = 1024 G = 240 . Mit diesen Größen ist z. B.: 216 232 = 26 · 210 = 22 · 230 = = 4G = 64 K 4096 M Hauptspeichergrößen werden in Bytes angegeben, auch wenn die Wortlänge anders ist. Zur Zeit (2012) sind folgende Hauptspeichergrößen üblich: 124 KAPITEL 4. RECHENSYSTEME PCs: Großrechner (mainframe): Größtrechner (z. B. Vektorrechner): 1 GB mehere GB 1 TB . . . 8 GB ... TB . . . 1 PB Die Entwicklung von Hauptspeichergrößen ist (immer noch) im Fluß, und es ist damit zu rechnen, daß die Hauptspeicher der verschiedenen Rechnertypen noch größer werden. Moore 1 bemerkte 1965, daß sich in integrierten Bausteinen die Zahl der Transistoren pro Flächeneinheit jedes Jahr verdoppelt. Diese als „Mooresches Gesetz“ bezeichnete Beobachtung ist erstaunlicherweise mit leichter Verlangsamung immer noch gültig, und man nimmt an, daß diese Entwicklung noch einige Jahre anhalten wird. Die exponentielle Zunahme der Transistor-Flächendichte führte zu einem rasanten Wachstum der Speichergrössen bei fallenden Preisen, aber auch zu immer leistungsfähigeren und schnelleren Prozessoren. Unter der Zykluszeit eines Hauptspeichers versteht man die Gesamtzeit für einen Speicherzugriff. Sie hängt vom Speichertyp ab und liegt im Bereich kleiner 10 ns bis 100 ns. Beim Hauptspeicher ist zwischen Hauptspeichergröße und Größe des Adreßraumes zu unterscheiden. Die Größe des Adreßraumes wird durch die Anzahl von Bits festgelegt, die zur Angabe von Adressen benutzt werden. Werden z. B. 24 Bits für Adressen benutzt, so lassen sich damit 224 Speicherwörter mit den Adressen 0, · · · , 224 −1 ansprechen. Sollen alle Speicherwörter direkt adressiert werden können, so muß die Hauptspeichergröße kleiner oder gleich der Adreßraumgröße sein. Mit besonderen Adressierungstechniken ist es bei kleinem Adreßraum jedoch möglich, auch größere Hauptspeicher zu benutzen. Für weitere Einzelheiten siehe den ergänzenden Abschnitt über virtuelle Adressierung, Seite 145. Anmerkung 4.1 („big endean“ und „little endean“) Auch in Rechnern mit Byteadressierung, also Rechnern, deren Speicherzellen Bytes sind, benutzt man den Begriff Wort. Man bezeichnet damit größere Speichereinheiten. Es gibt Rechner mit Wörtern zu 2 Bytes, zu 4 Bytes oder zu 8 Bytes. Wörter werden benutzt, um darin Bitmuster, die Zahlen darstellen, als Einheit zusammenzufassen. Dabei sind in der Praxis zwei unterschiedliche Formen der wortinternen Adressierung bei der Darstellung natürlicher (und auch ganzer) Zahlen üblich geworden. Sie sind unter den Namen „big endean“ und little endean“ 2 bekannt und sollen am Beispiel der Abbildung 4.3 erläutert werden. Dazu stellt man sich die Bytes eines Rechners vom Typ big endean als von links nach rechts adressiert vor (Teil A der Abbildung) und die eines Rechners vom Typ little endean als von rechts nach links adressiert (Teil B der Abbildung). In beiden Rechnern ist beginnend bei Adresse a die Zeicheneihe UNI–OL gefolgt von zwei Leerzeichen gespeichert. Im Moore, Gordon E., ∗1929, San Francisco, California. Amerikanischer Chemiker und Physiker. Mitbegründer der Firma Intel (1968), von 1975 - 1997 Präsident von Intel. 2 Die Namen stammen aus Jonathan Swifts Buch Gullivers Reisen, in dem wegen der Frage, ob Eier am großen oder kleinen Ende aufzuschlagen sind, Krieg geführt wird (siehe den Artikel von Cohen [Cohe1981]). 1 4.1. GROBSCHEMA DER KONVENTIONELLEN MASCHINE a A U a+1 a+2 a+3 N I – a + 11 a + 10 a + 9 a + 8 B 11 A2 33 C4 a + 11 a + 10 a + 9 a + 8 C C4 33 A2 11 a+4 a+5 a+6 a+7 O L t t a+7 a+6 a+5 a+4 t t L O a+7 a+6 a+5 a+4 t t L O 125 a + 8 a + 9 a + 10 a + 11 11 A2 33 a+3 a+2 a+1 – I N a+3 a+2 a+1 – I N C4 a U a U Abbildung 4.3: „big endean“ und „little endean“ darauffolgenden Wort, das bei Adresse a + 8 beginnt, ist in beiden Rechnern die natürliche Zahl 295 842 756 (in Hexadezimaldarstellung 11A233C4) gespeichert. Zur Darstellung natürlicher Zahlen vergleiche Abschnitt 3.2, Seite 96. Man sieht, daß im Fall A die Bits mit den niedrigen Werten am hinteren Ende des Wortes (Byte 3 des Wortes) gespeichert sind. Daher die Bezeichnung big endean. Im Fall B sind sie am vorderen Ende des Wortes (Byte 0 des Wortes) gespeichert, daher die Bezeichnung little endean. Intel-Prozessoren folgen der little-endean-Konvention, Motorola-Prozessoren, Großrechner der Firmen IBM und Siemens sowie viele andere Rechnertypen benutzen die big-endean-Konvention. Jede der beiden Konventionen ist in sich konsistent und führt innerhalb des entsprechenden Rechensystems nicht zu Schwierigkeiten. Schwierigkeiten treten auf, wenn Daten von einem System in das andere übertragen werden, z. B. über ein Netz. Werden die drei Wörter von Abbildung 4.3 in aufsteigender Adreßreihenfolge der Bytes vom System big endean zum System little endean übetragen und dort in aufsteigender Reihenfolge abgelegt, so ergibt sich die in Teil C der Abbildung gezeigte Speicherbelegung. In ihr ist die Zeichenreihe in den ersten beiden Wörtern korrekt, die Binärzahl im dritten Wort jedoch nicht. Das dort gespeicherte Bitmuster entspricht nach little-endean-Konventionen der Zahl 1 + 1 · 16 + 2 · 162 + 10 · 163 + 3 · 164 + 3 · 165 + 4 · 166 + 12 · 167 = 3 291 718 161 Um das Problem zu lösen, könnte man daran denken, bei der Übertragung die Reihenfolge der Bytes innerhalb eines Wortes umzukehren. Das würde jedoch zu Fehlern bei der Übertragung von Zeichenreihen führen. Eine einfache Lösung gibt es nicht. Es ist oft (aber nicht immer) möglich und sinnvoll, alle Daten (auch Zahlen) als Zeichenreihen zu übertragen, und dann tritt das Problem nicht auf. 2 126 4.1.3 KAPITEL 4. RECHENSYSTEME Ein-/Ausgabeschnittstellen Ein-/Ausgabeschnittstellen verbinden den Rechner mit der Außenwelt, d.h. den peripheren Geräten, und es ist wichtig, eine große Zahl unterschiedlicher Geräte an einen Rechner anschließen zu können. Ziel (bzw. Quelle) der Datentransporte von (bzw. zu) den peripheren Geräten ist der Hauptspeicher. Um den Prozessor nicht mit Transport- und Steuerfunktionen der Ein-/Ausgabe zu belasten, ist man bestrebt, die Datentransporte am Prozessor vorbeilaufen und möglichst viele Steuerfunktionen durch selbständige, parallel zum Prozessor arbeitende Komponenten ausführen zu lassen. Ein-/Ausgabeschnittstellen waren recht uneinheitlich, meist herstellerabhängig. Zum Teil sind sie es noch. Herstellerunabhängige Standardisierungen kommen über den PC- und Workstationmarkt voran. Es können die folgenden Realisierungen für Ein-/Ausgabeschnittstelle unterschieden werden: A. Ein-/Ausgaberegister: Bei einer Ausgabe zu einem peripheren Gerät werden die Daten Wort für Wort per Programm aus dem Hauptspeicher in ein Ein-/Ausgaberegister des Prozessors transportiert und von dort von dem Peripheriegerät gelesen. Bei einer Eingabe von einem Peripheriegerät geschieht alles in entgegengesetzter Richtung. Es kann mehrere Ein-/Ausgaberegister geben. Diese Art von Ein-/Ausgabe ist nur noch in ganz einfachen Rechnern üblich, z. B. bei Mikrorechnern zur Steuerung von Geräten oder zur Signalaufnahme. B. Ein-/Ausgabebausteine und DMA: Das ist die zur Zeit am weitesten verbreitete Art von Ein-/Ausgabeschnittstellen. Alle modernen PCs und Workstations sind so ausgerüstet. Die Ein-/Ausgabeschnittstellen bestehen aus speziellen Ein-/Ausgabebausteinen und DMA-Bausteinen. Die Ein-/Ausgabebausteine realisieren verschiedene Kommunikationsfunktionen ganz unterschiedlicher Komplexität. Z. B. gibt es einfache Bausteine für serielle Schnittstellen und komplexe zur Steuerung von Festplatten oder Netzanschlüssen. DMA bedeutet direct memory access. Ein DMA-Baustein kann direkt (d.h. ohne Beteiligung des Prozessors) auf den Hauptspeicher zugreifen. DMA-Bausteine und Ein/Ausgabebausteine realisieren im Zusammenspiel den direkten Transport von Daten zwischen Hauptspeicher und Peripheriegeräten. DMA-Bausteine und Ein-/Ausgabebausteine werden oft zusammen mit zusätzlichem Speicher und eigenem (verstecktem) Prozessor zu Schnittstellenkarten zusammengebaut. C. Kanäle: Bei Großrechnern sind Ein-/Ausgabeschnittstellen mit eigener Intelligenz und speziellen Ein-/Ausgabefunktionen schon seit vielen Jahren im Einsatz. Die Schnittstellen sind in diesem Fall aufgeteilt in Kanäle im Rechner und Geräteanpassungen (Steuereinheiten controller) zwischen Rechner und Peripheriegerät. Kanäle realisieren die Transporte von und zum Hauptspeicher und bieten nach außen eine einheitliche Schnittstelle. 4.1. GROBSCHEMA DER KONVENTIONELLEN MASCHINE 127 Sie können mit Kanalprogrammen in begrenztem Umfang programmiert werden. Geräteanpassungen realisieren die Umsetzung von der einheitlichen Kanalschnittstelle in die spezifische Schnittstelle des peripheren Gerätes und sind je nach Gerätetyp mehr oder weniger komplex. D. USB: USB (universal serial bus) ist ein externes Bussystem mit serieller Übertragung, das sich in den vergangenen Jahren in sehr starkem Maße durchgesetzt hat. Es hat normierte Stecker und Schnittstellen und verbindet Rechner aller Art mit peripheren Geräten und auch diese untereinander. 4.1.4 Übertragungsmedium Das Übertragungsmedium verbindet Prozessor, Hauptspeicher und Ein-/Ausgabeschnittstellen. In aller Regel wird es durch einen Bus oder auch durch mehrere Busse realisiert. Abbildung 4.4 zeigt schematisch einen Bus. Ein Bus ist ein zentraler Übertragungsweg, an den mehrere Komponenten, genannt Teilnehmer (Stationen) angeschlossen sind. Diese schreiben auf den Bus und lesen vom Bus. In Abbildung 4.4 ist das durch zwei Leitungsbündel gekennzeichnet. Auf rechnerinternen Bussen, wie der in Abbildung 4.4 werden mehrere Bits parallel übertragen. Für Daten und Adressen (manchmal auch noch für Steuerinformationen) hat man eigene Leitungen. Wer wann wie den Bus benutzen darf, regelt ein Buszugriffsverfahren. Für das Übertragungsmedium werden in manchen Fallen auch Direktverbindungen und andere Lösungen verwendet. 4.1.5 Periphere Geräte In diesem Unterabschnitt soll auf die wichtigsten peripheren Geräte kurz eingegangen werden. A. Speichergeräte: Speichergeräte dienen der Speicherung von Daten in rechnergerechter Form (Bitmuster). Es kann sich um temporäre Speicherung als Zwischenspeicher bei Bearbeitungsvorgängen oder um längerfristige Speicherung zur Aufbewahrung von Daten über einen Bearbeitungslauf hinweg handeln. Es gibt verschiedene Typen von Speichergeräten: • Magnetplatten(geräte): Magnetplatten sind rotierende Scheiben mit magnetischen Aufzeichnungen in konzentrischen Spuren und einem Lese-/Schreibsystem, das in radialer Richtung oder kreisförmiger Richtung (wie bei einem Plattenspieler) mechanisch bewegt wird. 128 Daten Adressen KAPITEL 4. RECHENSYSTEME r r r r r 4 4 ··· 4 r 5 ··· 5 5 Teilnehmer Abbildung 4.4: Bus mit angeschlossenem Teilnehmer • Magnetband(geräte): Rotierend abgespulte Bänder mit magnetischen Aufzeichnungen in Längsrichtung und festem Lese-/Schreibsystem. • Optische Speicher: Rotierende Plattenspeicher3 mit optischer oder magneto-optischer Aufzeichnungstechnik. Oft nur einmal beschreibbar. • Halbleiterspeicher Die neuere technische Entwicklung erlaubt es, auch längerfristige Speicherung mit Halbleitern vorzunehmen. Sehr stark haben sich USB-Speichersticks 4 durchgesetzt. Es ist bemerkenswert, daß man optische Speicherung auch schon mit Tesafilmrollen durchführen konnte. 4 USB-Speicherstick oder USB-Stick ist ein Scheinanglizimus (ähnlich wie „Handy“). Im Englischen wird meistens USB flash drive gesagt. 3 4.2. PROZESSOR UND MASCHINENBEFEHLE 129 B. Geräte zur Kommunikation Mensch/Rechner: Bildschirme, Tastaturen und Mäuse sind heutzutage die wichtigsten Geräte zum Dialog zwischen Mensch und Maschine. Bildschirme dienen zur Ausgabe, Tastaturen und Mäuse zur Eingabe. Obwohl zwei (mit Maus drei) Geräte, werden sie zusammenfassend oft als Dialoggerät (Terminal) bezeichnet. Ausgaben auf Papier sind auch wichtig. Sie erfolgen über Drucker und Plotter. Eingaben von Papier über Scanner sind noch nicht die Regel, nehmen aber an Häufigkeit und Bedeutung zu. Tonausgabe, insbesondere Sprachausgabe ist technisch möglich, wird jedoch nicht stark genutzt. Auch Toneingabe bereitet keine technischen Schwierigkeiten. Es gibt jedoch immer noch keine ausgereifte Erkennungstechnik für eingegebene Sprache. Zunehmend gewinnen auch virtuelle Wirklichkeiten (virtual reality) an Bedeutung (Darstellung von Gebäuden, Orten, Filmszenen usw.). Hierfür werden komplexe Geräte zur Darstellung dreidimensionaler Grafik benutzt und Tonausgaben synchronisiert. C. Geräte zur Kommunikation Rechner/Außenwelt: Gemeint ist die nicht-menschliche Außenwelt. Sensoren, Aktoren, Analog-/Digitalwandler, Steuerungs- und Regelungskomponenten sind Beispiele für diese peripheren Geräte. D. Netzanschlüsse: Zur Peripherie eines Rechners zählen auch die Anschlüsse an verschiedene Netze des Nahbereichs (lokale Netze) oder des öffentlichen Bereichs (Datennetze der Telekom oder anderer Anbieter). Über diese Netze ist der Rechner mit anderen Rechnern oder sonstigen Geräten verbunden. 4.2 4.2.1 Prozessor und Maschinenbefehle Register des Prozessors Der Prozessor führt Maschinenbefehle und Unterbrechungen aus. Er besteht aus Registern. Register sind Speicherstellen für Bitmuster, die bei der Ausführung von Befehlen als Daten oder Steuerinformationen verwendet werden. Man kann die Register in Rechenregister, Indexregister und Steuerregister einteilen. Die Inhalte von Rechenregistern sind Operanden oder Ergebnisse vor Maschinenbefehlen. Zum Beispiel: • Die Inhalte zweier Register werden (als Zahlen aufgefaßt und) addiert. Das Ergebnis steht anschließend in einem der beiden Register. 130 KAPITEL 4. RECHENSYSTEME • Der Inhalt eines Registers wird um 7 Bits nach links verschoben. Von rechts werden Nullen nachgezogen. Die Inhalte von Indexregistern werden als Hauptspeicheradressen interpretiert und zur Berechnung von Adressen von Operanden und Maschinenebefehlen verwendet. In modernen Rechnern hat man i. a. Mehrzweckregister (general purpose register). Diese Register können alle auf die gleiche Art, und zwar sowohl als Rechenregister wie auch als Indexregister benutzt werden. Steuerregister enthalten Daten, die den Ablauf im Rechner steuern. Das wichtigste Steuerregister ist der Befehlszähler (Programmzähler, instruction counter, program counter). Der Befehlszähler enthält die Adresse des nächsten auszuführenden Befehls. Maschinenbefehle sind Bitmuster im Hauptspeicher. Zur Ausführung werden sie in den Prozessor geholt. Die übrigen Steuerregister, unter denen insbesondere das Prozessorzustandsregister (processor status register) zu nennen ist, dienen internen Verwaltungszwecken. Siehe auch Unterabschnitt 4.2.3. Anmerkung 4.2 Sowohl die Register des Prozessors als auch die Bytes des Hauptspeichers sind in der hier dargestellten Sicht passive Speicherelemente, deren Inhalte die Befehlsausführungen steuern und durch die Befehlsausführungen verändert werden. Daß es zusätzlich aktive Elemente – man könnte sie „Befehlsausführer“ nennen – geben muß, wird in dieser Betrachtungsweise nicht sichtbar. Eine arithmetisch-logische Einheit (ALU) wäre z. B. ein solcher Befehlsausführer. Daß und wie man Befehlsausführer bauen kann, wird im Gebiet „Rechnerstrukturen“ behandelt. Beispiel 4.1 Als Beispiel soll der Rechner TUBS85 betrachtet werden. Es handelt sich hierbei nicht um einen realen Rechner, sondern um ein Programmsystem, mit dem ein stark vereinfachter Rechner simuliert wird. TUBS85 wurde an der TU Braunschweig entwickelt und wird für Ausbildungszwecke eingesetzt. TUBS85 hat den in Abbildung 4.5 gezeigten Aufbau. Tastatur und Bildschirm sind die des realen Rechners, an dem mit TUBS gearbeitet wird. Der Hauptspeicher besteht aus Bytes und ist 16 KB groß, enthält also Bytes mit den Adressen von 0 bis 16383. Abbildung 4.6 zeigt den Hauptspeicher von TUBS85. Die Adressen sind hexadezimal dargestellt. Der Prozessor (siehe Abbildung 4.7) besteht aus 16 Mehrzweckregistern, einem Befehlszähler und einem Anzeigenregister. Die Mehrzweckregister sind 32 Bits, das heißt 4 Bytes, lang und werden mit Register 0, Register 1, . . . , Register 15 bezeichnet. Der Befehlszähler ist 16 Bits lang und wird mit BZ bezeichnet. Die Anzeige besteht aus 2 Bits und wird mit AN bezeichnet. Der Rechner TUBS85 und der zugehörige Assembler SPASS sind in dem Skript Stiege/Gerns [StieG1996] beschrieben. Reale Rechner sind komplizierter. Siehe dazu die Literaturhinweise am Ende dieses Kapitels. 2 4.2. PROZESSOR UND MASCHINENBEFEHLE 131 Hauptspeicher Prozessor B U S Bildschirm Tastatur Abbildung 4.5: Der Rechner TUBS85 132 KAPITEL 4. RECHENSYSTEME Byte 0000 Byte 0001 Byte 0002 ... ... .. . Byte 3FFE Byte 3FFF Abbildung 4.6: Der Hauptspeicher von TUBS85 1.Byte 2.Byte 3.Byte 4.Byte .. .. .. .. .. ... .. .. .. .. ... ..... ..... ... ..... ... .... ..... ... .... ..... ... ..... ... ......................................................................................... .... .......................................................................................... .... .......................................................................................... .... .......................................................................................... .... ... ... ... ... ... .... .... .... .... .... Register 0 32 Bits Register 1 32 Bits Register 2 32 Bits .. . .. . .. . Register 14 32 Bits Register 15 32 Bits 1.Byte 2.Byte .. .. .. .... ... ... .. .... .. .. .. .. .. ... .......................................................................................... .... ............................................................................................ .... .. . .. .. ... ... ... ... ... ... . . . BZ 16 Bits AN 2 Bits Abbildung 4.7: Der Prozessor von TUBS85 4.2. PROZESSOR UND MASCHINENBEFEHLE 4.2.2 133 Maschinenbefehle und Unterbrechungen Abbildung 4.8 zeigt den schematischen Aufbau eines typischen Maschinenbefehls. Die Operanden OP-Code 0 1 ··· 7 8 ··· k−1 Abbildung 4.8: Aufbau eines Maschinenbefehls Länge ist i. a. ein Vielfaches von 8. OP-Code (Operationscode, Befehlscode, operation code, instruction code) gibt an, welcher Maschinenbefehl auszuführen ist. Operanden sind Eingangs- und Ergebnisdaten sowie Zusatzparameter für den Befehl. Wie werden nun Maschinenbefehle im Prozessor ausgeführt? Das geschieht in den folgenden, in Tabelle 4.1 angegebenen Phasen. Sprungbefehle verändern den Inhalt des Befehlszählers. 1. Hole den Befehl, dessen Adresse im Befehlszähler steht, aus dem Hauptspeicher in den Prozessor. 2. Entschlüssele den Befehl, d.h. bestimme den Befehl über seinen Operationscode. 3. Falls Operanden aus dem Hauptspeicher zu holen sind, und/oder das Ergebnis dorthin zu speichern ist, ermittele die zugehörigen Adressen. 4. Hole die Operanden aus dem Hauptspeicher, falls welche gebraucht werden. 5. Führe die dem Befehl zugrundeliegende Operation aus. 6. Speichere das Ergebnis in den Hauptspeicher, falls der Befehl das verlangt. 7. Erhöhe den Befehlszähler um die Länge des ausgeführten Befehls, falls der Befehl kein Sprungbefehl mit erfüllter Sprungbedingung war. Tabelle 4.1: Phasen der Befehlsausführung Bedingte Sprungbefehle tun das nur dann, wenn eine zu überprüfende Bedingung erfüllt ist, die Sprungbedingung. Die neue Adresse im Befehlszähler heißt Sprungziel. Wenn ein Programm abläuft, gibt es die normale Befehlszählerfortschaltung oder die Einstellung eines Sprungziels durch einen Sprungbefehl. Es können jedoch Situationen 134 KAPITEL 4. RECHENSYSTEME eintreten, bei denen bestimmte Aktionen ausgeführt werden, die nicht zum gerade ablaufenden Programm gehören. Dieses muß unterbrochen werden. In Tabelle 4.2 sind die verschiedenen Fälle aufgeführt. Im Falle einer Unterbrechung wird die Adresse des nächsten auszuführenden Befehls durch die Art der Unterbrechung bestimmt. Im Befehlszähler wird eine Unterbrechungsadresse eingestellt. Das gerade ablaufende Programm darf nicht weiterlaufen, sondern ein anderes Programm, eine Unterbrechungsroutine, wird vorgezogen. Wie kann es zu Unterbrechungen kommen? Es können drei Arten von Unterbrechungsursachen unterschieden werden. A. Alarme Alarme treten auf, wenn das ablaufende Programm eine besondere Situation verursacht, meistens einen Fehler. Beispiele sind: ◦ Division durch Null ◦ Überlauf bei Addition ◦ unzulässige Operandenadresse ◦ unzulässiger Befehlscode Es gibt auch einen Maschinenbefehl, mit dem ein Programm gezielt und beabsichtigt einen Alarm erzeugen kann: Systemaufruf. Der Systemaufruf wird bei vielen Rechnern SVC (supervisor call) genannt. Er dient zum Anruf des Betriebssystems (siehe Unterabschnitt 4.3.1). B. Externe Eingriffe Externe Eingriffe sind Signale, die von außen an den Prozessor gesandt werden. Beispiele: ◦ Wecksignal einer Uhr ◦ Abschlußmeldung einer Ein-/Ausgabeschnittstelle ◦ Anruf aus dem Netz C. Maschinenfehler Fehlersituationen im Rechner, die von der Hardware erkannt werden, führen i. a. auch zu Unterbrechungen. Beispiele: ◦ Stromausfall ◦ inkorrekte Prüfsumme Anmerkung: Für Alarm sind auch die Bezeichnungen trap oder synchrone Unterbrechung üblich. Externe Eingriffe werden auch als asynchrone Unterbrechungen bezeichnet. Oft wird der Begriff interrupt nur im Sinn von externem Eingriff gebraucht. 4.2. PROZESSOR UND MASCHINENBEFEHLE 135 A. Die Adresse des nächsten auszuführenden Befehls wird vom ablaufenden Programm bestimmt. A.1. Normale Befehlsfortschaltung Der Befehlszähler wird um die Länge des ausgeführten Befehls erhöht. A.2. Sprungbefehl Der ausgeführte Sprungbefehl setzt den Befehlszähler explizit auf einen neuen Wert. B. Die Adresse des nächsten auszuführenden Befehls wird nicht vom ablaufenden Programm, sondern durch ein besonderes Ereignis bestimmt. Es kommt zu einer Unterbrechung (interrupt). Tabelle 4.2: Adresse des nächsten Befehls 4.2.3 Befehlsklassen Die Maschinenbefehle sind von Rechnertyp zu Rechnertyp ganz unterschiedlich. Jedoch gibt es meistens ganz ähnliche Befehlsklassen. A. Transportbefehle: Transportbefehle kopieren Bitmuster von einer Stelle im Rechner zu einer anderen. Die Quelle des Transportvorgangs bleibt unverändert außer bei Überlappung mit dem Ziel. Das Ziel wird verändert. Transportrichtungen sind: Register Register Hauptspeicher ←→ Register ←→ Hauptspeicher ←→ Hauptspeicher. Die Länge der transportierten Bitmuster ist vom Befehl abhängig. B. Logische Befehle: Logische Befehle manipulieren oder testen Bitmuster, ohne sie als Zahlen zu interpretieren. Wichtige Operationen sind Negation, UND, ODER, ausschließliches ODER, Testen mit Maske u. a. Siehe dazu auch Abschnitt 3.7. Die Operanden logischer Befehle stehen in Registern oder im Hauptspeicher. Das Ergebnis ersetzt i. a. einen der Operanden. C. Arithmetische Befehle: Arithmetische Befehle manipulieren oder testen Bitmuster, die sie als Zahlen (natürliche Zahlen, ganze Zahlen, rationale Zahlen) in verschiedenen Darstellungen interpretieren (siehe Kapitel 3). Auch bei diesen Befehlen stehen die Operanden in Registern oder im Hauptspeicher, und das Ergebnis ersetzt i. a. einen der Operanden. Die wichtigsten Operationen sind Addieren, Multiplizieren, Subtrahieren, Dividieren, Betrag bilden, Vorzeichen wechseln, Vergleichen. 136 KAPITEL 4. RECHENSYSTEME D. Verschiebebefehle (shift): Diese Befehle verändern den Inhalt eines Registers, indem sie die Bits nach links oder nach rechts verschieben. Dabei gehen nach links (rechts) hinausgeschobene Bits verloren, und von rechts (links) werden Nullen nachgezogen. Bei manchen Befehlen werden Einsen nachgezogen oder es handelt sich um Kreisverschiebungen, bei denen die auf einer Seite hinausgeschobenen Bits auf der anderen Seite nachrücken. Es gibt auch Verschiebebefehle, die auf Registerpaaren oder auf Hauptspeicherstellen arbeiten. E. Sprungbefehle: Sprungbefehle setzen den Befehlszähler. Bedingte Sprungbefehle testen eine Bedingung und verändern den Befehlszähler nur bei erfüllter Sprungbedingung. Bei unbedingten Sprungbefehlen wird die Sprungadresse in jedem Fall gesetzt. Oft haben Sprungbefehle noch Zusatzeffekte. Z. B. wird bei Unterprogrammsprüngen die Rücksprungadresse gesichert. F. Transformationsbefehle: Es handelt sich um sehr komplexe Befehle, die nur in einigen (älteren) Rechnertypen vorhanden sind. Texte edieren, Zahlen umwandeln, Tabellen durchsuchen sind Beispiele für Transformationsbefehle. G. Visuelle Befehle: Neuere Prozessoren weisen Befehle auf, die grafische Anwendungen und Multimedia-Anwendungen in besonderem Maße unterstützen. H. Verwaltungsbefehle und privilegierte Befehle: Dienen zur internen Verwaltung des Rechners. Für normale Benutzer sind nur wenige dieser Befehle erlaubt, z. B. der in Unterabschnitt 4.2.2 erwähnte Systemaufruf. Die meisten Verwaltungsbefehle sind privilegierte Befehle. D. h. sie laufen nur im privilegierten Zustand des Prozessors ab. Der privilegierte Zustand, man spricht auch von einem geschützten Zustand (protected mode), wird durch ein Bit in einem speziellen Steuerregister, dem Prozessorzustandsregister, gekennzeichnet. Selbstverständlich kann das Prozessorzustandsregister nur mit einem privilegierten Verwaltungsbefehl geändert werden. Nach dem Urladen (boot) ist privilegierter Zustand eingestellt, ebenso nach allen Unterbrechungen. Dadurch ist gesichert, daß nur das Betriebssystem und nicht auch normale Programme privilegierte Befehle ausführen können. Die Maschinenbefehle, die in jedem Prozessorzustand ausgeführt werden können, heißen nichtprivilegierte Befehle. Beispiele für privilegierte Befehle sind: • EA-Befehle (Befehle an Ein-/Ausgabeschnittstellen) • Setzen Seiten-/Kacheladreßregister (für virtuelle Adressierung) • Setzen Prozessorzustandsregister. 4.2. PROZESSOR UND MASCHINENBEFEHLE 137 Nicht bei allen Rechnern wird im Prozessor ein privilegierter und ein nichtprivilegierter Zustand unterschieden. Insbesondere kennen ältere Mikroprozessoren diese Unterscheidung nicht. Anmerkung: Die vorgestellten Befehlsklassen beziehen sich auf CISC-Rechner (Complex Instruction Set Computers). Dazu gehören: • Herkömmliche Mikroprozessoren: Intel, Motorola, NSC u. a. • „Klassische“ Großrechner wie zum Beispiel 370/390er-Architektur (IBM/Siemens). CISC-Rechner sind zu unterscheiden von RISC-Rechnern (Reduced Instruction Set Computers), die einen einfacheren Befehlssatz und andere Vereinfachungen aufweisen und somit effizientere und schnellere Prozessorhardware ermöglichen. Moderne Workstations, aber auch Parallelrechner und möglicherweise künftig zum Teil auch PCs beruhen auf RISC-Prozessoren5 . 4.2.4 Adressierungsarten In diesem Unterabschnitt wird untersucht, wie bei der Befehlsausführung Adressen von Operanden und Befehlen ermittelt werden. Adressen von Registern (des Prozessors): Adressen von Registern des Prozessors werden in Maschinenbefehlen direkt als Bitmuster angegeben, häufig in vier Bits. Damit sind dann 16 Register adressierbar. Adressen von Hauptspeicherzellen (Hauptspeicheradressen): Hauptspeicheradressen können auf verschiede Arten angegeben werden. a. Direkt im Maschinenbefehl. Wird selten gemacht, da Adressen in modernen Rechnern 24 oder 32 Bits lang sind. Bei zwei Adressen wäre ein Befehl dann 6 oder 8 Bytes lang. Das ist zuviel. b. Adressierung mit Basis- und Indexregistern. Es werden ein Bitmuster im Befehl und die Inhalte eines oder mehrerer Register addiert, um die endgültige Adresse zu erhalten. Den Adreßteil im Befehl nennt man Distanz (displacement). Diese Adressierungsart ist in fast allen modernen Prozessoren üblich, allerdings gibt es recht unterschiedliche Realisierungen und Benennungen, z. B. auch Adreßsegmente (in Mikroprozessoren). Bei manchen Rechnertypen gibt es auch negative Distanzen, z.B. zur Modifikation des Befehlszählerinhalts. 5 Das ist zur Zeit (2012) wohl nicht mehr aktuell. 138 KAPITEL 4. RECHENSYSTEME c. Indirekte Adressierung Eine (i. a. nach b. gewonnene) Hauptspeicheradresse verweist auf einen Platz, wo nicht der Operand, sondern die Hauptspeicheradresse des Operanden steht. Indirekte Adressierung ist oft iterierbar, allerdings nur eine begrenzte Zahl von Malen, z. B. 7. d. Direkte Operanden (immediate operand) Diese Operanden werden nicht über eine Adresse angesprochen, sondern stehen als (Teil)Bitmuster im Befehl. Meistens sind sie 1 Byte lang; es können also nur kleine Operanden angegeben werden. Bei diesen ist die Angabe als Direktoperand jedoch nützlich, da der Adressierungsschritt für den Operanden entfällt. Befehle mit direkten Operanden sind in vielen Rechnern vorhanden und werden oft benutzt. Anmerkung: Die Befehlsklassen sind in den meisten Rechnertypen ähnlich. Die Adressierungsarten variieren zwischen den Rechnertypen recht stark. Geschichtliche Anmerkung: Rechner, wie sie in diesem Kapitel beschriebene sind, werden auch Von-Neumann-Rechner (nach John von Neumann 6) genannt, obwohl eher Zuse 7 als der Erfinder des modernen Computers anzusehen ist. Ein Vorläufer ist Babbage 8. 4.3 Grundsoftware Zur Grundsoftware zählt man das Betriebssystem und eine Reihe weiterer Programme wie z. B. die Sprachübersetzer. Nur zusammen mit der Grundsoftware bildet die Hardware eines Rechensystems eine Arbeitsumgebung, in der Anwendungssoftware entwickelt Neumann, John von (eigentlich Johann Baron von Neumann) ∗1903 Budapest, †1957 Washington, D.C. Amerikanischer Mathematiker osterreichisch-ungarischer Herkunft. Wirkte in Berlin, Hamburg und Princeton, New Jersey. Wesentliche Beiträge zur Mengenlehre und mathematischen Logik, Wahrscheinlichkeitstheorie und Spieltheorie, Funktionalanlysis, Quantentheorie. Die Bezeichnung „Von-NeumannRechner“ rührt von seinen Arbeiten zur Struktur programmgesteuerter Rechner [Neum1976] her. 7 Zuse, Konrad, ∗1910 in Berlin, †1995 in Hünfeld bei Fulda. Deutscher Bauingenieur. „Schöpfer der ersten vollautomatischen, programmgesteuerten und frei programmierbaren, mit binärer Gleitpunktrechnung arbeitenden Rechenanlage. Sie war 1941 betriebsfähig“ [Baue1996]. Entwarf 1943 - 1945 eine algorithmische Sprache, den „Plankalkül“. Sein Wirken hat Zuse in dem Buch „Der Computer – mein LebenswerK“ [Zuse1993] dargestellt. Eine lesenswerte Würdigung Zuses ist der Artikel von Bauer [Baue1996]. 6 Babbage, Charles, ∗1792 Teignmouth (Devonshire), †1871 London. Englisher Mathematiker. Professor der Mathematik in Cambridge. Arbeitete viele Jahre an der Entwicklung programmgesteuerter Rechenmaschinen („Analytical Engine“). Seine Ideen ließen sich jedoch mit den technischen Möglichkeiten seiner Zeit nicht erfolgreich realisieren. Nach seiner Mitarbeiterin Ada Countess of Lovelace (1811 1852, Tochter von Lord Byron) wurde die Programmiersprache Ada benannt. 8 4.3. GRUNDSOFTWARE 139 werden kann. Nur mit dieser wiederum kann letztlich das Rechensystem zur Bearbeitung von Aufgaben aller Art eingesetzt werden. 4.3.1 Betriebssystem In Abschnitt 2.1 wurde die Betriebssystem-Maschine als virtuelle Maschine eingeführt, die auf der konventionellen Maschine läuft. Programme einer Betriebssystem-Maschine bestehen aus den nichtprivilegierten Befehlen der konventionellen Maschine (siehe Unterabschnitt 4.2.3) sowie speziellen Anweisungen an das Betriebssystem, den Betriebsbefehlen (Systemaufruf, system call). Die in diesem Sinne definierte Betriebssystem-Maschine wird oft auch Betriebssystemkern (system kernel) genannt. Unter einem Betriebssystem (operating system) versteht man im allgemeinen jedoch mehr als nur den Betriebssystemkern. Zum Beispiel gehören in der Regel die Ausführung von Betriebssystem-Kommandos oder die Funktionen der Dateiverwaltung zum Betriebssystem, aber nicht zum Kern. Eine brauchbare Arbeitsdefinition für ein Betriebssystem liefert Siegert [Sieg1988]. Definition 4.1 ([Sieg1988]) Ein Betriebssystem . . . • steuert den Ablauf der Auftragsverarbeitung für einen Benutzer. • plant die Reihenfolge der Auftragsverarbeitung für verschiedene Benutzer. • lädt die Programme in den Arbeitsspeicher und startet sie. • stellt Systemdienste bereit, insbesondere für den Transport von Daten zwischen Programmen und Geräten. • koordiniert und synchronisiert beim gleichzeitigen Ablauf von mehreren Programmen den Zugriff auf gemeinsame Betriebsmittel. • erfaßt die verbrauchten Rechenleistungen und andere Leistungen. • schützt die Dateien und Programme vor Verlust und unberechtigtem Zugriff. • registriert aufgetretene Hardwarefehler und versucht sie abzufangen. Als wichtige Ergänzung zu Definition 4.1 wäre nachzutragen • stellt Dienste und Funktionen bereit, mit denen der Rechner, auf dem es läuft, in Netze eingebunden wird. 140 KAPITEL 4. RECHENSYSTEME Äußere Charakterisierung von Betriebssystemen: Betriebssysteme lassen sich nach ihrer Wirkung und ihren Schnittstellen nach außen einteilen und beschreiben. Es gibt Standard-Betriebssysteme und Spezialbetriebssysteme. Standardbetriebssysteme (Mehrzweckbetriebssystem, general purpose operating system) sind das, was man normalerweise unter einem Betriebssystem versteht. Charakteristisch für sie ist: • Sie interagieren mit menschlichen Benutzern. • Sie sind nicht Teil eines umfassenden technischen Systems. • Sie sind vielseitig nutzbar, z. B.: ◦ Programmentwicklung (Quelltext Codieren, Übersetzen, Binden, Testen, Installieren). ◦ „klassische“ kommerzielle Datenverarbeitung (Rechnungserstellung, Buchhaltung, Projektüberwachung, Lohn- und Gehaltsabrechnung usw.) ◦ „klassische“ technisch-wissenschaftliche Datenverarbeitung (numerische Berechnungen, Simulation usw). ◦ Datenbankanwendungen ◦ Textverarbeitung ◦ Kommerzielle und technisch-wissenschaftliche graphisch unterstützte Dialoganwendungen. ◦ Buchungssysteme (Banken, Reisebüros usw.). • .. . Im Gegensatz zu Standardbetriebssystemen gilt für Spezialbetriebssysteme (special purpose operating system: • Sie sind für besondere Anforderungen entworfen. • Sie sind i. a. Teil eines umfassenden technischen Systems. • Sie weisen nicht den vollen Funktionsumfang eines allgemeinen Betriebssystems auf, haben andererseits Eigenschaften, die dort fehlen. Beispiele für Spezialbetriebssysteme sind: • Realzeitbetriebssysteme (Echtzeitbetriebssystem, real time operating system). Realzeitbetriebssysteme müssen ablaufende Programme so steuern, daß Zeitbedingungen, die oft sehr eng (hart) sind, erfüllt werden. Es gibt auch Hybridsysteme; das sind Standardbetriebssysteme mit Realzeiteigenschaften. 4.3. GRUNDSOFTWARE 141 • Betriebssysteme für Netzknotenrechner und für (Telefon-)Vermittlungsrechner. • Betriebssoftware in Rechnern, die als intelligente Steuerungen eingesetzt werden (Aufzüge, Autos, Haushaltsgeräte, Flugzeuge, Spracherkennung usw.). Es handelt sich hierbei häufig nicht um echte Betriebssysteme, sondern um einfache Steuersoftware, die den Ablauf der eigentlichen Anwendungssoftware ermöglicht. Menschliche Benutzer kommunizieren mit einem Betriebssystem über Betriebssystemkommandos. Diese Kommandos werden interpretiert. Der Kommandointerpreter (auch Kommandoentschlüßler genannt) ist kein Teil des Betriebssystemkerns. Bei Unix heißt der Kommandointerpreter shell (Schale). Es gibt verschiedene Ausführungen der Shell (Cshell, Bourne-shell u.a.). Unter einem Job (Sitzung, session) versteht man einen zusammenhängenden, vollständigen, durch Kommandos eines Benutzers gesteuerten Arbeitsablauf in einem Rechensystem. Von Jobs spricht man bei Stapelbetrieb, von Sitzungen bei Dialogbetrieb. Die Kommandosprache eines Betriebssystems wird auch JCL (job control language) genannt. Oft enthält sie neben Anweisungen, die zu Aktivitäten führen (Übersetzen, Dateiliste anzeigen usw.) auch Steueranweisungen (if-then-else, while, case u.a.). Moderne Betriebssysteme bieten oft zusätzliche graphische Schnittstellen für die Benutzer. Bei diesen ist die Kommandostruktur stark zurückgedrängt und kaum noch erkennbar. Zur äußeren Charakterisierung gehört auch die Betriebsart von Betriebssystemen. Einprogrammbetrieb/Mehrprogrammbetrieb. Im Einprogrammbetrieb (single programming, Einbenutzerbetrieb, single user) läuft ein Benutzerprogramm nach dem anderen ab. Im Mehrprogrammbetrieb (multiprogramming, Mehrbenutzerbetrieb, multiuser) können mehrere Benutzerprogramme parallel ablaufen. Mehrprogrammbetrieb setzt nicht voraus, daß die ablaufenden Benutzerprogramme notwendigerweise von verschiedenen Benutzern stammen. In neueren Personalcomputern gibt es (in etwas eingeschränkter Form) Mehrprogrammbetrieb, obwohl nur ein einziger Benutzer an dem Rechner arbeitet. Will man hervorheben, daß ein Betriebssystem zu einem Zeitpunkt nur einen einzigen Benutzer und ein einziges Dialoggerät zuläßt, dieser Benutzer aber mehrere Programme parallel ablaufen lassen kann, so spricht man von Mehrprozeßbetrieb (multitasking). Stapelbetrieb/Dialogbetrieb. Im Stapelbetrieb (batch mode) liegt ein Job als fertige Folge von Kommandos und Daten vor, i. a. in einer Datei, früher auch auf Lochkarten. Ein Benutzer stellt einen Job vollständig zusammen und hat danach auf den Jobablauf keinen Einfluß mehr. Druckausgaben eines Stapeljob werden in einer Datei gesammelt und nach Beendigung des Jobs ausgedruckt (spooling). Dialogbetrieb (interactive mode) ist dadurch gekennzeichnet, daß der Benutzer über ein Dialoggerät den Ablauf unmittelbar steuert. Kommandos, die er eingibt, werden vom Betriebssystem sofort ausgeführt und – eventuell mit Fehlermeldungen – quittiert. Der Benutzer führt ein Wechselgespräch mit dem Betriebssystem. Darüber hinaus können Programme, die über Kommandos gestartet werden, Daten im Dialog anfordern und ausgeben. Der Benutzer führt im allgemeinen auch mit 142 KAPITEL 4. RECHENSYSTEME den ablaufenden Programmen ein Wechselgespräch. Es müssen aber nicht alle Programme auch wirklich einen Dialog durchführen. So ist es z. B. sinnvoll, wenn Sprachübersetzer die zu übersetzenden Quellen aus Dateien und nicht im Dialog einlesen. Bei Betriebssystemen mit graphischen Schnittstellen ist für die Benutzer der Unterschied zwischen Dialog mit dem Betriebssystem und Dialog mit einem Benutzerprogramm kaum noch erkennbar. Zunehmend werden unabhängige Rechner und Geräte zu einem Netz (Rechnernetz, network, computer network) über kleinere (lokales Netz) oder größere (Weitverkehrsnetz) Entfernungen zusammengeschlossen. Man spricht von Netzbetrieb. Allgemeines Teilnehmerbetriebssystem. Ein allgemeines Teilnehmerbetriebssystem (general purpose operating system) ist ein Betriebssystem, in dem mehrere unterschiedliche und unabhängige Jobs gleichzeitig ablaufen und für die Jobs folgende Betriebsarten möglich sind • Dialogbetrieb, • Stapelbetrieb, • Netzbetrieb . Beispiele für allgemeine Teilnehmerbetriebssysteme sind: MVS (Multiple Virtual Storage). MVS ist das Hauptbetriebssystem für Großrechner (mainframe) der IBM und eines der komplexesten Betriebssysteme überhaupt. Literatur: [Stal1992], [John1989], [Paan1986]. BS2000. BS2000 ist das Betriebssystem der Großrechner der Firma SNI (Siemens Nixdorf Informationssysteme) und auch ein sehr komplexes System. Literatur: [Gorl1990]. Unix. Unix ist ein allgemeines, „offenes“ Betriebssystem. Es wurde 1969 von Thompson 9 bei den AT&T Bell Laboratories entwickelt und läuft auf fast allen Workstations, etlichen Groß- und Größtrechnern und in der Version LINUX zunehmend auch auf PCs. Literatur: [Bach1986], [Stal1998], [LeffMKQS1988], [Hier1993], [Andl1990]. [GulbO1995] Keine allgemeine Teilnehmerbetriebssysteme, aber dennoch wichtige allgemeine Betriebssysteme sind die PC-Betriebssysteme Windows95 / WindowsNT. Windows 95 hat nur noch historisches Interesse. Es basierte auf auf dem einfachen Betriebssystem MS/DOS (Microsoft Disk Operating Thompson, Ken, ∗ 1943, New Orleans. Amerikanischer Informatiker. Den entscheidenden Durchbruch von Unix erzielte er zusammen mit Ritchie (siehe Seite 34) nach der Umstellung auf C [RitcT1974]. Zur Geschichte von Unix siehe Tanenbaum [Tane1994]. 9 4.3. GRUNDSOFTWARE 143 System). Spätere Microsoft Betriebssysteme setzten auf WindowsNT auf. Recht bekannt war Windows XP. Die aktuelle Version ist Windows7. Windows Betriebssysteme gehören zu den am stärksten verbreiteten Betriebssysteme für Personalcomputer. Trotz ständiger Erweiterungen und Zusätze bieten sie nur sehr eingeschränkten Mehrprogrammbetrieb und können nicht als Teilnehmerbetriebssysteme angesehen werden. Literatur: [Micr1996], [TiscJ1995]. Innere Struktur von Betriebssystemen: Die wichtigsten Bestandteile des Betriebssystemkerns sind • Prozeß- und Prozessorverwaltung. Startet Benutzerprogrammläufe als Prozesse und beendet sie. Verwaltet wartende Prozesse und ordnet den rechenbereiten unter ihnen freiwerdende Prozessoren zu. • Interprozeßkommunikation. Ermöglicht die Kommunikation zwischen unterschiedlichen Prozessen auf dem gleichen Rechner oder (über Netz) auf verschiedenen Rechnern. • Zeitverwaltung. Verwaltet die Hardware- und Softwareuhren des Systems. Führt Weckaufträge aus. Synchronisiert Uhren auf verschiedenen Rechnern. • Speicherverwaltung. Teilt den Prozessen den benötigten Hauptspeicher zu. Lagert Prozesse oder Prozeßstücke bei Bedarf auf Plattenspeicher aus, bzw. lagert diese wieder in den Hauptspeicher ein. • Ein-/Ausgabe-Kern. Nimmt Aufträge für Datentransporte von anderen Teilen des Betriebssystems und von Benutzerprogrammen entgegen. Verwaltet und steuert die Ein-/Ausgabeschnittstelle der Hardware. Steuert die peripheren Geräte. Diese Programme heißen Treiber (driver). Auch zum Betriebssystem, aber nicht zum Kern gehören • Kommandointerpretierer. Interpretiert die von Benutzern eingegeben Kommandos zum Aufruf von Systemdiensten. • Dateiverwaltung (file management). Erzeugt, löscht und verwaltet Dateien und Dateiverzeichnisse. Führt unter Benutzung der Dienste des Ein-/Ausgabe-Kerns Datentransporte zwischen Programmen und peripheren Geräten durch. Im weiteren Sinne werden auch Transporte über das Netz zur Dateiverwaltung gerechnet. • Stapelbetrieb und Spoolfunktionen. Ermöglichen den Ablauf von Jobs ohne Dialog mit dem Benutzer. Sammeln Druckausgaben in Dateien, drucken Dateien aus. 144 KAPITEL 4. RECHENSYSTEME • Systemladefunktionen. Programme, die den Aufbau des Betriebssystems nach dem Einschalten des Rechners steuern. Das Programm, das den Aufbau beginnt, wird Urlader (bootstrap) genannt. • Operateurteil. Führt die Kommunikation mit der Operateurkonsole durch. • Programmlader (loader). Bringt ablauffähige Programme in den Hauptsspeicher und startet sie. Stellt Programmteile, die während des Ablaufs von Benutzerprogrammen zusätzlich benötigt werden, zur Verfügung (dynamisches Binden, dynamic linking). • Bildschirmoberflächen. Fenstersysteme und andere grafische Systeme. • 4.3.2 .. . Weitere Programme der Grundsoftware Weitere Programme der Grundsoftware lassen sich gliedern in Programme, die zum Programmiersystem gehören, allgemeine Dienstprogramme und allgemeine Anwendungsprogramme. Die beiden letzten Klassen sollen hier nur genannt, aber nicht weiter erläutert werden. • Programmiersystem. Umfaßt die Programme, die Quellprogramme in eine auf der Betriebssystem-Maschine ablauffähige Form bringen oder Quellprogramme interpretieren. ◦ Sprachübersetzer. Compiler für höhere Programiersprachen, Assembler. ◦ Binder (linkage editor, linker, binder). Programm, das Programme und Programmstücke, die übersetzt worden sind, zu einem Programm zusammenfügt (binden). Die ursprünglichen Programme können durchaus in verschieden Programmiersprachen geschrieben worden sein. ◦ Laufzeitbibliotheken. Dateien, die vorübersetzte Programmstücke mit häufig benutzten allgemeinen Funktionen (z. B. für Ein/-Ausgabe) enthalten. Diese werden beim Übersetzen/Binden dem übersetzten Progamm hinzugefügt. ◦ Programmbibliotheken. Dateien fertig ablauffähiger Programme. • Allgemeine Dienstprogramme. ◦ Textedierer. ◦ Grafische Edierer. ◦ Kopier- und Umsetzprogramme. 4.4. VIRTUELLE ADRESSIERUNG 145 ◦ Systemgenerierungsprogramme. ◦ Sicherungsroutinen. .. ◦ . • Allgemeine Anwendungssysteme. ◦ Datenbanksysteme. ◦ Mathematische Programme und Unterprogramme. ◦ Kommerzielle und branchenspezifische Anwendungssysteme. ◦ Simulationssysteme. ◦ Entwurfsunterstützungssysteme. .. . ◦ 4.4 Virtuelle Adressierung Ausgangspunkt ist die Unterscheidung zwischen Hauptspeichergröße und Größe des Adreßraumes (Seite 124). Schon früh in der Rechnerentwicklung wurde klar, daß es von Vorteil ist, Adressen, mit denen auf den Hauptspeicher zugegriffen wird (sog. Speicheradressen), und Adressen, mit denen innerhalb des Prozessors Maschinenbefehle bearbeitet werden (sog. Programmadressen), zu trennen. Tut man das, so spricht man von virtueller Adressierung (virtual addressing) 10. Als Erfinder der virtuellen Adressierung ist F.-R. Güntsch anzusehen11 . Sind Programmadressen und Hauptspeicheradressen identisch, so spricht man von reeller Adressierung (real addressing) (Abbildung 4.9). Die wichtigste Form virtueller Adressierung ist virtuelle Seitenadressierung (virtual page addressing). Dabei sind sowohl der Programmadreßraum als auch der Hauptspeicher in Blöcke gleicher Größe, genannt Seiten (page), eingeteilt. Einem Programmlauf wird vom Betriebssystem zunächst ein bestimmtes Anfangstück des virtuellen Adreßraumes zugeteilt. Für jede der zugeteilten Seiten wird ein Block gleicher Größe auf einem Plattenspeicher, dem Seitenwechselgerät Erste Rechner mit virtueller Adressierung waren Atlas (1962, University of Manchester zusammen mit Ferranti und Plessey) und B5000 (1961, Burroughs Corp.) In den frühen 70er-Jahren des vorigen Jahunderts hatte sich virtuelle Adressierung bei größeren Rechnern allgemein durchgesetzt. Circa 10 Jahre später hatten die PCs nachgezogen. 11 Güntsch, Fritz-Rudolf, ∗ 1925, Berlin. Deutscher Physiker und Computer-Pionier. Führte die virtuelle Adressierung im Rahmen seiner Dissertation „Logischer Entwurf eines digitalen Rechengerätes mit mehreren asynchron laufenden Trommeln und automatischer Schnellspeicherbetrieb“ ein. Leiter des Bereichs Großrechner der AEG/Telefunken, Konstanz, wo die Großrechner TR4 und TR440 entwickelt wurden. Später war Güntsch im Bundesministerium für Forschung und Technologie zuständig für Datenverabeitung und hat maßgeblich am Aufbau der Informatik in Deutschland mitgewirkt. 10 146 KAPITEL 4. RECHENSYSTEME Reelle Adressierung: Programmadresse = Speicheradresse =⇒ Speicheradresse Virtuelle Adressierung: Programmadresse . ......... ......... ... .. Adreßumsetzung Abbildung 4.9: Schema für reelle und virtuelle Adressierung (paging device), angelegt. Damit das Programm Befehle ausführen und Daten bearbeiten kann, müssen die entsprechenden Seiten des Programmraumes aber nicht nur auf dem Seitenwechselgerät vorhanden sein, sondern sich auch im Hauptspeicher befinden. Eine zugwiesene Seite des Adreßraumes hat also immer einen Block des Seitenwechselgerätes als Träger und manchmal, nämlich wenn sie „angesprochen“ wird, zusätzlich einen Seite des Hauptspeichers. Sie ist dann „eingelagert“. Seiten des Adressraumes, man spricht vereinfachend auch von Seiten des Programms, sind also zu gewissen Zeiten eingelagert. Sie können zu verschiedenen Zeitpunkten auch unterschiedliche Hauptspeicherseiten als Träger haben. Eine vom Prozessor gelieferte Programmadresse muß bei jedem Speicherzugriff zunächst in eine Hauptspeicheradresse umgesetzt werde. Das muß sehr effizient geschehen. Programmabläufe müssen (fast) genau so schnell sein, als gäbe es keine Adreßumsetzung. Mit ausgefeilter assoziativ arbeitender Hardware, die zum Teil mehrstufig arbeitet, läßt sich das erreichen. Während eines Programmlaufs werden aber immer wieder Pogrammadressen erzeugt, die zu Seiten gehören, die nicht eingelagert sind. Wenn das passiert, kommt es zu einer Fehlseitenunterbrechung (page fault). Das Betriebssystem muß dann eine freie (eventuell freigeräumte) Seite im Hauptspeicher zur Verfügung stellen und auf sie die Programmseite von dem Seitenwechselgerät einlagern. Virtuelle Adressierung wurde ursprünglich eingeführt, um den Pogrammläufen auf einem Rechensystem einen deutlich größeren Adreßraum zuteilen zu können als der vorhandene Hauptspeicher zuließ. Ein weiterer Vorteil der Adreßumsetzung ist, daß Hauptspeicherseiten, die zu einem Zeitpunkt einem Programmlauf zugwiesen sind, nicht zusammenhängend sein müssen. Schließlich ist es mit der Adreßumsetzung auch möglich, ohne Zeitverzögerung Zulässigkeitstests auszuführen. Man kann zum Beispiel testen, ob die angesprochene Programmadresse dem Programmlauf überhaupt zugewiesen ist oder ob für sie ein Schreibzugriff erlaubt ist. 4.4. VIRTUELLE ADRESSIERUNG 147 Literatur Beschreibungen von Rechensystemen findet man in Kowalk [Kowa1996], Seiten 72-78 sowie 80-87, Goos [Goos1997], Abschnitt 1.5, sowie[Goos1996], Kapitel 11 „Vom Programm zur Maschine“, und Appelrath [AppeL1995], Abschnitt 1.3. Grundlegende Werke zur Hardware von Rechensystemen sind Stallings [Stal1996], Tanenbaum [Tane1990], Oberschelp/Vossen [OberV1994] und Hennessy/Patterson [HennP1994]. Das vorliegende Kapitel lehnt sich eng an die Ausführungen im Skript Stiege [Stie1995b] an. Allgemeine Lehrbücher zu Betriebssystemen sind Stallings [Stall2001], Tanenbaum [Tane1992], Brause [Brau1996] und Siegert/Baumgarten [SiegB1998]. Literatur zu speziellen Betriebssystemen ist auf Seite 142 ff angegeben. Die Funktionalität eines Compilers ist in Kowalk [Kowa1996], S. 79-80, kurz beschrieben; ein Standardwerk über Compilerbau ist Aho/Sethi/Ullman [AhoSU1986]. Virtuelle Adressierung und virtuelle Speicherverwaltung sind in den Lehrbüchern über Betriebssysteme beschrieben, z. B. Stallings [Stall2001]. Auf das Buch Computer Systems – A Programmers’s Perspective von Bryant und O’Halleron [BryaO2003] sei besonders hingewiesen. Es behandelt in einheitlicher Sicht und viel ausführlicher als es im vorliegenden Buch möglich das Zusammenspiel von Hardware und Software. 148 KAPITEL 4. RECHENSYSTEME Teil II Algorithmen 149 Kapitel 5 Algorithmen I: Naiv und formalisiert Die Bezeichnung Algorithmus geht auf Al Chwarismi 1 zurück, Algorithmen selber sind sehr viel älter. Rechenvorschriften, die den Namen Algorithmus verdienen, gab es schon in babylonischer Zeit (1800 v. Chr.), zum Beispiel zur Lösung spezieller Klassen quadratischer Gleichungen. Der älteste Algorithmus, der heute noch benutzt wird, ist wohl der Euklidische Algorithmus (siehe Abschnitt 1.1). In der ersten Hälfte des 20. Jahrhunderts gab es im Zusammenhang mit der Entwicklung der mathematischen Logik und der Erforschung der Grundlagen der Mathematik eine große Zahl von Untersuchungen und Ergebnissen zum Algorithmusbegriff. Ein erster Einblick in dieses Gebiet wird in Abschnitt 5.2 „Der formalisierte Algorithmusbegriff“ gegeben. Mit dem Aufkommen von digitalen Rechnenanlagen und ihrer Programmierung in der zweiten Hälfte des 20. Jahrhunderts wurde der Algorithmus zu einem zentralen Begriff der Informatik. Mit seinem grundlegenden Werk The Art of Computer Programming ([Knut1997], [Knut1998], [Knut1998a]) hat Knuth 2 in besonderem Maße dazu beigetragen, den Begriff des Algorithmus zu klären und seine Bedeutung für die Informatik zu Al Chwarismi, Muhammad Ibn Musa, ∗um 780 in Choresmien (Gebiet um Chiwa im heutigen Usbekistan), †nach 846, Bagdad. Iranisch-arabischer Mathematiker und Astronom am Hofe des Kalifen Al Mamun. Verfasser der ältesten systematischen Bücher über Gleichungslehre, indische Zahlen und jüdische Zeitrechnung sowie Verfasser astronomischer und trigonometrischer Tafelwerke und anderer Werke. Von seinem Namen leitet sich die Bezeichnung Algorithmus ab, der Titel eines seiner Werke (Al-kitab al-muqtasar fi hisab al-gabr wa al-muqabala = kurzes Buch über das Rechnen der Ergänzung und der Ausgleichung) ist der Ursprung des Begriffs Algebra. 2 Donald Ervin Knuth, ∗1938 Milwaukee, Wisconsin, USA. Studierte Mathematik am California Institute of Technology, dort Promotion 1963. 1968 Professor für Computer Science an der Stanford University, seit 1993 dort Professor Emeritus of the Art of Computer Programming. Hauptwerk: The Art of Computer Programming, das er selbst als „... a work-still-in-progress that attempts to organize and summarize what is known about the vast subject of computer methods and give it firm mathematical and historical foundations“ bezeichnet (Knuth, http://sunburn.stanford.edu/∼knuth). Geplant sind 7 Bände, seit 1968 sind 3 Bände (Fundamental Algorithms [Knut1997], Seminumerical Algorithms [Knut1998] und Sorting and Searching [Knut1998a]) in mehreren Auflagen erschienen. Ein vierter Band (Combinatorial Algo1 151 152 KAPITEL 5. ALGORITHMEN I: NAIV UND FORMALISIERT zeigen. 5.1 5.1.1 Der naive Algorithmus-Begriff Eigenschaften Ein Algorithmus ist eine Arbeitsvorschrift, oft eine Rechenvorschrift. Eine exakte Definition soll nicht gegeben werden, wohl aber sollen Eigenschaften genannt werden, die von einem Algorithmus verlangt werden. Endlichkeit: Ein Algorithmus muß für jede zulässige Eingabe nach endlich vielen Schritten halten. Ist ein Eingabewert möglich, aber nicht zulässig (z. B. eine negative ganze Zahl, wenn nur natürliche Zahlen erlaubt sind), so ist unbestimmt, ob der Algorithmus hält und, wenn ja, mit welchem Ergebnis. Bei der Formulierung eines Algorithmus als Programm ist es ratsam, alle prinzipiell möglichen Eingaben auch zu berücksichtigen und die davon nicht erlaubten über Fehlerbehandlung auszusondern. Eine Reihe wichtiger Programme und Programmsysteme sind keine Algorithmen, sie enden nach der Bearbeitung eines Eingabewertes nicht. Beispiele sind Systeme zur Verkehrsüberwachung oder zur Überwachung eines Kranken in einer Intensivstation. Auch Betriebssysteme gehören dazu. Eine Arbeitsvorschrift, die sonst alle Eigenschaften eines Algorithmus hat, aber nicht bei jedem Eingabewert hält, heißt Rechenmethode. Die als Beispiele genannten Programmsysteme arbeiten folgendermaßen: Sie sind in einem Wartezustand. Bei Eintreffen eines Eingabewertes (Kommando, Nachricht, Signal,...) werden die dafür vorgesehenen Aktionen ausgeführt und dann auf den nächsten Eingabewert gewartet. Aus diesem Grund werden sie auch reaktive Systeme genannt. Die Wichtigkeit reaktiver Systeme mindert die Bedeutung von Algorithmen und ihrer Untersuchung jedoch keinesfalls. Gerade bei diesen Systemen muß man sich darauf verlassen können, daß die Bearbeitung eines jeden Eingabewertes beendet wird und das System sich nicht „aufhängt“, sondern in den Wartezustand zurückkehrt. Bestimmtheit: Die Beschreibung der Schritte eines Algorithmus muß klar und eindeutig sein. Ob das der Fall ist, hängt zum einen davon ab, für wen der Algorithmus rithms) ist in Vorbereitung. Knuth ist Autor der Softwaresysteme TEXund METAFONT, der weltweit am stärksten verbreiteten Software für die Herstellung und den Satz mathematischer und informatischer Schriften und Bücher (darunter auch das vorliegende Buch). Aus dieser Arbeit ist das fünfbändige Werk Computers & Typesetting ([Knut1984], [Knut1986], [Knut1986a], [Knut1986b], [Knut1986c]) entstanden. 5.1. DER NAIVE ALGORITHMUS-BEGRIFF 153 bestimmt ist. Arbeitsvorschriften für Menschen können anders formuliert werden als Arbeitsvorschriften für Rechner. Bei Menschen können Kenntnis- und Wissensstand wiederum unterschiedliche Formulierungen zum Erreichen der Bestimmtheit erfordern. Die Bestimmheit eines Algorithmus hängt zum anderen vom Grad der Formalisierung ab. Die folgenden Darstellungsarten von Algorithmen weisen einen zunehmenden Formalisierungsgrad auf. a. Natürliche Sprache. Als Beispiel kann das Puddingrezept aus Abschnitt 1.3, Seite 25, dienen. b. Natürliche Sprache mit Regeln. Der Euklidische Algorithmus aus Unterabschnitt 1.1.1, Seite 3, ist hierfür ein Beispiel. c. Graphische Darstellungen. Beispiele für eine graphische Darstellung von Algorithmen sind Flußdiagramme (siehe Abbildungen 5.1, Seite 155, und 5.2, Seite 156) sowie Struktogramme (siehe Abbbildung 5.3, Seite 158). d. Pseudocode. Algorithmus IS zum Sortieren durch Einfügen (Tabelle 1.6, Seite 20) ist z. B. in Pseudocode formuliert. e. Programmiersprachen. Beispiele: In diesem Buch ist eine größere Anzahl von Programmen (in C) angegeben. Eine Übersicht ist in Anhang F „Verzeichnis der Algorithmen und Programme“, Seite 677, zu finden. f. Vollständig formalisierte Algorithmen (werden in Abschnitt 5.2 behandelt). Die aufgeführten Darstellungsarten von Algorithmen sind keine vollständige Aufzählung. Alle mit Ausnahme der formalisierten Algorithmen sind als naive Algorithmen anzusehen und für sie ist die Festlegung eines „Formalisierungsgrades“ unscharf. Intersubjektivierbarkeit: Algorithmen müssen von dem „Ausführer“ (Mensch oder Maschine) verstanden werden. Algorithmen, die in einer Sprache formuliert werden, die nur der, der den Algorithmus konzipiert, versteht, sind i. a. wenig nützlich. Eingabe und Ausgabe: Soll ein Algorithmus wirklich ablaufen, müssen ihm die Werte, mit denen er arbeiten soll, zugeführt werden können. Ebenso muß er Ergebnisse ausgeben können. 154 KAPITEL 5. ALGORITHMEN I: NAIV UND FORMALISIERT Effektivität: Alle Aktionen eines Algorithmus müssen effektiv ausführbar sein und alle Bedingen, die abgeprüft werden, müssen effektiv entscheidbar sein. Z. B. ist die bedingte Anweisung Wenn die Riemannsche Vermutung gilt, führe Aktion A aus, sonst Aktion B. z. Z. nicht effektiv ausführbar. Die Riemannsche Vermutung3 ist sicherlich richtig oder falsch, aber z. Z. ist nicht bekannt, was gilt. Anmerkung 5.1 Bis vor einigen Jahren war es üblich, an Stellen wie dieser den inzwischen bewiesenen „großen Satz von Fermat“ 4 zu zitieren. Er besagt, daß die Gleichung xn + y n = z n für positive natürliche Zahlen x, y, x, n keine Lösung hat, wenn n > 2. Der Satz wird auch als „Fermats letzter Satz“ bezeichnet und wurde 1995 von A. Wiles7 bewiesen. [Georg Friedrich] Berhard Riemann ∗1826 Breselenz (Kreis Lüchow-Danneberg), †1866 Selasca (heute zu Verbania, Piemont, Italien). Deutscher Mathematiker, Professor in Göttingen. Wesentliche Beiträge zur Theorie der Funktionen einer komplexen Veränderlichen („Riemannsche Zahlenkugel“, „Riemannsche Fläche“, „Riemannscher Abbildungssatz“), Geometrie („Riemannsche Geometrien“) Integralrechnung („Riemannsches Integral“), Zahlentheorie („Riemannsche ζ-Funktion“). Die Riemannsche Vermutung betrifft eine Aussage zur Lage der Nullstellen der Riemannschen ζ-Funktion. 4 Fermat, Pierre de ∗17.(?) August 1601 Beaumont-de-Lomagne (Tarnet-Garonne), †12. Januar 1665 Castres bei Toulouse. Französicher Mathematiker. Parlamentsrat in Toulouse. Wichtige Beiträge zur analytischen Geometrie und zur Analaysis. Bedeutendster Zahlentheoretiker seiner Zeit („kleiner Satz von Fermat“). Zeitgenosse von Pascal 5 , mit dem er über Fragen der Wahrscheinlichkeitsrechnung korrspondierte, und von Descartes 6 , dessen optische Arbeiten er um das „Fermatsche Prinzip“ ergänzte. 5 Pascal, Blaise ∗19. Juni 1623 Clermont-Ferrand, †19. August 1662 Paris. Französischer Philosoph, Mathematiker und Physiker. Religiös-philosophische und wissenschaftskritische Arbeiten. In der Mathematik Beiträge zur Elementargeometrie, zu den Kegelaschnitten und zur Analysis, insbesondere aber zur Kombinatorik und Wahrscheinlichkeitsrechnung („Pascalsches Dreieck“). Entwickelte eine der ersten Rechenmaschinen. Beiträge zur Physik (Kommunizierende Röhren, Verwendung des Barometers zur Höhenmessung). 6 Decartes, René ∗31. März 1596 La Haye-Decartes (Touraine), †11. Februar 1650 Stockholm. Französischer Philosoph, Mathematiker und Naturwissenschaftler. Nach Besuch einer Jesuitenschule ausgedehnte Reisen durch Europa. Einige Jahre Kriegsdienste (Nassau, Bayern). Aufenthalt in den Niederlanden, später in Schweden. Führte grundlegende erkenntnistheoretische Begriffe auf mathematischer und physikalischer Basis ein. In der Mathematik Beiträge zur Theorie transzendenter Kurven und zur Algebra. Besonders wichtig ist seine Grundlegung der analytischen Geometrie („kartesische Koordinaten“). Mitentdecker des Brechunsgesetzes der Optik 7 Wiles, Andrew John ∗11. April 1953 Cambridge, England. Britischer Mathematiker. Eugene Higgins Professor of Mathematics in Princeton, New Jersey, USA. Zum großen Fermatschen Satz und zu A. Wiles siehe im WWW www-history.mcs.st-andrews.ac.uk/history/HistTopics/Fermat’s_last_theorem.html www.treasure-troves.com/math/FermatsLastTheorem.html www-history.mcs.st-andrews.ac.uk/history/Mathematicians/Wiles.html . Siehe auch das Buch von Singh [Sing2000]. 3 5.1. DER NAIVE ALGORITHMUS-BEGRIFF 155 ' Flußdiagramme, auch Ablaufdiagramme genannt, geben den Steuerfluß eines Algorithmus oder Programms wieder. In der einfachsten Form werden die Zustände „Anfang“ und „Ende“ sowie Handlungen (Rechtecke) und Abfragen/Verzweigungen (Rauten) gezeichnet und durch Pfeile verbunden. Abbildung 5.2 zeigt den Algorithmus EEA (Euklid mit Ein- und Ausgabe) von Seite ... ... 7 als Flußdiagramm. ... ... qqqqq ................................................ ................... .......... .......... ....... ...... .... .... ... . ..... .. ..... ..... ....... . . . . . . ........... ..... . . . . . . . .......................... . . . . . . ............................... ... ... ... ... . ......... ......... .. ... ... ... ... ... ... ... . .... .. ....... .. . . ....... .......................................... ......... . . . . . . . . . . . . . . . . . . . . . . . ........... ........ . . . ....... . . . . ... ..... ..... .. ..... . .. .... ....... ..... . . . . . . ........... ..... . . . . . ..................... . . . . . . . . .................................... Anfang Zustände ... ... ... ... ... ......... ......... .. $ Ende qqqqq ... ... ... ... ... ......... ......... ... Handlungen ... ... ... ... ... .......... ........ ... qqq Abfragen und Verzweigungen & ... .. ... ... ... ... ... .. . . ... . .. ... ............... ............ .... ..... .... .. .. ... ...... .... .... ... .... .... .... ... . . .... .. . .... . . .. .... . . . . .. .............................................. ..................................................... . .. .... ....... .... .... ... . . . .... ... . .... . .. . .... . . .... ... .... ....... .... ... ... ja nein % Abbildung 5.1: Darstellungselemente für Flußdiagramme 5.1.2 Schrittweise Verfeinerung Schrittweise Verfeinerung (stepwise refinement) ist eine Vorgehensweise, bei der eine zu lösende Aufgabe in immer kleinere und einfachere Teilaufgaben zerlegt wird, bis diese direkt gelöst werden können. Wenn diese Technik die Arbeitsweise eines fertigen Algorithmus (vielleicht schon in Form eines Programms) ist, spricht man auch von dem Prinzip „Teile und Herrsche“. Mehr dazu ist in Unterabschnitt 5.1.3 zu finden. In diesem Unterabschnitt soll schrittweise Verfeinerung jedoch nicht als Funktionsprinzip von Algorithmen, sondern als Technik zum Finden und Entwerfen von Algorithmen untersucht werden. Als Beispiel soll das in Abschnitt 1.2 beschriebene Sortieren durch Einfügen dienen. Das entsprechende Programm ist in den Tabellen 1.7, Seite 22, und 1.8, 156 ' KAPITEL 5. ALGORITHMEN I: NAIV UND FORMALISIERT Euklidischer Algorithmus mit Ein- und Ausgabe (Algorithmus EEA, Seite 7) $ ........................................................................ ........... ........ ....... ..... ..... .. . ..... .. .... ....... ..... . . . . . . .......... ..... . . . . . .................... . . . . . . . . ....................................... ... ... ... ... . ......... ........ ... Anfang Lies Anfangswerte von n und m ... .. ... ... ... ... ... ... ... . ....... .. ......... ... ... ... ... ... ... ....... .. ........ ... Setze m :← n Dann n :← r .... ....... ........ .... ... ... ... ... ... ... ... ... ... ... . Teile m durch n und weise r den Rest... zu nein ... ... ... ... . ......... ........ ..... . . . ... ...... ... .... .... .... ... .... .... . .... . . .... .... . . .... .. . . ... . ...... .. .... .... .... .... . .... . . .... .... . . .... . .... ... .... .... .... ...... ..... ... ... ... ... .. ....... .. ......... ... r=0 ? ja Gib n aus ... ... ... ... ... ......... ........ . .................... . . . . . . . . . . . . . . ................ ............. ......... ............. ...... ........ .... ..... .. .... ... ... ..... ... ........ ...... . . . . . ............. . . . ................................................................ Ende & Abbildung 5.2: Flußdiagramm des Euklidischen Algorithmus mit Ein- und Ausgabe Seite 23, angegeben. Tabelle 5.1 zeigt, wie man mit schrittweiser Verfeinerung vorgehen könnte, wenn man Sortieren durch Einfügen realisieren möchte. Schrittweise Verfeinerung ist generell eine nützliche Vorgehensweise beim Entwurf von Algorithmen und Programmen. Oft wird die Bezeichnung jedoch in einem engeren Sinne, nämlich als gleichbedeutend zur strukturierten Programmierung benutzt. Bei strukturierter Programmierung sind als Konstruktionslemente von Programmen nur zugelassen: % 5.1. DER NAIVE ALGORITHMUS-BEGRIFF 1. 1. Schritt Vorbesetzungen und Eingabe 1.1 1.2 2. Sortieren 2.1 3. Ausgabe 3.1 2. Schritt Sortierfeld mit 0 vorbesetzen. 1. Sortierwert und eventuell weitere einlesen. 157 3. Schritt 1.2.1 Falls 1. Sortierwert negativ, Ende, sonst restliche Sortierwerte bis maximal 10000 einlesen. Werte des zweiten 2.1.1 Aktuellen einzuordbis letzten Sortiernenden Sortierwert feldes im sortierten solange mit Vorgänger Anfansgstück an im Feld vertauschen, der richtigen Stelle wie der Vorgängerwert einordnen. größer als der einzuordnende Sortierwert ist. Sortierfeld ausgeben. Tabelle 5.1: Gewinnung des Programms INSERTSORT durch schrittweise Verfeinerung 1. Sequenz (Folge). Alle Operationen einer Sequenz werden in der vorgegebenen Reihenfolge genau einmal ausgeführt. 2. Alternative (Auswahl). Abhängig von einer Bedingung wird eine von zwei Operationen ausgeführt und die andere nicht. Sprachlich wird eine Alternative meistens durch etwas ähnliches wie if...then...else ...fi ausgedrückt. Der else -Teil darf leer sein. Es ist auch möglich, daß genau eine von mehreren Operationen ausgeführt wird. Sprachlich wird das meistens durch case ausgedrückt. 3. Schleife (Iteration). Die Operation in einer Schleife wird so oft ausgeführt, wie die Schleifenbedingung es vorschreibt. Zählbedingungen werden i. a. über for -Schleifen, logische Bedingungen über while - oder until-Schleifen spezifiziert. 4. Funktionsaufruf. Funktionsaufrufe werden wie elementare Operationen angesehen. Werden die Funktionen einer Bibliothek entnommen oder sind es Betriebssystemaufrufe, so werden keine zusätzlichen Bedingungen an die Funktionen gestellt. Gehört die Konstruktion 158 KAPITEL 5. ALGORITHMEN I: NAIV UND FORMALISIERT der entsprechenden Unterprogramme jedoch zur Programmkonstruktion, so sind keine rekursiven Aufrufe zulässig. Zum Beispiel genügen im Programm LVA (Beispiel 2.3, Seite 69) die Unterprogramme aufbau und lvaneu den Regeln der strukturierten Programmierung, das rekursive Unterprogramm lvanamen jedoch nicht. Die Konstruktionselemente der strukturierten Programmierung dürfen beliebig kombiniert und geschachtelt werden. Als grafische Darstellung werden oft Struktogramme (auch NassiShneiderman-Diagramme genannt) verwendet. Abbildung 5.3 zeigt das Struktogramm zum Programm Sortieren durch Einfügen (siehe Abschnitt 1.2). ' Sortieren durch Einfügen (Programm INSERTSORT, Seiten 22 und 23.) $ Sortierfeld mit 0 vorbesetzen. 1. Sortierwert einlesen. ....................................... . ....................................... ...... ........................................ ...... ........................................ ...... .................................1 >= 0 ? ...... . . ............Sortierwert . . ........................................ . .... ....................................... ...... ........................................ ...... ....................................... ........................................ ...... nein ja ........................................ .......... ...... Sortierwert >= 0 ∧ weniger als 10001 Sortierwerte eingelesen Ende Nächsten Sortierwert einlesen for n := 1 to Anzahl Sortierwerte - 1 j := n - 1 k := Sortierfeld[n] j >= 0 ∧ Sortierfeld[j] > k Sortierfeld[j+1] := Sortierfeld[j] j := j + 1 Sortierfeld[j + 1] := k for n := 0 to Anzahl Sortierwerte - 1 Sortierfeld[n] ausgeben. & Abbildung 5.3: Struktogramm für Programm INSERTSORT % Anmerkung 5.2 Es läßt sich zeigen, daß sich alle Algorithmen - also auch solche, die Rekursionen enthalten, und solche, die wie z. B. Assemblerprogramme nur Sprunganweisungen kennen, durch Konstrukte der strukturierten Programmierung darstellen lassen8 . Man kann sich demnach, wenn man will, auf diese Konstrukte beschränken. Streng genommen, lassen sich Aussagen über alle Algorihtmen nur machen, wenn man einen formalisierten Algorithmusbegriff benutzt (siehe Abschnitt 5.2). Aus diesem Grund wird manchmal von einem „im Volk vordhandenen Wissen (folk theorem)“ gesprochen. Siehe den Aufsatz von Harel [Hare1980] und die Anmerkungen von Denning [Denn1980]. 8 5.1. DER NAIVE ALGORITHMUS-BEGRIFF 5.1.3 159 Entwurfstechniken für Algorithmen Der Name Entwurfstechniken für Algorithmen ist üblich, gemeint ist aber in erster Linie nicht der Vorgang des Findens und des Entstehens eine Algorithmus, sonder die typische Arbeistweise des fertigen Algorithmus. Im vorigen Jahrhundert sind für viele Probleme Lösungsalgorithmen und -programme gefunden worden, in der Informatik, in Mathematik, Naturwissenschaften und technischen Wissenschaften; aber auch in anderen Bereichen, insbesondere in den Wirtschaftswissenschaften und in der Liguistik. Dabei handelt es sich ebenso um sehr wichtige Anwendungen wie auch um eher theoretisch motivierte Fragestellungen. Aus diesem umfangreichen Erfahrungsschatz haben sich einige Grundideen und Vorgehensweisen herauskristallisiert, die auf große Klassen von Aufgaben angewendet werden konnten. Im folgenden werden die wichtigsten dieser Entwurfstechniken kurz umrissen. Eine ausführlichere Behandlung würde Kenntnisse von Datenstrukturen voraussetzen, die wir an dieser Stelle noch nicht haben und die erst in den Teilen III und IV vermittelt werden. Für weitere Entwurfstechniken siehe Abschnitt 6.3, Seite 216. Teile und herrsche Teile und herrsche9 (divide and conquer) ist eine Lösungmethode, bei der eine Aufgabe in immer kleinere Aufgaben zerlegt wird, bis schließlich eine ganz einache Lösung für die Teilaufgaben möglich ist. Diese Teillösungen werden dann zur gesuchten Gesamtlösung zusammengesetzt. Die Teillösungen werden alle auf die gleiche Art gfunden und daher ist Rekursion der geignete Ansatz. Ein typisches Beispiel dafür ist das in Unterabschnitt 6.1.2 beschriebene Mischsortieren. Ein weiteres typisches Beispiel ist QUICKSORT. Siehe Unterabschnitt 11.2. Häufig eignen sich Algorithmen, die auf Teile und Herrsche basieren, gut zur parallelen Bearbeitung. Zu parallelem Quicksort siehe Aufgabe 23.5, Seite 608. Dynamische Programmierung Dynamische Programmierung (dynamic programming) ist eine Lösungsansatz, bei dem wie bei „teile und herrsche “ ein Problem in immer kleinere Teileaufgaben zerlegt wird und diese zur Gesamtlösung zusammengesetzt werden. Die Lösungen der kleinsten Teilaufgaben werden jedoch nicht über Rekursion erreicht, sondern direkt gefunden und in einer Tabelle vermerkt. Daher (und nicht vom Codieren) die Bezeichnung “programming”. Im nachfolgenden Beispiel zur Berechnung von Fibonacci-Zahlen wird der Unterschied der beiden Vorgehensweisen deutlich. lat. diviae et impera. Trotz der lateinischen Formulierung ist der Ausspruch wohl nicht antik. Möglicherweise geht er auf Niccoló Machiavelli zurück. 9 160 KAPITEL 5. ALGORITHMEN I: NAIV UND FORMALISIERT Beispiel 5.1 (Berechnung der Fibonacci-Zahlen) Auf Seite 11 wurden die FibonacciZahlen durch F0 := 0, F1 := 1 und Fk := Fk−1 + Fk−2 für 2 ≤ k . rekursv definiert. Es ist unmittelbar klar, wie die Berechnung von fk unter Benutzumg von „teile und herrsche“ rekursiv zu programmieren ist. Das Beispiel in Abbildung 5.4 zeigt die rekursiven Aufrufe bei der Berechnung von f6 . Es ist leicht zu sehen, daß im f6 ................................... ......... ............. ............. ......... ............. ........ . . . . . . . ............. .. ............. ......... ............. ........ . . . . . . ............. . ..... . . ............. .... . . . . . . . . . ................ .. .... ................. ........... f5 f4 ..................... ......... ...... ......... ...... ......... ...... ......... ...... . . . . . ......... .... . . . ......... . . . ......... . ........... . ............. ............ . . . ................ ... f4 f3 . .......... .... ........... ...... ... ...... .... . . . ...... . ...... .... ...... . ...... ......... . .............. . .................... ... f.3 ... ... ..... ... ...... .... .. . .... ... .... . ... .... .. ............... ................. ... .. f1 f.2 ....... ... ........... ...... .... ...... ... ...... .... . . ...... . .. ...... . . . ...... .. ........ ... . . ............ . ................... . . f.2 ... ... ... ... ..... .. ... . . ... ... .. . ... .............. ............... ...... .. f1 f3 ...... ... ...... .... ... .... .. . .... ... ... .... .. .. .... . ................. ............. ... . f0 f1 f.2 .. ... .... ... .... ... .. . ... ... ... . ... .............. ................ ..... . .. f1 f0 f2 ....... ... ...... .... ... .... .. . .... ... .... ........... .. .. ................. .......... .. . f1 f.2 ..... ... .... ... .... ... .. . ... .. .. .. ..... ............. .. ...... ............. . . f1 f0 ... ... ... ... ..... .. ... . . ... ... .. .. ... .............. .............. ...... .. f1 f0 ....... ... .... ... .... ... ... . ... .. ........ ............... ... .... ......... . f1 f0 Abbildung 5.4: Rekursive Berechnung der Fibonacci-Zahlen allgemeinen Fall die Anzahl der Aufrufe exponentiell mit k wächst. Hingegen wird bei dynamischer Programmierung nacheinander f0 , f1 , . . . fk berechnet und der Aufwand ist linar in k. 2 Anmerkung 5.3 Dynamische Programmierung wurde 1953 von Richard Bellman 10 zur Lösung von Optimierungsaufgaben eingeführt. Sie wird in der Literatur über Optimierung deshalb auch dynamische Optimierung genannt. Von Bellman stammt auch das Optimalitätsprinzip das (vereinfacht ausgedrückt) besagt, dass man ein Optimum nur erhält, wenn Richard Ernest Bellman ∗20. August 1920 in New York City, New York, USA, † 19. März 1984 in Los Angeles, California, USA. Amerikanischer Mathematiker. Arbeitete auch in der Theoretischen Physik. Begründer der Dynamischen Programmierung. Fand einen Algorithmus zur Bestimmenung kürzester Wege in Graphen (siehe Abschnitt 19.3, Seite 516). 10 5.1. DER NAIVE ALGORITHMUS-BEGRIFF 161 man von einem Optimum ausgeht und bei jedem Schritt aus den gewonnen Zwischenergebnissen ein neues Optimum bestimmt [Bell2003]. 2 Rücksetzen Rücksetzen (backtracking) ist im wesentlichen die vollständige Durchsuchung eines Lösungraumes. Es wird zurückgesetzt, wenn die Suche an einer Stelle nicht fortgesetzt werden kann oder nicht fortgesetzt werden soll. Meistens wird mit Rekursion gearbeitet. Beispiel 5.2 (Rucksackproblem) Beim Rucksackproblem (knapsack problem) ist eine nichtleere endliche Menge I von Gegenständen gegeben, von denen eine Auswahl in einen Rucksack gepackt werden soll. Die Gegenstände haben eine Gewicht w und einen Wert v. Der Rucksack11 trägt maximal das Gewicht b. Zulässige Auswahlen J von Gegenständen sind solche, deren Gesamtegewicht b nicht überschreitet. X J := {J ⊆ I| w(g) ≤ b} g∈J Gesucht wird eine zulässige Auswahl J0 von Gegenständen, deren Wert maximal ist: X X v(g) = max v(g) g∈J0 J∈J g∈J Bei einer algorithmischen Lösung dieser Aufgabe wird man wohl mit einer Durchmusterung aller Teilmengen J von I beginnen und zunächst einmal eine Teilemenge solange erweitern, bis die Erweiterung zu schwer wird. Dann wird man die größte zulässige Erweiterung prüfen, ob ihr Wert den größten bisher gefundenen Wert übetrifft. Ist das nicht der Fall, setzt man zurück und fährt mit einer anderen Teilmenge fort. Die zum Schluß gefundene Lösung ist eine optimale Rucksackfüllung. Die Details des Vorgehens sind in Aufgabe 5.1 auszuarbeiten. Wie die Maximumsbildung über alle Teilmengen vermuten läßt, ist das Rucksackproblem aufwändig. In der Tat, es ist N P-vollständig (siehe Abschnitt 6.2). Wie in Unterabschnitt 6.3 erwähnt und in [Hrom2007], Abschnitt 7.2, ausgeführt, kann man Hilfe von Methoden der dynamischen Programmierung pseudoplynomielle Lösungen finden. 2 Lokale Suche Lokale Suche (local search) wird bei Optimierungsfragen eingesetzt. Im Raum der möglichen Lösungen wird eine Nachbarschaftsstruktur eingeführt und es wird geprüft, ob ein Nachbar der aktuellen Lösung einen besseren Wert der zu optimierenden Funktion liefert. Wenn das der Fall ist, wird mit diesem Nachbarn fortgefahren. Wenn nein, haben wir ein 11 Besser wohl der Träger ded Rucksacks. 162 KAPITEL 5. ALGORITHMEN I: NAIV UND FORMALISIERT lokales Optimum gefunden. Normalerweise ist ein lokales Optimum nicht global, also noch nicht die gewünschte Endlösung. Die Bestimmung minimaler erzeugender Bäume – siehe Seite 548 – ist ein Beispiel, in dem lokale Suche auch ein globales Optimum liefert. Gieralgorithmen Man spricht von einem Gieralgorithmus (greedy algorithm), wenn der Algorithmus bei jedem Schritt mit der Richtung fortfährt, die im Augenblick den größten Nutzen verspricht. Solche Algorithmen heißen auch gefräßig. Gieralgorithmen werden in erster Linie bei Optimierungsaufgaben eingesetzt. In manchen Fällen führt gieriges Vorgehen zum Optimum, in anderen nicht. Beispiele für das Finden des Optimums mit einem Gieralgorithmus sind die Bestimmung eines minimalen erzeugenden Baumes mit dem Algorithmus von Kruskal (Seite 547) oder mit dem Algorithmus von Prim (Seite 548). 5.1.4 Algorithmen in applikativer Darstellung Alle bisher behandelten Algorithmen und Programme hatten gemeinsam, daß die auszuführende Anweisungen explizit hingeschrieben wurden und die Reihenfolge des Hinschreibens im wesentlichen die Ausführungsreihenfolge bestimmt. Dabei waren Alternativen, Schleifen, nichtrekursive Funktionsaufrufe und, falls man sich nicht auf strukturierte Programmierung beschränkt, auch rekursive Funktionsaufrufe zur Steuerung des Ablaufs zugelasssen. Bei einigen Formulierungen, zum Beispiel beim Euklidischen Algorithmus E, Seite 4, wurden auch Sprünge benutzt. Algorithmen, die so formuliert werden, heißen imperativ. Bei Programmen spricht man von imperativer Programmierung oder auch prozeduraler Programmierung. Das wesentliche hieran ist, daß die Anweisungen explizit die Bearbeitung von Datenobjekten beschreiben. Man kann jedoch auch anders vorgehen und Algorithmen als Definition von Funktionen (im mathematischen Sinne) oder als Definition von logischen Prädikaten ansehen. Das führt zu der folgenden Einteilung (nach Goos [Goos1997]): ' • Algorithmen/Programme in imperativer (prozeduraler) Darstellung bilden und bearbeiten Datenobjekte explizit. $ • Algorithmen/Programme in applikativer (funktionaler) Darstellung sind Funktionsdefinitionen. Die Ausführung eines Algorithmus/Programms besteht in der Berechnung von Funktionswerten. • Algorithmen/Programme in logischer Darstellung sind logische Prädikate. Die Ausführung besteht in der Anfrage, ob ein solches Prädikat angewandt auf bestimmte Terme (die Eingabe) richtig ist. & % 5.1. DER NAIVE ALGORITHMUS-BEGRIFF 163 Diese Darstellungformen werden Programmierparadigmen genannt. In diesem Unterabschnitt geht es nur um applikative Darstellungen. Ihren Ursprung haben diese Darstellungen im λ-Kalkül von Church.12 McCarthy 13 entwickelte hieraus 1959 am Massachusetts Institute of Technology die Programmiersprache Lisp, die „Mutter aller funktionalen Programmiersprachen“. Moderne funktionale Programmiersprachen sind z. B. Gofer (siehe [Goos1997]) oder HASKELL [Thom1996], [Dobe2012]. Im Rest dieses Unterabschnitts wird nicht auf funktionale Programmierung eingegangen, sondern es wird an einigen Beispielen gezeigt, wie Algorithmen in applikativer Darstellung formuliert werden können. Als Datentypen werden dabei nur die ganzen Zahlen Z und die booleschen Werte true und false zugelassen. Bei diesen Beispielen folgen wir dem Skript von Ehrich [Ehri1987]. Die Algorithmen werden als Funktionen geschrieben: f(x) = x + 1 Der Algorithmus berechnet bei Eingabe eines ganzzahligen Wertes x den Wert x + 1. Auch Funktionen von mehreren Veränderlichen können dargestellt werde. Zum Beispiel kann f (x, y) = (x + y)2 als f(x,y) = x*x + 2*x*y + y*y geschrieben werden. Alternativen werden mit if ... fi angebeben, Absolutwert und Signum von x zum Beispiel als abs(x) = if x ≥ 0 then x else -x fi sgn(x) = if x = 0 then 0 else if x > 0 then 1 else -1 fi fi Rekursionen sind das wichtigste Ausdrucksmittel bei applikativen Darstellungen. Die Fakultätsfunktion x! := x · (x − 1) · · · · · 2 · 1 für x ≥ 1 und 0! := 1 kann z. B. wie folgt geschrieben werden fak(x) = if x = 0 then 1 else x*fak(x-1) fi Das ergibt z. B. fak(3) = 3*fak(2) = 3*2*fak(1) = 3*2*1*fak(0) = 3*2*1*1 = 6 Es sollen stets alle ganzen Zahlen als Eingabewerte zugelassen werden, auch wenn für einige der Algorithmus nicht hält, sondern kreist, streng genommen also kein Algorithmus mehr ist. Z. B. fak(-2) = (-2)*fak(-3) = (-2)*(-3)*fak(-4) = ... Rekursionen können gekoppelt sein. Mit den beiden folgenden rekursiv gekoppelten Funktionen kann man z. B. ohne ganzahlige Division testen, ob eine gerade oder eine ungerade Zahl vorliegt. 12 13 Zu Church siehe die Fußnote auf Seite 169. McCarthy, John, amerikanischer Informatiker, Pionier der künstlichen Intelligenz. 164 KAPITEL 5. ALGORITHMEN I: NAIV UND FORMALISIERT gerade(x) = ungerade(x) = if x else else if x else else = 0 then true if x > 0 then ungerade(x+1) = 0 then false if x > 0 then gerade(x+1) fi ungerade(x-1) fi fi gerade(x-1) fi Die vorgestellten Ausdrucksmittel für applikative Algorithmen sollen nun bei drei Beispielen angewandt werden. Beispiel 5.3 f(x) = if x > 100 then x-10 else f(f(x+11)) fi Für Werte größer als 100 kann das Ergebnis unmittelbar angegeben werden: f(101) = 91, f(102) = 92, f(1000) = 990. Einige Beispiele für Eingaben kleiner oder gleich 100: f(100) f(99) f(98) = = = f(f(111) f(f(110) f(f(109) = = = f(101) f(100) f(99) = = = 91 ... ... = = 91 91 Diese und weitere Beispiele lassen die Vermutung aufkommen, daß f(x) = 91 für alle x < 101. Die Vermutung ist richtig und kann durch vollständige Induktion bewiesen werden. Induktionsanfang: Die Behauptung ist richtig für x = 100, wie man direkt nachprüft. Induktionsschritt: Die Behauptung sei richtig für x = 100, 99, · · ·, n. Zu zeigen ist f(n-1) = 91. Unter Berücksichtigung der Induktionsvoraussetzung ergibt sich f(n) für 91 ≤ n ≤ 100 f(n-1) = f(f(n+10)) = = 91. 2 f(91) für n ≤ 90 Beispiel 5.4 f(x) g(x) = = if x = 1 then 1 else f(g(x)) fi if gerade(x) then x2 else 3*x+1 fi Für x = 0 kreist der Algorithmus: f(0) = f(0) = ... Falls x < 0, so sind stets x2 und 3*x+1 negativ und das Argument kann niemals den Wert 1 annehmen. Der Algorithmus kreist auch in diesem Fall. Einige Beispiele für positive x: f(1) = 1 f(2) = f(1) = 1 f(3) = f(10) = f(5) = f(16) = f(8) = f(4) = f(2) = f(1) = 1 f(4) = 1 5.1. DER NAIVE ALGORITHMUS-BEGRIFF 165 f(5) = 1 f(6) = f(3) = 1 f(7) = f(22) = f(11) = f(34) = f(17) = f(52) = f(26) = = f(13) = f(40) = f(20) = f(10) = 1 Es wird vermutet, ist aber noch nicht bewiesen, daß f(x) = 1 für alle x ≥ 1. 2 Beispiel 5.5 (Ackermannfunktion) Die Ackermannfunktion14 ist definiert durch af(x,y) = if x ≤ 0 then y+1 else if y ≤ 0 then af(x-1, 1) else af(x-1, af(x, y-1)) fi fi Einige Beispiele: af(0, 0) af(0, 1) af(1, 0) af(1, 1) af(2, 2) = = = = = = = = = = = = 1 2 af(0, 1) = 2 af(0, af(1, 0)) = af(0, af(0, 1) = af(0, 2) = 3 af(1, af(2, 1)) = af(1, af(1, af(2, 0))) af(1, af(1, af(1, 1))) = af(1, af(1, 3)) af(1, af(0, af(1, 2))) = af(1, af(0, af(0, af(1, 1)))) af(1, af(0, af(0, 3))) = af(1, af(0, 4)) = af(1, 5) af(0, af(1, 4) = af(0, af(0, af(1, 3))) = af(0, af(0, af(0, af(1, 2)))) af(0, af(0, af(0, af(0, af(1, 1))))) = af(0, af(0, af(0, af(0, 3)))) af(0, af(0, af(0, 4))) = af(0, af(0, 5) = af(0, 6) 7 Ähnlich läßt sich (über eine sehr lange Herleitung) af(3, 3) = 61 berechnen. Wir wollen uns einen systematischen Überblick über den Verlauf der Ackermannfunktion für kleine Werte von x verschaffen. Nach einigen Beispielen wird man af(1, y) = y + 2 für y ≥ 1 vermuten und kann das auch leicht zeigen (Aufgabe 5.3). Ackermann, Wilhelm, ∗1806 in Schönebeck (Kreis Altena), †1962 in Lüdenscheid. Deutscher Logiker. Studienrat an den Gymnasien in Burgsteinfeld und Lüdenscheid. Fand 1929 mit der später nach ihm benannten Funktion ein Beispiel für eine rekursive, aber nicht primitiv rekursive Funktion. Die in Beispiel 5.5 angebene Funktion ist eine vereinfachte Variante der ursprünglichen Ackermannfunktion. 14 166 KAPITEL 5. ALGORITHMEN I: NAIV UND FORMALISIERT In Aufgabe 5.3 ist auch eine geschlossene Darstellung für af(2, y) (y ≥ 1) zu finden und zu beweisen. Des weiteren ergib sich für y ≥ 1 af(3, y) = 2y+3 − 3 2 .2 . . 2 af(4, y) = 22 Beweise auch in Aufgabe 5.3. 2 22 2 (5.1) (y+3)-mal −3 (5.2) 265536 2 10log 2 · 65536 106553 −3 = 22 Das ergibt z. B. af(4, 4) = 22 −3 = 22 −3 > 22 , eine Zahl die jede Realität sprengt. 2 5.2 5.2.1 Der formalisierte Algorithmusbegriff Markov-Algorithmen Es gibt unterschiedliche Möglichkeiten, den Begriff eines Algorithmus streng formal zu fassen. Wir wollen die Formalisierung nach Markov 15 genauer untersuchen. Markov-Algorithmen bearbeiten Zeichenreihen (zu Zeichenreihen siehe Abschnitt 3.6). Wir gehen zunächst von Markov-Methoden (Markov method] aus. Eine Markov-Methode ist gegeben durch ein Alphabet A, eine Eingabemenge E (E ⊆ A∗ ), eine Markov-Tafel. Außerdem legt eine für alle Markov-Methoden einheitliche Übergangsvorschrift (transition rule) fest, wie ein Eingabewort mit Hilfe der Markov-Tafel in aufeinanderfolgenden Schritten zu transformieren ist und wann dieser Vorgang eventuell hält. Eine Markov-Tafel (Markov table) ist eine Tabelle mit 5 Spalten und mindestens einer Zeile. In der ersten Spalte sind die Zeilen von 0 an aufwärts numeriert. In der zweiten und in der vierten Zeile stehen Wörter aus A∗ . In der dritten und in der fünften Zeile stehen natürliche Zahlen (siehe Beispiel 5.6). Markov, Andrei Andrejewitsch (jun), geb. 1903. Russischer Mathematiker. Professor in Leningrad und Moskau. Arbeiten zur Topologie, toplogischen Algebra und zur Theorie dynamischer Systeme. Führte 1951 die „Markov-Algorithmen“ ein [Mark1954]. Die in diesem Buch behandelten Markov-Algorithmen sind eine modifizierte Form (Kandzia/Langmaack [KandL1973]) der ursprünglichen Markov-Algorithmen. Markov, A.A. (jun.) ist Sohn von Markov, Andrei Andrejewitsch, ∗1856 Rjasan, †1922 Petrograd (heute St.Petersburg). Russischer Mathematiker, Professor in St. Petersburg. Arbeiten zur allgemeinen Analysis und zur Stochastik („Markov-Eigenschaft“, „Markov-Ketten“). 15 5.2. DER FORMALISIERTE ALGORITHMUSBEGRIFF 167 Die Übergangsvorschrift legt fest, wie ein Wort aus der Eingabemenge E verändert wird. Die Bearbeitung beginnt stets mit Zeile 0 der Tafel. Es wird geprüft, ob die Zeichenreihe in Spalte 2 der Tafel eine Teilzeichenreihe des Eingabewortes ist. Wenn nein, bleibt das Eingabewort unverändert und es wird mit der Zeile, deren Nummer in Spalte 3 steht fortgefahren. Wenn ja, wird das in der Eingabe am weitesten links stehende Vorkommen der Teilzeichenreihe durch die Zeichenreihe in Spalte 4 der Tafel ersetzt und mit der veränderten Eingabezeichenreihe in der Zeile, deren Nummer in Spalte 5 steht, auf die gleiche Art fortgefahren. Die Bearbeitung endet, wenn in Spalte 2 bzw. Spalte 4 eine Zeilennummer gefunden wird, zu der in der Tafel keine Zeile existiert. Wir wollen als solche Zeilennumer stets die erste natürliche Zahl nehmen, die größer ist als alle in der Tafel vorkommenden Zeilennummern. Wenn die Bearbeitung endet, heißt die Zeichenreihe, die aus der Eingabezeichenreihe entsteht, die Ausgabe (das Ergebnis) der Bearbeitung (siehe Beispiel 5.6). Beispiel 5.6 A = {, ◦, M} E = A∗ 0 1 2 3 1 M 2 ◦ 4 ◦ 4 2 MM 3 ◦◦ 3 λ 2 (λ ist die leere Zeichenreihe.) Es sollen nun drei Eingabewörter durchgespielt werde. Die Ablauftabellen zeigen unter „ZR“ die Zeichenreihe vor der Bearbeitung in der entsprechenden Zeile. Eingabezeichenreihe: M ZR Zeile Schritt 0: M 0 Schritt 1: M 2 Schritt 2: M 4 Ausgabezeichenreihe: Eingabezeichenreihe: ◦ M Schritt Schritt Schritt Schritt Schritt 0: 1: 2: 3: 4: ZR Zeile ◦M 0 ◦M 1 ◦ MM 3 MM 2 MM 4 Eingabezeichenreihe: ◦ ◦ Ausgabezeichenreihe: MM M 168 KAPITEL 5. ALGORITHMEN I: NAIV UND FORMALISIERT Schritt Schritt Schritt Schritt Schritt 0: 1: 2: 3: 4: ZR Zeile ◦◦ 0 ◦◦ 2 ◦◦ ◦ 3 ◦◦ 2 ◦◦ ◦ 3 .. . Hält nicht; keine Ausgabezeichenreihe. Ein Markov-Algorithmus ist eine Markov-Methode, die für jedes Wort der Eingabemenge E hält. 2 Die Tabelle von Beispiel 5.6 bildet mit der Eingabemenge E = { M, ◦ M} und auch mit der Eingabemenge E = { M}∗ Algorithmen. Mit der Eingabemenge E = { M, ◦ M, ◦ ◦} bildet sie keinen Algorithmus. Zum Abschluß noch ein einfaches Beispiel. Beispiel 5.7 (Addition zweier natürlicher Zahlen) Es wird das Alphabet A = {|, t} zugrundegelegt. Natürliche Zahlen werden als Folge von Strichen dargestellt 1:| 3 : ||| 7 : ||||||| 0 : λ (leere Zeichenreihe). t wird als Trennzeichen benutzt. Die Addition der natürlichen Zahlen m und n wird darge||| · · · | t ||| · · · | stellt als | {z } | {z }. Als Eingabemenge ergibt sich E = {|}∗ t{|}∗ . Als Markov-Tafel m n kann 0 t 1 λ 1 genommen werden. Ist die Eingabemenge E = {|, t} und will man eine Fehlerüberprüfung durchführen (Das Zeichen t muß genau einmal vorkommen!), so kann man das mit der folgenden Tabelle erreichen: 0 t 3 1 t 4 2 | 4 3 λ 2 λ t λ t 1 2 2 2 Man mache sich klar, daß der Algorithmus bei korrekter Eingabe mit dem korrekten Ergebnis endet. Andernfalls endet er mit der Ausgabe t als Fehleranzeige. 2 5.2. DER FORMALISIERTE ALGORITHMUSBEGRIFF 5.2.2 169 Churchsche These Ein Problem heißt intuitiv-algorithmisch lösbar, wenn es einen Algorithmus (im naiven Sinn) gibt, der es löst. Ein Problem heißt formal-algorithmisch lösbar, wenn es einen formalisierten Algorithmus gibt, der es löst. Jeder formalisierte Algorithmus muß natürlich auch die in Unterabschnitt 5.1.1 angegeben Eigenschaften haben. In den ersten Jahrzehnten des 20. Jahrhunderts wurde von Hilbert 16 und anderen versucht, den Algorithmusbegriff mittels primitiv-rekursiver Funktionen zu formalisieren. Als jedoch Ackermann 1928 die später nach ihm benannte Funktion (siehe Unterabschnitt 5.1.4) entdeckte, hatte man ein Beispiel für eine berechenbare, aber nicht primitiv-rekursiv berechenbare Funktion. Primitiv-rekursive Funktionen reichen nicht aus, um alle bekannten Algorithmen zu erfassen. Danach wurden in kurzer Zeit eine Reihe verschiedener Formalisierungen des Algorithmusbegriffs gefunden. 1. Allgemein-rekursive Funktionen. Herbrand 17 (1931), Gödel 18 (1934), Kleene 19 (1936). 2. µ-rekursive Funktionen. Kleene (1936). 3. λ-definierbare Funktionen (λ-Kalkül). Church 20 (1936). 4. Turing-berechenbare Funktionen, d. h. mit Turingmaschinen berechenbare Funktionen. Turing 21 (1936). Hilbert, David ∗1862 in Königsberg, †1943 in Gottingen. Deutscher Mathematiker, Professor in Königsberg, ab 1895 in Göttingen. Grundlegende Beiträge zur Algebra, Geometrie, Analysis („Hilbertraum“) sowie zur mathematischen Physik. Außerdem wichtige Arbeiten zu den Grundlagen der Mathematik und zum formalisierten Algorithmusbegriff (siehe die Bücher Hilbert/Bernays „Grundlagen der Mathematik“ ([HilbB1968], [HilbB1970]) und Hilbert/Ackermann „Grundlagen der theoretischen Logik“ [HilbA1972]. 17 Herbrand, Jaques, ∗1908, †1931. Französischer Philosoph. Von ihm stammt der „Herbrandsche Satz“ zur Quantorenlogik, Ausgangspunkt für Resultate zum Entscheidungsproblem, Widerspruchsfreiheitsbeweise sowie zur Verbindung von Modell- und Beweistheorie. 18 Gödel, Kurt, ∗1906, Brünn, †1978 in Princeton, N.J. Österreichischer Mathematiker und Logiker. Professor am Institute for Advanced Studies in Princeton, New Jersey. Bedeutende Beiträge zur Logik und mathematischen Grundlagenforschung (Vollständigkeit der Quantorenlogik erster Stufe, Unvollständigkeitssatz zur Axiomatisierung der Mengelehre („Gödelisierung“)., 19 Kleene, Stephen Cole, ∗1909 in Hartford, Connecticut, †1994 in Madison, Wisconsin. Amerikanischer Mathematiker und Logiker. Wesentliche Beiträge zur Logik und Theorie der Berechenbarkeit. Führte die regulären Ausdrücke ein. 20 Church, Alonzo, ∗1903 in Washington, DC, †1995 in Hudson, Ohio. Amerikanischer Mathematiker und Logiker. Professor an der Princeton University, später an der University of California, Los Angeles. Arbeiten zur Logik und zum Algorithmusbegriff („Churchsche These“). 21 Turing, Alan Mathison, ∗1912 in London, †1954 in Wilmslow (GB). Britischer Mathematiker. Wirkte in Cambridge und Manchester, 1936 - 1938 Zusammenarbeit mit A. Church, dabei entstand der Entwurf der Turingmaschine. Arbeiten zur maschinellen Intelligenz („Turing-Experiment“), zur Codeentschlüsselung (Entschlüsselung deutscher militärischer Codes im Krieg) und zur Entwicklung von elektronischen 16 170 KAPITEL 5. ALGORITHMEN I: NAIV UND FORMALISIERT 5. Markov-Algorithmen. Markov (1951). Diese Formalisierungen und weitere, die später hinzukamen, stellten sich trotz ihrer Unterschiede als gleich leistungsfähig heraus und es ist auch bis jetzt kein naiver Algorithmus bekannt, der sich nicht auch formalisiert darstellen ließe. Zusammenfassend: a. Es gibt intuitiv-algorithmisch lösbare Probleme, die nicht durch primitiv-rekursive Funktionen lösbar sind (Ackermann). b. Es läßt sich mathematisch beweisen: ◦ Die Algorithmus-Formalisierungen 1 - 5 sind äquivalent. ◦ Primitiv-rekursive Funktionen lösen eine echte Teilmenge der mit 1 - 5 lösbaren Probleme. c. Es ist eine Erfahrungstatsache: Es ist kein intuitiv-algorithmisch lösbares Problem bekannt, das mit 1 - 5 nicht auch formal-algorithmisch lösbar wäre. Church hat daraus den Schluß gezogen, daß intuitiv-algorithmisch lösbar und formalalgorithmisch lösbar das gleiche bedeuten. Churchsche These: Ein Problem ist genau dann intuitiv-algorithmisch lösbar, wenn es formal-algorithmisch lösbar ist. 5.2.3 Algorithmisch unlösbare Probleme Im Zusammenhang mit der Formalisierung des Algorithmusbegriffs entdeckte man, daß es auch sinnvolle Fragenstellungen – „Probleme“ – gibt, für die kein Lösungsalgorithmus existiert. Nach der Churchschen These reicht es dazu aus, ein Problem anzugeben, das mit keinem Algorithmus eines bestimmten Formalismus, zum Beispiel mit keiner Turingmaschine, gelöst werden kann. Wir wollen als Beispiel eines algorithmisch unlösbaren Problems das Halteproblem betrachten, genauer das Halteproblem für Markov-Methoden. Dazu betrachten wir MarkovMethoden über dem Alphabet {0, 1}, für die alle Bitmuster als Eingabe zugelassen sind, d. h. für die E = {0, 1}∗. Ist M eine solche Methode, so fragen wir, ob M bei Eingabe des Bitmusters x hält oder nicht hält, d. h kreist. Das Halteproblem besteht darin, einen Algorithmus anzugeben, der für jede Markov-Methode M und jede Eingabe x feststellt, ob M angewandt auf x hält oder kreist. Rechenanlagen. Nach ihm ist der „Turing award“, die bedeutendste wissenschaftliche Auszeichnung in der Informatik, benannt. 5.2. DER FORMALISIERTE ALGORITHMUSBEGRIFF 171 Es soll gezeigt werden, daß es keinen Algorithmus gibt, der das Halteproblem löst. Nach der Churchschen These genügt es, zu zeigen. daß es keinen Markov-Algorithmus gibt, der das Problem löst. Um das zu sehen, werden zunächst M und x als eine Zeichenreihe über dem Alphabet {0, 1, s, z, e} dargestellt. Die Markov-Methode ist durch ihre Markov-Tafel gegeben. Die Werte in den Spalten 1, 3 und 5 der Tafel sind natürliche Zahlen und können als Bitmuster dargestellt werden, z. B wie auf Seite 168. Die Werte in den Spalten 2 und 4 sind von vornherein Bitmuster. Das Ende einer Spalte wird durch das Zeichen s, das Ende einer Zeile durch das Zeichen z gekennzeichnet. Das Zeichen e schließlich trennt die Darstellung der Markov-Tafel der Methode M von dem Eingabewert x, der wiederum ein Bitmuster ist. Trifft man nun die Zuordnung 0 1 s z e → → → → → 000 001 010 011 100 , so wird {0, 1, s, z, e}∗ injektiv in {0, 1}∗ abgebildet und für jedes Paar (M, x) erhält man eineindeutig ein Bitmuster, das wir B(M, x) nennen wollen. B(M, x) kann man einer f als Eingabe zuführen. M f wird dann halten oder kreisen. zweiten Markov-Methode M Wir wollen annehmen, daß es eine Markov-Methode M0 gibt, die immer hält, wenn sie auf B(M, x) angewandt wird und als Ergebnis 0 liefert, wenn M angewandt auf x kreist, und 1 liefert, wenn M angewandt auf x hält. Das soll für jedes M und jedes x gelten. Um diese Annahme auf einen Widerspruch zu führen, wird aus M0 eine abgewandelte Markov-Methode S konstruiert. Es habe die Markov-Tafel von M0 die Zeilen mit den Nummern 0, 1, 2, . . . , n − 1. Die Angabe der Zeilennummer n in Spalte 2 oder 4 der Tafel bedeutet dann, daß M0 anhält. Nach Annahme ist das Ergebnis an dieser Stelle 0 oder 1. Um S zu erhalten, wird die Tafel von M0 um eine Zeile erweitert: n 1 n+1 1 n Sie bewirkt, daß S kreist, wenn M0 eine 1 liefert, und daß S (mit dem Ergebnis 0) hält, wenn M0 eine 0 als Ergebnis hat. Der gesuchte Widerspruch ergibt sich, wenn man irgendein x nimmt und S auf B(S, x) anwendet: # Wenn S angewandt auf B(S, x) hält, wird M0 angewandt auf B(S, x) mit dem Ergebnis 1 halten und S angewandt auf B(S, x) kreisen. Wenn S angewandt auf B(S, x) kreist, wird M0 angewandt auf B(S, x) mit dem Ergebnis 0 halten und ... .. . S angewandt auf B(S, x) wird mit dem gleichen Ergebnis halten. ............................ " ! 172 KAPITEL 5. ALGORITHMEN I: NAIV UND FORMALISIERT Der gefundene Widerspruch zeigt, daß es einen Markov-Algorithmus M0 , der das Halteproblem löst, nicht geben kann. In Abbildung 5.5 wird der Widerspruch veranschaulicht. M angewandt auf x kreist. B(M, x) .. ..................................................................................................................... .. .. ..................................................................................................................... .. 0 .. ..................................................................................................................... . 1 M0 M angewandt auf x hält. 0 B(S, x) .. ......................................................................................................................................................... .. .. ................................................................................................................... .. M0 0 .. ........................................................................................................................................ ...... ...... .. ..... ..... . ... . . ... ... ... .... ... .. ... .. ... ... ... ... .. . . ... . . . ... ... ... ..... .... ..... ....... ................................ 1 S Abbildung 5.5: Das Halteproblem: Markov-Methoden M0 und S Anmerkung 5.4 Es gibt keinen Markov-Algorithmus, der das Halteproblem löst. Einen Algorithmus, der ein Bitmuster testet, ob es eine korrekte Darstellung B(M, x) ist, kann man jedoch leicht konstruieren. Man kann auch eine Markov-Methode U angeben, die jede andere Markov-Methode nachspielen kann. Wird U auf B(M, x) angewandt, so kreist U, wenn M angwandt auf x kreist. Hält M angewandt auf x, so hält auch U angewandt auf B(M, x) und zwar mit dem gleichen Ergebnis. Man spricht von einer universellen Markov-Methode 5.2.4 Turing-Maschinen und formale Sprachen* Die Turing-Maschine. In Abschnitt 2.1, Seite 29, haben wir virtuelle und reale Maschinen eingeführt. Eine Maschine, die keine virtuelle Maschine ist, also eine reale Maschine muß nicht notwendiger- 5.2. DER FORMALISIERTE ALGORITHMUSBEGRIFF 173 weise physikalisch existieren. Sie kann rein mathematisch definiert sein und als gedankliches Konstrukt existieren. Wie andere mathematische Konstrukte auch, wird sie durch Abbildungen und einen Satz von Axiomen definiert. Anschaulich besteht ein Turingmaschine aus einem Band, einem Schreib/Lesekopf und einer Steuereinheit. Siehe Abbildung 5.6. Das Band ist unendlich und in gleichartige Zeichenpositionen eingeteilt. An den Positionen befinden sich Zeichen aus einem endlichen Zeichevorrat. Ein besonderes Zeichen ist 2 (Leerzeichen), das besagt, daß diese Stelle des Bandes nicht beschrieben worden ist. Der Schreib/Lesekopf befindet sich an der durch den Pfeil gekennzeichneten Stelle des Bandes. Eine Turingmaschine arbeitet taktweise. In jedem Arbeitsschritt wird das Zeichen unter dem Schreiblesekopf gelesen. In Abhängigkeit vom Zustand der Steuereinheit und vom gelesenen Zeichen wird ein Zeichen geschrieben oder auch kein Zeichen geschrieben. Danach wird der Kopf um eine Stelle nach rechts oder um eine Stelle nach links geschoben oder er bleibt an der gleichen Stelle. Außerdem wird in Abhängigkeit vom alten Zustand und vom gelesenen Zeichen ein neuer Zustand der Steuereinheit eingestellt. Die Steuereinheit kann insgesamt nur endlich viele Zustände annehmen. Steuereinheit r r r ... ... ... ... ... ... ... .......................................................................................... ... ... ... .. ......... ........ ... 2 2 2 0 A / ♠ b 9 unendliches Band $ 2 2 r r r Abbildung 5.6: Turingmaschine Eine Turingmaschine beginnt stets mit einem ausgezeichneten Zustand, dem Startzustand, und einer ausgezeicheten Anfangsposition des Bandes. Wenn sie einen anderen ausgezeichneten Zustand erreicht, den Endzustand, hält sie. Man sagt, daß sie kreist, wenn sie den Endzustand nie erreicht. Wenn eine Turingmaschine hält, hat sie i.a. neue Zeichen auf das Band geschrieben, die Ausgabe, und dabei die anfangs auf dem Band stehendene Zeichen, die Eingabe, benutzt. Eine Turingmaschine ist also, ähnlich wie die Markov-Methoden, eine Maschine zur Umwandlung von Zeichenreihen, die manchmal nicht hält. Es gibt Turingmaschinen mit mehreren Bändern oder auch mit einseitig unendlichen Bän- 174 KAPITEL 5. ALGORITHMEN I: NAIV UND FORMALISIERT dern (Halbbänder). Sie sind jedoch alle gleichwertig in dem Sinne „Was die einen können, können die anderen auch“. Formale Sprachen und Automaten Wir haben formale Sprachen in Abschnitt 3.6 auf Seite 113 eingeführt. Es sind mathematisch definierte Sprachen. Sie sind in der Informatik von besonderer Wichtigkeit und ein Überlappungsgebiet zur Mathematik. Die wichtigsten Impulse stammen allerdings aus dem Gebiet der Linguistik, und zwar von Noam Chomsky22 . In der Informatik werden formale Sprachen nicht (oder kaum) zur Untersuchung natürlicher Sprachen benutzt. Bei der Definition von Programmiersprachen, also Sprachen zur Steuerung von Maschinen, dienen sie mehr der Festlegung der Gestalt, also der Syntax, der Sprache als deren Arbeitsweise, der Semantik. Ist A ein Alphabet und A∗ die Menge der Wörter über A, so ist jede Teilmenge S ⊆ A∗ eine formale Sprache über dem Alphabet A. Die Menge S := A∗ \ S ist die komplentäre Sprache (Komplement, complementary language) von S. A∗ ist abzählbar unendlich, d. h. die Menge der formalen Sprachen über A ist überabzählbar. Von Interesse sind dabei aber nur diejenigen, die durch eine endliche Beschreibung charakterisiert werden können und das sind abzählbar unendlich viele. Als besonders geeignet für die Charakterisierung haben sich formale Grammatiken (formal grammar) erwiesen. Eine (formale) Grammatik besteht aus endlich vielen Ersetzungsregeln der Art: Gehört ein Wort zur Sprache, dann darf man darin einige Teilzeichenreihen durch andere ersetzen und erhält wieder ein Wort der Sprache. Ausgehend vom leeren Wort erzeugt (generate) so eine Grammatik eine formale Sprache, wobei durchaus verschiedene Grammatiken die gleiche Sprache erzeugen können. Chomsky führte 4 Klassen von Grammatiken ein: Typ 0, Typ 1, Typ 2 und Typ 3. Diese bilden eine Hierarchie, d. h. eine Grammatik vom Typ i ist auch vom Typ i−1. Die von diesen Grammatiken erzeugten formalen Sprachen heißen wie jene, sie haben z. T. aber auch noch eigene Namen. Es ist nun aber nicht nur wichtig, formale Sprachen zu erzeugen, sondern auch sie zu erkennen (accept). Dafür werden spezielle Maschinen, oft Automaten genannt, verwendet. Als Eingabe erhält ein Automat die Grammatik und eine Zeichenreihe. Er soll feststellen, ob die Zeichenreihe zur Sprache gehört, die von der Grammatik erzeugt wird. Turingmaschinen sind Automaten, die Sprachen, die durch Grammatiken erzeugt wurden, akzeptieren. Das bedeutet: Zu jeder Sprache gibt es eine Turingmaschine, die bei Eingabe der Grammatik und einer Zeichenreihe der Sprache hält und das positive Ergebnis ausgibt. Wenn die Zeichenreihe nicht zur Sprache gehört, kann es sein, daß die Turingmaschine auch hält Chomsky, Avram Noam, ∗ 1928 Philadelphia, Pennsylvania. Amerikanischer Linguistiker. Professor am Massachusetts Institute of Technology. Legte mit den Grammatik-Typen 0 bis 4 die Grundlage der formalen Linguistik und schuf indirekt damit eine wesentliche Grundlage der theoretischen Informatik. Seit 1960 ist Chomsky auch als engagierter politischer Publizist tätig. Er ist einer der schärfsten Kritiker der amerikanischen Außenpolitk. Erhielt 2004 den Carl-von-Ossietzky-Preis der Stadt Oldenburg. 22 5.2. DER FORMALISIERTE ALGORITHMUSBEGRIFF 175 und das negative Ergebnis ausgibt. Für einige Sprachen kann es jedoch sein, daß es keine Turingmaschine gibt, die für jede Zeichenreihe des Komplements hält. In diesem Fall kann man nicht entscheiden, ob die Zeichenreihe zur Sprache gehört, denn man weiß ja nicht, ob die Maschine kreist oder mit ihren Berechnungen noch nicht fertig ist. Man hat nur eine „halbe Erkennung“. Man sagt, alle durch Grammatiken definierte Sprachen sind rekursiv aufzählbar (recursivily enumerable), aber nicht alle sind entscheidbar (decidable). Dieser Fall kann für Sprachen vom Typ 1, Typ2 oder Typ 3 nicht eintreten. Es gibt Turingmaschinen, die in jedem Fall halten. Man kann dann zur Erkennung der Sprache und auch zur Charkterisierung des Typs andere Automaten benutzen, z. B. Kellerautomaten für Sprachen vom Typ 2 und endliche Automaten für Sprachen vom Typ 3. Aufgaben Aufgabe 5.1 Es ist ein C-Programm zur Lösung des Rucksackproblems anzugeben. Aufgabe 5.2 Man untersuche die Funktionen: f (n) = if n = 0 then 1 else g(n) + f (n − 1)) g(n) = if (n = 1) ∨ (n = 0) then 0 else g(n − 1) + f (n − 2) Für welche n sind sie definiert? Geben Sie eine einfache Darstellung der Funktionen an. Aufgabe 5.3 Diese Aufgabe bezieht sich auf die Ackermannfunktion af, Beispiel 5.5, Seite 165. a. Man zeige af(1, y) = y + 2 für y ≥ 1. b. Geben Sie eine geschlossene Darstellung für af(2, y) für y ≥ 1 an. c. Besweisen Sie die Gleichungen 5.1 und 5.2. Literatur Allgemeines zum naiven Algorithmus-Begriff, wenn auch meist in recht knapper Darstellung23 , ist in vielen Lehrbüchern zu finden, z. B. in Knuth [Knut1997], Goos [Goos1997], Appelrath/Boles/Claus/Wegener [AppeBCW1998], Appelrath/Ludewig [AppeL1995], Goldschlager/Lister [GoldL1990], Cormen/Leiserson/Rivest [CormLR1990], Gumm/Sommer Man kann sogar der Meinung sein, daß die Frage „Was ist ein Algorithmus?“ eher philosophischer Art und eine präzise Antwort für die Untersuchung von Algorithmen und Datenstrukturen nicht nötig ist ([OttmW1996], Seite 1). 23 176 KAPITEL 5. ALGORITHMEN I: NAIV UND FORMALISIERT [GummS1998], Aho/Hopcroft/Ullman [AhoHU1983], Aho/Ullman [AhoU1995], Kowalk [Kowa1996], Horowitz/Sahni [HoroS1978], Kandzia/Langmaack [KandL1973], Saake/Sattler [SaakS2004]. Strukturiertes Programmieren und schrittweise Verfeinerung werden in Kowalk [Kowa1996], Goldschlager/Lister [GoldL1990] und Goos [Goos1996] behandelt. Eine ausführliche Darstellung strukturierter Programmierung aus Sicht der Softwaretechnik ist im ersten Band des zweibändigen Werkes von Balzert ([Balz1996] und [Balz1998]) zu finden. Entwurfstechniken für Algorithmen findet man bei Hromkovič [Hrom2001], bei Weiss [Weis1995], bei Saake/Sattler [SaakS2004] und bei Ottmann [Ottm1998]. Die Beispiele zu Algrithmen in applikativer Darstellung stammen aus dem Skript von Ehrich [Ehri1987]. Einiges zum formalisierten Algorithmusbegriff ist in allen Lehrbüchern, die in die Informatik einführen, zu finden, z. B bei Kowalk ([Kowa1996], S.58-66 oder Appelrath (Abschnitt 1.1 in [AppeL1995]). Ausführlichere Darstellungen bringen Goos (Abschnitt 1.6 in [Goos1997] und Kapitel 13 in [Goos1997a]) sowie Blum (Teil I in [Blum1998]. Die Darstellung in diesem Buch lehnt sich an Kandzia/Langmaack [KandL1973] an. Kapitel 6 Algorithmen II: Effizienz und Komplexität 6.1 Laufzeitanalyse von Algorithmen und Programmen In diesem Abschnitt werden Algorithmen im naiven Sinne betrachtet. 6.1.1 Vorbemerkungen Wann ist ein Algorithmus gut? Wann ist ein Programm gut? Dafür gibt es unterschiedliche Kriterien. In erster Linie muß ein Algorithmus (ein Programm) richtig sein, d.h. er muß die Aufgabe, für die er konstruiert ist, vollständig und fehlerfrei lösen. Es kann schwierig sein, das nachzuweisen. Von einem Programm wird man zusätzlich verlangen, daß es einfach zu bedienen, also benutzerfreundlich ist. Des weiteren soll ein Algorithmus einfach, d.h. leicht verständlich und leicht zu implementieren sein. Realisiert man einen Algorithmus als Programm, so soll dieses sauber strukturiert, klar gegliedert und gut dokumentiert sein. Um das zu erreichen, haben sich Dokumentationsregeln als sehr zweckmäßig erwiesen. Schließlich sollte ein Algorithmus (ein Programm) auch effizient sein. Allgemein bedeutet Effizienz gute, d.h. möglichst geringe Nutzung von Betriebsmitteln. Die wichtigsten Meßgrößen hierfür sind Prozessorzeit, Speicherbedarf, Netzbelastung und Ein-/Ausgabeaktivitäten. Wie schon in Unterabschnitt 1.1.3, Seite 10, erläutert, wollen wir bei der Festlegung und Untersuchung der Effizienz eines Algorithmus oder Programms die Laufzeit (Geschwindigkeit) zu Grunde legen und die anderen Faktoren nicht berücksichtigen. Da die reale Laufzeit natürlich auch von der Leistungsfähigkeit des benutzten Rechensystems abhängt und man von dieser abstrahieren will, wird als Maß die „Anzahl Operationen“ in Abhängigkeit vom „Umfang der Eingabe“ genommen. Was eine „Operation“ ist und was „Umfang der Eingabe“ bedeutet, ist nicht einheitlich definiert, sondern hängt vom 177 178 KAPITEL 6. ALGORITHMEN II: EFFIZIENZ UND KOMPLEXITÄT betrachteten Problem ab. Beim euklidischen Algorithmus (Abschnitt 1.1) war eine Operation (im wesentlichen) eine ganzzahlige Division mit Rest und als Umfang der Eingabe wurde die Größe der beiden Zahlen, für die der größte gemeinsame Teiler zu bestimmen war, genommen. Beim Sortieren durch Einfügen (Abschnitt 1.2) bestand eine Operation im Vergleichen und Vertauschen von zwei Sortierwerten und der Umfang der Eingabe war die Zahl der zu sortierenden Elemente. Im folgenden wird zunächst ein Beispiel in Unterabschnitt 6.1.2 vorgestellt und dann werden in weiteren Unterabschnitten mathematische Methoden zur Analyse der Laufzeit von Algorithmen behandelt. 6.1.2 Beispiel: Mischsortieren Wir wollen die Untersuchungen der Laufzeit von Algorithmen mit einem ausführlichen Beispiel, dem Mischsortieren, beginnen. Lösungsansatz und Algorithmen Sortieren wurde in Abschnitt 1.2, Seite 18, eingeführt. Dort wurde auch ein erstes Sortierverfahren, Sortieren durch Einfügen, untersucht. In diesem Unterabschnitt soll ein weiteres Sortierverfahren, Mischsortieren (merge sort), vorgestellt werden. Das Verfahren geht auf John von Neumann1 zurück. Die grundlegende Idee des Verfahrens ist einfach und in Abbildung 6.1 dargestellt. Die zu sortierenden Werte sind in einer Eingabeliste2 gegeben. Diese Liste wird in zwei gleich große Teillisten aufgeteilt. Jede Teilliste wird für sich sortiert und die sortierten Teillisten werden zu einer sortierten Ergebnisliste zusammengefügt, gemischt 3. Damit ist das Problem des Sortierens zunächst nur von der der ursprünglichen Liste auf die beiden Teillisten verlagert worden. Um diese zu sortieren, geht man rekursiv vor: Jede der Teillisten wird ihrerseits in Teillisten zerlegt usw. Zum Schluß erhält man Listen, die nur aus einem Elementen bestehen und demnach sortiert sind. In einem zweiten Schritt werden dann auf jeder Rekursionsstufe zwei sortierte Teillisten zu einer sortierten Gesamtliste gemischt. Tabelle 6.1 zeigt die Prozedur MS für das Mischsortieren in Pseudocode. Die Prozedur ruft sich selber rekursiv auf und benutzt außerdem die Prozeduren SPLIT und MERGE. Auf diese wird weiter unten bei der Diskussion des Programms MISCHSORT (Seite 180) eingegangen. Zuvor soll ein Beispiel behandelt werden. Siehe Fußnote Seite 138. Listen werden in Kapitel 8 ausführlich behandelt. 3 Das englische Verb to merge heißt verschmelzen. In der EDV-Fachsprache hat sich jedoch der Ausdruck mischen eingebürgert. 1 2 6.1. LAUFZEITANALYSE VON ALGORITHMEN UND PROGRAMMEN Unsortierte Eingabeliste Sortierte Teilliste 1 Sortierte Teilliste 2 ' & ........... ........... ........... ........... ........... ........... ........... ........... ........... ..... ............ .......... ................ ................. . . . . . . . . . ... ........... ........... . . . . . . . . . .... ........... ........... ........... ........... ................. .. . ................ .......... ........... ........... . . . . . . . . . . ... ........... ........... ........... ........... ........... ........... ........... ........... ........... ........... ........... ........... ........... .... .............. ........ . Unsortierte Teilliste 1 Unsortierte Teilliste 2 Sortierte Ergebnisliste Abbildung 6.1: Grundidee des Mischsortierens Prozedur MS (liste L) // Merge Sort Die Werte in der Liste L werden durch Mischen in aufsteigender Reihenfolge sortiert. 1 2 3 4 5 6 179 if (L besteht aus weniger als 2 Elementen) return L else { SP LIT (L1, L2, L); return MERGE(MS(L1), MS(L2)); }; Tabelle 6.1: Prozedur Mischsortieren $ % Beispiel 6.1 Es soll die Liste 10,8,1,4,7,7,2,2,7 durch Mischsortieren sortiert werden. Abbildung 6.2 veranschaulicht die Abläufe mit Hilfe eines Binärbaumes4 . Die Knoten des Baumes entsprechen den rekursiven Aufrufen von MS . Links neben jedem Knoten ist die Liste angegeben, mit der die Prozedur aufgerufen wird. Rechts (kursiv) steht die sortierte Liste, die zurückgeliefert wird. Beim Aufteilen einer Liste werden ihre Elemente abwechselnd auf die beiden Unterlisten verteilt. Dadurch kann die erste Unterliste höchstens 1 Element mehr als die zweite Unterliste enthalten. Beim Mischen werden die beiden sortierten Unterlisten zu einer insgesamt sortierten Liste verschmolzen. 2 4 Zu Binärbäumen siehe Unterabschnitt 9.1.2. 180 KAPITEL 6. ALGORITHMEN II: EFFIZIENZ UND KOMPLEXITÄT 10 8 1 4 7 7 2 2 ......7.....................u.............1............ 2 2 4 7 7 7 8 10 . . .. ....... ....... ........ ....... . . . . . . . . ........ ....... ........ ....... ........ . . . . . . . . ........ ....... ......... ..... ......... ..... ..... . . . . ..... .... ..... ..... ..... ..... ..... ..... ..... ..... ..... . . . . ..... ... . . . ..... . ... . ..... . . . ..... ... . . . ..... . .... . ..... . . . ..... .. . . . . ... ....... . ... .. . .. .... .. ... . . ... ... ..... .. . . . . . ... ... . .. . . . . . ... ... . ... ... ... ... ... .. ... ... ... .. ... ... . . . . . ... ... .. .. . . . . . ... ... . .. . . . . . ... ... .. . . . ... . . . ... . .. ... . . . . . . . .... . . ... ..... . ... .. . ... . ... ... ... ... ... ... ... .. . ... . . . ... ... ... . ... .. . .. ... 10 1 7 2 7 u 1 2 7 7 10 10 7 7 u 7 7 10 10 7 u 7 10 7 u7 1 2 u1 2 1 u1 2 u2 ........ ....... ........ ........ ....... ........ ........ ........ ....... ........ ........ ....... ........ ....... .. .... ...... .... .... . . .... ... . . .... . .... .... ... .... . . . .... ... . . .... ... . .... . .. . .... . . .. .... . . . ... .. . . .... . .... ..... . . .. .... . ... .. . .. . .. ... . . ... .. ..... ... . . . . ... ... . .. . . . . ... ... ... ... ... ... .. .. ... ... .. .. ... . . . . . . ... ... .. ... . . . . ... ... . .. . . . ... ... . .. . ... . . . ... . . ... . . . . . . . 8 4 7 2 u2 4 7 8 8 7 u7 8 8 u8 7 u7 4 2 u2 4 4 u4 2 u2 10 u 10 7 u 7 Abbildung 6.2: Ein Beispiel für Mischsortieren Im folgenden wird ein Programm für das Mischsortieren angegeben und diskutiert. Das Programm MISCHSORT In den Tabellen 6.2, 6.3, 6.4, 6.5 6.6 ist das Programm MISCHSORT wiedergegeben. Teil I des Programms (Tabelle 6.2) enthält die globalen Deklarationen: Strukturen, Routinen, Variable. Die Listen des Sortierverfahrens werden als verkettete Listen (siehe 8.3.1) realisiert. Der Satztyp liste der Listenelemente wird bei den Strukturen eingeführt. Die Sätze dieses Typ bestehen aus einem Feld für den Sortierwert und einem Feld für den Verweis auf den Nachfolger in der Liste. Als Datentyp wurden für den Sortierwert ganze Zahlen gewählt. Es hätte aber auch ein anderer Datentyp festgelegt werden können, z. B. Zeichenreihen. Teil II (Tabelle 6.3) des Programms ist das Hauptprogramm. Es liest die zu sortierenden Zahlen ein, wobei die Hilfsroutine leinfügen benutzt wird, und baut damit eine verkettete Liste auf. Mit dieser Liste als Eingabe wird mittels der Routine misort das eigentliche Sortieren aufgerufen. Die sortierte Liste wird schließlich mit der Routine ausliste ausgegeben. Tabelle 6.4 zeigt die Routinen misort und split. Die Routine misort ist eine unmittelbare Übertragung des Pseudocodes der Prozedur MS (Tabelle 6.1) in C. Die Routine split realisiert in C die Prozedur SPLIT von Tabelle 6.1 und verteilt die Elemente der aufzuteilenden Liste lein beginnend beim ersten Element abwechselnd auf die Unterlisten la und lb. Wenn ein Element der aufzuteilenden Liste in eine Teilliste eingefügt wurde, ist die verbleibende Restliste so aufzuteilen, daß ihr erstes Element an die andere Teilliste 6.1. LAUFZEITANALYSE VON ALGORITHMEN UND PROGRAMMEN 181 angehängt wird. Dies nutzt split für einen rekursiven Aufruf aus und ist so sehr einfach zu programmieren. Die Prozedur MISCH von Tabelle 6.1 wird durch die Routine misch in C realisiert. Diese ist in Tabelle 6.5 wiedergegeben. misch verschmilzt („mischt“) die sortierten Listen ls1 und ls2 zu einer sortierten Gesamtliste. Ist das Anfangselement von ls1 nicht kleiner als das Anfangselement von ls2, so wird es in die Gesamtliste übertragen. Andernfalls wird dafür das erste Element von ls2 genommen. Hat man ein Element aus einer Eingabeliste festgelegt, um es in die Ergebnisliste zu übertragen, so kann man die verbleibende Arbeit folgendermaßen erledigen: Man sortiert die beiden Restlisten durch Mischen und die sortierte (Rest-)Gesamtliste wird an das zu übertragende Element angehängt. Diese Tatsache erlaubt auch für misch eine sehr einfache rekursive Programmierung. Die Arbeitsweise von misch wird in Beispiel 6.2, das die einzelnen Rekursionsschritte zeigt, sichtbar. Tabelle 6.6 zeigt die Routinen leinfügen und ausliste. Die Routine leinfügen wird beim Einlesen der zu sortierenden Werte benutzt, um neue Listenelemente (Sätze) anzulegen. Die Routine ausliste gibt die sortierte Ergebnisliste aus. Beispiel 6.2 Es sollen die Listen ‹1, 2, 6, 7, 9, 9 › und ‹2.4.7.8 › gemischt werden. Die Symbole ‹ und › stellen die Listenbegrenzungen dar. ‹misch(‹1, 2, 6, 7, 9, 9 ›, ‹2, 4, 7, 8 ›) › = = = = = = = = = ‹1, misch(‹2, 6, 7, 9, 9 ›, ‹2, 4, 7, 8 ›) › ‹1, 2, misch(‹6, 7, 9, 9 ›, ‹2, 4, 7, 8 ›) › ‹1, 2, 2, misch(‹6, 7, 9, 9 ›, ‹4, 7, 8 ›) › ‹1, 2, 2, 4, misch(‹6, 7, 9, 9 ›, ‹7, 8 ›) › ‹1, 2, 2, 4, 6, misch(‹7, 9, 9 ›, ‹7, 8 ›) › ‹1, 2, 2, 4, 6, 7, misch(‹9, 9 ›, ‹7, 8 ›) › ‹1, 2, 2, 4, 6, 7, 7, misch(‹9, 9 ›, ‹8 ›) › ‹1, 2, 2, 4, 6, 7, 7, 8, misch(‹9, 9 ›, ‹ ›) › ‹1, 2, 2, 4, 6, 7, 7, 8, 9, 9 › 2 182 KAPITEL 6. ALGORITHMEN II: EFFIZIENZ UND KOMPLEXITÄT ' $ #define TRUE 1 #define FALSE 0 typedef int boolean; /****************************************************************/ /* Strukturen */ /****************************************************************/ struct lst /* Listenenlement */ { int sortwert; /* Sortierwert */ struct lst *nachfolger; /* Zeiger auf Nachf. */ }; typedef struct lst liste; /****************************************************************/ /* Routinen */ /****************************************************************/ liste *misort(liste *lein); liste *misch(liste *ls1, liste *ls2); liste *leinfuegen(void); void split(liste **la, liste **lb, liste *lein); void ausliste(liste *lanfang); /****************************************************************/ /* Globale Variablen und Konstanten */ /****************************************************************/ liste *elist; /* Liste fuer Eingabe und */ /* Endergebnis */ /****************************************************************/ & % /****************************************************************/ /* Programm MISCHSORT. */ /* */ /* Liest natuerliche Zahlen ein. Die erste eingelesene */ /* negative ganze Zahl beendet die Eingabe. */ /* Die eingelesenen natuerlichen Zahlen werden in einer */ /* verketteten Liste abgelegt und diese Liste nach dem */ /* Verfahren "Mischsortieren" sortiert. */ /****************************************************************/ #include <stdio.h> #include <malloc.h> Tabelle 6.2: Programm MISCHSORT (Teil I) 6.1. LAUFZEITANALYSE VON ALGORITHMEN UND PROGRAMMEN ' /***************************************************************/ /* Hauptprogramm */ /* */ /* Liest die zu sortierenden Zahlen ein und baut damit */ /* eine verkettete Liste auf. */ /* Die erste negative Zahl beendet die Eingabe. */ /***************************************************************/ main() { liste *listaktuell, *li; /* Liste mit Eingabewerten anlegen */ 183 $ elist = NULL; listaktuell = leinfuegen(); if (listaktuell->sortwert < 0) exit(0); elist = listaktuell; li = leinfuegen(); while (li->sortwert >= 0) { listaktuell->nachfolger = li; listaktuell = li; li = leinfuegen(); }; /* /* printf("\nEingabelisteA\n");*/ ausliste(elist); */ elist = misort(elist); /* } & printf("\nSortierte Ausgabeliste\n");*/ ausliste(elist); Tabelle 6.3: Programm MISCHSORT (Teil II) % 184 ' KAPITEL 6. ALGORITHMEN II: EFFIZIENZ UND KOMPLEXITÄT /****************************************************************/ /* Routine misort */ /* */ /* Erhaelt die zu sortierende Liste als Eingabe und liefert */ /* sie sortiert zurueck. Dazu wird die Eingabeliste in zwei */ /* gleich grosse Listen aufgeteilt (split), diese durch */ /* rekursiven Aufruf von misort sortiert, und die sortierten */ /* Teillisten zu einer sortierten Gesamtliste gemischt */ /* (misch). */ /****************************************************************/ liste *misort(liste *lein) { liste *la, *lb; $ if (lein == NULL) return lein; if (lein->nachfolger == NULL) return lein; split(&la, &lb, lein); /*printf("split: liste a\n"); ausliste(la);*/ /*printf("split: liste b\n"); ausliste(lb);*/ return misch(misort(la), misort(lb)); } /****************************************************************/ /* Routine split */ /* */ /* Teilt eine Eingabeliste in zwei gleich grosse Teillisten */ /* auf, indem das naechste Element der Ausgangsliste */ /* abwechselnd in die erste und die zweite Teilliste einge*/ /* fuegt wird. */ /****************************************************************/ void split(liste **la, liste **lb, liste *lein) { liste *listak; liste **la1, **lb1; int i; if (lein == NULL) { *la = NULL; *lb = NULL; } else { *la = lein; split(lb, &(lein->nachfolger), lein->nachfolger); }; } & % 6.1. LAUFZEITANALYSE VON ALGORITHMEN UND PROGRAMMEN ' /**************************************************************/ /* Routine misch */ /* */ /* Mischt rekursiv zwei vorsortierte Listen zu einer */ /* Ergebnisliste. */ /**************************************************************/ liste *misch(liste *ls1, liste *ls2) { if (ls1 == NULL) return ls2; if (ls2 == NULL) return ls1; if (ls1->sortwert <= ls2->sortwert) { ls1->nachfolger = misch(ls1->nachfolger, ls2); return ls1; } else { ls2->nachfolger = misch(ls1, ls2->nachfolger); return ls2; }; } & Tabelle 6.5: Programm MISCHSORT (Teil IV) 185 $ % 186 ' KAPITEL 6. ALGORITHMEN II: EFFIZIENZ UND KOMPLEXITÄT $ /**************************************************************/ /* Routine leinfuegen */ /* */ /* Fordert Speicherplatz fuer neues Listenelement, setzt */ /* Werte ein und liefert es zurueck. */ /**************************************************************/ liste *leinfuegen(void) { int i; liste *la; la = (liste *)malloc(sizeof(liste)); if (la == NULL) { printf("MISCHSORT Einlesen Daten: Nicht genug Speicher vorhanden\n"); exit(0); }; scanf("%d", &(la->sortwert)); la->nachfolger = NULL; return la; } /**************************************************************/ /* Routine ausliste */ /* */ /* Gibt die verkette Liste, deren Anfangselement uebergeben */ /* wurde, aus. */ /**************************************************************/ void ausliste(liste *lanfang) { liste *laus; laus = lanfang; while (laus != NULL) { printf("%3d\n", laus->sortwert); laus = laus->nachfolger; } printf("\n\n"); } & Tabelle 6.6: Programm MISCHSORT (Teil V) % 6.1. LAUFZEITANALYSE VON ALGORITHMEN UND PROGRAMMEN 187 Effizienz des Mischsortierens Wir wollen den Aufwand („Anzahl Operationen“) des Mischsortierens für n zu sortierenden Werte ermitteln. Dazu wird zunächst der Spezialfall n = 2k (k ≥ 1) betrachtet. In diesem Fall führt das Aufteilen der Listen stets zu zwei gleichlangen Unterlisten. Der Aufwand für das Sortieren einer Liste von n Elementen ergibt sich aus den folgenden einzelnen Schritten: • Aufteilen der Liste in zwei Unterlisten der Länge n2 . • Mischsortieren jede der beiden Unterlisten. • Mischen der sortierten Unterlisten. Aufteilen: Die Routine split (Tabelle 6.4) wird für jedes Element der Liste einmal aufgerufen und dann noch ein letztes mal mit einem NULL-Adreßwert. Bei den ersten n Aufrufen ist jeweils ein konstanter Aufwand für das Einketten in die Teilliste und den nächsten Aufruf von split zu leisten. Dieser Aufwand soll a (a > 0) genannt werden. Ist b (b > 0) der Aufwand für den letzten Aufruf, so ist der Aufwand für das Aufteilen a · n + b. Dieser Aufwand hängt nur von der Anzahl n der Sortierwerte ab, aber nicht von deren Anordnung. Er ist also im besten wie auch im schlechtesten Fall der gleiche. Mischsortieren der Unterlisten: Im besten Fall: n und im schlechtesten Fall: 2 · WOC( ) . 2 n 2 · BEC( ) 2 Mischen: Der beste Fall ergibt sich, wenn in der Routine misch (Tabelle 6.5) zunächst hintereinander alle Elemente der sortierten Unterliste ls1 in das Ergebnis übertragen und dann die sortierte Unterliste ls2 an das Ergebnis angehängt wird. Das ergibt n2 Aufrufe von misch mit einem Vergleich von zwei Sortierwerten und einen Aufruf ohne einen solchen Vergleich. Der Aufwand ist demnach c· n +d 2 mit positiven Werten c und d. Im schlechtesten Fall werden die Elemente der Listen ls1 und ls2 abwechselnd in das Ergebnis übertragen. Das ergibt n − 1 Aufrufe von misch mit einem Vergleich von zwei Sortierwerten und einen Aufruf ohne einen solchen Vergleich, also den Aufwand c · (n − 1) + d . 188 KAPITEL 6. ALGORITHMEN II: EFFIZIENZ UND KOMPLEXITÄT Lösen der Rekursionsgleichungen: Aus den obigen Betrachtungen ergibt sich n n BEC(n) = a · n + b + 2 · BEC( ) + c · + d. Stellt man für den schlechtesten Fall die 2 2 entsprechende Gleichung auf und faßt man zusammen, so erhält man c n BEC(n) = (b + d) + (a + ) · n + 2 · BEC( ) 2 2 n WOC(n) = (b − c + d) + (a + c) · n + 2 · WOC( ) . 2 (6.1) (6.2) Zum Lösen dieser Rekursionsgleichungen werden Anfangswerte gebraucht. Die Routine misort (Tabelle 6.4) führt für eine einelementige Liste nur zwei Tests durch, also BEC(1) = WOC(1) = f mit f > 0. Wir wollen nun die Rekursionsgleichungen 6.1 und 6.2 lösen. Sie sind vom Typ T (1) = A n und T (n) = B + C · n + 2 · T ( ) für n = 2, 4, 8, · · ·. Dazu werden wir für einige kleine 2 Werte von n die Gleichung ausrechnen, dann eine Lösung raten und schließlich beweisen, daß die geratene Lösung richtig ist. T (1) T (2) T (4) T (8) T (16) .... = = = = = A B + C · 2 + 2 · T (1) B + C · 4 + 2 · T (2) B + C · 8 + 2 · T (4) B + C · 16 + 2 · T (8) .... = = = = 2·A+1·B+2·1·C 4·A+3·B+4·2·C 8·A+7·B+8·3·C 16 · A + 15 · B + 16 · 4 · C .... Das legt folgende Vermutung nahe: T (n) = A · n + B · (n − 1) + C · n · log2 (n) für n = 2k und k ≥ 0 Beweis der Richtigkeit durch vollständige Induktion: n = 1: T(1) = A + 0 + 0 = A. Sei T (n) = A · n + B · (n − 1) + C · n · log2 (n) für n = 2k : Dann ist T (2k+1 ) = T (2n) = = = = = B + C · (2n) + 2 · T (n) B + C · (2n) + 2 · (A · n + B · (n − 1) + C · n · log2 (n)) A · (2n) + B · (2n − 1) + C · (2n) + C · (2n) · log2 (n) A · (2n) + B · (2n − 1) + C · (2n) · (1 + log2 (n)) A · (2n) + B · (2n − 1) + C · (2n) · log2 (2n) Das ergibt für die Gleichungen 6.1 und 6.2 die geschlossenen Formen c BEC(n) = f · n + (b + d) · (n − 1) + (a + ) · n · log2 (n) 2 WOC(n) = f · n + (b − c + d) · (n − 1) + (a + c) · n · log2 (n) (6.3) (6.4) 6.1. LAUFZEITANALYSE VON ALGORITHMEN UND PROGRAMMEN 189 Ist sn eine Folge von n Sortierwerten und n eine Zweierpotenz, so gilt für den Sortieraufwand R(sn ) mittels Mischsortieren BEC(n) ≤ R(sn ) ≤ WOC(n) (6.5) Es erscheint plausibel, daß die gefundenen Abschätzungen auch zur Aufwandsbestimmung von Sortierfolgen, deren Länge keine Zweierpotenz ist, benutzt werden können. In Unterabschnitt 6.1.5, Seite 198, wird das näher ausgeführt. 6.1.3 Boden und Decke In diesem Unterabschnitt und den folgenden werden einige Definitionen und Ergebnisse vorgestellt, die eigentlich zur Mathematik gehören. Wegen ihrer Bedeutung für die Analyse von Algorithmen und in anderen Teilen der Informatik sollen sie jedoch hier behandelt werden. Definition 6.1 Es seien x, y ∈ R. 1. Der Boden (floor) von x ist die größte ganze Zahl, die kleiner oder gleich x ist. Symbolisch: bxc 2. Die Decke (ceiling) von x ist die kleinste ganze Zahl, die größer oder gleich x ist. Symbolisch: dxe In der Schreibweise [x] wurde die Funktion Boden schon lange in der Mathematik benutzt. Die Schreibweisen bxc und dxe wurden 1962 von Iverson5 eingeführt und haben sich inzwischen durchgesetzt. Wichtige Eigenschaften der Funktionen Boden und Decke, die sich unmittelbar aus der Definition ergeben, sind: x − 1 < bxc ≤ x ≤ dxe < x + 1 b−xc = −dxe bxc = dxe genau dann, wenn x ∈ Z bxc + 1 = dxe genau dann, wenn x ∈ /Z (6.6) (6.7) (6.8) (6.9) Außerdem ist für x ≥ 0 ist bxc der ganzzahlige Anteil und x − bxc der nichtganzzahlige Anteil von x. Iverson, Kenneth E., ∗1920, Camrose, Alberta, Canada, †2004 Toronto, Ontario, Canada. Kanadischer Mathematiker und Informatiker. Entwickelte bei IBM die Programmiersprache APL [Iver1962] sowie einen zugehörigen Interpreter. APL benutzt eine mathematische Notation und ist kompakt, flexible und sehr leistungsfähig. 5 190 KAPITEL 6. ALGORITHMEN II: EFFIZIENZ UND KOMPLEXITÄT Als Beispiel für die Anwendung von Boden und Decke wollen wir noch einmal den Aufwand für das Mischsortieren berechnen, und zwar dieses Mal für Sortierfolgen beliebiger Länge. Allerdings soll die Fragestellung auch etwas vereinfacht werden: Wir wollen für die Aufwandsbestimmung nur die Anzahl der Vergleichsoperationen heranziehen. Es bezeichne WOC(n) die maximale Zahl von Vergleichsoperationen, die sich bei der Mischsortierung einer Eingabefolge der Länge n (n beliebig!) ergeben kann. Es gilt WOC(1) = 0 WOC(n) = WOC(d n2 e + WOC(b n2 c + n − 1 für n ≥ 1 (6.10) In der Tat: Es ist stets n = d n2 e + b n2 c und diese Gleichung entspricht dem Aufteilen einer Liste der Länge n. Beim Mischen der beiden Teillisten werden höchstens n − 1 Vergleiche ausgeführt. Wir wollen für WOC eine geschlossene Darstellung finden. Dazu betrachten wir als Beispiel Abbildung 6.3. Sie zeigt eine Eingabe von 9 Sortierwerten nach der Aufteilung in a1 a2 a3 a4 a5 a6 a7 a8 a......9................u.................... ..... ........ ....... ........ . . . . . . . . ........ ....... ....... ........ ....... . . . . . . .. ........ ....... ........ 1 3 5 7 9........................... ..... ... ..... ..... ..... ..... ..... ..... ..... ..... . . . ..... . ... ..... . . . . ..... ... . . . ..... . ... . ..... . . . ..... ... . . . . ..... ... . . ..... . . ... ...... . . 1 5 9 ...... ...... 3 7 ............. . . . . ... .. ..... .. . . . . . . . ... ... .. ... ... ... ... ... ... ... ... ... .. ... ... ... .. ... ... . . . . . ... ... . .. . . . . . ... ... .. .. . . . ... . . . ... .. . . . ... . . . ... . .. .. . . . . . 1 9 ............ 5 . 3 7 . . . ... .. . ... .. ... .. ... ... ... ... ... .. . ... .. . ... . . ... . .. ... . . .. ... a a a a a u a a a u a a u a1 u a a u a u a u a u ........ ........ ........ ....... ........ ........ ........ ....... ........ ........ ....... ........ ........ ...... 2 4 6 8..................... ... ... . . .... .... .... .... .... .... .... . . . .... .. . . . .... .. . .... . . .... ... . . .... .. . . . .... .. . . .... . ...... . 2 6 .... .... 4 8 ............. . ... . .. . . ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... .. .. ... . . ... ... .. .. . . ... . . ... . . . . ... ... .. ... . ... . . ... . .. . ... . . . ... ... ... . ... a a a a u a a u a2 u a6 u a a u a4 u a8 u a9 u Abbildung 6.3: Anzahl Vergleichsoperationen beim Mischsortieren Listen. Vergleiche auch Abbildung 6.2. Diese Listen sollen gemischt werden. Der Anschaulichkeit halber wollen wir uns vorstellen, daß bei jedem Vergleich einer der Sortierwerte einen Chip bezahlen muß. Wir wollen anfangs jeden Sortierwert mit so vielen Chips ausstatten, daß genau die beim Mischen notwendigen Vergleiche bezahlt werden können. Jeder Sortierwert, d. h. jedes Blatt des Baumes, durchläuft maximal so viele Vergleiche, wie es Knoten im Baum über ihm gibt. So viele Chips soll jedes Blatt zunächst auch 6.1. LAUFZEITANALYSE VON ALGORITHMEN UND PROGRAMMEN 191 bekommen. Wir statten also anfangs a1 und a9 mit je 4 Chips und alle übrigen Blätter mit je 3 Chips aus. Das Mischen und Bezahlen soll nun folgendermaßen ablaufen: 1. Mischstufe (von unten): a1 und a9 werden gemischt, a1 bezahlt. a9 durchläuft den Mischknoten, ohne zu bezahlen. Das heißt a9 bekommt anfangs nur 3 Chips. 2. Mischstufe: a1 , a9 , a3 , a2 , a4 bezahlen je einen Chip. a5 , a7 , a6 und a8 bezahlen nichts und bekommen jeweils eine um 1 verringerte Anfangsausstattung. 3. Mischstufe: a1 , a3 , a5 , a9 und a2 , a4 , a6 bezahlen je einen Chip, während für a7 und a8 die Anfangsausstattung um 1 vermindert wird. 4. Mischstufe: Schließlich bezahlen beim letzten Mischvorgang a1 , a2 , a3 , a4 , a5 , a6 , a7 , a9 und für a8 wird die Anfangsausstattung noch einmal um 1 herabgesetzt und beträgt dann 0. Das ergibt in aufsteigender Reihenfolge der endgültigen Anfangsausstattung Position Sortierwert Anfangsausstattung 1 a8 0 2 a7 1 3 a6 2 4 a5 2 5 a4 3 6 a2 3 7 a3 3 8 a9 3 9 a1 4 Mit ein wenig Übung erkennt man in der Gesamtanfangsausstattung die Formel 0+1+2+2+3+3+3+3+4 = 9 X k=1 dlog2 ke Das legt die Vermutung nahe, daß die Funktion f (n) := n X k=1 dlog2 ke (6.11) für n ≥ 1 die Lösung der Rekursionbedingungen 6.10 ist. Das ist in der Tat so. Wir wollen nun 1. beweisen, daß die Funktion den Rekursionsbedingungen 6.10 genügt, und 2. eine geschlossene Darstellung für die Summe herleiten. 1. Die durch Gleichung 6.11 gegebene Funktion löst die Rekursionsgleichungen 6.10. Es ist f (1) = 0. Es sein n > 1. Wir fassen in der Summe die ungeraden und die geraden Indizes getrennt zusammen und beachten, daß es stets d n2 e Summanden mit ungeradem Index und b n2 c Summanden mit geradem Index gibt. Außerdem benutzen wir die Beziehungen dlog2 (2j)e = dlog2 j + 1e = dlog2 je + 1 sowie dlog2 (2j − 1)e = dlog2 je für j = 1 und dlog2 (2j − 1)e = dlog2 (2j)e = dlog2 je + 1 für j ≥ 2. Das ergibt 192 KAPITEL 6. ALGORITHMEN II: EFFIZIENZ UND KOMPLEXITÄT n X f (n) = k=1 dlog2 ke n n d2e X = j=1 dlog2 (2j − 1)e + b2c X j=1 dlog2 (2j)e n n b2c d2e lnm jnk X X − 1 + dlog2 je + = dlog2 je + 2 2 j=1 j=1 = f (d n2 e) + f (b n2 c) + n − 1 2. Eine geschlossen Darstellung für die Summe 6.11. Um eine geschlossene Darstellung für die Summe 6.11 zu bekommen, verlängern wir sie, so daß die Anzahl Summanden eine Zweierpotenz wird. Mit m := dlog2 ne ergibt sich m m f (n) + (2 − n) · m = 2 X k=1 dlog2 ke = 0 + 1 + 2 + 2+ 3 + 3 + 3 + 3 + ...+ m = 20 · 1 + 21 · 2 + 22 · 3 + . . . + 2m−1 · m m X j · 2j−1 = j=1 m = 2 · (m − 1) + 1 Das ergibt schließlich f (n) = m · n − 2m + 1 = n · dlog2 ne − 2dlog2 ne + 1 (6.12) Für n, die Zweierpotenzen sind, vereinfacht sich das zu f (n) = n · log2 n − n + 1. Anmerkung 6.1 Es sei hier noch ein nützlicher Kunstgriff angegeben, mit dem Summen m P wie j · 2j−1 ausgerechnet werden können. Für x ∈ R, 0 < x, x 6= 1 gilt j=1 m X j=1 j−1 j·x m m X d X j d j x x i= = dx dx j=1 j=1 ! m m d X j d X j = x −1 = x dx j=0 dx j=0 6.1. LAUFZEITANALYSE VON ALGORITHMEN UND PROGRAMMEN 193 d xm+1 − 1 = dx x−1 (m + 1) · xm · (x − 1) − (xm+1 − 1) = (x − 1)2 Für x = 2 ergibt sich 2m · (m − 1) + 1. 6.1.4 2 Modulo Ist man nur an der Laufzeitanalyse von Algorithmen und Programmen interessiert, kann dieser Unterabschnitt übersprungen werden. Beim euklidischen Algorithmus, Abschnitt 1.1, Seite 3, haben wir die ganzzahlige Division m = n·q+r mit Rest benutzt. Dabei sind n und m positive ganze Zahlen, q ist der Quotient und r mit 0 ≤ r < n ist der Rest. Beachtet man, daß q = b m c und bezeichnet man den n Rest mit mod, so hat man in einer anderen Schreibweise jmk m=n· + m mod n. n Das zeigt, wie man eine allgemeine Restfunktion zu beliebigen reellen Zahlen x und y einführen kann. Definition 6.2 Die modulo-Operation von x bezüglich y ist definiert durch x für y 6= 0 x−y x mod y := y x für y = 0 (6.13) Die Bezeichnungen sind uneinheitlich. Die Operation heißt auch „Modulus“. Gelegentlich wird nicht nur die Operation, sondern auch das Ergebnis so bezeichnet. Manchmal wird auch y Modulus genannt. Im Englischen wird oft von der „mod operation“ gesprochen. Sowohl im Deutschen als auch im Englischen wird häufig die Ablativform „modulo“ benutzt. Auch wir wollen die Operation so nennen. Einige Beispiele sind angebracht. 7 mod 4 7 mod (−4) (−7) mod 4 (−7) mod (−4) = = = = 7 − 4 · b7/4c = 7 − 4 · 1 7 − (−4) · b7/(−4)c = 7 − (−4) · (−2) (−7) − 4 · b(−7)/4c = (−7) − 4 · (−2) (−7) − (−4) · b(−7)/(−4)c = (−7) − (−4) · 1 Ein weiteres Beispiel 18, 5 mod π = 18, 5 − π · b18, 5/πc = 18, 5 − π · 5 = 2, 792 = = = = 3 −1 1 −3 194 KAPITEL 6. ALGORITHMEN II: EFFIZIENZ UND KOMPLEXITÄT Die modulo-Funktion erfüllt eine wichtige Eigenschaft der Restbildung: Für y 6= 0 gilt 0 ≤ x/y − bx/yc < 1. Für positives y bedeutet das 0 ≤ x − y · bx/yc < y und für negatives y ergibt sich 0 ≥ x − y · bx/yc > y. Zusammenfassend 0 ≤ |x mod y| < |y|. (6.14) Vergleiche hierzu auch die Ausführungen zur ganzzahligen Division in C auf Seite 47. C läßt die durch mod gegebene Quotient- und Restbildung zu (6.13 und 6.14). Die für den Rechner donar gewählte Realisierung entspricht jedoch nicht der hier gegebenen Definition c definiert, so gilt z. B. von mod. Wird nämlich der ganzzahlige Quotient durch m/n := b m n |(−7/4) · 4| = | − 2 · 4| = 8 > 7. Als zweistellige Verknüpfung zwischen reellen Zahlen ist die modulo-Operation weder kommutativ noch assoziativ. Sie ist jedoch distributiv: (x) mod (cy) = c(x mod y). (Warum?) Wichtiger ist jedoch die Arithmetik mit Restklassen. Dazu zunächst die folgende Definition. Es sei z ∈ R, z 6= 0. Definition 6.3 Wir sagen, daß die reellen Zahlen x und y gleich sind modulo z und schreiben x = y (mod z) wenn x mod z = y mod z. Es unmittelbar klar, daß Gleichheit modulo z eine Äquivalenzrelation in den reellen Zahlen ist. Wenn zwei Zahlen zur gleichen Klasse gehören, also gleich sind modulo z, ist ihre Differenz ein ganzes Vielfaches von z. Aus der Gleichung bn + αc = n + bαc für n ∈ Z und α ∈ R folgt, daß zwei reelle Zahlen auch nur dann gleich sind modulo z, wenn ihre Differenz ein ganzes Vielfaches von z ist. Es ist zweckmäßig, das als Hilfssatz zu formulieren. Hilfssatz 6.1 Es gilt x = y mod z genau dann, wenn x − y ein ganzes Vielfaches von z ist. Im weiteren wollen wir uns auf die für die Informatik wichtigsten Anwendungen der modulo-Operation beschränken. Dazu schränken wir die Operation auf die ganzen Zahlen Z ein und bilden die Restklassen nach einer natürlichen Zahl q > 1. In der Informatik ist q meistens eine Zweierpotenz. Für jede ganze Zahl l ∈ Z gibt es dann eine eindeutige Zerlegung l l= + l mod q q mit (l mod q) ∈ {0, 1, . . . , q −1}. Die Operationen Addition und Multiplikation der ganzen Zahlen lassen sich auf die Restklassen modulo q übertragen, denn es gilt Proposition 6.1 Es seien l1 , l2 , l10 , l20 ∈ Z. Gilt l1 = l10 (mod q) und l2 = l20 (mod q), so gilt auch l1 + l2 = l10 + l20 (mod q) und l1 · l2 = l10 · l20 (mod q). 6.1. LAUFZEITANALYSE VON ALGORITHMEN UND PROGRAMMEN 195 Beweis: Wir benutzen Hilfssatz 6.1. Es ist l1 − l10 = n1 · q und l2 − l20 = n2 · q, also (l1 + l2 ) − (l10 + l20 ) = (n1 − n2 ) · q. Entsprechend finden wir l1 · l2 − l10 · l20 = l1 · l2 − (l1 − n1 · q) · (l2 − n2 · q) = (l1 · n2 + l2 · n1 − n1 · n2 · q) · q. 2 Proposition 6.1 bildet die Grundlage für die Operationen mit Zahlen vom Type unsigned in C. Siehe hierzu Seite 48. Dazu wird ein Bereich darstellbarer natürlicher Zahlen festgelegt: B := {0, 1, 2, . . . , b − 1}. Meistens ist b = 2k − 1, wobei k eine geeignete Wortlänge ist, z. B. k = 32 (siehe Abschnitt 3.2, Seite 96, und Unterabschnitt 4.1.2, Seite 122). Die ganzen Zahlen werden in Restklassen modulo b eingeteilt und zu jeder Restklasse gibt es genau einen darstellbaren Vertreter aus B. Proposition 6.1 garantiert, daß ein mit den Operationen +, −, · aufgebauter Ausdruck von ganzen Zahlen stets auf den gleichen Wert aus B führt, unabhängig davon, ob nur das Endresultat oder auch Zwischenergebnisse modulo b genommen werden. Ist l ∈ B so ist auch b − l ∈ B. Es gilt l + b − l = b und b = 0 (mod b). Da l − l = 0, wird die durch −l bestimmte Restklasse in B durch b − l repräsentiert. In C sind für Werte vom Typ unsigned auch die Operationen / und % erklärt. Sind l1 , l2 ∈ B, so führen l1 / l2 und l1 % l2 nicht aus B heraus und ergeben die erwarteten Resultate. Gibt es bei der Berechnung eines Ausdrucks Zwischenresultate, die außerhalb von B liegen, so kann es sein, daß die Operationen / und % unerwartete Ergebnisse liefern. Für ein Beispiel nehmen wir B = {0, 1, 2, . . . , 216 −1 = 65535}, also Werte vom Typ short unsigned int. Ausgehend von den Werten l1 = 17 und l2 = 6, wollen wir −l1 / − l2 und −l1 % − l2 bilden. In Tabelle 6.7 sind zwei C-Programme und die Ergebnisse ihrer Abläufe dargestellt. Was wird man erwarten? Es werden −l1 und −l2 gebildet und die entsprechenden Vertreter aus B bestimmt. Da sind 216 −17 = 654519 und 216 −6 = 65530. Beide Programme in Tabelle 6.7 liefern auch diese Werte. Es ist offensichtlich 65519 / 65530 = 0 und 65519 % 65530 = 65519, aber nur das zweite Programm in Tabelle 6.7 liefert diese Werte. Aus dem ersten Programm, das sich vom zweiten nur durch die fehlenden Zwischenspeicherungen von −l1 und −l2 unterscheidet, ergibt sich unerwarteterweise −l1 / − l2 = 2 und −l1 % − l2 = 65531. Wie läßt sich das erklären? Es werden Quotient und Rest zunächst im Bereich der ganzen Zahlen gebildet und man erhält (nach den Regeln des hier benutzten C-Compilers) −17 / − 6 = 2 und −17 % − 6 = −5. Erst die Ausgabe mit dem Format hu ordnet diesen Werten die entsprechenden Vertreter aus B zu und macht aus −5 den Wert 65531. Proposition 6.1 läßt sich also nicht auf die Operationen / und % erweitern. 6.1.5 Abschätzungen und Größenordnungen Definition 6.4 Es seien f : N → R und g : N → R mit f (n) > 0 und g(n) > 0. 1. Groß-O-Notation: f heißt von der Ordnung O von g, in Formeln f (n) = O(g(n)), 196 ' KAPITEL 6. ALGORITHMEN II: EFFIZIENZ UND KOMPLEXITÄT #include <stdio.h> main() { unsigned short int l1, l2; l1 = 17; l2 = 6; printf("-l1 = %hu -l2 = %hu\n", -l1, -l2); printf("-l1/-l2 = %hu\n", -l1/-l2); printf("-l1\%-l2 = %hu\n", -l1%-l2); } $ -l1 = 65519 -l2 = 65530 -l1/-l2 = 2 -l1%-2 = 65531 ***************************************************************** #include <stdio.h> main() { unsigned short int l1, l2; unsigned short int i1, i2; l1 = 17; l2 = 6; i1 = -l1; i2 = -l2; printf("i1 = %hu i2 = %hu\n", i1, i2); printf("i1/i2 = %hu\n", i1/i2); printf("i1%%i2 = %hu\n", i1%i2); } i1 = 65519 i2 = 65530 i1/i2 = 0 i1%i2 = 65519 & % Tabelle 6.7: Unterschiedliche Ergebnisse für die Operationen / und %. wenn es c > 0 und n0 ∈ N gibt mit f (n) ≤ c · g(n) für alle n ≥ n0 . 2. Groß-Omega-Notation: f heißt von der Ordnung Ω von g, in Formeln f (n) = Ω(g(n)), 6.1. LAUFZEITANALYSE VON ALGORITHMEN UND PROGRAMMEN 197 wenn es c > 0 und n0 ∈ N gibt mit f (n) ≥ c · g(n) für alle n ≥ n0 . 3. Groß-Theta-Notation: f heißt von der Ordnung Θ von g, in Formeln f (n) = Θ(g(n)), wenn es c1 > 0, c2 > 0 und n0 ∈ N gibt mit c1 · g(n) ≤ f (n) ≤ c2 · g(n) für alle n ≥ n0 . Aus der Definition folgt: • • f (n) = O(g(n)) genau dann, wenn g(n) = Ω(f (n)) und f (n) = Θ(g(n)) genau dann, wenn f (n) = O(g(n)) und g(n) = O(f (n)). Regeln Wir vollen nur Regeln für die Groß-O-Notation angeben. Für die anderen Notationen ergeben sie sich entsprechend. 1. Falls f (n) ≤ g(n) für alle n ≥ m, so gilt f (n) = O(g(n)) mit c = 1. Beispiel 1: log2 (n − 1) = O(log2 (n)). 2. Konstante Faktoren haben keine Bedeutung. Falls f (n) = O(g(n)), dann auch af (n) = O(g(n)) für alle a > 0. Beispiel 2: Ist f (n) = O(g(n)), so gilt auch a1 f (n) + a2 f (n) + · · · + al f (n) = (a1 + a2 + · · · + al )f (n) = O(g(n)). Beispiel 3: Logarithmen können zu beliebiger Basis genommen werden. Für a > 0, b > 0 gilt: Aus f (n) = O(loga (n)) folgt f (n) = O(logb (n)). log (n) In der Tat: Aus n = aloga (n) = blogb (a) a = blogb (a)·loga (n) folgt logb (n) = logb (a) · loga (n). D. h. f (n) ≤ c · loga (n) = logbc(n) · logb (n) für alle n ≥ n0 . Beispiel 4: Ist a > 0, so ist f (n) = O(ln(n)) genau dann, wenn f (n) = O(ln(n+a)). Gilt f (n) = O(ln(n + a)), so folgt f (n) = O(ln(n)) nach Regel 1. Es gelte f (n) = O(ln(n + a)), d. h. für alle n ≥ n0 f (n) ≤ c · ln(n + a) = c · ln( n+a · n) = c · (ln(1 + na ) + ln(n)). n Für n ≥ max(a, 2) gilt a ≤ n · (n − 1), also n + a ≤ n2 , d. h. 1 + na ≤ n. Das bedeutet f (n) ≤ c · (ln(n) + ln(n)) = 2 · c ln(n) für alle n ≥ max(a, 2, n0 ) , also f (n) = O(ln(n)). 3. Sei f1 (n) = O(g(n), f2(n) = O(g(n), . . . , fl (n) = O(g(n)). Dann ist auch f1 (n) + f2 (n) + · · · + fl (n) = O(g(n)). 198 KAPITEL 6. ALGORITHMEN II: EFFIZIENZ UND KOMPLEXITÄT Anwendung auf das Mischsortieren Wir wollen an das Ergebnis 6.5, Seite 189, anknüpfen. Es sei sn eine Sortierfolge beliebiger Länge n. Wir setzen k := dlog2 (n)e und nehmen n ≥ 2, also k ≥ 1 an. Mit wachsender Länge der Sortierfolgen kann der Aufwand für den schlechtesten Fall nicht geringer werden: W OC(n) ≤ W OC(2k ). Aus Gleichung 6.4, Seite 188, folgt für den Aufwand R(sn ) R(sn ) ≤ f · 2k + (b − c + d) · (2k − 1) + (a + c) · 2k · k ≤ C1 · 2k + C2 · 2k · k ≤ C3 · 2k · k mit C3 > 0.6 Aus 2k = 2log2 (n) · 2k−log2 (n) ≤ 2 · 2log2 (n) = 2 · n und k = log2 (n) + (k − log2 (n)) ≤ log2 (n) + 1 ≤ log2 (n) + log2 (n) = 2 · log2 (n) ergibt sich schließlich R(sn ) ≤ 4 · C3 · n · log2 (n) = O(n · ln(n)) Für eine Abschätzung des besten Falls setzen wir k 0 := blog2 (n)c und nehmen n ≥ 4 an. 0 Es gilt BEC(2k ) ≤ BEC(n) und aus Gleichung 6.3, Seite 188, ergibt sich c 0 0 0 R(sn ) ≥ f · 2k + (b + d) · (2k − 1) + (a + ) · 2k · k 0 2 k0 0 ≥ C4 · 2 · k Berücksichtigt man −1 < blog2 (n)c−log2 (n) ≤ 0, so findet man mit einfachen Rechnungen ähnlich den obigen R(sn ) ≥ Zusammenfassend 1 · C4 · n · log2 (n) = Ω(n · ln(n)) 4 R(sn ) = Θ(n · ln(n)) Es ist nicht schwer, auch aus Gleichung 6.12 Gleichung 6.15 herzuleiten. 6 Man mache sich klar, daß das auch gilt, falls b − c + d < 0 sein sollte. (6.15) 6.2. DIE KOMPLEXITÄT VON PROBLEMEN 6.1.6 199 Rekurrenzen und erzeugende Funktionen* In den vorangehenden Abschnitten 6.1.2 und 6.1.5 haben wir Rekurrenzen zur Bestimmung der Effizienz des Mischsortierens benutzt. Wir haben Lösungen geraten und dann bewiesen, daß es Lösungen sind. In der Informatik und mehr noch in der Kombinatorik treten sehr viele Rekurrenzen auf. Es ist wichtig, Methoden zu ihrer Lösung zu haben. Einen Überblick über die wichtigsten elementaren Methoden gibt Kapitel 4 in Cormen/Leiserson/Rivest [CormLR1990]. Eine wichtige Technik zu Lösung sind erzeugende Funktionen (generating functions). Das sind unendliche Folgen von reellen (oder komplexen) Zahlen α0 , α1 , . . . , αn , . . ., die man jedoch üblicherweise als eine formale unendliche Reihe schreibt ∞ X αn xn n=0 Für manche Zahlenfolgen konvergiert diese Reihe innerhalb eines positiven Konvergenzradius und stellt dann dort eine holomorphe Funktion dar. Aber auch wenn sie nicht konrvergieren, kann man mit diesen Reihen formal rechnen und erhält wichtige Ergebnisse, z. B. Lösungen von Rekurrenzen. Man spricht auch von formalen Potenzreihen. Erzeugende Funktionen werden in der Mathematik und ihren Anwendungen an vielen Stellen verwandt. Zu weiteren Einzelheiten sehe man in der angegebenen Literatur nach. 6.2 6.2.1 Die Komplexität von Problemen Überblick Normalerweise wird man einen Algorithmus komplex nennen, wenn er eine komplexe Struktur hat. Er besteht dann aus einer größeren Zahl von Einzelaktionen, die auf vielfältige Weise zusammenwirken. Im Zusammenhang mit der Komplexität von Problemen, wollen wir unter der Komplexität eines Algorithmus seine Effizienz verstehen. Wir sprechen von der WOC-Komplexität (BEC-Komplexität, AVC-Komplexität) des Algorithmus. Bisher haben wir die Komplexität des euklidischen Algorithmus (Unterabschnitt 1.1.3), des Sortierens durch Einfügen (Unterabschnitt 1.2.3) und des Mischsortierens (Gleichung 6.15) untersucht. Von diesen Algorithmen hat Sortieren durch Einfügen die schlechteste, d. h. größte WOC-Komplexität: Die Anzahl der Vergleichsoperationen wächst quadratisch mit der Anzahl n zu sortierender Elemente. In den folgenden Kapiteln werden wir eine Reihe weiterer Algorithmen kennenlernen, deren WOC-Komplexität nicht stärker als nk mit kleinem k wächst. Es gibt jedoch eine Reihe von interessanten und auch praktisch wichtigen Problemen, für k die man nur Lösungsalgorithmen kennt, deren WOC-Komplexität wie 2n , k ≥ 1 fest, 200 KAPITEL 6. ALGORITHMEN II: EFFIZIENZ UND KOMPLEXITÄT wächst. Das bedeutet, daß die Ausführungszeiten mit wachsendem Umfang der Eingabe sehr rasch das praktisch Machbare überschreiten. Man spricht von schwierigen (difficult) oder auch unbehandelbaren (intractable) Problemen. Für einige dieser Probleme hat man nachgewiesen, daß jeder Lösungsgalgorithmus exponentielle WOC-Komplexität haben muß. Für sehr viele, darunter die interessantesten, hat man das bisher nicht nachweisen können. Trotz intensiver Bemühungen hat man aber bisher auch keinen Lösungsalgorithmus mit polynomieller Komplexität gefunden. Was man jedoch gefunden hat, sind eindrucksvolle theoretische Resultate. In einer sehr groben Darstellung besagen sie folgendes: Für sehr viele der schwierigen Probleme kennt man „nichtdeterministische Lösungsverfahren“ mit polynomieller Komplexität. Dabei wird hier bewußt offengelassen, was das ist. Die Klasse der mit diesen Verfahren lösbaren Probleme wird mit N P bezeichnet. Die Klasse P der mit deterministischen Lösungsverfahren, also Algorithmen, polynomieller Komplexität lösbaren Probleme ist in N P enthalten. In N P hat man nun Probleme gefunden, die in gewissem Sinne als die schwierigsten der Klasse anzusehen sind, die N P-vollständigen Probleme (N P-complete problem): Gibt es für ein N P-vollständiges Problem einen Lösungsalgorithmus polynomieller Komplexität, dann gibt es einen solchen für jedes Problem aus N P und es gilt P = N P. Inzwischen kennt man eine lange Liste N P-vollständiger Probleme aus vielen unterschiedlichen Gebieten. Für keines ist jemals ein Lösungsalgorithmus polynomieller Komplexität gefunden worden. Aus diesem Grund wird allgemein angenommen, daß es auch keinen gibt und daß N P eine echte Obermenge7 von P ist. Eine praktische Konsequenz daraus ist: Weiß man, daß ein Problem, mit dem man sich beschäftigt, N P-vollständig ist, so sollte man keinen Lösungsalgorithmus polynomieller Komplexität suchen. Einige Anmerkungen dazu, was man denn sonst tun sollte, sind in in Abschnitt 6.3 zu finden. In den restlichen Unterabschnitten dieses Abschnitts soll eingehender auf die angesprochen Fragen eingegangen werden. Dazu wird zunächst festgelegt, was unter polynomieller und exponentieller Komplexität eines Algorithmus zu verstehen ist. Anschließend wird ausführlich ein Beispiel eines schwierigen Problems behandelt. Dieses Beispiel wird dann herangezogen, um die Komplexität von Problemen und die Fragestellung P = N P? exemplarisch zu behandeln. 6.2.2 Komplexität von Algorithmen Im folgenden wollen wir uns auf die WOC-Komplexität beschränken. Wenn wir von der Komplexität eines Algorithmus sprechen, ist immer WOC-Komplexität gemeint. WOCn (A) ist die Anzahl Schritte, die Anzahl Operationen, die der Algorithmus A im schlechtesten Fall durchführt, wenn er eine Eingabe vom Umfang n bearbeitet. Inzwischen (August 2010) sieht es so aus, als wenn Vinay Deolalikar von den HP Labs, Palo Alto, California, einen Beweis für N P 6= P gefunden hätte. 7 6.2. DIE KOMPLEXITÄT VON PROBLEMEN 201 Definition 6.5 I. Algorithmus A hat polynomielle Komplexität (polynomial complexity), wenn es eine natürliche Zahl k gibt, so daß WOCn (A) = O(nk ). II. Algorithmus A hat exponentielle Komplexität (exponential complexity), wenn 1. für jede natürliche Zahl k gilt WOCn (A) = Ω(nk ) und k0 2. es gibt eine natürliche Zahl k 0 , so daß WOCn (A) = O(2n ). Ist A ein Algorithmus mit polynomieller Komplexität, so gibt es c > 0, k ≥ 0 und n0 , so daß jede Eingabe sn vom Umfang n ≥ n0 nach spätestens c · nk Schritten bearbeitet ist: √ n k 4 2 3 RA (sn ) ≤ c · n . Beispiele sind n − 3n + 7, n · ln(n), n . Auch m ist mit wachsendem n(n−1)···(n−m+1) m n ≤ nm! . n und festem m von polynomieller Komplexität, denn es gilt m = m! Hat ein Algorithmus A exponentielle Komplexität, so wächst WOCn (A) schneller als jedes Polynom in n. Andererseits gibt es k 0 ≥ 0 und c0 > 0, so daß für alle hinreichend großen k0 n gilt RA (sn ) ≤ c0 · 2n . Einige Beispiele für exponentielle Komplexität: n 2 1. 10n , denn nk < 10n = 2log2 (10) = 2log2 (10)·n < 2n . k0 k0 2. nn , denn nk < nn = 2log2 (n) nk0 3. nln(n) , denn nk < nln(n) = 2log2 (n) 4. n!, denn nk < n! < nn . k0 = 2log2 (n)·n < 2n ln(n) k0 +1 2 < 2n . Die Abschätzungen gelten stets für alle hinreichend großen n. Auch Beispiel 3 ist nach der obigen Definition von exponentieller Komplexität, obgleich nln(n) langsamer wächst α als 2n für jedes α > 0. Zu den Beispielen siehe auch Aufgaben 6.1 und 6.2. Die Stirlingsche Formel (Stirling’s approximation) √ 2πn n n e ≤ n! ≤ √ 2πn n n+(1/12n) e (6.16) ist neben vielen anderen Dingen auch bei der Bestimmung der Komplexität von Algo rithmen von Nutzen. Mit ihrer Hilfe kann man z. B. zeigen, daß 2n von exponentieller n Komplexität ist. Siehe hierzu Aufgabe 6.3. 6.2.3 Beispiel: Hamiltonkreise Problembeschreibung Für dieses Beispiel müssen wir im Vorgriff auf Kapitel 12 einiges aus der Graphentheorie einführen. Ein schlichter Graph (simple graph) besteht aus einer nichtleeren, endlichen 202 KAPITEL 6. ALGORITHMEN II: EFFIZIENZ UND KOMPLEXITÄT Menge V von Knoten (vertex) und einer endlichen Menge E von Kanten (edge). Kanten verbinden jeweils zwei Knoten. Bei schlichten Graphen darf eine Kante keinen Knoten mit sich selbst verbinden (keine Schlingen) und zwei verschiedene Knoten dürfen durch höchstens eine Kante verbunden sein (keine Mehrfachkanten). Zwei Knoten, die durch eine Kante verbunden sind, heißen Nachbarn (neighbor). Abbildung 6.4 zeigt ein Beispiel ...................... ...................... ...................... ....... ....... ....... .... .... .... ... ... .... .... ... .... ... ... ... ... ... ... ... ... ... .... ..... ..... ... ... ... ... .. ... . ... ... ... .. . . . . .. . . ... . . . . . . . . . . . . . . . . ... ........ . ... .... ..... ..... . . . . . . . . . . . . . . . . . . . . . . . . . . . ...... . ....... .. ... ... ... ..... ..... ................................ ........... ........ .................... .......... .... ..................... ....... ... .... ........ ..... ..... .... ... ...... ........ .... ... ...... ....... ..... .. ........ .... ..... ... ... ....... .... ........ .. . . . . . . .... . . . . . . . . ... . . . . . ...... ....... .. .... ... ... ..... ....... ............... ... ... .... .... ..... .. ... ..... .... ... ... ..... ............ ... .... .. .. ...... .... ............... ............ ... ..... .. .. ...... . . . . . . . . . . . . . . . . . . . . . . . ...... ... ...... . .... ..... .... ....... ..... ... ............... ... ... .... .... ....... ..... .... ... ... ..... ....... ..... ....... .... .. .. ...... ........ ......... ..... ....... .... .. .. ........ . . . . . . . . . . . . . . . . . . . . . . . . . . . ....... .... ... ..... ....... ....... .... .... ... ... ........ .... .......... ......... .... ........ ... ... ..... ...... .................... ......................... ........ .... ..... ....... ... .. ...... ..... ..... .... ...... ........ .... ..... ... .. ................. .... . ... . ................... . .... . . . . . . . . . . .... ..... .. ... .... ... ... .. ..... .... ... ... ... ... .. ..... . ... ... .. . ... ...... .... ... .... .. ................................................................................................................................................................................................................................ .. .......... ..... ... .. . .. .. ..... ... .... ... ..... . . . . . . ... . . . . . ..... . .... ... . ... .. . . . . . .... . . ...... . . . . . . . . . . . . ...... ... .... . .. ...... ...... ................................ .. .... .......................... ... .... ........................ ... ...... ... .. ..... ... .... ... . . . ... . ... . . ....... . ... ... . ... . ... . ... . . ..... ... ... ... .. .. ..... ... ........ ... .. .. ...... .. . . . . . . . . .. .... ....... ... .. . .. . . . . . . . . . . . . . ... .... . .. ... ..... ..... . . . . . . . .. . . . . . . . . ... . . . . . . .... . ... ........ ...... ... ... .... . . . . . . . . . .. . . ... . . . . . . . . . . . . ................. .... ... .. ........ ..... ... .... ... ..... ... ... ........ ..... ... .... ....... ... ... .......... ..... ... ... .......... .. .......... ... ..... .. ... .... ..... .. ........... . . . . . . . . . . . . . . . . . . . . . . . ... ... . ..... ......... ... ..... ...... ... ... ... ........ ..... .... .... ... ... ... ......... ... ..... .... .... ... ... ................ .. ..... ... ..... .... ... .. ..... ... .. . . . .... . . . . . . . . . . . . . . . . . . . .... ......... ... . ... .... ..... . ... ... ... ... ... .... ..... ......... ... ... ... ... ..... ..... ........ ....... ... .. .... ... ..... ......... .... ... .... .. ... ..... ........ . . . ... . .. . . . . . . . . . . . . . . . . .... . . .. ... ......... .... .................... .... ......... .... ... ........ ....... ..... ........... ........................... ........ ......... ........ ..... ..... ........ ... ... ... .................. .. . . .... . . ... .. .. .. .. ..... .... .. .. ... ... ... ... ... ... . .. . ... . . . .... . . ..... . . . . . . . . . . . . . ........................ ........................ K1 K3 K2 K5 K4 K6 K7 K8 Abbildung 6.4: Schlichter Graph mit Hamiltonkreis eines schlichten Graphen. Ein Weg (path) ist eine Knotenfolge v0 , v1 , v2 , . . . , vn mit n ≥ 1, bei der je zwei aufeinanderfolgende Knoten durch eine Kante verbunden sind. Ein Weg heißt offen (open), wenn v1 6= vn , andernfalls geschlossen (closed). Ein geschlossener Weg, bei dem nur Anfangsund Endknoten gleich und alle anderen paarweise verschieden sind und bei dem keine Kante mehrfach auftritt, heißt Kreis (circuit). Ein Kreis, der alle Knoten eines schlichten Graphen enthält, heißt Hamiltonkreis (Hamiltonian circuit) des Graphen. Ein Graph mit einem Hamiltonktreis wird hamiltonsch (hamiltonian) genannt. Besitzt der Graph in Abbildung 6.4 einen Hamiltonkreis? Nach einigem Probieren wird man vielleicht einen finden, z. B. K8, K3, K7, K1, K5, K4, K2, K6, K8. Gibt es weitere? Ja, denn man kann den Durchlauf in jedem der Knoten beginnen und dann noch eine von zwei Durchlaufsrichtungen wählen. Ob es Hamiltonkreise gibt, die davon „wesentlich“ verschieden sind, ist nicht so einfach zu entscheiden. 6.2. DIE KOMPLEXITÄT VON PROBLEMEN 203 Algorithmen zum Finden von Hamiltonkreisen Gesucht sind Algorithmen, die feststellen, ob ein gegebener schlichter Graph Hamiltonkreise hat oder nicht. Falls ja, sollen sie alle diese Kreise liefern. Daß es solche Algorithmen gibt, ist leicht zu sehen. Wir bilden alle Permutationen der Knotenmenge und prüfen für jede Permutation nach, ob zwei in der Permutation benachbarte Knoten auch im Graphen Nachbarn sind und ob es vom letzten Knoten der Permutation eine Kante zum ersten Knoten gibt. Es ist klar, daß das ein sehr aufwendiges Verfahren ist, denn es sind n! Permutationen zu bilden und zu überprüfen, wenn n die Anzahl Knoten des Graphen ist. Schon für den Graphen von Abbildung 6.4, der ja nur „Spielformat“ hat, ergeben sich 40320 Fälle. Für Graphen mit 100 Knoten sind es mehr als 10100 Fälle. Für sie ist das Verfahren praktisch undurchführbar. Man kann nun auf die Idee kommen, daß der Aufwand des Algorithmus nur deswegen so groß ist, weil man ungeschickt und naiv an die Lösung herangegangen ist. Von den Permutationen, die man bildet, sind sehr viele überflüssig und brauchen nicht zu berücksichtigt werden: 1. Wir brauchen nur Permutationen zu betrachten, die mit einem festen Knoten beginnen, denn zwischen Hamiltonkreisen, die sich auseinander nur durch unterschiedliche Wahl der Anfangsknoten ergeben, wollen wir keinen Unterschied machen. 2. Alle Permutationen, in denen zwei nicht benachbarte Knoten aufeinander folgen, kann man streichen. Das bedeutet, daß außer dem letzten Knoten jeder Knoten einer zulässigen Permutation einen Nachbarn haben muß, der vorher in der Permutation nicht aufgetreten ist. Der letzte Knoten muß den ersten Knoten als Nachbarn haben. Mit diesen Feststellungen kann man ähnlich wie beim Mischsortieren (Tabelle 6.1, Seite 179) einen einfachen Algorithmus angeben, der alle Hamiltonkreise findet, die von einem festen Knoten ausgehen. Er ist in Tabelle 6.8 zu sehen. Er ist in C-ähnlichem Pseudocode for' Prozedur HMLT (VERTEX ∗v, int length) // Suche nach Hamiltonkreisen 1 2 3 4 5 6 7 & { v→aktiv = TRUE; for (alle Nachbarn v 0 von v) { if (v 0 == start && length == n − 1) Hamiltonkreis gefunden; if (v 0 →aktiv == FALSE) HMLT (v 0 , length + 1); } v→aktiv = FALSE; } Tabelle 6.8: Prozedur HMLT $ % 204 KAPITEL 6. ALGORITHMEN II: EFFIZIENZ UND KOMPLEXITÄT muliert, benutzt ein in jedem Knotensatz vorhandenes Feld aktiv, das anfangs mit FALSE vorbesetzt ist. Es wird rekursiv aufgerufen und arbeitet mit Rücksetzen (Zeile 6). In allen Rekursionstufen ist der Anfangsknoten start der Suche und die Knotenanzahl n des Graphen bekannt. Die Suche startet mit dem Aufruf HMLT (start, 0) und baut beginnend in start alle Wege auf, in denen keine Knoten mehrfach auftreten. Ein solcher Weg kann höchstens die Länge n haben. Hat er diese Länge, so wird in Zeile 3 geprüft, ob man vom Endknoten direkt zum Anfangsknoten kommen kann. Dann und nur dann, wenn das der Fall ist, hat man einen Hamiltonkreis gefunden. Anders als beim Mischsortieren ist es hier jedoch nicht ganz einfach, aus dem Algorithmus in Pseudocode ein ablauffähiges C-Programm zu gewinnen. Dort reichte es aus, einen Satztyp liste einzuführen. Hier müßten Satztypen für Knoten eingeführt und Datenstrukturen und Funktionen für Nachbarschaften angelegt werden. Das führt zu weit von unserem Thema fort. Deshalb sei nur auf Abschnitt E.5 im Anhang verwiesen, wo ein vollständiges C-Programm angegeben ist. Wendet man es auf den Graphen von Abbildung 6.4 mit dem Startknoten K8 an, so findet man die in Tabelle 6.9 aufgeführten maximalen Wege, d. h. Wege, die nicht mehr verlängerbar sind, ohne daß Knoten mehrfach auftreten. Es sind insgesamt 26 Wege, darunter 4 Wege, die zu Hamiltonkreisen führen. Wesentlich verschieden sind die Hamiltonkreise K8, K1, K5, K4, K2, K6, K7, K3, K8 und K8, K6, K2, K4, K5, K1, K7, K3, K8. Die beiden anderen ergeben sich durch Umkehrung der Durchlaufrichtung. Ändert man den Anfangsknoten, so findet man bis auf eine Kreisverschiebung die gleichen Hamiltonkreise. Die übrigen Wege sind jedoch nicht identisch. Als Beispiel wenden wir den Algorithmus HMLT auf den gleichen Graphen, aber mit Startknoten K1 an. Tabelle 6.10 zeigt das Ergebnis. Es gibt jetzt 30 Wege, darunter auch einige, die alle Knoten des Graphen enthalten, sich aber nicht zu Hamiltonkreisen ergänzen lassen. K1, K5, K8, K3, K7, K6, K2, K4 ist ein solcher Weg. Ein offener Weg der Länge n, der alle Knoten eines Graphen enthält, sich aber nicht zu einem Hamiltonkreis ergänzen läßt, heißt Hamiltonzug (Hamiltonian trail) . Exponentieller Zeitbedarf von HMLT In diesem Unterabschnitt soll die Komplexität von HMLT für den schlechtestem Fall (WOC) untersucht werden. Die Verbesserung des Algorithmus HMLT gegenüber der direkten Bildung aller Knotenpermutationen ist sehr deutlich. Leider ist sie nicht gut genug, um auch größere Graphen in akzeptabler Zeit zu bearbeiten. Um das zu sehen, betrachten wir den Graphen in Abbildung 6.5. Er hat n = 2k +2 Knoten. Startet man HT ML mit dem Anfangsknoten u0 , so werden unter anderem alle Wege der Länge k von u0 nach uk oder vk gefunden. Alle diese Wege sind n von der Form u0 , x1 , x2 , · · · , xk−1 , xk mit xκ = uκ . oder xκ = vκ . Es gibt also 2k = 2 2 −1 √ n 2 . Auch das ist exponentiWege dieser Art. Die Zahl der Wege wächst also mit 21 · elles Wachstum und führt schon bei Graphen mittlerer Größe zu extremen Rechenzeiten. 6.2. DIE KOMPLEXITÄT VON PROBLEMEN K8 K8 K8 K8 K8 K8 K8 K8 K8 K8 K8 K8 K8 K8 K8 K8 K8 K8 K8 K8 K8 K8 K8 K8 K8 K8 K1 K1 K1 K1 K1 K1 K1 K6 K6 K6 K6 K6 K6 K5 K5 K5 K5 K5 K5 K5 K5 K5 K3 K3 K3 K3 K5 K5 K5 K7 K7 K7 K7 K2 K2 K2 K7 K7 K7 K1 K1 K1 K1 K4 K4 K4 K4 K4 K7 K7 K7 K7 K4 K4 K4 K6 K2 K2 K3 K4 K7 K7 K1 K2 K3 K7 K7 K7 K7 K2 K2 K2 K2 K2 K1 K6 K2 K2 K2 K2 K2 K2 K4 K6 K7 K7 K6 K4 K5 K6 K3 K7 K3 K8 K5 K5 K1 K3 K5 K4 K1 K7 K3 K8 K5 K4 K6 K2 K2 K3 K7 K7 K7 K6 K6 K5 K2 K4 K6 K2 K4 K4 K6 205 Hamilton !! Hamilton !! K4 K2 K5 K1 K1 K6 K3 K7 K7 K4 K4 K5 K1 K3 K2 K6 K8 K5 K1 K8 K1 Hamilton !! Hamilton !! Tabelle 6.9: Bestimmung von Hamiltonkreisen (Anfangsknoten K8) Nehmen wir als Beispiel einen Graphen vom angegeben Typ mit 100 Knoten. Dann sind √ 100 mindestens 21 = 5.6294 · 1014 maximale Wege zu finden. Unter der Annahme, daß 2 der benutzte Rechner 100 Nanosekunden braucht, um einen Weg zu finden8 , benötigt der Rechner 5.6294 · 1016 Nanosekunden. Das sind mehr als 1.78 Jahre. Bei 105 Knoten sind es mehr als 10, bei 115 Knoten mehr als 323 und bei 150 Knoten mehr als eine halbe Million Jahre. Im übrigen sind die vorangehenden Abschätzungen viel zu optimistisch. Das liegt zum einen daran, daß keineswegs alle zu testenden Wege berücksichtigt wurden und daß na8 Auch noch heute (2006) eine unrealistisch kurze Zeit. 206 K1 K1 K1 K1 K1 K1 K1 K1 K1 K1 K1 K1 K1 K1 K1 K1 K1 K1 K1 K1 K1 K1 K1 K1 K1 K1 K1 K1 K1 K1 KAPITEL 6. ALGORITHMEN II: EFFIZIENZ UND KOMPLEXITÄT K5 K5 K5 K5 K5 K5 K5 K5 K5 K5 K5 K8 K8 K8 K8 K8 K8 K8 K8 K8 K8 K7 K7 K7 K7 K7 K7 K7 K7 K7 K8 K8 K8 K8 K8 K8 K8 K4 K4 K4 K4 K6 K6 K6 K6 K5 K5 K5 K3 K3 K3 K6 K6 K6 K2 K2 K2 K2 K3 K3 K6 K6 K6 K6 K3 K3 K3 K2 K2 K2 K2 K2 K2 K7 K7 K4 K4 K4 K7 K7 K7 K2 K8 K8 K4 K4 K6 K6 K8 K8 K2 K2 K7 K7 K7 K7 K7 K7 K7 K6 K6 K4 K7 K2 K3 K2 K2 K2 K6 K2 K2 K4 K5 K3 K5 K5 K8 K8 K6 K5 K4 K7 K2 K3 K6 K2 K2 K6 K3 K8 K7 K5 K3 K4 K3 K4 K2 K4 K6 K8 K8 K3 K3 K4 K3 K6 K7 K1 K8 K1 Hamilton !! Hamilton !! K5 K7 K7 K6 K2 K4 K6 K5 K4 K6 K3 K7 K3 K4 K5 K1 K5 K8 K8 K5 K3 K2 K4 K6 K3 K4 Hamilton !! K8 K3 K2 K4 K5 K1 K2 K6 Hamilton !! Tabelle 6.10: Bestimmung von Hamiltonkreisen (Anfangsknoten K1) türlich in einem „normalen“ Notebook9 und einem C-Programm mehr als 100 Nanosekunden zur Bestimmung eines maximalen Weges benötigt werden. In Tabelle 6.11 sind die durch das C-Programm von HMLT ermittelten Wegeanzahlen und Zeiten für Graphen des gegebenen Typs mit 6 bis 34 Knoten zu sehen. Man erkennt sehr gut das exponentielle Wachstum der Anzahl der untersuchten Wege, der Anzahl Hamiltonkreise und der 9 IMB ThinkPad T43 6.2. DIE KOMPLEXITÄT VON PROBLEMEN .......... ..................... ......................... ....... ........... .... ...... ..... ... .... ... ... ... ... .. ... .. ... ... ... ... ... .. . ... . ... 1 ................................................................ 2 ....................... 0 ................................................................. ... . . .... .... ..... . . . . . . . . . . . . . . . ....... . ... ........................ ....... ... ........................ ....... .................. ....... .... ... .... .... ... .... .... ... .... .... .... .... . ... .... ... ... ... .... ... .... . . . . ... . . . .... ... . . . . . . . ... . . . ... ... .... ... ..... . . ... . ...... .... ... ... ....... .... ...... ... ... .... .... ... .... .... .... ... ... ... ... .... ... . . . . .... . . ... .... ... ... .... .... .... ... ....... ... .... . . . . . . . . . . . . . . .... ........................ ....... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ....... ...... ....... ..... ... .... . . . . . . . . . . ... . . ... ... .. .. .. ... .. ... ... . .. .. . . .... 1 ................................................................. 2 ....................... 0 ................................................................. ... . . . . . .... ... . ..... . . . .. . . . ....... . . . . . . . . . . . . .................... ................... ............... u v u v u ···· v 207 ......................... ......................... .... ..... ... ... ... ... ... ... ... .. ... ... . . .................... ........................................................ . k ... k−1 .... ... .. . . . ... ... . . . . . . . . . . . . . . . . .. ........................ .. ........................ ...... . . . . . . .... .. ... ... .... .... .... .... .... ... .... ... .... ... .... .... ...... ... ....... ... ...... . . ... . ... ...... . ... . ... ... . . ... . .... .... .. . . . . ... .... .... .. . . . . .... .. ... . . . . . . .... ........................ ...... . . . . . . ...... ............... ..... . ... ... . . . ... ... ..... .... .. .. ........................................................ ..................... ... .. k ... k−1 .... ... .. . . ... . . .... . . ...... .. . . . . . . . . . . .................... .................. u u v v Abbildung 6.5: Die Anzahl der Wege wächst exponentiell Rechenzeiten. Diese sind bis ca. 18 Knoten mit der relativ ungenauen Zeitmessung des Systems10 nicht meßbar, liegen dann bis 24 Knoten im Sekunden- und ab 26 Knoten im Minutenbereich, um schließlich ab 34 Knoten in den Stundenbereich überzugehen. Danach war die Geduld des Autors erschöpft. Die Rechnungen wurden jeweils mit den Anfangsknoten U0 und U1 ausgeführt. Für U1 ist die Zahl der Wege geringer und damit auch die Rechenzeiten. Das Wachstumsverhalten ist das gleiche. Zum Vergleich ist in der letzten Spalte der Tabelle die Anzahl der Wege angegeben, die sich aus der obigen Abschätzung ergibt. Es fällt auf, daß die Anzahl der Hamiltonkreise durch 2n/2 gegeben ist und daß die Anzahl Wege, die durch die Abschätzung gegeben ist, halb so groß ist. Das ist auf die spezielle Struktur der Graphen zurückzuführen und leicht zu erklären. Jeder von u0 ausgehende Hamiltonkreis, der als zweiten Knoten u1 oder v1 hat, enthält genau einen der oben angegebenen Teilwege und ist auf der Reststrecke eindeutig bestimmt. Die Umkehrung der Durchlaufrichtung, d.h. v0 wird zweiter Knoten, verdoppelt die Anzahl der Hamiltonkreise. Die Daten der Tabelle ergeben überdies, daß die mittlere Zeit zur Bestimmung eines maximalen Weges ungefähr bei 30 µs liegt und mit der maximal möglichen Länge, also der Knotenzahl, langsam wächst. Bei genauerem Hinsehen stellt man fest, daß Algorithmus HMLT zwei Fragestellungen beantwortet: 1. Er liefert eine Liste aller Hamiltonkreise eines Graphen, die von einem festen Anfangsknoten ausgehen. Wie wir gesehen haben, gibt es Graphenklassen, in denen die Anzahl der Hamiltonkreise exponentiell mit der Anzahl der Knoten wächst. Daher muß jeder Algorithmus, der alle Hamiltonkreise findet, exponentielle WOC-Komplexität haben. Für jeden Hamiltonkreis muß nämlich mindestens eine „Operation“ ausgeführt werden, z. B. Eintrag in eine Liste. 2. Gibt es einen Hamiltonkreis in dem Graphen? 10 C-Funktionen time und difftime. Siehe [LoweP1995]. 208 KAPITEL 6. ALGORITHMEN II: EFFIZIENZ UND KOMPLEXITÄT Anz. Knoten 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 Startknoten U0 U1 U0 U1 U0 U1 U0 U1 U0 U1 U0 U1 U0 U1 U0 U1 U0 U1 U0 U1 U0 U1 U0 U1 U0 U1 U0 U1 U0 U1 Anz. Wege 24 20 84 62 276 206 900 638 2916 2030 9444 6446 30564 20654 98916 66350 320100 213806 1035876 698966 3352164 2229038 10847844 7205678 35104356 23302958 113600100 75379502 367617636 243872558 Anz. Hamiltonkrs 8 8 16 16 32 32 64 64 128 128 256 256 512 512 1024 1024 2048 2048 4096 4096 8192 8192 16384 16384 32768 32768 65536 65536 131072 131072 Sekunden nm nm nm nm nm nm nm nm nm nm nm nm 1 nm 2 1 7 4 24 16 84 55 293 193 1018 717 3564 2314 12726 7932 Absch. Wege 4 8 16 32 64 128 256 512 1024 2048 4096 8192 16484 32768 65536 nm: Nicht meßbar. (Das System gibt 0 Sekunden an) Tabelle 6.11: Anzahl Wege und Rechenzeit in Abhängigkeit von der Knotenzahl Dies ist ein Teilproblem von 1. Die Lösung soll in einer der Antworten „ ja, dies ist ein Hamiltonkreis“ oder „nein, es gibt keinen Hamiltonkreis“ bestehen. Der Umfang der Antwort erfordert keinen Aufwand. HMLT ist so zu modifizieren, daß er beim ersten gefundenen Hamiltonkreis mit „ ja“ abbricht und den gefundenen Hamilton- 6.2. DIE KOMPLEXITÄT VON PROBLEMEN 209 kreis ausgibt oder nach kompletter Abarbeitung aller maximalen Wege „nein, es gibt keinen Hamiltonkreis“ meldet. (a) Antwort: ja Wenn man „Glück hat“, erwischt man jedes Mal eine richtige Kante und hat nach n untersuchten Knoten einen Hamiltonkreis gefunden. Kann der Aufwand exponentiell werden, wenn man „Pech hat“? Ja, das ist möglich und soll in Aufgabe 6.4 untersucht werden. Wie kommt es zu Glück oder Pech? Das liegt an Zeile 2 des Algorithmus HMLT . Wie kommt man zu allen Nachbarn v 0 eines Knotens v? Das hängt von der Datenstruktur ab, die man zur Darstellung von Nachbarschaften gewählt hat, und für einen gegebenen Graphen und einen gegebenen Anfangsknoten kann die gewählte Datenstruktur günstig oder ungünstig sein. (b) Antwort: nein Der Aufwand ist im allgemeinen exponentiell. Siehe Aufgabe 6.5. Da Algorithmus HMLT das Problem, alle Hamiltonkreise zu finden, für größere Graphen auch nicht effizient löst, kann man versucht sein, ihn zu verfeinern und weitere graphentheoretische Eigenschaften von Hamiltonkreisen zu berücksichtigen. Man sollte das jedoch nicht tun. Man weiß nämlich, daß das Finden eines Hamiltonkreises ein N P-vollständiges Problem ist (siehe Unterabschnitte 6.2.1 und 6.2.4). Am Beispiel von HMLT wollen wir jedoch eine andere wichtige Betrachtungsweise einführen und auf die Frage „Was ist Nichtdeterminismus?“ eine erste Antwort geben. Tabelle 6.12 zeigt den „Algorithmus“ ' $ & % Prozedur NDHMLT (VERTEX ∗v, int length) // Nichtdeterministische Suche nach einem Hamiltonkreis 1 { v→aktiv = TRUE; 2 for (einen Nachbarn v 0 von v) 3 { if (v 0 →aktiv == FALSE) NDHMLT (v 0 , length + 1); 4 if (v 0 == start && length == n − 1) 5 { Ende: Hamiltonkreis gefunden; 6 } 7 else 8 { Ende: Kein Hamiltonkreis; 9 } 10 } 11 } Tabelle 6.12: Prozedur NDHMLT NDHMLT . Er unterscheidet sich von HT ML nur durch kleine Änderungen. Die ent- 210 KAPITEL 6. ALGORITHMEN II: EFFIZIENZ UND KOMPLEXITÄT scheidende Änderung befindet sich in Zeile 2. Statt aller Nachbarn des Knotens v wird nur irgendein Nachbar von von v geprüft. Dieser Unterschied ist wesentlich. Bei HT ML werden stets alle Nachbarn von v untersucht. Unbekannt, weil von der Implementierung der Graphstrukturen abhängig, ist nur die Reihenfolge, in der das geschieht. Man muß gewährleisten, daß das Endergebnis nicht von der Reihenfolge abhängt. Bei NDHT ML wird stets nur ein Nachbar von v betrachtet und es bleibt unbestimmt, welcher das ist. Nach spätestens n untersuchten Knoten endet der Ablauf von NDHT ML mit einem gefundenen Weg. Dieser Weg braucht nicht maximal zu sein und nur, wenn man „Glück gehabt hat“, ist es ein Hamiltonkreis. Die Prüfung, ob man Glück gehabt hat oder nicht, ist ganz einfach und wird in NDHT ML in Zeile 4 ausgeführt. 6.2.4 Die Problemklassen P, N P und weitere Vorbemerkungen Unter der Komplexität eines Problems wollen wir die beste, d. h. kleinste, WOC-Komplexität unter allen Algorithmen, die das Problem lösen, verstehen. Es läßt sich zum Beispiel zeigen (und das soll in Abschnitt 11.4 getan werden), daß jeder Sortieralgorithmus, der nur Vergleiche der Sortierwerte benutzt, eine WOC-Komplexität von mindestens O(n · ln(n)) haben muß. Es ist eine große Zahl von Problemen bekannt, für die man nur Lösungsalgorithmen mit exponentieller Komplexität kennt. Dazu gehören natürlich alle Probleme, bei denen der Umfang der Lösung exponentiell mit dem Umfang der Eingabe wächst, wie zum bei dem Problem, alle Hamiltonkreise eines Graphen zu finden. Aber es gehören auch Probleme dazu, bei denen der Umfang der Lösung höchstens polynomiell mit dem Umfang der Eingabe wächst. Diesen gilt unser besonderes Interesse. Bevor dies geschehen kann, muß jedoch einiges präzisiert werden: Was ist ein Problem, eine Lösung eines Problems? Was ist ein Lösungsalgorithmus? Will man diese Fragen streng, d. h. exakt beantworten, so muß man auf formalisierte Algorithmusdarstellungen zurückgreifen. Es soll hier nur angedeutet werden, wie das geschieht. Als formales Hilfsmittel werden in der Regel Turingmaschinen benutzt (siehe Unterabschnitt 5.2.4, Seite 172). Es wird ein Eingabealphabet festgelegt und Wörter aus diesem einer Turingmaschine als Eingabezeichenreihen zugeführt. Am Beispiel von Hamiltonkreisen soll das erläutert werden. Wir legen über dem Eingabealphabet eine Codierung von schlichten Graphen fest und suchen eine Turingmaschine, die Graphen mit einem Hamiltonkreis erkennt („akzeptiert“). Als Grundlage für die Konstruktion der Turingmaschine soll der naive Algorithmus HMLT dienen. Ein solche Turingmaschine ist ein formalisierter Algorithmus, der für jede Eingabe mit einem der folgenden Ergebnisse hält: 1. Die Zeichenreihe ist keine korrekte Darstellung eines schlichten Graphen. 2. Es handelt sich um einen Hamiltongraphen. 6.2. DIE KOMPLEXITÄT VON PROBLEMEN 211 3. Der Graph ist kein Hamiltongraph. Die Länge der Eingabezeichenreihe ist der Umfang der Eingabe, die Anzahl Schritte der Turingmaschine ist die Anzahl der Operationen. Die Syntaxprüfung unter Nummer 1 ist bei „vernünftiger“, d.h. naheliegender Codierung mit polynomiellem Aufwand möglich und kann z. B. durch eine vorgeschaltete Turingmaschine erledigt werden. Wir wollen daher nur Eingabezeichenreihen betrachten, die korrekte Darstellungen von schlichten Graphen sind. Bei wiederum vernünftiger Codierung wächst der Länge der Eingabezeichenreihe nur polynomiell mit der Anzahl Knoten des dargestellten Graphen und die Anzahl Schritte der Turingmaschine hängt nur polynomiell von der Anzahl Operationen im naiven Algorithmus HT ML ab. Wir können die Zeichenreihen, die Hamiltongraphen darstellen, als eine formale Sprache auffassen. Die Zeichenreihen, die Nicht-Hamiltongraphen darstellen, bilden die dazu komplementäre (formale) Sprache. Zu den Begriffen Alphabet und formale Sprache siehe Abschnitt 3.6, Seite 112. Unsere Turingmaschine erkennt also sowohl Hamiltongraphen als auch Nicht-Hamiltongraphen (im schlechtesten Fall) in exponentieller Zeit. Formal ist ein Problem (problem) eine formale Sprache und eine Problemlösung (problem solution) eine Turingmaschine, die die Zugehörigkeit einer Eingabe zu dieser Sprache erkennt. Die komplementäre (formale) Sprache (complementary language) charakterisiert das komplementäre Problem (complementary problem). Es ist nicht unmittelbar einsichtig, aber zutreffend, daß wichtige naiv formulierte Probleme, wie z. B. Sortierprobleme oder Optimierungsprobleme sich als formale Sprachen auffassen lassen. Für Komplexitätsbetrachtungen gilt außerdem so etwas Ähnliches wie die Churchsche These (Unterabschnitt 5.2.2, Seite 169): Ein naiver Algorithmus und jede seiner (vernünftig codierten) Formalisierungen sind beide von polynomieller oder beide von exponentieller Komplexität. Problemklassen Wir führen die Komplexitätsklassen P und EX P ein. Die Klasse P besteht aus allen Problemen, für die ein Lösungsalgorithmus polynomieller Komplexität existiert. Die Klasse EX P besteht aus allen Problemen, für die es einen Lösungsalgorithmus polynomieller oder exponentieller Komplexität gibt. EX P ist eine echte Oberklasse von P, denn sie enthält Probleme, für die nachweislich kein Lösungsalgorithmus polynomieller Komplexität existiert. Sowohl die Klasse P als auch die Klasse EX P werden durch Algorithmen, also deterministische Lösungsverfahren definiert. Das bedeutet, daß für jede Eingabe x beim Halten feststeht, ob sie zur betreffenden Sprache gehört oder nicht. Für jedes Problem S ∈ P existiert ein Algorithmus, der auch das zu S komplementäre Problem löst. Es ist also P = COP, wobei COP die Menge der komplementären Probleme von P ist. Ganz entsprechend gilt EX P = COEX P. Wie im einleitenden Überblick (Seite 199) ausgeführt, interessiert uns im besonderen Maße die Problemklasse N P. Diese Probleme gehören zu EX P, es ist aber unbekannt, ob 212 KAPITEL 6. ALGORITHMEN II: EFFIZIENZ UND KOMPLEXITÄT sie in P liegen. Um diese Klasse zu definieren, wollen wir zunächst einmal klären, was eine nichtdeterministische Turingmaschine (non-deterministic Turing machine) ist. Eine normale (deterministische) Turingmaschine hat bei gegebenem Eingabewert einen eindeutig bestimmten Ablauf. Bei einer nichtdeterministischen Turingmaschine ist der Ablauf nicht notwendigerweise eindeutig, sondern es kann Stellen geben, an denen verschiedene Fortsetzungen möglich sind. Es ist dabei unbestimmt, welche Fortsetzung eintritt. Eine nichtdeterministische Turingmaschine steht bei einer gegebenen Eingabe für eine Schar von möglichen Abläufen. Deterministische Turingmaschinen, bei denen nur ein Ablauf möglich ist, sind ein Spezialfall. Eine nichtdeterministische Turingmaschine erkennt die Sprache S, d. h. löst das S entsprechende Problem, wenn für jedes x ∈ S (mindestens) einer der möglichen Abläufe zur Akzeptanz von x führt. Mit Hilfe von nichtdeterministischen Turingmaschinen wollen wir nun die Probleme der Klasse N P charakterisieren. Als Beispiel soll der „Algorithmus“ NDHT ML (Tabelle 6.12) betrachtet werden. Wir nehmen an, er sei als nichtdetermistische Turingmaschine codiert, orientieren uns aber am Pseudocode der Tabelle. Ein Ablauf wird ausgehend vom Anfangsknoten zunächst auf nichtdeterministische Art einen Weg bis zu einem ersten schon besuchten Knoten finden. Das geschieht in den Zeilen 1 bis 3. Wird Zeile 4 erreicht, so ist der Weg gefunden. Der Rest läuft deterministisch ab und besteht nur in der Prüfung, ob der gefundene Kreis ein Hamiltonkreis ist oder nicht. Ausgehend von diesem Beispiel legen wir fest: Ein Problem gehört zur Klasse N P, wenn 1. ein Lösungsverfahren existiert, das nichtdeterministisch in polynomieller Zeit einen Lösungsvorschlag, ein Zertifikat (certificate), generiert, von dem deterministisch in polynomieller Zeit festgestellt wird, ob es eine Lösung ist und 2. mindestens ein Ablauf auch eine Lösung liefert. Salopp ausgedrückt: Ein Problem gehört zu P, wenn in polynomieller Zeit eine Lösung gefunden werden kann. Ein Problem gehört zu N P, wenn in polynomieller Zeit eine Lösung verifiziert werden kann. Aus dieser Definition folgt nicht unmittelbar, daß ein Problem aus N P stets auch mit einem deterministischen Verfahren exponentieller Komplexität gelöst werden kann. Es läßt sich aber zeigen, daß das so ist: N P ⊂ EX P. NDHMLT zeigt, daß „Ist G ein Hamiltongraph?“ ein Problem aus N P ist. Das komplementäre Problem ist „Ist G kein Hamiltongraph?“. Es ist aus CON P. Man kann NDHMLT nicht heranziehen, um zu zeigen, daß dieses Problem auch aus N P ist, und man nimmt an, daß es auch keine anderen nichtdeterministischen polynomiellen Lösungsverfahren dafür gibt. Wenn das so ist, gilt N P = 6 CON P. Es gilt jedoch N P ∩CON P = 6 ∅. Es ist nämlich P = COP und somit P Teilklasse von N P und von CON P. Abbildung 6.6 zeigt, wie die bisher betrachteten Komplexitätsklassen zusammenhängen. Das Bild zeigt 6.2. DIE KOMPLEXITÄT VON PROBLEMEN 213 ......................................................................................... ..................................... ....................... ...................... ................. ................. ............... ............... ............. ............. ............ . . . . . . . . . . .......... ....... . . . . . .......... . . . . ......... ...... . . . . . . . . ......... ...... . . . ........ . . . . ........ ..... . . . . . . ....... .... . . . ....... . . . ...... ..... . . . . . ...... .... . . ...... . . . ...... .... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ......................... ......................... ...... .................. .................. . . . . . . .... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ..... . . . . . . . . . . . . . . . . . . . . . ............. ...................... ......... . ... ..... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .......... .......... .... ....... ....... . . ... . . . . . . . . . . . . . . . . . . . .... . . . . . . . ........ ........ ..... ...... . . ... . . . . . . . .... . . . . . . . . . . . . . . . . ....... ....... .... ..... .. . . ... . . . . . . . . . . . . . . . . . . . . ...... ...... ... .... ..... .. . . . . . . . . . . . . . . . ... . . ...... ...... .... .... . . . ... . ... . . . . . . . . . . ..... ..... .... ... . .. ..... . . . . . . . . .... .... ... .. ... .. . . . . . . . . ... ... ... .. .. .. . . . . ... . . . ... ... . . .. . . . .. . . . . . ... . ... .. .. .. ... . . ... ... .. .. ..... ... ..... .. ... ... .. .. ... .. .. .. . ... . ... .. .... ... .. .. .. .. ... . . .. . .. . ... . . . ... . . . ... . ... . . . ... . . . . . . ... ... . .. ... ....................... ... ... ... ... ... ... ..... ... ... ... ... ... ... ... ... ... ... .... .. .. ... .. .... ... .... .... ... ... ... ... . . . ..... . . . ..... . . ... . . . . . ... ...... ..... . ..... ..... ... ... ...... ... ...... ... ..... ...... ... ...... .. ...... .... ...... ...... ... ... ....... ....... ... ....... ...... ....... ... ... ........ ........ ................. ....... ....... ... . . .... . . . . . . . . . . . . . . . . . . . . . . . . ......... ......... .... ......... .......... .......... ... ......... .... ............ .......... ............................. ..... ..... ............... . ............ ..... ..... ...................... ............... ................................... ............... ..... ...... ................................................................................... .................................................................................... ..... ...... . . . . . ...... ...... ...... ...... ...... ...... ....... ....... ....... ....... ........ . . . . . . . . ........ ........ ......... ........ ......... ......... .......... ........... .......... ........... .......... . . . . . . . . . . . ............. ............. ............... .............. ................. ....................... .................. ....................................... ....................... ................................................................................... EX P NP N P ∩ CON P CON P P Abbildung 6.6: Komplexitätsklassen den Stand, der zur Zeit (2006) als der wahrscheinlichste gilt. Es ist sicher, daß EX P eine echte Oberklasse von N P und von CON P ist. Für die anderen Beziehungen sind auch noch die folgenden anderen Möglichkeiten offen: 1. P = N P = CON P 2. P ⊂ N P = CON P 3. P = N P ∩ CON P ⊂ NP CON P Reduktion und Vollständigkeit In der Wissenschaft, aber auch im täglichen Leben, ist es oft möglich, ein Problem dadurch zu lösen, daß man es auf ein anderes, schon gelöstes Problem zurückführt, reduziert (reduce). Für Probleme in formalisierter Darstellung wollen wir das folgendermaßen präzisieren: Es sei S eine formale Sprache (ein Problem) und es wird eine (deterministische oder nichtdeterministische) Turingmaschine T (ein Lösungsverfahren) gesucht, die eine zulässige Eingabe x genau dann akzeptiert, wenn x ∈ S. Es seien weiter ein Problem S 0 und eine Turingmaschine T 0 gegeben, wobei T 0 genau die x0 ∈ S 0 akzeptiert. Abbildung 6.7 zeigt, wie man S auf S 0 reduzieren kann. Eine Reduktionsmaschine R wandelt x in x0 um. Dabei muß sichergestellt sein, daß jedes für S zulässige x in ein für S 0 zulässiges x0 transformiert wird. Anschließend wird x0 von T 0 bearbeitet. Es muß sichergestellt sein, daß dann und nur dann x0 von T 0 akzeptiert wird, wenn x ∈ S. 214 KAPITEL 6. ALGORITHMEN II: EFFIZIENZ UND KOMPLEXITÄT .................................................................................................................................................................................................................................................................................................... ... ... .. ... ... ... ... ... ... ... ... ... . .. ...................................................................................................... .. .... ... ... ... .... .. ... ... ... ... ... .. x ' R & T $ x0 % $ ' .. ................................................................................. .. T ... ... .. ... ... ... ... ... ... ... ... ... . .. ...................................................................................................... .. .... ... ... ... .. ... .... .. ... ... ... .. ja 0 & % .................................................................................................................................................................................................................................................................................................... Abbildung 6.7: Reduktion von Problemen Die Maschine R wird nicht umsonst arbeiten. Es kostet eine gewissen Aufwand, x in x0 umzuwandeln Für uns sind Transformationen wichtig, die in polynomieller Zeit möglich sind. Es ist nicht schwer, zu sehen, daß dann die Komplexität von T 0 die Komplexität von T bestimmt. Daraus folgt: Gehört S 0 zu Komplexitätsklasse P (bzw. N P, EX P), so gehört auch S zu dieser Klasse. Theoretisch kann man zeigen, daß es in jeder dieser Klassen Probleme gibt, auf die sich jedes Problem der Klasse reduzieren läßt. Man spricht von vollständigen Problemen (complete problem) und unterscheidet P-Vollständigkeit, N PVollständigkeit und EX P-Vollständigkeit. Es gibt auch CON P-vollständige Probleme; hierauf soll jedoch nicht weiter eingegangen werden. Wir wollen uns im folgenden auf N P-vollständige Probleme als dem wichtigsten Fall und auf die Frage „P = N P?“ beschränken. Wenn es möglich ist, ein N P-vollständiges Problem auf ein anderes Problem aus N P zu reduzieren, dann ist offenbar auch das zweite Problem N P-vollständig. Der große Durchbruch begann, als es Cook 11 1971 gelang [Cook1971], die N P-Vollständigkeit eines bekannten Problems, des Erfüllbarkeitsproblems (satisfiabilty problem) explizit nachzuweisen. Seitdem sind Hunderte von bekannten und weniger bekannten Problemen als N P-vollständig nachgewiesen worden, in der Regel durch (manchmal recht kunstvolle) Reduktionen. Auch das Problem, in einem Hamiltongraphen einen Hamiltonkreis zu finden, gehört dazu. Siehe auch die Literaturangaben auf Seite 223. Cook, Stephen Arthur ∗1939 Buffalo, New York. Amerikanischer Informatiker. Zur Zeit (2006) Professor an der University of Toronto, Canada. Schuf gleichzeitig mit Karp 12 und Edmonds 13 die moderne Theorie der N P-Vollständigkeit. 11 Karp, Richard M. ∗3.Januar 1935. Amerikanischer Informatiker. Zur Zeit (2006) Professor an der University of California at Berkeley. Wies 1972 [Karp1972] unter Benutzung von Reduzierbarkeit die N P-Vollständigkeit einer großen Zahl wichtiger kombinatorischer Probleme nach. 12 Edmonds, Jack ∗1961 (?). Kanadischer Mathematiker und Informatiker. Bis 1999 Professor an der University of Waterloo, Canada. Grundlegende Beiträge zur Kombinatorik, Graphentheorie und Algorithmik. Opfer akademischen Mobbings, was 1991 bis 1993 zu einem Zerwürfnis mit der Universität führte. 13 6.2. DIE KOMPLEXITÄT VON PROBLEMEN 215 Ein schönes Beispiel für die Anwendung von Reduktion ist auf Seite 512 zu finden. Dort wird durch Reduktion auf Hamiltonwege gezeigt, daß das Problem, einen kürzesten Weg in einem Graphen mit negativen Liniengewichten zu finden, N P-vollständig ist. Für alle N P-vollständigen Probleme hat man bisher vergeblich nach einem polynomiellen Algorithmus gesucht. Würde man für eines dieser Probleme einen finden, so wäre P = N P. Man hat aber auch für keines nachweisen können, daß es keinen Algorithmus mit polynomieller Komplexität geben kann. Könnte man das für eines der Probleme, so wäre N P ⊃ P und alle N P-vollständigen Probleme lägen in N P \ P. Abschließende Bemerkungen Im einführenden Überblick 6.2.1 haben wir Probleme, für die man nur Algorithmen exponentieller Komplexität kennt, als schwierig bezeichnet. Für einige von ihnen kann man nachweisen, daß es nur Lösungsalgorithmen exponentieller Komplexität geben kann. Für die übrigen besteht noch Hoffnung. Vielleicht findet man einen Lösungsalgorithmus polynomieller Komplexität. Allerdings ist bei den N P-vollständigen Problemen diese Hoffnung recht gering. Bei den dann noch verbleibenden Problemen wollen wir zwei Gruppen kurz erläutern. N P-harte Probleme: Ein Problem S heißt N P-hart (N P-hard), wenn sich jedes Problem aus N P auf S reduzieren läßt, aber nicht bekannt ist, ob S zu N P gehört. Es sind solche Probleme aus der theoretischen Informatik bekannt. Für sie ist die Hoffnung auf effiziente Lösungsalgorithmen noch geringer. N P-vollständige Problem haben einen Lösungsalgorithmus polynomieller Komplexität genau dann, wenn P = N P. N P-harte Problem können einen Lösungsalgorithmus polynomieller Komplexität nur dann haben, wenn P = N P. Nicht-vollständige Probleme aus N P: Es gibt Probleme aus N P, von denen man einerseits nicht weiß, ob sie N P-vollständig sind, für die man aber andererseits einen polynomiellen Lösungsalgorithmus bisher vergeblich gesucht hat. Ein bekanntes Problem dieser Art ist das Graphisomorphieproblem. Dabei werden Algorithmen gesucht, die (im einfachsten Fall) von zwei schlichten Graphen mit verschiedenen Knotenmengen feststellen, ob sie bis auf die Knotenbezeichnungen identisch sind. Probleme der hier betrachteten Art kann man dadurch „lösen“, daß man ihre N P-Vollständigkeit feststellt. Das ist in den vergangenen Jahren immer wieder einmal gelungen. Man kann ein solches Problem aber auch dadurch lösen, daß man einen polynomiellen Lösungsalgorithmus findet. In seltenen, spektakulären Fällen ist auch das gelungen. Zum Beispiel kennt man inzwischen polynomielle Lösungsverfahren für die lineare Optimierung [Khac1979], [Karm1984] oder das Primzahlproblem14 [AgraKS2004]. 14 Es ist festzustellen, ob eine Zahl Primzahl ist oder nicht. 216 KAPITEL 6. ALGORITHMEN II: EFFIZIENZ UND KOMPLEXITÄT 6.3 Näherungslösungen schwieriger Probleme* Es gibt sehr viele interessante und für die Praxis wichtige schwierige Probleme, die meisten davon sind N P-vollständig und viele sind Optimierunugsprobleme. Man kann es sich gar nicht leisten, die Hände in den Schoß zu legen und zu sagen „Da kann man halt nichts machen“. Mit den üblichen Entwurfstechniken für Algorithmen: Teile und herrsche, dynamische Programmierung, Rücksetzen, lokale Suche, Gieralgorithmen (Unterabschnitt 5.1.3, Seite 159) kann man nur in Ausnahmefällen die gewünschten Lösungen erhalten. Auch mit ad hoc konstruierten Algorithmen kommt man nur selten und nur bei Aufgabenstellungen mit kleiner Eingabemenge weiter. Aus diesem Grund wächst seit einigen Jahrzenten die Zahl der Lösungsansätze und Lösungsvorschläge, mit denen man bei diesen Problemen wenigstens etwas weiter kommt. Es ist naheliegend, daß diese Lösungen zunächst für spezielle Probleme gefunden werden und sich übergeordnete, methodenorientierte Zusammenhänge erst nach und nach herausstellen. Aus diesem Grund und auch, weil die Entwicklung sehr rasch und intensiv verläuft, gibt es kaum Lehrbücher, die das Gebiet als Ganzes behandeln. Zu nennen ist hier in erster Linie Hromkovič Algorithms for Hard Problems [Hrom2001]. Schwierigkeitsgrad und Umfang diese Buches übersteigen jedoch deutlich das, was in der Grundausbildung in Informatik zugemutet werden kann. Zum Glück hat der gleiche Autor in einem unkonventionellen Buch über theoretische Informatik ([Hrom2007], erste Auflage [Hrom2001a]) in zwei Kapiteln den Stoff auch „grundausbildungsgerecht“ dargestellt. Für eine Einführung in die Informatik ist das aber immer noch zu viel. Deshalb sollen im folgenden die zur Zeit wichtigsten Bereiche des Gebiets knapp und überblicksartig beschrieben und einige wenige Anwendungen genannt werden. Aus Erfahrung gut Lange, bevor man etwas von N P-Vollständigkeit wußte, ja sogar lange, bevor es moderne Rechner gab, haben gute Disponenten die Probleme der Routenplanung recht zufriedenstellend gelöst. Das war allerdings eher Intuition und Erfahrung als Informatik. Ein schon eher als Informatik anzusehendes Beispiel ist der Simplex-Algorithmus zur Lösung linearer Optimierungsprobleme15 . Es ist bekannt, daß der Simplex-Algorithmus in einigen Fällen exponentielle Laufzeiten hat. Obwohl man inzwischen weiß (siehe oben), daß lineare Optimierung zu P gehört und man brauchbare polynomielle Lösungsalgorithmen hat, ist der Simplex-Algorithmus immer noch das meist genutzte Instrument der linearen Optimierung. 15 Lineare Optimierung wird in diesem Buch nicht behandelt. Siehe Neumann/Morlock [NeumM1993] 6.3. NÄHERUNGSLÖSUNGEN SCHWIERIGER PROBLEME* 217 Beschränkung auf Teilpropleme Wenn man sich auf spezielle Teilprobleme beschränkt, dann gibt es für diese häufig Lösungsalgorithmen polynomieller Komplexität, obwohl das Ausgangsproblem N P-vollständig ist. Zum Beispiel gibt es für die meisten schweren Probleme auf Graphen polynomielle Lösungsalgorithmen, wenn man sich auch Bäume beschränkt. Siehe auch „minimale Rundwege“ Seite 552. Pseudopolynomielle Algorithmen Wir fangen mit dem Primzahltest an. Es sei n ≥ 3 eine natürliche Zahl und es soll festgestellt werden, ob n eine Primzahl ist. Wir teilen n durch 2, 3 · · · , n − 1 und wissen nach n − 2 Schritten, ob n einen echten Teiler√ hat oder nicht. Wenn wir es geschickter machen wollen, teilen wir nur durch 2, 3, · · · , b nc und kommen schneller zum Ergebnis. n ist der Umfang der Eingabe, also haben wir einen effizienten polynomiellen Algorithmus! Oder etwa nicht? Schauen wir uns den Aufwand einmal an. Bei n = 106 haben wir 1 Million Divisionen, bei n = 1012 sind es 1 Billion Divisionen. Andererseits haben wir im ersten Fall nur 6 Dezimalziffern, im zweiten nur 12. Das sieht nicht nach einem effizienten Algorithmus aus. Was wir suchen, ist ein effizienter Lösungsalgorithmus, also einen der polynomiell in der Anzahl der Ziffern, die zur Darstellung von n gebraucht werden, arbeitet. Probleme, deren Eingabe aus einer oder mehreren ganzen Zahlen besteht, heißen Zahlprobleme (integer-valued problems). Bei einem Zahlproblem, dessen Eingabe eine ganze Zahl n ist, sprechen wir von einer Lösung polynomieller Komplexität, wenn der Algorithmus polynomiell in der Länge der Darstellung von n, also polynomiell in Ω(ln(n)) ist. Ist der Algorithmus polynomiell in n, so sprechen wir von einer pseudopolynomiellen (pseudopolynomial) Lösung. Für den Primzahltest ist also ein pseudopolynomieller Lösungsalgorithmus rasch gefunden. Ob es auch einen polynomiellen Lösungsalgorihtmus gibt, war lange Zeit unklar. Der 2004 veröffentlichte und auf Seite 215 genannte Algorithmus von Agrawal/Kayal/Saxena – AKS-Algorithmus [AgraKS2004] – beantwortete die Frage im positiven Sinn. Übrigens hat hat nach Gleichung 1.9 auf Seite 13 der euklidische Algorithmus polynomielle Komplexität. Für uns ist nun folgende Frage wichtig: Gibt es Zahlprobleme, die N P-vollständig sind, für die jedoch pseudopolynomielle Lösungalgorithmen mit praktisch brauchbaren Ergebnissen existieren? Ja, das ist der Fall. Ein Beispiel ist das Rucksackproblem (siehe Beispiel 5.2, Seite 161). Zu Einzelheiten siehe Hromkovič [Hrom2007]. Schön wäre es, wenn alle Zahlprobleme pseudopolynomielle Lösungen aufweisen würden. Man hätte dann Näherungsverfahren für eine große Klasse schwieriger Probleme. Leider ist das nicht so. Zum Beispiel kann man nachweisen, daß das Problem des Handlungsreisenden (siehe Unterabschnitt 21.3.2, Seite 555) keine pseudopolynomiellen Lösungsalgorithmen zuläßt. Solche Probleme heißen stark N P-vollständig (strongly N P-complete). 218 KAPITEL 6. ALGORITHMEN II: EFFIZIENZ UND KOMPLEXITÄT Approximative Algorithmen Approximationsalgorithmen werden zur Lösung von schweren Optimierungsproblemen eingesetzt. Statt eine optimale Lösung zu fordern, geben wir uns mit einer „fast optimalen“ Lösung zufrieden. Diese wollen wir jedoch mit wesentlich weniger Aufwand, d. h. in polynomieller Zeit erhalten. Wir beginnen mit einem Beispiel aus der Planungstheorie. Beispiel 6.3 (Listenplanung) Wir haben K Aufträge und m gleichartige Maschinen. Jeder Auftrag kann auf jeder Maschine bearbeitet werden und braucht überall die gleiche Zeit tk . Die Aufträge sind unabhängig voneinander und können in beliebiger Reihenfolge bearbeitet werden. Ein Auftrag, dessen Bearbeitung begonnen wurde, wird auf der entsprechenden Maschine bis zum Ende ausgeführt und nicht unterbrochen. Eine Zuordnung der Aufträge zu den Maschinen, bei der jedem Auftrag genau eine Maschine und eine Anfangszeit zugeordnet ist, sich auf einer Maschine keine Bearbeitungen überlappen und keine Maschine leersteht, solange noch unbearbeitete Aufträge vorhanden sind, heißt Vergabeplan (schedule). Unter der Gesamtdurchlaufszeit (makespan) versteht man die Zeit vom Beginn der Bearbeitung bis zum Ende des letzten Auftrages. Diese Zeit soll minimiert werden. Für diese Zielfunktion gibt es (mindestens) einen optimalen Vergabeplan. Das Finden eines solchen ist N P-vollständig. Für m = 2 gibt es eine pseudopolynomielle Lösung, für m ≥ 3 ist das Problem stark N P-vollständig (Garey/Johnson [GareJ1979], [SS8], Seite 238). Man braucht Näherungslösungen. Es hat sich herausgestellt, daß man ohne viel Aufwand Näherungslösungen angeben kann, die ganz brauchbar sind. Das sind Listenpläne (list schedules). Ein Listenplan ergibt sich aus einer irgendwie angeordneten Liste aller Aufträge, indem auf einer freien oder frei gewordenen Maschine der nächste Auftrag aus der Liste begonnen wird. Abbildung 6.8 zeigt zwei Vergabepläne für 7 2 4 6 M1 k1 k6 M2 k2 k5 M3 k3 k4 8 10 × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × 2 k7 k5 4 6 8 k1 k3 k2 k4 k6 k7 10 × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × × Abbildung 6.8: Listenplan und optimaler Vergabeplan Aufträge und 3 Maschinen. Der erste enspricht der Liste (k1 , k2 , k3 , k4, k5 , k6 , k7 ), der zweite gehört zur Liste (k1 , k2 , k5 , k6 , k3, k4 , k7 ) und ist optimal. Die Ausführungszeiten sind t1 = 5, t2 = 5, t3 = 4, t4 = 4, t5 = 3, t6 = 3, t7 = 3. Es ist nun nicht schwer, abzuschätzen, um wieviel ein optimaler Vergabeplan besser sein kann als ein beliebiger Listenplan. Es sei Tl die Gesamtdurchlaufszeit eines Listenplanes 6.3. NÄHERUNGSLÖSUNGEN SCHWIERIGER PROBLEME* 219 und Topt die Gesamtdurchlaufszeit eine optimalen Vergabeplanes. Ein optimaler Vergabeplan braucht mindestens die Zeit eines längsten Auftrags,d. h. T opt ≥ tk (k = 1, 2, . . . , K) Außerdem wird mindestens die Zeit verstreichen, die m Maschinen brauchen, um den gesamten Auftragsbestand abzuarbeiten K Topt 1 X tk ≥ m k=1 Es sei k0 der Auftrag16 , der beim Listenplan als letzter fertig wird und somit die Gesamtdurchlausfzeit bestimmt. sk0 sei sein Startzeitpunkt. Vor diesem Zeitpunkt müssen alle Maschinen beschäftigt gewesen sein, denn sonst wäre k0 früher gestartet worden. Es kann auch sein, daß auch noch danach alle Maschinen aktiv waren. Also sk 0 ≤ 1 X tk m k6=k0 und das heißt K Tl = sk0 + tk0 1 X 1 ≤ +(1 − )tk0 m m k=1 Daraus folgt Tl ≤ Topt + (1 − 1 )Topt m also 1 )Topt (6.17) m Das Beispiel zeigt, daß beliebige Vergabepläne höchsten um den Faktor 2 schlechter sind als optimale Vergabepläne. Ob das ausreichend ist, hängt von der Anwendung ab. 2 Tl ≤ (2 − Für einige Optimierunsaufgaben gibt es polynomielle Approximationsschemata (polynomial approximation scheme) Das sind Approximatonsalgorithmen mit polynomieller Komplexität, mir denen eine beliebig gute Näherung erreicht werden kann, allerdings auf Kosten komplizerter werdender Algorithmen wachsender Komplexität. Nicht für alle Probleme gibt es polynomielle Approximationsalgorithmen. Zum Beispiel gibt es keinen für das Problem des Handlungsreisenden. Siehe Unterabschnitt 21.3.2, Seite 555. 16 Es kann auch einer von mehreren Aufträgen sein. 220 KAPITEL 6. ALGORITHMEN II: EFFIZIENZ UND KOMPLEXITÄT Lokale Suche und Heuristiken Lokale Suche wurde in Unterabschnitt 5.1.3, Seite 161, als eine Entwurfstechnik für Algorithmen eingeführt. Man kann sie auch bei schwierigen Problemen einsetzen. Allerdings ist dann in aller Regel der Gesamtsuchraum viel zu groß. Man findet häufig lokale Optima und diese sind wiederum nur in seltenen Fällen globale Optima. Man geht folgendermaßen vor: Von einem geeignet erscheinenden Anfangspunkt im Lösungsraum sucht man in dessen Nachbarschaft eine Lösung, die die Ziefunktion verbessert. Das macht man nach dem Gierprinzip und betrachtet die Lösung innerhalb der Nachbarschaft, die die größte Verbesserung liefert. In der Mathematik hat man für dieses Vorgehen den vornehmeren Namen steilster Abstieg17 (steepest descent) gewählt. Man spricht auch von einer Gradientensuche. Gibt es keine Verbesserung, so hat man ein lokales Optimum. Ist man damit nicht zufrieden, so kann man an einer anderen Stelle mit der lokalen Suche beginnen. Man kann aber auch versuchen, das lokale Optimum zu umgehen. Dafür sind eine Reihe von Heuristiken (heuristics) vorgeschlagen worden. Beispiele dafür sind: Tabusuche. Tabusuche (tabu search) versucht, sich zu merken, welche früheren Schritte nicht zum Erfolg geführt haben, und hält diese in einer Verbotsliste (Tabuliste) fest. Simulierte Abkühlung. Ähnlich wie Tabusuche ist Simulierte Abkühlung (simulated annealing) eine Vorgehensweise, die es erlaubt, lokale Optima wieder zu verlassen. Dafür nimmt man zufallsgesteuerte Verschlechterungen der Zielfunktion in Kauf. Genetische Algorithmen. Genetische Algorithmen (genetic algorithms) betrachten die Menge der Lösungen als eine Population, die es mit Vorgehensweisen aus der Genetik zu verbessern gilt. Ameisenalgorithmen Ameisen benutzen Duftstoffe (Pheronome) zur Markierung der Wege, die sie durchlaufen. Werden zwei unterschiedlich lange Wege zum gleichen Ziel benutzt, so wird nach einiger Zeit die Pheronomkonzentration auf dem kürzeren Weg größer sein als auf dem längeren. Nach diesem Prinzip können Ameisenalgorithmen (ant colony optimization, ACO) bei der Verbesserung von Lösungen aus kürzesten Wegen helfen. Randomisierte Algorithmen Bei randomisierten Algorithmen (randomized algorithm) werden in den Ablauf Zufallselemente eingebaut. Nimmt man dabei in Kauf, daß mit einer geringen Wahrscheinlichkeit 17 Wird ein Maximum der Zielfunktion gesucht, spricht man vom steilsten Anstieg. 6.3. NÄHERUNGSLÖSUNGEN SCHWIERIGER PROBLEME* 221 das Ergebnis falsch ist, so spricht man von einem Monte-Carlo-Algorithmus18 (Monte Carlo algorithm). Nimmt man in Kauf, daß der Algorithmus zwar stets das richtige Ergebnis liefert, jedoch unter Umständen sehr lange rechnet, so spricht man von einem Las-VegasAlgorithmus (Las Vegas algorithm). Weiter unten wollen wir uns anhand von Beispiel 6.4 einen Monte-Carlo-Algorithmus für ein einfaches Kommunikationsprotokoll ansehen. Als Beispiel für einen Las-Vegas-Algorithmus werden wir im Kapitel über Sortieren in Unterabschnitt 11.2.3, Seite 328, ein randomisiertes Quicksort vorstellen. Einige Anmerkunghen dazu, wie man zu den dafür gebrauchten Zufallszahlen kommt, sind in Abschitt B.5, Seite 626, in Anhang zu finden. Obwohl wir hier randomisierte Algorithmen als Methoden einführen, mit denen schwierige Probleme wenigstens ansatzweise gelöst werden können, ist bis jetzt kein randomisierter Algorithmus bekannt, der in polynomieller Zeit ein N P-vollständiges Problem löst, und man vermutet, daß es einen solchen unter der Voraussetzung N P = 6 P auch nicht gibt. Wozu braucht man dann randomisierte Algorithmen? Nun, nicht nur N P-vollständige Probleme sind schwer zu lösen. Das unten erläuterte Kommunikationsprotokoll ist ein Beispiel. Außerdem helfen randomisierte Algorithmen manchmal auch bei grundsätzlichen Problemen weiter, z. B. bei den auf Seite 215 genannten nicht-vollständigen Problemen aus N P. So waren randomisierte Primzahltests lange bekannt, ehe nachgewiesen wurde, daß das Problem in P liegt. Auch auf dem Gebiet der Graphisomorphie gib es Neues. Vor kurzem hat Schweitzer [Schw2009] in seiner Dissertation ein „Schraubenkasten“ genanntes randomisiertes Verfahren zur effizienten Lösung des Graphisomorphieproblems veröffentlicht. Beispiel 6.4 (Randomisiertes Kommunikationsprotokoll) Bei manchen Aufgabenstellungen ist es sinnvoll, einen Datenbestand mehrfach und an mehreren verschiedenen Orten zu halten. Idealerweise sollte an allen Orten exakt der gleiche Datenbestand vorhanden sein. In der Praxis wird das kaum zu erreichen sein, auch dann nicht, wenn nur an einer festen Stelle Änderungen vorgenommen werden und diese dann im Sinne einer Aktualisierung an den anderen Stellen „nachgezogen“ werden. Der Einfachheit halber wollen wir uns auf zwei Orte und zwei Datenbestände beschränken. Zu bestimmten Zeitpunkten wird es notwendig sein, zu überprüfen, ob die Inhalte beider Datenbestände identisch sind. Für das folgende ist es zweckmäßig, die Datenbestände als Folgen von Bits anzusehen, wobei ein Test auf Gleichheit nur notwendig ist, wenn beide Bitfolgen gleich lang sind. A = a1 , a2 , . . . , an und B = b1 , b2 , . . . , bn Wir wollen mit A und B auch die Orte bezeichnen, an denen sich die Datenbestände befinden. Bei einem sehr großen Datenbestand haben wir zum Beispiel 4 Terabytes Daten. Das sind 8 × 4 × 240 = 245 > 3 × 1013 Bits. Es ist also n ≥ 3 × 1013 . Nun weiß man, daß man mindestens n Bits von einer Stelle zur anderen transportieren muß, um 18 Die Bezeichnung wurde ursprünglich in der Numerik bennutzt. 222 KAPITEL 6. ALGORITHMEN II: EFFIZIENZ UND KOMPLEXITÄT mit Sicherheit Gleichheit der Datenbestände nachzuweisen. Das läßt sich beweisen. Dieser Transport ist sehr zeitaufwendig. Da er auch fehleranfällig ist, machen ihn Sicherheitsprotokolle noch aufwändiger. Eine randomisierte Form des Vergleichs erlaubt jedoch eine viel einfacherere Übertragung, allerdings auf Kosten einer gewissen Fehlerwahrscheinlichkeit. Der randomisierte Algorithmus benutzt Primzahlen. Mit P rim(n2 ) wollen wir die Menge 2 der Primzahlen bezeichnen, die kleiner als n2 sind. Man weiß, daß |P rim(n2 )| ∼ lnnn2 gilt. Der randomisierte Algorithmus arbeitet nun folgender maßen: 1. Man wählt mit gleicher Wahrscheinlichkeit unter den Primzahlen in P rim(n2 ) eine Zahl p aus. Wir gehen davon aus, daß bekannt ist, wie man das tut, und daß man das effizient machen kann. 2. Die Bitfolge in A betrachten wir als natürliche Zahl in Dualdarstellung und teilen sie durch p. Der Rest sei s. s := nummer(A) mod p s und p werden als Bitmuster betrachtet und zu B geschickt. 3. Nach dem Empfang von s und p wird in B t := nummer(B) mod p berechnet. Falls s = t, so ist die Ausgabe gleich. Falls s 6= t, so ist die Ausgabe ungleich. Der Algorithmus soll nun untersucht werden. Zunächst einmal der Kommunikationsaufwand: Es ist s < p und p < n2 . Um die Werte von s und p zu übertragen und dazu einige Verwaltungszeichen brauch man ungefähr 2 × dlg n2 e ∼ 4 ld n Bits. Für n = 245 sind das weniger als 200 Bits und die kann man ganz einfach übertragen. Als nächstes zur Fehlerbetrachtung: Sind die Datenbestände gleich, so meldet der Algprithmus gleich. Wenn die Datenbestände ungleich sind, kann es sein, daß der Algorithmus ebefalls die Antwort gleich gibt, und das ist ein Fehler. Wie häufig kann das passieren? Das passiert nur, wenn A 6= B, aber nummer(A) mod p = nummer(B) mod p. Sei r := nummer(A) mod p. Es ist nummer(A) = n1 p + r und nummer(B) = n2 p + r mit verschiedenen natürlichen Zahlen n1 und n2 . Es ist also |nummer(A) − nummer(B)| = |(n1 − n2 )|p. Eine falsche Antwort gibt der Algorithmnus demnach nur, wenn die gewählte Primzahl |nummer(A) − nummer(B)| teilt. Das wiederum ist nur möglich, wenn p eine der Primzahlen der (bis auf die Reihenfolge) eindeutig bestimmten Primfaktorenzerlegung |nummerA) − nummer(B)| = pi11 pi22 · · · pikk ist. Wir wollen zeigen, daß k ≤ n−1. Dazu stellen wir zunächst |nummer(A)−nummer(B)| < 2n fest, denn der Wert in Betragsstrichen laßt sich mit n Bits darstellen. Wäre nun k ≥ n, so wäre im Widerspruch hierzu pi11 pi22 · · · pikk ≥ p1 p2 · · · pn > 1 · 2 · 3 · · · n = n! > 2n 6.3. NÄHERUNGSLÖSUNGEN SCHWIERIGER PROBLEME* 223 Weil mit gleicher Wahrscheinlichkeit als p jede der Primzahlen aus {1, 2, 3, . . . n2 } gewählt werden kann, ist die Wahrscheinlichkeit eine der maximal n − 1 ungünstigen zu erwischen Pf ehler = n−1 ln n2 n−1 ∼ < |P rim(n2 | n2 / ln n2 n Für n = 1013 ergibt sich Pf ehler < 3 · 10−12 und das ist eine sehr kleine Wahrscheinlichkeit. 2 Aufgaben Aufgabe 6.1 Diese Aufgabe gehört zu Beispiel 3 in Unterabschnitt 6.2.2. Zeigen Sie: α 1. Es gilt nln(n) < 2n für α > 0 und alle hinreichend große n. α 2. Untersuchen Sie den Verlauf von 2n − nln(n) für einige Werte von α. Aufgabe 6.2 b. Beispiel 4: Geben Sie für ein festes k > 0 ein n0 an, so daß nk < n! für alle n ≥ n0 . Aufgabe 6.3 Zeigen Sie, daß 2n n von exponentieller Komplexität ist. Aufgabe 6.4 Geben Sie ein Beispiel für eine Klasse Hamiltonscher Graphen, bei der HTML eventuell exponentielle Laufzeit braucht, um einen Hamiltonkreis zu finden. Aufgabe 6.5 Geben Sie ein Beispiel für eine Klasse von Gaphen ohne Hamiltonkreis, bei der HMLT exponentielle Zeit braucht, um das festzustellen. Aufgabe 6.6 Wieso ist CON P eine echte Teilklasse von EX P? Literatur Zur Laufzeitanalyse von Algorithmen und Programmen siehe Kowalk ([Kowa1996], Seiten 455-461). Einen guten Einblick gibt auch Weiss ([Weis1995], Kapitel 2). Ausführliche Darstellungen findet man in Kapitel 3 des Buches von Aho/Ullman [AhoU1995] und in Kapitel 4 des Buches Cormen/Leiserson/Rivest [CormLR1990]. Spezialwerke sind Sedgewick/Flajolet [SedgF1996] und Gonnet/Baeza-Yates [GonnB1991]. Eine wahre Fundgrube sind die Bücher von Knuth ([Knut1997], [Knut1998], [Knut1998a]). Hingewiesen sei auch auf Greene/Knuth [GreeK1981], insbesondere aber auf das mit viel didaktischem Einfühlungsvermögen geschriebene Buch Graham/Knuth/Patashnik [GrahKP1994]. 224 KAPITEL 6. ALGORITHMEN II: EFFIZIENZ UND KOMPLEXITÄT Für Rekurrenzen und erzeugende Funktionen sei außer auf das schon genannte Buch Cormen/Leiserson/Rivest [CormLR1990] auf Knuth [Knut1997], insbesondere aber auf Petkovs̆ek/Wilf/Zeilberger [PetkWZ1997] und Wilf [Wilf1990] hingewiesen. Eine knappe, aber lesenswerte Einführung in Algorithmen und ihre Komplexität findet man in dem Buch „Applied and Algorithmic Graph Theory“ von Chartrand/Oellermann [CharO1993]. Ausführlichere Darstellungen von N P-Vollständigkeit sind in Aho/Hoprcroft/ Ullman [AhoHU1974] und in Büchern über Theoretische Informatik, z. B. Blum [Blum1998], zu finden. Ein Standardwerk über N P-Vollständigkeit ist [GareJ1979]. Es entält eine umfangreiche Liste N P-vollständiger Probleme. Wenn man sich nicht sicher ist, ob ein schwieriges Problem vorliegt oder nicht, sollte man immer zunächst in diesem Buch nachsehen. Zu Algorithmen, speziell zu Fragen der Komplexität und der Theorie der Algorithmen, existiert auch eine Reihe von Handbüchern. Genannt seien Gonnet/Baeza-Yates [GonnB1991], von Leeuwen [Leeu1990] und Atallah [Atal1999]. Literatur zu Näherunsglösungen schwieriger Probleme Als Gesamtübersicht sei auf die schon erwähnten Bücher von Hromkovič [Hrom2001] und [Hrom2007] hingewiesen. Für pseudopolynomielle Algorithmen siehe Garey/Johnson [GareJ1979]. Die in Beispiel 6.3 vorgestellte Approximation von optimalen Vergabeplänen durch Listenpläne stammt von Graham [Grah1966] und war wahrscheinlich die erste exakte Abschätzung der Güte eines Approximationsalgorithmus. Sie wurde eigentlich für Anomalien bei Listenplänen entwickelt. Siehe hierzu Kapitel 3 in Coffman/Denning [CoffD1973]. Zu Approximationsalgorithmen siehe auch Kapitel 37 in Cormen/Leiserson/ Rivest [CormLR1990], sowie die spezialisierten Werke von Vazirani [Vazi2001] und Hochbaum [Hoch1997]. Zur lokalen Suche und zu Heuristiken gibt es eine größere Zahlvon Büchern: Rego/Alidaee [RegoA2005], Rayward-Smith/ Osman/Reeves/Smith [RaywORS1996], Glover/Kochenberger [GlovK2003] und weitere. Zur stochastischen Suche siehe man Spall [Spal2003]. Randomisierte Algorithmen sind in Motwani/Raghavan [MotwR1995] beschrieben. Teil III Einfache Datenstrukturen 225 Kapitel 7 Allgemeines zu Datenstrukturen 7.1 Sätze und Vetretersätze In Abschnitt 2.4, Seite 37, wurden Datenstrukturen (data structure) als Datentypen eingeführt, die weder direkt in der Hardware noch direkt in der Programmiersprache definiert sind.1 Sie müssen durch zusätzliche Programme oder Programmteile realisiert werden. Einfache Datenstrukturen wie z. B. Listen (Kapitel 8) oder Suchbäume (Kapitel 9) werden in diesem Teil des Buches behandelt. Teil IV ist komplexeren Datenstrukturen, nämlich Allgemeinen Graphen gewidmet. Diesen und anderen Datenstrukturen ist gemeinsam, daß sie aus Sätzen bestehen. Zu Sätzen und Feldern siehe Unterabschnitt 2.6.4, Seite 68. Wir wollen uns auch weiterhin auf den Fall beschränken, Sätze und daraus gebildete Datenstrukturen nur innerhalb eines Programmlaufes zu betrachten. Auf Sätze und Datenstrukturen in Dateien und Datenbanken wird nur in Abschnitt 9.4 kurz eingegangen. Die Überlegungen gelten jedoch weitgehend auch für diese. Für Datenstrukturen muß die Frage „Was ist ein Satz“? etwas genauer untersucht werden. Die entscheidende Frage ist „Was identfiziert einen Satz innerhalb einer konkreten Datentruktur?“ Ein Satz ist ein Bitmuster eines bestimmten Aufbaus, das an einer definierten Stelle des Adreßraumes des Programmlaufs steht. Ein Bitmuster gleichen Aufbaus an einer anderen Stelle des Adreßraumes ist ein anderer Satz. C und andere Programmiersprachen sorgen nicht dafür, daß die Bitmuster, d.h. die Inhalte, verschieder Sätze auch verschieden sind. Das wäre auch nicht sinnvoll. Allerdings gewährleisten sie, daß sich die Bitmuster verschiedener Sätze nicht überlappen. Es gibt explizit aufzurufende Ausnahmen, die sich bei guter Programmierung fast immer vermeiden lassen. Der Aufbau eines Satzes aus Feldern wird durch den Satztyp (Satzklasse, record type, record class) bestimmt. Einfache Datenstrukturen wie z. B. Listen bestehen i. A. aus Sätzen einer einzigen Satzklasse. Streng genommen, muß man zwischen einer Datenstruktur als Datentyp und einer konkret vorliegenden Realisierung unterscheiden. Wir wollen das nicht tun. Aus dem Zusammenhang wird klar werden, was gemeint ist. 1 227 228 KAPITEL 7. ALLGEMEINES ZU DATENSTRUKTUREN Komplexere Datenstrukturen wie z. B. Graphen (siehe Kapitel 13) können durchaus aus Sätzen unterschiedlichen Typs gebildet sein. Was ist zu tun, wenn nun ein und der gleiche Satz in einer Datenstruktur mehrfach auftreten soll? Als Beispiel nehmen wir im Vorgriff auf Unterabschnitt 8.3.1 an, daß eine verkettete Liste vorliegt. Abbildung 8.1 zeigt die Verkettung mit Hilfe von Adreßfeldern. Soll in dieser Liste ein Satz mehrfach auftreten, so funktioniert die Lösung nicht, denn in ein und dem gleichen Adreßfeld können nicht mehre Verweise eingetragen sein. Man hilft sich, indem man Vertretersätze (Zeigersatz, reference record) einführt. Das sind Sätze, die für den eigentlichen Satz in der Kette stehen. Abbildung 7.1 zeigt zwei Vertretersätze eines ◦............................................................................................... ◦............................................................................................... ◦............................................................................................... ◦................................................................................................ • . . .. ...........................................................................................◦ ... .........................................................................................◦ .. ...........................................................................................◦ .. • .............................................................................................◦... .. . . ..... ........ .................. .......... .. ...... .......... ..... ...... .......... . . . . ...... . . . . .. ...... .......... ...... .......... ...... .......... ...... .......... ...... ......... . . . . . . . ...... . . .. ...... .......... ...... .......... ...... ......... ...... .......... ...... ......... . . . . . . . . . . ...... ......... ...... ......... ...... .......... ...... . .......... ........ . ........... ................................................. Abbildung 7.1: Vertretersätze in eine Liste Satzes in einer Liste. Auf die gleiche Art kann ein Satz durch Vertretersätze in verschiedene Ketten eingefügt werden. Vertretersätze sind nicht nur für Verkettungen notwendig und nützlich, sondern auch an anderen Stellen, z. B. bei Listenrealisierung durch Reihungen. Ein Nachteil von Vertretersätzen ist, daß es in einem Satz i. a. keine Rückverweise auf seine Vertretersätze gibt. 7.2 Schlüssel Häufig haben die Sätze eines Klasse ein Feld2 , dessen Inhalt den Satz eindeutig kennzeichnet. Man spricht von einem Schlüsselfeld (key field) oder kurz von einem Schlüssel. Die Werte, die in einem Schlüsselfeld auftreten können, bilden die Schüsselwertmenge (Schlüsselwertbereich, Schlüsselwertraum). Diese kann sehr groß sein, z. B. ist die Menge der Zeichenreihen der Länge mindestens 15 bestehend aus Großbuchstaben größer als 2615 . In vielen Fällen ist die Zahl der Schlüsselwerte, die in einer gegebenen Datenstruktur real auftreten, sehr viel kleiner. Oft, aber nicht immer, sind Schlüsselwertmengen linear 2 Unter Umständen kann das auch eine Kombination aus mehreren Feldern sein. 7.2. SCHLÜSSEL 229 geordnet, z. B. lexikographisch. In der Regel wird dann die Ordnung für den Aufbau und die Bearbeitung von Datenstrukturen benutzt. Bei den getroffenen Annahmen ist es Aufgabe des Programmlaufes3, dafür zu sorgen, daß bei Datenstrukturen mit einem Schlüsselfeld ein Schlüsselwert höchstens einmal auftritt. Wenn es kein identifizierendes Schlüsselfeld gibt, kann es sein, daß sich zwei verschiedene Sätze einer Klasse nur durch ihre Adressen, aber nicht durch ihre Inhalte unterscheiden. Natürlich können zwei analog aufgebaute konkrete Datenstrukturen, z. B. zwei Listen, zwei verschiedene Sätze mit identischen Schlüsselwerten enthalten. Des öfteren werden in Datenstrukturen auch Felder einer Satzklasse benutzt, die keine Schlüsselfelder sind, deren Inhalte einen Satz also nicht eindeutig identifizieren. In Personalsätzen ist das z. B. das Feld Nachname. Man spricht von Sekundärschlüsseln (secondary key) und nennt in diesem Fall zur Betonung des Unterschieds identifizierende Schlüssel häufig auch Primärschlüssel (primary key) Auch Sekundärschlüssel haben einen (möglicherweise linear geordneten) Schlüsselwertbereich und auch sie werden zum Aufbau und zur Bearbeitung von Datenstrukturen benutzt. Wir wollen uns allerdings in den folgenden Kapiteln auf Primärschlüssel beschränken. Auf Schlüssel soll noch etwas genauer eingegangen werden. Die Menge der möglichen Schlüsselwerte, der Schlüsselwertraum, soll mit K bezeichnet werden. k := |K| ist die Anzahl der möglichen Schlüsselwerte. Die Menge der in einer Datenstrutur wirklich vorkommenden Schlüsslewerte soll R heißen. Manchmal ist sie während der Bearbeitung konstant. Ist sie das nicht, wird sie im Allgemeinen während der Bearbeitung wachsen, gelegentlich auch schrumpfen. Die Anzahl real vorkommender Schlüsselwerte ist r := |R|. Nicht selten ist k >> r. Als dritte Menge wollen wir S einführen. Das ist die Menge von Plätzen, die die Sätze der Datenstruktur im Speicher (Adreßraum) belegen. Ihre Anzahl ist s := |S|. Für Primärschlüssel gilt natürlich s = r. Bei Sekundärschlüsseln gilt i. A. s > r. Wir nehmen weiter an, daß auf K eine lineare Ordnung gegeben ist. Schlüssel werden in Datenstrukturen nicht nur zur eindeutigen Identifizierung von Sätzen benutzt, sondern auf vielfältige Weise auch zum Aufbau von Datenstrukturen und zur Implementierung von Operationen auf diesen. Die folgenden Kapitel zeigen viele Beispiele dafür. Für die Operationen werden jeweils Effizienzbetrachtungen durchgeführt und dabei die in Kapitel 1 eingeführten Größen bestimmt: Für den schlechtesten Fall (WOC), für den besten Fall (BEC) und für den mittleren Fall (AVC). Bei gegebenem Schlüsselwertraum K gilt WOC für alle R einer gegebenen Datenstruktur – z. B. für alle Suchbäume – und hängt nur von Umfang r ab. Um Aussagen für den mittleren Fall AVC machen zu können, müssen Annahmen über die Häufigkeit, mit der Schlüsselwerte auftreten, gemacht werden. Dazu betrachten wir an dieser Stelle nur die wichtigste Operation: SEARCH. Dabei wird ein Schlüsselwert s aus K angegeben und in der Datentruktur der Satz mit diesem Schlüsselwert gesucht. Wir nehmen zunächst einmal an, daß es einen Satz mit 3 D.h. des Programms, das Datenstrukturen aufbaut und verwaltet. 230 KAPITEL 7. ALLGEMEINES ZU DATENSTRUKTUREN diesem Schlüsselwert in der Datenstruktur gibt. Wir nehmen weiter an, daß für jeden Schlüsselwert s ∈ R die Wahrscheinlichkeit ps , daß dieser Wert gesucht wird, bekannt ist und daß ebenso der Aufwand ts , den Satz mit dem Schlüssel s in der Datentruktur zu finden, angegeben werden kann. Dann wird man für diesen Fall sinnvollerweise P die mittlere 4 Komplexität durch den Erwartungswert definieren, also AV C := E(ts ) = ps ts . Nur in s∈R seltenen Fällen wird in der Praxis die Wahrscheinlichkeit ps bekannt sein. Wie in solchen Fällen üblich, nimmt man dann an, daß alle Schlüsselwerte die gleiche Wahrscheinlichkeit P 1 = 1r . Das ergibt ACV = 1r ts . haben, nämlich ps = |R| s∈R Wenn s ein Schlüsselwert ist, zu dem kein Satz in der Datenstruktur existiert, wird es schwieriger. Die Feststellung „Es gibt in der Datestruktur keinen Satz mit Schlüsselwert s“ erfordert bei vielen Datenstrukturen die Bestimmung der zu s gehörenden Lücke. Unter der Voraussetzung R 6= ∅, also daß in der Datenstruktur Sätze existieren, wird s in eine Lücke (gap) in K fallen, die durch zwei, eventuell auch nur durch einen Schlüsselwert aus R begrenzt ist. Welche Lücken es gibt und wie groß sie sind, wird durch R bestimmt und ist für verschiedene R sehr unterschiedlich. In der Praxis wird es i. A. so sein, daß eine Operation SEARCH deutlich häufiger mit einem existierenden Schlüsselwert auftritt als mit einem, der nicht in der Datenstruktur vorkommt. Wird angenommen, daß letztere mit gleicher Wahrscheinlichkeit auftreten (was auch nicht sehr praxisgerecht ist), so ist die Wahrscheinlichkeit einer Lücke proportional zu ihrer Größe. Auf keinen Fall ist es naheliegend und plausibel anzunehmen, daß alle Lücken mit der gleichen Wahrscheinlichkeit auftreten. 4 Zu den Grundbegriffen der Wahrscheinlichkeitstheorie siehe Kapitel B, Seite 619, im Anhang. Kapitel 8 Listen 8.1 8.1.1 Terminologie und Grundlagen Definition und Beispiele Listen sind die einfachsten und wohl auch die häufigsten Datenstrukturen in der Informatik. Definition 8.1 Eine Liste (list) ist leer oder eine endliche Folge von Elementen. Diese Definition ist sehr allgemein. Wir wollen zusätzlich verlangen, daß alle Elemente vom gleichen Datentyp sind. Wir wollen die Elemente von Listen Sätze nennen, auch wenn es sich um Werte eines elementaren Datentyps handelt. Zu Sätzen und Feldern siehe Unterabschnitt 2.6.4, Seite 68, insbesondere siehe auch Kapitel 7. Als endliche Folgen sind Listen Abbildungen eines Anfangsstücks der natürlichen Zahlen in die Menge der Listenelemente (vgl. Abschnitt A.3 im Anhang). Die Anzahl Zahlen im Anfangsstück ist die Länge der Liste. Die leere Liste hat die Länge 0. Für Listen gibt es unterschiedliche Schreibweisen. Wir wollen Listen in runde Klammern setzen: (a0 , a1 , . . . , an ) oder (K1,K2,. . ., KM). Gelegentlich werden die Klammern auch weggelassen. Beispiele a. (2, 3, 5, 7, 11, 13, 17, 19) Die ersten 8 Primzahlen. b. (Helium, Neon, Argon, Krypton, Xenon, Radon) Die Edelgase in aufsteigender Reihenfolge der Ordnungszahlen. c. (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) Die Monatslängen in Nicht-Schaltjahren. d. Die Zeichen einer Zeichenreihe bilden eine Liste, ebenso die Zeilen eines Textes. 231 232 KAPITEL 8. LISTEN e. (0.00, (-1.15, (1.00, Eine Liste 0.00, 0.00, 0.00, 0.00) 2.89, 0.00) 1.00, 1.00, 7.00, 1.00) von variabel langen Zahlenlisten. f. Bibliothekskartei. Der Buchbestand einer kleinen Privtabiliothek sei auf Karteikarten erfaßt. Diese seien nach Autoren geordnet und mögen außer den Autorennamen noch den Titel und weitere Angaben enthalten. Jede Karte kann als Satz einer Liste aufgefaßt werden. Dieses Beispiel zeigt, daß Listen nicht nur in der Informatik und erst recht nicht nur in Programmen vorkommen. Wir wollen allerdings im weiteren Listen und andere Datenstrukturen immer als programminterne Konstrukte vertehen. 2 Eine Teilliste (Unterliste, Teilfolge) ist eine Liste, die einige Elemente der ursprünglichen Liste in der gleichen Reihenfolge enthält. Eine Teilliste, die Anfangsstück einer gegebenen Liste ist, heißt Präfix. Ist sie Endstück, wird sie Suffix genannt. Das erste Element einer Liste heißt Kopf (head). Die verbleibende Liste wird als Restliste (tail) bezeichnet. 8.1.2 Operationen mit Listen Listenoperationen legen fest, wie man Listen aufbaut, d. h. verlängert oder verkürzt, und wie man in einer Liste etwas findet. Listen mit positionsabhängigen Operationen Als Bilder eines Anfangsstücks der natürlichen Zahlen weisen die Elemente einer Liste eine (strikte) lineare Ordnung auf. Ein Element der Liste wird durch seine Position indentifiziert. Die Zählung beginnt bei 0 . Dabei ist es durchaus möglich und oft auch sinnvoll, daß ein und das gleiche Element an mehreren Stellen der Liste steht. Achtung: Oft meint man damit, das der gleiche Wert, z. B. eine Zahl, an verschiedenen Stellen der Liste steht. Nach den Festlegungen in Abschnitt 7.1, Seite 227, sind das dann verschiedene Sätze. Ist wirklich der gleiche Satz gemeint, so werden i. a. Vertretersätze benutzt. Wir wollen in Listen einzelne Sätze suchen (finden, search, find), einfügen (insert) und löschen (entfernen, delete, remove). Darüber hinaus soll es möglich sein, die Listenelemente nacheinander in einem Listendurchlauf (traversing a list) zu bearbeiten. Dazu müssen wir auf den Nachfolger (successor) bzw. den Vorgänger (predecessor) eine Satzes zugreifen können. Zu diesem Zweck führt man bei der Bearbeitung einer Liste einen Aktualitätszeiger (current pointer). Dieser wird bei Such- und Einfügeoperationen gesetzt. Bei der Bearbeitung von Listen setzen wir voraus, daß es eine Beschreibung für die Liste als Ganzes gibt (Listenbeschreibung, list description) Darin gibt es einen Verweis auf das erste und auf das letzte Element der Liste und eine Angabe über ihre Länge. Außerdem gibt es einen Verweis auf das aktuelle Listenelement, den Aktualitätszeiger. Diese Angaben 8.1. TERMINOLOGIE UND GRUNDLAGEN 233 werden bei Listenveränderungen auf dem Laufenden gehalten und können unhabhängig vom Umfang der Liste in einer Operation abgefragt werden. Wir betrachten die folgenden positionsabhängigen Operationen: 1. SIZE Als Ergebnis wird die Länge n der Liste zurückgeliefert, 0 wenn die Liste leer ist. n − 1 ist die höchste Position in der Liste. 2. SEARCH Es wird das Element der Liste, das an einer gegebenen Position steht, gesucht und bereitgestellt. Ist die Liste leer oder die angegebene Position zu groß, so wird ein Nullverweis (NIL) bereitgestellt. 3. INSERT Die Liste wird um ein Element vergrößert. Es wird eine Position angegeben und das Element an dieser Stelle in die Liste eingefügt. Dahinterstehende Listenelemente werden in der Position um 1 nach hinten verschoben. Fehlermeldung, falls die angegebene Position negativ oder um mehr als 1 größer als die letzte Position der Liste ist. 4. DELETE Die Liste wird um ein Element verkürzt. Es wird eine Position angegeben und das Element an dieser Stelle aus der Liste entfernt, es wird gelöscht1 . Dahinterstehende Listenelemente werden in der Position um 1 nach vorn verschoben. Fehlermeldung, falls die angegebene Position nicht in der Liste vorhanden ist. 5. NEXT Ausgehend vom aktuellen Listenelement (Aktualitätszeiger) wird das darauffolgende Listenelement bereitgestellt. Der Aktualitätszeiger wird weitergeschaltet. Fehlermeldung, falls der Aktualitätszeiger undefiniert ist oder kein nächstes Element existiert. 6. PREVIOUS Ausgehend vom aktuellen Listenelement (Aktualitätszeiger) wird das vorangehende Listenelement bereitgestellt. Der Aktualitätszeiger wird zurückgeschaltet. Fehlermeldung, falls der Aktualitätszeiger undefiniert ist oder kein vorangehendes Element existiert. Mit diesen Operationen lassen sich leicht weitere Operationen, die ganze Listen bearbeiten, aufbauen. Man kann eine Liste als Teilliste in eine andere Liste einfügen, als Spezialfall Man spricht von „Löschen“, obwohl im allgemeinen der Satz nach dem Entfernen aus der Liste durchaus weiter existiert und z.B. an einer anderen Stelle wieder eingefügt werden kann. Die Bezeichnung remove wäre geeigneter. 1 234 KAPITEL 8. LISTEN (Konkatenation) kann eine Liste an eine andere angefügt werden. Eine Liste kann in Teillisten zerlegt werden. Eine Teilliste kann gelöscht werden. Listen mit schlüsselwertabhängigen Operationen Wir nehmen an, daß die Sätze in der Liste ein identifizierendes Schlüsselfeld aufweisen und daß die Schlüssewertmenge linear geordnet ist. In diesem Fall kann die Liste in aufsteigedner Reihefolge der Schlüsselwerte aufgebaut und diese Werte statt der Listenpostion zum Suchen der Sätze benutzt werden. Das gilt auch dann, wenn nachfolgende Einfügungen und Löschungen die Position der Elemente in der Liste verändert haben. Gelegentlich ist es vorteilhaft, in der Liste die entgegengesetze Ordung wiederzugeben. Man spricht von aufsteigend bzw. absteigend geordneten (sortierten) Listen. Auch bei der Benutzung von Schlüsselwerten sollen Sätze gesucht, eingefügt und gelöscht werden können. Außerdem wird auch hier ein Aktualitätszeiger geführt. Wir führen die folgenden schlüsselwertabhängigen Operationen ein und benutzen dabei auch wieder eine Listenbeschreibung. 1. SIZE Als Ergebnis wird die Länge n der Liste zurückgeliefert, 0 wenn die Liste leer ist. 2. SEARCH Es wird ein Element der Liste nach seinem Schlüsselwert gesucht und bereitgestellt. Wird das Element nicht gefunden, so wird ein Nullverweis (NIL) zurückgeliefert. 3. FIND Entspricht SEARCH. Ist ein Satz mit dem gegebenen Schlüsselwert nicht in der Liste und dieser kleiner als der größte und größer als der kleinste existierende Schlüsselwert, so wird der Satz mit dem nächstkleineren Schlüsselwert bereitgestellt. Anderfalls wird ein Nullverweis zurückgeliefert2 . 4. INSERT Es wird ein weiteres Element in die Liste eingefügt. Das geschieht an der Stelle der Liste, die dem Schlüsselwert des einzufügenden Satzes (in aufsteigender Sortierung) entspricht. Ist ein Satz mit gleichem Schlüsselwert in der Liste schon vorhanden, so erfolgt eine Fehlermeldung. 5. DELETE Es wird das Listenelement mit dem gegebenen Schlüsselwert gesucht und aus der Liste der entfernt, es wird gelöscht. Existiert kein Element mit diesem Schüsselwert, so erfolgt eine Fehlermeldung. Üblicherweise werden die Begriffe SEARCH und FIND synonym benutzt. Die hier gewählte Form der zusätzlichen Positionierung für FIND ist nützlich, aber selten. 2 8.2. BINÄRSUCHE 235 6. MIN Es wird das Listenelement mit dem kleinsten Schlüsselwert, also das erste Element der Liste zurückgeliefert. Fehlermeldung, falls Liste leer ist. 7. MAX Es wird das Listenelement mit dem größten Schlüsselwert, also das letzte Element der Liste zurückgeliefert. Fehlermeldung, falls Liste leer ist. 8. NEXT Ausgehend vom aktuellen Schlüsselwert (Aktualitätszeiger) wird das Listenelement mit nächstgrößerem Schlüsselwert gesucht und zurückgeliefert. Der Aktualitätszeiger wird weitergeschaltet. Fehlermeldung, falls es keine Listelemente mit größeren Schlüsselwerten gibt. 9. PREVIOUS Ausgehend vom aktuellen Schlüsselwert (Aktualitätszeiger) wird das Listenelement mit nächstkleinerem Schlüsselwert gesucht und zurückgeliefert. Der Aktualitätszeiger wird zurückgeschaltet. Fehlermeldung, falls es keine Listelemente mit kleineren Schlüsselwerten gibt. In Abschnitt 8.3 soll untersucht werden, wie sich Listen und Operationen mit ihnen realisieren lassen. Werden Listen als Reihungen aufgebaut, so kann mit einer speziellen Technik, der Binärsuche, in ihnen sehr effizient gesucht werden. Ihre Bedeutung wegen wird Binärsuche vorab in einem eigenen Abschnitt vorgestellt. 8.2 Binärsuche Es sei eine Reihung (siehe 2.6.1) von Werten eines elementaren Typs gegeben, z. B. ganze Zahlen oder Zeichenreihen. Für die Werte gebe es eine lineare Ordnung und die Werte seien in der Reihung in aufsteigender Reihenfolge gespeichert. Duplikate wollen wir nicht zulassen, siehe jedoch Anmerkung 8.1. Binärsuche (binary search) ist ein Verfahren, mit dem in einer solchen Reihung ein Element mit einem gegebenen Wert sehr effizient gefunden werden kann. Gibt es kein Element mit diesem Wert, so wird auch das sehr effizient festgestellt. Die Idee ist einfach: Wir suchen das mittlere Element der Reihung und vergleichen seinen Wert mit dem Suchwert. Bei Gleichheit haben wir das gesuchte Element gefunden. Ist der Suchwert kleiner, wird in der ersten Reihungshälfte weitergesucht, ist er größer, in der zweiten. Das Verfahren endet, wenn die Teilreihung, in der weitergesucht werden soll, die Länge 0 oder 1 hat. Im ersten Fall gibt es keinen Eintrag mit dem gegebenen Suchwert. Im zweiten Fall genügt ein weiterer Vergleich, um das gesuchte Reihungselement zu finden oder festzustellen, daß es ein solches nicht gibt. Schließlich muß 236 KAPITEL 8. LISTEN noch eine Vereinbarung getroffen werden, welcher von zwei Kandidaten das mittlere Element einer Reihung gerader Länge ist. Wir wollen uns auf das Element mit kleinerem Index festlegen. Beispiel 8.1 Die Werte SEHEN, DER, RUHEN, HAT, ANTON, ZAHN, TANNE, BESUCH, HUT, KNABE, LAGE, ROT, QUARK, PFAU, NOCH, NEIN, LUST, ZANK seien in einer Reihung in aufsteigender Reihenfolge der Werte gespeichert. Siehe Tabelle 8.1. Wir wollen mit Binärsuche den Wert 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ANTON BESUCH DER HAT HUT KNABE LAGE LUST NEIN NOCH PFAU QUARK ROT RUHEN SEHEN TANNE ZAHN ZANK Tabelle 8.1: Alphabetisch geordnete Liste als Reihung TANNE suchen. Es ergibt sich der in Tabelle 8.2 dargestellte Ablauf. Suchen wir den Wert TONNE, so erbgibt sich der Ablauf von Tabelle 8.3. Das Suchen von TANK können wir in Tabelle 8.4 verfolgen. 2 Indexintervall [0, 17] [9, 17] [14, 17] Mittelposition 8 13 15 Wert NEIN RUHEN TANNE Vergleich größer größer gleich Tabelle 8.2: Binärsuche des Wertes TANNE 8.2. BINÄRSUCHE 237 Indexintervall [0, 17] [9, 17] [14, 17] [16, 17] leer Mittelposition 8 13 15 16 Wert NEIN RUHEN TANNE ZAHN Vergleich größer größer größer kleiner nicht gefunden Tabelle 8.3: Binärsuche des Wertes TONNE Indexintervall [0, 17] [9, 17] [14, 17] [14, 14] Mittelposition 8 13 15 14 Wert NEIN RUHEN TANNE SEHEN Vergleich größer größer kleiner ungleich nicht gefunden Tabelle 8.4: Binärsuche des Wertes TANK In Tabelle 8.5 ist Binärsuche als Unterprogramm in C angegeben. Das Programm erwartet als Eingabe die Indexwerte low und high und den zu suchenden Wert compare. Die Reihung, in der zu suchen ist, ist als globaler Parameter array einzurichten. Der Einfachheit halber sind die Werte ganze Zahlen. Es ist einfach, das Programm so abzuändern, daß andere Werte, z. B. Zeichenreihen, verwendet werden. Als Ergebnis liefert das Programm -1, wenn der gesuchte Wert nicht in der Reihung auftritt, und den Index in der Reihung anderenfalls. Das Programm arbeitet rekursiv. Die Rekursion endet in einer der Anweisungen 1, 2 oder 9. Es ist Aufgabe des Rahmenprogramms, das BINSEARCH zum ersten Mal aufruft, die Reihung array richtig einzurichten und für die Parameter low und high die richtigen Anfangswerte zu setzen. Binärsuche ist ein einfaches und leistungsfähiges Verfahren, dessen Programmierung jedoch Tücken aufweisen kann. Nicht zu Unrecht steht im englischsprachlichen Wikipedia3 : Binary search is one of the trickiest “simple” algorithms to program correctly. Die Lektüre der dort zitierten Anmerkungen von Bently [Bent2000], Seite 34, und Kruse [KrusTL1997], Seite 280, wird sehr empfohlen. Von besonderer Bedeutung sind Fehler, die in neuerer Zeit gefunden wurden und die erst sichtbar wurden, als in der Praxis extrem große Reihungen durchsucht werden mußten. Die Fehler ergeben sich aus Überläufen des Zahlenbereichs für Indizes. Im Programm BINSEARCH von Tabelle 8.5 wird in Zeile 8 des mittlere Element einer Liste mit med = low + (high - low) /2 berechnet. Es wäre näherliegend 3 http://en.wikipedia.org/wiki/Binary_search 238 ' & KAPITEL 8. LISTEN int { 1 2 3 4 5 6 7 8 9 10 11 12 13 } BINSEARCH (int low, int high, int compare) $ if (high < low) return -1; if (high == low) { if( array[high] == compare) { return high; } else { return -1; } } med = low + (high - low) / 2; if (array[med] == compare) return med; if (array[med] < compare) { BINSEARCH (low, med - 1);} else { BINSEARCH (med + 1, high);} % Tabelle 8.5: Programm zur Binärsuche in einer Tabelle und einfacher, die Berechnung mit (low + high) / 2 auszuführen, und so hat man es auch jahrelang gemacht. Mathematisch sind beide Rechnungen gleichwertig. Im endlichen Bereich von Darstellungen ganzer Zahlen durch Bitmuster (Abschnitt 3.3, Seite 101) ist das aber nicht der Fall. Das Zwischenergebnis low + high kann aus dem darstellbaren Bereich hinausführen und unübersichtliche Folgefehler erzeugen4 . Sind low und high korrekte Darstellungen nichtnegativer Zahlen und gilt low < high, so bleiben in der Anweisung 8 alle Zwischenergebnisse und das Endergebnis im darstellbaren Bereich und sind nichtnegativ. Komplexität der Binärsuche Wir nehmen an, es liege eine Reihung der Länge n vor mit aufsteigend geordneten Werten. Duplikate sollen nicht vorkommen. Wir wollen die Komplexitätswerte W OC(n), BEC(n) und AV G(n), gemessen in der Anzahl Aufrufe von BINSEARCH, bestimmen. Es ist BEC(n) = 1, denn man kann mit Glück den gesuchten Wert beim ersten Zugriff finden. Berechnung von W OC(n). Dafür kann die Anzahl Schritte genommen werden, die es 4 http://googleresearch.blogspot.com/2006/06/extra-extra-read-all-about-it-nearly.html 8.3. REALISIERUNG VON LISTEN 239 braucht, um das letzte Element der Reihung zu finden. Mit 1 weiteren Schritt kann dann entschieden werden, ob der vorgegebene Wert gefunden wurde oder nicht in der Reihung vorkommt. Das letzte Element der Reihung ist erst bei einem Intervall der Länge 1 das mittlere Element und bei jedem Halbieren wird mit der größeren Hälfte fortgefahren. Sei nun k durch 2k−1 < n ≤ 2k eindeutig bestimmt. In diesem Fall kann das letzte Element der Reihung nicht nach k − 1 Teilungen in einem Intervall der Länge 1 liegen (warum?). Es werden also mindesten k Schritte zum Finden des Elementes benötigt. Andererseits ist man spätestens nach k Halbierungen immer bei einem Intervall der Länge 1. Wir haben gezeigt W OC(n) = dld (n)e + 1 d. h. W OC(n) = Θ(ln(n)) (8.1) Für die mittlere Komplexität AV C(n) folgt aus Gleichung 8.1 AV C(n) = O(ln(n). Setzt man voraus, daß alle Werte mit gleicher Wahrscheinlichkeit gesucht werden, so gilt sogar AV C(n) = Θ(ln(n)). Das soll hier nicht bewiesen werden. Anmerkung 8.1 a. Binärsuche liefert auch ein Ergebnis für Werte, die nicht in der Reihung vorhanden sind. Es ist leicht zu sehen, daß auch hierfür Gleichung 8.1 gilt. b. Die obige Berechnung der Komplexität gilt nicht mehr, wenn Duplikate zugelassen werden. Das sieht man leicht für den Extremfall, daß alle Reihungselemente den gleichen Wert haben. Im allgemeinen Fall bilden die Duplikate, also die Reihenelemente mit gleichem Schlüssel, ein zusammenhängendes Teilintervall der Reihung. Es ist klar, daß die Anzahl Schritte, mit Binärsuche ein solches Teilintervall zu treffen, im schlechtesten Fall nicht größer sein, kann als die Anzahl Schritte, die man braucht, um einen nur einmal vorkommenden Wert zu finden. Genauer soll das hier nicht untersucht werden. 2 8.3 8.3.1 Realisierung von Listen Verkettete Listen Es ist naheliegend, Listen dadurch zu realisieren, daß man in jedem Element einen Zeiger (pointer) speichert, der auf das nachfolgende Element zeigt. Im Englischen wird oft von einem link gesprochen und verkettete Listen heißen linked lists. Es ist oft sinnvoll, doppelt verkette Listen (doubly linked lists) zu benutzen. In dieser hat jedes Element einen zusätzlichen Zeiger auf das vorangehede Element der Liste. Abbildung 8.1 zeigt eine schematische Darstellung. Jeder Satz hat zwei Verweisfelder, schwarze Kreise zeigen NIL-Verweise an. Diese Verkettung ist nicht möglich, wenn ein Satz mehrfach in einer Kette vorkommt oder in mehr als eine Kette eingefügt werden soll. Die Adreßfelder in dem Satz können nur einmal belegt werden. Als Lösung bieten sich die schon in Abschnitt 7.1 eingeführten Vertretersätze an. Wenn die Ketten, in die ein Satz eingefügt werden soll, vorher bekannt sind und der Satz in jeder dieser Ketten höchstens einmal auftritt, kann man an Stelle von Vertretersätzen auch mehrfache Verkettungsfelder benutzen. Abbildung 8.2 zeigt einen 240 KAPITEL 8. LISTEN ◦.............................................................................................. ◦............................................................................................... ◦.............................................................................................. ◦............................................................................................... • .. .. .. .........................................................................................◦ ..........................................................................................◦ .........................................................................................◦ ... .. ... • ............................................................................................◦... . . . Abbildung 8.1: Doppelt verkettete Liste ............ ........................ . ........................ ............................... ............................. ........................ .... ....................... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .............................. ....... . . . . . . . . . . . . . . . . ........................ . .... . . . . . . ... ........................ .................. .................................. ........................ .... ◦ ◦ ............ ◦............................................................................... ..... . . . . . . . . . . . . . . . . . . . . . . . . ... ....................... ....................... ........................ . . . . . . . . ....... . . . . . . . . . . ◦.. ................................... .......... ........................ ... ........................ .... ..... ......................... ............................. .................... Abbildung 8.2: Mehrfache Verkettungsfelder Satz in zwei verschiedenen Ketten. Mehrfache Verkettungsfelder eignen sich nicht für mehrfaches Einfügen eines Satzes in die gleiche Kette und sind auch weniger flexibel als Vertretersätze. Mischsortieren, das in Unterabschnitt 6.1.2, Seite 178, vorgestellt wurde, liefert ein gutes Beispiel für die Anwendung von (einfach) verketteten Listen. Es zeigt ausführlich, wie Listenzeiger in C benutzt werden können und welche Vorteile rekursive Aufrufe bei der Bearbeitung von Listen bieten. Realisierung positionsabhängiger Operationen SIZE: Diese Operation kann anhand der Listenbeschreibung stets in einer Operation ermittelt werden. W OCSIZE (n) = AV CSIZE (n) = BECSIZE (n) = 1 (8.2) SEARCH(p): Es ist das Listenelement mit der Positionsnummer p zu suchen und bereitzustellen. Durch Vergleich mit der Listenlänge kann in einer Operation festgestellt werden, ob das Element existiert oder nicht. Wenn es existiert, werden beginnend am Listenanfang 8.3. REALISIERUNG VON LISTEN 241 solange nacheinander Listenelemente durchlaufen, bis die Positionsnummer erreicht ist. Der Aufwand wird in der Anzahl durchlaufener Sätze gemessen: W OCSEARCH (n) = n; AV CSEARCH (n) = n+1 ; 2 BECSEARCH (n) = 1 Zur Herleitung von AVC: Man braucht i + 1 Zugriffe, wenn das Element an der Position i (i = 0, 1, . . . , n − 1) gesucht wird. Wir nehmen an, daß jede der n möglichen Positionen mit gleicher Wahrscheinlichkeit n1 auftritt. Dann ergibt sich für den Erwartungswert (Mittelwert) AV CSEARCH (n) = 1 1 1 1 1 n(n + 1) n+1 · 1 + · 2 + · · · · n = · (1 + 2 + · · · + n) = · = n n n n n 2 2 Zusammenfassend W OCSEARCH (n) = O(n); AV CSEARCH (n) = O(n); BECSEARCH (n) = O(1) (8.3) Man kann den Suchzugriff etwas schneller machen, wenn man berücksichtigt, daß das letzte Listenelement über die Listenbeschreibung direkt errreicht werden kann. Liegen Rückwärtsverweise vor, so werden die Elemente der zweiten Listenhälfte schneller erreicht, wenn man mit dem Durchlauf am Ende de Liste anfängt. Diese Verbesserungen bringen jedoch nicht viel. Die Gleichungen 8.3 gelten weiterhin. INSERT(p): Ein gegebener Satz wird an Position p als neues Listenelement eingefügt. Es sind zwei Fälle zu unterscheiden. a. Die Position p existiert in der Liste. Dann wird mit SEARCH(p) der entsprechende Satz gesucht und der neue Satz als sein Vorgänger in die Liste eingekettet. Siehe schematische Abbildung 8.3, in der Satz L vor Satz G eingekettet wurde. . . . · · ·................................................. ◦..................................................................... ◦..................................................................... · · · . . . . . . . . . · · ·..............................................................◦... ................................................................◦... ............................................ · · · F G . . . . · · ·........................................... ◦..................................................................... ◦..................................................................... ◦..................................................................... · · · . . . . . . . . . . . . · · ·...........................................................◦.. .................................................................◦.. ................................................................◦... ............................................ · · · F L G Abbildung 8.3: Einketten eines Satzes b. p ist um 1 größer als die größte existierende Listenposition. Über die Listenbeschreibung wird direkt auf das letzte Element zugegriffen und an dieses der neue Satz angekettet. Der Aufwand für das Einketten eines Satzes ist von der Größe des Datenbestandes unabhängig. Daher hängt der Aufwand für das Einfügen nur vom Aufwand für das Suchen ab. Analog zu 8.3 gilt: 242 KAPITEL 8. LISTEN W OCIN SERT (n) = O(n); AV CIN SERT (n) = O(n); BECIN SERT (n) = O(1) (8.4) DELETE(p): Der Satz an Position p wird ausgekettet. Der Aufwand wird wieder durch das Suchen bestimmt: W OCDELET E (n) = O(n); BECDELET E (n) = O(1) (8.5) AV CDELET E (n) = O(n); Siehe auch Anmerkung 8.2. NEXT: Über den Aktualitätszeiger wird direkt auf den aktuellen Satz zugegriffen und über diesen direkt auf seinen Nachfolger in der Liste. Der Aufwand für die Operation ist unabhängig von der Länge der Liste. W OCN EXT (n) = O(1); AV CN EXT (n) = O(1); BECN EXT (n) = O(1) (8.6) PREVIOUS: Die Operation entspricht NEXT in Rückwärtsrichtung, ist aber nur sinnvoll, wenn eine doppelt verkettete Liste vorliegt. W OCP REV IOU S (n) = O(1); AV CP REV IOU S (n) = O(1); BECP REV IOU S (n) = O(1) (8.7) Realisierung schlüsselwertabhängiger Operationen Wir nehmen an, daß ein Schlüsselwert k höchstens einmal in der Liste vorkommt. Außerdem setzen wir voraus, daß die Liste in aufsteigender Reihenfolge der Schlüsselwerte angeordnet ist. Die Realisierung der schlüsselwertabhängigen Operationen entspricht weitgehend der für positionsabhänge Operationen. Die Operationen SIZE, MIN, MAX, NEXT und PREVIOUS benutzen nur die Listenbeschreibung und können in konstanter Zeit unabhängig von der Größe der Liste realisiert werden. Für sie gelten die Gleichungen 8.2 entsprechend. SEARCH(k) und FIND(k): Beginnend beim ersten Element der Liste werden nacheinander die Schlüsselwerte aller Kettenglieder mit k verglichen, bis entweder der Schlüsselwert gleich k ist oder zum ersten Mal ein Schlüsselwert größer als k gefunden wird oder die Liste endet. Im ersten Fall hat man das gesuchte Listenelement gefunden. In den beiden anderen Fällen hat man festgesellt, daß es einen Satz mit Schlüsselwert k nicht in der Liste gibt. SEARCH(k) liefert einen Nullverweis (NIL) zurück. FIND(k) liefert einen Nullverweis zurück, wenn k kleiner als der kleinste oder größer als der größte in der Liste existierende Schlüsselwert ist. Anderenfalls läßt sich ein Satz mit nächstkleinerem Schlüsselwert bestimmen und dieser Satz wird zurückgeliefert. Wenn ein Satz mit dem Schlüsselwert k gefunden wird, ist der Aufwand – gemessen in der Zahl der Vergleiche – durch die Gleichungen 8.3 korrekt angegeben. Wird ein Listenelement mit Schlüsselwert 8.3. REALISIERUNG VON LISTEN 243 k nicht gefunden, so braucht man dafür natürlich auch mindestens einen und höchsten n Vergleiche. Zur Berechnung eines Mittelwertes müßte man etwas über die Verteilung von Lücken wissen. Einige Bemerkungen dazu findet man in Abschnitt 7.2. Mit welcher Wahrscheinlichkeit die Lücken auch auftreten, der Mittelwert kann nicht größer sein als der Aufwand im schlechtesten Fall, d. h. die Gleichungen 8.3 sind auch in diesem Fall korrekt. INSERT(k): Der Satz mit dem neuen Schlüsselwert ist an der richtigen Stelle einzuketten. Dazu ist die zu diesem Schlüsselwert gehörende Lücke zu bestimmen. Die Gleichungen 8.4 bleiben gültig, ebenso der Hinweis auf Abschnitt 7.2. DELETE(k): Der Satz mit Schlüsselwert k ist zu finden und dann auszuketten. Zum Aufwand gilt das dort Gesagte. Die Gleichungen 8.5 gelten weiterhin. Siehe auch Anmerkung 8.2. Zu etwas anderen Problemstellungen bei Listen mit Schlüsselwerten siehe Aufgabe 8.1. Anmerkung 8.2 Es kommt vor, daß ein Satz aus einer Liste ausgekettet werden soll, dessen Adresse bekannt ist, der also nicht zuvor gesucht werden muß. Es ist sinnvoll, dafür eine eigene Anweisung zu benutzen, z. B. DELETE(addr). Eine effiziente Realisierung erfordert allerdings doppelt verkettete Listen. 2 8.3.2 Realisierung von Listen durch Reihungen Eine weitere Realisierungsmöglichkeit für Listen sind Reihungen (siehe 2.6.1, Seite 61). Ein Anfangsstück der Reihung enthält die Sätze der Liste, und zwar einen Satz pro Listenelement. Der Rest ist unbelegt. In Abbildung 8.4 hat die Reihung m Einträge. Davon sind n durch eine Liste belegt. Die Einträge n, n + 1, . . . , m − 2, m − 1 sind frei. Die Position eines Satzes in der Liste ist gleich dem Index des Reihenelementes, in dem der Satz enthalten ist. Ein Nachteil von Reihungen ist ihre feste Größe. Listen, die länger sind, passen nicht hinein. Eine Reihung innerhalb des Programmlaufs nach Bedarf zu vergrößern ist im Prinzip zwar möglich, aber umständlich und wird normalerwereise nicht gemacht. Hingegen ist es einfach, die Reihungsgröße von Programmlauf zu Programmlauf neu festzulegen. In Kapitel 2 wird auf Seite 38 ein Beispiel vorgestellt. Realisierung positionsabhängiger Operationen SIZE: Wir setzen wieder eine allgemeine Listenbeschreibung voraus. Darin ist die aktuelle Länge der Liste, die Größe der Reihung und der Index des aktuellen Listenelements enthalten. Die Information kann mit einem Zugriff erhalten werden. Es gelten wieder die Gleichungen 8.2. 244 KAPITEL 8. LISTEN 0 R0 1 R1 • • • n−1 Rn−1 n • • • m−1 Abbildung 8.4: Liste als Reihung SEARCH(p): Das Listenelement mit der Positionsnummer p wird durch einen Zugriff zum Reihenelement p gefunden. W OCSEARCH (n) = O(1); AV CSEARCH (n) = O(1); BECSEARCH (n) = O(1) (8.8) INSERT(p): Ein gegebener Satz wird an Position p als neues Listenelement eingefügt. Vorher werden die dahinterliegenden Sätze um eine Position verschoben. Die Liste habe vor dem Einfügen n Sätze. Es werden ein Zugrif für das Speichern des neuen Satzes und n − p Kopieroperationen für das vorangehende Verschieben benötigt. Im besten Fall haben wir 0 Verschiebungen, im schlechtesten n. Wenn wir annehmen, daß jede der n + 1 Einfügstellen mit gleicher Wahrscheinlichkeit auftritt, ergibt sich als Mittelwert für die Anzahl Verschiebungen n−1 1 0 1 n(n + 1) n n + +···+ + = · = n+1 n+1 n+1 n+1 n+1 2 2 D. h. es gelten die Gleichungen 8.4. DELETE(p): Der Satz an Position p wird aus der Reihung entfernt. Das geschieht, indem alle Sätze, die hinter Position p stehen, um 1 nach vorn verschoben werden und der Platz des ursprünglich letzten Listenlementes freigegeben wird. Wie nicht schwer zu sehen, gelten die Gleichungen 8.5. NEXT und PREVIOUS: Die aktuelle Position wird um 1 erhöht bzw. vermindert und auf das entsprechende Reihungselement zugegriffen. Bei Bereichsüberschreitung erfolgt Fehlermeldung. Es gelten die Gleichungen 8.6 und 8.7. 8.4. KELLER, SCHLANGEN, HALDEN 245 Realisierung schlüsselwertabhängiger Operationen Die Liste sei durch eine Reihung realisiert, es gibt identifizierende Schlüsselwerte aus einem linear geordneten Wertebereich und die Liste sei in aufsteigender Reihenfolge der Schlüsselwerte angelegt. Die Operationen SIZE, MAX, MIN, NEXT und PREVIOUS können mit Hilfe der Listenbeschreibung in konstanter Zeit realisiert werden. SEARCH(k) und FIND(k): Man könnte – wie bei Verkettungen – in der Reihung linear suchen, bis man den Satz mit dem gegebenen Schlüsselwert gefunden hat. Die Tatsache, daß man in Reihungen mit den Indizes der Reihenelemente rechnen kann, erlaubt jedoch ein deutlich effizienteres Suchen, nämlich Binärsuche. Diese ist in Abschnitt 8.2 vorgestellt worden. Dort sind auch Abschätzungen zu Komplexität angegeben. INSERT(k): Es ist zunächst die Stelle, d.h. die Lücke im Schlüsselwertbereich, zu finden, an der der neue Satz einzufügen ist. Das entspricht der Suche nach einem nicht vorhandenen Schlüssel. Dann ist der belegte Teil der Reihung um ein Element zu erweitern und die Sätze hinter der Lücke um eine Position zu verschieben. Schließlich ist der neue Satz in das freigewordene Reihungselement zu speichern. Die Komplexität wird durch das Verschieben bestimmt, das im schlechtesten Fall gleich der der Listenlänge ist. DELETE(k): Es ist mit Binärsuche der zu löschende Satz in der Liste zu bestimmen. Beginnen mit diesem Listenelement werden die nachfolgenden Sätze um eine Position nach vorn verschoben und dann das letzte von der Liste belegte Reihungselement freigegeben. Das Suchen erfolgt in logarithmischer Zeit, das Verschieben in linearer Zeit. 8.4 8.4.1 Keller, Schlangen, Halden Keller Definition, Operationen, Realisierung Keller (Stapel, stack) sind Listen mit eingeschränkten Zugriffsmöglichkeiten. Sie sind in der Informatik von besonderer Bedeutung und wurden 1957 von F.L. Bauer 5 und K. Samelson 6 eingeführt (Patentanmeldung!). Mit der Anweisung PUSH wird ein neues erstes Bauer, Friedrich Ludwig, ∗ 1924 Regensburg. Deutscher Mathematiker und Informatiker. Professor an der Universität Mainz und an der Technischen Universität München. Baute in München den ersten Studiengang Informatik in Deutschland auf. Schrieb zusammen mit Gerhard Goos eines der ersten deutschsprachige Lehrbücher zur Einführung in die Informatik: [BaueG1971] und [BaueG1974]. Arbeiten zu angewandten Mathematik, Programmiersprachen und Softwaretechnogie, Kryptologie. Reichte zusammen mit Klaus Samelson 1957 ein Patent auf das Kellerprinzip ein, wofür er 1988 den IEEE Computer Pioneer Award erhielt. 6 Samelson, Klaus, ∗ 1918, † 1980. Deutscher Mathematiker und Informatiker. Professor an der Universität Mainz und an der Technischen Universität Münchengen. Trug wesentlich zur Klärung informatischer Grundbegriffe bei: Unterprogramm, Kellerprinzip, Compiler. Der Informaitikbereich der Universität Hildesheim hat die Adresse Samelsonplatz 1. Für eine Würdigung Samelsons siehe Langmaack [Lang2002] 5 246 KAPITEL 8. LISTEN Element in den Keller eingefügt. Mit der Anweisung POP wird das erste Element aus dem Keller entfernt und dem Aufrufer zur Verfügung gestellt. Häufig gibt es auch eine Operation TOP, die das erste Element zur Verfügung stellt, den Keller aber nicht verändert. In der Regel sind die Elemente eines Kellers Sätze einer festen Satzklasse. Duplikate dürfen auftreten, Schlüsselwerte werden nicht beachtet. Ein Keller kann leer sein. In diesem Fall liefern POP und TOP den Wert NIL. Die Kapazität eines Kellers ist begrenzt. Kellerüberlauf, d. h. der Versuch, mit PUSH ein Element in einen vollen Keller einzufügen, führt zu einer Fehleranzeige. In der theoretischen Informatik werden auch Keller unbegrenzter Kapazität betrachtet. Keller realisieren die Bearbeitungsstrategie LIFO (last in first out). Statt LIFO wird auch oft LCFS (last come first served) gesagt. Realisierung durch Verkettung Diese Realisierung entspricht der Realisierung von verketteten Listen. Es wird eine Kette auf- und abgebaut. Zur Verwaltung benutzt man zwei Zeiger, siehe Abbildung 8.5. Der klrbas klrzgr ... ... .. ... ... ... ... ... .................................................................................................................................................................................................................................................................................................................................. ... . . ......... .......... ......... ........ .. .. .. .. .. .. .......................................................................................... ........................................................................................... ........................................................................................... ......................................................................................... .. .. .. .. . . . .. .............................................................................................. .............................................................................................. ............................................................................................. ........................................................................................... .. . . . ...................................................... ...................................................... ...................................................... ...................................................... ...................................................... ◦ • ◦ ◦ ◦ ◦ ◦ ◦ • ◦ Abbildung 8.5: Realisierung eines Kellers durch Verkettung Zeiger klrbas (Kellerbasis) zeigt auf den ersten Satz der Liste, d. h auf das unterste Element des Kellers. Der Zeiger klrzgr (Kellerzeiger) zeigt auf den letzten Satz der Liste, also auf das oberste Element des Kellers. Mit PUSH wird der Satz (oder Vertretersatz) am Ende der Kette angehängt. Bei POP wird das letzte Element der Kette entfernt. Doppelte Verkettung erlaubt, POP in konstanter Zeit auszuführen. Ein leerer Keller wird durch den Wert NIL im Kellerzeiger angezeigt. Eine Anzeige, daß der Keller voll ist, gibt es bei dieser Implementierung nicht. Allerdings wird in der Regel das Anhängen eines Satzes (oder Vertretersatzes) an die Kette mit einer dynamischen Speicherplatzanforderung (z. B. mit malloc) verbunden sein und die kann unter Umständen zum Fehler „Kein Speicherplatz mehr vorhanden“ führen. Es kann manchmal sinnvoll sein, außer Kellerbasis und Kellerzeiger auch die Kellerfüllung, also die Anzahl Elemente, die im Keller lagern, mitzuführen. und [Lang1999]. 8.4. KELLER, SCHLANGEN, HALDEN 247 Realisierung durch Reihungen Diese Realisierung entspricht der Realisierung von Listen durch Reihungen. Siehe Abbildung 8.4. Die Kellerbasis braucht nicht exlizit gespeichert zu werden, denn sie ist durch das Reihenelement mit Index 0 gegeben. Der Kellerzeiger entält den höchsten belegten Index, bzw. einen negativen Wert, wenn der Keller leer ist. Kellerüberlauf ist möglich und ergibt sich, wenn ein Element eingekellert werden soll, aber die Reihung voll ist. Aufwand In beiden besprochenen Realisierungen sind PUSH und POP sehr effiziente Operationen: W OCP U SH (n) = W OCP OP (n) = O(1). 8.4.2 Keller: Beispiele und Anwendungen Beispiel 8.2 (Ausdrücke) Wir wollen den aritmtischen Ausdruck (3+4)∗(2+5) auswerten. Dazu wird er zuerst in umgekehrte polnische Notation (UPN, reverse Polish notation, RPN) 7 umgeformt: 3 4 + 2 5 + ∗. Die nachfolgende Auswertung zeigt Tabelle 8.6. Symbol Anfang 3 4 + 2 5 + ∗ Actionen Keller leer push 3 3 push 4 3, 4 pop 4, pop 3, push 7 = 3 + 4 7 push 2 7, 2 push 5 7, 2, 5 pop 5, pop 2, push 7 = 5 + 2 7, 7 pop 7, pop 7, push 49 = 7 ∗ 7 49 Tabelle 8.6: Auswertung eines Arithmetischen Ausdrucks Keller eignen sich nicht nur zur Auswertung von Ausdrücken, sondern können auch zur Gewinnung von deren UPN eingestzt werden. Das kann man z. B. mit zwei Kellern ereichen. Es sei (3 + (4 ∗ 2)) + 5) umzuformen. Tabelle 8.7 zeigt den Ablauf. Bei einer schließenden UPN ist eine klammerfreie Schreibweise von Ausdrücken, bei der zunächst die Operanden hingeschrieben werden und ihnen dann der darauf anzuwendene Operator folgt. Sie wurde aus der von J. Łukasiewicz 8 eingeführten Polnischen Notation, bei der der Operator den Operanden vorangeht, von Ch. Hamblin 9 entwickelt. 8 Łukasiewicz, Jan, ∗1878 Lemberg (heute Lviv, Ukraine), † 1956 Dublin, Irland. Polnischer Philosoph, Mathematiker und Logiker. 9 Hamblin, Charles, ∗ 1922, † 1985. Australischer Philosoph, Logiker und Computerpionier. 7 248 KAPITEL 8. LISTEN Symbol Anfang ( 3 + ( 4 ∗ 2 ) ) + 5 Ende Hauptkeller leer leer 3 3 3 3, 4 3, 4 3, 4, 2 3, 4, 2, ∗ 3, 4, 2, ∗, + 3, 4, 2, ∗, + 3, 4, 2, ∗, +, 5 3, 4, 2, ∗, +, 5, + Nebenkeller leer ( ( (, + (, +, ( (, +, ( (, +, (, ∗ (, +, (, ∗ (, + + + Tabelle 8.7: Umformung in umgkehrte polnische Notation Klammer oder am Ende der Eingabezeichenreihe wird ein Operator in den Hauptkeller geschrieben. Es wird bei dem Ablauf vorausgesetzt, daß der gegebene Ausdruck korrekt ist und bis auf äußere Klammern überall Klammern gesetzt sind. Keller eignen sich gut zur Auswertung von arithmetischen Ausdrücken und werden deshalb auch in Taschenrechnern eingesetzt. Bei Taschenrechnern der Firma Hewlett Packard basiert sogar die Bedienoberfläche auf UPN, also dem Kellerprinzip. 2 Beispiel 8.3 (Unterprogrammaufrufe) Es soll zunächst allgemein auf die Speicherverwaltung in Programmläufen eingegangen werden. Das folgende gilt für die meisten Betriebssysteme und setzt virtuelle Adressierung (Abschnitt 4.4, Seite 145) voraus. Wird vom Betriebssystem ein Programm zum Ablauf gebracht, also zu einem Prozeß gemacht, so wird ihm ein Adreßraum zugewiesen und dessen Seiten auf dem Seitenwechselgerät gespeichert. Die Seiten des Adreßraumes, die während des Ablaufs angesprochen werden, erhalten zusätzlich zeitweilig eine Hauptspeicherseite als Träger. Für die Verwaltung des zugewiesenen Adreßraumes wollen wir annehmen, daß ein übersetztes C-Programm abläuft. Programme in anderen Programmiersprachen verhalten sich jedoch im wesentlichen gleichartig. Die Verwaltung wird von Programmen ausgeführt, die zum Laufzeitsystem (Seite 90 und Seite 144) des Compilers gehören. Aus der Sicht des Betriebssystems sind diese Programme Teil des Benutzerprozesses. Das vom Compiler (und Binder) erzeugte Objektprogramm hat den in Abbildung 8.6 gezeigten Aufbau. Er ergibt sich aus der Aufteilung des Quellprogramms in Definitionsteil und Anweisungsteil (Seite 80). Der Anweisungsteil wird ganz überwiegend zu den Maschinenbefehlen des 8.4. KELLER, SCHLANGEN, HALDEN Befehle Konstanten Globale Variable 249 Lokale Variable Abbildung 8.6: Speicherorganisation in Prozesse Befehlsbereichs. Auch ein Teil der Konstanten des Konstantenbereichs ergibt sich aus dem Anweisungsteil. Die restlichen Konstanten werden im Definitionsteil festgelegt oder sind im Quellcode nicht sichtbare Verwaltungsdaten. Globale und lokale Variable bilden zusammen den Variablenbereich. Auch sie werden ergänzt um Verwaltungsdaten, die im Quellcode nicht erkennbar sind, z. B. Zustandsdaten für Unterprogrammaufrufe. Dem Betriebssystem sind nur Befehlsbereich, Konstantenbereich und Variablenbereich bekannt. Befehlsbereich und Konstantenbereich sind schreibgeschützt, werden aber unterschieden, da Konstanten auch ausführungsgeschützt sind. Die Speicherverwaltung des Laufzeitsystems hat die Aufgabe, den Variablenbereich zu verwalten. Üblicherweise verwaltet sie den Bereich für globale Variable als Halde (s.u) und den Bereich für lokale Variable als einen oder mehrere Keller. Im Bereich für globale Variable befinden sich die Variablen, die im Hauptprogramm und allen Unterprogrammen einheitlich zur Verfügung stehen. Zum Bereich für globale Variable gehört aber auch ein Teilbereich, aus dem die Speicherverwaltung dynamisch angeforderte Speicherplätze (malloc und free) zuteilt. Dieser Bereich wird Halde (heap) genannt. Halden werden in Unterabschnitt 8.4.4 näher betrachtet. Es kann passieren, daß der anfangs für die Halde bereitgestellte Adreßraum nicht ausreicht. Bevor das als Fehlermeldung an das Benutzerprogramm gemeldet wird, sollte die Speicherverwaltung beim Betriebssystem eine Vergrößerung des Adreßraumes beantragen10 und erst, wenn das abgelehnt wird, „Kein Speicher mehr vorhanden“ melden. Das Hauptprogramm und jedes Unterprogramm hat eigene lokale Variable. Für diese lassen sich jedoch im allgemeinen keine festen Speicherplatze zuordnen. Unterprogramme lassen sich nämlich rekursiv aufrufen und brauchen dann einen Satz lokaler Variabler für jede Aufrufstufe, vergleiche Abbildung 2.13, Seite 89. Für die Verwaltung der Aufrufstufen und ihrer lokalen Variablenbereiche hat sich die Verwendung von Kellern als sehr nützlich erwiesen. Beim Aufruf eines Unterprogramms muß der Zustand des aufrufenden Programms so gesichert werden, daß dieses nach Rückkehr aus dem Unterprogramm fehlerfrei weiterlaufen kann. In den frühen Tagen der maschinennahen Programmierung – „Assemblerprogrammierung“ – reichte dafür ein Maschinenbefehl aus. Mit ihm wurde zur Startadresse des Streng genommen, ist der volle Adressraum von Anfang an da. Vergrößert wird nur der Teil, dessen Seiten Platz auf dem Seitenwechselgerät zugewiesen ist (siehe Abschnitt 4.4, Seite 145). 10 250 KAPITEL 8. LISTEN Unterprogramms gesprungen und gleichzeitig die Adresse für den Rückspung an die Aufrufstelle gesichert. Benutzt man höhere Programmierprachen wie z. B. C, so muß mehr Aufwand getrieben werden. Bei jedem Unterprogrammaufruf: 1. Müssen die Registerinhalte des Aufrufers gesichert und der Anfangszustand der Register des aufgerufenen Unterprogramms eingestellt werden. 2. Muß die Rücksprungadresse gesichert werden. 3. Muß die Basisadresse des lokalen Variablenbereichs des aufgerufenen Unterprogramms gesetzt werden. 4. Müssen die Werte der an das Unterprogram direkt zu übergebenden Parameter, die Aufrufliste (calling list), bereitgestellt werden. 5. Schließlich muß dafür gesorgt werden, daß freier Platz für den lokalen Variablenbereich zur Verfügung steht. Eventuell müssen einige lokale Variable mit einem Anfangswert versehen werden. Die Daten zu den Punkten 1 bis 3 haben bei gegebener Rechnerarchitekur und gegebenem Compiler für alle Unterprogramme das gleiche Format. Zu ihrer Speicherung kann man einen Keller mit Programmaufrufblöcken benutzen. In Beispiel 8.4 wird erklärt, in welcher Form diese Verwaltung durch die konventionelle Maschine unterstützt werden kann. Die Speicherbeiche zu den Punkten 4 und 5 können zusammengefaßt werden (Abbildung 8.7). Für unterschiedliche Unterprogramme sind sie jedoch verschieden lang. Das macht die Aufrufliste lokale Variable ... .. .. ................................................................................................................................................................................................................................................................................................................................................................................................................ ... ... ... Abbildung 8.7: Speicher für die Aufrufliste und die lokalen Variablen eines Unterprogramms) Verwaltung dieser Bereiche in einem zweiten Keller etwas schwieriger. Trotzdem wird in allgemeinen diese Lösung gewählt. Bei PUSH und POP muß stets auch die Länge des zuzuweisenden bwz. freizugebenden Bereichs bekannt sein. Da die Zahl der Aufrufstufen wegen der Möglichkeit rekursiver Aufrufe ablaufabhängig ist, kann es vorkommen, daß die dafür benutzten Keller überlaufen. Die Speicherverwaltung sollte dann so vorgehen wie oben für die Halde beschrieben. 2 Beispiel 8.4 (Keller in der konventionellen Maschine) Wegen der Bedeutung von Kelleraufrufen hat man für die meisten modernen Rechner die Befehlssätze der konventionellen Maschine um Kellerbefehle erweitert. Dazu benutzt man spezielle Register des 8.4. KELLER, SCHLANGEN, HALDEN 251 Prozessors, die Kellerregister. Üblich sind ein Kellerbasisregister, in dem die Anfangsadresse des aktuellen Kellerbereichs steht, und ein Kellerzeiger, der auf das aktuelle (oberste) Kellerelement zeigt. Ein drittes Register, das Kellerendregister, ist manchmal auch vorhanden und gewährleistet, daß der zugewiese Kellerbereich nicht überschritten wird. Die Maschinenbefehle push und pop schreiben die Inhalte eines Satzes von Mehrzweckregistern in den Keller aus bzw. laden sie aus diesem. Der Befehl call sichert bei einem Unterprogrammansprung die Rückkehradresse im Keller. Der Rücksprung erfolgt mit return. Arithmetische und logische Operationen direkt auf dem Keller sind möglich. Der Befehl mult könnte beispielsweise die ersten beiden Kellerelemente durch ihr Produkt ersetzen. Befehle dieser Art sind in der konventionellen Maschine kaum noch üblich. 2 8.4.3 Schlangen In einer Schlange muß man sich hinten anstellen und ist vorn an der Reihe. Die entsprechende Datenstruktur heißt auch Schlange (Warteschlange, queue). Für die Bearbeitung von Schlangen gibt es die Operationen ENQUEUE, mit der ein Element als letztes in die Schlange eingefügt wird, und DEQUEUE, mit der das erste Element aus der Schlange entnommen wird. In der Regel sind die Elemente einer Schlange Sätze einer festen Satzklasse. Duplikte dürfen auftreten, Schlüssel werden nicht beachtet. DEQUEUE angewandt auf eine leere Schlange führt zu einer Fehlermeldung. Der Platz für eine Schlange ist begrenzt. ENQUEUE für eine volle Schlange führt auch zu einer Fehlermeldung. Schlangen realisieren die Bearbeitungsstrategie FIFO (first in first out). Statt FIFO wird auch oft FCFS (first come first served) gesagt. Realisierung durch Verkettung Abbildung 8.8 zeigt die Realisierung einer Schlange durch Verkettung. Der Kette werden am Anfang Elemente entnommen und am Ende angefügt. Die Verwaltung geschieht mit quend qustart ... ... ... ... .. .. ... .. ....................................................................................................................................................................................................................... ............................................................................................................. . .. . .......... ....... ........ ........ ... ... ....... ....... ....... ...... ..................................................................................... ...................................................................................... ...................................................................................... ..................................................................................... .. .. .. .. . .. .. .. ............................................................................................. ............................................................................................ ............................................................................................ ........................................................................................... .. .. .. . ...................................................... ...................................................... ...................................................... ...................................................... ...................................................... ◦ • ◦ ◦ ◦ ◦ ◦ ◦ Abbildung 8.8: Realisierung einer Schlange durch Verkettung • ◦ 252 KAPITEL 8. LISTEN den Zeigern quend und qustart. Die Opertionen ENQUEUE und DEQUEUE können in konstanter Zeit ausgeführt werden, letztere dank der doppelten Verkettung. Wie bei Kellern, die durch Verkettung realisiert werden, wird „volle Schlange“ nur gemeldet werden, wenn die Speicherverwaltung keinen dynamischen Speicher mehr zuteilen kann. Realisierung durch Reihungen Die Realisierung einer Schlange durch eine Reihung entspricht der Realisierung eines Kellers durch eine Reihung (Seite 247). Es gibt aber einen wichtigen Unterschied. Es kann vorkommen, daß das letzte Element der Reihung in die Schlange eingefügt wurde, aber inzwischen am Anfang der Schlange Kette wieder Reihungselemente frei geworden sind. Um diese Platz zu nutzen und nicht zu früh „kein Speicher mehr vorhanden“ zu melden, organisiert man die Schlange in den Reihungselementen kreisförmig. Die Einzelheiten bleiben dem Leser überlassen. 8.4.4 Halden und Prioritätswarteschlangen Die Begriffe Halde (heap) und Prioritätswarteschlange (priority queue) werden in der Informatik in einem doppelten Sinne gebraucht. Eine der Bedeutungen wird im folgenden beschrieben, die andere in Abschnitt 11.3, Seite 329. Halden in der dynamischen Speicherverwaltung Anfangs wurde der Begriff Halde (heap) nur im Sinne der in Unterabschnitt 11.3.1 eingeführten Datenstruktur benutzt. Später kam die hier besprochene Bedeutung hinzu. In dieser Bedeutung ist eine Halde ein Speicherbreich, aus dem eine verwaltenden Instanz, eine Speicherverwaltung, Speicherblöcke auf Anforderung zuteilt. Freigegebene Blöcke werden der Halde wieder hinzugefügt. Ein wichtiges Beispiel ist die Speicherverwaltung eines Programmlaufs, wie sie in Beipiel 8.3 dargestellt ist. Da die Anforderungen und Freigaben zu unterschiedlichen Zeiten erfolgen, wird der freie Platz der Halde immer „löchriger“. Die Speicherverwaltung muß versuchen, trotzdem den zur Verfügung stehenden Platz der Halde möglichst gut zu nutzen. Das ist nicht schwer, wenn ausschließlich Blöck einer festen Länge vergeben und freigegeben werden. Die Halde wird in solche Blöcke eingteilt. Die Organisation beschränkt sich dann darauf, die Menge der unbelegten Blöcke zu verwalten. Das kann z. B. mit einer verketteten Liste freier Blöcke geschehen. Eine andere Verwaltung der Halde, die weniger Platz verbraucht und i. a. schneller ist, benutzt Bitlisten. Jeder Bitposition entspricht eineindeutig ein Block der Halde. Der Wert des Bits zeigt den Belegungzustand des Blockes an. In vielen Fällen wäre es jedoch sehr unvorteilhaft, Speicherplatz stets in Blöcken gleicher Größe zu vergeben und zurückzugeben. malloc ist ein gutes Beispiel. D. h. wir brauchen 8.4. KELLER, SCHLANGEN, HALDEN 253 Verfahren zur Zuteilung und Rückgabe von Blöcken variabler Länge. Man spricht von dynamischer Speicherverwaltung (dynamic memory management) oder auch von dynamischer Speichertzuteilung (dynamic storage allocation). Der insgesamt zur Verfügung stehende freie Speicher wird als eine Verkettung von maximal zusammenhängenden Blöcken verwaltet. In jedem Block sind seine Länge und ein Verweis auf den nächsten freien Block verzeichnet. Eine Anforderung an die Speicherverwaltung besteht dann im wesentlichen aus einer Längenangabe. Die Speicherverwaltung sucht in der Kette freier Blöcke einen passender Größe, schneidet ein Stück der benötigten Länge ab und aktualisiert die Kette freier Blöcke. In dem gefundenen Block wird seine Länge eingetragen und der auf das Längenfeld folgende Teil wird durch Adreßübergabe dem Anforderer zu Verfügung gestellt. Bei der Rückgabe eines Blockes wird nur seine Adresse angegeben und die Speicherverwaltung aktualisiert die Liste freier Blöcke. Eine Rückgabe kann zur Vergrößerung eines existierenden Freispeicherblocks führen, aber ebenso gut auch zur Einkettung eines neuen, möglicherweise kleinen Freispeicherblocks. Dadurch kann die oben erwähnte Löcherstruktur immer stärker werden. Bei einer gegeben Anfordungslänge muß ein Freispeicherblock gefunden werden, der mindestens diese Länge aufweist. Das einfachste Verfahren ist, den ersten passenden Block zu finden. Man spricht von first fit. Man kann auch etwas mehr Aufwand treiben und unter den passenden Freispeicherblöcken einen kleinster Länge suchen. Dann spricht man von best fit. Best fit beugt einer Zersplitterung des Speichers besser vor, aber nicht in dem Maße, wie man das erwarten würde. Deswegen wird häufig das deutlich einfachere Verfahren first fit gewählt. Was passiert nun, wenn eine Speicheranforderung kommt und es keinen freien Block mit dieser Länge gibt? Zunächst einmal kann die Speicherverwaltung versuchen, die Halde, wie auf Seite 249 beschrieben, zu vergrößern. Geht das nicht, so wird sie den Fehler „Kein Plaltz mehr vorhanden“ melden. Das kann passieren, obwohl der insgesamt noch zu Verfügung stehende freie Platz für die Anforderung ausreichen würde. Er ist jedoch zu stark zerplittert. Man spricht von externer Fragmentierung (external fragmentation). Man kann dann auf die Idee kommen, die belegten Blöcke und die Freispeicherblöcke so in der Halde zu verschieben, daß ein großer Freispeicherblock entsteht. Tut man das, so spricht man von Speichersammeln (garbage collection). Für einige Spezialfälle der Speicherverwaltung ist das sinnvoll. Für den sehr wichtigen Fall der Speicherverwaltung in Programmläufen (siehe Beispiel 8.3) ist das nicht der Fall. Die Adresse eines zugewiesenen Speicherblocks wird vom anfordernden Programm möglicherweise an vielen verschieden Stellen benutzt, eventuell an andere Programmteile, z. B. Unterprogramme, weitergereicht. Es ist praktisch nicht möglich festzustellen, wo diese Adresse gebraucht wird. Eine Adreßverschiebung des Blocks durch die Speicherverwaltung müsste ohne Änderung der Stellen geschehen, an denen die Blockadresse benutzt wird. Das ist nicht zulässig. Es gibt die Möglichkeit, Speicherplatz einer Halde in Böcken unterschiedlicher, aber nicht frei wählbarer Längen zu verwalten. Zum Beispiel können Blöcke nur in den Längen 254 KAPITEL 8. LISTEN 8, 16, 32, · · · , 2k zugewiesen werden. Dieses Verfahren bietet für die Verwaltung einige Vorteile. Es heißt buddy system. Einzelheiten sind bei Knuth [Knut1997], Seiten 442ff, zu finden. Auch in einem buddy system kann es zu externer Fragmentierung kommen. Da die Länge der zugwiesenen Blöcke im allgemeinen größer ist als die wirklich benötigte Länge, gibt es auch unbenutzten Platz innerhalb der Blöcke. Dieser wird interne Fragmentierung (internal fragmentation) genannt. Prioritätswarteschlangen Bei der Bearbeitung von Aufträgen werden diese meistens in einer Schlange verwaltet. Dabei kommt es oft vor, daß die Aufträge Prioritäten haben. Ein Auftrag wird nach allen Aufträgen mit höherer Priorität bearbeitet und vor allen mit niedriger Priorität. Außerdem werden alle Aufträge seiner Prioritätsklasse, die bei seiner Ankunft schon warten, vor ihm bearbeitet. Das entspricht einer Warteschlange, bei der ein ankommender Auftrag in die niedrigste Position seiner Priorität eingeordnet wird. Man kann das auch als n Warteschlangen ansehen, jeweils eine für jede Priorität. Zu Bearbeitung wird dann der erste Auftrag der nichtleeren Warteschlange höchster Priorität genommen. Als Beispiel kann der Scheduler eines Betriebssystems dienen (Seite 568). Wenn es eine feste, nicht zu große Zahl n von Priotitäten gibt und diese von 0 bis n − 1 numeriert sind, gibt es eine sehr einfache und effiziente Realisierung der Prioritätswarteschlange. Man legt eine Reihung mit n Elementen an, je eine für jede Prioriät. Jedes Element enthalt einen Zeiger auf auf den Anfang und einen auf das Ende der entsprechenden Schlange. Siehe Abbildung 8.9. Ein Zeiger first zeigt auf die erste nichtleere first ... ... .. ... ................................................................................................................................... . ......... ......... ... 0 quend0 qustart0 1 quend1 qustart1 ••• ••• k quendk qustartk ••• ••• n−1 quendn−1 qustartn−1 Abbildung 8.9: Prioritätswarteschlange als Reihung von Einzelschlangen Priorität. Die Schlangen der einzelnen Prioritäten kann man als Reihungen implementieren oder, wenn man etwas mehr Flexibilät braucht, in einem Pool von Blöcken fester Länge, wie oben beschrieben. Es kommt durchaus vor, daß Prioritätswarteschlangen gebraucht werden, bei denen die Anzahl der möglichen Prioritäten groß ist und nicht von vornherein festliegt. Dann bietet 8.4. KELLER, SCHLANGEN, HALDEN 255 eine effiziente Implementierung deutlich mehr Schwierigkeiten. Eine mögliche Implementierung benutzt ausgewogene Suchbäume und soll in Aufgabe 9.4, Seite 303, untersucht werden. Als weitere Lösungmöglichkeit werden des öfteren Halden nach Unterabschnitt 11.3.1, Seite 329 genannt. Diese sind jedoch für diesen Zweck nicht geeignet. Aufgaben Aufgabe 8.1 Untersuchen Sie Listenrealisierung durch Verkettung für die folgenden Fälle: 1. Der Fall, daß die Sätze identifizierende Schlüsselwerte haben, die jedoch keine Ordnung aufweisen bzw. deren Ordnung nicht benutzt werden soll. Ein Schlüsselwert darf in der Liste höchstens einmal vorkommen. 2. Die Sätze haben nichtidentifizierende Schlüsselwerte, sogenannte Sekundärschlüssel (secondary key). Die Reihenfolge in der Liste soll aufsteigend sortiert nach Sekundärschlüsselwerten sein. 3. Die Sätze haben identifizierende Schlüsselwerte, können jedoch mehrfach in der Liste auftreten können. Literatur Listen sind ein Standardthema in allen Lehrbüchern, die Datenstrukturen behandeln, z. B. Cormen/Leiserson/Rivest [CormLR1990] Teil III, Ottmann/Widmayer [OttmW1996], Noltemeier [Nolt1972], Kowalk [Kowa1996], Weiss [Weis1995]. Mit besonderer Brücksichtigung von C werden Listen in Kruse/Tondo/Leung [KrusTL1997] und Aho/Ullman [AhoU1995] behandelt. 256 KAPITEL 8. LISTEN Kapitel 9 Suchbäume 9.1 9.1.1 Binäre Suchbäume Allgemeines zum Suchen In Kapitel 8 haben wir die Sätze eines Datenbestandes als Listen angeordnet. Wir haben gesehen, daß das Suchen eines Satzes i. A. aufwendig ist, weil die Zahl der Vergleichsoperationen linear mit der Größe des Datenbestandes wächst. In diesem und im nächsten Kapitel werden wir Datenstrukturen und Algorithmen kennenlernen, mit denen das Suchen wesentlich effizienter realisiert werden kann. Wie bei den schlüsselwertabhängigen Operationen bei Listen (siehe Seite 234) wollen wir annehmen, daß es für die Sätze eindeutig identifizierende Schlüsselwerte gibt. Die wichtigsten Verfahren für das Suchen eines Satzes nach seinem Schlüsselwert sind Verfahren mit Schlüsseltransformation und Verfahren mit Schlüsselvergleich. Verfahren mit Schlüsseltransformation. Bei einer Schlüsseltransformation wird der Schlüsselwert von einem Adressierungsalgorithmus in eine Speicheradresse umgerechnet. Schematisch ist das in Abbildung 9.1 dargestellt. Schlüsseltransformation bietet sehr ef.......................................................... ................... ............ ........... ........ ......... ....... ...... ...... . . . . . .... .... ... . ... .... ... .. ... .. .... .. . . . ...... ... . . . ....... . . ..... ........ ........ ........... ........... ................. ............................................................... Algorithmus Schlüsselwert ... ... ... ... . ....... .. ........ ... .. ........................................................................................................................................... .. Speicheradresse Abbildung 9.1: Suchen mit Schlüsseltransformation 257 258 KAPITEL 9. SUCHBÄUME fizienten Zugriff bei Suchoperationen. Das Verfahren eignet sich jedoch nicht für die vollständige Bearbeitung aller Sätze in aufsteigender Reihenfolge. Schlüsseltransformation wird im nächsten Kapitel behandelt. Verfahren mit Schlüsselverlgeich. Es wird ein gegebener Schlüsselwert mit einem Wert in einem strukturierten Datenbestand verglichen. Bei Gleichheit hat man den gesuchten Satz gefunden. Bei Ungleichheit wird weitergesucht. Weist die Schlüsselwertmenge keine lineare Ordnung auf oder wird diese nicht benutzt, so entspricht dies dem Suchen in einer Liste (Kapitel 8). Liegt eine lineare Ordnung in der Schlüsselwertmenge vor, so kann man bei Ungleichheit prüfen, ob der Suchwert kleiner oder größer als der Vergleichswert ist. In Abhängigkeit davon wird die Suche in einer von zwei disjunkten Teilmengen fortgesetzt. Die Schlüsselwerte der anderen Teilmenge braucht man nicht mehr zu berücksichtgen und kommt so unter Umständen mit sehr viel weniger Vergleichen zum Ziel. Eine erste Realisierung dieses Vorgehens haben wir im vorigen Kapitel mit der Binärsuche kennengelernt (Abschnitt 8.2, Seite 235). Im vorliegenden Kapitel wird diese Vorgehensweise mit einer anderen Datenstruktur, nämlich mit Bäumen, in erster Linie mit Binärbäumen realisiert. 9.1.2 Binärbäume Bäume sind Graphen und werden als solche in Abschnitt 14.4, Seite 382, ausführlich behandelt. Hier wollen wir uns auf spezielle Bäume, die Binärbäume, beschränken. Diese haben wichtige zusätzliche Eigenschaften und sind in besonderem Maße für Suchaufgaben geeignet. In Beispiel 2.3, Seite 69, wurde eine Lehrveranstaltungsdatei als Binärbaum eingeführt. Siehe insbesondere Abbildung 2.9. Der Binärbaum wurde in C als Satz (struct) mit einem Schlüsselfeld (Lehrveranstaltungsname), und zwei Verweisfeldern (linker und rechter Nachfolger) realisiert und zur Ausgabe der Lehrveranstaltungsnamen in aufsteigender alphabetischer Reihenfolge benutzt. Ein weiteres Beispiel ist in Abbildung 9.2 zu sehen. Die Knoten des Baumes sind die Werte aus Tabelle 8.1. Mit Hilfe dieser Beispiele wollen wir zur Definition eines Binärbaumes kommen. Es gibt eine nichtleere Menge V von Knoten. Jeder Knoten kann höchstens einen linken Nachfolger (left successor) und höchstens einen rechten Nachfolger (right successor) haben. Ein Knoten ist Vorgänger (predecessor) seines rechten und seines linken Nachfolgers. Ein Weg (path) von Knoten a nach Knoten b ist eine Folge von Knoten a = v0 , v1 , · · · , vl−1 , vl = b mit l ≥ 1, für die stets vi+1 (linker oder rechter) Nachfolger von vi (i = 0, . . . , l − 1) ist. l ist die Zahl der Übergänge, die Weglänge (path length), und muß mindestens 1 sein.1 Definition 9.1 Ein Binärbaum ist eine endliche nichtleere Menge V von Knoten mit den folgenden Eigenschaften: 1 Ein wesentlich allgemeiner Wegbegriff wird in Abschnitt 14.1, Seite 373, eingeführt. 9.1. BINÄRE SUCHBÄUME QUARK .......................... .......... .......... .......... .......... .......... .......... . . . . . . . . . .......... ... .......... .......... .......... .......... . . . . . . . .......... . . ...... . . .......... ... . . . . . . . . . .............. ............. . . . . . . . . . . . .... ........... PFAU RUHE ...... ....... ....... ........ ....... . . . . . . ....... ....... . ........ .......... ................ ............ ....... .............. ....... ....... ....... ........ ....... ....... . . . . . . ........ . ....... ....... ....... . ....... . . . . . . .......... . . .......... . . . . .................. ............ BESUCH ....... ROT ...... ............. ....... ....... ....... ........ ....... ....... . . . . . . ........ ........ ....... ....... ....... . . . . . . ....... .. . .......... . .......... . . . . .. .............. ............... ANTON TANNE ....... ...... ............. ....... ....... ....... ........ ....... ....... . . . . . . ........ ........ ....... ....... ....... . . . . . . ....... .. . .............. ......... . . .............. ................ LAGE SEHEN .................... ....... ....... ....... ........ ....... . ....... . . . . . . ....... ....... ....... ....... . ........ . . . . . . . ....... .. ................ ......... . . ................ ................. HUT ...... ....... ....... ....... ....... . . . . . . . . ....... ....... . ....... ........... ............... HAT ........ ....... ........ ....... . . . . . . . ... ....... ....... . ........ ............ ............... DER ZAHN .... ........ ....... ....... ....... ........ ....... ....... ....... ....... ... .. ................... KNABE ZANK ............ ....... ............. ....... ....... ....... ....... ........ ....... . . . . . . . ....... .. ....... ....... ....... . ....... . . . . . ....... .. . . ............ . . . . . ................... ........... LUST NOCH ........ ....... ........ ....... . . . . . . . ... ....... ....... . ........ ............ ................ NEIN Abbildung 9.2: Binärbaum binbaum1 259 260 KAPITEL 9. SUCHBÄUME 1. Jeder Knoten hat höchstens einen linken Nachfolger und höchstens einen rechten Nachfolger. 2. Jeder Knoten hat höchstens einen Vorgänger. 3. Es gibt genau einen ausgezeichneten Knoten w, die Wurzel (root), der keinen Vorgänger hat. 4. Es gibt genau einen Weg von der Wurzel zu jedem anderen Knoten. Ein Weg a = v0 , v1 , · · · , vl−1 , vl = b in einem Binärbaum heißt einfach (simple), wenn alle Knoten paarweise verschieden sind2 . Er heißt offen (open), wenn a 6= b, anderenfalls geschlossen (closed). Gibt es einen Weg von einem Knoten u zu einem Knoten v, so heißt v von u erreichbar (reachable). Wichtige Eigenschaften von Binärbäumen sind im folgenden Satz zusammengefaßt. Satz 9.1 Es sei B ein Binärbaum. a. Außer der Wurzel hat jeder Knoten genau einen Vorgänger. b. In B sind alle Wege einfach. c. In B sind alle Wege offen. d. Ist Knoten v von Knoten u erreichbar, so gibt es genau einen Weg von u nach v. e. Jeder Knoten v aus B bildet zusammen mit allen von ihm erreichbarem Knoten einen Binärbaum, dessen Wurzel er ist. Beweis: a. Jeder Knoten außer der Wurzel ist von der Wurzel erreichbar, hat also mindestens einen Vorgänger. Nach Punkt 2 der Definition hat er also genau einen Vorgänger. b. Wir nehmen an, es gäbe einen nicht-einfachen Weg. v sei der erste Knoten, der auf ihm mehrfach vorkommt. v kann nicht die Wurzel sein, denn diese hat keinen Vorgänger. Es sei v 0 das erste Vorkommen von v auf dem Weg und v 00 das zweite. Der eindeutig bstimmte Vorgänger y von v 0 (und v 00 ) kann nicht auf dem Wege liegen. Dann wäre nämlich v 0 nicht das erste Auftreten eines mehrfachen Knoten auf dem Weg. Da y jedoch auch Vorgänger von v 00 , muß es andererseits auf dem Weg liegen, denn v 00 ist nicht der Anfangsknoten des Weges. Dieser Widerspruch zeigt, daß es nicht-einfache Wege nicht geben kann. c. In allen Wegen, die zwei oder mehr Knoten durchlaufen, ist nach b. der erste vom letzten Knoten verschieden. Sie sind offen. Es durchlaufe der Weg nur einen Knoten. Dieser ist Vorgänger und Nachfolger von sich selbst. Der Knoten kann also nicht die Wurzel sein. Es gibt dann einen Weg von der Wurzel zu dem Knoten und auf diesem 2 Diese Bedingung ist etwas stärker als die in Definition 14.1, Seite 374. 9.1. BINÄRE SUCHBÄUME 261 einen vom Knoten verschiedenen Vorgänger. Der Knoten hätte dann zwei verschiedene Vorgänger im Widerspruch zu Punkt 2 der Definition. d. Gäbe es zwei verschiedene Wege von u nach v, so gäbe es auch zwei verschiedene Wege von der Wurzel nach v. Das wäre ein Widerspruch zu Punkt 4 der Definition. e. Beweis als Übung. Der neue Binärbaum kann aus nur einem Knoten bestehen. 2 Folgerung: Der rechte Nachfolger eines Knotens v in einem Binärbaum existiert entweder nicht oder bildet zusammen mit allen von ihm erreichbaren Knoten einen Unterbaum, den rechten Unterbaum von v. Entsprechend wird der linke Unterbaum von v definiert. 2 Anmerkung 9.1 Linke und rechte Unterbäume kann man benutzen, um Binärbäume anders zu definieren. Ein Binärbaum ist entweder leer oder besteht aus einer Wurzel und einem rechten und einem linken Binärbaum als Unterbäumen. Es läßt sich zeigen, daß diese Definition und Definition 9.1 gleichwertig sind. 2 Größen in einem Binärbaum Die Anzahl Knoten eines Binärbaumes wird häufig mit n bezeichnet. Es gibt bei n Knoten die doppelte Anzahl von Feldern, die auf einen Nachfolger verweisen. Die Anzahl Knoten, die als Nachfolger infrage kommen, ist n−1. Die Anzahl Nachfolgerfelder mit NIL-Verweis ist demnach 2n − (n − 1) = n + 1. Ein Knoten eines Binärbaumes heißt Blatt (leaf ), wenn er keine Nachfolger hat. In einem Binärbaum gibt es mindestens 1 Blatt. Da jedes Blatt Blätter. zwei leere Nachfolgerfelder aufweist, gibt es höchstens n+1 2 In einem Binärbaum versteht man unter der Stufe (level) eines Knotens die Länge des Weges von der Wurzel zu dem Knoten +1. Eine sehr wichtige Größe in Binärbäumen ist die Höhe (height) h. Darunter versteht man die größte Länge eines Weges von der Wurzel zu einem Blatt +1. Statt von Höhe spricht man gelegentlich auch von Tiefe (depth). Die größte mögliche Höhe bei n Knoten ist h = n und ergibt sich bei einem linearen Baum ohne Verzweigungen. Wenn man in einem Binärbaum ein Blatt so umsetzt, daß es danach näher an der Wurzel liegt, kann die Höhe des Baumes nicht wachsen. Siehe Abbildung 9.3 Aus diesem Grund kann bei einem Binärbaum, der aus n = 2k − 1 Knoten besteht, die kleinste Höhe hmin (n) nur auftreten kann, wenn ein vollständiger ausgewogener Binärbaum (complete balanced binary tree) vorliegt. Das ist ein Binärbaum, der auf der ersten Stufe einen Knoten, auf der zweiten zwei usw. und auf der letzten 2k−1 Knoten aufweist. In ihm liegen alle Blätter auf der Stufe k und alle anderen Knoten haben zwei Nachfolger. Abbildung 9.4 zeigt eine solchen Binärbaum aus 15 Knoten. Für solche Binärbäume gilt hmin (n) = k, also hmin (n) = ld (n + 1). Gilt 2k − 1 < n < 2k+1 − 1, so kommt man mit k Stufen nicht mehr aus. Man kann jedoch stets Binärbäume angeben, in denen hmin (n) = k + 1 , also hmin (n) = dld (n + 1)e gilt. Dazu bildet man mit 2k − 1 Knoten einen vollständigen ausgewogenen Baum und verteilt die n − 2k + 1 verbleibenden Knoten 262 KAPITEL 9. SUCHBÄUME .......... .... ...... .. ..... ..... ....... . .... ......... ...... .... .... . . . .... .. .. .... .......... .................. ......... ..... .... ....................... .... ... .. ... ... .... . . ... .... ...... ... .......... ... ................. . .... . . .. .... . . . .... .. . .... .............. ................ ........ . . . . . . . . ...... ..... .... ......... . . .. .. ..... .... . . . .. ... ... . . . .................. ..................... . . . . . . .... .. .. . . . . . . . . .... . . ... . .... .. ...... ........ ................. ........ ...... ............ ...................... ....... ..... ..... .......... .... ..... . .. ..... .. .... . ..... . . ... .. . .... .... .. ................... ........... .................. .... .... .... .... .... . . ...... . .. .......... . ................. ......... . . ....... .... .................... . . .. ... .... ..... . . ... .. . . . ................. ...................... . . . . . . . .... . .. . . . . . . .... . ... .. . ..... . ..... ..... ........ ................. ......... ....... ........... ........................ ..... ...... ..... ......... .. .... .. ... . ..... ... .... ... . .. ... ... . ................. ................ .............. w b .......... .... ...... .... . .... .. . . . ...... .... ........ ....... ... . .... . . .... .. .. .... ......... ................. ......... ..... .... ........................ ... .... ... ... ... .... . .. .... ...... ... ........... .... ................. . . .... . . .... .... .... .. . .... .............. ................ ........ . . . . . . . . ...... .... .... ......... . .. . .. .... .... . .. .. ... . . .. . . .................... .................. . . . . . . .... .. . ... . . . . . . . .... . . ... . ... .. . ........ ........ ................. ......... ....................... ....................... ...... .... ... .... ..... . .... ..... .. .. ..... ..... ..... . . .... .... .. .. ... .......... .................. ....................... .... .... .... ..... .... .... . ..... ....... .. .. . . . .................. ......... ............ ............ ...................... ..................... . ...... ..... . ... ... ... .. .... .... ... .... ..... .... ..... .. ................ ........... ................ . . . .... ... . .... . . .... .. . .... ..... ................. ......... ....... ........... ..... ...... ..... ......... .... ... .. . . . .. ... ... ................. ................ w b Abbildung 9.3: Höhe in Binärbäumen ......... .... ...... . ... .... ... ......................... ................ . . . . . . . . ........ ...... . . ........ . . . . . ........ ....... ......... ........ ........ ........ ........ ........ . . . . . ......... . . ..... . ........ . . . . . . ........ ...... . . . . ........ . . . ......... ...... . . . . . . . ........ ..... . . . . ........ ... ........ . . . . ... . .. . ........................................... . . ...................... ...... . .. .... . ... .. . . . . .... ... .................... ......... . . . ...... .............. ............ . . ...... .... ...... ...... . . . . . . . . . . . . . ...... ...... ... .... ...... ...... ...... ...... ...... ...... ...... ...... ...... ...... ...... ...... . . . . . . ...... . . . . . . . . ...... . ... ...... .. . ........... . . . . . . . . . . . . . . . ........ ......... .. .......... ................... .............................. . . . . . . . . ................................... . . . . . . . . . . . . . . . . . ... . ... ... ... . . ... . . . ..... . . ..... ... . ... .. . . ... . ... . . . .... ..... ..... ..... .. . ....... ...... ...... ....... .... ......... ...... .... ......... ....... ... ....... ....... ... ...... ...... . . . . . . . . . . . . . . .... .... .... .... . .. . . ... . . . . . . . .... .. . . . . . . . . . . . ... .. ... .. ... .. .. .. .. ........ ........ ........ ....... ................. ......... ................. ......... ................. ......... .................. ......... ...... ........... ......... ............ .......... ............ .......... ............ .. . .... ..... ..... ..... ..... .......... ..... ...... .... ....... .... ...... ... ...... .... ...... . . . . .. . . . .. . . . . . ..... . . . . ... ... ... ... ... ... . . .. . .. ..... . ... . . . . . .. ... . . . . . . . .... ... .... ... .... ... .... ... .... ... .... ... ...... ....... ............... ........... ........... ........... .......... .......... .......... .... Abbildung 9.4: Vollständiger ausgeglichener Binärbaum so auf die 2 · 2k−1 = 2k Verweisfelder der Stufe k, daß alle Blätter höchstens Stufe k + 1 haben. Es gilt also allgemein hmin (n) = dld (n + 1)e = Θ(ln(n)) (9.1) Gleichung 9.1 fließt in die folgende Proposition ein, die für spätere Anwendungen gebraucht wird. Proposition 9.1 Die maximale und die mittlere Tiefe eines Binärbaumes mit m Blättern ist wenigstens ld m. Beweis: Das folgenden ist [OttmW1996] entnommen. Für die maximale Tiefe ergibt sich das aus Gleichung 9.1. Für die mittelere Tiefe soll das durch Widerspruch bewiesen werden. Es sei m die kleinste Anzahl von Blättern, für die es einen Binärbaum T gibt, der der Behauptung nicht genügt. 9.1. BINÄRE SUCHBÄUME 263 Dann ist m ≥ 2 und T hat einen linken Unterbaum T1 mit m1 Blättern und einen rechten Unterbaum T2 mit m2 Blättern. Es ist m1 + m2 = m. Da m1 und m2 kleiner als m sind, gilt mittlere Tiefe(T1 ) ≥ ld m1 mittlere Tiefe(T2 ) ≥ ld m2 . In T ist die Tiefe eines Blattes genau um 1 größer als in dem Unterbaum T1 oder T2 , in dem es liegt. Daraus folgt mittlere Tiefe(T ) = ≥ = m1 (mittlere Tiefe(T1 ) + 1) + mm2 (mittlere m m1 ((ld m1 ) + 1) + mm2 (ld m2 + 1) m 1 (m1 · ld (2m1 ) + m2 · ld (2m2 )). m Tiefe(T2 ) + 1) Die mittlere Tiefe von T ist somit eine Funktion f (m1 , m2 ) und wegen der Nebenbedinung m1 + m2 = m eine Funktion von m, nämlich f (m1 , m − m2 ). Mit einigem Rechenaufwand stellt man fest, daß diese Funktion für m1 = m2 = m2 ein Maximum aufweist. Also mittlerte Tiefe(T ) ≥ ld m, was ein Widerspruch zur obigen Voraussetzung ist, 2. Eine Klasse von Binärbäumen nennt man buschig (bushy), wenn h(n) = hmin (n) = Θ(ln(n)). Sie heißt dürr (sparse), wenn h(n) = Θ(n). In etwas ungenauer Sprechweise heißt ein Binärbaum buschig bzw. dürr, wenn er zu einer solchen Klasse gehört. Buschige Binärbäume werden häufig auch ausgewogen (balanced) genannt. Darstellungen von Binarbäumen Meistens werden Binärbäume wie in Abbildung 9.2 als gerichtete Graphen dargestellt. Linke Nachfolger werden durch einen nach links gehenden Pfeil, rechte durch einen nach rechts gehenden Pfeil gekennzeichnet. Manchmal werden die Pfeilspitzen weggelassen und die Nachfolgerrelation durch die Lage – darüber und darunter – ausgedrückt. Man kann rechts und links auch anders charakterisieren, z. B. durch Farben. Gerichtete Bäume sind hierarchische Strukturen und alle Formen der Darstellung solcher Strukturen können auf solche Bäume angewandt werden. Ein wichtiges Beipiel ist die aus dem Bibliothekswesen stammende Dezimalklassifikation3 . Tabelle 9.1 zeigt die Struktur des Binärbaumes aus Abbildung 9.6 in Dezimalklassifikation. Mit Hilfe von Klammern ist auch eine rein lineare Darstellung von Binärbäumen möglich. Hierauf soll jedoch nicht weiter eingegangen werden. Ursprünglich von Leibniz (siehe Seite 93) erdacht. Von Melvil Dewey 4 erweitert und in der zweiten Hälfte des 19. Jahrhunderts in den USA eingeführt. 3 Dewey, Melvil (eigentlich Melville Louis Kossuth Dewey) ∗10. Dezember 1851, Adams Center, N.Y. †26. Dezember 1931, Lake Placid, N.Y. Amerikanischer Bibliothekar. Mitbegründer der Amercian Library Association. Betätigte sich auch als Rechtschreibreformer der englischen Sprache. 4 264 KAPITEL 9. SUCHBÄUME 1 1.1 1.1.1 1.1.1.1 1.1.1.2 1.1.1.2.1. 1.1.1.2.1.1 1.1.1.2.1.1.1 1.1.1.2.2. 1.1.1.2.2.1 1.1.1.2.2.2 1.1.1.2.2.2.1 1.2 1.2.1 1.2.2 1.2.2.1 1.2.2.2 1.2.2.2.2 QUARK PFAU BESUCH ANTON LAGE HUT HAT DER KNABE LUST NOCH NEIN RUHE ROT TANNE SEHEN ZAHN ZANK Tabelle 9.1: Binärbaumstruktur in Dezimaldarstellung Realisierung von Binarbäumen Ein naheliegende Realisierung von Binärbäumen benutzt Sätze mit Verweisfeldern. In Abbbildung 2.9, Seite 72, wurde je ein Verweisfeld für den linken Nachfolger und ein Verweisfeld für den rechten Nachfolger benutzt. Damit lassen sich Einfüge- und Suchoperationen gut realisieren. Im allgemeinen Fall wird jedoch noch ein weiteres Feld, nämlich ein Verweis auf den Vatersatz, d. h. den unmittelbaren Vorgänger im Baum, benötigt. Abbildung 9.5 zeigt schematisch einen Ausschnitt aus einer solchen Realisierung. Man sieht, daß sie einer doppelten Verkettung entspricht. Wir wollen im folgenden bei Binärbäumen stets eine Realisierung durch Verweise voraussetzen. Um bei der Formulierung von Algorithmen in C-Pseudocode präziser werden zu können, wollen wir in den Sätzen der Binärbäume jeweils ein Feld left (linker Nachfolger), ein Feld right (rechter Nachfolger), eine Feld par (Vorgänger, parent) und ein Feld color (für Rot-Schwarz-Bäume) voraussetzen. Außerdem soll ein Feld key (Schlüsselfeld) vorhanden sein. Für dieses sei eine lineare Ordnung gegeben. Es sei erwähnt, daß Binärbäume auch anders als über Zeiger realisiert werden können. Ein Beispiel liefert die Binärsuche in Abschnitt 8.2. Die Reihung, in der gesucht wird, trägt implizit die Struktur eines Binärbaumes. Auch bei den Sortierverfahren Quicksort (Abschnitt 11.2) und Heapsort (Abschnitt 11.3) werden Binärbäume als Reihungen ge- 9.1. BINÄRE SUCHBÄUME 265 ....... ............ .. ...... .... .... ..... ..... .... .... ..... ..... .... .... ..... ..... .... .... ..... ..... .... .... ..... ..... .... .... ..... ... ◦ Satz1 ◦ ◦ ... .. ... ... .. .. ... ... ................. .................. ... .. .. .. .... ... .... ..... . ... . . . . . ... . ..... ... .. . . ... . . . . . . .... ... ... . . . ... . . . . . .... ... . ... . . . . . . . . ... ..... .. .... . . . ... . . . . ..... .. .. ... . . . . . . . . ..... ... ... .. . . . . . . . ... ..... .. .... . . . ... . . . . . ..... ... ... . ... . . . . . .... ... .. . . . . . . . . . . ... ..... .. .... . . . ... . . . . ..... .. ....... . . . . ..... ..... .. ....... . . . .... .... . .. ..... .. . ........ ......... ..... .......... .............. ..... ........ ............ ..... . .. ..... ◦ ◦ Satz2 ◦ .... .... .... .... ... . . .... ... .... ....... ............. . . .. satz3 ◦............ .... ... .... .... .... .... . .. ................. ... • ◦........... ... .... .... .... .... .... .. .................. ... Satz1 ist ein rechter Nachfolger. • ist ein NIL-Verweis. Abbildung 9.5: Realisierung von Binärbäumen durch Verweise speichert. Durchläufe durch einen Binärbaum Wie für Listen ist es auch für Binärbäume notwendig, Methoden zur Verfügung zu haben, mit denen der Datenbestand komplett durchlaufen werden kann. Man sagt, daß die Knoten nacheinander besucht (visit) werden. Von besonderer Wichtigkeit sind Methoden, die dabei die Struktur des Binärbaumes berücksichtigen. Wir definieren: W Besuche die Wurzel L Besuche die Knoten des linken Unterbaumes. Tue nichts, falls dieser leer. R Besuche die Knoten des rechten Unterbaumes. Tue nichts, falls dieser leer. Das ergibt 6 Methoden des Durchlaufs, nämlich WLR, WRL, LWR, RWL, LRW und RLW. Dabei wird vorausgesetzt, daß auch die Unterbäume rekursiv auf die gleiche Art durchlaufen werden. Tabelle 9.2 zeigt ein Programm für LWR. v ist darin eine Knotenva- 266 ' & KAPITEL 9. SUCHBÄUME void LWR (VERTEX ∗v) { 1 if (v == NULL) return; 2 LWR (v→lef t); 3 bearbeite (v); 4 LWR (v ← right); } $ % Tabelle 9.2: LWR-Durchlauf durch einen Binärbaum riable, die beim ersten Aufruf auf die Wurzel zeigt. Die Bearbeitung in Zeile 3 kann zum Beispiel in der Ausgabe des Schlüssels bestehen. Es ist unmittelbar klar, wie Programme für die anderen Durchlaufsarten aussehen. Drei Methoden haben eigene Namen. Preorder-Durchlauf steht für WLR, PostorderDurchlauf steht für LRW und Inorder-Durchlauf steht für LWR. Die Namen bezeichnen die Stellung der Wurzel in bezug auf linken und rechten Unterbaum. Die Durchlaufsmethoden, bei denen der rechte Unterbaum vor dem linken Unterbaum bearbeitet wird, haben keine besonderen Namen, aber im Allgemeinen durchaus eine andere Wirkung. Als Beispiel soll der Binärbaum von Abbildung 9.2 mit LWR durchlaufen werden. Es ergibt sich: ANTON, BESUCH, DER, HAT, HUT, LAGE, LUST, KNABE, NEIN, NOCH, PFAU, QUARK, ROT, RUHE, SEHEN, TANNE, ZAHN, ZANK. Mehr zu Durchläufen ist auf Seite 268 zu finden. 9.1.3 Binäre Suchbäume und Operationen auf ihnen Bei Aufbau und Benutzung von Binärbäumen ist die entscheidende Frage, wie festgelegt wird, welcher Satz linker bzw. rechter Nachfolger eines Satzes ist. Abbildung 6.2, Seite 180, zeigt, wie sich eine Binärbaumstruktur beim Mischsortieren durch die Zerlegung einer Liste in zwei Teillisten ergibt. Es gibt eine Vielzahl ähnlicher Beispiele. Die wichtigste Anwendung von Binärbäumen sind jedoch binäre Suchbäume und auf diese wollen wir uns im folgenden beschränken. Sie sind charakterisiert durch: Für jeden Knoten eines binären Suchbaumes gilt: Alle Knoten seines linken Unterbaumes haben einen kleineren Schlüsselwert. Alle Knoten seines rechten Unterbaumes haben einen größeren Schlüsselwert. Beispiel 9.1 Abbildung 9.2 zeigt die Schlüsselwerte von Beispiel 8.1, Seite 236, als Binärbaum, aber nicht als binären Suchbaum. Schlüsselwert KNABE müßte nämlich im linken Unterbaum von Schlüsselwert LAGE liegen. Abbildung 9.6 zeigt einen korrekten binären 9.1. BINÄRE SUCHBÄUME QUARK .......................... .......... .......... .......... .......... .......... .......... . . . . . . . . . .......... ... .......... .......... .......... .......... . . . . . . . .......... . . ...... . . .......... ... . . . . . . . . . .............. ............. . . . . . . . . . . . .... ........... PFAU RUHE ...... ....... ....... ........ ....... . . . . . . ....... ....... . ........ .......... ................ ............ ....... .............. ....... ....... ....... ........ ....... ....... . . . . . . ........ . ....... ....... ....... . ....... . . . . . . .......... . . .......... . . . . .................. ............ BESUCH ....... ROT ...... ............. ....... ....... ....... ........ ....... ....... . . . . . . ........ ........ ....... ....... ....... . . . . . . ....... .. . .......... . .......... . . . . .. .............. ............... ANTON TANNE ....... ...... ............. ....... ....... ....... ........ ....... ....... . . . . . . ........ ........ ....... ....... ....... . . . . . . ....... .. . .............. ......... . . .............. ................ KNABE ....... SEHEN ........ .............. ....... ....... ....... ....... ....... ....... ........ ....... . . . . . . ....... .... . . . . ....... . . . . ....... ... ................ . ... . .................. ................. HUT ...... ....... ....... ....... ....... . . . . . . . . ....... ....... . ....... ........... ............... HAT ........ ....... ........ ....... . . . . . . . ... ....... ....... . ........ ............ ............... DER ZAHN .... ........ ....... ....... ....... ........ ....... ....... ....... ....... ... .. ................... LUST ZANK ............ ....... ............. ....... ....... ....... ....... ........ ....... . . . . . . . ....... .. ....... ....... ....... . ....... . . . . . ....... .. . . ............ . . . . . ................... ........... LAGE NOCH ........ ....... ........ ....... . . . . . . . ... ....... ....... . ........ ............ ................ NEIN Abbildung 9.6: Binärer Suchbaum binbaum2 267 268 KAPITEL 9. SUCHBÄUME Suchbaum aus den gleichen Schlüsselwerten. Hierzu eine Anmerkung: Abbildung 9.6 ist keineswegs die einzige Möglichleit, die gegebene Schlüsselwertmenge als binären Suchbaum zu organisieren. Man kann z. B. leicht einen binären Suchbaum, in dem nur rechte Nachfolger vorkommen, daraus bilden. Es ist sogar möglich, in jeder Binärbaumstruktur mit 18 Knoten die Werte von Abbildung 9.6 so einzusetzen, daß sich ein binärer Suchbaum ergibt. Dieser ist eindeutig bestimmt. Siehe Aufgabe 9.1. 2 Operationen auf binären Suchbäumen Binäre Suchbäume benutzen die Binärbaumstruktur, um die Operationen auf einer abstrakten linear geordneten Liste von Schlüsselwerten effizient realisieren zu können. Sie dienen nicht der Darstellung hierarchischer Sachverhalte und deren Bearbeitung. Wir sollten also zunächst einmal die Möglichkeit haben, die Liste in aufsteigender bzw. absteigender Reihenfolge der Schlüsselwerte zu bearbeiten. Außerdem sollten die gleichen schlüsselwertabhängigen Einzeloperationen zur Verfügung stehen wie bei Listen. Das sind (siehe Seite 234): SIZE, SEARCH, FIND, INSERT, DELETE, MIN, MAX, NEXT, PREVIOUS. Wir wollen im folgenden zwischen Operationen, die den Suchbaum unverändert lassen, und solchen, die ihn verändern, unterscheiden. Durchläufe durch einen binären Suchbaum Die auf den Seiten 265ff eingeführten Durchlaufsformen WLR, WRL, LWR, RWL, LRW und RLW lassen sich natürlich auch auf binare Suchbäume anwenden. Als Beispiel sollen sie auf den Binärbaum von Abbildung 9.6 angewendet werden. Tabelle 9.3 zeigt, in welcher Reihenfolge die Knoten besucht werden. Es wird dabei jeder Knoten genau einmal besucht. D. h. wir haben z. B. W OCLW R = Θ(n) bzw. W OCRW L = Θ(n). Es fällt auf, daß LWR die Sätze in aufsteigender Reihenfolge der Schlüsselwerte liefert und RWL in absteigender. Das ist kein Zufall. Es gilt nämlich die folgende Proposition. Proposition 9.2 1. Ein Binärbaum ist genau dann ein binärer Suchbaum, wenn die Durchlaufreihenfolge LWR die Schlüsselwerte in aufsteigender Reihenfolge liefert. 2. Ein Binärbaum ist genau dann ein binärer Suchbaum, wenn die Durchlaufreihenfolge RWL die Schlüsselwerte in absteigender Reihenfolge liefert. Beweis: Behauptung 1. Es sei ein Binärbaum gegeben und k0 , k1 , . . . , kn , kn+1 , . . . die Reihenfolge in der LWR die Schlüsselwerte liefert (siehe Tabelle 9.2). Ist der Binärbaum ein binärer Suchbaum, so haben alle Sätze, die vor kn auftreten, einen kleineren Schlüsselwert als kn und alle, die danach auftreten, einen größeren. Die Sätze erscheinen in aufsteigender Schlüsselwertreihenfolge. Es liefere LWR für den Binärbaum eine andere Reihenfolge als aufsteigende Schlüsselwerte. Dann gibt es ein i und ein j > i mit ki > kj . Es liegt dann entweder Satz i im linken Unterbaum von Satz j oder es gibt einen Satz n, in dessen linken Unterbaum Satz i und in dessen rechten Unterbaum Satz j liegt. Der Binärbaum ist kein binärer Suchbaum. 9.1. BINÄRE SUCHBÄUME WLR QUARK PFAU BESUCH ANTON KNABE HUT HAT DER LUST LAGE NOCH NEIN RUHE ROT TANNE SEHEN ZAHN ZANK WRL QUARK RUHE TANNE ZAHN ZANK SEHEN ROT PFAU BESUCH KNABE LUST NOCH NEIN LAGE HUT HAT DER ANTON 269 LWR ANTON BESUCH DER HAT HUT LAGE LUST KNABE NEIN NOCH PFAU QUARK ROT RUHE SEHEN TANNE ZAHN ZANK RWL ZANK ZAHN TANNE SEHEN RUHE ROT QUARK PFAU NOCH NEIN KNABE LUST LAGE HUT HAT DER BESUCH ANTON LRW ANTON DER HAT HUT LAGE NEIN NOCH LUST KNABE BESUCH PFAU ROT SEHEN ZANK ZAHN TANNE RUHE QUARK RLW ZANK ZAHN SEHEN TANNE ROT RUHE NEIN NOCH LAGE LUST DER HAT HUT KNABE ANTON BESUCH PFAU QUARK Tabelle 9.3: Durchläufe durch einen binären Suchbaum Der Beweis für die Behauptung 2. verläuft analog. 2 Anmerkung 9.2 Liegen die Schlüsselwerte in einer anderen binären Suchbaumstruktur vor, z. B. so wie es die Ersetzung der Abbildung 9.8 zeigt, so ergibt LWR (RWL) natürlich auch wieder die aufsteigende (absteigende) Reihenfolge der Schlüsselwerte. Die Reihefolge bei anderen Durchlaufsformen ändert sich durchaus. Z. B. liefert WRL die Reihenfolge QUARK, RUHE, TANNE, ZAHN, ZANK, SEHEN, ROT, PFAU, BESUCH, KNABE, NEIN, NOCH, LUST, LAGE, HUT, HAT, DER, ANTON. 2 Operationen, die die Struktur unverändert lassen SIZE: Als Ergebnis wird die Anzahl Sätze im Baum zurückgeliefert, Null für einen leeren Baum. Diesen Werte kann man wie bei Listen in einer Gesamtbeschreibung des Baumes führen. Es ist jedoch manchmal zweckmäßig, eine Prozedur zu haben, die die Größe eines jeden Unterbaumes liefert. Tabelle 9.4 zeigt eine solche Prozedur. Der zu Knoten v gehörende Unterbaum wird in RLW Reihenfolge durchlaufen. Die Prozedur ist für alle Binärbäume, nicht nur für binäre Suchbäume sinnvoll. Der Aufwand ist W OCSIZE = Θ(n). 270 ' & KAPITEL 9. SUCHBÄUME int SIZE (VERTEX ∗v) { 1 if (v == NULL) return 0; 2 return (SIZE(v→right) + SIZE(v→lef t) + 1); } $ % Tabelle 9.4: Operation SIZE in einem MIN und MAX : Es wird die Adresse des Satzes mit dem kleinsten (größten) Schlüsselwert zurückgeliefert. In Tabelle 9.5 ist eine Realisierung für MIN zu sehen. Eine Realisierung für MAX sieht ganz entsprechend aus. Es wird ein Weg von der Wurzel zu einem Blatt zurückgelegt und alle ' & VERTEX ∗MIN (VERTEX ∗v) { 1 if (v == NULL) return NULL; 2 if (v→lef t == NULL) 3 {return v;} 4 else 5 {return MIN(v→lef t);} } $ % Tabelle 9.5: Minimum in einem Binärbaum Knoten dieses Weges besucht. Das bedeutet W OCM IN = W OCM AX = h, für buschige Bäume also W OCM IN = W OCM AX = Θ(ln(n)). Anmerkung 9.3 MIN und MAX liefern nur die Adresse des „linkesten“ („rechtesten“) Knoten im Baum. Das kann unter Umständen auch für Binärbaume, die keine Suchbäume sind, von Interesse sein. Wird in einem binären Suchbaum der kleinste (größte) Schlüsselwert gesucht, so muß dieser dem gefundenen Knoten explizit entnommen werden. Die Prozeduren sind so vom Datentyp der Schlüsselwerte unabhängig. 2 NEXT und PREVIOUS: Es wird zu einem Knoten die Adresse des Satzes mit nächstgrößerem (nächstkleinerem) Schlüsselwert zurückgeliefert. NULL, falls es diesen Satz nicht gibt. Im folgenden wird nur NEXT erläutert. Die Betrachtungten für PREVIOUS können leicht sinngemäß ergänzt werden. 9.1. BINÄRE SUCHBÄUME 271 Der nächstgrößere Schlüsselwert, wenn es ihn gibt, ist der kleinste Schlüsselwert unter den größeren. Wie können wir diesen Schlüsselwert finden? Dazu betrachten wir den Weg von der Wurzel zum gegebenen Knoten v. Abbildung 9.7 zeigt ein Beispiel. Wir wollen einen ................ ... .. .... . .... ....... ......... ... ... .... ... .. .................. ......... ...... ..... . .. .... . .... 1..... .......... ... .... .... .... .. ................. ......... ...... .... ... .... .. 2.... ................ . . . .. . . . .. . .... ..... ....... ............. ..... ......... .. ..... ... 3... ................. .... .... .... . .. ................. ......... ..... .... .. ..... . ... 4.... ................. .... .... .... . .. .................. ......... . . .... ..... .. ..... .... 5...... ........... .... .... .... .... .. ................. ......... .... .... ... ..... .. ... ................. w u u u u u v Abbildung 9.7: Linksvorgänger unf Rechtsvorgänger Knoten auf diesem Wege einen Linksvorgänger nennen, wenn sein Nachfolger auf dem Weg sein linker Nachfolger ist. Entsprechend ist Rechtsvorgänger definiert. Im Beispiel sind w, u1 , u3 , u4 und u5 Rechtsvorgänger. u2 ist Linksvorgänger. Die Schlüsselwerte eines Rechtsvorgängers ebenso wie die seines linken Unterbaumes sind kleiner als der Schlüsselwert von v und brauchen nicht berücksichtigt zu werden. Die Schlüsselwerte eines Linksvorgängers sowie die seines rechten Unterbaumes sind größer als die Schlüsselwerte aller Knoten, die ihnen auf dem Weg folgen, sowie deren Unterbäume. Insbesondere sind sie größer als der Schlüsselwert von v und als die Schlüsselwerte des rechten Unterbaumes von v. Das bedeutet: Der Knoten mit nachstgrößerem Schlüsselwert ist der mit kleintstem Schlüsselwert im rechten Unterbaum. Ist der rechte Unterbaum leer, so ist der Knoten mit nächstgrößerem Schlüsselwert der erste Linksvorgänger auf dem Weg vom Knoten zur Wurzel. Gibt es keinen solchen Linksvorgänger, so hat der Knoten den größten Schlüsselwert im Baum und es gibt keinen mit nächstgrößerem Schlüsselwert. Damit ergibt sich zur Bestimmung des Knotens mit nächstgrößerem Schlüssel die in Tabelle 9.6 angegebene Prozedur. Zur Bestimmung der Komplexität überlegt man sich, daß NEXT (und auch PREVIOUS) entweder einen Weg im rechten (linken) Unterbaum des Knotens oder höchstens in Rückwärtsrichtung den Weg vom Knoten zur Wurzel durchläuft. Das bedeutet W OCN EXT = W OCP REV IOU S = h und für buschige Bäume W OCN EXT = W OCP REV IOU S = Θ(ln(n)). 272 ' KAPITEL 9. SUCHBÄUME VERTEX ∗NEXT (VERTEX ∗v) { 1 VERTEX ∗u; 2 3 4 5 6 & 7 8 9 } if (v == NULL) return NULL; if (v→right != NULL) return MIN(v→right); u = v; while (u→par != NULL) { if (u == (u→par)→lef t) return u→par; u = u→par; } return NULL; $ % Tabelle 9.6: Knoten mit nächstgrößerem Schlüssel Anmerkung 9.4 Ähnlich wie in Anmerkung 9.3 wird bei NEXT und PREVIOUS nur die Struktur des Binärbaumes benutzt und der Datentyp des Schlüsselfeldes nicht gebraucht. Die beiden Anweisungen können auch auf Binärbäume, die keine binären Suchbäume sind, angewandt werden. 2 SEARCH: Es wird ein Satz des Baumes nach seinem Schlüsselwert gesucht und bereitgestellt. Wird der Satz nicht gefunden, so wird das zurückgemeldet. Bei der Suche wird in der Wurzel des Baumes begonnen. Ist deren Schlüsselwert gleich dem Suchwert, so hat man den Knoten gefunden. Ist der Suchwert kleiner, so fährt man mit dem linken Unterbaum fort. Ist er größer, so wird mit dem rechten fortgesetzt. Auf diese Weise findet man entweder den Satz mit dem gegebenen Suchwert oder man stößt zum ersten Mal auf einen leeren Unterbaum. Ist das der Fall, so weiß man, daß es einen Knoten mit dem gegebenen Suchwert im Baum nicht gibt. Diese Vorgehen läßt sich leicht rekursiv programmieren, wie die Prozedur in Tabelle 9.7 zeigt. Sie wird mit der Wurzel des Baumes und dem Suchwert als Parametern aufgerufen. Darin wird angenommen, das die Schlüsselwerte vom Typ int sind. Sind sie von einem anderen Datentyp, z. B. Zeichenreihen, so müssen die entsprechenden Vergleichsoperationen genommen werden. Da für SEARCH maximal ein Weg von der Wurzel zu einem Blatt durchlaufen wird, gilt für die Komplexität auch hier W OCSEARCH = h und für buschige Bäume W OCSEARCH = Θ(ln(n)). 9.1. BINÄRE SUCHBÄUME ' & 273 VERTEX ∗SEARCH(VERTEX ∗v, int schluessel) { 1 if (v == NULL) return NULL; 2 if (v→key == schluessel) return v; 3 if (schluessel < v→key) return SEARCH(v→lef t, schluessel); 4 if (schluessel > v→key) return SEARCH(v→right, schluessel); } $ % Tabelle 9.7: Prozedur SEARCH FIND Es wird ein Satz des Baumes nach seinem Schlüsselwert gesucht und bereitgestellt. Ist ein Satz mit dem gegebenen Schlüsselwert nicht in der Liste und dieser kleiner als der größte und größer als der kleinste existierende Schlüsselwert, so wird der Satz mit dem nächstkleineren Schlüsselwert bereitgestellt. Anderenfalls wird ein Nullverweis zurückgeliefert. Zu FIND siehe auch Seite 234, insbesondere die Fußnote. Tabelle 9.8 zeigt eine Realisie' & VERTEX ∗FIND(VERTEX ∗v, int schluessel) { 1 if (v == NULL) return NULL; 2 if (v→key == schluessel) return v; 3 if (schluessel < v→key) 4 { if {(v→lef t != NULL) return FIND(v→lef t, schluessel);} 5 else {return PREVIOUS(v);} 6 } 7 if (schluesse > v→key) 8 { if {(v→right != NULL) return FIND(v→right, schluessel);} 9 else 10 { if {(NEXT(v) == NULL) return NULL;} 11 else {return v;} 12 } } $ % Tabelle 9.8: Prozedur FIND rung von FIND. Anhand dieser Prozedur soll das Vorgehen erläutert werden. Wenn es einen Satz mit dem Suchwert gibt, wird er mit den Zeilen 2, 4 und 8 genau so gefunden 274 KAPITEL 9. SUCHBÄUME wie bei SEARCH. Wenn es ihn nicht gibt, tritt entweder die Abbruchbedingung in Zeile 5 oder die Abbruchbedingung in Zeile 9 ein. Zeile 5 wird erreicht, wenn die Suche im aktuellen linken Unterbaum fortgesetzt werden müßte, aber dieser leer ist. Da alle Rechtsvorgänger des aktuellen Knoten kleinere Schlüsselwerte haben als die Werte, die wie der Suchwert in den linken Unterbaum fallen würden, ist der größte unter ihnen der unmittelbare Vorgänger des aktuellen Knotens und auch des hypotetischen Knotens mit dem Suchwert als Schlüsselwert. Wenn es diesen Vorgänger nicht gibt, ist der Suchwert kleiner als der kleinste existierende Schlüsselwert und es wird korrekt NULL zurückgeliefert. Zeile 9 wird erreicht, wenn die Suche im aktuellen rechten Unterbaum fortgesetzt werden müßte, aber dieser leer ist. Dann kann es sein, daß der aktuelle Knoten v den größten Schlüsselwert im Baum hat (Zeile 10). Wenn das nicht der Fall ist, gibt es Knoten mit größerem Schlüsselwert. Der kleinste unter ihnen ist NEXT(v) und dessen Schlüsselwert ist größer als der Suchwert. v ist demnach der Knoten mit nächstkleinerem Schlüsselwert. Auch hier gilt wieder W OCF IN D = h und für buschige Bäume W OCF IN D = Θ(ln(n)). Beispiel 9.2 Wir betrachten die Werte aus Tabelle 8.1, Seite 236. Sie mögen als binärer Suchbaum vorliegen. Es soll mit FIND der Schlüsselwert NAHT gefunden werden. Er liegt in dem Intervall, das von den Schlüsselwerten LUST und NEIN begrenzt wird. Unabängig von der Struktur des vorliegenden binären Suchbaumes muß FIND den Satz mit dem Schlüsselwert LUST zurückliefern. In dem binären Suchbaum darf LUST keinen rechten Unterbaum oder NEIN keinen linken Unterbaum haben. In der Binärbaumstruktur von Abbildung 9.6 hat NEIN keinen linken Unterbaum und Zeile 5 liefert des Satz mit Schlüsselwert LUST als Ergebnis. Ersetzt man in Abbildung 9.6 den rechten Unterbaum von KNABE so, wie es Abbildung 9.8 angibt, so ergibt sich wieder ein binärer Suchbaum für die Schlüsselwerte. Jetzt hat LUST ..... .... ......... ...... ...... ...... ...... ...... ..... . . . . . ...... ..... . ...... . . . ... ...... . . . . . ...... ..... . . ...... . . ... ...... . . . . . ...... ..... . . ......... . . . ............. . . . .................. ...... LAGE NOCH ... ...... ...... ...... ...... ..... . . . . . .. ...... ..... ..... ...... ...... . . . . . . .. ......... ............. NEIN NEIN ..... .... ......... ...... ...... ...... ...... ...... ..... . . . . . ...... ..... . ...... . . . ... ...... . . . . . ...... ..... . . ...... . . ... ...... . . . . . ...... ..... . . ......... . . . ............. . . . .................. ....... =⇒ LUST ... NOCH ...... ...... ...... ...... ..... . . . . . .. ...... ..... ..... ...... ...... . . . . . . .. ......... ............. LAGE Abbildung 9.8: Ersetzung eines Unterbaumes jedoch LUST keinen rechten Unterbaum und der Satz mit dem Schlüsselwert LUST wird von Zeile 11 geliefert. 2 9.1. BINÄRE SUCHBÄUME 275 Operationen, die die Struktur verändern Mit diesen Operationen wird ein neuer Satz in einen binären Suchbaum eingefügt bzw. es wird ein Satz aus dem Baum entfernt. Bedingung ist, daß auch nach der Operation ein binärer Suchbaum vorliegt. Diese Bedingung kann auf ganz unterschiedliche Art und Weise erfüllt werden, z. B. dadurch, daß mit der neuen Schlüsselwertmenge eine ganz neue Binärbaumstruktur aufgebaut wird. In diesem Kapitel wollen wir „naheliegende“ Realisierungen der Operationen INSERT und DELETE betrachten. Darunter wollen wir Realisierungen verstehen, die die Struktur des gegebenen Binärbaumes weitestgehend erhalten. Man spricht auch von „naiven“ Realisierungen (naives Einfügen, naives Löschen). INSERT: Es wird ein weiterer Satz so eingefügt, daß die Eigenschaft eines binären Suchbaumes erhalten bleibt. Ist ein Satz mit dem gleichen Schlüsselwert in dem Baum schon vorhanden, so erfolgt eine Fehlermeldung. Es wird wie bei SEARCH im Baum die Stelle gesucht, an der Satz stehen müßte. Steht dort schon ein Satz, so wird fälschlicherweise, versucht einen Schlüsselwert zum zweiten Mal einzufügen. Andernfalls findet man ein leeres rechtes oder linkes Nachfolgerfeld und kann den neuen Satz dort einfügen. Tabelle 9.9 zeigt eine Realisierung der Prozedur. Diese liefert TRUE zurück, wenn der Satz eingefügt wurde, und FALSE, wenn der entsprechende Schlüsselwert schon vorhanden ist. Die Wurzel des Baumes ist t, der einzufügende Satz ist v. Beide sind vom gleichen Typ. Von v wird vorausgesetzt, daß es nicht NULL ist. Wie leicht zu sehen, gilt W OCIN SERT = h und für buschige Bäume W OCIN SERT = Θ(ln(n)). DELETE: Es wird der Satz mit dem gegebenen Schlüsselwert so aus dem Baum entfernt, daß die Eigenschaft eines binären Suchbaumes erhalten bleibt. Existiert kein Satz mit diesem Schlüsselwert, so erfolgt eine Fehlermeldung. Es sei v der Knoten, der aus der dem Baum entfernt werden soll. Wir nehmen zunächst einmal an, daß v die Wurzel des Baumes ist, und unterscheiden drei Fälle. v hat keinen Nachfolger. Dann bleibt nach dem Löschen ein leerer Baum zurück. v hat genau einen Nachfolger. Dann wird dieser Nachfolger Wurzel des neuen Baumes. v hat zwei Nachfolger. Der in der Schlüsselwertreihenfolge auf v folgende Knoten sei v 0 Dieser Knoten gehört zum rechten Unterbaum von v und ist dort Wurzel oder linker Nachfolger. In jedem Fall muß der linke Unterbaum von v 0 leer sein. Der rechte Unterbaum braucht nicht leer zu sein. Wenn v 0 nicht Wurzel des rechten Unterbaumes von v ist, wird es zunächst einmal zur Wurzel dieses Unterbaumes gemacht. Dazu wird v 0 herausgelöst und sein rechter Unterbaum wird zum linken Unterbaum seines Vorgängers. Nach dieser Umformung ist es nun leicht, v aus dem ursprünglichen Binärbaum zu entfernen. v 0 wird die Wurzel des neuen Binärbaumes. Der linke Unterbaum von v wird zum linken Unterbaum von v 0 , der rechte Unterbaum von v 0 bleibt so, wie er sich nach der Umformung ergab. Es bleibt noch der Fall, daß der zu entfernende Knoten v nicht Wurzel des gegebenen 276 ' KAPITEL 9. SUCHBÄUME BOOLEAN INSERT(VERTEX ∗ ∗ t, VERTEX ∗v) 1 { if (∗t == NULL) //v wird Wurzel 2 { v→par = NULL; 3 v→right = NULL; 4 v→lef t = NULL; 5 ∗t = v; 6 return TRUE; 7 } 8 else 9 { if (∗t→key == v→key) return FALSE; // Schlüsselwert existiert schon 10 if (v→key < ∗t→key) 11 { if (∗t→lef t != NULL) 12 { return INSERT(&(∗t→lef t), v); } 13 else 14 { v→par = ∗t 15 v→lef t = NULL; 16 v→right = NULL; 17 ∗t→lef t = v; // v wird linker Nachfolger von ∗t; 18 return TRUE; 19 } } 20 if (v→key > ∗t→key) 21 { if (∗t→right != NULL) 22 { return INSERT(∗t→right, v); } 23 else 24 { v→par = ∗t 25 v→lef t = NULL; 26 v→right = NULL; 27 ∗t→right = v; // v wird rechter Nachfolger von ∗t; 28 return TRUE; 29 } } } } & $ % Tabelle 9.9: Prozedur INSERT Binärbaumes ist. v ist dann aber Wurzel eines Unterbaumes und diese kann wie oben beschrieben gelöscht werden. Es ergibt sich ein neuer Unterbaum, der an den Vorgänger von v angefügt werden kann. Im ursprünglichen Binärbaum weisen v und beide Unterbäume 9.2. ROT-SCHWARZ-BÄUME 277 von v nämlich sämtlich größere oder sämtlich kleinere Schlüsselwerte als der Vorgänger von v auf. Anmerkung 9.5 Im Fall, daß v zwei Nachfolger hat, kann man auch den Knoten mit nächstkleinerem Schlüsselwert suchen. Der liegt im linken Unterbaum von v. Das weitere Vorgehen ist dann anlog zu dem oben beschriebenen. 2 Tabelle 9.10 zeigt eine Realisierung einer Prozedur für DELETE. Sie ruft die Prozedur ' BOOLEAN DELETE(VERTEX ∗∗t, int key) 1 VERTEX ∗v, ∗w, ∗p; // v wird gelöscht // w ersetzt v // p ist der Vorgänger von v, nach dem Löschen von w 2 BOOLEAN brechts; 3 4 5 6 7 8 9 10 11 } $ v = SEARCH(∗t, key); if ( v == NULL) return FALSE; // Schlüsselwert nicht im Baum p = v→par; w = ROOTDELETE(v); if (p == NULL) { ∗t = w; if (w != NULL) w→par = p; return TRUE;} brechts = (v == p→right); if (brechts) {p→right = w;} else {p→lef t = w;} return TRUE; & % Tabelle 9.10: Prozedur DELETE ROOTDELETE (siehe Tabelle 9.11) auf, in der die eigentliche Löschung passiert. 9.2 9.2.1 Rot-Schwarz-Bäume Definition und Eigenschaften von Rot-Schwarz-Bäumen Bie der Behandlung der Operationen in binären Suchbäumen (Unterabschnitt 9.1.3) haben wir gesehen, daß deren Komplexität Θ(ln(n)) ist, wenn es sich um buschige Bäume handelt. Ein buschiger Baum bleibt buschig, wenn Operationen, die ihn nicht verändern, auf ihn angewandt werden. Wir haben also sehr günstige binäre Suchbäume, wenn wir 278 ' & KAPITEL 9. SUCHBÄUME VERTEX ROOTDELETE(VERTEX ∗v) { 1 VERTEX ∗w; // v wird gelöscht // w ersetzt v // 2 // 3 4 // 5 6 // 7 8 9 10 11 12 13 14 15 } $ Keine Nachfolger if (v→lef t == NULL && v→right == NULL) return NULL; Nur rechter Nachfolger if (v→lef t == NULL && v→right != NULL) return v→right; Nur linker Nachfolger if (v→lef t != NULL && v→right == NULL) return v→lef tf ; Zwei Nachfolger w = NEXT(v); w→lef t = v→lef t; w→lef t→par = w; if (w == v→right) return w; (w→par)→lef t = w→right; if ((w→right != NULL) w→right→par = w→par; w→right = v→rightf ; v→right→par = w; return w; % Tabelle 9.11: Prozedur ROOTDELETE mit einem buschigen Baum beginnen und alle verändernden Operationen wieder einen buschigen Baum liefern. Rot-Schwarz-Bäume sind eine Datenstruktur, die das gewährleistet. Ein Rot-Schwarz-Baum ist ein binärer Suchbaum, in dem jeder Knoten genau eine der Farben SCHWARZ oder ROT aufweist. Die Farben werden benutzt, um zu erzwingen, daß alle Wege von der Wurzel zu einem Blatt Längen in der gleichen Größenordnung haben. Das allein reicht für Buschigkeit jedoch nicht. Es muß weiter garantiert werden, daß auch alle Wege von der Wurzel zu einer Stelle, an der ein „Ast abgeschnitten“ wurde, Längen in der gleichen Größenordung haben. Dazu lassen wir als Endknoten eines Weges auch NIL-Verweise zu und führen rb-Wege ein. Ein rb-Weg in einem binären Suchbaum ist ein 9.2. ROT-SCHWARZ-BÄUME 279 Weg, der in einem (echten) Knoten beginnt und in einem NIL-Verweis endet. Ein Beispiel soll das verdeutlichen. Beispiel 9.3 In Abbildung 9.9 ist der linke Suchbaum aus Abbildung 9.8, Seite 274, zu sehen. Die NIL-Verweise wurden hinzugefügt. Beispiele für rb-Wege sind: NOCH-NEINLUST .......... ..... .......... ..... ...... ...... ...... ...... ...... . . . . . ...... ...... ...... ...... ...... . . . . ...... ... . . . . . ...... ..... . ...... . . . ...... .. ........... . .. .......... . . .................... . . . ... LAGE .. .. ... ... ... ... .. ... . • NOCH .. ... .... ...... ...... ...... . . . . . .. ...... ..... ..... ...... ...... . . . . .. .. ....... ......... ............. ... ... ... ... .. ... . • ... ... ... ... .. ... . • NEIN. ... ... ... ... .. ... .. • ... .. ... ... .. ... .. • Abbildung 9.9: rb-Wege rechtsNEIN oder LUST-LAGE-linksLAGE oder NEIN-rechtsNEIN oder LUST-NOCHrechtsNOCH. 2 Definition 9.2 Ein binärer Suchbaum ist ein Rot-Schwarz-Baum, wenn er die folgenden Eigenschaften hat: 1. Jeder Knoten ist entweder rot oder schwarz. 2. Alle Nachfolger eines roten Knotens sind schwarz. 3. Alle rb-Wege, die von einem Knoten ausgehen, weisen die gleiche Anzahl schwarzer Knoten auf. 2 Der folgende, recht nützliche Hilfssatz ergibt sich unmittelbar aus der Definition Hilfssatz 9.1 Hat eine Knoten eines Rot-Schwarz-Baumes nur einen Nachfolger, so besteht der entsprechende Unterbaum aus nur einem roten Knoten und der Knoten selbst ist schwarz. Rot-Schwarz-Bäume sind in der Tat buschig, wie sich aus dem folgenden Satz eribt. Satz 9.2 Für die Höhe h(n) eines Rot-Schwarz-Baumes mit n Knoten gilt h(n) = Θ(ln(n)). 280 KAPITEL 9. SUCHBÄUME Beweis: h(n) kann nicht kleiner als die minimale Höhe in einem Binärbaum sein. Nach Gleichung 9.1, Seite 262, gilt also h(n) = Ω(ln(n)). Bleibt zu zeigen, daß auch h(n) = O(ln(n)) gilt. Es seien also B ein Rot-Schwarz-Baum mit n Knoten und bh(B) die Anzahl schwarzer Knoten auf einem rb-Weg, der in der Wurzel beginnt. n = 0 sei zugelassen. In diesem Fall ist bh(B) = 0. Behauptung: Der Baum hat mindestens n ≥ 2bh(x) − 1 Knoten. Beweis durch vollstandige Induktion: n = 0. Für einen leeren Baum gilt bh(x) = 0, also n = 20 − 1 = 0. Die Behauptung sei richtig für v = 0, 1, . . . , n − 1. Es sei B ein Rot-Schwarz-Baum mit n ≥ 1 Knoten. B1 sei der linke Unterbaum und B2 der rechte der Wurzel von B. Die Anzahl der Knoten des Baumes ist n = n1 + n2 + 1, wobei n1 die Anzahl der Knoten von B1 und n2 die Anzahl der Knoten von B2 ist. Ist die Wurzel von B schwarz, gilt bh(B1 ) = bh(B) − 1, anderenfalls bh(B1 ) = bh(B). Entsprechendes gilt für B2 . In jedem Fall haben wir n = n1 +n2 +1 ≥ 2bh(B)−1 −1+2bh(B)−1 −1+1 = 22bh(B)−1 −2+1 = 2bh(B) −1. Die Höhe h eines Rot-Schwarzbaumes ist die Anzahl Knoten in einem längsten Wege von der Wurzel zu einem Blatt. Mindestens die Hälfte der Knoten muß schwarz sein. Genauer . Für gerades bh(B) ≥ b h2 c. Ist h gerade, so ist b h2 c = h2 . Ist h ungerade, so ist b h2 c = h−1 2 h bh(B) h ergibt sich n ≥ 2 − 1 ≥ 2 2 − 1, also 2ld (n + 1) ≥ h. Für ungerades h finden wir 2ld (n + 1) + 1 ≥ h. In beiden Fällen ist h(n) = O(ln(n)). 2 9.2.2 Operationen auf Rot-Schwarz-Bäumen Alle Operationen aus Unterabschnitt 9.1.3, die den Baum nicht verändern, können direkt auf Rot-Schwarz-Bäume angewandt werden. Das sind vollständige Durchläufe, SIZE, MIN, MAX, NEXT, PREVIOUS, SEARCH, FIND. Für Durchläufe und SIZE gilt W OC = Θ(n), für alle übrigen W OC = Θ(ln(n)). Wenn die Operationen Einfügen und Löschen eines Satzes mit Auwand Θ(ln(n)) ausgeführt werden und nach ihnen der Baum weiterhin ein RotSchwarz-Baum bleibt, dann haben wir einen vollständigen Satz effizienter Operationen für Rot-Schwarz-Bäume. Man kann in der Tat Einfügungen und Löschungen so ausführen, daß beide Bedingungen erfüllt sind. Beim Einfügen erreichen wir das, indem wir einen neuen Satz auf naive Art einfügen und danach Veränderungen, die unabhängig von der Größe des Baumes nur eine feste Anzahl von Operationen benutzen, sogenannten lokale Veränderungen (local changes), anwenden und in höchstens O(ln(n)) Schritten den Baum zu einem Rot-Schwarz-Baum umformen. Beim Löschen kann man analog vorgehen. Allerdings werden wir einen etwas abweichenden Lösungsweg verfolgen. Außerdem werden wir die beiden Operationen so implementieren, daß die Wurzel stets schwarz ist. Rotationen Rotationen (rotation) sind Operationen, die lokale Veränderungen auf einem binären Suchbäum so bewirken, daß danach weiterhin ein binärer Suchbaum vorliegt. Es gibt die Ope- 9.2. ROT-SCHWARZ-BÄUME 281 rationen Rechtsrotation (right rotation) und Linksrotation (left rotation). Sie sind invers zueinander. Abbildung 9.10 zeigt schematisch die Wirkung von Links-und Rechtsrotation. ... ... ... ... ... ......... ......... .. Rechtsrotation =⇒ Y X .... .... ... .... . . . .... .. .... ........... ....... .... ... .... .... ... . . . ... .. ... ........... ...... .... .... .... .... .... .... . .. . ................. .. w .... ... .... .... .... .... ....... ............... .. ... ... ... ... ... ......... ......... .. ⇐= X ..... ...... ...... ...... ...... . . . . ..... .. ...... ......... ............ u .. .... .... ... . . . . .... .. .... ........... ...... Linksrotation v u .... .... .... .... .... .... . .. . ................. .. Y .... ... .... .... .... .... ... .. ................. .. v w Abbildung 9.10: Rotationen (nach [CormLR1990]) Man kann erkennen, daß die Eigenschaft, ein binärer Suchbaum zu sein, erhalten bleibt. Rotationen verändern keine Farben. Pseudocode für die Rotationen ist in den Tabellen 9.12 und 9.13 angegeben. ' $ & % void LEFTRORATE (VERTEX ∗ ∗ t, VERTEX ∗Y ) 1 { VERTEX ∗X; 2 X = Y →par; 3 if (X == NULL) { fehlermeldung; return; }// Kein Vater 4 if (X→right) != Y) { fehlermeldung; return; } // Nicht rechter Nachfolger 5 Y →par = X→par; 6 if (Y →par != NULL) // Y an Vorgänger von X anhängen 7 { if (isleft (X)) { Y →par→lef t = Y ; } else { Y →par→right = Y ; }} 8 else // Y wird Wurzel 9 { ∗t = Y ; } 10 X→right = Y →lef t; 11 Y →lef t = X; 12 X→par = Y ; 13 if (X→right != NULL) X→right→par = X; 14 return; Tabelle 9.12: Prozedur LEFTROTATE 282 ' KAPITEL 9. SUCHBÄUME void RIGHTRORATE (VERTEX ∗ ∗ t, VERTEX ∗X) 1 { VERTEX ∗Y ; 2 Y = X→par; 3 if (Y == NULL) { fehlermeldung; return; } // Kein Vater 4 if (Y →lef t) != X) { fehlermeldung; return; } // Nicht linker Nachfolger 5 X→par = Y →par; 6 if (X→par != NULL) // X an Vorgänger von Y anhängen 7 { if (isleft (Y )) { X→par→lef t = X; } else { X→par→right = X; }} 8 else // X wird Wurzel 9 { ∗t = X; } 10 Y →lef t = X→right; 11 X→right = Y ; 12 Y →par = X; 13 if (Y →lef t != NULL) Y →lef t→par = Y ; 14 return; & $ % Tabelle 9.13: Prozedur RIGHTROTATE RBINSERT Hier soll eine Lösung fur das Einfügen vorgestellt werden. Es sei ein Rot-Schwarz-Baum gegeben und es soll ein neuer Satz eingefügt werden. Dazu wird er zunächst naiv, also mit INSERT eingefügt. Er wird dann rot gefärbt. Wenn der Satz, an den er angehängt wurde, schwarz ist, bleibt der erweiterte Baum ein Rot-Schwarzbaum. Anderenfalls ist von den Bedingungen der Definition 9.2 nur Punkt 2. verletzt. Das weitere Vorgehen läßt sich am besten anhand eines Beispiels erklären. Beispiel 9.4 Abbildung 9.11 zeigt im oberen Teil einen binären Suchbaum mit roten Knoten (kursiv) und schwarzen Knoten (Fettdruck). Ohne den Knoten HUT ist es ein Rot-Schwarz-Baum. Wird HUT als roter Knoten an der entsprechenden Stelle eingefügt, so folgen die roten Knoten KNABE und HUT aufeinander. Im übrigen bleibt es ein korrekter Rot-Schwarz-Baum. Um die aufeinander folgenden beiden roten Knoten aufzulösen, sehen wir uns den Vorgänger von KNABE, nämlich den Knoten LAGE an. Er ist, wie es sein muß, schwarz. Sein zweiter Nachfolger, der Knoten QUARK, muß rot sein (warum?) und ist das auch. Man kann nun, ohne die Struktur des Baumes zu ändern, durch einfaches Umfärben weiterkommen: LAGE wird rot und KNABE und QUARK werden schwarz. Im unteren Teil von Abbildung 9.11 ist das Ergebnis dargestellt. Auf der untersten Stufe ist damit die Farbkollision beseitigt. Leider ist in unserem Beispiel eine Stufe darüber (also näher an der Wurzel) jedoch eine neue Farbkollision aufgetreten: 9.2. ROT-SCHWARZ-BÄUME 283 SEHEN ...... ..... ............. ............. ............. ............. ............ . . . . . . . . . . . . ......... ............. ..... ............. ...................... . ...... ....... ....... ...... . . . . . .. ...... . ........ ......... .............. DER .... ............. ............. ............. ............. ............. ............. ............. ............. .. ................... ............. TANNE .... ...... ....... ....... ....... ....... ....... ....... .. .. ................... BESUCH ...... ....... ....... ....... ....... ....... ....... .. .. .................... LAGE... ... ..... ..... ..... ..... . . . . .... ........ ........... ........ KNABE ZANK ..... ..... ..... ..... ..... ..... ..... ... ................... . QUARK ... ..... ..... ..... . . . . .. ..... .. ..... ............ ....... HUT Fall 1 SEHEN ....... . ............ ............. ............. ............. . . . . . . . . . . . ............ ............. ............. ..... .............. ...................... .... ....... ...... ...... ...... . . . . . . ...... . ........ ......... .............. DER ... ....... ....... ....... ....... ....... ....... .......... ................... BESUCH KNABE TANNE ... ....... ....... ....... ....... ....... ....... .......... .................... LAGE... .... ..... ..... ..... ..... . . . . .. .. ...... ............ ........ ............. ............. ............. ............. ............. ............. ............. ............. ... .................. ............ ZANK ..... ..... ..... ..... ..... ..... ..... ... ................... . QUARK . ..... .... ..... .... . . . . .... ..... ........... ........... HUT Abbildung 9.11: Beispiel für RBINSERT – Teil I (nach [CormLR1990]) DER und LAGE sind beide rot. Jetzt kommt man mit einfachem Umfärben nicht mehr weiter. Wird einer der Knoten DER oder LAGE schwarz, so ist auf jeden Fall Bedingung 3 der Definition 9.2 verletzt. Um weiterzukommen, wollen wir Rotationen anwenden und versuchen, damit die beiden Unterbäume der Wurzel auszugleichen. Dazu muß SEHEN in den rechten Unterbaum wandern und ein Knoten aus dem linken Unterbaum Wurzel werden. Das läßt sich mit einer Rechtsrotation um SEHEN erreichen. Würde man diese direkt anwenden, so wird, wie leicht zu sehen, das Ungleichgewicht auf die andere Seite der (neuen) Wurzel verlagert. Deswegen wird voher auf DER eine Linksrotation angwendet. 284 KAPITEL 9. SUCHBÄUME Fall 2 SEHEN ....... ... ........... ........... ........... ........... ........... . . . . . . . . . . ........ ........... . .... ............ .................... LAGE... ...... ....... ....... ....... . . . . . ... ....... .. ....... ........ .............. ... ... .. ... ....... .......... ...... ............. ............. ............. ............. ............. ............. ............. ............. ... .................. ............ TANNE ... ....... ....... ....... ....... ....... ....... .......... ................... ....... ....... ....... ....... ....... ....... .......... .................... QUARK DER .... ZANK ....... ....... ....... ....... ....... ....... . .......... ............... BESUCH KNABE ... ..... ..... ..... .... . . . . ... ......... ............ ....... HUT Fall 3 LAGE....... ........... ............. ............. ............. . . . . . . . . . . . . . ............. ............. ............. ..... ............. ...................... .. ....... ...... ...... ...... . . . . . . ... ...... .. ...... .......... ............. BESUCH DER ... ...... ....... ....... ....... ....... ....... ....... .. .. . .................. KNABE ... .. ..... ..... ..... ..... . . . . .. .. ...... .............. ........ HUT ............. ............. ............. ............. ............. ............. ............. ............. ... .................. ............ SEHEN... .. ....... ...... ...... ...... . . . . . . ... ...... .. ...... .......... .............. QUARK ...... ....... ....... ....... ....... ....... ....... .. .. . ................... TANNE ... ..... ..... ..... ..... ..... ..... ..... ... ................. . ZANK Abbildung 9.12: Beispiel für RBINSERT – Teil II (nach [CormLR1990]) Das Ergebnis ist im oberen Teil der Abbildung 9.12 zu sehen. Die Kollision zweier roter Knoten existiert weiterhin und sie ist auch nicht näher an die Wurzel gerückt. Die Lage der beiden roten Knoten im Baum hat sich jedoch verändert. Nun kann die Rechtsrotation auf SEHEN angewandt werden. Sie macht LAGE zur neuen Wurzel. Um die Farbkollision aufzuheben, wird LAGE schwarz und als Folge davon SEHEN rot. Das Ergebnis ist im unteren Teil von Abbildung 9.12 zu sehen. Man kann leicht nachprüfen, daß es sich um einen korrekten Rot-Schwarz-Baum handelt. 2 Dem Beispiel folgend ist etwas mühsam, aber nicht wirklich schwer den Pseudocode für RBINSERT zu entwickeln. Er ist in den Tabellen 9.14 und 9.15 wiedergegeben. Abbil- 9.2. ROT-SCHWARZ-BÄUME ' BOOLEAN RBINSERT(VERTEX ∗ ∗ t, VERTEX ∗v) 1 { VERTEX ∗x, ∗f , ∗g, ∗o; actual node, father, grandfather, oncle 2 BOOLEAN Bo = FALSE, Bred = FALSE, B3 = FALSE; 3 4 5 6 7 8 9 10 11 12 // 13 14 15 16 17 18 if (!INSERT(∗t, v)) return FALSE; //Schlüsselwert schon vorhanden x = v; x→color = RED; while (x→par) != NULL) { f = x→par; // Vater von x if (f→color == BLACK) break; // Fertig, wenn Vater schwarz g = f→par; // Grossvater von x; muss schwarz sein o = otherson(g, f ) // Onkel von x if (o != NULL) Bo = TRUE; // Onkel existiert if (Bo) Bred = (o→color == RED); Onkel existiert und ist rot Fall 1: Onkel existiert und ist rot – Umfaerben Vater, Grossvater und Onkel if (Bred) { f→color = BLACK; o→color = BLACK; g→color = RED; x = g; // Grossvater wird akltueller Knoten } & 285 $ % Tabelle 9.14: Prozedur RBINSERT – Teil I dung 9.13 zeigt den Rot-Schwarz-Baum, der sich ergibt, wenn die Knoten SEHEN, DER, RUHEN, ANTON, ZAHN, TANNE, BESUCH, HUT, KNABE, LAGE, ROT, QUARK, PFAU, NOCH, NEIN, LUST, ZANK, HAT in dieser Reihenfolge mit RBINSERT eingefügt werden. Zum Aufwand für RBINSERT: Das naive Einfügen eines Satzes kostet Θ(ln(n)) Schritte. Der Aufwand für das anschließende Umformen, um wieder einen Rot-Schwarz-Baum zu erhalten, wird durch die while-Schleife (Tabellen 9.14 und 9.15) bestimmt. In der Schleife werden bei jedem Durchlauf die beiden aufeinanderfolgende roten Knoten näher an die Wurzel heran geschoben. D. h. auch das Umformen, und damit die gesamte Operation kosten Θ(ln(n)) Schritte. RBDELETE Auch das Löschen eines Satzes in einem Rot-Schwarz-Baum läßt sich mit einem Zeitaufwand Θ(ln(n)) durchführen, und zwar so, daß hinterher wieder ein Rot-Schwarz-Baum entsteht. Um das zu sehen, betrachten wir die Fälle: Der zu löschende Knoten hat zwei 286 HUT .................................... ............. ................. ............. ................ ............. ................ . . . . . . . . . . . . . . ............. . ... ............. ................ ............. ................. . . . . . . . . . . . ............. . . . . .......... . . ............. ... . . . . . . . . . . . . . . .................. ....................... . . . . . . . . . ...... ............ QUARK . BESUCH ...... .............. .... .......... .......... .... .......... .... .......... . . . . . .... . . . . ....... . .... . . . . . . . . .... .. ....... . . . . . . . . . ............... ................. . . . . . . . ... ............. ... .. ....... ...... ....... .... ........ .... ....... . . . .... . . . .. .... ....... .... .. ....... . . . . . . . ......... ................. . . . . . ... .... ...... ANTON DER .. ... ... ... ... ... ... .............. ...... HAT LAGE RUHEN ..... ... ....... ... ....... .... ........ ... ....... . ... . . . . . ... ........ .. ....... . . . . . . . ................ .............. . . ..... . ................ KNABE NOCH .... .. ............ .... ....... ....... .... ....... ... . . ........ . ....... .... ....... ... . . ....... .. . . ......... ................ . ............... ... ROT ..... ... ...... ....... ...... .... ...... .... ...... . . .... . . . . .... . ...... .. . ............ . . ................. .... .. .............. NEIN .. PFAU .. ... ... ... . . .. .... .. . ......... .... Abbildung 9.13: Rot-Schwarz-Baum rbbaum ..... ... ...... ...... ...... ... .... ...... ...... .... . . . . . .... ... ...... ....... .............. . ............... . .. .............. SEHEN ZAHN .. .... .... .... .... .... .... .... .. ................. ... ZANK KAPITEL 9. SUCHBÄUME LUST TANNE .... 9.2. ROT-SCHWARZ-BÄUME ' // Fall 2: Onkel ist schwarz oder existiert nicht – Vater ist linker Nachfolger 19 if (!Bred && isleft(f )) 20 { if (isright(x)) {leftrotate(t, x); } else { x = f ; } 21 rightrotate(t, x); 22 x→color = BLACK; 23 g→color = RED; 24 break; 25 } // Fall 3: Onkel ist schwarz oder existiert nicht – Vater ist rechter Nachfolger 26 if (!Bred && isright(f )) 27 { if (isleft(x)) {rightrotate(t, x); } else { x = f ; } 28 lefttrotate(t, x); 29 x→color = BLACK; 30 g→color = RED; 31 break; 32 } 33 } 34 }; 35 ∗t→color = BLACK; // Wurzel ist immer schwarz 36 return TRUE; 37} & 287 $ % Tabelle 9.15: Prozedur RBINSERT – Teil II Nachfolger und der zu löschende Knoten hat höchstens einen Nachfolger. Die Prozeduren, mit denen man das Löschen durchführen kann, sind in den Tabellen 9.16 (Seite 288), 9.17 (Seite 290), 9.18 (Seite 291) und 9.19 (Seite 292) angegeben. Anhand dieser Prozeduren wird erläutert, wie man vorgeht. Der zu löschende Knoten hat zwei Nachfolger. Die Hauptrozedur RBDELETE (Tabelle 9.16) behandelt diesen Fall. In dieser Prozedur wird zunächst geprüft, ob es den zu löschenden Satz überhaupt gibt (Zeilen 5 und 6), und dann, ob ein Satz mit höchstens einem Nachfolger gelöscht werden soll (Zeilen 7 und 8). In diesem Fall gibt es im Baum einen nächstgrößeren Schlüsselwert. Wenn man einfach im zu löschenden Satz den Schlüsselwert mit dem nächstgrößeren überschriebe, bliebe der Rot-Schwarz-Baum korrekt, allerdings müßte der Satz mit diesem Schlüssel gelöscht werden. Mit dieser Idee kommt man weiter; es sind jedoch einige zusätzliche Überlegungen notwendig. Zunächst einmal wollen wir einen Satz aus dem Baum nur ausketten und nicht 288 ' KAPITEL 9. SUCHBÄUME BOOLEAN RBDELETE(VERTEX ∗ ∗ t, int key) 1 { VERTEX *v; // v wird gelöscht 2 VERTEX *w; // w ersetzt v 3 VERTEX *w1; // w1 Platzhalter für w 4 BOOLEAN bsucc; // w1 ist Nachfolger von v 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 } $ v = SEARCH(∗t, key); if ( v == NULL) return FALSE; // Schlüsselwert nicht im Baum // v hat höchstens einen Nachfolger if (v→right == NULL || v→lef t == NULL) {onedel(tree, v); return TRUE; } // v hat zwei Nachfolger w = min(v→right); bsucc = (w→par == v); // w ist rechter Nachfolger von v /* Platzhalter für w konstruieren */ w1 = allocate(); w1→color = w→color; w1→lef t = w→lef t; w1→right = w→right; if (bsucc) {w1→par = w; w→right = w1; } else {w1→par = w→par; w→par→lef t = w1; } /* v durch w ersetzen */ w→lef t = v→lef t; w→lef t→par = w; if (!bsucc) {w→right = v→right; w→right→par = w; } w→color = v→color; w→par = v→par; if (v->par != NULL) { if (v→par→lef t == v) v→par→lef t = w; else v→par→right = w; } else { *t = w; } onedel(t, w1); // Platzhalter entfernen deallocate(w1); // return TRUE; & % Tabelle 9.16: Prozedur RBDELETE – Teil I wirklich löschen. Außerdem dürfen Schlüsselwerte in Sätzen nicht überschrieben werden, denn sie sind ein Identifikationsmerkmal. Das führt zu der Lösung, die im Rest der Prozedur zu sehen ist. In Zeile 9 wird der Satz w mit dem nächsthöheren Schlüsselwert bestimmt 9.2. ROT-SCHWARZ-BÄUME 289 und in Zeile 10 festgehalten, ob dieser unmittelbarer Nachfolger des zu löschenden Satzes ist oder nicht. In den Zeilen 11 bis 14 wird ein Platzhalter für w konstruiert und an der entsprechenden Stelle in den Baum eingefügt. Die Anweisungen in den Zeilen 15 bis 20 ketten den zu löschenden Satz aus dem Baum aus und ersetzen ihn durch w. Schließlich wird in Zeilen 21 und 22 der Platzhalter gelöscht – er hat höchtens einen Nachfolger! – und sein Speicherplatz freigegeben. Der zu löschende Knoten hat höchsten einen Nachfolger. Es gibt dann die folgenden Möglichkeiten. 1. Der Knoten ist rot und hat keine Nachfolger. 2. Der Knoten ist schwarz und hat keine Nachfolger. 3. Der Knoten ist schwarz und hat einen linken roten Nachfolger. 4. Der Knoten ist schwarz und hat einen rechten roten Nachfolger. Dabei kann Fall 3 nicht auftreten, wenn der zu löschenden Satz der Platzhalter aus RBDELETE ist. Die Bearbeitung der vier Fälle beginnt mit der Prozedur ONEDEL, Tabelle 9.17 (Seite 290). Zeilen 3 bis 5 bestimmen den Nachfolger, der auch NULL sein kann. Zeile 6 findet den Vorgänger. Ist der zu löschende Knoten die Wurzel des Baumes, so wird er gelöscht und, falls ein Nachfolger vorhanden ist, wird dieser die neue Wurzel (Zeilen 7 bis 10). Ist er nicht die Wurzel und hat er einen von NULL verschiedenen Nachfolger, so wird er durch diesen ersetzt, wobei der Nachfolger schwarz gefärbt wird (Zeilen 16 bis 17) und sich somit ein korrekter Rot-Schwarz-Baum ergibt. Es bleibt der Fall, daß der zu löschende Knoten keine Nachfolger hat und nicht Wurzel ist. Dieser Fall wird in der Prozedur RBDELREBALANCE (Tabellen 9.18, Seite 291 und 9.19, Seite 292) behandelt. Diese Prozedur ruft sich rekursiv auf (Zeilen 11 und 19). Um zu verstehen, wieso die Löschung auf einen korrekten Rot-Schwarz-Baum führt, wollen wir den Anfangsaufruf und eventuelle Folgeaufrufe der Prozedur getrennt ansehen. Beim ersten Aufruf ist x der zu löschende Satz, nicht die Wurzel und ohne Nachfolger. Ist x rot, so wird mit der Anweisung 3 eine (in diesem Fall überflüssige) Umfärbung vorgenommen und die Prozedur verlassen. Der verbleibende Rot-Schwarz-Baum ist korrekt. Ist x schwarz, so muß es einen Bruder s geben. Vom mitgelieferten Parameterwert blef t hängt es ab, ob es der linke oder rechte Bruder ist. Die weitere Bearbeitung hängt von diesem Bruder ab. Fall A: Der Bruder ist rot. Der Vater, der schwarz war, wird rot gefärbt, der Bruder schwarz (Zeile 9). Danach wird durch eine entsprechende Rotation (Zeile 10) der Bruder zum Großvater. Da x schwarz ist, der Bruder rot war, mußte der Bruder vor der Rotation zwei scharze Söhne gehabt haben. Einer davon wird nach der Rotation neuer,jetzt schwarzer Bruder von x, der andere bleibt Sohn von s. Mit einigen Überlegungen erkennt 290 ' & KAPITEL 9. SUCHBÄUME void ONEDEL(VERTEX ∗ ∗ t, VERTEX ∗v) 1 { VERTEX ∗w, ∗p, ∗g, ∗o; // successor, predeccessor 2 BOOLEAN blef t; 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 $ if (v→right != NULL) w = v→right; // Nachfolger bestimmen if (v→lef t != NULL) w = v→lef t; if (v→right == NULL && v→lef t == NULL) w = NULL; p = v→par; // Vorgänger bestimmen if (p == NULL) // Wurzel löschen { *t = w; if (w != NULL) { w→color = BLACK; w→par = NULL;}; return; } else // Nichtwurzel löschen { if (v == p→lef t) { blef t = TRUE; p→lef t = w; } else { blef t = FALSE; p→right = w; } } if (w != NULL) { w→par = p; w→color = BLACK; return; } rbdelrebalance(t, v, blef t); return; } % Tabelle 9.17: Prozedur ONEDEL man, daß auch der neu gebildete Baum ein korrekter Rot-Scharz-Baum ist, wenn man x hinzurechnet. Mit diesem umgeformten Baum wird RBDELREBALANCE rekursiv aufgerufen (Zeile 11). Fall B.1: Der Bruder ist schwarz. Beide Neffen sind schwarz oder NULL. Fall B.2: Der Bruder ist schwarz. Es gib einen roten Neffen. 9.2. ROT-SCHWARZ-BÄUME 291 ' void RBDELREBALANCE(VERTEX ∗ ∗ t, VERTEX ∗x, BOOLEAN blef t) 1 { VERTEX ∗s, ∗p; // Bruder, Vater 2 BOOLEAN b, bblack1, bblack2, blef tred, brightred; 3 4 5 // 6 7 // 8 9 10 11 12 // 13 14 15 16 17 // 18 19 if (x→color == RED) { x→color = BLACK; return; } if (x→par == NULL) return; // Wurzel erreicht p = x→par; x→color ist schwarz; nicht Wurzel if (blef t) s = p→right; // rechter Bruder von v; stets vorhanden if (!bleft) s = p→lef t; // linker Bruder von v; stets vorhanden Fall A: Bruder ist rot if (s→color == RED) { p→color = RED; s→color = BLACK; if (blef t) leftrotate(t, s); else rightrotate (tree, s); rbdelrebalance(t, x, blef t); return; } Fall B: Bruder ist schwarz else { bblack1 = FALSE; bblack2 = FALSE; bblack1 = (s→right == NULL) && (s→lef t == NULL); if (s→right != NULL && s→lef t != NULL) bblack2 = (s→right→color == BLACK && s→lef t→color == BLACK); Fall B.1: Beide Neffen sind schwarz oder NULL if (bblack1 || bblack2) { s→color = RED; rbdelrebalance(t, p, (p→par→lef t == p)); return; } & Tabelle 9.18: Prozedur RBDELREBALANCE – Teil I $ % 292 KAPITEL 9. SUCHBÄUME ' $ & % // Fall B.2: Mindestens ein Neffe ist rot 20 else 21 { blef tred = FALSE; brightred = FALSE; 22 if (blef t) // x ist linker Nachfolger von p 23 { blef tred = (s→right == NULL); 24 if (!blef tred) blef tred = (s→right→color == BLACK); 25 if (blef tred) { s→lef t→color = BLACK; s→color = RED; 26 rightrotate(t, si→lef t); s = p→right; } 27 s->color = x->par->color; x->par->color = BLACK; 28 s→right→color = BLACK; leftrotate(t, s); 29 } 30 else // x ist rechter Nachfolger von p 31 { brightred = (s→lef t == NULL); 32 if (!brightred) brightred = (s→lef t→color == BLACK); 33 if (brightred) { s→right→color = BLACK; s→color = RED; 34 leftrotate(t, s→right); s = p→lef t; } 35 s→color=→color; x→par→color = BLACK; 36 s→lef t→color = BLACK; rightrotate(t, s); 37 } 38 } 39 } 40 return; 41 } Tabelle 9.19: Prozedur RBDELREBALANCE – Teil II 9.3. ZUFÄLLIGE BINÄRE SUCHBÄUME 9.3 293 Zufällige binäre Suchbäume Wenn ein binärer Suchbaum mit naivem Einfügen aufgbaut wird, kann er buschig, dürr oder irgend etwas dazwischen werden. Die Form des Baumes ergibt sich aus der Reihenfolge, in der die zu speichernden Schlüsselwerte eintreffen. Wenn man, was meistens der Fall ist, diese Reihenfolge nicht kennt, kann man annehmen, daß die Reihenfolge der Schlüsselwerte „zufällig“ ist. Das bedeutet, daß alle Reihenfolgen mit der gleichen Wahrscheinlichkeit P (p) = n!1 auftreten. Einige Suchbaumstrukturen werden durch genau eine Reihenfolge erzeugt, z. B. der Baum mit ausschließlich Rechtsnachfolgern durch die Reihenfolge der aufsteigend sortierten Schlüsselwerte. Für andere Strukturen gibt es mehrere Reihenfolgen, die sie erzeugen. Insgesamt ist es so, daß „günstige“ Suchbaumstrukturen mit größerer Wahrscheinlichkeit erzeugt werden als „ungünstige“. Um das zu präzisieren, wollen wir vereinfachend annehmen, daß ein binärer Suchbaum aus n nacheinander eintreffenden Sätzen einer zufälligen Ankunftsreihenfolge mit naivem Einfügen aufgebaut wird. Danach finden in ihm nur noch Operationen statt, die ihn nicht verändern. Dabei wiederum bechränken wir uns auf die Suchoperation SEARCH Für diese Suchoperationen müssen wir zusätztlich etwas darüber aussagen, mit welcher Wahrscheinlichkeit einer der vorhandenen Schlüssel gesucht wird. Mangels besseren Wissens setzen wir auch hier wieder voraus, daß alle Schlüssel mit gleicher Wahrscheinlichkeit auftreten. Dazu führen wir für einen binären Suchbaum B die die folgenden Größen ein: Mit i(v) bezeichnen wir die Länge des einfachen Weges von der Wurzel zu einem Knoten v plus 1. Das ist also die Höhe (oder Tiefe) des Knotens v im Baum. Die Summe der Höhen aller Knoten Baumes ergibt die interne Pfadlänge (internal path lerngth) X I(B) := i(v) v∈B Man müßte eigentlich von der kummulierten internen Pfadlänge sprechen. Die mittlere Suchpfadlänge (average search path length) wird dann sinnvollerweise definiert durch I(B) ¯ I(B) := n Dabei ist n die Anzahl der Knoten des Baumes. Für den leeren Baum und den Baum, der nur aus einer Wurzel besteht. gilt offenbar: I(0) = 0 und I(1) = 1 (9.2) Die mittlere Suchpfadlänge gibt an, wie viele Vergleiche im Mittel bei einer Suche auszuführen sind. Die interne Pfadlänge und die mittlere Suchpfadlänge beziehen sich auf einen gegebenen Baum B. Dieser aber ergibt sich aus einer zufälligen Ankunftsreihen¯ folge in der Aufbauphase. I(B) und I(B) sind ist also Zufallsvariable. Wir wollen ihre Erwartungswerte berechnen und beginnen mit dem Erwartungswert EI(n) der internen 294 KAPITEL 9. SUCHBÄUME Pfadlänge. Dieser Wert soll in Abhängigkeit von n bestimmt werden. Dazu müssten wir für jeden Baum die interne Pfadlänge kennen und wissen, mit welcher Wahrscheinlichlkeit der Baum auftritt. Das ist schwierig. Weiter kommen wir mit einer Rekursionsbetrachtung. Ausgangspunkt ist die Feststellung, daß sich die interne Pfadlänge I(B) eines Baumes B aus den internen Pfadlängen des linken Unterbaums Bl und des rechten Unterbaums Br der Wurzel folgendermaßen ergibt I(B) = I(Bl ) + I(Br ) + Anzahl Knoten von B Die Anzahl k der Knoten des linken Unterbaumes variiert zwischen 0 und n−1. Die Anzahl Knoten des rechten Unterbaumes ist (n − 1) − k Damit ergibt sich für den Erwartungwert EI(n) = E(I) = E(I(Bl )) + E(I(Br )) + n n X P(a(Bl ) = k − 1)EI(k − 1) + P(a(Br ) = n − k)EI(n − k) + n = k=1 n X P(a(Bl ) = k − 1)(EI(k − 1) + EI(n − k) = n+ k=1 Dabei ist a(B) die Anzahl Knoten des Baumes B und P(a(B) = m) die Wahrscheinlichkeit, daß diese Anzahl gleich m ist. Die Wahrscheinlichkeiten findet man mit folgender Überlegung: Die Füllung des Baumes Bl hängt bei einem binären Suchbaum B nur vom Schlüsselwert der Wurzel von B ab. Nach unseren Voraussetzungen sind alle Schlüsselwerte gleich wahrscheinlich. Daher sind auch alle Werte von a(B) gleich wahrscheinlich. Wir haben also n EI(n) = n + n−1 2X 1X EI(k − 1) + E(n − k) = n + EI(k) n k=1 n k=0 Indem wir etwas umformen und zusätzlich n durch n + 1 ersetzen, erhalten wir 2 (n + 1)EI(n + 1) = (n + 1) + 2 · (n)EI(n) = (n)2 + 2 · n−1 X n X EI(k) k=0 EI(k) k=0 Wir ziehen die zweite Gleichung von der ersten ab und erhalten nach weiterem Umformen EI(n + 1) = 2n + 1 n + 2 + EI(n) n+1 n+1 (9.3) Die Rekursionsgleichung 9.3 läßt sich geschlossen lösen, und zwar läßt sich (mit etwas Rechnerei) durch vollständige Induktion zeigen EI(n) = 2(n + 1)Hn − 3n (9.4) 9.3. ZUFÄLLIGE BINÄRE SUCHBÄUME 295 Hn ist dabei der n-te Abschnitt der harmonischen Reihe Hn := 1 + 1 1 1 + +···+ 2 3 n Man spricht auch von der n-ten harmonischen Zahl (harmonic number). Für harmonische Zahlen gilt Hn = ln(n) + γ + 1 1 1 + + − 2 2n 12n 120n4 mit 0<< 1 252n6 γ ist die Eulerkonstante γ = 0.57721 . . . . Es gilt also 1 EI(n) = 2n ln n − (3 − 2γ)n + 2 ln n + 1 + 2γ + O( ) n und für den gesuchten Erwartungswert der mittleren Suchpfadlänge EI(n) := EI(n) 2 ln n = 2 ln n − (3 − 2γ) + +··· n n (9.5) (9.6) Um zu sehen, was das bedeutet, vergleichen wir diesen Erwartungswert mit der mittleren Suchpfadlänge in einem optimalen Baum. Der Einfachheit halber setzen wir zunächst n = 2k voraus. Der optimale Baum ist dann ein vollständiger ausgewogener Binärbaum. In ihm liegen alle Blätter auf der Stufe h = hmin = ld (n+1) und seine mittlere Suchpfadlänge ist gegeben durch h−1 1 1X (i + 1) · 2i = h [(h − 1) · 2h + 1] I¯min (n) = n i=0 2 −1 Dabei ergibt sich der rechte Teil der Gleichung aus n = 2h − 1 und Anmerkung 6.1, Seite 192. D. h. wir haben I¯min (n) = 2h 1 ld (n + 1) [(h − 1)(2h − 1) + h] = ld (n + 1) + −1 −1 n Daraus ergibt sich EI(n) ≈ 1.39 I¯min (n) (9.7) Wählt man einen zufälligen Aufbau des binären Suchbaumes statt eines ausgewogenen Aufbaus, so wird man im Mittel einen nur 40% schlechteren Baum erhalten. Das bleibt auch so, wenn nur naive Einfügungen und rein lesende Operationen im Wechsel ausgeführt werden. Das bleibt i. A. nicht so, wenn zwischendurch auch gelöscht wird. Siehe auch die Literaturangaben. 296 9.4 9.4.1 KAPITEL 9. SUCHBÄUME B-Bäume und externe Datenspeicherung Mehrweg-Suchbäume Bei einem Binärbäumen ist es nicht nur wichtig, daß ein Knoten maximal zwei Nachfolger haben kann, sondern es ist auch ganz entscheidend, daß zwischen rechtem und linkem Nachfolger unterschieden wird. Es gibt eine Ordnung auf der Menge der Nachfolger. Es ist naheliegend, zu fragen, ob eine Ordnung auch nützlich ist, wenn ein Knoten mehr als zwei Nachfolger haben darf. Ja, das ist der Fall und für manche Suchaufgaben eignen sich solche Bäume in besonderem Maße. Wir wollen daher Mehrweg-Suchbäume (multiway search tree) einführen. Abbildung 9.14 zeigt einen solchen. Er hat 7 Knoten: Einen Wurzelknoten auf Stufe 0, fünf Knoten auf Stufe 1 und einen Knoten auf Stufe 2. Jeder Knoten ist mit mindestens einem und höchstens vier Schlüsselwerten gefüllt. Diese sind im Knoten in aufsteigender Reihenfolge gespeichert. Vor dem ersten Schlüsseleintrag eines Knotens, zwischen zwei aufeinanderfolgenden Schlüsseleinträgen und nach dem letzten Schlüsseleintrag gibt es ein Verweisfeld. Dieses enthält einen Verweis auf einen Knoten der nächsten Stufe oder einen NIL-Verweis. NIL-Verweise sind schwarz. Allgemein ist ein Mehrweg-Suchbaum zunächst einmal ein Baum, d. h. es gibt eine eindeutig bestimmte Wurzel ohne Vorgänger, jeder andere Knoten hat genau einen Vorgänger und es gibt einen eindeutig bestimmten Weg von der Wurzel zu jedem anderen Knoten. Vergleiche Unterabschnitt 9.1.2, Seite 258. Jeder Knoten enthält abwechselnd Verweisfelder und Schlüsselwerte. Die Schlüsselwerte sind in einem Knoten in aufsteigender Ordnung gespeichert. Knoten, bei denen alle Verweisfelder NIL aufweisen, heißen Blätter (leaf ). Charakteristisch für Mehrwegsuchbäume ist die folgende Regel: Alle Knoten eines Unterbaumes, auf den ein Verweiseintrag eines Knotens zeigt, haben Schlüsseleinträge, die größer sind als der Schlüsselwert vor dem Verweis und kleiner als der Schlüsselwert hinter dem Verweis. Für den ersten und den letzten Verweis in einem Knoten gilt eine entsprechende einseitige Festlegung. Ein Suche nach einem Schlüsselwert beginnt im Wurzelknoten und durchsucht einen jeden besuchten Knoten in aufsteigender Schlüsselwertreihenfolge. • Wird der Schlüsselwert gefunden, ist man fertig. • Anderenfalls gibt es entweder einen ersten Schlüsselwert, der größer als der Suchwert ist, und man folgt dem Verweis vor dem diesem Schlüssewlwert • oder es gibt im Knoten keinen Schlüsselwert, der größer als der Suchwert ist, und man folgt dem letzten Verweis im Knoten. • Ist ein zu folgender Verweis NIL, so gibt es im Baum den gesuchten Schüsselwert nicht. • ANTON • BESUCH ◦ . • ... ... ... ... ... ... ... ... ... ... ... ... .. ....... .. ....... ... DER • HUT • HAT • LAGE • ... ... ... ... ... ... ... ... ... ... ... ... .. ....... .. ......... .. LUST NOCH ◦ QUARK ◦ TANNE ◦ ... . .. ... ... ... ... ... ........ ......... ... • PFAU • • NEIN • ............. ...... ............. ....... ............ ...... ............. ....... ............. ....... ............. ...... ............. ....... ............. ....... ............. ...... .................... ....... ....... ................ ...... ....... ....... ....... ...... ....... ....... ......... ................... • • ROT • • RUHE ZAHN • ZANK • SEHEN • • 9.4. B-BÄUME UND EXTERNE DATENSPEICHERUNG ◦ KNABE ◦ . . ........... .......... .......... ........... .......... . . . . . . . . . ........... .......... ........... . .... ........... .................... Abbildung 9.14: Mehrweg-Suchbaum 297 298 KAPITEL 9. SUCHBÄUME Einige Beispiele in Abbildung 9.14: QUARK: Wird im Wurzelknoten gefunden. HAT: Wird im Knoten 2. Stufe gefunden. ZOFF: Es wird im letzten Knoten erster Stufe festgestellt, daß es den Schlüsseleintrag nicht gibt. ERNST: Es wird im Knoten 2. Stufe festgestellt, daß es den Schlüsseleintrag nicht gibt. Wozu braucht man Mehrweg-Suchbäume? Sind sie besser als binäre Suchbäume? Für Datenstrukturen, die nur innerhalb eines Programmlaufs existieren und deswegen im Hauptspeicher liegen, ist das in der Regel nicht der Fall. Für Datenstrukturen in Dateien und Datenbanken hingegen bieten Mehrweg-Suchbäume deutliche Vorteile. 9.4.2 Speicherung bei Dateien und Datenbanken Wir wollen uns daher Datenstrukturen, die speziell für die längerfristige Speicherung von Daten gedacht sind, einmal genauer ansehen. In Dateien und mehr noch in Datenbanken (siehe Abschnitt 2.2, Seite 33) sind Sätze die Transport- und Speichereinheiten. Gesucht und bereitgestellt werden sie entweder sequentiell oder direkt über einen Schlüsselwert. Sätze des gleichen Typs haben oft die gleiche Länge. Die Längen unterschiedlicher Satztypen differieren. Es gibt aber auch Satztypen mit variabler Satzlänge. Der Aufbau aus Sätzen unterschiedlicher Typen wird logische Schicht (logical level) genannt. Sätze werden im Allgemeinen auf Geräten und Dateträgern mit direktem Zugriff gespeichert. In der Mehrzahl der Fälle sind das Magnetplatten. Frühere Magnetplattengeräte hatten auswechselbare Träger. Die gespeicherten Blöcke hatten variable Längen und wurden über Zylinder- und Spurnummern addressiert. Neuere Geräte sind Festsplatten und sind in Sektoren gleicher Länge eingeteilt. Es hat sich als vorteilhaft erwiesen und ist heute üblich, über die verschiedenen Speichergeräte eine einheitliche physikalische Schicht (physical level) zu legen. Diese besteht aus Blöcken gleicher Länge, die zu größeren Bereichen, Partitionen (partition), zusammengfaßt sind. Innerhalb einer Partition sind die Blöcke durchnumeriert. Es ist nun Aufgabe der Dateiverwaltung bzw. des Datenbanksystems, die Abbildung zwischen den beiden Schichten vorzunehmen. Dabei sind Mehrweg-Suchbäume von großem Nutzen. Wir wollen dafür den folgenden Fall genauer untersuchen. Es sei ein Datenbestand mit Sätzen einer festen Klasse gegeben. Ihre Anzahl sei so groß, daß es nicht möglich oder zumindest nicht zweckmäßig ist, alle Schlüsselwerte im Hauptspeicher zu halten. Für die Schlüsselwerte wird ein Mehrweg-Suchbaum angelegt. Dabei ist ein Knoten ein Block der physdikalischen Schicht und wir gehen davon aus, daß ein Zugriff zu einem Knoten einem 9.4. B-BÄUME UND EXTERNE DATENSPEICHERUNG 299 Blocktransport vom Plattenspeicher entspricht. Dagegen finden Vergleiche von Schlüsselwerten mittels Maschinenbefehlen im Hauptspeicher statt und sind größenordnungsmäßig um den Faktor 1000 schneller. Es ist also sehr wichtig, einen Schlüsselwert mit wenigen Blocktransporten, d. h. Knotenzugriffen zu finden. Die Suchzeit innerhalb eines Knotens fällt nicht so stark ins Gewicht. Oft verwendet man dafür sequentielle Suche. Eine geringe Zahl von Knotenzugriffen erreicht man durch einen ausgewogenen Mehrweg-Suchbaum und durch eine große Zahl von Schlüsselwerten in jedem Knoten. Diese Anforderungen lasssen sich mit B-Bäumen und ihren Varianten erfüllen. 9.4.3 B-Bäume B-Bäume wurden von Bayer und McCreight eingeführt [BayeMCr1972]. Die Autoren gaben keine Erklärung für die Namenswahl. Die Grundidee ist, eine maximal mögliche Füllung eines Knotens vorzugeben und zu verlangen, daß alle Knoten außer der Wurzel mindestens halb voll sind. Außerdem müssen alle Blätter auf der gleichen Stufe liegen. Der Mehrweg-Suchbaum von Abbildung 9.14 ist demnach kein B-Baum. Das folgende Beispiel soll zeigen, wie ein B-Baum entstehen kann. Beispiel 9.5 Es soll ein B-Baum mit den Schlüsselwerten QUARK, PFAU, BESUCH, ANTON, LAGE, HUT, HAT, DER, KNABE, LUST, NOCH, NEIN, RUHE, ROT, TANNE, SEHEN, ZAHN, ZANK in dieser Reihenfolge aufgebaut werden. Ein Knoten soll maximal 4 und minimal 2 Knoten enthalten. Wir legen also einen leeren Wurzelknoten an und füllen ihn mit den ersten vier Schlüsselwerten, Abbildung 9.15, Teil A. Beim fünften Knoten, LAGE, läuft die Wurzel über und muß geteilt werden. Das wird so gemacht, daß zwei neue Blöcke mit je zwei Schlüsselwerten gebildet werden. Der mittlere Schlüsselwert wird einziger Eintrag einer neu zu bildenden Wurzel, Abbildung 9.15, Teil B. Danach können können alle Schlüsselwerte bis einschließlich ZAHN in Blöcke der ersten Stufe eingefügt werden. dabei kommt es zu drei weiteren Teilungen und dementsprechend zu drei weiteren Einträgen in der Wurzel, Abbildung 9.15, Teil C. Beim Einfügen von ZANK kommt es zu einer weiteren Blockteilung auf Stufe 1. Der Schlüsselwert TANNE müßte in die Wurzel kommen. Dabei läuft diese erneut über. Sie muß geteilt und ein neuer Wurzelknoten eingerichtet werden. Der Schlüsselwert NOCH wird in diesen verschoben, Abbildung 9.16, Teil D. 2 Definition 9.3 Ein Mehrweg-Suchbaum heist B-Baum (B-tree) der Ordnung m, wenn er die folgenden Eigenschaften hat: 1. Jeder Knoten enthält höchstens m − 1 Schlüsselwerte. 2. Jeder Knoten außer der Wurzel enthält mindestens b m−1 c Schlüsselwerte. 2 3. Der Baum ist entweder leer oder die Wurzel enthält mindestens einen Schlüsselwert. 4. Alle NIL-Verweise gehören zu Knoten der gleichen Stufe. 300 • ANTON • BESUCH • PFAU • QUARK • Teil A ◦ LAGE ... ........ ......... ......... ......... . . . . . . . ...... ......... ......... . .......... ............ .................. ◦ ......... ......... ......... ......... ......... ......... ......... ......... ......... . ............. ................ PFAU QUARK ANTON BESUCH Teil B ◦ ANTON BESUCH DER ◦ ...... ..... ...... ...... ..... . . . . .. ...... .. ...... ......... ............. HAT HUT KNABE LAGE ◦ ... ... ... ... ... .......... ......... .. LUST NEIN NOCH ◦ ROT ...... ...... ...... ...... ...... ...... ...... ...... .. .. . .................... PFAU QUARK Teil C Abbildung 9.15: Aufbau eines B-Baumes ◦ ............ ............ ............ ........... ............ ............ ............ ........... ............ ................. .. . ................ RUHE SEHEN TANNE ZAHN KAPITEL 9. SUCHBÄUME ............ ............ ............ ........... ............ . . . . . . . . . . . . ............ ........... . ............ .... ........... .................... ◦ ... ... ... ... ... .......... ........ ... ANTON BESUCH NOCH DER ◦ ... ... ... ... ... .......... ........ ... HAT HUT KNABE LAGE ◦ ....... ....... ....... ....... ....... ....... ....... ....... ....... ....... ....... ....... ....... ....... . .......... ................. ◦ ◦ ... ... ... ... ... .......... ......... .. ... ... ... ... ... .......... ........ ... PFAU QUARK LUST NEIN ROT ◦ TANNE ... ... ... ... ... .......... ......... .. RUHE SEHEN Teil D ◦ ... ... ... ... ... .......... ......... .. ZAHN ZANK 9.4. B-BÄUME UND EXTERNE DATENSPEICHERUNG ◦ ....... ...... ....... ....... ....... . . . . . .. ....... ...... ....... ....... ...... . . . . . . ....... .. ....... .......... .............. Abbildung 9.16: Aufbau eines B-Baumes (Fortsetzung) 301 302 KAPITEL 9. SUCHBÄUME Die folgende Proposition ist unmittelbar als richtig zu erkennen. Proposition 9.3 Punkt 4. von Definition 9.3 ist gleichwertig zu den beiden Aussagen 4a. Alle Blatter liegen auf der gleichen Stufe. 4b. Ein Knoten mit t − 1 Schlüsselwerten ist entweder ein Blatt oder hat t Nachfolger. Die Definition von B-Bäumen kann somit auch etwas anders gefaßt werden und ist in der entsprechenden Version in vielen Büchern formuliert. In einem B-Baum bestimmt jeder Weg von der Wurzel zu irgendeinem Blatt durch seine Länge + 1 die Höhe des Baumes. Es läßt sich leicht zeigen, daß B-Bäume ausgewogen sind. Hierfür ist es zweckmäßig, die c + 1. einzuführen. t ist der Minimalgrad eines Nichtblatt-Knotens im Größe t := b m−1 2 B-Baum. Satz 9.3 Für die Höhe h(n) eines B-Baumes der Ordnung m mit n Schlüsselwerten gilt h(n) ≤ logt n+1 2 Beweis: Die Wurzel enthält mindestens einen Schlüsselwert und alle anderen Knoten mindestens t − 1. Das bedeutet, es gibt auf Stufe 1 mindestens 2 Knoten, auf Stufe 2 mindestens 2t und auf der letzten Stufe h mindestens 2th−1 Knoten. Damit läßt sich die Anzahl n der Schlüsselwerte im Baum abschätzen zu h h X t −1 τ −1 n ≥ 1 + (t − 1) 2t = 1 + 2(t − 1) = 2th − 1 t − 1 τ =1 Daraus folgt die Behauptung des Satzes. 2 Operationen auf B-Bäumen Für B-Bäume soll kein kompletter Satz von Operationen besprochen werden. Die wichtigsten Operationen sind SEARCH, INSERT und DELETE. Auf Seite 296 wurde besprochen, wie gesucht werden kann. In Beispiel 9.5 wurde erläutert, wie B-B äume aufgebaut werden können. Einen Eintrag zu löschen, ist etwas schwieriger. Es sei auf die Literurangaben verwiesen. Varianten und Verfeinerungen Zu B-Bäumen gibt es eine Reihe von Varianten. Die wichtigsten sind wohl: a. Die Knoten sind mindestens zu 23 Dritteln gefüllt. Man spricht auch von B*-Bäumen. b. In der Praxis sind oft die Sätze einer Datei/Datenbak sehr viel länger als die Schlüssel. Dann ist es zweckmäßig, einen „Index“, der nur aus einem B-Baum von Schlüsseln besteht, 9.5. DIGITALE BÄUME 303 aufzubauen und von den Schlüsseln einen direkten Verweis auf die Sätze zu benutzen. Die eigentlichen Sätze sind für einen schnelleren direkten Geamtdurchlauf in einer doppelten Kette mit aufsteigenden Schlüsselwerten gespeichert. Allerdings muß auch diese Datenstufe B-Baumeigenschaft haben, die Blöcke also eine Mindestfüllung aufeinander folgender Sätze aufweisen. In diesem Fall spricht man manchmal von B+ Bäumen. 9.5 Digitale Bäume Sie werden auch Tries 5 (von retrieval) genannt. Man geht von Schlüsselwerten aus, die Zeichenreihen (vergleiche hierzu Abschnitt 3.6, Seite 112) sind. Daher auch der Name digit (Ziffer). Wir wollen jedoch die Elemente der Zeichenreihen weiterhin Zeichen nennen. Abbildung 9.17 zeigt einen Digitalbaum für die Wörter HER bis LAGE. Im unteren Teil der Zeichnung ist die vollständige Liste der Schlüsselwerte zu sehen, im oberen Teil der zugehörige Digitalbaum, genauer ein digitaler Wald (digital forest). Jedes Blatt des Waldes zeigt über eine Linie ohne Pfeilspitzen auf den eindeutig bestimmten Schlüsselwert. Dabei gilt: a. Der Weg im Baum umfaßt nicht notwendigerweise alle Zeichen des Schlüssels. Er bricht ab, sobald der Schlüsselwert eindeutig bestimmt ist. b. Ist ein Schlüsselwert Anfangsstück eines anderen Schlüsellwertes, so wird das durch ein angehängtes Leerzeichen (t) im Baum kenntlich gemacht. Implementierumgs-und Effizienzfragen sind in Aufgabe 9.3 zu bearbeiten. Aufgaben Aufgabe 9.1 Es ist ein Verfahren anzugeben, mit dem aus n Sätzen jede vorgegebene Binärbaumstruktur als binärer Suchbaum mit n Knoten gebildet werden kann. Aufgabe 9.2 Geben Sie ein Beispiel für einen Binärbaum mit n Knoten, dessen Höhe dld (n + 1)e ist und in dem die Knoten bis zur vorletzten Stufe einschließlich keinen vollständigen ausgewogenen Binärbaum bilden. Aufgabe 9.3 Skizzieren Sie ein Verfahren mit dem Digitalbäume der Art, wie sie in Abbildung 9.17 dargestellt ist, aufgebaut werden können. Wie kann man in diesen Bäumen suchen und einfügen? Welcher Aufwand fällt für Einfügen und Suchen an ? Wird eine Ordnung der Zeichen des benutzten Alphabets gebraucht? Aufgabe 9.4 Wie kann man mit Rot-Schwarz-Bäumen Prioritätswarteschlangen (siehe Seite 254) implementieren? 5 Gesprochen wie “try“. 304 H.... Q ... ... ... ... ...... .. ......... ... E.... A ......... ............. .... ........................................ ........ ...................... ....................... ... ......... .......................... ......................... ... ................................... ........ ............................ . . . . . . . . . . . ... . ......... . .. ... .. ......... ................................................. .............. ........... ... ... .... ................................... ......... . . . . . . . . . . . . . . . . . ......... ... ................................................... .. ........ ........ .... ................. . . . . . . . . . . ................. ................... . ... . . . . . . . . . .... . . .. ................... ...................... .................... ............. .............. A ... ... ... ... ........ ......... ... B.... ... ... ... ... ........ ......... ... E R .... ........ .. ... ... .. .... ..... . . .. .. .. ... .. ... . ... .. ... . ... . ............ ........... ................. .. ... ...... .. ...... ... .... .... .... .. .. .. ...... ..... .. ....... ........ .... .. . ... t R M N R L N ........ ......... .... ... ........ ......... .. ......... . . ... . . . . . . . ......... ......... ........ . . . . . . . . ........ ............ . . . . . . . .. .... ...... . ...... ... ... ... .. ... .... . . . ... . ... ...... . ........... .... .......... ... . L G T....... B ...... ....... .. ....... ..... ....... . . ... . . . . ..... ... ....... ....... ......... ............. . . . .......... ..... . ............... t L Z.... ... ... ... ... ...... .. ........ ... R Z A .. ..... . ... ... ... ... .. .. .. .... . . .. .. . ..... .. ......... ........ .. ..... ... H N ... ... ... .... ... ... .. .... ......... .... .. ........ ............ ..... ... O R E ..... ..... .......... ..... ... ....... ..... .... .............. ..... . ... ... ....... . . . . .. ... ...... ..... ..... ........ ..... .. ......... .. .. ..... ..... ......... ............. ................. . .... ................ ... .... t S N R S . ...... ... .. .. ... ... .... . . . . ... .. .... .. ... ..... .......... .......... . ...... t S Q U A R K A A A A A A A A A A B B L L L L L L L E E B L L L L L N R E E E E E D D N N R O S T E I N A L L E S A N G S T A N T O N A N T R I E B Abbildung 9.17: Digitaler Baum A R Z T A Z U R S E H E N Z A H N Z A N K L A G E KAPITEL 9. SUCHBÄUME H H H E E E R R R R M A N N 9.5. DIGITALE BÄUME 305 Literatur Suchbäume werden in allen Büchern über Datenstrukturen behandelt z. B. [AppeL1995], [CormLR1990] [AhoU1995], [SedgF1996], [Bras2008] [KrusTL1997],[AhoU1995], [Knut1997], [OttmW1996]. Für die Berechnung der mittleren Höhe eines Binärbaumes haben wir in Abschnitt 9.3 angenommen, daß alle Reihenfolgen, aus denen jeweils ein Baum aufgebaut wird, gleichwahrscheinlich sind. Dabei kommte es vor, daß unterschiedliche Reihenfolgen, den gleichen Baum ergeben. Wenn man nicht die Reihenfolgen, sondern die Bäume als gleichwahrscheinlich ansieht, man sagt die möglichen „Gestalten“ seien gleichwahrscheinlich, ergibt sich im Mittel auch ein buschiger Baum. Siehe hierzu [OttmW1996]. In einen Binärbaum gibt es n + 1 Verweisfelder mit NULL-Verweisen (siehe Seite 261). Man kann den enstprechenden Speicherplatz nutzen und darin zusätzliche Verweise, sogenannte Fädelungszeiger(threading pointer), unterbringen, mit denen sich in bestimmten Fällen rekursive Durchläufe und Keller vermeiden lassen. Zu Einzelheiten siehe Ottmann/Widmayer [OttmW1996], Abschnitt 5.1.2, oder Oberschelp/Wille [OberW1976], Abschnitt 6.4. Zur rekursiven Definiton von Binärbäumen siehe [Knut1997] 306 KAPITEL 9. SUCHBÄUME Kapitel 10 Suchen mit Schlüsseltransformation In der Einleitung zu Kapitel 9 wurden Suchverfahren in Verfahren mit Schlüsseltransformation und Verfahren mit Schlüsselvergleich eingeteilt. Das Schema für Schlüsseltransformation ist in Abbildung 9.1, Seite 257, zu sehen. Wir wollen Sätze mit eindeutigen Schlüsselwerten in einem Datenbestand gleichartiger Sätze suchen und beschränken uns auf den Fall, daß der Datenbestand nur innerhalb eines Programmlaufs existiert. Ähnlich wie bei Suchbäumen sind Suchverfahren mit Schlüsseltransformation jedoch auch für das Suchen in permanenten Datenbeständen (Dateien und Datenbanken) wichtig. Wir benutzen die folgenden Bezeichnungen: K ist die Menge der möglichen Schlüsselwerte und k := |K|. R ist die Menge der zu einem Zeitpunkt im Datenbestand existierenden Schlüsselwerte, d. h. Sätze. Anders ausgedrückt, wir haben einen sich dynamisch verändernden Datenbestand R und r := |R| ist sein Umfang. S ist der zur Verfügung stehende Speicherplatz und s := |S|. In S werden die Sätze, also die Zielinformation des Verfahrens gespeichert. s ändert sich während eines Programmlaufes nicht. Das weitere hängt von den Größenverhältnissen zwischen diesen Mengen ab. Zunächst einmal ist k ≥ r, denn jeder existierende Satz hat einen Schlüsselwert. Weiterhin gilt k ≥ s. denn es macht keinen Sinn, mehr Speicherplätze zu haben, als es Schlüsselwerte gibt. Schließlich ist es nicht sinnvoll, in einer Menge zu suchen, die man nicht speichern kann. Es ist also s ≥ r. Hier ist jedoch Vorsicht geboten. Die Suchverfahren mit Schlüsseltransformation – zumindestens die von uns untersuchten – arbeiten mit einer Adressierungstabelle (adressing table) H vom Umfang m := |H|. Die Elemente der Tabelle H können die Sätze selber sein oder Verweise auf sie. Es sind aber auch Verweise auf Mengen von Sätzen möglich, z. B. verkettete Sätze. Im letzteren Fall kann s > m sein, und somit ist auch r > m möglich. D. h. es können mehr Sätze vorhanden sein, als die Adressierungstabelle Einträge hat. Wir wollen die Fälle k = s = m und k > m unterscheiden. Im ersten Fall spricht man von direkter Speicherung, im zweiten Fall von Hashing. 307 308 10.1 KAPITEL 10. SUCHEN MIT SCHLÜSSELTRANSFORMATION Direkte Speicherung Direkte Speicherung (direct strorage) 1 kann angewandt werden. wenn k = s, wenn also für jeden Schlüsselwert ein Speicherplatz existiert. Wir haben dann eine bijektive Adressierungsfunktion h : K 7→ S. Am günstigsten läßt sich das realisieren, wenn man S als Reihung von s Elementen anlegt und h(k) ein Indexwert dieser Reihung ist. Die Reihungselemente sind die Sätze des Datebestandes oder auch Verweise auf diese. Außerdem muß eine Kennung vorhanden sein, die angibt, ob der entsprechende Schlüsselwert im Datenbestand vorhanden ist oder nicht. 10.2 Grundlagen des Hashings Beim Hashing (hashing) 2 ist der Schlüsselwertraum K größer, häufig sehr viel größer als der Speicheraum S. Er ist in jedem Fall größer als die Hashtabelle (hashing table) H. Die Adressierungsfunkion h : K 7→ H wird Hashfunction (hashing function) genannt. Sie bildet die möglichen Schlüsselwerte auf die Einträge der Tabelle, die Hashadressen (hashing address) ab. Sie kann nicht injektiv sein. Das bedeutet aber nur, daß man zu jedem möglichen Schlüsselwert eine Hashadresse berechnen kann. Es bedeutet nicht, daß auch jeder Eintrag in der Tabelle belegt ist. In der Hashtabelle wird nämlich nur dann ein Eintrag belegt, wenn der Schlüssel eines in R existierenden Satzes dorthin führt. Es wird also mit Sicherheit einen mehrfach belegten Eintrag in H geben, wenn im Datenbestand R mehr Sätze existieren als die Tabelle H Eintragsplätze hat. Man spricht von einer Kollision (collision) Bei einer sorgfältigen Planung wird man jedoch die Tabelle H hinreichend dimensionieren und Kollisionen wegen eines zu großen Datenbestandes werden im Allgemeinen nicht auftreten. Aber selbst, wenn im Prinzip für jeden existierenden Satz ein eigener Tabellenplatz vorhanden ist, kommt es immer dann zu Kollisionen, wenn die Hashfunktion h die Schlüssel zweier Sätze aus R auf die gleiche Hashadresse abbildet. Ob das passiert, hängt nicht nur von der Hashfunktion h ab, sondern auch von der Menge R der vorhandenen Schlüssel. In den meisten Anwendungen werden die Schlüssel aus R nicht gleichverteilt in K liegen. So sind in einem Telefonverzeichnis bestimmte Namen sehr viel häufiger als andere. Die symbolischen Namen eines C-Programms weisen auch Häufungen für bestimmte Werte auf. Im folgenden Unterabschnitt wollen wir untersuchen, wie geeignete Hashfunktionen aussehen und in zwei weiteren Unterabschnitten werden wir Methoden zu Kollisionauflösung kennenlernen. Direkte Speicherung wird manchmal auch direkte gestreute Speicherung und im Englischen direct addressing oder auch direct access genannt. 2 Hashing wird manchmal auch gestreute Speicherung oder indirekte gestreute Speicherung genannt. To hash bedeutet im Deutschen kleinhacken, zerhacken. 1 10.2. GRUNDLAGEN DES HASHINGS 10.2.1 309 Hashingalgorithmen Was muß ein Hashverfahren können? Sätze müssen im Datenbestand nach ihrem Schlüsselwert gesucht werden können. Außerdem ist es notwendig, Sätze in den Datenbestand einzufügen oder aus ihm zu entfernen. Das geschieht so, daß aus dem Schlüsselwert k zunächst einmal die Hashadresse brechnet wird. Wenn an dieser Position der Tabelle kein Eintrag steht, wird kein Satz gefunden und es kann auch keiner entfernt werden. Ein neuer Satz kann ohne Schwierigkeiten eingefügt werden. Steht dort genau ein Eintrag, so hat man den gesuchten Satz gefunden und kann ihn gegebenfalls löschen. Will man einfügen, so muß man prüfen, ob der Schlüsselwert des Tabelleneintrags gleich dem des einzufügenden Satzes ist. Ist er es, kann wegen mehrfachen Schlüsselwertes der Satz nicht gespeichert werden. Ist er es nicht, so tritt für diese Tabellenpostion, also für diese Hashadresse, eine erste Kollision auf und die Kollisionsbehebung muß aufgerufen werden. In allen Fällen ohne Kollisionsbehebung ist der Aufand für die auszuführende Operation unabhängig von der Größe des Datenbestandes R und kann als konstant angesehen werden, also durch O(1) abgeschätzt werden. Anders sieht es aus, wenn an der Hashadresse h(k) eine Kollision vorliegt, wenn also zu dieser Stelle der Tabelle eine Menge von mindestens zwei Sätzen gehört. Dann muß festgestellt werden, ob der gesuchte Schlüsselwert zu einem dieser Sätze gehört oder nicht. Falls ja, hat man den gesuchten Satz gefunden und kann gegebenenfalls auch löschen. Man kann einen neuen Satz nicht einfügen. Falls nein, endet das Suchen bzw. Löschen mit Fehlermeldung, aber ein neuer Satz kann eingefügt werde. Die Kollisionsmenge wird um einen Satz größer. In diesen Fällen wird der Aufwand für eine Operation vom Umfang der Kollisionsmenge abhängen und damit auch von der Gesamtgröße des Datenbestandes, Er kann nicht mehr durch O(1) abgeschätzt werden. Wie sollte nun die Hashfunktion h beschaffen sein und wie kann man h(k) ausrechnen? Zunächst einmal wird aus jedem Schlüssel k eine natürliche Zahl gewonnen, Dabei ist jedoch einiges zu beachten. Beispiel 10.1 Wir wollen uns das am Beispiel der Schlüsselwertmenge {QUARK, PFAU, BESUCH, ANTON, LAGE, HUT, HAT, DER, KNABE, LUST, NOCH, NEIN, RUHE, ROT, TANNE, SEHEN, ZAHN, ZANK} verdeutlichen. Als natürliche Zahlenwerte wählen wir die Darstellung zu Basis q = 26 mit den Zifferwerten der Tabelle 10.1. Diese Darstellung ist naheliegend. A 0 K 10 U 20 B 1 C L 11 M V 21 W 2 D 12 N 22 X 3 E 4 F 5 G 13 O 14 P 15 Q 23 Y 24 Z 25 6 H 16 R 7 I 8 17 S 18 J 9 T 19 Tabelle 10.1: Ziffernwerte der Großbuchstaben Es ergeben sich jedoch zwei Schwierigkeiten. Zum einen ergäben die Zeichenreihen AR, 310 KAPITEL 10. SUCHEN MIT SCHLÜSSELTRANSFORMATION AAR und R wegen führender Nullen die gleiche Zahl und damit auf jeden Fall Kollisionen. Das könnte man beheben, indem man die Ziffernwerte der Tabelle um 1 erhöht und zur Basis q = 27 übergeht. Die Abbildung ist dann nicht mehr surjektiv und es ergeben sich „Löcher“ im Bildbereich. Wie weit das die Güte der Hashfunktion beeinträchtigt, ist nicht unmittelbar klar. Bedeutsamer ist wahrscheinlich der folgende Einwand. Wenn man, wie moderne Compilerversionen das tun, Namen mit bis zu 30 Zeichen (oder mehr) zuläßt und ein Grundalphabet aus Großbuchstaben, Kleinbuchstaben, Dezimalziffern und einigen Sonderzeichen hat, wird der zu berücksichtigende Bereich natürlicher Zahlen zu groß für rechnerinterne Darstellungen ganzer Zahlen in 32 Bits (oder auch 64, ja sogar 128 Bits). Man muß zu Gleitpunktzahlen übergehen, was Auswirkungen auf die Hashfunktion haben kann, oder symbolisch rechnen, was umständlich und langsamer ist. Hilfe kann man durch moduloRechung (Unterabschnitt 6.1.4, Seite 193) bekommen. Danach ist 0 (an q n + an−1 q n−1 + · · ·+ a1 q 1 + a0 q 0 ) mod m = (an qn0 + an−1 qn−1 + · · · + a1 q10 + a0 q00 ) mod m (10.1) 0 i 0 mit qi := q mod m. Die qi berechnet man bei festgelegter Tabellengröße m nur einmal und hat danach Zahlen in vernünftigen Größen. Siehe Aufgabe 10.1. In unserem Beispiel wollen wir der Einfachheit halber führenden Nullen zulassen und die Tabelle 10.1 sowie q = 26 benutzen. Wir nehmen sogar die Zeichenreihen L und AAL hinzu, um absichtlich eine Kollision durch führende Nullen zu bewirken. Die Länge der Zeichenreihen wollen wir auf 6 begrenzen und haben somit keine Schwierigkeiten mit dem Zahlenbereich. 2 Liegt die Transformation der ursprünglichen Schlüsselwerte in natürliche Zahlen vor, so muß danach eine Funktion angewendet werden, die die Hashadresse ausrechnet. Wichtig sind die beiden folgenden Methoden. Divisionsmethode. Bei dieser naheliegenden Methode findet man zu einem Schlüsselwert die Hashadresse, indem man den Schlüsselwert k durch die Größe m der Hashtabelle teilt und den Rest als Hashwert nimmt. h(k) := k mod m Dabei muß man allerdings einen geeigneten Wert für m wählen. m sollte z. B. keine gerade Zahl sein, weil sonst gerade Schlüsselwerte auf gerade Adressen und ungerade auf ungerade Adressen abgebildet werden. Aus ähnlichen Gründen sollte man keine Zahl m wählen, die gleich q i ± j mit kleinem i, j ist. q ist hierbei die Basis der Zahlendarstellung – in unserem Beispiel 26. Empfohlen wird, eine Primzahl zu nehmen, die diesen Bedingungen genügt. Hält man sich an diese Regel, so liefert die Divisionsmethode in der Praxis gute Ergebnisse. 10.2. GRUNDLAGEN DES HASHINGS 311 Multiplikationsmethode. Bei dieser Methode wird der Schlüsselwert k mit einer irrationalen Zahl 0 < α < 1 multipliziert und davon der ganzzahlige Anteil abgezogen: β := kα − bkαc. Nach einem Satz von Vera Turan Sós3 ist β in [0, 1] gut verteilt und √ 5−1 h(k) := bkβc ist es in [0, m − 1]. besonders gute Werte erhält man für α := 2 ≈ 0.6180334. Der Wert von m ist bei dieser Methode nicht kritisch. Fortsetzung 1 von Beispiel 10.1: Unser Beispiel soll hier fortgesetzt werden. Bei der Divisionsmethode ist zuerst einmal die Größe m der Tabelle festzulegen. Dabei ist zunächst zu beachten, daß alle vorkommenden Schlüsselwerte hinein passen sollten. Wir brauchen also mindestens 20 Einträge. Dann kommen für m z. B. die Primzahlen 23, 29, 31, 37 infrage. Sie genügen allesamt den obigen Anforderungen nicht gut, am wenigsten 23, am besten 37. Tabelle 10.2 zeigt die Ergebnisse für m = 23 und m = 37. Für m = 23 ergeben sich die folgenden Kollisionsmengen {QUARK, ZANK, L, AAL}, {PFAU, DER}, {LUST, SEHEN} und {NOCH, ROT}. Für m = 37 ergben sich immer noch 3 Kollisionsmengen, an denen insgesamt 7 Schlüsselwerte beteiligt sind. Bei der Berechnung der Hashadressen durch die Multiplikationsmethode – letzte Spalte der Tabelle – wurden m = 23 und α = 0.618033 benutzt5 . Es ergeben sich die Kollisionsmengen {QUARK, HAT}, {ANTON, DER, KNABE, ROT} sowie {LUST, L, AAL}. 2 10.2.2 Kollisionsauflösung durch Verkettung Eine naheliegende Lösung des Kollisionsproblems liegt darin, alle existierenden Schlüsselwerte, die auf die gleiche Hashadresse führen (Synonyme), in einer linearen Liste zu verketten. Die Hashtabelle enhält entweder einen NIL-Verweis oder zeigt auf den Anfang der Kette. Solange keine langen Listen auftreten, haben wir ein sehr schnelles Suchverfahren. Etwas genauer: Wir nehmen einmal an, daß jeder zu bearbeitende Schlüsselwert (Suchen, Löschen, Einfügen) eine Hashadresse liefert, die mit gleicher Wahrscheinlichkeit auf jeden möglichen Tabellenplatz zeigt. Dann werden im Mittel mr Schlüsselwerte auf jede Adresse fallen, d. h. das ist die Länge der Kette. Bei jedem Eintrag müssen wir im Mittel die Hälfte der Kette durchsuchen, bis wir einen Schlüsselwert finden und gegebenenfalls löschen können. Wir müssen die gesamte Überlaufkette durchsuchen, bevor wir wissen, daß der Schlüsselwert nicht vorhanden ist und gegebenenfalls eine neuen Wert einfügen können. Unter unseren Annahmen haben wir bei hinreichend großer Hashtabelle im Mittel nur einen Eintrag in der Kette. Da unsere Annahmen die Realität im allgemeinen nicht Turan Sós, Vera. ∗September,11 1930 Budapest, Ungarn. Ungarische Mathematikerin (Graphentheorie, Zahlentheorie). Bewies den von H. Steinhaus vermuteten Drei-Abstands-Satz. 4 b geschriebeng und hängt mit dem goldenen Schnitt zusammen. Zum ZuDieser Wert wird auch −Φ sammenhang mit den Fibonaccizahlen siehe Seite 12. 5 Hier wird teilweise mit recht großen Zahlen multipliziert. Um die für die Multiplikationsmethode wichtigen Nachkommastellen nicht zu verlieren, sollte mit doppelter Genauigkeit gerechnet werden. 3 312 KAPITEL 10. SUCHEN MIT SCHLÜSSELTRANSFORMATION Schlüssel Num. Wert QUARK PFAU BESUCH ANTON LAGE HUT HAT DER KNABE LUST NOCH NEIN RUHE ROT TANNE SEHEN ZAHN ZANK L AAL 7663588 267040 14039227 241709 193496 5271 4751 2149 4798278 207343 238011 231413 312498 11875 8691674 8300721 439595 439748 11 11 DM DM MM (m = 23) (m = 37) 11 0 0 10 11 12 4 21 0 2 25 3 20 23 21 4 17 14 13 15 6 10 3 3 18 7 0 21 32 18 7 27 15 10 15 20 20 33 1 7 35 3 20 4 11 21 30 11 19 35 5 11 3 17 11 11 18 11 11 18 Tabelle 10.2: Divisions- und Multiplikatiosnmethode treffen, werden längere Ketten durchaus vorkommen. Wenn wir die Hashtabelle nicht zu groß dimensionieren wollen und bewußt eine Überladung in Kauf nehmen, wird die Wahrscheinlichkeit für längere Ketten größer, aber je nach Anwendung kann das Verhahren immer noch akzeptabel sein. Man könnte eine schellere Bearbeitung der Überlauflisten durch zusätzliche Strukturen – z. B. eine Baumstruktur für jede Kette – erreichen. Das ist in den meisten Fällen jedoch wenig sinnvoll. Abbildung 10.1 zeigt die Kollisionsauflösung durch Verkettung für die letzte Spalte der Tabelle 10.2. 10.2.3 Kollisionsauflösung durch offenes Hashing In der frühen Phase der Datenverarbeitung war es sehr wichtig, Programme mit geringem Speicherplatzbedarf zu haben. In besonderem Maße galt das für Sprachübersetzer. In diesen wiederum war es die Symboltabelle, die man klein zu halten versuchte. Da lag 10.2. GRUNDLAGEN DES HASHINGS 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 • ◦.................................................. RUHE • ◦................................................ ANTON ◦................................................ ZAHN • ◦................................................ QUARK • ◦................................................ TANNE • • ◦................................................ SEHEN ◦................................................ PFAU ◦.................................................. BESUCH ◦................................................ HUT ◦................................................ NOCH • ◦.................................................. ZANK ◦................................................ LUST • ◦................................................ NEIN ◦................................................. LAGE • 313 • ◦................................................. DER • ◦................................................. KNABE ◦................................................... ROT ◦................................................. HAT • • • • • • • • • ◦................................................. L ◦................................................. AAL • • • Abbildung 10.1: Kollisionsauflösung durch Verkettung es nahe, den Platz der Kette beim Hashen mit den unbelegten Plätzen in der Tabelle auszugleichen. Wenn man das tut, spricht man von offenem Hashing (open hashing). Wir wollen uns zunächst einmal klarmachen, wie ein Schlüsselwert gesucht wird. Wir berechnen als erstes die Hashadresse h(k) und prüfen, ob an dieser Stelle der gesuchte Wert steht. Wenn ja, hat man ihn gefunden. Wenn nein, muß man zwei Fälle unterscheiden. Ist der Tabellenplatz unbelegt, so gibt es den Schlüssel im Datenbestand nicht. Falls gewünscht, kann man ihn dort einfügen. Anderenfalls ist in dem Platz ein anderer Schlüsselwert gespeichert und man muß weitersuchen. Was heißt weitersuchen? Wir erweitern unsere Hashfunktion um eine Sondierfolge (probing 314 KAPITEL 10. SUCHEN MIT SCHLÜSSELTRANSFORMATION sequence). Das ist eine Funktion, die für jeden Versuch i = 0, 1, . . . , m − 1 einen Abstand p(i) liefert, um den man von h(k) ausgehend weiterzählen muß, um die nächste zu testende Stelle in der Tabelle zu finden. Wenn man am Tabellenende angekommen ist, muß man natürlich am Anfang weitermachen. Man muß modulo m zählen. Wir betrachten nur Fälle, bei denen p(i) nicht von k abhängt. Außerdem muß p(i) so beschaffen sein, daß jeder Tabellenplatz genau einmal drankommt. Man weiß also nach maximal m Sondierungen, ob es den gesuchten Schlüsselwert gibt oder nicht. Bei jedem der Veruche macht man das gleiche wie beim ersten. Wird der Schlüssel gefunden, ist man fertig. Falls gewünscht, kann man löschen. Neu einfügen kann man nicht. Wird ein leerer Platz gefunden, gibt es den Schlüssel nicht in der Tabelle und man kann ihn auch nicht löschen. Hingegen kann man einen neuen Schlüssel einfügen. Es kann schließlich auch passieren, daß man die ganze Tabelle durchsucht hat, ohne den Schlüssel zu finden. Dann kann man wegen Platzmangels auch keinen neuen einfügen. Was soll beim Löschen eines Satzes passieren? Das Satz ist aus dem Datenbestad zu entfernen. Wir können aber nicht ohne weiteres den entsprechende Platz in der Tabelle freigeben. Es könnte ja sein, das Schlüsselwerte mit dem gleichen Hashwert später eingefügt wurden und in der Sondierfolge später dran kamen. Diese Schlüssel stehen noch in der Tabelle, sind aber wegen des Loches nicht mehr erreichbar. Man hilft sich, indem man die Tabellenstelle als ungültig, aber nicht frei, kennzeichnet. Das ist ungünstig und bedeutet, daß offens Hashing bei Anwenungen mit häufigem Löschen nicht verwendet werden sollten. Siehe auch Anmerkung 10.1. Was für eine Sondierungsfolge soll man nehmen? Hier sollen zwei Techniken vorgestellt werden. Lineares Sondieren. Man setzt p(i) := i für (i = 0, 1, . . . , m − 1) und berechnet die nächste zu testende Stelle der Tabelle durch j = (h(k) + p(i))mod m. Ein Nachteil dieses Verfahrens ist leicht zu erkennen: Bei einer Kollision wird der nächst freie Platz belegt. Bei einer weiteren Kollision zum gleichen Schlüssel wiederum der nächste. So wächst die Kollisionsliste kompakt weiter. Verschlimmert wird das dadurch, das Schlüsselwerte mit anderen Hashadressen, die in die belegte Liste fallen, zusätzlich zu deren Verlängerung beitragen. Man spricht von primärer Häufung (primary clustering). Quadratisches Sondieren. Bei quadratischem Sondieren wächst der Entfernungswert zur Ursprungsadresse quadratisch und jeder Entfernungwert wird rückwärts und vorwärts eingesetzt, Man prüft nacheinander die Plätze h(k), h(k) − 1, h(k) + 1, h(k) − 4, h(k) + 4, · · · . Es ist also p(i) = d 2i e2 (−1)i für i = 0, 1, · · · , m−1. Für m = 4i+3 und m Primzahl ist dabei gewährleistet, daß die Sondierungsfolge eine Permutation der Hashadressen 0, 1, . . . , m−1 ergibt [OttmW1996]. 10.2. GRUNDLAGEN DES HASHINGS 315 Fortsetzung 2 von Beispiel 10.1: Unser Beispiel soll hier mit Tabelle 10.3 fortgesetzt werden. Es wird die Divisionsmethode mit m = 23 benutzt. Bei Kollisionen wird lineares Schlüssel QUARK PFAU BESUCH ANTON LAGE HUT HAT DER KNABE LUST NOCH NEIN RUHE ROT TANNE SEHEN ZAHN ZANK L AAL Num. Wert Hashadr. lin.Sond.DM quadr. Sond. 7663588 11 11 11 267040 10 10 10 14039227 4 4 4 241709 2 2 2 193496 20 20 20 5271 4 5 3 4751 13 13 13 2149 10 12 9 4798278 18 18 18 207343 21 21 21 238011 7 7 7 231413 10 14 6 312498 20 22 19 11875 7 8 8 8691674 20 0 16 8300721 21 1 22 439595 19 19 15 439748 11 15 12 11 11 16 1 11 11 17 14 Tabelle 10.3: Offenes Hashing und quadratisches Sondieren eingesetzt. Es ist von Interesse, die Kollisionen zum Schlüssel QUARK zu verfolgen. Die Kollisionsmenge ist {QUARK, ZANK, L, AAL}. Man erkennt, wie sich bei linearer Sondierung durch primäre Häufung ab der Hashadresse 11 ein kompaktes Feld von 7 belegten Adressen aufbaut. Es fallen dort die Adressen für die Kollisionsmenge hinein, aber auch die Adressen für DER, HAT und NEIN. Bei quadratischem Sondieren (letzte Spalte der Tabelle) ist das nicht der Fall. Man sieht jedoch, daß beim Suchen des Schlüssels AAL mehr als die 4 kollisionsbedingten Zugriffe zur Tabelle nötig sind. Dazu machen wir uns klar, welche Sondierungsfolge zum Hashwert 11 auftritt. Sie ist in Tabelle 10.4 zu sehen. Um den zu AAL gehörenden Tabellnplatz zu finden sind 15 Zugriffe zur Tabelle nötig. 11 Zugriffe davon ergeben sich aus besetzten Plätzen anderer Schlüsselwerte. Dies Form der Leisungsminderung wird sekundäre Häufung (secondary clustering) genannt. 2 316 0 11 KAPITEL 10. SUCHEN MIT SCHLÜSSELTRANSFORMATION 1 10 2 12 3 7 4 15 4 2 6 20 7 18 8 4 9 9 10 13 11 21 12 1 13 8 14 14 15 16 16 6 17 22 18 0 19 3 20 19 21 5 Tabelle 10.4: Quadratische Sondierungsfolge Anmerkung 10.1 Bei Kollisionsauflösung durch Verkettung bestimmt auschließlich die Anzahl der Kollisionen die Anzahl Zugriffe bis zum Finden des Schlüssels. Bei offenem Hashing ist das keineswegs so. Besonders teuer werden die Verzögerungen duch Häufungen, beim Suchen nach Schlüsseln, die nicht im Bestand sind. Andererseits sind heutzutage hauptspeichersparende Techniken unnötig oder nachrangig. Daher ist im Normalfall vom Einsatz von Kollisionsauflösung durch offenes Hashen abzuraten. 2 10.2.4 Universelles Hashing Wenn man mit der Leistung eines Hashverfahrens nicht oder nicht mehr zufrieden ist, liegt das in der Regel daran, daß die Hashfunktion zu viele Kollisionen bewirkt. Das wiederum kann daran liegen, daß die Hashfunktion die zu bearbeitende Schlüsselmenge ungleich verteilt. Es kann aber auch daran liegen, daß die Menge der zu bearbeitenden Schlüssel für den Platz der Tabelle zu groß geworden ist. Dieser Fall wird in im folgenden Unterabschnitt 10.2.5 behanderlt. Hier widmen wir uns dem ersten Problem. Bei jeder fest gewählten Hashfunktion ist es nöglich, daß die Menge der vorkommenden Schlüssel für diese Funktion ungüstig ist. D. h. es ergeben sich große Kollisionsmengen. Dann kommt man im Mittel wesentlich besser davon, wenn man zu Anfang der Bearbeitung aus einer Menge gleichartiger Hashfunktionen zufällig eine auswählt. Die Menge dieser Hashfunktionen läßt sich so bestimmen, daß sie der folgenden Bedingung genügt: Wird eine Funktion h zufällig ausgewählt und sind x und y zwei verschiedene Schlüssel aus einer Mennge von m Schlüsseln, so gibt es im Mittel m1 Kollisionen h(x) = h(y). Man spricht dann von einer universellen Menge von Hashfunktionen (universal set of hashing functions). Für eine solche Menge kann man beweisen: Hat eine Tabelle die Größe m und sind in ihr n < m Schlüssel gespeichert, so gibt es im Mittel weniger als eine Kollision bei einer Neueinfügung. Zu den Einzelheiten siehe die Literaturangaben. 10.2.5 Dynamisches Hashing Gegenüber Baumtechniken haben Hashhverfahren den Vorteil des schnelleren Zugriffs, oft in der Größenordnung O(n). Es gibt jedoch auch zwei deutliche Nachteile. Zum einen gibt es keine Hashverfahren, die die Bearbeitung in auf- oder absteigender Reihenfolge der Schlüsselwerte erlauben. Zum anderen ist die Größe der Hashtabelle fest. Bei offenen Hashverfahren begrenzt das die Zahl der Schlüsselwerte. Bei Kollisionsauflösung 22 17 10.2. GRUNDLAGEN DES HASHINGS 317 durch Verkettung kann eine Überladung der Tabelle zu deutlichem Leistungsabfall führen. Wenn man nicht umhinkommt, dynamisch wachsende Schlüsselmengen mit unbekannter Endgröße zuzulassen und auf jeden Fall Hashverfahren beutzen will, so braucht man Möglichkeiten zur dynamischen Vegrößerung der Hashtabelle. Das ist nicht einfach, denn die Größe der Hashtabelle ist ein essentieller Parameter jeder Hashfunktion. Man muß nicht nur den Tabellenplatz vergösßern, sondern auch die Hashfunktion ändern und die erweiterte Tabelle neu einrichten. In den Modalitäten, wie man das macht und wie man möglicherweise einen abrupten Übergang vermeidet und die Erweiterung gleitend durchführt, unterscheiden sich die Lösungen. Zu Einzelheiten siehe die Literaturangaben. Aufgaben Aufgabe 10.1 Man leite Gleichung 10.1, Seite 310, her. Literatur Ähnlich wie das im nächsten Kapitel zu behandelnde Sortieren gehören Hashverfahren zur Grundlage der Algorithmik. Alle Lehrbücher über Algorithmen und Datenstrukruren behandeln den Stoff, siehe z. B. Brass [Bras2008], Kruse/Tondo/Leung [KrusTL1997], Aho/Ullman [AhoU1995], Weiss [Weis1995]. Besonders sei hingewiesen auf die umfangreichen Darstellungen in Ottmann/Widmayer [OttmW1996] und Cormen/Leiserson/Rivest [CormLR1990], in denen die Dinge, auf die in den Unterabschnitten 10.2.4 und 10.2.5 hingewiesen wird, behandelt werden. Wie immer bei Algorithmen und Datenstrukturen ist auch hier Knuth [Knut1998a] Pflichtlektüre. 318 KAPITEL 10. SUCHEN MIT SCHLÜSSELTRANSFORMATION Kapitel 11 Sortieren 11.1 Allgemeines zum Sortieren Allgemeines zum Sortieren wurde in Abschnitt 1.2, Seite 18, gesagt. Der Vollständigkeit halber soll es hier wiederholt werden. Sortieren: Die Elemente einer endlichen, nichtleeren Menge, der Sortiermenge, sollen der Reihe nach angeordnet werden! Sortieren bedeutet nicht: Nach Sorten einteilen. Die Elemente der Sortiermenge wollen wir als Sätze (siehe Abschnitt 7.1, Seite 227) auffassen. Sie besitzen einen Sortierwert und sind nach diesem anzuordnen, zu sortieren. Der Wertebereich, aus dem die Sortierwerte sind, bildet das Sortierkriterium, auch Sortierwertemenge genannt. Damit man sortieren kann, muß auf der Sortierwertemenge eine lineare Ordnung definiert sein. Man kann nach dieser Ordnung aufsteigend oder absteigend sortieren. Der Sortierwert eines Satzes der Sortiermenge muß nicht eindeutig sein, d. h. unterschiedliche Sätze der zu sortierenden Menge dürfen den gleichen Sortierwert haben. Der Sortierwert ist nicht notwendigerweise ein Schlüsselwert. Die wichtigsten Beispiele für Sortierwertemengen sind: • Ganze Zahlen • Reelle Zahlen • Wörter mit lexikographischer Ordnung (siehe Seite 114) Unter einem Sortierverfahren versteht man einen Algorithmus oder ein Programm, mit dem eine Menge sortiert werden kann. In Abschnitt 1.2, Seite 18 wurde Sortieren durch Einfügen vorgestellt. Es hat im schlechtesten Fall und auch im Mittel die Komplexität 319 320 KAPITEL 11. SORTIEREN O(n2 ). Ein elegantes und sehr effizientes Sortierverfahren haben wir in Unterabschnitt 6.1.2, Seite 178, kennengelernt, das Mischortieren. Seine Komplexität ist Θ(n · ln(n) (Gleichung 6.15, Seite 198). Eine weitere Möglichkeit zu sortieren wäre, mit den Schlüsselwerten einen binären Suchbaum – z. B. einen Rot-Schwarz-Baum – aufzubauen und diesen für aufsteigende Sortierung mit LWR und für absteigende mir RWL zu durchlaufen (Proposition 9.2, Seite 268). Allerdings geht das ohne Zusatzüberlegungen nur für eindeutige Sortierschlüssel. Siehe hierzu Aufgabe 11.1. Man spricht von Baumsortieren (tree sort). Obwohl man in der Größenordnung nicht langsamer ist als die üblichen Sortierverfahren, wird dieser Weg selten gewählt. In diesem Kapitel wollen wir uns mit drei weiteren Sortierverfaheren beschäftigen und die Mindestkomplexität beim Sortieren untersuchen. 11.2 11.2.1 Quicksort Algorithmus und Programm In den frühen Jahren der Datenverabreitung waren die Rechner deutlich langsamer; noch viel schneller ist inzwischen das Verhältnis der Hauptspeichergrößen gewachsen. So war es in jener Zeit besonders wichtig, eine Reihung von Sortierwerten möglichst schnell sortieren zu können und das, ohne zusätzlichen Speicherplatz zu belegen. 1962 stellte Tony Hoare 1 das Sortierverfaheren Quicksort vor, das diese Anforderungen auf sehr elegante Art erfüllte. Quicksort ist bis heute ein viel benutzter Sortieralgorithmus geblieben. Quicksort geht folgendermaßen vor: 1. Es wird aus den Sortierwerten die abstrakte Struktur eines binären Suchbumes aufgebaut und damit die Sortierung bewirkt. 2. Durch geschickte Ausnutzung der Indextruktur der Reihung wird das sortierte Ergebnis in der Reihung selbst aufgebaut. Dabei geschehen die Schritte 1. und 2. nicht nacheinander, sondern sind ineinander verzahnt. Um den Aufbau des binären Suchbaumes zu erläutern, nehmen wir zunächst an, daß eine Menge von N eindeutigen Schlüsselwerten vorliegt, die aufsteigend sortiert werden soll. Ein Element aus dieser Menge wird als Vergleichselement – Pivotelement – ausgewählt, und der Rest der Menge in zwei Teilmengen zerlegt. Die erste Teilmenge enthält alle Schlüsselwerte, die kleiner sind als der Pivot, die zweite die größeren Schlüsselwerte. Dann wird das Verfahren rekursiv auf die Teilmengen angewandt. Es endet mit leeren oder einelementigen Teilmengen. Tabelle 11.1 zeigt den Algorithmus in Pseudocode. Es ist Hoare, Sir Charles Antony Richard ∗11.Januar 1934, Colombo, Sri Lanka. Britischer Informatiker. Studierte in Oxford und Moskau. Bis zur Emeritierung Professor in Oxford. Die Prozeßalgebra Communicating Sequential Processes ist sein wichtigster Betrag zu Theoretischen Informatik und zur Theorie der Programmiersptrachen. 1 11.2. QUICKSORT ' & 321 SMENGE ∗QUICKSORT(SMENGE ∗M) 1 { SWERT ∗s, ∗p; // Laufender Schluesselwert, Pivotelement 2 SMENGE ∗M1, ∗M2 // Aufzubauende Teilmengen 3 4 5 6 7 8 9 10 waehle Pivotelement p aus M; fuer alle s 6= p aus M { if (s < p) fuege S in M1 ein; else fuege s in M2 ein; } if (M1 enthaelt mehr als 1 Element) QUICKSORT (M1); if (M2 enthaelt mehr als 1 Element) QUICKSORT (M2); } % Tabelle 11.1: Pseudocode für Quicksort leicht einzusehen, daß sich so ein binärer Suchbaum ergibt und die Pivotelemente die Wurzeln seiner Unterbäume sind. Wie kann man nun vorgehen, damit die Teilmengen gebildet und alle Daten nur innerhalb der gegebenen Reihung bewegt werden? Das ist in den Tabellen 11.2 und 11.3 zu sehen2 . Tabelle 11.2 zeigt die Prozedur qcksort, die in C-Programmen aufgerufen werden kann. Sie setzt eine global verfügbare Reihung A, in der die zu sortierenden Werte stehen, voraus und wird auf eine Teilreihung von A rekursiv angewandt. Besteht diese Teilreihung aus einem oder zwei Elementen, so ist sie schon sortiert oder durch ein einfaches Vertauschen dazu zu bringen. Hat sie mehr als zwei Elemente, so wird ihr erstes Element, das ist A[a], als Pivotelement genommen und sie mit der Routine partition so umgeordnet, daß zunächst die kleineren Elemente, dann das Pivotelelement und schließlich die größeren Elemente in der Teilreihung stehen. Das Pivotelement hat dann die richtige Position, auch in der Ausgangreihung. Auf die Teilreihungen davor und danach, soweit nicht leer, wird dann qcksort rekursiv angewandt. Schematisch wird das in Abbildung 11.1 gezeigt. Im oberen Teil der Abbildung steht die Teilreihung, so wie sie in partition eingegeben wird. Das erste Element ist das Pivotelement. Danach folgen in irgendeiner Reihefolge Sortierwerte, die kleiner (KL) oder größer (GR) als der Pivotwert sind. Das umgeordnete Ergebnis steht im unteren Teil der Abbildung. Die Routine partition ist in Tabelle 11.3 zu sehen. Sie arbeitet nach dem folgenden Schema: Das erste Element wird als Pivotelement genommen. Dann wandern in der Reihung vom zweiten Element ein linker Zeiger nach rechts (in Richtung höherer Indizes) und ein rechter Zeiger vom Ende nach links. Der linke Zeiger wandert, solange er auf Sortierwerte 2 $ In Hinblick auf Beispiel 11.1 werden Sortierwertvergleiche mit der C-Funktion strcmp realisiert. 322 KAPITEL 11. SORTIEREN /***************************************************************/ /* Prozedur qcksort (fuer Zeichenreihen) */ /***************************************************************/ void qcksrt(int p, int q) { int s, i; char *y; if (q < p) { printf("quicksort: Fehler! q < p\n"); printf(" p = %d q = %d\n", p, q); exit(0); } if (q == p ) return; if (q - p == 1) { i = strcmp(A[p], A[q]); if (i > 0) { y = A[p]; A[p] = A[q]; A[q] = y; } return; } s = partition (p, q); // Es ist p - q > 2 if (s == p) qcksrt(p+1, q); else { if (s == q) qcksrt(p, q-1); else { qcksrt(p, s-1); qcksrt(s+1, q); } } } Tabelle 11.2: Prozedur qcksort (für Quicksort) trifft, die kleiner als der Pivotwert sind, und der rechte, solange die angetroffenen Elemente größer als der Pivot sind. Die Zeiger heißen in partition i und j. Das Wandern endet auch, sobald die Zeiger die gleiche Position erreicht haben. Wenn beide Zeiger stehen bleiben, ohne die gleich Position erreicht zu haben, muß ein Austausch der Sortierwerte vorgenommen werden. Wenn die Zeiger die gleiche Position 11.2. QUICKSORT 323 .................... .................... .................... .................... .................... .................... ..................... ..................... ..................... ..................... ..................... ..................... ..................... ..................... .... ........ .... ........ .... ......... ... ......... .... ........ .... ........ .... ........ ... ......... .... ....... .... ........ .... ......... ... ......... .... ........ .... ...... ... .. ... .. ... .. ... ...... ...... ...... ...... ...... ...... ...... ...... ...... ...... ... ..... ..... ..... ... .... .... ... .... .... .... ... .... .... .. . . . ... . . . . ... ... ... . .. .. .. . .. . .. . ... ... ... . . . . . . . . . . . . . . . . . . . . . . . .... .... .... .... ..... ..... .... .... ..... ..... .... .... ..... .. .. . . . . . . . . . . . . . . . . . . . . . . . . . . ... ... ... . .... . ... . .... . .... . .... . .... . .... ... . ..... . ..... . . .. ..... .. ...... . . . . . . . . . . . . . . . . . . . . . . . . ...... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ....... ....... ....... ....... ....... ...... ....... ...... ....... ...... ....... ....... ...... ....... ....... ...... ........ ........ ......... ......... .................... .................. .................. .................. .................. ........... ........... ........... ................. ........... ................. ........... ........ ..... PIV GR GR KL KL KL GR GR KL KL GR GR KL GR ..................... ..................... ..................... ..................... ..................... ..................... ..................... ..................... ..................... ..................... ..................... ..................... ..................... ..................... .... ........ ... ........ .... ........ .... ........ .... ....... .... ........ .... ........ ... ........ .... ........ .... ........ .... ........ ... ........ .... ........ .... ...... ... .. ... .. ... .. ... ...... ... .. ... .. ...... ...... ...... ...... ...... ...... ...... ... ..... ..... ..... ... .... .... ... .... ... ... .... ... .. ... .... .. . . . . . .. .. .... .... ... .... . . . . . . .... . ... .... .... .... ... .... .... .... .... ... . . . . . . . . . . . . . .. . . . ... . . . . . . . . . . . . . . . . .. .... .. .... .. ... .. .... .. .... .. .... .. .... .. .... .. ..... .. .... .. .... .. .... .. .... ... ... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ...... . ...... ...... . . . . . ....... . ...... . . . ...... . ...... . ....... . ...................... ........................... ...................... ...................... ........................... ........................... ...................... ...................... ...................... ........................... ........................... ...................... ...................... ........................... KL KL KL KL KL KL PIV GR GR GR GR GR GR GR Abbildung 11.1: Erläuterung zu Quicksort (Prozedur partition) erreichen, ist entweder i = j = a und alle Sortierwerte rechts von PIV waren vom Typ GR oder es gibt Sortierwerte vom Tip KL. Im ersten Fall bleibt das Pivotelement an der gleichen Stelle. Im zweiten wird geprüft, ob ai vom Typ KL ist oder nicht. Ist es das, so wird seine Position mit dem Pivotelemtent getauscht. Ist es das nicht, so tauscht sein Vorgänger die Position mit dem Pivotelement. Beispiel 11.1 (Quicksort) In Tabelle 11.4 ist eine Reihung von 18 Zeichenreihen zu sehen, die sortiert werden soll. Sie steht in Spalte 0. Wie in C sinnvoll und üblich werden nicht die Zeichnereihen selbst, sondern Verweise auf sie in der Reihung gehalten. Das erste zu teilende Intervall ist [0, 17] und das erste Pivotelement ist QUARK. Spalte 1 der Tabelle zeigt das Ergebnis nach der ersten Partition. Für die zweite Partition ergeben sich die Teilintertvalle [0, 10] mit Pivotelement NEIN und [12, 17] mit dem Pivotelement RUHE. Die Teilintervalle der dritten Stufe sind [0, 7] mit Pivotelement LUST, Teilintervall [9, 10] (zweielementig), Teilintervall [12, 12] (einelementig) und [14, 17] mit Pivotelement TANNE. Für die vierte Stufe haben wir die Teilreihungen [0, 6] mit Pivotelement DER, [14, 14] (einelementig) und [16, 17] (zweielementig). Für die fünfte Stufe verbleiben die Teilreihungen [0, 1] (zweielementig) und [3, 6] mit Pivotelemenmt LAGE. In der sechsten Stufe wird die Teilreihung [3, 5] mit Pivotelement HAT bearbeitet, die in der siebten und letzten Stufe zur Teilreihung [4, 5] (zweielementig) führt. Deren Bearbeitung ergibt schließlich die sortierte Anordung in der letzten Spalte. 2 Anmerkung 11.1 Die Prozedur qcksrt ruft sich selbst nach der Partitionierung zunächst rekusrsiv für die Teilreihung mit den kleineren Indizes auf. Danach folgt der rekursive Aufruf für die Teilreihung mit den größeren Indizes. Wir haben einen rekursiven Abstieg „links vor rechts“. Die Reihenfolge ist jedoch belanglos und beeinflußt weder Ergebnis noch Effizienz. Wie man in Tabelle 11.4 gut erkennen kann, könnte man die sich ergebenden Teilintervalle in irgendeiner Reihenfolge bearbeiten. Insbesondere wäre es auch möglich, sie gleichzeitig zu bearbeiten. Siehe Teil V, Aufgabe 23.5, Seite 608. 2 324 KAPITEL 11. SORTIEREN /***************************************************************/ /* Partition fuer QUICKSORT */ /***************************************************************/ int partition(int a, int b) { int i, j, k; char *x, *y; i = a+1; j = b; x = A[a]; while (TRUE) { while (i < j && strcmp(A[i], x) < 0) i++; while (i < j && strcmp(A[j], x) > 0) j--; if (i == j) break; y = A[i]; A[i] = A[j]; A[j] = y; } if (i == a + 1) return a; k = strcmp(A[a], A[i]); if (k < 0) i = i-1; y = A[i]; A[i] = A[a]; A[a] = y; return i; } Tabelle 11.3: Partition für Quicksort (C-Prozedur) Quicksort mit nicht eindeutigen Sortierwerten Bisher haben wir Quicksort nur mit eindeutigen Sortierwerten betrachtet. Natürlich muß ein Sortierverfahren auch mit mehrfachen Sortierwerten zurechtkommen und Quicksort kann das auch. Wie man sich leicht klarmachen kann, reicht es dafür aus, in der Routine partition, Tabelle 11.3, die Vergleiche beim Verschieben der Zeiger von „kleiner“ in „kleiner/gleich“ und von „größer“ in „größer/gleich“ abzuändern. 11.2. QUICKSORT 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 0 QUARK PFAU BESUCH ANTON KNABE ZAHN HAT DER LUST LAGE NOCH NEIN RUHE ROT TANNE SEHEN HUT ZANK 325 1 NEIN PFAU BESUCH ANTON KNABE HUT HAT DER LUST LAGE NOCH QUARK RUHE ROT TANNE SEHEN ZAHN ZANK 2 LUST LAGE BESUCH ANTON KNABE HUT HAT DER NEIN PFAU NOCH QUARK ROT RUHE TANNE SEHEN ZAHN ZANK 3 DER LAGE BESUCH ANTON KNABE HUT HAT LUST NEIN NOCH PFAU QUARK ROT RUHE SEHEN TANNE ZAHN ZANK 4 BESUCH ANTON DER LAGE KNABE HUT HAT LUST NEIN NOCH PFAU QUARK ROT RUHE SEHEN TANNE ZAHN ZANK 5 ANTON BESUCH DER HAT KNABE HUT LAGE LUST NEIN NOCH PFAU QUARK ROT RUHE SEHEN TANNE ZAHN ZANK Tabelle 11.4: Beispiel für QUICKSORT 6 ANTON BESUCH DER HAT KNABE HUT LAGE LUST NEIN NOCH PFAU QUARK ROT RUHE SEHEN TANNE ZAHN ZANK 7 ANTON BESUCH DER HAT HUT KNABE LAGE LUST NEIN NOCH PFAU QUARK ROT RUHE SEHEN TANNE ZAHN ZANK 326 KAPITEL 11. SORTIEREN 11.2.2 Komplexität von Quicksort Zur Bestimmung der Komplexität müßten die Anzahl der Vergleiche von zwei Sortierwerten und die Anzahl von Vertauschungen herangezogen werden. Man geht jedoch davon aus, daß ein (im Mittel) fester Prozentsatz von Vergleichen zum Vertauschen der Sortierwerte führt, so daß man sich auf die Anzahl der Vergleiche beschränken kann. Mit diesem Maß sind die Komplexitäten von Quicksort: W OC(n) = Θ(n2 ) BEC(n) = Θ(ln (n) · n) AV C(n) = Θ(ln (n) · n) (11.1) (11.2) (11.3) Mit den folgenden Überlegungen soll das hergeleitet werden. Dabei ist es zweckmäßig, den binären Suchbaum zu betrachten, der sich aus einer zu sortierenden Folge von Werten ergibt. Es sei also – analog zu Seite 198 – sn eine Folge von n Sortierwerten. Der Einfachheit halber nehmen wir zunächst an, daß sie paarweise verschieden sind. Der Aufwand für die Sortierung mit Quicksort, gemessen in der Anzahl Vergleiche, soll mit R(sn ) bezeichnet werden. Abbildung 11.2 zeigt zwei Beispiele für Quicksort-Bäume. Die Knoten zeigen die Sortierwerte, die als Pivotelemente ihr endgültige Position erreicht haben und entweder bei Vergleichen, um Doppelzählungen zu vermeiden, nicht gezählt werden oder nicht weiter in Vergleiche eingehen. Die Länge t(v) des Weges von der Wurzel zu einem Knoten v gibt die Anzahl der Vergleiche an, die dieser Knoten durchläuft. Die Summe über alle Knoten ergibt für die Folge sn den gesuchte Aufwand R(sn ). Hat der Quicksortbaum von sn die Höhe t(sn ), so bedeutet das t(sn )−1 X R(ns ) = ai · i i=0 Dabei ist ai die Anzahl Knoten der Stufe i + 1 des Baumes3 . Abschätzung des schlechtesten Falles (WOC). Ist sn eine Folge aufsteigender Sortierwerte, so ergibt sich ein linearer Quicksortbaum und n−1 P es gilt R(sn ) = i·1 = n·(n−1) also W OC(n) = Ω(n2 ). Wenn die Folge sn keinen linearen 2 i=0 Quicksortbaum ergibt, müssen einige Summanden der Gesamtsumme kleiner werden. Wir , d. h. W OC(n) = O(n2 ). Zusammen ergibt sich 11.1. haben also R(sn ) ≤ n·(n−1) 2 Abschätzung des besten Falles (BEC). Wir nehmen zunächst an, daß n = 2k eine Zweierpotenz ist. Wird daraus mit Quicksort ein t(sP n )−1 ai ·i. Wenn wir in dievollständiger ausgewogener Suchbaum gebildet, so ist R(sn ) = i=0 sem nur die Blätter betrachten, so haben wir die Abschätzung R(sn ) ≥ 2k−1 · (k − 1), d. h. 3 Man beachte, daß die auf Seite 261 eingeführte Stufennummeriererung bei 1 beginnt. 11.2. QUICKSORT 327 Quicksort-Baum zu (4,6,1,2,5,3,7) Quicksort-Baum zu (2,6,1,3,5,4,7) .................. .... ...... .... ... ... ... .... . ... .. ... .. ... ... . . . . . . . . . .. ....................... ...... .... .... .... .... ... .... .... .... . . . .... .. . . . .... .. . . .... . .... .... . . . ... . . . . . . . . . . . . . . . ....... ... .... ........................ ...... . .... . . . . . ... .... .. ... . . ... . . .. .. ... .... . . . ... ... .. ... . ... ... . .. . . ... ... . .. . . . . . ..... . . ...... ... ....................... ..................... ... ... ... ... ... ... ... ... ... .. . ... . ... . ... . ... . . ... . ... ... .. ... . ... . . .................. . . ........... ...................... ...... . ... . . . . ....... .......... . . . .... ... ... ... ... ... . . . . . . . ... .. .. . . . . . .... . . .. .. .. ... .... .... . .. . ... . ... ... . . .. . . . . ... ... ... .. . .. . . . . . . . . ..... . ...... ...... .. ... . . . . ....................... . . . . . . . . . . . . . . . . . . ........ ........ ... ... ... ... ... ... ... .. ....................... . . . . ... ... ... . ... ..... ... .. .. ... ... ... . . ..... . ....................... 4 6 1 2 7 5 3 .................. .... ...... ... .... ... ... .... . ... .. .. ... ... ... . . . . . ......... ......... ... ......... ..... ... ... ... ... ... ... . . ... .. . ... . .. ... . . ... ... . ... . . ... ................... . . . . . . . . . . . . . ..... ...... ........... . . . ..... ... ... . ... ... .. .... .. ... .. ... . ... .. .. ... . ... . .. ... . . . ... . ... .... . . . . . . . ....... . ....... .......... . . . . .................. . . . . . . . . . . . . ... .. ... ... ... ... ... ... ... ... . . ... .. ... . . ... .. . . ... . . . ... .................. . ..... .......................... . . . ...... ... ... .. ... . ... . . ... .. . .... .. . .. .... ... . . ... .. . ... . . ... ... ... . .. . . . . . ..... ...... ... . . .......................... . . . . . . . . . ........ ... ... ... ... ... ... ... ... ... ... .............. ...... . .. ... ..... ... ... ... .... .. .. ... ... ... . . . .... .. . . . . . . . . . . . . .. ............. ... ... ... ... . . .. ... ... ... .................... . . . . . . . ..... . .... ... .. ... ... .... .. .. ... .. ... . . . ...... . . . . . .................. 2 1 6 7 3 5 4 Abbildung 11.2: Beispiel für Quicksort-Bäume R(sn ) = Ω(n·ln(n). Jedoch ist t(sP n )−1 i=0 ai · i ≤ (k − 1) · t(sP n )−1 i=0 ai = (k − 1) · n = O(ln(n) · n). Es folgt 11.2. Mit den auf Seite 198 vorgestellten Methoden sieht man leicht, daß das Ergebnis auch für Sortierfolgen beliebiger Länge gilt. Abschätzung des mittleren Falles (AVC). Es werden alle Sortierfolgen als gleichwahrscheinlich angesehen. Da die Reihenfolge, in der der die Teilintervalle bearbeitet werden, beliebig ist, folgt daraus, daß auch alle Folgen der Pivotelemente gleichwahrscheinlich sind. Für die interne Pfadlänge der zugehörigen binären Suchbäume wurde in Abschnitt 9.3, Gleichung 9.5, Seite 295, hergeleitet 1 EI(n) = 2n ln n − (3 − 2γ)n + 2 ln n + 1 + 2γ + O( ) n Nun ist aber E(R(sn )) = EI(n) − n, also AV C(n) = Θ(ln(n) · n). Es gilt 11.3. Anmerkung 11.2 Es ist nicht schwer einzusehen, daß die Abschätzungen auch für den Fall mehrfach auftretender Sortierwerte gelten. 328 11.2.3 KAPITEL 11. SORTIEREN Randomisiertes Quicksort Randomisiertes Quicksort ist Quicksort, bei dem des Pivotelement nicht fest aus dem zu bearbeitenden Teilintervall ausgewählt wird, sondern zufällig, d. h. mit gleicher Wahrscheinlichkeit. Das ist einfach: In Tabelle 11.1 ist in Zeile 3 „wähle Pivotelement p aus M“ zu ersetzen durch „wähle Pivotelement p zufällig aus M“. Der Ablauf ist dann ein Zufallsereignis und die Anzahl Vergleiche, die bis zur endgültigen Sortierung benötigt werden, eine Zufallsvariable. Der Erwartungswert der Anzahl Vergleiche, die Randomisiertes Quicksort braucht, ist mit unseren bisherigen Ergebnissen leicht zu finden. Die zufällige Auswahl des Pivotelementes bewirkt nämlich, daß ein zufälliger Quicksortbaum aufgebaut wird, und zwar unabhängig von der Reihenfolge der zu sortierenden Werte. Wir haben also analog zum Mittelwert bei deterministischem Quicksort AV Crnd (n) = Θ(ln(n) · n) (11.4) Randomisiertes Quicksort ist ein Las-Vegas-Algorithmus, siehe Unterabschnitt 6.3, Seite 220. Es handelt sich also um einen Algorithmus, der auf jeden Fall endet und das richtige Ergebnis liefert. Unter Umständen braucht er jedoch dafür sehr lange. Letzteres ist eine recht ungenaue Aussage und soll präzisiert werden. Daß der Algorithmus im Mittel effizient ist, wissen wir schon. Das reicht aber nicht aus. Wir wollen uns eine Dauer, d. h eine Anzahl von Vergleichen m, vorgeben und fragen nach der Wahrscheinlichkeit, daß ein Ablauf länger dauert. Wir wollen P(R(sn ) > m) abschätzen. Dafür benutzen wir die Ungleichung von Markov (Gleichung B.4, Seite 622) P(R(sn ) > m) ≤ E(R(sn )) m (11.5) 1 Setzen wir m := 10 · E(R(sn )), so ist P(R(sn ) > m) ≤ 10 . Nach den Ausführungen im vorigen Unterabschnitt ist E(R(sn )) etwas kleiner, aber in der Größenordnung von 2 ln(n)n. Für n = 10000 bräuchte man im Mittel ungefähr 195.000 Vergleiche. Die Wahr1 . scheinlichkeit, daß die Sortierung länger als 1.950.000 Vergleiche dauert ist kleiner als 10 Die Ungleichung von Markov ist kein scharfes Instrument. Mit größerem mathematischen Aufwand kann man bessere Abschätzungen erzielen. Las-Vegas-Algorithmen können auch anders betrachtet werden. Man gibt sich eine „Ungeduldsgrenze“ vor, in unserem Beispiel könnten das 1.500.000 Vergleiche sein. Wird diese Grenze bei einem Lauf überschritten, bricht man den Lauf ab, so als wäre das Ergebnis falsch. Man startet eine neuen Lauf, bei dem die Pivotelement wieder zufälling und außerdem unabhängig vom ersten Lauf gewählt werden. Die Wahrscheinlichkeit, daß beide 1 Läufe jeweils mehr 1.500.000 Vergleiche brauchen, ist dann kleiner als 100 . Allgemein: Ist P(R(sn ) ≥ m) < α mit 0 < α < 1, so gilt für k ≥ 1 P(k aufeinanderfolgende Quicksortläufe brauchen jeweils mehr als m Schritte) < αk . Randomisiertes Quicksort verlagert den Zufall auf die Wahl des Pivotelementes und hat für alle Sortierfolgen im Mittel eine gute Effizienz. 11.3. HALDEN, HEAPSORT UND PRIORITÄTSWARTESCLANGEN 11.2.4 329 Qicksort und Mischsortieren Quicksort, insbesondere Randomisiertes Quicksort, ist im Mittel ein sehr effizientes Verfahren. Daher sieht man im allgemeinen in der Komplexität keinen deutlichen Vorteil von Mischsortieren gegenüber Quicksort. Im übrigen wird im Vergleich von Quicksort und Mischsortieren von vielen Autoren Quicksort ein leichter Vorteil eingeräumt4 . Meistens wird das damit begründet, daß Quicksort ohne zusätztlichen Speicherplatz auskommt, in situ arbeitet, wir man das nennt. Es ist richtig, daß Mischsortieren ein zusätzliches Verweisfeld für jeden Schlüsselwert der zu sortierenen Daten braucht. Mit diesem einen Zusatzfeld und einem Rekursionskeller, der auch nicht größer als der von Quicksort ist, läßt sich Misortieren jedoch sehr elegant programmieren und läuft sehr effiezient ab (Unterabschnitt 6.1.2, Seite 178). Die Aussage “MergeSorte requires tons of extra memory” (siehe Fußnote 4) geht weit an der Realität vorbei. Sie ist Unsinn. Es ist übrigens bemerkenswert, das die Routine qsort der GNU-C-Bibliothek, die Schlüsselwerte in Reihungen sortiert, Mischsortieren benutzt, falls der Rechner meint, genügend Platz zu haben. Außerdem ist erwähnenswert, daß ein wirklich böser Gegenspieler Quicksort immer zu einem n2 -Verhalten zwingen kann [McIl1999]. 11.3 11.3.1 Halden, Heapsort und Prioritätswartesclangen Halden (als Datenstruktur) Die hier einzuführenden Halden sind von denen in Unterabschnitt 8.4.4, Seite 252 zu unterscheiden. Eine Halde (heap) von n Elementen ist ein vollständig gefüllter Binärbaum von Sortierwerten, in dem jeder Knoten mindestens so groß ist wie seine Nachfolger. Die Werte müssen nicht notwendigerweise paarweise verschieden sein. Die Wurzel ist stets mindestens so groß wie jedes andere Element des Baumes. Das bzw. ein größtes Element ist also leicht zu finden. Hingegen ist kein Verfahren bekannt, mit dem effizient zu einem gegebenen Wert das zugehörige Element bzw. die zugehörigen Elemente gefunden werden können. Abbildung 11.3 zeigt eine Halde aus den Werten QUARK, PFAU, BESUCH, HUT, ZANK, LUST, DER, LAGE.. Der Binärbaum der Halde ist nicht eindeutig bestimmt. Zum Beispiel könnten HUT und DER in der Reihenfolge vertauscht sein sein. Ähnlich wie bei Quicksort wird die Baumstruktur in einer Reihung gespeichert. Die Knoten sind in der Reihung von links nach rechts und dann von oben nach unten nummeriert. Tabelle 11.5 zeigt die 4 Siehe z. B.http://stackoverflow.com/questions/70402/why-is-quicksort-better-than-mergesort 330 KAPITEL 11. SORTIEREN ZANK ...... .......... ................... .......... .......... .......... .......... .......... .......... .......... .......... . . . . . . . . .......... . ....... . . .......... . . . . . . . .......... . ...... . . . . . . . . .............. . . . ............... . . . . . . . . ........ ................. QUARK . PFAU ........ ...... ........... ...... ...... ...... ...... ...... ...... . . . . . ...... . ...... ...... ...... .. ...... . . . . . . ... . .............. . . .................. . . .... . ........ ...... ........... ...... ...... ...... ...... ...... ...... . . . . . ...... .. ...... ...... ...... .. ...... . . . . . . ... . ............... . .................. . ...... LAGE .. LUST HUT DER .... .... .... ... .... . . . ...... ............ ....... BESUCH Abbildung 11.3: Beispiel für eine Halde 0 ZANK 1 QUARK 2 PFAU 3 LAGE 4 LUST 5 HUT 6 DER 7 BESUCH Tabelle 11.5: Beispiel: Halde als Reihung Reihung zu Abbildung 11.3. Wie bei dem Beispiel für Quicksort sind in der Reihung Verweise und nicht die Werte selbst gespeichert. In den Fällen, in denen wir Halden anwenden wollen, brauchen wir Halden variabler Größe. Allerdings nicht beliebige Halden, sondern nur solche, die Anfangsstücke einer festen, gegeben Reihung A der Länge N sind. Solche Reihungen sind durch die Länge l des Anfangsstückes charakterisiert. Für l = 0 existiert keine Halde5 . Für l > 0 besteht die Halde aus den Elementen A[0], A[1], . . . , A[l − 1]. Der Vorgänger eines Nichtwurzelelementes A[i] ist A [b(i − 1)/2c]. Für die Nachfolger eines Elementes A[i], soweit vorhanden, gilt: Der linke ist A[2i + 1], der rechte A[2i + 2]. Ein Nachfolger ist vorhanden, wenn 2i + 1 < l, bzw. 2i + 2 < l. Wir wollen Halden für zwei Zwecke benutzen. Zum einen soll mit ihnen ein Sortierverfahren gebildet werden und zum anderen wollen wir ihre Eignung als Prioritätswarteschlangen untersuchen. In jedem Fall müssen wir in der Lage sein, eine Halde dynamisch aufzubauen. Die wichtigste Operation dafür ist VERSICKERN6. Eingabe für diese Operation ist eine Länge 1 ≤ l ≤ N und ein Element A[i] in dem durch l gegebenen Anfangsstück der Reihung A. Weiter wird angenommen, daß sowohl linker als auch rechter Unterbaum von A[i], soweit nicht leer, Halden sind. Nach dem Versickern ist der gesamte durch i oder, wenn man so will, eine leer Halde. Der englische Ausdruck ist sift down. Cormen/Leiserson/Rivest [CormLR1990] benutzen das einprägsame Wort heapify. Die Übersetzung haldisieren hätte alledings einen eigenartigen Klang. 5 6 11.3. HALDEN, HEAPSORT UND PRIORITÄTSWARTESCLANGEN 331 gegebene Unterbaum eine Halde. Das Beispiel in den Abbildungen 11.4 und 11.5 soll das ZANK . .............................. .......... .......... .......... .......... ......... .......... . . . . . . . . . .......... . .......... .......... .......... .......... . . . . . . . . . .......... ....... . . . . .......... .... . . . . . . . . ... . .. ..... ................... ................. BALD PFAU ........... ...... ........... ...... ...... ...... ...... ...... ...... . . . . . ...... .. ...... ...... ...... .. ...... . . . . . . .. ............. . ................... . . . . .. ........... ...... ........... ...... ...... ...... ...... ...... ...... . . . . . ...... .. ...... ...... ...... .. ...... . . . . . . .. ............. . ................... . . . . .. LAGE .. LUST HUT DER .. .... ... .... . . . .. .... ........ ........... ...... BESUCH Abbildung 11.4: Beispiel für das Versickern in einer Halde (Teil 1) ZANK ..................... .......... .......... .......... .......... .......... .......... .......... .......... . . . . . . . . . .......... ....... . . . .......... . . . . . . .......... ...... . . . . . . . .......... .. . . .................. .............. . . . ................. ............... LUST ..... ..... .......... ...... ...... ...... ...... ...... ...... ...... ...... . . . . . ...... .... . ...... . . . . . ........ ........... . . .... ................... ............. LAGE .. BALD PFAU ..... ..... .......... ...... ...... ...... ...... ...... ...... ...... ...... . . . . . ...... .... . ...... . . . . . ........ ........... . . ... .................. ............. HUT DER .. ... .... .... ... . . . .... .. .... ........... ...... BESUCH Abbildung 11.5: Beispiel für das Versickern in einer Halde (Teil 2) erläutern. Im Binärbaum der Abbildung 11.4 verletzt Knoten BALD an der Stelle A[1] die Haldenbedingung. Er ist kleiner als beide Nachfolger. Versickern wählt den größeren der beiden Werte – also LUST – und vertauscht ihn mit BALD (Abbildung 11.5). Der Wert BALD steht jetzt an der Stelle A[4] und der ganze Baum erfüllt die Haldenbedingung. Tabelle 11.6 zeigt den Pseudocode für VERSICKERN. Die Baumstruktur – z. B. „hat A[i] einen rechten Nachfolger?“ – wird über Indexrechnung behandelt, wobei l die Größe des Baumes angibt, d. h. die Länge eines Anfangsstücks der Reihung A. Was kostet VERSICKERN? Vergleichen und Vertauschen haben unabhängig von der Größe der Halde einen festen Aufwand. Sie werden in jeder Stufe der Halde höchstens 332 KAPITEL 11. SORTIEREN ' void VERSICKERN(char **A, int l, int i) 1 if (linker Nachfolger von A[i] == NULL) return; 2 if (rechter Nachfolger von A[i] == NULL) j = 2i + 1; 3 if (rechter Nachfolger von A[i] != NULL) 4 { if (linker Nachfolger von A[i] >= rechter Nachfolger von A[i]) j = 2i + 1; 5 else j = 2i + 2; 6 } 7 if (A[i] < A[j]) 8 { vertausche Werte von A[i] und A[j]; 9 VERSICKERN(A, l, j); 10 } 11 return; & $ % Tabelle 11.6: Pseudocode für VERSICKERN einmal aufgerufen. Da die Halde ein vollständiger Binärbaum von höchstens N Elementen ist, kann ein Aufruf von VERSICKERN durch O(ln N) abgeschätzt werden. 11.3.2 Sortieren mit Halden (Heapsort) Um mit einer Halde zu sortieren, muß eine Reihung von Sortierwerten zunächst zu einer Halde gemacht werden. Die Grundidee ist einfach. Eine einelementige Reihung ist eine Halde. Andernfalls duchläuft man die Knoten in Rückwärtsreihenfolge und erzwingt mit VERSICKERN, daß jeder Wurzel eines Unterbaums wird, der der Haldenbedingung genügt. Wir wollen das Verfahren BAUHALDE nennen. Pseudocode dafür ist in Tabelle 11.7 zu ' & void BAUHALDE(char **A, int N) 1 int j; 2 if (N == 1) return; 3 for (j = N − 1; j >= 0; j − −) VERSICKERN(A, N, j) 4 return; Tabelle 11.7: Pseudocode für BAUHALDE sehen. Die Komplexität von BAUHALDE ist leicht abzuschätzen. Wir haben N − 1 Knoten und für jeden wird genau einmal VERSICKERN aufgerufen. D. h. für die Komplexität gilt O(N ln N). $ % 11.3. HALDEN, HEAPSORT UND PRIORITÄTSWARTESCLANGEN 333 Man kann übrigens die Zahl der Aufrufe von VERSICKERN halbieren, wenn man beachtet, daß nur die Knoten versickern müssen, die Nachfolger haben. In Aufgabe 11.2 ist das näher zu untersuchen. In Aufgabe 11.3 ist zu zeigen, daß eine Halde auch mit Aufwand O(n) aufgebaut werden kann. Wenn einmal die zu sortierende Menge zur Halde geworden ist, läuft der Sortiervorgang folgendermaßen ab: Es wird das erster Element der Halde genommen und mit dem letzten vertauscht. Damit ist gewährleistet, daß das größte Element an der richtigen Stelle steht. Dann wird die Reihung um den letzten Platz gekürzt. Das neue Anfangsstück ist keine Halde, kann jedoch durch Versickern des ersten Elementes dazu gemacht werden. Man iteriert das Verfahren mit jeweils um 1 verkürzten Anfangsstücken, bis schließlich die ganze Ausgangsreihung sortiert ist. In Tabelle 11.8 ist der Pseudocode zu sehen. ' & void HEAPSORT(char **A, int N 1 void BAUHALDE(char **A, int N); 2 void VERSICKERN(char **A, int l, int i); 3 int j; 4 if (N == 1) return; 5 BAUHALDE(A, N); 6 for (j = N − 1; j > 0; j − −) 7 { xc = A[j]; 8 A[j] = A[0]; 9 A[0] = xc; 10 VERSICKERN(A, j − 1, 0)i; 11 } 12 return; Tabelle 11.8: Pseudocode für HEAPSORT Zur Abschätzung der Komplexität von HEAPSORT ist zunächst der Aufwand O(n ln(n)) für den Aufbau der Halde zu beachten. Danach wird in jedem Schritt eine Element durch Vertauschen an die richtige Stelle gebracht und durch VERSICKERN die Haldenbedingung wieder hergestellt. Wir haben auch dafür den Aufwand O(n ln(n)) und damit diesen Aufwand auch für HEAPSORT insgesamt. 11.3.3 Prioritätswarteschlangen (nach Knuth) Die für das Sortieren eingeführte Datenstruktur Halde bietet sich an, eine dynamisch wachsende und schrumpfende Menge von Werten (Prioritätswerten), so zu organisieren, $ % 334 KAPITEL 11. SORTIEREN daß stets das erste Element einen Wert hat, der nicht kleiner als die übrigen Werte ist. Von Knuth stammt dafür die Bezeichnung Prioritätswarteschlange (priority queue)7 . Für eine Prioritätswarteschlange dieser Art werden die folgenden Operationen geforder:: 1. FIRSTPQ: Übergib den ersten Satz der Prioritätswarteschlange, ohne diese zu verändern. 2. INSERTPQ: Füge einen neuen Satz in die Prioritätswarteschlange ein. Der Satz kann eine neue Priorität haben oder auch eine schon gespeicherte Priorität aufweisen. 3. REMOVEPQ: Entferne den ersten Satz der Prioritätswarteschlange. Wir wollen nun diese Operationen durch eine Halde realisieren und dann anhand eines Beispiels illustrieren, aber auch gleichzeitig eine Schwäche aufdecken. FIRSTPQ: Liefert den erten Satz der Halde. Die Implementierung ist unmittelbar klar. Der Aufwand ist O(1). INSERTPQ: Man fügt den neuen Satz zunächst so ein, daß die Halde weiterhin ein vollständig gefüllter Binärbaum bleibt. Danach muß man den neuen Satz soweit nach oben steigen lassen, bis sich eine korrekte Halde ergibt. Code für die Prozedur INSERTPQ ist in Tabelle 11.9 zu sehen. C ist die Reihung, in der die Halde aufgebaut wird, value ' & void INSERTPQ(char **C, char *value, int l) 1 { int n, parent; 2 char xc; 3 C[l] = value; 4 if (l == 0) return; 5 n = l; 6 while (n > 0) 7 { parent = (n-1)/2; 8 j = strcmp(C[parent], C[n]); 9 if (j < 0) 10 { xc = C[parent]; 11 C[parent] = C[n]; 12 C[n] = xc; 13 n = parent; 14 } 15 else n = 0; 16 } 17 return; 18 } Tabelle 11.9: Prozedur INSERTPQ 7 Man beachte, daß diese Definition nicht der Definition von Seite 254 entspricht. $ % 11.3. HALDEN, HEAPSORT UND PRIORITÄTSWARTESCLANGEN 335 ist der einzufügende Prioritätswert, l gibt die nächste freie Stelle der Halde an. Da der einzufügende Wert mit jedem Schritt eine Position näher an die Wurzel geschoben wird, ist der Aufwand O(ln(l)). Er ist also auch O(ln(N)), wobei N die Größe der Reihung C ist. REMOVEPQ: Der erste Satz wird aus der Halde entfernt. Der letzte Satz der Halde wird zum ersten gemacht und die Halde um eine Position gekürzt. Danach erfüllen linker und rechter Unterbaum des neuen ersten Elementes die Haldeneigenschaft und man läßt es versickern. Tabelle 11.10 enthält Code für die Prozedur REMOVEPQ. Der Aufwand ' & void *REMOVEPQ(char **C, int l) 1 char xc; 2 xc = C[0]; 3 if (l == 0) return xc; 4 C[0] = C[l]; 5 VERSICKERN(C, l − 1, 0); 6 return xc; Tabelle 11.10: Prozedur REMOVEPQ wird durch den Aufruf von VERSICKERN bestimmt und ist O(ln(N)). N ist wieder die Größe der Reihung C. Beispiel 11.2 Wir wollen mittels der Prozedur INSERTPQ aus den Werten ZANK, AAR, ZAHN1 , BALD, LAGE, LUST, BERN, HUT, PFAU, ZAHN2 in dieser Reihenfolge ein Prioritätswarteschlange bilden. Danach soll mit REMOVEPQ das erste Element entfernt werden. Der Wert ZAHN tritt zweimal auf; der Index dient zur Verfolgung des Wertes im Baum. Die Graphen der Abbildung 11.6 zeigen wie sich die Prioritätswarteschlanger auf- und abbaut. PSW A zeigt die Prioritätswarteschlange, die sich nach 6 Einfügungen ergibt, PSW B die nach 8 Einfügungen, PSW C die nach allen 10 Einfügen und PSW D den Stand nach der Entfernung des ersten Elementes. 2 Anmerkung 11.3 In Beispiel fällt auf, daß der zuletzt eingefügte Wert von ZAHN und nicht der zuerst eingefügte gelöscht wurde. D. h. bei mehrfachen Prioritätswerten ist unbestimmt, in welcher Reihenfolge sie beim Abbau der Schlange auftreten. Das steht im Gegensatz zu Prioritätswarteschlangen, wie sie auf Seite 254 eingeführt wurden, und ist für Auftragsbearbeitungen aller Art ein entscheidendes Manko. Ein Scheduler eines Betriebssystems muß jedenfalls Aufträge gleicher Priorität in der Reihenfolge ihres Eintreffens bearbeiten. Merke: $ % 336 ZANK ZANK ....... .......... ................ ......... ........ .......... ........ .......... ........ . . . . . . . . ........ .. ........ .......... ........ .. .......... . . . . . . . . . . ............ .. ..... ............. .................. ZAHN1 LAGE ........ ....... ............. ....... ....... ....... ....... ....... ....... . . . . . . ....... ..... . . ....... .. . . . . .......... ............. . . .............. ............. AAR ....... .......... ................ ......... ........ .......... ........ .......... ........ . . . . . . . . ........ .. ........ .......... ........ .. .......... . . . . . . . . . . ............ .. ..... ............. .................. .... ..... ..... ..... ..... . . . . .. ......... ............ ....... BALD LAGE ZAHN1 ........ ....... ............. ....... ....... ....... ....... ....... ....... . . . . . . ....... ..... . . ....... .. . . . . .......... ............. . . .............. ............. .......... ..... .......... ..... ..... ..... ..... ..... ..... . . . . ..... . .. ... . . ...... .................. ................... . . ... HUT . LUST LUST BALD BERN ... ... ... .... ... . . .. ...... ........... .... AAR PWS A PWS B ZAHN2 ZANK ... .......... ............... ......... ........ .......... ........ ........ .......... ........ .......... . . . . . . . . . ........ ...... . . . ........ .. . . . . . . ........... ............... . . . . . ................ ................ ZAHN2 ZAHN1 PFAU ZAHN1 . .................. ....... ....... ....... ....... ....... ....... . . . . . . ....... ....... ....... .............. .......... . . . ............... . .................. . . . . .......... ..... ......... ..... ..... ..... ..... . . . . ..... ... ..... ..... ........ ........... . . .................. ............ . . .................. ....... ....... ....... ....... ....... ....... . . . . . ....... .... ....... ....... .......... ............... . . .......... . ................... . . . . . .......... ..... ......... ..... ..... ..... ..... . . . . ..... ... ..... ..... ........ ........... . . .................. ............ . .. ... .... .... ...... ... .... ... . .... . ... .. . ..... ............... ............... . .... ... HUT PFAU . . ... ... ... ... . . . ..... ............. ....... BALD PWS C LUST BERN LAGE .. BALD LUST .. .. .... .... ..... ... .... ... .... . . ... . ... . ............. ................ . . . .... ....... AAR HUT PWS D Abbildung 11.6: Dynamischer Auf- und Abbau einer Prioritätswarteschlange BERN KAPITEL 11. SORTIEREN LAGE .. AAR ... .......... ............... ......... ........ .......... ........ ........ .......... ........ .......... . . . . . . . . . ........ ...... . . . ........ .. . . . . . . ........... ................. . . . . ................ ................ 11.4. MINDESTKOMPLEXITÄT BEIM SORTIEREN Prioritätswarteschklangen auf der Basis von Halden sind für die Pozeßplanung in Betriebssystemen nicht geeignet. 337 Zusammenfassend kann man sagen: Prioritätswarteschlangen auf der Basis von Halden sind eine interessante Datenstruktur. Sie liefern stets ein Element höchster Priorität zurück. Wir wenden sie in Abschnitt 19.2, Seite 513, bei der Bestimmung kürzester Wege in Graphen an. Allgemein werden sie in der Informatikund ihren Anwemdumgem nicht sehr häufig eingesetzt. 2 11.4 Mindestkomplexität beim Sortieren Ein Algorithmus zum Sortieren ode auch zur Lösung eines anderen Problems soll zunächst einmal das Problem vollständig und richtig lösen. Das soll er, wie wir mehrfach fesgestellt haben, möglichts effizient tun. Wir haben das definiert als „die Anzahl Operationen soll bei gegebenem Umfang der Eingabe möglichst klein sein“. Wir haben uns bemüht, so genau wie möglich abzuschätzen, wie groß der Aufwand im schlechtesten Fall sein wird, wie groß im Mittel und wie groß im besten Fall. In all diesen Fällen haben wir einen fest gegebenen Algorithmus betrachtet und den Umfang und andere Eigenschaften der Eingabe variert. Zwei oder mehr Algorithmen haben wir verglichen, indem wir die algorithmusspezifischen Einzelergebnisse miteinander veglichen haben. Die Frage nach dem Mindestaufwand, den alle Algorithmen, die das Problem lösen, verursachen haben wir bisher nicht gestellt. Das wollen wir jetzt tun. Um vom Mindestaufwand für die Lösung eines Problems sprechen zu können, müssen wir festlegen, 1. welches Problem gelöst werden soll, 2. welche Algorithmen betrachtet werden und 3. was es heißt, ein Algorithmus verursacht mindestens diesen Aufwand. Um 1. und 2. exakt zu behandeln, müßte man das das Problem als formale Sprache und die Lösungalgorithmen als Turingmaschinen ansehen, ähnlich wie in Unterabschnitt 6.2.4, Seite 210. Die Turingmaschinen wären allerdings deterministisch. Zum Glück kommt man manchmal auch ohne so viel formalen Aufwand weiter. Im Falle des Sortierens betrachten wir alle Algorithmen, die zum Finden des Ergebnisses nur Schlüsselvergleiche und sonst keine Eigenschaften der zu sortierenden Menge benutzen. Es bleibt Punkt 3. Dazu legen wir ein Komplexitätsmaß √ a(n) für das Problem fest. n ist der Umfang der Eingabe. Zum Beispiel a(n) = O(n2 n) oder a(n) = O(n ln n). Es sei A die betrachtete Menge von Algorithmen, die das Problem P lösen. Wir sagen: a(n) ist eine untere Schranke für die Algorithmen aus A, wenn 1. WOC(A) = Ω(a(n)) für alle A ∈ A (untere Schranke im schlechtesten Fall) 338 KAPITEL 11. SORTIEREN 2. AVC(A) = Ω(a(n)) für alle A ∈ A (untere Schranke im Mittel) Das bedeutet, keiner der Algorithmen aus A ist im schlechtesten Fall (im Mittel) in der Größenordnun besser als a(n). Die untere Schranke läßt sich vielleicht verbessern. D. h. es kann ein a0 (n) > a(n) geben, das auch noch untere Schranke ist. Wie man leicht sieht, ist das jedoch nicht möglich, wenn es einen Algorithmus A ∈ A gibt, der die untere Schranke annimmt: WOC(A) = O(a(n)) bzw. AVC(A) = O(a(n)). Wir haben dann nicht nur eine untere Schranke für die Komplexität, sondern die untere Grenze. Wir haben dann die Mindetskomplexität (minimal complexity). Zurück zur Mindestkomplexität beim Sortieren. A ist wie gesagt die Menge der Sortieralgorithmen, die ausschließlich Vergleiche zwischen den Sortierwerten anwenden, um das sortierte Ergebnis zu gewinnen. Wir wollen zeigen, daß a(n) := n ln n sowohl eine untere Schranke im schlechtesten Fall als auch eine untere Schranke im Mittel für die betrachteten Sortieralgorithmen ist. Da wir schon wissen, daß eine Reihe von Sortierverfahren der betrachteten Klasse im schlechtesten Fall durch O(n ln n) nach oben begrenzt sind, würde das bedeuten, daß wir die Mindeskomplexität dieser Sortieralgorithmen betimmt hätten. Dazu wählen wir einen Algorithmus A0 aus und lassen ihn eine Folge von n Eingabewerten sortieren. Wir bilden den zum Sortiervorgang gehörenden Entscheidungsbaum (decision tree) Was das bedeutet, macht man sich am besten anhand eines Beipiels klar. Der zu untersuchende Algorithmus sei das in Unterabschnitt 6.1.2, Seite 178, behandelte Mischsortieren. Eingabe sie eine beliebige Folge a1 , a2 , . . . , an−1 , an von n Sortierwerten. Wir wollen voraussetzen, daß die Werte paarweise verschieden sind und können dann o.B.d.A. annehmen, daß sie eine Permutation der natürlichen Zahlen {1, 2, . . . , n−1, n} bilden. Der Algorithmus zerlegt diese Eingabe in einem ersten Schritt zunächst in Teillisten, die dann im zweiten Schritt gemischt werden. Im ersten Schritt gibt es keine Vergleiche. Nach dem Muster von Abbildung 6.2, Seite 180, ergibt sich die in Abbildung 11.7 gezeigte Aufteilung bei n = 10. Man sieht, es werden bei fester Sortieranzahl n = 10 stets die Vergleiche a1 : a9 , a3 : a7 , a2 : a10 und a4 : a8 . ausgeführt. Beim Mischen von sortierten Teillisten der Länge größer als 1 hängt die Zahl der durchgeführten Vergleiche von den Teillisten ab. Es seien z. B. (a02 , a06 , a010 ) und (a04 , a08 ) sortierte Teillisten der Stufe 2, die zu eine sortierten Teilliste der Stufe 1 zusammengemischt werden sollen. Dann hängt die Zahl der ausgeführten Vergleiche von den Eingabelisten ab und ist z. B. für (1, 3, 4), (5, 8) anders als für (1, 4, 8), (3, 5). Bei einer Eingabe vom festen Umfang n läuft also das Mischsortieren so ab, daß in einem gegebenen Binärbaum von Vergleichen ein Weg von der Wurzel zu einem Blatt durchlaufen wird und das Blatt das sortierte Ergebnis ist. Die Zahl der Vergleiche längs dieses Weges, also die Länge des Weges, gibt den Aufwand an, der zur Gewinnung des Ergebnisses nötig ist. Wir gehen nun davon aus, daß das ein solcher Binärbaum von Vergleichen, ein solcher Entscheidungbsaum, zu jedem der Algorithmen der von uns betrachteten Klasse A existiert 11.5. LINEARES SORTIEREN 339 ...u a1 a2 a3 a4 a5 a6 a7 a8 a9 a...10 ........ .............. ...... ........ . ....... ....... ....... ........ ....... . . . . . . . . ........ ....... ........ ....... ........ . . . . . . . ....... ........ 1 3 5 7 9........................... ..... . . . . . ..... ... ..... ..... ..... ..... ..... ..... . . ..... . ... ..... . . . . ..... ... . . . ..... . ... ..... . . . . ..... ... . . . ..... . ... . ..... . . . .... ... . . . . 1 5 9 ............. 3 7 .............. . . . . ... .. .... .. . . . . . . ... ... ... .. ... ... .. ... . . ... . . . . ... ... ... .. ... ... .. ... ... . . . . ... . ... .. . . . ... . . ... . .. . ... . . . . . ... . .. ... . . . . . ... . ... . ... . . . 1 9............. 5 . 3 . 7 . .. ..... . ... ... ... ... ... ... ... .. ... . . ... . . ... ... . ... .. . ... . ... a a a a a u a a a u a a u a1 u a a u a u a u a9 u ........ ........ ....... ........ ........ ........ ....... ........ ........ ....... ........ ........ ........ ...... 2 4 6 8 10.................... . ... . . . .... . .... .... ... .... .... .... . . . .... .... . . .... ... .... . . .... ... . . .... .. . . . .... .. . . . .... ..... . 2 6 10 .......... 4 8 .............. ... ... . . ... ... ... ... ... ... .. ... ... ... .. .. . . . . ... ... . .. . . . ... ... . . . . ... . . . ... .. ... ... . . . . ... ... . .. . . . . ... ... . .. . . . ... ... . . . .... .. . 2 10 ..... ..... 6 4 . 8 . .. ..... . . ... ... ... ... ... ... ... ... .. . . ... . . ... ... . ... .. .... a a a a a a a a a u a a a2 u u u a u u a a u a u a u a10 u Abbildung 11.7: Entscheidungsbaum für Mischsortieren und die Sortierung steuert. Da jede der n! Permutiationen der Eingabefolge als Ergebnis herauskommen kann, haben wir in dem Baum n! Blätter. Nach Proposition 9.1, Seite 262, gilt hmin (n!)≥ ld (n!). Aus der Stirlingschen Formel (Gleichung 6.16. Seite 201) ergibt n n = Ω(n ln n), Wir wollen das Ergebnis als Satz sich n! > ne . D. h. hhmin = Ω ln ne formulieren. Satz 11.1 Für Algorithmen, die zum Sortieren ausschließlich Vergleichsoperationen benutzen, ist die Mindestskomlexität im schlechtesten Fall und im mittleren Fall Ω(n ln n). 11.5 Lineares Sortieren Wir wollen von linearem Sortieren (linear sort) sprechen, wenn ein Sortierverfahren vorliegt mit W OC = O(n) oder wenigstens AV C = O(n). Nach dem Ergebnis des Abschnitts 11.4 müssen solche Verfahren, wenn sie überhaupt existieren, Eigenschaften ausnutzen, die über den Vergleich von Sortierwerten hinausgehen. Ja, es gibt solche Verfahren und eines soll im folgenden dargestellt werden. Zählsortieren: Beim Zählsortieren (counting sort) müssen die Sortierwerte natürliche Zahlen aus einem bekannten Intervall [0, K] sein. Wir nehmen an, daß die zu sortierenden Werte in einer Reihung A der Länge n gegeben sind. Der Grundgedanke ist nun, den Sortierwert aus A als Index in einer Hilfsreihung B der Länge K + 1 aufzufassen. Die Elemente von B werden mit 0 vorbesetzt und jedes Mal, wenn in A der Wert s auftritt, wird B[s] um 1 erhöht. Danach kann man B aufsteigend durchlaufen und den aktuellen 340 KAPITEL 11. SORTIEREN Index s so oft ausgeben oder in eine Ergebnisreihung C schreiben, wie die Häufigkeit in B[s] angibt. D. h. für B(s) = 0 wird nichts ausgegeben. Es ist einfach, dafür ein Programm anzugeben. In vielen Fällen ist damit das Problem jedoch noch nicht gelöst. Die numerischen Sortierwerte sind häufig Felder von Sätzen mit weiteren Datenfeldern. Angaben dazu stehen oft in der Reihung A, ohne Teil des Sortierwerts zu sein. Im einfachsten Fall ist die Zusatzinformation eine Satzadresse. Das geht beim Übergang zur Datei B und Rückgewinnung des Sortierwertes aus dem Index s verloren, insbesondere ist das bei mehrfachen Sortierwerten der Fall. Deren Reihefolge in A ist nicht mehr erkennbar. Man braucht also ein Verfahren, mit dem man nach der Bestimmung der Sortierposition bei der Bildung des Ergebnisses auf die ursprüngliche Stelle in A zugreifen kann. Das gelingt mit dem folgenden eleganten Kunstgriff. Man hält in B[j] nicht nur fest, wie häufig der Wert j in A auftritt, sondern auch, wie viele Werte kleiner als j es in A gibt. Danach kann man, indem man A rückwärts durchläuft die auftretenden Werte an der richtigen Stelle einer Ergebnisreihung C einfügen. In Tabelle 11.11 ist ein Programm COUNTSORT für das Zählsortieren zu sehen. Die ganzzah- ' $ & % void COUNTSORT(void) // A, B und C sind ganzzahlige Reihungen; sie sind globale Variable // N ist Länge von A und von C; K ist die Länge von B // N und K sind auch globale Variable 1 int j, actpos; 2 for (j = 0; j < K; j++) B[j] = 0; // Vorbesetzung 3 for (j = 0; j < N; j++) B[A[j]]++; // Berechnung der Häufigkeit des Sortierwertes 4 for (j = 1; j < K; j++) B[j] = B[j] + B[j-1]; 5 for (j = 0; j < K; j++) B[j]−−; // höchste Position in C für Wert A[j] 6 for (j = N-1; j>=0; j−−) // A rückwärts durchlaufen 7 { actpos = B[A[j]]; 8 C[actpos] = A[j]; 9 B[A[j]]−−; // Eine Position näher zum Anfang der Reihung 10 } 11 return Tabelle 11.11: COUNTSORT ligen Reihungen A, B und C sind globale parameter für das Programm. Die zu sortierende Zahkenfolge steht in Reihung A; das Ergebnis in Reihung C. Das folgende Beispiel soll die Arbeitsweise von COUNTSORT erläutern. 11.5. LINEARES SORTIEREN 341 Beispiel 11.3 Abbildung 11.8 besteht aus den Teilen I, II, III und IV. Sie zeigen ver0 1 2 3 4 5 6 7 8 9 10 11 A: 3 7 7 6 1 3 4 8 1 3 3 1 0 1 2 3 4 5 6 7 8 9 B: -1 2 2 6 7 7 8 10 11 11 0 1 2 3 5 5 6 7 8 9 10 11 0 1 2 3 4 5 6 7 8 9 10 11 A: 3 7 7 6 1 3 4 8 1 3 3 1 0 1 2 3 4 5 6 7 8 9 B: -1 1 2 4 7 7 8 10 11 11 0 1 2 3 5 5 6 7 8 9 10 11 C: * * * * * * * * * * * * C: * * 1 * * 3 3 * * * * * I. Zustand nach den Vorbereitungen II. Zustand nach Durchlauf j = 9 0 1 2 3 4 5 6 7 8 9 10 11 A: 3 7 7 6 1 3 4 8 1 3 3 1 0 1 2 3 4 5 6 7 8 9 B: -1 -1 2 3 6 7 7 10 10 11 0 1 2 3 4 5 6 7 8 9 10 11 0 1 2 3 4 5 6 7 8 9 10 11 A: 3 7 7 6 1 3 4 8 1 3 3 1 0 1 2 3 4 5 6 7 8 9 B: -1 -1 2 3 6 7 7 8 10 11 0 1 2 3 5 5 6 7 8 9 10 11 C: 1 1 1 * 3 3 3 4 6 * * 8 C: 1 1 1 3 3 3 3 4 6 7 7 8 III. Zustand nach Durchlauf j = 3 IV. Endzustand Abbildung 11.8: Ablauf von COUNTSORT schiedene Zustände beim Ablauf von COUNTSORT. Es soll die Zahlenfolge 3,7,7,6,1,3,4, 8,1,3,3,1 sortiert werden. Diese Folge steht in allen vier Teilen in der Reihung A und wird dort nicht verändert. Teil I zeigt den Zustand nach der Vorbereitung der Reihung B. Als Intervall für die möglichen Sortierwerte wählen wir [0, 9], so daß B die Länge 10 hat. B wird in Zeile 2 zunächst mit 0 vorbesetzt. Dann wird in Zeile 3 für jeden Sortierwert A[j] seine Häufigkeit eingetragen und in Zeile 4 alle Anzahlen kleinerer Sortierwerte addiert. Schließlich wird in Zeile 5 duch Verminderung dieses Wertes um 1 die Ausgabepostion in der Reihung C berechnet, in die das letzte Vorkommen des Sortierwertes A[j] zu speichern ist. Man erkennt in B: Die Sortierwerte 0, 2, 5 und 9 treten nicht auf. Die Sterne in Reihung C besagen, daß dort noch nichts eingetragen ist. Danach wird die Reihung A rückwärts durchlaufen (Zeile 6). Über den Sortierwert A[j] wird in B die Zielposition des Sortierwertes in C gefunden und der Wert dort eingetragen (Zeilen 6 und 8). Damit ein eventuelles weiteres Vorkommen des Sortierwertes an die richtige Stelle in C gebracht werden kann, vermindert man schließlich die Zielposition um 1 (Zeile 9). Teil II der Abbildung zeigt den Zustand nach Bearbeitung der ersten drei Sortierwerte in Rückwärtsreihenfolge, das sind die Werte 1,3,3. Sie sind an die richtigen Stellen der Reihung C gebracht worden. Die übrigen Stellen in C wurden noch nicht angesprochen. 342 KAPITEL 11. SORTIEREN In Reihung B wurden die entsprechenden Positionen einmal um 1 und einmal um 2 vermindert. Den Zustand nach Bearbeitung von 9 Sortierwerten zeigt Teil III der Abbildung. Es fehlen noch zwei Siebenen und eine Drei. Die entsprechenden Positionen in der Reihung C sind noch mit Sternen besetzt. Reihung B zeigt die Zielpositionen der noch fehlenden Sortierwerte. In Teil IV der Abbildung ist der Endzustand zu sehen. In Reihung C sind die Sortierwerte in aufsteigender Reihenfolge gespeichert. Reihung B ist zu entnehmen, daß im Vergleich zu Bild I drei Einsen, vier Dreien, eine Vier, eine Sechs, zwei Siebenen und eine Acht umgespeichert wurden. 2 Komplexität von COUNTSORT: Für jede Folge von Sortierwerten wird die Reihung A zweimal und die Reihung B drei Mal durchlaufen. Es ist also W OC = BEC = AV C = Θ(n + K). Wenn K = O(n), d. h.die Zahl der Lückenstellen zwischen den Sortierwerten wächst ungefähr so wie die Zahl der Sortierwerte einschließlich der Mehrfachzählungen, dann haben wir W OC = BEC = AV C = O(n). Anmerkung 11.4 Ein Sortierverfahren heißt stabil (stable), wenn mehrfach auftretende Sortierwerte im Ergebnis in der gleichen Reihenfolge erscheinen wie bei der Eingabe. COUNTSORT ist stabil. Siehe auch Aufgabe 11.4. 2 Aufgaben Aufgabe 11.1 Geben Sie einen Weg an, wie man mit Rot-Schwarz-Bäumen auch Sortierwertmengen mit mehrfachen Schlüsseln effizient sortieren kann. Aufgabe 11.2 Der Algorithmus von Tabelle 11.7 ist so abzuändern, daß VERSICKERN nur für die Knoten aufgerufen wird, die Nachfolger haben. Aufgabe 11.3 Zeigen Sie, daß eine Halde auch mit Aufwand O(n) (statt mit O(n ln(n)) aufgebaut werden kann. Aufgabe 11.4 Geben Sie für die in diesem Buch behandelten Sortierverfahren an, ob sie stabil sind oder nicht. Literatur Sortieralgorithmen sind eines der am besten bekannten und erforschten Gebiete der Algorithmik. Alle Bücher über Algorithmen und Datenstrukturen widmen ihm (im allgemeinen umfangreichen) Platz, so zum Beispiel Cormen/Leiserson/Rivest [CormLR1990], Brass 11.5. LINEARES SORTIEREN 343 [Bras2008], Kruse/Tondo/Leung [KrusTL1997]i, Ottmann/Widmayer [OttmW1996] und Aho/Ullman [AhoU1995]. Knuth [Knut1998a] ist das Standardwerk über Sortieralgorithmen. Die in Abschnitt 11.3 eingeführten Halden werden auch binäre Halden genannt. Es gibt auch Binomialhalden und Fibonacci-Halden. Diese können im Gegensatz zu jenen mit Aufwand O(ln(n)) verschmolzen werden werden. Auf sie wird in diesem Buch nicht eingegangen. Siehe Cormen/Leiserson/Rivest [CormLR1990], Ottmann/Widmayer [OttmW1996] oder Brass [Bras2008]. Für das Suchne nach einem Schlüsselwert sind alle Halden ungeeignet. Auch Datenstrukturen für Mengen und Relationen werden in diesem Buch nicht behandelt. Es wird auf Ottmann/Widmayer [OttmW1996] und Cormen/Leiserson/Rivest [CormLR1990] verwiesen. 344 KAPITEL 11. SORTIEREN Teil IV Allgemeine Graphen 345 Kapitel 12 Grundlagen allgemeiner Graphen 12.1 Definitionen und Beispiele Wir wollen einen allgemeinen Graphbegriff einführen. Das soll zunächst intuitiv und auf graphischem Wege erfolgen. Dazu betrachten wir Abbildung 12.1 Wir sehen dort sechs Zeichnungen von Graphen: G1, G2, G3, G4, G5, G6. Die kleinen Kreise mit den Bezeichnungen A, B, C, D, E, F sind die Knoten. Zwei Knoten können durch eine Linie verbunden sein, einige Linien verbinden einen Knoten mit sich selbst. Auch die Linien tragen Bezeichnungen. Im Graphen G1 gibt es die Linien e, f, g, h, i, j, k. Zum Graphen G6 gehören die Linien e, f, g, h, i, j, k, l, m, n. Einige Linien tragen auf einer Seite Pfeilspitzen. Das soll eine Richtung angeben. Es ist erlaubt, daß zwei verschiedene Linien die gleichen Knoten verbinden. Z. B. sind die Knoten A und B in Graph G4 durch die Linien n und h verbunden. Das gleiche gilt auch für die Graphen G5 und G6. Damit sind alle Elemente, die in allgemeinen Graphen auftreten, eingeführt und wir können zu einer formalen Definition übergehen. Es seien V und L disjunkte endliche Mengen und V 6= ∅. Definition 12.1 Eine Graphinzidenzstruktur (graph incidence structure) über V und L ist eine Abbildung ϕ : L 7→ P(V ) mit 1 ≤ |ϕ(l)| ≤ 2.1 Eine Graphinzidenzstruktur soll auch einfach Inzidenzstruktur genannt werden. V ist die Menge der Knoten (vertex), L die Menge der Linien (line), ϕ ist die Inzidenzabbildung (incidence mapping). Für Knoten werden wir auch Punkt (point) sagen2 . Die Knoten ϕ(l) heißen Inzidenzpunkte (incidence points) der Linie l. Eine Linie, die nur einen Inzidenzpunkt hat, heißt Schlinge (loop). Ein Knoten, der mit keiner Linie inzidiert, heißt P(M ) ist die Potenzmenge der Menge M . |M | bezeichnet die Anzahl Elemente der Menge M (siehe Anhang A, Seite 611). 2 Im Englischen ist auch die Bezeichnung node üblich, im Deutschen werden Knoten oft Ecken genannt. Wir wollen diese Bezeichnungen jedoch nicht benutzen. 1 347 348 KAPITEL 12. GRUNDLAGEN ALLGEMEINER GRAPHEN D .......................... ......... ..... ... ..... ...... ... ... ..... .. . . . ... . . .... .. ... ... .... ... ... ........ . . . ... ... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ..... . ... ...... .. . . . . ..... . . . . . . . . ... ...................... .. ... ... ... .... .. ... ... ... .... ....... ................. ... ....... .... .... ....... . ... ...... . . . . ... . ...... . ............................ ..... ......... ... ... .. .... . .... ... .. ... .... ............ ... ................ .......... ........ ... . ....... ....... .... .... ....... .............. . ... ... ...... ... ... . .... .. ... . . ... ..... .... . ....... ................. ........................ ....... . . . . . . .... ........... ........ .... ............ .... .. ... ................ G1: Schlichter Graph G2: Ungerichteter Graph mit Schlingen ............... ... .. .... . .... ..... ............ . . ... . . ... . . . ................ ... .... ... ... .... .... ..... ... . . . . . . . . . .. ........... ....... .... ....... ... . . . . . . . . . ....... . ........................... ..... ......... ... ... . ..... .... .. ... ... .. ................ ............................ ....... ....... ....... ............ .......... ... .. . ... .. ....................... . . . . . . ..... . . . . . . .... . . . . . . . . . . . . . . . . . .... ............ .... .. .... ..... ........... B h A e f k C j g i h E m A e f k C j g i l A e f k C j g i E F D G5: Digraph mit mehrfachen gerichteten Schlingen i F e f k C j i E l m F D G3: Allgemeiner Graph mit Schlingen h h g D n j B A F B C E ..... ..................... ...... ........ ....... .... .. .. .... ... ......................... .. ... .. ........ . ..... ..... ................. . . . . . ... . ... ... . ... . . . . . . . ... ... . .............. . . .. . . . . . . . . . . . . .................... .. . . . . . . . .... . ... .. .... .... ... ... ........... ............................. ... .... ... ....... ............. ... .. ..... ....... .. .. ... ..... ... ....... . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . ......... .... ... . .. .. ... ........................................ ... ..... .. . . . . . . . . . ...... ... ... ........................ ......................... ............... ........ ... ........ ... ........ ........... ........... ... ... .. . ... .... .. .. ... ..... ....... ................ . . ... . . . . ..... . . . . . . . . .... ..... ...... ................. .. ..... .... ..... ........... n E .... ................. .......... .............. ... ... .... ... .. . ........................... ... ....... ....... ... . ....... . . . . . . . . . . ...... .. ............... ......... ... ...... ... .. .... . . .... .. .......... ... . . . . . . . . . . .... ............................ .. .. ... . . . . . . .. ... .. ... . ..... .. .... .................... ............................ .. .......... .... .... .......... ............... ........ ...... ... ... ....... . ... . ... ... . . . . . . . . . . . . .. ... ...... ... .. . . ... . . ..... ................ ..... ......... ... .. ................................................ ..... . . .... ... . . . ..... .... . .... ..... . . . . . . ...................... ....................... ........... . ........ . ........ .. .... ............ ............. ... ... .................. .. . .... ... ... ...... . ........................ ....... . ....... . . . . . .. . ... .......... ........... .... ...................... ..... . ... ..... ............ e k l D ..................... .... ...... ........... .... .... ..... ... .. .... ... .. ... ... . ... . . . ... ................ . . . . ... . . . .. . . . . . . . . .. .. ............. ........................ . . . . . . . . . . . . . . . .. ... . .. ... . . . . . ... . . . . . . . . . . . . ... .. ... . ............... .. ... .... .... .. ......... ... .... .... ... ....... ............. .. ......... ....... ... ... ... ............ ....... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ... . . .... .. .......... ... ........................................ .. ... .... . ... .. ... .... ..... ....... .............. ... ........... .... ........ ... ........ ... . ... ....... ........ . . ... . ........ ................ . .. . .... ... .... . ... . ... .. ... . . . ... .... . . .. ..... . . ....... ....... ............... . . .... . .................. . . . ..... . . . . . . . . . . .......... ......... .... .................... ..... . .... ..... ........... h A f g F B m B l m G4: Ungerichteter Graph mit Mehrfachkanten ................... .................. ....... .... ... .. .... ... . ............................. ... ... .. . . ....... . . . . . . . . . . . . . . . . . ........ ................ ........... ... ... . . . . . .. ...... .. . .......... . . ... . . . . . . . . . . .... ............................ .. . . . . .... . . . .. . ... . .... . .... .. ... ...................... ............................ ... .......... ... .... .......... ........... ....... ...... ... ... ....... . . . . . . . . . . . . . . . . .. ... ...... ....... . .... ..... ................ ..... ......... ... .. ... .. . ..... . .... ... .... . ... . . .... ..... . . . . . . ...................... ............... ........ ........... . . . ....... ........ .... ....... ............. ... ........ ... ... .. ..... ... . ....................... . . ....... . . . . ..... . ........ . . . . . .......... . . . . . . . . . . . . . . . ... ...................... ... .. .... .... ..... .......... B n h A i f k C j g e E l m F D G6: Allgemeiner Graph mit Mehrfachlinien Abbildung 12.1: Beispiele für Graphen isoliert (isolated). Zwei verschiedene Knoten, die mit der gleichen Linie inzidieren, heißen benachbart (Nachbar, neighbor, adjazent, adjacent). Ein Knoten ist Nachbar von sich selbst, wenn er mit einer Schlinge inzidiert. Auch zwei verschiedene Linien, die einen 12.1. DEFINITIONEN UND BEISPIELE 349 gemeinsamen Inzidenzpunkt haben, werden benachbart genannt. Die Linien l1 und l2 (l1 6= l2 ) sind Mehrfachlinien (multiple lines), wenn ϕ(l1 ) = ϕ(l2 ) gilt. Wir wollen nun die Definition erweitern und auch die Pfeilspitzen, d. h. die Richtung berücksichtigen. Die Linien, die eine Richtung aufweisen, werden gesondert gekenzeichnet. Die Richtung wird durch Angabe eines ersten und eines zweiten Knotens ausgedrückt. Definition 12.2 Eine allgemeine Graphstruktur (general graph structure) auf V und L ist definiert durch • eine Inzidenzabbildung ϕ, die auf V und L eine Graphinzidenzstruktur erzeugt, • eine Teilmenge A ⊆ L und • eine Abbildung ψ : A 7→ V × V mit ψ(a) = (u, v) ⇒ u, v ∈ ϕ(a). A ist die Menge der Bögen (arc). ψ ist die Orientierungsabbildung (orientation mapping) für Bögen. Ist ψ(a) = (u, v), dann heißt u Startpunkt (head) des Bogens a und v Zielpunkt (tail) des Bogens a. Es ist zweckmäßig, den nichtorientierten Linien einer allgemeinen Graphstruktur einen eigenen Namen zu geben: Die Menge E := L \ A soll Menge der Kanten (edge) heißen. Für Kanten, aber nicht für Bögen, werden die Inzidenzpunkte auch Endpunkte genannt. Eine allgemeine Graphstruktur auf V und L wollen wir auch G(V, E, A, ϕ, ψ) schreiben und einen allgemeinen Graphen (general graph) nennen. Dabei ist L implizit durch E ∪ A gegeben. Man beachte, daß ϕ stets für ganz L definiert ist. Ein allgemeiner Graph mit A = ∅ heißt ungerichteter Graph (undirected Graph), ein allgemeiner Graph mit E = ∅, d. h. A = L, heißt gerichteter Graph (directed graph). Statt gerichteter Graph werden wir meistens Digraph (digraph) sagen. Eine Kante, die eine Schlinge ist, heißt ungerichtete Schlinge (undirected loop), ein Bogen, der eine Schlinge ist, heißt gerichtete Schlinge (directed loop). Ein Bogen ist Ausgangsbogen (outgoing arc) für seinen Startpunkt und Eingangsbogen (incoming arc) für seinen Zielpunkt. Der Startpunkt eines Bogens ist Vorgänger (predecessor) des Zielpunktes, der Zielpunkt Nachfolger (successor) des Startpunktes. Die Kanten e1 und e2 heißen Mehrfachkanten (multiple edge), wenn sie Mehrfachlinien sind. Die Bögen a1 und a2 heißen Mehrfachbögen (multiple arc), wenn sie Mehrfachlinien sind und ψ(a1 ) = ψ(a2 ) gilt. Abbildung 12.2 zeigt drei Mehrfachlinien, von denen keine Mehrfachkante oder Mehrfachbogen ist. Ungerichtete Graphen mit Mehrfachkanten werden auch Multigraphen (multigraph) genannt. Ein ungerichteter Graph ohne Mehrfachkanten und ohne Schlingen heißt schlicht (einfach, simple, strict). 350 KAPITEL 12. GRUNDLAGEN ALLGEMEINER GRAPHEN .................................................................................................................. ... .. . .. .. .... ..................................................................................... . .. . .. .. ........ ... ...................................................................................................................... Abbildung 12.2: Mehrfachlinien Die Anzahl |V | der Knoten eines Graphen heißt seine Ordnung (order) und wird oft mit n bezeichnet. Die Anzahl |L| der Linien eines allgemeinen Graphen wird i. a. mit m bezeichnet. Für schlichte Graphen gilt offenbar m≤ n · (n − 1) 2 (12.1) Ein schlichter Graph heißt vollständig (complete), wenn je zwei Knoten benachbart sind. Ein vollständiger schlichter Graph der Ordnung n wird mit Kn bezeichnet. Für ihn gilt . In Erweiterung der Bezeichnung wollen wir auch einen allgemeinen Graphen m = n·(n−1) 2 vollständig nennen, wenn je zwei Knoten benachbart sind. Abbildung 12.1 soll die obigen Definitionen erläutern. Für Graph G3 gilt z. B. VC = {A, B, C, D, E, F }, LC = {e, f, g, h, i, j, k, l, m}, EC = {e, f, g, j, k, l} und AC = {h, i, m} sowie e 7→ {A, C} f 7→ {B, C} g 7→ {C, D} h 7→ (B, A) h 7→ {A, B} i 7→ {D, F } und ψ : i 7→ (F, D) ϕ : j 7→ {C, F } m 7→ (A, A) k 7→ {C, E} l 7→ {E} m 7→ {A} Die Graphen G2 und G3 haben die gleiche Inzidenzstruktur, ebenso die Graphen G4 und G5. G5 und G6 haben nicht die gleiche Inzidenzstruktur. Es gilt zwar LG5 = LG6 , aber ϕG5 (e) = {A, C} = 6 {D, F } = ϕG6 (e). Bezeichnung: Aus Zweckmäßigkeitsgründen werden zusätzlich die folgenden Bezeichnungen eingeführt: Es sei G ein allgemeiner Graph. Mit G(V ) wird die Menge seiner Knoten bezeichnet, mit G(L), G(E), G(A) die Mengen seiner Linien, Kanten, Bögen. Dichte und dünne Graphen Es ist oft wichtig zu wissen, wie die Anzahl Linien in einer Klasse von Graphen mit der Anzahl Knoten wächst. Da bei gegebener Knotenzahl die Linienzahl beliebig wachsen kann, wenn Mehrfachlinien zugelassen sind, bezieht sich die folgende Definition nur auf Graphen ohne Mehrfachkanten und ohne Mehrfachbögen. 12.2. ORIENTIERUNGSKLASSEN UND UNTERGRAPHEN 351 Definition 12.3 Eine Klasse endlicher Graphen heißt dicht (dense), wenn |E| + |A| = Ω(|V |2 ). Sie heißt dünn (sparse), wenn |E| + |A| = O(|V |) . 3 In Erweiterung der Definition nennt man einen Graph dicht bzw. dünn, wenn er zu einer entsprechenden Klasse gehört. Vollständige Graphen sind demnach dicht. Bei dünnen Graphen wächst die Anzahl Kanten/Bögen höchstens linear mit der Anzahl Knoten. |−1) + 2|V |, also Schätzt man |E| und |A| getrennt ab, so erhält man |E| + |A| ≤ 3|V |(|V 2 2 |E| + |A| = Θ(V ). Bei dichten Graphen wächst die Anzahl Linien quadratisch mit der Anzahl Knoten. 12.2 Orientierungsklassen und Untergraphen Orientierungsklassen Alle allgemeinen Graphen mit gemeinsamer Inzidenzstruktur bilden eine Orientierungsklasse (orientation class). Zwei Graphen der gleichen Orientierungsklasse heißen Umorientierungen (reorientation) voneinander. Bei der Überführung eines allgemeinen Graphen in eine Umorientierung werden Bögen zu Kanten, Kanten zu Bögen und es finden Richtungsänderungen auf Bögen statt. Eine Umorientierung, bei der nur Kanten zu Bögen werden, ist eine Orientierung (orientation). Man spricht von einer vollständigen Orientierung (complete orientation), wenn die Menge der Kanten leer ist, also wenn sich ein Digraph ergibt. Eine Umorientierung, bei der nur Bögen zu Kanten werden, heißt Desorientierung (disorientation). Die vollständige Desorientierung (complete disorientation) ist der eindeutig bestimmte ungerichtete Graph, der zur Inzidenzstruktur gehört. In Abbildung 12.1 sind also die Graphen G2 und G3 Umorientierungen voneinander, ebenso die Graphen G4 und G5. Insbesondere ist Graph G3 eine (nicht vollständige) Orientierung von Graph G2. Graph G5 ist eine (vollständige) Orientierung von Graph G4. Graph G6 gehört zu einer anderen Orientierungsklasse. Im folgenden werden wir sehen, daß einige wichtige Eigenschaften allgemeiner Graphen nur von der Inzidenzstruktur und andere auch von der Orientierung abhängen. Aus Gründen, die Abschnitt 14.1 klar werden, nennen wir Eigenschaften, die nur von der Inzidenzstruktur abhängen, a-Eigenschaften und solche, bei denen auch die Orientierung wesentlich ist, f-Eigenschaften. Untergraphen Aus einem allgemeinen Graphen gewinnt man Untergraphen, wenn man Linien wegläßt. Man kann auch Knoten entfernen, muß dann aber auch alle Linien löschen, die mit den entfernten Knoten inzidieren. 3 Zur Bedeutung von Ω, O und Θ siehe Seite Unterabschnitt 6.1.5, Seite 195. 352 KAPITEL 12. GRUNDLAGEN ALLGEMEINER GRAPHEN Definition 12.4 Ein allgemeiner Graph H = (VH , EH , AH , ϕH , ψH ) heißt Untergraph (subgraph) eines allgemeinen Graphen G(V, E, A, ϕ, ψ), wenn VH ⊆ V , EH ⊆ E und AH ⊆ A sowie ϕH (l) = ϕ(l) und ψH (l) = ψ(l) für alle l, für die ϕH (l) bzw. ψH (l) definiert ist. H ist ein echter Untergraph (eigentlicher Untergraph, proper subgraph), wenn VH ⊂ V oder EH ⊂ E oder AH ⊂ A. Ist VH = V , so nennt man H einen aufspannenden Untergraphen (erzeugenden Untergraphen, spanning subgraph) von G. Ein maximaler, d. h nicht mehr erweiterbarer, vollständiger Untergraph eines allgemeinen Graphen wird als Clique (clique) bezeichnet. Ein Untergraph eines Untergraphen ist auch Untergraph des Ausgangsgraphen. Die Untergraphbeziehung ist eine partielle Ordnung (siehe Seite 616) auf der Menge der Untergraphen eines Graphen. Sie soll mit den gleichen Symbolen wie die die Teilmengenbeziehung bezeichnet werden: ⊆, ⊇. Wichtig sind Untergraphen eines Graphen, die durch Teilmengen von Knoten oder Teilmengen von Linien erzeugt werden, bzw. die man durch Entfernen von Knoten oder Linien gewinnt. Dazu die folgende Definition. Definition 12.5 Es sei G(V, E, A, ϕ, ψ) ein allgemeiner Graph. 1. Eine Teilmenge von Knoten ∅ = 6 U ⊆ V erzeugt (generate) den Untergraph G[U] := (U, EU , AU , ϕU , ψU ). Dabei gilt EU := {l ∈ E | ϕ(l) ⊆ U} und AU := {l ∈ A | ϕ(l) ⊆ U}. ϕU ist die Einschränkung von ϕ auf EU ∪ AU . ψU ist die Einschränkung von ψ auf AU . 2. Eine Menge X 6= ∅ aus Kanten und Bögen von G erzeugt den Untergraph G[X] := (VX , EX , AX , ϕX , ψX ). Dabei gilt VX := ϕ(X), EX := E ∩ X und AX := A ∩ X. ϕX ist die Einschränkung von ϕ auf EX ∪ AX . ψX ist die Einschränkung von ψ auf AX . 3. Die Löschung (deletion) einer Teilmenge U 6= V von Knoten ergibt den durch V \ U erzeugten Untergraphen. 4. Die Löschung einer Teilmenge X von Kanten und Bögen ergibt den durch Y := (E ∪ A) \ X erzeugten Untergraphen. Man sagt auch, ein Untergraph werde durch eine Menge von Knoten bzw. Kanten induziert (induce) oder generiert. Die Knotenmenge eines aufspannenden Untergraphen erzeugt den Ausgangsgraph. Für jeden Untergraphen H = (V 0 , E 0 , A0 , ϕ0 , ψ)) von G(V, E, A, ϕ, ψ) gilt G[E 0 ∪ A0 ] ⊆ H ⊆ G[V 0 ] Anmerkung 12.1 Ist x ein Knoten des allgeneinen Graphen G, so wird mit G − x der durch V \ {x} erzeugte Untergraph von G bezeichnet. Ist l eine Linie des allgeneinen Graphen G, so wird mit G − l der durch L \ {l} erzeugte Untergraph von G bezeichnet. 2 12.3. BIPARTITE GRAPHEN 353 Anmerkung 12.2 1. Es werden in der Literatur auch Graphstrukturen betrachtet, bei denen eine Linie mit mehr als zwei Knoten inzidiert. Man spricht dann von Hyperkanten (hyperedge) und nennt die Graphen Hypergraphen (hypergraph). Siehe hierzu das Buch von Berge [Berg1976]. 2. Einige Autoren lassen auch einen Nullgraph (null graph oder empty graph), der weder Knoten noch Kanten hat, zu. Einige Betrachtungen werden dann glatter. Da mit Nullgraphen aber mehr Sonderfälle eingeführt als vermieden werden, wollen wir sie nicht benutzen. Der einfachste Graph ist also ein Graph, der nur aus einem isolierten Knoten besteht. 3. Man kann die Definitionen 12.1 und 12.2 so erweitern, daß für V und/oder L auch unendliche Mengen zugelassen werden. Man spricht dann von unendlichen Graphen. In diesem Buch werden ausschließlich endliche Graphen betrachtet. 12.3 Bipartite Graphen Ein allgemeiner Graph, dessen Knotenmenge so in zwei Teilmengen V1 und V2 partitioniert werden kann, daß jede Linie mit einem Knoten in V1 und mit einem Knoten in V2 inzidiert, heißt bipartiter Graph (paarer Graph, bipartite graph). Ein schlichter bipartiter Graph mit |V1 | = i und |V2 | = j wird mit Ki,j bezeichnet. Die Partitionsmengen sind i. a. nicht eindeutig bestimmt, z. B. kann man isolierte Knoten beliebig auf die beiden Partitionsmengen verteilen. In einem bipartiten Graphen gibt es keine Schlingen, Mehrfachlinien können auftreten. Ein schlichter bipartiter Graph mit n = |V1 | + |V2 | Knoten hat höchstens |V1 | · |V2| Kanten. Um festzustellen, ob ein allgemeiner Graph bipartit ist, und Partitionsmengen V1 und V2 zu finden, startet man mit leeren Mengen V1 und V2 und einem beliebigen Knoten, den man in V1 einfügt. Alle Nachbarn dieses Knoten müssen in V2 liegen, deren Nachbarn wieder in V1 usw. Sollten Knoten übrigbleiben, die auf diese Art und Weise nicht erreicht werden, so beginnt man mit einem von ihnen wieder von vorn. In Algorithmus BIPART, Tabelle 12.1, ist das Verfahren detailliert angegeben. Es ist zu erkennen, daß der Algorithmus genau dann ohne Fehlermeldung endet, wenn jeder Knoten markiert, d. h. einer der Mengen V1 und V2 zugeordnet ist und alle seine Nachbarn in der anderen Menge sind (wieso?). Daher gilt die folgenden Proposition. Proposition 12.1 Der Algorithmus BIPART liefert eine gültige Knotenpartition (V1 , V2 ), falls der Graph bipartit ist, und eine Meldung, falls er es nicht ist. Der Algorithmus benutzt Tiefensuche. Diese wird in Kapitel 15, Abschnitt 15.1 ausführlich behandelt. 354 ' KAPITEL 12. GRUNDLAGEN ALLGEMEINER GRAPHEN BIPART /* Anfangs sind alle Knoten und Linien unmarkiert. /* Die Knotenmengen V1 und V2 sind leer. /* Für einen markierten Knoten v ist elem(v) /* diejenige der Mengen V1 und V2 , die v enthält. /* nelem(v) ist die Menge, die v nicht enthält. 1 for (alle Knoten v aus V ) /* Knotenliste */ 2 { if (v unmarkiert) 3 { markiere v; 4 v in V1 einfügen; 5 for (alle Linien l, die mit v inzidieren) 6 { if (l nicht markiert) 7 { markiere l; 8 BP T (otherend(l, v), v); 9 } } } } BPT(u, v) 1 if (u nicht markiert) 2 { u in nelem(v) einfügen; /* v ∈ / nelem(v) */ 3 u markieren; 4 for (alle Linien l, die mit u inzidieren) 5 { if (l nicht markiert) 6 { l markieren; 7 BP T (otherend(l, u), u); 8 } } } 9 else 10 { if (u in elem(v)) /* v ∈ elem(v) */ 11 { printf(“Der Graph ist nicht bipartit\n”); 12 exit(0); 13 } }; 14 return; & $ */ */ */ */ */ % Tabelle 12.1: Algorithmus zum Testen auf Bipartitheit und Bestimmung einer gültigen Knotenpartition Anmerkung 12.3 Im Algorithmus wird die Funktion otherend(l, v) aufgerufen. Diese wird auch in Algorithmen, die an späterer Stelle behandelt werden, benutzt. Die Funktion liefert zu einer Linie und einem Knoten, der mit der Linie inzidiert, den anderen 12.4. DER GRAD EINES KNOTENS 355 Inzidenzpunkt der Linie. 2 Anmerkung 12.4 Bipartitheit ist eine Periodizitätseigenschaft in Graphen. Zu Einzelheiten siehe Kapitel 17, Seite 465. 12.4 Der Grad eines Knotens Es sei G(V, E, A, ϕ, ψ) ein allgemeiner Graph. Die Anzahl Linien, mit denen ein Knoten v, inzidiert wird Gesamtgrad (total degree) des Knotens genannt und mit td(v) bezeichnet. Dabei werden Schlingen doppelt gezählt. Die Anzahl Kanten, mit denen ein Knoten v inzidiert, heißt Grad (degree, valence) des Knoten und wird mit dg(v) bezeichnet. Ungerichtete Schlingen werden doppelt gezählt. Die Anzahl der von ihm ausgehenden Bögen heißt sein Ausgangsgrad (outdegree) und wird mit od(v) bezeichnet. Die Anzahl der bei ihm ankommenden Bögen heißt Eingangsgrad (indegree) und wird mit id(v) bezeichnet. Schlingen werden nur einmal gezählt, treten aber für den Knoten als Eingangs- und als Ausgangsbogen auf. Es gilt offenbar td(v) = d(v) + od(v) + id(v). Ein schlichter Graph heißt k-regulär (k-regular), wenn alle Knoten den gleichen Grad k haben. Abbildung 12.3 zeigt einen 3-regulären schlichten Graphen. ............... ................ .... ... ... .. ..... .... . .... ....... ...... ......... . .. .. . . . . . . . . . . . . . .... .. ....... ...... ... . . . . . . . .... ... . ... .... .... .... .... ... .... .... .... .... .... . ... ... ............. ................ .................... .................... . . . . . . ... . . .... . ... .. .. .. . . . . .... . . . . . . . . . . . . . . . . . . . . . ..................... . . ... ... ... ... . . . . .. . ... .... . . . . . . . . . . . . ................ ................ .................. .. ...... . . . ... .... . . .... . . . .... .. ... .... . ... ... . . . . . .... .... .. .. . ... ... . . . . . . . ... .... .. ... .... . .... .. ...... . .... . ... ...................... ......... .......... ... ... .. ... ... ..... .. ..... .. ... . . ... . ... . . .... ... .. ............... ... ........... ... .. ... ... .. . . .... . . . ................................................................................................................................................................................................................................................................ s w u r y v x t Abbildung 12.3: 3-regulärer Graph Da jede Linie zwei Inzidenzpunkte hat bzw. als Schlinge mit 2 zum Gesamtgrad ihres Knotens beiträgt, gilt für einen allgemeinen Graphen G(V, E, A, ϕ, ψ) X 2|E ∪ A| = td(v). (12.2) v∈V Daraus folgt Hilfssatz 12.1 (Handschlaglemma) Für jeden allgemeinen Graphen ist die Anzahl der Knoten ungeraden Gesamtgrades gerade. Als einfache Anwendung von Gleichung 12.2 soll der folgende Satz bewiesen werden. 2 Satz 12.1 Jeder schlichte Graph mit |V | = n Knoten und |E| = m > b n4 c Kanten enthält ein Dreieck. 356 KAPITEL 12. GRUNDLAGEN ALLGEMEINER GRAPHEN Beweis: Es sei ein schlichter, dreiecksfreier Graph gegeben. e sei eine Kante, x und y ihre Endpunkte. Wegen der Dreiecksfreiheit gibt es keine gemeinsamen Nachbarn von x und y. Also gilt dg(x) + dg(y) ≤ n. Das gilt für alle Kanten und der Summand dg(x) tritt in dg(x) Ungleichungen (Kanten) auf. Zur Erläuterung siehe Abbildung 12.4. Summation über alle Kanten und dann Zusammenfassung nach Knoten ergibt X (dg(v))2 ≤ m · n (12.3) v∈V Zusammen mit Gleichung 12.2 und der Ungleichung von Cauchy-Schwarz-Bunjakowski ........... ... ...... .... .. ...................................... . . . . . . . ........ . ........ ........ ........ ........ . . . . . . . ........ .. ........ ........ ........ ........ . . . . . ........ . . ..... . ........ . . . . . . ........ ..... . . . . ........ . . . ..... ........ . . . . . . . ........ ..... . . . ........ . . . . ........ ...... . . . . . . ........ . ..... . . ........ . . . . . ........ ..... . . . . . . ........ . ...... . ........ . . . . . . ........ ..... . . . . ........ . . . ..... ........ . . . . . . . ........ ............. ............................... ................ ................ ................ ... ......... . . . . . . . . . . .. ... .. ................................................................................................................................. .. . .... .... . ..... . ... 3 .. .. 1 .. ... 2.... .... 1...... ..... 2..... . . . ....... ...... .............. .............. .......... .... . . . . . . . . . . . . . . . . . . . . . . .. . .... . .. . . . . . . . . . . . ... .. .. . ... .... . . .... . . . .. .... .. ... .... .... .... ... .... ... .... .... .... ... .... ... ... ... .... .... ... . . 2 . . . 1 3 . . . . . .... .. .... .... .... .... .... ... ... ... ... .... .... .... ... ... .... .... .... ... ... . . . . . . . . . . .... . ... ... .... .... .... ... .... ... .... ... .... .... ... .. ....... .... .... ....... . . ..................... . . . . . . . . . ... . ..... ........ .... ......................................................................................................................................................................................................................... ... ... ... .. ................ ................ z x x e y x y e e e x y Abbildung 12.4: Dreiecksfreier Graph (siehe Abschnitt C.2, Seite 631) folgt daraus X X (2m)2 = ( 1 · dg(v))2 ≤ n · (dg(v))2 ≤ n2 · m, v∈V also m ≤ n2 , 4 2 d. h. m ≤ b n4 c. v∈V 2 Anmerkung 12.5 Satz 12.1 wurde ursprünglich von Mantel [Mant1907] als Aufgabe formuliert. Siehe auch Bollobás [Boll1998]. Die Ungleichung des Satzes ist scharf, d. h. zu 2 jeder Knotenzahl n ≥ 1 gibt es einen dreiecksfreien Graphen mit m = b n4 c Kanten (siehe Aufgabe 12.4). 2 12.5 Anmerkung: Ersetzung von Kanten durch Bögen Man könnte versucht sein, Kanten durch zwei Bögen in entgegengesetzter Richtung zu ersetzen. Manche Autoren tun das auch. Das ist jedoch falsch. Wie die nachfolgenden 12.6. GLEICHHEIT UND ISOMORPHIE VON GRAPHEN* 357 Kapitel zeigen, ist in den meisten Fällen eine Kante etwas anderes als zwei Bögen, z. B. bei der Definition von Brücken. Es gibt jedoch Fälle, wo mit zwei Bögen in entgegengesetzter Richtung besser gearbeitet werden kann, als mit Kanten. Das ist dann so zu verstehen, daß man einen allgemeinen Graphen durch einen anderen ersetzt und für die zu untersuchende Fragestellung – und nur dür diese – die Ergebnisse im zweiten Graphen auf den ersten übertragen werden können. Ein Beispiel hierfür findet sich im Kapitel 19 „Kürzeste Wege in Netzwerken“. 12.6 Gleichheit und Isomorphie von Graphen* Die allgemeinen Graphen G(V, E, A, ϕ, ψ) und G0 = (V 0 , E 0 , A0 , ϕ0 , ψ 0 ) sind gleich (equal), wenn V = V 0 , E = E 0 , A = A0 , ϕ = ϕ0 und ψ = ψ 0 . Definition 12.6 Es seien G(V, E, A, ϕ, ψ) und G0 = (V 0 , E 0 , A0 , ϕ0 , ψ 0 ) allgemeine Graphen. (Iv , Il ) ist ein Inzidenzisomorphismus (incidence isomorphism) von G auf G0 , wenn gilt 1. Iv : V 7→ V 0 und Il : E ∪ A 7→ E 0 ∪ A0 sind Bijektionen. 2. für alle l ∈ E ∪ A gilt : Ist ϕ(l) = {v1 , v2 }, so folgt ϕ0 (Il (l)) = {Iv (v1 ), Iv (v2 )} (Iv , Il ) ist ein Isomorphismus (Graphisomorphismus, graph isomorphism) von G auf G0 , wenn gilt 1. (Iv , Il ) ist ein Inzidenzisomorphismus von G auf G0 . 2. Il (A) = A0 3. Für alle a ∈ A gilt: Ist ψ(a) = (v1 , v2 ) so folgt ψ 0 (Il (a)) = (Iv (v1 ), Iv (v2 )) Man nennt G und G0 inzidenzisomorph (incidence isomorphic), wenn es einen Inzidenzisomorphismus von G auf G0 gibt. Man nennt G und G0 isomorph (isomorphic), wenn es einen Isomorphismus von G auf G0 gibt. Ist G = G0 , so spricht man von einem Inzidenzautomorphismus (incidence automorphism) bzw. von einem Graphautomorphismus (graph automorphism). Ist (Iv , Il ) ein Isomorphismus von G auf G0 , so ist (Iv−1 , Il−1 ) ein Isomorphismus von G0 auf G (Aufgabe 12.5). Ist ein Isomorphismus von G auf G0 bekannt, so kennt man alle Isomorphismen von G auf G0 , wenn die Automorphismen von G bekannt sind. Es seien die Graphen G(V, E, A, ϕ, ψ) und G0 = (V 0 , E 0 , A0 , ϕ0 , ψ 0 ) gegeben und g = (Iv , Il ) ein Isomorphismus von G auf G0 . Siehe Abbildung 12.5. f = (fv , fl ) bilde die Knoten und Linien G bijektiv auf die von G0 ab und es sei fl (A) = A0 . Dann gilt der folgende Satz. 358 KAPITEL 12. GRUNDLAGEN ALLGEMEINER GRAPHEN G ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...... .. ......... ... G h ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... .. . ................... . f g .. ..................................................................................................................... . G0 Abbildung 12.5: Zusammenhang zwischen Graphisomorphismen und Graphautomorphismen Satz 12.2 Ist g ein Graphisomorphismus von G auf G0 , so ist f genau dann auch ein Graphisomorphismus von G auf G0 , wenn h := g −1 ◦ f ein Graphautomorphismus auf G ist. Beweis: Siehe Aufgabe 12.6. 2 Graphisomorphismen und Graphautomorphismen sind ein gutes Beispiel für die Zusammenhänge, die generell für Isomorphismen und Automorphismen gelten, zum Beispiel bei algebraischen Strukturen. Siehe hierzu van der Waerden4 [Waer1960], Paragraph 12. Vergleiche auch Bourbaki5 [Bour1970],[Bour1970a]. Anmerkung 12.6 Sind zwei Graphen G(V, E, A, ϕ, ψ) und G0 = (V 0 , E 0 , A0 , ϕ0 , ψ 0 ) gegeben, so können sie nur isomorph sein, wenn |V | = |V 0 |, |E| = |E 0 | und |A| = |A0 |. Ist das van der Waerden, Bartel Leendert, ∗2.Februar 1902 Amsterdam, †12.Januar 1996. Niederländischer Mathematiker. War unter anderem Professor in Leipzig und Zürich. Wurde durch sein zweibändiges Lehrbuch über Algebra [Waer1959], [Waer1960] bekannt, das entscheidend zum Durchbruch der modernen Algebra beitrug. War auch in anderen Gebieten der Mathematik, z. B der Statistik, aktiv. Gilt als einer der letzten Generalisten der Mathematik. 5 Bourbaki, Nicolas, ∗1934 Paris, † ?. Pseudonym einer Gruppe französischer Mathematiker. wechselnder Zusammensetzung, benannt nach einem französischen General der zweiten Hälfte des 19. Jahrhunderts.. Die Gruppe hatte sich zum Ziel gesetzt, die wesentlichen Teile der reinen Mathematik in axiomatischer, einheitlicher und moderner Form als Lehrbuch darzustellen. Bis 1998 sind 10 Bände erschienen: 1. Mengenlehre, 2. Algebra, 3. Allgemeine Topologie, 4. Funktionen einer reellen Veränderliochen, 5. Topologische Vektorräume, 6. Integration, 7. Kommutative Algebra, 8. Lie-Gruppen und Lie-Algebren, 9.Differetial- und analytische Mannigfaltigkeiten, 10. Spektraltheorie. Der anfängliche Schwung der Veröffentlichungem hielt bis in die zweite Hälfte des vorigen Jahrhunderts an. Zur Zeit ist ungewiß, ob Bourbaki noch lebt. Die Entwicklung der Mathematik, insbesondere die Darstellung, ist von Bourbaki nachhaltig beeinflußt worden. Eine ausführliche Bschreibung des Wirkens von Bourbaki ist in Mashaal [Mash2006] zu finden. 4 12.6. GLEICHHEIT UND ISOMORPHIE VON GRAPHEN* 359 der Fall, so kann man im Prinzip Isomorphie feststellen, indem man Definition 12.6 für alle Bijektionen überprüft. Das ist natürlich von exponentieller Komplexität und nur für sehr kleine Graphen praktisch durchführbar. Bis jetzt (2006) hat man noch kein effizientes, d. h. polynomielles Verfahren zur Feststellung der Isomorphie zweier Graphen gefunden. Allerdings ist es bis jetzt auch nicht gelungen, die NP-Vollständigkeit des Problems zu zeigen. Möglicherweise gehört das Graphisomorphie-Problem zu einer zwischen P und NP liegenden Komplexitätsklasse. Vergleiche auch Unterabschnitt 6.2.4, speziell Seite 215. Zu mehr Einzelheiten siehe Garey/Johnson [GareJ1979], Kapitel 7. Siehe auch Abschnitt 6.3, Seite 221. 2 Aufgaben Siehe auch Aufgabe 13.2. Aufgabe 12.1 Es sei G(V, E, A, ϕ, ψ) ein allgemeiner Graph. Was bedeutet: • ϕ ist surjektiv? ϕ ist injektiv? ϕ ist bijektiv? • ϕ ist surjektiv, aber nicht injektiv? ϕ ist injektiv, aber nicht surjektiv? Es ist die Surjektivität, Injektivität und Bijektivität von ψ zu untersuchen. Wie hängen diese Eigenschaften mit ϕ zusammen? Es werde A 6= ∅ angenommen. Aufgabe 12.2 Es sei (V, L, ϕ) eine Graphinzidenzstruktur. Wieviele allgemeine Graphen enthält die zugehörige Orientierungsklasse? Aufgabe 12.3 Sind die Partitionsklassen eines schlichten regulären bipartiten Graphen stets gleich groß? ([Dies2000], S.29) Aufgabe 12.4 Zeigen Sie, daß es zu jedem n ≥ 1 einen dreiecksfreien schlichten Graphen 2 mit n Knoten und m = b n4 c Kanten gibt und daß dieser bis auf Isomorphie eindeutig bestimmt ist. Aufgabe 12.5 Es sei (Iv , Il ) ein Isomorphismus von G(V, E, A, ϕ, ψ) auf G0 (V 0 , E 0 , A0 , ϕ0 , ψ 0 ). Zeigen Sie, daß (Iv−1 , Il−1 ) ein Isomorphismus von G0 auf G ist. Aufgabe 12.6 Man beweise Satz 12.2. 360 KAPITEL 12. GRUNDLAGEN ALLGEMEINER GRAPHEN Literatur Die Begriffe „Graphinzidenzstruktur“, „allgemeiner Graph“, „Orientierungsklasse“ sind in der Literatur über Graphen nicht oder nur implizit vorhanden. Sie werden hier neu eingeführt. Die üblichen Grundbegriffe werden von allen Büchern über Graphentheorie dargestellt. Als Beispiele seien Diestel [Dies2000], Bollobás [Boll1998], Chartrand/Oellermann [CharO1993], Balakrishnan [Bala1997] und Harary [Hara1969] genannt. Einen ungewöhnlichen Zugang zur Graphentheorie, nämlich über das Vier-Farben-Problem, wählt Aigner [Aign1984]. Eine gut lesbare Einführung in die Graphen- und Netzwerktheorie, wie wie sie in Teil IV dieses Buches behandeln, ist Büsing [Busi2010]. Zu nennen ist auch Tittman [Titt2011]. Für weiterführende Literatur zu Graphisomorphie siehe Leeuwen [Leeu1990], Garey/Johnson [GareJ1979] und Köbler/Schöning/Torán [KoblST1993]. Geschichtliches: Moderne Graphentheorie ist als Zweig der Diskreten Mathematik aufzufassen. Sie ist im Vergleich zu anderen Teilen der Mathematik recht jung. Vorläufer sind Probleme wie die, einen Eulerweg oder einen Hamiltonweg zu finden oder auch eine Landkarte mit vier Farbe zu färben. Sie wurden zunächst so ähnlich wie Denksportaufgaben angesehen. Den Übergang zur modernen Graphentheorie markiert Königs 6 Buch „Theorie der endlichen und unendlichen Graphen“ [Koni1990], das auch heute noch sehr lesenswert ist. Von den weiteren Gründern der modernen Graphentheorie sollen insbesondere Whitney 7 und Tutte 8 genannt werden. König, Dénes ∗21.September 1884 in Budapest, †19.Oktober 1944 ebenda. Ungarischer Graphentheoretiker. Satz von König über unendliche Graphen. Professor an der Technischen Universität Budapest. Um der Judenverfolgung zu entgehen, beging er Selbstmord. 7 Whitney, Hassler ∗23. März 1907 in New York City, †10. Mai 1989 in Dents Blanches, Schweiz. Amerikanischer Mathematiker. Havard University, später Institute of Advanced Study, Princeton. War auf dem Gebiet der Differentialtopologie erfolgreich. Davor war er als eine Folge der Arbeit an seiner Dissertation eine Zeitlang auf dem Gebiet der Graphentheorie tätig, wo er in rascher Folge wichtige und grundlegende Ergebnisse erzielte. Frustriert über die Hartnäckigkeit des Vierfarben-Problems wandte er sich endgültig von der Graphentheorie ab. 8 Tutte, William Thomas ∗14.Mai 1917 in Newmarket, Suffolk, England, †2. Mai 2002 in Kitchener, Ontario, Canada. Britisch-kanadischer Mathematiker. Sohn eines Gärtners. Studierte in England erst Chemie, dann Matematik. Während des 2. Weltkriegs gelang ihm ein spektakulärer Erfolg bei der Entzifferung militärischer deutscher Verschlüsselungen. Übersiedelte 1962 nach Kanada. Professor an der University of Toronto und später an der University of Waterloo, Ontario. Tutte leistete eine Vielzahl von bedeutenden Beiträgen zu Graphentheorie und Kombinatorik. Er galt über 3 Jahrzehnte als der führende Kopf der Graphentheorie. Er hat nur wenige Lehrbücher verfaßt. Zu nennen sind [Tutt1966] und [Tutt1984]. 6 Kapitel 13 Darstellungen von Graphen Darstellungen (representation) von Graphen werden aus verschiedenen Gründen gebraucht. Es ist wichtig, einzelne Graphen explizit als Beispiele (insbesondere auch Gegenbeispiele) angegeben zu können. Außerdem muß man einzelne Graphen explizit zur Bearbeitung durch Menschen, häufiger jedoch durch Rechner, angeben können. Schließlich helfen Darstellungen bei der allgemeinen Untersuchung von Graphen, indem man Eigenschaften der Graphen auf Eigenschaften ihrer Darstellungen zurückführt. Bei der Darstellung von Graphen gehen wir davon aus, daß es für jeden Knoten ein eindeutiges Identifikationsmerkmal, einen Knotennamen (vertex name) gibt. Häufig sind die Knotennamen linear geordnet. Außerdem mag es weitere Knotenattribute geben, die für die Bearbeitung im Graphen wichtig sind und deshalb auch in der Darstellung berücksichtigt werden. Kanten und Bögen werden eindeutig durch Liniennamen (line name) identifiziert. Bei Graphen ohne Mehrfachkanten kann eine Kante auch durch Angabe der Endpunkte bestimmt werden. Entsprechend ist bei Graphen ohne Mehrfachbögen ein Bogen durch Angabe des geordneten Paares (Startpunkt, Zielpunkt) festgelegt. Kanten und Bögen können auch weitere Attribute (z. B. Gewichte) aufweisen. 13.1 Graphische Darstellung Bei einer graphischen Darstellung (sic!) eines Graphen (graphical representation) werden die Knoten als Kreise oder Punkte in der Ebene dargestellt. Kanten werden als Linien, meistens Geradenstücke, zwischen den Endpunkten gezeichnet. Bögen werden durch Linien mit Pfeilspitzen dargestellt. Die Abbildungen 12.1, Seite 348, und 12.3, Seite 355, zeigen Beispiele. Ein und derselbe Graph kann sehr unterschiedliche Darstellungen als Zeichnung haben. Andererseits können die graphischen Darstellungen verschiedener Graphen sehr ähnlich aussehen. In Abbildung 13.1 sind die Graphen G und H gleich, die Graphen H und I ungleich. Letzteres mag auf den ersten Blick etwas verwundern, da nach dem Entfernen 361 362 KAPITEL 13. DARSTELLUNGEN VON GRAPHEN ...... ..... ....... ..... ... ...... . .... .. ........ ........ ....... . ...... ... ... ... ... .. .... ... ... .. ... .. .... ......................... .. .. ... ... ... ....... ... .... ... ... . .. .. ......................... ......... ... ...... .. .. . . .. ..... ... .... .. .. .. . ... .. .. .. .. .. ... .. ... ............. .. .. .. ... .... .... ....... .. .... ... .... . . . . . . ... ...... ...... . ... .. .. ........ .... ... .... .. .... .. ... .. ... ... .... .... ... ... .... . .. .. ............ .. . . ... ...... ..... ..... .... .... ... ... .. ... .... ..... ...... ... .. ... ........ .. ...... ..... .. ... . . .. .... .... ... .... ... ................ ....... ... ...... ... ... ........ ..... . ... ................ C B A E D Graph G ....... ..... ..... ............... ........... ... ..... ... ..... .. .... ........................................ . .... .............. ..... ....... .. ........... ... .............. ................. ............ ............... ... ..... ........ . ............... . .... . . ...... .............. ... ....... ... ..... ... ... .... ....................... ...... ....... ...... . .. ... ... .. .......... . .. . ... . . .... .................... . . . ... . . . . . . . . ... . . ... ...... . . .... ... . ...... . . . . . ........................... ................. ... .. ... . . ........................................ ..... .. ... ..... ...... ............... ........ A B C E D Graph H ....... ..... ...... ................... ................ .. ... ......................................... .. .... . . . .. ... ..... .................... ........ ... ............. ................................ ........ ............... .. .. .... ........ .... . ............... .... . . ... .... .... ... ... .... ..... ... .... ....................... ...... ...... ....... ...... . ... . . . . .. .. ..... . . .. . ... . . .... . .. .................... . . . . . . . . . . ... . . .... .... . . . . . . ...... . . . . . . . . .... .......... ....... ............... . ...... ... ... ..... ... ........................................ ..... .. ... ..... ...... ................ ........ A C B E D Graph I Abbildung 13.1: Die Graphen G und H sind gleich, die Graphen H und I ungleich der Knotenbezeichnungen identische Zeichnungen übrigbleiben. Die beiden Graphen sind isomorph. Siehe Abschnitt 12.6 und Aufgabe 13.2. Graphische Darstellungen von Graphen sind, solange die Größe der Graphen nicht zu Unübersichtlichkeit führt, für Menschen gut geeignet. Sie erlauben die Darstellung von Mehrfachlinien und die Mischung von Kanten und Bögen. Auch Attribute von Knoten und Linien können hineingeschrieben werden. Für Rechner sind graphische Darstellungen ungeeignet. 13.2 Darstellung durch Matrizen Sowohl bei den Matrixdarstellungen, die in diesem Unterabschnitt betrachtet werden, als auch bei den in in Abschnitt 13.3 zu behandelnden Listendarstellungen können Kanten/Bögen implizit durch Adjazenzen (Knotennachbarschaften) oder explizit durch selbständige Objekte und Angabe ihrer Begrenzungsknoten (Inzidenzen) dargestellt werden. Nur mit Inzidenzen können Mehrfachkanten/-bögen dargestellt werden. Adjazenzmatrizen Adjazenzmatrizen werden nur für ungerichtete Graphen ohne Mehrfachkanten und Digraphen ohne Mehrfachbögen definiert. Adjazenzmatrizen für ungerichtete Graphen ohne Mehrfachkanten: Ein ungerichteter Graph ohne Mehrfachkanten ist völlig bestimmt, wenn man zu je zwei Knoten weiß, ob sie benachbart sind. Das läßt sich durch eine quadratische Matrix (Auv )(u,v)∈V ×V angeben, deren Zeilen und deren Spalten durch die Knoten indiziert sind und die an der Matrixposition (u, v) eine 1 aufweist, wenn u und v benachbart sind, und andernfalls dort den Wert 0 enthält. Eine solche Matrix wird Adjazenzmatrix (adjacency matrix) des 13.2. DARSTELLUNG DURCH MATRIZEN 363 Graphen genannt. Adjazenzmatrizen von ungerichteten Graphen ohne Mehrfachkanten sind symmetrisch (Nachbarschaft ist symmetrisch). Schlichte Graphen enthalten Nullen in der Diagonale (keine Schlingen). Ein Graph ist genau dann regulär, wenn alle Zeilen (und alle Spalten) die gleiche Anzahl von Einsen enthalten. Tabelle 13.1 enthält links die Adjazenzmatrix zu Graph A. der Abbildung 12.1, Seite 348, und rechts die zum Graphen der Abbildung 12.3, Seite 355. A B C D E F A 0 1 1 0 0 0 B 1 0 1 0 0 0 C D E 1 0 0 1 0 0 0 1 1 1 0 0 1 0 0 1 1 0 F 0 0 1 1 0 0 r s t u v w x y r 0 1 1 0 0 0 0 1 s 1 0 1 1 0 0 0 0 t u 1 0 1 1 0 1 1 0 0 1 0 0 0 0 0 0 v w x y 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 1 1 0 1 0 1 1 1 1 0 y 0 1 1 0 Tabelle 13.1: Beispiele für Adjazenzmatrizen In Programmen wird man Adjazenzmatrizen meistens durch ein zweidimensionales Feld (array) realisieren, eventuell unter Ausnutzung der Eigenschaften der Matrix in Dreiecksgestalt. Der Grad der Ausnutzung des Speichers wird durch das Verhältnis der Anzahl Einsen der Matrix zur Gesamtanzahl von Matrixpositionen bestimmt. Es gilt 0≤ Anzahl Einsen 2|E| − |LP | = ≤1 Anzahl Matrixpositionen |V |2 Dabei ist LP die Menge der Schlingen. Adjazenzmatrizen für Digraphen ohne Mehrfachbögen: Für jeden Bogen bestimmt der Startpunkt die Zeile und der Zielpunkt die Spalte in der Adjazenzmatrix. An der entsprechenden Position wird eine Eins eingetragen. Alle anderen Positionen werden Null gesetzt. Die Adjazenzmatrix ist i. A. nicht symmetrisch. Die obigen Betrachtungen zu Speicherung und Speicherausnutzung gelten analog für Adjazenzmatrizen von Digraphen. Abbildung 13.2 zeigt einen Digraphen und seine Adjazenzmatrix. Anmerkung 13.1 Adjazenzmatrizen sind zur Bearbeitung ungerichteter und gerichteter Graphen ohne Mehrfachkanten bzw. Mehrfachbögen durch Rechner gut geeignet. Außerdem läßt sich ein Reihe von graphentheoretischen Resultaten und Graphalgorithmen recht einfach durch Operationen mit Adjazenzmatrizen gewinnen. Für allgemeine Graphen, in denen Kanten und Bögen gemischt vorkommen, sind Adjazenzmatrizen selbst dann nicht geeignet, wenn keine Mehrfachkanten und keine Mehrfachbögen vorkommen. Es lassen 364 KAPITEL 13. DARSTELLUNGEN VON GRAPHEN ..................................................................... ......................................... ..................... ..................... ............... ............... ............ ........... .......... . . . . . . . . . ......... ...... . . . . ....... . . . ...... ..... . . . . . ..... .... . .... . . ... .. . . ... ..... .. .............. . . . ... ........ . ........ ... ........... ... . . . . . . . . . . . ................... ................. ................. ................. . . . . . . . .................... . . . . . . . . . . . . . . . . ... ... ... .. . ... .. . ... . . . . .. . . . . . . . . . . . . . .. .. .. ... .. ........ ... ........ .. ........ ... ...................................................... .. .... . . . . . . . . . . . ... ...... .... 1 ................................................... ....... 2 .................................................. ....... 3 ................................................... ....... ... ......... . ... . . . ... . . . . . . . . . . . . . . . ...... ...... ....... ...... ....... ...... ....... ...... ........... ................ .................................. . . ......... . . . . . . . .. ......... ........... ...... ....... . ... ... ........ .. ... ... ........ .. ....... .. .. ... ........ ... .. ... ....... ... ........ .. . ... ... ........ .. . ....... . . .... ..... .. . ........ . . . . ... ...... ....... .. . . ........ . . . .. ........ ........................... .. ... .......... .. .. ..... .. ... ..... ... ... 4 ....... .. . . ... ......... ........ . . . . . . . . . . . . ... ........ ..... .. .... . .. .... .. ... .... ... ... . . ... .... .... ... .... .... ..... .... ...... ... . ....... . ........ ........ ... ......... .... ........ ............. ............... ... ............. . .... 5 ...... ... . ...... . . . . . . ........... 11 9 a 1 v 2 v 3 4 v 5 6 v 8 7 v 10 b a v1 v2 v3 v4 v5 b a 0 0 0 0 1 0 1 v1 1 0 0 0 0 0 0 v2 0 1 0 0 1 0 0 v3 0 0 1 1 0 0 0 v4 0 0 1 0 0 1 0 v5 1 0 0 0 0 0 0 b 0 0 0 1 0 0 0 Abbildung 13.2: Beispiel für Adjazenzmatrix sich nämlich Kanten nicht von einem Paar Bögen in entgegengesetzter Richtung unterscheiden. 2 Anmerkung 13.2 Nur in Ausnahmefällen werden die Knotennamen im Programm direkt als Indizes zur Adjazenzmatrix genommen werden können. Man wird in aller Regel im Programm durch eine geeignete Hilfstruktur, z. B. Binärbäume, Kapitel 9, oder Hashtabellen, Kapitel 10, eine effiziente Umwandlung von Knotennamen in natürliche Zahlen bereitstellen und diese als Matrixindizes nehmen. Da es n! Möglichkeiten gibt, die n Knoten eines Graphen auf {1, 2, . . . , n} abzubilden, ergeben sich somit n! Darstellungen des Graphen als n-stellige quadratische Matrix. 2 Inzidenzmatrizen Wir wollen Inzidenzmatrizen nur für ungerichtete Graphen definieren. Ein ungerichteter Graph ist völlig bestimmt, wenn man zu jedem Knoten alle Kanten kennt, mit denen er inzidiert. Das läßt sich durch eine Rechtecksmatrix I(v,e) (v,e)∈V ×E festlegen, deren Zeilen durch die Knoten und deren Spalten durch die Kanten indiziert sind und die an der Matrixposition (v, e) eine 1 aufweist, wenn v Endpunkt von e ist, und anderenfalls dort den Wert 0 enthält. Dabei wird E 6= ∅ angenommen1 . Eine solche Matrix wird Inzidenzmatrix (incidence matrix) genannt. Tabelle 13.2 zeigt die Inzidenzmatrix zu Graph A. in Abbildung 12.1, Seite 348. 1 Falls E leer ist, wird eine Pseudokante, die mit keinem Knoten inzidiert, benutzt. 13.3. DARSTELLUNG DURCH LISTEN A B C D E F h 1 1 0 0 0 0 f k 0 0 1 0 1 1 0 0 0 1 0 0 365 e 1 0 1 0 0 0 j 0 0 1 0 0 1 g 0 0 1 1 0 0 i 0 0 0 1 0 1 Tabelle 13.2: Beispiel einer Inzidenzmatrix Wie die Adjazenzmatrix kann man in Programmen auch die Inzidenzmatrix eines Graphen durch ein zweidimensionales Feld realisieren. Für den Ausnutzungsgrad des Speichers gilt 2 · |E| − |LP | 2 Anzahl Einsen = ≤ Anzahl Matrixpositionen |V | · |E| |V | und geht mit wachsender Knotenzahl gegen Null. Ebenso wird man auch bei Inzidenzmatrizen im allgemeinen Umsetzungsmechanismen von Namen (Knotennamen und Liniennamen) in Matrixindizes bereitstellen und die Anordnung der Knoten (Zeilen) sowie die Anordnung der Kanten (Spalten) berücksichtigen müssen (vergleiche Anmerkung 13.2). Mehrfachkanten und Schlingen lassen sich durch Inzidenzmatrizen ausdrücken. Man kann im Prinzip Inzidenzmatrizen auch für Digraphen definieren; das ist jedoch nicht zweckmäßig. 13.3 Darstellung durch Listen Adjazenzlisten. Zur Darstellung eines ungerichteten Graphen ohne Mehrfachkanten durch eine Adjazenzliste (adjacency list) benutzt man eine Liste aller Knoten, die Knotenliste (vertex list). Zu jedem Knoten gibt es eine Unterliste von Verweisen auf seine Nachbarknoten. Abbildung 13.3 zeigt als Beispiel die Adjazenzliste zu Graph A in Abbildung 12.1, Seite 348. Digraphen ohne Mehrfachbögen lassen sich darstellen, indem man zu jedem Knoten eine Unterliste der Nachfolger und eine Unterliste der Vorgänger aufbaut. Abbildung 13.4 zeigt eine Adjazenzlistendarstellung des Digraphen von Abbildung Abbildung 13.2, Seite 364. Mit drei Unterlisten (Nachbarn, Nachfolger, Vorgänger) lassen sich auch allgemeine Graphen ohne Mehrfachkanten/Mehrfachbögen darstellen. Ob der Knotenname und gegebenenfalls weitere zum Knoten gehörende Daten im Knoteneintrag direkt gehalten werden oder ob vom Eintrag ein Zeiger zu diesen Daten führt (Vertretersätze), ist implementierungsabhängig. 366 KAPITEL 13. DARSTELLUNGEN VON GRAPHEN ... ... B .........r ..r..................................... ........r ..r.................................. ........r r .......... ......... .. ... ... ... ... .. .......... ........ .. E .........r r ... ... ... ... .. ......... ....... ... A rr .......... ....... ... C .. ...................................... .. ... .. ... ........ ......... .. C ... ... A .........r ..r..................................... ........r ..r.................................. ........r r ......... ......... .. ... ... ... ... .. .......... ....... ... C .........r r ... ... ... ... .. .......... ........ .. rr C rr rr rr rr .. .. .. .. .. ....................................... .. .................................... .. .................................... .. .................................... .. .................................... .. ... ... ... ... ... . . . . . ... ... ... ... .. . . . . . ........ ......... ......... ......... .......... ........ ........ ........ ........ ........ ... ... ... ... .. F .........r r ... ... ... ... .. ....... . ........ ... B ......... ........ .. F rr D rr A E B .. .. ...................................... .. ................................... .. ... ... .. .. ... .. . ........ ......... ......... ......... .. .. C D ......... ......... .. ......... ........ .. ... ... D r ..r..................................... ........r ..r.................................. ........r r C F Abbildung 13.3: Adjazenzliste zu Graph A Inzidenzlisten. Die allgemeinste Form von Graphdarstellungen sind Inzidenzlisten (incidence list). Sie sind die am besten geeignete Darstellung für allgemeine Graphen. Bei Inzidenzlisten gibt es außer der Knotenliste auch eine Liste der Linien (Kanten/Bögen). Zu jedem Knoten gibt es eine Unterliste der Linien, mit denen er inzidiert, und zu jeder Linie gibt es zwei Verweise auf ihre Inzidenzpunkte. Außerdem gibt es eine Kennzeichnung der Linie als Kante oder Bogen und in letzterem Fall eine Angabe, welcher Knoten Startknoten ist. Abbildung 13.5 zeigt als Beispiel die Inzidenzliste zu Graph A in Abbildung 12.1, Seite 348. Bis auf explizit genannte Ausnahmen sollen im folgenden stets Inzidenzlisten als Datenstruktur zur Darstellung von Graphen genommen werden. Aus diesem Grund wollen wir eine konkrete Festlegung für diese Listen treffen und und ausführlicher auf sie eingehen. Siehe hierzu Abbildung 13.6. Darin bedeuten fett gezeichnete Pfeile Verweise von einem Satz auf eine Liste von Sätzen und Pfeile normaler Strichstärke Verweise von einem Satz auf einen anderen. Es gibt vier Satztypen: GRAPH, VERTEX, EDGE, INC. Ein Satz vom Typ GRAPH ist Ausgangspunkt der Darstellung. Er enthält zentrale Angaben wie z. B. den Namen und den Typ des Graphen. Außerdem enthält er Verweise auf die Knotenliste 13.3. DARSTELLUNG DURCH LISTEN Vorgängerliste 367 Nachfolgerliste Knotenliste • ◦....... .............................................. ◦ ◦....... ...............................................◦... a ◦....... ◦.................................................. ◦....... ◦............................................... ◦....... • .... .. .......... ....... ... b .... .. .......... ...... ... v4 ... ... ... ... ... ... ... . .......... ........ .. .... .. .......... ........ ... v1 .... .. .......... ...... ... v5 • ◦....... ...............................................◦.. b ◦ ◦................................................ ◦....... • .. .. ... .. ....... .. ....... ... v3 ... ... ... ... ... ... .. ......... ........ .. ... .. ....... .. ........ ... a • ◦....... ................................................◦.. v1 ◦....... ◦................................................ ◦....... • ... ... ....... . ........ ... a ... ... ... .. ... ... ... .. ....... .. ......... .. ... ... ....... . ......... .. v2 • ◦....... .............................................. ◦ ◦....... ..............................................◦.. v2 ◦....... ◦................................................ ◦....... ◦............................................. ◦....... • ... ... ......... ........ ... v4 ... ... ......... ........ ... v1 ... ... ... ... ... ... ... . ......... ......... .. ... ... ......... ......... .. v3 ... ... ......... ....... ... v4 • ◦....... ............................................... ◦ ◦....... ...............................................◦... v3 ◦....... ◦................................................ ◦....... ◦............................................. ◦....... • ... ... .......... ........ .. v3 ... ... .......... ........ ... v2 ... ... ... ... .. ... ... .. .......... ........ .. ... ... .......... ......... .. b ... ... .......... ...... ... v3 • ◦....... ............................................... ◦ ◦....... ...............................................◦.. v4 ◦....... ◦................................................ ◦....... ◦............................................. ◦....... • .... .. ......... ........ ... v2 .... .. ......... ....... ... v5 .. ... ... ... ... ... ... .. ......... ......... .. .... .. ......... ........ ... a .... .. ......... ....... ... v2 • ◦....... ...............................................◦... v5 • ◦.................................................. ◦....... • .... .. .......... ...... ... a .... .. .......... ........ ... v4 Abbildung 13.4: Adjazenliste eines Digraphen (vertex list), auf die Kantenliste (edge list) und auf die Bogenliste (arc list). Die Knotenliste besteht aus Sätzen vom Typ VERTEX. Die Kantenliste und die Bogenliste bestehen beide aus Sätzen vom Typ EDGE. Ein satzinterner Indikator gibt an, ob es sich um eine ungerichtete Kante oder einen gerichteten Bogen handelt. Von jedem Knotensatz gibt es drei Verweise auf Listen von Inzidenzsätzen (Satztyp INC). Es gibt je eine Inzidenzliste (incidence list) für die Kanten, mit denen der Knoten inzidiert, eine für die vom Knoten ausgehenden Bögen und eine für die im Knoten ankommenden 368 KAPITEL 13. DARSTELLUNGEN VON GRAPHEN ... ... B .........r ..r..................................... ........r ..r.................................. ........r r ......... ...... ... ... ... ... ... .. .......... ........ .. E .........r r ... ... ... ... .. ......... ....... ... h rr ... ... ... ... .. .......... ........ .. .......... ...... .... F .........r r ... ... ... ... .. ....... . ........ ... rr r r ....r g r r ....r h r r ....r i r r ....r j r r r k .. ... ... .... .. . ......... ......... ......... ....... ... .. ......... ......... .. e ... ... ... ... ... C .........r ..r..................................... .........r ..r.................................. .........r ..r.................................. .........r ..r.................................. .........r ..r.................................. .........r r j f BC ... ... A .........r ..r..................................... ........r ..r.................................. ........r r h r r ....r ... . ... ... ... .. . .......... .......... ......... ....... ... .. k ......... ...... ... e AC f .. ...................................... .. ... .. ... ........ ...... ... ... ... ... ... .. .......... ....... ... r r ....r ... ... ... ... .. .. ....... . ....... . ......... ....... ... .. .......... ........ ... .......... ........ ... g rr .. .. ...................................... .. ................................... .. ... ... .. .. ... .. . ........ ......... ...... ......... ... .. .......... ........ ... e .......... ....... ... k .......... ....... ... f CD ... ... ... ... .. .. ......... ......... ......... ........ ... .. AB ... ... ... ... .. . .......... ......... ........ ........ ... ... j i DF ......... ...... ... ......... ......... .. .. .... .... .. .. . ......... ......... ......... ...... ... .. ... ... D r ..r..................................... ........r ..r.................................. ........r r g i CF ... ... ... ... .. .. ......... ......... ......... ........ ... .. ... ... ... ... ... ... .......... ........ .. ... ... ... ... ... ... .......... ......... .. ... ... ... ... ... ... .......... ........ .. ... ... ... ... ... ... .......... ........ .. ... ... ... ... ... ... .......... ......... .. ... ... ... ... ... ... ......... ........ .. CE Abbildung 13.5: Beispiel für eine Inzidenzliste Bögen. Jeder Inzidenzsatz zeigt auf die zugeordnete Kante, bzw. den zugeordneten Bogen. Sowohl die Liste der Kanten als auch die Liste der Bögen wird durch Sätze vom Typ EDGE realisiert. In jedem Fall zeigt der Satz auf zwei (nicht notwendigerweise verschiedene) Knoten. Bei Kanten sind das in irgendeiner Reihenfolge die beiden Endknoten. Bei Bögen zeigt der erste Verweis auf den Start- und der zweite Verweis auf den Zielknoten. Bei Inzidenzlisten ist der Aufwand für die Speicherung höherer als bei Adjazenzlisten, da die Kanten- und Bogenliste hinzukommen. Auch der Bearbeitungsaufwand, um von einem Knoten zu einem Nachbarn zu kommen, erhöht sich um den zusätzlichen Zugriff zum Eintrag in der Kantenliste bzw. Bogenliste. Die Tatsache, daß bei Inzidenzlistendarstellung die Kanten/Bögen eines Graphen explizit als eigene Objekte gegeben sind, ist jedoch ein Vorteil, der oft den Zusatzaufwand aufwiegt. Es können nicht nur Mehrfachkanten dargestellt werden, auch bei Graphen ohne Mehrfachkanten kann man die Kanten/Bögen mit Namen und weiteren Attributen, z. B. Gewichten, versehen. Wie bei der Adjazenzliste können Knotennamen und andere knotenspezifischen Daten 13.4. EXTERNE DARSTELLUNGEN UND BASISWERKZEUGE 369 GRAPH Bögen Kanten ? VERTEX ? ? ? INC INC INC ? ? ? EDGE EDGE EDGE Kanten Ausgangsbögen Eingangsbögen ? ? EDGE EDGE ? ? VERTEX VERTEX Endpunkt Endpunkt ? VERTEX ? VERTEX Startpunkt Zielpunkt Abbildung 13.6: Graphdarstellung durch Inzidenzlisten in den Einträgen der Knotenliste direkt gespeichert sein oder es kann auf sie verwiesen werden (Vertretersätze). Das gleiche gilt bei den Einträgen der Kantenliste/Bogenliste. Anmerkung 13.3 (Listenimplementierung) Die in den Listendarstellungen gegebenen Graphelemente werden in den meistens Algorithmen als Mengen benutzt: Für alle Knoten des Graphen tue .... oder Für alle Ausgangsbögen von Knoten v tue .... Für diesen Zweck kann jede Datenstruktur genommen werden, die zur Darstellung von Mengen geeignet ist, z. B. verkettete Listen. Diese hat den Vorteil, daß sie auch eine gelegentlich benötigte lineare Ordnung der Elemente wiedergibt. Es besteht jedoch des öfteren auch die Notwendigkeit, auf ein Element anhand seines Namens direkt zuzugreifen. Das wäre mit verketten Listen zu aufwendig. Als Datenstruktur, die allen Anforderungen genügt, bieten sich Baumstrukturen an. Die in Abschnitt 9.2, Seite 277 besprochenen Rot-SchwarzBäume sind sehr gut geeignet. 2 13.4 Externe Darstellungen und Basiswerkzeuge Externe Darstellungen Graphdarstellungen müssen auch in Dateien gespeichert werden können. Auch hier sind Listen sinnvoll. Als Beispiel soll wieder der Digraph in Abbildung 13.2, Seite 364, dienen. Tabelle 13.3 zeigt zwei externe Darstellungen in Form sequentieller Listen, die jeweils die Adjazenz- bzw. Inzidenzstruktur widerspiegeln. Diese Darstellungen sind in gedruckter 370 KAPITEL 13. DARSTELLUNGEN VON GRAPHEN a: v1, v5 b: a v1: v2 v2: v3, v4 v3: v3, b v4: v2, a v5: v4 *KNOTEN* a, b, v1, v2, v3, v4, v5 *BOEGEN* 1: (a, v1); 2: (v1, v2) 3: (v2, v4) 4: (v4, a) 5: (a, v5) 6: (v5, v4) 7: (v4, v2) 8: (v2, v3) 9: (v3, v3) 10: (v3, b) 11: (b, a) Tabelle 13.3: Beispiel für externe Darstellungen in Form sequentieller Listen Form auch für Menschen sinnvoll, da nicht für alle Zwecke, z. B. bei sehr umfangreichen Graphen, graphische Darstellungen ausreichen. Basiswerkzeuge Wenn Graphalgorithmen als Programme in einem Rechensystem ausgeführt werden sollen, sind einige Hilfsprogramme erforderlich, die oft nur wenig mit graphentheoretischen Eigenschaften zu tun haben. Auf diese Programme soll nicht näher eingegangen werden. Als Beispiele seien genannt: • Es werden Programme gebraucht, mit denen eine externe Graphdarstellung eingelesen und eine programminterne Darstellung (z. B. Adjazenzmatrix oder Inzidenzlisten) aufgebaut wird. Auch Programme, die eine interne Darstellung in eine externe Darstellung umwandeln und diese in einer Datei sichern, werden benötigt. Anmerkung: Nach dem Aufbau einer programminternen Darstellung aus einer externen Darstellung ist der Typ des Graphen bekannt, d. h. man weiß, ob es sich um einen ungerichteten Graphen, einen Digraphen oder um einen, in dem sowohl Kanten als auch Bögen auftreten, handelt. Auch die Existenz von Schlingen und Mehrfachkanten wird erkannt. Später angewandte Algorithmen können daher den Typ des Graphen als bekannt voraussetzen. • Programme, die elementare statistische Größen (Minimalgrad, Maximalgrad, mittlerer Grad u. ä) liefern, sind nützlich. Das gilt auch für Programme, mit denen sich 13.4. EXTERNE DARSTELLUNGEN UND BASISWERKZEUGE 371 Knoten- und Kantenmengen verwalten und Mengenoperationen darauf anwenden lassen. • Wichtig sind Programme, die aus einer Menge von Knoten oder Kanten den entsprechenden Untergraphen erzeugen. Auch Programme, die aus einem ungerichteten Graphen einen gerichteten erzeugen oder umgekehrt, gehören dazu. • Nützlich sind auch Programme, die Graphdarstellungen ineinander umwandeln. Aufgaben Aufgabe 13.1 Wie kann man anhand ihrer Adjazenzmatrizen feststellen, ob zwei schlichte Graphen isomorph sind? Aufgabe 13.2 Es werden die Graphen G, H und I aus Abbildung 13.1 betrachtet. Die Knotenmenge der Graphen ist {A,B,C,D,E}. Die Kanten sollen durch die Mengen ihrer Endpunkte charakterisiert werden. 1. Bilden Sie die Kantenmengen zum Nachweis, daß G = H 6= I. 2. Geben Sie alle Automorphismen von H an. 3. Benutzen Sie Satz 12.2 zur Bestimmung aller Isomorphismen von H auf I. Literatur Wie findet man automatisch, d. h. mit Hilfe von Rechnern, „gute“ graphische Darstellungen von Graphen? Das ist ein wichtiges Teilgebiet der algorithmischen Graphentheorie. Einen Einstieg bietet der Aufsatz von Brandenburg/Jünger/Mutzel [BranJM1997]. Weitere Einzelheiten kann man im Beitrag [EadeM1999] im Handbuch [Atal1999] finden. Rechnergerechte Darstellungen von Graphen mit zum Teil etwas anderer Bedeutung der Bezeichnungen Adjazenzliste und Inzidenzliste findet man zum Beispiel in Sedgewick [Sedg2002], Jungnickel ([Jung1994], Seiten 60 ff), Ahuja/Magnanti/Orlin ([AhujMO1993], Seiten 31 ff), Chartrand/Oellerman ([CharO1993], Seite 57 ff), Deo ([Deo1974], Seiten 270 ff), Evans/Minieka ([EvanM1992], Seiten 27 ff), Golumbic ([Golu1980], Seiten 31 ff), Noltemeier ([Nolt1976], Seite 38 ff), Nägler/Stopp ([NaglS1996], Seiten 32 ff), Skiena ([Skien1990], Seiten 81 ff). Es sei außerdem auf Aho/Hopcroft/Ullman [AhoHU1983] und Tarjan [Tarj1983] hingewiesen. Im System LEDA sind Datenstrukturen für Graphen sowie eine Reihe wichtiger Graphalgorithmen implmenentiert. Sie sind im dazugehörigen Handbuch [MehlN1999], das durchaus auch Lehrbuchcharakter hat, beschrieben. 372 KAPITEL 13. DARSTELLUNGEN VON GRAPHEN Eine interessante Sammlung von Graphen und Werkzeugen zu ihrer Beartbeitung ist in Knuth „The Stanford GraphBase“ [Knut1993] beschrieben. Kapitel 14 Wege und Zusammenhang 14.1 Wege: Definitionen und elementare Eigenschaften Als Struktureigenschaft eines Graphen sind Kanten ungerichtet und Bögen gerichtet. Als „normale“ Durchlaufsrichtung eines Bogens ist die vom Startpunkt zum Zielpunkt anzusehen. Es ist jedoch zweckmäßig als Bearbeitungseigenschaft eines Bogens auch Durchläufe in Rückwärtsrichtung zuzulassen. Das führt zu der folgenden Festlegung. Bearbeitungsmodi für Bögen eines Graphen: 1. vorwärts (forward) Bögen dürfen nur in Vorwärtsrichtung durchlaufen werden. 2. rückwärts (backward) Bögen dürfen nur in Rückwärtsrichtung durchlaufen werden. 3. beliebig (any) Bögen dürfen in beiden Richtungen durchlaufen werden. Kanten dürfen in jedem Modus in beiden Richtungen durchlaufen werden. 2 Wenn der Bearbeitungsmodus nicht explizit angegeben ist, ist ein beliebiger, aber fest gewählter Bearbeitungsmodus gemeint. Für ungerichtete Graphen fallen alle drei Modi zusammen. Der Bearbeitungsmodus rückwärts in einem Graphen mit Bögen entspricht in dem Graphen, den man durch Invertieren der Bogenrichtungen gewinnt, dem Bearbeitungsmodus vorwärts. In dem ungerichteten Graphen, den man durch Umwandlung der Bögen in Kanten gewinnt, hat man die gleichen Bearbeitungsmöglichkeiten wie im Originalgraphen mit dem Modus beliebig. Man beachte, daß der ungerichtete Graph auch dann Mehrfachkanten enthalten kann, wenn der Ausgangsgraph keine Mehrfachkanten und keine Mehrfachbögen enthält. 373 374 KAPITEL 14. WEGE UND ZUSAMMENHANG Wege sind in der Graphentheorie von besonderer Bedeutung. Es sind Folgen aneinandergrenzender Kanten und Bögen, die von dem Anfangspunkt des Weges zu seinem Endpunkt führen. Wir führen f-Wege (forward, d. h Bögen dürfen nur in Vorwärtsrichtung durchlaufen werden), b-Wege (backward) und a-Wege (any) ein. Wege haben immer eine eindeutige Richtung. Der Typ, nämlich a-Weg, f-Weg oder b-Weg, bestimmt dabei nur, wie Bögen auf einem Weg durchlaufen werden können. Definition 14.1 In einem allgemeinen Graphen G(V, E, A, ϕ, ψ) ist ein f-Weg (f-Pfad, f-path) eine endliche Folge v0 , l1 , v1 , l2 , v2 . . . , vn−1 , ln , vn mit (n ≥ 1). Die lν sind Kanten oder Bögen. Ist lν eine Kante, so ist vν−1 ein Inzidenzpunkt und vν der andere Inzidenzpunkt. Ist lν ein Bogen, so ist vν−1 sein Startpunkt und vν sein Zielpunkt. Entsprechend werden b-Wege und a-Wege definiert. v0 heißt Anfangspunkt (Anfangsknoten, starting vertex) und vn heißt Endpunkt (Endknoten, end vertex) des Weges, alle anderen Knoten des Weges heißen innere Knoten (internal vertex). n heißt Weglänge (path length). Ein Weg heißt geschlossen (closed), wenn v0 = vn , anderenfalls heißt er offen (open). Ein Weg heißt linieneinfach (line-simple), wenn seine Kanten und Bögen paarweise verschieden sind. Ein offener Weg heißt einfach (simple), wenn alle Knoten paarweise verschieden sind. Ein geschlossener Weg heißt einfach, wenn nur Anfangs- und Endpunkt gleich sind. Ein geschlossener einfacher und linieneinfacher Weg heißt Kreis (circuit). Ein Graph, in dem es keinen Kreis gibt, heißt kreisfrei (acyclic). Ein Lauf (run) ist eine unendliche Folge v0 , l1 , v1 , l2 , . . . von Knoten und Linien, für die jedes endliche Anfangsstück v0 , l1 , v1 , l2 , . . . , vn−1 , ln , vn ein Weg ist. Der Träger (support, carrier) eines Weges oder eines Laufes ist der Untergraph von G, der aus den durchlaufenen Knoten und Linien besteht. Im Graph G1 der Abbildung 12.1, Seite 348, ist D, g, C, j, F, j, C ein offener a-Weg, der nicht linieneinfach ist. C, f, B, h, A, e, C, j, F ist ein offener, linienneinfacher a-Weg, der nicht einfach ist. B, h, A, e, C, j, F, i, D ist ein einfacher offener a-Weg. Im gleichen Graph ist D, g, C, f, B, f, C, g, D ein geschlossener, nicht linieneinfacher a-Weg. C,g,D,i,F,j,C,e,A, h,B,f,C ist ein geschlossener, linieneinfacher a-Weg. A,e,C,f,B,h,A ist ein a-Kreis. Da G1 ungerichtet ist, sind alle a-Wege auch f-Wege. Ein weiteres Beispeil ist Graph Graph1 der Abbildung 14.1, Seite 377. In der Abbildung sind keine Kanten- und Bogennamen angegeben, um das Bild nicht zu überlasten. Diese sollen nach den in der Abbildung aufgeführten Regeln systematisch gebildet werden. In Graph1 ist K04, dK16K04, K16, dK17K16, K17, dK16K17a, K16, uK16K17a, K17, dK16K17b, K16, uK16K17b, K17 ein offener, linieneinfacher b-Weg (und auch a-Weg) von K04 nach K17. Der Weg ist nicht einfach und auch kein f-Weg. 14.1. WEGE: DEFINITIONEN UND ELEMENTARE EIGENSCHAFTEN 375 Anmerkung 14.1 In einem Graphen ohne Mehrfachlinien bestimmt die Folge der Knoten einen Weg eindeutig. Daher werden wir für solche Graphen Wege gelegentlich nur durch Knotenfolgen spezifizieren. 2 Anmerkung 14.2 Im folgenden werden wir nicht immer zwischen Weg und Träger des Weges unterscheiden. Wir werden z. B. von einem Untergraphen als Kreis sprechen. Aus dem Zusammenhang geht dann hervor, was für ein Kreis auf dem als Träger anzusehenden Untergraphen gemeint ist. 2 Wenn man einen offenen Weg von u nach v sucht, kann man sich auf linieneinfache oder einfache Wege beschränken, wie sich aus dem folgenden Hilfssatz ergibt. Hilfssatz 14.1 1. Jeder offene Weg von u nach v enthält einen linieneinfachen Weg von u nach v. 2. Jeder linieneinfache offene Weg von u nach v enthält einen einfachen Weg von u nach v. 3. Jeder einfache offene Weg ist linieneinfach. Beweis: Aufgabe 14.2. 2 Bei geschlossenen Wegen sind die Verhältnisse etwas komplizierter. Zunächst einmal gilt der folgende Hilfssatz. Hilfssatz 14.2 Jeder geschlossene Weg enthält einen geschlossenen einfachen Weg mit gleichem Anfangs- und Endpunkt. Beweis: Es sei v = v0 , l1 , v1 , . . . , vn−1 , ln , vn = v der geschlossene Weg. Kommt v als innerer Punkt vor, so betrachte man den geschlossenen Weg v = v0 , l1 , v1 , . . . , lj , vj = v mit dem kleinsten j. Dieser enthält v nicht als inneren Punkt. Enthält der neue Weg den von v verschiedenen Knoten u mehrfach, so reduziere man den Weg zu v = v0 , l1 , v1 , . . . , lν , vν = u, lµ , . . . , lj , vj = v, wobei ν der kleinste und µ der größte Index ist, bei dem u auftritt. Diesen Schritt wiederholt man so lange, bis ein geschlossener Weg übrigbleibt, in dem alle Knoten bis auf Anfangs- und Endpunkt paarweise verschieden sind. 2 Ein einfacher geschlossener Weg der Länge mindestens 2 kann keine Schlingen enthalten. Gibt es einfache geschlossene Wege, die nicht linieneinfach, also keine Kreise sind? Das beantwortet der nächste Hilfssatz. Hilfssatz 14.3 Ein geschlossener einfacher Weg, in dem Linien mehrfach auftreten, ist vom Typ v, l, u, l, v mit v 6= u. 376 KAPITEL 14. WEGE UND ZUSAMMENHANG Beweis: I. Ein Weg des angegebenen Typs kann als a-Weg auftreten. II: In jedem anderen geschlossen Weg, in dem eine Linie mehrfach auftritt, gibt es auch innere Knoten, die mehrfach auftreten oder gleich dem Anfangsknoten sind. In der Tat: Sei l eine mehrfach auftretende Linie in einem geschlossenen Weg. Die Linie muß mindestens zweimal auftreten und der Weg muß mindestens die Länge 2 haben. Ist l eine Schlinge, so tritt ihr Inzidenzpunkt auch als innerer Punkt auf. Es sei l keine Schlinge. Wenn der Weg nicht vom angegeben Typ ist, muß er mindestens die Länge 3 haben. Läßt man die letzte Linie des Weges weg, so erhält man einen verkürzten Weg der Länge mindestens 2. Dieser Weg muß offen sein, da sonst im ursprünglichen Weg der Anfangsknoten auch ein innerer Knoten wäre. Als einfacher offener Weg ist der verkürzte Weg auch linieneinfach. Eine mehrfach auftretende Linie im Ausgangsweg muß demnach die letzte Linie sein. Einer ihrer Inzidenzpunkte ist der Anfangsknoten des Weges. Der andere ist ein mindestens zweimal auftretender innerer Knoten des ursprünglichen Weges. 2 Der folgende Satz klärt den Zusammenhang zwischen einfachen geschlossenen Wegen und Kreisen. Satz 14.1 1. Jeder einfache geschlossene Weg der Länge 1 oder größer 2 ist ein Kreis. 2. In schlichten Graphen hat jeder Kreis mindestens die Länge 3. 3. In Digraphen ist jeder einfache geschlossene f-Weg (b-Weg) ein Kreis. Beweis: 1. Folgt aus Hilfssatz 14.3. 2. Folgt auch aus Hilfssatz 14.3, da schlichte Graphen keine Schlingen und keine Mehrfachkanten aufweisen. 3. In einem f-Weg (b-Weg) eines Digraphen kann eine Folge . . . , v, l, u, l, v, . . . mit v 6= u nicht auftreten. Die Behauptung folgt dann wieder aus Hilfssatz 14.3. 2 14.1. WEGE: DEFINITIONEN UND ELEMENTARE EIGENSCHAFTEN 377 ......................... ......................... ..... ..... ... ... ... ... ... ... ... ... .. ... . .... .... . ............... . ................ . . ................... . . . . . . . . . . ... . .... ... ...... .... ... ............................. . . . . .... ... . ................ . . . . . ..... ... ... ............ .............. . . ... . . . . . . . . . . . .. . . . . . . ..... ............ ..... ......... ........... ... ... . . . . . .. .. . . . ... ..... ... ... ... . .......... . . . ... . . . ..... ... ... . ... . . ..... . . . . . . . . . . . ..... ..... .. ..... .... ... . .. ............. .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ... ... .................. ... ..... .. .... .................. .................. ..... ......... . ... ... ....... ........... ..... ........... ..... ..... ......... .................... ... . ..... ..... .......................... ... .. ..... ...... .... . ... ..... ... .. ... ... ....... .... .... ..... ... ... ..... .. ... . . .... .. . . . . . . ... . . ..... ... ... ... .. ..... ... .. ...... . . . . . . . . . . . . . . ..... .. ..... ...... ... ... ... . . .. . . ... . . . . . . . . . . . . ... ..... .. ..... ... ... .. .... .. . . . .. . . . . ... . . . . . . . . . . . . . . . ..... ... ... ...... .... ... . .. ................... ...... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ... ..... ... ... ..................... ............. ..... . ............... .............. ..................... ................ ..... ... ... ....... . ... ................................. ... .. ... ..... ... ....................................... ... ... .... .... .. ... .. ..... .. ........................................ . ... ... ... ... ... ... ..... .... ... .................................................................... ... .... ... ... ... . . . . . . . . . . . . . . . . . . . . . . . . . . . . ... ....... .... ..... ... ... ............................................. .... .. . . . . . . . . . . . ....................... . . . . . . . . . . . ... ... ..... ... ........ ........................... . . . . ........ .............. .. . . . ..... . . . . . . . . . . . . . . . . . . . . . . . . . . . .... . . . ..... ... ... .... .. .......................................... ... . ........ ... . ... . . . . . . . . . . . . ..... ..... ... .... ... ... ... .. . .. .. ...... . . . . . . . . . .... . . . . . . .. . . . ... ..................................................................................................................................................................................................................................................................................................................................................................................................................................................................... . . ... ... ..... .. .. ..... ... . .. . ... .. ... . . . . . . . . . . . ..... ... .... ... ... ..... ... ... ... .. . . .. . . . . . . . . . . ..... . . . . . . . ... ..... ..... ... ... ...... ... .. . . . ....................... . . . . . .... . . . . . . . . . . . . . . . . . . ....... .. ..... ... ... ..... ... ... .. ... ... ..... ..... ... ... .... ..... ... ..... .... .... ... ... ... ... ..... ... ..... .. .. ... ... ... ... ..... ... ........ .. ... ... ... ..... ... ... . . . . . . . ... . . . . .... ... ..... ... ........... ... ... . . . . . . . . ..... ... ... ........... ... . . ... . . . . . . . . . . . .... .... ..... .... ... ... .. ....... .. . . . . . . . . . . . . . . . . . . . . . . . . . . ...... ... ..... ... ..... .... ........ ........... ..... ..... ... . . . . . . . . . . . . . . . . . . . . . ... ... ..... ..... ... . ... .... . ... .. . . . . ... . . ... . . . . . . . . . . ..... ... ... ... ..... . .. . ... . ... . .. . .... . . . . . . . . . . . ... ..... ..... ... ... .. ... ... .. .. .. ... ..... .... ... .... ... ... .... ..... .. .... ..... .. .. ... . .... . . . . . . . . . . . ..... ... ..... ... ... ... .. ... . . ... . . . . . . . . . . . . . . . ...... . . . . . . . . . ..... ....... ....... ... .... ..... ... ... . .. .................... ......... . . . . . . . . . . . . . . . . . . . . . . . ... ... ..... ..... . ... .... ..... ..... ......... ... ..... ..... ...... ... ... ... ... ..... ...... ..... ... ... ... ... ........ ... ..... ..... ... ... ... ... ......... ..... ... .. ... ..... ... ... ..... ... ......... .. .. . . . . . . . . . . . . . . . ..... ... ... ..... ..... ... .. ... ... ..... .... ..... ..... ... ... ... .. ... .. ..... ... ..... . . .... . . . . . . ... . . . . . ..... ..... ..... ... ... ..... ... .. . . . . . . . .. . . . . . . . . ... ....... ..... ..... ... ... . ..... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .................. .................. ..... ........ ... ... .. ... ... ..... ..... . . . . . ... . . . . . . . . . . . . . . . ... .. ... ..... .... . ... ... ... ... . .. . . . . . . . . . . . . . . . ... ... .... ..... .. .... .. . .. .. .. .. .... .............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................. . ... ..... . ... . . . ... ..... ... ... ... .. . .... ..... ... . . . . . . . . . . ... ..... ... ..... ... ... ... ... .... . .... . . . . . . . . . . . . . . . . . . . . . . . . . ....... ....... ... ... ..... ..... ... .. ... ....... ........................ . . . . . . ........... . . . . . . . . . . . . . . . . . ..... ... ..... ... ... ... ... ... .. .. ..... .. ... ... ..... ....................... ... .. ......... ... ..... .... .... . ... ... ...... ... ..... ...... .. .................. ... ... .. .... ... ..... .. ............ ... ..... .... ... ..... .. ..... .. ........... . . . . . . . . . . . . . . . . . . . . . . . . ... ...... ..... ..... .. .. ... ........ ..... ..... ... .. ... .. .. ... .... ... ............ ..... ..... ... .. ... ... ......... ............ ... ... ..... ..... ..... ... .. ... ............ ....... .. .... ..... .. ... ........ ... ... ... ....................... ... ..... ............. .. ... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ...... . . . . . . ............... ............... .. ...... .. ..... ... ... .... ..... ... .. ... ... ... ... .... ... ... ..... ... .... ............................. ... .. ... ... ... ..... .. ... ...... .. ... ........ ... . . . . . .. ..... . ... . . . . . . .. .. .. . . . . ... ... ... ... ... ..... . .................. ... . . ... . . . . . . . . . ... ..... ... ... ... ... .. . ... ... .............. . . . . . . . . . . . . . . ... ... ..... ... ... ... .... .... ... ...... ... . . . . . . . . . . . . . . . . . . . . . . . . ....... . . . . ..... ... ........ ........ ... ... ... ... . ....... . . . .................... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ..... ... ... ... ... ... .. ............... ... .... ... ... ... ... ... ... ... ... ............. ... ... ... ... ... ... .. ... ............ .......... ... ... .. .... .. ........... ... ..... ... ... ... ... .. ............ . . . . . . . . . . ... . ... . . . . . . . . . . . . . . ..... .... ... ... ... .. ... ..... ............ ... ... ....... ... ... ... ..... . .............. ............ ... .. ... ... .... ..... .......... ............ ..... ............... .... .. ... ... ..... ........................ ... .... ............ ......... .... ... ... ..... .... .... ....... ........... . . .... . . . . . . . . . .... . . . . . . . . . . . . . ..... . ... ... ... ... ... ... ...... . . ...... . ... . . .... . .. ........................... . ... ... ... ... .............. .... ... ... ... ......... . ... ... . . ... . .. .. ......... .. .. . . . . . . . ... ... ... ... ... ..... . .. . ... . . . . . . . . . . . . . ... ..... ... .... ... .... ... . . . .. . . . ........ ........... . . . . . . . . ..... ... ...... .... ... ... ... ........ ..... ... ... ... .... ........... ... ... ..... ... ... ... .... ... ..... ... ... ....... ... ... . ..... ... ... ..... ....... ... ... ............ ..... .... . . .. . . . ... . . . . . . .... . . . ..... ...... . ... ... .. ..... ... ... . ... ... ........................ ..... .... ...................................... .... ... ... ...... ..... ... .............. ... .... .. ..... ... ... ... ... .. ... ..... ................. ..... ... .... ... ... .... ..... ..... . .. .. ... ..... . . ... ... .. ... ..... .. .. ... ... ................... .. ... ..... ... ...................... ... .... ... ..... .... ... ........ ....... ....... ... ..... ........ ........... .. .................. ............ . . . . . . . . . . . ..... .. ... ...... ... ...... .. ....................... ................................................ .. . . .... ... . ... .............. ....................... .. ... ... ... ..... .. .. .. . .... . ... .. ... . . .. . .. . . . . . . . . ... ... .. . .... .... .......................................................................................................... . . . . . . . . . ... .... .... ....... ... ........... . . .... . . ........................ . . . . . . . . . .... . . . . . . . . . ..... ... .... ........... .................................................. ... . ......... .......... . . . . . . . . . . . . . . . . . . . . . . . . . . ......... ........... .................................................. . ........ ....... ... . . ... ... ..... ..... .. .. ........ . ... ..... ........ .. ... ................................................... ........ .... ........ .... ... . . . ....... . . . . . . . . . . . ................................................................................................. .. K06 K05 K04 K07 K03 K08 K02 K09 K01 K10 K00 K11 K21 K12 K20 K13 K19 K14 K18 K15 K16 K17 Die Kanten- und Bogennamen sollen systematisch vergeben sein, und zwar nach den folgen Regeln: Eine Kante beginnt mit u, ein Bogen mit d. Dann folgen die beiden Knotennamen, bei Bögen in der Richtungsreihenfolge. Mehrfachkanten/-bögen werden durch nachgestellte Buchstaben a, b, c, . . . unterschieden. So inzidieren z. B. mit dem Knoten K16 die folgenden Kanten/Bögen: dK16K04, dK16K17a, dK16K17b, dK17K16, uK16K17a, uK16K17b. Mit K14 inzidieren: uK14K14a, uK14K14b, dK14K14, dK14K15. Abbildung 14.1: Graph1 378 14.2 KAPITEL 14. WEGE UND ZUSAMMENHANG Erreichbarkeit und Zusammenhang Definition 14.2 1. In einem allgemeinen Graphen G(V, E, A, ϕ, ψ) heißt ein Knoten v von einem Knoten u f-erreichbar (f-reachable), wenn es einen f-Weg mit Anfangspunkt u und Endpunkt v gibt. Entsprechend wird b-Erreichbarkeit und a-Erreichbarkeit definiert. 2. In einem allgemeinen Graphen G(V, E, A, ϕ, ψ) heißen Knoten u und v gegenseitig f-erreichbar (mutually f-reachable), wenn es einen f-Weg von u nach v und einen f-Weg von v nach u gibt. Entsprechend wird gegenseitige b-Erreichbarkeit und gegenseitige a-Erreichbarkeit definiert. Da Wege der Länge 0 nicht zugelassen sind, kann man von einem isolierten Knoten keinen Knoten erreichen und er ist von keinem Knoten erreichbar. Betrachtet man nur f-Wege, so gibt es auch nicht-isolierte Knoten, die von keinem Knoten erreichbar sind. Z. B. ist Knoten K10 in Graph1, Seite 377, nicht f-erreichbar. Ein Knoten, der von keinem Knoten auf einem f-Weg erreicht werden kann, soll f-Startknoten (f-starting vertex) genannt werden. Ein Knoten, von dem kein Knoten auf einem f-Weg erreichbar ist, soll f-Stoppknoten (f-stopping vertex) heißen. Ein isolierter Knoten ist Start- und Stoppknoten zugleich. Ein Knoten, der von sich selbst f-erreichbar ist, soll Knoten mit f-Rückkehr (vertex of return) heißen. Alle anderen Knoten heißen Knoten ohne f-Rückkehr. Ein Knoten mit f-Rückkehr kann weder Start- noch Stoppknoten sein. Für a-Wege gelten die entsprechenden Definitionen. Da von jedem nicht-isolierten Knoten ein a-Weg zum Knoten zurückführt, wollen wir die Bezeichnungen Startknoten, Stoppknoten, Knoten mit Rückkehr und Knoten ohne Rückkehr nur für f-Wege benutzen und den Zusatz f- fortlassen. Ein Knoten u ist genau dann Knoten mit Rückkehr, wenn es einen Knoten v gibt, so daß u und v gegenseitig f-erreichbar sind. Erreichbarkeit ist offensichtlich transitiv. a-Erreichbarkeit ist eine Oberrelation von fErreichbarkeit und von b-Erreichbarkeit. b-Erreichbarkeit ist die inverse Relation zu fErreichbarkeit: Ein f-Weg von u nach v ist ein b-Weg von v nach u. Gegenseitige fErreichbarkeit ist der (eventuell leere) symmetrische Kern von f-Erreichbarkeit und auch von b-Erreichbarkeit und damit gleich gegenseitiger b-Erreichbarkeit. In beliebigen Graphen fallen a-Erreichbarkeit und gegenseitige a-Erreichbarkeit zusammen und jeder nichtisolierte Knoten ist ein Knoten mit a-Rückkehr. In ungerichteten Graphen ist in allen Bearbeitungsmodi Erreichbarkeit gleich gegenseitiger Erreichbarkeit und jeder nicht-isolierte Knoten ist ein Knoten mit Rückkehr. Zu Relationen siehe Abschnitt A.5, Seite 616, im Anhang. Damit ergibt sich der folgende Satz. Satz 14.2 1. f-Erreichbarkeit (a-Erreichbarkeit) ist eine transitive Relation auf der Menge der Knoten. 14.2. ERREICHBARKEIT UND ZUSAMMENHANG 379 2. Gegenseitige f-Erreichbarkeit (a-Erreichbarkeit) ist eine symmetrische Relation auf der Menge der Knoten. 3. Gegenseitige f-Erreichbarkeit (a-Erreichbarkeit) ist eine Äquivalenzrelation auf der Menge der Knoten mit Rückkehr (der Menge der nicht-isolierten Knoten). Als Äquivalenzrelation zerlegt gegenseitige f-Erreichbarkeit (a-Erreichbarkeit) die Menge der Knoten mit Rückkehr (die Menge der nicht-isolierten Knoten) in disjunkte Aquivalenzklassen. Das führt zu der folgenden Definition. Definition 14.3 Es sei G(V, E, A, ϕ, ψ) ein allgemeiner Graph. 1. Die durch die Äquivalenzklassen gegenseitig f-erreichbarer Knoten erzeugten Untergraphen heißen starke Zusammenhangskomponenten (strongly connected components, strong components) von G. 2. Die durch die Äquivalenzklassen gegenseitig a-erreichbarer Knoten erzeugten Untergraphen heißen schwache Zusammenhangskomponenten (weakly connected components, weak components) von G. 3. Besteht ein Graph aus einer einzigen starken/schwachen Zusammenhangskomponente, so heißt er stark/schwach zusammenhängend (strongly/weakly connected). Anmerkung 14.3 Bei ungerichteten Graphen fallen schwache und starke Zusammenhangskomponenten zusammen und man spricht nur von Zusammenhangskomponenten (connected components). 2 Da gegenseitige b-Erreichbarkeit das gleiche wie gegenseitige f-Erreichbarkeit ist, erzeugt auch gegenseitige b-Erreichbarkeit die starken Zusammenhangskomponenten eines Graphen. Jede starke Zusammenhangskomponente ist Untergraph einer schwachen Zusammenhangskomponente. Isolierte Knoten gehören zu keiner schwachen Zusammenhangskomponente. Die nur aus einem isolierten Knoten bestehenden Untergraphen sollen uneigentliche schwache Zusammenhangskomponenten (improper weakly connected component) heißen. Jeder nicht-isolierte Knoten und jede Linie gehört zu einer eindeutig bestimmten schwachen Zusammenhangskomponente. Jede Kante und jede Schlinge gehört zu einer eindeutig bestimmten starken Zusammenhangskomponente. Wie sehen nun allgemeine Graphen aus, die keine starken Zusammenhangskomponenten aufweisen? Eine Antwort gibt der folgende Satz. Satz 14.3 Ein allgemeiner Graph hat genau dann keine starken Zusammenhangskomponenten, wenn er ein f-kreisfreier Digraph ist. 380 KAPITEL 14. WEGE UND ZUSAMMENHANG Beweis: Enthält der Graph keine starken Zusammenhangskomponenten, so hat er keine Kanten und ist ein Digraph. Enthielte er einen f-Kreis, so gäbe es auch einen Knoten mit Rückkehr und somit eine starke Zusammenhangskomponente. Ist der Graph ein Digraph und enthält er keinen f-Kreis, so enthält er nach Satz 14.1, Nummer 3, keinen einfachen geschlossenen f-Weg und somit nach Hilfssatz 14.2 keinen geschlossen f-Weg. Dann enthält er auch keine starken Zusammenhangskomponenten. 2 f-Kreisfreiheit läßt sich durch b-Kreisfreiheit ersetzen. Ein Beispiel für einen Graphen ohne starke Zusammenhangskomponenten ist der durch die Knoten K00, K06, K11, K20 und K21 erzeugte Untergraph von Graph1, Seite 377. Satz 14.3 rechtfertigt die folgende Definition. Definition 14.4 Ein allgemeiner Graph ohne starke Zusammenhangskomponenten heißt Dag (directed acyclic graph). Wegen der Bedeutung dieser Digraphen in der Planungstheorie werden sie auch Präzedenzgraphen (precedence graph) genannt. In einer schwachen Zusammenhangskomponente, die starke Zusammenhangskomponenten enthält, kann es Bögen geben, die zu keiner starken Zusammenhangskomponente gehören. Der von diesen Bögen aufgespannte Untergraph ist ein Dag und soll externer Dag (external dag) der schwachen Zusammenhangskomponenten heißen. Er ist nicht notwendigerweise schwach zusammenhängend und kann ganz fehlen. Da in schwachen Zusammenhangskomponenten alle Knoten gegenseitig a-erreichbar sind, muß in jeder schwachen Zusammenhangskomponente mit mehr als einer starken Zusammenhangskomponente jede von diesen Knoten aufweisen, die auch zum externen Dag gehören. Sie sollen schwache Verheftungspunkte (weak attachment point) heißen. Knoten einer starken Zusammenhangskomponente, die keine schwachen Verheftungspunkte sind, sollen innere Knoten (internal vertices) genannt werden. Es ist möglich, daß im externen Dag beide Knoten, mit denen ein Bogen inzidiert, schwache Verheftungspunkte sind. Eine starke Zusammenhangskomponente kann mehrere schwache Verheftungspunkte haben, ein Knoten kann jedoch höchstens für eine starke Zusammenhangskomponente schwacher Verheftungspunkt sein. In einem externen Dag treten nur Bögen auf. Bögen kann es aber natürlich auch in einer starken Zusammenhangskomponente geben. Dann enthält diese nach der folgenden Proposition auch einen f-Kreis. Proposition 14.1 Enthält eine starke Zusammenhangskomponente einen Bogen, so liegt dieser auf einem f-Kreis. Beweis: Aufgabe 14.3. Aus Proposition 14.1 ergibt sich der folgende Satz. 2 Satz 14.4 Eine starke Zusammenhangskomponente ist genau dann f-kreisfrei, wenn sie ungerichtet und a-kreisfrei ist. 14.3. BRÜCKEN UND SCHNITTPUNKTE 381 Beweis: Es liege eine f-kreisfreie starke Zusammenhangskomponente vor. Sie muß ungerichtet sein, denn andernfalls wäre sie nach Proposition 14.1 nicht f-kreisfrei. Jeder f-kreisfreie ungerichtete Graph ist auch a-kreisfrei. Ist die starke Zusammenhangskomponente ungerichtet und a-kreisfrei, so ist sie auch fkreisfrei. 2 Proposition 14.2 Eine Kante liegt genau dann auf einem f-Kreis, wenn sie auf einem a-Kreis liegt, der ganz in der zugehörigen starken Zusammenhangskomponente verläuft. Beweis: Ein f-Kreis verläuft stets innerhalb einer starken Zusammenhangskomponente. Eine Kante auf einem f-Kreis liegt also auf einem a-Kreis, der innerhalb der zugehörigen starken Zusammenhangskomponente verläuft. Eine Kante auf einem a-Kreis, der ganz in der zugehörigen starken Zusammenhangskomponente verläuft, läßt sich nach Proposition 14.9, die in Abschnitt 14.5 bewiesen wird, zu einem Bogen machen, ohne den starken Zusammenhang zu zerstören. Nach Proposition 14.1 geht dann auch ein f-Kreis durch die Kante. 2 Weitere Eigenschaften von schwachen und starken Zusammenhangskomponenten werden in Abschnitt 14.6 behandelt. 14.3 Brücken und Schnittpunkte Schwacher Zusammenhang liefert eine erste Charakterisierung bestimmter Linien und Knoten. Eine Linie eines allgemeinen Graphen heißt Brücke (bridge), wenn ihre Entfernung die Zahl der schwachen (eigentlichen oder uneigentlichen) Zusammenhangskomponenten erhöht. Ein Knoten eines allgemeinen Graphen heißt Schnittpunkt (Artikulationspunkt, cut point, articulation point), wenn seine Entfernung zusammen mit allen mit ihm inzidenten Linien die Zahl der schwachen (eigentlichen oder uneigentlichen) Zusammenhangskomponenten erhöht. Eine Brücke/ein Schnittpunkt trennt (separate) die neu entstandenen schwachen Zusammenhangskomponenten. Es ist im Zusammenhang mit Brücken nützlich, die Bezeichnung [x] einzuführen. Für einen Knoten x eines allgemeinen Graphen wird mit [x] die eigentliche oder uneigentliche schwache Zusammenhangskomponente bezeichnet, zu der x gehört Hilfssatz 14.4 Eine Linie l eines allgemeinen Graphen G ist genau dann eine Brücke, wenn für ihre Inzidenzpunkte a und b in G − l gilt [a] 6= [b]. Beweis: Alle Knoten x, die in G weder von a noch von b erreichbar sind, erzeugen in G die gleichen Zusammenhangskomponenten [x] wie in G − l. Ist l keine Brücke, also die Zahl der schwachen Zusammenhangskomponenten in G und G − l gleich, so muß [a] = [b] 382 KAPITEL 14. WEGE UND ZUSAMMENHANG gelten. Ist l eine Brücke, also die Zahl der Zusammenhangskomponenten in G kleiner als in G − l, so muß [a] 6= [b] gelten. 2 Die folgende Propositionen charakterisieren Brücken durch a-Kreise und Schnittpunkte durch a-Wege. Proposition 14.3 Eine Linie l eines allgemeinen Graphen G ist genau dann eine Brücke, wenn sie auf keinem a-Kreis liegt. Beweis: Es seien a und b die Inzidenzpunkte von l. Liegt l auf einem a-Kreis, so ist [a] = [b] in G − l. Nach Hilfssatz 14.4 ist l dann keine Brücke. Liegt l auf keinem a-Kreis, so gibt es in G−l keinen a-Weg von [a] nach [b]. Es ist [a] 6= [b] und – wieder nach Hilfssatz 14.4 – l ist eine Brücke. 2 Proposition 14.4 Ein Knoten v eines allgemeinen Graphen ist genau dann ein Schnittpunkt, wenn es von v verschiedene Knoten u und w mit u 6= w gibt, so daß jeder a-Weg von u nach w über v führt und es mindestens einen solchen Weg gibt. Beweis: Es sei v ein Knoten des allgemeinen Graphen G und [v] die (eigentliche oder uneigentliche) Zusammenhangskomponente, die v enthält. Beim Übergang von G zu G−v bleiben alle schwachen Zusammenhangskomponenten, die v nicht enthalten, unverändert. Ist v ein Schnittpunkt, so gibt es in G − v schwache Zusammenhangskomponenten C1 und C2 , die vorher nicht existierten und sich aus [v] ergeben haben müssen. Es sei u ∈ C1 und w ∈ C2 . Es gibt in G einen innerhalb von [v] verlaufenden a-Weg von u nach v. In G − v gibt es keinen Weg von u nach w. D. h. in G muß jeder Weg von u nach w über v verlaufen. Sind in G Knoten u und w durch einen Weg verbunden und führt jeder Wege zwischen ihnen über v, so gibt es G − v keinen Weg von u nach w. Die Knoten gehören zu verschieden schwachen Zusammenhangskomponenten. Die Zahl der schwachen Zusammenhangskomponenten hat sich erhöht. 2 In Kapitel 16 „Die Biblockzerlegung“ wird genauer auf Schnittpunkte und ihre Bedeutung bei der Zerlegung von Graphen eingegangen. 14.4 14.4.1 Kreisfreiheit und Bäume Kreisfreiheit und Zusammenhang Wegen ihrer Wichtigkeit seien die beiden folgenden Aussagen, die unmittelbar aus den Definitionen folgen, noch einmal besonders betont: • Starker Zusammenhang impliziert schwachen Zusammenhang. 14.4. KREISFREIHEIT UND BÄUME 383 • a-Kreisfreiheit impliziert f-Kreisfreiheit. Wir wollen nun a-Kreisfreiheit und schwachen Zusammenhang in allgemeinen Graphen betrachten. Ein a-kreisfreier allgemeiner Graph kann keine Mehrfachlinien enthalten. Wenn einem Graphen bei fester Knotenmenge immer neue Linien hinzugefügt werden, wird er irgendwann einmal einen a-Kreis enthalten. Nehmen wir einem Graphen immer mehr Linien weg, wird er irgendwann einmal nicht mehr schwach zusammenhängend sein. Die beiden folgenden Propositionen geben zu diesen Überlegungen Abschätzungen. Proposition 14.5 Jeder schwach zusammenhängende allgemeine Graph G(V, E, A, ϕ, ψ) enthält mindestens |V | − 1 Linien. Beweis: Durch Induktion. Für |V | = 1 und |V | = 2 ist die Behauptung richtig. Es sei |V | ≥ 3. Weiter sei v ∈ V und U = V \{v}. H sei der von U erzeugte Untergraph von G. H zerfällt in die (eigentlichen oder uneigentlichen) schwachen Zusammenhangskomponenten C1 , C2 , . . . , Ck . Die Anzahl Knoten in Ci sei ni . Es ist n1 + n2 + · · · + nk = |V | − 1. Nach Induktionsvoraussetzung enthält Ci mindestens ni −1 Linien. Außerdem muß wegen des schwachen Zusammenhangs von G der Knoten v mit jedem Ci durch mindestens eine Linie verbunden sein. Also hat G mindestens (n1 − 1) + (n2 − 1) + · · · + (nk − 1) + k = |V | − 1 Linien. 2 Proposition 14.6 Jeder a-kreisfreie allgemeine Graph G(V, E, A, ϕ, ψ) enthält höchstens |V | − 1 Linien. Beweis: Durch Induktion. Für |V | = 1 und |V | = 2 ist die Behauptung richtig. Es sei G a-kreisfrei und |V | ≥ 3. Ist E ∪ A = ∅, so ist die Behauptung richtig. Andernfalls sei l ∈ E ∪ A und H der durch (E ∪ A) \ {l} erzeugte Untergraph von G. Wegen der a-Kreisfreiheit von G kann l keine Schlinge sein und in H müssen die beiden Knoten, mit denen l inzidiert, zu verschiedenen (eigentlichen oder uneigentlichen) schwachen Zusammenhangskomponenten gehören (Proposition 14.3). D.h. H zerfällt in mindestens zwei schwache Zusammenhangskomponenten und jede hat weniger als |V | Knoten. Nach Induktionsvoraussetzung gilt dann, daß die Anzahl Linien höchstens (n1 − 1) + (n2 − 1) + · · · + (nk − 1) + 1 = n1 + n2 + · · · nk − k + 1 ≤ n − 1 beträgt. 2 384 14.4.2 KAPITEL 14. WEGE UND ZUSAMMENHANG a-Bäume Definition 14.5 Ein schwach zusammenhängender, a-kreisfreier allgemeiner Graph wird a-Baum (a-tree) genannt. Ein allgemeiner Graph wird a-Wald (a-forest) genannt, wenn seine schwachen Zusammenhangskomponenten a-Bäume sind. Anmerkung 14.4 Für a-Bäume, die ungerichtete Graphen sind, ist in der Literatur auch die Bezeichnung freier Baum (free tree) üblich. Auch wir wollen ungerichtete Graphen dieser Art so nennen. 2 a-Bäume haben eine Reihe wichtiger Eigenschaften, die alle charakteristisch sind, also auch zur Definition herangezogen werden könnten. Dazu der folgende Satz. Satz 14.5 Es sei G(V, E, ϕ, ψ) ein allgemeiner Graph. Dann sind die folgenden Aussagen gleichwertig. 1. G ist ein a-Baum. 2. G ist schlingenfrei und zwei beliebige verschiedene Knoten sind durch einen eindeutig bestimmten einfachen a-Weg verbunden. 3. G ist schlingenfrei und schwach zusammenhängend. Die Entfernung einer Linie führt jedoch zu einem unzusammenhängenden Graphen. 4. G ist schwach zusammenhängend und hat |V | − 1 Linien. 5. G ist a-kreisfrei und hat |V | − 1 Linien. 6. G ist a-kreisfrei. Das Hinzufügen einer Linie führt jedoch zu einem a-Kreis. Beweis: 1. ⇒ 2. Die Aussage ist richtig, wenn G nur aus einem isolierten Knoten besteht. Es sei |V | ≥ 2. Schlingenfreiheit folgt aus der Kreisfreiheit und die Existenz eines einfachen a-Weges zwischen je zwei verschiedenen Knoten aus dem schwachen Zusammenhang. Bleibt die Eindeutigkeit eines solchen Weges zu zeigen. Dazu soll angenommen werden, daß es von u nach v mit u 6= v zwei verschiedene einfache a-Wege gibt. u = v0 , l1 , v1 , . . . , li , vi , li+1 , vi+1 , . . . , vj−1 , lj , vj , . . . , vn = v 0 0 0 , lk0 , vk0 = vj , . . . , vn = v , . . . , vk−1 , vi+1 u = v0 , l1 , v1 , . . . , li , vi , li+1 0 Bis einschließlich vi seien die Wege identisch, vi+1 und vi+1 seien die ersten verschiede0 nen Knoten. vj = vk sei der erste Knoten an dem die Wege sich wieder treffen. Dann 0 ist vi , li+1 , . . . , lj , vj = vk0 , lk0 , . . . , li+1 , vi ein a-Kreis im Widerspruch zu angenommenen Kreisfreiheit. 14.4. KREISFREIHEIT UND BÄUME 385 2. ⇒ 3. G ist schlingenfrei. Wenn zwischen je zwei verschiedenen Knoten ein eindeutig bestimmter einfacher a- Weg existiert, ist G schwach zusammenhängend. Es sei l eine Linie von G. Es seien u und v die Knoten, mit denen l inzidiert. Da l keine Schlinge ist, gilt u 6= v. Der einzige einfache a-Weg von u nach v ist u, l, v. Wird l entfernt, so gibt es keinen einfach a-Weg mehr von u nach v und damit überhaupt keinen a-Weg. Der neue Graph ist nicht mehr schwach zusammenhängend. 3. ⇒ 4. G ist schwach zusammenhängend und daher gilt nach Proposition 14.5 |E|+|A| ≥ |V | −1. Durch Induktion soll gezeigt werden, daß auch |E| + |A| ≤ |V | −1 gilt. Ist |V | = 1, so gilt E = 0, da es keine Schlingen gibt. Es sei |V | = n > 1 und die Behauptung richtig für alle Graphen G0 = (V 0 , E 0 , A0 ) mit |V 0 | < n. Wir entfernen eine Linie l. Der neue Graph hat mehr als eine (eigentliche oder uneigentliche) schwache Zusammenhangskomponente und jede schwache Zusammenhangskomponente weniger als n Knoten. Jede schwache Zusammenhangskomponente erfüllt 3., denn sonst würde die Bedingung auch von G nicht erfüllt werden. Nach Induktionsvoraussetzung gilt dann für die Summe der Linien der schwachen Zusammenhangskomponenten |E1 | + |A1 | + |E2 | + |A2 | + · · · |Ek | + |Ak | ≤ n−k ≤ n−2. Fügt man die entfernte Linie wieder hinzu, so gilt für den Ausgangsgraphen |E| + |A| ≤ n − 1. 4. ⇒ 5. Angenommen, es gäbe in G einen a-Kreis. Sein Träger ist ein Untergraph H0 mit k Knoten und k Linien (k ≥ 1). Wegen |E| + |A| = |V | − 1 müssen noch Knoten übrig sein und wegen des schwachen Zusammenhangs muß es darunter einen geben, der durch eine Linie mit einem Knoten von H0 verbunden ist. Wir erweitern H0 zu H1 , indem wir den Knoten und die Linie hinzufügen. Das wird so lange durchgeführt, bis alle Knoten verbraucht sind. Schließlich ergibt sich ein Untergraph von G, der |V | Linien und damit mehr als G hat, und das ist ein Widerspruch. 5. ⇒ 6. G bestehe aus k schwachen Zusammenhangskomponenten. Alle sind a-kreisfrei und damit a-Bäume. Für jede schwache Zusammenhangskomponente gilt also auch 5., d. h. die Summe der Anzahl Linien muß |V | − k sein und das bedeutet k = 1, G muß schwach zusammenhängend sein. Dann gibt es zwischen je zwei verschiedenen Knoten ein einfachen a-Weg und das Hinzufügen einer Linie, die diese Knoten verbindet schließt einen a-Kreis. Ist die hinzugefügte Linie eine Schlinge, geht die Kreisfreiheit auch verloren. 6. ⇒ 1. Es ist zu zeigen, daß G schwach zusammenhängend ist. Gäbe es mehr als eine schwache Zusammenhangskomponente, so wähle man die Knoten u und v aus verschiedenen Komponenten und verbinde sie durch eine Linie. Es müßte jetzt a-Kreise in G geben und die müßten natürlich durch die hinzugefügte Linie gehen. Da die neue Linie die einzige Verbindung zwischen den schwachen Zusammenhangskomponenten ist, müßte sie bei jedem geschlossenen Weg mindestens zweimal durchlaufen werden und dieser könnte kein a-Kreis sein. 2 386 14.4.3 KAPITEL 14. WEGE UND ZUSAMMENHANG f-Bäume In diesem Abschnitt soll der Begriff „Baum“ auf den Bearbeitungsmodus vorwärts übertragen werden. a-Bäume wurden als schwach zusammenhängende und a-kreisfreie allgemeine Graphen eingeführt. Was sollen nun f-Bäume sein? Sie sollten zugleich auch aBäume sein. Dann sind sie natürlich auch f-kreisfrei. Starken Zusammenhang kann man jedoch nicht fordern, denn das würde bei Graphen mit Bögen die f-Kreisfreiheit zerstören. Das folgt aus Proposition 14.1. Wenn es schon nicht möglich ist, jeden Knoten eines f-Baumes von jedem anderen Knoten über einen f-Weg zu erreichen, dann sollte es aber mindestens einen Knoten geben, von dem jeder andere f-erreichbar ist. Daher wollen wir einen allgemeinen Graphen einen f-Baum (f-tree) nennen, wenn er ein a-Baum ist und es einen Knoten gibt, von dem alle anderen f-erreichbar sind. Diese Knoten sollen Wurzelknoten (root vertex) heißen. Ganz entsprechend werden für den Bearbeitungsmodus rückwärts b-Bäume (b-tree) 1 definiert. Für f-Bäume (b-Bäume), die Digraphen sind, gilt die folgende Proposition. Proposition 14.7 1. Ein Digraph, der ein f-Baum ist, besitzt genau einen Knoten, von dem alle anderen Knoten f-erreichbar sind. Dieser hat keinen Vorgänger, alle anderen haben genau einen Vorgänger. 2. Ein Digraph, der ein b-Baum ist, besitzt genau einen Knoten, der von allen anderen Knoten f-erreichbar ist. Dieser hat keinen Nachfolger, alle anderen haben genau einen Nachfolger. Beweis: Gäbe es zwei verschiedene Knoten, von denen jeweils alle anderen f-erreichbar sind, so wären sie gegenseitig f-erreichbar. Damit wäre der Digraph nicht f-kreisfrei, also auch nicht a-kreisfrei und somit kein f-Baum. Sei w der so eindeutig bestimmte Knoten. Aus den gleichen Gründen kann w keinen Vorgänger haben. Alle anderen Knoten sind von ihm f-erreichbar, müssen also Vorgänger aufweisen. Hätte ein Knoten zwei Vorgänger, so gäbe es von w zu ihm zwei verschiedene einfache f-Wege, also auch zwei verschiedene einfache a-Wege und der Graph wäre kein a-Baum. 2. ergibt sich aus 1., wenn man berücksichtigt, daß b-Erreichbarkeit die inverse Relation zu f-Erreichbarkeit ist. 2 2 Ein Digraph, der ein f-Baum ist, soll gerichteter Baum (Wurzelbaum , directed tree, rooted tree) heißen. Der eindeutig bestimmte Knoten, von dem alle anderen Knoten f-erreichbar sind, wird Wurzel (root) genannt. Ein Digraph, dessen schwache Zusammenhangskomponenten gerichtete Bäume sind, heißt gerichteter Wald (directed forest). In ungerichteten Graphen fallen a-Bäume und f-Bäume zusammen. Von jedem Knoten sind alle anderen f-erreichbar. Man kann auch leicht Graphen angeben, die sowohl Kanten Nicht zu verwechseln mit B-Bäumen (siehe Unterabschnitt 9.4.3, Seite 299, oder Cormen/Leiserson/Rivest [CormLR1990], Kapitel 19). 2 Manchmal wird auch ein freier Baum, in dem ein Knoten besonders hervorgehoben wird, Wurzelbaum genannt. 1 14.5. PARTIELLE ORDNUNG UND SCHICHTENNUMERIERUNG 387 als auch Bögen aufweisen und in denen es mehr als einen Knoten gibt, von dem alle anderen f-erreichbar sind (Beispiel?). Auch den Begriff Wurzel kann man auf allgemeine Graphen übertragen. Das soll in Abschnitt 14.5 geschehen. Um zu entscheiden, ob ein gegebener Digraph ein f-Baum ist, kann man die Bedingungen der Definition überprüfen oder den folgenden Satz benutzen. Satz 14.6 Ein Digraph G(V, A, ϕ, ψ) ist genau dann ein gerichteter Baum, wenn die folgenden Bedingungen gelten: 1. G ist f-kreisfrei und 2. jeder Knoten hat höchstens einen Vorgänger und 3. es gibt einen Knoten von dem jeder andere Knoten f-erreichbar ist. Beweis: Es sei G ein gerichteter Baum. Dann hat er nach Definition und Proposition 14.7 die Eigenschaften 1., 2. und 3. Es sei G ein Digraph, für den 1., 2. und 3. gelten. w sei ein Knoten, von dem allen anderen f-erreichbar sind. Wegen der f-Kreisfreiheit ist w eindeutig bestimmt und kann keinen Vorgänger haben. Alle anderen Knoten haben nach 2. dann genau einen Vorgänger. G ist schwach zusammenhängend, denn es gibt von jedem Knoten u zu jedem verschiedenen Knoten v einen a-Weg über w. Es werde angenommen, daß es in G einen a-Kreis gibt. Wenn w auf diesem Kreis liegt, setzen wir y = w. Andernfalls wählen wir einen f-Weg von w zu einem Knoten x des Kreises. Es sei y der erste Knoten des Kreises, den wir auf diesem Wege treffen. y kann von x verschieden sein. Der eindeutig bestimmte Vorgänger von y gehört nicht zum Kreis. Es sei v0 = y, l1 , v1 , . . . , vk−1, lk , vk = y ein a-Kreis auf dem gleichen Träger. Da y entweder keinen Vorgänger hat oder dieser nicht zum Kreis gehört, wird l1 in Vorwärtsrichtung durchlaufen. Da auch v1 nur einen Vorgänger hat, wird auch l2 in Vorwärtsrichtung durchlaufen usw. Es müßten alle Bögen in Vorwärtsrichtung durchlaufen werden und der Kreis wäre im Widerspruch zu 1. ein f-Kreis. 2 14.5 Partielle Ordnung und Schichtennumerierung Ist in einem Dag v von u f-erreichbar, so ist u nicht von v f-erreichbar, denn es gibt keine starken Zusammenhangskomponenten. Daher ist für Dags f-Erreichbarkeit eine strikte partielle Ordnung auf der Menge der Knoten. Zu Ordnungsrelationen siehe Abschnitt A.5, Seite 616, im Anhang. In allgemeinen Graphen gibt es i. a. starke Zusammenhangskomponenten und für deren Knoten ist f-Erreichbarkeit keine partielle Ordnung, sondern eine Äquivalenzrelation. Es gibt aber durchaus eine partielle Ordnung zwischen den Knoten einer starken Zusammenhangskomponente und Knoten, die nicht zu ihr gehören. Das besagt der folgende Hilfssatz. 388 KAPITEL 14. WEGE UND ZUSAMMENHANG Dazu sei C(u) = {u}, wenn u ein Knoten ohne Rückkehr ist, und die von u erzeugte starke Zusammenhangskomponente sonst. Wir nennen C(u) die von u erzeugte Schichtenklasse (level class). Hilfssatz 14.5 Es sei G ein allgemeiner Graph und es gebe einen f-Weg von u nach v, aber keinen von v nach u. Dann gibt es für alle u0 ∈ C(u) und für alle v 0 ∈ C(v) einen f-Weg von u0 nach v 0 , aber keinen von v 0 nach u0 . Beweis: Wenn man von u0 in C(u) nach u kommen kann, dann kann man auf einem f-Wege über u auch von u0 nach v kommen und von dort innerhalb C(v) nach v 0 . Gäbe es einen f-Weg von v 0 nach u0, so könnte man analog einen f-Weg von v nach u finden. 2 Wir haben damit eine strikte partielle Ordnung „≺“ auf der Menge der oben definierten Klassen C(u). Diese wollen wir benutzen, um allen Knoten einer Klasse C(u) und damit auch den Klassen selbst eine Schichtennummer (level number) lv(C(u)) zuzuordnen: 1. Hat C(u) bezüglich ≺ keinen Vorgänger, so setzen wir lv(C(u)) := 0. 2. Andernfalls nehmen wir das Maximum der Vorgänger plus 1: lv(C(u)) := max lv(C(v)) + 1 C(v)≺C(u) C(u) heißt Anfangselement (starting element), wenn es bezüglich ≺ minimal ist. Es heißt Endelement (end element), wenn es bezüglich ≺ maximal ist. Alle Anfangselemente haben die Schichtennummer 0. Endelemente haben i. a. verschiedene Schichtennummern. Die Schichtennumerierung eignet sich gut zur graphischen Darstellung von allgemeinen Graphen, insbesondere zur Darstellung abgeleiteter Graphen. Beispiele sind in Unterabschnitt 14.7 angegeben. Wird auf einem f-Weg (a-Weg) eine Kante durchlaufen, dann ändert sich die Schichtennummer der beiden so gegebenen Knoten des Weges nicht. Das gleiche gilt, wenn ein Bogen, der zu einer starken Zusammenhangskomponente gehört, durchlaufen wird. Wird ein Bogen des externen Dags in f-Richtung durchlaufen, so ist die Klasse des Startpunktes des Bogens bezüglich ≺ Vorgänger der Klasse des Zielpunktes. D. h. die Schichtennummer wird größer. Entsprechend wird die Schichtennummer kleiner, wenn ein Bogen des externen Dags in b-Richtung durchlaufen wird. Wir wollen das als Proposition formulieren. Proposition 14.8 1. Beim Durchlaufen eines f-Weges kann die Schichtennummer nicht fallen. Sie wächst genau dann, wenn ein Bogen des externen Dags durchlaufen wird. 14.5. PARTIELLE ORDNUNG UND SCHICHTENNUMERIERUNG 389 2. Beim Durchlaufen eines a-Weges ändert sich die Schichtennummer genau dann, wenn ein Bogen des externen Dags durchlaufen wird. Sie wächst, wenn der Bogen in f-Richtung durchlaufen wird. Sie fällt, wenn der Bogen in b-Richtung durchlaufen wird. Anmerkung 14.5 Die Schichtenummer entspricht der Länge einer maximalen Vorgängerkette. Es ist möglich und manchmal zweckmäßig die Schichtennumerierung „von unten“ vorzunehmen, d. h. statt Vorgänger Nachfolger zu verwenden. 2 Die partielle Ordnung der Klassen C(u) hilft uns, eine Aussage über die Orientierbarkeit von Kanten zu beweisen und damit den Beweis von Proposition 14.2 zu vervollständigen. Eine Kante heißt orientierbar (orientable), wenn sie zu einem Bogen gemacht werden kann und ihre zugehörige starke Zusammenhangskomponente unverändert bleibt. Proposition 14.9 Eine Kante l eines allgemeinen Graphen ist genau dann orientierbar, wenn sie auf einem a-Kreis liegt, der ganz in ihrer starken Zusammenhangskomponente verläuft. Beweis: Eine Schlinge ist ein a-Kreis und orientierbar. Sei l keine Schlinge. Wir betrachten die starke Zusammenhangskomponente von l als selbständigen Graphen H. Gibt es darin keinen a-Kreis durch l, so ist l nach Proposition 14.3 eine Brücke und daher der einzige einfache a-Weg zwischen ihren Inzidenzpunkten. Macht man aus l einen Bogen, so gibt es einen Inzidenzpunkt, der vom anderen nicht mehr f-erreichbar ist. D. h. H ist nicht mehr stark zusammenhängend. Daran ändert sich auch nichts, wenn l in ganz G keine Brücke ist, denn kein a-Weg von einem Inzidenzpunkt zum anderen, der z. T. außerhalb von H verläuft, kann ein f-Weg sein. Es liege l auf einem a-Kreis. Wir betrachten den Graphen H − l, der aus H durch Entfernen der Kante l entsteht. Da l keine Brücke ist, ist er schwach zusammenhängend. Wir zerlegen ihn in starke Zusammenhangskomponenten und den externen Dag. Ergibt sich eine einzige starke Zusammenhangskomponenten, so hat die Entfernung von l den starken Zusammenhang von H nicht beeinflußt und wir können l in jeder der beiden Richtungen orientieren. Ergeben sich mehrere Klassen C(u), so müssen sie in der partiellen Ordnung so angeordnet sein, daß sie nach Hinzufügen von l zu einer einzigen Klasse zusammenfallen. Das ist genau dann möglich, wenn es genau ein Anfangselement (Schichtennummer 0) und genau ein Endelement (maximale Schichtennummer) gibt und wenn weiter ein Inzidenzpunkt von l im Anfangselement und der andere im Endelement liegt. Wenn wir l nun so orientieren, daß der Bogen vom Inzidenzpunkt mit höchster Schichtennummer zum Inzidenzpunkt mit Schichtennummer 0 zeigt, bleibt H stark zusammenhängend. Bei entgegengesetzter Orientierung wird der starke Zusammenhang verletzt. 2 Als weitere Anwendung der Schichtennummern wollen wir die Überlegungen von Abschnitt 14.4 fortsetzen und Knoten mit „Wurzeleigenschaft“ definieren und bestimmen. In 390 KAPITEL 14. WEGE UND ZUSAMMENHANG Erweiterung der Definition von Seite 386 wollen wir den Begriff Wurzelknoten auch für allgemeine Graphen, die keine a-Bäume sind benutzen. In einem allgemeinen Graphen ist also ein Wurzelknoten ein Knoten, von dem es zu jedem anderen Knoten einen f-Weg gibt. Die Behauptungen der folgenden Proposition sind unmittelbar klar. Proposition 14.10 Es sei G(V, E, A, ϕ, ψ) ein allgemeiner Graph. 1. Enthält G einen Wurzelknoten, so ist G schwach zusammenhängend. 2. Ist u ein Wurzelknoten von G, so sind auch alle u0 ∈ C(u) Wurzelknoten von G. 3. Ein Knoten u ist genau dann Wurzelknoten von G, wenn lv(C(u)) = 0 und lv(C(v)) > 0 für alle C(v) 6= C(u). Allgemeine Graphen, die Wurzelknoten aufweisen, sollen nun näher untersucht werden. Wir wollen der Frage nachgehen, ob es Knoten gibt, die von einem Wurzelknoten auf mehr als einem einfachen oder linieneinfachen Weg erreicht werden können. Es sei G(V, E, A, ϕ, ψ) ein allgemeiner Graph und u ∈ V ein Wurzelknoten. G ist schwach zusammenhängend. Um Trivialfälle auszuschließen, wollen wir annehmen, daß G mindestens eine Linie enthält. Wir wollen wir die folgenden Fälle unterscheiden: 1. G ist a-kreisfrei. G ist ein a-Baum und alle einfachen f-Wege von einem Wurzelknoten zu einem anderen Knoten sind eindeutig bestimmt. Nicht-einfache f-Wege gibt es nur, wenn der Graph Kanten aufweist, und sie entstehen nur durch Mehrfachdurchlauf von Kanten, sind also nicht linieneinfach. 2. G enthält keine f-Kreise, wohl aber einen a-Kreis. Die starken Zusammenhangskomponenten von G, wenn es sie gibt, bestehen nach Proposition 14.1 nur aus Kanten und sind nach Proposition 14.2 a-kreisfrei. Kein a-Kreis in G ist vollständig in einer starken Zusammenhangskomponente enthalten. In Proposition 14.11 wird bewiesen, daß dann in G ein Knoten u0 existiert, zu dem von u zwei verschiedene einfache f-Wege führen. 3. G enthält einen f-Kreis. In Proposition 14.11 wird bewiesen daß es einen Knoten u0 gibt, zu dem von u zwei verschiedene linieneinfache f-Wege führen. Allerdings kann es sein, daß einer der Wege die Länge 0 hat. Proposition 14.11 Es sei G ein allgemeiner Graph und u in ihm ein Wurzelknoten. 1. Enthält G einen f-Kreis, so gibt es einen Knoten u0 , der von u auf zwei verschiedenen linieneinfachen f-Wegen erreichbar ist. Einer der Wege kann die Länge 0 haben. 14.5. PARTIELLE ORDNUNG UND SCHICHTENNUMERIERUNG 391 2. Enthält G einen a-Kreis, der nicht vollständig in einer starken Zusammenhangskomponente enthalten ist, so gibt es einen von u verschiedenen Knoten u0 , der von u auf zwei verschiedenen einfachen f-Wegen erreichbar ist. Beweis: 1. Sei x0 , l1 , x1 , l2 , . . . , xn−1 , ln , xn = x0 ein f-Kreis. Liegt u auf dem Kreis, so wählen wir xo = u. Andernfalls wird ein einfacher f-Weg von u zu einem Knoten des Kreises gewählt. x0 soll der der erste Knoten des Kreises auf diesem Wege sein. Der f-Weg von u bis x0 ist der erste, der um den f-Kreis verlängerte Weg ist der zweite f-Weg von u nach x0 . Beide sind linieneinfach. 2. Ein a-Kreis, der nicht vollständig in einer starken Zusammenhangskomponente enthalten ist, kann keine Schlinge sein und hat mindestens die Länge 2. Alle seine Linien sind paarweise verschieden. Er muß Knoten mit unterschiedlichen Schichtennummern durchlaufen. Es sei u0 ein Knoten mit höchster Schichtennummer. Diese ist mindestens 1. Unter den Vorgängern von u0 auf dem a-Kreis gibt es einen ersten, der eine andere, also niedrigere Schichtennummer hat. Das sei w. Entsprechend gibt es unter den Nachfolgern auf dem a-Kreis einen auch einen ersten, der eine niedrigere Schichtennummer hat. Das sei w 0 . Der Fall w = w 0 kann auftreten. Der unmittelbare Nachfolger von w auf dem a-Kreis sei w1 , die w und w1 verbindende Linie des a-Kreises sei l1 . Der unmittelbare Vorgänger von w 0 auf dem a-Kreis sei w2 , die w 0 und w2 verbindende Linie des a-Kreises sei l2 . Die Fälle u0 = w1 und u0 = w2 sind möglich. u0 , w1 und w2 haben die gleiche (höchste) Schichtennummer und gehören, falls mindestens zwei von ihnen verschieden sind, zur gleichen starken Zusammenhangskomponente. Wenn alle drei Knoten zusammenfallen, kann es sein, daß u0 ein Knoten ohne Rückkehr ist. Wir wählen nun einfache f-Wege (eventuell der Länge 0) von u nach w und von u nach w 0. Auf ihnen haben wir nach Proposition 14.8 nicht-fallende Schichtennummern, so daß u0 auf keinem der Wege liegen kann. Die f-Wege können um l1 bzw. um l2 verlängert werden, denn beides sind Bögen des externen Dags und zeigen zum Knoten mit höherer Schichtennummer. Wir erhalten einfache f-Wege von u nach w1 und von u nach w2 . Die Wege sind mindestens in l1 und l2 verschieden. Ist w1 = u0 , so ist der erste Weg vollständig. Andernfalls verlängern wir ihn innerhalb der starken Zusammenhangskomponente um einen einfachen f-Weg von w1 nach u0 . Im Fall u0 6= w2 führen wir eine entsprechende Verlängerung des zweiten f-Weges durch und erhalten schließlich zwei verschiedene einfache f-Wege von u nach u0. 2 Zur Behauptung, daß auch Wege der Länge 0 zugelassen werden müssen und daß die Wege manchmal keine einfachen Wege sein können, siehe das folgende Beispiel. Beispiel 14.1 Abbildung 14.2 zeigt einen allgemeinen Graphen, der aus zwei schwachen Zusammenhangskomponenten besteht. Jede enthält eine starke Zusammenhangskomponente mit f-Kreis. In der ersten schwachen Zusammenhangskomponente gibt es nur den Wurzelknoten u. Der einzige Knoten, den man von ihm auf zwei verschiedenen linieneinfachen f-Wegen erreichen kann, ist u selber. Einer der Wege hat die Länge 0, der andere 392 KAPITEL 14. WEGE UND ZUSAMMENHANG ........... .... .... . ....... . ............ ...... ........ ........ ..... ..... . ..... .............................................. ... ... ...... ...... ................ ..... u .................. . ..... .. ... ................ .......... .......... .... ...... .... ...... ...... .... ....................................... . ... .... .... ........ .. . . . .......... ............. ... ... ... .... ... ... ... ... ... ...... .. . . ... ....... . . .. .............. .............. .............. . . . ................ . . . . . . ........ ... ........ ... ... . ........................................ . ..... 0............................................... .............................................. ...... ... . . ... . ..... .... ..... .... ...... ..... ............... ........ ........ ..... U U V Abbildung 14.2: f-Wege vom Wurzelknoten ist eine Schlinge. In der zweiten schwachen Zusammenhangskomponente gibt es die Wurzelknoten U und U 0 . Der einzige Knoten, den man von ihnen auf zwei verschiedenen linieneinfachen f-Wegen erreichen kann, ist V . Für jeden Wurzelknoten ist einer der Wege nicht einfach. 2 Anmerkung 14.6 Auch in einem allgemeinen Graphen, der f-Kreise aufweist, kann es einen a-Kreis geben, der nicht vollständig in einer starken Zusammenhangskomponente verläuft. Für ihn gilt, wenn u ein Wurzelknoten ist, die zweite Aussage von Proposition 14.11 natürlich auch. 2 14.6 Klassifizierung von Zusammenhangskomponenten Wir wollen in diesem Abschnitt allgemeine Graphen nach ihren Zusammenhangseigenschaften zerlegen und klassifizieren. In Unterabschnitt 14.2 haben wir eine hierarchische Zerlegung gefunden. Tabelle 14.1 zeigt die zugehörigen drei Ebenen: Allgemeiner Graph, schwache Zusammenhangskomponenten, starke Zusammenhangskomponenten. Darüber ' 1 2 3 4 5 & Ebene Ebene Ebene Ebene Ebene 0 allgemeiner Graph 1 uneigentliche schwache Zusammenhangskomponente 1 eigentliche schwache Zusammenhangskomponente 2 starke Zusammenhangskomponente 2 externer Dag Tabelle 14.1: Zerlegung in starke und schwache Zusammenhangskomponenten hinaus wollen wir die schwachen Zusammenhangskomponenten danach klassifizieren, ob sie a-kreisfrei sind oder nicht und ob sie starke Zusammenhangskomponenten enthalten oder nicht. Starke Zusammenhangskomponenten wollen wir einteilen in solche, die f-kreisfrei sind, und solche, die einen f-Kreis aufweisen. Diese Eigenschaften sind nicht voneinander unabhängig. Für alle schwachen Zusammenhangskomponenten läßt sich nach Proposition 14.10 bestimmen, ob es Wurzelknoten gibt oder nicht. $ % 14.6. KLASSIFIZIERUNG VON ZUSAMMENHANGSKOMPONENTEN 393 A. a-kreisfreie schwache Zusammenhangskomponente: Es handelt sich um einen a-Baum, d. h. zwischen je zwei verschiedenen Knoten gibt es genau einen einfachen a-Weg. 1. Ohne starke Zusammenhangskomponenten Es handelt sich um einen Dag. Wenn es Wurzelknoten gibt, dann genau einen (siehe Abschnitt 14.5) und die schwache Zusammenhangskomponente ist ein f-Baum. 2. Mit starken Zusammenhangskomponenten Die starken Zusammenhangskomponenten bestehen nur aus Kanten. Als Untergraphen sind sie freie Bäume. Wenn es Wurzelknoten gibt, ist jeder Wurzel eines f-Baumes. B. Nicht a-kreisfreie schwache Zusammenhangskomponente: 1. Ohne starke Zusammenhangskomponenten Es handelt sich um einen Dag, aber nicht um einen a-Baum. Wenn es Wurzelknoten gibt, existiert genau einer. Mindestens ein anderer Knoten ist dann nach Proposition 14.11 auf mehr als einem einfachen f-Weg erreichbar. 2. Mit starken Zusammenhangskomponenten 2.a. Jede f-kreisfreie starke Zusammenhangskomponente ist ungerichtet (Proposition 14.1) und als Untergraph ein freier Baum. Enthält die schwache Zusammenhangskomponente ausschließlich f-kreisfreie starke Zusammenhangskomponenten, so kann kein a-Kreis ein f-Kreis sein. D. h. jeder a-Kreis muß zwei Bögen in entgegengesetzter Richtung enthalten. Wenn es Wurzelknoten gibt, ist von jedem ein von ihm verschiedener Knoten auf zwei verschiedenen einfachen Wegen erreichbar (Proposition 14.11). 2.b. Im allgemeinen Fall gibt es starke Zusammenhangskomponenten mit f-Kreisen. Für diese ist eine weitergehende Zerlegung mit Hilfe der Biblockzerlegung möglich und sinnvoll (siehe Abschnitt 16.7, Seite 458). Wenn es Wurzelknoten gibt, kann von jedem von ihnen ein Knoten auf zwei verschiedenen linieneinfachen Wegen erreicht werden (Proposition 14.11). Wir werden in Kapitel 15 Algorithmen zur Bestimmung der einzelnen Typen schwacher und starker Zusammenhangskomponenten sowie der Schichtennummern angeben. Beispiel 14.2 Die eingeführten Begriffe sollen am Beispiel von Graph1, Seite 377, erläutert werden. Siehe dazu Tabelle 14.2. Es gibt keine uneigentlichen Zusammenhangskomponenten. Die schwachen Zusammenhangskomponenten sind W1 , W2 und W3 . W1 enthält auf Schicht Nummer 0 den Knoten ohne Rückkehr K12, auf Schicht Nummer 1 die starke Zusammenhangskomponente S11 , auf Schicht Nummer 2 die starken Zusammenhangskomponenten S12 und S13 sowie auf Schicht Nummer 3 die starke Zusammenhangskomponente S14 . Für jede starke Zusammenhangskomponente sind ihre Knoten und ihre schwachen Verheftungspunkte zum externen Dag angegeben. E1 ist der externe Dag von W1 , festgelegt durch seine Bögen. In analoger Weise ist in der Tabelle die Struktur der schwachen Zusammenhangskomponenten W2 und W3 dargestellt. 2 Ein weiteres Beispeil ist in Abschnitt E.3, Seite 660 im Anhang zu finden. 394 ' • KAPITEL 14. WEGE UND ZUSAMMENHANG W1 ◦ ◦ = 0 1 ◦ 2 ◦ 2 ◦ 3 ◦ • • W2 ◦ ◦ = 0 1 ◦ W3 ◦ = 0 ◦ 1 & ◦ {K00, K01, K02, K03, K06, K07, K08, K09, K11, K12, K13, K19, K20, K21} {K12} Knoten ohne Rückkehr S11 = {K00, K06, K11, K13} starke Zusammenhangskomponente Schwache Verheftungspunkte: K00, K11, K13 S12 = {K01, K02, K03, K09} starke Zusammenhangskomponente Schwache Verheftungspunkte: K01, K09 S13 = {K07, K19, K20, K21} starke Zusammenhangskomponente Schwache Verheftungspunkte: K19, K21 S14 = {K08} starke Zusammenhangskomponente Schwache Verheftungspunkte: K08, E1 = {dK00K01, dK00K21, dK09K08, dK12K11, dK12K13, dK19K08} externer Dag {K04, K05, K10, K16, K17, K18} {K10 } Knoten ohne Rückkehr S21 = {K04, K05, K16, K17, K18} starke Zusammenhangskomponente Schwache Verheftungspunkte: K18 E2 = {dK10K18} externer Dag {K14, K15} S31 = {K14} starke Zusammenhangskomponente Schwache Verheftungspunkte: K14 S32 = {K15} starke Zusammenhangskomponente Schwache Verheftungspunkte: K15 E3 = {dK14K15} externer Dag Tabelle 14.2: Zerlegung von Graph1 in schwache und starke Zusammenhangskomponenten 14.7 Abgeleitete Graphen Es ist oft zweckmäßig, zusätzlich zu einem Graphen weitere Graphen, die von ihm abgeleitet (derived) sind, zu betrachten. Zum einen sind diese Graphen Vereinfachungen, zum anderen heben sie Struktureigenschaften hervor. Zwei abgeleitete Graphen sollen im folgenden eingeführt werden. Andere werden an späterer Stelle behandelt. Kondensierter Digraph. Es sei G(V, E, A, ϕ, ψ) ein allgemeiner Graph. V wird zerlegt in die Mengen C1 , C2 , . . . , Ck . Dabei ist Ci die Menge der Knoten einer starken Zusammenhangskomponente von G oder eine durch einen Knoten ohne Rückkehr gebildete einelementige Knotenmenge. Es wird ein Digraph CG gebildet, dessen Knoten die Ci sind. Von Ci zu Cj wird genau dann ein Bogen eingeführt, wenn es in G Knoten vi ∈ Ci und vj ∈ Cj und einen Bogen von vi zu vj gibt. CG soll der zu G gehörige kondensierte Digraph (reduzierter Digraph, condensed digraph, reduced digraph) heißen. CG ist f-kreisfrei $ % 14.7. ABGELEITETE GRAPHEN 395 und es gibt genau dann in G einen f-Weg (b-Weg, a-Weg) von vi ∈ C1 nach vj ∈ Cj mit i 6= j, wenn es in CG einen f-Weg (b-Weg, a-Weg) von Ci nach Cj gibt (warum?). CG besitzt keine Mehrfachbögen. Komponentengraph. Der kondensierte Digraph gibt wichtige Eigenschaften eines allgemeinen Graphen wieder. Wenn man nach wie vor die starken Zusammenhangskomponenten zu einem Knoten zusammenschrumpfen will, ihre Einbettung in die entsprechenden externen Dags aber detaillierter braucht (zum Beispiel will man die schwachen Verheftungspunkte haben), dann ist der Komponentengraph (component graph) geeigneter. Er wird gebildet, indem man den externen Dag (also einschließlich der schwachen Verheftungspunkte) unverändert läßt. Zusätzlich führt man für jede starke Zusammenhangskomponente einen neuen Knoten ein. Diesen verbindet man mit jedem der schwachen Verheftungspunkte der starken Zusammenhangskomponente durch eine ungerichtete Kante. Dies ist keine Kante des Originalgraphen. Sie wird deshalb auch Metakante genannt. Auch der Komponentengraph ist f-kreisfrei. Er kann Mehrfachbögen aufweisen, nämlich die, die es im externen Dag gibt. Sind u und v Knoten im Originalgraphen, die nicht zur gleichen starken Zusammenhangskomponente gehören, so gibt es genau dann einen f-Weg (b-Weg, a-Weg) von u nach v, wenn es im Komponentengraphen einen entsprechenden Weg gibt. Dabei werden innere Knoten einer starken Zusammenhangskomponente durch deren Knoten im Komponentengraph repräsentiert3 . Beispiel 14.3 Als Beispiele sollen der kondensierte Digraph und der Komponentengraph von Graph2, Seite 396, erläutert werden. Graph2 ist schwach zusammenhängend. Die Zerlegung in starke Zusammenhangskomponenten und externen Dag zeigt Tabelle 14.3. Der zugehörige kondensierte Digraph ist in Abbildung 14.4, Seite 397, und der zugehörige Komponentengraph ist in Abbildung 14.5, Seite 398, zu sehen. In beiden Abbildungen wird die Schichtennumerierung durch horizontale punktierte Linien dargestellt. {v12 } hat die Schichtennummer 0. 2 Manchmal, wie zum Beispiel im Komponentengraph auf Seite 560, ist es wünschenswert, alle Knoten in der starken Zusammenhangskomponente aufzuführen und die schwachen Verheftungspunkte hervorzuheben. 3 396 KAPITEL 14. WEGE UND ZUSAMMENHANG ' SC1 = {V 01, V 02, V 03, V 04, V 05, V 06, V 07, V 08, V 09, V 10, V 11, V 13, V 14, V 15, V 16, V 17, V 20, V 25} Schwache Verheftungspunkte: {V 02, V 09, V 13, V 17, V 20, V 25} SC2 = {V 18, V 19, V 22, V 23} Schwache Verheftungspunkte: {V 18, V 23} ED = {dV 12V 09, dV 12V 17, dV 12V 21, dV 13V 18, dV 17V 18, dV 17V 21, dV 18V 21, dV 23V 24, dV 20V 24, dV 02V 26, dV 25V 26} & Tabelle 14.3: Zerlegung von Graph2 in starke Zusammenhangskomponenten ....................... ........................ ........................ ...... ...... .... .... ...... .... .... .... ... ... ... ... .. .. ... ... ... ... .. .. . .. ...... .... . .. . . ..... .... . .. ... ............................................................................ ..................................................................................................................................................... .. . . . ... . . . ... .. ... . . .. . ... . . . . . . . . . . . . . . . ........... ... .... . . ..... . . . . . . . . . . . . . . . . . . . . . . . . . . ........ .... .. . . ... .................... ....... . ...................................... ..... ................... ....... ... ........ ............. ......... ....... ........ ............. ............. . ... .......... ... ...... ..... ... ... ... .... ... ... ... . ... . . . ... . ... . . . . . ... . . . . ... .. ... ... ... .... ... .. .. .. . . . . . ... . . . ... ... . . . . . ... . ... . . . . . ... ............... ..... . . . . . . . . . ... . . . . . . ... .. .... .. ............................. .. . . . . ... . . . ... . . . . ......... .... . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ...... ...... ...... .... ............... ..... . ..... ..... . . . . ...... . . . . . . . . . . . . . . . . . . . . . . . . . ... ... ... .......... .. .. .. .. . . . . ... . . . . . . . ... ... ... . . . . .. . . . .... . . .. . . .. .. .. . . ........................................................................... . .... .... .. . .... .. . .. .. ...... .... . . ... . . . ... ... ... .. . . . . . . ... . . . . . . . ... ... . ... . . .... . .............. . . . . . . . . . . . . . . . . . . ...... ...... . . ...... ..................... .............. .................................. .......................... .......................... .............................. ............... ............................. ....... .... .............. ... ......... .... ......... ... .... ... ...... ... ... ... ... ... ... ... ... .. . . . . ... ... . ... . . . ......................................................................... ... ... ... ... . ... . . ... . ... .. ...... ... ... . . ... . . ... ... .. . ... ................ . . . . ... .... ... . . . . .. ..... ...... . .. .............. . . ... ..... . . . . . . . ... . . . . ....... . ........ .. .. .... . . ... .......................... . . . . . . . . . . . . . . . . . . ... . . . . . . .. ....... ....... ... . . . . . . . . . . . . ... ... ............ ............... . . . . . . . . . . . ........................ ....... .... ......................... ...................... ..................... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ....... .... .... . .... .... .......... .. ... ... . . . . ... . . .... . . . . . . . . . . . . . . . . . . ....... ... ... .............. . . ... . . . ... .... . . . .. .. . .. .. . .. ... .. .. .. ....... ... .. ... ........................................................................ . ......................................... . . . . . . .. ... ... ... . ... . . . .. . . . .. . . ... . . . . ... ... ... .. . .. . ... ... . . . . . . . . . ..... .... ..... . . .. ...... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ....................... ....................... ........... ...................... ..... ........... ....................... ... ... .... .. . .. .. .... .... ... .... . .. .... ...... ... ... ... .. ..... ... ... .... .... . .... . . . . ... . . . . . . .... ....... .. ... . .. ... . . . . . . . . . . .... . ...... . . .. . ... . . . . . . . . . . . ...... ................. . .................. .................. ..... ... . . .................... ..... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ..... ..... ........ ....... ....... ... . . .... .... . . . . . . . . . . . ..... . . . . . ... ... ... . . ... .. ... . . ... ... .... ... .... .... . . ... ... .. ... ... . .. ... .... .... .... . . . ... ... .... ... ... ...................................................................... ........................................................................ ... . . . . . ... ... . . . . . . . . ... ... ... . . . .. ... .. . . . ... . . . . . . . . . ..... .... .... . .... .. . ... ..... . . . . . ... . . . . . . . . . . . . . . . ....... . . . . . . . . . ........ ....... ......... ......... ............ ... .. .......................... ..... ...................... ...... .. . . . . . . . . . . . . . . . . . . . . . . . . . . .. .... . .... . ... ... ... . . . . . . . .... ... ... .... . . . . . . . . ..... . . . ... .... ... ... .. . ... . . . . .... ... . . . ... .... .. .......... ... . . ... . . . . . ... .... .... . ...... .. . ... . . . . . . . . . ......... ... ....... .... ..... ... . . . . . . . . . . . . . . . . . . . . . . . . . . . . ....... . . . . . . . . . . . . . . . . . . . . . ........ ............ ... ............ ... .... .... .. .. ... ... ... ... .............. ... ... ... ... ...... .. ... ... .. . ... . . . ... ... ......................... ... . .......................... .. . . . . . . . . . . .... .... ... ... .. ...................... ... .. . . . .. . . . . . . . . ....................... ... ... ... ... ... . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ........................... ... .. .. ... ... .. . . .. . . . . . . . . . ...... . . ... . . . . . . . . . . . . . . . ....... .. .. . .................................................................. ......................... ... ...................... ... ... .. ........ ... . ... ... ... ... .... .... ... ... . ... ... .... ... ... ... ............. ... ... . . . . . . . . . . . . . . . . . . . . . . ... ..................... ...................... . .... .... .. . ...... .. ... ...... .... .... ... ........ ... ... ..... ... ... ... ... ........ ... ...... . ... .. ... . ..... . . . . . ..... . . . ... ... ... . ... ... . . . . ... . . . ... .. . ... ... . . . . ... ..... . . ... . . ... . .... ..... . . .. ... . . .... . . . . .. . . ... . .... . ... . .. ... .. . . . . . . . . . . . . . . ........... ........ . ......... ... ........... ........ . . . . . . . . .... ... ..... ... ............. ... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ....... ....... ....... ....... .. .............. ...... ..... ....... ...... .... .... .... .................. .... .... .... ... .... .... ... ... ... ... ... ... ... .. ... ... . . . . . . .. . . .... ... .. .. .. . .. . . .. ... .. ................................................................................................ ............................................................................. .... .. . .. .. .. .. ... ... . ... ... .. . . . . . . . ... . . . . . . . ... ... ... . . . . .... . . . . . . . . . . . . . . . . . . ..... ...... ...... ...... . ........................... .......................... .......................... ......................... V01 V26 V02 V03 V04 V09 V12 V07 V08 V10 V11 V13 V14 V16 V15 V17 V21 V25 V06 V05 V20 V18 V19 V22 V23 Abbildung 14.3: Graph2 V24 $ % 14.7. ABGELEITETE GRAPHEN 397 .................. .... ....... .. .... .. ... .. ... ... ... ... .. . . ... .. . ...... . . ...................... ... .. . . . . ..... . ...... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ... .... ... ... ... ... ... ... ... ... ... ... ... ... ... ..... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ..... ... .. ... .... .. . ......................................... .. ............... ................... ........ ........... ....... ... ...... ... . . . . ...... .... . .... . ..... . . ... . .... ... . ... . ... .. . ... ... . ... ... ... . . ... ... . . ... ... ... . ... ... .. . ... . ... . .. . ... ... ... . .. .... ... ... .. .. ... . . ... ... .. .... 1 ... .. ... . . . ... . ... . . ... ... ... ... ... ... ... . ... .. .... .. ... . ... . ... . ... ... ... ... ... .... ... ... ... ... ... . . ... . ..... . . .. ...... ...... ...... ... ...... .... ...... ....... ... ....... .... .......... ... ......... .... .. ................................................... ... . .... ... ... .. .... .... . ... ... .... ... .. . ... .... ... ... .. . . .... ... ... ... . . .... . ... .. . . .... ..... . . ... ... .... ... .. . . ... .... .. ..... .... . .... ... .. .. .... . .... . . .. ... .... .. ... . . ... .... . .. ... . . . .... ... .... .. ... . .... ... ... .... .. .... . ... ... .... ... .. . . ... .... ... ... .. . ... .... ... ... .. .... . . ... .. .. .... ..... . . ... ... .... .. ... . . .... ... .. ... ..... . .... ... ... ... .. . . . . . . ... . . . . . . . . ... . . . . . . . . . . ... . . . . . . . .... . . . . . . . . . . . . . . . . . ....... . . . . . . . . . . . . .... ... ......... .. ..... . ........ .... ... .. ... .... . ... . .. .... ................................... ... ... ........ ...... ... .... . . .. . . . . . ..... .. .... ... ... . . .. . . . . . . .... .... ... ... ... .. . . . .... . . ... .. .. .... ... . . . . ..... ... ... . .... .. . ... . . .. ... .. .. .. . .. ... . . . . ................ .................... ... .. .. ... .. ........ ..... . . . ... .. ... .. ... .. . . ... . . ... ... .. . . . . .. .... . . .. 2 ... .. ... .. ... ... . . . . ... ... ... .. . .. . ... . . . . ... ... ... ... . . .. . . . . . .... . ..... ... ... .. . .. . . . .. . . . . . . . . . . ... . . . . . . ............. . ... .. ...... ....... ... ... ... ...... ...... ...... ... ...... ........ ... ... ...... ....... .... ....... ...... ............ ... ... ... ..... ....................... ...... . . .. . . .. . . . . .... .. ... . .. .... ...... .. . .. ... . . .. .. ...... . . . ........... .............. . . ............ . . . . . . . . . . . . . . . . . . . . . . . ........ ... ............... . . . . . . . . . . . . . . . . . . . . . . . . . ....... ................. ...................... .............. . . . . . . ...... . . . . . . . . . . . . . . ......... . .... ............. ... . .... . . . . . . . ... ... . .. ... .. ... .... .... .. .. .. . . ... . ... ... .. ... . . . . . ..... ..... .......................... ........................ V12 SC SC V21 V26 V24 Abbildung 14.4: Kondensierter Digraph zu Graph2 398 KAPITEL 14. WEGE UND ZUSAMMENHANG .................. .... ....... .. .... .. ... .. ... ... ... ... .. . . ... . . . ...... . . ...................... .. .. ... . . . ..... .... ..... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. .... .... .... ... ... ... ... ... ... ... ... ... . ..... ..... ..... .. .. ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... .. ... ... ... ... ... ... ... ... ... ..... ... ... .. ... ........... ... ... ...... ... ... . ....... ...................... ... ........... ............ .... ... ...... ... ... ... ... ... ... ... ... .. .. ... ... .. .. ................................................ ... ... . . . . ... . ................................................................................................................... . . . . . . . ........ ... .. ... . .. ... ..... . . . . ... . . . . . . ...... ... ... ... ... . ... .... . . . . . . . . . . . . ... ..... .... . . . ..... ... ... .. ............. ......... . ..... . ... ........................ ......... ........ . ... . ... .......... .... ....... ..... ... ... ... ... ... ..... ... ... . ... . ... ... ... ... . .... . ... ... . ... . . . ... ... ... .. ... . . ... . ... ... . ... . . ... ... ... ... . . .. . . . ... ... . ............... . .... . ... . . . . . . . . .. .... .. . .... ... .... .... .. ... ... . ... .. ... .. ... .. ... . ... ... ... .. ...................... ... . . . . ... . ... ... ... . . . . . 1 ... ... ... . .. . . . .... . . ..... .... ... ... . .. . . . . . .. . . ... . . . .................. ... ... . . ... . ... . ... ... ... . ... . . . . .... ... ... ... . ... . ... . . . . ... ... ... . . ... ... . . ... ... ... . ... . ... ... . ... . ... .. ... ... ... ....... . . . ... ... ....... ...... ... ... . . . . . . . . . ............. . ....... ... .. .... . . . . . . . . . . . . . . . . . . . . . . . . . . . .... . . . . . . . . ..... . . .......... ..... ..... ......... ... .... . ...... ..... ... . . . .. ... . . . . . . ... ... . ... ....... . ... . . . . ... . ... . . . . . . .. ........ ... . .... .. . . . . . ........... . .... . . . .... ... . ... ... . ......................................... ... . .. ... ... . . . . .. ... ... . ... . . ... . . . .. . . . .... .... ... .... . ... . ... . . ....... .......... . . . ..................... ... . ............. ... ... . . . . ... ... ... ... ............... . . ... .... . . . . . . . ..... ... .. ..... .... ... .... . . ..... . ... ... ... .. ... . ... .. .... .. ... . ... .. .. . . ... . . . . . ... ... .. . .. .... ... ... .... ... . . ... . ... . ... ... .. ... ... . . ... . .. . ....... .. .. ... . .. . . . . . . . ... ... ... ... .................. ... .. ... . . . ... . .. ... ... . . .. ... . . . . . ... ... ... ... ... . ... . . . . . . ... .. ... . .... . .... .... . ..... ... ... .. . .. . ... . . . . ... .. . .. . ..... .... ... . . ... . . ... ... .. . . ... . ... . . . . . ... ... .... .. .... ... ... . . . . . ... . ....... . . . . ... .. . . ... . . . . . . ... . . . .. . ........... . ......... . . . . . . . . . . . . . . . . . . . . . . . ... . . . . . . . . . . . . . . . . ..... . . . . ..... . . ... .... .... .. ........................................ .... . ... ... ... ... . .. .... . . . ... . ... .. ................................... .... .... ... ... ..... ... ........ ...... . . . . .. . .... .... . ... . . . ... ..... . ... . . .... . ... .... . . .... ... . . . . ... .... ... .. . ........ . . ... . . .. ..... . . ... . ......... ... ... . . .. . ...... . . ... . . . . . . . . . . . . . . . ......... ... .. .......... .. ... ................ . .... . .... . . . . . . .. ......... ....... ... .. .. ... . . . . . . . ... . . .. ................ . . .. ..... .. ... ... ....... .. ... .. ... .... ... ... ... ... ... .... ... .. . .. . ... . ..... . . . .. . 2 ... .. .... .. ... .. . . ... . . . ... ... .. . . ... ... .... . . ... ... ... ....... . . . . . . . . . . . . .... .... ... ............................ . . . ..... . . .. ... .. . . . . ... ............... . . . . . . . ... ... ... ... . . ............. ... . ....... .. .... ... .. ... ... .... ... .. ...... ... ... ... .. ... ...... ....... .. ....... ............ ... ... ... ... ... ....................... ......................... ... . ... ... .. . . ............. .... ... . . .. ............. ........ ........... .. ............. .. ... . ...... ........ . . . .................................. . . . . . . . . . . . . . . . ............................ . . . . . . . . . . . ............ . . . . . . . . . . . . . . . . . . . . . . . . ............. ................. ........................ . . . . . . . . . . . . . . . . . . . . . . . ............. .... .... ... ............ .... ...... ... ... ... ................. .. .. ... ........... .. ... ... .... . . .. . . ... . ... ... .. ... . . . . . ..... ..... .......................... ........................ V12 V09 V02 V25 SC V17 V20 V13 V18 SC V26 V23 V21 V24 Abbildung 14.5: Komponentengraph zu Graph2 14.8. EULERSCHE UND HAMILTONSCHE WEGE 14.8 399 Eulersche und hamiltonsche Wege Bestimmte „maximale“ Wege sind von besonderem Interesse in der Graphentheorie. Sie sind nach Euler 4 und Hamilton 5 benannt. 14.8.1 Eulerwege Definition 14.6 Es sei G ein allgemeiner Graph. Ein linieneinfacher a-Weg, der jede Linie aus G enthält, heißt Eulerweg (Eulerian path). Er wird a-Eulerkreis (a-Euler circuit), genannt, wenn er geschlossen ist, und a-Eulerzug (a-Euler trail), wenn er offen ist. Ein linieneinfacher f-Weg, der jede Linie aus G enthält, heißt f-Eulerweg (f-Euler path, directed Euler path). Er wird f-Euklerkreis (gerichteter Eulerkreis, f-Euler circuit, directed Euler circuit) genannt, wenn er geschlossen ist, und f-Eulerzug (gerichteter Eulerzug, fEuler trail, directed Euler trail), wenn er offen ist. Ein allgemeiner Graph heißt a-eulersch (a-Eulerian), wenn er einen a-Eulerkreis enthält. Ein allgemeiner Graph heißt f-eulersch (f-Eulerian), wenn er einen f-Eulerkreis enthält. 2 Im Englischen wird für Eulerkreise auch die Bezeichnung Euler tour und für eulersche Graphen die Bezeichnung unicursal graphs benutzt. Man beachte, daß Eulerkreise i. a. keine Kreise sind. f-eulersche Digraphen werden auch gerichtete Eulergraphen (directed Eulerian graph) genannt. Abgesehen von isolierten Knoten ist jeder a-eulersche Graph schwach zusammenhängend und brückenfrei. f-eulersche Graphen sind stark zusammenhängend und natürlich auch brückenfrei. Schlingen und Mehrfachlinien können in beiden Fällen beliebig vorhanden sein. 14.8.1.1 a-Eulerkreise a-Eulerkreise sind leicht zu charakterisieren und algorithmisch zu finden. Satz 14.7 Ein allgemeiner Graph G ist genau dann a-eulersch, wenn er schwach zusammenhängend ist und der Gesamtgrad eines jeden Knoten gerade ist. Euler, Leonhard ∗15. April 1707, Basel, †18. September 1783, Sankt Petersburg. Schweizer Mathematiker. Lebte und wirkte 20 Jahre in Berlin, davor und danach bis zu seinem Lebensende in Sankt Petersburg. War in der zweiten Lebenshälfte blind. Einer der genialsten Mathematiker aller Zeiten und wohl der produktivste. Wesentliche Beiträge zu allen Teilen der Mathematik, insbesondere der Analysis. Ebenso wichtige Beiträge zur Physik und Angewandten Mathematik. Euler war Zeitgenosse der Bernoullis und mit Daniel Bernoulli, siehe Seite 619, befreundet. Seine Lösung des Königsberger Brückenproblems – es geht dabei um die Aufgabe, alle Pregelbrücken, die einige Inseln und die Ufer verbinden, auf einem Rundweg genau einmal zu überqueren – ist eine der ersten Arbeiten der Graphentheorie. 5 Hamilton, Sir William Rowan ∗4. August 1805, Dublin, † 2. September 1865, Dunsink(Irland). Irischer Mathematiker. Professor für Astronomie am Trinity College, Dublin. Beiträge zur Theoretischen Mechanik. Führte die Quaternionen ein. Die Bezeichnung „Hamiltonweg“ ergab sich eher nebenbei aus einer Denksportaufgabe. 4 400 KAPITEL 14. WEGE UND ZUSAMMENHANG Beweis: Die Bedingungen sind notwendig: Daß G schwach zusammenhängend sein muß, wurde schon festgestellt. Außerdem muß ein Eulerkreis beim Durchgang durch einen Knoten über eine Nichtschlinge betreten und über eine andere Nichtschlinge wieder verlassen werden. Der Anfangsknoten des Eulerkreises, wenn er überhaupt verlassen wird, wird das erste Mal über eine Nichtschlinge verlassen und bei der letzten Rückkehr über eine andere Nichtschlinge wieder betreten. Die Anzahl Nichtschlingen, mit denen ein Knoten inzidiert, muß gerade sein. Da Schlingen den Gesamtgrad um 2 erhöhen (Seite 355), ist der Gesamtgrad gerade. Daß die Bedingungen hinreichend sind, zeigt der in Tabelle 14.4 aufgeführte Algorithmus von Hierholzer 6. Ist der Graph schwach zusammenhängend und hat jeder Knoten geraden Gesamtgrad, so so endet der Algorithmus stets mit einem Eulerkreis 2 14.8.1.2 f-Eulerkreise Festzustellen, ob ein allgemeiner Graph f-eulersch ist und in diesem Fall einen f-Eulerkreis anzugeben, ist jedoch keineswegs einfach. Wie ist vorzugehen? Die folgenden Überlegung hilft weiter. Wenn es in einem allgemeinen Graphen G einen f-Eulerkreis gibt und man diesen durchläuft, erhält man eine vollständige Orientierung von G, bei der in jedem Knoten Eingangsgrad gleich Ausgangsgrad ist. Eine vollständige Orientierung mit dieser Eigenschaft soll Eulerorientierung (Eulerian orientation) genannt werden. Damit ist die notwendige Bedingung des folgenden Satzes gegeben. Satz 14.8 Ein schwach zusammenhängender allgemeiner Graph besitzt genau dann einen f-Eulerkreis, wenn er eine Eulerortierung besitzt. Beweis: Daß die Existenz einer Eulerorientierung ausreicht, um einen f-Eulerkreis zu finden, zeigt eine leicht zu findende Abwandlung des Algorithmus von Hierholzer. 2 Aus diesem Satz sind leicht Folgerungen für ungerichtete Graphen und für Digraphen zu ziehen. Ein ungerichteter a-eulerscher Graph weist auch einen f-Eulerkreis auf. Also nach Satz 14.7: Korollar 1: Ein ungerichteteter Graph besitzt dann und nur dann eine Eulerorientierung, wenn er schwach zusammenhängend ist und alle Knoten einen geraden Grad haben. Ein Digraph ist vollständig orientiert. Also Hierholzer, Carl ∗2. Oktober 1840, Freiburg im Breisgau, †13. September 1871, Karlsruhe. Deutscher Mathematiker. Sein Beweis, daß die Geradzahligkeit aller Knoten auch hinreichend für die Existenz eines Eulerkreises ist, wurde nach seinem Tod von Christian Wiener und Jacob Lüroth aus dem Gedächtnis aufgeschrieben und 1873 veröffentlicht [Hier1873]. Auch ein anderer, von M. Fleury stammender Beweis [Fleu1883] wird in der Literatur des öfteren zitiert. 6 14.8. EULERSCHE UND HAMILTONSCHE WEGE ' 401 $ Algorithmus von Hierholzer Der Algorithmus bearbeitet einen allgemeinen Graphen mit nicht-leerer Linienmenge. Ist der Graph a-eulersch, so liefert der Algorithmus einen a-Eulerweg. Ist der Graph nicht a-eulersch, so endet er mit einer entsprechenden Fehlermeldung. Man startet in einem beliebigen Knoten einen Weg, markiert jede durchlaufene Linie, um sie nicht noch einmal zu berücksichtigen, und stoppt, wenn es nicht mehr weitergeht, d. h wenn es in dem erreichten Knoten keine unmarkierten Linien mehr gibt. Dabei werden beim ersten Besuch eines Knotens auch alle Schlingen durchlaufen. Außerdem wird beim ersten Besuch der Knoten in eine Warteschlange eingehängt, wenn es beim Verlassen nicht markierte Linien gibt. 1. Wird in einem anderen als dem Anfangspunkt gestoppt, so hat man einen Knoten ungeraden Grades gefunden. 2. Wird im Anfangsknoten gestoppt und ist die Warteschlange leer, so prüft man, ob im Graphen unmarkierte Linien verbleiben. Wenn ja, ist der Graph nicht schwach zusammenhängend und kann keinen Eulerkreis aufweisen. Wenn nein, hat man einen Eulerkreis gefunden. 3. Wird im Anfangsknoten gestoppt und ist die Warteschlange nicht leer, so entnimmt man ihr so lange Knoten, bis einer auftaucht, der mit einer noch nicht markierten Linie inzidiert. Man startet mit diesem Knoten das Verfahren aufs neue. Man findet so entweder einen Knoten ungeraden Grades oder einen linieneinfachen geschlossenen Teilweg, der in den schon gefundenen Weg eingefügt wird. Danach wird wieder die Warteschlange geprüft. 2 & % Tabelle 14.4: Algorithmus von Hierholzer Korollar 2: Ein Digraph ist genau dann f-eulersch, wenn er schwach zusammenhängend und in jedem Knoten Eingangsgrad gleich Ausgangsgrad ist. Um von einem allgemeinen Graphen zu wissen, daß er f-eulersch ist, muß man nicht immer eine Eulerorientierung kennen. Satz 14.9 Hat ein schwach zusammenhängender allgemeiner Graph G in jedem Knoten einen geraden Gesamtgrad und ist in jedem Knoten Eingangsgrad gleich Ausgangsgrad, so ist der Graph f-eulersch. 402 KAPITEL 14. WEGE UND ZUSAMMENHANG Beweis: Wir betrachten die beiden Untergraphen, die durch die Kanten bzw. durch die Bögen von G erzeugt werden. Es seien HE1 , HE2 , . . . , HEl und HA1 , HA2 , . . . , HAm deren schwache Zusammenhangskomponenten. Nach Satz 14.7 und Korollar 2 von Satz 14.8 besitzt jede einen f-Eulerkreis. Wegen des schwachen Zuammenhangs von G lassen sich diese zu einem Gesamteulerkreis zusammensetzen. 2 Wann hat denn nun ein allgemeiner Graph eine Eulerorientierung? Einfache notwendigen Bedingungen dafür sind leicht aufzustellen. Man braucht so viele Kanten, daß man die Anzahl Eingangsbögen bis zur Anzahl Ausgangsbögen ergänzen kann oder umgekehrt. Die Anzahl dann noch verbleibender Kanten muß gerade sein. In Formeln d(v) − |id(v) − od(v)| = 2p mit p ≥ 0 (14.1) Leider es nicht richtig, daß diese notwendige Bedingung auch hinreichend ist. Der Graph von Abbildung 14.6 ist stark zusammenhängend und genügt Gleichung 14.1. Macht man ................. ... ...................... ......................... ............... . ............... . .......... . . . . . . . .................. . . ......... .. ... ...... ........ ....... ...... ....... ...... . . . . . . . . . ...... . .. . ..... . . . ..... . . . . .. .. ..... ... . . . . . . . . .... ....... .. ... . . .... . . . . . .............. .. ... . . . . . ... ... .. .. . . . . . . . . . . ... . .......... ............................ . . . ... . ... . . . . .... .... .... ... .... . . . . .... . ....... ... ... .. ... ..... ... ...... .. ... .. ....... ... .. ....... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .... .. .... .. ............ .. .... . .. .. .... ............................................................... .. .... .... . . . . ... . . ... . . . . . . ... ... ... . .. ...... ....... . ...... ....... . . . . . . . . ............. ......... ..... .... . . . ..... ......... .. ... ... ..... ... ...... ........ .. ...... ...... ........ .... ............................... . ... ................. .. x w r s t u v Abbildung 14.6: f-eulersche und nicht-f-eulersche Orientierungen aus der Kante (r, s) einen Bogen von r nach s, so bleiben diese Eigenschaften erhalten. Orientiert man danach die Kante (s, t) in irgendeiner Richtung, so ergibt sich immer noch ein stark zusammenhängender Graph. Gleichung 14.1 ist aber auf jeden Fall verletzt. Wird am Anfang die Kante (r.s) von s nach r orientiert, so können, wie man leicht sieht, alle anderen Kanten so orientiert werden, daß ein f-eulerscher Digraph ensteht. D. h. orientiert man in einem gegebenen allgemeinen Graphen nacheinander alle Kanten unter Beachtung von Gleichung 14.1, so kann es günstige und ungünstige Reihefolgen geben. Wenn man sich das Beispiel in Abbildung 14.6 genauer ansieht, merkt man, daß für die Kante (t, s) die Richtung von t nach s erzwungen wird, wenn es eine Eulerorientierung geben soll. Aus dem gleichen Grund werden danach die Richtungen von s nach r, von r nach x und x nach u erzwungen und der Eulerkreis ist fertig. Wir wollen nun den Algorithmus Präeuler kennenlernen, der solche erzwungenen Orientierungen solange durchführt, wie es geht. Die wesentlichen Gedanken, aber nicht die Einzelheiten, sind in Tabelle 14.5 zu sehen. Der Algorithmus benutzt eine anfangs leere Warteschlange. Als Ergebnis von Präeuler erhalten wir : 14.8. EULERSCHE UND HAMILTONSCHE WEGE ' 403 1. Wandle alle ungerichteten Schlingen in gerichtete um. $ 2. Durchlaufe nacheinander alle Knoten des Graphen. Prüfe bei jedem, ob Gleichung 14.1 gilt. Falls nein, Ende. Der Graph läßt keine Eulerorientierung zu. Falls ja (a) Ausbalancierung ist möglich: i. Die Zahl der Kanten entspricht genau der Differenz zwischen ankommenden und abgehenden Bögen. Die Kanten werden zu Ausgangsbögen. Jeder Zielknoten wird in die Warteschlange eingefügt. ii. Die Zahl der Kanten entspricht genau der Differenz zwischen abgehenden und ankommemdem Bögen. Die Kanten werden zu Eingangsbögen. Jeder Startknoten wird in die Warteschlange eingefügt. (b) Von vornherein ausbalanciert. Verbleibende Anzahl Kanten gerade. (c) Ausbalancierung nicht möglich: Es bleiben Kanten übrig. Der Graph bleibt Kandidat für den widerspenstigem Fall. 3. Entnimm nacheinander die Knoten der Warteschlange, bis sie leer ist. Gehe wie bei Schritt 2 vor. 4. Mit einem erneuten Durchlauf durch alle Knoten, stelle fest ob die Voraussetzungen für Satz 14.9 erfüllt sind. Wenn ja, kann eine Eulerorientierung gefunden und ein f-Eulerkreis angegeben werden. Falls nein, liegt der widerspenstige Fall vor. & % Tabelle 14.5: Präeuler: Algorithmus zum Finden ein einer maximalen Anzahl von Bögen 1. entweder einen allgemeinen Graphen, in dem Gleichung 14.1 nicht gilt 2. oder einen allgemeinen Graphen, in dem in jedem Knoten die Anzahl ankommender Bögen gleich der Anzahl abgehender Bögen ist 3. oder einen allgemeinen Graphen, in dem Gleichung 14.1 gilt, aber Knoten existieren, für die die Anzahl ankommender Bögen ungleich der Anzahl abgehender Bögen ist. Fall 1. Der Graph besitzt keine Eulerorientierung, ist also nicht f-eulersch. Fall 2. Nach Satz 14.9 kann ein f-Eulerkreis gefunden werden. Fall 3. Das ist der widerspenstige Fall. Es kann immer noch sein, daß der Graph f-eulersch ist oder nicht. Man siehe hierzu Abbildung 14.7 und mache sich klar, daß die beiden Graphen wirklich die behaupteten Eigenschaften haben. 404 KAPITEL 14. WEGE UND ZUSAMMENHANG ......................................................................... ............ .......... ......... ........ ........ ....... ....... ....... . . . . . ...... ... . . . . ...... . .... . ..... . . . ... ... . . .... . ... . ... . ... ... . ... .. . ... .. ... . .. ... . ... .... ... ... ... ... . . . . ...... . . . . . . . . . . . . . . . . . . . .. ..... ... ....... ... ..... . . . . ..... ... ............................................................................................ ............................................................................................ ... ... . . . ... ..... ...... ...... ....... . ... ...... ... .............. .... ........ ..... .. .. ... .... ... . .... . . . . ... ..... . ... .. .... ... ... ... .... .... ... ...... ... ... ......... ... ... .. ..... ...... ............. .. .... . ................... ... ....... ..... ..... .. ... ... .................................................... .................................... ... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .... . . . . . .......... . . . . ... . . . . . . ... ... .... .... . . . ... .... .... . . . . . . . . . ... . . . .............. ........... .... ............. . ............. . . . . . ..... . ... . . . .... . . .... .. .... . . . . . . ... . . . ... ... .... . . ... .. . . . . . . . ... ..... . ... ... ... . ... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ... .... ........ .... ....... .. ...... ....... ... ... ... ... .. ... ..... ... .... .... .. ... .... ... ... . .... ..... . . . . . . . . ................ ............. .... ........... .. . ..... . . . ...... ... . . . . . ....... . ........ ....... .......... ....... ......... ............... ............................................... y x w s p u t q nicht f-eulersch v r ......................................................................... ............ .......... ......... ........ ........ ....... ....... ....... . . . . . ...... ... . . . . ...... . .... . ..... . . . .... .... . . .... .. . ... . ... ... . ... .. . ... .. . ... .. ... . .. .... ... ... ... ... . . . . . . ...... . . . . . . . . . . . . . . . . . .. ..... ... ....... ... ..... . . .... . . ........................................................................................... ............................................................................................ . ... ... . .. . ... . ..... ...... ... ..... ........ .. ...................... ... ......... ... .. ... .... ... .... .... ... . . . . ... ..... ... .. .... .. ... ... .... ... ... ... ......... ............... ... .. ........ ... ........ ... ... ... .... ....... .. .... ....... .... ..... ... .. ....... .... ... ................................................... ....................................... ... ..... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . ... ... ... . . .... . . .... ... ... . . . . . . . . . . . ... . . . . .............. ........... ... ............. ............... . . . . . . ..... . ... . . . . .... .. . .... .. .... . . . . . ... . . . .... ... .... . . . ... . . . . . . ... ..... . .... ... ... ... . .. . . . . . . . . . . . . . . . . . . . . . . . . . . ... . .... .... ...... ... ...... ..... ...... ... ... ... ... .. ... .... . ... .... ... . .... ... .. ... .. .. ... ... . . . . . . . . . . . . ................ ............. ............ .... ..... .... ...... ..... . . . . . ....... . ........ ....... .......... ....... ......... ............... ............................................... w s p y x u t q v r f-eulersch Abbildung 14.7: f-eulersche und nicht-f-eulersche Orientierungen (widerspenstiger Fall) Man braucht einen Algorithmus, der in polynomieller Zeit entscheidet, ob ein f-eulerscher Graph vorliegt, und in diesem Fall eine Eulerorientung liefert. Einen solchen Algorithmus gibt es. Er braucht allerdings Ergebnisse aus der Flußtheorie und soll hier nur skizziert werden. Es sei G ein widerspenstiger Graph. Wir versuchen einen Rundfluß in G zu finden. Das ist eine Belegung aller Linien mit einer positiven Zahl und Angabe einer Richtung, so daß in jeden Knoten genau soviel hineinfließt wie aus ihm herausfließt. Gelingt es, eine Rundfluß zu finden, der jeder Linie eine Richtung und den Wert 1 zuweist, so hat man eine Eulerorientierung gefunden. In der Flußtheorie kennt man Algorithmen, die in polynomieller Zeit feststellen, ob ein solcher Rundfluß exitiert, und im positiven Fall die zugehörigen Orientierungen liefern. Siehe die Literaturhinweise auf Seite 544. 14.8.2 Hamiltonwege Eulerwege sind Wege, die alle Linien eines Graphen enthalten. Es liegt nahe, auch Wege zu untersuchen, die alle Knoten eines Graphen durchlaufen. Wenn diese Wege einfach und linieneinfach sind, spricht man von Hamiltonwegen (Hamiltonian path). Definition 14.7 Es sei G ein allgemeiner Graph. Ein einfacher und linieneinfacher aWeg, der jeden Knoten aus G enthält, heißt a-Hamiltonkreis (a-Hamilton circuit), wenn er geschlossen ist, und a-Hamiltonzug (a-Hamilton trail), wenn er offen ist. Ein einfacher und linieneinfacher f-Weg, der jeden Knoten aus G enthält, heißt f-Hamiltonkreis (gerichteter Hamiltonkreis, f-Hamilton circuit, directed Hamilton circuit), wenn er geschlossen ist, und f-Hamiltonzug (gerichteter Hamiltonzug, f-Hamilton trail, directed Hamilton trail), wenn er offen ist. 14.9. DATENSTRUKTUREN FÜR WEGE UND KREISZERLEGUNG 405 Ein allgemeiner Graph heißt a-hamiltonsch (a-Hamiltonian), wenn er einen a-Hamiltonkreis enthält. Ein allgemeiner Graph heißt f-hamiltonsch (f-Hamiltonian), wenn er einen fHamiltonkreis enthält. Trotz der großen Ähnlichkeit der Definitionen 14.6 und 14.7 sind die Probleme bei Hamiltonwegen wesentlich schwieriger als bei Eulerwegen. Es gibt keine einfachen charakterisierenden Eigenschaften von Hamiltonwegen. Die algorithmische Bestimmung aller vier Typen von Hamiltonwegen ist NP-vollständig. Einiges hierzu wurde in Unterabschnitt 6.2.3, Seite 201, erläutert. Siehe auch Abschnitt E.5, Seite 667, im Anhang. Aus diesem Grund werden Hamiltonwege in diesem Buch nicht weiter behandelt. In einigen Sonderfällen lassen sich Hamiltonwege in polynomieller Zeit bestimmen. Siehe Aufgaben 14.13, 14.12 und 14.14. Weitere Anmerkungen zu Hamiltonwegen sind bei den Literaturhinweisen zu finden. 14.9 14.9.1 Datenstrukturen für Wege und Kreiszerlegung Wege als verkettete Listen Naheliegenderweise wird man zur Darstellung von Wegen im Rechner eine verkettete Liste benutzen. Man könnte in der Kette entsprechend Definition 14.1 Knoten und Linien abwechselnd speichern. Das ist jedoch nicht nötig, da die Folge der Linien allein den Weg eindeutig beschreibt. Abbildung 14.8 zeigt eine geeignete Datenstruktur. Ein Satz vom PHDR ? PTHA ? PTHA ? .. . ? PTHA Abbildung 14.8: Darstellung von Wegen Typ PHDR enthält allgemeine Information über den Weg: Weglänge, erste Linie, letzte 406 KAPITEL 14. WEGE UND ZUSAMMENHANG Linie, Wegtyp (a-Weg, f-Weg, b-Weg) und eventuell weitere. Es folgt eine Kette von Sätzen vom Typ PTHA, die den Weg darstellt. Die wesentliche Information in Sätzen vom Typ PTHA ist ein Zeiger auf den zugehörigen Liniensatz (vom Typ EDGE) und Verweise auf zwei Knoten (vom Typ VERTEX), und zwar den ersten Knoten in Wegrichtung und den zweiten Knoten in Wegrichtung der Linie. Zu Knoten- und Liniensätzen siehe Abbildung 13.6, Seite 369. Für Linien, die in einem Wege mehrfach auftreten, sind entsprechend viele PTHA-Sätze vorhanden. 14.9.2 Kreiszerlegung* Als Anwendung dieser Datenstruktur wollen wir die Kreiszerlegung (cycle decomposition) eines Weges bestimmen. Intuitiv ist klar, daß jeder nicht einfache, geschlossene Weg sich in einfache, geschlossene Wege zerlegen läßt. Hilfssatz 14.2 und sein Beweis sind ein erstes Ergebnis in dieser Richtung. Wir wollen Wegzerlegungen dieser Art näher untersuchen und beginnen mit einem Beispiel. Wir betrachten im Digraphen der Abbildung 14.9 den .................. ... ... .... .... .. .................... .......................................... . . . . . . . . . . . . ..... ............... . ......... ...... .............. ................... ..... ...................................... ....... .. .... .. ... . ... . ... ...... ........ ................ ..... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ... . ... ... ... . . ........ ... ... ... .. ....... .. .. .. ....... .... . .... . . . ........................................... ........................................... . ..... ... . . . . ... . . . . . .. . .. .... ............... .... ........ ... ...... ... .......... ......... .......... ............ ....... ... .... ..... ............ ......... ..... ......... ......... ... ... ..... ..... ...... ..... ....... .. ..... ..... ...... ... ..... . .. .......... ..... . . . . . . . . . . . . . . . . . . .. . . ...... ..... ... ........ .......... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ....... .......... ...... ...... .. ...... .. ..... . . ... . .. ......................................................... .... ..... . . ... ...................................................................... . . . . . . ... . ... . . . . . . .. . . ... ........ ... .... ...... .......... .. ............... .............. ............... .... ...... ........................................... . . . . . . . ...... ..... .... . .... .. . . . . . ...... . . . . . . . . ..... . .... ..... .. .... ..... .. ...... .. . ..... ...... ..... ................................. .................................. ...................... ........................ .. .. .. ... ................................................ .. .. ................................................ .... . . . . . . . . . . ... .... .... .... .... .... .... ................ ................ ................ .......... l m o n a k i c g j d b h e f Abbildung 14.9: Kreiszerlegungen eines geschlossenen Weges geschlossenen f-Weg a, b, c, d, e, f, g, h, i, d, j, g, k, l, m, d, n, o, a. Da es in diesem Digraphen keine Mehrfachbögen gibt, reicht es aus, die Folge der Knoten anzugeben. Der Weg ist nicht einfach. Er enthält innere Knoten, die mehrfach auftreten: d und g. Ein Weg, in dem innere Knoten mehrfach auftreten, enthält immer einen geschlossenen einfachen Unterweg, der in einem Mehrfachknoten beginnt. Im betrachteten Weg ist z. B. d, e, f, g, h, i, d ein solcher Unterweg. Wir können diesen Unterweg aus dem Originalweg entfernen und erhalten einen verkürzten geschlossenen Weg. In unserem Beispiel: a, b, c, d, j, g, k, l, m, d, n, o, a. In diesem tritt nur der innere Knoten d mehrfach auf. Es gibt nur einen einfachen geschlossenen Unterweg, der in d beginnt, nämlich d, j, g, k, l, m, d. Wenn wir diesen Unterweg entfernen, bleibt der einfache geschlossene Weg a, b, c, d, n, o, a übrig. Wir haben eine Kreiszerlegung7 des Ausgangsweges gefunden. Die Zerlegung ist nicht eindeutig. Wir hätten als erstes auch den einfachen geschlossenen Weg g, h, i, d, j, g entfernen können Die Bezeichnung ist nicht ganz korrekt, da bei allgemeinen Graphen auch einfache geschlossene Wege der Länge 2 auftreten können, die nicht linieneinfach sind. 7 14.9. DATENSTRUKTUREN FÜR WEGE UND KREISZERLEGUNG 407 und a, b, c, d, e, f, g, k, l, m, d, n, o, a erhalten. Nach Abspaltung von d, e, f, g, k, l, m, d wäre schließlich wieder a, b, c, d, n, o, a übriggeblieben. Im folgenden wird ein Algorithmus vorgestellt, der für jeden Weg eine Zerlegung in einfache Teilwege liefert. Alle Teilwege bis auf den letzten sind geschlossen. Der letzte ist genau dann geschlossen, wenn der Originalweg geschlossen ist. Der Grundgedanke ist, den Weg zu durchlaufen und jeden geschlossenen einfachen Teilweg so früh wie möglich zu identifizieren und abzuspalten. Tabelle 14.6 zeigt den Algorithmus. Er benutzt als zusätzliche Datenstrukturen einen aktuellen Restweg R und eine Menge Q zur Identifikation mehrfach auftretender Knoten. Der Pseudocode ist weitgehend selbsterklärend. Die Angabe “p an R anhängen” in Zeilen 5 bedeutet, daß man den entsprechenden PTHA-Satz an die Kette von R anhängt. In Zeile 7 wird ein Teilweg aus R ausgekettet. Das soll näher erläutert werden. Diese Zeile werden aufgerufen, wenn ein Mehrfachknoten in R erkannt wurde. Es ergibt sich dann zum ersten Mal die in Abbildung 14.10 gezeigte Situation. pi−1 .. ....................................... .. li−1 vi−1 u pj−1 pi .. ..................................... .. li u vi .. ...................................... .. ••• .. ...................................... .. lj−1 vj−1 u pj .. ...................................... .. lj u vj .. ...................................... .. Abbildung 14.10: Ausketten bei Kreiszerlegung Die pv sind PTHA-Sätze, die lv geben die zugehörigen Liniensätze an. Die mit den Linien inzidierenden Knoten sind in Wegreihenfolge angegeben. Der Knoten u tritt in den PTHASätzen pi und pj als Anfangsknoten auf. Der von diesen beiden Stellen begrenzte Teilweg wird als geschlossener einfacher Weg der gesuchten Kreiszerlegung angelegt und einer im Algorithmus nicht explizit aufgeführten Zerlegunsgstruktur hinzugefügt. Der geschlossene einfache Teilweg wird aus den PTHA-Sätzen pi bis einschließlich pj−1 gebildet und ausgekettet. Alle von der Umkettung betroffenen Einträge, die von u verschieden sind, werden in Q gelöscht. Der zu u gehörenden PTHA-Satz wird aktualisiert. pj wird an pi−1 angefügt. pi−1 kann fehlen, dann ist pj das erste Element des neuen Restweges. Anmerkung 14.7 Nicht jeder Mehrfachknoten eines Weges wird durch den Algorithmus PATHDCMP als solcher erkannt. Dann treten diese Knoten in verschiedenen Zerlegungswegen auf. Im Beispiel des Weges zur Abbildung 14.9 wird der Knoten g nicht als Mehrfachknoten erkannt. Er tritt in den Kreisen d, e, f, g, h, i, d und d, j, g, k, l, m, d als innerer Knoten auf. Wenn wir den Graphen der Abbildung durch einen Bogen von a nach g ergänzen und den Weg um den Schritt von a nach g erweitern, tritt g drei Mal als Mehrfachknoten auf und wird immer noch nicht als solcher erkannt. Bei dritten Mal ist g Endknoten des letzten (offenen) Zerlegungswegs a, g. 2 408 ' KAPITEL 14. WEGE UND ZUSAMMENHANG PATHDCMP(∗P ) // // // // // // // // // P ist der zu zerlegende Weg. R ist der anfangs leere Restweg. Q ist eine anfangs leere Menge von Elementen, deren Identifikationsmerkmal eine Knotenname ist und die außerdem einen Verweis auf einen Satz vom Typ PTHA (in R) enthalten. p ist eine Variable vom Typ PTHA. x ist eine Variable vom Typ VERTEX. y ist eine Variable vom Typ VERTEX. 1 p = erste Linie von P ; 2 while (p != NULL) 3 { x = erster Knoten von p; 4 y = zweiter Knoten von p; 5 p an R anhängen; 6 if (Element mit Knotennamen von x in Q vorhanden) 7 { Teilweg aus R ausketten; 8 Verweis auf PTHA-Satz aktualisieren; 9 } 10 else 11 { Element für x in Q anlegen; 12 // Verweis zeigt auf PTHA-Satz in R 13 } 14 p = Nachfolger von p; 15 } 16 return; // Falls R leer, war der Originalweg geschlossen. //Anderenfalls verbeibt ein offener einfacher Weg. & $ % Tabelle 14.6: Algorithmus PATHDCMP zum Finden der Kreiszerlegung eines Weges Aufgaben Aufgabe 14.1 a. In einem allgemeinen Graphen sei Pn ein a-Kreis der Länge n ≥ 3 und Cn sein Träger. Es sind alle auf Cn möglichen a-Kreise und f-Kreise zu bestimmen. b. Welche Eigenschaften charakterisieren die Untergraphen Cn eines schlichten Graphen, 14.9. DATENSTRUKTUREN FÜR WEGE UND KREISZERLEGUNG 409 die Träger eines a-Kreises sein können? Aufgabe 14.2 Man beweise Hilfssatz 14.1 (Seite 375): 1. Jeder offene Weg von u nach v einhält einen linieneinfachen Weg von u nach v. 2. Jeder linieneinfache offene Weg von u nach v einhält einen einfachen Weg von u nach v. 3. Jeder einfache offene Weg ist linieneinfach. Aufgabe 14.3 Beweisen Sie Proposition 14.1, Seite 380: Enthält eine starke Zusammenhangskomponente einen Bogen, so enthält sie auch einen f-Kreis Aufgabe 14.4 In welcher Form ist Proposition 14.9 auf den Graphen der Abbildung 14.11 anwendbar? Was ändert sich, wenn der Bogen von u nach v durch eine Kante ersetzt wird? .... ........... ...... ........ .... ..... ..... .. .... .. .. ... .. ...................... ............................... . . . ..... . .. ..... ....... .... ...... . . ..... . . . . ... ...... ..... . .... .. ..... ... ........ ......... ..... ..... ..... .... ................................. ................................ ...................... . . . .. ... . . . .. .. ... . . .. . ..................................................................... .... .... ... . . . .. . . . . . . . . . ... . . ....... ............... ..................... . . ................... . . . . . . . . . . . . . . . . ... . . . .......... ... ... . . . . . . . . . . . ... . . . . ...... .. . .. ...... .. ... .. .......... ..... . .... ...... ....... .......... .......... .......... ........... ............. .. . .... ......... ... ... ..... ..... .... ..... .... ..... .......... .......... u v Abbildung 14.11: Kanten auf f-Kreisen Aufgabe 14.5 a.Jeder freie Baum mit mindestens zwei Knoten hat mindestens zwei Knoten vom Grad 1. b. Wie hängt die Anzahl Knoten vom Grad 1 von der Struktur des Baumes ab? Aufgabe 14.6 Die Bedingung |E ∪A| = |V |−1 reicht nicht aus, um einen allgemeinen Graphen zu einem a-Baum zu machen. Wieso? Aufgabe 14.7 Zeigen Sie daß Binärbäume, wie sie in Unterabschnitt 9.1.2 eingeführt wurden, gerichtete f-Bäume im Sinne von Unterabschnitt 14.4.3 sind. Aufgabe 14.8 Weist der vollständige Graph K18 (siehe Seite 350) einen Eulerkreis auf? Einen Hamiltonkreis? 410 KAPITEL 14. WEGE UND ZUSAMMENHANG Aufgabe 14.9 Geben sie einen allgemeinen Graphen an, der unter Fall 3, Seite 403, fällt und bei passenden Orientierungen von Kanten f-eulersch oder nicht f-eulersch wird. Aufgabe 14.10 Was kann man zu a-Eulerzügen und f-Eulerzügen in allgemeinen Graphen sagen ? Aufgabe 14.11 Man zeige: Jeder allgemeine Graph kann mit einem geschlossenen Weg so durchlaufen werden, daß jede Linie genau zweimal vorkommt. Darüber hinaus wird jede Kante je einnmal in jeder Richtung und jeder Bogen genau einmal in f-Richtung und einmal in b-Richtung durchlaufen. Diese Eigenschaft ist unter dem Begriff double tracing bekannt. Aufgabe 14.12 Untersuchen Sie die Struktur von Digraphen, in denen kein Knoten einen Eingangsgrad größer 1 aufweist. Geben Sie einen polynomiellen Algorithmus an, der diese Struktur aufdeckt. Wann ist ein solcher Digraph f-hamiltonsch? Was läßt sich über Digraphen sagen, bei denen der maximale Ausgangsgrad 1 ist? Aufgabe 14.13 Es sei ein allgemeiner Graph gegeben, in dem alle Knoten den Gesamtgrad 2 aufweisen. Ist der Graph stets a-hamiltonsch? Wenn nein, geben Sie einen polynomiellen Algorithmus an, der feststellt, ob der Graph a-hamiltonsch ist, und gegebenenfalls einen a-Hamiltonkreis findet. Aufgabe 14.14 Untersuchen Sie die f-kreisfreien, schwach zusammenhängenden allgemeinen Graphen, in denen es einen f-Hamiltonzug gibt. Geben Sie einen Algorithmus an der, alle f-Hamiltonwege findet bzw. meldet, daß es keine gibt. Was kann man zur Komplexität des Algorithmus sagen? Hinweise: Für den anzugebenden Algorithmus kann angenommen werden, daß ein Algorithmus gegeben ist, der für einen schwach zusammenhängenden allgemeinen Graphen die starken Zusammenhangskomponenten, den externen Dag und die Schichtennummern in polynomieller Zeit liefert. Als Beispiel kann Algorithus STRONGCOMP aus Kapitel 15 dienen. Aufgabe 14.15 (Bestimmung von Wegen) Einige einfache graphentheoretische Eigenschaften sind der Adjazenzmatrix eines Digraphen (ohne Mehrfachbögen) unmittelbar anzusehen. So kann man zum Beispiel den Eingangsgrad eines Knoten als Anzahl Einsen in „seiner“ Spalte ablesen oder Schlingen als 14.9. DATENSTRUKTUREN FÜR WEGE UND KREISZERLEGUNG 411 Einsen in der Diagonale erkennen. Andere Eigenschaften sind nicht offensichtlich und erfordern Rechenschritte mit der Adjazenzmatrix. Der folgende Satz ist ein Beispiel dafür. Satz : Es sei D = (V, A) ein Digraph und M seine Adjazenmatrix. Dann gibt das Element (k) mij der Matrix M k für k = 1, 2, 3, . . . die Anzahl f-Wege der Länge k an, die von Knoten i zu Knoten j führen. a. Beweisen Sie den Satz. b. Wie kann man feststellen, ob es überhaupt einen f-Weg von Knoten i zu Knoten j gibt? c. Wie kann man feststellen, ob der Digraph stark zusammenhängend ist? d. Was läßt sich zu ungerichteten Graphen (ohne Mehrfachkanten) und ihren Adjazenzmatrizen sagen? Literatur Wege und Zusammenhangskomponenten gehören zu den wesentlichen Grundbegriffen der Graphentheorie und sind in allen Büchern über Graphentheorie beschrieben. Es sei deshalb hier noch einmal auf die zu Abschnitt 12.1 angegeben Literatur, Seite 360, hingewiesen. Die Bezeichnungen sind uneinheitlich. Im Englischen wird path oft im Sinne von einfacher Weg gebraucht. Manche Autoren definieren Wege als Graphen, also durch ihren Träger, und nicht als Abbildungen, z. B. Diestel [Dies2000]. Wir benutzen die in der Informatik üblichen Bezeichnungen, siehe Cormen/Leiserson/Rivest [CormLR1990]. Die explizite Einführung von f-Wegen, b-Wegen und a-Wegen in allgemeinen Graphen und ihre systematische Anwendung auf die Definition von schwachen und starken Zusammenhangskomponenten ist in dieser Form neu. Für Digraphen wurde ähnliches im Bericht [Stie2001c] beschrieben. Gerichtete Bäume, insbesondere solche, bei denen auf der Menge der Nachfolger eines Knotens eine Ordnung definiert ist, sind in der Informatik wichtige Datenstrukturen. Sie werden intensiv als Suchbäume genutzt. Siehe Kapitel 9, Seite 257. Eulerwege wurden von Leonhard Euler 1736 zur Lösung des „Königsberger-BrückenProblems“ eingeführt. Es besteht darin, alle Pregelbrücken des damaligen Königsberg bei einem Spaziergang genau einmal zu überqueren. Eine Ansicht der Stadt Königsberg in jener Zeit findet man bei Diestel [Dies2000]. Zur graphentheoretischen Formulierung des Problems siehe auch http://www.jcu.edu/math/vignettes/bridges.htm. Eulerwege für ungerichtete Graphen und Digraphen werden in allen Büchern über Graphentheorie behandelt, meistens kurz. Eine ausführliche Darstellung einschließlich Algorithmen ist in [Volk1996] zu finden. Zu Algorithmen für Eulerwege siehe auch [Sedg2002] und [McHu1990]. Algorithmen zur Bestimmung von f-Eulerwegen in allgemeinen Graphgen sind in den Lehrbüchern i.a. nicht zu finden. Das mag an dem allgemeinen Desinteresse an „gemischten“ Graphen liegen. Eine polynomielle Lösung des Problems haben Ford und 412 KAPITEL 14. WEGE UND ZUSAMMENHANG Fulkerson schon 1962 in ihrem Lehrbuch [FordF1962] angegeben. Hamiltonwege sind ein schwieriges Kapitel der Graphentheorie. Gute und anwendbare charakterisierende Eigenschaften sind nicht bekannt. Oftmals sind charktrisierende Eigenschafte von der Form „Genau dann, wenn dieser Graph einen Hamiltonweg besitzt, hat auch jener einen“. In Diestel [Dies2000] und Volkmann [Volk1996] sind Hamiltonwegen eigene Kapitel gewidmet. Kapitel 15 Tiefensuche und Breitensuche Es ist bei Algorithmen auf Graphen immer wieder nötig, alle Knoten nacheinander zu untersuchen und zu bearbeiten. Man spricht davon, daß die Knoten besucht (visit) werden. In den meisten Fällen werden dann alle Linien, mit denen der Knoten inzidiert, auch besucht. Das ist möglich, indem man die rechnerinterne Darstellung benutzt, bei Inzidenzlisten z. B. die Knotenliste und für jeden Knoten die Liste seiner Inzidenzsätze durchläuft. In vielen Fällen ist es jedoch zweckmäßiger, die Struktur des Graphen in den vollständigen Durchlauf einzubeziehen. Die beiden wichtigsten Methoden hierfür sind Tiefensuche und Breitensuche. Beide Methoden werden auf vielfältige Weise angewendet, zum Teil auch ohne kompletten Durchlauf durch den Graphen. 15.1 Tiefensuche Es sei G(V, E, A, ϕ, ψ) ein allgemeiner Graph und v einer seiner Knoten. Wir wollen in ihm drei Formen von Tiefensuche mit Startpunkt v untersuchen. a. Die Prozedur f-DFS in Tabelle 15.1 definiert f-Tiefensuche (Tiefensuche in Vorwärtsrichtung, f-depth-first search). b. Ersetzt man in Zeile 4 die Ausgangsbögen durch die Eingangsbögen, so wird b-Tiefensuche (Tiefensuche in Rückwärtsrichtung, b-depth-first search) definiert. Diese Prozedur soll bDFS heißen. c. Erweitert man die Prozedur so, daß Ausgangs- und Eingangsbögen berücksichtigt werden, so erhält man die Prozedur a-DFS, mit der a-Tiefensuche (Tiefensuche in beliebiger Richtung, a-depth-first search) definiert wird. Tiefensuche ist leicht zu programmieren und für Listendarstellungen gut geignet. Tiefensuche versucht, einen einfachen Weg so weit wie möglich fortzusetzen. Sie kehrt erst um, wenn ein Knoten erreicht wird, von dem es nicht mehr weitergeht, weil es entweder keine Knoten gibt, mit denen man fortsetzen könnte, oder weil all diese Knoten schon markiert sind. 413 414 ' KAPITEL 15. TIEFENSUCHE UND BREITENSUCHE f-DFS(v) 1 2 3 4 5 6 7 8 9 10 & if (v markiert) return; markiere v; setze v aktiv; for (alle Kanten und Ausgangsbögen l, die mit v inzidieren) { if (l nicht markiert) { markiere l; f-DFS (otherend(l, v)); } } setze v inaktiv; return; $ % Tabelle 15.1: Prozedur f-DFS für f-Tiefensuche in einem allgemeinen Graphen f-Tiefensuche soll jetzt genauer untersucht werden. Sie arbeitet mit Markierungen für Knoten und Markierungen für Linien. Wir sagen, ein Knoten v werde besucht, wenn für ihn in einer Rekursionsstufe von f-DFS Zeile 1 ausgeführt wird. Ein Knoten kann gar nicht, genau einmal oder mehrmals besucht werden. Nur wenn er zum ersten Mal besucht wird und zu diesem Zeitpunkt nicht markiert ist, wird er bearbeitet, d. h. werden die weiteren Anweisungen von f-DFS ausgeführt. Nur in diesem Fall wird er aktiviert (activated). Nach der Bearbeitung aller seiner Kanten und Ausgangsbögen wird er deaktiviert und seine gesamte Bearbeitung beendet. Anmerkung 15.1 In Tabelle 15.1 ist Zeile 4 so zu verstehen, daß die Kanten und Ausgangsbögen in beliebiger Reihenfolge drankommen. Welche Reihenfolge wirklich auftritt, hängt von der internen Darstellung von Knotenliste und Inzidenzlisten ab. Z. B. können die Kanten vor den Ausgangsbögen bearbeitet werden. Das muß aber nicht so sein, sondern auch jede beliebige Mischung ist zugelassen. Jede Festlegung der Reihenfolge von Knoten und Inzidenzen bestimmt einen Ablauf der Tiefensuche. Für b-Tiefensuche und a-Tiefensuche gelten die gleichen Feststellungen. 2 Für weitere Überlegungen betten wir f-DFS in den Rahmenablauf f-DFSgesamt ein. Siehe dazu Tabelle 15.2. b-DFSgesamt und a-DFSgesamt ergeben sich durch die entsprechenden Änderungen in Zeile 3. Zu Anfang sind alle Knoten unmarkiert und inaktiv. Auch alle Linien sind unmarkiert. Am Ende ist jeder Knoten markiert. f-DFSgesamt und f-DFS geben das Muster an, nach dem die meisten Anwendungen von Tiefensuche aufgebaut sind. Allerdings gibt es Abweichungen. So wird z. B. bei der Bestimmung von Bipartitionsmengen, Seite 354, die Bearbeitung und Markierung eines neuen, d. h. nicht über 15.1. TIEFENSUCHE ' & 415 $ f-DFSgesamt(v) 1 2 3 4 for (alle Knoten v aus V ) /* Knotenliste */ { if (v nicht markiert) { f-DFS (v); } } % Tabelle 15.2: Rahmenablauf einer vollständigen f-Tiefensuche in einem allgemeinen Graphen BPT gefundenen unmarkierten Knotens im Rahmenablauf BIPART und nicht in der rekursiven Prozedur BPT ausgeführt. Der Grund ist, daß sich die Menge, in der der Knoten eingefügt wird, nicht durch einen schon bearbeiteten Nachbarn bestimmen läßt, sondern frei gewählt werden kann. Beispiel 15.1 Am Beispiel von Graph3, Abbildung 15.1, soll der Ablauf einer f-Tiefensuche .......... ..... ...... . ... ......... .................. ........... .............. . . . ... ... ...... . ... .. ... ..... ..... ................. . . ..... .... 7..... ... 4..... .... 1.... ........... . . ............... . ... . . . . . . . . . . . . ... .... ... ... ... ... ... ... .... ... .... . . .... ..... . . .... . . ... ... ... . ... ... ... ... ... ... .. ... ... ... ... ... ... .. ... ... ... ... ... ... ... ... ... ... . . ... . . ..... ... . . . ... ... .. .. ... . . ... ... .. . . ... . ... . .. ... . . ... . . ... ... ... . . ... . . ... .. . . . . . . ... ... .. .. . . . ... ..... ... . . ......... ........ . ... .. ... .. ... . .. . . . . ....... ........ ....... .............. .... .... . .... . . . . . . . . .................. .............. . .................... . . . .... .... . . . . . .. .. ........ ... ......... ... ..... ... . ........................................... .. ...................................... ... 8.... ... 3..... ........ .. .. .... 5...... ...... ............... ............ ..... ..... .... .... ... ... ..... .. . ... ... . ... .. ... ... ..... ... ... ... ... ... .... ... ... ... ... ... ... ... ... ... .. . . ... . . ... ... ... .. ... . . . . ... ... . .. ... . . . . . ... ... ... . ... . . . . .. ... ... .. . ... . . .... . ... ... .. ... . . . ... .......... ........ . ... ... . . . . ....... ...... ... ....... ... . . . . . . . . . . . . ... ................ ... ... .. ..................... ... ........................ .. .. .... . ........ ..... 2.... . .. ..... 6 ............. ........... v v v v v v v v Abbildung 15.1: Graph3 detailliert durchgespielt werden. Die Bezeichnung der Linien folgt den Regeln, die in Abbildung 14.1, Seite 377, angegeben sind. Der Graph enthält die Kanten uv3 v4 und uv3 v6 , alles andere sind Bögen. Der Ablauf der f-Tiefensuche hängt von der Reihenfolge der Knoten in der Knotenliste und der Reihenfolge der Linien eines jeden Knoten in den entsprechenden Inzidenzlisten ab. Für Graph3 mögen die in Abbildung 15.2 dargestellte Knotenliste und zugehörigen Inzidenzlisten vorliegen. Siehe auch die Schemadarstellung in Abbildung 13.6, Seite 369. 416 KAPITEL 15. TIEFENSUCHE UND BREITENSUCHE v3 ... ... ... ... .. ... ... ... ... ... ... ....... .. ...... ... v2 ... ... ... ... ... .. ... ... ... ... ... ....... .. ...... ... v6 ... ... ... ... ... ... ... ... ... ... .. ....... .. ...... ... v5 ... ... ... ... ... ... .. ... ... ... ... ....... .. ...... ... v8 ... ... ... ... ... ... ... .. ... ... ... ....... .. ...... ... v4 ... ... ... ... ... ... ... ... ... ... .. ....... .. ...... ... v7 ..... .............. ... . ... . . . ... .. ............................... ... .. ... ... ... . .............. ...... . ... ... ... ... ... . .............. ...... . ..... ............... .. . ... . . . ... ..... ... ... ... ... . .............. ...... . .. .............................. ... .. ... ... ... . .............. ...... . .. .............................. ... .. ... ... ... . .............. ...... ..... ............. .. . ... . . . ... .. ................................ ... .. ... ... ... . .............. ...... .. ............................ .. uv3 v4 .. ............................ .. uv3 v6 .. ............................ .. dv1 v3 dv3 v2 dv5 v3 ....... dv1 v2 b ......................... dv3 v2 .. ............................. .. dv1 v2 a .. ............................. .. dv8 v5 uv3 v6 dv5 v6 dv5 v6 .. ............................ .. dv5 v3 dv4 v5 .. ............................ .. dv7 v5 .. ............................ .. dv4 v5 .. ............................ .. dv7 v5 dv8 v5 dv7 v8 uv3 v4 dv4 v4 dv4 v4 dv7 v8 ... ... ... ... ... ... ... ... .. ... ... ......... ...... ... v1 .. ............................ .. dv1 v2 a ................................ dv1 v2 b ................................. dv1 v3 Abbildung 15.2: Knotenliste und Inzidenzlisten zu Graph3 15.1. TIEFENSUCHE 417 Die Knotenliste beginnt mit v3 und endet mit v1 . Zu den Knoten sind die Liste der Kanten, die Liste der Ausgangsbögen und die Liste der Eingangsbögen – in dieser Reihenfolge – angegeben. Leere Listen sind nicht eingezeichnet. Das Ablaufprotokoll der f-Tiefensuche ist in Tabelle 15.3 zu sehen. Die Spalten der Tabelle geben aufeinanderfolgende Rekursionsstufen von f-DFS an, die erste Stufe wird von f-DFSgesamt aufgerufen. Übergänge zur nächsten Zeile bedeuten Eintritt in die nächste bzw. Rückkehr zur vorangegangenen Rekursionsstufe. Knoten oder Linien, die zum ersten Mal besucht und dann markiert und bearbeitet werden, sind in der Tabelle in Normalschrift geschrieben. Knoten werden an dieser Stelle aktiviert, z. B. Knoten v3 in Zeile 1, Knoten v6 in Zeile 7, Knoten v2 in Zeile 16. Aktivierung wird durch Umrahmung hervorgehoben. Knoten oder Linien, die bei einem Besuch markiert angetroffen und nicht weiter berücksichtigt werden, stehen in Kursivschrift. Knoten werden in der letzten Zeile ihrer Bearbeitung deaktiviert, z. B. Knoten v3 in Zeile 18. Auch Deaktivierung wird durch Umrahmung hervorgehoben. Die f-Tiefensuche kann auf einen markierten Knoten innerhalb seines Aktivitätsintervalls (z. B. Knoten v4 in Zeile 4, Knoten v3 in Zeile 7) oder außerhalb desselben (z. B. Knoten v5 in Zeile 20, Knoten v3 in Zeile 32) treffen. 2 Der folgende Satz ist von zentraler Bedeutung für die Tiefensuche. Satz 15.1 Es sei G(V, E, A, ϕ, ψ) ein allgemeiner Graph und v ∈ V . Sind anfangs alle Knoten unmarkiert, so besucht f-DFS(v) den Knoten v und alle von v f-erreichbaren Knoten und nur diese. Beweis: v wird besucht. Es sei u 6= v ein Knoten, der von v auf dem f-Wege v = v0 , l1 , v1 , . . . , vk−1 , lk , vk = u erreichbar ist. Anfangs sind die Knoten des Weges unmarkiert. Induktion: Jeder Knoten des Weges wird besucht. Für v1 passiert das spätestens über l1 . Wird vi besucht, so wird auch vi+1 besucht, nämlich spätestens über li+1 . u wird also besucht. Angenommen, es werde u 6= v besucht. Wir betrachten den Erstbesuch von u. Dieser geschieht in der zweiten oder einer späteren Aufrufstufe von f-DFS. Dann gibt es in der Vorgängerstufe einen eindeutig bestimmten Knoten w und eine eindeutig bestimmte Linie von w zu u. Ist w 6= v, so hat w selber einen eindeutig bestimmten Vorgänger für den Erstbesuch und wird von diesem auf einer eindeutig bestimmten Linie erreicht. Nach endlich vielen Schritten (es gibt nur endlich viele Rekursionsstufen!) hat man so einen b-Weg von u zu v gefunden. D. h. u ist von v f-erreichbar. 2 Satz 15.1 und sein Beweis gelten entsprechend auch für b-Tiefensuche und a-Tiefensuche. 418 KAPITEL 15. TIEFENSUCHE UND BREITENSUCHE 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 1. Stufe v3 uv3 v4 2. Stufe v4 3. Stufe 4. Stufe 5. Stufe uv3 v4 dv4 v4 v4 dv4 v5 v5 dv5 v6 v6 uv3 v6 v3 v6 dv5 v3 v3 v5 v4 uv3 v6 dv3 v2 v2 v2 v3 v2 v6 v5 v8 dv8 v5 v5 v8 v7 dv7 v8 v8 dv7 v5 v5 v7 v1 dv1 v2 a v2 dv1 v2 b v2 dv1 v3 v3 v1 Tabelle 15.3: Ablaufprotokoll einer f-Tiefensuche 15.2. TIEFENSUCHBÄUME 419 Ein Knoten u kann durchaus mehrfach besucht werden; markiert und bearbeitet wird er nur beim ersten Besuch. Was passiert, wenn es anfangs, also beim Aufruf von f-DFS (v), Knoten gibt, die schon markiert sind? Ist v selbst markiert, so wird kein weiterer Knoten besucht. Ist v unmarkiert, so gilt die folgende Proposition. Proposition 15.1 Es sei G(V, E, A, ϕ, ψ) ein allgemeiner Graph und v ein unmarkierter Knoten. Dann werden durch den Aufruf f-DFS(v) genau die Knoten u besucht, für die es zum Zeitpunkt des Aufrufs einen f-Weg von v nach u gibt, bei dem alle von u verschiedenen Knoten unmarkiert sind. Beweis: Nach dem Muster des Beweises von Satz 15.1. Man beachte dabei, daß auf dem b-Weg, der im zweiten Teil des Beweises gefunden wird, alle Knoten außer u anfangs unmarkiert sein müssen. 2 In entsprechender Abwandlung gilt Proposition 15.1 auch für b-DFS und a-DFS. Als Beispiel soll wieder auf Graph3 und den Ablauf in Tabelle 15.3 herangezogen werden. Beim Aufruf von f-DFS (v4 ) gibt es den markierten Knoten v3 . Die Knoten v5 , v6 und v3 werden bei diesem Aufruf besucht. Die ersten beiden Knoten sind beim Aufruf unmarkiert, der dritte ist markiert. Es ist auch möglich, daß ein f-erreichbarer und nicht markierter Knoten nicht besucht wird, weil es zu ihm keinen Weg mit unmarkierten Knoten gibt, er also von v durch markierte Knoten getrennt wird. Ein Beispiel dafür ist in Tabelle 15.3 Knoten v2 beim Aufruf von f-DF S(v5 ). Tiefensuche ist ein effizienter Algorithmus, wie die folgende Proposition zeigt. Proposition 15.2 Tiefensuche in Graphen erfordert O(|V | + |E| + |A|) Zeitschritte. Beweis: Beim Durchlauf durch die Knotenliste wird jeder Knoten genau einmal angesprochen. Bei der Bearbeitung eines Knotens werde alle mit ihm inzidenten Linien höchstens einmal bearbeitet, jede Linie also insgesamt höchstens zweimal. Wegen der Markierung wird jeder Knoten von f-DFSgesamt genau einmal bearbeitet. Die Behauptung ergibt sich dann durch die Tatsache, daß die Bearbeitung eines Knotens und die Bearbeitung einer Linie durch Konstanten beschränkt sind. 2 15.2 Tiefensuchbäume Beim Ablauf einer f-Tiefensuche (a-Tiefensuche, b-Tiefensuche) ergeben sich charakteristische Eigenschaften von Linien und bestimmte Bäume. Die Linien, mit denen bei der Tiefensuche von einem Knoten zu einem unmarkierten Knoten und damit zur nächsten Rekursionsstufe übergegangen wird, heißen Baumbögen (tree arc). Diese Bezeichnung soll unabhängig davon benutzt werden, ob es sich um eine Kante oder einen Bogen handelt und in welcher Richtung der Bogen bei dem Übergang durchlaufen wird. Mit Bezug auf 420 KAPITEL 15. TIEFENSUCHE UND BREITENSUCHE den Zielknoten eines Baumbogens wird dieser auch Eintrittsbogen (entry arc) genannt, da über ihn der Knoten zum ersten Mal besucht wird und die Bearbeitung beginnt. Es gilt der folgende Hilfssatz. Hilfssatz 15.1 Es sei G(V, E, A, ϕ, ψ) ein allgemeiner Graph und v ∈ V . Sind anfangs alle Knoten unmarkiert, so bilden die Baumbögen von f-DF S(v) (b-DF S(v), a-DF S(v)) einen gerichteten Baum mit Wurzel v. Beweis: Nach den obigen Festlegungen handelt es sich um einen Digraphen. Besteht er nur aus einem isolierten Knoten, ist die Behauptung richtig. Andernfalls ergibt sich aus ähnlichen Überlegungen wie beim Beweis von Satz 15.1, daß er f-kreisfrei ist, jeder Knoten höchstens einen Vorgänger hat und jeder von v verschiedene Knoten von v f-erreichbar ist. Nach Satz 14.6, Seite 387, ist der Digraph dann ein gerichteter Baum. 2 Wir starten nun mit einem vollständig unmarkierten allgemeinen Graphen. Nach dem Aufruf von f-DFS mit dem ersten Knoten der Knotenliste ergibt sich ein gerichteter Baum. Bleiben Knoten übrig, die nicht zum Baum gehören, so erzeugen diese einen Untergraphen, zu dem auch der erste unmarkierte Knoten der Knotenliste gehört. Wir betrachten den Untergraphen als selbständigen, unmarkierten Graphen und starten auf ihm im ersten unmarkierten Knoten der Knotenliste erneut f-DFS. Wir erhalten wieder einen gerichteten Baum. Wir fahren so fort, bis kein Knoten mehr übrig bleibt, und erhalten eine Zerlegung der ursprünglichen Knotenmenge in Knoten, die zu gerichteten Bäumen gehören. Wir erhalten auch eine entsprechende Zerlegung der Baumbögen. Die so erhaltenen gerichteten Bäume und den so erhaltenen gerichteten Wald wollen wir die zum Ablauf der Tiefensuche gehörenden Tiefensuchbäume (depth-first search tree) bzw. Tiefensuchwald (depth-first search forest) nennen. Es ist leicht zu sehen, daß sich alle Ergebnisse auf b-Tiefensuche und a-Tiefensuche übertragen lassen. Ebenso sieht man leicht, daß der entstehende Tiefensuchwald vom Ablauf der Tiefensuche abhängt. Siehe dazu Aufgabe 15.2. Eine Linie mit der von einem Knoten zu einem markierten und aktiven Knoten übergegangen wird, heißt Rückwärtsbogen (backward arc). Ein Rückwärtsbogen verbindet stets zwei Knoten des gleichen Tiefensuchbaums. Vom aktiven Zielknoten des Rückwärtsbogens zu seinem Startknoten gibt es einen einfachen Weg. Dieser wird mit dem Rückwärtsbogen, so geschlossen, daß er linieneinfach bleibt. Wir haben also das folgende Ergebnis. Hilfssatz 15.2 Bei dem Ablauf einer f-Tiefensuche schließt jeder Rückwärtsbogen einen f-Kreis durch Start- und Zielpunkt des Bogens. Für b-Tiefensuche und a-Tiefensuche gilt der Hilfssatz entsprechend. Ein Bogen, der bei dem Ablauf einer f-Tiefensuche von einem Knoten zu einem markierten inaktiven Knoten führt, heißt Vorwärtsbogen (forward arc), wenn er zu einem Knoten des gleichen Tiefensuchbaumes führt. Er heißt Querbogen (cross arc), wenn er zu einem 15.2. TIEFENSUCHBÄUME 421 Knoten eines anderen Tiefensuchbaumes führt. Achtung: Die hier gegebene Definiton für Vorwärtsbogen und Querbogen weicht von der sonst üblichen ab. Siehe z. B. Cormen/Leiserson/Rivest [CormLR1990]. Hilfssatz 15.3 Eine Kante kann niemals Vorwärtsbogen oder Querbogen sein. Beweis: Es werde bei der Tiefensuche der Knoten v bearbeitet. e sei eine mit v inzidente Kante. Wird bei der Tiefensuche v über e erreicht, so ist e ein Baumbogen. Das gleiche gilt, wenn über e ein unmarkierter Nachbar von v erreicht wird. Bleibt der Fall, daß e zu einem markierten Nachbarn u führt. Dessen Bearbeitung kann aber nicht beendet sein, denn sonst wäre die Kante e von u nach v durchlaufen worden, also doch Eingangsbogen für v. Also ist u aktiv und e ein Rückwärtsbogen. 2 Aus Hilfssatz 15.3 ergibt sich unmittelbar das folgende Korollar. Korollar. Bei a-Tiefensuche gibt es keine Vorwärtsbögen und keine Querbögen. Beispiel 15.2 Dieses Beispiel soll zeigen, daß es vom Ablauf einer f-Tiefensuche abhängt, ob ein f-Kreis nach Hilfssatz 15.2 gefunden wird oder nicht. Wir betrachten dazu Abbildung 15.3 und starten eine f-Tiefensuche in v0 . Wird in v2 zuerst der Bogen durchlaufen, ............................................................................................. ............. ............... ............... ............... ............... ................ ................ ... .. ... .. ... .. ... .. .... .. .. .. ... ........................................ ..... ... ................................................. .................................................. ................................................. ................................................ .... ..... . . . . . . . . . . . . . . . . .. ... 3.... .. ... 4..... .. ... 5..... .... 1... .... 2... ..... 0.... ............ ............ ............ .......... .......... ........ v v v v v v Abbildung 15.3: f-Tiefensuche findet nicht jeden f-Kreis so wird in v4 die Kante als Rückwärtsbogen eingeordnet und der f-Kreis erkannt. Wird jedoch in v2 zuerst die Kante durchlaufen, so erstreckt sich die Tiefensuche bis v5 , kehrt dann zu v2 zurück, fährt mit dem Bogen dv2 v3 fort und ordnet als letztes den Bogen dv3 v4 als Vorwärtsbogen ein. 2 Allerdings führt bei a-Tiefensuche oder bei Digraphen die Existenz eines Kreises unabhängig vom Ablauf der Tiefensuche immer zu einem Rückwärtsbogen. Es gilt nämlich der folgende Satz. Satz 15.2 1. Ein allgemeiner Graph enthält genau dann einen a-Kreis, wenn jeder Ablauf einer a-Tiefensuche einen Rückwärtsbogen findet. 2. Ein Digraph enthält genau dann einen f-Kreis, wenn jeder Ablauf einer f-Tiefensuche einen Rückwärtsbogen findet. 422 KAPITEL 15. TIEFENSUCHE UND BREITENSUCHE Beweis: Nach Hilfssatz 15.2 schließt jeder a-Rückwärtsbogen (f-Rückwärtsbogen) einen a-Kreis (einen f-Kreis). Es sei v0 , l1 , v1 , l2 , . . . , lk−1, vk−1 , lk , vk = v0 ein a-Kreis (bzw. f-Kreis). Bei einem Ablauf einer a-Tiefensuche wird einer der Knoten des Kreises als erster markiert. Ohne Beschränkung der Allgemeinheit können wir annehmen, daß v0 dieser Knoten ist. Ähnlich wie beim Beweis von Satz 15.1 zeigt man, daß alle anderen Knoten des Kreises besucht und bearbeitet werden, bevor die Bearbeitung von v0 beendet ist. Bei einer a-Tiefensuche können alle l in beliebiger Richtung durchlaufen werden. Sind l1 , l2 , . . . , lk−1 Baumbögen, so muß lk ein Rückwärtsbogen sein. Andernfalls sei li mit 1 ≤ i ≤ k − 1 das kleinste i, für das li kein Baumbogen ist. Dann muß es nach dem Korollar zu Hilfssatz 15.3 ein Rückwärtsbogen sein. Ist der Graph ein Digraph, auf den eine f-Tiefensuche angewandt wird, so wird die Bearbeitung von vk−1 vor der von v0 beendet und lk muß ein Rückwärtsbogen sein. 2 Der Satz gilt auch für b-Kreise in Digraphen, denn jeder f-Kreis ist in umgekehrter Richtung ein b-Kreis. Für stark zusammenhängende allgemeine Graphen gilt der folgenden Satz. Satz 15.3 Es sei G ein stark zusammenhängender allgemeiner Graph. 1. Kein Ablauf einer f-Tiefensuche in G liefert einen Querbogen. 2. Es gibt in G genau dann einen a-Kreis, wenn es einen f-Kreis gibt. Das ist genau dann der Fall, wenn jeder Ablauf einer f-Tiefensuche auf einen markierten Knoten trifft. Beweis: 1. In G ist jeder Knoten von jedem f-erreichbar. Jede f-Tiefensuche liefert also nur einen einzigen Tiefensuchbaum. Die Wurzel ist der Startknoten der Tiefensuche. Es kann keine Querbögen geben. 2.a Nach Satz 14.4, Seite 380, enthält der Graph G genau dann einen a-Kreis, wenn er einen f-Kreis enthält. Es laufe in G eine Tiefensuche ab. Trifft sie einen markierten Knoten, so gibt es einen Rückwärtsbogen oder einen Vorwärtsbogen. In beiden Fällen gibt es einen a-Kreis. b. Es gebe in G einen a-Kreis. Es soll gezeigt werden, daß dann jede f-Tiefensuche einen markierten Knoten trifft. Ist der a-Kreis eine Schlinge, so ist das offenbar richtig. Andernfalls sei l die letzte Kante / der letzte Bogen des Kreises, den die Tiefensuche bearbeitet. Nehmen wir an, die Bearbeitung führt von Knoten u zu Knoten v. Mit v inzidiert auch noch eine zweite Kante / ein zweiter Bogen k des Kreises. k ist von l verschieden und wurde vor l von der Tiefensuche bearbeitet. k verbindet v mit w, wobei u = w zugelassen ist. v wurde von der Tiefensuche erreicht, bevor l erreicht wurde. Die Bearbeitung von l führt auf jeden Fall zu einem markierten Knoten. 2 15.3. BESTIMMUNG SCHWACHER ZUSAMMENHANGSKOMPONENTEN 15.3 423 Bestimmung schwacher Zusammenhangskomponenten Wir haben gesehen, daß wichtige Eigenschaften der Tiefensuche ablaufabhängig sind. Tiefensuche kann aber auch sehr gut benutzt werden, um Eigenschaften, die nur vom Graphen abhängen, aufzudecken. Besonders wichtig ist dabei die Zerlegung von Graphen in schwache und starke Zusammenhangskomponenten (siehe Abschnitt 14.2). Schwache Zusammenhangskomponenten sind leicht, nämlich durch unmittelbare Anwendung von aTiefensuche zu bestimmen. Tabelle 15.4 zeigt den Algorithmus WCOMP. Er durchläuft die Knotenliste und richtet für jeden unmarkierten Knoten v eine neue schwache Zusammenhangskomponente C ein. Wenn diese eigentlich ist, wird die rekursive Prozedur WCP(v, C) aufgerufen. Da a-Erreichbarkeit die gleiche Relation wie gegenseitige a-Erreichbarkeit ist, werden mit diesem Aufruf alle Knoten besucht, die zur gleichen schwachen Zusammenhangskomponente gehören wie v, und nur diese. WCP(v, C) findet so alle Knoten, Kanten und Bögen, die zur Komponente gehören, und stellt dabei durch Überwachung der Rückwärtsbögen in Zeile 2 fest, ob a-Kreisfreiheit vorliegt oder nicht (Satz 15.2). Was kann man folgern, wenn eine eigentliche schwache Zusammenhangskomponente akreisfrei ist? Aus Proposition 14.1, Seite 380, folgt, daß sie entweder keine starken Zusammenhangskomponenten hat oder diese nur aus Kanten bestehen. Starke Zusammenhangskomponenten sind in diesem Fall a-kreisfreie ungerichtete Zusammenhangskomponenten, als Untergraphen also freie Bäume. Der Graph als ganzes ist dann ein a-Baum. Er ist i. a. kein f-Baum. Mit den noch zu behandelnden Methoden aus Abschnitt 15.4 lassen sich seine Bögen klassifizieren und es läßt sich feststellen ob, er ein f-Baum ist. Enthält eine eigentliche, a-kreisfreie schwache Zusammenhangskomponente nur Kanten, so ist sie stark zusammenhängend. Sie ist ein freier Baum. 15.4 Bestimmung starker Zusammenhangskomponenten Wir wollen in diesem Abschnitt nur schwach zusammenhängende allgemeine Graphen betrachten. Gegebenenfalls wenden wir vorher den Algorithmus WCOMP an. In Abschnitt 14.5, Seite 387, haben wir für die Knoten eines allgemeinen Graphen eine strikte partielle Ordnung ≺ eingeführt und darauf eine Schichtennumerierung aufgebaut. Eine totale Ordnung < auf der Menge der Knoten, heißt topologische Sortierung (topological sort) wenn sie Oberrelation von ≺ ist, d. h. gibt es einen f-Weg von u nach v, aber keinen von v nach u, so gilt u < v. Mit dem in Tabelle 15.5 aufgeführte Algorithmus PTOPSORT ist der erste Schritt zur Bestimmung einer topologischen Sortierung getan. Wichtiger ist jedoch, daß es auch der erste Schritt zur Bestimmung der starken Zusammenhangskomponenten ist. Bei der Aus- 424 KAPITEL 15. TIEFENSUCHE UND BREITENSUCHE ' WCOMP $ /* Anfangs sind alle Knoten und */ /* alle Kanten unmarkiert */ 1 for (alle Knoten v aus V ) /* Knotenliste */ 2 { if (v unmarkiert) 3 { richte neue schwache Zusammenhangskomponente C ein; 4 if (v isolierter Knoten) 5 { kennzeichne C als uneigentliche 6 schwache Zusammenhangskomponente; 7 } 8 else 9 { kennzeichne C als a-kreisfrei; 10 W CP (v, C); 11 } } } WCP(v,C) 1 2 3 4 5 6 7 8 9 10 11 12 13 if (v markiert) { kennzeichne C als nicht a-kreisfrei; return; } markiere v; füge v in die Zusammenhangskomponente C ein; for (alle Kanten, Ausgangsbögen und Eingangsbögen l von v) { if (l unmarkiert) { markiere l; füge l in die Zusammenhangskomponente C ein; W CP (otherend(l, v), C); } } return; & % Tabelle 15.4: Algorithmus WCOMP zur Bestimmung der schwachen Zusammenhangskomponenten führung von PTOPSORT gibt es für jede starke Zusammenhangskomponente einen ersten Knoten, den PTOPSORT besucht. Wir wollen ihn den Eingangsknoten der Zusammenhangskomponente (bezüglich der Ausführung von PTOPSORT) nennen. Die Bedeutung 15.4. BESTIMMUNG STARKER ZUSAMMENHANGSKOMPONENTEN ' PTOPSORT /* Anfangs sind alle Knoten unmarkiert. */ /* Der Keller stack ist anfangs leer. */ 1 2 3 4 for (alle Knoten v aus V ) { if (v unmarkiert) P T P S(v); }; 425 $ /* Knotenliste */ PTPS(v) 1 2 3 4 5 6 7 8 & if (v markiert) return; markiere v; for (alle Kanten und Ausgangsbögen l von v) { if (l unmarkiert) { markiere l; P T P S(otherend(l, v); } }; push (stack, v); /* rekursiver Aufruf */ % Tabelle 15.5: Algorithmus PTOPSORT zur topologischen Präsortierung eines allgemeinen Graphen von PTOPSORT besteht darin, daß für die entscheidenden Knoten im Keller die partielle Ordnung ≺ durch eine lineare Ordnung abgebildet wird. Das besagt die folgende Proposition. Proposition 15.3 Es sei u ein Knoten ohne Rückkehr oder der Eingangsknoten einer starken Zusammenhangskomponente. Es gebe einen f-Weg von u nach v aber keinen von v nach u. Dann liegt im Keller u vor 1 v. Beweis: 1. PTOPSORT besucht v vor u (Erstbesuch): Da u von v nicht erreicht werden kann, wird die Bearbeitung von v abgeschlossen und v dem Keller hinzugefügt, bevor die Bearbeitung von u beginnt. 2. PTOPSORT besucht u vor v (Erstbesuch): Die Bearbeitung von u wird nur dann v nicht erreichen und u vor v in den Keller eingefügt werden, wenn bei Beginn der Bearbeitung jeder f-Weg von u nach v durch einen markierten Knoten versperrt ist (Proposition 15.1). Es sei ein einfacher f-Weg von u nach v gegeben und darauf vm der letzte markierte 1 d. h. näher an der Kellerspitze 426 KAPITEL 15. TIEFENSUCHE UND BREITENSUCHE Knoten. Da v noch nicht markiert ist, muß vm beim Beginn der Bearbeitung von u noch aktiv sein, d. h es gibt einen f-Weg von vm nach u. Also gehören vm und u zur gleichen starken Zusammenhangskomponente. Dann ist jedoch u weder ein Knoten ohne Rückkehr noch Eingangsknoten der starken Zusammenhangskomponente. 2 Anmerkung 15.2 Wird PTOPSORT auf einen Dag angewendet, so ist man fast fertig. Es gibt keine starken Zusammenhangskomponenten und die lineare Ordnung im Keller ist eine topologische Sortierung. Allerdings hat man die Schichtennumerierung noch nicht gewonnen. 2 Der zweite und endgültige Schritt zur Gewinnung einer topologischen Sortierung und zur Bestimmung der starken Zusammenhangskomponenten ist der Algorithmus STRONGCOMP. Er ist in Tabelle 15.6, Seite 427, aufgeführt. Zusammen mit der in Tabelle 15.7 angegebenen Prozedur STRCP liefert er für einen allgemeinen Graphen, auf den vorher Algorithmus PTOPSORT angewandt wurde, das folgenden Ergebnis: Satz 15.4 Es sei G ein allgemeiner Graph, auf den Algorithmus PTOPSORT angewendet wurde. Dann ergibt die Anwendung von Algorithmus STRONGCOMP auf G: 1. Es werden alle Knoten ohne Rückkehr und alle starken Zusammenhangskomponenten mit ihren Knoten, Kanten und Bögen gefunden. Zu jeder starken Zusammenhangskomponente wird festgestellt, ob sie f-kreisfrei ist oder einen f-Kreis enthält. 2. Zu jeder starken Zusammenhangskomponente werden alle schwachen Verheftungspunkte identifiziert. 3. Es werden alle Bögen des externen Dag gefunden. 4. Den Knoten ohne Rückkehr und den starken Zusammenhangskomponenten wird ihre Schichtennummer zugeordnet. 5. In einer Warteschlange queue wird eine topologische Sortierung der Knoten von G angelegt. Beweis: Die Knoten im Keller sind anfangs alle unmarkiert und werden der Reihe nach abgearbeitet. Alle Kanten sind anfangs ungefärbt. Nach Proposition 15.3 ist der erste Knoten im Keller v ein Knoten ohne Rückkehr, der keine Vorgänger hat, oder der Eingangsknoten einer starken Zusammenhangskomponente, die keine von ihr nicht erreichbaren Vorgänger hat. Ist v ein Knoten ohne Rückkehr, so wird er in Zeile 4 als solcher erkannt. Er wird korrekt erster Knoten in der Warteschlange für die topologische Sortierung. Er behält die voreingestellte Schichtennummer 0, was auch richtig ist. Schließlich werden alle seine Ausgangsbögen als Bögen des externen Dag erkannt und gelb gefärbt. 15.4. BESTIMMUNG STARKER ZUSAMMENHANGSKOMPONENTEN ' 427 STRONGCOMP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 /* Anfangs sind alle Knoten unmarkiert und alle Kanten ungefärbt. */ /* Alle Knoten und alle starken Zusammenhangskomponenten */ /* haben 0 als Voreinstellung für die Schichtennummer */ /* Im Keller stack stehen die Knoten in PTOPSORT-Reihenfolge. /* v = pop(stack); while (v 6= NULL) /* Keller abarbeiten */ { if (v unmarkiert) { if (v hat keine Kanten und keine ungefärbten Eingangsbögen) { kennzeichne v als rückkehrfrei; füge v in Warteschlange queue ein; for (alle Eingangsbögen a von v) { if (lv(otherend(a, v)) >= lv(v)) lv(v) = lv(otherend(a, v)) + 1 ; } for (alle Ausgangsbögen a von v) { a als Bogen des externen Dag kennzeichnen; färbe a gelb; } } else { richte neue starke Zusammenhangskomponente SC ein; ST RCP (v, SC); for (alle Knoten v in SC) { if (v hat gelbe oder ungefärbte Bögen) { kennzeichne v als schwachen Verheftungspunkt von SC; for (alle gelben Eingangsbögen a von v) { if (lv(otherend(a, v)) >= lv(v)) lv(v) = lv(otherend(a, v)) + 1 ; } if (lv(v) > lv(SC)) lv(SC) = lv(v); for (alle ungefärbten Ausgangsbögen a von v) { a als Bogen des externen Dag kennzeichnen; färbe a gelb; } } } for (alle Knoten v in SC) lv(v) = lv(SC); } } v = pop(stack); } & Tabelle 15.6: Algorithmus STRONGCOMP zur Bestimmung der starken Zusammenhangskomponenten eines allgemeinen Graphen $ % 428 ' KAPITEL 15. TIEFENSUCHE UND BREITENSUCHE STRCP(v,SC) 1 2 3 4 5 6 7 8 9 10 11 12 13 if (v markiert) { kennzeichne SC als f-zyklisch; return; } markiere v; füge v in SC ein; füge v in Warteschlange queue ein; for (alle Kanten und Eingangsbögen l von v) { if (l ungefärbt) { färbe l blau; füge l in die Zusammenhangskomponente SC ein; ST RCP (otherend(l, v), SC); } } & $ % Tabelle 15.7: Prozedur STRCP zur Bestimmung der starken Zusammenhangskomponenten eines allgemeinen Graphen Ist der erste Knoten v ein Knoten mit Rückkehr, so wird er richtig als Eingangsknoten einer starken Zusammenhangskomponente erkannt. Es wird ein Beschreibungssatz SC eingerichtet und mit dem Aufruf ST RCP (v, SC) in v eine b-Tiefensuche gestartet. Diese besucht nach Proposition 15.1 alle von v b-erreichbaren Knoten. Da v der erste Knoten im Keller ist, muß (wieder nach Proposition 15.3) v auch von jedem dieser Knoten erreichbar sein. Es werden also genau die Knoten besucht und markiert, die zur gleichen starken Zusammenhangskomponente gehören wie v. Genau dann, wenn bei der b-Tiefensuche ein markierter Knoten gefunden wird, enthält SC einen f-Kreis: Ist ein f-Kreis vorhanden, wird mit Sicherheit ein Rückwärtsbogen oder ein Vorwärtsbogen gefunden. Falls andererseits die b-Tiefensuche auf einen markierten Knoten trifft, muß es nach Satz 15.3, Seite 422, einen a-Kreis und damit nach Satz 14.4, Seite 380, auch einen f-Kreis in SC geben. Die gefundenen Knoten werden der Komponente zugeordnet. Es werden auch alle Kanten und Bögen, die mit der b-Tiefensuche besucht werden, blau gefärbt und der Komponente zugeordnet. v wird als erster Knoten in die Warteschlange für topologische Sortierung eingetragen und direkt danach geschieht das gleiche für alle anderen Knoten der starken Zusammenhangskomponente. Die Reihenfolge ist durch den Ablauf der b-Tiefensuche bestimmt, ist aber stets in Einklang mit der partiellen Ordnung ≺. Anschließend werden alle Knoten u der starken Zusammenhangskomponente bearbeitet. Da es keine gelben Eingangsbögen gibt, bleibt die Schichtennumerierung der starken Zusammenhangskomponente auf dem voreingestellten Wert 0. Außerdem wird geprüft, ob die Knoten mit 15.4. BESTIMMUNG STARKER ZUSAMMENHANGSKOMPONENTEN 429 nicht-blauen Bögen inzidieren. Das können nur ungefärbte Ausgangsbögen sein. Sie werden dem externen Dag zugeordnet und gelb gefärbt. Die entsprechenden Knoten werden als schwache Verheftungsknoten der starken Zusammenhangskomponente klassifiziert und bekommen wie diese die Schichtennummer 0. Wir wollen nun annehmen, daß bis zu einem Punkt die Anwendung von STRONGCOMP korrekt gelaufen ist und v der nächste nicht markierte Knoten im Keller ist. v muß dann ein Knoten ohne Rückkehr oder der Eingangsknoten einer neuen starken Zusammenhangskomponente sein. Ist v ein Knoten ohne Rückkehr, so inzidiert er mit keiner Kante und hat Eingangsbögen nur von Knoten, die von ihm nicht erreichbar sind. Diese müssen dann im Keller vor v gelegen haben und schon korrekt abgearbeitet sein. D. h sie haben die richtige Schichtennummer, stehen an einer korrekten Stelle in der Warteschlange für topologische Sortierung und alle ihre zu v führenden Ausgangsbögen sind gelb. v hat also keine ungefärbten Eingangsbögen und wird in Zeile 4 korrekt als Knoten ohne Rückkehr erkannt. Auch die Einordnung in die Warteschlange für topologische Sortierung ist korrekt. Aus den Schichtennummern der unmittelbaren Vorgänger wird über die (gelben) Eingangsbögen die Schichtennummer von v richtig ermittelt. Die Ausgangsbögen von v werden gelb gefärbt und dem externen Dag zugeordnet. Ist v Eingangsknoten einer neuen starken Zusammenhangskomponente, dann inzidiert v mit einer Kante oder es gibt einen Eingangsbogen von einem Knoten der gleichen Komponente. Der Eingangsbogen muß ungefärbt sein. v wird richtig als Knoten mit Rückkehr erkannt und es wird einen neue starke Zusammenhangskomponente SC eingerichtet. Alle Knoten, von denen es einen Bogen in SC hinein gibt, die aber von SC nicht erreichbar sind, liegen im Keller vor v und die entprechenden Bögen sind nach Voraussetzung gelb gefärbt. Wird nun mit der Prozedur ST RCP in v eine b-Tiefensuche längs ungefärbter Linien gestartet, so besucht diese wieder genau die Knoten von SC und findet auch alle Kanten und Bögen der Komponente. Sie stellt fest, ob f-Kreisfreiheit vorliegt, und sorgt dafür, daß die Knoten an richtigen Stellen in der Warteschlange für die topologische Sortierung stehen. Die anschließende Durchmusterung aller Knoten der Komponente findet über die gelben Eingangsbögen die vorläufige Schichtennummer eines jeden schwachen Verheftungspunktes mit gelben Eingangsbögen. Das Maximum dieser vorläufigen Schichtennummern wird zur Schichtennummer der starken Zusammenhangskomponente. Die ungefärbten Ausgangsbögen werden gelb gefärbt und dem externen Dag zugeordnet. Auch sie bestimmen schwache Verheftungspunkte. Schließlich erhalten alle Knoten einer starken Zusammenhangskomponente die gleichen Schichtennummer wie diese. 2 Anmerkung 15.3 (Aufrufschemata für rekursive Unterprogramme) Die Aufrufschemata für Unterprogramme bilden einen Digraphen. Siehe Seit 88. Genau dann, wenn dieser f-kreisfrei ist, kommen keine rekursiven Aufrufe vor. Algorithmus STRONGCOMP bietet eine Möglichkeit, auf Rekursionsfreiheit zu testen. 2 430 KAPITEL 15. TIEFENSUCHE UND BREITENSUCHE Proposition 15.4 Die Anwendung von STRONGCOMP auf einen allgemeinen Graphen verursacht den Aufwand O(m + n), wobei m die Anzahl Linien und n die Antahl Knoten des Graphen ist. Beweis: Wird dem Leser überlassen. 15.5 Breitensuche Der Algorithmus für f-Breitensuche (Breitensuche in Vorwärtsrichtung, f-breadth-first search) in einem allgemeinen Graphen ist als Prozedur f-BFS in Tabelle 15.8 angegeben. ' & f-BFS(v) 1 enqueue(v); 2 markiere v; 3 while (Warteschlange nicht leer) 4 { u = dequeue; 5 for (alle Kanten und Ausgangsbögen l von u) 6 { if (l markiert) return; 7 markiere l; 8 w = otherend(l, u); 9 if (nicht markiert w) 10 { enqueue(w); 11 markiere w; 12 } } }; $ % Tabelle 15.8: Algorithmus f-BFS für Breitensuche in einem allgemeinen Graphen Werden in der Prozedur Kanten und Eingangsbögen durchlaufen, so erhalten wir b-Breitensuche (Breitensuche in Rückwärtsrichtung, b-breadth-first search). Bei a-Breitensuche (Breitensuche in beliebiger Richtung, a-breadth-first search) werden Kanten, Eingangsbögen und Ausgangsbögen durchlaufen. Ähnlich wie Tiefensuche besteht Breitensuche aus einem Rahmenablauf – f-BFSgesamt – und der darin aufgerufenen Prozedur f-BFS. Der Gesamtablauf f-BFSgesamt ist in Tabelle 15.9 dargestellt. Auch Breitensuche benutzt Markierungen und geht von anfänglich unmarkierten Knoten und Kanten aus. Die Prozedur f-BFS ist jedoch nicht rekursiv, sondern arbeitet mit einer Warteschlange. Sie markiert den Knoten, mit dem sie aufgerufen wird, und fügt ihn als ersten in die Warteschlange ein. Danach werden solange Knoten der Warteschlange entnommen, wie diese nicht leer ist. Zu jedem entnommenen Knoten werden alle Kanten und 15.5. BREITENSUCHE ' & f-BFSgesamt 1 2 3 4 /* Anfangs sind alle Knoten */ /* und alle Kanten unmarkiert */ for (alle Knoten v aus V ) /* Knotenliste */ { if (v unmarkiert) { f-BFS(v); } }; 431 $ % Tabelle 15.9: Algorithmus f-BFS für Breitensuche in einem allgemeinen Graphen Ausgangsbögen (Kanten und Eingangsbögen, Kanten und Bögen) getestet, ob sie markiert sind. Alle noch nicht markierten Linien werden markiert und der Knoten am anderen Ende geprüft, ob er markiert ist. Falls nein, wird er markiert und an die Warteschlange angehängt. Wird f-BFS mit dem Anfangsknoten v aufgerufen, so wird ein Knoten u gar nicht, einmal oder mehrmals besucht. Nur beim ersten Besuch wird er markiert und in die Warteschlange eingereiht. Die Bearbeitung eines Knotens findet statt, wenn er der Warteschlange entnommen wird. Breitensuche ist wie Tiefensuche leicht zu programmieren und für Listendarstellungen gut geeignet. Anders als Tiefensuche hat Breitensuche eine Optimalitätseigenschaft: Sie findet die erreichbaren Knoten auf kürzesten Wegen. Dazu soll der Begriff der f-Schale l zu einem Knoten v (f-shell) eingeführt werden. Ein von v f-erreichbarer Knoten w (w 6= v) gehört zur f-Schale l von v, wenn l die Länge eines kürzesten f-Weges von v nach w ist. {v} ist die Schale 0. Entsprechend werden die b-Schale l (b-shell) und die a-Schale l (a-shell) zu v definiert. Der folgende Satz besagt u. a., daß Breitensuche die Schalen in aufsteigender Reihenfolge der l bearbeitet. Der Durchlaufmodus – f, b oder a – ist dabei beliebig, aber fest. Satz 15.5 1. Breitensuche in einem allgemeinen Graphen erfordert c1 · n + c2 · 2m Zeitschritte. 2. In einem anfänglich unmarkierten Graphen besucht BF S(v) jeden von v erreichbaren Knoten w (w 6= v) und nur diese. 3. Es werden erst alle Knoten der Schale l markiert, bevor ein Knoten der Schale l + 1 markiert wird. Beweis: 1. Wegen der Markierung wird jeder Knoten von BFSgesamt genau einmal bearbeitet. Bei jedem Knoten wird jede Kante und jeder ausgehende Bogen (jeder ankommende Bogen, jeder Bogen) genau einmal bearbeitet. Falls sie nicht markiert ist, wird entweder mit dem Knoten am anderen Ende des Bogens die Breitensuche fortgesetzt oder 432 KAPITEL 15. TIEFENSUCHE UND BREITENSUCHE festgestellt, daß dieser schon markiert ist. Das sind c1 · n + c2 · 2m Zeitschritte. c1 ist die Bearbeitungszeit (ohne Untersuchung der Linien) in einem Knoten, c2 ist die Bearbeitungszeit einer Linie.2 2. und 3. Breitensuche baut einen f-Weg (b-Weg, a-Weg) von v zu jedem besuchten Knoten w auf, d. h. jeder besuchte Knoten ist auch von v erreichbar. Es sei w (w 6= v) von v f-erreichbar (b-erreichbar , a-erreichbar). Dann gibt es einen kürzesten Weg von v nach w. Seine Länge sei l. w gehört zur Schale l von v. w wird markiert und daher (mindestens einmal) besucht. Daß w auch wirklich markiert wird, und zwar nach allen Knoten der Schale l − 1 und vor allen Knoten der Schale l + 1 wird durch vollständige Induktion bewiesen: Es ist richtig für l = 1, denn alle Nachbarn von v, die infrage kommen, werden in die Warteschlange eingereiht, bevor ein Knoten, der nicht Nachbar ist, eingefügt wird. Ist die Aussage richtig für l, so werden nach dem Einfügen der Knoten der Schale l alle Knoten der Schale l + 1 (als unmarkierte Nachbarn von Knoten der Schale l) in die Warteschlange eingefügt, ehe der erste von ihnen bearbeitet wird. D. h. alle Knoten der Schale l + 1 kommen vor allen Knoten der Schale l + 2 in die Warteschlange. 2 Beispiel 15.3 Dieses Beispiel für den Ablauf einer f-Breitensuche ist so aufgebaut wie das Beispiel 15.1, Seite 415, für den Ablauf einer f-Tiefensuche. Es wird wieder Graph3, Seite 415, mit den in Abbildung 15.2, Seite 416, angegebenen Inzidenzlisten zugrunde gelegt. Tabelle 15.10 zeigt den Ablauf einer f-Breitensuche. Linien, die bei Bearbeitung eines Knotens markiert angetroffen werden oder der anderer Endknoten schon markiert ist, sind in der Tabelle kursiv dargestellt. 2 Anmerkung 15.4 Die in Tabelle 15.10 angegebenen Schalen beziehen sich nur auf den ersten bearbeiteten Knoten, nämlich v3 . Zu anderen Knoten, die von v3 f-erreichbar sind, werden die Schalen nicht angegeben, z. B. zu v4 . Sind die Knoten nicht von v3 f-erreichbar, wie zum Beispiel v7 , so werden ihre Schalen unvollständig angegeben. 2 Bei a-Breitensuche werden vom ersten besuchten Knoten v einer schwachen Zusammenhangskomponente alle ihre Knoten erreicht und die zu v gehörende Schalenstruktur festgestellt. Zur weiteren Ermittlung von Zusammenhangseigenschaften wie starker Zusammenhang, Schichtennumerierung usw. ist Breitensuche weniger gut geeignet.3 Streng genommen müßten Schlingen gesondert betrachtet werden, da sie nur an einem Knoten bearbeitet werden. 3 Allerdings ist bei anderen Problemen der Einsatz von Breitensuche von Vorteil. Z.B. bei der Bestimmung eines maximalen Flusses (siehe Abschnitt 20.4) und beim Finden von Mengerstrukturen (siehe Stiege [Stie2006]). 2 15.5. BREITENSUCHE 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 433 Schale 0 v3 uv3 v4 uv3 v6 dv3 v2 Schale 1 v4 v6 v2 Schale 2 uv3 v4 dv4 v4 dv4 v5 uv3 v6 v5 v8 v7 v1 dv5 v6 dv5 v3 dv8 v5 dv7 v8 dv7 v5 dv1 v2 a dv1 v2 b dv1 v3 Tabelle 15.10: Ablaufprotokoll einer f-Breitensuche Aufgaben Aufgabe 15.1 Für Graph3, Seite 415, sehe die Knotenliste folgendermaßen aus: v7 , v8 , v1 , v6 , v2 , v3 , v5 , v4 . Die Liste ist durch Inzidenzlisten zu ergänzen und für b-Tiefensuche ein Ablaufprotokoll nach dem Muster von Beispiel 15.1 anzugeben. Aufgabe 15.2 Geben Sie einen Graphen und auf ihm f-Tiefensuchabläufe an, die zu unterschiedlichen Tiefensuchwäldern mit unterschiedlicher Anzahl Baumbögen führen. Aufgabe 15.3 Es sei G(V, E, A, ϕ, ψ) ein allgemeiner Graph. Auf V × V werde die folgende reellwertige partielle Abbildung definiert 0, falls u = v Länge eines kürzesten f-Weges von u nach v, falls u 6= v und v von u f-erreichbar dgf (u, v) := undefiniert sonst Entsprechend sind dgb (u, v) und dga (u, v) definiert. a. Zeigen Sie, daß dga (u, v) eine Metrik ist, wenn G schwach zusammenhängend ist. b. Wie kann man feststellen, ob auch dgf (u, v) eine Metrik ist? 434 KAPITEL 15. TIEFENSUCHE UND BREITENSUCHE Aufgabe 15.4 Es sei G(V, E, A, ϕ, ψ) ein allgemeiner Graph und v ∈ V . Der f-Schalengraph von v in G ist der folgendermaßen definierte Digraph: Seine Knoten sind die Knoten der f-Schalen von v. Ein Bogen geht stets von einem Knoten u der Schale l zu einem Knoten w der Schale l + 1. u und w werden durch einen solchen Bogen verbunden, wenn es einen f-Übergang von u zu w gibt. Entsprechend werden der b-Schalengraph und der a-Schalengraph zu v definiert. Man gebe einen Algorithmus an, der zu gegebenem G und v die Schalengraphen findet. Aufgabe 15.5 Es sei G(V, E, A, ϕ, ψ) ein allgemeiner Graph und v ∈ V . w 6= v sei ein Knoten der a-Schale l von v. Der Distanzgraph von v bezüglich w ist der Untergraph des a-Schalengraphen von v, der durch die a-Wege kürzester Länge von v nach w gebildet wird. a. Wie sieht der Distanzgraph von w bezüglich v aus? b. Man gebe einen Algorithmus an, der zu G, v und w den Distanzgraphen von v bezüglich w findet. Aufgabe 15.6 Es sei G(V, E, A, ϕ, ψ) ein allgemeiner Graph und u, v ∈ V mit u 6= v. Es sei p ein einfacher f-Weg (b-Weg, a-Weg) von u nach v. p heißt direkt (gerade, straight), wenn kein innerer Punkt des Weges auf einem kürzeren einfachen f-Weg (b-Weg, a-Weg) von u nach v liegt. Skizziereb Sie einen Algorithmus, der alle direkten f-Wege (b-Wege, a-Wege) von u nach v bestimmt. Literatur Tiefensuche wurde Ende der 70er-Jahre von Tarjan und Hopcroft in einer Reihe bahnbrechender Aufsätze als wichtigstes Instrument der algorithmischen Graphentheorie eingeführt [Tarj1972], [HopcT1973], [HopcT1973], [HopcT1973a], [HopcT1974]. Siehe auch das klassische Lehrbuch von Aho/Hopcroft/Ullman [AhoHU1974]. Moderne Bücher über Algorithmen und Datenstrukturen behandeln im allgemeinen Tiefensuche ausführlich. Als Beispiele seien Cormen/Leiserson/Rivest [CormLR1990], Mehlhorn/Näher [MehlN1999] und Sedgewick [Sedg2002] genannt. Mathematisch ausgerichtete Bücher über Graphentheorie erwähnen entweder Tiefensuche überhaupt nicht oder nur ganz am Rande. Ausführlicher wird Tiefensuche in algorithmisch ausgerichtete Lehrbüchern über Graphentheorie behandelt. Siehe z. B. Chartrand/Oellermann [CharO1993], McHugh [McHu1990], Thulasiraman/Swamy [ThulS1992], Turau [Tura1996] und Jungnickel [Jung1994]. 15.5. BREITENSUCHE 435 Anders als Tiefensuche ist Breitensuche nicht durch eine Reihe bestimmter Artikel eingeführt und verbreitet worden, sondern „irgendwie“ gewachsen. Moderne Darstellungen von Breitensuche findet man in den oben genanten Büchern. 436 KAPITEL 15. TIEFENSUCHE UND BREITENSUCHE Kapitel 16 Die Biblockzerlegung In diesem Kapitel werden nur a-Wege betrachtet und die Ergebnisse hängen nur von der Inzidenzstruktur der betrachteten allgemeinen Graphen ab. Sie sind für alle Graphen einer Orientierungsklasse gleich. Schlingen und Mehrfachlinien sind zugelassen. 16.1 Klassen geschlossener a-Wege und Kantenzerlegungen Die Biblockzerlegung eines allgemeinen Graphen wird durch Untergraphen bestimmt, in denen es mindestens zwei „unabhängige“ a-Wege zwischen je zwei Knoten gibt. Unabhängig heißt dabei, daß die Wege keine inneren Knoten gemeinsam haben. Es wird auch eine schwächere Form der Unabhängigkeit betrachtet, bei der zwei Wege keine gemeinsamen Linien aufweisen. Wie man sich leicht klar macht, muß es in beiden Fällen a-Kreise in den Untergraphen geben. Wir wollen daher bei der Biblockzerlegung mit schwachen a-zyklischen Zusammenhangskomponenten beginnen (siehe hierzu Abschnitt 14.6, Seite 392). In diesen wird ausgehend von a-Kreisen eine Hierarchie geschlossener a-Wege betrachtet: a-Kreise ⊆ linieneinf ache geschlossene a-W ege ⊆ stoppf reie W ege Zu linieneinfachen Wegen und Kreisen siehe Kapitel 14, Seite 373. Ein stoppfreier Weg (stopfree path) ist ein geschlossener a-Weg, auf dem keine Linie zweimal hintereinander auftritt und, falls die Weglänge mindestens 2 ist, die erste von der letzten Linie verschieden ist1 . Eine schematische Darstellung ist in Abbildung 16.1 zu sehen. Der Name läßt sich so erklären: Man kann mit einer Lokomotive den geschlossenen Weg durchfahren, ohne anhalten und zurückfahren zu müssen. 1 437 438 KAPITEL 16. DIE BIBLOCKZERLEGUNG ....... .... ...... .... . .............. ............... .... .... ..... ........ ............... . .... .... ..... ........ ...... .... ..... .... . ............... ........ .... ..... .... . ............... .............. ... ............ ............ ...... ............. .......... . . . . .... . ... .... ... ... . ... .. .. ....... . . . . . . . ............ .... .... .... ..... ... ... .. . . ............ ............ . ... ... ... ... ... .. .... . . . ..... . ....... ............. .......... ........... ............ ... .............. ............... .... .... ..... ........ ......... ... ..... . ... ....................................... . ..... ...... . . . . .... ... ... ... ... . . . ... . ...... . . . . . . . ............ .... ..... .... .... ... .. ... . . ............ ............ . ... ... ... ... ... .. .... . . . ..... . ....... ............. .......... ........... ............ ... ............... a. Geschlossener a-Weg b. Stoppfreier Weg .......... .............. ... .... . ... ........ ........... ........ ........... ....... ............... ............ ....... ............. ........... .... ...... ..... . . . . . . . . ... ... ... .. . ... . . . ... ... ... ... ... ... .. ... . . . . ...... ....... .............. . ..... ..... ..... ..... .... . . . ... ... . . ............... ............. .............. ... .. .... ... .. .. ..... . . . . ... ... ... ...... ... ... ..... ... ..... ..... ...... ...... ............. .......... ......... ................. ............. ............ ........... .... . ... . . . ... .. ..... .... ........... ...... .............. . ... ......... ......... ....... .............. ............ ..... .... . . . ... ... ... . .. ... .. . . . ...... ............. . . ...... ..... .. .... ... ... ............... ............ ... .. ... .. . . ... .. ... ... .... ..... ...... ........ ............... ............. ..... ...... . ... .. ........... c. Linieneinfacher geschlossener a-Weg d. a-Kreis Abbildung 16.1: Klassen geschlossener a-Wege In Graph Ugraph1, Abbildung 16.5, Seite 443, ist d2 , d1, d0 , c0 , c3 , c2 , c1 , c0 , d0 , d2 ein stoppfreier Weg2 , der nicht linieneinfach ist. d2 , d1 , d0 , c0 , c3 , c2 , c1 , c0 , d0, d1 , d2 ist kein stoppfreier Weg, da die erste und die letzte Kante übereinstimmen. Definition 16.1 Es werden die folgenden Relationen zwischen Linien definiert: Zwei Linien e und f sind 1. stoppfrei verbunden, wenn es einen stoppfreien Weg gibt, der e und f enthält, 2. linieneinfach verbunden, wenn es einen linieneinfachen geschlossenen a-Weg gibt, der e und f enthält, 3. kreisverbunden, wenn es einen a-Kreis gibt, der e und f enthält. Mit Definition 16.1 erhält man eine Hierarchie von Zerlegungen der Linienmenge eines allgemeinen Graphen G. Dazu der folgende Satz. Satz 16.1 1. Stoppfreie Verbundenheit ist eine Äquivalenzrelation in der Menge der Linien, die auf einem stoppfreien Weg liegen. 2. Kanteneinfache Verbundenheit ist eine Äquivalenzrelation in der Menge der Linien, die auf einem linieneinfachen geschlossenen a-Weg liegen. 3. Kreisverbundenheit ist eine Äquivalenzrelation in der Menge der Linien, die auf einem a-Kreis liegen. 2 Siehe Anmerkung 14.1, Seite 374. 16.2. DIE BIBLOCKZERLEGUNG ALLGEMEINER GRAPHEN 439 Beweis: Reflexivität folgt aus der Definition der betrachteten Linienmengen. Symmetrie ist unmittelbar klar. Transitivität läßt sich mit einem einzigen Beweis für alle drei Relationen nachweisen. Es seien die Linien e und f und die Linien f und g stoppfrei verbunden (linieneinfach verbunden, kreisverbunden). Zu zeigen ist, daß e und g auf die gleiche Weise verbunden sind. Das ist klar, wenn zwei der drei Linien identisch sind. Das ist auch leicht einzusehen, wenn zwei der drei Linien Mehrfachlinien mit gleichen Inzidenzpunkten sind. Es werde angenommen, daß die Linien e, f und g paarweise verschieden und keine Mehrfachlinien zu den gleichen Inzidenzpunkten sind. Wir identifizieren die Linien durch ihre Inzidenzpunkte und wählen einen stoppfreien Weg (einen geschlossenen linieneinfachen a-Weg, einen a-Kreis) durch die Linien {a1 , a2 } und {b1 , b2 } und einen geschlossenen Weg gleichen Typs durch die Linien {b1 , b2 } und {c1 , c2 }: v0 = a1 , v1 = a2 , . . . , vi−1 , vi = b1 , vi+1 = b2 , . . . , vm−1 , vm = a1 und w0 = c1 , w1 = c2 , . . . , wj−1, wj = b1 , wj+1 = b2 , . . . , wn−1, wn = c1 , dabei kann die Durchlaufsrichtung des zweiten geschlossenen Weges so gewählt werde, daß die mittlere Linie in Richtung (b1 , b2 ) durchlaufen wird (siehe Abbildung 16.2). Wir durchlaufen den ersten geschlossenen Weg in positiver Richtung von a1 bis b1 und treffen in u1 zum ersten Mal auf einen Knoten, der auch zum zweiten geschlossenen Weg gehört: v0 = a1 , v1 = a2 , . . . , u1 , . . . , vi = b1 . Entsprechend durchlaufen wir den ersten geschlossenen Weg in negativer Richtung von a1 bis b2 und treffen in u2 zum ersten Mal auf den zweiten geschlossenen Weg: a1 = vm , vm−1 , . . . , u2 , . . . , vi+1 = b2 . Nun wird ein neuer geschlossener Weg konstruiert: Wir durchlaufen von a1 den ersten Weg in positiver Richtung bis u1 , dann den zweiten Weg in negativer Richtung bis c2 , dann die Linie bis c1 und weiter bis u2 . Danach wird in positiver Richtung auf dem ersten Weg fortgesetzt und a1 über vm−1 erreicht. Der neue geschlossene Weg enthält die Linien {a1 , a2 } und {c1 , c2 } und ist vom gleichen Typ wie die gegebenen beiden geschlossenen Wege. 2 Der obige Beweis ist auch dann gültig, wenn Teilwege der Konstruktion die Länge 0 haben. Einige Beispiele sind in Abbildung 16.3 zu sehen. 16.2 Die Biblockzerlegung allgemeiner Graphen Die in Abschnitt 16.1 gefundenen Äquivalenzklassen der Verbundenheitsrelationen erhalten eigene Namen, ebenso die von ihnen erzeugten Untergraphen. Definition 16.2 1. Eine Äquivalenzklasse stoppfrei verbundener Linien heißt stoppfreier Kern. 2. Eine Äquivalenzklasse linieneinfach verbundener Linien heißt Subkomponente. 3. Eine Äquivalenzklasse kreisverbundener Linien heißt Biblock. 440 KAPITEL 16. DIE BIBLOCKZERLEGUNG u1 .................. ..... ... ... .. .. ................. ................... . .................... . . . . . . . . . . . . . . . . . . ... ............ ... ......... . . . . ......... .. . . . . . ..... ........ ... . ....... . . . . . . . . . . . . . . ....... . . . ...... . ..... . ...... . . . . . . ...... . .... . . . . ...... . . ... . . ..... . . . .... . . ..... . . . ... . .. . ... . . ... . ... . . . ... . .. . . ... . .. . ... . . ... . .... . . .. ... . ... . . . . . . ............ . . . . . . . . .... . .... . . . . .. . . .... .. . ... . . . ... . . . ..... ... . . .................. . ....................... ..................... ... .. ... .... . .. .. .. .. .. ..... ..... .. . .. ... 2 .. 1 ....... . ... .... ...................... ...................... ........... ...... ........ ... ... .. .. .... .. ... .. . ..... . ................... ..................... ...................... .... ... .... ... ... .. ... .. .... . ..... . ... . 1 .... . 2 ....... .. . . ...... .. ....................... ................. . . . . . . . . .... ......... . . ..... ... . ... .. . . .... . . ... . . .. . . . ... . ...... ......... . . ........... . .. . ... . ... ... . . ... ... . . .. . ... . . ... .. . . ... ... . . ... ... . ... ... . . .... .. . . . . ..... ... . ..... . ..... . ...... ..... . . ...... ..... . ....... ....... . . . . . ........ . . . . . . . . . . ........ ........ .... ......... ... .. ......... ........... ... .. .......... ................ .......................................... .......................... ... .. . ... . .. ..... ................... zweiter Weg a2 = v1 a1 = v0 = vm vm−1 erster Weg b c b c u2 Abbildung 16.2: Zum Beweis der Transitivität a2 = u1 = c1 a2 = b1 = c1 = u1 c2 ....................... .... ... .. .. ... . ... .. ... ... . . . . . . . . . . . .. .............. ..... . . ... .. ... ... ... ... ... ... ... ... . . ... .. . ... . .. ... . . ... ... . ... . . . ... .................. . . . . . . . . . . . ........ .... ...... . . .... ... ... ..... ... .. .. ... ..................................................................................... .. ... . . . ... ... . .... . . . . . . . . . . . ................. ................ a1 = b2 c2 = u2 = b1 = vm−1 ...................... ...................... ... .... .... ... .. .. .. .. .............................................. ... . . .. ... ... .. ... ... .... ....... ........ .......................... ........... ... ... . .... ... ... .... .... ... ... .... ... ... ... .... ... ... ... ... ... .... ... ... ... .... . .... .... ........... . . . . . . . . . . . ...... ......... ....... ...... ... . . . ... ... .. .... .... .. ... ............................................ ... .. ... . . ... ... .... ...... ........ ...................... .......... a1 b2 = u2 = vm−1 a2 u =u =b =v b2 = c1 ...................... ...................... 1 2 1 ... .... .... ... .. .. .. .. . .... ... . . .. ... .. . . .. . . . . . . m−1 . .... . ..... ....... ... . . . . ...................... .............. . . . . . . .............. ....... ..... . . . . . ... . ....... . ....... ....... ... .... ........ ........................ ............. ... ... ...... ........ .. .. ... ... ..... ... ... ... . . . ... ... . . . ......... ..... ....... . . . . . . . . . . ... ... . . . ........ ............... ..... . . . . . . . . .... ... . . ....... .... . . . . . . . . . . . . . . . . . . . . . . . . . . ...... ....... ....... ..... ............ ...... . . . ... . . . . . . . . . ....... ...... ... .... ... .. ... .. . ... ... .. . . . . ... .... .... ....................... ....................... a1 c2 Abbildung 16.3: Spezialfälle beim Beweis der Transitivität Die von den Äquivalenzklassen erzeugten Untergraphen tragen die gleichen Namen. Zusammenfassend sollen sie als Komponenten der Biblockzerlegung bezeichnet werden. 16.2. DIE BIBLOCKZERLEGUNG ALLGEMEINER GRAPHEN 441 A-azyklische schwache Zusammenhangskomponenten sind a-Bäume. In ihnen muß jeder geschlossene a-Weg mindestens eine Linie zweimal hintereinander durchlaufen. Sie können also keine stoppfreien Kerne, und daher auch keine Subkomponenten und keine Biblöcke enthalten. A-zyklische schwache Zusammenhangskomponenten enthalten a-Kreise und damit mindestens einen stoppfreien Kern. Wie in Proposition 16.1 bewiesen wird, enthalten sie genau einen stoppfreien Kern. Die Menge der Linien einer a-zyklischen schwachen Zusammenhangskomponente, die nicht zum stoppfreien Kern gehören, ist entweder leer oder erzeugt einen a-kreisfreien Untergraphen. Die schwachen Zusammenhangskomponenten dieses Untergraphen sind a-Bäume. Sie werden die peripheren Bäume (peripheral tree) der schwachen Zusammenhangskomponente genannt. Einige Knoten gehören sowohl zum stoppfreien Kern als auch zu einem peripheren Baum. Sie heißen Grenzpunkte (border point). Die Menge der Linien eines stoppfreien Kerns, die nicht zu einer Subkomponente gehören, ist entweder leer oder erzeugt einen a-kreisfreien Untergraphen. Die schwachen Zusammenhangskomponenten dieses Untergraphen sind a-Bäume. Sie werden die internen Bäume (internal tree) des stoppfreien Kerns genannt. Einige Knoten gehören sowohl zu einer Subkomponente als auch zu einem internen Baum. Sie heißen Checkpunkte (check point). Eine Subkomponente besteht aus einem oder mehreren Biblöcken und jede ihrer Linien gehört zu genau einem Biblock. Ein Knoten kann jedoch zu mehreren Biblöcken gehören. Ein solcher Knoten heißt Angelpunkt (hinge point). Grenzpunkte, Checkpunkte und Angelpunkte werden zusammenfassend auch als Verheftungspunkte (attachment points) bezeichnet. Abbildung 16.4 zeigt eine schematische Darstellung der Biblockzerlegung allgemeiner Graphen. Beispiel 16.1 Graph Ugraph1, Abbildung 16.5, Seite 443, ist ungerichtet. Er besteht aus drei Zusammenhangskomponenten3 : 1. {h} Uneigentliche Zusammenhangskomponente. 2. {i0 , i1 , i2 , i3 , i4 } Eigentliche a-kreisfreie Zusammenhangskomponente. 3. {a0 , . . . , a15 , b0 , b1 , b2 , c0 , c1 , c2 , c3 , d0 , d1 , d2 , e0 , e1 , e2 , e3 , f0 , f1 , f2 } Eigentliche a-zyklische Zusammenhangskomponente. Der stoppfreie Kern besteht aus einer Subkomponente mit drei Biblöcken, nämlich {a0 , . . . , a15 , b0 , c0 }, {b0 , b1 , b2 }, {c0 , c1 , c2 , c3 }, und den beiden Subkomponenten {d0 , d1 , d2 } und {f0 , f1 , f2 }, die nur 3 Im folgenden werden die Zusammenhangskomponenten nur durch ihre Knoten angegeben, sind aber als Untergraphen zu verstehen. 442 KAPITEL 16. DIE BIBLOCKZERLEGUNG ' Graph & $ % ... .................. ....... .... ............... ........ ........ ... ........ ........ ... ........ ........ . . . . . . . ... . ........ ........ ........ ... ........ ........ . . . . . . . . ........ . ...... . . . . ........ . . . . .. ..... ........ . . . . . . . . ........ .... .. ..... . . . . . ........ . . . . . ....... ............ ........... . . . . . . . ... ................ ................ ' Uneigentliche Schwache Zush.-Komp. & ' $ A-kreisfreie Schwache Zush.-Komp. % & ' Angelpunkte & ' ... ... ... ... ... .. ... ... ... . ......... ........ ... Biblöcke & ' $ $ % & % Periphere Bäume ......... ...... ........... ...... ...... ...... ...... . . . . . . ...... ...... ...... ...... ...... . . . . . ...... .... . . . ...... . . .... ...... . . . . . ...... .... . . . ...... . . .... ...... . . . . . . . . ........ .................. . ................. ..... Subkomponenten % .. ...... ........... ...... ...... ...... ...... ...... ...... ...... ...... . . . . ...... . .... . ...... . . . . ...... .... . . . . ...... . .... ...... . . . . . ...... .... . . . ...... . . .... ...... . . . . . . . ......... ........... . . . ................. ............. & ' $ A-zyklische Schwache Zush.-Komp. % & .. Stoppfreier Kern Grenzpunkte Checkpunkte ' $ ' $ $ % & % Interne Bäume $ % Abbildung 16.4: Biblockzerlegung eines allgemeinen Graphen aus einem Biblock bestehen. Verbunden sind die Subkomponenten über den internen Baum {c0 , d0 , f0 }. Außerdem gibt es die peripheren Bäume {d2 , g} und {c0 , e0 , e1 , e2 , e3 }. c0 ist Grenzpunkt, Checkpunkt und Angelpunkt. d2 ist Grenzpunkt. d0 und f0 sind Checkpunkte. b0 ist Angelpunkt. 2 16.3. EIGENSCHAFTEN DER KOMPONENTEN DER BIBLOCKZERLEGUNG 443 .................. ... .. . ... .................... .................................. 8 ................................... ...................... . . .... ...... ...... .... ... ..... . . . . . . . .... .. . . 7 . . . ... 9............ . ................ ...... ....... .................... ...... ...... . . . . . ...... ..... ..... .......... ........... ..... ........ .... ..... ......... ... .. ................... ................... ................... . . . .. ..... .. . . . .. .. ..... .. ... .. ... ... 6 ... . . . . . . ... 10..... ... 2 .. ... 3 ... . ... ...... ...... . . . . . . . . . ............ .. .... .... . . ..... . . . .. ....... . . . ... . . . . . . . . . . . . . . . . . . . .......... . .. ......... ... .... ......... ... .. .... ... ..... . ... .... ... ...... ... ............... .......... ................... .............. ... . . . . . . . . . . . . . . ... .... ... ... .. .. .. .. . . . . . . . . . .. .. ... . ... ... ................. . .................. . . . . . . ... 2 ... ... 1 ... ... 1 ... ... ... ... .. . . . . . . . . ............... ... . . ........................ ....................... ......... . . . . . . . . . . . . . . . . . . . . .... .... ... 11..... ... 5 ... .... .... ....... ................. .... .... .... .................. ..... .... .... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ... .... ..... .... ..... . .... ...... ... ........ ... ........ ............. ... . . . . . . . . . . . . . . . . . . . . . .. .. .. ... ... ............................................. ............................................. ... ..... ..... ..... ... ... . . . ... ... ... ... ..... ... 0 .... ... 0 .... ... 1 ..... ... ... .... 0....... .... 2...... ... ................. ................. ... 1 ........ ..... .... ............. ...... ............. ... ... ... ................. ....... ...... .......... ...... . . .. . . . ... ....... ..... . . . ... . ...... . . ... ....... . . . . . . . . . ... . . . . ...... ................ ..... ....... ................. ................ . ................. . . . . . . . ... ..... ... . . . . . . . . ... .. ... .. .... ... ... . . . .... ..... ... .. ... 0 ... ... .... 0 ... ... 4 .... ... 12..... .. ... ...... ...... ... .. ... .. ................ ....... ... .......................... ....... ................. . . . . . . . . . . . . . .... .. ..... ... . .... .. . . . . . . . . . . . . . . . . . . . . .... ........ ... . ... ............ .. ..... ... ...... ...... .. ... .. ... ... ....... ............ ... .. .. .. ..... ... ... ... ... .. .... . .... ... 2 .... ... ... ... ... 3 ... ................. ... 1 ..... ... . . . . .. ..................... .... ... .. ... ..... .... .... ............. .................. ...... ..................... .... .... .... ........ .. ... .... ...... ...... .. ................. . ........ ......... . ..... ... 3 .... . . 13 . . ... .... .. ..... ...... ................. . ..... ........... . . ... ... 2 ..... . .. ................. ... . . ... .. ... ... ... ... ... ... ... ......... ........ ................... . . . . . ... ... . . .. .. ..... ..... ... 14..... ... 2 ..... ...................... . ..................... ...... . . . . ...... ..... ...... ...... ....... ................... ............................... ...... ... ... .. .. .. ..... .... ........... . . . . . . . . . ... ... 1 ................. ...... 15. ........................ ................ .............................. ..................... ... . .... 0.... .............. a a a g e b f e d d b a e e d a a a f f c a b c c a a a c a a a ............... ... .... .. ..... .. ... ...... ......... ...... h .................... ... .. .... . ... 0 .... ................ . i a a .................. ... ... .. ..... ... 1 ..... ................ i .................... ... .. .... . ... 2 .... ................ . i ............... ... .... .. ..... ... 3 .... ...... ....... ...... i .................... ... .. .... . ... 4 .... ................ . i Abbildung 16.5: Ugraph1 16.3 Eigenschaften der Komponenten der Biblockzerlegung Stoppfreier Kern und periphere Bäume: Die Bezeichnung stoppfreier Kern wird durch die folgenden Proposition begründet. Proposition 16.1 In einer schwachen a-zyklischen Zusammenhangskomponente gibt es genau einen stoppfreien Kern. Beweis: Der Beweis soll nur skizziert werden. Da eine eigentliche a-zyklische schwache Zusammenhangskomponente einen a-Kreis enthält, muß sie auch einen stoppfreien Kern besitzen. Um zeigen, daß es nicht mehr als einen stoppfreien Kern geben kann, werden zwei verschiedene Linien betrachtet, die jeweils auf einem stoppfreien Weg liegen. Es gibt dann auch einen stoppfreien Weg, auf dem beide Linien liegen, und sie gehören zum gleichen stoppfreien Kern. Das ist richtig, wenn beide Linien auf einem der beiden gegebenen stoppfreien Wege liegen. Anderenfalls liegt einer der folgenden drei Fälle vor 444 KAPITEL 16. DIE BIBLOCKZERLEGUNG 1. Die beiden stoppfreien Wege haben einen Knoten, aber keine Linie gemeinsam. 2. Die beiden stoppfreien Wege haben mindestens eine, aber nicht alle Linien gemeinsam. 3. Die beiden stoppfreien Wege haben keinen Knoten gemeinsam. Abbildung 16.6 zeigt, wie man aus den beiden gegebenen stoppfreien Wegen einen neuen ....... .......... . . .. .. .. .. .. .. .. .. .. . . . .. . . . . . . . ... .. .... . . ... .... . . . . . .................. . .. .... . . . .. . . . . . . . . . ..... ... . . ... .... . ... .... . ... . . . . . . .. . . . . . .. . . . .. . .. .. ... .. .. . .. ....... ....... 1. Die stoppfreien Wege haben einen Knoten, aber keine Linien gemeinsam. ......... . .. .. .. . . . . . . ......... . . .. .. . . .. .. . .... . . . .... . . .............. . . . . . . . ... . . . . . . .... ... . . . . ...... . . .. . . . . . . . . . . .... . . . . . . .......... . .... .... . . . . ..... .... . . . . .. . . . . . . .. . . .. . .. .. .. .. .... . . . . . . .. .. . .. . .. . . ...... 2. Die stoppfreien Wege haben mindestens eine, aber nicht alle Linien gemeinsam. ........ ........ . ... .. .. .. .. .. .. . .. . . .. .. . . . . . . . .. . . . . . . . . . . . . . . .............. ............. . . . .. ............ . . . . . ............ . . .... . . . .... ... .. .. . ......... . . .......... . .. . . .. . . .. . . . .. . . . .. .. . . . . . .. .. . . . . .. . . ... . . . . . . . . ..... .... 3. Die stoppfreien Wege haben keinen Knoten gemeinsam. Abbildung 16.6: Linien auf stoppfreien Wegen sind stoppfrei verbunden stoppfreien Weg konstruieren kann, der beide Linien enthält. Im dritten Fall wählt man einen kürzesten Weg zwischen den gegebenen stoppfreien Wegen. 2 Die Linien von Subkomponenten und natürlich auch von Biblöcken sind Nichtbrücken und jede Nichtbrücke gehört zu einem eindeutig bestimmten Biblock (Proposition 14.3, Seite 382). Alle Nichtbrücken gehören zum stoppfreien Kern und jeder stoppfreie Kern einer zyklischen Zusammenhangskomponente enthält eine Subkomponente. Ein stoppfreier Kern kann auch Brücken enthalten: Die Linien seiner internen Bäume sind Brücken. Kann ein stoppfreier Weg ausschließlich aus Brücken bestehen? Mit dem folgenden Satz wird bewiesen, daß das nicht möglich ist, und präzisiert, welche a-Kreise auftreten. Dazu wird die in Abschnitt 14.2, Seite 381, eingeführte Bezeichnung [x] benutzt. Es werde eine Brücke mit Inzidenzpunkten a und b aus einem Graphen entfernt. [a] ist die (eigentliche oder uneigentliche) schwache Zusammenhangskomponente des reduzierten Graphen, in der a liegt. Die schwache Zusammenhangskomponente, in der b liegt, ist [b]. Satz 16.2 Eine Brücke e mit Inzidenzpunkten a und b liegt genau dann auf einem stoppfreien Weg, wenn sowohl [a] als auch [b] einen a-Kreis enthalten. 16.3. EIGENSCHAFTEN DER KOMPONENTEN DER BIBLOCKZERLEGUNG 445 .... . ...... ... .. .. .... ... .. .. ... .. .. .. .. .. .. .. .. . . . .. .. . . . .. .. . .. . . . . . . . . . . . . . . . . . . . ................. ................. .................. .................. . . . . . . . . . . . . . . ... ... .. .. . .. . . .. . . . . . ............................... . . ... ........... . . . . . . . . . ............. . ........... . . . . . . . . . ............. . . . . . . . . . . ... ... ... 2... ... 1... . . . . . . . . . . . . . . . . ............... ............... ............... .............. . . . . . . . . . . . . . . . . . . .. . . .. .. .. .. .. . . .. . .. .. .. .. .. .. .. .. .. .. ... .. .... .. .. .. .... ........ u a b u Abbildung 16.7: Subkomponenten auf beiden Seiten einer Brücke Beweis: 1. Es seien SC1 und SC2 Subkomponenten mit SC1 ⊆ [a] und SC2 ⊆ [b] (siehe Abbildung 16.7). A Wir wählen u1 ∈ [a] auf die folgende Art: Falls a ∈ SC1 setzen wir u1 := a. Andernfalls wählen wir einen kürzesten und daher einfachen a-Weg von a zu SC1 . Dieser Weg hat nur einen Endpunkt mit SC1 gemeinsam. Es sei u1 dieser Endpunkt. Auf der b-Seite wird ein entsprechender Knoten u2 gewählt. Wir bilden nun den folgenden aWeg: Ausgehend von a durchlaufen wir den einfachen Weg (eventuell der Länge 0) bis u1 . Dort wählen wir eine Linie aus SC1 . Diese liegt auf einem a-Kreis. Wir durchlaufen von u1 aus diesen Kreis. Danach geht es auf dem einfachen Weg zurück nach a und weiter über die Brücke zu b. Von b aus folgen wir dem zweiten einfachen a-Weg nach u2 , durchlaufen in SC2 einen a-Kreis, gehen auf dem zweiten einfachen Weg zurück nach b, überqueren die Brücke e in entgegengesetzter Richtung und beenden den Weg in a. Der gefundene Weg ist stoppfrei. 2. Wenn e auf einem stoppfreien Weg liegt, können wir annehmen, daß a der Anfangspunkt ist und e die erste Linie. a = v0 , l1 = e, v1 = b, l2 , v2 , . . . , vn−2 , ln−1 , vn−1 , ln , vn = a . Da es sich um einen stoppfreien Weg handelt, gilt e 6= ln und, weil e eine Brücke ist, auch vn−1 6= b. Das bedeutet vn−1 ∈ / [b]. Es muß demnach ein k ≥ 2 geben, so daß vi ∈ [b] (i = 1, 2, . . . , k) und vk = b, vk+1 = a. Wir betrachten den geschlossenen Weg v1 = b, l1 , v2 , . . . , lk , vk = b. Nach Hilfssatz 14.2 kann er als einfach angenommen werden. Hat der Weg die Länge 1, so ist er eine Schlinge. Hat er die Länge 2 und wäre er nicht linieneinfach, so könnte nach Hilfssatz 14.3 der ursprüngliche geschlossene Weg nicht stoppfrei gewesen sein. Er ist also linieneinfach und somit ein a-Kreis. Hat der Weg eine Länge ≥ 3, so ist er nach Satz 14.1 ein a-Kreis. Es gibt also in [b] einen a-Kreis. Analog zeigt man, daß es auch in [a] einen a-Kreis gibt. 2 Eine unmittelbare Folgerung von Satz 16.2 ist, daß eine a-kreisfreie schwache Zusammenhangskomponente keinen stoppfreien Kern haben kann. In Abbildung 16.4 ist das schon dargestellt. Proposition 16.2 Jeder periphere Baum besitzt genau einen Grenzpunkt. 446 KAPITEL 16. DIE BIBLOCKZERLEGUNG Beweis: Es werde ein Weg von einem Knoten im peripheren Baum zu einem Knoten auf einem a-Kreis durchlaufen. Auf diesem Weg gibt es einen ersten Knoten, der Inzidenzpunkt einer Linie eines Kreises oder einer Linie eines internen Baumes ist. Dieser Knoten ist entweder der Anfangspunkt des Weges oder Inzidenzpunkt einer Linie des peripheren Baumes. In beiden Fällen ist es ein Grenzpunkt. Hätte ein peripherer Baum zwei Grenzpunkte, so könnte man sie im peripheren Baum und im stoppfreien Kern durch einfache Wege verbinden und hätte einen a-Kreis durch eine Linie des peripheren Baumes. 2 Subkomponenten und interne Bäume Im Gegensatz zu Biblöcken kann ein Knoten nicht zu mehr als einer Subkomponente gehören, wie sich aus dem folgenden ergibt. Proposition 16.3 Alle Nichtbrücken, die mit dem gleichen Knoten inzidieren, gehören zur gleichen Subkomponente. Beweis: Es sei v Knoten einer Subkomponente und e und f Nichtbrücken, die mit ihm inzidieren. Wir wählen einen a-Kreis,auf dem e liegt, und einen a-Kreis, auf dem f liegt. Es gilt einer der beiden folgenden Fälle. i. Die beiden a-Kreise haben eine Linie gemeinsam. Dann kann man ähnlich wie beim Beweis von Satz 16.1 einen linieneinfachen geschlossenen Weg angeben, auf dem e und f liegen. ii. Die beiden a-Kreise haben keinen gemeinsame Linie. Dann können sie in v zu einem linieneinfachen geschlossenen Weg verbunden werden, auf dem e und f liegen. 2 Korollar: Jeder Knoten eines Graphen gehört zu höchstens einer Subkomponente. Ein Weg, der keine Brücke enthält, heißt brückenfrei (bridgefree). Zwei Knoten einer Zusammenhangskomponente heißen brückenfrei zusammenhängend (bridgefree connected), wenn es einen brückenfreien a-Weg zwischen ihnen gibt. Sie heißen linieneinfach zyklisch zusammenhängend (edge-simply cyclic connected), wenn es einen linieneinfachen, geschlossenen a-Weg gibt, auf dem die Knoten liegen, d. h. wenn sie zur gleichen Subkomponente gehören. Satz 16.3 1. Zwei Knoten u und w einer schwachen Zusammenhangskomponente sind genau dann brückenfrei zusammenhängend, wenn sie linieneinfach zyklisch zusammenhängend sind. 2. Sind die Knoten verschieden und brückenfrei zusammenhängend, dann kann man jeden brückenfreien linieneinfachen a-Weg von u nach w durch einen linieneinfachen Weg von w nach u zu einem geschlossenen linieneinfachen a-Weg ergänzen. 16.3. EIGENSCHAFTEN DER KOMPONENTEN DER BIBLOCKZERLEGUNG 447 Beweis: 1. Keine Kante eines linieneinfachen geschlossenen a-Weges kann eine Brücke sein. Sind u und w linieneinfach zyklisch zusammenhängend, dann gibt es einen brückenfreien a-Weg zwischen ihnen. Für die Umkehrung unterscheiden wir die Fälle u = w und u 6= w. Im ersten Fall gibt es einen brückenfreien geschlossenen Weg durch u. Dann gibt es auch einen geschlossenen linieneinfachen Unterweg durch u. Dieser ist natürlich auch brückenfrei. Für den Fall u 6= w folgt die Behauptung aus 2. 2. Es sei u = v0 , l1 , v1 , l2 , . . . , ln−1 , vn−1 , ln , vn = w und u 6= w. Wir beweisen durch Induktion über n, daß es einen linieneinfachen geschlossenen a-Weg der geforderten Art durch u und w gibt. • n = 1. Das ist Proposition 14.3, Seite 382. • Der Satz sei richtig für brückenfreie Wege der Länge höchstens n. Sei u = v0 , l1 v1 , l2 , . . . , ln , vn , ln+1, vn+1 = w ein brückenfreier a-Weg der Länge n + 1 von u nach w. Ohne Beschränkung der Allgemeinheit kann dieser Weg linieneinfach gewählt werden. Dann kann nach Induktionsvoraussetzung das Teilstück von v0 bis vn zu einem linieneinfachen geschlossen a-Weg durch v0 und vn ergänzt werden. Außerdem gibt es einen a-Kreis durch ln+1 . Dieser und der linieneinfache geschlossene Weg können ähnlich wie beim Beweis von Satz 16.1, Seite 438, zu einem linieneinfachen geschlossenen Weg zusammengesetzt werden, der den ursprünglichen brückenfreien Weg als Teilweg enthält. 2 Die folgenden Proposition gibt einfache, unmittelbar einsichtige Eigenschaften interner Bäume wieder. Proposition 16.4 1. Ein interner Baum hat mit einer Subkomponente höchstens einen Knoten gemeinsam. 2. Ein interner Baum hat mindestens zwei Checkpunkte und hat jeden Checkpunkt mit genau einer Subkomponente gemeinsam. Biblöcke Einige wichtige, leicht zu verifizierende Eigenschaften von Angelpunkten sind in der folgenden Proposition zusammengefaßt. Proposition 16.5 1. Jeder Biblock einer Subkomponente, die mehrere Biblöcke umfaßt, enthält mindestens einen Angelpunkt. 2. Zwei verschiedene Biblöcke einer Subkomponente haben höchstens einen Angelpunkt gemeinsam. 3. Zwei Angelpunkte einer Subkomponente liegen dann und nur dann auf einem gemeinsamen a-Kreis, wenn sie zum selben Biblock gehören. 448 16.4 KAPITEL 16. DIE BIBLOCKZERLEGUNG Klassifikation von Linien und Knoten Eine Linie ist entweder Brücke oder Nichtbrücke. Brücken treten nur in Bäumen, also internen Bäumen, peripheren Bäumen oder a-azyklischen schwachen Zusammenhangskomponenten, auf und Nichtbrücken treten nur in Subkomponenten auf. Die folgende Proposition liefert eine Eigenschaft, die den Typ des Baumes, in dem eine Brücke auftritt, bestimmt. Proposition 16.6 Eine Brücke mit Inzidenzpunkten a und b ist genau dann 1. Linie eines internen Baumes, wenn sowohl [a] als auch [b] eine Subkomponente enthalten. 2. Linie eines peripheren Baumes, wenn entweder [a] oder [b] eine Subkomponente enthält. 3. Linie einer a-azyklischen schwachen Zusammenhangskomponente, wenn weder [a] noch [b] eine Subkomponente enthält. Beweis: 1. Das ist Satz 16.2. 2. Ist e Linie eines peripheren Baumes, so liegt der Grenzpunkt v0 auf einer Seite von e. Er inzidiert mit einer Linie des stoppfreien Kerns und dieser enthält eine Subkomponente. Gäbe es auf der anderen Seite von e auch eine Subkomponente, so gäbe es nach Satz 16.2 einen stoppfreien Weg durch e. 3. Folgt unmittelbar aus 1. und 2. 2 Die bisher gewonnenen Ergebnisse erlauben eine vollständige und einheitliche Klassifikation der a-Eigenschaften von Linien und Knoten eines allgemeinen Graphen. Die Klassifikation ist in den folgenden Tabellen 16.1 und 16.2 angegeben. Bezeichnung Linie einer a-kreisfreien schwachen Zush.-Komp. Linie eines peripheren Baumes Linie eines internen Baumes Nichtbrücke Anmerkungen Brücke. Auf keiner Seite ein a-Kreis. Brücke. a-Kreis auf genau einer Seite. Brücke. a-Kreise auf beiden Seiten. Gehört zu genau einem Biblock, einer Subkomponenten und einem stoppfreien Kern. Tabelle 16.1: Zusammenhangs-Klassifikation von Linien 16.5. DER BIBLOCKGRAPH Bezeichnung Isolierter Kno- 0 ten Endknoten 1 Interner Knoten eines Baumes Interner Knoten eines Biblocks Jede Kombination der folgenden Eigenschaften: ≥2 Inzidenzen ≥2 449 Anmerkungen Einziger Knoten, der nicht zu einer eigentlichen schwachen Zush.-Komp. gehört. Tritt nur in a-kreisfreien schwachen Zush.-Komp. und in peripheren Bäumen auf. Kein Schnittpunkt. Tritt in a-kreisfreien schwachen Zush.Komp., in peripheren Bäumen und in internen Bäumen auf. Schnittpunkt. Kein Schnittpunkt. Falls mehrere zutreffen, fallen die Nicht-Brücken zusammen: Grenzpunkt ≥ 1 für den peripheren Baum. ≥ 2 Linien des stoppfreien Kerns. Gehört zu genau einem stoppfreien Kern. Schnittpunkt. Checkpunkt ≥ 1 für den internen Baum. ≥ 2 Linien der Subkomponente. Gehört zu genau einer Subkomponente. Schnittpunkt. ≥ 2 für jeden Biblock. Gehört zu mindestens zwei Biblöcken. Schnittpunkt. Angelpunkt Tabelle 16.2: Zusammenhangs-Klassifikation von Knoten 16.5 Der Biblockgraph In Beispiel 16.1, Seite 441, wird die Biblockzerlegung des Graphen Ugraph1, Seite 443, angegeben. An diesem Beispiel soll auch eine nützliche abgeleitete Datenstruktur eines Graphen erläutert werden, der Biblockgraph (biblock graph). Der Biblockgraph eines allgemeinen Graphen ist ein ungerichteter Graph. Er wird von dem Ausgangsgraphen abgeleitet und gibt dessen Aufbau aus Biblöcken, peripheren und internen Bäumen und deren Zusammenhang wieder. Uneigentliche und a-kreifreisfreie schwache Zusammenhangskomponten des Graphen sind im Biblockgraph isolierte Knoten. Die a-zyklischen schwachen Zusammenhangskomponenten des Biblockgraphen bilden einen ungerichteten bipartiten Graphen. Dessen Knoten sind einerseits die Biblöcke, peripheren und internen Bäume 450 KAPITEL 16. DIE BIBLOCKZERLEGUNG und andererseits die Verheftungsknoten. Jeder Verheftungsknoten ist mit den Strukturelementen, zu denen er gehört, durch genau eine Kante verbunden. Andere Kanten gibt es nicht. In der Abbildung 16.8 ist der Biblockgraph dargestellt, der zum Graphen Ugraph1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. .. . .. . .. ..... .... .... .... .... .... .... .... .... .... .... .... .... . .. .... . . .. .... . .. ........... ... . .. ... . ..... . ........... .. . ... . ... .. . .. ... ... . . .. ... . . ... . . .. . . ... . . .. .. .. .. ... .. . .. . .. .. . . .. . .. .. . .. .. .. . . . .. 0 15 .. .. . 0 1 2 . .. .. .. . . . . .. .. . . .. ... . . . . . . .. . ... . . .. . ... 0 0 . 3 4 .. . ... . .. . .. . ... . . . ... .. .. . .. .... . . . .... .. . . .. .. . .... .. ....... . . .. .. . .... . ........... . . . . .. . . .................. .... . . . .. . . .... . . . . . . ............ . .. .... ............. .. .. .. . . . .... ............. . .. . . .... . . . . ............. .. . ... . .. .. .. ............. . . ... . . ............. . . . ... . . . . . ............. ... . .. .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ............ .. . .. .. ............. . . . .. . ............. . . . . . . . . ............. . . ............. .... . ... ... .. .................... ............. ......... .......... . ... ....... ... . ... ... .. ... ... ... . . .. .. ... 0 .... . ................. 0 ........... . . . ................. ................. ........... .. . . . . . . . . . . ... .. . . .......... .. . ... . .......... ............ ... . .... ... ... .. .......... ............ . . . . . . . . . . . . . . . . .......... ... . . ........ . . . . . . . . . ... .. . . . . . . . . . . .. ... . .... .... .... .... ............ .... .... .... .... .... .... .... .. ................. . ............ .... .......... ... .. ... . ............ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .......... ... . . .. .. . ....... . . . . . . ... .. . . . . . . . . . . . . . . ..... . . . ... ........... ...... ........... ..... ..... . . . . ... . . .. . . . . . . . . . . . . . . .... . .... ... . . ... . . .... .... ... ... . . ... ... ... . .. .. ... .. .. ... . . .. . . . . . . . ... . . . . ... . .. .. . . . . . . . . . . . . ... .. . . . . ... ... . ... . . . . . . ... ... .. .. . . 0 1 0 0 1 . .. ... . ... . . ... . .... . . .. . . . ... .. .. . . . 0 0 0 0 1 2 . . . . . . . . ... . . . .. .. . . 2 3 2 3 . . ... . . . . . . . . . . . . ... ... . . . .. . ... . .. .. . . ... ... ... ... . .. ... ... . .. . . . . . . . . . . . . . . . .. .... .... ... . . ... . . . . . . . .. . . . . . . . . . . . .. . . . . . .......... .......... .. . . . ......... ......... . . .. . ......... . . . ... .. ..... ......... . . . ..... ..... . . . .. .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .. ..... ..... . ..... ..... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . ...... . . ...... ..... . . ..... ...... . . ..... ...... . . . . . . . . . . . . . . . . . . . . . . . . . . . .......... ...... .. .......... . . ... .. .... . . . .. . ..... . ..... . . ... 0 ..... ... 0 ..... . . ....... ...... . . . . . . . . . . . . ....... ..... . . . . . . . . . . . . . .... .... .... .... .... .... .... .... .... .... .... .... . . .... .... .... .... .... .... .... .... .... .... .... .... .. . . . .. . . .. . . . . .. ........... .. ........... . . . . . . . . . . . . . . . . . . . . . ..... ..... . ..... . .. ...... . . . . . . . . . ... .. ... .. . . . . . ... .... . . . ... .. . ... .. . .. . . .. . . . . . . . ... ... ... . .. . . . .. . . ... ... . . . . . . .. . . ... .. . . . . .. . 0 1 2 0 1 2 . . . . . . . . . . ... . . . . . . . . . . . .. . . .. .......... . .. .. .. .. . . . . . . . ..... ...... . . . ... . . .. . .. . .. . . . ... ..... . ..... . . . . . .... . .. . ... . . . ..... . . . . . . . ... . . . . . . ........... ........... . ... . . .. .. ........... ........... . . ... . . ... ................. . . .. .... .... .... .... .... .... .... .... .... .... .... .... .. .... .... .... .... .... .... .... .... .... .... .... .... . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ... i ,i ,i i .i h a ,...,a b ,c c d f d ,d ,d f ,f ,f d c ,c , c ,c c ,e ,e e ,e c ,d ,f ........... ..... ...... .. ..... ... 2 .... ....... ...... ..... b .. ... .... ... .. .... ... ... ... .. . ... .. ...... . ... .... .. ..... Verheftungspunkt Peripherer Baum oder isolierter Knoten Interner Baum Biblock ...... Stoppfreier Kern .... .... .... Subkomponente d2 , g Abbildung 16.8: Der Biblockgraph zu Graph Ugraph1 gehört. b ,b ,b 16.6. ALGORITHMEN ZUR BESTIMMUNG DER BIBLOCKZERLEGUNG 451 Der Biblockgraph ist kreisfrei (Begründung?). Er ist zusammenhängend, wenn der Ausgangsgraph das ist, und heißt in diesem Fall Biblockbaum (biblock tree). Ein Endknoten eines Biblockbaumes ist stets ein peripherer Baum oder ein Biblock. Die Kanten des Biblockgraphen sind keine Kanten des Originalgraphen und werden deshalb auch Metakanten genannt. Anmerkung 16.1 (Anwendungen der Biblockzerlegung) Die Biblockzerlegung gestattet, es eine Reihe von graphentheoretischen Fragen in einen leicht zu behandelnden allgemeinen Teil und einen harten Teil, der die Lösung in einem Biblock betrifft, aufzuteilen. Für den leicht zu behandelnden Teil ist der Biblockbaum von großem Nutzen. An mehreren Stellen des Buches wird davon Gebrauch gemacht. Ein wichtiges Beispiel sind kürzeste Wege, wie sie in Abschnitt 19.4, Seite 518 untersucht werden. 2 16.6 Algorithmen zur Bestimmung der Biblockzerlegung In diesem Abschnitt werden zwei Algorithmen vorgestellt, mit deren Hilfe man die Biblockzerlegung eines allgemeinen Graphen bestimmen kann. Es sind die Algorithmen PERTREES, Tabelle 16.3, und STPFKERNEL, Tabellen 16.5 und 16.6. Man geht folgendermaßen vor: 1. Zuerst bestimmt man mit dem Algorithmus WCOMP, Tabelle 15.4, Seite 424, die uneigentlichen, die a-kreisfreien und die a-zyklischen schwachen Zusammenhangskomponenten des Graphen. 2. Dann bearbeitet man jede a-zyklische schwache Zusammenhangskomponente: (a) Mit einem Durchlauf durch die Liste ihrer Knoten werden die peripheren Bäume gefunden. Das geschieht mit PERTREES. (b) Danach wird mit dem rekursiven Algorithmus STPFKERNEL die Struktur des stoppfreien Kerns der schwachen Zusammenhangskomponente bestimmt. Es werden die Linienfarben braun, rosa, gelb, magenta, orange, und grün benutzt. STPFKERNEL benutzt außerdem eine Knotenmarkierung. Anfangs sind alle Knoten unmarkiert und alle Linien ungefärbt. Bestimmung der peripheren Bäume Tabelle 16.3 enthält den Algorithmus PERTREES, der die peripheren Bäume einer azyklischen schwachen Zusammenhangskomponente W C findet. Zur Bestimmung der peripheren Bäume wird die Knotenliste von W C durchlaufen und von Knoten vom Grad 1 ausgegangen. Von diesen gibt es einen eindeutig bestimmten a-Weg zum Grenzpunkt des 452 ' KAPITEL 16. DIE BIBLOCKZERLEGUNG PERTREES(W C) $ 1 2 for (jeden Knoten v von W C) { if (v ist mit genau einer Linie e inzident && e ist keine Schlinge) /* v ist ein noch nicht bearbeiteter Endknoten */ 3 { w = v; 4 while (w ist inzident mit genau einer ungefärbten Linie e && e ist keine Schlinge) 5 { färbe e braun; 6 kennzeichne w als Knoten eines peripheren Baumes (kein Grenzpunkt); 7 w = otherend(e, w); 8 }; 9 w als Grenzpunkt kennzeichnen; 10 } }; & Tabelle 16.3: Algorithmus zum Finden der peripheren Bäume Baumes. Dieser Weg wird ausgehend vom Endpunkt durchlaufen, soweit jeweils nur eine ungefärbte Linie vorhanden ist und damit die Richtung eindeutig vorgegeben wird (Zeile 4). Die Linien dieses Wegstücks werden braun gefärbt (Zeile 5). Wird ein Knoten über eine braune Linie erreicht und inzidiert er mit mehr als eine ungefärbten Linie, so wird er als Kandidat für einen Grenzpunkt angesehen (Zeile 9). Das wird jedoch rückgängig gemacht, sobald er nur noch mit braunen Linien inzidiert (Zeilen 6, 7). Hat PERTREES eine schwache a-zyklische Zusammenhangskomponente komplett bearbeitet, so sind in ihr alle Linien, die zu peripheren Bäumen gehören, braun. Alle anderen sind ungefärbt. Alle Knoten sind noch unmarkiert, aber von jedem Knoten ist bekannt, ob er Grenzpunkt, Knoten eines peripheren Baumes, aber kein Grenzpunkt, oder keines von beidem ist. Bestimmung der Struktur des stoppfreien Kerns Der stoppfreie Kern einer schwachen a-zyklischen Zusammenhangskomponente wird mit dem rekursiven Algorithmus STPFKERNEL bestimmt. STPFKERNEL ist im wesentlichen eine a-Tiefensuche. Diese muß allerdings in einem Knoten begonnen werden, der Grenzpunkt ist oder nicht zu einem peripheren Baum gehört. Außerdem muß ein globaler Zähler counter mit 0 vorbesetzt sein. Tabelle 16.4 zeigt diese Initialisierung. Tabelle 16.5 und 16.6 enthalten den rekursiven Algorithmus STPFKERNEL Er wird mit der schwachen Zusammenhangskomponente W C, dem Knoten v und der Eingangslinie entryedge % 16.6. ALGORITHMEN ZUR BESTIMMUNG DER BIBLOCKZERLEGUNG ' INITIALISIERUNG (W C) 1 & 453 2 3 v = erster Knoten in der Knotenliste von W C, der Grenzpunkt oder kein Knoten eines peripheren Baumes ist; counter = 0; ST P F KERNEL(W C, v, NULL); Tabelle 16.4: Anfangsknoten für STPFKERNEL aufgerufen. Beim ersten Aufruf ist entryedge gleich NULL. Beginnend beim ersten Knoten, mit dem STPFKERNEL aufgerufen wird, durchläuft der Algorithmus in einer a-Tiefensuche den gesamten stoppfreien Kern. Nach dem Korollar zu Hilfssatz 15.3, Seite 421, gibt es dabei nur Baumbögen und Rückwärtsbögen. Außer dem ersten Knoten wird jeder Knoten über einen eingehenden Baumbogen zum ersten Mal besucht. Alle anderen Linien, mit denen der Knoten inzidiert, sind entweder eingehende Rückwärtsbögen oder ausgehende Baumbögen oder ausgehende Rückwärtsbögen. Es gibt mindestens einen ausgehenden Bogen und es kann sein, das alle ausgehenden Bögen Rückwärtsbögen sind. Jeder ausgehende Baumbogen bestimmt eindeutig einen Tiefensuchunterbaum. Der aktuelle Knoten wird nicht zum Tiefensuchunterbaum gerechnet. Rückwärtsbögen können aus diesem Unterbaum herausführen. Es gilt, wie leicht zu sehen, die folgende Proposition. Proposition 16.7 1. Ein ausgehender Baumbogen ist genau dann eine Brücke, wenn alle Rückwärtsbögen aus dem zugehörigen Tiefensuchunterbaum in diesem enden. 2. Ein Knoten ist genau dann trennender Schnittpunkt für den Tiefensuchunterbaum eines ausgehenden Baumbogens, wenn alle Rückwärtsbögen des Tiefensuchunterbaumes in diesem oder dem gegebenen Knoten enden. STPFKERNEL bearbeitet alle unmarkierten Knoten. Bei einem Grenzpunkt werden die Linien des peripheren Baumes (braun) aufgesammelt. Dann werden alle ungefärbten Linien des Knoten bearbeitet und dabei gefärbt (Zeilen 5 bis 28). Das entscheidende an STPFKERNEL ist nun ein globaler Zähler counter. Dieser ist anfangs Null und wird für jeden erkannten ausgehenden Rückwärtsbogen, der keine Schlinge ist, um 1 erhöht. Der Bogen wird gelb gefärbt (Zeilen 10 und 11). Bei einem ausgehenden Baumbogen wird der Bogen rosa gefärbt, der Zählerstand in der lokalen Variable outcounter gesichert und die nächste Rekursionsstufe von STPFKERNEL aufgerufen (Zeilen 14, 15 und 16). Nach der Rückkehr zur betrachteten Rekursionsstufe (Zeile 16) wird counter mit outcounter verglichen (Zeile 18). Ist counter größer als outcounter, dann kann es Rückwärtsbögen des Tiefensuchunterbaumes des ausgehenden Baumbogens geben, die im gegebenen Knoten $ % 454 KAPITEL 16. DIE BIBLOCKZERLEGUNG enden. Diese Bögen werden magenta umgefärbt und für sie wird counter um 1 vermindert (Zeile 19, 20 und 21). Da counter nur für Linien vermindert wird, die vorher gelb gefärbt wurden, kann counter keine negativen Werte annehmen. Ist nach diesem Abgleich counter gleich outcounter, so gibt es mindestens eine Rückwärtsbogen aus dem Tiefensuchunterbaum zum gegebenen Knoten, aber keinen zu einem seiner Vorgänger. Der ausgehende Baumbogen liegt auf einem a-Kreis durch den Knoten, also in einem Biblock. Nach Proposition 16.7 trennt der Knoten, diesen Biblock von allen Vorgängern des Knotens, falls es welche gibt. Der Biblock ist mit dem gegebenen ausgehenden Baumbogen vollständig erfaßt. Ausgehend vom aktuellen Knoten werden dann mit einer Tiefensuche alle rosa und magenta Bögen eingesammelt, orange umgefärbt und dem neuen Biblock zugeordnet (Zeilen 22, 23 und 25). Weiter unten wird erläutert, warum das korrekt ist. Ist nach dem Abgleich counter immer noch größer als outcounter, dann gibt es gibt Rückwärtsbögen aus dem Tiefensuchunterbaum zu einem Vorgänger des Knotens. Es gibt einen a-Kreis durch den Vorgänger, den eingehenden Baumbogen des Knotens und den aktuellen ausgehenden Baumbogen. Der entsprechende Biblock ist noch nicht abgeschlossen. Wird nach Rückkehr zur aktuellen Rekursionsstufe festgestellt, daß counter gleich outcounter ist (Zeile 18), so ist counter für alle Rückwärtsbögen des Tiefensuchunterbaumes in diesem abgeglichen worden. Nach Proposition 16.7 ist der ausgehende Baumbogen dann eine Brücke und gehört zu einem internen Baum. Er wird grün gefärbt (Zeile 27). Sind alle ungefärbten ausgehenden Bögen des Knotens bearbeitet (Zeile 28), so wird geprüft, ob counter eine größeren Wert hat als zur Eintrittszeit (Zeile 29). Ist das der Fall, so gehört der eingehende Baumbogen zu einem noch nicht abgeschlossenen Biblock. Eventuell existierende grüne ausgehende Baumbögen des Knotens gehören zu einem internen Baum, der sich nicht über den eingehenden Baumbogen fortsetzt. Der interne Baum ist vollständig erfaßt und wird eingesammelt (Zeile 30). Die aktuelle Subkomponente ist noch nicht abgeschlossen (Zeile 31). Hat counter den gleichen Wert wie zur Eintrittszeit, ist entweder der eingehende Baumbogen eine Brücke (Proposition 16.7) oder es handelt sich um den ersten Knoten der Tiefensuche. Im ersten Fall ist eine Subkomponente abgeschlossen worden oder der Knoten ist innerer Knoten eines internen Baumes. Es wird keine aktuelle Subkomponente zurückgemeldet (Zeile 37). Im zweiten Fall ist entweder auch eine Subkomponente (die letzte) abgeschlossen oder es muß noch ein existierender interner Baum eingesammelt werden (Zeile 35) oder beides und der Algorithmus ist beendet. Es bleibt nachzutragen, warum das Einsammeln der rosa und magenta Linien (Zeilen 23 und 25) korrekt ist. Proposition 16.8 Der Algorithmus STPFKERNEL findet korrekt die Biblöcke, internen Bäume und Subkomponenten des stoppfreien Kerns einer a-zyklischen schwachen Zusammenhangskomponente. 16.6. ALGORITHMEN ZUR BESTIMMUNG DER BIBLOCKZERLEGUNG 455 Beweis: Der Algorithmus arbeitet korrekt, wenn der stoppfreie Kern nur aus einem Biblock besteht. Vom Anfangsknoten der a-Tiefensuche gibt es nur einen ausgehenden Baumbogen. Gäbe es noch einen weiteren, der später durchlaufen wird, so wäre das ein Widerspruch zur Tatsache, daß je zwei Linien, die mit dem Anfangsknoten inzidieren auf einem a-Kreis liegen. Nur nach der Rückkehr über diesen einzigen Baumbogen des Anfangsknoten wird counter gleich outcounter. Nehmen wir an, daß nach Rückkehr zu einem Knoten v, der vom ersten Knoten der Tiefensuche verschieden ist, counter gleich outcounter ist oder nach Ausführung der Anweisungen in Zeilen 19, 20 und 21 gleich wird. Es muß dann einer der folgenden Fälle vorliegen: 1. Der von v ausgehende Baumbogen ist eine Brücke. 2. Der von v ausgehende Baumbogen ist eine Schlinge. 3. Der zum ausgehenden Baumbogen gehörende Tiefensuchunterbaum wird durch v vom Rest des stoppfreien Kerns getrennt. Keiner der drei Fälle ist in einem Graphen, der aus nur einem Biblock besteht, möglich. Da jede Kante entweder magenta oder rosa ist, wird korrekt ein einziger Biblock erkannt und aufgesammelt. Daß STPFKERNEL auch im allgemeinen Fall korrekt arbeitet, soll durch vollständige Induktion über die Anzahl b von Biblöcken des stoppfreien Kerns bewiesen werden. Für b = 1 ist die Aussage richtig. Der Algorithmus arbeite korrekt für alle k mit 1 ≤ k ≤ b. Es sei ein stoppfreier Kern mit b + 1 Biblöcken gegeben. Es soll gezeigt werden, daß beim jedem Ablauf der Tiefensuche auch dieser stoppfreie Kern richtig zerlegt wird. Dazu unterscheiden wir die folgenden Fälle: 1. Der Anfangsknoten ist Knoten eines internen Baumes, aber kein Verheftungspunkt. 2. Der Anfangsknoten ist ein Verheftungspunkt. 3. Der Anfangsknoten ist Knoten eines Biblocks, aber kein Verheftungspunkt. Zum Beweis ist es zweckmäßig, von einer hybriden Darstellung des stoppfreien Kerns auszugehen. In dieser Darstellung wird wie beim Biblockbaum (siehe Abschnitt 16.5) jeder Biblock als ein einziger Knoten aufgefaßt und über eine Metakante mit seinen Verheftungspunkten verbunden. Die Knoten und Linien der internen Bäume sollen jedoch so wie im Originalgraphen angegeben werden. Als Beispiel soll Abbildung 16.9 dienen, die einen Ausschnitt aus einer solchen Darstellung zeigt. Biblöcke werden als Quadrate mit gerundeten Ecken dargestellt. Knoten interner Bäume sind Kreise; wenn sie Verheftungsknoten sind, Doppelkreise. 1. Der Anfangsknoten der Tiefensuche ist Knoten eines internen Baumes, aber kein Verheftungspunkt. 456 KAPITEL 16. DIE BIBLOCKZERLEGUNG . . . . . . . . . . . ........ ...... ... ... ... . . . . . . ........... ..... ... ... .. BLB9 ........ ...... ... ... ... . . . . . ... ... .. . . . . . ......... ......... ..... ... ... .. . .. ... ... ..... ........... . .......... .......... ......... . . . . . . . . ...... . . . . . . . . ..... .......... .............. ......... ......... ..................... ........ ...................... .. .. ..... .... .. ... ... ... ......... .......... ....................... ......... v7 . . . . . . ........... .... ... ... .. ... ... ... . . . . . ......... .. ... ... ..... ........... . ......... ..... ... ... .. ........... ..... ... ... .. BLB8 BLB7 ................... ............................. ... .. .. .. ..... ..... . .. ...... ... .. ............................ .................. . . . . ... ... ... ..... .......... BLB10 . . ........... .... ... ... .. . ............. .............. ..... ........ ............ ... .. .. .. .... .... . .. ....... .. . ............................ ................ .. .. ... ... . . . . . ........ .. ... ... ..... .......... . .......... .......... ......... . . . . . . . . ...... . . . . . . . . . ......... ......... ..................... .......... ... ........................................ .. .. .. .. .... ..... .. ... ... ... .. ........................... .................... . . . . . . . . . . . . . . ......... ....... ........ . . . . . . . . . . . . ... . . ... ... ... ... .. .. ... ... v8 .. .. .. ... . . . . . ........ v6 v5 .................... ... .. . ..... ... ... ....................... ..... ..... ..... ... .. ... ..... ... ... .. ..... ... ... ... ..... ..... ...... .... . . ..... . ......... . . . . . . . ...... ..... ..... .... ..... .......... .......... ..... .......... ..... . . . . . . . . ..... ..... . . . . . . . ..... . . ..... ......... .......... ..... ............. .......... .............. ..... ..... ........ ......................... ..... ... . ..... ..... . . ... ... ..... ... .. ..... ... .. .......... .... ..... ......... ...................................... ..... ......... ... ..... ......... . . . ..... . . . . . ... ..... ......... ..... ......... ..... ......... ..... ......... ..... ......... . . . . . . . . . ..... ........... .. ...... .... ................. ................... ........ ... .. ... .. ..... ..... . .. .. ... ... . . . . ................. ................. v0 BLB5 . ........... ..... ... ... .. BLB6 . .. ... .... . . . . . . . ..... v4 ................ .............................. ...... .. ... .. ..... .... ... .. ...... .............................. ................... ................ .............................. ... .. ...... . .. ..... .... ... .. ...... .............................. ................... v2 v1 .......... ..... ... .. ... ........... ..... ... ... .. BLB1 ... ... .... ....... ...... . . . . . . .. ... ... ... . . . . . . ..... ........... .... ... .. ... BLB2 . . . . .. ... ... .... . . . . . . ..... . . . . v3 ........... ..... ... ... .. ... ... ... ...... ........ ................ .............................. ...... .. ... .. ..... .... ... .. ...... .............................................. .......... ................... .......... ......... .......... .......... .......... ......... ...... ........... .... ........ .......... . . . ........ . . . . ... .... ... . . . . ... ..... ..... .. BLB3 ... ... ... ...... ........ . . . . . . . . . .. ... ... .... . . . . . . ..... BLB4 ... ... ... ..... ......... . . . . .. .. .. .... . . . . . . ..... . . . . ........... ..... ... ... .. . . . . . Abbildung 16.9: Modifizierter Biblockbaum (hybride Darstellung) Als Beispiel nehmen wir den Knoten v0 in Abbildung 16.9. All Linien, mit denen ein solcher Knoten inzidiert, gehören zum gleichen internen Baum. Die Tiefensuche beginnt mit einer solchen Linie (z. B. mit der Kante von v0 zu v5) und durchläuft den daran hängenden Unterbaum des internen Baumes, bevor sie zum Anfangsknoten zurückkehrt. In v5 wird die Tiefensuche (in nicht vorhersagbarer Reihenfolge) einer Linie des Biblocks 16.6. ALGORITHMEN ZUR BESTIMMUNG DER BIBLOCKZERLEGUNG 457 BLB7 und der Kante von v5 zu v7 folgen. BLB7 bildet mit dem Knoten v5 einen Untergraphen, an dem über weitere Verheftungsknoten noch zusätzliche Biblöcke und interne Bäume hängen können. Das ist in der Abbildung durch zwei gestrichelte Linien angedeutet. In dem so erweiterten Untergraphen beginnt mit der Bearbeitung der ersten Linie aus BLB7 ein Ablauf von STPFKERNEL mit Knoten v5 als Anfangspunkt. Nach Induktionsvoraussetzung ist der erweiterte Untergraph korrekt und komplett abgearbeitet, wenn STPFKERNEL über die erste Linie zu v5 zurückkehrt. Insbesondere enthält counter den gleichen Wert, wie beim Eintritt in BLB7, in unserem Fall also 0. Folgt die Tiefensuche der Kante von v5 zu v7, so wird dort begonnen, den Biblock BLB9 und den dahinter liegenden Untergraphen abzuarbeiten. Nach Induktionsvoraussetzung geschieht auch das korrekt und nach der Rückkehr zu v7 hat counter wieder den Eintrittswert, also 0. Kehrt STPFKERNEL über die Kante von v0 zu v5 nach v5 zurück, so sind die Biblöcke BLB7 und BLB9 samt der an ihnen hängenden Untergraphen komplett und korrekt erfaßt, die Kanten von v5 zu v7 und von v0 zu v5 sind grün gefärbt und counter hat den Wert 0. Auf die gleiche Weise läuft STPFKERNEL bei der Bearbeitung der beiden weiteren Kanten ab, die mit v0 inzidieren. Es sind danach die Biblöcke BLB1, . . . , BLB6, BLB8, BLB10 samt daranhängender Untergraphen korrekt erfaßt, die Kanten des internen Baums grün gefärbt und der Endwert von counter ist 0. Als letztes werden die grünen Kanten eingesammelt und ein interner Baum aus ihnen gebildet. 2. Der Anfangsknoten der Tiefensuche ist ein Verheftungspunkt Als Beispielknoten soll v4 dienen. Mit diesem Knoten inzidieren zwei Kanten eines internen Baumes und er ist Angelpunkt von zwei Biblöcken. Bearbeitet STPFKERNEL eine Kante des internen Baumes, so läuft alles so ab, wie unter 1. beschrieben. Tritt STPFKERNEL über eine Linie in einen der Biblöcke BLB5 oder BLB6 ein, so läuft auch alles so ab, wie unter 1 beschrieben. Zum Schluß haben wir in v4 den Wert 0 für counter und müssen noch die grünen Kanten einsammeln und einen internen Baum bilden. 3. Der Anfangsknoten der Tiefensuche ist Knoten eines Biblocks, aber kein Verheftungspunkt. Dieser Fall ist etwas komplizierter. Zur Veranschaulichung wählen wir einen Anfangsknoten in Biblock BLB5. Dieser hat die Verheftungsknoten v04 und v06 und der Anfangsknoten soll von beiden verschieden sein. Nehmen wir zunächst an, der Graph bestünde nur aus dem Biblock BLB5. Dann wird die Bearbeitung eines vom Anfangsknoten verschiedenen Knotens i. a. zu mehreren Baumbögen führen, die von diesem ausgehen. Das gilt auch für die Verheftungsknoten. Es werde nun v06 von der Tiefensuche erfaßt. Dann wird zwischen die Baumbögen, die wieder in BLB5 hineinführen, irgendwann der Baumbogen von v6 nach v8 eingeschoben werden. Dessen Bearbeitung wird, wie unter 2. beschrieben, den mit BLB10 beginnenden Untergraphen nach Induktionsvoraussetzung korrekt erkennen und die Linie von v6 nach v8 grün färben. Bei der Rückkehr zu v6 hat counter den gleichen Wert wie am Beginn der Bearbeitung des Baumbogens. Dieser Wert ist allerdings von 0 verschieden. In BLB5 wird danach die Tiefensuche so fortgesetzt, als wäre die Unterbre- 458 KAPITEL 16. DIE BIBLOCKZERLEGUNG chung nicht erfolgt. Das gleiche passiert bei der Bearbeitung des ersten Baumbogens, der von v6 in den Biblock BLB8 hineinführt. 2 Im obigen Beweis wird nicht auf das korrekte Auffinden von Subkomponenten eingegangen, um die Betrachtungen übersichtlich zu halten. Zu den notwendigen Ergänzungen für Subkomponenten siehe Aufgabe 16.1. Effizienzbetrachtungen Algorithmus PERTREES durchläuft die Knotenliste der schwachen Zusammenhangskomponente und bearbeitet jede Linie eines periphere Baumes einmal beim Braunfärben. Der Aufwand ist O(ns + ms ), wobei ns die Anzahl der Knoten und ms die Anzahl der Linien der schwachen Zusammenhangskomponente sind. Bei a-zyklischen Zusammenhangskomponenten ist stets ns ≤ ms , der Aufwand also O(ms ). Algorithmus STPFKERNEL ist eine Tiefensuche. Beim Umfärben und Aufsammeln wird jede Linie mehrmals angesprochen. Die Anzahl ist für jede Linie aber von der Größe der Zusammenhangskomponente unabhängig. Ohne Berücksichtigung des Aufwandes für das Anlegen der Datenstrukturen ist der Aufwand auch O(ms ). Die Datenstrukturen können in linearer Zeit angelegt werden. Es ist jedoch zweckmäßig, die verschiedenen Listen von Knoten und Linien als Bäume aufzubauen. Das ergibt O(ms · ln(ms )) als Aufwand. STPFKERNEL sammelt in einer Subkomponente alle Biblöcke, die zu dieser gehören. Dabei werden Teilsubkomponenten zusammengefügt (Zeile 17). Der dafür erforderliche Aufwand hängt von der Anzahl der Biblöcke und von deren Lage im Biblockbaum ab. Bei entsprechender Realisierung besteht er aus einem Anteil pro Biblock, der nicht von der Größe des Graphen abhängt, und dem zweimaligen Einfügen in eine Liste von Biblöcken. Das ergibt die Abschätzung O(bn · ln(bn)), wobei bn die Anzahl Biblöcke im Graphen ist. Im allgemeinen wird bn im Vergleich zur Anzahl Linien klein sein und der Aufwand für Zeile 17 vernachlässigbar. In keinem Fall wird jedoch die Abschätzung O(ms · ln(ms )) verletzt. 16.7 Digraphen und vollständige Orientierungen Die Biblockzerlegung berücksichtigt die Orientierung von Linien eines allgemeinen Graphen nicht. Sie erlaubt aber auch orientierungsabhängige Aussagen. Weiß man z. B., daß ein Graph stark zusammenhängend ist, so müssen alle Linien, die in peripheren oder internen Bäumen auftreten, Kanten sein, denn es sind Brücken. Alle Biblöcke sind stark zusammenhängende Untergraphen. Jeder Bogen und jede Kante liegt auf einem f-Kreis, je zwei Linien liegen auf einem a-Kreis. Es ist jedoch nicht richtig, daß je zwei Linien auch auf einem f-Kreis liegen (Aufgabe 16.2). Ein stark zusammenhängender Digraph ist demnach eine einzige Subkomponente. Sie kann aus mehreren Biblöcken bestehen, die durch Angelpunkte getrennt werden. 16.7. DIGRAPHEN UND VOLLSTÄNDIGE ORIENTIERUNGEN 459 Orientierungsklassen und vollständige Orientierung wurden in Abschnitt 12.2, Seite 351, eingeführt. Hier soll untersucht werden, ob es in einer Orientierungsklasse vollständige Orientierungen, also Digraphen, mit bestimmten Eigenschaften gibt, und wie diese von den allgemeinen Graphen der Klasse abhängen. Wir untersuchen zunächst die Frage, ob es f-kreisfreie vollständige Orientierungen gibt. Proposition 16.9 Zu einem ungerichteten Graphen gibt es genau dann eine f-kreisfreie vollständige Orientierung, wenn er schlingenfrei ist. Beweis: Gibt es eine Schlinge, so kann keine vollständige Orientierung f-kreisfrei sein. Ist der Graph schlingenfrei, so kann man eine f-kreisfreie vollständige Orientierung mit einer a-Tiefensuche erhalten. Aus allen Kanten, die als Baumbögen auftreten, werden Bögen in der entsprechenden Richtung und aus alle Kanten, die als Rückwärtsbögen auftreten, werden Bögen in entgegengesetzter Richtung. Zu Baum- und Rückwärtsbögen siehe Abschnitt 15.2. Wenn man die Knoten mit der Aufrufstufe der Tiefensuche markiert, sieht man, daß Bögen stets von einer kleineren zu einer größeren Markierung führen. Es kann also keinen f-Kreis geben. 2 Die Frage, ob es zu einem allgemeinen Graphen eine vollständige f-kreisfreie Orientierung gibt, ist schwieriger zu beantworten. Sicherlich gibt es keine f-kreisfreie vollständige Orientierung, wenn der Graph eine Schlinge aufweist oder schon anfangs einen f-Kreis nur aus Bögen besitzt. Es stellt sich heraus, daß diese Bedingungen nicht nur notwendig, sondern auch hinreichend sind. Proposition 16.10 Ein allgemeiner Graph besitzt genau dann eine f-kreisfreie vollständige Orientierung, wenn er schlingenfrei ist und keinen f-Kreis, der nur aus Bögen besteht, besitzt. Beweis: Gibt es in dem allgemeinen Graphen eine Schlinge oder einen f-Kreis aus Bögen, so kann keine vollständige Orientierung f-kreisfrei sein. Wir nehmen nun an, es sei ein schlingenfreier Graph gegeben, der anfangs keinen f-Kreis nur aus Bögen aufweist und der sich dennoch nicht f-kreisfrei vollständig orientieren läßt. D. h., in welcher Reihenfolge wir auch immer aus Kanten Bögen machen, irgendwann muß ein f-Kreis auftreten, der nur aus Bögen besteht. Wir geben uns nun eine Reihenfolge der Kanten vor. Aus einer Kante machen wir einen Bogen, wenn auch der neue Graph keinen fKreis aus Bögen hat. Andernfalls wählen wir die entgegengesetzte Richtung. Nach unserer Annahme muß es eine erste Kante geben, die weder in der einen noch in der anderen Richtung zu einem Bogen gemacht werden kann, ohne auf einen f-Kreis aus Bögen zu führen. Die beiden Bögen müssen Teil der beiden neu entstehenden f-Kreise sein. Es gibt also einen f-Weg aus Bögen von einem Endpunkt der Kante zum anderen und einen fWeg aus Bögen zurück, wobei die Kante in keinem der beiden Wege als Bogen vorkommt. 460 KAPITEL 16. DIE BIBLOCKZERLEGUNG Dann muß es aber schon vor der Orientierung der Kante einen f-Kreis aus Bögen gegeben haben. Das ist ein Widerspruch zur Annahme, daß es sich um die erste Kante handelt, mit der ein solcher f-Kreis eingeführt wird. 2 Anmerkung 16.2 Proposition 16.10 ist die Grundlage eines einfachen Algorithmus, mit dem eine f-kreisfreie Orientierung eines allgemeinen Graphen gefunden werden kann. Der Algorithmus soll nur skizziert werden. Es sei G ein allgemeiner Graph. 1. Auf dem Untergraphen, der durch die Bögen von G erzeugt wird, prüfe man mit f-Tiefensuche, ob ein f-Kreis vorliegt (Satz 15.2, Nummer 2, Seite 421). Ist das nicht der Fall, so fahre man fort. 2. Es wird die Liste der Kanten durchgegangen. Jede wird zu einem Bogen gemacht und dann wird geprüft, ob ein f-Kreis entstanden ist. Ist das nicht der Fall, wird mit der nächsten Kante fortgefahren. Ist das der Fall, so wird die Kante zu einem Bogen in entgegengesetzter Richtung und es wird auch mit der nächsten Kante fortgefahren. Beim ersten Versuch muß geprüft werden, ob der neue Bogen zu einem f-Kreis führt, der nur aus Bögen besteht. Ist das der Fall, so muß der Bogen selber Teil des f-Kreises sein. Daher beginnt man zweckmäßigerweise die f-Tiefensuche mit dem Zielpunkt des neuen Bogens und beschränkt sie auf Bögen. Die „worst case“-Komplexität des Verfahrens bleibt aber O(m · (m + n)). 2 Wir wollen nun untersuchen, wie es mit vollständigen Orientierungen steht, die in gewisser Weise das Gegenstück zu f-kreisfreien Orientierungen sind, nämlich mit stark zusammenhängenden vollständigen Orientierungen. Damit ein allgemeiner Graph eine stark zusammenhängende vollständige Orientierung besitzen kann, muß er selber stark zusammenhängend sein und darf keine Brücken enthalten. Es stellt sich heraus, daß diese notwendigen Bedingungen auch hinreichend sind. Satz 16.4 1. Ein stark zusammenhängender allgemeiner Graph besitzt genau dann eine stark zusammenhängende vollständige Orientierung, wenn er brückenfrei ist. 2. Ein stark zusammenhängender allgemeiner Graph besitzt genau dann eine stark zusammenhängende vollständige Orientierung, wenn jede Kante auf einem a-Kreis liegt. Beweis: 1. Ist der Graph stark zusammenhängend, so führt jede Orientierung einer Brücke zum Verlust des starken Zusammenhangs. Ist der Graph stark zusammenhängend und brückenfrei, so liegt eine Kante auf einem a-Kreis und ist nach Proposition 14.9, Seite 389, so orientierbar, daß der starke Zusammenhang erhalten bleibt. Weitere Kanten, so vorhanden, liegen weiterhin in einem brückenfreien, stark zusammenhängenden Graphen und können somit ebenfalls orientiert werden, ohne den starken Zusammenhang zu zerstören. 16.7. DIGRAPHEN UND VOLLSTÄNDIGE ORIENTIERUNGEN 461 2. In einem stark zusammenhängenden Graphen liegt genau dann jede Kante auf einem a-Kreis, wenn er brückenfrei ist. 2 Algorithmus Es soll ein Algorithmus skizziert werden, mit dem festgestellt wird, ob ein stark zusammenhängender allgemeiner Graph G eine stark zusammenhängende vollständige Orientierung besitzt, und, wenn das der Fall ist, eine solche gefunden wird. Dabei werden die Überlegungen zum Beweis von Proposition 14.9 benutzt. 1. Mit den Algorithmen aus Abschnitt 16.6 wird die Biblockzerlegung von G bestimmt. Genau dann, wenn diese keine peripheren Bäume aufweist und der stoppfreie Kern nur aus einer Subkomponente besteht, ist der Graph brückenfrei (siehe Seite 444). 2. Ist G brückenfrei, so wird für jede Kante l der Graph G − l gebildet und seine Zerlegung in starke Zusammenhangskomponenten und externen Dag bestimmt (Abschnitt 15.4). Ergibt sich nur eine starke Zusammenhangskomponente, so kann l beliebig orientiert werden. Ergibt sich ein ein nichtleerer externer Dag, so wird die Kante ein Bogen, der vom Endpunkt mit positiver Schichtennummer zum Endpunkt mit Schichtennummer 0 führt. 2 Anmerkung 16.3 Die „worst case“-Komplexität des Verfahrens ist O(m · (m + n)), da für jede Kante l die starke Zusammenhangsstruktur von G − l gefunden werden muß. Für den Fall, daß G ein ungerichteter, brückenfreier, zusammenhängender Graph ist, gibt es jedoch ein effizienteres Verfahren. Siehe hierzu Aufgabe 16.3. Eine Heuristik, die im allgemeinen Fall zu besseren Zeiten führen kann, basiert auf der Feststellung, daß eine Kante, die mit einem Knoten inzidiert, der sonst nur Eingangsbögen hat, zu einem Ausgangsbogen werden muß. Entsprechendes gilt für Kanten, die Eingangsbögen werden müssen. Es werden rekursiv alle diese Kanten mit einer Tiefensuche bestimmt und dann erst mit Schritt 2 des Algorithmus begonnen. Zu Einzelheiten siehe Aufgabe 16.4. 2 Aufgaben Aufgabe 16.1 Ergänzen Sie den Beweis von Proposition 16.8, Seite 454, um Überlegungen, die zeigen, daß STPFKERNEL alle Subkomponenten korrekt erkennt und erfaßt. Benutzen Sie das Beispiel der Abbildung 16.9. Aufgabe 16.2 Geben Sie ein Beispiel für einen stark zusammenhängenden Digraphen, in dem je 2 Bögen auf einem a-Kreis, aber nicht je 2 Bögen auf einem f-Kreis liegen. Aufgabe 16.3 Geben Sie einen möglichst effizienten Algorithmus an, der auf einem brückenfreien, ungerichteten und zusammenhängenden Graphen eine vollständige und stark zusammenhängende Orientierung findet. Welche Komplexität hat Ihr Algorithmus? 462 KAPITEL 16. DIE BIBLOCKZERLEGUNG Aufgabe 16.4 Es ist ein rekursiver Algorithmus anzugeben, der alle Bögen, die Ausgangsbögen werden müssen, und alle Bögen, die Eingangsbögen werden müssen, findet und entsprechend orientiert. Literatur In der Graphentheorie ist es üblich, in schlichten Graphen Blöcke (block) zu betrachten. Das sind maximale Untergraphen ohne Schnittpunkte. Außer den Biblöcken gehören dazu auch isolierte Knoten sowie alle Untergraphen, die von einer Brücke erzeugt werden. Mit den Blöcken wird eine dem Biblockgraph verwandte abgeleitete Graphstruktur, der BlockGraph (block-cutpoint graph), eingeführt, siehe Diestel [Dies2000] oder Harary [Hara1969]. Ein linearer Algorithmus zur Bestimmung der 2-zusammenhängenden Blöcke, also der Biblöcke, ist seit den frühen 70er-Jahren bekannt. Siehe Tarjan [Tarj1972]. Die in diesem Kapitel als Alternative zur Blockzerlegung eingeführte Biblockzerlegung stammt von Stiege [Stie1996b], [Stie1997] und [Stie1998]. Sie ist klarer, übersichtlicher und für Anwendungen besser geeignet. Auch die Algorithmen zu Bestimmung der Biblockzerlegung stammen von Stiege [Stie1997a]. Sie sind von von gleicher Effizienz wie die vorher bekannten, jedoch von größerer Einfachheit. 16.7. DIGRAPHEN UND VOLLSTÄNDIGE ORIENTIERUNGEN ' SUB 463 *STPFKERNEL(WCOMP ∗W C, VERTEX ∗v, EDGE ∗entryedge) integer incounter, outcounter; SUB ∗sub, ∗suba; 1 markiere v; 2 if (v Grenzpunkt) Datenstruktur für peripheren Baum anlegen und mittels Tiefensuche die braunen Kanten aufsammeln; 3 incounter = counter; 4 sub = NULL; 5 for (jede ungefärbte Linie e, die mit v inzidiert) 6 { if (otherend(e, v) markiert) /* Linie ist Rückwärtsbogen */ 7 { if (e Schlinge) 8 { neuen Biblock und, falls nötig, neue Subkomponente anlegen und e einfügen.; } 9 else 10 { färbe e gelb; 11 counter = counter + 1; } 12 } 13 else 14 { färbe e rosa; /* Baumbogen */ 15 outcounter = counter; 16 suba = ST P F KERNEL(W C, otherend(e, v), e) 17 suba zu sub hinzufügen; 18 if (counter > outcounter) 19 { for (alle gelben Linien, die als Rückwärtsbögen in v enden) 20 { Linien magenta umfärben; 21 counter = counter − 1; } 22 if (counter == outcounter) /* Biblock abgeschlossen */ 23 { neuen Biblock und, falls nötig, neue Subkomponente anlegen; 24 beginnend mit e über Tiefensuche alle rosa und magenta Linien einsammeln, orange umfärben und in Biblock einfügen; } 25 } 26 else 27 {färbe e grün; } 28 } } /* Alle Linien des Knotens sind bearbeitet */ & Tabelle 16.5: Rekursiver Algorithmus zur Bestimmung von Biblöcken, Subkomponenten und internen Bäumen (Teil I) $ % 464 KAPITEL 16. DIE BIBLOCKZERLEGUNG ' $ & % 29 if (counter > incounter) /* Es gibt gelbe Rückwärtsbögen, die von v oder einem Nachfolger von v ausgehen und in einem Vorgänger von v enden. */ 30 { rekursiv alle grünen Kanten, falls vorhanden, einsammeln und internen Baum anlegen; 31 return sub; 32 } 33 else /* Subkomponente abgeschlossen oder innerer Knoten eines internen Baumes */ 34 { if (entryedge == NULL /* 1. Knoten der Komponente */ 35 rekursiv alle grünen Kanten, falls vorhanden, einsammeln und internen Baum anlegen; 36 }; 37 return NULL; Tabelle 16.6: Rekursiver Algorithmus zur Bestimmung von Biblöcken, Subkomponenten und internen Bäumen (Teil II) Kapitel 17 Perioden* 17.1 a-Periode und f-Periode Wir beginnen mit einem Beispiel. Abbildung 17.1 zeigt einen stark zusmmenhängenden ..................... ...................... ... .... .... ... .. ... .. ... .... .... .. . .. . ... ... ... .... ... ..... .......................... ....................... ... ... ... . . . . ... .... ...... ...... .... ... ........ ......... ... ... .... .... .... .... ... ... ... .... ... ... .... . . .... . ... . ... ... . . . . . ........... ........... ................. ........... ............ . . . . . . . . ......... ...... . . . . . . . . . . .... .... .. ... .... . . . . . ... ... ... .. ... .. . .................................. .... .. ................................... ........ ... . . ..... . . . . . . ... ... . . .. ... . . . . . ..... ....... . ...... . ... . . . . . . . . . . . . . . . .................. . . . . . . . . ....... ............... ........ .... ... .... . .... ... ... ... ... . . . . ........ .. ........ .... ... .. ...................... ... ..... ... .... . .... .. ... ... .... ...................... V02 V05 V03 V06 V09 V04 Abbildung 17.1: Periode eines stark zusammenhängenden Digraphen Digraphen. Er hat folgende Eigenschaft: Jeder geschlossene Weg, der im Knoten V06 beginnt, hat eine Länge, die ein Vielfaches von 3 ist. Diese Beobachtung soll nun verallgemeinert werden. Zuvor einige Bemerkungen zum größten gemeinsamen Teiler einer Menge natürlicher Zahlen. Es sei A eine nicht leere Menge positiver natürlicher Zahlen. Dann ist der größte gemeinsame Teiler (greatest common divisor) von A definiert durch gcd(A) := max{q | q teilt alle a ∈ A} Da 1 stets gemeinsamer Teiler aller a ist und kein gemeinsamer Teiler größer als das kleinste Element von A sein kann, ist gcd(A) für alle A definiert. Es gilt A ⊆ A0 ⇒ gcd(A) ≥ gcd(A0 ). 465 (17.1) 466 KAPITEL 17. PERIODEN* . Angeregt durch das Beispiel von Abbildung 17.1 kommt man zur folgenden Definition. Definition 17.1 1. Es sei v ein nicht-isolierter Knoten eines allgemeinen Graphen G. Die a-Periode von v (a-period of v) ist der größte gemeinsame Teiler der Längen der a-Wege, die in v beginnen und enden. 2. Es sei v ein Knoten mit Rückkehr eines allgemeinen Graphen G. Die f-Periode von v (f-period of v) ist der größte gemeinsame Teiler der Längen der f-Wege, die in v beginnen und enden. Ist Pa (v) die Menge der geschlossenen a-Wege und Pf (v) die Menge der geschlossenen f-Wege, die in v beginnen, so besagt Definition 17.1 in Formeln a-Periode von v = gcd{len(p)|p ∈ Pa (v)} f-Periode von v = gcd{len(p)|p ∈ Pf (v)} Man kann auch eine b-Periode definieren. Sie stimmt aber immer mit der f-Periode überein. Für isolierte Knoten soll keine a-Periode und für Knoten ohne Rückkehr keine fPeriode definiert werden. In Abbildung 17.1 fällt auf, daß nicht nur V06, sondern alle Knoten des Graphen die f-Periode 3 haben. Der folgende Satz besagt, daß das allgemein gilt. Satz 17.1 Alle Knoten einer schwachen Zusammenhangskomponente haben die gleiche a-Periode. Alle Knoten einer starken Zusammenhangskomponente haben die gleiche f-Periode. Beweis: Wir zeigen, daß je zwei Knoten einer schwachen (starken) Zusammenhangskomponente die gleiche a-Periode (f-Periode) haben. Es seien u und v zwei Knoten einer schwachen (starken) Zusammenhangskomponente eines allgemeinen Graphen. s sei die a-Periode (f-Periode) von u und t die a-Periode (f-Periode) von v. Wir wählen einen geschlossenen a-Weg (f-Weg) von v nach u und von u zurück nach v. Seine Länge sei l1 . Außerdem sei ein beliebiger geschlossener a-Weg (f-Weg), der in u beginnt, gegeben. Dessen Länge sei l2 . Siehe Abbildung 17.2. Wir setzen die beiden Wege zusammen, indem wir auf dem ersten von v nach u gehen, dann auf dem zweiten von u nach u und schließlich auf dem ersten von u zurück nach v. Der zusammengesetzte Weg ist ein a-Weg (f-Weg) von v nach v und hat die Länge l1 + l2 . D. h. es gibt ein k 0 , so daß l1 + l2 = k 0 t. Außerdem gibt es k , so daß l1 = kt. Daraus folgt l2 = l1 + l2 − l1 = (k 0 − k)t Das bedeutet, daß die Länge eines jeden Weges von u nach u ein Vielfaches von t ist. Daraus folgt t ≤ s. Analog zeigt man s ≤ t und erhält s = t. 2 17.1. A-PERIODE UND F-PERIODE p pp pp ppp pp pp pp pp pp p pp pp pp 467 Länge l2 pppppppppppp pp p pp pp Länge l1 ppppppppppppppp pp ppp pp p p ......................... p p pp................................ ...p ..p.. . ... ... .... ... . pp ppp u p p pp ... . ... ... ... ... .. .. .... . . . ....... ................. pp ppp pppppppp p ppp pp p pp ppp p ppppppppppppppp p pp v ... .. .... ... ... .. ... .. . ...... . . .................... Abbildung 17.2: Gegenseitig erreichbare Knoten haben die gleiche Periode Wir können also von der a-Periode einer schwachen Zusammenhangskomponente W C und von der f-Periode einer starken Zusammenhangskomponente SC sprechen und wollen sie mit aper(W C) bzw. fper(SC) bezeichnen. Aus Definition 17.1 und Satz 17.1 ergibt sich unmittelbar die folgende Proposition. Proposition 17.1 Die Länge eines jeden geschlossenen a-Weges in einer schwachen Zusammenhangskomponente ist ein Vielfaches der a-Periode. Die Länge eines jeden geschlossenen f-Weges in einer starken Zusammenhangskomponente ist ein Vielfaches der f-Periode. Der a-Weg von einem nicht-isolierten Knoten zu einem Nachbarn und zurück hat die Länge zwei. Ebenso hat der f-Weg von einem Inzidenzpunkt einer Kante zum anderen und zurück die Länge zwei. Das ergibt deutliche Einschränkungen für die möglichen a-Perioden und f-Perioden. Proposition 17.2 Die a-Periode einer schwachen Zusammenhangskomponente kann höchstens den Wert 2 annehmen. Enthält eine starke Zusammenhangskomponente eine Kante, so kann ihre f-Periode höchstens den Wert 2 annehmen. f-Perioden größer als 2 können also nur in Digraphen auftreten. Eine schwache Zusammenhangskomponente mit a-Periode 1 heißt aperiodisch (aperiodic). Ebenso nennt man eine starke Zusammenhangskomponente mit f-Periode 1 aperiodisch. Eine schwache Zusammenhangskomponente mit a-Periode 2 heißt bipartit (bipartite). Satz 17.2 stellt die Verbindung zur Definition in Abschnitt 12.3, Seite 353, her und rechtfertigt die Bezeichnung. Hat eine schwache (starke) Zusammenhangskomponente eine Schlinge, so ist sie aperiodisch. Sie ist auch aperiodisch, wenn sie geschlossene a-Wege (f-Wege) sowohl gerader als auch ungerader Länge aufweist. a-Bäume haben nur geschlossene Wege gerader Länge (warum?) und deshalb die a-Periode 2. 468 KAPITEL 17. PERIODEN* Von besonderer Bedeutung sind bipartite schwache Zusammenhangskomponenten, das sind im wesentlichen die in Abschnitt 12.3 eingeführten bipartiten Graphen, und starke Zusammenhangskomponenten mit f-Periode größer 1. In Abschnitt 17.2 wird hierauf näher eingegangen. Die Bestimmung der a-Periode (f-Periode) über die Längen der geschlossenen a-Wege (f-Wege) mit festem Anfangspunkt kann mühsam sein. Die folgende Proposition enthält ein Kriterium, mit dem die Perioden auf andere Art bestimmt werden können. Dazu die folgenden Bezeichnungen: Einen geschlossenen a-Weg, der in v beginnt, wollen wir einen a-Weg mit Erstrückkehr zu v (a-path of first return to v) nennen, wenn v kein innerer Knoten des Weges ist. Ganz entsprechend werden f-Wege mit Erstrückkehr zu v (f-path of first return to v) definiert. Proposition 17.3 Es sei v ein Knoten der schwachen Zusammenhangskomponente W C. Dann ist die a-Periode von W C gleich dem größten gemeinsamen Teiler der Längen der Wege mit a-Erstrückkehr zu v. Es sei v ein Knoten der starken Zusammenhangskomponente SC. Dann ist die f-Periode von SC gleich dem größten gemeinsamen Teiler der Längen der Wege mit f-Erstrückkehr zu v. Beweis: Es sei l der größte gemeinsame Teiler der Längen der a-Wege mit Erstrückkehr zu v. Nach Formel 17.1 gilt aper(W C) ≤ l. Andererseits setzt sich jeder geschlossene a-Weg, der in v beginnt, aus aufeinanderfolgenden a-Wegen mit Erstrückkehr zu v zusammen. Die Länge des Weges ist gleich der Summe der Längen der Wege mit Erstrückkehr. l teilt daher die Länge eines jeden geschlossenen a-Weges, der in v beginnt. l teilt daher auch aper(W C) und das bedeutet aper(W C) ≥ l. Zusammen ergibt sich aper(W C) = l. Der Beweis für starke Zusammenhangskomponenten verläuft auf die gleiche Art und Weise. 2 17.2 Periodizitätsklassen In bipartiten Graphen alternieren die Knoten eines a-Weges zwischen den beiden Bipartitionsklassen. Etwas entsprechendes kann man auch bei f-Wegen in Digraphen beobachten. Im Digraphen von Abbildung 17.1 durchlaufen alle von V 06 ausgehenden f-Wege die Knotenmengen {V 06}, {V 03, V 05}, {V 02, V 04, V 09} zyklisch in dieser Reihenfolge. Der folgende Satz besagt, daß dies allgemein gilt. Satz 17.2 Es sei G(V, E, A, ϕ, ψ) ein schwach zusammenhängender (stark zusammenhängender) allgemeiner Graph. p ≥ 2 sei seine a-Periode (f-Periode). Dann gilt 17.2. PERIODIZITÄTSKLASSEN 469 1. Gibt es einen a-Weg (f-Weg) der Länge n0 p von u nach v, so ist die Länge eines jeden a-Weges (f-Weges) von u nach v ein Vielfaches von p. 2. Gegenseitige a-Erreichbarkeit (f-Erreichbarkeit) in np (n ≥ 1) Schritten ist eine Äquivalenzrelation über V . 3. V wird in p Äquivalenzklassen zerlegt. Jeder Schritt auf einem a-Weg (f-Weg) führt von einer Äquivalenzklasse in eine andere. In p Schritten werden alle p Klassen in einer festen Reihenfolge durchlaufen und der letzte Schritt endet in der ersten Klasse der Reihenfolge. Beweis: 1. Der a-Weg (f-Weg) der Länge n0 p läßt sich zu einem geschlossenen a-Weg (f-Weg), der in u beginnt, ergänzen. Dieser hat die Länge n1 p und daher hat der Rückweg die Länge (n1 −n0 )p. Gäbe es einen Weg von u nach v mit einer Länge, die kein Vielfaches von p ist, so könnte man diesen mit dem gefundenen Rückweg zu einem geschlossenen Weg ergänzen, der in u beginnt und dessen Länge kein Vielfaches von p ist. 2. Reflexivität folgt aus der Definition der a-Periode (f-Periode). Transitivität ist unmittelbar klar. Der unter 1. gefundene Rückweg zeigt die Symmetrie. 3. Wir betrachten einen a-Weg (f-Weg) v0 , v1 , · · · , vp−1 der Länge p. Die Knoten müssen zu paarweise verschiedenen Äquivalenzklassen gehören, da es sonst einen geschlossenen Weg einer Länge kleiner p gäbe. Es gibt also mindestens p Äquivalenzklassen. Daß es nicht mehr gibt, sieht man folgendermaßen: Es sei v ein von v0 , . . . , vp−1 verschiedener Knoten. Wir wählen einen a-Weg (f-Weg), der von vp−1 nach v führt, und stellen ihm als Anfangsstück den Weg v0 , . . . , vp−1 voran. Es sei l = np + r mit 0 ≤ r < p die Länge des Weges vp−1 , . . . , v. Ist r = 0, so liegen vp−1 und v in der gleichen Äquivalenzklasse. Ist 0 < r < p, so fehlen p − r Schritte, um auf eine Länge zu kommen, die ein Vielfaches von p ist. Nimmt man diese Schritte hinzu und startet den Weg in vr−1 , so sieht man, daß vr−1 und v zur gleichen Äquivalenzklasse gehören. Es gibt genau p Äquivalenzklassen. Es sei nun v von u in a-Richtung (f-Richtung) in 1 Schritt erreichbar. Ist u aus der gleichen Äquivalenzklasse wie vi mit 0 ≤ i < p − 1, so können wir folgendermaßen von v zu vi+1 kommen: Von v zu u, von u zu vi und von vi zu vi+1 . Für die Längen dieser Wege gilt l1 = n1 p − 1, l2 = n2 p und l3 = 1. Also gehören v und vi+1 zur gleichen Äquivalenzklasse. Ist u aus der gleichen Äquivalenzklasse wie vp−1 , so muß v zu gleichen Äquivalenzklasse wie v0 gehören. Wäre das nicht der Fall, sondern v in der Äquivalenzklasse von vi mit 1 ≤ i < p − 1, so könnte man auf die folgende Art von u zu u kommen: Von u zu v, von v zu vi von vi zu vp−1 und von vp−1 zu u. Für die Längen dieser Wege gilt l1 = 1, l2 = n1 p , l3 = p − 1 − i und l4 = n2 p. Wegen 1 ≤ i wäre die Länge des Gesamtweges kein Vielfaches von p, wie es sein müßte. Damit ist gezeigt, daß in p Schritten alle p Klassen in fester zyklischer Reihenfolge durchlaufen werden. 2 Die durch Satz 17.2 gegebenen Knotenklassen werden für f-Perioden Periodizitätsklassen (periodicity class) genannt. 470 KAPITEL 17. PERIODEN* Anmerkung 17.1 In Abschnitt 12.3, Seite 353, wurden bipartite Graphen über eine Einteilung der Knoten in zwei Klassen eingeführt. Satz 17.2 besagt, daß diese Partition vorliegt, wenn die a-Periode den Wert 2 hat. Es ist umgekehrt nicht schwer, nachzuweisen, daß ein schwach zusammenhängender Graph, dessen Knotenmenge so in zwei Klassen zerlegt werden kann, daß in jeder Klasse die Knoten paarweise nicht benachbart sind, die a-Periode 2 hat Das läßt sich nicht unverändert auf f-Perioden stark zusammenhängender Digraphen übertragen. Gibt es in einem solchen eine Zerlegung der Knotenmenge in q Klassen, die bei q Schritten stets in fester zyklischer Reihenfolge durchlaufen werden, so ist q nicht notwendigerweise die f-Periode des Digraphen. Die Periode ist jedoch ein Vielfaches von q. Siehe hierzu Aufgabe 17.2. 2 Für aperiodische Graphen wird man eine Einteilung der Knoten in Klassen mit Eigenschaften, wie sie in Satz 17.2 beschrieben sind, nicht erwarten. In der Tat, in gewissem Sinne gilt das Gegenteil, wie der folgenden Satz besagt. Satz 17.3 Ist v ein Knoten einer aperiodischen schwachen (starken) Zusammenhangskomponente, so existiert ein n0 ∈ N, so daß es für jedes n ≥ n0 einen geschlossenen Weg der Länge n durch v gibt. Beweis: Sei l die Länge eines kürzesten geschlossene Wegs durch v. Ist l = 1, so geht durch v eine Schlinge und die Behauptung ist richtig für n0 = 1. Sei l > 1. Da v aperiodisch ist und l nur endlich viele Teiler hat, muß es k ≥ 1 weitere Wege von v zu v mit den Längen l1 , · · · , lk geben, so daß ggT (l, l1, · · · , lk ) = 1. Die Behauptung folgt dann aus Hilfssatz C.2 Seite 632. 2 17.3 Ein Algorithmus zur Bestimmung der Periode Es sei eine schwache (starke) Zusammenhangskomponente mit a-Periode (f-Periode) p ≥ 2 gegeben. Dann lassen sich die Periodizitätsklassen auf die folgende einfache Art bestimmen: Man starte eine a-Tiefensuche (f-Tiefensuche) und sammle alle Knoten, die p Schritte voneinander entfernt sind, in einer Klasse. Das Problem ist, p zu finden. Tabelle 17.1 zeigt einen einfachen Algorithmus, der die f-Periode einer starken Zusammenhangskomponente berechnet. Der Algorithmus benutzt f-Tiefensuche und folgt Ausgangsbögen des externen Dags nicht. Die Komplexität ist O(n + m) = O(m). Der Algorithmus startet mit P ERIOD(root, 0), wobei root ein beliebiger Knoten der starken Zusammenhangskomponente ist. Anfangs sind alle Knoten unmarkiert. Knoten werden durch Sätze des Datentyps VERTEX dargestellt. Diese Sätze weisen u. a. ein Feld v→vlevel für ganzzahlige Werte auf, das vom Algorithmus benutzt wird. p ist eine globale Variable und hat den Anfangswert 0. 17.3. EIN ALGORITHMUS ZUR BESTIMMUNG DER PERIODE ' 471 void PERIOD(VERTEX ∗v, int level ) 1 { if (p == 1) return; /* aperiodisch */ 2 if (v ist nicht markiert) 3 { markiere v; 4 v→vlevel = level; 5 for (alle Kanten und Ausgangsbögen l, die mit v inzidieren) 6 { if (l kein Bogen des externen Dag) PERIOD(otherend(l, v), level + 1); 7 } 8 } 9 else 10 { p = gcd(p, |level − v→vlevel|); 11 }; 12 return; 13 } & Tabelle 17.1: Rekursive Prozedur zum Auffinden der f-Periode einer starken Zusammenhangskomponente Der Endwert von p ist, wie weiter unten gezeigt wird, die gesuchte f-Periode. gcd berechnet den größten gemeinsamen Teiler zweier nicht-negativer ganzer Zahlen, die nicht beide 0 sind. Es ist leicht den Algorithmus so abzuändern, daß die a-Periode einer schwachen Zusammenhangskomponente berechnet wird. Dazu läßt man in Zeile 5 alle Linien, die mit v inzidieren, zu und schließt in Zeile 6 auch Bögen des externen Dags nicht aus. Auch in diesem Fall wird der Algorithmus mit P ERIOD(root, 0) aufgerufen. Der Algorithmus PERIOD ist sehr einfach. Daß er tut, was er soll, ist jedoch nicht unmittelbar zu sehen und muß bewiesen werden. Proposition 17.4 Der in Tabelle 17.1 angegebene Algorithmus berechnet korrekt die fPeriode einer starken Zusammenhangskomponente. Beweis: Es sei vr der Knoten mit dem der Algorithmus startet. p̃ sei die gesuchte f-Periode. pf sei der vom Algorithmus gelieferte Endwert von p. Wir zeigen: I. Jede Ausführung von Zeile 10, auch die letzte, weist p einen Wert zu, der ein Vielfaches von p̃ ist. II. Der Endwert pf teilt die Länge eines jeden geschlossenen Weges durch vr . Zusammen ergibt das pf = p̃. $ %