He realizado 3 experimentos diferentes que involucran listas y vectores de C ++.
Aquellos con vectores demostraron ser más eficientes, incluso cuando estaban involucradas muchas inserciones en el medio.
De ahí la pregunta: ¿en qué caso las listas tienen más sentido que los vectores?
Si los vectores parecen más eficientes en la mayoría de los casos, y considerando cuán parecidos son sus miembros, ¿qué ventajas quedan para las listas?
Genere N enteros y colóquelos en un contenedor para que el contenedor permanezca ordenado. La inserción se ha realizado ingenuamente, leyendo elementos uno por uno e insertando el nuevo justo antes del primero más grande.
Con una lista, el tiempo pasa por el techo cuando la dimensión aumenta, en comparación con los vectores.Inserte N enteros al final del contenedor.
Para las listas y los vectores, el tiempo aumentó en el mismo orden de magnitud, aunque fue 3 veces más rápido con los vectores.Inserte N enteros en un contenedor.
Iniciar temporizador.
Ordene el contenedor usando list.sort para listas y std :: sort para vectores. Parar temporizador.
Nuevamente, el tiempo aumenta en el mismo orden de magnitud, pero en promedio es 5 veces más rápido con los vectores.
Podría continuar realizando pruebas y descubriendo un par de ejemplos en los que las listas resultarían mejores.
Pero la experiencia conjunta de ustedes al leer este mensaje podría proporcionar respuestas más productivas.
¿Es posible que haya encontrado situaciones en las que las listas fueron más convenientes de usar o tuvieron un mejor rendimiento?
fuente
list
probablemente le vaya mejor si está eliminando muchos elementos. No creovector
que alguna vez regrese la memoria al sistema hasta que se elimine todo el vector. También tenga en cuenta que su prueba n. ° 1 no prueba el tiempo de inserción solo. Es una prueba que combina búsqueda e inserción. Es encontrar el lugar para insertar dondelist
es lento. La inserción real será más rápida que el vector.Respuestas:
La respuesta corta es que los casos parecen ser pocos y distantes entre sí. Probablemente hay algunos sin embargo.
Una sería cuando necesita almacenar una pequeña cantidad de objetos grandes, especialmente, objetos que son tan grandes que no es práctico asignar espacio para unos pocos más. Básicamente, no hay forma de evitar que un vector o deque asigne espacio para objetos adicionales: es cómo se definen (es decir, deben asignar espacio adicional para cumplir con sus requisitos de complejidad). Si no puede permitir que se asigne ese espacio adicional,
std::list
puede ser el único contenedor estándar que satisfaga sus necesidades.Otra sería cuando / si almacenas un iterador en un punto "interesante" en una lista durante un período prolongado de tiempo, y cuando haces inserciones y / o eliminaciones, (casi) siempre lo haces desde un punto en el que ya tiene un iterador, por lo que no recorre la lista para llegar al punto donde va a realizar la inserción o eliminación. Obviamente, lo mismo se aplica si trabaja con más de un lugar, pero aún planea almacenar un iterador en cada lugar con el que es probable que trabaje, por lo que manipula la mayoría de los lugares a los que puede llegar directamente y rara vez recorre la lista para obtener a esos puntos
Para un ejemplo del primero, considere un navegador web. Puede mantener una lista vinculada de
Tab
objetos, con cada objeto de pestaña representando en la pestaña abierta en el navegador. Cada pestaña puede tener unas pocas docenas de megabytes de datos (o más, especialmente si se trata de un video). Su número típico de pestañas abiertas podría ser fácilmente inferior a una docena, y 100 probablemente esté cerca del extremo superior.Para un ejemplo del segundo, considere un procesador de texto que almacena texto como una lista vinculada de capítulos, cada uno de los cuales puede contener una lista vinculada de (digamos) párrafos. Cuando el usuario está editando, generalmente van a encontrar un lugar en particular donde van a editar, y luego harán una buena cantidad de trabajo en ese lugar (o dentro de ese párrafo, de todos modos). Sí, se moverán de un párrafo a otro de vez en cuando, pero en la mayoría de los casos será un párrafo cerca de donde ya estaban trabajando.
De vez en cuando (cosas como buscar y reemplazar globalmente) terminas recorriendo todos los elementos en todas las listas, pero es bastante poco común, e incluso cuando lo haces, probablemente harás suficiente trabajo buscando dentro de un elemento en la lista, que el tiempo para recorrer la lista es casi intrascendente.
Tenga en cuenta que, en un caso típico, es probable que esto también se ajuste al primer criterio: un capítulo contiene un número bastante pequeño de párrafos, cada uno de los cuales es bastante grande (al menos en relación con el tamaño de los punteros en el nodo, y tal). Del mismo modo, tiene un número relativamente pequeño de capítulos, cada uno de los cuales puede ser de varios kilobytes más o menos.
Dicho esto, tengo que admitir que ambos ejemplos son probablemente un poco inventados, y aunque una lista vinculada podría funcionar perfectamente bien para ambos, probablemente tampoco proporcionaría una gran ventaja en ninguno de los casos. En ambos casos, por ejemplo, es poco probable que la asignación de espacio adicional en un vector para algunas páginas web / pestañas (vacías) o algunos capítulos vacíos sea un problema real.
fuente
std::vector
puntero será más eficiente que todos los objetos de nodo de lista enlazados.std::vector<std::unique_ptr<T>>
podría ser una buena alternativa.Según el propio Bjarne Stroustrup, los vectores siempre deben ser la colección predeterminada para las secuencias de datos. Puede elegir la lista si desea optimizar para la inserción y eliminación de elementos, pero normalmente no debería hacerlo. El costo de la lista es un recorrido lento y el uso de memoria.
Él habla sobre esto en esta presentación .
Aproximadamente a las 0:44 habla sobre vectores vs. listas en general.
Alrededor de la 1:08, se le hace una pregunta sobre este tema.
fuente
El único lugar donde generalmente uso listas es donde necesito borrar elementos y no invalidar iteradores.
std::vector
invalida todos los iteradores al insertar y borrar.std::list
garantiza que los iteradores de los elementos existentes sigan siendo válidos después de insertar o eliminar.fuente
Además de las otras respuestas ya proporcionadas, las listas tienen ciertas características que no existen en los vectores (porque serían increíblemente costosas). Las operaciones de empalme y fusión son las más significativas. Si con frecuencia tiene un montón de listas que deben agregarse o fusionarse, una lista es probablemente una buena opción.
Pero si no necesita realizar estas operaciones, entonces probablemente no.
fuente
La falta de memoria caché inherente a las páginas vinculadas tiende a hacer que sean descartadas por completo por muchos desarrolladores de C ++, y con una buena justificación en esa forma predeterminada.
Las listas enlazadas aún pueden ser maravillosas
Sin embargo, las listas vinculadas pueden ser maravillosas cuando están respaldadas por un asignador fijo que les devuelve esa localidad espacial de la que carecen intrínsecamente.
Donde se destacan es que podemos dividir una lista en dos listas, por ejemplo, simplemente almacenando un nuevo puntero y manipulando uno o dos punteros. Podemos mover nodos de una lista a otra en tiempo constante mediante la simple manipulación del puntero, y una lista vacía simplemente puede tener el costo de memoria de un solo
head
puntero.Acelerador de cuadrícula simple
Como ejemplo práctico, considere una simulación visual 2D. Tiene una pantalla de desplazamiento con un mapa que abarca 400x400 (160,000 celdas de cuadrícula) que se usa para acelerar cosas como la detección de colisión entre millones de partículas que se mueven en cada cuadro (evitamos los árboles cuádruples aquí, ya que en realidad tienden a funcionar peor con este nivel de datos dinámicos). Una gran cantidad de partículas se mueven constantemente en cada cuadro, lo que significa que pasan de residir en una celda de la rejilla a otra constantemente.
En este caso, si cada partícula es un nodo de lista enlazado individualmente, cada celda de la cuadrícula puede comenzar como un
head
puntero que apuntanullptr
. Cuando nace una nueva partícula, simplemente la colocamos en la celda de la cuadrícula donde reside configurando elhead
puntero de esa celda para que apunte a este nodo de partículas. Cuando una partícula se mueve de una celda a la siguiente, simplemente manipulamos punteros.Esto puede ser mucho más eficiente que almacenar 160,000
vectors
para cada celda de la cuadrícula y empujar hacia atrás y borrar desde el medio todo el tiempo cuadro por cuadro.std :: list
Sin embargo, esto es para listas enrolladas a mano, intrusivas y enlazadas individualmente respaldadas por un asignador fijo.
std::list
representa una lista doblemente vinculada y puede que no sea tan compacta cuando está vacía como un puntero único (varía según la implementación del proveedor), además es un poco difícil implementar asignadores personalizados enstd::allocator
forma.Debo admitir que nunca uso en
list
absoluto. ¡Pero las listas enlazadas pueden seguir siendo maravillosas! Sin embargo, no son maravillosas por las razones por las que las personas a menudo se sienten tentadas a usarlas, y no son tan maravillosas a menos que estén respaldadas por un asignador fijo muy eficiente que mitigue al menos muchas fallas de página obligatorias y errores de caché asociados.fuente
std::forward_list
.Debe considerar el tamaño de los elementos en el contenedor.
int
El vector de elementos es muy rápido ya que la mayoría de los datos se ajustan dentro del caché de la CPU (y las instrucciones SIMD probablemente se pueden usar para copiar datos).Si el tamaño del elemento es mayor, entonces el resultado de las pruebas 1 y 3 podría cambiar significativamente.
De una comparación de rendimiento muy completa :
(como nota al margen
std::deque
es una estructura de datos muy subestimada).Desde un punto de vista conveniente,
std::list
garantiza que los iteradores nunca se invaliden al insertar y eliminar otros elementos. A menudo es un aspecto clave.fuente
La razón más destacada para usar listas en mi opinión es la invalidación de iteradores : si agrega / elimina elementos a un vector, todos los punteros, referencias, iteradores que mantuvo a elementos particulares de este vector pueden invalidarse y provocar errores sutiles. . o fallas de segmentación.
Este no es el caso con las listas.
Las reglas precisas para todos los contenedores estándar se dan en esta publicación de StackOverflow .
fuente
En resumen, no hay una buena razón para usar
std::list<>
:Si necesita un contenedor sin clasificar,
std::vector<>
reglas.(Eliminar elementos reemplazándolos con el último elemento del vector).
Si necesita un contenedor ordenado,
std::vector<shared_ptr<>>
reglas.Si necesita un índice escaso,
std::unordered_map<>
reglas.Eso es.
Me parece que solo hay una situación en la que tiendo a usar una lista vinculada: cuando tengo objetos preexistentes que necesitan estar conectados de alguna manera para implementar alguna lógica de aplicación adicional. Sin embargo, en ese caso, nunca uso
std::list<>
, sino que recurro al siguiente puntero (inteligente) dentro del objeto, especialmente porque la mayoría de los casos de uso dan como resultado un árbol en lugar de una lista lineal. En algunos casos, la estructura resultante es una lista vinculada, en otros, es un árbol o un gráfico acíclico dirigido. El objetivo principal de estos punteros es siempre construir una estructura lógica, nunca administrar objetos. Tenemosstd::vector<>
para eso.fuente
Debe mostrar cómo estaba haciendo los insertos en su primera prueba. Su segunda y tercera prueba, el vector ganará fácilmente.
Un uso significativo de las listas es cuando debe admitir la eliminación de elementos mientras itera. Cuando se modifica el vector, todos los iteradores son (potencialmente) inválidos. Con una lista, solo un iterador del elemento eliminado no es válido. Todos los demás iteradores siguen siendo válidos.
El orden típico de uso para contenedores es vector, deque, luego lista. La elección del contenedor generalmente se basa en push_back elegir vector, pop_front elegir deque, insertar elegir lista.
fuente
Un factor en el que puedo pensar es que a medida que crece un vector, la memoria libre se fragmentará a medida que el vector desasigne su memoria y asigne un bloque más grande una y otra vez. Esto no será un problema con las listas.
Esto se suma al hecho de que una gran cantidad de
push_back
s sin reserva también causará una copia durante cada cambio de tamaño, lo que lo hace ineficiente. Insertar en el medio de manera similar provoca un movimiento de todos los elementos hacia la derecha, y es aún peor.Sin embargo, no sé si esto es una preocupación importante, pero fue la razón que se me dio en mi trabajo (desarrollo de juegos móviles), para evitar vectores.
fuente