Como posso selecionar com eficiência um contêiner da Biblioteca Padrão no C ++ 11?

135

Há uma imagem bem conhecida (folha de dicas) chamada "Escolha do container C ++". É um fluxograma para escolher o melhor recipiente para o uso desejado.

Alguém sabe se já existe uma versão em C ++ 11?

Este é o anterior: Escolha do contêiner do eC ++

BlakBat
fonte
6
Nunca vi isso antes. obrigado!
WeaselFox
6
@ WeaselFox: Já faz parte do C ++ - Faq aqui no SO.
Alok Salvar
4
O C ++ 11 introduziu apenas um novo tipo de contêiner verdadeiro: os contêineres unordered_X. Incluí-los apenas confundiria consideravelmente a tabela, pois há várias considerações ao decidir se uma tabela de hash é apropriada.
Nicol Bolas
13
James está certo, há mais casos para usar vetor do que o que a tabela mostra. A vantagem da localidade dos dados supera em muitos casos a falta de eficiência em algumas operações (mais cedo, o C ++ 11). Eu não encontrar e gráfico para ainda útil para c ++ 03
David Rodríguez - dribeas
33
Isso é engraçado, mas acho que a leitura de qualquer livro comum sobre estruturas de dados o deixará em um estado no qual você não apenas poderá reinventar esse fluxograma em alguns minutos, mas também saberá muito mais coisas úteis que esse fluxograma encobrir.
Andrew Tomazos

Respostas:

97

Não que eu saiba, no entanto, isso pode ser feito textualmente, eu acho. Além disso, o gráfico está um pouco desligado, porque listgeralmente não é um contêiner tão bom nem o é forward_list. Ambas as listas são contêineres muito especializados para aplicações de nicho.

Para criar esse gráfico, você só precisa de duas diretrizes simples:

  • Escolha primeiro a semântica
  • Quando várias opções estiverem disponíveis, vá para o mais simples

Preocupar-se com o desempenho é geralmente inútil no início. As grandes considerações de O realmente surgem quando você começa a manusear alguns milhares (ou mais) de itens.

Existem duas grandes categorias de contêineres:

  • Contêineres associativos : eles têm uma findoperação
  • Recipientes de sequência simples

e então você pode construir vários adaptadores em cima deles: stack, queue, priority_queue. Deixarei os adaptadores aqui fora, eles são suficientemente especializados para serem reconhecidos.


Pergunta 1: associativo ?

  • Se você precisar pesquisar facilmente por uma chave, precisará de um contêiner associativo
  • Se você precisar classificar os elementos, precisará de um contêiner associativo ordenado
  • Caso contrário, pule para a pergunta 2.

Pergunta 1.1: Encomenda ?

  • Se você não precisar de um pedido específico, use um unordered_contêiner; caso contrário, use sua contraparte tradicional solicitada.

Pergunta 1.2: Chave separada ?

  • Se a chave estiver separada do valor, use a map, caso contrário, use umset

Pergunta 1.3: Duplicatas ?

  • Se você deseja manter duplicatas, use a multi, caso contrário não.

Exemplo:

Suponha que eu tenha várias pessoas com um ID exclusivo associado a elas e gostaria de recuperar os dados de uma pessoa a partir dele, da maneira mais simples possível.

  1. Eu quero uma findfunção, portanto, um contêiner associativo

    1.1 Eu não poderia me importar menos com a ordem, portanto, um unordered_contêiner

    1.2 Minha chave (ID) é separada do valor ao qual está associada, portanto, ummap

    1.3 O ID é único, portanto, nenhuma duplicata deve aparecer.

A resposta final é: std::unordered_map<ID, PersonData>.


Pergunta 2: Memória estável ?

  • Se os elementos permanecerem estáveis ​​na memória (ou seja, não devem se mover quando o próprio contêiner for modificado), use alguns list
  • Caso contrário, pule para a pergunta 3.

Pergunta 2.1: Qual ?

  • Aceite um list; a forward_listé útil apenas para menos espaço ocupado na memória.

Pergunta 3: Dimensionado dinamicamente ?

  • Se o contêiner tiver um tamanho conhecido (no momento da compilação) e esse tamanho não for alterado durante o curso do programa, e os elementos forem construtíveis por padrão ou você pode fornecer uma lista completa de inicialização (usando a { ... }sintaxe), use um array. Ele substitui o C-array tradicional, mas por funções convenientes.
  • Caso contrário, pule para a pergunta 4.

Pergunta 4: com duas pontas ?

  • Se você deseja remover itens da frente e de trás, use a deque; caso contrário, use a vector.

Você observará que, por padrão, a menos que precise de um contêiner associativo, sua escolha será a vector. Acontece que também é a recomendação de Sutter e Stroustrup .

Matthieu M.
fonte
5
+1, mas com algumas notas: 1) arraynão requer um tipo construtível padrão; 2) escolher multies não é tanto sobre duplicidades serem permitidas, mas mais sobre se mantê- las é importante (você pode colocar duplicatas em não multicontêineres, acontece que apenas uma é mantida).
R. Martinho Fernandes
2
O exemplo está um pouco errado. 1) podemos "encontrar" (não a função membro, a "<algoritmo>") em um contêiner não associativo, 1.1) se precisarmos encontrar "eficientemente", e desordenado_ será O (1) e não O ( log n).
BlakBat 22/05
4
@ BlakBat: map.find(key)é muito mais palatável do que no std::find(map.begin(), map.end(), [&](decltype(map.front()) p) { return p.first < key; }));entanto, portanto, é importante, semanticamente, que findseja uma função de membro e não a função de <algorithm>. Quanto a O (1) vs O (log n), não afeta a semântica; Vou remover o "eficientemente" do exemplo e substituí-lo por "facilmente".
Matthieu M.
"Se os elementos devem ser estáveis ​​na memória ... então use alguma lista" ... hmmm, pensei que dequetivesse essa propriedade também?
Martin Ba
@MartinBa: Sim e não. Em um dequeos elementos são estáveis somente se você pressionar / pop em cada extremidade; se você começar a inserir / apagar no meio, até N / 2 elementos serão embaralhados para preencher a lacuna criada.
Matthieu M.
51

Gosto da resposta de Matthieu, mas vou reafirmar o fluxograma da seguinte maneira:

Quando NÃO usar std :: vector

Por padrão, se você precisar de um contêiner de material, use std::vector. Assim, todos os outros contêineres são justificados apenas fornecendo alguma funcionalidade alternativa a std::vector.

Construtores

std::vectorrequer que seu conteúdo seja construtível para movimentação, pois precisa ser capaz de embaralhar os itens. Isso não é um fardo terrível para colocar no conteúdo (observe que construtores padrão não são necessários , graças a emplaceoutras coisas). No entanto, a maioria dos outros contêineres não requer nenhum construtor em particular (novamente, graças a emplace). Portanto, se você tem um objeto em que absolutamente não pode implementar um construtor de movimento, precisará escolher outra coisa.

A std::dequeseria a substituição geral, com muitas das propriedades de std::vector, mas você só pode inserir nas duas extremidades do deque. Inserções no meio exigem movimento. A std::listnão exige requisitos em seu conteúdo.

Precisa de Bools

std::vector<bool>não é. Bem, é padrão. Mas não é um vectorno sentido usual, pois as operações que std::vectornormalmente permitem são proibidas. E certamente não contém bools .

Portanto, se você precisar de um vectorcomportamento real de um contêiner de bools, não será possível obtê-lo std::vector<bool>. Então você terá que fazer o devido com um std::deque<bool>.

Procurando

Se você precisar encontrar elementos em um contêiner, e a tag de pesquisa não puder ser apenas um índice, poderá ser necessário abandonar a std::vectorfavor de sete map. Observe a palavra-chave " may "; um ordenado std::vectoràs vezes é uma alternativa razoável. Ou Boost.Container's flat_set/map, que implementa uma classificação std::vector.

Agora existem quatro variações delas, cada uma com suas próprias necessidades.

  • Use a mapquando a tag de pesquisa não for a mesma coisa que o item que você está procurando. Caso contrário, use a set.
  • Use unorderedquando você tiver muitos itens no contêiner e o desempenho da pesquisa for absolutamente necessário O(1), e não O(logn).
  • Use multise você precisar de vários itens para ter a mesma tag de pesquisa.

Encomenda

Se você precisar que um contêiner de itens seja sempre classificado com base em uma operação de comparação específica, poderá usar a set. Ou a, multi_setse você precisar de vários itens para ter o mesmo valor.

Ou você pode usar uma classificação std::vector, mas terá que mantê-la classificada.

Estabilidade

Quando iteradores e referências são invalidados, às vezes é uma preocupação. Se você precisar de uma lista de itens, de modo que tenha iteradores / ponteiros para esses itens em vários outros lugares, std::vectora abordagem da invalidação pode não ser apropriada. Qualquer operação de inserção pode causar invalidação, dependendo do tamanho e capacidade atuais.

std::listoferece uma garantia firme: um iterador e suas referências / indicadores associados só são invalidados quando o próprio item é removido do contêiner. std::forward_listexiste se a memória é uma preocupação séria.

Se essa é uma garantia muito forte, std::dequeoferece uma garantia mais fraca, mas útil. A invalidação resulta de inserções no meio, mas inserções na cabeça ou na cauda causam apenas invalidação de iteradores , não ponteiros / referências a itens no contêiner.

Desempenho de inserção

std::vector fornece apenas inserção barata no final (e mesmo assim, fica caro se você aumentar a capacidade).

std::listé caro em termos de desempenho (cada item recém-inserido custa uma alocação de memória), mas é consistente . Ele também oferece a capacidade ocasionalmente indispensável de embaralhar itens praticamente sem custo de desempenho, além de trocar itens com outros std::listcontêineres do mesmo tipo sem perda de desempenho. Se você precisar embaralhar bastante as coisas , use std::list.

std::dequefornece inserção / remoção em tempo constante na cabeça e cauda, ​​mas a inserção no meio pode ser bastante cara. Portanto, se você precisar adicionar / remover itens da parte frontal e traseira, std::dequepode ser o que você precisa.

Note-se que, graças à semântica da movimentação, std::vectoro desempenho da inserção pode não ser tão ruim quanto costumava ser. Algumas implementações implementaram uma forma de cópia de itens baseados em semântica de movimentação (a chamada "swaptimization"), mas agora que a movimentação faz parte do idioma, é exigida pelo padrão.

Sem alocações dinâmicas

std::arrayé um contêiner fino se você deseja o menor número possível de alocações dinâmicas. É apenas um invólucro em torno de uma matriz C; isso significa que seu tamanho deve ser conhecido em tempo de compilação . Se você pode viver com isso, use std::array.

Dito isto, o uso std::vectore reserveing de um tamanho funcionariam tão bem para um limite std::vector. Dessa forma, o tamanho real pode variar e você recebe apenas uma alocação de memória (a menos que reduza a capacidade).

Nicol Bolas
fonte
1
Bem, eu também gosto muito da sua resposta :) WRT mantendo um vetor classificado, além de std::sort, também std::inplace_mergeé interessante colocar facilmente novos elementos (em vez de uma chamada std::lower_bound+ std::vector::insert). É bom aprender sobre flat_sete flat_map!
Matthieu M.
2
Você também não pode usar um vetor com tipos alinhados de 16 bytes. Além disso também é um bom substituto para o vector<bool>é vector<char>.
Inverso
@ Inverse: "Você também não pode usar um vetor com tipos alinhados de 16 bytes." Quem disse? Se std::allocator<T>não suportar esse alinhamento (e não sei por que não seria), você sempre poderá usar seu próprio alocador personalizado.
Nicol Bolas 26/05
2
@ Inverse: C ++ 11 std::vector::resizetem uma sobrecarga que não leva um valor (apenas assume o novo tamanho; quaisquer novos elementos serão construídos no local por padrão). Além disso, por que os compiladores não conseguem alinhar corretamente os parâmetros de valor, mesmo quando são declarados com esse alinhamento?
Nicol Bolas 26/05
1
bitsetpara bool se você sabe o tamanho de antecedência en.cppreference.com/w/cpp/utility/bitset
bendervader
25

Aqui está a versão C ++ 11 do fluxograma acima. [publicado originalmente sem atribuição ao autor original, Mikael Persson ]

Wasim Thabraze
fonte
2
@NO_NAME Uau, fico feliz que alguém tenha se dado ao trabalho de citar uma fonte.
underscore_d
1

Aqui está uma rápida rotação, embora provavelmente precise de trabalho

Should the container let you manage the order of the elements?
Yes:
  Will the container contain always exactly the same number of elements? 
  Yes:
    Does the container need a fast move operator?
    Yes: std::vector
    No: std::array
  No:
    Do you absolutely need stable iterators? (be certain!)
    Yes: boost::stable_vector (as a last case fallback, std::list)
    No: 
      Do inserts happen only at the ends?
      Yes: std::deque
      No: std::vector
No: 
  Are keys associated with Values?
  Yes:
    Do the keys need to be sorted?
    Yes: 
      Are there more than one value per key?
      Yes: boost::flat_map (as a last case fallback, std::map)
      No: boost::flat_multimap (as a last case fallback, std::map)
    No:
      Are there more than one value per key?
      Yes: std::unordered_multimap
      No: std::unordered_map
  No:
    Are elements read then removed in a certain order?
    Yes:
      Order is:
      Ordered by element: std::priority_queue
      First in First out: std::queue
      First in Last out: std::stack
      Other: Custom based on std::vector????? 
    No:
      Should the elements be sorted by value?
      Yes: boost::flat_set
      No: std::vector

Você pode notar que isso difere bastante da versão C ++ 03, principalmente devido ao fato de eu realmente não gostar de nós vinculados. Os contêineres do nó vinculado geralmente podem ser superados no desempenho por um contêiner não vinculado, exceto em algumas situações raras. Se você não souber quais são essas situações e tiver acesso a melhorias, não use contêineres de nós vinculados. (std :: list, std :: slist, std :: map, std :: multimap, std :: set, std :: multiset). Essa lista se concentra principalmente em contêineres pequenos e médios, porque (A) é 99,99% do que lidamos com código e (B) Um grande número de elementos precisa de algoritmos personalizados, não de contêineres diferentes.

Mooing Duck
fonte