Вы находитесь на странице: 1из 266

 

PROYECTO

Aplicación  de  las  Estructuras  de  Datos    


PROYECTO  
 
TIPO  DE  INVESTIGACIÓN   Formativa  

Técnico,  tecnólogo  y  profesional  


NIVEL  ACADÉMICO  
 
Utilizar  sistemas  de  Tecnología,  Información  y  Telecomunicaciones  que  
permitan  la  recreación  de  un  modelo,  que  permita  resolver  problemas  en  el  
mundo  real.    
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.    
Servirse  de  la  autodisciplina  para  la  ejecución  de  tareas  de  manera  efectiva.    
COMPETENCIAS  A   Desarrollar  sistemas  de  información,  a  través  de  simulaciones,  bien  sea  
DESARROLLAR   programadas  o  desarrolladas  desde  su  base.  
Explorar  diferentes  fuentes  de  información  y  conocimiento  para  el  
aprendizaje  de  diferentes  Tecnologías  de  Información  y  Telecomunicaciones.    
Actuar  de  manera  autónoma,  ética  y  responsable.    
Generar  estrategias  de  trabajo  efectivo  en  equipo.    
Aplicar  herramientas  de  análisis  y  diseño  en  la  construcción  y  creación  de  
sistemas  de  información  y  soluciones  en  telecomunicaciones.  

 
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
 

INSTRUCCIONES  PARA  ELABORAR  EL  PROYECTO  

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.  

INSTRUCCIONES  DE  ENTREGA  1  –  Semana  3  

 
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.  
 

INSTRUCCIONES  DE  ENTREGA  2  –  Semana  5  

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.  

INSTRUCCIONES  DE  ENTREGA  3  –  Semana  7  

 
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?  
 

 
 

Escala  de  Valores  o  criterios    

Use  esta  escala  de  valores  para  los  criterios  de  evaluación    

Asignaciones   Resultado  de  aprendizaje   Competente   Necesita  Mejorar  

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  
   
 

  El  grupo  presenta  un    


proyecto  original  definido  
El  grupo  propone  un   por  sí  mismo   El  grupo  propone  un  
proyecto  original  y  creativo   proyecto  que  es  copia  del  
Creatividad  y  
que  implica  un  reto  para  sus     de  otro  grupo  o  es  un  
originalidad.  
conocimientos  y   problema  que  no  implica  
capacidades.     un  reto  real.  

 
5 [  POLITÉCNICO  GRANCOLOMBIANO  ]  
 

El  grupo  presenta   El  grupo  presenta   El  grupo  presenta  


información  clara  y  completa   información  básica  sobre:     información  insuficiente,  
relacionada  con:     vaga  y/o  ambigua  sobre:    
Nombre  del  proyecto.    
Nombre  del  proyecto.     Nombre  del  proyecto.    
Objetivos  del  proyecto.    
Objetivos  del  proyecto.     Objetivos  del  proyecto.    
Resultados  esperados.    
Resultados  esperados.     Resultados  esperados.    
Completitud  de  la  
Descripción  del  problema  
información  
Descripción  del  problema   que  se  desea  solucionar.     Descripción  del  problema  
suministrada  sobre  el  
que  se  desea  solucionar.     que  se  desea  solucionar.    
proyecto.  
Aplicabilidad  de  los  temas  
Aplicabilidad  de  los  temas   del  módulo.     Aplicabilidad  de  los  temas  
del  módulo.     del  módulo.    
Requerimientos  
Requerimientos  funcionales   funcionales  que  se  van  a   Requerimientos  
que  se  van  a  soportar.   soportar   funcionales  que  se  van  a  
soportar.  

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
 

Los  pseudocódigos  diseñados   La  mayoría  de  los   La  mayoría  de  los  


Calidad  de  las   son  claros  y  realmente   pseudocódigos  diseñados   pseudocódigos  son  
estrategias  de  solución   ayudan  a  resolver  las   son  claros  y  ayudan  a   incomprensibles  y  el  
funcionalidades.     resolver  las   trabajo  realizado  no  va  a  
  funcionalidades.  hay   representar  ninguna  ayuda  
algunos  pseudocódigos   en  la  etapa  de  
  que  no  son  claros  o  que   implementación.    
no  dan  solución  a  la  
  funcionalidad    

Los  documentos  presentados   Los  documentos   El  contenido  de  los  


son  consecuentes  con  los   presentados  son   documentos  presentados  
Calidad  general  de  la   lineamientos  definidos  al   coherentes  con  el  tiempo   no  revela  la  dedicación  
entrega.   inicio  del  proyecto  y  reflejan   dedicado  al  proyecto.   esperada  de  los  
un  trabajo  de  calidad.     estudiantes.    

Los  estudiantes  cumplen  a   Los  estudiantes  cumplen   Los  estudiantes  cumplen  


Cumplimiento  de  las  
cabalidad  con  el  95%  o  más   por  lo  menos  con  el  80%   con  menos  del  80%  del  
metas  definidas  
de  las  metas  definidas  al   de  las  metas  del  proyecto.   total  del  proyecto.  
inicio  del  proyecto.  
 
 

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  ]  
 

El  grupo  documentó  de   El  grupo  documentó  una   El  grupo  no  entregó  


Documentación  de  la   manera  informal  la   que  otra  experiencia   documentación  alguna  
etapa  de   experiencia  vivida  durante  la   vivida  durante  la   sobre  la  experiencia  vivida  
implementación.   implementación  del  código   implementación  del   durante  la  implementación  
fuente.     código  fuente.   del  código  fuente.    
 
   

Se  desarrolló  y  documentó   Se  realizaron  algunas   Se  realizaron  pruebas  de  


un  proceso  de  pruebas  que   pruebas  documentando   manera  informal,  que  el  
Documentación  de  la   permitió  corregir  la  mayoría   vagamente  el  proceso   grupo  no  es  capaz  de  
etapa  de  pruebas.   de  los  errores  cometidos  en   demostrar  debido  a  la  
la  etapa  de  implementación.   ausencia  de  
  Hay  certeza  de  que  se  puede   documentación.  
asegurar  la  calidad  del  
producto  

Los  estudiantes  demuestran   Los  estudiantes   Los  estudiantes  no  son  


  dominio  sobre  el  tema  y   comprenden  el  trabajo   capaces  de  sustentar  las  
sobre  los  resultados  del   realizado  para  lograr  los   estrategias  definidas  o  el  
Sustentación     proyecto  que  fue   objetivos  del  proyecto.   código  implementado  por  
presentado.     ellos.    
   
 

 
[ 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

El término abstracción se refiere al proceso mediante el que 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. De esta manera podemos definir el término
clase como la abstracción de un conjunto de objetos reunidos bajo el mismo concepto.

Por ejemplo, observe las siguientes imágenes:



Gráfica 1: Cuatro animales .

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.

Dependiendo de la situación, nuestro cerebro considera sólo aquellas características que


interesan de los perros, por ejemplo:
Nombre.
Raza.
Fecha de nacimiento.
Género (masculino/femenino).
Documento de identificación del dueño.
Nombres y apellidos del dueño.

Si fuésemos un médico veterinario probablemente nos interesarían más características de los


perros:
Historial de vacunación.
Historial de desparasitación.
Enfermedades que ha tenido.
Cirugías que ha tenido.
Tipo de alimento que consume.


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:

Código 2: Declaración de la clase Perro en Java.


public class Perro {
...
}

2. ATRIBUTOS Y MÉTODOS
Recurso como proyecto en Eclipse: Clases.zip.

Una clase se define mediante:

Un nombre que la identifica.


Atributos que describen las características de los objetos de la clase.
Métodos que describen el comportamiento de los objetos de la clase.

Código 3: Atributos y métodos de ejemplo para la clase Perro.


// Declaración de la clase Perro
public class Perro {

// -------------------------
// - Atributos de la clase -
// -------------------------

private String nombre; // Nombre del perro


private String raza; // Raza del perro
private String fechaNacimiento; // Fecha de nacimiento del perro en formato dd/mm/aaaa
private boolean genero; // true si es masculino, false si es femenino

// ----------------------------------
// - Método constructor de la clase -
// ----------------------------------

public Perro(String pNombre, String pRaza, String pFechaNacimiento, boolean pGenero) {


nombre=pNombre;
raza=pRaza;
fechaNacimiento=pFechaNacimiento;
genero=pGenero;
}

ESTRUCTURAS DE DATOS 3
// --------------------------------------
// - Atributos analizadores de la clase -
// --------------------------------------

public String darNombre() {


return nombre;
}

public String darRaza() {


return raza;
}

public String darFechaNacimiento() {


return fechaNacimiento;
}

public boolean esMasculino() {


if (genero) {
return true;
}
else {
return false;
}
}

public boolean esFemenino() {


if (genero) {
return false;
}
else {
return true;
}
}

// ---------------------------------------
// - Atributos modificadores de la clase -
// ---------------------------------------

public void cambiarNombre(String pNuevoNombre) {


nombre=pNuevoNombre;
}

public void ladrar() {


// ...
}

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:

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.

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);

Si se desea llamar un método de un objeto, se coloca el nombre del objeto, un punto y el


nombre del método.

Código 6: Ejemplo que ilustra la invocación de métodos en Java.


String s=perro4.darNombre();
System.out.println("El nombre del cuarto perro es: "+s);

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.

Gráfica 7: Arreglo que puede guardar ocho elementos.

0 1 2 3 4 5 6 7

Gráfica 8: Arreglo de caracteres que guarda los elementos M,A,N,Z,A,N,A.

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.

Código 9: Programa que crea un arreglo de caracteres e imprime su contenido en consola.


public class EjemploArreglos {
public static void main(String[] args) {
char[] arreglo=new char[7];
arreglo[0]='M';
arreglo[1]='A';
arreglo[2]='N';
arreglo[3]='Z';
arreglo[4]='A';
arreglo[5]='N';
arreglo[6]='A';
for (int i=0; i<arreglo.length; i++) {
char c=arreglo[i];
System.out.println("El elemento de la posición #"+i+" es: "+c);
}

ESTRUCTURAS DE DATOS 6
}
}

El tamaño de un arreglo se puede averiguar a través del atributo llamado length.

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.

PARA TENER EN CUENTA


 Revise con cuidado el módulo de Programación de Computadores si necesita repasar sus conceptos
sobre el lenguaje de programación Java.
 En el módulo de Paradigmas de Programación se profundizará sobre los conceptos de la
programación orientada a objetos.

ESTRUCTURAS DE DATOS 7
ESTRUCTURAS DE DATOS
UNIDAD UNO - SEMANA UNO
EJERCICIOS PROPUESTOS *

1. PROMEDIO DE UN ARREGLO DE NÚMEROS

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

Implemente en Java una función


public static int[] concatenar(int[] pArreglo1, int pArreglo2) {
// ...
}
que, dados dos arreglos de números, retorne un arreglo de números que sea concatenación
de los dos arreglos recibidos. Por ejemplo, si pArreglo1 tiene los elementos 4,9,13,2, y
pArreglo2 tiene los elementos 1,8,0,4, entonces la función debe entregar como resultado un
arreglo con los elementos 4,9,13,2,1,8,0,4.

3. REVERSO DE UN ARREGLO

Implemente en Java una función


public static int[] reverso(int[] pArreglo) {
// ...
}
que, dado un arreglo de números, retorne su reverso. Por ejemplo, si pArreglo tiene los
elementos 4,9,13,2, entonces la función debe entregar como resultado un nuevo arreglo con
los elementos 2,13,9,4.

*
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 .
...

Observe que el proceso seguiría indefinidamente, involucrando un número infinito de


llamados recursivos. Así pues, 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.

El diseño de algoritmos mediante la recursión como técnica de programación tiene ventajas y


desventajas.

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.

1.1. FUNCIÓN DE FIBONACCI


Recurso como proyecto en Eclipse: Fibonacci.zip.

La función de Fibonacci se define por la fórmula

que puede ser evaluada sobre cualquier número natural , simulando los llamados
recursivos:






Código 1: Implementación básica en Java de la función de Fibonacci.


// VERSIÓN A : ALGORITMO RECURSIVO TÍPICO
public static int fibA(int n) { // n>=0
if (n==0) { // Si n es cero
return 0; // Retornar 0
}
else if (n==1) { // Si n es uno
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)
}
}

Código 2: Programa que prueba la función de Fibonacci para diversos valores de .


public class Fibonacci { // Declaración de la clase
public static void main(String[] args) { // Método main
for (int i=0; i<=2000; i++) {
System.out.println("fib("+i+")="+Fibonacci.fibA(i));
}
}
// VERSIÓN A : ALGORITMO RECURSIVO TÍPICO
public static int fibA(int n) { // n>=0
if (n==0) { // Si n es cero
return 0; // Retornar 0
}
else if (n==1) { // Si n es uno

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)
}
}
}

Tabla 3: Impresión en consola del programa anterior.


fib(0)=0 fib(16)=987 fib(32)=2178309
fib(1)=1 fib(17)=1597 fib(33)=3524578
fib(2)=1 fib(18)=2584 fib(34)=5702887
fib(3)=2 fib(19)=4181 fib(35)=9227465
fib(4)=3 fib(20)=6765 fib(36)=14930352
fib(5)=5 fib(21)=10946 fib(37)=24157817
fib(6)=8 fib(22)=17711 fib(38)=39088169
fib(7)=13 fib(23)=28657 fib(39)=63245986
fib(8)=21 fib(24)=46368 fib(40)=102334155
fib(9)=34 fib(25)=75025 fib(41)=165580141
fib(10)=55 fib(26)=121393 fib(42)=267914296
fib(11)=89 fib(27)=196418 fib(43)=433494437
fib(12)=144 fib(28)=317811 fib(44)=701408733
fib(13)=233 fib(29)=514229 fib(45)=1134903170
fib(14)=377 fib(30)=832040 fib(46)=1836311903
fib(15)=610 fib(31)=1346269 fib(47)=-1323752223

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 .

Abordemos el primer problema: el de desbordamiento del tipo de datos. Una solución


temporal es cambiar int por long, que usa 64 bits para su representación, permitiendo la
manipulación de números hasta (es decir, hasta 9223372036854775807).
Claramente esta modificación sólo pospondría el problema y no lo solucionaría
definitivamente. Para corregir el desbordamiento de una vez por todas, es necesario el uso
de una librería especializada en el manejo de números grandes, como BigInteger en Java.

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

Código 5: Programa que ilustra el uso de la clase BigInteger.


import java.math.*; // Paquete necesario para usar BigInteger
public class EjemploBigInteger { // Declaración de la clase
// Declaración del método main
public static void main(String[] args) {
BigInteger a=new BigInteger("761809243486409043837");
BigInteger b=new BigInteger("2046696616531860150");
BigInteger c=new BigInteger("4525695587262334931605");
BigInteger d=new BigInteger("324298040889334");
BigInteger r=(a.multiply(b)).subtract(c.multiply(d));
System.out.println(r);
}
}

que nos informa por consola


que el resultado es el valor
1557724726873718731381506195680239394480, que claramente no cabe ni en una variable de
tipo int ni en una variable de tipo long. Usar el tipo de datos double no es una opción,
porque nos da como resultado 1.557724726873719E39 (en notación exponencial, sería
), que no es exacto porque no tiene todos los dígitos de la
respuesta.

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.

Código 6: Implementación recursiva en Java de la función de Fibonacci, usando BigInteger.


import java.math.*; // Paquete necesario para usar BigInteger
...
// VERSIÓN B : ALGORITMO RECURSIVO TÍPICO, USANDO BIGINTEGER
public static BigInteger fibB(int n) { // n>=0
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)
}
}

Ahora, ataquemos el problema de eficiencia. En el computador en el que se probó la función,


el cálculo del Fibonacci de se demoró 655906 milisegundos (10.9 minutos) y el
cálculo del Fibonacci de se demoró 1097766 milisegundos (18.3 minutos). Sin duda
alguna, ¡esto es demasiado tiempo!.

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 .

Código 7: Programa que mide cuánto se demora la implementación recursiva en calcular


la función de Fibonacci sobre algunos valores.
import java.math.*; // Importar todas las clases del paquete java.math
import java.io.*; // Importar todas las clases del paquete java.io
public class FibonacciTiempos { // Declaración de la clase
public static void main(String[] args) throws Exception { // Método main
// Abrir el archivo tablaTiempos_fibB.csv para escritura
PrintWriter pw=new PrintWriter("tablaTiempos_fibB.csv");
// Imprimir el encabezado de la tabla csv
pw.println("n;fib(n);demora real calculando fib(n)");
for (int i=0; i<=45; i++) { // Para cada i desde 0 hasta 45
// Tomar el tiempo inicial
long tmIni=System.currentTimeMillis();
// Calcular el fibonacci de i con el método fibB
BigInteger r=FibonacciTiempos.fibB(i);
// Tomar el tiempo final
long tmFin=System.currentTimeMillis();
// Calcular cuánto tiempo se demoró la función fibB en ejecutar
long tmDif=tmFin-tmIni;
// Imprimir tanto en consola como en el archivo la información
System.out.println(i+";"+r+";"+tmDif);
pw.println(i+";"+r+";"+tmDif);
// Obligar al PrintWriter pw a que baje a disco duro la información escrita
pw.flush();
}
pw.close(); // Cerrar el archivo tablaTiempos_fibB.csv
}
// VERSIÓN B : ALGORITMO RECURSIVO TÍPICO, USANDO BIGINTEGER
public static BigInteger fibB(int n) { // n>=0

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)
}
}
}

El archivo exportado (tablaTiempos_fibB.csv) se puede visualizar con Microsoft Excel o con


Open Office Calc. Graficando el tiempo en milisegundos que se demoró la función fibB
calculando el Fibonacci de , versus , obtenemos:

Gráfica 8: Demora real (en milisegundos) calculando el Fibonacci de , versus .

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.

Se dice entonces que la complejidad temporal de la función fibB es ). Informalmente,


la complejidad temporal es una medida de qué tanto tiempo consume un algoritmo en su
ejecución y, en general, sirve para medir la eficiencia de un programa.

¿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.

Código 9: Algoritmo iterativo eficiente para calcular la función de Fibonacci.


// VERSIÓN C : ALGORITMO ITERATIVO TÍPICO
public static BigInteger fibC(int n) { // n>=0
// Inicialice la variable 'a' en 0 y la variable 'b' en 1:
BigInteger a=new BigInteger("0"),b=new BigInteger("1");
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'.

ESTRUCTURAS DE DATOS 7
}
return a; // Retorne el valor de la variable 'a'.
}

¡Convénzase usted mismo de que el algoritmo funciona! Teóricamente nos vamos a


concientizar de que esta versión (fibC) es mucho más eficiente que la anterior (fibB).

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 .

Por lo tanto, la complejidad temporal del método fibC es .

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

En una función recursiva 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. Por ejemplo, la razón de que la implementación fibB sea muy demorada es que
repite de manera innecesaria muchas veces los mismos cálculos.

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(2) fib(1) fib(1) fib(0)

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.

2.1. IMPLEMENTACIÓN DE LA FUNCIÓN DE FIBONACCI USANDO


MEMOIZATION
Recurso como proyecto en Eclipse: Fibonacci.zip.

Con la ayuda de una tabla es posible memorizar los valores retornados por la función de
Fibonacci.

Código 11: Implementación recursiva de la función de Fibonacci, usando la técnica de Memoization.


// VERSIÓN D : ALGORITMO RECURSIVO MEMORIZANDO LOS RESULTADOS
// Tabla para memorizar los resultados de la función.
private static BigInteger tabla[]=new BigInteger[100001];
// Declaración de la función.
public static BigInteger fibD(int n) { // n>=0
// Si tabla[n]!=null es porque el fibonacci de n ya fue calculado y guardado
// en tabla[n].
if (tabla[n]!=null) {
// Retornar el fibonacci de n, que está guardado precisamente en tabla[n].
return tabla[n];

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.
}
}
}

La complejidad temporal de esta última versión es porque, para calcular el Fibonacci de


, en el peor de los casos se va a tener que llenar todas las posiciones de la tabla desde
hasta . Observe que bajo ninguna circunstancia una posición de la tabla es calculada dos o
más veces.

3. DIVIDIR Y CONQUISTAR (DIVIDIR Y VENCER)

Generalmente una función recursiva refleja el uso de la técnica de Dividir y Conquistar,


también conocida como Dividir y Vencer. La técnica de Dividir y Vencer consiste en dividir un
problema en subproblemas similares más pequeños, solucionar tales subproblemas y unir
estas soluciones para resolver el problema original.

3.1. TORRES DE HANOI


Recurso como proyecto en Eclipse: Hanoi.zip.

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.

Gráfica 13: Estado inicial y estado final del juego.

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.

Puede practicar el juego en el sitio http://www.mazeworks.com/hanoi/index.htm. ¿Se


imagina un algoritmo para resolverlo? Piense durante algunos minutos … ¿Será que un
diseño recursivo le facilita pensar?

Vamos a proceder recursivamente:

Caso 1 ( ): si es , no tengo discos! Y como no hay discos, entonces no debo hacer


nada.

Caso 2 ( ): si es mayor que , entonces si tengo discos. Reflexionemos …

ESTRUCTURAS DE DATOS 11
Quiero mover discos de la columna A a la columna C usando la columna B como auxiliar.

Columna A Columna B Columna C


Concentrémonos en el disco más grande de la columna A, que está resaltado de azul. En
algún momento tendremos que mover ese disco azul de la columna A a la columna C, pero
para poderlo mover, no debe haber ningún disco encima suyo (por la regla 3) y no debe
haber ningún disco en la columna C porque de lo contrario quedaría encima de un disco más
pequeño, hecho que no puede suceder (por la regla 2).
La única opción posible es que todos los discos verdes se hayan movido de la columna
A a la columna B. ¿Y cómo los muevo? ¡Pues recursivamente! Supongamos que
recursivamente cumplimos con el objetivo de pasar los discos verdes de la columna A a
la columna B:

Columna A Columna B Columna C


Ahora, pasamos el disco azul de la columna A a la columna C.

Columna A Columna B Columna C


Y finalmente trasladamos recursivamente los discos verdes de la columna B a la
columna C.

Columna A Columna B Columna C

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);
}
}
}

¿Cuál es la complejidad temporal del algoritmo? Responderemos esta pregunta contando


exactamente cuántos movimientos se realizan para solucionar un juego de torres de Hanoi
con discos. Obviamente, entre más movimientos haga el programa, más tiempo se va a
demorar.

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.

Evidentemente, porque cuando no se tienen discos, entonces no hay que realizar


ningún movimiento. Para se tiene que
porque para mover una torre de discos, primero hay que trasladar
recursivamente discos de la columna inicial a la columna intermedia (lo que usaría
movimientos), luego hay que trasladar un disco de la columna inicial a la final (lo
que usaría movimiento), y finalmente hay que trasladar discos de la columna
intermedia a la columna final (lo que usaría movimientos). ¿Por qué trasladar
recursivamente discos de una columna a otra requiere de movimientos? ¡La
respuesta está al final del párrafo anterior!

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

Paso 2: tome el patrón, trate de eliminar los llamados recursivos a la función y


simplifique.
patrón encontrado
Sea . Por tanto . hipótesis para eliminar los llamados recursivos.
usando que y que
puesto que se sabe que
usando aritmética
usando aritmética

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

Código 15: Algoritmo de Búsqueda Lineal.


public static int busquedaLineal(long[] pArreglo, long pValor) {
// Llamar al método de abajo
return busquedaLineal(pArreglo,0,pArreglo.length-1,pValor);
}
public static int busquedaLineal(long[] pArreglo, int pInf, int pSup, long pValor) {
// Por cada posición k desde el límite inferior pInf hasta el límite superior pSup:
for (int k=pInf; k<=pSup; k++) {
// Si la posición k del arreglo tiene el valor buscado, se retorna la posición:
if (pArreglo[k]==pValor) return k;
}
// Si la porción del arreglo que va de pInf a pSup no tiene el valor buscado,
// hay que retornar -1:
return -1;
}

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.

El argumento recursivo es el siguiente: si el arreglo no tiene elementos se informa que el


elemento no aparece; de lo contrario, si el arreglo tiene elementos: 1. si el valor buscado
está en la mitad del arreglo se informa que se encontró en la mitad, 2. si el valor buscado es
menor que el valor que está en la mitad del arreglo se llama recursivamente al algoritmo
sobre la mitad izquierda del arreglo, y 3. si el valor buscado es mayor que el valor que está en
la mitad del arreglo se llama recursivamente al algoritmo sobre la mitad derecha del arreglo.
Estamos aprovechándonos de que el arreglo está ordenado para decidir sobre qué mitad
operar: si el valor es menor que lo que está en medio debería estar en la mitad izquierda y, si
es mayor, debería estar en la mitad derecha.

Código 17: Algoritmo de Búsqueda Binaria.


public static int busquedaBinaria(long[] pArreglo, long pValor) {
// Llamar al método de abajo
return busquedaBinaria(pArreglo,0,pArreglo.length-1,pValor);
}
public static int busquedaBinaria(long[] pArreglo, int pInf, int pSup, long pValor) {
// Hallar el número de elementos que tiene la porción de arreglo a inspeccionar:
int n=pSup-pInf+1;
// Si no hay elementos en la porción de arreglo, retornar -1 como código de error:
if (n<=0) {
return -1;
}
// Si hay elementos en la porción de arreglo:
else {
// Hallar la posición correspondiente a la mitad del arreglo:
int mitad=(pInf+pSup)/2;
// Si el valor buscado es igual a lo que está en la mitad:
if (pValor==pArreglo[mitad]) {
return mitad; // Informar que el valor buscado se encontró en la mitad.
}
// Si el valor buscado es menor que lo que está en la mitad:
else if (pValor<pArreglo[mitad]) {

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);
}
}
}

Sea el tiempo que se demora la función busquedaBinaria en buscar un valor en un


arreglo de tamaño . para todo porque para buscar un valor en un arreglo
de tamaño 1 o menos hay que hacer un número constante de operaciones. Para se
cumple que porque, en el peor de los casos, hay que efectuar un
número constante de operaciones y terminar buscando el valor en la mitad izquierda o en la
mitad derecha (nunca se busca en ambas mitades). Buscar el valor en cualquiera de las
mitades se demora un tiempo de porque cada mitad tiene elementos y, según la
definición, sería el tiempo que se demora la función busquedaBinaria en buscar un
valor en un arreglo de tamaño .

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

Paso 2: tome el patrón, trate de eliminar los llamados recursivos a la función y


simplifique.
patrón encontrado
Sea . Entonces, . hipótesis para eliminar los llamados recursivos.
usando que
puesto que se sabe que
por que

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 .

3.3. ALGORITMO DE ORDENAMIENTO POR MEZCLA (MERGE SORT)

Para ordenar arreglos se cuenta con un proceso muy eficiente llamado algoritmo de
Ordenamiento por Mezcla (Merge Sort en inglés).

Gráfica 18: Diagrama de flujo para el algoritmo de Ordenamiento por Mezcla.

Sea el tiempo que se demora el proceso descrito en ordenar un arreglo de tamaño .


para todo porque para ordenar un arreglo de tamaño 1 o menos hay que
hacer un número constante de operaciones. Para se cumple que
porque ordenar la mitad de la izquierda se demora , ordenar la mitad de
la derecha se demora y mezclar ambas mitades se puede hacer con un algoritmo
eficiente con complejidad . Ordenar cada una de las dos mitades se demora un tiempo
de porque cada mitad tiene elementos y, según la definición, sería el
tiempo que se demora el proceso en ordenar un arreglo de tamaño .

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

Paso 2: tome el patrón, trate de eliminar los llamados recursivos a la función y


simplifique.
patrón encontrado
Sea . Entonces, . hipótesis para eliminar los
llamados recursivos.
usando que
puesto que se sabe que
usando que
porque
usando aritmética
porque es más pequeño que
para grande
usando aritmética

Entonces, la complejidad temporal del algoritmo de Ordenamiento por Mezcla es


.

4. BACKTRACKING

Otra técnica de programación que puede implementarse con recursión es el Backtracking. 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, utilizando las
restricciones que ofrece el problema. La idea fundamental se basa en que no es necesario

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.

PARA TENER EN CUENTA


 Si no recuerda las propiedades de los logaritmos repáselas de su libro preferido de cálculo. Son muy
útiles al momento de calcular complejidades.
 Después de que lea el desarrollo de cada uno de los ejemplos de esta lectura intente fomentar la
práctica volviéndolos a hacer usted mismo en una hoja blanca y sin ayuda de nadie. Es muy
importante que se ejercite de esta forma para que adquiera destreza en la solución de ejercicios.
 Ponga especial atención a los procedimientos que involucran la aplicación de leyes del álgebra y de
la aritmética. Analice con cuidado, paso a paso, la forma en que se calculó la complejidad temporal de
los distintos programas implementados en la lectura.
 Descargue los proyectos, ábralos en Eclipse y juegue haciendo programas que usen las funciones
tratadas en la lectura. ¡La única manera de aprender a programar es practicando!

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

Muchas veces uno se pregunta lo siguiente:


¿Qué tan demorado será mi programa?
¿Cuál solución será más eficiente, la mía o la de mi amigo?
¿Qué tantos recursos consumirá mi programa?
¿De qué manera depende el consumo de tiempo respecto al tamaño del problema a
resolver?
¿Habrá algoritmos más eficientes que el que acabé de desarrollar?

Sabemos que la ejecución de todo algoritmo demanda recursos computacionales, entre


estos:
Procesador: se refleja en el consumo de tiempo utilizado para ejecutar las instrucciones del
programa.
Memoria RAM: se refleja en el consumo de espacio ocupado por las variables del programa.
Disco duro.
Recursos de red.

Respecto a un recurso computacional, 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.

2. COMPLEJIDAD TEMPORAL Y COMPLEJIDAD ESPACIAL

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.

Para calcular complejidades temporales de forma teórica:


1. Identifique las variables que describen el tamaño del problema a resolver. Por ejemplo, si
se están sumando dos matrices de dimensiones el tamaño del problema estaría
descrito por la variable (el alto de las matrices) y por la variable (el ancho de las
matrices).
2. Defina las operaciones básicas que desea contabilizar. En el ejemplo anterior de suma de
matrices importaría contar cuántas sumas de celdas se realizan, razón por la que la suma de
números reales podría ser una operación básica interesante.

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.

Para calcular complejidades espaciales de forma teórica:


1. Identifique las variables que describen el tamaño del problema a resolver. Por ejemplo, si
se están sumando dos matrices de dimensiones el tamaño del problema estaría
descrito por la variable (el alto de las matrices) y por la variable (el ancho de las
matrices).
2. Encuentre una función que relacione el tamaño del problema con el número de variables
declaradas en el programa. Esta función refleja la complejidad espacial porque entre más
variables se declaren, más espacio en memoria RAM requeriría el algoritmo para ejecutarse.

En pocas palabras, la complejidad temporal mide el tiempo gastado y la complejidad espacial


mide el espacio requerido.

3. NOTACIÓN O

Para medir complejidades se utiliza la notación (¡ojo!: es la letra O del alfabeto, no es un


cero), conocida en inglés como Big-Oh notation. La notación se centra en un análisis en el
peor caso, es decir, en el escenario más adverso imaginable que haga que la función ejecute
el máximo número de operaciones posible.

Dada una función se dice que un algoritmo tiene complejidad temporal si la


cantidad de operaciones que ejecuta siempre está por debajo de un múltiplo constante de
, para grande. Similarmente se define para la complejidad espacial, pero sobre la
cantidad de variables declaradas.

4. ANÁLISIS DE COMPLEJIDAD DE PROGRAMAS ITERATIVOS

Normalmente, la complejidad temporal de los algoritmos iterativos se analiza mediante un


proceso de inspección, estudiando la forma de las instrucciones del programa. Como la
complejidad espacial generalmente es más fácil de calcular porque involucra un conteo de
variables, haremos énfasis sobre el cálculo de la complejidad temporal.

Para calcular la complejidad temporal de una secuencia de instrucciones en Java


instrucción 1;
instrucción 2;
instrucción 3;
...

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 una asignación en Java


variable=expresión;
halle la complejidad del cálculo de la expresión que está siendo asignada.

Para calcular la complejidad temporal de un condicional en Java halle la complejidad de cada


guarda y de cada comando 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;
}

5. ANÁLISIS DE COMPLEJIDAD DE PROGRAMAS RECURSIVOS

Para calcular la complejidad temporal de un algoritmo recursivo se debe formular una


ecuación de recurrencia que describa el consumo de tiempo y después se debe encontrar la
versión cerrada de esta ecuación. En la lectura anterior, donde se trató el tema de recursión,
se practicó con varios ejemplos el cálculo de complejidad de programas recursivos.

ESTRUCTURAS DE DATOS 5
6. TIPOS DE ANÁLISIS DE COMPLEJIDAD

En muchas situaciones, el peor caso de un algoritmo ocurre sólo en situaciones


extraordinarias. Por ejemplo, imagine un algoritmo con complejidad temporal que el
99.999% de las veces se comporta como si tuviera complejidad temporal , pero que en
el 0.001% restante alcanza su peor caso, adquiriendo la complejidad temporal de . Es
muy injusto, ¿cierto? Por esta razón hay tres tipos de análisis que se pueden hacer sobre los
algoritmos:

Análisis en el peor caso: se calcula la complejidad temporal pensando en el peor escenario


para el algoritmo, donde se ejecuta el máximo número de operaciones posible.
Análisis en el mejor caso: se calcula la complejidad temporal pensando en el mejor
escenario para el algoritmo, donde se ejecuta el mínimo número de operaciones posible.
Análisis en el caso promedio: se calcula la complejidad temporal pensando en un escenario
típico que no sea ni el mejor ni el peor.

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 .

PARA TENER EN CUENTA


 Uno de los trucos para calcular complejidades temporales es identificar las porciones de código más
lentas y concentrarse solamente en estas porciones.
 En todo el módulo estaremos calculando complejidades temporales. El estudio de las Estructuras de
Datos se basa fuertemente en la teoría de la complejidad computacional, así que ¡póngale cuidado!.
 Habiendo estudiado esta lectura sobre análisis de algoritmos, es aconsejable que realice una ojeada
sobre la lectura anterior (recursión) deteniéndose sobre los párrafos que tratan sobre complejidad
temporal. De esta manera puede fortalecer los conceptos adquiridos.

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 4: Opción 5: Opción 6:

Opción 7: Opción 8:

1.2. Implemente en Java la función diseñada en el numeral anterior.

2. MÁXIMO COMÚN DIVISOR

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!

2.3. Aplique la técnica de Memoization sobre la función implementada. Siga el esquema:


public static BigInteger[][] tabla=new BigInteger[101][101];
public static BigInteger gcdMemoization(BigInteger a, BigInteger b) {
//...
}

3. ALGORITMO DE ORDENAMIENTO RÁPIDO (QUICK SORT)

3.1. Investigue sobre el algoritmo de Ordenamiento Rápido (Quick Sort). Describa el


algoritmo con sus propias palabras, en pseudocódigo.

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.4. ¿Por qué la complejidad temporal del algoritmo en el caso promedio es de


?

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?

3.6. Implemente el algoritmo en Java por sus propios medios.

4. STOOGE SORT

El algoritmo de Ordenamiento Chiflado (Stooge Sort) ordena un arreglo de la siguiente


manera:

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);
}
}
}

4.1. ¿Por qué el algoritmo usa la técnica de Dividir y Vencer?

4.2. Demuestre detalladamente que el algoritmo tiene complejidad temporal ,


que es aproximadamente . Ayuda: puede usar la fórmula de cambio de base
, la fórmula y la ecuación .

5. MEMOIZATION EN DOS DIMENSIONES

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.5. ¿Valdría la pena aplicar la técnica de Memoization? ¿Por qué?

5.6. Sin importar la respuesta a la pregunta anterior, implemente en Java un procedimiento


recursivo que calcule con la técnica de Memoization, almacenando los resultados en
una matriz de números (de tipo BigInteger) con filas y columnas, siguiendo el
siguiente esquema:
public static BigInteger tabla[][]=new BigInteger[100][100];
public static BigInteger cMemoization(int n, int k) {
// ...
}

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>.

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>.

Competencias Semanales de Aprendizaje:

1- Aplicar modelos matemáticos en el planteamiento de problemas.


2- 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.

3- Utilizar sistemas de Tecnología, Información y Telecomunicaciones que permitan la


recreación de un modelo, que permita resolver problemas en el mundo real.

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.

7- 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.
8- Ser emprendedor, capaz de decidir en condiciones de incertidumbre valorando las
consecuencias que ello implica.
9- Detectar oportunidades de trabajo, negocio y desarrollo en beneficio de sí mismo y de
su comunidad.
10- Manejar sus sentimientos y pensamientos hacia el logro de sus metas y propósitos, sin
perder de vista la manera como esto se relaciona con las dinámicas sociales en que está
inmerso.
11- Ser consciente de los valores y de los objetivos personales en relación con los objetivos
de grupo y de comunidad.
12- Actuar de manera autónoma, ética y responsable.

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>.

ACTIVIDAD SEMANA INSTRUCTIVO


Lectura 3-1 - Tres Leer y comprender los conceptos expuestos en las lecturas.
Las listas como
estructuras de datos.

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.

Proyecto. Tres Entrega de los documentos que soportan la etapa de levantamiento de


requerimientos y la etapa de análisis del proyecto de aula.

Ejercicios propuestos. Tres Desarrollar de forma individual algunos de los ejercicios propuestos para la
semana.

Teleconferencia Tres Asistir a la teleconferencia de 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>.

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.

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>.

Competencias Semanales de Aprendizaje:

1- Aplicar modelos matemáticos en el planteamiento de problemas.


2- 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.

3- Utilizar sistemas de Tecnología, Información y Telecomunicaciones que permitan la


recreación de un modelo, que permita resolver problemas en el mundo real.
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.

7- 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.
8- Ser emprendedor, capaz de decidir en condiciones de incertidumbre valorando las
consecuencias que ello implica.
9- Detectar oportunidades de trabajo, negocio y desarrollo en beneficio de sí mismo y de
su comunidad.
10- Manejar sus sentimientos y pensamientos hacia el logro de sus metas y propósitos, sin
perder de vista la manera como esto se relaciona con las dinámicas sociales en que está
inmerso.
11- Ser consciente de los valores y de los objetivos personales en relación con los objetivos
de grupo y de comunidad.
12- Actuar de manera autónoma, ética y responsable.

[ 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>.

ACTIVIDAD SEMANA INSTRUCTIVO


Lectura 4-1 - Cuatro Leer y comprender los conceptos expuestos en las lecturas.
Implementaciones de
listas (parte 2).

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.

Evaluación parcial Cuatro Resolver y enviar.

Teleconferencia Cuatro Asistir a la teleconferencia de 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

representaremos una lista cuyos elementos son , , , …, , en ese orden. Para


denotar la lista vacía, que no tiene ningún elemento, usaremos la expresió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:

La lista de los artículos que hay que comprar en el supermercado.


La lista de los artículos comprados en la tienda, en el orden en que aparecen en el recibo de
compra.
etc.

3. CONCEPTOS BÁSICOS

3.1. POSICIÓN

Todos los elementos de una lista se pueden acceder por posición. En la lista

el elemento de la posición es , el elemento de la posición es , el elemento de la


posición es , y así sucesivamente. En general, el elemento que se encuentra ubicado en
la posición de la lista sería .

De vez en cuando colocaremos números en azul encima de las listas, mostrando


explícitamente las posiciones de cada uno de sus elementos

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

Un número ordinal es un número que representa la posición de un elemento dentro de una


lista. Siendo el tamaño de la lista:
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.

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
.

Tabla 2: Ejemplos sobre los conceptos de igualdad, contenencia y sublista.


¿La lista 1 ¿La lista 1 ¿La lista 1
Lista 1 Lista 2 es igual a está contenida es sublista de
la lista 2? en la lista 2? la lista 2?
SI SI SI

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

Para administrar el contenido de una lista existen cuatro operaciones básicas:


1. Consulta: Obtener el valor presente en una posición dada.
2. Modificación: Modificar el valor presente en una posición dada.
3. Inserción: Añadir un nuevo elemento en cierta posición de la lista.
4. Eliminación: Quitar de la lista el elemento que está en cierta posición.

Observe que todas las operaciones básicas se definen en términos de posiciones. A


continuación se presenta un ejemplo que ilustra la aplicación de las operaciones básicas,
donde en cada paso:
Se resaltan en amarillo los elementos consultados.
Se resaltan en cian los elementos modificados.
Se resaltan en verde los elementos insertados.
Se resaltan en rojo los elementos que van a ser eliminados en el siguiente paso.

Tabla 3: Ejemplo de la aplicación de las operaciones básicas sobre listas.


Lista Operación Descripción de la operación realizada
Creación Creación de una lista vacía.
Inserción del elemento en la posición .
Inserción
(inserción al final)
Inserción del elemento en la posición .
Inserción
(inserción al final)
Inserción del elemento en la posición .
Inserción
(inserción al final)
Inserción del elemento en la posición
Inserción
(inserción al principio)
Inserción del elemento en la posición .
Inserción
(inserción al principio)

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 .

Eliminación Eliminación del elemento de la posición .


Eliminación del elemento de la posición .
Eliminación
(eliminación al final)
Eliminación del elemento de la posición .
Eliminación
(eliminación al principio)
Eliminación del elemento de la posición .
Eliminación
(eliminación al principio)
Inserción Inserción del elemento en la posición .

Inserción Inserción del elemento en la posición .

Inserción Inserción del elemento en la posición .


Modificación del elemento de la posición con el valor
Modificación
.
Modificación del elemento de la posición con el valor
Modificación
.
Modificación del elemento de la posición con el valor
Modificación
.
Consulta del elemento de la posición : nos entrega el
Consulta
valor .
Consulta del elemento de la posición : nos entrega el
Consulta
valor .
Consulta del elemento de la posición : nos entrega el
Consulta
valor .

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.

PARA TENER EN CUENTA


 Dada una lista de tamaño :
o Para quitar el primer valor de la lista debemos eliminar el elemento que está en la posición .
o Para quitar el último valor de la lista debemos eliminar el elemento que está en la posición .
o Para añadir un valor al principio de la lista debemos insertarlo en la posición .
o Para añadir un valor al final de la lista debemos insertarlo en la posición .
o Si insertamos un valor en la posición de la lista, éste nos quedará de penúltimo pero no de
último. Recuerde que las inserciones al final debemos hacerlas en la posición y no en la posición
.
 Dada una lista de tamaño , la operaciones básicas reportan error bajo las siguientes situaciones:
o Cuando intentamos consultar el valor de la posición , donde o .
o Cuando intentamos modificar el valor de la posición , donde o .
o Cuando intentamos insertar un valor en la posición , donde o .
o Cuando intentamos eliminar el valor de la posición , donde o .
o Observe que, cuando tratamos con la posición , la operación de inserción no lanza error pero el
resto de operaciones sí reportan error. ¿Por qué este comportamiento tiene sentido?

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>

En el área de la programación orientada a objetos, 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 †.

Gráfica 1: Documentación de la interfaz List<E> en el API de Java.

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.

2. IMPLEMENTACIONES SUMINISTRADAS POR JAVA PARA LA


INTERFAZ LIST<E>

Trabajaremos tres implementaciones de Java para la interfaz List<E>:

ArrayList<E>: implementa listas a través de vectores (arreglos dinámicos de tamaño


variable).


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

casillas ocupadas casillas de reserva

Vector<E>: implementa listas a través de vectores (arreglos dinámicos de tamaño variable).

Gráfica 3: Representación de la lista con la clase Vector<E> de Java.


0 1 2 n-1 n t-1
Arreglo ... ... Tamaño

casillas ocupadas casillas de reserva

LinkedList<E>: implementa listas a través de nodos doblemente encadenados en anillo con


encabezado.

Gráfica 4: Representación de la lista con la clase LinkedList<E> de Java.


Encabezado 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

3. SERVICIOS PROVISTOS POR LA INTERFAZ LIST<E>


Tabla 6: Los veinticinco métodos sin implementación ofrecidos por la interfaz List<E>.
Método Descripción
Operaciones de consulta sobre la lista
int size() Retorna el tamaño de la lista.
boolean isEmpty()
Retorna true si la lista está vacía; retorna
false si no.

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.

PARA TENER EN CUENTA


 Para poder usar las implementaciones ArrayList<E> y LinkedList<E> de la forma más eficiente
posible, es necesario conocerlas internamente. Este es el propósito de la siguiente lectura:
implementaciones de listas.
 En la próxima semana practicaremos sobre el uso de los métodos de la interfaz List<E>.

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.

Para tener criterio en la escogencia, es requisito conocer perfectamente la estructura interna


de ambas implementaciones. Así sabremos exactamente qué estamos usando y qué
complejidad temporal nos ofrecen todos los servicios provistos.

2. DECLARACIÓN DE UNA INTERFAZ PARA LA


REPRESENTACIÓN DE LISTAS
Recurso como proyecto en Eclipse: ImplementacionesListas.zip.

Con el objetivo de conocer la estructura interna detallada de las distintas implementaciones


de listas, desarrollaremos desde cero una librería sobre listas que nos dé funcionalidad
similar a la de las clases ArrayList<E>, Vector<E> y LinkedList<E> de Java.

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.

El recurso ImplementacionesListas.zip contiene el código fuente de la interfaz VEDList<E> y


de las clases VEDArrayList<E> y VEDLinkedList<E>. Las clases VEDLinkedListD<E> y

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

3. APUNTES SOBRE LA ADMINISTRACIÓN DE LA MEMORIA


PRINCIPAL EN JAVA

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.

Gráfica 2: Las variables de tipo básico se administran por valor.


MEMORIA RAM
CÓDIGO FUENTE
int x=5,y=8; f
x
char c= 'M'; c
double f= 3.5; y

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

CÓDIGO FUENTE centroX


Circulo a=new Circulo(3.5,2.0,5.0); a centroY
radio

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.

4. IMPLEMENTACIÓN CON VECTORES


Recurso como proyecto en Eclipse: ImplementacionesListas.zip.

La clase VEDArrayList<E> implementa listas con arreglos dinámicos de tamaño variable,


exhibiendo la estructura interna de las clases ArrayList<E> y Vector<E> de Java.

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.

Gráfica 5: Representación de la lista como un arreglo dinámico de tamaño variable.


VEDArrayList<E>
0 1 2 n-1 n t-1
arreglo ...  ... 
tamanho
casillas ocupadas casillas de reserva

Los atributos de la clase VEDArrayList<E> son:

 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.

Código 6: Declaración de la clase VEDArrayList<E>.


public class VEDArrayList<E> implements VEDList<E> { // Declaración de la clase

// *************************
// * 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.

Código 7: Método constructor de la clase VEDArrayList<E>.


public VEDArrayList() {
}

Tabla 8: Análisis de complejidad temporal del método constructor VEDArrayList().


Tipo de análisis Complejidad Justificación
Peor caso En todo caso, el atributo arreglo se inicializa con un arreglo de
Caso promedio capacidad y el atributo tamanho se inicializa en , dando una
complejidad temporal de por ser un número constante de
Mejor caso
operaciones.

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.

Código 9: Método que elimina todos los elementos de la lista.


public void clear() {
// Liberar los apuntadores de todas las casillas ocupadas del arreglo:
for (int i=0; i<tamanho; i++) {
arreglo[i]=null;
}
// Poner el tamaño en cero para que la lista quede vacía:
tamanho=0;
}

Tabla 10: Análisis de complejidad temporal del método void clear().


Tipo de análisis Complejidad Justificación
Peor caso
En todo caso, se le debe asignar el valor null a las casillas
Caso promedio
ocupadas del arreglo para poder liberar la memoria.
Mejor caso

4.3. OPERACIÓN DE CONSULTA

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;
}

Tabla 12: Análisis de complejidad temporal del método E get(int index).


Tipo de análisis Complejidad Justificación
Peor caso
Acceder la posición de un arreglo para consultarla tiene
Caso promedio
complejidad temporal .
Mejor caso

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

4.5. OPERACIÓN DE INSERCIÓN

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:

1. Crear un nuevo arreglo con la nueva capacidad deseada.


2. Copiar todos los elementos de la lista al nuevo arreglo.
3. Poner a apuntar la variable arreglo al arreglo nuevo, para desechar el arreglo viejo. El
Recolector de Basura será el encargado de liberar la memoria ocupada por el arreglo viejo.

El método privado garantizarCapacidad de la clase VEDArrayList<E> se responsabiliza de


garantizar que cierta cantidad de elementos quepa en el arreglo, creciendo la capacidad de
éste (de ser necesario).

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.

Analicemos un ejemplo: suponga que tenemos un arreglo de capacidad que no tiene


casillas de reserva porque todas sus casillas están ocupadas. Al intentar añadir el
decimoséptimo elemento nos damos cuenta de que el arreglo está lleno y de que se requiere
incrementar su capacidad. Entonces agrandamos la capacidad del arreglo a (¡ya sabemos
cómo!), lo que nos deja casillas ocupadas y casillas de reserva. Como nos quedan
casillas de reserva, podemos insertar el decimoséptimo, el decimoctavo, …, y el trigésimo
segundo elemento sin necesidad de volver a crecer la capacidad del arreglo. Al intentar
añadir el trigésimo tercer elemento nos percatamos de que debemos volver a duplicar la
capacidad, quedando un arreglo con casillas, donde son ocupadas y son de reserva.
Y ahora, esas casillas de reserva nos darían permiso de agregar el trigésimo tercer, el
trigésimo cuarto, … y el sexagésimo cuarto † elemento. Siguiendo este argumento,
¡descubrimos que el proceso que aumenta la capacidad del arreglo debe llamarse cada vez
con menos frecuencia a medida que el tamaño de la lista aumenta! ¿Cuántas veces habría
que duplicar el tamaño del arreglo para efectuar un millón de inserciones consecutivas?

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:

1. Se ejecuta el método garantizarCapacidad para asegurar que quepan en total tamanho+1


elementos en el arreglo.


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
... ...   ... 
... ...

... ...  ... 


0 1 2 k-1 k k+1 k+2 k+3 n n+1 t-1
Espacio donde será
colocado el nuevo elemento

Código 17: Método para insertar un elemento en determinada posición de la lista.


public void add(int index, E element) {
// Garantizar que quepan en total tamanho+1 elementos en el arreglo:
garantizarCapacidad(tamanho+1);
// Correr una posición hacia la derecha todos los elementos desde la
// posición tamanho-1 hasta la posición index:
for (int i=tamanho-1; i>=index; i--) {
arreglo[i+1]=arreglo[i];
}
// Ubicar el nuevo elemento en la posición index:
arreglo[index]=element;
// Incrementar el tamaño en una unidad:
tamanho++;
}

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.

4.1. OPERACIÓN DE ELIMINACIÓN

Para eliminar el elemento ubicado en la posición index de la lista:

1. Se almacena en una variable auxiliar el elemento que se encuentra en la posición index.


2. Se corren una posición hacia la izquierda todos los elementos de la lista que están
ubicados desde la posición index+1 hasta la posición tamanho-1, recorriendo el arreglo al
derecho: primero se traslada el elemento de la posición index+1, luego se traslada el
elemento de la posición index+2, …, y finalmente se mueve el elemento de la posición
tamanho-1. ¿Qué tan complicada queda la programación si hacemos el corrimiento
recorriendo el arreglo al revés en lugar de hacerlo al derecho?
3. Se asigna el valor null a la casilla de la posición tamanho-1 del arreglo.
4. Se disminuye el tamaño de la lista (tamanho) en una unidad.
5. Se retorna el elemento eliminado, que fue almacenado en el paso 1 en nuestra variable
auxiliar.

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

... ...  ... 


0 1 2 k-1 k k+1 n-3 n-2 n-1 n t-1 Paso 3
... ...   ... 
0 1 2 k-1 k k+1 n-3 n-2 n-1 n t-1

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;
}

Tabla 21: Análisis de complejidad temporal del método E remove(int index).


Tipo de análisis Complejidad Justificación
El peor caso ocurre cuando se desea eliminar el primer elemento
de la lista, alcanzando la complejidad temporal porque hay
Peor caso
que correr todos los elementos de la lista (excepto el primero)
una posición a la izquierda.
En el caso promedio la eliminació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 izquierda.
El mejor caso ocurre cuando se desea eliminar el último
Mejor caso elemento de la lista. La complejidad temporal en esta situación
es porque no hay que trasladar ningún 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

1. IMPLEMENTACIÓN CON LISTAS DOBLEMENTE ENCADENADAS EN ANILLO CON ENCABEZADO 2


1.1. CONSTRUCTOR 4
1.2. DESTRUCTOR 5
1.3. OPERACIÓN DE CONSULTA 6
1.4. OPERACIÓN DE MODIFICACIÓN 7
1.5. OPERACIÓN DE INSERCIÓN 8
1.6. OPERACIÓN DE ELIMINACIÓN 9
2. IMPLEMENTACIÓN CON LISTAS DOBLEMENTE ENCADENADAS 11
3. IMPLEMENTACIÓN CON LISTAS SENCILLAMENTE ENCADENADAS 12
4. COMENTARIOS SOBRE EFICIENCIA 12
EN RESUMEN 13
PARA TENER EN CUENTA 14

*
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.

La clase VEDLinkedList<E> implementa listas con nodos doblemente encadenados en anillo


con encabezado (en inglés: header), exhibiendo la estructura interna de la clase
LinkedList<E> de Java.

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:

 val: valor almacenado por el nodo.


 ant: es un apuntador al anterior nodo en el encadenamiento.
 sig: es un apuntador al siguiente nodo en el encadenamiento.

Gráfica 1: Representación gráfica de los nodos en una lista doblemente encadenada.

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.

Gráfica 2: Representación de la lista como


una lista doblemente encadenada en anillo con encabezado.
VEDLinkedList<E>

header

tamanho

...
...

Por simplicidad, preferiremos dibujar los nodos de forma lineal y no circular.

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

La lista vacía se representaría internamente como el encabezado apuntándose a sí mismo.

Gráfica 4: Representación de la lista vacía como una lista doblemente encadenada en anillo con encabezado.
VEDLinkedList<E>
header 
tamanho

Los atributos de la clase VEDLinkedList<E> son:

 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.

Código 5: Declaración de la clase VEDLinkedList<E>.


public class VEDLinkedList<E> implements VEDList<E> { // Declaración de la clase

// **********************************
// * 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 6: Estado deseado después de la construcción de una lista vacía.


VEDLinkedList<E>
header 
tamanho

Observe que, en la inicialización de los atributos de la clase, se colocó el tamaño en y se


creó el encabezado con valor null, anterior null y siguiente null, quedando:

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!

Código 8: Método constructor de la clase VEDLinkedList<E>.


public VEDLinkedList() {
// El siguiente del encabezado ponerlo en el encabezado:
header.sig=header;
// El anterior del encabezado ponerlo en el encabezado:
header.ant=header;
}

Tabla 9: Análisis de complejidad temporal del método constructor VEDLinkedList().


Tipo de análisis Complejidad Justificación
Peor caso
En todo caso, la complejidad temporal es porque se
Caso promedio
ejecutan un número constante de operaciones.
Mejor caso

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:

Gráfica 10: Estado de la lista antes de llamar al método void clear().


VEDLinkedList<E>
header  ...
tamanho

a la situación:

Gráfica 11: Estado de la lista después de llamar al método void clear().


VEDLinkedList<E>
header 
tamanho

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.

Código 13: Método que elimina todos los elementos de la lista.


public void clear() {
// El siguiente del encabezado ponerlo en el encabezado:
header.sig=header;
// El anterior del encabezado ponerlo en el encabezado:
header.ant=header;
// Poner el tamaño en cero para que la lista quede vacía:
tamanho=0;
}

Tabla 14: Análisis de complejidad temporal del método void clear().


Tipo de análisis Complejidad Justificación
Peor caso Sin considerar el trabajo del Recolector de Basura, la complejidad

ESTRUCTURAS DE DATOS 5
Caso promedio temporal es , pero considerándolo, la complejidad sería
Mejor caso .

1.3. OPERACIÓN DE CONSULTA

Para las operaciones de consulta, modificación, inserción y eliminación es necesario contar


con un método que dada una posición, nos entregue el nodo de la lista que guarda el
elemento de la posición dada.

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.

Tabla 18: Análisis de complejidad temporal del método E get(int index).


Tipo de análisis Complejidad Justificación
Consultar el elemento de la mitad de la lista tiene complejidad
Peor caso
.
Consultar elementos cerca de la mitad de la lista tiene
Caso promedio
complejidad .
Consultar el primer o el último elemento de la lista tiene
Mejor caso
complejidad .

1.4. OPERACIÓN DE MODIFICACIÓN

Usando el método getNodo podemos modificar el elemento que se encuentra en cierta


posición de la lista.

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.5. OPERACIÓN DE INSERCIÓN

Igualmente, usando el método getNodo podemos insertar un elemento en cierta posición de


la lista (suponga que v es el valor a insertar y que k es la posición donde queremos insertar
este valor):

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

2. 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.
VEDNodo<E> b=getNodo(index);
VEDNodo<E> a=b.ant;
VEDLinkedList<E> a b

header  ... ...


tamanho



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

header  ... ...


tamanho

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

header  ... ...


tamanho

nuevo

5. Incremente el tamaño de la lista en .

Código 21: Método para insertar un elemento en determinada posición de la lista.


public void add(int index, E element) {
// Crear un nuevo nodo, cuyo valor sea element:
VEDNodo<E> nuevo=new VEDNodo<E>(element);
// 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;
// Reencadenar los nodos de la lista:
nuevo.ant=a;
nuevo.sig=b;
a.sig=nuevo;
b.ant=nuevo;
// Incrementar el tamaño en una unidad:
tamanho++;
}

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?

1.6. OPERACIÓN DE ELIMINACIÓN

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

header  ... ...


tamanho
b

2. Ponga el siguiente del nodo a en el nodo c y el anterior del nodo c en el nodo a.


a.sig=c;
c.ant=a;
VEDLinkedList<E> a c

header  ... ...


tamanho
b

3. Decrezca el tamaño de la lista en .

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.

Tabla 24: Análisis de complejidad temporal del método E remove(int index).


Tipo de análisis Complejidad Justificación
Peor caso Eliminar un valor de la mitad de la lista tiene complejidad .
Eliminar valores cerca de la mitad de la lista tiene complejidad
Caso promedio
.
Eliminar el valor del principio o del final de la lista tiene
Mejor caso
complejidad .

¿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?

2. IMPLEMENTACIÓN CON LISTAS DOBLEMENTE ENCADENADAS

Otra implementación de listas es con nodos doblemente encadenados con apuntador al


primero y al último (para futura referencia, llamaremos a esta versión VEDLinkedListD<E>).

Gráfica 25: Representación de la lista como


una lista doblemente encadenada de tipo VEDLinkedListD<E>.
VEDLinkedListD<E> 
primero ...
ultimo

tamanho

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 28: Representación de la lista como


una lista sencillamente encadenada de tipo VEDLinkedListS<E>.
VEDLinkedListS<E>
primero
...
tamanho

Gráfica 29: Representación de la lista vacía como una lista sencillamente encadenada de tipo VEDLinkedListS<E>.
VEDLinkedListS<E>
primero 
tamanho

4. COMENTARIOS SOBRE EFICIENCIA


Tabla 30: Ventajas y desventajas de las implementaciones estudiadas.
Implementación Ventajas Desventajas
VEDArrayList  Todos los elementos están contiguos  Se debe ejecutar una operación
en memoria principal. costosa cada vez que se llene la
 No requiere tanta memoria principal capacidad del arreglo (no importa
puesto que no hay encadenamientos. mucho porque no sucede muy
 Las operaciones de consulta por frecuentemente).
posición y modificación por posición  Las inserciones al principio de la lista
son muy eficientes: . son ineficientes: .
 Las inserciones al final de la lista son  Las eliminaciones al principio de la
eficientes cuando no toca crecer la lista son ineficientes: .
capacidad del arreglo: .
 Las eliminaciones al final de la lista
siempre son eficientes .

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.

PARA TENER EN CUENTA


La clase VEDLinkedList<E> nos sirvió para conocer con detalle la estructura interna de la clase
LinkedList<E> de Java. De ahora en adelante, usaremos LinkedList<E> en vez de
VEDLinkedList<E> porque está disponible en la librería estándar de Java, nos ofrece muchos más
servicios, y es capaz 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.
Prefiera ArrayList<E> en vez de LinkedList<E> si desea:
 Usar poca memoria principal.
 Eficiencia consultando el elemento de cualquier posición.
 Eficiencia modificando el elemento de cualquier posición.
 Eficiencia en las operaciones básicas sólo sobre el final de la lista.
Prefiera LinkedList<E> en vez de ArrayList<E> si desea:
 Eficiencia en las operaciones básicas tanto al principio como al final de la lista.
 Eficiencia en las operaciones básicas sólo sobre el principio de la lista.
 Crear una lista tan grande que no sea posible reservar en memoria un arreglo del tamaño suficiente
para alojar sus elementos.

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:

Tabla 1: Métodos de la interfaz Iterator<E>.


Método Descripción
Informa si la iteración tiene algún elemento pendiente por visitar. En otras
boolean hasNext() palabras, retorna true si aún faltan elementos por visitar y retorna false
de lo contrario.
E next() Retorna el siguiente elemento a visitar.
void remove()
Elimina de la colección el elemento que fue retornado por la última
invocación hecha al método next().

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.

2. RECORRIDO DE UNA LISTA USANDO EL MÉTODO GET

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:

Código 2: Recorrido de una lista usando el método get.


int n=lista.size();
for (int i=0; i<n; i++) {
E elementoActual=lista.get(i);
// Procesar el elemento actual como sea conveniente:
// ...
}

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.

Tabla 6: Ejemplos sobre recorridos de listas usando el método get.


Ejemplo Descripción
List<Integer> lista=new
ArrayList<Integer>();
lista.addAll(Arrays.asList(17,25,19,20));
int n=lista.size(); Imprime en consola los elementos de la lista
for (int i=0; i<n; i++) {
Integer elementoActual=lista.get(i);
System.out.println(elementoActual);
}
List<Integer> lista=new
ArrayList<Integer>();
lista.addAll(Arrays.asList(17,25,19,20));
int n=lista.size(),r=0; Imprime en consola el resultado de la suma de
for (int i=0; i<n; i++) { los elementos de la lista que es
Integer elementoActual=lista.get(i); exactamente .
r+=elementoActual;
}
System.out.println(r);

3. RECORRIDO DE UNA LISTA USANDO ITERADORES

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.

Código 7: Recorrido de una lista usando un iterador.


Iterator<E> iterador=lista.iterator();
while (iterador.hasNext()) {
E elementoActual=iterador.next();
// Procesar el elemento actual como sea conveniente:
// ...
}

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);

4. RECORRIDO DE UNA LISTA USANDO LA INSTRUCCIÓN FOR-


EACH

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:
// ...
}

Tabla 9: Ejemplos sobre recorridos de listas usando la instrucción for-each.


Ejemplo Descripción
List<Integer> lista=new
ArrayList<Integer>();
lista.addAll(Arrays.asList(17,25,19,20)); Imprime en consola los elementos de la lista
for (Integer elementoActual:lista) {
System.out.println(elementoActual);
}
List<Integer> lista=new
ArrayList<Integer>();
lista.addAll(Arrays.asList(17,25,19,20)); Imprime en consola el resultado de la suma de
int r=0; los elementos de la lista
for (Integer elementoActual:lista) {
r+=elementoActual;
}
que es exactamente .
System.out.println(r);

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.

PARA TENER EN CUENTA


Investigue por sus propios medios sobre el método listIterator de la interfaz List<E>.
¿Qué servicios provee la interfaz ListIterator<E>?

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:

Tabla 1: Algunos métodos de la clase Stack<E>.


Método Descripción
E push(E item)
Inserta en el tope de la pila el ítem suministrado. Retorna el ítem
insertado.
E pop() Elimina y retorna el objeto que se encuentra en el tope de la pila.
E peek() Retorna el objeto que se encuentra en el tope de la pila.

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).

Gráfica 3: Abstracción de una pila.


inserción eliminación
(push) (pop)

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:

Tabla 6: Algunos métodos de la interfaz Queue<E>.


Método Descripción
boolean offer(E item)
Inserta en la cola el ítem suministrado. Retorna verdadero en caso de
éxito.
E poll() Elimina y retorna el objeto que se encuentra en la cabeza de la cola.
E peek() Retorna el objeto que se encuentra en la cabeza de la cola.

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).

Gráfica 8: Abstracción de una cola.


inserción
cabeza cola (offer)

...

eliminación
(poll)

La clase LinkedList<E> provee una implementación de colas con nodos doblemente


encadenados en anillo con encabezado.

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:

Código 12: Clase para representar enfermos.


public class 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;
}
}

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;
}
}
}

El método compareTo es el responsable de definir cuál enfermo debe ir primero en la cola.


Siendo f y g dos enfermos, la expresión f.compareTo(g) debe retornar un número negativo si
el enfermo f tiene mayor prioridad que el enfermo g, un número positivo si tiene menor
prioridad, y cero si tiene igual prioridad. Así entonces, podemos atender los enfermos en
orden de urgencia, utilizando una cola de prioridad:

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());
}
}
}

Gráfica 15: Impresión en consola del programa anterior.

ESTRUCTURAS DE DATOS 8
5. APLICACIONES

5.1. EVALUACIÓN DE EXPRESIONES EN NOTACIÓN INFIJA


Recurso como proyecto en Eclipse: Expresiones.zip.

Considere la expresión aritmética

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:

1. Se crea una pila vacía de cadenas de texto.


2. Se descompone la cadena de texto en paréntesis, operadores ( ) y números. Cada
una de las partes de la descomposición se denomina token.
3. Por cada token de la descomposición, en el orden en el que aparecen en la expresión:
3.1. Si el token representa un paréntesis de apertura '(', se descarta.
3.2. Si el token representa un operador, se inserta en el tope de la pila.
3.3. Si el token representa un número, se inserta en el tope de la pila.
3.4. Si el token representa un paréntesis de cierre ')':
3.4.1. Se elimina tres veces el tope de la pila, almacenando los valores en las variables
'operando2', 'operador' y 'operando1', respectivamente.
3.4.2. Se transforman las cadenas de texto operando1 y operando2 en números flotantes f y g,
respectivamente.
3.4.3. Se inserta en el tope de la pila un valor u otro de acuerdo a la siguiente regla:
3.4.3.1. Si operador es '+': se inserta en el tope de la pila el resultado de f+g.
3.4.3.2. Si operador es '-': se inserta en el tope de la pila el resultado de f-g.
3.4.3.3. Si operador es '*': se inserta en el tope de la pila el resultado de f*g.
3.4.3.4. Si operador es '/': se inserta en el tope de la pila el resultado de f/g.
4. Retorne como resultado el valor que se encuentra en el tope de la pila.

Código 16: Algoritmo para evaluar una expresión en notación infija.


import java.util.*;
public class ExpresionesInfijas {
public static void main(String[] args) {
String expresion="((((11.2+8.8)-6)*(4+(4*2)))/6)";
double resultado=evaluarInfijo(expresion);
System.out.println(expresion+"="+resultado);

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());
}
}

Gráfica 17: Impresión en consola del programa anterior.

5.2. EVALUACIÓN DE EXPRESIONES EN NOTACIÓN POSFIJA (NOTACIÓN


POLACA INVERSA)
Recurso como proyecto en Eclipse: Expresiones.zip.

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.

PARA TENER EN CUENTA


En la unidad tres del módulo, que trata sobre árboles, veremos algunas aplicaciones de las pilas y de
las colas.

ESTRUCTURAS DE DATOS 11
ESTRUCTURAS DE DATOS
UNIDAD DOS - SEMANA CUATRO
EJERCICIOS PROPUESTOS *

1. LISTAS DOBLEMENTE ENCADENADAS CON APUNTADOR AL PRIMERO Y AL


ÚLTIMO

La clase VEDLinkedListD<E> implementa listas con nodos doblemente encadenados con


apuntador al primero y al último. La lista se representaría en la forma
VEDLinkedListD<E> 
primero ...
ultimo

tamanho

y la lista vacía se representaría en la forma


VEDLinkedListD<E>
primero 
ultimo 
tamanho

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:

 Tenga cuidado, porque no hay encabezado.


 Si index<tamanho/2, sale más barata la búsqueda a partir del nodo primero.
 Si index>tamanho/2, sale más barata la búsqueda a partir del nodo ultimo.
 Si index==tamanho/2, no importa por dónde se inicie la búsqueda, ya sea a partir de primero
o a partir de ultimo.

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;
}
}

1.4. Implemente el método get(int index), apoyándose en el método getNodo.

1.5. Implemente el método set(int index, E element), apoyándose en el método getNodo.

1.6. Implemente el método add(int index, E element), traduciendo a Java el pseudocódigo:

 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++;).

1.7. Implemente el método remove(int index), traduciendo a Java el pseudocódigo:

 Si la lista tiene tamaño 1 (o sea, si la eliminación remueve el único elemento de la lista):


o Ponga a apuntar al atributo “primero” a null (en código: primero=null;).
o Ponga a apuntar al atributo “ultimo” a null (en código: ultimo=null;).
o Decrezca el tamaño de la lista en (en código: tamanho--;).
 De lo contrario, si index es 0 (o sea, si la eliminación remueve el primer elemento de la
lista):
o Ponga el atributo “primero” en el siguiente del primer nodo (en código:
primero=primero.sig;).
o Ponga el anterior del “primero” en null (en código: primero.ant=null;).
o Decrezca 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.10. Calcule la complejidad temporal de todos los métodos que implementó.

1.11. ¿Por qué será más sencillo implementar una lista doblemente encadenada en anillo con
encabezado?

2. ALGORÍTMICA SOBRE LISTAS

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.1. Explique detalladamente por qué la versión 1 no funciona si se cambia “ p.remove(i--)”


por “p.remove(i)”.

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.4. Explique detalladamente por qué la versión 4 no funciona.

2.5. Calcule la complejidad temporal de las versiones 1, 2 y 3 suponiendo que p es un


ArrayList<Integer>.

2.6. Calcule la complejidad temporal de las versiones 1, 2 y 3 suponiendo que p es un


LinkedList<Integer>.

2.7. ¿Qué implementación es mejor? ¿Por qué? ¿Qué criterios tiene en cuenta?

3. ITERADORES

Usando iteradores, implemente un método


public static boolean f(List<Double> p) {
// ...
}
que informe si existen dos elementos consecutivos en la lista que multiplicados den el valor
1.0. La complejidad temporal de su solución debe ser tanto para vectores como para
listas encadenadas.

4. PALÍNDROMOS

4.1. Implemente un método


public static <E> boolean esPalidromo(List<E> p) {
// ...
}
que informe si la lista p es palíndromo o no. La complejidad temporal de su solución debe ser
tanto para vectores como para listas encadenadas, y debe seguir el siguiente esquema:

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.2. Explique por qué el algoritmo funciona.

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. Teoría e implementación de estructuras de datos recurrentes.

1.1. Árboles binarios.

1.1.1. Definiciones y conceptos básicos.

1.1.2. Recorridos (preorden, inorden, postorden, por niveles).

1.1.3. Reconstrucción de árboles binarios a partir de sus recorridos.

NÚCLEO TEMÁTICO: ÁRBOLES

2. Teoría e implementación de estructuras de datos recurrentes.

2.1. Árboles binarios.

2.1.1. Definiciones y conceptos básicos.

2.1.2. Recorridos (preorden, inorden, postorden, por niveles).

2.1.3. Reconstrucción de árboles binarios a partir de sus recorridos.

Competencias Semanales de Aprendizaje:

1- Aplicar modelos matemáticos en el planteamiento de problemas.


2- Utilizar sistemas de Tecnología, Información y Telecomunicaciones que permitan la recreación
de un modelo, que permita resolver problemas en el mundo real.
3- Evaluar Sistemas de Tecnologías de Información, tanto software como hardware, a partir de
diferentes criterios

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.

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.

8- Detectar oportunidades de trabajo, negocio y desarrollo en beneficio de sí mismo y de su


comunidad.
9- Generar estrategias de trabajo efectivo en equipo.
10- Generar confianza en los demás a través la honestidad, sinceridad, respeto, lealtad y demás
valores.

NÚCLEO TEMÁTICO CINCO: ÁRBOLES


2. Teoría e implementación de estructuras de datos recurrentes.
2.1. Árboles binarios.
2.1.1. Definiciones y conceptos básicos.
2.1.2. Recorridos (preorden, inorden, postorden, por niveles).
2.1.3. Reconstrucción de árboles binarios a partir de sus recorridos.

ACTIVIDAD SEMANA INSTRUCTIVO


Lectura 5-1 - Introducción Cinco Leer y comprender los conceptos expuestos en las lecturas.
a los árboles.
Lectura 5-2 - Árboles Cinco Leer y comprender los conceptos expuestos en las lecturas.
binarios.
Video diapositivas 1 - Cinco Leer y comprender los conceptos expuestos en los videos
Recorridos sobre árboles. diapositivas.
Segunda entrega del Cinco Entregar los productos requeridos en el enunciado del
proyecto proyecto.
Ejercicios propuestos. Cinco Desarrollar de forma individual algunos de los ejercicios
propuestos para la semana.
Teleconferencia. Cinco Asistir a la teleconferencia de la semana.
Recursos adicionales. Cinco Revisar cuidadosamente el material en Java organizado
como proyectos en Eclipse, con el fin de afianzar la
práctica.

[ ESTRUCTURA DE DATOS ] 2
 SEMANA 6
TEMA (S): NÚCLEO TEMÁTICO CINCO: ÁRBOLES

1. Teoría e implementación de estructuras de datos recurrentes.

1.1. Árboles binarios.

1.1.1. Definiciones y conceptos básicos.

1.1.2. Recorridos (preorden, inorden, postorden, por niveles).

1.1.3. Reconstrucción de árboles binarios a partir de sus recorridos.

NÚCLEO TEMÁTICO: ÁRBOLES

2. Teoría e implementación de estructuras de datos recurrentes.

2.1. Árboles binarios.

2.1.1. Definiciones y conceptos básicos.

2.1.2. Recorridos (preorden, inorden, postorden, por niveles).

2.1.3. Reconstrucción de árboles binarios a partir de sus recorridos.

Competencias Semanales de Aprendizaje:

1- Aplicar modelos matemáticos en el planteamiento de problemas.


2- Utilizar sistemas de Tecnología, Información y Telecomunicaciones que permitan la recreación
de un modelo, que permita resolver problemas en el mundo real.
3- Evaluar Sistemas de Tecnologías de Información, tanto software como hardware, a partir de
diferentes criterios

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 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.

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.

8- Detectar oportunidades de trabajo, negocio y desarrollo en beneficio de sí mismo y de su


comunidad.
9- Generar estrategias de trabajo efectivo en equipo.
10- Generar confianza en los demás a través la honestidad, sinceridad, respeto, lealtad y demás
valores.
NÚCLEO TEMÁTIC0: ÁRBOLES
2. Teoría e implementación de estructuras de datos recurrentes.
2.1. Árboles binarios.
2.1.1. Definiciones y conceptos básicos.
2.1.2. Recorridos (preorden, inorden, postorden, por niveles).
2.1.3. Reconstrucción de árboles binarios a partir de sus recorridos.

ACTIVIDAD SEMANA INSTRUCTIVO


Lectura 6-1 - Algoritmos Seis Leer y comprender los conceptos expuestos en las lecturas.
de ordenamiento.
Lectura 6-2 - Seis Leer y comprender los conceptos expuestos en las lecturas.
Comparadores de orden
en Java.
Lectura 6-3 - Árboles Seis Leer y comprender los conceptos expuestos en las lecturas.
binarios ordenados.
Lectura 6-4 - Árboles Seis Leer y comprender los conceptos expuestos en las lecturas.
binarios ordenados
balanceados.
Video diapositivas 1 - Seis Leer y comprender los conceptos expuestos en los videos
Inserción sobre árboles diapositivas.
AVL
Ejercicios propuestos. Seis Desarrollar de forma individual algunos de los ejercicios
propuestos para la semana.
Teleconferencias. Seis Asistir a la teleconferencia de la semana.
Recursos adicionales. Seis Revisar cuidadosamente el material en Java organizado
como proyectos en Eclipse, con el fin de afianzar la
práctica.
Video resumen. Seis Ver el resumen de la unidad tres.

[ ESTRUCTURA DE DATOS ] 4
ESTRUCTURAS DE DATOS
UNIDAD TRES - SEMANA CINCO
INTRODUCCIÓN A LOS ÁRBOLES *

TABLA DE CONTENIDO

1. LOS ÁRBOLES COMO ESTRUCTURAS DE DATOS 2


2. APLICACIONES DE LOS ÁRBOLES 2
2.1. REPRESENTACIÓN DE INFORMACIÓN 2
2.1.1. ORGANIGRAMAS 2
2.1.2. MAPAS CONCEPTUALES 3
2.1.3. ÁRBOLES GENEALÓGICOS 4
2.1.4. ÁRBOLES FILOGENÉTICOS 4
2.1.5. ÁRBOLES DE SINTAXIS 4
2.1.6. SISTEMAS DE ARCHIVOS 5
2.1.7. ÁRBOLES DE JUEGO 5
2.2. REPRESENTACIÓN EFICIENTE DE INFORMACIÓN 6
2.2.1. TRIES 6
2.2.2. QUADTREES 7
2.3. ADMINISTRACIÓN EFICIENTE DE INFORMACIÓN 7
2.3.1. ÁRBOLES BINARIOS ORDENADOS 7
EN RESUMEN 8
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. 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.

Gráfica 1: Un árbol compuesto por siete nodos.

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.

2. APLICACIONES DE LOS ÁRBOLES

2.1. REPRESENTACIÓN DE INFORMACIÓN

Naturalmente, los árboles sirven para representar datos organizados de forma jerárquica,
como organigramas y mapas conceptuales.

2.1.1. ORGANIGRAMAS

Un organigrama representa a través de un árbol la estructura interna de una empresa,


organización, entidad o institución, exhibiendo las relaciones jerárquicas que existen entre
los miembros o dependencias que las conforman.

ESTRUCTURAS DE DATOS 2
Gráfica 2: Organigrama que explica una pequeña parte de la estructura del estado colombiano.
Estado

Ramas del poder Órganos de control

Rama ejecutiva Rama legislativa Rama judicial Ministerio público Contraloría

Presidente Congreso Procurador Defensor del pueblo Procuradores


delegados
Senado Cámara de representantes

2.1.2. MAPAS CONCEPTUALES

Un mapa conceptual es un diagrama que representa a través de un árbol la relación


jerárquica entre un conjunto de conceptos. Normalmente, las dependencias están
etiquetadas con la descripción de la relación que establecen.

Gráfica 3: Mapa conceptual que relaciona conceptos dispersos a partir del término árbol.
Árbol
tiene

Tallo Hojas Frutos Ramas Raíces Flores

a veces pueden tener


está hecho de da vivienda a transporta tienen algunos son otros son
Madera Ardillas Savia Aroma Comestibles Venenosos Tubérculos
que sirve como que son con la que como el
se produce como la como la
Materia prima Animales Caucho Eucalipto Papa Yuca

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

Juan Mario Darío

Roberto Jaime Jorge Francisco Fernando

Carlos Andrés Fabio

2.1.4. ÁRBOLES FILOGENÉTICOS

En biología, un árbol filogenético es un diagrama que describe a través de un árbol las


relaciones evolutivas entre las especies vivas del mundo natural, usando como subdivisiones
las siguientes: dominio, reino, filo, clase, orden, familia, género y especie.

Gráfica 5: Árbol filogenético que muestra únicamente los dominios y los reinos.
Vida

Dominios Bacteria Archaea Eukaryota

Reinos Animalia Plantae Fungi Chromista Protozoa

2.1.5. ÁRBOLES DE SINTAXIS

En matemáticas, un árbol de sintaxis representa el orden en el que se hacen las operaciones


en una expresión aritmética o lógica.

Gráfica 6: Árbol de sintaxis de la expresión aritmética .


+
z 7

ESTRUCTURAS DE DATOS 4
Gráfica 7: Árbol de sintaxis de la expresión aritmética .
+
* * –
– y 3 x +
5 x z 7

2.1.6. SISTEMAS DE ARCHIVOS

El sistema de archivos de un dispositivo de almacenamiento también se puede representar


con árboles, mostrando la jerarquía entre los directorios y los subdirectorios.

Gráfica 8: Parte del árbol de subdirectorios de la carpeta de instalación de Java en el sistema operativo Windows.

2.1.7. ÁRBOLES DE JUEGO

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).

2.2. REPRESENTACIÓN EFICIENTE DE INFORMACIÓN

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.

Gráfica 10: Trie que almacena el conjunto de palabras


FARO, FOCA, LUNA, LUZ, MAR, MAREA, MAREO, MARTA, MARTE, SOFÁ y SOL.

ESTRUCTURAS DE DATOS 6
2.2.2. QUADTREES

Un Quadtree (pronunciado cuadtri) es una estructura de datos utilizada para almacenar de


forma eficiente una imagen en blanco y negro de tamaño . Un nodo negro
representa una imagen completamente negra, un nodo blanco representa una imagen
completamente blanca y un nodo gris representa una imagen que tiene pixeles negros y
blancos a la vez.

Gráfica 11: Varias imágenes en blanco y negro con su respectiva representación como Quadtrees.

2.3. ADMINISTRACIÓN EFICIENTE DE INFORMACIÓN

2.3.1. ÁRBOLES BINARIOS ORDENADOS

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.

Gráfica 12: Un árbol binario ordenado con siete nodos.

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, …

PARA TENER EN CUENTA


En las próximas lecturas exploraremos algunas aplicaciones interesantes de los árboles como
estructuras de datos, teniendo como base la teoría de listas, pilas y colas que ya estudiamos.

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

izq : subárbol izquierdo

izq der der : subárbol derecho

es un árbol binario cuya raíz es val, cuyo subárbol izquierdo es izq, y cuyo subárbol derecho
es der.

Tabla 1: Árboles binarios de ejemplo.


Árbol binario Raíz Subárbol izquierdo Subárbol derecho
no tiene no tiene no tiene

Un nodo se define como la raíz de un árbol binario no vacío. En particular, , , , , ,


y son los nodos que conforman los árboles del ejemplo anterior.

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

2.1. RELACIONES DE PARENTESCO ENTRE NODOS

Las relaciones familiares se utilizan para nombrar relaciones típicas entre nodos:

Tabla 2: Relaciones de parentesco entre nodos.


Parentesco Definición
Hijo Un nodo es hijo de un nodo si y sólo si es la raíz de alguno de los
subárboles de .
Padre Un nodo es padre de un nodo si y sólo si es hijo de .
Hermano Un nodo es hermano de un nodo si y sólo si y son distintos y tienen el
mismo padre.
Nieto Un nodo es nieto de un nodo si y sólo si es hijo de un hijo de .
Abuelo Un nodo es abuelo de un nodo si y sólo si es nieto de .
Tío Un nodo es tío de un nodo si y sólo si es hermano del padre de .
Sobrino Un nodo es sobrino de un nodo si y sólo si es tío de .
Primo Un nodo es primo de un nodo si y sólo si el padre de es hermano del padre
de .

De la misma forma se puede definir bisnieto, tataranieto, bisabuelo y tatarabuelo. Además,


tenga en cuenta que el árbol vacío no puede estar emparentado porque no es un nodo.

Tabla 3: Ejemplos sobre relaciones de parentesco.


Árbol binario Ejemplos
El árbol vacío no tiene nodos.
es hijo de , y es padre de .
no tiene padre, y no tiene hijos.
Los nodos y no tienen hermanos.
y son hijos de . es el único hijo de . y son hijos de .
es padre de y de . es padre de . es padre de y de .
es el único nodo que no tiene padre.
Los nodos que tienen exactamente dos hijos son , y .
Sólo hay un nodo que tiene exactamente un hijo: el .
Los nodos que no tienen hijos son , , y .

2.2. HOJAS Y NODOS INTERNOS


Tabla 4: Definición de hoja y de nodo interno.
Clasificación Definición
Hoja Un nodo es una hoja si y sólo si no tiene hijos.
Nodo interno Un nodo es un nodo interno si y sólo si tiene por lo menos un hijo.

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.

Tabla 5: Ejemplos sobre los conceptos de hoja y nodo interno.


Árbol binario Ejemplos
El número total de nodos del árbol es .
La raíz del árbol es el nodo con valor .
Los nodos , , , y son hojas porque no tienen hijos.
Los nodos , , , y 23 son nodos internos porque no son hojas.
Todo nodo del árbol es una hoja o es un nodo interno.

2.3. CAMINOS Y RAMAS

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.

Tabla 6: Ejemplos sobre los conceptos de camino y rama.


Árbol binario Ejemplos
Hay caminos de longitud : , , , , ,y .
Hay caminos de longitud : , , , ,y .
Hay caminos de longitud : , ,y .
No hay caminos de longitud en adelante.
, y son hojas. Las ramas son , y .

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.

Tabla 7: Ejemplos sobre existencia y no existencia de caminos.


Árbol binario Ejemplos
no es camino porque no es padre de . no es
camino pues no es padre de y no es padre de .
tampoco es camino. No hay camino que vaya desde hasta .
Hay camino del nodo hacia todos los demás porque es la raíz del árbol.

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 .

Tabla 9: Ejemplos sobre los conceptos de ancestro y descendiente.


Árbol binario Nodo Ancestros Descendientes
, , , y .
y , y .
y .
y .
y .

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.

Tabla 10: Ejemplos sobre el concepto de nivel.


Árbol binario Ejemplos
El único nodo que está en el nivel es el .
Los nodos que están en el nivel son y .
Los nodos que están en el nivel son , , y .
Los nodos que están en el nivel son , y .
No hay nodos que estén desde el nivel en adelante.
Los niveles , y están llenos. El nivel no está lleno.

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.

2.6. PESO Y ALTURA

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.

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 ( )

2.7. ÁRBOLES DEGENERADOS, LLENOS, COMPLETOS Y PERFECTOS


Tabla 13: Definición de árbol degenerado, árbol lleno, árbol completo y árbol perfecto.
Concepto Definición (puede variar según la referencia bibliográfica)
Degenerado Un árbol es degenerado cuando su altura es igual a su peso.
Lleno Un árbol está lleno cuando todos sus nodos tienen cero o dos hijos.
Completo Un árbol está completo cuando todos sus niveles hasta el penúltimo están llenos, y
todos los nodos del último nivel están lo más a la
izquierda posible.
Perfecto Un árbol es perfecto cuando todos sus niveles están llenos.

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

3. IMPLEMENTACIÓN DE ÁRBOLES BINARIOS CON ÁRBOLES


SENCILLAMENTE ENCADENADOS
Recurso como proyecto en Eclipse: ArbolesBinarios.zip.

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

VEDArbin<E> VEDArbin<E> VEDArbin<E> VEDArbin<E>


val  val  val  val 
izq  izq  izq  izq 
der  der  der  der 

Durante la implementación de la estructura de datos, abreviaremos el peso del árbol con la


letra y la altura del árbol con la letra . Todas las complejidades temporales serán
expresadas en términos de y de .

Código 18: Declaración de la clase VEDArbin<E>.


public class VEDArbin<E> { // Declaración de la clase
// *************************
// * Atributos de la clase *
// *************************
protected E val;
protected VEDArbin<E> izq;
protected VEDArbin<E> der;
// ***********************
// * Métodos de la clase *
// ***********************
// ...
}

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.

Código 19: Métodos constructores de la clase VEDArbin<E>.


public VEDArbin() {
// 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;
}
public VEDArbin(E pVal, VEDArbin<E> pIzq, VEDArbin<E> pDer) {
// Si la raíz dada es null, lanzar error:
if (pVal==null) {
throw new NullPointerException("¡Un árbol no vacío debe tener raíz!");
}
// Si el subárbol izquierdo dado es null, lanzar error:
if (pIzq==null) {
throw new NullPointerException("¡Un árbol no vacío necesita subárbol izquierdo!");
}
// Si el subárbol derecho dado es null, lanzar error:
if (pDer==null) {
throw new NullPointerException("¡Un árbol no vacío necesita subárbol derecho!");
}
// Inicializar los atributos del árbol de acuerdo a los parámetros dados:
val=pVal;
izq=pIzq;
der=pDer;
}

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:Ø,Ø]],Ø]

3.2. MÉTODOS DE CONSULTA DE LOS ATRIBUTOS DE UN ÁRBOL BINARIO


Código 21: Método para consultar la raíz del árbol binario.
public E getVal() {
return val; // Retornar el valor de la raíz.
}

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();
}
}

Código 27: Función que calcula la altura del árbol.


public int altura() {
if (esVacio()) { // Si este árbol es vacío:
return 0; // La altura del árbol vacío es 0.
}
else { // Si este árbol no es vacío:
// La altura de un árbol no vacío es uno más el máximo entre la altura del subárbol
// izquierdo y la altura del subárbol derecho:
return 1+Math.max(izq.altura(),der.altura());
}
}

La complejidad temporal de los dos métodos anteriores es porque visitan exactamente


una vez cada uno de los nodos del árbol. Pero ¿por qué el procesamiento de los subárboles
vacíos no afecta la complejidad temporal? Debido a que es el número de nodos del árbol y
que cada nodo puede aportar máximo dos subárboles vacíos, entonces el número total de
subárboles vacíos que puede tener el árbol nunca puede ser mayor que . Dado que los
dos métodos anteriores procesan exactamente una vez cada nodo y cada subárbol vacío,
entonces el número de invocaciones recurrentes es menor o igual que (el número de
nodos) más (el máximo número de subárboles vacíos), lo que resulta en máximo
llamados. Se concluye entonces que la complejidad temporal de ambos algoritmos es .

3.4. MÉTODOS DE BÚSQUEDA SOBRE UN ÁRBOL BINARIO

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.

Código 28: Función que determina la cantidad de ocurrencias de un elemento en el árbol.


public int numeroOcurrencias(Object pObject) {
if (esVacio()) { // Si este árbol es vacío:
// El objeto no está en el árbol porque el árbol vacío no tiene elementos:
return 0;
}
else { // Si este árbol no es vacío:
// Contar cuántas veces está el objeto dado en el subárbol izquierdo:
int contadorIzq=izq.numeroOcurrencias(pObject);
// Contar cuántas veces está el objeto dado en el subárbol derecho:
int contadorDer=der.numeroOcurrencias(pObject);
if (pObject.equals(val)) { // Si el objeto buscado es igual a la raíz del árbol:

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.

4. RECORRIDOS SOBRE ÁRBOLES BINARIOS


Recurso como proyecto en Eclipse: ArbolesBinarios.zip.

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.

Tabla 30: Recorridos de algunos árboles binarios.


Recorrido
Árbol binario
Preorden Inorden Postorden Niveles

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.

Tabla 35: Algoritmo para hallar manualmente el recorrido en preorden.


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 izquierda, 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 preorden del ejemplo:

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:

Tabla 37: Algoritmo para hallar manualmente el recorrido en postorden.


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 derecha, 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 postorden del ejemplo:

Tabla 38: Algoritmo para hallar manualmente el recorrido por niveles.


Ilustración representativa Algoritmo
Mentalmente, nivel por nivel, visite los nodos de
izquierda a derecha, añadiéndolos a una lista. Los
números dentro de las cajas rojas muestran el
orden en el que se van visitando los elementos.
Recorrido por niveles del ejemplo:

5. RECONSTRUCCIÓN DE UN ÁRBOL BINARIO A PARTIR DE SUS


RECORRIDOS
Recurso como proyecto en Eclipse: ArbolesBinarios.zip.

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

Para poder reconstruir un árbol binario sin ambigüedad, se requiere:


1. Que el árbol no tenga elementos repetidos.
2. Contar con el recorrido en inorden y con algún otro recorrido, ya sea en preorden, en
postorden o por niveles. En otras palabras, se requiere el inorden y el preorden, o el inorden
y el postorden, o el inorden y el recorrido por niveles.

La reconstrucción de árboles es importante ya que nos permite la persistencia de un árbol


binario mediante el almacenamiento del inorden y de otro de sus recorridos. Es decir, para
guardar y cargar un árbol binario basta manipular dos listas: dos de sus recorridos donde uno
de éstos es el inorden.

5.1. RECONSTRUCCIÓN DE UN ÁRBOL BINARIO DADO SU INORDEN Y SU


PREORDEN

Sabiendo que el inorden recorre nodos en la forma


subárbol izquierdo en inorden – raíz – subárbol derecho en inorden
y que el preorden recorre nodos en la forma
raíz – subárbol izquierdo en preorden – subárbol derecho en preorden
podemos utilizar el siguiente algoritmo para reconstruir un árbol binario sin elementos
repetidos dados sus recorridos en inorden y en preorden:

1. Si el inorden y el preorden son vacíos, retorne el árbol vacío.


2. Si el inorden y el preorden no son vacíos:

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

inorden ... ...


Subárbol izquierdo en inorden Subárbol derecho en inorden
pos+1
pos-1

pos

n-3

n-2

n-1
0

preorden ... ...


Subárbol izquierdo en preorden Subárbol derecho en preorden

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.

Note cómo se reconstruyó el árbol binario


usando recursivamente el mismo argumento.

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);
}
}

Recuerde que el método subList(fromIndex,toIndex) de la interfaz List<E> entrega una


sublista que va desde la posición fromIndex hasta la posición toIndex-1 (¡la posición final no
se incluye!).

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.

PARA TENER EN CUENTA


Es necesario practicar a través del desarrollo de algunos de los ejercicios propuestos para fortalecer
los conceptos tratados en esta lectura.

ESTRUCTURAS DE DATOS 19
ESTRUCTURAS DE DATOS
UNIDAD TRES - SEMANA CINCO
EJERCICIOS PROPUESTOS *

1. RECORRIDOS SOBRE ÁRBOLES BINARIOS

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

2. RECONSTRUCCIÓN DE ÁRBOLES BINARIOS DADO SU INORDEN Y SU PREORDEN

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. IMPLEMENTACIÓN ITERATIVA DEL RECORRIDO EN INORDEN

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.2. Calcule la complejidad temporal de su implementación.

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

Implemente en la clase VEDArbin<E> un método recursivo que reconstruya un árbol binario


sin elementos repetidos a partir de su inorden y de su postorden.
public static <T>
VEDArbin<T> reconstruirArbolInPost(List<T> pInorden, List<T> pPostorden) {
// ...
}

6. ALGORÍTMICA SOBRE ÁRBOLES BINARIOS (LECTURA DE CÓDIGO)

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.1. Método que determina si un valor está o no dentro del árbol.


public boolean esta(E pValor) {
if (esVacio()) {
return false;
}
else {
if (val.equals(pValor)) {
return true;
}
else {
return izq.esta(pValor)||der.esta(pValor);
}
}
}

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
}

7. ALGORÍTMICA SOBRE ÁRBOLES BINARIOS (ESCRITURA DE CÓDIGO)

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.3. Método que retorna el número de ramas del árbol.


public int getCantidadRamas()

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

1. INTRODUCCIÓN A LOS ALGORITMOS DE ORDENAMIENTO 2


2. ALGUNOS ALGORITMOS DE ORDENAMIENTO 2
2.1. ORDENAMIENTO POR SELECCIÓN (SELECTION SORT) 2
2.2. ORDENAMIENTO POR INSERCIÓN (INSERTION SORT) 3
2.3. ORDENAMIENTO DE BURBUJA (BUBBLE SORT) 3
2.4. ORDENAMIENTO POR MEZCLA (MERGE SORT) 3
2.5. ORDENAMIENTO RÁPIDO (QUICK SORT) 4
3. EL ANIMADOR DE ALGORITMOS DE ORDENAMIENTO ALPHERATZ (VERSIÓN 6.0) 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. 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.

Tabla 1: Ordenamiento de algunos arreglos de ejemplo.


Relación de orden Arreglo sin ordenar Arreglo ordenado
Orden numérico, de menor
a mayor
Orden numérico, de mayor
a menor
Orden lexicográfico, de
menor a mayor
Orden lexicográfico, de
mayor a menor
Orden numérico (romano)

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.

Siendo el tamaño del arreglo a ordenar, la complejidad temporal de un algoritmo de


ordenamiento dependería de . Es común que el mejor caso consista en un arreglo ordenado
de menor a mayor, que el caso promedio consista en un arreglo con valores al azar, y que el
peor caso consista en un arreglo ordenado de mayor a menor.

2. ALGUNOS ALGORITMOS DE ORDENAMIENTO


Tabla 2: Algunos de los algoritmos de ordenamiento más conocidos.
Algoritmo Eficiencia (complejidad temporal)
En inglés En español Mejor caso Caso promedio Peor caso
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

2.1. ORDENAMIENTO POR SELECCIÓN (SELECTION SORT)

El algoritmo de Ordenamiento por Selección ordena un arreglo arr de tamaño n así:

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.

2.2. ORDENAMIENTO POR INSERCIÓN (INSERTION SORT)

El algoritmo de Ordenamiento por Inserción ordena un arreglo arr de tamaño n así:


1. Para cada i desde 1 hasta n-1:
1.1. Se almacena en una variable temporal el elemento de la posición i del arreglo.
1.2. Inicialice j en i.
1.3. Mientras j sea mayor que 0 y arr[j-1] sea mayor que el valor de la variable temporal:
1.3.1. Asigne a la posición j del arreglo el valor arr[j-1].
1.3.2. Decrezca j en 1.
1.4. Asigne a la posición j del arreglo el valor de la variable temporal.

2.3. ORDENAMIENTO DE BURBUJA (BUBBLE SORT)

El algoritmo de Ordenamiento de Burbuja ordena un arreglo arr de tamaño n así:


1. Para cada i desde 1 hasta n-1:
1.1. Para cada j desde n-1 hasta i, decreciendo j de a 1 en 1:
1.1.1. Si arr[j-1] es mayor que arr[j], se intercambian tales valores.
1.2. Si el ciclo 1.1. no efectuó ningún intercambio, el proceso termina.

2.4. ORDENAMIENTO POR MEZCLA (MERGE SORT)

El algoritmo de Ordenamiento por Mezcla ordena recursivamente un arreglo arr de tamaño n


así:
1. Si el tamaño del arreglo es menor o igual que 1, entonces no se realiza ninguna operación.
2. De lo contrario, si el tamaño del arreglo es mayor que 1:
2.1. Se ordena recursivamente la primera mitad del arreglo, cuyo tamaño es n/2.
2.2. Se ordena recursivamente la segunda mitad del arreglo, cuyo tamaño es n/2.
2.3. Se mezclan ambas mitades ordenadas para producir un arreglo ordenado de tamaño n.

ESTRUCTURAS DE DATOS 3
2.5. ORDENAMIENTO RÁPIDO (QUICK SORT)

El algoritmo de Ordenamiento Rápido ordena recursivamente un arreglo arr de tamaño n así:


1. Si el tamaño del arreglo es menor o igual que 1, entonces no se realiza ninguna operación.
2. De lo contrario, si el tamaño del arreglo es mayor que 1:
2.1. Se escoge un valor arbitrario del arreglo (en particular, puede ser el elemento ubicado
en la mitad). A este valor se le llama pivote.
2.2. Se procesa el arreglo de tal forma que al principio queden los valores menores que el
pivote y al final queden los valores mayores o iguales que el pivote. A este paso se le llama
partición.
2.3. Se ordena recursivamente la parte del principio del arreglo, donde quedaron los
elementos menores que el pivote.
2.4. Se ordena recursivamente la parte del final del arreglo, donde quedaron los elementos
mayores que el pivote. La zona con valores iguales al pivote se deja intacta.

3. EL ANIMADOR DE ALGORITMOS DE ORDENAMIENTO


ALPHERATZ (VERSIÓN 6.0)
Instalador y manual de usuario de Alpheratz 6.0: Alpheratz6.zip.

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.

Gráfica 3: Ventana principal de Alpheratz 6.0.

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).

PARA TENER EN CUENTA


Es importante tener nociones básicas sobre orden para poder estudiar con comodidad el tema de
árboles binarios ordenados, de conjuntos y de asociaciones llave-valor.

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 1: Algunas clases en Java que implementan la interfaz Comparable.


Clase Descripción Relación natural de orden
Integer
Orden numérico sobre los números
Valores enteros entre y .
enteros.
Orden numérico sobre los números
Long Valores enteros entre y .
enteros.
Double Valores flotantes de precisión doble. Orden numérico sobre los números reales.
Caracteres codificados en el estándar
Character Orden según el estándar Unicode.
Unicode.
String Cadenas de texto. Orden lexicográfico (orden del diccionario).
BigInteger
Números enteros de precisión Orden numérico sobre los números
arbitraria. enteros.
BigDecimal
Números flotantes de precisión
Orden numérico sobre los números reales.
arbitraria.
Date Fechas. Orden cronológico.

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 3: Declaración de la clase Persona, que representa personas mutuamente comparables.


public class Persona implements Comparable<Persona> {
private String nombres; // Nombres de la persona.
private String apellidos; // Apellidos de la persona.
private Long documento; // Documento de identificación de la persona.
// Constructor de la clase Persona.
public Persona(String pNombres, String pApellidos, Long pDocumento) {
nombres=pNombres;
apellidos=pApellidos;
documento=pDocumento;
}
// Método que determina si dos personas son iguales.
public boolean equals(Object pObject) {
return documento.equals(((Persona)pObject).documento);
}
// Relación de orden entre personas, que compara los documentos de identificación.
public int compareTo(Persona pPersona) {
return documento.compareTo(pPersona.documento);
}
// Método para representar una persona en formato texto.
public String toString() {
return "("+documento+") "+nombres+" "+apellidos;
}
}

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.

Formalmente, siendo c un comparador de elementos de tipo T (es decir, un objeto de tipo


Comparator<T>), y siendo f y g dos objetos de tipo T, la expresión c.compare(f,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.

Cada comparador debe declararse en una clase por separado.

Tabla 6: Declaración de un comparador de ejemplo.


import java.util.*;
// Comparador de números enteros que implementa el orden numérico de mayor a menor
public class ComparadorEnterosReves implements Comparator<Integer> {
public int compare(Integer f, Integer g) {
if (f<g) { // Si f es menor que g
return +1; // Retornar un número positivo
}
else if (f==g) { // Si f es igual a g
return 0; // Retornar cero
}
else { // Si f es mayor que g
return -1; // Retornar un número negativo
}
}
}

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.

PARA TENER EN CUENTA


Para trabajar sobre árboles binarios ordenados necesitamos saber cómo definir relaciones de orden.
Es importante ordenar nuestros datos para poder administrarlos con eficiencia, sin importar de qué
tipo sean: números enteros, números flotantes, cadenas de texto, fechas, personas, etc.
Si desea profundizar sobre el tema, se recomienda leer el artículo Object Ordering del tutorial de Java:
http://java.sun.com/docs/books/tutorial/collections/interfaces/order.html

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

izq : subárbol izquierdo

izq der der : subárbol derecho

es un árbol binario ordenado si se cumplen las siguientes condiciones:


El valor val es mayor que el valor de todos los nodos del subárbol izq.
El valor val es menor que el valor de todos los nodos del subárbol der.
Tanto el subárbol izq como el subárbol der son árboles binarios ordenados.

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.

Tabla 1: Algunos árboles ordenados y no ordenados.


Ejemplo Recorrido en inorden ¿El árbol es ordenado?
SI

SI

ESTRUCTURAS DE DATOS 2
NO

2. UN VISTAZO A LA INTERFAZ SET<E>

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. La interfaz Set<E> del paquete java.util representa un
conjunto de elementos de tipo E †.

Tabla 2: Algunos métodos sin implementación ofrecidos por la interfaz Set<E>.


Método Descripción
boolean isEmpty() Retorna true si y sólo si el conjunto es vacío.
int size() Retorna el número de elementos del conjunto.
void clear() Elimina todos los elementos del conjunto.
boolean contains(Object obj) Retorna true si y sólo si obj pertenece al conjunto.
boolean add(E element)
Agrega el valor element al conjunto. Retorna true si y sólo si el
conjunto no contenía el valor dado.
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.

3. IMPLEMENTACIÓN
Recurso como proyecto en Eclipse: ArbolesBinariosOrdenados.zip.

La clase VEDArbinOrdenado<E> del recurso ArbolesBinariosOrdenados.zip representa un árbol


ordenado de elementos de tipo E, suponiendo que los objetos de la clase E son mutuamente
comparables.

Código 3: Declaración de la clase VEDArbinOrdenado<E>.


public class VEDArbinOrdenado<E> extends VEDArbin<E> implements Set<E>

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.

Código 4: Métodos constructores de la clase VEDArbinOrdenado<E>.


public VEDArbinOrdenado() {
super(); // Invoca al constructor de la superclase, que crea un árbol vacío.
}
public VEDArbinOrdenado(Collection<? extends E> coll) {
super(); // Invoca al constructor de la superclase, que crea un árbol vacío.
addAll(coll); // Insertar todos los elementos de coll en este árbol ordenado.
}

La instrucción super() llama al constructor de la superclase VEDArbin<E>, que crea un árbol


vacío.

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 8: Método para informar si el árbol binario ordenado es vacío o no.


public boolean isEmpty() {
return esVacio(); // Llamar al método esVacio() de la superclase.
}

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.
}

3.4. ALGORITMO DE BÚSQUEDA

Para determinar si un valor x está presente en el árbol binario ordenado:


1. Si el árbol es vacío, retorne false porque el árbol vacío no tiene elementos.
2. Si el árbol no es vacío:
2.1. Compare el valor x con el valor de la raíz del árbol.
2.2. Si el valor x es igual a la raíz del árbol: retorne true porque ya se encontró el valor
buscado.
2.3. Si el valor x es menor que la raíz del árbol: busque recursivamente el valor x en el
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: busque recursivamente el valor x en el
subárbol derecho (sabemos que el valor no puede estar a la izquierda porque el árbol es
ordenado).

Código 10: Método para verificar si un valor aparece en el árbol ordenado.


public boolean contains(Object obj) {
if (esVacio()) { // Si este árbol es vacío:
return false; // Informar que el valor buscado no está en el árbol.
}
else { // Si este árbol no es vacío:
// Tratar el objeto obj como una instancia de la clase Comparable<E>:
Comparable<E> x=(Comparable<E>)obj;
// Comparar el valor a buscar con la raíz del árbol:
int c=x.compareTo(val);
if (c==0) { // Si el valor a buscar es igual a la raíz del árbol:

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);
}
}
}

La complejidad temporal es donde es la altura del árbol porque en el peor de los


casos la recursión baja nivel por nivel hasta llegar a un subárbol vacío de una hoja
perteneciente al último nivel del árbol.

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
.

3.5. ALGORITMO DE INSERCIÓN

Para insertar un valor x en un árbol binario ordenado:


1. Si el árbol es vacío, conviértalo en una hoja con raíz x, con subárbol izquierdo vacío y con
subárbol derecho vacío.
2. Si el árbol no es vacío:
2.1. Compare el valor x con el valor de la raíz del árbol.
2.2. Si el valor x es igual a la raíz del árbol: aborte el proceso porque x ya está presente en el
árbol (no se podría insertar debido a que un árbol ordenado no debe tener elementos
repetidos).

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).

Código 13: Método para insertar un valor en el árbol ordenado.


public boolean add(E element) {
if (esVacio()) { // Si este árbol es vacío:
// Convertir este árbol vacío en una hoja con raíz element:
val=element; // Poner como raíz el elemento insertado.
izq=new VEDArbinOrdenado<E>(); // Poner a la izquierda un árbol ordenado vacío.
der=new VEDArbinOrdenado<E>(); // Poner a la derecha un árbol ordenado vacío.
return true; // Informar que el valor a insertar no estaba en el árbol.
}
else { // Si este árbol no es vacío:
// Tratar el objeto element como una instancia de la clase Comparable<E>:
Comparable<E> x=(Comparable<E>)element;
// Comparar el valor a insertar con la raíz del árbol:
int c=x.compareTo(val);
if (c==0) { // Si el valor a insertar es igual a la raíz del árbol:
return false; // Informar que el valor a insertar ya estaba en el árbol.
}
else if (c<0) { // Si el valor a insertar es menor que la raíz del árbol:
// Insertar recursivamente el valor x a la izquierda del árbol:
return getIzq().add(element);
}
else { // Si el valor a insertar es mayor que la raíz del árbol:
// Insertar recursivamente el valor x a la derecha del árbol:
return getDer().add(element);
}
}
}

La complejidad temporal es donde es la altura del árbol.

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

3.6. ALGORITMO DE ELIMINACIÓN

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.
}
}
}

Para eliminar un valor x de un árbol binario ordenado:


1. Si el árbol es vacío, no efectúe ninguna operación porque el árbol vacío no tiene
elementos.
2. Si el árbol no es vacío:
2.1. Compare el valor x con el valor de la raíz del árbol.
2.2. Si el valor x es igual a la raíz del árbol:
2.2.1. Si los dos subárboles son vacíos (es decir, si es una hoja): convierta el árbol en el árbol
vacío.
x transformar en

2.2.2. Si el subárbol izquierdo es vacío: convierta el árbol en el subárbol derecho.


x
transformar en
der
der

2.2.3. Si el subárbol derecho es vacío: convierta el árbol en el subárbol izquierdo.


x
transformar en
izq
izq

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).

Código 17: Método para eliminar un valor del árbol ordenado.


public boolean remove(Object obj) {
if (esVacio()) { // Si este árbol es vacío:
return false; // Informar que el árbol ordenado no contenía el elemento dado.
}
else { // Si este árbol no es vacío:
// Tratar el objeto obj como una instancia de la clase Comparable<E>:
Comparable<E> x=(Comparable<E>)obj;
// Comparar el valor a eliminar con la raíz del árbol:
int c=x.compareTo(val);
if (c==0) { // Si el valor a eliminar es igual a la raíz del árbol:
if (esHoja()) { // Si el nodo es una hoja:
// Convertir este árbol ordenado en el árbol vacío:
val=null;
izq=null;
der=null;
}
else if (izq.esVacio()) { // Si el subárbol izquierdo es vacío:
// Convertir este árbol ordenado en el subárbol derecho:
VEDArbinOrdenado<E> temporal=getDer();
val=temporal.val;
izq=temporal.izq;
der=temporal.der;
}
else if (der.esVacio()) { // Si el subárbol derecho es vacío:
// Convertir este árbol ordenado en el subárbol izquierdo:
VEDArbinOrdenado<E> temporal=getIzq();
val=temporal.val;
izq=temporal.izq;
der=temporal.der;
}
else { // Si ambos subárboles no son vacíos:
// Hallar el mayor elemento del subárbol izquierdo:
E m=getIzq().max();
// Poner como raíz el mayor elemento del subárbol izquierdo:
val=m;
// Eliminar recursivamente el mayor del subárbol izquierdo:
getIzq().remove(m);
}
return true; // Informar que el árbol ordenado sí contenía el elemento dado.
}
else if (c<0) { // Si el valor a eliminar es menor que la raíz del árbol:
// Eliminar recursivamente el valor x a la izquierda del árbol:
return getIzq().remove(obj);

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);
}
}
}

La complejidad temporal es donde es la altura del árbol (¡convénzase Usted mismo!).

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
.

PARA TENER EN CUENTA


Los árboles binarios ordenados se pueden degenerar fácilmente, afectando la complejidad temporal
de las operaciones básicas. En la próxima lectura estudiaremos cómo controlar la altura de los árboles
binarios ordenados para que todas nuestras operaciones básicas tengan complejidad temporal
, en todo caso.

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

En la lectura anterior concluimos que las operaciones básicas de búsqueda, inserción,


eliminación y modificación sobre los árboles binarios ordenados tienen complejidad
temporal , donde es la altura del árbol. Además sabemos que, 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 .

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.

2. ÁRBOLES AVL (ÁRBOLES BALANCEADOS POR ALTURA)

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

izq : subárbol izquierdo

izq der der : subárbol derecho

es un árbol AVL si se cumplen las siguientes condiciones:


Es un árbol binario ordenado.
La altura de sus dos subárboles no difiere en más de uno:


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.

El factor de balanceo por altura de un nodo es la altura de su subárbol derecho menos la


altura de su subárbol izquierdo:
.

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.

Tabla 2: Ejemplo de árboles AVL y no AVL.


Ejemplo ¿Es AVL?
Sí, porque es el árbol vacío.

Sí, porque está ordenado y todos sus nodos tienen factores de


balanceo por altura que están entre y .

No, porque hay nodos cuyos factores de balanceo por altura no


están entre y .

Todo árbol AVL cumple que donde es el peso y es la altura del árbol.

ESTRUCTURAS DE DATOS 3
2.2. OPERACIONES BÁSICAS

2.2.1. ALGORITMO DE BÚSQUEDA

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.

2.2.2. ALGORITMO DE INSERCIÓN

Para insertar un valor x en un árbol AVL:


1. Inserte el valor x en el árbol siguiendo el proceso de inserción descrito para árboles
binarios ordenados comunes y corrientes.
2. Si después de la inserción el árbol sigue cumpliendo la condición de ser AVL, entonces no
realice ninguna operación.
3. De lo contrario, si después de la inserción el árbol no cumple la condición de ser AVL,
entonces:
3.1. Identifique el nodo de mayor nivel que no tiene un factor de balanceo por altura entre
y (es decir, identifique el nodo de más abajo que se desbalanceó por altura).
3.2. Corrija el árbol mediante operaciones denominadas rotaciones, que son capaces de
rebalancearlo para que vuelva a adquirir la condición de ser AVL, bajo cuatro escenarios
posibles:
3.2.1. Caso Izq-Izq (Izquierda-Izquierda):
e -1 e -2
f 0 insertar x f -1 La inserción se efectúa en el
c c
 subárbol a, aumentando su altura

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

Tabla 3: Ejemplo que ilustra cómo insertar valores en un árbol AVL.


Evolución del árbol durante la operación Caso Operación
Insertar
2.
el valor
Insertar
2.
el valor

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.

2.2.3. ALGORITMO DE ELIMINACIÓN

Para eliminar un valor x de un árbol AVL:


1. Elimine el valor x del árbol siguiendo el proceso de eliminación descrito para árboles
binarios ordenados comunes y corrientes.
2. Para todo nodo que sea ancestro del nodo eliminado, comenzando desde tal nodo y
terminando en el nodo correspondiente a la raíz del árbol:
2.1.1. Si el nodo tiene un factor de balanceo por altura entre y : deje el nodo intacto.
2.1.2. Si el nodo no tiene un factor de balanceo por altura entre y : corrija el nodo
mediante las rotaciones explicadas en el algoritmo de inserción, identificando cuál de sus
subárboles causó el desbalanceo.

Es posible que después de una eliminación se requiera corregir con rotaciones más de un
nodo.

Tabla 4: Ejemplo que ilustra cómo eliminar valores de un árbol AVL.


Evolución del árbol durante la operación Caso Operación

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

Gracias a que en todo árbol AVL se cumple que , entonces la complejidad


temporal del algoritmo de eliminación sería en todo caso.

3. ÁRBOLES PERFECTAMENTE BALANCEADOS (BALANCEADOS


POR PESO)

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:

Los algoritmos de inserción y de eliminación en árboles perfectamente balanceados no serán


trabajados en esta lectura debido a la dificultad que representan.

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).

Tabla 5: Ejemplos de árboles Roji-negros y no Roji-negros.


Ejemplo ¿Es Roji-negro?
Sí, porque es el árbol vacío y está coloreado de negro.
No, porque no todos los caminos que van desde el hasta
un subárbol vacío pasan por la misma cantidad de nodos
negros: por ejemplo, el camino pasa por dos
nodos negros, y los caminos , ,
y pasan por tres nodos
negros cada uno (incluyendo el vacío).

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 .

El algoritmo de búsqueda es el mismo que ya estudiamos para árboles binarios ordenados, y


tiene complejidad temporal porque sabemos que nunca visita más de nodos y
que en todo árbol Roji-negro se cumple que . En el enlace
http://en.wikipedia.org/wiki/Red-Black_tree de la Wikipedia y en el libro Introduction to
Algorithms de Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, y Clifford Stein se
tratan con detalle procesos de inserción y eliminación de valores sobre árboles Roji-negros,
que tienen complejidad temporal y que garantizan que el árbol no pierda su
condición de ser Roji-negro.

Más adelante aprenderemos a manipular las clases TreeSet<E> y TreeMap<K,V> de la librería


estándar de Java, que implementan los árboles Roji-negros.

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
( ).

PARA TENER EN CUENTA


Más adelante estudiaremos las clases TreeSet<E> y TreeMap<K,V>, que implementan los árboles
Roji-negros para representar conjuntos y asociaciones llave-valor, respectivamente.

ESTRUCTURAS DE DATOS 10
ESTRUCTURAS DE DATOS
UNIDAD TRES - SEMANA SEIS
EJERCICIOS PROPUESTOS *

1. OPERACIONES SOBRE ÁRBOLES ORDENADOS

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:

Debe mostrar paso a paso su procedimiento, a través de dibujos.

2. IMPLEMENTACIONES ITERATIVAS DE ALGUNOS PROCESOS SOBRE ÁRBOLES


ORDENADOS

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. ALGORÍTMICA SOBRE ÁRBOLES ORDENADOS

Implemente los siguientes métodos de la clase VEDArbinOrdenado<E>, calculando la


complejidad temporal de cada uno de éstos.

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?

4.2. Estudie el código fuente de la clase VEDArbinOrdenado<E> para saber cómo se


implementan todos los métodos de la interfaz Set<E> a través de árboles ordenados. Escriba
pseudocódigos claros de todos los métodos de la clase VEDArbinOrdenado<E>.

5. OPERACIONES SOBRE ÁRBOLES AVL

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. Teoría e implementación de estructuras de datos recurrentes.

1.1. Árboles binarios.

1.1.1. Definiciones y conceptos básicos.

1.1.2. Recorridos (preorden, inorden, postorden, por niveles).

1.1.3. Reconstrucción de árboles binarios a partir de sus recorridos.

NÚCLEO TEMÁTICO: TABLAS DE HASHING.

1. Motivación.

2. Definiciones y conceptos básicos.

3. Funciones de Hashing.

4. Implementación.

5. Conjuntos, Bolsas y Asociaciones llave-valor.

5.1. Representación de conjuntos con árboles binarios ordenados y con Tablas


de Hashing.

5.1.1. Estudio de la interfaz Set<E> y de las clases TreeSet<E> y HashSet<E> de


Java.

5.2. Representación de asociaciones llave-valor y de bolsas con árboles binarios


ordenados y con Tablas de Hashing.

5.2.1. Estudio de la interfaz Map<K,V> y de las clases TreeMap<K,V>,


HashMap<K,V> y Hashtable<K,V> de Java.

1 [ POLITÉCNICO GRANCOLOMBIANO ]
NÚCLEO TEMÁTICO: GRAFOS

1. Conceptos básicos.

2. Recorridos en grafos (por profundidad: Depth-first search (DFS), por anchura:


Breadth-first search (BFS)).

3. Implementaciones.

4. Problemas típicos en grafos.

4.1. Ruta más corta (Shortest Path).

4.1.1. Algoritmo de Dijkstra.

4.2. Árbol de expansión minimal (Minimum Spanning Tree : MST).

4.2.1. Algoritmo de Prim.

4.2.2. Algoritmo de Kruskal.

4.3. Agente viajero (Traveling Salesman Problem : TSP).

Competencias Semanales de Aprendizaje:

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.

3- Utilizar sistemas de Tecnología, Información y Telecomunicaciones que permitan la recreación


de un modelo, que permita resolver problemas en el mundo real.
4- Aplicar diferentes metodologías de Ingeniería de Software para la construcción de Sistemas de
Tecnología de Información y Telecomunicaciones.
5- 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.
6- Evaluar Sistemas de Tecnologías de Información, tanto software como hardware, a partir de
diferentes criterios.
7- 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.
8- 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.

[ 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.

NÚCLEO TEMÁTIC0: ÁRBOLES


2. Teoría e implementación de estructuras de datos recurrentes.
2.1. Árboles binarios.
2.1.1. Definiciones y conceptos básicos.
2.1.2. Recorridos (preorden, inorden, postorden, por niveles).
2.1.3. Reconstrucción de árboles binarios a partir de sus recorridos.
NÚCLEO TEMÁTICO: TABLAS DE HASHING.
1. Motivación.
2. Definiciones y conceptos básicos.
3. Funciones de Hashing.
4. Implementación.
5. Conjuntos, Bolsas y Asociaciones llave-valor.
5.1. Representación de conjuntos con árboles binarios ordenados y con Tablas de Hashing.
5.1.1. Estudio de la interfaz Set<E> y de las clases TreeSet<E> y HashSet<E> de Java.
5.2. Representación de asociaciones llave-valor y de bolsas con árboles binarios ordenados y con Tablas de
Hashing.
5.2.1. Estudio de la interfaz Map<K,V> y de las clases TreeMap<K,V>, HashMap<K,V> y Hashtable<K,V> de Java.

ACTIVIDAD SEMANA INSTRUCTIVO


Lectura 7-1 - Árboles Siete Leer y comprender los conceptos expuestos en las lecturas.
enearios.
Lectura 7-2 - Siete Leer y comprender los conceptos expuestos en las lecturas.
Aplicaciones de los
árboles enearios.
Lectura 7-3 - Tablas Siete Leer y comprender los conceptos expuestos en las lecturas.
de Hashing.
Lectura 7-4 - Siete Leer y comprender los conceptos expuestos en las lecturas.
Conjuntos,
Asociaciones llave-
valor y Bolsas.
Video diapositivas 1 - Siete Leer y comprender los conceptos expuestos en los videos
Reconstrucción de diapositivas.
Quadtrees.
Ejercicios propuestos. Siete Desarrollar de forma individual algunos de los ejercicios propuestos
para la semana.
Proyecto. Siete Finalizar desarrollo del proyecto y enviar.

3 [ POLITÉCNICO GRANCOLOMBIANO ]
 SEMANA 8
TEMA (S): NÚCLEO TEMÁTICO: ÁRBOLES

1. Teoría e implementación de estructuras de datos recurrentes.

1.1. Árboles binarios.

1.1.1. Definiciones y conceptos básicos.

1.1.2. Recorridos (preorden, inorden, postorden, por niveles).

1.1.3. Reconstrucción de árboles binarios a partir de sus recorridos.

NÚCLEO TEMÁTICO: TABLAS DE HASHING.

1. Motivación.

2. Definiciones y conceptos básicos.

3. Funciones de Hashing.

4. Implementación.

5. Conjuntos, Bolsas y Asociaciones llave-valor.

5.1. Representación de conjuntos con árboles binarios ordenados y con Tablas


de Hashing.

5.1.1. Estudio de la interfaz Set<E> y de las clases TreeSet<E> y HashSet<E> de


Java.

5.2. Representación de asociaciones llave-valor y de bolsas con árboles binarios


ordenados y con Tablas de Hashing.

5.2.1. Estudio de la interfaz Map<K,V> y de las clases TreeMap<K,V>,


HashMap<K,V> y Hashtable<K,V> de Java.

[ ESTRUCTURA DE DATOS ] 4
NÚCLEO TEMÁTICO: GRAFOS

1. Conceptos básicos.

2. Recorridos en grafos (por profundidad: Depth-first search (DFS), por anchura:


Breadth-first search (BFS)).

3. Implementaciones.

4. Problemas típicos en grafos.

4.1. Ruta más corta (Shortest Path).

4.1.1. Algoritmo de Dijkstra.

4.2. Árbol de expansión minimal (Minimum Spanning Tree : MST).

4.2.1. Algoritmo de Prim.

4.2.2. Algoritmo de Kruskal.

4.3. Agente viajero (Traveling Salesman Problem : TSP).

Competencias Semanales de Aprendizaje:

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.

3- Utilizar sistemas de Tecnología, Información y Telecomunicaciones que permitan la recreación


de un modelo, que permita resolver problemas en el mundo real.
4- Aplicar diferentes metodologías de Ingeniería de Software para la construcción de Sistemas de
Tecnología de Información y Telecomunicaciones.
5- 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.
6- Evaluar Sistemas de Tecnologías de Información, tanto software como hardware, a partir de
diferentes criterios.
7- 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.
8- 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.

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.

NÚCLEO TEMÁTICO: GRAFOS


1. Conceptos básicos.
2. Recorridos en grafos (por profundidad: Depth-first search (DFS), por anchura: Breadth-first search (BFS)).
3. Implementaciones.
4. Problemas típicos en grafos.
4.1. Ruta más corta (Shortest Path).
4.1.1. Algoritmo de Dijkstra.
4.2. Árbol de expansión minimal (Minimum Spanning Tree : MST).
4.2.1. Algoritmo de Prim.
4.2.2. Algoritmo de Kruskal.
4.3. Agente viajero (Traveling Salesman Problem : TSP).

ACTIVIDAD SEMANA INSTRUCTIVO


Lectura 8-1 - Ocho Leer y comprender los conceptos expuestos en las lecturas.
Conceptos básicos
sobre Grafos.
Lectura 8-2 - Ocho Leer y comprender los conceptos expuestos en las lecturas.
Implementación de
Grafos.
Lectura 8-3 - Ocho Leer y comprender los conceptos expuestos en las lecturas.
Problemas clásicos
sobre Grafos.
Video diapositivas 1 - Ocho Leer y comprender los conceptos expuestos en los videos
Algoritmo de Dijkstra. diapositivas.
Ejercicios propuestos. Ocho Desarrollar de forma individual algunos de los ejercicios propuestos
para la semana.
Repaso. Ocho Autoestudio de los contenidos del módulo. Realización de ejercicios
como preparación para la evaluación final.
Evaluación final. Ocho Solucionar y enviar.
Teleconferencias Ocho Asistir a la teleconferencia de la semana.
Recursos adicionales. Ocho Revisar cuidadosamente el material en Java organizado como
proyectos en Eclipse, con el fin de afianzar la práctica.
Video resumen. Ocho Ver el resumen de la unidad cuatro.

[ 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.

Tabla 1: Árboles enearios de ejemplo.


Árbol eneario Raíz Subárboles
no
no tiene
tiene
,

, , ,

no tiene

, , ,

, , , ,

Un nodo se define como la raíz de un árbol eneario no vacío. En particular, , , , ,


, , , ,y son los nodos que conforman los árboles del ejemplo anterior. El árbol
vacío nunca puede ser considerado como nodo porque no es raíz de un árbol eneario no
vacío.

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.1. PESO Y ALTURA

Dos propiedades importantes de un árbol eneario son su peso y su altura. El peso ( ) de un


árbol eneario es el número de nodos que tiene, y se puede calcular como uno más el peso de
todos sus subárboles. Por otro lado, la altura ( ) de un árbol eneario 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. De
la misma forma que se estableció para árboles binarios, tanto el peso como la altura del árbol
vacío se definen como cero.

Tabla 2: Peso y altura de algunos árboles enearios.


Árbol eneario Peso ( ) Altura ( )

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 .

PARA TENER EN CUENTA


En la siguiente lectura estudiaremos aplicaciones interesantes de los árboles enearios.

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

Un Quadtree es un árbol eneario capaz de representar matrices de valores booleanos de


tamaño .

Gráfica 1: Una matriz de valores booleanos de tamaño 8 , que es de la forma con .


false true true true false false false false
true false true true false false false false
true true false true false false false false
true true true true false false false false
false false false true true true true false
true true true true true true false true
true false false false false false false true
true true true true true true false true

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.

Cada nodo de un Quadtree es capaz de almacenar información de una porción de imagen.


Detalladamente, para representar una imagen de tamaño con un Quadtree:

Si la imagen es completamente blanca, entonces colocamos un nodo blanco.


Gráfica 3: Representación de una imagen blanca como Quadtree.
Se representa
como
Si la imagen es completamente negra, entonces colocamos un nodo negro.
Gráfica 4: Representación de una imagen negra como Quadtree.
Se representa
como
Si la imagen no es ni blanca ni negra, entonces:
1. Creamos un nodo gris.

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.

Tabla 6: Algunos Quadtrees de ejemplo.


Imagen Representación como Quadtree

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

Un Trie (pronunciado trai) es un árbol eneario capaz de representar un conjunto de palabras


cuyas letras se toman de cierto alfabeto. Por simplicidad, supondremos que el alfabeto
únicamente consta de las veintiséis letras mayúsculas latinas (‘A’, ‘B’, ‘C’, …, ‘Z’) sin incluir
caracteres tildados (‘Á’, …, ‘Ú’), la eñe (‘Ñ’), ni la diéresis (‘Ü’).

Gráfica 8: Trie que almacena el conjunto de palabras


FARO, FOCA, LUNA, LUZ, MAR, MAREA, MAREO, MARTA, MARTE, SOFA y SOL.
Cada camino que va desde la raíz del árbol hasta un nodo coloreado de azul representa una palabra del conjunto.

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.

Un árbol B de orden es un árbol eneario que cumple las siguientes propiedades:

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.

No, porque está


desordenado.

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.

PARA TENER EN CUENTA


Más adelante estudiaremos las clases TreeSet<E> y TreeMap<K,V>, que implementan los árboles
Roji-negros para representar conjuntos y asociaciones llave-valor, respectivamente.

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?

Considere el siguiente conjunto , que contiene treinta elementos:

Sea y la función que recibe un valor entero y devuelve como


resultado el valor absoluto de módulo . Aprecie que sería el último dígito de si lo
escribimos en base diez.

Tabla 1: Evaluación de la función sobre los treinta valores del conjunto .

Imagínese listas identificadas con índices enteros de a (es decir, de a ). Ahora,


para cada elemento del conjunto , insértelo en la lista cuyo número sea el descrito por la
funció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

Cambiemos el valor de a sin alterar la definición de la función , a ver qué


sucede. En este nuevo escenario tenemos que computa el valor absoluto de módulo

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.

Tabla 4: Algunas funciones de Hashing sobre el conjunto de los números enteros ( ).


Función de Hashing Ejemplos

suma de los dígitos de


en base diez

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

(la longitud de la cadena


de texto )

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()

2.2. TABLAS DE HASHING

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 .

Dirección Dado un valor de tipo , la dirección de en la Tabla de Hashing se define como el


resultado de la expresión donde es la función de Hashing y es la
capacidad de la tabla. La dirección de un elemento indica cuál fila de la Tabla de
Hashing se le asocia, y siempre es un número natural entre y . La dirección
corresponde a la primera fila de la Tabla de Hashing, la dirección a la segunda, …, y la
dirección a la última.
Colisión Dos elementos y de tipo colisionan si y sólo si tienen la misma dirección en la
Tabla de Hashing, lo que sucede cuando es igual a . Una colisión
sucede cuando se intenta insertar un elemento en una fila que ya tiene datos.

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).

Aprecie las siguientes relaciones de proporcionalidad que se pueden deducir de la fórmula


:
Entre más filas (capacidad ) tenga la Tabla de Hashing menor será el factor de carga ( ) y
menor será la probabilidad de que sucedan colisiones.
Entre más datos (tamaño ) tenga la Tabla de Hashing mayor será el factor de carga ( ) y
mayor será la probabilidad de que sucedan colisiones.

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.

2.3. ORIGEN DEL TÉRMINO HASH

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.

Tabla 12: Ejemplos sobre las técnicas de la tabla anterior,


suponiendo que y que .
Técnica Ejemplo de aplicación de la técnica
Funciones de Hallando el valor módulo obtenemos la dirección :
división
Funciones de Quitando alternadamente dígitos del valor obtenemos la dirección
truncamiento :

Funciones de Dividiendo el valor en pedazos de cuatro dígitos que se suman,


plegamiento obtenemos la dirección :

3. OPERACIONES BÁSICAS

3.1. ALGORITMO DE BÚSQUEDA

Para determinar si un valor x está presente en la Tabla de Hashing:


1. Sea r el resultado de la función de Hashing evaluada sobre el objeto x.
2. Sea dir la dirección de x, calculada como el valor absoluto de r módulo la capacidad de la
tabla (nótese que se está usando la función de división para picar el número r).
3. Si x está presente en la fila ubicada en la posición dir de la Tabla de Hashing, retorne true.
De lo contrario, retorne false.

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.

3.2. ALGORITMO DE INSERCIÓN

Para insertar un valor x en la Tabla de Hashing:


1. Sea r el resultado de la función de Hashing evaluada sobre el objeto x.
2. Sea dir la dirección de x, calculada como el valor absoluto de r módulo la capacidad de la
tabla (nótese que se está usando la función de división para picar el número r).
3. Si x no está presente en la fila ubicada en la posición dir de la Tabla de Hashing, entonces x
se inserta precisamente en esta fila. De lo contrario, no se altera la tabla.

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.

3.3. ALGORITMO DE ELIMINACIÓN

Para eliminar un valor x de la Tabla de Hashing:


1. Sea r el resultado de la función de Hashing evaluada sobre el objeto x.
2. Sea dir la dirección de x, calculada como el valor absoluto de r módulo la capacidad de la
tabla (nótese que se está usando la función de división para picar el número r).
3. Si x está presente en la fila ubicada en la posición dir de la Tabla de Hashing, entonces x se
elimina precisamente de esta fila. De lo contrario, no se altera la tabla.

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 .

PARA TENER EN CUENTA


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.

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

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.

1.2. LA INTERFAZ SET<E>

La interfaz Set<E> del paquete java.util representa un conjunto de elementos de tipo E †. El


tipo E es genérico: puede ser cualquier clase en Java. Por ejemplo, Set<Integer> representa
un conjunto de números enteros, Set<Double> representa un conjunto de números flotantes,
Set<String> representa un conjunto de cadenas de texto, etc.

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.

1.3. IMPLEMENTACIONES SUMINISTRADAS POR JAVA PARA LA INTERFAZ


SET<E>
Tabla 2: Estructuras de datos que hemos estudiado para representar conjuntos.
Estructura de datos Breve definición
Una lista ordenada es una lista sin elementos repetidos que se almacenan
Listas ordenadas
de menor a mayor según cierto criterio de ordenamiento.
Un árbol binario ordenado es un árbol binario cuyo recorrido en inorden
Árboles ordenados
está ordenado según cierto criterio de ordenamiento.
Árboles AVL Un árbol AVL es un árbol binario ordenado que está balanceado por altura.

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í ‡.

Tabla 5: Implementaciones clásicas provistas por Java para la interfaz Set<E>.


Paquete Clase Estructura de datos ¿Clase sincronizada?
java.util TreeSet<E> Árbol Roji-negro No
java.util HashSet<E> Tabla de Hashing No

Tabla 6: Evolución de un conjunto tras una secuencia de operaciones de ejemplo.


Estado del conjunto Operación Descripción
Set<Integer> conjunto
=new TreeSet<Integer>(); Crear un conjunto vacío.
conjunto.add(3); Insertar el valor en el conjunto.
conjunto.add(5); Insertar el valor en el conjunto.
conjunto.add(3); Insertar el valor en el conjunto.
conjunto.add(8); Insertar el valor en el conjunto.
conjunto.add(5); Insertar el valor en el conjunto.
conjunto.add(2); Insertar el valor en el conjunto.
conjunto.add(9); Insertar el valor en el conjunto.
conjunto.remove(3); Eliminar el valor del conjunto.
conjunto.remove(7); Eliminar el valor del conjunto.
conjunto.add(7); Insertar el valor en el conjunto.
conjunto.remove(2); Eliminar el valor del conjunto.
conjunto.add(9); Insertar el valor en el conjunto.

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 7: Operaciones básicas soportadas por una asociación llave-valor.


Operación Descripción
Búsqueda Determina el valor asociado a una llave.
Inserción Asocia a una llave un valor específico.
Eliminación Elimina la asociación correspondiente a una llave.

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.

2.3. IMPLEMENTACIONES SUMINISTRADAS POR JAVA PARA LA INTERFAZ


MAP<K,V>

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.

PARA TENER EN CUENTA


Los árboles Roji-negros y las Tablas de Hashing son estructuras de datos potentes para la
administración eficiente de información en memoria principal. Practique su uso a través de las clases
TreeSet<E>, TreeMap<K,V>, HashSet<E> y HashMap<K,V>.

ESTRUCTURAS DE DATOS 9
ESTRUCTURAS DE DATOS
UNIDAD CUATRO - SEMANA SIETE
EJERCICIOS PROPUESTOS *

1. ALGORITMO DE AHO-CORASICK

Investigue sobre el algoritmo de Aho-Corasick y responda las siguientes preguntas:

1. ¿Qué problema resuelve?


2. ¿De qué manera utiliza los Tries como estructuras de datos?
3. ¿Qué complejidad temporal tiene?

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

Su clase debe extender de VEDArbinEneario y debe proveer un método capaz de evaluar la


expresión y entregar el resultado. Por ejemplo, el resultado de la expresión aritmética
es exactamente .

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

Reconstruya los Quadtrees cuyo recorrido en postorden se da a continuación.


Postorden Quadtree Imagen blanco y negro
BNBBG
BBNBBNGNG
NBBBGBNNNNBGG
BBNNBGBBNGBNBBGNG
BBNNGNBBGNNNBNGBBGNBBBNNGNNGG
BBBNBGNNNBGBGNNBNNBBGBGBNNBBGGNNNBGNBNGBG

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

Un grafo es una estructura de datos conformada por:


1. Un conjunto de nodos.
2. Un conjunto de arcos, que conectan nodos entre sí. El conjunto debe ser subconjunto
de .

Gráfica 1: Grafo de ejemplo con nodos y arcos.

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 .

Formalmente, dado un grafo :


Un nodo es un elemento del conjunto .
Un arco es una pareja donde es el nodo origen y es el nodo destino.
El conjunto de arcos es un subconjunto de , es decir, es un conjunto conformado
por parejas de la forma donde y son valores de tipo .

Tabla 2: Notación gráfica para los arcos.


Notación Notación Notación Descripción
formal compacta gráfica
Hay arco del nodo al nodo , pero no hay arco
del nodo al nodo .
y Hay arco del nodo al nodo , y también hay
arco del nodo al nodo .
y Hay arco del nodo al nodo , y también hay
arco del nodo al nodo .
Hay arco del nodo al nodo , es decir, del
nodo a sí mismo.

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

2.1. GRAFOS DIRIGIDOS Y GRAFOS NO DIRIGIDOS


Tabla 4: Definición de grafo no dirigido y grafo dirigido.
Concepto Definición
Un grafo es un grafo no dirigido si y sólo si para todo par de nodos
Grafo no dirigido y tales que haya un arco desde hacia , se cumple que también hay un arco
desde hacia .
Un grafo es un grafo dirigido si y sólo si existe un par de nodos y
Grafo dirigido
tales que hay un arco desde hacia pero no existe un arco desde hacia .

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

2.2. SUCESORES Y ANTECESORES


Tabla 6: Definición de sucesor y antecesor.
Concepto Definición
Sucesor Un nodo es sucesor de un nodo si y sólo si existe un arco que va desde hacia .
Antecesor Un nodo es antecesor de un nodo si y sólo si existe un arco que va desde hacia .
Adyacente Un nodo es adyacente a un nodo si y sólo si es sucesor y/o antecesor de .

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.

Tabla 7: Ejemplos sobre los conceptos de sucesor y antecesor.


Grafo Ejemplos
Nodo Sucesores Antecesores Adyancentes

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.

Un nodo que sea fuente y sumidero a la vez es considerado un nodo aislado.

Tabla 9: Ejemplos sobre los conceptos de fuente, sumidero y nodo aislado.


Grafo Ejemplos
Fuentes
Sumideros
Nodos
aislados

2.4. GRADO DE UN NODO


Tabla 10: Definición de grado.
Concepto Definición
Grado de salida El grado de salida de un nodo es la cantidad de sucesores que tiene.
Grado de entrada El grado de entrada de un nodo es la cantidad de antecesores que tiene.
En un grafo no dirigido, el grado de un nodo es la cantidad de sucesores que
Grado tiene (recuerde que los antecesores serían los mismos sucesores en un grafo no
dirigido).

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.

Tabla 11: Ejemplos sobre el concepto de grado.


Grafo Ejemplos
Nodo Grado de salida Grado de entrada

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.

Dado un grafo donde la variable representa el número de nodos y la variable


representa el número de arcos, se debe cumplir que . Esto porque un grafo
podría no tener arcos ( ) o podría llegar a tener la máxima cantidad posible de arcos
( ), lo que ocurre cuando se conecta cada nodo con todos los demás, incluido sí mismo.

Gráfica 13: Grafos completos de ejemplo.

, , , , ,

, , , , ,

En un grafo completo con nodos debe haber exactamente arcos si contamos


cada enlace ( ) como dos arcos por separado ( y . Pero, si contabilizamos cada enlace
( ) como un solo arco, un grafo completo con nodos tendría exactamente
arcos. ¿Por qué?

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.

denso denso denso disperso disperso


, , , , ,

2.6. CAMINOS Y CADENAS


Tabla 15: Definición de camino y cadena.
Concepto Definición
Un camino es una lista no vacía de nodos donde cada nodo es
sucesor de su anterior (para todo desde hasta , se cumple que es sucesor
Camino de ). La longitud de un camino es , es decir, la cantidad
de nodos en la lista menos uno. El primer nodo de un camino se llama origen y el
último se llama destino.
Un camino simple es un camino tal que todos los nodos que recorre son
Camino simple
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 (para todo desde hasta , se cumple que es
Cadena
sucesor y/o antecesor de ). La longitud de una cadena es
, es decir, la cantidad de nodos en la lista menos uno.

Reflexione sobre por qué se cumplen los siguientes hechos:


Todo camino es cadena, pero no toda cadena es camino.
En un grafo no dirigido los conceptos de camino y cadena son equivalentes.
Toda cadena es un camino en la clausura simétrica del grafo, y viceversa.

Tabla 16: Ejemplos sobre los conceptos de camino y cadena.


Grafo Ejemplos
Longitud ¿Es ¿Es camino ¿Es
Lista
(tamaño-1) camino? simple? cadena?
SI SI SI
SI NO SI
NO NO SI
NO NO SI
NO NO NO
SI NO SI

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

El grafo es acíclico porque no tiene ciclos.


En particular, el grafo es un DAG.

El grafo es cíclico porque tiene ciclos. Algunos ciclos simples son ,


, , , , ,
y . Algunos ciclos no simples son ,
, y . El grafo no es un
DAG porque no es dirigido.
El grafo es cíclico porque tiene ciclos. Algunos ciclos simples son ,
, , , y . Algunos ciclos no simples
son , , , , ,
, y . El grafo no es un DAG
porque no es acíclico.

2.8. DESCENDIENTES Y ANCESTROS


Tabla 19: Definición de descendiente y ancestro.
Concepto Definición
Un nodo es descendiente de un nodo si y sólo si existe un camino que va de
Descendiente
hacia .
Un nodo es ancestro de un nodo si y sólo si existe un camino que va de
Ancestro
hacia .

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.

Tabla 20: Ejemplos sobre los conceptos de descendiente y ancestro.


Grafo Ejemplos
Nodo Descendientes Ancestros

2.9. GRAFOS CONEXOS Y GRAFOS FUERTEMENTE CONEXOS


Tabla 21: Definición de grafo conexo y grafo fuertemente conexo.
Concepto Definición
Un grafo es un grafo conexo si y sólo si para todo par de nodos y ,
Conexo
se cumple que hay una cadena desde hacia .
Fuertemente Un grafo es un grafo fuertemente conexo si y sólo si para todo par de
conexo nodos y , se cumple que hay un camino desde hacia .

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

2.10. SUBGRAFOS, COMPONENTES CONEXAS Y COMPONENTES FUERTEMENTE


CONEXAS
Tabla 23: Definición de subgrafo, componente conexa y componente fuertemente conexa.
Concepto Definición
Un grafo es un subgrafo de un grafo si y sólo si:
Subgrafo  (el conjunto de nodos de es subconjunto del conjunto de nodos de ).
 (el conjunto de arcos de es subconjunto del conjunto de arcos de ).
Componente Una componente conexa es un subgrafo conexo que cumple la propiedad de que
conexa no se le puede agregar ningún nodo ni arco de tal forma que siga siendo conexo.
Componente Una componente fuertemente conexa es un subgrafo fuertemente conexo que
fuertemente cumple la propiedad de que no se le puede agregar ningún nodo ni arco de tal
conexa forma que siga siendo fuertemente conexo.

Tabla 24: Ejemplos sobre el concepto de componente conexa.


Número de componentes
Grafo
conexas

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.

Tabla 26: Ejemplos sobre el concepto de camino Hamiltoniano y ciclo Hamiltoniano.


Los caminos y ciclos Hamiltonianos se obtienen siguiendo los arcos en orden lexicográfico.
Camino Hamiltoniano Ciclo Hamiltoniano (ejemplo)
Grafo
(ejemplo)

ESTRUCTURAS DE DATOS 11
El grafo no tiene ningún
ciclo Hamiltoniano.

El grafo no tiene ningún


ciclo Hamiltoniano.

El grafo no tiene ningún El grafo no tiene ningún


camino Hamiltoniano. ciclo Hamiltoniano.

2.12. CAMINOS Y CICLOS EULERIANOS


Tabla 27: Definición de camino Euleriano y ciclo Euleriano.
Concepto Definición
Un camino Euleriano es un camino que recorre todos los arcos del grafo, sin
Camino Euleriano
repetir.
Un ciclo Euleriano es un ciclo que recorre todos los arcos del grafo, sin
Ciclo Euleriano
repetir.

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.

Tabla 28: Ejemplos sobre el concepto de camino Euleriano y ciclo Euleriano.


Los caminos y ciclos Eulerianos se obtienen siguiendo los arcos en orden lexicográfico.
Grafo Camino Euleriano (ejemplo) Ciclo Euleriano (ejemplo)

ESTRUCTURAS DE DATOS 12
El grafo no tiene ningún
ciclo Euleriano.

El grafo no tiene ningún


ciclo Euleriano.

El grafo no tiene ningún El grafo no tiene ningún


camino Euleriano. ciclo Euleriano.

El grafo no tiene ningún


ciclo Euleriano.

El grafo no tiene ningún


ciclo Euleriano.

El grafo no tiene ningún El grafo no tiene ningún


camino Euleriano. 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:

 : dado un nodo de un grafo dirigido, da como respuesta su grado de entrada.


 : dado un nodo de un grafo dirigido, da como respuesta su grado de salida.

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 †.

3.1. ALMACENAMIENTO DE LOS NODOS


Tabla 32: Estructuras de datos típicas en las que se pueden almacenar los nodos de un grafo.
Nombre de la Descripción
estructura
Arreglo de nodos Consiste en almacenar los nodos en un arreglo.
Consiste en almacenar los nodos en una lista, implementada con vectores
Lista de nodos
de tamaño variable o con nodos encadenados, por ejemplo.
Consiste en almacenar los nodos en un conjunto, implementado con
Conjunto de nodos
árboles Roji-negros o con tablas de Hashing, por ejemplo.

3.2. ALMACENAMIENTO DE LOS ARCOS

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.

Tabla 34: Ejemplos de algunos grafos cuyos arcos son representados


bajo algunas de las estructuras de datos enumeradas.
Matriz de
Grafo Lista de arcos Listas de sucesores
adyacencia

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.

PARA TENER EN CUENTA


Estudie con calma los conceptos básicos. Se recomienda hacer un breve resumen que le ayude a
recordarlos en el momento que los necesite.

ESTRUCTURAS DE DATOS 18
ESTRUCTURAS DE DATOS
UNIDAD CUATRO - SEMANA OCHO
IMPLEMENTACIÓN DE GRAFOS *

TABLA DE CONTENIDO

1. IMPLEMENTACIÓN DE GRAFOS DIRIGIDOS CON LISTAS DE SUCESORES 2


1.1. CREACIÓN DE GRAFOS 3
1.2. MÉTODOS PARA CONSULTAR LOS SUCESORES Y PREDECESORES DE UN NODO 4
1.3. MÉTODOS PARA ALTERAR EL VALOR DE LOS NODOS 4
1.4. MÉTODOS PARA CONSULTAR ARCOS 5
1.5. MÉTODOS PARA AGREGAR Y ELIMINAR ARCOS 5
1.6. MÉTODO PARA CALCULAR LA MATRIZ DE ADYACENCIA DEL GRAFO 5
2. RECORRIDOS SOBRE GRAFOS 6
2.1. RECORRIDO POR PROFUNDIDAD 7
2.2. RECORRIDO POR ANCHURA 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. IMPLEMENTACIÓN DE GRAFOS DIRIGIDOS CON LISTAS DE
SUCESORES
Recurso como proyecto en Eclipse: Grafos.zip.

La clase Nodo representa un vértice de un grafo, y tiene los siguientes atributos:


 id: es un número natural de a que identifica de manera única al nodo, donde es la
cantidad total de vértices del grafo.
 valor: almacena el valor asociado al nodo.

Código 1: Declaración de la clase Nodo.


public class Nodo { // Declaración de la clase
// *************************
// * Atributos de la clase *
// *************************
public int id;
public Object valor;
// ***********************
// * Métodos de la clase *
// ***********************
public Nodo(int pId, Object pValor) {
id=pId; // Inicializar el identificador del nodo.
valor=pValor; // Inicializar el valor correspondiente al nodo.
}
}

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.

Código 2: Declaración de la clase Arco.


public class Arco { // Declaración de la clase
// *************************
// * Atributos de la clase *
// *************************
public Nodo origen;
public Nodo destino;
public double costo;
// ***********************
// * Métodos de la clase *
// ***********************
public Arco(Nodo pOrigen, Nodo pDestino, double pCosto) {
origen=pOrigen; // Inicializar el origen del arco.
destino=pDestino; // Inicializar el destino del arco.
costo=pCosto; // Inicializar el costo del arco.
}
}

La clase VEDGrafoDirigido representa un grafo dirigido, y tiene los siguientes atributos:


 n: es una variable de tipo int que almacena el número de nodos del grafo.
 nodos: es un arreglo que almacena los nodos del grafo.
 arcos: son las listas de sucesores que almacenan los arcos del grafo.

Código 3: Declaración de la clase VEDGrafoDirigido.


public class VEDGrafoDirigido { // Declaración de la clase

ESTRUCTURAS DE DATOS 2
// *************************
// * Atributos de la clase *
// *************************
protected int n;
protected Nodo[] nodos;
protected List<Arco>[] arcos;
// ***********************
// * Métodos de la clase *
// ***********************
// ...
}

Es importante recalcar que bajo nuestra implementación, arcos[i] es la lista de arcos de


salida del nodo con identificador i, que van desde dicho nodo hacia sus sucesores.

Tabla 4: Algunos grafos representados bajo la implementación.


Grafo Nodos Arcos (listas de sucesores)
0
0 1 2 3 1
2
3

0
0 1 2 3 1
2
3

1.1. CREACIÓN DE GRAFOS

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.

Código 5: Método constructor de la clase VEDGrafoDirigido.


public VEDGrafoDirigido(int pNumeroNodos) {
n=pNumeroNodos; // Inicializar el número de nodos.
nodos=new Nodo[n]; // Inicializar el arreglo de los nodos.
for (int i=0; i<n; i++) { // Por cada i desde 0 hasta n-1:
// Inicializar el nodo con identificador i, asignándole valor null:
nodos[i]=new Nodo(i,null);
}
arcos=new List[n]; // Inicializar el arreglo con las listas de sucesores.
for (int i=0; i<n; i++) { // Por cada i desde 0 hasta n-1:
// Inicializar la lista de arcos de salida del nodo con identificador i:
arcos[i]=new ArrayList<Arco>();
}
}

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.
}

1.3. MÉTODOS PARA ALTERAR EL VALOR DE LOS NODOS

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.
}
}

1.5. MÉTODOS PARA AGREGAR Y ELIMINAR ARCOS


Código 13: Método que agrega un arco al grafo, con complejidad temporal
donde es el grado de salida del nodo origen.
public void insertarArco(int pIdOrigen, int pIdDestino, double pCosto) {
if (!existeArco(pIdOrigen,pIdDestino)) { // Si no existe el arco:
Nodo origen=nodos[pIdOrigen]; // Obtener el nodo de origen dado su identificador.
Nodo destino=nodos[pIdDestino]; // Obtener el nodo de destino dado su identificador.
Arco nuevoArco=new Arco(origen,destino,pCosto); // Crear el nuevo arco.
// Añadir el nuevo arco a la lista de arcos de salida del nodo de origen:
arcos[pIdOrigen].add(nuevoArco);
}
}

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);
}
}

1.6. MÉTODO PARA CALCULAR LA MATRIZ DE ADYACENCIA DEL GRAFO

Recuerde que la matriz de adyacencia es una matriz de números flotantes de tamaño


donde para todos y entre y se tiene que es el costo del arco que va

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.
}

2. RECORRIDOS SOBRE GRAFOS


Recurso como proyecto en Eclipse: Grafos.zip.

Un recorrido de un grafo es un proceso que visita exactamente una vez cada uno de sus
nodos, en cierto orden determinado.

Tabla 16: Algoritmos clásicos de búsqueda sobre grafos.


Concepto Definición
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
Búsqueda
siguiente manera: si el nodo aún no ha sido marcado como visitado, entonces
por profundidad
se marca como visitado, y para cada sucesor del nodo , se llama
recursivamente el proceso desde el nodo .
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
Búsqueda
siguiente manera: se visita el nodo , luego se visitan todos los sucesores del
por anchura
nodo que no hayan sido visitados, a continuación se visitan todos los sucesores
de los sucesores del nodo que no hayan sido visitados, y así sucesivamente.

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.

2.1. RECORRIDO POR PROFUNDIDAD

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.
}
}
}

El recorrido por profundidad se logra aplicando la búsqueda por profundidad 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 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.

PARA TENER EN CUENTA


En la próxima lectura trataremos algunos problemas típicos sobre grafos.

ESTRUCTURAS DE DATOS 10
ESTRUCTURAS DE DATOS
UNIDAD CUATRO - SEMANA OCHO
PROBLEMAS CLÁSICOS SOBRE GRAFOS *

TABLA DE CONTENIDO

1. EL PROBLEMA DE LA RUTA MÁS CORTA 2


1.1. EL ALGORITMO DE DIJKSTRA 2
2. EL PROBLEMA DEL ÁRBOL DE EXPANSIÓN MINIMAL 5
2.1. EL ALGORITMO DE KRUSKAL 6
2.2. EL ALGORITMO DE PRIM 8
3. EL PROBLEMA DEL AGENTE VIAJERO 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. 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.

Tabla 1: Ejemplos de rutas más cortas.


Nodo Nodo
Grafo Ruta más corta Menor costo
inicial final

1.1. EL ALGORITMO DE DIJKSTRA


Tabla 2: Información básica del algoritmo de Dijkstra.
Descubridores Descubierto por Edsger Dijkstra en el año 1959.
Problema que Dado un nodo de partida , encuentra las rutas más cortas que van desde el nodo
resuelve hacia todos los demás, en un grafo donde todos los arcos tienen costo no
negativo.
Restricciones Funciona siempre y cuando todos los arcos del grafo tengan costo no negativo.
Entradas Recibe como parámetro un nodo de partida .
Salidas Para todo nodo del grafo, informa:
 El menor costo de ir de a , ó infinito ( ) en caso de que no haya camino.
 Una ruta de menor costo que va desde hasta .
Pseudocódigo 1. Se declara una estructura de datos para registrar los mínimos costos de ir del
nodo a todos los demás. Basta crear un arreglo costos de números flotantes de
tamaño donde para todo nodo , costos[i] guardará el menor costo de ir desde
hacia .
2. Se declara una estructura de datos para marcar los nodos visitados. Basta crear
un arreglo marcados de booleanos de tamaño donde para todo nodo ,

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.

Se concluye que la complejidad temporal de la implementación básica es , que es


porque siempre se cumple que . Usando una cola de prioridad para extraer el
nodo no visitado con menor costo, obtendríamos una implementación más eficiente.

2. EL PROBLEMA DEL ÁRBOL DE EXPANSIÓN MINIMAL


Tabla 5: Definición de árbol de expansión minimal.
Concepto Definición
Árbol Un árbol es un grafo no dirigido sin ciclos simples.
Costo de un árbol El costo de un árbol es la suma de los costos de sus arcos.
Árbol de Dado un grafo no dirigido , un árbol de expansión es un subgrafo de
expansión sin ciclos simples que incluye todos los nodos de .
Árbol de Dado un grafo no dirigido , un árbol de expansión minimal es un
expansión árbol de expansión tal que no existe ningún otro árbol de expansión con costo
minimal menor.

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.

Tabla 6: Ejemplos sobre el concepto de árbol de expansión minimal.


Un árbol de Costo de cualquier árbol de
Grafo
expansión minimal expansión minimal

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.

2.1. EL ALGORITMO DE KRUSKAL


Tabla 7: Definición de bosque.
Concepto Definición
Bosque Un bosque es un conjunto de árboles.

Tabla 8: Información básica del algoritmo de Kruskal.


Descubridores Descubierto por Joseph Kruskal en el año 1956.
Problema que Dado un grafo conexo no dirigido, halla uno de sus árboles de expansión minimal.
resuelve

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.

Gráfica 9: Algoritmo de Kruskal aplicado sobre un grafo de ejemplo.

grafo inicial bosque inicial arco arco

arco arco arco arco

arco arco arco arco

ESTRUCTURAS DE DATOS 7
arco arco arco arco

arco arco arco arco

arco arco arco arco

arco arco arco árbol de expansión minimal

2.2. EL ALGORITMO DE PRIM


Tabla 10: Información básica del algoritmo de Prim.
Descubridores Descubierto por Vojtěch Jarník en el año 1930 y por Robert C. Prim en el año 1957.
Problema que Dado un grafo conexo no dirigido con arcos de costo no negativo, halla uno de sus
resuelve árboles de expansión minimal.
RestriccionesFunciona siempre y cuando el grafo sea conexo y no dirigido, con arcos de costo no
negativo.
Entradas Ninguna.
Salidas Los arcos pertenecientes a uno de los árboles de expansión minimal del grafo.
Pseudocódigo 1. Si el grafo no tiene nodos, se retorna una lista vacía de arcos. De lo contrario:

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 .

grafo inicial nodo escogido al azar: arco arco

arco arco arco arco

arco arco arco arco

arco arco arco arco

ESTRUCTURAS DE DATOS 9
arco árbol de expansión minimal

3. EL PROBLEMA DEL AGENTE VIAJERO

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.

PARA TENER EN CUENTA


Para fortalecer los conceptos es necesario que realice algunos de los ejercicios propuestos.

ESTRUCTURAS DE DATOS 11
ESTRUCTURAS DE DATOS
UNIDAD CUATRO - SEMANA OCHO
EJERCICIOS PROPUESTOS *

1. PROBLEMA DE LA RUTA MÁS CORTA – ALGORITMO DE DIJKSTRA

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.

2. PROBLEMA DEL ÁRBOL DE EXPANSIÓN MINIMAL – ALGORITMOS DE KRUSKAL Y


DE PRIM

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.

2.3. ¿Cuántos árboles de expansión minimal distintos tiene el grafo 1?

3. PROBLEMA DEL AGENTE VIAJERO – SOLUCIÓN POR PROGRAMACIÓN DINÁMICA

Investigue sobre el algoritmo de programación dinámica para resolver el problema del


agente viajero, con complejidad temporal .

4. ALGORÍTMICA SOBRE GRAFOS – CONCEPTOS BÁSICOS

Implemente los siguientes métodos de la clase VEDGrafoDirigido<E>, calculando la


complejidad temporal de cada uno de éstos. En todos los ejercicios suponga que el grafo es
dirigido.

4.1. Método que retorna una lista con los vértices adyacentes a cierto nodo.
public List<Nodo> getAdyacentes(int pIdNodo)

4.2. Método que determina si un nodo es una fuente.


public boolean esFuente(int pIdNodo)

4.3. Método que calcula el grado de entrada de un nodo.


public int getGradoEntrada(int pIdNodo)

5. ALGORÍTMICA SOBRE GRAFOS – CAMINOS HAMILTONIANOS Y EULERIANOS EN


GRAFOS DIRIGIDOS

Implemente los siguientes métodos de la clase VEDGrafoDirigido<E>, calculando la


complejidad temporal de cada uno de éstos. En todos los ejercicios suponga que el grafo es
dirigido.

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

Вам также может понравиться