Um std :: map que rastreia a ordem de inserção?

113

Atualmente, tenho um std::map<std::string,int>que armazena um valor inteiro em um identificador de string exclusivo e procuro na string. Ele faz principalmente o que eu desejo, exceto que não acompanha o pedido de inserção. Então, quando eu itero o mapa para imprimir os valores, eles são classificados de acordo com a string; mas eu quero que eles sejam classificados de acordo com a ordem de (primeira) inserção.

Pensei em usar um vector<pair<string,int>>, mas preciso pesquisar a string e incrementar os valores inteiros cerca de 10.000.000 vezes, então não sei se a std::vectorserá significativamente mais lento.

Existe uma maneira de usar std::mapou existe outro stdcontainer que melhor se adapte às minhas necessidades?

[Estou no GCC 3.4 e provavelmente não tenho mais do que 50 pares de valores no meu std::map].

Obrigado.

poliglota
fonte
8
Bem, parte do tempo de pesquisa rápida para std :: map tem a ver com o fato de que ele é classificado em ordem, para que possa fazer pesquisa binária. Só não pode ter seu bolo e comê-lo!
bobobobo
1
O que você acabou usando naquela época?
aggsol

Respostas:

56

Se você tiver apenas 50 valores em std :: map, poderá copiá-los para std :: vector antes de imprimir e classificar por meio de std :: sort usando o functor apropriado.

Ou você pode usar boost :: multi_index . Permite usar vários índices. No seu caso, poderia ser o seguinte:

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;
Kirill V. Lyadvinsky
fonte
Isso é ótimo! O Boost tem até um seletor de membros para fazer o trabalho!
xtofl
2
Sim, multi_index é meu recurso favorito no boost :)
Kirill V. Lyadvinsky
3
@Kristo: não se trata de tamanho de container, trata-se de reutilizar a implementação existente exatamente para este problema. Isso é elegante. Reconhecidamente, C ++ não é uma linguagem funcional, então a sintaxe é um tanto elaborada.
xtofl
4
Desde quando programar significa salvar pressionamentos de teclas?
GManNickG
1
Obrigado por postar isso. Existe um livro "boost multi-index for dummies"? Eu poderia usá-lo ...
vestido brilhante em
25

Você pode combinar a std::vectorcom a std::tr1::unordered_map(uma tabela hash). Aqui está um link para a documentação do Boost para unordered_map. Você pode usar o vetor para controlar a ordem de inserção e a tabela hash para fazer pesquisas frequentes. Se você estiver fazendo centenas de milhares de pesquisas, a diferença entre O (log n) pesquisa para std::mape O (1) para uma tabela hash pode ser 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';
}
Michael Kristofik
fonte
4
@xtofl, Como isso torna minha resposta inútil e, portanto, digna de um voto negativo? Meu código está incorreto de alguma forma?
Michael Kristofik
Esta é a melhor forma de o fazer. Custo de memória muito barato (para apenas 50 strings!), Permite std::maptrabalhar como deveria (ou seja, ordenando-se conforme você insere), e tem um tempo de execução rápido. (Eu li isso depois de escrever minha versão, onde usei std :: list!)
bobobobo
Eu acho que std :: vector ou std :: list é uma questão de gosto, e não está claro qual é melhor. (Vector tem acesso aleatório que não é necessário, também tem memória contígua, que também não é necessária. List armazena o pedido sem a despesa de nenhum desses 2 recursos, por exemplo, realocações durante o crescimento).
Oliver Schönrock
14

Mantenha um paralelo list<string> insertionOrder.

Quando chegar a hora de imprimir, itere na lista e faça pesquisas no mapa .

each element in insertionOrder  // walks in insertionOrder..
    print map[ element ].second // but lookup is in map
bobobobo
fonte
1
Este foi o meu primeiro pensamento também, mas duplica as chaves em um segundo container, certo? No caso de uma chave std :: string que não é brilhante, certo?
Oliver Schönrock
2
@OliverSchonrock A partir do C ++ 17, você pode usar std::string_viewpara as chaves do mapa referentes ao std::stringna insertionOrderlista. Isso evita a cópia, mas você precisa ter cuidado para que os insertionOrderelementos sobrevivam às chaves no mapa que se referem a eles.
flyx
Acabei escrevendo um contêiner que integrou o mapa e a lista em um: codereview.stackexchange.com/questions/233177/… Sem duplicação
Oliver Schönrock
10

Tessil tem uma implementação muito boa de mapa ordenado (e conjunto) que é uma licença do MIT. Você pode encontrá-lo aqui: mapa-ordenado

Exemplo de mapa

#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;
}
}
aggsol
fonte
4

Se você precisar das duas estratégias de pesquisa, terá dois contêineres. Você pode usar um vectorcom seu ( ints) valor ( es) real ( is) e colocar um map< string, vector< T >::difference_type> próximo a ele, retornando o índice para o vetor.

Para completar tudo isso, você pode encapsular ambos em uma classe.

Mas acredito que boost tem um contêiner com vários índices.

xtofl
fonte
3

O que você deseja (sem recorrer ao Boost) é o que chamo de "hash ordenado", que é essencialmente um mashup de um hash e uma lista vinculada com strings ou chaves inteiras (ou ambos ao mesmo tempo). Um hash ordenado mantém a ordem dos elementos durante a iteração com o desempenho absoluto de um hash.

Tenho reunido uma biblioteca de snippet C ++ relativamente nova que preenche o que considero lacunas na linguagem C ++ para desenvolvedores de bibliotecas C ++. Vá aqui:

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

Agarrar:

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

Se os dados controlados pelo usuário forem colocados no hash, você também pode querer:

security/security_csprng.cpp
security/security_csprng.h

Invoque-o:

#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();
}

Corri para este segmento SO durante minha fase de pesquisa para ver se algo como OrderedHash já existia sem exigir que eu colocasse em uma biblioteca enorme. Fiquei desapontado. Então eu escrevi meu próprio. E agora eu compartilhei isso.

CubicleSoft
fonte
2

Você não pode fazer isso com um mapa, mas pode usar duas estruturas separadas - o mapa e o vetor e mantê-los sincronizados - ou seja, quando você exclui do mapa, encontra e exclui o elemento do vetor. Ou você pode criar um map<string, pair<int,int>>- e em seu par armazenar o tamanho () do mapa na inserção para registrar a posição, junto com o valor do int, e então quando você imprimir, usar o membro da posição para classificar.

Faisal Vali
fonte
2

Outra maneira de implementar isso é com um em mapvez de um vector. Vou mostrar essa abordagem e discutir as diferenças:

Basta criar uma classe que tenha dois mapas nos bastidores.

#include <map>
#include <string>

using namespace std;

class SpecialMap {
  // usual stuff...

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

Você pode então expor um iterador para iterador data_na ordem adequada. A maneira como você faz isso é iterar insertion_order_e, para cada elemento obtido dessa iteração, faça uma pesquisa no data_com o valor deinsertion_order_

Você pode usar o mais eficiente hash_mappara insertion_order, pois não se preocupa com a iteração direta insertion_order_.

Para fazer inserções, você pode ter um método como este:

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

Há muitas maneiras de melhorar o design e se preocupar com o desempenho, mas este é um bom esqueleto para você começar a implementar essa funcionalidade por conta própria. Você pode torná-lo modelado e pode realmente armazenar pares como valores em data_ para que possa referenciar facilmente a entrada em insertion_order_. Mas deixo essas questões de design como um exercício :-).

Atualização : suponho que devo dizer algo sobre a eficiência do uso de mapa vs. vetor para insertion_order_

  • pesquisas diretamente nos dados, em ambos os casos são O (1)
  • inserções na abordagem vetorial são O (1), inserções na abordagem do mapa são O (logn)
  • exclusões na abordagem vetorial são O (n) porque você tem que procurar o item a ser removido. Com a abordagem do mapa, eles são O (logn).

Talvez se você não for usar exclusões tanto, deve usar a abordagem vetorial. A abordagem do mapa seria melhor se você suportasse uma ordem diferente (como prioridade) em vez de uma ordem de inserção.

Tom
fonte
A abordagem do mapa também é melhor se você precisar obter itens pelo "id de inserção". Por exemplo, se você quiser o item que foi inserido em quinto lugar, faça uma pesquisa em insertion_order com a chave 5 (ou 4, dependendo de onde você iniciar counter_). Com a abordagem vetorial, se o 5º item fosse excluído, você na verdade obteria o 6º item que foi inserido.
Tom
2

Aqui está uma solução que requer apenas a biblioteca de modelos padrão sem usar o multiindex do boost:
Você pode usar std::map<std::string,int>;e vector <data>;onde no mapa você armazena o índice da localização dos dados no vetor e o vetor armazena os dados na ordem de inserção. Aqui, o acesso aos dados tem complexidade O (log n). exibir dados no pedido de inserção tem complexidade O (n). a inserção de dados tem complexidade O (log n).

Por exemplo:

#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
}
Himanshu Pandey
fonte
1

Isso está um tanto relacionado à resposta de Faisals. Você pode simplesmente criar uma classe de wrapper em torno de um mapa e vetor e mantê-los sincronizados facilmente. O encapsulamento adequado permitirá que você controle o método de acesso e, portanto, qual contêiner usar ... o vetor ou o mapa. Isso evita o uso de Boost ou algo parecido.

Polaris878
fonte
1

Uma coisa que você precisa considerar é o pequeno número de elementos de dados que você está usando. É possível que seja mais rápido usar apenas o vetor. Há alguma sobrecarga no mapa que pode fazer com que seja mais caro fazer pesquisas em pequenos conjuntos de dados do que no vetor mais simples. Portanto, se você sabe que sempre usará o mesmo número de elementos, faça um benchmarking e veja se o desempenho do mapa e do vetor é o que você realmente pensa que é. Você pode descobrir que a pesquisa em um vetor com apenas 50 elementos é quase igual à do mapa.

Chad Simpkins
fonte
1

// Deve ser como esse homem!

// Isso mantém a complexidade da inserção é O (logN) e a exclusão também é 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_;
};
Ka Yan
fonte
0

Use boost::multi_indexcom índices de mapa e lista.

Vladimir Voznesensky
fonte
-1

Um mapa de par (str, int) e int estático que incrementa em pares de índices de chamadas de inserção de dados. Colocar em um struct que pode retornar o int val estático com um membro index () talvez?

Mike
fonte
2
Você deve adicionar um exemplo.
m02ph3u5