web-dev-qa-db-fra.com

Trois façons de stocker un graphique en mémoire, avantages et inconvénients

Il existe trois façons de stocker un graphique en mémoire:

  1. Les nœuds comme objets et les arêtes comme pointeurs
  2. Une matrice contenant tous les poids de bord entre le nœud numéroté x et le nœud y
  3. Une liste d'arêtes entre les nœuds numérotés

Je sais écrire les trois, mais je ne suis pas sûr d'avoir pensé à tous les avantages et inconvénients de chacun.

Quels sont les avantages et les inconvénients de chacune de ces façons de stocker un graphique en mémoire?

83
Dean J

Une façon de les analyser est en termes de mémoire et de complexité temporelle (qui dépend de la façon dont vous souhaitez accéder au graphique).

Stockage de nœuds en tant qu'objets avec des pointeurs les uns sur les autres

  • La complexité de la mémoire pour cette approche est O(n) car vous avez autant d'objets que de nœuds. Le nombre de pointeurs (vers les nœuds) requis est jusqu'à O (n ^ 2) comme chaque objet nœud peut contenir des pointeurs pour un maximum de n nœuds.
  • La complexité temporelle de cette structure de données est O(n) pour accéder à un nœud donné.

Stockage d'une matrice de poids Edge

  • Ce serait une complexité mémoire de O (n ^ 2) pour la matrice.
  • L'avantage de cette structure de données est que la complexité temporelle pour accéder à un nœud donné est O (1).

Selon l'algorithme que vous exécutez sur le graphique et le nombre de nœuds, vous devrez choisir une représentation appropriée.

47
f64 rainbow

Quelques autres choses à considérer:

  1. Le modèle matriciel se prête plus facilement aux graphiques à arêtes pondérées, en stockant les poids dans la matrice. Le modèle objet/pointeur devrait stocker les poids Edge dans un tableau parallèle, ce qui nécessite une synchronisation avec le tableau de pointeurs.

  2. Le modèle objet/pointeur fonctionne mieux avec les graphes dirigés qu'avec les graphes non orientés car les pointeurs devraient être maintenus par paires, qui peuvent devenir non synchronisés.

10
Barry Fruitman

La méthode des objets et des pointeurs souffre de difficultés de recherche, comme certains l'ont noté, mais sont assez naturelles pour faire des choses comme construire des arbres de recherche binaires, où il y a beaucoup de structure supplémentaire.

Personnellement, j'aime les matrices d'adjacence car elles facilitent beaucoup toutes sortes de problèmes, en utilisant des outils de la théorie des graphes algébriques. (La kième puissance de la matrice d'adjacence donne le nombre de chemins de longueur k du sommet i au sommet j, par exemple. Ajoutez une matrice d'identité avant de prendre la kième puissance pour obtenir le nombre de chemins de longueur <= k. Prenez un rang n-1 mineur du laplacien pour obtenir le nombre d'arbres couvrant ... Et ainsi de suite.)

Mais tout le monde dit que les matrices d'adjacence coûtent cher en mémoire! Ils ne sont qu'à moitié exacts: vous pouvez contourner ce problème en utilisant des matrices clairsemées lorsque votre graphique a peu d'arêtes. Les structures de données à matrice clairsemée font exactement le travail de simplement conserver une liste de contiguïté, mais ont toujours la gamme complète des opérations de matrice standard disponibles, vous offrant le meilleur des deux mondes.

7
sdenton4

Je pense que votre premier exemple est un peu ambigu - les nœuds comme objets et les arêtes comme pointeurs. Vous pouvez garder une trace de ceux-ci en stockant uniquement un pointeur vers un nœud racine, auquel cas l'accès à un nœud donné peut être inefficace (disons que vous voulez le nœud 4 - si l'objet nœud n'est pas fourni, vous devrez peut-être le rechercher) . Dans ce cas, vous perdriez également des parties du graphique qui ne sont pas accessibles à partir du nœud racine. Je pense que c'est le cas f64 que Rainbow suppose quand il dit que la complexité temporelle pour accéder à un nœud donné est O (n).

Sinon, vous pouvez également conserver un tableau (ou une table de hachage) rempli de pointeurs vers chaque nœud. Cela permet à O(1) d'accéder à un nœud donné, mais augmente un peu l'utilisation de la mémoire. Si n est le nombre de nœuds et e est le nombre d'arêtes, la complexité spatiale de cette approche serait O (n + e).

La complexité de l'espace pour l'approche matricielle serait le long de la ligne de O (n ^ 2) (en supposant que les bords sont unidirectionnels). Si votre graphique est clairsemé, vous aurez beaucoup de cellules vides dans votre matrice. Mais si votre graphique est entièrement connecté (e = n ^ 2), cela se compare favorablement à la première approche. Comme le dit RG, vous pouvez également avoir moins de ratés de cache avec cette approche si vous allouez la matrice en un seul morceau de mémoire, ce qui pourrait accélérer le suivi de nombreux bords autour du graphique.

La troisième approche est probablement la plus économe en espace pour la plupart des cas - O(e) - mais ferait de la recherche de tous les bords d'un nœud donné une corvée O(e). Je ne peux pas penser à un cas où cela serait très utile.

7
ajduff574

Jetez un oeil à tableau de comparaison sur wikipedia. Il permet de bien comprendre quand utiliser chaque représentation de graphiques.

4
Innokenty

D'accord, donc si les bords n'ont pas de poids, la matrice peut être un tableau binaire, et l'utilisation d'opérateurs binaires peut faire avancer les choses vraiment, très rapidement dans ce cas.

Si le graphique est clairsemé, la méthode objet/pointeur semble beaucoup plus efficace. Tenir l'objet/les pointeurs dans une structure de données spécifiquement pour les amadouer dans un seul bloc de mémoire peut également être un bon plan, ou toute autre méthode pour les amener à rester ensemble.

La liste d'adjacence - simplement une liste de nœuds connectés - semble de loin la plus efficace en mémoire, mais probablement aussi la plus lente.

Inverser un graphe orienté est facile avec la représentation matricielle, et facile avec la liste d'adjacence, mais pas si grand avec la représentation objet/pointeur.

3
Dean J

Il existe une autre option: les nœuds en tant qu'objets, les arêtes en tant qu'objets, chaque arête étant en même temps dans deux listes doublement liées: la liste de toutes les arêtes sortant du même nœud et la liste de toutes les arêtes entrant dans le même nœud .

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"
};

La surcharge de mémoire est importante (2 pointeurs par nœud et 6 pointeurs par Edge) mais vous obtenez

  • Insertion de noeud O (1)
  • O(1) Edge insertion (given pointers to "from" and "to" nodes)
  • O(1) Edge deletion (given the pointer)
  • O(deg(n)) node deletion (given the pointer)
  • O (deg (n)) recherche de voisins d'un nœud

La structure peut également représenter un graphique assez général: multigraphe orienté avec boucles (c'est-à-dire que vous pouvez avoir plusieurs arêtes distinctes entre les deux mêmes nœuds, y compris plusieurs boucles distinctes - les arêtes allant de x à x).

Une explication plus détaillée de cette approche est disponible ici .

3
6502