Намагаючись відповісти на таке запитання, вам дійсно потрібно вказати обмеження коду, який ви пропонуєте як рішення. Якби мова йшла лише про виступи, я б не заперечував надто багато, але більшість кодів, запропонованих як рішення (включаючи прийняту відповідь), не змогли сплостити жоден список, глибина якого перевищує 1000.
Коли я кажу більшість кодів я маю на увазі всі коди, які використовують будь-яку форму рекурсії (або називають стандартну функцію бібліотеки, яка є рекурсивною). Усі ці коди виходять з ладу, оскільки для кожного здійсненого рекурсивного виклику стек (виклик) зростає на одну одиницю, а стек виклику (за замовчуванням) пітона має розмір 1000.
Якщо ви не надто знайомі зі стеком викликів, можливо, допоможе наступне (інакше ви можете просто прокрутити до реалізації ).
Розмір стека викликів та рекурсивне програмування (аналогія підземелля)
Пошук скарбу та вихід
Уявіть, що ви заходите у величезну підземелля з пронумерованими кімнатами , шукаючи скарб. Ви не знаєте місця, але у вас є деякі вказівки, як знайти скарб. Кожен показ є загадкою (складність різна, але ви не можете передбачити, наскільки вони будуть важкими). Ви вирішили трохи подумати над стратегією економії часу, зробите два спостереження:
- Важко (довго) знайти скарб, оскільки вам доведеться розгадати (потенційно важкі) загадки, щоб потрапити туди.
- Після того, як скарб знайдений, повернення до входу може бути простим, вам просто потрібно використовувати той самий шлях у іншому напрямку (хоча для запам’ятовування цього шляху потрібно трохи пам’яті).
Входячи в підземелля, ви помічаєте тут невеликий зошит . Ви вирішите використовувати його для запису кожної кімнати, з якої виходите після розгадування загадки (під час входу в нову кімнату), таким чином ви зможете повернутися назад до входу. Це геніальна ідея, ви навіть не витратите ні цента на реалізацію своєї стратегії.
Ви входите в підземелля, з великим успіхом вирішуючи перші 1001 загадки, але ось виходить щось, чого ви не планували, у вас не залишилось місця в зошиті, який ви позичили. Ви вирішили відмовитися від пошуків, оскільки не хочете мати скарб, ніж назавжди загубитися всередині підземелля (це справді виглядає розумно).
Виконання рекурсивної програми
В основному, це саме те саме, що і пошук скарбу. Підземелля - це пам’ять комп’ютера , ваша мета зараз - не знайти скарб, а обчислити якусь функцію (знайти f (x) для даного x ). Індикації просто підпрограми, які допоможуть вам вирішити f (x) . Ваша стратегія така ж, як стратегія стека викликів , ноутбук - це стек, номери - це зворотні адреси функцій:
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))
Проблема, з якою ви зіткнулися в підземеллі, буде однаковою тут, стек викликів має обмежений розмір (тут 1000), і тому, якщо ви вводите занадто багато функцій, не повертаючись назад, то ви заповните стек викликів і отримаєте помилку на кшталт "Шановний авантюрист, мені дуже шкода, але ваш ноутбук заповнений" :RecursionError: maximum recursion depth exceeded
. Зауважте, що для заповнення стека викликів вам не потрібна рекурсія, але дуже малоймовірно, що нерекурсивна програма викликає 1000 функцій, не повертаючись ніколи. Важливо також розуміти, що після повернення з функції стек виклику звільняється від використовуваної адреси (звідси і назва "стек", повертаюча адреса вводиться перед введенням функції та вилучається при поверненні). В окремому випадку простої рекурсії (функціяf
що дзвонить собі раз і знову -) ви будете входити f
знову і знову, поки обчислення не будуть закінчені (поки скарб не знайдеться) і повернетесь, f
поки не повернетесь туди, куди ви зателефонували f
в першу чергу. Стек викликів ніколи не буде звільнений від нічого до кінця, коли він буде звільнений від усіх зворотних адрес одна за одною.
Як уникнути цього питання?
Це насправді досить просто: "не використовуйте рекурсії, якщо ви не знаєте, наскільки глибоко це може пройти". Це не завжди так, як в деяких випадках, рекурсія виклику хвоста може бути оптимізована (TCO) . Але в python це не так, і навіть «добре написана» рекурсивна функція не оптимізує використання стеку. З цього питання є цікавий пост від Гуйдо: Усунення рекурсії хвоста .
Існує техніка, яку ви можете використовувати, щоб зробити будь-яку рекурсивну функцію ітеративною, ця методика, яку ми могли б назвати, принесе свій власний ноутбук . Наприклад, у нашому конкретному випадку ми просто вивчаємо список, вхід до кімнати є рівнозначним введенню підспису. Питання, яке ви повинні задати собі, - як я можу повернутися зі списку до його батьківського списку? Відповідь не така складна, повторюйте наступне, доки поле не stack
буде порожнім:
- натисніть на поточний список
address
і index
в a stack
при введенні нового підсписку (зауважте, що адреса списку + індекс - це також адреса, тому ми просто використовуємо точно таку саму техніку, яку використовує стек викликів);
- кожного разу, коли предмет знайдеться,
yield
він (або додавати їх у список);
- як тільки список буде повністю досліджений, поверніться до батьківського списку, використовуючи
stack
return address
(і index
) .
Також зауважте, що це еквівалентно DFS у дереві, де деякі вузли є підсписками, A = [1, 2]
а деякі - простими елементами: 0, 1, 2, 3, 4
(для L = [0, [1,2], 3, 4]
). Дерево виглядає так:
L
|
-------------------
| | | |
0 --A-- 3 4
| |
1 2
Попереднє замовлення DFS: L, 0, A, 1, 2, 3, 4. Пам'ятайте, що для реалізації ітеративного DFS вам також потрібен стек. Реалізація, яку я запропонував раніше, призвела до наявності наступних станів (для stack
і 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]
У цьому прикладі максимальний розмір стека дорівнює 2, оскільки список вхідних даних (а отже, дерево) має глибину 2.
Впровадження
Для реалізації, у python ви можете трохи спростити, використовуючи ітератори замість простих списків. Посилання на (під) ітераторів використовуватимуться для зберігання зворотних адрес підспісків (замість того, щоб мати як адресу списку, так і індекс). Це не велика різниця, але я вважаю, що це читабельніше (а також трохи швидше):
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)
Крім того, зауважте, що в is_list_like
мені isinstance(item, list)
, яке можна змінити, щоб обробляти більше типів вводу, тут я просто хотів мати найпростішу версію, де (ітерабельна) - це лише список. Але ви також можете це зробити:
def is_list_like(item):
try:
iter(item)
return not isinstance(item, str) # strings are not lists (hmm...)
except TypeError:
return False
Це розглядає рядки як "прості елементи", тому flatten_iter([["test", "a"], "b])
повертається, ["test", "a", "b"]
а не ["t", "e", "s", "t", "a", "b"]
. Зауважте, що в такому випадку iter(item)
викликується двічі на кожен елемент, давайте зробимо вигляд, що читач робить вправу чистішою.
Тестування та зауваження щодо інших реалізацій
Зрештою, пам’ятайте, що ви не можете надрукувати нескінченно вкладений список, L
використовуючи, print(L)
оскільки внутрішньо він використовуватиме рекурсивні дзвінки до __repr__
( RecursionError: maximum recursion depth exceeded while getting the repr of an object
). З тієї ж причини рішення щодо flatten
залучення str
не вдасться з тим самим повідомленням про помилку.
Якщо вам потрібно протестувати рішення, ви можете використовувати цю функцію для створення простого вкладеного списку:
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
Що дає: build_deep_list(5)
>>> [4, [3, [2, [1, [0]]]]]
.