C ++의 그래프 문제에 대해 더 나은 인접 목록 또는 인접 행렬은 무엇입니까? 각각의 장점과 단점은 무엇입니까?
std::list
(또는 더 좋은 방법은 std::vector
).
std::deque
또는 std::set
. 그래프가 시간에 따라 변경되는 방식과 실행하려는 알고리즘에 따라 다릅니다.
C ++의 그래프 문제에 대해 더 나은 인접 목록 또는 인접 행렬은 무엇입니까? 각각의 장점과 단점은 무엇입니까?
std::list
(또는 더 좋은 방법은 std::vector
).
std::deque
또는 std::set
. 그래프가 시간에 따라 변경되는 방식과 실행하려는 알고리즘에 따라 다릅니다.
답변:
문제에 따라 다릅니다.
언급 된 모든 것이 언어에 관계없이 데이터 구조 자체에 관한 것이기 때문에이 대답은 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를 작성하고 노드의 모든 인접 항목을 가져 오려는 경우 코드 한 줄만 있으면됩니다. 그러나 일반적으로 인접 목록을 홍보하지 않습니다.
e = n / s
. 여기서는 s
포인터 크기입니다.
좋아, 그래프에 대한 기본 작업의 시간 및 공간 복잡성을 컴파일했습니다.
아래 이미지는 설명이 필요 없습니다.
그래프가 조밀 할 것으로 예상 할 때 Adjacency Matrix가 선호되는 방식과 그래프가 희소 할 것으로 예상 할 때 Adjacency List가 어떻게 선호되는지 확인하십시오.
나는 몇 가지 가정을했습니다. 복잡성 (시간 또는 공간)에 대한 설명이 필요한지 물어보세요. (예를 들어, 희소 그래프의 경우 새 정점을 추가하면 가장자리가 몇 개만 추가된다고 가정 했으므로 En을 작은 상수로 사용했습니다. 꼭지점.)
실수가 있으면 알려주세요.
찾고있는 것에 따라 다릅니다.
인접 행렬 을 사용하면 두 정점 사이의 특정 가장자리가 그래프에 속하는지 여부에 대한 질문에 빠르게 답할 수 있으며 가장자리를 빠르게 삽입하고 삭제할 수도 있습니다. 단점은 당신이 특히 그래프는 스파 스 특히 매우 비효율적이며 많은 정점과 그래프에 대한 과도한 공간을 사용해야한다는 것입니다.
반면에 인접 목록을 사용 하면 가장자리를 찾기 위해 적절한 목록을 검색해야하므로 주어진 가장자리가 그래프에 있는지 확인하기가 더 어렵지만 공간 효율적입니다.
일반적으로 인접 목록은 대부분의 그래프 응용 프로그램에 적합한 데이터 구조입니다.
n 개의 노드와 m 개의 간선이있는 그래프가 있다고 가정 해 보겠습니다 .
인접 행렬 : n 개의 행과 열 이있는 행렬을 만들고 있으므로 메모리에서 n 2에 비례하는 공간을 차지합니다 . u 와 v 로 명명 된 두 노드 사이에 가장자리가 있는지 확인하는 데 Θ (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)이 필요합니다.
예제 그래프의 인접 목록
C ++로 그래프 분석을보고 있다면 BFS를 포함한 여러 알고리즘을 구현하는 부스트 그래프 라이브러리 가 가장 먼저 시작될 것 입니다.
편집하다
SO에 대한 이전 질문이 도움이 될 것입니다.
ac-boost-undirected-graph-and-traverse-it-in-depth-first-searc h를 만드는 방법
이것은 예를 통해 가장 잘 대답됩니다.
예를 들어 Floyd-Warshall 을 생각해보십시오 . 인접 행렬을 사용해야합니다. 그렇지 않으면 알고리즘이 점근 적으로 느려집니다.
아니면 30,000 개의 꼭지점에 대한 조밀 한 그래프라면 어떻게 될까요? 그런 다음 에지 당 16 비트 (인접 목록에 필요한 최소값)가 아닌 정점 쌍당 1 비트를 저장하므로 인접 행렬이 의미가있을 수 있습니다. 1.7GB가 아닌 107MB입니다.
그러나 DFS, BFS (및 Edmonds-Karp와 같이이를 사용하는 알고리즘), 우선 순위 우선 검색 (Dijkstra, Prim, A *) 등과 같은 알고리즘의 경우 인접 목록은 행렬만큼 좋습니다. 글쎄, 행렬은 그래프가 조밀 할 때 약간의 가장자리를 가질 수 있지만 주목할만한 상수 요소에 의해서만 가능합니다. (얼마나? 실험의 문제입니다.)
an adjacency list is as good as a matrix
그런 경우에 생각 하십니까?
메모리 사용량에 대한 keyser5053의 답변에 추가합니다.
방향성 그래프의 경우 인접 행렬 (에지 당 1 비트)은 n^2 * (1)
메모리 비트를 사용합니다.
A에 대한 완전한 그래프 (64 비트 포인터), 인접성리스트 소비 n * (n * 64)
에서 오버 헤드 제외 메모리 비트.
불완전한 그래프의 경우 인접 목록은 0
목록 오버 헤드를 제외하고 메모리 비트를 사용합니다.
인접 목록의 경우 다음 공식을 사용하여 e
인접 행렬이 메모리에 최적화되기 전에 최대 간선 수 ( ) 를 결정할 수 있습니다 .
edges = n^2 / s
최대 가장자리 수를 결정합니다. 여기서는 s
플랫폼의 포인터 크기입니다.
그래프가 동적으로 업데이트되는 경우 평균 에지 수 (노드 당)를 사용하여이 효율성을 유지할 수 있습니다 n / s
.
64 비트 포인터 및 동적 그래프가있는 일부 예제 (동적 그래프는 변경 후 매번 처음부터 다시 계산하는 대신 변경 후 문제의 솔루션을 효율적으로 업데이트합니다.)
유 방향 그래프 ( n
300)의 경우 인접 목록을 사용하는 노드 당 최적 간선 수는 다음과 같습니다.
= 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
.
Adjacency Matrix 구현에 따라 효율적인 구현을 위해 그래프의 'n'을 더 일찍 알아야합니다. 그래프가 너무 동적이고 때때로 행렬의 확장이 필요한 경우에도 단점으로 간주 될 수 있습니까?
다른 답변이 다른 측면을 다루었 기 때문에 일반 인접 목록 표현의 절충점을 극복하는 방법에 대해서만 설명하겠습니다.
Dictionary 및 HashSet 데이터 구조 를 활용하여 상각 된 일정 시간에 EdgeExists 쿼리를 사용하여 인접 목록에 그래프를 표현할 수 있습니다 . 아이디어는 사전에 정점을 유지하는 것이며 각 정점에 대해 가장자리가있는 다른 정점을 참조하는 해시 세트를 유지합니다.
이 구현에서 한 가지 사소한 절충안은 일반 인접 목록 에서처럼 O (V + E) 대신 공간 복잡성 O (V + 2E)가 있다는 것입니다. 왜냐하면 가장자리는 여기에서 두 번 표현되기 때문입니다 (각 정점에는 자체 해시 세트가 있기 때문). 가장자리). 그러나 AddVertex , AddEdge , RemoveEdge 와 같은 작업 은 인접 행렬과 같은 O (V)를 취하는 RemoveVertex 를 제외하고는이 구현에서 상각 된 시간 O (1)에서 수행 할 수 있습니다 . 이는 구현의 단순성 외에 인접 매트릭스에는 특별한 이점이 없음을 의미합니다. 이 인접 목록 구현에서 거의 동일한 성능으로 희소 그래프의 공간을 절약 할 수 있습니다.
자세한 내용은 Github C # 저장소의 아래 구현을 살펴보세요. 가중치 그래프의 경우 가중치 값을 수용하기 위해 사전-해시 세트 조합 대신 중첩 사전을 사용합니다. 유 방향 그래프의 경우와 유사하게 내부 및 외부 에지에 대한 별도의 해시 세트가 있습니다.
참고 : 지연 삭제를 사용하면 해당 아이디어를 테스트하지 않았지만 RemoveVertex 작업을 O (1) 분할로 더 최적화 할 수 있다고 생각합니다. 예를 들어, 삭제시 사전에서 삭제 된 것으로 정점을 표시 한 다음 다른 작업 중에 고아 가장자리를 느리게 지 웁니다.