포화이진트리의 구조와 용량 계산, 활용 패턴

포화이진트리의 정의와 노드 수 공식, 정·완전 이진 트리와의 차이, 세그먼트 트리와 버디 할당자 활용 방식을 정리한다.

2026-08-14 · 최초 발행 2024-04-29

리프 높이가 맞춰진 이진 트리

포화이진트리(Perfect Binary Tree)는 모든 내부 노드가 정확히 두 자식을 가지며, 모든 리프가 같은 레벨에 있는 이진 트리다. 자식의 완결성과 리프 레벨의 균일성을 함께 만족하므로, 이진 트리의 구조적 밀도와 균형을 분석할 때 기준점이 된다.

정 이진 트리(Full/Proper Binary Tree)는 각 노드의 자식 수가 0 또는 2인 트리다. 리프가 모두 같은 레벨에 있을 필요는 없다. 완전 이진 트리(Complete Binary Tree)는 레벨 순서로 왼쪽부터 빈틈없이 채우되, 마지막 레벨만 부분적으로 비어 있을 수 있다.

Full Binary Tree는 문헌에 따라 정 이진 트리 또는 포화이진트리를 뜻하기도 한다. 한국어 용어에서는 포화를 Perfect, 정을 Full/Proper로 나누어 쓰는 편이 혼동을 줄인다.

레벨과 높이로 계산하는 노드 수

루트의 레벨을 L=1로 두면 포화이진트리의 총 노드 수 N2^L − 1이다. 리프 수는 2^(L−1), 내부 노드 수는 2^(L−1) − 1이 된다.

높이를 간선 수 h로 정의하면 h=L−1이고, 총 노드 수는 2^(h+1) − 1, 리프 수는 2^h로 표현된다.

정 이진 트리에서는 내부 노드 수를 I, 리프 수를 F라고 할 때 F = I + 1, 총 노드 수는 N = 2I + 1이라는 관계가 성립한다.

모든 노드의 수가 2^(n−1)이라는 표현은 일반적으로 맞지 않는다. 포화이진트리의 정확한 노드 수 공식은 N = 2^L − 1, 또는 N = 2^(h+1) − 1이다.

배열로 다루기 쉬운 균형 구조

리프가 같은 레벨에 배치되므로 포화이진트리는 균형을 보장한다. 높이는 h = O(log N)이며, 탐색과 갱신의 상한을 분석하기 좋다.

노드를 1-기반 배열에 넣으면 부모 인덱스 i의 자식은 2i, 2i+1이고, 부모는 ⌊i/2⌋로 찾을 수 있다. 포인터 오버헤드를 줄이고 캐시 지역성을 높이는 배치가 가능하다.

서브트리 역시 같은 포화 또는 정 구조를 재귀적으로 유지한다. 이 성질은 균등한 과업 분배와 병렬 분할에 맞는다. 목표 높이 또는 리프 수가 정해지면 총 노드 수와 메모리 요구량을 바로 산출할 수 있으며, 전처리에서 다음 2의 거듭제곱까지 패딩해 구조를 정형화할 수도 있다.

루트L2-좌L2-우L3-좌L3-우L3-좌L3-우

구간 연산과 메모리 분할에서의 적용

세그먼트 트리와 레인지 쿼리 구조에서는 길이 n인 배열을 다음 2^k ≥ n 크기까지 패딩한 뒤 리프에 배치한다. 내부 노드는 합이나 최솟값 같은 결합 연산을 상향식으로 구성한다. 이 방식은 구간 쿼리를 O(log N)에 처리하고, 포인트 또는 구간 갱신도 O(log N)에 수행한다. 균형 깊이 덕분에 최악과 평균의 차이가 줄고 상수 인자를 예측하기 쉽다.

버디 메모리 할당자(Buddy Allocator)는 2^k 크기의 메모리를 요청 크기에 맞을 때까지 1/2씩 나눈다. 메모리를 반납하면 버디 블록을 병합한다. 완전한 2진 분할 트리 형태를 사용하므로 병합 여부를 빠르게 판별할 수 있고 메타데이터도 단순해진다.

게임 트리나 결정 트리에서 최대 깊이 d와 균등한 분기 규칙을 적용할 때도 포화형 구조가 기준이 된다. 레벨별로 확장하고 탐색·평가를 진행하면 2^d − 1을 기준으로 노드 수를 예측할 수 있어 시간과 메모리 상한을 세우고 병렬 배치를 구성하기 좋다.

FFT와 합병 정렬처럼 분할 정복을 병렬화할 때는 포화형 태스크 트리로 워크 스틸링 균형화를 설계할 수 있다. 스레드 풀만으로 작업을 구성하는 경우보다 작업 그래프의 깊이와 폭을 추정하기 쉽다.

깊이와 용량을 미리 산정하는 방법

탐색과 갱신은 O(log N) 깊이에서 이뤄지며, 최악과 평균의 편차를 작게 유지할 수 있다. 배열로 표현하면 포인터를 제거해 구조체 오버헤드를 줄일 수 있고, 노드당 수바이트를 절감한다. 순차 접근 패턴은 캐시 미스율 감소에도 유리하다.

루트 레벨을 1로 두고 L=20일 때 총 노드 수는 N=2^20−1=1,048,575, 리프 수는 2^19=524,288이다. 이처럼 용량을 사전에 계산하면 메모리 블록 예약과 GC·파편화 리스크 완화에 활용할 수 있다.

구분 성능(깊이) 확장성(증설/패딩) 일관성(형태) 안정성(분석 용이) 운영 편의(배열 매핑)
포화(Perfect) O(log N), 최악=평균 2의 거듭제곱 단위 증설 권장 리프 레벨 동일 수식/상한 명확 매우 용이
정(Full/Proper) O(height), 균형 미보장 구조 유연, 불균형 가능 자식 0/2만 허용 일부 성질만 보장 용이
완전(Complete) O(log N), 힙 최적 n±1 노드 증감 용이 마지막 레벨만 부분 비움 실용적 상한 확보 매우 용이

구조 조건을 확인하는 구현

포화 여부를 판별할 때는 루트 노드에서 왼쪽 가장자리를 따라 높이 h를 계산하고, 전체 노드 수를 센다. 이어서 cnt == 2^(h+1) − 1인지 확인하면서 모든 내부 노드가 두 자식을 가지는지도 검증한다.

빈 트리(None)는 포화로 보거나 정책에 따라 False로 처리할 수 있다. 한쪽 자식만 있는 노드를 만나면 즉시 False다. 큰 트리에서는 overflow를 고려해 64비트 정수를 사용한다.

from collections import deque
from typing import Optional

class Node:
    __slots__ = ("val", "left", "right")
    def __init__(self, val, left: Optional["Node"]=None, right: Optional["Node"]=None):
        self.val, self.left, self.right = val, left, right

def height_left_spine(root: Optional[Node]) -> int:
    # 간선 기준 높이 h 반환
    h, cur = -1, root
    while cur:
        h += 1
        cur = cur.left
    return h

def is_perfect(root: Optional[Node]) -> bool:
    if root is None:
        return True
    # 높이 h와 기대 노드수
    h = height_left_spine(root)
    expected = (1 << (h + 1)) - 1
    # BFS로 노드 수와 구조 확인
    q = deque([root])
    count = 0
    while q:
        node = q.popleft()
        count += 1
        l, r = node.left, node.right
        if (l is None) ^ (r is None):  # 한쪽만 있으면 실패
            return False
        if l: q.append(l)
        if r: q.append(r)
    return count == expected
포화이진트리이진트리자료구조세그먼트트리분할정복버디할당자