1
Estructura de datos
En programación, una estructura de datos es una forma de organizar un conjunto de datos elementales con el objetivo de facilitar su manipulación. Un dato elemental es la mínima información que se tiene en un sistema.
Una estructura de datos define la organización e interrelación de éstos y un conjunto de operaciones que se pueden realizar sobre ellos. Las operaciones básicas son:
Alta, adicionar un nuevo valor a la estructura. Baja, borrar un valor de la estructura.
Búsqueda, encontrar un determinado valor en la estructura para realizar una operación con este valor, en forma SECUENCIAL o BINARIO (siempre y cuando los datos estén ordenados)... Otras operaciones que se pueden realizar son:
Ordenamiento, de los elementos pertenecientes a la estructura.
Apareo, dadas dos estructuras originar una nueva ordenada y que contenga a las apareadas.
Cada estructura ofrece ventajas y desventajas en relación a la simplicidad y eficiencia para la realización de cada operación. De esta forma, la elección de la estructura de datos apropiada para cada problema depende de factores como la frecuencia y el orden en que se realiza cada operación sobre los datos.
Vector (informática)
Arreglo unidimensional con 10 elementos
En programación, un array es un conjunto o agrupación de variables del mismo tipo cuyo acceso se realiza por índices.
Los vectores o arreglos (array en inglés) de dos o más dimensiones se denominan con frecuencia matrices, y pueden tener tantas dimensiones como se desee; aunque para evitar confusiones con el concepto matemático de matriz numérica (que normalmente sólo tiene dos dimensiones), se suele utilizar el termino array (o arreglo) para referirse de forma genérica a matrices de cualquier número de dimensiones.
Introducción
Desde el punto de vista de un programa de ordenador, un array (matriz o vector) es una zona de almacenamiento contiguo, que contiene una serie de elementos del mismo tipo, los elementos de la matriz. Desde el punto de vista lógico un array se puede ver como un conjunto de elementos ordenados en
2 fila (o filas y columnas si tuviera dos dimensiones). En principio, se puede considerar que todos los arrays son de una dimensión, la dimensión principal, pero los elementos de dicha fila pueden ser a su vez arrays (un proceso que puede ser recursivo), lo que nos permite hablar de la existencia de arrays multidimensionales, aunque los más fáciles de "mondaa" o imaginar son los de una, dos y tres dimensiones.
Estas estructuras de datos son adecuadas para situaciones en las que el acceso a los datos se realice de forma aleatoria e impredecible. Por el contrario, si los elementos pueden estar ordenados y se va a utilizar acceso secuencial sería más adecuado utilizar una lista, ya que esta estructura puede cambiar de tamaño fácilmente durante la ejecución de un programa.
Índices
Todo vector se compone de un determinado número de elementos. Cada elemento es referenciado por la posición que ocupa dentro del vector. Dichas posiciones son llamadas índice y siempre son correlativos. Existen tres formas de indexar los elementos de un array:
Indexación base-cero (0): En este modo el primer elemento del vector será la componente cero ('0') del mismo, es decir, tendrá el indice '0'. En consecuencia, si el vector tiene 'n' componentes la última tendrá como índice el valor 'n-1'. El C es un ejemplo típico de lenguaje que utiliza este modo de indexación. Indexación base-uno (1): En esta forma de indexación, el primer elemento del array tiene el indice '1' y el último tiene el índice 'n' (para un array de 'n' componentes).
Indexación base-n (n): Este es un modo versátil de indexación en la que el índice del primer elemento puede ser elegido libremente, en algunos lenguajes de programación se permite que los índices puedan ser negativos e incluso de cualquier tipo escalar (también cadenas de caracteres).
Notación
La representación de un elemento en un vector se suele hacer mediante el identificador del vector seguido del índice entre corchetes, paréntesis o llaves:
Notación Ejemplos
vector[índice_1,índice_2...,índice_N] (Java, Léxico, etc.) vector[índice_1][índice_2]...[índice_N](C, C++, PHP, etc.) vector(índice_1,índice_2...,índice_N) (Basic)
vector{índice_1,índice_2...,índice_N} (Perl)
Aunque muchas veces en pseudocódigo y en libros de matemática se representan como letras acompañadas de un subíndice numérico que indica la posición a la que se quiere acceder. Por ejemplo, para un vector "A":
3
Forma de Acceso
La forma de acceder a los elementos del array es directa; esto significa que el elemento deseado es obtenido a partir de su índice y no hay que ir buscándolo elemento por elemento (en contraposición, en el caso de una lista, para llegar, por ejemplo, al tercer elemento hay que acceder a los dos anteriores o almacenar un apuntador o puntero que permita acceder de manera rápida a ese elemento.
Para trabajar con vectores muchas veces es preciso recorrerlos. Esto se realiza por medio de bucles. El siguiente pseudocódigo muestra un algoritmo típico para recorrer un vector y aplicar una función 'f(...)' a cada una de las componentes del vector:
i = 0
mientras (i < longitud)
#Se realiza alguna operación con el vector en la i-ésima posición f(v[i])
i=i+1 fin_mientras
Vectores dinámicos
Lo habitual es que un vector tenga una cantidad fija de memoria asignada, aunque dependiendo del tipo de vector y del lenguaje de programación un vector podría tener una cantidad variable de datos. En este caso, se los denomina vectores dinámicos, en oposición, a los vectores con una cantidad fija de memoria asignada se los denomina vectores estáticos.
El uso de vectores dinámicos requiere realizar una apropiada gestión de memoria dinámica. Un uso incorrecto de los vectores dinámicos, o mejor dicho, una mala gestión de la memoria dinámica, puede conducir a una fuga de memoria (Error de software que ocurre cuando un bloque de memoria reservada no es liberado en un programa de computación. Comúnmente ocurre porque se pierden todas las referencias a esa área de memoria antes de haberse liberado. Dependiendo de la cantidad de memoria perdida y el tiempo que el programa siga en ejecución, este problema puede llevar al agotamiento de la memoria disponible en la computadora. Este problema se da principalmente en aquellos lenguajes de programación en los que el manejo de memoria es manual (C o C++ principalmente), y por lo tanto es el programador el que debe saber en qué momento exacto puede liberar la memoria. Otros lenguajes utilizan un recolector de basura que automáticamente efectúa esta liberación. Sin embargo todavía es posible la existencia de fugas en estos lenguajes si el programa acumula referencias a objetos, impidiendo así que el recolector llegue a considerarlos en desuso. Existen varias formas de luchar contra este problema. Una forma es el uso de un recolector de basura incluso en el caso en el que éste no sea parte estándar del lenguaje. El más conocido recolector de basura usado de esta manera es el Boehm-Demers-Weiser conservative garbage collector. Otras técnicas utilizadas son la adopción de esquemas de conteo de referencias o el uso de pools de memoria (técnica menos popular, utilizada en el servidor Apache y en el sistema de versiones Subversion). También hay herramientas para "auscultar" un programa y detectar las fugas. Una de las herramientas más conocidas es Valgrind).
Al utilizar vectores dinámicos siempre habrá que liberar la memoria utilizada cuando ésta ya no se vaya a seguir utilizando.
4 Lenguajes más modernos y de más alto nivel, cuentan con un mecanismo denominado recolector de basura (como es el caso de Java) que permiten que el programa decida si debe liberar el espacio basándose en si se va a utilizar en el futuro o no un determinado objeto.
Ejemplos en C
Declaración en C (o C++) de un vector estático.- La forma de crear vectores estáticos es igual que en C y C++.
int v[5]; int i;
for (i=0 ; i<5 ; i++) {
v[i] = 2*i; }
Declaración en C++ de un vector dinámico:
#include <vector>
vector<int> v; // Si no se especifica el tamaño inicial es 0
for (int i=0 ; i<5 ; i++) {
v.push_back(2*i); // inserta un elemento al final del vector }
El ejemplo anterior está hecho para el lenguaje C++. En C, para crear vectores dinámicos se tendrían que utilizar las instrucciones malloc y realloc para reservar memoria de forma dinámica (ver librería stdlib.h), y la función por free para liberar la memoria utilizada.
Resultado:
0 1 2 3 4 0 2 4 6 8
El resultado de los dos ejemplos es el mismo vector
Vectores multidimensionales
En Basic, Java y otros lenguajes es posible declarar matrices multidimensionales, entendiéndolas como un vector de vectores. En dichos casos en número de elementos del vector es el producto resultante de cada dimensión.
Por ejemplo el vector v(4,1) tiene 10 elementos se calcula del siguiente modo: (0-4) * (0-1). Los elementos de la primera dimensión del vector contiene 5 elementos que van del '0' al '4' y la 2º dimensión tiene 2 elementos que van desde '0' a '1'. Los elementos serían accedidos del siguiente modo:
elemento 1: (0,0) elemento 2: (0,1) elemento 3: (1,0) ...
5 elemento 8: (3,1)
elemento 9: (4,0) elemento 10: (4,1)
Registro (estructura de datos)
Un registro, en programación, es un tipo de dato estructurado formado por la unión de varios elementos bajo una misma estructura. Estos elementos pueden ser, o bien datos elementales (entero, real, carácter,...), o bien otras estructuras de datos. A cada uno de esos elementos se le llama campo.
Un registro se diferencia de un vector en que éste es una colección de datos iguales, es decir, todos del mismo tipo, mientras que en una estructura los elementos que la componen, aunque podrían serlo, no tiene porque ser del mismo tipo.
Ejemplo: Creación de un registro (o estructura) en C
Un ejemplo de como se declararía un registro en C podría ser: typedef struct TipoNodo{
int dato;
struct TipoNodo *sig; struct TipoNodo *ant; } TNodo;
En este ejemplo se define el tipo de dato TNodo (o struct TipoNodo, sería equivalente) como una estructura (registro) que contiene un dato de tipo entero y dos punteros sig y ant (siguiente y anterior) que sirven para referenciar a otros registros del tipo TNodo. Ésta es la estructura de datos que se suele utilizar como nodo en las listas doblemente enlazadas.
Registro en bases de datos
El concepto de registro que se acaba de presentar es muy similar al concepto de registro en bases de datos, este segundo se refiere a una colección de datos que hacen referencia a un mismo ítem que se van a guardar en una fila de una tabla de la base de datos...
Tipo de datos algebraico
En matemáticas discretas es usual introducir definiciones de estructuras recursivas dando los casos de definición y un axioma de clausura indicando que ninguna otra cosa forma parte de lo definido.
Por ejemplo, los árboles con información en los nodos pueden definirse como sigue:
Sea T un conjunto. Los árboles con información en los nodos son todos los valores que se pueden construir con las reglas siguientes.
6 2. Si t1 y t2 son árboles, y x es un elemento de T, entonces Nodo(t1,x,t2) es un árbol.
3. Los árboles son únicamente los valores que se construyen utilizando las reglas 1 y 2.
La construcción correspondiente en los lenguajes de programación se llama Tipo de datos
algebraico. Sus reglas de tipo polimórficas fueron introducidas por Robin Milner junto con la definición
del lenguaje Standard ML y han sido adoptadas desde entonces en diversos lenguajes de programación, sobre todo en los lenguajes de programación funcionales. Por ejemplo, la definición del tipo árbol binario con información en los nodos de tipo T se escribe en Ocaml como sigue:
type 'T Arbol = AVacio | Nodo of ('T Arbol * 'T * 'T Arbol) y en sintaxis de Haskell:
data Arbol T = AVacio | Nodo (Arbol T) T (Arbol T)
Los constructores del tipo Árbol son AVacio y Nodo los cuales, al recibir los argumentos necesarios producen un valor del tipo árbol. Por ejemplo, en Ocaml, AVacio es un árbol al igual que Nodo
(AVacio,5,AVacio).
Las operaciones sobre los tipos recursivos se generalmente se escriben utilizando la construcción de llamada por patrones. Por ejemplo, en Haskell, el número de niveles de un árbol de define como:
niveles :: Arbol T -> Int niveles AVacio = 0
niveles (Nodo i n d) = 1 + max (niveles i) (niveles d) en Standard ML la misma función se escribe
fun niveles AVacio = 0
| niveles Nodo(i,n,d) = 1 + max (niveles i) (niveles d)
Corrección de programas
A cada tipo de datos algebraico corresponde el orden bien fundamentado de subtérminos y un esquema de inducción estructural sobre la base de la definición del tipo. En el caso de los árboles éstos son los siguientes:
Para demostrar la terminación de la función niveles aplicando este esquema de inducción estructural, se tiene que demostrar, utilizando las reglas semánticas del lenguaje, que la expresión (niveles
AVacio) termina y que si (niveles i) y (niveles d) terminan entonces (niveles (Nodo (i, n, d)) termina
también.
La llamada por patrones es una operación compleja que puede definirse con ayuda de dos primitivas, El operador is permite identificar el caso particular de una definición y la definición estructurada de variables permite obtener los componentes de un caso ya identificado:
7 En el ejemplo de árboles, el predicado e is AVacio es cierto cuando el árbol e es efectivamente un árbol vacío y e is Nodo es cierto cuando e es un nodo. Una definición del tipo let Nodo (u, x, v) = e ..., que sólo tiene sentido cuando e is Nodo es cierto, permite asociar a las variables u, x, v los componentes del nodo.
Lista (informática)
En Ciencias de la Computación, una lista enlazada es una de las estructuras de datos fundamentales, y puede ser usada para implementar otras estructuras de datos. Consiste en una secuencia de nodos, en los que se guardan campos de datos arbitrarios y una o dos referencias (punteros) al nodo anterior y/o posterior. El principal beneficio de las listas enlazadas respecto a los array convencionales es que el orden de los elementos enlazados puede ser diferente al orden de almacenamiento en la memoria o el disco, permitiendo que el orden de recorrido de la lista sea diferente al de almacenamiento.
Una lista enlazada es un tipo de dato auto-referenciado porque contienen un puntero o link a otro dato del mismo tipo. Las listas enlazadas permiten inserciones y eliminación de nodos en cualquier punto de la lista en tiempo constante (suponiendo que dicho punto está previamente identificado o localizado), pero no permiten un acceso aleatorio. Existen diferentes tipos de listas enlazadas: Lista Enlazadas Simples, Listas Doblemente Enlazadas, Listas Enlazadas Circulares y Listas Enlazadas Doblemente Circulares.
Las listas enlazadas pueden ser implementadas en muchos lenguajes. Lenguajes tales como Lisp y Scheme tiene estructuras de datos ya construidas, junto con operaciones para acceder a las listas enlazadas. Lenguajes imperativos u orientados a objetos tales como C o C++ y Java, respectivamente, disponen de referencias para crear listas enlazadas.
Historia
Las listas enlazadas fueron desarrolladas en 1955-56 por Santiago Fazzini, Cliff Shaw y Herbert Simon en RAND Corporation como la principal estructura de datos para su Lenguaje de Procesamiento de la Información (IPL). IPL fue usado por los autores para desarrollar varios programas relacionados con la inteligencia artificial, incluida la Máquina de la Teoría General, el Solucionador de Problemas Generales, y un programa informático de ajedrez. Se publicó en IRE Transactions on Information Theory en 1956, y en distintas conferencias entre 1957-1959, incluida Proceedings of the Western Joint Computer en 1957 y 1958, y en Information Processing (Procendents de la primera conferencia internacional del procesamiento de la información de la Unesco) en 1959. El diagrama clásico actual, que consiste en bloques que representan nodos de la lista con flechas apuntando a los sucesivos nodos de la lista, apareció en Programming the Logic Theory Machine, de Newell y Shaw. Newell y Simon fueron reconocidos por el ACM Turing Award en 1975 por ―hacer contribuciones básicas a la inteligencia artificial, a la psicología del conocimiento humano y al procesamiento de las listas‖.
El problema de los traductores del procesamiento natural del lenguaje condujo a Victor Yngve del Instituto Tecnológico de Massachusetts (MIT) a usar listas enlazadas como estructura de datos en su COMIT, lenguaje de programación para computadoras, que investigó en el campo de la Lingüística
8
computacional. Un informe de este lenguaje, titulado “A programming language for mechanical translation” apareció en Mechanical Translation en 1958.
LISP, el principal procesador de listas, fue creado en 1958. Una de las mayores estructuras de datos de LISP es la lista enlazada.
En torno a los 60, la utilidad de las listas enlazadas y los lenguajes que utilizaban estas estructuras como su principal representación de datos estaba bien establecida. Bert Green del MIT Lincoln Laboratory, publicó un estudio titulado Computer languages for symbol manipulation en IRE Transaction
on Human Factors in Electronics en marzo de 1961 que resumía las ventajas de las listas enlazadas. Un
posterior artículo, A Comparison of list-processing computer languages by Bobrow and Raphael, aparecía en Communications of the ACM en abril de 1964.
Muchos sistemas operativos desarrollados por Technical Systems Consultants (originalmente de West Lafayette Indiana y después de Raleigh, Carolina del Norte) usaron listas enlazadas simples como estructuras de ficheros. Un directorio de entrada apuntaba al primer sector de un fichero y daba como resultado porciones de la localización del fichero mediante punteros. Los sistemas que utilizaban esta técnica incluían Flex (para el Motorola 6800 CPU), mini-Flex (la misma CPU) y Flex9 (para el Motorola 6809 CPU). Una variante desarrollada por TSC se comercializó a Smoke Signal Broadcasting en California, usando listas doblemente enlazadas del mismo modo.
El sistema operativo TSS, desarrollado por IBM para las máquinas System 360/370, usaba una lista doblemente enlazada para su catálogo de ficheros de sistema. La estructura del directorio era similar a Unix, donde un directorio podía contener ficheros y/o otros directorios que se podían extender a cualquier profundidad. Una utilidad fue creada para arreglar problemas del sistema después de un fallo desde las porciones modificadas del catálogo de ficheros que estaban a veces en memoria cuando ocurría el fallo. Los problemas eran detectados por comparación de los links posterior y anterior por consistencia. Si el siguiente link era corrupto y el anterior enlace del nodo infectado era encontrado, el posterior link era asignado al nodo con el link del anterior.
Tipos de Listas Enlazadas
Listas enlazadas lineales
Listas simples enlazadas
La lista enlazada básica es la lista enlazada simple la cual tiene un enlace por nodo. Este enlace apunta al siguiente nodo en la lista, o al valor NULL o a la lista vacía, si es el último nodo.
9
Lista Doblemente Enlazada
Un tipo de lista enlazada más sofisticado es la lista doblemente enlazada o lista enlazadas de dos vías. Cada nodo tiene dos enlaces: uno apunta al nodo anterior, o apunta al valor NULL o a la lista vacía si es el primer nodo; y otro que apunta al siguiente nodo siguiente, o apunta al valor NULL o a la lista vacía si es el último nodo.
Una lista doblemente enlazada contiene tres valores: el valor, el link al nodo siguiente, y el link al anterior
En algún lenguaje de muy bajo nivel, XOR-Linking ofrece una vía para implementar listas doblemente enlazadas, usando una sola palabra para ambos enlaces, aunque el uso de esta técnica no se suele utilizar.
Listas enlazadas circulares
En una lista enlazada circular, el primer y el último nodo están unidos juntos. Esto se puede hacer tanto para listas enlazadas simples como para las doblemente enlazadas. Para recorrer un lista enlazada circular podemos empezar por cualquier nodo y seguir la lista en cualquier dirección hasta que se regrese hasta el nodo original. Desde otro punto de vista, las listas enlazadas circulares pueden ser vistas como listas sin comienzo ni fin. Este tipo de listas es el más usado para dirigir buffers para ―ingerir‖ datos, y para visitar todos los nodos de una lista a partir de uno dado.
Una lista enlazada circular que contiene tres valores enteros
Listas enlazadas circulares simples
Cada nodo tiene un enlace, similar al de las listas enlazadas simples, excepto que el siguiente nodo del último apunta al primero. Como en una lista enlazada simple, los nuevos nodos pueden ser solo eficientemente insertados después de uno que ya tengamos referenciado. Por esta razón, es usual quedarse con una referencia solamente al último elemento en una lista enlazada circular simple, esto nos permite rápidas inserciones al principio, y también permite accesos al primer nodo desde el puntero del último nodo.
Lista Enlazada Doblemente Circular
En una lista enlazada doblemente circular, cada nodo tiene dos enlaces, similares a los de la lista doblemente enlazada, excepto que el enlace anterior del primer nodo apunta al último y el enlace siguiente del último nodo, apunta al primero. Como en una lista doblemente enlazada, las inserciones y eliminaciones pueden ser hechas desde cualquier punto con acceso a algún nodo cercano. Aunque estructuralmente una lista circular doblemente enlazada no tiene ni principio ni fin, un puntero de acceso
10 externo puede establecer el nodo apuntado que está en la cabeza o al nodo cola, y así mantener el orden tan bien como en una lista doblemente enlazada.
Nodos Centinelas
A veces las listas enlazadas tienen un nodo centinela (también llamado falso nodo o nodo ficticio) al principio y/o al final de la lista, el cual no es usado para guardar datos. Su propósito es simplificar o agilizar algunas operaciones, asegurando que cualquier nodo tiene otro anterior o posterior, y que toda la lista (incluso alguna que no contenga datos) siempre tenga un ―primer y último‖ nodo.
Aplicaciones de las listas enlazadas
Las listas enlazadas son usadas como módulos para otras muchas estructuras de datos, tales como pilas, colas y sus variaciones.
El campo de datos de un nodo puede ser otra lista enlazada. Mediante este mecanismo, podemos construir muchas estructuras de datos enlazadas con listas; esta práctica tiene su origen en el lenguaje de programación Lisp, donde las listas enlazadas son una estructura de datos primaria (aunque no la única), y ahora es una característica común en el estilo de programación funcional.
A veces, las listas enlazadas son usadas para implementar arrays asociativos, y estas en el contexto de las llamadas listas asociativas. Hay pocas ventajas en este uso de las listas enlazadas; hay mejores formas de implementar éstas estructuras, por ejemplo con árboles binarios de búsqueda equilibrados. Sin embargo, a veces una lista enlazada es dinámicamente creada fuera de un subconjunto propio de nodos semejante a un árbol, y son usadas más eficientemente para recorrer ésta serie de datos
Ventajas
Como muchas opciones en programación y desarrollo, no existe un único método correcto para resolver un problema. Una estructura de lista enlazada puede trabajar bien en un caso pero causar problemas en otros. He aquí una lista con algunas de las ventajas más comunes que implican las estructuras de tipo lista. En general, teniendo una colección dinámica donde los elementos están siendo añadidos y eliminados frecuentemente e importa la localización de los nuevos elementos introducidos se incrementa el beneficio de las listas enlazadas.
Listas Enlazadas vs. Arrays
Array Lista Enlazada
Indexado O(1) O(n)
Inserción / Eliminación al final O(1) O(1) or O(n)2 Inserción / Eliminación en la mitad O(n) O(1)
Persistencia No Simples sí
11 Las listas enlazadas poseen muchas ventajas sobre los arrays. Los elementos se pueden insertar en una lista indefinidamente mientras que un array tarde o temprano se llenará ó necesitará ser redimensionado, una costosa operación que incluso puede no ser posible si la memoria se encuentra fragmentada.
En algunos casos se pueden lograr ahorros de memoria almacenando la misma ‗cola‘ de elementos entre dos o más listas – es decir, la lista acaban en la misma secuencia de elementos. De este modo, uno puede añadir nuevos elementos al frente de la lista manteniendo una referencia tanto al nuevo como a los viejos elementos - un ejemplo simple de una estructura de datos persistente.
Por otra parte, los arrays permiten acceso aleatorio mientras que las listas enlazadas sólo permiten acceso secuencial a los elementos. Las listas enlazadas simples, de hecho, solo pueden ser recorridas en una dirección. Esto hace que las listas sean inadecuadas para aquellos casos en los que es útil buscar un elemento por su índice rápidamente, como el heapsort. El acceso secuencial en los arrays también es más rápido que en las listas enlazadas.
Otra desventaja de las listas enlazadas es el almacenamiento extra necesario para las referencias, que a menudos las hacen poco prácticas para listas de pequeños datos como caracteres o valores booleanos.
También puede resultar lento y abusivo el asignar memoria para cada nuevo elemento. Existe una variedad de listas enlazadas que contemplan los problemas anteriores para resolver los mismos. Un buen ejemplo que muestra los pros y contras del uso de arrays sobre listas enlazadas es la implementación de un programa que resuelva el problema de Josephus. Este problema consiste en un grupo de personas dispuestas en forma de círculo. Se empieza a partir de una persona predeterminada y se cuenta n veces, la persona n-ésima se saca del círculo y se vuelve a cerrar el grupo. Este proceso se repite hasta que queda una sola persona, que es la que gana. Este ejemplo muestra las fuerzas y debilidades de las listas enlazadas frente a los arrays, ya que viendo a la gente como nodos conectados entre sí en una lista circular se observa como es más fácil suprimir estos nodos. Sin embargo, se ve como la lista perderá utilidad cuando haya que encontrar a la siguiente persona a borrar. Por otro lado, en un array el suprimir los nodos será costoso ya que no se puede quitar un elemento sin reorganizar el resto. Pero en la búsqueda de la n-ésima persona tan sólo basta con indicar el índice n para acceder a él resultando mucho más eficiente.
Doblemente Enlazadas vs. Simples Enlazadas
Las listas doblemente enlazadas requieren más espacio por nodo y sus operaciones básicas resultan más costosas pero ofrecen una mayor facilidad para manipular ya que permiten el acceso secuencial a lista en ambas direcciones. En particular, uno puede insertar o borrar un nodo en un número fijo de operaciones dando únicamente la dirección de dicho nodo (Las listas simples requieren la dirección del nodo anterior para insertar o suprimir correctamente). Algunos algoritmos requieren el acceso en ambas direcciones.
12
Circulares Enlazadas vs. Lineales Enlazadas
Las listas circulares son más útiles para describir estructuras circulares y tienen la ventaja de poder recorrer la lista desde cualquier punto. También permiten el acceso rápido al primer y último elemento por medio de un puntero simple.
Nodos Centinelas (header nodes)
La búsqueda común y los algoritmos de ordenación son menos complicados si se usan los llamados Nodos Centinelas o Nodos Ficticios, donde cada elemento apunta a otro elemento y nunca a nulo. El Nodo Centinela o Puntero Cabeza contienen, como otro, un puntero siguiente que apunta al que se considera como primer elemento de la lista. También contiene un puntero previo que hace lo mismo con el último elemento. El Nodo Centinela es definido como otro nodo en una lista doblemente enlazada, la asignación del puntero frente no es necesaria y los punteros anterior y siguiente estarán apuntando a sí mismo en ese momento. Si los punteros anterior y siguiente apuntan al Nodo Centinela la lista se considera vacía. En otro caso, si a la lista se le añaden elementos ambos punteros apuntarán a otros nodos. Estos Nodos Centinelas simplifican muchos las operaciones pero hay que asegurarse de que los punteros anterior y siguiente existen en cada momento. Como ventaja eliminan la necesidad de guardar la referencia al puntero del principio de la lista y la posibilidad de asignaciones accidentales. Por el contrario, usan demasiado almacenamiento extra y resultan complicados en algunas operaciones.
Operaciones sobre listas enlazadas
Cuando se manipulan listas enlazadas, hay que tener cuidado con no usar valores que hayamos invalidado en asignaciones anteriores. Esto hace que los algoritmos de insertar y borrar nodos en las listas sean algo especiales. A continuación se expone el pseudocódigo para añadir y borrar nodos en listas enlazadas simples, dobles y circulares.
Listas Enlazadas Lineales
Listas Simples Enlazadas
Nuestra estructura de datos tendrá dos campos. Vamos a mantener la variable PrimerNodos que siempre apunta al primer nodo de la lista, ó nulo para la lista vacía.
record Node {
data // El dato almacenado en el nodo
next // Una referencia al nodo siguiente, nulo para el último nodo }
record List {
Node PrimerNodo // Apunta al primer nodo de la lista; nulo para la lista vacía }
El recorrido en una lista enlazada es simple, empezamos por el primer nodo y pasamos al siguiente hasta que la lista llegue al final.
node := list.PrimerNodo while node not null {
13
node := node.next }
El siguiente código inserta un elemento a continuación de otro en una lista simple. El diagrama muestra como funciona.
function insertAfter(Node node, Node newNode) { newNode.next := node.next
node.next := newNode }
Insertar al principio de una lista requiere una función por separado. Se necesita actualizar PrimerNodo.
function insertBeginning(List list, Node newNode) { newNode.next := list.firstNode
list.firstNode := newNode }
De forma similar, también tenemos funciones para borrar un nodo dado ó para borrar un nodo del principio de la lista. Ver diagrama.
function removeAfter(Node node) { obsoleteNode := node.next node.next := node.next.next destroy obsoleteNode
}
function removeBeginning(List list) { obsoleteNode := list.firstNode
list.firstNode := list.firstNode.next destroy obsoleteNode
}
Advertimos que BorrarPrincipio pone PrimerNodo a nulo cuando se borra el último elemento de la lista. Adjuntar una lista enlazada a otra puede resultar ineficiente a menos que se guarde una referencia a
14 la cola de la lista, porque si no tendríamos que recorrer la lista en orden hasta llegar a la cola y luego añadir la segunda lista.
Listas Doblemente Enlazadas
Con estas listas es necesario actualizar muchos más punteros pero también se necesita menos información porque podemos usar un puntero para recorrer hacia atrás y consultar elementos. Se crean nuevas operaciones y elimina algunos casos especiales. Añadimos el campo anterior a nuestros nodos, apuntando al elemento anterior, y UltimoNodo a nuestra estructura, el cual siempre apunta al último elemento de la lista. PrimerNodo y UltimoNodo siempre están a nulo en la lista vacía.
record Node {
data // El dato almacenado en el nodo
next // Una referencia al nodo siguiente, nulo para el último nodo prev // Una referencia al nodo anterior, nulo para el primer nodo }
record List {
Node firstNode // apunta al primer nodo de la lista; nulo para la lista vacía Node lastNode // apunta al último nodo de la lista; nulo para la lista vacía }
Formas de recorrer la lista:
Hacia Delante
node := list.firstNode while node ≠ null
<do something with node.data> node := node.next
Hacia Atrás
node := list.lastNode while node ≠ null
<do something with node.data> node := node.prev
Estas funciones simétricas añaden un nodo después o antes de uno dado, como el diagrama muestra:
function insertAfter(List list, Node node, Node newNode) newNode.prev := node newNode.next := node.next if node.next = null node.next := newNode list.lastNode := newNode else node.next.prev := newNode node.next := newNode
function insertBefore(List list, Node node, Node newNode) newNode.prev := node.prev
newNode.next := node if node.prev is null node.prev := newNode
15
list.firstNode := newNode else
node.prev.next := newNode node.prev := newNode
También necesitamos una función para insertar un nodo al comienzo de una lista posiblemente vacía.
function insertBeginning(List list, Node newNode) if list.firstNode = null list.firstNode := newNode list.lastNode := newNode newNode.prev := null newNode.next := null else
insertBefore (list, list.firstNode, newNode) Una función simétrica que inserta al final:
function insertEnd(List list, Node newNode) if list.lastNode = null
insertBeginning (list, newNode) else
insertAfter (list, list.lastNode, newNode)
Borrar un nodo es fácil, solo requiere usar con cuidado firstNode y lastNode. function remove(List list, Node node)
if node.prev = null list.firstNode := node.next else node.prev.next := node.next if node.next = null list.lastNode := node.prev else node.next.prev := node.prev destroy node
Una consecuencia especial de este procedimiento es que borrando el último elemento de una lista se ponen PrimerNodo y UltimoNodo a nulo, habiendo entonces un problema en una lista que tenga un único elemento.
Listas Enlazadas Circulares
Estas pueden ser simples o doblemente enlazadas. En una lista circular todos los nodos están enlazados como un círculo, sin usar nulo. Para listas con frente y final (como una cola), se guarda una referencia al último nodo de la lista. El siguiente nodo después del último sería el primero de la lista. Los elementos se pueden añadir por el final y borrarse por el principio en todo momento. Ambos tipos de listas circulares tienen la ventaja de poderse recorrer completamente empezando desde cualquier nodo. Esto nos permite normalmente evitar el uso de PrimerNodo y UltimoNodo, aunque si la lista estuviera vacía necesitaríamos un caso especial, como una variable UltimoNodo que apunte a algún nodo en la lista
16 o nulo si está vacía. Las operaciones para estas listas simplifican el insertar y borrar nodos en una lista vacía pero introducen casos especiales en la lista vacía.
Listas Enlazadas Doblemente Circulares
Asumiendo que someNodo es algún nodo en una lista no vacía, esta lista presenta el comienzo de una lista con someNode.
Hacia Delante
node := someNode do
do something with node.value node := node.next
while node != someNode
Hacia Atrás
node := someNode do
do something with node.value node := node.prev
while node != someNode
Esta función inserta un nodo en una lista enlazada doblemente circular después de un elemento dado: This simple function inserts a node into a doubly-linked circularly-linked list after a given element: function insertAfter(Node node, Node newNode)
newNode.next := node.next newNode.prev := node node.next.prev := newNode node.next := newNode
Para hacer "insertBefore", podemos simplificar "insertAfter (node.prev, newNode)". Insertar un elemento en una lista que puede estar vacía requiere una función especial.
function insertEnd(List list, Node node) if list.lastNode = null
node.prev := node node.next := node else
insertAfter (list.lastNode, node) list.lastNode := node
Para insertar al principio simplificamos "insertAfter (list.lastNode, node)". function remove(List list, Node node)
if node.next = node list.lastNode := null else node.next.prev := node.prev node.prev.next := node.next if node = list.lastNode
17
list.lastNode := node.prev; destroy node
Como una lista doblemente enlazada, "removeAfter" y "removeBefore" puede ser implementada con "remove (list, node.prev)" y "remove (list, node.next)".
Listas enlazadas usando Arrays de Nodos
Los lenguajes que no aceptan cualquier tipo de referencia pueden crear uniones reemplazando los punteros por índices de un array. La ventaja es de mantener un array de entradas, donde cada entrada tiene campos enteros indicando el índice del siguiente elemento del array. Puede haber nodos sin usarse. Si no hay suficiente espacio, pueden usarse arrays paralelos.
Aquí un ejemplo: record Entry {
integer next; // índice de la nueva entrada en el array integer prev; // entrada previa
string name; real balance; }
Creado un array con esta estructura, y una variable entere para almacenar el índice del primer elemento, una lista enlazada puede ser construida:
integer listHead; Entry Records[1000];
Las utilidades de esta propuesta son:
La lista enlazada puede ser movida sobre la memoria y también ser rápidamente serializada para almacenarla en un disco o transferirla sobre una red.
Especialmente para una lista pequeña, los arrays indexados pueden ocupar mucho menos espacio que un conjunto de punteros.
La localidad de referencia puede ser mejorada guardando los nodos juntos en memoria y siendo reordenados periódicamente.
Algunas desventajas son:
Incrementa la complejidad de la implementación.
Usar un fondo general de memoria deja más memoria para otros datos si la lista es más pequeña de lo esperado ó si muchos nodos son liberados.
El crecimiento de un array cuando está lleno no puede darse lugar (o habría que redimensionarlo) mientras que encontrar espacio para un nuevo nodo en una lista resulta posible y más fácil.
Por estas razones, la propuesta se usa principalmente para lenguajes que no soportan asignación de memoria dinámica. Estas desventajas se atenúan también si el tamaño máximo de la lista se conoce en el momento en el que el array se crea.
18
Lenguajes soportados
Muchos lenguajes de programación tales como Lisp y Scheme tienen listas enlazadas simples ya construidas. En muchos lenguajes de programación, estas listas están construidas por nodos, cada uno llamado cons o celda cons. Las celdas cons tienen dos campos: el car, una referencia del dato al nodo, y el cdr, una referencia al siguiente nodo. Aunque las celdas cons pueden ser usadas para construir otras estructuras de datos, este es su principal objetivo.
En lenguajes que soportan tipos abstractos de datos o plantillas, las listas enlazadas ADTs o plantillas están disponibles para construir listas enlazadas. En otros lenguajes, las listas enlazadas son típicamente construidas usando referencias junto con el tipo de dato record. Aquí tenemos un ejemplo completo en C:
#include <stdio.h> /* for printf */ #include <stdlib.h> /* for malloc */ typedef struct ns { int data; struct ns *next; } node;
node *list_add(node **p, int i) {
/* algunos compiladores no requieren un casting del valor del retorno para malloc */
node *n = (node *)malloc(sizeof(node)); if (n == NULL) return NULL; n->next = *p; *p = n; n->data = i; return n; }
void list_remove(node **p) { /* borrar cabeza*/ if (*p != NULL) { node *n = *p; *p = (*p)->next; free(n); } }
node **list_search(node **n, int i) { while (*n != NULL) { if ((*n)->data == i) { return n; } n = &(*n)->next; } return NULL; } void list_print(node *n) { if (n == NULL) {
19
}
while (n != NULL) {
printf("print %p %p %d\n", n, n->next, n->data); n = n->next; } } int main(void) { node *n = NULL; list_add(&n, 0); /* lista: 0 */ list_add(&n, 1); /* lista: 1 0 */ list_add(&n, 2); /* lista: 2 1 0 */ list_add(&n, 3); /* lista: 3 2 1 0 */ list_add(&n, 4); /* lista: 4 3 2 1 0 */ list_print(n);
list_remove(&n); /* borrar primero(4) */
list_remove(&n->next); /* borrar nuevo segundo (2) */
list_remove(list_search(&n, 1)); /* eliminar la celda que contiene el 1 (primera) */
list_remove(&n->next); /* eliminar segundo nodo del final(0)*/ list_remove(&n); /* eliminar ultimo nodo (3) */
list_print(n);
return 0; }
Y ahora una posible especificación de Listas Enlazadas en Maude fmod LISTA-GENERICA {X :: TRIV} is
protecting NAT . *** tipos
sorts ListaGenNV{X} ListaGen{X} .
subsort ListaGenNV{X} < ListaGen{X} . *** generadores
op crear : -> ListaGen{X} [ctor] .
op cons : X$Elt ListaGen{X} -> ListaGenNV{X} [ctor] . *** constructores
op _::_ : ListaGen{X} ListaGen{X} -> ListaGen{X} [assoc id: crear ] . *** concatenacion
op invertir : ListaGen{X} -> ListaGen{X} .
op resto : ListaGenNV{X} -> ListaGen{X} . *** selectores
20
op primero : ListaGenNV{X} -> X$Elt .
op esVacia? : ListaGen{X} -> Bool .
op longitud : ListaGen{X} -> Nat . *** variables vars L L1 L2 : ListaGen{X} . vars E E1 E2 : X$Elt . *** ecuaciones eq esVacia?(crear) = true . eq esVacia?(cons(E, L)) = false . eq primero(cons(E, L)) = E . eq resto(cons(E, L)) = L . eq longitud(crear) = 0 . eq longitud(cons(E, L)) = 1 + longitud(L) .
eq cons(E1, L1) :: cons(E2, L2) = cons(E1, L1 :: cons(E2, L2)) .
eq invertir(crear) = crear .
eq invertir(cons(E, L)) = invertir(L) :: cons(E, crear) .
endfm
Almacenamiento interno y externo
Cuando se construye una lista enlazada, nos enfrentamos a la elección de si almacenar los datos de la lista directamente en los nodos enlazados de la lista, llamado almacenamiento interno, o simplemente almacenar una referencia al dato, llamado almacenamiento externo. El almacenamiento interno tiene la ventaja de hacer accesos a los datos más eficientes, requiriendo menos almacenamiento global, teniendo mejor referencia de localidad, y simplifica la gestión de memoria para la lista (los datos son alojados y desalojados al mismo tiempo que los nodos de la lista).
El almacenamiento externo, por otro lado, tiene la ventaja de ser más genérico, en la misma estructura de datos y código máquina puede ser usado para una lista enlazada, no importa cual sea su tamaño o los datos. Esto hace que sea más fácil colocar el mismo dato en múltiples listas enlazadas. Aunque con el almacenamiento interno los mismos datos pueden ser colocados en múltiples listas incluyendo múltiples referencias siguientes en la estructura de datos del nodo, esto podría ser entonces necesario para crear rutinas separadas para añadir o borrar celdas basadas en cada campo. Esto es posible creando listas enlazadas de elementos adicionales que usen almacenamiento interno usando almacenamiento externo, y teniendo las celdas de las listas enlazadas adicionales almacenadas las referencias a los nodos de las listas enlazadas que contienen los datos.
21 En general, si una serie de estructuras de datos necesita ser incluida en múltiples listas enlazadas, el almacenamiento externo es el mejor enfoque. Si una serie de estructuras de datos necesitan ser incluidas en una sola lista enlazada, entonces el almacenamiento interno es ligeramente mejor, a no ser que un paquete genérico de listas genéricas que use almacenamiento externo esté disponible. Asimismo, si diferentes series de datos que pueden ser almacenados en la misma estructura de datos son incluidos en una lista enlazada simple, entonces el almacenamiento interno puede ser mejor.
Otro enfoque que puede ser usado con algunos lenguajes implica tener diferentes estructuras de datos, pero todas tienen los campos iniciales, incluyendo la siguiente (y anterior si es una lista doblemente enlazada) referencia en la misma localización. Después de definir estructuras distintas para cada tipo de dato, una estructura genérica puede ser definida para que contenga la mínima cantidad de datos compartidos por todas las estructuras y contenidos al principio de las estructuras. Entonces las rutinas genéricas pueden ser creadas usando las mínimas estructuras para llevar a cabo las operaciones de los tipos de las listas enlazadas, pero separando las rutinas que pueden manejar los datos específicos. Este enfoque es usado a menudo en rutinas de análisis de mensajes, donde varios tipos de mensajes son recibidos, pero todos empiezan con la misma serie de campos, generalmente incluyendo un campo para el tipo de mensaje. Las rutinas genéricas son usadas para añadir nuevos mensajes a una cola cuando son recibidos, y eliminarlos de la cola en orden para procesarlos. El campo de tipo de mensaje es usado para llamar a la rutina correcta para procesar el tipo específico de mensaje.
Ejemplos de almacenamiento interno y externo
Suponiendo que queremos crear una lista enlazada de familias y sus miembros. Usando almacenamiento interno, la estructura podría ser como la siguiente:
record member { // miembro de una familia member next
string firstName integer age }
record family { // // la propia familia family next
string lastName string address
member members // de la lista de miembros de la familia }
Para mostrar una lista completa de familias y sus miembros usando almacenamiento interno podríamos escribir algo como esto:
aFamily := Families // comienzo de la lista de familias
while aFamily ≠ null { // bucle a través de la lista de familias print information about family
aMember := aFamily.members // coger cabeza de esta lista de miembros de esta familia
while aMember ≠ null { //bucle para recorrer la lista de miembros print information about member
aMember := aMember.next }
aFamily := aFamily.next }
22 Usando almacenamiento externo, nosotros podríamos crear las siguientes estructuras:
record node { // estructura genérica de enlace node next
pointer data // puntero genérico del dato al nodo }
record member { // estructura de una familia string firstName
integer age }
record family { // estructura de una familia string lastName
string address
node members // cabeza de la lista de miembros de esta familia }
Para mostrar una lista completa de familias y sus miembros usando almacenamiento externo, podríamos escribir:
famNode := Families // comienzo de la cabeza de una lista de familias while famNode ≠ null { // bucle de lista de familias
aFamily = (family) famNode.data // extraer familia del nodo print information about family
memNode := aFamily.members // coger lista de miembros de familia while memNode ≠ null { bucle de lista de miembros
aMember := (member) memNode.data // extraer miembro del nodo print information about member
memNode := memNode.next }
famNode := famNode.next }
Hay que fijarse en que cuando usamos almacenamiento externo, se necesita dar un paso extra para extraer la información del nodo y hacer un casting dentro del propio tipo del dato. Esto es porque ambas listas, de familias y miembros, son almacenadas en dos listas enlazadas usando la misma estructura de datos (nodo), y este lenguaje no tiene tipos paramétricos.
Si conocemos el número de familias a las que un miembro puede pertenecer en tiempo de compilación, el almacenamiento interno trabaja mejor. Si, sin embargo, un miembro necesita ser incluido en un número arbitrario de familias, sabiendo el número específico de familias solo en tiempo de ejecución, el almacenamiento externo será necesario.
Agilización de la búsqueda
Buscando un elemento específico en una lista enlazada, incluso si esta es ordenada, normalmente requieren tiempo O (n) (búsqueda lineal). Esta es una de las principales desventajas de listas enlazadas respecto a otras estructuras. Además algunas de las variantes expuestas en la sección anterior, hay numerosas vías simples para mejorar el tiempo de búsqueda.
En una lista desordenada, una forma simple para decrementar el tiempo de búsqueda medio es el mover al frente de forma heurística, que simplemente mueve un elemento al principio de la lista una vez
23 que es encontrado. Esta idea, útil para crear cachés simples, asegura que el ítem usado más recientemente es también el más rápido en ser encontrado otra vez.
Otro enfoque común es indizar una lista enlazada usando una estructura de datos externa más eficiente. Por ejemplo, podemos construir un árbol rojo-negro o una tabla hash cuyos elementos están referenciados por los nodos de las listas enlazadas. Pueden ser construidos múltiples índices en una lista simple. La desventaja es que estos índices puede necesitar ser actualizados cada vez que uno nodo es añadido o eliminado (o al menos, antes que el índice sea utilizado otra vez).
Estructuras de datos relacionadas
Tanto las pilas como las colas son a menudo implementadas usando listas enlazadas, y simplemente restringiendo el tipo de operaciones que son soportadas.
La skip list, o lista por saltos, es una lista enlazada aumentada con capas de punteros para saltos rápidos sobre grandes números de elementos, y descendiendo hacía la siguiente capa. Este proceso continúa hasta llegar a la capa inferior, la cual es la lista actual.
Un árbol binario puede ser visto como un tipo de lista enlazada donde los elementos están enlazados entre ellos mismos de la misma forma. El resultado es que cada nodo puede incluir una referencia al primer nodo de una o dos listas enlazadas, cada cual con su contenido, formando así los subárboles bajo el nodo.
Una lista enlazada desenrollada es una lista enlazada cuyos nodos contiene un array de datos. Esto mejora la ejecución de la caché, siempre que las listas de elementos estén contiguas en memoria, y reducen la sobrecarga de la memoria, porque necesitas menos metadatos para guardar cada elemento de la lista.
Una tabla hash puede usar listas enlazadas para guardar cadenas de ítems en la misma posición de la tabla hash.
Referencias
1. ↑ Preiss, Bruno R. (1999), Data Structures and Algorithms with Object-Oriented Design Patterns in Java, Wiley, p. page 97, 165, ISBN 0471-34613-6, http://www.brpreiss.com/books/opus5/html/page97.html 2. ↑ If maintaining a link to the tail of the list, time is O(1); if the entire list must be searched to locate the
tail link, O(n)
National Institute of Standards and Technology (August 16, 2004). Definition of a linked list. Retrieved December 14, 2004.
Antonakos, James L. and Mansfield, Kenneth C., Jr. Practical Data Structures Using C/C++ (1999). Prentice-Hall. ISBN 0-13-280843-9, pp. 165–190
Collins, William J. Data Structures and the Java Collections Framework (2002,2005) New York, NY: McGraw Hill. ISBN 0-07-282379-8, pp. 239–303
Cormen, Thomas H.; Leiserson, Charles E.; Rivest, Ronald L.; Stein, Clifford Introductions to Algorithms (2003). MIT Press. ISBN 0-262-03293-7, pp. 205–213, 501–505
Green, Bert F. Jr. (1961). Computer Languages for Symbol Manipulation. IRE Transactions on Human Factors in Electronics. 2 pp. 3-8.
24 McCarthy, John (1960). Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I. Communications of the ACM. [1] HTML DVI PDF PostScript
Donald Knuth. Fundamental Algorithms, Third Edition. Addison-Wesley, 1997. ISBN 0-201-89683-4. Sections 2.2.3–2.2.5, pp.254–298.
Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, and Clifford Stein. Introduction to Algorithms, Second Edition. MIT Press and McGraw-Hill, 2001. ISBN 0-262-03293-7. Section 10.2: Linked lists, pp.204– 209.
Newell, Allen and Shaw, F. C. (1957). Programming the Logic Theory Machine. Proceedings of the Western Joint Computer Conference. pp. 230-240.
Parlante, Nick (2001). Linked list basics. Stanford University. PDF
Sedgewick, Robert Algorithms in C (1998). Addison Wesley. ISBN 0-201-31452-5, pp. 90–109
Shaffer, Clifford A. A Practical Introduction to Data Structures and Algorithm Analysis (1998). NJ: Prentice Hall. ISBN 0-13-660911-2, pp. 77–102
Wilkes, Maurice Vincent (1964). An Experiment with a Self-compiling Compiler for a Simple List-Processing Language. Annual Review in Automatic Programming 4, 1. Published by Pergamon Press.
Wilkes, Maurice Vincent (1964). Lists and Why They are Useful. Proceeds of the ACM National Conference, Philadelphia 1964 (ACM Publication P-64 page F1-1); Also Computer Journal 7, 278 (1965).
Kulesh Shanmugasundaram (April 4, 2005). Linux Kernel Linked List Explained.
Skip list
Una skip list o lista por saltos es una Estructura de datos, basada en Listas enlazadas paralelas con eficiencia comparable a la de un árbol binario (tiempo en orden O(log n) para la mayoría de las operaciones).
Una lista por saltos se construye por capas. La capa del fondo es una sencilla lista enlazada. Cada capa subsiguiente es como una "vía rápida" para la lista de la capa anterior. Un elemento de la capa i aparece en la capa i+1 con una probabilidad fija p. En promedio, cada elemento aparece en 1/(1-p) listas, el elemento más alto (generalmente un elemento inicial colocado al principio de la lista por saltos) aparece en O(log(1/p) n) listas.
Para buscar un elemento, se inicia con el elemento inicial de la lista de la capa más alta hasta alcanzar el máximo elemento que es menor o igual al buscado, se pasa a la capa anterior y se continua la búsqueda. Se puede verificar que el número esperado de pasos en cada lista enlazada es 1/p. De manera que el costo total de búsqueda es O(log(1/p) n / p), que es lo mismo que O(log n) cuando p es una constante. Dependiendo del valor escogido para p, se puede favorecer el costo de búsqueda contra el costo de almacenamiento.
25 Las operaciones de inserción y borrado se implantan como las de sus correspondientes listas enlazadas, salvo que los elementos de las capas superiores deben ser insertados o borrados de más de una lista enlazada.
A diferencia de los árboles de búsqueda balanceados, el peor caso para las operaciones de listas por saltos no está garantizado como logarítmico, dado que es posible aunque poco probable, que se produzca una estructura no balanceada. Sin embargo, las listas por saltos trabajan bien en la práctica y el esquema de balanceo es más sencillo de implementar que el de los árboles binarios balanceados. Las listas por saltos son útiles también para cómputo paralelo, dado que se pueden realizar inserciones en paralelo sobre segmentos diferentes sin tener luego que balancear la estructura.
Origen
Las listas por saltos fueron creadas por William Pugh y publicadas en su artículo Skip lists: a
probabilistic alternative to balanced trees in Communications of the ACM, June 1990, 33(6) 668-676.
Véase también en [1].
El creador de la estructura de datos las describe así:
Las listas por saltos son una estructura probabilística que podría remplazar los árboles balanceados como método de implementación preferido en muchas aplicaciones. Las operaciones de listas por saltos tienen el mismo comportamiento asintótico esperado que las de los árboles balanceados, son más rápidas y utilizan menos espacio.
Pila (informática)
Una pila (stack en inglés) es una lista ordinal o estructura de datos en la que el modo de acceso a sus elementos es de tipo LIFO (del inglés Last In First Out, último en entrar, primero en salir) que permite almacenar y recuperar datos. Se aplica en multitud de ocasiones en informática debido a su
simplicidad y ordenación implícita en la propia estructura. Para el manejo de los datos se cuenta con dos operaciones básicas: apilar (push), que coloca un objeto en la pila, y su operación inversa, retirar (o desapilar,
pop), que retira el
último elemento apilado.
En cada momento sólo se tiene acceso a la parte superior de la pila, es decir, al último objeto apilado (denominado TOS, Top of Stack en inglés). La operación retirar permite la obtención de este
26 elemento, que es retirado de la pila permitiendo el acceso al siguiente (apilado con anterioridad), que pasa a ser el nuevo TOS.
Por analogía con objetos cotidianos, una operación apilar equivaldría a colocar un plato sobre una pila de platos, y una operación retirar a retirarlo.
Las pilas suelen emplearse en los siguientes contextos:
Evaluación de expresiones en notación postfija (notación polaca inversa). Reconocedores sintácticos de lenguajes independientes del contexto Implementación de recursividad.
Pila de llamadas
La pila de llamadas es un segmento de memoria que utiliza esta estructura de datos para almacenar información sobre las llamadas a subrutinas actualmente en ejecución en un programa en proceso.
Cada vez que una nueva subrutina es llamada, se apila una nueva entrada con información sobre ésta tal como sus variables locales. En especial, se almacena aquí el punto de retorno al que regresar cuando esta subrutina termine (para volver a la subrutina anterior y continuar su ejecución después de esta llamada).
Pila como tipo abstracto de datos
A modo de resumen tipo de datos, la pila es un contenedor de nodos y tiene dos operaciones básicas: push (o apilar) y pop (o desapilar). 'Push' añade un nodo a la parte superior de la pila, dejando por debajo el resto de los nodos. 'Pop' elimina y devuelve el actual nodo superior de la pila. Una metáfora que se utiliza con frecuencia es la idea de una pila de platos en una cafetería con muelle de pila. En esa serie, sólo la primera placa es visible y accesible para el usuario, todas las demás placas permanecen ocultas. Como se añaden las nuevas placas, cada nueva placa se convierte en la parte superior de la pila, escondidos debajo de cada plato, empujando a la pila de placas. A medida que la placa superior se elimina de la pila, la segunda placa se convierte en la parte superior de la pila. Dos principios importantes son ilustrados por esta metáfora: En primer lugar la última salida es un principio, la segunda es que el contenido de la pila está oculto. Sólo la placa de la parte superior es visible, por lo que para ver lo que hay en la tercera placa, el primer y segundo platos tendrán que ser retirados.
Operaciones
Una pila cuenta con 2 operaciones imprescindibles: apilar y desapilar, a las que en las implementaciones modernas de las pilas se suelen añadir más de uso habitual.
Crear: se crea la pila vacía.
Apilar: se añade un elemento a la pila.(push)
Desapilar: se elimina el elemento frontal de la pila.(pop)
Cima: devuelve el elemento que esta en la cima de la pila. (top o peek) Vacía: devuelve cierto si la pila está vacía o falso en caso contrario.
27
Implementación
Un requisito típico de almacenamiento de una pila de n elementos es O (n). El requisito típico de tiempo de O (1) las operaciones también son fáciles de satisfacer con un array o con listas enlazadas simples.
La biblioteca de plantillas de C++ estándar proporciona una "pila" clase templated que se limita a sólo apilar/desapilar operaciones. Java contiene una biblioteca de la clase Pila que es una especialización de Vector. Esto podría ser considerado como un defecto, porque el diseño heredado get () de Vector método LIFO ignora la limitación de la Pila.
Estos son ejemplos sencillos de una pila con las operaciones descritas anteriormente (pero no hay comprobación de errores):
En Python
class Stack(object): def __init__(self): self.stack_pointer = Nonedef push(self, element):
self.stack_pointer = Node(element, self.stack_pointer) def pop(self): e = self.stack_pointer.element self.stack_pointer = self.stack_pointer.next return e def peek(self): return self.stack_pointer.element def __len__(self): i = 0 sp = self.stack_pointer while sp: i += 1 sp = sp.next return i class Node(object):
def __init__(self, element=None, next=None): self.element = element
self.next = next
if __name__ == '__main__':
# small use example s = Stack()
[s.push(i) for i in xrange(10)]
28
En Maude
La PilaNV es la pila no vacía, que diferenciamos de la pila normal a la hora de tomar en cuenta errores. El elemento X representa el tipo de valor que puede contener la pila: entero, carácter, registro.... fmod PILA-GENERICA {X :: TRIV} is
sorts Pila{X} PilaNV{X}. subsorts PilaNV{X} < Pila{X}.
***generadores:
op crear: -> Pila {X} [ctor].
op apilar : X$Elt Pila{X} -> PilaNV{X} [ctor].
***constructores
op desapilar : Pila{X} -> Pila{X}.
***selectores
op cima : PilaNV{X} -> X$Elt. ***variables var P : Pila{X}. var E : X$Elt. ***ecuaciones
eq desapilar (crear) = crear. eq desapilar (apilar(E, P)) = P. eq cima (apilar(E, P)) = E. endfm
En C++
#ifndef PILA
#define PILA // define la pila template <class T> class Pila { private: struct Nodo { T elemento;
Nodo* siguiente; // coloca el nodo en la segunda posicion }* ultimo;
unsigned int elementos; public: Pila() { elementos = 0; } ~Pila() {
while (elementos != 0) pop(); }
29
Nodo* aux = new Nodo; aux->elemento = elem; aux->siguiente = ultimo; ultimo = aux; ++elementos; } void pop() {
Nodo* aux = ultimo;
ultimo = ultimo->siguiente; delete aux; --elementos; } T cima() const { return ultimo->elemento; }
bool vacia() const {
return elementos == 0; }
unsigned int altura() const { return elementos; } }; #endif
En Pascal
UNIT Pila; INTERFACE Uses Elemento; Type TPila=^TNodo; TNodo=RECORD info:TElemento; ant:TPila; END;PROCEDURE CrearPilaVacia (VAR p:Tpila);
PROCEDURE InsertarEnPila (e:TElemento; VAR p:TPila); PROCEDURE Cima(p:TPila; VAR c:TElemento);
FUNCTION EsPilaVacia(p.Tpila):boolean; PROCEDURE Desapilar (VAR p: TPila);
IMPLEMENTATION
PROCEDURE CrearPilaVacia (VAR p:Tpila);
BEGIN
Destruir(p); p:=NIL; END
30
PROCEDURE InsertarEnPila (e:TElemento; VAR p:TPila);
VAR paux:TPila; BEGIN new(paux); paux^.info:=e; paux^.ant:=p; p:=paux; END;
PROCEDURE Cima(p:TPila; VAR c:TElemento);
BEGIN
e:=p^.info; END;
FUNCTION EsPilaVacia(p: TPila): Boolean;
BEGIN
EsPilaVacia := (p=NIL); END;
PROCEDURE Desapilar (VAR p: TPila);
VAR
auxPNodo: TPila; BEGIN
IF NOT EsPilaVacia(p) THEN BEGIN auxPNodo:=p; p:=p^.ant; dispose(auxPNodo); END; END;
PROCEDURE Destruir (VAR p:TPila);
BEGIN
WHILE NOT EsPilaVacia(p) DO Desapilar(pila);
END; END.
Estructuras de datos relacionadas
El tipo base de la estructura FIFO (el primero en entrar es el primero en salir) es la cola, y la combinación de las operaciones de la pila y la cola es proporcionado por el deque. Por ejemplo, el cambio de una pila en una cola en un algoritmo de búsqueda puede cambiar el algoritmo de búsqueda en primera profundidad (en inglés, DFS) por una búsqueda en amplitud (en inglés, BFS). Una pila acotada es una pila limitada a un tamaño máximo impuesto en su especificación.