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

 

GUÍA DE COMPETENCIAS Y ACTIVIDADES


 

 
 

• SEMANA  1  
Tema  (s):  NÚCLEO  TEMÁTICO:  REPASO  Y  MOTIVACIÓN  DE  LOS  TEMAS  DEL  MÓDULO  
                                         JAVA  COMO  LENGUAJE  DE  PROGRAMACIÓN  
                                         ECLIPSE  COMO  ENTORNO  DE  PROGRAMACIÓN  
                                         CONCEPTOS  SOBRE  PROGRAMACIÓN  ORIENTADA  A  OBJETOS  
 

                                         NÚCLEO  TEMÁTICO:  RECURSIÓN  


                                         FUNCIONES  RECURRENTES.  
                                         MEMORIZATION.  
                                         DIVIDIR  Y  CONQUISTAR  (DIVIDIR  Y  VENCER).  
                                         BACKTRACKING.  
 

                                         NÚCLEO  TEMÁTICO:  ANÁLISIS  DE  ALGORITMOS.  


                                         MOTIVACIÓN.  
                                         COMPLEJIDAD  TEMPORAL  Y  COMPLEJIDAD  ESPACIAL.  
                                         NOTACIÓN  O.  
                                         ANÁLISIS  DE  COMPLEJIDAD  DE  PROGRAMAS  ITERATIVOS.  
                                         ANÁLISIS  DE  COMPLEJIDAD  DE  PROGRAMAS  RECURRENTES.  
 

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.    

 
1   [      POLITÉCNICO  GRANCOLOMBIANO    ]  
 

6-­‐ 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.  

7-­‐ Servirse  de  la  autodisciplina  para  la  ejecución  de  tareas  de  manera  efectiva.  

8-­‐ Generar  estrategias  de  trabajo  efectivo  en  equipo.  

NÚCLEO  TEMÁTICO:  REPASO  Y  MOTIVACIÓN  DE  LOS  TEMAS  DEL  MÓDULO  


•  Motivación  de  los  temas  del  módulo.  
•  Java  como  lenguaje  de  programación.  
•  Eclipse  como  entorno  de  programación.  
•  Conceptos  sobre  programación  orientada  a  objetos.  

ACTIVIDAD   SEMANA   INSTRUCTIVO  

Lectura  1-­‐1  -­‐     Uno   Leer  y  comprender  los  conceptos  expuestos  en  las  lecturas.  
Java  como  lenguaje  de  
programación.  

Lectura  1-­‐2  -­‐     Uno   Leer  y  comprender  los  conceptos  expuestos  en  las  lecturas.  
Eclipse  como  entorno  de  
programación.  

Lectura  1-­‐3  -­‐   Uno   Leer  y  comprender  los  conceptos  expuestos  en  las  lecturas.  
Conceptos  sobre  
programación  orientada  a  
objetos  

OVA  -­‐Introducción  a  Java.   Uno   Comprender  los  conceptos  expuestos  en  el  OVA.  

Video  diapositivas  Uno   Uno   Leer  y  comprender  los  conceptos  expuestos  en  los  videos  
Programación  orientada  a   diapositivas.  
objetos.  

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

Proyecto  de  aula.   Uno   Revisar  el  instructivo  e  iniciar  el  proyecto.  

Teleconferencia.   Uno   Asistir  a  la  teleconferencia  de  la  semana.  

Recursos  adicionales.   Uno   Revisar  cuidadosamente  el  material  en  Java  organizado  como  
proyectos  en  Eclipse,  con  el  fin  de  afianzar  la  práctica.  

 
[      ESTRUCTURAS  DE  DATOS        ]  
2  
 

• SEMANA  2  
 
Tema  (s):    

NÚCLEO  TEMÁTICO:  RECURSIÓN  


•  Funciones  recurrentes.  
•  Memoization.  
•  Dividir  y  Conquistar  (Dividir  y  Vencer).  
•  Backtracking.  

NÚCLEO  TEMÁTICO  :  ANÁLISIS  DE  ALGORITMOS  


•  Motivación.  
•  Complejidad  temporal  y  complejidad  espacial.  
•  Notación  O.  
•  Análisis  de  complejidad  de  programas  iterativos.  
•  Análisis  de  complejidad  de  programas  recurrentes.  
 

ACTIVIDAD   SEMANA   INSTRUCTIVO  

Lectura  2-­‐1  -­‐   Dos   Leer  y  comprender  los  conceptos  expuestos  en  las  
Recursión.   lecturas.  

Lectura  2-­‐2  -­‐   Dos   Leer  y  comprender  los  conceptos  expuestos  en  las  
Análisis  de  Algoritmos.   lecturas.  

Video  diapositivas  1  -­‐  Torres   Dos   Leer  y  comprender  los  conceptos  expuestos  en  los  
de  Hanoi.   videos  diapositivas.  

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

Teleconferencia.   Dos   Asistir  a  la  teleconferencia  de  la  semana.  

Video  resumen.   Dos   Ver  el  resumen  de  la  unidad  uno.  

Recursos  adicionales.   Dos   Revisar  cuidadosamente  el  material  en  Java  


organizado  como  proyectos  en  Eclipse,  con  el  fin  de  
afianzar  la  práctica.  

 
3   [      POLITÉCNICO  GRANCOLOMBIANO    ]  
ESTRUCTURAS DE DATOS
UNIDAD UNO - SEMANA UNO
JAVA COMO LENGUAJE DE PROGRAMACIÓN *

TABLA DE CONTENIDO

1. ¿QUÉ ES JAVA? 2
2. ¿CÓMO DESCARGO JAVA? 3
3. ¿CÓMO INSTALO JAVA? 5
3.1. INSTALACIÓN DE JAVA EN LINUX 5
3.2. INSTALACIÓN DE JAVA EN WINDOWS 6
4. ¿CÓMO ESCRIBO UN PROGRAMA EN JAVA? 7
5. ¿CÓMO COMPILO UN PROGRAMA EN JAVA? 9
6. ¿CÓMO EJECUTO UN PROGRAMA EN JAVA? 10
7. ¿DÓNDE ENCUENTRO DOCUMENTACIÓN SOBRE JAVA? 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. ¿QUÉ ES JAVA?

Java es un lenguaje de programación orientado a objetos creado por Sun Microsystems en


los años noventa y que hoy en día es usado ampliamente tanto en entornos académicos
como empresariales. Su sintaxis † es muy parecida a la de C y C++, lo que facilita a los
desarrolladores de estos lenguajes aprender muy rápidamente a programar en Java.

Gráfica 1: Logo de Java.

Una de las principales virtudes de Java es la gran cantidad de documentación existente y de


librerías especializadas que nos permiten ahorrar mucho trabajo. Por ejemplo, JFreeChart es
una librería Java que nos permite desplegar gráficas estadísticas con un estilo profesional.

Gráfica 2: Ejemplos de uso de JFreeChart .

Todos los programas de Java deben ser codificados en archivos con extensión .java que
luego son compilados para generar archivos con extensión .class que se pueden ejecutar en
la Máquina Virtual de Java (conocida en inglés como la Java Virtual Machine (JVM)). La
Máquina Virtual de Java está disponible para los sistemas operativos Linux, Solaris y
Windows.

Gráfica 3: Proceso de compilación en Java.

Código fuente Compilación Código compilado


(archivo .java) (archivo .class)


La sintaxis se refiere a las reglas que indican cómo escribir programas en el lenguaje.

Imágenes tomadas de http://www.jfree.org/jfreechart/samples.html. JFreeChart Samples, © 2005-2009
Object Refinery Limited (recuperado en noviembre de 2010).

ESTRUCTURAS DE DATOS 2
Una vez compilado un programa en Java, el .class generado puede ejecutarse en cualquier
máquina que tenga instalada la JVM, sin importar que tenga como sistema operativo Solaris,
Linux o Windows. Por esta razón se dice que los programas en Java son portables, ya que se
pueden ejecutar en cualquier computador que tenga la Máquina Virtual de Java, sin tener
que modificar el código fuente.

Escriba una vez, ejecute en cualquier lugar

2. ¿CÓMO DESCARGO JAVA?

Para poder programar aplicaciones en Java se debe instalar la última versión del Kit de
Desarrollo de Java (Java Development Kit (JDK)) en su versión estándar (Java Standard Edition
(Java SE)) §.

Gráfica 4: Encabezado de la página http://java.sun.com/.

Haga clic en Downloads, a continuación clic en Java SE, y finalmente clic en Download JDK
para poder iniciar el proceso de descarga.

Gráfica 5: Paso 1 de la descarga del JDK.

§
Visite la página http://java.sun.com/ con su explorador favorito (Firefox o Internet Explorer por ejemplo).

ESTRUCTURAS DE DATOS 3
Gráfica 6: Paso 2 de la descarga del JDK.

Ahora, seleccione su sistema operativo y haga clic en Download.

Gráfica 7: Paso 3 de la descarga del JDK.

Si no tiene cuenta de usuario en la página de Sun Microsystems, simplemente de clic en Skip


this Step.

Gráfica 8: Paso 4 de la descarga del JDK.

Finalmente, guarde el instalador en el directorio que desee.

ESTRUCTURAS DE DATOS 4
Gráfica 9: Paso 5 de la descarga del JDK.

En caso de que la descarga no inicie automáticamente, debe hacer clic en el enlace que
referencia al instalador.

Gráfica 10: Paso 6 de la descarga del JDK.

3. ¿CÓMO INSTALO JAVA?

La instalación del Kit de Desarrollo de Java (JDK) depende de su sistema operativo. Una vez
complete el proceso de instalación, tendrá en su computador:

1. El Kit de Desarrollo de Java (Java Development Kit (JDK)), que nos permite compilar
programas escritos en Java.

2. El Ambiente de Ejecución de Java (Java Runtime Environment (JRE)) que suministra la


Máquina Virtual con la que podremos ejecutar nuestros programas una vez compilados.

3.1. INSTALACIÓN DE JAVA EN LINUX

Si su sistema operativo es Linux, basta instalar el paquete sun-java6-jdk a través de un gestor


de paquetes como Synaptic.

ESTRUCTURAS DE DATOS 5
Gráfica 11: Paquetes relacionados con Java vistos en Synaptic en la distribución Ubuntu 9.10 (Karmic Koala) de Linux.

A partir de la distribución 10.04 LTS de Ubuntu (Lucid Lynx), Java se provee en el paquete
openjdk-6-jdk, que es la versión libre para la plataforma Java.

3.2. INSTALACIÓN DE JAVA EN WINDOWS

Una vez descargado de Internet el instalador de Java, sólo resta ejecutarlo. Primero, dé doble
clic al archivo descargado en la sección 1.2. ¿Cómo descargo Java?.

Si Windows lanza una advertencia de seguridad, omítala haciendo clic en Ejecutar.

Gráfica 12: Paso 1 de la instalación del JDK (en Windows).

Ahora, simplemente continúe con la instalación.

ESTRUCTURAS DE DATOS 6
Gráfica 13: Siguientes pasos de la instalación del JDK (en Windows).

4. ¿CÓMO ESCRIBO UN PROGRAMA EN JAVA?

El programa típico introductorio a un lenguaje de programación es el Hola Mundo, cuyo


propósito es imprimir una línea de texto con la frase Hola Mundo y así mostrar cómo se
puede escribir algo que funcione en el lenguaje. Abramos una ventana de Bloc de Notas y en
ella escribamos el siguiente código fuente:

Gráfica 14: Hola Mundo en Java.

ESTRUCTURAS DE DATOS 7
public class declara una clase **, que contendrá el comportamiento que
Ejemplo1
queremos que tenga nuestro programa. Dentro de la clase Ejemplo1 se coloca public static
void main(String[] args) para definir el método main del programa, que es el punto de
inicio de la ejecución (es decir, por donde va a comenzar a correr el programa). Y finalmente,
dentro del método main se pone la instrucción System.out.println("Hola Mundo"), que
imprime en la consola del sistema una línea de texto con la frase Hola mundo.

Sólo resta guardar nuestro programa. Hacemos clic en Archivo, luego en Guardar Como, y
escogemos el directorio donde queremos almacenar el archivo.

Gráfica 15: Guardar código fuente Java en un archivo con extensión .java.

Es importante que:

Como tipo de archivo se seleccione Todos los archivos en vez de Documentos de texto
(*.txt).
Como nombre de archivo se coloque el nombre de la clase seguido de la extensión .java (en
nuestro ejemplo en particular, el archivo debe llamarse Ejemplo1.java porque la clase se
llama Ejemplo1).
Como codificación de archivo se escoja ANSI.

Más adelante introduciremos Eclipse, que es un entorno de programación que nos facilita el
desarrollo de proyectos de programación sobre Java.

**
En la programación orientada a objetos, una clase representa un concepto del mundo real.

ESTRUCTURAS DE DATOS 8
5. ¿CÓMO COMPILO UN PROGRAMA EN JAVA?

Para compilar un programa en Java, abra una consola de comandos y ubíquela en el


directorio donde guardó el archivo .java.

Gráfica 16: Búsqueda de la consola de comandos (símbolo del sistema) en el menú de Accesorios de Windows.

Gráfica 17: Ejemplo de ubicación de la consola de comandos sobre un directorio, con el comando cd.

Para compilar la clase Ejemplo1, que se encuentra declarada en el archivo Ejemplo1.java,


ejecute en la consola de comandos la instrucción javac Ejemplo1.java.

Gráfica 18: Ejecución del comando javac.

El comando javac llama al compilador de Java, que es el responsable de convertir código


fuente de Java (.java) a archivos compilados (.class). Observe que después de la
compilación fue creado el archivo Ejemplo1.class, justo en el mismo directorio donde está el
archivo Ejemplo1.java.

ESTRUCTURAS DE DATOS 9
En caso de que la consola de comandos no reconozca javac después de haber instalado Java,
en la opción Propiedades del sistema (teclas Windows+Pausa) > Opciones avanzadas >
Variables de entorno, busque la variable Path y añádale un punto y coma seguido del
directorio bin donde se encuentra instalado Java (por ejemplo, C:\Archivos de
programa\Java\jdk1.6.0_18\bin). El Path debería lucir así:

...;C:\Archivos de programa\Java\jdk1.6.0_18\bin

Gráfica 19: Ventana de modificación de la variable de sistema Path.

6. ¿CÓMO EJECUTO UN PROGRAMA EN JAVA?

Para correr el método main de la clase Ejemplo1, sobre la misma consola de comandos donde
compiló el programa, ejecute la instrucción java –cp . Ejemplo1.

Gráfica 20: Ejecución del comando java.

ESTRUCTURAS DE DATOS 10
El comando java es el responsable de invocar a la Máquina Virtual de Java para que ejecute
el programa recién compilado. Observe que después de colocar java –cp . Ejemplo1 en la
consola de comandos, nuestro programa ejecutó e imprimió exitosamente en la consola la
frase Hola Mundo.

7. ¿DÓNDE ENCUENTRO DOCUMENTACIÓN SOBRE JAVA?

Documentación del API de la versión 6 del Kit de Desarrollo de Java (conocido como Java
Development Kit (JDK))
Java™ Platform, Standard Edition 6 API Specification
http://java.sun.com/javase/6/docs/api/

Tutorial del lenguaje de programación Java


Java language basics
http://java.sun.com/docs/books/tutorial/java/nutsandbolts/index.html

Tutorial de la librería Swing para la implementación de interfaces gráficas


The Swing Tutorial
http://java.sun.com/docs/books/tutorial/uiswing/

EN RESUMEN
Java es un lenguaje de programación orientado a objetos creado por Sun Microsystems en los años
noventa y que hoy en día es usado ampliamente tanto en entornos académicos como empresariales.
El instalador de Java se encuentra en la página WEB http://java.sun.com/.
El Hola Mundo en Java se escribe de la siguiente manera:
public class Ejemplo1 {
public static void main(String[] args) {
System.out.println("Hola Mundo");
}
}
Para compilar un programa escrito en Java, ejecute en una consola de comandos la instrucción
javac NombreClase.java
donde NombreClase debe reemplazarse por el nombre de la clase que tiene el método main.
Para ejecutar un programa compilado en Java, ejecute en una consola de comandos la instrucción
java –cp . NombreClase
donde NombreClase debe reemplazarse por el nombre de la clase que tiene el método main.

PARA TENER EN CUENTA


 Implemente una clase Ejemplo2 que imprima en varias líneas por separado las cadenas de texto
"Pera", "Manzana" y "Melocotón". Después de ejecutar su programa, ¿cómo quedó impresa en
consola la cadena de texto "Melocotón"? ¿A qué le atribuye la situación que se está presentando?

ESTRUCTURAS DE DATOS 11
 ¿Qué sucede si el nombre del archivo no coincide con el nombre de la clase? Haga un experimento
donde defina una clase Ejemplo3 que imprima Hola Mundo pero que no quede guardada en el
archivo Ejemplo3.java sino en el archivo Ejemplo.java. ¿Qué error se lanza cuando se compila
Ejemplo.java?
 Investigue cómo se le puede pedir datos al usuario por consola, y desarrolle un programa que pida
dos números enteros al usuario e imprima en la consola la suma de éstos.

ESTRUCTURAS DE DATOS 12
ESTRUCTURAS DE DATOS
UNIDAD UNO - SEMANA UNO
ECLIPSE COMO ENTORNO DE PROGRAMACIÓN *

TABLA DE CONTENIDO

1. ¿QUÉ ES ECLIPSE? 2
2. ¿CÓMO DESCARGO ECLIPSE? 2
3. ¿CÓMO INSTALO ECLIPSE? 3
4. ¿CÓMO INICIO ECLIPSE? 3
5. ¿CÓMO CONSTRUYO UN PROYECTO EN ECLIPSE? 5
6. ¿CÓMO CONSTRUYO EL PROGRAMA HOLA MUNDO EN ECLIPSE? 7
7. ¿CÓMO EJECUTO EL PROGRAMA HOLA MUNDO EN ECLIPSE? 9
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. ¿QUÉ ES ECLIPSE?

Eclipse es un entorno de programación utilizado para desarrollar proyectos en lenguajes


como Java. Ofrece gran cantidad de opciones y facilidades que hacen de la programación una
actividad más cómoda y sencilla.

Gráfica 1: Logo de Eclipse.

2. ¿CÓMO DESCARGO ECLIPSE?

Para instalar la última versión de Eclipse para programar sobre Java, visite la página
http://www.eclipse.org/ con su explorador favorito (Firefox o Internet Explorer por ejemplo).
Haga clic en Downloads y luego en Eclipse Classic. Finalmente, escoja el lugar de donde
quiere efectuar la descarga y continúe.

Gráfica 2: Encabezado de la página principal de Eclipse.

Gráfica 3: Versión clásica de Eclipse para programar en Java.

Gráfica 4: Selección del sitio de descarga.

ESTRUCTURAS DE DATOS 2
3. ¿CÓMO INSTALO ECLIPSE?

Instalar Eclipse es tan fácil como descomprimir el archivo empaquetado .zip que descargó de
la página WEB, usando alguna herramienta de compresión como WinZip o WinRar.

Gráfica 5: Descompresión de Eclipse con WinZip.

Gráfica 6: Directorio de Eclipse después de la descompresión.

Si su sistema operativo es Linux, basta instalar el paquete eclipse a través de un gestor de


paquetes como Synaptic.

4. ¿CÓMO INICIO ECLIPSE?

Basta con ejecutar eclipse.exe y esperar a que cargue.

ESTRUCTURAS DE DATOS 3
Gráfica 7: Pantalla de bienvenida de Eclipse, mientras se efectúa su carga.

La primera vez que se carga Eclipse va a pedir que configuremos el directorio de trabajo
donde se van a guardar todos los proyectos que se realicen.

Gráfica 8: Selección del directorio de trabajo (workspace) de Eclipse.

Gráfica 9: Ventana principal de Eclipse.

En el menú Window bajo la opción Show View se pueden activar vistas como las siguientes:

Vista Navegador (Navigator): nos muestra todos los proyectos disponibles y los directorios
que conforman el proyecto que se está trabajando.
Vista Problemas (Problems): nos muestra los errores de compilación.
Vista Búsquedas (Search): nos muestra los resultados de las búsquedas que se realicen.

ESTRUCTURAS DE DATOS 4
Vista Consola (Console): nos muestra la consola del sistema, donde se puede pedir
información al usuario e imprimir resultados.

Gráfica 10: Vistas disponibles en Eclipse.

5. ¿CÓMO CONSTRUYO UN PROYECTO EN ECLIPSE?

Para crear un proyecto en Eclipse se hace clic derecho en la vista Navigator, se posa el
puntero del ratón sobre New y se hace clic en Java Project.

Gráfica 11: Creación de un proyecto en Eclipse.

Luego se suministra el nombre del proyecto, se diligencian los demás datos de ser necesario,
y se hace clic en el botón Finish.

ESTRUCTURAS DE DATOS 5
Gráfica 12: Configuración de un nuevo proyecto en Eclipse.

El proyecto creado se muestra en la vista Navigator. El directorio src contendrá los archivos
.java con el código fuente, y el directorio bin contendrá los archivos .class compilados.

Gráfica 13: Proyecto nuevo creado en Eclipse.

ESTRUCTURAS DE DATOS 6
6. ¿CÓMO CONSTRUYO EL PROGRAMA HOLA MUNDO EN
ECLIPSE?

Para crear una clase sobre el proyecto se hace clic derecho en el nombre del proyecto
(Prueba en nuestro ejemplo), se posa el puntero del ratón sobre New y se hace clic en Class.

Gráfica 14: Creación de una clase en Eclipse.

Luego, se suministra el nombre de la clase, se diligencian los demás datos de ser necesario, y
se hace clic en el botón Finish.

ESTRUCTURAS DE DATOS 7
Gráfica 15: Configuración de una nueva clase en Eclipse.

Eclipse automáticamente crea el archivo .java y declara la clase con la sentencia public
class NombreClase, donde NombreClase es el nombre que le dimos a la clase (Prueba en
nuestro ejemplo).

Gráfica 16: Nueva clase creada en Eclipse.

Para construir el método main ubicamos el cursor dentro de los corchetes, y lo declaramos.
Luego, añadimos la instrucción System.out.println("Hola Mundo") para imprimir la frase
Hola Mundo en la consola. No hay que olvidar guardar el programa con la opción Save del
menú File.

ESTRUCTURAS DE DATOS 8
Gráfica 17: Hola Mundo en Eclipse.

7. ¿CÓMO EJECUTO EL PROGRAMA HOLA MUNDO EN ECLIPSE?

La primera vez que se ejecuta un programa debe hacerse clic en el menú Run, luego en Run
As, y finalmente en Java Application.

Gráfica 18: Opción para ejecutar un nuevo programa.

Se puede observar en la vista Console que el programa imprimió la frase Hola Mundo.

Gráfica 19: Consola del sistema mostrando el texto Hola Mundo.

La próxima vez que se necesite volver a ejecutar el programa, es suficiente hacer clic en Run.

Gráfica 20: Botón para ejecutar programas en Eclipse.

ESTRUCTURAS DE DATOS 9
EN RESUMEN
Eclipse es un entorno de programación utilizado para desarrollar proyectos en lenguajes como Java.
Ofrece gran cantidad de opciones y facilidades que hacen de la programación una actividad más
cómoda y sencilla.

PARA TENER EN CUENTA


 Actualice periódicamente su versión de Eclipse.
 Puede personalizar Eclipse a través de la opción Window > Preferences.
 Guarde sus proyectos periódicamente para evitar pérdida de la información.
 Al oprimir las teclas Control+Espacio, Eclipse lo puede ayudar autocompletando código.
 Para indentar código automáticamente, selecciónelo y oprima las teclas Control+I.
 Haga sus propias pruebas sobre Eclipse, auto-explorando las funcionalidades que provee.
 Implemente varios programas en Java utilizando Eclipse. Use su imaginación.

ESTRUCTURAS DE DATOS 10
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
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

Вам также может понравиться