Vorlesungsskript 19
Rekursion
Eine Funktion ist rekursiv, wenn sie sich selbst aufruft, wenn also in ihrem Körper ein Aufruf derselben Funktion vorkommt. Ein einfaches Beispiel:
def f():
'''Returns the result of calling itself.'''
return f()
Diese Funktion hat keine Argumente. Auch macht sie offensichtlich keinen
Sinn. Um ihren Rückgabewert zu bestimmen, ruft sie sich selbst mit denselben
Argumenten (keinen) auf. Sie ist also zirkulär definiert. Beim Versuch, den
Funktionsaufruf f()
auszuwerten, würde das zu einer
Endlosschleife führen, wenn der Python-Interpreter nicht nach einer gewissen
Anzahl rekursiver Aufrufe aufgeben würde:
>>> f()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in f
File "<stdin>", line 2, in f
File "<stdin>", line 2, in f
[Previous line repeated 995 more times]
RecursionError: maximum recursion depth exceeded
Und dennoch können rekursive Funktionen nützlich sein. Nämlich immer dann, wenn man, um ein Problem zu lösen, zunächst eine kleinere Version desselben Problems lösen muss. Und um diese kleinere Version zu lösen, wiederum eine noch kleinere Version. Bis man irgendwann bei einem so kleinen Problem landet, dem sogenannten Rekursionsanfang, dass man es ohne einen weiteren rekursiven Aufruf lösen kann.
Ein klassisches Beispiel aus der Mathematik ist die Berechnung der Fakultät einer nichtnegativen ganzen Zahl n, geschrieben n!. Zum Beispiel ist 4! = 4⋅3⋅2⋅1, 10! = 10⋅9⋅8⋅7⋅6⋅5⋅4⋅3⋅2⋅1, etc. Die allgemeine Definition der Fakultät n! unterscheidet zwei Fälle: Entweder ist n=0. Dann gilt: n!=1. Das ist der Rekursionsanfang. Oder n ist eine positive ganze Zahl. Dann gilt: n!=n⋅(n-1)!, die Fakultät von n ist also das Produkt von n und der Fakultät von n-1. Dieser zweite Fall ist der Rekursionsschritt. Um die Fakultät einer positiven Ganzzahl zu berechnen, muss man also zunächst die Fakultät der nächstkleineren Ganzzahl kennen. Das ist aber kein Problem, denn wenn man so rechnet, kommt man irgendwann bei 0! raus, dem Rekursionsanfang. Dieser lässt sich ohne weiteren Rekursionsschritt lösen.
Eine Python-Funktion zur Berechnung der Fakultät könnte also so aussehen:
def fac(n):
'''Returns the factorial of the nonnegative integer n.'''
if n == 0:
return 1
else:
return n * fac(n - 1)
Die Unterscheidung zwischen Rekursionsanfang und Rekursionsschritt ist mit
Hilfe einer Verzweigung umgesetzt. Es kommt nicht zu einer Endlosschleife, da
die Funktion jedes Mal mit einem anderen Argument aufgerufen wird und
schließlich der Rekursionsanfang (n == 0
) erreicht
wird.
Rekursive Datenstrukturen
Einer der wichtigen Einsatzzwecke für Rekursion ist das Verarbeiten von rekursiven Datenstrukturen. Eine rekursive Datenstruktur ist eine Datenstruktur, die wiederum Datenstrukturen desselben Typs enthalten kann. Python-Listen sind zum Beispiel rekursive Datenstrukturen, da sie wiederum Python-Listen enthalten können. Zum Beispiel:
[2, [3, 4, [5, 6]], [7, 8]]
In dieser Liste sind wiederum Listen verschachtelt, und darin wiederum
Listen. Im Prinzip könnte diese Verschachtelung bis zu einer beliebigen Tiefe
weitergehen. (Für die Computerlinguistik ist das u.a. für die automatische
Analyse von Schachtelsätzen interessant, aber bleiben wir für den Moment bei
dem abstrakten Beispiel.) Wie finden wir z.B. heraus, ob die Zahl 5
irgendwo als ein verschachteltes Element der Liste
vorkommt? Der in
-Operator macht das nicht, der guckt
nur die direkten Elemente der Liste an:
>>> 5 in [2, [3, 4, [5, 6]], [7, 8]]
False
Die Lösung ist eine rekursive Funktion – nennen wir sie deep_in
–, die das gesuchte Objekt nicht nur bei
den Elementen der Liste sucht, sondern auch bei den Elemente der Elemente, den
Elementen der Elemente der Elemente usw. deep_in
wird
mit zwei Argumenten aufgerufen: dem gesuchten Objekt x
und dem durchsuchten Objekt l
:
def deep_in(x, l):
'''Returns whether x occurs in l.
x "occurs" in l if x and l are equal or l is a list and x "occurs" in one
of l's elements.'''
if x == l:
return True
if isinstance(l, list):
for e in l:
if deep_in(x, e):
return True
return False
Der Aufruf isinstance(l, list)
gibt True
zurück, wenn l
eine Liste ist, sonst False
.
Sind x
und l
gleich,
gibt die Funktion direkt True
zurück. Das ist der
Rekursionsanfang. Andernfalls, falls l
eine Liste ist, werden seine Elemente entsprechend durchsucht. Das ist der
Rekursionsschritt. Ist die Suche erfolgreich, wird True
zurückgegeben. Andernfalls wird False
zurückgegeben.
Zum Beispiel:
>>> deep_in(1, [2, [3, 4, [5, 6]], [7, 8]])
False
>>> deep_in(2, [2, [3, 4, [5, 6]], [7, 8]])
True
>>> deep_in(3, [2, [3, 4, [5, 6]], [7, 8]])
True
>>> deep_in(4, [2, [3, 4, [5, 6]], [7, 8]])
True
>>> deep_in(5, [2, [3, 4, [5, 6]], [7, 8]])
True
>>> deep_in(6, [2, [3, 4, [5, 6]], [7, 8]])
True
>>> deep_in(7, [2, [3, 4, [5, 6]], [7, 8]])
True
>>> deep_in(8, [2, [3, 4, [5, 6]], [7, 8]])
True
>>> deep_in(9, [2, [3, 4, [5, 6]], [7, 8]])
False