RBT는 균형을 유지하는 이진 탐색 트리입니다. 이는 트리의 높이가 O(log n)이 되도록 보장하며, 이로 인해 탐색, 삽입, 삭제 연산의 평균 시간 복잡도가 O(log n)이 됩니다.
RBT는 이진 탐색 트리의 기본 속성을 BST와 동일하게 모두 가지고 있으나, 추가적으로 아래의 속성을 가지고 있다.
RBT 를 구현하기 위해 앞서 구현했던 BST 를 상속받은 이후, 추가적으로 Red-Black Tree 속성을 유지하기 위해 색상 속성과 색상에 따른 조정 메커니즘을 추가해주어야 합니다.
주요 변경사항은 아래와 같습니다.
rb_insert()
: BST의 노드 삽입 함수를 수정하여 삽입된 노드의 색깔을 빨간색으로 설정하고, 이후에 Red-Black Tree 속성을 유지하기 위해 rb_insert_fixup()
함수를 호출합니다.left_rotate()
, right_rotate()
: RBT의 속성을 유지하기 위해 회전 연산 함수를 추가하여 Red-Black Tree 속성을 만족하도록 합니다.rb_insert_fixup()
: 새로운 노드를 삽입한 후에 Red-Black Tree 속성을 유지하기 위한 함수입니다.RBT code
class Node:
def __init__(self, key=None, nil=None):
self.key = key
self.left = nil
self.right = nil
self.parent = nil
self.color = "RED"
class RBT(BST):
def __init__(self):
super().__init__()
self.nil = Node(None)
self.nil.color = "BLACK"
self.root = self.nil
def left_rotate(self, x):
y = x.right # y를 설정
x.right = y.left # y의 왼쪽 서브트리를 x의 오른쪽 서브트리로 변환
if y.left != self.nil:
y.left.parent = x
y.parent = x.parent # x의 부모를 y로 연결
if x.parent == self.nil:
self.root = y
elif x == x.parent.left:
x.parent.left = y
else:
x.parent.right = y
y.left = x # x를 y의 왼쪽으로 놓음
x.parent = y
def right_rotate(self, y):
x = y.left # x를 설정
y.left = x.right # x의 오른쪽 서브트리를 y의 왼쪽 서브트리로 변환
if x.right != self.nil:
x.right.parent = y
x.parent = y.parent # y의 부모를 x로 연결
if y.parent == self.nil:
self.root = x
elif y == y.parent.right:
y.parent.right = x
else:
y.parent.left = x
x.right = y # y를 x의 오른쪽으로 놓음
y.parent = x
def rb_insert(self, z):
y = self.nil
x = self.root
while x != self.nil:
y = x
if z.key < x.key:
x = x.left
else:
x = x.right
z.parent = y
if y == self.nil:
self.root = z
elif z.key < y.key:
y.left = z
else:
y.right = z
z.left = self.nil
z.right = self.nil
z.color = "RED"
self.rb_insert_fixup(z)
def rb_insert_fixup(self, z):
while z.parent.color == "RED":
if z.parent == z.parent.parent.left: # z의 부모가 왼쪽 자식인 경우
y = z.parent.parent.right # y는 삼촌 노드
if y.color == "RED": # Case 1
z.parent.color = "BLACK"
y.color = "BLACK"
z.parent.parent.color = "RED"
z = z.parent.parent
else:
if z == z.parent.right:
z = z.parent
self.left_rotate(z)
z.parent.color = "BLACK"
z.parent.parent.color = "RED"
self.right_rotate(z.parent.parent)
else: # z의 부모가 오른쪽 자식인 경우
y = z.parent.parent.left # y는 삼촌 노드
if y.color == "RED":
z.parent.color = "BLACK"
y.color = "BLACK"
z.parent.parent.color = "RED"
z = z.parent.parent
else:
if z == z.parent.left:
z = z.parent
self.right_rotate(z)
z.parent.color = "BLACK"
z.parent.parent.color = "RED"
self.left_rotate(z.parent.parent)
self.root.color = "BLACK"
def tree_insert(self, key):
node = Node(key)
self.rb_insert(node)
def inorder_tree_walk(self, x):
if x != self.nil:
self.inorder_tree_walk(x.left)
print(x.key, end=' ')
self.inorder_tree_walk(x.right)
nil
노드를 나타내는 속성과 노드의 색상을 저장하는 속성이 추가되었습니다.class Node:
def __init__(self, key=None, nil=None):
self.key = key
self.left = nil
self.right = nil
self.parent = nil
self.color = "RED"
앞에서 구현했던 BST 클래스에 기본적인 동작이 구현되어 있으므로, BST 를 상속받은 이후, Red-Black Tree 속성을 만족시켜주는 메서드를 추가하여 RBT 클래스를 구현하였습니다.
RBT를 구현하기 위해 추가하고 수정한 메서드와 코드에 대해 설명해보겠습니다.
__init__()
def __init__(self):
super().__init__()
self.nil = Node(None)
self.nil.color = "BLACK"
self.root = self.nil
tree_insert()
rb_insert_fixup
메서드를 호출하여 레드블랙 트리의 속성을 유지합니다. def tree_insert(self, key):
node = Node(key)
self.rb_insert(node)
rb_insert_fixup()
Red-Black Tree의 속성을 유지하면서 노드를 삽입한 후에 발생할 수 있는 위반된 상황을 해결하는 메서드입니다.
이 메서드는 레드블랙 트리의 속성을 보존하기 위해 부모와 삼촌 노드의 색깔을 확인하고, 필요에 의해 회전 연산을 수행합니다.
함수의 작동방식은 아래 세가지 케이스에 따라 나누어 작동합니다.
Case 1: Uncle y is red
Case 2: Uncle y is black and z is a right child
Case 3: Uncle y is black and z is a left child
def rb_insert_fixup(self, z):
while z.parent.color == "RED": # z의 부모가 빨간색인 동안 반복
if z.parent == z.parent.parent.left: # z의 부모가 왼쪽 자식인 경우
y = z.parent.parent.right # y는 삼촌 노드
if y.color == "RED": # Case 1: 삼촌 노드가 빨간색인 경우
z.parent.color = "BLACK" # 부모 노드를 검은색으로 변경
y.color = "BLACK" # 삼촌 노드를 검은색으로 변경
z.parent.parent.color = "RED" # 조부모 노드를 빨간색으로 변경
z = z.parent.parent # z를 조부모 노드로 이동
else:
if z == z.parent.right: # Case 2: z가 부모의 오른쪽 자식인 경우
z = z.parent # z를 부모로 이동
self.left_rotate(z) # 부모를 기준으로 왼쪽 회전
z.parent.color = "BLACK" # Case 3: 부모를 검은색으로 변경
z.parent.parent.color = "RED" # 조부모를 빨간색으로 변경
self.right_rotate(z.parent.parent) # 조부모를 기준으로 오른쪽 회전
else: # z의 부모가 오른쪽 자식인 경우
y = z.parent.parent.left # y는 삼촌 노드
if y.color == "RED": # Case 4: 삼촌 노드가 빨간색인 경우
z.parent.color = "BLACK" # 부모 노드를 검은색으로 변경
y.color = "BLACK" # 삼촌 노드를 검은색으로 변경
z.parent.parent.color = "RED" # 조부모 노드를 빨간색으로 변경
z = z.parent.parent # z를 조부모 노드로 이동
else:
if z == z.parent.left: # Case 5: z가 부모의 왼쪽 자식인 경우
z = z.parent # z를 부모로 이동
self.right_rotate(z) # 부모를 기준으로 오른쪽 회전
z.parent.color = "BLACK" # Case 6: 부모를 검은색으로 변경
z.parent.parent.color = "RED" # 조부모를 빨간색으로 변경
self.left_rotate(z.parent.parent) # 조부모를 기준으로 왼쪽 회전
self.root.color = "BLACK" # 루트를 검은색으로 변경
left_rotate()
, right_rotate()
left_rotate() 의 작동 원리
왼쪽 회전은 한 노드를 기준으로 그 노드의 오른쪽 자식을 새로운 루트로 하여 회전하는 연산입니다.
이 과정에서 해당 노드의 오른쪽 자식을 가져와서 새로운 루트로 설정하며, 이 때 오른쪽 자식의 왼쪽 서브트리는 원래 부모 노드의 오른쪽 서브트리로 이동합니다.
따라서 회전 후에는 회전하는 노드의 오른쪽 자식이 새로운 루트가 되고, 회전하는 노드는 해당 자식의 왼쪽 자식으로 들어가게 됩니다.
이로써 회전한 노드와 그 자식 노드들 사이의 관계가 변경되며, 이에 따라 부모 및 자식 포인터가 새로 설정됩니다.
left_rotate() code
def left_rotate(self, x):
y = x.right # y를 설정
x.right = y.left # y의 왼쪽 서브트리를 x의 오른쪽 서브트리로 변환
if y.left != self.nil:
y.left.parent = x
y.parent = x.parent # x의 부모를 y로 연결
if x.parent == self.nil:
self.root = y
elif x == x.parent.left:
x.parent.left = y
else:
x.parent.right = y
y.left = x # x를 y의 왼쪽으로 놓음
x.parent = y
def right_rotate(self, y):
x = y.left # x를 설정
y.left = x.right # x의 오른쪽 서브트리를 y의 왼쪽 서브트리로 변환
if x.right != self.nil:
x.right.parent = y
x.parent = y.parent # y의 부모를 x로 연결
if y.parent == self.nil:
self.root = x
elif y == y.parent.right:
y.parent.right = x
else:
y.parent.left = x
x.right = y # y를 x의 오른쪽으로 놓음
y.parent = x
print_tree()
, _print_tree_recursive
실습을 위해 tree를 출력하기 위한 함수를 아래와 같이 정의했습니다.
def print_tree(self):
self._print_tree_recursive(self.root, 0)
def _print_tree_recursive(self, node, level):
if node == self.nil:
return
self._print_tree_recursive(node.right, level + 1)
print(' ' * level + str(node.key) + f" ({node.color})")
self._print_tree_recursive(node.left, level + 1)
RBT insertion Test를 해보겠습니다.
코드와 결과는 아래와 같습니다.
def test_rbt():
# 빈 Red-Black Tree 생성
rbt = RBT()
# Case 1: Uncle y is red
print("RBT Insert Test ")
rbt.tree_insert(8)
rbt.tree_insert(4)
rbt.tree_insert(20)
rbt.tree_insert(6)
rbt.tree_insert(10)
rbt.tree_insert(25)
rbt.tree_insert(8)
rbt.tree_insert(12)
rbt.tree_insert(37)
print()
rbt.print_tree()
print()
rbt.tree_insert(15)
print(" After inserting nodes 15 :")
print()
rbt.print_tree()
print()
test_rbt()
이제 과정을 설명해보겠습니다.
RBT성질을 만족하며 아래와 같이 Red-Black Tree가 완성된것을 확인할 수 있습니다.
Tree에 15를 Insert 해주었습니다. 새로운 노드를 삽입할 때, 트리의 속성을 최대한 유지하면서 삽입을 간단하게 만들기 위해 빨간색으로 넣어주었습니다. 12의 Right child으로 삽입되어 빨간색 노드가 연속하여 나와 Red-black Tree의 속성 4를 위배하는것을 볼 수 있습니다.
RBT의 속성을 만족시키기 위해 12를 Black, 10을 Red으로 Recolor 했습니다. 아래와 같이 20, 10 에서 빨간색 노드가 연속하여 나와 Red-black Tree의 속성 4가 위배된 것 확인할 수 있습니다.
Red - Black Tree의 속성을 만족시키기 위해 Right-Rotate(10) 을 해주었습니다.
RBT의 속성을 만족시키기 위해 left Rotate(10) 한 이후, recolor해주었습니다. 그 결과, 10을 root로 하는 red-black Tree가 완성된것을 확인할 수 있습니다.
레드블랙트리에서 노드를 삭제하고 난 후, 삭제된 노드의 자리에 이식된 노드를 x, w를 x의 형제노드라고 할 때, 발생할 주요 케이스들이 존재합니다.
다음은 삭제 과정에서 발생할 수 있는 주요 케이스들입니다.
w
is redw
를 검은색으로 바꾼다.x
의 부모 p[x]
를 빨간색으로 바꾼다.p[x]
를 기준으로 좌회전을 수행한다.w
가 검은색이 되고, 새 w
는 x
의 새 형제 노드가 된다. 그 다음은 새로운 w
가 검은색인 경우를 처리하는 나머지 케이스로 넘어간다.w
is black, and both w
's children are blackw
를 빨간색으로 바꾼다.x
의 부모 p[x]
가 검은색이라면 x
를 p[x]
로 설정하여 반복한다. (다음 반복에서 새로운 x
의 형제 w
를 검사한다.)p[x]
가 빨간색이라면 p[x]
를 검은색으로 바꿔서 균형을 맞춘다.w
is black, w
's left child is red, and w
's right child is blackw
의 왼쪽 자식을 검은색으로 바꾼다.w
를 빨간색으로 바꾼다.w
를 기준으로 우회전을 수행한다.w
의 새로운 형제 노드 w
가 검은색이고, w
의 오른쪽 자식이 빨간색인 경우로 전환된다. 이때는 다음 케이스를 처리한다.w
is black, w
's right child is redw
의 색을 p[x]
의 색으로 바꾼다.p[x]
를 검은색으로 바꾼다.w
의 오른쪽 자식을 검은색으로 바꾼다.p[x]
를 기준으로 좌회전을 수행한다.