Iteration versus Rekursion: Difference between revisions

From Alda
Jump to navigationJump to search
Line 72: Line 72:
:⇒  Das ist ein Spezialfall der linearen Rekursion, denn es kann nur einen letzten Befehl vor jedem return geben, ein zweiter rekursiver Aufruf könnte allenfalls der vorletzte Befehl sein.
:⇒  Das ist ein Spezialfall der linearen Rekursion, denn es kann nur einen letzten Befehl vor jedem return geben, ein zweiter rekursiver Aufruf könnte allenfalls der vorletzte Befehl sein.


==Implementationen==
==Beispiele für die Rekursionsarten==


Als Beispiel nehmen wir die Fibonacci-Zahl:<br>
Wir verdeutlichen die verschiednenen Rekursionsarten und ihre Umwandlung in iterative Algorithmen am Beispiel der Fibonacci-Zahlen.
Definition: f<sub>0</sub>=0, f<sub>1</sub>=1,...., f<sub>n</sub>=f<sub>n-1</sub>+f<sub>n-2</sub><br>
Es ergibt sich folgende Reihe: f<sub>k</sub>=0, 1, 1, 2, 3, 5, 8, 13,....., (=>die Reihe wächst exponentiell an).


;Fibonacci-Zahlen: Die n-te Fibonacci-Zahl ist gemäß der folgenden Rekursionsformel definiert.
:::f<sub>0</sub>=0
:::f<sub>1</sub>=1
:::f<sub>n</sub>=f<sub>n-1</sub>+f<sub>n-2</sub>
:Es ergibt sich die Folge:
:::f<sub>k</sub>=0, 1, 1, 2, 3, 5, 8, 13,....., (&rArr; Die Reihe wächst exponentiell an.)
Im folgenden zeigen wir 5 verschiedene Algorithmen zu berechnung der n-ten Fibonacci-Zahl:
===1. Version: Naive rekursive Implementation===
Implementiert man einfach die rekursive Formel der Definition, erhält man:


===1. Version: Implementation Rekursiv - naive Implementation:===
  def fib1(n):                      # Funtion berechnet die n-te Fibonacci-Zahl
  def fib1(n):                      # Funtion berechnet die n-te Fibonacci-Zahl
     if n <= 1: return n           # Rekursionsabschluss
     if n <= 1:  
     return fib1(n-1) + fib1(n-2)
          return n                 # Rekursionsabschluss
     return fib1(n-1) + fib1(n-2) # Baumrekursion


Die Funktion fib1(n) ist ein Beispiel für eine Baumrekursion mit exponentieller Komplexität.
Die Funktion fib1(n) ist ein Beispiel für eine Baumrekursion mit exponentieller Komplexität.
Der Aufrufbaum sieht dann wie folgt aus:
Der Aufrufbaum sieht dann wie folgt aus:


                                               fib1(n)
                                               fib1(n)
Line 99: Line 109:
                               fib1(n-2)  fib1(n-3)  fib1(n-3)  fib1(n-4)
                               fib1(n-2)  fib1(n-3)  fib1(n-3)  fib1(n-4)


Das ist natürlich ein ungünstiger Algorithmus, weil wiederholte Berechnungen gemacht werden.<br>
Im Falle der Fibonacci-Zahlen ist dies ein ungünstiger Algorithmus, weil viele Teilergebnise wiederholt berechnet werden (z.B. f<sub>n-2</sub> und f<sub>n-3</sub>).
Aber einige Probleme sind <u>echt</u> Baumrekursiv, z.B. das Ausrechnen der möglichen Züge beim Schachspiel.
Aber einige Probleme sind <u>echt</u> Baumrekursiv, z.B. das Ausrechnen der möglichen Züge beim Schachspiel.


 
;Es gilt folgender grundlegender Satz:
'''<u>Es gilt folgender Satz:</u>'''<br>
Jeder rekursiver Algorithmus kann mit Hilfe eines Stacks in einen iterativen Algorithmus umgewandelt werden (d.h. mit einer Schleife statt einer Rekursion). Das bedeutet, dass rekursive Programme gleichmächtig sind wie Programme mit Schleifen (z.B. WHILE-Programme, siehe [[Einführung#Zur Frage der elementaren Schritte|erste Vorlesung]]), d.h. gleichmächtig wie die Turing-Maschine. Die Komplexität des Algorithmus ändert sich durch die Umwandlung nicht.
Jeder rekursiver Algorithmus kann mit einem Stack in iterativen Algorithmus umgewandelt werden (d.h. mit einer Schleife statt einer Rekursion). Das bedeutet, dass die Rekursion gleichmächtig ist wie die Schleife, d.h. gleichmächtig wie die Turing-Maschine. Die Komplexität ändert sich nicht.




===2. Version: iterativ mit einem Stack===
===2. Version: Umwandlung in einen iterativen Algorithmus mit Stack===


  def fib2(n):
  def fib2(n):
     stack = [n]  # unser Array
     stack = [n]  # der Stack enthält als erstes Teilproblem das ursprüngliche Problem "n-te Fibonacci-Zahl berechnen"
     f = 0        # f wird später die Lösung enthalten
     f = 0        # f wird später die Lösung enthalten
      
      
     while len(stack) > 0:          # solange noch was auf dem Stack liegt, ist noch Arbeit zu tun
     while len(stack) > 0:          # solange noch was auf dem Stack liegt, ist noch Arbeit zu tun
           k = stack.pop(-1)       # wenn etwas auf dem Stack liegt, dann nehme vom Stack herunter
           k = stack.pop()         # Teilproblem von Stack entnehmen und lösen
           if k <= 1:              # Abschluss der Schleife
           if k <= 1:               
                 f += k
                 f += k             # entweder: Teilergebnis zur Lösung addieren (das war vorher der Rekursionsabschluss)
           else:                    # Umwandlung der Rekursion in Iteration
           else:                     
                 stack.append(k-1)   
                 stack.append(k-1)  # oder: zwei neue Teilprobleme auf dem Stack ablegen
                 stack.append(k-2)
                 stack.append(k-2)  # (das waren vorher die rekursiven Aufrufe)
     return f
     return f


===3. Version: Course-of-values===
===3. Version: Course-of-values recursion===
Fibonacci ist Course-of-values rekursiv (folgt unmittelbar aus der Definition) mit p=2.
 
Wie man unmittelbar aus der Definitione erkennt, ist die Berechnung der Fibonacci-Zahlen Course-of-values rekursiv mit p=2. Eine entsprechende Implementation verwendet eine Hilfsunfktion, die immer die Ergebnisse der p letzten Aufrufe zurückgibt:


  def fib3(n):
  def fib3(n):
Line 130: Line 140:
     return f<sub>1</sub>
     return f<sub>1</sub>
   
   
Die Hilfsfunktion:
Die Hilfsfunktion enthält jetzt die eigentliche Rekursion. Sie berechnet die Fibonacci-Zahlen f<sub>k</sub> und f<sub>k-1</sub> aus den Zahlen f<sub>k-1</sub> und f<sub>k-2</sub>:


  def fib3Impl(n):
  def fib3Impl(n):
Line 136: Line 146:
     else:                          # rekursiver Aufruf
     else:                          # rekursiver Aufruf
         f<sub>1</sub>,f<sub>2</sub> = fib3Impl(n-1)      # f<sub>1</sub> ist Fibonacci-Zahl von (n-1), f<sub>2</sub> die von (n-2)
         f<sub>1</sub>,f<sub>2</sub> = fib3Impl(n-1)      # f<sub>1</sub> ist Fibonacci-Zahl von (n-1), f<sub>2</sub> die von (n-2)
         return f<sub>1</sub> + f<sub>2</sub>, f<sub>1</sub>          # gebe neue Fibonacci-Zahl f<sub>1</sub>+f<sub>2</sub> und die vorherige (f<sub>1</sub>) zurück
         return f<sub>1</sub> + f<sub>2</sub>, f<sub>1</sub>          # gebe neue Fibonacci-Zahl f<sub>n</sub>=f<sub>1</sub>+f<sub>2</sub> und die vorherige (f<sub>n-1</sub>=f<sub>1</sub>) zurück.
 
==> Diese Implementation ist jetzt linear-rekursiv und hat damit auch lineare Komplexität und nicht exponentielle wie die vorherigen Versionen.


&rArr; Diese Implementation ist jetzt linear-rekursiv (aber nicht Endrekursiv). Sie hat damit lineare Komplexität und nicht exponentielle wie die beiden vorherigen Versionen.


'''<u>Es gilt folgender Satz:</u>'''<br>
;Es gilt folgender Satz:
Jede Course-of-values Rekursion kann in Endrekursion umgewandelt werden.
Jede Course-of-values Rekursion kann in Endrekursion umgewandelt werden.


===4. Version: Endrekursiv===


===4. Version: Endrekursiv===
Man gelangt von der Course-of-values recursion zur Endrekursion, indem man einfach die Berechnungsreihenfolge umkehrt: statt rückwärts von f<sub>n</sub> nach f<sub>0</sub> zu arbeiten, arbeitet man vorwärts von f<sub>0</sub> nach f<sub>n</sub>. Dazu muss der Hilfsfunktion eine Zählvariable übergeben werden, die angibt, wie viele Schritte noch bis zum Ziel f<sub>n</sub> verbleiben. Außerdem erhält die Hilfsfunktion die beiden letzen Fibonacci-Zahlen als Argument übergeben:


  def fib4(n)
  def fib4(n)
     if n == 0: return 0     
     if n == 0: return 0     
     else:                     
     else:                     
         return fib4Impl(0,1,n)  
         return fib4Impl(0, 1, n)  
   
   
Die Hilfsfunktion:
Die Hilfsfunktion:


  def fib4Impl(f<sub>1</sub>,f<sub>2</sub>,n)
  def fib4Impl(f<sub>1</sub>, f<sub>2</sub>, counter)
     if n == 1: return f<sub>2</sub>
     if counter == 1: return f<sub>2</sub>
     else:
     else:
         return fib4Impl(f<sub>2</sub>,f<sub>1</sub>+f<sub>2</sub>,n-1)  # f<sub>2</sub> ist die vorletzte Fibonacci-Zahl,<br>
         return fib4Impl(f<sub>2</sub>, f<sub>1</sub>+f<sub>2</sub>, n-1)  # f<sub>2</sub> ist die vorletzte Fibonacci-Zahl,<br>
                                        # f<sub>1</sub>+f<sub>2</sub> wird die neue Fibonacci-Zahl und wir müssen n um 1 herunterzählen
                                                                          # f<sub>1</sub>+f<sub>2</sub> wird die neue Fibonacci-Zahl und wir müssen n um 1 herunterzählen


Beispiel mit n=2:
Beispiel mit n=3:


{| style="width:30%; height:75px" border="1"
{| style="width:30%; height:75px" border="1"
|-  
|-  
|    || f<sub>1</sub> || f<sub>2</sub> || n || return
|    || f<sub>1</sub> || f<sub>2</sub> || n  
|-  
|-  
| fib4Impl 1.Aufruf || 0 || 1 || 2 || 1
| fib4Impl 1.Aufruf || 0 || 1 || 3
|-
| fib4Impl 2.Aufruf || 1 || 1 || 2
|-
|-
| fib4Impl 2.Aufruf || 1 || 1 || 1 || 1
| fib4Impl 3.Aufruf || 1 || 2 || 1 &rArr; Rekursionsabschluss &rArr; return 2
|}
|}


==> Ergebnis von 2.Fibonacci-Zahl ist: 1
&rArr; Ergebnis von fib4(3) (die 3. Fibonacci-Zahl) ist 2.
 


'''<u>Es gilt folgender Satz:</u>'''<br>
;Es gilt folgender grundlegender Satz:
Jede endrekursive Funktion kann <u>ohne</u> Stack in eine iterative Funtion umgewandelt werden.
Jede endrekursive Funktion kann <u>ohne</u> Stack in eine iterative Funktion umgewandelt werden.  


Bei einigen Programmiersprachen (z.B. Lisp, Scheme) wird dies von Compiler sogar automatisch erledigt (als Programmoptimierung, weil Iteration im allgemeinen schneller ist als Rekursion).


===5. Version: iterativ ohne Stack===
===5. Version: Umwandlung in einen iterativen Algorithmus ohne Stack===


  def fib5(n):
  def fib5(n):
    if n == 0: return 0
     f<sub>1</sub>, f<sub>2</sub> = 0, 1
     f<sub>1</sub>,f<sub>2</sub> = 0,1
     while n > 0:
    k = 1
         f<sub>1</sub>, f<sub>2</sub> = f<sub>2</sub>, f<sub>1</sub>+f<sub>2</sub>
     while k < n:
         n -= 1
         f<sub>1</sub>,f<sub>2</sub> = f<sub>2</sub>,f<sub>1</sub>+f<sub>2</sub>
     return f<sub>1</sub>
         k += 1
     return f<sub>2</sub>
 
Das ist genau das gleiche wie fib4 nur das bei fib4 das n rückwärts gezählt wird und hier das k vorwärts. Das könnte man auch so schreiben:
 
while n > 0:
  f<sub>1</sub>,f<sub>2</sub> = f<sub>2</sub>,f<sub>1</sub>+f<sub>2</sub>
  n -= 1


Das ist genau das gleiche wie <tt>fib4Impl</tt>. Aber anstelle eines rekursiven Aufrufes werden einfach die Variablen f<sub>1</sub> und f<sub>2</sub> wiederverwendet (mit den neuen Werten überschrieben). Dies ist möglich, weil die originalen Werte nicht mehr benötigt werden, denn der rekursive Aufruf bei <tt>fib4Impl</tt> war der letzte Befehl vor dem return (Endrekursion!).


'''Beispiel: treeSort'''
'''Beispiel: treeSort'''

Revision as of 18:51, 10 July 2008

Rekursion in der Informatik:

Einen Funktion f ist rekursiv wenn sie sich selbst aufruft. Die Funktion heißt indirekt-rekursiv wenn sie sich selbst aufruft über den Umweg einer anderen Funktion (oder mehrerer anderer Funktionen).

Beispiel einer indirekten Rekursion:

   foo  --->  bar  --->  foo
      ruft auf    ruft auf


Entscheidende Eigenschaften der Rekursion

  • Jeder Aufruf einer rekursiven Funktion f hat sein eigenes Set von lokalen Variablen, d.h. der rekursive Aufruf hat die gleichen Anweisungen auszuführen, aber er hat seine eigenen Daten. Betrachten wir z.B. die rekursive Funktion
     def f(n):
         r = f(n-1)
         ... # weiterer Code
         return r
Für ein gegebenes n erhalten wir die Aufrufkette:
     f(n)             1.Ebene
        f(n-1)        2.Ebene
             f(n-2)   3.Ebene
                      ...
Die Funktionsaufrufe der verschiedenen Ebenen werden so ausgeführt, als ob wir für jede Ebene eine eigene Funktion f1(n1), f2(n2), f3(n3) usw. definiert hätten:
     f1(n1)            1.Ebene ---> n1 = n
         f2(n2)        2.Ebene ---> n2 = n1-1
             f3(n3)    3.Ebene ---> n3 = n2-1 = n1-2 = n-2
                       ...
Die Funktionen f1(n1), f2(n2), f3(n3) enthalten alle den gleichen Code, aber mit unterschiedlich benannten Variablen n1, n2, n3. Durch diese Umbenennungen sollte die obige Aussage deutlich werden.
  • Jede rekursive Funktion muß mindestens einen nicht-rekursiven Zweig haben. Dieser Zweig wird als Basisfall oder Rekursionsabschluss bezeichnet.
  • Jeder rekursive Aufruf muß über endlich viele Stufen auf einen Basisfall zurückgeführt werden, denn sonst erhält man eine Endlosrekursion (und das ist fast so übel wie eine Endlosschleife -- "fast" deshalb, weil die Endlosrekursion spätestens dann abbricht wenn der Speicher voll ist --> siehe Übung 7 Aufgabe 1).
  • Die Anzahl der rekursiven Aufrufe bis zum Rekursionsabschluss bezeichnet man als Rekursionstiefe d.
  • Jede rekursive Funktion kann in eine iterative Funktion umgewandelt werden, die statt rekursiver Aufrufe eine Schleife enthält. Wir beschreiben dies unten anhand von Beispielen.


Arten der Rekursion

Die Arten der Rekursion sind hier nicht vollständig angegeben, es gibt noch weitere. Aber die hier folgenden sind für die Programmierung am wichtigsten.

Lineare Rekursion

  • Jeder Ausführungspfad durch die Funktion f enthält höchstens einen rekursiven Aufruf.
  • Höchstens einen, weil mindestens ein Pfad existieren muss, der keinen rekursiven Aufruf enthält (der Basisfall).
⇒ Das Ergebnis der Aufrufe ist eine 1-dimensionale "Rekursionskette".

Baumrekursion

(auch Kaskadenrekursion)

  • Mindestens ein Ausführungspfad durch die Funktion enthält mindestens 2 rekursive Aufrufe.
  • Baumrekursion ist damit das Gegenteil der linearen Rekursion.
⇒ Das Ergebnis der Aufrufe ist ein verzweigter "Rekursionsbaum". Ein baum-rekursiver Algorithmus ist deshalb häufig ineffizient: Wird der Pfad mit mehreren rekursiven Aufrufen mindestens Ω(d)-mal ausgeführt (wobei d die Rekursionstiefe) ist, entsteht ein Rekursionsbaum mit O(2d) Knoten, also ein Algorithmus mit exponentieller Komplexität.

Course-of-values recursion

  • Das Ergebnis des rekursiven Aufrufes für Argument n hängt nur von den Ergebnissen der letzten p-Aufrufe, also für Argument n-1,n-2,...,n-p ab (wobei p eine Konstante ist).

Primitive Rekursion

  • Spezialfall der course-of-values recursion mit p=1.
⇒ Das ist auch ein Spezialfall der linearen Rekursion, denn wenn das Ergebnis nur von einem rekursiven Aufruf abhängt, kann kein Rekursionsbaum entstehen.

Endrekursion

  • In jedem rekursiven Ausführungspfad ist der rekursive Aufruf der letzte Befehl vor dem return-Befehl.
⇒ Das ist ein Spezialfall der linearen Rekursion, denn es kann nur einen letzten Befehl vor jedem return geben, ein zweiter rekursiver Aufruf könnte allenfalls der vorletzte Befehl sein.

Beispiele für die Rekursionsarten

Wir verdeutlichen die verschiednenen Rekursionsarten und ihre Umwandlung in iterative Algorithmen am Beispiel der Fibonacci-Zahlen.

Fibonacci-Zahlen
Die n-te Fibonacci-Zahl ist gemäß der folgenden Rekursionsformel definiert.
f0=0
f1=1
fn=fn-1+fn-2
Es ergibt sich die Folge:
fk=0, 1, 1, 2, 3, 5, 8, 13,....., (⇒ Die Reihe wächst exponentiell an.)

Im folgenden zeigen wir 5 verschiedene Algorithmen zu berechnung der n-ten Fibonacci-Zahl:

1. Version: Naive rekursive Implementation

Implementiert man einfach die rekursive Formel der Definition, erhält man:

def fib1(n):                      # Funtion berechnet die n-te Fibonacci-Zahl
    if n <= 1: 
         return n                 # Rekursionsabschluss
    return fib1(n-1) + fib1(n-2)  # Baumrekursion

Die Funktion fib1(n) ist ein Beispiel für eine Baumrekursion mit exponentieller Komplexität. Der Aufrufbaum sieht dann wie folgt aus:


                                              fib1(n)
                                             /       \
                                            /         \
                                           /           \
                                          /             \
                                   fib1(n-1)           fib1(n-2)
                                   /       \           /       \
                                  /         \         /         \
                                 /           \       /           \
                             fib1(n-2)  fib1(n-3)  fib1(n-3)   fib1(n-4)

Im Falle der Fibonacci-Zahlen ist dies ein ungünstiger Algorithmus, weil viele Teilergebnise wiederholt berechnet werden (z.B. fn-2 und fn-3). Aber einige Probleme sind echt Baumrekursiv, z.B. das Ausrechnen der möglichen Züge beim Schachspiel.

Es gilt folgender grundlegender Satz

Jeder rekursiver Algorithmus kann mit Hilfe eines Stacks in einen iterativen Algorithmus umgewandelt werden (d.h. mit einer Schleife statt einer Rekursion). Das bedeutet, dass rekursive Programme gleichmächtig sind wie Programme mit Schleifen (z.B. WHILE-Programme, siehe erste Vorlesung), d.h. gleichmächtig wie die Turing-Maschine. Die Komplexität des Algorithmus ändert sich durch die Umwandlung nicht.


2. Version: Umwandlung in einen iterativen Algorithmus mit Stack

def fib2(n):
    stack = [n]   # der Stack enthält als erstes Teilproblem das ursprüngliche Problem "n-te Fibonacci-Zahl berechnen"
    f = 0         # f wird später die Lösung enthalten
    
    while len(stack) > 0:          # solange noch was auf dem Stack liegt, ist noch Arbeit zu tun
          k = stack.pop()          # Teilproblem von Stack entnehmen und lösen
          if k <= 1:               
               f += k              # entweder: Teilergebnis zur Lösung addieren (das war vorher der Rekursionsabschluss)
          else:                    
               stack.append(k-1)   # oder: zwei neue Teilprobleme auf dem Stack ablegen 
               stack.append(k-2)   # (das waren vorher die rekursiven Aufrufe)
    return f

3. Version: Course-of-values recursion

Wie man unmittelbar aus der Definitione erkennt, ist die Berechnung der Fibonacci-Zahlen Course-of-values rekursiv mit p=2. Eine entsprechende Implementation verwendet eine Hilfsunfktion, die immer die Ergebnisse der p letzten Aufrufe zurückgibt:

def fib3(n):
    if n == 0: return 0     # Rekursionsabschluss
    f1,f2 = fib3Impl(n)     # Hilfsfuntion, f1 ist die Fibonacci-Zahl von n und f2 ist dei Fibonacci-Zahl von (n-1)
    return f1

Die Hilfsfunktion enthält jetzt die eigentliche Rekursion. Sie berechnet die Fibonacci-Zahlen fk und fk-1 aus den Zahlen fk-1 und fk-2:

def fib3Impl(n):
    if n == 1: return 1, 0         # gebe die Fibonacci-Zahl von 1 und die davor zurück
    else:                          # rekursiver Aufruf
       f1,f2 = fib3Impl(n-1)       # f1 ist Fibonacci-Zahl von (n-1), f2 die von (n-2)
       return f1 + f2, f1          # gebe neue Fibonacci-Zahl fn=f1+f2 und die vorherige (fn-1=f1) zurück.

⇒ Diese Implementation ist jetzt linear-rekursiv (aber nicht Endrekursiv). Sie hat damit lineare Komplexität und nicht exponentielle wie die beiden vorherigen Versionen.

Es gilt folgender Satz

Jede Course-of-values Rekursion kann in Endrekursion umgewandelt werden.

4. Version: Endrekursiv

Man gelangt von der Course-of-values recursion zur Endrekursion, indem man einfach die Berechnungsreihenfolge umkehrt: statt rückwärts von fn nach f0 zu arbeiten, arbeitet man vorwärts von f0 nach fn. Dazu muss der Hilfsfunktion eine Zählvariable übergeben werden, die angibt, wie viele Schritte noch bis zum Ziel fn verbleiben. Außerdem erhält die Hilfsfunktion die beiden letzen Fibonacci-Zahlen als Argument übergeben:

def fib4(n)
    if n == 0: return 0    
    else:                    
       return fib4Impl(0, 1, n) 

Die Hilfsfunktion:

def fib4Impl(f1, f2, counter)
    if counter == 1: return f2
    else:
       return fib4Impl(f2, f1+f2, n-1)   # f2 ist die vorletzte Fibonacci-Zahl,
# f1+f2 wird die neue Fibonacci-Zahl und wir müssen n um 1 herunterzählen

Beispiel mit n=3:

f1 f2 n
fib4Impl 1.Aufruf 0 1 3
fib4Impl 2.Aufruf 1 1 2
fib4Impl 3.Aufruf 1 2 1 ⇒ Rekursionsabschluss ⇒ return 2

⇒ Ergebnis von fib4(3) (die 3. Fibonacci-Zahl) ist 2.

Es gilt folgender grundlegender Satz

Jede endrekursive Funktion kann ohne Stack in eine iterative Funktion umgewandelt werden.

Bei einigen Programmiersprachen (z.B. Lisp, Scheme) wird dies von Compiler sogar automatisch erledigt (als Programmoptimierung, weil Iteration im allgemeinen schneller ist als Rekursion).

5. Version: Umwandlung in einen iterativen Algorithmus ohne Stack

def fib5(n):
    f1, f2 = 0, 1
    while n > 0:
        f1, f2 = f2, f1+f2
        n -= 1
    return f1

Das ist genau das gleiche wie fib4Impl. Aber anstelle eines rekursiven Aufrufes werden einfach die Variablen f1 und f2 wiederverwendet (mit den neuen Werten überschrieben). Dies ist möglich, weil die originalen Werte nicht mehr benötigt werden, denn der rekursive Aufruf bei fib4Impl war der letzte Befehl vor dem return (Endrekursion!).

Beispiel: treeSort

Input:
-ein balancierter Binärbaum root
-ein leeres dynamisches Array a

Aufgerufen wird: treeSort(root,a) # kopiert Elemente des Baums sortiert in Array a

Wiederholung (vergleiche Abschnitt "Sortieren mit Hilfe eines selbst-balancierenden Suchbaums"):

def treeSort(node,a):
   if node is None: return
   treeSort(node.left,a)
   a.append(node.key)
   treeSort(node.right,a)

Implementation als iterative Funktion:

def treeSortIterative(node,a):
   stack = []
   traverseLeft(node,stack)   # Hilfsfuntion, füllt Stack
   while len(stack) > 0:
       node = stack.pop(-1)
       a.append(node.key)
       traverseLeft(node.right,stack)

Hilfsfunktion:

def traverseLeft(node,stack):
   while node is not None:
       stack.append(node)
       node = node.left

Auflösung rekursiver Gleichungen

Komplexitätsberechnung rekursiver Algorithmen

<math>\underbrace{T(n)}_{Anzahl der Befehle} = \underbrace{ a_1T\left(\frac{n}{b_1}\right)+a_2T\left(\frac{n}{b_2}\right)+\cdots+a_kT\left(\frac{n}{b_k}\right) }_{rekursives Teilproblem} +\underbrace{f(n)}_{nicht-rekursiver Teil}</math>

Bemerkung: Aufrunden von <math>\lceil\frac{n}{b_i}\rceil</math> oder abrunden von <math>\lfloor\frac{n}{b_i}\rfloor</math> spielen für die Auflösung der Formeln i.a. keine Rolle.


Spezialfall: k = 1, d.h. alle Unterprobleme haben die gleiche Größe.

<math>T(n) = aT\left(\frac{n}{b}\right)+f(n)</math>

In diesem Fall muss folgender Rekursionsexponent eingeführt werden:

<math>\boldsymbol{\rho}=log_ba</math>

==> Master-Theorem


Master-Theorem

Fall 1:

<math>f(n) \in O(n^{\rho-\epsilon}) , \epsilon>0</math>
==> das bedeutet: die Kosten der Rekursion überwiegen
==><math>\boldsymbol{T(n) =\Theta(n^{\rho})}</math>

Fall 2:

<math>f(n) \in \Theta(n^{\rho}) </math>
==> das bedeutet: die Kosten von f und Rekursion addieren sich
==><math>\boldsymbol{T(n) =\Theta(n^{\rho}*logn})</math>


Fall 3:

<math>f(n) \in \Omega(n^{\rho+\epsilon})</math>
==> das bedeutet: die Kosten von f überwiegen
==><math>\boldsymbol{T(n) =\Theta(f(n))}</math> es muß ausserdem noch gelten: <math>a*f\left(\frac{n}{b}\right)\le c*f(n)</math> für c<1




Beispiel: Merge Sort

<math>T(n)  =  \underbrace{ 2T\left(\frac{1}{2}\right)}_{rekursiver Aufruf von MergeSort}+\underbrace {\Theta(n)}_{Merge}</math>
a=2, b=2 ==> <math>\rho=log_22 =1</math> ==> das ist Fall 2: <math>f(n) = \Theta(n) = O(n^{\rho}) </math>
==> Komplexität von MergeSort: <math>T(n) \in \Theta(n^\rho*logn) </math>


Fälle die nicht durch das Master-Theorem abgedeckt sind:

- wenn <math>k \ > 1</math>: rekursive Teilprobleme verschiedener Grösse
- wenn <math>f(n) \in O\left(\frac{n^\rho}{logn}\right)</math>
- wenn <math>f(n) \in \Omega\left(n^\rho*logn\right)</math>


Wo das Master-Theorem nicht anwendbar ist:

Beispiel ist die folgende Rekursion:
 <math>T(n)  =  \underbrace{ T\left(\frac{n}{3}\right)}_{\frac{1}{3} der Daten}+\underbrace {T\left(\frac{2n}{3}\right)}_{\frac{2}{3} der Daten}+O(n)</math>
Rekursionsbaum:
n --> wächst mit c*n / \ / \ / \ / \ / \ (n/3) (2n/3) / \ / \ / \ / \ / \ / \ (n/9) (2n/9) (2n/9) (4n/9) ==> <math>c*\left(\frac{n}{9}+\frac{2n}{9}+\frac{2n}{9}+\frac{4n}{9}\right)=c*n</math> Rekursion endet wenn: <math>\left(\frac{2}{3}\right)^d*n=1</math> wird, wobei d die Tiefe des Baums ist. <math>n=\left(\frac{3}{2}\right) ---> d=log_\frac{3}{2}n</math> Vermutung: <math>T(n) \in O\left(log_\frac{3}{2}n*c*n\right)</math> nach den Regeln der O-Notation ist das: 0(n*logn), d.h unsere Vermutung stimmt.
Ungleiche Aufteilung in Teilprobleme ändert nicht dei Komplexität, falls das Teilungsverhältnis konstant ist.

 
Beweis durch Substitution:

<math>T(n)\le T\left(\frac{n}{3}\right)+ T\left(\frac{2n}{3}\right)+ c\,n</math>
<math>\le d\left(\frac{n}{3}\,\log\frac{n}{3}\right)+ d\left(\frac{2n}{3}\,\log\frac{2n}{3}\right)+ c\,n</math>
<math> = d\frac{n}{3}\,\log n-d\,\frac{n}{3}\,\log 3+d\,\frac{2}{3}\,n\,\log n-d\,\frac{2}{3}\,n\,\log\frac{3}{2}+c\,n</math>
<math> = d\,n\,\log n-d\underbrace{\left( \frac{n}{3}\,\log 3+\frac{2n}{3}\,\log\frac{3}{2}\right) }_{\frac{n}{3}\,\log 3+\frac{2n}{3}\,\log 3-\frac{2n}{3}\,\log 2}+c\,n</math>
<math> = d\,n\,\log n-d\,n\left(\log 3-\frac{2}{3}\right)+c\,n</math>
<math> \le d\,n\,\log n</math> falls <math>c\,n-d\,n\left(\log 3-\frac{2}{3}\right)< 0</math>
<math> \Longleftrightarrow d \ge \frac{c}{\log 3-\frac{2}{3}}</math>