트리(tree)는 계층적 관계를 표현하는 자료구조다. 용어만 들으면 복잡해 보일 수 있지만, 사실 우리 일상 생활에서도 트리 구조를 많이 접하고 있다. 가족 트리나 조직도 등이 대표적인 예이다.
트리 구조는 노드(node)들의 집합으로, 노드들은 부모-자식 관계를 갖는다. 특히 다음과 같은 특징을 가진다.
트리는 노드의 추가, 검색, 삭제 등의 연산을 효율적으로 수행할 수 있도록 설계되어 있다. 이진 트리, 이진 검색 트리, AVL 트리, 레드-블랙 트리, B트리 등 다양한 종류의 트리가 존재하며, 각각의 트리는 특별한 목적에 맞게 설계되어 있다.
이진 탐색 트리(Binary Search Tree, BST)는 자료의 탐색, 삽입, 삭제 등의 연산을 효율적으로 수행할 수 있어, 다양한 상황에서 활용된다.
장점
주요 용도
class Node:
def __init__(self, value):
self.value = value
self.left = None
self.right = None
이 코드에서는 이진 탐색 트리를 구현하는데 필요한 노드 클래스를 정의하고 있다.
각 노드는 세 가지 속성을 가진다.
노드의 init 메서드는 노드를 초기화합니다. 새 노드는 값이 주어지고, 처음에는 양쪽 자식 노드가 없다 (left와 right는 None). 이것이 이진 탐색 트리를 구성하는 기본 블록이다. 이제 이 노드들을 사용하여 이진 탐색 트리를 구성할 수 있다.
class NodeMgmt:
def __init__(self, head):
self.head = head
def insert(self, value):
self.current_node = self.head
while True:
if value < self.current_node.value:
if self.current_node.left != None:
self.current_node = self.current_node.left
else:
self.current_node.left = Node(value)
break
else:
if self.current_node.right != None:
self.current_node = self.current_node.right
else:
self.current_node.right = Node(value)
break
head = Node(1)
BST = NodeMgmt(head)
BST.insert(2)
이 코드는 이진 탐색 트리(Binary Search Tree, BST)를 관리하는 NodeMgmt 클래스를 정의하고 있습니다. NodeMgmt 클래스는 이진 탐색 트리에 노드를 삽입하는 기능을 제공한다.
NodeMgmt 클래스의 init 메서드는 트리의 루트 노드(head)를 초기화한다.
insert 메서드는 트리에 새 노드를 삽입한다. 삽입하려는 값이 현재 노드의 값보다 작으면 현재 노드를 왼쪽 자식 노드로 이동하고, 그렇지 않으면 오른쪽 자식 노드로 이동한다. 이 작업을 삽입하려는 위치를 찾을 때까지 반복한다.
삽입하려는 위치를 찾으면, 새 노드를 생성하고 현재 노드의 적절한 자식 노드로 설정한다. 만약 삽입하려는 값이 현재 노드의 값보다 작다면 왼쪽 자식 노드로, 크다면 오른쪽 자식 노드로 설정한다.
이 코드는 새로운 NodeMgmt 객체를 생성하고, insert 메서드를 사용하여 이진 탐색 트리에 새 노드를 삽입하는 방법을 보여준다. 이 과정에서 먼저 노드를 하나 생성하여 이를 헤드(루트 노드)로 설정하고, 그 후 insert 메서드를 통해 새로운 값을 가진 노드를 이진 탐색 트리에 추가하고 있다.
class NodeMgmt:
def __init__(self, head):
self.head = head
def insert(self, value):
self.current_node = self.head
while True:
if value < self.current_node.value:
if self.current_node.left != None:
self.current_node = self.current_node.left
else:
self.current_node.left = Node(value)
break
else:
if self.current_node.right != None:
self.current_node = self.current_node.right
else:
self.current_node.right = Node(value)
break
def search(self, value):
self.current_node = self.head
while self.current_node:
if self.current_node.value == value:
return True
elif value < self.current_node.value:
self.current_node = self.current_node.left
else:
self.current_node = self.current_node.right
return False
head = Node(1)
BST = NodeMgmt(head)
BST.insert(2)
BST.insert(3)
BST.insert(0)
BST.insert(4)
BST.insert(8)
BST.search(-1)
이 코드는 이진 탐색 트리에서 특정 값을 가진 노드를 찾는 search 메서드를 추가한 NodeMgmt 클래스를 정의하고 있다.
search 메서드는 이진 탐색 트리에서 주어진 값을 가진 노드를 찾는다. 메서드는 루트 노드에서 시작하여, 찾고자 하는 값과 현재 노드의 값을 비교한다. 만약 찾고자 하는 값이 현재 노드의 값과 같다면, True를 반환한다.
찾고자 하는 값이 현재 노드의 값보다 작으면, 현재 노드를 왼쪽 자식 노드로 이동시키고, 그렇지 않으면 오른쪽 자식 노드로 이동시킨다. 이 과정을 트리를 통해 찾고자 하는 값이 나올 때까지 반복한다. 만약 트리의 모든 노드를 검사한 후에도 찾고자 하는 값이 없다면, False를 반환한다.
이 코드는 새로운 NodeMgmt 객체를 생성하고, 여러 개의 값을 가진 노드를 이진 탐색 트리에 삽입한 후, search 메서드를 사용하여 특정 값을 가진 노드를 이진 탐색 트리에서 찾는 방법을 보여준다. BST.search(-1)은 이진 탐색 트리에서 값 -1을 가진 노드를 찾는 것을 시도하고, 해당 값이 트리 내에 없으므로 False를 반환한다.
매우 복잡함으로, 경우를 나누어서 이해하는 것이 좋다.
이진 탐색 트리에서 노드를 삭제할 때, 그 노드가 어떤 자식 노드를 가지고 있는지에 따라서 처리 방식이 달라진다.
Leaf Node는 Chuld Node가 없는 Node이다.
삭제할 Node의 Parent Node가 삭제할 Node를 가리키지 않도록 해야 한다.
삭제할 Node의 Parent Node가 삭제할 Node의 Child Node를 가리키도록 한다.
삭제할 Node가 없는 경우도 처리해야 한다. 이를 위해선, 삭제할 Node가 없는 경우는 False를 리턴하고 함수를 종료시킨다.
# def delete(self, value):
searched = False
self.current_node = self.head
self.parent = self.head
while self.current_node:
if self.current_node.value == value:
searched = True
break
elif value < self.current_node.value:
self.parent = self.current_node
self.current_node = self.current_node.left
else:
self.parent = self.current_node
self.current_node = self.current_node.right
if searched == False:
return False
### 이후부터 Case들을 분리해서, 코드 작성
이 코드는 이진 탐색 트리에서 주어진 값을 가진 노드를 찾아 삭제하는 과정의 일부다. 먼저 삭제하려는 노드를 찾는 과정이다.
삭제하려는 노드의 값을 value라는 매개변수로 받아서 삭제하려는 노드가 있는지 탐색한다. 이를 위해 루트 노드에서 시작하여 각 노드의 값을 비교한다. 만약 찾으려는 노드의 값이 현재 노드의 값과 같으면 searched를 True로 변경하고 루프를 종료한다.
만약 찾으려는 값이 현재 노드의 값보다 작다면, 현재 노드의 부모 노드를 갱신하고 현재 노드를 왼쪽 자식 노드로 이동한다. 반대로 찾으려는 값이 현재 노드의 값보다 크다면, 현재 노드의 부모 노드를 갱신하고 현재 노드를 오른쪽 자식 노드로 이동한다. 이 과정을 찾으려는 값을 가진 노드를 찾을 때까지 반복한다.
만약 찾으려는 값을 가진 노드가 없다면 searched가 False인 상태로 루프가 종료되므로, 함수는 False를 반환하고 종료한다. 이후의 삭제 과정은 ### 이후부터 Case들을 분리해서, 코드 작성 부분에 작성하면 된다.
이 코드는 이진 탐색 트리에서 삭제하려는 노드가 있는지 확인하는 과정을 담당한다. 노드가 있으면 노드와 그 노드의 부모 노드에 대한 정보를 기록하여 삭제 작업을 계속 진행할 수 있다.
# self.current_node 가 삭제할 Node, self.parent는 삭제할 Node의 Parent Node인 상태
if self.current_node.left == None and self.current_node.right == None:
if value < self.parent.value:
self.parent.left = None
else:
self.parent.right = None
del self.current_node
이 코드는 삭제하려는 노드가 리프 노드(Leaf Node, 자식 노드가 없는 노드)인 경우를 처리한다. 리프 노드는 자식 노드가 없기 때문에 간단하게 삭제할 수 있다.
삭제하려는 노드인 self.current_node의 왼쪽과 오른쪽 자식 노드가 모두 None인 경우에 이 코드가 실행된다. self.current_node는 삭제하려는 노드이고, self.parent는 삭제하려는 노드의 부모 노드를 참조하고 있다.
삭제하려는 노드의 값이 부모 노드의 값보다 작다면, 삭제하려는 노드는 부모 노드의 왼쪽 자식 노드임을 의미한다. 이 경우 self.parent.left를 None으로 설정하여 부모 노드와의 연결을 끊는다.
반대로 삭제하려는 노드의 값이 부모 노드의 값보다 크다면, 삭제하려는 노드는 부모 노드의 오른쪽 자식 노드임을 의미합니다. 이 경우 self.parent.right를 None으로 설정하여 부모 노드와의 연결을 끊는다.
마지막으로 del self.current_node를 통해 삭제하려는 노드를 메모리에서 제거한다. 이렇게 해서 리프 노드의 삭제 과정이 완료된다.
if self.current_node.left != None and self.current_node.right == None:
if value < self.parent.value:
self.parent.left = self.current_node.left
else:
self.parent.right = self.current_node.left
elif self.current_node.left == None and self.current_node.right != None:
if value < self.parent.value:
self.parent.left = self.current_node.right
else:
self.parent.right = self.current_node.right
이 코드는 삭제하려는 노드가 하나의 자식 노드만 가지고 있는 경우를 처리한다. 두 가지 하위 케이스로 나뉘는데, 삭제하려는 노드가 왼쪽 자식 노드만 가지고 있을 때와 오른쪽 자식 노드만 가지고 있을 때이다.
첫 번째 if 문은 삭제하려는 노드가 왼쪽 자식 노드만 가지고 있는 경우를 다룬다. self.current_node.left는 존재하고 self.current_node.right는 존재하지 않는다. 이 경우, 삭제하려는 노드의 부모 노드를 삭제하려는 노드의 자식 노드와 연결해준다. 삭제하려는 노드가 부모 노드의 왼쪽에 있을 경우(value < self.parent.value)는 self.parent.left를 삭제하려는 노드의 왼쪽 자식 노드와 연결해주고, 반대로 삭제하려는 노드가 부모 노드의 오른쪽에 있을 경우는 self.parent.right를 삭제하려는 노드의 왼쪽 자식 노드와 연결해준다.
두 번째 elif 문은 삭제하려는 노드가 오른쪽 자식 노드만 가지고 있는 경우를 다룬다. self.current_node.left는 존재하지 않고 self.current_node.right는 존재한다. 이 경우도 마찬가지로, 삭제하려는 노드의 부모 노드를 삭제하려는 노드의 자식 노드와 연결해준다. 삭제하려는 노드가 부모 노드의 왼쪽에 있을 경우는 self.parent.left를 삭제하려는 노드의 오른쪽 자식 노드와 연결해주고, 반대로 삭제하려는 노드가 부모 노드의 오른쪽에 있을 경우는 self.parent.right를 삭제하려는 노드의 오른쪽 자식 노드와 연결해준다.
if self.current_node.left != None and self.current_node.right != None: # case3
if value < self.parent.value: # case3-1
self.change_node = self.current_node.right
self.change_node_parent = self.current_node.right
while self.change_node.left != None:
self.change_node_parent = self.change_node
self.change_node = self.change_node.left
if self.change_node.right != None:
self.change_node_parent.left = self.change_node.right
else:
self.change_node_parent.left = None
self.parent.left = self.change_node
self.change_node.right = self.current_node.right
self.change_node.left = self.change_node.left
이 코드는 삭제하려는 노드가 두 개의 자식 노드를 모두 가지고 있고, 삭제하려는 노드가 부모 노드의 왼쪽에 위치한 경우를 처리한다.
삭제하려는 노드가 두 개의 자식 노드를 모두 가지고 있다면, 가장 까다로운 상황이다. 이럴 경우에는 삭제하려는 노드의 오른쪽 자식 중 가장 작은 값을 삭제하려는 노드의 부모 노드가 가리키도록 하는 전략을 사용한다.
이를 위해서 self.change_node라는 변수를 설정하여 삭제하려는 노드의 오른쪽 자식을 가리키게 한다. 이 노드는 우리가 교체하려는 노드다. 그리고 이 노드의 부모 노드를 self.change_node_parent에 저장한다.
그 다음 while 문을 통해 self.change_node의 왼쪽 자식 노드를 탐색한다. 왜냐하면 노드의 왼쪽 자식 노드가 노드의 오른쪽 자식 노드보다 항상 작기 때문이다. 따라서 왼쪽 자식 노드가 없을 때까지 self.change_node와 self.change_node_parent를 계속해서 왼쪽으로 이동시킨다.
이제 가장 작은 값을 가진 노드를 찾았으니, 이 노드를 삭제하려는 노드와 교체해야 한다. 하지만 이 노드가 오른쪽 자식 노드를 가지고 있을 수도 있으므로 이 경우를 처리해야 한다. 이 노드가 오른쪽 자식 노드를 가지고 있다면 self.change_node_parent의 왼쪽 자식 노드를 self.change_node의 오른쪽 자식 노드로 바꾸고, self.change_node가 오른쪽 자식 노드를 가지고 있지 않다면 self.change_node_parent의 왼쪽 자식 노드를 None으로 설정하여 노드를 삭제한다.
마지막으로 self.parent의 왼쪽 자식 노드를 self.change_node로 바꾸고, self.change_node의 오른쪽과 왼쪽 자식 노드를 각각 삭제하려는 노드의 오른쪽과 왼쪽 자식 노드로 설정하여 노드를 교체한다.
else:
self.change_node = self.current_node.right
self.change_node_parent = self.current_node.right
while self.change_node.left != None:
self.change_node_parent = self.change_node
self.change_node = self.change_node.left
if self.change_node.right != None:
self.change_node_parent.left = self.change_node.right
else:
self.change_node_parent.left = None
self.parent.right = self.change_node
self.change_node.left = self.current_node.left
self.change_node.right = self.current_node.right
이 코드는 삭제하려는 노드가 두 개의 자식 노드를 모두 가지고 있고, 삭제하려는 노드가 부모 노드의 오른쪽에 위치한 경우를 처리한다.
이전의 경우와 비슷하게 이 경우에도 삭제하려는 노드의 오른쪽 자식 중 가장 작은 값을 삭제하려는 노드의 위치로 이동시키는 전략을 사용한다.
self.change_node를 설정하여 삭제하려는 노드의 오른쪽 자식을 가리키게 하고, 이 노드의 부모 노드를 self.change_node_parent에 저장한다.
while 문을 통해 self.change_node의 왼쪽 자식 노드를 탐색한다. 왼쪽 자식 노드가 없을 때까지 self.change_node와 self.change_node_parent를 계속해서 왼쪽으로 이동시키면, 가장 작은 값을 가진 노드를 찾을 수 있다.
이 노드를 삭제하려는 노드와 교체하려면, 이 노드가 오른쪽 자식 노드를 가지고 있는지를 확인해야 한다. 만약 오른쪽 자식 노드를 가지고 있다면, self.change_node_parent의 왼쪽 자식 노드를 self.change_node의 오른쪽 자식 노드로 설정합니다. 만약 오른쪽 자식 노드를 가지고 있지 않다면, self.change_node_parent의 왼쪽 자식 노드를 None으로 설정하여 노드를 삭제한다.
마지막으로 self.parent의 오른쪽 자식 노드를 self.change_node로 설정하고, self.change_node의 왼쪽과 오른쪽 자식 노드를 각각 삭제하려는 노드의 왼쪽과 오른쪽 자식 노드로 설정한다. 이렇게 하면, 삭제하려는 노드와 self.change_node가 교체된다.
class Node:
def __init__(self, value):
self.value = value
self.left = None
self.right = None
class NodeMgmt:
def __init__(self, head):
self.head = head
def insert(self, value):
self.current_node = self.head
while True:
if value < self.current_node.value:
if self.current_node.left != None:
self.current_node = self.current_node.left
else:
self.current_node.left = Node(value)
break
else:
if self.current_node.right != None:
self.current_node = self.current_node.right
else:
self.current_node.right = Node(value)
break
def search(self, value):
self.current_node = self.head
while self.current_node:
if self.current_node.value == value:
return True
elif value < self.current_node.value:
self.current_node = self.current_node.left
else:
self.current_node = self.current_node.right
return False
def delete(self, value):
# 삭제할 노드 탐색
searched = False
self.current_node = self.head
self.parent = self.head
while self.current_node:
if self.current_node.value == value:
searched = True
break
elif value < self.current_node.value:
self.parent = self.current_node
self.current_node = self.current_node.left
else:
self.parent = self.current_node
self.current_node = self.current_node.right
if searched == False:
return False
# case1
if self.current_node.left == None and self.current_node.right == None:
if value < self.parent.value:
self.parent.left = None
else:
self.parent.right = None
# case2
elif self.current_node.left != None and self.current_node.right == None:
if value < self.parent.value:
self.parent.left = self.current_node.left
else:
self.parent.right = self.current_node.left
elif self.current_node.left == None and self.current_node.right != None:
if value < self.parent.value:
self.parent.left = self.current_node.right
else:
self.parent.right = self.current_node.right
# case 3
elif self.current_node.left != None and self.current_node.right != None:
# case3-1
if value < self.parent.value:
self.change_node = self.current_node.right
self.change_node_parent = self.current_node.right
while self.change_node.left != None:
self.change_node_parent = self.change_node
self.change_node = self.change_node.left
if self.change_node.right != None:
self.change_node_parent.left = self.change_node.right
else:
self.change_node_parent.left = None
self.parent.left = self.change_node
self.change_node.right = self.current_node.right
self.change_node.left = self.change_node.left
# case 3-2
else:
self.change_node = self.current_node.right
self.change_node_parent = self.current_node.right
while self.change_node.left != None:
self.change_node_parent = self.change_node
self.change_node = self.change_node.left
if self.change_node.right != None:
self.change_node_parent.left = self.change_node.right
else:
self.change_node_parent.left = None
self.parent.right = self.change_node
self.change_node.right = self.current_node.right
self.change_node.left = self.current_node.left
return True
# 0 ~ 999 숫자 중에서 임의로 100개를 추출해서, 이진 탐색 트리에 입력, 검색, 삭제
import random
# 0 ~ 999 중, 100 개의 숫자 랜덤 선택
bst_nums = set()
while len(bst_nums) != 100:
bst_nums.add(random.randint(0, 999))
# print (bst_nums)
# 선택된 100개의 숫자를 이진 탐색 트리에 입력, 임의로 루트노드는 500을 넣기로 함
head = Node(500)
binary_tree = NodeMgmt(head)
for num in bst_nums:
binary_tree.insert(num)
# 입력한 100개의 숫자 검색 (검색 기능 확인)
for num in bst_nums:
if binary_tree.search(num) == False:
print ('search failed', num)
# 입력한 100개의 숫자 중 10개의 숫자를 랜덤 선택
delete_nums = set()
bst_nums = list(bst_nums)
while len(delete_nums) != 10:
delete_nums.add(bst_nums[random.randint(0, 99)])
# 선택한 10개의 숫자를 삭제 (삭제 기능 확인)
for del_num in delete_nums:
if binary_tree.delete(del_num) == False:
print('delete failed', del_num)