C ++의 그래프 문제에 대해 더 나은 인접 목록 또는 인접 행렬은 무엇입니까?


129

C ++의 그래프 문제에 대해 더 나은 인접 목록 또는 인접 행렬은 무엇입니까? 각각의 장점과 단점은 무엇입니까?


21
사용하는 구조는 언어가 아니라 해결하려는 문제에 따라 다릅니다.
avakar

1
나는 djikstra 알고리즘과 같은 일반적인 사용을 의미했습니다.이 질문을 던졌습니다.
magiix 2010

C ++의 목록은 타이핑만큼이나 쉽습니다 std::list(또는 더 좋은 방법은 std::vector).
avakar

1
@avakar : 또는 std::deque또는 std::set. 그래프가 시간에 따라 변경되는 방식과 실행하려는 알고리즘에 따라 다릅니다.
Alexandre C.

답변:


125

문제에 따라 다릅니다.

인접 매트릭스

  • O (n ^ 2) 메모리 사용

  • 두 노드 O (1) 사이에 특정 에지의 유무를 조회하고 확인하는 것이 빠릅니다.
  • 모든 가장자리를 반복하는 것이 느립니다.
  • 노드 추가 / 삭제 속도가 느립니다. 복잡한 연산 O (n ^ 2)
  • 새로운 엣지를 추가하는 것이 빠르다 O (1)

인접 목록

  • 메모리 사용량은 에지 수 (노드 수가 아님)에 따라 달라 지므로
    인접 행렬이 희소 한 경우 많은 메모리를 절약 할 수 있습니다.
  • 두 노드 사이에 특정 에지의 유무를 찾는 것은
    행렬 O (k)보다 약간 느립니다. 여기서 k는 이웃 노드의 수입니다.
  • 노드 인접 항목에 직접 액세스 할 수 있으므로 모든 에지에서 반복이 빠릅니다.
  • 노드 추가 / 삭제가 빠릅니다. 행렬 표현보다 쉽습니다.
  • 새로운 에지를 추가하는 것이 빠르다 O (1)

연결 목록은 코딩하기가 더 어렵습니다. 구현이 학습에 시간을 할애 할 가치가 있다고 생각하십니까?
magiix 2010

11
@magiix : 네, 필요한 경우 코드 목록을 연결하는 방법을 이해해야한다고 생각하지만, 바퀴 재발견하지 않는 것도 중요 : cplusplus.com/reference/stl/list
마크 바이어스

누구든지 연결 목록 형식으로 Breadth 첫 번째 검색에 대한 깨끗한 코드로 링크를 제공 할 수 있습니까?
magiix


78

언급 된 모든 것이 언어에 관계없이 데이터 구조 자체에 관한 것이기 때문에이 대답은 C ++에만 국한되지 않습니다. 그리고 내 대답은 인접 목록과 행렬의 기본 구조를 알고 있다고 가정하는 것입니다.

기억

메모리가 주요 관심사 인 경우 루프를 허용하는 간단한 그래프에 대해 다음 공식을 따를 수 있습니다.

인접성 매트릭스는 N 차지하는 2 / 8 바이트의 공간 (엔트리 당 비트).

인접 목록은 8e 공간을 차지합니다. 여기서 e는 에지 수입니다 (32 비트 컴퓨터).

그래프의 밀도를 d = e / n 2 (가장자리 수를 최대 간선 수로 나눈 값)로 정의하면 목록이 행렬보다 더 많은 메모리를 차지하는 "중단 점"을 찾을 수 있습니다.

8E> N 2 / 8 D> 1/64

따라서 이러한 숫자 (여전히 32 비트에만 해당)를 사용하면 중단 점이 1/64에 도달 합니다. 밀도 (e / n 2 )가 1/64보다 크면 메모리를 절약하려면 행렬 이 바람직합니다.

위키피디아 (인접 행렬에 대한 기사)와 다른 많은 사이트 에서 이에 대해 읽을 수 있습니다 .

참고 : 키가 정점 쌍인 해시 테이블을 사용하여 인접 행렬의 공간 효율성을 향상시킬 수 있습니다 (무 방향 전용).

반복 및 조회

인접 목록은 기존 가장자리 만 나타내는 간단한 방법입니다. 그러나 이것은 특정 에지의 검색 속도가 느려질 수 있습니다. 각 목록은 꼭지점의 정도만큼 길기 때문에 목록이 정렬되지 않은 경우 특정 가장자리를 확인하는 최악의 경우 조회 시간이 O (n)이 될 수 있습니다. 그러나 정점의 이웃을 찾는 것은 사소한 일이되며, 희소하거나 작은 그래프의 경우 인접 목록을 반복하는 비용은 무시할 수 있습니다.

반면 인접 행렬은 일정한 조회 시간을 제공하기 위해 더 많은 공간을 사용합니다. 가능한 모든 항목이 존재하므로 인덱스를 사용하여 일정한 시간에 에지의 존재를 확인할 수 있습니다. 그러나 가능한 모든 이웃을 확인해야하므로 이웃 조회는 O (n)을 사용합니다. 명백한 공간 단점은 희소 그래프의 경우 많은 패딩이 추가된다는 것입니다. 이에 대한 자세한 내용은 위의 메모리 토론을 참조하십시오.

여전히 무엇을 사용해야할지 확실하지 않은 경우 : 대부분의 실제 문제는 인접 목록 표현에 더 적합한 희소 및 / 또는 큰 그래프를 생성합니다. 구현하기가 더 어려워 보일 수 있지만 그렇지 않다는 것을 확신합니다. BFS 또는 DFS를 작성하고 노드의 모든 인접 항목을 가져 오려는 경우 코드 한 줄만 있으면됩니다. 그러나 일반적으로 인접 목록을 홍보하지 않습니다.


9
통찰을 위해 +1하지만 인접 목록을 저장하는 데 사용되는 실제 데이터 구조로 수정해야합니다. 각 정점에 대해 인접 목록을 맵이나 벡터로 저장할 수 있습니다.이 경우 수식의 실제 숫자를 업데이트해야합니다. 또한 유사한 계산을 사용하여 특정 알고리즘의 시간 복잡성에 대한 손익분기 점을 평가할 수 있습니다.
Alexandre C.

3
예,이 공식은 특정 시나리오를위한 것입니다. 대략적인 대답을 원하면이 공식을 사용하거나 필요에 따라 사양에 따라 수정하십시오 (예 : 오늘날 대부분의 사람들은 64 비트 컴퓨터를 사용합니다 :))
keyser 2011-04-22

1
관심있는 사람들을 위해 중단 점 (n 개 노드의 그래프에서 평균 간선의 최대 개수)에 대한 공식은입니다 e = n / s. 여기서는 s포인터 크기입니다.
deceleratedcaviar

33

좋아, 그래프에 대한 기본 작업의 시간 및 공간 복잡성을 컴파일했습니다.
아래 이미지는 설명이 필요 없습니다.
그래프가 조밀 할 것으로 예상 할 때 Adjacency Matrix가 선호되는 방식과 그래프가 희소 할 것으로 예상 할 때 Adjacency List가 어떻게 선호되는지 확인하십시오.
나는 몇 가지 가정을했습니다. 복잡성 (시간 또는 공간)에 대한 설명이 필요한지 물어보세요. (예를 들어, 희소 그래프의 경우 새 정점을 추가하면 가장자리가 몇 개만 추가된다고 가정 했으므로 En을 작은 상수로 사용했습니다. 꼭지점.)

실수가 있으면 알려주세요.

여기에 이미지 설명 입력


그래프가 조밀 한 것인지 희소 한 것인지 알 수없는 경우 인접 목록의 공간 복잡성이 O (v + e)라고 말하는 것이 옳습니까?

가장 실용적인 알고리즘의 경우 가장 중요한 작업 중 하나는 주어진 정점을 벗어나는 모든 가장자리를 반복하는 것입니다. 목록에 추가 할 수 있습니다. AL은 O (도)이고 AM은 O (V)입니다.
최대

@johnred는 AL에 대한 정점 (시간)을 추가하는 것이 O (1)라고 말하는 것이 낫지 않습니다. 왜냐하면 정점을 추가 할 때 실제로 가장자리를 추가하지 않기 때문입니다. 모서리 추가는 별도의 작업으로 처리 할 수 ​​있습니다. AM의 경우 고려하는 것이 합리적이지만 거기에서도 새 정점의 관련 행과 열을 0으로 초기화하면됩니다. AM의 경우에도 모서리 추가는 별도로 설명 할 수 있습니다.
우스만

AL O (V)에 정점을 추가하는 방법은 무엇입니까? 새 행렬을 만들고 이전 값을 여기에 복사해야합니다. O (v ^ 2) 여야합니다.
Alex_ban

19

찾고있는 것에 따라 다릅니다.

인접 행렬 을 사용하면 두 정점 사이의 특정 가장자리가 그래프에 속하는지 여부에 대한 질문에 빠르게 답할 수 있으며 가장자리를 빠르게 삽입하고 삭제할 수도 있습니다. 단점은 당신이 특히 그래프는 스파 스 특히 매우 비효율적이며 많은 정점과 그래프에 대한 과도한 공간을 사용해야한다는 것입니다.

반면에 인접 목록을 사용 하면 가장자리를 찾기 위해 적절한 목록을 검색해야하므로 주어진 가장자리가 그래프에 있는지 확인하기가 더 어렵지만 공간 효율적입니다.

일반적으로 인접 목록은 대부분의 그래프 응용 프로그램에 적합한 데이터 구조입니다.


사전을 사용하여 인접 목록을 저장하면 O (1) 상각 시간에 에지의 존재를 알 수 있습니다.
Rohith Yeravothula 2010 년

10

n 개의 노드와 m 개의 간선이있는 그래프가 있다고 가정 해 보겠습니다 .

그래프 예
여기에 이미지 설명 입력

인접 행렬 : n 개의 행과 열 이있는 행렬을 만들고 있으므로 메모리에서 n 2에 비례하는 공간을 차지합니다 . uv 로 명명 된 두 노드 사이에 가장자리가 있는지 확인하는 데 Θ (1) 시간이 걸립니다. 예를 들어 (1, 2)가 가장자리인지 확인하는 것은 코드에서 다음과 같습니다.

if(matrix[1][2] == 1)

모든 모서리를 식별하려면 두 개의 중첩 루프가 필요하고 Θ (n 2 ) 를 사용하는 행렬을 반복해야합니다 . (모든 모서리를 결정하기 위해 행렬의 위쪽 삼각형 부분을 사용할 수 있지만 다시 Θ (n 2 ))

인접 목록 : 각 노드가 다른 목록을 가리키는 목록을 만들고 있습니다. 목록에는 n 개의 요소가 있으며 각 요소는이 노드의 인접 항목 수와 동일한 항목 수를 포함하는 목록을 가리 킵니다 (더 나은 시각화를 위해 이미지보기). 따라서 n + m에 비례하는 메모리 공간을 차지합니다 . (u, v)가 에지인지 확인하는 것은 deg (u)가 u의 이웃 수와 같은 O (deg (u)) 시간이 걸립니다. 기껏해야 u가 가리키는 목록을 반복해야하기 때문입니다. 모든 모서리를 식별하려면 Θ (n + m)이 필요합니다.

예제 그래프의 인접 목록

여기에 이미지 설명 입력
필요에 따라 선택해야합니다. 평판 때문에 매트릭스 이미지를 넣지 못해 미안해


7

C ++로 그래프 분석을보고 있다면 BFS를 포함한 여러 알고리즘을 구현하는 부스트 그래프 라이브러리 가 가장 먼저 시작될 것 입니다.

편집하다

SO에 대한 이전 질문이 도움이 될 것입니다.

ac-boost-undirected-graph-and-traverse-it-in-depth-first-searc h를 만드는 방법


감사합니다.이 라이브러리를 확인하겠습니다
magiix 2010

부스트 그래프에 +1. 이것이 갈 길이다 (물론 교육 목적인 경우는 제외)
Tristram Gräbener 2011 년

5

이것은 예를 통해 가장 잘 대답됩니다.

예를 들어 Floyd-Warshall 을 생각해보십시오 . 인접 행렬을 사용해야합니다. 그렇지 않으면 알고리즘이 점근 적으로 느려집니다.

아니면 30,000 개의 꼭지점에 대한 조밀 한 그래프라면 어떻게 될까요? 그런 다음 에지 당 16 비트 (인접 목록에 필요한 최소값)가 아닌 정점 쌍당 1 비트를 저장하므로 인접 행렬이 의미가있을 수 있습니다. 1.7GB가 아닌 107MB입니다.

그러나 DFS, BFS (및 Edmonds-Karp와 같이이를 사용하는 알고리즘), 우선 순위 우선 검색 (Dijkstra, Prim, A *) 등과 같은 알고리즘의 경우 인접 목록은 행렬만큼 좋습니다. 글쎄, 행렬은 그래프가 조밀 할 때 약간의 가장자리를 가질 수 있지만 주목할만한 상수 요소에 의해서만 가능합니다. (얼마나? 실험의 문제입니다.)


2
DFS 및 BFS와 같은 알고리즘의 경우 행렬을 사용하면 인접한 노드를 찾을 때마다 전체 행을 확인해야하지만 인접한 목록에는 이미 인접한 노드가 있습니다. 왜 an adjacency list is as good as a matrix그런 경우에 생각 하십니까?
realUser404

@ realUser404 정확히, 전체 행렬 행을 스캔하는 것은 O (n) 작업입니다. 인접 목록은 모든 나가는 가장자리를 통과해야 할 때 희소 그래프에 더 적합합니다. O (d) (d : 노드의 정도)에서 수행 할 수 있습니다. 행렬은 순차 액세스로 인해 인접 목록보다 캐시 성능이 우수하므로 다소 조밀 한 그래프의 경우 행렬 스캔이 더 합리적 일 수 있습니다.
Jochem Kuijpers

3

메모리 사용량에 대한 keyser5053의 답변에 추가합니다.

방향성 그래프의 경우 인접 행렬 (에지 당 1 비트)은 n^2 * (1)메모리 비트를 사용합니다.

A에 대한 완전한 그래프 (64 비트 포인터), 인접성리스트 소비 n * (n * 64)에서 오버 헤드 제외 메모리 비트.

불완전한 그래프의 경우 인접 목록은 0목록 오버 헤드를 제외하고 메모리 비트를 사용합니다.


인접 목록의 경우 다음 공식을 사용하여 e인접 행렬이 메모리에 최적화되기 전에 최대 간선 수 ( ) 를 결정할 수 있습니다 .

edges = n^2 / s최대 가장자리 수를 결정합니다. 여기서는 s플랫폼의 포인터 크기입니다.

그래프가 동적으로 업데이트되는 경우 평균 에지 수 (노드 당)를 사용하여이 효율성을 유지할 수 있습니다 n / s.


64 비트 포인터 및 동적 그래프가있는 일부 예제 (동적 그래프는 변경 후 매번 처음부터 다시 계산하는 대신 변경 후 문제의 솔루션을 효율적으로 업데이트합니다.)

유 방향 그래프 ( n300)의 경우 인접 목록을 사용하는 노드 당 최적 간선 수는 다음과 같습니다.

= 300 / 64
= 4

이것을 keyser5053의 공식 d = e / n^2(여기서는 e총 에지 수)에 연결하면 중단 점 ( 1 / s) 아래에 있음을 알 수 있습니다 .

d = (4 * 300) / (300 * 300)
d < 1/64
aka 0.0133 < 0.0156

그러나 포인터에 대한 64 비트는 과도 할 수 있습니다. 대신 16 비트 정수를 포인터 오프셋으로 사용하면 중단 점 전에 최대 18 개의 가장자리를 맞출 수 있습니다.

= 300 / 16
= 18

d = ((18 * 300) / (300^2))
d < 1/16
aka 0.06 < 0.0625

이러한 각 예제는 인접 목록 자체의 오버 헤드를 무시합니다 ( 64*2벡터 및 64 비트 포인터의 경우).


부분을 ​​이해 d = (4 * 300) / (300 * 300)하지 못합니다. 그렇지 d = 4 / (300 * 300)않습니까? 공식은 d = e / n^2.
Saurabh

2

Adjacency Matrix 구현에 따라 효율적인 구현을 위해 그래프의 'n'을 더 일찍 알아야합니다. 그래프가 너무 동적이고 때때로 행렬의 확장이 필요한 경우에도 단점으로 간주 될 수 있습니까?


1

인접 행렬이나 목록 대신 해시 테이블을 사용하면 모든 작업에 대해 더 나은 또는 동일한 big-O 런타임 및 공간을 얻을 수 있습니다 (가장자리를 확인하는 것은 O(1), 모든 인접 엣지를 가져 오는 것은 O(degree)등).

런타임과 공간 모두에 대해 일정한 요소 오버 헤드가 있습니다 (해시 테이블은 연결 목록이나 배열 조회만큼 빠르지 않으며 충돌을 줄이기 위해 상당한 양의 추가 공간을 사용합니다).


1

다른 답변이 다른 측면을 다루었 기 때문에 일반 인접 목록 표현의 절충점을 극복하는 방법에 대해서만 설명하겠습니다.

DictionaryHashSet 데이터 구조 를 활용하여 상각 된 일정 시간에 EdgeExists 쿼리를 사용하여 인접 목록에 그래프를 표현할 수 있습니다 . 아이디어는 사전에 정점을 유지하는 것이며 각 정점에 대해 가장자리가있는 다른 정점을 참조하는 해시 세트를 유지합니다.

이 구현에서 한 가지 사소한 절충안은 일반 인접 목록 에서처럼 O (V + E) 대신 공간 복잡성 O (V + 2E)가 있다는 것입니다. 왜냐하면 가장자리는 여기에서 두 번 표현되기 때문입니다 (각 정점에는 자체 해시 세트가 있기 때문). 가장자리). 그러나 AddVertex , AddEdge , RemoveEdge 와 같은 작업 은 인접 행렬과 같은 O (V)를 취하는 RemoveVertex 를 제외하고는이 구현에서 상각 된 시간 O (1)에서 수행 할 수 있습니다 . 이는 구현의 단순성 외에 인접 매트릭스에는 특별한 이점이 없음을 의미합니다. 이 인접 목록 구현에서 거의 동일한 성능으로 희소 그래프의 공간을 절약 할 수 있습니다.

자세한 내용은 Github C # 저장소의 아래 구현을 살펴보세요. 가중치 그래프의 경우 가중치 값을 수용하기 위해 사전-해시 세트 조합 대신 중첩 사전을 사용합니다. 유 방향 그래프의 경우와 유사하게 내부 및 외부 에지에 대한 별도의 해시 세트가 있습니다.

고급 알고리즘

참고 : 지연 삭제를 사용하면 해당 아이디어를 테스트하지 않았지만 RemoveVertex 작업을 O (1) 분할로 더 최적화 할 수 있다고 생각합니다. 예를 들어, 삭제시 사전에서 삭제 된 것으로 정점을 표시 한 다음 다른 작업 중에 고아 가장자리를 느리게 지 웁니다.


인접 행렬의 경우 정점 제거는 O (V)가 아닌 O (V ^ 2)를 취합니다
Saurabh

예. 그러나 사전을 사용하여 배열 인덱스를 추적하면 O (V)로 내려갑니다. 이 RemoveVertex 구현을 살펴보십시오 .
justcoding121
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.