트리도 많은 종류가 있지만, 이 포스트에서는 트리 구조 중에서도 많이 사용하는 이진 탐색 트리에 대한 종류와 추가, 순회(탐색), 삭제하는 방법을 정리하고 구현해 볼 것이다.
왼쪽의 자식노드는 부모보다 작으며, 오른쪽 자식 노드는 부모보다 큰 값을 가지도록 구성한 이진 트리이다.
tree = [None] * 20
tree[1] = 5
print(tree)
def add_node(num, index = 1):
while True:
if num <= tree[index]:
index = 2 * index
else:
index = 2 * index + 1
if tree[index] == None:
tree[index] = num
break
print(tree)
노드의 인덱스를 구할 땐 다음과 같다.
위의 규칙에 따라 노드의 인덱스를 구해서 값을 바꿔주면 된다.
def find_node(num, index = 1):
while tree[index] != num:
if tree[index] != None:
if num <= tree[index]:
index = 2 * index
else:
index = 2 * index + 1
return index
삽입과 마찬가지로 이진 탐색 트리의 규칙에 따라 노드를 탐색한 뒤 해당 노드가 있는 배열의 인덱스를 반환한다.
이진 트리에서 노드를 삭제할 때는 세 가지 경우에 따라 다르게 진행한다.
- 단말 노드를 삭제할 때
- 자식 노드가 하나인 노드를 삭제할 때
- 자식 노드가 두 개인 노드를 삭제할 때
단말 노드를 삭제할 때는 해당 노드를 삭제하기만 하면 된다.
하나의 자식 노드를 가지고 있을 경우 해당 노드를 삭제하고 부모 노드와 자식 노드를 이어주면 된다.
두 개의 자식 노드를 가지고 있을 경우
- 왼쪽에서 가장 큰 값
- 오른쪽에서 가장 작은 값
위의 두 가지 중 하나의 값과 바꿔주면 된다. 위 두 가지 모두 반드시 단말 노드이기 때문에 값을 교환한 후 값이 바뀐 단말 노드를 삭제해주면 된다.
배열로 이진 탐색 트리를 만들면, 2번(하나의 자식 노드)의 경우 배열이라는 자료구조의 특성상 하나의 노드를 삭제하면 하위 노드들의 인덱스를 모두 재조정해주어야 한다. 그래서 두 개의 자식 노드를 가지고 있는 경우와 같은 방법으로 삭제를 구현했다.
def del_node(num):
index = find_node(num)
left = 2 * index
right = 2 * index + 1
# 단말 노드
if tree[left] == None and tree[right] == None:
tree[index] = None
# 자식 노드를 두 개 가진 노드
elif tree[left] != None and tree[right] != None:
while tree[2 * right] != None:
right = 2 * right
tree[index] = tree[right]
tree[right] = None
#자식 노드를 하나 가진 노드
else:
# 오른쪽 노드만 가졌을 때
if tree[left] == None:
while tree[2 * right] != None:
right = 2 * right
tree[index] = tree[right]
tree[right] = None
#왼쪽 노드만 가졌을 때
else:
while tree[2 * left + 1] != None:
left = 2 * left
tree[index] = tree[left]
tree[left] = None
# 노드
class Node:
def __init__(self, value):
self.value = value
self.left = None
self.right = None
# 이진 탐색 트리
# 노드
class Node:
def __init__(self, value):
self.value = value
self.left = None
self.right = None
# 이진 탐색 트리
class BinaryTree:
def __init__(self, value):
new_node = Node(value)
self.root = new_node
def addNode(self, value):
new_node = Node(value)
tmp = self.root
while True:
# 현재 노드(tmp)보다 크고
if tmp.value > new_node.value:
# 왼쪽 노드가 비었다면 왼쪽 노드로
if tmp.left == None:
tmp.left = new_node
break
# 비어 있지 않다면 현재노드를 왼쪽 노드로 변경
else:
tmp = tmp.left
# 현재 노드(tmp)보다 작거나 같고
else:
# 오른쪽 노드가 비었다면 오른쪽 노드로
if tmp.right == None:
tmp.right = new_node
break
# 비어 있지 않다면 현재노드를 오른쪽 노드로 변경
else:
tmp = tmp.right
def findNode(self, value):
tmp = self.root
while tmp.value != value:
if tmp.value > value:
parant = tmp
tmp = tmp.left
else:
parant = tmp
tmp = tmp.right
if tmp == None:
break
if tmp.value == value:
return tmp, parant
return False, False
def deleteNode(self, value):
target, parant = self.findNode(value)
if not target:
print("해당 노드가 없습니다.")
# 삭제할 노드가 존재 한다면
else:
# 단말 노드일 경우
if target.left == None and target.right == None:
if parant.value > target.value:
parant.left = None
else:
parant.right = None
# 왼쪽 자식 노드만 가지고 있을 경우
elif target.left != None and target.right == None:
if parant.value > target.value:
parant.left = target.left
else:
parant.right = target.left
# 오른쪽 자식 노드만 가지고 있을 경우
elif target.left == None and target.right != None:
if parant.value > target.value:
parant.left = target.right
else:
parant.right = target.right
# 두 개의 자식 노드를 가지고 있을 경우
else:
tmp = target.left
# 단말 노드를 찾을 때까지
while tmp.right != None and tmp.left != None:
# 오른쪽 자식 노드로 이동
tmp = tmp.right
target_num = tmp.value # 왼쪽에서 가장 큰 값
self.deleteNode(target_num) # 왼쪽에서 가장 큰 값 삭제
target.value = target_num # 삭제하려고 한 노드 value 변경
아래와 같이 임의의 이진 탐색 트리를 만들었다.
tree = BinaryTree(10)
tree.addNode(8)
tree.addNode(23)
tree.addNode(9)
tree.addNode(6)
tree.addNode(7)
tree.addNode(3)
tree.addNode(2)
tree.addNode(4)
tree.addNode(27)
이진 탐색 트리에서 삭제 연산을 할 때, 고려해야 하는 부분이 또 있다. 두 개의 자식 노드를 가지고 있는 노드를 삭제할 땐
- 왼쪽에서 가장 큰 값
- 오른쪽에서 가장 작은 값
위의 두 가지 중 하나를 택해서 삭제하고자 하는 노드(A)와 값을 바꾸기 위해 택한 노드(B)를 삭제한다.
그런데 (B)노드가 단말 노드가 아닐 수도 있다. 예를 들면 왼쪽에서 가장 큰 값을 가지는 노드가 왼쪽 자식 노드 하나만 가지고 있는 경우가 있다.
그래서 구현할 때 (B)노드의 value
를 따로 저장한 뒤에 해당 노드를 재귀 방식으로 삭제 연산을 한 번 더 진행한다.
이 때, (B)노드는 절대로 자식 노드를 두 개 가질 수 없다. 자식 노드를 두 개 가지고 있으면 값을 바꾸기 위해 택하지 않는다.
def Traversal(self, order, tmp):
if order == 'pre':
print(tmp.value, end=" ")
if tmp.left != None:
self.Traversal(order, tmp.left)
if tmp.right != None:
self.Traversal(order, tmp.right)
elif order == 'in':
if tmp.left != None:
self.Traversal(order, tmp.left)
print(tmp.value, end=" ")
if tmp.right != None:
self.Traversal(order, tmp.right)
elif order == 'post':
if tmp.left != None:
self.Traversal(order, tmp.left)
if tmp.right != None:
self.Traversal(order, tmp.right)
print(tmp.value, end=" ")