Pfiffige Algorithmen - ABZ
Pfiffige Algorithmen - ABZ
Pfiffige Algorithmen - ABZ
Sie wollen auch ein ePaper? Erhöhen Sie die Reichweite Ihrer Titel.
YUMPU macht aus Druck-PDFs automatisch weboptimierte ePaper, die Google liebt.
<strong>Pfiffige</strong> <strong>Algorithmen</strong><br />
Mentorierte Arbeit in der Fachdidaktik Informatik<br />
Autor: Emil Müller<br />
Betreuer: Prof. Juraj Hromkovic, Giovanni Serafini<br />
19. Februar 2012
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Inhalt<br />
Lernziele ...................................................................................................................................... 3<br />
Ablauf .......................................................................................................................................... 4<br />
Einleitung .................................................................................................................................... 4<br />
Binomialkoeffizienten ................................................................................................................. 5<br />
Pascalsches Dreieck ................................................................................................................ 6<br />
Neuer Ansatz ......................................................................................................................... 16<br />
<strong>Pfiffige</strong> Idee ........................................................................................................................... 18<br />
Fibonacci-Zahlen berechnen ..................................................................................................... 29<br />
Intuitiver Ansatz .................................................................................................................... 29<br />
Iterative Berechnung wie von Hand ..................................................................................... 32<br />
Raffinierte Iteration .............................................................................................................. 33<br />
Fazit ....................................................................................................................................... 44<br />
Literaturverzeichnis .................................................................................................................. 45<br />
Anhänge ....................................................................................................................................... I<br />
Fachdidaktik Informatik Seite 2
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Fach<br />
Informatik<br />
Schultyp<br />
Mittelstufe<br />
Alter<br />
Ab 11. Schuljahr<br />
Vorkenntnisse Grundlegende Kenntnisse in Java, inkl.<br />
Objektorientierung.<br />
Umgang mit Komplexitätsklassen und -Schreibweise<br />
Binomialkoeffizient ( )<br />
<br />
<br />
<br />
<br />
<br />
Pascalsches Dreieck.<br />
Primfaktorzerlegung von Zahlen.<br />
Binärdarstellung von Zahlen<br />
Matrix-Multiplikation<br />
Variablen im mathematischen Sinne<br />
Bearbeitungsdauer<br />
ca. 8 Lektionen<br />
Lernziele<br />
Leitidee<br />
In vielen Bereichen der Informatik spielen rekursive <strong>Algorithmen</strong> eine wichtige Rolle. Sei es<br />
weil oft mit sehr wenig Code Probleme gelöst werden können, oder weil viele Probleme und<br />
Fragestellungen auf den ersten Blick nach einem rekursiven Problem aussehen.<br />
In einigen Bereichen ist aber die Rekursion, obwohl intuitiv sofort einleuchtend, nicht der<br />
beste Ansatz. In dieser Unterrichtseinheit soll den Schülerinnen und Schülern aufgezeigt<br />
werden, dass mit relativ wenig Mathematik auf raffinierte Weise schnelle <strong>Algorithmen</strong><br />
entwickelt werden können.<br />
Lernziele<br />
- Sie lernen drei verschiedene <strong>Algorithmen</strong> kennen, um Binomialkoeffizienten ( ) zu<br />
berechnen.<br />
Fachdidaktik Informatik Seite 3
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
- Sie lernen drei verschiedene <strong>Algorithmen</strong> kennen, um die -te Fibonacci-Zahl zu<br />
berechnen.<br />
- Sie lernen, wie die Effizienz von <strong>Algorithmen</strong> beurteilt werden kann und wie sie in<br />
Komplexitätsklassen eingeteilt werden können.<br />
- Sie lernen, wie in Primfaktoren zerlegt werden kann.<br />
- Sie lernen, wie man algorithmisch auf effiziente Art und Weise grosse Potenzen<br />
berechnen kann.<br />
Ablauf<br />
Diese Unterrichtseinheit ist so konzipiert, dass sie von den Schülerinnen und Schülern<br />
selbständig erarbeitet werden kann. Aufgrund der relativ komplexen Unterthemen und der<br />
vielen Aufgaben ist mit einem Aufwand von ca. 8 Lektionen zu rechnen.<br />
Diese Unterlagen sind so aufgebaut, dass vieles im Text explizit erklärt wird. An den<br />
wichtigen Stellen werden Sie mit Aufgaben (inklusive detaillierter Lösungen im Anhang) an<br />
die richtige Idee herangeführt. Versuchen Sie, die Aufgaben selbständig zu lösen und danach<br />
anhand der Lösungen zu kontrollieren, ob Sie alles verstanden haben.<br />
Bei den Programmier-Aufgaben kann es nützlich sein, wenn sie zusätzlich zu Ihrer eigenen<br />
Lösung auch den Code aus der Musterlösung übernehmen und ihn testen. Dies ist deshalb<br />
hilfreich, weil im weiteren Text jeweils Bezug genommen wird auf den Code aus der<br />
Musterlösung.<br />
Einleitung<br />
Programmieren ist ein kreativer Akt! Die Kreativität offenbart sich dabei jedoch meistens<br />
nicht auf den ersten Blick. Denn – anders als etwa bei einem Musikstück – ist die erste<br />
Anforderung an einen Algorithmus, dass er jene Aufgabe zur Zufriedenheit löst, für die er<br />
erfunden worden ist. Darin lässt sich die Kreativität nicht erkennen. Vergleicht man jedoch<br />
zwei verschiedene <strong>Algorithmen</strong>, die das gleiche Problem lösen, können sehr grosse<br />
Unterschiede zu Tage treten. Diese Unterschiede sind meistens der Kreativität der<br />
Programmierer zuzuschreiben. Um zu erkennen, muss man jedoch meist ziemlich genau<br />
hinsehen.<br />
Fachdidaktik Informatik Seite 4
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Ein Anspruch an einen guten Algorithmus kann beispielsweise sein, dass er mit möglichst<br />
wenig Zeilen Code auskommt. Ein anderer Anspruch kann sein, dass der Algorithmus einen<br />
gegebenen Input – der mitunter sehr gross sein kann – mit möglichst wenigen<br />
Rechenschritten verarbeitet. In dieser Unterrichtseinheit wird der zweite Aspekt im<br />
Vordergrund stehen.<br />
An zwei Beispielen – Berechnung der Binomialkoeffizienten ( ) und Berechnung des -ten<br />
Gliedes der Fibonacci-Folge – soll gezeigt werden, wie in der Informatik mathematische<br />
Resultate und Zusammenhänge äusserst raffiniert eingesetzt werden können. Das Resultat<br />
werden zwei <strong>Algorithmen</strong> sein, deren Rechenaufwand um einige Grössenordnungen kleiner<br />
ist verglichen mit intuitiven Ansätzen.<br />
Binomialkoeffizienten<br />
Der Binomialkoeffizient ( ) (lies: „n tief k“) spielt in der Mathematik – und<br />
überraschenderweise auch im Alltag – eine wichtige Rolle. So gibt es beispielsweise ( )<br />
verschiedene Möglichkeiten einen Lottoschein auszufüllen, wenn 6 Kugeln aus 45 gezogen<br />
werden. Beim Berechnen von ( ) hat das Glied den Koeffizienten ( ). (Daher<br />
kommt übrigens auch sein Name.) Oder es gibt zwischen 0 und 1‘000‘000 genau ( ) Zahlen,<br />
in denen genau 3 Mal die Ziffer 7 vorkommt.<br />
Allgemein spielt der Binomialkoeffizient vor allem beim Berechnen von<br />
Wahrscheinlichkeiten und in der Statistik eine sehr wichtige Rolle. Deshalb ist die<br />
Entwicklung eines Algorithmus, der mit möglichst wenig Rechenaufwand beispielsweise<br />
( ) berechnen kann, sehr wünschenswert. Der Wert von ( ) ist übrigens eine<br />
unsäglich grosse Zahl, die auch von den besten Taschenrechnern im Moment nicht<br />
berechnet werden kann. Sie hat sage und schreibe 1116 Stellen. An diesem Beispiel wird<br />
ersichtlich, weshalb es sehr sinnvoll ist, einen Algorithmus zu haben, der raffiniert genug ist,<br />
mit derart grossen Zahlen rechnen zu können.<br />
Auf dem Weg zu einem schnellen Algorithmus, setzen wir als erstes die naheliegende Idee<br />
um, die Sie aus dem Mathematik-Unterricht kennen.<br />
Fachdidaktik Informatik Seite 5
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Pascalsches Dreieck<br />
Im Mathematik-Unterricht haben Sie gelernt, wie man beispielsweise (<br />
) berechnet.<br />
Diese Summe besteht aus den Termen und . Jeder Term wird<br />
noch mit einem<br />
Koeffizienten multipliziert,<br />
den man aus dem<br />
Pascalschen Dreieck abliest.<br />
Dabei gilt: Für die<br />
Berechnung von ( )<br />
liest man die Koeffizienten<br />
in der -ten Zeile des<br />
Pascalschen Dreiecks ab.<br />
Tab. 1<br />
1<br />
1 1<br />
1 2 1<br />
1 3 3 1<br />
1 4 6 4 1<br />
1 5 10 10 5 1<br />
1 6 15 20 15 6 1<br />
Will man also (<br />
ist. Dabei gilt:<br />
) berechnen, benötigt man die Koeffizienten aus jener Zeile, wo<br />
- 1. Eintrag: ( )<br />
- 2. Eintrag: ( )<br />
- 3. Eintrag: ( )<br />
- usw.<br />
Trägt man in der obigen Tabelle anstatt der Zahlen die Symbole ein, tritt das Muster zu Tage,<br />
das uns im nächsten Abschnitt helfen wird, einen Algorithmus zu entwickeln.<br />
( ) 1<br />
( ) ( ) 2<br />
( ) ( ) ( ) 3<br />
( ) ( ) ( ) ( ) 4<br />
( ) ( ) ( ) ( ) ( ) 5<br />
( ) ( ) ( ) ( ) ( ) ( ) 6<br />
Tab. 2<br />
( ) ( ) ( ) ( ) ( ) ( ) ( )<br />
Fachdidaktik Informatik Seite 6
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Algorithmische Vorüberlegungen<br />
Will man einen Algorithmus schreiben, der beliebige Binomialkoeffizienten berechnet, sind<br />
einige Vorüberlegungen nötig. Insbesondere wollen wir die Tatsache ausnützen, dass ein<br />
Eintrag im Pascalschen Dreieck die Summe der zwei darüber liegenden Einträge ist.<br />
Aus Tab. 2 wird ersichtlich, dass beispielsweise für die Berechnung von ( ) die beiden<br />
Einträge ( ) und ( ) herangezogen werden müssen. Für die Berechnung von ( ) wiederum<br />
werden ( ) und ( ) benötigt. Und so weiter.<br />
Verallgemeinert man diese Beobachtung für die Berechnung von ( ), so werden dazu die<br />
beiden Einträge ( ) und ( ) in der Zeile darüber benötigt. Also Formel bedeutet<br />
dies:<br />
( ) ( ) ( )<br />
Mit anderen Worten: Anstatt ( ) direkt zu berechnen, berechnen wir die Summe von<br />
( ) ( ). Allerdings um beispielsweise ( ) zu berechnen, benötigen wir<br />
( ) ( ). Und so weiter. Sollte zu irgend einem Zeitpunkt ( ) oder ( ) berechnet<br />
werden, stoppt die Berechnungskette, denn ( ) und ( ) . Mit diesen Überlegungen<br />
lässt sich ein rekursiver Algorithmus entwickeln. Im Pseudocode sieht sie so aus:<br />
BinKoeffizientRek(n,k){<br />
Falls oder<br />
return 1<br />
sonst<br />
return BinKoeffizientRek(n-1,k-1)+BinKoeffizientRek(n-1,k)<br />
}<br />
Aufgabe 1 Schreiben Sie eine Java-Klasse BinKoeff, die eine main-Methode enthält<br />
und eine Methode int binKoeffRek(int n, int k). Letztere soll den<br />
Binomialkoeffizienten ( ) rekursiv berechnen, wie oben beschrieben.<br />
Aufgabe 2<br />
Testen Sie Ihre Klasse mit den Beispielen<br />
( ) ( ) ( ) ( ) ( ) ( ) ( ) auf Ihre Richtigkeit.<br />
Fachdidaktik Informatik Seite 7
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Ist das ein guter Algorithmus?<br />
Für die Beurteilung der Güte des Algorithmus‘ ist entscheidend, wie viele<br />
Berechnungsschritte er durchführen muss, um ( ) zu berechnen. Aufgrund seiner<br />
rekursiven Struktur ist deshalb massgebend, wie oft sich die Funktion selbst aufruft.<br />
Betrachtet man den Algorithmus, erkennt man, dass sich die Funktion zwei Mal selbst<br />
wieder aufruft, ausser wenn oder . Da letzterer Fall jedoch nicht so oft auftritt,<br />
ist das zweimalige Aufrufen von sich selbst der wichtige Aspekt.<br />
Das Zählen der Funktionsaufrufe für die Berechnung von ( ) wird stark erleichtert, wenn<br />
man sich den Berechnungsbaum aufzeichnet.<br />
Für die Berechnung von beispielsweise ( ), startet der Algorithmus mit den Werten<br />
und . Danach wird bei jedem Aufruf um eins verringert und die Funktion selbst zwei<br />
Mal aufgerufen. Dies wird so lange wiederholt, bis entweder oder ist.<br />
Insgesamt wird also auf Stufen die Funktion 2 Mal - total also in der Grössenordnung von<br />
Mal - aufgerufen. Diese grosse Zahl von Funktionsaufrufen rührt daher, dass die Funktion<br />
mehrere Male mit den gleichen Werten aufgerufen wird – viele Werte also mehrfach<br />
Fachdidaktik Informatik Seite 8
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
berechnet werden. Im Bild ist der Wert ( ) hervorgehoben. Er wird schon für die<br />
vergleichsweise kurze Berechnung von ( ) drei Mal berechnet.<br />
Für grosse<br />
bedeutet dies, dass für die Berechnung von ( ) die Funktion unheimlich oft<br />
aufgerufen wird und somit enorm viele Berechnungsschritte nötig sind.<br />
Berechnung des Pascalschen Dreiecks<br />
Müssten Sie selbst - ohne Hilfe eines Computers - einen bestimmten Binomialkoeffizienten<br />
berechnen, würden Sie sicher anders vorgehen, als oben beschrieben. Sie würden von oben<br />
nach unten das Pascalsche Dreieck nach und nach berechnen. Dabei schreiben Sie die bereits<br />
berechneten Werte in eine Tabelle und ziehen diese für die nächsten Berechnungen wieder<br />
heran.<br />
Diese Idee wollen wir nun in einen Algorithmus übersetzen und überprüfen, ob wir<br />
gegenüber dem rekursiven Ansatz eine Verbesserung erreichen können.<br />
Dazu benötigen wir als erstes einen zweidimensionalen Array, der das Pascalsche Dreieck<br />
repräsentiert. Dabei soll gelten, dass die erste Zeile die Länge 1 hat, die zweite Zeile die<br />
Länge 2, usw. Dieser Array soll in zwei verschachtelten Schleifen mit den entsprechenden<br />
Werten gefüllt werden. Auch bei diesen Berechnungen nutzen wir die Eigenschaft des<br />
Pascalschen Dreiecks, dass jeder Wert – ausser den Einträgen am Rand – der Summe der<br />
zwei darüber liegenden Werte entspricht.<br />
Nach den Berechnungen hat dieser zweidimensionale Array die Form:<br />
1 pascal[0]<br />
1 1 pascal[1]<br />
1 2 1 pascal[2]<br />
1 3 3 1 pascal[3]<br />
1 4 6 4 1 pascal[4]<br />
1 5 10 10 5 1 pascal[5]<br />
1 6 15 20 15 6 1 pascal[6]<br />
1 7 21 35 35 21 7 1 pascal[7]<br />
1 8 28 56 70 56 28 8 1 pascal[8]<br />
1 9 36 84 126 126 84 36 9 1 pascal[9]<br />
1 10 45 120 210 252 210 120 45 10 1 pascal[10]<br />
Tab. 3<br />
Dabei entspricht die eingekreiste Zelle pascal[8][3] dem Wert des Binomialkoeffizienten<br />
( ). Will man nun damit eine Funktion int binKoeffPascal(int n, int k) entwickeln,<br />
Fachdidaktik Informatik Seite 9
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
die den Binomialkoeffizienten mittels des Pascalschen Dreiecks ausrechnet, so muss in<br />
einem ersten Schritt der zweidimensionale Array pascal[][] mit der entsprechenden Länge<br />
(entspricht dem Wert von ) initialisiert werden. Die Längen der Arrays für die einzelnen<br />
Zeilen bleiben dabei vorerst noch unbestimmt. Danach müssen in einer Schleife die Längen<br />
der Zeilen festgelegt und in jeder Zeile die Rand-Zellen mit 1 beschrieben werden.<br />
Schliesslich folgen die verschachtelten Schleifen – eine für die Zeilen, eine für die Spalten -,<br />
die die Werte berechnen und in den Array schreiben.<br />
Innerhalb der Schleife soll der Algorithmus so funktionieren, dass er für die Berechnung<br />
eines Eintrags pascal[i][j] immer auf die zwei Einträge pascal[i-1][j-1] und<br />
pascal[i-1][j] in der Zeile darüber zugreifen kann und es dabei zu keinen Array-Index-<br />
Problemen kommt. Dazu braucht es jedoch noch eine Überlegung in Bezug auf die<br />
möglichen Werte der Schleifenvariablen i (Zeile) und j (Spalte). Denn darf nicht 0 sein,<br />
über der Zeile 0 keine Zeile mehr existiert. Zudem darf auch nicht 1 sein, da beide Einträge<br />
in der ersten Zeile Rand-Einträge sind und nicht mittels der darüber liegenden Werte<br />
berechnet werden können. Für j gilt, dass es ebenfalls nicht 0 sein darf, da in der Zeile<br />
darüber kein Eintrag mit dem Index -1 existiert. Auch am rechten Rand gibt es eine<br />
Einschränkung für j. Dort muss der Wert für j strikt kleiner bleiben als der Zeilenindex i.<br />
Andernfalls würde der Algorithmus für die Berechnung von pascal[i][i] versuchen, auf<br />
den Eintrag pascal[i-1][i] zuzugreifen, der ebenfalls nicht existiert. Zusammengefasst gilt<br />
also:<br />
Werte für den Zeilenindex : bis<br />
Werte für den Spaltenindex : bis<br />
Damit können wir nun den Algorithmus als Pseudocode festhalten:<br />
int binKoeffPascal(int n, int k){<br />
intialisiere den int-Array pascal[] mit Länge n+1 //Zeilen von 0 bis n<br />
for i=0 to n<br />
intialisiere pascal[i] mit Länge i+1<br />
pascal[i][0]
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Aufgabe 3<br />
Schreiben Sie in ihrer Klasse BinKoeff, eine Methode public static int<br />
binKoeffPascal(int n, int k), die den Binomialkoeffizienten ( ) wie oben<br />
beschrieben berechnet und zurückgibt.<br />
Ist dieser Algorithmus schneller?<br />
Schon im letzten Abschnitt haben wir uns gefragt, wie man beurteilen kann, wie schnell ein<br />
Algorithmus seine Aufgabe erledigt. Dies wollen wir auch für diesen Algorithmus<br />
durchführen. Doch zuerst benötigen wir einige Werkzeuge, die uns auch später helfen<br />
werden, über die Güte von <strong>Algorithmen</strong> zu sprechen.<br />
Bei der Beurteilung eines Algorithmus‘ interessiert und uns hier vor allem die Frage, wie viele<br />
Berechnungsschritte durchgeführt werden müssen, um das Resultat zu erhalten. Und zwar<br />
soll dies nicht für jeden einzelnen Input angegeben werden, sondern ein Algorithmus so zu<br />
klassifizieren, dass man in Abhängigkeit von der Grösse des Inputs – in unserem Fall –<br />
angeben kann, wie viele Schritte der Algorithmus schlimmstenfalls benötigt.<br />
Wir werden uns dies in folgenden Schritten überlegen:<br />
1. Schritte in einem konkreten Beispiel zählen<br />
2. Schritte im allgemeinen Fall zählen.<br />
3. Schreibweise entwickeln für die Klassifizierung<br />
4. Schlussfolgerungen ziehen.<br />
1. Schritte in einem konkreten Beispiel zählen<br />
Beim Zählen von Schritten, die ein Algorithmus durchführt ist etwas Vorsicht geboten. Denn<br />
die Frage dabei ist, was ein Schritt ist. Ist es eine Zeile Code? Ist es die Durchführung einer<br />
mathematischen Operation? Kann es auch ein Vergleich der Werte zweier Variablen oder gar<br />
die Zuweisung eines Wertes zu einer Variablen sein? Eine einfache Antwort auf diese Fragen<br />
gibt es nicht. Sicher ist es falsch, eine Code-Zeile immer nur als einen Schritt zu zählen. Denn<br />
in einer einzigen Zeile lassen sich mehrere Berechnungsschritte zusammenfassen. Darüber<br />
hinaus hängt die Antwort auf die obigen Fragen jedoch vom behandelten Problem ab. So ist<br />
es beispielsweise zulässig, eine mathematische Operation als einen Schritt zu zählen, so<br />
lange die Zahlen, die in die Berechnung involviert sind, relativ klein sind. Wachsen diese<br />
jedoch an, kann eine Multiplikation zweier Zahlen plötzlich mit viel mehr als nur einem<br />
einzigen Schritt zu Buche schlagen, weil im Register viele Operationen durchzuführen sind,<br />
Fachdidaktik Informatik Seite 11
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
bis die Multiplikation bewerkstelligt ist. Auch der Vergleich der Werte zweier Variablen oder<br />
die Zuweisung eines Wertes zu einer Variablen kann als ein Schritt gewertet werden. Denn<br />
immerhin muss der Rechner etwas tun, um im Register einen Wert abzuspeichern oder zwei<br />
Werte für einen Vergleich aus dem Register auszulesen.<br />
In diesem Abschnitt werden wir die Schritte zählen, die für die Berechnung von ( ) nötig<br />
sind. Dabei treten keine ganz grossen Zahlen auf. Aus diesem Grund können wir die Addition<br />
als einen einzigen Schritt zählen. Variablen-Vergleiche lassen wir ausser Betracht.<br />
Wir überlegen uns, wie viele Rechenschritte nötig sind, um mithilfe des Pascalschen Dreiecks<br />
die Zahl ( ) zu berechnen. Offensichtlich ist in diesem Fall und . Für die<br />
Initialisierung des zweidimensionalen Arrays durchläuft der Algorithmus eine Schleife Mal<br />
und führt in jedem Durchgang 3 Schritte (Initialisierung und zwei Additionen) aus. Diese<br />
erste Schleife benötigt deshalb Schritte – im konkreten Fall 18 Schritte.<br />
Interessanter wird die Betrachtung der zwei verschachtelten Schleifen. Dort läuft die<br />
Variable der äusseren Schleife die Werte von bis . Für jeden Durchgang durchläuft die<br />
Variable der inneren Schleife die Werte von 1 bis . Und in jedem Durchgang wird eine<br />
Addition durchgeführt.<br />
Um die Anzahl Schritte zu zählen, halten wir in einer Tabelle fest, wie gross jeweils ist und<br />
wie viele Werte die Variable durchlaufen muss. Am Schluss lassen sich die Durchgänge<br />
addieren.<br />
2 1<br />
3 2<br />
4 3<br />
5 4<br />
6 5<br />
Total<br />
Tab. 4<br />
Anzahl Durchläufe der inneren Schleife<br />
Zählt man alles zusammen benötigt der Algorithmus 18+15=33 Schritte, um ( ) zu<br />
berechnen.<br />
Fachdidaktik Informatik Seite 12
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Aufgabe 4<br />
Bestimmen Sie mit dem obigen Verfahren, wie viele Schritte nötig sind, um<br />
( ) ( ) ( ) und ( ) zu berechnen.<br />
2. Schritte im allgemeinen Fall zählen<br />
Will man die Schritte im allgemeinen Fall – für ( ) – zählen, führen wir die Tabelle von oben<br />
fort und versuchen, die Schritte für ein allgemeines<br />
zu zählen.<br />
2 1<br />
3 2<br />
4 3<br />
5 4<br />
6 5<br />
7 6<br />
…<br />
Anzahl Durchläufe der inneren Schleife<br />
Tab. 5<br />
Aus der Tabelle wird ersichtlich, dass in den verschachtelten Schleifen insgesamt<br />
∑<br />
Schritte nötig sind. Aus dem Mathematik-Unterricht wissen Sie, dass diese Summe als das<br />
Produkt<br />
∑<br />
( )<br />
dargestellt werden kann.<br />
Zu diesen Berechnungen kommen noch die<br />
wir insgesamt für die maximale Anzahl Schritte<br />
Schritte für die Initialisierung. Damit haben<br />
( ), die zur Berechnung von ( ) nötig<br />
sind, folgende Formel:<br />
( )<br />
( )<br />
Fachdidaktik Informatik Seite 13
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
3. Schreibweise entwickeln<br />
Wir betrachten, wie sich diese Anzahlen für wachsende<br />
entwickeln.<br />
Betrachtet man den Funktionsterm von<br />
6 33<br />
7 42<br />
8 52<br />
9 63<br />
10 75<br />
20 250<br />
30 525<br />
50 1375<br />
75 3000<br />
100 5250<br />
( )<br />
1000 502500<br />
10000 50025000<br />
100000 5000250000<br />
Tab. 6<br />
( ), fällt auf, dass er ein Polynom zweiten<br />
Grades in ist. Aus diesem Grund versuchen wir nun, eine Beziehung zwischen ( ) und<br />
herzustellen.<br />
Aufgabe 5<br />
Vergleichen Sie die Werte aus Tab. 6 mit den entsprechenden Werten für<br />
und untersuchen Sie, ob der Wert<br />
( )<br />
für konvergiert? Und wenn ja,<br />
gegen welchen Wert?<br />
Wenn Sie die Aufgabe 5 richtig bearbeitet haben, sollten Sie herausgefunden haben, dass die<br />
folgende Gesetzmässigkeit gilt:<br />
( )<br />
Das bedeutet, dass die Funktion ( ) für grosse gegen den Wert konvergiert.<br />
Anders gesagt: ( ) kann nicht wesentlich schneller wachsen als , wenn grosse<br />
Werte annimmt.<br />
Mit diesen Überlegungen können wir nun ein Qualitäts-Mass für unseren Algorithmus<br />
angeben. Für kleine liefert er in kürzester Zeit das richtige Resultat. Für grosse benötigt<br />
er jedoch ziemlich viele Rechenschritte: Erhöht man um den Faktor 2, so wächst die Anzahl<br />
Berechnungsschritt ungefähr um den Faktor 4. Erhöht man um den Faktor 3, wächsts<br />
( ) um den Faktor 9. Und so weiter.<br />
Fachdidaktik Informatik Seite 14
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Mit anderen Worten:<br />
( ) wächst ungefähr quadratisch in .<br />
Damit ist unser Algorithmus klassifiziert: Er benötigt mit steigendem ungefähr Schritte<br />
für die die Berechnung von ( ). Erstaunlicherweise ist unser Algorithmus nur von<br />
abhängig. Wie gross<br />
ist, spielt für die Anzahl Berechnungsschritte überhaupt keine Rolle.<br />
Die Funktion ( ) gehört demnach zu einer bestimmten Klasse von Funktionen, die<br />
ungefähr quadratisch mit dem Input wachsen. In der Mathematik und in der Informatik<br />
haben solche Klassen von Funktionen einen eigenen Namen.<br />
Man schreibt:<br />
( ) ( )<br />
Dabei ist<br />
( ) so definiert, wie man es nach den obigen Überlegungen erwartet.<br />
Definition (Landau Symbol )<br />
( )<br />
( ) ( )<br />
( ) ( ( ))<br />
( ( )) ( )<br />
( )<br />
( ( )) { | ( )<br />
( ) | }<br />
Für jede Funktion ( ) ( ( )) sagen wir, dass mit wachsendem asymptotisch nicht<br />
schneller wächst als .<br />
Als Bild:<br />
Fachdidaktik Informatik Seite 15
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Aufgabe 6 Wie muss die Konstante gewählt werden, damit die Funktion<br />
gegen<br />
konvergiert?<br />
Aufgabe 7 Gilt ( ) ( )? (Begründen Sie Ihre Antwort.)<br />
Aufgabe 8 In welcher Komplexitätsklasse liegt ? (Begründen<br />
Sie Ihre Antwort.)<br />
Aufgabe 9 Liegt in ( ) Falls ja, geben Sie die Konstante an.<br />
Aufgabe 10 Liegt in ( )? Falls ja, geben Sie die Konstante an.<br />
Aufgabe 11 Liegt in ( ( ))? Falls ja, geben sie die Konstante an.<br />
Aufgabe 12 Geben Sie an, zu welcher Komplexitätsklasse<br />
gehört, der die Binomialkoeffizienten rekursiv berechnet.<br />
( ( )) der Algorithmus<br />
4. Schlussfolgerung<br />
Sowohl der binKoeffRek- als auch der binKoeffPascal-Algorithmus sind nur für kleine<br />
wirklich brauchbar. Falls sehr gross wird, müssen exponentiell oder quadratisch viele<br />
Rechenschritte gemacht werden. Die Frage ist also: Gelingt es uns, einen anderen<br />
Algorithmus zu finden, der für grosse merklich weniger Berechnungen machen muss?<br />
Neuer Ansatz<br />
Wie Sie bestimmt aus dem Mathematik-Unterricht wissen, lassen sich die<br />
Binomialkoeffizienten auch anders als mit dem Pascalschen Dreieck berechnen. Es gilt:<br />
( )<br />
( )<br />
So ist beispielsweise ( ) .<br />
Dies ermöglicht einen neuen Ansatz für die algorithmische Berechnung – und zwar iterativ<br />
mit Schleifen. Wir betrachten den Term etwas genauer:<br />
Fachdidaktik Informatik Seite 16
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
( )<br />
( ) ( ) ( ) ( ) ( )<br />
⏟ ( )<br />
( ⏟ ) ( )<br />
( )<br />
Man erkennt, dass der Term (<br />
man mit (<br />
) kürzen.<br />
) sowohl im Zähler als auch im Nenner steht. Also kann<br />
Es bleibt:<br />
( )<br />
⏞<br />
( ) ( ) ( )<br />
( )<br />
Dieser Bruch enthält im Zähler und im Nenner je Faktoren, von denen jeder um eins<br />
kleiner ist als sein Vorgänger/Nachfolger. Im Zähler hat der grösste Faktor den Wert im<br />
Nenner hat der grösste Faktor den Wert .<br />
In unserem Algorithmus benötigen wir deshalb eine Schleife über<br />
Durchgang um 1 erhöht werden.<br />
Werte, die bei jedem<br />
Im Pseudocode sieht das so aus:<br />
binKoeffIterativ(n,k){<br />
i=0;<br />
ZwResultat =1;<br />
Solange i
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
durchlaufen werden. Zwar werden bei jedem Schleifendurchgang zwei Rechenschritte<br />
durchgeführt. Aber trotzdem: Die maximale Anzahl Schritte ( ) wächst linear in .<br />
Es gilt für den binKoeffIterativ-Algorithmus:<br />
( ) ( )<br />
Wenn beispielsweise<br />
, benötigt die Berechnung mit dem Pascalschen Dreieck<br />
ungefähr Schritte. Der Iterative Ansatz dagegen kommt mit maximal 200<br />
Schritten aus. Dieser Ansatz ist also sehr viel effizienter als der erste.<br />
<strong>Pfiffige</strong> Idee<br />
Wie in der Einleitung schon bemerkt, werden Binomialkoeffizienten für grosse schnell<br />
sehr, sehr gross. Deshalb ist einigermassen erstaunlich, dass wir einen Algorithmus gefunden<br />
haben, dessen Anzahl Berechnungen nur mit der linear in wächst. Es kommt aber noch<br />
besser! In diesem Abschnitt werden wir einen Algorithmus entwickeln, der noch schneller<br />
ist! Doch dazu sind jedoch erst einige mathematische Überlegungen notwendig.<br />
Fakultät in Primfaktoren zerlegen<br />
Zur Vorbereitung überlegen wir uns, wie die Primfaktorzerlegung einer Fakultät<br />
Betrachten wir zuerst einige Beispiele:<br />
aussieht.<br />
Offensichtlich gibt es bei der Primfaktorzerlegung ein Muster!<br />
Aufgabe 14 Bei welchem wird in der Primfaktorzerlegung von zum ersten Mal<br />
stehen?<br />
Aufgabe 15 Bei welchem<br />
erhöhen?<br />
wird sich der Exponent von 3 zum nächsten Mal<br />
Aufgabe 16 Wie viele Faktoren von<br />
Aufgabe 17 Wie viele Faktoren von<br />
sind durch 2 teilbar?<br />
sind durch 4 teilbar?<br />
Fachdidaktik Informatik Seite 18
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Aufgabe 18 Wie viele Faktoren von 101! sind durch 2 teilbar?<br />
Aufgabe 19 Wie viele Faktoren von 101! sind durch 31 teilbar?<br />
Die Resultate aus den Aufgaben wollen wir nun systematisch untersuchen. Dazu betrachten<br />
wir .<br />
Zuerst überlegen wir uns, mit welcher Potenz die Zahl 2 in der Primfaktorzerlegung von<br />
vorkommt. In den Aufgaben haben Sie festgestellt, dass jede 2. Zahl die kleiner als 11 ist<br />
durch 2 teilbar ist. Zudem ist jede 4. durch 4 teilbar, jede 8. durch 8.<br />
Dies wird ersichtlich, wenn man die Faktoren der Fakultät in Primfaktoren zerlegt:<br />
⏟ ⏟ ⏟ ⏟ ⏟<br />
Es gilt:<br />
- ⌊ ⌋ Zahlen sind durch 2 teilbar (rot). (Dabei bezeichnet die auf die nächste<br />
ganze Zahl abgerundete rationale (oder reelle) Zahl . Diese Klammer heisst auch<br />
Gauss-Klammer) In der Primfaktorzerlegung muss also mindestens<br />
vorkommen.<br />
- ⌊ ⌋ Zahlen sind zudem durch 4 teilbar (grün). Jede dieser 2 Zahlen ist also nicht<br />
nur durch 2, sondern auch durch 4 teilbar. Die Teilbarkeit durch 2 haben wir bereits<br />
berücksichtigt. Also fehlt für jede dieser beiden Zahlen noch ein Faktor 2, den wir<br />
noch nicht berücksichtigt haben. Damit muss in der Primfaktorzerlegung mindestens<br />
stehen.<br />
- ⌊ ⌋ Zahl ist zudem durch 8 teilbar (blau). Damit haben wir .<br />
Resultat: In der Primfaktorzerlegung von kommt der Faktor vor.<br />
Aufgabe 20 Mit welcher Potenz kommen die Zahlen 3,5,7 und 11 in der<br />
Primfaktorzerlegung von vor? Notiere danach die Primfaktorzerlegung von<br />
Fachdidaktik Informatik Seite 19
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Was Sie eben entdeckt haben, ist in der Mathematik bekannt als<br />
Satz von Legendre<br />
∏ ⌊ ⌋ ⌊ ⌋ ⌊ ⌋<br />
Mit dieser Idee werden wir nun einen besseren Algorithmus zur Berechnung der<br />
Binomialkoeffizienten entwickeln. Wir werden versuchen die Fakultäten in<br />
Satz von Legendre zu berechnen.<br />
( )<br />
mit dem<br />
Dazu werden wir zwei zusätzliche Dinge benötigen:<br />
1. Wir brauchen eine Liste der Primzahlen, die kleiner als sind, um danach die<br />
Potenzen dieser Primzahlen zu berechnen.<br />
2. Eine Funktion, die für ein gegebenes und eine Primzahl den Exponenten von in<br />
der Primfaktorzerlegung von berechnet.<br />
Liste der Primzahlen erzeugen<br />
Primzahlen zu finden, ist auf den ersten Blick keine einfache Sache, weil sie nicht regelmässig<br />
auftreten. Allerdings hat der griechische Gelehrte Eratosthenes von Kyrene ca. 275 v. Chr.<br />
eine Methode gefunden, wie man systematisch eine Liste von Primzahlen bis zu einer<br />
bestimmten Maximalgrösse erzeugen kann.<br />
Die Idee ist:<br />
- Man schreibt alle Zahlen von 2 bis in eine Liste<br />
- Dann beginnt man mit der ersten Zahl in der Liste (also 2), lässt diese stehen und<br />
streicht alle Vielfachen.<br />
- Danach geht man zur nächsten Zahl, die noch in der Liste steht, lässt diese stehen<br />
und streicht alle Vielfachen dieser Zahl<br />
- usw.<br />
Fachdidaktik Informatik Seite 20
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Am Ende liefert dieses Verfahren alle Primzahlen, die kleiner als<br />
public static int[] getPrimes(int n) {<br />
int[] allNumbers = new int[n + 1];<br />
// Alle Zahlen in eine Liste schreiben<br />
for (int i = 2; i
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Für den 2. Punkt werden wir eine eigene Funktion int calculatePower(int p, int n)<br />
schreiben. Der Exponent von in der Primfaktorzerlegung von wird berechnet mit der<br />
Summe ( ) ⌊ ⌋ ⌊ ⌋ ⌊ ⌋ .<br />
Demnach würde der Algorithmus so aussehen:<br />
calculatePowert(int p, int n){<br />
i=1<br />
resultat=0<br />
solange i
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
abzurunden, und Math.pow(double b, double e), um die Potenz<br />
berechnen.)<br />
zu<br />
Optimierung<br />
Im Prinzip hätten wir jetzt alle Hilfsmittel beisammen, um den Binomialkoeffizienten mit<br />
dem Satz von Legendre zu berechnen. Ein Algorithmus könnte so aussehen:<br />
binKoeffLegendre(int n, int k){<br />
erzeuge Liste PRIMES der Primazahlen, die kleiner als n sind.<br />
berechne ZAEHLER=n! mit PRIMES<br />
berechne NENER=k!*(n-k)! mit Primzahlen
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Die Funktion sieht so aus:<br />
/**<br />
* Berechnet die Primfaktorzerlegung von k!. Diese Methode erzeugt einen<br />
* Array mit der gleichen Länge wie primes. Dort werden die<br />
* Exponenten der Primzahlen in der Primfaktorzerlegung von k! nach dem<br />
* Muster res[i]=Exponent von primes[i] in der Primfaktorzerlegung von k!<br />
* gespeichert.<br />
*<br />
* @param primes Array, der die Primzahlen < n enthält.<br />
* @param k Zahl, zu deren Fakultät die Primfaktorzerlegung berechnet wird.<br />
*<br />
*/<br />
public static int[] primeFakt(int[] primes, int k) {<br />
int[] res = new int[primes.length];<br />
// Nur Primzahlen beachten, die kleiner als k sind.<br />
for (int i = 0; primes[i]
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Zeit in Sekunden<br />
n<br />
k<br />
Anz.<br />
Stellen rekursiv Pasc. Dreieck iterativ Legendre<br />
10 5 3 0.00184898 1.22E-04 3.62E-04 1.11E-04<br />
20 10 6 0.04593385 1.19E-04 3.74E-04 1.15E-04<br />
50 25 15 > 600 0.00236871 7.65E-04 1.79E-04<br />
100 50 30 0.00302797 1.07E-03 2.48E-04<br />
200 100 59 0.0064442 2.39E-03 6.93E-04<br />
500 250 150 0.05917525 4.91E-03 4.72E-04<br />
1000 500 300 0.27922667 4.13E-03 5.62E-04<br />
2000 1000 601 2.18885341 9.78E-03 1.02E-03<br />
5000 2500 1504<br />
zu wenig<br />
Speicher<br />
3.30E-02 2.70E-03<br />
10000 5000 3009 9.42E-02 5.25E-03<br />
20000 10000 6019 2.91E-01 1.05E-02<br />
50000 25000 15050 1.64E+00 4.71E-02<br />
100000 50000 30101 6.49E+00 1.67E-01<br />
200000 100000 60204 2.48E+01 6.73E-01<br />
500000 250000 150513 1.56E+02 3.42E+00<br />
1000000 500000 301027 6.31E+02 1.34E+01<br />
Tab. 7<br />
Für kleine Werte von und sind die Unterschiede sehr gering und spielen in der Praxis<br />
keine Rolle. Doch schon bei<br />
Minuten rechnet. Bei<br />
versagt der rekursive Algorithmus, weil er über zehn<br />
versagt auch die Berechnung mit dem Pascalschen Dreieck,<br />
weil der Speicherplatz (1.5 GB!) nicht ausreicht, um sämtliche Einträge im Pascalschen<br />
Dreieck festzuhalten.<br />
Betrachtet man die Einträge für<br />
, bei denen die Resultate mehrere Tausend<br />
Stellen aufweisen, so werden auch die Unterschiede zwischen dem iterativen und dem<br />
Legendre-Algorithmus eklatant. Während der Legendre-Algorithmus deutlich weniger als<br />
eine Sekunde rechnet, benötigt der iterative Ansatz schon mehrere Sekunden. Am<br />
deutlichsten zu Tage treten die Unterschiede in der letzten Zeile für (<br />
). Das<br />
Resultat ist eine Zahl mit sagenhaften 301027 Stellen. Der iterative Algorithmus rechnet hier<br />
mehr als zehn Minuten (!) – für Computer eine Ewigkeit. Der Legendre-Algorithmus dagegen<br />
hat das unglaublich grosse Resultat nach 13 Sekunden berechnet.<br />
Grafisch dargestellt:<br />
Fachdidaktik Informatik Seite 25
Sekunden<br />
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
1000<br />
100<br />
10<br />
1<br />
0.1<br />
0.01<br />
0.001<br />
0.0001<br />
rekursiv<br />
Pasc. Dreieck<br />
iterativ<br />
Legendre<br />
n<br />
Tab. 8<br />
In dieser Darstellung ist zu beachten, dass die vertikale Achse eine logarithmische Skala<br />
aufweist. Das bedeutet: Ein Unterschied von einem Linienabstand entspricht einem<br />
Unterschied von Faktor 10. Am rechten Rand der Darstellung (für<br />
) ist<br />
demnach der Legendre-Algorithmus fast 100 mal schneller als der iterative Algorithmus.<br />
Offensichtlich wächst der Zeitaufwand des iterativen Algorithmus‘ ungefähr linear zu ,<br />
dessen Grössen auf der horizontalen Achse ebenfalls logarithmisch aufgetragen sind. Die<br />
Frage, die uns zum Abschluss dieses Kapitels beschäftigt, ist:<br />
Wie schnell ist der Legendre-Algorithmus?<br />
Der Algorithmus besteht im Wesentlichen aus drei Teilen:<br />
1. Liste erzeugen mit allen Primzahlen .<br />
2. und ( ) in Primfaktoren zerlegen.<br />
3. Das Resultat als Produkt von Primfaktoren berechnen.<br />
Der Einfachheit halber lassen wir die Erzeugung der Primzahlliste mal beiseite und nehmen<br />
an, wir hätten irgendwo eine grosse Liste mit Primzahlen zur Verfügung, auf die wir ohne<br />
grossen Rechenaufwand zurückgreifen können. Man kann sich auch vorstellen, dass diese<br />
Liste ein Mal erzeugt und abgespeichert wird, so dass wir später immer wieder darauf<br />
zurückgreifen können.<br />
Die Frage ist also, wie viele Berechnungen werden für die Schritte 2 und 3 im schlechtesten<br />
Fall benötigt?<br />
Fachdidaktik Informatik Seite 26
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Primfaktoren berechnen<br />
Bei der Zerlegung einer Fakultät in ihre Primfaktoren ist die entscheidende Grösse die Liste<br />
mit den Primzahlen. Deshalb müssen wir uns zuerst überlegen, wie viele Primzahlen es bei<br />
einem gegebenen geben kann, so dass ist. Leider ist diese Überlegung hier nicht<br />
möglich, weil die Primzahlen ziemlich widerspenstige mathematische Objekte sind, die sehr<br />
unregelmässig verteilt sind. Es ist deshalb bis heute nicht gelungen, eine Funktion ( )<br />
konkret anzugeben, die einem die Anzahl Primzahlen liefert, die kleiner als sind. Allerdings<br />
haben sich zum Glück einige der grössten Mathematiker der Geschichte (Carl Friedrich<br />
Gauss, Adrien-Marie Legendre, Bernhard Riemann etc.) mit dem Problem befasst – wie in<br />
[Gol04] nachzulesen ist – und eine Abschätzung gefunden. Demnach gibt es für grosse<br />
ungefähr<br />
( )<br />
Primzahlen, die kleiner als sind. Mit anderen Worten: Die für unseren<br />
Algorithmus massgebende Länge der Liste der Primzahlen hat für grosse ungefähr die<br />
Länge<br />
( ) .<br />
In fast allen Schleifen, die im Legendre Algorithmus durchlaufen werden müssen, ist die<br />
Länge der Primzahlliste – also<br />
( )<br />
– die entscheidende Grösse. Die einzige Ausnahme bildet<br />
die Schleife für die Berechnung des Exponenten einer Primzahl in der Primzahlzerlegung von<br />
. Im Code sieht die Schleife so aus:<br />
int grenze = (int) Math.floor(Math.log(n) / Math.log(p));<br />
for (int i = 1; i
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Mit dieser Überlegung sieht man dass ( ) ( ( )) ist.<br />
Insgesamt haben wir also:<br />
( )<br />
( )<br />
( )<br />
(<br />
( )<br />
( ) ) ( )<br />
Die Anzahl Schritte des Legendre-Algorithmus‘ wächst also ebenfalls linear mit der Grösse<br />
von an – wie der iterative Algorithmus.<br />
Wieso gibt es denn dann so grosse Unterschiede in der Berechnungszeit?<br />
- Der iterative Algorithmus berechnet in jedem Schritt Ausdrücke von der Form<br />
result = result * (n - i)/(i+1);<br />
Diese Zwischenresultate werden sehr schnell sehr gross und können Zahlen mit<br />
mehreren tausend Stellen umfassen. In jedem Schritt werden derart grosse Zahlen<br />
multipliziert. Das ist sehr aufwändig.<br />
- Der Legendre-Algorithmus dagegen rechnet stets mit Zahlen, die kleiner als sind.<br />
Erst ganz am Schluss, wenn das Schlussresultat aus den Primzahlpotenzen berechnet<br />
wird, entsteht eine grosse Zahl.<br />
Diese beiden Beobachtungen führen zum Schluss, dass die Anzahl der Operationen beider<br />
<strong>Algorithmen</strong> zwar linear von der Grösse von abhängen. Aber offenbar hängt die real<br />
benötigte Rechenzeit des iterativen Algorithmus‘ auch noch von der Grösse der<br />
Zwischenresultate ab. Und diese wachsen bei den Binomialkoeffizienten sehr rasant an.<br />
Fazit<br />
Wir haben am Beispiel der Berechnung von Binomialkoeffizienten gesehen, dass man mit<br />
einer raffinierten Idee einen Algorithmus entwickeln kann, der extrem grosse Zahlen in<br />
relativ kurzer Zeit berechnen kann. Voraussetzung dafür ist allerdings, dass man das<br />
mathematische Problem gut versteht.<br />
Ein weiteres Beispiel, wie mit einer pfiffigen mathematischen Idee ein langsamer<br />
Algorithmus extrem beschleunigt werden kann, wollen wir im nächsten Kapitel studieren.<br />
Fachdidaktik Informatik Seite 28
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Fibonacci-Zahlen berechnen<br />
Die Folge der Fibonacci-Zahlen spielen in der Mathematik, in der Natur, in der Kunst und in<br />
der Architektur eine wichtige Rolle. Dies vor allem deshalb, weil sie eng mit dem goldenen<br />
Schnitt verwandt sind, der in der Antike – und in vielen Kreisen bis heute – als ein Mass für<br />
Ästhetik und Schönheit gilt.<br />
Die Fibonacci Zahlen gehen zurück auf den italienischen Mathematiker Leonardo Pisano,<br />
genannt Fibonacci (von Filio di Bonacci). Er lebte um 1200 herum und war Rechenmeister in<br />
Pisa. Sein grösstes Verdienst war, dass er die Mathematik aus dem arabischen Kulturraum<br />
wieder zurück nach Europa gebracht hat. In seinem Buch „Liber abaci“ erklärte er den<br />
Europäern, die bis dahin Zahlen nur mit römischen Ziffern geschrieben haben, dass man mit<br />
dem Gebrauch der Null ein Ziffernsystem erhält, das schriftliche Addition und Multiplikation<br />
erlaubt.<br />
In diesem Buch überlegte er sich auch, wie sich Kaninchen vermehren. Dies war die<br />
Geburtsstunde der Fibonacci-Folge, die so definiert ist:<br />
Ausgeschrieben:<br />
(Jedes Glied ist die Summe seiner beiden Vorgänger).<br />
In diesem Kapitel werden wir verschiedene Ansätze studieren, wie man algorithmisch ein<br />
berechnen kann, beispielsweise .<br />
Intuitiver Ansatz<br />
Die erste Idee für einen Algorithmus folgt der Definition der Fibonacci-Folge<br />
mit und . Diese besagt, dass man beispielsweise für die Glieder<br />
und benötigt. Für diese wiederum benötigt man und . Und so weiter.<br />
Diesen Ablauf möchten wir mit einer rekursiven Funktion, die sich selber aufruft,<br />
programmieren – ganz analog zum ersten Kapitel.<br />
Aufgabe 27 Schreiben Sie eine Klasse Fibonacci, die eine main-Methode enthält.<br />
Aufgabe 28 Fügen Sie der Klasse Fibonacci eine Methode int fibonacciRek(int n)<br />
bei, die das -te Glied der Fibonacci-Folge berechnet.<br />
Fachdidaktik Informatik Seite 29
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Rechenaufwand<br />
Wenn Sie versuchen, mit dem Algorithmus aus Aufgabe 23 die Zahl zu berechnen, stellen<br />
Sie fest, dass er schon einige Zeit benötigt. Und für ist er bereits einige Sekunden am<br />
Rechnen. Weshalb?<br />
Um diese Frage zu beantworten, müssen wir erneut fragen, wie viele Schritte nötig sind, um<br />
zu berechnen.<br />
Dazu erstellen wir eine Tabelle, die aufzeigt, wie viele Schritte benötigt werden, um die<br />
ersten paar zu berechnen.<br />
Betrachten wir das Beispiel .<br />
Es wird folgendermassen gerechnet:<br />
n Schritte<br />
4 5<br />
5 9<br />
6 15<br />
7 25<br />
8 41<br />
9 67<br />
10 109<br />
11 177<br />
12 287<br />
13 465<br />
14 753<br />
15 1219<br />
Tab. 9<br />
Insgesamt wird die Funktion also 9 Mal aufgerufen.<br />
Fachdidaktik Informatik Seite 30
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Aufgrund der rekursiven Struktur der Methode, setzt sich der Aufwand für die<br />
Berechnung von Zusammen aus dem Aufwand für die Berechnung von und dem<br />
Aufwand für die Berechnung von . Als Formel:<br />
⏟<br />
Mit anderen Worten, der Aufwand für setzt sich zusammen aus den Aufwänden für und<br />
plus 1. Dies ist auch aus der Tabelle ersichtlich ⏟ ⏟ .<br />
Dies bedeutet, dass die Anzahl Rechenschritte Ziemlich genau mit der Zahl selbst<br />
übereinstimmt, die ebenfalls die Summe ihrer beiden Vorgänger ist. Betrachtet man die<br />
Zahlen aus Tab. 9 genauer und stellt sie grafisch dar, stellt man fest, dass die Entwicklung<br />
exponentiell ist (siehe Abbildung 1 unten).<br />
1400<br />
1200<br />
1000<br />
800<br />
600<br />
400<br />
200<br />
0<br />
Schritte<br />
1 2 3 4 5 6 7 8 9 10 11 12<br />
Abbildung 1<br />
Aufgabe 29 Berechnen Sie anhand der obigen Tabelle für wie sich der<br />
Wachstumsfaktor in der Funktion ( ) , die die Anzahl Schritte für ein<br />
bestimmtes angibt, entwickeln. Suchen Sie eine untere Grenze für .<br />
Erkenntnis<br />
Werden die Fibonacci-Zahlen mit einem rekursiven Algorithmus berechnet, so ist die Anzahl<br />
Schritte, die für Berechnung von nötig sind charakterisiert durch<br />
( ) ( )<br />
Mit anderen Worten: Die Anzahl Schritte wächst mit steigendem<br />
ist damit sehr ineffizient.<br />
exponentiell schnell und<br />
Fachdidaktik Informatik Seite 31
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Iterative Berechnung wie von Hand<br />
Wir versuchen deshalb, einen besseren Algorithmus für die Berechnung der Fibonacci-Zahlen<br />
zu finden. Bei dieser Suche überlegen wir uns, wie wir Menschen ohne technische Hilfsmittel<br />
die 15. Fibonacci-Zahl berechnen würden. Aufgrund unserer in diesem Bereich beschränkten<br />
Fähigkeiten, würden wir von Hand bestimmt keinen rekursiven Algorithmus anwenden,<br />
sondern versuchen, die Zahlen „straight forward“ aufzuschreiben. Wir beginnen mit den<br />
ersten zwei Zahlen, schreiben diese auf ein Blatt Papier und betrachten dann für die dritte<br />
Zahl ihre beiden Vorgänger und addieren diese. Für die 4. Zahl betrachten wir erneut die<br />
beiden Vorgänger und addieren diese. Mit diesem Verfahren gelangen wir früher oder<br />
später zu jeder beliebigen Fibonacci-Zahl.<br />
Wenn wir so vorgehen, eliminieren wir einen grossen Nachteil des obigen rekursiven<br />
Ansatzes. Dieser hat nämlich viele Zahlen mehrfach berechnet und damit den Aufwand<br />
aufgeblasen. Mit dem „von Hand“-Ansatz tun wir das nicht. Wir berechnen jede Zahl genau<br />
ein einziges Mal und benützen danach dieses Resultat für die Berechnung der weiteren<br />
Zahlen.<br />
Aufgabe 30 Entwickeln Sie einen Algorithmus int fibonacciIterativ(int n), der –<br />
unter Verwendung eines Arrays – die -te Fibonacci-Zahl berechnet und dabei jeweils<br />
ihre beiden Vorgänger ausnützt. Schreiben Sie ihre Funktion so, dass sie auch für<br />
ein richtiges Resultat liefert.<br />
Rechenaufwand<br />
Der Rechenaufwand dieses Algorithmus‘ findet hauptsächlich in der for-Schleife statt. Diese<br />
wird genau -2 mal durchlaufen, und in jedem Durchgang gibt es genau eine Addition und<br />
eine Zuweisung. Zudem benötigt der Algorithmus, bevor er zur Schleife kommt, zwei<br />
Schritte. Insgesamt ist also in diesem Fall ( ) ( ) ( )<br />
.<br />
In der Schreibweise mit dem Landau-Symbol wird dieser Algorithmus charakterisiert durch<br />
( ) ( )<br />
Mit anderen Worten: Der Rechenaufwand für die Berechnung der -ten Fibonacci-Zahl ist<br />
ungefähr proportional zu .<br />
Fachdidaktik Informatik Seite 32
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Dies ist verglichen mit dem rekursiven Algorithmus ( (<br />
Sprung!<br />
) ) ein massiver Effizienz-<br />
Raffinierte Iteration<br />
Auf den ersten Blick könnte man glauben, dass dieser Algorithmus der schnellst Mögliche ist.<br />
Denn es ist kaum vorstellbar, dass man eine -te Fibonacci-Zahl mit weniger als ungefähr<br />
Schritten berechnen kann. Doch wie wir schon bei den Binomialkoeffizienten gesehen<br />
haben, können mathematische Ideen uns helfen, äusserst pfiffige <strong>Algorithmen</strong> zu schreiben.<br />
Einen solchen Algorithmus wollen wir nun auch für die Berechnung der Fibonacci-Zahlen<br />
entwickeln. Sie werden erstaunt sein!<br />
Überlegung<br />
Wir betrachten ab jetzt nicht mehr nur eine einzige Fibonacci-Zahl, sondern jeweils zwei<br />
zusammen. Es gilt:<br />
Schreibt man dies in Matrix-Form, erhält man:<br />
( ) ( ) ( )<br />
Was bringt uns das? Das Resultat fördert Erstaunliches zu Tage. Betrachtet man nämlich den<br />
Vektor (<br />
) auf der rechten Seite, so entspricht dies gerade dem Vektor ( ). Diesem Vektor<br />
geben wir den Namen . Und entsprechend benennen wir den Vektor ( ) in um.<br />
Damit erhalten wir für den nächsten Vektor<br />
( ) ( ) ( ) ( )<br />
⏟<br />
( ) ( )<br />
Dies entspricht einer impliziten Formel für das Folgenglied !<br />
Denken wir den Gedanken weiter, so erhalten wir<br />
( ) ( ) ( )<br />
Fachdidaktik Informatik Seite 33
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Mit der Idee, die Fibonacci-Folge mit Matrizen zu berechnen, haben wir also erreicht, dass<br />
wir die -te Fibonacci-Zahl direkt mit einer impliziten Formel angeben können. Wir brauchen<br />
keine Vorgänger mehr zu berechnen! So finden wir zum Beispiel für<br />
( ) ( ) ( ) ( ) ( ) ( )<br />
Damit ist .<br />
Allerdings hat die Sache noch einen Haken: Wir müssen ( ) berechnen. Und im<br />
Moment ist noch nicht klar, ob wir damit einen Geschwindigkeitsvorteil herausholen<br />
können.<br />
Matrix-Multiplikation<br />
Die Potenzierung einer Matrix mit dem Exponenten entspricht Multiplikationen mit<br />
sich selbst. Als Formel:<br />
( ) ( ) ( ) ( )<br />
⏟<br />
Für eine Multiplikation gilt:<br />
( ) ( ) ( ) ( )<br />
Und allgemein:<br />
( ) ( ) ( )<br />
Oder mit grafischen Hilfen dargestellt:<br />
( ) ( ) ( )<br />
Um den Eintrag des Resultats zu berechnen, multipliziert man komponentenweise die 1.<br />
Zeile des ersten Faktors mit der 2. Spalte des zweiten Faktors. Diese Anleitung gilt allgemein<br />
für beliebige Matrizen mit Spalten und Zeilen.<br />
Um unser Problem weiter zu untersuchen, benötigen wir als erstes eine Klasse Matrix für<br />
quadratische Matrizen.<br />
Fachdidaktik Informatik Seite 34
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Aufgabe 31 Berechnen Sie<br />
mit Hilfe der Matrix-Multiplikation. Das bedeutet<br />
berechnen Sie ( ) ( ) ( ) und lesen Sie dann aus dem Resultatvektor das<br />
entsprechende Resultat ab.<br />
Aufgabe 32 Schreiben Sie eine Klasse Matrix mit einem Konstruktor public<br />
Matrix(int m), wobei die Zahl die Dimension der quadratischen Matrix angibt.<br />
Schreiben Sie zudem eine Funktion public Matrix multiply(Matrix b), die prüft<br />
ob die Matrix B die gleiche Dimension hat wie die Matrix A und danach die beiden<br />
Matrizen multipliziert.<br />
Schreiben Sie eine Funktion public int getEntry(int row, int col), die den<br />
Eintrag der Matrix an der entsprechenden Stelle liefert.<br />
Schreiben Sie eine Funktion public void setEntry(int row, int col, int<br />
val), die den entsprechenden Eintrag in der Matrix setzt.<br />
Schnelles Potenzieren<br />
Nun hätten wir im Prinzip alle Werkzeuge, um grosse Fibonacci-Zahlen algorithmisch zu<br />
berechnen. Der Algorithmus in Pseudocode, würde so aussehen:<br />
fibonacci(n){<br />
Erzeuge Matrix ( ) -> M<br />
Berechne -> M<br />
Gib den zweiten Eintrag von<br />
}<br />
( ) aus.<br />
Das Problem dieses Algorithmus‘ ist jedoch, dass die Berechnung von aus genau<br />
Multiplikationen mit sich selber besteht. Und jede Multiplikation einer Matrix mit sich<br />
selbst benötigt 8 Schritte – insgesamt also ( ) Schritte. Würden wir<br />
diesen Weg gehen, hätten wir gegenüber dem iterativen Algorithmus nichts gewonnen. Wir<br />
brauchen noch eine neue Idee!<br />
Schnelles Potenzieren mit reellen Zahlen<br />
Wir versuchen, eine Idee zu finden, die uns beim Potenzieren hilft. Als erstes betrachten wir<br />
das Potenzieren von reellen Zahlen. Das reduziert den Schreibaufwand. Danach werden wir<br />
die Idee auf die Matrizen übertragen. Das Ziel dieser Idee ist, die Anzahl Multiplikationen<br />
beim Berechnen von zu reduzieren.<br />
Fachdidaktik Informatik Seite 35
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Betrachten wir beispielsweise , so berechnet man dies normalerweise als . Dies<br />
entspricht drei Multiplikationen. Schreibt man allerdings<br />
plötzlich nur noch 2 Multiplikationen durchführen:<br />
leicht um zu ( ) , so muss man<br />
- Im ersten Schritt berechnet man und merkt sich das Resultat und sagt ihm zum<br />
Beispiel .<br />
- Im zweiten Schritt berechnet man und hat das Resultat bereits gefunden.<br />
Die Einsparung an Multiplikationen in diesem Beispiel rührt daher, dass der Exponent 4<br />
gerade eine Potenz von 2 ist. Damit lässt er sich als Kette von Quadraten schreiben.<br />
Doch wie sieht es aus, wenn man Beispielsweise berechnen will? Würde man diese<br />
Potenz als ⏟ berechnen, müsste man 9 Multiplikationen durchführen. Viel schneller<br />
ist es jedoch, wenn man es umformt zu (( ) ) .<br />
Damit müssen folgende Multiplikationen ausgeführt werden:<br />
Mit diesem Trick sind nur 4 Multiplikationen nötig! Ein ordentlicher Fortschritt gegenüber 9<br />
Multiplikationen.<br />
Aufgabe 33 Berechnen Sie<br />
Aufgabe 34 Berechnen Sie<br />
mit 5 Multiplikationen.<br />
mit möglichst wenig Multiplikationen.<br />
Aufgabe 35 Berechnen Sie auf zwei verschiedene Arten mit jeweils 3<br />
Multiplikationen.<br />
Aufgabe 36 Wie viele Multiplikationen sind mindestens nötig, um<br />
Aufgabe 37 Wie viele Multiplikationen sind mindestens nötig, um<br />
zu berechnen?<br />
zu berechnen?<br />
System für die schnelle Exponentiation<br />
Um ein System für die schnelle Exponentiation zu finden, betrachten wir noch einmal<br />
( ) und schreiben die nötigen Multiplikationen als Kette auf. Man beginnt mit ,<br />
Fachdidaktik Informatik Seite 36
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
multipliziert mit sich selbst und multipliziert dieses Resultat noch einmal mit sich selbst. Man<br />
startet also mit und quadriert das Resultat zwei Mal hintereinander.<br />
Als Kette dargestellt sieht das so aus:<br />
Dabei steht<br />
für Quadrieren.<br />
Betrachten wir das Beispiel . Dies lässt sich schreiben als ( ) und benötigt drei<br />
Multiplikationen. Oder als Kette:<br />
Im Unterschied zur Berechnung von gibt es in dieser Kette nicht nur , sondern am<br />
Schluss muss man einmal noch mit<br />
Multiplizieren.<br />
Die Frage ist nun, ob sich diese Idee so verallgemeinern lässt, so dass man – ohne lange<br />
ausprobieren zu müssen – eine Regel angeben kann, wie sich beispielsweise<br />
in eine Kette von Quadrieren und Multiplizieren verwandeln lässt.<br />
oder<br />
Um dieses Muster zu finden (siehe 0), schreiben wir die obigen Ketten noch leicht um. Jede<br />
Kette beginnt mit einer 1. Diese wird dann sukzessive quadriert und mit<br />
Schreibweise hilft beim Finden des Musters:<br />
multipliziert. Diese<br />
(Kurz: )<br />
: (Kurz: )<br />
: (Kurz: )<br />
Beispiele tabellarisch:<br />
Potenz<br />
Anz. Multiplikationen<br />
( ) Dies entspricht3 Multiplikationen (wenn man erst bei zu<br />
zählen beginnt):<br />
1. mit sich selber -><br />
2. mit sich selber -><br />
3. mit -> Schlussresultat<br />
Oder als Kette (mit 1 beginnend):<br />
( )<br />
3 Multiplikationen.<br />
Als Kette:<br />
( ) 4 Multiplikationen.<br />
Fachdidaktik Informatik Seite 37
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Als Kette:<br />
(( ) )<br />
3 Multiplikationen.<br />
(( ) )<br />
4 Multiplikationen.<br />
Aufgabe 38 Führen Sie die obige Tabelle weiter, suchen Sie eine Regelmässigkeit und<br />
versuchen Sie, diese Regelmässigkeit in Worte zu fassen.<br />
Führt man die obige Tabelle nach dem gleichen Muster wie am Anfang weiter, stellt man<br />
fest, dass die Folgen der und gerade der Binärdarstellung des Exponenten<br />
entspricht, wenn man durch und durch erstetzt.<br />
Beispiel:<br />
(Der tiefgestellte Index 2 deutet die Binärdarstellung an.) Damit heisst<br />
die Kette: oder oder (( ) ) .<br />
Betrachtet man die Ketten – mit 1 beginnend – etwas genauer, stellt man fest, dass die erste<br />
Quadrierung und die erste Multiplikation mit benötigt werden, um aus einer 1 ein zu<br />
erhalten. Diesen Schritt kann man auch weglassen und direkt mit beginnen. Dies entspricht<br />
dem Weglassen der ersten 1 in der Binärdarstellung<br />
Die Berechnung von besteht nun also darin, die Binärdarstellung von 1413 zu finden<br />
und danach die entsprechende Kette von und aufzuschreiben:<br />
1. Binärdarstellung von<br />
2. Erste 1 weglassen:<br />
3. Kette aufschreiben: (((((((( ) ) ) ) ) ) ) )<br />
lässt sich also mit 12 Multiplikationen berechnen!<br />
Aufgabe 39 Erstellen Sie die Kette für die Berechnung von<br />
Exponentiation.<br />
Aufgabe 40 Erstellen Sie die Kette für die Berechnung von<br />
Exponentiation.<br />
mittels schneller<br />
mittels schneller<br />
Damit haben wir nun alle Ideen beisammen, um einen Algorithmus zu entwickeln, mit dem<br />
sich grosse Potenzen schnell berechnen lassen.<br />
Fachdidaktik Informatik Seite 38
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Der Algorithmus besteht aus folgenden Schritten:<br />
binär[]
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Die Anzahl Multiplikationen hängt jedoch nicht alleine von dieser Länge ab, sondern auch<br />
noch von der Anzahl der Einsen in der Binärdarstellung. Dabei gilt, dass für jede 1 in der<br />
Binärdarstellung zwei Multiplikationen nötig sind (einmal Quadrieren, einmal mit<br />
multiplizieren). Für jede 0 in der Binärdarstellung schlägt eine Multiplikation zu Buche,<br />
nämlich ein einfaches Quadrieren.<br />
Damit lässt sich nun die Anzahl<br />
( ) der Multiplikationen berechnen:<br />
( ) ⏟ ( )<br />
⏟ ( )<br />
Nachdem es in der Binärdarstellung von höchstens ( ) viele geben kann, erhält<br />
man insgesamt<br />
( ) ( ) ( ( ))<br />
Die Frage ist nun noch, ob dieses Verfahren optimal ist? Die Antwort ist klar: Nein. Am<br />
besten sieht man das am Beispiel . Unser Verfahren liefert:<br />
. Dies entspricht 6 Multiplikationen. Das gleiche Resultat<br />
könnte man aber auch erzielen, indem man mit 2 Multiplikationen zuerst berechnet<br />
und danach mit dem obigen Verfahren in drei Multiplikationen berechnet. Damit<br />
benötigte man also insgesamt nur 5 Multiplikationen (siehe [Don98] S.462). Doch der<br />
Unterschied ist auch für grössere sehr gering. Und immerhin benötigt unser Verfahren<br />
weniger als Schritte, um Potenzen mit einem grossen Exponenten zu berechnen.<br />
Das soeben entwickelte Verfahren, um Potenzen mit grossen Exponenten zu berechnen,<br />
heisst in der Literatur binäre Exponentiation oder auch Square&Multiply (Square, engl. für<br />
Quadrieren). Leider können wir hier nicht für uns beanspruchen, etwas Neues gefunden zu<br />
haben. Denn dieser Algorithmus wurde schon ca. 200 v. Chr. in Indien entdeckt und wurde<br />
erstmals erwähnt in einem Werk namens „Chandah-sûtra“.<br />
Nun mit Matrizen<br />
Das Verfahren der binären Exponentiation funktioniert auch mit Matrizen, denn bei allen<br />
obigen Überlegungen haben wir uns nur mit dem Exponenten befasst. Nirgends gibt es eine<br />
Einschränkung in Bezug auf die Basis – also darf die Basis auch eine Matrix sein. Damit sind<br />
wir nun in der Lage, einen Algorithmus zu entwickeln, um die -te Fibonacci-Zahl mit<br />
( ) ( ) direkt zu berechnen. Für die Berechnung von ( ) werden wir<br />
Fachdidaktik Informatik Seite 40
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
selbstverständlich die binäre Exponentiation benützen, um diese Potenzen schnell<br />
durchzuführen.<br />
Dazu benötigen wir als erstes die folgende Aufgabe.<br />
Aufgabe 43 Erweitern Sie die Klasse Matrix um die Methode Matrix fastExp(Matrix<br />
a, int n), die mit Hilfe der schnellen Exponentiation berechnet mit .<br />
Wenn Sie die Aufgabe 43 richtig gelöst haben, verfügen Sie nun über eine komplette Klasse,<br />
um die -te Fibonacci-Zahl direkt zu berechnen. Ob der Algorithmus auch wirklich schnell ist,<br />
werden wir im nächsten Abschnitt prüfen. Ob er auch korrekt ist, werden Sie mit der<br />
folgenden Aufgabe feststellen.<br />
Aufgabe 44 Ergänzen Sie die Klasse Fibonacci um eine Methode int<br />
fibonacciFast(int n), die als Input einen natürliche Zahl erhält und als Output<br />
die -te Fibonacci Zahl zurückgibt.<br />
Prüfen Sie ihre neue Methode, indem Sie aus der main-Methode heraus einige<br />
Fibonacci-Zahlen berechnen.<br />
Wenn Sie ihre neue Methode ausgiebig getestet haben und dabei auch versucht haben, die<br />
entsprechenden Fibonacci-Zahlen zu grossen zu berechnen, werden Sie festgestellt haben,<br />
dass die Resultate nicht korrekt, resp. negativ sind. Dies hat damit zu tun, dass der Bereich<br />
von Zahlen, die mit einem Integer int dargestellt werden können, begrenzt ist, und<br />
Fibonacci-Zahlen sehr schnell sehr gross werden können. Dieses Problem lässt sich beheben,<br />
indem im gesamten Code alle int-Variablen, die grosse Werte annehmen können, durch<br />
Objekte vom Typ BigInteger ersetzt werden. Diese Objekte können (fast) beliebig grosse<br />
Zahlen verarbeiten und darstellen. Im Anhang findet sich die abgeänderte Klasse<br />
Fibonacci, die in der Lage ist, sehr grosse Zahlen zu berechnen.<br />
Geschwindigkeitssprung<br />
Schliesslich bleibt noch zu prüfen, was wir mit dieser Idee an Effizienz gewonnen haben.<br />
Schon der iterative Ansatz zur Berechnung einer Fibonacci-Zahl brachte einen drastischen<br />
Gewinn gegenüber der Rekursiven Berechnung. Mit der Idee, die Berechnung mit Matrizen<br />
anzustellen, können wir noch einmal eine deutliche Beschleunigung feststellen.<br />
Im letzten Absatz haben wir festgehalten, dass die Anzahl schnelle Exponentiation<br />
( ) ( ) Multiplikationen durchführt (im schlimmsten Fall). Wir müssen<br />
Fachdidaktik Informatik Seite 41
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
also noch überlegen, mit wie vielen Berechnungsschritten die Multiplikation zweier -<br />
Matrizen zu Buche schlägt.<br />
Wir zählen die Operationen, die nötig sind, um zwei -Matrizen zu multiplizieren. Dabei<br />
betrachten wir zuerst, wie viele Operationen durchgeführt werden müssen, um einen<br />
einzigen Eintrag in der Resultat-Matrix zu berechnen:<br />
- Jeder Eintrag einer Zeile wird mit einem Eintrag einer Spalte multipliziert <br />
Multiplikationen.<br />
- Die Resultate dieser Multiplikationen werden miteinander addiert <br />
Additionen.<br />
Zusammen haben wir also für die Berechnung eines einzigen Eintrages im Resultat<br />
Operationen.<br />
Insgesamt müssen<br />
Einträge berechnet werden. Zusammen erhalten wir also<br />
( )<br />
Operationen.<br />
In unserem Fall der<br />
-Matrix ergibt dies 12 Operationen für eine Multiplikation. Diese<br />
Anzahl bleibt für jede Multiplikation gleich, da die Grösse der Matrix nicht ändert. Was sich<br />
für grosse<br />
werden.<br />
ändert, sind allein die Einträge in der Matrix, die relativ schnell relativ gross<br />
Verrechnen wir nun die schnelle Exponentiation und die Matrix-Multiplikation, erhalten wir<br />
die Anzahl Schritte<br />
berechnen.<br />
( ), um mit Hilfe der Matrix das -te Glied der Fibonacci-Folge zu<br />
( ) ⏟ ( )<br />
⏟ ( ) ( ( ))<br />
Die Idee mit der Matrix hat sich also ausbezahlt. Das ist von den drei hier vorgestellten<br />
<strong>Algorithmen</strong> mit Abstand der schnellste, um die Fibonacci-Zahl<br />
zu berechnen.<br />
Fachdidaktik Informatik Seite 42
10<br />
50<br />
90<br />
130<br />
170<br />
210<br />
250<br />
290<br />
330<br />
370<br />
410<br />
450<br />
490<br />
Shritte<br />
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
In der nebenstehenden Grafik<br />
ist der Effizienzsprung<br />
offensichtlich. Die Schrittzahl<br />
des rekursiven Algorithmus‘ ist<br />
nicht dargestellt. Denn dieser<br />
wächst exponentiell und<br />
übertrifft die anderen beiden<br />
bei weitem. In der Grafik sieht<br />
man, dass der iterative<br />
1200<br />
1000<br />
800<br />
600<br />
400<br />
200<br />
0<br />
n<br />
Algorithmus für kleine bis ca. 75 schneller ist. Für grössere wächst jedoch die Anzahl<br />
Schritte des Matrix-Algorithmus‘ deutlich weniger schnell.<br />
Iterativ<br />
Matrix<br />
Berechnung mit der Formel von Moivre-Binet<br />
Wie sie vielleicht aus dem Mathematik-Unterricht wissen, lassen sich die Fibonacci-Zahlen<br />
auch direkt berechnen. Die Formel heisst:<br />
√ (( √ √ ) ( ) )<br />
Sie wurde von den französischen Mathematikern Abraham de Moivre und Jacques Philippe<br />
Marie Binet unabhängig voneinander in den Jahren 1730 und 1843 entdeckt. Die Frage ist<br />
nun, weshalb wir nicht einen Algorithmus schreiben, der das -te Fibonacci-Glied direkt mit<br />
der Formel berechnet und so viel Aufwand sparen kann.<br />
Das Problem dabei ist, dass diese Idee zu keinem Geschwindigkeitsgewinn führen würde.<br />
Denn der Algorithmus müsste ja ebenfalls zwei Mal eine Potenz mit im Exponenten<br />
ausrechnen. Dies kann aber nicht schneller geschehen, als mit dem eben entwickelten<br />
Algorithmus. Zudem müsste der Algorithmus mit der reellen Zahl √ rechnen, was für<br />
Computer ein grosses Problem darstellt, da mit reellen Zahlen nicht exakt gerechnet werden<br />
kann.<br />
Der von uns entwickelte Algorithmus ist damit effektiv der schnellst mögliche Weg, um mit<br />
einem Computer die -te Fibonacci-Zahl zu berechnen.<br />
Fachdidaktik Informatik Seite 43
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Fazit<br />
Rekursion ist in vielen Bereichen der Algorithmik ein wichtiges Instrument, um Probleme zu<br />
lösen. Allerdings hat der rekursive Ansatz oft den Nachteil, dass er viele – oft unnötige –<br />
Berechnungsschritte durchführen muss. In dieser Unterrichtseinheit haben wir gesehen,<br />
dass man mit Ideen aus der Mathematik Berechnungsprobleme, die auf den ersten Blick<br />
nach einer Rekursion aussehen, deutlich effizienter lösen kann.<br />
Vor allem das Beispiel der Fibonacci-Zahlen hat gezeigt, dass es sehr hilfreich sein kann, in<br />
die mathematische Trickkiste zu greifen und damit einen pfiffigen Algorithmus zu<br />
entwickeln. Voraussetzung dafür ist natürlich, dass man sich traut, das Problem tiefgreifend<br />
zu analysieren und die mathematischen Ideen umzusetzen.<br />
Dieses Vorgehen ist typisch für die Informatik: Der intuitive Ansatz funktioniert zwar. Mit viel<br />
Ideenreichtum und Kreativität gelingt es einem jedoch, die Berechnungszeit radikal zu<br />
verkürzen. Ein äusserst aktuelles Problem, das noch darauf wartet, effizient gelöst zu<br />
werden, ist die Zerlegung einer sehr grossen Zahl in ihre zwei Primfaktoren. Bisher benötigen<br />
alle Versuche, dieses Problem zu lösen, sehr viel Rechenzeit. Zum Glück! Denn ein grosser<br />
Teil der Datensicherheit im Internet oder beim Online-Banking beruht darauf, dass bisher<br />
niemand einen schnellen Algorithmus gefunden hat. Aber wer weiss: Vielleicht findet sich<br />
irgendwann jemand, der mit einer sehr pfiffigen Idee ein schnelles Verfahren findet. Im<br />
Prinzip ist es – mindestens nach heutigem Wissensstand – möglich!<br />
Fachdidaktik Informatik Seite 44
Mentorierte Arbeit <strong>Pfiffige</strong> <strong>Algorithmen</strong> Emil Müller<br />
Literaturverzeichnis<br />
[Möh08] MÖHRING, ROLF, OELLRICH, M.: Das Sieb des Eratosthenes: Wie schnell kann man<br />
eine Primzahl berechnen? In Vöcking, Berthold, Alt, H. et al. (eds.) : Taschenbuch<br />
der <strong>Algorithmen</strong>. Springer-Verlag, Berlin Heidelberg (2008). P. 127-138<br />
[Gol04] GOLDFELD, DORIAN: The elementary proof of the prime number theorem: an<br />
historical perspective. In Chudnovsky, David, et al. (eds.) : Number theory: New<br />
York seminar 2003. Springer, New York (2004). P. 179-192<br />
[Woh10] WOHLGEMUTH, MARTIN: Berechnung grosser Binomialkoeffizienten. In :<br />
Mathematisch für fortgeschrittene Anfänger. Spektrum Akademischer Verlag,<br />
Heidelberg (2010)<br />
[Hro07] HROMKOVIC, JURAJ: Theoretische Informatik. B.G. Teubner Verlag, Wiesbaden<br />
(2007)<br />
Fachdidaktik Informatik Seite 45
Anhänge<br />
I. Lösungen zu den Aufgaben<br />
II. Gesamte Klasse für die Binomialkoeffizienten-Berechnung<br />
III. Klassen für die Fibonacci-Zahlen<br />
Anhänge I
I. Lösungen zu den Aufgaben<br />
Aufgabe 1<br />
public class BinKoeff {<br />
public static void main(String[] args) {<br />
int n=5;<br />
int k=1;<br />
System.out.println(n+" tief "+ k +" = "+ binKoeffRek(n, k));<br />
}<br />
}<br />
public static int binKoeffRek(int n, int k) {<br />
if (k == 0 || k == n) {<br />
return 1;<br />
} else {<br />
return binKoeffRek(n - 1, k - 1) + binKoeffRek(n - 1, k);<br />
}<br />
}<br />
Aufgabe 2<br />
Diese Methode liefert die Outputs: Beispielen ( ) ( ) ( ) ( ) ( ) ( ) ( )<br />
- 5 tief 1 = 5<br />
- 5 tief 0 = 1<br />
- 6 tief 3 = 20<br />
- 0 tief 0 = 1<br />
- 6 tief 6 = 1<br />
- 11 tief 7 = 330<br />
- 45 tief 6 = 9366819<br />
Letztere Zahl besagt übrigens, wie viele mögliche verschiedene Lottoscheine es in der<br />
Schweiz geben kann.<br />
Wie man mit dem Pascalschen Dreieck nachprüfen kann, liefert der Algorithmus<br />
immer die richtige Zahl.<br />
Anhänge II
Aufgabe 3<br />
/**<br />
* Diese Methode erzeugt einen zweidimensionalen Array mit<br />
unterschiedlichen<br />
* Zeilen-Längen und berechnet die Werte des Pascalschen Dreiecks. Dabei<br />
* gilt: pascal[n][k] entsrpicht dem Wert von "n tief k".<br />
*<br />
* @param n<br />
* @param k<br />
* @return<br />
*/<br />
public static int binKoeffPascal(int n, int k) {<br />
int[][] pascal = new int[n + 1][];<br />
for (int i = 0; i
( )<br />
( )<br />
6 33 36 0.92<br />
7 42 49 0.86<br />
8 52 64 0.81<br />
9 63 81 0.78<br />
10 75 100 0.75<br />
20 250 400 0.63<br />
30 525 900 0.58<br />
50 1375 2500 0.55<br />
75 3000 5625 0.53<br />
100 5250 10000 0.50<br />
1000 502500 1000000 0.50<br />
10000 50025000 100000000 0.50<br />
100000 5000250000 1E+10 0.50<br />
Tab. 11<br />
Offenbar scheint der Wert<br />
( )<br />
gegen den Wert zu konvergieren.<br />
Dies überprüfen wir nun analytisch:<br />
( )<br />
⏟<br />
Die Beobachtung wird also durch die analytische Überlegung gestützt. Je grösser<br />
wird, desto eher nähert sich ( ) dem Wert an.<br />
Aufgabe 6<br />
Man berechnet<br />
Aufgabe 7 Wenn ( ) ( ) liegen soll, muss<br />
( )<br />
gelten.<br />
( )<br />
( ) ( )<br />
Aufgabe 8 Der grösste Exponent ist 3 in deshalb berechnen wir:<br />
Die Funktion liegt also in ( ).<br />
Aufgabe 9<br />
Es gilt also: ( ). Die Konstante .<br />
Anhänge IV
Aufgabe 10 ( )<br />
Deshalb: ( )<br />
Aufgabe 11<br />
Deshalb: ( ), für alle . .<br />
Aufgabe 12<br />
Wir haben uns überlegt, dass sich die Funktion für jede Zeile des Pascalschen<br />
Dreiecks zwei Mal selbst aufruft. Damit kommt man auf ungefähr Aufrufe der<br />
Funktion und somit auch auf eine Grössenordnung von .<br />
Damit ist ( ) ( ).<br />
Aufgabe 13<br />
public static int binKoeffIterativ(int n, int k) {<br />
int result = 1;<br />
for (int i = 0; i < k; i++) {<br />
result = result * (n - i)/(i+1);<br />
}<br />
return result;<br />
}<br />
Aufgabe 14 Wenn<br />
Aufgabe 15 Wenn<br />
Aufgabe 16 Jeder 2. Faktor ist durch 2 teilbar. Also bei sind es 4.<br />
Aufgabe 17 Jeder 4. Faktor ist durch 4 teilbar. Bei sind es 2.<br />
Aufgabe 18 ⌊ ⌋ ( auf die nächst kleinere ganze Zahl abgerundet)<br />
Aufgabe 19 ⌊<br />
⌋<br />
Aufgabe 20<br />
3 erhält als Exponent ⌊ ⌋ ⌊ ⌋<br />
5 erhält als Exponent ⌊ ⌋<br />
Anhänge V
7 erhält als Exponent ⌊ ⌋<br />
11 erhält als Exponent ⌊ ⌋<br />
Zusammen:<br />
Aufgabe 21 ( ) ⌊ ⌋ ⌊ ⌋ ⌊ ⌋ ⌊ ⌋ 4 Summanden<br />
Aufgabe 22<br />
( ) ⌊ ⌋ ⌊ ⌋ ⌊ ⌋ ⌊ ⌋ ⌊ ⌋ ⌊ ⌋ ⌊ ⌋ ⌊ ⌋<br />
⌊ ⌋ ⌊ ⌋ 10 Summanden.<br />
Aufgabe 23 Nach den beiden obigen Aufgaben sieht man, dass die Anzahl Summanden<br />
gerade dem Wert ( ) entspricht. Also hat ( ) genau ( )<br />
Summanden.<br />
Aufgabe 24 Allgemein gilt ( ) hat ( ) Summanden.<br />
Aufgabe 25<br />
public static int calculatePower(int p, int n) {<br />
int pow = 0;<br />
// log(n)zur Basis p lässt sich berechnen mit ln(n)/ln(p)<br />
for (int i = 1; i
Aufgabe 27 siehe Aufgabe 27<br />
Aufgabe 28<br />
public class Fibonacci {<br />
public static void main(String[] args) {<br />
System.out.println(fibonacciRek(30) + " ");<br />
}<br />
}<br />
public static int fibonacciRek(int n) {<br />
if (n == 1 || n == 2) {<br />
return 1;<br />
}<br />
return fibonacciRek(n - 1) + fibonacciRek(n - 2);<br />
}<br />
Aufgabe 29<br />
Aus der Tabelle<br />
n Schritte Wachst.faktor<br />
4 5<br />
5 9 1.8<br />
6 15 1.66666667<br />
7 25 1.66666667<br />
8 41 1.64<br />
9 67 1.63414634<br />
10 109 1.62686567<br />
11 177 1.62385321<br />
12 287 1.62146893<br />
13 465 1.62020906<br />
14 753 1.61935484<br />
15 1219 1.6188579<br />
wird ersichtlich, dass sich die Faktoren, um die die Schritte zunehmen, laufend<br />
verkleinern. Allerdings wird die Abnahme mit steigendem kleiner. Als Grenzwert<br />
zeichnet sich eine Zahl ab.<br />
Anhänge VII
Aufgabe 30<br />
public static int fibonacciIterativ(int n) {<br />
//Wenn n 0 && col > 0 && row
}<br />
*/<br />
public int getEntry(int row, int col) throws ArrayIndexOutOfBoundsException {<br />
if (row > 0 && col > 0 && row
(( ) )<br />
(4 Multiplikationen)<br />
(( ) )<br />
(5 Multiplikationen)<br />
(( ) )<br />
(4 Multiplikationen)<br />
etc.<br />
(( ) )<br />
(5 Multiplikationen)<br />
Regelmässigkeit: Die Kette mit den Zeichen „<br />
“ und „ “ ist identisch mit der<br />
Binärdarstellung des Exponenten, wenn man durch und 0 durch ersetzt.<br />
Aufgabe 39 (((( ) ) ) )<br />
Aufgabe 40 (((((((( ) ) ) ) ) ) ) )<br />
Aufgabe 41<br />
/**<br />
* Erzeugt einen Array, der die Binärdarstellung von n enthält. Bedingung: n>=0.<br />
*/<br />
public static int[] binary(int n) {<br />
int[] result = { 0 };<br />
if (n > 0) {<br />
//Länge des Arrays ist floor(log_2(n))+1<br />
result = new int[(int) (Math.log(n) / Math.log(2)) + 1];<br />
int rest = 1;<br />
int index = 1;<br />
while (n != 0) {<br />
rest = n % 2;<br />
result[result.length - index++] = rest;<br />
n = n / 2;<br />
}<br />
}<br />
return result;<br />
}<br />
Aufgabe 42<br />
/**<br />
* Berechnet mit Hilfe der schnellen Exponentiation die Potenz b^e.<br />
* @param b<br />
* @param e<br />
* @return<br />
*/<br />
public static long fastExp(int b, int e) {<br />
int[] binaryOfExp = binary(e);<br />
//Startwert für die schnelle Exponentiation<br />
long res = b;<br />
//Das erste Bit der Binärdarstellung wird ignoriert.<br />
for(int i=1;i
**<br />
* Berechnet mit Hilfe der schnellen Exponentiation die Potenz b^e.<br />
* @param b<br />
* @param e<br />
* @return<br />
*/<br />
public static BigInteger fastExp(int b, int e) {<br />
int[] binaryOfExp = binary(e);<br />
//Startwert für die schnelle Exponentiation<br />
BigInteger res = BigInteger.valueOf(b);<br />
//Das erste Bit der Binärdarstellung wird ignoriert.<br />
for(int i=1;i
**<br />
* Berechnet das n-te Fibonacci-Glied mit schneller Matrix-Exponentiation<br />
*<br />
* @param n<br />
* @return<br />
*/<br />
public static int fibonacciFast(int n) {<br />
Matrix fib = getFibonacciMatrix();<br />
fib = fib.fastExp(n - 1);<br />
return fib.getEntry(2, 1) + fib.getEntry(2, 2);<br />
}<br />
/**<br />
* Berechnet die Fibonacci-Matrix |1 1| |1 0|<br />
*<br />
* @return<br />
*/<br />
public static Matrix getFibonacciMatrix() {<br />
Matrix res = new Matrix(2);<br />
res.setEntry(1, 1, 1);<br />
res.setEntry(1, 2, 1);<br />
res.setEntry(2, 1, 1);<br />
res.setEntry(2, 2, 0);<br />
return res;<br />
}<br />
/**<br />
* Erzeugt einen Array, der die Binärdarstellung von n enthält. Bedingung:<br />
* n>=0.<br />
*/<br />
public static int[] binary(int n) {<br />
int[] result = { 0 };<br />
if (n > 0) {<br />
// Länge des Arrays ist floor(log_2(n))+1<br />
result = new int[(int) (Math.log(n) / Math.log(2)) + 1];<br />
int rest = 1;<br />
int index = 1;<br />
while (n != 0) {<br />
rest = n % 2;<br />
result[result.length - index++] = rest;<br />
n = n / 2;<br />
}<br />
}<br />
return result;<br />
}<br />
Anhänge XII
II.<br />
Gesamte Klasse für die Binomialkoeffizienten-Berechnung<br />
BinKoeff.java<br />
package binomialkoeff;<br />
import java.math.BigInteger;<br />
public class BinKoeffOpt {<br />
public static void main(String[] args) {<br />
int n = 10000;<br />
int k = 5000;<br />
long start = System.nanoTime(); // Zeit beim Start<br />
binKoeffIterativ(n, k);<br />
System.out.println("Iterativ: " + (System.nanoTime() - start) / 1.E9<br />
+ " sec.");<br />
int[] primes = getPrimes(n);<br />
start = System.nanoTime();<br />
binKoeffLegendre(n, k, primes);<br />
System.out.println("Legendre: " + (System.nanoTime() - start) / 1.E9<br />
+ " sec.");<br />
}<br />
public static int binKoeffRek(int n, int k) {<br />
if (k == 0 || k == n) {<br />
return 1;<br />
} else {<br />
return binKoeffRek(n - 1, k - 1) + binKoeffRek(n - 1, k);<br />
}<br />
}<br />
/**<br />
* Iterative Methode, um den Binomialkoeffizient "n tief k" zu berechnen.<br />
* Dabei wird das Resultat in BigInteger gespeichert. Dieses<br />
* Objekt kann fast beliebig grosse ganze Zahlen speichern.<br />
*<br />
* @param n<br />
*<br />
* @param k<br />
*<br />
* @return BigInteger<br />
*/<br />
public static BigInteger binKoeffIterativ(int n, int k) {<br />
BigInteger result = new BigInteger("1");<br />
for (int i = 0; i < k; i++) {<br />
result = result.multiply(new BigInteger(Integer.toString(n - i)))<br />
.divide(new BigInteger(Integer.toString(i + 1)));<br />
}<br />
return result;<br />
}<br />
/**<br />
* Methode, um mit dem Satz von Legendre "n tief k" zu berechnen. Dabei wird<br />
* das Resultat in BigInteger gespeichert. Dieses Objekt kann<br />
* fast beliebig grosse ganze Zahlen speichern.<br />
*<br />
* @param n<br />
*<br />
* @param k<br />
*<br />
* @param primes<br />
* Integer Array, der die Liste aller<br />
* Primzahlen hält, die kleiner als n sind.<br />
* @return BigInteger<br />
*/<br />
public static BigInteger binKoeffLegendre(int n, int k, int[] primes) {<br />
BigInteger res = new BigInteger("1");<br />
int[] exponenten = new int[primes.length];<br />
primeFakt(primes, exponenten, 1, n);<br />
primeFakt(primes, exponenten, -1, k);<br />
primeFakt(primes, exponenten, -1, n - k);<br />
for (int i = 0; i < exponenten.length; i++) {<br />
if (exponenten[i] != 0) {<br />
res = res.multiply(new BigInteger(Long.toString((long) (Math<br />
.pow(primes[i], exponenten[i])))));<br />
}<br />
}<br />
return res;<br />
}<br />
/**<br />
* Berechnet die Primfaktorzerlegung von k!. Die Exponenten der einzelnen<br />
* Primzahlen werden zu den bereits vorher berechneten Exponenten in<br />
* exponenten addiert (z >0) oder subtrahiert (z
*<br />
* @param primes<br />
* Array, der die Primzahlen < n enthält.<br />
* @param exponenten<br />
* Array, der die Exponenten der<br />
* Primzahlzerlegung hält. Dieser Array wird durch diese Methode<br />
* verändert.<br />
* @param z<br />
* Ist z>0, werden die berechneten Exponenten<br />
* zu den bestehenden Exponenten addiert. Ist z
}<br />
}<br />
pow = pow + quot;<br />
quot = (int) quot / p;<br />
}<br />
return pow;<br />
Anhänge XV
III.<br />
Klassen für die Fibonacci-Zahlen<br />
Fibonacci.java<br />
package fibonacci;<br />
import java.math.BigInteger;<br />
public class Fibonacci {<br />
public static void main(String[] args) {<br />
int n = 43;<br />
}<br />
System.out.println("Rekursiv: " + fibonacciRek(n));<br />
start=System.currentTimeMillis();<br />
System.out.println("Iterativ: " + fibonacciIterativ(n));<br />
start=System.currentTimeMillis();<br />
System.out.println("Schnelle Exp. : " + fibonacciFast(n));<br />
/**<br />
* Berechnet das n-te Fibonacci-Glied mittels Rekursion<br />
*<br />
* @param n<br />
* @return<br />
*/<br />
public static int fibonacciRek(int n) {<br />
if (n == 1 || n == 2) {<br />
return 1;<br />
}<br />
return fibonacciRek(n - 1) + fibonacciRek(n - 2);<br />
}<br />
/**<br />
* Berechnet das n-te Fibonacci-Glied iterativ<br />
*<br />
* @param n<br />
* @return<br />
*/<br />
public static int fibonacciIterativ(int n) {<br />
// Wenn n
**<br />
* Berechnet das n-te Fibonacci-Glied mittels schneller<br />
* Matrix-Exponentiation<br />
*<br />
* @param n<br />
* @return BigInteger<br />
*/<br />
public static BigInteger fibonacciFastBigInt(int n) {<br />
MatrixBigInt fib = getFibonacciMatrixBigInt();<br />
fib = fib.fastExp(n - 1);<br />
return fib.getEntry(2, 1).add(fib.getEntry(2, 2));<br />
}<br />
/**<br />
* Berechnet die Fibonacci-Matrix |1 1| |1 0| Die Werte in der Matrix sind<br />
* BigInteger<br />
*<br />
* @return MatrixBigInteger<br />
*/<br />
public static MatrixBigInt getFibonacciMatrixBigInt() {<br />
MatrixBigInt res = new MatrixBigInt(2);<br />
res.setEntry(1, 1, BigInteger.valueOf(1));<br />
res.setEntry(1, 2, BigInteger.valueOf(1));<br />
res.setEntry(2, 1, BigInteger.valueOf(1));<br />
res.setEntry(2, 2, BigInteger.valueOf(0));<br />
return res;<br />
}<br />
/**<br />
* Berechnet das n-te Fibonacci-Glied mit schneller Matrix-Exponentiation<br />
*<br />
* @param n<br />
* @return<br />
*/<br />
public static int fibonacciFast(int n) {<br />
Matrix fib = getFibonacciMatrix();<br />
fib = fib.fastExp(n - 1);<br />
return fib.getEntry(2, 1) + fib.getEntry(2, 2);<br />
}<br />
/**<br />
* Berechnet die Fibonacci-Matrix |1 1| |1 0|<br />
*<br />
* @return<br />
*/<br />
public static Matrix getFibonacciMatrix() {<br />
Matrix res = new Matrix(2);<br />
res.setEntry(1, 1, 1);<br />
res.setEntry(1, 2, 1);<br />
res.setEntry(2, 1, 1);<br />
res.setEntry(2, 2, 0);<br />
return res;<br />
}<br />
}<br />
/**<br />
* Erzeugt einen Array, der die Binärdarstellung von n enthält. Bedingung:<br />
* n>=0.<br />
*/<br />
public static int[] binary(int n) {<br />
int[] result = { 0 };<br />
if (n > 0) {<br />
// Länge des Arrays ist floor(log_2(n))+1<br />
result = new int[(int) (Math.log(n) / Math.log(2)) + 1];<br />
int rest = 1;<br />
int index = 1;<br />
while (n != 0) {<br />
rest = n % 2;<br />
result[result.length - index++] = rest;<br />
n = n / 2;<br />
}<br />
}<br />
return result;<br />
}<br />
Anhänge XVII
Matrix.java<br />
package fibonacci;<br />
import java.math.BigInteger;<br />
public class Matrix {<br />
private int[][] mat;<br />
private int dim;<br />
/**<br />
* Konstruktor für die quadratische Matrix mit Dimension m.<br />
*<br />
* @param m<br />
*/<br />
public Matrix(int m) {<br />
dim = m;<br />
mat = new int[m][m];<br />
}<br />
/**<br />
* Setzt das Matrix-Element mat_(row,col) auf den Wert val.<br />
*<br />
* @param row<br />
* @param col<br />
* @param val<br />
*/<br />
public void setEntry(int row, int col, int val) {<br />
if (row > 0 && col > 0 && row 0 && row
* @param n<br />
* @return<br />
*/<br />
public Matrix fastExp(int n) {<br />
Matrix result = new Matrix(this.dim);<br />
// Matrix kopieren<br />
for (int row = 1; row
MatrixBigInt.java<br />
package fibonacci;<br />
import java.math.BigInteger;<br />
public class MatrixBigInt {<br />
private BigInteger[][] mat;<br />
private int dim;<br />
/**<br />
* Konstruktor für die quadratische Matrix mit Dimension m.<br />
*<br />
* @param m<br />
*/<br />
public MatrixBigInt(int m) {<br />
dim = m;<br />
mat = new BigInteger[m][m];<br />
}<br />
/**<br />
* Setzt das Matrix-Element mat_(row,col) auf den Wert val.<br />
*<br />
* @param row<br />
* @param col<br />
* @param val<br />
*/<br />
public void setEntry(int row, int col, BigInteger val) {<br />
if (row > 0 && col > 0 && row 0 && row
}<br />
}<br />
int[] binaryOfExp = Fibonacci.binary(n);<br />
// Das erste Bit der Binärdarstellung wird ignoriert.<br />
for (int i = 1; i < binaryOfExp.length; i++) {<br />
// Quadrieren<br />
result = result.multiply(result);<br />
if (binaryOfExp[i] == 1) {<br />
// Mit sich selber multiplizieren<br />
result = result.multiply(this);<br />
}<br />
}<br />
return result;<br />
}<br />
/**<br />
* Gibt die Matrix in lesbarer Form an der Konsole aus.<br />
*/<br />
public void printMatrix() {<br />
for (int row = 1; row