

노드 (node)
간선 (edge)
루트 노드 (root node)
형제 노드 (sibling node)
조상 노드 (ancestor node)
서브 트리 (subtree)
자손 노드 (descendant node)
차수 (degree)
- 노드의 차수
- 노드에 연결된 자식 노드 수
- B의 차수 = 2, C의 차수 = 1
- 트리의 차수
- 트리에 있는 노드의 차수 중에서 가장 큰 값
- 트리 T의 차수 = 3
- 단말 노드 (leaf node)
- 차수가 0인 노드. 즉, 자식 노드가 없는 노드

레벨
높이
ex)






우선 이진 트리를 숫자로 표현하면 다음과 같다.

여기서 규칙을 찾을 수 있다.
바로 왼쪽 자식 노드는 (부모 노드 x 2), 오른쪽 자식 노드는 (부모 노드 x 2 + 1) 이다.
이를 활용하여 노드를 표현하면 다음과 같다.

이렇게 인덱스를 활용하여 노드를 표현하고 이를 활용해 해당 노드의 부모 노드와 자식 노드들을 찾을 수 있다!
ex) 이를 활용한 이진 트리 구현
class BinaryTree:
def __init__(self):
self.tree = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L']
def insert(self, value):
self.tree.append(value)
def get_node(self, index):
if index < len(self.tree):
return self.tree[index]
return None
def set_node(self, index, value):
while len(self,tree) < index:
self.tree.append(None)
self.tree[index] = value
def get_left_child(self, index):
left_index = 2 * index
if left_index < len(self.tree):
return self.tree[left_index]
return None
def get_right_child(self, index):
right_index = 2 * index + 1
if right_index < len(self.tree):
return self.tree[right_index]
return None
def get_parent(self, index):
if index = 0:
return None
parent_index = index // 2
return self.tree[parent_index]
리스트를 이용한 이진 트리 표현의 단점



해당 구현들은 재귀의 순서만 바꾸면 쉽게 구현할 수 있다.
ex) 전위 순회 구현
class TreeNode:
def __init__(self, key):
self.left = None
self.right = None
self.val = key
def preorder_traversal(root):
if root:
print(root.val)
preorder_traversal(root.left)
preorder_traversal(root.right)
ex) 중위 순회 구현
class TreeNode:
def __init__(self, key):
self.left = None
self.right = None
self.val = key
def inorder_traversal(root):
if root:
inorder_traversal(root.left)
print(root.val)
inorder_traversal(root.right)
ex) 후위 순회 구현
class TreeNode:
def __init__(self, key):
self.left = None
self.right = None
self.val = key
def postorder_traversal(root):
if root:
postorder_traversal(root.left)
postorder_traversal(root.right)
print(root.val)

동일한 트리에 대해 세가지 순회 방법에 따른 계산 방법


아이템(사물 또는 추상적 개념)들과 이들 사이의 연결 관계를 표현한다.
정점(Vertex): 그래프의 구성요소로 하나의 연결점
간선(Edge): 두 정점을 연결하는 선
차수(Degree): 정점에 연결된 간선의 수
그래프 = 정점(Vertex)들의 집합과 이들을 연결하는 간선(Edge)들의 집합으로 구성된 자료 구조
- V: 정점의 개수, E: 그래프에 포함된 간선의 개수
선형 자료구조나 트리 자료구조로 표현하기 어려운 M:N 관계를 가지는 원소들을 표현하기에 용이하다.
무방향 그래프(Undirected Graph)

방향 그래프(Directed Graph)

가중치 그래프(Weighted Graph)

사이클 없는 방향 그래프(DAG, Directed Acyclic Graph)

트리(Tree)

완전 그래프
정점들에 대해 가능한 모든 간선들을 가진 그래프

부분 그래프
원래 그래프에서 일부의 정점이나 간선을 제외한 그래프
인접(Adjacency)


경로 : 어떤 정점 A에서 시작하여 다른 정점 B로 끝나는 순회로 두 정점 사이를 잇는 간선들을 순서대로 나열할 것.

사이클 : 경로의 시작 정점과 끝 정점이 같음 = 시작한 정점에서 끝나는 경로

이렇게 무향 그래프, 유향 그래프인지에 따라 행렬의 형태가 달라진다.
무향 = 대칭 행렬
유향 = 비대칭 행렬
장점
- 두 정점 사이의 간선이 있는지 확인하는 연산이 O(1)로 빠름
- 구현이 단순
- 정적 그래프에 적합.(정적 그래프: 정점과 간선의 수가 변하지 않음)
단점
- 많은 메모리를 차지함 = O(V^2)
- 간선의 수를 확인하거나 인접한 정점을 나열하는 연산이 느림
사용하기 좋은 상황
- Dense Graph(밀집 그래프)에 적합
- 두 정점 사이에 간선이 있는지 빠르게 확인해야 하는 경우

이렇게 해당 정점과 연결된 정점을 표시. 무향이면 그 표시가 2배가 됨.
장점
- 필요한 공간만 사용하므로 공간 복잡도 O(V + E)
- 인접한 정점을 나열하는 연산이 빠름
단점
- 두 정점 사이에 간선이 있는지 확인하는 연산이 느림 = O(V)
- Linked List로 구현이 복잡
사용하기 좋은 상황
- Sparse Graph(희소 그래프)에 적합
- 그래프가 동적으로 변하는 경우 (정점과 간선이 자주 추가/삭제 되는 경우)
- 인접한 정점을 자주 순회해야 하는 경우

두 정점에 대한 간선 그 자체를 객체로 표현하여 리스트로 저장
장점
- 필요한 간선만 저장하므로 공간 복잡도 O(E)
- 간선을 직접 다루는 연산에 효율적
단점
- 두 정점 사이에 간선이 있는 지 확인하는 연산이 느림 = O(E)
- 특정 정점에 인접한 정점을 찾는 연산이 느림 = O(E)
사용하기 좋은 상황
- 간선 중심 연산을 자주 수행해야 하는 경우. ex) MST

정의
특징
구조
장점
- 배열이나 Linked List와 달라, 삽입/삭제 후에도 데이터가 정렬된 상태를 유지
- 데이터가 균형있게 분포되어 있을 때 평균적으로 탐색/삽입/삭제 연산의 시간 복잡도가 O(logN)
- 동적으로 크기를 조절할 수 있어, 크기가 고정된 배열에 비해 유연성이 높음
단점
- 트리가 한쪽으로 치우치면 (즉, 균형이 맞지 않으면) 최악의 경우, 시간 복잡도가 O(n)이 될 수 있음
- 각 노드의 두 개의 자식 포인터를 저장해야 하므로, 큰 데이터 집합의 경우 메모리 오버헤드가 발생할 수 있음


높이
깊이
루트 노드부터 탐색을 시작하며, 현재 노드값과 찾고자 하는 키를 비교, 그 차에 따라 왼쪽 또는 오른쪽으로 이동.
이를 반복하다 현재 노드값과 찾고자 하는 값이 같으면 탐색 종료
class Node:
def __init__(self, key):
self.key = key
self.left = None
self.right = None
class BST:
def __init__(self):
self.root = None
def search(self, key):
return self._search(self.root, key)
def _search(self, node, key):
if node is None or node.key == key:
return node
if key < node.key:
return self._search(node.left,key)
return self._search(node.right, key)
새로운 노드를 삽입해도 BST의 특징인 "자식노드는 최대 2개"를 유지하면서 삽입할 위치를 찾기위해 루트부터 적절한 위치까지 내려감.
트리의 순서 속성을 유지하기 위해서 새로운 노드는 리프 노드로 삽입
class BST:
def insert(self, key):
if self.root is None:
self.root = Node(key)
else:
self._insert(self.root, key)
def _insert(self, node, key):
if key < node.key:
if node.left is None:
node.left = Node(key)
else:
self._insert(node.left, key)
else:
if node.right is None:
node.right = Node(key)
else:
self._insert(node.right, key)
삭제하려는 노드의 위치와 자식 노드의 유무에 따라 세 가지 경우로 나눠서 처리
1. 삭제할 노드가 리프 노드인 경우 -> 단순히 제거하면 됨

삭제할 노드가 한 개의 자식 노드를 가질 경우 -> 삭제할 노드의 자식노드를 부모 노드에 연결

삭제할 노드가 두 개의 자식 노드를 가질 경우 -> 중위 후속자 또는 중위 전임자 찾기



class BST:
def delete(self, key):
self.root = self._delete(self.root, key)
def _delete(self, node, key):
if node is None:
return node
if key < node.key:
node.left = self._delete(node.left, key)
elif key > node.key:
node.right = self._delete(node.right, key)
else:
if node.left is None:
return node.right
elif node.right is None:
return node.left
temp = self._minValueNode(node.right)
node.key = temp.key
node.right = self._delete(node.right, temp.key)
return node
def _minValueNode(self, node):
current = node
while current.left is not None:
current = current.left
return current
BST의 구조는 삽입되는 데이터의 순서에 따라 결정되기 때문에 특정 패턴으로 삽입되는 경우 불균형이 발생
불균형한 BST의 문제점
불균형 문제를 해결할 수 있는 방법은?