¿Por qué la función inversa para la std::list
clase en la biblioteca estándar de C ++ tiene tiempo de ejecución lineal? Creo que para las listas doblemente vinculadas, la función inversa debería haber sido O (1).
Revertir una lista doblemente vinculada solo debería implicar cambiar los punteros de cabeza y cola.
c++
c++11
stl
linked-list
Curioso
fuente
fuente
Reverse
que se implementara la función en O (1)?Respuestas:
Hipotéticamente,
reverse
podría haber sido O (1) . Allí (de nuevo hipotéticamente) podría haber sido un miembro de la lista booleana que indica si la dirección de la lista vinculada es actualmente la misma o la opuesta a la original donde se creó la lista.Desafortunadamente, eso reduciría el rendimiento de básicamente cualquier otra operación (aunque sin cambiar el tiempo de ejecución asintótico). En cada operación, un booleano necesitaría ser consultado para considerar si seguir un puntero "siguiente" o "anterior" de un enlace.
Dado que esto presumiblemente se consideraba una operación relativamente infrecuente, el estándar (que no dicta implementaciones, solo complejidad) especificaba que la complejidad podía ser lineal. Esto permite que los "siguientes" punteros siempre signifiquen la misma dirección sin ambigüedades, acelerando las operaciones de casos comunes.
fuente
reverse
conO(1)
la complejidad sin afectar a la gran-o de cualquier otra operación , mediante el uso de este truco bandera booleana. Pero, en la práctica, una rama adicional en cada operación es costosa, incluso si técnicamente es O (1). Por el contrario, no puede hacer una estructura de lista en la quesort
esté O (1) y todas las demás operaciones tengan el mismo costo. El punto de la pregunta es que, aparentemente, puedes obtenerO(1)
reversa gratis si solo te importa la gran O, entonces, ¿por qué no hicieron eso?std::uintptr_t
. Luego puede hacerlos xor.)std::uintptr_t
, puede convertir a unachar
matriz y luego XOR los componentes. Sería más lento pero 100% portátil. Probablemente podría hacer que seleccione entre estas dos implementaciones y solo use la segunda como alternativa siuintptr_t
falta. Algunos si se describe en esta respuesta: stackoverflow.com/questions/14243971/…Se podría ser O (1) si la lista almacenaría una bandera que permite intercambiar el significado de los “
prev
” y “next
” punteros de cada nodo. Si invertir la lista sería una operación frecuente, tal adición podría ser de hecho útil y no sé de ninguna razón por la cual la norma actual prohibiría su implementación . Sin embargo, tener dicha bandera haría que el recorrido ordinario de la lista sea más costoso (aunque solo sea por un factor constante) porque en lugar deen el
operator++
iterador de la lista, obtendríaque no es algo que decidas agregar fácilmente. Dado que las listas generalmente se recorren con mucha más frecuencia de lo que se invierten, sería muy imprudente que el estándar ordenara esta técnica. Por lo tanto, se permite que la operación inversa tenga una complejidad lineal. Sin embargo, tenga en cuenta que t ∈ O (1) ⇒ t ∈ O ( n ) por lo que, como se mencionó anteriormente, la implementación de su "optimización" técnicamente estaría permitida.
Si proviene de un entorno Java o similar, es posible que se pregunte por qué el iterador debe verificar la bandera cada vez. ¿No podríamos tener dos tipos de iteradores distintos, ambos derivados de un tipo base común, y tener
std::list::begin
ystd::list::rbegin
polimórficamente devolver el iterador apropiado? Si bien es posible, esto empeoraría aún más las cosas porque avanzar el iterador sería una llamada de función indirecta (difícil de alinear) ahora. En Java, está pagando este precio de forma rutinaria de todos modos, pero, de nuevo, esta es una de las razones por las que muchas personas recurren a C ++ cuando el rendimiento es crítico.Como señaló Benjamin Lindley en los comentarios, dado
reverse
que no está permitido invalidar iteradores, el único enfoque permitido por el estándar parece ser almacenar un puntero en la lista dentro del iterador que causa un acceso doble indirecto a la memoria.fuente
std::list::reverse
no invalida los iteradores.next
yprev
en una matriz, y almacene la dirección como0
o1
. Para iterar hacia adelante, seguiríaspointers[direction]
y para iterar en reversapointers[1-direction]
(o viceversa). Esto aún agregaría un poco de sobrecarga, pero probablemente menos que una rama.swap()
se especifica como tiempo constante y no invalida ningún iterador.Seguramente, dado que todos los contenedores que admiten iteradores bidireccionales tienen el concepto de rbegin () y rend (), esta pregunta es discutible.
Es trivial construir un proxy que invierta los iteradores y acceda al contenedor a través de eso.
Esta no operación es de hecho O (1).
como:
Rendimiento esperado:
Dado esto, me parece que el comité de estándares no se ha tomado el tiempo para ordenar O (1) ordenar en reversa el contenedor porque no es necesario, y la biblioteca estándar se basa en gran medida en el principio de ordenar solo lo estrictamente necesario mientras evitando la duplicación.
Solo mi 2c.
fuente
Debido a que tiene que atravesar cada nodo (
n
total) y actualizar sus datos (el paso de actualización es realmenteO(1)
). Esto hace que toda la operaciónO(n*1) = O(n)
.fuente
También intercambia puntero anterior y siguiente para cada nodo. Por eso se necesita lineal. Aunque se puede hacer en O (1) si la función que usa este LL también toma información sobre LL como entrada, como si está accediendo normalmente o en reversa.
fuente
Solo una explicación de algoritmo. Imagine que tiene una matriz con elementos, luego necesita invertirla. La idea básica es iterar en cada elemento cambiando el elemento en la primera posición a la última posición, el elemento en la segunda posición a la penúltima posición, y así sucesivamente. Cuando llegue al centro de la matriz, todos los elementos cambiarán, por lo tanto, en (n / 2) iteraciones, lo que se considera O (n).
fuente
Es O (n) simplemente porque necesita copiar la lista en orden inverso. Cada operación de elemento individual es O (1) pero hay n de ellos en toda la lista.
Por supuesto, hay algunas operaciones de tiempo constante involucradas en configurar el espacio para la nueva lista y cambiar los punteros después, etc. La notación O no considera constantes individuales una vez que se incluye un factor n de primer orden.
fuente