해당 문제는 프로그래머스에서 풀어볼 수 있기에 문제 설명은 생략하겠습니다.
사실 이 문제는 해결 방법을 초기부터 카카오측에서 알려주고 시작을 했습니다.
"이진트리 그래프를 만들고 전위순회 후위순회 해봐" 라는 문제였고 사실 그래프만드는 코드가 조금 까다로울 수 있지만 전위순회, 중위순회, 후위순회는 그래프만 제대로 만들었다면 root 호출 순서에 따라 달라지므로 어렵지 않게 풀 수 있을 것 같았습니다.
이진트리에서 preorder, inorder, postorder 는 순서대로
위 경우에 따라 나눠 볼 수 있고 코드로 보면 아래처럼 구현이 가능하다.
def findRoot(nodes):
return 루트 노드 인덱스 or 값 등 루트노드를 구분할 수 있는 값을 리턴
def divideNode(nodes, root):
nodes.sort(key=lambda x: x[0])
# nodes 의 root 를 기준으로 왼쪽과 오른쪽으로 나눔
for i, node in enumerate(nodes):
if node[1] == root:
leftNodes, rightNodes = nodes[:i], nodes[i+1:]
return leftNodes, rightNodes
def visitNode(nodes):
nonlocal preorder, postorder
if not nodes: return
root = findRoot(nodes)
leftNodes, rightNodes = divideNode(nodes, root)
preorder.append(root) # 전위순회
visitNode(leftNodes) # 왼쪽노드 탐색
inorder.append(root) # 중위순회
visitNode(rightNodes) # 오른쪽노드 탐색
postorder.append(root) # 후위순회
return
이진트리 문제의 기본적인 구조는 위와 같다.
문제의 제한사항은 "트리 깊이가 1,000 이하인 경우만 입력으로 주어진다." 라고 되어 있고, nodeinfo 길이도 10,000 이하이므로 시간복잡도는 충분할거라 생각했고 recursion dpeth 도 python 의 기본 recursion depth 의 경우 1,000 으로 문제의 트리 깊이 1,000 을 커버할 수 있는 문제라고 생각했다.
2개의 테스트 케이스가 런타임에러를 뱉었는데 recursion limit error 인 것 같은 느낌을 받았다 실제로 조금 늘려주니 해결되었다.
크게 늘릴 필요도 없이 1010 까지만 늘려줘도 통과가 됐는데, 이유를 찾아보니 python recursion limit 은 Python runtime 에서 자체적으로 재귀 호출을 스택에 담게되는데 스택의 기본 length 가 1,000으로 되어 있지만 해당 스택에 초기에 값이 들어있기에 문제가 발생하는 것으로 보입니다.
즉 초기 스택 프레임 사용은 정해져 있지 않으므로 널널히 늘리고 사용하는 것이 좋아보인다.
import sys
sys.setrecursionlimit(1010)
사실 문제가 너무 명확하고 간결해서 그래프를 그리는게 문제이며, 이진트리 문제를 잘 접하지 않는다면 어려울 수 있지만 풀어본 적이 있다면 크게 어려운 문제는 아니였던거 같다.
import sys
sys.setrecursionlimit(10**6)
def findRoot(nodes):
return sorted([[node[1], val] for node, val in nodes], key=lambda x : x[0])[-1][1]
def divideNode(nodes, root):
xNode = sorted(nodes, key=lambda x: x[0][0])
for i, node in enumerate(xNode):
if node[1] == root:
leftNodes, rightNodes = xNode[:i], xNode[i+1:]
return leftNodes, rightNodes
def solution(nodeinfo):
preorder, postorder = [], []
nodes = [[node, i+1] for i, node in enumerate(nodeinfo)]
def visitNode(nodes):
nonlocal preorder, postorder
if not nodes: return
root = findRoot(nodes)
leftNodes, rightNodes = divideNode(nodes, root)
preorder.append(root)
visitNode(leftNodes)
visitNode(rightNodes)
postorder.append(root)
return
visitNode(nodes)
answer = [preorder, postorder]
return answer
위 문제를 풀며 언급했던 내용처럼 Python 의 기본 recursionlimit 은 1,000 입니다. 하지만 실제로 재귀함수를 돌려보며 재귀탐색을 하다보면 1,000에 도달하기전에 recursionlimit 을 초과했다는 런타임에러가 발생하게됩니다.
왜 그런걸까요?
>>> import sys
>>> sys.getrecursionlimit()
1000
확인을 해보면 초기 recursionlimit 은 1,000으로 되어 있습니다.
n = 0
def test_recursion_limit():
def call():
global n
n += 1
call()
try:
call()
except RuntimeError:
print(n)
test_recursion_limit()
# 출력 : 998
998번에서 에러가 발생합니다. 즉 999개의 call() 함수가 호출되었고 1000번째 call() 함수가 호출되지 않았습니다.
이를 통해 depth 가 999 까지 가능하다는걸 알게되었습니다.
(base) ☁ ~ python
Python 3.11.9 (main, Apr 19 2024, 11:43:47) [Clang 14.0.6 ] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import inspect
>>> len(inspect.stack())
1
>>> print(inspect.stack())
[FrameInfo(frame=<frame at 0x101800bf0, file '<stdin>', line 1, code <module>>, filename='<stdin>', lineno=1, function='<module>', code_context=None, index=None, positions=Positions(lineno=1, end_lineno=1, col_offset=6, end_col_offset=21))]
이 가설이 맞았네요. inspect.stack() 을 통해 재귀함수 스택을 확인한 결과 FrameInfo 가 포함되어 있네요.
Python REPL이 단순히 입력된 코드를 실행하는 기본적인 인터프리터이기 때문에, 호출 스택에는 현재 실행 중인 코드 프레임만 포함되어 Length는 1입니다.
그렇다면 좀 더 다양한 기능(구문 강조, 자동 완성, 매직 명령어 등)을 제공해주는 ipython 에서는 어떨까요?
(base) ☁ ~ ipython
Python 3.12.4 | packaged by Anaconda, Inc. | (main, Jun 18 2024, 10:07:17) [Clang 14.0.6 ]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.25.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]: import inspect
In [2]: len(inspect.stack())
Out[2]: 13
여기서 호출 스택의 깊이는 13입니다. 이는 IPython REPL이 기본 Python 인터프리터 위에서 동작하며, 다양한 추가 기능을 제공하기 위해 여러 프레임을 추가로 사용하기 때문입니다. 이러한 추가 프레임들은 IPython의 다양한 기능을 지원하기 위해 필요합니다.
즉 사용하는 REPL에 의해 해당 call Stck code Frame 기본 구성이 달라집니다.
import sys
import inspect
sys.setrecursionlimit(1008)
print(len(inspect.stack()))
# 출력 : 7
문제가 깔끔하게 이진트리 자료 구조에 대해 물어보고 있어서 해당 자료구조를 알고 있는지 없는지에 따라 문제 풀이 가능 여부가 정해진거 같다. 알고리즘의 경우 해당 알고리즘을 모르더라도 다른 풀이 방법으로 풀었을 때 효율성에서 안좋을 뿐 풀 수 있는 방법은 존재하지만 자료 구조에 대해서 모르는 문제는 건들 수 조차없어서 아쉬움이 많이 남을 것 같다.
다른 자료 구조 문제들을 많이 풀어봐야겠다.
참고자료
출처 : https://velog.io/@yunu/프로그래머스-길-찾기-게임
출처 : https://stackoverflow.com/questions/40115683/why-is-the-maximum-recursion-depth-in-python-1000