C ++でのグラフの問題について、隣接リストまたは隣接行列は何が良いですか?


129

C ++でのグラフの問題について、隣接リストまたは隣接行列は何が良いですか?それぞれの長所と短所は何ですか?


21
使用する構造は言語に依存しませんが、解決しようとしている問題に依存します。
avakar 2010

1
私はdjikstraアルゴリズムのような一般的な使用を意味しました、私はこの質問をしました原因は、隣接行列よりもコーディングが難しい原因であるリンクリストの実装が試してみる価値があるのか​​わかりません。
magiix

C ++のリストは、入力するのと同じくらい簡単ですstd::list(または、さらに優れていますがstd::vector)。
avakar 2010

1
@avakar:またはstd::dequeまたはstd::set。それは、グラフが時間とともにどのように変化するか、そしてどのアルゴリズムで実行するかによって異なります。
アレクサンドル

回答:


125

問題によって異なります。

隣接マトリックス

  • O(n ^ 2)メモリを使用

  • 任意の2つのノードO(1)間の特定のエッジの有無を調べて確認するのが高速です。
  • すべてのエッジを反復するのは遅い
  • ノードの追加/削除には時間がかかります。複雑な操作O(n ^ 2)
  • 新しいエッジO(1)を追加するのが速い

隣接リスト

  • メモリ使用量はエッジの数(ノードの数ではなく)に依存し
    ます。隣接行列がスパースの場合、メモリを大幅に節約できます。
  • 2つのノード間の特定のエッジの有無の検出は
    、行列O(k)の場合よりも少し遅くなります。ここで、kは隣接ノードの数です
  • すべてのノードネイバーに直接アクセスできるため、すべてのエッジを反復処理するのが高速です。
  • ノードの追加/削除は高速です。行列表現よりも簡単
  • 新しいエッジO(1)を追加するのは速い

リンクされたリストはコーディングが困難ですが、実装はそれを学ぶのに時間をかける価値があると思いますか?
magiix

11
@magiix:はい、必要に応じてリンクリストのコーディング方法を理解する必要があると思いますが、ホイールを再発明しないことも重要です:cplusplus.com/reference/stl/list
Mark Byers

誰でもリンクリスト形式で幅の最初の検索のためのクリーンなコードを含むリンクを提供できますか?
magiix


78

言語に関係なく、言及されているすべてがデータ構造自体に関するものであるため、この答えはC ++だけのものではありません。そして、私の答えは、隣接リストと行列の基本構造を知っていると仮定しています。

記憶

メモリが主な関心事である場合は、次の式に従ってループを許可する単純なグラフを作成できます。

隣接行列は、N占める2 /8バイトの空間(エントリごとに1ビット)。

隣接リストは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を記述し、ノードのすべてのネイバーをフェッチしたい場合、それらは1行のコードだけです。ただし、隣接リストは一般的に宣伝していません。


9
洞察のために+1。ただし、これは隣接リストの保存に使用される実際のデータ構造によって修正する必要があります。各頂点の隣接リストをマップまたはベクトルとして保存することができます。その場合、数式の実際の数値を更新する必要があります。また、同様の計算を使用して、特定のアルゴリズムの時間の複雑さについて損益分岐点を評価できます。
アレクサンドル

3
ええ、この式は特定のシナリオ用です。大まかな答えが必要な場合は、この式を使用するか、必要に応じて仕様に応じて変更します(たとえば、最近のほとんどの人は64ビットコンピュータを使用しています:))
キーヤー

1
興味のある方のために、ブレークポイント(nノードのグラフの平均エッジの最大数)の式はですe = n / s。ここsで、はポインターのサイズです。
deceleratedcaviar

33

さて、グラフの基本的な操作の時間と空間の複雑さをまとめました。
下の画像は一目瞭然です。
グラフが密であると予想される場合は隣接行列がどのように望ましいか、グラフが疎であると予想される場合は隣接リストがどのように望ましいかに注意してください。
私はいくつかの仮定をしました。複雑さ(時間または空間)を明確にする必要があるかどうかを尋ねます。(たとえば、スパースグラフの場合、新しい頂点を追加するとエッジが数個しか追加されないと想定しているため、Enを小さな定数にしています。バーテックス。)

間違いがあったら教えてください。

ここに画像の説明を入力してください


グラフが密なものか疎なものかがわからない場合、隣接リストのスペースの複雑度はO(v + e)となるのは当然でしょうか。

最も実用的なアルゴリズムの場合、最も重要な操作の1つは、特定の頂点から出て行くすべてのエッジを反復処理することです。あなたはそれをあなたのリストに追加したいかもしれません-それはALのためのO(度)とAMのためのO(V)です。
最大

@johnredは、ALの頂点(時間)の追加はO(1)であると言った方がいいのではないか。エッジの追加は、別の操作として処理できます。AMの場合は説明に意味がありますが、それでも、新しい頂点の関連する行と列をゼロに初期化する必要があります。AMであってもエッジの追加は個別に説明できます。
Usman

AL O(V)に頂点を追加するにはどうすればよいですか?新しいマトリックスを作成し、前の値をコピーする必要があります。O(v ^ 2)である必要があります。
Alex_ban 2017年

19

それはあなたが探しているものに依存します。

隣接行列あなたは、2つの頂点間の特定のエッジがグラフに属し、あなたはまた、迅速な挿入およびエッジの欠失を有することができるかどうかについての質問に高速に答えることができます。欠点は、あなたがあなたのグラフがまばらである場合は特に、非常に非効率的である、特に多くの頂点を持つグラフの、過度のスペースを使用しなければならないことです。

一方、隣接リストでは、エッジを見つけるために適切なリストを検索する必要があるため、特定のエッジがグラフに含まれているかどうかを確認することは困難ですが、スペース効率が向上します。

ただし、一般的に、隣接リストはグラフのほとんどのアプリケーションに適切なデータ構造です。


辞書を使用して隣接リストを保存すると、O(1)償却時間にエッジの存在がわかります。
Rohith Yeravothula、2018年

10

n個のノードとm個のエッジを持つグラフがあるとします。

グラフの例
ここに画像の説明を入力してください

隣接行列:行と列の数 がnである行列を作成しているため、メモリ内ではn 2に比例するスペースを使用します。uvという名前の2つのノードの間にエッジがあるかどうかを確認すると、Θ(1)時間かかります。たとえば、(1、2)がエッジであるかどうかのチェックは、コードでは次のようになります。

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

すべてのエッジを識別したい場合、ここで行列を反復処理する必要があります。これには2つのネストされたループが必要で、Θ(n 2)かかります。(行列の上三角部分を使用してすべてのエッジを決定できますが、これもΘ(n 2)になります)

隣接リスト: 各ノードが別のリストも指すリストを作成しています。リストにはn個の要素があり、各要素はこのノードの隣接要素の数と等しいアイテム数のリストを指します(視覚化を向上させるには画像を参照してください)。したがって、n + mに比例するメモリのスペースが必要になります。(u、v)がエッジかどうかを確認するには、O(deg(u))時間かかります。この場合、deg(u)はuの近傍の数と等しくなります。せいぜい、uが指すリストを繰り返し処理する必要があるからです。すべてのエッジを識別するには、Θ(n + m)かかります。

グラフ例の隣接リスト

ここに画像の説明を入力してください
ニーズに応じて選択してください。 評判のせいで、マトリックスのイメージを入れることができませんでした。


7

C ++でグラフ分析を検討している場合、おそらく最初に開始するのは、BFSを含む多数のアルゴリズムを実装するブーストグラフライブラリです。

編集

SOに関するこの前の質問はおそらく役立つでしょう:

How-to-Create-ac-boost-undirected-graph-and-traverse-it-in-depth-first-searc h


このライブラリをチェックしていただきありがとうございます
magiix

ブーストグラフの+1。これは進むべき道です(もちろん、教育目的の場合を除いて)
TristramGräbener11年

5

これは例で最もよく答えられます。

たとえば、フロイドワーシャルを考えてみてください。隣接行列を使用する必要があります。そうしないと、アルゴリズムが漸近的に遅くなります。

または、それが30,000の頂点の密なグラフである場合はどうなりますか?隣接行列は、エッジあたり16ビット(隣接リストに必要な最小値)ではなく、頂点のペアあたり1ビットを格納するため、1.7 GBではなく107 MBになるため、意味がある場合があります。

ただし、DFS、BFS(およびそれを使用するアルゴリズム、Edmonds-Karpなど)、優先度優先検索(Dijkstra、Prim、A *)などのアルゴリズムの場合、隣接リストはマトリックスと同じくらい優れています。まあ、グラフが密である場合、マトリックスにはわずかなエッジがあるかもしれませんが、それは目立たない定数係数によるものだけです。(どれくらいですか?実験の問題です。)


2
DFSやBFSなどのアルゴリズムでは、マトリックスを使用する場合、隣接ノードを見つけるたびに行全体をチェックする必要がありますが、隣接リストにはすでに隣接ノードがあります。なぜあなたan adjacency list is as good as a matrixはそれらの場合にどう思いますか?
realUser404 2018

@ realUser404正確には、行列の行全体をスキャンすることはO(n)操作です。隣接リストは、すべての出力エッジをトラバースする必要があるスパースグラフに適しています。O(d)(d:ノードの次数)でそれを行うことができます。ただし、シーケンシャルアクセスであるため、行列は隣接リストよりもキャッシュパフォーマンスが優れているため、やや密度の高いグラフの場合、行列をスキャンする方が理にかなっています。
Jochem Kuijpers、2018年

3

keyser5053のメモリ使用量に関する回答に追加します。

有向グラフの場合、隣接行列(エッジあたり1ビット)n^2 * (1)はメモリのビットを消費します。

以下のために完全グラフ(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
Saurabh

2

隣接行列の実装によっては、効率的な実装のために、グラフの「n」が以前にわかっている必要があります。グラフが動的すぎて、時々行列を拡張する必要がある場合、それもマイナス面として数えることができますか?


1

隣接行列またはリストの代わりにハッシュテーブルを使用すると、すべての操作(エッジのチェックがであるか、O(1)隣接するすべてのエッジがO(degree)であるかなど)のBig -Oランタイムとスペースが向上します。

ただし、ランタイムとスペースの両方で一定の要素のオーバーヘッドがあります(ハッシュテーブルはリンクリストや配列のルックアップほど高速ではなく、衝突を減らすために適切な量の余分なスペースを取ります)。


1

他の回答が他の側面をカバーしているので、通常の隣接リスト表現のトレードオフを克服することに触れます。

との隣接リストのグラフを表現することが可能であるEdgeExistsのを利用することによって、一定の償却時間のクエリ辞書HashSetのデータ構造。アイデアは、頂点をディクショナリに保持することです。頂点ごとに、エッジを持つ他の頂点を参照するハッシュセットを保持します。

この実装での小さなトレードオフの1つは、エッジがここで2回表されるため、通常の隣接リストのようにO(V + E)ではなくスペースの複雑さO(V + 2E)になることです(各頂点には独自のハッシュセットがあるため)エッジの)。しかし、などの操作AddVertex AddEdgeRemoveEdgeはを除いて、この実装で償却時間O(1)で行うことができるRemoveVertex隣接マトリックス状O(V)となります。これは、実装が単純であることを除いて、隣接行列には特定の利点がないことを意味します。この隣接リストの実装では、ほぼ同じパフォーマンスでスパースグラフのスペースを節約できます。

詳細については、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.