¿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.
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.
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 .
+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).
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?
@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::vectorNo: std::arrayNo: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::vectorNo: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_multimapNo: std::unordered_mapNo:Are elements read then removed in a certain order?Yes:Order is:Ordered by element: std::priority_queueFirst in First out: std::queueFirst in Last out: std::stackOther: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.
Respuestas:
No que yo sepa, sin embargo, se puede hacer textualmente, supongo. Además, el gráfico está ligeramente apagado, porque
list
no es un contenedor tan bueno en general, y tampoco lo esforward_list
. Ambas listas son contenedores muy especializados para aplicaciones de nicho.Para construir dicho gráfico, solo necesita dos pautas simples:
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:
find
operacióny 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 ?
Pregunta 1.1: Pedido ?
unordered_
contenedor, de lo contrario use su contraparte ordenada tradicional.Pregunta 1.2: ¿ Clave separada ?
map
, de lo contrario use unset
Pregunta 1.3: ¿ Duplicados ?
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.
Quiero una
find
función, por lo tanto, un contenedor asociativo1.1. No podría importarme menos el orden, por lo tanto, un
unordered_
contenedor1.2. Mi clave (ID) está separada del valor con el que está asociada, por lo tanto, un
map
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 ?
list
Pregunta 2.1: ¿Cuál ?
list
; aforward_list
solo es útil para una menor huella de memoria.Pregunta 3: ¿ Tamaño dinámico ?
{ ... }
sintaxis), entonces use unarray
. Reemplaza la matriz C tradicional, pero con funciones convenientes.Pregunta 4: doble final ?
deque
, de lo contrario use avector
.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 .fuente
array
no requiere un tipo constructivo predeterminado; 2) elegir elmulti
s no se trata tanto de que se permitan duplicados sino más bien de si mantenerlos importa (puede poner duplicados en nomulti
contenedores, simplemente sucede que solo se conserva uno).map.find(key)
es mucho más apetecible questd::find(map.begin(), map.end(), [&](decltype(map.front()) p) { return p.first < key; }));
sin embargo, por lo tanto, es importante, semánticamente, quefind
sea 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".deque
tenía esta propiedad?deque
los 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.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 astd::vector
.Constructores
std::vector
requiere 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,emplace
etc.). Sin embargo, la mayoría de los otros contenedores no requieren ningún constructor particular (de nuevo, gracias aemplace
). Entonces, si tiene un objeto en el que no puede implementar absolutamente un constructor de movimiento, tendrá que elegir otra cosa.A
std::deque
sería el reemplazo general, que tiene muchas de las propiedades destd::vector
, pero solo puede insertar en cualquiera de los extremos de la deque. Las inserciones en el medio requieren movimiento. Astd::list
no impone ningún requisito sobre su contenido.Necesita Bools
std::vector<bool>
no es. Bueno, es estándar. Pero no es unavector
en el sentido habitual, ya que las operaciones questd::vector
normalmente permiten están prohibidas. Y ciertamente no contienebool
s .Por lo tanto, si necesita un
vector
comportamiento real de un contenedor debool
s, no lo obtendrástd::vector<bool>
. Por lo tanto, tendrá que pagar con astd::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::vector
a favor deset
ymap
. Tenga en cuenta la palabra clave " puede "; un ordenadostd::vector
es a veces una alternativa razonable. O Boost.Container'sflat_set/map
, que implementa un ordenadostd::vector
.Ahora hay cuatro variaciones de estos, cada uno con sus propias necesidades.
map
cuando la etiqueta de búsqueda no sea lo mismo que el elemento que está buscando. De lo contrario, utilice aset
.unordered
cuando tenga muchos elementos en el contenedor y el rendimiento de la búsqueda sea absolutamente necesarioO(1)
, en lugar de hacerloO(logn)
.multi
si 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_set
si 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::vector
el 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::list
ofrece 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_list
está ahí si la memoria es una preocupación seria.Si esa es una garantía demasiado fuerte,
std::deque
ofrece 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::list
es 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 otrosstd::list
contenedores del mismo tipo sin pérdida de rendimiento. Si tiene que mezclar las cosas mucho , el usostd::list
.std::deque
proporciona 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::deque
podría ser lo que necesita.Cabe señalar que, gracias a la semántica de movimiento, el
std::vector
rendimiento 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::array
es 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 úsalostd::array
.Dicho esto, usar
std::vector
y usarreserve
un tamaño funcionaría igual de bien para un acotadostd::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).fuente
std::sort
que tambiénstd::inplace_merge
es interesante colocar fácilmente nuevos elementos (en lugar de una llamadastd::lower_bound
+std::vector::insert
). Es bueno aprenderflat_set
yflat_map
!vector<bool>
esvector<char>
.std::allocator<T>
no admite esa alineación (y no sé por qué no lo haría), siempre puede usar su propio asignador personalizado.std::vector::resize
tiene 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?bitset
para bool si conoce el tamaño de antemano es.cppreference.com/w/cpp/utility/bitsetAquí está la versión C ++ 11 del diagrama de flujo anterior. [publicado originalmente sin atribución a su autor original, Mikael Persson ]
fuente
Aquí hay un giro rápido, aunque probablemente necesite trabajo
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.
fuente