みなさん、こんにちは:)今日は、グラフ理論とデータ構造に関するスキルを磨きます。私はC++で小さなプロジェクトを行うことにしました。これは、C++で働いてからしばらく時間が経過しているためです。
有向グラフの隣接リストを作りたいのですが。つまり、次のようになります。
0-->1-->3
1-->2
2-->4
3-->
4-->
これは、次のように、V0(頂点0)がEdge to V1とV3、V1がEdge to V2、V2がEdge to V4の有向グラフになります。
V0----->V1---->V2---->V4
|
|
v
V3
これを行うには、C++で隣接リストを作成する必要があることを知っています。隣接リストは、基本的にリンクリストの配列です。では、いくつかの疑似C++コードを見てみましょう。
#include <stdio>
#include <iostream>
using namespace std;
struct graph{
//The graph is essentially an array of the adjList struct.
node* List[];
};
struct adjList{
//A simple linked list which can contain an int at each node in the list.
};
struct node {
int vertex;
node* next;
};
int main() {
//insert cool graph theory sorting algorithm here
}
ご存知のように、この疑似コードは現時点では目立たない。そして、それは私がいくつかの助けを求めていたものです-C++のポインターと構造体は私の強力なスーツではありませんでした。まず第一に、これは頂点が指す頂点を処理します-しかし、頂点自体はどうですか?どうすればその頂点を追跡できますか?配列をループするとき、どのポイントが指しているかを知るだけでなく、どのポイントが指しているかを知るだけでは意味がありませんtoそれら。各リストの最初の要素はおそらくその頂点である必要があり、その後の要素はそれが指す頂点です。しかし、メインプログラムでリストのこの最初の要素にアクセスするにはどうすればよいでしょうか。 (これが複雑で混乱している場合は、申し訳ありませんが、言い直します)。
この隣接リストをループして、グラフでいくつかのクールなことを行えるようにしたいと思います。たとえば、隣接リスト表現を使用して、いくつかのグラフ理論アルゴリズム(ソート、最短パスなど)を実装します。
(また、隣接リストについて質問がありました。配列のリストを使用することと何が違うのですか?リスト内の各要素に配列を持つリストを作成できないのはなぜですか?)
隣接リストとして、ノードで vector を使用できます。
class node {
int value;
vector<node*> neighbors;
};
コンパイル時にグラフがわかっている場合は、 array を使用できますが、「少し」難しいです。 (コンパイル時に)グラフのサイズだけがわかっている場合は、そのようなことを行うことができます。
template<unsigned int N>
class graph {
array<node, N> nodes;
}
隣人を追加するには、そのようなことをします(ゼロからの番号付けを忘れないでください):
nodes[i].neighbors.Push_back(nodes+j); //or &nodes[j]
もちろん、ポインタのない隣接リストを作成して、テーブルの「上」で作業できます。ノードにvector<int>
があり、隣人を押しているより。グラフの両方の表現により、隣接リストを使用するすべてのアルゴリズムを実現できます。
そして最後に、追加します。削除がO(1)時間であるため、ベクトルの代わりに list を使用するものもあります。間違い。ほとんどのアルゴリズムでは、隣接リストの順序は重要ではありません。したがって、O(1)時間でベクトルから任意の要素を消去できます。最後の要素と交換するだけです pop_back はO(1)複雑さです。そんな感じ:
if(i != number_of_last_element_in_list) //neighbors.size() - 1
swap(neighbors[i], neighbor.back());
neighbors.pop_back();
具体的な例(ノードにベクターがある場合、C++ 11(!)):
//creation of nodes, as previously
constexpr unsigned int N = 3;
array<node,N> nodes; //or array<node, 3> nodes;
//creating Edge (adding neighbors), in the constructor, or somewhere
nodes[0].neighbors = {&nodes[1]};
nodes[1].neighbors = {&nodes[0], &nodes[1]};
//adding runtime, i,j from user, eg. i = 2, j = 0
nodes[i].neighbors.Push_back(&nodes[j]); //nodes[2].neighbors = {&nodes[0]};
はっきりしていると思います。 0
から1
へ、1
から0
へ、そしてそれ自体へ、そして(例えば)2
から0
へ行くことができます。有向グラフです。無向が必要な場合は、両方のノードにネイバーのアドレスを追加する必要があります。ポインタの代わりに数値を使用できます。 vector<unsigned int>
のclass node
とアドレスのプッシュバック番号。
ご存知のように、ポインタを使用する必要はありません。こちらもその一例です
頂点の数が変わる可能性がある場合は、配列の代わりにノードのベクトル(vector<node>
)を使用し、 resizing を使用できます。残りは変更されません。例えば:
vector<node> nodes(n); //you have n nodes
nodes.emplace_back(); //you added new node, or .resize(n+1)
//here is place to runtime graph generate
//as previously, i,j from user, but now you have 'vector<unsigned int>' in node
nodes[i].neighbors.Push_back(j);
しかし、ノードを消去することはできません、これは番号付け違反です!何かを消去したい場合は、ポインタのリスト(list<node*>
)を使用する必要があります。それ以外の場合は、存在しない頂点を保持する必要があります。ここでは、順序が重要です!
nodes.emplace_back(); //adding node
という行については、ポインタのないグラフでも安全です。ポインタを使用したい場合は、主にグラフのサイズを変更しないでください。 vector
が新しい場所(スペース不足)にコピーされるときに、頂点を追加するときに、いくつかのノードのアドレスを誤って変更する可能性があります。
グラフの最大サイズを知っている必要がありますが、それに対処する1つの方法は reserve を使用することです!しかし実際には、ポインタを使用しているときは、頂点を保持するためにvector
を使用しないことをお勧めします。実装がわからない場合は、セルフメモリ管理(スマートポインター、たとえば shared_ptr または単に new )の方が安全です。
node* const graph = new node[size]; //<-- It is your graph.
//Here no address change accidentally.
隣接リストとしてvector
を使用することは常にfineです!ノードのアドレスを変更することはできません。
これは非常に一般的なアプローチではないかもしれませんが、ほとんどの場合、これが隣接リストの処理方法です。 C++には、list
という名前のリンクリストのデータ構造をサポートするSTLライブラリがあります。
グラフにN
ノードがあるとすると、すべてのノードのリンクリストを作成します。
list graph[N];
今graph[i]
はノードiの近傍を表します。エッジiからjごとに、
graph[i].Push_back(j);
最高の快適さは、セグメンテーションフォールトエラーのようにポインターを処理しないことです。
ノード構造に隣接リストを追加し、グラフ構造を隣接リストのリストではなくノードのリストとして定義することをお勧めします:)
struct node {
int vertex;
node* next;
adjList m_neighbors;
};
struct graph{
//List of nodes
};
ベクトルとペアを使用するより一般的で単純なアプローチをお勧めします。#include #include
typedef std::pair<int, int> ii; /* the first int is for the data, and the second is for the weight of the Edge - Mostly usable for Dijkstra */
typedef std::vector<ii> vii;
typedef std::vector <vii> WeightedAdjList; /* Usable for Dijkstra -for example */
typedef std::vector<vi> AdjList; /*use this one for DFS/BFS */
またはエイリアススタイル(> = C++ 11):
using ii = std::pair<int,int>;
using vii = std::vector<ii>;
using vi = std::vector<int>;
using WeightedAdjList = std::vector<vii>;
using AdjList = std::vector<vi>;
ここから: ベクトルとペアを使用(tejasの回答から)
追加情報については、トップコーダーの非常に優れた要約を参照できます: STL でc ++をパワーアップ
私のアプローチは、ハッシュマップを使用してグラフのノードのリストを保存することです
class Graph {
private:
unordered_map<uint64_t, Node> nodeList;
...
}
マップはノードIDをキーとして取り、ノード自体を値として取ります。このようにして、一定の時間でグラフ内のノードを検索できます。
ノードには隣接リストが含まれます。この場合はc ++ 11ベクトルです。これはリンクされたリストである場合もありますが、この使用例では効率に違いは見られません。どうにかして並べ替えたままにしたい場合は、リストの方が良いでしょう。
class Node{
uint64_t id; // Node ID
vector<uint64_t> adjList;
...
}
このアプローチでは、隣接リストを調べ、IDでマップを検索してノードを取得する必要があります。
別の方法として、隣接ノード自体へのポインターのベクトルを持つことができます。これにより、隣接ノードに直接アクセスできますが、マップを使用してグラフ内のすべてのノードを保持できず、グラフ内のエントリを簡単に検索する可能性が失われます。
ご覧のとおり、グラフを実装する際にはトレードオフの決定がたくさんありますが、すべてユースケースによって異なります。