Krafton Jungle/2. Keywords

[WEEK06] AVL 트리

munsik22 2025. 4. 21. 15:19

AVL 트리

AVL 트리Adelson-Velsky and Landis Tree는 이진 탐색 트리의 일종으로, 모든 노드에서 왼쪽 서브트리와 오른쪽 서브트리의 높이 차이(균형 인수)가 1 이하가 되도록 자동으로 균형을 맞추는 구조다.

🎯 핵심 포인트: 삽입이나 삭제 이후 트리의 균형이 깨지면, 회전Rotation을 통해 다시 균형을 맞춘다.

균형 인수 (Balance Factor)

  • 각 노드의 균형 인수 = 왼쪽 서브트리 높이 - 오른쪽 서브트리 높이
  • 가능한 균형 인수: -1, 0, 1
  • 이 범위를 벗어나는 경우 불균형 상태이며, 회전이 필요하다.

회전 (Rotation)

불균형 유형 해결 방법
LL (Left-Left) 오른쪽 회전 (Right Rotation)
RR (Right-Right) 왼쪽 회전 (Left Rotation)
LR (Left-Right) 왼쪽 회전 후 오른쪽 회전 (Left-Right Rotation)
RL (Right-Left) 오른쪽 회전 후 왼쪽 회전 (Right-Left Rotation)

각 회전의 목적은 불균형 노드를 루트로 하는 서브트리의 균형을 되찾는 것이다.

삽입

기본적으로는 BST의 삽입과 같으나, 항상 balance factor에 유의해야 한다.

90 삽입 후 균형이 깨진 예시
30 삽입 후 균형이 깨진 예시

삭제

마찬가지로 BST의 삭제와 같으나, 항상 balance factor에 유의해야 한다.

30 삭제 후 균형이 깨진 예시
60 삭제 후 균형이 깨진 예시

시간 복잡도

AVL 트리는 항상 균형을 유지하므로, 다음과 같은 시간 복잡도를 보장한다.

  • 탐색: O(log n)
  • 삽입: O(log n)
  • 삭제: O(log n)

BST는 최악의 경우 선형 구조로 퇴화(O(n))할 수 있지만, AVL 트리는 그럴 위험이 없다는 장점이 있다.

💻 AVL 트리 구현 예제

더보기
#include <stdio.h>
#include <stdlib.h>

typedef struct node {
    int key;
    int height;
    int count; // 중복 개수
    struct node* left;
    struct node* right;
} node;

int max(int a, int b) {
    return (a > b) ? a : b;
}

int height(node* n) {
    if (n == NULL) return 0;
    return n->height;
}

node* create_node(int key) {
    node* new_node = (node*)malloc(sizeof(node));
    new_node->key = key;
    new_node->height = 1;
    new_node->count = 1; // 초기 중복 count
    new_node->left = new_node->right = NULL;
    return new_node;
}

node* rotate_right(node* y) {
    node* x = y->left;
    node* T2 = x->right;

    x->right = y;
    y->left = T2;

    y->height = max(height(y->left), height(y->right)) + 1;
    x->height = max(height(x->left), height(x->right)) + 1;

    return x;
}

node* rotate_left(node* x) {
    node* y = x->right;
    node* T2 = y->left;

    y->left = x;
    x->right = T2;

    x->height = max(height(x->left), height(x->right)) + 1;
    y->height = max(height(y->left), height(y->right)) + 1;

    return y;
}

int get_balance(node* n) {
    if (n == NULL) return 0;
    return height(n->left) - height(n->right);
}

// 중복 허용 삽입
node* insert(node* n, int key) {
    if (n == NULL) return create_node(key);

    if (key < n->key)
        n->left = insert(n->left, key);
    else if (key > n->key)
        n->right = insert(n->right, key);
    else {
        n->count += 1; // 중복 key
        return n;
    }

    n->height = 1 + max(height(n->left), height(n->right));
    int balance = get_balance(n);

    // LL
    if (balance > 1 && key < n->left->key)
        return rotate_right(n);

    // RR
    if (balance < -1 && key > n->right->key)
        return rotate_left(n);

    // LR
    if (balance > 1 && key > n->left->key) {
        n->left = rotate_left(n->left);
        return rotate_right(n);
    }

    // RL
    if (balance < -1 && key < n->right->key) {
        n->right = rotate_right(n->right);
        return rotate_left(n);
    }

    return n;
}

// 최소값 노드 찾기
node* min_value_node(node* n) {
    node* current = n;
    while (current->left != NULL)
        current = current->left;
    return current;
}

// 삭제 함수
node* delete_node(node* root, int key) {
    if (root == NULL) return root;

    if (key < root->key)
        root->left = delete_node(root->left, key);
    else if (key > root->key)
        root->right = delete_node(root->right, key);
    else {
        if (root->count > 1) {
            root->count -= 1;
            return root;
        }

        if (root->left == NULL || root->right == NULL) {
            node* temp = root->left ? root->left : root->right;
            if (temp == NULL) {
                temp = root;
                root = NULL;
            } else {
                *root = *temp;
            }
            free(temp);
        } else {
            node* temp = min_value_node(root->right);
            root->key = temp->key;
            root->count = temp->count;
            temp->count = 1; // 삭제 대상만 줄이기
            root->right = delete_node(root->right, temp->key);
        }
    }

    if (root == NULL) return root;

    root->height = 1 + max(height(root->left), height(root->right));
    int balance = get_balance(root);

    // 회전 케이스
    if (balance > 1 && get_balance(root->left) >= 0)
        return rotate_right(root);

    if (balance > 1 && get_balance(root->left) < 0) {
        root->left = rotate_left(root->left);
        return rotate_right(root);
    }

    if (balance < -1 && get_balance(root->right) <= 0)
        return rotate_left(root);

    if (balance < -1 && get_balance(root->right) > 0) {
        root->right = rotate_right(root->right);
        return rotate_left(root);
    }

    return root;
}

// 검색 함수
node* search(node* root, int key) {
    if (root == NULL || root->key == key)
        return root;
    if (key < root->key)
        return search(root->left, key);
    return search(root->right, key);
}

// 중위 순회
void inorder(node* root) {
    if (root != NULL) {
        inorder(root->left);
        for (int i = 0; i < root->count; i++)
            printf("%d ", root->key);
        inorder(root->right);
    }
}

// 동적 메모리 해제
void free_tree(node* root) {
    if (root == NULL) return;
    free_tree(root->left);
    free_tree(root->right);
    free(root);
}

int main() {
    node* root = NULL;
    int keys[] = {10, 20, 10, 30, 20, 20, 40, 25};
    int n = sizeof(keys) / sizeof(keys[0]);

    for (int i = 0; i < n; i++) {
        root = insert(root, keys[i]);
    }

    printf("중위 순회 결과: ");
    inorder(root);
    printf("\n");

    int search_key = 20;
    node* found = search(root, search_key);
    if (found)
        printf("키 %d 존재 (개수: %d)\n", search_key, found->count);
    else
        printf("키 %d 없음\n", search_key);

    // 삭제 테스트
    root = delete_node(root, 20);
    printf("20 삭제 후: ");
    inorder(root);
    printf("\n");

    root = delete_node(root, 20);
    root = delete_node(root, 20);
    printf("20 완전 삭제 후: ");
    inorder(root);
    printf("\n");

    free_tree(root);
    return 0;
}

AVL 트리 vs RB 트리

AVL 트리와 레드-블랙 트리는 자기 균형 이진 탐색 트리Self-Balancing BST라는 공통점을 가지지만, 균형을 유지하는 방식과 그로 인한 성능 특성에는 차이점이 있다.

항목 AVL 트리 레드-블랙 트리
균형 조건 모든 노드의 왼쪽, 오른쪽 서브트리 높이 차이가 ≤ 1 색상 규칙(레드/블랙) 기반의 완화된 균형
균형 정도 더 엄격하게 균형을 유지 덜 엄격하게 균형을 유지
회전 횟수 삽입/삭제 시 회전이 더 자주 발생 상대적으로 회전이 적음
탐색 성능 더 균형 잡혀 있어 탐색 속도 우수 AVL보다는 약간 느릴 수 있음
삽입/삭제 성능 삭제 연산이 느릴 수 있음 (재균형이 복잡) 삽입/삭제 성능이 상대적으로 더 일정함
사용 사례 읽기/탐색이 많은 시스템에 유리
(ex. 데이터베이스 색인)
쓰기/수정이 많은 시스템에 유리
(ex. OS의 map/set 구현)
구현 복잡도 더 복잡 (높이 계산 + 다양한 회전 케이스) 상대적으로 간단 (색상 규칙 중심)
노드 속성 키, 높이 (또는 균형 인수) 키, 색상(Red/Black)

 

  • AVL 트리빠른 탐색이 중요한 상황에서 유리하다. (검색 위주의 시스템, 데이터베이스 색인 구조 등에서 사용)
  • 레드-블랙 트리삽입/삭제가 빈번한 경우에 더 효율적이다. (STL의 map, set, Java의 TreeMap, Linux 커널 등에서 사용)

[참고자료]

 

AVL Tree를 알아보자

일반적인 이진검색트리에서는 트리구조가 한 쪽으로 치우쳐지는 경우가 발생할 수 있습니다. 이진검색트리의 평균 검색속도는 O(logN)이지만 한 쪽으로 치우쳐진 경우에는 검색속도가 O(N)까지

velog.io

'Krafton Jungle > 2. Keywords' 카테고리의 다른 글

[WEEK07] 묵시적 가용 리스트  (0) 2025.04.25
[WEEK07] 동적 메모리 할당  (0) 2025.04.25
[WEEK06] 메모리 누수  (0) 2025.04.18
[WEEK06] 레드 블랙 트리 (Red-Black Tree)  (0) 2025.04.17
[WEEK06] 균형 탐색 트리  (0) 2025.04.16