G = (V, E)
그래프(G)는 정점들의 집합(V, set of Vertices)과 간선들의 집합(E, set of Edges)으로 이뤄진 자료구조이다. 정점은 vertex, node로 간선은 edge, arc, link 등 여러 용어로 표현되기도 한다. 정점을 유의미한 데이터의 단위라고 한다면, 간선은 그 정점들간의 관계(정확히는 "관계가 있음")을 나타내는 도식 표현이다. 앞서 배운 트리 또한 그래프의 한 종류라고 볼 수 있는데, 단 트리는 '단방향성' 그래프이자 '사이클이 존재하지 않는' 그래프라고 할 수 있다.
edge : 간선
e = (노드1, 노드2)
로 표현한다.isolated vertex : 연결된 edge가 없는 정점으로, 아래 그림 또한 그래프이다.
parallel edges : 임의의 두 노드에 대해서 2개 이상의 edge가 있는 경우
loop : 시작 노드와 끝 노드가 같은 edge
위 그림에서 parallel edge와 loop를 찾을 수 있다.
a and b are joined by two parallel edges
그리고 vertex d has a loop
라고 표현할 수 있다.
path : 경로
p = (v5 v2 v4 v1)
length of a path : 경로의 길이
p
: 3p
: 17degree : 분지수
cycle : 사이클
앞서 살펴본 개념을 가지고, 아래 그래프를 파헤쳐 보자..!
V = {v1, v2, v3, v4}
E = {e1, e2, e3, e4, e5, e6}
e1 = (v1, v2), e2 = (v1, v3), e3 = (v2, v3),
e4 = (v2, v4), e5 = (v3, v4), e6 = (v1, v4)
앞서, 그래프의 edge는 두 노드간에 "관계가 있음(incident)"을 나타낸다고 했다.
노드 간 edge의 유무 즉, 그래프의 인접성을 표현하는 방법에는 두 가지가 있는데 바로 인접 행렬과 인접 리스트이다.
(앞서 살펴본 그래프를 인접 행렬과 인접 리스트로 표현하면 위와 같다.)
그래프를 구현하기 위해 필요한 정보는 다음과 같다. (undirected graph)
1) 노드의 갯수
2) 간선의 갯수
3) 각 간선의 양 끝 노드
adjacency_matrix.py
import sys
node, edge = map(int, sys.stdin.readline().split())
adj = [[0 for _ in range(node) for _ in range(node)]]
for _ in range(edge):
src, dest = map(int, sys.stdin.readline().split())
adj[src][dest] = 1
adj[dest][src] = 1
(노드 번호, weight)
로 한다.구현.py
import sys
node, edge = map(int, sys.stdin.readline().split())
adj = [[] for _ in range(node)]
for _ in range(edge):
src, dest = map(int, sys.stdin.readline().split())
adj[src].append(dest)
adj[dest].append(src)
앞서 그래프를 인접 행렬과 인접 리스트로 표현해 보았다.
그래프의 인접성을 표현하는 방식이 왜 두 개나 될까? (물론, 두 개보다 더 많을 것이다..)
바로, 동일한 그래프이더라도 어떤 연산에는 인접 행렬이 또 어떤 때는 인접 리스트가 더 효과적이기 때문이다!
그럼, 그래프의 5가지 기본 연산을 가지고 인접 행렬과 인접 리스트를 비교해 보겠다!
G = (V, E) 에서 |V| = n, |E| = m이라고 할 때
인접리스트
승!👍인접 행렬은 edge가 있는지 없는지에 상관 없이 무조건 n X n 개의 2차원 배열을 만들어 놓고 시작한다. 반면, 인접 리스트는 노드들로 1차원 배열을 만들고, 각 노드마다 edge가 있는 경우를 연결 리스트로 표현한다.(edge의 갯수 * 2 만큼만 더하면 된다.)
따라서, (그래프에 edge가 적은 경우) "메모리 낭비를 줄이고 싶다!" 하면 인접 리스트로 표현하는 걸 권장한다.
※ 추가로,
노드 갯수에 비해 edge가 상당히 적으면 → sparse
/ 상당히 많으면 → dense
하다고 한다.
인접 행렬
승! 👏인접 행렬은 G[u][v] == 1
로 바로 찾을 수 있는 반면, 인접 리스트는 G[u].search(v)
로 u 노드에 인접한 노드들의 인접 리스트에 search() 함수를 써서 찾아야 한다. 만약 u에 연결된 노드가 n-1개라면(최악의 경우) n-1번의 연산을 수행하게 된다.
인접 리스트
승..? 👍인접 리스트는 u에 연결된 head 노드부터 tail 노드까지만 따라가면 끝난다. (즉, 찾는 노드의 갯수는 u에 연결된 노드들만 보면 된다.) 반면 인접 행렬은 1부터 n까지의 모든 노드들을 확인해야 하기 때문에, (edge들의 상황에 따라 다르겠지만) 상대적으로 인접 리스트가 더 효과적이다!
인접 행렬은 G[u][v] = 1
로 할당하면 되고, 인접 리스트는 G[u].pushFront(v)
를 하면 뚝딱 금방 끝난다! (연결 리스트로 표현할 때는 pushFront가 가능하지만, 리스트로 표현하면 append로 해야 한다.)
인접 행렬
승! 👏인접 행렬은 G[u][v] = 0
을 해서 기존 값 1을 0으로 바꾸기만 하면 된다. 반면 인접 리스트는 x = G[u].search(v)
▶ G[u].remove(x)
과정을 거치게 되므로 더 오랜 시간이 걸린다.
정리하자면,
5가지 연산에 대해, 경우에 따라서는 인접 행렬 혹은 인접 리스트가 더 우세할 수 있지만 메모리 측면에서는 인접 리스트가 짱이다!