Fibonacci und Rekursion
Fibonacci Zahl
đ Was ist die Fibonacci-Zahl bzw. -Reihe?
Die Fibonacci-Reihe ist eine unendliche Folge von Zahlen, bei der jede Zahl die Summe der zwei vorhergehenden Zahlen ist.
- Die Reihe beginnt mit
0und1.- Danach wird jede neue Zahl durch die Addition der letzten beiden Zahlen berechnet.
Die ersten Berechnungen der Fibonacci-Zahlen sehen wie folgt aus:
- Start:
F(0) = 0,F(1) = 1- NĂ€chste Zahl:
F(2) = F(1) + F(0) = 1 + 0 = 1- NĂ€chste Zahl:
F(3) = F(2) + F(1) = 1 + 1 = 2- NĂ€chste Zahl:
F(4) = F(3) + F(2) = 2 + 1 = 3- NĂ€chste Zahl:
F(5) = F(4) + F(3) = 3 + 2 = 5- NĂ€chste Zahl:
F(6) = F(5) + F(4) = 5 + 3 = 8- NĂ€chste Zahl:
F(7) = F(6) + F(5) = 8 + 5 = 13Die ersten Zahlen der Fibonacci-Reihe sind also:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, âŠFormel:
F(0) = 0F(1) = 1- FĂŒr
n â„ 2:F(n) = F(n-1) + F(n-2)Anwendungen der Fibonacci-Zahlen:
Mathematik: Fibonacci-Zahlen tauchen in der Zahlentheorie und Kombinatorik auf. Sie haben Verbindungen zu Teilbarkeitsregeln, goldenem Schnitt und Pascalschem Dreieck.
Informatik: Fibonacci-Zahlen werden in Algorithmen und Datenstrukturen verwendet, wie etwa in der Fibonacci-Suche und Fibonacci-Heaps. Sie sind auch in der Dynamischen Programmierung von Bedeutung.
Biologie: In der Natur sind Fibonacci-Zahlen bei der Anordnung von BlĂ€ttern, BlĂŒtenblĂ€ttern, FruchtstĂ€nden (wie Tannenzapfen oder Sonnenblumen) sowie bei der Wachstumsstruktur von Populationen zu finden. Diese natĂŒrliche Optimierung wird oft als Fibonacci-Phyllotaxis bezeichnet.
Kunst und Architektur: Der Goldene Schnitt â ein VerhĂ€ltnis, das eng mit der Fibonacci-Reihe verbunden ist â wird in Kunstwerken und architektonischen EntwĂŒrfen verwendet, um Ă€sthetische Proportionen zu erreichen. Bekannte Beispiele sind das Parthenon in Griechenland oder GemĂ€lde der Renaissance.
Rekursiv vs. iterativ
đ Unterschied zwischen rekursiv und iterativ:
Rekursiv: Bei einem rekursiven Algorithmus ruft sich die Funktion selbst auf, bis eine Basisbedingung erreicht ist. Rekursion ist oft eleganter und direkter, spiegelt aber nicht immer die effizienteste Lösung wider, da sie viele wiederholte Berechnungen machen kann und zusĂ€tzlichen Speicher fĂŒr den Rekursionsstapel benötigt.
Iterativ: Ein iterativer Algorithmus verwendet Schleifen (wie
foroderwhile), um wiederholte Berechnungen durchzufĂŒhren. Iteration ist in der Regel effizienter, da sie weniger Speicher benötigt und keine wiederholten Berechnungen macht, was sie insbesondere bei groĂen Problemen (wie Fibonacci) schneller und ressourcenschonender macht.
1. Ein iterativer Algorithmus
Der iterative Algorithmus zur Berechnung der n-ten Fibonacci-Zahl verwendet eine Schleife, um die Werte der Fibonacci-Zahlen zu berechnen, anstatt rekursive Funktionsaufrufe zu verwenden.
Code:
def fibi (n):
a = 0
b = 1
for i in range(n):
hilf = a + b
a = b
b = hilf
return a
print(fibi(40))ErklÀrung:
-
Initialisierung:
- Der Algorithmus beginnt mit zwei Variablen
aundb, die die ersten beiden Fibonacci-Zahlen speichern.a = 0ist die 0-te Fibonacci-Zahl.b = 1ist die 1-te Fibonacci-Zahl.
- Der Algorithmus beginnt mit zwei Variablen
-
Schleife:
-
Die
for-Schleife lÀuftnMal (voni = 0bisi = n-1), um die n-te Fibonacci-Zahl zu berechnen. -
In jedem Schleifendurchlauf wird die Variable
hilfals Summe der aktuellen Werte vonaundbberechnet:hilf = a + bDies ist die nÀchste Fibonacci-Zahl, die auf der Addition der beiden vorherigen Fibonacci-Zahlen basiert.
-
Danach wird
aauf den Wert vonbgesetzt, undbwird auf den neuen Fibonacci-Wert (inhilf) gesetzt:a = b b = hilfAuf diese Weise werden die beiden letzten Fibonacci-Zahlen fĂŒr den nĂ€chsten Schleifendurchlauf aktualisiert.
-
-
RĂŒckgabewert:
- Nach dem Ende der Schleife enthÀlt die Variable
adie n-te Fibonacci-Zahl. Diese wird dann zurĂŒckgegeben.
- Nach dem Ende der Schleife enthÀlt die Variable
Beispiel:
- Wenn du
fibi(5)aufrufst:- Die Schleife durchlÀuft 5 Iterationen:
a = 0,b = 1âa = 1,b = 1âa = 1,b = 2âa = 2,b = 3âa = 3,b = 5.
- Nach der 5. Iteration ist
a = 5, was die 5-te Fibonacci-Zahl ist.
- Die Schleife durchlÀuft 5 Iterationen:
Zeit- und SpeicherkomplexitÀt:
- ZeitkomplexitĂ€t: O(n) â Der Algorithmus durchlĂ€uft
nIterationen, was bedeutet, dass die Anzahl der Rechenschritte linear zur EingabegröĂenist. - SpeicherkomplexitĂ€t: O(1) â Es werden nur eine konstante Anzahl von Variablen (
a,bundhilf) verwendet, unabhĂ€ngig von der EingabegröĂen.
Vorteile:
- Der iterative Ansatz ist sehr effizient, insbesondere im Vergleich zu rekursiven AnsĂ€tzen, da er keine unnötigen Berechnungen durchfĂŒhrt und auch keine Speicherprobleme durch zu viele Rekursionsaufrufe entstehen.
2. Ein rekursiver Algorithmus zur Berechnung der n-ten Fibonacci-Zahl
Rekursiver Algorithmus
Der rekursive Ansatz zur Berechnung der n-ten Fibonacci-Zahl folgt der mathematischen Definition der Fibonacci-Folge:
- Fibonacci-Folge:
F(0) = 0F(1) = 1F(n) = F(n-1) + F(n-2)fĂŒr n â„ 2
Die Idee ist, die Fibonacci-Zahl als Summe der zwei vorhergehenden Fibonacci-Zahlen zu berechnen. Der Algorithmus ruft sich selbst rekursiv auf, bis die BasisfÀlle F(0) und F(1) erreicht werden.
Python-Code:
def fibi2(n):
if n <= 1:
return n
else:
return fibi2(n-1) + fibi2(n-2)
print("Rekursiv: " + str(fibi2(40))) # Ausgabe: 102334155ErklÀrung:
- BasisfÀlle: Wenn
n <= 1, wirdndirekt zurĂŒckgegeben, da die ersten beiden Fibonacci-Zahlen bekannt sind:F(0) = 0undF(1) = 1. - Rekursiver Aufruf: FĂŒr
n > 1wird die Funktion rekursiv mit den beiden Wertenn-1undn-2aufgerufen. Die RĂŒckgabewerte dieser beiden Funktionsaufrufe werden addiert, um die Fibonacci-Zahl fĂŒrnzu berechnen. - ZeitkomplexitĂ€t: Der rekursive Algorithmus hat eine ZeitkomplexitĂ€t von O(2^n), da jeder Aufruf zwei weitere rekursive Aufrufe erzeugt. Dies macht den Algorithmus fĂŒr groĂe Werte von
nineffizient. - SpeicherkomplexitÀt: Die SpeicherkomplexitÀt betrÀgt O(n), da der Rekursionsstapel maximal
ntief ist.
Beispiel:
- Eingabe:
fibi2(5)- Aufrufe:
fibi2(5) -> fibi2(4) + fibi2(3) -> fibi2(3) + fibi2(2) + fibi2(2) + fibi2(1) -> ⊠- Ergebnis:
5
- Aufrufe:
- Eingabe:
fibi2(40)- Ergebnis:
102334155
- Ergebnis:
Vorteile und Nachteile:
- Vorteile: Der rekursive Ansatz ist elegant und einfach zu verstehen, da er direkt der mathematischen Definition der Fibonacci-Zahlen folgt.
- Nachteile: Aufgrund der vielen rekursiven Aufrufe und der Tatsache, dass viele Berechnungen mehrfach durchgefĂŒhrt werden, ist dieser Ansatz sehr ineffizient fĂŒr gröĂere
n. Eine Optimierung, z.B. durch Memoization, könnte diesen Nachteil beheben.
3. Welcher Ansatz ist vorzuziehen, welcher ist eleganter?
-
Rekursiver Ansatz: Eleganter, da er die mathematische Definition direkt widerspiegelt. Allerdings ist er ineffizient, da viele Berechnungen wiederholt werden und die Laufzeit exponentiell ansteigt.
-
Iterativer Ansatz: Deutlich effizienter mit einer linearen Laufzeit (O(n)) und konstantem Speicherbedarf (O(1)). Er vermeidet unnötige Wiederholungen und ist besser fĂŒr groĂe
ngeeignet.
Fazit:
- Der rekursive Ansatz ist eleganter.
- Der iterative Ansatz ist in der Praxis effizienter und vorzuziehen.
- â In der Praxis wird der iterative Ansatz bevorzugt, da er effizienter ist, auch wenn der rekursive Ansatz die mathematische Definition direkter widerspiegelt
4. Schreiben Sie ohne Verwendung einer Schleife:
a) Eine rekursive Funktion, die die Zeichen einer Zeichenkette nacheinander ausgibt:
Pseudocode:
- Wenn die Zeichenkette nicht leer ist:
- Gib das erste Zeichen aus.
- Entferne das erste Zeichen aus der Zeichenkette.
- Rufe die Funktion mit der verkĂŒrzten Zeichenkette erneut auf.
- Wiederhole dies, bis die Zeichenkette leer ist.
def rekursive_ausgabe_vorwaerts(kette):
if kette:
print(kette[0], end="")
kette = kette[1:]
rekursive_ausgabe_vorwaerts(kette)
# Beispielaufruf
zeichenkette = "Hallo"
rekursive_ausgabe_vorwaerts(zeichenkette)
print()ErklÀrung:
- Die Funktion beginnt mit der vollen Zeichenkette und druckt immer das erste Zeichen. AnschlieĂend wird der Rest der Zeichenkette ohne das erste Zeichen als neue Eingabe verwendet, und die Funktion ruft sich selbst auf. Dies wird so lange wiederholt, bis die Zeichenkette leer ist. Auf diese Weise werden die Zeichen nacheinander in ihrer ursprĂŒnglichen Reihenfolge ausgegeben.
b) Eine rekursive Funktion, die die Zeichen der Zeichenkette in umgekehrter Reihenfolge druckt:
def rekursive_ausgabe_rueckwaerts(kette):
if kette: # PrĂŒft, ob die Zeichenkette nicht leer ist
rekursive_ausgabe_rueckwaerts(kette[1:]) # Rekursiver Aufruf mit dem Rest der Zeichenkette (ohne das erste Zeichen)
print(kette[0], end="") # Gibt das erste Zeichen der aktuellen Zeichenkette aus
# Beispielaufruf
zeichenkette = "Hallo"
rekursive_ausgabe_rueckwaerts(zeichenkette)
print() # FĂŒgt nach der Ausgabe eine neue Zeile hinzuSchritt-fĂŒr-Schritt-ErklĂ€rung:
-
if kette:- PrĂŒft, ob die Zeichenkette nicht leer ist (Basisfall der Rekursion).
- Falls leer, wird die Rekursion beendet.
-
rekursive_ausgabe_rueckwaerts(kette[1:])- FĂŒhrt die Funktion rekursiv mit der Zeichenkette ohne das erste Zeichen aus.
- Dies verkĂŒrzt die Zeichenkette bei jedem Rekursionsschritt, bis sie leer ist.
-
print(kette[0], end="")- Nachdem die Rekursion den letzten Aufruf erreicht hat (leere Zeichenkette), werden die Zeichen auf dem RĂŒckweg ausgegeben.
- Druckt das aktuelle Zeichen (
kette[0]) aus, sodass die Zeichen in umgekehrter Reihenfolge erscheinen.
-
print()- FĂŒgt eine neue Zeile nach der Ausgabe der Zeichen hinzu, um den Cursor in die nĂ€chste Zeile zu setzen.
Beispiel mit "Hallo":
"Hallo"â rekursiver Aufruf mit"allo""allo"â rekursiver Aufruf mit"llo""llo"â rekursiver Aufruf mit"lo""lo"â rekursiver Aufruf mit"o""o"â rekursiver Aufruf mit""(leere Zeichenkette, Ende der Rekursion)- Auf dem RĂŒckweg wird ausgegeben:
"o","l","l","a","H"
Wichtige Punkte:
- Die Rekursion lÀuft bis zum Ende der Zeichenkette, bevor die Ausgabe startet.
- Durch das Drucken der Zeichen beim RĂŒckweg erscheinen sie in umgekehrter Reihenfolge.
- Basisfall: Die Rekursion stoppt, wenn die Zeichenkette leer ist.
Zusammenfassung der Idee:
- VorwĂ€rtsausgabe: Die Funktion druckt das erste Zeichen und ruft sich mit dem Rest der Zeichenkette erneut auf. Dadurch werden die Zeichen in ihrer ursprĂŒnglichen Reihenfolge gedruckt.
- RĂŒckwĂ€rtsausgabe: Die Funktion ruft sich zuerst rekursiv mit dem Rest der Zeichenkette auf und druckt das Zeichen erst danach. Dies fĂŒhrt dazu, dass die Zeichen in umgekehrter Reihenfolge erscheinen.
Beide Funktionen verwenden Rekursion, um jedes Zeichen der Zeichenkette ohne Schleifen zu bearbeiten und auszugeben.
5. Diskutieren Sie, wann rekursive Algorithmen besser sind als iterative und wann nicht.
Kurzzusammenfassung
Rekursive und iterative Algorithmen sind zwei AnsÀtze zur Problemlösung.
Rekursive Algorithmen:
- Vorteile: Einfach zu schreiben und zu verstehen, gut fĂŒr selbstĂ€hnliche Probleme (z. B. Fibonacci).
- Nachteile: Hoher Speicherverbrauch und langsamer bei tiefen Rekursionen (Risiko von Stack Overflow).
Iterative Algorithmen:
- Vorteile: Effizienter, benötigen weniger Speicher und sind fĂŒr groĂe Probleme geeignet.
- Nachteile: Komplexer und weniger intuitiv, besonders bei natĂŒrlichen rekursiven Lösungen.
Fazit:
Rekursion eignet sich fĂŒr teilbare Probleme, wĂ€hrend Iteration besser fĂŒr Effizienz und Speicherverbrauch ist. Die Wahl hĂ€ngt von der Situation ab.
Rekursive und iterative Algorithmen sind zwei AnsÀtze, um Probleme zu lösen. Beide haben Vor- und Nachteile, die man je nach Situation abwÀgen muss.
Rekursive Algorithmen:
Vorteile:
- Einfachheit: Rekursive Algorithmen sind oft einfacher zu schreiben und zu verstehen, besonders bei Problemen, die sich in kleinere Teilprobleme aufteilen lassen. Ein Beispiel ist die Traversierung eines Baumes: Man kann leicht sagen, âgehe zu jedem Knoten und rufe die Funktion fĂŒr die Kinderknoten aufâ.
- Klarheit bei bestimmten Problemen: Bei Problemen wie der Fibonacci-Reihe oder dem Suchen in BĂ€umen ist der rekursive Ansatz natĂŒrlich und gut lesbar.
Nachteile:
- Hoher Speicherverbrauch: Jeder rekursive Funktionsaufruf wird im sogenannten âCall Stackâ gespeichert. Wenn es viele Aufrufe gibt, kann der Speicher knapp werden, was zu einem Stack Overflow fĂŒhren kann. Zum Beispiel kann eine naive rekursive Berechnung der Fibonacci-Zahlen fĂŒr groĂe Werte sehr ineffizient sein.
- Langsam bei groĂen Problemen: Rekursive Algorithmen können durch die vielen Funktionsaufrufe langsamer sein. Wenn es viele Rekursionsebenen gibt, wie bei tiefen Problemen oder langen Zahlenreihen, wĂ€re eine Schleife oft schneller.
Iterative Algorithmen:
Vorteile:
- Effizienz: Iterative Algorithmen (Schleifen) verbrauchen in der Regel weniger Speicher, da sie keine zusÀtzlichen Funktionsaufrufe benötigen. Beispielsweise kann man die Fibonacci-Zahlen auch mit einer Schleife berechnen, was viel schneller und speichereffizienter ist.
- Kein Risiko fĂŒr Stack Overflow: Da Iterationen den Stack nicht fĂŒllen, eignen sie sich gut fĂŒr sehr groĂe Probleme, bei denen eine Rekursion abstĂŒrzen könnte.
Nachteile:
- KomplexitĂ€t des Codes: Iterative Algorithmen können manchmal komplizierter und schwerer zu verstehen sein, besonders bei Problemen, die sich natĂŒrlich rekursiv lösen lassen. Zum Beispiel erfordert es mehr Aufwand, die Tiefensuche in einem Baum iterativ zu implementieren.
Beispiel:
Nehmen wir das Problem, die FakultÀt einer Zahl zu berechnen, z. B. 5! = 5 à 4 à 3 à 2 à 1.
- Rekursiv: Die FakultĂ€t von 5 ist 5 Ă FakultĂ€t von 4. Das fĂŒhrt zu einem kurzen und klaren Code.
- Iterativ: Man könnte auch eine Schleife verwenden, um alle Zahlen von 5 bis 1 zu multiplizieren. Dies verbraucht weniger Speicher, ist aber weniger intuitiv.
Fazit:
Rekursive Algorithmen sind nĂŒtzlich, wenn das Problem sich gut in kleinere Teile zerlegen lĂ€sst und der Speicherverbrauch nicht zu hoch ist. Iterative Algorithmen sind besser, wenn Effizienz und Speicher wichtig sind, wie bei sehr groĂen Problemen. Man sollte also je nach Situation entscheiden, welcher Ansatz besser passt.