03.10.2014 Aufrufe

Pfiffige Algorithmen - ABZ

Pfiffige Algorithmen - ABZ

Pfiffige Algorithmen - ABZ

MEHR ANZEIGEN
WENIGER ANZEIGEN

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 &lt; 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

Hurra! Ihre Datei wurde hochgeladen und ist bereit für die Veröffentlichung.

Erfolgreich gespeichert!

Leider ist etwas schief gelaufen!