균형 이진 탐색 트리: AVL과 레드-블랙 트리의 회전·갱신 전략
AVL 트리와 레드-블랙 트리의 균형 조건, 회전, 삽입·삭제 보정 방식과 인메모리 자료구조 선택 기준을 정리한다.
2026-08-14 · 최초 발행 2024-04-29
삽입과 삭제가 누적된 이진 탐색 트리는 한쪽으로 기울 수 있다. 균형 이진 탐색 트리는 이 문제를 회전과 불변식으로 제어해, 탐색·삽입·삭제의 최악 시간 복잡도를 O(log n)으로 유지한다.
AVL 트리와 레드-블랙 트리는 모두 이 목적을 달성하지만, 균형을 유지하는 강도와 갱신 과정의 비용은 다르다.
높이 제한을 만드는 규칙
균형 이진 탐색 트리는 BST의 키 순서 규칙을 지키면서 노드 삽입·삭제 뒤에도 트리 높이를 O(log n)으로 제한하는 구조다. 경로 길이의 편차를 억제하므로 탐색·삽입·삭제 모두 최악 O(log n)을 보장한다.
AVL 트리는 각 노드의 왼쪽과 오른쪽 서브트리 높이 차인 밸런스 팩터를 -1, 0, +1 범위에 둔다. 균형 조건이 강한 만큼 트리 높이가 낮아 조회에 유리하지만, 삽입과 삭제 때 회전이 비교적 자주 발생한다.
레드-블랙 트리는 RED/BLACK 색상과 블랙-높이 불변식으로 약한 균형을 유지한다. 루트는 BLACK이어야 하고, RED 노드의 자식은 BLACK이어야 하며, 모든 리프(NIL)까지의 BLACK 수는 동일해야 한다. 트리 높이는 ≤ 2·log2(n+1)이며, 평균 회전 수가 적어 갱신 작업에 유리하다.
회전은 정렬 순서를 바꾸지 않는다
LL, RR 단일 회전과 LR, RL 이중 회전은 포인터 배치만 바꾼다. 따라서 In-order 순서는 유지한 채 국소적으로 높이 불균형을 복구할 수 있다.
AVL은 회전을 중심으로 밸런스 팩터를 되돌린다. 레드-블랙 트리는 회전과 재색칠을 함께 사용해 색상 및 블랙-높이 불변식을 복구한다.
삽입의 공통 흐름은 BST 규칙에 따라 노드를 추가한 뒤 위반을 탐지하고, 트리 유형에 맞는 회전과 색 변경을 적용하는 방식이다. AVL은 삽입 경로에서 처음 발견한 위반 조상에 대해 LL/LR/RL/RR를 판정해 1~2회 회전한다. 레드-블랙 트리는 RED-RED 위반에서 삼촌 노드의 색에 따라 재색칠 또는 회전+재색칠을 수행한다.
삭제는 후계자 대체 등을 포함한 BST 삭제 뒤에 보정 단계가 이어진다. AVL은 높이 변화가 상위 노드까지 영향을 줄 수 있어 상향식으로 여러 노드의 회전이 필요할 수 있다. 레드-블랙 트리는 Double-black 처리에서 재색칠을 중심으로 보정하며, 일반적으로 회전 수가 적고 평균 성능이 안정적이다.
조회 중심과 갱신 중심의 선택
| 구분 | 성능 | 확장성 | 일관성(높이) | 안정성(최악) | 운영 편의 |
|---|---|---|---|---|---|
| AVL | 탐색 매우 우수, 삭제·삽입 회전 많음 | 메모리 +height 필드, 균형 유지 비용 증가 | 더 낮은 높이(log2 n) 근접 | 엄격 균형으로 예측 가능성 높음 | 구현 복잡도 중간, 튜닝 항목 적음 |
| Red-Black | 삽입/삭제 평균적으로 유리 | 대규모 갱신 워크로드에 강함 | 높이 ≤ 2·log2(n+1) | 최악 보장 양호, 진동 적음 | 라이브러리/런타임 표준 구현 풍부 |
두 트리 모두 탐색·삽입·삭제는 평균·최악 O(log n), 공간은 O(n)이다. 평균 높이가 더 낮은 AVL은 조회에 강점이 있고, 갱신이 빈번할수록 레드-블랙 트리가 적합하다.
AVL 삽입과 회전 구현
다음 구현은 AVL 삽입과 회전을 간단히 보여 준다. 단조 증가 입력에서도 높이가 한쪽으로 폭주하지 않는지 확인하는 데 초점을 둔다.
# Python 3.10+
from __future__ import annotations
class Node:
__slots__ = ("key", "left", "right", "h")
def __init__(self, key):
self.key = key
self.left: Node | None = None
self.right: Node | None = None
self.h = 1
def height(n: Node | None) -> int:
return n.h if n else 0
def update(n: Node) -> None:
n.h = max(height(n.left), height(n.right)) + 1
def rotate_right(y: Node) -> Node:
x = y.left
T2 = x.right if x else None
x.right = y
y.left = T2
update(y); update(x)
return x
def rotate_left(x: Node) -> Node:
y = x.right
T2 = y.left if y else None
y.left = x
x.right = T2
update(x); update(y)
return y
def balance_factor(n: Node) -> int:
return height(n.left) - height(n.right)
def rebalance(n: Node) -> Node:
update(n)
bf = balance_factor(n)
if bf > 1: # Left heavy
if balance_factor(n.left) < 0: # LR
n.left = rotate_left(n.left)
return rotate_right(n) # LL
if bf < -1: # Right heavy
if balance_factor(n.right) > 0: # RL
n.right = rotate_right(n.right)
return rotate_left(n) # RR
return n
def insert(root: Node | None, key) -> Node:
if root is None:
return Node(key)
if key == root.key:
# 정책: 중복 무시. 필요 시 카운트/리스트로 확장 가능.
return root
if key < root.key:
root.left = insert(root.left, key)
else:
root.right = insert(root.right, key)
return rebalance(root)
# 간단 검증
if __name__ == "__main__":
r = None
for k in range(1, 1000):
r = insert(r, k)
# 균형 확인: 높이는 O(log n) ≈ 10
print("height:", height(r))
전제조건은 단순 키 비교가 가능한 타입이며, 중복 키는 무시한다. 삭제는 후계자 치환 뒤 상향식 rebalance를 적용하는 방식으로 확장할 수 있다.
라이브러리와 시스템에서의 활용
Java TreeMap/TreeSet, C++ std::map/std::set은 레드-블랙 기반이다. 일반 목적 컬렉션에서 삽입·삭제 빈도와 다양한 워크로드를 다루기에 적합하다.
Linux 커널과 nginx 등은 만료 시간을 정렬하고 빠르게 최솟값을 추출하는 용도로 레드-블랙 트리를 사용한다. 다수 이벤트 환경에서 갱신 안정성과 예측 가능한 지연을 확보하는 데 쓰인다.
탐색 지연의 상한이 중요한 임베디드·RT 환경에서는 AVL을 선택할 수 있다. 더 낮은 높이로 일정한 조회 지연을 보장하기 때문이다.
조회가 매우 빈번하고 갱신 빈도가 낮다면 AVL을 우선 검토한다. 갱신이 빈번하고 다양한 패턴이나 경합이 있다면 레드-블랙이 더 맞는다. 블록 I/O가 중심인 디스크 인덱스라면 노드 팬아웃으로 캐시·페이지 효율을 높이는 B-Tree/B+Tree를 고려한다.
규모와 운영 조건이 바꾸는 비용
n=1,000,000일 때 log2 n ≈ 20이다. AVL 높이는 ≈ 2022, 레드-블랙 트리의 최대 높이는 ≤ 1회다.40이다. 탐색 깊이가 짧아지면 캐시 미스 감소와 분기 예측 향상으로 이어진다. 삽입 평균 회전은 AVL 12회, 레드-블랙 트리는 0
최악 O(log n) 보장은 일관된 지연과 SLA 수립에 도움이 된다. 표준 라이브러리와 런타임 구현을 활용하면 구현 리스크와 유지보수 비용도 줄일 수 있다.
동시성 환경에서 단일 전역 락은 구현은 단순하지만 경쟁이 심하다. 경량 RW락과 경로 락킹(path locking)을 적용하면 읽기 확장성을 확보하면서 쓰기 시 경합 구간을 줄일 수 있다.
노드를 분할 할당하면 캐시 지역성이 떨어질 수 있다. 풀 할당(슬랩/arena)이나 연속 배열 기반 트리(implicit tree)는 캐시 미스를 줄이는 선택지다. AVL은 height 필드를, 레드-블랙 트리는 color 비트를 사용하므로 메모리 오버헤드도 다르다.
회전과 재색칠은 로컬 변환이다. 중단 가능 구간에서는 원자적 포인터 스왑을 보장하고, 높이·색속성을 확인하는 불변식 검증 헬스체크를 주기적으로 실행하는 방식을 권장한다.