Algorithmen und Datenstrukturen

Werbung
Algorithmen und Datenstrukturen
Werner Struckmann
Wintersemester 2005/06
2. Imperative Algorithmen
2.1 Variable, Anweisungen und Zustände
2.2 Felder
2.3 Komplexität von Algorithmen
2.4 Korrektheit von Algorithmen
2.5 Konzepte imperativer Sprachen
2.6 Rekursionen
Paradigmen zur Algorithmenbeschreibung
In einem imperativen Algorithmus gibt es Variable, die
verschiedene Werte annehmen können. Die Menge aller Variablen
und ihrer Werte sowie der Programmzähler beschreiben den
Zustand zu einem bestimmten Zeitpunkt. Ein Algorithmus bewirkt
eine Zustandstransformation.
Ein funktionaler Algorithmus formuliert die Berechnung durch
Funktionen. Die Funktionen können rekursiv sein; auch gibt es
Funktionen höherer Ordnung.
2.1 Variable, Anweisungen und Zustände
2-1
Paradigmen zur Algorithmenbeschreibung
In einem objektorientierten Algorithmus werden Datenstrukturen
und Methoden zu einer Klasse zusammengefasst. Von jeder
Klasse können Objekte gemäß der Datenstruktur erstellt und über
die Methoden manipuliert werden.
Ein logischer (deduktiver) Algorithmus führt Berechnungen durch,
indem er aus Fakten und Regeln durch Ableitungen in einem
logischem Kalkül weitere Fakten beweist.
2.1 Variable, Anweisungen und Zustände
2-2
Beispiel: Algorithmus von Euklid
Der folgende, in einer imperativen Programmiersprache formulierte
Algorithmus von Euklid berechnet den größten gemeinsamen
Teiler der Zahlen x , y ∈ N mit x ≥ 0 und y > 0:
a := x;
b := y;
while b
do r :=
a :=
b :=
od
# 0
a mod b;
b;
r
Anschließend gilt a = ggT(x , y ).
2.1 Variable, Anweisungen und Zustände
2-3
Beispiel: Algorithmus von Euklid
Variable
r
a
b
z2
–
36
52
z5
36
52
36
z8
16
36
16
z11
4
16
4
z14
0
4
0
ggT(36, 52) = 4
Durchlaufene Zustände:
z0 , z1 , z2 , . . . , z14
Zustandstransformation:
z0 7−→ z14
2.1 Variable, Anweisungen und Zustände
2-4
Imperative Konzepte
◮
Variable: Abstraktion eines Speicherbereichs, ein Wert eines
gegebenen Datentyps kann gespeichert und beliebig oft
gelesen werden, solange nicht ein neuer Wert gespeichert
und der alte damit überschrieben wird.
◮
Zustand: Abstraktion des Speicherinhalts, Gesamtheit der
momentanen Werte aller Variablen, ändert sich durch die
Ausführung von Anweisungen.
◮
Anweisung: Vorschrift zur Ausführung einer Operation,
ändert im Allgemeinen den Zustand.
2.1 Variable, Anweisungen und Zustände
2-5
Datentypen
◮
Datentyp: Die Zusammenfassung von Wertebereichen und
Operationen zu einer Einheit.
◮
Abstrakter Datentyp: Schwerpunkt liegt auf den
Eigenschaften, die die Operationen und Wertebereiche
besitzen.
◮
Konkreter Datentyp: Beschreibt die Implementierung eines
Datentyps.
2.1 Variable, Anweisungen und Zustände
2-6
Grundlegende Datentypen
Oft werden mathematische Konzepte als Grundlage für einen
Datentyp verwendet. Ein Datentyp besteht aus einem
Wertebereich und einer Menge von Operationen.
◮
bool: die booleschen Werte „wahr“ und „falsch“
Operationen: logische Verknüpfungen, wie zum Beispiel „und“
und „oder“.
◮
int: ganze Zahlen, {minint, · · · , −1, 0, 1, · · · , maxint} ⊆ Z
minint und maxint besitzen etwa den gleichen Betrag.
Operationen: arithmetische und Vergleichsoperationen.
Die Rechengesetze gelten in der Regel nicht, wenn man über
die Darstellungsgrenzen gerät.
2.1 Variable, Anweisungen und Zustände
2-7
Grundlegende Datentypen
◮
real, float: Näherungswerte für reelle Zahlen, dargestellt
durch Gleitpunktzahlen:
r = m · be
m∈Z
Mantisse,
b ∈N
Basis,
e∈Z
Exponent
Die Darstellung erschließt den Zahlenbereich mit konstanter
relativer Genauigkeit.
Operationen: arithmetische und Vergleichsoperationen.
Die Rechengesetze gelten im Allgemeinen nur
näherungsweise.
Gleitpunktzahlen sollten nicht auf Gleichheit überprüft
werden, stattdessen sollte |x − y | < ε für einen kleinen Wert
von ε getestet werden.
2.1 Variable, Anweisungen und Zustände
2-8
Grundlegende Datentypen
◮
char: Zeichen aus einem Alphabet
Mit Vergleichsoperationen und den Funktionen
ord : char → int und chr : int → char, die eine Zuordnung
zwischen den Zeichen aus dem Alphabet und ganzen Zahlen
herstellen.
2.1 Variable, Anweisungen und Zustände
2-9
Variable
◮
Variable x : t: Abstraktion eines Speicherplatzes und
Zuordnung eines Datentyps
X
Menge der Variablen
x:t
x∈X
t = τ(x )
Typ von x
v = σ(x ) Wert von x
v ∈ W (t ) Wertemenge von t
◮
Deklaration: Ein Variablenname wird einem Speicherbereich
und einem Typ zugeordnet.
var i, j: int;
var r: real;
var c: char;
2.1 Variable, Anweisungen und Zustände
2-10
Zustand
Ein Zustand bezeichnet die Belegung aller Variablen zu einem
Zeitpunkt.
◮
Modellierung:
σ : x1 7→ v1 , . . . , xn 7→ vn
xi ∈ X , vi ∈ W (ti ) ∀i ∈ {1, . . . , n}
◮
◮
x1 · · · xn
Tabellarisch:
v1 · · · vn
Mathematisch: Abbildung σ : X → W = {v1 , . . . , vn }, wobei
σ(xi ) ∈ W (ti ) ∀i ∈ {1, . . . , n}
σ(x ) ist die Belegung der Variablen x im Zustand σ.
2.1 Variable, Anweisungen und Zustände
2-11
Zuweisung
v ∈ W (τ(x ))
◮
Syntax: x ← v
◮
Zuweisung, Grundbaustein: Nach Ausführung der
Zuweisung gilt σ(x ) = v .
Ist σ : X → W ein Zustand und wählt man eine Variable x ∈ X
sowie einen Wert v vom passenden Typ, so ist der transformierte
Zustand σ<x ←v > wie folgt definiert:
σ<x ←v > (y ) =
2.1 Variable, Anweisungen und Zustände
(
v
σ(y )
falls x = y
sonst
2-12
Semantik
Die Semantik beschreibt die Bedeutung eines Algorithmus.
Sie ist eine (partielle) Funktion, die jeder Anweisung eine
Zustandsänderung zuordnet:
[ ] : A → ((X → W ) → (X → W )) mit [a ](σ) = σ′
Semantik einer Zuweisung: [x ← v ](σ) = σ<x ←v >
2.1 Variable, Anweisungen und Zustände
2-13
Ausdrücke/Terme
Beispiele für Ausdrücke:
◮
Konstante: 3, 2.7182, ′ A ′
◮
Variable: x1 , x2 , y
◮
Operatoren und Funktionsaufrufe: x1 + 3, f (x1 )
◮
Zusammengesetzte Ausdrücke: f (x1 + 3) − 2.7182 + y
Falls ein Ausdruck t die Variablen x1 , . . . , xn enthält, schreiben wir
t (x1 , . . . , xn ).
Beispiel eines booleschen Ausdrucks: x ≤ 5 ∧ y < 4
2.1 Variable, Anweisungen und Zustände
2-14
Wert eines Ausdrucks
Die Auswertung von Ausdrücken mit Variablen ist
zustandsabhängig. An die Stelle der Variablen wird ihr aktueller
Wert gesetzt.
Beispiel: Für den Ausdruck 2 · x + 1 ist sein Wert im Zustand σ
durch 2σ(x ) + 1 gegeben.
Der so bestimmte Wert des Ausdrucks t (x1 , . . . , xn ) im Zustand σ
wird mit σ(t (x1 , . . . , xn )) bezeichnet.
Beispiel: σ(2 · x + 1) = 2σ(x ) + 1
2.1 Variable, Anweisungen und Zustände
2-15
Zuweisungen
Diese Festlegung erlaubt Wertzuweisungen mit Ausdrücken auf
der rechten Seite
y ← t (x1 , . . . , xn )
Der transformierte Zustand hierfür ist wie folgt definiert:
[y ← t (x1 , . . . , xn )](σ) = σ<y ←σ(t (x1 ,...,xn ))>
2.1 Variable, Anweisungen und Zustände
2-16
Zuweisungen
Beispiel: Semantik der Zuweisung
x ←2·x +1
Transformation: [x ← 2 · x + 1](σ) = σ<x ←2·σ(x )+1>
Hier handelt es sich nicht um eine rekursive Gleichung für x , da
auf der rechten Seite der „alte“ Wert von x benutzt wird.
Wertzuweisungen sind die einzigen elementaren Anweisungen
imperativer Algorithmen. Aus ihnen werden zusammengesetzte
Anweisungen gebildet, aus denen imperative Algorithmen
bestehen.
2.1 Variable, Anweisungen und Zustände
2-17
Zusammengesetzte Anweisungen
Elementare Anweisungen können auf unterschiedliche Arten zu
komplexen Anweisungen zusammengesetzt werden:
1. sequenzielle Ausführung,
2. bedingte Ausführung,
3. wiederholte Ausführung,
4. Ausführung als Unterprogramm,
5. rekursive Ausführung eines Unterprogramms.
Diese Möglichkeiten werden als Kontrollstrukturen bezeichnet. Wir
betrachten jetzt die ersten drei dieser zusammengesetzten
Anweisungen. Auf die anderen kommen wir am Schluss des
Kapitels zu sprechen.
2.1 Variable, Anweisungen und Zustände
2-18
Sequenzielle Ausführung von Anweisungen
◮
Definition: Sind a1 und a2 Anweisungen, so auch a1 ; a2
◮
Informelle Bedeutung: „Führe erst a1 , dann a2 aus.“
◮
Semantik: [a1 ; a2 ](σ) = [a2 ]([a1 ](σ))
◮
Darstellung im Flussdiagramm:
a1
a2
2.1 Variable, Anweisungen und Zustände
2-19
Bedingte Ausführung von Anweisungen
◮
Definition: Sind a1 und a2 Anweisungen und P ein
boolescher Ausdruck, so ist auch
if P then a1 else a2 fi
eine Anweisung.
◮
Voraussetzung: σ(P ) kann ausgewertet werden (ansonsten
ist die Anweisung undefiniert)
◮
Informelle Bedeutung: „Falls P gilt, führe a1 aus, sonst a2 .“
2.1 Variable, Anweisungen und Zustände
2-20
Bedingte Ausführung von Anweisungen
◮
Semantik:
◮



[a1 ](σ) falls σ(P ) = true
[ if P then a1 else a2 fi ](σ) = 

[a2 ](σ) falls σ(P ) = false
Darstellung im Flussdiagramm:
true
false
P
a1
2.1 Variable, Anweisungen und Zustände
a2
2-21
Wiederholte Ausführung von Anweisungen
◮
Definition: Ist a eine Anweisung und P ein boolescher
Ausdruck, so ist auch
while P do a od
eine Anweisung.
◮
Voraussetzung: σ(P ) kann ausgewertet werden (ansonsten
ist die Anweisung undefiniert)
◮
Informelle Bedeutung: „Solange P gilt, führe a aus.“
2.1 Variable, Anweisungen und Zustände
2-22
Wiederholte Ausführung von Anweisungen
◮
Semantik:
[ while P do a od ](σ) =



falls σ(P ) = false
σ


[ while P do a od ]([a ](σ)) sonst
◮
Diese Definition ist rekursiv.
While-Schleifen müssen nicht terminieren.
Darstellung im Flussdiagramm:
false
P
true
a
2.1 Variable, Anweisungen und Zustände
2-23
Flussdiagramme
Normierte Methode (DIN 66001) zur Darstellung von Programmen
◮
Beginn: Start
◮
Ende: Stop
◮
Elementare Anweisung:
a
P
◮
Entscheidung durch booleschen Ausdruck:
◮
Eingabe nach n:
Eingabe n
◮
Ausgabe von p:
Ausgabe p
2.1 Variable, Anweisungen und Zustände
2-24
Konkrete Umsetzung
◮
Sprachen mit nur diesen Anweisungen sind bereits
berechnungsuniversell.
◮
In existierenden Programmiersprachen gibt es fast immer
diese Anweisungen, oft jedoch mehr.
Beispiel: case- oder repeat-Anweisung.
◮
Schlüsselwörter und Syntax der Kontrollstrukturen variieren
von Sprache zu Sprache.
Beispiel: end if statt fi.
◮
Hierarchische Struktur der Anweisungen: Anweisungen
können Bestandteil anderer Anweisungen sein.
2.1 Variable, Anweisungen und Zustände
2-25
Imperative Algorithmen
◮
Es werden die Datentypen int, real und bool verwendet.
◮
Aufbau imperativer Algorithmen (Syntax):
<Programmname>
var x ,y ,. . . : int ; p ,q: bool ;
input : x1 , . . . , xn ;
a;
output : y1 , . . . , ym
◮
Variablendeklarationen
Eingabevariablen
Anweisung(en)
Ausgabevariablen
Die Semantik wird durch eine Zustandsüberführungsfunktion
beschrieben.
2.1 Variable, Anweisungen und Zustände
2-26
Imperative Algorithmen
Die Semantik eines imperativen Algorithmus ist eine partielle
Funktion
[PROG ] : W1 × · · · × Wn → V1 × · · · × Vm
[PROG ](w1 , . . . , wn ) = (σ(y1 ), . . . , σ(ym ))
σ = [a ](σ0 )
σ0 (xi ) = wi ∈ Wi für i = 1, . . . , n
wobei
PROG: <Programmname>
Wi : Wertebereich des Typs von xi , für i = 1, . . . , n
Vj : Wertebereich des Typs von yj , für j = 1, . . . , m
2.1 Variable, Anweisungen und Zustände
2-27
Imperative Algorithmen
◮
Der Algorithmus legt eine Zustandstransformation fest. Die
Eingabe befindet sich im Zustand σ0 .
◮
Die Ausführung imperativer Algorithmen besteht aus einer
Abfolge von Zuweisungen. Diese Folge wird mittels
„Sequenz“, „bedingter Ausführung“ und „wiederholter
Ausführung“ aus den Zuweisungen gebildet.
◮
Jede Zuweisung definiert eine Transformation des Zustands.
Die Semantik des Algorithmus ist durch die Kombination all
dieser Zustandstransformationen festgelegt.
◮
Falls die Auswertung von a nicht terminiert, ist die Funktion
[PROG ] nicht definiert.
2.1 Variable, Anweisungen und Zustände
2-28
Algorithmus von Euklid
Der Algorithmus von Euklid lautet in dieser Notation:
EUKLID
var a, b, r, x, y: int;
input x, y;
a ← x;
b ← y;
while b # 0 do
r ← a mod b;
a ← b;
b ← r;
od;
output a;
2.1 Variable, Anweisungen und Zustände
2-29
Fakultätsfunktion
k! = 1 · 2 · 3 · · · k
Start
FAK
var x, y: int;
input x;
y ← 1;
while x > 1 do
y ← y * x;
x ← x - 1;
od;
output y;
Eingabe x
y←1
x>1
true
y ←y·x
Es gilt:
[FAK ](w ) =
false
Ausgabe y
Stop
(
w ! für w > 0
1
2.1 Variable, Anweisungen und Zustände
x←x−1
sonst
2-30
Auswertung der Fakultätsfunktion
◮
Signatur der Semantikfunktion: [FAK ] : int → int
◮
Das Ergebnis der Berechnung ist die Belegung der Variablen
y im Endzustand: σ(y ).
◮
Der Endzustand σ ist laut Definition σ = [a ](σ0 ), wobei a die
Folge aller Anweisungen des Algorithmus ist.
◮
σ0 ist der Startzustand. Die Eingabe befindet sich in σ0 (x ).
◮
Da nur die zwei Variablen x und y auftreten, können wir einen
Zustand σ als Paar (σ(x ), σ(y )) schreiben.
◮
⊥ bedeutet, dass der Wert der Variablen keine Rolle spielt
bzw. undefiniert ist.
2.1 Variable, Anweisungen und Zustände
2-31
Auswertung der Fakultätsfunktion
Gesucht: [FAK ](3)
σ = [a ](σ0 ) = [a ](3, ⊥)
= [y ← 1; while x > 1 do y ← y ∗ x ; x ← x − 1 od ](3, ⊥)
= [ while x > 1 do y ← y ∗ x ; x ← x − 1 od ]([y ← 1](3, ⊥))
=: [ while B do β od ]([y ← 1](3, ⊥))
= [ while B do β od ](3, 1)
(
σ
falls σ(B ) = false
=
[ while B do β od ]([β](σ)) sonst
(
(3, 1)
falls σ(x > 1) = false
=
[ while B do β od ]([y ← y ∗ x ; x ← x − 1](σ)) sonst
2.1 Variable, Anweisungen und Zustände
2-32
Auswertung der Fakultätsfunktion
· · · = [ while B do β od ]([y ← y ∗ x ; x ← x − 1](3, 1))
= [ while B do β od ]([x ← x − 1]([y ← y ∗ x ](3, 1)))
= [ while B do β od ]([x ← x − 1](3, 3))
= [ while B do β od ](2, 3)
= [ while B do β od ]([y ← y ∗ x ; x ← x − 1](2, 3))
= [ while B do β od ](1, 6)
(
(1, 6)
falls σ(x > 1) = false
=
[ while B do β od ]([y ← y ∗ x ; x ← x − 1](1, 6)) sonst
= (1, 6)
[FAK](3) = 6
2.1 Variable, Anweisungen und Zustände
2-33
Fibonacci-Zahlen (iterativ)
f0 = 0,
f1 = 1,
fn = fn−1 + fn−2 , n > 1
FIB:
var x,a,b,c: int;
input x;
a ← 0;
b ← 1;
while x > 0 do
c ← a + b;
a ← b;
b ← c;
x ← x - 1;
od;
output a;
[FIB ](x ) =
2.1 Variable, Anweisungen und Zustände
(
die x -te Fibonacci-Zahl, falls x > 0,
0,
sonst.
2-34
Fibonacci-Zahlen (rekursiv)
Einzelheiten zu rekursiven Programmen werden später behandelt!
f0 = 0,
f1 = 1,
fn = fn−1 + fn−2 , n > 1
FIB:
var n, x: int;
input n;
if n = 0 then x ← 0 fi;
if n = 1 then x ← 1 fi;
if n > 1 then x ← FIB(n - 1) + FIB(n - 2) fi;
output x;
Ein rekursiv ausgedrückter Algorithmus ist häufig eleganter als
sein iteratives Äquivalent. Nachteil ist evtl. eine längere Laufzeit.
2.1 Variable, Anweisungen und Zustände
2-35
Felder
◮
Definition: Ein Feld ist eine indizierte Menge von Variablen
des gleichen Typs. Felder sind generische Datentypen.
◮
Deklaration:
var x[I]: T;
var coords[1..3]: real;
var str[0..4095]: char;
◮
Indexmenge: I endlich, häufig I ⊆ N, aber andere
Indexmengen sind möglich.
◮
Zugriff: x [i ] sowohl lesend als auch schreibend, wobei i ∈ I .
Längenänderungen von Feldern sind nicht in allen
Programmiersprachen möglich.
Felder werden auch als Arrays bezeichnet.
2.2 Felder
2-36
Verbreitete Spezialfälle von Feldern
◮
Vektor:
var v[1..3]: real;
◮
Matrix:
var v[1..4, 1..4]: real;
◮
Strings:
var str[0..102]: char;
2.2 Felder
2-37
For-Schleife
Die For-Schleife ist eine weitere Kontrollstruktur zur Wiederholung
von Anweisungen.
Es seien j , k ∈ N Konstante, var i : int eine Variable und a eine
Anweisung.
for i ← j to k do a; od entspricht
i ← j; while i ≤ k do a; i ← i + 1; od
i wird Laufvariable der Schleife genannt.
Viele Programmiersprachen enthalten Varianten dieser Schleife.
2.2 Felder
2-38
Maximale Summe eines Teilfelds
Die folgenden Programme berechnen die maximale Summe
aufeinanderfolgender Zahlen in einem Feld var x [1..n] : int .
 j





X

m = max 
x
[
k
]
|
1
≤
i
≤
j
≤
n





k =i
x sei das Feld:
2
3
4
5
6
7
8
9
10
1
31 −41 59 26 −53 58 97 −93 −23 84
Die maximale Summe aufeinanderfolgender Zahlen ist
x [3] + x [4] + x [5] + x [6] + x [7] = 187. Keine andere Summe
besitzt einen höheren Wert. Beispielsweise ist
x [1] + x [2] + x [3] + x [4] = 75.
2.2 Felder
2-39
Maximale Summe eines Teilfelds
Idee: Berechne alle möglichen Summen.
MaxSum1
var x[1..n]: int;
var i,j,k,s,m: int;
input x;
m ← 0; // Wert der leeren Summe
for i ← 1 to n do
for j ← i to n do
s ← 0;
for k ← i to j do
s ← s + x[k];
od;
m ← max(m, s);
od;
od;
output m;
Dieser Algorithmus enthält drei ineinandergeschachtelte Schleifen.
2.2 Felder
2-40
Maximale Summe eines Teilfelds
Idee: Einsparen der inneren Schleife durch Wiederverwenden
bereits bekannter Summen.
MaxSum2
var x[1..n]: int;
var i,j,s,m: int;
input x;
m ← 0; // Wert der leeren Summe
for i ← 1 to n do
s ← x[i];
m ← max(m, s);
for j = i+1 to n do
s ← s + x[j];
m ← max(m, s);
od;
od;
output m;
Dieser Algorithmus besitzt zwei ineinandergeschachtelte Schleifen.
2.2 Felder
2-41
Maximale Summe eines Teilfelds
Idee: Genau überlegen, was eine hohe Summe ausmacht. Diese
Lösung stammt von M. Shamos (1977).
MaxSum3
var x[1..n]: int;
var i,s,m: int;
input x;
m ← 0;
s ← 0;
for i ← 1 to n do
s ← max(0, s + x[i]);
m ← max(m, s);
od;
output m;
Dieser Algorithmus hat nur noch eine Schleife.
2.2 Felder
2-42
Einführung
◮
In der Regel lässt sich ein Problem durch verschiedene
Algorithmen lösen (zum Beispiel „maximale Summe
aufeinanderfolgender Elemente“).
◮
Welcher Algorithmus soll gewählt werden?
◮
Die Algorithmen müssen hinsichtlich ihres Verhaltens
verglichen werden.
◮
Man benötigt ein Maß für den Aufwand eines Algorithmus.
2.3 Komplexität von Algorithmen
2-43
Komplexität von Algorithmen und Problemen
◮
Unter der Komplexität eines Algorithmus versteht man den
Aufwand, den der Algorithmus zur Lösung des Problems
benötigt. Typischerweise ist hier die
◮
◮
Laufzeit des Algorithmus oder der
Speicherbedarf des Algorithmus
gemeint. Man unterscheidet die
◮
◮
◮
◮
Komplexität im günstigsten Fall (best-case), die
Komplexität im mittleren Fall (average-case) und die
Komplexität im ungünstigsten Fall (worst-case).
Unter der Komplexität eines Problems versteht man die
Komplexität des besten Algorithmus zur Lösung des Problems
im ungünstigsten Fall.
2.3 Komplexität von Algorithmen
2-44
Komplexität eines Algorithmus
◮
Umfang n eines Problems:
„Anzahl der Eingabewerte“ oder „Größe der Eingabewerte“
oder . . .
◮
Aufwand T (n) eines Algorithmus:
„Anzahl der Schritte“ oder „Anzahl bestimmter Operationen“
oder „Anzahl der benötigten Speicherplätze“ oder . . . , die der
Algorithmus braucht, um ein Problem vom Umfang n zu lösen.
Um sinnvolle Aussagen über die Komplexität eines Algorithmus zu
treffen, müssen n und T (n) mit Bedacht gewählt werden.
Beispiel: Im Algorithmus „Sortieren durch Einfügen“ war n die
Anzahl der zu sortierenden Zahlen und T (n) die Anzahl der
benötigten Vergleiche.
2.3 Komplexität von Algorithmen
2-45
Wachstum von Funktionen
◮
In der Regel stellt die Wachstumsrate der Laufzeit ein
einfaches und geeignetes Kriterium zur Messung der Effizienz
eines Algorithmus dar.
◮
Die Wachstumsrate erlaubt es uns, die relative
Leistungsfähigkeit alternativer Algorithmen zu vergleichen.
◮
Die Wachstumsrate von Algorithmen wird meistens mithilfe
der asymptotischen Notation angegeben.
◮
Die asymptotische Notation lässt konstante Faktoren
unberücksichtigt.
2.3 Komplexität von Algorithmen
2-46
Asymptotische Notation
Es sei eine Funktion g : N −→ R gegeben.
Θ(g ) = {f : N −→ R | ∃c1 > 0, c2 > 0, n0 > 0 ∀n ≥ n0 .
0 ≤ c1 g (n) ≤ f (n) ≤ c2 g (n)}
O (g ) = {f : N −→ R | ∃c > 0, n0 > 0 ∀n ≥ n0 . 0 ≤ f (n) ≤ cg (n)}
Ω(g ) = {f : N −→ R | ∃c > 0, n0 > 0 ∀n ≥ n0 . 0 ≤ cg (n) ≤ f (n)}
o (g ) = {f : N −→ R | ∀c > 0 ∃n0 > 0 ∀n ≥ n0 . 0 ≤ f (n) < cg (n)}
ω(g ) = {f : N −→ R | ∀c > 0 ∃n0 > 0 ∀n ≥ n0 . 0 ≤ cg (n) < f (n)}
2.3 Komplexität von Algorithmen
2-47
Gebräuchliche Wachstumsklassen
Θ(1)
Θ(log(n))
Θ(n)
Θ(n log(n))
Θ(n2 )
Θ(nk )
Θ(2n )
konstantes Wachstum
logarithmisches Wachstum
lineares Wachstum
„fast lineares“ Wachstum
quadratisches Wachstum
polynomiales Wachstum
exponentielles Wachstum
O (n2 )
Ω(n)
höchstens quadratisches Wachstum
mindestens lineares Wachstum
Beispiel: Die Laufzeit beim „Sortieren durch Einfügen“ beträgt im
günstigsten Fall Θ(n) und im ungünstigsten Falls Θ(n2 ). Die
Laufzeit kann also durch Ω(n) nach unten und durch O (n2 ) nach
oben abgeschätzt werden. Die Laufzeit wird auch durch O (n3 )
nach oben abgeschätzt! In der Praxis werden häufig möglichst
scharfe obere Schranken gesucht.
2.3 Komplexität von Algorithmen
2-48
Graphen einiger Funktionen
n log(n)
n
log(n)
1
2.3 Komplexität von Algorithmen
2-49
Graphen einiger Funktionen
2n
n3
n2
2.3 Komplexität von Algorithmen
2-50
Exemplarische Werte
n=
log2 (n) ≈
n log2 (n) ≈
n2 =
2n ≈
1
0
0
1
2
10
3
33
100
103
100
7
664
10000
1030
1000
10
9966
1000000
10301
10000
13
132877
100000000
103010
Maximales n bei gegebener Zeit, Ann.: 1 Schritt benötigt 1µs
n
n2
n3
2n
1 Min.
1 Std.
1 Tag
1 Woche
1 Jahr
6 · 107
7750
391
25
3.6 · 109
6 · 104
1530
31
8.6 · 1010
2.9 · 105
4420
36
6 · 1011
7.9 · 105
8450
39
3 · 1013
5.6 · 106
31600
44
2.3 Komplexität von Algorithmen
2-51
Asymptotische Notationen in Gleichungen
◮
◮
◮
◮
2n2 + 3n + 1 = Θ(n2 ) heißt 2n2 + 3n + 1 ∈ Θ(n2 ).
2n2 + 3n + 1 = 2n2 + Θ(n) heißt: Es gibt eine Funktion
f (n) ∈ Θ(n) mit 2n2 + 3n + 1 = 2n2 + f (n).
2n2 + Θ(n) = Θ(n2 ) heißt: Für jede Funktion f (n) ∈ Θ(n) gibt
es eine Funktion g (n) ∈ Θ(n2 ) mit 2n2 + f (n) = g (n).
In Gleichungsketten
2n2 + 3n + 1 = 2n2 + Θ(n) = Θ(n2 )
werden die Gleichungen einzeln gelesen: Die erste Gleichung
besagt, dass es eine Funktion f (n) ∈ Θ(n) mit
2n2 + 3n + 1 = 2n2 + f (n) gibt. Die zweite Gleichung sagt
aus, dass es für jede Funktion g (n) ∈ Θ(n) eine Funktion
h (n) ∈ Θ(n2 ) mit 2n2 + g (n) = h (n) gibt. Aus der
Hintereinanderausführung folgt 2n2 + 3n + 1 = Θ(n2 ).
2.3 Komplexität von Algorithmen
2-52
Eigenschaften der asymptotischen Notation
Eine Funktion f : N −→ R heißt asymptotisch positiv, wenn gilt:
∃n0 > 0 ∀n ≥ n0 . f (n) > 0.
Für alle asymptotisch positiven Funktionen f , g : N −→ R gelten
die folgenden Aussagen.
Transitivität:
f (n) = Θ(g (n)) ∧ g (n) = Θ(h (n)) =⇒ f (n) = Θ(h (n)),
f (n) = O (g (n)) ∧ g (n) = O (h (n)) =⇒ f (n) = O (h (n)),
f (n) = Ω(g (n)) ∧ g (n) = Ω(h (n)) =⇒ f (n) = Ω(h (n)),
f (n) = o (g (n)) ∧ g (n) = o (h (n)) =⇒ f (n) = o (h (n)),
f (n) = ω(g (n)) ∧ g (n) = ω(h (n)) =⇒ f (n) = ω(h (n)).
2.3 Komplexität von Algorithmen
2-53
Eigenschaften der asymptotischen Notation
Reflexivität:
f (n) = Θ(f (n)),
f (n) = O (f (n)),
f (n) = Ω(f (n)).
Symmetrie:
f (n) = Θ(g (n)) ⇐⇒ g (n) = Θ(f (n)).
Austausch-Symmetrie:
f (n) = Θ(g (n)) ⇐⇒ f (n) = O (g (n)) ∧ f (n) = Ω(g (n)),
f (n) = O (g (n)) ⇐⇒ g (n) = Ω(f (n)),
f (n) = o (g (n)) ⇐⇒ g (n) = ω(f (n)).
Θ, O , Ω, o und ω werden landausche Symbole genannt.
2.3 Komplexität von Algorithmen
2-54
Beispiele
8 = Θ(1)
3n2 − 5n + 8 = Θ(n2 )
3n2 − 5n + 8 = O (n2 )
3n2 − 5n + 8 = Ω(n2 )
logb (n)
loga (n) =
logb (a )
Θ(loga (n)) = Θ(logb (n))
12 log10 (n) = Θ(log(n))
12 log2 (n) = Θ(log(n))
2.3 Komplexität von Algorithmen
2-55
Laufzeit von imperativen Algorithmen
Folgende Annahmen werden zur Analyse der Laufzeit von
imperativen Algorithmen getroffen:
◮
Zuweisung: Die Laufzeit ist konstant.
◮
Sequenz: Die Laufzeit ist die Summe der Laufzeiten der
Einzelanweisungen.
◮
Alternative: Die Laufzeit ist im ungünstigsten Fall die Laufzeit
der Bedingungsauswertung plus dem Maximum der
Laufzeiten der Alternativen.
◮
Iteration: Die Laufzeit errechnet sich aus dem Produkt der
Laufzeit der inneren Anweisung und der Anzahl der
Iterationen. Hinzu kommt die Laufzeit für einen weiteren Test.
2.3 Komplexität von Algorithmen
2-56
Sortieren durch Einfügen
Code
Kosten
insertionSort(A)
c1
j ← 2;
c2
while j ≤ length(A) do
key ← A[j];
c3
i ← j - 1;
c4
while i > 0 und A[i] > key c5
do
A[i + 1] ← A[i];
c6
c7
i ← i - 1;
od;
A[i + 1] ← key;
c8
j ← j + 1;
c9
od;
2.3 Komplexität von Algorithmen
Anzahl
1
n
n−1
n−1
Pn
j =2 tj
Pn
(tj − 1)
j
=
2
Pn
j =2 (tj − 1)
n−1
n−1
2-57
Abschätzung durch die O-Notation
Code
Kosten
insertionSort(A)
j ← 2;
O (1)
O (n)
while j ≤ length(A) do
key ← A[j];
O (n)
i ← j - 1;
O (n)
while i > 0 und A[i] > key; O (n2 )
do
A[i + 1] ← A[i];
O (n2 )
i ← i - 1;
O (n2 )
od;
O (n)
A[i + 1] ← key;
j ← j + 1;
O (n)
od;
Die Laufzeit T (n) liegt in O (n2 ), d. h. T (n) = O (n2 ).
2.3 Komplexität von Algorithmen
2-58
Korrektheit von Softwaresystemen
In vielen Situationen ist eine korrekte Funktionsweise eines
Softwaresystems von großer Bedeutung. Dies gilt insbesondere,
wenn das System
◮
sicherheitskritisch (z. B. Atomreaktor),
◮
kommerziell kritisch (z. B. massenproduzierte Chips) oder
◮
politisch kritisch (z. B. Militär)
ist.
2.4 Korrektheit von Algorithmen
2-59
Korrektheit von Softwaresystemen
Es gibt mehrere Möglichkeiten, die Zuverlässigkeit von
Softwaresystemen zu erhöhen:
◮
Software Engineering:
Maßnahmen während des gesamten
Softwareentwicklungsprozesses.
◮
Programmierung:
Beispiel: Ausnahmebehandlung, Zusicherungen.
◮
Validation:
Systematische Tests unter Einsatzbedingungen; Tests zeigen
die Anwesenheit, aber nicht die Abwesenheit von Fehlern.
◮
Verifikation:
Mathematischer Nachweis der Korrektheit von Algorithmen.
2.4 Korrektheit von Algorithmen
2-60
Korrektheit von Softwaresystemen
Um ein System zu verifizieren zu können benötigt man Methoden,
Werkzeuge und Sprachen, zur
◮
Modellierung von Systemen auf hoher Abstraktionsebene,
◮
Spezifikation nachzuweisender Eigenschaften dieser
Systeme (Terminierungsverhalten, berechnete
Funktionswerte, . . . ) und zur
◮
Verifikation, d. h. zum formalen Beweis, dass ein
implementiertes System die spezifizierten Eigenschaften hat.
In diesem Abschnitt behandeln wir eine Möglichkeit zur
Spezifikation und Verifikation imperativer Algorithmen.
2.4 Korrektheit von Algorithmen
2-61
Hoaresche Logik
Es seien ein Algorithmus S sowie Bedingungen p und q gegeben.
◮
Wir schreiben in diesem Fall
{p} S {q}
und nennen
◮
p Vorbedingung,
◮
q Nachbedingung und
◮
(p,q) Spezifikation von S.
2.4 Korrektheit von Algorithmen
2-62
Hoaresche Logik
◮
S heißt partiell-korrekt bezüglich der Spezifikation (p,q), wenn
jede Ausführung von S, die in einem Zustand beginnt, der p
erfüllt und die terminiert, zu einem Zustand führt, der q erfüllt.
|= {p} S {q}
◮
S wird total-korrekt bezüglich der Spezifikation (p,q) genannt,
wenn jede Ausführung von S, die in einem Zustand beginnt,
der p erfüllt, terminiert und zu einem Zustand führt, der q
erfüllt.
2.4 Korrektheit von Algorithmen
2-63
Beispiele
◮
|= {true }x ← 1{x = 1}
◮
|= {x = 1}x ← x + 1{x = 2}
◮
|= {y = a }x ← y {x = a ∧ y = a }
◮
|= {x = a ∧ y = b }z ← x ; x ← y ; y ← z {x = b ∧ y = a }
◮
|= {false }x ← 1{x = 42}
◮
|= {true } while 0 = 0 do x ← 1 od {x = 23}
◮
|= {x > 0} while x , 0 do x ← x − 1 od {x = 0}
◮
|= {true } while x , 0 do x ← x − 1 od {x = 0}
◮
|= {true } while p (x ) do α od {¬p (x )}
◮
|= {x + y = a } while x , 0 do x ← x − 1; y ← y + 1 od {x =
0 ∧ x + y = a}
2.4 Korrektheit von Algorithmen
2-64
Hoarescher Kalkül
Zuweisung:
Sequenz:
If:
While:
Anpassungsregel:
2.4 Korrektheit von Algorithmen
{pxt } x ← t {p }
{p } S1 {r }, {r } S2 {q}
{p } S 1 ; S 2 {q }
{p ∧ e } S1 {q}, {p ∧ ¬e } S2 {q}
{p } if e then S1 else S2 fi {q}
{p ∧ e } S {p }
{p } while e do S od {p ∧ ¬e }
p ⊃ q, {q} S {r }, r ⊃ s
{p } S {s }
2-65
Hoarescher Kalkül
◮
Der hoaresche Kalkül besteht aus dem Axiomenschema für
die Zuweisung und Ableitungsregeln.
◮
Falls {p} S {q} mithilfe des hoareschen Kalküls hergeleitet
werden kann, schreibt man
⊢ {p} S {q}.
2.4 Korrektheit von Algorithmen
2-66
Schleifeninvariante
Die Bedingung p in der While-Regel heißt Schleifeninvariante.
{p ∧ e } S {p }
{p } while e do S od {p ∧ ¬e }
Eine Schleifeninvariante gilt vor jedem Schleifendurchlauf und
damit auch nach jedem Schleifendurchlauf, speziell also nach
Beendigung der Wiederholungsanweisung. Dann gilt zudem ¬e .
2.4 Korrektheit von Algorithmen
2-67
Schleifeninvariante
Beispiel: Für den folgenden Algorithmus ist q = k − 1 eine
Schleifeninvariante.
{q = 0 ∧ k = 1}
while k , n + 1 do
q ← q + 1;
k ← k + 1;
od;
{q = n }
Nach Ausführung der Schleife gilt: q = k − 1 ∧ ¬(k , n + 1)
Daraus folgt: q = n
2.4 Korrektheit von Algorithmen
2-68
Eigenschaften des Kalküls
Korrektheit:
⊢ {p } S {q} =⇒ |= {p } S {q}
relative Vollständigkeit:
⊢ {p } S {q} ⇐= |= {p } S {q}
2.4 Korrektheit von Algorithmen
2-69
Beispiel: Division mit Rest
◮
Es sollen zwei ganze Zahlen x , y ∈ Z mit x ≥ 0 und y > 0
durcheinander dividiert werden.
◮
Das Ergebnis der Division x /y ist der Quotient q und der Rest
r mit x = qy + r ∧ 0 ≤ r < y .
var x, y, q, r: int;
input x, y;
q ← 0;
r ← x;
while r >= y do
r = r - y;
q = q + 1;
od;
output q, r;
2.4 Korrektheit von Algorithmen
2-70
Beispiel: Division mit Rest
◮
Mithilfe des hoareschen Kalküls leiten wir jetzt den Ausdruck
{x ≥ 0} S {x = qy + r ∧ 0 ≤ r < y }
her, wobei S der obige imperative Algorithmus ist.
◮
Wegen der Korrektheit des Kalküls können wir dann
schließen, dass der Algorithmus bezüglich der angegebenen
Vor- und Nachbedingung partiell-korrekt ist.
◮
Die Bedingung y > 0 wird zum Nachweis der totalen
Korrektheit benötigt.
2.4 Korrektheit von Algorithmen
2-71
Beispiel: Division mit Rest
Wir zeigen zuerst, dass
x = qy + r ∧ 0 ≤ r
eine Schleifeninvariante ist. Dies ergibt sich aus:
x = qy + r ∧ 0 ≤ r ∧ r ≥ y
2.4 Korrektheit von Algorithmen
=⇒
x = (q + 1)y + (r − y ) ∧ 0 ≤ r − y
2-72
Beispiel: Division mit Rest
Die Behauptung folgt dann aus den beiden Aussagen
x ≥ 0 =⇒ 0 ≤ x
und
x = qy + r ∧ 0 ≤ r ∧ ¬(r ≥ y )
2.4 Korrektheit von Algorithmen
=⇒
x = qy + r ∧ 0 ≤ r < y .
2-73
Beispiel: Division mit Rest
◮
Da nach Voraussetzung y > 0 gilt, durchläuft die Variable r
eine monoton streng fallende Folge natürlicher Zahlen:
r0 , r1 , r2 , . . .
◮
Da y konstant ist, wird deshalb für ein i die Bedingung
ri < y
wahr. Das heißt, das Programm terminiert schließlich und ist
deshalb total-korrekt bezüglich der angegebenen
Bedingungen.
Bemerkung: Es gibt auch Kalküle zum Nachweis der totalen
Korrektheit.
2.4 Korrektheit von Algorithmen
2-74
Beispiel: Division mit Rest
Vor- und Nachbedingung, Schleifeninvariante als Annotation:
static private int remainder (int x, int y) {
assert x >= 0 && y > 0;
int q = 0,
r = x;
assert x == q * y + r && 0 <= r;
while (r >= y) {
r = r - y;
q = q + 1;
assert x == q * y + r && 0 <= r;
}
assert x == q * y + r && 0 <= r && r < y;
return r;
}
2.4 Korrektheit von Algorithmen
2-75
Wunschtraum
Es ist ein Algorithmus gesucht, der für beliebige p, S und q beweist
oder widerlegt, dass
⊢ {p} S {q}
gilt.
Solch ein Algorithmus kann nicht existieren!
Dann existiert natürlich erst recht kein analoger Algorithmus für die
totale Korrektheit.
2.4 Korrektheit von Algorithmen
2-76
Totale Korrektheit
◮
Zum Nachweis der totalen Korrektheit muss zusätzlich zur
partiellen Korrektheit die Terminierung gezeigt werden.
◮
Um die Terminierung von Schleifen nachzuweisen, gibt es
keine allgemeine Methode, oft funktioniert aber die
Vorgehensweise vom obigen Beispiel:
1. Man suche einen Ausdruck u, dessen Wert eine natürliche
Zahl ist.
2. Man beweise, dass u bei jedem Schleifendurchlauf echt
kleiner wird.
3. Da die natürlichen Zahlen wohlgeordnet sind, muss die
Schleife terminieren.
2.4 Korrektheit von Algorithmen
2-77
Beispiel: Insertionsort
P (j ): Das Feld A [1 · · · j ] ist eine Permutation des Ausgangsfeldes
A [1 · · · j ].
S (j ): Das Feld A [1 · · · j ] ist sortiert.
A ∗ [j ]: Der ursprünglich in A [j ] enthaltene Wert.
Pk∗ (j ): Das Feld A [1 · · · k − 1, k + 1 · · · j ] ist eine Permutation des
Ausgangsfeldes A [1 · · · j − 1].
Sk∗ (j ): Das Feld A [1 · · · k − 1, k + 1 · · · j ] ist sortiert.
2.4 Korrektheit von Algorithmen
2-78
Beispiel: Insertionsort
{n ≥ 1}
{P (1) ∧ S (1) ∧ n ≥ 1}
j ← 2;
{P (j − 1) ∧ S (j − 1) ∧ 2 ≤ j ≤ n + 1}
while j ≤ n do
key ← A[j];
i ← j - 1;
while i > 0 ∧ A[i] > key do
A[i+1] ← A[i];
i ← i - 1;
od;
A[i+1] ← key;
j ← j + 1;
od;
{P (j − 1) ∧ S (j − 1) ∧ 2 ≤ j ≤ n + 1 ∧ j > n}
{P (n) ∧ S (n)}
2.4 Korrektheit von Algorithmen
2-79
Beispiel: Insertionsort
{P (j − 1) ∧ S (j − 1) ∧ 2 ≤ j ≤ n + 1 ∧ j ≤ n}
{Pj∗ (j ) ∧ Sj∗(j ) ∧ 0 ≤ j − 1}
key ← A[j];
i ← j - 1;
{Pi∗+1 (j ) ∧ Si∗+1 (j ) ∧ key < A [i + 2], . . . , A [j ]
∧0 ≤ i ≤ j − 1 ∧ key = A ∗ [j ]}
while i > 0 ∧ A[i] > key do
A[i+1] ← A[i];
i ← i - 1;
od;
A[i+1] ← key;
j ← j + 1;
{P (j − 1) ∧ S (j − 1) ∧ 2 ≤ j ≤ n + 1}
2.4 Korrektheit von Algorithmen
2-80
Konzepte imperativer Sprachen
◮
Anweisungen
◮
◮
◮
Ausdrücke
◮
◮
◮
primitive Anweisungen: Zuweisung, Block, Prozeduraufruf
zusammengesetzte Anweisungen: Sequenz, Auswahl,
Iteration
primitive Ausdrücke: Konstante, Variable, Funktionsaufruf
zusammengesetzte Ausdrücke: Operanden/Operatoren
Datentypen
◮
◮
primitive Datentypen: Wahrheitswerte, Zeichen, Zahlen,
Aufzählung
zusammengesetzte Datentypen: Felder, Verbund, Vereinigung,
Zeiger
2.5 Konzepte imperativer Sprachen
2-81
Konzepte imperativer Sprachen
◮
Abstraktion
◮
◮
◮
◮
Anweisung: Prozedurdeklaration
Ausdruck: Funktionsdeklaration
Datentyp: Typdeklaration
Weitere Konzepte
◮
◮
◮
◮
◮
Ein- und Ausgabe
Ausnahmebehandlung
Bibliotheken
Parallele und verteilte Berechnungen
...
2.5 Konzepte imperativer Sprachen
2-82
Blöcke
◮
Motivation: Ein Block fasst mehrere Anweisungen und
Deklarationen zu einer Einheit zusammen.
◮
Syntax: Die Abgrenzung erfolgt durch syntaktische Elemente,
wie z. B. Schlüsselwörter (begin, end), Klammerung oder
Einrückung. Blöcke dürfen überall statt einer einzelnen
Anweisung stehen.
◮
Kontrollfluss: Die Ausführung eines Blocks beginnt mit der
ersten Anweisung und wird im Normalfall nach der letzten
beendet. Es gibt auch Anweisungen zum Verlassen des
Blocks: break, return.
◮
Lokale Variablen: Innerhalb eines Blocks können Variablen
deklariert werden, die nur in diesem Block verfügbar sind.
2.5 Konzepte imperativer Sprachen
2-83
Blöcke
◮
Globale Variable: Innerhalb eines Blocks sind alle
Bezeichner aus den umschließenden Blöcken sichtbar, soweit
sie nicht von einer inneren Deklaration überdeckt werden.
◮
Gültigkeitsbereich (scope): Ein Bezeichner ist innerhalb des
Blocks gültig, in dem er deklariert wurde, nicht aber
außerhalb. Die Gültigkeit ist eine statische Eigenschaft, die
sich aus dem Programmtext ableitet.
◮
Lebensdauer: Ist eine dynamische Eigenschaft und
bezeichnet den Zeitraum der Verfügbarkeit eines Wertes
während der Laufzeit. Werte von überdeckten Variablen sind
nach Beendigung des überdeckenden Blocks wieder
verfügbar.
2.5 Konzepte imperativer Sprachen
2-84
Blöcke
Code
var x,y: int;
· · · do
var x: int;
···
···
od;
···
Gültig
|
|
|
x
Sichtbar
|
|
|
|
|
|
|
y
|
|
|
|
|
|
|
x
|
|
|
x
|
|
|
|
|
|
|
y
|
|
|
|
x
Einzelheiten lernen Sie in der Vorlesung „Programmieren“.
2.5 Konzepte imperativer Sprachen
2-85
Prozeduren
◮
Abstraktion: Eine Prozedur fasst mehrere Anweisungen
zusammen und gibt ihnen einen Namen. Der Aufruf einer
Prozedur führt die Anweisungen aus und wirkt dabei wie eine
elementare Anweisung. Über Parameter können die
Anweisungen gesteuert werden.
◮
Wiederverwertung: Eine Prozedur wird nur einmal deklariert
und kann beliebig oft verwendet werden.
◮
Modularisierung: Die Implementation der Prozedur muss
dem aufrufenden Programm nicht bekannt sein.
Veränderungen innerhalb der Prozedur erfordern keine
Änderung des aufrufenden Programms.
2.5 Konzepte imperativer Sprachen
2-86
Deklaration von Prozeduren
◮
Deklaration:
proc P(p1 , . . . , pn ) begin a; end
◮
Name: P ist der Name der Prozedur und frei wählbar.
◮
Parameter: p1 , . . . , pn sind die Parameter der Prozedur. Sie
sind lokale Variablen mit eigenem Typ (formale Parameter),
denen beim Aufruf der Prozedur Werte (aktuelle Parameter)
zugewiesen werden. Die Gültigkeit der Parameter ist auf den
Rumpf der Prozedur beschränkt.
◮
Rumpf: a ist der Rumpf der Prozedur. Er enthält die
auszuführenden Anweisungen.
2.5 Konzepte imperativer Sprachen
2-87
Werte- und Referenzparameter
Werteparameter (call by value):
◮
Der aktuelle Parameter kann ein Ausdruck oder speziell auch
eine Variable sein.
◮
Es wird der Wert des Ausdrucks übergeben.
◮
Die Deklaration erfolgt (zum Beispiel) ohne das Präfix var.
2.5 Konzepte imperativer Sprachen
2-88
Werte- und Referenzparameter
Referenzparameter (call by reference):
◮
Der aktuelle Parameter muss eine Variable sein.
◮
Es wird die Variable (Adresse) übergeben.
◮
In der Deklaration werden Referenzparameter (zum Beispiel)
mit var bezeichnet.
Es gibt weitere Arten der Parameterübergabe.
2.5 Konzepte imperativer Sprachen
2-89
Beispiel
Aufgabe: Vertausche die Inhalte der Variablen x und y und addiere
zu beiden den Wert a .
proc vertausche(a: int; var x, y: int) begin
var z: int;
// lokale Variable
z ← x;
x ← y;
y ← z;
x ← x + a;
y ← y + a;
end
Die Wirkung ist die „simultane“ Ersetzung.
(x , y ) ← (y + a , x + a )
2.5 Konzepte imperativer Sprachen
2-90
Beispiel
Aufrufen lässt sich die Prozedur in unterschiedlichen Umgebungen
mit verschiedenen aktuellen Parametern:
vertausche(0, i, j);
vertausche(3, a[1], a[2]);
vertausche(a[3], a[1], a[2]);
vertausche(i, i, j);
2.5 Konzepte imperativer Sprachen
2-91
Funktionen
◮
Abstraktion: Funktionen sind Abstraktionen von Ausdrücken.
Der Aufruf einer Funktion berechnet einen Wert eines Typs τ.
◮
Deklaration:
func F(p1 , . . . , pn ): τ begin a; end
◮
Auswertung: Der Rückgabewert der Funktion wird (zum
Beispiel) durch eine spezielle return-Anweisung angegeben.
Diese Anweisung verlässt den Funktionsblock mit sofortiger
Wirkung. Häufig wird auch der letzte Term innerhalb einer
Funktion als Rückgabewert genommen, oder eine Zuweisung
zum Funktionsnamen legt den Rückgabewert fest.
◮
Seiteneffekt: Wenn der Aufruf einer Funktion den Wert einer
globalen Variablen verändert, spricht man von einem
Seiteneffekt.
2.5 Konzepte imperativer Sprachen
2-92
Beispiel
func EUKLID(x, y: int): int begin
var a,b,r: int
a ← x;
b ← y;
while b # 0 do
r ← a mod b;
a ← b;
b ← r;
od;
return a;
end
Für negative Werte der Parameter x und y hängt das Verhalten der
Funktion von der Implementierung des mod-Operators ab.
2.5 Konzepte imperativer Sprachen
2-93
Rekursive Definitionen
Die Funktion f : N −→ N wird durch


1







1 f (n) = 


f n2





f (3n + 1)
n = 0,
n = 1,
n ≥ 2, n gerade,
n ≥ 2, n ungerade.
rekursiv definiert.
Damit eine rekursive Funktion einen Wert liefern kann, muss
mindestens eine Alternative eine Abbruchbedingung enthalten.
2.6 Rekursionen
2-94
Auswertung von Funktionen
Funktionsdefinitionen können als Ersetzungssysteme gesehen
werden. Funktionswerte lassen sich aus dieser Sicht durch
wiederholtes Einsetzen berechnen. Die Auswertung von f (3) ergibt
f (3) → f (10) → f (5) → f (16) → f (8) → f (4) → f (2) →
f (1) → 1.
Terminiert der Einsetzungsprozess stets?
2.6 Rekursionen
2-95
Formen der Rekursion
◮
Lineare Rekursion
In jedem Zweig einer Fallunterscheidung tritt die Rekursion
höchstens einmal auf. Bei der Auswertung ergibt sich eine
lineare Folge von rekursiven Aufrufen.
◮
Endrekursion
Der Spezialfall einer linearen Rekursion bei dem in jedem
Zweig die Rekursion als letzte Operation auftritt.
Endrekursionen können effizient implementiert werden.
2.6 Rekursionen
2-96
Formen der Rekursion
◮
◮
Verzweigende Rekursion oder Baumrekursion


! 
k = 0, k = n,
1



n

!
!
=
n−1
n−1


k

+
sonst.

 k −1
k
Geschachtelte Rekursion



m+1
n = 0,




f (n, m) = 
f (n − 1, 1)
m = 0,




f (n − 1, f (n, m − 1)) sonst.
2.6 Rekursionen
2-97
Formen der Rekursion
◮
Verschränkte Rekursion oder wechselseitige Rekursion
Der rekursive Aufruf erfolgt indirekt.



n = 0,
true
even(n) = 

odd(n − 1) n > 0.



n = 0,
false
odd(n) = 

even(n − 1) n > 0.
2.6 Rekursionen
2-98
Fibonacci-Zahlen
Definition:



n=0
0




fib(n) = 
1
n=1




fib(n − 1) + fib(n − 2) n ≥ 2
Funktionswerte:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, . . .
2.6 Rekursionen
2-99
Fibonacci-Zahlen
Auswertung:
fib5
fib4
fib3
fib3
fib2
2.6 Rekursionen
fib1
fib0
1
0
fib2
fib2
fib1
fib1
fib1
fib0
fib1
fib0
1
1
0
1
0
1
2-100
Algorithmus von Euklid
Rekursive Version:



falls b = 0
a
ggT(a , b ) = 

ggT(b , a mod b ) falls b > 0
Auswertung:
ggT(36, 52) → ggT(52, 36) → ggT(36, 16) →
ggT(16, 4) → ggT(4, 0) → 4
ggT(36, 52) = 4
2.6 Rekursionen
2-101
Fakultät
Definition:
Auswertung:



x≤0
1
x! = 

x · (x − 1)! sonst
4! = 4 · 3! = 4 · 3 · 2! = 4 · 3 · 2 · 1! = 4 · 3 · 2 · 1 · 0! = 4 · 3 · 2 · 1 · 1 = 24
2.6 Rekursionen
2-102
Fakultät (rekursiv)
func FAK(x: int): int begin
if x <= 0
then return 1
else return x * FAK(x - 1)
fi
end
2.6 Rekursionen
2-103
Fakultät (rekursiv)
Berechnung von FAK(2):
1. Anlegen einer lokalen Variable x , mit Wert 2.
2. x , 0, daher Rückgabewert x · FAK (x − 1) = 2 · FAK (1)
2.1 Anlegen einer neuen lokalen Variable x mit dem Wert 1.
2.2 x , 0, daher Rückgabewert x · FAK (x − 1) = 1 · FAK (0)
2.2.1 Anlegen einer neuen lokalen Variable x mit dem Wert 0.
2.2.2 x = 0, daher ist der Rückgabewert 1.
2.3 Rückgabewert ist 1 · 1 = 1
3. Rückgabewert ist 2 · 1 = 2
2.6 Rekursionen
2-104
Fakultät (iterativ)
func FAK(x: int): int begin
var result: int;
result ← 1;
while x > 0 do
result ← result * x;
x ← x - 1;
od;
return result;
end
Diese Lösung hat einen von x unabhängigen Speicherbedarf.
2.6 Rekursionen
2-105
Rekursive Probleme
Viele Probleme besitzen eine rekursive Struktur. Für sie können
rekursive Lösungen häufig kurz und elegant formuliert werden:
◮
Berechnung rekursiv definierter mathematischer Funktionen,
◮
Überprüfen der Syntax von Ausdrücken,
◮
Operationen auf rekursiv definierten Datenstrukturen.
Wichtige Unterschiede:
◮
Rekursive Formulierungen sparen explizite Laufvariablen und
Kontrollstrukturen.
◮
Rekursive Aufrufe verbrauchen Speicher auf dem Stack, es
sei denn, es handelt sich um Endrekursion, die von modernen
Compilern optimiert wird.
2.6 Rekursionen
2-106
Herunterladen