💻 백준 1991번: 트리 순회
- 문제 링크: https://www.acmicpc.net/problem/1991
- 알고리즘 분류: 트리, 재귀
문제 소개 🧐
이진 트리를 입력받아 전위 순회(preorder traversal), 중위 순회(inorder traversal), 후위 순회(postorder traversal)한 결과를 출력하는 프로그램을 작성하는 문제였다.
입력 📝
첫째 줄에 이진 트리의 노드 개수 N(1 ≤ N ≤ 26)이 주어진다.
둘째 줄부터 N개의 줄에 걸쳐 각 노드와 그의 왼쪽 자식 노드, 오른쪽 자식 노드가 주어진다.
노드의 이름은 A부터 차례대로 알파벳 대문자로 매겨지며, 항상 A가 루트 노드가 된다. 자식 노드가 없는 경우에는 .으로 표현한다.
출력 📤
총 3줄에 걸쳐 전위 순회, 중위 순회, 후위 순회한 결과를 각각 출력한다. 각 줄에 N개의 알파벳을 공백 없이 출력하면 된다.
제한 ❌
- 시간: 2초
- 메모리: 128 MB
나의 풀이: 반복문과 큐를 이용한 시도 😥
Idea
처음엔 Node
클래스를 직접 정의하고, 각 노드를 리스트에 저장한 뒤 반복문과 큐(deque
)를 사용해 순회를 구현하려 했다. 전위 순회는 스택처럼, 나머지는 큐처럼 동작하게 하려다 보니 로직이 매우 복잡해졌다.
Code
import sys
from collections import deque
class Node:
val: int
left: "Node"
right: "Node"
parent: "Node"
visited: bool
def __init__(self, val: str):
self.val = val
self.left = None
self.right = None
self.parent = None
self.visited = False
def set_child(self, left, right):
self.left = left
self.right = right
def set_parent(self, parent):
self.parent = parent
def __str__(self):
return self.val
def get_right(self):
return self.right
def get_left(self):
return self.left
def get_parent(self):
return self.parent
n = int(sys.stdin.readline().rstrip())
node_list = list()
in_list = [list(map(lambda x: ord(x)-65, sys.stdin.readline().split())) for _ in range(n)]
for i in range(65, 91):
node = Node(chr(i))
node_list.append(node)
def reset_node_tree(in_list):
global node_list
for li in in_list: # '.' 은 46번
node_idx, l_idx, r_idx = li
node: Node = node_list[node_idx]
left = node_list[l_idx] if 0 <= l_idx else None
right = node_list[r_idx] if 0 <= r_idx else None
node.set_child(left, right)
left.set_parent(node) if left != None else None
right.set_parent(node) if right != None else None
for node in node_list:
node.visited = False
# 전위 순회 v l r
def prev_search():
global node_list
q = deque()
q.append(node_list[0])
answer = ""
while q:
node = q.pop()
answer += node.__str__()
if node.get_right() is not None:
q.append(node.get_right())
if node.left is not None:
q.append(node.get_left())
print(answer)
def mid_search(): # 중위 순회, l v r
global node_list
q = deque()
q.append(node_list[0])
answer = ""
while q:
node = q.popleft()
if node.left != None:
q.appendleft(node.get_left())
elif not node.visited:
answer += node.__str__()
parent = node.get_parent()
node.visited = True
if parent != None:
parent.set_child(None, None)
q.appendleft(parent)
if node.right != None:
q.append(node.get_right())
print(answer)
def end_search(): # 후위 순회, l r v
global node_list
q = deque()
q.append(node_list[0])
answer = ""
while q:
node = q.popleft()
# 자식 노드 담기
if node.left != None:
q.append(node.get_left())
if node.right != None:
q.append(node.get_right())
if node.left is None and node.right is None and not node.visited: # 말단 노드면
parent = node.get_parent()
if parent is None:
continue
if node == parent.left: # parent와의 연결을 끊음
parent.set_child(None, parent.right)
else:
parent.set_child(parent.left, None)
q.appendleft(parent)
node.visited = True
answer += node.__str__()
print(answer + 'A')
reset_node_tree(in_list)
prev_search()
mid_search()
reset_node_tree(in_list)
end_search()
Result
실패! 😱
중위, 후위 순회를 반복문으로 구현하는 로직이 너무 복잡했다. 특히 방문한 노드를 처리하고 부모 노드로 돌아가는 과정에서 예외 케이스가 많아 결국 해결하지 못했다.
두 번째 시도: 재귀를 활용한 풀이 🎉
Idea ✨
복잡한 반복문 대신 재귀(Recursion) 를 사용하면 훨씬 간결하게 트리 순회를 구현할 수 있다는 것을 깨달았다.
- 전위 순회:
(루트) -> (왼쪽) -> (오른쪽)
순서로 재귀 호출 - 중위 순회:
(왼쪽) -> (루트) -> (오른쪽)
순서로 재귀 호출 - 후위 순회:
(왼쪽) -> (오른쪽) -> (루트)
순서로 재귀 호출
또한, 노드를 리스트가 아닌 딕셔너리로 관리하면 {'A': Node('A'), ...}
와 같이 각 노드에 이름으로 바로 접근할 수 있어 편리하겠다고 생각했다.
Code
import sys
class Node:
def __init__(self, val: str):
self.val = val
self.left = None
self.right = None
def preorder(node):
if node is None:
return ""
return node.val + preorder(node.left) + preorder(node.right)
def inorder(node):
if node is None:
return ""
return inorder(node.left) + node.val + inorder(node.right)
def postorder(node):
if node is None:
return ""
return postorder(node.left) + postorder(node.right) + node.val
n = int(sys.stdin.readline().rstrip())
nodes = {chr(65 + i): Node(chr(65 + i)) for i in range(26)} # A~Z 미리 생성
for _ in range(n):
p, l, r = sys.stdin.readline().split()
if l != '.':
nodes[p].left = nodes[l]
if r != '.':
nodes[p].right = nodes[r]
root = nodes['A'] # 항상 A가 루트라고 가정
print(preorder(root))
print(inorder(root))
print(postorder(root))
Result
성공! ✅
재귀를 사용하니 코드가 놀랍도록 간결해지고, 각 순회의 논리가 명확하게 드러났다. 딕셔너리로 노드를 관리하니 특정 노드를 찾아 연결하는 과정도 훨씬 쉬워졌다.
배운 점 및 느낀 점 ✍️
- 재귀의 힘: 트리, 그래프와 같이 재귀적인 자료구조는 반복문보다 재귀 함수를 사용하는 것이 훨씬 직관적이고 효율적일 수 있다는 것을 깨달았다.
- 자료구조 선택의 중요성: 문제의 특성에 맞는 적절한 자료구조(이번엔 딕셔너리)를 선택하는 것이 코드의 간결성과 효율성을 크게 좌우한다는 것을 배웠다.
- 실패로부터 배우기: 첫 번째 시도의 실패 원인을 분석하고 다른 접근법을 고민하는 과정에서 더 나은 해결책을 찾을 수 있었다. 복잡한 코드는 버리고 더 간단한 해결책을 찾는 용기도 필요하다.
- 기본기: 트리 순회와 같은 기본적인 개념에 대한 확실한 이해가 뒷받침되어야 응용 문제를 해결할 수 있다.