Rekursion/Funktionale Programmierung

Werbung
Rekursion/Funktionale Programmierung:
Einführung und Begriffe
5.1 Einführung und Begriffe
5.2 Beispiele rekursiver Methoden in Java
5.3 Ein Blick auf funktionales Programmieren
5.1 Einführung und Begriffe
5-1
Rekursion
Kann ein Problem auf gleiche Probleme anderen Umfangs zurückgeführt werden?
5.1 Einführung und Begriffe
5-2
Fakultät
Definition:
(
1
n == 0, n == 1
n! =
n · (n − 1)! sonst
Auswertung:
4! = 4 · 3! = 4 · 3 · 2! = 4 · 3 · 2 · 1! = 4 · 3 · 2 · 1 = 24
5.1 Einführung und Begriffe
5-3
Quersumme
Als einführendes Java-Beispiel sehen wir uns zwei statische Methoden zur Berechnung
der iterierten Quersumme positiver Zahlen an.
6→6
15 → 1 + 5 = 6
789 → 7 + 8 + 9 = 24 → 2 + 4 = 6
5.1 Einführung und Begriffe
5-4
Quersumme (iterative Methode)
static int g(int n) {
int s = n;
while (s > 9) {
int i = s;
s = 0;
do {
s = s + i%10;
i = i / 10;
} while (i > 0);
}
return s;
}
5.1 Einführung und Begriffe
5-5
Quersumme (rekursive Methode)
static int f(int n) {
return n <= 9 ? n : f( f(n/10) + n%10 );
}
5.1 Einführung und Begriffe
5-6
Quersumme
• Iterative Methode: 10 Zeilen
• Rekursive Methode: 1 Zeile
(Weniger als 1 Zeile ist nicht möglich.)
• Rekursion ist eine mächtige Programmiertechnik.
• Wir haben das imperative und das objektorientierte Paradigma kennengelernt.
• In diesem Kapitel sehen wir uns die Rekursion als Programmiertechnik sowie
das funktionale/deklarative Paradigma an.
5.1 Einführung und Begriffe
5-7
Quersumme
Der obige rekursive Algorithmus zur Berechnung der Quersumme lautet als
Haskell-Programm:
f :: Integer -> Integer
f n | n <= 9
= n
| otherwise = f(f(n ‘div‘ 10) + (n ‘mod‘ 10))
f 789
6
Die Sprache Haskell bieten wir Ihnen im Modul Programmieren für Forgeschrittene an.
5.1 Einführung und Begriffe
5-8
Partielle und totale Funktionen
Eine partielle Funktion
f : A −→ B
ordnet jedem Element x einer Teilmenge Df ⊆ A genau ein Element f (x ) ∈ B zu. Die
Menge Df heißt Definitionsbereich von f . f ist eine totale Funktion, wenn Df = A gilt.
Beispiel:
f : R −→ R,
Df = R \ {0},
1
f (x ) =
x
Algorithmen können undefinierte Ausdrücke enthalten und müssen nicht in jedem Fall
terminieren, d. h.:
Algorithmen berechnen partielle Funktionen!
5.1 Einführung und Begriffe
5-9
Definition von Funktionen
• Wenn der Definitionsbereich einer Funktion endlich ist, lässt sie sich durch Angabe
aller Funktionswerte in einer Tabelle definieren.
• Beispiel:
∧:B×B→B
false
false
true
true
5.1 Einführung und Begriffe
false
true
false
true
false
false
false
true
5-10
Definition von Funktionen
• In vielen Fällen wird eine Funktion f : A → B durch einen Ausdruck, der zu jedem
Element aus Df genau einen Wert von B liefert, beschrieben.
• Beispiel:
max : R × R → R
(
x
max(x , y ) =
y
x ≥y
x <y
= if x ≥ y then x else y fi
5.1 Einführung und Begriffe
5-11
Rekursive Definitionen
Die Funktion f : N −→ N wird durch


1



1
f (n) =
n

f 2



f (3n + 1)
n = 0,
n = 1,
n ≥ 2, n gerade,
n ≥ 2, n ungerade.
rekursiv definiert.
5.1 Einführung und Begriffe
5-12
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? (Collatz-Problem: Lothar Collatz, 1937)
5.1 Einführung und Begriffe
5-13
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 sehr effizient implementiert
werden.
5.1 Einführung und Begriffe
5-14
Formen der Rekursion
• Verzweigende Rekursion oder Baumrekursion


1
k = 0, k = n,

n
= n − 1 n − 1

k
+
0 < k < n.

k −1
k
• Geschachtelte Rekursion


n = 0,
m + 1
f (n, m) = f (n − 1, 1)
n=
6 0, m = 0,


f (n − 1, f (n, m − 1)) n =
6 0, m 6= 0.
5.1 Einführung und Begriffe
5-15
Formen der Rekursion
• Verschränkte Rekursion oder wechselseitige Rekursion
Der rekursive Aufruf erfolgt indirekt.
(
true
n = 0,
even(n) =
odd(n − 1) n > 0.
(
false
n = 0,
odd(n) =
even(n − 1) n > 0.
5.1 Einführung und Begriffe
5-16
Fakultät
Definition:
(
1
n <= 0
n! =
n · (n − 1)! sonst
Auswertung:
4! = 4 · 3! = 4 · 3 · 2! = 4 · 3 · 2 · 1! = 4 · 3 · 2 · 1 = 24
5.1 Einführung und Begriffe
5-17
Fakultät
class Fakultaet {
static long fak(long n) {
if (n <= 0)
return 1;
else
return n * fak(n-1);
}
static long f(long n) {
return n <= 0 ? 1 : n * f(n-1);
// Dreistelliger Operator.
}
public static void main(String[] args) {
for (int i = 0; i <= 20; i++) {
System.out.println(i + "! = " + fak(i));
assert f(i) == fak(i); // Diesen Befehl sehen wir uns demnächst an.
}
}
}
5.1 Einführung und Begriffe
5-18
Fibonacci-Folge
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, ...
5.1 Einführung und Begriffe
5-19
Fibonacci-Folge
Auswertung:
fib 5
fib 4
fib 3
fib 3
fib 2
fib 1
fib 0
1
0
5.1 Einführung und Begriffe
fib 2
fib 2
fib 1
fib 1
fib 1
fib 0
fib 1
fib 0
1
1
0
1
0
1
5-20
Fibonacci-Folge
class Fibonacci {
static long fib(long n) {
if ((n == 0) || (n == 1))
return n;
else
return fib(n-1) + fib(n-2);
}
public static void main(String[] args) {
for (int i = 0; i <= 20; i++) {
System.out.println(i + ": " + fib(i));
}
}
}
5.1 Einführung und Begriffe
5-21
Fibonacci-Folge
Die Anzahl der Aufrufe g(n) ist gegeben durch die folgende Rekurrenzgleichung:
(
1
n = 0, n = 1
g(n) =
1 + g(n − 1) + g(n − 2) n ≥ 2
Die Funktion g wächst exponentiell. Beispielsweise ist g(5) = 15, vgl. obiges
Aufrufdiagramm:
g(0)=1, g(1)=1, g(2)=3, g(3)=5, g(4)=9, g(5)=15, .....
Zur Lösung von Rekurrenzgleichungen s. Abschnitt 4.4 in Struckmann/Wätjen.
5.1 Einführung und Begriffe
5-22
Fibonacci-Folge/Dynamische Algorithmen
Am Aufrufdiagramm von fib(5) erkennen wir, dass etliche Funktionswerte mehrfach
berechnet werden. Die Idee der dynamischen Algorithmen ist es, Ergebnisse von
Berechnungen zu speichern, so dass die Berechnungen nicht mehrfach durchgeführt
werden müssen.
Die folgende Methode realisiert die Fibonacci-Folge mit einem dynamischen
Algorithmus, indem sie bereits berechnete Funktionswerte in einem Array speichert.
Die Array-Elemente werden mit dem Wert −1 vorbesetzt. Die Methode verwendet
eine Hilfsmethode.
Implementieren Sie die beiden Algorithmen und vergleichen Sie die Laufzeiten bei der
Berechnung einiger Funktionswerte, zum Beispiel fib(45).
5.1 Einführung und Begriffe
5-23
Fibonacci-Folge
static long fib(int n) {
if (n==0||n==1) return n;
long[] a = new long[n+1];
a[0]= 0;
a[1]= 1;
for (int i=2; i<=n; i++) a[i]=-1;
return f(a, n);
}
static long f(long[] a, int n) {
if (a[n]>=0) return a[n];
return a[n]=f(a,n-1)+f(a,n-2);
}
5.1 Einführung und Begriffe
5-24
Algorithmus von Euklid
Rekursive Version:
(
a,
ggT(a, b) =
ggT(b, a mod b),
falls b = 0,
falls b =
6 0.
Auswertung:
ggT(36, 52) → ggT(52, 36) → ggT(36, 16) → ggT(16, 4) → ggT(4, 0) → 4
ggT(36, 52) = 4
5.1 Einführung und Begriffe
5-25
Algorithmus von Euklid
class Euklid {
static long ggt(long a, long b) {
if (b == 0)
return a;
else
return ggt(b, a % b);
}
public static void main(String[] args) {
System.out.println(ggt(36,52));
}
}
5.1 Einführung und Begriffe
5-26
Algorithmus von Euklid
class Euklid {
static long ggt(long a, long b) {
return b == 0 ? a : ggt(b, a % b); // nur 1 Zeile
}
public static void main(String[] args) {
System.out.println(ggt(36,52));
}
}
5.1 Einführung und Begriffe
5-27
Exponentiation
rekursiv:
(
1
n=0
bn =
b · b n−1 n ≥ 1
iterativ:
b n = b| · b {z
· ... · b}
n-mal
schnell (rekursiv):


n=0
1
2
n
n/2
b =
b
n ≥ 1, n gerade


b · b n−1 n ≥ 1, n ungerade
5.1 Einführung und Begriffe
5-28
Schnelle Exponentiation
long exp(long b, long n) {
if (n == 0)
return 1;
else if (n % 2 == 0) {
long l = exp(b, n / 2);
return l * l;
}
else
return b * exp(b, n - 1);
}
Es kann
if (n==1) return b;
Evtl. eine Multiplikation weniger.
5.1 Einführung und Begriffe
hinzugefügt werden.
5-29
Summe
Es soll die Summe der Zahlen von a bis b, d. h.
b
X
i=a+
i=a
Pb
b
X
i=a
i berechnet werden. Es gilt
i.
i=a+1
long sum(long a, long b) {
if (a == b)
return b;
else
return a + sum(a + 1, b);
}
5.1 Einführung und Begriffe
5-30
Skalarprodukt
Es soll das Skalarprodukt der Vektoren a und b rekursiv berechnet werden.
int skalar(int[] a, int[] b, int n) {
if (n < 0)
return 0;
else {
return a[n] * b[n] + skalar(a, b, n - 1);
}
}
5.1 Einführung und Begriffe
5-31
Skalarprodukt
int skalar(int[] a, int[] b) {
// eine ineffiziente Lösung
int l = a.length;
if (l == 0)
return 0;
else {
int[] aa = new int[l - 1];
int[] bb = new int[l - 1];
for (int i = 0; i <= l - 2; i++) {
aa[i] = a[i];
bb[i] = b[i];
}
return a[l - 1] * b[l - 1] + skalar(aa, bb);
}
}
5.1 Einführung und Begriffe
5-32
Semantik rekursiv definierter Funktionen 1
(∗)
f :N→N
(
0
n = 0,
f (n) =
f (n + 1) n > 0.
• Operationelle (algorithmische) Semantik/Ersetzungssystem:
f (0) = 0
f (1) = f (2) = f (3) = ...
• Die Auswertung von (∗) terminiert für n > 0 nicht. Daher ist D(f ) = {0} und
f (0) = 0.
5.1 Einführung und Begriffe
5-33
Semantik rekursiv definierter Funktionen 2
• Denotationale (mathematische) Semantik/Gleichungssystem: Wir fassen (∗) als
Gleichung für eine unbekannte Funktion f auf.
• Für eine partielle Funktion f bedeutet f (k) = f (l ), dass die Werte f (k) und f (l )
beide undefiniert oder aber definiert und gleich sind.
• Alle Funktionen f : N → N mit D(f ) = {0} und f (0) = 0 oder
D(f ) = {0, 1, 2, ...} = N und
f (0) = 0, f (1) = f (2) = f (3) = ... = a
mit a ∈ N erfüllen die Gleichung (∗). Die operationelle Lösung ist die Lösung mit
dem kleinsten Definitionsbereich. Die Lösung von (∗) ist also nicht eindeutig.
Mathematisch gibt es also unendlich viele Lösungen.
5.1 Einführung und Begriffe
5-34
Semantik rekursiv definierter Funktionen 3
a :N×N→N
Outermost:
Innermost:
Mixed:


m = 0,
n + 1
a(m, n) = a(m − 1, 1)
m=
6 0, n = 0,


a(m − 1, a(m, n − 1)) m =
6 0, n 6= 0.
a(1, 1) = a(0, a(1, 0)) = a(1, 0) + 1 = a(0, 1) + 1 = (1 + 1) + 1 = 3
a(1, 1) = a(0, a(1, 0)) = a(0, a(0, 1)) = a(0, 2) = 3
a(1, 1) = a(0, a(1, 0)) = a(0, a(0, 1)) = a(0, 1) + 1 = 3
a ist sog. die Ackermann-Funktion. Es gilt:
216
a(3, 3) = 61,
5.1 Einführung und Begriffe
a(4, 4) = 2
22
− 3.
5-35
Semantik rekursiv definierter Funktionen 4
f :N×N→N
Outermost:
Innermost:
(
1
m = 0,
f (m, n) =
f (m − 1, f (1, 0)) m =
6 0.
f (1, 0) = f (0, f (1, 0)) = 1
f (1, 0) = f (0, f (1, 0)) = f (0, f (0, f (1, 0))) = ...
(1, 0) ∈ D(f )
(1, 0) ∈
/ D(f )
Welche Semantik nehmen Programmiersprachen? Welche sollten sie nehmen?
5.1 Einführung und Begriffe
5-36
Rekursion/Funktionale Programmierung:
Beispiele rekursiver Methoden in Java
5.1 Einführung und Begriffe
5.2 Beispiele rekursiver Methoden in Java
5.3 Ein Blick auf funktionales Programmieren
5.2 Beispiele rekursiver Methoden in Java
5-37
Newton-Verfahren
Es soll rekursiv die Nullstelle der Funktion
80
60
f (x ) = x 3 + 3x + 5
y
40
20
mithilfe des Newton-Verfahrens berechnet
werden:
f 0(x ) = 3x 2 + 3
xn+1
f (xn )
= xn − 0
f (xn )
5.2 Beispiele rekursiver Methoden in Java
–4
–3
–2
–1
0
1
2
3
4
x
–20
–40
–60
–80
5-38
Newton-Verfahren
class Newton {
double eps;
static double f(double x) {
return x*x*x+3*x+5;
}
static double fs(double x) {
return 3*x*x+3;
}
Newton(double eps) {
this.eps = eps;
}
5.2 Beispiele rekursiver Methoden in Java
5-39
Newton-Verfahren
double newton(double x) {
System.out.println(x);
if (Math.abs(f(x)) <= eps)
return x;
else
return newton(x - f(x) / fs(x));
}
public static void main(String[] args) {
Newton n = new Newton(0.00001);
System.out.println(n.newton(1.0));
}
}
Alternativ: Newton als abstrakte Klasse: f, fs abstrakt, newton konkret.
5.2 Beispiele rekursiver Methoden in Java
5-40
Intervallschachtelung
Problem: Gegeben seien eine stetige reelle Funktion f und Intervallgrenzen
a, b ∈ R, a < b. Wir wollen annehmen, dass f (a) und f (b) unterschiedliches
Vorzeichen aufweisen. Nach dem Zwischenwertsatz besitzt f im Intervall a ≤ x ≤ b
dann wenigstens eine Nullstelle.
Es soll eine Java-Methode
double bisek(double a, double b, double eps)
programmiert werden, die a und b sowie eine Fehlerschranke eps als Eingaben
akzeptiert und eine Nullstelle von f im gegebenen Intervall als Ergebnis liefert.
5.2 Beispiele rekursiver Methoden in Java
5-41
Intervallschachtelung
Lösungsidee: Wir halbieren das Intervall schrittweise, sodass die Bedingung, dass f
an den Intervallgrenzen Werte mit unterschiedlichen Vorzeichen annimmt, erhalten
bleibt. Damit befindet sich nach jedem Schritt eine Nullstelle von f im aktuellen
Intervall. Das Verfahren bricht ab, wenn die Intervallgrenzen nahe genug beieinander
liegen.
5.2 Beispiele rekursiver Methoden in Java
5-42
Intervallschachtelung
static double bisek(double a, double b, double eps) {
double m = (a + b) * 0.5;
if (Math.abs(a - b) < eps)
return m;
else if (f(a) * f(m) > 0)
a = m;
else
b = m;
return bisek(a, b, eps);
}
5.2 Beispiele rekursiver Methoden in Java
5-43
Rekursionen
• Rekursive Problemlösungen dienen in erster Linie der Einsicht in das Problem.
Häufig ist jedoch die rekursive Formulierung auch effizient (z. B. Quicksort) oder
eine iterative Formulierung nur schwierig zu finden.
• Wir geben jetzt drei Beispiele – einen Geldwechselalgorithmus, die „Türme von
Hanoi“ sowie das Acht-Damen-Problem – an, für die rekursive Lösungen auf der
Hand liegen, iterative aber nur schwer zu finden sind.
5.2 Beispiele rekursiver Methoden in Java
5-44
Wechseln eines Geldbetrags
Es soll die Anzahl der Möglichkeiten, ein 10-Cent-Stück in 1-, 2- oder 5-Cent-Stücke
zu wechseln, berechnet werden. Die gesuchte Anzahl beträgt 10:
•
•
•
•
•
•
•
•
•
•
5+5
5+2+2+1
5+2+1+1+1
5+1+1+1+1+1
2+2+2+2+2
2+2+2+2+1+1
2+2+2+1+1+1+1
2+2+1+1+1+1+1+1
2+1+1+1+1+1+1+1+1
1+1+1+1+1+1+1+1+1+1
5.2 Beispiele rekursiver Methoden in Java
5-45
Wechseln eines Geldbetrags
Problem: Es gibt k ≥ 1 verschiedene Münzen mit den Beträgen b0, b1, ... , bk−1 mit
0 < b0 < b1 < ... < bk−1. Auf wie viele Arten kann der Betrag a gewechselt werden?
Idee: Wir verwenden einen rekursiven Algorithmus. Wenn die größte Münze nicht
verwendet wird, ist die gesuchte Anzahl gleich der Anzahl der Möglichkeiten, den
Betrag mit den k − 1 anderen Münzen zu wechseln. Wenn die größte Münze benutzt
wird, ist die gesuchte Zahl gleich der Anzahl der Möglichkeiten, a − bk−1 mit allen k
Münzen zu wechseln.
Beobachtung: Bei jedem Schritt wird entweder die Anzahl der verwendeten
Münzsorten oder aber der zu wechselnde Betrag kleiner.
5.2 Beispiele rekursiver Methoden in Java
5-46
Wechseln eines Geldbetrags
Als Funktion erhalten wir:


a < 0 oder k = 0,
0,
f (a, k) = 1,
a = 0,


f (a, k − 1) + f (a − bk−1, k), sonst.
5.2 Beispiele rekursiver Methoden in Java
5-47
Wechseln eines Geldbetrags
class Wechsel {
static int[] b = {1,2,5,10,20,50,100,200};
static int wechsel(int a, int k) {
if ((a < 0) || (k == 0))
return 0;
else if (a == 0)
return 1;
else
return wechsel(a, k - 1) + wechsel(a - b[k - 1], k);
}
public static void main(String[] args) {
System.out.println(wechsel(10,3));
}
}
5.2 Beispiele rekursiver Methoden in Java
5-48
Türme von Hanoi
Problem: Gegeben seien n Scheiben unterschiedlichen Durchmessers, die der Größe
nach geordnet zu einem Turm geschichtet sind. Die größte Scheibe liegt dabei unten.
Der Turm steht auf Platz 1. Unter Verwendung des Hilfsplatzes 3 soll der Turm auf
Platz 2 transportiert werden.
Beim Transport sind die folgenden Bedingungen einzuhalten:
• In jedem Schritt darf nur eine Scheibe – und zwar die oberste eines Turms –
bewegt werden.
• Zu keinem Zeitpunkt darf eine größere Scheibe auf einer kleineren liegen.
5.2 Beispiele rekursiver Methoden in Java
5-49
Türme von Hanoi
Beispiel: n = 3
Wir führen die folgenden Schritte aus:
Von
Von
Von
Von
Von
Von
Von
1
1
2
1
3
3
1
nach
nach
nach
nach
nach
nach
nach
2.
3.
3.
2.
1.
2.
2.
5.2 Beispiele rekursiver Methoden in Java
5-50
Türme von Hanoi
Lösungsidee: Wir benutzen den folgenden rekursiven Algorithmus:
• Transportiere n − 1 Scheiben von 1 nach 3.
• Transportiere die verbliebene Scheibe von 1 nach 2.
• Transportiere n − 1 Scheiben von 3 nach 2.
5.2 Beispiele rekursiver Methoden in Java
5-51
Türme von Hanoi
class Hanoi {
static void hanoi(int n, int a, int z) {
if (n > 1)
hanoi(n - 1, a, 6 - (a + z));
System.out.println("Von " + a + " nach " + z + ".");
if (n > 1)
hanoi(n - 1, 6 - (a + z), z);
}
public static void main(String[] args) {
hanoi(3,1,2);
}
}
5.2 Beispiele rekursiver Methoden in Java
5-52
Das Acht-Damen-Problem
Problemstellung: Es sind acht Damen so auf einem Schachbrett aufzustellen, dass
sie sich gegenseitig nicht bedrohen.
Beispiel:
|D| | | | | | | |
| | | | | | |D| |
| | | | |D| | | |
| | | | | | | |D|
| |D| | | | | | |
| | | |D| | | | |
| | | | | |D| | |
| | |D| | | | | |
5.2 Beispiele rekursiver Methoden in Java
5-53
Das Acht-Damen-Problem
Lösungsidee:
• Das Spielfeld wird als eindimensionales Feld der Länge 8 gespeichert. Es gilt
brett[j] == i genau dann, wenn sich in Zeile i und Spalte j eine Dame
befindet.
• Wir schreiben drei Methoden:
◦ ausgabe gibt eine gefundene Lösung auf dem Bildschirm aus.
◦ bedroht überprüft, ob die zuletzt gesetzte Dame von einer bereits vorher
platzierten Dame geschlagen werden kann.
◦ setze versucht, eine Dame in die nächste Spalte zu stellen. Diese Methode
arbeitet rekursiv von links nach rechts.
• Verfahren des Algorithmus: Backtracking
5.2 Beispiele rekursiver Methoden in Java
5-54
Das Acht-Damen-Problem
/** Gibt das Schachbrett auf dem Bildschirm aus. */
public static void ausgabe(int[] brett) {
System.out.println();
// Leerzeile vorher
for (int i=0; i < 8; i++) {
// Anzahl der Zeilen
for (int j=0; j < 8; j++)
// Anzahl der Spalten
System.out.print("|" + ((i == brett[j]) ? ’D’ : ’ ’));
System.out.println("|");
// Zeilenende
}
System.out.println();
// Leerzeile hinterher
}
Die Methode ausgabe verwendet den Fragezeichen-Operator.
5.2 Beispiele rekursiver Methoden in Java
5-55
Das Acht-Damen-Problem
/** Testet, ob die Dame in "spalte" von einer
anderen geschlagen werden kann. */
public static boolean bedroht(int[] brett, int spalte) {
// Teste zuerst, ob eine Dame in derselben Zeile steht.
for (int i=0; i < spalte; i++)
if (brett[i] == brett[spalte])
return true;
// Teste dann, ob in der oberen Diagonale eine Dame steht.
for (int i = spalte-1, j = brett[spalte]-1; i >= 0; i--,j--)
if (brett[i] == j)
return true;
5.2 Beispiele rekursiver Methoden in Java
5-56
Das Acht-Damen-Problem
// Teste danach, ob in der unteren Diagonale eine Dame steht.
for (int i = spalte-1, j = brett[spalte]+1; i >= 0; i--,j++)
if (brett[i] == j)
return true;
// Wenn das Programm hier ist, steht die Dame "frei".
return false;
}
Die beiden letzten for-Anweisungen besitzen Initialisierungs- und Update-Listen.
5.2 Beispiele rekursiver Methoden in Java
5-57
Das Acht-Damen-Problem
/** Sucht rekursiv eine Lösung des Problems. */
public static boolean setze(int[] brett, int spalte) {
// Sind wir fertig?
if (spalte == 8) {
ausgabe(brett);
return true;
}
5.2 Beispiele rekursiver Methoden in Java
5-58
Das Acht-Damen-Problem
// Suche die richtige Position für die neue Dame.
for (int i=0; i < 8; i++) {
brett[spalte] = i;
// Probiere jede Stelle aus.
if (bedroht(brett,spalte)) // Falls Dame nicht frei steht,
continue;
// versuche die nächste Stelle.
boolean success = setze(brett,spalte+1);
if (success)
// <------------return true;
// <------------}
// Wenn das Programm hier angekommen ist,
// stecken wir in einer Sackgasse.
return false;
}
5.2 Beispiele rekursiver Methoden in Java
5-59
Das Acht-Damen-Problem
/** Initialisiert das Schachbrett und
ruft die Methode "setze" auf. */
public static void main(String[] args) {
int[] feld = {0,0,0,0,0,0,0,0}; // Initialisiere Spielfeld.
setze(feld,0);
// Starte am linken Rand.
}
5.2 Beispiele rekursiver Methoden in Java
5-60
Das Acht-Damen-Problem
• Das obige Programm bricht ab, sobald es eine Lösung gefunden hat.
• Wenn alle Lösungen berechnet werden sollen, müssen lediglich die beiden durch
<------------gekennzeichneten Zeilen aus der Methode setze entfernt werden. Das Programm
gibt dann 92 Stellungen aus, von denen sich aber etliche nur durch Drehungen
oder Spiegelungen unterscheiden.
5.2 Beispiele rekursiver Methoden in Java
5-61
Sortieren
• Wir sehen uns jetzt den Algorithmus „Quicksort“ als Beispiel für einen effizienten
rekursiven Algorithmus an.
• Zur Einführung wiederholen wir zunächst kurz das Bubblesort-Verfahren.
• Beide Algorithmen und ihre Eigenschaften werden in der Vorlesung „Algorithmen
und Datenstrukturen“ ausführlich besprochen.
5.2 Beispiele rekursiver Methoden in Java
5-62
Bubblesort
Comparable[] bubbleSort(Comparable[] objs) {
boolean sorted;
do {
sorted = true;
for (int i = 0; i < objs.length-1; ++i) {
if (objs[i].compareTo(objs[i + 1]) > 0) {
Comparable tmp = objs[i];
objs[i] = objs[i + 1];
objs[i + 1] = tmp;
sorted = false;
}
}
} while (!sorted);
return objs;
}
Wie schon gesehen: Comparable<T>
5.2 Beispiele rekursiver Methoden in Java
5-63
Quicksort
Problem: Gegeben ist ein Feld objs, dessen Elemente paarweise vergleichbar sind.
Das Teilfeld objs[l] .. objs[r] ist zu sortieren.
Grundidee des Algorithmus:
• Suche ein Pivotelement, z. B. objs[k] mit k =
l+r
2 .
• Teile das Feld, sodass alle Elemente des linken Felds kleiner als das Pivotelement
und alle Elemente des rechten Felds größer oder gleich dem Pivotelement sind.
• Wende das Verfahren rekursiv auf die beiden Teilfelder an.
5.2 Beispiele rekursiver Methoden in Java
5-64
Quicksort
static Comparable[] quickSort(Comparable[] objs, int l, int r) {
if (l < r) {
int i = l,
j = r,
k = (int) ((l + r) / 2);
Comparable pivot = objs[k];
5.2 Beispiele rekursiver Methoden in Java
5-65
Quicksort
do {
while (objs[i].compareTo(pivot) < 0) i++;
while (pivot.compareTo(objs[j]) < 0) j--;
if (i <= j) {
Comparable t = objs[i];
objs[i] = objs[j];
objs[j] = t;
i++;
j--;
}
} while (i < j);
objs = quickSort(objs, l, j);
objs = quickSort(objs, i, r);
}
return objs; //Übungsaufgabe: Formulierung ohne Rückgabetyp.
}
5.2 Beispiele rekursiver Methoden in Java
5-66
Wrapper-Klassen
Problem:
• Primitive Datentypen sind keine Referenztypen.
• Bubblesort und Quicksort können daher in der obigen Form nicht zum Sortieren
von Zahlen verwendet werden.
Lösung:
Man verwende Wrapper-Klassen!
Später: Generics
5.2 Beispiele rekursiver Methoden in Java
5-67
Wrapper-Klassen
• Eine Wrapper-Klasse kapselt einen primitiven Wert in einer objektorientierten Hülle
und stellt Methoden zum Zugriff auf diesen zur Verfügung. Der Wert kann nicht
verändert werden.
• Wrapper-Klassen ermöglichen es, primitive Datentypen und Referenztypen
einheitlich zu behandeln.
• Zu jedem primitiven Datentyp existiert eine Wrapper-Klasse:
Boolean, Character, Byte, Short, Integer, Long, Float, Double, Void.
5.2 Beispiele rekursiver Methoden in Java
5-68
Wrapper-Klassen
• Konstruktor mit dem jeweiligen Typ als Parameter:
Integer(int i)
Float(float f)
...
• Konstruktor mit einer Zeichenkette als Parameter:
Integer(String s)
Float(String s)
...
5.2 Beispiele rekursiver Methoden in Java
5-69
Wrapper-Klassen
• Einige Methoden der Klasse Integer:
int intValue()
int String toString()
int compareTo(Integer anotherInteger)
static int compareUnsigned(int x, int y)
static int parseInt(String s)
static int parseInt(String s, int radix)
• Einige Konstanten der Klasse Float:
MIN_VALUE
MAX_VALUE
NaN
POSITIVE_INFINITY
5.2 Beispiele rekursiver Methoden in Java
5-70
Quicksort
int n = ...;
Integer[] a = new Integer[n];
a[0]
a[1]
a[2]
a[3]
...
=
=
=
=
new
new
new
new
Integer(5);
Integer(-5);
Integer(0);
Integer(1);
Sort.quickSort(a,0,a.length-1);
5.2 Beispiele rekursiver Methoden in Java
5-71
Eine Schnittstelle zum Sortieren
interface Sortieren {
Comparable[] sort(Comparable[] objs);
}
class BubbleSort
implements Sortieren {
Comparable[] feld;
...
}
class QuickSort
implements Sortieren {
Comparable[] feld;
...
}
5.2 Beispiele rekursiver Methoden in Java
5-72
Autoboxing und Unboxing 1
• Um Werte der primitiven Datentypen als Objekte zu behandeln, müssen sie in
Wrapper-Klassen eingewickelt werden.
• Um z. B. einen int-Wert als Objekt behandeln zu können, muss ein
Integer-Objekt erzeugt werden:
Integer iWrapper = new Integer(4500)
• Um den eingewickelten Wert auszulesen, muss eine entsprechende Methode der
Wrapper-Klasse aufgerufen werden:
int iValue = iWrapper.intValue()
• Seit der Version 5 von Java gibt es das Autoboxing und das Unboxing.
5.2 Beispiele rekursiver Methoden in Java
5-73
Autoboxing und Unboxing 2
• Beispiel:
Object obj = 4500;
int i = (Integer) obj;
// Autoboxing
// Unboxing
• Autoboxing und Unboxing gibt es für alle Wrapper-Klassen:
Object obj = 3.14;
double d = (Double) obj;
5.2 Beispiele rekursiver Methoden in Java
// Autoboxing
// Unboxing
5-74
Autoboxing und Unboxing 3
int n = ...;
Integer[] a = new Integer[n];
a[0]
a[1]
a[2]
a[3]
...
=
=
=
=
5;
-5;
0;
1;
//
//
//
//
Autoboxing
Autoboxing
Autoboxing
Autoboxing
Sort.quickSort(a,0,a.length-1);
int s = 0;
for (int i = 0; i < n; ++i)
s += a[i];
// Unboxing
5.2 Beispiele rekursiver Methoden in Java
5-75
Rekursion/Funktionale Programmierung:
Ein Blick auf funktionales Programmieren
5.1 Einführung und Begriffe
5.2 Beispiele rekursiver Methoden in Java
5.3 Ein Blick auf funktionales Programmieren
5.3 Ein Blick auf funktionales Programmieren
5-76
Funktionale Programmierung
Turing Award 1977:
John Backus: Can Programming Be Liberated from the von Neumann Style?
A Functional Style and its Algebra of Programs.
5.3 Ein Blick auf funktionales Programmieren
5-77
Funktionale Programmierung
Wie schon mehrfach erwähnt, macht Java 8 Schritte in die funktionale
Programmierung.
Aspekte hierzu sind:
• Lambda-Ausdrücke,
• Funktionale Interfaces,
• Interfaces mit Default-Methoden und statischen Methoden.
Ein weiterer wichtiger Aspekt von Java 8 ist:
• Streams und Pipeline-Operationen.
Ein erstes Beispiel für Java 8 haben wir in der Einführung gesehen.
Weitere Beispiele werden wir noch sehen.
5.3 Ein Blick auf funktionales Programmieren
5-78
Funktionen höherer Ordnung
Funktionen können selbst Argumente oder Werte sein. In diesem Fall spricht man von
Funktionen höherer Ordnung oder Funktionalen.
f : (A1 → A2) × A3 → B
g : A → (B1 → B2)
h : (A1 → A2) → (B1 → B2)
5.3 Ein Blick auf funktionales Programmieren
5-79
Funktionen höherer Ordnung
Beispiele:
• Summe:
b
X
f (i)
i=a
• Komposition von Funktionen:
• Auswahl zwischen Funktionen:
• Bestimmtes Integral:
Z
f ◦g
if p then f else g fi
b
f (x ) dx
a
5.3 Ein Blick auf funktionales Programmieren
5-80
Funktionale Algorithmen
Ein Algorithmus heißt funktional, wenn die Berechnungsvorschrift mittels einer
Sammlung von Funktionen definiert wird. Die Funktionsdefinitionen dürfen
insbesondere Rekursionen und Funktionen höherer Ordnung enthalten.
5.3 Ein Blick auf funktionales Programmieren
5-81
Funktionale Algorithmen
Beispiel:
f (0) = 0
f (1) = 1
f (n) = nf (n − 2)
Wenn wir als Datenbereich die Menge der ganzen Zahlen zugrundelegen, berechnet
dieser Algorithmus die Funktion f : Z → Z mit Df = N und
f (n) =


0





n gerade
n−1
2

Y



(2i + 1) n ungerade


i=0
5.3 Ein Blick auf funktionales Programmieren
5-82
Funktionale Programmiersprachen
Programmiersprachen, die in erster Linie für die Formulierung funktionaler
Algorithmen gedacht sind, heißen funktional. Funktionale Programmiersprachen sind
beispielsweise
• Lisp, ML, SML,
• Scheme,
• Scala (Erweiterung von Java) und
• Haskell.
Wie schon gesehen: Java 8 ging auch etwas in die Richtung Funktionalität.
Man kann in vielen imperativen Programmiersprachen funktional programmieren –
und umgekehrt! (hybrid)
5.3 Ein Blick auf funktionales Programmieren
5-83
Lisp und Scheme
• Lisp wurde Ende der 50er Jahre von John McCarthy entwickelt.
• Im Laufe der Jahre wurden viele Lisp-Dialekte, u. a. Common Lisp und Scheme
definiert.
• Die erste Version von Scheme stammt aus dem Jahre 1975. Autoren waren Guy
Lewis Steele Jr. und Gerald Jay Sussman.
• Lisp und Scheme werden in der Regel interpretiert, nicht compiliert.
5.3 Ein Blick auf funktionales Programmieren
5-84
Algorithmus von Euklid
Funktional geschrieben hat der
Algorithmus von Euklid
die Form:
ggT(a, 0) = a
ggT(a, b) = ggT(b, a mod b)
5.3 Ein Blick auf funktionales Programmieren
5-85
Algorithmus von Euklid
Auswertung:
ggT(36, 52) → ggT(52, 36) → ggT(36, 16) → ggT(16, 4) → ggT(4, 0) → 4
ggT(36, 52) = 4
5.3 Ein Blick auf funktionales Programmieren
5-86
Scheme: Algorithmus von Euklid
Der funktionale Algorithmus von Euklid lautet als Scheme-Programm:
(define (ggT a b)
(if (= b 0)
a
(ggT b (remainder a b))))
(ggT 36 52)
4
5.3 Ein Blick auf funktionales Programmieren
5-87
Haskell
• Haskell ist eine funktionale Programmiersprache.
• Haskell Brooks Curry (1900–1982) war ein amerikanischer Mathematiker und
Logiker.
• Erste Überlegungen zur Programmiersprache Haskell stammen aus dem Jahr 1987.
Die Version Haskell 98 wurde 2003 veröffentlicht. Die aktuelle Version Haskell’
(Haskell Prime, Haskell 2010) stammt vom Dezember 2009.
• Für Haskell existieren freie Compiler und Interpreter (s. http://haskell.org).
• Es gibt etliche sog. Sprachderivate.
Beispiele: Parallel Haskell, Haskell++ (objektorientiert), ...
Haskell++ ist eine kleine Erweiterung zur Objektorientierung.
5.3 Ein Blick auf funktionales Programmieren
5-88
Haskell: Algorithmus von Euklid
Der funktionale Algorithmus von Euklid lautet als Haskell-Programm:
ggT :: Integer -> Integer -> Integer
ggT a b | b == 0 = a
| otherwise = ggT b (a ‘mod‘ b)
ggT 36 52
4
5.3 Ein Blick auf funktionales Programmieren
5-89
Haskell: Quicksort
Der Quicksort-Algorithmus lautet als Haskell-Programm (Version 1):
qsort [] = []
qsort (x:xs) = qsort smaller ++ [x] ++ qsort larger
where
smaller = [a | a<-xs, a<=x]
larger = [a | a<-xs, a>x]
Nur 5 Zeilen. Geht es noch kürzer?
5.3 Ein Blick auf funktionales Programmieren
5-90
Haskell: Quicksort
Der Quicksort-Algorithmus lautet als Haskell-Programm (Version 2):
qs [] = []
qs (x:xs) = qs (filter (< x) xs) ++ [x] ++ qs (filter (>= x) xs)
Im Modul „Programmieren für Fortgeschrittene“ erfahren Sie mehr zu diesem
Paradigma und können die Sprache „Haskell“ erlernen.
5.3 Ein Blick auf funktionales Programmieren
5-91
Wichtige Aspekte der Rekursion
• Begriff der Rekursion
• Varianten der Rekursion
• Semantik der Rekursion
• Funktion höherer Ordnung
• Komplexität von rekursiven Algorithmen
• Methoden zum Entwurf von Algorithmen
• Funktionales Paradigma
• Currying, Lambda-Ausdrücke, . . .
Es gibt weitere Aspekte (s. Programmieren für Fortgeschrittene).
5.3 Ein Blick auf funktionales Programmieren
5-92
Lambda-Ausdrücke und funktionale Interfaces
Ein Funktionales Interface ist ein normales Interface, das aber nur eine einzige
abstrakte Methode enthält.
Grundlage der Lambda-Ausdrücke sind funktionale Interfaces. Lambda-Ausdrücke
bestehen aus einer Parameterliste und dem Rumpf einer Methode, die durch ->
verbunden sind.
So können die Lambda-Ausdrücke aussehen:
(Liste der Parameter) -> { Befehl; ...; Befehl; }
(Liste der Parameter) -> Ausdruck;
Die Liste der Parameter kann evtl. auch leer sein.
5.3 Ein Blick auf funktionales Programmieren
5-93
Lambda-Ausdrücke und funktionale Interfaces: Beispiel
interface Funktion {
double wurzel (double y);
}
public class Lambda {
public static void main(String[] args) {
Funktion f = (y) -> Math.sqrt(y);
// Lambda-Ausdruck
for (double x=0.0; x<=5.0; x++) {
double r = f.wurzel(x);
System.out.printf("Wurzel von %4.2f: %6.5f%n",x,r);
}
}
}
5.3 Ein Blick auf funktionales Programmieren
5-94
Ausgabe:
Wurzel
Wurzel
Wurzel
Wurzel
Wurzel
Wurzel
von
von
von
von
von
von
0.00:
1.00:
2.00:
3.00:
4.00:
5.00:
0.00000
1.00000
1.41421
1.73205
2.00000
2.23607
5.3 Ein Blick auf funktionales Programmieren
5-95
Interfaces mit Default- und statischen Methoden
Eine Default-Methode ermöglicht es, eine Standardimplementierung in einem Interface
zu definieren. Diese Methode muss das Schlüsselwort default bekommen.
Eine Standardimplementierung einer Interface-Methode kann auch eine static
Methode werden. Diese Methode kann auch schon dann verwendet werden, wenn es
noch kein Objekt der Klasse, die das Interface implementiert, gibt.
5.3 Ein Blick auf funktionales Programmieren
5-96
Interface mit Default-Methode: Beispiel
interface Funktion {
double wurzel (double y);
default double quadrat (double x) {return x*x;}
}
public class Formel implements Funktion {
public double wurzel (double y) {return Math.sqrt(y);}
public static void main(String[] args) {
Formel f = new Formel();
for (double x=0.0; x<=5.0; x++) {
double r = f.wurzel(x);
System.out.printf("Wurzel von %4.2f: %6.5f%n",x,r);
r = f.quadrat(x);
System.out.printf("Quadrat von %4.2f: %6.5f%n",x,r);
}
}
}
5.3 Ein Blick auf funktionales Programmieren
5-97
Interface mit statischer Methode: Beispiel
interface Funktion {
double wurzel (double y);
static double hochdrei (double x) {return x*x*x;}
}
public class Test implements Funktion {
public double wurzel (double y) {return Math.sqrt(y);}
public static void main(String[] args) {
for (double x=0.0; x<=5.0; x++) {
double r = Funktion.hochdrei(x);
System.out.printf("Hochdrei von %4.2f: %8.3f%n",x,r);
// Für die statische Methode muss es kein Objekt geben!
}
}
}
5.3 Ein Blick auf funktionales Programmieren
5-98
Funktionale Programmierung
Wie angekündigt haben wir also aus Java 8 Beispiele für
• Lambda-Ausdrücke,
• Funktionale Interfaces und
• Interfaces mit Default-Methoden und statischen Methoden
gesehen. Den Aspekt
• Streams und Pipeline-Operationen
werden wir später sehen.
5.3 Ein Blick auf funktionales Programmieren
5-99
Herunterladen