¿Por qué agregar a una Lista en Scala tiene una complejidad de tiempo O (n)?

13

Acabo de leer que el tiempo de ejecución de la operación append para a List(: +) crece linealmente con el tamaño de List.

Agregar a un Listparece una operación bastante común. ¿Por qué la forma idiomática de hacer esto es anteponer los componentes y luego invertir la lista? Tampoco puede ser un fallo de diseño, ya que la implementación podría modificarse en cualquier momento.

Desde mi punto de vista, tanto antes como anexar deberían ser O (1).

¿Hay alguna razón legítima para esto?

DPM
fuente
2
Depende de su definición de "legítimo". Scala está muy involucrado en estructuras de datos inmutables, listas anónimas ubicuas, composición funcional, etc. La implementación de la lista predeterminada (sin un puntero mutable adicional a la cola de la lista) funciona bien para ese estilo. Si necesita una lista más poderosa, al menos es muy fácil escribir su propio contenedor que es casi indistinguible de los estándares.
Kilian Foth
1
Relacionado con sitios cruzados: al agregar un elemento al final de una lista en Scala , hay un poco de información sobre su naturaleza. Se parece que una lista en la Scala es inmutable, por lo que necesita para copiarlo, que es O (N).
Puede usar una de las muchas estructuras de datos mutables disponibles, o estructuras de datos inmutables con O (1) tiempo de adición (Vector) que proporciona Scala. List[T]supone que lo está utilizando como lo haría en un lenguaje funcional puro, generalmente trabajando desde la cabeza con deconstrucción y pretendientes.
KChaloux
3
Anteponer colocará el siguiente puntero de nodo del nuevo encabezado a la lista inmutable existente, que no puede cambiar. Eso es O (1).
1
Para el trabajo seminal y el análisis sobre el tema general de las mediciones de complejidad de la estructura de datos en FP pura, lea la tesis de Okasaki que luego se publicó como un libro. Es una lectura muy apreciada y muy buena para cualquiera que esté aprendiendo algo de FP para comprender cómo pensar sobre la organización de datos en FP. También está bien escrito y es fácil de leer y seguir, por lo que es un texto de calidad.
Jimmy Hoffa

Respuestas:

24

Ampliaré mi comentario un poco. La List[T]estructura de datos, desde scala.collection.immutableestá optimizada para funcionar como funciona una lista inmutable en un lenguaje de programación más puramente funcional. Tiene tiempos de preparación previos muy rápidos , y se supone que estará trabajando en la cabeza para casi todo su acceso.

Las listas inmutables tienen tiempos de preparación previos muy rápidos debido al hecho de que modelan sus listas vinculadas como una serie de "celdas en contra". La celda define un valor único y un puntero a la siguiente celda (estilo clásico de lista individualmente vinculada):

Cell [Value| -> Nil]

Cuando antepone una lista, en realidad solo está creando una nueva celda, con el resto de la lista existente apuntando a:

Cell [NewValue| -> [Cell[Value| -> Nil]]

Debido a que la lista es inmutable, puede hacerlo sin ninguna copia real . No hay peligro de que la lista anterior cambie y provoque que todos los valores de su nueva lista se vuelvan inválidos. Sin embargo, pierde la capacidad de tener un puntero mutable al final de su lista como un compromiso.

Esto se presta muy bien para trabajar recursivamente en listas. Digamos que definiste tu propia versión de filter:

def deleteIf[T](list : List[T])(f : T => Boolean): List[T] = list match {
  case Nil => Nil
  case (x::xs) => f(x) match {
    case true => deleteIf(xs)(f)
    case false => x :: deleteIf(xs)(f)
  }
}

Esa es una función recursiva que funciona exclusivamente desde el principio de la lista y aprovecha la coincidencia de patrones a través del :: extractor. Esto es algo que se ve mucho en idiomas como Haskell.

Si realmente desea adiciones rápidas, Scala proporciona muchas estructuras de datos mutables e inmutables para elegir. En el lado mutable, podría investigar ListBuffer. Alternativamente, Vectordesde scala.collection.immutabletiene un tiempo de adición rápido.

KChaloux
fuente
¡Ahora entiendo! Tiene mucho sentido.
DPM
No conozco ninguna Scala, pero ¿no es eso elseun bucle infinito? Creo que debería ser algo así x::deleteIf(xs)(f).
svick
@svick Uh ... sí. Sí lo es. Lo escribí rápidamente y no verifiqué mi código, porque tenía que ir a una reunión: p (¡Debería arreglarse ahora!)
KChaloux
Debido @Jubbat heady tailel acceso a este tipo de lista es muy rápido - más rápido que usar cualquier mapa o matriz basado en hash - es un excelente tipo de funciones recursivas. Esta es una de las razones por las que las listas son un tipo básico en la mayoría de los lenguajes funcionales (por ejemplo, Haskell o Scheme)
itsbruce
Excelente respuesta Quizás agregaría un TL; DR que simplemente dice "porque deberías estar anteponiendo, no agregando" (podría ayudar a aclarar las suposiciones básicas que la mayoría de los desarrolladores tienen sobre Listsy agregar / preponer).
Daniel B