균형 이진트리(AVL/RB)
이진 탐색 트리(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)입니다.
균형 트리의 배경
AVL 트리는 1962년 소련의 수학자 아델손-벨스키(Adelson-Velsky)와 란디스(Landis)가 발표한 자가 균형 이진 탐색 트리이고, 두 사람 성의 첫 글자를 따 이름이 붙었습니다. 레드-블랙 트리는 1972년 루돌프 바이어(Rudolf Bayer)가 대칭 이진 B-트리로 제안했고, 1978년 귀바스(Guibas)와 세지윅(Sedgewick)이 빨강과 검정 색을 쓰는 지금의 형태로 정리했습니다. 이후 교재(CLRS 등)와 여러 언어의 표준 라이브러리에 널리 채택되었습니다.
숫자로 확인하기
자주 하는 오해
면접에서 이렇게 물어요
면접 질문
이진 탐색 트리가 왜 균형이 필요한가요?
정렬된 입력처럼 한쪽으로 치우친 삽입이 이어지면 높이가 n이 되어 탐색, 삽입, 삭제가 O(n)이 됩니다. 균형 트리는 높이를 O(log n)으로 유지해 모든 연산을 O(log n)으로 보장합니다.
면접 질문
AVL 트리와 레드-블랙 트리의 차이는?
AVL은 모든 노드의 높이 차이를 1 이하로 엄격히 유지해 트리가 낮아 탐색에 유리하지만, 일반적으로 삽입과 삭제에 회전이 더 필요합니다. 레드-블랙은 색 규칙으로 느슨하게 맞춰 회전이 적어 변경이 많은 곳에 유리한 경향이 있습니다(워크로드에 따라 다름).
면접 질문
LR 불균형은 어떻게 해결하나요?
왼쪽 자식의 오른쪽 서브트리에 삽입되어 생긴 불균형입니다. 먼저 왼쪽 자식을 기준으로 왼쪽 회전해 LL 모양으로 만들고, 불균형 노드를 기준으로 오른쪽 회전합니다.
면접 질문
언제 해시 테이블 대신 균형 트리를 쓰나요?
키를 정렬된 순서로 순회하거나 범위 검색, 가장 가까운 키 찾기가 필요할 때입니다. 해시 테이블은 평균 O(1)이지만 순서 정보를 가지지 않습니다.
실제 시스템에서는
함께 보면 좋은 토픽
참고 자료
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 트리가 유지하는 균형 조건은?
Q2
AVL 트리에서 LL 불균형(왼쪽 자식의 왼쪽 서브트리에 삽입되어 생긴 불균형)을 해결하는 회전은?
Q3
레드-블랙 트리가 AVL 트리보다 삽입·삭제에 유리하다고 평가되는 이유는?