이진 트리 (Binary Tree)

자식이 최대 2개인 이진 트리의 완전·포화·완벽·편향 유형, 배열 표현, 그리고 이진 트리의 주요 성질과 구현을 알아봅니다.

· 4 min read · PALDYN Team

지난 글에서 트리의 기본 용어와 성질을 살펴봤습니다. 이번 글은 트리 계열 자료구조의 핵심인 **이진 트리(Binary Tree)**를 깊이 다룹니다. 이진 트리는 힙, BST, AVL, Red-Black Tree, 세그먼트 트리 등 대부분의 고급 트리 자료구조의 기반입니다.

이진 트리란

각 노드가 최대 2개의 자식을 갖는 트리입니다. 자식을 각각 **왼쪽 자식(left child)**과 **오른쪽 자식(right child)**으로 구분합니다. 이 구분이 단순 트리와 가장 큰 차이입니다. left(A) = B이고 left(A) = C인 트리는 B ≠ C여도 서로 다른 이진 트리로 취급합니다.

이진 트리 종류

이진 트리 종류

완전 이진 트리(Complete): 마지막 레벨을 제외한 모든 레벨이 꽉 차 있고, 마지막 레벨의 노드는 왼쪽부터 채워져 있습니다. 힙(Heap)이 이 구조를 사용합니다.

포화 이진 트리(Full): 모든 내부 노드가 정확히 2개의 자식을 갖습니다. 리프 노드는 0개, 내부 노드는 2개의 자식입니다. Huffman 코딩 트리가 이 구조입니다.

완벽 이진 트리(Perfect): 완전이면서 모든 리프가 같은 깊이에 있습니다. n = 2^(h+1) - 1개의 노드를 가집니다.

편향 이진 트리(Skewed): 모든 노드가 왼쪽 또는 오른쪽 자식만 갖습니다. 연결 리스트와 동일하며 높이 O(n), 검색 O(n)의 최악 케이스입니다.

Python 구현

from __future__ import annotations
from dataclasses import dataclass
from typing import Optional

@dataclass
class BinaryTree:
    val: int
    left: Optional['BinaryTree'] = None
    right: Optional['BinaryTree'] = None

def height(root: Optional[BinaryTree]) -> int:
    if root is None:
        return -1   # 빈 트리의 높이 = -1 (또는 0 정의에 따라 다름)
    return 1 + max(height(root.left), height(root.right))

def count_nodes(root: Optional[BinaryTree]) -> int:
    if root is None:
        return 0
    return 1 + count_nodes(root.left) + count_nodes(root.right)

def is_complete(root: Optional[BinaryTree]) -> bool:
    if root is None:
        return True
    from collections import deque
    q = deque([root])
    found_null = False
    while q:
        node = q.popleft()
        if node is None:
            found_null = True
        else:
            if found_null:
                return False   # null 이후에 노드가 있으면 불완전
            q.append(node.left)
            q.append(node.right)
    return True

배열로 표현하기

이진 트리 배열 표현

완전 이진 트리는 배열로 효율적으로 표현할 수 있습니다. 인덱스 i의 노드에서:

  • 왼쪽 자식: 2i + 1
  • 오른쪽 자식: 2i + 2
  • 부모: (i - 1) // 2
class ArrayBinaryTree:
    def __init__(self):
        self.data = []

    def left(self, i):
        return 2 * i + 1

    def right(self, i):
        return 2 * i + 2

    def parent(self, i):
        return (i - 1) // 2

    def insert(self, val):
        self.data.append(val)

    def __len__(self):
        return len(self.data)

포인터 없이 배열 인덱스로만 트리를 탐색할 수 있어 캐시 효율이 뛰어납니다. 힙(Heap)과 세그먼트 트리가 이 방식을 사용합니다.

이진 트리의 주요 성질

높이 h인 이진 트리에서:

  • 최대 노드 수: 2^(h+1) - 1 (완벽 이진 트리)
  • 최소 노드 수: h + 1 (편향 트리)
  • 리프 수 ≤ 2^h

n개 노드의 이진 트리 높이:

  • 최소: ⌊log₂ n⌋ (완전 이진 트리에 가까울수록)
  • 최대: n - 1 (편향 트리)

이 높이 차이가 BST, AVL, Red-Black Tree 등 균형 트리가 중요한 이유입니다. 검색은 높이에 비례하므로 O(log n)을 보장하려면 높이를 O(log n)으로 유지해야 합니다.


지난 글: 트리 기초 (Tree Basics)

다음 글: 트리 순회 (Tree Traversal)


읽어주셔서 감사합니다. 😊