자료구조와 알고리즘을 천천히, 차근차근 진도를 나가서 오늘 완강하였다. 하지만 계속 복습을 하고 내 것으로 만들 예정이다.
Binary Search Tree라고도 부르며, 모든 노드의 왼쪽 자식 노드에는 노드의 데이터보다 작은 값이, 오른쪽 자식 노드에는 노드의 데이터보다 큰 값이 들어가는 등 규칙이 있어 데이터를 Tree의 높이만큼의 비교 연산을 하면 원하는 데이터를 찾을 수 있어 탐색 트리라고 한다.
시간 복잡도
코드
def insert(self, data):
new_node = Node(data) # 삽입할 데이터를 갖는 새 노드 생성
# 트리가 비었으면 새로운 노드를 root 노드로 만든다
if self.root is None:
self.root = new_node
return
# 여기에 코드를 작성하세요
node_to_compare = self.root
while True:
if new_node.data < node_to_compare.data:
if node_to_compare.left_child is None:
new_node.parent = node_to_compare
node_to_compare.left_child = new_node
break
node_to_compare = node_to_compare.left_child
else:
if node_to_compare.right_child is None:
new_node.parent = node_to_compare
node_to_compare.right_child = new_node
break
node_to_compare = node_to_compare.right_child
시간 복잡도:
코드
def search(self, data):
"""이진 탐색 트리 탐색 메소드, 찾는 데이터를 갖는 노드가 없으면 None을 리턴한다"""
# 여기에 코드를 작성하세요
node_to_compare = self.root
while node_to_compare is not None:
if node_to_compare.data == data:
return node_to_compare
elif node_to_compare.data > data:
node_to_compare = node_to_compare.left_child
else:
node_to_compare = node_to_compare.right_child
return None
삭제할 노드를 탐색하여 찾은 후 트리에서 지우는 연산
경우1: 삭제하려는 노드가 leaf노드일 때
경우2: 삭제하려는 데이터 노드가 하나의 자식 노드만 있을 때
경우3: 삭제하려는 데이터의 노드가 2개의 자식이 있을 때
시간 복잡도:
코드
def delete(self, data):
"""이진 탐색 트리 삭제 메소드"""
node_to_delete = self.search(data) # 삭제할 노드를 가지고 온다
parent_node = node_to_delete.parent # 삭제할 노드의 부모 노드
# 경우 1: 지우려는 노드가 leaf 노드일 때
if node_to_delete.left_child is None and node_to_delete.right_child is None:
if self.root is node_to_delete:
self.root = None
else: # 일반적인 경우
if node_to_delete is parent_node.left_child:
parent_node.left_child = None
else:
parent_node.right_child = None
# 경우 2: 지우려는 노드가 자식이 하나인 노드일 때:
elif node_to_delete.left_child is None: # 지우려는 노드가 오른쪽 자식만 있을 때:
# 지우려는 노드가 root 노드일 때
if node_to_delete is self.root:
self.root = node_to_delete.right_child
self.root.parent = None
# 지우려는 노드가 부모의 왼쪽 자식일 때
elif node_to_delete is parent_node.left_child:
parent_node.left_child = node_to_delete.right_child
node_to_delete.right_child.parent = parent_node
# 지우려는 노드가 부모의 오른쪽 자식일 때
else:
parent_node.right_child = node_to_delete.right_child
node_to_delete.right_child.parent = parent_node
elif node_to_delete.right_child is None: # 지우려는 노드가 왼쪽 자식만 있을 때:
# 지우려는 노드가 root 노드일 때
if node_to_delete is self.root:
self.root = node_to_delete.left_child
self.root.parent = None
# 지우려는 노드가 부모의 왼쪽 자식일 때
elif node_to_delete is parent_node.left_child:
parent_node.left_child = node_to_delete.left_child
node_to_delete.left_child.parent = parent_node
# 지우려는 노드가 부모의 오른쪽 자식일 때
else:
parent_node.right_child = node_to_delete.left_child
node_to_delete.left_child.parent = parent_node
# 경우 3: 지우려는 노드가 2개의 자식이 있을 때
else:
successor = self.find_min(node_to_delete.right_child) # 삭제하려는 노드의 successor 노드 받아오기
node_to_delete.data = successor.data # 삭제하려는 노드의 데이터에 successor의 데이터 저장
# successor 노드 트리에서 삭제
if successor is successor.parent.left_child: # successor 노드가 오른쪽 자식일 때
successor.parent.left_child = successor.right_child
else: # successor 노드가 왼쪽 자식일 때
successor.parent.right_child = successor.right_child
if successor.right_child is not None: # successor 노드가 오른쪽 자식이 있을 떄
successor.right_child.parent = successor.parent
그래프는 데이터간 연결 관계를 나타낼 때 사용하는 자료구조이다. 네트워크 통신, 위치 데이터, sns 친구관계 데이터 등을 처리할 때 많이 사용한다. 그래프를 구성하는 두 가지 요소는 노드와 엣지라고 부른다.
노드: 하나의 데이터 단위를 나타내는 객체
엣지: 그래프에서 두 노드의 직접적인 연결 관계 데이터
그래프 용어
방향 그래프(undirected graph)
가중치 그래프(weighted graph)
그래프 노드 구현
클래스로 구현
class StationNode:
def __init__(self, name, num_exits):
self.name = name
self.num_exits = num_exits
station_0 = StationNode("교대역", 14)
station_1 = StationNode("사당역", 14)
station_2 = StationNode("종로3가역", 16)
station_3 = StationNode("서울역", 16)
# 노드들을 파이썬 리스트에 저장
stations_list = [station_0, station_1, station_2, station_3]
# 노드들을 해시 테이블에 저장(딕셔너리)
stations_dict = {
"교대역": station_0,
"사당역": station_1,
"종로3가역": station_2,
"서울역": station_3
}
그래프 엣지 구현: 인접 행렬
인접 행렬은 각 행, 열의 인덱스를 노드의 인덱스로 생각하고, 행렬의 각 셀 데이터에 두 노드의 인접 여부를 1 또는 0으로 저장하는 2차원 배열이다.
예를 들어 3번 노드와 4번노드가 인접해 있다면(사이에 엣지가 존재한다면) 인접행렬의 (3, 4)셀 에는 1이 저장된다. 마찬가지로 (4, 3)셀에도 1이 저장된다.
가중치 그래프의 경우 각 셀에 엣지의 가중치가 저장된다.
방향 그래프의 경우, (3, 4)셀과 (4, 3)셀이 다를 수 있다. 3→4 엣지는 (3, 4)셀에 저장되고 4→3엣지는 (4, 3)셀에 저장된다.
그래프 엣지 구현: 인접 리스트
인접 리스트는 각 노드마다 인접한 노드를 배열로 각각 가지게 하는 것이다. 클래스로 구현한 노드에 adjacent_list와 같은 프로퍼티를 추가해줌으로써 구현할 수 있다.
엣지를 인접 리스트로 구현하면 최악의 경우(우리가 보는 노드가 모든 노드들과 연결되어 있다면) 의 시간복잡도로 인접노드들을 전부 확인할 수 있다. 하지만 이것은 최악의 경우일 뿐 평균은 훨씬 더 적은 연산으로 알아낼 수 있다. 반대로 인접 행렬을 사용하면 인접 노드들을 모두 확인하려면 만큼 걸린다. 따라서 노드의 인접 노드들을 전부 확인하는 연산이 많다면 인접 리스트가 조금 더 효율적이라고 할 수 있다.
그래프 탐색이란 하나의 시작점 노드에서 연결된 노드들을 모두 찾는 것을 말한다. 그래프 순회라고도 한다. 그래프 탐색을 통해 그래프에 대한 여러 의미 있는 정보들을 알아낼 수 있다. 예를 들면 하나의 노드에 얼마나 많은 노드가 연결되어 있는지, 또는 두 노드 사이의 최단 경로 등을 알 수 있다.
다음 두 가지 알고리즘을 통해 그래프 탐색이 이루어진다.
시작 노드를 방문 표시 후, 큐에 넣는다
큐에 아무 노드가 없을 때까지:
def bfs(graph, start_node):
"""시작 노드에서 bfs를 실행하는 함수"""
queue = deque() # 빈 큐 생성
# 일단 모든 노드를 방문하지 않은 노드로 표시
for station_node in graph.values():
station_node.visited = False
queue.append(start_node)
while queue:
station = queue.popleft()
for near_station in station.adjacent_stations:
if not near_station.visited:
near_station.visited = True
queue.append(near_station)
시작 노드를 방문 표시 후, 스택에 넣음
스택에 아무 노드가 없을 때까지:
def dfs(graph, start_node):
"""dfs 함수"""
stack = deque() # 빈 스택 생성
# 모든 노드를 처음 보는 노드로 초기화
for station_node in graph.values():
station_node.visited = 0
start_node.visited = 1
stack.append(start_node)
while stack:
station = stack.pop()
station.visited = 2
for near_station in station.adjacent_stations:
if near_station.visited == 0:
near_station.visited = 1
stack.append(near_station)
그래프에서 최단 경로란, 두 노드를 이어주는 모든 경로 중 거리가 가장 작은 경로를 말한다.
비 가중치 그래프에서는 BFS알고리즘을 통해 최단경로를 찾을 수 있고, 가중치 그래프에서는 다익스트라 알고리즘을 이용하여 찾을 수 있다.
BFS predecessor 변수
BFS로 최단경로를 찾을 때 필요한 추가 변수
BFS를 수행하며 특정 노드에 오기 직전의 노드
백트래킹 기법을 통해 종착지부터 predecessor를 타고 올라가 시작점을 찾으면 그 경로가 최단 경로이다.
최단 경로용 BFS알고리즘
시작 노드를 방문 표시 후, 큐에 넣음
큐에 아무 노드가 없을 때까지:
백트래킹
def bfs(graph, start_node):
"""최단 경로용 bfs 함수"""
queue = deque() # 빈 큐 생성
# 모든 노드를 방문하지 않은 노드로 표시, 모든 predecessor는 None으로 초기화
for station_node in graph.values():
station_node.visited = False
station_node.predecessor = None
# 시작점 노드를 방문 표시한 후 큐에 넣어준다
start_node.visited = True
queue.append(start_node)
while queue: # 큐에 노드가 있을 때까지
current_station = queue.popleft() # 큐의 가장 앞 데이터를 갖고 온다
for neighbor in current_station.adjacent_stations: # 인접한 노드를 돌면서
if not neighbor.visited: # 방문하지 않은 노드면
neighbor.visited = True # 방문 표시를 하고
neighbor.predecessor = current_station
queue.append(neighbor) # 큐에 넣는다
def back_track(destination_node):
"""최단 경로를 찾기 위한 back tracking 함수"""
res_str = destination_node.station_name # 리턴할 결과 문자열
current_node = destination_node.predecessor
while current_node is not None:
res_str = current_node.station_name + " " + res_str
current_node = current_node.predecessor
return res_str
stations = create_station_graph("./new_stations.txt") # stations.txt 파일로 그래프를 만든다
bfs(stations, stations["을지로3가"]) # 지하철 그래프에서 을지로3가역을 시작 노드로 bfs 실행
print(back_track(stations["강동구청"])) # 을지로3가에서 강동구청역까지 최단 경로 출력
Dijkstra알고리즘
(a, b)를 relax한다
라고 말한다. 엣지 relaxation을 하려면 a.distance + (a, b)의 weight 와 b.distance를 비교해서 전자가 더 작다면 b.distance를 전자로, b.predecessor를 a로 만들어주면 된다.Link
컴포넌트: a
태그로 렌더링 되며, 사용자의 클릭에 반응하는 라우팅을 구현할 때 사용하면 좋습니다. 사용법은 일반 a태그와 같습니다. 그래서 UI와 함께 많이 사용됩니다.
Navigate
컴포넌트: 사용자의 의사와 상관 없이 컴포넌트 렌더링 시 특정 조건을 만족하거나 만족하지 못하면 특정 페이지로 리다이렉션 시킬 때 사용하면 좋습니다. 예를 들면 다음과 같은 코드가 있습니다.
import { Navigate } from 'react-router-dom';
function MyComponent() {
const isAuthenticated = false;
return (
<>
{isAuthenticated ? (
<Navigate to="/dashboard" />
) : (
<Navigate to="/login" replace={true} />
)}
</>
);
}
useNavigate
훅: 특정 코드의 실행 이후에 라우팅을 할 때 사용하면 좋습니다. 예를 들어 결제하기 버튼을 누르면 결제 모듈이 실행되고, 결제가 끝난 후에 결제 완료 페이지나, 다시 홈 페이지로 이동시키는 등 이벤트 핸들러에 많이 사용합니다.
그래프 알고리즘을 공부하다가 시간이 너무 많이 흘러갔다. 다익스트라 알고리즘이 내가 알고있던 방식과 약간 다른 방식으로 나와서 차이를 이해하는데 조금 오래 걸렸던것 같다.