위 그림처럼, key값이 오름차순 정렬된 순서로 BST를 구성해봅시다. 이 트리는 삽입, 삭제, 탐색이 의 시간복잡도를 가지게 됩니다. 어떻게 하면 이진 탐색 트리가 의 시간복잡도를 보장하게 할 수 있을까요?
아이디어는 간단합니다. 트리의 균형이 깨질 수 있는 삽입 혹은 삭제가 일어날 때마다, 왼쪽 서브 트리(left subtree)와 오른쪽 서브 트리(right subtree)가 같은 깊이(depth)를 갖도록 조정하면 됩니다. 이러한 트리를 AVL 트리
라고 합니다. 그럼 구현은 어떻게 해야 할까요?
Balance factor
라는 용어로 이를 설명해보겠습니다. Balance factor
는 (왼쪽 서브트리의 높이) - (오른쪽 서브트리의 높이)
입니다. 그렇다면 Balance factor
는 -1
또는 0
또는 1
이어야 합니다.
정리
- Balance factor should be -1 or 0 or 1
- Balance factor = height of left subtree - height of right subtree
위 그림을 C가 삽입된 직후라고 가정해 봅시다. C는 자식이 없으므로, balance factor
는 0입니다. 따라서 정상적으로 리턴하고 B로 올라갑니다. B의 balance factor
는 -1이며, 정상적으로 리턴하고 A로 올라갑니다. A의 balance factor
는 -2이므로, 불균형이 탐지되었습니다. 오른쪽 subtree가 더 무겁기 때문에, 균형을 맞춰줘야 합니다.
B를 새로운 루트로 하고, A를 B의 왼쪽 자식으로 보내면, BST property
를 유지하면서 balance
를 맞출 수 있습니다. B가 이미 왼쪽 자식을 갖고 있었다면 어떻게 할까요? B의 왼쪽 자식의 key값은 A의 key값보다 크기 때문에, A의 오른쪽 자식으로 보내면 됩니다. 이러한 과정을 left rotation
이라 하고, 다음과 같이 정리할 수 있습니다.
B
를 새로운 루트로 선택A
를 B
의 왼쪽 자식으로 바꾸기B
가 이미 왼쪽 자식이 있다면, 그 자식을 A
의 오른쪽 자식으로 지정balance factor
업데이트
위 그림에서는 A가 삽입된 직후라고 가정해 봅시다. A가 없을 때에는, 모든 노드가 balance
를 이루고 있습니다. A가 삽입된 이후, A는 자식이 없으므로 balance factor
는 0이며 정상적으로 리턴하고 B로 올라갑니다. B의 balance factor
는 1이며 리턴하고 C로 갑니다. C의 balance factor
도 1이며 리턴하고 E로 갑니다. E의 balance factor
는 2이므로, 불균형이 탐지되었습니다.
C를 새로운 루트로 하고 E를 C의 오른쪽 자식으로 보내면, BST property
를 유지하면서 balance
를 맞출 수 있습니다. D의 키값은 어차피 E의 키값보다 작으므로, E의 왼쪽 자식으로 보내면 됩니다. 이 과정을 right rotation
이라고 하며, 다음과 같이 정리할 수 있습니다.
C
를 새로운 루트로 선택E
를 C
의 오른쪽 자식으로 바꾸기C
가 이미 오른쪽 자식이 있으면, 그 자식을 E
의 왼쪽 자식으로 지정balance factor
업데이트left rotation
혹은 right rotation
은 매번 balance factor
를 업데이트합니다. balance factor
는 어떻게 업데이트 할까요?
left rotation
을 수행하고 마지막에 balance factor
를 업데이트하는 상황을 가정해봅시다.
위 그림에서, A, C, E는 balance factor
가 바뀌지 않습니다. B와 D만 바뀌며, 이 둘을 업데이트해야 합니다.
를 노드 의 높이라고 하겠습니다. 그럼 다음과 같이 표현할 수 있습니다.
그런데 는 서브 트리의 높이 중 큰 값보다 1큽니다. 즉, 입니다.
이렇게 하면 B의 새로운 balance factor
는 다음과 같이 balance factor
만으로 표현할 수 있습니다.
따라서,
마찬가지로 D에 대해서도 위와 비슷한 과정을 수행하면 다음과 같습니다.
balance factor
를 업데이트하는 과정은 다음의 코드로 표현할 수 있습니다.
rotRoot.balanceFactor = rotRoot.balanceFactor + 1 - min(0, newRoot.balanceFactor);
newRoot.balanceFactor = newRoot.balanceFactor + 1 + max(0, rotRoot.balanceFactor);
그런데, 이렇게만 하면 충분할까요? 다음 그림을 보시죠.
A의 balance factor
가 -2이므로 left rotation
을 수행합니다. C를 새로운 루트로 하고, C의 왼쪽 자식으로 A를 보냅니다. C에 이미 왼쪽 자식이 있으므로, 이 왼쪽 자식은 A의 오른쪽 자식이 됩니다. 그럼 위 그림처럼, balancing이 이루어지지 않게 됩니다.
C의 balance factor
가 2이므로, 다시 right rotation
을 수행하면 어떨까요? 결과는 원래대로 돌아와서 결국 balancing이 이루어지지 않게 됩니다.
이 문제를 해결하기 위해, 다음의 규칙을 따라야 합니다.
left rotation
을 할 경우
만약 오른쪽 자식의 balance factor
가 0보다 크다면, 오른쪽 자식에 대해 right rotation
을 수행하고, 루트에 대해 left rotation
한다
right rotation
을 할 경우
만약 왼쪽 자식의 balance factor
가 0보다 작다면, 왼쪽 자식에 대해 left rotaion
하고, 루트에 대해 right rotation
한다
위 그림은 A를 left rotation
하기 전에, C에 대해 right rotation
하는 결과를 보여줍니다.
Balanced Binary Search Tree는 삽입 혹은 삭제가 이루어질 때, Balancing 작업을 통해 항상 왼쪽 서브트리와 오른쪽 서브트리의 높이 차이를 일정하게 유지하여 트리의 시간복잡도를 으로 보장합니다.
이때, Balancing 작업(회전 연산)의 시간복잡도는 입니다.
Balanced BST는 C++의 std::map
(TreeMap)과 같은 Ordered Map
자료구조의 구현에 사용됩니다.
https://runestone.academy/ns/books/published/pythonds/Trees/AVLTreeImplementation.html