Was ist besser, Adjazenzlisten oder Adjazenzmatrix für Grafikprobleme in C ++? Was sind die Vor- und Nachteile von jedem?
c++
graph
adjacency-list
adjacency-matrix
magiix
quelle
quelle
std::list
(oder noch besserstd::vector
).std::deque
oderstd::set
. Dies hängt davon ab, wie sich das Diagramm mit der Zeit ändert und welche Algorithmen Sie darauf ausführen möchten.Antworten:
Das hängt vom Problem ab.
Adjazenzmatrix
zwischen zwei beliebigen Knoten vorhanden ist oder nicht. O (1)
Adjazenzliste
Dies kann viel Speicherplatz sparen, wenn die Adjazenzmatrix dünn ist
ist etwas langsamer als bei der Matrix O (k); Dabei ist k die Anzahl der Nachbarknoten
quelle
Diese Antwort gilt nicht nur für C ++, da sich alles, was erwähnt wird, auf die Datenstrukturen selbst bezieht, unabhängig von der Sprache. Meine Antwort geht davon aus, dass Sie die Grundstruktur von Adjazenzlisten und -matrizen kennen.
Erinnerung
Wenn der Speicher Ihr Hauptanliegen ist, können Sie diese Formel für ein einfaches Diagramm befolgen, das Schleifen zulässt:
Eine Adjazenzmatrix nimmt n 2 /8 - Byte - Raum (ein Bit pro Eintrag).
Eine Adjazenzliste belegt 8e Platz, wobei e die Anzahl der Kanten ist (32-Bit-Computer).
Wenn wir die Dichte des Graphen als d = e / n 2 definieren (Anzahl der Kanten geteilt durch die maximale Anzahl der Kanten), können wir den "Haltepunkt" finden, an dem eine Liste mehr Speicherplatz beansprucht als eine Matrix:
8e> n 2 /8 , wenn d> 1/64
Mit diesen Zahlen (immer noch 32-Bit-spezifisch) landet der Haltepunkt also bei 1/64 . Wenn die Dichte (e / n 2 ) größer als 1/64 ist, ist eine Matrix vorzuziehen, wenn Sie Speicher sparen möchten.
Sie können darüber auf Wikipedia (Artikel über Adjazenzmatrizen) und vielen anderen Websites lesen .
Randnotiz : Sie können die Raumeffizienz der Adjazenzmatrix verbessern, indem Sie eine Hash-Tabelle verwenden, bei der die Schlüssel Paare von Eckpunkten sind (nur ungerichtet).
Iteration und Nachschlagen
Adjazenzlisten sind eine kompakte Methode, um nur vorhandene Kanten darzustellen. Dies geht jedoch zu Lasten einer möglicherweise langsamen Suche nach bestimmten Kanten. Da jede Liste so lang ist wie der Grad eines Scheitelpunkts, kann die Suchzeit im ungünstigsten Fall für die Überprüfung einer bestimmten Kante O (n) werden, wenn die Liste ungeordnet ist. Das Nachschlagen der Nachbarn eines Scheitelpunkts wird jedoch trivial, und für ein spärliches oder kleines Diagramm können die Kosten für das Durchlaufen der Adjazenzlisten vernachlässigbar sein.
Adjazenzmatrizen hingegen benötigen mehr Platz, um eine konstante Suchzeit zu gewährleisten. Da jeder mögliche Eintrag vorhanden ist, können Sie mithilfe von Indizes in konstanter Zeit prüfen, ob eine Kante vorhanden ist. Die Nachbarsuche benötigt jedoch O (n), da Sie alle möglichen Nachbarn überprüfen müssen. Der offensichtliche Platznachteil besteht darin, dass bei spärlichen Diagrammen viel Polsterung hinzugefügt wird. Weitere Informationen hierzu finden Sie in der obigen Speicherdiskussion.
Wenn Sie sich immer noch nicht sicher sind, was Sie verwenden sollen : Die meisten Probleme in der realen Welt erzeugen spärliche und / oder große Diagramme, die sich besser für die Darstellung von Adjazenzlisten eignen. Sie scheinen schwieriger zu implementieren zu sein, aber ich versichere Ihnen, dass dies nicht der Fall ist. Wenn Sie ein BFS oder DFS schreiben und alle Nachbarn eines Knotens abrufen möchten, sind sie nur eine Codezeile entfernt. Beachten Sie jedoch, dass ich Adjazenzlisten im Allgemeinen nicht bewerbe.
quelle
e = n / s
, wobeis
die Zeigergröße ist.Okay, ich habe die zeitliche und räumliche Komplexität grundlegender Operationen in Diagrammen zusammengestellt.
Das Bild unten sollte selbsterklärend sein.
Beachten Sie, wie die Adjazenzmatrix vorzuziehen ist, wenn wir erwarten, dass das Diagramm dicht ist, und wie die Adjazenzliste vorzuziehen ist, wenn wir erwarten, dass das Diagramm dünn ist.
Ich habe einige Annahmen gemacht. Fragen Sie mich, ob eine Komplexität (Zeit oder Raum) geklärt werden muss. (Beispiel: Für ein Diagramm mit geringer Dichte habe ich En als kleine Konstante angenommen, da ich davon ausgegangen bin, dass durch Hinzufügen eines neuen Scheitelpunkts nur wenige Kanten hinzugefügt werden, da wir davon ausgehen, dass das Diagramm auch nach dem Hinzufügen dünn bleibt Scheitel.)
Bitte sagen Sie mir, wenn es Fehler gibt.
quelle
Es kommt darauf an, wonach Sie suchen.
Mit Adjazenzmatrizen können Sie schnell Fragen beantworten, ob eine bestimmte Kante zwischen zwei Scheitelpunkten zum Diagramm gehört, und Sie können Kanten schnell einfügen und löschen. Der Nachteil ist, dass Sie übermäßig viel Platz benötigen, insbesondere für Diagramme mit vielen Scheitelpunkten, was besonders dann ineffizient ist, wenn Ihr Diagramm spärlich ist.
Andererseits ist es bei Adjazenzlisten schwieriger zu überprüfen, ob sich eine bestimmte Kante in einem Diagramm befindet, da Sie die entsprechende Liste durchsuchen müssen, um die Kante zu finden, diese jedoch platzsparender sind.
Im Allgemeinen sind Adjazenzlisten jedoch die richtige Datenstruktur für die meisten Anwendungen von Diagrammen.
quelle
Nehmen wir an, wir haben einen Graphen mit n Knoten und m Kanten.
Beispieldiagramm
Adjazenzmatrix: Wir erstellen eine Matrix mit n Zeilen und Spalten, sodass im Speicher Platz benötigt wird, der proportional zu n 2 ist . Die Überprüfung, ob zwischen zwei Knoten mit den Namen u und v eine Kante liegt, dauert Θ (1). Wenn Sie beispielsweise nach (1, 2) suchen, sieht eine Kante im Code wie folgt aus:
Wenn Sie alle Kanten identifizieren möchten, müssen Sie über die Matrix iterieren. Dies erfordert zwei verschachtelte Schleifen und es wird Θ (n 2 ) benötigt. (Sie können einfach den oberen dreieckigen Teil der Matrix verwenden, um alle Kanten zu bestimmen, aber es wird wieder Θ (n 2 ) sein.)
Adjazenzliste: Wir erstellen eine Liste, die jeder Knoten auch auf eine andere Liste verweist. Ihre Liste enthält n Elemente und jedes Element zeigt auf eine Liste mit einer Anzahl von Elementen, die der Anzahl der Nachbarn dieses Knotens entspricht (siehe Bild zur besseren Visualisierung). Es wird also Speicherplatz im Speicher benötigt, der proportional zu n + m ist . Die Überprüfung, ob (u, v) eine Kante ist, dauert O (Grad (u)) Zeit, in der Grad (u) der Anzahl der Nachbarn von u entspricht. Denn höchstens müssen Sie über die Liste iterieren, auf die das u zeigt. Das Identifizieren aller Kanten dauert Θ (n + m).
Adjazenzliste des Beispieldiagramms
Sie sollten Ihre Wahl nach Ihren Bedürfnissen treffen. Aufgrund meines Rufs konnte ich kein Bild von der Matrix erstellen, tut mir leid
quelle
Wenn Sie sich die Diagrammanalyse in C ++ ansehen, ist der erste Startpunkt wahrscheinlich die Boost-Diagrammbibliothek , die eine Reihe von Algorithmen einschließlich BFS implementiert.
BEARBEITEN
Diese vorherige Frage zu SO wird wahrscheinlich helfen:
Wie erstelle ich einen AC-Boost-ungerichteten Graphen und durchquere ihn in der Tiefe? Erstes Suchen h
quelle
Dies lässt sich am besten mit Beispielen beantworten.
Denken Sie zum Beispiel an Floyd-Warshall . Wir müssen eine Adjazenzmatrix verwenden, sonst ist der Algorithmus asymptotisch langsamer.
Oder was ist, wenn es sich um ein dichtes Diagramm auf 30.000 Eckpunkten handelt? Dann könnte eine Adjazenzmatrix sinnvoll sein, da Sie 1 Bit pro Scheitelpunktpaar speichern und nicht die 16 Bits pro Kante (das Minimum, das Sie für eine Adjazenzliste benötigen würden): Das sind 107 MB statt 1,7 GB.
Für Algorithmen wie DFS, BFS (und diejenigen, die es verwenden, wie Edmonds-Karp), Priority-First-Suche (Dijkstra, Prim, A *) usw. ist eine Adjazenzliste so gut wie eine Matrix. Nun, eine Matrix könnte eine leichte Kante haben, wenn der Graph dicht ist, aber nur durch einen unauffälligen konstanten Faktor. (Wie viel? Es geht ums Experimentieren.)
quelle
an adjacency list is as good as a matrix
in diesen Fällen?Hinzufügen zur Antwort von keyser5053 zur Speichernutzung.
Für jeden gerichteten Graphen verbraucht eine Adjazenzmatrix (mit 1 Bit pro Kante)
n^2 * (1)
Speicherbits.Für ein vollständiges Diagramm verbraucht eine Adjazenzliste (mit 64-Bit-Zeigern)
n * (n * 64)
Speicherbits, ausgenommen Listen-Overhead.Bei einem unvollständigen Diagramm verbraucht eine Adjazenzliste
0
Speicherbits, ausgenommen Listen-Overhead.Für eine Adjazenzliste können Sie die folgende Formel verwenden, um die maximale Anzahl von Kanten (
e
) zu bestimmen, bevor eine Adjazenzmatrix für den Speicher optimal ist.edges = n^2 / s
um die maximale Anzahl von Kanten zu bestimmen, wobeis
die Zeigergröße der Plattform ist.Wenn Ihr Diagramm dynamisch aktualisiert wird, können Sie diese Effizienz mit einer durchschnittlichen Kantenanzahl (pro Knoten) von beibehalten
n / s
.Einige Beispiele mit 64-Bit-Zeigern und dynamischem Diagramm (Ein dynamisches Diagramm aktualisiert die Lösung eines Problems nach Änderungen effizient, anstatt es jedes Mal nach einer Änderung von Grund auf neu zu berechnen.)
Für einen gerichteten Graphen mit
n
300 ist die optimale Anzahl von Kanten pro Knoten unter Verwendung einer Adjazenzliste:Wenn wir dies in die Formel von keyser5053 einfügen
d = e / n^2
(woe
ist die Gesamtzahl der Kanten), können wir sehen, dass wir uns unter dem Haltepunkt befinden (1 / s
):64 Bit für einen Zeiger können jedoch übertrieben sein. Wenn Sie stattdessen 16-Bit-Ganzzahlen als Zeigerversätze verwenden, können Sie bis zu 18 Kanten anpassen, bevor Sie den Bruchpunkt erreichen.
Jedes dieser Beispiele ignoriert den Overhead der Adjazenzlisten selbst (
64*2
für einen Vektor und 64-Bit-Zeiger).quelle
d = (4 * 300) / (300 * 300)
, sollte es nicht seind = 4 / (300 * 300)
? Da ist die Formeld = e / n^2
.Abhängig von der Adjacency Matrix-Implementierung sollte das 'n' des Diagramms für eine effiziente Implementierung früher bekannt sein. Wenn der Graph zu dynamisch ist und ab und zu eine Erweiterung der Matrix erfordert, kann dies auch als Nachteil gewertet werden?
quelle
Wenn Sie eine Hash-Tabelle anstelle einer Adjazenzmatrix oder -liste verwenden, erhalten Sie für alle Operationen eine bessere oder gleiche Big-O-Laufzeit und denselben Platz (Überprüfen auf eine Kante
O(1)
, Abrufen aller benachbarten KantenO(degree)
usw.).Es gibt jedoch einen konstanten Faktor-Overhead sowohl für die Laufzeit als auch für den Speicherplatz (die Hash-Tabelle ist nicht so schnell wie die Suche nach verknüpften Listen oder Arrays und benötigt eine angemessene Menge zusätzlichen Speicherplatz, um Kollisionen zu reduzieren).
quelle
Ich werde nur auf die Überwindung des Kompromisses bei der regelmäßigen Darstellung von Adjazenzlisten eingehen, da andere Antworten andere Aspekte abgedeckt haben.
Es ist möglich, ein Diagramm in einer Adjazenzliste mit der EdgeExists- Abfrage in einer amortisierten konstanten Zeit darzustellen , indem Dictionary- und HashSet- Datenstrukturen genutzt werden. Die Idee ist, Scheitelpunkte in einem Wörterbuch zu behalten, und für jeden Scheitelpunkt behalten wir einen Hash-Satz bei, der auf andere Scheitelpunkte verweist, mit denen er Kanten hat.
Ein kleiner Kompromiss bei dieser Implementierung besteht darin, dass die Raumkomplexität O (V + 2E) anstelle von O (V + E) wie in der regulären Adjazenzliste vorliegt, da Kanten hier zweimal dargestellt werden (da jeder Scheitelpunkt seine eigene Hash-Menge hat von Kanten). Operationen wie AddVertex , AddEdge , RemoveEdge können mit dieser Implementierung jedoch in der amortisierten Zeit O (1) ausgeführt werden, mit Ausnahme von RemoveVertex, das O (V) wie eine Adjazenzmatrix verwendet. Dies würde bedeuten, dass die Adjazenzmatrix außer der Einfachheit der Implementierung keinen spezifischen Vorteil hat. In dieser Implementierung der Adjazenzliste können wir Platz für spärliche Diagramme mit nahezu derselben Leistung sparen.
Weitere Informationen finden Sie in den folgenden Implementierungen im Github C # -Repository. Beachten Sie, dass für gewichtete Diagramme ein verschachteltes Wörterbuch anstelle einer Kombination aus Wörterbuch und Hash-Satz verwendet wird, um den Gewichtungswert zu berücksichtigen. In ähnlicher Weise gibt es für gerichtete Graphen separate Hash-Sets für In- und Out-Kanten.
Fortgeschrittene Algorithmen
Hinweis: Ich glaube, dass wir durch verzögertes Löschen die RemoveVertex- Operation weiter optimieren können , um O (1) amortisiert zu erhalten, obwohl ich diese Idee nicht getestet habe. Markieren Sie beispielsweise beim Löschen einfach den Scheitelpunkt als im Wörterbuch gelöscht und löschen Sie verwaiste Kanten während anderer Vorgänge träge.
quelle