트리(Tree)도 루트(Root) 노드에서 시작해서 여러 개의 하위 노드로 이어집니다. 이런 구조는 데이터를 계층적이고 효율적으로 관리할 수 있습니다.
이진 트리는 각 노드가 최대 두 개의 자식 노드를 가지는 트리입니다. 이진 트리에는 여러 하위 유형이 있습니다.
이진 검색 트리는 이진 트리의 한 유형으로, 왼쪽 하위 트리의 모든 노드가 부모 노드보다 작고, 오른쪽 하위 트리의 모든 노드가 부모 노드보다 큽니다.
# 트리 노드 클래스 정의
class TreeNode:
def __init__(self, data):
self.data = data # 현재 노드의 데이터
self.children = [] # 자식 노드들을 저장하기 위한 리스트
def add_child(self, child_node):
self.children.append(child_node)
# 이진 트리 노드 클래스 정의
class BinaryTreeNode:
def __init__(self, data):
self.data = data
self.left = None # 왼쪽 자식 노드
self.right = None # 오른쪽 자식 노드
# 이진 검색 트리 클래스 정의
class BinarySearchTree:
def __init__(self):
self.root = None # 루트 노드
# 이진 검색 트리에 데이터 삽입
def insert(self, data):
new_node = BinaryTreeNode(data)
if self.root is None:
self.root = new_node
else:
self._insert_recursive(self.root, new_node)
# 재귀적으로 데이터 삽입 수행
def _insert_recursive(self, current_node, new_node):
if new_node.data < current_node.data:
if current_node.left is None:
current_node.left = new_node
else:
self._insert_recursive(current_node.left, new_node)
else:
if current_node.right is None:
current_node.right = new_node
else:
self._insert_recursive(current_node.right, new_node)
# 이진 검색 트리에서 데이터 검색
def search(self, data):
return self._search_recursive(self.root, data)
# 재귀적으로 데이터 검색 수행
def _search_recursive(self, current_node, data):
if current_node is None:
return False
if data == current_node.data:
return True
elif data < current_node.data:
return self._search_recursive(current_node.left, data)
else:
return self._search_recursive(current_node.right, data)
# 이진 검색 트리 생성과 사용 예제
bst = BinarySearchTree()
bst.insert(10)
bst.insert(5)
bst.insert(15)
bst.insert(3)
bst.insert(7)
print(bst.search(10)) # True
print(bst.search(8)) # False
B-Tree와 B+ Tree는 데이터베이스와 파일 시스템에서 주로 사용됩니다. 노드가 여러 개의 자식을 가질 수 있어 디스크 접근을 최소화하는 데 유용합니다.
트리 순회(Tree Traversal)는 트리의 모든 노드를 방문하는 방법을 말합니다. 주요한 트리 순회 방법은 다음과 같습니다.
현재 노드를 먼저 방문한 후, 왼쪽 서브트리를 전위 순회하고, 오른쪽 서브트리를 전위 순회합니다.
왼쪽 서브트리를 중위 순회한 후, 현재 노드를 방문하고, 오른쪽 서브트리를 중위 순회합니다. 이 때, 이진 검색 트리를 중위 순회하면 정렬된 결과를 얻을 수 있습니다.
왼쪽 서브트리를 후위 순회한 후, 오른쪽 서브트리를 후위 순회하고, 마지막으로 현재 노드를 방문합니다.
자료의 검색, 삽입, 삭제와 같은 기본적인 연산을 효율적으로 수행하는 데 있어 중요한 자료구조입니다.
특히 평균적으로 이러한 연산의 시간 복잡도가 으로 매우 빠르다는 것이 큰 장점입니다. 그러나 이는 "평균적으로"라는 단어에 주목해야 합니다.
이진 검색 트리가 항상 이러한 효율성을 보장하는 것은 아닙니다. 트리의 형태가 불균형하게 될 경우, 이는 사실상 링크드 리스트와 다름없이 연산의 시간 복잡도가 에 이를 수 있습니다.
이러한 문제점은 특별한 경우에 발생할 수 있으나, 발생한다면 그 비효율성은 무시할 수 없습니다.
이러한 문제를 해결하기 위해 고안된 것이 바로 "균형 이진 검색 트리(Balanced Binary Search Tree)"입니다. 이는 여러 알고리즘과 메커니즘을 활용하여 트리의 균형을 자동으로 조정합니다.
AVL 트리, 레드-블랙 트리 등이 이에 속합니다. 이러한 균형 이진 검색 트리는 연산의 시간 복잡도를 최악의 경우에도 으로 유지하는 것을 보장합니다.
데이터의 양이 증가하면 증가할수록, 불균형이 발생할 확률도 늘어납니다. 이를 예방하고 안정적인 성능을 유지하기 위해 균형 이진 검색 트리는 필수적입니다. 데이터베이스에서 사용하는 B-tree도 균형 트리의 일종입니다.
AVL 트리, 레드-블랙 트리에서 어떻게 균형을 유지하는지 알고 있으면 도움이 많이 됩니다.
AVL 트리는 삽입이나 삭제 연산이 이루어진 직후에 트리의 균형을 유지하기 위한 특별한 “회전 연산”을 수행합니다. 이를 통해 모든 노드에 대해 왼쪽과 오른쪽 하위 트리의 높이 차이가 최대 1이 되도록 합니다.
AVL 트리인 상태에서 삽입이나 삭제 직후에 균형이 깨질 때 전혀 관계없는 두 곳 이상에서 균형이 깨지는 경우는 없습니다. 어떤 서브 트리에서 균형이 깨졌다면, 그 서브 트리를 포함하는 위쪽 서브 트리도 균형이 깨질 수 있습니다.
여러 서브 트리에서 균형이 깨진 경우, 균형이 깨진 서브 트리 중 가장 낮은 곳에 있는 트리부터 균형을 잡아주는 작업을 하면서 올라갑니다.
삽입 후 균형 맞추기
삭제 후 균형 맞추기
AVL 트리에서의 삽입과 삭제 연산은 기본적으로 시간에 수행됩니다. 이는 높이 균형을 유지하기 위해 회전 연산을 수행하는 데에 드는 시간이 이기 때문입니다. 따라서 AVL 트리는 최악의 경우에도 삽입, 삭제, 검색 연산이 모두 에 수행될 수 있는 자료구조입니다.
class Node:
def __init__(self, key):
self.key = key
self.height = 1
self.left = None
self.right = None
def getHeight(node):
if not node:
return 0
return node.height
def updateHeight(node):
if node:
node.height = 1 + max(getHeight(node.left), getHeight(node.right))
def getBalance(node):
if not node:
return 0
return getHeight(node.left) - getHeight(node.right)
# 단일 오른쪽 회전
def rotateRight(y):
x = y.left
T = x.right
x.right = y
y.left = T
updateHeight(y)
updateHeight(x)
return x
# 단일 왼쪽 회전
def rotateLeft(x):
y = x.right
T = y.left
y.left = x
x.right = T
updateHeight(x)
updateHeight(y)
return y
# 노드를 삽입하고 균형을 맞춘다
def insert(root, key):
if not root:
return Node(key)
if key < root.key:
root.left = insert(root.left, key)
elif key > root.key:
root.right = insert(root.right, key)
else:
return root # 중복 노드는 허용하지 않는다
updateHeight(root)
balance = getBalance(root)
# 균형이 깨진 경우 회전을 수행
# Left Heavy
if balance > 1:
if key < root.left.key:
return rotateRight(root) # 단일 오른쪽 회전
else:
root.left = rotateLeft(root.left) # 왼쪽 하위 트리를 왼쪽으로 회전
return rotateRight(root) # 단일 오른쪽 회전
# Right Heavy
if balance < -1:
if key > root.right.key:
return rotateLeft(root) # 단일 왼쪽 회전
else:
root.right = rotateRight(root.right) # 오른쪽 하위 트리를 오른쪽으로 회전
return rotateLeft(root) # 단일 왼쪽 회전
return root
# 트리를 순회하며 출력한다 (In-order Traversal)
def inOrder(root):
if not root:
return
inOrder(root.left)
print(root.key, end=' ')
inOrder(root.right)
def printTree(root, level=0, indent="----"):
if root is not None:
printTree(root.right, level + 1, " ")
print(indent * level + "-> " + str(root.key))
printTree(root.left, level + 1, " ")
# 예제 사용
root = None
keys = [30, 40, 50, 10, 20, 22, 24, 26, 14, 16, 18]
# 출력을 통해 균형이 잘 유지되고 있는지 확인한다
for key in keys:
root = insert(root, key)
print("After inserting {}: ".format(key))
printTree(root)
print("\n---")
출력 결과
After inserting 30:
-> 30
---
After inserting 40:
-> 40
-> 30
---
After inserting 50:
-> 50
-> 40
-> 30
---
After inserting 10:
-> 50
-> 40
-> 30
-> 10
---
After inserting 20:
-> 50
-> 40
-> 30
-> 20
-> 10
---
After inserting 22:
-> 50
-> 40
-> 30
-> 22
-> 20
-> 10
---
After inserting 24:
-> 50
-> 40
-> 30
-> 24
-> 22
-> 20
-> 10
---
After inserting 26:
-> 50
-> 40
-> 30
-> 26
-> 24
-> 22
-> 20
-> 10
---
After inserting 14:
-> 50
-> 40
-> 30
-> 26
-> 24
-> 22
-> 20
-> 14
-> 10
---
After inserting 16:
-> 50
-> 40
-> 30
-> 26
-> 24
-> 22
-> 20
-> 16
-> 14
-> 10
---
After inserting 18:
-> 50
-> 40
-> 30
-> 26
-> 24
-> 22
-> 20
-> 18
-> 16
-> 14
-> 10
---
AVL 트리는 높이 균형을 유지하기 위해 회전 연산을 수행하므로 삽입과 삭제 연산의 시간 복잡도는 입니다. 하지만 이러한 높이 균형을 유지하기 위한 추가적인 연산은 상대적으로 느립니다. 따라서 AVL 트리는 삽입과 삭제 연산이 빈번하게 일어나는 상황에서는 성능이 떨어질 수 있습니다.
또한, AVL 트리의 균형을 맞추기 위한 회전 연산은 비교적 복잡한 연산이므로 상수 시간 내에 수행하기 어렵습니다. 따라서 AVL 트리의 상수항이 더 크기 때문에 실제로는 효율적인 자료구조로 사용되기 어려울 수 있습니다.
또한 AVL 트리의 높이는 데이터의 삽입 순서에 따라 달라질 수 있습니다. 특정한 데이터를 삽입하는 순서에 따라 트리의 높이가 증가하면 검색 연산의 성능이 떨어질 수 있습니다. 이러한 경우를 방지하려면 데이터를 삽입하는 순서를 무작위화하거나, 다른 균형 이진 검색 트리 구조를 사용하는 것이 고려될 수 있습니다.
AVL 트리는 높이 균형을 유지하는 데에 강점을 가지고 있으므로 검색 연산의 수행 시간이 중요한 상황에서 사용됩니다. 주로 데이터베이스 시스템이나 파일 시스템 등에서 활용됩니다.
데이터베이스 시스템에서는 데이터의 빠른 검색과 삽입, 삭제 연산이 중요하므로 AVL 트리와 같은 균형 이진 검색 트리가 사용됩니다. 또한, 파일 시스템에서는 파일의 이름을 효율적으로 검색하기 위해 AVL 트리를 활용할 수 있습니다.
이 외에도 AVL 트리는 데이터베이스 인덱스 구조, 자동 완성 기능을 제공하는 검색 엔진, 심볼 테이블 등 다양한 분야에서 사용될 수 있습니다.
레드-블랙 트리는 다음과 같은 특성을 가집니다:
레드-블랙 트리는 이러한 특성을 유지하면서 삽입, 삭제 연산을 수행하며 균형을 유지합니다. 각각의 연산에서 레드-블랙 트리의 특성을 위반하지 않도록 색상을 조절하고, 회전 연산을 통해 균형을 맞춥니다. 이로써 모든 연산의 시간 복잡도는 이 보장됩니다.
AVL 트리와 레드-블랙 트리는 모두 균형 이진 검색 트리의 일종이지만, 다음과 같은 차이점이 있습니다:
따라서 데이터의 삽입 및 삭제 연산이 빈번하게 일어나는 상황에서는 레드-블랙 트리가 AVL 트리보다 성능이 우수하다고 볼 수 있습니다. 그러나 검색 연산의 경우 두 트리의 성능은 비슷합니다.
레드-블랙 트리는 데이터베이스, 파일 시스템, 네트워크 라우팅 등 다양한 분야에서 사용됩니다. 몇 가지 예시로는:
std::map
및 std::set
: C++ 표준 라이브러리에서 레드-블랙 트리가 기반으로 사용됩니다.레드-블랙 트리의 균형을 유지하는 능력과 삽입, 삭제 연산의 효율성으로 인해 다양한 응용 분야에서 사용되며, 효율적인 데이터 구조로 폭넓게 활용됩니다.
class Node:
# Node 생성자
def __init__(self, data, color, parent=None):
self.data = data # 노드의 값
self.color = color # 노드의 색상 (RED 또는 BLACK)
self.parent = parent # 부모 노드
self.left = None # 왼쪽 자식
self.right = None # 오른쪽 자식
class RedBlackTree:
# RedBlackTree 생성자
def __init__(self):
self.TNULL = Node(0, "BLACK") # NULL을 대체하는 블랙 노드
self.root = self.TNULL # 루트 초기화
# 전위 순회 함수
def pre_order_helper(self, node):
if node != self.TNULL:
print(node.data, end=" ")
self.pre_order_helper(node.left)
self.pre_order_helper(node.right)
# 노드 삽입 후 균형을 유지하는 함수
def balance_insert(self, k):
# 부모가 RED이면 균형을 맞춤
while k.parent.color == "RED":
# 부모가 할아버지의 오른쪽 자식일 때
if k.parent == k.parent.parent.right:
u = k.parent.parent.left # Uncle 노드
# Case 1: Uncle이 RED
if u.color == "RED":
u.color = "BLACK"
k.parent.color = "BLACK"
k.parent.parent.color = "RED"
k = k.parent.parent
else: # Case 2 or Case 3
# Case 2: 현재 노드가 왼쪽 자식일 때
if k == k.parent.left:
k = k.parent
self.rotate_right(k)
# Case 3
k.parent.color = "BLACK"
k.parent.parent.color = "RED"
self.rotate_left(k.parent.parent)
else: # 부모가 할아버지의 왼쪽 자식일 때
u = k.parent.parent.right # Uncle 노드
# Case 1: Uncle이 RED
if u.color == "RED":
u.color = "BLACK"
k.parent.color = "BLACK"
k.parent.parent.color = "RED"
k = k.parent.parent
else: # Case 2 or Case 3
# Case 2: 현재 노드가 오른쪽 자식일 때
if k == k.parent.right:
k = k.parent
self.rotate_left(k)
# Case 3
k.parent.color = "BLACK"
k.parent.parent.color = "RED"
self.rotate_right(k.parent.parent)
# 루트 노드까지 올라갔으면 종료
if k == self.root:
break
# 루트는 항상 BLACK
self.root.color = "BLACK"
# 삽입 함수
def insert(self, key):
# 새로운 노드 초기화
node = Node(key, "RED")
node.parent = None
node.data = key
node.left = self.TNULL
node.right = self.TNULL
node.color = "RED"
y = None
x = self.root
# 삽입 위치를 찾음
while x != self.TNULL:
y = x
if node.data < x.data:
x = x.left
else:
x = x.right
# 부모 노드 설정
node.parent = y
if y == None:
self.root = node
elif node.data < y.data:
y.left = node
else:
y.right = node
# 루트 노드면 BLACK으로 설정
if node.parent == None:
node.color = "BLACK"
return
# 할아버지 노드가 없으면 종료
if node.parent.parent == None:
return
# 균형을 맞춤
self.balance_insert(node)
# 왼쪽 회전 함수
def rotate_left(self, x):
y = x.right
x.right = y.left
if y.left != self.TNULL:
y.left.parent = x
y.parent = x.parent
if x.parent == None:
self.root = y
elif x == x.parent.left:
x.parent.left = y
else:
x.parent.right = y
y.left = x
x.parent = y
# 오른쪽 회전 함수
def rotate_right(self, x):
y = x.left
x.left = y.right
if y.right != self.TNULL:
y.right.parent = x
y.parent = x.parent
if x.parent == None:
self.root = y
elif x == x.parent.right:
x.parent.right = y
else:
x.parent.left = y
y.right = x
x.parent = y
# 트리 출력 함수
def print_tree(self, node, level=0, indent="----"):
if node != self.TNULL:
self.print_tree(node.right, level + 1, " ")
print(indent * level + "-> " + str(node.data) + "(" + node.color + ")")
self.print_tree(node.left, level + 1, " ")
# 테스트 코드
tree = RedBlackTree()
numbers = [30, 40, 50, 10, 20, 22, 24, 26, 14, 16, 18]
for num in numbers:
tree.insert(num)
print(f"After inserting {num}: ")
tree.print_tree(tree.root)
print("---")
테스트 결과
After inserting 30:
-> 30(BLACK)
---
After inserting 40:
-> 40(RED)
-> 30(BLACK)
---
After inserting 50:
-> 50(RED)
-> 40(BLACK)
-> 30(RED)
---
After inserting 10:
-> 50(BLACK)
-> 40(BLACK)
-> 30(BLACK)
-> 10(RED)
---
After inserting 20:
-> 50(BLACK)
-> 40(BLACK)
-> 30(RED)
-> 20(BLACK)
-> 10(RED)
---
After inserting 22:
-> 50(BLACK)
-> 40(BLACK)
-> 30(BLACK)
-> 22(RED)
-> 20(RED)
-> 10(BLACK)
---
After inserting 24:
-> 50(BLACK)
-> 40(BLACK)
-> 30(RED)
-> 24(BLACK)
-> 22(RED)
-> 20(RED)
-> 10(BLACK)
---
After inserting 26:
-> 50(BLACK)
-> 40(RED)
-> 30(BLACK)
-> 26(RED)
-> 24(BLACK)
-> 22(BLACK)
-> 20(RED)
-> 10(BLACK)
---
After inserting 14:
-> 50(BLACK)
-> 40(RED)
-> 30(BLACK)
-> 26(RED)
-> 24(BLACK)
-> 22(BLACK)
-> 20(RED)
-> 14(RED)
-> 10(BLACK)
---
After inserting 16:
-> 50(BLACK)
-> 40(RED)
-> 30(BLACK)
-> 26(RED)
-> 24(BLACK)
-> 22(BLACK)
-> 20(RED)
-> 16(RED)
-> 14(BLACK)
-> 10(RED)
---
After inserting 18:
-> 50(BLACK)
-> 40(BLACK)
-> 30(BLACK)
-> 26(RED)
-> 24(BLACK)
-> 22(BLACK)
-> 20(BLACK)
-> 18(RED)
-> 16(BLACK)
-> 14(RED)
-> 10(BLACK)
---