전위 순회(Preorder Traversal)#
전위 순회(Preorder Traversal)는 트리 자료구조를 탐색하는 가장 기본적인 방법 중 하나이다.
전위 순회는 트리를 탐색하는 깊이 우선 탐색(Depth-First Search, DFS)의 한 형태이다.
이 방법에서는 다음과 같은 순서로 노드를 방문한다:
- 현재 노드(루트)를 방문합니다.
- 왼쪽 서브트리를 전위 순회한다.
- 오른쪽 서브트리를 전위 순회한다.
이 과정은 재귀적으로 수행되며, 루트 노드부터 시작하여 왼쪽 가지를 따라 깊이 내려간 후 오른쪽 가지로 이동한다. 전위 순회의 이름에서 “전위(Pre)“는 부모 노드를 자식 노드보다 먼저(before) 방문한다는 의미를 담고 있다.
전위 순회는 트리를 탐색하는 기본적이면서도 강력한 방법이다.
루트를 먼저 방문한 후 왼쪽과 오른쪽 서브트리를 순서대로 방문하는 이 방법은 트리 복제, 표현식 트리 처리, 디렉토리 구조 탐색 등 다양한 문제를 해결하는 데 활용된다.
재귀적 구현이 간결하고 직관적이지만, 스택 오버플로우 위험이 있는 깊은 트리에서는 반복적 방법이나 모리스 순회와 같은 최적화된 알고리즘을 고려해야 할 수 있다.
전위 순회의 알고리즘#
아래의 이진 트리를 예로 들어 전위 순회 과정을 살펴보면:
1
2
3
4
5
| 1
/ \
2 3
/ \ / \
4 5 6 7
|
전위 순회 순서: 1 → 2 → 4 → 5 → 3 → 6 → 7
이 순서를 단계별로 설명하면:
- 루트 노드 1을 방문한다.
- 왼쪽 서브트리로 이동하여 노드 2를 방문한다.
- 노드 2의 왼쪽 서브트리로 이동하여 노드 4를 방문한다.
- 노드 4는 자식이 없으므로, 노드 2의 오른쪽 서브트리로 이동하여 노드 5를 방문한다.
- 노드 5도 자식이 없으므로, 루트 노드 1의 오른쪽 서브트리로 이동하여 노드 3을 방문한다.
- 노드 3의 왼쪽 서브트리로 이동하여 노드 6을 방문한다.
- 노드 6은 자식이 없으므로, 노드 3의 오른쪽 서브트리로 이동하여 노드 7을 방문한다.
1
2
3
4
5
| A
/ \
B C
/ \ \
D E F
|
이진 트리의 전위 순회 순서를 구하는 과정은 다음과 같다.
- A 방문 → 출력:
A
- B 방문 (A의 왼쪽 자식) → 출력:
A B
- D 방문 (B의 왼쪽 자식) → 출력:
A B D
- E 방문 (B의 오른쪽 자식) → 출력:
A B D E
- C 방문 (A의 오른쪽 자식) → 출력:
A B D E C
- F 방문 (C의 오른쪽 자식) → 출력:
A B D E C F
재귀적 구현#
전위 순회는 재귀를 사용하여 간결하게 구현할 수 있다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| class Node:
def __init__(self, value):
self.value = value
self.left = None
self.right = None
def preorder_traversal(node):
if node is None:
return
# 현재 노드 출력
print(node.value, end=" ")
# 왼쪽 서브트리 탐색
preorder_traversal(node.left)
# 오른쪽 서브트리 탐색
preorder_traversal(node.right)
# 트리 생성
root = Node('A')
root.left = Node('B')
root.right = Node('C')
root.left.left = Node('D')
root.left.right = Node('E')
root.right.right = Node('F')
# 전위 순회 실행
preorder_traversal(root)
|
실행 결과:
반복적(비재귀적) 구현#
스택을 사용한 반복적 구현 방법도 가능합니다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| def preorder_traversal_iterative(root):
if root is None:
return
stack = [root] # 스택에 루트 노드 삽입
while stack:
node = stack.pop()
print(node.value, end=" ") # 현재 노드 방문
# 스택의 특성상 오른쪽 자식을 먼저 삽입해야 왼쪽 자식이 먼저 처리됨
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
# 실행
preorder_traversal_iterative(root)
|
실행 결과:
다양한 트리 구조에서의 전위 순회#
완전 이진 트리(Complete Binary Tree)
완전 이진 트리는 마지막 레벨을 제외한 모든 레벨이 완전히 채워져 있고, 마지막 레벨의 노드들은 가능한 한 왼쪽부터 채워진 트리이다.
1
2
3
4
5
| 1
/ \
2 3
/ \ /
4 5 6
|
전위 순회 순서: 1 → 2 → 4 → 5 → 3 → 6
편향 트리(Skewed Tree)
한쪽으로만 자식 노드가 있는 트리이다.
왼쪽 편향 트리:
전위 순회 순서: 1 → 2 → 3 → 4
오른쪽 편향 트리:
전위 순회 순서: 1 → 2 → 3 → 4
일반 트리(N-ary Tree)
각 노드가 0개 이상의 자식 노드를 가질 수 있는 트리이다.
1
2
3
4
5
| 1
/ | \
2 3 4
/\ | /\
5 6 7 8 9
|
일반 트리에서의 전위 순회:
- 현재 노드를 방문한다.
- 각 자식 노드를 왼쪽에서 오른쪽 순서로 전위 순회한다.
전위 순회 순서: 1 → 2 → 5 → 6 → 3 → 7 → 4 → 8 → 9
전위 순회의 활용 사례#
트리 복제(Tree Cloning)
전위 순회는 트리의 복제본을 만드는 데 유용하다.
각 노드를 방문하면서 새 노드를 생성하고 연결하는 방식으로 트리를 복제할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| def clone_tree(node):
if node is None:
return None
# 현재 노드 복제
new_node = TreeNode(node.value)
# 왼쪽 서브트리 복제
new_node.left = clone_tree(node.left)
# 오른쪽 서브트리 복제
new_node.right = clone_tree(node.right)
return new_node
|
디렉토리 구조 출력
파일 시스템의 디렉토리 구조를 출력할 때 전위 순회가 자연스러운 방법이다.
루트 디렉토리부터 시작하여 각 하위 디렉토리와 파일을 순차적으로 탐색한다.
1
2
3
4
5
6
7
8
9
| def print_directory_structure(path, indent=""):
# 현재 디렉토리 출력
print(indent + os.path.basename(path))
# 하위 항목이 디렉토리인 경우 재귀적으로 탐색
if os.path.isdir(path):
for item in os.listdir(path):
item_path = os.path.join(path, item)
print_directory_structure(item_path, indent + " ")
|
표현식 트리의 전위 표기법(Prefix Notation) 변환
표현식 트리에서 연산자는 내부 노드에, 피연산자는 리프 노드에 저장된다.
전위 순회를 사용하면 중위 표기법(infix notation)으로 작성된 수식을 전위 표기법(prefix notation 또는 Polish notation)으로 변환할 수 있다.
예를 들어, 중위 표기법 a + b * c
를 표현하는 트리는 다음과 같다:
1
2
3
4
5
| +
/ \
a *
/ \
b c
|
전위 순회를 수행하면 + a * b c
라는 전위 표기법을 얻을 수 있다.
XML/HTML 문서 파싱
XML이나 HTML과 같은 구조화된 문서는 트리 형태로 표현될 수 있으며, 문서를 파싱할 때 전위 순회가 사용된다.
먼저 부모 태그를 처리한 후 자식 요소들을 처리한다.
1
2
3
4
5
6
7
8
9
10
11
| def parse_xml_element(element):
# 현재 요소(태그) 처리
print(f"처리 중인 태그: {element.tag}")
# 요소의 속성 처리
for attr_name, attr_value in element.attrib.items():
print(f" 속성: {attr_name} = {attr_value}")
# 자식 요소 처리
for child in element:
parse_xml_element(child)
|
트리의 직렬화(Serialization)
트리를 저장하거나 전송하기 위해 선형 구조로 변환하는 과정에서 전위 순회가 사용된다.
전위 순회의 특성상 부모 노드가 자식 노드보다 먼저 저장되므로, 역직렬화 과정에서 트리를 쉽게 재구성할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| def serialize_tree(root):
if root is None:
return "null,"
# 현재 노드의 값을 문자열에 추가
serialized = str(root.value) + ","
# 왼쪽 서브트리 직렬화
serialized += serialize_tree(root.left)
# 오른쪽 서브트리 직렬화
serialized += serialize_tree(root.right)
return serialized
|
전위 순회의 시간 및 공간 복잡도#
시간 복잡도
전위 순회는 트리의 모든 노드를 한 번씩 방문하므로 시간 복잡도는 O(n). 여기서 n은 트리의 노드 수이다.
공간 복잡도
- 재귀적 구현: 재귀 호출 스택의 크기는 트리의 높이 h에 비례하므로 공간 복잡도는 O(h)이다. 최악의 경우(편향 트리)에는 O(n)이 될 수 있다.
- 반복적 구현: 명시적 스택을 사용하는 경우에도 공간 복잡도는 O(h)이다. 마찬가지로 최악의 경우에는 O(n)이 될 수 있다.
전위 순회와 다른 순회 방법 비교#
전위 순회 vs 중위 순회(Inorder Traversal)
- 전위 순회: 루트 → 왼쪽 → 오른쪽
- 중위 순회: 왼쪽 → 루트 → 오른쪽
중위 순회는 이진 검색 트리(BST)에서 노드를 오름차순으로 방문할 때 유용하다.
중위 표기법의 수식을 생성할 때도 사용된다.
전위 순회 vs 후위 순회(Postorder Traversal)
- 전위 순회: 루트 → 왼쪽 → 오른쪽
- 후위 순회: 왼쪽 → 오른쪽 → 루트
후위 순회는 트리를 삭제하거나 후위 표기법(postfix notation)의 수식을 생성할 때 유용하다.
자식 노드를 먼저 처리한 후 부모 노드를 처리하는 상향식 접근 방식이다.
전위 순회 vs 레벨 순회(Level Order Traversal)
- 전위 순회: 깊이 우선(DFS) 방식으로 루트부터 시작하여 왼쪽 가지를 따라 내려간다.
- 레벨 순회: 너비 우선(BFS) 방식으로 각 레벨의 노드를 왼쪽에서 오른쪽으로 방문한다.
레벨 순회는 최단 경로 문제나 트리의 너비 계산에 유용하다.
실제 코드 예제: 이진 트리의 전위 순회 구현#
아래는 Python으로 이진 트리 노드 클래스와 전위 순회 메서드를 구현한 예제:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
| class TreeNode:
def __init__(self, value=0, left=None, right=None):
self.value = value
self.left = left
self.right = right
def preorder_traversal_recursive(root):
result = []
def dfs(node):
if not node:
return
# 현재 노드 방문
result.append(node.value)
# 왼쪽 서브트리 순회
dfs(node.left)
# 오른쪽 서브트리 순회
dfs(node.right)
dfs(root)
return result
def preorder_traversal_iterative(root):
if not root:
return []
result = []
stack = [root]
while stack:
node = stack.pop()
result.append(node.value)
# 스택은 LIFO이므로 오른쪽 자식을 먼저 푸시
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
return result
# 다음과 같은 이진 트리 생성:
# 1
# / \
# 2 3
# / \ / \
# 4 5 6 7
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)
root.right.left = TreeNode(6)
root.right.right = TreeNode(7)
# 재귀적 방법으로 전위 순회
print("재귀적 전위 순회:", preorder_traversal_recursive(root)) # [1, 2, 4, 5, 3, 6, 7]
# 반복적 방법으로 전위 순회
print("반복적 전위 순회:", preorder_traversal_iterative(root)) # [1, 2, 4, 5, 3, 6, 7]
|
전위 순회의 변형#
모리스 전위 순회(Morris Preorder Traversal)
모리스 순회 알고리즘은 추가 공간을 사용하지 않고(O(1) 공간 복잡도) 트리를 순회하는 방법이다.
이 방법은 트리의 구조를 일시적으로 수정하여 포인터를 사용해 이전 노드로 돌아갈 수 있게 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| def morris_preorder_traversal(root):
result = []
current = root
while current:
# 왼쪽 자식이 없으면 현재 노드를 방문하고 오른쪽으로 이동
if not current.left:
result.append(current.value)
current = current.right
else:
# 왼쪽 서브트리에서 가장 오른쪽 노드(중위 순회에서 현재 노드의 선행자)를 찾음
predecessor = current.left
while predecessor.right and predecessor.right != current:
predecessor = predecessor.right
# 선행자의 오른쪽이 None이면, 현재 노드로 연결하고 현재 노드를 방문한 후 왼쪽으로 이동
if not predecessor.right:
result.append(current.value)
predecessor.right = current
current = current.left
# 선행자의 오른쪽이 현재 노드를 가리키면, 그 연결을 끊고 오른쪽으로 이동
else:
predecessor.right = None
current = current.right
return result
|
N진 트리의 전위 순회
N진 트리(각 노드가 N개 이하의 자식을 가질 수 있는 트리)에서도 전위 순회를 적용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| class NaryTreeNode:
def __init__(self, value=0):
self.value = value
self.children = []
def preorder_nary_tree(root):
if not root:
return []
result = [root.value]
for child in root.children:
result.extend(preorder_nary_tree(child))
return result
|
실전 문제: 전위 순회 응용#
이진 트리의 경로 합 확인#
주어진 이진 트리와 목표 합이 있을 때, 루트에서 리프까지의 경로 중 합이 목표 값과 일치하는 경로가 있는지 확인하는 문제.
1
2
3
4
5
6
7
8
9
10
11
| def has_path_sum(root, target_sum):
if not root:
return False
# 리프 노드에 도달했을 때 합이 목표 값과 일치하는지 확인
if not root.left and not root.right:
return root.value == target_sum
# 왼쪽 또는 오른쪽 서브트리에서 경로를 찾음
return (has_path_sum(root.left, target_sum - root.value) or
has_path_sum(root.right, target_sum - root.value))
|
이진 트리의 전위 순회 결과로부터 트리 재구성#
전위 순회의 결과와 중위 순회의 결과가 주어졌을 때, 원래 이진 트리를 재구성하는 문제.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| def build_tree(preorder, inorder):
if not preorder or not inorder:
return None
# 전위 순회의 첫 번째 요소는 루트
root_value = preorder[0]
root = TreeNode(root_value)
# 중위 순회에서 루트의 위치 찾기
root_index = inorder.index(root_value)
# 왼쪽 서브트리와 오른쪽 서브트리로 분할
root.left = build_tree(preorder[1:1+root_index], inorder[:root_index])
root.right = build_tree(preorder[1+root_index:], inorder[root_index+1:])
return root
|
참고 및 출처#