Skript - TU Berlin - Institut für Mathematik

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