Wenn Sie versuchen, eine solche Frage zu beantworten, müssen Sie wirklich die Einschränkungen des Codes angeben, den Sie als Lösung vorschlagen. Wenn es nur um Leistungen ginge, würde es mir nichts ausmachen, aber die meisten als Lösung vorgeschlagenen Codes (einschließlich der akzeptierten Antwort) können keine Liste mit einer Tiefe von mehr als 1000 abflachen.
Wenn ich die meisten Codes sage sage, meine ich alle Codes, die irgendeine Form der Rekursion verwenden (oder eine rekursive Standardbibliotheksfunktion aufrufen). Alle diese Codes schlagen fehl, da der (Aufruf-) Stapel für jeden rekursiven Aufruf um eine Einheit wächst und der (Standard-) Python-Aufrufstapel eine Größe von 1000 hat.
Wenn Sie mit dem Aufrufstapel nicht allzu vertraut sind, hilft möglicherweise Folgendes: Andernfalls können Sie einfach zur Implementierung scrollen .
Aufrufstapelgröße und rekursive Programmierung (Dungeon-Analogie)
Den Schatz finden und gehen
Stellen Sie sich vor, Sie betreten einen riesigen Kerker mit nummerierten Räumen und suchen nach einem Schatz. Sie kennen den Ort nicht, haben aber einige Hinweise, wie Sie den Schatz finden können. Jede Anzeige ist ein Rätsel (Schwierigkeitsgrad variiert, aber Sie können nicht vorhersagen, wie schwer sie sein werden). Sie beschließen, ein wenig über eine Strategie nachzudenken, um Zeit zu sparen, und machen zwei Beobachtungen:
- Es ist schwierig (lang), den Schatz zu finden, da Sie (möglicherweise schwierige) Rätsel lösen müssen, um dorthin zu gelangen.
- Sobald der Schatz gefunden wurde, kann es einfach sein, zum Eingang zurückzukehren. Sie müssen nur denselben Pfad in die andere Richtung verwenden (obwohl dies ein wenig Speicher benötigt, um sich an Ihren Pfad zu erinnern).
Wenn Sie den Dungeon betreten, sehen Sie hier ein kleines Notizbuch . Sie beschließen, damit jeden Raum aufzuschreiben, den Sie nach dem Lösen eines Rätsels verlassen (wenn Sie einen neuen Raum betreten). Auf diese Weise können Sie zum Eingang zurückkehren. Das ist eine geniale Idee, Sie werden nicht einmal einen Cent für die Umsetzung Ihrer Strategie ausgeben .
Sie betreten den Dungeon und lösen mit großem Erfolg die ersten 1001 Rätsel, aber hier kommt etwas, das Sie nicht geplant hatten. Sie haben keinen Platz mehr in dem Notizbuch, das Sie ausgeliehen haben. Sie beschließen , Ihre Suche abzubrechen, da Sie es vorziehen, den Schatz nicht zu haben, als für immer im Dungeon verloren zu sein (das sieht in der Tat klug aus).
Ausführen eines rekursiven Programms
Im Grunde ist es genau das Gleiche wie den Schatz zu finden. Der Dungeon ist der Speicher des Computers . Ihr Ziel ist es nun nicht, einen Schatz zu finden, sondern eine Funktion zu berechnen (finden Sie f (x) für ein gegebenes x ). Die Angaben sind einfach Unterprogramme, die Ihnen beim Lösen von f (x) helfen . Ihre Strategie ist dieselbe wie die Call-Stack- Strategie, das Notebook ist der Stack, die Räume sind die Rücksprungadressen der Funktionen:
x = ["over here", "am", "I"]
y = sorted(x) # You're about to enter a room named `sorted`, note down the current room address here so you can return back: 0x4004f4 (that room address looks weird)
# Seems like you went back from your quest using the return address 0x4004f4
# Let's see what you've collected
print(' '.join(y))
Das Problem, auf das Sie im Dungeon gestoßen sind, ist hier dasselbe. Der Aufrufstapel hat eine endliche Größe (hier 1000). Wenn Sie also zu viele Funktionen eingeben, ohne zurückzukehren, füllen Sie den Aufrufstapel und haben einen Fehler, der aussieht wie "Lieber Abenteurer, es tut mir sehr leid, aber dein Notizbuch ist voll" : das ruft sich einmal auf - immer und immer wieder -) du wirst immer wieder eingeben, bis die Berechnung abgeschlossen ist (bis der Schatz gefunden ist) und von zurückkehren bis Sie zu dem Ort zurückkehren, an dem Sie zuerst angerufen haben. Der Aufrufstapel wird bis zum Ende, an dem er nacheinander von allen Rücksprungadressen befreit wird, niemals von irgendetwas befreit.RecursionError: maximum recursion depth exceeded
. Beachten Sie, dass Sie keine Rekursion benötigen, um den Aufrufstapel zu füllen. Es ist jedoch sehr unwahrscheinlich, dass ein nicht rekursiver Programmaufruf 1000 funktioniert, ohne jemals zurückzukehren. Es ist auch wichtig zu verstehen, dass nach der Rückkehr von einer Funktion der Aufrufstapel von der verwendeten Adresse befreit wird (daher der Name "Stapel", die Rücksprungadresse wird vor der Eingabe einer Funktion eingegeben und bei der Rückkehr herausgezogen). Im Sonderfall einer einfachen Rekursion (eine Funktionf
f
f
f
Wie vermeide ich dieses Problem?
Das ist eigentlich ziemlich einfach: "Verwenden Sie keine Rekursion, wenn Sie nicht wissen, wie tief sie gehen kann". Dies ist nicht immer der Fall, da in einigen Fällen die Tail Call-Rekursion optimiert werden kann (TCO) . In Python ist dies jedoch nicht der Fall, und selbst eine "gut geschriebene" rekursive Funktion optimiert die Stapelverwendung nicht . Zu dieser Frage gibt es einen interessanten Beitrag von Guido: Eliminierung der Schwanzrekursion .
Es gibt eine Technik, mit der Sie jede rekursive Funktion iterativ machen können. Diese Technik können wir als Ihr eigenes Notizbuch bezeichnen . In unserem speziellen Fall untersuchen wir beispielsweise einfach eine Liste. Das Betreten eines Raums entspricht dem Eingeben einer Unterliste. Die Frage, die Sie sich stellen sollten, lautet: Wie kann ich von einer Liste zu ihrer übergeordneten Liste zurückkehren? Die Antwort ist nicht so komplex. Wiederholen Sie Folgendes, bis das stack
leer ist:
- Drücken Sie die aktuelle Liste
address
und index
in a, stack
wenn Sie eine neue Unterliste eingeben (beachten Sie, dass eine Listenadresse + ein Index auch eine Adresse ist, daher verwenden wir genau die gleiche Technik, die vom Aufrufstapel verwendet wird).
- Jedes Mal, wenn ein Element gefunden wird, wird
yield
es angezeigt (oder in eine Liste aufgenommen).
- Sobald eine Liste vollständig durchsucht ist,
stack
kehren Sieaddress
index
mit return (und ) zur übergeordneten Liste zurück .
Beachten Sie auch, dass dies einer DFS in einem Baum entspricht, in dem einige Knoten Unterlisten A = [1, 2]
und andere einfache Elemente sind: 0, 1, 2, 3, 4
(für L = [0, [1,2], 3, 4]
). Der Baum sieht so aus:
L
|
-------------------
| | | |
0 --A-- 3 4
| |
1 2
Die Vorbestellung für die DFS-Durchquerung lautet: L, 0, A, 1, 2, 3, 4. Denken Sie daran, dass Sie zur Implementierung einer iterativen DFS auch einen Stapel "benötigen". Die Implementierung, die ich zuvor vorgeschlagen habe, führt zu folgenden Zuständen (für die stack
und die flat_list
):
init.: stack=[(L, 0)]
**0**: stack=[(L, 0)], flat_list=[0]
**A**: stack=[(L, 1), (A, 0)], flat_list=[0]
**1**: stack=[(L, 1), (A, 0)], flat_list=[0, 1]
**2**: stack=[(L, 1), (A, 1)], flat_list=[0, 1, 2]
**3**: stack=[(L, 2)], flat_list=[0, 1, 2, 3]
**3**: stack=[(L, 3)], flat_list=[0, 1, 2, 3, 4]
return: stack=[], flat_list=[0, 1, 2, 3, 4]
In diesem Beispiel beträgt die maximale Größe des Stapels 2, da die Eingabeliste (und damit der Baum) die Tiefe 2 haben.
Implementierung
Für die Implementierung können Sie in Python ein wenig vereinfachen, indem Sie Iteratoren anstelle einfacher Listen verwenden. Verweise auf die (Unter-) Iteratoren werden zum Speichern von Unterlisten-Rücksprungadressen verwendet (anstatt sowohl die Listenadresse als auch den Index zu haben). Dies ist kein großer Unterschied, aber ich denke, dies ist besser lesbar (und auch etwas schneller):
def flatten(iterable):
return list(items_from(iterable))
def items_from(iterable):
cursor_stack = [iter(iterable)]
while cursor_stack:
sub_iterable = cursor_stack[-1]
try:
item = next(sub_iterable)
except StopIteration: # post-order
cursor_stack.pop()
continue
if is_list_like(item): # pre-order
cursor_stack.append(iter(item))
elif item is not None:
yield item # in-order
def is_list_like(item):
return isinstance(item, list)
Beachten Sie auch, dass is_list_like
ich in I have isinstance(item, list)
, das geändert werden könnte, um mehr Eingabetypen zu verarbeiten, hier nur die einfachste Version haben wollte, in der (iterable) nur eine Liste ist. Das können Sie aber auch tun:
def is_list_like(item):
try:
iter(item)
return not isinstance(item, str) # strings are not lists (hmm...)
except TypeError:
return False
Dies betrachtet Zeichenfolgen als "einfache Elemente" und wird daher flatten_iter([["test", "a"], "b])
zurückgegeben ["test", "a", "b"]
und nicht ["t", "e", "s", "t", "a", "b"]
. Beachten Sie, dass in diesem Fall iter(item)
jedes Element zweimal aufgerufen wird. Stellen Sie sich vor, es sei eine Übung für den Leser, dies sauberer zu machen.
Tests und Anmerkungen zu anderen Implementierungen
Denken Sie am Ende daran, dass Sie eine unendlich verschachtelte Liste nicht mit drucken können L
, print(L)
da intern rekursive Aufrufe von __repr__
( RecursionError: maximum recursion depth exceeded while getting the repr of an object
) verwendet werden. Aus dem gleichen Grund flatten
schlagen Lösungen für das Einbeziehen str
mit derselben Fehlermeldung fehl.
Wenn Sie Ihre Lösung testen müssen, können Sie mit dieser Funktion eine einfache verschachtelte Liste erstellen:
def build_deep_list(depth):
"""Returns a list of the form $l_{depth} = [depth-1, l_{depth-1}]$
with $depth > 1$ and $l_0 = [0]$.
"""
sub_list = [0]
for d in range(1, depth):
sub_list = [d, sub_list]
return sub_list
Welches gibt: build_deep_list(5)
>>> [4, [3, [2, [1, [0]]]]]
.