DataStructure
20210622 DataStructure 06 : Tree (트리), Binary Tree(이진 트리), Binary Search Tree(이중 탐색 트리), python으로 이진 탐색 트리 구현
DataStructure 06
Tree (트리)
- 자료구조이기도 하지만, 알고리즘 같은 로직도 많이 들어가서 알고리즘의 느낌도 있음
- 실제로 구현시 난이도가 높음
- Tree(트리) : Node와 Branch를 이용해서, 사이클을 이루지 않도록 구성한(사이클 관계X) 데이터 구조
- 이진 트리 (Binary Tree) 형태의 구조로, 검색 알고리즘 구현을 위해 많이 사용됨
용어
- Node : 트리에서 데이터를 저장하는 기본 요소 (데이터와 다른 연결된 도드에 대한 Branch 정보 포함)
- Root Node : 트리 최상위 노드
- Level : 최상위 노드를 Level 0으로 하였을 때, 하위 Branch로 연결된 해당 노드의 깊이
- Parent Node : 어떤 노드의 상위 레벨을 이루는 노드 (부모 노드)
- Child Node : 상위 레벨에 연결된 노드 (자식 노드)
- Leaf Node (Terminal Node) : Child Node가 하나도 없는 노드(끝나는 노드)
- Sibling (Brother Node) : 동일한 Parent Node를 가진 노드 (같은 레벨이라도, Sibling이 아닐 수 있음)
- Depth: 트리에서 Node가 가질 수 있는 최대 Level
이진 트리(Binary Tree) vs 이진 탐색 트리(Binary Search Tree)
- 이진 트리 : 노드의 최대 Branch가 2인 트리 (child Node가 0, 1, 2)
- 이진 탐색 트리(BST) : 이진 트리에서 부모 노드 보다 작은 값을 가지는 자식노드는 왼쪽 자식 노드로, 부모 모느 보다 큰 값을 가지는 자식노드는 오른쪽 자식 노드로 배치하는 트리
- 새로 들어오는 값을 계속 비교해서 배치함
이진 탐색트리 장단점& 주요용도
- 데이터 검색(탐색)에 주로 쓰임
- 장점 : 탐색 속도 개선
- 단점 : 복잡함
배열 vs 이진 탐색 트리: 탐색 비교
- 배열의 경우, 찾을 때 까지 모든 자료를 순회하면서 탐색하게 됨 (최악의 경우 배열의 크기만큼 걸릴 수 있음)
- 이진 탐색 트리의 경우, 해당 자료가 놓인 level 만큼의 횟수로 찾을 수 있음
Python 링크드 리스트를 활용한 이진 탐색 트리 구현
- 좌 우로 pointer를 가지고 있음
# Node 만들기
class Node:
def __init__(self, value):
self.value = value
self.left = None
self.right = None
# 이진 탐색 트리 구현
class NodeMgmt:
def __init__(self, head): # root Node 가 Head
self.head = head
def insert(self, value):
self.current_node = self.head
while True: # 일단 계속 돌게 하기 위해서
if value < self.current_node.value: # parent Node 보다 넣는 값이 작은 경우
if self.current_node.left != None: # parent 에서 left에 값이 있는 경우
self.current_node = self.current_node.left # 현재 노드를 left 노드로 함
else:
self.current_node.left = Node(value)
break
else: # 값이 같을 경우에도 오른쪽 노드로 배치
if self.current_node != 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
# 이진 탐색 트리 TEST
head = Node(1)
BST = NodeMgmt(head)
BST.insert(2)
BST.insert(3)
BST.insert(0)
BST.insert(4)
BST.insert(8)
BST.search(4) # True
BST.search(-1) # False
이진 탐색 트리 삭제 구현
- 삭제의 경우의 수
- Leaf Node를 삭제하는 경우
- 해당 노드 삭제
- 삭제할 노드의 Child Node가 1개인 경우
- 해당 자식을 올림
- 삭제할 노드의 child Node가 2개인 경우
- 왼쪽 기준으로 올리는 경우 : left 노드의 후손 중 제일 큰 값인 노드
- 오른쪽 기준으로 올리는 경우 : right 노드의 후손 중 제일 작은 값인 노드
- 삭제할 노드의 right 노드의 제일 작은 값인 노드(올리는 노드)가 자식을 가지는 경우 해당 자식은 올리는 노드의 부모의 left로 연결해줌
- 삭제할 노드가 없는 경우
- Leaf Node를 삭제하는 경우
# Node 만들기
class Node:
def __init__(self, value):
self.value = value
self.left = None
self.right = None
# 이진 탐색 트리 구현
class NodeMgmt:
def __init__(self, head): # root Node 가 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 != 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
# 해당 Node 찾기 및 parent 기억
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:
value > self.current_node.value:
self.parent = self.current_node
self.current_node = self.current_node.right
if searched == False:
return False
# Leaf Node
if self.current_node.left == None and current_node.right == None:
if value < self.parent.value:
self.parent.left = None
else:
self.parent.right = None
# 1 child
# current node에 left에 child가 있는 경우
elif self.current_node.left != None and current_node.right == None:
# current node가 parent의 left인 경우
if value < self.parent.value:
self.parent.left = self.current_node.left
# current node가 parent의 right인 경우
else:
self.parent.right = self.current_node.right
# current node에 right에 child가 있는 경우
elif self.current_node.left == None and current_node.right != None:
# current node가 parent의 left인 경우
if value < self.parent.value:
self.parent.left = self.current_node.right
# current node가 parent의 right인 경우
else:
self.parent.right = self.current_node.right
# 2 child
# (삭제하여, 대체되는 node의 경우 3갈래를 수정해줘야 함)
elif self.current_node.left != None and current_node.right != None:
# current_node : parent의 left에 있는 경우
if value < self.parent.value:
self.change = self.current_node.right
while self.change.left != None:
self.change_parent = self.change
self.change = self.change.left
# change.right 처리
if self.change.right != None:
self.change_parent.left = self.change.right
else:
self.change_parent.left = None
# 새로운 current로 오는 change의 3갈래 연결부 연결
# parent - change
self.parent.left = self.change
# change right - 기존 current right
self.change.right = self.current_node.right
# change left - 기존 current left
self.change.left = self.current_node.left
del self.current_node
# current_node : parent의 right에 있는 경우 (새로운 parent 연결 차이)
else:
self.change = self.current_node.right
while self.change.left != None:
self.change_parent = self.change
self.change = self.change.left
# change.right 처리
if self.change.right != None:
self.change_parent.left = self.change.right
else:
self.change_parent.left = None
# 새로운 current로 오는 change의 3갈래 연결부 연결
# parent - change
self.parent.right = self.change
# change right - 기존 current right
self.change.right = self.current_node.right
# change left - 기존 current left
self.change.left = self.current_node.left
# 마침내 해당 노드 삭제
del self.current_node
return True
- 최대의 경우의 수를 고려하고, 각각 나누어 디테일하게 만듦
- 그림으로 이해
- 수정될 노드 선정 및 위치 경우의 수
- 대체될 노드 선정
- 연결 수정 부분 체크
python 전체 코드 테스트
- random 라이브러리 활용
radom.randint(startNumber, endNumber)
: star ~ end 사이에 있는 숫자를 랜덤하게 return
import random
# 0 ~ 999 중 100개 숫자 랜덤 선택
bst_nums = set() # 중복 제거를 위해 집합을 사용
while len(bst_nums) != 100:
bst_nums.add(random.randint(0, 999))
# root 노드는 500 (좌우 균형)
head = Node(500)
# 랜덤 100 개의 숫자 BST 에 넣음
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() # 중복 제거를 위해 set 사용
bst_nums = list(bst_nums)
while len(delete_nums) != 10:
delete_nums.add(bts_nums[random.randint(0, 99)])
# 랜덤 10개의 숫자 삭제
for del_num in delete_nums:
if binary_tree.delete(del_num) == False:
print('delete failed', del_num)
이진 탐색 트리의 시간 복잡도와 단점
시간 복잡도
- depth (트리 높이)를 h라고 표기 한다면,
O(h)
- n개의 노드를 가진 다면, h = log n에 가까우니까 시간복잡도는
O(log n)
- log의 밑은 2 임
- 한번 찾을 때, 50%의 나머지 경우의 명령을 제거 -> 50%의 실행시간을 단축 시킬수 있음
단점
- 트리가 균형이 잡혀 있는 경우 평균 시간 복잡도는
O(log n)
이지만,- 트리가 균형잡히지 않은 경우 링크드 리스트 등과 동일한 성능인
O(n)
성능을 보여줌 - root 값이 중간 값에서 멀어 질 수록, 들어가는 값이 연속되어 커지는 경우 -> 트리 불균형
- right로 치우치는 경우 : root(1) - (2) - (3) - (4) - ...
- left로 치우치는 경우 : root(n) - (n-1) - (n-2) ...
- 트리가 균형잡히지 않은 경우 링크드 리스트 등과 동일한 성능인