2.4 Rekursion versus Iteration Beispiel Fakultätsfunktion (factorial): Spezifikation (= applikatives Programm): fact n = if n<2 then 1 else n*fact(n-1) imperativ: int fact(int n){ return n<2? 1 : n*fact(n-1); und iterativ: int fact(int n){ int result = 1; while(n>1) result *= n--; return result; 2.4 1
iterativ: rekursiv: schneller - denn Unterprogrammaufrufe kosten Zeit; schöner, einfacher, naheliegender, besser? int pos(int x) { // position of x in a[] for(int i=0; ; i++) if(x==a[i]) return i; int pos(int x, int i) { //... at i or beyond return x==a[i]? i : pos(x,i+1); Was ist besser? 2.4 2
int position(int unten, int oben) { // Einschachteln, // rekursiv, aus 2.3.1 int zeiger = (unten+oben)/2; if (a[zeiger]< x) return position(zeiger,oben); else if(a[zeiger]==x) return zeiger; else /* a[zeiger]> x */ return position(unten,zeiger); int position(int unten, int oben) { // iterativ, aus 2.1.3 for(;;){ int zeiger = (unten+oben)/2; if (a[zeiger]< x) unten = zeiger; else if(a[zeiger]==x) return zeiger; else /* a[zeiger]> x */ oben = zeiger;? Systematische Umwandlung Iteration Rekursion? 2.4 3
Ein anderes Beispiel stimmt skeptisch: reverse[] = [] reverse(x:xs) = reverse xs ++ [x] String reverse(string s) { // poor efficiency! return s.length()==0? "" : reverse(s.substring(1)) + s.charat(0); String reverse(string s) { // good efficiency! char[] a = s.tochararray(); for(int i=0; i<a.length/2 ; i++) { char x = a[i]; a[i] = a[a.length-1-i]; a[a.length-1-i] = x; return new String(a); 2.4 4
Beispiel für einfache Umwandlungsregel: der Effekt von while(c){ S kann auch erzielt werden durch Aufruf von void whilecs() { if(c){s; whilecs(); Beachte: Beim rekursiven Abstieg wird S wiederholt, beim rekursiven Aufstieg passiert nichts mehr! void whilecs(){ while(c){ S void whilecs() { if(c){s; whilecs(); 2.4 5
... und verallgemeinert: void loop() { // iterative for(;;) { S1 if(c1) return; S2 if(c2) return;... void loop() { // recursive S1 if(c1) return; S2 if(c2) return;... loop(); 2.4 6
Entrekursivierung ist eine Programmtransformation, die eine rekursive Prozedur in eine iterative Prozedur umwandelt, ist von Bedeutung, weil: iterativ braucht weniger Zeit und Speicher, Spezifikation ist häufig rekursiv formuliert. Systematische Verfahren? 2.4 7
2.4.1 Entrekursivierung endrekursiver Prozeduren Zur Erinnerung: Ein Prozeduraufruf erzeugt eine Inkarnation (activation, instance) der Prozedur, die beim Rücksprung beendet wird (und verloren geht). Ein rekursiver Aufruf heißt schlicht, wenn unmittelbar nach Beendigung der erzeugten Inkarnation auch die erzeugende Inkarnation beendet wird. 2.4 8
Verschiedene Rekursionsarten, je nach dem möglichen (dynamischen) Verhalten einer Inkarnation: nichtlineare Rekursion: dynamisch mehr als ein rekursiver Aufruf, z.b. fibonacci hanoi(1.4.3) qsort(2.3.2) lineare Rekursion: dynamisch höchstens ein rekursiver Aufruf, z.b. loop position pos gcd(1.4.3) fact reverse Endrekursion: alle rekursiven Aufrufe sind schlicht, z.b. loop position pos gcd(1.4.3) Endrekursion ist am einfachsten, auch für die Entrekursivierung: 2.4 9
2.4.1.1... ohne Parameter T proc() {locals... return E; // recursive... return proc();... return E;... return proc();... T proc() {locals loop: for(;;) {... return E; // iterative... continue loop;... return E;... continue loop;... Beachte: nichtlokale Variable unproblematisch lokale Variable unproblematisch (bei Entrekursivierung - nicht in umgekehrter Richtung!) Bei T=void muss implizites return explizit gemacht werden! 2.4 10
2.4.1.2... mit Parametern T proc(a a) {locals... return G(a); // recursive... return proc(f(a));... T proc(a a) {locals loop: for(;;) {... return G(a); // iterative... a = F(a); continue loop;... (lokale und nichtlokale Variable auch hier unproblematisch) 2.4 11
Beispiel 1: Euklidischer Algorithmus (1.3.4, 1.4.3 ): static int gcd(int a, int b) { if(b==0) return a; else return gcd(b,a%b); static int gcd(int a, int b) { loop: for(;;) { if(b==0) return a; else {// (a,b) = (b,a%b); int r = a%b; a = b; b = r; continue loop; Vergleiche dies mit 1.3.4, S. 29! 2.4 12
Beispiel 2: Einschachtelungsverfahren: int position(int unten, int oben) { int zeiger = (unten+oben)/2; if (a[zeiger]< x) return position(zeiger,oben); else if(a[zeiger]==x) return zeiger; else /* a[zeiger]> x */ return position(unten,zeiger); int position(int unten, int oben) { loop: for(;;) { int zeiger = (unten+oben)/2; if (a[zeiger]< x) unten = zeiger; // oben = oben; else if(a[zeiger]==x) return zeiger; else /* a[zeiger]> x */ oben = zeiger; // unten=unten; (continue loop; bereits gestrichen, vgl. S. 3) 2.4 13
Beispiel 3: mit Objektaufrufen class Spelling { // linked list of Strings String word; Spelling next; Spelling(String w, Spelling n) { word = w; next = n; void insert(string w) { if(next==null next.word.compareto(w)>0) next = new Spelling(w,next); else if(next.word.equals(w)); /* ignore */ else next.insert(w); boolean check(string w) { if(word.equals(w)) return true; else if(next==null) return false; else return next.check(w); 2.4 14
Modifizierte Technik: Zielobjekt des Operationsaufrufs als zusätzlichen Parameter betrachten boolean check(string w) { // check(x,w) Spelling x = this; // simulate parameter x if(x.word.equals(w)) return true; else if(x.next==null) return false; else return x.next.check(w); // check(x.next,w) boolean checkiter(string w) { // iterative version Spelling x = this; loop: for(;;) if(x.word.equals(w)) return true; else if(x.next==null) return false; else x = x.next; // w = w; // continue loop; 2.4 15
2.4.2 Nicht endrekursive Prozeduren Einbettung (embedding) erlaubt Entrekursivierung bei linearer Rekursion: finde zu der gegebenen Prozedur p0 eine verallgemeinerte Version p1, die endrekursiv ist und die Leistung der gegebenen Prozedur umfasst; ersetze den Rumpf von p0 durch einem Rumpf, der aus einem Aufruf von p1 besteht und bestätige dadurch die Verwendbarkeit von p1; ersetze den Rumpf von p1 durch eine iterative Version und erhalte dadurch eine gleichnamige Version p2; ersetze in p0 den Aufruf von p1 durch den Rumpf von p2. 2.4 16
Beispiel 1: static int fact(int n) { if(n<2) return 1; else return n*fact(n-1); static int fact(int n, int acc) { // using accumulator if(n<2) return acc; else return f(n-1,n*acc); static int fact(int n) { return fact(n,1); 2.4 17
static int fact(int n, int acc) { // using accumulator for(;;) if(n<2) return acc; else acc *= n--;// (n,acc)=(n-1,n*acc) Vereinfachung: while(n>1) acc *= n--; return acc; static int fact(int n) { int acc = 1; // einbettender Parameter wird lokale Variable while(n>1) acc *= n--; return acc; Endversion - vgl. S. 1! 2.4 18
String reverse(string s) { Beispiel 2 (vgl. S. 4) return s.length()==0? "" : reverse(s.substring(1)) + s.charat(0); Einbettung: String reverse(string s, String acc) { if(s.length()==0) return acc; else return reverse(s.substring(1),s.charat(0)+acc);... benutzt in neuer Version: String reverse1(string s) { return reverse(s,""); Einbettung iterativ: String reverseiter(string s, String acc) { for(;;) if(s.length()==0) return acc; else{acc = s.charat(0)+acc; s = s.substring(1); Neue Version mit vereinfachter iterativer Einbettung: String reverseiter(string s) { String acc = ""; while(s.length()!=0) { acc = s.charat(0)+acc; s = s.substring(1); return acc;... bringt aber kaum Effizienzgewinn.
Nichtlineare Rekursion ( hanoi, qsort,...): Umwandlung in Iteration erfordert weitergehende Hilfsmittel ( ALP III) 2.4 20
2.4.3 Umwandlung von Schleifen in rekursive Funktionen Gegeben: eine Schleife Gesucht: eine Haskell-Funktion Feststellung: Dies ist prinzipiell einfacher als Entrekursivierung, weil Rekursion ausdrucksstärker als Iteration ist. Zur Erinnerung (S. 5): der Effekt von while(c){ S kann auch erzielt werden durch Aufruf von void whilecs() { if(c){s; whilecs(); Zu lösen bleibt: imperativer Zustand funktionale Argumente 2.4 21
Allgemeine Form der while-schleife (S. 5) über dem Zustand z eines Programms: void whilecf() { while( C(z) ) z = F(z); void whilecf() { if( C(z) ) { z = F(z); whilecf(); Argumente statt nichtlokaler Variablen: T whilecf(t z) { if(c(z)) return whilecf(f(z)); else return z; D.h. Effekt der Schleife wird durch z = whilecf(z) erzielt. 2.4 22
Direkte Umformulierung nach Haskell liefert whilecf z C z = whilecf(f z) otherwise = z Resümee: void whilecf() { while( C(z) ) z = F(z); whilecf z C z = whilecf(f z) otherwise = z 2.4 23
C und F zu Argumenten machen! Allgemein verwendbares Funktional als Entwurfsmuster (2.3 ): while :: (t->bool) -> (t->t) -> t -> t while cond func init cond init = while cond func (func init) otherwise = init 2.4 24
Beispiel: Approximation der Quadratwurzel x = a mit Newton-Verfahren: x i+1 = (x i + a/x i ) / 2 (z.b. mit x 0 = 1) while(math.abs(x*x-a) > eps) x = (x+a/x)/2; Zustand: z = (a,x,eps), dabei a und eps konstant. Bedingung: inaccurate a x eps = abs(x*x-a) > eps Änderung: improve a x eps = (a, (x+a/x)/2, eps)... und damit sqrt = while inaccurate improve... und damit z.b. sqrt 2 1 0.0001 (2, 1.414..., 0.0001) 2.4 25