Folgen und Funktionen in der Mathematik Anhand von einigen exemplarischen Beispielen soll die Implementierung von mathematischen Algorithmen in C/C++ gezeigt werden: Reelle Funktionen in C/C++ Diese wird man meist als double-Funktion mit einem oder mehreren Argumenten programmieren, also z.B: double f(double x) oder double f(double x, double a, ...) Verwendet man mathematische Funktionen wie Exponentialfunktionen, Winkelfunktionen usw. , so ist in C meist die Headerdatei math.h zu inkludieren und das Programm mit gcc –lm zu kompilieren. In C++ genügt es, den Header cmath zu includieren. Folgen in C/C++ bzw. Funktionen von ganzzahligen positiven Argumenten a) Folgen sind in der Mathematik oft durch einen Funktionsterm wie a = ( n^2-n+3 ) definiert. Folgen natürlicher Zahlen können sehr leicht durch eine int a(int n) oder auch int a(unsigned n) Funktion nachgebildet werden, z.B. hier int a(int n) { return n*n-n+3; } // NICHT VERGESSEN: ^ ist nicht das Quadrat Reelle Zahlenfolgen wird man eher mit double-Ergebnis definieren: double a(int n) { return (n+1.0)/n; // NICHT VERGESSEN: Vermeide int-Division } Manchmal macht es durchaus Sinn, bei der Berechnung der Funktionsterme Gleitkommazahlen zu verwenden, um Überläufe zu vermeiden und um Ganzzahldivisionen zu verhindern: double a(int n) { return (n*n*n-1)/(n*n+3); } // Überlauf und Ganzzahldivision! double a(int n) { double x = n; return (x*x*x-1)/(x*x+3); } // Alles behoben! b) Manchmal sind Folgen in der Mathematik rekursiv definiert, d.h. man erfährt nur, wie man aus alten Folgengliedern neue berechnen kann: Fibonacci-Folge und ähnliche rekursive Folgen: Die Fibonacci-Folge ist festgelegt durch F(0)=0, F(1)=1, F(n) = F(n-2)+F(n-1) für alle n >= 2 d.h. aus 2 Folgengliedern ergibt sich das nächste als deren Summe. es gibt auch nichtrekursive Formeln für diese Folge, aber die einfachste Art der Berechnung ist die Rekursion. C/C++ erlaubt rekursive Funktionen, sodass sich diese Definition fast wörtlich umsetzen lässt: Die Fibonacci-Folge int Fibonacci(int n) // berechne Folgenglied mit Index n { if (n > 1) // der Hauptfall, n = 2, 3, 4, … return Fibonacci(n-2)+Fibonacci(n-1); if (n == 0) // man braucht wegen des return hier kein else return 0; if (n == 1) return 1; // ab hier Fehler, das darf nicht sein cerr << "Fehler: Fibonacci(" << n << ")\n"; return -1; } Man stellt aber trotz Optimierung fest, dass z.B. der Aufruf Fibonacci(50) ewig braucht. Der Grund liegt darin, dass z.B. Fibonacci(50) die Aufrufe Fibonacci(49) und Fibonacci(48) verursacht, diese erzeugen Aufrufe von Fibonacci(48), Fibonacci(47) sowie Fibonacci(47), Fibonacci(46). D.h. jeder Aufruf erzeugt 2 weitere Aufrufe und die Arbeitslast steigt wie 2n. Für n=50 ist das die riesige Zahl 250= 1125899906842624, sodass der enorme Zeitaufwand damit erklärbar ist. Manchmal lassen sich rekursiv definierte Funktionen in eine Schleife umschreiben, manchmal lässt sich die Tiefe der Rekursion (aus wie vielen alten Elementen berechnet man das neue) reduzieren, z.B. hier: Die Funktion Fibonacci(n, a, b) berechne das n.te Glied der Fibonacci-Folge mit den Startwerten a, b, d.h. Fibonacci(0, a, b) = a, Fibonacci(1, a, b) = b und Fibonacci(n, a, b) = Fibonacci(n-2, a, b) + Fibonacci(n-1, a, b) sonst. schreibt man die ersten Glieder dieser Folge an, erhält man: a, b, a+b, a+2b, 2a+3b … und man sieht, dass das n. Glied dieser Folge das (n-1). Glied der Folge ist, bei der man den allerersten Wert weglässt: b, a+b, a+2b, 2a+3b … d.h. Fibonacci(n, a, b) = Fibonacci(n-1, b, a+b) Diese Rekursion hat nur noch eine Tiefe von 1 und wächst linear. Größter gemeinsamer Teiler ggT und der Euklidische Algorithmus Euklidischer Algorithmus: Es seien a >= b > 0 natürliche Zahlen; a = q*b + r (Division von a durch b mit Rest r, Quotient q) Es gilt: r == 0: r != 0: ggt(a,b) = b ggt(a,b) = ggt(b,r) (Rekursion) die ggt-Funktion kann man genauso programmieren, sie ruft sich dann selbst auf -> rekursive Funktion int __ggt(int a,int b) // berechnet rekursiv den ggT von a, b // a,b muessen positiv sein!!! // außerdem muss a >= b sein { int r; r = a % b; // Divisionsrest if (r == 0) return b; else return __ggt(b,r); } Noch kürzer kann man die Funktion noch schreiben, wenn man den if else-Block mit dem ?: Operator wegoptimiert. Die Syntax ist Bedingung ? Wert_if_true : Wert_if_false Damit lassen sich kurze if else-Blöcke oft vermeiden: int __ggt(int a,int b) // berechnet rekursiv den ggT von a, b // a,b muessen positiv sein!!! // außerdem muss a >= b sein { int r; return (r = a % b) == 0? b : __gtt(b, r); } Bei dieser Rekursion erzeugt ein Aufruf des __ggt nur einen weiteren Aufruf, sodass hier kein exponentielles Wachstum der Aufrufe erfolgt und die Rechenzeit erträglich klein bleibt. Trotzdem kann man auch hier die Abarbeitung in eine Schleife verlagern und dadurch Ressourcen einsparen: int __ggt(a,b) // berechnet den ggT von a, b in Schleife // a, b müssen positiv sein!!! // außerdem muss a >= b sein { int r; do } { r = a % b; // Divisionsrest a = b; // b übernimmt die Rolle von a b = r; // r übernimmt die Rolle von b while (r != 0); return a; // eigentlich muss man b zurückgeben // b wurde in der Schleife schon mit r = 0 überschrieben // ist aber Gott sei Dank noch in a gespeichert } Wenn es möglich ist, Algorithmen nichtrekursiv zu formulieren, wird man meist mit Zeitoder Speicherplatzersparnis belohnt. Nicht alle rekursiven Folgen lassen sich in eine nichtrekursive Form bringen! In beiden Varianten muss man noch eine zusätzliche Funktion int ggt(int a, int b) schreiben, die dafür sorgt, dass die Bedingungen an a und b (a >= b > 0) zutreffen oder hergestellt werden, und die abschließend __ggt() aufruft int ggt(int a, int b) { int n; if (a < 0) a = -a; if (b < 0) b = -b; if (a < b) { n = a; a = b; b = n; } if (b == 0) return a; // jetzt ist sicher a >= 0 // jetzt ist sicher b >= 0 // vertausche a und b // oder: std::swap(a, b); // jetzt gilt 0 <= b <= a // ggt(0, ?) = ?! // hier gilt sicher 0 < b <= a return __ggt(a,b); // rufe obige __ggt-Funktion auf, egal welche } Die Potenz-Funktion Da C/C++ keine eingebaute Potenzfunktion besitzt, könnte man Sie recht leicht mithilfe von Schleifen und Multiplikationen realisieren (oder die Exponentialfunktion und Logarithmusfunktion verwenden). Eine sehr elegante rekursive Version: double hoch(double x, int n) { if (n > 0) // Hauptfall return (n % 2 != 0)? x * hoch(x, n-1)// n ist ungerade? : hoch(x*x, n/2); // n ist gerade! if (n == 0) return 1; return hoch(1/x, -n); }