숫자 맞추기(Up Down) 게임은 이진 검색을 이용한 것이다.
1 ~ 100 사이에 임의의 숫자를 맞추는 놀이를 할 때, 처음 추측하는 값은 50으로 한다. 그러면 이 값보다 크거나 작은지에 따라 Up, Down이라고 얘기하면서 다시 그 사이의 값을 추측하는 방식으로 답을 찾는다.
이러한 방식이 이진 검색이다. Up과 Down을 왼쪽과 오른쪽으로 바꿔서 생각해보자.
이진 트리는 앞서 공부했으므로 이진 트리와 이진 탐색 트리의 차이점을 알아보자.
- 모든 왼쪽 서브 트리의 노드는 루트 노드보다 작다.
- 모든 오른쪽 서브 트리의 노드는 루트 노드보다 크다.
- 왼쪽과 오른쪽 서브 트리도 모두 이진 탐색 트리이다.
- 중복 노드는 없다.
(a)는 이진 탐색 트리이지만, (b)는 이진 탐색 트리가 아니다. 값이 1인 노드는 4의 오른쪽에 있으므로 4보다 커야 하는데, 이 조건을 만족하지 않으므로 이진 탐색 트리가 아니다.
Binary Search Tree 클래스 구현하기
class Node:
def __init__(self,data):
self.data = data
self.left = None
self.right = None
class BS_Tree:
def __init__(self):
self.root = None
1. contains 메서드 만들기
특정 값을 찾는 메서드를 만든다. __contains__ 메서드로 만들면, 다른 컨테이너형 객체처럼 in 또는 not in을 사용할 수 있다.
class Node:
...
class BS_Tree:
...
def __contains__(self,data):
node = self.root
while node:
if node.data == data:
return True
elif node.data > data:
node = node.left
else:
node = node.right
return False
- 루트 노드부터 data와 비교하여 값이 같으면 True를 반환한다.
- 매개변수로 주어진 data가 노드 data보다 작으면 왼쪽 노드로 이동하여 비교한다.
2. insert 메서드 구현하기
이진 탐색 트리를 만족하도록 key,data를 삽입하는 메서드를 만든다.
2-1. 반복문으로 구현하기
def insert(self,data):
if self.root is None:
self.root = Node(data)
return
node = self.root
while True:
if node.data > data:
if node.left is None:
node.left = Node(data)
return
else:
node = node.left
else:
if node.right is None:
node.right = Node(data)
return
else:
node = node.right
2-2. 재귀 함수로 구현하기
- 앞선 반복문 코드가 if...else 구문도 많고 반복되는 부분도 있어 재귀로 구현해본다.
- 루트 노드의 서브 트리 노드들도 이진 탐색 트리이기 때문에 재귀적으로 접근할 수 있다.
def insert_recursive(self,data):
def _insert(node, data):
if node is None:
return Node(data)
if node.data > data:
node.left = _insert(node.left, data)
else:
node.right = _insert(node.right, data)
return Node
self.root = _insert(self.root, data)
3. delete 메서드 구현하기
삭제할 노드를 찾고 삭제할 노드의 연결 상태에 따라 이진 탐색 트리를 재구성해야한다.
삭제할 노드의 경우의 수는 다음과 같다.
1. 리프 노드일 경우: 그냥 삭제
2. 자식 노드가 하나일 경우: 자식 노드를 현재 노드 위치로 끌어올린다.
3. 자식 노드가 2개일 경우
- 오른쪽 자식 노드 중에서 가장 작은 노드를 끌어올린다. 혹은 왼쪽 자식 노드 중에서 가장 큰 노드를 끌어올린다.
- 오른쪽 자식 노드가 서브 트리면, 서브 트리에서 가장 왼쪽 노드가 가장 작은 노드이다.
(a) 삭제할 노드가 리프 노드일 경우
- 값이 2,5,8 인 노드는 리프 노드이다. 이 노드를 삭제하려면, 해당 노드는 None으로 만든 후 부모 노드에 연결한다.
(b) 삭제할 노드의 자식이 하나일 경우
- 값이 7, 9 노드의 자식 노드는 왼쪽 또는 오른쪽 하나만 있다. 예를 들어 값이 7인 노드를 삭제하려면 삭제할 노드와 삭제할 노드의 부모 노드를 찾는다.
- 값이 7인 노드의 자식 노드(8)를 부모 노드(9)의 왼쪽에 연결한다.
9를 삭제할 때도 위와 같이 처리한다.
(c) 삭제할 노드의 자식이 2개인 경우
- 삭제할 노드와 삭제할 노드의 부모 노드를 찾는다.
- 값이 4인 노드의 오른쪽 자식 중에서 값이 가장 작은 노드(5)를 찾는다.
- 즉, 값이 가장 작은 노드는 리프 노드이거나 오른쪽 자식만 가진다.
- 삭제할 노드의 값을 위에서 찾은 가장 작은 노드의 값(5)로 대체한다.
- 값이 5인 노드를 삭제한다.
- 이는 (a)인 경우와 동일하다.
- 만약 5의 오른쪽 자식이 있다면 (b)와 동일하다.
# 삭제할 노드가 리프 노드이거나, 자식이 하나일 때 처리한다.
# 왼쪽 자식이 있으면 반환하고, 그렇지 않으면 오른쪽 자식을 반환한다.
# 리프 노드이면 왼쪽과 오른쪽이 모두 None이므로 None을 반환한다.
def _del_node1(self,del_node):
if del_node.left:
return del_node.left
return del_node.right
# 자식이 둘인 노드를 삭제한다.
def _del_node2(self,del_node):
# 삭제할 노드의 부모 노드와 오른쪽에서 가장 왼쪽 노드를 찾는다.
parent = del_node
leftmost = del_node.right
while leftmost.left:
parent = leftmost
leftmost = leftmost.left
# 삭제할 노드의 값을 가장 왼쪽 노드의 값으로 대체한다.
del_node.data = leftmost.data
# 가장 왼쪽 노드를 삭제한다.
# 가장 왼쪽 노드는 리프 노드이거나 오른쪽에 자식이 있다.
parent.left = self._del_node1(leftmost)
def delete(self,data):
# 루트가 None일 때와 삭제 대상일 때를 처리한다.
if self.root is None:
return
if self.root.data == data:
if self.root.left and self.root.right:
self._del_node2(self.root)
else:
self.root = self._del_node1(self.root)
return
#삭제할 노드와 그 부모 노드를 찾는다.
del_node = self.root
while del_node and del_node.data != data:
parent = del_node
if del_node.data > data:
del_node = del_node.left
else:
del_node = del_node.right
if del_node is None: #삭제할 노드가 없으면 종료
return
#삭제할 노드의 자식이 둘일 때
if del_node.left and del_node.right:
self._del_node2(del_node)
return
#삭제할 노드가 리프 노드이거나 자식이 하나일 때
if parent.left == del_node:
parent.left = self._del_node1(del_node)
else:
parent.right = self._del_node1(del_node)
'Python' 카테고리의 다른 글
프로그래머스 연습문제 - 두 정수 사이의 합 (0) | 2024.03.27 |
---|---|
프로그래머스 연습문제 - 정수 제곱근 판별 (0) | 2024.03.27 |
[좌충우돌, 파이썬으로 자료구조 구현하기] 더 맵게 - 프로그래머스 (0) | 2023.09.19 |
[좌충우돌, 파이썬으로 자료구조 구현하기] Heapify 구현하기 (1) | 2023.09.18 |
[좌충우돌, 파이썬으로 자료구조 구현하기] 힙(heap) 구현하기 (0) | 2023.09.16 |