Articulo de referencia

Lista enlazada

Una lista enlazada es una secuencia de nodos que contienen dos campos: datos (un valor entero, por ejemplo) y un enlace al siguiente nodo. El último nodo está enlazado a un term...

Una lista enlazada es una secuencia de nodos que contienen dos campos: datos (un valor entero, por ejemplo) y un enlace al siguiente nodo. El último nodo está enlazado a un terminador que indica el final de la lista.

En informática , una lista enlazada es una colección lineal de elementos de datos cuyo orden no viene dado por su ubicación física en la memoria. En cambio, cada elemento apunta al siguiente. Es una estructura de datos que consiste en una colección de nodos que, en conjunto, representan una secuencia . En su forma más básica, cada nodo contiene datos y una referencia (es decir, un enlace ) al siguiente nodo de la secuencia. Esta estructura permite la inserción o eliminación eficiente de elementos desde cualquier posición de la secuencia durante la iteración. Las variantes más complejas añaden enlaces adicionales, lo que permite una inserción o eliminación más eficiente de nodos en posiciones arbitrarias. Una desventaja de las listas enlazadas es que el tiempo de acceso a los datos es lineal con respecto al número de nodos en la lista. Debido a que los nodos están enlazados en serie, acceder a cualquier nodo requiere acceder previamente al nodo anterior (lo que introduce dificultades en la segmentación ). Un acceso más rápido, como el acceso aleatorio, no es factible. Los arrays tienen una mejor localidad de caché en comparación con las listas enlazadas.

Las listas enlazadas se encuentran entre las estructuras de datos más simples y comunes. Se pueden usar para implementar varios otros tipos de datos abstractos comunes , como listas , pilas , colas , arreglos asociativos y expresiones S , aunque no es raro implementar esas estructuras de datos directamente sin usar una lista enlazada como base.

La principal ventaja de una lista enlazada sobre un arreglo convencional es que los elementos de la lista se pueden insertar o eliminar fácilmente sin reasignar ni reorganizar toda la estructura, ya que los datos no necesitan almacenarse de forma contigua en memoria o en disco, mientras que reestructurar un arreglo en tiempo de ejecución es una operación mucho más costosa. En un arreglo, los datos se almacenan en memoria de forma contigua, es decir, se almacenan en la siguiente ubicación de memoria disponible. En cambio, en una lista enlazada, los datos no se almacenan en ubicaciones de memoria contiguas; en su lugar, el nodo que contiene la referencia a la dirección de memoria de otro nodo se encuentra en orden secuencial. Las listas enlazadas permiten insertar y eliminar nodos en cualquier punto de la lista, y lo hacen con un número constante de operaciones al mantener en memoria el enlace anterior al que se agrega o elimina durante el recorrido de la lista.

Por otro lado, dado que las listas enlazadas simples por sí solas no permiten el acceso aleatorio a los datos ni ningún tipo de indexación eficiente, muchas operaciones básicas, como obtener el último nodo de la lista, encontrar un nodo que contenga un dato determinado o localizar el lugar donde se debe insertar un nuevo nodo, pueden requerir iterar a través de la mayoría o de todos los elementos de la lista.

Historia

La lista enlazada de información precede a la era digital en más de dos milenios, habiéndose originado a más tardar en la época homérica, cuando los escribas que copiaban rollos de papiro frecuentemente proporcionaban a los lectores y copistas posteriores una guía sobre el orden de lectura previsto escribiendo al final de un rollo determinado la primera palabra, posteriormente conocida por el participio presente latino reclamans (plural reclamantes ; literalmente "gritando de vuelta" o su equivalente nominalizado ), del rollo siguiente en ese orden. [ 1 ] Esta práctica resurgió durante los primeros años de la imprenta mecánica de libros en Europa, cuando era común que los impresores incluyeran al final de cada página impresa una " palabra clave " que correspondía a la primera palabra de la página siguiente en el orden. Esta práctica mejoró la capacidad de los impresores para verificar que estaban imprimiendo cada página verso (en lenguas europeas, página izquierda del lector) en el reverso de la página recto precedente y que, durante la cotejación y encuadernación, estaban organizando cada página recto de manera que siguiera a la página verso precedente. [ 2 ]

La primera implementación de listado enlazado en el contexto de la informática fue desarrollada en 1955-1956 por Allen Newell , Cliff Shaw y Herbert A. Simon en RAND Corporation y Carnegie Mellon University como la estructura de datos principal para su lenguaje de procesamiento de información (IPL). Los autores utilizaron IPL para desarrollar varios programas de inteligencia artificial tempranos , incluyendo la Logic Theory Machine, el General Problem Solver y un programa de ajedrez por computadora. Los informes sobre su trabajo aparecieron en IRE Transactions on Information Theory en 1956, y varias actas de conferencias de 1957 a 1959, incluyendo Proceedings of the Western Joint Computer Conference en 1957 y 1958, e Information Processing (Actas de la primera Conferencia Internacional de la UNESCO sobre Procesamiento de la Información) en 1959. El diagrama ahora clásico que consiste en bloques que representan nodos de lista con flechas que apuntan a nodos de lista sucesivos aparece en "Programming the Logic Theory Machine" de Newell y Shaw en Proc. WJCC, febrero de 1957. Newell y Simon fueron galardonados con el Premio Turing de la ACM en 1975 por haber realizado contribuciones fundamentales a la inteligencia artificial, la psicología de la cognición humana y el procesamiento de listas. El problema de la traducción automática para el procesamiento del lenguaje natural llevó a Victor Yngve, del Instituto Tecnológico de Massachusetts (MIT), a utilizar listas enlazadas como estructuras de datos en su lenguaje de programación COMIT para la investigación informática en el campo de la lingüística . Un informe sobre este lenguaje, titulado «Un lenguaje de programación para la traducción mecánica», apareció en Mechanical Translation en 1958.

Otra aparición temprana de las listas enlazadas fue la de Hans Peter Luhn, quien escribió un memorando interno de IBM en enero de 1953 que sugería el uso de listas enlazadas en tablas hash encadenadas. [ 3 ]

LISP , acrónimo de List Processor (procesador de listas), fue creado por John McCarthy en 1958 mientras estudiaba en el MIT. En 1960 publicó su diseño en un artículo de la revista Communications of the ACM , titulado "Funciones recursivas de expresiones simbólicas y su computación por máquina, Parte I". Una de las principales estructuras de datos de LISP es la lista enlazada.

A principios de la década de 1960, la utilidad tanto de las listas enlazadas como de los lenguajes que las utilizan como representación principal de datos estaba bien establecida. Bert Green, del Laboratorio Lincoln del MIT, publicó un artículo de revisión titulado "Lenguajes informáticos para la manipulación de símbolos" en IRE Transactions on Human Factors in Electronics en marzo de 1961, donde resumía las ventajas del enfoque de listas enlazadas. Un artículo de revisión posterior, "Una comparación de lenguajes informáticos para el procesamiento de listas", de Bobrow y Raphael, apareció en Communications of the ACM en abril de 1964.

Varios sistemas operativos desarrollados por Technical Systems Consultants (originalmente en West Lafayette, Indiana, y posteriormente en Chapel Hill, Carolina del Norte) utilizaban listas enlazadas simples como estructuras de archivos. Una entrada de directorio apuntaba al primer sector de un archivo, y las partes subsiguientes se localizaban recorriendo punteros. Entre los sistemas que empleaban esta técnica se encontraban Flex (para la CPU Motorola 6800 ), mini-Flex (con la misma CPU) y Flex9 (para la CPU Motorola 6809 ). Una variante desarrollada por TSC para Smoke Signal Broadcasting en California, y comercializada por esta misma empresa, utilizaba listas enlazadas dobles de la misma manera.

El sistema operativo TSS/360 , desarrollado por IBM para las máquinas System 360/370, utilizaba una lista doblemente enlazada para su catálogo de archivos . La estructura de directorios era similar a la de Unix, donde un directorio podía contener archivos y otros directorios, y extenderse a cualquier profundidad.

Conceptos básicos y nomenclatura

Cada registro de una lista enlazada se suele denominar "elemento" o " nodo ".

El campo de cada nodo que contiene la dirección del siguiente nodo se suele denominar "siguiente enlace" o "siguiente puntero". Los campos restantes se conocen como campos de "datos", "información", "valor", "carga" o "carga útil".

El «cabeza» de una lista es su primer nodo. El «cola» de una lista puede referirse al resto de la lista después del cabeza o al último nodo de la lista. En Lisp y algunos lenguajes derivados, el siguiente nodo puede llamarse « cdr » (pronunciado /kʊd.əɹ/ ) de la lista, mientras que la carga útil del nodo cabeza puede llamarse «car».

Lista enlazada simple

Las listas enlazadas simples contienen nodos que poseen un campo 'valor' y un campo 'siguiente', que apunta al siguiente nodo en la secuencia. Las operaciones que se pueden realizar en las listas enlazadas simples incluyen la inserción, la eliminación y el recorrido.

Una lista enlazada simple cuyos nodos contienen dos campos: un valor entero (datos) y un enlace al siguiente nodo.

El siguiente código en lenguaje C demuestra cómo agregar un nuevo nodo con el "valor" al final de una lista enlazada simple:

#include <stdlib.h>// Cada nodo en una lista enlazada es una estructura. El nodo cabeza es el primer nodo de la lista.typedef struct Nodo {valor entero ;struct Nodo * siguiente ;} Nodo ;Nodo * addNodeToTail ( Nodo * cabeza , int valor ) {// Declara un puntero al nodo y lo inicializa para que apunte al nuevo nodo (es decir, tendrá la dirección de memoria del nuevo nodo) que se agregará al final de la lista.Nodo * temp = ( Nodo * ) malloc ( sizeof * temp ); /// 'malloc' en stdlib.temp -> valor = valor ; // Agrega datos al campo valor del nuevo nodo.temp -> next = NULL ; // inicializar los enlaces no válidos a nil.si ( ! cabeza ) {cabeza = temp ; // Si la lista enlazada está vacía (es decir, el puntero del nodo cabeza es un puntero nulo), entonces el puntero del nodo cabeza apunta al nuevo nodo.} demás {Nodo * p = cabeza ; // Asigna el puntero del nodo cabeza al puntero del nodo 'p'.mientras ( p -> siguiente ) {p = p -> next ; // Recorre la lista hasta que p sea el último nodo. El último nodo siempre apunta a NULL.}p -> next = temp ; // Hace que el último nodo anterior apunte al nuevo nodo.}return head ; // Devuelve el puntero al nodo cabeza.}

Lista doblemente enlazada

En una lista doblemente enlazada, cada nodo contiene, además del enlace al nodo siguiente, un segundo campo de enlace que apunta al nodo anterior en la secuencia. Estos dos enlaces pueden denominarse «adelante» y «atrás», o «siguiente» y «anterior».

Una lista doblemente enlazada cuyos nodos contienen tres campos: un valor entero, el enlace hacia adelante al siguiente nodo y el enlace hacia atrás al nodo anterior.

Una técnica conocida como enlace XOR permite implementar una lista doblemente enlazada utilizando un único campo de enlace en cada nodo. Sin embargo, esta técnica requiere la capacidad de realizar operaciones de bits en direcciones, por lo que podría no estar disponible en algunos lenguajes de alto nivel.

Muchos sistemas operativos modernos utilizan listas doblemente enlazadas para mantener referencias a procesos activos, subprocesos y otros objetos dinámicos. [ 4 ] Una estrategia común para que los rootkits evadan la detección es desvincularse de estas listas. [ 5 ]

Lista enlazada múltiple

En una lista enlazada múltiple, cada nodo contiene dos o más campos de enlace, donde cada campo conecta el mismo conjunto de datos organizados en un orden diferente (por ejemplo, por nombre, departamento, fecha de nacimiento, etc.). Si bien una lista doblemente enlazada puede considerarse un caso especial de una lista enlazada múltiple, el hecho de que los dos o más órdenes sean opuestos entre sí da lugar a algoritmos más sencillos y eficientes, por lo que generalmente se tratan como un caso aparte.

Lista enlazada circular

En el último nodo de una lista enlazada, el campo de enlace suele contener una referencia nula , un valor especial que indica la ausencia de más nodos. Una convención menos común es que apunte al primer nodo de la lista; en ese caso, se dice que la lista es circular o está enlazada circularmente; de ​​lo contrario, se dice que es abierta o lineal. Se trata de una lista en la que el puntero del último nodo apunta al primero (es decir, el puntero "enlace al siguiente nodo" del último nodo tiene la dirección de memoria del primer nodo).

Una lista enlazada circular

En el caso de una lista doblemente enlazada circular, el primer nodo también apunta al último nodo de la lista.

nodos centinela

En algunas implementaciones, se puede agregar un nodo adicional, denominado "centinela" o "ficticio", antes del primer registro de datos o después del último. Esta convención simplifica y acelera algunos algoritmos de manejo de listas, al garantizar que todos los enlaces se puedan desreferenciar de forma segura y que cada lista (incluso una que no contenga elementos de datos) siempre tenga un nodo "inicial" y un nodo "final".

listas vacías

Una lista vacía es aquella que no contiene ningún registro de datos. Esto suele ser equivalente a decir que tiene cero nodos. Si se utilizan nodos centinela, se suele decir que la lista está vacía cuando solo contiene dichos nodos.

Enlace hash

Los campos de enlace no tienen por qué formar parte físicamente de los nodos. Si los registros de datos se almacenan en una matriz y se referencian mediante sus índices, el campo de enlace puede almacenarse en una matriz independiente con los mismos índices que los registros de datos.

Identificadores de lista

Dado que una referencia al primer nodo da acceso a toda la lista, a esa referencia se la suele llamar «dirección», «puntero» o «identificador» de la lista. Los algoritmos que manipulan listas enlazadas suelen obtener estos identificadores de las listas de entrada y devolver los identificadores de las listas resultantes. De hecho, en el contexto de estos algoritmos, la palabra «lista» suele significar «identificador de lista». Sin embargo, en algunas situaciones, puede resultar conveniente referirse a una lista mediante un identificador compuesto por dos enlaces que apuntan a su primer y último nodo.

Combinando alternativas

Las alternativas enumeradas anteriormente se pueden combinar arbitrariamente de casi cualquier forma, por lo que se pueden tener listas doblemente enlazadas circulares sin centinelas, listas simplemente enlazadas circulares con centinelas, etc.

Compensaciones

Como ocurre con la mayoría de las decisiones en programación y diseño informático, ningún método es adecuado para todas las circunstancias. Una estructura de datos de lista enlazada puede funcionar bien en un caso, pero causar problemas en otro. Esta es una lista de algunas de las ventajas y desventajas comunes de las estructuras de lista enlazada.

Listas enlazadas frente a matrices dinámicas

Un array dinámico es una estructura de datos que asigna todos los elementos de forma contigua en la memoria y mantiene un contador del número actual de elementos. Si se supera el espacio reservado para el array dinámico, este se reasigna y (posiblemente) se copia, lo cual es una operación costosa.

Las listas enlazadas presentan varias ventajas sobre los arreglos dinámicos. La inserción o eliminación de un elemento en un punto específico de una lista, suponiendo que ya existe un puntero que apunta al nodo (anterior al que se va a eliminar o al punto de inserción), es una operación de tiempo constante (de lo contrario, sin esta referencia, la complejidad es O(n)). En cambio, la inserción en un arreglo dinámico en posiciones aleatorias requiere, en promedio, mover la mitad de los elementos, y en el peor de los casos, todos. Si bien es posible "eliminar" un elemento de un arreglo en tiempo constante marcando su posición como "vacante", esto provoca una fragmentación que dificulta la iteración.

Además, se puede insertar una cantidad arbitraria de elementos en una lista enlazada, limitada únicamente por la memoria total disponible; mientras que un arreglo dinámico eventualmente llenará su estructura de datos subyacente y tendrá que reasignar memoria, una operación costosa que incluso podría no ser posible si la memoria está fragmentada, aunque el costo de la reasignación se puede promediar entre las inserciones, y el costo de una inserción debido a la reasignación aún se amortizaría O(1). Esto facilita la adición de elementos al final del arreglo, pero insertar (o eliminar) en posiciones intermedias aún conlleva costos prohibitivos debido al movimiento de datos para mantener la contigüidad. Un arreglo del que se eliminan muchos elementos también podría tener que redimensionarse para evitar desperdiciar demasiado espacio.

Por otro lado, los arreglos dinámicos (así como las estructuras de datos de arreglos de tamaño fijo ) permiten el acceso aleatorio en tiempo constante , mientras que las listas enlazadas solo permiten el acceso secuencial a los elementos. De hecho, las listas enlazadas simples solo se pueden recorrer fácilmente en una dirección. Esto las hace inadecuadas para aplicaciones donde es útil buscar un elemento rápidamente por su índice, como en el algoritmo de ordenación por montículos (heapsort ). El acceso secuencial a arreglos y arreglos dinámicos también es más rápido que a listas enlazadas en muchas máquinas, debido a que tienen una localidad de referencia óptima y, por lo tanto, hacen un buen uso del almacenamiento en caché de datos.

Otra desventaja de las listas enlazadas es el almacenamiento adicional necesario para las referencias, lo que a menudo las hace poco prácticas para listas de elementos de datos pequeños como caracteres o valores booleanos , porque la sobrecarga de almacenamiento para los enlaces puede superar en un factor de dos o más el tamaño de los datos. En cambio, un arreglo dinámico solo requiere el espacio para los datos en sí (y una cantidad muy pequeña de datos de control). [ nota 1 ] También puede ser lento, y con un asignador ingenuo, derrochador, asignar memoria por separado para cada nuevo elemento, un problema que generalmente se resuelve usando grupos de memoria .

Algunas soluciones híbridas intentan combinar las ventajas de ambas representaciones. Las listas enlazadas desplegadas almacenan varios elementos en cada nodo, lo que aumenta el rendimiento de la caché y reduce la sobrecarga de memoria para las referencias. La codificación CDR también logra esto, reemplazando las referencias con los datos reales referenciados, que se extienden más allá del final del registro de referencia.

Un buen ejemplo que ilustra las ventajas y desventajas de usar arreglos dinámicos frente a listas enlazadas es la implementación de un programa que resuelve el problema de Josefo . El problema de Josefo es un método de elección que consiste en que un grupo de personas se coloque en círculo. Comenzando con una persona predeterminada, se puede contar alrededor del círculo n veces. Una vez que se llega a la persona n , se debe eliminar del círculo y hacer que los miembros lo cierren. El proceso se repite hasta que solo queda una persona. Esa persona gana la elección. Esto muestra las fortalezas y debilidades de una lista enlazada frente a un arreglo dinámico, porque si las personas se consideran nodos conectados en una lista enlazada circular, entonces se muestra la facilidad con la que la lista enlazada puede eliminar nodos (ya que solo tiene que reorganizar los enlaces a los diferentes nodos). Sin embargo, la lista enlazada será ineficiente para encontrar a la siguiente persona a eliminar y tendrá que buscar en la lista hasta encontrarla. Por otro lado, un array dinámico tendrá dificultades para eliminar nodos (o elementos), ya que no puede eliminar un nodo sin desplazar individualmente todos los elementos de la lista una posición hacia arriba. Sin embargo, es excepcionalmente fácil encontrar a la enésima persona del círculo haciendo referencia directa a su posición en el array.

El problema de la clasificación de listas se refiere a la conversión eficiente de una lista enlazada en una matriz. Si bien es trivial para una computadora convencional, resolver este problema mediante un algoritmo paralelo es complejo y ha sido objeto de numerosas investigaciones.

Un árbol equilibrado tiene patrones de acceso a memoria y sobrecarga de espacio similares a los de una lista enlazada, pero permite una indexación mucho más eficiente, con un tiempo de O(log n) en lugar de O(n) para un acceso aleatorio. Sin embargo, las operaciones de inserción y eliminación son más costosas debido a la sobrecarga de las manipulaciones del árbol para mantener el equilibrio. Existen esquemas para que los árboles se mantengan automáticamente en un estado equilibrado: árboles AVL o árboles rojo-negro .

Listas lineales enlazadas simples frente a otras listas

Si bien las listas doblemente enlazadas y circulares tienen ventajas sobre las listas lineales simplemente enlazadas, las listas lineales ofrecen algunas ventajas que las hacen preferibles en ciertas situaciones.

Una lista lineal enlazada simple es una estructura de datos recursiva , ya que contiene un puntero a un objeto más pequeño del mismo tipo. Por ello, muchas operaciones con listas lineales enlazadas simples (como la fusión de dos listas o la enumeración de los elementos en orden inverso) suelen tener algoritmos recursivos muy sencillos, mucho más simples que cualquier solución que utilice comandos iterativos . Si bien estas soluciones recursivas pueden adaptarse a listas doblemente enlazadas y circulares, los procedimientos generalmente requieren argumentos adicionales y casos base más complejos.

Las listas enlazadas simples lineales también permiten compartir la cola , es decir, usar una porción final común de una sublista como la porción terminal de dos listas diferentes. En concreto, si se añade un nuevo nodo al principio de una lista, la lista anterior permanece disponible como la cola de la nueva, un ejemplo sencillo de estructura de datos persistente . Sin embargo, esto no ocurre con las otras variantes: un nodo nunca puede pertenecer a dos listas circulares o doblemente enlazadas diferentes.

En particular, los nodos centinela final pueden compartirse entre listas no circulares enlazadas individualmente. El mismo nodo centinela final puede usarse para cada una de dichas listas. En Lisp , por ejemplo, cada lista propia termina con un enlace a un nodo especial, denotado por nilo ().

Las ventajas de las variantes más sofisticadas suelen limitarse a la complejidad de los algoritmos, no a su eficiencia. Una lista circular, en particular, generalmente puede emularse mediante una lista lineal con dos variables que apunten al primer y último nodo, sin coste adicional.

Doblemente enlazado frente a simplemente enlazado

Las listas doblemente enlazadas requieren más espacio por nodo (a menos que se utilice el enlace XOR ), y sus operaciones elementales son más costosas; pero suelen ser más fáciles de manipular porque permiten un acceso secuencial rápido y sencillo a la lista en ambas direcciones. En una lista doblemente enlazada, se puede insertar o eliminar un nodo en un número constante de operaciones con solo conocer la dirección de ese nodo. Para hacer lo mismo en una lista simplemente enlazada, se necesita la dirección del puntero a ese nodo, que es el identificador de toda la lista (en el caso del primer nodo) o el campo de enlace en el nodo anterior . Algunos algoritmos requieren acceso en ambas direcciones. Por otro lado, las listas doblemente enlazadas no permiten compartir la cola y no se pueden usar como estructuras de datos persistentes .

Enlace circular frente a enlace lineal

Una lista enlazada circular puede ser una opción natural para representar arreglos que son inherentemente circulares, por ejemplo, los vértices de un polígono , un conjunto de búferes que se utilizan y liberan en orden FIFO ("primero en entrar, primero en salir") o un conjunto de procesos que deben compartirse en orden rotatorio . En estas aplicaciones, un puntero a cualquier nodo sirve como identificador de toda la lista.

En una lista circular, un puntero al último nodo permite acceder fácilmente también al primero, siguiendo un único enlace. Por lo tanto, en aplicaciones que requieren acceso a ambos extremos de la lista (por ejemplo, en la implementación de una cola), una estructura circular permite gestionarla con un solo puntero, en lugar de dos.

Una lista circular se puede dividir en dos listas circulares, en tiempo constante, proporcionando las direcciones del último nodo de cada segmento. La operación consiste en intercambiar el contenido de los campos de enlace de esos dos nodos. Al aplicar la misma operación a cualquier par de nodos en dos listas distintas, se unen las dos listas en una sola. Esta propiedad simplifica enormemente algunos algoritmos y estructuras de datos, como las de aristas cuádruples y de caras .

La representación más sencilla de una lista circular vacía (cuando tiene sentido) es un puntero nulo, que indica que la lista no tiene nodos. Sin esta opción, muchos algoritmos deben comprobar este caso especial y gestionarlo por separado. En cambio, usar el puntero nulo para denotar una lista lineal vacía es más natural y suele generar menos casos especiales.

Para algunas aplicaciones, puede ser útil usar listas enlazadas simples que pueden ser circulares o lineales, o incluso circulares con un segmento inicial lineal. Los algoritmos para buscar o trabajar con estas listas deben tomar precauciones para evitar entrar accidentalmente en un bucle infinito. Un método conocido consiste en que un segundo puntero recorra la lista a la mitad o al doble de velocidad; si ambos punteros coinciden en el mismo nodo, se ha encontrado un ciclo.

Uso de nodos centinela

Un nodo centinela puede simplificar ciertas operaciones con listas, asegurando que existan nodos siguientes o anteriores para cada elemento, e incluso que las listas vacías tengan al menos un nodo. También se puede usar un nodo centinela al final de la lista, con un campo de datos apropiado, para eliminar algunas comprobaciones de fin de lista. Por ejemplo, al recorrer la lista buscando un nodo con un valor x dado , establecer el campo de datos del centinela en x hace innecesario comprobar el fin de lista dentro del bucle. Otro ejemplo es la fusión de dos listas ordenadas: si sus centinelas tienen campos de datos establecidos en +∞, la elección del siguiente nodo de salida no requiere un tratamiento especial para listas vacías.

Sin embargo, los nodos centinela consumen espacio adicional (especialmente en aplicaciones que utilizan muchas listas cortas) y pueden complicar otras operaciones (como la creación de una nueva lista vacía).

Sin embargo, si la lista circular se usa simplemente para simular una lista lineal, se puede evitar parte de esta complejidad añadiendo un nodo centinela a cada lista, entre el último y el primer nodo de datos. Con esta convención, una lista vacía consta únicamente del nodo centinela, que apunta a sí mismo mediante el enlace al siguiente nodo. El identificador de la lista debería ser entonces un puntero al último nodo de datos, anterior al centinela, si la lista no está vacía; o al propio centinela, si la lista está vacía.

El mismo truco puede utilizarse para simplificar el manejo de una lista lineal doblemente enlazada, convirtiéndola en una lista circular doblemente enlazada con un único nodo centinela. Sin embargo, en este caso, el manejador debe ser un único puntero al propio nodo ficticio. [ 8 ]

Operaciones con listas enlazadas

Al manipular listas enlazadas in situ, es fundamental evitar el uso de valores invalidados en asignaciones previas. Esto hace que los algoritmos para insertar o eliminar nodos de listas enlazadas sean algo complejos. Esta sección presenta pseudocódigo para agregar o eliminar nodos de listas enlazadas simples, dobles y circulares in situ. En todo el texto, se utiliza `null` para referirse a un marcador de fin de lista o centinela , que puede implementarse de diversas maneras.

Listas enlazadas linealmente

Listas enlazadas simples

La estructura de datos del nodo tendrá dos campos. También existe una variable, firstNode , que siempre apunta al primer nodo de la lista, o es nula si la lista está vacía.

nodo de registro { datos; // Los datos que se almacenan en el nodo Nodo siguiente // Una referencia [ 4 ] al siguiente nodo, nulo para el último nodo }
Lista de registros { Nodo firstNode // apunta al primer nodo de la lista; null para una lista vacía }

Recorrer una lista enlazada simple es sencillo: se empieza por el primer nodo y se sigue cada enlace siguiente hasta llegar al final.

nodo := lista.primerNodo mientras nodo no sea nulo (hacer algo con nodo.datos) nodo := nodo.siguiente

El siguiente código inserta un nodo después de un nodo existente en una lista enlazada simple. El diagrama muestra su funcionamiento. No es posible insertar un nodo antes de uno existente directamente; en su lugar, se debe mantener un registro del nodo anterior e insertar el nodo después de este.

Diagrama de inserción de un nodo en una lista enlazada simple.
función insertAfter( Nodo nodo , Nodo nuevoNodo) // inserta nuevoNodo después de nodo nuevoNodo.siguiente := nodo.siguiente nodo.siguiente := nuevoNodo

Insertar al principio de la lista requiere una función aparte. Esto requiere actualizar firstNode .

función insertBeginning( Lista lista , Nodo nuevoNodo) // inserta un nodo antes del primer nodo actual nuevoNodo.siguiente := lista.primerNodo lista.primerNodo := nuevoNodo

De igual forma, existen funciones para eliminar el nodo que sigue a otro y para eliminar un nodo del principio de la lista. El diagrama ilustra la primera opción. Para encontrar y eliminar un nodo específico, es necesario volver a registrar el elemento anterior.

Diagrama de cómo eliminar un nodo de una lista enlazada simple.
función removeAfter( Node node) // elimina el nodo posterior a este obsoleteNode := node.next nodo.siguiente := nodo.siguiente.siguiente destruir nodo obsoleto
función removeBeginning( Lista lista ) // elimina el primer nodo obsoleteNode := lista.firstNode lista.firstNode := lista.firstNode.next // apunta más allá del nodo eliminado destroy obsoleteNode

Observe que removeBeginning()se establece list.firstNodeal nulleliminar el último nodo de la lista.

Dado que no es posible iterar hacia atrás, las operaciones insertBeforeOR eficientes removeBeforeno son posibles. Insertar un elemento en una lista antes de un nodo específico requiere recorrer la lista, lo que tendría un tiempo de ejecución en el peor de los casos de O(n).

Agregar una lista enlazada a otra puede ser ineficiente a menos que se mantenga una referencia al final como parte de la estructura de la lista, porque es necesario recorrer toda la primera lista para encontrar el final y luego agregar la segunda lista a esta. Por lo tanto, si dos listas enlazadas linealmente tienen cada una una longitudnorte{\displaystyle n}, la adición de listas tiene una complejidad temporal asintótica deO(norte){\displaystyle O(n)}En la familia de lenguajes Lisp, la adición de elementos a listas se proporciona mediante el appendprocedimiento.

Muchos de los casos especiales de las operaciones con listas enlazadas se pueden eliminar incluyendo un elemento ficticio al principio de la lista. Esto garantiza que no haya casos especiales para el inicio de la lista y hace innecesarios tanto el inicio insertBeginning()como removeBeginning()el final, es decir, cada elemento o nodo está junto a otro nodo (incluso el primer nodo está junto al nodo ficticio). En este caso, los primeros datos útiles de la lista se encontrarán en .list.firstNode.next

Lista enlazada circular

En una lista enlazada circular, todos los nodos están enlazados en un círculo continuo, sin usar el valor nulo. Para listas con un frente y un final (como una cola), se almacena una referencia al último nodo de la lista. El siguiente nodo después del último es el primero. Se pueden agregar elementos al final de la lista y eliminarlos del frente en tiempo constante.

Las listas enlazadas circularmente pueden estar enlazadas de forma simple o doble.

Ambos tipos de listas enlazadas circularmente se benefician de la capacidad de recorrer la lista completa comenzando en cualquier nodo. Esto a menudo nos permite evitar almacenar `firstNode` y `lastNode` , aunque si la lista puede estar vacía, se necesita una representación especial para la lista vacía, como una variable `lastNode` que apunte a algún nodo de la lista o sea nula si está vacía; aquí se utiliza una variable `lastNode` de este tipo . Esta representación simplifica significativamente la adición y eliminación de nodos en una lista no vacía, pero las listas vacías constituyen un caso especial.

Algoritmos

Suponiendo que someNode es algún nodo en una lista circular enlazada simple no vacía, este código itera a través de esa lista comenzando con someNode :

función iterar(someNode) si someNode ≠ null nodo := algúnNodo hacer Haz algo con node.value nodo := nodo.siguiente mientras nodo ≠ algúnNodo

Tenga en cuenta que la condición " while node ≠ someNode" debe estar al final del bucle. Si la condición se moviera al principio del bucle, el procedimiento fallaría siempre que la lista tuviera un solo nodo.

Esta función inserta un nodo "newNode" en una lista enlazada circular después de un nodo dado "node". Si "node" es nulo, se asume que la lista está vacía.

función insertAfter( Nodo nodo , Nodo nuevoNodo) si nodo = null // se asume que la lista está vacía nuevoNodo.siguiente := nuevoNodo demás nuevoNodo.siguiente := nodo.siguiente nodo.siguiente := nuevoNodo Actualizar la variable lastNode si es necesario.

Supongamos que "L" es una variable que apunta al último nodo de una lista enlazada circular (o null si la lista está vacía). Para agregar "newNode" al final de la lista, se puede hacer lo siguiente:

insertarDespués(L, nuevoNodo) L := nuevoNodo

Para insertar "newNode" al principio de la lista, se puede hacer lo siguiente:

insertAfter(L, newNode) si L = null L := newNode

Esta función inserta un valor "newVal" antes de un nodo dado "node" en tiempo O(1). Se crea un nuevo nodo entre "node" y el siguiente, luego se coloca el valor de "node" en ese nuevo nodo y se inserta "newVal" en "node". De esta manera, una lista circular enlazada simple con solo una variable firstNode puede insertar tanto al principio como al final en tiempo O(1).

función insertBefore( Node node, newVal) if node = null // se asume que la lista está vacía newNode := newNode (data:=newVal, next:=newNode) else newNode := newNode (data:=node.data, next:=node.next) nodo.datos := nuevoValor nodo.siguiente := nuevoNodo Actualizar la variable firstNode si es necesario .

Esta función elimina un nodo no nulo de una lista de tamaño mayor que 1 en tiempo O(1). Copia los datos del siguiente nodo al nodo actual y, a continuación, ajusta el puntero del nodo para que omita el siguiente nodo.

función remove( Nodo nodo ) si nodo ≠ null y tamaño de la lista > 1 removedData := node.data nodo.datos := nodo.siguiente.datos nodo.siguiente = nodo.siguiente.siguiente devolver datos eliminados

Listas enlazadas que utilizan matrices de nodos

Los lenguajes que no admiten ningún tipo de referencia aún pueden crear enlaces reemplazando los punteros por índices de matrices. El método consiste en mantener una matriz de registros , donde cada registro tiene campos enteros que indican el índice del siguiente (y posiblemente anterior) nodo en la matriz. No es necesario utilizar todos los nodos de la matriz. Si tampoco se admiten registros, a menudo se pueden usar matrices paralelas .

Como ejemplo, considere el siguiente registro de lista enlazada que utiliza matrices en lugar de punteros:

Registro Entrada { entero siguiente; // índice de la siguiente entrada en el array entero anterior; // entrada anterior (si está doblemente vinculada) cadena nombre; real saldo; }

Se puede construir una lista enlazada creando una matriz de estas estructuras y una variable entera para almacenar el índice del primer elemento.

Lista de enteros Registros de entrada de encabezado [1000]

Los vínculos entre elementos se forman colocando el índice de la matriz de la celda siguiente (o anterior) en el campo Siguiente o Anterior dentro de un elemento dado. Por ejemplo:

En el ejemplo anterior, ListHeadse establecería en 2, la posición de la primera entrada en la lista. Nótese que las entradas 3 y de la 5 a la 7 no forman parte de la lista. Estas celdas están disponibles para añadir elementos a la lista. Al crear una ListFreevariable entera, se podría crear una lista de celdas libres para controlar qué celdas están disponibles. Si todas las entradas están en uso, habría que aumentar el tamaño del array o eliminar algunos elementos antes de poder almacenar nuevas entradas en la lista.

El siguiente código recorrería la lista y mostraría los nombres y el saldo de la cuenta:

i := listHead mientras i ≥ 0 // iterar sobre la lista imprimir i, Records[i].name, Records[i].balance // imprimir entrada i := Records[i].next

Ante la disyuntiva, las ventajas de este enfoque incluyen:

  • La lista enlazada es reubicable, lo que significa que se puede mover en la memoria a voluntad, y también se puede serializar de forma rápida y directa para su almacenamiento en disco o transferencia a través de una red.
  • Especialmente para listas pequeñas, los índices de matrices pueden ocupar mucho menos espacio que un puntero completo en muchas arquitecturas.
  • La localidad de referencia se puede mejorar manteniendo los nodos juntos en la memoria y reorganizándolos periódicamente, aunque esto también se puede hacer en un almacén general.
  • Los asignadores de memoria dinámica ingenuos pueden producir una cantidad excesiva de almacenamiento adicional por cada nodo asignado; con este enfoque, prácticamente no se incurre en ningún gasto adicional de asignación por nodo.
  • Tomar una entrada de una matriz preasignada es más rápido que usar la asignación dinámica de memoria para cada nodo, ya que la asignación dinámica de memoria normalmente requiere la búsqueda de un bloque de memoria libre del tamaño deseado.

Sin embargo, este enfoque tiene una desventaja principal: crea y gestiona un espacio de memoria privado para sus nodos. Esto conlleva los siguientes problemas:

  • Esto aumenta la complejidad de la implementación.
  • Ampliar una matriz grande cuando está llena puede ser difícil o imposible, mientras que encontrar espacio para un nuevo nodo de lista enlazada en un gran conjunto de memoria general puede ser más fácil.
  • Agregar elementos a una matriz dinámica ocasionalmente (cuando está llena) tomará inesperadamente un tiempo lineal ( O (n)) en lugar de un tiempo constante (aunque sigue siendo una constante amortizada ).
  • El uso de un grupo de memoria general deja más memoria disponible para otros datos si la lista es más pequeña de lo esperado o si se liberan muchos nodos.

Por estas razones, este enfoque se utiliza principalmente en lenguajes que no admiten la asignación dinámica de memoria. Estas desventajas también se mitigan si se conoce el tamaño máximo de la lista al crear el array.

Soporte de idiomas

Muchos lenguajes de programación, como Lisp y Scheme, incorporan listas enlazadas simples. En muchos lenguajes funcionales , estas listas se construyen a partir de nodos , cada uno denominado celda cons . La celda cons tiene dos campos: ` car` , que hace referencia a los datos de ese nodo, y ` cdr` , que hace referencia al siguiente nodo. Si bien las celdas cons pueden utilizarse para construir otras estructuras de datos, este es su propósito principal.

En los lenguajes que admiten tipos de datos abstractos o plantillas, se encuentran disponibles tipos de datos abstractos o plantillas de listas enlazadas para construir listas enlazadas. En otros lenguajes, las listas enlazadas se construyen normalmente utilizando referencias junto con registros .

Almacenamiento interno y externo

Al construir una lista enlazada, surge la disyuntiva de almacenar los datos directamente en los nodos de la lista ( almacenamiento interno ) o simplemente almacenar una referencia a los datos ( almacenamiento externo ). El almacenamiento interno tiene la ventaja de hacer el acceso a los datos más eficiente, requerir menos espacio de almacenamiento en general, tener una mejor localidad de referencia y simplificar la gestión de la memoria de la lista (sus datos se asignan y liberan al mismo tiempo que los nodos de la lista).

Por otro lado, el almacenamiento externo tiene la ventaja de ser más genérico, ya que se puede usar la misma estructura de datos y código máquina para una lista enlazada, independientemente del tamaño de los datos. También facilita la colocación de los mismos datos en varias listas enlazadas. Si bien con el almacenamiento interno se pueden colocar los mismos datos en varias listas incluyendo múltiples referencias `next` en la estructura de datos del nodo, sería necesario crear rutinas separadas para agregar o eliminar celdas según cada campo. Es posible crear listas enlazadas adicionales de elementos que usan almacenamiento interno mediante el uso de almacenamiento externo, y hacer que las celdas de las listas enlazadas adicionales almacenen referencias a los nodos de la lista enlazada que contiene los datos.

En general, si se necesita incluir un conjunto de estructuras de datos en listas enlazadas, el almacenamiento externo es la mejor opción. Si se necesita incluir un conjunto de estructuras de datos en una sola lista enlazada, el almacenamiento interno es ligeramente mejor, a menos que exista un paquete genérico de listas enlazadas que utilice almacenamiento externo. Del mismo modo, si se van a incluir en una sola lista enlazada diferentes conjuntos de datos que se pueden almacenar en la misma estructura de datos, el almacenamiento interno sería suficiente.

Otro enfoque que se puede utilizar con algunos lenguajes consiste en tener diferentes estructuras de datos, pero todas comparten los campos iniciales, incluidas las referencias next (y prev si se trata de una lista doblemente enlazada), en la misma ubicación. Tras definir estructuras separadas para cada tipo de dato, se puede definir una estructura genérica que contenga la cantidad mínima de datos compartidos por todas las demás estructuras y que se encuentre al principio de las mismas. A continuación, se pueden crear rutinas genéricas que utilicen la estructura mínima para realizar operaciones de tipo lista enlazada, mientras que otras rutinas independientes pueden gestionar los datos específicos. Este enfoque se utiliza a menudo en rutinas de análisis de mensajes, donde se reciben varios tipos de mensajes, pero todos comienzan con el mismo conjunto de campos, que suele incluir un campo para el tipo de mensaje. Las rutinas genéricas se utilizan para añadir nuevos mensajes a una cola cuando se reciben y eliminarlos de la cola para procesar el mensaje. El campo de tipo de mensaje se utiliza entonces para llamar a la rutina correcta para procesar el tipo de mensaje específico.

Ejemplo de almacenamiento interno y externo

Para crear una lista enlazada de familias y sus miembros, utilizando el almacenamiento interno, la estructura podría ser la siguiente:

registro miembro { // miembro de una familia miembro siguiente; cadena nombre; entero edad; } registro familia { // la familia en sí familia siguiente; cadena apellido; cadena dirección; miembro miembros // cabeza de la lista de miembros de esta familia }

Para imprimir una lista completa de familias y sus miembros utilizando el almacenamiento interno, escriba:

aFamily := Families // comienza en el inicio de la lista de familias mientras aFamily ≠ null // recorre la lista de familias Imprimir información sobre la familia aMember := aFamily.members // obtener el primer elemento de la lista de miembros de esta familia mientras aMember ≠ null // recorrer la lista de miembros Imprimir información sobre el miembro unMiembro := unMiembro.siguiente unaFamilia := unaFamilia.siguiente

Mediante el uso de almacenamiento externo, se pueden crear las siguientes estructuras:

nodo de registro { // nodo de estructura de enlace genérico siguiente; puntero de datos // puntero genérico para datos en el nodo } miembro del registro { // estructura para el miembro de la familia cadena firstName; entero edad } registro familia { // estructura para la familia cadena apellido; cadena dirección; nodo miembros // cabeza de la lista de miembros de esta familia }

Para imprimir una lista completa de familias y sus miembros utilizando almacenamiento externo, escriba:

famNode := Familias // comienza en la cabecera de la lista de familias mientras famNode ≠ null // recorre la lista de familias aFamily := (familia) famNode.data // extrae la familia del nodo Imprimir información sobre la familia memNode := aFamily.members // obtener la lista de miembros de la familia while memNode ≠ null // recorrer la lista de miembros aMember := (member)memNode.data // extraer el miembro del nodo Imprimir información sobre el miembro memNode := memNode.next famNode := famNode.next

Tenga en cuenta que, al usar almacenamiento externo, se requiere un paso adicional para extraer el registro del nodo y convertirlo al tipo de dato adecuado. Esto se debe a que tanto la lista de familias como la lista de miembros dentro de la familia se almacenan en dos listas enlazadas que utilizan la misma estructura de datos ( nodo ), y este lenguaje no admite tipos paramétricos.

Siempre que se conozca el número de familias a las que puede pertenecer un miembro en tiempo de compilación, el almacenamiento interno funciona correctamente. Sin embargo, si un miembro necesitara pertenecer a un número arbitrario de familias, cuyo número específico solo se conoce en tiempo de ejecución, sería necesario el almacenamiento externo.

Encontrar un elemento específico en una lista enlazada, incluso si está ordenada, normalmente requiere un tiempo de O( n ) ( búsqueda lineal ). Esta es una de las principales desventajas de las listas enlazadas frente a otras estructuras de datos. Además de las variantes mencionadas anteriormente, a continuación se presentan dos maneras sencillas de mejorar el tiempo de búsqueda.

En una lista no ordenada, una heurística sencilla para reducir el tiempo medio de búsqueda es la heurística de mover al principio , que consiste simplemente en colocar un elemento al inicio de la lista una vez encontrado. Este método, útil para crear cachés sencillas, garantiza que los elementos utilizados más recientemente sean también los más fáciles de encontrar.

Otro enfoque común consiste en indexar una lista enlazada utilizando una estructura de datos externa más eficiente. Por ejemplo, se puede construir un árbol rojo-negro o una tabla hash cuyos elementos sean referencias a los nodos de la lista enlazada. Se pueden crear varios índices de este tipo sobre una misma lista. La desventaja es que estos índices pueden necesitar actualizarse cada vez que se añade o elimina un nodo (o, al menos, antes de volver a utilizar ese índice).

Listas de acceso aleatorio

Una lista de acceso aleatorio es una lista que permite un acceso aleatorio rápido para leer o modificar cualquier elemento de la lista. [ 9 ] Una posible implementación es una lista de acceso aleatorio binaria asimétrica que utiliza el sistema numérico binario asimétrico , el cual implica una lista de árboles con propiedades especiales; esto permite operaciones de cabeza/consecuencia en el peor de los casos con tiempo constante y acceso aleatorio a un elemento por índice en el peor de los casos con tiempo logarítmico. [ 9 ] Las listas de acceso aleatorio pueden implementarse como estructuras de datos persistentes . [ 9 ]

Las listas de acceso aleatorio pueden considerarse listas enlazadas inmutables, ya que también admiten las mismas operaciones de cabeza y cola de complejidad O(1). [ 9 ]

Una extensión simple de las listas de acceso aleatorio es la min-lista , que proporciona una operación adicional que produce el elemento mínimo en toda la lista en tiempo constante (sin complejidades de mutación). [ 9 ]

Tanto las pilas como las colas suelen implementarse utilizando listas enlazadas, y simplemente restringen el tipo de operaciones que admiten.

La lista de saltos es una lista enlazada con capas de punteros para saltar rápidamente por encima de un gran número de elementos y luego descender a la siguiente capa. Este proceso continúa hasta la capa inferior, que es la lista propiamente dicha.

Un árbol binario puede considerarse un tipo de lista enlazada donde los elementos son, a su vez, listas enlazadas de la misma naturaleza. Como resultado, cada nodo puede incluir una referencia al primer nodo de una o dos listas enlazadas, las cuales, junto con su contenido, forman los subárboles que se encuentran debajo de ese nodo.

Una lista enlazada desplegada es una lista enlazada en la que cada nodo contiene una matriz de valores de datos. Esto mejora el rendimiento de la caché , ya que más elementos de la lista son contiguos en la memoria, y reduce la sobrecarga de memoria, puesto que se necesita almacenar menos metadatos para cada elemento de la lista.

Una tabla hash puede utilizar listas enlazadas para almacenar las cadenas de elementos que, al generar su función hash, alcanzan la misma posición en la tabla hash.

Un montón comparte algunas de las propiedades de ordenación de una lista enlazada, pero casi siempre se implementa mediante un array. En lugar de referencias de nodo a nodo, los índices de los datos siguiente y anterior se calculan utilizando el índice del dato actual.

Una lista autoorganizada reorganiza sus nodos basándose en alguna heurística que reduce los tiempos de búsqueda para la recuperación de datos al mantener los nodos a los que se accede con frecuencia al principio de la lista.

Notas

  1. La cantidad de datos de control necesarios para una matriz dinámica suele tener la formaK+Bnorte{\displaystyle K+Bn}, dóndeK{\displaystyle K}es una constante por matriz,B{\displaystyle B}es una constante por dimensión, ynorte{\displaystyle n}es el número de dimensiones.K{\displaystyle K}yB{\displaystyle B}Suelen tener un tamaño del orden de 10 bytes.

Referencias

  1. ^ West, S. (1963), "Reclamantes en papiros griegos", Scriptorium , 17 (2): 314– 15, doi : 10.3406/scrip.1963.3188
  2. De Vinne, Theodore Low (1901). La práctica de la tipografía; composición correcta; un tratado sobre ortografía, abreviaturas, composición y división de palabras, el uso adecuado de cifras y numerales . The Century Company . págs. 142-143 vía Wikisource .   [ escanear ] Enlace a Wikisource
  3. Knuth, Donald (1998). El arte de la programación informática . Vol. 3: Ordenación y búsqueda (2.ª ed.). Addison-Wesley. pág. 547. ISBN    978-0-201-89685-5.
  4. 1 2 "The NT Insider: Fundamentos del modo kernel: Listas enlazadas de Windows" . Archivado del original el 23 de septiembre de 2015. Recuperado el 31 de julio de 2015 .
  5. Butler, Jamie; Hoglund, Greg. "VICE – ¡Atrapa a las prostitutas! (Además de nuevas técnicas de rootkit)" (PDF) . Archivado del original (PDF) el 1 de octubre de 2016. Consultado el 31 de agosto de 2021 .
  6. Brodnik, Andrej; Carlsson, Svante; Sedgewick, Robert ; Munro, JI; Demaine, ED (1999), Resizable Arrays in Optimal Time and Space (Informe técnico CS-99-09) (PDF) , Departamento de Ciencias de la Computación, Universidad de Waterloo
  7. 1 2 3 Chris Okasaki (1995). "Listas de acceso aleatorio puramente funcionales". Actas de la Séptima Conferencia Internacional sobre Lenguajes de Programación Funcionales y Arquitectura de Computadoras : 86–95 . doi : 10.1145/224164.224187 .
  8. Ford, William; Topp, William (2002). Estructuras de datos con C++ usando STL (Segunda edición). Prentice-Hall. págs. 466–467 . ISBN   0-13-085850-1.
  9. 1 2 3 4 5 Okasaki, Chris (1995). "Listas de acceso aleatorio puramente funcionales" . Actas de la séptima conferencia internacional sobre lenguajes de programación funcional y arquitectura de computadoras - FPCA '95 . ACM Press. págs. 86–95 . doi : 10.1145/224164.224187 . ISBN  0-89791-719-7. Consultado el 7 de mayo de 2015 .{{cite book}}: |work=ignorado ( ayuda )

Lecturas adicionales

  • Juan, Ángel (2006). "Capítulo 20 – Estructuras de datos; ID06 - PROGRAMACIÓN con JAVA (parte de diapositivas del libro 'Big Java', de CayS. Horstmann)" (PDF) . pág.  3. Archivado del original (PDF) el 6 de enero de 2012. Recuperado el 10 de julio de 2011 .
  • Black, Paul E. (16 de agosto de 2004). Pieterse, Vreda; Black, Paul E. (eds.). "Lista enlazada" . Diccionario de algoritmos y estructuras de datos . Instituto Nacional de Estándares y Tecnología . Recuperado el 14 de diciembre de 2004 .
  • Antonakos, James L.; Mansfield, Kenneth C. Jr. (1999). Estructuras de datos prácticas con C/C++ . Prentice-Hall. págs. 165–190 . ISBN  0-13-280843-9.
  • Collins, William J. (2005) [2002]. Estructuras de datos y el marco de colecciones de Java . Nueva York: McGraw Hill. págs. 239–303 . ISBN  0-07-282379-8.
  • Cormen, Thomas H .; Leiserson, Charles E .; Rivest, Ronald L .; Stein, Clifford (2003). Introducción a los algoritmos . Prensa del MIT. págs. 205– 213, 501– 505. ISBN  0-262-03293-7.
  • Cormen, Thomas H.; Leiserson , Charles E.; Rivest , Ronald L .; Stein, Clifford (2001). «10.2: Listas enlazadas». Introducción a los algoritmos (2.ª  ed.). MIT Press. págs. 204–209 . ISBN  0-262-03293-7.
  • Green, Bert F. Jr. (1961). "Lenguajes informáticos para la manipulación de símbolos". IRE Transactions on Human Factors in Electronics . 2 (2): 3– 8. Bibcode : 1961IRTHF...2....3G . doi : 10.1109/THFE2.1961.4503292 .
  • McCarthy, John (1960). "Funciones recursivas de expresiones simbólicas y su computación por máquina, parte I" . Communications of the ACM . 3 (4): 184. doi : 10.1145/367177.367199 . S2CID 1489409 . 
  • Knuth, Donald (1997). "2.2.3-2.2.5". Algoritmos fundamentales (3.ª  ed.). Addison-Wesley. págs. 254–298 . ISBN  0-201-89683-4.
  • Newell, Allen ; Shaw, FC (1957). "Programación de la máquina de teoría lógica". Actas de la Conferencia Conjunta Occidental de Computación : 230–240 .
  • Parlante, Nick (2001). "Conceptos básicos de listas enlazadas" (PDF) . Universidad de Stanford . Recuperado el 21 de septiembre de 2009 .
  • Sedgewick, Robert (1998). Algoritmos en C. Addison Wesley. págs. 90–109 . ISBN  0-201-31452-5.
  • Shaffer, Clifford A. (1998). Introducción práctica a las estructuras de datos y al análisis de algoritmos . Nueva Jersey: Prentice Hall. págs. 77–102 . ISBN  0-13-660911-2.
  • Shanmugasundaram, Kulesh (04/04/2005). "Lista enlazada del kernel de Linux explicada" . Archivado del original el 25/09/2009 . Recuperado el 21/09/2009 .
  • West, S. (1963), "Reclamantes in Greek Papyri", Scriptorium , 17 (2): 314– 15, doi : 10.3406/scrip.1963.3188
  • Wilkes, Maurice Vincent (1964). "Un experimento con un compilador autocompilador para un lenguaje simple de procesamiento de listas". Annual Review in Automatic Programming . 4 (1). Pergamon Press: 1. doi : 10.1016/0066-4138(64)90013-8 .
  • Wilkes, Maurice Vincent (1964). "Las listas y por qué son útiles". Actas de la Conferencia Nacional de la ACM, Filadelfia 1964 (P–64). ACM: F1–1.
  • Descripción del Diccionario de Algoritmos y Estructuras de Datos
  • Introducción a las listas enlazadas , Biblioteca de Ciencias de la Computación de la Universidad de Stanford
  • Problemas de listas enlazadas , Biblioteca de Ciencias de la Computación de la Universidad de Stanford
  • Estructuras de datos abiertas - Capítulo 3 - Listas enlazadas , Pat Morin
  • Patente para la idea de tener nodos que se encuentran simultáneamente en varias listas enlazadas (tenga en cuenta que esta técnica se utilizó ampliamente durante muchas décadas antes de que se concediera la patente).
  • Implementación de una lista enlazada simple en C
  • Implementación de una lista enlazada simple en C++
  • Implementación de una lista doblemente enlazada en C
  • Implementación de una lista doblemente enlazada en C++