¿Por qué std :: list :: reverse tiene complejidad O (n)?

192

¿Por qué la función inversa para la std::listclase 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.

Curioso
fuente
18
No entiendo por qué la gente rechaza esta pregunta. Es una pregunta perfectamente razonable de hacer. Revertir una lista doblemente vinculada debería llevar O (1) tiempo.
Curioso
46
desafortunadamente, algunas personas confunden los conceptos de "la pregunta es buena" con "la pregunta tiene una buena idea". Me encantan preguntas como esta, en las que básicamente "mi comprensión parece diferente a una práctica comúnmente aceptada, ayúdenme a resolver este conflicto", porque expandir su forma de pensar lo ayuda a resolver muchos más problemas en el futuro. Parece que otros adoptan el enfoque de "eso es un desperdicio de procesamiento en el 99.9999% de los casos, ni siquiera lo piensen". Si te sirve de consuelo, ¡he sido rechazado por mucho, mucho menos!
corsiKa
44
Sí, esta pregunta recibió una cantidad excesiva de votos negativos por su calidad. Probablemente sea lo mismo que quien votó por la respuesta de Blindy. Para ser justos, "revertir una lista doblemente vinculada debería implicar simplemente cambiar los punteros de cabeza y cola" generalmente no es cierto para la lista vinculada estándar que implica que todos aprenden en la escuela secundaria, o para muchas implementaciones que las personas usan. Muchas veces en SO la reacción instintiva inmediata de las personas a la pregunta o respuesta impulsa la decisión de voto positivo / negativo. Si hubiera sido más claro en esa oración o la hubiera omitido, creo que habría recibido menos votos negativos.
Chris Beck
3
O permítame poner la carga de la prueba con usted, @Curious: he preparado una implementación de lista doblemente vinculada aquí: ideone.com/c1HebO . ¿Puede indicar cómo esperaría Reverseque se implementara la función en O (1)?
CompuChip
2
@CompuChip: en realidad, dependiendo de la implementación, puede que no. No necesita un booleano adicional para saber qué puntero usar: solo use el que no le apunta hacia atrás ... por cierto, podría ser automático con una lista vinculada XOR. Entonces, sí, depende de cómo se implemente la lista, y la declaración OP podría aclararse.
Matthieu M.

Respuestas:

187

Hipotéticamente, reversepodrí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.

Ami Tavory
fuente
29
@ MooBoys: No estoy de acuerdo con tu analogía. La diferencia es que, en el caso de una lista, la aplicación podría proporcionar reversecon O(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 que sortesté O (1) y todas las demás operaciones tengan el mismo costo. El punto de la pregunta es que, aparentemente, puedes obtener O(1)reversa gratis si solo te importa la gran O, entonces, ¿por qué no hicieron eso?
Chris Beck
9
Si utilizó una lista vinculada a XOR, la inversión se convertiría en tiempo constante. Sin embargo, un iterador sería más grande e incrementarlo / disminuirlo sería un poco más costoso computacionalmente. Eso podría verse eclipsado por los inevitables accesos a la memoria, aunque para cualquier tipo de lista vinculada.
Deduplicador
3
@IlyaPopov: ¿Todos los nodos realmente necesitan esto? El usuario nunca hace preguntas al nodo de la lista en sí, solo al cuerpo de la lista principal. Entonces acceder al booleano es fácil para cualquier método que el usuario llame. Podría establecer la regla de que los iteradores se invalidan si la lista se invierte y / o almacenar una copia del booleano con el iterador, por ejemplo. Así que creo que no afectaría a la gran O, necesariamente. Lo admito, no fui línea por línea a través de la especificación. :)
Chris Beck
44
@ Kevin: Hm, ¿qué? De todos modos, no puede hacer xor dos punteros directamente, primero debe convertirlos a enteros (obviamente de tipo std::uintptr_t. Luego puede hacerlos xor.)
Deduplicador
3
@Kevin, definitivamente podrías hacer una lista vinculada de XOR en C ++, es prácticamente el lenguaje de póster para este tipo de cosas. Nada dice que tiene que usar std::uintptr_t, puede convertir a una charmatriz 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 si uintptr_tfalta. Algunos si se describe en esta respuesta: stackoverflow.com/questions/14243971/…
Chris Beck
61

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 de

current = current->next;

en el operator++iterador de la lista, obtendría

if (reversed)
  current = current->prev;
else
  current = current->next;

que 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 tO (1) ⇒ tO ( 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::beginy std::list::rbeginpolimó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 reverseque 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.

5gon12eder
fuente
77
@galinette: std::list::reverseno invalida los iteradores.
Benjamin Lindley
1
@galinette Lo siento, leí mal tu comentario anterior como " marca por iterador " en lugar de "marca por nodo " como lo escribiste. Por supuesto, una bandera por nodo sería contraproducente ya que, de nuevo, tendría que hacer un recorrido lineal para voltearlos a todos.
5gon12eder
2
@ 5gon12eder: puede eliminar la ramificación a un costo muy perdido: almacene los punteros nexty preven una matriz, y almacene la dirección como 0o 1. Para iterar hacia adelante, seguirías pointers[direction]y para iterar en reversa pointers[1-direction](o viceversa). Esto aún agregaría un poco de sobrecarga, pero probablemente menos que una rama.
ataúd Jerry
44
Probablemente no pueda almacenar un puntero a la lista dentro de los iteradores. swap()se especifica como tiempo constante y no invalida ningún iterador.
Tavian Barnes
1
@TavianBarnes ¡Maldita sea! Bueno, triple indirección entonces ... (quiero decir, en realidad no es triple. Tendría que almacenar la bandera en un objeto asignado dinámicamente, pero el puntero en el iterador puede, por supuesto, apuntar directamente a ese objeto en lugar de indirectamente sobre la lista)
5gon12eder
37

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:

#include <iostream>
#include <list>
#include <string>
#include <iterator>

template<class Container>
struct reverse_proxy
{
    reverse_proxy(Container& c)
    : _c(c)
    {}

    auto begin() { return std::make_reverse_iterator(std::end(_c)); }
    auto end() { return std::make_reverse_iterator(std::begin(_c)); }

    auto begin() const { return std::make_reverse_iterator(std::end(_c)); }
    auto end() const { return std::make_reverse_iterator(std::begin(_c)); }

    Container& _c;
};

template<class Container>
auto reversed(Container& c)
{
    return reverse_proxy<Container>(c);
}

int main()
{
    using namespace std;
    list<string> l { "the", "cat", "sat", "on", "the", "mat" };

    auto r = reversed(l);
    copy(begin(r), end(r), ostream_iterator<string>(cout, "\n"));

    return 0;
}

Rendimiento esperado:

mat
the
on
sat
cat
the

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.

Richard Hodges
fuente
18

Debido a que tiene que atravesar cada nodo ( ntotal) y actualizar sus datos (el paso de actualización es realmente O(1)). Esto hace que toda la operación O(n*1) = O(n).

Ciego
fuente
27
Porque también necesitas actualizar los enlaces entre cada elemento. Saque un trozo de papel y extráigalo en lugar de votar abajo.
Blindy
11
¿Por qué preguntar si estás tan seguro entonces? Estás desperdiciando nuestro tiempo.
Blindy
28
Los nodos de @Curious Doublely enlazados tienen un sentido de dirección. Razón a partir de ahí.
Zapato
11
@Blindy Una buena respuesta debe estar completa. "Sacar un trozo de papel y dibujarlo", por lo tanto, no debería ser un componente necesario de una buena respuesta. Las respuestas que no son buenas están sujetas a votos negativos.
RM
77
@ Zapato: ¿Tienen que hacerlo? Por favor, investigue la lista de enlaces XOR y similares.
Deduplicador
2

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.

techcomp
fuente
1

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).

danilobraga
fuente
1

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.

SDsolar
fuente