Decidí escribir una lista vinculada individualmente y tenía el plan en marcha para hacer que la estructura del nodo interno vinculado fuera inmutable.
Sin embargo, me encontré con un inconveniente. Digamos que tengo los siguientes nodos vinculados (de add
operaciones anteriores ):
1 -> 2 -> 3 -> 4
y decir que quiero agregar a 5
.
Para hacer esto, dado que el nodo 4
es inmutable, necesito crear una nueva copia de 4
, pero reemplazar su next
campo con un nuevo nodo que contenga un 5
. El problema ahora 3
es hacer referencia a lo viejo 4
; el que no tiene el anexo 5
. Ahora necesito copiar 3
y reemplazar su next
campo para hacer referencia a la 4
copia, pero ahora 2
hace referencia al viejo 3
...
O, en otras palabras, para hacer un apéndice, parece que se necesita copiar toda la lista.
Mis preguntas:
¿Es correcto mi pensamiento? ¿Hay alguna forma de hacer un anexo sin copiar toda la estructura?
Aparentemente, "Java efectivo" contiene la recomendación:
Las clases deben ser inmutables a menos que haya una muy buena razón para hacerlas mutables ...
¿Es este un buen caso para la mutabilidad?
No creo que este sea un duplicado de la respuesta sugerida ya que no estoy hablando de la lista en sí; eso obviamente tiene que ser mutable para ajustarse a la interfaz (sin hacer algo como mantener la nueva lista internamente y recuperarla a través de un getter. Sin embargo, pensándolo bien, incluso eso requeriría algo de mutación; simplemente se mantendría al mínimo). Estoy hablando de si las partes internas de la lista deben ser inmutables o no.
fuente
CopyOnWritexxx
clases increíblemente caras que se usan para subprocesos múltiples. Nadie espera que las colecciones sean realmente inmutables (aunque crea algunas peculiaridades)Respuestas:
Con listas en lenguajes funcionales, casi siempre trabaja con cabeza y cola, el primer elemento y el resto de la lista. Preponer es mucho más común porque, como supusiste, agregar requiere copiar toda la lista (u otras estructuras de datos perezosas que no se parecen exactamente a una lista vinculada).
En los idiomas imperativos, agregar es mucho más común, ya que tiende a sentirse semánticamente más natural, y no le importa invalidar referencias a versiones anteriores de la lista.
Como ejemplo de por qué los preparativos previos no requieren copiar toda la lista, considere que tiene:
Anteponer a
1
te da:Pero tenga en cuenta que no importa si alguien más sigue teniendo una referencia
2
como encabezado de su lista, porque la lista es inmutable y los enlaces solo van en una dirección. No hay forma de saber1
si existe incluso si solo tiene una referencia2
. Ahora, si agregaste una5
a cualquiera de las listas, tendrías que hacer una copia de toda la lista, porque de lo contrario también aparecería en la otra lista.fuente
Está en lo correcto, anexar requiere copiar toda la lista si no está dispuesto a mutar ningún nodo en su lugar. Porque necesitamos establecer el
next
puntero del (ahora) penúltimo nodo, que en una configuración inmutable crea un nuevo nodo, y luego necesitamos establecer elnext
puntero del penúltimo nodo, y así sucesivamente.Creo que el problema central aquí no es la inmutabilidad, ni la
append
operación es desaconsejada. Ambos están perfectamente bien en sus respectivos dominios. Mezclarlos es malo: la interfaz natural (eficiente) para una lista inmutable enfatizó la manipulación al principio de la lista, pero para las listas mutables a menudo es más natural construir la lista agregando sucesivamente los elementos del primero al último.Por lo tanto, le sugiero que decida: ¿Desea una interfaz efímera o persistente? ¿La mayoría de las operaciones producen una nueva lista y dejan una versión no modificada accesible (persistente), o escribes código como este (efímero):
Ambas opciones están bien, pero la implementación debe reflejar la interfaz: una estructura de datos persistente se beneficia de nodos inmutables, mientras que una efímera necesita mutabilidad interna para cumplir realmente las promesas de rendimiento que hace implícitamente.
java.util.List
y otras interfaces son efímeras: implementarlas en una lista inmutable es inapropiado y, de hecho, representa un peligro para el rendimiento. Los buenos algoritmos en estructuras de datos mutables a menudo son bastante diferentes de los buenos algoritmos en estructuras de datos inmutables, por lo que vestir una estructura de datos inmutable como mutable (o viceversa) invita a algoritmos malos.Si bien una lista persistente tiene algunas desventajas (sin anexos eficientes), esto no tiene por qué ser un problema grave cuando se programa funcionalmente: muchos algoritmos se pueden formular de manera eficiente cambiando la mentalidad y utilizando funciones de orden superior como
map
ofold
(por nombrar dos relativamente primitivas) ), o pretendiendo repetidamente. Además, nadie lo obliga a usar solo esta estructura de datos: cuando otros (efímeros o persistentes pero más sofisticados) son más apropiados, úselos. También debo señalar que las listas persistentes tienen algunas ventajas para otras cargas de trabajo: comparten sus colas, lo que puede conservar la memoria.fuente
Si tiene una lista enlazada individualmente, trabajará con el frente si es más de lo que haría con el reverso.
Los lenguajes funcionales como prolog y haskel proporcionan formas fáciles de obtener el elemento frontal y el resto de la matriz. Anexando al reverso hay una operación O (n) con copias de cada nodo.
fuente
List
interfaz (aunque podría estar equivocado allí). Sin embargo, no creo que ese bit realmente responda la pregunta. Toda la lista aún necesitaría ser copiada; solo haría que el acceso al último elemento agregado sea más rápido ya que no se requeriría un recorrido completo.java.util.List
Como otros han señalado, tiene razón en que una lista inmutable enlazada individualmente requiere copiar toda la lista al realizar operaciones de adición.
A menudo, puede utilizar la solución alternativa para implementar su algoritmo en términos de
cons
operaciones (anteponer) y luego invertir la lista final una vez. Esto todavía necesita copiar la lista una vez, pero la sobrecarga de complejidad es lineal en la longitud de la lista, mientras que al usar repetidamente append puede obtener fácilmente una complejidad cuadrática.Las listas de diferencias (ver, por ejemplo, aquí ) son una alternativa interesante. Una lista de diferencias envuelve una lista y proporciona una operación de adición en tiempo constante. Básicamente, trabaja con el contenedor todo el tiempo que necesite agregar, y luego vuelve a convertirlo en una lista cuando haya terminado. Esto es de alguna manera similar a lo que haces cuando usas a
StringBuilder
para construir una cadena, y al final obtienes el resultado como unString
(¡inmutable!) LlamandotoString
. Una diferencia es que aStringBuilder
es mutable, pero una lista de diferencias es inmutable. Además, cuando convierte una lista de diferencias de nuevo en una lista, aún tiene que construir la lista nueva completa, pero, nuevamente, solo necesita hacer esto una vez.Debería ser bastante fácil implementar una
DList
clase inmutable que proporcione una interfaz similar a la de HaskellData.DList
.fuente
Debes ver este gran video de 2015 React conf del creador de Immutable.js , Lee Byron. Le dará punteros y estructuras para comprender cómo implementar una lista inmutable eficiente que no duplique el contenido. Las ideas básicas son: - siempre que dos listas usen nodos idénticos (el mismo valor, el mismo nodo siguiente), se usa el mismo nodo - cuando las listas comienzan a diferir, se crea una estructura en el nodo de divergencia, que mantiene punteros al siguiente nodo particular de cada lista
Esta imagen de un tutorial de reacción puede ser más clara que mi inglés roto:
fuente
No es estrictamente Java, pero le sugiero que lea este artículo sobre una estructura de datos persistente indexada inmutable pero performante escrita en Scala:
http://www.codecommit.com/blog/scala/implementing-persistent-vectors-in-scala
Dado que es una estructura de datos Scala, también se puede usar desde Java (con un poco más de verbosidad). Se basa en una estructura de datos disponible en Clojure, y estoy seguro de que también hay una biblioteca Java más "nativa" que lo ofrece.
Además, una nota sobre la construcción de estructuras de datos inmutables: lo que generalmente desea es algún tipo de "Generador", que le permite "mutar" su estructura de datos "en construcción" añadiéndole elementos (dentro de un solo hilo); una vez que haya terminado de agregar, llama a un método en el objeto "en construcción" como
.build()
o.result()
, que "construye" el objeto, proporcionándole una estructura de datos inmutable que luego puede compartir de manera segura.fuente
Un enfoque que a veces puede ser útil sería tener dos clases de objetos que contengan listas: un objeto de lista hacia adelante con una
final
referencia al primer nodo en una lista vinculada hacia adelante y una nofinal
referencia inicialmente nula que (cuando no -nulo) identifica un objeto de lista inversa que contiene los mismos elementos en el orden opuesto, y un objeto de lista inversa con unafinal
referencia al último elemento de una lista vinculada inversa y una referencia no final inicialmente nula que (cuando no -null) identifica un objeto de lista directa que contiene los mismos elementos en orden opuesto.Anteponer un elemento a una lista de reenvío o agregar un elemento a una lista inversa simplemente requeriría crear un nuevo nodo que se vincule con el nodo identificado por la
final
referencia y crear un nuevo objeto de lista del mismo tipo que el original, con unafinal
referencia a ese nuevo nodo.Agregar un elemento a una lista de reenvío o anteponer una lista de reversa requeriría tener una lista del tipo opuesto; la primera vez que se hace con una lista particular, debe crear un nuevo objeto del tipo opuesto y almacenar una referencia; repetir la acción debería reutilizar la lista.
Tenga en cuenta que el estado externo de un objeto de lista se considerará igual si su referencia a una lista de tipo opuesto es nula o identifica una lista de orden opuesto. No habría necesidad de hacer ninguna variable
final
, incluso cuando se usa código de subprocesos múltiples, ya que cada objeto de la lista tendría unafinal
referencia a una copia completa de su contenido.Si el código en un hilo crea y almacena en caché una copia invertida de una lista, y el campo en el que se almacena esa copia en caché no es volátil, sería posible que el código en otro hilo no vea la lista en caché, pero el único efecto adverso a partir de eso sería que el otro hilo necesitaría hacer un trabajo extra para construir otra copia invertida de la lista. Debido a que tal acción perjudicaría en el peor de los casos la eficiencia, pero no afectaría la corrección, y debido a que las
volatile
variables crean deficiencias en la eficiencia por sí mismas, a menudo será mejor que la variable sea no volátil y acepte la posibilidad de operaciones redundantes ocasionales.fuente