Próbując odpowiedzieć na takie pytanie, naprawdę musisz podać ograniczenia kodu, który zaproponujesz jako rozwiązanie. Gdyby chodziło tylko o występy, nie miałbym nic przeciwko, ale większość kodów zaproponowanych jako rozwiązanie (w tym zaakceptowana odpowiedź) nie spłaszcza żadnej listy o głębokości większej niż 1000.
Kiedy mówię większość kodów, mam na myśli wszystkie kody, które używają dowolnej formy rekurencji (lub wywołują standardową funkcję biblioteki, która jest rekurencyjna). Wszystkie te kody zawodzą, ponieważ dla każdego z wykonanych wywołań rekurencyjnych stos (wywołań) rośnie o jedną jednostkę, a stos (domyślny) wywołań python ma rozmiar 1000.
Jeśli nie znasz się zbytnio na stosie wywołań, być może poniższe informacje pomogą (w przeciwnym razie możesz po prostu przewinąć do Implementacji ).
Rozmiar stosu wywołań i programowanie rekurencyjne (analogia lochów)
Odnalezienie skarbu i wyjście
Wyobraź sobie, że wchodzisz do ogromnego lochu z ponumerowanymi pokojami i szukasz skarbu. Nie znasz tego miejsca, ale masz pewne wskazówki, jak znaleźć skarb. Każde wskazanie jest zagadką (trudność jest różna, ale nie można przewidzieć, jak trudne będą). Postanawiasz trochę pomyśleć o strategii oszczędzania czasu, robisz dwie obserwacje:
- Trudno (długo) znaleźć skarb, ponieważ będziesz musiał rozwiązać (potencjalnie trudne) zagadki, aby się tam dostać.
- Po znalezieniu skarbu powrót do wejścia może być łatwy, musisz po prostu podążać tą samą ścieżką w drugą stronę (choć to wymaga trochę pamięci, aby przywołać twoją ścieżkę).
Wchodząc do lochu, zauważysz mały notatnik . Postanawiasz użyć go do spisania każdego pomieszczenia, z którego wyjdziesz po rozwiązaniu zagadki (po wejściu do nowego pokoju), w ten sposób będziesz mógł wrócić z powrotem do wejścia. To genialny pomysł, nawet nie wydasz ani grosza wdrożenie swojej strategii.
Wchodzisz do lochu, rozwiązując z powodzeniem pierwsze 1001 zagadek, ale nadchodzi coś, czego nie planowałeś, nie ma już miejsca w pożyczonym notesie. Postanawiasz porzucić swoją misję, ponieważ wolisz nie mieć skarbu niż zagubić się na zawsze w lochu (to wygląda naprawdę mądrze).
Wykonywanie programu rekurencyjnego
Zasadniczo jest to dokładnie to samo, co znalezienie skarbu. Loch jest pamięcią komputera , Twoim celem nie jest teraz znalezienie skarbu, ale obliczenie jakiejś funkcji (znajdź f (x) dla danego x ). Wskazania są po prostu podprogramami, które pomogą ci rozwiązać f (x) . Twoja strategia jest taka sama jak strategia stosu wywołań , notebook to stos, pokoje to adresy zwrotne funkcji:
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))
Problem napotkany w lochu będzie tutaj taki sam, stos wywołań ma skończony rozmiar (tutaj 1000), a zatem jeśli wprowadzisz zbyt wiele funkcji bez powrotu, wypełnisz stos wywołań i pojawi się błąd, który wygląda jak „Drogi poszukiwaczu przygód, bardzo mi przykro, ale twój notatnik jest pełny” :RecursionError: maximum recursion depth exceeded
. Zauważ, że nie potrzebujesz rekurencji, aby wypełnić stos wywołań, ale jest bardzo mało prawdopodobne, aby nierekurencyjny program wywoływał 1000 funkcji bez powrotu. Ważne jest również, aby zrozumieć, że po powrocie z funkcji stos wywołań jest zwalniany z użytego adresu (stąd nazwa „stos”, adres zwrotny jest wpychany przed wejściem do funkcji i wyciągany podczas powrotu). W szczególnym przypadku prostej rekurencji (funkcjaf
które wywołują się raz po raz - raz za razem - będziesz wchodził f
w kółko, aż obliczenia się zakończą (aż do znalezienia skarbu) i powrócisz, f
dopóki nie wrócisz do miejsca, do którego dzwoniłeś f
w pierwszej kolejności. Stos wywołań nigdy nie zostanie uwolniony od niczego, dopóki nie zostanie zwolniony ze wszystkich adresów zwrotnych jeden po drugim.
Jak uniknąć tego problemu?
To właściwie dość proste: „nie używaj rekurencji, jeśli nie wiesz, jak głęboko może ona sięgać”. Nie zawsze jest to prawdą, ponieważ w niektórych przypadkach rekurencję Tail Call można zoptymalizować (TCO) . Ale w Pythonie tak nie jest, a nawet „dobrze napisana” funkcja rekurencyjna nie zoptymalizuje wykorzystania stosu. Guido ma interesujący post na ten temat: Eliminacja rekurencji ogona .
Istnieje technika, której można użyć do iteracji dowolnej funkcji rekurencyjnej, którą możemy nazwać przyniesieniem własnego notatnika . Na przykład w naszym konkretnym przypadku po prostu przeglądamy listę, wejście do pokoju jest równoznaczne z wejściem na listę podrzędną. Pytanie, które powinieneś sobie zadać, to jak mogę wrócić z listy do listy nadrzędnej? Odpowiedź nie jest tak skomplikowana, powtarzaj następujące czynności, ażstack
będzie pusta:
- wcisnąć aktualną listę
address
i index
w stack
przypadku wprowadzania nowego podmenu (Należy pamiętać, że adres lista + indeks jest również adres, dlatego wystarczy użyć dokładnie taką samą technikę stosowaną przez stos wywołań);
- za każdym razem, gdy element zostanie znaleziony,
yield
to (lub dodaj je do listy);
- gdy lista zostanie w pełni zbadana, wróć do listy nadrzędnej za pomocą
stack
returnaddress
(i index
) .
Zauważ również, że jest to równoważne z DFS w drzewie, w którym niektóre węzły są listami podrzędnymi, A = [1, 2]
a niektóre są prostymi elementami: 0, 1, 2, 3, 4
(dlaL = [0, [1,2], 3, 4]
). Drzewo wygląda następująco:
L
|
-------------------
| | | |
0 --A-- 3 4
| |
1 2
Wstępne zamówienie przejścia DFS to: L, 0, A, 1, 2, 3, 4. Pamiętaj, że aby zaimplementować iteracyjny DFS, musisz również „mieć stos”. Wdrożenie, które zaproponowałem wcześniej, skutkuje następującymi stanami (dla stack
iflat_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]
W tym przykładzie maksymalny rozmiar stosu wynosi 2, ponieważ lista wejściowa (a zatem drzewo) ma głębokość 2.
Realizacja
W przypadku implementacji w Pythonie można nieco uprościć, używając iteratorów zamiast prostych list. Odniesienia do (pod) iteratorów będą używane do przechowywania adresów zwrotnych list podrzędnych (zamiast posiadania zarówno adresu listy, jak i indeksu). Nie jest to duża różnica, ale uważam, że jest to bardziej czytelne (a także nieco szybsze):
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)
Zauważ też, że w „ is_list_like
Mam” isinstance(item, list)
, który można zmienić w celu obsługi większej liczby typów danych wejściowych, tutaj chciałem mieć najprostszą wersję, w której (iterowalna) jest tylko listą. Ale możesz też to zrobić:
def is_list_like(item):
try:
iter(item)
return not isinstance(item, str) # strings are not lists (hmm...)
except TypeError:
return False
Traktuje to ciągi jako „proste elementy” i dlatego flatten_iter([["test", "a"], "b])
wróci ["test", "a", "b"]
i nie ["t", "e", "s", "t", "a", "b"]
. Zauważ, że w takim przypadkuiter(item)
jest wywoływany dwukrotnie na każdym elemencie, udawajmy, że jest to ćwiczenie dla czytelnika, aby uczynić to czystszym.
Testowanie i uwagi na temat innych wdrożeń
W końcu, należy pamiętać, że nie można wydrukować nieskończenie zagnieżdżonej listy L
używając print(L)
ponieważ wewnętrznie użyje do wywołań cyklicznych __repr__
( RecursionError: maximum recursion depth exceeded while getting the repr of an object
). Z tego samego powodu rozwiązania flatten
dotyczące str
niepowodzenia zakończy się niepowodzeniem z tym samym komunikatem o błędzie.
Jeśli potrzebujesz przetestować swoje rozwiązanie, możesz użyć tej funkcji do wygenerowania prostej listy zagnieżdżonej:
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
Co daje: build_deep_list(5)
>>> [4, [3, [2, [1, [0]]]]]
.