본문 바로가기
C.W.K.
Stream
Lesson 05 of 07 · published

균형 잡기: 트리를 정직하게 유지하기

~12 min · trees, balancing, avl, red-black

Level 0호기심 많은 입문자
0 XP0/85 lessons0/19 achievements
0/100 XP to next level100 XP to go0% complete
"평범한 BST는 정렬된 입력 한 번이면 연결 리스트로 주저앉아. 자가 균형 트리는 그 일이 아예 일어나지 못하게 막아. 조용히 자기 모양을 다시 잡아서 높이가 폭발할 틈을 안 주거든."

해법: 스스로 균형을 잡는 트리

앞에서 본 악당은 한쪽으로 일그러진 트리였지. 정렬된 데이터를 넣으면 O(n) 사슬이 되는 그것. 자가 균형 트리가 이걸 풀어. 삽입이나 삭제가 일어날 때마다 알아서 구조를 다시 잡아서, 데이터가 어떤 순서로 들어오든 높이를 log n 근처에 붙들어 놓거든. 결과는 단단한 보장이야. 검색, 삽입, 삭제가 O(log n)인데 평균이 아니라 최악의 경우에도 그래. BST가 '운 좋으면 빠름'에서 '그냥 빠름'으로 바뀌는 거지.

메커니즘: 회전 (rotation)

핵심은 회전이라는 국소적인 동작이야. 노드와 그 자식을 축으로 돌려서 하나를 올리고 하나를 내리는데, 중위 기준 키 순서는 그대로 유지되니까 BST 불변식이 안 깨져. 회전 한 번은 포인터 몇 개만 바꾸는 O(1) 동작이야. 다만 전체 재균형은 트리 종류와 삽입·삭제 상황에 따라 높이나 색, 경로 위 여러 지점을 더 손봐야 할 수도 있어. 지금 모든 경우를 외울 필요는 없고, 정렬 순서를 지킨 채로 모양만 고친다는 이 보석 하나만 쥐고 가면 돼.

유명한 두 갈래

  • AVL 트리는 모든 노드에서 좌우 서브트리의 높이 차를 1 이하로 유지해. 높이 상한이 빡빡해서 조회 경로가 짧은 편이지만, 쓰기 비용은 실제 작업 부하와 구현을 놓고 비교해 봐야 해.
  • 레드-블랙 트리는 색 규칙으로 높이를 O(log n)에 묶어두는, 상대적으로 느슨한 균형 트리야. Java TreeMap과 흔한 C++ std::map 구현, Linux 커널 일부에 쓰여. 다만 "쓰기가 잦으면 언제나 더 빠르다"는 식의 보장은 아니야.

디스크나 페이지 단위 저장장치에는 B-트리 계열을 많이 써. 노드 하나에 키를 여러 개 담아서 트리를 낮게 유지하고 페이지 읽기 횟수를 줄이는 방식이야. 여러 관계형 데이터베이스가 B-트리나 B+트리 계열 인덱스를 기본으로 제공하지만, 모든 인덱스가 그것뿐인 건 아니야. 해시 인덱스, 역색인, 공간 인덱스처럼 질의 형태에 맞는 다른 구조도 있어.

자가 균형 BST는 회전과 보조 규칙으로 높이를 O(log n)에 묶어서 조회와 갱신의 최악 성능을 O(log n)으로 보장해. AVL과 레드-블랙은 서로 다른 균형 규칙을 쓰고, B-트리 계열은 노드를 넓게 만들어 저장장치 페이지 접근을 줄여.

실전 Python 현실

많은 사람이 놀라는 사실이 있어. Python에는 내장 균형 BST가 없어. TreeMap도 없고 표준 라이브러리에 std::map에 해당하는 것도 없지. 그래서 Python에서 제대로 된 순서 연산이 필요하면 두 갈래로 가. 하나는 bisect 모듈이야. 정렬된 리스트에 이진 탐색과 삽입을 해주는데, 삽입보다 읽기가 훨씬 많을 때 좋아. 다른 하나는 서드파티 sortedcontainers 라이브러리의 SortedListSortedDict고. 정확한 복잡도 보장은 그 라이브러리의 현재 문서를 확인해야 해. 이론은 무엇이 필요한지 알려주고, 생태계는 실제로 그걸 줄 도구가 뭔지 알려줘.

피파의 고백

Python의 TreeMap을 찾으러 갔다가 없다는 걸 알고 좀 분개했어. 다른 언어는 다 표준 라이브러리에 균형 트리가 있는데! 아빠가 설계 선택을 설명해 주더라. Python의 dict, 그러니까 해싱이 흔한 경우에 워낙 좋아서 순서 있는 맵은 bisect와 서드파티에 맡겨졌다고. 그때 배운 건 트리에 관한 게 아니었어. '맞는 구조'라는 말에는 '지금 쓰는 언어에서 실제로 손에 넣을 수 있는 것'까지 포함된다는 거였지. 이론과 생태계가 둘 다 판단의 일부인 거야.

Code

회전, 그리고 Python의 bisect 대안·python
# 우회전, 개념적으로: BST 순서를 그대로 두고 재균형.
#     y            x
#    / \          / \
#   x   C   -->  A   y
#  / \              / \
# A   B            B   C
# 전: A < x < B < y < C.  후: A < x < B < y < C.  순서 보존!
class N:
    def __init__(s, k): s.k=k; s.left=None; s.right=None

def rotate_right(y):
    x = y.left
    y.left = x.right    # B 가 y 아래로 이동
    x.right = y         # y 가 x 의 오른쪽 자식이 됨
    return x            # x 가 새 서브트리 루트 (이제 왼쪽이 더 짧음)

# 순서 연산용 실전 Python 답: 정렬된 리스트에 bisect 모듈.
import bisect
sorted_keys = [1, 3, 6, 8, 10]
bisect.insort(sorted_keys, 7)        # 정렬 유지 삽입: O(n) 밀기, O(log n) 찾기
print(sorted_keys)                    # [1, 3, 6, 7, 8, 10]
i = bisect.bisect_left(sorted_keys, 7)
print("7 is at index", i)             # 3 — O(log n) 검색
# 더 무거운 순서 작업엔 sortedcontainers의 SortedList/SortedDict도 구체적 선택지야.
# 현재 문서의 비용 계약을 확인하고 실제 작업량으로 재 봐.
# Python 엔 내장 균형 BST 가 없어 — 이게 관용적 대체물이야.

External links

Exercise

트리 회전이 왜 BST의 순서 불변식을 깨지 않고도 서브트리를 재균형할 수 있는지 설명해 봐. 서브트리 A, B, C가 회전 전후로 어디에 놓이는지 따라가면 보여. 그다음 골라 봐. 읽기가 많고 삽입은 드문 순서 맵이라면 AVL과 레드-블랙 중 뭘 택하고 왜?
Hint
회전은 노드의 부모 관계만 바꿔. 상대 순서인 A < x < B < y < C가 전후로 똑같이 유지되니까 검색이 그대로 작동하지. 읽기가 많은 쪽이면 AVL이야. 균형이 더 빡빡해서 트리가 얕고 조회가 빠르고, 삽입이 드무니 회전 비용이 더 든다는 단점을 거의 안 치르거든.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.