개념

각 노드가 최대 2개의 자식을 가지는 트리 구조

계층적 데이터를 표현하는 비선형 자료구조


기본 용어

        1          ← root (루트)
       / \
      2   3        ← level 1
     / \   \
    4   5   6      ← level 2 (leaf nodes)
  • 루트(Root): 최상위 노드
  • 부모(Parent): 상위 노드
  • 자식(Child): 하위 노드
  • 리프(Leaf): 자식이 없는 노드
  • 레벨(Level): 루트로부터의 깊이
  • 높이(Height): 루트부터 가장 깊은 노드까지의 거리

트리의 종류

1. 정 이진 트리 (Full Binary Tree)

모든 노드가 0개 또는 2개의 자식을 가짐

      1
     / \
    2   3
   / \
  4   5

2. 완전 이진 트리 (Complete Binary Tree)

마지막 레벨을 제외하고 모든 레벨이 완전히 채워짐

      1
     / \
    2   3
   / \  /
  4  5 6

3. 포화 이진 트리 (Perfect Binary Tree)

모든 리프 노드가 같은 레벨에 있음

      1
     / \
    2   3
   / \ / \
  4  5 6  7

4. 편향 트리 (Skewed Tree)

한쪽으로만 치우친 트리

  1          1
 /            \
2              2
 \              \
  3              3

Python 구현

1. 노드 클래스

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

2. 이진 트리 생성

# 수동으로 트리 생성
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)
 
#       1
#      / \
#     2   3
#    / \
#   4   5

트리 순회 (Tree Traversal)

1. 전위 순회 (Preorder: Root → Left → Right)

def preorder(root):
    if not root:
        return []
 
    result = []
    result.append(root.val)
    result.extend(preorder(root.left))
    result.extend(preorder(root.right))
    return result
 
# 반복문 버전 (스택 사용)
def preorder_iterative(root):
    if not root:
        return []
 
    result = []
    stack = [root]
 
    while stack:
        node = stack.pop()
        result.append(node.val)
 
        # 오른쪽 먼저 push (나중에 처리)
        if node.right:
            stack.append(node.right)
        if node.left:
            stack.append(node.left)
 
    return result
 
# 테스트: [1, 2, 4, 5, 3]

활용: 트리 복사, 수식 트리의 전위 표기법


2. 중위 순회 (Inorder: Left → Root → Right)

def inorder(root):
    if not root:
        return []
 
    result = []
    result.extend(inorder(root.left))
    result.append(root.val)
    result.extend(inorder(root.right))
    return result
 
# 반복문 버전
def inorder_iterative(root):
    result = []
    stack = []
    current = root
 
    while current or stack:
        # 왼쪽 끝까지 이동
        while current:
            stack.append(current)
            current = current.left
 
        current = stack.pop()
        result.append(current.val)
        current = current.right
 
    return result
 
# 테스트: [4, 2, 5, 1, 3]

활용: BST를 오름차순으로 출력


3. 후위 순회 (Postorder: Left → Right → Root)

def postorder(root):
    if not root:
        return []
 
    result = []
    result.extend(postorder(root.left))
    result.extend(postorder(root.right))
    result.append(root.val)
    return result
 
# 반복문 버전
def postorder_iterative(root):
    if not root:
        return []
 
    result = []
    stack = [root]
 
    while stack:
        node = stack.pop()
        result.append(node.val)
 
        if node.left:
            stack.append(node.left)
        if node.right:
            stack.append(node.right)
 
    return result[::-1]  # 역순
 
# 테스트: [4, 5, 2, 3, 1]

활용: 트리 삭제, 수식 트리의 후위 표기법


4. 레벨 순회 (Level Order: BFS)

from collections import deque
 
def level_order(root):
    if not root:
        return []
 
    result = []
    queue = deque([root])
 
    while queue:
        level_size = len(queue)
        level = []
 
        for _ in range(level_size):
            node = queue.popleft()
            level.append(node.val)
 
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
 
        result.append(level)
 
    return result
 
# 테스트: [[1], [2, 3], [4, 5]]

활용: 레벨별 처리, 최단 경로


자주 나오는 문제 유형

1. 트리의 최대 깊이

문제: 트리의 최대 깊이 구하기

def max_depth(root):
    if not root:
        return 0
 
    left_depth = max_depth(root.left)
    right_depth = max_depth(root.right)
 
    return max(left_depth, right_depth) + 1
 
# 테스트
#       1
#      / \
#     2   3
#    / \
#   4   5
# 결과: 3
 
# 시간복잡도: O(n)
# 공간복잡도: O(h) - h: 높이

핵심: 재귀로 왼쪽/오른쪽 깊이 계산 후 최댓값 + 1


2. 대칭 트리 확인

문제: 트리가 좌우 대칭인지 확인

def is_symmetric(root):
    def is_mirror(left, right):
        if not left and not right:
            return True
        if not left or not right:
            return False
 
        return (left.val == right.val and
                is_mirror(left.left, right.right) and
                is_mirror(left.right, right.left))
 
    return is_mirror(root.left, root.right) if root else True
 
# 테스트
#       1
#      / \
#     2   2
#    / \ / \
#   3  4 4  3
# 결과: True
 
# 시간복잡도: O(n)

핵심: 왼쪽 서브트리와 오른쪽 서브트리가 거울상인지 확인


3. 경로의 합

문제: 루트부터 리프까지 경로의 합이 target인지 확인

def has_path_sum(root, target_sum):
    if not root:
        return False
 
    # 리프 노드 도달
    if not root.left and not root.right:
        return root.val == target_sum
 
    # 자식 노드로 재귀
    remaining = target_sum - root.val
    return (has_path_sum(root.left, remaining) or
            has_path_sum(root.right, remaining))
 
# 테스트
#       5
#      / \
#     4   8
#    /   / \
#   11  13  4
#  /  \      \
# 7    2      1
# has_path_sum(root, 22) → True (5→4→11→2)
 
# 시간복잡도: O(n)

핵심: 리프 노드에서 합 비교, 재귀로 남은 합 전달


4. 트리의 지름

문제: 트리에서 가장 긴 경로의 길이

def diameter_of_tree(root):
    diameter = 0
 
    def depth(node):
        nonlocal diameter
 
        if not node:
            return 0
 
        left = depth(node.left)
        right = depth(node.right)
 
        # 현재 노드를 거치는 경로 길이
        diameter = max(diameter, left + right)
 
        return max(left, right) + 1
 
    depth(root)
    return diameter
 
# 테스트
#       1
#      / \
#     2   3
#    / \
#   4   5
# 결과: 3 (4→2→1→3 또는 5→2→1→3)
 
# 시간복잡도: O(n)

핵심: 각 노드에서 좌우 깊이의 합이 지름 후보


5. 최소 공통 조상 (LCA)

문제: 두 노드의 최소 공통 조상 찾기

def lowest_common_ancestor(root, p, q):
    # Base Case
    if not root or root == p or root == q:
        return root
 
    # 왼쪽과 오른쪽 서브트리에서 탐색
    left = lowest_common_ancestor(root.left, p, q)
    right = lowest_common_ancestor(root.right, p, q)
 
    # 양쪽 모두에서 발견 → 현재 노드가 LCA
    if left and right:
        return root
 
    # 한쪽에서만 발견 → 그쪽 결과 반환
    return left if left else right
 
# 테스트
#       3
#      / \
#     5   1
#    / \ / \
#   6  2 0  8
#     / \
#    7   4
# LCA(5, 1) → 3
# LCA(5, 4) → 5
 
# 시간복잡도: O(n)

핵심: 양쪽 서브트리에서 모두 발견되는 첫 노드


6. 트리 평탄화 (Flatten to Linked List)

문제: 이진 트리를 연결 리스트로 평탄화

def flatten(root):
    if not root:
        return
 
    # 후위 순회로 오른쪽부터 처리
    flatten(root.right)
    flatten(root.left)
 
    # 왼쪽을 오른쪽으로 이동
    right_subtree = root.right
    root.right = root.left
    root.left = None
 
    # 원래 오른쪽을 맨 끝에 연결
    current = root
    while current.right:
        current = current.right
    current.right = right_subtree
 
# 테스트
#       1               1
#      / \               \
#     2   5      →        2
#    / \   \               \
#   3   4   6               3
#                            \
#                             4
#                              \
#                               5
#                                \
#                                 6
 
# 시간복잡도: O(n)

핵심: 왼쪽 서브트리를 오른쪽으로 옮기고 원래 오른쪽을 끝에 연결


추천 연습 문제

기초

중급

고급


핵심 요약

개념설명
전위 순회Root → Left → Right
중위 순회Left → Root → Right
후위 순회Left → Right → Root
레벨 순회BFS로 레벨별 탐색

트리 순회 선택 가이드

전위 순회 사용 시기

  • 트리 복사
  • 수식 트리의 전위 표기법
  • 트리 구조를 그대로 출력

중위 순회 사용 시기

  • BST를 정렬된 순서로 출력
  • 수식 트리의 중위 표기법

후위 순회 사용 시기

  • 트리 삭제 (자식부터 삭제)
  • 디렉토리 크기 계산
  • 수식 트리 계산

레벨 순회 사용 시기

  • 레벨별 처리
  • 최단 경로 문제
  • 트리를 레벨별로 출력

트리 문제 해결 패턴

1. 재귀 템플릿

def solve(root):
    # Base Case
    if not root:
        return base_value
 
    # 왼쪽, 오른쪽 서브트리 처리
    left_result = solve(root.left)
    right_result = solve(root.right)
 
    # 현재 노드 처리
    return combine(root.val, left_result, right_result)

2. 레벨별 처리 템플릿

def level_process(root):
    if not root:
        return
 
    queue = deque([root])
 
    while queue:
        level_size = len(queue)
 
        for _ in range(level_size):
            node = queue.popleft()
            # 노드 처리
 
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)

3. 두 트리 비교 템플릿

def compare(root1, root2):
    if not root1 and not root2:
        return True
    if not root1 or not root2:
        return False
 
    return (root1.val == root2.val and
            compare(root1.left, root2.left) and
            compare(root1.right, root2.right))

시간복잡도

연산시간복잡도
순회O(n)
탐색O(n) - 일반 트리, O(log n) - 균형 트리
삽입O(n) - 일반 트리, O(log n) - 균형 트리
삭제O(n) - 일반 트리, O(log n) - 균형 트리