TAD y estructuras lineales
La diferencia entre el qué y el cómo, y el funcionamiento por dentro de la pila, la cola y la lista, con el precio que se paga por elegir array o lista enlazada.
Tipo abstracto de datos frente a estructura de datos
Un tipo abstracto de datos (TAD) es un modelo matemático: dice qué operaciones existen y qué efecto tienen, pero no dice cómo se consiguen. Una estructura de datos es la implementación concreta: la forma real en que los datos se colocan en memoria para que esas operaciones funcionen.
El ejemplo más claro es la pila. Como TAD, una pila es «algo donde el último que entra es el primero que sale», y con eso basta para programar contra ella. Como estructura, esa pila puede estar hecha con un array o con una lista enlazada, y el programa que la usa no tiene por qué enterarse.
El tipo abstracto responde al qué y el comportamiento; la estructura de datos responde al cómo. Un mismo TAD admite varias implementaciones, y cada una tiene costes distintos.
| Tipo abstracto | Qué garantiza | Implementaciones habituales |
|---|---|---|
| Lista o secuencia | Elementos en posiciones ordenadas, con repetidos | Array o lista enlazada |
| Pila (stack) | Sale el último que entró | Array o lista enlazada |
| Cola (queue) y bicola (deque) | Sale el primero que entró; la bicola trabaja por los dos extremos | Array circular o lista doblemente enlazada |
| Conjunto y multiconjunto | Pertenencia; el conjunto sin repetidos, el multiconjunto con ellos | Tabla hash o árbol equilibrado (rojo-negro) |
| Diccionario o array asociativo | Pares clave-valor y acceso por clave | Tabla hash o árbol equilibrado |
| Cola de prioridad | Sale siempre el de mayor prioridad, no el más antiguo | Montículo (heap) |
| Árbol | Jerarquía de nodos con una raíz | Nodos enlazados por punteros, o array si el árbol es completo |
| Grafo | Relaciones entre nodos cualesquiera, sin raíz | Matriz de adyacencia o listas de adyacencia |
Para el examen
Tipo abstracto de datos: define el QUÉ: el comportamiento
Estructura de datos: define el CÓMO: la implementación
Cola de prioridad: se implementa con montículo
Diccionario: se implementa con tabla hash o árbol equilibrado
La pila: LIFO
La pila es un almacén con una sola puerta. Todo entra y sale por el mismo extremo, llamado cima, de modo que el último elemento en entrar es el primero en salir. Esa regla se abrevia LIFO, de «last in, first out».
- push: apila un elemento sobre la cima.
- pop: saca el elemento de la cima y lo devuelve, con lo que la pila se queda con uno menos.
- peek o top: mira qué hay en la cima sin sacarlo. Es la diferencia que más se pregunta respecto a pop.
- isEmpty: dice si la pila está vacía, que es lo que hay que comprobar antes de hacer pop.
pila = [ ]
push(pila, "A") -> [A]
push(pila, "B") -> [A, B]
push(pila, "C") -> [A, B, C]
peek(pila) -> devuelve "C", la pila no cambia
pop(pila) -> devuelve "C" -> [A, B]
pop(pila) -> devuelve "B" -> [A]
isEmpty(pila) -> falsoSe usa siempre que haga falta deshacer en orden inverso: la pila de llamadas a funciones de cualquier programa, el botón de deshacer de un editor, el botón de atrás del navegador o la evaluación de expresiones con paréntesis.
Para el examen
Disciplina: LIFO: el último en entrar es el primero en salir
Operaciones: push, pop, peek (consulta sin sacar) e isEmpty
Dónde aparece: pila de llamadas y función deshacer
La cola y la bicola: FIFO
La cola tiene dos puertas: se entra por una y se sale por la otra. El primero que llega es el primero en salir, regla que se abrevia FIFO, de «first in, first out». Es la cola del supermercado, y por eso es la estructura natural de las listas de espera: trabajos de impresión, peticiones a un servidor o mensajes pendientes de procesar.
- enqueue: mete un elemento por el final.
- dequeue: saca el elemento del principio y lo devuelve.
- peek o front: consulta el primero sin sacarlo.
- isEmpty: indica si la cola está vacía.
La bicola o cola doblemente terminada (deque) relaja la regla: permite insertar y extraer por los dos extremos. Con ella se pueden simular tanto una pila como una cola, y por eso su implementación típica es una lista doblemente enlazada, que llega a los dos extremos en tiempo constante.
La cola de prioridad se llama cola pero no es FIFO: no sale el que lleva más tiempo esperando, sino el de mayor prioridad. Su implementación habitual es el montículo, que se ve más adelante.
Pila LIFO, cola FIFO. La bicola trabaja por los dos extremos y la cola de prioridad rompe el orden de llegada.
Para el examen
Disciplina: FIFO: el primero en entrar es el primero en salir
Operaciones: enqueue y dequeue
Bicola (deque): opera por los dos extremos
Cola de prioridad: entrega por prioridad, no por orden de llegada
La lista: acceso posicional
La lista es una secuencia de elementos en la que cada uno ocupa una posición. Admite repetidos y, a diferencia del conjunto, el orden importa: la lista formada por 5, 2, 5 no es la misma que la formada por 2, 5, 5.
- insertarDelante e insertarDetrás: añaden un elemento por la cabeza o por la cola de la secuencia.
- head: devuelve el primer elemento de la lista.
- tail: devuelve la lista entera menos su primer elemento. Ojo, no devuelve el último elemento: devuelve el resto.
- isEmpty: indica si la lista está vacía.
Ese par head y tail es lo que explica el punto que más se pregunta: la lista es posicional pero no tiene una primitiva para saltar directamente al elemento número n. Para llegar al tercero hay que quitar dos veces la cabeza y quedarse con la del resto, es decir, encadenar tail sobre tail y aplicar head al final. Eso obliga a recorrer, y recorrer cuesta tiempo lineal.
En un array el elemento n se alcanza de un salto; en una lista enlazada hay que atravesar los n−1 anteriores. Esa es la diferencia práctica entre las dos implementaciones del mismo tipo abstracto.
Para el examen
head: devuelve la cabeza
tail: devuelve la lista SIN la cabeza
Para llegar al elemento n: encadenar tail: hay que recorrer
Array o lista enlazada: qué se gana y qué se paga
El array reserva memoria contigua: los elementos están pegados unos a otros, así que la dirección de cualquiera se calcula con una multiplicación y una suma. Eso da acceso inmediato por índice, pero insertar en medio obliga a desplazar todo lo que viene detrás, y crecer obliga a pedir un bloque mayor y copiarlo entero.
La lista enlazada guarda cada elemento en un nodo suelto con un puntero al siguiente. Insertar o borrar es cambiar un par de punteros, pero no hay forma de calcular dónde está el elemento n: hay que recorrer desde el principio. Además cada nodo gasta memoria extra en sus punteros.
| Operación | Array | Lista enlazada |
|---|---|---|
| Acceso al elemento de la posición n | O(1) | O(n) |
| Búsqueda de un valor sin orden previo | O(n) | O(n) |
| Búsqueda si está ordenado (binaria) | O(log n) | No aplicable: exige acceso directo |
| Insertar o borrar por la cabeza | O(n) | O(1) |
| Insertar o borrar en un punto ya localizado | O(n) | O(1) si es doblemente enlazada |
| Memoria | Sin punteros, pero exige un bloque contiguo | Punteros por nodo, pero memoria dispersa |
La búsqueda binaria no se puede aplicar a una lista enlazada aunque esté ordenada, porque necesita saltar al elemento central en tiempo constante y ahí no se puede.
Para el examen
Array: acceso O(1), inserción O(n)
Lista enlazada: acceso O(n), inserción O(1) en punto ya localizado
Búsqueda binaria: exige array: necesita acceso directo por índice