std :: map insert oder std :: map find?

89

Angenommen, Sie möchten eine Karte erstellen, in der vorhandene Einträge beibehalten werden sollen. In 20% der Fälle handelt es sich bei dem Eintrag, den Sie einfügen, um neue Daten. Gibt es einen Vorteil, wenn Sie std :: map :: find und dann std :: map :: insert mit diesem zurückgegebenen Iterator ausführen? Oder ist es schneller, das Einfügen zu versuchen und dann basierend darauf zu handeln, ob der Iterator angibt, dass der Datensatz eingefügt wurde oder nicht?

Superpolock
quelle
4
Ich wurde korrigiert und wollte std :: map :: lower_bound anstelle von std :: map :: find verwenden.
Superpolock

Antworten:

145

Die Antwort ist, dass Sie beides nicht tun. Stattdessen möchten Sie etwas tun, das in Punkt 24 von Effective STL von Scott Meyers vorgeschlagen wurde :

typedef map<int, int> MapType;    // Your map type may vary, just change the typedef

MapType mymap;
// Add elements to map here
int k = 4;   // assume we're searching for keys equal to 4
int v = 0;   // assume we want the value 0 associated with the key of 4

MapType::iterator lb = mymap.lower_bound(k);

if(lb != mymap.end() && !(mymap.key_comp()(k, lb->first)))
{
    // key already exists
    // update lb->second if you care to
}
else
{
    // the key does not exist in the map
    // add it to the map
    mymap.insert(lb, MapType::value_type(k, v));    // Use lb as a hint to insert,
                                                    // so it can avoid another lookup
}
Luke
quelle
2
So funktioniert find in der Tat. Der Trick besteht darin, dass die für find und insert erforderliche Suche kombiniert wird. Dies gilt natürlich auch, wenn Sie nur einfügen und dann den zweiten Rückgabewert betrachten.
Puetzk
1
Zwei Fragen: 1) Wie unterscheidet sich die Verwendung von lower_bound von der Verwendung von find für eine Karte? 2) Ist es für eine 'Karte' nicht so, dass die rechte Operation von && immer wahr ist, wenn 'lb! = Mymap.end ()'?
Richard Corden
11
@Richard: find () gibt end () zurück, wenn der Schlüssel nicht vorhanden ist, gibt lower_bound die Position zurück, an der sich das Element befinden soll (was wiederum als Einfügungshinweis verwendet werden kann). @puetzek: Würde "nur einfügen" nicht den Referenzwert für vorhandene Schlüssel überschreiben? Es ist nicht sicher, ob das OP das wünscht.
Peterchen
2
weiß jemand, ob es etwas ähnliches für unordered_map gibt?
Giovanni Funchal
3
@peterchen map :: insert überschreibt den vorhandenen Wert nicht, falls vorhanden (siehe cplusplus.com/reference/map/map/insert) .
Chris Drew
11

Die Antwort auf diese Frage hängt auch davon ab, wie teuer es ist, den Werttyp zu erstellen, den Sie in der Karte speichern:

typedef std::map <int, int> MapOfInts;
typedef std::pair <MapOfInts::iterator, bool> IResult;

void foo (MapOfInts & m, int k, int v) {
  IResult ir = m.insert (std::make_pair (k, v));
  if (ir.second) {
    // insertion took place (ie. new entry)
  }
  else if ( replaceEntry ( ir.first->first ) ) {
    ir.second->second = v;
  }
}

Für einen Werttyp wie ein int ist das Obige effizienter als ein Fund, gefolgt von einer Einfügung (ohne Compiler-Optimierungen). Wie oben erwähnt, liegt dies daran, dass die Suche in der Karte nur einmal erfolgt.

Für den Aufruf zum Einfügen muss jedoch der neue "Wert" bereits erstellt sein:

class LargeDataType { /* ... */ };
typedef std::map <int, LargeDataType> MapOfLargeDataType;
typedef std::pair <MapOfLargeDataType::iterator, bool> IResult;

void foo (MapOfLargeDataType & m, int k) {

  // This call is more expensive than a find through the map:
  LargeDataType const & v = VeryExpensiveCall ( /* ... */ );

  IResult ir = m.insert (std::make_pair (k, v));
  if (ir.second) {
    // insertion took place (ie. new entry)
  }
  else if ( replaceEntry ( ir.first->first ) ) {
    ir.second->second = v;
  }
}

Um 'Einfügen' aufzurufen, zahlen wir für den teuren Aufruf zur Erstellung unseres Werttyps - und nach dem, was Sie in der Frage gesagt haben, werden Sie diesen neuen Wert in 20% der Fälle nicht verwenden. Wenn im obigen Fall das Ändern des Kartenwerttyps keine Option ist, ist es effizienter, zuerst die Suche durchzuführen, um zu überprüfen, ob das Element erstellt werden muss.

Alternativ kann der Wertetyp der Karte geändert werden, um Handles für die Daten unter Verwendung Ihres bevorzugten Smart-Pointer-Typs zu speichern. Der Aufruf zum Einfügen verwendet einen Nullzeiger (sehr billig zu erstellen) und nur bei Bedarf wird der neue Datentyp erstellt.

Richard Corden
quelle
8

Es wird kaum einen Geschwindigkeitsunterschied zwischen den beiden geben, find gibt einen Iterator zurück, insert macht dasselbe und durchsucht die Karte trotzdem, um festzustellen, ob der Eintrag bereits vorhanden ist.

Also ... es liegt an den persönlichen Vorlieben. Ich versuche immer einzufügen und bei Bedarf zu aktualisieren, aber einige Leute mögen es nicht, mit dem zurückgegebenen Paar umzugehen.

gbjbaanb
quelle
5

Ich würde denken, wenn Sie eine Suche durchführen und dann einfügen, wären die zusätzlichen Kosten, wenn Sie den Schlüssel nicht finden und die Einfügung danach durchführen. Es ist so, als würde man Bücher in alphabetischer Reihenfolge durchsehen und das Buch nicht finden und dann erneut durch die Bücher schauen, um zu sehen, wo es eingefügt werden soll. Es läuft darauf hinaus, wie Sie mit den Schlüsseln umgehen und ob sie sich ständig ändern. Jetzt gibt es eine gewisse Flexibilität: Wenn Sie es nicht finden, können Sie protokollieren, Ausnahmen machen, tun, was Sie wollen ...

PiNoYBoY82
quelle
3

Ich habe die beste Antwort verloren.

Find gibt map.end () zurück, wenn nichts gefunden wird, was bedeutet, dass Sie dann neue Dinge hinzufügen

iter = map.find();
if (iter == map.end()) {
  map.insert(..) or map[key] = value
} else {
  // do nothing. You said you did not want to effect existing stuff.
}

ist doppelt so langsam wie

map.insert

für jedes Element, das noch nicht in der Karte enthalten ist, da es zweimal suchen muss. Einmal, um zu sehen, ob es da ist, wieder, um den Platz für das neue Ding zu finden.

gman
quelle
1
Eine Version der STL-Einfügung gibt ein Paar zurück, das einen Iterator und einen Bool enthält. Der Bool gibt an, ob er gefunden wurde oder nicht. Der Iterator ist entweder der gefundene Eintrag oder der eingefügte Eintrag. Dies ist aus Effizienzgründen kaum zu übertreffen. unmöglich, würde ich sagen.
Zan Lynx
4
Nein, die überprüfte Antwort lower_boundwurde nicht verwendet find. Wenn der Schlüssel nicht gefunden wurde, gab er einen Iterator an die Einfügemarke zurück, nicht an das Ende. Dadurch ist es schneller.
Steven Sudit
1

Wenn Sie sich Gedanken über die Effizienz machen, sollten Sie sich hash_map <> ansehen .

In der Regel wird map <> als Binärbaum implementiert. Abhängig von Ihren Anforderungen kann eine hash_map effizienter sein.

Adam Tegen
quelle
Hätte es gerne getan. Es gibt jedoch keine hash_map in der C ++ - Standardbibliothek, und PHBs erlauben keinen Code außerhalb davon.
Superpolock
1
std :: tr1 :: unordered_map ist die Hash-Map, die dem nächsten Standard hinzugefügt werden soll und in den meisten aktuellen Implementierungen der STL verfügbar sein sollte.
Beldaz
1

Ich habe anscheinend nicht genug Punkte, um einen Kommentar zu hinterlassen, aber die angekreuzte Antwort scheint mir langwierig zu sein. Wenn Sie bedenken, dass insert den Iterator trotzdem zurückgibt, warum sollten Sie dann nach lower_bound suchen, wenn Sie nur den zurückgegebenen Iterator verwenden können. Seltsam.

Stonky
quelle
1
Da (sicherlich vor C ++ 11) die Verwendung von Einfügen bedeutet, dass Sie noch ein std::map::value_typeObjekt erstellen müssen , vermeidet die akzeptierte Antwort sogar dies.
KillianDS
-1

Alle Antworten zur Effizienz hängen von der genauen Implementierung Ihrer STL ab. Die einzige Möglichkeit, dies sicher zu wissen, besteht darin, es in beide Richtungen zu vergleichen. Ich würde vermuten, dass der Unterschied wahrscheinlich nicht signifikant ist. Entscheiden Sie sich also für den Stil, den Sie bevorzugen.

Mark Ransom
quelle
1
Das ist nicht genau richtig. Die STL unterscheidet sich von den meisten anderen Bibliotheken darin, dass sie für die meisten ihrer Vorgänge explizite Big-O-Anforderungen bietet. Es gibt einen garantierten Unterschied zwischen 2 * O (log n) und 1 * O (log n), unabhängig davon, welche Implementierung die Funktionen verwenden, um dieses O (log n) -Verhalten zu erreichen. Ob dieser Unterschied ist oder nicht auf Ihrer Plattform signifikant ist eine andere Frage. Aber der Unterschied wird immer da sein.
srm
@srm, das Big-O-Anforderungen definiert, sagt Ihnen immer noch nicht, wie lange eine Operation in absoluten Zahlen dauern wird. Der garantierte Unterschied, von dem Sie sprechen, existiert nicht.
Mark Ransom
-2

map [key] - lass es stl klären. Das kommuniziert Ihre Absicht am effektivsten.

Ja, fair genug.

Wenn Sie eine Suche und dann eine Einfügung durchführen, führen Sie 2 x O (log N) aus, wenn Sie einen Fehler erhalten, da die Suche Sie nur darüber informiert, wenn Sie nicht einfügen müssen, wohin die Einfügung gehen soll (lower_bound kann Ihnen dort helfen). . Nur eine gerade Einfügung und dann das Ergebnis zu untersuchen, ist der Weg, den ich gehen würde.


quelle
Nein, wenn der Eintrag vorhanden ist, wird ein Verweis auf den vorhandenen Eintrag zurückgegeben.
Kris Kumler
2
-1 für diese Antwort. Wie Kris K sagte, überschreibt die Verwendung von map [key] = value den vorhandenen Eintrag und "bewahrt" ihn nicht wie in der Frage gefordert. Sie können die Existenz nicht mit map [key] testen, da es ein standardmäßig erstelltes Objekt
zurückgibt,
Es geht darum zu testen, ob die Karte bereits gefüllt ist, und nur dann hinzuzufügen / zu überschreiben, wenn sie nicht vorhanden ist. Bei Verwendung von map [key] wird davon ausgegangen, dass der Wert immer schon vorhanden ist.
srm