Do it! 자료구조와 함께 배우는 알고리즘 입문 | 파이썬편을 읽고 작성된 글입니다.
Github Link : https://github.com/WE-Learning-CS/Data-Structure/tree/main/chap09/02
출처 : https://towardsdatascience.com/8-useful-tree-data-structures-worth-knowing-8532c7231e8c
출처 : https://www.geeksforgeeks.org/combinatorics-ordered-trees/
순서 트리의 노드를 스캔하는 방법은 크게 다음 2가지 이다.
폭 우선 검색, 가로 검색, 수평 검색이라고도 한다.
사진에서 BFS를 검색하는 순서는 다음과 같다
F → B → G→ A →D → I → C→ E→ H
# 출처 : https://favtutor.com/blogs/breadth-first-search-python
graph = {
'5' : ['3','7'],
'3' : ['2', '4'],
'7' : ['8'],
'2' : [],
'4' : ['8'],
'8' : []
}
visited = [] # List for visited nodes.
queue = [] #Initialize a queue
def bfs(visited, graph, node): #function for BFS
visited.append(node)
queue.append(node)
while queue: # Creating loop to visit each node
m = queue.pop(0)
print (m, end = " ")
for neighbour in graph[m]:
if neighbour not in visited:
visited.append(neighbour)
queue.append(neighbour)
# Driver Code
print("Following is the Breadth-First Search")
bfs(visited, graph, '5') # function calling
장점
단점
세로 검색, 수직 검색이라고도 한다.
깊이 우선 검색은 다음과 같이 세 종류의 스캔 방법으로 구분한다.
전위 순회(preorder)
전위 순회는 다음과 같은 순서로 스캔한다.
노드 방문 -> 왼쪽 자식 -> 오른쪽 자식
위의 사진에서 트리 전체 스캔은 다음과 같이 진행한다.
F -> B -> A -> D -> C -> E -> G -> I -> H
중위 순회(inorder)
중위 순회는 다음과 같은 순서로 스캔한다.
왼쪽 자식 -> 노드 방문 -> 오른쪽 자식
위의 사진에서 트리 전체 스캔은 다음과 같이 진행한다.
A -> B -> C -> D -> E -> F -> H -> I -> G
후위 순회(postorder)
후위 순회는 다음과 같은 순서로 스캔한다.
왼쪽 자식 -> 오른쪽 자식 -> 노드 방문
위의 사진에서 트리 전체 스캔은 다음과 같이 진행한다.
A -> C -> E -> D -> B -> H -> I -> G -> F
장점
단점
# 출처 : https://favtutor.com/blogs/depth-first-search-python
# Using a Python dictionary to act as an adjacency list
graph = {
'5' : ['3','7'],
'3' : ['2', '4'],
'7' : ['8'],
'2' : [],
'4' : ['8'],
'8' : []
}
visited = set() # Set to keep track of visited nodes of graph.
def dfs(visited, graph, node): #function for dfs
if node not in visited:
print (node)
visited.add(node)
for neighbour in graph[node]:
dfs(visited, graph, neighbour)
# Driver Code
print("Following is the Depth-First Search")
dfs(visited, graph, '5')
노드가 왼쪽 자식
과 오른쪽 자식
만을 갖는 트리를 이진트리라고 한다.
이때 두 자식 가운데 하나 또는 둘 다 존재하지 않는 노드가 있어도 상관 없다.
이진트리는 일반적으로 빠른 검색을 위해서 사용된다.
정렬이 된 array가 O(N)
인 반면 이진 트리는 O(log n)
이다.
루트부터 아래쪽 레벨로 노드가 가득 차 있고, 같은 레벨 안에서 왼쪽부터 오른쪽으로 노드가 채워져 있는 이진 트리를 완전 이진 트리(complete binary tree)라고 한다.
높이가 k인 완전 이진 트리가 가질 수 있는 노드의 수는 최대 2^k+1 - 1
개 이므로 n개의 노드를 저장할 수 있는 완전 이진 트리의 높이는 log n
이다.
이진 검색트리는 위 사진 처럼키의 오름차순으로 노드가 삽입되면 트리의 높이가 깊어지는 단점이 있다.(데이터가 이미 정렬되어 있는 경우)
이럴 경우 선형 리스트 처럼 되어 빠른 검색을 할 수 없어 시간 복잡도가 O(n)
이 나올 수 있다.
이를 방지하고자 높이를 O(log n)
으로 제한하여 고안된 검색 트리를 균형 검색 트리라고 하고 다음과 같은 종류가 있다.
레드 - 블랙 트리는 항상 다음 규칙을 준수해야 한다.
레드
이거나 블랙
이어야 한다.블랙
이어야 한다.블랙
이어야 한다.레드
노드는 연달아 나올 수 없다.레드
노드의 부모는 항상 블랙
노드여야하고, 자식 노드들도 블랙
노드여야 한다.블랙
노드의 자식은 블랙
일 수 있다.블랙
노드의 수는 모두 동일하다.이진 검색 트리는 모든 노드가 다음 조건을 만족해야 한다.
from __future__ import annotations
from typing import Any, Type
class Node:
'''이진 검색 트리의 노드'''
def __init__(self, key: Any, value: Any, left: Node = None, right: Node = None):
self.key = key
self.value = value
self.left = left
self.right = right
class BinarySearchTree:
'''이진 검색 트리'''
def __init__(self):
self.root = None # 루트
def search(self, key: Any) -> Any:
'''키가 key인 노드를 검색'''
p = self.root # 루트 주목
while True:
if p is None: # 더이상 진행할 수 없으면
return None # 검색 실패
if key == p.key : # key와 노드 p의 키가 같으면
return key.value # 검색 성공
elif key < p.key : # key쪽이 작으면
p = p.left # 왼쪽 서브 트리에서 검색
else : # key 쪽이 크면
p = p.right # 오른쪽 서브 트리에서 검색
위의 사진에서 키 값이 7
인 노드를 검색하여 성공하는 과정은 다음과 같다.
위의 사진에서 키 값이 11
인 노드를 검색하여 실패하는 과정은 다음과 같다.
노드를 삽입한 뒤에 트리의 형태가 이진 검색 트리의 조건을 유지해야 한다.
따라서 노드를 삽입할 때에는 검색할 때와 마찬가지로 먼저 삽입할 위치를 찾아낸 뒤에 수행해야 한다.
def add(self, key: Any, value: Any) -> bool:
'''키가 key이고 값이 value인 노드를 삽입'''
def add_node(node: Node, key: Any, value: Any) -> None:
'''node를 루트로 하는 서브트리에 키가 key이고 값이 value인 노드를 삽입'''
if key == node.key:
return False # key가 이진 검색 트리에 이미 존재
elif key < node.key:
if node.left is None:
node.left = Node(key, value, None, None)
else:
add_node(node.left, key, value)
elif key > node.key:
if node.right is None:
node.right = Node(key, value, None, None)
else:
add_node(node.right, key, value)
return True
if self.root is None:
self.root = Node(key, value, None, None)
return True
else:
return add_node(self.root, key, value)
위의 사진에서 키 값이 15
인 노드를 삽입하여 성공하는 과정은 다음과 같다.
노드를 삭제할때는 다음 3가지 경우가 존재한다.
자식 노드가 없는 노드의 삭제
에 따라 노드를 삭제한다.자식 노드가 1개인 노드의 삭제
에 따라 노드를 삭제한다. def remove(self, key: Any) -> bool:
'''키가 key인 노드를 삭제'''
p = self.root # 스캔중인 노드
parent = None # 스캔 중인 노드의 부모 노드
is_left_child = True # p는 parent의 왼쪽 자식 노드인지 확인
while True:
if p is None: # 더이상 진행할 수 없으면
return False # 그 키는 존재하지 않음
if key == p.key: # key와 자식 노드 p의 키가 같으면
break # 검색 성공
else:
parent = p # 가지를 내려가기 전에 부모를 설정
if key < p.key: # key쪽이 작으면
is_left_child = True # 여기서 내려가는 것은 왼쪽 자식
p = p.left # 왼쪽 서브트리에서 검색
else: # key쪽이 크면
is_left_child = False # 여기서 내려가는 것은 오른쪽 자식
p = p.right # 오른쪽 서브 트리에서 검색
if p.left is None: # p 왼쪽에 자식이 없으면
'''자식 노드가 없는 노드를 삭제하는 경우와
자식 노드가 1개인 노드를 삭제하는 경우'''
if p is self.root:
self.root = p.right
elif is_left_child:
parent.left = p.right # 부모의 왼쪽 포인터가 오른쪽 자식을 가리킴
else:
parent.right = p.right # 부모의 오른쪽 포인터가 오른쪽 자식을 가리킴
elif p.right is None: # p에 오른쪽 자식이 없으면
if p is self.root:
self.root = p.left
elif is_left_child:
parent.left = p.left # 부모의 왼쪽 포인터가 왼쪽 자식을 가리킴
else:
parent.right = p.left # 부모의 오른쪽 포인터가 왼쪽 자식을 가리킴
else:
'''자식 노드가 2개인 노드를 삭제하는 경우'''
parent = p
left = p.left # 서브트리 안에서 가장 큰 노드
is_left_child = True
while left.right is not None: # 가장 큰 노드 left를 검색
parent = left
left = left.right
is_left_child = False
p.key = left.key # left의 키를 p로 이동
p.value = left.value # left의 데이터를 p로 이동
if is_left_child:
parent.left = left.left # left를 삭제
else:
parent.right = left.left # left를 삭제
return True