본문 바로가기

트리

넓이 우선 탐색, 깊이 우선 탐색은 자료구조가 아니라 알고리즘 파트로 가야 하지만, 트리 자료구조를 탐색하는 대표적인 방법이기에 이 챕터에서 다룹니다.

1. 트리 (Tree)

트리는 노드들이 계층적으로 연결된 자료구조입니다. 하나의 루트 노드에서 시작하여 자식 노드들로 뻗어나가는 형태로, 사이클이 없는 연결 그래프입니다.

1.1 트리의 기본 용어

  • 노드(Node): 트리의 구성 요소
  • 루트(Root): 최상위 노드 (노드 1)
  • 부모(Parent): 특정 노드의 상위 노드 (2의 부모는 1)
  • 자식(Child): 특정 노드의 하위 노드 (1의 자식은 2, 3)
  • 형제(Sibling): 같은 부모를 가진 노드 (2와 3)
  • 리프(Leaf): 자식이 없는 노드 (4, 5, 6, 7)
  • 내부 노드(Internal Node): 자식이 있는 노드 (1, 2, 3)
  • 깊이(Depth): 루트에서 특정 노드까지의 경로 길이 (4의 깊이: 2)
  • 높이(Height): 트리의 최대 깊이 (이 트리의 높이: 2)
  • 레벨(Level): 루트를 0 또는 1로 시작하는 깊이

1.2 트리의 특징

  • 계층 구조: 부모-자식 관계로 연결
  • 사이클 없음: 순환 구조가 없음
  • 하나의 경로: 두 노드 사이에는 정확히 하나의 경로만 존재
  • N개 노드, N-1개 간선: N개의 노드는 N-1개의 간선으로 연결

2. 이진 트리 (Binary Tree)

이진 트리는 각 노드가 최대 2개의 자식을 가지는 트리입니다.

2.1 이진 트리의 종류

  • 포화 이진 트리 (Full Binary Tree): 모든 레벨이 노드로 꽉 차있음
  • 완전 이진 트리 (Complete Binary Tree): 마지막 레벨을 제외하고 모든 레벨이 꽉 차있고, 마지막 레벨은 왼쪽부터 채워짐
  • 편향 이진 트리 (Skewed Binary Tree): 한쪽으로만 자식이 있는 트리

2.2 이진 트리 구현

class TreeNode:
    def __init__(self, value):
        self.value = value
        self.left = None
        self.right = None

# 트리 생성
#     1
#    / \
#   2   3
#  / \
# 4   5

root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)

만약 위 문법이 어렵다면 다음과 같이 딕셔너리와 리스트로도 구현할 수 있습니다. 함께 공부하기 좋은 개념으로는 링크드 리스트가 있습니다. 여기서는 링크드 리스트를 다루지는 않습니다. 스택과 마찬가지로 '개념'이기 때문에 다양한 방식으로 구현할 수 있습니다.

  1. 딕셔너리 활용: 딕셔너리에서 'value'는 노드의 값, 'left'는 왼쪽 자식, 'right'는 오른쪽 자식을 의미합니다.
tree = {
    'value': 1,
    'left': {
        'value': 2,
        'left': {'value': 4, 'left': None, 'right': None},
        'right': {'value': 5, 'left': None, 'right': None}
    },
    'right': {
        'value': 3,
        'left': None,
        'right': None
    }
}
  1. 리스트 활용 (완전 이진 트리의 경우) : 리스트에서 0번째 값은 값이고, 1번째 값은 왼쪽 자식, 2번째 값은 오른쪽 자식을 의미합니다.
tree =  [1, 
            [2, 
                [4, None, None], 
                [5, None, None]
            ], 
            [3, None, None]
        ]

2.3 이진 트리 순회

트리 순회는 트리의 모든 노드를 체계적으로 방문하는 과정입니다. 재귀로 구현이 되어 있습니다. 가볍게만 훑어봐주세요. 이 부분은 깊이우선탐색, 넓이우선탐색을 제외하고 코딩테스트에서는 잘 출제되지 않습니다.

2.3.1 전위 순회 (Preorder)

루트 → 왼쪽 → 오른쪽 순서로 방문합니다.

def preorder(node):
    """전위 순회: Root → Left → Right"""
    if node is None:
        return

    print(node.value, end=' ')  # 루트 방문
    preorder(node.left)         # 왼쪽 서브트리
    preorder(node.right)        # 오른쪽 서브트리

# 결과: 1 2 4 5 3

2.3.2 중위 순회 (Inorder)

왼쪽 → 루트 → 오른쪽 순서로 방문합니다.

def inorder(node):
    """중위 순회: Left → Root → Right"""
    if node is None:
        return

    inorder(node.left)          # 왼쪽 서브트리
    print(node.value, end=' ')  # 루트 방문
    inorder(node.right)         # 오른쪽 서브트리

# 결과: 4 2 5 1 3

이진 탐색 트리(BST)와 중위 순회

이진 탐색 트리를 중위 순회하면 오름차순으로 정렬된 값을 얻을 수 있습니다. 이는 BST의 왼쪽 자식 < 부모 < 오른쪽 자식 속성 때문입니다.

2.3.3 후위 순회 (Postorder)

왼쪽 → 오른쪽 → 루트 순서로 방문합니다.

def postorder(node):
    """후위 순회: Left → Right → Root"""
    if node is None:
        return

    postorder(node.left)        # 왼쪽 서브트리
    postorder(node.right)       # 오른쪽 서브트리
    print(node.value, end=' ')  # 루트 방문

# 결과: 4 5 2 3 1

2.3.4 순회 방식 비교

2.4 깊이 우선 탐색 (DFS)과 넓이 우선 탐색 (BFS)

트리 순회는 깊이 우선 탐색(DFS)과 넓이 우선 탐색(BFS)으로 나눌 수 있습니다. DFS는 스택을 사용하고, BFS는 큐를 사용합니다. 예제를 간소화 하기 위해서 트리는 클래스로 구현하지 않고, 딕셔너리로 표현하였습니다.

2.4.1 깊이 우선 탐색의 개념

깊이 우선 탐색(DFS, Depth-First Search)은 한 방향으로 끝까지 탐색한 후 다시 돌아와서 다른 방향을 탐색하는 방법입니다. 스택(Stack) 자료구조를 사용하거나 재귀 함수로 구현합니다.

DFS 탐색 순서: A → B → D → E → C → F (깊이 우선으로 탐색)

2.4.2 단계별 DFS 동작 과정

초기 상태: A를 스택에 넣은 상태로 시작합니다. 값은 뒤에서 추가하고, 뒤에서 꺼내는 구조입니다.

Step 1: A를 스택에서 꺼내고 방문

Step 2: B를 스택에서 꺼내고 방문

Step 3: D를 스택에서 꺼내고 방문

Step 4: E를 스택에서 꺼내고 방문

Step 5: C를 스택에서 꺼내고 방문

Step 6: F를 스택에서 꺼내고 방문 (완료)

2.4.3 깊이 우선 탐색 구현

def dfs(graph, start):
    """딕셔너리 그래프에서 DFS 수행"""
    visited = []  # 방문한 노드 기록
    stack = [start]  # 스택 초기화

    while stack:
        # 스택에서 노드 꺼내기
        current = stack.pop()

        # 아직 방문하지 않았다면
        if current not in visited:
            visited.append(current)
            print(f"방문: {current}")

            # 자식 노드들을 스택에 추가 (역순으로)
            if current in graph:
                # 역순으로 추가해야 올바른 순서로 방문
                for neighbor in reversed(graph[current]):
                    if neighbor not in visited:
                        stack.append(neighbor)

    return visited

# 그래프 정의 (딕셔너리)
graph = {
    'A': ['B', 'C'],
    'B': ['D', 'E'],
    'C': ['F'],
    'D': [],
    'E': [],
    'F': []
}

# DFS 실행
result = dfs(graph, 'A')
print(f"DFS 탐색 순서: {' → '.join(result)}")
# 출력: DFS 탐색 순서: A → B → D → E → C → F

2.4.4 넓이 우선 탐색의 개념

넓이 우선 탐색(BFS, Breadth-First Search)은 같은 레벨의 노드들을 먼저 모두 탐색한 후 다음 레벨로 이동하는 방법입니다. 큐(Queue) 자료구조를 사용합니다.

BFS 탐색 순서: A → B → C → D → E → F (레벨별로 탐색)

2.4.6 단계별 BFS 동작 과정

초기 상태: A를 큐에 넣은 상태로 시작합니다. 값은 뒤에서 추가하고, 앞에서 꺼내는 구조입니다.

Step 1: A를 큐에서 꺼내고 방문

Step 2: B를 큐에서 꺼내고 방문

Step 3: C를 큐에서 꺼내고 방문

Step 4: D를 큐에서 꺼내고 방문

Step 5: E를 큐에서 꺼내고 방문

Step 6: F를 큐에서 꺼내고 방문 (완료)

2.4.6 넓이 우선 탐색 구현

from collections import deque

def bfs(graph, start):
    """딕셔너리 그래프에서 BFS 수행"""
    visited = []  # 방문한 노드 기록
    queue = deque([start])  # 큐 초기화

    while queue:
        # 큐에서 노드 꺼내기 (앞에서 꺼냄)
        current = queue.popleft()

        # 아직 방문하지 않았다면
        if current not in visited:
            visited.append(current)
            print(f"방문: {current}")

            # 자식 노드들을 큐에 추가 (순서대로)
            if current in graph:
                for neighbor in graph[current]:
                    if neighbor not in visited and neighbor not in queue:
                        queue.append(neighbor)

    return visited

# 그래프 정의 (딕셔너리)
graph = {
    'A': ['B', 'C'],
    'B': ['D', 'E'],
    'C': ['F'],
    'D': [],
    'E': [],
    'F': []
}

# BFS 실행
result = bfs(graph, 'A')
print(f"BFS 탐색 순서: {' → '.join(result)}")
# 출력: BFS 탐색 순서: A → B → C → D → E → F

2.4.7 DFS vs BFS 비교

특성DFS (깊이 우선)BFS (넓이 우선)
자료구조스택 (Stack)큐 (Queue)
탐색 방식한 방향 끝까지레벨별로
메모리적게 사용많이 사용
최단 경로보장 안 됨보장됨
구현재귀/반복문반복문 주로 사용
적용 사례미로 탐색, 백트래킹최단 경로, 레벨 순회

5. 연습문제

# 딕셔너리로 표현한 트리 (인접 리스트 방식)
tree = {
    'A': ['B', 'C'],
    'B': ['D', 'E'],
    'C': ['F', 'G'],
    'D': [],
    'E': [],
    'F': [],
    'G': []
}
#         A
#        / \
#       B   C
#      / \ / \
#     D  E F  G
트리 - 코딩 테스트 에센셜 with 파이썬 | 위니버시티