Programmieren in C Rekursive Funktionen Prof. Dr. Nikolaus Wulff
Rekursive Funktionen Jede C Funktion besitzt ihren eigenen lokalen Satz an Variablen. Dies bietet ganze neue Möglichkeiten Funktionen zu implementieren, die bis lang noch nicht betrachtet wurden. Ein Funktion kann sich selbst rekursiv aufrufen! Dies ist ein mächtiges Konzept der Sprache C, das nicht in jeder Programmiersprache möglich ist. Fortran oder Cobol kennen dies nicht... Prof. Dr. Nikolaus Wulff Programmieren in C 2
Verkettung von Funktionen Rekursion möglicht eine ganz neue Klasse von Algorithmen, diese gehen über die bis lang betrachteten einfachen Schleifen hinaus. Aus der Mathematik ist uns ein der Rekursion ähnliches Verfahren, die Verkettung zweier Funktionen f und g zu einer Funktion h, bekannt: h(x) := (f g)(x) = f(g(x)) Der Spezialfall f g ergibt genau die Konstellation, dass sich die Funktion f selber aufruft. Allerdings ist dies keine Rekursion! Prof. Dr. Nikolaus Wulff Programmieren in C 3
Verkettung versus Rekursion Eine Gleichung der Form f(x) = h(x, f (g(x))) führt zu einer Rekursion. Meist verknüpft mit einer weiteren Funktion g, die das Argument x modifiziert und einer Funktion h. Rekursion liegt vor, wenn die Funktion f sowohl auf der linken als auch der rechten Seite vorkommt! Eine solche Funktion f kann sich unter Umständen beliebig oft aufrufen. Meist mit modifizierten Argument x g(x) Damit ein solcher Algorithmus terminiert muss der Rekursion eine geeignete Grenze gesetzt werden. Prof. Dr. Nikolaus Wulff Programmieren in C 4
Rekursive Algorithmen Wir wollen einen Algorithmus sum entwickeln, der alle Zahlen von 1 bis n aufsummiert: n n = j=1 j Die Definition σ(n) für den Algorithmus sum legt eher einen Algorithmus nahe, der eine Schleife verwendet. Es kostet einige Überlegungen hierfür einen rekursiven Algorithmus zu finden... Prof. Dr. Nikolaus Wulff Programmieren in C 5
sum als Schleife Die folgende Implementierung von sum entspricht unserem bisherigen Programmierstil, wir verwenden eine Schleife: int sum(unsigned int n){ int j, s = 0; for(j=1; j<=n; j++) { s += j; return s; Es ist offensichtlich, dass diese Implementierung eine direkte Umsetzung der Definition der Formel σ(n) der letzten Folie ist. Prof. Dr. Nikolaus Wulff Programmieren in C 6
Aus Schleife wird Rekursion... Ein rekursiver Algorithmus lässt sich aus der Analyse der definierenden Formel gewinnen: n n = j=1 j n = n n 1 2 1 n 1 n = n j=1 j n = n n 1 Prof. Dr. Nikolaus Wulff Programmieren in C 7
sum per Rekursion Die folgende Implementierung berechnet die Summe per Rekursion: int sum(unsigned int n) { if(n>1) { return n + sum(n-1); return n; Um den Algorithmus besser zu verstehen, macht es Sinn ihn mit etwas Debug Output zu versehen... Verankerung sum(0) = 0 sowie sum(1) = 1 sum(n) = n + sum(n-1) Prof. Dr. Nikolaus Wulff Programmieren in C 8
sum per Rekursion static int depth = 0; int sum(unsigned int x) { depth++; printf("beg %d x: %d \n", depth, x); int y = x; if(x>1) { y += sum(x-1); printf("end %d sum(%d)=%d \n", depth, x, y); depth--; return y; Die Rekursionstiefe wird mit der statischen Variablen depth gezählt. Jeweils beim Eintritt und vor dem Verlassen der Methode werden depth, x und y ausgegeben. Prof. Dr. Nikolaus Wulff Programmieren in C 9
Rekursive Sum in Aktion Die folgende main Routine int main() { int y, x = 5; y = sum(x); printf("\nresult: sum(%d)=%d\n", x, y); return 0; BEG 1 x: 5 BEG 2 x: 4 BEG 3 x: 3 BEG 4 x: 2 BEG 5 x: 1 END 5 sum(1)=1 END 4 sum(2)=3 END 3 sum(3)=6 END 2 sum(4)=10 END 1 sum(5)=15 liefert als Ausgabe: Result: sum(5)=15 Prof. Dr. Nikolaus Wulff Programmieren in C 10
Call Graph von sum Aufruf mit Argument 5 liefert nach 5 Rekursionen das Ergebnis 15 depth =1 sum(5) return 5 + 10 = 15 depth =2 sum(4) return 4 + 6 = 10 depth =3 sum(3) return 3 + 3 = 6 depth =4 sum(2) return 2 + 1 = 3 depth =5 sum(1) return 1 sum(1) = 1 sum(x) x + sum(x-1) für x > 1 Prof. Dr. Nikolaus Wulff Programmieren in C 11
Die Fakultät Die Fakultät Funktion n! besitzt sowohl eine Definition per Produkt: n! := n j=1 j als auch eine rekursive Definition: 1! := 1 n! := n*(n-1)! Prof. Dr. Nikolaus Wulff Programmieren in C 12
Fakultät rekursiv long factorial(long n) { if (n>1) { return n*factorial(n-1); return 1; Die rekursive faktorial Implementierung der Fakultät ist ähnlich zur sum Funktion strukturiert. Wichtig bei rekursiven Funktionen ist es, eine geeignete Abbruchbedingung vorzusehen, ansonsten terminiert der Algorithmus nie. => Stackoverflow Dies ist vollkommen analog zur Problematik bei falsch programmierten Schleifen... Prof. Dr. Nikolaus Wulff Programmieren in C 13
Gemeinsamkeiten Die beiden Beispiele besitzen die Struktur: Mit der Funktion g(n)=n-1 und den Funktionen h 1 (n,m) = n+m für die Summe und h 2 (n,m)=n*m für die Fakultät. f n =h n, f g n n =h 1 n, g n =n g n =n n 1 n!=h 2 n, g n! =n g n!=n n 1! Die Funktion g garantiert durch Verkleinerung von n, dass die Abbruchbedingung erreicht wird. Prof. Dr. Nikolaus Wulff Programmieren in C 14
Binärdarstellung Ein Ganzzahl x soll binär dargestellt werden: n x= j=0 b j 2 j Die Entwicklungskoeffizienten b j {0,1 lassen sich mit dem Pseudo-Algorithmus while(x) { b = x mod 2 x = x/2 print(b) ermitteln, allerdings in der falschen Reihenfolge! Prof. Dr. Nikolaus Wulff Programmieren in C 15
Rekursive Binärdarstellung Ein rekursiver Algorithmus ermöglicht eine einfache Implementierung ohne revers Operation... void printbinaery(unsigned int x) { if (x<2) { printf( %d,x); else { printbinaery(x/2); printf( %d,x%2); Prof. Dr. Nikolaus Wulff Programmieren in C 16
Rekursive Sortierung Quicksort ist ein rekursiver Algorithmus. Aus einem unsortierten Feld wird ein Element p ausgewählt und der Rest in zwei Untermengen aufgeteilt. Eine Menge mit Elementen größer als das ausgewählte Element p, die andere mit Elementen kleiner gleich dem ausgewählten Element. Diese Unterteilung wird so lange vorgenommen bis die Teilmengen weniger als zwei Elemente haben, dann terminiert die jeweilige Rekursion. Am Ende aller Rekursionen ist das Feld sortiert. Prof. Dr. Nikolaus Wulff Programmieren in C 17
Rekursiver Quicksort void qindex(int v[], int left, int right); void qsort(int v[], int left, int right) { if (left>=right) return;/* Terminator */ int p = qindex(v, left, right); qsort(v, left, p-1); qsort(v, p+1, right); void sort(int v[], int length) { qsort(v, 0, length); sort initialisiert den rekursiven qsort Algorithmus. Dieser verwendet die Hilfsfunktion qindex, um rekursiv das Feld in zwei Teilbereiche zu unterteilen und zu sortieren. Prof. Dr. Nikolaus Wulff Programmieren in C 18
Pivot Element ermitteln void qswap(int a[], int x, int y) { int tmp=a[x]; a[x]=a[y]; a[y]=tmp; int qindex(int v[], int left, int right) { int i, p, mid = (left+right)/2; qswap(v, left, mid); /* store pivot element */ p = left; for(i=left+1; i<=right; i++) { if (v[i] < v[left]) { /* compare to pivot */ qswap(v, ++p, i); qswap(v, left, p); /* restore pivot element */ return p; /* v[left,...,p]<= v[p] */ Prof. Dr. Nikolaus Wulff Programmieren in C 19
Der Einfluss von qswap Was die Übersich bei der Rekursionsfolge erschwert, ist dass qindex mehrmals qswap aufruft. {5,3,9,1,6,7,0,2,8,4 Ausgangssituation {6,3,9,1,5,7,0,2,8,4 Pivotelement 6 sichern {6,3,9,1,5,7,0,2,8,4 3 3 {6,3,1,9,5,7,0,2,8,4 9 1 {6,3,1,5,9,7,0,2,8,4 9 5 {6,3,1,5,0,7,9,2,8,4 9 0 {6,3,1,5,0,2,9,7,8,4 7 2 {6,3,1,5,0,2,4,7,8,9 9 4 {4,3,1,5,0,2,6,7,8,9 Pivotelement 6 restaurieren Nach dem Aufruf von qindex liegt also die Folge {431502< 6 <{789 vor. Prof. Dr. Nikolaus Wulff Programmieren in C 20
Rekursion kann kompliziert sein... Die Rekursion dieses Algorithmus ist nicht mehr so einfach nachzuvollziehen: Vorbelegung int v[]={5,3,9,1,6,7,0,2,8,4; Es resultiert die folgende Folge von Rekursionen: {5,3,9,1,6,7,0,2,8,4 {4,3,1,5,0,2 {7,8,9 {0 {4,5,3,2 {7 {9 {2 {2,4,3 {3,2 Prof. Dr. Nikolaus Wulff Programmieren in C 21