グラフをメモリに保存する方法は3つあります。
- オブジェクトとしてのノードとポインタとしてのエッジ
- 番号が付けられたノードxとノードyの間のすべてのエッジの重みを含む行列
- 番号が付けられたノード間のエッジのリスト
3つすべてを書く方法は知っていますが、それぞれの長所と短所をすべて考えたことがあるかどうかはわかりません。
メモリにグラフを保存するこれらの方法それぞれの長所と短所は何ですか?
グラフをメモリに保存する方法は3つあります。
3つすべてを書く方法は知っていますが、それぞれの長所と短所をすべて考えたことがあるかどうかはわかりません。
メモリにグラフを保存するこれらの方法それぞれの長所と短所は何ですか?
回答:
これらを分析する方法の1つは、メモリと時間の複雑さです(グラフへのアクセス方法によって異なります)。
お互いへのポインタを持つオブジェクトとしてノードを保存する
エッジウェイトの行列の保存
グラフで実行するアルゴリズムとノードの数に応じて、適切な表現を選択する必要があります。
考慮すべきいくつかの追加事項:
行列モデルは、重みを行列に格納することにより、重み付けされたエッジを持つグラフに簡単に適用できます。オブジェクト/ポインターモデルは、エッジの重みを並列配列に格納する必要があります。これには、ポインター配列との同期が必要です。
オブジェクト/ポインターモデルは、ポインターをペアで維持する必要があるため、無向グラフよりも有向グラフでうまく機能します。
オブジェクトとポインターの方法は、一部の人が指摘したように、検索の難しさに悩まされていますが、バイナリ検索ツリーの構築など、余分な構造がたくさんある場合にはかなり自然です。
代数グラフ理論のツールを使用してあらゆる種類の問題を非常に簡単にするため、私は隣接行列が個人的に大好きです。(たとえば、隣接行列のk乗は、頂点iから頂点jまでの長さkのパスの数を示します。k乗する前に単位行列を追加して、長さ<= kのパスの数を取得します。ランクを取得します。スパニングツリーの数を取得するためのラプラシアンのn-1マイナー...など)
しかし、隣接行列はメモリが高いと誰もが言っています!グラフは半分しか正しくありません。グラフのエッジが少ない場合は、スパース行列を使用してこれを回避できます。スパースマトリックスデータ構造は、隣接リストを維持するだけで機能しますが、標準的なマトリックス操作の全範囲を利用できるため、両方の長所があります。
最初の例は少し曖昧だと思います—ノードをオブジェクトとして、エッジをポインタとして。一部のルートノードへのポインタのみを保存することでこれらを追跡できます。その場合、特定のノードへのアクセスが非効率になる可能性があります(ノード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)の雑用になります。これが非常に役立つケースは考えられません。
別のオプションがあります:オブジェクトとしてのノード、オブジェクトとしてのエッジ、各エッジは同時に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ポインタ)が、
構造は、かなり一般的なグラフを表すこともできます:ループ付きの指向性マルチグラフ(つまり、複数の異なるループを含む同じ2つのノード間に複数の異なるエッジを持つことができます-xからxへのエッジ)。
このアプローチの詳細については、こちらをご覧ください。
さて、エッジに重みがない場合、行列はバイナリ配列になる可能性があり、その場合、バイナリ演算子を使用すると、物事を本当に高速に実行できます。
グラフがスパースである場合、オブジェクト/ポインターメソッドの方がはるかに効率的です。特にオブジェクト/ポインターをデータ構造に保持して、それらを単一のメモリチャンクにまとめることも、それらを一緒にしておくための適切な計画、またはその他の方法かもしれません。
隣接リスト-単に接続されたノードのリスト-は、メモリ効率がはるかに優れていますが、おそらく最も遅いようです。
有向グラフを逆にすることで簡単に行列表現と、及び容易隣接リストではなく、オブジェクト/ポインタ表現とそれほど大きくありません。