グラフをメモリに保存する3つの方法、利点と欠点


90

グラフをメモリに保存する方法は3つあります。

  1. オブジェクトとしてのノードとポインタとしてのエッジ
  2. 番号が付けられたノードxとノードyの間のすべてのエッジの重みを含む行列
  3. 番号が付けられたノード間のエッジのリスト

3つすべてを書く方法は知っていますが、それぞれの長所と短所をすべて考えたことがあるかどうかはわかりません。

メモリにグラフを保存するこれらの方法それぞれの長所と短所は何ですか?


3
グラフが非常に接続されているか非常に小さい場合にのみ、マトリックスを検討します。疎に接続されたグラフの場合、オブジェクト/ポインターまたはエッジのリストのアプローチはどちらも、はるかに優れたメモリ使用を提供します。ストレージ以外に見落としてきたものに興味があります。;)
sarnold、2010

2
それらは時間の複雑さも異なり、行列はO(1)であり、他の表現は、探しているものに応じて大きく異なる可能性があります。
msw 2010

1
ポインタのリストよりもグラフを行列として実装することのハードウェアの利点について説明した記事をしばらく読んだことを思い出します。あなたがメモリの連続したブロックを扱っているとき、いつでもあなたのワーキングセットの多くが非常によくL2キャッシュにあるかもしれないことを除いて、私はそれについて多くを思い出すことができません。一方、ノード/ポインタのリストはメモリを介してショットガンされる可能性があり、キャッシュにヒットしないフェッチが必要になる可能性があります。同意するかどうかはわかりませんが、興味深い考えです。
nerraga 2010

1
@Dean J:「オブジェクトとしてのノードとポインタ表現としてのエッジ」についての質問。オブジェクトにポインタを格納するためにどのデータ構造を使用しますか?リストですか?
Timofey

4
一般的な名前は次のとおりです。(1)隣接リストと同等、(2)隣接行列、(3)エッジリスト
Evgeni Sergeev 2016年

回答:


51

これらを分析する方法の1つは、メモリと時間の複雑さです(グラフへのアクセス方法によって異なります)。

お互いへのポインタを持つオブジェクトとしてノードを保存する

  • このアプローチのメモリの複雑さはO(n)です。これは、ノードと同じ数のオブジェクトがあるためです。各ノードオブジェクトには最大n個のノードのポインタが含まれる可能性があるため、必要な(ノードへの)ポインタの数は最大でO(n ^ 2)です。
  • このデータ構造の時間の複雑さは、任意のノードにアクセスするためのO(n)です。

エッジウェイトの行列の保存

  • これは、行列のO(n ^ 2)のメモリの複雑さになります。
  • このデータ構造の利点は、特定のノードにアクセスするための時間の複雑さがO(1)であることです。

グラフで実行するアルゴリズムとノードの数に応じて、適切な表現を選択する必要があります。


3
ノードを別の配列に格納している場合、オブジェクト/ポインターモデルでの検索の時間の複雑さはO(n)だけだと思います。それ以外の場合は、目的のノードを検索してグラフをトラバースする必要があります。任意のグラフのすべてのノード(必ずしもすべてのエッジではない)をトラバースすることは、O(n)では実行できません。
バリーフルーツマン2013

@BarryFruitman私はあなたが正しいと確信しています。BFSはO(V + E)です。また、他のノードに接続されていないノードを検索している場合、そのノードを見つけることはできません。
WilderField

10

考慮すべきいくつかの追加事項:

  1. 行列モデルは、重みを行列に格納することにより、重み付けされたエッジを持つグラフに簡単に適用できます。オブジェクト/ポインターモデルは、エッジの重みを並列配列に格納する必要があります。これには、ポインター配列との同期が必要です。

  2. オブジェクト/ポインターモデルは、ポインターをペアで維持する必要があるため、無向グラフよりも有向グラフでうまく機能します。


1
ポインタを無向グラフとペアで維持する必要があるということですか?指示されている場合は、特定の頂点の隣接リストに頂点を追加するだけですが、指示されていない場合は、両方の頂点の隣接リストに1つ追加する必要がありますか?
FrostyStraw 2017

@FrostyStrawはい、そうです。
バリーフルーツマン2017

8

オブジェクトとポインターの方法は、一部の人が指摘したように、検索の難しさに悩まされていますが、バイナリ検索ツリーの構築など、余分な構造がたくさんある場合にはかなり自然です。

代数グラフ理論のツールを使用してあらゆる種類の問題を非常に簡単にするため、私は隣接行列が個人的に大好きです。(たとえば、隣接行列のk乗は、頂点iから頂点jまでの長さkのパスの数を示します。k乗する前に単位行列を追加して、長さ<= kのパスの数を取得します。ランクを取得します。スパニングツリーの数を取得するためのラプラシアンのn-1マイナー...など)

しかし、隣接行列はメモリが高いと誰もが言っています!グラフは半分しか正しくありません。グラフのエッジが少ない場合は、スパース行列を使用してこれを回避できます。スパースマトリックスデータ構造は、隣接リストを維持するだけで機能しますが、標準的なマトリックス操作の全範囲を利用できるため、両方の長所があります。


7

最初の例は少し曖昧だと思います—ノードをオブジェクトとして、エッジをポインタとして。一部のルートノードへのポインタのみを保存することでこれらを追跡できます。その場合、特定のノードへのアクセスが非効率になる可能性があります(ノード4が必要な場合など。ノードオブジェクトが指定されていない場合は、検索する必要があります)。 。この場合、ルートノードから到達できないグラフの部分も失われます。これは、特定のノードにアクセスするための時間の複雑性がO(n)であると彼が言ったときに、f64 rainbowが想定しているケースだと思います。

それ以外の場合は、各ノードへのポインタでいっぱいの配列(またはハッシュマップ)を保持することもできます。これにより、特定のノードへのO(1)アクセスが可能になりますが、メモリ使用量が少し増加します。nがノードの数でeがエッジの数である場合、このアプローチの空間の複雑さはO(n + e)になります。

マトリックスアプローチの空間の複雑さは、O(n ^ 2)の線に沿ったものになります(エッジが一方向であると仮定)。グラフが疎である場合、マトリックスに空のセルがたくさんあります。しかし、グラフが完全に接続されている場合(e = n ^ 2)、これは最初のアプローチと比較して有利です。RGが言うように、マトリックスをメモリの1つのチャンクとして割り当てる場合、このアプローチではキャッシュミスが少なくなる可能性があります。

3番目のアプローチは、ほとんどの場合O(e)でおそらく最もスペース効率が良いですが、特定のノードのすべてのエッジを見つけることはO(e)の雑用になります。これが非常に役立つケースは考えられません。


エッジリストはクラスカルのアルゴリズムにとって自然なものです(「各エッジについて、union-findでルックアップを行います」)。また、スキーナ(第2版、157ページ)は、彼のライブラリCombinatorica(多くのアルゴリズムの汎用ライブラリ)のグラフの基本的なデータ構造としてのエッジリストについて説明しています。彼は、これの理由の1つが、Combinatoricaが住んでいる環境であるMathematicaの計算モデルによって課された制約であると述べています。
Evgeni Sergeev 2016年

5

見てください比較表ウィキペディア上を。これにより、グラフの各表現をいつ使用するかがかなりよく理解できます。


4

別のオプションがあります:オブジェクトとしてのノード、オブジェクトとしてのエッジ、各エッジは同時に2つの二重リンクリストにあります:同じノードから出てくるすべてのエッジのリストと同じノードに入るすべてのエッジのリスト。

struct Node {
    ... node payload ...
    Edge *first_in;    // All incoming edges
    Edge *first_out;   // All outgoing edges
};

struct Edge {
    ... edge payload ...
    Node *from, *to;
    Edge *prev_in_from, *next_in_from; // dlist of same "from"
    Edge *prev_in_to, *next_in_to;     // dlist of same "to"
};

メモリのオーバーヘッドは大きい(ノードあたり2ポインタ、エッジあたり6ポインタ)が、

  • O(1)ノード挿入
  • O(1)エッジの挿入(「from」ノードと「to」ノードへのポインターを指定)
  • O(1)エッジの削除(ポインターを指定)
  • O(deg(n))ノードの削除(ポインターを指定)
  • O(deg(n))ノードの近傍を見つける

構造は、かなり一般的なグラフを表すこともできます:ループ付きの指向性マルチグラフ(つまり、複数の異なるループを含む同じ2つのノード間に複数の異なるエッジを持つことができます-xからxへのエッジ)。

このアプローチの詳細については、こちらをご覧ください


3

さて、エッジに重みがない場合、行列はバイナリ配列になる可能性があり、その場合、バイナリ演算子を使用すると、物事を本当に高速に実行できます。

グラフがスパースである場合、オブジェクト/ポインターメソッドの方がはるかに効率的です。特にオブジェクト/ポインターをデータ構造に保持して、それらを単一のメモリチャンクにまとめることも、それらを一緒にしておくための適切な計画、またはその他の方法かもしれません。

隣接リスト-単に接続されたノードのリスト-は、メモリ効率がはるかに優れていますが、おそらく最も遅いようです。

有向グラフを逆にすることで簡単に行列表現と、及び容易隣接リストではなく、オブジェクト/ポインタ表現とそれほど大きくありません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.