Una lista doblemente vinculada tiene una sobrecarga mínima (solo otro puntero por celda), y le permite agregar a ambos extremos, ir y venir y, en general, divertirse mucho.
data-structures
functional-programming
Elliot Gorokhovsky
fuente
fuente
next
puntero del elemento anterior apunte al elemento siguiente y que elprev
puntero del elemento siguiente apunte al elemento anterior. Sin embargo, uno de esos dos elementos se crea antes que el otro, lo que significa que uno de esos elementos debe tener un puntero apuntando a un objeto que aún no existe. Recuerde, no puede crear primero un elemento, luego el otro y luego configurar los punteros: son inmutables. (Nota: Sé que hay una manera, explotando la pereza, llamada "Atar el nudo".)Respuestas:
Bueno, si miras un poco más profundo, ambos incluyen matrices en el lenguaje base también:
Sin embargo, la instrucción de programación funcional ha enfatizado durante mucho tiempo las listas de un solo enlace sobre las matrices o las listas de doble enlace. Muy probablemente exagerado, de hecho. Sin embargo, hay varias razones para ello.
El primero es que las listas de enlaces únicos son uno de los tipos de datos recursivos más simples y útiles. Un equivalente definido por el usuario del tipo de lista de Haskell se puede definir así:
El hecho de que las listas sean un tipo de datos recursivo significa que las funciones que funcionan en las listas generalmente usan recursividad estructural . En términos de Haskell: el patrón coincide en los constructores de la lista y se repite en una subparte de la lista. En estas dos definiciones de funciones básicas, uso la variable
as
para referirme al final de la lista. Tenga en cuenta que las llamadas recursivas "descienden" en la lista:Esta técnica garantiza que su función finalizará para todas las listas finitas, y también es una buena técnica de resolución de problemas: tiende a dividir los problemas de manera natural en subpartes más simples y más manejables.
Por lo tanto, las listas de enlace único son probablemente el mejor tipo de datos para presentar a los estudiantes estas técnicas, que son muy importantes en la programación funcional.
La segunda razón es menos una razón de "por qué listas unidas", pero más una razón de "por qué no listas o matrices de doble enlace": esos últimos tipos de datos a menudo requieren mutación (variables modificables), que la programación funcional muy a menudo huye de Entonces, como sucede:
vector
embargo, las bibliotecas Haskell recientes han encontrado técnicas que mejoran en gran medida este problema).La tercera y última razón se aplica principalmente a los lenguajes perezosos como Haskell: en la práctica, las listas perezosas de un solo enlace son a menudo más similares a los iteradores que a las listas en memoria propiamente dichas. Si su código consume los elementos de una lista secuencialmente y los arroja a medida que avanza, el código objeto solo materializará las celdas de la lista y su contenido a medida que avance por la lista.
Esto significa que no es necesario que toda la lista exista en la memoria a la vez, solo la celda actual. Las celdas anteriores a la actual se pueden recolectar basura (lo que no sería posible con una lista de doble enlace); las celdas posteriores a la actual no necesitan ser calculadas hasta que llegue allí.
Va incluso más allá que eso. Hay una técnica utilizada en varias bibliotecas populares de Haskell, llamada fusión , donde el compilador analiza su código de procesamiento de listas y detecta listas intermedias que se generan y consumen de forma secuencial y luego "desechan". Con este conocimiento, el compilador puede eliminar completamente la asignación de memoria de las celdas de esas listas. Esto significa que una lista de un solo enlace en un programa fuente de Haskell, después de la compilación, podría convertirse en un bucle en lugar de una estructura de datos.
Fusion es también la técnica que
vector
usa la biblioteca mencionada para generar código eficiente para matrices inmutables. Lo mismo ocurre con las bibliotecas extremadamente popularesbytestring
(matrices de bytes) ytext
(cadenas Unicode), que se construyeron como un reemplazo para elString
tipo nativo no muy grande de Haskell (que es lo mismo que[Char]
una lista de caracteres con un solo enlace). Entonces, en el Haskell moderno hay una tendencia en la que los tipos de matriz inmutables con soporte de fusión se están volviendo muy comunes.La fusión de listas se ve facilitada por el hecho de que en una lista enlazada puede avanzar pero nunca hacia atrás . Esto plantea un tema muy importante en la programación funcional: usar la "forma" de un tipo de datos para derivar la "forma" de un cálculo. Si desea procesar elementos secuencialmente, una lista de un solo enlace es un tipo de datos que, cuando la consume con recursividad estructural, le proporciona ese patrón de acceso de forma muy natural. Si desea utilizar una estrategia de "divide y vencerás" para atacar un problema, entonces las estructuras de datos en árbol tienden a soportarlo muy bien.
Muchas personas abandonan el vagón de programación funcional desde el principio, por lo que se exponen a las listas de enlaces únicos pero no a las ideas subyacentes más avanzadas.
fuente
Porque funcionan bien con la inmutabilidad. Supongamos que tiene dos listas inmutables,
[1, 2, 3]
y[10, 2, 3]
. Representados como listas enlazadas individualmente donde cada elemento de la lista es un nodo que contiene el elemento y un puntero al resto de la lista, se verían así:¿Ves cómo las
[2, 3]
porciones son idénticas? Con estructuras de datos mutables, son dos listas diferentes porque el código que escribe datos nuevos en uno de ellos no debe afectar el código que usa el otro. Sin embargo, con datos inmutables , sabemos que el contenido de las listas nunca cambiará y que el código no puede escribir nuevos datos. Entonces podemos reutilizar las colas y hacer que las dos listas compartan parte de su estructura:Dado que el código que usa las dos listas nunca las mutará, nunca tenemos que preocuparnos de que los cambios en una lista afecten a la otra. Esto también significa que al agregar un elemento al principio de la lista, no tiene que copiar y hacer una lista completamente nueva.
Sin embargo, si intenta representar
[1, 2, 3]
y[10, 2, 3]
como listas doblemente vinculadas:Ahora las colas ya no son idénticas. El primero
[2, 3]
tiene un puntero a1
la cabeza, pero el segundo tiene un puntero a10
. Además, si desea agregar un nuevo elemento al encabezado de la lista, debe mutar el encabezado anterior de la lista para que apunte al nuevo encabezado.El problema de múltiples cabezas podría solucionarse haciendo que cada nodo almacene una lista de cabezas conocidas y que la creación de nuevas listas modifique eso, pero luego debe trabajar para mantener esa lista en los ciclos de recolección de basura cuando las versiones de la lista con diferentes cabezas tienen diferentes vidas debido a que se utilizan en diferentes piezas de código. Agrega complejidad y gastos generales, y la mayoría de las veces no vale la pena.
fuente
xs
construye1:xs
en un lugar y10:xs
en otro.La respuesta de @ sacundim es mayormente cierta, pero también hay algunas otras ideas importantes sobre el intercambio de diseños de idiomas y requisitos prácticos.
Objetos y referencias
Estos lenguajes generalmente exigen (o asumen) objetos que tienen extensiones dinámicas no vinculadas (o en el lenguaje de C, duración , aunque no exactamente lo mismo debido a las diferencias de significado de los objetos entre estos lenguajes, ver más abajo) de forma predeterminada, evitando referencias de primera clase ( por ejemplo, punteros de objetos en C) y comportamiento impredecible en las reglas semánticas (por ejemplo, comportamiento indefinido de ISO C relacionado con la semántica).
Además, la noción de objetos (de primera clase) en dichos lenguajes es conservadoramente restrictiva: no se especifican y garantizan de forma predeterminada propiedades "locativas". Esto es completamente diferente en algunos lenguajes similares a ALGOL cuyos objetos no tienen extensiones dinámicas no vinculadas (por ejemplo, en C y C ++), donde los objetos básicamente significan algunos tipos de "almacenamiento con tipo", generalmente junto con ubicaciones de memoria.
Codificar el almacenamiento dentro de los objetos tiene algunos beneficios adicionales, como poder adjuntar efectos computacionales deterministas a lo largo de su vida útil, pero es otro tema.
Problemas de simulación de estructuras de datos
Sin referencias de primera clase, las listas enlazadas individualmente no pueden simular muchas estructuras de datos tradicionales (ansiosas / mutables) de manera efectiva y portátil, debido a la naturaleza de la representación de estas estructuras de datos y las limitadas operaciones primitivas en estos idiomas. (Por el contrario, en C, puede derivar listas vinculadas con bastante facilidad incluso en un programa estrictamente conforme ). Y tales estructuras de datos alternativas como matrices / vectores tienen algunas propiedades superiores en comparación con las listas enlazadas en la práctica. Es por eso que R 5 RS introduce nuevas operaciones primitivas.
Pero existen diferencias en los tipos de vectores / matrices frente a listas doblemente vinculadas. A menudo se supone una matriz con O (1) complejidad de tiempo de acceso y menos sobrecarga de espacio, que son excelentes propiedades que no comparten las listas. (Aunque estrictamente hablando, ninguno de los dos está garantizado por ISO C, pero los usuarios casi siempre lo esperan y ninguna implementación práctica violaría estas garantías implícitas demasiado obviamente). , mientras que la iteración hacia atrás / adelante también es compatible con una matriz o un vector (junto con índices enteros) con incluso menos sobrecarga. Por lo tanto, una lista doblemente vinculada no funciona mejor en general. Peor aún, El rendimiento sobre la eficiencia de caché y la latencia en la asignación dinámica de memoria de las listas es catastróficamente peor que el rendimiento de las matrices / vectores cuando se utiliza el asignador predeterminado proporcionado por el entorno de implementación subyacente (por ejemplo, libc). Entonces, sin un tiempo de ejecución muy específico e "inteligente" que optimice en gran medida tales creaciones de objetos, los tipos de matriz / vector a menudo se prefieren a las listas vinculadas. (Por ejemplo, usando ISO C ++, hay una advertencia que
std::vector
debería preferirsestd::list
por defecto.) Por lo tanto, introducir nuevas primitivas para soportar específicamente (doblemente) listas enlazadas definitivamente no es tan beneficioso como para soportar estructuras de datos de matriz / vector en la práctica.Para ser justos, las listas aún tienen algunas propiedades específicas mejores que las matrices / vectores:
Sin embargo, estas propiedades no son demasiado importantes para un lenguaje con soporte integrado de listas enlazadas individualmente, que ya es capaz de tal uso. Aunque todavía existen diferencias, en los lenguajes con extensiones dinámicas obligatorias de objetos (lo que generalmente significa que hay un recolector de basura que mantiene alejadas las referencias colgantes), la invalidación también puede ser menos importante, dependiendo de los intentos. Entonces, los únicos casos en que las listas doblemente vinculadas pueden ganar son:
Inmutabilidad y alias
En un lenguaje puro como Haskell, los objetos son inmutables. El objeto de Scheme a menudo se usa sin mutación. Tal hecho hace posible mejorar efectivamente la eficiencia de la memoria con la internación de objetos : el intercambio implícito de múltiples objetos con el mismo valor sobre la marcha.
Esta es una estrategia agresiva de optimización de alto nivel en el diseño del lenguaje. Sin embargo, esto implica problemas de implementación. En realidad, introduce alias implícitos a las celdas de almacenamiento subyacentes. Hace que el análisis de alias sea más difícil. Como resultado, es probable que haya menos posibilidades de eliminar la sobrecarga de referencias que no sean de primera clase, incluso los usuarios nunca las tocan. En lenguajes como Scheme, una vez que la mutación no se descarta totalmente, esto también interfiere en el paralelismo. Sin embargo, puede estar bien en un lenguaje perezoso (que de todos modos ya tiene problemas de rendimiento causados por thunks).
Para la programación de propósito general, tal elección del diseño del lenguaje puede ser problemática. Pero con algunos patrones comunes de codificación funcional, los lenguajes parecen funcionar bien.
fuente