본 시리즈는 프로그래밍 자료구조의 개념을 정리하고 실습을 진행해보는 시리즈입니다.
발명자의 이름인 Adelson-Velsky and Landis에서 따온 이름입니다.
AVL Tree는 균형 이진 탐색 트리(Self-balancing Binary Search Tree)의 일종으로, 트리의 높이를 항상 일정하게 유지하여 탐색, 삽입, 삭제 연산에서 O(log n)의 성능을 보장하는 자료구조입니다.
AVL 트리는 삽입이나 삭제가 발생할 때마다 트리의 균형을 유지하기 위해 회전(Rotation) 연산을 사용합니다. 이러한 균형 유지 메커니즘 덕분에, 비균형 트리로 인한 성능 저하를 방지할 수 있습니다.
균형 인수(Height Balance Factor):
-1
, 0
, 1
을 유지해야 합니다.0
: 왼쪽과 오른쪽 서브트리의 높이가 동일함.1
: 왼쪽 서브트리의 높이가 오른쪽보다 1 크다는 의미.-1
: 오른쪽 서브트리의 높이가 왼쪽보다 1 크다는 의미.왼쪽 높이 - 오른쪽 높이
입니다.균형을 깨트리는 연산:
회전 연산:
LL회전
, RR회전
LR회전
, RL회전
시간 복잡도:
장점:
단점:
주요 용도:
AVL 트리는 삽입(Insertion), 삭제(Deletion) 시 트리의 균형을 유지하기 위해 회전 연산을 사용합니다. 이 섹션에서는 트리의 균형이 깨졌을 때 회전으로 균형을 되찾는 과정을 설명합니다.
AVL 트리에서 노드를 삽입하거나 삭제할 때, 특정 노드의 균형 인수(Balance Factor)가 2 이상 또는 -2 이하로 변할 수 있습니다.
트리의 균형이 깨질 때는 네 가지 케이스가 발생합니다:
각 케이스에 맞는 회전 연산을 적용하여 트리의 균형을 맞출 수 있습니다.
다음과 같이 각 회전 케이스를 표로 정리할 수 있습니다:
회전 종류 | 조건 (루트 노드의 BF) | 자식 노드의 BF 조건 | 설명 |
---|---|---|---|
LL 회전 | BF ≥ +2 | 왼쪽 자식의 BF ≥ 0 | 왼쪽 자식이 다시 왼쪽으로 치우친 경우. 단순 LL 회전으로 해결. |
RR 회전 | BF ≤ -2 | 오른쪽 자식의 BF ≤ 0 | 오른쪽 자식이 다시 오른쪽으로 치우친 경우. 단순 RR 회전으로 해결. |
LR 회전 | BF ≥ +2 | 왼쪽 자식의 BF < 0 | 왼쪽 자식이 오른쪽으로 치우친 경우. 먼저 RR 회전 후, LL 회전. |
RL 회전 | BF ≤ -2 | 오른쪽 자식의 BF > 0 | 오른쪽 자식이 왼쪽으로 치우친 경우. 먼저 LL 회전 후, RR 회전. |
노드를 삽입할 때, AVL 트리는 삽입 후 트리의 균형이 깨질 수 있기 때문에 균형 인수(Balance Factor, BF)를 확인하면서 회전 연산이 필요할 수 있습니다.
+2
또는 -2
로 변화하면, 불균형 상태가 발생한 것입니다. 회전 종류 | 조건 (루트 노드의 BF) | 자식 노드의 BF 조건 | 설명 |
---|---|---|---|
LL 회전 | BF ≥ +2 | 왼쪽 자식의 BF ≥ 0 | 왼쪽 자식이 다시 왼쪽으로 치우친 경우. 단순 LL 회전으로 해결. |
RR 회전 | BF ≤ -2 | 오른쪽 자식의 BF ≤ 0 | 오른쪽 자식이 다시 오른쪽으로 치우친 경우. 단순 RR 회전으로 해결. |
LR 회전 | BF ≥ +2 | 왼쪽 자식의 BF < 0 | 왼쪽 자식이 오른쪽으로 치우친 경우. 먼저 RR 회전 후, LL 회전. |
RL 회전 | BF ≤ -2 | 오른쪽 자식의 BF > 0 | 오른쪽 자식이 왼쪽으로 치우친 경우. 먼저 LL 회전 후, RR 회전. |
아래는 노드 삽입 후 발생하는 RR 케이스의 예시입니다.
20
의 BF는 -1
입니다.25
의 BF는 0
으로 균형 상태에 있습니다.삽입될 노드는 27
이며, 이 삽입으로 인해 트리의 균형이 깨지게 됩니다.
27
을 노드 30
의 왼쪽 자식으로 삽입합니다. 균형 인수
를 다시 계산합니다.25
는 왼쪽 서브트리의 높이보다 오른쪽 서브트리의 높이가 커져 균형 인수가 -1
로 변합니다.20
은 오른쪽 서브트리의 높이가 왼쪽보다 3으로 커져서 균형 인수가 -2
가 됩니다. 이로 인해 불균형 상태가 발생합니다.20
의 균형 인수가 -2
이고 오른쪽 자식 노드의 균형 인수가 -1
로 0보다 작거나 같기 때문에
오른쪽 서브트리가 더 깊은 RR 케이스입니다.25
가 새로운 루트가 되고, 노드 20
은 노드 25
의 왼쪽 자식으로 내려오면서 노드 22
는 노드 20
의 오른쪽 자식이 됩니다. 30
과 27
은 회전 이후에도 같은 위치에 남아있습니다.0
또는 1
로 유지되어 균형 상태를 되찾았습니다.25
의 균형 인수는 0
이 됐습니다.20
과 오른쪽 자식 노드 30
도 각각 균형 인수가 0
또는 1
로 맞춰졌습니다.이 예시에서는 노드 삽입 후 트리의 균형이 깨졌을 때, RR 회전을 통해 트리가 다시 균형을 되찾는 과정을 볼 수 있었습니다.
AVL 트리에서 노드를 삭제할 때도 삽입과 마찬가지로 트리의 균형이 깨질 수 있습니다.
회전 종류 | 조건 (루트 노드의 BF) | 자식 노드의 BF 조건 | 설명 |
---|---|---|---|
LL 회전 | BF ≥ +2 | 왼쪽 자식의 BF ≥ 0 | 왼쪽 자식이 다시 왼쪽으로 치우친 경우. 단순 LL 회전으로 해결. |
RR 회전 | BF ≤ -2 | 오른쪽 자식의 BF ≤ 0 | 오른쪽 자식이 다시 오른쪽으로 치우친 경우. 단순 RR 회전으로 해결. |
LR 회전 | BF ≥ +2 | 왼쪽 자식의 BF < 0 | 왼쪽 자식이 오른쪽으로 치우친 경우. 먼저 RR 회전 후, LL 회전. |
RL 회전 | BF ≤ -2 | 오른쪽 자식의 BF > 0 | 오른쪽 자식이 왼쪽으로 치우친 경우. 먼저 LL 회전 후, RR 회전. |
아래 예시는 노드 삭제 후 LL 회전을 통해 트리의 균형을 맞추는 과정을 보여줍니다.
초기 상태: 삭제 전에 트리의 각 노드의 균형 인수는 적절히 유지되고 있습니다.
25
의 BF는 1
입니다.20
의 BF는 1
입니다.30
의 BF는 1
입니다.27
입니다.노드 삭제: 노드 27
을 삭제합니다.
30
의 오른쪽 자식이 없어져 서브트리의 높이가 감소하고, 그로 인해 부모 노드들의 균형 인수가 변하게 됩니다.30
의 균형 인수는 0
으로 감소했고, 부모 노드 25
의 균형 인수가 +2
로 변하여 불균형 상태가 됩니다.25
는 왼쪽 서브트리가 오른쪽 서브트리보다 2
만큼 크고, 그 왼쪽 자식 노드의 균형 인수가 1
로 0보다 크거나 같아서
왼쪽 서브트리가 더 깊은 LL 케이스에 해당합니다.20
이 새로운 루트가 되고, 노드 25
는 오른쪽 자식으로 내려가고, 노드 22
는 노드 25
의 왼쪽 자식이 됩니다.30
, 10
, 15
노드의 위치는 변하지 않습니다.20
의 균형 인수는 0
이 됐습니다.10
의 BF는 -1
, 오른쪽 자식 25
의 BF는 0
입니다.위 예시는 노드 삭제 후 트리의 균형이 깨질 때, LL 회전을 통해 트리가 다시 균형을 유지하는 과정을 시각적으로 볼 수 있었습니다.
먼저, Java로 AVL 트리를 구현하는 방법을 살펴보겠습니다. AVL 트리의 핵심 기능인 삽입, 삭제, 균형 유지(회전 연산)을 구현할 것입니다.
먼저 각 노드를 정의하는 클래스를 작성합니다.
class Node {
int key, height;
Node left, right;
Node(int d) {
key = d;
height = 1;
}
}
이제 AVL 트리 클래스에서 주요 연산들을 구현합니다.
class AVLTree {
Node root;
// 높이 반환
int height(Node N) {
if (N == null)
return 0;
return N.height;
}
// 균형 인수 계산
int getBalance(Node N) {
if (N == null)
return 0;
return height(N.left) - height(N.right);
}
// 오른쪽 회전
Node rightRotate(Node y) {
Node x = y.left;
Node T2 = x.right;
// 회전 수행
x.right = y;
y.left = T2;
// 높이 업데이트
y.height = Math.max(height(y.left), height(y.right)) + 1;
x.height = Math.max(height(x.left), height(x.right)) + 1;
return x;
}
// 왼쪽 회전
Node leftRotate(Node x) {
Node y = x.right;
Node T2 = y.left;
// 회전 수행
y.left = x;
x.right = T2;
// 높이 업데이트
x.height = Math.max(height(x.left), height(x.right)) + 1;
y.height = Math.max(height(y.left), height(y.right)) + 1;
return y;
}
// 노드 삽입
Node insert(Node node, int key) {
if (node == null)
return (new Node(key));
if (key < node.key)
node.left = insert(node.left, key);
else if (key > node.key)
node.right = insert(node.right, key);
else
return node;
// 높이 업데이트
node.height = 1 + Math.max(height(node.left), height(node.right));
// 균형 확인
int balance = getBalance(node);
// LL 케이스
if (balance > 1 && key < node.left.key)
return rightRotate(node);
// RR 케이스
if (balance < -1 && key > node.right.key)
return leftRotate(node);
// LR 케이스
if (balance > 1 && key > node.left.key) {
node.left = leftRotate(node.left);
return rightRotate(node);
}
// RL 케이스
if (balance < -1 && key < node.right.key) {
node.right = rightRotate(node.right);
return leftRotate(node);
}
return node;
}
// 노드를 전위 순회하며 출력 (테스트 용도)
void preOrder(Node node) {
if (node != null) {
System.out.print(node.key + " ");
preOrder(node.left);
preOrder(node.right);
}
}
public static void main(String[] args) {
AVLTree tree = new AVLTree();
tree.root = tree.insert(tree.root, 10);
tree.root = tree.insert(tree.root, 20);
tree.root = tree.insert(tree.root, 30);
tree.root = tree.insert(tree.root, 40);
tree.root = tree.insert(tree.root, 50);
tree.root = tree.insert(tree.root, 25);
System.out.println("Preorder traversal of constructed AVL tree is:");
tree.preOrder(tree.root); // 30 20 10 25 40 50
}
}
6개
의 노드가 삽입되며, 불균형이 발생할 때마다 적절한 회전 연산이 수행됩니다.이번에는 AVL 트리에서 노드를 삭제하는 방법을 살펴보겠습니다.
class AVLTree {
// ... 이전 코드 생략 ...
// 삭제 연산
Node deleteNode(Node root, int key) {
// 기본적인 이진 검색 트리 삭제 연산
if (root == null)
return root;
if (key < root.key)
root.left = deleteNode(root.left, key);
else if (key > root.key)
root.right = deleteNode(root.right, key);
else {
// 삭제할 노드 찾음
if ((root.left == null) || (root.right == null)) {
Node temp = null;
if (temp == root.left)
temp = root.right;
else
temp = root.left;
if (temp == null) {
temp = root;
root = null;
} else
root = temp;
} else {
Node temp = minValueNode(root.right);
root.key = temp.key;
root.right = deleteNode(root.right, temp.key);
}
}
// 트리가 한 노드도 없는 경우
if (root == null)
return root;
// 높이 갱신
root.height = Math.max(height(root.left), height(root.right)) + 1;
// 균형 확인
int balance = getBalance(root);
// LL 케이스
if (balance > 1 && getBalance(root.left) >= 0)
return rightRotate(root);
// LR 케이스
if (balance > 1 && getBalance(root.left) < 0) {
root.left = leftRotate(root.left);
return rightRotate(root);
}
// RR 케이스
if (balance < -1 && getBalance(root.right) <= 0)
return leftRotate(root);
// RL 케이스
if (balance < -1 && getBalance(root.right) > 0) {
root.right = rightRotate(root.right);
return leftRotate(root);
}
return root;
}
// 최소값을 찾는 함수 (오른쪽 서브트리에서 가장 작은 값)
Node minValueNode(Node node) {
Node current = node;
while (current.left != null)
current = current.left;
return current;
}
}
이 코드는 노드 삭제 후 AVL 트리의 균형을 맞추기 위해 회전 연산을 수행합니다.
이번에는 Python을 사용하여 동일한 AVL 트리 구현을 해보겠습니다. 기본적인 구조와 논리는 Java와 동일하지만 Python 문법에 맞춰 구현합니다.
class Node:
def __init__(self, key):
self.key = key
self.left = None
self.right = None
self.height = 1
AVL 트리 클래스는 삽입과 회전, 그리고 균형 인수를 계산하는 메서드로 구성됩니다.
class AVLTree:
# 노드 높이 반환
def height(self, root):
if not root:
return 0
return root.height
# 균형 인수 반환
def get_balance(self, root):
if not root:
return 0
return self.height(root.left) - self.height(root.right)
# 오른쪽 회전
def right_rotate(self, y):
x = y.left
T2 = x.right
x.right = y
y.left = T2
y.height = 1 + max(self.height(y.left), self.height(y.right))
x.height = 1 + max(self.height(x.left), self.height(x.right))
return x
# 왼쪽 회전
def left_rotate(self, x):
y = x.right
T2 = y.left
y.left = x
x.right = T2
x.height = 1 + max(self.height(x.left), self.height(x.right))
y.height = 1 + max(self.height(y.left), self.height(y.right))
return y
# 삽입 연산
def insert(self, root, key):
if not root:
return Node(key)
if key < root.key:
root.left = self.insert(root.left, key)
elif key > root.key:
root.right = self.insert(root.right, key)
else:
return root
root.height = 1 + max(self.height(root.left), self.height(root.right))
balance = self.get_balance(root)
# LL 케이스
if balance > 1 and key < root.left.key:
return self.right_rotate(root)
# RR 케이스
if balance < -1 and key > root.right.key:
return self.left_rotate(root)
# LR 케이스
if balance > 1 and key > root.left.key:
root.left = self.left_rotate(root.left)
return self.right_rotate(root)
# RL 케이스
if balance < -1 and key < root.right.key:
root.right = self.right_rotate(root.right)
return self.left_rotate(root)
return root
# 전위 순회 출력
def pre_order(self, root):
if not root:
return
print("{0} ".format(root.key), end="")
self.pre_order(root.left)
self.pre_order(root.right)
# 실행 테스트
tree = AVLTree()
root = None
root = tree.insert(root, 10)
root = tree.insert(root, 20)
root = tree.insert(root, 30)
root = tree.insert(root, 40)
root = tree.insert(root, 50)
root = tree.insert(root, 25)
print("Preorder traversal of the constructed AVL tree is:")
tree.pre_order(root) # 30 20 10 25 40 50
Python 코드는 Java 코드와 마찬가지로 노드 삽입과 회전 연산을 처리하며, 전위 순회로 트리의 결과를 출력합니다. 삽입 과정에서 적절한 회전 연산이 수행되어 트리의 균형을 유지합니다.
이번에는 Python으로 노드 삭제 연산을 구현해 보겠습니다.
class AVLTree:
# 이전 코드 생략
# 삭제 연산
def delete(self, root, key):
if not root:
return root
if key < root.key:
root.left = self.delete(root.left, key)
elif key > root.key:
root.right = self.delete(root.right, key)
else:
if root.left is None:
temp = root.right
root = None
return temp
elif root.right is None:
temp = root.left
root = None
return temp
temp = self.get_min_value_node(root.right)
root.key = temp.key
root.right = self.delete(root.right, temp.key)
if root is None:
return root
root.height = 1 + max(self.height(root.left), self.height(root.right))
balance = self.get_balance(root)
# LL 케이스
if balance > 1 and self.get_balance(root.left) >= 0:
return self.right_rotate(root)
# LR 케이스
if balance > 1 and self.get_balance(root.left) < 0:
root.left = self.left_rotate(root.left)
return self.right_rotate(root)
# RR 케이스
if balance < -1 and self.get_balance(root.right) <= 0:
return self.left_rotate(root)
# RL 케이스
if balance < -1 and self.get_balance(root.right) > 0:
root.right = self.right_rotate(root.right)
return self.left_rotate(root)
return root
# 최소값 찾기 (오른쪽 서브트리에서)
def get_min_value_node(self, root):
if root is None or root.left is None:
return root
return self.get_min_value_node(root.left)
Python 코드 역시 노드 삭제 후 트리의 균형을 맞추기 위해 회전 연산을 수행하며, 기본적인 삭제 연산 후 균형 인수를 확인해 불균형이 발생한 경우 적절한 회전 연산을 적용합니다.
이번 포스팅에서는 AVL 트리의 기본 개념부터 시작해, 삽입 및 삭제 시 발생하는 불균형을 회전 연산을 통해 해결하는 방식을 살펴보았습니다. 또한, Java와 Python을 통해 AVL 트리의 주요 연산을 구현하는 방법도 다루었습니다.
핵심 요약:
AVL 트리의 장점:
AVL 트리의 단점:
실제 활용:
다음 포스팅에서는 또 다른 대표적인 균형 이진 탐색 트리인 레드-블랙 트리(Red-Black Tree)에 대해 다룰 예정입니다.