Theme:

BST가 편향되면 O(n)이 되는 문제, 어떻게 하면 항상 O(log n)을 보장할 수 있을까요?

AVL 트리란

AVL 트리는 모든 노드에서 왼쪽과 오른쪽 서브트리의 높이 차이가 최대 1 인 자가 균형 이진 탐색 트리입니다. 1962년에 Adelson-Velsky와 Landis가 발명했습니다.

핵심 개념은 균형 인수(Balance Factor) 입니다.

PLAINTEXT
Balance Factor = 왼쪽 서브트리 높이 - 오른쪽 서브트리 높이
  • BF가 -1, 0, 1이면 균형 상태
  • BF가 -2 이하 또는 2 이상이면 ** 불균형** → 회전이 필요

왜 필요한가

일반 BST는 편향되면 O(n)이 됩니다. AVL 트리는 삽입/삭제 후 불균형이 감지되면 ** 회전 **으로 즉시 균형을 맞추므로, ** 항상 O(log n)**을 보장합니다.

PLAINTEXT
편향 BST (O(n)):        AVL 트리 (O(log n)):
    1                       3
     \                     / \
      2                   2   4
       \                 /     \
        3               1       5
         \
          4
           \
            5

노드 구조

JAVA
class AVLNode<T extends Comparable<T>> {
    T value;
    AVLNode<T> left, right;
    int height; // 노드의 높이

    AVLNode(T value) {
        this.value = value;
        this.height = 1; // 리프 노드의 높이는 1
    }
}

유틸리티 메서드

JAVA
int height(AVLNode<T> node) {
    return (node == null) ? 0 : node.height;
}

int balanceFactor(AVLNode<T> node) {
    return height(node.left) - height(node.right);
}

void updateHeight(AVLNode<T> node) {
    node.height = 1 + Math.max(height(node.left), height(node.right));
}

회전 — 균형을 맞추는 핵심 연산

불균형이 발생하면 네 가지 케이스 중 하나에 해당하며, 각각에 맞는 회전을 수행합니다.

오른쪽 회전 (Right Rotation) — LL 케이스

왼쪽 자식의 왼쪽에 삽입되어 불균형이 생긴 경우입니다.

PLAINTEXT
       z (BF=2)                y
      / \                    /   \
     y   T4       →        x     z
    / \                   / \   / \
   x   T3               T1 T2 T3 T4
  / \
 T1  T2
JAVA
AVLNode<T> rotateRight(AVLNode<T> z) {
    AVLNode<T> y = z.left;
    AVLNode<T> T3 = y.right;

    // 회전 수행
    y.right = z;
    z.left = T3;

    // 높이 업데이트 (z를 먼저, y가 z의 부모이므로)
    updateHeight(z);
    updateHeight(y);

    return y; // 새로운 루트
}

왼쪽 회전 (Left Rotation) — RR 케이스

오른쪽 자식의 오른쪽에 삽입되어 불균형이 생긴 경우입니다.

PLAINTEXT
   z (BF=-2)                 y
  / \                      /   \
 T1  y          →         z     x
    / \                  / \   / \
   T2  x               T1 T2 T3 T4
      / \
     T3  T4
JAVA
AVLNode<T> rotateLeft(AVLNode<T> z) {
    AVLNode<T> y = z.right;
    AVLNode<T> T2 = y.left;

    y.left = z;
    z.right = T2;

    updateHeight(z);
    updateHeight(y);

    return y;
}

LR 케이스 — 왼쪽 회전 후 오른쪽 회전

왼쪽 자식의 ** 오른쪽 **에 삽입된 경우입니다. 단순 오른쪽 회전으로는 해결되지 않습니다.

PLAINTEXT
     z (BF=2)        z               x
    / \              / \            /   \
   y   T4    →      x   T4   →    y     z
  / \              / \            / \   / \
 T1  x            y   T3        T1 T2 T3 T4
    / \          / \
   T2  T3       T1  T2

   원래         왼쪽 회전(y)     오른쪽 회전(z)
JAVA
// LR 케이스: 왼쪽 자식을 왼쪽 회전 → 전체를 오른쪽 회전
node.left = rotateLeft(node.left);
return rotateRight(node);

RL 케이스 — 오른쪽 회전 후 왼쪽 회전

오른쪽 자식의 ** 왼쪽 **에 삽입된 경우입니다. LR의 대칭입니다.

JAVA
// RL 케이스: 오른쪽 자식을 오른쪽 회전 → 전체를 왼쪽 회전
node.right = rotateRight(node.right);
return rotateLeft(node);

삽입

JAVA
AVLNode<T> insert(AVLNode<T> node, T value) {
    // 1. 일반 BST 삽입
    if (node == null) return new AVLNode<>(value);

    int cmp = value.compareTo(node.value);
    if (cmp < 0) {
        node.left = insert(node.left, value);
    } else if (cmp > 0) {
        node.right = insert(node.right, value);
    } else {
        return node; // 중복 허용하지 않음
    }

    // 2. 높이 업데이트
    updateHeight(node);

    // 3. 균형 확인 및 회전
    int bf = balanceFactor(node);

    // LL 케이스
    if (bf > 1 && value.compareTo(node.left.value) < 0) {
        return rotateRight(node);
    }
    // RR 케이스
    if (bf < -1 && value.compareTo(node.right.value) > 0) {
        return rotateLeft(node);
    }
    // LR 케이스
    if (bf > 1 && value.compareTo(node.left.value) > 0) {
        node.left = rotateLeft(node.left);
        return rotateRight(node);
    }
    // RL 케이스
    if (bf < -1 && value.compareTo(node.right.value) < 0) {
        node.right = rotateRight(node.right);
        return rotateLeft(node);
    }

    return node;
}

삽입 예시

1, 2, 3을 순서대로 삽입하면:

PLAINTEXT
삽입 1:  1       삽입 2:  1 (BF=-2)     회전 후:  2
                  \                             / \
                   2                           1   3
                    \
                     3

정렬된 데이터를 삽입해도 균형이 유지됩니다.

삭제

삭제 후에도 균형 확인과 회전이 필요합니다. BST 삭제 로직에 균형 유지 코드를 추가합니다.

JAVA
AVLNode<T> delete(AVLNode<T> node, T value) {
    if (node == null) return null;

    int cmp = value.compareTo(node.value);
    if (cmp < 0) {
        node.left = delete(node.left, value);
    } else if (cmp > 0) {
        node.right = delete(node.right, value);
    } else {
        // 삭제 대상 발견
        if (node.left == null) return node.right;
        if (node.right == null) return node.left;

        // 자식이 2개: 중위 후속자로 대체
        AVLNode<T> successor = findMin(node.right);
        node.value = successor.value;
        node.right = delete(node.right, successor.value);
    }

    updateHeight(node);

    // 균형 확인 및 회전 (삽입과 동일한 로직)
    int bf = balanceFactor(node);

    if (bf > 1 && balanceFactor(node.left) >= 0) return rotateRight(node);
    if (bf > 1 && balanceFactor(node.left) < 0) {
        node.left = rotateLeft(node.left);
        return rotateRight(node);
    }
    if (bf < -1 && balanceFactor(node.right) <= 0) return rotateLeft(node);
    if (bf < -1 && balanceFactor(node.right) > 0) {
        node.right = rotateRight(node.right);
        return rotateLeft(node);
    }

    return node;
}

삭제에서는 삽입과 달리 ** 연쇄 회전 **이 발생할 수 있습니다. 삭제로 인해 여러 조상 노드의 균형이 깨질 수 있기 때문입니다.

AVL 트리 vs Red-Black 트리

특성AVL 트리Red-Black 트리
균형 조건높이 차이 ≤ 1 (엄격)색상 규칙 (느슨)
최대 높이~1.44 log n~2 log n
검색 성능더 빠름약간 느림
삽입/삭제회전 많음 (최대 O(log n))회전 적음 (최대 2~3번)
사용 사례읽기 중심읽기/쓰기 균형

실제 사용

  • Java TreeMap/TreeSet: Red-Black 트리 사용
  • **Linux CFS 스케줄러 **: Red-Black 트리 사용
  • ** 데이터베이스 인메모리 인덱스 **: AVL 트리가 유리할 수 있음
  • C++ STL map: Red-Black 트리 사용

Red-Black 트리가 더 널리 쓰이는 이유는 삽입/삭제가 빈번한 실무 환경에서 회전 횟수가 적기 때문입니다.

AVL 트리의 수학적 성질

높이 h인 AVL 트리의 최소 노드 수를 N(h)라 하면:

PLAINTEXT
N(0) = 1
N(1) = 2
N(h) = N(h-1) + N(h-2) + 1

이는 피보나치 수와 유사하며, 역으로 n개의 노드를 가진 AVL 트리의 최대 높이는 약 1.44 × log₂(n) 입니다.

이것이 AVL 트리가 "높이 최적화된" 트리인 이유입니다. Red-Black 트리(~2 log n)보다 높이가 낮으므로, 읽기 연산이 더 빠릅니다.

정리

  • AVL 트리는 균형 인수(BF)가 -1, 0, 1 인 자가 균형 BST입니다.
  • 불균형 시 LL, RR, LR, RL 네 가지 회전으로 균형을 맞춥니다.
  • ** 검색 **은 Red-Black 트리보다 빠르지만, ** 삽입/삭제 **는 회전이 더 많이 필요합니다.
  • 실무에서는 Red-Black 트리가 더 많이 사용되지만, 읽기 중심 워크로드에서는 AVL이 유리할 수 있습니다.
  • 회전의 핵심은 BST 속성을 유지하면서 높이를 줄이는 것 입니다.
댓글 로딩 중...