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