¿Cómo puedo seleccionar eficientemente un contenedor de biblioteca estándar en C ++ 11?


135

Hay una imagen bien conocida (hoja de trucos) llamada "C ++ Container choice". Es un diagrama de flujo para elegir el mejor contenedor para el uso deseado.

¿Alguien sabe si ya hay una versión de C ++ 11?

Este es el anterior: Elección del contenedor eC ++


66
Nunca he visto esto antes. ¡Gracias!
WeaselFox

66
@WeaselFox: ya es parte de C ++ - Preguntas frecuentes aquí en SO.
Alok Save

44
C ++ 11 solo introdujo un nuevo tipo de contenedor verdadero: los contenedores unordered_X. Incluirlos solo confundiría considerablemente la tabla, ya que hay una serie de consideraciones al decidir si una tabla hash es adecuada.
Nicol Bolas

13
James tiene razón, hay más casos para usar el vector que lo que muestra la tabla. La ventaja de la localidad de datos supera en muchos casos la falta de eficiencia en algunas operaciones (más pronto C ++ 11). No encuentro e chart tan útil incluso para c ++ 03
David Rodríguez - dribeas

33
Esto es lindo, pero creo que leer cualquier libro de texto común sobre estructuras de datos lo dejará en un estado en el que no solo podrá reinventar este diagrama de flujo en unos minutos, sino que también conocerá muchas más cosas útiles que este diagrama de flujo pasa por alto.
Andrew Tomazos

Respuestas:


97

No que yo sepa, sin embargo, se puede hacer textualmente, supongo. Además, el gráfico está ligeramente apagado, porque listno es un contenedor tan bueno en general, y tampoco lo es forward_list. Ambas listas son contenedores muy especializados para aplicaciones de nicho.

Para construir dicho gráfico, solo necesita dos pautas simples:

  • Elija para la semántica primero
  • Cuando hay varias opciones disponibles, elija la más simple

Preocuparse por el rendimiento suele ser inútil al principio. Las grandes consideraciones de O realmente solo entran en vigencia cuando comienzas a manejar unos pocos miles (o más) de artículos.

Hay dos grandes categorías de contenedores:

  • Contenedores asociativos : tienen una findoperación
  • Contenedores de secuencia simple

y entonces usted puede construir varios adaptadores en la parte superior de ellos: stack, queue, priority_queue. Dejaré los adaptadores aquí, están lo suficientemente especializados como para ser reconocibles.


Pregunta 1: ¿ asociativa ?

  • Si necesita buscar fácilmente por una tecla, entonces necesita un contenedor asociativo
  • Si necesita ordenar los elementos, entonces necesita un contenedor asociativo ordenado
  • De lo contrario, salte a la pregunta 2.

Pregunta 1.1: Pedido ?

  • Si no necesita un pedido específico, use un unordered_contenedor, de lo contrario use su contraparte ordenada tradicional.

Pregunta 1.2: ¿ Clave separada ?

  • Si la clave está separada del valor, use a map, de lo contrario use unset

Pregunta 1.3: ¿ Duplicados ?

  • Si desea mantener duplicados, use a multi, de lo contrario no lo haga.

Ejemplo:

Supongamos que tengo varias personas con una ID única asociada a ellas, y me gustaría recuperar los datos de una persona de su ID de la manera más simple posible.

  1. Quiero una findfunción, por lo tanto, un contenedor asociativo

    1.1. No podría importarme menos el orden, por lo tanto, un unordered_contenedor

    1.2. Mi clave (ID) está separada del valor con el que está asociada, por lo tanto, unmap

    1.3. La ID es única, por lo tanto, no debe introducirse ningún duplicado.

La respuesta final es: std::unordered_map<ID, PersonData>.


Pregunta 2: ¿ Memoria estable ?

  • Si los elementos deben ser estables en la memoria (es decir, no deben moverse cuando se modifica el contenedor), entonces use algunos list
  • De lo contrario, salte a la pregunta 3.

Pregunta 2.1: ¿Cuál ?

  • Conformarse con un list; a forward_listsolo es útil para una menor huella de memoria.

Pregunta 3: ¿ Tamaño dinámico ?

  • Si el contenedor tiene un tamaño conocido (en el momento de la compilación), y este tamaño no se modificará durante el curso del programa, y los elementos son construibles por defecto o puede proporcionar una lista de inicialización completa (usando la { ... }sintaxis), entonces use un array. Reemplaza la matriz C tradicional, pero con funciones convenientes.
  • De lo contrario, salte a la pregunta 4.

Pregunta 4: doble final ?

  • Si desea eliminar elementos de la parte delantera y trasera, use a deque, de lo contrario use a vector.

Notará que, por defecto, a menos que necesite un contenedor asociativo, su elección será a vector. Resulta que también es la recomendación de Sutter y Stroustrup .


55
+1, pero con algunas notas: 1) arrayno requiere un tipo constructivo predeterminado; 2) elegir el multis no se trata tanto de que se permitan duplicados sino más bien de si mantenerlos importa (puede poner duplicados en no multicontenedores, simplemente sucede que solo se conserva uno).
R. Martinho Fernandes

2
El ejemplo está un poco fuera de lugar. 1) podemos "encontrar" (no la función miembro, el "<algoritmo>" uno) en un contenedor no asociativo, 1.1) si necesitamos encontrar "eficientemente", y sin ordenar_ será O (1) y no O ( log n).
BlakBat

44
@BlakBat: map.find(key)es mucho más apetecible que std::find(map.begin(), map.end(), [&](decltype(map.front()) p) { return p.first < key; }));sin embargo, por lo tanto, es importante, semánticamente, que findsea ​​una función miembro en lugar de una <algorithm>. En cuanto a O (1) vs O (log n), no afecta la semántica; Eliminaré el "eficientemente" del ejemplo y lo reemplazaré con "fácilmente".
Matthieu M.

"Si los elementos deben ser estables en la memoria ... entonces use alguna lista" ... hmmm, ¿pensé que también dequetenía esta propiedad?
Martin Ba

@ MartinBa: Sí y no. En a dequelos elementos son estables solo si presiona / pop en cualquier extremo; Si comienza a insertar / borrar en el medio, se barajan hasta N / 2 elementos para llenar el espacio creado.
Matthieu M.

51

Me gusta la respuesta de Matthieu, pero voy a reformular el diagrama de flujo de esta manera:

Cuándo NO usar std :: vector

Por defecto, si necesita un contenedor de cosas, úselo std::vector. Por lo tanto, cualquier otro contenedor solo se justifica proporcionando alguna alternativa de funcionalidad a std::vector.

Constructores

std::vectorrequiere que su contenido sea movible, ya que necesita poder barajar los elementos. Esta no es una carga terrible para colocar en los contenidos (tenga en cuenta que no se requieren constructores predeterminados , gracias, emplaceetc.). Sin embargo, la mayoría de los otros contenedores no requieren ningún constructor particular (de nuevo, gracias a emplace). Entonces, si tiene un objeto en el que no puede implementar absolutamente un constructor de movimiento, tendrá que elegir otra cosa.

A std::dequesería el reemplazo general, que tiene muchas de las propiedades de std::vector, pero solo puede insertar en cualquiera de los extremos de la deque. Las inserciones en el medio requieren movimiento. A std::listno impone ningún requisito sobre su contenido.

Necesita Bools

std::vector<bool>no es. Bueno, es estándar. Pero no es una vectoren el sentido habitual, ya que las operaciones que std::vectornormalmente permiten están prohibidas. Y ciertamente no contiene bools .

Por lo tanto, si necesita un vectorcomportamiento real de un contenedor de bools, no lo obtendrá std::vector<bool>. Por lo tanto, tendrá que pagar con a std::deque<bool>.

buscando

Si necesita encontrar elementos en un contenedor, y la etiqueta de búsqueda no puede ser solo un índice, entonces es posible que deba abandonar std::vectora favor de sety map. Tenga en cuenta la palabra clave " puede "; un ordenado std::vectores a veces una alternativa razonable. O Boost.Container's flat_set/map, que implementa un ordenado std::vector.

Ahora hay cuatro variaciones de estos, cada uno con sus propias necesidades.

  • Use a mapcuando la etiqueta de búsqueda no sea lo mismo que el elemento que está buscando. De lo contrario, utilice a set.
  • Úselo unorderedcuando tenga muchos elementos en el contenedor y el rendimiento de la búsqueda sea absolutamente necesario O(1), en lugar de hacerlo O(logn).
  • Úselo multisi necesita varios elementos para tener la misma etiqueta de búsqueda.

Ordenar

Si necesita un contenedor de elementos para ordenar siempre en función de una operación de comparación particular, puede usar a set. O bien, multi_setsi necesita varios elementos para tener el mismo valor.

O puede usar un ordenado std::vector, pero tendrá que mantenerlo ordenado.

Estabilidad

Cuando los iteradores y las referencias se invalidan, a veces es una preocupación. Si necesita una lista de elementos, de modo que tenga iteradores / punteros a esos elementos en varios otros lugares, entonces std::vectorel enfoque de invalidación puede no ser apropiado. Cualquier operación de inserción puede causar invalidación, dependiendo del tamaño y la capacidad actual.

std::listofrece una garantía firme: un iterador y sus referencias / punteros asociados solo se invalidan cuando el artículo en sí se retira del contenedor. std::forward_listestá ahí si la memoria es una preocupación seria.

Si esa es una garantía demasiado fuerte, std::dequeofrece una garantía más débil pero útil. La invalidación es el resultado de las inserciones en el medio, pero las inserciones en la cabeza o la cola solo causan la invalidación de los iteradores , no los punteros / referencias a los elementos en el contenedor.

Rendimiento de inserción

std::vector solo proporciona una inserción económica al final (e incluso entonces, se vuelve costoso si se sopla la capacidad).

std::listes costoso en términos de rendimiento (cada elemento recién insertado cuesta una asignación de memoria), pero es consistente . También ofrece la capacidad ocasionalmente indispensable de mezclar elementos sin prácticamente ningún costo de rendimiento, así como de intercambiar artículos con otros std::listcontenedores del mismo tipo sin pérdida de rendimiento. Si tiene que mezclar las cosas mucho , el uso std::list.

std::dequeproporciona inserción / extracción en tiempo constante en la cabeza y la cola, pero la inserción en el medio puede ser bastante costosa. Por lo tanto, si necesita agregar / quitar elementos del frente y de la parte posterior, std::dequepodría ser lo que necesita.

Cabe señalar que, gracias a la semántica de movimiento, el std::vectorrendimiento de inserción puede no ser tan malo como solía ser. Algunas implementaciones implementaron una forma de mover copia de elementos basada en la semántica (la llamada "swaptimization"), pero ahora que mover es parte del lenguaje, es obligatorio por el estándar.

Sin asignaciones dinámicas

std::arrayes un buen contenedor si desea la menor cantidad posible de asignaciones dinámicas. Es solo una envoltura alrededor de una matriz C; Esto significa que su tamaño debe conocerse en tiempo de compilación . Si puedes vivir con eso, entonces úsalo std::array.

Dicho esto, usar std::vectory usar reserveun tamaño funcionaría igual de bien para un acotado std::vector. De esta forma, el tamaño real puede variar y solo se obtiene una asignación de memoria (a menos que se reduzca la capacidad).


1
Bueno, también me gusta mucho tu respuesta :) WRT mantiene un vector ordenado, además de std::sortque también std::inplace_mergees interesante colocar fácilmente nuevos elementos (en lugar de una llamada std::lower_bound+ std::vector::insert). Es bueno aprender flat_sety flat_map!
Matthieu M.

2
Tampoco puede usar un vector con tipos alineados de 16 bytes. También también es un buen reemplazo para vector<bool>es vector<char>.
Inverso

@Inverse: "Tampoco puede usar un vector con tipos alineados de 16 bytes". ¿Dice quién? Si std::allocator<T>no admite esa alineación (y no sé por qué no lo haría), siempre puede usar su propio asignador personalizado.
Nicol Bolas

2
@Inverse: C ++ 11 std::vector::resizetiene una sobrecarga que no toma un valor (solo toma el nuevo tamaño; cualquier elemento nuevo se construirá por defecto en el lugar). Además, ¿por qué los compiladores no pueden alinear correctamente los parámetros de valor, incluso cuando se declara que tienen esa alineación?
Nicol Bolas

1
bitsetpara bool si conoce el tamaño de antemano es.cppreference.com/w/cpp/utility/bitset
bendervader

25

Aquí está la versión C ++ 11 del diagrama de flujo anterior. [publicado originalmente sin atribución a su autor original, Mikael Persson ]


2
@NO_NAME Wow, me alegra que alguien se haya molestado en citar una fuente.
underscore_d

1

Aquí hay un giro rápido, aunque probablemente necesite trabajo

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

Puede notar que esto difiere enormemente de la versión C ++ 03, principalmente debido al hecho de que realmente no me gustan los nodos vinculados. Los contenedores de nodos vinculados generalmente pueden verse superados por un contenedor no vinculado, excepto en algunas situaciones poco frecuentes. Si no sabe cuáles son esas situaciones y tiene acceso a impulso, no use contenedores de nodos vinculados. (std :: list, std :: slist, std :: map, std :: multimap, std :: set, std :: multiset). Esta lista se centra principalmente en contenedores pequeños y medianos, porque (A) es el 99.99% de lo que tratamos en código, y (B) Un gran número de elementos necesitan algoritmos personalizados, no contenedores diferentes.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.