-
[Python 자료구조 #탐색트리1] 이진탐색트리(BST)Programming 기초/Python 2023. 5. 26. 15:53
* 이진탐색트리(BST, Binary Search Tree)
- 효율적인 탐색을 위한 이진트리 기반의 자료구조이다.(이진탐색트리는 이진트리의 한 종류이다.)
- 트리의 연산들을 보다 단순하게 설계하기 위해 이진탐색트리에서는 보통 중복을 허용하지 않는다.
- 그렇지만, 필요하다면 중복된 키를 허용할 수도 있다.
- 이진탐색트리의 모든 노드들을 중위 순회로 방문하면 오름차순으로 노드를 방문하게 된다.
- 따라서 어느 정도 정렬된 상태를 유지하고 있다고 볼 수 있다.
- 이진탐색트리는 탐색을 위한 자료구조이므로 노드의 데이터는 하나의 엔트리, (탐색키, 키에 대한 값)의 형태가 되어야 한다.
< 이진탐색트리 정의 >
● 모든 노드는 유일한 키를 갖는다.
● 왼쪽 서브트리의 키들은 루트의 키보다 작다.
● 오른쪽 서브트리의 키들은 루트의 키보다 크다.
● 왼쪽과 오른쪽 서브트리도 이진탐색트리이다.* 이진 탐색트리를 위한 노드 클래스
class BSTNode: # 이진탐색트리를 위한 노드 클래스 def __init__ (self, key, value): # 생성자 : 키와 값을 받음 self.key = key # 키(key) self.value = value # 값(value) self.left = None # 왼쪽 자식에 대한 링크 self.right = None # 오른쪽 자식에 대한 링크
* 탐색 연산
1. 키를 이용한 탐색
# 순환함수로 구현한 이진탐색트리 탐색연산 def search_bst(n, key) : # n은 탐색을 시작할 노드, key는 탐색 대상 노드의 key if n == None : return None elif key == n.key: # n의 키 값과 동일 -> 탐색성공 return n elif key < n.key: # key<n의 키 return search_bst(n.left, key) # 순환호출로 왼쪽 서브트리 탐색 else: # key>n의 키 return search_bst(n.right, key) # 순환호출로 오른쪽 서브트리 탐색
# 반복 함수로 구현한 이진탐색트리 탐색연산 def search_bst_iter(n, key) : while n != None : # n이 None이 아닐 때 까지 if key == n.key: # n의 키 값과 동일 -> 탐색 성공 return n elif key < n.key: # key < n의 키 n = n.left # n을 왼쪽 서브트리의 루트로 이동 else: # key > n의 키 n = n.right # n을 오른쪽 서브트리의 루트로 이동 return None # 찾는 키의 노드가 없음
2. 값을 이용한 탐색
- 트리의 모든 노드를 하나씩 검사한다.
- 순회의 방법에는 제한이 없다.
- 모든 노드를 검사해야하기 때문에 효율이 떨어진다.
- 최악의 경우 시간 복잡도는 O(n)이다.
# 값을 이용한 이진탐색트리 탐색연산(preorder 사용) def search_value_bst(n, value) : if n == None : return None elif value == n.value: # n의 value와 동일 -> 탐색성공 return n res = search_value_bst(n.left, value) # 왼쪽서브트리에서 탐색 if res is not None : # 탐색이 성공이면 return res # 결과 반환 else : # 왼쪽에서 탐색 실패이면 return search_value_bst(n.right, value) # 오른쪽을 탐색해 결과 반환
3. 최대/최소 키를 가진 노드 탐색
최대 키는 트리의 가장 오른쪽에 있고, 최소 키는 트리의 가장 왼쪽 노드에 있다.
# 반복 구조를 이용한 최대/최소 값 노드 탐색 구현 def search_max_bst(n) : # 최대 값의 노드 탐색 while n != None and n.right != None: n = n.right return n def search_min_bst(n) : # 최소 값의 노드 탐색 while n != None and n.left != None: n = n.left return n
# 순환구조를 이용한 최대/최소 값 노드 탐색 구현 def search_max_bst(n): if n != None and n.right != None: n = search_max_bst(n.right) return n def search_min_bst(n): if n != None and n.left != None: n = search_min_bst(n.left) return n
# 위 그림의 이진탐색트리를 간단하게 구현하고 최대/ 최소 값의 노드 탐색 방법 적용 d = BSTNode(3, "D") d.left = None d.right = None e = BSTNode(12, "E") e.left = None e.right = None b = BSTNode(7, "B") b.left = d b.right = e f = BSTNode(27, "F") f.left = None f.right = None c = BSTNode(31, "C") c.left = f c.right = None root = BSTNode(18, "A") root.left = b root.right = c Max_k = search_max_bst(root) print(Max_k.key, Max_k.value) Min_k = search_min_bst(root) print(Min_k.key, Min_k.value) ------------------------------ 31 C 3 D
* 삽입 연산
삽입할 노드의 키를 이용한 탐색 과정을 수행했을 때 탐색에 실패한 위치가 바로 새로운 노드를 삽입해야 하는 위치이다.
여기서는 키의 중복을 허용하지 않는다.
# 이진탐색트리 삽입연산(노드를 삽입함) : 순환구조 이용 def insert_bst(r, n): # r은 루트노드, n은 삽입할 노드 if n.key < r.key: # 삽입할 노드의 키가 루트보다 작으면 if r.left is None: # 루트의 왼쪽 자식이 없으면 r.left = n # n은 루트의 왼쪽 자식이 됨. return True else: # 루트의 왼쪽 자식이 있으면 return insert_bst(r.left, n) # 왼쪽 자식에게 삽입하도록 함(순환) elif n.key > r.key: # 삽입할 노드의 키가 루트보다 크면 if r.right is None: # 루트의 오른쪽 자식이 없으면 r.right = n # n은 루트의 오른족 자식이 됨 return True else: # 루트의 오른쪽 자식이 있으면 return insert_bst(r.right, n) # 오른쪽 자식에게 삽입하도록 함(순환) else: # 키가 중복되면 return False # 삽입하지 않음
* 삭제 연산
- 삭제할 노드의 종류에 따라 3가지 경우로 구분된다.
▶case1: 단말 노드의 삭제(가장 간단)
#case 1: 단말노드의 삭제 def delete_bst_case1 (parent, node, root) : if parent is None: # 삭제할 단말 노드가 루트이면 root = None # 공백 트리가 됨 else : if parent.left == node : # 삭제할 노드가 부모의 왼쪽 자식이면 parent.left = None # 부모의 왼쪽 링크를 None else : # 오른쪽 자식이면 parent.right = None # 부모의 오른쪽 링크를 None return root # root가 변경될 수도 있으므로 반환
▶case 2: 자식이 하나인 노드의 삭제
# case2 : 자식이 하나인 노드의 삭제 def delete_bst_case2 (parent, node, root) : if node.left is not None : # 삭제할 노드가 왼쪽 자식만 가짐 child = node.left # child는 왼쪽 자식 else : # 삭제할 노드가 오른쪽 자식만 가짐 child = node.right # child는 오른쪽 자식 if node == root : # 없애려는 노드가 루트이면 root = child # 이제 child가 새로운 루트가 됨 else : if node is parent.left : # 삭제할 노드가 부모의 왼쪽 자식 parent.left = child # 부모의 왼쪽 링크를 변경 else : # 삭제할 노드가 부모의 오른쪽 자식 parent.right = child # 부모의 오른쪽 링크를 변경 return root # root가 변경될 수도 있으므로 반환
▶case 3: 두 개의 자식을 모두 갖는 노드의 삭제(가장 복잡)
- 삭제할 노드를 실제로 삭제하지 않고 노드의 내용만을 적절한 "후계자(successor)"의 내용으로 교체하는 방법
- 후계자 노드는 삭제할 노드와 키값이 비슷한 노드이다.
- 후계자로서, 왼쪽 서브트리의 가장 큰 값(12)이나 오른쪽 서브트리의 가장 작은 값(22)을 가진 노드가 적절하다.
- 후계자 노드 탐색 방법
- 왼쪽 서브트리에서 가장 큰 값은 왼쪽 서브트리의 가장 오른쪽에 있는 노드이고,
- 오른쪽 서브트리에서 가장 작은 값은 오른쪽 서브트리의 가장 왼쪽에 있는 노드이다.
- 두 개의 후계자 중 어떤 것을 선택해도 괜찮다.
- 앞서 설명한 최대/최소 키 탐색 방법을 사용한다.
- 삭제할 노드의 후계자(succ, 22)와 그의 부모노드(succp, 26)를 찾는다.
- 후계자의 자식을 후계자 부모의 자식으로 연결한다.
- 후계자(succ)의 데이터(키와 값)를 모두 삭제할 노드(node, 18)에 복사한다.
# case3 : 두 개의 자식을 모두 갖는 노드의 삭제 ( 후보자 = 오른쪽 서브트리의 최소 키) def delete_bst_case3 (parent, node, root) : # parent는 여기서 쓰이지 않지만 앞선 delete_bst_case의 삭제 연산들과의 일관성을 위해 삭제하지 않고 넣음 succp = node # 후계자의 부모 노드(succp)를 node로 초기화 succ = node.right # 후계자 노드(succ)를 node.right로 초기화 while (succ.left != None) : # 후계자와 부모노드 탐색에 최소 키 탐색 방법 적용 succp = succ succ = succ.left if (succp.left == succ) : # 후계자가 왼쪽 자식이면 succp.left = succ.right # 후계자의 오른쪽 자식을 후계자의 부모에 연결 else : # 후계자가 오른쪽 자식이면(후계자가 왼쪽 자식을 가지고 있지 않은 경우) succp.right = succ.right # 후계자의 오른쪽 자식을 후계자의 부모에 연결 node.key = succ.key # 후계자의 키와 값을 node.value= succ.value # 삭제할 노드에 복사 return root # 일관성을 위해 root 반환
▶모든 경우에 대한 삭제연산
- 먼저 삭제할 노드 node와 부모 노드 parent를 찾아야 한다.
# 이진탐색트리 삭제연산(노드를 삭제함) def delete_bst (root, key) : if root == None : return None # 공백 트리 parent = None # 삭제할 노드의 부모 변수 초기화 node = root # 삭제할 노드 변수 root로 초기화 while node != None and node.key != key : # parent 탐색 parent = node if key < node.key : node = node.left else : node = node.right if node == None : return None # 삭제할 노드를 못 찾는 경우 if node.left == None and node.right == None: # case 1: 단말 노드 삭제 root = delete_bst_case1 (parent, node, root) elif node.left==None or node.right==None : # case 2: 유일한 자식 root = delete_bst_case2 (parent, node, root) else : # case 3: 두 개의 자식 root = delete_bst_case3 (parent, node, root)
* 이진탐색트리의 성능 분석
- 좌우의 서브 트리가 균형을 이루고 있는 경우에는 O(log_2 n)이고,
- 한쪽으로 치우치는 완전한 경사 이진트리일 경우에는 트리의 높이 h가 n과 같아진다.
- 이진탐색트리의 효율을 높이기 위해서는 가능한 한 트리가 좌우로 균형 잡히게 만들어야 한다.
- 이진탐색트리의 높이를 log_2 n으로 한정시키는 균형기법에는 AVL트리가 있다.
'Programming 기초 > Python' 카테고리의 다른 글
[Python 자료구조 #탐색트리3] AVL트리, AVL트리를 이용한 맵 (0) 2023.05.26 [Python 자료구조 #탐색트리2] 이진탐색트리를 이용한 맵 (0) 2023.05.26 [Python 자료구조 #트리3] 힙(Heap), 허프만 코드 (0) 2023.05.25 [Python 자료구조 #트리2] 모르스 코드 결정트리 (0) 2023.05.25 [Python 자료구조 #트리1] 이진 트리, 전위/중위/후위/레벨 순회 (1) 2023.05.25