Version vom 28. Februar 1996 Rolf H. M ohring Computerorientierte Mathematik I Einfuhrung in die Rechnerbenutzung Einfuhrung in die Programmiersprache C++ Einuben von Techniken fur das Erstellen und Testen von Programmen und Algorithmen Realisierung von Algorithmen auf dem Rechner Besprechung der Ubungsaufgaben Praktische Ubungen am Rechner 1 Es werden keinerlei Vorkenntnisse aus dem Bereich der Informatik vorausgesetzt. Hinsichtlich der Mathematik sind Kenntnisse im Umfang der Schulmathematik ausreichend. - Die Ubung vertieft diesen Sto in praktischen und theoretischen Aufgaben. Der praktische Teil enthalt eine Einfuhrung in die Rechnerbenutzung an UNIX-Workstations und das Erlernen der Programmiersprache C++. Sie untergliedert sich in folgende Punkte: - Grundlagen des Entwurfs und der Analyse von Algorithmen - Standardalgorithmen und Datenstrukturen - Grundlagen von prozeduralen und objektorientierten Programmiersprachen, insbesondere C++ - Einfuhrung in Aufbau und Funktionsweise von Rechnern (einschlielich Schaltkreistheorie) - Aspekte der Rechnernutzung in der Mathematik: Zahlendarstellung, Computerarithmetik, grundlegende Algorithmen der Diskreten und der Numerischen Mathematik Die Computerorientierte Mathematik stellt die Informatikgrundausbildung an der TU Berlin fur die Studiengange Techno- und Wirtschaftsmathematik sowie Informationstechnik im Maschinenwesen dar. Zugleich bildet sie das Bindeglied zwischen Informatik und Mathematik. Dies auert sich vor allem in der Auswahl der Themen und der algorithmischen Fragen, die in der Veranstaltung behandelt werden. Die Veranstaltung streckt sich uber zwei Semester und vermittelt den Sto in insgesamt 8 Semesterwochenstunden Vorlesung und 8 Semesterwochenstunden Ubung. Die Vorlesung umfat folgende Punkte Vorbemerkungen 2 3 Wir leben im Zeitalter der Computerrevolution. Sie wird vergleichbare Auswirkungen fur die Gesellschafts- und Sozialordnung haben wie die Industrielle Revolution. Wahrend die Industrielle Revolution im wesentlichen eine Steigerung der korperlichen Krafte des Menschen war, ist die Computerrevolution eine Steigerung der geistigen Krafte, eine Verstarkung des menschlichen Gehirns. Die Bedeutung des Computers hat zur Informatik (Computer Science ) als neue wissenschaftliche Disziplin gefuhrt. Sie behandelt alle Aspekte des Computereinsatzes und der Rechnerentwicklung. Wenn man sich einmal fragt, was einen Computer so revolutionar macht, so kann man in erster Naherung zur Beantwortung dieser Frage sagen, da ein Computer eine Maschine ist, die geistige Routinearbeiten durchfuhrt, indem sie einfache Operationen (Basisoperationen) mit hoher Geschwindigkeit ausfuhrt. Ein Beispiel ist etwa das Suchen eines Namens in einer Liste oder das Sortieren einer Menge von Namen in alphabetischer Reihenfolge. Dies bedeutet, da ein Computer naturlich nur solche Aufgaben erledigen kann, die durch solche einfachen Operationen beschreibbar sind. Auerdem mu man dem Computer mitteilen konnen, wie die Aufgabe durchzufuhren ist. Eine solche Beschreibung der Aufgabe fur den Computer nennt man Algorithmus . Ein Algorithmus ist also eine Handlungsvorschrift , und keine Problembeschreibung. Etwas genauer: Ein Algorithmus ist eine prazise, das heit in einer festgelegten Sprache abgefate, endliche Beschreibung eines allgemeinen Verfahrens unter Verwendung ausfuhrbarer elementarer Verarbeitungsschritte zur Losung einer gestellten Aufgabe. In der Informatik mu man diese umgangssprachliche Beschreibung weiter prazisieren durch die Angabe eines geeigneten Modells fur den Computer (Maschinenmodell) und die Angabe der moglichen elementaren Schritte (d. h. der Angabe einer Programmiersprache). Mogliche Modelle in der Informatik sind die Turing-Maschine, die Random-Access-Maschine (RAM) und viele andere. Diese Maschinenmodelle werden genauer in der Theorie der Berechenbarkeit untersucht. Es hat sich gezeigt, da alle gangigen Maschinenmodelle zueinander aquivalent 1.1 Computer und Algorithmen Einleitung Kapitel 1 KAPITEL 1. EINLEITUNG Algorithmus Strickmuster Montageanleitung Backrezept Notenblatt Typische Schritte im Algorithmus 2 rechts, 2 links leime Teil A an Flugel B 3 Eier unterruhren einzelne Noten - Zentraleinheit CPU (1) - Speicher (memory) (2) Diese Komponenten bilden die Hardware . Das sind die physikalischen Einheiten, aus denen sich ein Computer zusammensetzt. Im Rahmen der Vorlesung werden Ihnen die Computer 3. Die Ein-Ausgabegerate (Input and Output Devices ), uber die der Algorithmus und die Daten, die in den Hauptspeicher gebracht werden und uber die der Computer die Ergebnisse seiner Tatigkeit mitteilt. die auszufuhrenden Operationen des Algorithmus, die Information (Daten bzw. Objekte ), auf der die Operationen wirken (wesentliches Kennzeichen des von-Neumann Rechners ). 2. Der Speicher (Memory). Er enthalt 1. Die Zentraleinheit (CPU oder Central Processing Unit ). Sie fuhrt die Basisoperationen aus. Es sind: Abbildung 1.1: Hauptkomponenten eines Computers. Ein-, Ausgabegerate (3) Ein Computer ist also nichts anderes als ein spezieller Prozessor. Ein gangiger Computer hat drei Hauptkomponenten wie sie Abbildung 1.1 zeigt. Proze Pullover stricken Modellugzeug bauen Kuchen backen Beethovensonate spielen Beispiel: sind in dem Sinne, da man das eine Modell durch das andere simulieren kann. Man kann also eine Turingmaschine so programmieren, da sie eine RAM darstellt und umgekehrt (Churchsche These). Die konkrete Ausfuhrung bzw. Abarbeitung des Algorithmus nennt man einen Proze . Die Einheit, die den Proze ausfuhrt heit Prozessor . Ein Proze besitzt zu jedem Zeitpunkt einen Zustand , der den aktuellen Stand der Ausfuhrung angibt. Man beachte, da ein Prozessor nicht nur ein Computer, sondern auch ein Mensch oder ein Gerat sein kann. 4 5 Die Punkte 1.{4. werden deutlich, wenn man sich ein modernes Flugreservierungssystem vorstellt, bei dem parallel in vielen Landern Reiseburos auf zentrale Dateien Buchungen und Stornierungen vornehmen. Man stelle sich einmal vor, wie man das ohne Computer realisieren mute. 4. Kosten Die Kosten der Computer sind in vielen Bereichen niedrig im Vergleich zu aquivalenter menschlicher Arbeit. 3. Speicher Ein Rechner kann riesige Informationsmengen speichern und schnell darauf zugreifen, da seine Speichertechnik den beliebigen Zugri (Random Access) gestattet. Hier existiert ein groer Unterschied zum menschlichen Gehirn, das in der Regel assoziativ arbeitet, d. h. beim Aunden von Informationen wird nicht auf die Adresse der Information zuruckgegrien, sondern man benutzt die Assoziierung mit anderen Informationen (\Eselsbrucken"). 2. Zuverlassigkeit Die Wahrscheinlichkeit fur elektronische Fehler sind auerst gering. Meist sind Absturze oder Unkorrektheiten in Programmen auf Programmierfehler oder logische Fehler zuruckzufuhren. In einem gewissen Sinne ist ein Computer also ein billiger und gehorsamer Diener. Er fuhrt blindlings Befehle aus und wiederholt sie, wenn notig, beliebig oft ohne Beschwerde. Diese Starke ist aber zugleich auch eine Schwache, da die Anweisungen blindlings ausgefuhrt werden, egal ob sie nun den beabsichtigten Ablauf korrekt beschreiben oder nicht. 1. Geschwindigkeit Selbst komplexe Algorithmen mit sehr vielen Basisoperationen konnen schnell ausgefuhrt werden. Man beachte jedoch, da trotz hoher Computergeschwindigkeit Aufgaben bleiben, die zu zeitintensiv sind, um durchfuhrbar zu sein (z. B. die Bestimmung einer Gewinnstrategie beim Schachspielen). Im Rahmen der Komplexitatstheorie wird der Schwierigkeitsgrad von Problemen untersucht, den sie fur die Behandlung auf dem Computer darstellen. sowohl als theoretisches Modell in der Vorlesung, als auch als praktische Hardware begegnen. Die praktische Hardware wird durch UNIX-Workstations dargestellt. Die typischen Schritte in den Algorithmen fur diese Computer sind Statements der Programmiersprache C++, die Sie im Rahmen der Veranstaltung erlernen werden. In theoretischer Hinsicht wird in der Vorlesung ein idealisiertes Rechnermodell (ahnlich zu einer RAM) betrachtet, das C++-Statements verarbeiten kann, aber idealisiert in der Hinsicht ist, da vorab keine Beschrankungen durch Wortlange, grote zu verarbeitende Zahl usw. angenommen werden. Sie spielen erst bei konkreten Implementationen eine Rolle. Wenn man sich einmal fragt, was den Computer denn so revolutionar macht, so lassen sich folgende kennzeichnende Computermerkmale hervorheben. 1.1. COMPUTER UND ALGORITHMEN KAPITEL 1. EINLEITUNG c =a+b Dies ist erreichbar durch die schrittweise Verfeinerung (ein wichtiges Instrument fur die Programmiermethodik) bis hin auf das Verstandnisniveau des Prozessors. So kann z. B. bei der Anleitung zum Stricken die Anweisung \2 links-2 rechts" Teil der Verfeinerung der Anweisung \Zopfmuster" sein. Bei Computern als Prozessor mu der Algorithmus in einer Programmiersprache ausgedruckt werden. Die Schritte im Algorithmus heien dann Anweisung oder Befehl (statement). Ihr Detailliertheitsgrad und ihre konkrete Formulierung ist abhangig von der verwendeten Programmiersprache. Bei den einfachsten Sprachen (Maschinensprachen ) kann jede Anweisung direkt vom Computer interpretiert werden. Dies bedeutet, da Anweisungen jeweils nur kleine Teile des Algorithmus ausdrucken und man lange Programme fur komplexe Aufgaben schreiben mu. Die Programmierung in der Maschinensprache ist also langwierig und muhsam und dadurch auch fehleranfallig. Zur Vereinfachung der Programmierung wurden andere Sprachen entwickelt, die sogenannten hoheren Programmiersprachen . Sie sind komfortabler, da eine Anweisung bereits einen groeren Algorithmusteil abdecken kann, was wiederum die Erstellung vom Programmen erleichtert. Programme in hoheren Programmiersprachen konnen nicht direkt durch die CPU eines Computers interpretiert werden. Der gangige Weg, dies zu erreichen besteht darin, Programme aus hoheren Programmiersprachen in die Maschinensprachen zu ubersetzen , bevor sie ausgefuhrt werden (evtl. uber mehrere Zwischensprachen. Diese Ubersetzung kann selbst wiederum von einem Computer ausgefuhrt werden und ist damit ein automatisierter Teil der schrittweisen Verfeinerung, vgl. Abbildung 1.2. Der Ubergang zwischen problem- und maschinenorientierten Sprachen ist ieend. So wurde beispielsweise in den letzten Jahren die Programmiersprache C entwickelt, die einerseits den Sprachumfang und die Notation einer hoheren Programmiersprache aufweist, andererseits aber viele Funktionen einer Assemblersprache fur eine maschinennahe Programmierung aufweist. Am maschinenorientierten Ende der Skala ist die auf vielen Rechnern vorhandene sogenannte Mikroprogrammierung zu erwahnen, mit deren Hilfe elementare Algorithmen zwischen der Ebene der internen Maschinendarstellung und der Assemblerebene realisiert werden konnen. Wir verdeutlichen dies anhand eines sehr einfachen Beispiels. Die Addition zweier Zahlen a und b und die Zuweisung des Ergebnisses an eine Variable c kann in einer Programmiersprache folgendermaen formuliert sein: die jeweilige Operation ausfuhren konnen. verstehen, was jeder Schritt bedeutet und Die Ausfuhrung eines Algorithmus auf einem Prozessor setzt voraus, da der Prozessor den Algorithmus interpretieren konnen mu, d. h. er mu 1.2 Programmiersprachen 6 Programm in Maschinensprache ? automatisierte Ubersetzung Programm in hoherer Programmiersprache ? Programmierung (Codierung) gewunschter Ablauf wird ausgefuhrt ? Interpretation durch CPU (Decodierung) Algorithmus 7 R1,a R2,b R2,R1 c,R (hole a aus dem Speicher und schreibe a in das Register R1) (hole b aus dem Speicher und schreibe b in das Register R2) (addiere den Inhalt von Register R1 zum Inhalt von Register R2) (schreibe den Inhalt von Register R2 unter dem Namen c in den Speicher) Zusatzlich zur eigentlichen Additionsoperation mussen nun noch die Lese- und Schreiboperationen auf dem Speicher berucksichtigt werden. Fur alle Assembleroperationen sind sehr genaue Kenntnisse uber die Organisation des verwendeten Rechners erforderlich, etwa die Funktionsweise, Anzahl und Benennung der Register. Fur den nur am Ergebnis c interessierten Programmierer ist weder von Interesse, welche Speicheroperationen erforderlich sind, noch, was denn ein Register uberhaupt ist. Schon dieses extrem einfache Beispiel zeigt die Unubersichtlichkeit von Assemblerprogrammen. Auf der Ebene der Mikroprogrammierung konnen die Schritte der Assemblerprogramme weiter zerlegt werden. Dies kann bis hinunter zu Operationen auf einzelnen Bits gehen. Die Mikroprogrammierung erlaubt die Programmierung auch der kleinsten Teilfunktionen eines Rechners. Auf der untersten Maschinenebene erhalt man nur noch intern verschlusselte Darstellungen, mit denen nur noch sehr geduldige und bis ins kleinste mit \ihrem" Rechner vertraute Spe- MOVE MOVE ADD MOVE Die Notation ahnelt der von der Mathematik her bekannten Formelschreibweise und ist unmittelbar verstandlich. Die Formulierung in einer Assemblersprache konnte wie folgt aussehen: Abbildung 1.2: Ubersetzung von Programmen. Hauptthema der Vorlesung 1.2. PROGRAMMIERSPRACHEN KAPITEL 1. EINLEITUNG 0111 1110 1010 1010 0110 1010 1010 1010 1011 0101 1010 1010 1001 0100 1011 1010 1111 0101 0101 1010 1010 0100 0101 1010 0010 0010 0101 1010 1110 1010 0101 1010 1010 1010 0101 1010 1010 1111 0101 1001 Die auf den ersten Personal Computern verfugbare Programmiersprache BASIC wurde speziell im Hinblick auf Interpretation entworfen. Im allgemeinen sind interpretierte Sprachen \strukturschwacher", haben jedoch den Vorteil, da man Programme sehr schnell zum Laufen bekommt. Compilieren erfordert mehr Speicherplatz als interpretieren. Die Fehlersuche ist im allgemeinen muhsamer, dafur laufen compilierte Programme jedoch wesentlich schneller. Die im Rahmen der Vorlesung gelehrte Programmiersprache C++ ist eine compilierende Programmiersprache. Bei beiden Vorgehensweisen erfolgt bei der Ubersetzung eine Syntaxanalyse des Programms. Dabei erlaubt die Interpretation die Suche einfacher Fehler, wahrend die Compilation meistens eine weitergehende Syntaxanalyse und auch eine partielle Semantikuberprufung erlaubt. Es gibt eine ganze Hierarchie von Programmiersprachen, die von einfachen Sprachen (Maschinensprache) uber mittleres Niveau (FORTRAN, BASIC) bis zu hohem Niveau reichen (Pascal, C, C++). Diese Sammlung von Programmen auf einem Rechner nennt man die Software . Auch bei der Software gibt es eine Hierarchie, die am unteren Ende mit der Hardware verknupft ist: die Software-Hardware-Hierarchie (vgl. Abbildung 1.3). Auf der mittleren Ebene ist das Betriebssystem (operating system ) besonders wichtig. Es dient der das Programm als ganzes ubersetzt durch den sogenannten Compiler. Ein Compiler ist also ein Programm, das ein anderes Programm aus dem Quelltext (source code) in maschinenlesbare Form ubersetzt (object code). Der object code steht dann in Maschinensprache fur jeden Aufruf zur Verfugung. Beim compilieren wird jede Anweisung einzeln ubersetzt, vor der Ubersetzung der nachsten Anweisung zunachst die vorige Anweisung ausgefuhrt, bei jedem Lauf des Programms wieder neu ubersetzt. Am anderen Ende der Skala gibt es verschiedene Versuche uber die hoheren Programmiersprachen hinaus in Richtung auf die Problemformulierung in naturlicher Sprache. Bei der Ubersetzung von einer hoheren Programmiersprache in die Maschinensprache unterscheidet man zwischen interpretieren und compilieren . Beim interpretieren wird 0000 0101 0001 1010 zialisten umgehen konnen. Ein ktives Beispiel fur einen Maschinencode konnte lauten: 8 z. B. CPU, Speicher, Ein-, Ausgabegerate Computerhardware z. B. Betriebssystem, Editor, Compiler Systemsoftware z. B. Textverarbeitung, Statistikpacket 9 Die Rolle von Algorithmen ist grundlegend. Ohne Algorithmus gibt es kein Programm und ohne Programm gibt es nichts aufzufuhren. Algorithmen sind unabhangig von einer konkreten Programmiersprache und einem konkreten Computertyp, auf denen sie ausgefuhrt werden. Ein wesentlicher Teil der Vorlesung besteht darin, den Entwurf von Algorithmen unabhangig von der \Tagestechnologie" zu entwerfen und zu studieren. ein Algorithmus entworfen wird, der beschreibt, wie der Proze auszufuhren ist, der Algorithmus als Programm in einer geeigneten Programmiersprache ausgedruckt wird, der Computer das Programm ausfuhrt. Wie wir gesehen haben, erfordert die Durchfuhrung eines Prozesses auf einem Computer, da 1.3 Algorithmen versus Programmiersprachen In der Vorlesung wird als Betriebssystem UNIX verwendet. Eine Einfuhrung in UNIX wird in der Ubung gegeben. Verwaltung und Steuerung der Ein-Ausgabe-Einheiten, z. B. Drucker, Speicherung von Informationen (z. B. auf Diskette), Unterstutzung der gleichzeitigen Benutzung von Computern durch mehrere Benutzer, Bereitstellung von Kommandooberachen (Shells) fur die Benutzer zur Kommunikation mit dem Rechner (Start von Programmen, Kopieren von Files usw.). Abbildung 1.3: Die Software-Hardware Hierarchie. umgebung Programmier- Anwendungssoftware 1.3. ALGORITHMEN VERSUS PROGRAMMIERSPRACHEN KAPITEL 1. EINLEITUNG Neuere einfuhrende Werke, die die gesamte Informatik behandeln, sind AU92, Bro94, GL84]. Die hier gegebene Einleitung lehnt sich an GL84] an. Speziell fur die theoretische Informatik sei auf die umfangreichen Handbucher vL90a, vL90b] verwiesen. Im Hinblick auf den Entwurf und die Analyse von Algorithmen sind in den letzten Jahren eine ganze Reihe guter Lehrbucher erschienen. Besonders empfehlenswert ist CLR90], weitere gute Bucher sind Meh88, Mei91, OW90, Sed92] und die Klassiker Knu73, AHU74]. Die zunehmende Bedeutung von C++ spiegelt sich zur Zeit in einer Flut von Buchern uber C++ und objektorientiertes Programmieren wieder. Der Klassiker hieruber ist das Buch Str94] vom Entwickler der Sprache C++. Es ist allerdings fur Anfanger schwer verstandlich. Eine sehr gute Einfuhrung mit vielen Anwendungen gibt HR94] die C++ Teile der Vorlesung basieren weitgehend hierauf. Besonders ausfuhrlich und anschaulich ist DD94]. Weitere empfehlenswerte Bucher sind Lip92, Poh89, Poh93], sowie Tea93] speziell fur I/O Streams. 1.4 Literaturhinweise - Korrektheit von Algorithmen Hier werden Methoden entwickelt um nachzuweisen, da ein Algorithmus korrekt arbeitet. Diese Methoden sind teilweise wieder automatisierbar (automatisches Beweisen!). - Komplexitat von Algorithmen Dieses Gebiet befat sich mit der Untersuchung des Aufwandes an Laufzeit und Speicherplatz und der Ermittlung in Form von unteren Komplexitatsschranken fur Problemklassen und der Entwicklung von \schnellen" Algorithmen zu ihrer Losung. Im Rahmen der Vorlesung wird dies bereits an einfachen Beispielen (Sortieren) erlautert. - Berechenbarkeit Gibt es Prozesse, fur die kein Algorithmus existiert? Die Antwort auf diese Frage und das Studium dessen, was Berechenbarkeit ist, d. h. auf einem Algorithmus ausfuhrbar ist oder nicht, ist Gegenstand dieses Gebietes. - Entwurf (Design) von Algorithmen Dies ist im allgemeinen eine schwierige Tatigkeit, die viel Kreativitat und Einsicht erfordert (es gibt keinen Algorithmus zum Entwurf von Algorithmen). Dieses Thema ist ein wesentlicher Gegenstand der Vorlesung. Uberspitzt gesagt sind Algorithmen wichtiger als Computer und Programmiersprachen. Programmiersprachen sind nur Mittel zum Zweck, um Algorithmen in Form von Prozessen auszufuhren. Naturlich sind auch Computer und Programmiersprachen wichtig, da sie z. B. die Ausfuhrgeschwindigkeit eines Programms und den Aufwand zur Erstellung des Programms bestimmen, aber sie sind letztlich nur Mittel zur eektiveren Darstellung und Ausfuhrung von Algorithmen. Wegen dieser grundlegenden Bedeutung von Algorithmen gibt es viele Gebiete der Angewandten Mathematik und der Informatik, die sich mit Algorithmen beschaftigen. Dies sind z. B. 10 11 Die Analyse von Algorithmen verlangt Grundkenntnisse aus der Diskreten Mathematik (vor allem aus der Kombinatorik und der Graphentheorie. Die notigen Techniken sind u. a. in CLR90] enthalten. Weiterfuhrende, empfehlenswerte Bucher sind Aig93, Gri94, Wii87]. 1.4. LITERATURHINWEISE 12 KAPITEL 1. EINLEITUNG 13 1 Der anekdotischen Uberlieferung zufolge kamen diese auf folgende Weise zustande. Fahrenheit wollte eines Tages eine \normierte" Temperaturskala entwickeln. Es war gerade Winter und ziemlich kalt (namlich ;16 79 Grad Celsius). Da er sich keine kaltere Temperatur vorstellen konnte, normierte er diese zu 0. Anschlie end wollte er die normale Korpertemperatur des Menschen zu 100 normieren. Da er aber an diesem Tage leichtes Fieber hatte, wurden daraus 37 79 Grad Celsius. 1. Einlesen von F 2. Umrechnung in C 3. Ausgabe von C Algorithmus 2.1 (Temperatur Umrechnung) Dies resultiert in den folgenden Algorithmus. C = 95 (F ; 32) : Dafur nutzt man den Zusammenhang zwischen beiden Temperaturskalen. Beide Skalen haben eine aquidistante Unterteilung mit folgenden Entsprechungen 1 : 0 Grad Fahrenheit = ;16 79 Grad Celsius, 100 Grad Fahrenheit = 37 97 Grad Celsius. Hieraus lat sich die Temperatur C in Celsius als lineare Funktion der Temperatur F in Fahrenheit berechnen, vgl. Abbildung 2.1: 2.1.2 Der Algorithmus: Temperaturangaben in Fahrenheit sollen in Celsius umgerechnet werden. 2.1.1 Das Problem: 2.1 Temperatur Umrechnung Probleme, Algorithmen, Programme: Einige Beispiele Kapitel 2 32 0 100 37 78 50 F C (10) (2) (3) (4) (5) (6) (7) (8) (9) (1) (1) (1) (1) Alles nach // bis zum Ende einer Zeile ist Kommentar. Die Kommentare enthalten den Namen des Programms (temperatur.cc) und Informationen daruber, was das Programm tut. Kommentare sollten auf Englisch sein, man wei nie, wer das Programm einmal verwenden mu. (2) Zeilen, die mit # beginnen, sind Praprozessor Direktiven, die vor der Compilierung des Programms durch den Praprozesor ausgefuhrt werden. #include <iostream.h> bedeutet, da der Praprozesor diese Zeile durch den Inhalt des Files iostream.h ersetzt. Dieses File stellt C++ Klassen mit Operatoren und Funktionen zur Handhabung von Input/Output in Form von sogenannten Streams zur Verfugung. Standardmaig stehen jedem C++ Programm drei Streams zur Verfugung: Wir erlautern jetzt die Bedeutung der einzelnen Anweisungen gema der Numerierung am rechten Rand. #include <iostream.h> void main() { double celsius, fahrenheit cout << "Geben sie eine Temperatur in Fahrenheit an: " cin >> fahrenheit celsius = (5.0/9) * (fahrenheit - 32) cout << fahrenheit << " Grad Fahrenheit entspricht " << celsius << " Grad Celsius.\n" } temperatur.cc // temperatur.cc // // Transforms Fahrenheit to Celsius Programm 2.1 Wir sehen uns jetzt ein zugehoriges C++ Programm an. Zunachst der Programmtext: 212 - 100 - KAPITEL 2. PROBLEME, ALGORITHMEN, PROGRAMME Abbildung 2.1: Die Celsius- und Fahrenheitskala. 0 ;17 78 2.1.3 Das Programm 14 ist eine Funktion ohne Argumente, daher die leeren Klammern. void gibt an, da die Funktion keine Werte zuruckgibt. I. a. steht hier ein Bezeichner fur den Typ des Ruckgabewertes. 2 Jedes C++ Programm mu genau eine Funktion namens main haben. Aus main konnen weitere Funktionen aufgerufen werden. main() 15 der Wert der Variablen fahrenheit, der String " Grad Fahrenheit entspricht " (ohne die "), der Wert der Variablen celsius, der String "Grad Celsius.\n" (ohne die "), 2 Manche Compiler verlangen den Ruckgabetyp int (ganze Zahlen) bei main zur moglichen Ruckgabe eines Fehlerstatus. ausgegeben. Dabei steht \n fur das (nicht schreibbare) Zeilenende-Zeichen, das den Cursor auf die erste Position der neuen Zeile setzt. - (9) bewirkt eine Ausgabe auf dem Bildschirm. Nacheinander werden (8) ist eine Zuweisung . Der Variablen celsius wird der Wert des Ausdrucks (5.0/9) * (fahrenheit-32) zugewiesen. Da / sowohl die ganzzahlige Division mit Rest, als auch die reellwertige Division bezeichnet, mu man (z. B. durch Angabe von 5.0 statt 5) dem Compiler klar machen, da hier die reellwertige Division gemeint ist. Beachte: 5/9 ergibt den Wert 0, aber 5.0/9 den Wert 0.555556. (7) liest den uber die Tastatur eingegebenen Wert in die Variable fahrenheit bei Drucken der <RETURN> Taste. Gema der Denition von fahrenheit als double werden Gleitkommazahlen erwartet (die auch ganze Zahlen sein durfen). Andere Zeichen werden uberlesen. (6) schreibt den String Geben Sie ... an: auf cout, also auf den Bildschirm. Die Quotes "..." dienen als Begrenzer des Strings, werden aber nicht geschrieben. (5) deniert die Variablen celsius, fahrenheit vom Datentyp double. Dieser bezeichnet Gleitkommazahlen doppelter Prazision (daher die Bezeichnung double), also im Rechner darstellbare reelle Zahlen mit groer Genauigkeit. celsius, fahrenheit konnen also reelle Werte annehmen, aber keine anderen wie z. B. Buchstaben oder Strings. Die Bezeichner celsius und fahrenheit sind mnemonisch gewahlt, d. h. aus den Namen lat sich leicht auf die Bedeutung schlieen. Man sollte stets mnemonische Bezeichner verwenden. (4) { (10) Die geschweiften Klammern f...g enthalten den Block der Funktion main. Im Block wird festgelegt, was die Funktion main leisten soll. (3) { Standard Output (cout) auf den Bildschirm, { Standard Input (cin) von der Tastatur, { Error Output (cerr) fur Fehlermeldungen. 2.1. TEMPERATUR UMRECHNUNG KAPITEL 2. PROBLEME, ALGORITHMEN, PROGRAMME nicht initialisiert nicht initialisiert . 40 35.0 95.0 3 Um genau zu sein, auch einen Wert, dieser wird jedoch durch das Semikolon unterdruckt. In einem (hypothetischen) Steuermodell sind Einkommenssteuern S in Abhangigkeit des zu versteuernden Einkommens E zu zahlen. Das Steuermodell verwendet den sogenannten 2.2.1 Das Problem 2.2 Einkommensteuerberechnung Man beachte: Ein Ausdruck hat einen Wert . Eine Zuweisung hat einen Eekt 3. celsius fahrenheit celsius = (5.0/9)*(fahrenheit-32) Ein entsprechendes C++ Programm ist: 2.2.3 Das Programm 1. Einlesen von E 2. Berechnung von S 3. Ausgabe von S 80 100 - void main() { #include <iostream.h> steuer.cc // steuer.cc // // calculates taxes depending on the income in a piecewise linear fashion Programm 2.2 60 Einkommen E in TDM !! !!! ! ! !!! ! ! ! Abbildung 2.2: Die Einkommensteuer Kurve. 20 Algorithmus 2.2 (Steuer Berechnung) 0 Steuer S in TDM bewirkt die Auswertung des Ausdrucks (5.0/9)*(fahrenheit-32) zu (5:0=9) (95 ; 32) = 59 63 = 35 und die Zuweisung an die Variable celsius. 8 24 6 Aus dem Scheibentarif ergibt sich die Einkommensteuer S als stuckweise lineare, monoton steigende Funktion von E , vgl. Abbildung 2.2. 2.2.2 Der Algorithmus E 20:000 DM 0 DM Steuer 20:000 < E 60:000 20% von E ; 20:000 E > 60:000 DM 8:000 + 40% von E ; 60:000 Dies resultiert in den folgenden Algorithmus. 95.0 nicht initialisiert 17 Stufen- oder Scheibentarif, in dem das Einkommen E in Scheiben unterteilt wird, die mit unterschiedlichen Satzen besteuert werden. Hier sind es 3 Scheiben: 2.2. EINKOMMENSTEUERBERECHNUNG Die Zuweisung celsius fahrenheit wird der Variablen fahrenheit der eingelesene Wert zugewiesen. cin >> fahrenheit Die Eingabe von 95 bewirkt folgendes. Durch fahrenheit celsius erzeugt im Speicher die (uninitialisierten) Objekte double fahrenheit, celsius Beispiel 2.1 Die Denition Wir haben im Programm temperatur.cc zwei wichtige Begrie kennengelernt: Variable und Zuweisung . Eine Variable ist ein Name (Platzhalter) fur Objekte (Daten) eines Typs, z. B. des Typs \reelle Zahl". Sie belegt im Speicher einen bestimmten (dem Benutzer unbekannten) Speicherplatz, dessen Groe vom vereinbarten Typ des Objekts abhangt. Dieser Speicherplatz ist unter dem Namen der Variablen ansprechbar. Eine Zuweisung aktualisiert den Wert (Inhalt) des Speicherplatzes. 16 KAPITEL 2. PROBLEME, ALGORITHMEN, PROGRAMME const double noTaxBound = 20000, lowRate = .2, lowTaxBound = 60000, highRate = .4 double income,tax cout << "Geben Sie das zu versteuernde Einkommen an: DM " cin >> income if (income <= lowTaxBound) // no tax tax = 0 else if (income <= lowTaxBound) // low rate applies tax = lowRate * (income - noTaxBound) else tax = lowRate * (lowTaxBound - noTaxBound) // low rate applies + highRate * (income - lowTaxBound) // high rate applies cout << "Es sind " << tax << " DM Steuern zu zahlen.\nIhr Finanzamt.\n" bestehen. Das erste Zeichen darf keine Zier sein (und sollte auch nicht der Underscore sein, der vom C++ Compiler als erstes Zeichen fur interne Namensgebungen genutzt wird). Bei Bezeichnern wird Gro- und Kleinschreibung unterschieden. a...z A...Z 0...9 Konstanten sollte man immer uber Bezeichner ansprechen, da man so ihre Werte bei einer moglichen Anderung nur an einer Stelle andern mu. (Ein Negativbeispiel war die Umstellung der Postleitzahlen die Anzahl der Zeichen hierfur war meist nie als Konstante deklariert worden, was einen immensen Umstellungsaufwand erforderte und ganze Programmsysteme lahmlegte). Die if-Anweisung (if-statement) hat in C++ die Form if (Bedingung ) Anweisung 1 else Anweisung 2 wobei der else-Teil fehlen kann. Dabei ist Bedingung ein ganzzahliger Ausdruck (also ein Ausdruck, der einen ganzzahligen Wert ergibt). Ist sein Wert ungleich 0 (Bedingung ist \wahr"), so wird Anweisung 1 ausgefuhrt, ansonsten (Bedingung ist \falsch") Anweisung 2 (bzw. nichts, falls der else-Teil fehlt). Anweisung 1 bzw. Anweisung 2 konnen wieder if-statements sein, wodurch eine Verschachtelung wie im obigen Programm auftritt. Wieder wurden suggestive, mnemonische Namen als Bezeichner verwendet und Kommentare eingefugt, was die Lesbarkeit des Programms erhoht. Als Bezeichner (Identier ) kommen alle Strings in Frage, die aus den Zeichen Die if-Anweisung. Die Denition von Konstanten. Hier sehen wir zusatzlich: } 18 2 IN n > 0 soll die kleinste Primzahl p bestimmt 19 4 IN bezeichnet die Menge der naturlichen Zahlen 0 1 2 : : : Algorithmus 2.3 und 2.4 werden jetzt in ein C++ Programm umgesetzt. Setze k := 2 Setze teilerGefunden := falsch fnoch kein Teiler gefundeng Solange (teilerGefunden = falsch) und (k k z ) fuhre aus Teile z durch k. Sei rest der entstehende Rest Falls rest = 0 so setze teilerGefunden := wahr Setze k := k + 1 fz ist Primzahl falls teilergefunden = falsch beim Austritt aus der Schleifeg Algorithmus 2.4 (Primzahltest) Der Test, ob z eine Primzahl ist, geschieht nach folgender Idee: Teile z durch alle Zahlen k = 2 3 : : : (bis k k z ) und prufe ob der Rest 0 ist. Ist dies fur ein k der Fall, so ist z keine Primzahl, andernfalls (d. h. keines der k teilt z ohne Rest) ist z eine Primzahl. Diese Idee fuhrt zu dem folgenden Algorithmus. Hierin ist teilerGefunden eine sogenannte Boolesche Variable , also eine Variable, die nur die Werte wahr oder falsch annimmt. Die geschweiften Klammern f: : :g enthalten Kommentare. Lese die Zahl n ein Setze z := n Wiederhole Erhohe z um 1 Uberprufe ob z Primzahl ist bis z Primzahl ist Gebe z aus Algorithmus 2.3 (Primzahl) Der Algorithmus zur Losung dieses Problems basiert auf der folgenden Idee. Prufe Zahlen n + 1 n + 2 n + 3 : : :, ob sie durch eine kleinere naturliche Zahl k > 1 ohne Rest teilbar ist. Die erste Zahl z , die das nicht ist, ist die gesucht Primzahl. 2.3.2 Der Algorithmus Zu einer gegebenen naturlichen Zahl n werden, die groer als n ist.4 2.3.1 Das Problem 2.3 Primzahl 2.3. PRIMZAHL primzahl.cc the input integer read from terminal candidate for the prime, set to n+1, n+2 etc. possible divisor of z Boolean, indicates that a divisor k of z has been found Neu sind hier folgende Konstrukte fur Schleifen: while (Fortsetzungsbedingung ) Anweisung bzw. do Anweisung while (Fortsetzungsbedingung ) Im ersten Fall wird Anweisung so oft ausgefuhrt, wie Forsetzungsbedingung gilt. Diese Bedingung wird vor jedem Eintritt in die Schleife uberpruft. Evtl. wird die Schleife also auch kein mal durchlaufen. cout << "Geben Sie die Zahl n ein: " cin >> n z = n do { z++ k = 2 divisorFound = 0 // so far no divisor of z has been found while ( !divisorFound && k*k <= z ) { if ( z % k == 0 ) // check if k is a divisor of z { divisorFound = 1} k++ }\\endwhile } while ( divisorFound ) cout << "Die naechstgroessere Primzahl nach " << n << " ist " << z << ".\n" // // // // // method: apply prime test to n+1, n+2, ... until prime is found, prime testing is done by testing numbers k = 2 ... with k * k <= z if they are factors input: integer n output: smallest prime number p with p > n void main() { int n, z, k, divisorFound } KAPITEL 2. PROBLEME, ALGORITHMEN, PROGRAMME primzahl.cc #include <iostream.h> // // // // // // // // Programm 2.3 20 21 % 3 ergibt 2 1 1 1 1 0 0 1 1 1 1 0 0 50 51 3 50 51 4 4 2 2 2 50 50 50 50 50 52 2 50 52 3 50 50 50 50 53 53 53 53 3 2 2 2 0 50 51 3 52 52 52 52 0 0 50 51 51 2 51 2 51 2 divisorFound 50 50 50 50 50 50 k z n Aktion im Programm uninitialisiert bei Denition nach cin >> n nach z = n nach z++ nach k = 2 nach divisorFound = 0 Eintritt in while-Schleife z % k = 51 % 2 6= 0 nach k++ neuer Eintritt in while-Schleife z % k = 51 % 3 = 0 \then" statement ausgefuhrt nach k++ kein neuer Eintritt in while-Schleife neuer Eintritt in do-Schleife nach z++ nach k = 2 nach divisorFound = 0 Eintritt in while-Schleife z % k = 52 % 2 = 0 \then" statement ausgefuhrt nach k++ kein neuer Eintritt in while-Schleife neuer Eintritt in do-Schleife nach z++ nach k = 2 nach divisorFound = 0 Eintritt in while-Schleife Tabelle 2.1: Werte der Variablen in Programm 2.3 Betrachten wir das Programm fur das Zahlenbeispiel n = 50. Tabelle 2.1 dokumentiert die Veranderung der Werte der Variablen n, z, k, divisorFound wahrend des Programmablaufs (von oben nach unten). Neu sind ferner folgende Operatoren: ! logische Negation && logisches und % Rest bei ganzzahliger Division, 8 Im zweiten Fall erfolgt die Uberprufung der Fortsetzungsbedingung nach jeder Abarbeitung von Anweisung . Anweisung wird also mindestens einmal ausgefuhrt. 2.3. PRIMZAHL 0 0 0 0 0 0 0 0 0 0 50 53 5 50 53 5 50 53 6 50 53 6 50 53 7 50 53 7 50 53 8 50 53 8 divisorFound 50 53 4 50 53 4 k 0 0 z 50 53 3 50 53 3 n Dabei ist z % k == 0 ein Boolescher Ausdruck, dessen Wert der Variablen divisorFound zugewiesen wird. Ebenfalls moglich ware divisorFound = (z % k == 0) kann ersetzt werden durch die (allerdings schwer lesbare) Zuweisung if ( z % k == 0 ) f divisorFound = 1 g 1. Die if-Anweisung Der Algorithmus kann auf verschiedene Weisen verbessert und schneller gemacht werden. - Da es zu jeder Zahl n eine Primzahl p mit p > n gibt, terminiert auch die do-Schleife nach endlich vielen Schritten (keine Endlosschleife). - Die do-Schleife terminiert mit einem z , fur das kein Teiler gefunden wurde. - Die while-Schleife testet alle in Frage kommenden Zahlen k z ob sie Teiler von z sind. p Aktion im Programm z % k = 53 % 2 6= 0 nach k++ neuer Eintritt in while-Schleife z % k = 53 % 3 6= 0 nach k++ neuer Eintritt in while-Schleife z % k = 53 % 4 6= 0 nach k++ neuer Eintritt in while-Schleife z % k = 53 % 5 6= 0 nach k++ neuer Eintritt in while-Schleife z % k = 53 % 6 6= 0 nach k++ neuer Eintritt in while-Schleife z % k = 53 % 7 6= 0 nach k++ k*k = 8 8 > 53 ) kein neuer Eintritt in while-Schleife divisorFound == 0 ist wahr ) kein neuer Eintritt in do-Schleife z = 53 ausgegeben KAPITEL 2. PROBLEME, ALGORITHMEN, PROGRAMME Die Korrektheit des Programms folgt aus den Voruberlegungen: 22 23 Die Beispiele sind OSW83] entnommen. Dort nden sich insgesamt 100 Beispielaufgaben fur einfache Programme und zugehorige Losungsalgorithmen (allerdings in Pascal). Weitere Beispiele fur einfache C++ Programme sowie fur eine gute Dokumentation und die Wahl der Bezeichner ndet man in HR94]. 2.4 Literaturhinweise if (z \% k == 0) { divisorFound = 1 } else { k = k+2 } Dabei ist k += 2 aquivalent zu k = k+2. 4. Eine andere Verbesserung besteht darin, in der while-Schleife eine unnotige Erhohung der Variablen k zu vermeiden. Dies kann erreicht werden durch: . . . cin >> n if (n%2 == 0) {z = n-1} else {z = n} // this makes z+2 smallest odd number > n do { z += 2 k = 3 divisorFound = 0 while ( !divisorFound && k*k <= z ) { divisorFound = (z % k == 0), k += 2 }\\endwhile } while (divisorFound) . . . 2. Es kommen nur ungerade Zahlen als Primzahlen in Frage. Daher kann man als ersten Wert von z die erste ungerade Zahl > n nehmen und dann stets z um 2 erhohen. 3. Da z ungerade gewahlt wird, kommen nur ungerade Zahlen k als Teiler von z in Frage. Der Einbau dieser Anderungen ergibt (Anfang und Ende wie in Programm 2.3): divisorFound = !(z % k) 2.4. LITERATURHINWEISE 24 KAPITEL 2. PROBLEME, ALGORITHMEN, PROGRAMME logischer Ausdruck logischer Ausdruck arithmetischer Ausdruck 25 In C++ unterscheidet man lvalues und rvalues . Beide sind Ausdrucke, jedoch konnen lvalues in Zuweisungen nur links vom Zuweisungszeichen = stehen. Genauer: lvalues bezeichnen 3.1.1 Ausdrucke, genaue Erklarung Haben a,: : :,e die Werte 1 2 : : : 6, so liefern die Ausdrucke die Werte 0 (falsch), 1 (wahr), bzw. 4.5. Der Vorrang von Operatoren ist im Zweifelsfall durch Klammern zu regeln. a>b a*b/c <> c+d*e (a+b)*3/2 Ein Ausdruck ist grob gesprochen eine Formel oder Rechenregel, die stets einen Wert (Resultat) speziziert. Der Ausdruck besteht aus Operatoren und Operanden . Beispiele fur Operanden sind Konstanten, Variablen, Funktionen Beispiele fur Operatoren sind die arithmetischen Operatoren + - * / und die logischen Operatoren ! && || (nicht, und, oder). Operatoren sind immer in Zusammenhang mit zugehorigen Wertebereichen (im Programmiersprachen Jargon: Datentypen oder Typen ) zu sehen, z. B. ganze Zahlen oder Gleitkommazahlen. Sie werden daher in Zusammenhang mit Datentypen in Kapitel 5 noch eingehender diskutiert. Beispiele fur Ausdrucke in C++ sind: 3.1 Ausdrucke Wir behandeln nun ubersichtsartig die wichtigsten Sprachelemente in (hoheren) Programmiersprachen fur die Formulierung von Algorithmen. Ausdrucke, Anweisungen, Kontrollstrukturen Kapitel 3 KAPITEL 3. AUSDRUCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN 1 a = b statt a == b, so steht in der Bedingung der Zuweisungsausdruck a = b, der den Wert von b zuruckgibt und gleichzeitig (als Seiteneekt) a den Wert von b zuweist. Ist b ganzzahlig mit Wert 0, so wird dies als falsch interpretiert und der Wert von b auf cout geschrieben. Ist b ganzzahlig mit Wert 6= 0, so wird dies als wahr interpretiert if ( a == b ) f cout << a g else f cout << b g weist also a den Wert von b zu. Der Wert des Ausdrucks a = b, also b, wird unterdruckt. Seiteneekte des Zuweisungsausdrucks treten oft durch Verwechslung des Zuweisungssymbols = mit dem Gleichheitssymbol == auf. Schreibt man etwa versehentlich in der if-Anweisung a = b Dies ist ein Zuweisungsausdruck mit dem Zuweisungsoperator =. Dabei wird dem lvalue a (der einen Speicherplatz bezeichnet) der Wert des rvalue b zugewiesen. Es wird also ein Speicherinhalt verandert dies zahlt aber nicht als Seiteneekt, da es eine Zuweisung an einen lvalue ist. Der Wert des Ausdrucks ist der Wert, der der linken Seite zugeweisen wird. Der Typ ist der Typ der linken Seite. Die einfache Anweisung a = b Manchmal wird auch die Zuweisung an einen lvalue als Seiteneekt bezeichnet. 1. 3.1.3 Beispiele Eine wichtige Regel in der C-Programmierung ist: Ein Ausdruck wird zu einer (einfachen) Anweisung durch Anfugen eines Semikolons. In diesem Fall wird der Ruckgabewert unterdruckt, und Zuweisungen an lvalues und ggf. Seiteneekte sind die einzigen Eekte. Weitere einfache Anweisungen sind: Aufruf einer void-Funktion oder eine Denition, jeweils abgeschlossen durch ein Semikolon. Die leere Anweisung besteht nur aus einem Semikolon. 3.1.2 Ausdrucke und einfache Anweisungen Einen Wert oder Ruckgabewert , der sich durch vollstandige Auswertung des Ausdrucks ergibt. Einen Typ , namlich den Typ seines Werts. Bei Funktionsaufrufen ist dies der Typ des Ruckgabewertes (Ruckgabetyp ). Seiteneekte . Darunter versteht man einen Eekt auf Speicherinhalte, der sich nicht aus der Zuweisung eines Wertes an einen lvalue ergibt.1 Seiteneekte entstehen vor allem bei Funktionsaufrufen (Anderung von Parametern oder globalen Variablen), aber auch bei vielen Operatoren. Objekte , also alles, was Inhalt einer Speicheradresse ist (also insbesondere Variablennamen, wahrend rvalues allgemein den Wert eines Ausdrucks bezeichnet. Ein Ausdruck hat ganz allgemein 3 Eigenschaften: 26 b = a a b c = c + b 3 2 Seiteneekt! 2 c Solche Seiteneekte sind zwar moglich, aber sollten tunlichst vermieden werden, da sie zu unlesbaren Programmen fuhren. Die Zuweisungen b) nachher Zuweisungsoperator mit folgender Bedeutung: x += y ist aquivalent zu x In dem Ausdruck c += b = a ist c der lvalue und b = a der rvalue. Die Auswertung des rvalue weist b (als Seiteneekt!) den Wert von a zu. Der Wert des rvalues ist der Wert von a. Zu diesem wird der Wert von c addiert. (wegen des += Operators) und die Summe dem lvalue a zugewiesen. Ein Beispiel mit konkreten Werten: a) vorher a 1 b 5 c 2 += ist ein = x + y. c += b = a a = b++ a = ++b a++ ++a weist a den Wert von b zu weist a den Wert von b plus 1 zu weist a den Wert von a plus 1 zu, da a ein lvalue ist weist a den Wert von a plus 1 zu, da a ein lvalue ist bedeutet: erst benutzen, dann erhohen bedeutet: erst erhohen, dann benutzen 2 Manche Compiler geben allerdings Warnmeldungen aus. In C++ unterscheidet man zwischen Deklaration und Denition . Deklarationen fuhren Identier ein und assoziieren mit ihnen Typen, so da der Compiler aufgrund dieser Deklaration die Typvertraglichkeit uberprufen kann (type checking). 3.2 Denitionen und Deklarationen Beispiel: a++ ++a leisten dasselbe und sind klarer. 3. Der Inkrementoperator ++ (entsprechend --) Er kann als Prax (++a) oder Postx (a++) auf einen arithmetischen Ausdruck a angewendet werden. Ist a ein lvalue, so wird der Wert von a um 1 erhoht. Der Wert von a++ ist der Wert von a der Wert von ++a ist der Wert von a plus 1. Also: 2. 27 und der Wert von a auf cout geschrieben. Dieser ist wegen des Seiteneekts gleich dem Wert von b. In beiden Fallen wird also der Wert von b auf cout geschrieben. Auerdem wird der Wert von a per Seiteneekt verandert. Die Verwechslung von a = b und a == b hat also weitreichende, unerwunschte Folgen, deren Ursache oft schwer zu nden ist.2 3.2. DEFINITIONEN UND DEKLARATIONEN KAPITEL 3. AUSDRUCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN deniert die Integer Variable a. deniert die Gleitkomma Variable x und initialisiert sie zu 3.14. deklariert den Typ Boolean als int. deniert die Funktion f (x) = x2 . In den f...g Klammern steht der Funktionsrumpf. deklariert eine Funktion Square. Dem Compiler sind dadurch Name, Ruckgabetyp und Argumenttyp bekannt. Danach wird auf die entsprechende Realisierung der Kontrollstrukturen in C++ eingegangen. Struktogramme (auch Nassi-Shneiderman-owcharts genannt) und Flu diagramme sind graphische Beschreibungen von Algorithmen unter Verwendung der genannten Kontrollstrukturen. Pseudocode, Struktogramm, Fludiagramm. und daraus abgeleitete Anweisungen (z. B. die selektive Anweisung). Sie werden mit den nachfolgend beschriebenen Kontrollstrukturen gebildet, die in allen hoheren Programmiersprachen existieren. Fur diese Kontrollstrukturen werden wir neben einer umgangsprachlichen Beschreibung und einem Beispiel drei programmiersprachenunabhangige Darstellungen angeben, und zwar als: zusammengesetzte Anweisung (Verbundanweisung) bedingte Anweisung wiederholende Anweisung Neben den einfachen Anweisungen gibt es die strukturierten Anweisungen : 3.3 Strukturierte Anweisungen 1. Jeder Identier mu deklariert sein, bevor er benutzt werden kann. 2. Ein Identier kann mehrmals deklariert, aber nur einmal deniert werden. Weitere Beispiele werden in Kapitel 7.1.4 diskutiert. In C++ gelten folgende Grundregeln fur Identier: typedef int Boolean int Square(int x) freturn x*xg int Square(int) int a double x = 3.14 Denitionen sind Deklarationen, die zugleich (bei Variablen und Konstanten) den Identiern Speicherplatz zuordnen oder (bei Funktionen) den Rumpf der Funktion auuhren. Beispiele sind: 28 29 ! && || /*...*/ Bedeutung Zuweisung Test auf Gleichheit Test auf Ungleichheit logisches nicht logisches und logisches oder Kommentare 3 Dies ist eine gerichtlich umstrittene Praxis vieler Banken. An sich mu ten die Zahlungen bereits|wie bei Sparguthaben|fur Teile eines Jahres verzinst werden. Die entsprechende Rechnung bleibt Ihnen als Ubung uberlassen. z Dazu stellen wir folgende Uberlegung an. Der Betrag b wachst in n Jahren bei der angegebenen Verzinsung auf b (1 + z )n , falls keine Ruckzahlungen erfolgen. Die monatlichen Zahlungen von a mussen, wenn sie zum selben Zinssatz verzinst werden, nach n Jahren auf den selben Betrag von b (1 + z )n fuhren. Durch Gleichsetzung beider Betrage lat sich a berechnen. Sehen wir uns an, wie sich die monatlichen Zahlungen verzinsen. Im ersten Jahr wird 12a \angespart". Dieser Betrag wird allerdings erst ab dem zweiten Jahr verzinst.3 Er wachst also nach n Jahren auf 12a(1 + z )n;1 DM. Entsprechend ergeben die Zahlungen im zweiten Jahr am Ende 12a(1 + z )n;2 DM, usw. Insgesamt ist der Wert aller Zahlungen nach n Jahren auf 12a (1 + z )n;1 + 12a (1 + z )n;2 + : : : + 12a angewachsen. Die Gleichsetzung der Betrage ergibt dann: b (1 + z )n = 12a (1 + z )n;1 + 12a (1 + z )n;2 + : : : + 12a = 12a (1 + z )n;1 + : : : + (1 + z )1 + (1 + z )0 ] n = 12a (1(1++zz)) ;;11 n = 12a (1 + z ) ; 1 zur Ablosung einer Annuitatshypothek in Hohe von b DM bei einer Laufzeit von n Jahren und jahrlicher Verzinsung zum Zinssatz von z % anfallt. Beispiel 3.1 (Hypothekberechnung) Es ist der monatliche Betrag a zu berechnen, der Die zusammengesetzte Anweisung (compound statement ) besteht in der Hintereinanderschaltung (Verkettung ) von Anweisungen (einfachen oder strukturierten) M1 : : : Mt , die in der gegebenen Reihenfolge sequentiell abgearbeitet werden. 3.3.1 Zusammengesetzte Anweisung, Verkettung f: : : g not and or Pseudocode C++ := = = == != 6 = Pseudocode leistet das gleiche in einer an Pascal orientierten, mit normalem Text durchsetzten Beschreibung. Die folgende Tabelle stellt die Pseudocode Aquivalente bereits eingefuhrter C++ Konstrukte zusammen. 3.3. STRUKTURIERTE ANWEISUNGEN Lese b ein (in DM) Lese z ein (in %) Lese n ein (in Jahren) Berechne r := 1 + z Berechne R := rn Berechne a := (b=12)(R z )=(R ; 1) Gebe a aus M1 ;! M2 ;! : : : ;! Mt ;! In C++ werden zusammengesetzte Anweisungen durch geschweifte Klammern dargestellt: Abbildung 3.2: Fludiagramm der Verkettung. ;! Abbildung 3.1: Struktogramm der Verkettung. Mt .. . M1 M2 Das Struktogramm fur die Verkettung ist in Abbildung 3.1 angegeben, das Fludiagramm in Abbildung 3.2. end Mt .. . M1 M2 begin Die Verkettung erlaubt keine Verzweigung. Sie hat daher (ohne Verwendung weiterer Kontrollstrukturen) nur einen beschrankter Anwendungsbereich. Viele sogenannte \programmierbare" Taschenrechner der ersten Generation waren nur so programmierbar. Der Pseudocode fur die Verkettung lautet: M1 M2 M3 M4 M5 M6 M7 Algorithmus 3.1 (Hypothek Abtrag) Dies resultiert in folgenden Algorithmus zur Berechnung von a. n a = 12b (1(1++zz))n ; z1 KAPITEL 3. AUSDRUCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN Hieraus folgt 30 Mt .. . M1 M2 31 total amount monthly rate interest rate in % 1 + interest rate interestFactor^period number of years interestFactor = 1 + interestRate power = pow(interestFactor,period) ratePerMonth = (amount/12)*power*interestRate/(power-1) cout << "Ihre monatliche Rate betraegt DM " << ratePerMonth << "\nIhre Bank.\n" 4 Beim Compilieren mu zusatzlich die Option -lm angegeben werden (library, mathematics). Durch #include <math.h> wird eine Bibliothek mathematischer Funktionen zugeladen.4 Hieraus wird die Funktion pow fur die Potenzierung verwendet. pow(a,b) berechnet den Wert ab und gibt ihn als Ruckgabewert zuruck. } // // // // // // cout << "Geben sie den Gesamtbetrag des Darlehens in DM an: " cin >> amount cout << "Geben sie den Zinssatz in % an: " cin >> interestRate interestRate = interestRate / 100.0 // change to "per hundred" cout << "Geben sie die Laufzeit in Jahren an: " cin >> period #include <iostream.h> #include <math.h> void main() { double amount, ratePerMonth, interestRate, interestFactor, power int period hypothek.cc // hypothek.cc // // calculates monthly mortgage rate Programm 3.1 Ein Beispiel fur ein C++ Programm mit nur einer zusammengesetzten Anweisung ist Programm 2.1 (temperatur.cc). Hier folgt ein weiteres fur die Hypothekberechnung. g f 3.3. STRUKTURIERTE ANWEISUNGEN KAPITEL 3. AUSDRUCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN Auch hier darf der else-Teil fehlen. B ) B1 else B2 In C++ wird die if-Anweisung folgendermaen gebildet: if ( S1 XXX XXX B XX X true XXX Abbildung 3.3: Struktogramm der if-Anweisung. S2 B HH false H S1 true HH H Das Struktogramm der if-Anweisung ist in Abbildung 3.3 dargestellt, das Fludiagramm in Abbildung 3.4. if a > b then berechne a ; b else berechne b ; a zu berechnen. Eine Losung in Pseudocode lautet: Beispiel 3.2 (Betrag) Der Betrag ja ; bj der Dierenz a ; b zweier reeller Zahlen a b ist Dabei ist B ein Boolescher Ausdruck und sind S1 S2 beliebige Anweisungen (insb. wieder strukturierte Anweisungen). Der Prozessor berechnet den Wahrheitswert (true bzw. false ) von B und steuert in Abhangigkeit davon S1 bzw. S2 an. Der else-Teil darf fehlen. if B then S1 else S2 Die if-Anweisung Der Pseudocode fur die if-Anweisung lautet: Die Selektion ermoglicht das Ansteuern einer Alternative in Abhangigkeit von Daten. Der Prototyp der Selektion ist die if-Anweisung. 3.3.2 Selektion, Bedingte Anweisung 32 ? ? S2 ? ; HH HH B H H ? S1 ? HH ; HH B H H + ? 33 else if B = cn then Sn Das zugehorige Struktogramm ist in Abbildung 3.5 dargestellt, das Fludiagramm in Abbildung 3.6. In C++ existiert dafur die switch-Anweisung , siehe Ubung. Bei ineinandergeschachtelten if-Anweisungen kann es wegen fehlender else Teile zu Unklarheiten bei der Zuordnung der else zu den if kommen. Falls dies nicht durch Klammerung end cn : Sn .. . c1 : S1 c2 : S2 case B of Hierfur existiert in Pseudocode die folgende Kurzform: ... if B = c1 then S1 else if B = c2 then S2 else if : : : Sollen mehr als die zwei Falle true und false unterschieden werden, so ist dies mit der selektiven Anweisung oder Selektion moglich. Diese lat sich als \geschachtelte" Variante der if-Anweisung auassen. Sind etwa c1 : : : cn die Werte, die der Ausdruck B annehmen kann, und soll beim Wert ci die Anweisung Si ausgefuhrt werden, so lat sich die entsprechende Selektion wie folgt in Pseudocode realisieren: Die selektive Anweisung Abbildung 3.4: Fludiagramm der if-Anweisung. S1 ? + 3.3. STRUKTURIERTE ANWEISUNGEN 34 c1 S2 ::: aa aa aa aa c2 aaa ? S1 c2 ? r ? S2 ::: PP PP PP P ? cn Sn PP Abbildung 3.6: Fludiagramm der Selektion. c1 B Sn a cn aaaa aa a aa aa B Abbildung 3.5: Struktogramm der Selektion. aa a S1 aa a KAPITEL 3. AUSDRUCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN 35 S B)S Beispiel 3.3 (Groter gemeinsamer Teiler) Fur zwei positive naturliche Zahlen x y ist der gro te gemeinsame Teiler ggT (x y) zu berechnen. while ( In C++ wird die while-Anweisung wie folgt gebildet: Abbildung 3.7: Struktogramm der while-Anweisung. B Dabei ist B ein Boolescher Ausdruck und ist S eine beliebige Anweisung (insb. wieder eine strukturierte Anweisung). Der Prozessor berechnet den Wahrheitswert (true bzw. false ) von B vor jeder Ausfuhrung von S . Ist B = true so wird S ausgefuhrt, sonst nicht. Falls der Wahrheitswert von S immer true bleibt, so wird S stets wieder ausgefuhrt. Man gerat dann in eine sog. Endlosschleife . Sie stellt bei Anfangern einen haug gemachten Programmierfehler dar. Das Struktogramm der while-Anweisung ist in Abbildung 3.7 dargestellt, das Fludiagramm in Abbildung 3.8. Die while-Anweisung Der Pseudocode fur die while-Anweisung lautet: while B do S Die Wiederholung ermoglicht die wiederholte Durchfuhrung einer Anweisung (meist mit veranderten Werten von Variablen). Die Haugkeit der Wiederholung wird dabei durch eine Boolesche Bedingung kontrolliert. Der Prototyp der Selektion ist die while-Anweisung. 3.3.3 Wiederholung bezieht sich das else also auf das zweite if. if (Bedingung1 ) Anweisung1 if (Bedingung2 ) Anweisung2 else Anweisung3 mit f...g geregelt wird, bezieht sich ein \hangendes" else immer auf das letzte if, auf das eine Zuordnung moglich ist. Bei 3.3. STRUKTURIERTE ANWEISUNGEN + S ? Abbildung 3.8: Fludiagramm der while-Anweisung. ? H HH B H H ; ? H KAPITEL 3. AUSDRUCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN Algorithmus 3.2 (Groter gemeinsamer Teiler) Dies fuhrt zu folgendem Algorithmus zur Berechnung von ggT (x y) (in Pseudocode). Aus 1. und 2. folgt: a b und a ; b b haben dieselben Teiler, und damit auch denselben gro ten gemeinsamen Teiler. zu 1: Sei c ein Teiler von a und b. Dann existieren k1 k2 2 IN mit k1 c = a und k2 c = b. Hieraus folgt: a ; b = k1 c ; k2 c = (k1 ; k2 ) c Da a > b, ist k1 ; k2 > 0. Also ist a ; b ein Vielfaches von c und somit c ein Teiler von a ; b. Da c nach Annahme auch ein Teiler von b ist, ist c ein Teiler von a ; b und b. Also gilt 1. zu 2: Sei c ein Teiler von a ; b und b. Dann existieren l1 l2 2 IN mit l1 c = a ; b und l2 c = b. Hieraus folgt: a = (a ; b) + b = l1 c + l2 c = (l1 + l2 ) c Also ist c ein Teiler von a. Da c nach Annahme auch ein Teiler von b ist, ist c ein Teiler von a und b. Also gilt 2. 1. Ist c ein Teiler von a und b, so auch von a ; b und b. 2. Ist c ein Teiler von a ; b und b, so auch von a und b. Beweis: Zeige: ggT (a b) = ggT (a ; b b): Lemma 3.1 Sind a b 2 IN und ist a > b > 0, so ist Der grote gemeinsame Teiler von x und y ist die grote naturliche Zahl, die x und y teilt. So ist ggT (12 16) = 4 und ggT (12 17) = 1. Zur Berechnung des ggT nutzen wir folgende mathematische Aussage: 36 37 ! ! 16 12 ! ! 4!4 12! 8 ! ! 4 4 ggT.cc void main() { int x, y, // the input integers read from the terminal method: a > b ==> ggT(a,b) = ggT(a-b,b) input: integers x, y output: greatest common divisor ggT(x,y) of x and y ggT.cc #include <iostream.h> // // // // // // Programm 3.2 Nach dem 4. Schleifendurchlauf ist a = b = 4, und der Algorithmus terminiert mit ggT (28 12) = 4. Ein entsprechendes C++ Programm lautet: a: 28 b: 12 Ein Zahlenbeispiel: Fur x = 28 y = 12 ergeben sich folgende Werte fur a und b. Beweis: Lemma 3.1 garantiert, da am Ende der if-Anweisung bei jedem Durchlauf der while-Schleife die Bedingung ggT (a b) = ggT (x y) gilt. Da a oder b in jedem Durchlauf der while-Schleife um mindestens 1 kleiner wird und a und b positiv bleiben, kann die while-Schleife hochstens maxfx yg mal durchlaufen werden. Also terminiert die while-Schleife. Beim Austritt aus der while-Schleife gilt a = b, also ggT (a b) = a = b. Zusammen mit der oben gezeigten Gleichheit ggT (a b) = ggT (x y) folgt die Behauptung. Satz 3.1 Algorithmus 3.2 berechnet fur zwei beliebige positive naturliche Zahlen x y ihren gro ten gemeinsamen Teiler. Wir uberlegen uns zunachst, da dieser Algorithmus korrekt arbeitet. a := x b := y while a 6= b do if a > b then a := a ; b else b := b ; a fggT (a b) = ggT (x y )g fggT (x y ) = a = bg ggT (x y) := a 3.3. STRUKTURIERTE ANWEISUNGEN // auxiliary variables In C++ gibt es hierfur die do-while-Anweisung: Abbildung 3.9: Struktogramm der repeat-Anweisung. not B S Der Prozessor fuhrt erst S aus, dann wird B gepruft (post checking im Gegensatz zu pre checking bei der while-Anweisung). Falls B noch erfullt ist, so wird S erneut ausgefuhrt. Das Struktogramm der repeat-Anweisung ist in Abbildung 3.9 dargestellt, das Fludiagramm in Abbildung 3.10. repeat S until not B oder, aquivalent dazu, repeat S while B Da dies haug vorkommt, gibt es hierfur die repeat-Anweisung als eigene Kontrollstruktur. while B do S S ? B ) Abbildung 3.10: Fludiagramm der repeat-Anweisung HH H ; ? 39 In C++ gibt es dafur das for-statement als eigene Kontrollstruktur, siehe Ubung. Abbildung 3.11: Struktogramm der n-maligen Wiederholung. n times S Das Struktogramm hierfur ist in Abbildung 3.11 dargestellt. repeat S n times Kurzform: z := n while z > 0 do z := z ; 1 S Manchmal mu man eine feste Anzahl von Wiederholungen (Iterationen ) von S machen (z. B. n mal). Dies kann realisiert werden durch die Mitfuhrung einer Kontrollvariablen z , die nicht in S vorkommt. Diese wird ublicherweise als Zahler bezeichnet. Bei der while-Anweisung wird S nie ausgefuhrt, wenn B bereits zu Anfang den Wert false hat. Oft mochte man jedoch die Anweisung S mindestens einmal (unabhangig von B ) ausfuhren. Dies ist mit der while-Anweisung wie folgt moglich: S while ( ? S - +H B HH 3.3. STRUKTURIERTE ANWEISUNGEN do cout << "Geben Sie die Zahl x ein: " cin >> x cout << "Geben Sie die Zahl y ein: " cin >> y a = x b = y while (a != b) if ( a > b ) a = a - b else b = b - a // ggT(a,b) = ggT(x,y) // ggT(a,b) = a = b cout << "Der ggT von " << x << " und " << y << " ist " << a << ".\n" a, b KAPITEL 3. AUSDRUCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN Varianten der while-Anweisung } 38 KAPITEL 3. AUSDRUCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN weitere Anweisungen Im ersten Fall wird die while-Schleife ganz verlassen, im zweiten Fall werden die \weiteren Anweisungen " ubersprungen und erfolgt der nachste Durchlauf der while-Schleife. break kann auch in der if- bzw. switch-Anweisung auftreten, vgl. Ubung. Zum Abschlu geben wir in den Abbildungen 3.12 und 3.13 die verbesserte Version des Algorithmus zur Berechnung der nachst groeren Primzahl zu einer gegebenen Zahl (vgl. Kapitel 2.3) in Form von Struktogrammen an. g while ( Fortsetzungsbedingung )f ... if ( !Bedingung ) continue bzw. zu g while ( 1 ) f ... if ( !Fortsetzungsbedingung ) break ... Mit den eingefuhrten Kontrollstrukturen Verkettung, Selektion und Wiederholung kann alles berechnet werden, was im intuitiven Sinne berechenbar ist. Der Nachweis hiervon wird in der Theorie der Berechenbarkeit gefuhrt. Tatsachlich lat sich dies bereits mit weniger Kontrollstrukturen erreichen, z. B. reichen die Verkettung und die while-Schleife bereits aus. (Ubung: hierdurch ist die if-Anweisung simulierbar). Verkettung und Iteration (n-malige Wiederholung) reichen jedoch nicht aus. Man braucht die Moglichkeit, beliebige Boolesche Ausdrucke B als Abbruchbedingung zu wahlen. Die gleiche Machtigkeit erreicht man alternativ auch mit der Verkettung, der Selektion und der goto-Anweisung. Die goto-Anweisung ermoglicht es, zu beliebigen Stellen im Programm zu \springen". In alteren Programmiersprachen (Basic, Fortran) und in Assembler gehoren goto-Anweisungen zum Standard, in moderneren Sprachen sind sie verpont, da die Programme durch sie meist unstrukturiert werden und schwer zu verstehen und zu uberprufen sind. Das goto ist in C++ erlaubt, sollte aber mit groer Vorsicht benutzt werden. Nutzlich ist es nur in speziellen Situationen, z. B. um aus geschachtelten if- oder while-Anweisungen herauszuspringen. Hierfur gibt es in C++ die folgenden,\abgeschwachten" Varianten der goto-Anweisung. Zum Verlassen von Schleifen \in der Mitte" gibt es die Anweisungen break und continue. Die while Anweisung wird dann zu 3.3.4 Machtigkeit von Kontrollstrukturen 40 k := k + 2 false Abbildung 3.13: Struktogramm des Primzahl-Algorithmus (Feinversion). divisorFound := true divisorFound = false Gebe z aus true X XX XX divisorFound := false (divisorFound = false and (k k z ) XXX XXX XXX z modulo k = 0 z := n false ``` n gerade ``` ``` ` z := n ; 1 z := z + 2 k := 3 true ``` Lese Zahl n ein ` `` ` Abbildung 3.12: Struktogramm des Primzahl-Algorithmus (Grobversion). Lese Zahl n ein Durchlaufe alle ungeraden Zahlen z > n der Reihe nach k kein Teiler von z und k k z Teste alle ungeraden Zahlen k ab k = 3 bis k k z ob sie Teiler von z sind kein Teiler von z gefunden Gebe z aus 3.3. STRUKTURIERTE ANWEISUNGEN 41 KAPITEL 3. AUSDRUCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN Kontrollstrukturen werden in nahezu allen Buchern uber Programmiersprachen behandelt. Geschichtliche Hinweise zur Entstehung der Kontrollstrukturen und Vergleiche zwischen verschiedenen Programmierstilen und Sprachen nden sich in Hor84]. Fur C++ sowie auf Vergleiche mit Pascal und C verweisen wir auf HR94]. Die Behandlung von Kontrollstrukturen, Ausdrucken, Typechecking usw. durch Compiler ist ausfuhrlich in ASU86] dargestellt. Eine Einfuhrung in die Theorie der Formalen Sprachen ist in Wii87] enthalten. Tiefer geht HU79]. 3.4 Literaturhinweise 42 ::: ::: ::: ::: if, while, return, int, a z, A Z, 0 9 /, %, (, ), f, g, +, -, *, &&, ++, ==, ::: Option Alternative (Vereinigung) (begrenzte) Wiederholung Konkatenation (Produkt) 43 1 so benannt nach Backus und Naur, zwei Informatikern, die in den 60ger Jahren ma geblich an der Entwicklung von ALGOL 60, der ersten strukturierten Programmiersprache, mitwirkten. Zur Darstellung der genannten Konstrukte werden sogenannte Metazeichen verwendet: 4.1 Backus Naur Form - Beide Arten stellen Beschreibungsmittel fur folgende Konstrukte zur Verfugung Backus Naur Form (BNF) bzw. erweiterte Backus-Naur-Form (EBNF)1 Syntaxdiagramme Zur Darstellung der Syntaxregeln von Programmiersprachen gibt es standardisierte Beschreibungsarten: Schlusselworter : Zeichen : Sonderzeichen : Unter der Syntax einer Sprache versteht man die Menge der grammatischen Regeln, nach denen aus den Symbolen (auch Terminalsymbole genannt) einer Sprache korrekte Ausdrucke gebildet werden. In C++ sind die Symbole: Syntax und Semantik von Programmiersprachen Kapitel 4 Aneinanderreihung : : :] :::j:::j:::j::: f: : :g <integer constant> <Vorzeichen> <Zahlenwert> ::= <Vorzeichen>] <Zahlenwert> <Typsux>] ::= +j::= <Null> j <Dezimalwert> j <Oktalwert> j <Hexadezimalwert> Beispiel 4.3 (Ganze Zahl (integer constant) in C++) Wir verzichten auf die weitere Erklarung der syntaktischen Variablen <Ausdruck der einen int-Wert liefert> <einfache oder zusammengesetzte Anweisung >. Sie ergibt sich aus Kapitel ??. Das folgende Beispiel ist ausfuhrlicher und erklart alle syntaktischen Variablen bis hinunter zu Terminalsymbolen. <if statement> ::= if (<Bedingung>) <statement> else <statement> <Bedingung> ::= <Ausdruck der einen int-Wert liefert> <statement> ::= <einfache oder zusammengesetzte Anweisung> Beispiel 4.2 (if-statement in C++) - Gro- und Kleinschreibweise werden unterschieden - Schlusselworte konnen keine Identier sein. Nach diesen Regeln korrekt geformte Identier sind: a, a1, a very long identifier, , . Nicht korrekt sind: 1a, x-11, a*. Neben der Syntax , die die Regeln zur korrekten Bildung festlegt, gibt es die Semantik . Sie legt die Bedeutung der korrekt gebildeten Ausdrucke fest. Dies geschieht meist durch zusatzliche Regeln, die nichts mit der Syntax zu tun haben. Fur Identier sind dies folgende Regeln: <underscore> ::= <sign> ::= <rst sign> j <digit> <digit> ::= 0j1j2j3j4j5j6j7j8j9 ajbjcjdjejfjgjhjijjjkjljmjnjojpjqjrjsjtjujvjwjxjyjz <identier> ::= <rst sign>f<sign>g <rst sign> ::= <letter> j <underscore> <letter> ::= AjBjCjDjEjFjGjHjIjJjKjLjMjNjOjPjQjRjSjTjUjVjWjXjYjZj Beispiel 4.1 (Identier in C++) Zusatzlich zu den Terminalsymbolen verwendet man Begrie, die noch nicht erklart sind. Diese heien Nonterminalsymbole oder syntaktische Variable . Sie werden in < : : : > geschrieben und durch ::= deniert. durch durch durch durch KAPITEL 4. SYNTAX UND SEMANTIK VON PROGRAMMIERSPRACHEN Option Alternativen Wiederholung Konkatenation 44 ::= <unsigned int> j <long int> j <unsigned long int> ::= 0f0g ::= <erste Dezimalzier> f<Dezimalzier>g ::= 1j2j3j4j5j6j7j8j9 ::= 0 j <erste Dezimalzier> ::= <Oktalprax><Oktalzier> f<Oktalzier>g ::= 0 ::= 0j1j2j3j4j5j6j7 ::= <Hexadezimalprax><Hexadezimalzier> f<Hexadezimalzier>g ::= 0xj0X ::= 0j1j2j3j4j5j6j7j8j9jajAjbjBjcjCjdjDjejEjfjF ::= ujU ::= ljL ::= uljlujuLj LujUljlUjULjLU 45 unsigned int, unsigned long int long int, unsigned long int unsigned long int. 2 Wieviel Speicherplatz dies ist, bekommt man durch die C++ Funktion sizeof(Typ) heraus. Auf einer SUN Sparc ist sizeof(int) = sizeof(long int) = 4, also 4 Byte = 32 Bit. Mit unsigned long int konnen also im Prinzip die Zahlen 0 1 : : : 232 ; 1 dargestellt werden (232 ; 1 = 4294967295), mit long int die Zahlen 2;32 2;31 + 1 : : : 0 : : : 2;31 ; 1. Genauer wird die Darstellung ganzer Zahlen in Kapitel ?? erlautert. { bei u : { bei l : { bei ul : positiv. Prax 0 bedeutet Oktaldarstellung, d. h. zur Basis 8 mit den Ziern 0,1,: : :,7. Die Zahl 045 hat also den Dezimalwert 5 80 + 4 81 = 37. Prax 0x oder 0X bedeutet Hexadezimaldarstellung, d. h. zur Basis 16 mit den Ziern 0,: : :,9,A,B,C,D,E,F, die den Zahlenwerten 0 1 : : : 15 entsprechen. Dabei konnen statt A: : :F auch die Kleinbuchstaben a: : :f genommen werden. Der Zahl -0x2fA entspricht also die Dezimalzahl ;(A 160 + F 161 + 2 162 ) = ;(10 160 + 15 161 + 2 162 ) = ;762. Der Typsux bezieht sich auf die C++ Typen int, unsigned int, long int, unsigned long int zur Darstellung ganzer Zahlen. long int erlaubt i. a. die Darstellung groerer Zahlen als int, da mehr Speicherplatz fur die Darstellung einer Zahl bereit gestellt wird. Das fuhrende Bit wird dabei fur die Unterscheidung positiv/negativ verwendet. Bei den unsigned Varianten kann das fuhrende Bit mit fur den Zahlenwert verwendet werden.2 Bei vorgegebenen Sux wird versucht, die gegebene Zahl als Zahl in dem zugehorigen Typ darzustellen geht dies nicht, so wird der nachst groere Typ, in dem sie dargestellt werden kann, gema folgenden Listen genommen. < V orzeichen > gibt das Vorzeichen an. Es kann fehlen. Dann ist die dargestellte Zahl Die Bedeutung der nach diesen syntaktischen Regeln geformten integer-constants ergibt sich aus den folgenden semantischen Zusatzregeln. <Hexadezimalprax> <Hexadezimalzier> <unsigned int> <long int> <unsigned long int> <Null> <Dezimalwert> <erste Dezimalzier> <Dezimalzier> <Oktalwert> <Oktalprax> <Oktalzier> <Hexadezimalwert> <Typsux> 4.1. BACKUS NAUR FORM Dies ist jedoch nur bei undenierten Konstanten sinnvoll, da sonst der Typ durch die Denition festgelegt wird. KAPITEL 4. SYNTAX UND SEMANTIK VON PROGRAMMIERSPRACHEN (mindestens ` mal, hochstens k mal) (mindestens ` mal, sonst beliebig oft) ::= f< Vorzeichen >g10 < Zahlenwert > f< Typsux >g10 < Oktalprax > f< Oktalzier >g0 (()()) (() {{}{}{}}} korrekt nicht korrekt nicht korrekt Die BNF ist nur eine von vielen Moglichkeiten zur Darstellung von Syntaxregeln. Eine graphisch orientierte Darstellungsform sind die sogenannten Syntaxgraphen oder Syntaxdiagamme . Die verwendeten Metasymbole sind in Abbildung 4.1 dargestellt. Dabei sind A B C Nicht-Terminalsymbole und a b c Terminalsymbole. Diese \kompakte" Notation erlaubt es, die Syntax der Programmiersprache C++ auf wenigen Seiten darzustellen, vgl. HR94]. Als Beispiel sind hier der Syntaxgraph der if-Anweisung (Abbildung 4.2) und Teile des Syntaxgraphen fur die integer constant (Abbildung 4.3) aufgefuhrt. Als ein weiteres Beispiel betrachten wir korrekte Klammerausdrucke . Sie sind wichtig fur die korrekte Darstellung von arithmetischen und logischen Ausdrucken (,) Klammern], und zur Bildung korrekter C++ Programme {,} Klammern]. Der Compiler mu in der Lage sein, Klammerausdrucke auf Korrektheit zu uberprufen und \korrespondierende" Klammerpaare zu nden, etwa: 4.3 Syntaxgraphen < Oktalwert > < integer constant > ::= Dann kann : : :] durch f: : :g10 ausgedruckt werden. So lassen sich etwa in Beispiel 4.3 einige Syntaxregeln einfacher formulieren: f: : :gk` f: : :g` Die erweiterte Backus Naur Form (extended Backus Naur Form oder EBNF sieht einige vereinfachende Schreibweisen vor: 4.2 Erweiterte Backus Naur Form Beispiele fur korrekt geformte integer-constants sind: -13, +0, -000, +0x0fu. Beispiele fur nicht korrekt geformte integer-constants sind: 039, f17, 0-3. Die ersten beiden sind sinnlos, das letzte Beispiel ist ein Ausdruck der den Wert -3 liefert. 46 A A A Option Alternative B A Wiederholung - - - Konkatenation - a Terminalsymbole - B - 6 ? 6 - - - - - - - - else - ) - Abbildung 4.2: Syntaxgraph der if-Anweisung. - ( Bedingung Anweisung Anweisung - if 6 - - A Nichtterminalsymbole - Abbildung 4.1: Metasymbole von Syntaxgraphen. 4.3. SYNTAXGRAPHEN - 47 48 - - 9 - 8 - 7 - 6 - 5 - 4 - 3 - 2 6 6 Zahlenwert - 9 - 8 - 7 - 6 - 5 - 4 - 3 - 2 - 1 - 0 ? ? - - Typsux - Abbildung 4.3: Syntaxgraph der integer constant (Auszuge). - 1 Dezimalwert: - Vorzeichen integer constant: KAPITEL 4. SYNTAX UND SEMANTIK VON PROGRAMMIERSPRACHEN 49 Nach Satz 4.1 ist ein korrekter Klammerausdrucke entweder der einfachste Ausdruck (), oder er lat sich durch zwei Regeln aus einfacheren korrekten Ausdrucken aufbauen. Daraus ergibt sich das in Abbildung 4.4 dargestellte Syntaxdiagramm zur Bildung korrekter Klammerausdrucke. Man beachte, da das Syntaxdiagramm rekursiv deniert ist, d. h. die syntaktische Variable \korrekter Klammerausdruck" kommt auf der rechten Seite ihrer Denition wieder vor (und entspricht dann kleineren korrekten Ausdrucken wie im Beweis von Satz 4.1. Als letztes Beispiel fur Syntaxdiagramme behandeln wir die Notation beim Schach (ausfuhrliche Notation). Beispiele fur Zuge in dieser Notation sind: ersten Klammer auf ( eine entsprechende Klammer zu ). Ist dies die letzte Klammer in A, so ist A von der Form (B ) wobei B wieder ein korrekter Klammerausdruck ist (da sonst A nicht korrekt ware). Ist die \Klammer zu" nicht die letzte Klammer in A, so sei B der Teilausdruck vom Anfang von A bis einschlielich dieser Klammer, und C der Rest. Dann sind B und C korrekte Klammerausdrucke (sonst ware A nicht korrekt). Die Fallunterscheidung zeigt also: A = (B ) oder A = BC , wobei B bzw. B C kurzere korrekte Klammerausdrucke als A sind. Also trit eine der beiden Regeln zu. Beweis: Sei A ein korrekter Klammerausdruck mit mehr als 2 Klammern. Dann gehort zur Moglichkeiten aus kleineren konstruiert werden. Satz 4.1 Jeder korrekte Klammerausdruck mit mehr als 2 Klammern kann mit einer dieser { Der einfachste korrekte Klammerausdruck ist A = (). { Man kann korrekte Klammerausdrucke hintereinander schreiben und erhalt einen neuen korrekten Klammerausdruck. A B korrekt ) AB korrekt Beispiel: A = () B = (()) ) AB = ()(()) { Man kann einen korrekten Klammerausdruck A wieder klammern und erhalt einen neuen korrekten Klammerausdruck. A korrekt ) (A) korrekt Beispiel: A = ()() ) (A) = (()()) Zur Erstellung eines Syntaxdiagramms uberlegt man sich, wie man korrekte Klammerausdrucke aus einfacheren (d. h. mit weniger Klammern) zusammensetzen kann. Es bezeichnen A B im weiteren immer korrekte Klammerausdrucke. Dann gilt bzgl. der Zusammensetzung von Klammerausdrucken: 4.3. SYNTAXGRAPHEN - - - ) 6 korrekter Klammerausdruck korrekter Klammerausdruck - Sg1{f3 e2{e4 Ta1xa4 Ta1{a8+ Th1{h7++ 0{0 0{0{0 a7{a8D e4xd3 e.p. Bewegung eines Springers Bewegung eines Bauern Schlagen (x) Schach (+) Matt (++) kurze Rochade lange Rochade Umwandlung in eine Dame \en passent" schlagen (nur bei Bauern) Abbildung 4.4: Syntaxgraph fur korrekte Klammerausdrucke. - ( korrekter Klammerausdruck 6 KAPITEL 4. SYNTAX UND SEMANTIK VON PROGRAMMIERSPRACHEN Man stellt in den vorangegangenen Beispielen deutlich einen Unterschied fest zwischen der Syntax (also den Regeln, die festlegen, wie die Ausdrucke gebildet werden) und der Semantik (also der Bedeutung der Ausdrucke). So ist der Schachzug Ta1{b2 syntaktisch (nach den gegebenen Regeln korrekt) aber semantisch sinnlos, da Turme nicht so ziehen durfen. Man konnte diese Zugregeln fur Figuren noch durch ausgefeiltere Syntaxregeln formulieren, wie z. B. in Abbildung 4.7. Dann wird zwar Ta1-b2 als syntaktisch falsch erkannt, aber von dem syntaktisch korrektem Zug Ta1-a4 kann erst aus der Spielsituation heraus (d. h. \zur Laufzeit") entschieden werden, ob er semantisch korrekt ist. (Es konnte ja auf a2 noch eine eigene Figur stehen.) Diese ieende Grenze zwischen Syntax und Semantik tritt in allen formalen Systemen, insbesondere auch in Programmiersprachen auf. In Programmiersprachen wird die syntaktische Korrektheit des Programmtextes bei der Programmubersetzung in Maschienensprache vom Interpreter bzw. Compiler getestet. Das setzt voraus, da die Syntaxanalyse ohne Bezug auf die Semantik der Ausdrucke moglich ist. Ist dies moglich, so spricht man (in der Theorie der formalen Sprachen) von kontextfreien Sprachen. Programmiersprachen sind nur in Teilen kontextfrei (z. B. korrekte Klammerausdrucke oder integer-constant), in anderen aber nicht (z. B. Korrespondenz zwischen formalen und aktuellen Parametern bei Funktionen, vgl. Kapitel 7.1). Syntaktisch korrekte Ausdrucke konnen semantisch sinnlos sein, wie das Beispiel der Turmzuge zeigte. Dies gilt auch fur die Umgangssprache. So sind die Anweisungen 4.4 Syntax versus Semantik Das Syntaxdiagramm eines Zuges ist in den Abbildungen 4.5 und 4.6 dargestellt. 50 Figur - - - - - - - - - - - - - 0{0 0{0{0 - - - - - Feld: - - Linie 8 1 - - 6 - - - Reihe - Umwandlungsgur - 6 + + - - 6 e.p. 3 6 Abbildung 4.5: Syntaxgraph fur Zuge im Schach (Teil 1). Rochade: - - \en passant" Zug: - - Umwandlungszug - - Feld \en passant" Zug - - - Rochade Feld { - 6 - 5 Linie Linie 4 Linie Umwandlungszug: { 7 Linie Linie { 2 Linie - Zug: 4.4. SYNTAX VERSUS SEMANTIK 51 52 Linie: Figur: K - - Reihe: - - - - - - - - - - - - - - D T L S 1 2 3 4 5 6 7 8 Umwandlungsgur: Abbildung 4.6: Syntaxgraph fur Zuge im Schach (Teil 2). - - - - - - - - - - - - - D T L S a b c d e f g h KAPITEL 4. SYNTAX UND SEMANTIK VON PROGRAMMIERSPRACHEN Linie - - - - - - - - - - - - - - - - - - 6 - - - - 6 { qqq a Reihe { b { h { 1 1 Linie qq q { Linie 8 8 - 53 Neben den Syntax- und Semantikfehlern gibt es die Logikfehler . Das Programm beschreibt den gewunschten Ablauf nicht, ist aber syntaktisch und semantisch korrekt. Ein Beispiel: 1. die Symbole verstehen, die der Algorithmusschritt enthalt (dies ist die Stufe zum Finden der Syntaxfehler), 2. jedem Schritt eine Bedeutung zuordnen in Form von auszufuhrenden Operationen (dies ist die Stufe zum Finden mancher Semantikfehler), 3. die Operationen ausfuhren (manche Semantikfehler konnen erst hier festgestellt werden). Schreibe den Namen des 1. Monats im Jahr. Schreibe den Namen des 13. Monats im Jahr. beide syntaktisch korrekt, die zweite ist jedoch semantisch sinnlos. Semantikfehler konnen durch einen Prozessor nur aufgedeckt werden, falls er genugend uber die Objekte, auf die der Algorithmus Bezug nimmt, wei. Auch dann sind sie meist schwer aundbar, wenn sie in versteckter Form vorkommen, wie etwa in den folgenden Anweisungen: Wahle n aus f1 : : : 13g. Schreibe den Namen des n-ten Monats im Jahr. Die hier auftretende Unstimmigkeit ist ein Ergebnis der Algorithmusausfuhrung, daher schwer aus dem Text der Anweisung (dem \Programmtext") zu ermitteln. Zur Ausfuhrung von Anweisungen in einem Programm mu ein Prozessor folgendes konnen: Abbildung 4.7: Ein detaillierterer Syntaxgraph fur Turmzuge. - - - - - - - a Reihe T Reihe b qq q h Reihe 4.4. SYNTAX VERSUS SEMANTIK KAPITEL 4. SYNTAX UND SEMANTIK VON PROGRAMMIERSPRACHEN Eine leicht verstandliche Einfuhrung in formale Sprachen gibt Wii87]. Zur Vertiefung sei auf HU79] verwiesen. HR94] enthalt eine vollstandige Beschreibung der Syntax von C++ mittels Syntaxdiagrammen. ASU86] geht ausfuhrlich auf die Verwendung von Syntaxregeln in Compilern ein. 4.5 Literaturhinweise { Ist 1=0 syntaktisch oder semantisch (oder nur pragmatisch) unzulassig? { Ist a := 0 b := 1=a zulassig? { Ist a + b syntaktisch zulassig, wenn a als double Variable und b als int Variable deniert ist? Berechne den Umfang durch Multipikation des Radius mit . ist syntaktisch und semantisch korrekt, aber falsch. Bei Programmiersprachen bilden die Syntaxdiagramme die Basis fur die Syntaxanalyse durch den Compiler. Auch hier sind die Grenzen zwischen Syntax und Semantik ieend: 54 55 der ganzen Zahlen, wobei Nmin < 0, Nmax > 0 maschinenabhangig sind. Operationen: = Zuweisung + Addition Subtraktion fNmin Nmin + 1 Nmin + 2 : : : 0 1 2 : : : Nmax g Beispiel 5.1 (Der C++ Typ int) Wertebereich: Eine endliche Teilmenge Jede Programmiersprache verfugt uber eingebaute (Standard) Datentypen. Andere mussen als sogenannte abstrakte oder selbstdenierte Datentypen mit den Ausdrucksmitteln der Programmiersprache deniert werden. einer Menge von Operationen auf diesen Werten dem Wertebereich (domain) des Typs Die Syntax einer algorithmischen Sprache beschreibt die formalen Regeln, mit denen ein Algorithmus formuliert werden kann. Sie erklart jedoch nicht die Bedeutung der Daten und Operationen, die in einem in einer bestimmten algorithmischen Sprache geschriebenen Algorithmus zulassig sind. Dies ist ein Problem der Semantik. Fur Daten eines vorgegebenen Typs ergibt sich die Semantik aus den moglichen Werten und den zugelassenen Operationen auf diesen Werten. Beide zusammen bilden einen Typ oder Datentyp. Denition: Ein Datentyp (kurz Typ ) besteht aus 5.1 Datentypen und Operationen Objekte, Typen, Datenstrukturen: Einfuhrung und Beispiele Kapitel 5 Multiplikation Ganzzahlige Division Test auf Gleichheit Test auf Ungleichheit und viele andere mehr (vgl. Ubung). * / == != KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN Schreibe den Bruch r in der Form \p=q" Berechne den entsprechenden float Wert Reduziere r auf die Form p=q, so da p und q teilerfremd sind Erzeuge den Bruch r aus gegebenen int Zahlen a,b Gebe den Zahler (enumerator) zuruck Gebe den Nenner (denominator) zuruck Multipliziere zwei Bruche Teste zwei Bruche auf Gleichheit (2=4 = 1=2) int a b und = sind Operatoren , f deniert 2 ganzzahlige Variable a bg bedeutet also die Initialisierung der Variablen r von Typ FracType gema r := 3=4. die in der Klasse FracType deniert werden und eine andere als die ubliche Bedeutung haben. Man spricht in diesem Zusammenhang von operator overloading . In Pseudocode sind also folgende Anweisungen denkbar: FracType r(3 4) usw. zugegrien. Diese Anweisung bewirkt das Schreiben des Bruches r in der Form a=b. Die Initialisierung erfolgt mit der Typdenition. r.Write() nennt man Instantiierung und r s Instanzen der Klasse. Auf die zugehorigen ElementFunktionen wird mit FracType r s Der Typ FracType ist in C++ nicht vorhanden, kann aber (uber Klassen , vgl. Kapitel ??) implementiert werden. Wir werden jedoch bereits in unserem Pseudocode die Schreibweise (und Ausdrucksweise) der C++ Klassen ubernehmen. Write(),: : :, FracType() sind C++ Funktionen der Klasse FracType, die Element-Funktionen der Klasse genannt werden. Die Denition von Variablen vom Typ FracType gema = Write() FloatEquiv() Simplify() FracType(a,b) Enum() Denom() Beispiel 5.2 (Ein abstrakter Typ FracType) Wertebereich: Alle Bruche der Form r = p=q wobei p und q int Werte sind und q > 0 ist. Operationen: 56 f f f f f f f f r = 3=4 g s = 4=8 g a=3g s = 1=2 g b=2g s = 3=2 g r = (3=4) (3=2) = 9=8 g r = 9=8 g 57 Gibt die Farbe (Kreuz, Pik, Herz oder Karo) einer Karte an Gibt den Wert (7,: : :,10, Bube, Dame, Konig oder As) einer Karte an Zieht eine zufallige Karte Erzeugt eine Karte mit Farbe f und Wert w den Typ des Wertes eines Ausdrucks bereits (weitgehend) ermitteln kann, ohne den Rechenproze durchfuhren zu mussen (zum Beispiel ist die Multiplikation einer int Zahl mit einer double Zahl vom Typ double, siehe unten.). die Typinformation zur Uberprufung der Zulassigkeit von Programmstatements benutzen kann (syntaktisch und partiell auch semantisch), beim Compilieren allen denierten Objekten den erforderlichen Speicherplatz zuweisen kann, der Variablen w den Wert einer zufalligen Skatkarte zu. Auch dieser Typ ist in C++ nicht vorhanden, wir werden aber spater sehen, wie man ihn mit Arrays einfach implementieren kann. Wichtig ist die Unterscheidung zwischen (abstrakten) Datentypen und Implementationen (z. B. in C++) solcher Datentypen. Algorithmenentwicklung basiert nur auf abstrakten Datentypen. Die Umsetzung abstrakter Datentypen in eine Implementation erfolgt entweder erst danach, oder ist unnotig, da bereits Implementationen existieren, die man verwenden kann (Wiederverwendbarkeit wird gerade von C++ besonders unterstutzt). Datentypen sind extrem wichtig fur die Compilierung, da der Compiler dann Skatkarte karte w := karte .Wert() Dann weist die Pseudocode Sequenz Farbe() Wert() Skatkarte() Skatkarte(f,w) Funktionen: Spielkarten im Skat Spiel darstellen. Beispiel 5.3 (Skatkarte) Wertebereich:=fKaro 7 : : : Karo As : : : Kreuz 7 : : : Kreuz Asg, d. h. 32 Werte, die die FracType r(3 4) FracType s(4 8) a := r.Enum() s:Simplify() b := s.Denom() FracType s(a b) r := r s if r = s then r := r r 5.1. DATENTYPEN UND OPERATIONEN KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN (Prax Notation) (Funktionsschreibweise) intVar = (int)floatVar intVar = int(floatVar) char. vgl. Beispiel 4.3. 1 Die Einfuhrung ist zur Zeit in der Diskussion und in manchen Compilern auch schon erfolgt, u. a. im GNU gcc Compiler ab 1995. Die genaue Behandlung dieser Datentypen erfolgt in der Ubung. Es fehlt ein Datentyp Boolean fur Wahrheitswerte. Wahrheitswerte werden in C++ als Teilmenge von int angesehen wird, wobei 0 dem Wahrheitswert false, und jeder von 0 verschiedene Wert dem Wahrheitswert true entspricht.1 Da ein Typ Boolean auch C++ Programme lesbarer macht, lat sich dies im Prinzip erreichen durch die Denition Zeichen: float, double, long double. int, long int, unsigned int, unsigned long int, Gleitkommazahlen: ganze Zahlen: geschrieben. C++ stellt elementare Datentypen mit zugehorigen Operationen bereit fur TypName identier eine Zuweisung mit expliziter Typumwandlung. Ein Beispiel: Da / bei ganzen Zahlen die ganzzahlige Division bezeichnet, hat 5/8 den ganzzahligen Wert 0 und 5.0/8 den gebrochenen Wert 0.625. Statt 5.0/8 kann man auch float(5)/float(8) schreiben, was vor allem fur Ausdrucke wie float(x)/float(y) wichtig ist in denen x,y ganzzahlige Werte annahmen, man aber die reelle Division meint. In Pseudocode werden Typvereinbarungen in der Form bzw. Dies setzt voraus, da der Datentyp jedes Identiers im Programm deklariert bzw. deniert wird und damit \zur Compilierzeit" bekannt ist. Diese Eigenschaft kennzeichnet statisch getypte Sprachen wie C++ und Pascal. Bei der Typuberprufung (type checking) unterscheidet man zwischen strikter Typuberprufung wie in Pascal und nicht strikter Typuberprufung wie in C++. Bei strikter Typuberprufung ist das \Mischen" von Typen stark eingeschrankt. So erlaubt Pascal keine Zuweisung eines real Wertes an eine integer Variable. In C++ ist dies jedoch durch implizite Typumwandlungen moglich. Allerdings verliert man hierdurch i. a. Information, da der zur Verfugung stehende Speicherplatz fur eine int Variable kleiner ist als fur eine double oder float Variable. Explizite Typumwandlungen erfolgen in C++ durch das sogenannte casting oder type casting . Ist intVar eine int Variable und floatVar eine float Variable, so bedeutet: 58 59 false bool 0 2 Die zur Zeit diskutierte Losung zur Erweiterung von C++ sieht einen Typ bool mit Werten true und vor. Dabei sind implizite Typkonversionen von bool zu int (true zu 1 und false zu 0) und von int zu ( zu false und 6= 0 zu true) vorgesehen. Programmiersprachen haben i. a. nur wenige Datenstrukturen als eingebaute Typen (alle haben z. B. Arrays, C++ daruberhinaus struct und union). Die meisten mu der Programmierer selber implementieren, wobei ihm C++ machtige Konstruktionsmoglichkeiten (vor allem Klassen und Vererbung ) bereitstellt. Wir behandeln zunachst die wichtigsten Datenstrukturen aus abstrakter Sicht . Die Implementation in C++ wird erst jeweils dann erfolgen, wenn die notigen Konstruktionsmoglichkeiten besprochen sind. Abbildung 5.1 gibt eine hierarchische Ubersicht uber einige der wichtigsten Datenstrukturen. Komponenten-Daten , die atomar oder wieder strukturiert sein konnen Regeln , die das Zusammenwirken der Komponenten zur gesamten Struktur denieren. Die bisherigen Beispiele waren Beispiele fur einfache (unstrukturierten oder atomaren ) Datentypen. Neben einfachen Datentypen gibt es sogenannte zusammengesetzte oder strukturierte Datentypen . Sie setzen sich aus bereits eingefuhrten Datentypen gema bestimmter Strukturierungsmerkmale zusammen. Stehen die Strukturierungsmerkmale im Vordergrund (und nicht der Typ der \Grunddaten"), so redet man von Datenstrukturen . Strukturierte Typen oder Datenstrukturen haben (neben Wertebereich und Operationen ) 5.2 Strukturierte Datentypen (Datenstrukturen) Gema der Praprozessor Direktiven werden die Denitionen Boolean, TRUE und FALSE also nur einmal (bei mehreren #include Anweisungen in unterschiedlichen Dateien) vorgenommen.2 #ifndef BOOL_H #define BOOL_H typedef int Boolean const int TRUE = 1 const int FALSE = 0 #endif Hierdurch ist der Wertebereich Boolean deniert, und man kann die Konstanten TRUE und FALSE als Wahrheitswerte verwenden, z. B. als Wertebereich fur Funktionen, die Wahrheitswerte liefern (Boolesche Funktionen). Zur besseren Benutzung sollten diese Werte uber eine include Datei bool.h bereitgestellt werden und bei Bedarf mittels der Praprozessor Direktive #include "bool.h" zugeladen werden. Die Datei bool.h sieht wie folgt aus typedef int Boolean const int TRUE = 1 const int FALSE = 0 5.2. STRUKTURIERTE DATENTYPEN (DATENSTRUKTUREN) hhhh @ Heterogene Komponenten @ @ ; ; ; @ @ @ Set Nichtlinear Last-In First-out First-In First-Out ``` HH ` ``` HH ``` `` H Sequentieller Zugri h hhhh Allgemein hhhh XX XXX X X XXX Abbildung 5.1: Klassikation einiger wichtiger Datenstrukturen. Array Record Liste Stack Queue Homogene Komponenten ; ; ; Direkter Zugri Linearh Datenstrukturen KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN { Musikstucke auf einem Tonband. { Ein Stapel von Buchern um das i-te zu nehmen, mussen erst die i ; 1 obersten entfernt werden. Sequentieller Zugri bedeutet, da man auf die i-te Komponente nur zugreifen kann, nachdem man vorher auf die Komponenten 1 2 : : : i ; 1 zugegrien hat. Beispiele sind: { CDs mit direkter Ansteuerung von Musikstucken. { Ein Regalbrett mit Buchern auf jedes Buch kann direkt zugegrien werden. Direkter Zugri (auch random access genannt) bedeutet, da man auf jede beliebige Komponente zugreifen kann, ohne vorher auf andere Komponenten zugreifen zu mussen. Beispiele sind: Jede Komponente (auer der letzten) hat einen eindeutigen Nachfolger. Jede Komponente (auer der ersten) hat einen eindeutigen Vorganger. Es gibt eine eindeutige letzte Komponente. Es gibt eine eindeutige erste Komponente. Eine lineare Datenstruktur hat (bei mindestens 2 Komponenten) eine Ordnung auf den Komponenten mit folgenden Eigenschaften: 60 61 const int n = 10 int x"n], y"n] for (int i = 0 i < n i++) x"i] = i*i Zuweisung und Test auf Gleichheit existieren nicht in C++, sondern mussen uber for Schleifen realisiert werden. So initialisiert x"i] = v wird der Variablen a der Wert der 4-ten Komponente von x zugewiesen. Bei lvalues reprasentiert "i] auch die Operation Store(i,v). Sie hat in C++ also die Form a = x"3] wird eine Array Variable x mit 10 Komponenten mit Komponententyp int deniert. Die Komponenten haben die Indizes 0 1 : : : 9. In C++ sind 0,1,...,n-1 die einzig moglichen Indizes eines Arrays mit n Komponenten. Der Operation V alue(i) entspricht in C++ der Selektor "i]. Durch int x"10] In C++ existiert bereits ein eingebauter Array Typ als sogenannter abgeleiteter Typ mittels des Operators " ]. Durch die Anweisung V alue(a i) Ermittelt den Wert der Komponente eines Arrays a mit Index i, also den Wert der (i + Ist a = (a0 : : : ak;1 ), so liefert Value(a i) den Wert ai Store(i v) Weist der Komponente von a mit Index i den Wert v zu. Danach ist ai = v. a := b Zuweisung von Arrays. Danach gilt ai = bi i = 0 : : : k ; 1 a=b Test auf Gleichheit. Liefert den Wert true genau dann, wenn ai = bi fur i = 0 : : : k ; 1. Ist k die Anzahl der Komponenten und A der Wertebereich des Grundtyps, so ist der Wertebereich X des Arrays das kartesische Produkt A : : : A (k-fach). Die k Komponenten haben in der Regel ganze Zahlen (meist 0 1 : : : k ; 1) als Index. Mathematisch entspricht also X den Vektoren der Lange k mit Komponenten aus A, d. h. X = f(a0 a1 : : : ak;1 ) j ai 2 A i = 0 : : : k ; 1g Zu den Operationen auf Arrays gehoren: feste Komponentenzahl, direkter Zugri auf Komponenten mittels Indizes, homogener Grundtyp, Indizes konnen berechnet werden. Das Array ist die verbreiteste Datenstruktur in einigen Programmiersprachen (Fortran, Basic, Algol 60) sogar die einzige. Kennzeichen der Datenstruktur Array sind: 5.3 Arrays 5.3. ARRAYS 1 1 2 4 3 4 = y 5 7 8 9 ist nicht zulassig, sondern mu durch eine 6 9 16 25 36 49 64 81 Die Zuweisung x 0 0 KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN Row table"5] i table"4]"5] table"0] table"1] table"2] table"3] table"4] 1 3 2 ;! 2 3 1 4 const int n = 4 int vector"n] int temp // Hilfsvariable int limit = n/2 // obere Grenze in der for Schleife for (int i = 0 i < limit i++){ 4 nenten soll umgedreht werden. Beispiel 5.4 (Umdrehen eines Arrays) Ein 1-dimensionales Array mit n int Kompo- ein zu table aquivalentes Array. Indizes von Arrays konnen berechnet werden, d. h. in den "...] darf ein beliebiger Ausdruck stehen, der einen Wert vom Indextyp int liefert. Hier besteht die Gefahr der Bereichsuberschreitung , denn der berechnete Wert konnte den Index Bereich 0,...,n-1 uberschreiten. Hierfur ist der Programmierer selbst verantwortlich. double tabelle"5]"10] Variable lassen sich auch direkt denieren, z. B. deniert j table"i] greift dann auf die -te Komponente von table zu (also ein Row Objekt), und table"i]"j] auf die -te Komponente des Row Objektes table"i]. Mehrdimensionale Array table den Typ Row als Array mit 10 double Komponenten und die Variable table mit 5 Row Komponenten. table ist ein Beispiel eines 2-dimensionalen Arrays . Man stellt es sich am besten so vor: typedef double Row"10] realisiert werden. Der Komponententyp kann naturlich wieder ein strukturierter Typ sein. So deniert for (int i = 0 i < n i++) y"i] = x"i] y ist zunachst undeniert. for Schleife wie z. B. das Array x mit den Werten 62 63 deniert einen String name als Array von 11 char und initialisiert ihn zu Meier. Die Belegung des Arrays name ist dann wie folgt: char name"11] = "Meier" Hier sind weitere Operationen denkbar wir beschranken uns jedoch auf diese. In C++ existiert kein eingebauter Datentyp String Zeichenketten mussen als Arrays of char realisiert werden. Ein Beispiel: Konkatenation. strlen(string) Ermittelt die Anzahl der Zeichen von string. strcmp(str1 str2) Vergleicht str1 und str2 lexikographisch. Das Ergebnis ist < 0 falls str1 <lex str2, 0 falls str1 = str2 > 0 falls str1 >lex str2. strcpy(str1 str2) Kopiert str2 nach str1, d. h. funktioniert wie die Zuweisung. strcat(str1 str2) Fugt str2 am Ende von str1 an, d. h. realisiert die Der Wertebereich von Strings ist die Menge der Zeichenketten aus char Zeichen (einschlielich der leeren Zeichenkette). Die Operationen auf Strings sind meist sehr umfangreich. Typisch sind: variable Komponentenzahl, Komponenten sind homogen vom Typ char, direkter Zugri auf Komponenten, typische Stringoperationen wie: { Verkettung, { Zuweisung, { Vergleich bezuglich lexikographischer Ordnung, { Einfugen von Zeichen an bestimmter Stelle. Strings sind Zeichenketten. Sie sind extrem wichtig fur die EDV, werden aber sehr unterschiedlich in den verschiedenen Programmiersprachen behandelt. Kennzeichen der Datenstruktur String sind: 5.4 Strings Hier wird in vector"n-1-i] der Index n-1-i berechnet und dann auf die entsprechende Komponente von vector zugegrien bzw. ihr etwas zugewiesen. temp = vector"i] vector"i] = vector"n-1-i] // Zugriff auf Komponente n-1-i vector"n-1-i] = temp // Zuweisung an Komponente n-1-i }\\ end for 5.4. STRINGS 0 M 1 e 2 i 3 e 4 r 5 \0 6 7 8 9 10 KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN Kennzeichen der Datenstruktur Record sind: 5.5 Records schreibt dann Meier auf den Bildschirm. cout << andererName << '\n' Die Anweisung char andererName"11] = "" strcpy(andererName,name) Dann ist die Zuweisung bequem realisierbar durch #include <string.h> Sie realisiert die Zuweisung komponentenweise. Man beachte die Abbruchbedingung name"i] in der for-Schleife. name"i] wird (als int) 0, wenn der Null-Character \0 vorliegt. Da diese Behelfslosungen schwerfallig sind, hat man (bereits in C) so etwas wie einen abstrakten Datentyp String implementiert. Er behalt die Reprasentation von Strings als Array von char bei, stellt aber die oben genannten Operationen als Funktionen strlen, strcmp, strcpy, strcat zur Verfugung, deren Seiteneekte genau das Gewunschte leisten. Man kann sie sich verfugbar machen durch char andererName"11]=""\\initialisierung als leerer string int i for (i=0 name"i] i++){ andererName"i] = name"i] } nicht moglich. Man mu sich daher entweder behelfen oder einen entsprechenden abstrakten Datentyp implementieren. Eine Behelfslosung ware z. B. char andererName"11] andererName = name Dabei dient das Zeichen \0 als Stringbegrenzer. Ein 11-elementiges Array kann also nur Strings der Lange n aufnehmen, da ein Platz fur den Stringbegrenzer reserviert ist. \0 ist der Null-Character und besteht aus lauter 0-Bits. sein Wert als int ist damit 0, was vorteilhaft in Abfragen auf Ende des Strings verwendet werden kann (siehe unten). Da die Zuweisung von Arrays nicht in C++ realisiert ist, ist 64 name adresse matrikelnr fach 65 3 Genaugenommen ist struct ein Spezialfall einer class, in der alle Member public sind, vgl. Kapitel 7.3.1. enum GeschlechtsTyp{maennlich, weiblich, unbekannt} enum ZivilstandTyp{ledig, verheiratet, verwitwet, geschieden, unbekannt} struct AngestelltenTyp{ char vorname"11] nachname"21] char int alter GeschlechtsTyp geschlecht ZivilstandTyp stand wobei Ai der Wertebereich des i-ten Komponententyps ist. Ein Beispiel (in C++): X = A1 A2 : : : Ak Aus mathematischer Sicht entspricht der Wertebereich X eines Recordtyps dem kartesischen Produkt verschiedener Mengen, also struct StructTypName { Komponententyp_1 feldname_1 . . . Komponententyp_k feldname_k } Selektion von Komponenten ndet mit dem Punkt (.) statt. StudentRec student student.matrikelnr := 127538 In C++ existiert der eingebaute Typ struct als Implementation des Datentyps Record.3 end record String String Integer String typeStudentRec = record Die Komponenten von Records heien auch Felder . Record Typen werden in der Pseudosprache wie folgt deklariert feste Komponentenzahl, direkter Zugri auf Komponenten mittels Namen , heterogene Komponententypen, dafur keine Berechnung von Indizes. 5.5. RECORDS float } AngestelltenTyp manager, arbeiter1, arbeiter2 monatsgehalt KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN arbeiter2.nachname = "Meier" arbeiter1.vorname = arbeiter2.vorname deniert also die Instanz arbeiter2, die sich von unterscheidet (falls dieser ungleich Meier war). Beachte : Die Zuweisungen arbeiter1 arbeiter2 = arbeiter1 strcpy(arbeiter2.nachname,"Meier") // Verwendung von strcpy aus string.h nur durch den Nachnamen andert den Zivilstand von arbeiter2. In C++ ist die Zuweisung fur Instanzen eines struct zulassig (im Gegensatz zu Arrays). arbeiter2.stand = verheiratet ermittelt man den Anfangsbuchstaben des Vornamens, und cout << manager.vorname"0] dem manager das doppelte Gehalt des arbeiter1. Mit manager.monatsgehalt = arbeiter1.monatsgehalt * 2.0 das Alter von arbeiter 1 auf den Bildschirm, und sichert die Zuweisung cout << arbeiter1.alter Die Selektion einer Komponente (Member) erfolgt uber \.", z. B. schreibt { einer Fliekomma Variablen fur das Monatsgehalt. { zwei enum Typen fur Geschlecht und Stand, { einer int Variablen fur das Alter, { zwei Arrays mit 10 bzw. 20 Zeichen fur Vor- und Nachname, Hier sind manager, arbeiter1, und arbeiter2 Variable (in der Sprechweise von C++: Instanzen ) des Typs Angestelltentyp. Die Komponenten (in C++: Members ) dieser Instanzen bestehen aus: 66 67 homogene Komponenten (elementar oder strukturiert), veranderliche Lange (Listen konnen wachsen und schrumpfen ), Eine Liste ist eine lineare Datenstruktur. Ihre Komponenten werden Items oder Listenelemente genannt. Das erste Element heit Anfang oder Kopf (head ) der Liste, das letzte Element heit Ende (tail ). Kennzeichen der Datenstruktur Liste sind: 5.6 Listen Dann bezeichnet (nach entsprechender Initialisierung) skatblatt"10].wert den Wert der 11. Karte in skatblatt. Eine andere Losung bestunde in der Zuordnung der Karten Karo 7, : : :, Karo As, Herz 7, : : :, Pik 7, : : :, Kreuz 7, : : :, Kreuz As zu den Zahlen 0 2 : : : 31 und einer entsprechenden Ausrechnung durch Funktionen. So mute dann die Zahl 7 den Wert As und die Farbe Karo liefern usw. enum FarbenTyp{Karo, Herz, Pique, Kreuz} enum WertTyp{sieben, acht, neun, zehn, Bube, Dame, Koenig, As} struct Skatkarte{ FarbenTyp farbe WertTyp wert } Skatkarte skatblatt"32] Dann ist angestellter"i] ein struct vom Typ AngestelltenTyp. angestellter"2].alter bezeichnet also das Alter des 3. Angestellten im Array angestellter, und den 4. Buchstaben seines Nachnamens erhalt man durch angestellter"2].nachname"3]. Als weiteres Beispiel betrachten wir die Realisierung des Datentyps Skatkarte aus Beispiel 5.3 mit Arrays und Records const int NMAX = 1000 // maximale Anzahl von Datensaetzen AngestelltenTyp angestellter"NMAX] sind nicht moglich, da in C++ keine Zuweisungen an Array Variablen erlaubt sind. Dagegen ist jedoch die Zuweisung arbeiter1 = arbeiter2 moglich, allerdings nicht der Vergleich arbeiter1 == arbeiter2. Auch in C++ konnen die Komponenten eines struct beliebige Typen haben, insbesondere also wieder ein struct sein. Besonders wichtig sind Arrays von structs . Sie bilden die geeignete Datenstruktur fur alle moglichen Datensammlungen homogener Datensatze, wobei die einzelnen Datensatze aus heterogenen Komponenten bestehen, z. B. Personaldateien, Kontodateien usw. Die Personaldatei einer Firma konnte etwa folgendermaen aussehen: 5.6. LISTEN sequentieller Zugri auf Komponenten durch einen (impliziten) Listenzeiger , der stets auf ein bestimmtes Element der Liste zeigt, und nur immer ein Listenelement vor oder zuruck gesetzt werden kann. KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN void Delete() // PRE: NOT IsEmpty() && NOT EndOfList() // POST: Item at list cursor deleted void InsertAfter( /* in */ ItemType someItem ) // PRE: Assigned(someItem) // && NOT IsFull() && NOT EndOfList() // POST: someItem inserted after list cursor // && This is the new current item void InsertBefore( /* in */ ItemType someItem ) // PRE: Assigned(someItem) && NOT IsFull() // POST: someItem inserted before list cursor // (at back, if EndOfList()) // (at front, if IsEmpty()) // && This is the new current item ItemType CurrentItem() const // PRE: NOT IsEmpty() && NOT EndOfList() // POST: FCTVAL == item at list cursor void Advance() // PRE: NOT IsEmpty() && NOT EndOfList() // POST: List cursor has advanced to next item Boolean EndOfList() const // POST: FCTVAL == (list cursor is beyond end of list) void Reset() // PRE: NOT IsEmpty() // POST: List cursor is at front of list Boolean IsEmpty() const // POST: FCTVAL == (list is empty) Ein Beispiel sind Guterwaggons eines Zuges an einer Verladestation (= Listenzeiger), die nur einen Waggon zur Zeit beladen kann. Typische Listenoperationen sind das Einfugen in eine Liste, der sequentielle Ubergang zum nachsten Element, das Loschen eines Elementes usw. Wir werden sie nachstehend als C++ Member Funktionen einer Klasse ItemList wiedergeben. Dies greift dem spater eingefuhrten Klassenkonzept von C++ vor (vgl. Kapitel 7.3.2). Der hier gewonnene Vorteil ist, da diese Funktionen bereits genutzt werden konnen, ohne die Implementationsdetails der einzelnen Funktionen zu kennen. 68 EndOfList() 69 4 In der Coma II werden wir uber Templates bessere Methoden kennenlernen. // Uebertragen des Strings in eine Liste von Characters char string"81] char zeichen cout << "Schreiben Sie einen String und beenden Sie ihn mit <Return>.\n" << "String: " cin.get(string,81) // cin ist eine Klasse mit der Member Funktion get(String,n), die // bis zu n - 1 Zeichen oder bis zum ersten \r liest und die gelesenen // Zeichen in string abspeichert einschliesslich einem \0 als Terminator void main() { // Einlesen eines Strings als array of char string_liste.cc #include <iostream.h> #include "ilist.h" Programm 5.1 Dies leistet folgendes C++ Programm: { Ausgabe der gekurzten Liste. { Loschen des Anfangs der Liste bis zu einem vorgegebenen Zeichen, { Ausgabe der Liste auf dem Bildschirm, { Einlesen eines Strings in eine Liste von char, Beispiel 5.5 (Einlesen eines Strings in eine Liste) Es sollen folgende Aktionen ausgefuhrt werden: und die Listenoperationen durch Hinzuladen der Headerdatei ItemList.h (und indirekt der zugehorigen Implementation der in ItemList.h deklarierten Funktionen) nutzen. typedef char ItemType Um hieraus eine konkrete Liste zu machen | etwa eine Liste von | chars, kann man ItemType entsprechend umdenieren4 && && Successor of deleted item is now the current item ItemList() // Constructor // POST: Empty list created // 5.6. LISTEN 70 cout << "Gekuerzte Liste: " if ( stringlist.IsEmpty() ) { // Rausschreiben der gekuerzten Liste auf den Bildschirm cout << "Geben Sie das Zeichen an, bis zu dem geloescht wird: " cin >> zeichen if ( ! stringlist.IsEmpty() ) // sonst nichts machen { stringlist.Reset() while ( stringlist.CurrentItem() != zeichen && ! stringlist.EndOfList()) { stringlist.Delete() } } // Loeschen des Anfangs bis zum ersten eingegebenen Zeichen cout << "Liste: " if ( stringlist.IsEmpty() ) { cout << "leer.\n" } else { stringlist.Reset() while ( ! stringlist.EndOfList() ) { cout << stringlist.CurrentItem() stringlist.Advance() } cout << '\n' } // Rausschreiben der Liste auf den Bildschirm ItemList stringlist // erzeugt leere Liste if ( string"0] != '\0' ) // sonst leerer String { stringlist.InsertBefore( string"0] ) // erstes Zeichen uebertragen // InsertAfter() nicht anwendbar, da EndOflist() int i = 1 while ( string"i] ) { stringlist.InsertAfter( string"i] ) // weitere Zeichen uebertragen i++ } } KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN 71 Die logische Zusicherung vor Ausfuhrung der Funktion, sie mu vor Ausfuhrung der Funktion gewahrleistet sein. Die logische Zusicherung nach Ausfuhrung der Funktion, sie gilt nach der Ausfuhrung. Der gelieferte Funktionswert. Die Argumente fur die Funktion. void Pop() ItemType Top() const // PRE: NOT IsEmpty() // POST: FCTVAL == item at top of stack void Push( /* in */ ItemType newItem ) // PRE: NOT IsFull() && Assigned(newItem) // POST: newItem is at top of stack Boolean IsEmpty() const // POST: FCTVAL == (stack is empty) Stacks sind eine eingeschrankte Form von Listen, bei denen das Einfugen und Loschen nur am Kopf (genannt top ) moglich ist. Als Liste gesehen kann der Listenzeiger also nur auf das erste Element zeigen. Ein Beispiel ist ein Bucherstapel in einem engen Karton, man hat immer nur auf das obere Buch Zugri. Man nennt daher Stacks auch Last-In, First-Out oder LIFO Listen. Wie bei Listen sehen wir uns die Stack Operationen ausgedruckt als Member-Funktionen einer C++ Klasse ItemStack an. 5.7 Stacks // FCTVAL /* IN */ // POST // PRE Die Kommentare bei den Listen-Operationen in Form von C++ Funktionen sind ein Beispiel fur einen guten Dokumentationsstil . Dabei bedeutet: } cout << "leer.\n" } else { while ( ! stringlist.EndOfList() ) { cout << stringlist.CurrentItem() stringlist.Advance() } cout << '\n' } 5.7. STACKS ItemStack() // Constructor // POST: Empty stack created // PRE: NOT IsEmpty() // POST: Top item removed from stack a b a b a c b a a leer Top b a a So ist z. B. (()()))() nicht korrekt, aber Paarung des zweiten Ausdrucks ist f f g f f g g g korrekt. Die entsprechende Klammerausdrucke sind auf Korrektheit zu uberprufen und einander entsprechende Klammern sind zu paaren. Beispiel 5.6 (Erkennung von korrekten Klammerausdrucken) a Laufzeitverwaltung von Funktions- und Prozeduraufrufen, Realisierung von Rekursion, Auswertung von Ausdrucken in Postxnotation, z. B. in HP-Taschenrechnern, etwa Eingabe: abc+* Stackfolge: leer a b c c+b (c+b)*a Top Dabei wird in der while-Schleife cba auf den Bildschirm geschrieben. Stacks sind fundamental fur viele Aufgabenstellungen der Informatik, z. B. leer die folgende Folge von Belegungen der Instanz mystack: ItemStack mystack mystack.Push('a') mystack.Push('b') mystack.Push('c') while ( ! mystack.IsEmpty() ) { cout << mystack.Top() mystack.Pop() } cout << '\n' KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN Dann erzeugt die Folge der C++ Anweisungen 72 f f g f f g g g 73 1 (1 3 (3 (2 (1 Paar (3 )4 + 4 (2 (1 5 (5 (2 (1 + + 7 (1 Paar Paar (5 )6 (2 )7 6 (2 (1 8 (8 (1 + 1 (1 2 3 leer Error korrekten Klammerausdrucke als korrekt und identiziert zueinandergehorende Klammerpaare richtig. Satz 5.1 (Erkennung korrekter Klammerausdrucke) Algorithmus 5.1 erkennt genau die Also wird (1 )2 )3 (4 )5 als nicht-korrekter Klammerausdruck erkannt. 0 leer Bei dem Beispiel (1 )2 )3 (4 )5 ergibt sich die Stackfolge (1 (2 (3 )4 (5 )6 )7 (8 )9 )10 + 10 leer Paar Paar (8 )9 (1 )10 9 (1 Abbildung 5.2: Ein Beispiel zu Algorithmus 5.1. 2 (2 (1 Sie zeigt, da der Ausdruck korrekt ist mit der folgenden Klammerung: 0 leer Als konkretes Beispiel betrachten wir (1 (2 (3 )4 (5 )6 )7 (8 )9 )10 , wobei die Klammern zur besseren Identizierung mit Indizes versehen sind. Dann ergibt sich in Algorithmus 5.1 die in Abbildung 5.2 dargestellte Stackfolge. 1. Lese die Folge der Klammern von links nach rechts. 2. Falls \(", so pushe diese auf den Stack. 3. Falls \)" so poppe eine \(" vom Stack, erklare diese als zur momentan gelesenen \)" gehorig. 4. Erklare den Klammerausdruck als korrekt, falls der Stack am Ende leer ist, jedoch zwischendurch nie vom leeren Stack gepoppt wird. Algorithmus 5.1 (Erkennung von korrekten Klammerausdrucken) Dies geschieht mit dem folgenden Algorithmus. 5.7. STACKS KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN a) Aus ` < k folgt, da sich A zerlegen lat in einen ersten Teil B := a1 a2 : : : a` und einen zweiten Teil C := a`+1 : : : ak ai 2 f( )g. Da die Stack Bedingungen in A erfullt sind, sind sie nach der Wahl von ` auch in B und C erfullt, und die Induktionsvoraussetzung trit wegen ` < k auf B und C zu. Also sind B und C korrekt, und damit nach den Syntaxregeln auch A = BC . 2. Sind die Stack Bedingungen erfullt, so ist A ein korrekter Klammerausdruck. Betrachte die erste Stelle `, an der der Stack nach der zugehorigen Push/Pop Operation leer ist. Es gibt 2 Falle: a) ` < k oder b) ` = k. Abbildung 5.3 illustriert die Stackfolgen fur die beiden auftretenden Falle. a) Da auf B C die Induktionsvoraussetzung zutrit, werden sie als korrekt erkannt und werden die zugehorigen Korrespondenzen richtig ermittelt. Die Stackfolge fur A ergibt sich als Konkatenation der Stackfolgen von B und C . Also gelten auch in A die Stack Bedingungen. Da der Stack am Ende von B leer ist, konnen auch innerhalb von A Klammern aus B nur mit Klammern aus B korrespondieren. Diese Korrespondenzen werden nach Induktionsvoraussetzung richtig erkannt. Das gilt entsprechend auch fur C . b) Da auf B die Induktionsvoraussetzung zutrit, wird B als korrekt erkannt und werden die zugehorigen Korrespondenzen richtig ermittelt. Dies bedeutet, da sich in A die aueren Klammern entsprechen mussen, da alle Klammern in B bereits fur die Korrespondenzen innerhalb von B \verbraucht" werden. Die Stackfolge fur A ergibt sich also aus der Stackfolge von B durch Anhangen der ersten \(" von A als unterste Komponente des Stacks. Also gelten auch in A die Stack Bedingungen. Da der Stack am Ende von B genau noch die erste \(" von A enthalt, werden die aueren Klammern als korrespondierend erkannt. Da innerhalb von B stets die erste \(" von A als unterste Komponente im Stack enthalten ist, werden die Korrespondenzen innerhalb von B auch nach Induktionsvoraussetzung richtig erkannt. 1. Ist A ein korrekter Klammerausdruck, so sind die Stack Bedingungen erfullt und korrespondierende Klammern werden richtig ermittelt. Sei A korrekt mit der Lange k > 2. Dann gibt es aufgrund der Syntax fur korrekte Klammerausdrucke (vgl. 4.1) 2 Falle: a) A = B C oder b) A = (B ), wobei B C kurzere korrekte Klammerausdrucke sind, auf die dann die Induktionsvoraussetzung zutrit. gefuhrt. Induktionsanfang : k = 2. Oenbar wird bei k = 2 Klammern genau () als korrekt erkannt, und die Klammerkorrespondenz hergestellt. Induktionsvoraussetzung : Die Methode arbeitet fur alle Klammerausdrucke der Lange < k korrekt (k > 2). Induktionsschlu auf die Lange k: Beweis: Der Beweis wird durch Induktion nach der Anzahl k der Klammern im Ausdruck 74 75 ppp 1 (1 leer leer unterer Komponente Stackfolge fur B mit ( als zusatzlicher, (2 (1 ppp Stackfolge fur C leer (C1 Fur den String ((a+b)(-1))/(2+c) ergibt sich mit n= 20 die Belegung Wir betrachten jetzt eine Implementation von Algorithmus 5.1 fur Strings mit hochstens n Zeichen, der auer den Klammern ( und ) auch andere Zeichen enthalten kann. Der Test auf korrekte Klammern bezieht sich auf ( und ). Dazu verwenden wir folgende Datenstrukturen. folge : Array von char mit n Komponenten folge"i] entspricht dem (i+1)-ten Zeichen eines eingegebenen Strings \\0" entspricht dem Ende des Ausdrucks. partner : Array von int mit n Komponenten Am Ende soll partner"i] die zu folge"i] zugehorige Klammer angeben. Dabei soll gelten (mit k > 0): 8 > < k folge"k] und folge"i] bilden ein Paar (..) oder partner"i] := und folge"k] bilden ein Paar (..), > ;1 folge"i] : folge"i] 6= (,). Abbildung 5.3: Die Stackfolgen aus dem Beweis von Satz 5.1. leer (1 Fall b) ppp Stackfolge fur B leer (B1 Fall a) Aus 1 und 2 folgt die Behauptung. b) Aus ` = k folgt, da in der Stackfolge die zuerst gelesene \(" von A bis zum Schlu auf dem Stack bleibt. Da die Stack Bedingungen erfullt sind, mu die letzte Klammer von A eine \)" sein, die dann mit der ersten Klammer korrespondiert. Also ist A von der Form A = (B ). Die Stackfolge von B ist dann gleich der Stackfolge von A ohne die erste Klammer \(" von A. Also folgt, da auch die Stack Bedingungen fur B erfullt sind. Da B kurzer als A ist, ist B nach Induktionsvoraussetzung korrekt, und damit nach den Syntaxregeln auch A. 5.7. STACKS 0 ( 1 ( 2 a 3 + 4 b 5 ) 6 ( 7 - 8 1 9 ) ) 5 2 -1 -1 4 -1 1 6 9 -1 8 -1 6 2 0 10 -1 12 16 c ) 16 17 \0 18 19 -1 14 -1 -1 16 12 -1 18 -1 -1 undeniert sind. Fur das Array + 14 15 // main loop, use stack for checking correctness i = 0 Boolean korrekt = TRUE // define stack instance S ItemStack S // define partner and initialize to -1 ... -1 int partner"NMAX + 1] int i for (i = 0 i < NMAX + 1 i++) { partner"i] = -1 }//endfor void main() { // read the string const int NMAX = 80 // maximum length of the string char folge"NMAX + 1] // takes the string cout << "Bitte String eingeben und mit <RETURN> beenden.\n" << "Folge: " cin.get(folge,NMAX + 1) #include <iostream.h> #include "bool.h" #include "istack.h" // with ItemType defined as int klammern.cc // reads string of parantheses and other chars // uses stack to check for correct parantheses rules Programm 5.2 Um diese Belegung von partner zu erreichen wird statt der \(" in einem Stack jeweils die Position i im Array folge abgespeichert, d. h. man deniert den benotigten Stack als S : Stack von int Abbildung 5.4 zeigt ein Struktogramm fur die Verfeinerung von Algorithmus 5.1 mit diesen Datenstrukturen. Eine Implementierung in C++ gibt Programm 5.2. 0 10 ( folge"19] / 10 11 12 13 KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN fur das Array folge, wobei folge"18] und partner ergibt sich entsprechend: 76 true false folge"i] := false kann bei \)" nicht vom Stack gepoppt werdeng fes korrekt true partner"m] partner"i] := := fentferne \("g S.Pop() i m Position der \(" in der Variablen mg fmerke Abbildung 5.4: Struktogramm fur Algorithmus 5.1. and false i := i+1 i := i+1 ` `` ``` S.IsEmpty() ``` korrekt ``` ``` `` Gebe folge und partner aus Fehlermeldung: Der Ausdruck ist fDer Ausdruck ist korrektg an Stelle i inkorrekt die Position der \(" im Stackg fmerke hhhh hhh h hhhh h h X XXX XXX S.IsEmpty() XXX X m := S.Pop() '(' hhhh hh h hhhh h S.Push(i) hhhh '(' Initialisiere das Array partner zu -1 ... -1 Deniere S fEinrichten des leeren Stacksg i := 0 fInitialisierung der Zahlvariableng korrekt := true fBoolesche Variable bleibt true bis festgestellt wird, da folge nicht korrekt istg i<n and korrekt and folge"i] 6= '\0' h k n Initialisiere das Array folge, d. h. lese die k gegebenen Zeichen auf folge"0] ... folge"k-1] ein und setze folge"k] := '\0', falls 5.7. STACKS 77 78 // output correct pairs or a mistake if ( korrekt && S.IsEmpty() ) { // correct paranthesises // output correct pairs cout << "Der String ist korrekt mit folgender Klammerung:\n" cout << folge i = 0 while ( TRUE ) { while ( folge"i] != '(' && folge"i] != '\0') { // look for next '(' i++ }//endwhile if ( folge"i] == '\0' ) { // no "(" break // leave while loop }//endif cout << '\n' // newline on current '(' for ( m = 0 m < i m++) { cout << ' ' // indent until current '(' }//endfor cout << folge"i] // write current '(' for ( m = i + 1 m < partner"i] m++) { cout << ' ' // indent until corresponding ')' }//endfor int m while ( i < NMAX +1 && korrekt && folge"i] != '\0' ) { switch ( folge"i] ) { case '(' : { S.Push(i) // remember position i of '(' on stack S break} case ')' : { if ( S.IsEmpty() ) { korrekt = FALSE // no corresponding '(' } else { m = S.Top() // corresponding '(' is in position m S.Pop() // remove the position of '(' partner"i] = m // positions i and m have coresponding partner"m] = i // paranthesises } //endif break} default : { // neither '(' nor ')' at position i // nothing to do, partner"i] is already -1 } }//endswitch i++ }//endwhile // if NOT korrekt, then at position i (counting from 1) KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN 79 Queues haben viele Anwendungen, z. B. in Rechnerbetriebssystemen (Verwaltung wartender ItemQueue() // Constructor // POST: Empty queue created void Dequeue() // PRE: NOT IsEmpty() // POST: Front item removed from queue ItemType Front() const // PRE: NOT IsEmpty() // POST: FCTVAL == item at front of queue void Enqueue( /* in */ ItemType newItem ) // PRE: NOT IsFull() && Assigned(newItem) // POST: newItem is at rear of queue Boolean IsEmpty() const // POST: FCTVAL == (queue is empty) Queues sind wie Stacks eingeschrankte Listen, bei denen das Einfugen nur am Ende (rear oder tail ) und das Loschen nur am Kopf (front oder head ) moglich ist. Sie bilden also die geeignete Datenstruktur fur das, was man im taglichen Leben unter \Warteschlange" versteht. Man nennt daher Queues auch First-In, First-Out oder FIFO Listen. Wie bei Stacks drucken wir die Operationen als Member-Funktionen einer C++ Klasse ItemQueue aus. 5.8 Queues (Warteschlangen) } cout << folge"partner"i]] // write corresponding ')' i++ // increase i for next while loop }//endwhile }//endif else { cout << '\n' cout << "Die Klammerung ist an Position " << i << " nicht korrekt.\n" // indicate the wrong position cout << folge << '\n' for ( m = 0 m < i - 1 m++) { // indent until wrong position cout << ' ' }//endfor cout << '!' // write '!' at wrong position }//endelse cout << '\n' 5.8. QUEUES (WARTESCHLANGEN) KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN j " j " j # j # j # z. B. Row Vektoren Matrizen int Row"10] // table"5] // z. B. typedef niedrigste Stufe, z. B. int " " # b) Die axiomatische Methode Beispiele hierzu sind die Datenstrukturen Liste, Stack, Queue. Die Denition geschieht implizit Denition mittels Operationen und deren Eigenschaften in Form von Axiomen. Auch diese Methode ist in der Mathematik gebrauchlich, z. B. Mengen von Mengen (Potenzmenge) Mengen von Elementen (Menge) # Elemente einer Grundmenge Aus der Mathematik ist diese fortgesetzte Abstraktion z. B. von Mengen bekannt: .. . Konstruktor Selektor strukturierte Objekte 2. Stufe Konstruktor Selektor strukturierte Objekte 1. Stufe Konstruktor Selektor " j einfache Datentypen a) Die konstruktive Methode Hierzu gehort die Denition von Arrays in C++. Die Denition \hoherer" Datentypen erfolgt aus bereits eingefuhrten Datentypen nach folgendem Muster. Es gibt verschiedene Methoden, um Datentypen zu denieren: Datentypen und zugehorige Operationen bilden eine Einheit und konnen nicht getrennt gesehen werden. Die Semantik ergibt sich erst durch den Wertebereich und die Operationen (mit den zugehorigen Axiomen) Die diskutierten Beispiele von Datenstrukturen zeigen: 5.9 Zusammenfassung Jobs, Puerung von Ein/Ausgabe) und bei der Simulation von \Wartesituationen" in vielen Algorithmen und Modellen betrieblicher Ablaufe (Abfertigung an Bankschaltern u. a.) 80 81 Datenstrukturen werden in nahezu allen Buchern uber Entwurf und Analyse von Algorithmen behandelt. Die hier gegebene Darstellung lehnt sich an HR94] an. Dort wird im Gegensatz zu den meisten Buchern ausfuhrlich auf die Umsetzung von der abstrakten Spezikation zu C++ Programmen eingegangen. Die hier gegebenen Deklarationen der C++ Funktionen fur Listen, Stack und Queues sind eine leichte Modikation der dort gegebenen Darstellung. 5.10 Literaturhinweise Zusammenfassend lat sich feststellen, da die konstruktive Methode sich bereits als Implementationsvorschrift verstehen lat, wahrend die axiomatische Methode erlaubt wesentlich mehr Freiheitsgrade erlaubt. { Peano Axiome fur naturliche Zahlen { Inzidenzbeziehungen in der Geometrie usw. Die Vorteile der axiomatischen Methode ist die Abstraktion von der Implementation (Implementationsdetails sind unwesentlich). Dies erlaubt eine genauere Spezikation und leichtere Korrekheitsbeweise. Ein (leichter) Nachteil besteht darin, da i. a. verschiedene Interpretationen (Modelle) einer abstrakten Datenstruktur moglich sind. Daher ist die Ubereinstimmung von Modell und Spezikation i. a. nicht leicht uberprufbar, insbesondere fur ungeubte Benutzer. 5.10. LITERATURHINWEISE 82 KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN int int int int a"n] x index i Programm 6.1 83 sequuential_search.cc // the array // the value to search for // variable to hold the index or -1 Eine einfache Losung dieses Problems basiert auf folgender Idee. Durchlaufe die Komponenten des Arrays sequentiell bis die aktuelle Komponente i den Wert x enthalt. Gebe dann i aus. Ist x in keiner Komponente, gebe ;1 aus. Diese Methode heit sequentielle Suche . Sie erfordert im schlimmsten Fall n Vergleiche bis der Index i gefunden ist, bzw. festgestellt wird, da keine Komponente den Wert x hat. Ein C++ Programmfragment hierfur ist: 6.1.1 Sequentielle Suche Hier handelt es sich um folgendes Basisproblem in Arrays: Gegeben: Ein Array mit n ganzen Zahlen, eine ganze Zahl x. Gesucht: Der Index i einer Komponente mit Wert x (falls vorhanden). 6.1 Suchen einer Komponente vorgegebenen Wertes Berechnung kurzester Wege in Graphen. Losung linearer Gleichungssysteme, Suchen eines Werts in einem Array, Wir behandeln jetzt drei Aufgabenstellungen mit zunehmendem Schwierigkeitsgrad, deren Losung zu Algorithmen auf Arrays fuhrt: Algorithmen auf Arrays Kapitel 6 85 1 2 3 4 5 6 7 4 5 6 7 10 12 13 16 und x = 10. Zuerst wird i in der Mitte gewahlt, etwa i = 3. Dann ist a i] = 8 < x = 10, und es wird in der rechten Halfte von a weitergesucht. 0 3 5 6 8 10 12 13 16 Als Beispiel betrachten wir a mit den Werten { Wahle den mittleren Index i und vergleiche x und a i]. { Ist a i] = x, so ist der Index i gefunden. { Ist a i] < x, so kann x nur in der \linken Halfte" von a, d. h. in a 0] : : : a i ; 1] sein. Wende das Verfahren auf die linke Halfte an. { Ist a i] > x, so kann x nur in der \rechten Halfte" von a, d. h. in a i + 1] : : : a n ; 1] sein. Wende das Verfahren auf die rechte Halfte an. { Verfahre so weiter bis x gefunden, oder der noch zu durchsuchende Teil, in dem x sein konnte, leer ist. Liegt das Array a in sortierter Form vor, gilt also a 0] a 1] : : : a n ; 1], so lat sich die Suche wesentlich beschleunigen durch die Verwendung der binaren Suche oder Bisection . Die Idee der binaren Suche ist wie folgt: 6.1.2 Binare Suche Ist i = n, so ist x nicht im Array. Diese Uberlegungen zeigen die Korrektheit des Algorithmus. Zusicherungen bei Schleifen nennt man auch Schleifeninvarianten . Sie spielen bei Korrektheitsbeweisen eine wichtige Rolle. Das Beispiel zeigt ubrigens auch eine sinnvolle Verwendung fur die break-Anweisung. a i] = x oder i = n a"n] x index i,j,k Beweis: Der Beweis erfolgt durch vollstandige Induktion nach n. Induktionsanfang : Ist n = 1, so ist nur 1 Vergleich erforderlich. Also stimmt die Behauptung wegen blog 1c + 1 = 0 + 1 = 1. 1 log n bedeutet hier (wie stets in der Informatik) den Logarithmus von n zur Basis 2. dae ist a nach oben gerundet, bac ist a nach unten gerundet, also d5 2e = 6 und b5 2c=5. Satz 6.1 (Aufwand der binaren Suche) Fur die Anzahl C (n) von Vergleichen bei der binaren Suche in einem Array mit n Komponenten gilt1 C (n) blog nc + 1. Also arbeitet der Algorithmus korrekt. Bezuglich der notigen Anzahl von Vergleichen ergibt sich: a i ; 1] < x a j + 1] und 0 i j n ; 1: Da in jedem Schleifendurchlauf i erhoht oder j erniedrigt wird, terminiert das Programm mit a k] = x oder i > j . Ist dann a k] 6= x, so mu i > j gelten. Daher mu im Durchlauf k = i oder k = j sein. Wegen k = (i + j ) div 2 folgt, da i = j = k in diesem Durchlauf ist. Es wurde also die letzte nach der Invarianten mogliche Komponente a k] auf Gleichheit mit x getestet. Da a k] 6= x festgestellt wurde, kann x nicht im Array sein. Vor dem i-ten Eintritt (i 2) in die Schleife gilt (falls x im Array ist) die Invariante k halfway between i and j lower index i upper index j index i and upper index j binary_search.cc // the array // the value to search for // variable to hold the index or -1 i = 0 j = n - 1 // initialize lower do { k = ( i + j ) / 2 // choose if ( x > a"k] ) i = k + 1 // update else j = k - 1 // update } while ( a"k] != x && i <= j ) if ( a"k] == x ) index = k else index = -1 int int int int Programm 6.2 i < n und a 0] : : : a i ; 1] 6= x Beim Austritt aus der Schleife gilt 4 10 der linken Halfte des verbleibenden Arrays weitergesucht, also in i wird wieder in der Mitte gewahlt, etwa i = 5. Dann ist a i] = 12 > x = 10, und es wird in 6.1. SUCHEN EINER KOMPONENTE VORGEGEBENEN WERTES Beim i-ten Wiedereintritt in die for Schleife gilt die Zusicherung (Assertion ) = i = -1 i++ ) { ) break KAPITEL 6. ALGORITHMEN AUF ARRAYS Die einzig verbleibende Wahl von i = 4 ndet dann den gesuchten Wert. Ware jetzt x 6= a i], so stellte man an dieser Stelle fest da x nicht im Array enthalten sein kann. Ein C++ Programmfragment hierfur ist: for ( i = 0 i < n if ( a"i] == x }//endfor if ( i < n ) index else index 84 KAPITEL 6. ALGORITHMEN AUF ARRAYS 1 + C (bn=2c) 1 + blog(bn=2c)c + 1 1 + log(n=2) + 1 = 1 + log(n=2) + log 2 = 1 + log(n=2 2) = 1 + log n da log(a b) = log a + log b da bn=2c n=2 1 xn .. C mit xi 2 I i = 1 : : : n: . A x1 x2 C CC Die Menge aller solchen Vektoren wird mit Vn (I ) bezeichnet. Meist ist I = IR, d. h. Vn (I ) = Vn (IR). Dafur schreibt man auch kurz IRn . 0 BB x=B B@ Ein n-dimensionaler Vektor (genauer: Spaltenvektor ) mit Elementen aus einer Menge I ist ein n-Tupel 6.2.1 Vektoren und Matrizen 6.2 Lineare Gleichungssysteme Es sind also wesentlich weniger Vergleiche notig als bei der sequentiellen Suche. Bei n = 1:000:000 220 reichen C (n) = 20 Vergleiche bei der binaren Suche aus, wahrend die sequentielle Suche 1.000.000 Vergleiche braucht. Also ist C (n) 1 + log n. Da C (n) ganzzahlig ist, folgt C (n) b1 + log nc = blog nc + 1. C (n) Vergleiche. Insgesamt sind dann 1 + C (bn=2c) Vergleiche erforderlich. Einsetzen ergibt: C (bn=2c) blog(bn=2c)c + 1 Induktionsvoraussetzung : Die Behauptung sei richtig fur alle Arrays der Lange < n n 2. Schlu auf n: Nach dem ersten Vergleich mit a k] mu nur in einer der Halften weitergesucht werden. Beide Halften haben eine Lange bn=2c und erfordern daher nach Induktionsvoraussetzung 86 .. . .. . .. . .. . ai1 ai2 : : : aij : : : ain .. . C C C C C C C A a11 a12 : : : a1j : : : a1n 1 a21 a22 : : : a2j : : : a2n C C 87 1 C C C A 0 B B B B B B B @ .. . .. . am1 : : : amp ai1 : : : aip .. . .. . a11 : : : a1p 1 C 0 C CC B b11. : : : C @ .. C C A bp1 : : : .. C . A cm1 : : : cmn B . .. C . A = @ .. bpj : : : bpn .. . b1j : : : b1n 1 0 c11 : : : c1n 1 Multiplikation einer m p-Matrix A mit einer p n-Matrix B : 2. Multiplikation einer 1 3 Matrix mit einem 3-Vektor. Ergebnis ist eine Zahl. 0 1 B 1 C 1 2 1 @ 0 A = 1 1 + 2 0 + 1 (;1) = 0 ;1 1. Multiplikation einer 2 3 Matrix mit einem 3-Vektor. Ergebnis ist ein 2-Vektor. ! 0 1 1 ! ! 1 2 1 B 1 1 + 2 0 + 1 (;1) = 0 C 0 = @ A 2 0 ;1 2 1 + 0 0 + ;1 (;1) 3 ;1 Beispiele: denieren entsprechende Typen. Matrizen und Vektoren konnen wie folgt miteinander multipliziert werden: Multiplikation einer m n-Matrix A mit einem n-Vektor x: 0 a11 : : : a1n 1 0 x1 1 0 a11 x1 + a12 x2 + + a1nxn B a21 x1 + a22 x2 + + a2nxn B . C B B .. C @ ... . A @ .. A = B @ ::::::::::::::::::::::::::::: xn am1 : : : amn am1 x1 + am2 x2 + + amn xn typedef double Vektor"n] typedef double Matrix "m]"n] Oensichtlich lassen sich Vektoren und Matrizen in C++ durch 1- bzw. 2-dimensionale Arrays darstellen. am1 am2 : : : amj : : : amn von m n Elementen aus I . Mmn (I ) bezeichnet die Menge aller m n-Matrizen von Elementen aus I . Meist ist I = IR oder I IR. Jede Spalte von A bildet einen Vektor Aj , den sogenannten Spaltenvektor (j = 1 : : : n) analog bildet jede Zeile Ai einen Zeilenvektor (i = 1 : : : m). 0 B B B B A=B B B B B @ Eine m n-Matrix mit Elementen aus einer Menge I ist eine Zusammenfassung 6.2. LINEARE GLEICHUNGSSYSTEME = p X k=1 aik bkj cij = ai1 b1j + ai2 b2j + + aip bpj KAPITEL 6. ALGORITHMEN AUF ARRAYS 1 2 + 2 (;1) + 1 1 1 1 + 2 0 + 1 1 1 3 + 0 + 0 2 2 + 0 0 + (;1) 1 2 1 + 0 0 + (;1) 1 2 3 + 0 + 0 ! = 13 21 36 ij ! 3. Multiplikation eines 3 Vektors mit einer 1 3 Matrix. Ergebnis ist eine 3 3 Matrix. 0 1 0 1 0 1 B@ 20 CA 1 2 1 = B@ 20 11 20 22 20 11 CA = B@ 20 40 20 CA 1 11 12 11 1 2 1 2. Multiplikation einer 1 3 Matrix mit einem 3-Vektor. Ergebnis ist eine Zahl. 0 1 B 2 C 1 2 1 @ 0 A = 1 2 + 2 0 + 1 1 = 3 1 = 1. Multiplikation einer 2 3 Matrix mit einer 3 3 Matrix. Ergebnis ist eine 2 3 Matrix. !0 2 1 3 1 1 2 1 B C 2 0 ;1 @ ;11 01 00 A Beispiele: j = Abbildung 6.1: Schema der Matrixmultiplikation. i Die Formel zur Berechnung von cij zeigt, da cij aus der i-ten Zeile von A und der j -ten Spalte von B gebildet wird. Die illustriert Abbildung 6.1. mit 88 89 a"m]"p] b"p]"n] c"m]"n] j, k // Array fuer Matrix A // Array fuer Matrix B // Array fuer Ergebnismatrixatrix C A := (aij ) ij=1=1 :::::: mp B := (bij ) ji=1 ::: p =1 ::: n Ein Betrieb verarbeitet an Produktionsstatte P1 die Rohstoe R1 : : : Rn zu Zwischenprodukten Z1 : : : Zp , die dann an der Produktionsstatte P2 zu Endprodukten E1 : : : Em weiterverarbeitet werden. Der Bedarf an Rohstoen fur die Zwischenprodukte ist durch Tabelle 6.1 gegeben, d. h. zur Produktion einer Einheit von Zi werden bij Einheiten von Rj benotigt, j = 1 : : : n. Ebenso ist der Bedarf an Zwischenprodukten fur die Endprodukte durch Tabelle 6.2 gegeben. Diese beiden Tabellen denieren Matrizen A und B gema 6.2.2 Ein Produktionsmodell verwenden (geeignete \Abmessungen" vorausgesetzt). A x bzw. Ax fur die Matrix-Vektor Multiplikation, und A B bzw. AB fur die Matrizenmultiplikation Die algebraische Struktur der Menge der Matrizen (Rechenstruktur!) wird in der Linearen Algebra ausfuhrlich behandelt. Wir werden im weiteren die Kurzschreibweisen // multiplication for ( i = 0 i < m i++ ) { for ( j = 0 j < n j++ ) { c"i]"j] = 0 for ( k = 0 k < p k++ ) { c"i]"j] = c"i]"j] + a"i]"k] * b"k]"j] }//endfor k }//endfor j }//endfor i double double double int i, In C++ lat sich die Matrizenmultiplikation folgendermaen realisieren. A B = A (B1 : : : Bn ) = (A B1 : : : A Bn ): Diese Beispiele zeigen, da die Matrixmultiplikation die Multiplikation von Matrizen mit Vektoren als Spezialfall enthalt. Stellt man sich die n Spalten der p n-Matrix B als pSpaltenvektoren Bj j = 1 : : : n, so gilt 6.2. LINEARE GLEICHUNGSSYSTEME ai1 ai2 : : : aip Em am1 am2 : : : amp .. . Ei .. . Z1 Z2 : : : Zp E1 a11 a12 : : : a1p Tabelle 6.2: Zwischenproduktbedarf fur Endprodukte. Zp bp1 bp2 : : : bpn .. . Zi bi1 bi2 : : : bin .. . Z1 R1 R2 : : : Rn b11 b12 : : : b1n Tabelle 6.1: Rohstobedarf fur Zwischenprodukte. KAPITEL 6. ALGORITHMEN AUF ARRAYS 0 BB y=B B@ 1 zj = y1 a1j + y2 a2j + + ym amj Die benotigte Menge zj des Zwischenprodukts Zj an Produktionsstatte P1 ergibt sich zu ym .. C . A y1 y2 C CC Diese Daten sollen jetzt zum Einkauf der benotigten Rohstoe fur eine vorgegebene Produktion von yi Einheiten von Endprodukt Ei (i = 1 : : : m) genutzt werden. Gegeben ist also der Produktionsvektor C heit Rohstobedarfsmatrix . C =AB : Bezeichnet man die Matrix der cij als C , also C := (cij ) ij=1 ::: m , so ergibt sich C also durch =1 ::: n Multiplikation der Matrizen A und B , d. h. Dann ergibt sich der Bedarf cij an Rohsto Rj fur die Produktion einer Einheit von Endprodukt Ei zu cij = ai1 b1j + ai2 b2j + + aip bpj : 90 zp 0 z1 1 z=B A @ ... C amj .. . a1j a2j xn zp .. C . A = BTj z : 91 x = C T y = B T AT y : 3 2 T A ist die zu A transponierte Matrix AT = (aTij ) mit aTij = aji . Sie ergibt sich aus A durch Spiegeln an der Hauptdiagonalen. 3 Es gilt allgemein bezuglich der Transponierung (A B )T = B T AT (Ubung). Daher hatte man die obige Gleichung auch aus dieser Regel direkt ableiten konnen. Also ist xj = Bedarf an Rj fur alle Ei zusammen bei Produktionsvektor y = y1 c1j + y2 c2j + + ym cmj 0 1 0 1 c1j y1 B C B c2j C y2 C B B C B C B = (y1 y2 : : : ym ) B .. C = (c1j : : : cmj ) B .. C @ . A @ . C A cmj ym = CTj y : Dieser Bedarfsvektor lat sich naturlich auch direkt uber die Rohstobedarfsmatrix C = A B ermitteln: 0 x1 1 x=B A = B T z = B T AT y : @ ... C Also gilt fur den Bedarfsvektor x xj = z1 b1j + z2 b2j + + zp bpj 0 b1j 1 0 B C = (z1 : : : zp ) @ ... A = (b1j : : : bpj ) B @ bpj Hieraus ergibt sich der Bedarf xj an Rohsto Rj wie folgt: z = AT y : 2 z1 1 1 0 y1 1 C C C = (a a : : : amj ) B @ ... C A C A 1j 2j ym den Bedarfsvektor an Zwischenprodukten, so gilt also Bezeichnet 0 B B = (y1 : : : ym ) B B @ 6.2. LINEARE GLEICHUNGSSYSTEME KAPITEL 6. ALGORITHMEN AUF ARRAYS am1 : : : amn xn bm 0 b1 1 0 1 0 x1 1 a11 : : : a1n C C B B . . A=@ A x = @ . A b = B@ ... CA : 4 Es handelt sich hier um ein spezielles lineares Gleichungssystem in nicht ublicher Schreibweise, die daraus resultiert, da die transponierte Matrix C T verwendet wird. Gleichungssystems. A wird Koezientenmatrix des Gleichungssystems genannt, und b heit rechte Seite des mit Ax = b zenschreibweise schreibt sich dieses System kurz als :::::::::::::::::::::::::::::::::: am1 x1 + am2 x2 + : : : + amnxn = bm heit lineares Gleichungssystem mit m Gleichungen in den n Variablen x1 : : : xn . In Matri- Wir betrachten jetzt lineare Gleichungssysteme in allgemeiner Form (und Schreibweise). Das System a11 x1 + a12 x2 + : : : + a1n xn = b1 a21 x1 + a22 x2 + : : : + a2n xn = b2 6.2.3 Das Gausche Eliminationsverfahren mit n Gleichungen und m Unbekannten .4 c11 y1 + c21 y2 + : : : + cm1 ym = r1 c12 y1 + c22 y2 + : : : + cm2 ym = r2 ::: c1n y1 + c2n y2 + : : : + cnm ym = rn der die bezuglich r mogliche Produktion angibt. Die obige Matrizengleichung liefert C T y = r, d. h. das lineare Gleichungssystem ym 0 y1 1 y=B @ ... CA beschrieben werden. Gesucht ist ein Produktionsvektor rn 0 r1 1 r=B @ ... CA Jetzt betrachten wir die umgekehrte Fragestellung. Es sind nur bestimmte Mengen von Rohstoen verfugbar, die durch einen Rohstovektor 92 xn 0 x1 1 x=B @ ... C A 93 a11 x1 + ::: + a1n xn = b1 ::::::::::::::::::::::::::::::::::::::::::::::::::: (ai1 + c ak1 )x1 + : : : + (ain + c akn)xn = bi + c bk ::::::::::::::::::::::::::::::::::::::::::::::::::: a11 x1 + ::: + a1n xn = bn Die Umkehrung folgt entsprechend. bi = ai1x#1 + + ain x#n d. h. x# lost auch die i-te Gleichung von Ax = b. Also ist jede Losung von Ax# = #b auch eine Losung von Ax = b. oder bi + c bk = ai1 x#1 + + ain x#n + c bk Nun ist (ak1 x#1 + + akn x#n ) = bk , da x# die k-te Gleichung von Ax = b lost. Also folgt: (ai1 x#1 + + ain x#n ) + c(ak1 x#1 + + akn x#n) : Die linke Seite ist gleich (ai1 + cak1 )#x1 + + (ain + c akn)#xn = bi + c bk : # = #b. Dann erfullt x# auch alle Gleichungen von Ax = b (auer evtl. der Sei x# Losung von Ax # = #b unverandert sind. i-ten), da diese in Ax Wir zeigen nun, da x# auch die i-te Gleichung von Ax = b erfullt und daher auch eine Losung von Ax = b ist. # = #b ist, gilt (die i-te Gleichung hingeschrieben): Da x# eine Losung von Ax # = #b: Ax Beweis: Addiere das c-fache der k-ten Gleichung zur i-ten. Es entsteht das Gleichungssystem Lemma 6.1 Die Addition bzw. Subtraktion des Vielfachen einer Gleichung zu einer anderen andert den Losungsraum nicht. mit Ax = b heit Losung des linearen Gleichungssystems. Die Menge aller Losungen wird Losungsraum des Gleichungssystem genannt. Wir werden jetzt einen einfachen Algorithmus zur Losung linearer Gleichungssysteme entwickeln, das sogenannte Gau sche Eliminationsverfahren . Dazu benotigen wir zunachst einige Hilfsaussagen uber Umformungen des Gleichungssystems, die den Losungsraum unverandert lassen. Ein Vektor 6.2. LINEARE GLEICHUNGSSYSTEME KAPITEL 6. ALGORITHMEN AUF ARRAYS 1 CC CC CC CC CC CC CC CA 1. m n Beispiel 6.1 x1 + x2 ; x3 = 2 x1 + x3 = ;1 Bevor wir Satz 6.2 beweisen, rechnen wir zunachst einige Beispiele durch. Abbildung 6.2: Dreiecksform der erweiterten Matrix. 0 .. # . b1 BB a#11 . .. .. BB 0 . . a#ij . . BB .. .. . . . . . BB . .. # BB .. a#kk . bk BB . BB 0 0 : : : 0 0 .. #bk+1 B@ . 0 0 ::: 0 0 .. #bm a#ii 6= 0 fur i = 1 : : : k a#ij = 0 fur i > 1 und i < j : nenfalls Zeilen und Spaltenvertauschungen so umgeformt werden, da die erweiterte Matrix . (A .. b) die in Abbildung 6.2 dargestellte Dreiecksform hat, d. h. Satz 6.2 Das Gleichungssystem Ax = b kann durch elementare Umformungen und gegebe- Beweis: Klar. (Umnumerieren der Variablen) andern den Losungsraum nicht. Lemma 6.3 Das Vertauschen von Zeilen (Umnumerieren der Gleichungen) und Spalten Die Operationen \Addition des Vielfachen einer Zeile zu einer anderen" und \Multiplikation einer Zeile mit einer Konstanten" heien auch elementare Umformungen . Beweis: Der Beweis erfolgt analog zu Lemma 6.1. raum nicht. Lemma 6.2 Die Multiplikation einer Gleichung mit einer Konstanten 6= 0 andert den Losungs- 94 x1 + x2 = 1 x1 ; x2 = 0 x1 + 2x2 = 2 95 Die erweiterte Matrix hat bei jedem Eintritt in die Schleife die in Abbildung 6.4 dargestellte Form. Beweis: (von Satz 6.2) Der Beweis erfolgt durch die Angabe eines Algorithmus (im Struktogramm) und den Nachweis seiner Korrektheit. Das Struktogramm ist in Abbildung 6.3 angegeben. Die Korrektheit des Algorithmus basiert auf der folgenden Schleifeninvariante : wodurch die Dreiecksform erreicht ist. 0 0 .. 1=2 0 1 . 1 1 .. 1 C B B C . B @ 0 ;2 ... ;1 C A und die Addition des 12 -fachen der zweiten Zeile zu der dritten Die Subtraktion der ersten Zeile von der zweiten und der dritten ergibt 0 1 . 1 1 .. 1 C B B C . B @ 0 ;2 ... ;1 C A 0 1 .. 1 1 2 .. 2 0 . 1 1 1 .. 1 C B B . C: B @ 1 ;1 ... 0 C A . Die erweiterte Matrix (A .. b) hat die Form 2. m > n wodurch die Dreiecksform erreicht ist. Die Subtraktion der ersten Zeile von der zweiten ergibt 0 1 . @ 1 1 ;1 ... 2 A 0 ;1 2 .. ;3 ;1 1 .. . 2 A: . 1 0 1 .. ;1 0 @1 1 . Die erweiterte Matrix (A .. b) hat die Form 6.2. LINEARE GLEICHUNGSSYSTEME 96 hhhh h hhhh h i minfn mg hhhh h hhhh hhhh hhhh . 6= 0 ii a~ A~ ~b Abbildung 6.4: Die Schleifeninvariante im Beweis zu Satz 6.2. 0 6= 0 6= 0. . Abbildung 6.3: Struktogramm des Gau-Eliminationsverfahrens. . Lasse von (A~ .. ~b) die Zeile und Spalte mit Index i ; 1 weg. . Bezeichne die entstehende Matrix wieder mit (A~ .. ~b). true hhhh i := i + 1 Addiere zu allen Zeilen mit a~li 6= 0 ein geeignetes Vielfaches, so da a~li + c a~ii = 0 gilt. fDann ist c = ; a~a~iili . Dies nennt man Pivotoperation oder Pivotisierung .g Andere A~ durch Zeilen- und Spaltenpermutationen bis a~ii 6= 0 gilt. fDas gewahlte Element heit Pivotelement in Zeile i.g . . Setze (A~ .. ~b) := (A .. b) und i := 1 A~ 6= 0 (Nullmatrix) and i minfn mg KAPITEL 6. ALGORITHMEN AUF ARRAYS 97 Beispiel 6.2 (Fortsetzung von Beispiel 6.1) Genauer werden diese Zusammenhange in der Linearen Algebra in einem allgemeineren Rahmen untersucht. { Der Losungsraum des zugehorigen homogenen Systems Ax = 0 ist ein (n ; k)-dimensionaler Vektorraum L. { Man erhalt alle Losungen x des inhomogenen Systems Ax = b als Kombination x = x~+y, wobei x~ irgendeine fest gewahlte Losung von Ax = b ist, und y alle Losungen von Ax = 0 durchlauft. Man nennt k auch den Rang der Matrix A. Aussage a) bedeutet dann, da Ax = b genau . dann losbar ist, wenn der Rang von A gleich dem Rang der erweiterten Matrix (A .. b) ist. Aussage b) und c) bedeuten, da der Losungsraum ein (n ; k)-dimensionaler aner Raum ist. Dies bedeutet grob gesagt folgendes: c) Dies sind bereits alle Losungen von Ax = b. durch sukzessives Ausrechnen von xk xk;1 : : : x1 \von unten nach oben" erhalt. b) In diesem Falle erhalt man alle Losungen, indem man (fur k < n) xk+1 xk+2 : : : xn beliebig wahlt und die anderen Variablen x1 : : : xk aus der Dreiecksform a#11 x1 + a#12 x2 + + a#1k xk = #b1 ; (#a1k+1 xk+1 + + a#1n xn) a#22 x2 + + a#2k xk = #b2 ; (#a2k+1 xk+1 + + a#2n xn) ... a#kk xk = #bk ; (#akk+1xk+1 + + a#knxn ) a) Ax = b hat genau dann (mindestens) eine Losung wenn #bi = 0 fur alle i > k (falls k < m). Satz 6.3 (Losungskriterien fur lineare Gleichungssysteme) Sei Ax = b gegeben, und . seien (A# .. #b) und k die durch das Gau sche Eliminationsverfahren gelieferten Gro en. Dann gilt: Der obige Algorithmus wird auch als Gau sches Eliminationsverfahren zur Losung linearer Gleichungssysteme bezeichnet. Seine Nutzung zur Losung linearer Gleichungssysteme beruht auf dem folgenden Satz: Die folgt direkt aus der Tatsache, da die Addition in den ersten Stellen 1 : : : i ; 1 nur 0 + 0 ergibt und die i-te Spalte beim i-ten Durchlauf unterhalb von Zeile i zu Null gemacht wird. Also verlat man die Schleife mit k = minfn mg oder k < minfn mg und A~ = 0. 6.2. LINEARE GLEICHUNGSSYSTEME KAPITEL 6. ALGORITHMEN AUF ARRAYS Sind xk xk;1 : : : x`+1 bereits berechnet, so ergibt sich x` (wegen a#`` 6= 0) als x` = a#1 #b` ; (#a``+1x`+1 + + a#`k xk ) ; d`] : `` 1. Die Bedingung #bi = 0 fur i > k ist notwendig fur die Losbarkeit : Der Gau-Algorithmus lat wegen Lemma 6.1{6.3 den Losungsraum unverandert. Ist #bi 6= 0 fur ein i > k, so lautet die i-te Gleichung von Ax # = #b 0 x1 + + 0 xn = bi 6= 0 Dies ist fur kein x erfullt. 2. Die Bedingung #bi = 0 fur alle i > k ist auch hinreichend fur die Losbarkeit : Man kann aufgrund der Bedingung b) eine Losung wie folgt ausrechnen: Fur beliebig, aber fest gewahlte Werte von xk+1 : : : xn ist d` := a#`k+1 xk+1 + a#`k+2xk+2 + a#`nxn ` = 1 : : : m, eine Konstante. Dann ergibt sich (wegen a#kk 6= 0) xk = a#1 #bk ; (# |akk+1xk+1 +{z + a#knx#n})] : kk zu a) Konstante dk Raum. 2. Das Gleichungssystem ist nicht losbar, da k = 2 < m und #b3 = 21 6= 0. c Hieraus ergibt sich x2 = 3 + 2c, und dann aus der ersten Gleichung x1 = ;x2 + 2 + c = ;(3 + 2c) + 2 + c = ;1 ; c : 0 1 ;1 ; c B Also ist der Losungsraum f@ 3 + 2c C A jc 2 IR1 beliebig g ein 1-dimensionaler aner : . 1 1 ;1 .. 2 .. 0 ;1 2 . ;3 Man kann x3 beliebig wahlen (z. B. x3 = c) und erhalt x1 + x2 = 2 + c ;x2 = ;3 ; 2c ::::::::::::::::::: 1. Das Gleichungssystem ist losbar, da k = 2 = m. Man erhalt x1 x2 x3 ... b Beweis: (von Satz 6.3) 98 99 =:b =:c 5 Dies bedeutet, da nur 3 Nachkommastellen in der normalisierten Gleitkommadarstellung (vgl. Kapitel 12) einer Zahl dargestellt werden konnen. Die darstellbaren Zahlen haben also die Form 0 x1 x2 x3 10e mit xi 2 f0 1 : : : 9g x1 6= 0 und e ganzzahlig. Bei mehr Nachkommastellen wird entsprechend gerundet. 0 000100x1 + x2 = 1 ;10000x2 = ;10000 Das transformierte Gleichungssystem lautet dann a = 1 ; 10000 0 000100 = 1 ; 1 = 0 b = 1 ; 10000 = ;9999 = ;0 9999 104 ;0 100 105 = ;10000 c = 2 ; 10000 = ;9998 = ;0 9998 104 ;0 100 105 = ;10000 In 3-stelliger Arithmetik ergeben sich folgende Werte fur a b c ( steht fur das Runden): =:a x1 (1 | ; {z10000)} = 2| ; {z10000} | ; 10000{z 0 000100)} +x2 (1 Die Gau Elimination ergibt (Zeile 1 mit 10000 multiplizieren und von Zeile 2 abziehen): soll mit 3-stelliger Arithmetik5 gelost werden. 0 000100x1 + x2 = 1 x1 + x2 = 2 Beispiel 6.3 (Ungunstige Wahl des Pivotelementes) Das lineare Gleichungssystem Im Gau-Algorithmus wurde die Auswahl der Pivotelemente oen|d. h. beliebig|gelassen. Aus numerischen Grunden sind jedoch bestimmte Pivotelemente vorzuziehen. 6.2.4 Wahl des Pivotelements Also folgt a) und auch b). zu c) Zeige: Jede Losung la t sich gema b) gewinnen . Sei 0 x#1 1 x# = B @ ... C A x#n eine Losung. Wahle dann in b) xk+1 := x#k+1 : : : xn := x#n . Dann sind (wie die Formel oben fur xl zeigt) x1 x2 : : : xk eindeutig bestimmt durch die Wahl von xj = x#j (j = k +1 : : : n). Also mu xi = x#i fur i = 1 : : : k sein. 6.2. LINEARE GLEICHUNGSSYSTEME x1 = 1 00010 x2 = 0 99990 =:b =:c Permutiere in Schritt i diejenige Zeile (unter den Zeilen l = i : : : m) mit grotem absolutem Wert jali j an die i-te Stelle. woraus sich die angenaherte Losung x1 = x2 = 1 ergibt, die die exakte Losung fur eine 3-stellige Arithmetik sehr gut approximiert. Dies Beispiel zeigt, da man das Gausche Eliminationsverfahren nicht einfach \naiv" anwenden darf, sondern sich Gedanken uber die numerische Genauigkeit machen mu. Dies geschieht durch die Suche nach geeigneten Pivotelementen, die sogenannte Pivotstrategie . Es gibt zwei Standard-Pivotstrategien , die partielle und die totale Pivotsuche. Bei der partiellen Pivotsuche sucht man das Pivotelement in der jeweiligen Spalte nach folgender Regel (vgl. Abbildung 6.5): x1 + x2 = 2 x2 = 1 Das transformierte Gleichungssystem lautet dann a = 0 000100 ; 0 000100 = 0 b = 1 ; 0 000100 = 0 9999 = 0 9999 100 0 100 101 = 1 c = 1 ; 0 000200 = 0 9998 = ;0 9998 100 0 100 101 = 1 mit folgenden Werten fur a b c in 3-stelliger Arithmetik: =:a x1 (0 | ; 0{z000100)} = |1 ; 0{z000200} | 000100 {z; 0 000100)} +x2 (1 Die Gausche Elimination ergibt x1 + x2 = 2 0 000100x1 + x2 = 1 was bedeutet, das die angenaherte Losung fur x1 auf Basis der 3-stelligen Arithmetik unvertretbar schlecht ist. Die Ursache liegt darin, da a11 = 0 000100 ein zu kleines Pivotelement im Verhaltnis zu a21 = 1 ist. Will man dies vermeiden, so kann man (z. B. durch Zeilenvertauschung) nach einem groeren Pivotelement suchen. Im Beispiel ergibt die Zeilenvertauschung Die exakte Losung ist jedoch KAPITEL 6. ALGORITHMEN AUF ARRAYS x1 = 0 000 x2 = 1 000 : Hieraus ergibt sich die angenaherte Losung 100 ... Vertauschung jali j = max jaji j j =i:::m 101 (m ; i) = (m ; 1) + (m ; 2) + + 1 m X Spaltenpermutation Zeilenpermutation groter Absolutbetrag i=1 (n ; 1)2 + (n ; 2)2 + + 1 = n n ;3 1 (n ; 21 ) (m ; i)(n ; i) = (m ; 1)(n ; 1) + (m ; 2)(n ; 2) + + 1 Abbildung 6.6: Schema der totalen Pivotsuche. Hierdurch ergibt sich in Schritt i der Gau Elimination ein zusatzlicher Suchaufwand von (m ; i)(n ; i) Vergleichen, also insgesamt uber alle Schritte i i = m(m2; 1) Vergleiche, d. h. quadratisch in m viele. Bei der totalen Pivotsuche sucht man das Pivotelement in der gesamten verbleibenden Restmatrix nach folgender Regel (vgl. Abbildung 6.6): Permutiere in Schritt i die verbleibenden Zeilen und Spalten bis an Stelle (i i) der Eintrag mit dem groten Absolutbetrag steht. i=1 m X Hierdurch ergibt sich in Schritt i der Gau Elimination ein zusatzlicher Suchaufwand von m ; i Vergleichen, also insgesamt uber alle Schritte Abbildung 6.5: Schema der partiellen Pivotsuche. i l i 6.2. LINEARE GLEICHUNGSSYSTEME KAPITEL 6. ALGORITHMEN AUF ARRAYS n // Zeilenindex // Spaltenindex { gauss.cc // matrix of coefficients // right-hand side // variables // number of rows // number of columns // precision for "testing for 0" double absValue, max, q, pivot Boolean solvable // TRUE if there is a solution Boolean exitLoop // TRUE if main loop should be exited int row"m] // stores row permutation int col"n] // stores column permutation // a"row"i]]"col"m]] denotes curent matrix entry after // row and column permutations double a"m]"n] double b"m] double x"n] const int m = ... const int n = ... const double eps = 0.000001 Programm 6.3 const double eps=0.000001 int rank // kontrolliert die Rechengenauigkeit // gibt den Rang von a an // Variable // rechte Seite An weiteren Groen sind noch wichtig: double x" ] double b"m] n die Koezientenmatrix deniert, so ist a"row"i]]"col"j]] also das \aktuelle" Element an der Stelle (i j ) entsprechend sind x"col"j]] und b"row"i]] die aktuellen Elemente fur double a"m]" ] Ist durch n int row"m] int col" ] Wir geben nun ein C++ Programmfragment fur die Gau-Elimination mit totaler Pivotsuche an. Dabei werden Vertauschungen von Zeilen und Spalten werden nicht tatsachlich durchgefuhrt (dies wurde zu viel Rechenzeit erfordern), sondern man merkt sich die Indexvertauschungen in zwei Hilfsarrays, namlich 6.2.5 Ein C++-Programm fur die Gau-Elimination Vergleiche, d. h. kubisch in m viele. In der Numerischen Mathematik werden weitere Methoden zum Erreichen numerischer Genauigkeit und zum Abschatzen des maximalen Fehlers untersucht. 102 rank pivotRow, i, j, k index // rank of matrix a pivotCol // row and column index of pivot element // counters // aux. variable // permute rows and colums // only necessary if max > if ( max > eps ) { index = row"k] row"k] index = col"k] col"k] }//endif = row"pivotRow] row"pivotRow] = index = col"pivotCol] col"pivotCol] = index to get this entry onto the diagonal eps // compare absValue with value max found so far if ( max < absValue ) { // remember new value and position max = absValue pivotRow = i pivotCol = j }//endif }//endfor }//endfor // compute absolute value of current entry in temp absValue = a"row"i]]"col"j]] if ( absValue < 0 ) absValue = - absValue // total pivot search for entry with largest absolut value max // in remaining matrix, store position in pivotRow, pivotCol max = 0 for ( i = k i < m i++ ) { for ( j = k j < n j++ ) { // main loop, transformation to triangle form exitLoop = FALSE k = -1 // denotes current position on diagonal while ( ! exitLoop ) { k++ // initialize row"m] and col"n], no permutations yet for ( i = 0 i < m i++ ) { row"i] = i }//endfor for ( j = 0 j < n j++ ) { col"j] = j }//endfor // add code for reading the data into a and b int int int int 6.2. LINEARE GLEICHUNGSSYSTEME 103 104 // compute a solution // check for solvability solvable = TRUE for ( i = rank + 1 i < m i++ ) { if ( abs(b"row"i]]) > eps ){ // uses abs() from math.h solvable = FALSE // not solvable break // no need to check remaining b-values }//endif }//endfor // modify right-hand-side b"row"i]] = b"row"i]] - b"row"k]] * q }//endfor }//endif }//endwhile // modify entries a"i,j], i > k fixed, j = k+1...n-1 for (j = k + 1 j < n j++ ) { a"row"i]]"col"j]] = a"row"i]]"col"j]] - a"row"k]]"col"j]] * q }//endfor // modify entry a"i,k], i > k a"row"i]]"col"k]] = 0 // compute factor q = a"row"i]]"col"k]] / (double) pivot // pivoting // only if max > eps and k < m - 1 if ( max > eps && k < m - 1 ){ pivot = a"row"k]]"col"k]] for ( i = k + 1 i < m i++ ) { test conditions for exiting loop after this iteration reasons are: max < eps : numerically 0 in remaining matrix k == m - 1 : no more rows k == n - 1 : no more colums if max < eps then rank of matrix is k - 1 ( max < eps || k == m - 1 || k == n - 1 ) { exitLoop = TRUE if ( max < eps ) rank = k - 1 // decrease k to denote last non-zero else rank = k // diagonal entry }//endif // // // // // if KAPITEL 6. ALGORITHMEN AUF ARRAYS } // add code for output }//endif // change values below eps to 0 for ( j = 0 j < n j++ ){ absValue = x"j] if ( absValue < 0 ) absValue = - absValue if ( absValue < eps ) x"j] = 0 }//endfor // compute remaining x"i] backwards for (i = rank - 1 i >= 0 i-- ) { x"col"i]] = b"row"i]] for (j = i + 1 j <= rank j++ ) { x"col"i]] = x"col"i]] - a"row"i]]"col"j]] * x"col"j]] }//endfor x"col"i]] = x"col"i]] / (double) a"row"i]]"col"i]] }//endfor // compute x"rank] x"col"rank]] = b"row"rank]] / (double) a"row"rank]]"col"rank]] // set x"rank+1] = ... = x"n-1] = 0 if ( rank < n - 1 ) { for (j = rank + 1 j < n j++ ) { x"col"j]] = 0 }//endfor }//endif if ( solvable ) { 6.2. LINEARE GLEICHUNGSSYSTEME 105 KAPITEL 6. ALGORITHMEN AUF ARRAYS 3 5 4 Verbindungen zwischen Orten in einem Netzwerk (Straennetz, U-Bahnnetz,: : :), Hierarchien, Syntax- und Fludiagrammen, Arbeitsablaufen, und vielem anderen mehr. In solchen Anwendungen haben die Kanten (i j ) des Graphen meist eine Bewertung , die je nach Anwendung als Lange oder Entfernung von i nach j (Straennetz), Kosten (Arbeitsablauf) oder ahnlich interpretiert wird. Ein solcher bewerteter Graph ist beschreibbar durch eine Matrix A = (aij )ij =1:::n mit 8 > < Bewertung (Lange) von Kante (i j ) falls (i j ) 2 E aij = > 0 falls i = j : 1 sonst. { { { { Graphen haben viele Anwendungen. Sie eignen sich zur Beschreibung von Abbildung 6.7: Zeichnung des Graphen aus Beispiel 6.7. 1 2 ist ein Graph mit 5 Knoten und 8 gerichteten Kanten. Er ist in Abbildung 6.7 dargestellt. E = f(1 2) (1 3) (2 3) (2 4) (3 5) (4 1) (4 3) (5 4)g Beispiel 6.4 G = (V E ) mit V = f1 2 3 4 5g und { Der Menge V der Knoten . Bei uns ist meist V = f1 2 : : : ng oder (in Implementationen) V = f0 1 : : : n ; 1g. { Der Menge E der (gerichteten) Kanten oder Bogen zwischen Knoten. Bei uns ist E V V n f(i i)ji 2 V g e = (i j ) bedeutet, da die Kante e vom Knoten i zum Knoten j gerichtet ist. Ein gerichteter Graph oder Digraph (directed graph) ist ein Paar G = (V E ) bestehend aus 6.3.1 Graphen und Wege 6.3 Kurzeste Wege in gerichteten Graphen 106 107 1 5 1 2 1 1 2 3 1 0 1 1 0 0 1 1 1 1 1 0 0 0 ;1 B 1 0 B1 1 ) A=B B @ ;1 1 Abbildung 6.8: Bewerteter Graph aus Beispiel 6.8. 3 0 2 4 1 C C C C A 1 - 2 - 3 - ` - k 1 - k i p p p i i i pp i pp pp p i ; pppppppp i = i1 (i1 i2 ) i2 (i2 i3 ) : : : ik;1 (ik;1 ik ) ik = j : r r r r Diese Matrix heit Adjazenzmatrix des Graphen G. In ihr ist durch Zugri auf aij in konstanter Zeit (d. h. unabhangig von der Groe des Graphen) feststellbar, ob i und j durch eine Kante verbunden sind. Dafur benotigt man jedoch quadratischen Speicherplatz (n n Matrix). Es sind auch andere Datenstrukturen zur Speicherung von Graphen moglich, z. B. fur jeden Knoten i eine Liste der Knoten j , die mit i durch eine Kante (i j ) verbunden sind. Diese benotigen weniger Platz, erfordern jedoch mehr Zeit, um festzustellen, ob (i j ) 2 E ist oder nicht, da Listen nur sequentiellen Zugri erlauben. Bei manchen Anwendungen spielen die Richtungen der Kanten keine Rolle. In diesem Fall ist mit einer Kante (i j ) auch die Kante (j i) im Graphen, und beide haben dieselbe Bewertung. In der Darstellung des Graphen wird dann statt der beiden gerichteten Kanten eine ungerichstatt R . Man spricht dann von ungerichteten Kanten. Ein Graph tete gezeichnet, also mit nur ungerichteten Kanten I heit ungerichteter Graph . Im folgenden werden wir die Kantenbewertung als Entfernung interpretieren, bzgl. der wir kurzeste Wege zwischen je zwei Knoten berechnen wollen. Ein Weg von i nach j ist eine endliche Folge von Knoten und Kanten Falls nur der Graph G beschrieben werden soll (d. h. ohne Kantenbewertungen), so reicht auch die Matrix ( i j ) 2 E A = (aij ) mit aij = 10 (sonst. 1 3 ;1 2 ;1 die in Abbildung 6.8 links neben den Kanten angegebenen Kantenbewertungen fest. Die zugehorige Matrix A ist rechts daneben angegeben. Beispiel 6.5 (Fortsetzung von Beispiel 6.4) Fur den Graphen aus Beispiel 6.4 legen wir Wir nennen A die Bewertungsmatrix des Graphen G. Zur Abspeicherung von A im Rechner wird statt 1 eine sehr groe Zahl M (z. B. M := n max(ij )2E jaij j + 1) verwendet. 6.3. KURZESTE WEGE IN GERICHTETEN GRAPHEN KAPITEL 6. ALGORITHMEN AUF ARRAYS Abbildung 6.9: Ausschnitt aus dem BVG-Netz. Berliner Str. Mockernbrucke Yorckstr. (Grogorschenstr.) S1 Die Bewertungsmatrix A dieses Graphen ist in Tabelle 6.3 angegeben. Als kurzester Weg von U7 U9 Bus 119 MA-Gebaude Bismarckstr. Fuwege S-Tiergarten ErnstReuter-Pl. Zoologischer Garten Wittenbergpl. U1 S-Friedrichstr. Als erste Anwendung betrachten wir den Ausschnitt aus dem BVG-Netz von Berlin in Abbildung 6.9. Dieses Netz wird in Abbildung 6.10 als bewerteter ungerichteter Graph wiedergegeben, da jede Kante in beiden Richtungen \bereist" werden kann, und die Reisezeit fur beide Richtungen gleich ist. Die Bewertung einer Kante ist fur beide Richtungen gleich und entspricht der mittleren Reisezeit in Minuten (ohne Umsteigezeiten und Wartezeiten). 6.3.2 Zwei konkrete Anwendungen Dabei sind Knotenwiederholungen zugelassen, und auch der triviale Weg, der nur aus einem Knoten besteht (man \geht" dann von i nach i indem man in i bleibt). Ein Weg heit elementar , wenn keine Knotenwiederholungen auftreten. Ein Weg von i nach j heit Zykel , falls i = j gilt. Die Lange eines Weges ist die Summe der Entfernungen der Kanten auf dem Weg. Die Lange des trivialen Weges ist 0. Ein Weg von i nach j heit kurzester Weg , falls alle anderen Wege von i nach j eine mindestens ebenso groe Lange haben. Unser Ziel ist nun die Berechnung der kurzesten Weglangen uij zwischen je 2 Knoten i j (mit uij = 1 falls kein Weg von i nach j existiert) sowie zugehoriger kurzester Wege. 108 Bi 2 7 Be 5 3 Zoo 2 Ti Wi 5 15 7 7 Abbildung 6.10: Der Graph fur das BVG-Netz. MA 10 5 ER 11 2 6.3. KURZESTE WEGE IN GERICHTETEN GRAPHEN Yo 2 Mo 10 Fr 109 KAPITEL 6. ALGORITHMEN AUF ARRAYS Bi ER MA Ti Fr Zoo Be Yo Mo Wi 0 2 1 1 1 1 7 1 1 1 2 0 5 1 1 2 1 1 1 1 1 5 0 10 1 11 1 1 1 1 1 1 10 0 7 3 1 1 1 1 1 1 1 7 0 1 1 10 1 1 1 2 11 3 1 0 5 1 1 2 7 1 1 1 1 5 0 5 1 1 1 1 1 1 10 1 5 0 2 15 1 1 1 1 1 1 1 2 0 7 1 1 1 1 1 2 1 15 7 0 2 Be 5 10 5 10 Zoo 3 2 Wi Bei Entfernungen als Kantenbewertungen treten nur nicht-negative Zahlen auf. Oft sind Abbildung 6.11: Zoologischer Garten mit Umsteige- und Wartezeiten. ER Ti Umsteige- und Wartezeiten konnen berucksichtigt werden, indem man die Stationen \aufblaht" zu mehreren Knoten, die den verschiedenen Linien entsprechen, und den Kanten dazwischen die Umsteige- und Wartezeiten zuordnet bzw. die Kantenbewertungen um diese Werte vergroert. Dies ist in Abbildung 6.11 fur die Station Zoologischer Garten ausgefuhrt. Bi ER MA Ti Fr Zoo Be Yo Mo Wi Tabelle 6.3: Bewertungsmatrix des Graphen zum BVG-Netz. der Yorkstrae zum Mathematikgebaude ergibt sich Yo ;! Mo ;! Wi ;! Zoo ;! ER ;! MA mit der Lange 2 + 7 + 2 + 2 + 5 = 18 Minuten. 110 111 100Yen Tokio 0,81 1,25 0,63 1,86 1,61 0,42 0,67 0,54 $ New York 1,50 $ London In dieser Anwendung mochte man fur gegebene \Heimatwahrung" i und Zielwahrung j eine Umtauschfolge (also einen Weg) bestimmen, so da das Produkt der Bewertungen entlang des Weges (also der Preis fur eine Einheit der Zielwahrung) moglichst klein wird. Die Problem lat sich folgendermaen auf ein Kurzeste-Wege Problem transformieren. Ersetze die Kantenbewertung aij der Kante (i j ) durch log aij . Dann ist (nach den Logarithmengesetzen) das Produkt entlang eines Weges gleich der Summe der Logarithmen der Kanten. Man erhalt also ein Kurzestes Wege Problem mit a#ij := log aij als Bewertung der Kanten. Dabei konnen negative Bewertungen a#ij auftreten, namlich genau dann, wenn aij < 1 ist. Als Konsequenz negativer Kantenbewertungen konnen auch Zykel negativer Lange auftreten. Diese machen, wie wir sehen werden, bei der Berechnung kurzester Wege Schwierigkeiten. Beim Devisentausch haben sie jedoch eine besondere Bedeutung: Sie entsprechen einem gewinnbringenden Zykel, auf dem man durch Umtausch seinen Einsatz vermehren kann. Im Graphen aus Abbildung 6.12 existiert ein solcher Zykel, vgl. Abbildung 6.13. Der Tausch entlang des Zykels ermoglicht den Kauf einer DM fur 2 39 0 77 0 54 = 0 993762 DM, also eine Vermehrung des eingesetzten Kapitals um den Faktor 1=0 993762 ' 1 006277. Abbildung 6.12: Ein Graph fur den Devisentausch. 1,30 0,77 DM Frankfurt 2,39 jedoch auch negative Kantenbewertungen sinnvoll, wie die folgende Anwendung des Devisentausches zeigt. Gegeben ist ein gerichteter Graph, bei dem die Knoten Devisenborsen darstellen, und eine Kante (i j ) dem Tausch von Wahrung i in die \Zielwahrung" j entspricht. Die Bewertung der Kante (i j ) gibt den Preis (Kurs) fur eine Einheit der Zielwahrung j bzgl. der Wahrung i an. Daher haben beide Kantenrichtungen unterschiedliche Werte. Ein Beispiel ist in Abbildung 6.12 angegeben (Kurse vom November 1992). 6.3. KURZESTE WEGE IN GERICHTETEN GRAPHEN KAPITEL 6. ALGORITHMEN AUF ARRAYS 0,54 $ London Allgemein gilt: (2) (3) (4) (5) u(1) 15 = 1 u15 = 3 u15 = 2 u15 = 2 u15 = 2 : Diese Groe ist wohldeniert, da es fur festes m nur endlich viele Wege mit hochstens m Kanten zwischen i und j gibt, und somit unter diesen endlich vielen auch einen kurzesten. In Beispiel 6.4 ergeben sich fur i = 1 und j = 5 folgende Werte: 8 > < Lange eines kurzesten Weges von i nach j ( m ) uij := > mit hochstens m Kanten, falls dieser existiert, : 1 falls kein solcher Weg existiert. Wir werden jetzt eine Methode kennenlernen, mit der man die kurzesten Weglangen (und auch die kurzesten Wege selbst) zwischen je 2 Knoten iterativ berechnen kann (sofern sie existieren). Die Grundidee basiert darauf, die Anzahl der Kanten auf den betrachteten Wegen in jeder Iteration um 1 zu vergroern. Dazu denieren wir fur i j 2 V und m 2 IN die Groe 6.3.3 Die Bellman Gleichungen Abbildung 6.13: Ein gewinnbringender Zykel beim Devisentausch. 100Yen Tokio 0,77 DM Frankfurt 2,39 Solche Zykel konnen tatsachlich bei Devisengeschaften vorkommen, allerdings nur kurzfristig, da sich Kurse dauernd aufgrund von Angebot und Nachfrage andern. Die Rechner der Devisenhandler bemerken solche Zykel sofort und ordern entsprechend bis die steigende Nachfrage durch das Steigen der Preise den Faktor wieder groer als 1 werden lat. 112 113 (6.1) - 1 - 2 - - - i i p p p ` j i - 1 - 2 - - i p p p i i ` da ` bei der Minimumsbildung vorkommt. Also ist Gleichung (6.1) erfullt. min u(m) + akj ] u(ijm+1) = u(i`m) + a`j k=1 :::n ik von W ein kurzester Weg von i nach ` mit hochstens m Kanten denn gabe es einen kurzeren, so wurde man durch Anhangen der Kante (` j ) an W 0 einen kurzeren Weg mit hochstens m +1 Kanten von i nach j erhalten, im Widerspruch zur Annahme, da W ein kurzester Weg von i nach j ist. Also ist die Lange von W 0 gleich u(i`m) und somit die Lange von W gleich W 0 := ein kurzester Weg von i nach j mit hochstens m + 1 Kanten. Dann hat W die Lange u(ijm+1) . Das Anfangsstuck W := Diese Ungleichung ist trivialerweise erfullt, falls u(ijm+1) = 1 ist, also kein Weg von i nach j mit hochstens m + 1 Kanten existiert. Also nehmen wir an, da ein solcher Weg existiert. Sei dann u(ijm+1) k=1 min u(m) + akj ] : :::n ik Beweis: Fur m = 1 kommen nur Wege mit hochstens einer Kante in Frage. Fur i 6= j also gerade die Lange der Kante (i j ), falls diese existiert, bzw. 1 andernfalls. Fur i = j kommt nur der triviale Weg der Lange 0 in Frage. Also gilt in allen Fallen u(1) ij = aij . ( m +1) Betrachte nun uij . Wir zeigen zunachst (m+1) = min u(m) + a ] fur m 1 : u(1) kj ij = aij uij k=1:::n ik Satz 6.4 (Bellman Gleichungen) Die u(ijm) erfullen die folgenden Rekursionsgleichungen (die sog. Bellman Gleichungen). Beweis: Beim Ubergang von m auf m + 1 vergroert sich die Menge der Wege unter denen man die kurzeste Menge ermittelt. Daher gilt u(ijm) u(ijm+1) . (2) (m) (m+1) : u(1) ij uij : : : uij uij Lemma 6.4 Fur festes i und j und m 2ist 6.3. KURZESTE WEGE IN GERICHTETEN GRAPHEN - 1 - 2 - - j j p p p r i - - Beispiel 6.6 (Der Einu negativer Zykel) Betrachte den Graph aus Abbildung 6.14. Um die kurzeste Weglange von 1 nach n zu berechnen, mussen m := n ; 1 Kanten berucksichtigt werden. Dann gibt zwar u(1mn ) die richtige kurzeste Weglange an, aber die Werte u(1mj ) sind fur j = 2 : : : n ; 2 ungleich der kurzesten Lange eines elementaren Weges von 1 nach j (die 0 betragt). Zum Beispiel ergibt sich fur j = 4 (3) = u(4) = 0 u(5) = u(6) = ;1 u(7) = u(8) = ;2 : : : u(n;1) = ; n ; 3 : u14 14 14 14 14 14 14 2 Die Ursache ist naturlich der negative Zykel 2 ;! 3 ;! 2 der Lange ;1. Es stellt sich nun die Frage, wie gro man die Anzahl m der Kanten maximal wahlen mu, um kurzeste Weglangen zu berechnen. Hier gibt es Schwierigkeiten falls der Graph Zykel negativer Lange (kurz: negative Zykel ) enthalt. In diesem Fall konnen die Wege mit hochstens m Kanten einen solchen Zykel (sogar mehrfach) enthalten, so da das Ergebnis u(ijm) keinem elementaren Weg (also einem ohne Knotenwiederholungen) mehr entspricht, wie das folgende Beispiel zeigt. 6.3.4 Der Einu negativer Zykel ein Weg von i nach j mit hochstens m + 1 Kanten und seine Lange ist u(irm) + arj . Diese kann naturlich nicht kleiner als die kurzeste Lange eines Weges von i nach j mit hochstens m + 1 Kanten sein, d. h. u(irm) + arj u(ijm+1) : Also gilt Gleichung (6.2). Aus den Gleichungen (6.1) und (6.2) folgt die Behauptung. - 1 - 2 - p p p j j r j i ein kurzester Weg von i nach r mit hochstens m Kanten. Dieser existiert, da u(irm) + arj < 1. Dann ist auch arj < 1, und somit existiert im Graphen die Kante (r j ). Also ist Sei dann Diese Gleichung ist wieder trivialerweise erfullt, falls die rechte Seite 1 ist. Ist sie endlich, so sei r der Index, fur den das Minimum angenommen werde, d. h. min u(m) + akj ] = u(irm) + arj : k=1:::n ik (6.2) KAPITEL 6. ALGORITHMEN AUF ARRAYS u(ijm+1) k=1 min u(m) + akj ] : :::n ik Jetzt zeigen wir umgekehrt die Gleichung 114 ; - - - 0 - n 115 Satz 6.5 lat sich direkt in den folgenden Algorithmus umsetzen, der nach seinen Entdeckern Bellman-Ford Algorithmus genannt wird. Wir geben ihn direkt als C++ Fragment an. 6.3.5 Der Bellman-Ford Algorithmus werden kann. Elementare Wege konnen bei n Knoten hochstens n ; 1 Kanten haben. Die Monotonieeigenschaft der u(ijm) aus Lemma 6.4 ergibt dann die Behauptung. Beweis: Da G keine negativen Zykel hat, folgt aus Lemma 6.5, da u(ijm) nie kleiner als uij (2) (n;1) = u : u(1) ij ij uij : : : uij Die Bellman Gleichungen berechnen also in n ; 2 Iterationen die kurzesten Weglangen uij . Satz 6.5 Hat G keine negativen Zykel, so ist fur festes i und j Dann gilt (vgl. Lemma 6.4): 8 > < Lange eines kurzesten elementaren Weges von i nach j uij := > falls kein Weg von i nach j existiert, : 1 falls ein Weg von i nach j existiert. Wir denieren nun Beweis: Da G keine negativen Zykel enthalt, kann die Wegnahme von Zykel aus einem Weg die Weglange hochstens verkurzen. Also kann man sich bei der Suche nach kurzesten Wegen auf elementare Wege beschranken. Da es nur endlich viele elementare Wege von i nach j gibt, existiert hierunter auch ein kurzester. ein kurzester Weg von i nach j , der elementar ist. Lemma 6.5 Hat G keinen negativen Zykel, und gibt es einen Weg von i nach j , so existiert Wir untersuchen daher zunachst den Fall, da der gegebene Graph G keine negativen Zykel hat. Dies lat immer noch negative Kantenbewertungen zu, nur durfen sich diese nicht entlang eines Zykels zu einer negativen Zahl summieren. Abbildung 6.14: Ein Graph mit einem Zykel negativer Lange. - ? 1 ppp 2 3 4 1 0 0 0 0 6.3. KURZESTE WEGE IN GERICHTETEN GRAPHEN KAPITEL 6. ALGORITHMEN AUF ARRAYS Ist man nur an den uij interessiert, nicht jedoch an den u(ijm) (d. h. den kurzesten Weglangen mit hochstens m Kanten), so kann man wegen der Monotonieeigenschaft der u(ijm) in U"i,j] n n n Vergleiche und hochstens (n ; 2) n n 1 + n] + 1] Zuweisungen, d. h. seine Laufzeit liegt in der Gro enordnung von n4 Operationen. Satz 6.6 Der Bellman-Ford Algorithmus in der Version von Programm 6.4 benotigt (n ; 2) Die Korrektheit des Bellman-Ford Algorithmus folgt direkt aus Satz 6.5 und den Bellman Gleichungen (Satz 6.4). Fur den Aufwand uberlegt man sich aus der Schachtelung der Schleifen: // add routines for output // main loop, calculates Bellman equations for ( m = 1 m < n m++ ){ for ( i = 0 i < n i++ ){ for ( j = 0 j < n j++ ){ // calculate min {u"i,k] + a"k,j]} in Bellman equations // and store them in temp temp"j] = U"i]"j] for ( k = 0 k < n k++ ){ if ( temp"j] > U"i]"k] + A"k]"j] ){ temp"j] = U"i]"k] + A"k]"j] }//endif }//endfor k }//endfor j // store temp"0]...temp"n-1] in U"i,1]...U"i,n-1] for ( j = 0 j < n j++ ) { U"i]"j] = temp"j] }//endfor }//endfor i }//endfor m bellman_ford.cc // assumes that A an U have been initialized Programm 6.4 und initialisieren A und U zu A"i]"j]= aij und U"i]"j]= aij , und wenden dann die BellmanGleichungen an. Dabei speichert U die u(ijm) ab und dient temp zur Berechnung der u(ijm+1) fur festes i. typedef int Matrix"n]"n] Matrix A,U int temp"n] Wir denieren zunachst 116 117 kurzesten Weglangen mit dem Bellman-Ford Algorithmus berechnet werden (in der Version von Programm 6.4). Dabei bezeichnet U (m) die Matrix (u(ijm) )ij =1:::n und U die Matrix (uij )ij =1:::n . Gema Satz 6.4 ist 0 0 ;1 2 1 1 1 B B 1 0 2 3 1C CC U (1) = A = B 1 1 0 1 1 C B B @ ;1 1 0 0 1 C A 1 1 1 1 0 Beispiel 6.7 (Fortsetzung von Beispiel 6.5) Fur den Graphen aus Beispiel 6.5 sollen die Dieser Algorithmus hat allerdings immer noch eine Laufzeit von der Groenordnung n4 . Tatsachlich gibt es, wenn man nur an den uij interessiert ist, andere einfache Algorithmen, deren Laufzeit von der Groenordnung n3 ist, vgl. die Literaturhinweise am Ende des Kapitels. // add routines for output // main loop, calculates a variant of the Bellman equations for ( m = 1 m < n m++ ){ for ( i = 0 i < n i++ ){ for ( j = 0 j < n j++ ){ // calculate min {u"i,k] + a"k,j]} in Bellman equations for ( k = 0 k < n k++ ){ if ( U"i]"j] > U"i]"k] + A"k]"j] ){ U"i]"j] = U"i]"k] + A"k]"j] }//endif }//endfor k }//endfor j }//endfor i }//endfor m bellman_ford_short.cc // assumes that A an U have been initialized Programm 6.5 fur ein ` mit 0 ` n ; 1 ; m. Dies liegt daran, da kurzere Weglangen mit einer Kante mehr direkt in U"i,j] abgespeichert werden und fur weitere Rechnungen schon benutzt werden. Dies hat im Programm den Vorteil, da die Zwischenspeicherung in temp uberussig wird und dadurch weniger Zuweisungen notig sind. Dafur wird in der m-ten Iteration jedoch nicht u(ijm) berechnet, sondern ein Wert zwischen u(ijm) und u(ijn;1) = uij . Der Algorithmus vereinfacht sich dann zu direkt jedesmal den Wert von temp"j] abspeichern. Dann gilt nach der m-ten Iteration der aueren Schleife u(ijm) U"i,j] = u(ijm+`) uij 6.3. KURZESTE WEGE IN GERICHTETEN GRAPHEN KAPITEL 6. ALGORITHMEN AUF ARRAYS min u(1) + ak1 ] k=1:::n 1k 1 = min 0 + 2 ;1 + 2 2 + 0 1 + 0 1 + 1] = 1 : 1 = min 0 ; 1 ;1 + 0 2 + 1 1 + 1 1 + 1] = ;1 0 2 1 BB 2 CC BB 0 CC = U1(1) A = (0 ; 1 2 1 1 ) 3 B@ 0 CA 0 ;1 1 BB 0 CC BB 1 CC = U1(1) A = (0 ; 1 2 1 1 ) 2 B@ 1 CA u(2) 15 u(2) 14 = min 0 + 1 ;1 + 1 2 + 1 1 + 1 1 + 0] = 3 : 0 011 BB 1 CC (1) = U1 A5 = (0 ;1 2 1 1) B BB 1 CCC @1A 1 = min 0 + 1 ;1 + 3 2 + 1 1 + 0 1 + 1] = 2 011 BB 3 CC BB 1 CC = (0 ; 1 2 1 1 ) = U1(1) A 4 B@ 0 CA Hier wurde gegenuber u(1) 13 = 2 eine kurzere Weglange uber 2 Kanten ermittelt. Weiterhin ist u(2) 13 u(2) 12 Entsprechend ist 1 0 0 1 BB 1 CC U1(1) A1 = (0 ;1 2 1 1) B u(1) + ak1] : BB 1 CCC = k=1min :::n k @ ;1 A Dies entspricht einer Verknupfung der ersten Zeile von mit der ersten Spalte von A. Diese Verknupfung, wir wollen sie mit \" bezeichnen, ist gerade die rechte Seite der Bellman Gleichungen, also U (1) ) = min 0 + 0 ;1 + 1 2 + 1 1 ; 1 1 + 1] = 0 : u(2) 11 = Dann ergibt sich u(2) 11 aus den Bellman Gleichungen gema 118 119 1 Die Analogie zwischen Matrixmultiplikation und Bellman Gleichungen wird besonders deutlich, wenn man sich nur dafur interessiert, ob i und j mit einem Weg mit hochstens m Kanten n;1 mal also mit \" als innerer und \min" als au erer Operation. Speziell ist U (1) = A U (2) = U (1) A = A A U (3) = U (2) A = A A A .. . ( n ; 1) n;1 U = U (n;2) A = A | {z A} =: A : Hier ist \" die innere und \+" die au ere Operation. In den Bellman Gleichungen C := A B ist der Eintrag cij der Ergebnismatrix C gegeben als cij = k=1 min a + bkj ] :::n ik k=1 1 C C C : C C A 1 C C C C C A 2 3 1 1 0 ;1 1 1 0 1 0 0 ;1 1 2 B B 2 0 2 3 1 0 0 2 U = U (4) = B B B @ ;1 ;2 0 0 0 3 3 1 1 1 1 0 0 0 ;1 1 2 B B 2 0 2 3 U (2) = B 1 1 0 2 B B @ ;1 ;2 0 0 = min 1 + 0 0 + 1 2 + 1 3 ; 1 1 + 1] = 2 : Das Beispiel zeigt, da die Bellman Gleichungen eine starke Analogie zur Matrixmultiplikation zeigt. Bei der Matrixmultiplikation C := A B ist der Eintrag cij der Ergebnismatrix C gegeben als n X cij = aik bkj ] : und schlielich Insgesamt erhalt man u(2) 21 0 0 1 B C B C B1 C = U2(1) 1 C A1 = (1 0 2 3 1) B B @ ;1 C A Also ist die erste Zeile von U (2) gleich (0 ;1 1 2 3). Die anderen u(2) ij berechnen sich entsprechend, z. B. 6.3. KURZESTE WEGE IN GERICHTETEN GRAPHEN ( KAPITEL 6. ALGORITHMEN AUF ARRAYS aij = ( true falls (i j ) 2 E false sonst (6.3) nach Wahl von ` im Widerspruch dazu, da u(ii`+1) < 0 ist. Also ist k0 ein von i verschiedener Knoten. Wegen u(ii`+1) < 0 sind u(ii`) und ak0 i < 1. Also ist u(ik`)0 die Lange eines Weges W von i nach k0 6= i, und (k0 i) eine Kante. Der Weg W bildet zusammen mit dieser Kante einen Zykel der Lange u(ik`)0 + ak0 i , die wegen Gleichung (6.3) negativ ist. Also ist dies ein negativer Zykel, der i enthalt. u(ii`+1) = u(ii`) + aii = u(ii`) + 0 = u(ii`) 0 Dann mu k0 6= i sein, denn sonst hatte Gleichung (6.3) die Form u(ii`+1) = u(ik`)0 + ak0 i : Sei m = ` die Iteration, bei der u(iim) zum letzten mal nichtnegativ ist. (Dieser Wert ` ( n) (`+1) < 0. Dieser Wert wird berechnet als existiert, da u(1) ii = 0 und uii < 0.) Dann ist uii ( ` +1) ( ` ) uii = mink uik + aki]. Dabei werde das Minimum bei k = k0 angenommen, d. h. Beweis: Sei u(iin) < 0. Zu zeigen ist, da i auf einem negativen Zykel liegt. d. h. die Existenz negativer Zykel kann durch Uberpr ufen der Diagonalelemente in U (n) festgestellt bzw. ausgeschlossen werden. Z = fi 2 V j u(iin) < 0g Dann gilt nach Durchfuhrung des Bellman-Ford Algorithmus mit einer zusatzlichen Iteration Satz 6.7 (Test auf negative Zykel) Sei Z := fi 2 V j i liegt auf einem negativen Zykelg. Fur die Berechnung der kurzesten Weglangen uij hat es sich als notwendig erwiesen, da der Graph G keine negativen Zykel enthalt. Es stellt sich daher die Frage, wie man die Gultigkeit dieser Voraussetzung ezient uberprufen kann. 6.3.6 Ermittlung negativer Zykel uij(m+1) = u(i1m) ^ a1j ] _ u(i2m) ^ a2j ] _ : : : _ u(inm) ^ anj ] was genau der Matrixmultiplikation entspricht (^ entspricht _ entspricht +). erhalt man Ausgehend von A = (aij ) mit true es gibt einen Weg von i nach j mit hochstens m Kanten, u(ijm) = false sonst. verbunden sind oder nicht. Dann kann u(ijm) als Boolesche Variable deniert werden, d. h. 120 u(ij`;1) + aji da j bei der Minimumsbildung beteiligt ist L(W0 ) + aji = Lange von Z0 < 0: mit der Initialisierung int tree"n]"n] tree"i]"j] := ( i -1 falls (i j ) 2 E : sonst Bisher haben wir nur die kurzesten Weglangen uij berechnet. Naturlich mochte man auch einen zugehorigen kurzesten Weg ermitteln. Dazu nutzt man die folgende, im Beweis von Satz 6.4 durchgefuhrte Uberlegung aus: Bei der Berechnung der Bellman Gleichungen u(ijm+1) = mink u(ikm) + akj ] entspricht (im Fall u(ijm+1) < 1) der Index k0 , fur den das Minimum angenommen wird, einem Knoten k0 , so da (k0 j ) die letzte Kante auf einem kurzesten Weg von i nach j ist. Merkt man sich also im Bellman-Ford Algorithmus jeweils diesen Index, so lat sich aus dieser Information ein kurzester Weg rekonstruieren. Die Realisierung im Programm erfolgt durch ein Array 6.3.7 Ermittlung kurzester Wege Es existieren also genau dann negative Zykel, sobald ein u(iim) < 0 wird. Da dies bei der Berechnung von u(iim) festgestellt werden kann, kann man abbrechen, sobald dies zum erstenmal eintritt, und eine entsprechende Fehlermeldung ausgeben. Der einzige Mehraufwand (neben der Uberprufung) besteht in einer zusatzlichen Iteration. Diese wird notwendig, da bei n Knoten ein elementarer Zykel maximal n Kanten haben kann (vgl. die zweite Richtung des Beweises). u(iin) u(ii`) = k=1 min u(`;1) + aki ] :::n ik 121 Sei umgekehrt i ein Knoten auf einem Zykel negativer Lange. Zu zeigen ist, da u(iin) < 0 ist. Betrachte einen negativen elementarer Zykel Z0 , der i enthalt. (Dieser existiert, da man bei Knotenwiederholungen Teilzykel so weglassen kann, da der verbleibende Rest immer noch ein negativer Zykel ist.) Sei j der Knoten vor i auf Z0 und sei ` die Anzahl der Kanten von Z0. Sei ferner W0 das Wegstuck von i nach j auf Z0 und sei L(W0 ) seine Lange. W0 hat dann ` ; 1 Kanten und daher ist u(ij`;1) L(W0 ). Dann ist ` n, da Z0 elementar ist, und daher u(iin) u(ii`) wegen Lemma 6.4. Es folgt 6.3. KURZESTE WEGE IN GERICHTETEN GRAPHEN KAPITEL 6. ALGORITHMEN AUF ARRAYS Knoten i der Graph Ti := (V Ei ) mit der Knotenmenge V = f0 : : : n ; 1g und der Kantenmenge Ei = f(tree"i]"j],j) j j = 0 1 : : : n ; 1 tree"i]"j] 6= ;1g ein gerichteter Baum mit i als Wurzel. Ein Weg von i nach j in Ti ist ein kurzester Weg von i nach j in G. Ti hei t daher auch Kurzester-Wege-Baum zum Knoten i. Satz 6.8 Hat G keine negativen Zykel, so ist bei Termination von Programm 6.6 fur jeden Dies bedeutet, da keine zwei Wege in den gleichen Knoten einmunden. Der Graph kann sich ausgehend von der Wurzel also nur verzweigen. Daher kommt auch der Name Baum . { Es gibt genau einen Knoten r, in dem keine Kante endet (die sogenannte Wurzel von T ). { Zu jedem Knoten i 6= r gibt es genau einen Weg von der Wurzel r zu i. Das Array tree enthalt nach Programmablauf die Information uber die kurzesten Wege (falls keine negativen Zykel gefunden wurden). Um dies genauer zu erlautern, brauchen wir den Begri des gerichteten Baums. Ein gerichteter Baum ist ein Digraph T = (V E ) mit folgenden Eigenschaften: // add routines for output // main loop, calculates a variant of the Bellman equations for ( m = 1 m < n m++ ){ for ( i = 0 i < n i++ ){ for ( j = 0 j < n j++ ){ // calculate min {u"i,k] + a"k,j]} in Bellman equations // and update shortest path tree for ( k = 0 k < n k++ ){ if ( U"i]"j] > U"i]"k] + A"k]"j] ){ U"i]"j] = U"i]"k] + A"k]"j] tree"i]"j] = k }//endif }//endfor k }//endfor j }//endfor i }//endfor m bellman_ford_tree.cc // assumes that A an U have been initialized Programm 6.6 In jedem Durchlauf der aueren Schleife des Algorithmus wird dann tree "i]"j] der Wert k zugewiesen, wenn bei U"i]"j] eine Anderung auftritt und das zugehorige Minimum bei k angenommen wird. Der Algorithmus (in der Version von Programm 6.5) wird dann zu: 122 123 - 1 i1 i2 - 2 i2 i3 - ppp i` j - j a ai`;1i` i` u(4) 11 = 0 (3) u(4) 12 = u11 + a12 (4) u13 = u(3) 12 + a23 (3) u(4) 14 = u12 + a24 (3) u(4) 15 = u13 + a35 k=1 k=2 k=2 k=3 j=2 j=3 j=4 j=5 und somit der in Abbildung 6.15 dargestellte Kurzeste-Wege Baum T1 . Die Kanten aus E1 entsprechen folgenden Bellman Gleichungen: E1 = f(1 2) (2 3) (2 4) (3 5)g 4 1 4 5 ;1 Als E1 ergibt sich aus der ersten Zeile von tree die Kantenmenge Beispiel 6.8 (Fortsetzung von Beispiel 6.5) Fur den Graphen aus Beispiel 6.5 erhalt 0 ;1 1 2 2 3 1 B C B B 4 ;11 ;21 52 33 C C tree = B 4 C: B @ 4 1 4 ;1 3 C A man Daher ist uij = aii1 + ai1 i2 + : : : + ai`;1 j und somit der Weg in Ti ein kurzester Weg von i nach j in G. .. . uii2 = uii1 + ai1 i2 uii1 = aii1 : uij = uii` + ai` j uii` = uii`;1 + ai`;1i` Da die Werte im Array tree gerade bei der Minimumsbildung aktualisiert werden, folgt ii1 i a i a i a i` = tree"i]"j ] i`;1 = tree"i]"i` ] : : : i = tree"i]"i2 ] : Vorgangerknoten, namlich tree"i]"j]. Also konnen in Ti keine zwei Kanten in einem Knoten zusammentreen. Daher existiert zu jedem Knoten j , der von i aus in G erreichbar ist, ein eindeutiger Weg von i nach j in Ti . Also bildet Ti einen Baum mit Wurzel i. Sei i = i0 i1 : : : i`+1 = j die Folge der Knoten auf dem Weg von i nach j in Ti . Dann ist Beweis: Jeder Knoten j 6= i hat in Ti (wenn er von i aus in G erreichbar ist) genau einen 6.3. KURZESTE WEGE IN GERICHTETEN GRAPHEN ? 3 2 > 2 1 3 - - 5 4 KAPITEL 6. ALGORITHMEN AUF ARRAYS Abbildung 6.15: Der Kurzeste-Wege Baum zum Knoten 1 in Beispiel 6.8. 1 ;1 Suchverfahren in Arrays werden in nahezu allen Buchern uber Entwurf und die Analyse von Algorithmen behandelt. Besonders ausfuhrlich gehen Knu73, Meh88, OW90] hierauf ein. Die Losung linearer Gleichungssysteme ist Gegenstand aller Bucher uber lineare Algebra. Ein empfehlenswertes Buch, das lineare Algebra und Numerik verbindet, ist GMW91]. Die Berechnung kurzester Wege ndet sich sowohl in Buchern uber Entwurf und die Analyse von Algorithmen (z. B. in CLR90, OW90]), als auch in Buchern uber Graphenalgorithmen und/oder kombinatorische Optimierung (etwa Jun94]). Eine sehr gute Darstellung verschiedener kurzeste Wegealgorithmen gibt Tar83]. 6.4 Literaturhinweise 124 125 Funktionen fallen in zwei Kategorien, solche die einen einzelnen Funktionswert zuruckgeben (in Pascal \functions") und solche, die keinen Wert zuruckgeben (in Pascal \procedures"). In C++ hat jede Funktion einen Ruckgabetyp, der in der Denition der Funktion angegeben werden mu. Die allgemeine Denition hat die Form 7.1.1 Funktionen und Prozeduren Funktionale Abstraktion erlaubt die \Auslagerung" haug auftretender ahnlicher oder gleicher Programmteile auf eigene \Untereinheiten" des Hauptprogramms. Diese Untereinheiten oder Unterprogramme existieren in allen Programmiersprachen unter verschiedenen Namen: procedures, functions, subroutines. In C++ heien alle Unterprogramme Funktionen (functions ), egal ob sie Werte zuruckgeben oder nicht. Funktionen sind ein Werkzeug zur Abstraktion, da man ihr Input-Output Verhalten (was tut die Funktion ) von ihrer Implementation (wie tut sie es ) trennen kann. Sie bilden daher ein Werkzeug sowohl fur den Algorithmenentwurf (Aufteilung des Algorithmus \kleine" Einheiten die alle Funktionen sind) als auch fur die Schaung wiederverwendbarer Software (gut implementierte Funktionen konnen in unterschiedlichen Aufgabenbereichen eingesetzt werden). In beiden Fallen ist alles, was der Programmierer braucht, die Spezikation der Funktion (d. h. eine Beschreibung dessen, was die Funktion tut). Die Implementation selbst ist fur ihn irrelevant, sofern sie die Spezikation erfullt. p Beispiele sind mathematische Funktionen wie sqrt(x) (berechnet x) oder pow(x,n) (ben rechnet x ), Funktionen zur Handhabung von Strings wie strcpy(string1,string2) zum Kopieren, und viele andere mehr. In allen Fallen interessiert nur das Verhalten, aber nicht die Implementation. 7.1 Funktionale (Prozedurale) Abstraktion Abstraktion von Methoden und Daten Kapitel 7 f g Funktionsrumpf Ruckgabetyp FunktionsName (formale Parameterliste ) KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN 1 Der Ruckgabetyp kann fehlen (schlechter Programmierstil!). Dann wird int als default angenommen. an die Funktion ubergeben. Die Funktion arbeitet mit der Kopie und andert den aktuellen Parameter des aufrufenden Programms nicht. Call by value (Wertparameter): Eine Kopie des aktuellen Parameters wird beim Aufruf Call by value (Wertparameter), Call by reference (variabler Parameter). Die Arten des Datenues sind geeignet zu kommentieren, z. B. durch die Kommentare /* in */, /* out */, bzw. /* inout */. Zur Realisierung dieses Datenues stehen verschiedene Methoden zur Parameterubergabe zur Verfugung: Flu nur in die Funktion, Flu nur aus der Funktion heraus, Flu sowohl in die Funktion, als auch aus der Funktion heraus. Der Datenu bezeichnet die Art des Datenaustausches zwischen der Funktion und dem sie aufrufenden Programm. Fur jeden Parameter in der formalen Parameterliste gibt es dabei drei Moglichkeiten 7.1.2 Parameter und Datenu von Variablen- und Konstantendenitionen, wobei Initialisierungen weggelassen werden konnen (und meist werden). Jede Denition Defi deniert genau eine Variable oder Konstante. Def1 Def2 : : : Defk Ist der Ruckgabetyp void, so wird kein Wert zuruckgegeben (wie bei einer Pascal Prozedur). Ist der Ruckgabetyp verschieden von void, so wird pro Aufruf genau ein Wert vom Ruckgabetyp mit einer return Anweisung im Rumpf zuruckgegeben. Als Ruckgabetyp sind alle Typen au er Array Typen erlaubt.1 Genauer: alle Typen deren Variable lvalues sein konnen. Die formale Parameterliste ist optional. Falls vorhanden, so besteht sie aus einer durch Kommas getrennten Folge Dabei gelten folgende semantische Regeln: 126 127 ? n by value ) Abbildung 7.1: Datenu bei der Funktion Factorial. Funktionswert ( 2 Eine genauere Erklarung erfolgt im Zusammenhang mit Pointer- und Referenztypen in Kapitel ??. Dies leistet folgende C++ Funktion. Beispiel 7.2 (Initialisierung von Variablen) Zwei Variablen sollen initialisiert werden. Factorial Beim Aufruf wird 5*a berechnet und dem Parameter n zugewiesen, der im Rumpf von wie eine Variable verwendet wird. Die return Anweisung gibt den Funktionswert zuruck, und dieser wird der Variablen x zugewiesen. Call by reference (Variabler Parameter): Die Adresse des aktuellen Parameters wird beim Aufruf an die Funktion ubergeben. Die Funktion arbeitet im Rumpf auf dem Speicherplatz des aktuellen Parameters und kann (aber mu nicht) diesen dadurch modizieren. Die Ubergabe einer Adresse wird durch das Anhangen von & an den Typbezeichner in der Denition Defi in der formalen Parameterliste gekennzeichnet.2 x = Factorial(5*a)+b Das aufrufende Programm kann diese Funktion in beliebigen Ausdrucken verwenden, z. B. in Factorial ist also eine Funktion mit einem Wertparameter n, die einen Wert zuruck gibt. Schematisch ist dies in Abbildung 7.1 dargestellt. Der Datenu erfolgt nur in die Funktion uber den Wertparameter n. Eine solche Funktion entspricht am ehesten der in der Mathematik ublichen Vorstellung einer Funktion. int Factorial (/* in */ int n) { int product = 1 for (int i = 2 i <= n i++) product *= i return product } zu berechnen. Dies leistet die folgende C++ Funktion. n! := 1 2 3 : : : n Beispiel 7.1 (Berechnung der Fakultat) Fur eine naturliche Zahl n > 0 ist 7.1. FUNKTIONALE (PROZEDURALE) ABSTRAKTION KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN by reference by reference 6 epsilon ) Abbildung 7.2: Datenu bei der Funktion Initialize. ( 6 delta Der Datenu fur diese Funktion ist in Abbildung 7.3 dargestellt. Im aufrufenden Programm bewirken die Anweisungen void MinMax( /* inout */ double& a, /* inout */ double& b ) { if ( a > b ){ double temp = a a = b b = temp }//end if Beispiel 7.3 (Berechnung des Minimums und Maximums zweier Zahlen) Zu zwei gegebenen Gleitkommazahlen a b sollen minfa bg und maxfa bg berechnet werden. Da zwei Werte zuruckgegeben werden sollen, ist dies uber den Funktionswert nicht moglich. Daher wird mit call by reference gearbeitet. Dabei werden die Adressen von alpha und beta an die Funktion Initialize ubergeben und den Variablen delta und epsilon zugewiesen. Die Zuweisung delta = 1.0 modiziert dann direkt den durch die ubergebene Adresse bezeichneten Speicherplatz, also den von alpha. Nach Abarbeitung von Initialize hat alpha den Wert 1.0 und beta den Wert 0.0001. double alpha, beta Initialize (alpha, beta) Als void-Funktion kann Initialize nur als Anweisung geschrieben werden und nicht in Ausdrucken verwendet werden, z. B. Initialize ist also eine Funktion mit zwei variablen Parametern delta und epsilon, die in der Funktion Werte bekommen, die dann auerhalb der Funktion verwendet werden konnen. Die Funktion gibt keinen Wert zuruck. Schematisch ist dies in Abbildung 7.2 dargestellt. Der Datenu erfolgt nur aus der Funktion. void Initialize(/* out */ double& delta, /* out */ double& epsilon) { delta = 1.0 epsilon = 0.0001 } 128 129 ? by reference 6 6 ? by reference b // falls x > y // entsprechende Aktion In der if-Anweisung interessiert an sich nur der Funktionswert. Dennoch wird bei der Berechnung dieses Wertes eine Vertauschung von x und y (falls x > y) im aufrufenden Programm if (MinMax_2(x,y)){ ... }//end if Der Datenu fur diese Funktion ist in Abbildung 7.4 dargestellt. Im aufrufenden Programm kann der Funktionswert in Ausdrucken genutzt werden, z. B. Boolean MinMax_2 ( /* inout */ double& a, /* inout */ double& b ) { if ( a > b ){ double temp = a a = b b = temp return TRUE }//end if else return FALSE } Beispiel 7.4 (Fortsetzung von Beispiel 7.3) Zusatzlich zu Beispiel 7.3 soll als Funktionswert TRUE zuruckgegeben werden, falls eine Vertauschung stattgefunden hat, d. h. a > b vor dem Aufruf gilt. Sonst soll FALSE zuruckgegeben werden (#include "bool.h" wird vorausgesetzt). ) Abbildung 7.3: Datenu bei der Funktion MinMax. ( a lat dagegen die Werte von x und y unverandert. MinMax(y,x) uber den call by reference, da x und y nach Abarbeitung die Werte getauscht haben, also x == 3.7 und y == 5.1 gilt. Der Aufruf double x = 5.1, y = 3.7 MinMax(x,y) 7.1. FUNKTIONALE (PROZEDURALE) ABSTRAKTION ? ? b by reference a by reference 6 Abbildung 7.4: Datenu bei der Funktion MinMax 2. Funktionswert ( 6 ) KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN Komponententyp Arrayname "Anzahl ] Die Art der Parameterubergabe (call by value oder call by reference) kann fur alle Datentypen beliebig gewahlt werden au er fur Arrays . Bei Arrays ist nur die Ubergabe durch call by reference moglich. Dies liegt daran, da in C++ unter dem Namen des Arrays nicht (wie z. B. in Pascal) der gesamte Speicherbereich des Arrays angesprochen wird, sondern nur die Adresse der 1. Komponente des Arrays. Die Denition 7.1.3 Der Sonderfall des Arrays in C++ Ein Ausdruck wird zu einer (einfachen) Anweisung durch Anfugen eines Semikolons. In diesem Fall wird der Ruckgabewert unterdruckt, und die Seiteneekte sind die einzigen Eekte. Man beachte wieder die in Kapitel 3.1.1 erlauterte wichtige Regel der C Programmierung: MinMax_2(x,y) Ist man nur am Seiteneekt, nicht jedoch am Funktionswert interessiert, lat sich die Funktion wie eine Anweisung verwenden: double x,y ... if (MinMax_2(x,y)) // Vertauschung als Seiteneffekt cout << "Das Minimum von x und y ist y = " << x << endl else cout << "Das Minimum von x und y ist x = " << x << endl durchgefuhrt, was ja im Funktionsrumpf vorgesehen ist. Man bezeichnet dies (vgl. Kapitel 3.1.1) als Seiteneekt des Aufrufs. Seiteneekte sollten immer gut dokumentiert sein! Eine sinnvolle Verwendung des Seiteneffektes liefert folgendes Programmfragment. 130 131 {z } {z }| } { Arrayname"2] | z sizeof(Komponententyp) ::: 3 Genaugenommen als ein constant pointer , vgl. ??. Dies ist auch der Grund dafur, da keine Zuweisungen an Arrayvariable moglich sind. 4 Eine andere Moglichkeit besteht in der Verwendung von Pointern, vgl. Kapitel ??. Hierdurch lassen sich auch die hiernach angesprochenen Schwierigkeiten bei mehrdimensionalen Arrays vermeiden. Beispiel 7.5 (Minimale und maximale Arraykomponente) In einem Array von Gleitkommazahlen soll der minimale und der maximale Wert einer Komponente ermittelt werden. Da 2 Werte zuruckgegeben werden sollen, erfolgt dies wieder uber variable Parameter. Die eckigen Klammern konnen leer bleiben. Die Angabe eines zur Compilierzeit auswertbaren Ausdrucks wird ignoriert. Nicht zur Compilierzeit auswertbare Ausdrucke ergeben Fehlermeldungen. Die tatsachliche Anzahl der Komponenten sollte als zusatzlicher (Wert-)Parameter ubergeben werden. Auf diese Weise lassen sich Funktionen fur (eindimensionale) Arrays unterschiedlicher Groe verwenden, ganz im Sinne der funktionalen Abstraktion. Komponententyp Arrayname "] Die Anfangsadresse der i-ten Komponente ergibt sich also als Wert | von {zArrayname} + i sizeof(Komponententyp): Anfangsadresse Dies setzt voraus, da die Groe des Komponententyps ebenfalls beim Compilieren bekannt ist. Dies ist wichtig bei mehrdimensionalen Arrays, die intern als eindimensionale Arrays angesehen werden, deren Komponententyp wieder ein Arraytyp ist. Die Groe der KomponentenArrays mu also zur Compilierzeit bekannt sein. Bei der Parameterubergabe sollte man eindimensionale Arrays als Parameter denieren durch4 isizeof(Komponententyp ). Dafur wird ein entsprechender Oset berechnet. Fur Arrayname "i] ist dies Abbildung 7.5: Organisation von Arrays im Speicher. Arrayname"0] | ? Arrayname bewirkt beim Compilieren, da Arrayname als Konstante 3 angelegt wird, die eine Adresse enthalt, namlich die der 1. Komponente. Der Compiler mu Anzahl beim Compilieren auswerten konnen (sonst erfolgt eine Fehlermeldung) und reserviert dann Anzahl viele konsekutiv hintereinanderliegende Speicherbereiche der Groe sizeof(Komponententyp ). Der (i + 1)-te dieser Speicherbereiche wird dann mit Arrayname "i] angesprochen, siehe Abbildung 7.5. 7.1. FUNKTIONALE (PROZEDURALE) ABSTRAKTION KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN double coeff"M]"N], double rhs"], int n, int m, double eps, Boolean& solvable, int& rank, double solution"] ) 5 Da man die Gro e auch der anderen Dimensionen meist erst zur Laufzeit festlegen mochte, empehlt sich eine Losung mit Pointern, vgl. Kapitel ??. /* out */ void Gauss( /* in */ ren (Algorithmus 6.3) zur Losung linearer Gleichungssysteme soll als Funktion bereitgestellt werden. Hierbei werden M und N als globale Konstante vorausgesetzt (die elegantere Losung uber Pointer wird in Kapitel ?? angegeben). Sie stellen obere Schranken fur die Anzahl der Gleichungen bzw. Variablen dar. Beispiel 7.6 (Gausche Elimination als Funktion) Das Gausche Eliminationsverfah- Nach Abarbeitung enthalten x, y den minimalen bzw. maximalen Komponentenwert von a. Das Array a bleibt unverandert, wird aber mit call by reference ubergeben. Bei mehrdimensionalen Arrays mu, wie oben erlautert, die Groe des Komponententyps dem Compiler beim Compilieren bekannt sein. Daher mu bei der Parameterubergabe die Groe aller Dimensionen (bis auf die erste) beim Compilieren bekannt sein.5 const int n = 100 double a"n] double x,y ... VectorMinMax(a,n,x,y) Im aufrufenden Programm konnte diese Funktion wie folgt verwendet werden: void VectorMinMax( /* in */ double vector"], /* in */ int size, /* out */ double& min, /* out */ double& max) { min = max = vector"0] for ( int i = 1 i < size i++ ){ if ( vector"i] < min ) min = vector"i] if ( vector"i] > max ) max = vector"i] }//endfor } #include <iostream.h> 132 } { // matrix of coefficients // right-hand side // variables // output if ( solvable ) { // output the computed solution rank++ // return the correct rank, counting from 1 for ( j = 0 j < n j++ ) { solution"j] = x"col"j]] }//endfor }//endif ... // code aus Algorithmus gauss.cc // initialize a and b from the input parameters for ( i = 0 i < m i++ ) { for ( j = 0 j < n j++ ) { a"i]"j] = coeff"i]"j] }//endfor b"i] = rhs"i] }//endfor int pivotRow, pivotCol // row and column index of pivot element int i, j, k // counters int index // aux. variable double absValue, max, q, pivot Boolean exitLoop // TRUE if main loop should be exited int row"M] // stores row permutation int col"N] // stores column permutation // a"row"i]]"col"m]] denotes curent matrix entry after // row and column permutations double a"N]"M] double b"M] double x"N] //----------------------------------------------------------// Solves system of linear equations Ax = b by Gaussian elimination // coeff"m]"n] is the m*n Matrix A // rhs"m] is the right-hand-side b // eps is the numerical precision // solvable is TRUE if the system has a solution // solution"n] outputs a solution if solvable == TRUE // rank outputs the rank of A //----------------------------------------------------------- 7.1. FUNKTIONALE (PROZEDURALE) ABSTRAKTION 133 KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN {z Scope von Def } 6 Andere Programmiersprachen wie LISP oder APL verwenden dynamische Scoperegeln, bei der die Hierarchie der Aufrufe zur Laufzeit festlegt, welcher Identier gemeint ist. 7 Also nicht die f ... g von enum-Denitionen und anderen Initialisierungen, wohl aber die f ... g von struct-Denitionen, Funktions Blocken usw. Dies schliet in ihm direkt und indirekt enthaltene Blocke ein, sofern keine Uberdeckung durch Neudenition in einem \tieferen" Block auftritt. | f : : : f : : : Def : : : f : : : f : : : g : : : g : : : f : : : g : : : g : : : g 3. Der Scope eines Identiers, der innerhalb eines Blocks deniert wurde (man nennt das lokal deniert), ist der gesamte Block ab der Denition. 2. Der Scope eines Identiers, der auerhalb aller Blocke { ... } in einem Textle deniert wurde (man nennt das global deniert), ist das gesamte Textle von seiner Denition an (aber nicht andere Textles). 1. Der Scope eines Identiers ist der Teil des Programmtextes, in dem man ihn (und keinen anderen gleichen Namens) im Programm verwenden kann. In C++ gelten folgende Scoperegeln: In Unterprogrammen konnen Identier verwendet werden, die auch in anderen Programmteilen oder Unterprogrammen auftreten. Dies geschieht zwangslaug wenn Programme von mehreren Personen entwickelt werden oder Fremdsoftware benutzt wird. Jede Programmiersprache braucht daher Regeln, die festlegen, welcher Identier wann gemeint ist, und wie lange ein ihm ihm eventuell zugeordneter Speicherplatz mit dem Identier angesprochen wird. In C++ werden solche Gultigkeitsbereiche oder Scopes (wie auch in Pascal) durch den Programmtext festgelegt. Man spricht daher auch von statischen Scoperegeln .6 In C++ bilden die Blocke die Grundlage der Scoperegeln. Diese sind durch f ... g geklammerte Programmteile, genauer: compound statements .7 Diese Klammern bilden in einem korrekten Programm einen korrekten Klammerausdruck . Also liegen wegen Satz 4.1 je 2 Blocke entweder disjunkt hintereinander im Programmtext, oder einer ist vollstandig im anderen enthalten. f:::g ::: f:::g | {z } | {z } bzw. f: : : f| :{z: : g} : : : g Block 1 Block 2 {z 1 } | Block Block 2 7.1.4 Gultigkeitsbereiche von Identiern (Scope and Lifetime) Im Rumpf der Funktion werden zunachst die benotigten Variablen deklariert und die Eingabedaten in die lokalen Variablen a und b kopiert, dann wird der Code aus Algorithmus 6.3 wie bisher verwendet, und zum Schlu wird solution zuruckgegeben. Die Ruckgabe von solvable und rank erfolgt implizit. Dies wurde auch fur solution gelten, falls man uberall im Rumpf mit solution"i] statt x"i] rechnet. 134 135 | {z Neudef }| {z Def } wird der Identier a zweimal deniert, als globale int-Variable und in Q als double-Variable. Diese Neudenition uberdeckt die int-Variable innerhalb des Blocks von Q. In main wird daher der int-Variablen a der Wert 1 zugewiesen. Der Aufruf von P innerhalb von Q bezieht sich ebenfalls auf die int-Variable a, da sie im Block von P Gultigkeit hat. Das Programm schreibt also 1 auf den Bildschirm. void main() { a = 1 Q() } void Q() { double a a = 3.14 P() } void P() { cout << a << endl } int a #include <iostream.h> Beispiel 7.7 (Statische versus dynamische Scoperegeln) Im Programmfragment 5. Formale Parameter in Funktionsdenitionen haben als Scope den gesamten auersten Block der Funktion (wie in Regel 3., inklusiv enthaltener Blocke und exklusiv moglicher Uberdeckung). 6. Funktionen konnen nicht geschachtelt werden (im Gegensatz zu Pascal). Jede Funktion mu deniert (oder deklariert) werden, bevor sie verwendet werden kann. Da Funktionsnamen auerhalb von Blocken deklariert oder deniert werden, gilt fur ihren Scope Regel 1. Def | {z } f : : : f : : : Def : : : f : : : Neudef : : : f : : : g : : : g : : : f : : : g : : : g : : : g 4. Neudenition eines Identiers (mit vollig anderer Bedeutung!) in anderen Blocken ist moglich. Erfolgt die Neudenition in einem Block B1 , der innerhalb eines Blocks B2 liegt, in dem der Identier bereits deniert war, so tritt Uberdeckung auf. Der Scope der ersten, \aueren" Denition wird vom Scope der zweiten, \inneren" Denition uberdeckt. Innerhalb des \inneren" Scopes wird die neue Denition verwendet. 7.1. FUNKTIONALE (PROZEDURALE) ABSTRAKTION KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN // Beim Verlassen des Rumpfes freigegeben // Behaelt Wert von Aufruf zu Aufruf Ein C++ Programm ist normalerweise in verschiedene Files unterteilt. Daher braucht man Mechanismen, um Deklarationen und Denitionen, die in einem File F1 enthalten sind, auch in einem anderen File F2 \bekannt" zu machen. Wegen der Scoperegeln geht dies nur fur global denierte Identier, also globale Typen, Konstanten, Variable und Funktionen (alles, was nicht innerhalb von Blocken deniert wird). 7.1.5 Funktionsprototypen und externe Deklarationen void SomeFunc( int someParam ) { int i = 0 // bei jedem Aufruf initialisiert static int n = 100 // zur Compilierzeit einmal initialisiert ... } Insbesondere werden Initialisierungen von static-Variablen in Denitionen nur einmal durchgefuhrt. Auerdem mu der Wert der Initialisierung zur Compilierzeit ermittelbar sein. void SomeFunc() { double x static int a ... } Aus den Scope Regeln ergibt sich, da jede Funktion auch auf globale Variable zugreifen kann und ihre Werte verwenden bzw. andern kann. Dies stellt eine zusatzliche Form des Datenues dar (neben Parametern und Funktionswert). Da diese Art des Datenues nicht aus der Parameterliste ersichtlich ist, sollte die Verwendung globaler Variablen nur sparsam erfolgen und stets gut dokumentiert werden. Ein mit dem Scopekonzept verwandtes Konzept ist die Lebenszeit (Lifetime ) von Variablen. Das ist die Zeitspanne wahrend der Programmausfuhrung, in der dem Identier der Variablen Speicherplatz zugeordnet wird. Lifetime wird also zur Laufzeit bestimmt, Scope zur Compilierzeit . In C++ wird die Lifetime einer Variablen von ihrer Speicherklasse bestimmt: automatic oder static . Automatic bedeutet, da der Variablen beim Eintritt in den denierenden Block Speicher zugeweisen wird, und dieser beim Verlassen des denierenden Blocks wieder freigegeben wird. Static bedeutet, da der Speicherplatz nur einmal zugewiesen wird und wahrend der gesamten Laufzeit des Programms erhalten bleibt. Die Zugehorigkeit zu den Speicherklassen automatic und static kann bei der Deklaration durch die reservierten Worte auto bzw. static angegeben werden. Fehlt eine Angabe, so wird auto bei lokalen Variablen, und static bei globalen Variablen als default angenommen. Bei Verwendung der dynamischen Scoperegeln (LISP, APL) wurde der Scope aus der Aufrufhierarchie ermittelt. main ruft Q auf, und Q ruft P auf. Daher wurde P auf die in Q denierte double-Variable a zugreifen und 3.14 ausgeben. 136 137 am Ende. Der Rumpf, der die eigentliche Denition ausmacht, fehlt // in File F1 // in File F2 8 sofern sie in dem File enthalten ist. Der Aufruf einer Funktion erfolgt mit den sogenannten aktuellen Parametern , die mit den in der Denition der Funktion aufgefuhrten formalen Parametern typkompatibel sein mussen. 7.1.6 Abarbeitung von Funktionsaufrufen bezeichnet also 2 verschiedene Variable N mit unterschiedlichem Scope. static int N static int N Es ist guter Programmierstil, in jedem File eines C++ Programms zunachst alle Funktionen mittels Prototyp zu deklarieren, dann die Funktion main zu denieren8 und dann die deklarierten Funktionen nach main zu denieren . In diesem Zusammenhang kommt static-Variablen eine besondere Bedeutung zu. Ihr scope wird durch den Zusatz static auf das File beschrankt, in dem sie deniert werden. int Sqr( int x ) { return x*x } ein Prototyp fur eine Funktion Sqr, dem der Compiler bereits den Funktionsnamen, den Ruckgabetyp, und Typen (und ggf. auch Namen) der Parameter entnehmen kann. Eine Denition ware dann z. B.: int Sqr( int x ) einschlielich eines also. So ist Ruckgabetyp Funktionsname (formale Parameterliste ) sind Deklarationen, die die Identier N und pi und die zugehorigen Typen einfuhren, wobei die Denition in einem anderen File erfolgen kann. Funktionen werden durch sogenannte Prototypen deklariert. Sie bestehen aus extern const int N extern double pi Dazu nutzt man die in C++ gemachte Unterscheidung zwischen Deklaration und Denition , vgl. Kapitel 3.2. Da Deklarationen mehrmals erfolgen konnen (im Gegensatz zu Denitionen) sind sie das geeignete Mittel, um globale Denitionen in einem File auch in anderen Files bekannt zu machen. Bei Variablen und Konstanten benutzt man dafur das reservierte Wort extern. 7.1. FUNKTIONALE (PROZEDURALE) ABSTRAKTION KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN 5,1 y 3,7 - r 5,1 b y - - r 3,7 3,7 y b y - 5,1 r 5,1 r 3,7 Und schlielich nach Ende des Programmfragments: x Nach Abarbeitung der Funktion: a x Unmittelbar vor dem Ende der Abarbeitung: a x temp 3,7 Beim Aufruf von MinMax(x,y) wird zusatzlicher Speicherplatz fur die formalen Parameter a und b und (bei Abarbeitung) die lokale Variable temp angelegt. Stellt man Referenzen als Pfeile (Zeiger) dar, ergibt sich unmittelbar nach der Parameterubergabe folgendes Bild im Speicher: x Vor dem Aufruf ist die Situation im Speicher wie folgt: double x = 5.1, y = 3.7 MinMax(x,y) x = y - x // Absolutbetrag der Differenz in x Beispiel 7.8 (Fortsetzung von Beispiel 7.3) Betrachte folgenden Aufruf von MinMax. Bei Wertparametern darf der aktuelle Parameter ein Ausdruck sein, bei variablen Parametern mu es eine Variable (genauer: ein lvalue) sein. Beim Aufruf der Funktion werden Speicherplatze fur die formalen Parameter angelegt, die unter den Namen dieser Parameter im Rumpf der Funktion angesprochen werden. Wertparameter werden ausgewertet und ihr Wert in den Speicherplatz des zugehorigen formalen Parameters kopiert. Bei variablen Parametern wird die Referenz (Adresse) des ubergebenen lvalues ermittelt und im Speicherplatz des zugehorigen formalen Parameters abgelegt. Im Rumpf arbeitet man dann bei Nennung dieses Parameters stets auf dem Speicherplatz des ubergebenen lvalues. Durch ein return-Statement wird ein Wert zuruckgegeben und die Abarbeitung der Funktion beendet. Ansonsten (bei void-Funktionen) endet die Abarbeitung der Funktion mit der Ausfuhrung des Rumpfes oder einer return Anweisung ohne Ruckgabe eines Wertes. Nach Abarbeitung der Funktion werden alle Speicherplatze fur formale Parameter, lokale Variablen usw. geloscht, und im aufrufenden Programm wird an der Stelle nach dem Aufruf der Funktion weitergemacht. Da bei variablen Parametern auf dem Speicherplatz des aktuellen Parameters gearbeitet wurde, bleiben im Funktionsrumpf vorgenommene Anderungen erhalten. 138 1,4 y 5,1 139 // arrays with a name attached to them void SquareNumbers( /* in */ NamedArray inNumbers, /* out */ NamedArray& outSquares ) { int i for ( i = 0 i < N i++ ) { outSquares.comp"i] = inNumbers.comp"i] * inNumbers.comp"i] }//endfor void main() { NamedArray A,B int i,j strcpy( A.name, "numbers 0-100") for ( i = 0 i < N i++ ) A.comp"i] = i strcpy( B.name, "sqares of 0-100") SquareNumbers( A,B ) cout << B.name << " :\n" for ( i = 0 i < 10 i++ ) { cout << A.comp"i] << " : " << B.comp"i] << endl }//endfor } void SquareNumbers( /* in */ NamedArray inNumbers, /* out */ NamedArray& outSquares ) // calculates the square of every component of inNumbers // and outputs it in the named array outSquares struct NamedArray{ char name"21] int comp"N] } const int N = 1001 #include <iostream.h> #include <string.h> #include <time.h> Beispiel 7.9 (U bergabe von Structs als Parameter) Betrachte das folgende Programm: Besonders deutlich wird der Unterschied zwischen \call by value" und \call by reference" bei strukturierten Datentypen (auer bei Arrays). Die Ubergabe als Wertparameter erfordert dann das Kopieren aller Komponenten des Arrays, wahrend die Ubergabe als variabler Parameter nur die Zuweisung einer Adresse erfordert. x 7.1. FUNKTIONALE (PROZEDURALE) ABSTRAKTION KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN B squares of 0{100 0 1 2 .. .. . . 100 numbers 0{100 0 0 1 1 2 2 .. .. . . 100 100 numbers 0{100 0 0 1 1 2 2 .. .. . . 100 100 outSquares B - r squares of 0{100 0 1 2 .. .. . . 100 Abbildung 7.6: Vor dem Aufruf von SquareNumbers. numbers 0{100 0 0 1 1 2 2 .. .. . . 100 100 9 Auf Ihrem Rechner konnen Sie die entsprechenden Werte mit sizeof(A) und sizeof(&A) ermitteln. Es werden also Speicherplatze fur die formalen Parameter inNumbers und outSquares angelegt. Da inNumbers ein Wertparameter ist, wird der Wert von A ermittelt und nach inNumbers kopiert. Bei outSquares mu lediglich die Adresse von B abgelegt werden. Man beachte den Unterschied im Speicherplatz! inNumbers belegt 426 Byte (101 4 Byte fur das Array + 21 Byte fur den String + 1 Byte fur Struct Verwaltung), wahrend outSquares nur 4 Byte fur die Adresse belegt.9 Abbildung 7.7: Nach der Parameterubergabe an SquareNumbers. inNumbers A A Der globale Typ NamedArray stellt als Struct Arrays mit einem Namen zur Verfugung (\benannte Arrays"). Die Funktion SquareNumbers hat als formale Parameter zwei solche Structs, wobei inNumbers ein Wertparameter und outSquares ein variabler Parameter ist. Wir sehen uns wieder die Situation im Speicher zu bestimmten Zeitpunkten an: Die Situation vor dem Aufruf von SquareNumbers ist in Abbildung 7.6 dargestellt, die nach der Parameterubergabe in Abbildung 7.7. } 140 141 Ruckgabetyp Funktionsname ( formale Parameterliste ) 3. der Adresse der Anweisung im ubergeordneten Block, mit der nach Verlassen des Blocks weitergemacht wird (Rucksprungadresse ). 2. Pointern auf globale Identier bzw. Identier aus ubergeordneten Blocken, die nicht im momentanen Block neu deniert werden, 1. Eintragen fur lokale Identier (inklusive formale Parameter bei Funktionen), Wir sehen uns jetzt die Organisation der Speicherplatzverwaltung beim Ein- und Austritt in Scopeblocke etwas genauer an. Jedes Scopeblock hat zur Laufzeit ein sogenanntes Environment in Form eines Activation Record mit 7.1.7 Der Run-Time Stack bewirkt, da die Funktion bei der Syntaxuberprufung durch den Compiler als normale Funktion behandelt wird (ermoglicht funktionale Abstraktion auch an zeitkritischen Stellen!), beim Ubersetzen jedoch jeder Aufruf der Funktion durch einen Programmtext ersetzt wird, der dem Aufruf aquivalent ist, aber keine Parameterubergabe usw. ordert (ermoglicht ezienten Code). Die Grundidee besteht im Kopieren des Rumpfes der Funktion, wobei formale Parameter textuell durch die aktuellen Parameter ersetzt werden. inline mit const verhindert also, da der aktuelle Parameter vector unabsichtlich im Rumpf der Funktion geandert wird. Sind Funktionen sehr zeitkritische Teile des Programms, die zudem sehr oft benutzt werden, so kann man die Zeit, die zur Ubergabe der Parameter erforderlich ist, sparen, indem man sie als inline-Funktionen deklariert. Die Deklaration/Denition. int someFunc( const int vector"], int size) { ... } Bei groen strukturierten Datentypen empehlt es sich daher, sie stets durch call by reference zu ubergeben, um Speicherplatz und Zeit zu sparen. Dabei mu man naturlich sicherstellen, da durch den call by reference keine unerwunschte Anderung des ubergebenen lvalues entsteht. Bei Arrays, die ja nur durch call by reference ubergeben werden konnen, erlaubt C++ die Ubergabe mit dem Zusatz const. Die Denition Dies bedeutet auch, da bei der Parameterubergabe entsprechend Zeit fur das Kopieren von A verbraucht wird. 7.1. FUNKTIONALE (PROZEDURALE) ABSTRAKTION KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN 10 Die Wurzel ist dabei der oberste Knoten, und alle gerichteten Kanten verlaufen von \hoheren" zu \tieferen" Knoten. Daher verzichtet man bei derart dargestellten Baumen auf die Angabe der Kantenrichtung durch Pfeile wie sonst bei gerichteten Graphen ublich. dung 7.8. Die Scopeblocke sind mit A (gesamtes File) und B {D (Blocke aufgrund von f ... g-compound statements) gekennzeichnet. Der Aufrufbaum hat die in Abbildung 7.9 angegebene Gestalt.10 (Durch rekursive Aufrufe kann der Aufrufbaum im Prinzip unendlich gro werden.) Wir sehen uns jetzt den Run-Time-Stack an. In Block A werden alle globalen Groen deniert, also double x,y, int z und die Funktionen f, g und main. Diese werden nicht auf dem Run-Time-Stack, sondern als globale Groen in einen anderen Bereich des Speichers, dem sogenannten Heap abgelegt, vgl. Abbildung 7.10. Beim Aufruf von main werden keine lokalen Groen deniert. Der Aufruf g(z) in main bewirkt den Eintritt in den Block C und die Parameteridentikation von x (deniert in C ) mit z (global deniert). Da noch kein ubergeordneter Block existiert, erubrigt sich die Einrichtung eines static Pointers. Alle ubergeordneten Identier ndet man im Heap (gekennzeichnet durch Heap im Stack), vgl. Abbildung 7.11. Abbildung 7.12 beschreibt den Run-Time-Stack beim Eintritt in den Scopeblock D aus Block C (links) und beim Eintritt in den Scopeblock B aus Block D (rechts). Im Statement x = x + i*a in Block B ist also mit x die globale Variable x und nicht die an f ubergebene Variable x gemeint, da der static pointer auf den Heap zeigt. Nach Abarbeitung des Blocks B wird der entsprechende Activation Record auf dem Stack geloscht. Die Rucksprungadresse 3 gibt an, wo im Programm weitergemacht wird. Die dann entstehende Situation ist in Abbildung 7.13 links angegeben, die nach Abarbeitung von Block D rechts. Die Situation beim Eintritt in Block B aus Block C ist in Abbildung 7.14 angegeben. Danach werden B und C abgearbeitet und die zugehorigen Activation Records geloscht. Beispiel 7.10 (Aufrufbaum und Run-Time-Stack) Betrachte das Programm in Abbil- Beim Eintritt in den Scopeblock werden diese Records mit den entsprechenden Eintragen auf einem Stack, dem sogenannten Run-Time-Stack abgelegt. Die Adressen innerhalb eines Activation Records ergeben sich dann durch die Anfangsadresse des Records plus dem jeweiligen Oset innerhalb des Records, der zur Compilierzeit bekannt ist. Zur Einrichtung der Pointer auf Identier aus ubergeordneten Blocks gibt es mehrere Moglichkeiten. Eine gangige besteht in der Einrichtung eines Zeigers (static pointer ), der auf den Record des nachsten Scopeblocks in der statischen Hierarchie zeigt. Dadurch kann der denierende Scopeblock eines Identiers uber eine Kette von Pointern erreicht werden. Neben diesem Run-Time-Stack ist zur Analyse der Aufrufe von Funktionen bzw. des Ein- und Austritts in Scopeblocke der sogenannten Aufrufbaum von Bedeutung, der die Aufrufhierarchie zur Laufzeit darstellt. In Zusammenhang mit der Rekursion (Kapitel 8) wird er auch Rekursionsbaum genannt. Beide Begrie sollen nun an folgendem Beispiel erlautert werden. 142 long int x, y ... f(x) ... ... f(y) ... g f int y ... ... g(z) ... 9 > > > > > 9 > > > > > > > > > > > > = > B > > > > > > > > > > > > > > > > 9 > > > > > > > > > > > > > > > > > 9 = > > > > > A > > > > > > = = > D > C > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > 9 > > > > > > > > > = > > E > > > > > > > > Abbildung 7.8: Ein Programm mit seinen Scopes. g f void main() g f int i ... x = x + i*a ... void g( int x ) g f void f( double a ) double x, y int z 7.1. FUNKTIONALE (PROZEDURALE) ABSTRAKTION 143 144 B D @ @ B @ C Aufruf von f in D Eintritt in Block D und Aufruf von f in C Aufruf von g in main Aufruf von main x y z f g main function function function double double int Abbildung 7.11: Der Stack nach dem Aufruf von main. 8 > (Wert von z) > < xy int int C > Rucksprungadresse 1 > static pointer: Heap : Abbildung 7.10: Der Heap zum Programm aus Abbildung 7.8. Heap Abbildung 7.9: Der Aufrufbaum zum Programm aus Abbildung 7.8. ; ; ; E KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN Rucksprungadresse 1 static pointer: Heap y int Rucksprungadresse 2 static pointer: x int (Wert von z) x long int y long int r 8 > > < B> > : 8 > > < D> > : 8 > > < C> > : Rucksprungadresse 1 static pointer: Heap y int Rucksprungadresse 2 static pointer: x int (Wert von z) x long int y long int r (Wert von x) Rucksprungadresse 3 static pointer: Heap a double i int 145 Rucksprungadresse 1 static pointer: Heap y int Rucksprungadresse 2 static pointer: x int (Wert von z) x long int y long int r 8 > (Wert von z) > < x int int C > yRucksprungadresse > : static pointer: Heap1 (Wert von x) Rucksprungadresse 1 static pointer: Heap y int Rucksprungadresse 4 static pointer: Heap x int (Wert von y) a double i int Abbildung 7.14: Der Stack nach dem Eintritt in Scopeblock B aus C . 8 > > < B> > : 8 > > < C> > : Abbildung 7.13: Der Stack nach dem Austritt aus Scopeblock B (links) und aus D (rechts). 8 > > < D> > : 8 > > < C> > : Abbildung 7.12: Der Stack beim Eintritt in den Scopeblock D aus C (links) und in B aus D (rechts). 8 > > < D> > : 8 > > < C> > : 7.1. FUNKTIONALE (PROZEDURALE) ABSTRAKTION KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN 8 Modul Variable 9 > > > > > Modul Variable > > > < Funktion = Modul M Funktion > > > > Funktion > > : ::: 11 Die Hohe eines Baumes ist die maximale Kantenzahl auf einem Weg von der Wurzel bis zu einem Blatt. Module sind so konzipiert, da andere Module oder Programme Teile oder das Modul als Ganzes nutzen konnen. Man nennt die Nutzer Klienten des genutzten Moduls. Beispiele fur Module sind Bibliotheken wie math.h, string.h oder Implementationen von abstrakten Datentypen. Module werden meist beschrieben durch eine Spezikation , die das Verhalten und die Eigenschaften des Moduls festlegt. Diese wird getrennt von der Implementation , die die zugehorigen Programme enthalt. Abbildung 7.15: Funktion versus Modul. 8 9 > > > > < Anweisungen = lokale Variable Funktion F > lokale Variable > > ::: > : Die durch Funktionen gewonnene Abstraktion beschrankt sich weitgehend auf das InputOutput Verhalten von Programmteilen, also auf den Datenu . Oft mochte man jedoch weiter gehen und auch ganze Datenstrukturen mit mehreren zugehorigen Variablen, Funktionen und Typen abstrahieren, so wie bei den in Kapitel 5 besprochenen Datenstrukturen. Dies geschieht in sogenannten Modulen . Sie stellen Verallgemeinerungen von Funktionen dar, indem sie eine Kollektion miteinander zusammenhangender Objekte (z. B. mehrere Funktionen, Typen, Konstanten, Variable) zu einer separat compilierten Einheit zusammenfat. Dies ist schematisch in Abbildung 7.15 dargestellt. 7.2 Modulare Abstraktion auch die Rekursionstiefe . Hohe(Aufrufbaum) + 1 Im Zusammenhang mit der Rekursion (Kapitel 8) nennt man die Zahl 2. Die maximale Anzahl von Activation Records auf dem Run-Time-Stack, die ein Ma fur die Gro e des zur Laufzeit beanspruchten Speicherplatzes darstellt, ist gleich der Hohe des Aufrufbaumes+1.11 1. Der Aufrufbaum wird in der Reihenfolge LRW (linker Teilbaum vor rechter Teilbaum vor Wurzel) abgearbeitet. Satz 7.1 (Eigenschaften des Run-Time-Stack und des Aufrufbaumes) Aus den Regeln der Abarbeitung von Scopeblocken ergibt sich bezuglich des Aufrufbaumes und des Stacks folgender Satz. 146 147 13 12 Sprachabhangig kann auch der Zugri auf privat Daten moglich sein. Es sollte jedoch unterbleiben. oder systemabhangig .c,.cpp oder ahnliches 14 Genauer, im Intervall 0 1] gleichverteilt sind. Teilt man also 0 1] in n gleichlange Teilintervalle und erzeugt man N n2 Zufallszahlen, so sollten in jedes Teilintervall ungefahr gleich viele (also N=n) Zufallszahlen fallen. Zufallszahlen sind ein Standardwerkzeug fur die Simulation vieler technischer Ablaufe. Die Aufgabe eines Zufallszahlengenerators ist es, wiederholt (d. h. in der Regel sehr lange Folgen von) Zahlen im Interval 0 1] zu generieren, die den Charakter zufalliger Ziehungen haben.14 7.2.2 Ein Modul fur einen Zufallszahlengenerator Es gibt Sprachen wie Modula oder Erweiterungen von Pascal, die speziell im Hinblick auf Module entworfen worden sind. In C++ erfolgt die Modularisierung durch Files. Ein Modul hat ein Spezikations-File und ein Implementations-File , die nach Konvention durch die Suxe .h und .cc13 gekennzeichnet werden. In der C++ Terminologie wird ein .h File ein Header-File genannt, da es u. a. die Header (d. h. die Prototypen) von Funktionen enthalt. Header Files sollten (bis auf die Denition globaler Konstanten) nur Deklarationen enthalten. Die Verwendung der Header-Files erfolgt uber die #include Praprozessor Direktive. Ein Modul graphics mit Graphik Routinen wurde also in die Files graphics.h und graphics.cc unterteilt. graphics.cc enthalt die Direktive #include "graphics.h". Ein File mit einem Klientenprogramm von graphics enthalt ebenfalls die Direktive #include "graphics.h". Dies illustriert Abbildung 7.16. Die Implementations-Files sollten naturlich genau dieselben Typen, Funktionen usw. wie die zugehorigen header-Files enthalten und die benotigten Denitionen liefern, insbesondere bei Funktionen. 7.2.1 Module in C++ 1. Module sind wiederverwendbar (reusable, o-the-shelve components). 2. Module konnen bei Anderung rekompiliert werden, ohne die Klienten rekompilieren zu mussen. 3. Klienten konnen geandert werden, ohne das Modul andern zu mussen. 4. Module konnen nur als Objektcode zur Verfugung gestellt werden, so da Details der Implementation verborgen bleiben (encapsulation). Die Spezikation ist der oentliche (public ) Teil des Moduls. Klienten konnen (oder sollten) nur die dort deklarierten Begrie nutzen.12 Im privaten (private ) Teil sind die Rumpfe der Funktionen und zusatzliche private Variable enthalten, die nach auen verborgen bleiben (sollten). Man spricht daher auch von Einkapselung (encapsulation ). Wirksame encapsulation setzt voraus, da die Programmiersprache getrennte Compilation von Programmteilen erlaubt, oder uber Konstrukte ermoglicht, die Daten als privat erklaren. Getrennte Compilierung hat viele Vorteile: 7.2. MODULARE ABSTRAKTION 148 . . . #include "graphics.h" . . . #include "graphics.h" .. . Abbildung 7.16: Spezikations-, Implementations- und Klienten-File - clientprog.cc - graphics.cc graphics.h KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN 149 long NextIntRand( /* in */ long limit) // PRE: SetSeed previously invoked at least once // POST: FCTVAL == next pseudorandom integer number // && 1 <= FCTVAL <= limit double NextRand() // PRE: SetSeed previously invoked at least once // POST: FCTVAL == next pseudorandom number // && 0.0 < FCTVAL < 1.0 void SetSeed( /* in */ long initSeed ) // PRE: initSeed >= 1 // POST: Pseudorandom number sequence initialized using initSeed // NOTE: This routine MUST be called prior to NextRand() // and NextIntRand( limit ) rand.h //---------------------------------------------------------------------// SPECIFICATION FILE (rand.h) // This module exports facilities for pseudorandom number generation. // Machine dependency: longs must be at least 32 bits (4 bytes). //---------------------------------------------------------------------- Programm 7.1 liefert dann \Zufallszahlen" im Interval 0 1]. Diese Folge ist naturlich bei festem Startwert x0 alles andere als zufallig, da man alle Werte berechnen kann. Auerdem wird irgendwann ein Wert xr zum zweiten mal auftreten und die Folge wird sich von da ab wiederholen. Man spricht daher auch von Pseudozufallszahlen . Dennoch verhalten sich lange Anfangsstucke dieser Folge angenahert zufallig, so da man sie in Simulationen gut nutzen kann. Die untenstehende Spezikation deklariert Funktionen SetSeed zum Setzen einer Ausgangszahl z aus f1 2 : : : ng, aus der dann x0 als x0 = nz berechnet wird, NextRand zum Erzeugen von xk+1 aus xk , und NextIntRand zum Erzeugen einer zufalligen ganzen Zahl aus f1 2 : : : mg bei vorgegebenen m. x0k := xnk i = 0 1 2 : : : von Zahlen aus f0 1 : : : n ; 1g. Die zugehorige Folge x0 x1 x2 : : : xm xm+1 : : : Erfahrungen (und Uberlegungen der Wahrscheinlichkeitstheorie) zeigen, da sich mit der Funktion f (x) = (a x) mod n 31 mit a = 16807 und n = 2 ; 1 \gute" Zufallszahlen generieren lassen. Man startet mit beliebigem x0 2 f0 1 : : : n ; 1g (der sogenannten seed ) und erzeugt gema xk+1 = f (xk ) eine Folge 7.2. MODULARE ABSTRAKTION KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN * MULTIPLIER zu verhin- // Quotient of MAX / MULTIPLIER // Remainder of MAX / MULTIPLIER 15 Beweis als Ubung. void SetSeed( /* in */ long initSeed ) //.................................................................. // PRE: initSeed >= 1 // POST: 1 <= currentSeed < MAX // NOTE: This routine MUST be called prior to NextRand() // and NextIntRand( limit ) //.................................................................. { initSeed = initSeed % MAX currentSeed = (initSeed > 0) ? initSeed : 1 MULTIPLIER = 16807 MAX = 2147483647 QUOT = 127773 REM = 2836 const const const const long long long long // Updated as a global variable static long currentSeed rand.cc //---------------------------------------------------------------------// IMPLEMENTATION FILE (rand.cc) // This module exports facilities for pseudorandom number generation. // Machine dependency: longs must be at least 32 bits (4 bytes). //---------------------------------------------------------------------#include "rand.h" Programm 7.2 zu bringen15 (dies geschieht als bedingte Anweisung in rand.cc). 1 currentSeed MAX wobei QUOT = MAX / MULTIPLIER und REM = MAX % MULTIPLIER ist. Zum Resultat mu, falls es nicht positiv ist, noch MAX hinzuaddiert werden, um es in den gewunschten Bereich MULTIPLIER * (currentSeed % QUOT) - REM * (currentSeed/QUOT), Die modulo Berechnung wird, um einen Uberlauf bei currentSeed dern, zerlegt in currentSeed = (currentSeed * MULTIPLIER) % MAX Das Implementationsle rand.cc nutzt currentSeed als statische globale Variable, die die momentane Zufallszahl enthalt. Die Benutzung einer globalen Variablen ist hier zulassig, da die Implementation nach auen verborgen bleibt. Auerdem sorgt die Denition dieser Variablen als static dafur, da diese Variable nur im File rand.cc benutzt werden kann. Die Formel xk+1 mod n wird hier zu 150 0 random01 < 1 0 random01 * limit < limit 0 long(random01 * limit) limit ; 1 1 long(random01 * limit) + 1 limit int nach 151 Dabei werden genau die Werte von random01 * limit im Interval i i + 1 auf i gerundet. Die Gleichverteilung der Zufallszahlen auf 0 1] ubersetzt sich daher auf die Gleichverteilung auf f1 2 : : : limitg. Eine mogliche Verwendung des Moduls rand.h zeigt das folgende Programm, das die Gute der Zufallszahlen fur die Simulation eines Wurfelspiels testet. ) ) ) nutzt die Tatsache, da bei der Konvertierung von double zu long unten gerundet wird. Es gilt also NextIntRand } currentSeed = (temp > 0) ? temp : temp + MAX double random01 = double(currentSeed) / double(MAX) return long (random01 * limit) + 1 long NextIntRand( /* in */ long limit ) //.................................................................. // PRE: 1 <= currentSeed < MAX // POST: currentSeed == (currentSeed<entry> * MULTIPLIER) modulo MAX // && 1 <= FCTVAL <= limit // NOTE: This is a prime modulus multiplicative linear congruential // generator that uses the global variable currentSeed //.................................................................. { long temp = MULTIPLIER*(currentSeed%QUOT) - REM*(currentSeed/QUOT) } currentSeed = (temp > 0) ? temp : temp + MAX return double(currentSeed) / double(MAX) double NextRand() //.................................................................. // PRE: 1 <= currentSeed < MAX // POST: currentSeed == (currentSeed<entry> * MULTIPLIER) modulo MAX // && FCTVAL == currentSeed / MAX // NOTE: This is a prime modulus multiplicative linear congruential // generator that uses the global variable currentSeed //.................................................................. { long temp = MULTIPLIER*(currentSeed%QUOT) - REM*(currentSeed/QUOT) } 7.2. MODULARE ABSTRAKTION KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN // output cout << "Wurf Haeufigkeit relative Haeufigkeit \n" for ( i = 1 i <= NO_SIDES i++ ) { cout << setw(3) << i << setw(15) << rollCount"i] << " " << rollCount"i]/double(no_rolls) << endl }//endfor Bitte Anzahl der Wuerfe angeben: 600000 Bitte Anfangszahl (ganze Zahl >= 1) angeben: 100 Der folgende Output zeigt, da der Zufallszahlengenerator eine sehr gleichmaige Verteilung der Augenzahlen 1 bis 6 liefert. } count to 0 generator of times dice // prompt for seed cout << "Bitte Anfangszahl (ganze Zahl >= 1) angeben: " cin >> seed SetSeed(seed) for ( i = 0 i < no_rolls i++ ) { rollCount"NextIntRand(NO_SIDES)]++ }//endfor // prompt for number of rolls cout << "Bitte Anzahl der Wuerfe angeben: " cin >> no_rolls void main() { const NO_SIDES = 6 // number of sides of the int no_rolls = 12000 // number of rolls int rollCount"NO_SIDES + 1] // rollCount"i] == number // that i was rolled long seed // seed for random number int i for ( i = 1 i <= NO_SIDES i++ ) { // initialize roll rollCount"i] = 0 }//endfor rolldice.cc //---------------------------------------------------------------------// rolldice.cc // This program investigates the odds for rolling pairs // of dice by randomly generating such rolls. //---------------------------------------------------------------------#include <iostream.h> #include <iomanip.h> // for setw() #include "rand.h" Programm 7.3 152 Haeufigkeit 99530 100111 99893 100429 99867 100170 relative Haeufigkeit 0.165883 0.166852 0.166488 0.167382 0.166445 0.16695 153 16 Auf Unix Anlagen i. a. unter /usr/include bzw. Unterdirectories hiervon. Dabei ist :: der sogenannte Resolutionsoperator , der den Zugri auf den Namen FktName auch auerhalb des Scopeblocks des Structs StructName erlaubt. Instanzen werden wie Variablen deklariert: RuckgabeTyp StructName :: FktName (Def1 : : : Defk )f: : :g Formal gesehen ist eine Klasse eine Verallgemeinerung eines struct. Structs konnen in ihrer allgemeinen Form auch Funktionen (in C++: member functions oder Methoden) haben. Ist eine solche Funktionsdeklaration bereits eine Denition, so ist sie inline. Ansonsten mu man sie auerhalb der Struct-Denition wie folgt denieren: 7.3.1 Structs in allgemeiner Form C++ bietet ein eigenes Konstrukt zur Abstraktion von Datentypen mit den zugehorigen Operationen: Klassen. Eine Klasse (class ) ist ein durch den Programmierer denierter strukturierter Typ. Seine Komponenten heien class members . Dies konnen Variablen oder Funktionen sein. Da Klassen Typen sind, kann man Variable (in C++: Instanzen ) dieses Typs denieren (in C++: instantiieren ). Diese konnen static oder automatic sein, als Parameter ubergeben werden, und als Funktionswert zuruckgegeben werden. Durch Operator-Overloading ist es moglich, auch die sonst bei eingebauten Typen ublichen Operationen der Zuweisung (=), den Test auf Gleichheit (==) usw. auf Klassen zu ubertragen. 7.3 Daten Abstraktion durch Klassen und andere. Es empehlt sich, die entsprechenden header-Files einmal anzusehen.16 math.h stdlib.h ctype.h string.h assert.h Weitere Beispiele fur Module sind die C bzw. C++ Bibliotheken Wurf 1 2 3 4 5 6 7.3. DATEN ABSTRAKTION DURCH KLASSEN StructTyp S KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN Ausdruck1 : : : Ausdruckn ) class KlassenName f public: PublicDef1 . . . PublicDefr private PrivateDef1 . . . PrivateDefs g g PublicDefr . . . PublicDef1 public: 17 Wir werden spater sehen, da this ein Pointer auf die Instanz ist, mit der die Methode aufgerufen wurde. Statt this->Komponente kann man daher in Pointer Syntax auch *this.Komponente schreiben. this->Komponente In beiden Fallen werden durch PublicDef1 : : : PublicDefr die public-Komponenten und durch PrivateDef1 : : : PrivateDefs die private-Komponenten deniert bzw. deklariert. Auf public deklarierte Komponenten und Funktionen kann man wie bei Structs zugreifen. Auf Komponenten und Methoden, die private deklariert sind, konnen nur die Methoden der Klasse selbst mittels einer vordenierten Variablen this17 zugreifen, und zwar gema oder . . . PrivateDefs class KlassenName f PrivateDef1 Klassen sind eine Verallgemeinerung von structs mit weitgehend ahnlicher Denition. Statt struct wird das Schlusselwort class verwendet. Der Hauptunterschied besteht (im einfachsten Fall) darin, da die Komponenten in einen public-Teil und einen private-Teil aufgeteilt werden konnen. Dafur gibt es zwei gangige Moglichkeiten: 7.3.2 Denition von Klassen S.FktName ( und auf Methoden greift man wie bei Structs ublich mit dem \." zu: 154 155 istack.h //---------------------------------------------------------------------// SPECIFICATION FILE (istack.h) // This module exports an ADT for a stack of ItemTypes //---------------------------------------------------------------------- Programm 7.4 In Kapitel 5.7 haben wir Stacks als abstrakten Datentyp kennengelernt und bereits Memberfunktionen dafur deklariert. Wir fuhren jetzt die gesamte Klasse ein und sehen uns dann eine mogliche Implementation an. 7.3.3 Die Klasse istack in der Klassendenition deklariert. Am Ende des Scopes eines Objekts vom Typ KlassenName wird der Destruktor implizit aufgerufen, um durch das Objekt belegten Speicherplatz wieder freizugeben. Wie bei Modulen, so kann man auch bei Klassen die Spezikation von der Implementation trennen. Ein .h File enthalt die Deklaration der Klasse mit Prototypen aller Methoden, und ein zugehoriges Implementationsle die Details der Implementation. Wir werden diese Technik hier stets verwenden. ~KlassenName () Der Destruktor einer Klasse ist eine Methode ohne Parameter und leerem Ruckgabetyp, die man durch KlassenName ObjektName wobei Ausdri eine mogliche Initialisierung fur Defi ist. Meist wird ein parameterloser Konstruktor als Default Konstruktor genommen. Er wird stets verwendet, wenn eine Instanz ohne Parameter deniert wird, wie in KlassenName Objektanme (Ausdruck1 : : : Ausdruckk ) Man kann dann Objekte vom Typ KlassenName denieren durch KlassenName (Def1 : : : Defk ) Ein nicht erlaubter Zugri resultiert in eine Fehlermeldung beim Compilieren. Klassen stellen daher (neben weiteren Eigenschaften, die wir spater noch kennenlernen werden) ein geeignetes Konstrukt zum encapsulation und information hiding dar. Jede Klasse kann auch Konstruktoren und einen Destruktor haben. Konstruktoren sind Methoden, die implizit aufgerufen werden, wenn eine Instanz der Klasse geschaen wird. Sie sind public, haben denselben Namen wie die Klasse, konnen aber unterschiedliche Parameterlisten haben. Der Ruckgabetyp ist stets void. Die Denition eines Konstruktors lautet also 7.3. DATEN ABSTRAKTION DURCH KLASSEN KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN // define the type of ItemType // maximum number of items in the stack istack.cc //---------------------------------------------------------------------// IMPLEMENTATION FILE (istack.cc) // This module exports an ADT for a stack of ItemType Programm 7.5 Man sieht, da einige Methoden den Zusatz const haben. Diese nennt man in C++ auch const member functions Sie sind dazu da, um den Zustand von privaten Daten zu inspizieren (hier: ist der Stack leer oder voll, was ist das Top-Element), sie aber nicht zu andern . Die Implementation verwendet das private Array data als Reprasentation des Stacks. Daher resultiert auch die beschrankte Anzahl der Stack Items. Erst spater (Kapitel ??) werden wir uber Zeiger richtige unbeschrankte Stacks implementieren. ItemStack() // Constructor // POST: Empty stack created private: int top ItemType data"MAX_LENG] } void Pop() // PRE: NOT IsEmpty() // POST: Top item removed from stack ItemType Top() const // PRE: NOT IsEmpty() // POST: FCTVAL == item at top of stack void Push( /* in */ ItemType newItem ) // PRE: NOT IsFull() && Assigned(newItem) // POST: newItem is at top of stack Boolean IsFull() const // POST: FCTVAL == (stack is full) class ItemStack { public: Boolean IsEmpty() const // POST: FCTVAL == (stack is empty) typedef int ItemType const int MAX_LENG = 100 // DOMAIN: Each stack is a list of ItemType values #include "bool.h" 156 ItemType ItemStack::Top() const void ItemStack::Push( /* in */ ItemType newItem ) //.................................................................. // PRE: top < MAX_LENG-1 && Assigned(newItem) // POST: top == top<entry> + 1 // && data"top<entry>+1] == newItem //.................................................................. { data"++top] = newItem } Boolean ItemStack::IsFull() const //.................................................................. // POST: FCTVAL == (top == MAX_LENG-1) //.................................................................. { return (top == MAX_LENG-1) } Boolean ItemStack::IsEmpty() const //.................................................................. // POST: FCTVAL == (top == -1) //.................................................................. { return (top == -1) } ItemStack::ItemStack() //.................................................................. // Constructor // POST: top == -1 //.................................................................. { top = -1 } // Private members of class: // int top // Subscript of current top item (or -1 if // // stack is empty) // ItemType data"MAX_LENG] // Vector representing the stack // // CLASSINV: -1 <= top < MAX_LENG // Stack representation: a vector. //---------------------------------------------------------------------#include "istack.h" 7.3. DATEN ABSTRAKTION DURCH KLASSEN 157 return data"top] //.................................................................. // PRE: top >= 0 // POST: FCTVAL == data"top] //.................................................................. KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN class FracType { public: void Write() const // POST: The value of this fraction has been displayed as: // <numerator> / <denominator> fraction.h //---------------------------------------------------------------------// SPECIFICATION FILE (fraction.h) // This module exports a FracType class with two operator functions, // operator* and operator==. A more general FracType class // might include more operations. //---------------------------------------------------------------------#include "bool.h" Programm 7.6 In Beispiel 5.2 haben wir Bruche der Form a=b als abstrakten Datentyp betrachtet. Wir sehen uns jetzt eine C++ Klasse an, die diesen Datentyp realisiert (nur ein Teil der moglichen oder denkbaren Operationen). 7.3.4 Die Klasse FracType Man sieht, da die Methoden die privaten Komponenten top und data nutzen, und zwar wie Objekte aus einem ubergeordneten Scope. Da alle Methoden auerhalb der Klasse deniert werden, wird der scope resolution operator :: verwendet. Als default Konstruktor ist die Einrichtung des leeren Stack (aquivalent zu top == -1) vorgesehen. Die Implementation ist naturlich denkbar einfach, da der Stack durch ein Array \simuliert" wird. In Kapitel ?? werden wir diese Implementation durch eine wesentlich bessere ersetzen. Dabei brauchen wir nur den privaten Teil der Klasse und das Implementationsle zu andern. Der public Teil bleibt gleich und daher brauchen alle Klienten auch nichts zu andern. void ItemStack::Pop() //.................................................................. // PRE: top<entry> >= 0 // POST: top == top<entry> - 1 //.................................................................. { top-- } } { 158 with no blanks 159 18 Daneben existiert noch die Schreibweise r.operator*(s) in KlassenSyntax. Naturlich ist r*s wesentlich naturlicher und lesbarer. Neu sind hier die Funktionen mit dem Prax operator, operator* und operator==. Diese Funktionen heien Operator Funktionen . Sie denieren hier neue Bedeutungen der fur int, double etc. bekannten Operatoren * und ==. Man nennt dies Uberladung von Operatoren (operator overloading ). Diese Operatoren konnen dann in der neuen Bedeutung mit der gewohnten Inx-Schreibweise r*s bzw. r == s fur Objekte r, s vom Typ FracType verwendet werden.18 Sie verallgemeinern also die Multiplikation (*) bzw. den Test auf Gleichheit (==) von int auf FracType in naturlicher Weise. Wir sehen uns jetzt die Implementation an. Sie verwendet die privaten Komponenten numer und denom zur Reprasentation eines Bruches. Neben den Member-Funktionen wird noch die zusatzliche Funktion GreatestCommonDivisor deniert, die in Simplify genutzt wird. Sie ist nicht private Methode der Klasse (was moglich ware) sondern wird wie bei Modulen im FracType( /* in */ int initNumer, /* in */ int initDenom ) // Constructor // PRE: Assigned(initNumer) && initDenom > 0 // POST: Fraction has been created and can be thought of // as the fraction initNumer / initDenom // NOTE: (initNumer < 0) --> fraction is a negative number private: int numer int denom } Boolean operator==( /* in */ FracType frac2 ) const // PRE: This fraction and frac2 are in simplest terms // POST: FCTVAL == TRUE, if this fraction == frac2 (numerically) // == FALSE, otherwise FracType operator*( /* in */ FracType frac2 ) const // PRE: This fraction and frac2 are in simplest terms // POST: FCTVAL == this fraction * frac2 (fraction // multiplication), reduced to lowest terms // NOTE: Numerators or denominators of large magnitude // may produce overflow void Simplify() // POST: Fraction is reduced to lowest terms. (No integer > 1 // evenly divides both the numerator and denominator) double FloatEquiv() const // POST: FCTVAL == double equivalent of this fraction // 7.3. DATEN ABSTRAKTION DURCH KLASSEN KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN // Auxiliary function prototype double FracType::FloatEquiv() const //.................................................................. // POST: FCTVAL == double equivalent of this fraction //.................................................................. { return double(numer) / double(denom) } void FracType::Write() const //.................................................................. // POST: Fraction has been displayed as numer/denom with no blanks //.................................................................. { cout << numer << '/' << denom } FracType::FracType( /* in */ int initNumer, /* in */ int initDenom ) //.................................................................. // Constructor // PRE: Assigned(initNumer) && initDenom > 0 // POST: numer == initNumer && denom == initDenom //.................................................................. { numer = initNumer denom = initDenom } // Private members of class: // int numer // int denom // // CLASSINV: denom > 0 int GreatestCommonDivisor( int, int ) fraction.cc //---------------------------------------------------------------------// IMPLEMENTATION FILE (fraction.cc) // This module exports a FracType class with two operator functions, // operator* and operator==. //---------------------------------------------------------------------#include <iostream.h> #include <stdlib.h> // For abs() function #include "fraction.h" Programm 7.7 Implementationsle verborgen. 160 int GreatestCommonDivisor( /* in */ int a, Boolean FracType::operator==( /* in */ FracType frac2 ) const //.................................................................. // PRE: This fraction and frac2 are in simplest terms // POST: FCTVAL == TRUE, if this fraction == frac2 (numerically) // == FALSE, otherwise //.................................................................. { return (numer==frac2.numer) && (denom==frac2.denom) } } resultFrac.Simplify() return resultFrac FracType resultFrac(resultNumer, resultDenom) // ASSERT: New fraction created FracType FracType::operator*( /* in */ FracType frac2 ) const //.................................................................. // PRE: This fraction and frac2 are in simplest terms // POST: FCTVAL == this fraction * frac2 (fraction multiplication) // (WARNING: Overflow is possible) //.................................................................. { int resultNumer = numer * frac2.numer int resultDenom = denom * frac2.denom } if (numer==0 || absNumer==1 || denom==1) return gcd = GreatestCommonDivisor(absNumer, denom) if (gcd > 1) { numer /= gcd denom /= gcd } void FracType::Simplify() //.................................................................. // POST: Fraction is reduced to lowest terms. (No integer > 1 // evenly divides both numer and denom) //.................................................................. { int gcd int absNumer = abs(numer) 7.3. DATEN ABSTRAKTION DURCH KLASSEN 161 // INV (prior to test): // No integer > b evenly divides // both a<entry> and b<entry> } // ASSERT: b == Greatest common divisor of a<entry> and b<entry> return b a = b b = temp temp = a % b int temp = a % b while (temp > 0) { /* in */ int b ) //.................................................................. // PRE: a >= 0 && b > 0 // POST: FCTVAL == Greatest common divisor of a and b // (Algorithm: the Euclidean algorithm) //.................................................................. KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN Die hier gegebene Darstellung lehnt sich stark an HR94] an. Dies gilt insbesondere fur den Modul fur den Zufallszahlengenerator und die Deklaration und Implementation der Klassen istack und fraction. Weitere Beispiele fur Klassen und eine ausfuhrliche Beschreibung aller (hier nicht aufgefuhrten) Feinheiten und Variationen von Klassen in C++ nden sich in Str94]. 7.4 Literaturhinweise } { 162 2. Binare Baume 163 { 0 2 IN { Ist n 2 IN, so auch der Nachfolger n + 1 von n. 1. Naturliche Zahlen Die Menge IN der naturlichen Zahlen kann wie folgt rekursiv deniert werden. Rekursion kommt speziell in mathematischen Denitionen zur Geltung. Bekannte Beispiele sind die naturlichen Zahlen, Baumstrukturen und gewisse Funktionen: Abbildung 8.1: Rekursion im Bild. Ein Objekt heit rekursiv , wenn es sich selbst als Teil enthalt oder mit Hilfe von sich selbst deniert ist. Rekursion kommt nicht nur in der Mathematik, sondern auch im taglichen Leben vor. Wer hat etwa noch nie Bilder gesehen, die sich selbst enthalten? Rekursion Kapitel 8 A A A A =) w r ;@ ; @ ; @ ; @ T1 A T2 A A A A A A A A A Abbildung 8.2: Rekursion bei binaren Baumen. T2 @ @ ;@ ; @ ; ; ; ; ; @ ; @ @ @ @ @ ; @ ; @ Abbildung 8.3: Die nichtleeren binaren Baume mit hochstens 3 Knoten. ; ; r r r r r r r r r r r r r r r r r r r r A A A A A A T1 { Der leere Baum ohne Knoten ist ein binarer Baum (genannt der leere Baum ). { Sind T1 und T2 binare Baume, so ist auch der Graph bestehend aus einer Wurzel w und den Teilbaumen T1 und T2 ein binarer Baum, vgl. Abbildung 8.2. Ist einer der Teilbaume leer, so fehlt die zu ihm fuhrende Kante. Die nichtleeren binaren Baume mit hochstens 3 Knoten sind in Abbildung 8.3 dargestellt. Binare Baume sind gerichtete Baume (vgl. Seite 122), bei denen jeder Knoten Anfangspunkt von hochstens zwei Kanten ist. Die Menge der binaren Baume kann wie folgt rekursiv deniert werden (Ubung). KAPITEL 8. REKURSION ( 1 falls n = 0 n (n ; 1) falls n > 0 Das Wesentliche der Rekursion ist die Moglichkeit, eine unendliche Menge von Objekten durch eine endliche Aussage zu denieren. Auf die gleiche Art kann eine unendliche Zahl von Berechnungen durch ein endliches rekursives Programm beschrieben werden, ohne da das Programm explizite Schleifen enthalt. Rekursive Algorithmen sind hauptsachlich dort angebracht, wo das Problem, die Funktion oder die Datenstruktur bereits rekursiv deniert ist. Ein notwendiges und hinreichendes Werkzeug zur Darstellung rekursiver Programme sind Unterprogramme (also in C++ Funktionen), die sich selbst oder gegenseitig aufrufen konnen. Enthalt eine Funktion f() einen expliziten Aufruf ihrer selbst, so heit f() direkt rekursiv enthalt f() einen Aufruf einer zweiten Funktion g(), die dann ihrerseits f() (direkt oder indirekt) aufruft, so heit f() indirekt rekursiv . Das Vorhandensein von Rekursion mu daher nicht direkt aus der Funktion ersichtlich sein. n! = 3. Die Fakultat n! einer naturlichen Zahl n kann rekursiv deniert werden als 164 165 ( int ggT( /* in */ int a, /* in */ int b) { int temp if ( a < b ){ void main() { int x, y cout << "Bitte natuerliche Zahlen x und y zur Bestimmung von ggT(x,y)" << " eingeben.\nx,y > 0: " cin >> x >> y cout << "ggT(" << x << ',' << y << ") == " << ggT(x,y) << endl } int ggT( /* in */ int a, /* in */ int b) //............................................................ // PRE: a > 0 && b > 0 // POST: FCTVAL == greatest common divisor of a and b // (algorithm: the Euclidean algorithm) //............................................................ ggTrekursiv.cc #include <iostream.h> Programm 8.1 Diese rekursive Darstellung fuhrt direkt zu dem folgenden rekursiven C++-Programm. falls y = 0 ggT (x y) = xggT (y x mod y) falls y > 0 Die Berechnung des ggT erfolgte in Beispiel 3.3 iterativ durch eine while-Schleife. Aufgrund des dort gezeigten Lemma 3.1 gilt auch die folgende rekursive Darstellung des ggT fur naturliche Zahlen x y 1. 8.1.1 Berechnung des ggT Zur Beachtung vorab: manchmal sollte man aus Komplexitatsgrunden statt Rekursion lieber Iteration verwenden. Hierauf wird in Kapitel 8.2 ausfuhrlich eingegangen. 8.1 Beispiele fur Rekursive Algorithmen Wie Wiederholungsanweisungen bergen auch rekursive Funktionen die Gefahr nicht abbrechender Ausfuhrung und verlangen daher die Betrachtung des Problems der Terminierung . Grundlegend ist daher die Bedingung, da der rekursive Aufruf einer Funktion von einer Bedingung B abhangt, die irgendwann nicht mehr erfullt ist. REKURSIVE ALGORITHMEN 8.1. BEISPIELE FUR temp = a a = b b = temp }//endif temp = a % b if ( temp == 0 ) return b else return ggT(b,temp) KAPITEL 8. REKURSION x = t y + r y + r (da t 1) > r + r (da y > r) Also ist r < x2 . Somit wird in der Paarfolge (x y) (y r) (r ) usw., die beim rekursiven Aufruf von ggT entsteht, das groere der beiden Elemente x y nach zwei Aufrufen mehr als halbiert. Also kann es hochstens 1 + 2 log M Aufrufe geben. Beweis: Sei x > y und sei t := x div y und r := x mod y. Dann gilt: ein Aufruf des Programms und ist M := maxfn mg, so gilt fur die Rekursionstiefe (und die Gesamtzahl der Aufrufe) RggT (n m): RggT (n m) 1 + 2 log M Satz 8.1 Das Programm 8.1 berechnet den gro ten gemeinsamen Teiler korrekt. Ist ggT(n m) Die Rekursionstiefe ist die Hohe des Rekursionsbaums plus 1, also 5. Der Run-Time Stack enthalt daher beim \tiefsten" Aufruf 5 Activation Records, die in Abbildung 8.5 dargestellt sind. Wir werden jetzt die Korrektheit des Programms beweisen und die maximale Rekursionstiefe abschatzen. Bei der Korrektheit unterscheiden wir zwischen partieller Korrektheit (beim Terminieren des rekursiven Algorithmus liegt das korrekte Resultat vor) und totaler Korrektheit (bei jedem Aufruf terminiert die Abarbeitung mit dem korrekten Resultat). Abbildung 8.4: Der Rekursionsbaum von Programm 8.1. ggT(4,2) ggT(6,4) ggT(10,6) ggT(76,10) ggT(76,238) Der Aufrufbaum (Rekursionsbaum) ist in Abbildung 8.4 angegeben. Er entartet hier zur Liste, da keine Verzweigung bei den Aufrufen auftritt. } 166 ) ) ) ) Aufruf ggT(76,10) Aufruf ggT(10,6) Aufruf ggT(6,4) Aufruf ggT(4,2) ) return 2 Abbildung 8.5: Der Run-Time-Stack von Programm 8.1. 8 > > < 1. Aufruf > aus main > > > : 0 Rucksprungadresse 5 static pointer: Heap a=6 b=4 temp = 2 Rucksprungadresse 4 static pointer: Heap a = 10 b=6 temp = 4 Rucksprungadresse 3 static pointer: Heap a = 76 b = 10 temp = 6 Rucksprungadresse 2 static pointer: Heap a = 76 b = 238 temp = 10 Rucksprungadresse 1 static pointer: Heap a=4 b=2 temp = REKURSIVE ALGORITHMEN 8.1. BEISPIELE FUR 167 KAPITEL 8. REKURSION Die Sage berichtet, da das Ende der Welt gekommen ist, wenn die Monche ihre Aufgabe beendet haben. Abbildung 8.6: Umschichten von 3 Scheiben. Dieses Problem geht auf eine hinterindische Sage zuruck: in einem im Dschungel verborgenen hinterindischen Tempel sind Monche seit Beginn der Zeitrechnung damit beschaftigt, einen Stapel von 50 goldenen Scheiben mit nach oben hin abnehmendem Durchmesser, die durch einen goldenen Pfeiler in der Mitte zusammengehalten werden, durch sukzessive Bewegungen jeweils einer einzigen Scheibe auf einen anderen goldenen Pfeiler umzuschichten. Dabei durfen sie einen dritten Pfeiler als Hilfspfeiler benutzen, mussen aber darauf achten, da niemals eine Scheibe mit groerem Durchmesser auf eine mit kleinerem Durchmesser zu liegen kommt. Eine Losung fur 3 Scheiben ist in Abbildung 8.6 dargestellt. 8.1.2 Die Turme von Hanoi Der Algorithmus terminiert also bei jedem Aufruf (sogar schnell). Die Korrektheit des dann gelieferten Ergebnisses folgt aus Lemma 3.1, da ja die Operation x mod y auf die fortgesetzte Subtraktion zuruckgefuhrt werden kann. Die t-malige Anwendung von Lemma 3.1 auf x = t y + r ergibt namlich gerade ggT (x y) = ggT (y r). 168 169 i k n;1 i n Abbildung 8.7: Umschichten von n Scheiben. j j k n;1 void Move( /* in */ int numberOfDisks, /* in */ int origin, /* in */ int destination, /* in */ int aux_pile ) //..................................................................... // PRE: numberOfDisks > 0 denotes the number of disks on origin // && origin, destination, aux_pile are pairwise distinct with // values in {1,2,3} // && there are no disks on destination and aux_pile // POST: numberOfDisks disks are on destination // EFFECT: writes the necessary moves on cout in the form Wir denieren nun eine Funktion Move() so, da der Aufruf Move(n,i,j,k) bewirkt, da n Scheiben vom Pfeiler i zum Pfeiler j mit Hilfe des Pfeilers k so umgeschichtet werden, da niemals eine groere auf eine kleinere Scheibe zu liegen kommt. Der oben erlauterte rekursive Zusammenhang zwischen Move(n,...) und Move(n-1,...) fuhrt dann zu folgender Funktion: n Wir wollen uns nun ganz allgemein uberlegen, wie man n Scheiben abnehmender Groe von einem Pfeiler i auf einen Pfeiler j (1 i j 3 i 6= j ) entsprechend der angegebenen Vorschrift umschichten kann. Sei k der dritte zur Verfugung stehende Hilfspfeiler. Dann kann man das Problem, n Scheiben vom Pfeiler i zum Pfeiler j mit Hilfe des Pfeilers k umzuschichten, folgendermaen losen: Man schichtet die obersten n ; 1 Scheiben vom Pfeiler i zum Pfeiler k mit Hilfe des Pfeilers j dann bringt man die auf dem Pfeiler i verbliebene einzige (anfangs unterste) Scheibe (als einzige Scheibe) auf den Pfeiler j . Nun ist der Pfeiler i frei und man kann die n ; 1 Scheiben vom Pfeiler k auf den Pfeiler j mit Hilfe des Pfeilers i umschichten. Dies ist in Abbildung 8.7 dargestellt. REKURSIVE ALGORITHMEN 8.1. BEISPIELE FUR // move the numberOfDisks - 1 smallest disks from aux_pile // to destination with origin as auxiliary file // (the largest disk on destination does not interfere) Move( numberOfDisks - 1, aux_pile, destination, origin) // write the move of the largest disk on cout cout << origin << " --> " << destination << endl // move numberOfDisks - 1 smallest disks from origin to aux_pile // with destination as auxiliary file Move( numberOfDisks - 1, origin, aux_pile, destination) if ( numberOfDisks == 0 ) return // nothing to do // i --> j, with i,j in {1,2,3} //..................................................................... KAPITEL 8. REKURSION --> --> --> --> --> --> --> 2 3 3 2 1 2 2 Beweis: Ubung. zahl der rekursiven Aufrufe 2n+1 ; 1. Satz 8.2 Beim Aufruf von Move(n,1,2,3) ist die Rekursionstiefe n + 1, und die Gesamtan- Die rekursive Losung ist hier besonders elegant, da der Rekursionsansatz einfach ist, wahrend die Herleitung einer iterativen Losung (die der Angabe einer expliziten \Strategie" zum Bewegen der Scheiben gleichkommt) wesentlich schwieriger ist (vgl. Ubung). Der Rekursionsbaum fur den Aufruf von Move(3,1,2,3) ist in Abbildung 8.8 dargestellt. Er hat die Hohe 3. Also hat der Aufruf die Rekursionstiefe 4. Die Zahlen an den Aufrufen geben die Reihenfolge der Aufrufe an. Die zugehorige Belegung des Run-Time stack ist in Abbildung 8.9 wiedergegeben. Sie entspricht dem Durchlauf des Baumes in LRW-Ordnung. Allgemein gilt: 1 1 2 1 3 3 1 Diese Funktion fuhrt bei Aufruf von Move(3,1,2,3) zu folgendem Output: } { 170 171 int A( /* in */ int m, /* in */ int n) //............................................................ // PRE: m >= 0 && n >= 0 // POST: FCTVAL == value of Ackermann function for m,n //............................................................ { if ( m == 0 ) return n+1 else if ( n == 0 ) return A(m-1,1) else return A(m-1,A(m,n-1)) Auch diese Denition lat sich unmittelbar in eine C++ Funktion ubersetzen: 8 > falls m = 0 < n + 1 A(m n) = > A(m ; 1 1) falls m > 0 n = 0 : A(m ; 1 A(m n ; 1)) falls m n > 0 Als Beispiel fur eine rekursive Funktionsdenition komplexerer Art betrachten wir das Beispiel der Ackermann Funktion A, die als Extrapolation der Folge immer starker wachsenden Funktionen Summe, Produkt, Potenz, : : : aufgefat werden kann. Sie ist wie folgt deniert. 8.1.3 Die Ackermann Funktion Abbildung 8.9: Der Run-Time Stack zu Move(3,1,2,3). Die Zahlen sind die Nummern der Aufrufe aus Abbildung 8.8 leer 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 usw. 2 2 2 2 2 2 2 2 2 2 2 2 2 9 9 9 3 3 3 3 3 6 6 6 6 6 10 10 4 5 7 8 11 Abbildung 8.8: Rekursionsbaum zu den Turmen von Hanoi. Move(3,1,2,3) PPP PP 2 9PPPP Move(2,1,3,2) Move(2,3,2,1) ; @ ; @ ; @ ; @ 3 6 10 13 ; @ ; @ Move(1,1,2,3) Move(1,2,3,1) Move(1,3,1,2) Move(1,1,2,3) A A A A 4 5 AA 7 8 AA 11 12 AA 14 15 AA M(0,..) M(0,..) M(0,..) M(0,..) M(0,..) M(0,..) M(0,..) M(0,..) 1 REKURSIVE ALGORITHMEN 8.1. BEISPIELE FUR KAPITEL 8. REKURSION ( A(3 n) > 2n 2 A(4 n) > 22 n mal 10000 A(5 4) > 10 173 Rekursive Algorithmen eignen sich besonders, wenn das zugrunde liegende Problem oder die zu behandelnden Daten rekursiv deniert sind. Das bedeutet aber nicht, da eine solche rekursive Denition eine Garantie dafur bietet, da ein rekursiver Algorithmus der beste Weg zur Losung des Problems ist. Der Aufwand bei rekursiven Aufrufen wird im wesentlichen durch den Aufrufbaum (Rekursionsbaum) bestimmt. Seine Hohe plus 1 ist die Rekursionstiefe . Sie bestimmt die maximale Die Ackermann Funktion wachst sehr stark, und zwar, wie in der Theorie der rekursiven Funktionen oder der Berechenbarkeit gezeigt wird, starker als jede sogenannte primitiv rekursive Funktion (das sind Funktionen mit einem \einfachen" Rekursionsschema). Es gilt z. B. (vgl. Ubung): Oenbar berechnet ulam(a) gerade die Lange der Folge mit a0 = a. Obwohl wir nicht wissen, ob diese Funktion fur jede naturliche Zahl als Input einen Funktionswert liefert, ist die Denition als C++ Funktion naturlich zulassig. Es ist jedoch die (zumindest theoretische) Moglichkeit nicht auszuschlieen, da ein Aufruf der Funktion ulam fur bestimmte Argumente eine nicht abbrechende Folge von rekursiven Aufrufen in Gang setzt. Dies ist zugleich ein Beispiel fur den Fall, da eine formal zulassige Funktionsdenition nicht auch inhaltlich vernunftig sein mu. Man sollte sich stets vergewissern, ob der durch eine Funktionsdenition beschriebene Berechnungsproze fur beliebige Argumente abbricht, also auch fur solche, an die man vielleicht zunachst nicht gedacht hat. 8.2 Wo Rekursion zu vermeiden ist 8 > falls a = 1 < 0 ulam(a) := > 1 + ulam(a=2) falls a gerade a 2 : 1 + ulam(3a + 1) falls a ungerade a 2 : und bricht ab, sobald an = 1 gilt. Zum Beispiel fuhrt a0 = 3 zur Folge 3 10 5 16 8 4 2 1. Es ist oen, ob der Algorithmus bei beliebigen Input stets zum Abbruch fuhrt, d. h. ob die Lange der Folge immer endlich ist. Wir kleiden dies in eine rekursive Funktion. Sei a 2 IN a > 0 an gerade ist, an+1 = a3an =2+ 1 falls sonst, n Diese Funktion wurde mehrfach von Mathematikern untersucht (Klam, Collatz, Kakutani, vgl. LV92]). Sie stellt ein Beispiel fur einen einfachen Algorithmus dar, fur den bis heute nicht bekannt ist, ob er bei allen Eingaben terminiert. Der Algorithmus erzeugt, ausgehend von einer naturlichen Zahl a0 > 0, eine Folge von Zahlen a0 a1 : : : an : : : gema der Vorschrift 8.1.4 Ulams Funktion A(0 n) > n A(1 n) > n + 1 A(2 n) > 2n 8.2. WO REKURSION ZU VERMEIDEN IST Induktion ) und, bei festem m, uber n (innere Induktion ). Induktionsanfang (m = 0): Dann erfolgt unabhangig von n nur ein Aufruf. Also wird terminiert. Induktionsvoraussetzung : Die Behauptung sei richtig fur alle k mit 0 k < m, und fur alle n, d. h. der Aufruf A(k n) terminiert nach endlich vielen Schritten. Schlu auf m durch Induktion uber n (innere Induktion ): Induktionsanfang (n = 0): Dann wird fur A(m 0) der Wert von A(m ; 1 1) zuruckgegeben. Hierfur terminiert der Algorithmus nach Induktionsvorausetzung der aueren Induktion. Induktionsvoraussetzung : Der Aufruf A(m l) terminiert fur (festes) m und alle l < n. Schlu auf n: Der Aufruf von A(m n) erzeugt den Aufruf von A(m ; 1 A(m n ; 1). Nach innerer Induktionsvoraussetzung terminiert der Aufruf A(m n ; 1) und liefert eine Zahl k. Dies erzeugt den Aufruf A(m ; 1 k), der nach auerer Induktionsvorausetzung terminiert. Beweis: Der Beweis erfolgt durch zwei ineinander geschachtelte Induktionen uber m (au ere Satz 8.3 Fur alle m n 2 IN terminiert der Aufruf A(m n) nach endlich vielen Schritten. = 5 A(1 3) = A(0 A(1 2)) = A(0 A(0 A(1 1))) = A(0 A(0 A(0 A(1 0)))) = A(0 A(0 A(0 A(0 1)))) = A(0 A(0 A(0 2))) = A(0 A(0 3)) = A(0 4) Fur die Funktion A ist es bereits viel schwieriger zu sehen, wie (und da uberhaupt) jede Berechnung nach endlich vielen Schritten terminiert. Dies ist zwar der Fall, wie Satz 8.3 zeigt, aber in der Praxis scheitert die Berechnung bereits fur relativ kleine Argumente an der riesigen Rekursionstiefe. So erfordert bereits die Ausrechnung von A(1 3) \per Hand" folgenden Aufwand: } 172 KAPITEL 8. REKURSION n == 0 ) return 0 if ( n == 1 ) return 1 return Fib(n-1) + Fib(n-2) /* in */ int n) Fib(1) Fib(0) Fib(1) Fib(0) Sie stellen eine schnell wachsende Folge von Zahlen dar, die das Wachstum einer Population von sich schnell vermehrenden Lebewesen (Bakterien, Kaninchen) modelliert. Ist fn die Anzahl der \Neugeburten" in Periode n, und reproduzieren sich in einer Periode genau die in den beiden vorherigen Perioden \geborenen" Mitglieder (\gebarfahiges Alter") so entsteht die Folge der Fibonacci-Zahlen als Folge der Geburtenzahlen. Die rekursive Denition fuhrt auf folgende rekursive Funktion: f0 := 0 f1 := 1 fn+1 := fn + fn;1 fur n > 0 : Die Folge f0 f1 f2 : : : der Fibonacci-Zahlen wachst nach dem Gesetz 8.2.4 Berechnung der Fibonacci-Zahlen Induktionsanfang : TFib(n + 1) 2 TFib (n ; 1) 2 2b(n;1)=2c = 2b(n+1)=2c : 2b0=2c = 20 = 1 2b1=2c = 20 = 1 Die erste Ungleichung folgt aus der Tatsache, da Fib(n ; 1) zweimal aufgerufen wird die zweite folgt aus der Induktionsvoraussetzung. Schlu von n auf n + 1: n = 0 : TFib(0) = 1 n = 1 : TFib(1) = 1 Beweis: Dies beweist man durch vollstandige Induktion wie folgt: TFib (n) 2bn=2c fur n 2 : Satz 8.4 TFib(n) wachst mindestens exponentiell. Genauer gilt 8.2.3 Die Turme von Hanoi Die Rekursionstiefe ist bei n Scheiben n + 1, und die Gesamtanzahl der Aufrufe ist 2n+1 ; 1. Dies ist sehr gro, so da eine iterative Losung vorzuziehen ware, wenn sie denn einfach zu nden ware. Die rekursive Struktur des Problems legt jedoch die Verwendung der Rekursion nahe. Beim Aufruf von Fib(n) ist also die Rekursionstiefe n, und fur die Gesamtanzahl TFib (n) der rekursiven Aufrufe gilt Abbildung 8.10: Rekursionsbaum zu der Fibonaccifolge. Fib(2) Fib(1) HH H Fib(1) Fib(0) 175 Fib(5) ( hhhh (((( hhh hh (((( Fib(4) Fib(3) X X X XXX XXXX X X Fib(3) Fib(2) Fib(2) Fib(1) HH HH HH H H H Beim Aufruf von Fib(5) ergibt sich der in Abbildung 8.10 dargestellte Rekursionsbaum. int Fib( { if ( else else } 8.2. WO REKURSION ZU VERMEIDEN IST Der rekursive Algorithmus in Kapitel 8.1.1 fuhrt zu einer Rekursionstiefe von 1+2log maxfn mg (Satz 8.1). Dies ist akzeptabel. Allerdings ist der Rekursionsbaum wieder eine Liste, do da man auch einen iterativen Algorithmus verwenden konnte (vgl. Algorithmus 3.2). 8.2.2 Berechnung des groten gemeinsamen Teilers fuhrt zur Rekursionstiefe n + 1, die deutlich zu gro ist. Der Rekursionsbaum entartet zudem zu einer Liste (keine Verzweigungen), was meist ein Zeichen dafur ist, da es auch einen einfachen iterativen Algorithmus gibt (vgl. Beispiel 7.1). falls n = 0 Fak(n) := 1n Fak(n ; 1) falls n > 0 ( Die Implementation der Fakultatsfunktion gema der rekursiven Denition 8.2.1 Berechnung der Fakultat Gro e des Run-Time-Stacks , der durch den ersten Aufruf verursacht wird, und bestimmt damit den benotigten Speicherplatz . Die Gesamtanzahl der Knoten im Aufrufbaum bestimmt die Anzahl aller rekursiven Aufrufe . Sie ist ein Ma fur die benotigte Laufzeit . Als Generalregel sollte man daher Rekursion immer dann vermeiden, wenn der benotigte Speicherplatz (also die Rekursionstiefe) oder die benotigte Laufzeit (also die Anzahl der Knoten des Rekursionsbaums) zu gro werden. Wir erlautern dies an einigen Beispielen. 174 KAPITEL 8. REKURSION int currentFib = 1, prevFib = 0 for (int i = 1, i < n i++){ currentFib = currentFib + prevFib prevFib = currentFib - prevFib }\\endfor Die hier gegebene Darstellung lehnt sich in Teilen an Wir86] an. Von dort ist auch Abbildung 8.1 entnommen. Die Darstellung der Turme von Hanoi folgt OW82]. Der Artikel LV92] gibt vertiefende Informationen (und Berechnungen) zu Ulams Funktion. Weitere Beispiele fur Rekursion nden sich in nahezu allen Buchern uber Algorithmen und Datenstrukturen. 8.3 Literaturhinweise Die Folgerung aus diesen Uberlegungen ist, da man auf die Verwendung von Rekursion immer dann verzichten sollte, wenn es eine oensichtliche Losung mit Iteration gibt. Das bedeutet aber nicht, da Rekursion um jeden Preis zu umgehen ist. Es gibt viele gute Anwendungen fur Rekursion, wie die folgenden Kapitel zeigen werden. Die Tatsache, da Implementationen von rekursiven Funktionen auf nicht-rekursiven Maschinen existieren, beweist, da gegebenenfalls jedes rekursive Programm in ein rein iteratives umgeformt werden kann. Dies verlangt jedoch das explizite Verwalten eines Rekursions-Stacks. Durch diese Operationen wird das Grundprinzip eines Programms oft so sehr verschleiert, da es schwer zu verstehen ist. Zusammenfassend lat sich sagen, da Algorithmen, die ihrem Wesen nach eher rekursiv als iterativ sind, tatsachlich als rekursive Funktionen formuliert werden sollten. 8.2.5 Zusammenfassung Dabei spielen die Variablen currentFib und prevFib die Rolle von fn und fn;1 , die in beiden Zuweisungen zu fn+1 = fn + fn;1 und fn = fn+1 ; fn;1 aktualisiert werden. Man vergleiche einmal die Laufzeit beider Varianten fur Zahlen n ab n = 35. } { Der Aufwand ist also ahnlich wie bei den Turmen von Hanoi. Wahrend jedoch bei den Turmen von Hanoi stets andere Teilprobleme in den rekursiven Aufrufen berechnet werden, tauchen hier dieselben Teilprobleme wiederholt auf. Eine solche Mehrfachberechnung identischer Teilprobleme sollte man unbedingt vermeiden. Bei den Fibonacci-Zahlen geht dies durch folgende iterative Variante. Der letzte else Teil in Fib wird dabei ersetzt durch 176 A priori Analyse (rechnerunabhangig) 177 Man unterscheidet folgende Arten der Analyse. 9.1 Analysearten Ziel der Analyse ist die Ezienzuntersuchung und die Entwicklung ezienterer Algorithmen Die Analyse soll zunachst rechnerunabhangig durchgefuhrt werden. Man braucht also ein geeignetes Rechnermodell. Die Annahmen uber das Rechnermodell konnen wichtige Konsequenzen haben in bezug auf die Frage, wieviel Zeit man zur Losung eines Problems braucht. Es existieren zwar formale Rechnermodelle (z. B. Turingmaschinen oder Maschinen mit wahlfreiem Speicherzugri (random access machines)), wir werden jedoch fast immer einen \ganz gewohnlichen Rechner" zugrunde legen. Damit ist gemeint, da Befehle eines Programms zeitlich nacheinander (sequentiell ) ausgefuhrt werden, und da die Kosten eines Algorithmus im wesentlichen abhangen von der Anzahl der erforderlichen Operationen. Wir nehmen an, da ein sogenanntes RAM (Random Access Memory) zur Verfugung steht, also ein Speicher mit wahlfreiem Zugri. Mit diesem Speicher ist es moglich, jedes beliebige Objekt eines unstrukturierten Datentyps (also int, double usw. in einer fest vorgegebenen Zeitspanne zu speichern bzw. auszulesen. Bei der Analyse von Algorithmen besteht die erste Aufgabe darin festzustellen, welche Operationen verwendet werden und wie hoch ihre relative Kosten sind. Typische Operationen sind z. B. die vier Grundrechenarten Addition, Subtraktion, Multiplikation und Division, angewendet auf ganze Zahlen. Andere elementare Operationen sind arithmetische Operationen mit Gleitkommazahlen, Vergleichsbefehle, Wertzuweisungen an Variablen und ggf. Funktionsaufrufe. Elementare Operationen benotigen typischerweise nie mehr als eine gewisse feste Zeitspanne zur Ausfuhrung wir sagen, da ihre Ausfuhrungszeit durch eine Konstante beschrankt ist. Dies trit nicht mehr zu fur strukturierte Daten, z. B. fur Vergleich von Strings, die abhangig von der Lange der Strings ist, oder die Suche des maximalen Elementes in einem Array. Die Analyse von Algorithmen Kapitel 9 (a) (b) for (i=0 i<n i++) x = x+y (c) Bei der a priori Analyse der Rechenzeit werden alle Faktoren auer acht gelassen, die von der Maschine oder der Programmiersprache abhangig sind. Man konzentriert sich ganz auf die Bestimmung der Groenordnung der Haugkeit von Anweisungen. Es gibt mehrere Arten der mathematischen Notation, die sich hierfur anbieten. Eine davon ist die O-Notation: 9.2 Die Asymptotische Notation In allen Fallen nehmen wir an, da die Anweisung x = x+y in keiner anderen Schleife steht als in den hier gezeigten. Daher ist im Segment (a) die Haugkeit dieser Anweisung 1. Im Segment (b) ist sie n, und im Segment (c) ist sie n2 . Diese Haugkeiten 1 n n2 unterscheiden sich um Groenordnungen. Der Begri \Groenordnung" ist uns allen vertraut so unterscheiden sich z. B. Gehen, Autofahren und Fliegen durch verschiedene Groenordnungen bzgl. der Entfernung, die ein Mensch in einer Stunde zurucklegen kann. Im Zusammenhang mit der Algorithmenanalyse bezieht sich die Groenordnung einer Anweisung auf die Haugkeit ihrer Ausfuhrung, wahrend die Groenordnung eines Algorithmus sich auf die Summe der Haugkeit aller seiner Anweisungen bezieht. Die a priori Analyse beschaftigt sich hauptsachlich mit der Bestimmung von Groenordnungen. Glucklicherweise gibt es eine bequeme mathematische Notation, die diesem Begri entspricht. x = x+y for (i=0 i<n i++) for (j=0 j<n j++) x = x+y { worst-case Komplexitat : Dies ist eine obere Schranke fur die Ausfuhrungszeit (in Form von Anzahl der auszufuhrenden Operationen) in Abhangigkeit der Groe des Inputs, gemessen in relevanten Parametern (z. B. die Anzahl der zu sortierenden Objekte, Stellenzahl von Zahlen, etc.) { Mittlere Komplexitat : Dies ist eine obere Schranke fur die mittlere Ausfuhrungszeit bei gewissen (Wahrscheinlichkeits-) Annahmen uber das Auftreten der Problemdaten { Untere Komplexitatsschranken : Hierunter versteht man die Ermittlung unterer Schranken fur die (worst-case oder mittlere) Ausfuhrungszeit. Im Idealfall liegen obere und und untere Schranke \dicht" zusammen. Dies ist meist jedoch sehr schwer zu erreichen. Hierfur hat sich eine eigene Disziplin, die Komplexitatstheorie entwickelt. A posteriori Analyse (rechnerabhangig) Hierunter versteht man das Testen einer Implementation des Algorithmus an hinreichend groen Datensatzen, so da \alle" Verhaltensweisen des Algorithmus auftreten. Man erstellt dann eine Sammlung statistischer Daten uber Ziel- und Speicherbedarf in Abhangigkeit des Datenmaterials. KAPITEL 9. DIE ANALYSE VON ALGORITHMEN Als Beispiel betrachten wir drei Programmsegmente 178 179 n Um Groenordnungen unterscheiden zu konnen, gibt es die o-Notation (lies: klein oh Notation). Seien f g : IN ! IN. Dann ist f (n) = o(g(n)), wenn f (n) = O(g(n)) und nicht g(n) = O(f (n)). { Fur groe n ist die Groenordnung allein magebend. Z. B. ist bei 104 n und n2 die erste Laufzeit fur groe n (n 104 ) zu bevorzugen. { Konstanten und Terme niedrigerer Ordnung hangen von vielen Faktoren ab, z. B. der gewahlten Sprache oder der verwendeten Maschine, und sind daher meist nicht maschinenunabhangig. Die asymptotische Notation vernachlassigt Konstanten und Terme niedriger Ordnung (wie z. B. die Terme ak nk mit k < m im Polynom). Dafur gibt es zwei gute Grunde: Setzt man c = jam j + : : : + ja0 j und n0 = 1, so folgt unmittelbar die Behauptung. jam j + : : : + ja0 j)nm n 1 : n jf (n)j jam jnm + : : : + ja1 jn + ja0 j = (jam j + jam;1 j + : : : + jam0 j )nm Beweis: Wir benutzen die Denition von f (n) und eine einfache Ungleichung: Satz 9.1 Fur ein Polynom f (n) = amnm + : : : + a1 n + a0 vom Grade m gilt: f (n) = O(nm ). Seien f g : IN ! IN. Dann ist f (n) = O(g(n)) (gesprochen: \f von n ist gleich gro Oh von g von n"), wenn zwei positive Konstanten c 2 IR und n0 2 IN existieren, do da fur alle n n0 gilt: jf (n)j c jg (n)j Eine andere gebrauchliche Schreibweise hierfur ist f (n) 2 O(g(n)). Nehmen wir an, wir ermitteln die Rechenzeit f (n) fur einen bestimmten Algorithmus. Die Variable n kann z. B. die Anzahl der Ein- und Ausgabewerte sein, ihre Summe, oder auch die Groe eines dieser Werte. Da f (n) maschinenabhangig ist, genugt eine a priori Analyse nicht. jedoch kann man mit Hilfe einer a priori Analyse ein g(n) bestimmen, so da f (n) = O(g(n)). Wenn wir sagen, da ein Algorithmus eine Rechenzeit O(g(n)) hat, dann meinen wir damit folgendes: Wenn der Algorithmus auf unterschiedlichen Computern mit den gleichen Datensatzen lauft, und diese die Groe n haben, dann werden die resultierenden Laufzeiten immer kleiner sein als eine Konstante mal jg(n)j. Bei der Suche nach der Groenordnung von f (n) werden wir darum bemuht sein, das kleinste g(n) zu nden, so da f (n) = O(g(n)) gilt. Ist f (n) z. B. ein Polynom, so gilt: 9.2.1 Obere Schranken 9.2. DIE ASYMPTOTISCHE NOTATION KAPITEL 9. DIE ANALYSE VON ALGORITHMEN Laufzeitfunktion fur ein Problem, und sei n0 die Problemgroe, die man mit der jetzigen Technologie in einer bestimmten Zeitspanne t losen kann. (Z. B. Berechnen kurzester Wege Beispiel 9.1 (Vergleich der Groenordnungen polynomial/exponentiell) Sei f (n) die Folgendes Beispiel verdeutlicht den drastischen Unterschied zwischen polynomialem und exponentiellem Wachstum. Es zeigt, da selbst enorme Fortschritte in der Rechnergeschwindigkeit bei exponentiellen Laufzeitfunktionen honungslos versagen. log n n n log n n2 n3 2n 0 1 0 1 1 2 1 2 2 4 8 4 2 4 8 16 64 16 3 9 24 64 512 256 4 16 64 256 4096 65536 5 32 160 1024 32768 4294967296 Tabelle 9.1: Wachstum verschiedener Groenordnungen. Dabei bedeutet O(f (n)) < O(g(n)), da f (n) = o(g(n)), f (n) also von kleinerer Groenordnung als g(n) ist. O(1) bedeutet, da die Anzahl der Ausfuhrungen elementarer Operationen unabhangig von der Groe des Inputs durch eine Konstante beschrankt ist. Die ersten sechs Groenordnungen haben eine wichtige Eigenschaft gemeinsam: sie sind durch ein Polynom beschrankt. (Sprechweisen: polynomial beschrankt, polynomial, schnell, ezient ). O(n) O(n2 ) und O(n3 ) sind selbst Polynome, die man|bzgl. ihrer Grade|linear , quadratisch und kubisch nennt. Es gibt jedoch keine ganze Zahl m, so da nm eine Schranke fur 2n darstellt, d. h. 2n 62 O(nm ) fur jede feste ganze Zahl m. Die Ordnung von 2n ist O(2n ). Man sagt, da ein Algorithmus mit der Schranke O(2n ) einen exponentiellen Zeitbedarf hat. Fur groe n ist der Unterschied zwischen Algorithmen mit exponentiellem bzw. durch ein Polynom begrenztem Zeitbedarf ganz betrachtlich. Es ist eine groe Leistung, einen Algorithmus zu nden, der statt eines exponentiellen einen durch ein Polynom begrenzten Zeitbedarf hat. Tabelle 9.1 zeigt, wie die Rechenzeiten der sechs typischen Funktionen anwachsen, wobei die Konstante gleich 1 gesetzt wurde. Wie man feststellt, zeigen die Zeiten vom Typ O(n) und O(n log n) ein wesentlich schwacheres Wachstum als die anderen. Fur sehr groe Datenmengen ist dies oft das einzig noch verkraftbare Wachstum. Die gangigsten Groenordnungen fur Rechenzeiten sind: O(1) < O(log n) < On < O(n log n)O(n2) < O(nk ) fur k 2 IN fest, k 3 < O(nlog n ) < O(2n) f heit dann von (echt) kleinerer Gro enordnung als g. Statt f (n) = o(g(n)) schreibt man gelegentlich auch f (n) 2 o(g(n)). 180 181 Beispiel 9.2 (Sequentielle Suche) Sei f (n) die Anzahl von Vergleichen bei der sequentiellen Suche in einem unsortierten Array mit n Komponenten (vgl. Kapitel 6.1.1). Dann ist f (n) = O(n), da man je mit n Vergleichen auskommt. Andererseits mu man aber auch jede Komponente uberprufen, denn ihr Wert konnte ja der gesuchte Wert sein. Also ist f (n) = %(n) und damit f (n) = &(n). Die O-Notation dient der Beschreibung oberer Schranken. Groenordnungen fur untere Schranken werden mit der %-Notation ausgedruckt. Seien f g : IN ! IN. Dann ist f (n) = %(g(n)) (gelesen:\f von n gleich Omega von g von n"), wenn es positive Konstanten c 2 IR und n0 2 IN gibt, so da fur alle n n0 gilt: jf (n)j c jg (n)j. Manchmal kommt es vor, da fur die Laufzeit f (n) eines Algorithmus gilt: f (n) = %(g(n)) und f (n) = O(g(n)). Dafur benutzen wir folgende Schreibweise. Seien f g : IN ! IN. Dann ist f (n) = &(g(n)) (gelesen:\f von n gleich theta von g von n"), wenn es positive Konstante c1 c2 2 IR und n0 2 IN gibt, so da fur alle n n0 gilt: c1 jg(n)j f (n) c2 jg(n)j. Falls f (n) = &(g(n)) gilt, dann ist g(n) sowohl eine obere, als auch eine untere Schranke fur f (n). Das bedeutet, da die beiden Extremfalle{der beste und der schlechteste Fall| die gleiche Zeit brauchen (bis auf einen konstanten Faktor). 9.2.2 Untere Schranken d. h., man kann jetzt nur eine um den additiven Term log(100) groere Probleme in derselben Zeit losen. Der Fortschritt macht sich also kaum bemerkbar. 2n? = 100 2n0 , also n? = n0 + log(100) n0 + 7 d. h., man kann jetzt eine um den Faktor k 100 groere Probleme in derselben Zeit losen. Ist dagegen die Laufzeitfunktion f (n) exponentiell, etwa f (n) = 2n , so folgt p nk? = 100 nk0 , also n? = k 100 n0 p Ist die Laufzeitfunktion f (n) polynomial, etwa f (n) = nk , k fest, so folgt f (n?) = 100 f (n0) : in einem Graphen mit n Knoten, t = 60 Minuten. Dann ist n0 die Groe der Graphen, fur die in einer Stunde die kurzesten Wege berechnet werden konnen.) Wir stellen jetzt die Frage, wie n0 wachst, wenn die Rechner 100 mal so schnell werden. Sei dazu n? die Problemgroe, die man auf den schnelleren Rechnern in der gleichen Zeitspanne t losen kann. Oenbar erfullt n0 die Gleichung f (n0 ) = t bei der alten Technologie, und f (n0) = t=100 bei der neuen Technologie. Da n? bei der neuen Technologie f (n?) = t erfullt, ergibt sich 9.2. DIE ASYMPTOTISCHE NOTATION KAPITEL 9. DIE ANALYSE VON ALGORITHMEN ) Def ) ) ) Def 1 1 1 c ) > 0 n0 := n1 2 IN mit g(n) cf(n) 8n n0 g(n) = O(f (n)) 9c(:= > 0 n1 2 IN mit f(n) c1 g(n) 8n n1 1 9c1 > 0 n1 2 IN mit g(n) f(n) 8n n1 c 9c1 ) ) Def ) ) Def 2 f (n) = %(g(n)) 1 Die schnellsten bekannten Algorithmen zur Matrixmultiplikation kommen mit O(n2 376 ) Operationen aus. 2 1 c > 0 n0 := n2 2 IN mit f(n) c g(n) 8n n0 > 0 n2 2 IN mit g(n) c2 f(n) 8n n2 > 0 n2 2 IN mit f(n) c1 g(n) 8n n2 9c := 9c2 9c2 im Widerspruch zur Voraussetzung f (n) 6= %(g(n)). f (n) = O(g(n)) Def Dies ist ein Widerspruch dazu, da g(n) 6= O(f (n)). \(" Sei f (n) = O(g(n)) und f (n) 6= %(g(n)). Zu zeigen ist f (n) = o(g(n)), d. h. (nach Denition) f (n) 2 O(g(n)) (was nach Voraussetzung erfullt ist) und g(n) 6= O(f (n)). Angenommen, dies sei doch der Fall. Dann folgt f (n) = %(g(n)) Sei f (n) = o(g(n)). Nach Denition ist dann f (n) = O(g(n)) und nicht g(n) = O(f (n)). Zu zeigen ist also noch: :f (n) = %(g(n)) Angenommen, dies sei doch der Fall. Dann folgt Beweis:\)": Lemma 9.1 f (n) = o(g(n)) genau dann, wenn f (n) = O(g(n)) ^ : f (n) = %(g(n))]]: Bezuglich der eingefuhrten o-Notation gilt: ergibt sich die Berechnung eines Eintrags cij von C = A B gema cij = nk=1 aik bkj (vgl. Kapitel 6.2.1. Sie erfordert also n Multiplikation und n ; 1 Additionen. Insgesamt sind fur ganz C also n2 Eintrage cij zu berechnen, und somit n2 (n + n ; 1) = 2n3 ; n2 = O(n3 ) arithmetische Operationen insgesamt auszufuhren.. Da jeder Algorithmus fur die Matrixmultiplikation n2 Eintrage berechnen mu, folgt andererseits, da jeder Algorithmus zur Matrixmultiplikation von zwei n n Matrizen %(n2 ) Operationen benotigt. Es klat zwischen %(n2 ) und O(n3 ) also noch eine \Komplexitatslucke".1 Beispiel 9.3 (Matrizenmultiplikation) Bei der Matrixmultiplikation P von n n Matrizen 182 183 n == 0 ) return 0 if ( n == 1 ) return 1 return Fib(n-1) + Fib(n-2) /* in */ int n) 3 2 vgl. die entsprechenden Manual pages durch Aufruf von man vgl. die entsprechenden Manual pages durch Aufruf von man int Fib2( /* in */ int n) { if ( n == 0 ) return 0 else if ( n == 1 ) return 1 else { int Fib( { if ( else else } const int TIME_UNIT = 1000000 zeitmessung.cc #include <iostream.h> #include <sys/time.h> #include <sys/resource.h> Programm 9.1 . time 3 clock bzw. man 3 getrusage . Das folgende Programm zeigt die Verwendung von time.h zum Vergleich des Zeitaufwandes fur die rekursive und iterative Variante zur Berechnung der Fibonacci Zahlen. clock_t clock() // measures times in microseconds und die Funktion typedef unsigned long clock_t Nehmen wir an, wir haben ein Programm zur Losung eines Problems entworfen, kodiert, als korrekt bewiesen und am Rechner die Fehlersuche erfolgreich durchgefuhrt. Wie konnen wir ein Leistungsprol erstellen, welches exakt den Rechenzeit- und Speicherbedarf dieses Programms angibt? Um exakte Zeiten zu erhalten, mu unser Computer uber eine Uhr verfugen, von der die Zeit per Programm abgelesen werden kann. Mit dieser Moglichkeit der Zeitmessung konnen viele Faktoren der Programmausfuhrung uberpruft werden. Der wichtigste Test eines Programms besteht darin, zu zeigen, da die fruhere Analyse bzgl. der Groenordnung richtig war. Mit Hilfe der tatsachlich gemessenen Zeiten sollten wir in der Lage sein, die exakte Laufzeitfunktion in Abhangigkeit von der benutzten Programmiersprache und dem Rechner zu bestimmen. Zur Zeitmessung existieren UNIX-utilities wie /bin/time2 aber auch C-bzw. C++-Module wie time.h bzw. resource.h.3 So enthalt time.h den Typ 9.3 A posteriori Analyse, Laufzeitmessungen 9.3. A POSTERIORI ANALYSE, LAUFZEITMESSUNGEN int currentFib = 1, prevFib = 0 for ( int i = 1 i < n i++ ) { currentFib = currentFib + prevFib prevFib = currentFib - prevFib }//endfor return currentFib } KAPITEL 9. DIE ANALYSE VON ALGORITHMEN Eine gute Einfuhrung in asymptotische Notation mit vielen Beispielen und Abschatzungstechniken ndet sich in CLR90]. 9.4 Literaturhinweise void main() { int n, number cout << "Bitte Zahl n eingeben: " cin >> n clock_t start, end start = clock() number = Fib(n) end = clock() cout << "Die " << n << "-te Fibonacci Zahl ist: " << number << endl << "Berechnung durch Rekursion: " << (end - start)/double(TIME_UNIT) << " sec." << endl start = clock() number = Fib2(n) end = clock() cout << "Berechnung durch Iteration: " << (end - start)/double(TIME_UNIT) << " sec." << endl } } 184 185 1 In Kapitel ?? werden wir uber Templates in C++ eine Moglichkeit sehen, abstrakte Sortieralgorithmen zu schreiben, die die Art der Vergleiche (ganze Zahlen, Strings) auslagern. Direkt bedeutet: Sortieren der Komponenten \am Ort". Ein typisches Beispiel dafur ist das 10.1 Direkte Methoden Objekte dieses Typs sind \Karteikarten", z. B. aus einer Studentenkartei. Jede Karteikarte enthalt neben den Daten (data components) einen Schlussel (key) vom Typ int, z. B. die Matrikelnummer, nach denen sortiert oder gesucht werden kann. Die gesamte Kartei wird durch ein Array vec mit Grundtyp Item dargestellt. Wir reden daher im weiteren auch von Komponenten oder Elementen statt Karteikarten. Die Wahl von int als Schlusseltyp ist willkurlich. Hier kann jeder andere Typ verwendet werden, fur den eine vollstandige Ordnungsrelation deniert ist, also zwischen je zwei Werten a b genau eine der Relationen a < b a = b a > b gilt. Dies konnen z. B. auch Strings mit der lexikographischen Ordnung sein (Meier < Mueller), nur mute dann der \eingebaute" Vergleich \<" von ganzen Zahlen durch eine selbstdenierte Funktion zum Vergleich von Strings ersetzt werden.1 struct Item { int key // data components } Sortieralgorithmen gehoren zu den am haugsten angewendeten Algorithmen in der Datenverarbeitung. Man hatte daher bereits fruh ein groes Interesse an der Entwicklung moglichst ezienter Sortieralgorithmen. Zu diesem Thema gibt es umfangreiche Literatur, nahezu jedes Buch uber Algorithmen und Datenstrukturen beschaftigt sich mit Sortieralgorithmen, da sie besonders geeignet sind, Anfangern Programmiermethodik, Entwurf von Algorithmen, und Aufwandsanalyse zu lehren. Wir erlautern die Sortieralgorithmen vor folgendem Hintergrund. gegeben ist ein Datentyp Item. Sortieren in Arrays Kapitel 10 KAPITEL 10. SORTIEREN IN ARRAYS 12 63 24 18 53 72 35 44 i=1 2 12 18 63 24 35 53 72 44 3 12 18 24 63 35 44 53 72 4 12 18 24 35 63 44 53 72 5 12 18 24 35 44 63 53 72 6 12 18 24 35 44 53 63 72 Abbildung 10.1: Phasen bei Bubblesort. 63 24 12 53 72 18 44 35 vec"j].key 7 12 18 24 35 44 53 63 72 struct Item { int key Am Ende der Phase i ist vec"i-1].key der i-kleinste Schlussel in vec und (10.1) es gilt: vec"0].key vec"1].key : : : vec"i-1].key vec"j].key fur j = i i + 1 : : :vSize-1. Dies ist klar fur die 1. Phase. Nimmt man die Richtigkeit fur Phase i an (Induktionsvoraussetzung), so ndet Phase i + 1 das kleinste Element in vec"i]...vec"vSize-1] und bringt es durch ggf. fortgesetzte Austauschoperationen an die Position i. Also gilt die Invariante auch nach Phase i (Induktionsschlu). Fur i = vSize ; 1 folgt sofort die Korrektheit des Algorithmus. Dies resultiert in die folgende C++ Funktion. Die Korrektheit des Algorithmus ergibt sich direkt aus folgender Schleifeninvarianten , die nach jeder Phase gilt: 1. Gegeben ist vec"] mit vSize Komponenten. 2. Das Array vec wird vSize ; 1 mal von hinten nach vorn durchlaufen. Ein Durchlauf heit Phase . 3. Phase i lauft von Komponente j = vSize ; 1 bis j = i und vergleicht jeweils vec"j].key mit vec"j-1].key. Ist vec"j-1.key] > vec"j].key] so werden vec"j-1] und vec"j] getauscht. Informell lat sich Bubblesort wie folgt beschreiben: 0 1 2 3 4 5 6 7 j Bubblesort durchlauft das Array mehrmals und lat durch paarweise Vergleiche das kleinste Element der restlichen Menge zum linken Ende des Arrays wandern. Stellt man sich das Array senkrecht angeordnet vor, und die Elemente als Blasen, so steigt bei jedem Durchlauf durch das Array eine Blase auf die ihrem Gewicht (Schlusselwert) entsprechende Hohe auf (vgl. Abbildung 10.1) 10.1.1 Sortieren durch Austauschen: Bubblesort 186 187 Vergleich i=1 nX ;1 i = n(n2; 1) : Folglich ist C (n) = O(n2 ). Da die Vergleiche unabhangig von der Eingabe durchgefuhrt werden (auch bei einem bereits sortierten Array) gilt: C (n) = &(n2 ). Oensichtlich kann dieser Algorithmus verbessert werden, wenn man sich merkt, ob in einer Phase uberhaupt ein Austausch stattgefunden hat. Findet kein Austausch statt, so ist das Array sortiert und man kann abbrechen. Eine weitere Verbesserung besteht darin, sich in einer Phase die Position (Index) k des letzten Austausches zu merken. In den darauf folgenden Phasen mussen vec"0]...vec"k] nicht mehr uberpruft werden. Schlielich kann man noch die Phasen abwechselnd von hinten nach vorn und von vorn nach hinten laufen lassen (Shakersort ), um Asymmetrie zwischen \leichten" Elementen (gehen gleich ganz nach oben) und \schweren" Elementen (sinken jeweils nur um eine Position ab) zu durchbrechen. C (n) = 1 + 2 + : : : + (n ; 2) + (n ; 1) = Also gilt fur die Anzahl C (n) der Vergleiche bei n Komponenten Phase n ; 1 : ::: 1 : n ; i Vergleiche Phase i ::: : n ; 1 Vergleiche : n ; 2 Vergleiche Phase 2 Phase 1 Wir berechnen nun den Worst Case Aufwand von Bubblesort nach der Anzahl der Vergleiche . Dazu betrachten wir die Vergleiche pro Phase. Sei n = vSize. void bubblesort( /* inout */ Item vec"], /* in */ int vSize) //............................................................ // PRE: unsorted array vector // POST: vec"0].key <= vec"1].key <= ... vec"vSize-1].key //............................................................ { for ( int i = 1 i < vSize i++ ) for ( int j = vSize-1 j >= i j-- ) if ( vec"j-1].key > vec"j].key ) { Item temp = vec"j-1] vec"j-1] = vec"j] vec"j] = temp } } // data components } 10.1. DIREKTE METHODEN KAPITEL 10. SORTIEREN IN ARRAYS void StraightSelectionSort( /* inout */ Item vec"], /* in */ int vSize ) //...................................................................... // PRE: Assigned(vSize) && Assigned(vec"0].key ... vec"vSize-1].key) // POST: vec"0].key <= vec"1].key <= ... vec"vSize-1].key // and the set of values is the same //...................................................................... { int minIndx // Index of smallest key in each pass int bottom // bottom for each pass int i Item temp Die Korrektheit basiert hier auf derselben Invarianten wie bei Bubblesort. Es folgt eine C++ Funktion: mittels sequentieller Suche , und tauscht diese Komponente an die Stelle bottom. Es werden also vec"bottom] und vec"minIndx] vertauscht. vec"bottom], vec"bottom+1],...,vec"vSize-1], 1. Gegeben ist vec"] mit vSize Komponenten. 2. Das Array vec wird vSize-1 mal von vorn nach hinten durchlaufen. Ein Durchlauf heit Phase (pass). 3. Phase bottom sucht den Index minIndx einer Komponente mit kleinstem Schlusselwert im Bereich Wir geben zunachst eine informelle Beschreibung: 10.1.2 Sortieren durch direktes Auswahlen: Selection Sort Satz 10.2 Bubblesort erfordert &(n2) Zuweisungen im Worst Case. Neben der Anzahl C (n) der Vergleiche ist fur die Laufzeit auch die Anzahl A(n) der Zuweisungen (Assignments ) von Arraykomponenten von groer Bedeutung, da sie auer den Schlusseln noch weitere (ggf. groe) Datenmengen enthalten. Oenbar kann jeder Vergleich einen Austausch und damit 3 Zuweisungen verursachen. Es gilt also A(n) 3 C (n) und damit Satz 10.1 Bubblesort erfordert &(n2) Vergleiche im Worst Case. Diese Verbesserungen bewirken aber nur eine Verringerung der mittleren Anzahl der Vergleiche. Fur den Worst Case lassen sich stets (Ubung) Beispiele nden, die C (n) = n(n2;1) Vergleiche benotigen. Es gilt also: 188 189 63 12 12 12 12 12 12 12 24 24 18 18 18 18 18 18 12 63 63 24 24 24 24 24 Array vec 53 72 18 53 72 18 53 72 24 53 72 63 35 72 63 35 44 63 35 44 53 35 44 53 44 44 44 44 44 72 72 63 35 35 35 35 53 53 63 72 i=1 (n ; i) = n(n2; 1) nX ;1 da die sequentielle Suche in Phase i (Bestimmung des Minimums in n ; i + 1 Komponenten) gerade n ; i Vergleiche erfordert. Die Anzahl A(n) der Zuweisungen von Arraykomponenten ist jedoch deutlich geringer, da pro Phase maximal ein Austausch erfolgt. Also gilt A(n) 3(n ; 1) = O(n) : C (n) = Fur die Anzahl C (n) der Vergleiche ergibt sich analog zu Bubblesort: Abbildung 10.2: Phasen bei Straight Selection Sort. Phase Input 1 2 3 4 5 6 7 Fur das Beispiel aus Abbildung 10.1 ergeben sich die in Abbildung 10.2 dargestellten Zustande nach den einzelnen Phasen } temp = vec"bottom] vec"bottom] = vec"minIndx] vec"minIndx] = temp }//endfor bottom for (bottom = 0 bottom < vSize-1 bottom++) { // INV (prior to test): // All vec"bottom+1..vSize-1] are >= vec"bottom] // && vec"0..bottom] are in ascending order // && bottom >= 0 minIndx = bottom for (i = bottom+1 i < vSize i++) // INV (prior to test): // vec"minIndx] <= all // vec"0..i-1] // && i >= bottom+1 if (vec"i].key < vec"minIndx].key) minIndx = i 10.1. DIREKTE METHODEN KAPITEL 10. SORTIEREN IN ARRAYS : : : vec"i-1].key : (10.2) 63 24 12 12 12 12 12 12 24 63 24 24 24 18 18 18 12 12 63 53 53 24 24 24 Array vec 53 72 53 72 53 72 63 72 63 72 53 63 44 53 35 44 18 18 18 18 18 72 63 53 44 44 44 44 44 44 72 63 35 35 35 35 35 35 35 72 Phase n-1 : n ; 1 Vergleiche ::: Die Anzahl der Vergleiche hangt davon ab, wie das Einfugen in die Zielsequenz durchgefuhrt wird. Bei sequentieller Suche der Stelle (von links nach rechts) ergeben sich im Worst Case folgende Zahlen: Phase 1 : 1 Vergleich Phase 2 : 2 Vergleiche Abbildung 10.3: Phasen bei Insertion Sort. Phase Input 1 2 3 4 5 6 7 Im Standardbeispiel ergeben sich die in Abbildung 10.3 dargestellten Zustande nach jeder Phase: vec"0].key vec"1].key Die Korrektheit dieses Algorithmus folgt aus der folgenden Invariante: Nach jeder Phase i gilt 3. In Phase i wird die nachste \Karte" vec"i] der Quellsequenz genommen und an der richtigen Stelle (bzgl. vec"i].key) in die Zielsequenz vec"0]: : :vec"i-1] eingefugt. 2. Es nden vSize-1 Phasen i = 1 : : :vSize-1 statt. 1. Gegeben ist vec"] mit vSize Komponenten. Anfangs besteht die Zielsequenz aus vec"0] und die Quellsequenz aus vec"1]: : :vec"vSize-1]. Diese Methode wird oft beim Kartenspiel genutzt. Die Arraykomponenten (Karten) werden gedanklich in eine Zielsequenz vec"0]: : :vec"i-1] (die Karten, die man bereits in der Hand hat) und die Quellsequenz vec"i]: : :vec"vSize-1] (die Karten fur die eigene Hand, die noch verdeckt auf dem Tisch liegen) aufgeteilt. Dann lat sich der Algorithmus folgendermaen beschreiben: 10.1.3 Sortieren durch direktes Einfugen: Insertion Sort Beispiele zeigen wieder, da C (n) = &(n2 ) und A(n) = &(n) gilt. 190 nX ;1 191 (blog ic + 1) i=1 nX ;1 i=1 (log(n ; 1) + 1) = (n ; 1)(log(n ; 1) + 1) = (n ; 1) log(n ; 1) + (n ; 1) = O(n log n) A(n) (i + 1) = i=( n X n X i=2 i=1 i=1 n(n+1) ; 1 = O(n2 ) : 2 nX ;1 i) ; 1 Wir betrachten daher zunachst das Mischen von zwei bereits sortierten Arrays. Seien dazu vec1"] und vec2"] bereits sortierte Arrays der Lange m bzw. n mit Komponenten vom Typ Item. Diese sind in das Array vec"] der Lange m + n zu verschmelzen. 10.2.1 Mischen sortierter Arrays Mergesort teilt das zu sortierende Array in zwei gleichgroe Teilfolgen (Unterschied hochstens eine Komponente), sortiert diese (durch rekursive Anwendung von Mergesort auf die beiden Teile) und mischt die dann sortierten Teile zusammen. 10.2 Mergesort = Das Beispiel des absteigend sortierten Arrays zeigt, da dieser Fall auch eintritt, also A(n) = %(n2 ) gilt. Straight Insertion (mit binarer Suche) ist also bezuglich der Anzahl der Vergleiche sehr gut (O(n log n)), aber bezuglich der Anzahl der Zuweisungen schlecht (%(n2 )). Die hier vorgestellten direkten Methoden sind mit ihrer Worst Case Laufzeit von &(n2 ) als sehr aufwendig einzustufen. Wir werden im Rest des Kapitels drei \intelligentere" Sortiermethoden kennenlernen, die im Mittel, und teilweise auch im Worst Case, mit O(n log n) Vergleichen und Zuweisungen auskommen. Also ist temp = vec"i] for ( j = i-1 j >= 0 j++ ) vec"j+1] = vec"j] vec"0] = temp Bezuglich der Zahl A(n) der Zuweisungen von Arraykomponenten ist in beiden Varianten (sequentielle oder binare Suche) eine Verschiebung der Komponenten der Quelldatei rechts von der Einfugestelle k um jeweils eine Stelle erforderlich, im schlimmsten Fall (k = 0) also i Verschiebungen in Phase i. Dies lat sich mit i + 1 Zuweisungen realisieren: C (n) In diesem Fall ist wiederum C (n) = n(n2;1) = O(n2 ). Da die Zielsequenz bereits aufsteigend sortiert ist, lat sich statt der sequentiellen Suche die binare Suche verwenden. Phase i erfordert dann (Zieldatei enthalt i Elemente) gema Satz 6.1 hochstens blog ic + 1 Vergleiche. Also gilt dann: 10.2. MERGESORT KAPITEL 10. SORTIEREN IN ARRAYS ::: ::: 12 24 53 63 vec2: 18 35 44 72 (10.3) 0 1 1 2 3 3 3 j 12 12 12 12 12 12 12 vec 18 18 18 18 18 18 24 24 24 24 24 - - - - - - - - - 35 - - 35 44 - 35 44 53 35 44 53 63 Abbildung 10.4: Phasen bei Merge. 1 2 3 4 5 6 7 k - C (m n) m + n ; 1 : Sei C (m n) die maximale Anzahl von Schlusselvergleichen und A(m n) die maximale Anzahl von Zuweisungen von Komponenten beim Mischen. Vergleiche treten nur in der Schleife 3 auf, und zwar genau einer pro Durchlauf. Da die Schleife maximal m + n ; 1 mal durchlaufen wird, gilt 1 1 2 2 2 3 4 i Die dazugehorige Folge der Werte von i,j,k und vec bei jedem Wiedereintritt in die Schleife 3 ist in Abbildung 10.4 angegeben. Am Ende dieser Schleife ist i = 4 und Schritt 4 des Algorithmus wird ausgefuhrt, d. h. der \Rest" von vec2, also die 72, wird nach vec ubertragen. vec1: Hieraus folgt sofort, da vec am Ende aufsteigend sortiert ist. Als Beispiel betrachten wir die Arrays: vec"0].key vec"k-1].key vec"k-1].key vec1"i].key vec1"m-1].key vec"k-1].key vec2"j].key vec2"n-1].key ::: Bei jedem Wiedereintritt in die Schleife 3 gilt die Invariante 1. Initialisierung: i = 0 j = 0 k= 0 2. Wiederhole Schritt 3 bis i = m oder j = n. 3. Falls vec1"i].key < vec2"j].key, so kopiere vec1"i] an die Position k von vec und erhohe i und k um 1. Andernfalls kopiere vec2"j] an die Position k von vec und erhohe j und k um 1. 4. Ist i = m und j < n so ubertrage die restlichen Komponenten von vec2 nach vec. 5. Ist j = n und j < m so ubertrage die restlichen Komponenten von vec1 nach vec. Dazu durchlaufen wir vec1 und vec2 von links nach rechts mit zwei Indexzeigern i und j wie folgt: 192 193 vec 6 left 6 middle 6 right vec 6 left 6 right Merge void Merge( /* inout */ Item vec"], /* in */ int left, /* in */ int middle, /* in */ int right ) //...................................................................... // PRE: Assigned(left) && Assigned(middle) && Assigned(right) // && Assigned(vec"0].key ... vec"vSize-1].key) // && 0 <= left <= middle < right <= vSize-1 // && vec"left].key <= ... <= vec"middle].key // && vec"middle+1].key <= ... <= vec"right].key // POST: vec"left].key <= vec"left+1].key <= ...<= vec"right].key // and the set of values is the same //...................................................................... { int i, j, k, m, n Item temp Programm 10.1 Auf den genauen Mechanismus dieser Anweisungen wird erst in Kapitel ?? eingegangen, hier sollte reichen, da vec1 und vec2 dann wie \normale" Arrays verwendet werden konnen. Item* vec1=new Item"m] Item* vec2=new Item"n] mit dem aufsteigend sortierten Bereich vec"left]: : :vec"right]. Dazu werden zunachst die beiden sortierten Bereiche auf lokale Arrays vec1 und vec2 kopiert, die dann in den entsprechenden Bereich von vec zuruckgemischt werden. Den Speicherplatz fur die lokalen Arrays besorgt man sich in der benotigten Lange uber Pointer mit den Anweisungen Output: mit den sortierten Bereichen vec"left]: : :vec"middle] und vec"middle+1]: : :vec"right]. Input: Zuweisungen treten genau n + m mal auf, da jede Komponente von vec einen Wert bekommt. Also ist A(m n) = m + n : Wir geben nun eine C++ Funktion fur dieses Verfahren an, und zwar in der (spater benotigten) Version, da zwei benachbarte, bereits sortierte Teilbereiche eines Arrays vec gemischt werden und dann in demselben Bereich von vec aufsteigend sortiert gemischt stehen. Wir verlangen also folgendes Input/Output Verhalten: 10.2. MERGESORT of first part using pointers left + 1 // number of components of vec1 new Item"m] i < m i++ ) vec1"i] = vec"left + i] // copy rest of vec2 if necessary while ( j < n ) { vec"left+k] = vec2"j] j++ k++ } //endwhile // copy rest of vec1 if necessary while ( i < m ) { vec"left+k] = vec1"i] i++ k++ } //endwhile // merge vec1 and vec2 into vec"left...right] until i == m or j == n while ( i < m && j < n ) { if ( vec1"i].key <= vec2"j].key ) { vec"left+k] = vec1"i] i++ k++ } else { vec"left+k] = vec2"j] j++ k++ }//endif } //endwhile i = 0 j = 0 k = 0 // make copy of second part using pointers n = right - middle // number of components of vec2 Item* vec2 = new Item"n] for ( j = 0 j < n j++ ) vec2"j] = vec"middle + 1 + j] // make copy m = middle Item* vec1 = for ( i = 0 KAPITEL 10. SORTIEREN IN ARRAYS Merge ergibt sich dann sehr einfach folgende rekursive Variante von MergeSort void MergeSort( /* inout */ Item vec"], /* in */ int first, /* in */ int last ) Programm 10.2 Mit dieser Funktion Mergesort. 10.2.2 Sortieren durch rekursives Mischen: Mergesort } 194 int middle if ( first < last ) { middle = ( first + last ) / 2 MergeSort( vec, first, middle ) MergeSort( vec, middle+1, last ) Merge( vec, first, middle, last ) }//endif // // // // devide vec into 2 equal parts sort the first part sort the second part merge the 2 sorted parts 63 24 12 53 72 18 44 35 Wir ermitteln nun den Worst Case Aufwand C (n) fur die Anzahl der Vergleiche und A(n) fur die Anzahl der Zuweisungen von MergeSort beim Sortieren eines Arrays mit n Komponenten. 10.2.3 Die Analyse von Mergesort ergibt der Aufruf MergeSort(a,0,7) dann den in Abbildung 10.5 dargestellten Ablauf. Dabei beschreiben die Einrucktiefe die Aufrufhierarchie (Rekursionsbaum), und die Kasten die bereits sortierten Teile des Arrays. a im gesamten Bereich von 0 bis n-1. Die Korrektheit von MergeSort ergibt sich sofort durch vollstandige Induktion nach der Anzahl n = last - first + 1 der zu sortierenden Komponenten. Ist n = 1, also last=first (Induktionsanfang), so wird im Rumpf von MergeSort nichts gemacht und das Array vec ist nach Abarbeitung von MergeSort trivialerweise im Bereich first: : :last sortiert. Ist n > 1, so sind first: : :middle und middle+1: : :last Bereiche mit weniger als n Elementen, die also nach Induktionsvoraussetzung durch die Aufrufe MergeSort(vec, first, middle) und MergeSort(vec, middle+1, last) korrekt sortiert werden. Die Korrektheit von Merge ergibt dann die Korrektheit von MergeSort. Fur das Standardbeispiel Item a"n] sortiert dann ein Array MergeSort(a,0,n-1) Der Aufruf } { 195 //...................................................................... // PRE: Assigned(vec"first].key ... vec"last].key) // && 0 <= first <= last <= no. of components - 1 // POST: vec"first].key <= vec"first+1].key <= ... <= vec"last].key // and the set of values is the same //...................................................................... 10.2. MERGESORT 196 63 63 63 63 63 24 24 24 24 24 12 12 12 12 12 12 12 12 12 12 12 12 24 24 24 24 24 63 63 63 63 63 24 24 24 24 24 24 24 24 24 24 24 18 12 12 12 12 12 12 12 12 12 12 53 53 53 53 53 53 53 53 53 53 53 24 53 53 53 53 53 53 53 53 53 53 63 63 63 63 63 63 63 63 63 63 63 35 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 18 18 18 18 18 18 44 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 72 72 72 72 72 35 53 Abbildung 10.5: Die Rekursion bei MergeSort. Merge(a,4,5,7) Merge(a,0,3,7) Merge(a,6,6,7) MergeSort(a,7,7) MergeSort(a,6,6) MergeSort(a,6,7) Merge(a,4,4,5) MergeSort(a,5,5) MergeSort(a,4,4) MergeSort(a,4,5) MergeSort(a,4,7) Merge(a,0,1,3) MergeSort(a,3,3) Merge(a,2,2,3) MergeSort(a,2,2) MergeSort(a,2,3) MergeSort(a,1,1) Merge(a,0,0,1) MergeSort(a,0,0) MergeSort(a,0,7) MergeSort(a,0,3) MergeSort(a,0,1) 44 44 44 44 44 44 44 44 44 44 44 44 44 44 44 44 44 44 44 35 44 63 35 35 35 35 35 35 35 35 35 35 35 35 35 35 35 35 35 35 35 44 72 72 KAPITEL 10. SORTIEREN IN ARRAYS = 2 C ( n2 ) + n ; 1 1 mal angewendet 197 8 2 Eine Alternative ist die Verikation durch Einsetzen der \vermuteten" Formel C (2q ) = (q ; 1)2q + 1 in die Rekursionsgleichung und Nachrechnen der Gleichung. Dies fuhrt auf die gleichen Beweisschritte. und (q ; 1)2q +1 = 1 (Induktionsanfang). Also sei die Behauptung richtig fur 2r mit 1 r q. Wir schlieen jetzt auf q + 1: C (2q+1 ) = 2 C (2q ) + 2 2q ; 1 Rekursionsgleichung = 2 (q ; 1)2q + 1] + 2 2q ; 1 Induktionsvoraussetzung = (q ; 1)2q+1 + 2 + qq+1 ; 1 = q 2q+1 + 1 Beweis: Der Beweis erfolgt durch vollstandige Induktion nach q. Ist q = 1, so ist C (21 ) = 1 1. Lemma 10.1 Ist n = 2q , so hat die Rekursionsgleichung 10.4 die Losung C (2q ) = (q ; 1)2q + Wir verizieren diese eher intuitive Vorgehensweise durch einen formalen Beweis mit vollstandiger Induktion.2 Wegen n = 2q und C (2) = 1 folgt hieraus C (2q ) = (q ; 1)2q + 1 : = : : : (insgesamt q ; 1 mal anwenden) = 2q;1 C (2) + (q ; 1)n ; (2q;1 ; 1) = = = 2 2 C ( n4 ) + 2 n4 ; 1] + 2 n2 ; 1 4 C ( n4 ) + 2n ; 3 2 mal angewendet n n 4 2 C ( 8 ) + 2 8 ; 1] + 2n ; 3 8 C( n) + 3 n ; 7 3 mal angewendet Aus dem rekursiven Aufbau des Algorithmus ergeben sich sofort die folgenden Rekursionsgleichungen fur C (n). C (2) = 1 C (2n) = 2 C (n) + C (n n) fur n > 1 In Worten: Das Sortieren eines 2-elementigen Arrays erfordert einen Vergleich. Das Sortieren eines Arrays der Lange 2n erfordert den Aufwand fur das Sortieren von 2 Arrays der Lange n (rekursive Aufrufe von MergeSort fur die beiden Teile), also 2 C (n), plus den Aufwand C (n n) fur das Mischen (Aufruf von Merge). Da C (n n) = 2n ; 1, ist also C (2) = 1 C (2n) = 2 C (n) + 2n ; 1 fur n > 1 : (10.4) Um diese Rekursionsgleichung zu losen, betrachten wir zunachst den Fall, da n eine Zweierpotenz ist, etwa n = 2q . Dann ist n genau q mal durch 2 ohne Rest teilbar und wir erhalten durch mehrfache Anwendung der Rekursionsgleichung C (n) = 2 C ( n2 ) + 2 n2 ; 1 10.2. MERGESORT KAPITEL 10. SORTIEREN IN ARRAYS A(2n) = 2 A(n) + 4 n fur n > 1 (10.5) @ @ 2 3 ;@ ; @ 4 ; ; 3 4 @ @ 5 C (n) C (n0) = C (2q ) = (q ; 1)2q + 1 < (log n) n0 + 1 < (log n) 2n + 1 = 2n log n + 1 = O(n log n) : Dies fuhrt dazu, da im zugehorigen Rekursionsbaum nicht alle Zweige bis auf die unterste Ebene reichen. Vervollstandigt man (gedanklich) den Rekursionsbaum bis auf die unterste Ebene, so wurde dies dem Rekursionsbaum fur ein Array der Lange n0 entsprechen, wobei n0 die nachstgroere Zweierpotenz nach n ist, also n0 = minf2r j 2r n r = 1 2 : : :g. Sei 2q dieses Minimum. Dann ist 2q;1 < n 2q = n0 , also n0 = 2 2q;1 < 2n sowie q ; 1 < log n q. Also ist (wegen der Vervollstandigung): Abbildung 10.6: Rekursionsbaum von MergeSort fur n = 6. 0 ;@ ; @ 1 ; ; 0 1 3 4 5 @ @ ; ; 0 1 2 4 5 0 1 2 3 Wir betrachten nun den Fall, da n keine Zweierpotenz ist. Dann unterscheiden sich die jeweiligen Teilarrays um maximal 1, vgl. 10.6. A(n) = (q + 1)2q : Der gleiche Losungsansatz liefert fur n = 2q A(2) = 4 In Merge werden zunachst die Teile von vec nach vec1 und vec2 kopiert. Dies erfordert 2n Zuweisungen. Fur das Mergen sind dann wieder A(n n) = 2n Zuweisungen erforderlich. Also ergibt sich folgende Rekursionsgleichung: A(2n) = 2 A(n) + Zuweisungen in Merge Bezuglich der Anzahl A(n) der Zuweisungen von Arraykomponenten ergibt sich ganz analog: was zu zeigen war. 198 MergeSort A(n0 ) = A(2q ) = (q + 1)2q < (log n + 2)n0 < (log n + 2)2n = 2n log n + 4n = O(n log n) : sortiert ein Array mit n Komponenten mit O(n log n) Vergleichen und A(n) 199 Dann gilt: c a b f (n) + c n fur n > 1 8 > falls a > b < O(n) f (n) 2 > O(n log2 n) falls a = b : O(nloga b) falls a < b f (1) f (a n) sei folgende Rekursionsgleichung gegeben: Satz 10.4 (Aufteilungs-Beschleunigungs-Satz) Seien a > 0 b c naturliche Zahlen und Gegeben ist ein Problem der Groe a n mit der Laufzeit f (a n). Dieses zerlegt man in b Teilprobleme der Laufzeit f (n). Ist die Laufzeit fur das Aufteilen respektive Zusammenfugen der Teillosungen c n, so ergibt sich die folgende Rekursionsgleichung und der 10.3.1 Aufteilungs-Beschleunigungs Satze Mergesort ist ein typisches Beispiel fur die sogenannte \Beschleunigung durch Aufteilung" Dies Prinzip tritt oft bei der Konzeption von Algorithmen auf. Daher hat man Interesse an einer allgemeinen Aussage uber die Laufzeit in solchen Situationen. 10.3 Beschleunigung durch Aufteilung: Divide and Conquer Rekursionsaufwand und Rekursionstiefe halten sich also in vernunftigen Grenzen. i=0 Betrachten wir zum Abschlu noch den Rekursionsaufwand und die Rekursionstiefe. Fur n = 2q ist die Rekursionstiefe gerade q = log n, fur beliebige n ergibt sich aus der soeben durchgefuhrten Vervollstandigungsuberlegung log n0 < log(2n) = log n + 1 als Schranke fur die Rekursionstiefe. Die Anzahl der rekursiven Aufrufe ergibt sich als Summe entlang der Schichten des Rekursionsbaums zu q X 2i = 2q+1 ; 1 = 2n0 ; 1 < 4n ; 1 = O(n) : Zuweisungen. Satz 10.3 Wir erhalten also Entsprechend folgt 10.3. BESCHLEUNIGUNG DURCH AUFTEILUNG: DIVIDE AND CONQUER KAPITEL 10. SORTIEREN IN ARRAYS i=0 log Xa n b i f (n) = ac n a : 1 b i b < 1 ) logXa n b i < X : a i=0 a i=0 a Die letzte Summe ist eine geometrische Reihe mit Wert k1 = 1;1 b = a;a b . Also ist a c k1 f (n) < a n ) f (n) 2 O(n) : 2. a = b: Dann ist log Xa n f (n) = ac n 1 = ac n(loga n + 1) = ac n loga n + ac n : i=0 Fur n a ist loga n 1 und daher f (n) ac n loga n + ac n loga n = 2ac n loga n = ( 2ac loga 2) n log2 n 2 O(n log2 n) : 1. a > b: Dann ist Wir betrachten jetzt 3 Falle: Also ist q b i X f (n) = ac n i=0 a Dies zeigt man durch Induktion uber q. Fur q = 0 ist die Summe 0 und daher f (1) = ac . Die Behauptung sei nun fur q gezeigt. Dann ergibt sich im Induktionsschlu auf q + 1: f (aq+1) = f (a aq ) = b f (aq ) + c aq (Rekursionsformel) q b i X = b ac aq + c aq (Induktionsvoraussetzung) i=0 a q b i c X = ac aq+1 ab + a aq+1 i=0 a q b i+1 c X = ac aq+1 + a aq+1 i0 =0 a 1 qX +1 b i c q +1 @ + 1A = aa i=1 a qX +1 b i = ac aq+1 i=0 a Beweis:: Fur n = aq gilt 200 log Xa n b i f (n) = ac n i=0 a q b i X c = a aq da n = aq i=0 a q q X X = ac bi aq;i = ac bq;i ai i=0 i=0 q a i c X 1 a i X c q q = ab < b a i=0 b : i=0 b 201 Die Unterscheidung erfolgt hier also nach dem Wachstum von g(n) im Verhaltnis zu nloga b . Ist g(n) deutlich kleiner (Fall 1.) als nloga b , so bestimmt nloga b das Wachstum von f (n). Hat g(n) dasselbe Wachstum wie nloga b , so kommt im Wachstum von f (n) ein log n Faktor dazu. Ist schlielich g(n) deutlich kleiner als nloga b und gilt die zusatzliche \Regularitatsbedingung" aus 3., so bestimmt g(n) allein die Groenordnung von f (n). Deutlich kleiner bzw. deutlich gro er bedeutet dabei jeweils mindestens um einen polynomialen Faktor n" . 1. Ist g(n) = O(nloga b;" ) fur eine Konstante " > 0, so ist f (n) = &(nloga b ). 2. Ist g(n) = &(nloga b ), so ist f (n) = &(nlogb a log n) 3. Ist g(n) = %(nloga b+" ) fur eine Konstante " > 0, und ist b g( na ) c g(n) fur eine Konstante c < 1 und alle n n0 (c n0 geeignet gewahlt), so ist f (n) = &(g(n)). Satz 10.5 (Aufteilungs-Beschleunigungssatz, Allgemeine Version) Seien a > 0 und b naturliche Zahlen und sei die folgende Rekursionsgleichung gegeben: f (a n) = b f (n) + g(n) : Dann hat f (n) folgendes asymptotisches Wachstum: Oenbar ist der Fall 2 gerade der auf Mergesort zutreende Fall. Der Aufteilungs-Beschleunigungssatz wurde hier nur in einer speziellen Form bewiesen, um den Beweis einfacher zu halten. Wir geben nachstehend eine allgemeinere Version an und verweisen fur den Beweis auf CLR90, S. 61 ]. f (n) < cka2 bq = cka2 bloga n = c ak2 nloga b 2 O(nlogab ) : P1 ; a i ist wie in Fall 1 eine geometrische Reihe mit Wert k = b . Also ist 2 b;a i=0 b 3. a < b: Dann ist 10.3. BESCHLEUNIGUNG DURCH AUFTEILUNG: DIVIDE AND CONQUER KAPITEL 10. SORTIEREN IN ARRAYS b d xy = (a2 n2 + b)(c2 n2 + d) n = ac2n + (ad + bc)2 2 + bd: a c T (n) = 4 T ( n2 ) + c0 n Man hat die Multiplikation also auf 4 Multiplikationen von n2 -stelligen Zahlen und einige n n Additionen und Shifts (Multiplikationen mit 2 2 bzw. 2 ), die nur linearen Aufwand erfordern) zuruckgefuhrt. Dies fuhrt zur Rekursionsgleichung Dann ist x = y = Als weitere Anwendung betrachten wir die Multiplikation von zwei n-stelligen Dualzahlen . Die traditionelle Methode erfordert &(n2 ) Bit Operationen. Durch Aufteilung und Beschleunigung erreicht man O(nlog 3 ) = O(n159 ) Operationen. Dies ist nutzlich bei der Implementation der Multiplikation beliebig langer Dualzahlen, z. B. in Programmpaketen, die mit den Standard long int Zahlen nicht auskommen. Seien x y zwei n-stellige Dualzahlen, wobei n eine Zweierpotenz sei. Wir teilen x y in zwei n -stellige Zahlen wie folgt: 2 10.3.2 Multiplikation von Dualzahlen 1. f (3n) = 9f (n) + n Hier ergibt sich bereits aus der einfachen Version des Satzes (Fall 3) f (n) = O(nlog3 9 ) = O(n2). In der allgemeinen Version trit Fall 1 zu, da f (n))O(nloga b;1 ) (also " = 1), und man erhalt f (n) = &(nloga b ) = &(n2 ). 2. f ( 32 n) = f (n) + 1 Hier ist g(n) = 1 und nlog3=2 1 = n0 = 1. Also trit Fall 2 der allgemeinen Version zu, und man erhalt f (n) = &(log n). 3. f (4n) = 3f (n) + n log n Hier ist g(n) = n log n und nloga b = nlog4 3 = O(n0793 ). Also ist g(n) = %(nloga b+" ) mit " = 0 2. Es wurde also Fall 3. zutreen, ; ; falls wir die Regularitatsbedingung fur g(n) zeigen konnen. Es ist b f ( na ) = 3 n4 log n4 < 34 n log n, so da die Regularitatsbedingung mit c = 43 gilt. Also folgt f (n) = &(n log n). 4. f (2n) = 2f (n) + n log n Diese Rekursionsgleichung fallt nicht unter das allgemeine Schema, da g(n) = n log n zwar asymptotisch groer als nloga b = n ist, aber nicht um einen Faktor n" . Dieser Fall fallt also in die \Lucke" zwischen Fall 2. und 3. Beispiel 10.1 202 := := := := ac bd v2n + (u ; v ; w)2 n2 + w (a + b)(c + d) 203 a# c# { { { { { { { { { { Additionen a + b c + d : Produkt : Shift von auf 2n : Produkte a# c#: Addition a# + c#: Shift von a# c# um n2 Stellen: Shift von v auf v2n : Addition u ; v ; w: Shift von u ; v ; w um n2 Stellen: Addition zu z : wobei c2 n folgenden Aufwand enthalt: 2n n 2 2( n2 + 1) n n 2 2 n2 n +1 2 n 2 n2 1 T (n) = 3T n2 + c2 n also a + b = 2 n2 + a# c + d = 2 n2 + c# mit den fuhrenden Bits und den n2 -stelligen Resten a# c#. Dann ist n (a + b)(c + d) = 2n + (a# + c#)2 2 + a#c# : Hierin tritt nur ein Produkt von n2 -stelligen Zahlen auf (namlich a#c#). Der Rest sind Shifts bzw. lineare Operationen auf n2 -stelligen Zahlen (z. B. a#). Daher erhalt man insgesamt die Rekursionsgleichung a+b = c+d = mit der gewunschten Losung T (n) = &(nlog2 3 ). Um den Ubertrag zu berucksichtigen, schreiben wir a + b und c + d in der Form T (n) = 3T n2 + c1 n fuhren jedoch zur Berechnung von z = xy mit 3 Multiplikation von Zahlen der Lange n2 bzw. n + 1, da bei a + b bzw. c + d ein Ubertrag auf die ; n + 1-te Position entstehen konnte. 2 2 Ignorieren wir diesen Ubertrag, so erhalt man u v w z mit der Losung T (n) = &(n2 ), also ohne Gewinn gegenuber der traditionellen Methode. Die Anweisungen 10.3. BESCHLEUNIGUNG DURCH AUFTEILUNG: DIVIDE AND CONQUER KAPITEL 10. SORTIEREN IN ARRAYS u v w xy = = = = = = = (a + b)(c + d) = 59 88 = 5192 ac = 42 52 = 2184 bd = 17 36 = 612 v 104 + (u ; v ; w) 102 + w 2184 104 + 2396 102 + 612 21:840:000 + 239:600 + 612 22:080:212 a = 42 b = 17 und a + b = 59 c = 52 d = 36 und c + d = 88 : Quicksort basiert (im Gegensatz zu Mergesort) auf variabler Aufteilung des Eingabearrays. Es wurde 1962 von Hoare entwickelt. Es benotigt zwar im Worst Case %(n2 ) Vergleiche, im Mittel jedoch nur O(n log n) Vergleiche, und ist aufgrund empirischer Vergleiche allen anderen O(n log n) Sortierverfahren uberlegen. 10.4 Quicksort n n Matrizen beschleunigen, vgl. CLR90] Hier erhalt man die Rekursionsgleichung T (2n) = 7 T (n) + 14n2 mit der Losung &(nlog 7 ) = &(n281 ), also eine Beschleunigung gegenuber der normalen Methode mit dem Aufwand &(n3 ). Hier lassen sich noch weitere Beschleunigungen erzielen. Der momentane \Rekord" steht bei O(n239 ), vgl. CW87]. Auf ahnliche Weise wie die Multiplikation von Zahlen lat sich auch die Multiplikation (groer) Es folgt Dann ist Der \Trick" bestand also darin, auf Kosten zusatzlicher Additionen und Shifts, eine \teure" Multiplikation von n2 -stelligen Zahlen einzusparen. Die rekursive Anwendung dieses Tricks ergibt dann die Beschleunigung von &(n2 ) auf &(n159 ). Fur die normale Computerarithmetik (n = 32) zahlt sich dieser Trick nicht aus, jedoch bedeutet er fur Computerarithmetiken mit beliebigstelligen Dualzahlen, die meist softwaremaig realisiert werden, eine wichtige Beschleunigung. Das Verfahren lat sich naturlich auch im Dezimalsystem anwenden. Wir geben ein Beispiel fur n = 4: x = 4217 y = 5236 Dieser Aufwand addiert sich zu 8 5 n + 2 9n fur n 2. Also kann c2 als 9 angenommen werden. Als Losung erhalt man nach dem Aufteilung-Beschleunigungssatz T (n) = &(nlog 3 ) = &(n159 ) : 204 205 63 18 12 12 12 24 24 24 18 18 12 12* 18* 24 24 53* 35 35 35* 35 72 44 44 44 44 18 53 53 53 53 44 72* 63 63 63 35 63 72 72 72 1. 2. 3. 4. 5. Gegeben ist vec und der Bereich zwischen loBound und hiBound. Wahle eine Komponente vec"pivot]. Tausche vec"pivot] mit vec"loBound]. Setze Indexzeiger loSwap auf loBound+1 und hiSwap auf hiBound. Solange loSwap < hiSwap wiederhole: Grobbeschreibung der Aufteilung Wir betrachten nun die Durchfuhrung der Aufteilung im Detail. Da sie rekursiv auf stets andere Teile des Arrays vec angewendet wird, betrachten wir einen Bereich von loBound bis hiBound. Abbildung 10.7: Phasen bei Quicksort. Input 1. Aufteilung 2. Aufteilung 3. Aufteilung Output Die Korrektheit des Algorithmus folgt leicht durch vollstandige Induktion. Die Aufteilung erzeugt Arrays kleinerer Lange, die nach Induktionsvoraussetzung durch die Aufrufe von Quicksort in Schritt 4. korrekt sortiert werden. Die Eigenschaften 3a){3c) ergeben dann die Korrektheit fur das ganze Array. Im Standardbeispiel ergibt sich, falls man stets die mittlere Komponente wahlt (gekennzeichnet durch *) die in Abbildung 10.7 dargestellte Folge von Zustanden (jeweils nach der Aufteilung). Die umrahmten Bereiche geben die aufzuteilenden Bereiche an. 1. Gegeben ist vec"] mit vSize Komponenten. 2. Wahle eine beliebige Komponente vec"pivot]. 3. Zerlege das Array vec in zwei Teilbereiche vec"0]: : :vec"k-1] und vec"k+1]: : :vec"vSize-1] mit a) vec"i].key vec"pivot].key fur i = 0 : : : k ; 1 b) vec"k].key = vec"pivot].key c) vec"j].key > vec"pivot].key fur j = k + 1 : : : vSize-1 4. Sofern ein Teilbereich aus mehr als einer Komponente besteht, so wende Quicksort rekursiv auf ihn an. Wir geben zunachst eine Grobbeschreibung von Quicksort an. 10.4.1 Der Algorithmus 10.4. QUICKSORT KAPITEL 10. SORTIEREN IN ARRAYS 5.1 Inkrementiere ggf. loSwap solange, bis vec"loSwap].key > vec"loBound].key. 5.2 Dekrementiere ggf. hiSwap solange, bis vec"hiSwap].key vec"loBound].key. 5.3 Falls loSwap < hiSwap, so vertausche vec"loSwap] und vec"hiSwap]. 63 63 53* 53* 53* 53* 53* 53* 53* 53* 53* 53* 18 24 24 24 24l 24 24 24 24 24 24 24 24 24 12 12 12 12 12 12 12 12 12 12 12 12 12 53 53* 63 63 63l 63l 35l 35 35 35 35 35 35 72 72 72 72 72 72 72 72l 72l 44l 44 44 44 18 18 18 18 18 18 18 18 18 18 18 18h 53* vec"hiSwap].key vec"pivot].key 35 35 35 35h 35h 35h 63h 63h 63 63 63 63 63 (10.7) (10.6) QuickSort void QuickSort( /* inout */ Item vec"], /* in */ int loBound, /* in */ int hiBound ) //.................................................................. // PRE: loBound < hiBound (i.e., at least 2 items to sort) // && Assigned(vec"loBound..hiBound]) Programm 10.3 so da der Tausch in Schritt 6 die \Pivot"-Komponente genau an die richtige Stelle tauscht. Es folgt eine C++-Implementation dieses Algorithmus. loSwap hiSwap Beim Austritt gilt zusatzlich vec"hiSwap].key. < hiSwap ) vec"loSwap].key vec"pivot].key Bei jedem Eintritt in die Schleife 5 gilt die Invariante: vec"i].key vec"pivot].key fur i = loBound : : : loSwap ; 1, vec"j].key > vec"pivot].key fur j = hiSwap + 1 : : :hiBound, loSwap 44 44 44 44 44 44 44 44 44h 72h 72lh 72l 72 Abbildung 10.8: Die Aufteilung bei Quicksort im Detail. Input Schritt 2 Schritt 3 Schritt 4 Am Ende von 5.1 Am Ende von 5.2 Schritt 5.3 Am Ende von 5.1 Am Ende von 5.2 Schritt 5.3 Am Ende von 5.1 Am Ende von 5.2 Schritt 6 Abbildung 10.8 illustriert diese Aufteilung. l und h geben die jeweilige Position von loSwap und hiSwap an, das gewahlte vec"pivot] Element. 6. Tausche vec"loBound] und vec"hiSwap]. 206 { if ( hiBound - loBound == 1) { // Two items to sort if (vec"loBound].key > vec"hiBound].key) { temp = vec"loBound] vec"loBound] = vec"hiBound] vec"hiBound] = temp } return } pivotIndex = (loBound+hiBound)/2 // 3 or more items to sort pivotItem = vec"pivotIndex] vec"pivotIndex] = vec"loBound] vec"loBound] = pivotItem pivotKey = pivotItem.key loSwap = loBound + 1 hiSwap = hiBound do { while ( loSwap <= hiSwap && vec"loSwap].key <= pivotKey ) // INV (prior to test): // All vec"loBound+1..loSwap-1] // are <= pivot && loSwap <= hiSwap+1 loSwap++ while (vec"hiSwap].key > pivotKey) // INV (prior to test): // All vec"hiSwap+1..hiBound] // are > pivot && hiSwap >= loSwap-1 hiSwap-- if (loSwap < hiSwap) { temp = vec"loSwap] vec"loSwap] = vec"hiSwap] vec"hiSwap] = temp } // INV: All vec"loBound..loSwap-1] are <= pivot // && All vec"hiSwap+1..hiBound] are > pivot // && (loSwap < hiSwap) --> vec"loSwap] <= pivot < vec"hiSwap] // && (loSwap >= hiSwap) --> vec"hiSwap] <= pivot // && loBound <= loSwap <= hiSwap+1 <= hiBound+1 } while (loSwap < hiSwap) vec"loBound] = vec"hiSwap] vec"hiSwap] = pivotItem int loSwap, hiSwap int pivotKey, pivotIndex Item temp, pivotItem // POST: vec"loBound..hiBound] contain same values as // at invocation but are sorted into ascending order //.................................................................. 10.4. QUICKSORT 207 if (hiSwap+1 < hiBound) // 2 or more items in 2nd subvec QuickSort(vec, hiSwap+1, hiBound) if (loBound < hiSwap-1) // 2 or more items in 1st subvec QuickSort(vec, loBound, hiSwap-1) KAPITEL 10. SORTIEREN IN ARRAYS Also ist R(n) n ; 1. R(n) = 1 + R(n1 ) + R(n2) 1 + (n1 ; 1) + (n2 ; 1) (Induktionsvoraussetzung) = n1 + n2 ; 1 = n ; 1: Man sieht, da der Baum am tiefsten wird, wenn als Vergleichselement jeweils das kleinste oder grote Element des aufzuteilenden Bereiches gewahlt wird. In diesem Fall entartet der Rekursionsbaum zu einer Liste (keine Verzweigungen). Die Rekursionstiefe kann also bis zu n ; 1 betragen. Wir zeigen durch Induktion, da auch die Anzahl R(n) der rekursiven Aufrufe bei einem Arraybereich der Lange n hochstens n ; 1 betragt. Fur n = 1 erfolgt kein Aufruf, also gilt R(1) = 0 (Induktionsanfang). Fur n > 1 bewirkt der erste Aufruf eine Aufteilung in Bereiche mit n1 und n2 Komponenten, wobei n1 + n2 = n ; 1 gilt, da die Vergleichskomponente wegfallt. Also sind n1 n2 < n und man erhalt Abbildung 10.9: Rekursionsbaum zu Quicksort. PP PP PP P QuickSort(vec,0,4) QuickSort(vec,6,7) QuickSort(vec,1,4) QuickSort(vec,2,4) QuickSort(vec,0,7) Da die Aufteilung (im Unterschied zur Mergesort) variabel ist, hat der Rekursionsbaum bei Quicksort i. a. Teilbaume unterschiedlicher Hohe. Im Standardbeispiel ergibt sich der in Abbildung 10.9 dargestellte Baum. 10.4.2 Der Rekursionsaufwand von Quicksort } 208 209 3 In der Terminologie der Wahrscheinlichkeitstheorie sagt man, da jede Reihenfolge mit der gleichen Wahrscheinlichkeit 1=n! auftritt (daher der Name Gleichverteilung ), und man nennt C (n) auch die erwartete Anzahl der Vergleiche . Quicksort ist also im Worst Case schlecht. Erfahrungsgema ist Quicksort aber sehr schnell im Vergleich zu anderen %(n2 ) Sortierverfahren wie Bubblesort u. a. Dies liegt daran, da der Worst Case nur bei wenigen Eingabefolgen auftritt. Man wird daher Quicksort gerechter, wenn man nicht den Worst Case betrachtet, sondern den Aufwand uber alle moglichen Eingabefolgen mittelt , also den mittleren Aufwand C# (n) bei Gleichverteilung aller n! Reihenfolgen der Schlussel 1 2 : : : n betrachtet. Gleichverteilung bedeutet hier, da jede Reihenfolge (Permutation) der Werte 1 : : : n mit dem gleichen Gewicht (namlich 1) in das Mittel eingeht.3 Als Vergleichsbeispiel betrachten wir das Wurfeln mit einem Wurfel mit den Augenzahlen :::+6 = 3 5. 1 2 : : : 6. Dann ergibt sich die mittlere Augenzahl bei Gleichverteilung zu 1+2+3+ 6 Die mittlere Schrittlange beim Mensch-argere-dich-nicht betragt also 3 5 der Worst Case (bzgl. Weiterkommen) jedoch nur 1. Fur n = 3 ergeben sich bei Quicksort 3! = 6 Permutationen, namlich 123 132 213 231 312, 321. Die Anzahl der Vergleiche pro Permutation betragt (gema der Implementation in = 103 Programm 10.3) 2 4 4 4 4 2. Der mittlere Aufwand betragt also 2+4+4+4+4+2 3! Vergleiche gegenuber dem Worst Case von 4 Vergleichen. Sei ( die Menge aller Permutationen von 1 : : : n. Fur 2 ( sei C () die Anzahl von Vergleichen, die Quicksort benotigt, um zu sortieren. Dann ist X C# (n) = n1! C () : 2 10.4.4 Der mittlere Aufwand von Quicksort A(n) = %(n2) : Entsprechend ergibt sich fur die Worst Case Anzahl A(n) von Zuweisungen (Ubung) C (n) = %(n2 ) : Also gilt fur die Worst Case Anzahl C (n) von Vergleichen: (n ; 1) + (n ; 2) + : : : + 2 + 1 = n(n2; 1) Vergleiche. Vergleiche von Schlusseln treten bei Quicksort nur bei den Aufteilungen auf. Dabei mu jeder andere Schlussel mit dem Vergleichsschlussel verglichen werden, also erfolgen bei einem Bereich von n Komponenten n ; 1 Vergleiche. Wird nun jeweils der grote bzw. kleinste Schlusselwert als Vergleichsschlussel gewahlt, so verkleinert sich der Bereich jeweils nur um ein Element und man erhalt: 10.4.3 Der Worst Case Aufwand von Quicksort 10.4. QUICKSORT KAPITEL 10. SORTIEREN IN ARRAYS fur k = 1 : : : n : C () = 2k X S1 2k X } Z () + 2k | {z =:S1 X n = (n ; 1)! n = n! : Z () + C (1) + C (2 )] = X 2k | {z =:S2 } C (1 ) + X 2k | {z =:S3 } C (2) S3 = (n ; 1)!C# (n ; k) : Durch Zusammensetzen aller Gleichungen bzw. Ungleichungen ergibt sich X C# (n) = n1! C () 2 n X X 1 = n! C () k=1 2k n 1X # # n! k=1 n! + (n ; 1)!C (k ; 1) + (n ; 1)!C (n ; k)] Entsprechend folgt Wenn alle Permutationen aus (k durchlauft, entstehen bei (k alle Permutationen von 1 : : : k ; 1, und zwar jede (n ; 1)!=(k ; 1)! mal, da (k ja insgesamt (n ; 1)! Permutationen enthalt. Also ist X ; 1)! S2 = ((nk ; 1)! 1 Permutation von 1:::k;1 C (1 ) = (n ; 1)!C# (k ; 1) : Hierin ist 2k X Fur alle 2 (k ergibt die erste Aufteilung in Quicksort die Teilarrays bestehend aus einer Permutation 1 von 1 2 : : : k ; 1 und einer Permutation 2 von k + 1 : : : n (da ja das Vergleichselement gerade k ist). Z () sei die Anzahl der Vergleiche mit der in die Teile 1 und 2 zerlegt wird. Dann ist fur alle 2 (k C () = Z () + C (1 ) + C (2 ) : Dabei ist Z () n (in der Implementation in Programm 10.3). Summiert uber alle 2 (k , so ergibt sich wegen j(k j = (n ; 1)!: j(k j = (n ; 1)! Fur n = 6 ergibt sich (1 = f213 312g (2 = f123 321g und (3 = f132 231g. In (k ist das Vergleichselement fest vorgeschrieben, die anderen Komponenten konnen jedoch in jeder Reihenfolge auftreten. Also ist (k = f 2 ( j das Vergleichselement hat den Wert kg : Wir werden jetzt C# (n) nach oben abschatzen. Dafur teilen wir die Menge ( aller Permutationen in die Mengen (1 (2 : : : (n , wobei 210 n k=0 n k=0 211 C# (n) n + n2 k=2 nX ;1 C# (k) fur n 2 : k=2 nX ;1 r (k ) n 2 n + n2 k=2 nX ;1 c k ln k (nach Induktionsvoraussetzung) 4 ln = loge n (naturlicher Logarithmus). Um diesen Ausdruck weiter nach oben abzuschatzen, betrachten wir die Funktion f (x) = x ln x. r(n) = n + n2 r(k) k=2 nX ;1 Beweis: Der Beweis erfolgt durch vollstandige Induktion nach n. Induktionsanfang : Fur n = 2 ist r(2) = 1. Andererseits ist c 2 ln 2 c 1 39. Also gilt der Induktionsanfang. Induktionsvoraussetzung : Die Behauptung gelte fur 2 3 : : : n ; 1. Schlu auf n: mit c = 2.4 r(n) c n ln n : mit den Anfangswerten r(0) = r(1) = 0 r(2) = 1 gilt fur alle n 2 r(n) = n + n2 Lemma 10.2 Fur die Losung r(n) der Rekursionsgleichung so ist Wir haben damit eine Rekursionsgleichung fur C# (n) gefunden. Beachtet man noch die Anfangswerte C# (0) = C# (1) = 0 C# (2) = 1 = n + n C# (k) : k=0 ;1 2 nX n n n X 1)! X 1)! X C# (k ; 1) + (n ; C# (n ; k) = nn!! 1 + (n ; n ! n ! k=1 k=1 k=1 nX ;1 nX ;1 = n + 1 C# (k) + 1 C# (k) 10.4. QUICKSORT nX ;1 nX ;1 Zn KAPITEL 10. SORTIEREN IN ARRAYS 2 x ln x dx n 2 3 4 n;1 n x r(n) |4 =:R(n) {z 2 } n k=2 n k=2 2 2! n n c n+ n 2 ln n ; 4 = n + 2c n ln n ; 4c n = c n ln n + 4 ; c n ; c n ln n nX ;1 nX ;1 n + 2 c k ln k = n + c k ln k Abbildung 10.10: Verlauf von f (x) = x ln x. 1 f (x) = x log x 2 2 = n2 ln n ; 2 ln 2 ; n4 ; 1] n2 ln n ; n2 2 4 n Z 2 = x2 ln x ; x2 dx (partielle Integration) 2 2 c n ln n falls R(n) 0 : Nun ist wegen c = 2 fur n 2 R(n) = n2 ; n ln n n ; n ln 2 = n ; n 0 69 < 0 : 2 2 Hieraus folgt k=2 k ln k Dann ist k ln k gerade der Flacheninhalt der schraerten Flache unter der Kurve f (x), k=2 siehe Abbildung 10.10. Also gilt: 212 213 Ein Heap (auch Priority Queue genannt) ist eine abstrakte Datenstruktur mit folgenden Kennzeichen: Wertebereich: Eine Menge von Werten des (homogenen) Komponententyps. Alle Komponenten besitzen einen Wert (Schlussel). Operationen: 1. Einfugen einer Komponente. 10.5.1 Die Grobstruktur von Heapsort Heapsort basiert im Gegensatz zu Mergesort und Quicksort nicht auf dem Prinzip der Aufteilung, sondern nutzt eine spezielle Datenstruktur (den Heap ), mit der wiederholt auf das grote Element eines Arrays zugegrien wird. Es ahnelt damit eher einem verbesserten Selection Sort. 10.5 Heapsort Quicksort arbeitet also im Mittel beweisbar sehr schnell, und dies wird auch in allen Laufzeituntersuchungen bestatigt. Quicksort ist der Sortieralgorithmus, den man verwenden sollte. A#(n) = O(n log n): Entsprechend kann man fur die mittlere Anzahl A#(n) von Zuweisungen beweisen (Ubung) fur alle n 2. Also ist C# (n) = O(n log n) mit der O-Konstanten 2= log e 2 89. 2 n C# (n) 2 n ln n = 2 n log log e = log e n log n Beweis: Aus C# (n) r(n) und Lemma 10.2 folgt Satz 10.6 Fur die mittlere Anzahl C# (n) der Vergleiche zum Sortieren eines n-elementigen Arrays mit Quicksort gilt C# (n) = O(n log n) : Aus dem Lemma folgt: 10.5. HEAPSORT betragt O(n log n). Satz 10.7 Der Worst Case Aufwand zum Sortieren eines n-elementigen Arrays mit Heapsort Dabei ist n = vSize, und werden sowohl Vergleiche als auch Zuweisungen von Arraykomponenten berucksichtigt. Hieraus folgt sofort: Entfernen des groten Elements in O(log n). Zugri auf das grote Element in O(1). Initialisierung in O(n). Worst Case-Aufwand der hier gewahlten Heap Implementation Die Korrektheit des Algorithmus ist oensichtlich, und auch die Ahnlichkeit zu Selection Sort. Um zu einem schnellen Algorithmus zu kommen, mu man den Heap so implementieren, da die benotigten Operationen schnell ausgefuhrt werden konnen. In Heapsort sind dies die Operationen 2 (in 3.1) und 3 (in 3.3), die jeweils vSize mal nacheinander ausgefuhrt werden. Hinzu kommt das initialisieren des Heaps in Schritt 2. Wir brauchen oenbar nicht alle Heapoperationen (4 wird nicht benotigt) und die anderen Operationen nur in bestimmter Reihenfolge (Einfugen nur bei der Initialisierung, Zugri und Entfernen stets nacheinander). Daher werden wir keinen allgemeinen Heap verwenden (vgl. dazu CLR90]), sondern die benotigten Heapoperationen direkt im Array vec implementieren, und zwar so, da gilt: 3.1 Greife auf das grote Element des Heaps zu. 3.2 Weise diesen Wert der Arraykomponente vec"i] zu. 3.3 Entferne das grote Element aus dem Heap. 3. for i := vSize ; 1 downto 0 do 2. Initialisiere den Heap mit den Komponenten von vec. 1. Gegeben sei das Array vec"] mit vSize Komponenten. Grobstruktur von Heapsort Statt des groten Wertes wird auch oft der kleinste Wert in 2. und 3. genommen. Bei der Anwendung in Heapsort sind die Komponenten von vec die Elemente des Heaps, und die Schlussel vec"i].key sind die Werte. Dann arbeitet Heapsort nach folgender Idee: 4. Anderung des Wertes einer Komponente. = 0 1 2 3 4 5 35 4 72 5 63 18 12 6 44 ; @ ; @ 2 HH H 7 vec"0] ist die Wurzel des Baumes. 2 j vSize k haben Sohne. Wir sagen, da vec die Heapeigenschaft (heap ordering ) erfullt, wenn fur i = 0 : : : vSize ; 1 gilt: Beweis: Ubung. d) Nur Knoten vec"i] mit i c) Der rechte Sohn von vec"i] (falls vorhanden) ist vec"2i+2]. b) Der linke Sohn von vec"i] (falls vorhanden) ist vec"2i+1]. a) Lemma 10.3 Fur einen Knoten v in einem binaren Baum bezeichnet man die von v aus uber eine gerichtete Kante erreichbaren Knoten als Sohne und nennt v den Vater dieser Sohne. Bei 2 Sohnen unterscheidet man (bzgl. einer gegebenen Darstellung) zwischen linkem und rechtem Sohn. Mit diesen Bezeichnungen gelten fur den Baum zu einem Heap folgende Eigenschaften: Abbildung 10.11: Array als Heap. 7 53 ; ; 3 24 ; @ ; @ 1 0 6 63 24 12 53 72 18 44 35 ergibt sich so der in Abbildung 10.11 dargestellte Baum. vec Die Grundidee besteht darin, sich das Array vec als binaren Baum vorzustellen, wobei die Komponenten der Reihe nach in die Schichten 0 1 2 : : : von links nach rechts angeordnet werden. Fur das Standardbeispiel 215 3. Entfernen der Komponente mit dem groten Wert. 10.5. HEAPSORT 10.5.2 Die Implementation des Heaps KAPITEL 10. SORTIEREN IN ARRAYS 2. Zugri auf die Komponente mit dem groten Wert. 214 i < < vec"i].keyvec"2i+1].key falls 2 + 1 vSize vec"i].key vec"2i+2].key falls 2 + 2 vSize i KAPITEL 10. SORTIEREN IN ARRAYS Die Korrektheit von Heapify sieht man wie folgt: Seien r und s die Sohne von top und seien b = vec"r], c = vec"s] und a = vec"top] die zugehorigen key-Werte. In den dazugehorigen Teilbaumen (siehe Abbildung 10.12 gilt nach Voraussetzung die Heapeigenschaft. O. B. d. A. sei r der groere Sohn (also b c) und b > a. Dann werden die Inhalte der Komponenten top und r getauscht. Nach dem Tausch ist b = vec"top].key > a c und somit die Heapeigenschaft in top erfullt. Im rechten Teilbaum gilt sie unverandert. Im linken Teilbaum konnte sie verletzt sein, weshalb der rekursive Aufruf von Heapify fur r notig wird (und per Induktionsannahme die Herstellung der Heapeigenschaft in diesem Teilbaum sichert). Als Beispiel betrachten wir in Abbildung 10.13 das Einfugen von 27 als Wurzel in einen Baum, dessen Teilbaume beide die Heapeigenschaft erfullen. Durchgezogene Linien bedeuten dabei, da die Heapeigenschaft erfullt ist, gepunktete Linien den durch Heapify durchgefuhrten Vergleich mit den Sohnen. Es folgt eine C++ Implementation von Heapify: 1. Ermittle den groeren der beiden Sohne (child) von top (falls keine Sohne existieren, so ist die Heapeigenschaft trivialerweise erfullt, falls nur ein Sohn existiert, so nehme diesen). 2. Vergleiche vec"child] mit vec"top]. Falls vec"child].key > vec"top].key so tausche vec"top] und vec"child]. 3. Wende Heapify rekursiv auf child an. Erfullt vec die Heapeigenschaft, so kann also auf die grote Komponente vec"0] in O(1) Zeit zugegrien werden. Wenn wir sie entfernen, so haben beide Teilbaume noch die Heapeigenschaft. Wenn wir dann ein neues Element an die Wurzel stellen (einfugen ), so mussen wir die Heapeigenschaft wieder herstellen. Dazu verwenden wir die Funktion Heapify. Sie setzt voraus, da die Heapeigenschaft bereits im Bereich vec"top+1]: : :vec"bottom] gilt, fugt vec"top] hinzu und ordnet die Komponenten so um, da hinterher die Heapordnung im Bereich vec"top]: : :vec"bottom] gilt. Dazu werden folgende Schritte ausgefuhrt: a) vec"0].key ist der gro te auftretende Schlusselwert. b) Entlang jeden Weges von einem Blatt zu der Wurzel sind die Schlusselwerte aufsteigend sortiert. Lemma 10.4 Erfullt vec die Heapeigenschaft, so gilt: Fur jeden Knoten vec"i] mit einem oder zwei Sohnen ist also der key-Wert der Sohne nicht groer als der des Vaters. Hieraus ergibt sich direkt: 216 7 - 7 3 b s c HH A A AA - r a A A AA b 53 24 5 63 5 44 18 6 12 6 12 ; @ @ ; 2 HH H 18 7 - 7 - 27 53 4 24 0 4 ; @ ; @ 1 35 ; ; 3 35 3 ; ; p53 p 27p p24 1 0 5 63 5 63 Abbildung 10.13: Ein Beispiel fur Heapify. 4 0 24 ; @ ; @ 1 4 ; @ ; @ ; @ ; @ 53 2 1 0 p63 p p p 27 p p p p44 s 2 44 A A 18 2 44 6 12 6 12 ; @ ; @ HH H 18 AA ; @ ; @ HH H c H H HH top Abbildung 10.12: Zur Korrektheit von Heapify a H H AA A A p p27 35 3 35 top ; ; r 10.5. HEAPSORT 217 KAPITEL 10. SORTIEREN IN ARRAYS CreateHeap void CreateHeap( /* inout */ Item vec"], /* in */ int vSize) //...................................................................... // PRE: Assigned(vec"0].key ... vec"vSize-1].key) // POST: vec"0].key ... vec"vSize-1].key fulfill the heap ordering //...................................................................... Programm 10.5 Heapify lat sich nun einfach zum Herstellen der Heapeigenschaft im gesamten Array vec nutzen, indem man das Array von hinten nach vorn durchlauft und in jeder Komponente i (die Sohne hat) Heapify(vec,i,vSize-1) aufruft. Vor dem Aufruf erfullt vec"i+1]: : :"vSize-1] bereits die Heapeigenschaft, und der Aufruf stellt die Heapeigenschaft fur vec"i]: : :vec"vSize-1] her. Eine C++ Implementation lautet: } // check if exchange is necessary if ( vec"top].key < vec"child].key ) { temp = vec"top] vec"top] = vec"child] vec"child] = temp // recursive call for possible further exchanges Heapify( vec, child, bottom ) }//endif if ( 2*top+2 > bottom ) // 2*top+1 is only child of top child = 2*top+1 else { // 2 sons, determine bigger one if ( vec"2*top+1].key > vec"2*top+2].key ) child = 2*top+1 else child = 2*top+2 }//endif if ( 2*top+1 > bottom ) return // nothing to do Heapify void Heapify( /* inout */ Item vec"], /* in */ int top, /* in */ int bottom) //...................................................................... // PRE: Assigned(vec"top].key ... vec"bottom].key) // && vec"top+1].key ... vec"bottom].key fulfill heap ordering // && 0 <= top <= bottom <= no. of components - 1 // POST: vec"top] is inserted into the right place such that // vec"top].key ... vec"bottom].key fulfill the heap ordering //...................................................................... { Item temp int child Programm 10.4 218 for ( int i = vSize/2 - 1 i >= 0 i-- ) Heapify( vec, i, vSize-1 ) 219 3 35 1 4 53 24 5 5 2 18 6 6 5 72 18 2 44 6 12 ; @ ; @ HH H 18 ; @ ; @ 12 44 7 i =-1 7 3 35 1 4 24 0 5 6 5 6 12 Programm 10.6 HeapSort Mit Heapify und CreateHeap lat sich Heapsort jetzt einfach implementieren: 18 ; @ ; @ 2 2 p72 p p p 63 p p p p44 4 ; @ ; @ 53 ; ; 3 35 ; ; 1 0 p24 p p p 63 p p p p12 pp pp pp pp 53 72 18 44 Abbildung 10.14: Ein Beispiel fur CreateHeap. ; @ ; @ 63 0 72 1 ; ; 3 35 4 0 72 p24 p p p 63 p p p p44 pp pp 53 ; ; 4 2 i =-3 10.5.3 Die Implementation von Heapsort 7 i =-0 7 i =-2 7 3 p p53 35 1 0 p24 p p p 63 p p p p12 pp pp pp pp Im Standardbeispiel ergeben sich die in Abbildung 10.14 dargestellten Zustande nach jedem Durchlauf der Schleife. Dabei deuten durchgezogene Linien bereits hergestellte Heapbedingungen an. } { 10.5. HEAPSORT KAPITEL 10. SORTIEREN IN ARRAYS Abbildung 10.16. Also ist hX ;1 ! i=0 2i + 1 n i=0 h X 2i : Beweis: In T sind alle Schichten voll bis eventuell auf die letzte. Also sind in Schicht i fur i = 0 : : : h ; 1 genau 2i Knoten, und in Schicht h zwischen 1 und 2h Knoten (vergleiche a) 2h n 2h+1 ; 1 b) h blog nc gilt: Lemma 10.5 Sei T ein voller binarer Baum mit n Knoten. Sei h die Hohe von T . Dann Zur Vorbereitung der Analyse betrachten wir die Interpretation des Arrays als Baum genauer: Da die Komponenten des Arrays schichtweise im Baum angeordnet sind, hat er die Eigenschaft, da alle Schichten i bis auf eventuell die letzte voll sind, d. h. 2i Knoten enthalten. Ein solcher Baum heit voller (binarer) Baum. 10.5.4 Die Analyse von Heapsort Die Komponenten vec"0]: : :vec"last] bilden also den aktuellen Heap (mit jeweiligem groten Element vec"0]), und die Komponenten vec"last+1]: : :vec"vSize-1] den bereits sortierten Teil des Arrays. Im Standardbeispiel ergeben sich jeweils beim Eintritt in die for-Schleife die in Abbildung 10.15 dargestellten Heaps. Die noch verbundenen Komponenten des Arrays stellen den jeweiligen Heap vec"0]: : :vec"last] dar, die Aktionen von Heapify werden nicht mehr dargestellt. } CreateHeap( vec, vSize ) for ( last = vSize-1 last > 0 last-- ) { // exchange top component with current last component of vec temp = vec"0] vec"0] = vec"last] vec"last] = temp // call Heapify to reestablish heap property Heapify( vec, 0, last-1) }//endfor void HeapSort( /* inout */ Item vec"], /* in */ int vSize) //...................................................................... // PRE: Assigned(vec"0].key ... vec"vSize].key) // POST: vec"0].key <=... <= vec"vSize].key //...................................................................... { Item temp int last 220 72 3 7 72 3 =2 7 - last 72 3 =4 7 - last 35 =6 - last 7 4 53 1 35 4 12 4 35 44 5 18 5 35 5 53 5 72 53 2 53 2 6 6 24 6 18 6 44 HH H 18 2 ; ; HH H 18 44 H 12 63 63 63 ; @ ; @ 2 HH 7 - last = 7 - last = 7 - last = 7 - last = 72 3 1 72 3 3 72 3 5 72 3 7 35 1 35 1 12 4 18 4 12 44 0 44 0 24 4 0 24 35 4 ; @ ; @ 1 35 53 ; @ ; @ 1 0 5 12 5 24 5 44 5 63 Abbildung 10.15: Ein Beispiel fur HeapSort. 12 1 0 44 24 ; ; 0 24 4 1 12 ; @ ; @ 0 24 0 ; @ ; @ 63 1 ; ; 3 10.5. HEAPSORT 44 53 2 53 2 6 6 24 6 18 6 18 HH H 53 2 HH H 18 2 12 63 63 63 ; @ ; @ HH H 221 Schicht 0 20 Knoten Schicht 1 21 Knoten Schicht 2 22 Knoten Schicht h = 3 KAPITEL 10. SORTIEREN IN ARRAYS Abbildung 10.16: Ein voller Baum. u u u uu u uuu u QQ Q ;@ ;@ ; @ ; @ A A Also ist Schicht 0: ::: P1 (n) 2h;1 1 + 2h;2 2 + : : : + 2h;i i + : : : + 20 h hochstens h Prufaktionen. Schicht h ; i: hochstens i Prufaktionen pro Knoten der Schicht ::: Schicht h: keine Prufaktionen Schicht h ; 1: hochstens 1 Prufaktionen pro Knoten der Schicht Schicht h ; 2: hochstens 2 Prufaktionen pro Knoten der Schicht Anzahl der Prufaktionen nach der angestellten Voruberlegung durch die Anzahl der Schichten unterhalb des Knotens beschrankt ist, folgt: Beweis: Wir betrachten (wie in CreateHeap) die Schichten von unten nach oben. Da die Lemma 10.6 Fur die Anzahl P1 (n) der Prufaktionen beim Herstellen der Heapeigenschaft in einem n-elementigen Array gilt P1 (n) 2n : Betrachten wir jetzt den Aufwand fur einen Aufruf von Heapify einschlielich der dadurch erzeugten weiteren rekursiven Aufrufe. Bei jedem Aufruf, bei dem ein Austausch erfolgt, \sinkt" das betrachtete Element um eine Stufe nach unten im Baum. Die Anzahl der Folgeaufrufe durch Rekursion ist also durch die Anzahl der Schichten unterhalb der Ausgangsstufe des Elements beschrankt. Diese Beobachtung ist der Kern der folgenden Abschatzung. Wir bezeichnen die Uberprufung und ggf. Herstellung der Heapeigenschaft fur einen Knoten mit seinen Sohnen als eine Prufaktion . also a). Aus 2h n folgt h log n und somit h blog nc, da h eine ganze Zahl ist. 2h n 2h+1 ; 1 P Wegen ki=0 2i = 2k+1 ; 1 ergibt sich 222 2i i = 2 h 2h X i=1 log(n ; i) i=1 log n = (n ; 1) log n : 3 P1 (n) + 3 (n ; 1) + 3 P2 (n) 9n + 3n log n 5n log n fur n 23 : 2 P1 (n) + 2 P2 (n) 4n + 2n log n 3n log n fur n 16 Sortieralgorithmen werden in allen Buchern uber Algorithmen und Datenstrukturen behandelt, vom Klassiker Knu73] bis zu CLR90]. Die Implementationen der Algorithmen lehnt sich zum Teil an HR94] an. 10.6 Literaturhinweise A(n) C (n) Beweis: Nachrechnen ergibt: Satz 10.8 Fur die Anzahl C (n) der Vergleiche und die Anzahl A(n) der Zuweisungen bei Heapsort gilt: C (n) = O(n log n) A(n) = O(n log n) : Pro Prufaktion erfolgen 2 Vergleiche und maximal 3 Zuweisungen (1 Austausch). Hinzu kommen jeweils 3 Zuweisungen (1 Austausch) in der for-Schleife von HeapSort. Hieraus folgt: P2 (n) nX ;1 = 2: h hX i i i=1 2 i=1 1 X h da 2 n und 2ii i=1 2h;i i = nX ;2 2n i=1 h X 223 Die Anzahl P2 (n) der Prufaktionen in HeapSort (auerhalb von CreateHeap) ergibt sich analog. Im i-ten Durchlauf der for-Schleife wird Heapify fur die Komponente mit Index 0 und den Heap mit n ; i Komponenten aufgerufen, dessen Hohe nach Lemma 10.5 hochstens log(n ; i) ist. Also folgt = 10.6. LITERATURHINWEISE KAPITEL 10. SORTIEREN IN ARRAYS Die allgemeine Version des Aufteilungs-Beschleunigungssatzes ist aus CLR90]. Dort nden sich auch weitere Details zur Beschleunigung der Matrixmultiplikation. Die Anwendung auf die Multiplikation n-stelliger Dualzahlen ist aus AHU83]. 224 225 Dieser Satz zeigt also, da Mergesort, Heapsort und Quicksort bzgl. der Groenordnung optimal sind, und da Laufzeitunterschiede hochstens der O-Konstanten zuzuschreiben sind. Man beachte noch einmal den Unterschied zu den bisher gemachten O(: : :) Abschatzungen fur ein Problem. Diese haben wir dadurch erhalten, da ein konkreter Algorithmus, der das Problem lost, analysiert wurde. Die im Satz formulierte %(: : :) Abschatzung bezieht sich jedoch auf alle moglichen Sortierverfahren (bekannte und unbekannte). Sie macht also eine Aussage uber eine Klasse von Algorithmen statt uber einen konkreten Algorithmus und ist damit von ganz anderer Natur. Satz 11.1 (Untere Schranken fur das Sortieren mit Vergleichen) Jeder deterministische Sortieralgorithmus, der auf paarweisen Vergleichen von Schlusseln basiert, braucht zum Sortieren eines n-elementigen Arrays sowohl im Worst Case, als auch im Mittel (bei Gleichverteilung) %(n log n) Vergleiche. Die besten bisher kennengelernten Sortierverfahren fur Arrays haben einen Aufwand von O(n log n) im Worst Case (Mergesort, Heapsort), bzw. im Average Case (Quicksort). Es stellt sich nun die Frage, ob es noch bessere Sortierverfahren geben kann. Dies kann tatsachlich der Fall sein, wenn man zusatzliche Informationen uber die Schlusselmenge hat, wie das Verfahren Bucketsort in Kapitel ?? zeigt. Fur eine groe Klasse von Algorithmen, die alle in Kapitel 10 behandelten Algorithmen umfat, ist dies jedoch nicht der Fall. Um dies zu zeigen, benotigen wir noch einen Begri. Ein Algorithmus heit deterministisch , wenn der Ablauf des Algorithmus nur vom Input abhangt (und nicht etwa von im Algorithmus ausgefuhrten Zufallsexperimenten wie bei sogenannten randomisierten Algorithmen). Mit diesem Begri gilt dann: Untere Komplexitatsschranken fur das Sortieren Kapitel 11 DAS SORTIEREN KAPITEL 11. UNTERE KOMPLEXITATSSCHRANKEN FUR fertig sortierte Arrays .. . 2. Vergleich beim Mischen 1. Vergleich beim Mischen Abbildung 11.1: Der Entscheidungsbaum fur Mergesort. w4 w1 w3 w2 w4 w3 w1 w2 HHnein HH ppp ppp w2 < w4 @ nein @ @ Vergleich fur 2. Teilfolge Vergleich fur 1. Teilfolge erzeugt einen solchen Entscheidungsbaum T . Satz 11.2 Jeder deterministische Sortieralgorithmus, der auf paarweisen Vergleichen basiert, w4 w1 w2 w3 @ @ ja ;;@@ nein ; ; w1 < w3 @ @ @ p ppp nein p p w2 < w3 ja ; ; ; ;@ w3 < w4 nein HH H HH HH H @ ja ppp HH w1 < w4 @ nein @ @ w2 < w3 w1 < w3 ppp ppp ja ; ; ; ;@ w3 < w4 ja w1 < w2 Als Beispiel betrachten wir einen Ausschnitt des Entscheidungsbaums fur das Sortieren von w1 w2 w3 w4 mit Mergesort. Es erfolgt also zunachst die Aufteilung in die Teilfolgen w1 w2 und w3 w4 . Diese werden dann sortiert und gemischt. Es entsteht der in Abbildung 11.1 Baum. Ein Weg von der Wurzel bis zu einem Blatt entspricht im Algorithmus der Folge der angestellten Vergleiche. Dabei wird vereinbart, da beim Weitergehen nach links bzw. rechts der letzte Vergleich richtig (true) bzw. falsch (false) ist. Blatter des Baumes sind die sortierten Arrays, also n! bei n zu sortierenden Elementen. Innere Knoten des Entscheidungsbaums sind Vergleiche im Algorithmus. Zum Beweis des Satzes werden wir die Tatsache nutzen, da jeder deterministische Sortieralgorithmus, der nur auf paarweisen Vergleichen von Schlusseln basiert, durch einen Entscheidungsbaum wie folgt beschrieben werden kann. 11.1 Das Entscheidungsbaum-Modell 226 v Blatt von T X h(v) = n1! H (T ). Beweis: Der Beweis wird in beiden Fallen durch vollstandige Induktion nach der Hohe h(T ) von T gefuhrt. Ist h(T ) = 0, so besteht T nur aus der Wurzel, P die zugleich ein Blatt ist. Also ist b = 1 und log b = 0 = h(T ). Entsprechend ist H (T ) = v Blatt h(v) = 0 und b log b = 0. b) H (T ) b log b a) h(T ) log b Lemma 11.2 Sei T ein binarer Baum mit b Blattern. Dann gilt: Die Abschatzung von C (n) bzw. C (n) nach unten reduziert sich also auf die Abschatzung der Hohe bzw. der Blatterhohensumme eines binaren Baumes mit n! Blattern nach unten. Dazu zeigen wir folgendes Lemma. 11.2 Analyse des Entscheidungsbaums h(v). Also folgt a) direkt. Da wir Gleichverteilung der n! verschiedenen Eingabereihenfolgen (und damit Ausgabereihenfolgen) annehmen, folgt b). Beweis: Die Anzahl der Vergleiche, um zu einer sortierten Ausgabe v zu kommen, ist gerade b) C (n) = n1! a) C (n) = vmax h(v) = h(T ), Blatt die Worst Case bzw. Average Case (bei Gleichverteilung) Anzahl von Vergleichen bei n zu sortierenden Komponenten. Dann gilt: Lemma 11.1 Sei T der Entscheidungsbaum fur den Algorithmus A und C (n) bzw. C (n) Wir uberlegen nun, wie wir den Worst Case bzw. Average Case Aufwand C (n) bzw. C (n) des Algorithmus im Baum T ablesen konnen. Sei dazu h(v) die Hohe des Knoten v im Baum T , h(T ) die Hohe von T , und H (T ) := Pv Blatt h(v) die sogenannte Blatterhohensumme von T . Vergleich zwischen Arraykomponenten. Dieser bildet die Wurzel des Entscheidungsbaums. In Abhangigkeit vom Ausgang des Vergleichs (\<" oder \>") ist der nachste Vergleich wiederum eindeutig bestimmt. Die Fortsetzung dieser Argumentation liefert fur jede Eingabefolge eine endliche Folge von Vergleichen, die einem Weg von der Wurzel bis zu einem Blatt (sortierte Ausgabe) entspricht. 227 Beweis: Da der Algorithmus deterministisch ist, hat er fur jede Eingabefolge denselben ersten 11.2. ANALYSE DES ENTSCHEIDUNGSBAUMS DAS SORTIEREN KAPITEL 11. UNTERE KOMPLEXITATSSCHRANKEN FUR | b1 {z } | b2 {z } HH A A A A T 2 A A 9 > > = h2 > > b + b1 log b1 + b2 log b2 (Induktionsvoraussetzung) = b + b1 log b1 + (b ; b1 ) log(b ; b1 ) : b + xmin x log x + (b ; x) log(b ; x)] : 21b] Die Funktion f (x) := x log x + (b ; x) log(b ; x) hat auf dem Interval 1 b] in etwa den in Abbildung 11.3 dargestellten Verlauf. H (T ) Da wir nicht genau wissen, wie gro b1 ist, fassen wir die rechte Seite als Funktion von x = b1 auf und suchen ihr Minimum. Also ist H (T ) Induktionsvoraussetzung anwendbar und es folgt H (T ) = b + H (T1 ) + H (T2 ) da in T jedes Blatt gegenuber T1 und T2 eine um 1 groere Hohe hat. Auf T1 und T2 ist die Dies beweist a). im Fall b) gilt h(T ) = 1 + maxfh1 h2 g 1 + h1 1 + log b1 (Induktionsvoraussetzung) = log 2 + log b1 = log(2 b1 ) log(b1 + b2 ) (da b1 b2 ) = log b: Jeder der Teilbaume hat eine geringere Hohe als h, also trit auf T1 und T2 die Induktionsvoraussetzung zu. Sei bi die Anzahl der Blatter und hi die Hohe von Ti (i = 1 2), und sei o. B. d. A. b1 b2 . Dann gilt: Abbildung 11.2: Teilbaume im Beweis von Lemma 11.2 8 > > > > < h1 > > > > : u HH HH A A A A A T1 A A A A T Es gelten nun a), b) fur h(T ) = 0 1 : : : h ; 1 (Induktionsvoraussetzung). Zum Schlu auf h betrachte man die beiden Teilbaume T1 und T2 von T , wobei einer leer sein kann, vgl. Abbildung 11.2 228 2 3 b=2 b C (n) = n1! H (T ) n1! (n! log n!) = log n! p 1 ergibt sich aus der Stirlingschen Formel n! = 2n( nl )n (1 + ( n1 )), speziell p Einen ngenauere pAbschatzung 2n( e ) n! 2n( ne )n+(1=12n) . Entsprechend ist = n3 log n + n6 log n ; n2 n log n fur n log n n also n 8 3 6 2 = %(n log n): Nach diesen Vorbereitungen kommen wir jetzt zum Beweis von Satz 11.1: Sei T der Entscheidungsbaum zum gegebenen Sortieralgorithmus A. Bei einem Inputarray der Lange n hat T n! Blatter. Es folgt C (n) = h(T ) log n! n n n n log (n=2)n=2 ] = log = log n ; 2 2 2 2 bn=2c+1 Faktoren Fur die endgultige Abschatzung benotigen wir noch eine Abschatzung von n! nach unten. Es ist1 n! = n(n ; 1) 2 1 n| (n ; 1){z dn=2e} dn=2ebn=2c+1 (n=2)n=2 : H (T ) b + 2b log 2b + (b ; 2b ) log(b ; 2b ) = b + b log 2b = b + b(log b ; log 2) = b + b(log b ; 1) = b log b : 229 Eine Kurvendiskussion zeigt, da f (x) das Minimum auf 1 b] bei x = b=2 annimmt (Ubung). Damit ist Abbildung 11.3: Verlauf der Funktion f (x) := x log x + (b ; x) log(b ; x). 1 11.2. ANALYSE DES ENTSCHEIDUNGSBAUMS = %(n log n) (wie oben). DAS SORTIEREN KAPITEL 11. UNTERE KOMPLEXITATSSCHRANKEN FUR Untere Schranken fur Sortieralgorithmen werden in CLR90, Meh88, OW90] behandelt. Meh88] geht auch ausfuhrlich auf Erweiterungen solcher Schrankenresultate ein und zeigt unter anderem, da durch Randomisierung , also die zufallsgesteuerte Wahl der Vergleiche, keine Beschleunigung moglich ist. Die erwartete Anzahl der Vergleiche betragt immer noch (n log n) Vergleiche. 11.3 Literaturhinweise werden auch als informations-theoretische Schranke fur das Sortieren bezeichnet. Sie lassen sich anschaulich folgendermaen interpretieren. Jeder Sortieralgorithmus mu zwischen n! Moglichkeiten (den sortierten Reihenfolgen) unterscheiden und mu daher log n! Bits an Information sammeln. Ein Vergleich ergibt hochstens ein Bit Information. C (n) log n! C (n) log n! Dieses Ergebnis, bzw. genauer die Ungleichungen 230 231 i=0 jede naturliche Zahl z mit 0 z bn ; 1 (und n 2 IN) eindeutig als Wort der Lange n uber )b darstellbar durch nX ;1 z = z i bi Satz 12.1 (b-adische Darstellung naturlicher Zahlen) Sei b 2 IN mit b > 1. Dann ist Naturliche Zahlen konnen in jedem b-adischen Zahlensystem dargestellt werden: 1. Dem Dezimalsystem liegt das Alphabet )10 = f0 1 2 : : : 9g zugrunde. Worte uber diesem Alphabet sind etwa 123, 734, 7806. Feste Wortlange, etwa n = 4, erreicht man durch \fuhrende Nullen": 0123, 0734, 7806. 2. )2 = f0 1g: Dual- oder Binaralphabet )8 = f0 1 2 3 4 5 6 7g: Oktalalphabet )16 = f0 : : : 9 A : : : F g: Hexadezimalalphabet Man beachte, da )16 strenggenommen das Alphabet f0 : : : 15g bezeichnet anstelle der \Ziern" 10, 11 usw. werden jedoch generell, d. h. in allen Alphabeten )b mit b > 9, \neue" Symbole A B usw. (hier also A : : : F ) verwendet. Diese Basen b = 2 8 bzw. 16 spielen in der Informatik eine besondere Rolle. 3. Alte Basen sind b = 12 (Dutzend) und b = 60 (Zeitrechnung). Beispiel 12.1 Die Darstellung von Zahlen (ganz allgemein von Daten) basieren auf sogenannten Zahlensystemen . Diese wiederum nutzen Zeichen eines sogenannten Alphabets zu ihrer Darstellung. Ist b > 1 eine beliebige naturliche Zahl, so heit die Menge )b := f0 1 : : : b ; 1g das Alphabet des b-adischen Zahlensystems . 12.1 Zahlensysteme Zahlendarstellungen und Rechnerarithmetik Kapitel 12 end i := i + 1 while z > 0 do begin zi := z mod b z := z div b i := 0 Algorithmus 12.1 for i := 0 to n ; 1 do zi := 0 Aus dem Beweis erhalt man direkt einen Algorithmus zur Umwandlung in die b-adische Darstellung: Da 1 b` gerade eine Einheit der hoherwertigen Stelle ` ist, kann diese Einheit nicht \wettgemacht" werden. Also ergibt sich ein Widerspruch, d. h. beide Darstellungen mussen gleich sein. (b ; 1)b0 + (b ; 1)b1 + + (b ; 1)b`;1 = (b ; 1)(b0 + b1 + + b`;1 ) ` = (b ; 1) bb ;; 11 = b` ; 1 < b` : Nun hat aber der grote durch niedrigwertige Stellen erreichbare Wert die Form Sei dann ` der grote Index mit z`1 6= z`2 , o. B. d. A. z`1 > z`2 . Dann mussen die niedrigwertigen Stellen z`2;1 : : : z02 eine Einheit der hoherwertigen Stelle ` \wettmachen". z = (zn1 ;1 : : : z01 )b = (zn2;1 : : : z02 )b : mit zn0 ;1 = 0, da z 0 b z bn ; 1. Also ist (zn;1 : : : z0 ) mit zn;1 := zn0 ;2 zn;2 := zn0 ;3 : : : z1 := z00 und z0 := z mod b eine n-stellige Darstellung von z in )b. Angenommen, es gibt zwei verschiedene solche Darstellungen von z , etwa z 0 = (zn0 ;1 zn0 ;2 : : : z10 z00 ) Induktionsanfang : z < b hat eine eindeutige Darstellung mit z0 = z und zi = 0 sonst. Induktionsvoraussetzung : Die Behauptung sei richtig fur alle Zahlen 0 1 : : : z ; 1. Schlu auf z b: Es ist z = (z div b) b + (z mod b). Da z 0 := z div b < z ist, hat z 0 nach Induktionsvoraussetzung die (eindeutige) Darstellung z = (zn;1 zn;2 : : : z1 z0 )b : i=0 nX ;1 zi 2 i (1)16 (6)16 (12)16 i=0 zi j =0 N iY ;1 X bj = z0 + z1 b0 + z2 b1 b0 + : : : + zN bN ;1 : : : b0 Beweis: Ubung. mit 0 zi < bi fur i = 0 : : : N . z= turlicher Zahlen mit bn > 1 fur alle n 2 IN. Dann gibt es fur jede naturliche Zahl z genau eine Darstellung der Form Satz 12.2 (Polyadische Darstellung naturlicher Zahlen) Es sei (bn)n2IN eine Folge na- folgender Satz zeigt. b-adische Zahlensysteme sind jedoch nicht die einzige Moglichkeit zur Zahldarstellung, wie (5)8 (5)8 (4)8 So ist zum Beispiel (364)10 = (101101100)2 . Zwischen )2 )8 und )16 bestehen besonders einfache Umwandlungsalgorithmen. Dies liegt daran, da eine Zier zi 2 )8 bzw. )16 gerade in )2 durch Worter der Lange 3 bzw. 4 dargestellt werden kann. Man kann daher die Umwandlungen )2 $ )8 und )2 $ )16 ziernweise vornehmen. Ein Beispiel ist: 100 )2 = (554)8 = (0001 (364)10 = (101101100)2 = (101 101 |{z} | {z } |0110 {z } |1100 {z })2 = (16C )16 |{z} |{z} mit zi 2 )2 = f0 1g (i = 0 : : : n ; 1). z= Korollar 12.1 (Dualdarstellung naturlicher Zahlen) Sei n 2 IN. Dann ist jede naturliche Zahl z mit 0 z 2n ; 1 eindeutig darstellbar in der Form So ergibt (0554)8 den Dezimalwert ((0 8 + 5) 8 + 5) 8 + 4 = 364. z := 0 for i := n ; 1 downto 0 do z := z b + zi Algorithmus 12.2 Voraussetzung dieser Umwandlung ist also, da die mod und div Operation im Ausgangszahlensystem implementiert ist. Die Umkehrung b-adisch nach dezimal ist einfach: 233 Beweis: Der Beweis erfolgt durch Induktion nach z. 12.1. ZAHLENSYSTEME Als Beispiel betrachten wir die Umwandlung von z = (364)10 in eine Oktalzahl: 9 364 = 45 8 + 4 > = 45 = 5 8 + 5 > ) z = (554)8 5 = 08+5 KAPITEL 12. ZAHLENDARSTELLUNGEN UND RECHNERARITHMETIK mit zi 2 )b fur i = 0 : : : n ; 1. Als vereinfachende Schreibweise ist dabei auch folgendes ublich (\Ziernschreibweise") 232 KAPITEL 12. ZAHLENDARSTELLUNGEN UND RECHNERARITHMETIK Beispiel: K10 ((325)10 ) = (674)10 + (1)10 = (675)10 K2 ((10110)2 ) = (01001)2 + (1)2 = (01010)2 Das b-Komplement von x erhalt man, indem man das Kb;1 -Komplement bildet und 1 dazu addiert, d. h. Kb (x) = Kb;1(x) + 1 : Beispiel: K9 ((325)10 ) = (674)10 K1 ((10110)2 ) = (01001)2 Komplement-Darstellungen erlauben eine einfache Ruckfuhrung der Subtraktion auf die Addition. Sei x = (xn;1 : : : x0 )b eine n-stellige b-adische Zahl. Das (b ; 1)-Komplement Kb;1 (x) von x erhalt man, indem man stellenweise das \Komplement" (b ; 1) ; xi zu xi bildet. 12.2.2 Komplement Darstellungen Diese \naturliche" Darstellung ist auf Rechnern vollig unublich. Der Grund liegt darin, da hardwaremaig nur Addierwerke (plus Zusatzlogik) verwendet werden. Daher verwendet man Darstellungen, in denen die Subtraktion sehr einfach auf die Addition zuruckgefuhrt werden kann. Dies ist bei der Vorzeichen-Betrag Darstellung nicht der Fall. Wir haben daher in der Schule alle fur die Addition und Subtraktion verschiedene Algorithmen gelernt. Bitmuster Dezimalwert 000 0 001 1 101 2 011 3 100 ;0 101 ;1 110 ;2 111 ;3 Tabelle 12.1: 3-stellige Dualzahlen in Vorzeichen-Betrag Darstellung. Dies ist die im Alltag verwendete Darstellung. Hat man Worter der Lange n (n Bit) zur Darstellung zur Verfugung, wird das linkeste Bit fur das Vorzeichen verwendet (0 = + 1 = ;), und die restlichen Bits fur den Betrag der Zahl. Bei n = 3 lassen sich also die in Tabelle 12.1 angegebenen Zahlen darstellen. Da die 0 zwei Darstellungen hat, konnen so 2n ; 1 Zahlen dargestellt werden. 12.2.1 Die Vorzeichen-Betrag Darstellung 12.2 Darstellung ganzer Zahlen im Rechner 234 235 i=0 (b ; 1)bi = (b ; 1) nX ;1 nX ;1 i=0 bi b Man beachte, da man das Vorzeichen der in b-Komplement Darstellung gegebenen Zahl x an der Zier xn;1 ablesen kann. Wir betrachten einige Beispiele: b Bezeichnen wir mit (x)Kb = (xn;1 : : : x0 )Kb = die Ziern von x in der b-Komplement Darstellung, so gilt also: ( x 0 (x)Kb = ((xK)b(jxj)) falls (12.1) falls x < 0 : nicht-negative Zahlen durch ihre b-adische Darstellung, negative Zahlen x durch das b-Komplement ihres Betrages jxj. Aussage a) bedeutet anschaulich, da sich das b-Komplement von x bei n Stellen stets als Dierenz von x zu bn ergibt. Im Dezimalsystem mit n = 3 Stellen ergibt sich so fur x = 374 K10 (x) = 1000 ; 374 = 626. Komplemente lassen sich nun wie folgt zur Darstellung ganzer Zahlen verwenden. Wir betrachten zunachst das meistens verwendete Zweierkomplement (bzw. allgemein das b-Komplement). Sei nn die zur Verfugung stehende Wortlange. Im b-Komplement werden die bn Zahlen x mit n ;d b2 e x d b2 e ; 1 dargestellt. Dieser Bereich heit der darstellbare Bereich . Zahlen aus diesem Bereich werden wie folgt dargestellt: Also gilt b). a) folgt aus Kb (x) = Kb;1 (x) + 1. c) folgt aus a) und b). n = (b ; 1) bb ;;11 = bn ; 1: = x + Kb;1 (x) = (b ; 1 b ; 1 : : : b ; 1)b Beweis: Nach Denition des (b ; 1)-Komplements ist c) Kb;1 (Kb;1 (x)) = x Kb (Kb (x)) = x. b) x + Kb;1 (x) = bn ; 1 = (b ; 1 b ; 1 : : : b ; 1)b , a) x + Kb (x) = bn , Lemma 12.1 Fur jede n-stellige b-adische Zahl x gilt: Speziell im Dualsystem nennt man Kb;1 = K1 das Einerkomplement und Kb = K2 das Zweierkomplement , im Dezimalsystem spricht man bei Kb;1 = K9 vom Neunerkomplement und bei Kb = K10 vom Zehnerkomplement . Oenbar gilt: 12.2. DARSTELLUNG GANZER ZAHLEN IM RECHNER KAPITEL 12. ZAHLENDARSTELLUNGEN UND RECHNERARITHMETIK x + y = (x)Kb (y)Kb : (x)Kb (y)Kb = = = = = (Kb (jxj))b + (Kb (jyj))b mod bn Kb (jxj) + Kb (jyj) mod bn bn ; jxj + bn ; jyj mod bn ;jxj ; jy j = ;(jxj + jy j) x+y: an der hochsten Stelle entspricht einem Verlassen des darstellbaren Bereiches (Uberlauf ). Sind x y < 0 so ist nach Denition und Lemma12.1 Beweis: Sind x y 0, so ist (x)Kb (y)Kb = (x)b +(y)b mod bn = x+y mod bn. Ein Ubertrag stellbaren Bereich. Dann gilt: Satz 12.3 Seien x y n-stellige Zahlen in b-Komplement-Darstellung, und sei x + y im dar- Wir betrachten jetzt die Addition von Zahlen in dieser Darstellung. Dazu bezeichne (x)Kb (y)Kb die ziernweise Addition der Darstellungen von x und y, wobei ein eventueller Ubertrag auf die n + 1-Stelle zwar zur Interpretation des Ergebnisses genutzt werden kann, aber in der Darstellung des Ergebnisses verlorengeht. Man rechnet also modulo bn . Man beachte die Darstellung von x = ;4. jxj hat das Bitmuster (100)2 . Daraus ergibt sich K2 ((100)2 ) = K1 ((100)2 ) + (1)2 = (011)2 + (1)2 = (100)2 . Bitmuster Dezimalwert 000 0 001 1 010 2 011 3 100 ;4 101 ;3 110 ;2 111 ;1 Tabelle 12.2: 3-stellige Dualzahlen in Zweier-Komplement Darstellung. 2. Dualsystem, n = 3. Der darstellbare Bereich ist in Tabelle 12.2 angegeben. Zahl Darstellung 43 43 ;13 87 ;27 73 38 38 1. Dezimalsystem, n = 2. Der darstellbare Bereich ist ;50 x 49. Konkrete Darstellungen sind: 236 237 x ; jy j x+y: (x)b + (Kb (jyj))b mod bn x + Kb(jyj) mod bn x + bn ; jyj mod bn = = = = (27)K10 (12)K10 (27)K10 (85)K10 (27)K10 (66)K10 (73)K10 (79)K10 = = = = (39)K10 (12)K10 (93)K10 (52)K10 = 39 = 12 = ;7 = ;48 37 ; 28 = (37)K10 (K10 (28))K10 = (37)K10 (72)K10 = (9)K10 = 9 37 ; 48 = (37)K10 (K10 (48))K10 = (37)K10 (52)K10 = (89)K10 = ;11 ;12 ; 24 = (88)K10 (K10 (24))K10 = (88)K10 (76)K10 = (64)K10 = ;36 Betrachten wir wieder einige Beispiele im 2-stelligen Dezimalsystem. Beweis: Nach Lemma 12.1 ist y + Kb(y) = bn. Also ist ;y = Kb (y) ; bn. Daraus folgt wegen der modulo bn Rechnung: (;y)Kb = (Kb (y))Kb : Also ist x ; y = x + (;y) = (x)Kb (;y)Kb = (x)Kb (Kb (y))Kb : x ; y = (x)Kb (Kb (y))Kb : Satz 12.4 Seien x y n-stellige Zahlen in b-Komplement-Darstellung, und sei x ; y im darstellbaren Bereich. Dann gilt: Die Ruckfuhrung der Subtraktion auf die Addition beruht dann einfach auf der Gleichung x ; y = x + (;y): 27 + 12 27 + (;15) 27 + (;34) (;27) + (;21) Betrachten wir einige Beispiele im 2-stelligen Dezimalsystem. Dann ist n = 2 und ;50 x 49 der Bereich der darstellbaren Zahlen. Es folgt: (x)Kb (y)Kb = = = = = Ein Ubertrag an der hochsten Stelle entspricht einem Verlassen des darstellbaren Bereiches (Unterlauf ). Ist x 0 und y < 0, so ist entsprechend 12.2. DARSTELLUNG GANZER ZAHLEN IM RECHNER KAPITEL 12. ZAHLENDARSTELLUNGEN UND RECHNERARITHMETIK -3 -1 -4 100 0 3 1 011 2 010 +1 001 1 Der genaue mathematische Hintergrund hierfur wird in der Algebra geliefert. Der Bereich ;2n;1 x 2n;1 ; 1 bildet einen sogenannten Restklassenring modulo 2n;1 . Bei der Festkommadarstellung wird bei vorgegebener Stellenzahl n eine feste Stelle als erste Nachkommastelle vereinbart. Festkommazahlen im b-adischen System haben also die Dar- 12.3.1 Festkommazahlen Bei der Darstellung von reellen Zahlen unterscheidet man zwei Moglichkeiten, Festkommadarstellung (xed point representation) und Gleitkommadarstellung (oating point representation). 12.3 Darstellung reeller Zahlen Durchlauft man den Ring im Uhrzeigersinn, so ergibt sich die jeweils nachste Zahl durch Addition von 1 (und Weglassen des Ubertrags beim Ubergang von 111 zu 000), umgekehrt entspricht die Subtraktion von jeweils 1 dem Durchlauf entgegen dem Uhrzeigersinn. In dieser Interpretation sind die Uberlauehler ganz naturlich erklarbar. Abbildung 12.1: Ringdarstellung der Binarzahlen im Zweierkomplement. 101 -1 110 -2 111 000 Dies beweist die Ruckfuhrung der Subtraktion auf die Addition. Naturlich mu dabei (wie bereits bei der Addition) ein eventueller Uberlauf bzw. Unterlauf (Verlassen des dargestellten Bereiches) durch den Rechner oder durch den Programmierer abgefangen werden. Geschieht dies nicht, so werden die entstehenden Bitmuster als Zahlen aus dem dargestellten Bereich interpretiert, was zu vollig falschen Ergebnissen fuhrt. So ist z. B. bei n-stelliger Dualzahlarithmetik die grote darstellbare Zahl xmax = (011 : : : 1)2 , und xmax + 1 erzeugt das Bitmuster (100 : : : 0), was als 2;n interpretiert wird. Die darstellbaren Zahlen kann man sich beim b-Komplement ringformig angeordnet vorstellen, wobei die grote positive und kleinste negative Zahl benachbart sind. Fur b = 2 und n = 3 ergibt sich der in Abbildung 12.1 dargestellte Ring.1 238 x = (xk;1xk;2 x0:y1 y2 yn;k )b i=0 kX ;1 xi + j =1 nX ;k yj b;j : 239 2 Oft wird fur die Mantisse die Basis 2 und fur b eine Potenz von 2 gewahlt, z. B. 26 = 64. ;213 78 = ;2:1378 102 0 000031 = 0:31 10;4 = 3:1 10;5 (1101:011)2 = 1:101011 23 z = m be : Dabei heit m Mantisse und e Exponent , b ist die Basis fur den Exponenten. Die Basis der Mantisse kann dabei von der Basis b des Exponenten verschieden sein.2 Wir werden hier jedoch die gleiche Basis b voraussetzen. Beispiele sind: Gleitkommazahlen haben die Form 12.3.2 Gleitkommazahlen Fur manche Anwendungen reicht es vollig aus, mit Festkommazahlen zu rechnen, z. B. bei okonomischen Anwendungen (DM Betrage). In diesen Bereichen eingesetzte Programmiersprachen wie COBOL stellen daher reelle Zahlen als Festkommazahlen dar. Der Nachteil der Festkommazahlen besteht darin, da (wie auch bei ganzen Zahlen) der Bereich der darstellbaren Zahlen nach oben und unten stark beschrankt ist, und die Genauigkeit (der Abstand zwischen zwei benachbarten Zahlen) uberall gleich ist. Dies fuhrt zu groen Ungenauigkeiten bei wissenschaftlichen Rechnungen. Liegt etwa ein Rechenergebnis x zwischen der kleinsten (z1 ) und der zweitkleinsten (z2 ) positiven darstellbaren Zahl (etwa x = z2 ;2 z1 ; "), so mu x zur internen Darstellung auf eine der beiden Zahlen z1 z2 gerundet werden. Der dabei entstehende relative Fehler ist (bei Rundung auf z1 ) x ; z1 1=2z1 = 1 x 3=2z1 3 also ungefahr 33% bei kleinem ". Dies ist fur wissenschaftlich-numerische Rechnungen vollig inakzeptabel. Aus diesem Grunde verwendet man fur solche Rechnungen die Gleitkommadarstellung. (271:314)10 = 271 314 (101:011)2 = 1 22 + 0 21 + 1 20 + 0 2;1 + 1 2;2 + 1 2;3 = 4 + 1 + 41 + 18 = 5 375 Beispiele sind: x= mit k Vorkomma- und n ; k Nachkommastellen. Der Zahlenwert von x ergibt sich zu stellung 12.3. DARSTELLUNG REELLER ZAHLEN KAPITEL 12. ZAHLENDARSTELLUNGEN UND RECHNERARITHMETIK 5 8 3 4 7 8 1 5 4 3 2 7 4 - Abbildung 12.2: Nichtnegative Gleitkommazahlen aus F (2 3 ;1 1). 1 5 3 7 1 4 16 8 16 2 Man sieht, da der Abstand zwischen benachbarten Zahlen mit der Groe der Zahlen wachst. Ferner erkennt man eine relativ groe \Lucke" zwischen der 0 und der kleinsten positiven 0 0:000 20 = 0 = 0 0:100 2;1 = (1=2) 1=2 = 4=16 0:101 2;1 = (1=2 + 1=8) 1=2 = 5=16 0:110 2;1 = (1=2 + 1=4) 1=2 = 6=16 0:111 2;1 = (1=2 + 1=4 + 1=8) 1=2 = 7=16 0:100 20 = (1=2) 1 = 8=16 0:101 20 = (1=2 + 1=8) 1 = 10=16 0:110 20 = (1=2 + 1=4) 1 = 12=16 0:111 20 = (1=2 + 1=4 + 1=8) 1 = 14=16 = 16=16 0:100 21 = (1=2) 2 0:101 21 = (1=2 + 1=8) 2 = 20=16 0:110 21 = (1=2 + 1=4) 2 = 24=16 0:111 21 = (1=2 + 1=4 + 1=8) 2 = 28=16 Auf der Zahlengeraden ergibt sich die in Abbildung 12.2 dargestellte Verteilung. nach geordnet. Beispiel 12.2 Fur F = F (2 3 ;1 1) ergeben sich folgende nichtnegative Zahlen, der Groe Bei der rechnerinternen Darstellung mu festgelegt werden, wieviele Stellen fur die Mantisse, und wieviele fur den Exponenten zur Verfugung stehen. Dies resultiert in die Menge F der auf dem Rechner darstellbaren Maschinenzahlen . Wir kennzeichnen F durch F = F (b t emin emax ) wobei b die Basis (von Mantisse und Exponent), t die Anzahl der Stellen der Nachkommastellen der Mantisse, und emin e emax der Bereich des Exponenten ist. Es sind dann alle Zahlen x der Form x = 0:m1 : : : mt be mit emin e emax darstellbar. Die Beispiele zeigen, da die Gleitkommadarstellung nicht eindeutig ist, da ein \Austausch" zwischen Vorkommastellen und Exponent moglich ist. Um Eindeutigkeit fur die rechnerinterne Darstellung zu bekommen, verwendet man die normalisierte Gleitkommadarstellung . Diese verlangt eine Mantisse der Form m = 0:m1 m2 mt mit m1 6= 0 oder, aquivalent dazu 1 m < 1: b 240 241 e e C = x ;x xc = 0:x1 x2 : : : xtxt+1 xt+2 x: : : b ; 0:x1 : : : xt b ;t e e = 0:0 : : : 0xt+1xxt+2 : : : b = 0:xt+1 xt+2x: : : b b ;t e < b x b da 0:xt+1 xt+2 : : : < b0 = 1 Wie bereits erlautert, erzwingt der begrenzte Vorrat von Maschinenzahlen bei der Konvertierung oder Darstellung von Zwischenergebnissen die Rundung auf Maschinenzahlen und dadurch Rundungsfehler. Den maximalen relativen Rundungsfehler nennt man die (relative) Rechnergenauigkeit . Die Stellen der Mantisse nennt man die signikanten Stellen , eine t-stellige Mantisse bezeichnet man auch als t-stellige Arithmetik . Wir berechnen jetzt die Rechnergenauigkeit fur F = F (b t emin emax ). Dabei betrachten wir zunachst den relativen Rundungsfehler C , der durch Abschneiden (Chopping) nicht signikanter Stellen entsteht. Dies entspricht dem Runden zur betragskleineren Maschinenzahl. Sei x > pmin und xc 2 M die durch Abschneiden entstehende Zahl. Dann ist 12.3.3 Genauigkeit von Gleitkommadarstellungen Zahl pmin. Sie ergibt sich aus der Normalisierungsbedingung und ist deutlich groer als der Abstand zur zweitgroten positiven Zahl. Der Grund dafur ist die erwunschte Kontrolle des Rundungsfehlers , der dadurch entsteht, da Nicht-Maschinenzahlen durch Maschinenzahlen dargestellt werden mussen. Solche Fehler entstehen durch Konvertierung (Eingabe einer Nicht-Maschinenzahl, z. B. x = (1:2)10 0:101 21 = 5=4) oder durch Rundung von Zwischenergebnissen auf eine Maschinenzahl (z. B. 0:100 21 + 0:110 2;1 = 1 + 3=8 3=2 = 0:110 21 ). Der dabei entstehende Fehler wird im nachsten Abschnitt analysiert. Da bei der Basis b = 2 das erste Bit der Mantisse wegen der Normalisierungsbedingung eindeutig festlegt (m1 = 1), wird es oft nicht explizit dargestellt (hidden bit). In diesem Fall werden die t Mantissenbits zur Darstellung von m2 : : : mt+1 genutzt, d. h. man erreicht eine hohere Genauigkeit. Allerdings mu dann die Null besonders dargestellt werden, z. B. als int mit impliziten Konvertierungsregeln. Zur Darstellung der Exponenten verwendet man oft die Excess - oder Bias-Darstellung . Die Exponenten werden dabei durch Addition des Excesses emin in den Bereich 0 : : : emin + emax transformiert. Dies erleichtert den Vergleich von Exponenten. Als konkretes Beispiel betrachten wir die SUN, auf der dieser Text erstellt wurde. Die Datei /usr/include/values.h enthalt folgende Informationen (und Setzungen) fur die Darstellung von double-Zahlen: Fur die gesamte Zahl stehen 64 Bit zur Verfugung, davon 53 fur die Mantisse und (in hidden bit Darstellung) 11 fur den Exponenten. Die Basis fur Mantisse und Exponent ist 2, als maximale darstellbare Zahl ergibt sich MAXDOUBLE = 1:797693134862315708 10308 , und als kleinste darstellbare positive Zahl MINDOUBLE = 4:94065645841246544 10;324 . 12.3. DARSTELLUNG REELLER ZAHLEN b;t be da x > 1 wegen der Normalisierung 1 b 1 be b = b;t b = bt1;1 : KAPITEL 12. ZAHLENDARSTELLUNGEN UND RECHNERARITHMETIK In Kapitel 6.2.4 haben wir bereits ein Beispiel fur die Auswirkung von Rundungsfehlern bei der Losung linearer Gleichungssysteme mit dem Gau-Algorithmus kennengelernt. Hier folgt ein weiteres Beispiel fur die Losung quadratischer Gleichungen . Die quadratische Gleichung ax2 + bx + c = 0 hat die Losungen p 2 x12 = ;b 2ba ; 4ac : Diese lassen sich nach dieser Formel berechnen durch d := sqrt(sqr(b) ; 4ac) x1 := (d ; b)=(2a) x2 := ;(b + d)=(2a) p wobei sqrt(x) = x und sqr(b) = b2 als verfugbare Funktionen vorausgesetzt werden. Ein numerisches Beispiel mit den Werten a = 1 b = ;200 und c = 1, ausgefuhrt mit einer 4-stelligen dezimalen Arithmetik (d. h. 4 Stellen fur die Mantisse) ergibt folgende Resultate. d = sqrt(sqr(;0:2000 103 ) ; 4) 1 1 = sqrt( |0:4000 10 {z; 0:4000 10} ) 0:4000 105 in 4-stelliger Arithmetik = 02000 103 12.3.4 Der Einu von Algorithmen auf die numerische Genauigkeit Der relative Rundungsfehler R ist oenbar hochstens halb so gro wie der relative Abschneidefehler. Also ergibt sich die relative Rechnergenauigkeit R als R = 21 C = 12 bt1;1 : Diese Zahl ist die wichtigste Groe bei Genauigkeitsbetrachtungen fur ein Gleitkommasystem und Grundlage aller Fehlerabschatzungen in der Numerischen Mathematik . Die Genauigkeit bezieht sich auf die Zahl t von signikanten Stellen zur Basis b. Die entsprechende Zahl s von signikanten Stellen im Dezimalsystem erhalt man durch Auosung der Gleichung R = 21 bt1;1 = 12 101s;1 nach s. Dies ergibt s = 1 + (t ; 1) log10 b : Umgekehrt berechnet man t aus einer gewunschten Anzahl s von dezimalen Stellen gema t = 1 + (s ; 1) log10 b : 242 x1 = (0:2000 103 ; (;0:2000 103 ))=(0:2000 101 ) = (0:4000 103 )=(0:2000 101 ) = 0:2000 103 = 200 x2 = ;(0:2000 103 + (;0:2000 103 ))=(0:2000 101 ) = 0=(0:2000 101 ) = 0: 243 Kapitel 12.1 und 12.2 folgen OV94]. Das Beispiel der Losung quadratischer Gleichungen in Kapitel 12.3.4 stammt aus Wir78]. Als Einstieg in die Methoden der numerischen Mathematik sei auf Ric83, Van85] verwiesen. 12.4 Literaturhinweise Dies ist ein Beispiel fur den haugen Fall, da die ublichen mathematischen \Losungsmethoden" nicht unbesehen ubernommen werden konnen, wenn mit wirklichen Rechnern gearbeitet werden soll. Die Numerik ist der Zweig der Mathematik, der sich mit der Entwicklung von Losungsmethoden befat, die die Tucken der Gleitkommaarithmetik berucksichtigen. Im Beispiel fuhrt dies zu den Werten d = 0:2000 103 (wie oben), x1 = (d ; b)=(2a) = 200 (wie oben), x2 = 1=200 = 0:005 : d := sqrt(sqr(b) ; 4ac) if b 0 then x1 := ;(b + d)=(2a) else x1 := (d ; b)=(2a) x2 := c=(x1 a) Die 4-stellige Rechnerarithmetik hat die relative Rechnergenauigkeit R = 21 1041;1 = 0 0005. Die Abweichung des errechneten Ergebnisses (0 anstelle des exakten Wertes 0,005) mu daher als vollig inakzeptabel betrachtet werden. Das Ergebnis x2 = 0 ist schlicht falsch. Eine Losungsmethode, die die Gefahren der Gleitkomma-Arithmetik berucksichtigt, beruht auf der Beziehung x1 x2 = ac (Vieta) : Es wird jetzt lediglich eine Losung mit der ublichen Formel Berechnet, und zwar die mit dem gro ten Absolutwert . Die zweite Losung errechnet man dann aus der Formel von Vieta. Dies ergibt das Programmstuck: Die exakten Werte mit 4-stelliger Arithmetik lauten jedoch x1 = 200 und x2 = 0:005 : Hieraus folgt 12.4. LITERATURHINWEISE 244 KAPITEL 12. ZAHLENDARSTELLUNGEN UND RECHNERARITHMETIK Martin Aigner. Diskrete Mathematik. Vieweg, Wiesbaden/Braunschweig, 1993. J. Glenn Brookshear. Computer Science: An Overview. Benjamin/Cummings Publ., Menlo Park, CA, 4th edition, 1994. Bro94] Harvey M. Deitel and Paul J. Deitel. C++ How to Program. Prentice Hall, Englewood Clis, NJ, 1994. L. Goldschlager and A. Lister. Informatik - Eine moderne Einfuhrung. Carl Hanser Verlag, Munchen, 1984. DD94] GL84] E. Horowitz. Fundaments of Programming Languages. Springer-Verlag, Berlin, 1984. Second Edition. Mark R. Headington and David D. Riley. Data Abstraction and Structures Using C++. D. C. Heath and Company, Lexington, MA, 1994. Hor84] HR94] 245 Ralph P. Grimaldi. Discrete and Combinatorial Mathematics: An Applied Introduction. Addison-Wesley, Reading, NY, 3rd edition, 1994. Gri94] GMW91] Philip E. Gill, Walter Murray, and Margaret H. Wright. Numerical Linear Algebra and Optimization, volume 1. Addison-Wesley, Reading, NY, 1991. D. Coppersmith and S. Winograd. Matrix multiplication via arithmetic progression. In Proc. 19th Ann. Symp. on Comp., pages 1{6, 1987. CW87] CLR90] Thomas H. Corman, Charles E. Leiserson, and Ronald R. Rivest. Introduction to Algorithms. The MIT Press, Cambridge, MA, 1990. Alfred V. Aho and Jerey D. Ullman. Foundations of Computer Science. W. H. Freeman and Company, New York, 1992. AU92] ASU86] Alfred V. Aho, Ravi Sethi, and Jerey D. Ullman. Compilers, Principles, Techniques, and Tools. Addison-Wesley, Reading, NY, 1986. Aig93] AHU83] Alfred V. Aho, John E. Hopcroft, and Jerey D. Ullman. Data Structures and Algorithmus. Addison-Wesley, Reading, NY, 1983. AHU74] Alfred V. Aho, J. E. Hopcroft, and Jerey D. Ullman. The Design and Analysis of Computer Algorithms. Addison-Wesley, Reading, NY, 1974. Literaturverzeichnis LITERATURVERZEICHNIS John E. Hopcroft and Jerey D. Ullman. Introduction to Automata Theory, Languages, and Computation. Addison-Wesley, Reading, NY, 1979. Jun94] Dieter Jungnickel. Graphen, Netzwerke und Algorithmen, volume 3. Auage. BI, Mannheim, 1994. Knu73] Donald. E. Knuth. The Art of Computer Programming, volume I-III. AddisonWesley, Reading, NY, 1973. Lip92] Stanley Lippman. The C++ Primer. Addison-Wesley, Reading, NY, 1992. LV92] G. T. Leavens and M. Vermeulen. The 3 + x problem. Computers Math. Appl., 24:79{99, 1992. Meh88] K. Mehlhorn. Datenstrukturen und eziente Algorithmen, Band I: Sortieren und Suchen. Teubner Verlag, Stuttgart, 1988. Mei91] C. Meinel. Eziente Algorithmen: Entwurf und Analyse. Fachbuchverlag Leipzig, 1991. OSW83] Thomas Ottmann, Michael Schrapp, and Peter Widmayer. PASCAL in 100 Beispielen. Teubner Verlag, Stuttgart, 1983. OV94] W. Oberschelp and G. Vossen. Rechneraufbau und Rechnerstrukturen. Oldenburg Verlag, Munchen, 6 edition, 1994. OW82] T. Ottmann and P. Widmayer. Programmierung mit PASCAL. Teubner Verlag, Stuttgart, 1982. OW90] T. Ottmann and P. Widmayer. Algorithmen und Datenstrukturen. BI Wissenschaftsverlag, Mannheim, 1990. Poh89] Ira Pohl. C++ for C Programmers. Benjamin/Cummings Publ., Menlo Park, CA, 1989. Poh93] Ira Pohl. Object-Oriented Programming. Benjamin/Cummings Publ., Menlo Park, CA, 1993. Ric83] J. R. Rice. Numerical Methods, Software and Analysis. Mc Graw-Hill, London, 1983. Sed92] Robert Sedgewick. Algorithms in C++. Addison-Wesley, Reading, NY, 1992. Str94] Bjarne Stroustrup. Die C++ Programmier Sprache, 2. uberarbeitete Auage. Addison-Wesley, Reading, NY, 1994. Tar83] Robert Endre Tarjan. Data Structures and Network Algorithms. CBMS-NSF Regional Conference Series in Applied Mathematics. SIAM, Philadelphia, PA, 1983. Tea93] Steve Teale. The IOStreams Handbook. Addison-Wesley, Reading, NY, 1993. Van85] R. Vandergraft. Introduction to Numerical Computation. Academic Press, New York, 1985. HU79] 246 Wir78] Wir86] Wii87] vL90b] vL90a] 247 Jan van Leeuwen, editor. Handbook of Theoretical Computer Science, A: Algorithms and Complexity Theory. North-Holland, Amsterdam, 1990. Jan van Leeuwen, editor. Handbook of Theoretical Computer Science, B: Formal Models and Semantics. North-Holland, Amsterdam, 1990. Stephen A. Wiitala. Discrete Mathematics: A Unied Approach. Mc Graw-Hill, New York, 1987. N. Wirth. Systematisches Programmieren. Teubner Verlag, Stuttgart, 1978. N. Wirth. Algorithmen und Datenstrukturen. Teubner Verlag, Stuttgart, 1986. LITERATURVERZEICHNIS