Una std :: map che tenga traccia dell'ordine di inserimento?


113

Al momento ho un std::map<std::string,int>che memorizza un valore intero in un identificatore di stringa univoco e cerco la stringa. Fa principalmente quello che voglio, tranne per il fatto che non tiene traccia dell'ordine di inserzione. Quindi, quando itero la mappa per stampare i valori, questi vengono ordinati in base alla stringa; ma voglio che vengano ordinati in base all'ordine del (primo) inserimento.

Ho pensato di usare a vector<pair<string,int>>invece, ma ho bisogno di cercare la stringa e incrementare i valori interi di circa 10.000.000 di volte, quindi non so se a std::vectorsarà significativamente più lento.

C'è un modo per utilizzare std::mapo c'è un altro stdcontenitore che si adatta meglio alle mie esigenze?

[Sono su GCC 3.4 e probabilmente non ho più di 50 coppie di valori nel mio std::map].

Grazie.


8
Bene, parte del tempo di ricerca veloce per std :: map ha a che fare con il fatto che è ordinato in ordine, quindi può eseguire la ricerca binaria. Non puoi avere la tua torta e mangiarla anche tu!
bobobobo

1
Cosa hai finito per usare allora?
aggsol

Risposte:


56

Se hai solo 50 valori in std :: map puoi copiarli in std :: vector prima di stamparli e ordinare tramite std :: sort usando il funtore appropriato.

Oppure potresti usare boost :: multi_index . Permette di utilizzare diversi indici. Nel tuo caso potrebbe essere il seguente:

struct value_t {
      string s;
      int    i;
};
struct string_tag {};
typedef multi_index_container<
    value_t,
    indexed_by<
        random_access<>, // this index represents insertion order
        hashed_unique< tag<string_tag>, member<value_t, string, &value_t::s> >
    >
> values_t;

È fantastico! Boost ha persino un selezionatore di membri per fare il lavoro!
xtofl

2
Sì, multi_index è la mia funzione preferita in boost :)
Kirill V. Lyadvinsky

3
@Kristo: non si tratta di dimensioni del contenitore, si tratta di riutilizzare l'implementazione esistente esattamente per questo problema. È di classe. Certo, il C ++ non è un linguaggio funzionale, quindi la sintassi è piuttosto elaborata.
xtofl

4
Da quando la programmazione prevedeva il salvataggio dei tasti premuti?
GManNickG

1
Grazie per aver postato questo. Esiste un libro "boost multi-index for dummies"? Potrei usarlo ...
Don Bright

25

Potresti combinare a std::vectorcon a std::tr1::unordered_map(una tabella hash). Ecco un collegamento alla documentazione di Boost per unordered_map. È possibile utilizzare il vettore per tenere traccia dell'ordine di inserimento e la tabella hash per eseguire le ricerche frequenti. Se stai eseguendo centinaia di migliaia di ricerche, la differenza tra la ricerca O (log n) std::mape O (1) per una tabella hash potrebbe essere significativa.

std::vector<std::string> insertOrder;
std::tr1::unordered_map<std::string, long> myTable;

// Initialize the hash table and record insert order.
myTable["foo"] = 0;
insertOrder.push_back("foo");
myTable["bar"] = 0;
insertOrder.push_back("bar");
myTable["baz"] = 0;
insertOrder.push_back("baz");

/* Increment things in myTable 100000 times */

// Print the final results.
for (int i = 0; i < insertOrder.size(); ++i)
{
    const std::string &s = insertOrder[i];
    std::cout << s << ' ' << myTable[s] << '\n';
}

4
@xtofl, in che modo la mia risposta non è utile e quindi degna di un voto negativo? Il mio codice è errato in qualche modo?
Michael Kristofik

Questo è il modo migliore per farlo. Costo della memoria molto basso (solo per 50 stringhe!), Consente std::mapdi funzionare come dovrebbe (cioè ordinandosi da solo mentre si inserisce) e ha un tempo di esecuzione veloce. (L'ho letto dopo aver scritto la mia versione, dove ho usato std :: list!)
bobobobo

Penso che std :: vector o std :: list sia una questione di gusti e non sia chiaro quale sia il migliore. (Vector ha un accesso casuale che non è necessario, ha anche una memoria contigua, che non è necessaria. List memorizza l'ordine senza la spesa di nessuna di queste 2 funzionalità, ad esempio le riallocazioni durante la crescita).
Oliver Schönrock

14

Mantieni un parallelo list<string> insertionOrder.

Quando è il momento di stampare, iterare l' elenco ed eseguire ricerche nella mappa .

each element in insertionOrder  // walks in insertionOrder..
    print map[ element ].second // but lookup is in map

1
Questo è stato anche il mio primo pensiero, ma duplica le chiavi in ​​un secondo contenitore, giusto? In caso di una chiave std :: string che non è brillante, giusto?
Oliver Schönrock,

2
@OliverSchonrock partire dal C ++ 17, è possibile utilizzare std::string_viewper le chiavi della mappa riferendosi alla std::stringnella insertionOrderlista. Ciò evita la copia ma è necessario fare attenzione che gli insertionOrderelementi sopravvivano alle chiavi nella mappa che si riferiscono ad essi.
flyx

Ho finito per scrivere un contenitore che integrava mappa ed elenco in uno: codereview.stackexchange.com/questions/233177/… Nessuna duplicazione
Oliver Schönrock

10

Tessil ha un'implementazione molto carina della mappa ordinata (e dell'insieme) che è la licenza del MIT. Puoi trovarlo qui: mappa-ordinata

Esempio di mappa

#include <iostream>
#include <string>
#include <cstdlib>
#include "ordered_map.h"

int main() {
tsl::ordered_map<char, int> map = {{'d', 1}, {'a', 2}, {'g', 3}};
map.insert({'b', 4});
map['h'] = 5;
map['e'] = 6;

map.erase('a');


// {d, 1} {g, 3} {b, 4} {h, 5} {e, 6}
for(const auto& key_value : map) {
    std::cout << "{" << key_value.first << ", " << key_value.second << "}" << std::endl;
}


map.unordered_erase('b');

// Break order: {d, 1} {g, 3} {e, 6} {h, 5}
for(const auto& key_value : map) {
    std::cout << "{" << key_value.first << ", " << key_value.second << "}" << std::endl;
}
}

4

Se hai bisogno di entrambe le strategie di ricerca, ti ritroverai con due contenitori. Puoi usare a vectorcon i tuoi valori effettivi inte mettere un map< string, vector< T >::difference_type> accanto ad esso, restituendo l'indice nel vettore.

Per completare tutto ciò, puoi incapsulare entrambi in una classe.

Ma credo che Boost abbia un contenitore con più indici.


3

Quello che vuoi (senza ricorrere a Boost) è quello che chiamo un "hash ordinato", che è essenzialmente un mashup di un hash e un elenco collegato con chiavi stringa o numeri interi (o entrambi allo stesso tempo). Un hash ordinato mantiene l'ordine degli elementi durante l'iterazione con le prestazioni assolute di un hash.

Ho messo insieme una libreria di frammenti C ++ relativamente nuova che riempie ciò che considero buchi nel linguaggio C ++ per gli sviluppatori di librerie C ++. Andare qui:

https://github.com/cubiclesoft/cross-platform-cpp

Afferrare:

templates/detachable_ordered_hash.cpp
templates/detachable_ordered_hash.h
templates/detachable_ordered_hash_util.h

Se i dati controllati dall'utente verranno inseriti nell'hash, potresti anche voler:

security/security_csprng.cpp
security/security_csprng.h

Invocalo:

#include "templates/detachable_ordered_hash.h"
...
// The 47 is the nearest prime to a power of two
// that is close to your data size.
//
// If your brain hurts, just use the lookup table
// in 'detachable_ordered_hash.cpp'.
//
// If you don't care about some minimal memory thrashing,
// just use a value of 3.  It'll auto-resize itself.
int y;
CubicleSoft::OrderedHash<int> TempHash(47);
// If you need a secure hash (many hashes are vulnerable
// to DoS attacks), pass in two randomly selected 64-bit
// integer keys.  Construct with CSPRNG.
// CubicleSoft::OrderedHash<int> TempHash(47, Key1, Key2);
CubicleSoft::OrderedHashNode<int> *Node;
...
// Push() for string keys takes a pointer to the string,
// its length, and the value to store.  The new node is
// pushed onto the end of the linked list and wherever it
// goes in the hash.
y = 80;
TempHash.Push("key1", 5, y++);
TempHash.Push("key22", 6, y++);
TempHash.Push("key3", 5, y++);
// Adding an integer key into the same hash just for kicks.
TempHash.Push(12345, y++);
...
// Finding a node and modifying its value.
Node = TempHash.Find("key1", 5);
Node->Value = y++;
...
Node = TempHash.FirstList();
while (Node != NULL)
{
  if (Node->GetStrKey())  printf("%s => %d\n", Node->GetStrKey(), Node->Value);
  else  printf("%d => %d\n", (int)Node->GetIntKey(), Node->Value);

  Node = Node->NextList();
}

Mi sono imbattuto in questo thread SO durante la mia fase di ricerca per vedere se qualcosa come OrderedHash esisteva già senza che mi fosse richiesto di inserire una libreria enorme. Io ero delusa. Quindi ho scritto il mio. E ora l'ho condiviso.


2

Non puoi farlo con una mappa, ma puoi usare due strutture separate - la mappa e il vettore e mantenerli sincronizzati - cioè quando elimini dalla mappa, trovi ed elimini l'elemento dal vettore. Oppure puoi creare un map<string, pair<int,int>>- e nella tua coppia memorizzare la dimensione () della mappa al momento dell'inserimento per registrare la posizione, insieme al valore di int, e poi quando stampi, usa il membro della posizione per ordinare.


2

Un altro modo per implementare questo è con a mapinvece di vector. Ti mostrerò questo approccio e discuterò le differenze:

Basta creare una classe che abbia due mappe dietro le quinte.

#include <map>
#include <string>

using namespace std;

class SpecialMap {
  // usual stuff...

 private:
  int counter_;
  map<int, string> insertion_order_;
  map<string, int> data_;
};

È quindi possibile esporre un iteratore all'iteratore data_nell'ordine corretto. Il modo in cui lo fai è iterare insertion_order_e per ogni elemento che ottieni da quell'iterazione, fai una ricerca nel data_con il valore dainsertion_order_

Puoi usare il più efficiente hash_mapper insertion_order poiché non ti interessa scorrere direttamente insertion_order_.

Per fare gli inserti, puoi avere un metodo come questo:

void SpecialMap::Insert(const string& key, int value) {
  // This may be an over simplification... You ought to check
  // if you are overwriting a value in data_ so that you can update
  // insertion_order_ accordingly
  insertion_order_[counter_++] = key;
  data_[key] = value;
}

Ci sono molti modi in cui puoi migliorare il design e preoccuparti delle prestazioni, ma questo è un buon scheletro per iniziare a implementare questa funzionalità da solo. Puoi renderlo basato su modelli e potresti effettivamente memorizzare le coppie come valori in data_ in modo da poter fare facilmente riferimento alla voce in insertion_order_. Ma lascio questi problemi di progettazione come esercizio :-).

Aggiornamento : suppongo di dover dire qualcosa sull'efficienza dell'utilizzo di map vs. vector per insertion_order_

  • ricerche direttamente nei dati, in entrambi i casi sono O (1)
  • gli inserti nell'approccio vettoriale sono O (1), gli inserti nell'approccio alla mappa sono O (logn)
  • le eliminazioni nell'approccio vettoriale sono O (n) perché devi cercare l'elemento da rimuovere. Con l'approccio alla mappa sono O (logn).

Forse se non hai intenzione di utilizzare le eliminazioni così tanto, dovresti usare l'approccio vettoriale. L'approccio alla mappa sarebbe migliore se sostenessi un ordine diverso (come la priorità) invece di un ordine di inserzione.


L'approccio alla mappa è anche migliore se hai bisogno di ottenere elementi con l '"ID di inserimento". Ad esempio, se vuoi l'elemento che è stato inserito 5 °, fai una ricerca in insertion_order con il tasto 5 (o 4, a seconda di dove inizi counter_). Con l'approccio vettoriale, se il quinto elemento è stato eliminato, si otterrebbe effettivamente il sesto elemento che è stato inserito.
Tom,

2

Ecco la soluzione che richiede solo la libreria di modelli standard senza utilizzare il multiindice di boost:
è possibile utilizzare std::map<std::string,int>;e vector <data>;dove nella mappa si memorizza l'indice della posizione dei dati nel vettore e il vettore memorizza i dati in ordine di inserimento. Qui l'accesso ai dati ha complessità O (log n). la visualizzazione dei dati in ordine di inserzione ha una complessità O (n). l'inserimento dei dati ha complessità O (log n).

Per esempio:

#include<iostream>
#include<map>
#include<vector>

struct data{
int value;
std::string s;
}

typedef std::map<std::string,int> MapIndex;//this map stores the index of data stored 
                                           //in VectorData mapped to a string              
typedef std::vector<data> VectorData;//stores the data in insertion order

void display_data_according_insertion_order(VectorData vectorData){
    for(std::vector<data>::iterator it=vectorData.begin();it!=vectorData.end();it++){
        std::cout<<it->value<<it->s<<std::endl;
    }
}
int lookup_string(std::string s,MapIndex mapIndex){
    std::MapIndex::iterator pt=mapIndex.find(s)
    if (pt!=mapIndex.end())return it->second;
    else return -1;//it signifies that key does not exist in map
}
int insert_value(data d,mapIndex,vectorData){
    if(mapIndex.find(d.s)==mapIndex.end()){
        mapIndex.insert(std::make_pair(d.s,vectorData.size()));//as the data is to be
                                                               //inserted at back 
                                                               //therefore index is
                                                               //size of vector before
                                                               //insertion
        vectorData.push_back(d);
        return 1;
    }
    else return 0;//it signifies that insertion of data is failed due to the presence
                  //string in the map and map stores unique keys
}

1

Questo è in qualche modo correlato alla risposta di Faisals. Puoi semplicemente creare una classe wrapper attorno a una mappa e un vettore e mantenerli facilmente sincronizzati. Un corretto incapsulamento ti consentirà di controllare il metodo di accesso e quindi quale contenitore utilizzare ... il vettore o la mappa. Questo evita di usare Boost o qualcosa del genere.


1

Una cosa che devi considerare è il numero limitato di elementi di dati che stai utilizzando. È possibile che sia più veloce utilizzare solo il vettore. C'è un sovraccarico nella mappa che può rendere più costoso eseguire ricerche in piccoli set di dati rispetto al vettore più semplice. Quindi, se sai che utilizzerai sempre circa lo stesso numero di elementi, fai qualche benchmarking e vedi se le prestazioni della mappa e del vettore sono ciò che pensi veramente che sia. Potresti scoprire che la ricerca in un vettore con solo 50 elementi è simile alla mappa.


1

// Dovrebbe essere come quest'uomo!

// Ciò mantiene la complessità dell'inserimento è O (logN) e anche l'eliminazione è O (logN).

class SpecialMap {
private:
  int counter_;
  map<int, string> insertion_order_;
  map<string, int> insertion_order_reverse_look_up; // <- for fast delete
  map<string, Data> data_;
};


-1

Una mappa di coppia (str, int) e int statico che incrementa all'atto dell'inserimento chiama coppie di dati. Mettere in una struttura che può restituire il valore int statico con un membro index () forse?


2
Dovresti aggiungere un esempio.
m02ph3u5
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.