UD1 EstructurasDatos
UD1 EstructurasDatos
ESTRUCTURAS DINÁMICAS DE
DATOS
Nombre de la asignatura
[Link]
ÍNDICE Contenido
1. INTRODUCCIÓN........................................................................................ 3
2. OBJETIVOS................................................................................................ 5
2.1 ESPECÍFICOS ........................................................................................... 5
3. TEMA 1: REPASO DE TIPOS DE DATOS ..................................................... 6
3.1. TIPOS DE DATOS BÁSICOS VS. TIPOS DE DATOS ABSTRACTOS (TDA)............... 6
3.2. ESTRUCTURAS DE DATOS (EEDD) .......................................................... 9
3.3. LISTAS ENLAZADAS ............................................................................ 13
3.4. PILAS .............................................................................................. 34
3.5. LISTAS ENLAZADAS ............................................................................ 37
3.6. ÁRBOLES ......................................................................................... 47
3.7. GRAFOS .......................................................................................... 54
3.8. TABLAS DE HASH .............................................................................. 70
3.9. MONTÍCULOS ................................................................................... 74
3.10. COLAS DE PRIORIDAD (PRIORITY QUEUES) .............................................. 94
3.11. CONJUNTOS (SETS).......................................................................... 101
4. BIBLIOGRAFÍA....................................................................................... 108
[Link]
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
1. Introducción
Como bien sabes, los tres pilares fundamentales del desarrollo de sistemas son el diseño, la
programación (o desarrollo) y el almacenamiento. Es en este segundo de estos pilares en el que
cobra mayor importancia el estudio de los algoritmos y las estructuras de datos. Siendo estas
herramientas indispensables para cualquier programador.
Si bien en esta asignatura vamos a trabajar con el lenguaje de programación Python, las
estructuras de datos son herramientas disponibles en todo lenguaje de programación. Por lo
que el correcto entendimiento de estas, nos permitirá aplicar nuestros conocimientos sobre
cualquier otro lenguaje. Estas estructuras de datos nos permiten almacenar o agrupar un
conjunto de elementos o datos, de forma que nos facilitan su gestión enormemente.
Es importante que entiendas que no existe una estructura de datos mejor que otras, cada una
tiene sus pros y sus contras. Es por este motivo que comprendas el funcionamiento de cada una
para poder utilizar la más adecuada en cada caso.
En muchas ocasiones, te encontrarás con la necesidad de trabajar con más de una estructura de
datos a la vez, combinándolas en un mismo algoritmo (en memoria principal) y los datos
almacenados físicamente en disco (bien sea en una base de datos o en un archivo). Trataremos
estos contenidos en los temas 1 y 2 en los que te presentaré y analizaremos las distintas
estructuras de datos y su modelado.
A la hora de diseñar y seleccionar los algoritmos, deberemos observar distintos factores que
condicionan su programación, tales como la eficiencia (referido a las técnicas, recursos y
[Link]
© Universidad Alfonso X el Sabio 3
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Finalmente, en los temas 5 y 6, aplicarás los conocimientos adquiridos en los temas anteriores
para la resolución de problemas y el diseño de nuevos algoritmos.
Ten en cuenta
Dependiendo del problema y las circunstancias que lo rodean,
deberemos seleccionar la o las estructuras de datos más adecuadas, así
como implantar los algoritmos más eficientes.
[Link]
© Universidad Alfonso X el Sabio 4
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
2. Objetivos
2.1 Específicos
Resolver ejercicios prácticos que involucren el uso de listas enlazadas simples para
comprender su aplicación en situaciones reales
Estos conceptos son fundamentales para comprender las estructuras de datos y los
algoritmos en profundidad
[Link]
© Universidad Alfonso X el Sabio 5
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Hasta el momento, seguramente hayas estado utilizando tipos de datos básicos y algunos tipos
de datos abstractos. Vamos a hacer un breve repaso.
Los tipos de datos básicos, los “de toda la vida”, con los que hemos trabajado siempre para
representar datos básicos:
Boolean: o lógicos, representan valores del tipo TRUE o FALSE (verdadero o falso),
ampliamente utilizados en operaciones condicionales o de comparación.
Por otro lado, tenemos los tipos de datos abstractos (TAD) también conocidos como
colecciones que también conocéis del curso pasado:
Tuplas (tuples): similares a las listas, pero inmutables (no se pueden modificar una
vez creadas). Las encontrarás también por el nombre de registros. Por ejemplo:
(1,2,3)
[Link]
© Universidad Alfonso X el Sabio 6
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Veamos una tabla comparativa de las características de los tipos de datos abstractos o
colecciones:
Clave-
TAD Ejemplo Ordenada Mutable Indexable
Valor
[1,2,3] Sí Sí Sí No
Lista
(1,2,3) Sí No Sí No
Tupla
{“id”:1, No Sí No Sí
Diccionario “nombre”:”Pepe”}
{1, 2, 3} No Sí No No
Conjunto
Como puedes observar, cada colección o tipo de dato abstracto tiene sus propias características,
lo cual debemos aprovechar a nuestro favor adecuando su uso para obtener la solución más
adecuada a cada contexto.
Todos los tipos de datos abstractos, excepto las tuplas – debido a su inmutabilidad – disponen
de sistemas que nos permiten añadir, buscar y eliminar elementos dentro de estos. Recordemos
algunos métodos que nos proporciona Python:
Listas (List):
[Link]
© Universidad Alfonso X el Sabio 7
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Diccionarios (Dictionary):
Conjuntos (Set):
[Link]
© Universidad Alfonso X el Sabio 8
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Estos tipos de datos básicos y tipos de datos abstractos son las piezas con las que podemos
construir las estructuras de datos. Comencemos.
Una estructura de datos es como un armario o estantería donde guardas tus cosas. Dependiendo
de qué quieras guardar y cómo quieras acceder a esas cosas, elegirás diferentes tipos de
armarios.
Por ejemplo:
• Si tienes muchos zapatos, quizás quieras un zapatero donde los organices por color o
por tipo. En programación, esto sería como una "lista" donde guardas elementos en un
orden específico.
• Si tienes llaves y quieres acceder rápidamente a cada una según su función (llave de la
casa, del coche, del trabajo...), quizás uses un llavero con etiquetas. Esto se asemejaría
a lo que llamamos "diccionario" o "mapa" en programación, donde guardas cosas según
una clave y un valor.
En resumen, una estructura de datos es como un contenedor que elegimos según cómo
queremos guardar y acceder a la información. Hay muchos tipos, y cada uno tiene sus ventajas
según lo que necesites hacer.
Es como escoger el mejor lugar para guardar tus cosas dependiendo de lo que son y cómo las
usarás. ¡Espero que esto te haya ayudado a entenderlo mejor!
Expresándolo de una forma un poco más formal, una estructura de datos es una forma
organizada y específica de almacenar y organizar datos en una computadora para que puedan
ser utilizados de manera eficiente. Estas estructuras permiten a los datos ser procesados,
accedidos y transferidos de manera efectiva. Son implementaciones concretas de los Tipos de
Datos Abstractos (TDAs) y otras formas de organizar la información. Son esenciales para crear
programas eficientes y efectivos.
Definición
Una estructura de datos es una forma organizada y específica de
almacenar y organizar datos en una computadora para que puedan ser
utilizados de manera eficiente. Son implementaciones concretas de los
TDAs. Estas estructuras permiten a los datos ser procesados, accedidos y
transferidos de manera efectiva.
[Link]
© Universidad Alfonso X el Sabio 9
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Las estructuras de datos varían en términos de eficiencia para diferentes tipos de operaciones,
por lo que la selección adecuada es crucial en la programación. Algunas de las estructuras de
datos más comunes incluyen listas, pilas, colas, árboles y grafos.
El diseño y elección adecuados de las estructuras de datos y sus operaciones son fundamentales
para asegurar que los programas y sistemas funcionen de manera eficiente y efectiva.
[Link]
© Universidad Alfonso X el Sabio 10
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
- Estructuras Estáticas:
o Ejemplos: Arreglos (Arrays) en muchos lenguajes de programación. Ya las
conoces de sobra, por lo que no vamos a entrar en ellas en esta asignatura.
o Memoria Contigua: Los elementos en estructuras estáticas (como los arreglos)
se almacenan en bloques contiguos de memoria.
o Tamaño Predefinido: Al crear una estructura estática, necesitas definir su
tamaño de antemano. Una vez definido, este tamaño no cambia.
- Estructuras Dinámicas:
o Ejemplos: Listas enlazadas, árboles, grafos.
o Memoria No Contigua: Los elementos en estructuras dinámicas (como listas
enlazadas) no necesariamente están ubicados uno al lado del otro en
memoria. Están conectados a través de referencias o punteros.
o Tamaño Variable: Estas estructuras pueden cambiar de tamaño durante la
ejecución del programa. Por ejemplo, puedes añadir o eliminar nodos de una
lista enlazada.
- Estructuras Estáticas:
o Rigidez: Debido a su naturaleza fija, las operaciones que requieren cambios en
el tamaño de la estructura (como inserciones y eliminaciones en un arreglo)
suelen ser ineficientes.
- Estructuras Dinámicas:
o Flexibilidad: Estas estructuras son flexibles en términos de tamaño. Pueden
crecer o encoger según las necesidades del programa. Sin embargo, esta
adaptabilidad viene con el costo de un mayor consumo de memoria debido al
uso de punteros o referencias adicionales.
Ten en cuenta
La diferencia fundamental entre estructuras estáticas y dinámicas se basa
en cómo gestionan y utilizan la memoria. Las estructuras estáticas son fijas
y utilizan bloques de memoria contigua, mientras que las estructuras
dinámicas son adaptables y utilizan punteros o referencias para conectar
sus elementos, que pueden estar dispersos en la memoria.
[Link]
© Universidad Alfonso X el Sabio 11
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
[Link]
© Universidad Alfonso X el Sabio 12
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
En la sección anterior veíamos que las listas enlazadas son colecciones de elementos
denominados nodos, donde cada nodo apunta al siguiente en la lista. Y que estas pueden ser
simples (solo apuntan hacia adelante) o doblemente enlazadas (apuntan hacia adelante y atrás).
- Dato: Contiene el valor del elemento. Este puede ser de cualquier tipo de dato admitido
por el lenguaje - básico o complejo -
- Referencia o Enlace: Apunta al siguiente nodo de la lista
El primer nodo de la lista se llama cabeza (head) y el último nodo apunta a null (None, en
Python), indicando el final de la lista. En el caso de listas circulares, el último nodo apuntará a la
cabeza, en lugar de a null.
- Lista Enlazada Doble: Cada nodo tiene dos enlaces: uno apunta al siguiente nodo y otro
al anterior. Esto facilita el recorrido en ambas direcciones.
[Link]
© Universidad Alfonso X el Sabio 13
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
- Lista Enlazada Circular: El último nodo de la lista apunta de nuevo al primer nodo en
lugar de tener un enlace null.
Dinamismo: pueden crecer o encoger Acceso Secuencial: no hay acceso directo a los
fácilmente. elementos, se debe recorrer desde el inicio.
Inserción y eliminación eficientes Mayor Consumo de Memoria: se necesita
espacio adicional para almacenar
enlaces/pointers.
Uso de memoria según necesidad: no hay No hay soporte para acceso por índice, como
espacio desperdiciado. en los arreglos.
Flexibilidad: se pueden crear variantes como Mayor complejidad en el código al manejar
listas doblemente enlazadas o circulares. enlaces y casos especiales ([Link]., inserciones al
inicio).
Las listas enlazadas son estructuras dinámicas que son especialmente útiles cuando no sabes
cuántos elementos tendrás de antemano o cuando deseas realizar muchas inserciones y
eliminaciones. Sin embargo, su acceso secuencial y el uso adicional de memoria para enlaces
son desventajas a considerar en comparación con otras estructuras como los arreglos.
[Link]
© Universidad Alfonso X el Sabio 14
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
class Nodo:
def __init__(self, valor):
[Link] = valor
[Link] = None
# Ejemplo de uso
nodo1 = Nodo(10)
nodo2 = Nodo(20)
[Link] = nodo2
print([Link]) # Imprimirá 10
print([Link]) # Imprimirá 20
Podemos observar cómo se declaran las dos partes del nodo, el dato y el enlace al elemento
siguiente de la lista. Fíjate que este enlace se crea vacío durante la instanciación del nodo, esto
es por que será la clase ListaEnlazada quien le de valor al incorporarlo a esta.
class ListaEnlazada:
def __init__(self):
[Link] = None
[Link] Insercción
[Link]
© Universidad Alfonso X el Sabio 15
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
En el caso de la inserción, primero comprueba si la lista está vacía. Si lo está, simplemente coloca
el nuevo nodo en la cabeza de la lista. Si no lo está, navega hasta el final de la lista y coloca el
nuevo nodo allí.
[Link]
© Universidad Alfonso X el Sabio 16
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Ilustración. Flujograma de inserción de nuevo nodo al final de la lista. Fuente: elaboración propia
[Link]
© Universidad Alfonso X el Sabio 17
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
# Inserción al final
def insertar(self, dato):
nuevo_nodo = Nodo(dato)
if not [Link]:
[Link] = nuevo_nodo
return
ultimo_nodo = [Link]
while ultimo_nodo.siguiente:
ultimo_nodo = ultimo_nodo.siguiente
ultimo_nodo.siguiente = nuevo_nodo
En este caso eliminamos un nodo con un valor específico. Para ello debemos encontrarlo y
posteriormente eliminarlo. Veamos los pasos:
[Link]
© Universidad Alfonso X el Sabio 18
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
a. El "nodo actual" es `None`, lo que significa que hemos recorrido toda la lista y
el valor no se encuentra en ella. En ese caso, no hay nada más que hacer.
b. Hemos encontrado el nodo con el valor que buscas. El "nodo actual" apunta a
ese nodo y el "nodo previo" apunta al nodo justo antes del que deseas eliminar.
7. Si hemos encontrado el nodo a eliminar:
a. Hacemos que el "nodo siguiente" del "nodo previo" apunte al nodo que viene
después del "nodo actual". De esta manera, hemos "saltado" sobre el "nodo
actual", eliminándolo de la lista.
8. Fin del proceso.
[Link]
© Universidad Alfonso X el Sabio 19
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
[Link]
© Universidad Alfonso X el Sabio 20
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
if nodo_actual is None:
return
[Link] = nodo_actual.siguiente
nodo_actual = None
Ten en cuenta que, dependiendo del lenguaje de programación que estés utilizando - es el caso
de C, C++ o C#, entre otros-, puede ser necesario que liberes la memoria del nodo eliminado de
forma manual. En Python, gracias al recolector de basura, el nodo se eliminará automáticamente
cuando no haya referencias a él.
[Link]
© Universidad Alfonso X el Sabio 21
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
[Link]
© Universidad Alfonso X el Sabio 22
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
[Link]
© Universidad Alfonso X el Sabio 23
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
# Búsqueda
def buscar(self, dato):
nodo_actual = [Link]
while nodo_actual:
if nodo_actual.dato == dato:
return nodo_actual
nodo_actual = nodo_actual.siguiente
return False
Ten en cuenta
Es un proceso sencillo, pero es una operación de búsqueda lineal, lo que
significa que, en el peor de los casos, tendríamos que recorrer toda la lista
para determinar si un valor está presente o no.
[Link]
© Universidad Alfonso X el Sabio 24
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
[Link]
© Universidad Alfonso X el Sabio 25
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
[Link]
© Universidad Alfonso X el Sabio 26
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
1.3.3. 5. Recorrer
En este caso vamos a recorrer todos los nodos de la lista. Para ello comenzamos desde el primer
elemento y avanzamos uno a uno hasta el final. Veamos los pasos:
1. Inicio.
2. Verificamos si la lista está vacía.
a. Si la lista está vacía, informamos que no hay elementos para recorrer y
terminamos el proceso.
3. Si la lista no está vacía:
a. Establecemos un "nodo actual" para empezar en la cabeza de la lista.
4. Comenzamos a recorrer la lista:
a. Mientras el "nodo actual" no sea `None`:
i. Procesamos (o mostramos) el valor del "nodo actual".
ii. Movemos el "nodo actual" al siguiente nodo en la lista.
5. Fin del proceso.
[Link]
© Universidad Alfonso X el Sabio 27
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
[Link]
© Universidad Alfonso X el Sabio 28
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
# Travesía o recorrido
def recorrer(self):
nodos = []
nodo_actual = [Link]
while nodo_actual:
print(nodo_actual.dato)
[Link](nodo_actual.dato)
nodo_actual = nodo_actual.siguiente
return nodos
En este fragmento de código, no solo recorremos la lista, si no que imprimimos el dato de cada
nodo y añadimos cada nodo a una colección de nodos que devolveremos al finalizer el recorrido.
1.3.3. 6. Ordenar
Veamos el proceso de ordenar los elementos de una lista enlazada bajo un criterio específico,
ya sea ascendente o descendente. Hay varios algoritmos para ordenar una lista -los
trabajaremos en temas posteriores-, pero explicaremos un enfoque básico: el algoritmo de
burbuja adaptado para listas enlazadas. Aunque no es el más eficiente, ilustra bien el concepto.
En este ejemplo, asumimos que los datos son de tipo numérico para simplificar el código.
[Link]
© Universidad Alfonso X el Sabio 29
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
[Link]
© Universidad Alfonso X el Sabio 30
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
nodo_actual = [Link]
while nodo_actual:
siguiente = nodo_actual.siguiente
while siguiente:
if nodo_actual.dato > [Link]:
nodo_actual.dato, [Link] = [Link], nodo_actual.dato
siguiente = [Link]
nodo_actual = nodo_actual.siguiente
1. Inicio
2. Comprobamos si la lista está vacía.
a. Si la lista está vacía, informamos que la posición no es válida y terminamos el
proceso.
3. Establecemos un "nodo actual" en la cabeza de la lista y una variable "contador" en 0.
4. Recorremos la lista:
a. Mientras el "nodo actual" no sea `None`:
i. Si "contador" es igual a la posición deseada:
1. Devolvemos el dato del "nodo actual" y terminamos el proceso.
ii. Si no, movemos el "nodo actual" al siguiente nodo y incrementamos el
"contador" en 1.
5. Si se ha recorrido toda la lista y no se ha encontrado la posición, informamos que la
posición no es válida.
6. Fin
[Link]
© Universidad Alfonso X el Sabio 31
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
[Link]
© Universidad Alfonso X el Sabio 32
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Y el otro sistema consiste en incorporar un atributo a nuestra clase ListaEnlazada que nos
permita mantener un contador de elementos de modo que este se actualice cada vez que
llevemos a cabo una operación de insercción o eliminación de nodos.
Desarrolla
Te dejo la doble tarea de:
- Desarrollar un método que recorra la lista contando los elementos
encontrados y devuelva el valor del recuento.
- Incorporar el atributo de clase ‘longitud’ a la clase ‘ListaEnlazada’ y
modificar los métodos de insertar y eliminar para actualizar ese
atributo según convenga.
[Link]
© Universidad Alfonso X el Sabio 33
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
No necesitas el diagrama de flujo de este método ¿verdad? Pero, por si las dudas, te dejo el
código fuente:
# Verificación de vacío
def esta_vacia(self):
return [Link] is None
3.4. Pilas
A esto lo definimos como “Last-In, First-Out” – útimo en llegar, primero en salir, en español – lo
que venimos llamando LIFO.
Ilustración 1. Título. Pila. Fuente: User:Boivie, Public domain, via Wikimedia Commons
[Link]
© Universidad Alfonso X el Sabio 34
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Principales características:
Ordenamiento LIFO: Los elementos se eliminan en el orden inverso en el que se
agregaron. El último elemento que se agrega es el primero en ser eliminado. Orden
inverso.
Operaciones básicas:
Un caso especial es el de las "colas de prioridad", donde los elementos tienen asignadas
prioridades y se eliminan en función de su prioridad en lugar de su orden de llegada. Y, en caso
de que tengan la misma prioridad, se decide por su orden de llegada.
Implementación
Aquí tienes el código fuente de implementación de una pila o stack:
class Stack:
def __init__(self):
[Link] = []
def esta_vacia(self):
return len([Link]) == 0
def pop(self):
if not self.esta_vacia():
return [Link]()
else:
raise IndexError("La pila está vacía")
[Link]
© Universidad Alfonso X el Sabio 35
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
def peek(self):
if not self.esta_vacia():
return [Link][-1]
else:
raise IndexError("La pila está vacía")
def tamano(self):
return len([Link])
# Ejemplo de uso
mi_pila = Stack()
mi_pila.push(1)
mi_pila.push(2)
mi_pila.push(3)
ultimo_elemento = mi_pila.pop()
print("Último elemento eliminado:", ultimo_elemento) # Imprime 3
[Link]
© Universidad Alfonso X el Sabio 36
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Recuerda las fiestas del pueblo de este verano, cuando la orquesta empieza a tocar Paquito el
chocolatero, y todo el mundo se coloca en fila y le da la mano a la persona que tiene delante y
a la que tiene detrás, cada quién sabe quiénes son sus compañeros más inmediatos ya que está
enlazado a ellos. Pues eso es una lista enlazada o linked list.
Dato o Valor: Esta parte almacena el elemento de datos en sí, como un número, una
cadena o cualquier otro tipo de valor.
Valor Ref.
class Nodo:
def __init__(self, valor):
[Link] = valor
[Link] = None
class ListaEnlazada:
def __init__(self):
[Link] = None
[Link]
© Universidad Alfonso X el Sabio 37
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Uso de Memoria: Cada nodo en una lista enlazada requiere memoria adicional para
almacenar la referencia al siguiente nodo. Esto puede hacer que las listas enlazadas
consuman más memoria que estructuras de datos similares, como arrays, que
almacenan elementos de manera contigua.
Podemos encontrar, principalmente, tres tipos de listas enlazadas:
Inserción al principio
[Link]
© Universidad Alfonso X el Sabio 38
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Es importante que sigamos el orden de los pasos anteriores, ya que en caso contrario
perderíamos información sobre los elementos.
Inserción al final
[Link]
© Universidad Alfonso X el Sabio 39
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
2) Comprobamos si la lista está vacía. En caso de que esté vacía insertamos al principio
de la lista. En caso de que no esté vacía seguimos al paso 3.
3) Recorremos la lista hasta que encontramos el último nodo
4) Conectamos el último nodo al nuevo nodo
La eliminación de un nodo en una lista enlazada puede involucrar varios casos, dependiendo de
dónde se encuentra el nodo que se va a eliminar y cómo se relaciona con otros nodos en la lista.
Aquí están los casos más comunes de eliminación de un nodo en una lista enlazada:
Este caso implica eliminar el nodo que está al principio de la lista (la cabeza). Para realizar esta
eliminación, se actualiza la cabeza de la lista para que apunte al siguiente nodo en la lista. Es
importante verificar si la lista está vacía antes de intentar eliminar el primer nodo.
def eliminar_primero(self):
if [Link] is not None:
[Link] = [Link]
[Link]
© Universidad Alfonso X el Sabio 40
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Para eliminar el último nodo de la lista, se debe recorrer la lista hasta llegar al penúltimo nodo
(el nodo anterior al último). Luego, se ajusta el puntero del penúltimo nodo para que apunte a
`None` o al nodo siguiente (dependiendo de cómo se implemente la lista enlazada), lo que
desconecta el último nodo.
def eliminar_ultimo(self):
if [Link] is None:
return
if [Link] is None:
[Link] = None
return
actual = [Link]
while [Link]:
actual = [Link]
[Link] = None
Cuando el nodo no es ni el primero ni el último, se deben ajustar los punteros del nodo anterior
al nodo que se va a eliminar y del nodo que se va a eliminar al siguiente nodo. Este proceso
desconecta el nodo que se va a eliminar de la lista y lo hace elegible para la recolección de
basura.
En caso de que se deba eliminar el nodo en base a su valor, se debe buscar el nodo que contiene
el valor que se desea eliminar. Una vez encontrado, se ajustan los punteros del nodo anterior al
nodo que se va a eliminar y del nodo que se va a eliminar al siguiente nodo.
if [Link] == valor:
[Link] = [Link]
return
actual = [Link]
while [Link]:
if [Link] == valor:
[Link]
© Universidad Alfonso X el Sabio 41
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
[Link] = [Link]
return
actual = [Link]
Otro caso un tanto más particular es cuando necesitamos eliminar todos los nodos que
contienen un valor duplicado en la lista. Esto significa que debemos recorrer la lista y, cuando
encontramos un nodo con el valor deseado, lo eliminamos de la misma manera que en los casos
anteriores.
Este proceso suele requerir un bucle para eliminar todas las ocurrencias del valor duplicado.
En caso de que necesitemos eliminar todos los nodos de la lista para dejarla vacía, podemos
hacer un bucle que recorra la lista y elimine cada nodo uno por uno.
La eliminación de nodos en una lista enlazada es una operación importante que debe
implementarse con cuidado para asegurarnos de que los punteros se actualicen correctamente
y que no se produzcan fugas de memoria. La implementación exacta de estos casos de
eliminación puede variar según la estructura de la lista enlazada y cómo se gestionen los
punteros o enlaces.
Dejo para ti la implementación de los métodos necesarios para estos dos últimos casos.
¿Cuál sería el código base para un método de búsqueda de elementos en una lista enlazada?
[Link]
© Universidad Alfonso X el Sabio 42
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
En este punto podemos optar por dos soluciones, una es recorrer todos los elementos de la lista,
contando uno a uno. La otra opción es que incorporemos una propiedad a nuestra clase lista, en
la que llevemos el control de la cantidad de elementos que contiene. En este caso, recuerda que
debemos aumentar o disminuir su valor según el tipo de operación que hayamos realizado.
class ListaEnlazada:
def __init__(self):
[Link] = None
[Link] = 0 # Inicialmente, la lista está vacía
# ...
[Link]
© Universidad Alfonso X el Sabio 43
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
El método invertir recorre la lista enlazada original y, en cada paso, invierte el puntero siguiente
del nodo actual para que apunte al nodo anterior (previo). De esta manera, los nodos se
reorganizan en orden inverso. Al final del bucle, la cabeza de la lista se establece como el último
nodo invertido (anterior cabeza).
def invertir(self):
previo = None # Nodo anterior
actual = [Link] # Nodo actual
while actual:
siguiente = [Link] # Guardar el siguiente nodo
[Link] = previo # Invertir el ptro. del nodo actual
previo = actual # Mover previo al nodo actual
actual = siguiente # Mover actual al siguiente nodo
[Link] = previo # La nueva cabeza será el último nodo invertido
[Link]
© Universidad Alfonso X el Sabio 44
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
En este método tomamos otra lista enlazada como argumento y la concatenamos con la lista
actual. Si la lista actual está vacía, simplemente establecemos su cabeza como la cabeza de la
otra lista. Si la lista actual no está vacía, recorremos la lista actual hasta encontrar el último nodo
y conectamos su siguiente al principio de la otra lista.
[Link]
© Universidad Alfonso X el Sabio 45
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
class NodoDoble:
def __init__(self, dato):
[Link] = dato
[Link] = None
[Link] = None
Te propongo un ejercicio, escribe el código de los siguientes métodos de una lista doblemente
enlazada:
[Link]
© Universidad Alfonso X el Sabio 46
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
3.6. Árboles
Imagina un árbol como el que puedes ver en la naturaleza, con un tronco y ramas que se dividen
en más ramas, y así sucesivamente. En el mundo de las estructuras de datos, un árbol es una
forma de organizar datos de manera jerárquica, donde cada elemento es un nodo.
Tipos de árboles
Dependiendo de la cantidad máxima de hijos por nodo y su comportamiento encontramos
distintos tipos de árboles
Árbol N-ario: Un tipo de árbol en el que cada nodo puede tener hasta n nodos hijos.
Árbol Binario: Un tipo de árbol en el que cada nodo tiene como máximo dos hijos,
uno izquierdo y otro derecho.
Árbol de Búsqueda Binaria (BST): Un árbol binario en el que los valores están
organizados de manera que los valores menores se ubican en el subárbol izquierdo
y los valores mayores en el subárbol derecho.
En cuanto a los nodos – también conocidos como vértices - de un árbol, cada nodo puede tener
cero o más "hijos". Estos hijos son otros nodos conectados a él. Entre los nodos tenemos
diferentes categorías:
Nodo Raíz: es el nodo superior en la jerarquía del árbol. Es el punto de partida desde
el cual se ramifican todos los demás nodos. En un árbol, solo hay un nodo raíz.
[Link]
© Universidad Alfonso X el Sabio 47
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Nodo Padre: todo nodo (excepto el nodo raíz) tiene un nodo padre, que es el nodo
directamente superior a él. El nodo padre tiene una referencia a cada uno de sus
nodos hijos
Nodo Hijo: son los nodos que están conectados directamente a un nodo padre.
Pueden ser cero o más, dependiendo del tipo de árbol.
Nodo Hoja: son los nodos que no tienen hijos. Estos son los nodos finales en una
rama del árbol.
Nodo Interno: los nodos internos son aquellos que tienen uno o más nodos hijos.
Están en algún lugar entre el nodo raíz y los nodos hoja.
Antes de continuar, vamos a hacer un repaso de otros conceptos importantes acerca de los
árboles:
Altura: La longitud del camino más largo desde un nodo hasta un nodo hoja en un
árbol.
Altura del árbol: Es la longitud del camino desde el nodo raíz hasta el nodo hoja más
lejano.
Nivel: La distancia entre un nodo y el nodo raíz. El nodo raíz tiene nivel 0.
[Link]
© Universidad Alfonso X el Sabio 48
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Arista: La relación entre un par de nodos, solemos pintarlas con una flecha (o línea)
Descendientes de un nodo, son todos aquellos nodos que forman parte del subárbol
cuya raíz es ese nodo (los hijos, los nietos, bisnietos …)
Bien, ahora que ya tenemos claros estos conceptos, vamos a por el código. Veamos la
implementación de un árbol N-ario.
[Link]
© Universidad Alfonso X el Sabio 49
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Tipo de árbol: Es un árbol jerárquico donde "Raíz" es el nodo raíz del árbol y tiene
tres hijos directos: "P1," "P2," y "P3."
Niveles: El árbol tiene cuatro niveles en total, contando desde la raíz ("Raíz") hasta
los nodos más profundos.
Número de hijos: Los nodos pueden tener entre 0 y 2 hijos, lo que representa la
variabilidad en la cantidad de descendientes que un nodo puede tener.
A continuación, vamos a analizar los nodos individuales:
Raíz: Es el nodo raíz del árbol y no tiene un padre. Tiene tres hijos directos: "P1,"
"P2," y "P3."
P1: Es un nodo intermedio con tres hijos: "H1.1," "P1.2," y "H1.3." Muestra que un
nodo intermedio puede tener múltiples descendientes, incluyendo nodos hoja y
nodos intermedios.
P2: Es un nodo intermedio con un solo hijo, "H2.1." Representa un caso donde un
nodo intermedio tiene un único descendiente.
P3: Es un nodo intermedio con dos hijos: "P3.1" y "H3.2." Esto muestra cómo un
nodo intermedio puede tener 0 o más hijos, incluyendo otros nodos intermedios y
nodos hoja.
H2.1: Es una hoja porque no tiene hijos. Esto demuestra que un nodo puede ser una
hoja directamente bajo un nodo intermedio.
P1.2: Es un nodo intermedio con dos hijos: "H1.2.1" y "H1.2.2." Representa un caso
de un nodo intermedio con múltiples descendientes.
Definamos, en primer lugar, el nodo de un árbol N-Ario, la clase del árbol y su constructor:
class NodoArbolNArio:
def __init__(self, valor):
[Link] = valor
[Link] = []
class ArbolNArio:
def __init__(self, raiz=None):
[Link] = raiz
[Link]
© Universidad Alfonso X el Sabio 51
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
def enumerar_nodos(self):
nodos = []
if [Link]:
self._enumerar_recursivo([Link], nodos)
return nodos
[Link]
© Universidad Alfonso X el Sabio 52
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Como puedes observar, en los árboles hacemos uso intensivo de la recursividad para recorrer el
árbol.
[Link]
© Universidad Alfonso X el Sabio 53
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
3.7. Grafos
En esta ocasión te pido que pienses en un plano de un metro o autobús de tu ciudad más
cercana. Si haces un pequeño ejercicio de abstracción podrás identificar cada una de las
estaciones o paradas como si fueran nodos – de hecho, lo son -, y cada una de las líneas que
conectan dos paradas serían – ¡oh sorpresa! – las aristas.
Ahora que ya lo visualizamos, podemos decir que un grafo es una estructura de datos que se
utiliza para representar relaciones entre elementos. Puedes pensar en un grafo como un
conjunto de nodos que están conectados entre sí mediante enlaces. Los grafos se utilizan para
modelar una amplia variedad de situaciones del mundo real, como redes sociales, sistemas de
navegación, mapas, diagramas de flujo y más.
Podemos definir un grafo como una estructura de datos que consiste en un conjunto finito de
nodos (o vértices) y un conjunto de aristas (o enlaces) que conectan estos nodos. Cada arista
conecta dos nodos y puede tener una dirección (en el caso de un grafo dirigido) o no tenerla (en
el caso de un grafo no dirigido). Además, las aristas pueden tener pesos asociados, que
representan costos, distancias, etc.
Tipos de Grafos
Existen varios tipos de grafos, centrémonos en los principales:
Grafo dirigido (digrafo): Las aristas tienen dirección, es decir, van de un vértice a otro.
Terminología de grafos
Aristas adyacentes: Las aristas adyacentes son dos aristas que comparten un mismo
nodo final. Es decir, ambas aristas están conectadas a un mismo nodo.
Aristas paralelas: Son dos o más aristas que conectan los mismos dos nodos. En otras
palabras, representan múltiples conexiones entre los mismos nodos.
[Link]
© Universidad Alfonso X el Sabio 54
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Aristas cíclicas o bucles: Una arista cíclica o bucle es una arista que conecta un nodo
consigo mismo. Esto significa que un nodo tiene una relación o conexión directa con él
mismo.
Camino: Un camino en un grafo es una secuencia de nodos en la que cada par de nodos
adyacentes en la secuencia está conectado por una arista. En otras palabras, es una ruta
que se puede seguir desde un nodo inicial a un nodo final pasando por otros nodos
intermedios a lo largo de las aristas.
Grafo Dirigido (u Orientado): En un grafo dirigido, las aristas tienen una dirección
específica, es decir, tienen un punto de inicio y un punto final. La relación entre dos
nodos va en una dirección definida, como las flechas en un diagrama de flujo.
Grafo Etiquetado (o Ponderado): En un grafo etiquetado, cada arista tiene una etiqueta
o peso asociado que representa alguna medida o valor. Estos pesos pueden representar
distancias – piensa en las rutas del GPS del coche -, costos, tiempos, etc.
Grafo Plano: Un grafo plano es aquel que puede dibujarse en el plano (en un papel o
una pantalla) de manera que las aristas no se crucen entre sí. Es decir, no hay
superposición de aristas.
Grafo Conexo: Un grafo conexo es aquel en el que existe un camino entre cualquier par
de nodos. No importa cuán lejos estén los nodos, siempre es posible llegar de uno a otro
siguiendo las aristas.
Redes Sociales: Los grafos se utilizan para representar y analizar redes sociales, donde
los nodos son individuos y las aristas representan relaciones entre ellos (amigos,
seguidores, etc.).
[Link]
© Universidad Alfonso X el Sabio 55
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Mapeo y Navegación: Los servicios de mapas utilizan grafos donde los nodos son lugares
y las aristas son caminos o carreteras para encontrar la ruta más corta entre dos puntos.
Optimización: Los problemas de flujo de red, como el flujo máximo, son modelados y
resueltos utilizando grafos.
Base de Datos: Las bases de datos basadas en grafos, como Neo4j, se especializan en
almacenar y consultar datos en forma de grafo, lo que es útil para casos donde las
relaciones entre los datos son tan importantes como los datos en sí.
Estos son solo algunos ejemplos de cómo se utilizan los grafos en la vida real. Su aplicabilidad es
vasta y sigue creciendo a medida que emergen nuevos desafíos y tecnologías.
� Usamos los grafos cuando las relaciones entre los datos son tan importantes como
los datos en sí.
[Link]
© Universidad Alfonso X el Sabio 56
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Ventajas y desventajas
Ventajas Desventajas
Flexibilidad para modelar diferentes tipos de Puede ser difícil de visualizar para grandes
relaciones (ponderadas, no ponderadas, datasets.
dirigidas, no dirigidas).
Los grafos son una herramienta poderosa en ciencias de la computación y matemáticas y tienen
aplicaciones en muchas áreas, desde la planificación de rutas y redes hasta la organización de
información y la modelización de sistemas sociales y biológicos.
Matriz de adyacencia
Una matriz de adyacencia es una forma de representar un grafo finito G=(V,E), donde V es un
conjunto de vértices/nodos y E es un conjunto de aristas/edges. Esta matriz es cuadrada (tiene
la misma cantidad de filas que de columnas) y su tamaño es ∣V∣×∣V∣, donde ∣V∣ es el número de
vértices en el grafo.
[Link]
© Universidad Alfonso X el Sabio 57
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Las filas y columnas corresponden a los vértices. El valor almacenado en la celda en la i-ésima
fila y j-ésima columna de la matriz indica la relación entre el i-ésimo vértice y el j-ésimo vértice.
Si el valor en la posición (i,j) es 1, significa que hay una arista entre los vértices i y j.
[Link]
© Universidad Alfonso X el Sabio 58
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Si el valor en la posición (i,j) es 1, significa que hay una arista que va del vértice i al vértice j.
A 0 1 0 1
B 1 0 1 0
C 0 1 0 1
D 1 0 1 0
Donde:
Implementación de un grafo
Dentro de las formas de implementar un grafo podemos destacar las siguientes:
[Link]
© Universidad Alfonso X el Sabio 59
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Lista de adyacencia: En esta representación, cada nodo tiene una lista de sus nodos
vecinos. Esto es especialmente útil para grafos dispersos, donde no todas las parejas de
nodos están conectadas.
o Nodo A: [B, D]
o Nodo B: [A, C, D]
o Nodo C: [B, D]
o Nodo D: [A, B, C]
Ilustración 7 Grafo obtenido a partir de la matriz y/o lista de adyacencia. Fuente: elaboración propia
Agregar nodo
Agregar arista
Eliminar nodo
Eliminar arista
Verificar conexión: (comprobar si existe un camino entre dos nodos) o recorrer el grafo.
Vecinos o adyacentes: listar los nodos con los que un nodo determinado tiene conexión
directa
Grado de un nodo: cantidad de nodos con los que un nodo determinado tiene conexión
directa
[Link]
© Universidad Alfonso X el Sabio 60
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Implementación
Veamos una implementación básica de un grafo.
class Grafo:
'''
Clase Grafo para implementar una estructura de datos de grafo.
Atributos:
nodos (dict): Diccionario que mapea cada nodo a una lista de nodos adyacentes.
'''
def __init__(self):
'''
Inicializa un grafo vacío.
'''
[Link] = {}
[Link]
© Universidad Alfonso X el Sabio 61
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
[Link][destino].append(origen)
def mostrar_grafo(self):
'''
Imprime una representación del grafo.
Muestra cada nodo y sus nodos adyacentes.
'''
# Recorre cada nodo y sus nodos adyacentes, imprimiéndolos.
for nodo in [Link]:
print(f"{nodo} -> {', '.join(map(str, [Link][nodo]))}")
Este método es útil para encontrar todos los nodos que están directamente
conectados a un nodo dado, junto con el peso de las aristas que los conectan.
[Link]
© Universidad Alfonso X el Sabio 62
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Veamos cómo sería la matriz de adyacencia de un grafo ponderado. Observa cómo los valores
de la matriz ya no se limitan a valores de 0 ó 1. Fíjate también en cómo se establece el bucle o
arista cíclica en el nodo D.
A B C D
A 0 2 4 0
B 2 0 3 1
C 4 3 0 5
D 0 1 5 3
En el caso de grafos ponderados, debemos tener en cuenta que la implementación es algo más
compleja que en los grafos no ponderados. Tenemos que manejar, además de
class GrafoPonderado:
'''
Clase GrafoPonderado para representar un grafo ponderado.
Atributos:
[Link]
© Universidad Alfonso X el Sabio 63
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
grafo (dict): Diccionario que representa el grafo, donde las claves son los
vértices y los valores son diccionarios
que mapean los vértices adyacentes y sus pesos asociados.
'''
def __init__(self):
'''
Inicializa un grafo ponderado vacío.
'''
[Link] = {}
def agregar_arista(self, vertice1: Any, vertice2: Any, peso: float) -> None:
'''
Agrega una arista ponderada entre dos vértices.
[Link]
© Universidad Alfonso X el Sabio 64
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
del [Link][v][vertice]
[Link]
© Universidad Alfonso X el Sabio 65
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
return [Link][vertice1][vertice2]
else:
return None
:return: Una cadena que representa el grafo, mostrando cada arista y su peso.
'''
resultado = ""
for vertice1 in [Link]:
for vertice2, peso in [Link][vertice1].items():
resultado += f"{vertice1} --({peso})--> {vertice2}\n"
return resultado
# Imprimir el grafo
print("Grafo Ponderado:")
print(grafo)
[Link]
© Universidad Alfonso X el Sabio 66
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
print(grafo)
Grafo dirigido
La diferencia reside en que las aristas sí tienen dirección, es decir, si hay una arista entre el nodo
A y el nodo B, no necesariamente hay hay una arista entre el nodo B y el nodo A. A continuación,
vemos una implementación básica de un grafo dirigido utilizando diccionarios en Python:
class GrafoDirigido:
'''
Clase GrafoDirigido para representar un grafo dirigido.
Atributos:
nodos (dict): Diccionario que representa el grafo, mapeando cada nodo a una lista
de nodos a los que se dirige.
'''
def __init__(self):
'''
Inicializa un grafo dirigido vacío.
'''
[Link] = {}
[Link]
© Universidad Alfonso X el Sabio 67
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Muestra cada nodo junto con una lista de nodos a los cuales se dirige.
Esta representación ayuda a visualizar las conexiones del grafo dirigido.
'''
for nodo in [Link]:
print(f"{nodo} -> {', '.join(map(str, [Link][nodo]))}")
Ejercicio de repaso
Sistema de recomendación de rutas en una ciudad
Escenario:
Te has unido a un equipo encargado de desarrollar una aplicación móvil de navegación para una
ciudad. El objetivo principal de esta aplicación es ofrecer a los usuarios diferentes rutas entre
dos puntos de la ciudad, considerando diferentes criterios como la distancia más corta, el menor
tiempo o el número mínimo de paradas. La ciudad se modelará usando un grafo, donde cada
nodo representa un punto de interés o una intersección y cada arista representa un camino
entre esos dos puntos.
Funcionalidades a implementar:
o agregar_punto(nombre): Permite agregar un punto de interés o
intersección a la ciudad.
o conectar_puntos(nombre_a, nombre_b, distancia): Conecta
dos puntos con una distancia específica.
[Link]
© Universidad Alfonso X el Sabio 68
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Caso práctico:
o Crea un pequeño mapa de una ciudad ficticia con al menos 10 puntos de interés.
o Agrega conexiones entre estos puntos con diferentes distancias.
o Implementa y prueba los algoritmos para obtener la ruta más corta y la ruta con
menos paradas entre diferentes puntos de interés.
Nota: Para resolver este ejercicio, podrías considerar algoritmos como el de Dijkstra (lo
encontrarás en unidades posteriores) para determinar la ruta más corta entre dos puntos.
[Link]
© Universidad Alfonso X el Sabio 69
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
A menudo utilizamos códigos hash en nuestro día a día, aunque quizás no seamos conscientes
de ello, el más frecuente es el de la letra del DNI, aunque también las usamos en multitud de
bases de datos, en la caché de los navegadores, para detectar duplicados en una colección de
elementos complejos o para acelerar procesos de búsqueda en nuestro sistema de archivos.
Vídeo
Calcular la letra del DNI en segundos
[Link]
Las tablas hash son como una especie de "caja mágica" que nos ayuda a guardar información de
manera rápida y eficiente. Imagina que tienes una caja con muchos compartimentos, y cada
compartimento tiene un número único. Puedes poner cosas dentro de estos compartimentos y
luego encontrarlas muy rápido cuando las necesites. Eso es básicamente lo que hace una tabla
hash, pero en lugar de compartimentos, usa números especiales para guardar y encontrar
información.
Las tablas hash utilizan una función especial, llamada función de hash, para convertir la
información que queremos guardar en un número. Esta función de hash toma la información y
la transforma en un número único. Luego, ese número se usa como "dirección" para encontrar
el lugar donde se guarda la información en la tabla hash.
Imagina que tienes un libro que quieres guardar en tu biblioteca. En lugar de buscar el libro en
todas las estanterías, puedes usar la función de hash para obtener un número único para ese
libro y luego guardarlo en la estantería con ese número. Cuando quieras encontrar el libro,
simplemente usas la función de hash nuevamente para obtener el número y vas directamente
a la estantería correcta, ¡así encuentras el libro mucho más rápido! Pero ¡espera! Eso es una
biblioteca ¿no?
[Link]
© Universidad Alfonso X el Sabio 70
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
class TablaHash:
def __init__(self, capacidad_inicial=10):
[Link] = capacidad_inicial
[Link] = 0 # Número de elementos en la tabla
[Link] = [None] * [Link] # Inicializa la tabla con valores nulos
[Link]
© Universidad Alfonso X el Sabio 71
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
[Link] += 1
def _redimensionar(self):
# Duplica la capacidad de la tabla y reorganiza los elementos.
nueva_capacidad = [Link] * 2
nueva_tabla = [None] * nueva_capacidad
[Link] = nueva_capacidad
[Link]
© Universidad Alfonso X el Sabio 72
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
[Link] = nueva_tabla
[Link]
© Universidad Alfonso X el Sabio 73
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
3.9. Montículos
Un montículo (heap) es una estructura de datos de tipo árbol binario especializado que satisface
la propiedad del montículo. Esta propiedad implica un orden específico entre nodos padre e
hijo. Hay dos tipos principales de montículos:
• Montículo máximo (Max Heap): Para cualquier nodo dado, el valor de ese nodo es
mayor o igual que los valores de sus hijos.
• Montículo mínimo (Min Heap): Para cualquier nodo dado, el valor de ese nodo es
menor o igual que los valores de sus hijos.
Las operaciones clave en un montículo incluyen la inserción, la eliminación y la obtención del
elemento máximo (en un Max Heap) o mínimo (en un Min Heap). Los montículos son esenciales
para algoritmos como el de ordenación por montículos (Heapsort) y algoritmos de ruta más
corta como Dijkstra.
Operaciones básicas
Las operaciones básicas asociadas a un montículo (heap) son:
[Link]
© Universidad Alfonso X el Sabio 74
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Estas son las operaciones básicas asociadas a los montículos. Es importante señalar que la
eficiencia de estas operaciones es lo que hace que los montículos sean tan útiles en algoritmos
como los algoritmos de ruta más corta y algoritmos de ordenación como heapsort.
Ventajas y Desventajas
Ventajas Desventajas
Aprovecha bien la localidad de referencia en Los montículos binarios tienen una estructura
memoria, lo que lo hace relativamente rápido menos rígida que los árboles de búsqueda
en la práctica binaria, por lo que podrían no ser tan
intuitivos.
[Link]
© Universidad Alfonso X el Sabio 75
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Implementación
Comenzamos con la implementación de una clase Empleado a modo de ejemplo – o plantilla
de base - utilizada como valor de los nodos
Clase Empleado
class Empleado:
'''
Clase Empleado para representar la información de un empleado.
Atributos:
num_empleado (int): Número identificador del empleado.
nombre (str): Nombre del empleado.
apellidos (str): Apellidos del empleado.
dni (str): Documento Nacional de Identidad del empleado.
fecha_nac (str): Fecha de nacimiento del empleado.
'''
[Link]
© Universidad Alfonso X el Sabio 76
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
def __str__(self):
'''
Representación en cadena del empleado, mostrando su número identificador.
[Link]
© Universidad Alfonso X el Sabio 77
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
[Link]
© Universidad Alfonso X el Sabio 78
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Clase Nodo
Continuamos con la clase Nodo que nos permitirá crear la estructura básica de almacenamiento
de la información
class Nodo:
'''
Clase Nodo para representar un nodo en estructuras de datos basadas en árboles.
Atributos:
valor (Any): El valor almacenado en el nodo.
hijo_izquierda (Nodo): Referencia al hijo izquierdo del nodo.
hijo_derecha (Nodo): Referencia al hijo derecho del nodo.
'''
[Link]
© Universidad Alfonso X el Sabio 79
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Recuerda que un montículo máximo (Max Heap) cumple que el nodo padre siempre es mayor
que sus nodos hijos, mientras que en un Min Heap es al contrario.
class MaxHeap:
'''
Clase MaxHeap para representar un montículo máximo.
En un montículo máximo, cada nodo padre es mayor o igual que sus hijos.
'''
[Link]
© Universidad Alfonso X el Sabio 80
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
[Link](nuevoNodo)
self._bubble_up(len([Link]) - 1)
raiz = [Link][0]
[Link][0] = [Link]()
self._sink_down(0)
return raiz
[Link]
© Universidad Alfonso X el Sabio 81
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
[Link]
© Universidad Alfonso X el Sabio 82
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
# Reajusta el montículo
for i in range(([Link]() // 2) - 1, -1, -1):
self._sink_down(i)
[Link]
© Universidad Alfonso X el Sabio 83
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
[Link][index].value = new_value
self._sink_down(index)
Un montículo mínimo (Min Heap) es similar en estructura a un montículo máximo (Max Heap).
La principal diferencia radica en la propiedad de ordenación: en un Min Heap, el nodo padre
siempre es menor que sus nodos hijos, mientras que en un Max Heap es al contrario.
class MinHeap:
'''
Clase MinHeap para representar un montículo mínimo.
En un montículo mínimo, cada nodo es menor que sus hijos.
[Link]
© Universidad Alfonso X el Sabio 84
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
'''
if menor != index:
[Link][menor], [Link][index] = [Link][index], [Link][menor]
self._sink_down(menor)
[Link]
© Universidad Alfonso X el Sabio 85
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
def pop(self):
'''
[Link]
© Universidad Alfonso X el Sabio 86
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
[Link]
© Universidad Alfonso X el Sabio 87
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
index = self.find_value(value)
if index < 0:
print('Valor no encontrado')
return
# Reajusta el montículo
for i in range(([Link]() // 2) - 1, -1, -1):
self._sink_down(i)
[Link]
© Universidad Alfonso X el Sabio 88
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Heap es la clase base que implementa la estructura y las operaciones comunes de un montículo.
Acepta una función de comparación (comparison_func) que determina cómo se comparan
los elementos (por ejemplo, para un montículo máximo o mínimo).
MaxHeap y MinHeap son subclases de Heap que especifican la lógica de comparación para
montículos máximos y mínimos, respectivamente. La función lambda pasada a
super().__init__ en cada subclase determina el comportamiento del montículo (máximo
o mínimo).
Esta estructura aprovecha la herencia y la reutilización del código, lo que nos facilita la gestión
y mantenimiento del código relacionado con los montículos.
[Link]
© Universidad Alfonso X el Sabio 89
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
class Heap:
'''
Clase base Heap para implementar la estructura y operaciones comunes de un
montículo.
Atributos:
heap (list): Lista para almacenar los elementos del montículo.
compare (function): Función de comparación para determinar el ordenamiento en el
montículo.
'''
[Link]
© Universidad Alfonso X el Sabio 90
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
while True:
left = self._left_child(index)
right = self._right_child(index)
if element != index:
[Link]
© Universidad Alfonso X el Sabio 91
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
def pop(self):
'''
Elimina y devuelve el elemento en la cima del montículo.
item = [Link][0]
last = [Link]()
if [Link]:
[Link][0] = last
self._sink_down(0)
return item
def peek(self):
'''
Devuelve el elemento en la cima del montículo sin eliminarlo.
[Link]
© Universidad Alfonso X el Sabio 92
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
def size(self):
'''
Devuelve el número de elementos en el montículo.
def is_empty(self):
'''
Verifica si el montículo está vacío.
class MaxHeap(Heap):
'''
Clase MaxHeap para implementar un montículo máximo, donde cada nodo padre es
mayor que sus hijos.
Hereda de la clase Heap.
'''
def __init__(self):
'''
Inicializa un montículo máximo.
Utiliza una función de comparación donde los padres son mayores que sus
hijos.
'''
super().__init__(lambda parent, child: parent < child)
[Link]
© Universidad Alfonso X el Sabio 93
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
class MinHeap(Heap):
'''
Clase MinHeap para implementar un montículo mínimo, donde cada nodo padre es
menor que sus hijos.
Hereda de la clase Heap.
'''
def __init__(self):
'''
Inicializa un montículo mínimo.
Utiliza una función de comparación donde los padres son menores que sus
hijos.
'''
super().__init__(lambda parent, child: parent > child)
Una cola de prioridad es una estructura de datos abstracta similar a una cola común o una pila,
pero donde cada elemento tiene una prioridad asignada. Los elementos se sirven según sus
prioridades, en lugar de el orden en que se añadieron a la cola. En general, los elementos con
prioridades más altas se sirven antes que los elementos con prioridades más bajas.
Operaciones básicas
Las operaciones básicas de una cola de prioridad son:
Insertar (o encolar):
o Añadir un elemento a la cola con una prioridad dada.
Eliminar (o desencolar):
o Remover y retornar el elemento con la más alta (o más baja, dependiendo de la
implementación) prioridad.
isEmpty:
[Link]
© Universidad Alfonso X el Sabio 94
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Tamaño:
o Retornar el número de elementos en la cola.
Modificar prioridad:
o Cambiar la prioridad de un elemento específico. (Nota: no todas las
implementaciones ofrecen esta operación de manera eficiente).
Fusionar:
o Combinar dos colas de prioridad en una sola. (Esta operación tampoco es común
en todas las implementaciones, pero algunas estructuras avanzadas la
soportan).
Estas operaciones garantizan que se pueda administrar de manera efectiva una colección de
elementos con prioridades, asegurando que los elementos de mayor prioridad sean procesados
antes que aquellos de menor prioridad.
Compresión de Datos:
o En algoritmos de compresión como Huffman, se utiliza una cola de prioridad
para construir el árbol de Huffman.
Simulaciones:
o Las colas de prioridad son útiles en simulaciones basadas en eventos. Por
ejemplo, en la simulación de un sistema en el que varios eventos ocurren en
diferentes momentos, la cola de prioridad puede ser usada para determinar cuál
es el próximo evento a simular.
[Link]
© Universidad Alfonso X el Sabio 95
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Algoritmo de Prim:
o Usado para encontrar el árbol de expansión mínima en un grafo. La cola de
prioridad ayuda a seleccionar el siguiente borde de peso mínimo.
Algoritmos de Búsqueda:
o En algoritmos como A*, la cola de prioridad se utiliza para explorar el siguiente
nodo más prometedor basado en una función heurística.
Gestión de Red:
o En la gestión de tráfico de redes, las colas de prioridad pueden ser usadas para
manejar paquetes de datos basados en su prioridad o urgencia.
Algoritmos de Fusión:
o Al fusionar múltiples listas ordenadas en una sola lista ordenada.
Estos son solo algunos ejemplos y hay muchas otras aplicaciones de colas de prioridad en la
informática. La capacidad de administrar elementos basados en la prioridad es una necesidad
común en muchos escenarios, y las colas de prioridad ofrecen una forma eficiente de hacerlo.
Ventajas Desventajas
[Link]
© Universidad Alfonso X el Sabio 96
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Implementación
Las colas de prioridad se pueden implementar de diversas maneras, siendo los montículos
(heaps) la estructura de datos más comúnmente utilizada para este fin, debido a su eficiencia.
En particular, el montículo binario (binary heap) es una estructura muy popular para
implementar una cola de prioridad.
En la librería estándar de Python, el módulo heapq provee funciones para convertir listas en
montículos con complejidad O(n) y operaciones de inserción y extracción con complejidad
logarítmica. Sin embargo, hay que tener en cuenta que, aunque este módulo permite simular
una cola de prioridad, no provee una implementación directa de una estructura de cola de
prioridad.
import heapq
class PriorityQueue:
'''
Clase PriorityQueue para implementar una cola de prioridades.
Atributos:
queue (list): Lista para almacenar los elementos de la cola de prioridades.
index (int): Índice secuencial para mantener un orden estable en elementos con
igual prioridad.
'''
def __init__(self):
'''
Inicializa la cola de prioridades.
'''
[Link] = []
[Link] = 0
[Link]
© Universidad Alfonso X el Sabio 97
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
def pop(self):
'''
Elimina y devuelve el elemento con la máxima prioridad de la cola.
def peek(self):
'''
Devuelve el elemento con la máxima prioridad sin eliminarlo de la cola.
def is_empty(self):
'''
Verifica si la cola de prioridades está vacía.
[Link]
© Universidad Alfonso X el Sabio 98
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
[Link]("Tarea 1", 1)
[Link]("Tarea 2", 4)
[Link]("Tarea 3", 2)
En esta implementación, cada elemento de la cola tiene una prioridad asociada. Cuando los
elementos se agregan a la cola, se les da una prioridad y se encolan en consecuencia. Cuando se
desencolan, el elemento con la mayor prioridad (el número más grande en este caso) se
desencola primero.
La variable index es una técnica utilizada para desempatar en caso de que dos elementos
tengan la misma prioridad. Gracias a ello, se garantiza que los elementos con la misma prioridad
se desencolarán en el orden en que se encolaron.
Ejercicio de repaso
Ejercicio: Gestión de Tareas con Prioridades
Contexto:
Eres el jefe de un equipo de desarrollo de software y tienes varias tareas que asignar a tu equipo.
Dado que algunas tareas son más críticas que otras, decides usar una cola de prioridad para
gestionarlas y asegurarte de que se completen en el orden adecuado.
Instrucciones:
[Link]
© Universidad Alfonso X el Sabio 99
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
1. Si se descubre un error de seguridad crítico mientras las tareas están siendo procesadas,
¿cómo garantizas que este error se corrija antes que las demás tareas?
2. ¿Cómo manejarías la situación si dos tareas tienen la misma prioridad?
3. Imagina que la cola de prioridad es utilizada en un entorno donde las tareas y sus
prioridades cambian constantemente. ¿Qué desafíos podrías enfrentar y cómo podrías
adaptar tu implementación para manejarlos?
Salida esperada:
[Link]
© Universidad Alfonso X el Sabio 100
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
Los conjuntos (o "sets" en inglés) son colecciones no ordenadas de elementos únicos. Es decir,
en un conjunto, cada elemento aparece una sola vez. Los conjuntos no tienen un orden
específico y no se pueden acceder por índices.
En la mayoría de los lenguajes de programación, incluido Python, existen estructuras de datos
que implementan la idea matemática de un conjunto.
Operaciones básicas
Los conjuntos, como estructuras de datos, tienen varias operaciones básicas que reflejan las
operaciones matemáticas tradicionales de los conjuntos, además de algunas operaciones
específicas relacionadas con su implementación en programación. A continuación, vemos las
operaciones más comunes:
Diferencia: Crea un nuevo conjunto que contiene todos los elementos del
primer conjunto que no están presentes en el segundo conjunto.
[Link]
© Universidad Alfonso X el Sabio 101
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
[Link]
© Universidad Alfonso X el Sabio 102
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
En general, los conjuntos son útiles en cualquier situación en la que sea importante garantizar
la unicidad de los elementos y cuando se realizan operaciones de conjunto tradicionales de
manera regular. La eficiencia de las operaciones de conjunto en ciertas implementaciones
también hace que sean preferibles a las listas en ciertos escenarios.
Ventajas y desventajas
Ventajas Desventajas
Implementación
[Link]
© Universidad Alfonso X el Sabio 103
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
En Python, los conjuntos se pueden crear utilizando llaves {} (similar a los diccionarios, pero sin
claves y valores) o usando la función set().
mi_conjunto = {1, 2, 3, 4, 5}
otro_conjunto = set([1, 2, 2, 3, 4]) # Esto crea un conjunto con elementos {1, 2, 3,
4}
Las operaciones comunes con conjuntos incluyen la adición y eliminación de elementos, y las
operaciones matemáticas de conjuntos como mencionado anteriormente.
Aunque Python ya dispone de esta estructura de datos es interesante que veamos cuál sería una
posible implementación de este tipo de estructura:
class CustomSet:
'''
Clase CustomSet para implementar una estructura de conjunto personalizada.
Atributos:
elements (dict): Diccionario para almacenar los elementos del conjunto.
Los valores son irrelevantes, se utiliza principalmente la
clave.
'''
def __init__(self):
'''
Inicializa un conjunto vacío.
'''
[Link] = {}
[Link]
© Universidad Alfonso X el Sabio 104
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
def size(self):
'''
Devuelve el tamaño del conjunto.
[Link]
© Universidad Alfonso X el Sabio 105
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
def __str__(self):
'''
Representación en cadena del conjunto.
[Link]
© Universidad Alfonso X el Sabio 106
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
implementación básica, pero espero que esta implementación nos da una idea clara de cómo se
puede construir desde cero.
Ejercicio de repaso
Ejercicio: Administración de un Club de Lectura
Eres el encargado de administrar un club de lectura en tu localidad. Para ello, decides utilizar
conjuntos o sets para llevar un control sobre los libros que cada miembro ha leído.
Escenario:
1. Cada miembro del club de lectura tiene una lista de libros que ha leído.
2. Cada libro tiene un título único.
3. Los miembros suelen intercambiar recomendaciones entre ellos sobre qué libros leer a
continuación.
Tareas a realizar:
1. Crear conjuntos para al menos 3 miembros diferentes, con los libros que han leído.
2. Encuentra qué libros han leído todos los miembros (intersección de todos los
conjuntos).
3. Encuentra qué libros ha leído el primer miembro pero no los otros (diferencia).
4. Recomienda a un miembro los libros que otros dos miembros han leído pero él no
(diferencia y unión).
5. Añade un nuevo libro leído a la lista de uno de los miembros.
6. Elimina un libro leído de la lista de uno de los miembros.
7. Encuentra y muestra qué libros son únicos para cada miembro.
Datos de Muestra:
• Jose ha leído: "Orgullo y prejuicio", "1984", "Un mundo feliz", "Cien años de soledad".
• María ha leído: "Orgullo y prejuicio", "Moby Dick", "La Odisea", "Cien años de soledad",
"Los juegos del hambre".
• Miriam ha leído: "1984", "Un mundo feliz", "La Odisea", "Los juegos del hambre".
Usa los métodos del set para llevar a cabo las tareas solicitadas y muestra los resultados.
Este ejercicio te ayudará a practicar las operaciones básicas de conjuntos como unión,
intersección y diferencia en un escenario práctico y real.
[Link]
© Universidad Alfonso X el Sabio 107
Unidad 1: Estructuras dinámicas de datos
Algoritmos y Estructuras de Datos
4. Bibliografía
• Aho, A. V., Hopcroft, J. E., & Ullman, J. D. (1983). Data Structures and Algorithms.
Addison-Wesley.
• Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2009). Introduction to Algorithms
(3ra ed.). MIT Press.
• Dasgupta, S., Papadimitriou, C. H., & Vazirani, U. V. (2006). Algorithms. McGraw-Hill
Higher Education.
• Goodrich, M. T., & Tamassia, R. (2014). Data Structures and Algorithms in Java (6ta ed.).
Wiley.
• Goodrich, M. T., Tamassia, R., & Goldwasser, M. H. (2013). Data Structures and
Algorithms in Python. Wiley.
• Kleinberg, J., & Tardos, É. (2005). Algorithm Design. Pearson Education.
• Knuth, D. E. (1968-2011). The Art of Computer Programming (Vols. 1-4). Addison-
Wesley.
• Mehlhorn, K., & Sanders, P. (2008). Algorithms and Data Structures: The Basic Toolbox.
Springer.
• Nadal Farré, M. (2022). Estructuras de Datos y Algoritmos. [Título del libro o editorial no
especificado].
• Python Software Foundation. (2023). Python 3 Documentation. Recuperado de
[Link]
• Sedgewick, R. (1998). Algorithms in C++, Parts 1-4: Fundamentals, Data Structures,
Sorting, Searching (3ra ed.). Addison-Wesley Professional.
• Sedgewick, R., & Wayne, K. (2011). Algorithms (4ta ed.). Addison-Wesley Professional.
• Skiena, S. S. (2008). The Algorithm Design Manual (2da ed.). Springer.
• Wirth, N. (1976). Algorithms + Data Structures = Programs. Prentice-Hall.
[Link]
© Universidad Alfonso X el Sabio 108
GRACIAS
[Link]