어제는 DFS
, BFS
를 배우기 위한 기초 단계로 스택(Stack)
과 큐(Queue)
에 대해서 배웠다.
오늘은 DFS
, BFS
모두 배우면 좋겠지만,
그럼 머리 아플테니 DFS
먼저 배워보자.
깊이 우선탐색이라고 부르며, 그래프에서 깊은 부분을 우선적으로 탐색하는 알고리즘
DFS
는 stack
자료구조에 기초한다는 점에서 구현이 간단하다.
구현에 앞서 그래프(Graph)
에 대해 배워보자.
그래프는 노드(Node)
와 간선(Edge)
으로 표현되며, 이때 노드를 정점(Vertex)
이라고도 말한다.
그래프 탐색이란 하나의 노드를 시작으로 다수의 노드를 방문하는 것을 말한다. 또한, 두 노드가 간선으로 연결되어 있다면, 두 노드는 인접하다(Adjacent)
라고 표현한다.
프로그래밍에서 그래프는 크게 2가지 방식으로 표현할 수 있는데, 코딩 테스트에서는 이 두 방식 모두 필요하니 두 개념에 대해 숙지하자.
인접 행렬(Adjacency Matrix)
: 2차원 배열로 그래프의 연결 관계를 표현하는 방식인접 리스트(Adjacency List)
: 리스트로 그래프의 연결 관계를 표현하는 방식
2차원 배열에 각 노드가 연결된 형태를 기록하는 방식이다. 위와 같이 연결된 그래프를 인접 행렬로 표현할 때 파이썬에서는 2차원 리스트로 구현 할 수 있다.
연결이 되어 있지 않은 노드끼리는 무한(Infinity)의 비용이라고 작성한다.
실제 코드에서는 논리적으로 정답이 될 수 없는 큰 값으로 초기화하는 경우가 많다.
# 인접 행렬
# 무한 비용선언
INF = 999999999
graph = [
[0, 7, 5],
[7, 0, INF],
[5, INF, 0]
]
print(graph)
👉🏽 [[0, 7, 5], [7, 0, 999999999], [5, 999999999, 0]]
인접리스트 방식에서는 모든 노드에 연결된 노드에 대한 정보를 차례대로 연결하여 저장한다.
인접리스트는 연결 리스트
라는 자료구조를 이용해 구현하는데, 파이썬은 기본 자료형인 리스트 자료형이 append()
와 메소드를 제공하므로, 모두 기본으로 제공한다.
파이썬으로 인접 리스트를 이용해 그래프를 표현하고자 할 때도 단순히 2차원 리스트를 이용하면 되는 점을 기억하자.
#### ⚡️ 인접 리스트(Adjacency List)
# 행(Row)이 3개인 2차원 리스트로 인접 리스트 표현
graph = [[] for _ in range(3)]
# 노드 0에 연결된 노드 정보 저장(노드, 거리)
graph[0].append((1, 7))
graph[0].append((2, 5))
# 노드 1에 연결된 노드 정보 저장(노드, 거리)
graph[1].append((0, 7))
# 노드 2에 연결된 노드 정보 저장(노드, 거리)
graph[2].append((0, 5))
print(graph)
👉🏽 [[(1, 7), (2, 5)], [(0, 7)], [(0, 5)]]
이 두방식의 차이는 어떤점이 있을까?
메모리측면, 속도측면 에서 살펴보자.
❏ 메모리측면
인접 행렬(Adjacency Matrix)
: 모든 관계를 저장하므로 노드 개수가 많을 수록 불필요한 메모리가 소요된다.인접 리스트(Adjacency List)
: 연결된 정보만을 저장하기 때문에 메모리를 효율적으로 사용한다.
❏ 속도측면
인접 행렬(Adjacency Matrix)
: 모든 경우의 수가 제시되어 있으므로 인접리스트보다 빠르다.인접 리스트(Adjacency List)
: 인접행렬방식에 비해 정보를 얻는 속도가 느리다. 인접리스트방식에서는 연결된 데이터를 하나씩 확인해야 하기 때문이다.
DFS의 구체적인 동작 과정은 다음과 같다.
- 탐색 시작 노드를 스택에 삽입하고 방문 처리를 한다.
- 스택의 최상단 노드에 방문하지 않은 인접 노드가 있으면 그 인접 노드를 스택에 넣고 방문 처리를 한다. 방문하지 않은 인접 노드가 없으면 스택에서 최상단 노드를 꺼낸다.
- 2번의 과정을 더 이상 수행할 수 없을 때까지 반복한다.
방문처리(visited)
란 스택에 한 번 삽입되어 처리된 노드가 다시 삽입되지 않게 체크하는것을 의미한다. 방문처리를 함으로써 각 노드를 한 번씩만 처리 할 수 있다.
다음과 같은 그래프에서 DFS
순서를 고려해보자.
결과적으로 노드의 탐색 순서(스택 순서)는 다음과 같다.
👉🏽 1 → 2 → 7 → 6 → 8 → 3 → 4 → 5
깊이 우선 탐색 알고리즘인 DFS
는 스택자료구조에 기초한다는 점에서 구현이 간단하다.
탐색을 수행함에 있어서 데이터의 개수가 N
개인 경우 O(N)
의 시간이 소요된다.
실제로 스택을 쓰지 않고, 재귀 함수를 이용했을 때 매우 간결하게 구현할 수 있다.
다음 코드를 보자.
dfs(graph, v, visited):
visited[v] = True
print(v, end=' ')
for i in graph[v]:
if not visited[i]:
dfs(graph, i, visited)
graph = [
[],
[2, 3, 8],
[1, 7],
[1, 4, 5],
[3, 5],
[3, 4],
[7],
[2, 6, 8],
[1, 7]
]
visited[v] = [False] * 9
dfs(graph, 1, visited)
👉🏽 1 2 7 6 8 3 4 5
지금까지 DFS
에 대해 배워봤는데, 익숙하지 않은 개념들이 많이 나왔다.(재귀함수, 그래프 등등)
BFS
과정으로 넘어가기 전에 그래프, 재귀함수 관련 알고리즘 문제들을 몇 개 풀어본 뒤에 넘어가면 좋을 것 같다.