1. 정렬된 배열과 해시 테이블의 단점
- 정렬된 배열은 삽입과 삭제가 느리다. 최악의 시나리오(배열 앞에 값 삽입, 배열 첫번째 값 삭제)의 경우 O(N) 시간복잡도
- 해시 테이블은 순서 유지가 안된다.
- 순서를 유지하면서도 빠른 검색과 삽입, 삭제가 가능한 자료구조 -> 이진 트리
- 이진 트리와 정렬된 배열의 탐색 비교
2. 트리 (Tree) 구조
- 트리: Node와 Branch를 이용해서, 사이클을 이루지 않도록 구성한 데이터 구조
- 실제로 어디에 많이 사용되나?
- 트리 중 이진 트리 (Binary Tree) 형태의 구조로, 탐색(검색) 알고리즘 구현을 위해 많이 사용
- 브랜치가 최대 2개인 트리구조가 이진 트리이다.
3. 알아둘 용어
- Node: 트리에서 데이터를 저장하는 기본 요소 (데이터와 다른 연결된 노드에 대한 Branch 정보 포함)
- Root Node: 트리 맨 위에 있는 노드
- Level: 최상위 노드를 Level 0으로 하였을 때, 하위 Branch로 연결된 노드의 깊이를 나타냄
- Parent Node: 어떤 노드의 다음 레벨에 연결된 노드
- Child Node: 어떤 노드의 상위 레벨에 연결된 노드
- Leaf Node (Terminal Node): Child Node가 하나도 없는 노드
- Sibling (Brother Node): 동일한 Parent Node를 가진 노드
- Depth: 트리에서 Node가 가질 수 있는 최대 Level
4. 이진 트리와 이진 탐색 트리 (Binary Search Tree)
- 이진 트리: 노드의 최대 Branch가 2인 트리
- 이진 탐색 트리 (Binary Search Tree, BST): 이진 트리에 다음과 같은 추가적인 조건이 있는 트리
- 왼쪽 노드는 해당 노드보다 작은 값, 오른쪽 노드는 해당 노드보다 큰 값을 가지고 있음!
- 이진 탐색 트리는 더블 링크드 리스트로 구현
5. 자료 구조 이진 탐색 트리의 장점과 주요 용도
- 주요 용도: 데이터 검색(탐색)
- 장점: 탐색 속도를 개선할 수 있음
6. 파이썬으로 이진 탐색 트리 구현
6.1. 노드 클래스
class Node:
def __init__(self, value):
self.value = value
self.left = None
self.right = None
6.2 삽입
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
삽입의 경우 log N(검색) + 1(삽입) 단계가 걸리며 시간복잡도는 O(log N)이 된다. 정렬된 배열에서는 검색 뿐 아니라 삽입할 공간을 마련하기 위해 많은 데이터를 시프트해야 하므로 삽입에 O(N) 시간복잡도가 걸린다.
* 정렬된 배열 -> 검색 : O(log N), 삽입 : O(N)
* 이진 트리 -> 검색: O(log N), 삽입 : O(log N)
이와 같이 이진 트리가 정렬된 배열보다 더욱 효과적임을 알 수 있다.
6.3. 탐색
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:
sefl.current_node = self.current_node.right
return False
위에서 말했듯이 시간복잡도는 O(log N)이다. 각 단계를 수행할 때마다 값이 들어있을 남은 공간 중 반을 제거하기 때문이다.
6.4. 삭제의 이해
가능한 경우의 수는 크게 3가지이다.
1. leaf node삭제
2. child node가 하나인 node 삭제
- 삭제할 노드에 자식이 하나면 노드를 삭제하고 그 자식을 삭제된 노드가 있던 위치로 옮긴다.
3. child node가 두 개인 node 삭제
- 삭제된 노드를 후속자 노드로 대체한다. 후속자 노드란 삭제된 노드보다 큰 값(오른쪽) 중 최솟값(삭제된 노드보다 큰 값의 제일 왼쪽 노드)
- 만약 후속자 노드에 오른쪽 자식이 있으면 후속자를 삭제된 노드가 있던 자리에 넣은 후, 후속자 노드의 오른쪽 자식을 후속자 노드의 원래 부모의 왼쪽 자식으로 넣는다.
6.5. 삭제코드 구현
6.5.1. 삭제할 노드 탐색 / Case 1 : 삭제할 노드가 leaf Node
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 : 삭제할 노드가 leaf node일 경우
# self.current_node가 삭제할 노드, self.parent_node가 삭제할 노드의 parent node인 상태
if self.current_node.left == None and self.current_node.right == None: # 현재 leaf Node인 상황
if value < self.parent_node.value: # parent node의 왼쪽에 있는 경우
self.parent_node.left = None
else: # parent node의 오른쪽에 있는 경우
self.parent_node.right = None
del self.current_node
6.5.2 Case2 : 삭제할 Node가 child node를 한 개 가지고 있을 경우
# Case2 : 삭제할 노드가 child node를 한 개 가지고 있을 경우
# child node가 왼쪽에 있을 경우
if self.current_node.left != None and self.current_node.right == None:
if value < self.parent_node.value: # current node가 parent의 왼쪽에 있을 때
self.parent_node.left = self.current_node.left
else: # current node가 parent의 오른쪽에 있을 때
self.parent_node.right = self.current_node.left
# child node가 오른쪽에 있을 경우
elif self.current_node.left == None and self.current_node.right != None:
if value < self.parent_node.value: # current node가 parent의 왼쪽에 있을 때
self.parent_node.left = self.current_node.right
else: # current node가 parent의 오른쪽에 있을 때
self.parent_node.right = self.current_node.right
6.5.3. Case3-1 : 삭제할 Node가 parent node의 왼쪽에 있고 child node를 두 개 가지고 있을 때
- Case3-1-1 : 삭제할 Node가 parent node의 왼쪽에 있고 삭제할 Node의 오른쪽 자식 중 가장 작은 값을 가진 Node의 child node가 없을때
- Case3-1-2 : 삭제할 Node가 parent node의 왼쪽에 있고 삭제할 Node의 오른쪽 자식 중 가장 작은 값을 가진 Node의 오른쪽에 child node가 있을때
if self.current_node.left != None and self.current_node.right != None: # case3
if value < self.parent_node.value: # case 3-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: # case 3-1-2
self.change_node_parent.left = self.change_node.right
else: # case 3-1-1
self.change_node_parent.left = None
self.parent_node.left = self.change_node
self.change_node.left = self.current_node.left
self.change_node.right = self.current_node.right
6.5.4. Case3-2 : 삭제할 Node가 parent node의 오른쪽에 있고 child node를 두 개 가지고 있을 때
- Case3-2-1 : 삭제할 Node가 parent node의 오른쪽에 있고 삭제할 Node의 오른쪽 자식 중 가장 작은 값을 가진 Node의 child node가 없을때
- Case3-2-2 : 삭제할 Node가 parent node의 오른쪽에 있고 삭제할 Node의 오른쪽 자식 중 가장 작은 값을 가진 Node의 오른쪽에 child node가 있을때
else: # case 3-2
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: # case 3-2-2
self.change_node_parent.left = self.change_node.right
else: # case 3-2-1
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
6.6 파이썬 코드 병합
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
7. 이진탐색트리의 시간복잡도와 단점
7.1 시간복잡도(탐색시)
- depth를 h라 표현한다면 O(h)
- n개의 노드를 가진다면 h = log n에 가까우므로 시간복잡도는 O(log n)
- 한번 실행시마다 50%의 실행할 수도 있던 명령을 제거한다는 의미. 50%의 실행시간 단축
- sorted array이 경우는 O(n)이다
7.2. 이진탐색트리의 단점
- 평균 시간 복잡도는 O(log n)이지만 이는 트리가 균형잡혀 있을 때의 평균 시간 복잡도
- 최악의 경우(만약 한쪽으로만 치우친 극단적인 경우) 링크드리스트 등과 동일한 성능(O(n))
** 패스트캠퍼스의 '알고리즘/기술면접 완전 정복 올인원 패키지' 강의를 듣고 작성한 글입니다
'TIL > Data Structures and Algorithms' 카테고리의 다른 글
공간복잡도 (0) | 2021.01.26 |
---|---|
(자료구조) 힙 (0) | 2021.01.26 |
(자료구조) 해쉬테이블 (0) | 2021.01.22 |
알고리즘 복잡도 (0) | 2021.01.22 |
(자료구조) 링크드리스트 (Linked List) (0) | 2021.01.15 |