그러한 질문에 대답하려고 할 때 실제로 솔루션으로 제안하는 코드의 한계를 제시해야합니다. 성능에만 관심이 있다면별로 신경 쓰지 않지만 솔루션으로 제안 된 대부분의 코드 (응답 포함)는 깊이가 1000보다 큰 목록을 평평하게하지 못합니다.
내가 대부분의 코드를 말할 때 모든 형태의 재귀를 사용하는 모든 코드를 의미합니다 (또는 재귀적인 표준 라이브러리 함수를 호출합니다). 모든 재귀 호출에 대해 (호출) 스택이 한 단위 씩 증가하고 (기본) 파이썬 호출 스택의 크기가 1000이기 때문에 이러한 코드는 모두 실패합니다.
호출 스택에 익숙하지 않은 경우 다음이 도움이 될 것입니다 (그렇지 않으면 구현으로 스크롤 할 수 있습니다 ).
콜 스택 크기 및 재귀 프로그래밍 (던전 유추)
보물 찾기 및 종료
번호가 매겨진 방 으로 거대한 지하 감옥에 들어가 보물을 찾고 있다고 상상해보십시오 . 당신은 장소를 모르지만 보물을 찾는 방법에 대한 표시 가 있습니다. 각 표시는 수수께끼입니다 (난이도는 다르지만 얼마나 힘들지는 예측할 수 없습니다). 시간을 절약하기위한 전략에 대해 약간 생각하고 두 가지 관찰을합니다.
- 보물을 찾기가 어렵 기 때문에 (잠재적으로) 수수께끼를 풀어야합니다.
- 보물이 발견되면 입구로 돌아가는 것이 쉬울 수 있지만 다른 방향으로 동일한 경로를 사용해야합니다 (경로를 기억하려면 약간의 메모리가 필요하지만).
지하 감옥에 들어가면 여기에 작은 공책이 있습니다. 수수께끼를 풀고 (새 방에 들어올 때) 나가는 모든 방을 적어두기로 결정하면 입구로 돌아갈 수 있습니다. 그것은 천재적인 아이디어입니다, 당신 은 심지어 센트를 소비하지 않을 것입니다 입니다. 전략을 구현 .
당신은 지하 감옥에 들어가서 첫 번째 1001 수수께끼를 성공적으로 해결했지만 여기에 계획하지 않은 것이 나오고, 빌린 노트북에 남은 공간이 없습니다. 던전 안에서 영원히 잃어버린 것보다 보물을 좋아하지 않기 때문에 퀘스트 를 포기 하기로 결정합니다 (실제로는 똑똑해 보입니다).
재귀 프로그램 실행
기본적으로 보물을 찾는 것과 똑같습니다. 던전은 컴퓨터의 메모리입니다 . 이제 목표는 보물을 찾는 것이 아니라 일부 함수 를 계산하는 것입니다 ( 주어진 x에 대한 f (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을 호출 할 가능성은 거의 없습니다. 함수에서 리턴 한 후에는 호출 스택이 사용 된 주소에서 해제됩니다 (따라서 이름 "stack", 리턴 주소는 함수에 들어가기 전에 푸시되고 리턴 할 때 꺼내집니다). 간단한 재귀의 특별한 경우 (함수f
f
계산이 끝날 때까지 (보물이 발견 될 때까지) 반복해서 입력 하고 처음부터 f
전화했던 곳으로 돌아갈 때까지 돌아갑니다 f
. 호출 스택은 모든 리턴 주소에서 차례로 해제 될 때까지 어떤 것도 해제되지 않습니다.
이 문제를 피하는 방법?
실제로는 매우 간단합니다. "얼마나 깊이 갈 수 있는지 모르는 경우 재귀를 사용하지 마십시오". 경우에 따라 Tail Call 재귀를 최적화 할 수있는 것처럼 항상 사실은 아닙니다 (TCO) . 그러나 파이썬에서는 그렇지 않습니다. 심지어 "잘 작성된"재귀 함수조차도 스택 사용을 최적화 하지 않습니다 . 이 질문에 대한 귀도의 흥미로운 게시물이 있습니다 : Tail Recursion Elimination .
재귀 함수를 반복적으로 만드는 데 사용할 수있는 기술이 있습니다.이 기술을 사용 하여 자신의 노트북을 가져올 수 있습니다 . 예를 들어, 특별한 경우에 우리는 단순히 목록을 탐색하고 방을 입력하는 것은 하위 목록을 입력하는 것과 같습니다. 스스로 질문해야 할 것은 목록에서 부모 목록으로 어떻게 돌아갈 수 있습니까? 대답은 그렇게 복잡하지 않습니다. stack
이 비어 있을 때까지 다음을 반복하십시오 .
- 현재리스트를 푸시
address
및 index
A의 stack
새로운 하위 목록 입력 (주리스트 어드레스 + 인덱스 따라서 우리는 단지 호출 스택에서 사용되는 동일한 기술을 사용하여, 또한 어드레스 있음);
- 항목을 찾을 때마다
yield
(또는 목록에 추가)
- 목록이 완전히 탐색되면
stack
return address
(및 index
)을 사용하여 상위 목록으로 돌아갑니다 .
또한 이것은 일부 노드가 하위 목록 A = [1, 2]
이고 일부는 간단한 항목 인 0, 1, 2, 3, 4
(for L = [0, [1,2], 3, 4]
) 트리의 DFS와 같습니다 . 나무는 다음과 같습니다
L
|
-------------------
| | | |
0 --A-- 3 4
| |
1 2
DFS 순회 예약 주문은 L, 0, A, 1, 2, 3, 4입니다. 반복적 인 DFS를 구현하려면 스택이 "필요"합니다. 구현 I합니다 (상태가 다음 필요에 따라서 이전에 제안 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입니다.
이행
구현을 위해 파이썬에서는 간단한 목록 대신 반복자를 사용하여 조금 단순화 할 수 있습니다. (반복자) 반복자에 대한 참조는 ( 목록 주소와 색인이 아닌) 하위 목록 리턴 주소 를 저장하는 데 사용됩니다 . 이것은 큰 차이는 아니지만 이것이 더 읽기 쉽다고 느낍니다.
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]]]]]
.