Академический Документы
Профессиональный Документы
Культура Документы
PROYECTO
Primera
entrega:
1.
Definición
de
objetivos
y
resultados
2.
Creatividad
y
originalidad
3.
Completitud
de
la
información
suministrada
sobre
el
proyecto
4.
Descripción
de
los
requerimientos
funcionales
Segunda
entrega:
1.
Calidad
de
las
soluciones
planteadas.
CRITERIOS
DE
Tercera
entrega:
EVALUACIÓN
1.
Cumplimiento
de
las
metas
definidas
2.
Cubrimiento
de
los
requerimientos
funcionales
3.
Estabilidad
del
producto
entregado
4.
Documentación
de
la
etapa
de
implementación
5.
Documentación
de
la
etapa
de
pruebas
6.
Sustentación
1 [
POLITÉCNICO
GRANCOLOMBIANO
]
El
objetivo
de
este
proyecto
es
llevar
a
la
práctica
los
conceptos
de
cada
una
de
las
etapas
del
desarrollo
de
software:
levantamiento
de
requerimientos,
Información
General
del
análisis
del
problema,
implementación
de
la
solución
y
pruebas
del
producto.
proyecto
En
cada
etapa
se
implementarán
estructuras
de
datos
estudiada
en
el
curso,
previo
análisis
de
su
aplicabilidad
en
el
contexto
del
proyecto.
1. Levantamiento
de
requerimientos:
en
esta
etapa
se
define
el
enunciado
del
proyecto
que
se
va
a
desarrollar,
que
debe
atacar
un
problema
interesante
de
la
vida
real
donde
se
puedan
aplicar
los
conceptos
del
módulo.
Es
necesario
responder
las
siguientes
preguntas:
¿Qué
proyecto
quiero
realizar?
¿Qué
problema
quiero
solucionar?
¿Qué
impacto
tiene
el
problema
en
la
sociedad?
¿De
acuerdo
a
la
motivación
al
módulo,
cómo
puedo
aplicar
los
conceptos
que
voy
a
aprender?
¿Qué
funcionalidades
debe
ofrecer
el
producto
deseado?
2.
Análisis
del
problema:
en
esta
etapa
se
analiza
completamente
el
problema
definido
en
el
levantamiento
de
requerimientos.
Para
cada
funcionalidad
a
satisfacer,
se
debe:
Enumerar
las
variables
de
entrada
y
enunciar
la
precondición.
Enumerar
las
variables
de
salida
y
enunciar
la
poscondición.
Resultados
del
Describir
detalladamente
una
estrategia
de
solución.
Desempeño
3.
Implementación
de
la
solución:
en
este
paso
se
debe
programar
en
Java
una
herramienta
que
satisfaga
los
requerimientos
definidos,
aplicando
las
distintas
estructuras
de
datos
estudiadas
a
lo
largo
del
módulo.
Se
debe
usar
Eclipse
como
ambiente
de
desarrollo
y
la
interacción
con
el
usuario
debe
ser
a
través
de
una
interfaz
gráfica.
4.
Pruebas
del
producto:
la
etapa
final
consiste
en
realizar
y
documentar
pruebas
sobre
el
sistema
desarrollado,
que
garanticen
la
calidad
del
producto
entregado.
Es
deseable
que
se
utilice
un
framework
como
JUnit
para
la
elaboración
de
las
pruebas.
5.
Entrega
final:
en
esta
fase
se
verificarán
los
logros
obtenidos
de
acuerdo
a
lo
planeado
al
inicio
del
proyecto.
La
entrega
debe
incluir
el
código
fuente
del
producto,
los
ejecutables
del
proyecto
y
la
documentación
relacionada
con
las
etapas
de
implementación
y
de
pruebas.
[ ESTRUCTURA DE DATOS ] 2
Este
proyecto
debe
realizarse
en
grupos
de
cuatro
a
cinco
estudiantes.
El
tutor
deberá
apoyar
la
selección
del
tema
sobre
el
que
se
va
a
desarrollar
el
proyecto
y
las
metas
definidas
por
los
estudiantes,
a
través
del
foro
y
de
la
mensajería.
Es
necesario
que
el
tutor
ayude
a
decidir
el
tema
del
proyecto,
para
garantizar
que
los
temas
del
módulo
tengan
aplicabilidad
y
que
su
desarrollo
sea
viable
en
ocho
semanas.
En
esta
primera
etapa
se
busca
que
el
grupo
defina
los
objetivos
y
metas
de
su
proyecto,
establezca
los
requerimientos
funcionales
del
producto
y
analice
el
problema
enunciando
para
cada
funcionalidad
sus
precondiciones,
sus
poscondiciones
y
una
estrategia
de
solución.
Cada
grupo
debe
definir
y
documentar(este
documento
es
el
entregable)
la
siguiente
información:
1.
Integrantes:
Nombres
y
apellidos
de
los
miembros
del
equipo
(entre
cuatro
y
cinco
personas).
2.
Nombre
del
proyecto.
3.
Objetivos:
¿Qué
se
quiere
lograr
con
el
desarrollo
del
proyecto?
¿Es
viable
la
implementación
del
proyecto
en
las
semanas
que
dura
el
módulo?
4.
Resultados
esperados:
¿Qué
se
tendrá
como
resultado
del
proyecto?
5.
Descripción:
relato
en
lenguaje
natural
de
lo
que
debería
hacer
el
software
que
se
construya.
En
la
descripción,
es
importante
tener
en
cuenta
como
punto
obligatorio,
que
el
software
desarrollado
interactuará
con
el
usuario
a
través
de
una
interfaz
gráfica.
Este
tema
deberá
ser
investigado
por
cuenta
de
cada
grupo.
6.
Aplicabilidad
de
los
temas
del
módulo:
¿Cómo
se
piensa
vincular
el
contenido
del
módulo
con
el
desarrollo
del
proyecto?
7.
Requerimientos
funcionales:
lista
de
servicios
que
ofrecerá
al
usuario
el
producto
final.
En
esta
etapa
se
busca
que
el
grupo
implemente
herramientas
útiles
para
la
documentación
y
construcción
de
un
proyecto.
Es
necesario
que
la
documentación
sea
lo
más
detallada
posible
para
que
la
construcción
del
proyecto
tenga
la
menor
cantidad
posible
de
imprevistos.
Cada
grupo
debe
definir
y
documentar
(este
documento
es
el
entregable)
la
siguiente
información:
3 [
POLITÉCNICO
GRANCOLOMBIANO
]
1.
Casos
de
uso:
para
cada
requerimiento
funcional
del
software
a
construir,
brindar
información
detallada
sobre
éste,
mediante
el
diligenciamiento
de
un
caso
de
uso
con
el
siguiente
formato
básico:
Identificador:
un
código
que
identifica
el
caso
de
uso
(debe
ser
de
la
forma
“CU-‐XXX”,
donde
XXX
es
un
número
consecutivo).
Nombre:
un
nombre
que
describe
el
caso
de
uso
(por
ejemplo:
“Matricular
estudiante”).
Descripción:
un
párrafo
que
describa
claramente
qué
servicios
y
funcionalidades
se
le
ofrece
al
usuario
(por
ejemplo:
“Dada
la
información
básica
del
estudiante,
matricularlo
en
la
Institución,
y
asignarle
un
número
de
carné,
una
cuenta
de
usuario
y
una
contraseña.
Actores:
usuarios
que
intervienen
en
el
caso
de
uso
(por
ejemplo:
“El
estudiante
y
el
asesor
de
matrículas
de
la
Institución”).
Entradas:
datos
de
entrada
del
proceso
(por
ejemplo:
“Nombres
y
apellidos
del
estudiante,
puntaje
del
examen
de
estado
del
estudiante,
dirección
de
residencia
del
estudiante,
etc.”).
Salidas:
datos
de
salida
del
proceso
(por
ejemplo:
“Número
de
carné
del
estudiante,
cuenta
de
usuario
del
estudiante,
y
contraseña.”).
Precondiciones:
supuestos
que
deben
cumplir
los
datos
de
entrada
antes
de
usar
la
funcionalidad.
Post-‐condiciones:
condiciones
que
cumplen
los
datos
de
salida
después
de
ejecutarse
la
funcionalidad.
Casos
de
excepción:
condiciones
que
de
cumplirse,
deberían
desplegar
un
mensaje
de
error
al
usuario.
2.
Pseudocódigos:
para
cada
requerimiento
funcional,
escribir
un
pseudoalgoritmo
que
describa
una
estrategia
de
solución
para
proveer
la
funcionalidad,
haciendo
énfasis
(en
lo
posible)
en
el
uso
de
las
estructuras
de
datos.
En
esta
última
etapa
se
busca
que
el
grupo
realice
la
entrega
final
del
producto,
incluyendo
el
código
fuente,
los
ejecutables
del
proyecto
y
la
documentación
relacionada
con
las
etapas
de
implementación
y
de
pruebas.
Tanto
los
estudiantes
como
el
tutor
deben
verificar
los
logros
obtenidos
por
el
grupo
de
acuerdo
a
lo
planeado
al
inicio
del
proyecto.
Las
conclusiones
corresponden
a
puntos
identificados
por
el
grupo
como
puntos
de
encuentro
o
divergencia
entre
los
conceptos
y
la
práctica;
y
conocimiento
nuevo.
Cada
grupo
debe
definir
y
entregar
los
productos:
1.
Ejecutables
del
proyecto:
archivo
empaquetado
.jar
con
el
que
se
pueda
ejecutar
el
producto
entregado,
adjuntando
un
archivo
ReadMe.txt
donde
se
explique
brevemente
cómo
se
pone
en
funcionamiento
la
herramienta
tanto
en
Windows
como
en
Linux.
2.
Código
fuente
del
producto:
archivo
empaquetado
.zip
con
el
proyecto
en
Eclipse
que
contiene
la
totalidad
del
código
fuente
y
de
los
archivos
auxiliares.
3.
Documentación
de
la
etapa
de
implementación:
un
pequeño
artículo
donde
se
resuma
la
experiencia
vivida
durante
la
implementación
del
código
fuente.
4.
Documentación
de
la
etapa
de
pruebas:
un
documento
donde
se
describan
las
pruebas
desarrolladas,
los
errores
encontrados
y
los
errores
corregidos.
[ ESTRUCTURA DE DATOS ] 4
SUSTENTACIÓN
En
la
sustentación
del
proyecto,
que
será
presidida
por
el
tutor,
se
verificarán
los
logros
obtenidos
por
el
grupo
y
se
contrastará
la
autoevaluación
con
el
desempeño
de
los
estudiantes
durante
la
sustentación.
El
tutor
debe
indagar
sobre
los
siguientes
puntos:
Nivel
de
aprendizaje:
en
función
de
la
adquisición
de
competencias
argumentativas
con
base
en
los
logros
individuales
y
grupales.
Nivel
de
participación:
¿Qué
tanto
aportó
cada
estudiante
al
proceso
general?
Oportunidades
de
mejoramiento:
¿Qué
creen
los
estudiantes
que
puede
mejorar
en
un
próximo
proyecto,
en
relación
con
los
resultados
y
con
el
proceso
que
llevaron?
Use esta escala de valores para los criterios de evaluación
El
grupo
comprende,
define
y
El
grupo
entrega
un
El
grupo
no
define
con
documenta
claramente
los
documento
con
claridad
cuál
es
la
idea
del
alcances,
objetivos
y
los
definiciones
generales
de
proyecto
ni
cuáles
serán
Definición
de
objetivos
resultados
esperados
de
su
lo
que
quiere
hacer.
los
resultados
esperados.
y
resultados.
proyecto
5 [
POLITÉCNICO
GRANCOLOMBIANO
]
Las
funcionalidades
están
Hay
un
intento
por
Las
funcionalidades
son
claramente
definidas,
y
las
describir
las
ambiguas,
no
están
precondiciones
y
post-‐ funcionalidades
que
debe
claramente
definidas
y
condiciones
están
ofrecer
el
producto,
carecen
de
documentación
enunciados
de
forma
clara,
describiendo
de
forma
clara.
El
grupo
tiene
completa
y
sin
vaga
pero
entendible
las
problemas
proponiendo
Descripción
de
los
ambigüedades.
El
formato
de
precondiciones
y
las
post-‐ precondiciones
y
post-‐
requerimientos
casos
de
uso
fue
diligenciado
condiciones.
El
formato
de
condiciones
que
se
funcionales.
en
su
totalidad
de
forma
casos
de
uso
fue
entiendan
y
los
datos
de
clara
y
precisa
aunque
puede
diligenciado
en
su
entrada
y
de
salida
no
son
haber
detalles
mínimos
que
totalidad,
pero
con
adecuados.
se
pasaron
por
alto.
algunas
imprecisiones
que
no
comprometen
seriamente
la
calidad
de
la
entrega.
[ ESTRUCTURA DE DATOS ] 6
Usando
eclipse
se
programó
Usando
eclipse
se
Se
atacaron
unos
pocos
una
herramienta
que
es
programó
una
requerimientos
Cubrimiento
de
los
capaz
de
dar
soporte
a
la
herramienta
que
es
capaz
funcionales,
se
dejaron
requerimientos
totalidad
de
los
de
dar
soporte
a
la
algunas
funcionalidades
sin
funcionales
requerimientos
definidos,
si
mayoría
de
los
implementar
y
hay
errores
bien
es
posible
que
haya
requerimientos
definidos,
críticos
que
restringen
la
algunos
errores
si
bien
es
posible
que
haya
calidad
del
producto.
algunos
errores.
La
totalidad
o
casi
la
Más
del
85%
de
la
El
85%
o
menos
de
la
Estabilidad
del
totalidad
de
la
funcionalidad
funcionalidad
soportada
funcionalidad
soportada
producto
entregado
soportada
por
el
producto
por
el
producto
está
libre
por
el
producto
está
libre
está
libre
de
errores
de
errores.
de
errores
7 [
POLITÉCNICO
GRANCOLOMBIANO
]
[ ESTRUCTURA DE DATOS ] 8
ESTRUCTURAS DE DATOS
UNIDAD UNO - SEMANA UNO
CONCEPTOS SOBRE PROGRAMACIÓN
ORIENTADA A OBJETOS *
TABLA DE CONTENIDO
1. CLASES 2
2. ATRIBUTOS Y MÉTODOS 3
3. OBJETOS 5
4. ARREGLOS 6
EN RESUMEN 7
PARA TENER EN CUENTA 7
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
1. CLASES
Todos los animales mostrados se reúnen bajo el mismo concepto: perro, y aunque los cuatro
perros son distintos, tienen muchas cosas en común, pues todos son perros.
†
Imágenes tomadas de http://www.morguefile.com/. morgueFile: public image archive for creatives by
creatives, © 2009-2010 MORGUEFILE.
ESTRUCTURAS DE DATOS 2
Observe que las características consideradas dependen de qué nos importa. Así pues, la
abstracción es el proceso que seguimos para escoger las características que nos interesan
sobre los perros con el objetivo de tener una idea en la mente de qué es un perro.
Obviamente los perros tienen muchas más propiedades que pueden no interesarnos en
nuestra abstracción: número de pulgas, color favorito, número de horas promedio que
duerme, secuencia de ADN, etc. Por esto es importante el proceso de abstraer: nos permite
quedarnos con lo que realmente necesitamos.
Entonces, la clase Perro corresponde con la abstracción mental de todos los objetos del
mundo que nosotros identificamos como perros. Para definir la clase Perro en Java se debe
escribir:
2. ATRIBUTOS Y MÉTODOS
Recurso como proyecto en Eclipse: Clases.zip.
// -------------------------
// - Atributos de la clase -
// -------------------------
// ----------------------------------
// - Método constructor de la clase -
// ----------------------------------
ESTRUCTURAS DE DATOS 3
// --------------------------------------
// - Atributos analizadores de la clase -
// --------------------------------------
// ---------------------------------------
// - Atributos modificadores de la clase -
// ---------------------------------------
Es importante resaltar que los atributos representan características y que los métodos
representan comportamiento. Según su función, los métodos se clasifican en:
ESTRUCTURAS DE DATOS 4
3. OBJETOS
Recurso como proyecto en Eclipse: Clases.zip.
Un objeto es una instancia o particularización de una clase. Por ejemplo, cuatro objetos de la
clase Perro son:
‡
Gráfica 4: Cuatro objetos distintos de la clase Perro .
Hay tantos objetos de la clase Perro como perros en este mundo. Para crear en Java una
instancia de una clase hay que invocar un método constructor a través de la palabra clave
new.
Código 5: Ejemplo que ilustra la creación de instancias en Java, que crea cinco perros.
Perro perro1=new Perro("Firulais","Chihuahua","17/02/2007",true);
Perro perro2=new Perro("Paca","Dálmata","31/12/2009",false);
Perro perro3=new Perro("Fito","Pastor Alemán","07/08/2008",true);
Perro perro4=new Perro("Sally","French Poodle","07/08/2008",false);
Perro perro5=new Perro("Fito","French Poodle","28/02/2009",true);
Las propiedades de cada objeto son definidas mediante los valores que toman sus atributos.
Por ejemplo:
El objeto perro1 tiene nombre "Firulais", raza "Chihuahua", fecha de nacimiento
"17/02/2007", y es de género masculino.
El objeto perro2 tiene nombre "Paca", raza "Dálmata", fecha de nacimiento "31/12/2009", y
es de género femenino.
El objeto perro3 tiene nombre "Fito", raza "Pastor Alemán", fecha de nacimiento
"07/08/2008", y es de género masculino.
El objeto perro4 tiene nombre "Sally", raza "French Poodle", fecha de nacimiento
"07/08/2008" y es de género femenino.
‡
Imágenes tomadas de http://www.morguefile.com/. morgueFile: public image archive for creatives by
creatives, © 2009-2010 MORGUEFILE.
ESTRUCTURAS DE DATOS 5
El objeto perro5 tiene nombre "Fito", raza "French Poodle", fecha de nacimiento
"28/02/2009", y es de género masculino.
Aunque todos los objetos de la clase Perro tienen nombre, raza, fecha de nacimiento y
género, dos perros distintos podrían tener distinto nombre, distinta raza, distinta fecha de
nacimiento y distinto género. En particular, perro1 tiene nombre "Firulais", y perro3 tiene un
nombre distinto ("Fito"), pero ambos son perros de género masculino.
4. ARREGLOS
Recurso como proyecto en Eclipse: Clases.zip.
Un arreglo es una secuencia de elementos del mismo tipo, que son almacenados de forma
contigua.
0 1 2 3 4 5 6 7
M A N Z A N A
0 1 2 3 4 5 6
El acceso a los elementos de un arreglo se realiza mediante índices, que van desde 0 para la
primera posición del arreglo hasta el tamaño menos uno para la última posición del arreglo.
Por ejemplo, considere el arreglo M, A, N, Z, A, N, A. El tamaño del arreglo es siete, el elemento
de la posición 0 es una M, el elemento de la posición 1 es una A, el elemento de la posición 2
es una N, …, y el elemento de la posición 6 es una A. Observe que la primera posición del
arreglo es la 0, y que la última posición del arreglo es la 6, que coincide con el tamaño del
arreglo menos uno.
ESTRUCTURAS DE DATOS 6
}
}
EN RESUMEN
Abstracción es el proceso mediante el cual se representa en el mundo de las ideas un conjunto de
objetos, seleccionando sólo aquellas características que interesan, excluyendo toda propiedad que no
importe.
Una clase es la abstracción de un conjunto de objetos reunidos bajo el mismo concepto. Una clase se
define mediante 1. un nombre que la identifica, 2. atributos que describen las características de los
objetos de la clase, y 3. métodos que describen el comportamiento de los objetos de la clase.
Según su función, los métodos se clasifican en:
Métodos constructores: son los responsables de crear nuevas instancias de la clase.
Métodos analizadores: son los responsables de permitir consultas sobre los atributos de la clase.
Métodos modificadores: son los responsables de permitir modificaciones sobre los atributos de la
clase.
Un objeto es una instancia o particularización de una clase.
Un arreglo es una secuencia de elementos del mismo tipo, que son almacenados de forma contigua.
ESTRUCTURAS DE DATOS 7
ESTRUCTURAS DE DATOS
UNIDAD UNO - SEMANA UNO
EJERCICIOS PROPUESTOS *
Desarrolle una aplicación en Java que, dado un arreglo de números flotantes, imprima en
consola el promedio de los valores del arreglo. Tanto el número de elementos del arreglo
como los elementos del arreglo deben ser pedidos al usuario a través de la consola del
sistema.
2. CONCATENACIÓN DE ARREGLOS
3. REVERSO DE UN ARREGLO
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
ESTRUCTURAS DE DATOS
UNIDAD UNO - SEMANA DOS
RECURSIÓN *
TABLA DE CONTENIDO
1. FUNCIONES RECURSIVAS 2
1.1. FUNCIÓN DE FIBONACCI 3
2. MEMOIZATION 9
2.1. IMPLEMENTACIÓN DE LA FUNCIÓN DE FIBONACCI USANDO MEMOIZATION 9
3. DIVIDIR Y CONQUISTAR (DIVIDIR Y VENCER) 10
3.1. TORRES DE HANOI 10
3.2. BÚSQUEDA BINARIA (BINARY SEARCH) 15
3.3. ALGORITMO DE ORDENAMIENTO POR MEZCLA (MERGE SORT) 18
4. BACKTRACKING 19
EN RESUMEN 20
PARA TENER EN CUENTA 20
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
1. FUNCIONES RECURSIVAS
Una función es recursiva si está definida en términos de sí misma. Claramente debe haber
casos especiales en los que una función recursiva esté definida mediante constantes y
llamados a otras funciones, porque de lo contrario existirían llamados recursivos infinitos a la
función. Por ejemplo, la función
no está bien definida porque la evaluación en cualquier punto implicaría un número infinito
de llamados, lo que se evidencia al intentar calcular la función en algún , por decir algo, en
:
aplicando la definición con .
aplicando la definición con .
aplicando la definición con .
aplicando la definición con .
aplicando la definición con .
...
Ventajas:
• Frecuentemente, al trabajar sobre estructuras de datos definidas recursivamente es más
natural diseñar algoritmos recursivos que iterativos. La forma de las soluciones recursivas
sobre estructuras de datos definidas recursivamente reflejan el carácter recursivo de la
definición.
• A veces es más fácil pensar una solución recursiva que una iterativa.
• Para algunos problemas, se puede diseñar algoritmos recursivos eficientes más sencillos de
escribir que sus contrapartes iterativas.
Desventajas:
• La máquina debe manejar estructuras adicionales para controlar los llamados recursivos, lo
que resulta en mayor tiempo de ejecución y espacio extra adicional.
• Hay un límite para el nivel de anidamiento de los llamados recursivos que, si se sobrepasa,
se lanza una excepción en la máquina (StackOverflow).
ESTRUCTURAS DE DATOS 2
• Si los casos inductivos tienen más de una referencia a la función es posible que se efectúen
reiteradamente llamados a la función con los mismos parámetros, lo que implica gastar
tiempo de ejecución en repetir cálculos innecesarios. Para resolver este problema se puede
aplicar la técnica conocida como Memoization, que consiste en declarar una estructura de
datos adicional que guarde los resultados de los llamados recursivos de tal forma que no se
repitan cálculos innecesarios.
que puede ser evaluada sobre cualquier número natural , simulando los llamados
recursivos:
ESTRUCTURAS DE DATOS 3
return 1; // Retornar 1
}
else { // Si n es mayor o igual que dos
return fibA(n-1)+fibA(n-2); // Retornar fibA(n-1)+fibA(n-2)
}
}
}
Todo parece ir bien, pero … la implementación fib1 tiene los siguientes problemas:
1. El tipo de datos int sólo permite representar números enteros hasta (es decir,
hasta 2147483647), porque utiliza sólo 32 bits para su representación. Por lo tanto, todas las
operaciones que superen este umbral arrojarían resultados erróneos, hecho que se conoce
como desbordamiento del tipo de datos. Por esta razón es que el Fibonacci de 47 da un valor
extraño (-1323752223), que no corresponde con el resultado que debería tener:
1134903170+1836311903=2971215073.
2. Se demora muchísimo para valores de cercanos a .
BigInteger es una clase de Java que nos permite operar con números de precisión arbitraria
†
.
†
La documentación de la clase BigInteger está disponible en el API de Java en el sitio
http://java.sun.com/javase/6/docs/api/
ESTRUCTURAS DE DATOS 4
Tabla 4: Algunos servicios provistos por la clase BigInteger.
Operación Descripción
BigInteger.valueOf(x) Convierte un número x de tipo long a un número de tipo BigInteger.
new BigInteger(s)
Convierte un número s almacenado como cadena de texto a un número
de tipo BigInteger.
a.add(b)
Retorna un nuevo BigInteger con el resultado de a+b (la suma), donde
a y b son dos números de tipo BigInteger.
a.subtract(b)
Retorna un nuevo BigInteger con el resultado de a-b (la resta), donde
a y b son dos números de tipo BigInteger.
Retorna un nuevo BigInteger con el resultado de a*b (la
a.multiply(b)
multiplicación), donde a y b son dos números de tipo BigInteger.
Retorna un nuevo BigInteger con el resultado de a/b (la división
a.divide(b)
entera), donde a y b son dos números de tipo BigInteger.
Retorna un nuevo BigInteger con el resultado de a%b (el residuo
a.mod(b)
entero), donde a y b son dos números de tipo BigInteger.
Retorna un nuevo BigInteger con el máximo común divisor de a y b,
a.gcd(b)
donde a y b son dos números de tipo BigInteger.
Retorna un nuevo BigInteger con el mínimo valor entre a y b, donde a
a.min(b)
y b son dos números de tipo BigInteger.
Retorna un nuevo BigInteger con el máximo valor entre a y b, donde a
a.max(b)
y b son dos números de tipo BigInteger.
Retorna verdadero si los números a y b son iguales, y retorna falso si
a.equals(b)
son distintos, donde a y b son dos números de tipo BigInteger.
Contando con BigInteger ahora sí somos capaces de hacer operaciones con números
grandes, como
761809243486409043837*2046696616531860150-4525695587262334931605*324298040889334
escribiendo un programa muy sencillo
ESTRUCTURAS DE DATOS 5
Corrigiendo el defecto de desbordamiento obtenemos una nueva versión de la
implementación de Fibonacci, que da solución a nuestro primer inconveniente.
Para saber qué tan demorada es la versión fibB codificaremos un programa capaz de
exportar una tabla csv que muestre cuántos milisegundos tarda la función fibB calculando
cada uno de los Fibonacci’s desde hasta .
ESTRUCTURAS DE DATOS 6
if (n==0) { // Si n es cero
return new BigInteger("0"); // Retornar 0
}
else if (n==1) { // Si n es uno
return new BigInteger("1"); // Retornar 1
}
else { // Si n es mayor o igual que dos
return fibB(n-1).add(fibB(n-2)); // Retornar fibB(n-1)+fibB(n-2)
}
}
}
Observe que la función que describe el consumo de tiempo de la función fibB versus es
una función exponencial. Más precisamente, se puede demostrar que la forma de esta
función es una constante multiplicada por , donde es una constante conocida en
el mundo matemático como phi o número de oro, y que es aproximadamente igual a 1.61.
¿Qué significa esa rara que se colocó? En términos informales, se dice que un
algoritmo es (lo que se lee textualmente de ) si el tiempo que se demora el
algoritmo para resolver un problema de tamaño está por debajo de un múltiplo constante
de la función . Esta notación se conoce en español como la notación de la gran , y en
inglés como Big-Oh notation.
ESTRUCTURAS DE DATOS 7
}
return a; // Retorne el valor de la variable 'a'.
}
El fragmento de código
BigInteger c=a.add(b); // La variable auxiliar c guarda el valor de a+b.
a=b; // A la variable 'a' asígnele el valor de la variable 'b'.
b=c; // A la variable 'b' asígnele el valor de la variable 'c'.
hace una suma y tres asignaciones. Por lo tanto su complejidad temporal es , porque
ejecuta un número constante de operaciones. En general, todo programa que ejecute un
número constante de operaciones tiene complejidad temporal .
La inicialización
BigInteger a=new BigInteger("0"),b=new BigInteger("1");
crea dos números y hace dos asignaciones. Entonces, también tiene complejidad .
El retorno
return a; // Retorne el valor de la variable 'a'.
simplemente entrega el valor de la variable a como resultado. Su complejidad también es
.
Y finalmente, el ciclo
for (int i=0; i<n; i++) { // Ejecutar exactamente n veces el siguiente proceso:
BigInteger c=a.add(b); // La variable auxiliar c guarda el valor de a+b.
a=b; // A la variable 'a' asígnele el valor de la variable 'b'.
b=c; // A la variable 'b' asígnele el valor de la variable 'c'.
}
hace exactamente iteraciones, donde en cada una de éstas se efectúa un número
constante de operaciones. Se concluye pues que la complejidad temporal del ciclo es ,
porque ejecuta un número de operaciones que siempre está por debajo de una constante
multiplicada por .
Formúlese la siguiente pregunta: ¿Qué es mejor: la función fibB con complejidad temporal
) donde , o la función fibC con complejidad temporal ?
Obviamente es mejor la función fibC porque su consumo de tiempo es menor, dado que la
función lineal es más pequeña que la función exponencial cuando
es grande.
ESTRUCTURAS DE DATOS 8
¿Recuerda que el método fibB se demoró calculando 18.3 minutos el Fibonacci de ?
Para que note la diferencia, ¡la versión fibC se demoró menos de un milisegundo entregando
el mismo resultado!
2. MEMOIZATION
Gráfica 10: Evidencia que muestra que el método fibB repite cálculos.
fib(4) Aquí se repitió el
cálculo de fib(2)
fib(3) fib(2)
fib(1) fib(0)
Para resolver este problema se puede aplicar una técnica conocida como Memoization, que
consiste en crear una estructura de datos adicional cuyo propósito sea guardar los resultados
de los llamados recursivos de tal forma que no se repitan cálculos innecesarios.
Con la ayuda de una tabla es posible memorizar los valores retornados por la función de
Fibonacci.
ESTRUCTURAS DE DATOS 9
}
// De lo contrario, si tabla[n]==null es porque el fibonacci de n aún no ha
// sido calculado.
else {
if (n==0) { // Si n es cero.
BigInteger res=BigInteger.valueOf(0); // Calcular el fibonacci de 0, que es 0.
tabla[n]=res; // Guardar en la tabla el fibonacci de 0, para memorizarlo.
return res; // Retornar el resultado.
}
else if (n==1) { // Si n es uno.
BigInteger res=BigInteger.valueOf(1); // Calcular el fibonacci de 1, que es 1.
tabla[n]=res; // Guardar en la tabla el fibonacci de 1, para memorizarlo.
return res; // Retornar el resultado.
}
else { // Si n es mayor o igual a dos.
BigInteger res=fibD(n-1).add(fibD(n-2)); // Calcular el fibonacci de n.
tabla[n]=res; // Guardar en la tabla el fibonacci de n, para memorizarlo.
return res; // Retornar el resultado.
}
}
}
El juego de las torres de Hanoi está conformado por tres columnas verticales y un conjunto
de discos de diámetros distintos que tienen un orificio en el centro que coincide con el
grosor de las columnas.
ESTRUCTURAS DE DATOS 10
Gráfica 12: Insumos para el juego con : cinco discos de diferente diámetro y tres columnas.
Por simplicidad, las columnas se etiquetan con las letras A, B y C, donde la columna A es la
columna inicial, la columna B es la columna intermedia, y la columna C es la columna final. Al
principio, todos los discos se encuentran apilados en la primera columna (la columna A),
ordenados por diámetro, comenzando con el de mayor diámetro y terminando con el de
menor diámetro.
El objetivo del juego consiste en trasladar todos los discos de la columna inicial (la A) hacia la
columna final (la C) mediante una serie de movimientos que deben seguir tres reglas:
Regla 1: sólo se puede mover un disco a la vez.
Regla 2: no se puede colocar un disco encima de un disco de diámetro menor.
Regla 3: no se puede trasladar un disco que tenga otros discos encima suyo.
ESTRUCTURAS DE DATOS 11
Quiero mover discos de la columna A a la columna C usando la columna B como auxiliar.
ESTRUCTURAS DE DATOS 12
Pseudocódigo de la solución:
Algoritmo solucionarHanoi(A,B,C,n)
// A es la columna inicial, B es la columna intermedia, C es la columna final
// n es el número de discos a mover
si n>0 entonces:
// Trasladar recursivamente n-1 discos de A a B usando como intermedia la C
solucionarHanoi(A,C,B,n-1)
// Trasladar un disco de A a C
trasladarDisco(A,C)
// Trasladar recursivamente n-1 discos de B a C usando como intermedia la A
solucionarHanoi(B,A,C,n-1)
fin si
Código 14: Traducción a Java del pseudocódigo, imprimiendo los movimientos a desarrollar en la consola del sistema.
public class HanoiConsola {
public static void main(String[] args) {
solucionar('A','B','C',5); // Llamado inicial con 5 discos
}
public static void solucionar(char A, char B, char C, int n) {
if (n>0) {
solucionar(A,C,B,n-1);
System.out.println("Movimiento "+A+"->"+C);
solucionar(B,A,C,n-1);
}
}
}
Sea el número exacto de movimientos que hace el algoritmo para solucionar un juego
de torres de Hanoi con discos. Por ejemplo, sería el número de movimientos para
trasladar discos, sería el número de movimientos para trasladar discos,
sería el número de movimientos para trasladar discos y sería el número de
movimientos para trasladar discos.
ESTRUCTURAS DE DATOS 13
A una ecuación como la anterior se le llama relación de recurrencia. ¿Crecerá más que la
función ? ¿Crecerá menos que la función ?. Para poder comparar
cómodamente contra otras funciones, es necesario encontrar una fórmula cerrada que
la describa, es decir, una fórmula que no tenga llamados recursivos. Para tal efecto,
seguiremos una receta útil que consta de dos pasos:
Paso 1: suponga que es grande y aplique la función varias veces hasta que encuentre
un patrón.
porque
porque
distribuyendo y simplificando
porque
distribuyendo y simplificando
porque
distribuyendo y simplificando
porque
distribuyendo y simplificando
...
generalizando la fórmula donde es
el número de paso
Hemos demostrado que el número exacto de movimientos que realiza el algoritmo para
solucionar un juego de torres de Hanoi con discos es exactamente . Por tanto, para
discos, el programa debe hacer movimientos ( jugadas). Se concluye pues que la
complejidad temporal del algoritmo es .
El proyecto en Eclipse Hanoi.zip provee una animación gráfica del algoritmo que soluciona el
juego de Hanoi.
ESTRUCTURAS DE DATOS 14
3.2. BÚSQUEDA BINARIA (BINARY SEARCH)
Recurso como proyecto en Eclipse: BusquedaBinaria.zip.
El proceso de búsqueda tradicional, llamado Búsqueda Lineal, consiste en pasar posición por
posición de un arreglo para buscar el valor requerido
El método
public static int busquedaLineal(long[] pArreglo, int pInf, int pSup, long pValor)
entrega como respuesta la posición donde se encuentra el valor pValor dentro del arreglo
pArreglo, considerando posiciones del arreglo desde pInf (inclusive) hasta pSup (inclusive).
En caso de que el valor aparezca varias veces en el arreglo, se entrega la menor posición
donde se encuentre; y en caso de que el valor no aparezca, se retorna -1. La complejidad del
algoritmo es , donde es la cantidad de elementos en la porción de arreglo a
inspeccionar, porque en el peor de los casos se deberán procesar todas las posiciones del
arreglo en cuestión.
¿Se podría implementar un algoritmo más eficiente si sabemos que el arreglo está ordenado?
Imagine un diccionario de trescientas páginas que tiene definiciones de diez mil palabras del
idioma español. Si el diccionario estuviera desordenado, no nos queda otro camino que
leerlo todo para determinar si una palabra está o no está. Pero como todos sabemos que los
términos del diccionario están ordenados alfabéticamente, podemos buscar una palabra más
rápidamente mediante una destreza que aprendimos desde la infancia: buscar en un
diccionario o en un directorio telefónico. Suponga que estamos buscando la palabra Faro y
que abrimos el diccionario justo en la mitad, encontrando la palabra Jarro. Sólo con esto
sabemos que Faro está en la primera mitad del diccionario y no en la segunda, porque Faro
está antes que Jarro. Luego, abrimos el diccionario en medio de la primera mitad y
encontramos la palabra Doncella. Sabemos entonces que Faro está después de Doncella y
antes que Jarro, lo que nos deja con un cuarto del total del diccionario. Siguiendo este
proceso de manera sucesiva llegamos a la página donde debe estar Faro, y dentro de la
página hacemos lo mismo para encontrar el lugar preciso donde aparece el término. El
proceso llevado a cabo se conoce en computación como Búsqueda Binaria, y como pudo
notarlo, es mucho más eficiente que la Búsqueda Lineal, ¿pero qué tanto?
ESTRUCTURAS DE DATOS 15
Gráfica 16: Diagrama de flujo para el algoritmo de Búsqueda Binaria.
ESTRUCTURAS DE DATOS 16
// Buscar el valor en la mitad de la izquierda:
return busquedaBinaria(pArreglo,pInf,mitad-1,pValor);
}
// Si el valor buscado es mayor que lo que está en la mitad:
else {
// Buscar el valor en la mitad de la derecha:
return busquedaBinaria(pArreglo,mitad+1,pSup,pValor);
}
}
}
Paso 1: suponga que es grande y aplique la función varias veces hasta que encuentre
un patrón.
porque
porque
simplificando
porque
simplificando
porque
simplificando
porque
simplificando
...
generalizando la fórmula donde
es el número de paso
ESTRUCTURAS DE DATOS 17
Entonces, la complejidad temporal del algoritmo de Búsqueda Binaria es , lo que
demuestra que es más eficiente que el algoritmo de Búsqueda Lineal, cuya complejidad
temporal es .
Para ordenar arreglos se cuenta con un proceso muy eficiente llamado algoritmo de
Ordenamiento por Mezcla (Merge Sort en inglés).
ESTRUCTURAS DE DATOS 18
Paso 1: suponga que es grande y aplique la función varias veces hasta que encuentre
un patrón.
porque
porque
distribuyendo y simplificando
porque
distribuyendo y simplificando
porque
distribuyendo y simplificando
porque
distribuyendo y simplificando
...
generalizando la fórmula donde
es el número de paso
4. BACKTRACKING
ESTRUCTURAS DE DATOS 19
construir todas las posibilidades que resultan de una combinación parcialmente construida si
se sabe que generará posibilidades que violan las restricciones del problema.
EN RESUMEN
Una función es recursiva si está definida en términos de sí misma.
La definición de una función recursiva debe estar formada por:
Casos base: son casos triviales en los que la definición de la función no depende de la función
misma.
Casos recursivos: son casos complejos en los que la definición de la función referencia a la función
misma.
Una función es cerrada si no está definida en términos de sí misma.
BigInteger es una clase de Java que nos permite operar con números de precisión arbitraria.
La complejidad temporal es una medida de qué tanto tiempo consume un algoritmo en su ejecución.
La complejidad temporal sirve para medir la eficiencia de un programa.
Todo programa que ejecute un número constante de operaciones tiene complejidad temporal .
Todo programa que ejecute un número de operaciones que siempre esté por debajo de una
constante multiplicada por , tiene complejidad temporal .
La técnica Memoization consiste en crear una estructura de datos adicional cuyo propósito sea
guardar los resultados de los llamados recursivos de tal forma que no se repitan cálculos innecesarios.
Dividir y Conquistar, también conocida como Dividir y Vencer, es una técnica que consiste en dividir
un problema en subproblemas similares más pequeños, solucionar tales subproblemas y unir estas
soluciones para resolver el problema original.
El algoritmo de Búsqueda Binaria es un proceso eficiente para buscar valores en arreglos ordenados.
El Backtracking es una técnica de búsqueda por fuerza bruta que consiste en iterar sobre todas las
posibilidades hasta que se encuentre una solución adecuada al problema, descartando en masa
conjuntos de posibilidades sin haberlas construido explícitamente, que se sabe que no van a llegar a
la solución.
ESTRUCTURAS DE DATOS 20
ESTRUCTURAS DE DATOS
UNIDAD UNO - SEMANA DOS
ANÁLISIS DE ALGORITMOS *
TABLA DE CONTENIDO
1. MOTIVACIÓN 2
2. COMPLEJIDAD TEMPORAL Y COMPLEJIDAD ESPACIAL 2
3. NOTACIÓN O 3
4. ANÁLISIS DE COMPLEJIDAD DE PROGRAMAS ITERATIVOS 3
5. ANÁLISIS DE COMPLEJIDAD DE PROGRAMAS RECURSIVOS 5
6. TIPOS DE ANÁLISIS DE COMPLEJIDAD 6
EN RESUMEN 6
PARA TENER EN CUENTA 6
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
1. MOTIVACIÓN
Hay dos mecanismos para medir complejidad: el teórico y el práctico. El mecanismo práctico
es completamente inviable, puesto que hay computadores más rápidos que otros y el tiempo
de ejecución depende de factores como el sistema operativo de la máquina, el lenguaje de
programación, el compilador utilizado, los procesos que se estén corriendo en el computador
en el momento de la prueba (como juegos y antivirus), etc.
ESTRUCTURAS DE DATOS 2
3. Encuentre una función que relacione el tamaño del problema con el número de
operaciones básicas ejecutadas, analizando el peor escenario posible. Esta función refleja la
complejidad temporal, porque entre más operaciones se ejecuten, más tiempo se demoraría
el algoritmo en ejecutar.
3. NOTACIÓN O
ESTRUCTURAS DE DATOS 3
instrucción m;
halle la complejidad de cada instrucción por separado y quédese con la más grande de éstas.
Para calcular la complejidad temporal de un ciclo while o for en Java multiplique el número
de iteraciones que hace el ciclo por la complejidad del cuerpo del ciclo.
Tabla 1: Ejemplos que buscan enseñar a calcular complejidades temporales de forma intuitiva.
Programa Complejidad temporal
double x=7,y=x+3,z=0;
if (x>=6) {
z=x+y;
} porque hace un número constante de operaciones.
else {
z=x-y;
}
int f01(int n) {
int r=0,i=0; porque el ciclo realiza iteraciones y la complejidad
while (i<n) {
r+=i; del cuerpo
i++; r+=i;
} i++;
return r; es .
}
int f02(int n) {
porque el ciclo realiza iteraciones y la complejidad
int r=0; del cuerpo
for (int i=0; i<n; i++) { r+=i;
r+=i; i++;
} es .
return r;
Nótese que este for es traducción del while del ejemplo
}
anterior.
int f03(int n, int m) {
int r=0;
for (int i=0; i<n; i++) {
for (int j=0; j<m; j++) {
porque los dos ciclos están anidados, el primer
r+=i*j;
} ciclo hace iteraciones, y el segundo hace iteraciones.
}
return r;
}
ESTRUCTURAS DE DATOS 4
int f04(int n) { porque los dos ciclos están anidados, el primer ciclo
int r=0;
for (int i=0; i<n; i++) {
hace iteraciones, y el segundo nunca hace más de
for (int j=i; j<n; j++) { iteraciones. Observe que el número de iteraciones del
r+=i*j; segundo ciclo depende del valor de i porque j varía desde
} i hasta n-1. ¡No se complique!: como estamos analizando
}
return r; el peor caso, nos contentamos con decir que el segundo
} ciclo nunca hace más de iteraciones, lo que es cierto.
int f05(int n) {
int r=0;
for (int i=0; i<n; i++) {
r+=i; porque un ciclo está después del otro, el primer ciclo
} hace iteraciones, y el segundo hace también
for (int j=0; j<n; j++) { iteraciones. Además sabemos que la función es
r+=j;
} porque es un múltiplo constante de .
return r;
}
int f06(int n, int m) {
int r=0;
for (int i=0; i<n; i++) {
r+=i*i+3;
} porque un ciclo está después del otro, el primer
for (int j=0; j<m; j++) { ciclo hace iteraciones, y el segundo hace iteraciones.
r+=j*5-2;
}
return r;
}
int f07(int n) {
int r=0;
if (n>=15) {
for (int i=1; i<n; i++) {
for (int j=i; j<n; j++) {
r+=i-j;
}
}
} . ¿Por qué? Piense en el peor caso.
else {
for (int i=1; i<n; i++) {
r+=i;
}
}
return r;
}
ESTRUCTURAS DE DATOS 5
6. TIPOS DE ANÁLISIS DE COMPLEJIDAD
EN RESUMEN
Dado cierto recurso, la complejidad de un algoritmo es la cantidad de recurso que el programa
demanda para su ejecución. La demanda de procesador se mide con la complejidad temporal,
mientras que la demanda de memoria RAM se mide con la complejidad espacial.
Calcular la complejidad de nuestros programas nos sirve para:
Medir los recursos que gasta un algoritmo.
Estimar la demanda de recursos que necesita un programa.
Comparar programas para saber cuál es más eficiente en tiempo o en espacio utilizado.
La complejidad temporal mide el tiempo gastado y la complejidad espacial mide el espacio requerido.
Para medir complejidades se utiliza la notación , que se centra en un análisis en el peor caso.
Todo programa que ejecute un número de operaciones que siempre esté por debajo de una
constante multiplicada por (para valores grandes de ), tiene complejidad temporal .
ESTRUCTURAS DE DATOS 6
ESTRUCTURAS DE DATOS
UNIDAD UNO - SEMANA DOS
EJERCICIOS PROPUESTOS *
1. PAVIMENTACIÓN
1.1. Halle una función recursiva que entregue como resultado el número de maneras de
pavimentar un camino de dimensiones utilizando baldosas de tamaño que se
pueden ubicar horizontal o verticalmente. Por ejemplo, para hay ocho maneras de
pavimentar un camino de dimensiones utilizando baldosas de tamaño , para
hay cinco maneras, para hay tres maneras, para hay dos maneras, para
hay una manera, y para hay una manera (no colocar ninguna baldosa).
Camino
Opciones
Opción 1: Opción 2: Opción 3:
Opción 7: Opción 8:
2.1. Investigue sobre el algoritmo de Euclides, que calcula recursivamente el máximo común
divisor entre dos números, e implemente una función
public static int gcd(int a, int b) {
//...
}
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
que cumpla tal fin.
2.2. Adapte la solución del numeral anterior para que funcione sobre números de tipo
BigInteger, implementando el método
public static BigInteger gcd(BigInteger a, BigInteger b) {
//...
}
Su solución debe ser recursiva. ¡No haga trampa usando la función gcd de la clase
BigInteger!
3.2. ¿El algoritmo Quick Sort usa la técnica de Dividir y Vencer? ¿Por qué?
3.3. Invéntese un arreglo de números enteros de tamaño 8 y explique paso a paso en sus
propias palabras cómo el algoritmo Quick Sort ordena el arreglo.
3.5. ¿Por qué la complejidad temporal del algoritmo en el peor caso es de ? ¿En qué
consiste el peor caso? ¿Qué tan frecuentemente ocurre?
4. STOOGE SORT
1. Si el último elemento del arreglo es menor que el primero, intercambia tales elementos.
2. Si hay tres o más elementos en el arreglo:
2.1. Ordena recursivamente los primeros dos tercios del arreglo.
2.2. Ordena recursivamente los últimos dos tercios del arreglo.
ESTRUCTURAS DE DATOS 2
2.3. Ordena (de nuevo) recursivamente los primeros dos tercios del arreglo.
Implementado en Java:
public static void stoogeSort(long[] pArreglo, int pInf, int pSup) {
// Si hay uno o cero elementos en el arreglo:
if (pSup<=pInf) {
// No hacer ninguna operación
}
// Si hay dos o más elementos en el arreglo:
else {
long primero=pArreglo[pInf],ultimo=pArreglo[pSup];
// Si pArreglo[pSup] es menor que pArreglo[pInf]:
if (ultimo<primero) {
// Intercambiar el primero con el último:
pArreglo[pInf]=ultimo;
pArreglo[pSup]=primero;
}
// Si hay tres o más elementos en el arreglo:
if (pSup-pInf+1>=3) {
int t=(pSup-pInf+1)/3;
// Ordenar recursivamente los primeros 2/3:
stoogeSort(pArreglo,pInf,pSup-t);
// Ordenar recursivamente los últimos 2/3:
stoogeSort(pArreglo,pInf+t,pSup);
// Ordenar recursivamente (de nuevo) los primeros 2/3
stoogeSort(pArreglo,pInf,pSup-t);
}
}
}
5.1. Sea una función que recibe como parámetro dos números naturales y tales
que , y que está definida de la siguiente manera:
5.2. Construya una tabla de tamaño que muestre los resultados de para todos
los valores de y que estén entre y .
5.3. Implemente en Java un procedimiento recursivo (sin usar Memoization) que calcule
. Utilice la clase BigInteger para los cálculos y siga el siguiente esquema:
public static BigInteger gRecursivo(int n, int k) {
// ...
ESTRUCTURAS DE DATOS 3
}
5.4. Halle una ecuación de recurrencia que describa la complejidad temporal del algoritmo
codificado en el numeral anterior considerando como operaciones básicas las sumas, las
restas, las multiplicaciones y las comparaciones. Explique detalladamente cómo determinó la
fórmula, pero no encuentre una fórmula cerrada para la recurrencia.
5.7. Calcule el valor de con los programas que implementó (el recursivo sin
Memoization y el recursivo con Memoization). Indique cuántos milisegundos demoró la
ejecución de cada uno de los dos programas para calcular la respuesta. Tenga en
consideración que la ejecución podría tardar más de un minuto.
5.8. ¿Cuál versión del algoritmo se demoró menos ejecutando las pruebas? Justifique
plenamente por qué el algoritmo que se demoró menos es más eficiente.
ESTRUCTURAS DE DATOS 4
GUÍA DE COMPETENCIAS Y ACTIVIDADES
SEMANA 3
TEMA(S): NÚCLEO TEMÁTICO: ESTRUCTURAS DE DATOS LINEALES
1. Teoría e implementación de estructuras de datos lineales
1.1. Listas.
1.1.1. Vectores.
2. Estructuras lineales en la librería estándar de Java.
2.1. Listas: Estudio de la interfaz List<E> y de las clases Vector<E>, ArrayList<E> y
LinkedList<E>.
1 [ POLITÉCNICO GRANCOLOMBIANO]
4- Abstraer información y comportamiento de objetos del mundo real, utilizando
herramientas formales, para la construcción de modelos que permitan el diseño de
soluciones.
5- Plantear, diseñar e implementar Sistemas de Tecnología, Información y
Telecomunicaciones capaces de resolver una problemática en un contexto dado, con
restricciones identificadas y recursos definidos.
6- Identificar cuales variables o parámetros son los más relevantes en la dinámica del
sistema, y así mismo descartar aspectos irrelevantes, o de poca incidencia con el fin de
llegar a modelos matemáticos que permitan soluciones analíticas.
Lectura 3-2 - Tres Leer y comprender los conceptos expuestos en las lecturas.
Las listas en el
lenguaje Java.
Lectura 3-3 - Tres Leer y comprender los conceptos expuestos en las lecturas.
Implementaciones de
listas (parte 1).
[ ESTRUCTURA DE DATOS ] 2
Video diapositivas 1 - Leer y comprender los conceptos expuestos en los videos diapositivas.
Operaciones básicas Tres
sobre listas.
Ejercicios propuestos. Tres Desarrollar de forma individual algunos de los ejercicios propuestos para la
semana.
Recursos adicionales. Tres Revisar cuidadosamente el material en Java organizado como proyectos en
Eclipse, con el fin de afianzar la práctica.
SEMANA 4
TEMA(S): NÚCLEO TEMÁTICO: ESTRUCTURAS DE DATOS LINEALES
1. Teoría e implementación de estructuras de datos lineales
1.1. Listas.
1.1.1. Vectores.
2. Estructuras lineales en la librería estándar de Java.
2.1. Listas: Estudio de la interfaz List<E> y de las clases Vector<E>, ArrayList<E> y
LinkedList<E>.
3 [ POLITÉCNICO GRANCOLOMBIANO]
1.5. Colas de prioridad.
2. Estructuras lineales en la librería estándar de Java.
2.2. Pilas: Estudio de la clase Stack<E>.
2.3. Colas: Estudio de la interfaz Queue<E> y de la clase LinkedList<E>.
2.4. Colas de prioridad: Estudio de la clase PriorityQueue<E>.
[ ESTRUCTURA DE DATOS ] 4
NÚCLEO TEMÁTICO: ESTRUCTURAS DE DATOS LINEALES
1. Teoría e implementación de estructuras de datos lineales
1.1. Listas.
1.1.2. Listas doblemente encadenadas en anillo con encabezado.
1.1.3. Listas doblemente encadenadas.
1.1.4. Listas sencillamente encadenadas.
1.2. Listas ordenadas.
1.3. Pilas.
1.4. Colas.
1.5. Colas de prioridad.
2. Estructuras lineales en la librería estándar de Java.
2.2. Pilas: Estudio de la clase Stack<E>.
2.3. Colas: Estudio de la interfaz Queue<E> y de la clase LinkedList<E>.
2.4. Colas de prioridad: Estudio de la clase PriorityQueue<E>.
Lectura 4-2 - Cuatro Leer y comprender los conceptos expuestos en las lecturas.
Iteradores sobre listas.
Lectura 4-3 - Cuatro Leer y comprender los conceptos expuestos en las lecturas.
Otras estructuras de
datos lineales.
Video diapositivas 1 - Cuatro Leer y comprender los conceptos expuestos en los videos diapositivas.
Notación infija.pptx.
Video diapositivas 2 - Cuatro Leer y comprender los conceptos expuestos en los videos diapositivas.
Notación posfija.pptx.
Ejercicios propuestos. Cuatro Desarrollar de forma individual algunos de los ejercicios propuestos para la
semana.
Recursos adicionales. Cuatro Revisar cuidadosamente el material en Java organizado como proyectos en
Eclipse, con el fin de afianzar la práctica.
5 [ POLITÉCNICO GRANCOLOMBIANO]
ESTRUCTURAS DE DATOS
UNIDAD DOS - SEMANA TRES
LAS LISTAS COMO ESTRUCTURAS DE DATOS *
TABLA DE CONTENIDO
1. DEFINICIÓN 2
2. APLICABILIDAD 2
3. CONCEPTOS BÁSICOS 2
3.1. POSICIÓN 2
3.2. TAMAÑO 3
3.3. ORDINALES 3
3.4. SIGUIENTE 3
3.5. ANTERIOR 3
3.6. RECORRIDOS 4
3.7. IGUALDAD 4
3.8. CONTENENCIA 4
3.9. SUBLISTAS 4
3.10. EJEMPLOS 4
4. OPERACIONES BÁSICAS 6
EN RESUMEN 7
PARA TENER EN CUENTA 8
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
1. DEFINICIÓN
Una lista es una secuencia finita de elementos del mismo tipo. Por medio de la notación
Es importante tener en cuenta que en toda lista importa el orden en el que se encuentran
sus elementos.
2. APLICABILIDAD
Las listas tienen gran aplicabilidad como estructuras de datos porque nos permiten
almacenar elementos en secuencia, un elemento seguido de otro. En el mundo cotidiano
tenemos listas por doquier, por ejemplo:
La lista de alumnos registrados en una asignatura.
La lista de asistencia a clase.
La lista de personas que asisten a un evento.
La lista de actividades a realizar en el día.
La lista que contiene los días de la semana:
3. CONCEPTOS BÁSICOS
3.1. POSICIÓN
Todos los elementos de una lista se pueden acceder por posición. En la lista
ESTRUCTURAS DE DATOS 2
3.2. TAMAÑO
El tamaño o longitud de la lista se define como el número de elementos que almacena. Así
pues, la lista tendría elementos, y la lista vacía tendría elementos.
3.3. ORDINALES
Nótese en todo caso, que el primer elemento de la lista se encuentra en la posición cero y
que el último elemento se encuentra en la posición correspondiente al tamaño de la lista
menos uno.
3.4. SIGUIENTE
Cada elemento de la lista excepto el último tiene un siguiente elemento. Por ejemplo, el
siguiente del primero es el segundo, el siguiente del segundo es el tercero, etc. En general, el
siguiente elemento del que está en la posición es el que está en la posición .
3.5. ANTERIOR
Cada elemento de la lista excepto el primero tiene un anterior elemento. Por ejemplo, el
anterior del segundo es el primero, el anterior del tercero es el segundo, etc. En general, el
anterior elemento del que está en la posición es el que está en la posición .
ESTRUCTURAS DE DATOS 3
3.6. RECORRIDOS
Un recorrido de una lista es una forma de visitar todos los elementos de la lista. Dos posibles
recorridos de una lista son:
Recorrido al derecho: visita los elementos de la lista en el orden que aparecen, comenzando
por el primer elemento y terminando en el último.
Recorrido al revés: visita los elementos de la lista en el orden contrario al que aparecen,
comenzando por el último elemento y terminando en el primero.
3.7. IGUALDAD
Dos listas son iguales si y sólo si tienen el mismo tamaño y almacenan los mismos elementos
en las mismas posiciones.
3.8. CONTENENCIA
Una lista está contenida dentro de otra si y sólo si todo elemento de la primera está presente
dentro de la segunda, así sea en distinto orden.
3.9. SUBLISTAS
Una lista es sublista de otra si y sólo si todos los elementos de la primera aparecen en el
mismo orden a partir de alguna posición de la segunda.
3.10. EJEMPLOS
Tabla 1: Ejemplos sobre los conceptos básicos.
Concepto Lista Ejemplo
El elemento de la posición de la lista está indefinido,
porque esta posición se encuentra por fuera de la lista.
El elemento de la posición de la lista es .
El elemento de la posición de la lista es .
Posición
El elemento de la posición de la lista está indefinido,
porque esta posición se encuentra por fuera de la lista.
El elemento de la posición de la lista está indefinido,
porque esta posición se encuentra por fuera de la lista.
El elemento de la posición de la lista es .
Posición
El elemento de la posición de la lista es .
ESTRUCTURAS DE DATOS 4
El elemento de la posición de la lista es .
El elemento de la posición de la lista es .
El elemento de la posición de la lista es .
El elemento de la posición de la lista es .
El elemento de la posición de la lista está indefinido,
porque esta posición se encuentra por fuera de la lista.
Tamaño El tamaño de la lista es porque la lista está vacía.
Tamaño El tamaño de la lista es .
Tamaño El tamaño de la lista es .
Tamaño El tamaño de la lista es .
El primer elemento de la lista es el (es también el
penúltimo).
Ordinales
El segundo elemento de la lista es el (es también el
último).
El primer elemento de la lista es el .
El segundo elemento de la lista es el .
El tercer elemento de la lista es el .
El cuarto elemento de la lista es el (es también el
Ordinales antepenúltimo).
El quinto elemento de la lista es el (es también el
penúltimo).
El sexto elemento de la lista es el (es también el
último).
El siguiente del elemento es el elemento .
Siguiente El siguiente del elemento es el elemento .
El elemento no tiene siguiente porque es el último.
El siguiente del elemento no está definido porque el
Siguiente
valor aparece dos veces dentro de la lista.
El elemento no tiene anterior porque es el primero.
Anterior El anterior del elemento es el elemento .
El anterior del elemento es el elemento .
El anterior del elemento no está definido porque el
Anterior
valor aparece dos veces dentro de la lista.
Al derecho: visita los elementos en el orden .
Recorridos
Al revés: visita los elementos en el orden .
Al derecho: visita los elementos en el orden
.
Recorridos
Al revés: visita los elementos en el orden
.
ESTRUCTURAS DE DATOS 5
NO NO NO
NO NO NO
NO SI NO
NO SI SI
NO NO NO
NO SI SI
NO SI SI
NO SI SI
NO NO NO
SI SI SI
NO SI NO
4. OPERACIONES BÁSICAS
ESTRUCTURAS DE DATOS 6
Inserción del elemento en la posición .
Inserción
(inserción al principio)
Inserción Inserción del elemento en la posición .
EN RESUMEN
Una lista es una secuencia finita de elementos del mismo tipo.
La lista vacía no tiene ningún elemento.
Todos los elementos de una lista se pueden acceder por posición.
El tamaño o longitud de la lista se define como el número de elementos que almacena.
El primer elemento de la lista es el que se encuentra en la posición , el segundo elemento es el
que se encuentra en la posición , el tercer elemento es el que se encuentra en la posición , etc.
El último elemento de la lista es el que se encuentra en la posición , el penúltimo elemento es
el que se encuentra en la posición , el antepenúltimo elemento es el que se encuentra en la
posición , etc.
Un recorrido de una lista es una forma de visitar todos los elementos de la lista.
Dos listas son iguales si y sólo si tienen el mismo tamaño y almacenan los mismos elementos en las
mismas posiciones.
ESTRUCTURAS DE DATOS 7
Una lista está contenida dentro de otra si y sólo si todo elemento de la primera está presente
dentro de la segunda, así sea en distinto orden.
Una lista es sublista de otra si y sólo si todos los elementos de la primera aparecen en el mismo
orden a partir de alguna posición de la segunda.
Para administrar el contenido de una lista existen cuatro operaciones básicas: consulta,
modificación, inserción y eliminación.
ESTRUCTURAS DE DATOS 8
ESTRUCTURAS DE DATOS
UNIDAD DOS - SEMANA TRES
LAS LISTAS EN EL LENGUAJE JAVA *
TABLA DE CONTENIDO
1. LA INTERFAZ LIST<E> 2
2. IMPLEMENTACIONES SUMINISTRADAS POR JAVA PARA LA INTERFAZ LIST<E> 2
3. SERVICIOS PROVISTOS POR LA INTERFAZ LIST<E> 4
EN RESUMEN 6
PARA TENER EN CUENTA 6
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
1. LA INTERFAZ LIST<E>
El tipo E es genérico: puede ser cualquier clase en Java. Por ejemplo, List<Integer>
representa una lista de números enteros, List<Double> representa una lista de números
flotantes, List<String> representa una lista de cadenas de texto, List<Persona> representa
una lista de personas, List<Circulo> representa una lista de círculos, etc.
†
La interfaz List<E> se encuentra ubicada en el paquete java.util, y su documentación en inglés está
disponible en el API de Java en la página http://java.sun.com/javase/6/docs/api/java/util/List.html.
ESTRUCTURAS DE DATOS 2
Gráfica 2: Representación de la lista con la clase ArrayList<E> de Java.
0 1 2 n-1 n t-1
Arreglo ... ... Tamaño
Anillo
...
...
La única diferencia entre ArrayList<E> y Vector<E> es que los métodos de la clase Vector<E>
están sincronizados, mientras que los de la clase ArrayList<E> no. Para describir qué significa
esto hay que hablar sobre Hilos de ejecución (mejor conocidos en inglés como Threads).
Resulta que, en lenguajes como Java y C++, un programa puede estar formado por varios
Threads, donde todos ejecutan en paralelo varias tareas a la vez. Imagínese dos Threads
accediendo sobre el mismo objeto: una cuenta bancaria que tiene tres mil euros. Suponga
que el primer Thread comienza a extraer de la cuenta bancaria dos mil euros y que justo en
el mismo momento, el segundo Thread intenta extraer también dos mil euros de la misma
cuenta. Bajo este escenario puede suceder que ambas extracciones de dinero sean exitosas,
lo que permitiría sacar cuatro mil euros sobre una cuenta bancaria que sólo tenía tres mil. La
sincronización soluciona este problema: evita que dos Threads ejecuten en el mismo instante
de tiempo dos operaciones sobre el mismo objeto, obligándolos a que hagan cola uno detrás
del otro para que tengan permiso de acceder al objeto sin estorbarse entre sí.
ESTRUCTURAS DE DATOS 3
Gráfica 5: Peligros de no sincronizar los métodos de una clase.
Inicio Thread 1 Saldo inicial de la cuenta bancaria: 3000 euros
Monto del retiro: 2000 euros
Inicio Thread 2 Consultar saldo: 3000 euros
¿El saldo es mayor o igual que Monto del retiro: 2000 euros
el monto del retiro?: SI Consultar saldo: 3000 euros
Entregar dinero al cliente … ¿El saldo es mayor o igual que
THREAD #1 Restarle al saldo el monto el monto del retiro?: SI
del retiro: 3000€-2000€=1000€ Entregar dinero al cliente …
THREAD #2
Fin de la transacción Restarle al saldo el monto
del retiro: 3000€-2000€=1000€
Fin de la transacción
Monto total retirado: 4000 euros
E get(int index)
Retorna el elemento de la posición index de la
lista.
Operaciones de modificación sobre la lista
Modifica el elemento de la posición index de la
lista por el valor element.
E set(int index, E element) Retorna como resultado el valor que se
encontraba en la posición index antes de
realizar la modificación.
Operaciones de búsqueda sobre la lista
boolean contains(Object obj)
Retorna true si el objeto obj está dentro de la
lista; retorna false si no.
Retorna true si todos los objetos de la
boolean containsAll(Collection<?> coll) colección coll están dentro de la lista; retorna
false si no.
Retorna el índice de la primera aparición del
int indexOf(Object obj) objeto obj dentro de la lista. En caso de que el
objeto obj no esté en la lista, retorna -1.
Retorna el índice de la última aparición del
int lastIndexOf(Object obj) objeto obj dentro de la lista. En caso de que el
objeto obj no esté en la lista, retorna -1.
Operaciones de inserción sobre la lista
boolean add(E element)
Inserta el elemento element al final de la lista.
Retorna true en todo caso.
ESTRUCTURAS DE DATOS 4
void add(int index, E element)
Inserta el elemento element en la posición
index de la lista.
Inserta todos los elementos de la colección
coll al final de la lista.
boolean addAll(Collection<? extends E> coll)
Retorna true si la lista fue modificada por la
operación; retorna false si no.
Inserta todos los elementos de la colección
boolean addAll(int index, coll a partir de la posición index de la lista.
Collection<? extends E> coll) Retorna true si la lista fue modificada por la
operación; retorna false si no.
Operaciones de eliminación sobre la lista
void clear() Elimina todos los elementos de la lista.
Elimina el elemento de la posición index de la
E remove(int index)
lista.
Retorna el valor que precisamente fue
eliminado.
Elimina la primera aparición del objeto obj
dentro de la lista. En caso de que el objeto obj
no aparezca, no se hace nada. En caso de que el
boolean remove(Object obj) objeto obj aparezca más de una vez, sólo se
elimina su primera aparición.
Retorna true si la lista fue modificada por la
operación; retorna false si no.
Elimina de la lista todos los elementos que
boolean removeAll(Collection<?> coll)
aparezcan dentro de la colección coll.
Retorna true si la lista fue modificada por la
operación; retorna false si no.
Retiene en la lista sólo los elementos que
aparezcan dentro de la colección coll. En otras
boolean retainAll(Collection<?> coll)
palabras, elimina de la lista todos los elementos
que no aparezcan dentro de la colección coll.
Retorna true si la lista fue modificada por la
operación; retorna false si no.
Métodos que entregan iteradores sobre la lista
Iterator<E> iterator()
Retorna un iterador capaz de visitar todos los
elementos de la lista.
ListIterator<E> listIterator()
Retorna un iterador capaz de visitar todos los
elementos de la lista.
Retorna un iterador capaz de visitar los
ListIterator<E> listIterator(int index) elementos de la lista, iniciando en la posición
index.
Métodos que entregan un arreglo con el contenido de la lista
Object[] toArray()
Retorna un arreglo de objetos contiendo todos
los elementos de la lista.
<T> T[] toArray(T[] array) Retorna un arreglo de objetos contiendo todos
ESTRUCTURAS DE DATOS 5
los elementos de la lista. Si el tamaño del
arreglo array es mayor o igual que el tamaño
de la lista, se usa array para guardar los
elementos de la lista.
Métodos de comparación
boolean equals(Object obj)
Retorna true si obj es una lista igual a esta
lista; retorna false si no.
Métodos de hashing
int hashCode()
Dejaremos la descripción de este método hasta
cuando estemos estudiando Tablas de Hashing.
Métodos que entregan vistas de la lista
Retorna la sublista de esta lista que va desde la
posición fromIndex hasta la posición toIndex-
1. La sublista retornada es una vista de la lista
List<E> subList(int fromIndex, int toIndex)
original, es decir, cualquier modificación sobre
la sublista retornada también tiene efecto
sobre la lista original.
EN RESUMEN
Una interfaz es un conjunto de métodos definidos sin implementación.
List<E> es una interfaz de Java que representa una lista de elementos de tipo E.
ArrayList<E> implementa listas a través de vectores (arreglos dinámicos de tamaño variable).
Vector<E> también implementa listas a través de vectores (arreglos dinámicos de tamaño variable).
LinkedList<E> implementa listas a través de nodos doblemente encadenados en anillo con
encabezado.
La interfaz List<E> define 25 métodos de propósito general para manipular listas.
ESTRUCTURAS DE DATOS 6
ESTRUCTURAS DE DATOS
UNIDAD DOS - SEMANA TRES
IMPLEMENTACIONES DE LISTAS (PARTE 1) *
TABLA DE CONTENIDO
1. INTRODUCCIÓN 2
2. DECLARACIÓN DE UNA INTERFAZ PARA LA REPRESENTACIÓN DE LISTAS 2
3. APUNTES SOBRE LA ADMINISTRACIÓN DE LA MEMORIA PRINCIPAL EN JAVA 3
4. IMPLEMENTACIÓN CON VECTORES 4
4.1. CONSTRUCTOR 5
4.2. DESTRUCTOR 6
4.3. OPERACIÓN DE CONSULTA 6
4.4. OPERACIÓN DE MODIFICACIÓN 7
4.5. OPERACIÓN DE INSERCIÓN 7
4.1. OPERACIÓN DE ELIMINACIÓN 10
EN RESUMEN 11
PARA TENER EN CUENTA 12
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
1. INTRODUCCIÓN
Cada vez que necesitemos una lista deberemos decidir si escoger la implementación con
vectores (ArrayList<E>) o la implementación con nodos encadenados (LinkedList<E>),
dependiendo de cuál nos dé la mejor eficiencia en el problema que estemos resolviendo.
En esta lectura codificaremos una interfaz para representar listas y cuatro implementaciones
de tal interfaz (las siglas VED significan Versión especialmente diseñada para el módulo de
Estructuras de Datos):
Tabla 1: Interfaces y clases que trataremos en el estudio de las listas como estructuras de datos.
Tipo Nombre Descripción
Interfaz para representar una lista de elementos de tipo E. Es una
Interfaz VEDList<E>
copia exacta de la interfaz List<E> de Java.
Clase que implementa listas con vectores (arreglos dinámicos de
Clase VEDArrayList<E> tamaño variable). Pretende enseñar cómo están construidas las
clases ArrayList<E> y Vector<E> de Java.
Clase que implementa listas con nodos doblemente encadenados
Clase VEDLinkedList<E> en anillo con encabezado. Pretende enseñar cómo está construida
la clase LinkedList<E> de Java.
Clase que implementa listas con nodos doblemente encadenados.
Clase VEDLinkedListD<E> Esta implementación no es provista por Java y no se distribuye su
código fuente en el recurso ImplementacionesListas.zip.
Clase que implementa listas con nodos sencillamente
encadenados. Esta implementación no es provista por Java y no se
Clase VEDLinkedListS<E>
distribuye su código fuente en el recurso
ImplementacionesListas.zip.
ESTRUCTURAS DE DATOS 2
VEDLinkedListS<E> no se incluyen puesto que son objeto de estudio de los ejercicios
prácticos propuestos para la unidad.
Las clases están documentadas siguiendo Javadoc, que es un estándar de Sun Microsystems
para documentar código en Java. Puede encontrar más información sobre Javadoc en los
siguientes enlaces:
1. How to Write Doc Comments for the Javadoc Tool:
http://java.sun.com/j2se/javadoc/writingdoccomments/index.html
2. javadoc - The Java API Documentation Generator:
http://java.sun.com/javase/6/docs/technotes/tools/windows/javadoc.html
En Java toda variable de tipo básico (byte, short, int, long, float, double, char, boolean) se
administra por valor, es decir, se trata como un registro que almacena un valor.
Por otro lado, toda variable cuyo tipo sea una clase (String, Persona, ArrayList, etc.) se
administra por referencia, es decir, se trata como un apuntador que referencia a un objeto,
almacenando la dirección en memoria donde éste se encuentra.
Gráfica 3: Las variables cuyo tipo sea una clase se administran por referencia.
MEMORIA RAM
La constante null, que representaremos con el símbolo , actúa en Java como un apuntador
a la nada. Asignándole a un apuntador la constante null, se puede dejar de referenciar el
objeto apuntado.
ESTRUCTURAS DE DATOS 3
Gráfica 4: La constante null como apuntador a la nada.
Basura que está MEMORIA RAM
ocupando memoria
CÓDIGO FUENTE centroX
a=null; a centroY
radio
En Java existe un proceso muy importante que se llama el Recolector de Basura (en inglés:
Garbage Collector), cuyo propósito consiste en liberar la memoria ocupada por aquellos
objetos que se han dejado de referenciar por nuestro programa. El Recolector de Basura se
ejecuta cada vez que el uso de memoria sobrepase cierto umbral de porcentaje, o cada vez
que la implementación de la máquina virtual lo decida. Por esta razón no nos debemos
preocupar en Java por liberar explícitamente la memoria reservada: apenas dejemos de
referenciar uno o más objetos éstos se convierten en basura, cuya memoria será liberada por
el Recolector de Basura cuando se ejecute la próxima vez.
Formalmente hablando, en Java no existen los arreglos de tamaño variable, puesto que todos
los arreglos en Java tienen una longitud fija después de haber sido creados. ¿Y entonces qué
hacemos? La respuesta es sencilla: cada vez que necesitemos cambiar el tamaño del arreglo
creamos uno nuevo del tamaño deseado, pasamos el contenido del arreglo viejo al arreglo
nuevo, desechamos el arreglo viejo y nos quedamos con el arreglo nuevo. Este truco nos
permite simular en Java arreglos dinámicos de tamaño variable.
tamanho: es una variable de tipo int que almacena el tamaño de la lista, y que abreviaremos con la
letra . Recuerde que el tamaño de una lista es el número de elementos que almacena.
arreglo: es un arreglo de objetos que almacena todos los elementos de la lista, en el orden
que aparecen. Al tamaño del arreglo se le llama capacidad y lo abreviaremos con la letra .
ESTRUCTURAS DE DATOS 4
Siempre debe cumplirse que (que la capacidad del arreglo sea mayor o igual que el
tamaño de la lista), porque de lo contrario, los elementos de la lista no cabrían en el arreglo.
Todas las posiciones del arreglo desde hasta son casillas ocupadas por los elementos
de la lista, y todas las posiciones del arreglo desde hasta son casillas de reserva para
permitir de forma eficiente la inserción de elementos futuros.
// *************************
// * Atributos de la clase *
// *************************
/**
* Arreglo que contiene los elementos de la lista (inicia con capacidad 1).
*/
private Object[] arreglo=new Object[1];
/**
* Tamaño de la lista (inicia en 0).
*/
private int tamanho=0;
// ***********************
// * Métodos de la clase *
// ***********************
// ...
4.1. CONSTRUCTOR
El método constructor de la clase, que se responsabiliza de crear una nueva lista vacía, no
debe realizar ninguna operación adicional porque con la inicialización de los atributos ya se
está representando una lista vacía: el atributo arreglo se inicializó con un arreglo de
capacidad , y el atributo tamanho se inicializó en . No importa la capacidad inicial del
arreglo porque a medida que se vayan realizando inserciones sobre la lista, se va ajustando
su capacidad.
ESTRUCTURAS DE DATOS 5
4.2. DESTRUCTOR
Para eliminar todos los elementos de la lista existe el método clear, cuya implementación
debe asignarle a todas las casillas ocupadas del arreglo el valor null y debe ponerle el valor
al atributo tamanho. De esta manera, se dejan de referenciar los objetos de la lista,
marcándolos como basura, y dejándole el trabajo de la liberación de memoria al Recolector
de Basura.
Para consultar el elemento que se encuentra en cierta posición de la lista, basta acceder al
arreglo en la posición dada.
Código 11: Método para consultar el valor del elemento que está en determinada posición de la lista.
public E get(int index) {
// Obtener el elemento que está en la posición index del arreglo:
Object elemento=arreglo[index];
// Retornar el elemento obtenido, convertido al tipo E por medio de un cast:
return (E)elemento;
}
ESTRUCTURAS DE DATOS 6
4.4. OPERACIÓN DE MODIFICACIÓN
Para modificar el elemento que se encuentra en cierta posición de la lista, basta acceder al
arreglo en la posición dada y cambiar el valor.
Código 13: Método para modificar el valor del elemento que está en determinada posición de la lista.
public E set(int index, E element) {
// Obtener el elemento que se encuentra en la posición index:
E anteriorValor=get(index);
// Modificar el elemento de la posición index con el valor element:
arreglo[index]=element;
// Retornar el elemento que anteriormente se encontraba en la posición dada:
return anteriorValor;
}
Tabla 14: Análisis de complejidad temporal del método E set(int index, E element).
Tipo de análisis Complejidad Justificación
Peor caso
Acceder la posición de un arreglo para modificarla tiene
Caso promedio
complejidad temporal .
Mejor caso
Antes de insertar un elemento en la estructura de datos hay que revisar si cabe en el arreglo.
En caso de que el arreglo esté completamente lleno (lo que sucede cuando se agotan las
casillas de reserva), es necesario incrementar su capacidad a través del siguiente proceso:
Código 15: Método para asegurar que cierta cantidad de valores quepa en el arreglo.
private void garantizarCapacidad(int nuevaCantidadDeElementos) {
// Si la nueva cantidad de elementos es menor o igual que la capacidad del
// arreglo:
if (nuevaCantidadDeElementos<=arreglo.length) {
// No hacer nada porque los elementos ya caben.
}
// Si la nueva cantidad de elementos es mayor o igual que la capacidad del
// arreglo, tocaría "crecer" la capacidad del arreglo.
else {
// Si la nueva cantidad de elementos es menor que el doble de la capacidad
// del arreglo, dejarla en el doble de la capacidad del arreglo:
ESTRUCTURAS DE DATOS 7
if (nuevaCantidadDeElementos<arreglo.length*2) {
nuevaCantidadDeElementos=arreglo.length*2;
}
// Crear un nuevo arreglo donde quepa la nueva cantidad de elementos:
Object[] nuevoArreglo=new Object[nuevaCantidadDeElementos];
// Copiar todos los elementos del viejo arreglo al nuevo arreglo:
for (int i=0; i<tamanho; i++) {
nuevoArreglo[i]=arreglo[i];
}
// Desechar el viejo arreglo, asignándole el nuevo:
arreglo=nuevoArreglo;
}
}
Como el proceso que incrementa la capacidad del arreglo es ineficiente, pues su complejidad
temporal es , hay que evitar a toda costa que sea ejecutado frecuentemente. Un
mecanismo para reducir sustancialmente el número de veces que se debe crecer el arreglo es
duplicar su capacidad cada vez que se necesite crecer.
Es posible alterar la capacidad del arreglo con un porcentaje distinto al 200% (que
corresponde a duplicar el número de casillas). Por ejemplo, la clase ArrayList<E> de Java
modifica la capacidad en un 150% cada vez que se necesite hacer crecer el arreglo.
Ahora pensemos en el proceso de inserción como tal, sabiendo que contamos con un
mecanismo que es capaz de asegurarnos el espacio para alojar el nuevo elemento. Para
insertar el valor element en la posición index de la lista:
†
Sexagésimo cuarto es el número ordinal para representar el elemento número 64.
ESTRUCTURAS DE DATOS 8
2. Se corren una posición hacia la derecha todos los elementos de la lista que están ubicados
desde la posición index hasta la posición tamanho-1, recorriendo el arreglo al revés: primero
se traslada el elemento de la posición tamanho-1, luego se traslada el elemento de la posición
tamanho-2, …, y finalmente se mueve el elemento de la posición index. ¿Qué tan complicada
queda la programación si hacemos el corrimiento recorriendo el arreglo al derecho en lugar
de hacerlo al revés?
3. Se ubica el valor element en la posición index de la lista.
4. Se incrementa el tamaño de la lista (tamanho) en una unidad.
Gráfica 16: Paso 2 del proceso que inserta un elemento en la posición k (donde k=index).
0 1 2 k-1 k k+1 k+2 n-1 n n+1 t-1
... ... ...
... ...
Tabla 18: Análisis de complejidad temporal del método void add(int index, E element).
Tipo de análisis Complejidad Justificación
El peor caso ocurre en dos situaciones:
Cuando la inserción es al principio: la complejidad temporal es
pues todos los elementos de la lista se deben correr una
posición a la derecha.
Peor caso
Cuando se debe crecer la capacidad del arreglo: la complejidad
temporal es pues se debe crear el nuevo arreglo, trasladar
todos los elementos a ese nuevo arreglo, y desarrollar la
inserción.
En el caso promedio la inserción ocurre cerca de la mitad de la
lista. La complejidad temporal en esta situación es porque
Caso promedio
hay que trasladar aproximadamente elementos una posición
a la derecha.
ESTRUCTURAS DE DATOS 9
El mejor caso ocurre cuando hay casillas de reserva y la inserción
se realiza al final de la lista. La complejidad temporal en esta
Mejor caso
situación es porque no hay que trasladar ningún elemento
ni hay que crecer el arreglo.
Gráfica 19: Pasos 2 y 3 del proceso que elimina el elemento de la posición k (donde k=index).
0 1 2 k-1 k k+1 k+2 n-2 n-1 n t-1
... ... ...
... ... Paso 2
Código 20: Método para eliminar el elemento presente en determinada posición de la lista.
public E remove(int index) {
// Obtener el elemento que se encuentra en la posición index:
E elemento=get(index);
// Correr una posición hacia la izquierda todos los elementos desde la
// posición index+1 hasta la posición tamanho-1:
for (int i=index+1; i<tamanho; i++) {
arreglo[i-1]=arreglo[i];
}
// Poner el valor null en la posición tamanho-1 del arreglo:
arreglo[tamanho-1]=null;
// Decrecer el tamaño en una unidad:
tamanho--;
// Retornar el elemento que se antes se encontraba en la posición index:
ESTRUCTURAS DE DATOS 10
return elemento;
}
EN RESUMEN
En Java, toda variable de tipo básico se administra por valor, es decir, se trata como un registro que
almacena un valor. Por otro lado, toda variable cuyo tipo sea una clase se administra por referencia,
es decir, se trata como un apuntador que referencia a un objeto, almacenando la dirección en
memoria donde éste se encuentra.
La constante null, que representaremos con el símbolo , actúa en Java como un apuntador a la
nada.
El Recolector de Basura (en inglés: Garbage Collector) libera la memoria ocupada por aquellos objetos
que se han dejado de referenciar en nuestro programa.
Cada vez que necesitemos una lista, deberemos decidir si escoger la implementación con vectores
(ArrayList<E>) o la implementación con nodos encadenados (LinkedList<E>), dependiendo de cuál
nos dé la mejor eficiencia en el problema que estemos resolviendo.
La clase VEDArrayList<E> implementa listas con arreglos dinámicos de tamaño variable (vectores).
Ventajas de la implementación de listas con vectores (VEDArrayList<E>):
Todos los elementos están contiguos en memoria principal.
No requiere tanta memoria principal puesto que no hay encadenamientos.
Las operaciones de consulta por posición y modificación por posición son muy eficientes: .
Las inserciones al final de la lista son eficientes cuando no toca crecer la capacidad del arreglo: .
Las eliminaciones al final de la lista siempre son eficientes .
Desventajas de la implementación de listas con vectores (VEDArrayList<E>):
Se debe ejecutar una operación costosa cada vez que se llene la capacidad del arreglo (no importa
mucho porque no sucede muy frecuentemente).
Las inserciones al principio de la lista son ineficientes: .
Las eliminaciones al principio de la lista son ineficientes: .
ESTRUCTURAS DE DATOS 11
PARA TENER EN CUENTA
La clase VEDArrayList<E> nos sirvió para conocer con detalle la estructura interna de las clases
ArrayList<E> y Vector<E> de Java. De ahora en adelante, usaremos ArrayList<E> y Vector<E> en
vez de VEDArrayList<E> porque están disponibles en la librería estándar de Java, nos ofrecen
muchos más servicios, y son capaces de lanzar error cada vez que el usuario intente realizar alguna
operación inválida, como acceder a una posición que esté por fuera de la lista.
ESTRUCTURAS DE DATOS 12
ESTRUCTURAS DE DATOS
UNIDAD DOS - SEMANA CUATRO
IMPLEMENTACIONES DE LISTAS (PARTE 2) *
TABLA DE CONTENIDO
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
1. IMPLEMENTACIÓN CON LISTAS DOBLEMENTE ENCADENADAS
EN ANILLO CON ENCABEZADO
Recurso como proyecto en Eclipse: ImplementacionesListas.zip.
En esta estructura de datos, los nodos son capaces de almacenar valores y de apuntar al
nodo anterior y al nodo siguiente en el encadenamiento. Un nodo, representado con la clase
VEDNodo<E>, tiene los siguientes atributos:
sig
val
ant
Cada elemento de la lista se almacena en un nodo por separado. Además hay un nodo
especial, llamado encabezado o header, cuyo valor es null y que cumple como propósito
hacer más cómoda la programación de la estructura de datos.
header
tamanho
...
...
ESTRUCTURAS DE DATOS 2
Gráfica 3: Representación lineal de la lista como
una lista doblemente encadenada en anillo con encabezado.
VEDLinkedList<E>
header ...
tamanho
Gráfica 4: Representación de la lista vacía como una lista doblemente encadenada en anillo con encabezado.
VEDLinkedList<E>
header
tamanho
tamanho: es una variable de tipo int que almacena el tamaño de la lista, y que abreviaremos con la
letra . Recuerde que el tamaño de una lista es el número de elementos que almacena.
header: es un nodo especial llamado el encabezado de la lista. El propósito de su existencia es
facilitar la labor de programar la estructura de datos. Siempre tiene valor null.
Nunca olvide que el encabezado no guarda ningún elemento de la lista. Por esta razón, si el tamaño
de la lista es , entonces habrán nodos, contando el encabezado.
// **********************************
// * Clase que representa los nodos *
// **********************************
private static class VEDNodo<E> {
E val=null; // Valor del nodo
VEDNodo<E> ant=null; // Nodo anterior
VEDNodo<E> sig=null; // Nodo siguiente
VEDNodo(E pVal) { // Constructor de la clase nodo
val=pVal;
}
}
// *************************
// * Atributos de la clase *
// *************************
/**
* Encabezado. Es declarado "final" para que no sea reasignado.
*/
private final VEDNodo<E> header=new VEDNodo<E>(null);
/**
* Tamaño de la lista (inicia en 0).
*/
private int tamanho=0;
ESTRUCTURAS DE DATOS 3
// ***********************
// * Métodos de la clase *
// ***********************
// ...
1.1. CONSTRUCTOR
El método constructor de la clase, que se responsabiliza de crear una nueva lista vacía, debe
asegurar que se cumpla la situación:
Gráfica 7: Estado de la lista justo después de la inicialización de los atributos de la clase VEDLinkedList<E>.
VEDLinkedList<E>
header
tamanho
¿Falta algo? ¡Poner como anterior y como siguiente del encabezado al mismo encabezado!
ESTRUCTURAS DE DATOS 4
1.2. DESTRUCTOR
Para eliminar todos los elementos de la lista existe el método clear, cuya implementación
debe asegurar que se pase de la situación:
a la situación:
Basta con poner como anterior y como siguiente del encabezado al mismo encabezado y
asignarle el valor a la variable tamanho.
Gráfica 12: Estado de la lista después de reencadenar el encabezado consigo mismo y de asignarle cero al tamaño.
VEDLinkedList<E>
header ...
tamanho
¿Qué pasa con los nodos de la lista que están coloreados de verde? Debido a que no hay
forma de llegar a ellos, se convierten en basura y la memoria que ocupan sería liberada por el
Recolector de Basura cuando sea el momento. Concluimos entonces que después de las
operaciones descritas, eliminamos todos los elementos de la lista, dejándola vacía.
ESTRUCTURAS DE DATOS 5
Caso promedio temporal es , pero considerándolo, la complejidad sería
Mejor caso .
Código 15: Método que retorna el nodo que almacena el valor que se encuentra en determinada posición de la lista.
private VEDNodo<E> getNodo(int index) {
VEDNodo<E> x=header;
if (index<tamanho/2) {
for (int i=0; i<=index; i++) {
x=x.sig;
}
}
else {
for (int i=tamanho-1; i>=index; i--) {
x=x.ant;
}
}
return x;
}
Si index está entre 0 y tamanho-1, el método getNodo nos retorna como resultado el nodo que
almacena el elemento de la posición index de la lista; de lo contrario, nos retorna header. En
caso de que index sea menor que la mitad del tamaño de la lista, sale más barato llegar al
nodo yendo hacia delante de siguiente en siguiente; por otro lado, si index es mayor que la
mitad del tamaño de la lista, sale más barato llegar al nodo yendo hacia atrás de anterior en
anterior. Observe que si index es igual a la mitad del tamaño de la lista, es igual de pesado ir
hacia delante que ir hacia atrás (¿Por qué?)
Tabla 16: Análisis de complejidad temporal del método VEDNodo<E> getNodo(int index).
Tipo de análisis Complejidad Justificación
El peor caso ocurre cuando index está cerca de la mitad de la
lista, dando una complejidad temporal de porque hay que
Peor caso
pasar aproximadamente por la mitad de los nodos para llegar al
nodo que deseamos.
En el caso promedio, index está cerca de la mitad de la lista. La
Caso promedio
complejidad temporal es por la razón dada en el peor caso.
El mejor caso ocurre cuando index es 0 o tamanho-1. Si index es
0, el nodo requerido es el siguiente del header, y si index es
Mejor caso
tamanho-1, el nodo requerido es el anterior del header. En
ambos casos, la complejidad es .
ESTRUCTURAS DE DATOS 6
Usando el método getNodo podemos consultar el elemento que se encuentra en cierta
posición de la lista.
Código 17: Método para consultar el valor del elemento que está en determinada posición de la lista.
public E get(int index) {
// Obtener el nodo que guarda el elemento que está en la posición index:
VEDNodo<E> nodo=getNodo(index);
// Retornar el valor almacenado en el nodo:
return nodo.val;
}
Note que la complejidad temporal del algoritmo depende completamente del método
getNodo.
Código 19: Método para modificar el valor del elemento que está en determinada posición de la lista.
public E set(int index, E element) {
// Obtener el nodo que guarda el elemento que está en la posición index:
VEDNodo<E> nodo=getNodo(index);
// Recuperar el valor almacenado en el nodo:
E anteriorValor=nodo.val;
// Cambiar el valor almacenado en el nodo por el valor element:
nodo.val=element;
// Retornar el elemento que anteriormente se encontraba en la posición dada:
return anteriorValor;
}
Observe que la complejidad temporal del algoritmo depende completamente del método
getNodo.
Tabla 20: Análisis de complejidad temporal del método E set(int index, E element).
Tipo de análisis Complejidad Justificación
Modificar el elemento de la mitad de la lista tiene complejidad
Peor caso
.
Caso promedio Modificar elementos cerca de la mitad de la lista tiene
ESTRUCTURAS DE DATOS 7
complejidad .
Modificar el primer o el último elemento de la lista tiene
Mejor caso
complejidad .
1. Creamos un nuevo nodo cuyo valor sea el elemento que queremos insertar.
VEDNodo<E> nuevo=new VEDNodo<E>(e);
VEDLinkedList<E>
header ... ...
tamanho
nuevo
nuevo
3. Ponga el anterior del nodo nuevo en el nodo a y el siguiente del nodo nuevo en el nodo b.
nuevo.ant=a;
nuevo.sig=b;
VEDLinkedList<E> a b
nuevo
4. Ponga el siguiente del nodo a en el nodo nuevo y el anterior del nodo b en el nodo nuevo.
ESTRUCTURAS DE DATOS 8
a.sig=nuevo;
b.ant=nuevo;
VEDLinkedList<E> a b
nuevo
Observe que la complejidad temporal del algoritmo depende completamente del método
getNodo.
Tabla 22: Análisis de complejidad temporal del método void add(int index, E element).
Tipo de análisis Complejidad Justificación
Peor caso Insertar un valor en la mitad de la lista tiene complejidad .
Insertar valores cerca de la mitad de la lista tiene complejidad
Caso promedio
.
Insertar un valor al principio o al final de la lista tiene
Mejor caso
complejidad .
¿Por qué el algoritmo de inserción descrito funciona para insertar al principio de la lista, para
insertar al final de la lista y para insertar en medio de la lista? ¿Tiene algo que ver con el
encabezado?
Usando el método getNodo también podemos eliminar el elemento ubicado en cierta posición
de la lista (suponga que k es la posición del elemento que queremos eliminar de la lista):
ESTRUCTURAS DE DATOS 9
1. Con la ayuda de la función getNodo, obtenemos un apuntador b al nodo que almacena el
elemento de la posición index, declaramos a como un apuntador al nodo anterior del nodo b,
y declaramos c como un apuntador al nodo siguiente del nodo b.
VEDNodo<E> b=getNodo(index);
VEDNodo<E> a=b.ant;
VEDNodo<E> c=b.sig;
VEDLinkedList<E> a c
4. Como el elemento dejó de ser referenciado por la lista, se volvió basura. Cuando lo
decida la Máquina Virtual de Java, el Recolector de Basura liberará la memoria ocupada por
este nodo.
VEDLinkedList<E>
header ... ...
tamanho
Código 23: Método para eliminar el elemento presente en determinada posición de la lista.
public E remove(int index) {
// Obtener el nodo que guarda el elemento que está en la posición index:
VEDNodo<E> b=getNodo(index);
// Obtener el anterior del nodo b:
VEDNodo<E> a=b.ant;
// Obtener el siguiente del nodo b:
VEDNodo<E> c=b.sig;
// Reencadenar los nodos de la lista:
a.sig=c;
c.ant=a;
// Decrecer el tamaño en una unidad:
tamanho--;
// Retornar el elemento que se antes se encontraba en la posición index:
return b.val;
}
ESTRUCTURAS DE DATOS 10
Observe que la complejidad temporal del algoritmo depende completamente del método
getNodo.
¿Por qué el algoritmo de eliminación descrito funciona para eliminar del principio de la lista,
para eliminar del final de la lista y para eliminar de en medio de la lista? ¿Tiene algo que ver
con el encabezado?
Gráfica 26: Representación de la lista vacía como una lista doblemente encadenada de tipo VEDLinkedListD<E>.
VEDLinkedListD<E>
primero
ultimo
tamanho
ESTRUCTURAS DE DATOS 11
3. IMPLEMENTACIÓN CON LISTAS SENCILLAMENTE
ENCADENADAS
Las listas también se pueden implementar con nodos sencillamente encadenados con
apuntador al primero (para futura referencia, llamaremos a esta versión VEDLinkedListS<E>).
En una lista sencillamente encadenada los nodos sólo tienen valor y apuntador al siguiente.
Gráfica 27: Representación gráfica de los nodos en una lista sencillamente encadenada.
sig
val
Gráfica 29: Representación de la lista vacía como una lista sencillamente encadenada de tipo VEDLinkedListS<E>.
VEDLinkedListS<E>
primero
tamanho
ESTRUCTURAS DE DATOS 12
VEDLinkedList Las operaciones de consulta, Las operaciones de consulta,
modificación, inserción y eliminación modificación, inserción y eliminación
son muy eficientes tanto al principio son ineficientes cerca de la mitad de la
como al final de la lista: . lista: .
Es fácil de implementar debido a la Las consultas sólo son eficientes al
presencia de encabezado y a la principio y al final de la lista.
ausencia de apuntadores nulos. Requiere más memoria principal que
VEDArrayList<E> puesto que se deben
mantener los encadenamientos.
VEDLinkedListD Las operaciones de consulta, Las operaciones de consulta,
modificación, inserción y eliminación modificación, inserción y eliminación
son muy eficientes tanto al principio son ineficientes cerca de la mitad de la
como al final de la lista: . lista: .
Las consultas sólo son eficientes al
principio y al final de la lista.
Requiere más memoria principal que
VEDArrayList<E> puesto que se deben
mantener los encadenamientos.
Es difícil de implementar debido a la
ausencia de encabezado y a la
presencia de apuntadores nulos.
VEDLinkedListS Las operaciones de consulta, Las operaciones de consulta,
modificación, inserción y eliminación modificación, inserción y eliminación
son muy eficientes al principio de la son ineficientes en toda posición de la
lista: . lista excepto al principio: .
Es difícil de implementar debido a la
ausencia de encabezado y a la
presencia de apuntadores nulos.
Tabla 31: Algunos criterios importantes para decidir cuál estructura de datos de Java escoger para representar listas.
Si desea … Prefiera usar …
usar poca memoria principal ArrayList<E>
eficiencia consultando el elemento de cualquier posición ArrayList<E>
eficiencia modificando el elemento de cualquier posición ArrayList<E>
eficiencia en las operaciones básicas sólo sobre el final de la lista ArrayList<E>
eficiencia en las operaciones básicas tanto al principio como al final de la lista LinkedList<E>
eficiencia en las operaciones básicas sólo sobre el principio de la lista LinkedList<E>
crear una lista tan grande que no sea posible reservar en memoria un arreglo
LinkedList<E>
del tamaño suficiente para alojar sus elementos
EN RESUMEN
Cada vez que necesitemos una lista, deberemos decidir si escoger la implementación con vectores
(ArrayList<E>) o la implementación con nodos encadenados (LinkedList<E>), dependiendo de cuál
nos dé la mejor eficiencia en el problema que estemos resolviendo.
ESTRUCTURAS DE DATOS 13
La clase ArrayList<E> implementa listas con arreglos dinámicos de tamaño variable (vectores).
Ventajas de la implementación de listas con vectores (ArrayList<E>):
Todos los elementos están contiguos en memoria principal.
No requiere tanta memoria principal puesto que no hay encadenamientos.
Las operaciones de consulta por posición y modificación por posición son muy eficientes: .
Las inserciones al final de la lista son eficientes cuando no toca crecer la capacidad del arreglo: .
Las eliminaciones al final de la lista siempre son eficientes .
Desventajas de la implementación de listas con vectores (ArrayList<E>):
Se debe ejecutar una operación costosa cada vez que se llene la capacidad del arreglo (no importa
mucho porque no sucede muy frecuentemente).
Las inserciones al principio de la lista son ineficientes: .
Las eliminaciones al principio de la lista son ineficientes: .
La clase LinkedList<E> implementa listas con nodos doblemente encadenados en anillo con
encabezado.
Ventajas de la implementación de listas con nodos doblemente encadenados (LinkedList<E>):
Las operaciones de consulta, modificación, inserción y eliminación son muy eficientes tanto al
principio como al final de la lista: .
Es fácil de implementar debido a la presencia de encabezado y a la ausencia de apuntadores nulos.
Desventajas de la implementación de listas con nodos doblemente encadenados (LinkedList<E>):
Las operaciones de consulta, modificación, inserción y eliminación son ineficientes cerca de la mitad
de la lista: .
Las consultas sólo son eficientes al principio y al final de la lista.
Requiere más memoria principal que ArrayList<E> puesto que se deben mantener los
encadenamientos.
ESTRUCTURAS DE DATOS 14
ESTRUCTURAS DE DATOS
UNIDAD DOS - SEMANA CUATRO
ITERADORES SOBRE LISTAS *
TABLA DE CONTENIDO
1. DEFINICIÓN 2
2. RECORRIDO DE UNA LISTA USANDO EL MÉTODO GET 2
3. RECORRIDO DE UNA LISTA USANDO ITERADORES 4
4. RECORRIDO DE UNA LISTA USANDO LA INSTRUCCIÓN FOR-EACH 5
5. ELIMINACIÓN DE ELEMENTOS DE UNA LISTA USANDO ITERADORES 6
EN RESUMEN 6
PARA TENER EN CUENTA 6
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
1. DEFINICIÓN
Considerando una colección como una agrupación de elementos del mismo tipo, se puede
definir un iterador como un objeto responsable de visitar los objetos de una colección en
cierto orden preestablecido, sin que el programador tenga que preocuparse por su
estructura interna. La interfaz Iterator<E> representa en Java un iterador de elementos de
tipo E, ofreciéndonos los siguientes servicios:
Mientras se está desarrollando una iteración sobre una colección, se debe evitar modificar el
contenido de ésta con métodos distintos al remove de la interfaz Iterator<E>, para no
obtener resultados inesperados como errores de ejecución o pérdida de datos.
Lo más natural que muchos nos imaginamos para lograr pasar por todos los elementos de
una lista es un ciclo que inspeccione cada una de sus posiciones, desde la posición hasta la
posición , donde es el tamaño de la lista:
El gran problema de esta forma de recorrer es que es muy ineficiente si la lista está
implementada con nodos encadenados. Habíamos estudiado que el método get, con el que
consultamos el elemento que está en cierta posición de la lista, tiene complejidad temporal
en la implementación de listas con vectores y tiene complejidad temporal en la
implementación de listas con nodos encadenados. Como el ciclo for (int i=0; i<n; i++)
realiza exactamente iteraciones, la complejidad temporal del anterior fragmento de código
sería multiplicado por la complejidad del método get. Por esta razón, si la lista está
implementada con nodos encadenados, el recorrido sobre la lista que usa reiterativamente el
ESTRUCTURAS DE DATOS 2
método get tendría complejidad temporal , lo que es completamente inaceptable si la
lista es demasiado grande. Para convencernos de esto, considere la siguiente prueba:
Código 3: Programa que mide el tiempo que se demora el recorrido de una lista encadenada usando el método get.
import java.util.*;
public class Ejemplo {
public static void main(String[] args) {
for (int n=10000; n<=100000; n+=10000) {
List<String> lista=new LinkedList<String>();
for (int i=0; i<n; i++) { // Llenar la lista con n elementos cualesquiera
lista.add("");
}
long tiempo1=System.currentTimeMillis();
for (int i=0; i<n; i++) { // Realizar el recorrido de forma ineficiente
String elementoActual=lista.get(i);
}
long tiempo2=System.currentTimeMillis();
long demora=tiempo2-tiempo1;
System.out.println("Demora iterando una lista encadenada de tamaño "+n+": "+
demora+" milisegundos");
System.gc(); // Invocar al recolector de basura
}
}
}
Gráfica 4: Tamaño de la lista encadenada ( ) versus el tiempo consumido para recorrerla, usando el método get.
Observe que la gráfica que relaciona el tamaño de la lista con el consumo de tiempo refleja
una función cuadrática, lo que nos confirma que el recorrido de una lista encadenada usando
el método get tiene complejidad temporal . Cambiando LinkedList<String> por
ArrayList<String> obtenemos tiempos mucho menores que exhiben una tendencia lineal
( ) y no cuadrática ( ), incluso si aumentamos la cantidad de datos de la muestra de
cien mil a diez millones.
ESTRUCTURAS DE DATOS 3
Gráfica 5: Tamaño de la lista implementada con vectores ( ) versus el tiempo
consumido para recorrerla, usando el método get.
La forma más adecuada y eficiente de recorrer una lista es a través de un iterador, que nos
ofrece un mecanismo para pasar ordenadamente por todos sus elementos, con complejidad
temporal , sin importar si la lista está implementada con vectores o con nodos
encadenados.
ESTRUCTURAS DE DATOS 4
Tabla 8: Ejemplos sobre recorridos de listas usando iteradores.
Ejemplo Descripción
List<Integer> lista=new
ArrayList<Integer>();
lista.addAll(Arrays.asList(17,25,19,20));
Iterator<Integer>
iterador=lista.iterator();
Imprime en consola los elementos de la lista
while (iterador.hasNext()) {
Integer elementoActual=iterador.next();
System.out.println(elementoActual);
}
List<Integer> lista=new
ArrayList<Integer>();
lista.addAll(Arrays.asList(17,25,19,20));
int r=0;
Iterator<Integer> Imprime en consola el resultado de la suma de
iterador=lista.iterator(); los elementos de la lista que es
while (iterador.hasNext()) {
exactamente .
Integer elementoActual=iterador.next();
r+=elementoActual;
}
System.out.println(r);
En Java, la iteración
Iterator<E> iterador=lista.iterator();
while (iterador.hasNext()) {
E elementoActual=iterador.next();
// Procesar el elemento actual como sea conveniente:
// ...
}
se puede abreviar por medio de una instrucción denominada for-each:
for (E elementoActual:lista) {
// Procesar el elemento actual como sea conveniente:
// ...
}
ESTRUCTURAS DE DATOS 5
5. ELIMINACIÓN DE ELEMENTOS DE UNA LISTA USANDO
ITERADORES
Usando el método remove() de la interfaz Iterator<E> se pueden eliminar de una lista los
elementos que cumplen cierta condición dada.
Código 10: Remoción de los valores de una lista que cumplen determinada condición.
Iterator<E> iterador=lista.iterator();
while (iterador.hasNext()) {
E elementoActual=iterador.next();
if (condición) iterador.remove();
}
Tabla 11: Ejemplos sobre eliminación de elementos en las listas usando iteradores.
Ejemplo Descripción
public static void f01(List<Integer>
lista) {
Iterator<Integer> it=lista.iterator();
while (it.hasNext()) { Función que elimina de la lista dada todos los
int x=it.next(); elementos cuyo valor sea .
if (x==5) it.remove();
}
}
public static void f02(List<Integer>
lista) {
Iterator<Integer> it=lista.iterator();
while (it.hasNext()) { Función que elimina de la lista dada todos los
int x=it.next(); elementos cuyo valor esté entre y .
if (x>=5&&x<=8) it.remove();
}
}
EN RESUMEN
Los iteradores nos proveen una forma eficiente de visitar todos los elementos de una lista.
ESTRUCTURAS DE DATOS 6
ESTRUCTURAS DE DATOS
UNIDAD DOS - SEMANA CUATRO
OTRAS ESTRUCTURAS DE DATOS LINEALES *
TABLA DE CONTENIDO
1. LISTAS ORDENADAS 2
2. PILAS 2
3. COLAS 4
4. COLAS DE PRIORIDAD 6
5. APLICACIONES 9
5.1. EVALUACIÓN DE EXPRESIONES EN NOTACIÓN INFIJA 9
5.2. EVALUACIÓN DE EXPRESIONES EN NOTACIÓN POSFIJA (NOTACIÓN POLACA INVERSA) 10
EN RESUMEN 11
PARA TENER EN CUENTA 11
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
1. LISTAS ORDENADAS
Una lista ordenada es una lista sin elementos repetidos que se almacenan de menor a mayor
según cierto criterio de ordenamiento.
Ejemplos:
es una lista ordenada de enteros.
es una lista ordenada de cadenas de texto según el orden del
diccionario.
es una lista ordenada de cadenas de texto que representan
números romanos, según el orden establecido por la numeración romana (I, II, III, IV, V, VI,
VII, VIII, IX, X, …)
Para mantener ordenada una lista es necesario modificar las operaciones, garantizando que
no se inserten elementos repetidos y que siempre queden almacenados los elementos en
orden. Recuerde que, con el algoritmo de Búsqueda Binaria seríamos capaces de encontrar
un valor en una lista ordenada de tamaño , con una complejidad temporal de .
Se aconseja almacenar las listas ordenadas en un arreglo dinámico de tamaño variable en vez
de en una lista encadenada, porque la Búsqueda Binaria requiere que las consultas por
posición sean para que pueda ofrecernos la complejidad temporal de .
2. PILAS
Una pila (en inglés: stack) es una estructura de datos lineal que sólo permite inserciones y
eliminaciones en uno de los extremos, llamado tope. La clase Stack<E> de Java, que extiende
de la clase Vector<E>, representa una pila de elementos de tipo E, brindándonos las
siguientes operaciones:
ESTRUCTURAS DE DATOS 2
†
Gráfica 2: Ejemplos de pilas en la vida real .
Pila de discos en el
Pila de libros Pila de monedas Pila de piedras
juego de Hanoi
Como todo elemento en una pila puede ser añadido o eliminado sólo por el tope, se cumple
que el último elemento que se inserta es el primer elemento que puede eliminarse, razón
por la que se dice que esta estructura de datos satisface que el último que entra es el primero
que sale (Last-in First-Out en inglés, abreviado con las siglas LIFO).
tope
...
Gracias a que la clase Stack<E> extiende de la clase Vector<E>, las pilas en Java están
implementadas con arreglos dinámicos de tamaño variable (vectores).
Gráfica 4: Representación interna de las pilas en Java, a través de arreglos dinámicos de tamaño variable.
Stack<E>
extiende
Vector<E>
0 1 2 n-1 n t-1
arreglo ... ...
tamanho
casillas ocupadas casillas de reserva
tope
Estando el tope en la última posición del vector, las operaciones push, pop y peek tienen
complejidad temporal , porque las inserciones, eliminaciones y consultas al final de una
†
La segunda y tercera imagen se descargaron de http://www.public-domain-photos.com/ y la cuarta ilustración
pertenece a la galería de imágenes prediseñadas de Microsoft Office. JFreeChart Samples, © 2005-2009 Object
Refinery Limited (recuperado en noviembre de 2010).
ESTRUCTURAS DE DATOS 3
lista implementada con arreglos son todas, con la salvedad de que la complejidad de las
inserciones se vuelve cuando hay que crecer el tamaño del arreglo.
Tabla 5: Evolución de una pila de cadenas de texto tras una secuencia de operaciones de ejemplo.
Estado de la pila Operación Descripción
Stack<String> pila
=new Stack<String>(); Crear una pila vacía.
pila.push("Luz"); Agregar Luz en el tope de la pila.
pila.push("Mar"); Agregar Mar en el tope de la pila.
pila.push("Sol"); Agregar Sol en el tope de la pila.
pila.pop(); Eliminar el tope de la pila.
pila.push("Mes"); Agregar Mes en el tope de la pila.
pila.push("Año"); Agregar Año en el tope de la pila.
pila.pop(); Eliminar el tope de la pila.
pila.pop(); Eliminar el tope de la pila.
pila.pop(); Eliminar el tope de la pila.
pila.push("Día"); Agregar Día en el tope de la pila.
pila.push("Luz"); Agregar Luz en el tope de la pila.
pila.pop(); Eliminar el tope de la pila.
pila.pop(); Eliminar el tope de la pila.
pila.pop(); Eliminar el tope de la pila.
3. COLAS
Una cola (en inglés: queue, pronunciado kiu) es una estructura de datos lineal que permite
inserciones en un extremo (llamado cola) y eliminaciones del otro extremo (llamado cabeza).
La interfaz Queue<E> de Java, implementada por la clase LinkedList<E>, representa una cola
de elementos de tipo E, brindándonos las siguientes operaciones:
ESTRUCTURAS DE DATOS 4
‡
Gráfica 7: Cola de personas .
Como todo elemento en una cola puede ser insertado sólo en la cola y eliminado sólo de la
cabeza, se cumple que el primer elemento que se inserta es el primer elemento que puede
eliminarse, razón por la que se dice que esta estructura de datos satisface que el primero que
entra es el primero que sale (First-in First-Out en inglés, abreviado con las siglas FIFO).
...
eliminación
(poll)
Gráfica 9: Representación interna de las colas en Java, a través de nodos doblemente encadenados.
Queue<E>
implementa
LinkedList<E>
header ...
tamanho
cabeza cola
‡
La ilustración pertenece a la galería de imágenes prediseñadas de Microsoft Office.
ESTRUCTURAS DE DATOS 5
Estando la cola en la última posición de la lista y la cabeza en la primera, las operaciones
offer, poll y peek tienen complejidad temporal , porque las inserciones, eliminaciones y
consultas tanto al principio como al final de una lista implementada con nodos doblemente
encadenados son todas. Observe que no sería muy apropiado implementar colas con
arreglos dinámicos de tamaño variable de tipo ArrayList<E> porque las inserciones y
eliminaciones al principio del arreglo tienen complejidad .
Tabla 10: Evolución de una cola de cadenas de texto tras una secuencia de operaciones de ejemplo.
Estado de la cola Operación Descripción
Queue<String> cola
=new LinkedList<String>(); Crear una cola vacía.
cola.offer("Luz"); Agregar Luz en la cola.
cola.offer("Mar"); Agregar Mar en la cola.
cola.offer("Sol"); Agregar Sol en la cola.
cola.poll(); Eliminar la cabeza de la cola.
cola.offer("Mes"); Agregar Mes en la cola.
cola.offer("Año"); Agregar Año en la cola.
cola.poll(); Eliminar la cabeza de la cola.
cola.poll(); Eliminar la cabeza de la cola.
cola.poll(); Eliminar la cabeza de la cola.
cola.offer("Día"); Agregar Día en la cola.
cola.offer("Luz"); Agregar Luz en la cola.
cola.poll(); Eliminar la cabeza de la cola.
cola.poll(); Eliminar la cabeza de la cola.
cola.poll(); Eliminar la cabeza de la cola.
4. COLAS DE PRIORIDAD
Recurso como proyecto en Eclipse: ColasDePrioridad.zip.
Una cola de prioridad es una cola donde los elementos son eliminados según el orden
establecido por cierto criterio de prioridad. La clase PriorityQueue<E> de Java, que
implementa la interfaz Queue<E>, representa una cola de prioridad de elementos de tipo E,
brindándonos las siguientes operaciones:
Tabla 11: Algunos métodos de la clase PriorityQueue<E>, que implementa la interfaz Queue<E>.
Método Descripción
boolean offer(E item)
Inserta en la cola el ítem suministrado, con la prioridad que éste
tenga definida. Retorna verdadero en caso de éxito.
Elimina y retorna el objeto de la cola que tiene la mayor prioridad
E poll()
para ser atendido entre todos los elementos.
E peek()
Retorna el objeto de la cola que tiene la mayor prioridad para ser
atendido entre todos los elementos.
ESTRUCTURAS DE DATOS 6
En la clase PriorityQueue<E>, los métodos offer y poll tienen complejidad , y el
método peek tiene complejidad . Internamente, PriorityQueue administra los elementos
almacenándolos en un arreglo de una forma tal que se garantice eficiencia al momento de
insertar y eliminar elementos de la cola (específicamente es un priority heap, que no
trataremos en las lecturas).
Para conocer la utilidad de esta estructura de datos, considere el siguiente ejemplo: todos
sabemos que los enfermos en un hospital deben ser atendidos según la prioridad que
representen sus heridas; por ejemplo, un herido de muerte tiene más prioridad que una
persona con dolor de cabeza. La siguiente clase modela los enfermos del hospital:
El atributo nombre almacena los nombres y apellidos del enfermo, y el atributo prioridad es
un puntaje que indica qué tanta urgencia tiene el enfermo para ser atendido, donde mayor
puntaje significa mayor prioridad. Para que una cola de prioridad sepa que la persona con
mayor prioridad entre todas es la que primero debe ser atendida, es necesario que la clase
Enfermo implemente la interfaz Comparable<Enfermo>.
Código 13: Clase para representar enfermos, que implementa la interfaz Comparable.
public class Enfermo implements Comparable<Enfermo> {
private String nombre;
private int prioridad;
public Enfermo(String pNombre, int pPrioridad) {
nombre=pNombre;
prioridad=pPrioridad;
}
public String getNombre() {
return nombre;
}
public int getPrioridad() {
return prioridad;
}
public int compareTo(Enfermo pEnfermo) {
Enfermo f=this,g=pEnfermo;
if (f.getPrioridad()>g.getPrioridad()) {
return -1;
}
else if (f.getPrioridad()<g.getPrioridad()) {
return 1;
ESTRUCTURAS DE DATOS 7
}
else {
return 0;
}
}
}
Código 14: Ejemplo que ilustra el uso de las colas de prioridad para atender elementos según su urgencia.
import java.util.*;
public class Hospital {
public static void main(String[] args) {
PriorityQueue<Enfermo> c=new PriorityQueue<Enfermo>();
c.add(new Enfermo("Juan Pérez",5)); // Dolor de barriga
c.add(new Enfermo("Andrea Sánchez",3)); // Dolor de cabeza medio
c.add(new Enfermo("Óscar Muñoz",1)); // Dolor de cabeza leve
c.add(new Enfermo("Juliana Ortiz",7)); // Paro respiratorio
c.add(new Enfermo("Pedro Ríos",6)); // Dolor de cabeza fuerte
c.add(new Enfermo("María Ruiz",2)); // Cólicos
c.add(new Enfermo("Andrés Rodríguez",5)); // Dolor de barriga
c.add(new Enfermo("Eliana Guzmán",8)); // Infarto cardiaco
c.add(new Enfermo("Fredy Olarte",3)); // Dolor de cabeza medio
c.add(new Enfermo("Jorge Ramos",1)); // Dolor de cabeza leve
while (!c.isEmpty()) {
Enfermo e=c.poll();
System.out.println("Prioridad: "+e.getPrioridad()+". Enfermo: "+e.getNombre());
}
}
}
ESTRUCTURAS DE DATOS 8
5. APLICACIONES
Se dice que una expresión como la anterior está en notación infija, pues los operadores se
ubican en medio de los operandos. Además, toda operación se coloca entre paréntesis para
facilitar su procesamiento.
Dada una expresión en notación infija, almacenada en una cadena de texto, el siguiente
algoritmo es capaz de evaluar la expresión, dando el resultado numérico de realizar las
operaciones:
ESTRUCTURAS DE DATOS 9
}
public static double evaluarInfijo(String expresion) {
Stack<String> pila=new Stack<String>();
StringTokenizer tokenizer=new StringTokenizer(expresion,"()+-*/",true);
while (tokenizer.hasMoreTokens()) {
String token=tokenizer.nextToken();
char c=token.charAt(0);
if (c=='(') { // Paréntesis de apertura
// Se descarta
}
else if (c=='+'||c=='-'||c=='*'||c=='/') { // Operador
pila.push(token);
}
else if (c!=')') { // Número
pila.push(token);
}
else { // Paréntesis de cierre
String operando2=pila.pop();
String operador=pila.pop();
String operando1=pila.pop();
double f=Double.parseDouble(operando1),g=Double.parseDouble(operando2);
if (operador.equals("+")) {
pila.push(String.valueOf(f+g));
}
else if (operador.equals("-")) {
pila.push(String.valueOf(f-g));
}
else if (operador.equals("*")) {
pila.push(String.valueOf(f*g));
}
else if (operador.equals("/")) {
pila.push(String.valueOf(f/g));
}
}
}
return Double.parseDouble(pila.peek());
}
}
Una expresión en notación posfija (también llamada notación polaca inversa) ubica los
operadores al final:
Investigue sobre la notación posfija, sobre cómo se evalúan estas expresiones, y analice el
programa ExpresionesPosfijas.java, que se incluye dentro del proyecto en Eclipse bajo el
recurso Expresiones.zip.
ESTRUCTURAS DE DATOS 10
EN RESUMEN
Una lista ordenada es una lista sin elementos repetidos que se almacenan de menor a mayor según
cierto criterio de ordenamiento.
Una pila es una estructura de datos lineal que sólo permite inserciones y eliminaciones en uno de los
extremos, llamado tope. En la librería estándar de Java, la clase Stack<E> representa pilas.
Una cola es una estructura de datos lineal que permite inserciones en un extremo (llamado cola) y
eliminaciones del otro extremo (llamado cabeza). En la librería estándar de Java, la interfaz Queue<E>
representa colas.
Una cola de prioridad es una cola donde los elementos son eliminados según el orden establecido por
cierto criterio de prioridad. En la librería estándar de Java, la clase PriorityQueue<E> representa
colas de prioridad.
ESTRUCTURAS DE DATOS 11
ESTRUCTURAS DE DATOS
UNIDAD DOS - SEMANA CUATRO
EJERCICIOS PROPUESTOS *
1.1. Declare la clase VEDLinkedListD<E>, que implementa listas con nodos doblemente
encadenados con apuntador al primero y al último:
public class VEDLinkedListD<E> implements VEDList<E> { // Declaración de la clase
// **********************************
// * Clase que representa los nodos *
// **********************************
private static class VEDNodoD<E> {
E val=null; // Valor del nodo
VEDNodoD<E> ant=null; // Nodo anterior
VEDNodoD<E> sig=null; // Nodo siguiente
VEDNodoD(E pVal) { // Constructor de la clase nodo
val=pVal;
}
}
// *************************
// * Atributos de la clase *
// *************************
// Nodo que guarda el primer elemento de la lista:
private final VEDNodoD<E> primero=null;
// Nodo que guarda el último elemento de la lista:
private final VEDNodoD<E> ultimo=null;
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
// Tamaño de la lista (inicia en 0).
private int tamanho=0;
// ***********************
// * Métodos de la clase *
// ***********************
// Constructor de la lista vacía:
public VEDLinkedListD() {
}
}
1.2. Implemente el método clear(), que elimina todos los nodos de la lista.
1.3. Implemente el método privado getNodo(int index), que retorna el nodo que almacena
el elemento que se encuentra en la posición index de la lista, con las siguientes
consideraciones:
Siga el esquema:
private VEDNodoD<E> getNodo(int index) {
if (index<tamanho/2) {
VEDNodoD<E> x=primero;
for (...) { // Falta llenar el for (...)
x=x.sig;
}
return x;
}
else {
VEDNodoD<E> x=ultimo;
for (...) { // Falta llenar el for (...)
x=x.ant;
}
return x;
}
}
Cree un nodo “nuevo” cuyo valor sea “element”, cuyo anterior sea null y cuyo siguiente sea
null (en código: VEDNodoD<E> nuevo=new VEDNodoD<E>(element);).
Si la lista tiene tamaño 0 (o sea, si la inserción se efectúa sobre una lista vacía):
o Asigne al atributo “primero” el nodo “nuevo” (en código: primero=nuevo;).
ESTRUCTURAS DE DATOS 2
o Asigne al atributo “ultimo” el nodo “nuevo” (en código: ultimo=nuevo;).
o Incremente el tamaño de la lista en (en código: tamanho++;).
De lo contrario, si index es 0 (o sea, si la inserción se efectúa al principio de una lista no
vacía):
o Ponga el siguiente del nodo “nuevo” apuntando al nodo “primero” (en código: …).
o Ponga el anterior del nodo “primero” apuntando al nodo “nuevo” (en código: …).
o Asigne al atributo “primero” el nodo “nuevo” (en código: primero=nuevo;).
o Incremente el tamaño de la lista en (en código: tamanho++;).
De lo contrario, si index es tamanho (o sea, si la inserción se efectúa al final de una lista no
vacía):
o Ponga el anterior del nodo “nuevo” apuntando al nodo “ultimo” (en código: …).
o Ponga el siguiente del nodo “ultimo” apuntando al nodo “nuevo” (en código: …).
o Asigne al atributo “ultimo” el nodo “nuevo” (en código: ultimo=nuevo;).
o Incremente el tamaño de la lista en (en código: tamanho++;).
De lo contrario (si la inserción se efectúa en medio de dos nodos):
o Con la ayuda de la función getNodo, obtenemos un apuntador “b” al nodo que almacena el
elemento de la posición index, y declaramos “a” como un apuntador al nodo anterior del
nodo “b”.
o Ponga el anterior del nodo “nuevo” en el nodo “a” y el siguiente del nodo “nuevo” en el
nodo “b”.
o Ponga el siguiente del nodo “a” en el nodo “nuevo” y el anterior del nodo “b” en el nodo
“nuevo”.
o Incremente el tamaño de la lista en (en código: tamanho++;).
ESTRUCTURAS DE DATOS 3
De lo contrario, si index es tamanho (o sea, si la eliminación remueve el último elemento de
la lista):
o Ponga el atributo “ultimo” en el anterior del último nodo (en código:
ultimo=ultimo.ant;).
o Ponga el siguiente del “ultimo” en null (en código: ultimo.sig=null;).
o Decrezca el tamaño de la lista en (en código: tamanho--;).
De lo contrario (si la eliminación se efectúa en medio de dos nodos):
o Con la ayuda de la función getNodo, obtenemos un apuntador “b” al nodo que almacena el
elemento de la posición index, declaramos “a” como un apuntador al nodo anterior del
nodo “b”, y declaramos “c” como un apuntador al nodo siguiente del nodo “b”.
o Ponga el siguiente del nodo “a” en el nodo “c” y el anterior del nodo “c” en el nodo “a”.
o Decrezca el tamaño de la lista en (en código: tamanho--;).
1.8. ¿Por qué su implementación del método add funciona? Explique claramente caso por
caso, usando dibujos.
1.9. ¿Por qué su implementación del método remove funciona? Explique claramente caso por
caso, usando dibujos.
1.11. ¿Por qué será más sencillo implementar una lista doblemente encadenada en anillo con
encabezado?
Considere los siguientes cuatro métodos que eliminan los números pares de una lista:
public static void eliminarValoresParesVersion1(List<Integer> p) {
for (int i=0; i<p.size(); i++) {
if (p.get(i).intValue()%2==0) p.remove(i--);
}
}
public static void eliminarValoresParesVersion2(List<Integer> p) {
List<Integer> q=new LinkedList<Integer>(); // Posiciones a eliminar
for (int i=0; i<p.size(); i++) {
if (p.get(i).intValue()%2==0) q.add(0,i);
}
for (int j=0; j<q.size(); j++) {
p.remove(q.get(j).intValue());
}
}
public static void eliminarValoresParesVersion3(List<Integer> p) {
for (Iterator<Integer> it=p.iterator(); it.hasNext(); ) {
if (it.next().intValue()%2==0) it.remove();
}
}
ESTRUCTURAS DE DATOS 4
public static void eliminarValoresParesVersion4(List<Integer> p) {
for (int x:p) {
if (x%2==0) p.remove(x);
}
}
2.2. Explique detalladamente por qué la versión 2 no funciona si se cambia “ q.add(0,i)” por
“q.add(i)”.
2.3. Justifique claramente por qué las versiones 1, 2 y 3 funcionan (sea cuidadoso con la
versión 3).
2.7. ¿Qué implementación es mejor? ¿Por qué? ¿Qué criterios tiene en cuenta?
3. ITERADORES
4. PALÍNDROMOS
ESTRUCTURAS DE DATOS 5
Cree una lista vacía q instanciando una implementación adecuada que ayude a lograr la
complejidad deseada (debe escoger entre ArrayList<E> y LinkedList<E>).
Cree un iterador it sobre la lista p.
Construya un ciclo que usando el iterador it visite todos los elementos de la lista p. Por
cada elemento iterado, añádalo al principio de la lista q.
Cree un iterador itP sobre la lista p y un iterador itQ sobre la lista q.
Itere en paralelo ambas listas: mientras los iteradores itP e itQ tengan un siguiente
elemento por visitar:
o Guarde en una variable elemP de tipo E el siguiente elemento entregado por el iterador
itP.
o Guarde en una variable elemQ de tipo E el siguiente elemento entregado por el iterador
itQ.
o Si elemP no es igual a elemQ, retorne false.
Retorne true.
4.3. Explique por qué el método tiene complejidad temporal tanto para vectores como
para listas encadenadas.
ESTRUCTURAS DE DATOS 6
GUÍA DE COMPETENCIAS Y ACTIVIDADES
SEMANA 5
TEMA (S): NÚCLEO TEMÁTICO CINCO: ÁRBOLES
1 [ POLITÉCNICO GRANCOLOMBIANO ]
5- Plantear, diseñar e implementar Sistemas de Tecnología, Información y Telecomunicaciones
capaces de resolver una problemática en un contexto dado, con restricciones identificadas y
recursos definidos.
6- Identificar diferentes fenómenos y leyes físicas en el comportamiento de Sistemas de
Tecnología, Información y Telecomunicaciones, y analizar su influencia en el funcionamiento de
los mismos.
7- Explorar diferentes fuentes de información y conocimiento para el aprendizaje de diferentes
Tecnologías de Información y Telecomunicaciones.
[ ESTRUCTURA DE DATOS ] 2
SEMANA 6
TEMA (S): NÚCLEO TEMÁTICO CINCO: ÁRBOLES
3 [ POLITÉCNICO GRANCOLOMBIANO ]
7- Explorar diferentes fuentes de información y conocimiento para el aprendizaje de diferentes
Tecnologías de Información y Telecomunicaciones.
[ ESTRUCTURA DE DATOS ] 4
ESTRUCTURAS DE DATOS
UNIDAD TRES - SEMANA CINCO
INTRODUCCIÓN A LOS ÁRBOLES *
TABLA DE CONTENIDO
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
1. LOS ÁRBOLES COMO ESTRUCTURAS DE DATOS
Un árbol es una estructura jerárquica conformada por nodos relacionados entre sí, donde:
1. Cada nodo puede tener uno o más nodos que cumplen el rol de hijos.
2. Todo nodo excepto la raíz tiene un nodo que cumple el rol de padre.
3. El nodo raíz no tiene padre.
Raíz A
B C D
E F G
En el árbol de ejemplo, el nodo A tiene tres hijos (B, C, D), el nodo B tiene dos hijos (E, F), el
nodo D tiene un hijo (G) y los nodos E, F, C, y G no tienen hijos. Además, la raíz del árbol es el
nodo A.
Naturalmente, los árboles sirven para representar datos organizados de forma jerárquica,
como organigramas y mapas conceptuales.
2.1.1. ORGANIGRAMAS
ESTRUCTURAS DE DATOS 2
Gráfica 2: Organigrama que explica una pequeña parte de la estructura del estado colombiano.
Estado
Gráfica 3: Mapa conceptual que relaciona conceptos dispersos a partir del término árbol.
Árbol
tiene
En un mapa conceptual, es posible que dos nodos que no sean padre-hijo se relacionen entre
sí, violando la definición de árbol. Cuando esto suceda, el mapa conceptual se convierte en
una estructura de datos más general, llamada grafo, que estudiaremos más adelante.
ESTRUCTURAS DE DATOS 3
2.1.3. ÁRBOLES GENEALÓGICOS
Un árbol genealógico es un diagrama que describe a través de un árbol los descendientes y/o
los ancestros de una persona, exhibiendo explícitamente su linaje. Cada enlace entre nodos
del árbol representa una relación que significa ser hijo de.
Gráfica 4: Árbol genealógico ficticio de Pedro, mostrando sólo miembros masculinos de su descendencia.
Pedro
Gráfica 5: Árbol filogenético que muestra únicamente los dominios y los reinos.
Vida
ESTRUCTURAS DE DATOS 4
Gráfica 7: Árbol de sintaxis de la expresión aritmética .
+
* * –
– y 3 x +
5 x z 7
Gráfica 8: Parte del árbol de subdirectorios de la carpeta de instalación de Java en el sistema operativo Windows.
Un árbol de juego es un árbol que presenta los distintos movimientos que se pueden realizar
en un determinado juego, dependiendo del estado en el que éste se encuentre.
ESTRUCTURAS DE DATOS 5
Gráfica 9: Parte del árbol de juego para el Triqui (también conocido como Tres en Línea o Tic-Tac-Toe).
Los árboles también sirven para representar de forma compacta cierto tipo de información,
como cadenas de texto e imágenes en blanco y negro.
2.2.1. TRIES
Un Trie (pronunciado trai) es una estructura de datos utilizada para almacenar un conjunto
de palabras de forma compacta.
ESTRUCTURAS DE DATOS 6
2.2.2. QUADTREES
Gráfica 11: Varias imágenes en blanco y negro con su respectiva representación como Quadtrees.
Un árbol binario es un árbol donde todo nodo tiene máximo dos hijos: el hijo izquierdo y el
hijo derecho. Por otro lado, un árbol ordenado es un árbol binario donde cada nodo cumple
que su valor es mayor que todos los valores a su izquierda, y que es menor que todos los
valores a su derecha.
Hay varios tipos de árboles binarios ordenados que son capaces de soportar inserciones,
eliminaciones, consultas y modificaciones de elementos con complejidad temporal
, donde es la cantidad de datos: los árboles perfectamente balanceados, los
árboles AVL y los árboles Roji-negros.
ESTRUCTURAS DE DATOS 7
EN RESUMEN
Un árbol es una estructura jerárquica conformada por nodos relacionados entre sí, donde cada nodo
puede tener uno o más nodos que cumplen el rol de hijos, todo nodo excepto la raíz tiene un nodo
que cumple el rol de padre, y el nodo raíz no tiene padre.
Los árboles tienen muchísimas aplicaciones:
Representación de información: organigramas, mapas conceptuales, árboles genealógicos, árboles
filogenéticos, árboles de sintaxis, sistemas de archivos, árboles de juego, …
Representación eficiente de información: Tries, Quadtrees, …
Administración eficiente de información: árboles binarios ordenados, …
ESTRUCTURAS DE DATOS 8
ESTRUCTURAS DE DATOS
UNIDAD TRES - SEMANA CINCO
ÁRBOLES BINARIOS *
TABLA DE CONTENIDO
1. DEFINICIÓN 2
2. CONCEPTOS BÁSICOS 3
2.1. RELACIONES DE PARENTESCO ENTRE NODOS 3
2.2. HOJAS Y NODOS INTERNOS 3
2.3. CAMINOS Y RAMAS 4
2.4. DESCENDIENTES Y ANCESTROS 5
2.5. NIVEL 5
2.6. PESO Y ALTURA 5
2.7. ÁRBOLES DEGENERADOS, LLENOS, COMPLETOS Y PERFECTOS 6
3. IMPLEMENTACIÓN DE ÁRBOLES BINARIOS CON ÁRBOLES SENCILLAMENTE ENCADENADOS 7
3.1. CREACIÓN DE ÁRBOLES BINARIOS 9
3.2. MÉTODOS DE CONSULTA DE LOS ATRIBUTOS DE UN ÁRBOL BINARIO 10
3.3. MÉTODOS PARA CALCULAR EL PESO Y LA ALTURA DE UN ÁRBOL BINARIO 11
3.4. MÉTODOS DE BÚSQUEDA SOBRE UN ÁRBOL BINARIO 11
4. RECORRIDOS SOBRE ÁRBOLES BINARIOS 12
5. RECONSTRUCCIÓN DE UN ÁRBOL BINARIO A PARTIR DE SUS RECORRIDOS 15
5.1. RECONSTRUCCIÓN DE UN ÁRBOL BINARIO DADO SU INORDEN Y SU PREORDEN 16
EN RESUMEN 19
PARA TENER EN CUENTA 19
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
1. DEFINICIÓN
Un árbol binario es una estructura de datos recursiva que está compuesta por un valor,
llamado raíz, y por dos árboles, llamados subárbol izquierdo y subárbol derecho.
Formalmente:
1. El árbol que no posee ningún elemento, que representaremos gráficamente mediante el
símbolo , es un árbol binario denominado el árbol vacío.
2. Dado un valor val, y dos árboles binarios izq y der, se tiene que el árbol
val val : raíz
es un árbol binario cuya raíz es val, cuyo subárbol izquierdo es izq, y cuyo subárbol derecho
es der.
Observe que:
El árbol vacío no tiene raíz, no tiene subárbol izquierdo y no tiene subárbol derecho.
Todo árbol no vacío tiene raíz y tiene exactamente dos subárboles: el subárbol izquierdo y el
subárbol derecho. Es posible que uno o ambos subárboles sean vacíos.
El árbol vacío nunca puede ser considerado como nodo porque no es raíz de un árbol
binario no vacío.
ESTRUCTURAS DE DATOS 2
2. CONCEPTOS BÁSICOS
Las relaciones familiares se utilizan para nombrar relaciones típicas entre nodos:
ESTRUCTURAS DE DATOS 3
En otras palabras, un nodo es una hoja si sus dos subárboles son vacíos y es un nodo interno
si no es una hoja.
Un camino es una lista no vacía de nodos donde cada uno es padre del
que le sigue en la lista. La longitud de un camino es , o sea, el
número de nodos que enumera menos uno. Además, se define una rama como un camino
que parte de la raíz del árbol y llega a una hoja.
Siempre existe un camino de longitud que va desde un nodo hacia sí mismo, y siempre
existe un camino que va desde la raíz del árbol hasta cualquier nodo. A veces puede no existir
camino entre un par de nodos.
ESTRUCTURAS DE DATOS 4
2.4. DESCENDIENTES Y ANCESTROS
Tabla 8: Definición de ancestro y descendiente.
Relación Definición
Ancestro Un nodo es ancestro de un nodo si y sólo si hay camino que va de a .
Descendiente Un nodo es descendiente de un nodo si y sólo si es ancestro de .
2.5. NIVEL
El nivel de un nodo es la longitud del camino que va desde la raíz hasta el mismo nodo. Se
dice que un nivel está lleno si alberga la máxima cantidad posible de nodos que puede tener.
El nivel puede tener máximo un nodo, el nivel puede tener máximo dos nodos, el nivel
puede tener máximo cuatro nodos, el nivel puede tener máximo ocho nodos, y así
sucesivamente. En general, el nivel puede tener máximo nodos porque cada nivel aloja
máximo el doble de los nodos que el nivel anterior.
ESTRUCTURAS DE DATOS 5
Tabla 11: Ejemplos sobre el concepto de peso y altura.
Árbol binario Longitud de la rama más larga Peso ( ) Altura ( )
No tiene ramas.
Para todo árbol binario no vacío, su peso se puede calcular como uno más la suma de los
pesos de sus dos subárboles, y su altura se puede calcular como uno más el máximo entre las
alturas de sus dos subárboles. Dado un árbol con altura , el peso mínimo que puede tener
es y el peso máximo que puede tener es . Así mismo, dado un árbol con peso , la
altura mínima que puede tener es la parte entera de y la altura máxima que
puede tener es . ¿Por qué se cumple todo esto?
Tabla 12: Ejemplos que ilustran la máxima cantidad de nodos que pueden tener los árboles según su altura.
Árbol binario Peso ( ) Altura ( )
ESTRUCTURAS DE DATOS 6
Tabla 14: Ejemplos sobre los conceptos de árbol degenerado, árbol lleno, árbol completo y árbol perfecto.
Árbol binario ¿Degenerado? ¿Lleno? ¿Completo? ¿Perfecto?
SI SI SI SI
SI NO NO NO
NO SI NO NO
NO NO SI NO
NO SI SI NO
NO SI SI SI
La clase VEDArbin<E> representa un árbol binario de elementos de tipo E (Arbin abrevia Árbol
binario). Los atributos de la clase VEDArbin<E> son:
val: es una variable de tipo E que almacena el valor de la raíz del árbol.
izq: es una variable de tipo VEDArbin<E> que apunta al subárbol izquierdo del árbol.
der: es una variable de tipo VEDArbin<E> que apunta al subárbol derecho del árbol.
El árbol vacío se representa asignando null a los atributos val, izq y der, y los árboles no
vacíos se representan asignando valores distintos de null a los atributos val, izq y der, que
apuntan a la raíz, al subárbol izquierdo y al subárbol derecho, respectivamente.
Gráfica 15: Representación en memoria del árbol binario vacío, implementado con árboles sencillamente encadenados.
VEDArbin<E>
val
izq
der
ESTRUCTURAS DE DATOS 7
Gráfica 16: Representación en memoria del árbol binario no vacío,
implementado con árboles sencillamente encadenados.
VEDArbin<E>
val apunta a la raíz …
izq apunta al subárbol izquierdo …
der apunta al subárbol derecho …
En toda situación, los tres atributos tienen el valor null (cuando el árbol es vacío) o los tres
atributos tienen valores distintos de null (cuando el árbol no es vacío). Entonces, bajo la
estructura de datos, para diferenciar un árbol vacío de uno no vacío basta inspeccionar el
atributo val: si val es null es porque el árbol es vacío, y si val no es null es porque el árbol
no es vacío.
Gráfica 17: Representación interna de un árbol binario, implementado con un árbol sencillamente encadenado.
VEDArbin<E>
val 8
izq
árbol der
VEDArbin<E> VEDArbin<E>
val 5 val 3
izq izq
der der
ESTRUCTURAS DE DATOS 8
3.1. CREACIÓN DE ÁRBOLES BINARIOS
La clase VEDArbin<E> tiene dos métodos constructores: uno para crear un árbol vacío y otro
para crear un árbol no vacío a partir de su raíz, de su subárbol izquierdo y de su subárbol
derecho.
VEDArbin<E> provee métodos para exportar un árbol como texto o como imagen.
Específicamente,
System.out.println(arb.toString());
imprime en consola la representación textual del árbol arb según cierto formato que
definiremos más adelante, y la instrucción
arb.exportarComoImagen("ejemplo.png");
es capaz de exportar una imagen en formato .png con una representación gráfica del árbol
arb.
Tabla 20: Ejemplos que ilustran cómo construir árboles y cómo transformarlos en texto e imagen.
Imagen exportada
Programa
y texto impreso
VEDArbin<Integer> vacio=new VEDArbin<Integer>();
System.out.println(vacio.toString());
vacio.exportarComoImagen("ejemploCreacion01.png"); Ø
VEDArbin<Integer> vacio=new VEDArbin<Integer>();
VEDArbin<Integer> arb5=new VEDArbin<Integer>(5,vacio,vacio);
System.out.println(arb5.toString());
arb5.exportarComoImagen("ejemploCreacion02.png"); [5:Ø,Ø]
ESTRUCTURAS DE DATOS 9
VEDArbin<Integer> vacio=new VEDArbin<Integer>();
VEDArbin<Integer> arb89=new VEDArbin<Integer>(89,vacio,vacio);
VEDArbin<Integer> arb23=new VEDArbin<Integer>(23,vacio,arb89);
VEDArbin<Integer> arb10=new VEDArbin<Integer>(10,vacio,vacio);
VEDArbin<Integer> arb98=new VEDArbin<Integer>(98,arb23,arb10);
System.out.println(arb98.toString());
arb98.exportarComoImagen("ejemploCreacion03.png"); [98:[23:Ø,[89:Ø,Ø]],[10:Ø,Ø]]
VEDArbin<String> vacio=new VEDArbin<String>();
VEDArbin<String> arbA=new VEDArbin<String>("LUZ",vacio,vacio);
VEDArbin<String> arbB=new VEDArbin<String>("FÉ",vacio,arbA);
VEDArbin<String> arbC=new VEDArbin<String>("SOL",arbB,vacio);
System.out.println(arbC.toString());
arbC.exportarComoImagen("ejemploCreacion04.png");
[SOL:[FÉ:Ø,[LUZ:Ø,Ø]],Ø]
Código 22: Método para consultar el subárbol izquierdo del árbol binario.
public VEDArbin<E> getIzq() {
return izq; // Retornar el subárbol izquierdo.
}
Código 23: Método para consultar el subárbol derecho del árbol binario.
public VEDArbin<E> getDer() {
return der; // Retornar el subárbol derecho.
}
Código 24: Método para informar si el árbol binario es vacío o no, con complejidad temporal .
public boolean esVacio() {
if (val==null) { // Si la raíz es null:
return true; // Retornar true porque este árbol representa el árbol vacío.
}
else { // Si la raíz no es null:
return false; // Retornar false porque este árbol no representa el árbol vacío.
}
}
Código 25: Método para determinar si un nodo es una hoja o no, con complejidad temporal .
public boolean esHoja() {
if (esVacio()) { // Si este árbol es vacío:
return false; // Decir que este árbol no representa una hoja.
}
else { // Si este árbol no es vacío:
if (izq.esVacio()&&der.esVacio()) { // Si los dos subárboles son vacíos:
return true; // Decir que este árbol sí representa una hoja.
}
else { // Si alguno de los dos subárboles no es vacío:
return false; // Decir que este árbol no representa una hoja.
}
}
}
ESTRUCTURAS DE DATOS 10
3.3. MÉTODOS PARA CALCULAR EL PESO Y LA ALTURA DE UN ÁRBOL BINARIO
Código 26: Función que calcula el peso del árbol.
public int peso() {
if (esVacio()) { // Si este árbol es vacío:
return 0; // El peso del árbol vacío es 0.
}
else { // Si este árbol no es vacío:
// El peso de un árbol no vacío es uno más el peso del subárbol izquierdo más el
// peso del subárbol derecho:
return 1+izq.peso()+der.peso();
}
}
Para determinar el número de veces que aparece un valor dentro del árbol binario, se deben
inspeccionar todos los nodos del árbol, comparándolos contra el valor buscado.
ESTRUCTURAS DE DATOS 11
// Retornar uno más el número de ocurrencias en izq y en der:
return 1+contadorIzq+contadorDer;
}
else { // Si el objeto buscado no es igual a la raíz del árbol:
// Retornar el número de ocurrencias en izq y en der:
return contadorIzq+contadorDer;
}
}
}
El algoritmo tiene complejidad temporal porque procesa exactamente una vez cada
nodo.
Un recorrido de un árbol binario es un proceso que visita exactamente una vez cada uno de
los nodos del árbol, en cierto orden determinado. Existen muchas formas de recorrer un
árbol binario, entre estas:
Tabla 29: Resumen de los cuatro principales recorridos sobre árboles binarios.
Recorrido Esquema Descripción
Visita primero la raíz del árbol, luego recorre el subárbol
En preorden val-[izq]-[der] izquierdo en preorden, y después recorre el subárbol derecho
en preorden.
Recorre el subárbol izquierdo en inorden, luego visita la raíz del
En inorden [izq]-val-[der]
árbol, y después recorre el subárbol derecho en inorden.
Recorre el subárbol izquierdo en postorden, luego recorre el
En
[izq]-[der]-val subárbol derecho en postorden, y después visita la raíz del
postorden
árbol.
nivel0-…-nivelh- Visita los nodos nivel por nivel, desde el nivel hasta el nivel
Por niveles 1 , donde cada nivel se recorre de izquierda a derecha.
Todos los recorridos se pueden tratar como listas que enumeran los nodos del árbol en un
orden específico. Observe que los procesos descritos para hallar los recorridos en preorden,
en inorden y en postorden son recursivos, y que el proceso definido para el recorrido por
niveles es iterativo.
ESTRUCTURAS DE DATOS 12
Código 31: Método recursivo que halla el recorrido en preorden con complejidad temporal ,
donde es el peso del árbol.
public List<E> preorden() {
List<E> lista=new ArrayList<E>(); // Crear una lista nueva.
preorden(lista); // Alimentar la lista con el recorrido en preorden.
return lista; // Retornar la lista.
}
private void preorden(List<E> pLista) {
if (esVacio()) { // Si este árbol es vacío:
// No hacer ninguna operación.
}
else { // Si este árbol no es vacío:
pLista.add(val); // Añadir la raíz del árbol a la lista.
izq.preorden(pLista); // Recorrer el subárbol izquierdo en preorden.
der.preorden(pLista); // Recorrer el subárbol derecho en preorden.
}
}
Código 32: Método recursivo que halla el recorrido en inorden con complejidad temporal ,
donde es el peso del árbol.
public List<E> inorden() {
List<E> lista=new ArrayList<E>(); // Crear una lista nueva.
inorden(lista); // Alimentar la lista con el recorrido en inorden.
return lista; // Retornar la lista.
}
private void inorden(List<E> pLista) {
if (esVacio()) { // Si este árbol es vacío:
// No hacer ninguna operación.
}
else { // Si este árbol no es vacío:
izq.inorden(pLista); // Recorrer el subárbol izquierdo en inorden.
pLista.add(val); // Añadir la raíz del árbol a la lista.
der.inorden(pLista); // Recorrer el subárbol derecho en inorden.
}
}
Código 33: Método recursivo que halla el recorrido en postorden con complejidad temporal ,
donde es el peso del árbol.
public List<E> postorden() {
List<E> lista=new ArrayList<E>(); // Crear una lista nueva.
postorden(lista); // Alimentar la lista con el recorrido en postorden.
return lista; // Retornar la lista.
}
private void postorden(List<E> pLista) {
if (esVacio()) { // Si este árbol es vacío:
// No hacer ninguna operación.
}
else { // Si este árbol no es vacío:
izq.postorden(pLista); // Recorrer el subárbol izquierdo en postorden.
der.postorden(pLista); // Recorrer el subárbol derecho en postorden.
pLista.add(val); // Añadir la raíz del árbol a la lista.
}
}
ESTRUCTURAS DE DATOS 13
Para recorrer un árbol por niveles tenemos el siguiente algoritmo iterativo con complejidad
temporal :
1. Cree una cola vacía de árboles.
2. Inserte en la cola el árbol cuyo recorrido por niveles se desee hallar.
3. Mientras la cola no sea vacía:
3.1. Elimine el árbol que se encuentra en la cabeza de la cola. Llámese arb a este árbol.
3.2. Si el árbol arb no es vacío:
3.2.1. Visite la raíz del árbol arb.
3.2.2. Inserte en la cola el subárbol izquierdo del árbol arb.
3.2.3. Inserte en la cola el subárbol derecho del árbol arb.
Código 34: Método iterativo que halla el recorrido por niveles con complejidad temporal ,
donde es el peso del árbol.
public List<E> niveles() {
List<E> lista=new ArrayList<E>(); // Crear una lista nueva.
// Crear una nueva cola vacía para realizar el proceso:
Queue<VEDArbin<E>> cola=new LinkedList<VEDArbin<E>>();
cola.offer(this); // Añadir a la cola este árbol.
while (!cola.isEmpty()) { // Mientras la cola no sea vacía:
// Extraer la cabeza de la cola y guardarla en la variable 'actual':
VEDArbin<E> actual=cola.poll();
if (!actual.esVacio()) { // Si el árbol 'actual' no es vacío:
lista.add(actual.val); // Añadir a la lista la raíz del árbol 'actual'.
cola.offer(actual.izq); // Agregar a la cola el subárbol izquierdo de 'actual'.
cola.offer(actual.der); // Agregar a la cola el subárbol derecho de 'actual'.
}
}
return lista; // Retornar la lista.
}
¿Por qué funciona este algoritmo? El primer árbol que se añade a la cola es el árbol raíz, que
pertenece al nivel . Al sacar de la cola este árbol, se añaden a la cola sus subárboles, que
son precisamente los que pertenecen al nivel . Luego, para cada árbol del nivel , se saca de
la cabeza de la cola y se insertan en la cola sus subárboles, que son precisamente los que
pertenecen al nivel . Después, para cada árbol del nivel , se saca de la cabeza de la cola y
se insertan en la cola sus subárboles, que son precisamente los que pertenecen al nivel .
Siguiendo el proceso, descartando en cada paso los subárboles vacíos, se terminarían
visitando todos los nodos del árbol nivel por nivel.
ESTRUCTURAS DE DATOS 14
Tabla 36: Algoritmo para hallar manualmente el recorrido en inorden.
Ilustración representativa Algoritmo
Mentalmente, siga el rastro de la silueta del árbol
como muestra la ilustración. Cada vez que toque
un nodo por su parte inferior, añada su valor a
una lista. Los números dentro de las cajas rojas
muestran el orden en el que se van visitando los
elementos.
Recorrido en inorden del ejemplo:
Para poder reconstruir un árbol binario es necesario que todos sus elementos sean distintos,
porque de lo contrario, no se podría determinar de forma única la estructura del árbol.
ESTRUCTURAS DE DATOS 15
Gráfica 39: Cinco árboles binarios con elementos repetidos y con distinta forma, donde todos sus recorridos son iguales.
Además, es necesario tener el recorrido en inorden. El siguiente ejemplo muestra dos árboles
que tienen los mismos recorridos en preorden, en postorden y por niveles, pero diferente
recorrido en inorden:
Tabla 40: Dos árboles binarios tales que todos sus recorridos son iguales, excepto el inorden.
Recorrido
Árbol binario
Preorden Inorden Postorden Niveles
ESTRUCTURAS DE DATOS 16
2.1. Sea n el número de elementos del inorden, que coincide con el número de elementos
que tendrá el árbol que vamos a reconstruir.
2.2. Sea nuevoVal el primer elemento del recorrido en preorden, que es precisamente la raíz
del árbol.
2.3. Busque el valor nuevoVal en el inorden (sabemos que no aparece más de una vez porque
el árbol no tiene elementos repetidos). Sea pos la posición donde se encontró nuevoVal en el
inorden.
2.4. Aislamos el inorden del subárbol izquierdo, que es la sublista del inorden que va de la
posición 0 a la posición pos-1. Así mismo, aislamos el preorden del subárbol izquierdo, que es
la sublista del preorden que va de la posición 1 a la posición pos.
2.5. Llamamos recursivamente al algoritmo para reconstruir el subárbol izquierdo, según el
inorden y el preorden que aislamos en el paso anterior. Sea nuevoIzq el subárbol
reconstruido.
2.6. Aislamos el inorden del subárbol derecho, que es la sublista del inorden que va de la
posición pos+1 a la posición n-1. Así mismo, aislamos el preorden del subárbol derecho, que
es la sublista del preorden que va de la posición pos+1 a la posición n-1.
2.7. Llamamos recursivamente al algoritmo para reconstruir el subárbol derecho, según el
inorden y el preorden que aislamos en el paso anterior. Sea nuevoDer el subárbol
reconstruido.
2.8. Retorne un nuevo árbol binario cuya raíz sea nuevoVal, cuyo subárbol izquierdo sea
nuevoIzq y cuyo subárbol derecho sea nuevoDer.
Gráfica 41: Diagrama para guiar la reconstrucción de un árbol binario dado su recorrido en inorden y su recorrido en
preorden.
pos+1
pos-1
pos
n-3
n-2
n-1
0
pos
n-3
n-2
n-1
0
Tabla 42: Reconstrucción del árbol binario con inorden y con preorden .
Inorden Preorden Árbol Operación
La raíz es porque es el primer elemento del
preorden. En el inorden, a la izquierda del hay
dos elementos, y a la derecha hay cuatro. Por lo
tanto, se deduce que el subárbol izquierdo tiene
dos nodos y que el subárbol derecho tiene
cuatro nodos. Finalmente, aislamos tanto el
subárbol izquierdo como el subárbol derecho en
cada uno de los dos recorridos.
ESTRUCTURAS DE DATOS 17
Sabemos que el subárbol izquierdo tiene
inorden y preorden y que el
subárbol derecho tiene inorden y
preorden . De la misma forma,
reconstruimos ambos subárboles.
Sabemos que el subárbol izquierdo del nodo
tiene inorden y preorden . Con el
mismo procedimiento reconstruimos este árbol.
El siguiente algoritmo en Java reconstruye un árbol binario sin elementos repetidos a partir
de su inorden y de su preorden, con complejidad temporal .
Código 43: Rutina que reconstruye un árbol binario dado su inorden y su preorden.
public static <T> VEDArbin<T> reconstruirArbol(List<T> pInorden, List<T> pPreorden) {
if (pInorden.isEmpty()) { // Si el recorrido en inorden es vacío:
return new VEDArbin<T>(); // Retornar el árbol vacío.
}
else { // Si el recorrido en inorden no es vacío:
// Guardar en una variable el número de nodos que tendría el árbol a reconstruir:
int n=pInorden.size();
// Guardar en una variable la raíz del árbol, que está al principio del preorden:
T nuevoVal=pPreorden.get(0);
// Buscar la posición en la que aparece la raíz dentro del inorden:
int pos=pInorden.indexOf(nuevoVal);
// Extraer el inorden del subárbol izquierdo:
List<T> inordenIzq=pInorden.subList(0,pos);
// Extraer el preorden del subárbol izquierdo:
List<T> preordenIzq=pPreorden.subList(1,pos+1);
// Reconstruir recursivamente el subárbol izquierdo:
VEDArbin<T> nuevoIzq=VEDArbin.reconstruirArbol(inordenIzq,preordenIzq);
// Extraer el inorden del subárbol derecho:
List<T> inordenDer=pInorden.subList(pos+1,n);
// Extraer el preorden del subárbol derecho:
List<T> preordenDer=pPreorden.subList(pos+1,n);
// Reconstruir recursivamente el subárbol derecho:
VEDArbin<T> nuevoDer=VEDArbin.reconstruirArbol(inordenDer,preordenDer);
// Retornar el nuevo árbol reconstruido:
return new VEDArbin<T>(nuevoVal,nuevoIzq,nuevoDer);
}
}
Tabla 44: Ejemplos que ilustran cómo reconstruir árboles binarios usando la rutina descrita,
y cómo transformarlos en texto e imagen.
Programa Imagen exportada
List<Integer> in=Arrays.asList(); // Inorden
List<Integer> pre=Arrays.asList(); // Preorden
ESTRUCTURAS DE DATOS 18
VEDArbin<Integer> arbol=VEDArbin.reconstruirArbol(in,pre);
arbol.exportarComoImagen("ejemploReconstruccion01.png");
List<Integer> in=Arrays.asList(23,89,98,10); // Inorden
List<Integer> pre=Arrays.asList(98,23,89,10); // Preorden
VEDArbin<Integer> arbol=VEDArbin.reconstruirArbol(in,pre);
arbol.exportarComoImagen("ejemploReconstruccion02.png");
List<String> in=Arrays.asList("FÉ","LUZ","SOL"); // Inorden
List<String> pre=Arrays.asList("SOL","FÉ","LUZ"); // Preorden
VEDArbin<String> arbol=VEDArbin.reconstruirArbol(in,pre);
arbol.exportarComoImagen("ejemploReconstruccion03.png");
EN RESUMEN
Un árbol binario es una estructura de datos recursiva que está compuesta por un valor, llamado raíz,
y por dos árboles, llamados subárbol izquierdo y subárbol derecho.
Una hoja es un nodo sin hijos. Un nodo interno es un nodo que no es una hoja.
Una rama es un camino que va de la raíz a una hoja.
El peso ( ) de un árbol binario es el número de nodos que tiene. La altura ( ) de un árbol binario es
uno más la longitud de la rama más larga. Tanto el peso como la altura del árbol vacío se definen
como cero.
El recorrido en preorden visita primero la raíz del árbol, luego recorre el subárbol izquierdo en
preorden, y después recorre el subárbol derecho en preorden.
El recorrido en inorden recorre el subárbol izquierdo en inorden, luego visita la raíz del árbol, y
después recorre el subárbol derecho en inorden.
El recorrido en postorden recorre el subárbol izquierdo en postorden, luego recorre el subárbol
derecho en postorden, y después visita la raíz del árbol.
El recorrido por niveles visita los nodos nivel por nivel, desde el nivel hasta el nivel , donde
cada nivel se recorre de izquierda a derecha.
Para poder reconstruir un árbol binario sin ambigüedad, se requiere: 1. que el árbol no tenga
elementos repetidos, y 2. contar con el recorrido en inorden y con algún otro recorrido, ya sea en
preorden, en postorden o por niveles.
ESTRUCTURAS DE DATOS 19
ESTRUCTURAS DE DATOS
UNIDAD TRES - SEMANA CINCO
EJERCICIOS PROPUESTOS *
Para los árboles binarios mostrados a continuación, halle sus recorridos en preorden, en
inorden, en postorden y por niveles.
Recorrido
Árbol binario
Preorden Inorden Postorden Niveles
Reconstruya cada uno de los árboles binarios cuyos recorridos se listan a continuación:
Inorden Preorden
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
3. RECONSTRUCCIÓN DE ÁRBOLES BINARIOS DADO SU INORDEN Y SU RECORRIDO
POR NIVELES
Reconstruya cada uno de los árboles binarios cuyos recorridos se listan a continuación:
Inorden Niveles
4.1. En la clase VEDArbin<E>, escriba una función iterativa que calcule el recorrido en inorden
public List<E> inordenIterativo() {
List<E> lst=new ...;
Stack<VEDArbin<E>> pil=new Stack<VEDArbin<E>>();
VEDArbin<E> arb=izq;
pil.push(...);
while (...) {
if (!arb.esVacio()) {
...
arb=arb.izq;
}
else {
...
arb=arb.der;
}
}
return lst;
}
basándose en el siguiente pseudocódigo:
1. Cree una lista vacía “lst” de elementos de tipo E.
2. Cree una pila vacía “pil” de árboles de elementos de tipo E.
3. Declare una variable “arb” de tipo VEDArbin<E> que apunte al subárbol izquierdo del árbol.
4. Inserte en el tope de la pila el árbol cuyo recorrido en inorden se desee hallar.
5. Mientras el árbol arb no sea vacío o la pila pil no sea vacía:
5.1. Si el árbol arb no es vacío:
5.1.1. Inserte en el tope de la pila el árbol arb.
5.1.2. Asígnele a la variable arb el subárbol izquierdo del árbol arb.
5.2. De lo contrario, si el árbol arb es vacío:
5.2.1. Elimine el árbol que se encuentra en el tope de la pila pil, guardándolo en la variable
arb.
5.2.2. Añada la raíz del árbol arb al final de la lista lst.
5.2.3. Asígnele a la variable arb el subárbol derecho del árbol arb.
4.3. ¿Qué implementación sería mejor para la lista lst? ¿Con vectores o con nodos
doblemente encadenados? ¿Por qué?
ESTRUCTURAS DE DATOS 2
5. FUNCIÓN DE RECONSTRUCCIÓN DE ÁRBOLES BINARIOS DADO SU INORDEN Y SU
POSTORDEN
Suponiendo que se está trabajando con árboles binarios sin elementos repetidos, explique
completa y detalladamente cómo funcionan los siguientes métodos de la clase VEDArbin<E> y
calcule la complejidad temporal de cada uno de éstos.
6.2. Método que retorna el padre del nodo que tiene cierto valor dado. Si no existe un nodo
con el valor suministrado, o si dicho nodo no tiene padre, retorna null.
public E getPadre(E pValor) {
return getPadre(pValor,null);
}
private E getPadre(E pValor, E pPadre) {
if (esVacio()) {
return null;
}
else {
if (val.equals(pValor)) {
return pPadre;
}
else {
E respuestaIzq=izq.getPadre(pValor,val);
if (respuestaIzq!=null) return respuestaIzq;
E respuestaDer=der.getPadre(pValor,val);
if (respuestaDer!=null) return respuestaDer;
return null;
}
}
ESTRUCTURAS DE DATOS 3
}
Suponiendo que se está trabajando con árboles binarios sin elementos repetidos,
implemente los siguientes métodos de la clase VEDArbin<E>, calculando la complejidad
temporal de cada uno de éstos.
7.1. Método que retorna el hermano del nodo que tiene cierto valor dado. Si no existe un
nodo con el valor suministrado, o si dicho nodo no tiene hermano, retorna null.
public E getHermano(E pValor)
7.2. Método que informa si la raíz del árbol representa un nodo interno o no.
public boolean esNodoInterno()
7.4. Método que retorna la cantidad de subárboles vacíos presentes en determinado nivel
del árbol.
public int getCantidadVaciosNivel(int pNivel)
ESTRUCTURAS DE DATOS 4
ESTRUCTURAS DE DATOS
UNIDAD TRES - SEMANA SEIS
ALGORITMOS DE ORDENAMIENTO *
TABLA DE CONTENIDO
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
1. INTRODUCCIÓN A LOS ALGORITMOS DE ORDENAMIENTO
Los algoritmos de ordenamiento son procesos que ordenan los elementos de un arreglo de
acuerdo a cierto criterio (llamado relación de orden). Los números típicamente se ordenan de
menor a mayor, y las cadenas de texto normalmente se ordenan de acuerdo al orden
lexicográfico, como si estuviésemos listándolas en un diccionario.
Por simplicidad, durante el desarrollo de la lectura, supondremos que los arreglos son de
números enteros y que el criterio corresponde al orden numérico de menor a mayor.
ESTRUCTURAS DE DATOS 2
1. Busque el menor valor del arreglo e intercámbielo con el valor de la primera posición.
2. Busque el menor valor del resto de arreglo e intercámbielo con el valor de la segunda
posición.
3. Busque el menor valor del resto de arreglo e intercámbielo con el valor de la tercera
posición.
4. Repita el proceso hasta que se llegue al final del arreglo.
ESTRUCTURAS DE DATOS 3
2.5. ORDENAMIENTO RÁPIDO (QUICK SORT)
Alpheratz 6.0 es una herramienta diseñada por Alejandro Sotelo que enseña de forma
didáctica diversos algoritmos de ordenamiento ampliamente conocidos, permitiendo al
estudiante descubrir por sí mismo cómo opera cada uno de estos.
ESTRUCTURAS DE DATOS 4
Algunos de los algoritmos de ordenamiento que se pueden animar con la herramienta son:
Selection Sort (Ordenamiento por Selección), Insertion Sort (Ordenamiento por Inserción),
Bubble Sort (Ordenamiento de Burbuja), Cocktail Sort (Ordenamiento de Burbuja
Bidireccional), Merge Sort (Ordenamiento por Mezcla), Quick Sort (Ordenamiento Rápido),
Heap Sort (Ordenamiento por Montículos) y Shell Sort (Ordenamiento Shell).
Alpheratz incluye pseudocódigos de todos los algoritmos animados, ofrece diversas opciones
para la configuración y visualización de los datos y es capaz de exportar estadísticas de la
operación de cada uno de los algoritmos de ordenamiento variando la cantidad y la
distribución de los datos.
EN RESUMEN
Los algoritmos de ordenamiento son procesos que ordenan los elementos de un arreglo de acuerdo a
cierto criterio (llamado relación de orden).
El orden lexicográfico aplicado sobre cadenas de texto compara palabras según la posición que
ocuparían en un diccionario.
Algunos algoritmos de ordenamiento básicos son:
Selection Sort (Ordenamiento por Selección).
Insertion Sort (Ordenamiento por Inserción).
Bubble Sort (Ordenamiento de Burbuja).
Merge Sort (Ordenamiento por Mezcla).
Quick Sort (Ordenamiento Rápido).
ESTRUCTURAS DE DATOS 5
ESTRUCTURAS DE DATOS
UNIDAD TRES - SEMANA SEIS
COMPARADORES DE ORDEN EN JAVA *
TABLA DE CONTENIDO
1. INTERFAZ COMPARABLE<T> 2
2. INTERFAZ COMPARATOR<T> 4
EN RESUMEN 5
PARA TENER EN CUENTA 5
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
1. INTERFAZ COMPARABLE<T>
Recurso como proyecto en Eclipse: EjemploInterfazComparable.zip.
Para ordenar arreglos y buscar valores en arreglos ordenados es necesario conocer el criterio
con el que los datos se están ordenando, que se puede definir implementando la interfaz
Comparable<T> de Java. Esta interfaz permite especificar una relación natural de orden sobre
los objetos de una clase determinada, mediante el método
public int compareTo(T element)
que compara el objeto en cuestión con el objeto element dado por parámetro, retornando un
valor negativo si este objeto es menor que element, cero si es igual a element, y un valor
positivo si es mayor que element.
Dos objetos son mutuamente comparables si pueden ser comparados entre sí a través del
método compareTo. Formalmente, siendo f y g dos objetos mutuamente comparables, la
expresión f.compareTo(g):
Retorna un número entero negativo si f es menor que g.
Retorna cero si f es igual a g.
Retorna un número entero positivo si f es mayor que g.
Tabla 2: Algunas funciones útiles en Java para ordenar arreglos de acuerdo al orden natural de sus elementos.
Función Descripción
Collections.sort(lst) Ordena la lista lst con el algoritmo Merge Sort.
Arrays.sort(arr) Ordena el arreglo arr con el algoritmo Merge Sort.
Ordena el arreglo arr desde la posición inf hasta la
Arrays.sort(arr,inf,sup)
posición sup-1 con el algoritmo Merge Sort.
ESTRUCTURAS DE DATOS 2
Para dotar a los objetos de nuestras clases de una relación natural de orden debemos
implementar la interfaz Comparable<E>. El siguiente ejemplo crea una lista de personas y las
ordena de menor a mayor según su documento de identificación personal:
Código 4: Ejemplo sobre el uso de la interfaz Comparable, en conjunción con el método sort(…) de la clase Arrays.
import java.util.*;
public class EjemploPersonas {
public static void main(String[] args) {
// A los números se les pone al final la letra L para que el compilador de Java los
// reconozca como números de tipo long.
List<Persona> lst=new ArrayList<Persona>();
lst.add(new Persona("Pedro","Jiménez Roa",38708511L));
lst.add(new Persona("Juan Andrés","Gil Ruiz",36834396L));
lst.add(new Persona("Rodrigo","Martínez Guzmán",40014776L));
lst.add(new Persona("Ana María","López Paz",16587293L));
lst.add(new Persona("Carlos","Pérez",997293L));
System.out.println("Lista antes de ordenar por documento:");
for (Persona p:lst) {
System.out.println(" "+p.toString());
}
Collections.sort(lst);
System.out.println("Lista después de ordenar por documento:");
for (Persona p:lst) {
System.out.println(" "+p.toString());
}
}
}
ESTRUCTURAS DE DATOS 3
Gráfica 5: Impresión en consola del programa anterior.
2. INTERFAZ COMPARATOR<T>
Recurso como proyecto en Eclipse: EjemploInterfazComparator.zip.
Para especificar relaciones de orden distintas a las provistas por la interfaz Comparable<T> se
debe implementar la interfaz Comparator<T> de Java, que tiene un único método
public int compare(T element1, T element2)
que compara los objetos element1 y element2, retornando un valor negativo si element1 es
menor que element2, cero si element1 es igual a element2, y un valor positivo si element1 es
mayor que element2.
ESTRUCTURAS DE DATOS 4
Tabla 7: Un ejemplo sobre el uso de comparadores.
Ejemplo Impresión en consola
Comparator<Integer> c=new ComparadorEnterosReves();
Integer[] arr={80,31,97,13,28,13,36,32,61,70};
[97,80,70,61,36,32,31,28,13,13]
Arrays.sort(arr,c);
System.out.println(Arrays.toString(arr));
EN RESUMEN
La interfaz Comparable<T> permite especificar una relación natural de orden sobre los objetos de una
clase determinada, mediante el método public int compareTo(T element), que compara el objeto
en cuestión con el objeto element dado por parámetro, retornando un valor negativo si este objeto
es menor que element, cero si es igual a element, y un valor positivo si es mayor que element.
La interfaz Comparator<T> de Java tiene un único método public int compare(T element1, T
element2), que compara los objetos element1 y element2, retornando un valor negativo si element1
es menor que element2, cero si element1 es igual a element2, y un valor positivo si element1 es
mayor que element2.
ESTRUCTURAS DE DATOS 5
ESTRUCTURAS DE DATOS
UNIDAD TRES - SEMANA SEIS
ÁRBOLES BINARIOS ORDENADOS *
TABLA DE CONTENIDO
1. DEFINICIÓN 2
2. UN VISTAZO A LA INTERFAZ SET<E> 3
3. IMPLEMENTACIÓN 3
3.1. CONSTRUCTOR 4
3.2. DESTRUCTOR 4
3.3. MÉTODOS DE CONSULTA BÁSICOS 5
3.4. ALGORITMO DE BÚSQUEDA 5
3.5. ALGORITMO DE INSERCIÓN 6
3.6. ALGORITMO DE ELIMINACIÓN 8
3.7. ALGORITMO DE MODIFICACIÓN 11
EN RESUMEN 11
PARA TENER EN CUENTA 11
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
1. DEFINICIÓN
Los árboles binarios ordenados, también conocidos como árboles de búsqueda, tienen como
propósito:
El almacenamiento de información de forma ordenada.
La administración de información en memoria principal a través de algoritmos eficientes
para las operaciones básicas de consulta, inserción, eliminación y modificación de datos.
Dada cierta relación de orden capaz de establecer si un elemento es menor, igual o mayor
que otro, el concepto de árbol binario ordenado se puede definir recursivamente así:
1. El árbol vacío es un árbol binario ordenado, por definición.
2. El árbol no vacío
val val : raíz
En pocas palabras, un árbol binario ordenado es un árbol binario donde todo nodo cumple la
condición de que cada uno de los valores presentes en su subárbol izquierdo son menores
que la raíz y cada uno de los valores presentes en su subárbol derecho son mayores que la
raíz.
Observe los siguientes hechos: un árbol binario ordenado no debe tener elementos repetidos
(¿por qué?), y el recorrido en inorden de todo árbol binario ordenado está ordenado de
menor a mayor (¿por qué?). Por lo tanto, un árbol binario es ordenado si y sólo si su
recorrido en inorden no tiene elementos repetidos y además está ordenado de menor a
mayor. Este criterio nos brinda una forma fácil de determinar si un árbol es ordenado.
SI
ESTRUCTURAS DE DATOS 2
NO
3. IMPLEMENTACIÓN
Recurso como proyecto en Eclipse: ArbolesBinariosOrdenados.zip.
Los árboles binarios ordenados son adecuados para representar conjuntos porque
almacenan elementos sin repeticiones. Por esta razón, tiene sentido que
VEDArbinOrdenado<E> implemente la interfaz Set<E>.
†
La documentación de la interfaz Set<E> está disponible en el API de Java en la página
http://java.sun.com/javase/6/docs/api/java/util/Set.html.
ESTRUCTURAS DE DATOS 3
Además, todo árbol binario ordenado es un árbol binario. Para reutilizar el código fuente que
ya escribimos en VEDArbin<E>, la clase VEDArbinOrdenado<E> debe extender la clase
VEDArbin<E>:
VEDArbinOrdenado<E> extends VEDArbin<E>
Cuando una clase F extiende de otra clase G, todos los atributos y métodos no privados de la
clase G son heredados a la clase F. En la programación orientada a objetos esta característica
se denomina herencia, y es bastante útil para definir clases en términos de otras ya
existentes, para reutilizar código, y para modelar relaciones del estilo es un (entre objetos).
Si tenemos que todo objeto de la clase F es un objeto de la clase G, decimos que la clase F
extiende de la clase G, donde F es la subclase y G es la superclase. Por ejemplo, como todo
perro es un animal, entonces la clase Perro extiende de la clase Animal, siendo Perro la
subclase y Animal la superclase. Por lo tanto, VEDArbinOrdenado<E> debe heredar todos los
atributos y métodos de la clase VEDArbin<E>.
3.1. CONSTRUCTOR
Hay dos métodos constructores de la clase, uno para crear un árbol binario ordenado vacío, y
otro para crear un árbol binario ordenado que contenga ciertos elementos.
3.2. DESTRUCTOR
Código 5: Método que elimina todos los nodos del árbol.
public void clear() {
// El árbol vacío es representado como un árbol binario con raíz null, subárbol
// izquierdo null y subárbol derecho null.
val=null;
izq=null;
der=null;
}
ESTRUCTURAS DE DATOS 4
3.3. MÉTODOS DE CONSULTA BÁSICOS
Código 6: Método para consultar el subárbol izquierdo, visto como un árbol binario ordenado.
public VEDArbinOrdenado<E> getIzq() {
// Retornar el subárbol izquierdo, visto como árbol ordenado:
return (VEDArbinOrdenado<E>)izq; // Efectuar un cast.
}
Código 7: Método para consultar el subárbol derecho, visto como un árbol binario ordenado.
public VEDArbinOrdenado<E> getDer() {
// Retornar el subárbol derecho, visto como árbol ordenado:
return (VEDArbinOrdenado<E>)der; // Efectuar un cast.
}
Código 9: Método para informar el número de elementos que tiene el árbol binario ordenado.
public int size() {
return peso(); // Llamar al método peso() de la superclase.
}
ESTRUCTURAS DE DATOS 5
return true; // Informar que el valor buscado sí está en el árbol.
}
else if (c<0) { // Si el valor a buscar es menor que la raíz del árbol:
// Buscar recursivamente el valor x a la izquierda del árbol:
return getIzq().contains(obj);
}
else { // Si el valor a buscar es mayor que la raíz del árbol:
// Buscar recursivamente el valor x a la derecha del árbol:
return getDer().contains(obj);
}
}
}
Tabla 11: Análisis de complejidad temporal del método boolean contains(Object obj).
Tipo de análisis Complejidad Justificación
Peor caso En un árbol degenerado la altura ( ) es igual al peso ( ).
En un árbol binario ordenado poblado aleatoriamente, la altura
Caso promedio ( ) está por debajo de un múltiplo constante del logaritmo del
peso ( ).
Tabla 12: Ejemplo que ilustra cómo buscar valores en un árbol ordenado.
Árbol Operación
Para buscar el valor :
Seguimos la siguiente ruta en el proceso de búsqueda: izquierda (pues
), derecha (pues ), izquierda (pues ), derecha
(pues ), derecha ( ). Como terminamos en un subárbol
vacío, concluimos que el valor no está presente en el árbol.
Para buscar el valor :
Seguimos la siguiente ruta en el proceso de búsqueda: derecha (pues
), derecha (pues ), derecha (pues ), izquierda
(pues ). Terminamos encontrando el valor a la izquierda del
.
ESTRUCTURAS DE DATOS 6
2.3. Si el valor x es menor que la raíz del árbol: inserte recursivamente el valor x en el
subárbol izquierdo (sabemos que no se puede insertar a la derecha porque el árbol debe
terminar ordenado).
2.4. Si el valor x es mayor que la raíz del árbol: inserte recursivamente el valor x en el
subárbol derecho (sabemos que no se puede insertar a la izquierda porque el árbol debe
terminar ordenado).
Tabla 14: Análisis de complejidad temporal del método boolean add(E element).
Tipo de análisis Complejidad Justificación
Peor caso En un árbol degenerado la altura ( ) es igual al peso ( ).
En un árbol binario ordenado poblado aleatoriamente, la altura
Caso promedio ( ) está por debajo de un múltiplo constante del logaritmo del
peso ( ).
Tabla 15: Ejemplo que ilustra cómo insertar valores en un árbol ordenado.
Árbol antes de la operación Árbol después de la operación Operación
Insertar el valor .
Insertar el valor .
Insertar el valor
ESTRUCTURAS DE DATOS 7
Insertar el valor
Nuestro proceso de eliminación necesita una rutina para encontrar el mayor valor de un
árbol binario ordenado. Sabiendo que el máximo elemento de un árbol ordenado se
encuentra lo más a la derecha posible en el árbol, podemos implementar la siguiente función
con complejidad temporal :
Código 16: Método para retornar el mayor valor del árbol ordenado.
public E max() {
if (esVacio()) { // Si este árbol es vacío:
return null; // Retornar null porque no hay ningún elemento en este árbol.
}
else { // Si este árbol no es vacío:
if (der.esVacio()) { // Si el subárbol derecho es vacío:
return val; // El máximo del árbol sería la raíz.
}
else { // Si el subárbol derecho no es vacío:
return getDer().max(); // El máximo del árbol sería el máximo valor de der.
}
}
}
ESTRUCTURAS DE DATOS 8
2.2.4. Si los dos subárboles no son vacíos:
2.2.4.1. Halle el mayor valor del subárbol izquierdo. Guarde en la variable m este valor.
2.2.4.2. Ponga como raíz del árbol el valor m.
2.2.4.3. Elimine recursivamente el valor m del subárbol izquierdo.
x x m m
encontrar el poner m eliminar m
máximo de izq como raíz del subárbol izq
izq der izq m der izq m der izq der
Comentario: también sería posible hacer lo mismo con el menor valor del subárbol derecho.
2.3. Si el valor x es menor que la raíz del árbol: elimine recursivamente el valor x del subárbol
izquierdo (sabemos que el valor no puede estar a la derecha porque el árbol es ordenado).
2.4. Si el valor x es mayor que la raíz del árbol: elimine recursivamente el valor x del subárbol
derecho (sabemos que el valor no puede estar a la izquierda porque el árbol es ordenado).
ESTRUCTURAS DE DATOS 9
}
else { // Si el valor a eliminar es mayor que la raíz del árbol:
// Eliminar recursivamente el valor x a la derecha del árbol:
return getDer().remove(obj);
}
}
}
Tabla 18: Análisis de complejidad temporal del método boolean remove(Object obj).
Tipo de análisis Complejidad Justificación
Peor caso En un árbol degenerado la altura ( ) es igual al peso ( ).
En un árbol binario ordenado poblado aleatoriamente, la altura
Caso promedio ( ) está por debajo de un múltiplo constante del logaritmo del
peso ( ).
Tabla 19: Ejemplo que ilustra cómo eliminar valores de un árbol ordenado.
Evolución del árbol durante la operación Caso de eliminación Operación
2.2.1.
(ambos subárboles Eliminar el valor .
vacíos)
2.2.1.
(ambos subárboles Eliminar el valor .
vacíos)
2.2.2.
(subárbol izquierdo Eliminar el valor .
vacío)
2.2.3.
(subárbol derecho Eliminar el valor .
vacío)
2.2.4.
(ambos subárboles Eliminar el valor .
no vacíos)
ESTRUCTURAS DE DATOS 10
3.7. ALGORITMO DE MODIFICACIÓN
El proceso de modificación puede ser visto como una eliminación seguida de una inserción.
Su complejidad temporal también sería .
EN RESUMEN
Un árbol binario ordenado es un árbol binario donde todo nodo cumple la condición de que cada uno
de los valores presentes en su subárbol izquierdo son menores que la raíz y cada uno de los valores
presentes en su subárbol derecho son mayores que la raíz. Un árbol binario es ordenado si y sólo si su
recorrido en inorden no tiene elementos repetidos y está ordenado de menor a mayor.
Las operaciones básicas de búsqueda, inserción, eliminación y modificación sobre árboles binarios
ordenados tienen complejidad temporal , que en el peor caso da y en el caso promedio da
.
ESTRUCTURAS DE DATOS 11
ESTRUCTURAS DE DATOS
UNIDAD TRES - SEMANA SEIS
ÁRBOLES BINARIOS ORDENADOS BALANCEADOS *
TABLA DE CONTENIDO
1. INTRODUCCIÓN 2
2. ÁRBOLES AVL (ÁRBOLES BALANCEADOS POR ALTURA) 2
2.1. DEFINICIÓN 2
2.2. OPERACIONES BÁSICAS 4
2.2.1. ALGORITMO DE BÚSQUEDA 4
2.2.2. ALGORITMO DE INSERCIÓN 4
2.2.3. ALGORITMO DE ELIMINACIÓN 7
3. ÁRBOLES PERFECTAMENTE BALANCEADOS (BALANCEADOS POR PESO) 8
4. ÁRBOLES ROJI-NEGROS 8
EN RESUMEN 10
PARA TENER EN CUENTA 10
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
1. INTRODUCCIÓN
Como es posible que los árboles binarios ordenados se degeneren alcanzando una altura
similar a su peso, entonces la complejidad temporal de las operaciones básicas se podría
degradar a (recuerde que un árbol degenerado cumple que su altura es igual a su
peso). Para no degradar la eficiencia es necesario balancear los árboles, procurando que todo
nodo tenga aproximadamente la mitad de sus descendientes a la izquierda y el resto de
descendientes a la derecha. Estudiaremos tres tipos de árboles binarios ordenados
balanceados que son capaces de garantizar complejidades logarítmicas sobre las operaciones
básicas de búsqueda, inserción, eliminación y modificación: los árboles AVL, los árboles
perfectamente balanceados, y los árboles Roji-negros.
La teoría expuesta en esta sección está inspirada en el libro Diseño y Manejo de Estructuras
de Datos en C de Jorge Villalobos †.
2.1. DEFINICIÓN
Un árbol AVL (también conocido como árbol balanceado por altura) se puede definir
recursivamente así:
1. El árbol vacío es un árbol AVL, por definición.
2. El árbol no vacío
val val : raíz
†
VILLALOBOS, Jorge A. Diseño y Manejo de Estructuras de Datos en C. Bogotá: McGraw-Hill, 1996.
ESTRUCTURAS DE DATOS 2
Es decir, la altura del subárbol derecho menos la altura del subárbol izquierdo está entre
y .
Tanto el subárbol izquierdo como el subárbol derecho son árboles AVL.
Dado un árbol, un factor de balanceo de significa que sus dos subárboles tienen igual
altura, un factor de balanceo de significa que su subárbol izquierdo tiene niveles más
de altura que su subárbol derecho, y un factor de balanceo de significa que su subárbol
derecho tiene niveles más de altura que su subárbol izquierdo.
Gráfica 1: Procedimiento mnemotécnico para el cálculo de los factores de balanceo por altura.
Un árbol AVL se puede definir como un árbol binario ordenado tal que todos sus nodos
tienen un factor de balanceo por altura entre menos uno y uno.
Todo árbol AVL cumple que donde es el peso y es la altura del árbol.
ESTRUCTURAS DE DATOS 3
2.2. OPERACIONES BÁSICAS
Como todo árbol AVL es también un árbol binario ordenado, el algoritmo de búsqueda es el
mismo que ya estudiamos para árboles binarios ordenados, que tiene complejidad temporal
donde es la altura del árbol. Gracias a que en todo árbol AVL se cumple que
, entonces la complejidad temporal del algoritmo de búsqueda sería
en todo caso.
a b b
2
a
Este escenario sucede cuando la inserción del valor x se efectúa a la izquierda de la izquierda
del nodo que se desbalanceó. Para arreglar el árbol se debe realizar una rotación a la derecha
sobre el nodo e.
e -2 f 0
f -1 rotación e 0
derecha Se rota a la derecha el nodo e
c
b a b c
a
ESTRUCTURAS DE DATOS 4
3.2.2. Caso Der-Der (Derecha-Derecha):
e +1 e +2
f 0 insertar x f +1 La inserción se efectúa en el
subárbol c, aumentando su altura
a a
b c b
2
c
Este escenario sucede cuando la inserción del valor x se efectúa a la derecha de la derecha
del nodo que se desbalanceó. Para arreglar el árbol se debe realizar una rotación a la
izquierda sobre el nodo e.
e +2 f 0
f +1 rotación e 0
izquierda Se rota a la izquierda el nodo e
a
b a b c
c
3.2.3. Caso Izq-Der (Izquierda-Derecha):
e -1 e -2
f 0 f +1 La inserción se efectúa en el
insertar x
subárbol b1 o en el subárbol b2,
g? aumentando su altura
c c
a b a 2
b1 b2
Este escenario sucede cuando la inserción del valor x se efectúa a la derecha de la izquierda
del nodo que se desbalanceó. Para arreglar el árbol se debe realizar una rotación a la
izquierda sobre el nodo f y luego una rotación a la derecha sobre el nodo e,
independientemente de si la inserción afectó el subárbol b1 o el subárbol b2.
e -2 e -2 g 0
Se rota a la izquierda
f +1 rotación g -1 rotación f? e? el nodo f, y luego se
g? izquierda f? derecha se rota a la derecha
c c
el nodo e
a b2 a b1 b2 c
b1 b2 a b1
3.2.4. Caso Der-Izq (Derecha-Izquierda):
e +1 e +2
f 0 f -1 La inserción se efectúa en el
insertar x
subárbol b1 o en el subárbol b2,
a a
g? aumentando su altura
b c c
2
b1 b2
Este escenario sucede cuando la inserción del valor x se efectúa a la izquierda de la derecha
del nodo que se desbalanceó. Para arreglar el árbol se debe realizar una rotación a la derecha
sobre el nodo f y luego una rotación a la izquierda sobre el nodo e, independientemente de
si la inserción afectó el subárbol b1 o el subárbol b2.
ESTRUCTURAS DE DATOS 5
e +2 e +2 g 0
Se rota a la derecha
f -1 rotación g +1 rotación e? f? el nodo f, y luego se
g? derecha f? izquierda se rota a la izquierda
a a
el nodo e
c b1 a b1 b2 c
b1 b2 b2 c
3.2.1. Insertar
(Izq-Izq) el valor
Insertar
2.
el valor
3.2.2. Insertar
(Der-Der) el valor
3.2.3. Insertar
(Izq-Der) el valor
ESTRUCTURAS DE DATOS 6
Gracias a que en todo árbol AVL se cumple que , entonces la complejidad
temporal del algoritmo de inserción sería en todo caso.
Es posible que después de una eliminación se requiera corregir con rotaciones más de un
nodo.
Izq-Der
(aunque también Eliminar
se puede aplicar el valor
Izq-Izq)
ESTRUCTURAS DE DATOS 7
Eliminar
Izq-Izq
el valor
Eliminar
el valor
Un árbol perfectamente balanceado es un árbol binario ordenado tal que todos sus nodos
tienen un factor de balanceo por peso entre menos uno y uno, donde el factor de balanceo
por peso de un nodo es el peso de su subárbol derecho menos el peso de su subárbol
izquierdo:
4. ÁRBOLES ROJI-NEGROS
Los árboles Roji-negros son otro tipo de árboles ordenados balanceados que proveen
complejidades logarítmicas para las operaciones básicas de búsqueda, inserción,
eliminación y modificación.
Un árbol Roji-negro (conocido en inglés como Red-black tree) es un árbol binario que cumple
las siguientes propiedades:
1. Es un árbol ordenado.
ESTRUCTURAS DE DATOS 8
2. Todos sus nodos están coloreados de rojo o de color negro (el color es un atributo de los
nodos de un árbol Roji-negro, que sólo puede tomar dos posibles valores: rojo o negro).
3. Su raíz está coloreada de negro.
4. Todos sus subárboles vacíos están coloreados de negro y son considerados como nodos del
árbol cuyo valor almacenado es null.
5. Todos los hijos de un nodo coloreado de rojo deben estar coloreados de negro.
6. Todos los caminos que parten de un nodo y llegan a un subárbol vacío deben pasar por la
misma cantidad de nodos coloreados de negro.
Muchos textos usan el término hoja en vez de subárbol vacío en la definición. Preferiremos
designar los nodos terminales con el término subárbol vacío para ser coherentes con la
terminología que hemos estado empleando en toda la teoría, trataremos los subárboles
vacíos como nodos, y permitiremos colocar el subárbol vacío en los caminos de nodos.
Suponga en los ejemplos que el árbol vacío ya está coloreado de negro por defecto.
Las restricciones de la definición obligan a que en un árbol Roji-negro, la longitud del camino
más largo que va de la raíz hacia cualquier subárbol vacío no sea mayor que el doble de la
longitud del camino más corto que va de la misma raíz a cualquier otro subárbol vacío. La
razón es la siguiente: debido a que todos los caminos desde la raíz hacia cualquier subárbol
vacío tienen la misma cantidad de nodos negros y que no puede haber ningún camino con
dos nodos rojos consecutivos, entonces el camino más corto posible estaría compuesto
únicamente por nodos negros (negro-negro-…-negro) y el camino más largo posible estaría
compuesto por nodos negros y nodos rojos de forma alternada (negro-rojo-negro-…-
rojo-negro).
Sí. Observe que todos los caminos que van desde el hasta
un subárbol vacío pasan por exactamente cuatro nodos
negros (incluyendo el vacío).
ESTRUCTURAS DE DATOS 9
Todo árbol Roji-negro cumple que , donde es el peso y es la altura del
árbol. Se tiene entonces que los árboles AVL están más estrictamente balanceados que los
árboles Roji-negros porque la altura de todo árbol AVL está acotada por .
EN RESUMEN
Un árbol AVL es un árbol binario ordenado tal que todos sus nodos tienen un factor de balanceo por
altura entre menos uno y uno. Un árbol perfectamente balanceado es un árbol binario ordenado tal
que todos sus nodos tienen un factor de balanceo por peso entre menos uno y uno.
Los árboles AVL, los árboles perfectamente balanceados y los árboles Roji-negros son estructuras de
datos que permiten buscar, insertar, eliminar y modificar datos con complejidad logarítmica
( ).
ESTRUCTURAS DE DATOS 10
ESTRUCTURAS DE DATOS
UNIDAD TRES - SEMANA SEIS
EJERCICIOS PROPUESTOS *
Comenzando desde el árbol ordenado vacío, inserte los valores presentes en la siguiente
secuencia de elementos uno por uno y en orden:
Partiendo del árbol obtenido, elimine uno a uno los valores de la siguiente secuencia de
elementos:
En la clase VEDArbinOrdenado<E>:
2.1. Implemente una función iterativa con complejidad temporal que indique si un
valor dado está o no presente en el árbol.
public boolean containsIterativo(Object obj)
2.2. Implemente una función iterativa con complejidad temporal que inserte un valor
en el árbol.
public boolean addIterativo(E element)
3.1. Método que calcula cuántos elementos del árbol son menores que cierto valor.
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
public int cuantosMenores(E pValor)
4. INTERFAZ SET<E>
4.1. Investigue sobre la interfaz Set<E> de Java. ¿Qué representa? ¿Qué servicios provee?
5.1. Para cada uno de los siguientes casos, muestre paso a paso el proceso de inserción de la
secuencia de elementos dada en un árbol AVL inicialmente vacío. Cada vez que necesite
desarrollar rotaciones sobre el árbol, indique en qué caso del algoritmo de rebalanceo se
encuentra (Izq-Izq, Izq-Der, Der-Izq, ó Der-Der), identifique los subárboles involucrados, y
etiquete cada nodo con su factor de balanceo. No olvide que no se puede insertar ningún
valor que ya se encuentre presente en un árbol AVL, porque en un árbol ordenado no debe
haber elementos repetidos.
Caso Árbol inicial Secuencia de elementos a insertar
1
2
3
4
5.2. Para cada uno de los siguientes casos, muestre paso a paso el proceso de eliminación de
la secuencia de elementos dada partiendo del árbol AVL suministrado. Cada vez que necesite
desarrollar rotaciones sobre el árbol, indique en qué caso del algoritmo de rebalanceo se
encuentra (Izq-Izq, Izq-Der, Der-Izq, ó Der-Der), identifique los subárboles involucrados, y
etiquete cada nodo con su factor de balanceo.
Caso Árbol inicial Secuencia de elementos a insertar
ESTRUCTURAS DE DATOS 2
6. ÁRBOLES ROJI-NEGROS
Investigue en libros y en internet acerca de los árboles Roji-negros. Escriba un artículo que
trate acerca de:
Conceptos básicos.
Definición y propiedades.
Aplicaciones.
Ventajas y desventajas respecto a los árboles AVL.
Inserción de valores en árboles Roji-negros, describiendo cada caso en el algoritmo de
inserción.
Eliminación de valores en árboles Roji-negros, describiendo cada caso en el algoritmo de
eliminación.
Ejemplos donde se ilustren los algoritmos de inserción y de eliminación.
ESTRUCTURAS DE DATOS 3
GUÍA DE COMPETENCIAS Y ACTIVIDADES
SEMANA 7
TEMA (S): NÚCLEO TEMÁTICO: ÁRBOLES
1. Motivación.
3. Funciones de Hashing.
4. Implementación.
1 [ POLITÉCNICO GRANCOLOMBIANO ]
NÚCLEO TEMÁTICO: GRAFOS
1. Conceptos básicos.
3. Implementaciones.
1- Identificar variables y/o parámetros relevantes en la dinámica del sistema, y así mismo descartar
aspectos irrelevantes, o de poca incidencia con el fin de llegar a modelos matemáticos que
permitan soluciones analíticas.
2- Aplicar modelos matemáticos en el planteamiento de problemas.
[ ESTRUCTURA DE DATOS ] 2
9- Aprender autónomamente el dominio de las herramientas tecnológicas actuales para la
implementación de soluciones de sistemas, esto incluye lenguajes de programación, ambientes
de programación, metodologías, paradigmas de desarrollo, librerías, frameworks, etc.
10- Generar estrategias de trabajo efectivo en equipo.
11- Ser emprendedor, capaz de decidir en condiciones de incertidumbre valorando las
consecuencias que ello implica.
12- Desarrollar sistemas de información, a través de simulaciones, bien sea programadas o
desarrolladas desde su base.
13- Aplicar herramientas de análisis y diseño en la construcción y creación de sistemas de
información y soluciones en telecomunicaciones.
3 [ POLITÉCNICO GRANCOLOMBIANO ]
SEMANA 8
TEMA (S): NÚCLEO TEMÁTICO: ÁRBOLES
1. Motivación.
3. Funciones de Hashing.
4. Implementación.
[ ESTRUCTURA DE DATOS ] 4
NÚCLEO TEMÁTICO: GRAFOS
1. Conceptos básicos.
3. Implementaciones.
1- Identificar variables y/o parámetros relevantes en la dinámica del sistema, y así mismo descartar
aspectos irrelevantes, o de poca incidencia con el fin de llegar a modelos matemáticos que
permitan soluciones analíticas.
2- Aplicar modelos matemáticos en el planteamiento de problemas.
5 [ POLITÉCNICO GRANCOLOMBIANO ]
9- Aprender autónomamente el dominio de las herramientas tecnológicas actuales para la
implementación de soluciones de sistemas, esto incluye lenguajes de programación, ambientes
de programación, metodologías, paradigmas de desarrollo, librerías, frameworks, etc.
10- Generar estrategias de trabajo efectivo en equipo.
11- Ser emprendedor, capaz de decidir en condiciones de incertidumbre valorando las
consecuencias que ello implica.
12- Desarrollar sistemas de información, a través de simulaciones, bien sea programadas o
desarrolladas desde su base.
13- Aplicar herramientas de análisis y diseño en la construcción y creación de sistemas de
información y soluciones en telecomunicaciones.
[ ESTRUCTURA DE DATOS ] 6
ESTRUCTURAS DE DATOS
UNIDAD CUATRO - SEMANA SIETE
ÁRBOLES ENEARIOS *
TABLA DE CONTENIDO
1. DEFINICIÓN 2
2. CONCEPTOS BÁSICOS 3
2.1. PESO Y ALTURA 3
2.2. ORDEN 3
EN RESUMEN 3
PARA TENER EN CUENTA 4
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
1. DEFINICIÓN
Como generalización de los árboles binarios tenemos los árboles enearios (también escrito n-
arios), que son estructuras de datos recursivas que están compuestas por un valor, llamado
raíz, y por cualquier cantidad finita de subárboles. La diferencia entre los árboles binarios y
los árboles enearios es que los primeros sólo permiten dos subárboles por nodo, mientras
que los segundos permiten cualquier cantidad de subárboles por nodo.
Formalmente:
1. El árbol que no posee ningún elemento, que representaremos gráficamente mediante el
símbolo , es un árbol eneario denominado el árbol vacío.
2. Dado un valor val, y n árboles enearios arb1, arb2, …, y arbn, se tiene que el árbol
val val : raíz
arb1 : primer subárbol
arb2 : segundo subárbol
…
arb1 arb2 ... arbn arbn : último subárbol
es un árbol eneario cuya raíz es val, y cuyos subárboles son arb1, arb2, …, y arbn.
Precisamente, el árbol se llama eneario (n-ario) porque cada nodo tiene máximo n hijos.
, , ,
no tiene
, , ,
, , , ,
ESTRUCTURAS DE DATOS 2
2. CONCEPTOS BÁSICOS
La gran mayoría de los conceptos definidos para árboles binarios se pueden generalizar a los
árboles enearios de una forma intuitiva: relaciones de parentesco (hijo, padre, hermano,
nieto, abuelo, tío, sobrino, primo, bisnieto, tataranieto, bisabuelo y tatarabuelo, etc.), hojas y
nodos internos, caminos y ramas, descendientes y ancestros, nivel, peso y altura, entre otros.
2.2. ORDEN
El orden de un árbol eneario es la cantidad de hijos del nodo que más hijos tiene. Por
ejemplo, todo árbol eneario de orden uno es una lista encadenada, todo árbol eneario de
orden dos es un árbol binario y todo árbol eneario de orden tres cumple que todos sus nodos
tienen máximo tres hijos cada uno. En general, todo árbol eneario de orden m es un árbol
donde cada uno de sus nodos tienen a lo sumo m hijos.
EN RESUMEN
Los árboles enearios son estructuras de datos recursivas que están compuestas por un valor, llamado
raíz, y por cualquier cantidad finita de subárboles.
ESTRUCTURAS DE DATOS 3
El peso de un árbol eneario no vacío es el número de nodos que tiene, y se puede calcular como uno
más el peso de todos sus subárboles. El peso de un árbol eneario vacío es .
La altura de un árbol eneario no vacío es el número de niveles que tiene, y se puede calcular como
uno más el máximo de las alturas de sus subárboles. La altura de un árbol eneario vacío es .
ESTRUCTURAS DE DATOS 4
ESTRUCTURAS DE DATOS
UNIDAD CUATRO - SEMANA SIETE
APLICACIONES DE LOS ÁRBOLES ENEARIOS *
TABLA DE CONTENIDO
1. QUADTREES 2
1.1. DEFINICIÓN 2
1.2. RECONSTRUCCIÓN 4
2. TRIES 5
2.1. DEFINICIÓN 5
3. ÁRBOLES B 6
3.1. DEFINICIÓN 6
EN RESUMEN 7
PARA TENER EN CUENTA 8
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
1. QUADTREES
1.1. DEFINICIÓN
Una matriz de booleanos podría representar una matriz de unos y ceros (cambiando los
verdaderos por unos y los falsos por ceros) o una imagen en blanco y negro (cambiando los
verdaderos por píxeles negros y los falsos por píxeles blancos). Por comodidad y claridad,
supondremos que un Quadtree representa imágenes en blanco y negro de tamaño
para algún , dándonos la posibilidad de manipular imágenes cuadradas donde su lado
es una potencia de dos ( , , , , , , etcétera).
Gráfica 2: El anterior Quadtree visto como una matriz de unos y ceros y como una imagen en blanco y negro.
ESTRUCTURAS DE DATOS 2
2. Dividimos la imagen en cuatro cuadrantes, numerados como se muestra a continuación:
0 1
2 3
3. Representamos recursivamente cada uno de los cuatro cuadrantes como un Quadtree.
4. Ponemos como hijos del nodo gris cada uno de los cuatro Quadtrees obtenidos, en el
mismo orden.
Gráfica 5: Representación como Quadtrees de imágenes que no son ni completamente blancas ni completamente negras.
Observe que todo Quadtree es un árbol eneario de orden porque todos sus nodos tienen
máximo cuatro hijos, y que los Quadtrees nos permiten representar de forma eficiente y
compacta imágenes en blanco y negro siempre y cuando hayan muchísimos más píxeles
negros que blancos o viceversa.
ESTRUCTURAS DE DATOS 3
1.2. RECONSTRUCCIÓN
Existe una manera muy cómoda de crear un Quadtree: reconstruirlo a partir de su recorrido
en postorden.
Tabla 7: Postorden de algunos Quadtrees de ejemplo, donde ‘B’ representa un nodo blanco,
‘N’ un nodo negro y ‘G’ un nodo gris.
Imagen Representación como Quadtree Postorden como cadena de texto
BNNBG
BNNBGNNBNNBGG
BNNBGBNNBGBNNBGBNNBGG
NBBNGNBBNGNBBNGNBBNGG
NBBBNGBBNNGBNNNGG
Expresando el postorden del Quadtree como una cadena de texto donde la letra ‘B’ denota
nodos blancos, ‘N’ nodos negros y ‘G’ nodos grises, es posible reconstruirlo usando el
siguiente algoritmo iterativo:
1. Cree una pila vacía de Quadtrees.
2. Para cada i desde cero hasta el tamaño del postorden menos uno:
2.1. Sea c el carácter ubicado en la posición i del postorden.
ESTRUCTURAS DE DATOS 4
2.2. Si c es la letra ‘B’, entonces inserte en el tope de la pila un Quadtree sin hijos cuya raíz
sea blanca.
2.3. Si c es la letra ‘N’, entonces inserte en el tope de la pila un Quadtree sin hijos cuya raíz
sea negra.
2.4. Si c es la letra ‘G’, entonces:
2.4.1. Cree un Quadtree llamado q cuya raíz sea gris.
2.4.2. Elimine el Quadtree que se encuentra en el tope de la pila y asígnelo como cuarto hijo
de q.
2.4.3. Elimine el Quadtree que se encuentra en el tope de la pila y asígnelo como tercer hijo
de q.
2.4.4. Elimine el Quadtree que se encuentra en el tope de la pila y asígnelo como segundo
hijo de q.
2.4.5. Elimine el Quadtree que se encuentra en el tope de la pila y asígnelo como primer hijo
de q.
2.4.6. Inserte en el tope de la pila el Quadtree q.
3. Entregue como resultado el Quadtree presente en el tope de la pila.
2. TRIES
2.1. DEFINICIÓN
ESTRUCTURAS DE DATOS 5
Cada nodo del árbol almacena una letra y posee un indicador de si el camino que parte de la
raíz y llega al nodo está conformado por letras que construyen una palabra. Se deben cumplir
las siguientes condiciones:
El nodo raíz representa la cadena vacía y almacena una letra especial que lo identifica.
Escogeremos el asterisco (‘*’) para distinguir al nodo raíz.
Cada nodo tiene una marca que indica si corresponde con el fin de una palabra. En nuestros
dibujos de Tries, los nodos marcados serían los pintados de azul.
Todo camino que parte del nodo raíz y llega a un nodo marcado representa una palabra del
conjunto (gráficamente serían los caminos que parten del nodo marcado con asterisco y que
llegan a un nodo azul).
Los hijos de todo nodo están ordenados según su letra (primero la ‘A’, luego la ‘B’ y así
sucesivamente).
Observe que los Tries nos permiten representar de forma eficiente y compacta conjuntos de
palabras, ahorrándonos espacio en memoria principal.
3. ÁRBOLES B
Los árboles binarios ordenados balanceados son apropiados para administrar eficientemente
datos en memoria principal (memoria RAM), pero no son aconsejables para administrar
información en memoria secundaria porque cada vez que deseemos consultar el valor de un
nodo es necesario realizar un acceso a disco duro, que es una operación mucho más lenta
que acceder a la memoria RAM. Pensando en reducir la cantidad de lecturas del disco duro
se concibieron los árboles B, que permiten guardar más de un valor por nodo.
Al permitir guardar más de un valor por nodo, la cantidad de nodos consultados de reduce y
por ende, la cantidad de accesos al disco duro también se reduce. Por lo tanto, los árboles B
son idóneos para representar sistemas de archivos y tablas ordenadas de las bases de datos,
que por su naturaleza deben residir en dispositivos de almacenamiento de acceso lento
como discos duros y memorias USB, pues como manejan grandes cantidades de datos (del
orden de terabytes), no pueden almacenarse en memoria RAM.
3.1. DEFINICIÓN
Gráfica 9: Árbol B de orden con nodos y valores.
ESTRUCTURAS DE DATOS 6
1. Guarda valores de tipo entero.
2. Es de orden , es decir, es un árbol eneario donde cada nodo tiene máximo hijos.
3. Todo nodo tiene mínimo hijos, exceptuando la raíz y las hojas.
4. La raíz no es una hoja y tiene mínimo hijos.
5. Todas las hojas se encuentran en el último nivel del árbol.
6. Todo nodo del árbol pertenece a una de las siguientes categorías:
6.1. Nodo hoja:
6.1.1. Tiene raíces de tipo entero y ordenadas de menor a mayor, donde
y , lo que asegura que haya mínimo una y máximo raíces.
6.1.2. No tiene subárboles.
6.2. Nodo interno -nodo (donde y ):
6.2.1. Tiene raíces de tipo entero, ordenadas de menor a mayor.
6.2.2. Tiene subárboles no vacíos , que denotan los hijos del
nodo.
6.2.3. Todos los elementos del subárbol son menores o iguales que la raíz .
6.2.4. Para todo desde hasta se satisface que todos los elementos del subárbol
son mayores o iguales que la raíz y menores o iguales que la raíz .
6.2.5. Todos los elementos del subárbol son mayores o iguales que la raíz .
Los árboles B mantienen ordenados los datos, están balanceados por altura y sirven para
buscar información eficientemente porque están ordenados. Así mismo, al requerir menos
nodos que los árboles binarios ordenados para albergar la misma cantidad de información,
son mejores para gestionar datos en la memoria secundaria.
Tabla 10: Ejemplos de árboles enearios que son árboles B de orden y que no lo son.
Árbol eneario ¿Es árbol B de orden ?
No, porque no todas las hojas
se encuentran en el mismo
nivel.
Sí es un árbol B de orden .
EN RESUMEN
Un Quadtree puede representar imágenes en blanco y negro de tamaño para algún ,
dándonos la posibilidad de manipular imágenes cuadradas donde su lado es una potencia de dos
( , , , , , , etcétera).
ESTRUCTURAS DE DATOS 7
Un Trie (pronunciado trai) es un árbol eneario capaz de representar un conjunto de palabras cuyas
letras se toman de cierto alfabeto.
Los árboles B permiten almacenar cantidades arbitrarias de datos por nodo, reduciendo
considerablemente el número de accesos que se deben efectuar sobre el disco duro. Los árboles B
son más apropiados que los árboles binarios para administrar datos en memoria secundaria en
dispositivos tales como los discos duros.
ESTRUCTURAS DE DATOS 8
ESTRUCTURAS DE DATOS
UNIDAD CUATRO - SEMANA SIETE
TABLAS DE HASHING *
TABLA DE CONTENIDO
1. MOTIVACIÓN 2
2. CONCEPTOS BÁSICOS 3
2.1. FUNCIONES DE HASHING 4
2.2. TABLAS DE HASHING 5
2.3. ORIGEN DEL TÉRMINO HASH 9
3. OPERACIONES BÁSICAS 10
3.1. ALGORITMO DE BÚSQUEDA 10
3.2. ALGORITMO DE INSERCIÓN 11
3.3. ALGORITMO DE ELIMINACIÓN 11
EN RESUMEN 12
PARA TENER EN CUENTA 12
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
1. MOTIVACIÓN
Sabemos que las listas son ineficientes para administrar información dado que nos ofrecen
operaciones de búsqueda, inserción y modificación de valores con complejidad temporal
donde es la cantidad de datos. ¿Pero qué pasaría si usamos varias listas para albergar
la misma información?
Tabla 2: Diez listas donde cada elemento del conjunto es insertado en la lista de la posición .
Número de lista Valores que contiene la lista
ESTRUCTURAS DE DATOS 2
. A continuación, construyamos nuevas listas identificadas con índices enteros de a
(es decir, de a ), e insertemos cada elemento del conjunto en la lista cuyo
número sea el descrito por la función .
Tabla 3: Veinte listas donde cada elemento del conjunto es insertado en la lista de la posición .
Número de lista Valores que contiene la lista
Conforme aumenta, cada una de las listas va a quedar con menos elementos. En
particular, cuando en nuestro ejemplo, ¡cada valor queda en su propia lista sin
compartirla con ningún otro elemento!
A este tipo de estructuras de datos se les llama Tablas de Hashing, y la función que se
aplicó para determinar la fila asociada con cada elemento se denomina función de Hashing.
Si el número de listas se configura adecuadamente, es improbable que dos valores
compartan la misma fila, lo que aseguraría operaciones básicas de búsqueda, inserción y
eliminación con complejidad temporal . Pero si el número de listas resulta ser muy
pequeño comparado con la cantidad de datos en el conjunto, las mismas operaciones
terminarían siendo .
2. CONCEPTOS BÁSICOS
La teoría expuesta en esta sección está inspirada en las siguientes referencias: 1. Diseño y
Manejo de Estructuras de Datos en C de Jorge Villalobos † y 2. Estructuras de datos en C de
Luis Joyanes et al. ‡
†
VILLALOBOS, Jorge A. Diseño y Manejo de Estructuras de Datos en C. Bogotá: McGraw-Hill, 1996.
ESTRUCTURAS DE DATOS 3
2.1. FUNCIONES DE HASHING
Dado un tipo de datos , una función de Hashing es una función que toma como
parámetro un valor de tipo para entregar como resultado un número entero.
donde
son los dígitos
de en base diez.
Para diseñar funciones de Hashing sobre cadenas de texto es necesario conocer cómo son
codificados los caracteres en el lenguaje de programación. El estándar de codificación de
caracteres Unicode es el utilizado por Java para representar los caracteres de las cadenas de
texto, permitiendo el uso de símbolos especiales como vocales tildadas, operadores
matemáticos, letras del alfabeto griego e ideogramas chinos, entre otros.
Tabla 5: Códigos Unicode de las letras minúsculas y mayúsculas del idioma español,
incluyendo las tildes, la eñe (ñ) y la diéresis (ü).
Letra Código Letra Código Letra Código Letra Código
A 65 R 82 a 97 r 114
B 66 S 83 b 98 s 115
C 67 T 84 c 99 t 116
D 68 U 85 d 100 u 117
E 69 V 86 e 101 v 118
F 70 W 87 f 102 w 119
G 71 X 88 g 103 x 120
H 72 Y 89 h 104 y 121
I 73 Z 90 i 105 z 122
J 74 Á 193 j 106 á 225
K 75 É 201 k 107 é 233
L 76 Í 205 l 108 í 237
M 77 Ó 211 m 109 ó 243
N 78 Ú 218 n 110 ú 250
O 79 Ü 220 o 111 ü 252
P 80 Ñ 209 p 112 ñ 241
Q 81 q 113
‡
JOYANES, Luis et al. Estructuras de datos en C. Madrid: McGraw-Hill, 2005.
ESTRUCTURAS DE DATOS 4
Tabla 6: Algunas funciones de Hashing sobre el tipo de datos de las cadenas de texto ( ).
Función de Hashing Ejemplos
suma de los
códigos Unicode de los
caracteres de la cadena de
texto
donde es
el código Unicode del
carácter de la posición de
la cadena de texto , y es
la longitud de
En Java, para dotar todos los objetos de una clase con una función de Hashing, se debe
reimplementar el método
public int hashCode()
Se define una Tabla de Hashing como una estructura de datos que está conformada por un
arreglo de listas y por una función de Hashing, donde el arreglo de listas puede ser visto
como una tabla donde cada una de las listas corresponde a una fila.
Tabla 7: Una Tabla de Hashing con un arreglo de listas de tamaño y una función de Hashing específica.
0
1
2
3
4
5
6
7
8
9
En general, las Tablas de Hashing sirven para representar conjuntos y las funciones de
Hashing tienen como propósito asociarle exactamente una fila a cada uno de los valores del
conjunto.
ESTRUCTURAS DE DATOS 5
Tabla 8: Conceptos básicos sobre las Tablas de Hashing, siendo el tipo de datos
de los elementos del conjunto que representa.
Concepto Definición
Función de Es una función que dado un valor de tipo , entrega como resultado un
Hashing número entero.
Tabla de Es una estructura de datos que está conformada por un arreglo de listas y por una
Hashing función de Hashing que indica de cierta manera la fila asociada con cada elemento.
Fila Es una lista de la Tabla de Hashing.
Capacidad Es el número de filas de la Tabla de Hashing. A lo largo de la lectura, representaremos
la capacidad de la Tabla de Hashing con la variable .
Tamaño Es la cantidad de elementos almacenados en la Tabla de Hashing. A lo largo de la
lectura, representaremos el tamaño de la Tabla de Hashing con la variable .
Factor de Es una medida de qué tan llena se encuentra la Tabla de Hashing. Se calcula como el
carga tamaño de la tabla dividido por la capacidad de la tabla. A lo largo de la lectura,
representaremos el factor de carga de la Tabla de Hashing con la variable .
Más adelante veremos cómo las Tablas de Hashing pueden representar asociaciones llave-
valor, donde los valores del conjunto representado por la Tabla de Hashing son vistos como
llaves con las que se acceden valores. Por ejemplo, en una asociación llave-valor, las llaves
pueden ser los documentos de identificación de ciertas personas y los valores pueden
describir la información detallada de estas personas.
ESTRUCTURAS DE DATOS 6
Tabla 9: Algunas Tablas de Hashing con distintas capacidades, que representan el conjunto de números enteros
.
Tabla de Hashing Propiedades
Propiedad Valor
Función de Hashing
0 Capacidad
1
2 Tamaño
3
4 Factor de carga .
Dirección de en la tabla
Elementos que colisionan Todos ( en total).
0 Propiedad Valor
1 Función de Hashing
2
3 Capacidad
4 Tamaño
5
6 Factor de carga .
7
8 Dirección de en la tabla
9 Elementos que colisionan Todos ( en total).
Por estas razones es que el factor de carga es una medida de qué tan poblada está la Tabla
de Hashing y de qué tan probable es que suceda una colisión si se insertara un dato más a la
tabla. En particular, cuando la capacidad es igual al tamaño , se tiene un factor de carga
del ( ).
Evidentemente, si el factor de carga supera el entonces hay más datos que filas (
) y con plena seguridad hay colisiones en la Tabla de Hashing. Para que las Tablas de Hashing
no se llenen excesivamente ni se degeneren, debemos evitar a toda costa que el factor de
carga supere cierto umbral , que definiremos subjetivamente como
. Si fuésemos capaces de asegurar que el factor de carga siempre sea menor o
igual que el umbral del ( ), se cumpliría que el número de datos en la
tabla ( ) no es superior que las tres cuartas partes del número de filas de la tabla. De esta
forma, en la Tabla de Hashing no habrían muchas colisiones y se podría tener un buen
desempeño en las operaciones de búsqueda, inserción y eliminación, con complejidad
temporal siempre y cuando la función de Hashing esté bien configurada.
ESTRUCTURAS DE DATOS 7
Es aconsejable que el número de filas ( ) se configure inicialmente como una cantidad mayor
o igual que cuatro tercios el número de datos que se pretenden insertar en la Tabla de
Hashing ( ), para que el factor de carga siempre sea menor o igual que (en este
escenario, ). Por ejemplo, si se desean
agregar elementos se recomiendan mínimo filas, y si se desean agregar
elementos se recomiendan mínimo filas.
Consejos a tener en cuenta cuando esté diseñando o usando una Tabla de Hashing:
Configure adecuadamente la capacidad inicial ( ) de su Tabla de Hashing para que las
operaciones básicas de búsqueda, inserción y eliminación de datos sean eficientes, porque
de lo contrario, la tabla se podría comportar como una lista en la que la complejidad de
dichas operaciones se degrada a . Se recomienda que la capacidad inicial ( ) sea mayor
o igual que cuatro tercios la máxima cantidad de datos que se piensan almacenar en la Tabla
de Hashing ( ).
Entre más capacidad tenga la Tabla de Hashing, mejor es la eficiencia de las operaciones
básicas, pero se sacrifica más memoria RAM.
No abuse de la memoria RAM para aumentar indiscriminadamente la capacidad de la Tabla
de Hashing. En todo caso, analice cuidadosamente cuánta RAM está dispuesto a darle a la
tabla de tal forma que no se desperdicie y que se cumpla la recomendación de la capacidad
inicial.
Si por algún motivo se calculó mal la capacidad inicial ( ) de la Tabla de Hashing,
eventualmente el factor de carga ( ) superará el umbral del y será necesario aplicar
algún correctivo de emergencia para disminuirlo. Este correctivo se llama
redimensionamiento dinámico (rehashing) y consiste en crear una nueva Tabla de Hashing
con el doble de capacidad (por ejemplo) y con la misma función de Hashing, trasladar los
elementos de la vieja tabla a la nueva tabla conforme a la nueva capacidad, y desechar la
vieja tabla. La anterior operación es tremendamente ineficiente porque reubica todos los
elementos almacenados, pero no se debe aplicar sino cuando se supere el umbral del .
Note que al duplicar la capacidad ( ) en estos casos, el factor de carga ( ) se reduciría a la
mitad, quedando aproximadamente en .
Por naturaleza, en una Tabla de Hashing los elementos no se guardan de forma ordenada. Si
desea mantener ordenados sus elementos de acuerdo a alguna relación de orden, use un
árbol binario ordenado como los árboles AVL o como los árboles Roji-negros, en vez de una
Tabla de Hashing.
En Java, los objetos de una clase se pueden dotar de una función de Hashing
reimplementando el método
public int hashCode()
que se encuentra declarado en la clase Object, que es extendida por todas las clases en Java.
Si no se implementa adecuadamente la función de Hashing, puede suceder que las
operaciones básicas se vuelvan ineficientes, independientemente de cómo se haya
configurado la capacidad de la tabla. Por ejemplo, si la función de Hashing es
ESTRUCTURAS DE DATOS 8
entonces todos los elementos serían puestos en la primera fila, sin importar la cantidad de
filas que la tabla posea.
Tabla 10: Criterios que deben satisfacer las funciones de Hashing para un buen desempeño de la Tabla de Hashing.
Criterio Descripción
Constante a lo Para todo elemento de tipo , el valor debe ser el mismo en todo
largo del momento. Si un dato está presente dentro de la Tabla de Hashing y
tiempo cambia, entonces el algoritmo de búsqueda no sería capaz de encontrarlo y el
algoritmo de eliminación no sería capaz de removerlo.
Entrega valores La función de Hashing debe dar resultados que se distribuyan en un rango amplio
dispersos de valores. Si la función tuviera un rango limitado, entonces los datos serían
acumulados en unas pocas filas de la Tabla de Hashing. Pero si la función tuviera
un rango disperso, se esperaría que los datos se distribuyan uniformemente en
todas las filas de la tabla, logrando que posean aproximadamente el mismo
tamaño y que disminuya el número de colisiones.
Fácil de La función de Hashing debe ser fácil de calcular. Si la función requiriera de
calcular operaciones complejas, se emplearía gran cantidad de tiempo computándola, lo
que afectaría negativamente el desempeño de las operaciones básicas. Es
preferible diseñar una función cuya evaluación sea veloz y que distribuya los
valores razonablemente, que una función lenta que garantice dispersión perfecta.
La palabra inglesa hash traduce textualmente al español picar o trocear §, que trata de
describir con un verbo la operación que se realiza sobre los elementos del conjunto para
determinar cuál fila de la tabla se le asocia. Pese a que definimos arbitrariamente que la
dirección de en la Tabla de Hashing es el resultado de la expresión donde es la
función de Hashing y es la capacidad de la tabla, existen más formas de trocear el valor de
la función para que arroje un número entre y :
**
Tabla 11: Técnicas típicas para distribuir los valores de una función de Hashing entre y ,
donde es la capacidad de la tabla.
Técnica Algoritmo para hallar la dirección de un valor en la tabla de Hashing
Funciones de Se evalúa la fórmula Es aconsejable usar un que sea número primo
división pues así se garantiza una mayor dispersión en los resultados de la función.
Funciones de Se evalúa , se representa en base binaria o decimal, y se le quitan dígitos
truncamiento en cierto orden preestablecido hasta que se obtenga un número menor que .
§
Según la Real Academia Española, picar es cortar o dividir en trozos muy menudos, y trocear es dividir en
trozos.
**
Basado en la referencia: JOYANES, Luis et al. Estructuras de datos en C. Madrid: McGraw-Hill, 2005.
ESTRUCTURAS DE DATOS 9
Funciones de Se evalúa , se representa en base decimal, se parte en pedazos que
plegamiento tengan la misma cantidad de dígitos (salvo el último pedazo, que puede tener
menos dígitos), y finalmente se suman o se multiplican estos pedazos.
Mientras el número obtenido no sea menor que , el proceso se repite con
pedazos cada vez más pequeños.
3. OPERACIONES BÁSICAS
Tabla 13: Análisis de complejidad temporal del proceso de búsqueda en Tablas de Hashing.
Tipo de análisis Complejidad Justificación
La complejidad temporal es cuando la función de Hashing
está mal diseñada y la búsqueda termina inspeccionando una fila
que concentra la mayoría de los elementos albergados en toda la
Peor caso
tabla. Por otro lado, si la función de Hashing fuese difícil de
calcular, el desempeño del algoritmo quedaría sujeto a la
eficiencia de dicha función.
Caso promedio Si la función de Hashing está bien diseñada entonces es fácil de
ESTRUCTURAS DE DATOS 10
calcular y distribuye los valores uniformemente, asegurando
pocas colisiones y por ende, una complejidad temporal de
en el proceso de búsqueda.
Tabla 14: Análisis de complejidad temporal del proceso de inserción en Tablas de Hashing.
Tipo de análisis Complejidad Justificación
La complejidad temporal es en los siguientes escenarios:
Cuando la función de Hashing está mal diseñada y la inserción
termina operando sobre una fila que concentra la mayoría de los
elementos albergados en toda la tabla.
Cuando después de efectuar la inserción el factor de carga
Peor caso
supera el umbral, pues se terminaría ejecutando un
redimensionamiento dinámico.
No olvide que si la función de Hashing fuese difícil de calcular, el
desempeño del algoritmo quedaría sujeto a la eficiencia de dicha
función.
Si la función de Hashing está bien diseñada entonces es fácil de
calcular y distribuye los valores uniformemente, asegurando
Caso promedio pocas colisiones y por ende, una complejidad temporal de
en el proceso de inserción, suponiendo que el factor de carga no
supera el umbral después de la operación.
ESTRUCTURAS DE DATOS 11
Tabla 15: Análisis de complejidad temporal del proceso de eliminación en Tablas de Hashing.
Tipo de análisis Complejidad Justificación
La complejidad temporal es cuando la función de Hashing
está mal diseñada y la eliminación termina operando sobre una
fila que concentra la mayoría de los elementos albergados en
Peor caso
toda la tabla. Recuerde que si la función de Hashing fuese difícil
de calcular, el desempeño del algoritmo quedaría sujeto a la
eficiencia de dicha función.
Si la función de Hashing está bien diseñada entonces es fácil de
calcular y distribuye los valores uniformemente, asegurando
Caso promedio
pocas colisiones y por ende, una complejidad temporal de
en el proceso de eliminación.
EN RESUMEN
Una función de Hashing es una función que dado un valor de cierto tipo, entrega como resultado un
número entero. Debe ser constante a lo largo del tiempo, debe entregar valores dispersos, y debe ser
fácil de calcular.
En Java, para dotar todos los objetos de una clase con una función de Hashing, se debe
reimplementar el método hashCode().
Una Tabla de Hashing es una estructura de datos que está conformada por un arreglo de listas y por
una función de Hashing que indica de cierta manera la fila asociada con cada elemento.
La complejidad de los algoritmos de búsqueda, inserción y eliminación sobre las Tablas de Hashing
son si la tabla y la función de Hashing están bien configuradas. De lo contrario, la complejidad se
puede degradar a .
ESTRUCTURAS DE DATOS 12
ESTRUCTURAS DE DATOS
UNIDAD CUATRO - SEMANA SIETE
CONJUNTOS, ASOCIACIONES LLAVE-VALOR Y BOLSAS *
TABLA DE CONTENIDO
1. CONJUNTOS 2
1.1. DEFINICIÓN 2
1.2. LA INTERFAZ SET<E> 2
1.3. IMPLEMENTACIONES SUMINISTRADAS POR JAVA PARA LA INTERFAZ SET<E> 3
2. ASOCIACIONES LLAVE-VALOR 5
2.1. DEFINICIÓN 5
2.2. LA INTERFAZ MAP<K,V> 6
2.3. IMPLEMENTACIONES SUMINISTRADAS POR JAVA PARA LA INTERFAZ MAP<K,V> 7
3. BOLSAS (MULTICONJUNTOS) 8
EN RESUMEN 9
PARA TENER EN CUENTA 9
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
1. CONJUNTOS
1.1. DEFINICIÓN
Tabla 1: Los quince métodos sin implementación ofrecidos por la interfaz Set<E>.
Método Descripción
Operaciones de consulta sobre el conjunto
int size() Retorna el número de elementos del conjunto.
boolean isEmpty() Retorna true si y sólo si el conjunto es vacío.
Operaciones de búsqueda sobre el conjunto
boolean contains(Object obj)
Retorna true si y sólo si obj pertenece al
conjunto.
boolean containsAll(Collection<?> coll)
Retorna true si y sólo si todos los objetos de la
colección coll están dentro del conjunto.
Operaciones de inserción sobre el conjunto
Agrega el valor element al conjunto. Retorna
boolean add(E element) true si y sólo si el conjunto no contenía el valor
dado.
Agrega todos los elementos de la colección
boolean addAll(Collection<? extends E> coll) coll al conjunto. Retorna true si y sólo si el
conjunto fue modificado por la operación.
Operaciones de eliminación sobre el conjunto
void clear() Elimina todos los elementos del conjunto.
boolean remove(Object obj)
Elimina el valor obj del conjunto. Retorna true
si y sólo si el conjunto sí contenía el valor dado.
†
La documentación de la interfaz Set<E> está disponible en el API de Java en el enlace
http://java.sun.com/javase/6/docs/api/java/util/Set.html.
ESTRUCTURAS DE DATOS 2
Elimina del conjunto todos los elementos que
boolean removeAll(Collection<?> coll)
estén en la colección coll. Retorna true si y
sólo si el conjunto fue modificado por la
operación.
Retiene en el conjunto sólo los elementos que
boolean retainAll(Collection<?> coll)
estén en la colección coll. Retorna true si y
sólo si el conjunto fue modificado por la
operación.
Métodos que entregan iteradores sobre el conjunto
Iterator<E> iterator()
Retorna un iterador capaz de visitar todos los
elementos del conjunto.
Métodos que entregan un arreglo con el contenido del arreglo
Object[] toArray()
Retorna un arreglo con todos los elementos del
conjunto.
Retorna un arreglo con todos los elementos del
<T> T[] toArray(T[] array)
conjunto. Se usa array para guardar los
elementos del conjunto si éstos caben dentro
del arreglo.
Métodos de comparación
boolean equals(Object obj)
Retorna true si y sólo si obj es un conjunto
igual a este conjunto.
Métodos de hashing
int hashCode()
Retorna el resultado de evaluar la función de
Hashing sobre el conjunto.
ESTRUCTURAS DE DATOS 3
Un árbol Roji-negro es un árbol binario ordenado tal que:
1. Todos sus nodos están coloreados de rojo o de color negro (el color es
un atributo de los nodos de un árbol Roji-negro, que sólo puede tomar dos
posibles valores: rojo o negro).
2. Su raíz está coloreada de negro.
Árboles Roji-negros 3. Todos sus subárboles vacíos están coloreados de negro y son
considerados como nodos del árbol cuyo valor almacenado es null.
4. Todos los hijos de un nodo coloreado de rojo deben estar coloreados de
negro.
5. Todos los caminos que parten de un nodo y llegan a un subárbol vacío
deben pasar por la misma cantidad de nodos coloreados de negro.
Una Tabla de Hashing es una estructura de datos que está conformada por
Tablas de Hashing un arreglo de listas y por una función de Hashing que indica de cierta
manera la fila asociada con cada elemento.
Tabla 3: Complejidad temporal de las operaciones básicas sobre cada estructura de datos de la tabla anterior,
según el tipo de análisis. Suponga que es la cantidad de elementos en el conjunto
y que es la altura de cualquier árbol involucrado en el análisis.
Listas Árboles Árboles Árboles Tablas de
Operación Tipo de análisis
ordenadas ordenados AVL Roji-negros Hashing
Peor caso
Búsqueda
Caso promedio
Peor caso
Inserción
Caso promedio
Peor caso
Eliminación
Caso promedio
Tabla 4: Algunos criterios importantes para decidir cuál estructura de datos preferir para representar conjuntos.
Si desea … Prefiera usar …
eficiencia en las búsquedas y gastar poca memoria RAM, sabiendo que la
cantidad de inserciones y eliminaciones es insignificante comparado con la Listas ordenadas.
cantidad de búsquedas que se van a efectuar.
eficiencia en las búsquedas, inserciones y eliminaciones, donde tienen
Árboles AVL.
prelación las búsquedas.
eficiencia en las búsquedas, inserciones y eliminaciones, donde tienen
Árboles Roji-negros.
prelación las inserciones y eliminaciones.
mantener sus datos siempre ordenados para poder recorrerlos Árboles AVL ó
eficientemente de menor a mayor cuando se desee. Árboles Roji-negros.
eficiencia con complejidad temporal en las búsquedas, inserciones y
eliminaciones, donde el programador está dispuesto a sacrificar memoria
Tablas de Hashing.
RAM y a sacrificar tiempo para diseñar una función de Hashing y para
configurar la estructura de datos.
eficiencia razonable en las búsquedas, inserciones y eliminaciones, sin
preocuparse por detalles técnicos de ningún tipo y sin descartar la
Árboles Roji-negros.
posibilidad de aprovechar una implementación fiable que ya esté disponible
en la librería estándar de Java.
ESTRUCTURAS DE DATOS 4
En Java, una clase es sincronizada si todos sus métodos están sincronizados, es decir, si se
debe evitar que dos hijos de ejecución accedan en el mismo instante de tiempo dos
operaciones sobre el mismo objeto, obligándolos a que hagan cola uno detrás del otro para
que tengan permiso de operar sobre éste sin estorbarse entre sí ‡.
2. ASOCIACIONES LLAVE-VALOR
2.1. DEFINICIÓN
Una asociación llave-valor es una estructura de datos que asocia valores a los elementos de
un conjunto, denominados llaves o claves. Es importante enfatizar el hecho de que cada llave
es única y que tiene asociado exactamente un valor, aunque es posible que llaves distintas
tengan asociado el mismo valor. En otras palabras, una asociación llave-valor está
conformada por un conjunto de llaves únicas y por una colección de valores, donde cada
llave tiene asociado exactamente un valor.
‡
Para mayor información sobre hijos de ejecución y sobre sincronización de procesos en Java, véase el enlace
http://java.sun.com/docs/books/tutorial/essential/concurrency/sync.html.
ESTRUCTURAS DE DATOS 5
2.2. LA INTERFAZ MAP<K,V>
La interfaz Map<K,V> del paquete java.util representa una asociación llave-valor donde las
llaves son elementos de tipo K y los valores son elementos de tipo V §. Los tipos K y V son
genéricos: pueden ser cualquier clase en Java. Por ejemplo, Map<String,Persona> representa
una asociación llave-valor donde las llaves con cadenas de texto y los valores son personas.
Tabla 8: Los catorce métodos sin implementación ofrecidos por la interfaz Map<K,V>.
Método Descripción
Operaciones de consulta sobre la asociación llave-valor
int size()
Retorna el número de asociaciones llave-valor
en el mapa.
boolean isEmpty()
Retorna true si y sólo si el mapa no contiene
ninguna asociación llave-valor.
boolean containsKey(Object key)
Retorna true si y sólo si el mapa contiene una
asociación para la llave key.
Retorna true si y sólo si el mapa contiene por lo
boolean containsValue(Object value) menos una llave que esté asociada con el valor
value.
Retorna el valor asociado con la llave key. En
V get(Object key)
caso de que en el mapa no exista ninguna
asociación para la llave key, retorna el valor
null.
Operaciones de modificación sobre la asociación llave-valor
Asocia la llave key con el valor value. Si antes de
realizar la operación el mapa ya tenía una
asociación para la llave key, entonces el viejo
V put(K key, V value)
valor asociado se reemplaza por el valor value.
Retorna como resultado el valor que estaba
previamente asociado a la llave key antes de la
operación, o null si no había ningún valor
asociado a la llave key.
§
La documentación de la interfaz Map<K,V> está disponible en el API de Java en el enlace
http://java.sun.com/javase/6/docs/api/java/util/Map.html.
ESTRUCTURAS DE DATOS 6
Elimina la asociación correspondiente a la llave
key. Si antes de realizar la operación el mapa no
tenía una asociación para la llave key, entonces
V remove(Object key) el mapa no se altera. Retorna como resultado el
valor que estaba previamente asociado a la llave
key antes de la operación, o null si no había
ningún valor asociado a la llave key.
void putAll(Map<? extends K, ? extends V> m)
Añade en el mapa todas las asociaciones llave-
valor presentes en el mapa m.
void clear() Elimina todas las asociaciones del mapa.
Operaciones que entregan vistas sobre la asociación llave-valor
Set<K> keySet()
Retorna un conjunto que contiene las llaves
presentes en el mapa.
Retorna una colección que contiene los valores
Collection<V> values()
presentes en el mapa.
Retorna un conjunto que contiene las
Set<Map.Entry<K,V>> entrySet()
asociaciones presentes en el mapa.
La clase Map.Entry<K,V> representa una llave
de tipo K asociada con un valor de tipo V.
Métodos de comparación
Retorna true si y sólo si obj es una asociación
boolean equals(Object obj) llave-valor igual a este mapa (es decir, si
contienen las mismas asociaciones).
Métodos de hashing
int hashCode()
Retorna el resultado de evaluar la función de
Hashing sobre el mapa.
Para implementar una asociación llave-valor basta escoger una representación para el
conjunto de las llaves y asignar a cada llave un campo adicional que almacene el valor
asociado. Por ejemplo, en un árbol Roji-negro podemos guardar en cada nodo la información
correspondiente a las llaves y contar con un registro adicional que apunte al valor asociado a
la llave.
Dado que es suficiente representar el conjunto de las llaves con alguna estructura de datos
diseñada para tal efecto, y enlazar cada llave del conjunto con su valor asociado, entonces
todas las consideraciones mencionadas cuando exploramos los conjuntos también aplican
para las asociaciones llave-valor.
ESTRUCTURAS DE DATOS 7
Tabla 9: Implementaciones clásicas provistas por Java para la interfaz Map<K,V>.
Paquete Clase Estructura de datos ¿Clase sincronizada?
java.util TreeMap<K,V> Árbol Roji-negro No
java.util HashMap<K,V> Tabla de Hashing No
java.util Hashtable<K,V> Tabla de Hashing Si
Tabla 10: Evolución de una asociación llave-valor tras una secuencia de operaciones de ejemplo.
Estado del mapa Operación Descripción
Map<String,Integer> mapa=
new TreeMap<String,Integer>(); Crear un mapa vacío.
mapa.put("Luz",51);
Asociar la llave "Luz" con
el valor en el mapa.
mapa.put("Mar",24);
Asociar la llave "Mar" con
el valor en el mapa.
Asociar la llave "Día" con
mapa.put("Día",40);
el valor en el mapa.
Asociar la llave "Bus" con
mapa.put("Bus",28);
el valor en el mapa.
Asociar la llave "Mar" con
mapa.put("Mar",45);
el valor en el mapa.
Asociar la llave "Día" con
mapa.put("Día",93);
el valor en el mapa.
Eliminar la asociación
, mapa.remove("Día"); correspondiente a la llave
"Día" del mapa.
Asociar la llave "Día" con
mapa.put("Día",45);
el valor en el mapa.
Eliminar la asociación
mapa.remove("Bus"); correspondiente a la llave
"Bus" del mapa.
Eliminar la asociación
mapa.remove("Luz"); correspondiente a la llave
"Luz" del mapa.
3. BOLSAS (MULTICONJUNTOS)
Una bolsa es una colección de elementos de cierto tipo, que no tienen orden pero sí pueden
tener repeticiones. También son conocidas como multiconjuntos por su capacidad de
almacenar repeticiones múltiples de los valores.
Tabla 11: Diferencia entre las listas, los conjuntos y las bolsas, vistas como colecciones de elementos.
Colección ¿Importa el orden? ¿Importan las repeticiones?
Listas Si Si
Conjuntos No No
ESTRUCTURAS DE DATOS 8
Bolsas No Si
Una bolsa se puede representar con una asociación llave-valor donde las llaves son
elementos distintos y los valores son enteros que indican cuántas veces aparece cada llave
dentro de la bolsa. Por ejemplo, la bolsa cuyos elementos son es
representada por la asociación llave-valor donde la llave tiene valor , la llave tiene valor
, la llave tiene valor , y la llave tiene valor , cumpliendo que el valor asociado a cada
llave informa cuántas veces aparece dentro de la bolsa.
EN RESUMEN
Un conjunto es una colección de elementos de cierto tipo, que no tienen orden ni repeticiones. A
diferencia de una lista, en un conjunto los valores aparecen máximo una vez y no tienen asociada una
posición.
Una asociación llave-valor está conformada por un conjunto de llaves únicas y por una colección de
valores, donde cada llave tiene asociado exactamente un valor.
Una bolsa es una colección de elementos de cierto tipo, que no tienen orden pero sí pueden tener
repeticiones.
La interfaz Set<E> representa un conjunto de elementos de tipo E. La clase TreeSet<E> implementa
conjuntos con árboles Roji-negros y la clase HashSet<E> implementa conjuntos con Tablas de
Hashing.
La interfaz Map<K,V> representa una asociación llave-valor que asocia llaves de tipo K con valores de
tipo V. La clase TreeMap<K,V> implementa asociaciones llave-valor con árboles Roji-negros, y las
clases HashMap<K,V> y Hashtable<K,V> implementan asociaciones llave-valor con Tablas de Hashing.
Una bolsa se puede representar con una asociación llave-valor donde las llaves son elementos
distintos y los valores son enteros que indican cuántas veces aparece cada llave dentro de la bolsa.
ESTRUCTURAS DE DATOS 9
ESTRUCTURAS DE DATOS
UNIDAD CUATRO - SEMANA SIETE
EJERCICIOS PROPUESTOS *
1. ALGORITMO DE AHO-CORASICK
2. ÁRBOLES DE SINTAXIS
Implemente una clase VEDArbolSintaxis que represente un árbol de sintaxis donde los nodos
pueden tener como valor un número flotante o un operador aritmético (suma, resta,
multiplicación o división).
Gráfica: Árbol de sintaxis de la expresión aritmética .
+
* / –
– 7 3 1 +
2
5 1 2 7
3. CONSTRUCCIÓN DE QUADTREES
Dibuje el Quadtree correspondiente a cada una de las siguientes imágenes blanco y negro, y
luego halle el recorrido en postorden de cada Quadtree.
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
Imagen blanco y negro Quadtree Postorden
4. RECONSTRUCCIÓN DE QUADTREES
ESTRUCTURAS DE DATOS 2
ESTRUCTURAS DE DATOS
UNIDAD CUATRO - SEMANA OCHO
CONCEPTOS BÁSICOS SOBRE GRAFOS *
TABLA DE CONTENIDO
1. DEFINICIÓN 2
2. CONCEPTOS BÁSICOS 3
2.1. GRAFOS DIRIGIDOS Y GRAFOS NO DIRIGIDOS 3
2.2. SUCESORES Y ANTECESORES 4
2.3. FUENTES, SUMIDEROS Y NODOS AISLADOS 5
2.4. GRADO DE UN NODO 5
2.5. GRAFOS COMPLETOS, GRAFOS DENSOS Y GRAFOS DISPERSOS 6
2.6. CAMINOS Y CADENAS 7
2.7. CICLOS, GRAFOS CÍCLICOS Y GRAFOS DIRIGIDOS ACÍCLICOS 8
2.8. DESCENDIENTES Y ANCESTROS 8
2.9. GRAFOS CONEXOS Y GRAFOS FUERTEMENTE CONEXOS 9
2.10. SUBGRAFOS, COMPONENTES CONEXAS Y COMPONENTES FUERTEMENTE CONEXAS 10
2.11. CAMINOS Y CICLOS HAMILTONIANOS 11
2.12. CAMINOS Y CICLOS EULERIANOS 12
3. REPRESENTACIÓN 15
3.1. ALMACENAMIENTO DE LOS NODOS 15
3.2. ALMACENAMIENTO DE LOS ARCOS 15
EN RESUMEN 18
PARA TENER EN CUENTA 18
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
1. DEFINICIÓN
A los nodos también se les llama vértices y a los arcos también se les llama aristas o enlaces.
El producto cartesiano , visto como el conjunto de todas las
parejas de la forma donde ambas componentes son valores de tipo , representa
todos los posibles arcos en un grafo cuyo conjunto de nodos es .
Adicionalmente, los arcos de un grafo se pueden etiquetar con costos a través de una función
de costos , que dado un arco del conjunto da como resultado el costo de ir
del nodo al nodo .
ESTRUCTURAS DE DATOS 2
Tabla 3: Algunos grafos de ejemplo.
Notación gráfica Notación formal
, , ,
, , .
, , , , ,
, , , , ,
, , .
, , , ,
, , , ,
, .
Observe que el conjunto de arcos de un grafo puede ser vacío. A lo largo del desarrollo de la
teoría representaremos el número de nodos con la variable y el número de arcos con la
variable Las definiciones introducidas en esta lectura podrían diferir un poco respecto a
otros textos pues no hay consenso estándar sobre éstas.
2. CONCEPTOS BÁSICOS
En otras palabras, un grafo no dirigido es un grafo donde todos sus arcos se pueden visitar en
ambos sentidos, y un grafo dirigido es un grafo donde uno de sus arcos puede recorrerse en
una dirección pero no en la otra.
ESTRUCTURAS DE DATOS 3
Tabla 5: Ejemplos sobre los conceptos de grafo no dirigido y grafo dirigido.
Grafo Tipo de grafo Número de nodos ( ) Número de arcos ( )
Grafo dirigido
Grafo no dirigido
Grafo no dirigido
Observe lo siguiente:
Un nodo es antecesor de un nodo si y sólo si es sucesor de .
Para todo arco se cumple que su origen es antecesor de su destino y que su destino es
sucesor de su origen.
Para todo nodo de un grafo no dirigido se cumple que sus antecesores, que sus sucesores y
que sus adyacentes son los mismos.
Dos nodos son adyacentes si y sólo si uno de los dos es sucesor del otro, es decir, si y sólo si
hay un arco que va del uno al otro.
ESTRUCTURAS DE DATOS 4
2.3. FUENTES, SUMIDEROS Y NODOS AISLADOS
Tabla 8: Definición de fuente, sumidero y nodo aislado.
Concepto Definición
Fuente Un nodo es una fuente si y sólo si no tiene antecesores.
Sumidero Un nodo es un sumidero si y sólo si no tiene sucesores.
Nodo aislado Un nodo es un nodo aislado si y sólo si no tiene antecesores ni sucesores.
Con esta nueva terminología podemos definir una fuente como un nodo con grado de
entrada cero, un sumidero como un nodo con grado de salida cero y un nodo aislado como
un nodo con grado de entrada cero y con grado de salida cero.
ESTRUCTURAS DE DATOS 5
2.5. GRAFOS COMPLETOS, GRAFOS DENSOS Y GRAFOS DISPERSOS
Tabla 12: Definición de grafo completo, grafo denso y grafo disperso.
Concepto Definición
Un grafo es un grafo completo si y sólo si para todo par de nodos
Completo
distintos y , se cumple que hay un arco desde hacia .
Subjetivamente, un grafo es un grafo denso si y sólo si está cerca de
Denso
, donde es la cantidad de nodos y es la cantidad de arcos.
Subjetivamente, un grafo es un grafo disperso si y sólo si está por
Disperso debajo de un múltiplo constante , donde es la cantidad de nodos y es la
cantidad de arcos.
, , , , ,
, , , , ,
Los términos denso y disperso pretenden describir de una manera subjetiva qué tantos arcos
posee un grafo: si tiene muchísimos arcos en relación al número de nodos se dice que el
grafo es denso, y si tiene poquitos se dice que el grafo es disperso.
ESTRUCTURAS DE DATOS 6
Gráfica 14: Algunos grafos de ejemplo, tanto densos como dispersos.
ESTRUCTURAS DE DATOS 7
2.7. CICLOS, GRAFOS CÍCLICOS Y GRAFOS DIRIGIDOS ACÍCLICOS
Tabla 17: Definición de ciclo, grafo cíclico y grafo dirigido acíclico (DAG).
Concepto Definición
Ciclo Un ciclo es un camino cuyo origen es igual a su destino.
Ciclo simple Un ciclo simple es un camino simple cuyo origen es igual a su destino.
Cíclico Un grafo es un grafo cíclico si y sólo si tiene por lo menos un ciclo.
Acíclico Un grafo es un grafo acíclico si y sólo si no tiene ningún ciclo.
Un grafo es un grafo dirigido acíclico si y sólo si es un grafo dirigido
que no tiene ningún ciclo. Estos grafos se conocen en inglés como Directed
Grafo dirigido
Acyclic Graphs (DAG por sus siglas), y tienen especial importancia en la teoría de
acíclico
grafos porque todo camino en un DAG recorre nodos distintos sin caer en un
ciclo, lo que facilita el proceso de recorrerlo.
Tabla 18: Ejemplos sobre los conceptos de ciclo, grafo cíclico y grafo dirigido acíclico (DAG).
Grafo Ejemplos
Observe lo siguiente:
Un nodo es ancestro de un nodo si y sólo si es descendiente de .
ESTRUCTURAS DE DATOS 8
Todo nodo es ancestro y descendiente de sí mismo, debido a la existencia de caminos de
longitud cero.
Para todo camino se cumple que su origen es ancestro de su destino y que su destino es
descendiente de su origen.
Para todo nodo de un grafo no dirigido se cumple que sus ancestros y que sus
descendientes son los mismos.
Todo grafo fuertemente conexo también es conexo, pero no todo grafo conexo es
fuertemente conexo.
Tabla 22: Ejemplos sobre los conceptos de grafo conexo y grafo fuertemente conexo.
Grafo ¿Es conexo? ¿Es fuertemente conexo?
SI NO
SI SI
ESTRUCTURAS DE DATOS 9
NO NO
NO NO
ESTRUCTURAS DE DATOS 10
2.11. CAMINOS Y CICLOS HAMILTONIANOS
Tabla 25: Definición de camino Hamiltoniano y ciclo Hamiltoniano.
Concepto Definición
Un camino Hamiltoniano es un camino simple que recorre todos los
Camino Hamiltoniano
nodos del grafo.
Un ciclo Hamiltoniano es un ciclo simple que recorre todos los nodos del
Ciclo Hamiltoniano
grafo.
Como los ciclos también son caminos, entonces todo ciclo Hamiltoniano también es un
camino Hamiltoniano.
ESTRUCTURAS DE DATOS 11
El grafo no tiene ningún
ciclo Hamiltoniano.
Como los ciclos también son caminos, entonces todo ciclo Euleriano también es un camino
Euleriano. Además, hay que tener en cuenta que si el grafo es no dirigido, cada arco se debe
visitar en una sola dirección.
Existen criterios para determinar si en un grafo hay un camino o un ciclo Euleriano, que
trataremos más adelante.
ESTRUCTURAS DE DATOS 12
El grafo no tiene ningún
ciclo Euleriano.
Hay criterios fáciles de aplicar para determinar si en un grafo existe o no un camino Euleriano
o un ciclo Euleriano. Para definir con comodidad estos criterios sobre grafos dirigidos,
necesitaremos dos funciones:
ESTRUCTURAS DE DATOS 13
Tabla 29: Criterios para determinar la existencia de caminos y ciclos Eulerianos.
Tipo de grafo Tipo de ruta Criterio
En un grafo dirigido existe un camino Euleriano si y sólo si:
1. El grafo es conexo.
Camino
2. Para todo nodo se cumple que , con
Euleriano
la posible excepción de dos nodos y tales que
Grafo dirigido
y .
En un grafo dirigido existe un ciclo Euleriano si y sólo si:
Ciclo Euleriano 1. El grafo es conexo.
2. Para todo nodo se cumple que .
En un grafo no dirigido existe un camino Euleriano si y sólo si:
Camino 1. El grafo es conexo.
Euleriano 2. Todos los nodos del grafo tienen grado par, con la posible
Grafo no
excepción de dos nodos que pueden tener grado impar.
dirigido
En un grafo no dirigido existe un ciclo Euleriano si y sólo si:
Ciclo Euleriano 1. El grafo es conexo.
2. Todos los nodos del grafo tienen grado par.
En las ilustraciones mostradas a continuación, los nodos de los grafos dirigidos se etiquetan
con textos de la forma donde es su grado de salida y es su grado de entrada, y los
nodos de los grafos no dirigidos se etiquetan con su grado.
Gráfica 30: Aplicación de los criterios para determinar la existencia de caminos y ciclos Eulerianos.
Tiene camino Euleriano. Tiene camino Euleriano. Tiene camino Euleriano. No tiene camino Euleriano.
Tiene ciclo Euleriano. No tiene ciclo Euleriano. No tiene ciclo Euleriano. No tiene ciclo Euleriano.
Tiene camino Euleriano. Tiene camino Euleriano. Tiene camino Euleriano. No tiene camino Euleriano.
Tiene ciclo Euleriano. No tiene ciclo Euleriano. No tiene ciclo Euleriano. No tiene ciclo Euleriano.
Un problema típico sobre grafos consiste en trazar el contorno de la casita de Papá Noel sin
levantar el lápiz. La solución a este problema involucra hallar un camino Euleriano!
ESTRUCTURAS DE DATOS 14
Gráfica 31: La casita de Papá Noel.
3. REPRESENTACIÓN
Para implementar un grafo es necesario representar sus nodos y sus arcos con estructuras de
datos apropiadas según las necesidades particulares que se tengan. La teoría expuesta en
esta sección está inspirada en el libro Diseño y Manejo de Estructuras de Datos en C de Jorge
Villalobos †.
Por simplicidad, suponga que los nodos del grafo están identificados con números naturales
desde hasta , donde es la cantidad total de nodos.
Tabla 33: Algunas estructuras de datos típicas en las que se pueden almacenar los arcos de un grafo.
Nombre de la Descripción
estructura
Lista de arcos Consiste en almacenar los arcos en una lista.
Consiste en almacenar la información de los arcos en una matriz de
números flotantes de tamaño donde para todos y entre y
Matriz de adyacencia se tiene que es el costo del arco que va del nodo con
identificador al nodo con identificador . En caso de que no haya arco
desde hacia , entonces se llena con el valor infinito ( ).
†
VILLALOBOS, Jorge A. Diseño y Manejo de Estructuras de Datos en C. Bogotá: McGraw-Hill, 1996.
ESTRUCTURAS DE DATOS 15
Consiste en tener para cada nodo:
Listas de adyacencia Una lista con sus arcos de salida, que van hacia sus sucesores.
Otra lista con sus arcos de entrada, que vienen desde sus predecesores.
Consiste en tener para cada nodo una lista con sus arcos de salida, que
Listas de sucesores
van hacia sus sucesores.
ESTRUCTURAS DE DATOS 16
Tabla 35: Ventajas y desventajas de las estructuras de datos típicas para alojar los arcos de un grafo.
Nombre de la Ventajas Desventajas
estructura
Fácil de entender. Es ineficiente el proceso de
No desperdicia espacio en determinar si hay arco de un nodo
memoria principal. a otro.
Es ineficiente el proceso de hallar
Lista de arcos el costo de ir de un nodo a otro.
Es ineficiente el proceso de hallar
los sucesores de un nodo.
Es ineficiente el proceso de hallar
los antecesores de un nodo.
Es eficiente el proceso de Es ineficiente el proceso de hallar
determinar si hay arco de un nodo los sucesores de un nodo.
a otro. Es ineficiente el proceso de hallar
Es eficiente el proceso de hallar el los antecesores de un nodo.
Matriz de adyacencia
costo de ir de un nodo a otro. Desperdicia mucho espacio en
Desperdicia poco espacio en memoria principal si el grafo es
memoria principal si el grafo es disperso.
denso.
Es eficiente el proceso de hallar Es ineficiente el proceso de
los sucesores de un nodo. determinar si hay arco de un nodo
Es eficiente el proceso de hallar a otro.
los antecesores de un nodo. Es ineficiente el proceso de hallar
Listas de adyacencia
Desperdicia poco espacio en el costo de ir de un nodo a otro.
memoria principal si el grafo es Desperdicia mucho espacio en
disperso. memoria principal si el grafo es
denso.
Es eficiente el proceso de hallar Es ineficiente el proceso de
los sucesores de un nodo. determinar si hay arco de un nodo
Desperdicia poco espacio en a otro.
Listas de sucesores memoria principal. Es ineficiente el proceso de hallar
el costo de ir de un nodo a otro.
Es ineficiente el proceso de hallar
los antecesores de un nodo.
Tabla 36: Algunos criterios importantes para decidir cuál estructura de datos
escoger para representar los arcos de un grafo.
Si Prefiera usar …
el grafo es disperso Listas de sucesores
el grafo es denso Matriz de adyacencia
requiere intensivamente hallar los predecesores de los nodos del grafo Listas de adyacencia
requiere intensivamente hallar los sucesores y predecesores de los nodos
Listas de adyacencia
del grafo
requiere intensivamente hallar los sucesores de los nodos del grafo Listas de sucesores
ESTRUCTURAS DE DATOS 17
desea complicar innecesariamente la algorítmica sobre el grafo Lista de arcos
EN RESUMEN
Un grafo es una estructura de datos conformada por un conjunto de nodos y por un conjunto de
arcos que conectan nodos entre sí.
Un grafo es no dirigido si y sólo si para todo par de nodos y tales que haya un arco desde hacia
, se cumple que también hay un arco desde hacia . Un grafo es dirigido si y sólo si existe un par
de nodos y tales que hay un arco desde hacia pero no existe un arco desde hacia .
Un nodo es sucesor de un nodo si y sólo si existe un arco que va desde hacia . Un nodo es
antecesor de un nodo si y sólo si existe un arco que va desde hacia . Un nodo es adyacente a
un nodo si y sólo si es sucesor y/o antecesor de .
Un grafo es completo si y sólo si para todo par de nodos distintos y , se cumple que hay un arco
desde hacia .
Un camino es una lista no vacía de nodos donde cada nodo es sucesor de su anterior. Un camino
simple es un camino tal que todos los nodos que recorre son distintos, con la excepción de que su
origen puede ser igual a su destino. Una cadena es una lista no vacía de nodos donde cada nodo es
adyacente a su anterior.
Un ciclo es un camino cuyo origen es igual a su destino. Un ciclo simple es un camino simple cuyo
origen es igual a su destino.
Un grafo es cíclico si y sólo si tiene por lo menos un ciclo. Un grafo es acíclico si y sólo si no tiene
ningún ciclo.
Un nodo es descendiente de un nodo si y sólo si existe un camino que va de hacia . Un nodo
es ancestro de un nodo si y sólo si existe un camino que va de hacia .
Un grafo es conexo si y sólo si para todo par de nodos y , se cumple que hay una cadena desde
hacia . Un grafo es fuertemente conexo si y sólo si para todo par de nodos y , se cumple que hay
un camino desde hacia .
Un camino Hamiltoniano es un camino simple que recorre todos los nodos del grafo.
Un ciclo Hamiltoniano es un ciclo simple que recorre todos los nodos del grafo.
Un camino Euleriano es un camino que recorre todos los arcos del grafo, sin repetir.
Un ciclo Euleriano es un ciclo que recorre todos los arcos del grafo, sin repetir.
Algunas estructuras de datos típicas en las que se pueden almacenar los arcos de un grafo son: 1.
Lista de arcos, 2. Matriz de adyacencia, 3. Listas de adyacencia, y 4. Listas de sucesores.
ESTRUCTURAS DE DATOS 18
ESTRUCTURAS DE DATOS
UNIDAD CUATRO - SEMANA OCHO
IMPLEMENTACIÓN DE GRAFOS *
TABLA DE CONTENIDO
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
1. IMPLEMENTACIÓN DE GRAFOS DIRIGIDOS CON LISTAS DE
SUCESORES
Recurso como proyecto en Eclipse: Grafos.zip.
Por otro lado, la clase Arco representa un enlace de un grafo, y tiene los siguientes atributos:
origen: es una variable de tipo Nodo que apunta al vértice de origen del arco.
destino: es una variable de tipo Nodo que apunta al vértice de destino del arco.
costo: es una variable de tipo double que almacena el costo del arco.
ESTRUCTURAS DE DATOS 2
// *************************
// * Atributos de la clase *
// *************************
protected int n;
protected Nodo[] nodos;
protected List<Arco>[] arcos;
// ***********************
// * Métodos de la clase *
// ***********************
// ...
}
0
0 1 2 3 1
2
3
Nuestros grafos dirigidos se inicializan con una cantidad constante de nodos que no puede
ser alterada. Sin embargo, después de creado un grafo, se pueden adicionar o eliminar arcos
en cualquier momento.
ESTRUCTURAS DE DATOS 3
1.2. MÉTODOS PARA CONSULTAR LOS SUCESORES Y PREDECESORES DE UN
NODO
Código 6: Método que halla los arcos de salida de un nodo, con complejidad temporal .
public List<Arco> getArcosSalida(int pIdNodo) {
// Retornar la lista de arcos de salida correspondiente al nodo dado:
return arcos[pIdNodo];
}
Código 7: Método que halla los arcos de entrada de un nodo, con complejidad temporal .
public List<Arco> getArcosEntrada(int pIdNodo) {
List<Arco> respuesta=new ArrayList<Arco>(); // Crear la lista de respuesta.
for (int i=0; i<n; i++) { // Por cada nodo desde el nodo i=0 hasta el nodo i=n-1:
for (Arco a:getArcosSalida(i)) { // Por cada arco que sale del nodo i:
if (a.destino.id==pIdNodo) { // Si el destino del arco es el nodo dado:
respuesta.add(a); // Adicionar el arco a la lista de respuesta.
}
}
}
return respuesta; // Retornar la lista de respuesta.
}
Código 8: Método que halla los sucesores de un nodo, con complejidad temporal ,
donde es el grado de salida del nodo.
public List<Nodo> getSucesores(int pIdNodo) {
List<Nodo> respuesta=new ArrayList<Nodo>(); // Crear la lista de respuesta.
for (Arco a:getArcosSalida(pIdNodo)) { // Por cada arco de salida del nodo dado:
respuesta.add(a.destino); // Adicionar a la lista el destino del arco.
}
return respuesta; // Retornar la lista de respuesta.
}
Código 9: Método que halla los antecesores de un nodo, con complejidad temporal .
public List<Nodo> getAntecesores(int pIdNodo) {
List<Nodo> respuesta=new ArrayList<Nodo>(); // Crear la lista de respuesta.
for (Arco a:getArcosEntrada(pIdNodo)) { // Por cada arco de entrada del nodo dado:
respuesta.add(a.origen); // Adicionar a la lista el origen del arco.
}
return respuesta; // Retornar la lista de respuesta.
}
Como se dijo anteriormente, una vez se construya un nuevo grafo de tipo VEDGrafoDirigido,
no podemos agregar ni eliminar nodos. Sin embargo, los valores de los nodos se pueden
modificar.
Código 10: Método que modifica el valor de un nodo, con complejidad temporal .
public void setValor(int pIdNodo, Object pValor) {
Nodo nodo=nodos[pIdNodo]; // Obtener el nodo con identificador pIdNodo.
nodo.valor=pValor; // Modificar el valor del nodo con identificador pIdNodo.
}
ESTRUCTURAS DE DATOS 4
1.4. MÉTODOS PARA CONSULTAR ARCOS
Código 11: Método que retorna el arco que enlaza dos nodos, con complejidad
donde es el grado de salida del nodo origen.
public Arco getArco(int pIdOrigen, int pIdDestino) {
for (Arco a:getArcosSalida(pIdOrigen)) { // Por cada arco que sale del nodo de origen:
if (a.destino.id==pIdDestino) { // Si el destino del arco es el nodo de destino:
return a; // Retornar el arco que va del nodo de origen al nodo de destino.
}
}
return null; // Retornar null porque no se encontró ningún arco del origen al destino.
}
Código 12: Método que informa si hay un arco entre dos nodos, con complejidad
donde es el grado de salida del nodo origen.
public boolean existeArco(int pIdOrigen, int pIdDestino) {
Arco a=getArco(pIdOrigen,pIdDestino); // Obtener el arco que va del origen al destino.
if (a!=null) { // Si hay un arco del origen al destino:
return true; // Retornar verdadero.
}
else { // Si no hay un arco del origen al destino:
return false; // Retornar falso.
}
}
Código 14: Método que elimina un arco del grafo, con complejidad temporal
donde es el grado de salida del nodo origen.
public void eliminarArco(int pIdOrigen, int pIdDestino) {
Arco a=getArco(pIdOrigen,pIdDestino); // Obtener el arco que va del origen al destino.
if (a!=null) { // Si hay un arco del origen al destino:
// Eliminar el arco de la lista de arcos de salida del nodo de origen:
arcos[pIdOrigen].remove(a);
}
}
ESTRUCTURAS DE DATOS 5
del nodo con identificador al nodo con identificador . En caso de que no haya arco desde
hacia , entonces se llena con el valor infinito ( ).
Código 15: Método que retorna la matriz de adyacencia del grafo, con complejidad temporal .
public double[][] getMatrizDeAdyacencia() {
double[][] matriz=new double[n][n]; // Crear una matriz de tamaño n por n.
for (int i=0; i<n; i++) { // Por cada fila de la matriz:
for (int j=0; j<n; j++) { // Por cada columna de la matriz:
matriz[i][j]=Double.POSITIVE_INFINITY; // Llenar la celda con el valor infinito.
}
}
for (int i=0; i<n; i++) { // Por cada nodo desde el nodo i=0 hasta el nodo i=n-1:
for (Arco a:getArcosSalida(i)) { // Por cada arco que salga del nodo i:
int idOrigen=a.origen.id; // Obtener el identificador del nodo origen.
int idDestino=a.destino.id; // Obtener el identificador del nodo destino.
double costo=a.costo; // Obtener el costo del arco.
matriz[idOrigen][idDestino]=costo; // Actualizar la matriz de adyacencia.
}
}
return matriz; // Retornar la matriz de adyacencia.
}
Un recorrido de un grafo es un proceso que visita exactamente una vez cada uno de sus
nodos, en cierto orden determinado.
Tabla 17: Tres de las formas más conocidas para recorrer los nodos de un grafo.
Concepto Definición
Los nodos del grafo se visitan en orden según su identificador, iterando la
Recorrido
estructura de datos en la que están almacenados e ignorando por completo los
secuencial
arcos.
Recorrido Mientras haya nodos sin visitar en el grafo, aplique la búsqueda por profundidad
por profundidad comenzando desde cualquiera de los nodos no visitados.
ESTRUCTURAS DE DATOS 6
Recorrido Mientras haya nodos sin visitar en el grafo, aplique la búsqueda por anchura
por anchura comenzando desde cualquiera de los nodos no visitados.
Iniciando desde un nodo de partida , la búsqueda por profundidad (conocida como Depth
First Search (DFS) en inglés) es un proceso que explora nodos de la siguiente manera:
1. Si el nodo ya ha sido visitado, no se realiza ninguna operación.
2. De lo contrario, si el nodo aún no ha sido marcado como visitado:
2.1. Se marca como visitado el nodo .
2.2. Para cada sucesor del nodo , se llama recursivamente el proceso sobre el nodo de
partida .
Código 18: Método recursivo que aplica el algoritmo DFS sobre un nodo, con complejidad temporal .
private void dfs(int pIdNodo, boolean[] pMarcados, List<Nodo> pRecorrido) {
if (pMarcados[pIdNodo]) { // Si el nodo actual está marcado como visitado:
// No efectuar ninguna operación.
}
else { // Si el nodo actual no ha sido marcado como visitado:
pMarcados[pIdNodo]=true; // Marcar como visitado el nodo actual.
pRecorrido.add(nodos[pIdNodo]); // Insertar el nodo actual al final de la respuesta.
for (Arco a:getArcosSalida(pIdNodo)) { // Por cada arco que salga del nodo actual:
Nodo q=a.destino; // Guardar en la variable q el destino del arco.
dfs(q.id,pMarcados,pRecorrido); // Llamar recursivamente el método sobre el nodo q.
}
}
}
Gráfica 19: Recorrido por profundidad de un grafo. Las etiquetas azules muestran el orden en el que se visitan los nodos.
ESTRUCTURAS DE DATOS 7
2.2. RECORRIDO POR ANCHURA
Iniciando desde un nodo de partida , la búsqueda por anchura (conocida como Breadth First
Search (BFS) en inglés) es un proceso que explora nodos de la siguiente manera:
1. Se visita el nodo .
2. Se visitan todos los sucesores del nodo que no hayan sido visitados.
3. Se visitan todos los sucesores de los sucesores del nodo que no hayan sido visitados.
ESTRUCTURAS DE DATOS 8
4. En general, mientras quede algún descendiente de sin visitar, el proceso se repite
visitando los sucesores de los nodos del paso anterior que aún no hayan sido visitados.
El siguiente algoritmo iterativo aplica una búsqueda por anchura desde un nodo de partida :
1. Se crea una cola vacía de nodos.
2. Se inserta en la cola el nodo .
3. Se marca como visitado el nodo .
4. Mientras la cola no sea vacía:
4.1. Sea el nodo que se encuentra en la cabeza de la cola.
4.2. Se elimina la cabeza de la cola.
4.3. Para cada sucesor del nodo , que no haya sido visitado:
4.3.1. Se inserta en la cola el nodo .
4.3.2. Se marca como visitado el nodo .
Código 20: Método iterativo que aplica el algoritmo BFS sobre un nodo, con complejidad temporal .
private void bfs(int pIdNodo, boolean[] pMarcados, List<Nodo> pRecorrido) {
Queue<Nodo> cola=new LinkedList<Nodo>(); // Crear una cola vacía de nodos.
cola.offer(nodos[pIdNodo]); // Insertar en la cola el nodo inicial.
pMarcados[pIdNodo]=true; // Marcar como visitado el nodo inicial.
while (!cola.isEmpty()) { // Mientras la cola no sea vacía:
Nodo p=cola.poll(); // Extraer la cabeza de la cola y guardarla en la variable p.
pRecorrido.add(p); // Insertar el nodo p al final de la lista de respuesta.
for (Arco a:getArcosSalida(p.id)) { // Por cada arco que salga del nodo p:
Nodo q=a.destino; // Guardar en la variable q el destino del arco.
if (!pMarcados[q.id]) { // Si el nodo q no ha sido marcado como visitado:
cola.offer(q); // Insertar en la cola el nodo q.
pMarcados[q.id]=true; // Marcar como visitado el nodo q.
}
}
}
}
El recorrido por anchura se logra aplicando la búsqueda por anchura mientras no se hayan
visitado todos los nodos del grafo. Depende del orden en que se escojan los nodos y del
orden en que se guarden los sucesores.
Gráfica 21: Recorrido por anchura de un grafo. Las etiquetas azules muestran el orden en el que se visitan los nodos.
ESTRUCTURAS DE DATOS 9
EN RESUMEN
Iniciando desde un nodo de partida , la búsqueda por profundidad (conocida como Depth First
Search (DFS) en inglés) es un proceso que explora nodos de la siguiente manera:
1. Si el nodo aún no ha sido marcado como visitado:
1.1.Se marca como visitado el nodo .
1.2.Para cada sucesor del nodo , se llama recursivamente el proceso sobre el nodo de partida .
Iniciando desde un nodo de partida , la búsqueda por anchura (conocida como Breadth First Search
(BFS) en inglés) es un proceso que explora nodos de la siguiente manera:
1. Se visita el nodo .
2. Se visitan todos los sucesores del nodo que no hayan sido visitados.
3. Se visitan todos los sucesores de los sucesores del nodo que no hayan sido visitados.
4. En general, mientras quede algún descendiente de sin visitar, el proceso se repite visitando los
sucesores de los nodos del paso anterior que aún no hayan sido visitados.
ESTRUCTURAS DE DATOS 10
ESTRUCTURAS DE DATOS
UNIDAD CUATRO - SEMANA OCHO
PROBLEMAS CLÁSICOS SOBRE GRAFOS *
TABLA DE CONTENIDO
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
1. EL PROBLEMA DE LA RUTA MÁS CORTA
Recurso como proyecto en Eclipse: Grafos.zip.
El problema de la ruta más corta consiste en encontrar el camino de menor costo que va
desde un nodo hacia otro, donde el costo de un camino se calcula como la suma de los costos
de los arcos por donde pasa.
ESTRUCTURAS DE DATOS 2
marcados[i] es verdadero si y sólo si el nodo ha sido visitado.
3. Se inicializan todas las posiciones del arreglo costos con el valor infinito.
4. Se inicializan todas las posiciones del arreglo marcados con el valor falso, para
marcar todos los nodos como no visitados.
5. Se ejecuta la asignación costos[v]=0, para registrar que el mínimo costo de ir
del nodo hacia sí mismo es cero.
6. Realizar el siguiente proceso mientras existan nodos sin visitar:
6.1.Sea el nodo no visitado que tenga registrado el menor costo (en caso de que
hayan varios nodos no visitados que compartan el menor costo, se escoge
cualquiera).
6.2.Si costos[p] es infinito, termine la ejecución de este ciclo porque tanto el
nodo como el resto de nodos no visitados no son descendientes del nodo .
6.3.Se marca como visitado el nodo .
6.4.Para cada sucesor del nodo , se relaja el arco :
6.4.1. Sea el costo del arco que va desde hacia .
6.4.2. Si el nodo no ha sido marcado como visitado, y además, costos[p]+c es
menor que costos[q], entonces se actualiza el costo de llegar al nodo , mediante
la asignación costos[q]=costos[p]+c.
Gráfica 3: Algoritmo de Dijkstra para hallar los costos de las rutas más cortas desde el nodo hacia todos los demás.
ESTRUCTURAS DE DATOS 3
Código 4: Implementación básica del algoritmo de Dijkstra, con complejidad temporal .
public double[] rutaMasCortaDijkstraBasico(int pIdNodo) {
double infinito=Double.POSITIVE_INFINITY; // Variable que representa el infinito.
double[] costos=new double[n]; // Crear un arreglo para anotar los mínimos costos.
boolean[] marcados=new boolean[n]; // Crear un arreglo para marcar los nodos visitados.
Arrays.fill(costos,infinito); // Inicializar los costos en el valor infinito.
Arrays.fill(marcados,false); // Marcar todos los nodos como no visitados.
costos[pIdNodo]=0; // El mínimo costo para llegar al nodo inicial es cero.
while (true) { // Mientras existan nodos sin visitar:
// Hallar el nodo no visitado con el menor costo:
int p=-1; // Variable para almacenar el id del nodo no visitado con menor costo.
for (int i=0; i<n; i++) { // Para cada nodo desde el nodo i=0 hasta el nodo i=n-1:
if (!marcados[i]) { // Si el nodo i no ha sido marcado como visitado:
if (p==-1||costos[i]<costos[p]) { // Si el nodo i tiene menos costo que el p:
p=i; // Ahora, el nodo no visitado con menor costo encontrado es el nodo i.
}
}
}
if (p==-1) { // Si todos los nodos ya han sido marcados como visitados:
break; // Terminar la ejecución del ciclo.
}
else if (costos[p]==infinito) { // Si el nodo p no es descendiente del nodo inicial:
break; // Terminar la ejecución del ciclo.
}
else { // Si existe un nodo no visitado que sea descendiente del nodo inicial:
ESTRUCTURAS DE DATOS 4
// Hecho: p sería el identificador del nodo no visitado con menor costo.
marcados[p]=true; // Marcar como visitado el nodo p.
for (Arco a:getArcosSalida(p)) { // Relajar cada arco que salga del nodo p:
int q=a.destino.id; // Guardar en la variable q el id del destino del arco.
if (!marcados[q]) { // Si el nodo q no ha sido marcado como visitado:
if (costos[p]+a.costo<costos[q]) { // Si sale más barato llegar a q desde p:
costos[q]=costos[p]+a.costo; // Actualizar el costo de llegar al nodo q.
}
}
}
}
}
return costos; // Retornar el arreglo con los costos mínimos.
}
En el peor de los casos, el ciclo del algoritmo de Dijkstra se ejecuta exactamente veces, y
cada uno de los arcos se visita una sola vez. Observe que nuestra implementación básica
incluye una búsqueda lineal con complejidad temporal para encontrar el nodo no
visitado con el menor costo registrado.
El problema del árbol de expansión minimal (Minimal Spanning Tree (MST) en inglés) consiste
en encontrar un árbol de costo mínimo que sea un subgrafo que pase por todos los nodos del
grafo.
ESTRUCTURAS DE DATOS 5
Imagine un grafo en el que los nodos representan ciudades y los arcos representan vías no
pavimentadas cuyos costos son distancias en kilómetros. Formalmente, un arco de la forma
con costo indica que hay una carretera sin pavimentar de longitud kilómetros
entre las ciudades y . Observe que la mínima cantidad de kilómetros que se deben
pavimentar para garantizar que entre todo par de ciudades haya un camino pavimentado es
precisamente el costo del árbol de expansión minimal, y que un árbol de expansión minimal
exhibiría las carreteras que se deben pavimentar para conectar todas las ciudades entre sí de
la forma más barata posible.
ESTRUCTURAS DE DATOS 6
Restricciones Funciona siempre y cuando el grafo sea conexo y no dirigido.
Entradas Ninguna.
Salidas Los arcos pertenecientes a uno de los árboles de expansión minimal del grafo.
Pseudocódigo 1. Se crea un conjunto que contenga todos los arcos del grafo y se crea un bosque
con exactamente árboles donde cada nodo del grafo forma por sí solo un árbol
por separado.
2. Mientras el conjunto de arcos no sea vacío y no se hayan agregado arcos
al bosque:
2.1.Sea el arco del conjunto que tenga el menor costo (en caso de que hayan
varios arcos en el conjunto que compartan el menor costo, se escoge cualquiera).
2.2.Se elimina el arco del conjunto.
2.3.Si el origen y el destino del arco se encuentran en dos árboles distintos:
2.3.1. Se añade el arco al bosque, uniendo los dos árboles para formar uno solo.
2.4.De lo contrario, si el origen y el destino del arco se encuentran en un mismo
árbol:
2.4.1. El arco se desecha.
3. Se retorna una lista con los arcos añadidos al bosque.
ESTRUCTURAS DE DATOS 7
arco arco arco arco
ESTRUCTURAS DE DATOS 8
2. Se declara una estructura de datos para marcar los nodos visitados.
3. Se marcan todos los nodos como no visitados.
4. Se marca como visitado un nodo escogido al azar.
5. Se realiza el siguiente proceso exactamente veces:
5.1.Sea el arco de menor costo cuyo origen sea un nodo visitado y cuyo destino
sea un nodo no visitado (en caso de que hayan varios arcos que cumplan el criterio,
se escoge cualquiera).
5.2.Se añade el arco a la respuesta.
5.3.Se marca como visitado el destino del arco .
6. Se retornan los arcos añadidos a la respuesta.
Gráfica 11: Algoritmo de Prim aplicado sobre un grafo de ejemplo, comenzando las operaciones desde el nodo .
ESTRUCTURAS DE DATOS 9
arco árbol de expansión minimal
El problema del agente viajero (Travelling Salesman Problem (TSP) en inglés) consiste en
encontrar el ciclo Hamiltoniano de menor costo, es decir, el ciclo simple más barato que pasa
por todos los nodos exactamente una vez.
†
Gráfica 12: Problema del agente viajero sobre un grafo que conecta algunas ciudades colombianas .
costo: 354
Una opción para resolver el problema consiste en revisar sistemáticamente todos los ciclos
que pasen por todos los nodos para encontrar el más barato. La complejidad temporal de
este algoritmo es porque en el peor de los casos el grafo es completo y la fuerza bruta
†
La imagen del mapa de Colombia sin decoración fue tomada de http://es.wikipedia.org/wiki/Colombia.
WIKIPEDIA La Enciclopedia Libre, © 2010 (recuperado en noviembre de 2010).
ESTRUCTURAS DE DATOS 10
terminaría analizando todos los posibles ciclos Hamiltonianos del grafo. Observe que en un
grafo completo, cada una de las permutaciones de los nodos daría lugar a un camino
Hamiltoniano.
EN RESUMEN
El problema de la ruta más corta consiste en encontrar el camino de menor costo que va desde un
nodo hacia otro. Uno de los algoritmos para solucionar este problema es el algoritmo de Dijkstra, que
dado un nodo de partida , encuentra las rutas más cortas que van desde el nodo hacia todos los
demás, en un grafo donde todos los arcos tienen costo no negativo.
El problema del árbol de expansión minimal (Minimal Spanning Tree (MST)) consiste en encontrar un
árbol de costo mínimo que sea un subgrafo que pase por todos los nodos del grafo. Dos de los
algoritmos para solucionar este problema son el algoritmo de Kruskal y el algoritmo de Prim.
El problema del agente viajero (Travelling Salesman Problem (TSP)) consiste en encontrar el ciclo
Hamiltoniano de menor costo, es decir, el ciclo simple más barato que pasa por todos los nodos
exactamente una vez.
ESTRUCTURAS DE DATOS 11
ESTRUCTURAS DE DATOS
UNIDAD CUATRO - SEMANA OCHO
EJERCICIOS PROPUESTOS *
Explique paso a paso cómo ejecuta el algoritmo de Dijkstra para encontrar en el anterior
grafo no dirigido los costos de las rutas más baratas desde
el nodo hacia todos los demás.
el nodo hacia todos los demás.
el nodo hacia todos los demás.
el nodo hacia todos los demás.
Además de los costos, enuncie las rutas de costo mínimo desde los nodos enumerados hacia
todos los demás.
grafo 1 grafo 2
*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.
ESTRUCTURAS DE DATOS 1
2.1. Explique paso a paso cómo ejecuta el algoritmo de Kruskal para encontrar en los dos
grafos un árbol de expansión minimal.
2.2. Explique paso a paso cómo ejecuta el algoritmo de Prim para encontrar en los dos grafos
un árbol de expansión minimal.
4.1. Método que retorna una lista con los vértices adyacentes a cierto nodo.
public List<Nodo> getAdyacentes(int pIdNodo)
5.1. Método que retorna un ciclo Hamiltoniano. Si el grafo no tiene ciclos Hamiltonianos,
retorne null.
public List<Nodo> getCicloHamiltoniano()
ESTRUCTURAS DE DATOS 2
5.2. Método que determina si en el grafo existe un ciclo Euleriano.
public boolean tieneCicloEuleriano()
5.3. Método que retorna un ciclo Euleriano. Si el grafo no tiene ciclos Eulerianos, retorne
null.
public List<Nodo> getCicloEuleriano()
Ayuda: investigue sobre el algoritmo de Fleury y sobre el algoritmo de Hierholzer.
ESTRUCTURAS DE DATOS 3