컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
ds · 11

균형 이진트리(AVL/RB)

레드블랙AVL트리

이진 탐색 트리(BST)는 키를 정렬된 상태로 두고 평균 O(log n)에 찾을 수 있게 해 주지만, 키가 이미 정렬된 순서로 들어오면 한쪽으로만 가지가 자라 연결 리스트처럼 높이가 n이 되고 탐색이 O(n)으로 떨어집니다. 균형 이진 탐색 트리는 삽입과 삭제 뒤에 트리를 스스로 다듬어, 입력 순서와 관계없이 높이를 O(log n)으로 유지하는 트리입니다. 가장 널리 알려진 두 가지가 AVL 트리와 레드-블랙 트리이고, 둘 다 회전(rotation)으로 모양을 바꿔 균형을 되찾습니다.

AVL 트리는 모든 노드에서 왼쪽과 오른쪽 서브트리의 높이 차이를 1 이하로 엄격하게 유지합니다. 레드-블랙 트리는 노드에 빨강과 검정 색을 두고 몇 가지 규칙으로 느슨하게 균형을 맞춥니다. 이 토픽에서는 AVL 트리의 불균형이 회전으로 복구되는 과정, 레드-블랙 트리의 색 바꾸기와 회전, 두 트리와 일반 이진 탐색 트리의 높이 차이를 단계별로 봅니다.

핵심 개념

균형 인수

노드의 왼쪽 서브트리 높이에서 오른쪽 서브트리 높이를 뺀 값입니다. AVL 트리는 모든 노드의 균형 인수가 -1, 0, 1 중 하나가 되도록 유지합니다. 이 토픽은 왼쪽 - 오른쪽으로 정의하며, 교재에 따라 부호가 반대일 수 있습니다.

핵심 개념

회전

노드 몇 개의 부모-자식 관계를 바꿔 트리 모양을 고치는 O(1) 연산입니다. 회전해도 중위 순회 순서(키의 정렬 순서)는 그대로여서 이진 탐색 트리의 성질이 유지됩니다.

핵심 개념

LL · RR · LR · RL

불균형이 생긴 방향입니다. LL과 RR은 단일 회전(오른쪽, 왼쪽) 한 번으로, LR과 RL은 안쪽 자식을 먼저 회전한 뒤 반대쪽으로 한 번 더 도는 이중 회전으로 해결합니다.

핵심 개념

레드-블랙 규칙

모든 노드는 빨강 또는 검정이고, 루트는 검정이며, NIL 리프는 검정이고, 빨강 노드의 자식은 모두 검정이며, 어떤 노드에서 자손 NIL 리프까지 가는 모든 경로의 검정 노드 수가 같습니다.

핵심 개념

삼촌의 색

레드-블랙 삽입에서 부모와 새 노드가 모두 빨강일 때 삼촌(부모의 형제)의 색이 처리 방법을 정합니다. 빨강이면 색만 바꾸고, 검정이면 회전과 색 바꾸기로 해결합니다.

핵심 개념

높이의 상한

노드가 n개일 때 AVL 트리의 높이는 약 1.44 log2(n + 2) 이하이고, 레드-블랙 트리의 높이는 2 log2(n + 1) 이하입니다. 둘 다 O(log n)입니다.

순서가 나빠도 느려지지 않습니다 — 정렬된 입력처럼 일반 이진 탐색 트리에 가장 나쁜 입력도, 균형 트리는 회전으로 높이를 O(log n)에 묶어 둡니다.
AVL은 탐색에, 레드-블랙은 변경에 유리한 경향이 있습니다 — AVL은 더 엄격하게 균형을 맞춰 트리가 낮은 경향이 있어 탐색에 유리하고, 레드-블랙은 느슨해서 삽입과 삭제에 필요한 재균형이 적은 경향이 있습니다(워크로드에 따라 다름).
삽입 한 번의 회전은 최대 두 번입니다 — AVL 삽입은 이중 회전까지 해서 최대 두 번의 회전으로 끝나고, 레드-블랙 삽입도 회전은 최대 두 번이며 색 바꾸기가 위로 올라갈 수 있습니다.
균형 정보를 저장합니다 — AVL 노드는 높이(또는 균형 인수)를, 레드-블랙 노드는 색 1비트를 추가로 저장합니다.
이 시각화에서 확인할 것 — "AVL 트리" 탭에서 균형 인수가 깨지고 회전으로 복구되는 순간을, "레드-블랙 트리" 탭에서 삼촌의 색에 따른 처리 차이를, "높이 비교" 탭에서 정렬된 입력의 높이 차이를 STEP으로 확인하세요.

균형 트리의 배경

AVL 트리는 1962년 소련의 수학자 아델손-벨스키(Adelson-Velsky)와 란디스(Landis)가 발표한 자가 균형 이진 탐색 트리이고, 두 사람 성의 첫 글자를 따 이름이 붙었습니다. 레드-블랙 트리는 1972년 루돌프 바이어(Rudolf Bayer)가 대칭 이진 B-트리로 제안했고, 1978년 귀바스(Guibas)와 세지윅(Sedgewick)이 빨강과 검정 색을 쓰는 지금의 형태로 정리했습니다. 이후 교재(CLRS 등)와 여러 언어의 표준 라이브러리에 널리 채택되었습니다.

숫자로 확인하기

AVL 삽입 시나리오 — 키 10, 20, 30, 40, 50, 25를 넣으면 RR 두 번(30 삽입 때와 50 삽입 때)과 RL 한 번(25 삽입 때)이 일어나 회전은 모두 4번(RL은 이중 회전이라 2번)이고, 최종 높이는 3입니다.
레드-블랙 삽입 시나리오 — 키 10, 18, 7, 15, 16, 30, 25를 넣으면 색 바꾸기(삼촌이 빨강)가 두 번, 회전을 쓰는 해결(삼촌이 검정)이 두 번 일어나고 회전은 모두 4번(LR, RL의 첫 회전 포함), 최종 높이는 4입니다.
정렬된 입력 1~15 — 일반 이진 탐색 트리는 높이가 15, AVL 트리는 4, 레드-블랙 트리는 6입니다. 회전은 AVL 11번, 레드-블랙 9번입니다.
높이 상한 — n = 15이면 레드-블랙 높이 상한 2 log2(16) = 8이고, 실제 높이 6은 이 안에 들어옵니다. 키가 100만 개여도 AVL의 높이는 30 이하입니다.

자주 하는 오해

레드-블랙 트리는 AVL보다 항상 빠르다 — 삽입과 삭제가 많을 때 회전이 적어 유리하다고 평가되지만, 탐색이 많으면 더 낮은 AVL이 유리할 수 있습니다. 워크로드에 따라 다릅니다.
균형 트리는 완전히 균형잡힌 트리다 — 완전한 균형이 아니라 높이를 O(log n)으로 묶어 두는 느슨한 조건입니다. 레드-블랙 트리는 가장 긴 경로가 가장 짧은 경로의 최대 2배입니다.
회전하면 키의 순서가 바뀐다 — 회전은 부모-자식 관계만 바꾸고 중위 순회 순서는 보존합니다. 그래서 회전 뒤에도 이진 탐색 트리의 성질이 유지됩니다.
빨강과 검정은 노드의 우선순위를 뜻한다 — 색은 균형을 계산하기 위한 표시일 뿐 데이터의 의미와 무관합니다. 규칙으로 높이를 제한하기 위해 쓰는 1비트 정보입니다.

면접에서 이렇게 물어요

면접 질문

이진 탐색 트리가 왜 균형이 필요한가요?

정렬된 입력처럼 한쪽으로 치우친 삽입이 이어지면 높이가 n이 되어 탐색, 삽입, 삭제가 O(n)이 됩니다. 균형 트리는 높이를 O(log n)으로 유지해 모든 연산을 O(log n)으로 보장합니다.

면접 질문

AVL 트리와 레드-블랙 트리의 차이는?

AVL은 모든 노드의 높이 차이를 1 이하로 엄격히 유지해 트리가 낮아 탐색에 유리하지만, 일반적으로 삽입과 삭제에 회전이 더 필요합니다. 레드-블랙은 색 규칙으로 느슨하게 맞춰 회전이 적어 변경이 많은 곳에 유리한 경향이 있습니다(워크로드에 따라 다름).

면접 질문

LR 불균형은 어떻게 해결하나요?

왼쪽 자식의 오른쪽 서브트리에 삽입되어 생긴 불균형입니다. 먼저 왼쪽 자식을 기준으로 왼쪽 회전해 LL 모양으로 만들고, 불균형 노드를 기준으로 오른쪽 회전합니다.

면접 질문

언제 해시 테이블 대신 균형 트리를 쓰나요?

키를 정렬된 순서로 순회하거나 범위 검색, 가장 가까운 키 찾기가 필요할 때입니다. 해시 테이블은 평균 O(1)이지만 순서 정보를 가지지 않습니다.

실제 시스템에서는

자바 컬렉션 — TreeMap과 TreeSet이 레드-블랙 트리를 쓰고, HashMap은 한 버킷에 충돌이 많이 쌓이면(기본 8개 이상, 테이블 용량 64 이상) 그 버킷을 레드-블랙 트리로 바꿔 최악의 경우를 완화합니다.
C++ 표준 라이브러리 — std::map과 std::set은 정렬된 키와 O(log n) 연산을 보장하며, 주요 구현체는 레드-블랙 트리로 만듭니다.
리눅스 커널 — 완전 공정 스케줄러(CFS, Linux 6.6부터는 EEVDF)가 실행 가능한 태스크를 레드-블랙 트리 기반 구조로 관리합니다. 메모리 영역(VMA)은 Linux 6.0까지 레드-블랙 트리로, 6.1부터는 B-트리 계열의 maple tree로 관리합니다.
데이터베이스 인덱스 — 디스크 기반 인덱스는 B-트리 계열을 기본으로 씁니다. B-트리는 한 노드에 키를 여러 개 담아 디스크 접근 횟수를 줄이도록 설계된 균형 탐색 트리입니다.

함께 보면 좋은 토픽

이진 탐색 트리 — 균형 트리가 고치려는 기본 구조와 한쪽으로 치우치는 문제를 봅니다.
트리 — 높이, 깊이, 순회 같은 트리의 기본 용어를 봅니다.
해시 테이블 — 정렬 순서가 필요 없을 때 쓰는 평균 O(1) 구조를 비교합니다.
힙 — 또 다른 O(log n) 트리 구조인 우선순위 큐를 봅니다.

참고 자료

교재 — Cormen 외, Introduction to Algorithms(CLRS, 3판 · 4판)의 레드-블랙 트리 장, Sedgewick과 Wayne의 Algorithms 4판 3.3절 균형 탐색 트리(왼쪽 기울기 레드-블랙)
논문 — Adelson-Velsky, Landis, "An algorithm for the organization of information" (Doklady Akademii Nauk SSSR 146, 1962), Guibas, Sedgewick, "A Dichromatic Framework for Balanced Trees" (FOCS 1978)

Python — AVL 트리 삽입과 회전

class Node:
    def __init__(self, key):
        self.key, self.left, self.right, self.h = key, None, None, 1

def h(n): return n.h if n else 0
def update(n): n.h = 1 + max(h(n.left), h(n.right))
def balance(n): return h(n.left) - h(n.right)    # 균형 인수

def rotate_right(z):
    y = z.left
    z.left, y.right = y.right, z
    update(z); update(y)
    return y                          # 서브트리의 새 루트

def rotate_left(z):
    y = z.right
    z.right, y.left = y.left, z
    update(z); update(y)
    return y

def insert(n, key):
    if not n: return Node(key)
    if key < n.key: n.left = insert(n.left, key)
    else: n.right = insert(n.right, key)
    update(n)
    b = balance(n)
    if b > 1:                                  # 왼쪽이 무거움
        if balance(n.left) < 0: n.left = rotate_left(n.left)    # LR: 먼저 왼쪽 회전
        return rotate_right(n)
    if b < -1:                                 # 오른쪽이 무거움
        if balance(n.right) > 0: n.right = rotate_right(n.right)  # RL: 먼저 오른쪽 회전
        return rotate_left(n)
    return n

root = None
for k in [10, 20, 30, 40, 50, 25]:
    root = insert(root, k)
print(root.key, root.left.key, root.right.key)   # 30 20 40

각 노드는 서브트리의 높이 h를 저장하고, 균형 인수(왼쪽 높이 - 오른쪽 높이)가 1보다 크거나 -1보다 작아지면 회전으로 복구합니다. 균형 인수가 2이고 왼쪽 자식이 왼쪽으로 기운 경우는 오른쪽 회전 한 번(LL), 왼쪽 자식이 오른쪽으로 기운 경우는 왼쪽 자식을 먼저 왼쪽 회전한 뒤 오른쪽 회전(LR)입니다. 오른쪽 방향은 대칭입니다. 재귀로 삽입한 뒤 돌아오면서 경로의 노드마다 높이를 갱신하고 균형을 검사하므로, 불균형이 생긴 가장 아래 노드에서 회전하면 위쪽은 자동으로 균형이 복구됩니다. 회전은 포인터 몇 개를 바꾸는 O(1) 작업입니다. 키 10, 20, 30, 40, 50, 25를 넣으면 시각화의 AVL 탭과 같이 루트 30, 왼쪽 자식 20, 오른쪽 자식 40이 됩니다. 삭제도 같은 회전으로 복구하지만 위쪽까지 여러 번 회전이 필요할 수 있어 이 코드에는 넣지 않았습니다.

Python — 정렬된 입력에서 BST와 AVL의 높이

def bst_insert(n, key):          # 균형을 맞추지 않는 일반 삽입
    if not n: return Node(key)
    if key < n.key: n.left = bst_insert(n.left, key)
    else: n.right = bst_insert(n.right, key)
    update(n)
    return n

plain = avl = None
for k in range(1, 16):          # 1, 2, ..., 15를 정렬된 순서로
    plain = bst_insert(plain, k)
    avl = insert(avl, k)
print(h(plain), h(avl))        # 15 4

같은 Node와 update를 쓰되 회전을 하지 않는 삽입 함수를 하나 더 만들어, 1부터 15까지 정렬된 순서로 넣었을 때의 높이를 비교합니다. 일반 이진 탐색 트리는 모든 키가 오른쪽으로 이어져 높이가 15인 연결 리스트가 되고, AVL 트리는 회전으로 높이 4를 유지합니다. 탐색은 높이만큼 비교하므로 일반 트리에서는 최대 15번, AVL에서는 최대 4번입니다. 키가 100만 개이면 이 차이는 100만 번 대 약 20번(정렬된 입력일 때 AVL의 높이)으로 벌어집니다. 시각화의 높이 비교 탭이 보여 주는 값(15와 4)과 같습니다. 레드-블랙 트리도 같은 입력에서 높이 O(log n)을 유지합니다.

Java — TreeMap(레드-블랙 트리 기반) 사용

import java.util.TreeMap;

TreeMap<Integer, String> map = new TreeMap<>();
map.put(30, "c"); map.put(10, "a"); map.put(20, "b");

System.out.println(map.firstKey());      // 10  (가장 작은 키)
System.out.println(map.floorKey(25));    // 20  (25 이하 중 가장 큰 키)
System.out.println(map.ceilingKey(25));  // 30  (25 이상 중 가장 작은 키)
System.out.println(map.headMap(30));     // {10=a, 20=b}  (30 미만)

자바의 TreeMap은 레드-블랙 트리를 바탕으로 한 NavigableMap으로, put, get, remove가 O(log n)에 동작하고 키를 정렬된 순서로 유지합니다. 직접 균형을 맞추는 코드를 쓰지 않아도 레드-블랙 트리의 이점을 그대로 얻습니다. 해시 맵과 달리 정렬된 순서를 알기 때문에 floorKey, ceilingKey, headMap 같은 범위 질의를 쓸 수 있습니다. 25 이하에서 가장 큰 키를 O(log n)에 찾는 일은, 해시 맵으로는 전체를 훑어야 해서 O(n)이 걸립니다. 정렬이나 범위 검색이 필요 없다면 해시 맵이 평균 O(1)로 더 빠르므로, 순서가 필요한 곳에 TreeMap을 고르는 것이 일반적입니다.

Q1

AVL 트리가 유지하는 균형 조건은?

각 노드의 "균형 인수"의 절댓값이 얼마 이하여야 할까요?
AVL 트리는 모든 노드에서 좌우 서브트리의 높이 차이(균형 인수)가 -1, 0, 1 중 하나가 되도록 유지합니다. 삽입·삭제로 이 조건이 깨지면 회전으로 복구합니다.

Q2

AVL 트리에서 LL 불균형(왼쪽 자식의 왼쪽 서브트리에 삽입되어 생긴 불균형)을 해결하는 회전은?

왼쪽으로 쏠렸으니 오른쪽으로 당겨야 합니다.
LL 불균형은 왼쪽 방향으로 치우쳐 있으므로 불균형 노드를 기준으로 오른쪽 단일 회전을 하면 균형이 복구됩니다. LR 불균형은 왼쪽-오른쪽 이중 회전이 필요합니다.

Q3

레드-블랙 트리가 AVL 트리보다 삽입·삭제에 유리하다고 평가되는 이유는?

AVL은 더 엄격하게 균형을 맞춥니다.
레드-블랙 트리는 색과 규칙으로 대략적인 균형만 유지해 삽입·삭제 시 회전이 적습니다. 대신 트리가 조금 더 높을 수 있어 탐색은 AVL이 약간 더 빠른 편입니다.