Bonjour à tous :) Aujourd'hui, je peaufine mes compétences en théorie des graphes et en structures de données. J'ai décidé de faire un petit projet en C++ parce que cela fait longtemps que je ne travaille pas en C++.
Je veux faire une liste de contiguïté pour un graphe dirigé. En d'autres termes, quelque chose qui ressemble à:
0-->1-->3
1-->2
2-->4
3-->
4-->
Ce serait un graphe orienté avec V0 (sommet 0) ayant un bord vers V1 et V3, V1 ayant un bord vers V2 et V2 ayant un bord vers V4, comme ceci:
V0----->V1---->V2---->V4
|
|
v
V3
Je sais que pour ce faire, je devrai créer une liste d'adjacence en C++. Une liste de contiguïté est fondamentalement un tableau de listes chaînées . Ok, voyons du code pseudo 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
}
Comme vous pouvez le constater, ce pseudocode est actuellement loin de la marque. Et c'est ce que je voulais de l'aide - les pointeurs et les structures en C++ n'ont jamais été mon point fort. Tout d'abord, cela prend en compte les sommets pointés par un sommet - mais qu'en est-il du sommet lui-même? Comment puis-je suivre ce sommet? Lorsque je boucle sur le tableau, cela ne me sert à rien de savoir uniquement quels sommets sont pointés, plutôt que de savoir également quels points to eux. Le premier élément de chaque liste devrait probablement être ce sommet, puis les éléments suivants sont les sommets vers lesquels il pointe. Mais alors, comment puis-je accéder à ce premier élément de la liste dans mon programme principal? (désolé si cela est compliqué ou déroutant, je me ferais un plaisir de reformuler).
J'aimerais pouvoir parcourir cette liste de contiguïté pour faire des choses intéressantes avec des graphiques. Par exemple, pour implémenter certains algorithmes de la théorie des graphes (tris, chemins les plus courts, etc.) en utilisant la représentation de liste d'adjacence.
(J'avais aussi une question sur la liste de contiguïté. Qu'est-ce qui est différent de simplement utiliser une liste de tableaux? Pourquoi ne puis-je pas simplement avoir une liste avec un tableau à chaque élément de la liste?)
Vous pouvez utiliser un vecteur dans le noeud, en tant que liste de contiguïté.
class node {
int value;
vector<node*> neighbors;
};
Si le graphique est connu au moment de la compilation, vous pouvez utiliser array , mais c'est "un peu" plus difficile. Si vous connaissez seulement la taille du graphique (au moment de la compilation), vous pouvez faire quelque chose comme ça.
template<unsigned int N>
class graph {
array<node, N> nodes;
}
Pour ajouter un voisin, vous faites quelque chose comme ça (n'oubliez pas la numérotation à partir de zéro):
nodes[i].neighbors.Push_back(nodes+j); //or &nodes[j]
Bien sûr, vous pouvez faire une liste de contiguïté sans pointeur et travailler "au-dessus" d'une table. Alors vous avez vector<int>
dans le noeud et vous poussez le nombre de voisin. Avec les deux représentations du graphique, vous pouvez réaliser tous les algorithmes utilisant la liste de contiguïté.
Et enfin, je pourrais ajouter. Certains utilisent un liste au lieu d'un vecteur, car la suppression se fait dans le temps O (1). Erreur. Pour la plupart des algorithmes, l'ordre dans la liste d'adjacence n'est pas important. Ainsi, vous pouvez effacer n'importe quel élément du vecteur dans O (1) temps. Il suffit de l'échanger avec le dernier élément, pop_back est O (1) complexité. Quelque chose comme ca:
if(i != number_of_last_element_in_list) //neighbors.size() - 1
swap(neighbors[i], neighbor.back());
neighbors.pop_back();
Exemple spécifique (vous avez un vecteur dans le noeud, 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]};
Je crois que c'est clair. De 0
vous pouvez aller à 1
, de 1
à 0
et à lui-même, et (comme dans eg.) De 2
à 0
. C'est un graphique dirigé. Si vous souhaitez être non dirigé, vous devez ajouter aux adresses des voisins du voisin. Vous pouvez utiliser des chiffres au lieu de pointeurs. vector<unsigned int>
dans class node
et en reportant des numéros, pas d'adresses.
Comme nous le savons, vous n’avez pas besoin d’utiliser des pointeurs. En voici un exemple:
Lorsque le nombre de sommets peut changer, vous pouvez utiliser le vecteur de nœuds (vector<node>
) à la place de tableau, et simplement redimensionnement . Le reste reste inchangé. Par exemple:
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);
Mais vous ne pouvez pas effacer un nœud, cela empêche la numérotation! Si vous voulez effacer quelque chose, vous devriez utiliser la liste (list<node*>
) des pointeurs. Sinon, vous devez conserver des sommets inexistants. Ici, l'ordre compte!
En ce qui concerne la ligne nodes.emplace_back(); //adding node
, il est sécurisé avec un graphique sans pointeur. Si vous voulez utiliser des pointeurs, vous aurez principalement ne devriez pas changer la taille du graphique. Vous pouvez modifier accidentellement l'adresse de certains nœuds, tout en ajoutant un sommet, lorsque vector
sera copié vers un nouvel emplacement (espace insuffisant).
Une solution consiste à utiliser reserve , bien qu'il faille connaître la taille maximale du graphe! Mais en fait, je vous encourage à ne pas utiliser vector
pour conserver les sommets, lorsque vous utilisez des pointeurs. Si vous ne connaissez pas l'implémentation, la gestion de la mémoire peut être plus sûre (pointeurs intelligents, par exemple. shared_ptr ou simplement nouvea ).
node* const graph = new node[size]; //<-- It is your graph.
//Here no address change accidentally.
Utiliser vector
comme liste de contiguïté est toujours bien! Il n'y a aucune chance de changer l'adresse du noeud.
Ce n’est peut-être pas une approche très générale, mais c’est comme ça que je gère la liste des adjacences dans la plupart des cas. C++ possède une bibliothèque STL qui prend en charge une structure de données pour une liste chaînée nommée list
.
Supposons que vous ayez des nœuds N
dans le graphique et créez une liste liée pour chaque nœud.
list graph[N];
Maintenant, graph[i]
représente les voisins du noeud i. Pour chaque bord i à j, faites
graph[i].Push_back(j);
Le meilleur confort consiste à ne pas manipuler les pointeurs, donc les erreurs de segmentation.
Pour plus de références http://www.cplusplus.com/reference/list/list/
Je vous suggère d’ajouter dans la structure des noeuds la liste d’adjacences .__ et de définir la structure du graphique en tant que liste de nœuds au lieu de liste de listes d’adjacences :)
struct node {
int vertex;
node* next;
adjList m_neighbors;
};
struct graph{
//List of nodes
};
Je recommanderais l'approche plus générale et simple consistant à utiliser des vecteurs et des paires: #comprendre #comprendre
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 */
Ou style alias (> = 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>;
De là: en utilisant un vecteur et des paires (de la réponse de tejas)
Pour plus d'informations, vous pouvez vous référer à un très bon résumé de topcoder: Allumez c ++ avec STL
Mon approche consisterait à utiliser une carte de hachage pour stocker la liste des nœuds dans le graphique
class Graph {
private:
unordered_map<uint64_t, Node> nodeList;
...
}
La carte prend l'ID du nœud comme clé et le nœud lui-même comme valeur. De cette façon, vous pouvez rechercher un nœud dans le graphique en temps constant.
Le nœud contient la liste d'adjacence, dans ce cas en tant que vecteur c ++ 11. Il pourrait également s'agir d'une liste chaînée, bien que pour ce cas d'utilisation, je ne verrais aucune différence d'efficacité. Peut-être que la liste serait meilleure si vous souhaitez la conserver en quelque sorte.
class Node{
uint64_t id; // Node ID
vector<uint64_t> adjList;
...
}
Avec cette approche, vous devez parcourir la liste de contiguïté puis rechercher la carte sur l'ID pour obtenir le nœud.
En guise d'alternative, vous pourriez avoir un vecteur de pointeurs sur les nœuds voisins. Cela vous donnerait un accès direct aux nœuds voisins, mais vous ne pourriez alors pas utiliser une carte pour conserver tous vos nœuds dans le graphique et vous perdriez la possibilité de rechercher facilement des entrées dans votre graphique.
Comme vous pouvez le constater, la mise en œuvre d’un graphique nécessite de nombreux compromis, tout dépend de vos cas d’utilisation.