Notas Del Curso Alg. E.D

135
Algoritmos y Estructuras de Datos 1. JAVA 1.1 ¿QUÉ ES JAVA? Sun describió a Java de la siguiente forma: Java: Es un lenguaje simple, orientado a objetos, distribuido, interpretado, robusto, seguro, de arquitectura neutra, portable, de alto desempeño, de hilos múltiples y dinámico. Java es un lenguaje de programación orientado a objetos. A diferencia de C++, Java se diseñó desde un principio para estar orientado a objetos. La mayoría de las cosas en Java son objetos; los primitivos tipos numéricos, carácter y booleano son la única excepción. En Java, las cadenas se representan con objetos, los hilos, etc. Como los programas de Java se compilan en un formato de bytecode (código de bytes) de arquitectura neutral, una aplicación de Java se puede ejecutar en cualquier sistema, siempre y cuando dicho sistema instrumente la máquina virtual de Java. Esto resulta muy importante para las aplicaciones distribuidas en Internet u otras redes heterogéneas. Para asegurar que los programas de Java sean realmente independientes, de la plataforma, hay una arquitectura única sobre la que se compilan todos los programas en Java. Es decir, cuando se compila para una plataforma en Windows/Intel x86 se obtiene la misma salida que la compilada en un sistema Macintosh o Unix. El compilador no compila para la plataforma de origen, sino para una plataforma abstracta llamada máquina virtual de Java, o JVM-Java Virtual Machine. A Java también se le denomina lenguaje distribuido. Esto significa, simplemente, que proporciona un soporte de alto nivel para redes. Por ejemplo, la clase URL y las clases relacionadas en el paquete java.net hacen que la lectura de un archivo o una fuente remota sea tan fácil como leer un archivo local. Java se ha diseñado para escribir software robusto y muy confiable. Una de las cosas que hace simple a Java es la carencia de punteros y aritmética de puntero. Todos los accesos a arreglos y cadenas se verifican al tiempo de ejecución. Las formas de los objetos de un tipo a otro también se verifican al momento de la ejecución. La recolección automática de basura en Java evita que la memoria tenga fugas, así como la presencia de otros defectos perniciosos relacionados con la asignación y desasignación de memoria. 1

description

N

Transcript of Notas Del Curso Alg. E.D

Page 1: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

1. JAVA 1.1 ¿QUÉ ES JAVA?

Sun describió a Java de la siguiente forma: Java: Es un lenguaje simple, orientado a objetos, distribuido, interpretado, robusto, seguro, de arquitectura neutra, portable, de alto desempeño, de hilos múltiples y dinámico.

• Java es un lenguaje de programación orientado a objetos. A diferencia de C++, Java se diseñó desde un principio para estar orientado a objetos. La mayoría de las cosas en Java son objetos; los primitivos tipos numéricos, carácter y booleano son la única excepción. En Java, las cadenas se representan con objetos, los hilos, etc.

• Como los programas de Java se compilan en un formato de bytecode (código de bytes) de arquitectura neutral, una aplicación de Java se puede ejecutar en cualquier sistema, siempre y cuando dicho sistema instrumente la máquina virtual de Java. Esto resulta muy importante para las aplicaciones distribuidas en Internet u otras redes heterogéneas. Para asegurar que los programas de Java sean realmente independientes, de la plataforma, hay una arquitectura única sobre la que se compilan todos los programas en Java. Es decir, cuando se compila para una plataforma en Windows/Intel x86 se obtiene la misma salida que la compilada en un sistema Macintosh o Unix. El compilador no compila para la plataforma de origen, sino para una plataforma abstracta llamada máquina virtual de Java, o JVM-Java Virtual Machine.

• A Java también se le denomina lenguaje distribuido. Esto significa, simplemente, que proporciona un soporte de alto nivel para redes. Por ejemplo, la clase URL y las clases relacionadas en el paquete java.net hacen que la lectura de un archivo o una fuente remota sea tan fácil como leer un archivo local.

• Java se ha diseñado para escribir software robusto y muy confiable.

• Una de las cosas que hace simple a Java es la carencia de punteros y

aritmética de puntero. Todos los accesos a arreglos y cadenas se verifican al tiempo de ejecución. Las formas de los objetos de un tipo a otro también se verifican al momento de la ejecución. La recolección automática de basura en Java evita que la memoria tenga fugas, así como la presencia de otros defectos perniciosos relacionados con la asignación y desasignación de memoria.

1

Page 2: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

• El manejo de excepciones es otra característica de Java que contribuye a formar programas más robustos. Una excepción es una señal de que ha ocurrido una condición excepcional, por ejemplo, un error de “no se puede encontrar el archivo”. Con esto puede simplificar la tarea del manejo y recuperación de errores.

• Uno de los aspectos más resonados de Java es que se trata de un

lenguaje seguro. Proporciona varias capas de controles de seguridad que protegen contra código malicioso; estas capas permiten a los usuarios ejecutar con comodidad programas desconocidos, como los applet.

• Java es un lenguaje de hilos múltiples; proporciona soporte para varios

hilos de ejecución que pueden manejar diferentes tareas. Un beneficio importante de los hilos múltiples es que se mejora el desempeño interactivo de las aplicaciones gráficas para el usuario.

1.2 JAVA VS C++ Java se asemeja mucho a C y C++. Esta similitud, evidentemente intencionada,

es la mejor herramienta para los programadores, ya que facilita en gran manera su transición a Java. Desafortunadamente, tantas similitudes hacen que no nos paremos en algunas diferencias que son vitales. La terminología utilizada en estos lenguajes, a veces es la misma, pero hay grandes diferencias subyacentes en su significado.

C tiene tipos de datos básicos y punteros. C++ modifica un poco este panorama y le añade los tipos referencia. Java también especifica sus tipos primitivos, elimina cualquier tipo de punteros y tiene tipos referencia mucho más claros.

Conocemos ya ampliamente todos los tipos básicos de datos: datos base, integrados, primitivos e internos; que son muy semejantes en C, C++ y Java; aunque Java simplifica un poco su uso a los desarrolladores haciendo que el chequeo de tipos sea bastante más rígido. Además, Java añade los tipos boolean y hace imprescindible el uso de este tipo booleano en sentencias condicionales.

1.3 LA SIMPLICIDAD DE JAVA Java ha sido diseñado de modo de eliminar las complejidades de otros lenguajes como C y C++. Si bien Java posee una sintaxis similar a C, con el objeto de facilitar la migración de C hacia a Java, Java es semánticamente muy distinto a C:

• Java no posee aritmética de punteros: La aritmética de punteros es el origen de muchos errores de programación que no se manifiestan durante

2

Page 3: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

la depuración y que una vez que el usuario los detecta son difíciles de resolver.

• No se necesita hacer delete: Determinar el momento en que se debe liberar

el espacio ocupado por un objeto es un problema difícil de resolver correctamente. Esto también es el origen a errores difíciles de detectar y solucionar.

• No hay herencia múltiple: En C++ esta característica da origen a muchas

situaciones de borde en donde es difícil predecir cuál será el resultado. Por esta razón en Java se opta por herencia simple que es mucho más simple de aprender y dominar.

1.4 JAVA ES INDEPENDIENTE DE LA PLATAFORMA La independencia de la plataforma es la capacidad del programa de trasladarse

con facilidad de un sistema computacional a otro. Esta independencia de la plataforma es una de las principales ventajas que

tiene Java sobre otros lenguajes de programación, en particular para los sistemas que necesitan trabajar en varias plataformas.

A nivel de código fuente, los tipos primitivos de datos Java, tiene tamaños consistentes, en todas las plataformas de desarrollo. Los fundamentos de bibliotecas de Java facilitan la escritura del código, el cual puede desplazarse de plataforma a plataforma sin necesidad de volver a escribirlo.

Los archivos binarios Java, también son independientes de la plataforma, y

pueden compilarse en múltiples plataformas sin necesidad de volver a compilar la fuente, esto se logra, ya que los archivos binario Java, se encuentran en una forma llamada bytecode (conjunto de instrucciones parecidas al lenguaje de máquina, pero que no son específicas para un procesador). El ambiente de desarrollo Java tiene dos partes: un compilador y un intérprete Java. El compilador Java toma su programa Java y en lugar de generar códigos de máquina para sus archivos fuente, genera un bytecode. Para ejecutar un programa Java debe utilizar un intérprete de bytecode el cual a su vez ejecuta su programa Java. Puede ejecutar el intérprete por si mismo o en el caso de los applets puede recurrir a los visualizadores que los ejecutan.

3

Page 4: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Compilador Java

bytecode de Java

Código de Java

SPARC

PowerPC

Pentium

Interprete Java

La desventaja de utilizar bytecode se halla en la velocidad de ejecución, puesto que los programas específicos del sistema corren directamente en el hardware en que se compilaron estos, ya que los bytecodes deben ser procesados por el intérprete

Los programas en Java pueden ejecutarse en cualquiera de las siguientes plataformas, sin necesidad de hacer cambios:

• Windows/95 y /NT • Power/Mac • Unix (Solaris, Silicon Graphics, ...)

La compatibilidad es total:

• A nivel de fuentes: El lenguaje es exactamente el mismo en todas las plataformas.

• A nivel de bibliotecas: En todas las plataformas están presentes las mismas

bibliotecas estándares.

• A nivel del código compilado: el código intermedio que genera el compilador es el mismo para todas las plataformas. Lo que cambia es el intérprete del código intermedio.

4

Page 5: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

1.5 SEGURIDAD EN JAVA

La seguridad es un aspecto importante en Java, el visualizador baja el código

de toda la red y lo ejecuta en el anfitrión del usuario, se incluye varias capas de seguridad tales como:

• El propio lenguaje Java incluye restricciones cerradas de acceso a memoria

muy diferentes al modelo de memoria que utiliza el lenguaje C. Estas restricciones incluyen la remoción de apuntadores aritméticos y de operadores de conversión forzada ilegales.

• Una rutina de verificación de códigos de byte en el intérprete de Java verifica que los códigos de byte (Bytecodes) no violen ninguna construcción del lenguaje (lo que podría suceder si utiliza un compilador de Java alterado). Esta rutina de verificación se asegura de que el código no falsifique apuntadores, memoria de acceso restringido u objetos de acceso diferentes a los que corresponden a sus definiciones. Esta verificación también asegura que las llamadas al método incluyen el número correcto de argumentos del tipo adecuado, y que no hay desbordamiento de pilas.

• Una verificación del nombre de la clase y de las restricciones de acceso sobre la carga.

• Una interfaz de sistema de seguridad que refuerza las políticas de seguridad en varios niveles.

• Al nivel de acceso del archivo, si un código de byte intenta el acceso a un archivo para el que no tiene permiso, aparecerá una caja de diálogo para permitir que el usuario continúe o detenga la ejecución.

• Al nivel de red, se tendrán opciones para emplear codificación de clave pública y otras técnicas de encriptación para verificar la fuente del código y su integridad después de haber pasado por la red. Esta tecnología de encriptación será la clave parra transacciones financieras seguras a través de la red.

• Al momento de la ejecución, puede utilizarse la información sobre el origen del código de byte para decidir lo que el código puede hacer. El mecanismo de seguridad puede indicar si un código de byte se originó o no desde el interior de una firewall (barrera de protección). También se puede definir una política de seguridad que restrinja el código dónde no se confía.

• Java siempre checa los índices al acceder un arreglo. • Java realiza chequeo de tipos durante la compilación (al igual que C). En

una asignación entre punteros el compilador verifica que los tipos sean compatibles.

• Además, Java realiza chequeo de tipos durante la ejecución (cosa que C y C++ no hacen). Cuando un programa usa un cast para accesar un objeto como si fuese de un tipo específico, se verifica durante la ejecución que el objeto en cuestión sea compatible con el cast que se le aplica. Si el objeto no es compatible, entonces se levanta una excepción que informa al programador la línea exacta en donde está la fuente del error.

5

Page 6: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

• Java posee un recolector de basuras que administra automáticamente la memoria. Es el recolector el que determina cuando se puede liberar el espacio ocupado por un objeto. El programador no puede liberar explícitamente el espacio ocupado por un objeto.

• Java no posee aritmética de punteros, porque es una propiedad que no se necesita para programar aplicaciones. En C sólo se necesita la aritmética de punteros para programa malloc/free o para programar el núcleo del sistema operativo.

Por lo tanto Java no es un lenguaje para hacer sistemas operativos o administradores de memoria, pero sí es un excelente lenguaje para programar aplicaciones. 1.6 JAVA ES FLEXIBLE Pascal también es un lenguaje robusto, pero logra su robustez prohibiendo tener punteros a objetos de tipo desconocido. Lamentablemente esta prohibición es demasiado rígida. Aunque son pocos los casos en que se necesita tener punteros a objetos de tipo desconocido, las contorsiones que están obligados a realizar los programadores cuando necesitan estos punteros dan origen a programas ilegibles. Lisp por su parte es un lenguaje flexible y robusto. Todas las variables son punteros a objetos de cualquier tipo (un arreglo, un elemento de lista, etc.). El tipo del objeto se encuentra almacenado en el mismo objeto. Durante la ejecución, en cada operación se chequea que el tipo del objeto manipulado sea del tipo apropiado. Esto da flexibilidad a los programadores sin sacrificar la robustez. Lamentablemente, esto hace que los programas en Lisp sean poco legibles debido a que al estudiar su código es difícil determinar cuál es el tipo del objeto que referencia una variable. Java combina flexibilidad, robustez y legibilidad gracias a una mezcla de chequeo de tipos durante la compilación y durante la ejecución. En Java se pueden tener punteros a objetos de un tipo específico y también se pueden tener punteros a objetos de cualquier tipo. Estos punteros se pueden convertir a punteros de un tipo específico aplicando un cast, en cuyo caso se chequea en tiempo de ejecución de que el objeto sea de un tipo compatible. El programador usa entonces punteros de tipo específico en la mayoría de los casos con el fin de ganar legibilidad y en unos pocos casos usa punteros a tipos desconocidos cuando necesita tener flexibilidad. Por lo tanto Java combina la robustez de Pascal con la flexibilidad de Lisp, sin que lo programas pierdan legibilidad en ningún caso.

6

Page 7: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

1.7 JAVA ADMINISTRA AUTOMÁTICAMENTE LA MEMORIA En Java los programadores no necesitan preocuparse de liberar un trozo de memoria cuando ya no lo necesitan. Es el recolector de basuras el que determina cuando se puede liberar la memoria ocupada por un objeto. Un recolector de basuras es un gran aporte a la productividad. Se ha estudiado en casos concretos que los programadores han dedicado un 40% del tiempo de desarrollo a determinar en qué momento se puede liberar un trozo de memoria. Además este porcentaje de tiempo aumenta a medida que aumenta la complejidad del software en desarrollo. Es relativamente sencillo liberar correctamente la memoria en un programa de 1000 líneas. Sin embargo, es difícil hacerlo en un programa de 10000 líneas. Y se puede postular que es imposible liberar correctamente la memoria en un programa de 100000 líneas. Para entender mejor esta afirmación, supongamos que hicimos un programa de 1000 líneas hace un par de meses y ahora necesitamos hacer algunas modificaciones. Ahora hemos olvidado gran parte de los detalles de la lógica de este programa y ya no es sencillo determinar si un puntero referencia un objeto que todavía existe, o si ya fue liberado. Peor aún, suponga que el programa fue hecho por otra persona y evalúe cuan probable es cometer errores de memoria al tratar de modificar ese programa. Ahora volvamos al caso de un programa de 100000 líneas. Este tipo de programas los desarrolla un grupo de programadores que pueden tomar años en terminarlo. Cada programador desarrolla un módulo que eventualmente utiliza objetos de otros módulos desarrollados por otros programadores. ¿Quién libera la memoria de estos objetos? ¿Cómo se ponen de acuerdo los programadores sobre cuándo y quién libera un objeto compartido? ¿Como probar el programa completo ante las infinitas condiciones de borde que pueden existir en un programa de 100000 líneas? Es inevitable que la fase de prueba dejará pasar errores en el manejo de memoria que sólo serán detectados más tarde por el usuario final. Probablemente se incorporan otros errores en la fase de mantenimiento. Se puede concluir:

• Todo programa de 100000 líneas que libera explícitamente la memoria tiene errores latentes.

• Sin un recolector de basuras no hay verdadera modularidad.

7

Page 8: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

• Un recolector de basuras resuelve todos los problemas de manejo de memoria en forma trivial.

La pregunta es: ¿Cuál es el impacto de un recolector de basura en el desempeño de un programa? El sobrecosto de la recolección de basuras no es superior al 100%. Es decir si se tiene un programa que libera explícitamente la memoria y que toma tiempo X, el mismo programa modificado de modo que utilice un recolector de basuras para liberar la memoria tomará un tiempo no superior a 2X. Este sobrecosto no es importante si se considera el periódico incremento en la velocidad de los procesadores. El impacto que un recolector de basura en el tiempo de desarrollo y en la confiabilidad del software resultante es muchos más importante que la pérdida en eficiencia.

1.8 LA APLICACIÓN HELLO WORLD Una aplicación es un programa convencional que se invoca desde el intérprete de comandos. Este programa se carga directamente desde el disco y no de la red Internet. Ahora veremos la aplicación más simple que se puede escribir en Java: el clásico ``Hello World''.

• Crear un archivo llamado Hello1.java con:

// La aplicación Hello World! public class Hello1 { public static void main (String args[]) { System.out.println("Hello World!"); } }

• Compilar con: javac Hello1.java

• Ejecutar con: java Hello1

Observaciones:

• La primera línea es un comentario. Todo lo que viene después de la secuencia // hasta el fin de línea es un comentario.

Java también acepta comentarios ``a la C'': /* ... */

8

Page 9: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

• Luego viene la definición de una clase llamada Hello1: public class Hello1 { ... }

En Java un programa es un conjunto de definiciones de clases que están dispuestas en uno o más archivos.

• Dentro de la clase Hello1 se define el método main:

public static void main (String args[]) { ... }

En una clase se definen uno o más métodos.

• Las palabras public y static son atributos del método que discutiremos más tarde.

• La palabra void indica que el método main no retorna ningún valor.

• La forma (String args[]) es la definición de los argumentos que recibe el

método main. En este caso se recibe un argumento. Los paréntesis [] indican que el argumentos es un arreglo y la palabra String es el tipo de los elementos del arreglo.

Por lo tanto main recibe como argumento un arreglo de strings que corresponden a los argumentos con que se invoca el programa.

• La instrucción System.out.println(...) despliega un string en la consola.

Java no posee una sintaxis abreviada para desplegar strings.

Consideraciones importantes:

• El nombre del archivo (Hello1.java) siempre debe ser el nombre de la clase (Hello1) con la extensión ``.java''.

• Todas las aplicaciones deben definir el método main.

• Al invocar el intérprete de java con java Hello1, se busca y se invoca un

método main que textualmente haya sido definido con: public static void main (String args[]) { ... } No cambie el nombre de este procedimiento ni omita ninguno de sus atributos. Tampoco cambie el tipo de los argumentos o el valor retornado.

9

Page 10: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

1.9 EL APPLET HELLO WORLD Un applet es un programa que anima una porción de una página Web. Se recupera a partir de la red y corre en la máquina del usuario, pero con muchas restricciones de modo que no pueda afectar la integridad del ambiente del usuario. A continuación veremos la versión applet del ejemplo anterior. Es decir un programa que coloca en una página Web el mensaje ``Hello World!''.

• Crear el programa fuente Hello2.java con:

import java.awt.Graphics; import java.applet.Applet; public class Hello2 extends Applet { public void paint(Graphics g) { g.drawString("Hello world!", 50, 25); } }

• Compilar con: javac Hello2.java

• Crear la página Hello.html con el siguiente contenido:

<html> <body> Este es un applet: <applet code="Hello2.class" width=150 height=25> </applet> </body> </html>

Atención: Hello.html debe estar en el mismo directorio que Hello2.java.

• Ver el applet con: appletviewer Hello.html

• El mismo applet también se puede ver desde un browser Web como netscape 2.x o superior.

Observaciones:

• Dado que un applet no se invoca desde el intérprete de comandos, no tiene sentido definir el método main. El browser Web notifica al applet

10

Page 11: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

que debe dibujar su contenido invocando el método paint. Esto ocurre cada vez que se muestra la porción de la página html que contiene este applet. Por lo tanto un applet debe definir el método paint (en vez de

• main).

• Las instrucciones:

import java.awt.Graphics; import java.applet.Applet;

indican que dentro del archivo las clases java.awt.Graphics y java.applet.Applet serán conocidas simplemente como Graphics y Applet.

• Luego viene la definición de la clase Hello2 con:

public class Hello2 extends Applet { ... }

Las palabras extends indican que Hello2 es una extensión de la clase de biblioteca Applet. Esto significa que Hello2 es casi como Applet, solo que se modifica el comportamiento del método paint.

• El método paint recibe como argumento un objeto de tipo Graphics que corresponde a una clase de biblioteca. Este objeto se usa para dibujar en la porción de página html asignada al applet.

• La instrucción g.drawString("Hello world!", 50, 25); dibuja el string ``Hello

World!'' en la porción asignada en las coordenadas (50, 25).

• Además de programar el applet es necesario construir la página html que va a contener el applet. Por limitaciones de espacio en este curso sólo veremos las etiquetas de html que permiten agregar un applet a la página, sin detenernos a ver en profundidad el lenguaje html.

1.10 INSTRUCCIONES Y EXPRESIONES

Una instrucción es lo más sencillo que se puede hacer en Java. Una instrucción forma una sola operación Java. Todas las siguientes son instrucciones Java sencillas:

int i =1; import java.awt.Font; System.out.println(“Esta motocicleta es ” + color + “ ” + make); m.engineState = true;

11

Page 12: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Algunas veces, las instrucciones regresan valores. Por ejemplo, cuando sumamos dos valores o una prueba para ver si un valor es igual al otro. Este tipo de enunciados se llaman expresiones.

Una instrucción puede ser escrita en un solo renglón o en múltiples renglones y

el compilador Java lo entenderá perfectamente. Java también cuenta con instrucciones compuestas, o bloques de instrucciones,

que pueden ser ubicadas en cualquier parte en cualquier lugar donde pondría una sola instrucción. Los enunciados de bloques están rodeados por llaves({}).

1.11 LA DECLARACIÓN IMPORT (IMPORTAR)

La declaración import hace que las clases de Java estén disponibles para la

clase actual, bajo un nombre abreviado. Las clases públicas de Java siempre estén disponibles vía sus nombres totalmente calificados, siempre y cuando el archivo de la clase en cuestión se pueda encontrar.

Hay dos formas de la declaración import:

import package.class; import package.*; La primera forma permite que la clase especificada en el paquete especificado

se conozca por su nombre de clase simplemente. Por ejemplo: import java.awt.Graphics; import java.awt.Color; La segunda forma de la declaración import logra que todas las

clases de un paquete estén disponibles mediante su nombre de clase. Por ejemplo:

import java.awt.*;

1.12 REGLAS DE ESCRITURA DE UN PROGRAMA • Los identificadores pueden iniciar con una letra, un guión de subrayado(

_ ) o un signo de dólares ($), de ninguna manera puede empezar con un número. Después del primer carácter, los nombres pueden incluir cualquier letra o número. Los identificadores no pueden ser una palabra clave de Java.

12

Page 13: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

• El lenguaje Java utiliza un conjunto de caracteres Unicode. Esta es la definición del conjunto de signos que no solo ofrece caracteres en el conjunto estándar ASCII, sino también varios millones de caracteres para representar la mayoría de los alfabetos internacionales. Esto significa que puede utilizar caracteres acentuados y otros símbolos, así como caracteres legales en nombre de variables, siempre que cuenten con un número de carácter Unicode sobre 00C0.

• El lenguaje Java es sensible al tamaño de las letras, la cual significa que

las mayúsculas son diferentes de las minúsculas. La variable x es diferente de X, rose no es Rose ni ROSE.

Por convención las variables Java tienen nombres significativos, con frecuencia formados de varias palabras combinadas. La primera palabra está en minúsculas, pero las siguientes tienen su letra inicial en mayúsculas.

Button the Button Long reallyBigNumber; • Los espacios en blanco pueden hacer que el código sea más fácil de

leer.

• Cada instrucción en Java termina con un punto y coma, de no tenerlo, no se compilará el programa Java.

• Java tiene tres clases de comentarios. Uno de ellos, los delimitadores /* y */, rodean comentarios en varias líneas. Estos comentarios no pueden anidarse, no puede tener comentarios dentro de los comentarios. También puede utilizar la doble diagonal (//) para una solo línea de comentario, donde todo el texto hasta el final de la línea se ignora. El último tipo de comentario empieza con /** y termina con */. El contenido de estos comentarios especiales los emplea el sistema Javadoc, pero a excepción de éste, se emplea de manera idéntica que el primer tipo de comentario. Javadoc se emplea para generar la documentación API del código.

1.14 VARIABLES Y TIPOS DE DATOS

13

Page 14: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Las variables son lugares en la memoria en donde pueden guardarse valores; tiene un nombre, un tipo y un valor. Antes de poder utilizar una variable, primero debe declararla y a partir de ahí es factible asignarles valores. De hecho Java posee tres clases de valores: de instancia, de clase y locales.

Las variables de instancia, se utilizan para definir atributos o el estado de un

objeto en particular. Las variables de clase son similares a las variables de instancia, con la

diferencia de que sus valores se aplican a todas las instancias de clase, en lugar de tener diferentes valores para el mismo objeto.

Las variables locales, se declaran y utilizan dentro de la definición de método,

por ejemplo, para contadores de índice dentro de un ciclo, como variables temporales, o para guardar valores que solo necesita dentro de la definición. También pueden usarse dentro de bloques({}). Una vez que el método o bloque termina su ejecución, la definición de variable y su método dejan de existir.

Aunque las tres clases de variables se declaran en forma parecida, las variables

de clase y de instancia se accesan y se asignan en formas poco diferente a las variables locales

1.14.1 DECLARACIÓN DE VARIABLES Una variable en Java es un identificador que representa una palabra de memoria que contiene información. El tipo de información almacenado en una variable sólo puede ser del tipo con que se declaró esa variable. Una variable se declara usando la misma sintaxis de C.

tipoVariable nombre;

Por ejemplo la siguiente tabla indica una declaración, el nombre de la variable introducida y el tipo de información que almacena la variable:

Declaración identificador tipo

14

Page 15: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

int i; i entero String s; s referencia a string int a[]; a referencia a arreglo de

enteros int[] b; b referencia a arreglo de

enteros Todas las variables en el lenguaje Java deben tener un tipo de dato. El tipo de la variable determina los valores que la variable puede contener y las operaciones que se pueden realizar con ella.

Existen dos categorías de datos principales en el lenguaje Java: los tipos primitivos y los tipos referenciados.

Tipos Primitivos Referencias a Objetos

int, short, byte, long Strings

char, boolean Arreglos float, double otros objetos

Los tipos primitivos contienen un sólo valor e incluyen los tipos como los enteros, coma flotante, los caracteres, etc... La tabla siguiente muestra todos los tipos primitivos soportados por el lenguaje Java, su formato, su tamaño y una breve descripción de cada uno:

Tipo Tamaño/Formato Descripción(Números enteros) byte 8-bit complemento a 2 Entero de un Byteshort 16-bit complemento a 2 Entero cortoint 32-bit complemento a 2 Enterolong 64-bit complemento a 2 Entero largo(Números reales) float 32-bit IEEE 754 Coma flotante de precisión simple double 64-bit IEEE 754 Coma flotante de precisión doble (otros tipos) char 16-bit Caracter Un sólo carácter

boolean true o false Un valor booleano (verdadero o falso)

Los ocho tipos de datos primitivos manejan tipos comunes para enteros,

números de punto flotante, caracteres y valores booléanos. Se llaman primitivos, porque están integrados en el sistema y no son objetos en realidad, lo cual hace su uso más eficiente.

15

Page 16: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Observe que estos tipos de datos son independientes de la computadora, puede confiar que su tamaño y características son consistentes en los programas Java.

Enteros

Existen cuatro tipos de enteros Java, cada uno con un rango diferente de valores, todos tienen signo.

Tipo

Tamaño

Rango

byte 8 bits -128 a 127 short

16 bits -32, 768 a 32, 767

int 32 bits -2, 147, 483, 648 a 2, 147, 483, 647 long 64 bits -9,223,372,036,854,775,808 a

9,223,372,036,854,775,807

También puede forzar a un entero más pequeño a un long al agregarle una L o l a ese número.

Los enteros también pueden representarse en sistema octal o hexadecimal: un cero indica que un número es octal (0777). Un 0x o 0X inicial, significa que se expresa en hexadecimal (0xFF, oXAF45).

Punto flotante

Los tipos de punto flotante contienen más información que los tipos enteros. Las variables de punto flotante son números fraccionarios. Existen dos subtipos de punto flotante : float y double.

Puede forzar el número al tipo float al agregarle la letra f o F a ese número ( 2.56f). Tipo Rango Double -1.7x10-308 a 1.7x10308 Float -3.4x10-38 a 3.4x1038

Booleanos

El tipo booleano tiene dos valores: True y False (verdadero y falso).

Caracter

16

Page 17: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

El tipo carácter representa un carácter con base en el conjunto de caracteres de Unicode. Este tipo se define con la palabra clave char. El valor correspondiente a un tipo de carácter debe estar encerrado entre comillas sencillas (‘). Se representa con un código de 16 bits, permitiendo 65,536 caracteres diferentes: una magnitud mayor que la del conjunto de caracteres ASCII/ANSI.

Los valores pueden aparecer en forma normal como a,b,c o pueden representarse con una codificación hexadecimal. Estos valores hexadecimales comienzan con el prefijo \u, a fin de que Java sepa que se trata de un valor hexadecimal.

Por ejemplo, el retorno de carro en hexadecimal es \u000d. El tipo char, al igual que el booleano, no tiene un gemelo numérico. Sin embargo, el tipo char sí puede convertirse a enteros en caso necesario.

char coal; coal =’c’;

La siguiente tabla muestra los códigos especiales que pueden representar

caracteres no imprimibles, así como los del conjunto de caracteres Unicode.

Escape Significado \n Línea nueva \t Tabulador \b Retroceso \r Regreso de carro \f Alimentación de forma \/ Diagonal inversa \’ Comilla sencilla \” Comilla doble \ddd Octal \xdd Hexadecimal \udddd Carácter Unicode

Cadenas y arreglos

Con excepción de los tipos entero, de punto flotante, booleano y carácter, la mayoría de los tipos restantes en Java son un objeto. En esta regla se incluyen las cadenas y los arreglos, los cuales pueden tener su propia clase.

Las cadenas son sólo una manera de representar una secuencia de caracteres.

Ya que no son tipos integrados, tendrá que declararlas mediante la clase String. Así como a los tipos char se les da un valor entre comillas sencillas (‘), a las cadenas se les dan valores contenidos entre comillas dobles (“). Es posible unir (concatenar) varias cadenas por medio del signo más (+).

Las cadenas no son simples arreglos de caracteres como lo son en C o C++,

aunque sí cuentan con las características parecidas a las de los arreglos. Puesto

17

Page 18: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

que los objetos de cadena en Java son reales, cuenta con métodos que le permiten combinar, verificar, y modificar cadenas con gran facilidad.

Las siguientes instrucciones Java declaran tres variables que usan los tipos int,

float y long:

class Class1 { public static void main (String args[ ]) { int test_score; float salary; long distancia_a_la_luna;} }

Asignación e inicialización de variables

Una vez que se ha declarado una variable puede asignarle un valor mediante el uso del operador de asignación =

Size =14; tooMuchCaffiene = true; int goo; goo=100; char coal; coal= “b”;

El siguiente ejemplo asigna valores a tres variables y luego muestra el valor de

cada una:

class Class1 { public static void main (String args[ ]) { int age = 35; double salary = 25000.75; long Distancia_a_la_luna=238857; System.out.println("Employee age: " + age); System.out.println("Employee salary: " + salary); System.out.println("Distancia_a_la_luna: " + Distancia_a_la_luna); }

} Las palabras reservadas Java no pueden usarse para nombres de variables, a

continuación se muestra una tabla con las palabras reservadas que tienen un significado especial para el compilar:

18

Page 19: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

abstract boolean break byte case cast catch Char class cons continue default do double Else extends final finally float for future generic goto if implements import inner instanceof In interface long native new null operator Outer package private protected public rest return Short static super switch synchronized this throw throws transient try var unsigned virtual void volatile while

Los tipos referenciados se llaman así porque el valor de una variable de

referencia es una referencia (un puntero) hacia el valor real. En Java tenemos los arrays, las clases y los interfaces como tipos de datos referenciados.

Las variables de tipo referencia a objetos almacenan direcciones y no valores directamente. Una referencia a un objeto es la dirección de un área en memoria destinada a representar ese objeto. El área de memoria se solicita con el operador new.

Al asignar una variable de tipo referencia a objeto a otra variable se asigna la dirección y no el objeto referenciado por esa dirección. Esto significa que ambas variables quedan referenciando el mismo objeto. La diferencia entre ambas asignaciones se observa en la siguiente figura:

19

Page 20: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Esto tiene implicancias mayores ya que si se modifica el objeto referenciado por r, entonces también se modifica el objeto referenciado por s, puesto que son el mismo objeto.

En Java una variable no puede almacenar directamente un objeto, como ocurre en C y C++.

Por lo tanto cuando se dice en Java que una variable es un string, lo que se quiere decir en realidad es que la variable es una referencia a un string. 1.14.2 NOMBRES DE VARIABLES

Un programa se refiere al valor de una variable por su nombre. Por convención, en Java, los nombres de las variables empiezan con una letra minúscula (los nombres de las clases empiezan con una letra mayúscula).

Un nombre de variable Java:

1. debe ser un identificador legal de Java comprendido en una serie de caracteres Unicode. Unicode es un sistema de codificación que soporta texto escrito en distintos lenguajes humanos. Unicode permite la codificación de 34,168 caracteres. Esto le permite utilizar en sus programas Java varios alfabetos como el Japonés, el Griego, el Ruso o el Hebreo. Esto es importante para que los programadores pueden escribir código en su lenguaje nativo.

2. no puede ser el mismo que una palabra clave o el nombre de un valor booleano (true or false)

3. no deben tener el mismo nombre que otras variables cuyas declaraciones aparezcan en el mismo ámbito.

La regla número 3 implica que podría existir el mismo nombre en otra variable que aparezca en un ámbito diferente. Por convención, los nombres de variables empiezan por un letra minúscula. Si una variable está compuesta de más de una palabra, como 'nombreDato' las palabras se ponen juntas y cada palabra después de la primera empieza con una letra mayúscula.

1.15 EXPRESIONES Y OPERADORES

Los operadores realizan algunas funciones en uno o dos operandos. Los operadores que requieren un operador se llaman operadores unarios. Por ejemplo, ++ es un operador unario que incrementa el valor su operando en uno.

20

Page 21: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Los operadores que requieren dos operandos se llaman operadores

binarios. El operador = es un operador binario que asigna un valor del operando derecho al operando izquierdo.

Los operadores unarios en Java pueden utilizar la notación de prefijo o de sufijo. La notación de prefijo significa que el operador aparece antes de su operando:

operador operando La notación de sufijo significa que el operador aparece después de su

operando: operando operador Todos los operadores binarios de Java tienen la misma notación, es decir

aparecen entre los dos operandos: op1 operador op2 Además de realizar una operación también devuelve un valor. El valor y su

tipo dependen del tipo del operador y del tipo de sus operandos. Por ejemplo, los operadores aritméticos (realizan las operaciones de aritmética básica como la suma o la resta) devuelven números, el resultado típico de las operaciones aritméticas. El tipo de datos devuelto por los operadores aritméticos depende del tipo de sus operandos: si sumas dos enteros, obtendrás un entero. Se dice que una operación evalúa su resultado.

Es muy útil dividir los operadores Java en las siguientes categorías: aritméticos, relacionales y condicionales, lógicos y de desplazamiento, y de asignación.

1.15.1 OPERADORES ARITMÉTICOS El lenguaje Java soporta varios operadores aritméticos, incluyendo + (suma), - (resta), * (multiplicación), / (división), y % (módulo), en todos los números enteros y de punto flotante. Por ejemplo, se puede utilizar este código Java para sumar dos números:

sumaEsto + Esto

O este código para calcular el resto de una división:

divideEsto % porEsto

21

Page 22: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

La siguiente tabla contempla todas las operaciones aritméticas binarias en Java:

Operador Uso Descripción + op1 + op2 Suma op1 y op2 - op1 - op2 Resta op2 de op1 * op1 * op2 Multiplica op1 y op2 / op1 / op2 Divide op1 por op2

% op1 % op2 Obtiene el resto de dividir op1 por op2

Nota: El lenguaje Java extiende la definición del operador + para incluir la concatenación de cadenas.

Los operadores + y - tienen versiones unarias que seleccionan el signo del operando:

Operador Uso Descripción

+ + op Indica un valor positivo - - op Niega el operando

Además, existen dos operadores de atajos aritméticos, ++ que incrementa en uno su operando, y -- que decrementa en uno el valor de su operando.

El siguiente listado es un ejemplo de aritmética sencilla:

class Class1 { public static void main (String args[ ]) { short x = 6; int y = 4; float a = 12.5f; float b = 7f; System.out.println(" x es " + x + ", y es " + y); System.out.println(" x + y = " + (x + y )); System.out.println(" x - y = " + (x - y )); System.out.println(" x * y = " + (x * y )); System.out.println(" x % y = " + (x % y )); System.out.println(" a es " + a + ", b es " + b); System.out.println(" a / b = " + (a / b ));

} }

El resultado que obtendrá del siguiente listado es:

22

Page 23: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

x es 6, y es 4 x + y = 10 x - y = 2 x / y = 1 x % y = 2 a es 12.5, b es 7 a / b = 1.78571

El siguiente ejemplo, muestra el resultado de varias operaciones matemáticas

simples:

class Class1 { public static void main (String args[ ]) { System.out.println(" 5+7 = " + (5+7)); System.out.println(" 12-7 = " + (12-7)); System.out.println("1.2345 * 2 = " + (1.2345 * 2 ) ); System.out.println(" 15 / 3 = " + (15 / 3 )); } }

1.15.2 INCREMENTOS Y DECREMENTOS Como en C y C++, los operadores ++ y -- se utilizan para incrementar o

decrementar un valor en 1, a diferencia de C y de C++, Java permite que el valor a modificar sea de punto flotante.

Estos operadores se pueden fijar antes o después; es decir, el ++ o el -- puede aparecer antes o después del valor que incrementa o decrece. Veamos los siguientes ejemplos:

Operador Uso Descripción

++ op ++ Incrementa op en 1; evalúa el valor antes de incrementar

++ ++ op Incrementa op en 1; evalúa el valor después de incrementar

-- op -- Decrementa op en 1; evalúa el valor antes de decrementar

-- -- op Decrementa op en 1; evalúa el valor después de decrementar

Veamos los siguientes ejemplos:

23

Page 24: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Expresión Significado Y = X++ Y=X

X=X+1 Y = ++X X=X+1 Y=X Z=X++ +Y Z = X +Y

X=X+1 Z=X + --Y Y = Y-1

Z=X+Y

El siguiente ejemplo ilustra el uso de los operadores de incremento prefijo y sufijo:

class Class1 { public static void main (String args[ ]) { int small_count = 0; int big_count = 1000; System.out.println ("small_count is " + small_count); System.out.println ("small_count is " + small_count++); System.out.println ("El valor final de samall_count es " + small_count); System.out.println ("big_count is " +big_count); System.out.println ("++big_count is " + ++ big_count); System.out.println ("El valor final de big_count es " + big_count); } }

El operador de sustracción (--) disminuye en 1 el valor de la variable. Al igual

que el operador de incremento soporta el prefijo y el sufijo:

class Class1 { public static void main (String args[ ]) { int small_count = 0; int big_count = 1000; System.out.println ("small_count is " + small_count); System.out.println ("small_count is " + small_count--); System.out.println ("El valor final de samall_count es " + small_count); System.out.println ("big_count is " +big_count); System.out.println ("--big_count is " + -- big_count); System.out.println ("El valor final de big_count es " + big_count); } }

24

Page 25: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

1.15.3 OPERADORES RELACIÓNALES Y LOGICOS

Los valores relacionales comparan dos valores y determinan la relación entre ellos. Por ejemplo, != devuelve true si los dos operandos son distintos.

Esta tabla contempla los operadores relacionales de Java:

Operador Uso Devuelve true si> op1 > op2 op1 es mayor que op2

>= op1 >= op2

op1 es mayor o igual que op2

< op1 < op2 op1 es menor que op2

<= op1 <= op2

op1 es menor o igual que op2

== op1 == op2 op1 y op2 son iguales

!= op1 != op2 op1 y op2 son distintos

Frecuentemente los operadores relacionales se utilizan con otro juego de operadores, los operadores condicionales, para construir expresiones de decisión más complejas. Uno de estos operadores es && que realiza la operación Y booleana . Por ejemplo puedes utilizar dos operadores relacionales diferentes junto con && para determinar si ambas relaciones son ciertas. La siguiente línea de código utiliza esta técnica para determinar si un índice de un array está entre dos límites, esto es, para determinar si el índice es mayor que 0 o menor que NUM_ENTRIES (que se ha definido previamente como un valor constante):

0 < index && index < NUM_ENTRIES

Observa que en algunas situaciones, el segundo operando de un operador relacional no será evaluado. Consideremos esta sentencia:

((count > NUM_ENTRIES) && (System.in.read() != -1))

Si count es menor que NUM_ENTRIES, la parte izquierda del operando de && evalúa a false. El operador && sólo devuelve true si los dos operandos son verdaderos. Por eso, en esta situación se puede deteminar el valor de && sin evaluar el operador de la derecha. En un caso como este, Java no evalúa el operando de la derecha. Así no se llamará a System.in.read() y no se leerá un carácter de la entrada estándar.

1.15.4 OPERADORES LÓGICOS

25

Page 26: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

La siguiente tabla muestra tres operadores lógicos (condicionales):

Operador Uso Devuelve true si&& op1 && op2 op1 y op2 son verdaderos

|| op1 || op2 uno de los dos es verdadero

! ! op op es falso

El operador & se puede utilizar como un sinónimo de && si ambos operadores son boléanos. Similarmente, | es un sinónimo de || si ambos operandos son boléanos.

1.15.5 OPERADORES DE BITS Los operadores de desplazamiento permiten realizar una manipulación de

los bits de los datos. Esta tabla sumariza los operadores lógicos y de desplazamiento disponibles en el lenguaje Java:

Operador Uso Descripción>> op1 >> op2 desplaza a la derecha op2 bits de op1 << op1 << op2 desplaza a la izquierda op2 bits de op1

>>> op1 >>> op2 desplaza a la derecha op2 bits de op1(sin signo)

& op1 & op2 bitwise and | op1 | op2 bitwise or ^ op1 ^ op2 bitwise xor ~ ~ op bitwise complemento

Los tres operadores de desplazamiento simplemente desplazan los bits del operando de la izquierda el número de posiciones indicadas por el operador de la derecha. Los desplazamientos ocurren en la dirección indicada por el propio operador. Por ejemplo:

13 >> 1;

desplaza los bits del entero 13 una posición a la derecha. La representación binaria del número 13 es 1101. El resultado de la operación de desplazamiento es 110 o el 6 decimal. Observe que el bit situado más a la derecha desaparece. Un desplazamiento a la derecha de un bit es equivalente, pero más eficiente que, dividir el operando de la izquierda por dos. Un desplazamiento a la izquierda es equivalente a multiplicar por dos.

Los otros operadores realizan las funciones lógicas para cada uno de los pares de bits de cada operando. La función "y" (and) activa el bit resultante si los dos operandos son 1.

op1 op2 resultado0 0 0

26

Page 27: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

0 1 0 1 0 0 1 1 1

Supóngase que se quiere evaluar los valores 12 "and" 13:

12 & 13

El resultado de esta operación es 12. ¿Por qué? Bien, la representación binaria de 12 es 1100 y la de 13 es 1101. La función "and" activa los bits resultantes cuando los bits de los dos operandos son 1, de otra forma el resultado es 0. Entonces si colocas en línea los dos operandos y realizas la función "and", puedes ver que los dos bits de mayor peso (los dos bits situados más a la izquierda de cada número) son 1 así el bit resultante de cada uno es 1. Los dos bits de menor peso se evalúan a 0 poque al menos uno de los dos operandos es 0:

1101 & 1100 ------- 1100 El operador | realiza la operación “or” inclusiva y el operador ^ realiza la

operación “or” exclusiva. “or” inclusiva significa que si uno de los dos operandos es 1 el resultado es 1.

op1 op2 resultado

0 0 0 0 1 1 1 0 1 1 1 1

“or” exclusivo significa que si los dos operandos son diferentes el resultado es 1, de otra forma el resultado es 0:

op1 op2 resultado

0 0 0 0 1 1 1 0 1 1 1 0

Y finalmente el operador complemento invierte el valor de cada uno de los bites del operando: si el bit del operando es 1 el resultado es 0 y si el bit del operando es 0 el resultado es 1.

27

Page 28: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

1.15.6 OPERADORES DE ASIGNACIÓN

Puedes utilizar el operador de asignación =, para asignar un valor a otro. Además del operador de asignación básico, Java proporciona varios operadores de asignación que permiten realizar operaciones aritméticas, lógicas o de bits y una operación de asignación al mismo tiempo. Específicamente, supóngase que se quiere añadir un número a una variable y asignar el resultado dentro de la misma variable, como esto:

i = i + 2;

Se puede hacer lo mismo utilizando el operador +=.

i += 2;

Las dos líneas de código anteriores son equivalentes.

La siguiente tabla lista los operadores de asignación y sus equivalentes:

Operador Uso Equivale a += op1 += op2 op1 = op1 + op2 -= op1 -= op2 op1 = op1 - op2 *= op1 *= op2 op1 = op1 * op2 /= op1 /= op2 op1 = op1 / op2

%= op1 %= op2 op1 = op1 % op2 &= op1 &= op2 op1 = op1 & op2 |= op1 |= op2 op1 = op1 | op2 ^= op1 ^= op2 op1 = op1 ^ op2

<<= op1 <<= op2 op1 = op1 << op2 >>= op1 >>= op2 op1 = op1 >> op2

>>>= op1 >>>= op2 op1 = op1 >>> op2

1.16 Expresiones

Las expresiones realizan el trabajo de un programa Java. Entre otras cosas, las expresiones se utilizan para calcular y asignar valores a las variables y para controlar el flujo de un programa Java. El trabajo de una expresión se divide en dos partes: realizar los cálculos indicados por los elementos de la expresión y devolver algún valor.

28

Page 29: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Definición: Una expresión es una serie de variables, operadores y llamadas a métodos (construida de acuerdo a la sintaxis del lenguaje) que evalúa a un valor sencillo.

El tipo del dato devuelto por una expresión depende de los elementos utilizados en la expresión. La expresión count++ devuelve un entero porque ++ devuelve un valor del mismo tipo que su operando y count es un entero. Otras expresiones devuelven valores boléanos, cadenas, etc... Una expresión de llamada a un método devuelve el valor del método; así el tipo de dato de una expresión de llamada a un método es el mismo tipo de dato que el valor de retorno del método. El método System.in.read() se ha declarado como un entero, por lo tanto, la expresión System.in.read() devuelve un entero.

La segunda expresión contenida en la sentencia System.in.read() != -1 utiliza el operador !=. Recuerda que este operador comprueba si los dos operandos son distintos. En esta sentencia los operandos son System.in.read() y -1. System.in.read() es un operando válido para != porque devuelve un entero. Así System.in.read() != -1 compara dos enteros, el valor devuelto por System.in.read() y -1. El valor devuelto por != es true o false dependiendo de la salida de la comparación.

Como has podido ver, Java te permite construir expresiones compuestas y sentencias a partir de varias expresiones pequeñas siempre que los tipos de datos requeridos por una parte de la expresión correspondan con los tipos de datos de la otra. También habrás podido concluir del ejemplo anterior, el orden en que se evalúan los componentes de una expresión compuesta. Por ejemplo, toma la siguiente expresión compuesta:

x * y * z En este ejemplo particular, no importa el orden en que se evalúe la

expresión porque el resultado de la multiplicación es independiente del orden. La salida es siempre la misma sin importar el orden en que se apliquen las multiplicaciones. Sin embargo, esto no es cierto para todas las expresiones. Por ejemplo, esta expresión obtiene un resultado diferente dependiendo de si se realiza primero la suma o la división:

x + y / 100 Puedes decirle directamente al compilador de Java cómo quieres que se

evalúe una expresión utilizando los paréntesis ( y ). Por ejemplo, para aclarar la sentencia anterior, se podría escribir: (x + y)/ 100. Si no le dices explícitamente al compilador el orden en el que quieres que se realicen las operaciones, él decide basándose en la precedencia asignada a los operadores y otros elementos que se utilizan dentro de una expresión. Los operadores con una precedencia más alta se evalúan primero. Por ejemplo. el operador división tiene una precedencia mayor que el operador suma, por eso, en la expresión anterior x + y / 100, el compilador evaluará primero y / 100. Así

x + y / 100

29

Page 30: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

es equivalente a: x + (y / 100) Para hacer que tu código sea más fácil de leer y de mantener deberías

explicar e indicar con paréntesis los operadores que se deben evaluar primero. La tabla siguiente muestra la precedencia asignada a los operadores de Java. Los operadores se han listado por orden de precedencia de mayor a menor. Los operadores con mayor precedencia se evalúan antes que los operadores con un precedencia relativamente menor. Lo operadores con la misma precedencia se evalúan de izquierda a derecha.

Precedencia de Operadores en Java

operadores sufijo [] . (params) expr++ expr-- operadores unarios ++expr --expr +expr -expr ~ !

creación o tipo new (type)expr multiplicadores * / % suma/resta + - desplazamiento << >> >>> relacionales < > <= >= instanceof igualdad == != bitwise AND & bitwise exclusive OR ^

bitwise inclusive OR |

AND lógico && OR lógico || condicional ? :

asignación = += -= *= /= %= ^= &= |= <<= >>= >>>=

1.17 SENTENCIAS DE CONTROL DE FLUJO

30

Page 31: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Las sentencias de control de flujo determinan el orden en que se ejecutarán las otras sentencias dentro del programa. El lenguaje Java soporta varias sentencias de control de flujo, incluyendo:

Sentencias palabras clave condicionales if-else, switch-case

bucles for, while, do-while excepciones try-catch-finally, throw miscelaneas break, continue, label:, return

1.17.1 CONDICIONAL if

La condicional if permite ejecutar diferentes partes del código con base en una simple prueba, contiene la palabra clave if, seguida de una prueba booleana y de un enunciado a ejecutar si la prueba es verdadera:

La diferencia entre los condicionales en Java y C o C++ es que la prueba debe

regresar un valor booleano (falso o verdadero). El formato de la instrucción if es la siguiente:

El siguiente ejemplo usa if para comparar el valor almacenado en la variable

TestScore con 90. Si TestScore es mayor o igual a 90, se muestra un mensaje de felicitación indicando que obtuvo una A, de otra manera, si el valor es menor de 90 el programa termina.

class Class1 { public static void main (String args[ ]) { int TestScore = 95; if (TestScore >= 90)

System.out.println ("Felicidades obtuviste una A"); } }

If (condición_es_verdadera)

sentencia;

Para lograr que se ejecute el conjunto de instrucciones en el caso de la condición falsa, debe utilizarse else. El formato de la instrucción else es como sigue:

31

Page 32: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

If (condición_es_verdadera) Sentencia; else Sentencia;

class Class1 { public static void main (String args[ ]) { int TestScore = 95;

if (TestScore >= 90){ System.out.println ("Felicidades obtuviste una A"); System.out.println ("Tu puntuación es" + TestScore); } else { System.out.println ("Deberías estudiar mas"); System.out.println ("Perdiste de tu puntuación" + (TestScore - 10) ); } } } Cuando se lee información del teclado, Java prueba si hay problemas durante la

entrada (por ejemplo, no hay mas datos a introducir). Si ocurre un problema Java genera una excepción, la cual es un caso excepcional durante la ejecución de un programa. Para ejecutar un programa que reciba datos del teclado, puede hacerlo indicándole al compilador que está consiente de los problemas que pueden ocurrir, esto se logrará incluyendo en el programa las palabras clave throws IOException.

El siguiente programa el usuario va a teclear calificaciones de letra que se

introducen en el programa para calcular el promedio en las calificaciones de un grupo:

import java.io.*; class Class1 { public static void main (String args[ ] ) throws IOException { int counter, grade, total, average; // fase de inicialización total = 0; counter = 1; //fase de procesamiento while (counter <= 10 ) {

32

Page 33: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

System.out.print ("Teclee calificación de letra: " ); System.out.flush ( ); grade = System.in.read ( ); if (grade == 'A' ) total = total + 4; else if (grade == 'B' ) total = total + 3; else if (grade == 'C' ) total = total + 2; else if (grade == 'D' ) total = total + 1; else if (grade == 'F' ) total = total + 0; System.in.skip ( 2 ); //Saltar el carácter de nueva línea counter = counter + 1;

} //fase de terminación average = total / 10; //divisió entera System.out.println ("El promedio del grupo es " + average);

} }

El operador condicional Una alternativa para utilizar las palabras claves if y else en un enunciado

condicional es usar el operador condicional también conocido como el operador ternario.

El operador condicional es más útil para condicionales cortos o sencillos, y

tiene esta apariencia: test ? trueresult : falseresult

El test es una expresión que regresa true o false, al igual que en la prueba del

enunciado if. En el siguiente ejemplo, este condicional prueba los valores de x y y, regresa al más pequeño de los dos y asigna ese valor a la variable smaller:

int smaller = x < y ? x : y;

1.17.2 CONDICIONALES switch

Este condicional nos sirve para probar alguna variable contra algún valor, y si no coincide con ese valor, probarla con otro y así sucesivamente.

33

Page 34: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

switch (test) { case valueOne:

resultOne; break; case valueTwo:

resultTwo; break; case valueThree:

resultThree; break;

… default: defaultresult; }

En el siguiente ejemplo, se usa la instrucción switch para mostrar un mensaje,

con base en la nota actual del estudiante: class Class1 { public static void main (String args[ ] ) { char grade = 'B'; switch (grade){ case 'A' :System.out.print("Felicidades obtuviste una A"); break; case 'B' :System.out.print("Bien, obtuviste una B"); break; case 'C' :System.out.print("Suficiente obtuviste una C"); break; case 'D' :System.out.print("No hay excusas, estudia más "); break; }

} } La desventaja de este tipo de pruebas, es que no pueden trabajar con valores

de tipos deferentes de int, esto limita a trabajar con if anidado para la utilización de valores grandes como los que maneja (long y float).

También puede utilizar este condicional cuando quiera que más de una opción

ejecuten la misma línea de código, esto lo podrá hacer al omitir la sentencia break, lo que le permitirá desplazarse hasta que encuentre el condicional de paro.

switch (x){ case2: case4:

34

Page 35: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

case6: case8: system.out.println(“x es un número par.”); break; default: system.out.println(“x es cualquier número.”); }

1.17.3 CICLOS for

Este ciclo repite una declaración o un bloque de enunciados un número de veces hasta que una condición se cumple. Estos ciclos con frecuencia se utilizan para una iteración sencilla en donde usted repite un bloque de enunciados un cierto número de veces y después se detiene; aunque también puede usarlos para cualquier clase de ciclos. El ciclo for en Java tiene esta apariencia:

El inicio del ciclo for tiene tres partes:

for (initialization; test; increment){ staments }

1. initialization es una expresión que inicializa el principio del ciclo. Si tiene un índice, esta expresión puede declararla e inicializarla. Las variables que se declararán dentro del ciclo; dejan de existir después de acabar la ejecución del ciclo.

2. test es la prueba que ocurre después de cada vuelta del ciclo. La prueba puede ser una expresión booleana o una función que regresa un valor booleano. Si la prueba es verdadera el ciclo se ejecutará, de lo contrario el ciclo detiene su ejecución.

3. increment es una expresión o llamada de función. Por lo común el incremento se utiliza para cambiar el valor del índice del ciclo a fin de acercar el estado del ciclo a false y se complete.

La parte del enunciado del ciclo for son los enunciados que se ejecutan cada

vez que se itera el ciclo, al igual que con if puede incluir un solo enunciado o un bloque. Cualquiera de las partes de un ciclo for pueden ser enunciados vacíos, es decir, puede incluir un solo punto y coma sin ninguna expresión o declaración, y esa parte del ciclo se ignorará.

Es importante recordar que no debe de colocar punto y coma después de la

primera línea del ciclo for:

35

Page 36: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

for (i = 4001 ; i<=10;i++); system.out.println(“Hola”);

En este caso como el primer punto y coma termina el ciclo con enunciado vacío,

el ciclo no hace nada en general, la forma correcta para la ejecución de este ciclo sería:

for (i = 4001 ; i<=10;i++)

system.out.println(“Hola”);

Ejemplos:

En el siguiente ejemplo el ciclo for muestra los números desde el 1 hasta el valor contenido en la variable EndCount:

public class Class1 { public static void main (String[] args) { int count ; int end_count = 10; for (count = 1; count <=end_count ; count++) System.out.print("" + count); }

}

El programa siguiente efectúa repeticiones de 1 hasta 10, mostrando y sumando cada número a un gran total:

public class Class1 { public static void main (String[] args) { int count ; int total = 0; int end_count = 10; for (count = 1; count <=end_count ; count++) { System.out.println("Sumando " + count + "hasta " + total); total = total + count ; } System.out.println("La suma total es : " + total); } }

El ciclo for no tiene la limitación de incrementar solo en 1 la variable. El

siguiente programa muestra cada quinto número entre uno y cincuenta:

36

Page 37: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

public class Class1 { public static void main (String[] args) { int count, y ; for (count = 0, y = 15; count <=50 ; count += 5, y += 15) { System.out.println( " count: " + count); } }

} Dentro de un ciclo for no es obligatorio que el conteo sea ascendente, el

siguiente programa usa el ciclo for para mostrar en forma descendente los números del 1 al 10:

public class Class1 { public static void main (String[] args) { int count, y ; for (count =1 0, y = 15; count <=50 ; count --, y += 15) System.out.println( " count: " + count); }

} Los ciclos for no están restringidos a usar valores de tipo int en las variables de

control de ciclos:

public class Class1{ public static void main (String[] args) { int x ; char letter; float value; for (letter = 'A', x = 5;letter <= 'Z' ;letter ++) System.out.println(""+ letter ); for (value = 0 , x = 5; value <1.0; value += 0.1, x +=20) System.out.println(""+ value); }

}

1.17.4 CICLOS while y do

Al igual que los ciclos for, le permiten a un código de bloque Java ejecutarse de manera repetida hasta encontrar una condición específica. Utilizar uno de estos tres ciclos solo es cuestión de estilo de programación

Los ciclos while y do son exactamente los mismos que en C y C++, a excepción

de que su condición de prueba debe ser un booleano.

37

Page 38: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

1.17.4.1 CICLOS while

Se utilizan para repetir un enunciado o bloque de enunciados, siempre que una

condición en particular sea verdadera. Los ciclos while tienen esta apariencia: while (condition) {

bodyOfLoop }

La condition es una expresión booleana. Si regresa true, el ciclo while ejecutará los enunciados dentro del bodyOfLoop, y después prueba la condición de nuevo, repitiéndola hasta que sea falsa.

Si la condición es en un principio falsa la primera vez que se prueba, el cuerpo

del ciclo while nunca se ejecutará. Si necesita que el ciclo por lo menos se ejecute una vez, tiene dos opciones a elegir:

1. Duplicar el cuerpo del ciclo fuera del ciclo while. 2. Utilizar un ciclo do. El ciclo do se considera la mejor opción en ambas.

1.17.4.2 CICLOS do... while

El ciclo do es como while, excepto que do ejecuta un enunciado o bloque hasta

que la condición es false. La principal diferencia es que los ciclos while prueban la condición antes de realizar el ciclo, lo cual hace posible que el cuerpo del ciclo nunca se ejecute si la condición es falsa la primera vez que se prueba. Así los ciclos do ejecutan el cuerpo del ciclo por lo menos una vez antes de probar la condición. Los ciclos do se ven así:

do{

bodyOfLoop } while (condition);

El siguiente programa muestra los resultados obtenidos de un examen:

import java.io.*; public class Class1{ public static void main (String args [ ] ) throws IOException { int passes = 0, failures = 0, student = 1, result; while (student <= 10 ){

38

Page 39: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

System .out.print( "Teclee resultado (1= aprobó, 2= reprobó ): " ); System.out.flush ( ); result = System.in.read ( ); if (result =='1' ) passes = passes +1; else failures = failures + 1; student = student + 1; System.in.skip (2); } System.out.println ("Aprobados " + passes ); System.out.println ("Reporbados " + failures ); if ( passes > 8 ) System.out.println ("Aumentar la colegiatutra " ); }

}

Programa que calcula el promedio de un grupo.

import java.io.*; public class Average{ public static void main (String args[ ] ) throws IOException { double average; int counter, grade, total; // fase de inicialización total = 0; counter = 0; //fase de procesamiento

System.out.print (“Teclee calificación de letra, Z para terminar: “ ); System.out.flush ( ); grade = System.in.read ( );

while (grade != 10 ) {

39

Page 40: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

if (grade == ‘A’ )

total = total + 4; else if (grade == ‘B’ )

total = total + 3; else if (grade == ‘C’ )

total = total + 2; else if (grade == ‘D’ )

total = total + 1; else if (grade == ‘F’ )

total = total + 0;

System.in.skip ( 1 ); //Saltar el carácter de nueva línea counter = counter + 1; System.out.print(“Teclee calificación de letra, Z para terminar: “ ); System.out.flush( ); grade = System.in.read ( ); }

//fase de terminación

if (counter != 0 ) { average = (double) total / counter; System.out.println (“El promedio del grupo es “ + average);

} else

System.out.println( “No se introducieron calificaciones” ); }

}

1.17.4.3 COMO SALIR DE LOS CICLOS

En todos los ciclos (for, while y do), éstos se terminan cuando la condición que prueba se cumple. Para salir de manera anticipada del ciclo, puede utilizar las palabras clave break y continue.

El uso de continue es similar al de break, a excepción de que en lugar de

detener por completo la ejecución del ciclo, este inicia otra vez en la siguiente iteración. Para los ciclos do y while, esto significa que la ejecución del bloque se inicia de nuevo. Para los ciclos for la expresión de incremento se evalúa y después el bloque se ejecuta .continue es útil cuando se desea tener elementos especiales de condición dentro de un ciclo.

40

Page 41: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Ejemplo que muestra el uso de la instrucción break:

import java.io.*; public class Class1{ public static void main (String args[ ] ) throws IOException { int count, xpos = 25; for (count = 1; count <= 10; count++) { if (count ==5) break; // Romper el ciclo sólo si count ==5 System.out.println(Integer.toString (count )); } System.out.print ("Me salí del ciclo con count = " + count); } }

Ejemplo que muestra el uso de la instrucción continue:

import java.io.*; public class Class1{ public static void main (String args[ ] ) throws IOException { int xPos = 25; for ( int count = 1; count <= 10; count++ ) { if ( count ==5 ) continue; //saltarse el resto del código sólo si count==5 System.out.println(Integer.toString ( count )); } System.out.println("Usé continue para no I mprimir 5"); } }

41

Page 42: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

1.17.5 EXCEPCIONES Java implementa excepciones para facilitar la construcción de código robusto.

Cuando ocurre un error en un programa, el código que encuentra el error lanza una excepción, que se puede capturar y recuperarse de ella. Java proporciona muchas excepciones predefinidas.

try-catch-throw try { // Normalmente este código corre desde arriba del bloque hasta // el final sin problemas. Pero algunas veces puede ocasionar // excepciones o salir del bloque con una sentencia break, // continue, o return. sentencias; } catch( AlgunaException e1 ) { // El manejo de un objeto de excepcion el de tipo AlgunaExcepcion // o de una subclase de ese tipo. sentencias; catch( OtraException e2 ) { // El manejo de un objeto de excepción e2 de tipo OtraExcepcion // o de una subclase de ese tipo. }

try (tratar) La cláusula try simplemente establece un bloque de código que habrá de

manejar todas las excepciones y salidas anormales(vía break, continue o propagación de excepción). La clásula try, por sí misma, no hace nada interesante.

catch (atrapar) Un bloque try puede ir seguido de cero o más cláusulas catch, las cuales

especifican el código que manejará los distintos tipos de excepciones. Las cláusulas catch tienen una sintaxis inusual: cada una se declara con un argumento, parecido a un argumento de método. Este argumento debe ser del tipo Throwable o una subclase. Cuando ocurre una excepción, se invoca la primera cláusula catch que tenga un argumento del tipo adecuado. El tipo de argumento debe concordar con el tipo de objeto de excepción, o ser una superclase de la excepción. Este argumento catch es válido sólo dentro del bloque catch, y hacer referencia al objeto de excepción real que fue lanzado.

42

Page 43: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

1.17.6 CONTROL GENERAL DEL FLUJO break [etiqueta] continue [etiqueta] return expr; etiqueta: sentencia; En caso de que nos encontremos con bucles anidados, se permite el uso de

etiquetas para poder salirse de ellos, por ejemplo:

uno: for( ) { dos: for( ) { continue; // seguiría en el bucle interno continue uno; // seguiría en el bucle principal break uno; // se saldría del bucle principal } }

Si se declara una función para que devuelva un entero, es imprescindible que

se coloque un return final para salir de esa función, independientemente de que haya otros en medio del código que también provoquen la salida de la función.

int func() { if ( a == 0 ) return 1; return 0; // es imprescindible porque se retorna un entero }

1.18 ARREGLOS Y CADENAS Al igual que otros lenguajes de programación, Java permite juntar y manejar

múltiples valores a través de un objeto array (matriz). También se pueden manejar datos compuestos de múltiples caracteres utilizando el objeto String (cadena).

1.18.1 ARREGLOS (ARRAYS)

Esta sección te enseñará todo lo que necesitas para crear y utilizar arrays en tus programas Java.

Como otras variables, antes de poder utilizar un array primero se debe declarar. De nuevo, al igual que otras variables, la declaración de un array tiene dos componentes primarios: el tipo del array y su nombre. Un tipo de array incluye el tipo de dato de los elementos que va contener el array. Por ejemplo, el tipo de dato para un array que sólo va a contener elementos enteros es un array de

43

Page 44: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

enteros. No puede existir un array de tipo de datos genérico en el que el tipo de sus elementos esté indefinido cuando se declara el array. Aquí tienes la declaración de un array de enteros:

int[] arrayDeEnteros;

La parte int[] de la declaración indica que arrayDeEnteros es un array de enteros. La declaración no asigna ninguna memoria para contener los elementos del array. Si se intenta asignar un valor o acceder a cualquier elemento de arrayDeEnteros antes de haber asignado la memoria para él, el compilador dará un error como este y no compilará el programa: testing.java:64: Variable arraydeenteros may not have been initialized. Para asignar memoria a los elementos de un array, primero se debe ejemplarizar el array. Se puede hacer esto utilizando el operador new de Java. (Realmente, los pasos que se deben seguir para crear un array son similares a los se deben seguir para crear un objeto de una clase: declaración, ejemplarización e inicialización.

La siguiente sentencia asigna la suficiente memoria para que arrayDeEnteros pueda contener diez enteros.

int[] arraydeenteros = new int[10] En general, cuando se crea un array, se utiliza el operador new, más el tipo de dato de los elementos del array, más el número de elementos deseados encerrado entre cochetes cuadrados ('[' y ']'). TipodeElemento[] NombredeArray = new TipodeElementos[tamanoArray] Ahora que se ha asignado memoria para un array ya se pueden asignar valores a los elemetos y recuperar esos valores:

for (int j = 0; j < arrayDeEnteros.length; j ++) { arrayDeEnteros[j] = j; System.out.println("[j] = " + arrayDeEnteros[j]); }

Como se puede ver en el ejemplo anterior, para referirse a un elemento del array, se añade corchetes cuadrados al nombre del array. Entre los corchetes caudrados se indica (bien con una variable o con una expresión) el índice del elemento al que se quiere acceder. Observa que en Java, el índice del array empieza en 0 y termina en la longitud del array menos uno. Hay otro elemento interesante en el pequeño ejemplo anterior. El bucle for itera sobre cada elemento de arrayDeEnteros asignándole valores e imprimiendo esos valores. Observa el uso de arrayDeEnteros.length para obtener el tamaño real del array. length es una propiedad proporcionada para todos los arrays de Java. Los arrays pueden contener cualquier tipo de dato legal en Java incluyendo los tipos de referencia como son los objetos u otros array. Por ejemplo, el siguiente ejemplo declara un array que puede contener diez objetos String.

String[] arrayDeStrings = new String[10];

44

Page 45: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Los elementos en este array son del tipo referencia, esto es, cada elemento contiene una referencia a un objeto String. En este punto, se ha asignado suficiente memoria para contener las referencias a los Strings, pero no se ha asignado memoria para los propios strings. Si se intenta acceder a uno de los elementos de arraydeStrings obtendrá una excepción 'NullPointerException' porque el array está vacio y no contiene ni cadenas ni objetos String. Se debe asignar memoria de forma separada para los objetos String:

for (int i = 0; i < arraydeStrings.length; i ++) { arraydeStrings[i] = new String("Hello " + i); }

1.18.2 Strings Una secuencia de datos del tipo carácter se llama un string (cadena) y en el

entorno Java está implementada por la clase String (un miembro del paquete java.lang).

String[] args; Este código declara explícitamente un array, llamado args, que contiene objetos del tipo String. Los corchetes vacios indican que la longitud del array no se conoce en el momento de la compilación, porque el array se pasa en el momento de la ejecución.

El segundo uso de String es el uso de cadenas literales (una cadena de caracteres entre comillas " y "):

"Hola mundo!" El compilador asigna implicitamente espacio para un objeto String cuando encuentra una cadena literal. Los objetos String son inmutables - es decir, no se pueden modificar una vez que han sido creados. El paquete java.lang proporciona una clase diferente, StringBuffer, que se podrá utilizar para crear y manipular caracteres al vuelo. Concatenación de Cadenas Java permite concatenar cadenas facilmente utilizando el operador +. El siguiente fragmento de código concatena tres cadenas para producir su salida:

"La entrada tiene " + contador + " caracteres." Dos de las cadenas concatenadas son cadenas literales: "La entrada tiene " y " caracteres.". La tercera cadena - la del cadena y luego se concatena con las otras.

45

Page 46: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

2. GENERALIDADES 2.1 Características de las Estructuras de Datos (ED).

Se sabe que los programas actúan sobre la información para lograr el propósito de resolver un problema. Tal información se dispondrá de una manera particular, organizada en forma que se faciliten las operaciones que conforman el algoritmo para lograr la solución. Hablar de organización conlleva a distinguir aquellos elementos que nos permiten describir las relaciones presentes, en este caso, alrededor de la información. El término Estructura de Datos refiere a dos partes de la Organización de la Información.

ación para lograr el propósito de resolver un problema. Tal información se dispondrá de una manera particular, organizada en forma que se faciliten las operaciones que conforman el algoritmo para lograr la solución. Hablar de organización conlleva a distinguir aquellos elementos que nos permiten describir las relaciones presentes, en este caso, alrededor de la información. El término Estructura de Datos refiere a dos partes de la Organización de la Información.

INFORMACIÓN

PROGRAMA Solución de un problema

Organización Lógica: Involucra todo aquello que tenga que ver con las partes de cada elemento, tipo de los elementos, referencia a alguno o algunos elementos, cantidad de los elementos que contiene la estructura, relaciones entre los elementos, etc.

Organización Lógica: Involucra todo aquello que tenga que ver con las partes de cada elemento, tipo de los elementos, referencia a alguno o algunos elementos, cantidad de los elementos que contiene la estructura, relaciones entre los elementos, etc. Organización Física: Se refiere a todo aquello que tenga que ver con la ubicación de la información en la memoria y la forma de almacenarla de acuerdo a sus dominios, es decir, traducir los aspectos de la organización lógica en direcciones de memoria y tamaños, reflejando las relaciones de los datos dentro de la memoria de la máquina.

Organización Física: Se refiere a todo aquello que tenga que ver con la ubicación de la información en la memoria y la forma de almacenarla de acuerdo a sus dominios, es decir, traducir los aspectos de la organización lógica en direcciones de memoria y tamaños, reflejando las relaciones de los datos dentro de la memoria de la máquina.

Estructura de Datos

+

Las operaciones que se pueden realizar sobre los elementos de la estructura

Se caracteriza parcialmente por sus dos tipos de organización

Existen tres operaciones básicas para manejar todo tipo de ED, a saber: Existen tres operaciones básicas para manejar todo tipo de ED, a saber:

Eliminar un elemento Eliminar un elemento Añadir un elemento Añadir un elemento Buscar un elemento Buscar un elemento

De lo anterior se tiene que, al cambiar la organización de la información, entonces debemos cambiar los algoritmos que la manipulan, de aquí que se tenga lo siguiente: De lo anterior se tiene que, al cambiar la organización de la información, entonces debemos cambiar los algoritmos que la manipulan, de aquí que se tenga lo siguiente:

46

Page 47: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Ejemplo: Sea un arreglo de 50 elementos de tipo entero en donde se añadan y eliminen elementos. A[1] A[2]

A[3]

A[50] Organización Lógica Arreglo de un índice; Inicio: 1, Fin: 50; Tipo de elementos: Entero; Organización Física Almacenamiento: Secuencial Dirección Inicial: dir(A) Tamaño del elemento: 2 bytes; Número de elementos: 50; Analicemos la operación de eliminación de un elemento en dos formas. a) por desplazamiento. b) por marca.

47

E.D. Algoritmo Vínculo

Plantea un concepto más amplio que es el tipo

∼ ∼ ∼ ∼. .

∼ ∼∼ ∼. . .

m

∼∼

Page 48: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

elimina1(x) { busca1 (x,pos); SI pos = 0 ENTONCES ESCRIBE (“no se encuentra”,x) SINO { PARA k = pos HASTA pfin-1 HACER A[k] = A[k+1] pfin = pfin-1 } } elimina2(x) { busca2 (x, pos); SI pos = 0 ENTOCES ESCRIBE (“no se encuentra”,x) SINO A[pos].marca = m; } Podemos observar en los anteriores algoritmos que el segundo es más rápido, mientras que el primero se limita a ocupar el espacio mínimo.

En conclusión, viendo a las ED como entidades que integran sus operaciones, siempre se encontraran dos parámetros antagónicos, a saber: cantidad de memoria que consume la estructura contra tiempo de realización de una operación.

2.2 Tipos de Estructuras de Datos y sus Dominios

A partir de las diferentes formas que existen para organizar la información tenemos que en cuanto a la Organización Lógica los diversos lenguajes de programación proporcionan los elementos básicos de información y constructores para definir ED.

Tomando como base los tipos de datos que la mayoría de los lenguajes proporcionan, tenemos que: los tipos escalares son: Enteros, Reales, Booleanos, Carácter (además del tipo enumerado) y también se proporcionan constructores para formar estructuras tales como: - ARREGLOS: que permiten definir estructuras con n elementos, todos del mismo tipo.

48

Page 49: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

- REGISTROS*: que permiten definir estructuras con n elementos, llamados campos, que pueden ser de cualquier tipo y,

- CONJUNTOS*: que permiten definir estructuras con elementos de diferentes tipos, tales estructuras pueden ser manipuladas a través de las operaciones de conjuntos.

Tales constructores se caracterizan en función de los dominios de los datos que

puedan contener, así para los tipos básicos tenemos: Enteros (Z) Reales ( R ) Booelanos (B) Carácter ( C )

De lo anterior, tenemos que para un arreglo de n enteros le corresponde el dominio Z , es decir elementos de la forma (z1, z2, ... , zn ) zi ∈ Z n

El REGISTRO proporciona heterogeneidad en este producto cartesiano; por

ejemplo: Sea x un REGISTRO con los campos y de tipo ENTERO z de tipo REAL tiene como dominio Dom (x) = Z X R, cuyos elementos son de la forma (a,b) a∈ Z y b∈R. En general, tenemos que por ejemplo: Sea k un REGISTRO con los campos l de tipo booleano

m de tipo x Dom(k) = B X Dom(x) Es decir, el producto cartesiano de cada campo definido.

En el caso de los registros variantes tendremos en los dominios la unión de los dominios de cada campo o campos que pueda formar el registro. Entonces para

Sea u un REGISTRO, tal que, en CASO de que c (de tipo booleano) sea igual a Cierto : el registro tendrá el campo a de tipo carácter Falso : el registro tendrá el campo b de tipo entero Su dominio resulta: Dom (u) = {(true,a) a ∈ C} U {(false, b) b∈ Z}

* Cabe señalar que aunque en lenguajes como C, C++ y JAVA no existen específicamente los REGISTROS y los CONJUNTOS y en otros lenguajes si, se mencionan aquí para establecer una diferencia entre algunas de las estructuras que serán consideradas en este tema. Para el lector no deberá implicar dificultad alguna crear estructuras, con tales características, con el constructor struct de dichos lenguajes.

49

Page 50: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Para poder generalizar esto último, se deben considerar los elementos de referencia (apuntadores), para ello consideremos que en el siguiente ejemplo el operador ^ define tales elementos:

Sea p un REGISTRO con los campos xc, yc de tipo Real Sea l-p un REGISTRO con los campos punto de tipo p sp de tipo ^l-p

Sea type una cadena que puede tomar los valores recta o círculo o polígono Sea f un REGISTRO con los campos c de tipo color g de tipo Real en CASO de que figura (de tipo type) sea igual a recta: el registro tendrá los campos p1, p2 de tipo p círculo: el registro tendrá los campos p1, p2 de tipo p y ángulo de

tipo Real polígono: el registro tendrá el campo lista-p de tipo l-p Dom (f) = Dom (color) X R X ({recta} X Dom (p)2 U {círculo} X Dom (p)2 X R U {polígono} X Dom (l-p)) Dom (l-p) define un dominio recursivo: Dom (l-p) = Dom (p) x dom (^l-p)

Los elementos de Dom (^l-p) son de la forma Dom(^l-p) = {nil} U [Dom (l-p)] Los símbolos “[“ y “]” se emplean para hacer referencia a un elemento del dominio que encierran. Tal referencia a nivel de conjuntos es irrelevante ya que los conjuntos [Dom (w)] y Dom(w) son isomorfos, se tiene que Dom (^l-p) es isomorfo a Dom(p) * Dom (l-p). Este tipo de dominios consideran como elementos secuencias finitas de la forma: (p, [ ]), (p, [p, []]),…,(p, [p,…,[p, []] …) donde [] significa nil.

En resumen, todo lo anterior tiene que ver con la Organización Lógica de las Estructuras de Datos. Veamos ahora una característica general de la Organización Física.

50

Page 51: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Como hemos visto, la organización física tiene que ver con el “lugar” y la “forma” dentro de la memoria donde se almacena la información. Tanto el “lugar” como la “forma” son representados por el “espacio” el cual puede ser “fijo” o “variable”. De lo anterior, se tiene que las E.D. se clasifican en:

a) Estáticas (Espacio Fijo) b) Dinámicas (Espacio Variante)

Las estructuras de datos del tipo a) están orientados a mantener información, por lo

general, para consulta. Se puede observar que es en las estructuras de datos del tipo b) en donde es posible variar el tamaño de los elementos del dominio de tales estructuras. Esto, nos ofrece una guía muy general de selección de las estructuras de datos para las aplicaciones, ya que es necesario analizar los algoritmos que actúan en ellas. Catálogo de algunas estructuras de datos conocidas. Dinámicas:

- Lista ligada simple. A D

Circular AD

sublistas AP/A Digráficas

árboles Ortogonal izarr coreD

Ligada doble deD iz Anular aba der Darr iz

Estáticas: - Arreglo Unidimensional Multidimensional - Pila Multipila - Cola Cola doble

51

Page 52: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

2.3 Almacenamiento Secuencial

Las estructuras de datos con este tipo de almacenamiento son los más fáciles de manejar debido a que la memoria de la computadora tiene una estructura secuencial, algunas estructuras de datos de este tipo son:

a) Arreglo b) Multipila c) Cola doble d) Conjuntos

En esta ocasión solo veremos las estructuras de datos del tipo a). Arreglos: Organización Lógica: Dimensiones. Límite inferior y superior de cada dimensión. Tipo de elementos. Organización Física: Dirección inicial (de un intervalo de memoria) Tamaño de los elementos. Orden de las dimensiones.

Desplazamientos Las operaciones con los arreglos son:

a) Recuperación de uno de sus elementos. b) Actualización de un elemento.

Ambas operaciones se realizan en función de los índices que señalan la ubicación

del elemento. Arreglos unidimensionales Para propósitos de claridad tomemos la declaración siguiente: Sea A un ARREGLO [1..20] con elementos de tipo Caracter

Reserva un intervalo de memoria de 20 lugares consecutivos a partir de una dirección que denotaremos por DirA.

∼ ∼∼∼ . .

A[1]

A[2] A[3] A[4]

DirA

DirA+1 DirA+2 DirA+3

Intervalo de memoria

[ DirA, DirA + 19 ]

52

Page 53: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Suponiendo que el direccionamiento sea a nivel de bytes.

Las operaciones a) y b) se reducen al cálculo del así llamado Polinomio de Direccionamiento (Pd).

El Pd obtiene la dirección absoluta de un elemento del arreglo dados sus índices. Por ejemplo: Si nos referimos a A[7] ⇒ Pd (A[7]) = DirA + 6 Pd (A[x]) = DirA + x-1. ⇒ En general se tiene que si B es un ARREGLO [1..5] con elementos de tipo T Pd (B[x]) = DirB + (x-1) lt; con lt igual a la longitud en bytes asignada al tipo T. NOTA: Se puede ver que en el arreglo unidimensional no interviene la cantidad de elementos para encontrar la dirección absoluta. Arreglos Bidimensionales: A estos arreglos se les asocia: Un intervalo de memoria a cada dimensión. Por tanto el Pd depende del orden en que se tome cada dimensión. Existen dos ordenes a saber:

i) Por columnas; variando primero el segundo índice. ii) Por renglones; variando primero el primer índice.

Por ejemplo:

Sea C un ARREGLO [1..n] [1..m] con elementos de tipo Carácter

para la referencia C[k1][ k2] considerando el orden por columnas, su dirección absoluta es: Pd(C[k1][k2]) = Dirc + n*(k2-1) + (k1-1) En el caso general: . . . . . .

. . . . . . . .1 2 m

. . . . . . . .

53

Page 54: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Sea E un ARREGLO [i1.. s1][ i2 .. s2] ... [in .. sn] con elementos de tipo T Donde ij, sj – límites inferior y superior, respectivamente de la j-ésima dimesión. rj = sj – ij + 1 – rango de la j-ésima dimensión. n - número de dimensiones. T – tipo de los elementos. Orden de las dimensiones – todas las posibles formas de variar n-dimensiones.

Si tomamos en orden decreciente las dimensiones para el almacenamiento (se almacena primero la última dimensión, luego la penúltima, así sucesivamente hasta almacenar la primera) tenemos que: Pd(E[k1][ k2]...[ kn]) = DirE + ((kn – 1) * rn-1* rn-2* ...* r1 + (kn-1 – 1) * rn-2* rn-3* ...* r1 + ...+ (k2-1) * r1 + k1 – 1) * lt Aplicaciones de los Polinomios de Direccionamiento en los Arreglos Regulares

Existen matrices, empleadas en Algebra Lineal Numérica, con estructura especial en donde se puede aprovechar el Pd para almacenarlas; por ejemplo, supongamos que queremos manejar una matriz triangular inferior almacenada por columnas de orden n. Se puede optimizar el almacenamiento de esta matriz empleando un arreglo y el Pd correspondiente. Si la almacenamos por columnas, a partir de la segunda columna debemos descontar una cantidad de elementos iguales a cero que no se requiere almacenar, así tendremos que el número de elementos a descontar será: uno para la segunda, dos para la 3ra, etc. Tales elementos se acumulan con respecto a la k2-ésima columna.

x 0 0 0 0 x x 0 0 0 x x x 0 0 x x x x 0 x x x x x

Pd (m[k1][ k2]) = Dirm + n * (k2-1) + (k1 –1) - i

12

1

=∑k

i

= Dirm + n * (k2 - 1) + (k1 – 1) – k2 * (k2 – 1) 2

2.4 Tipos Abstractos de Datos (TAD’s) Como se ha podido constatar en las Secciones anteriores, es necesario realizar un análisis de la Información antes de poder procesarla, dicho análisis produce, entre otras cosas, cuales son los elementos necesarios para poder organizar la información. De aquí que tengamos que, son las Estructuras de Datos las que nos permiten llevar a cabo tal organización, sin embargo el análisis antedicho no está completo si no se considera la forma en que cada estructura de datos integra sus operaciones para trabajar con éllas. Lo anterior nos conduce a un concepto más amplio de estructuras de datos, el de los Tipos Abstractos de Datos (TAD).

54

Page 55: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Un TAD consiste en la separación clara entre:

a) Su implantación, y b) Su uso, a través de su interfaz, que es la parte

visible para el usuario terminal del TAD. Lo anterior implica que un TAD tiene una interfaz y puede tener varias implantaciones. La definición de un TAD se hace en dos partes:

i) La especificación, y ii) La implantación.

Una especificación formal consiste en determinar o explicar en términos formales (matemáticos) lo que se desee, en nuestro caso un tipo de dato que es necesario para la resolución de un problema. Consideremos a T, como tal tipo de dato, el cual se define "como una clase de valores y una colección de operaciones sobre esos valores. Si las propiedades de esas operaciones son especificadas solo con axiomas, entonces T es un tipo abstracto de dato o una abstracción de dato" [Gutt-78].

Una implantación correcta del TAD cumple con cada uno de los axiomas especificados para él. La especificación por axiomas algebraicos para el tipo T se compone de: i.1) Una especificación sintáctica: donde se definen los nombres, dominios y rangos de las operaciones sobre T, y i.2) Una especificación semántica: compuesta del conjunto de axiomas en forma de ecuaciones, que dicen como opera cada una de las operaciones sobre las otras. La implantación se compone de: ii.1) Una representación: especifica como los valores del TAD serán almacenados en la memoria, es decir su estructura de datos, y ii.2) Los algoritmos: especifican como será usada y manipulada la estructura de datos, es decir las operaciones del TAD.

55

Page 56: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

El acceso al TAD se hace a través de su interfaz que es visible para los usuarios terminales de ella. La implantación del TAD es invisible para el usuario terminal y es visible para el que desarrolla el TAD. Como ejemplo: Consideremos la especificación del TAD : Pila, la cual se hará para una pila no limitada. Tipo de dato: Pila[elemento:tipoEle] Especificación sintáctica: creaPila() -> Pila, Crea la Pila meterElePila(Pila,tipoEle) -> Pila, Inserta un nuevo elemento en el tope sacarElePila(Pila) -> Pila, Elimina el elemento que está en el tope conTopePila(Pila) -> tipoEle V {TipoEleNoDef}, Consulta el elemento que está en el tope vacíaPila(Pila) -> Lógico, Verifica si la pila está vacía destruyePila(Pila) -> . Destruye la pila Especificación semántica: Declaración: P: Pila, el: tipoEle; sacarElePila(creaPila()) = creaPila() conTopePila(creaPila()) = {TipoEleNoDef} conTopePila(meterElePila(P,el)) = el vacmaPila(creaPila()) = Verdadero vacmaPila(meterElePila(P,el)) = Falso

En la especificación sintáctica es posible ver claramente cuáles son las operaciones primitivas validas sobre la estructura y cuáles son los tipos de datos que cada una regresa, después de efectuada la operación. Del ejemplo, obsérvese la operación sacarElePila() que se necesita para operar la Pila, devolviendo la misma Pila pero disminuida en un elemento, ya que suprime el elemento que está en el tope.

Es en la especificación en donde se incluyen aquellas operaciones que son básicas para el TAD, es decir las operaciones que sirven de base para construir cualquier otra operación sobre el tipo.

56

Page 57: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Del ejemplo, (en la mayoría de las estructuras de datos, sino es que en todas) es útil tener una operación que vacíe o limpie la estructura, en el caso de la Pila, sería la operación vaciarPila(Pila)->Pila.

Tal operación no aparece en la especificación del TAD Pila, debido a que puede ser "construida" invocando varias veces la operación sacarElePila(Pila) hasta que vacíaPila(Pila) sea verdadero.

Por lo tanto, el número de las operaciones que aparecen en cualquier implantación de un TAD no tiene por que ser igual, al número de las operaciones de su especificación.

En la especificación semántica se determina el efecto que tiene cada una de las operaciones especificadas sobre las demás. Del ejemplo: ¿Qué sucede si deseamos saber si la Pila está vacía, habiéndose creado recientemente?, esto es: vacmaPila(creaPila()), da como resultado Verdadero, ya que aunque la pila existe, y como ha sido recientemente creada, está vacía. En el caso de: conTopePila(creaPila()), el resultado es un valor especial denominado TipoEleNoDef, (tipoElemento no definido) para expresar que no puede regresarse elemento alguno, pues la pila está vacía. Este valor especial para "el" debe estar definido en la implantación del TAD.

En la práctica la especificación puede hacerse escogiendo, una representación física para el tipo Pila, que puede ser, por ejemplo escogiendo: - La secuencial. - La enlazada.

Ambas implantaciones, deben tomar en cuenta que la memoria es finita, y por ello

tal especificación es tan solo para un TAD que diferirá en algunos detalles de la implantación realizada basándose en élla.

57

Page 58: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

3. Grafos Introducción

El grafo es una estructura no lineal que debido a su generalidad tiene gran uso en diferentes aplicaciones de la ciencia y la ingeniería. Su aplicación, además de las estructuras de datos, se puede observar en los diferentes tipos de redes, como de computadoras, transportes, rutas áreas, interconexión eléctrica, aspectos relacionados en la economía e incluso en las redes neuronales artificiales se representan por medio de grafos.

3.1 Fundamentos y terminología básica Un grafo, G, es un par, compuesto por dos conjuntos V y A. Al conjunto V se le llama conjunto de nodos del grafo. A es un conjunto de pares de vértices, estos pares se conocen habitualmente con el nombre de arcos o ejes del grafo. Se suele utilizar la notación G = (V, A) para identificar un grafo. Los grafos representan un conjunto de objetos donde no hay restricción a la relación entre ellos. Son estructuras más generales y menos restrictivas. Podemos clasificar los grafos en dos grupos: dirigidos y no dirigidos. En un grafo no dirigido el par de vértices que representa un arco no está ordenado. Por lo tanto, los pares (v1, v2) y (v2, v1) representan el mismo arco. En un grafo dirigido cada arco está representado por un par ordenado de vértices , de forma que y representan dos arcos diferentes. El arco, a diferencia del segmento, tiene un punto de origen y punto terminal, lo cual implica una dirección. Se representa gráficamente mediante una flecha. Ejemplos de grafos (dirigidos y no dirigidos): a) Grafo 1 G1 = (V1, A1) V1 = {1, 2, 3, 4} A1 = {(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)} b) Grafo 2 G2 = (V2, A2) V2 = {1, 2, 3, 4, 5, 6} A2 = {(1, 2), (1, 3), (2, 4), (2, 5), (3, 6)} c) Grafo 3 G3 = (V3, A3) V3 = {1, 2, 3} A3 = { <1, 2>, <2, 1>, <2, 3> } Gráficamente estas tres estructuras de vértices y arcos se pueden representar de la siguiente manera:

58

Page 59: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

a)Grafo 1 b) Grafo 2 c) Grafo 3 Los grafos permiten representar conjuntos de objetos arbitrariamente relacionados. Se puede asociar el conjunto de vértices con el conjunto de objetos y el conjunto de arcos con las relaciones que se establecen entre ellos. Los grafos son modelos matemáticos de numerosas situaciones reales: un mapa de carreteras, la red de ferrocarriles, el plano de un circuito eléctrico, el esquema de la red telefónica de una compañia, etc. El número de distintos pares de vértices (v(i), v(j)), con v(i) <> v(j), en un grafo con n vértices es n*(n-1)/2. Este es el número máximo de arcos en un grafo no dirigido de n vértices. Un grafo no dirigido que tenga exactamente n*(n-1)/2 arcos se dice que es un grafo completo. En el caso de un grafo dirigido de n vértices el número máximo de arcos es n*(n-1). Algunas definiciones básicas de grafos:

• Orden de un grafo: es el número de nodos (vértices) del grafo.

• Grado de un nodo: es el número de ejes (arcos) que inciden sobre el nodo

• Grafo simétrico: es un grafo dirigido tal que si existe la relación entonces existe , con u, v pertenecientes a V.

• Grafo no simétrico: es un grafo que no cumple la propiedad anterior.

• Grafo reflexivo: es el grafo que cumple que para todo nodo u de V existe la relación (u, u) de A.

• Grafo transitivo: es aquél que cumple que si existen las relaciones (u, v) y (v, z) de A entonces existe (u, z) de A.

59

Page 60: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

• Grafo completo: es el grafo que contiene todos los posibles pares de relaciones, es decir, para cualquier par de nodos u, v de V, (u <> v), existe (u, v) de A.

• Camino: un camino en el grafo G es una sucesión de vértices y arcos: v(0), a(1), v(1), a(2), v(2), ... , a(k), v(k); tal que los extremos del arco a(i) son los vértices v(i-1) y v(i).

• Longitud de un camino: es el número de arcos que componen el camino.

• Camino cerrado (circuito): camino en el que coinciden los vértices extremos (v(0) = v(k)).

• Camino simple: camino donde sus vértices son distintos dos a dos, salvo a lo sumo los extremos.

• Camino elemental: camino donde sus arcos son distintos dos a dos.

• Camino euleriano: camino simple que contiene todos los arcos del grafo.

• Grafo euleriano: es un grafo que tiene un camino euleriano cerrado.

• Grafo conexo: es un grafo no dirigido tal que para cualquier par de nodos existe al menos un camino que los une.

• Grafo fuertemente conexo: es un grafo dirigido tal que para cualquier par de nodos existe un camino que los une.

• Punto de articulación: es un nodo que si desaparece provoca que se cree un grafo no conexo.

• Componente conexa: subgrafo conexo máxima de un grafo no dirigido (parte más grande de un grafo que sea conexa).

Tipo de datos abstracto de un grafo dirigido conexo

El TDA de un grafo dirirgido conexo se puede representar de la siguiente manera:

TDA Grafo_dirigido_ conexo (Elementos E, Operadores O, Axiomas A)

E Los elementos básicos son:

- Un conjunto de nodos {N1,N2,N3,N4,...Nk}

- Un conjunto de arcos {ai,j} cada arco nace en el nodo i y termina en el nodo j.

- Un conjunto de pesos {pi,j} asociado a cada arco ai,j existe un peso pi,j que implica un costo

60

Page 61: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

- Un conjunto de datos {Dato i}asociado a cada nodo i.

- Referencias (apuntadores) de entrada. ENTRADA

- Indicador nulo Λ

- Operadores: unión (U) y diferencia (-)

O Los operadores para el manejo del grafo :

- Iniciar G(N,A),

- Insertar_Nodo (G(N,A),Ni) G(N,A)

- Insertar_Arco (G(N,A),ai,j) (G(N,A)

- Eliminar_Nodo (G(N,A),Ni) G(N,A)

- Eliminar_Arco (G(N,A),ai,j) (G(N,A)

- Consultar (G(N,A)Ni) Dato

- Recorrer G(N,A) {N1,N2,N3,N4,...Nk}

A Los axiomas para un grafo dirigido conexo para el que su último nodo es K

ESTADO (INICIAR) = VACIO ENTRADA =Λ

INSERTAR (INICIAR, N1) = G(N,A) ENTRADA = Liga N1

INSERTAR_NODO ( G(N,A),Nk) = G(N,A) Es conexo si para

1<=i<k, 1<=j<k

{ ai,j} U { ak,j}#0

ELIMINAR_NODO (G(N,A),N1) = G(N,A)

Generar error si N1 ∉ { Ni}

Implica que para 1<=i <=k, 1<=j<=k

{ ai,j} U { ak,j}=0

INSERTAR_ARCO (G(N,A),al,m) = G(N,A)

Implica que N1 ∉ { Ni} y Nm∉ { Ni}

ELIMINAR_ARCO (G(N,A),al,m) )

Generar error si al,m ∉ { ai,j}

Implica que para 1<=i <=k, 1<=j<=k

{ ai,l} U { a1,,j} #0 y { ai,m} U { am,,j} # 0

61

Page 62: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

3.2 Representación de grafos Existen tres maneras básicas de representar los grafos: mediante matrices de adyacencia, listas de adyacencia y matrices dispersas. Cada representación tiene unas ciertas ventajas e inconvenientes respecto de las demás, que comentaremos más adelante. 3.2.1 Representación mediante matrices de adyacencia Un grafo es un par compuesto por dos conjuntos: un conjunto de nodos y un conjunto de relaciones entre los nodos. La representación tendrá que ser capaz de guardar esta información en memoria. La forma más fácil de guardar la información de los nodos es mediante la utilización de un vector que indexe los nodos, de manera que los arcos entre los nodos se pueden ver como relaciones entre los índices. Esta relación entre índices se puede guardar en una matriz, que llamaremos de adyacencia.

1. Definir el máximo de nodos= MAX; 2. Indice =1-MAX; Valor_Nodo = ??; Valor_Arco = ??; Clase Arco { Info: Valor_Arco; // información asociada a cada arco *) Existe: Boolean; } clase Nodo { Info: Valor_Nodo; // información asociada a cada nodo Existe: Boolean; } Clase Grafo { MATRIZ [MAX][MAX] }

Con esta representación tendremos que reservar al menos del orden de (n^2) espacios de memoria para la información de los arcos, y las operaciones relacionadas con el grafo implicarán, habitualmente, recorrer toda la matriz, con lo que el orden de las operaciones será, en general, cuadrático, aunque tengamos un número de relaciones entre los nodos mucho menor que (n^2).

62

Page 63: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

En cambio, con esta representación es muy fácil determinar, a partir de dos nodos, si están o no relacionados: sólo hay que acceder al elemento adecuado de la matriz y comprobar el valor que guarda. Ejemplo:

Figura 3.1 Grafo no dirigido

Supongamos el grafo representado en la figura 3.1. A partir de ese grafo la información que guardaríamos, con esta representación, sería:

Tabla 3.1 Matriz de adyacencia del grafo de la figura 3.1 3.2.2 Representación mediante punteros: listas de adyacencia En las listas de adyacencia se intenta evitar justamente el reservar espacio para aquellos arcos que no contienen ningún tipo de información. El sustituto obvio a los vectores con huecos son las listas.

63

Page 64: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

En las listas de adyacencia lo que haremos será guardar por cada nodo, además de la información que pueda contener el propio nodo, una lista dinámica con los nodos a los que se puede acceder desde él. La información de los nodos se puede guardar en un vector, al igual que antes, o en otra lista dinámica. Si elegimos la representación en un vector para los nodos, tendríamos la siguiente definición de grafo en Pascal:

Const MAX_NODOS = ??; Type Indice = 1..MAX_NODOS; Valor_Nodo = ??; Valor_Arco = ??; Punt_Arco = ^Arco; Arco = Record Info: Valor_Arco; (* información asociada a cada arco *) Destino: Indice; Sig_Arco: Punt_Arco; end; Nodo = Record Info: Valor_Nodo; (* información asociada a cada nodo *) Existe: Boolean; Lista_Arcos: Punt_Arco; end; Grafo = Record Nodos: Array[Indice] of Nodo; end;

En general se está guardando menor cantidad de elementos, sólo se reservará memoria para aquellos arcos que efectivamente existan, pero como contrapartida estamos guardando más espacio para cada uno de los arcos (estamos añadiendo el índice destino del arco y el puntero al siguiente elemento de la lista de arcos). Las tareas relacionadas con el recorrido del grafo supondrán sólo trabajar con los vértices existentes en el grafo, que puede ser mucho menor que (n^2). Pero

64

Page 65: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

comprobar las relaciones entre nodos no es tan directo como lo era en la matriz, sino que supone recorrer la lista de elementos adyacentes perteneciente al nodo analizado. Además, sólo estamos guardando realmente la mitad de la información que guardábamos en el caso anterior, ya que las relaciones inversas (las relaciones que llegan a un cierto nodo) en este caso no se guardan, y averiguarlas supone recorrer todas las listas de todos los nodos. 3.2.3 Representación mediante referencias: matrices dispersas o estructuras tipo red o malla Para evitar uno de los problemas que teníamos con las listas de adyacencia, que era la dificultad de obtener las relaciones inversas, podemos utilizar las matrices disperas, que contienen tanta información como las matrices de adyacencia, pero, en principio, no ocupan tanta memoria como las matrices, ya que al igual que en las listas de adyacencia, sólo representaremos aquellos enlaces que existen en el grafo.

Estructura Nodo Datos_Nodo Conexión_nodo1 Número_nodo Valor_arco Liga_1 Conexión_nodo2 Número_nodo Valor_arco Liga_2 Conexión_nodo_n Fin_Estructura

3.3 Recorrido de grafos Recorrer un grafo supone intentar alcanzar todos los nodos que estén relacionados con uno dado que tomaremos como nodo de salida. Existen básicamente dos técnicas para recorrer un grafo: el recorrido en anchura; y el recorrido en profundidad. 3. 3. 1 Propiedades: 1) El nº de recorridos de longitud k de vi a vj es el elemento ij de la matriz M(G)k. 2) Un grafo G es bipartito � G no tiene ciclos de longitud impar. 3) Si G tiene sólo dos vértices impares existe un camino entre ellos.

65

Page 66: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Un grafo es conexo si para cada par de vértices u y v existe un camino de u a v. Si G es un grafo no conexo (o disconexo), cada uno de sus subgrafos conexos maximales se llama componente conexa de G. Designaremos por k(G) al nº de componentes conexas del grafo G Un vértice v se llama vértice-corte (o punto de articulación) de G si el grafo G-{v} tiene más componentes conexas que G. Una arista a de un grafo G se llama puente si G-{a} tiene más componentes conexas que G. 4)Los bloques de un grafo G son los subgrafos de G sin vértices-corte y maximales con respecto a esta propiedad.

3.3. 2 Recorrido en anchura o BFS (Breadth First Search) El recorrido en anchura supone recorrer el grafo, a partir de un nodo dado, en niveles, es decir, primero los que están a una distancia de un arco del nodo de salida, después los que estan a dos arcos de distancia, y así sucesivamente hasta alcanzar todos los nodos a los que se pudiese llegar desde el nodo salida. El algoritmo general de recorrido es el siguiente: Algoritmo Recorrido_en_Anchura (BFS)

Entradas gr: Grafo (* grafo a recorrer *) nodo_salida: Indice (* origen del recorrido *) Variables queue: Cola de Indice aux_nod1, aux_nod2: Indice Inicio Iniciar_Cola(queue) Procesar(nodo_salida) Visitado(nodo_salida) <-- CIERTO Encolar(queue, nodo_salida) mientras NO Cola_Vacia(queue) hacer aux_nod1 <-- Desencolar(queue) para (todos los nodos), aux_nod2,adyacentes a aux_nod1 hacer si NO Visitado[aux_nod2] entonces Procesar(aux_nod2) Visitado[aux_nod2] <-- CIERTO Encolar(queue, aux_nod2)

66

Page 67: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

fin_si fin_para fin_mientras Fin

La diferencia a la hora de implementar el algoritmo general para cada una de las implementaciones de la estructura de datos grafo, residirá en la manera de averiguar los diferentes nodos adyacentes a uno dado. En el caso de las matrices de adyacencia se tendrán que comprobar si los enlaces entre los nodos existen en la matriz. En los casos de las listas de adyacencia y de las matrices dispersas sólo habrá que recorrer las listas de enlaces que parten del nodo en cuestión para averiguar qué nodos son adyacentes al estudiado. 3.3.3Recorrido en profundidad o DFS (Depth First Search) A diferencia del algoritmo anterior, el recorrido en profundidad trata de buscar los caminos que parten desde el nodo de salida hasta que ya no es posible avanzar más. Cuando ya no puede avanzarse más sobre el camino elegido, se vuelve atrás en busca de caminos alternativos, que no se estudiaron previamente. El algoritmo es similar al anterior, pero utilizando, para guardar los nodos accesibles desde uno dado, una pila en lugar de una cola. Algoritmo Recorrido_en_Profundidad (DFS)

Entradas gr: Grafo (* grafo a recorrer *) nodo_salida: Indice (* origen del recorrido *) Variables stack: Pila de Indice aux_nod1, aux_nod2: Indice Inicio Iniciar_Pila(stack) Procesar(nodo_salida) Visitado(nodo_salida) <-- CIERTO Apilar(stack, nodo_salida) mientras NO Pila_Vacia(stack) hacer aux_nod1 <-- Desapilar(stack) para (todos los nodos), aux_nod2,adyacentes a aux_nod1 hacer

67

Page 68: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

si NO Visitado[aux_nod2] entonces Procesar(aux_nod2) Visitado[aux_nod2] <-- CIERTO Apilar(stack, aux_nod2) fin_si fin_para fin_mientras Fin

La utilización de la pila se puede sustituir por la utilización de la recurrencia, de manera que el algoritmo quedaría como sigue: Algoritmo Recorrido_en_Profundidad (DFS)

Entradas gr: Grafo (* grafo a recorrer *) nodo_salida: Indice (* origen del recorrido *) Variables aux_nod2: Indice Inicio Procesar(nodo_salida) Visitado(nodo_salida) <-- CIERTO para (todos los nodos), aux_nod2, adyacentes a aux_nod1hacer si NO Visitado[aux_nod2] entonces Recorrido_en_Profundidad(gr, aux_nod2) fin_si fin_para Fin

68

Page 69: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

3.4 Caminos mínimos en grafos Uno de los objetivos de tener un grafo es poder analizar los desplazamientos desde cualquier nodo a los demás que conforman el grafo. Fundamentalmente, el planteamiento consiste que a partir de un nodo i del grafo, encontrar los recorridos óptimos para ir a cada uno de los nodos restantes, a partir de la base de que los arcos pueden representar, además la existencia de la conexión, el costo o la distancia para desplazarse de un nodo a otro. Existen varias formas de alcanzar la solución utilizando uno de los siguientes algoritmos:

1. Algoritmo de DijKstra 2. Algoritmo de Floyd 3. Algoritmo de Ford

Distancia en un grafo:

Sean G=(V,A) un grafo (o digrafo) ponderado, u, v vértices de G. Se llama distancia de u a v, d(u,v), a la mínima longitud de los caminos que unen u con v. Si no existe camino de u a v se dice que d(u,v)=� Propiedades: Si las aristas (o arcos, en el caso dirigido) no reciben pesos negativos

entonces 1. d(x,y)�0 y d(x,y)=0 si y sólo si x=y 2. d(x,y)=d(y,x) 3. d(x,y)+d(y,z)�d(x,z)

Nociones relacionadas con distancia:

La excentricidad de un vértice v es e(v) = máx{d(v,z)/ z� V(G)} El radio de un grafo es rad(G) = mín{e(v)/ v� V(G)} El diámetro de un grafo es diam(G) = máx{e(v)/ v� V(G)}

El centro de un grafo G es el subgrafo inducido por el conjunto de vértices de excentricidad mínima.

La distancia total de un vértice v es dt(v) = La mediana de un grafo G es el subgrafo inducido por el conjunto de

vértices de distancia total mínima

69

Page 70: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

e(b)=3, e(a)=e(c)=2 Centro <a>, Mediana <b>

Figura 3.2 Caminos mínimos Propiedades:

1. Si G es un grafo conexo entonces rad(G)�diam(G)�2rad(G) 2. Todo grafo es el centro de un grafo conexo 3. El centro de un árbol T está formado por uno o dos vértices de T

3. 4.1 Algoritmos de Caminos Mínimos

Dado un grafo (o digrafo) ponderado y dos vértices s y t se quiere hallar

d(s,t) y el camino con dicha longitud. Los primeros algoritmos que presentamos obtienen todos los caminos de longitud mínima desde un vértice dado s al resto de vértices del grafo. El último algoritmo resuelve el problema para un par cualquiera de vértices de G.

Si el vértice u se encuentra en un camino C de longitud mínima entre

los vértices s y z entonces, la parte de C comprendida entre los vértices s y u es un camino de longitud mínima entre s y u. Por tanto, el conjunto de caminos mínimos desde s a los restantes vértices del grafo G es un árbol, llamado el árbol de caminos mínimos desde s.

3.4.1.1 Algoritmo de Dijkstra (1959) La idea básica del algoritmo es la siguiente: Si P es un camino de longitud

mínima s--z y P contiene al vértice v, entonces la parte s--v de P es también camino de longitud mínima de s a v. Esto sugiere que si deseamos determinar el camino óptimo de s a cada vértice z de G, podremos hacerlo en orden creciente de la distancia d(s,z)

70

Page 71: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Descripción del algoritmo

Entrada: Un grafo (o digrafo) ponderado, un vértice s�V. El peso de la arista uv se indica por w(uv), poniendo w(uv)=� si uv no es arista. (Las aristas tienen pesos no negativos)

Clave: Mantener el conjunto T de vértices para el que se conoce el camino más corto y ampliar T hasta que T=V. Para ello etiquetamos cada vértice z con t(z) que es la longitud del camino más corto ya encontrado. Mantenemos un conjunto A de vértices que se encuentran en el árbol de búsquea pero no en el árbol de camino mínimo y que son alcazables desde los vértices de T.

Inicialización: Sea A={s}, T{}, t(s)=d(s,s)=0, t(z)=� para z�s. Iteración: Elegir el vértice v �A con etiqueta mínima. Añadir v a T.

Designar v como "Vértice Actual" y eliminarlo de A Analizar cada arista vz con z�T y actualizar la etiqueta de z a min{t(z),

t(v)+w(vz)}. Añadir z a A. La iteración continua hasta que T=V(G) o hasta que t(z)=� para cada

vértice z�T En cualquier caso la etiqueta de cada vértice z en T será la distancia de

s a z. En el segundo caso los vértices que no están en T no son accesibles desde s.

Procedimiento

1. Se utilizan como estructuras adicionales los vectores D y R, de tañamo N, para almacenar los desplazamientos.

2. Se determinan los valores de caminos entre el nodo Ni y los nodos

adyacentes, a los que no están conectados se les coloca un valor infinito y se incluyen en el vector de desplazamiento, {Di,1 , Di,2 , Di,3,….Di,m }.

3. Se selecciona el menor valor Di,j, este es el resultado optimo para ir del

nodo i al nodo j . 4. Se determinan las distancias del nodo j a sus nodos adyacentes, Rj,k

incluido Ri,j, se calcula el valor de los recorridos Ri,k = Ri,j +Rj,k.

5. Se comparan los valores del vector base de desplazamiento D con R y se deja en el vector de desplazamiento del menor entre Dj,k y Ri,k.

6. Se regresa al segundo paso, y el algoritmo se termina cuando se han

seleccionado todos los N-1 nodos.

71

Page 72: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Ejemplo

3

1 5

42

4 2

3

10

8

5 12

Figura 3.3 Grafo dirigido

Encontrar el desplazamiento mínimo para ir del nodo 1 a los demás nodos del grafo mostrado en la figura 3.3.

1. Se determinan los desplazamientos Di,j, de la primera fila, para ello se coloca ∝ cuando no hay arcos. Estos valores son mostrados en la tabla 3.2 Del vector base se selecciona el menor. Valor D1,2 =3 Se obtiene de esta forma el mínimo desplazmiento entre el nodo1 y el nodo2. Se sale del nodo2.

2. Se buscanlos nodos adyacentes del nodo2, que son nodo3 y nodo5. 3. Se calcula R1,3 y R 1,5 : R1,3 = 3+4 y R1,5 = 3+8 Se pasa por N2 4. Se compara con D1,3 y D1,5, con R1,3 y R 1,5, y se sustituye el vector de

desplazamiento aquellos que sean menores 5. Se selecciona el mínimo y se reemplaza en el vector base

6. Se continua el proceso hasta ciando se haya determinado el mínimo en

todas las distancias.

D2 D3 D4 D5 Nodo salida Distancia 3 ∝ 10 12 2 3 3 7 10 11 3 7 3 7 9 11 4 9 3 7 9 11 5 11

Tabla 3.2 Distancias mínimas del grafo dirigido

72

Page 73: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Análisis de la complejidad

En cada iteración se añade un vértice a T, luego el nº de iteraciones es n. En cada una se elige una etiqueta mínima, la primera vez entre n-1, la segunda entre n-2, ..., luego la complejidad total de estas elecciones es O(n2). Por otra parte cada arista da lugar a una actualización de etiqueta, que se puede hacer en tiempo constante O(1), en total pues O(q). Por tanto la complejidad del algoritmo es O(n2)

Teorema (Validez del algoritmo)

El algoritmo de Dijkstra calcula d(s,z) para cada vértice z�V(G) Demostración

Debemos probar que la etiqueta definitiva t(z) es d(s,z). Sean x1, x2, ..., xn los vértices de G ordenados por su incorporación al conjunto T. Así x1=s. Vamos a demostrar el resultado por inducción sobre i.

Primer paso) El resultado es cierto para i=1, pues x1=s y sabemos que d(s,s)=0=t(s)

Paso de i a i+1) La hipótesis de inducción es que t(x1)=d(s,x1), ..., t(xi)=d(s,xi). Debemos probar que t(xi+1)=d(s,xi+1)

Llamemos S={x1, x2, ..., xi}, La etiqueta t(xi+1) es, por la construcción del algoritmo, la longitud de un camino Q s,..., u, xi+1, donde u es el último vértice en S y e=uxi+1 es una arista de G. Si hay otro camino Q' de s a xi+1 debemos probar que long(Q)�long(Q').

Sea z el primer vértice de Q' fuera de S, vz la primera arista y Q'' el resto del camino de z a xi+1

long(Q')=d(s,v)+w(vz) +long(Q'') � t(z)+long(Q'') y como xi+1 se elige como vértice de menor etiqueta será t(z)�t(xi+1) y

así long(Q') � t(xi+1)+long(Q'')� t(xi+1) por ser todas las aristas de peso no negativo. Por tanto t(xi+1)=long(Q)=d(s,xi+1)

3.4.1.2 Algoritmo de Ford (1956) Es una variante del algoritmo de Dijkstra que admite la asignación de

pesos negativos en los arcos, aunque no permite la existencia en el digrafo de ciclos de peso negativo.

73

Page 74: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Descripción del algoritmo Entrada: Un digrafo ponderado con pesos no negativos en los arcos,

un vértice s�V. El peso del arco uv se indica por w(uv), poniendo w(uv)=� si uv no es arco.

Salida: La distancia desde s a cada vértice del grafo Clave: Mejorar en cada paso las etiquetas de los vértices, t(u) Inicialización: Sea T={s}, t(s)=d(s,s)=0, t(z)=� para z�s. Iteración: Mientras existan arcos e=xz para los que t(z)>t(x) + w(e)

actualizar la etiqueta de z a min{t(z), t(x)+w(xz)} Análisis de la complejidad

En primer lugar debemos observar que cada arco puede considerarse varias veces. Empecemos ordenando los arcos del digrafo D siendo este el orden en que se considerarán los arcos en el algoritmo. Después de la primera pasada se repite el proceso hasta que en una pasada completa no se produzca ningún cambio de etiquetas. Si D no contiene ciclos negativos puede demostrarse que, si el camino mínimo s� u contiene k arcos entonces, después de k pasadas se alcanza la etiqueta definitiva para u. Como k�n y el nº de arcos es q, resulta que la complejidad del algoritmo de Ford es O(qn). Además podemos detectar un ciclo negativo si se produce una mejora en las etiquetas en la pasada número n.

3.4.1.3 Algoritmo de Floyd (1962)

A veces no es suficiente calcular las distancias con respecto a un

vértice s, si no que necesitamos conocer la distancia entre cada par de vértices. Para ello se puede aplicar reiteradamente alguno de los algoritmos anteriores, variando el vértice s de partida. Así tendríamos algoritmos de complejidad O(n3) (si usamos el algoritmo de Dijkstra) u O(n2q) (si usamos el algoritmo de Ford). A continuación se describe un algoritmo, debido a Floyd y Warshall, con una estructura sencilla, que permite la presencia de arcos de peso negativo y que resuelve el mismo problema. (Naturalmente los ciclos de peso negativo siguen estando prohibidos).

La idea básica del algoritmo es la construcción de una sucesión de

matrices W0, W1, ..., Wn, donde el elemento ij de la matriz Wk nos indique la longitud del camino mínimo i�j utilizando como vértices interiores del camino los del conjunto {v1, v2, ..., vk}. La matriz W0 es la matriz de pesos del digrafo, con w0

ij=w(ij) si existe el arco i�j, w0ii=0 y w0

ij=� si no existe el arco i�j. Descripción del algoritmo

Entrada: Un digrafo ponderado sin ciclos de peso negativos. El peso del arco uv se indica por w(uv), poniendo w(uv)=� si uv no es arco.

74

Page 75: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Salida: La distancia entre dos vértices cualesquiera del grafo Clave: Construimos la matriz Wk a partir de la matriz Wk-1 observando

que

Iteración: Para cada k =1, ..., n, hacer �i,j=1,...,n El elemento ij de la matriz Wn nos da la longitud de un camino mínimo

entre los vértices i y j Análisis de la complejidad

Se deben construir n matrices de tamaño nxn y cada elemento se halla en tiempo constante. Por tanto, la complejidad del algoritmo es O(n3)

Validez del algoritmo

La indicación dada en la clave se puede demostrar por inducción sobre k, con lo que tendríamos demostrada la validez del algoritmo

Observaciones:

1. Si existe un ciclo negativo entonces algún elemento de la diagonal principal se hará negativo en alguna de las matrices Wk

2. Si además de las longitudes de los caminos mínimos se desean obtener dichos caminos basta construir una sucesión auxiliar de matrices P0, P1,..., Pn de la siguiente forma:

El elemento ij de la matriz P0 es j Si en el elemento ij de la matriz Wk no se produce cambio entonces pk

ij coincide con el elemento correspondiente de la matriz Pk-1 . Si hay cambio entonces pk

ij = pk-1ik

Así el elemento ij de la matriz Pn indica el primer vértice después de vi en el camino de longitud mínima que conecta los vértices vi y vj. Y con esto resulta fácil reconstruir todo el camino.

75

Page 76: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

1. Programa para representar un grafo conexo mediante una matriz

import java.io.*; public class grafi{ String nombre[]=new String[20]; int matriz[][]=new int[20][20]; int n,l; grafi(){ for(int i=0;i<n;i++) for(int j=0;j<n;j++) matriz[i][j]=0; } void lista(){ for(int i=0;i<n;i++) System.out.println(i+"."+nombre[i]+" ");} void pideDimension()throws IOException{ do{ System.out.println("¿Cual es la dimension de tu matriz? max 20"); BufferedReader in=new BufferedReader(new InputStreamReader(System.in)); n=Integer.parseInt(in.readLine()); }while(n>20||n<2); } void pideN()throws IOException{ BufferedReader in=new BufferedReader(new InputStreamReader(System.in)); System.out.println("Proporciona identificadores de los vertices"); for(int i=0;i<n;i++) nombre[i]=in.readLine(); } void pideMz()throws IOException{ BufferedReader in=new BufferedReader(new InputStreamReader(System.in)); for(int i=0;i<n;i++) for(int j=0;j<n;j++){ if(i!=j) if(matriz[i][j]==0){ System.out.println("¿Cual es el valor del eje "+nombre[i]+" con respecto a "+nombre[j]+" ?, si no existe escribe un numero <1"); matriz[i][j]=matriz[j][i]=Integer.parseInt(in.readLine()); if(matriz[i][j]>0) l++; } } } void muestra(){ for(int i=0;i<n;i++) System.out.print(" "+nombre[i]); System.out.print("\n"); for(int i=0;i<n;i++){ System.out.print(nombre[i]); for(int j=0;j<n;j++) System.out.print(" "+matriz[i][j]+" "); System.out.print("\n"); } }

76

Page 77: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

void escriOrden(){ System.out.println("Orden del grafo "+n);} void grado(int s){ boolean f=false; int cont=0; for(int i=0;i<n;i++) if(i==s) f=true; if(!f) System.out.println("El vertice no existe"); else{ System.out.print("El grado del vertice es "); for(int i=0;i<n;i++) if(i==s) for(int j=0;j<n;j++){ if(matriz[i][j]>0) cont++; System.out.println(cont); } } } void conVertice(){ System.out.print("V={"); for(int i=0;i<n;i++) System.out.print(nombre[i]+","); System.out.print("}\n"); } void conArcos(){ System.out.print("A={"); for(int i=0;i<n/2;i++) for(int j=0;j<n;j++) if(matriz[i][j]>0) System.out.print("("+nombre[i]+","+nombre[j]+"),"); System.out.println("}"); } public static void main(String a[])throws IOException{ int op; BufferedReader in=new BufferedReader(new InputStreamReader(System.in)); grafi grafo=new grafi(); grafo.pideDimension(); grafo.pideN(); grafo.pideMz(); do{ System.out.println("Que deseas hacer?\n1.Ver matriz\n2.Ver orden del grafo\n3.Buscar grado de un vertice\n4.Salir,\n5.Ver conjunto de vertices\n6.Ver conjunto de Arcos"); op=Integer.parseInt(in.readLine()); switch(op){ case 1:grafo.muestra(); break; case 2:grafo.escriOrden(); break; case 3:System.out.println("Proporciona el numero del vertice a buscar el grado"); grafo.lista();

77

Page 78: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

grafo.grado(Integer.parseInt(in.readLine())); break; case 4:System.out.println("Finalizando..."); break; case 5:grafo.conVertice(); break; case 6:grafo.conArcos(); break; default:System.out.println("Opcion invalida"); } }while(op!=4); } }

78

Page 79: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

2. Programa que calcula los grados de los vértices de un grafo dirigido en una representación matricial import java.io.*; public class grafis2{ String nombre[]=new String[20]; int matriz[][]=new int[20][20]; boolean matrix[][]=new boolean[20][20]; int n,l; grafis2(){ for(int i=0;i<n;i++) for(int j=0;j<n;j++){ if(i!=j) matriz[i][j]=10000; else matriz[i][j]=0;} } void floyd(){ int mat[][]=new int[20][20]; for(int i=0;i<n;i++) for(int j=0;j<n;j++) mat[i][j]=matriz[i][j]; for(int k=0;k<n;k++) for(int i=0;i<n;i++) for(int j=0;j<n;j++) if((mat[i][k]+mat[k][j])<mat[i][j]) mat[i][j]=mat[i][k]+mat[k][j]; for(int i=0;i<n;i++) System.out.print(" "+nombre[i]); System.out.print("\n"); for(int i=0;i<n;i++){ System.out.print(nombre[i]); for(int j=0;j<n;j++) System.out.print(" "+mat[i][j]+" "); System.out.print("\n"); } } void lista(){ for(int i=0;i<n;i++) System.out.println(i+"."+nombre[i]+" ");} void pideDimension()throws IOException{ do{ System.out.println("¿Cual es la dimension de tu matriz? max 20"); BufferedReader in=new BufferedReader(new InputStreamReader(System.in)); n=Integer.parseInt(in.readLine()); }while(n>20||n<2); } void valor(int i,int j,int res,int x){ if(x>0){ if(res==1)

79

Page 80: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

matrix[i][j]=true; else matrix[i][j]=false; } } void gradoi(int s){ boolean f=false; int cont=0; for(int i=0;i<n;i++) if(i==s) f=true; if(!f) System.out.println("El eje no existe"); else{ System.out.print("El grado interno del vertice es "); for(int i=0;i<n;i++) if(i==s) for(int j=0;j<n;j++){ if(matriz[i][j]>0) if(matrix[i][j]==true) cont++; } System.out.println(cont); } } void gradoe(int s){ boolean f=false; int cont=0; for(int i=0;i<n;i++) if(i==s) f=true; if(!f) System.out.println("El eje no existe"); else{ System.out.print("El grado externo del vertice es "); for(int i=0;i<n;i++) if(i==s) for(int j=0;j<n;j++){ if(matriz[i][j]>0) if(matrix[i][j]==false) cont++; } System.out.println(cont); } } void pideN()throws IOException{ BufferedReader in=new BufferedReader(new InputStreamReader(System.in)); System.out.println("Proporciona identificadores de los vertices"); for(int i=0;i<n;i++) nombre[i]=in.readLine(); } void pideMz()throws IOException{ BufferedReader in=new BufferedReader(new InputStreamReader(System.in));

80

Page 81: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

int res,aux; for(int i=0;i<n;i++) for(int j=0;j<n;j++){ if(i!=j) if(matriz[i][j]==0){ do{ System.out.println("¿Cual es el valor del eje "+nombre[i]+" con respecto a "+nombre[j]+" ?"); aux=Integer.parseInt(in.readLine()); if(aux<0) System.out.println("Valor invalido, no se admiten negativos"); }while(aux<0); matriz[i][j]=aux; do{ System.out.println("El grafo es dirigido de "+nombre[i]+" con respecto a "+nombre[j]+" ?"); System.out.println("1.Si\n2.No"); res=Integer.parseInt(in.readLine()); }while(res<1||res>2); valor(i,j,res,matriz[i][j]); if(matriz[i][j]>0) l++; } } } void muestra(){ for(int i=0;i<n;i++) System.out.print(" "+nombre[i]); System.out.print("\n"); for(int i=0;i<n;i++){ System.out.print(nombre[i]); for(int j=0;j<n;j++) System.out.print(" "+matriz[i][j]+" "); System.out.print("\n"); } } void escriOrden(){ System.out.println("Orden del grafo "+n);} void grado(int s){ boolean f=false; int cont=0; for(int i=0;i<n;i++) if(i==s) f=true; if(!f) System.out.println("El eje no existe"); else{ System.out.print("El grado del vertice es "); for(int i=0;i<n;i++) if(i==s) for(int j=0;j<n;j++){ if(matriz[i][j]>0) cont++; } System.out.println(cont); }

81

Page 82: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

} void conVertice(){ System.out.print("V={"); for(int i=0;i<n;i++) System.out.print(nombre[i]+","); System.out.print("}\n"); } void conArcos(){ System.out.print("A={"); for(int i=0;i<n;i++) for(int j=0;j<n;j++) if(matriz[i][j]>0) System.out.print("("+nombre[i]+","+nombre[j]+"),"); System.out.println("}"); } public static void main(String a[])throws IOException{ int op; BufferedReader in=new BufferedReader(new InputStreamReader(System.in)); grafis2 grafo=new grafis2(); grafo.pideDimension(); grafo.pideN(); grafo.pideMz(); do{ System.out.println("Que deseas hacer?\n1.Ver matriz\n2.Ver orden del grafo\n3.Buscar grado de un vertice\n4.Salir\n5.Ver conjunto de vertices\n6.Ver conjunto de Arcos\n7.Buscar grado interno de un vertice\n8.Buscar grado externo de un vertice\n9.Algoritmo de Floyd"); op=Integer.parseInt(in.readLine()); switch(op){ case 1:grafo.muestra(); break; case 2:grafo.escriOrden(); break; case 3:System.out.println("Proporciona el numero del vertice a buscar el grado"); grafo.lista(); grafo.grado(Integer.parseInt(in.readLine())); break; case 4:System.out.println("Finalizando..."); break; case 5:grafo.conVertice(); break; case 6:grafo.conArcos(); break; case 7: System.out.println("Proporciona el numero del vertice a buscar el grado interno"); grafo.lista(); grafo.gradoi(Integer.parseInt(in.readLine())); break; case 8:System.out.println("Proporciona el numero del vertice a buscar el grado externo"); grafo.lista(); grafo.gradoe(Integer.parseInt(in.readLine())); break; case 9:grafo.floyd();

82

Page 83: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

break; default:System.out.println("Opcion invalida"); } }while(op!=4); } } 3. Crear un programa para la representación de grafos mediante multilistas. a) // Clase que crea nodos tipo principal import java.io.*; class grafin{ grafin liga; grafini sub; String identifica; grafin(){sub=null;} grafin(String nombre,grafini m){ this(nombre,null,m);} grafin(String nombre){ this(nombre,null,null);} grafin(String nombre,grafin r,grafini m){ liga=r; sub=m; identifica=nombre; } grafin getliga(){ return liga;} grafini getsub(){ return sub; } void setliga(grafin m){ liga=m;} void setsub(grafini dato){ sub=dato;} String setide(){ return identifica;} } b) // Clase que crea nodos secundarios para la representación de grafos mediante listas import java.io.*; class grafini{ grafini liga; String identifica; int valor; grafini(String nombre,int val,grafini m){ liga=m;

83

Page 84: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

identifica=nombre; valor=val; } grafini getliga(){ return liga;} void setliga(grafini m){ liga=m;} String setide(){ return identifica;} int getVal(){ return valor; } } c) // Clase para el manejo de listas para la representación de un grafo import java.io.*; class grafit{ int matrix[][]=new int[20][20]; String nombres[]=new String [20]; int num; private grafin cabeza; grafit(){ cabeza=null; num=0; for(int i=0;i<20;i++) for(int j=0;j<20;j++) matrix[i][j]=10000; } int numnod(){ int nod=0; grafin prev,sig; prev=cabeza; sig=cabeza; while(sig!=null){ nod++; prev=sig; sig=sig.getliga(); } return nod;} void fullmat(){ grafini prev,sig; grafin ant,pos; ant=cabeza; pos=cabeza; int i=0; int n=0; while(pos!=null){ ant=pos; pos=pos.getliga(); nombres[i]=ant.setide(); prev=ant.getsub(); sig=prev; while(sig!=null){

84

Page 85: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

prev=sig; for(int m=0;m<numnod();m++){ if(sig.setide().equals(nombres[m])) matrix[i][m]=prev.getVal(); if(i==m) matrix[i][m]=0;} sig=sig.getliga(); } i++;} } void floyd(){ fullmat(); int mat[][]=new int[20][20]; for(int i=0;i<numnod();i++) for(int j=0;j<numnod();j++) mat[i][j]=matrix[i][j]; for(int k=0;k<numnod();k++) for(int i=0;i<numnod();i++) for(int j=0;j<numnod();j++) if((mat[i][k]+mat[k][j])<mat[i][j]) mat[i][j]=mat[i][k]+mat[k][j]; for(int i=0;i<numnod();i++) System.out.print(" "+nombres[i]); System.out.print("\n"); for(int i=0;i<numnod();i++){ System.out.print(nombres[i]); for(int j=0;j<numnod();j++) System.out.print(" "+mat[i][j]+" "); System.out.print("\n"); } } void floyd2(){ fullmat(); int mat[][]=new int[20][20]; for(int i=0;i<numnod();i++) for(int j=0;j<numnod();j++) mat[i][j]=matrix[i][j]; for(int k=0;k<numnod();k++) for(int i=0;i<numnod();i++) for(int j=0;j<numnod();j++) if((mat[i][k]+mat[k][j])>mat[i][j]) mat[i][j]=mat[i][k]+mat[k][j]; for(int i=0;i<numnod();i++) System.out.print(" "+nombres[i]); System.out.print("\n"); for(int i=0;i<numnod();i++){ System.out.print(nombres[i]); for(int j=0;j<numnod();j++) System.out.print(" "+mat[i][j]+" "); System.out.print("\n"); } } void creaLis(String nom){ grafin p,prev,sig; grafini r=null;

85

Page 86: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

if(cabeza==null) cabeza=new grafin(nom); else{ prev=cabeza; sig=prev; while(sig!=null){ prev=sig; sig=sig.getliga(); } p=new grafin(nom); prev.setliga(p); } } void creaSub()throws IOException{ grafin p,prev,sig; sig=cabeza; prev=sig; while(sig!=null){ prev=sig; pide(sig); sig=sig.getliga(); } } void pide(grafin g)throws IOException{ int op; String aux,aux2; int val; int op2=2; BufferedReader in=new BufferedReader(new InputStreamReader(System.in)); System.out.println("Proporciona las relaciones con "+g.setide()); do{ do{ System.out.println("1.Introducir nuevo vertice\n2.Terminar con vertice principal"); op=Integer.parseInt(in.readLine()); }while(op>2||op<1); if(op==1){ do{ System.out.println("Introduce nombre del vertice a relacionar con "+g.setide()); aux2=g.setide(); aux=in.readLine(); if(!recoprin(aux)) System.out.println("Ese nodo es inexistente"); else if(!recopron(aux,aux2,g)) System.out.println("Ese nodo ya fue relacionado"); System.out.println("Deseas continuar con este eje?\n1.No\n(cualquier numero).Si"); op2=Integer.parseInt(in.readLine()); }while((!recoprin(aux)||!recopron(aux,aux2,g))&&op2!=1); if(op2!=1){ do{ System.out.println("Introduce valor del eje, mayor a cero"); val=Integer.parseInt(in.readLine()); }while(val<1); creaSb(aux,val,g);}}

86

Page 87: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

}while(op!=2); } boolean recoprin(String g){ boolean band=false; grafin prev,sig; sig=cabeza; prev=sig; while(sig!=null&&!g.equals(prev.setide())){ prev=sig; sig=sig.getliga(); } if(g.equals(prev.setide())) band=true; return band; } boolean recopron(String aux,String aux2,grafin g){ boolean band=true; grafini p,sig,prev; if(g.getsub()==null) if(aux.equals(aux2)) band=false; else{ p=g.getsub(); prev=p; sig=p; while(sig!=null&&!aux.equals(prev.setide())){ prev=sig; sig=sig.getliga(); } if(sig!=null) band=false; if(aux2.equals(aux)) band=false;} return band; } void creaSb(String s,int v,grafin m){ grafini p,prev,sig; if(m.getsub()==null){ p=new grafini(s,v,null); m.setsub(p);} else{ prev=m.getsub(); sig=prev; while(sig.getliga()!=null){ prev=sig; sig=sig.getliga(); } p=new grafini(s,v,prev.getliga()); prev.setliga(p); } } void mostrar(){ grafin prev,sig; prev=cabeza;

87

Page 88: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

sig=prev; while(sig!=null){ prev=sig; System.out.print(sig.setide()+"-->"); todo(sig.getsub()); System.out.print("\n"); sig=sig.getliga(); } } void todo(grafini m){ grafini prev,sig; prev=m; sig=prev; while(sig!=null){ prev=sig; System.out.print(" "+sig.getVal()+" "+sig.setide()+"-->"); sig=sig.getliga(); } } } d) Main para la representación de grafos mediante multilistas import java.io.*; class grafiti{ public static void main(String args[])throws IOException{ grafit grafo=new grafit(); int op,op2; BufferedReader in=new BufferedReader(new InputStreamReader(System.in)); do{ System.out.println("Opciones\n1.Crear vertices\n2.Asignar ejes\n3.Mostrar grafo\n4.Salir\n5.Cotos minimos(para verla presione 2 veces esta opcion)"); op=Integer.parseInt(in.readLine()); switch(op){ case 1:do{ do{ System.out.println("1.Insertar nuevo nodo\n2.Salir"); op2=Integer.parseInt(in.readLine()); }while(op<1||op>2); if(op2==1){ System.out.println("Proporciona nombre del nodo"); grafo.creaLis(in.readLine()); } }while(op2!=2); break; case 2:grafo.creaSub(); break; case 3:grafo.mostrar(); break; case 4:System.out.println("Finalizando..."); break; case 5:grafo.floyd(); break; /* case 6:grafo.floyd2(); break;*/

88

Page 89: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

default:System.out.println("Opcion Invalida"); } }while(op!=4); } } 4. Programa que obtiene los caminos mínimos de un grafo mediante la implementación

del algoritmo de Floyd . import java.io.*; import java.io.StreamTokenizer.*; class Grafo { File file; FileReader inFile; FileOutputStream fo; PrintStream out; String line; double time=0; final int INFINITO=999; double num=0; int ren=0; int col=0; int Matriz[][]=new int[70][70]; int P[][]=new int[70][70]; public static void main(String[] args) { if(args.length>=1) { try { Grafo g=new Grafo(args[0]); } catch(IOException e) {} } else System.out.println("De el archivo de entrada"); } public Grafo(String fileName) throws IOException { file=new File(fileName); inFile=new FileReader(file); Reader r=new BufferedReader(inFile); StreamTokenizer st=new StreamTokenizer(r); st.parseNumbers(); //lee numeros st.eolIsSignificant(true); //toma en cuenta \n int token=0;

89

Page 90: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

int aux=0; try { while(true) { token=st.nextToken(); if(token==st.TT_EOF) break; num=st.nval; if(token!=st.TT_EOL) //mientras no acabe el archivo { col=aux; Matriz[ren][col]=(new Double(num)).intValue(); aux++; } else { ren++; aux=0; } } col++; //el archivo debe terminar en \n file=new File("R_"+fileName); fo=new FileOutputStream(file); out=new PrintStream(fo,true); time=new Long(System.currentTimeMillis()).doubleValue(); out.println("Algoritmo Floyd"); out.println(); Despliega(Matriz,"La matriz del camino m s corto es: "); DespliegaCaminos(); time=(new Long(System.currentTimeMillis()).doubleValue()-time)/1000; out.println(); out.println("Tiempo de ejecucion= "+time+" segundos"); out.close(); } catch(EOFException e) {} } public void Floyd() { for(int k=0; k<col; k++) for(int i=0; i<ren; i++) for(int j=0; j<col; j++) if((Matriz[i][k]+Matriz[k][j])<Matriz[i][j]) { P[i][j]=k; Matriz[i][j]=Matriz[i][k]+Matriz[k][j]; } }

90

Page 91: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

void camino(int inicio, int fin) { if(P[inicio][fin]!=0) { camino(inicio, P[inicio][fin]); out.print(getPath(P[inicio][fin])); camino(P[inicio][fin],fin); } } String getPath(int x) { char to=inc('A',x); return "->["+to+"]"; } char inc(char x, int max) { for(int i=0; i<max; i++) x++; return x; } void Despliega(int [][]M, String men) { out.println(men); for(int i=0; i<ren; i++) { for(int j=0; j<col; j++) if(M[i][j]<INFINITO) //se concidera oo a cualquiera >= INFINITO out.print("oo"+'\t'); //oo=infinito out.println(); } out.println("Renglones= "+ren); out.println("Columnas= "+col); } void DespliegaCaminos() { out.println(); out.println("Los cminos mas cortos son: "); char A='A'; //se denotan los nodos por letras iniciando en A char X=A; char Y=A; for(int i=0; i<ren; i++) { for(int j=0; j<col; j++) { if(i!=j) { out.print("El camino mas corto del nodo ("+X+") al nodo ("+Y+") es: "); out.print("["+X+"]"); camino(i,j); out.print("->["+Y+"]"); out.print(" y tiene un costo de: "+Matriz[i][j]);

91

Page 92: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

out.println(); } Y++; } X++; Y=A; } } } 5. Programa de un grafo no dirigido utilizando el algoritmo de Dijkstra para caminos

minimos import java.util.*; public class Grafo_Nodirigido { Vector vertices=new Vector(); int[][] mat_ady; int[][] mat_costos; int[] costos_minimos; int[] p; Vector s=new Vector(); public int tam_matriz(String[][] datos) { Integer num; for(int i=0;i<datos.length;i++) { for(int j=0;j<datos[i].length-1;j++) { num=new Integer(Integer.parseInt(datos[i][j])); if(vertices.indexOf(num)==-1) vertices.addElement(num); } } vertices.trimToSize(); return vertices.capacity(); } public void crea_mat_ady(int tam,String[][] datos) { mat_ady=new int[tam][tam]; for(int k=0;k<tam;k++) for(int l=0;l<tam;l++) mat_ady[k][l]=0;

92

Page 93: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

for(int i=0;i<datos.length;i++) mat_ady[Integer.parseInt(datos[i][0])][Integer.parseInt(datos[i][1])]=1; } public void crea_mat_costos(int tam,String[][] datos) { mat_costos=new int[tam][tam]; for(int k=0;k<tam;k++) for(int l=0;l<tam;l++) mat_costos[k][l]=10000; for(int i=0;i<datos.length;i++) mat_costos[Integer.parseInt(datos[i][0])][Integer.parseInt(datos[i][1])]=Integer.parseInt(datos[i][2]); } public void imprime_matriz(int[][] mat) { for(int i=0;i<mat.length;i++) { for(int j=0;j<mat.length;j++) System.out.print("\t"+mat[i][j]); System.out.println(" "); } //System.out.println("\n"); } public int primero(int v) { int v_ady=-1; for(int i=0;i<mat_ady.length;i++) { if(mat_ady[v][i]==1) { v_ady=i; break; } } return v_ady; } public int siguiente(int v, int i) { int v_sig=-1; for(int j=i+1;j<mat_ady.length;j++) { if(mat_ady[v][j]==1) { v_sig=j; break;

93

Page 94: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

} } return v_sig; } public void encuentra_vertices_adyacentes(int v) { int i=primero(v); System.out.print("\"Los Nodos adyacentes a "+v+" son\" : "); do{ System.out.print("\t"+i+""); i=siguiente(v,i); }while(i!=-1); System.out.print("\n"); } public void dijkstra() { costos_minimos=new int[mat_costos.length]; p=new int[mat_costos.length]; int min,ind,w; s.addElement(new Integer(0)); vertices.removeElementAt(0); vertices.trimToSize(); for(int i=1;i<mat_costos.length;i++) costos_minimos[i]=mat_costos[0][i]; for(int m=1;m<p.length;m++) p[m]=0; for(int j=0;j<mat_costos.length-1;j++) { ind=0; min=costos_minimos[((Integer)vertices.elementAt(0)).intValue()]; w=((Integer)vertices.elementAt(0)).intValue(); for(int k=1;k<vertices.size();k++) { if(min>costos_minimos[((Integer)vertices.elementAt(k)).intValue()]) { min=costos_minimos[((Integer)vertices.elementAt(k)).intValue()]; ind=k; w=((Integer)vertices.elementAt(k)).intValue(); } } vertices.removeElementAt(ind); vertices.trimToSize(); s.addElement(new Integer(w)); s.trimToSize();

94

Page 95: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

for(int l=0;l<vertices.size();l++) { if(costos_minimos[((Integer)vertices.elementAt(l)).intValue()]>costos_minimos[w]+mat_costos[w][((Integer)vertices.elementAt(l)).intValue()]) { costos_minimos[((Integer)vertices.elementAt(l)).intValue()]=costos_minimos[w]+mat_costos[w][((Integer)vertices.elementAt(l)).intValue()]; p[((Integer)vertices.elementAt(l)).intValue()]=w; } } } } public void recorre(int destino) { Vector recorrido=new Vector(); System.out.print("\"Camino mas corto entre el nodo 0 y el nodo "+destino+"\": "); if((destino!=0)&&(costos_minimos[destino]!=10000)) { do{ recorrido.insertElementAt(new Integer(destino),0); destino=p[destino]; }while(destino!=0); recorrido.insertElementAt(new Integer(0),0); for(int t=0;t<recorrido.size();t++) System.out.print(recorrido.elementAt(t)+"°"); System.out.print("\n"); }else System.out.println("****"); } public static void main(String[] args) { String[][] datos; int i; Grafo_Nodirigido nografo=new Grafo_Nodirigido(); try{ Lee_Grafo archgrafo=new Lee_Grafo(args[0]); datos=archgrafo.lee_arch(); if(archgrafo.formato_valido()) { int n=nografo.tam_matriz(datos); nografo.crea_mat_ady(n,datos); nografo.crea_mat_costos(n,datos);

95

Page 96: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

System.out.println("\"Esta es la Matriz de Adyacencia de nuestro Grafo\": "); nografo.imprime_matriz(nografo.mat_ady); System.out.println("\"Esta es la Matriz de Costos de nuestro Grafo\": "); nografo.imprime_matriz(nografo.mat_costos); for(i=0;i<n;i++) nografo.encuentra_vertices_adyacentes(i); nografo.dijkstra(); System.out.print("\"Acontinuacion presentamos las distancias mas cortas\"\n"); for(int v=1;v<nografo.costos_minimos.length;v++) System.out.print("\t\t\tDel nodo 0 a el nodo"+v+": "+nografo.costos_minimos[v]+"\n"); for(i=0;i<n;i++) nografo.recorre(i); } else System.out.println("ERROR: El archivono contiene un formato correcto"); }catch(Exception e){ /*System.err.println("Falta nombre del archivo correcto")*/;} } } * Clase para implementar programas de grafos import java.io.*; import java.util.*; public class Lee_Grafo { public String[][] datos; String nomarch; boolean correcto; public Lee_Grafo(String nomarch) { this.nomarch=nomarch; this.correcto=true; this.datos=null; }

96

Page 97: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

public int cuenta_lineas() { FileReader fich; int x=0; try{ fich=new FileReader(nomarch); } catch(Exception e){System.err.println(e);return -1;} BufferedReader fichGrafo=new BufferedReader(fich); try{ while(fichGrafo.readLine()!=null) x++; }catch(Exception e){ System.err.println("Error: "+e);} return x; } public boolean formato_valido() { if(correcto) return true; else return false; } public String[][] lee_arch() { FileReader fich; try{ fich=new FileReader(nomarch); }catch(Exception e){ System.err.println(e);return datos;} BufferedReader fichGrafo=new BufferedReader(fich); try{ String linea; int x=-1; //System.out.println(cuenta_lineas()); datos=new String[cuenta_lineas()][3]; while((linea=fichGrafo.readLine())!=null) { x++; StringTokenizer tokens=new StringTokenizer(linea); try{ if(tokens.countTokens()!=3) { correcto=false; throw new Exception();

97

Page 98: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

} datos[x][0]=tokens.nextToken(); datos[x][1]=tokens.nextToken(); datos[x][2]=tokens.nextToken(); }catch(Exception e){ System.err.println("ERROR (linea "+x+"):"+linea);} } }catch(Exception e){ System.err.println("Error :"+e);} return datos; } }

98

Page 99: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

4. Árboles Un árbol es una estructura jerárquica, organizada y dinámica aplicada sobre una colección de objetos llamados nodos.

Jerárquica porque los componentes están a distinto nivel. Organizada porque importa la forma en que este dispuesto el contenido. Dinámica porque su forma, tamaño y contenido pueden variar durante la

ejecución. Los árboles genealógicos y los organigramas son ejemplos comunes de árboles. Entre otras cosas, los árboles son útiles para analizar circuitos eléctricos, para representar la estructura de fórmulas matemáticas, para organizar información en una base de datos, para representar el sistema de archivos y para analizar la estructura sintáctica de un programa fuente en los compiladores. Existen diferentes formas de representación de un árbol, entre las más comunes se tienen las siguientes:

Mediante círculos y flechas a

f e

b d c

Mediante paréntesis anidados: ( a ( b (e,f), c, d ))

Mediante notación decimal de Dewey 1a, 1.1b, 1.1.1e, 1.1.2f, 1.2c, 1.3d

Indentado, mediante nodos. Un buen ejemplo, es la forma de representar gráficamente las carpetas (directorios) de un sistema de archivos. En este caso, una carpeta es un nodo padre de los archivos y subcarpetas contenidas en el.

99

Page 100: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

La forma de representación más fácil, común y que utilizaremos a lo largo de estas notas es la representación mediante círculos y flechas.

IV.1 Definición y conceptos básicos Definición Formalmente, un árbol se puede definir recursívamente como sigue [ ]:

1. Un solo nodo es, por sí mismo, un árbol. Ese nodo es también la raíz de dicho árbol.

2. Supóngase que r es un nodo y que A1, A2, ...An son árboles con raíces r1, r2, ...rn, respectivamente. Se puede construir un nuevo árbol diciendo que r se constituya en el padre de los nodos r1, r2, ...rn. Por lo que, en dicho árbol, r será ahora la raíz y A1, A2, ...An serán los subárboles de r. Los nodos r1, r2, ...rn serán ahora también hijos del nodo r (ver Figura 1)

Algunas veces se incluye entre los árboles el árbol nulo o vacío. El cual es un árbol sin nodos que se representa mediante la letra Λ.

100

Page 101: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Figura 1 Ejemplo de un árbol

Generalmente, se crea una relación o parentesco entre los nodos de un árbol que impone una estructura jerárquica y que da lugar a términos como padre, hijo, hermano, antecesor, sucesor, etc. Se dice que la raíz de cada subárbol Ak es un hijo de r y que r es el padre de cada raíz de los subárboles. En principio cualquier nodo del árbol puede tener un número arbitrario de nodos hijos, a esto se le conoce como un árbol general. La Figura 2 muestra un ejemplo de esto. Si se limita el número de nodos hijos para cada nodo del árbol, digamos a un número n > 2 (llamado la aridad del árbol), entonces el árbol de aridad n es llamado n-ario.

r

A1 A2An

A

B C D E

G H

K

I J M

L

F

Figura 2 Arbol General

El nodo A es la raíz (padre). Los hijos de A son B,C, D, E Los nodos F,G, M son hermanos e hijos de B A es abuelo de H K y L son hijos de H y nietos de A

101

Page 102: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Con estas consideraciones podemos definir las siguientes características y propiedades de los árboles. Algunos de los siguientes conceptos; si embargo, no son uniformes en toda la literatura referente a la teoría de árboles. Conceptos básicos

a) Si hay un camino de A hasta B, se dice que A es antecesor de B, y que B es sucesor de A.

b) Padre es el antecesor inmediato de un nodo c) Hijo, cualquiera de sus descendientes inmediatos. d) Antepasado de un nodo, es cualquier antecesor de dicho nodo. e) Descendiente de un nodo, es cualquier sucesor de dicho nodo. f) Hermano de un nodo, es otro nodo con el mismo padre. g) Raíz es el nodo que no tiene ningún predecesor. h) Hoja (o nodo terminal) es el nodo que no tiene sucesores. i) Los nodos que tienen predecesor y sucesor se llaman nodos interiores. j) Rama es cualquier camino del árbol. k) Bosque es un conjunto de árboles desconectados. l) Grado de un nodo, es el número de flechas que salen de ese nodo. El número de

flechas que entran siempre es uno. m) Grado de un árbol, es el mayor grado que puede hallarse en sus nodos. n) Nivel o profundidad de un nodo, es la longitud del camino desde la raíz hasta ese

nodo. El nivel puede definirse como 1 para la raíz y nivel(predecesor)+1 para los demás nodos.

o) Generación, es un conjunto de nodos con la misma profundidad. p) Altura de un nodo, es la longitud del camino desde ese nodo hasta la hoja más

alejada (la altura de una hoja es 0 y la de un árbol vacío se considera -1). q) Altura de un árbol, es la altura desde la raíz. Esto es, es el máximo de los niveles

de todos los nodos del árbol. r) Un camino de un nodo n1 a otro nk, se define como la secuencia de nodos n1, n2,

... nk tal que ni es padre de ni+1 para 1 ≤ i < k. s) Longitud del camino entre 2 nodos: Es el número de arcos que hay entre ellos. Se

supone que la longitud del camino a la raíz es 1, por lo que su hijo tiene longitud 2. t) Longitud del camino interno (LCI) de un árbol, es la suma de las longitudes a todos

los nodos. Se supone que la longitud de la raíz es uno.

∑=

=h

i

LiNiLCI1

*

donde Ni=número de nodos en el nivel i, Li=longitud hasta generación i y h es la altura

u) Media de longitud del camino interno (MLCI) de un árbol, es el promedio de accesos al árbol para llegar a cualquier nodo del árbol y se calcula

nLCIMLCI =

donde n es el número de nodos del árbol.

102

Page 103: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Ejemplo: Utilizando el árbol de la Figura 2 tenemos: a) A es antecesor de F y F es sucesor de A b) B es el padre de G y H es el padre de K c) I y J son hijos de E y K y L son hijos de H. d) A , D y H son antepasados de K y L e) Los descendientes de D son H, K y L f) I y J son hermanos. B,C, D, y E son también hermanos. g) El nodo A es la raíz h) F,G, K,L, I y J son hojas del árbol i) B, D, H, E son nodos interiores l) El grado de A es 4

El grado de B es 2 El grado de C es 0

m) El grado del árbol es 4 n) El nivel de A es 1

El nivel de B es 2 El nivel de H es 3 El nivel de K es 4

o) F,G, H, I, y J son de la generación 3 p) La altura del nodo D es 2

La altura del nodo H es 1 La altura del nodo G es 0

q) La altura del árbol es 4 r) El camino de A a K es único y lo forman los nodos A-D-H-K s) El nodo B tiene longitud de camino 2 desde A

El nodo I tiene longitud de camino 3 desde A El nodo K tiene longitud de camino 4 desde A

Orden de los nodos Generalmente los árboles de un nodo se ordenan de izquierda a derecha. Por ejemplo, los árboles de la Figura 3 son distintos porque los dos hijos del nodo x aparecen en diferente orden en los dos árboles. Si no se toma en cuenta el orden de los nodos hijos, entonces se habla de un árbol no ordenado.

x

y z z y

x

Figura 3 árboles ordenados distintos El orden de izquierda a derecha de los hermanos se puede extender para comparar dos nodos cualesquiera entre los cuales no exista la relación antecesor-descendiente. La

103

Page 104: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

regla que se aplica es que si y y z son hermanos y y está a la izquierda de z, entonces todos los descendientes de y estarán a la izquierda de todos los descendientes de z. Esto es, y es menor que z.

IV.2 Árboles Binarios Un árbol binario es un árbol de grado 2, en el que todo nodo del árbol tiene un subárbol binario izquierdo y derecho asociados (ver la Figura 4 )

Figura 4 Árbol binario genérico

Subárbol Izquierdo

Subárbol Derecho

Raíz

Terminología de Arboles Binarios Arbol Binario Completo o Lleno: Es un árbol binario en el que todos sus nodos, excepto las hojas, tienen siempre dos hijos ( el subárbol izquierdo y el derecho) no nulos. El número de nodos de un árbol completo se calcula por la fórmula:

Número de nodos = 2h-1 (donde h es la altura) Además, siendo 1 el nivel de la raiz, el número máximo de nodos en un nivel k será 2k–1. Arbol Binario Completo de Altura o Profundidad H: Es un árbol Binario Completo en donde todas las hojas están en el nivel H. Esta es una de las pocas estructuras de árbol que se pueden representar eficientemente usando arreglos. Arbol Binario de Expresión (ABE):Una de las aplicaciones de árboles binarios son los llamados árboles de expresión. Una expresión es una secuencia de componentes léxicos (tokens), que siguen reglas preescritas. Un token puede ser un operador o un operando. Las propiedades de un árbol de expresión son las siguientes:

1. Cada hoja es un operando 2. El nodo raíz y los nodos internos son operadores 3. Los subárboles son sub-expresiones en las que el nodo raíz es un operador

La Figura 5 muestra un ejemplo de un árbol de expresión de la expresión (a+b) * (c-d)

104

Page 105: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

*

Figura 5 Ejemplo de árbol de expresión Arboles Binarios de búsqueda:Un árbol binario de búsqueda es un árbol en el que todo nodo existente tiene un solo elemento y cumple lo siguiente:

todas las claves del subárbol izquierdo son menores que la raíz, todas las claves del subárbol derecho son mayores que la raíz, los subárboles izquierdo y derecho son también árboles de búsqueda.

Los nodos insertados en árboles de búsqueda binarios se insertan como hojas. Hacerlo de otro modo no solo no mejoraría la eficiencia buscada, sino que además habría que reajustar el árbol tras cada inserción. La Figura 6 muestra un ejemplo de un árbol de búsqueda de número ordenados.

Figura 6 Ejemplo de árbol binario de búsqueda Por ejemplo, al insertar la clave 8, el árbol de la Figura 6, quedaría de la siguiente forma:

Figura 7 Inserción en un árbol binario de búsqueda

-+

b c d a

5 10

6 9

7

11 3

5 10

6 9

7

11 3

8

105

Page 106: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Conversión de árboles generales como binarios Como veremos más adelante, la manipulación y representación de árboles binarios es mucho más eficiente que la de árboles generales, por lo que es muy útil conocer una forma de convertir un árbol general en un árbol binario. Esto se puede lograr a través de los siguientes pasos: través de los siguientes pasos: 1. Enlazar horizontalmente todos los hermanos. 1. Enlazar horizontalmente todos los hermanos. 2. Para cada nodo, enlazar verticalmente el nodo padre con el nodo hijo que está más

a la izquierda, eliminado el vínculo del padre con el resto de sus hijos. 2. Para cada nodo, enlazar verticalmente el nodo padre con el nodo hijo que está más

a la izquierda, eliminado el vínculo del padre con el resto de sus hijos. 3. Todos los enlaces verticales son hijos izquierdos del nuevo árbol binario. 3. Todos los enlaces verticales son hijos izquierdos del nuevo árbol binario. 4. Todos los enlaces horizontales (hermanos) son hijos derechos. 4. Todos los enlaces horizontales (hermanos) son hijos derechos.

Figura 8 Ejemplo de la representación de Árboles Generales como Árbol Binario Figura 8 Ejemplo de la representación de Árboles Generales como Árbol Binario Conversión de Bosques como Arboles Binarios Conversión de Bosques como Arboles Binarios 1. Enlazar horizontalmente las raíces de los distintos árboles generales. 1. Enlazar horizontalmente las raíces de los distintos árboles generales. 2. Para todo nodo del bosque enlazar verticalmente el nodo padre con el nodo hijo que

está más a la izquierda, eliminando el vínculo del padre con el resto de sus hijos. 2. Para todo nodo del bosque enlazar verticalmente el nodo padre con el nodo hijo que

está más a la izquierda, eliminando el vínculo del padre con el resto de sus hijos. 3. Enlazar horizontalmente los hermanos. 3. Enlazar horizontalmente los hermanos. 4. Todos los enlaces verticales son hijos izquierdos del nuevo árbol binario. 4. Todos los enlaces verticales son hijos izquierdos del nuevo árbol binario. 5. Todos los enlaces horizontales son hijos derechos. 5. Todos los enlaces horizontales son hijos derechos.

a

g h

c db

e f

j k i

a

g h

c db

e f

j k i

e

a

h

g

c

i f

j

k

d

b

a

b c d

f e g h

i j k

106

Page 107: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Figura 9 Ejemplo de la conversión de un bosque con árbol binario

Recorridos Muchas de las operaciones del TDA -Arbol Binario implican recorrer o visitar cada uno de los nodos del árbol, ya sea para insertar, eliminar, visitar o buscar un elemento de una forma eficiente. Existen en general cuatro formas de hacerlo, tres de naturaleza recursiva y uno más de naturaleza iterativa.

a. Recorrido en PreOrden (u orden previo).- Iniciando en la raíz, primero se visita ésta, luego se hace un recorrido en PreOrden del subárbol Izquierdo y luego en el subárbol derecho, también en PreOrden.

b. Recorrido en InOrden (orden simétrico).- Iniciando en la raíz, primero se efectúa

un recorrido en InOrden en el subárbol izquierdo, luego se visita la raíz, y luego se visita el subárbol derecho también en InOrden.

c. Recorrido en PostOrden (u orden posterior).- Iniciando en la raíz, primero se

visita en PostOrden el subárbol izquierdo, luego el subárbol derecho, también en PostOrden, y por último se visita la raíz.

d. Recorrido por niveles.- Iniciando en la raíz, primero se visita la raíz, y luego se

visitan los elementos del segundo nivel de izquierda a derecha, seguidos por los del nivel 3 en el mismo orden, y así sucesivamente hasta terminar de visitar todos los elementos.

Como ejemplo consideremos el árbol de la Figura 10.

Figura 10 Ejemplo 1 de recorridos

a

d e

j k i

c

g h

d

f

PreOrden: daebcp InOrden: aedcbp PostOrden: eacpdb Niveles: dabecp

d

a b

e c p

107

Page 108: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Otro ejemplo más se muestra en la Figura 11.

a

g h

c db

e f

j k i

Figura 11 Ejemplo 2 de recorridos El siguiente ejemplo ( Figura 12) muestra el recorrido en un árbol de expresión.

Figura 12 Recorrido en un árbol de expresión Así, el algoritmo para el recorrido en PreOrden sería de la siguiente forma: PreOrden (NodoB Nodo) { If (Nodo != null) {

Imprime(Nodo.info); //Visita Raiz PreOrden(Nodo.Izq); PreOrden(Nodo.Der);

} }

+ -

b c

PreOrden : a b e i j k f c d g h InOrden: i e j k b f a c g d h Postorden : i j k e f b c g h d a

PreOrden : *+ab-cd (expresión Prefija) InOrden: a+b*c-d (expresión Infija) Postorden : ab+cd-* (expresión Postfija)

*

d a

108

Page 109: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Los algoritmos para InOrden y PostOrden son semejantes. Especificación del TDA - Arbol Binario Los elementos de un árbol binario son los Nodos Binarios. Se les llama así puesto que además de guardar información asociada con el nodo, tienen información sobre sus sucesores en la estructura jerárquica del árbol. Por esto, es conveniente especificar e implementar primero el TDA-Nodo Binario, antes de especificar el TDA - Arbol Binario. Implementación del TDA-Nodo Binario (Código en Java) public class NodoBin extends Nodo { protected NodoBin izq; protected NodoBin der; public NodoBin() { super(); izq = null; der = null; } public NodoBin(String str) { super(str); izq = null; der = null; } public NodoBin(Nodo x) { super(x); izq = null; der = null; } public void setIzq(NodoBin x) { izq = x; } public void setDer(NodoBin x) { der = x; } public NodoBin getIzq() { return izq; } public NodoBin getDer() { return der; } public static void main(String [] args) {

109

Page 110: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

NodoBin e1 = new NodoBin("6678-VB"); NodoBin e2 = new NodoBin("56TY-UYA"); NodoBin e3 = new NodoBin("12345"); e1.show(); e2.show(); e3.show(); } } Para identificar el estado del TDA - Arbol Binario se debe considerar lo siguiente:

Peso o Número de elementos del árbol Grado del árbol Altura del árbol Raíz del árbol Elemento corriente o actual del árbol Elementos del árbol

Además, se debe asumir que:

a. El árbol binario está formado por elementos del tipo TDA-Nodo Binario y no hay nodos repetidos en el árbol.

b. El árbol binario puede tener cero o más nodos, si tiene cero, se trata de una árbol binario vacío.

c. De existir el primer elemento de un árbol binario, este es su nodo raíz. d. El nodo sobre el que se efectúan algunas operaciones es el nodo actual o nodo

corriente del árbol binario. e. Todos los nodos excepto la raíz, tienen un nodo predecesor o nodo padre, así

como dos sucesores: el subárbol izquierdo y derecho, respectivamente. f. Los nodos hojas tienen subárboles binarios izquierdo y derecho vacíos.

Operaciones del TDA-Arbol Binario Las posibles operaciones con el TDA - Arbol Binario serían:

CrearArbolBinario insertar borrar getraiz getSubIzq getSubDer getPeso getAltura getGrado esVacio esCompleto showPreOrden showInOrden showPostOrden showNiveles

110

Page 111: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Implantación del TDA-Arbol Binario Existen diversas formas de implantar un árbol binario entre estas están:

1. Árboles Enlazados

a) Árboles encadenados. b) Árboles con encadenamiento al padre. c) Árboles enhebrados por la derecha. d) Árboles enhebrados por la izquierda. e) Árboles totalmente enhebrados.

2. Arreglos lineales

a) Arreglos Estáticos b) Arreglos Dinámicos

Implementación del TDA-Arbol Binario con Arboles Enlazados

Árboles Encadenados Es la implementación más sencilla y una de las más usadas. La idea es la de representar un árbol binario a través de nodos encadenados (ver Figura 13 )

Figura 13 Representación de un árbol utilizando nodos encadenados

111

Page 112: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Implementación del TDA-Arbol Binario con Arboles con Encadenamiento al Padre Es una variante de la implementación de árbol encadenado, consiste en que cada elemento del árbol mantiene una referencia a su padre. Esto permite ascender con facilidad por la estructura jerárquica en busca de los antecesores. (ver Figura 14)

Figura 14 Representación del TDA-Arbol Binario con Encadenamiento al Padre

En los árboles enhebrados todas las referencias nulas, son reemplazadas por referencias hacia sus predecesores y/o sucesores, según su recorrido en InOrden en el árbol Binario. A estas ligas en un árbol enhebrado se les conoce como lianas. La idea de esta implementación consiste en mantener un encadenamiento adicional, que permita a algunas operaciones del TDA-Árbol binario moverse con mayor facilidad al interior del árbol, lo que va a permitir realizar recorridos, inserciones y eliminaciones no recursivas. Para distinguir si se trata de una referencia a un hijo o a un sucesor/predecesor en InOrden, el TDA-NodoBin requiere de un campo adicional, que le indique si se trata de una rama o de una liana.

Ejemplo de Implementación del TDA-Arbol Binario con Arboles Encadenados (Código en Java) public class ArbBin { protected NodoBin raiz; public ArbBin() { raiz = null; } public ArbBin(NodoBin n) {

112

Page 113: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

raiz = n; } public void insertar(NodoBin r, NodoBin izq, NodoBin der) { r.setIzq(izq); r.setDer(der); } public NodoBin getRaiz() { return raiz; } public ArbBin getSubIzq() { return new ArbBin(raiz.getIzq()); } public ArbBin getSubDer() { return new ArbBin(raiz.getDer()); } public boolean esVacio() { if(raiz == null)

{ return true; } else

{ return false; } } public static void showPreOrden(ArbBin a) { if(a.esVacio() == false) { a.getRaiz().show(); showPreOrden(a.getSubIzq()); showPreOrden(a.getSubDer()); } } public static void showInOrden(ArbBin a) { if(a.esVacio() == false) { showInOrden(a.getSubIzq()); a.getRaiz().show(); showInOrden(a.getSubDer()); } } public static void showPostOrden(ArbBin a) { if(a.esVacio() == false) { showPostOrden(a.getSubIzq()); showPostOrden(a.getSubDer()); a.getRaiz().show(); } } public static void main(String [] args) { NodoBin n1 = new NodoBin("1"); NodoBin n2 = new NodoBin("2");

113

Page 114: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

NodoBin n3 = new NodoBin("3"); NodoBin n4 = new NodoBin("4"); NodoBin n5 = new NodoBin("5"); ArbBin a = new ArbBin(n1); a.insertar(n1,n2,n3); a.insertar(n3,n4,n5); showPreOrden(a); showInOrden(a); showPostOrden(a); } }

Implementación del TDA - Arbol Binario con arreglos dinámicos Es posible utilizar una estructura lineal para representar un árbol binario, aunque es de esperarse que no sea muy eficiente. Para el caso de un árbol binario de altura H, éste se puede representar en un arreglo lineal de 2H-1 elementos:

a={x0, x1, …, xn}, n� < 2k-1. 1. Primero debe completar el arbol de tal forma que este se convierta en un árbol binario

completo de altura H. Todos los elementos nulos pueden representarse con un símbolo especial.

2. Colocar, secuencialmente en el arreglo, todos los elementos del árbol binario completo, según su recorrido por niveles. No olvide colocar también los elementos nulos.

3. De esta forma, el k-ésimo elemento del arreglo tiene a sus hijos izquierdo y derecho en las posiciones 2k+1 y 2k+2, respectivamente.

A continuación, en la Figura 15, se muestra la implantación de un árbol binario urilizando arreglos.

Figura 15 Representación de un árbol con arreglos

IV.3 Árboles AVL Un arbol AVL (Adelson-Velskii & Landis) es un árbol binario de búsqueda con una condición de equilibrio. Una buena condición de equilibrio es pedir que, para todo nodo en

114

Page 115: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

el árbol, la altura1 de sus subárboles izquierdo y derecho difiera como máximo en 1. En la Figura 16, se muestran dos ejemplos de árboles AVL válidos. 1 6 2 8

Figura 16 Ejemplos de árboles AVL

A continuación, se muestran dos ejemplos de árboles que no cumplen la condición de equilibrio establecida y por lo tanto no son árboles AVL.

Figura 17 Ejemplo de árboles que no son AVL

Inserción de un nodo Podemos insertar un nodo utilizando el algoritmo de inserción de un nodo en un árbol binario de búsqueda, comparando la clave del nuevo elemento con la raíz e insertando el nodo en el subárbol izquierdo o derecho según corresponda. Sin embargo, la razón por la que la inserción en un árbol AVL es potencialmente difícil es que al insertar un nodo se puede violar su condición de equilibrio. En la siguiente figura se muestra lo que pasaría al insertar la clave 6 En este caso se tiene que restaurar la condición de equilibrio antes de terminar el proceso de inserción. Para saber cuando se ha perdido la propiedad AVL es 1 Altura de un nodo es la longitud del nodo a la hoja más alejada. Altura de una hoja es cero, altura de árbol vacío es –1

2 4

1 4 7

33

5 6

24 6

3

8

1 4

73

7

2 8

Condición de equilibrio AVL, destruida en el nodo 8 (Las alturas de sus subárboles izquierdo y derecho difieren en más de 1)

5

2 8

1 4 7

3 6Nuevo nodo

115

Page 116: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

necesario conocer el factor de equilibrio de cada nodo afectado por la inserción o borrado. Esto puede hacerse utilizando alguna de las siguientes formas:

Calculando la diferencia de altura de los subárboles para cada nodo. Almacenando la altura de cada nodo en un campo extra del propio nodo. Almacenando la diferencia de altura de cada nodo en un campo extra del propio

nodo. Este campo se llama factor de equilibrio (FE) y es igual a (altura subárbol derecho)–(altura subárbol izquierdo).

La última opción es la más eficiente. A cambio exige que:

1. Al insertar un nuevo nodo, este campo tomará el valor cero ya que siempre se inserta por las hojas.

2. Después de cada inserción hay que revisar el FE de los nodos involucrados, que serán los que componen el camino desde la raíz del árbol hasta el padre del nodo insertado.

3. Decimos que el nodo esta desequilibrado a la izquierda o derecha cuando su FE es, respectivamente, negativo o positivo. Para corregir el FE de un nodo, debemos realizar sobre él una rotación simple o doble según corresponda.

Restauración de propiedad AVL Como mencionamos anteriormente, al insertar un nuevo nodo (o al borrarlo) es posible que se destruya la condición de equilibrio requerida por los árboles AVL. Para restaurar esta propiedad se puede hacer una modificación al árbol conocida como rotación. Básicamente, existen dos tipos de rotaciones:

1. Rotación sencilla (simple) 2. Rotación doble

No es obligatorio que la rotación se haga en la raíz del árbol, se puede hacer en cualquier nodo del árbol, ya que cualquier nodo es raíz de algún subárbol y puede transformar cualquier árbol en otro. Rotaciones simples Sea el árbol binario de búsqueda

Que sabemos?

1. B < A 2. Todos los elementos de T1 son

menores que B 3. Todos los elementos de T3 son

mayores que A 4. Todos los elementos de T2 están

entre A y B

A B T3

T1 T2

116

Page 117: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

B Así, el método ás sencillo para arreglar un árbol AVL, si la inserción (o borrado) causa que algún nodo pierda la propiedad de equilibrio es hacer una rotación en ese nodo, bajo las siguientes consideraciones:

Si está desequilibrado a la izquierda (FE < –1), y su hijo derecho tiene el mismo signo (–) hacemos rotación sencilla izquierda.

Si está desequilibrado a la derecha (FE > +1), y su hijo izquierdo tiene el mismo signo (+) hacemos rotación sencilla derecha.

A continuación se muestra gráficamente las rotaciones sencillas a izquierda y erecha. d

A

T3

T1

T2

Al realizar una rotación simple a la derecha, cambia la estructura del árbol pero preserva la propiedad de árbol binario de búsqueda

117

Page 118: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Rotacionaciones dobl

otación doble de derecha a izquier

otaciones dobles

jo derecho tiene distinto

o izquierdo tiene distinto signo (–) hacemos rotación doble derecha-izquierda.

es dobl

otación doble de derecha a izquier

otaciones dobles

jo derecho tiene distinto

o izquierdo tiene distinto signo (–) hacemos rotación doble derecha-izquierda.

es es RR da da

C

A T1 B RR

Si está desequilibrado a la izquierda (FE < –1), y su hi

Si está desequilibrado a la izquierda (FE < –1), y su hisigno (+) hacemos rotación doble izquierda-derecha.

Si está desequilibrado a la derecha (FE > +1), y su hijsigno (+) hacemos rotación doble izquierda-derecha.

Si está desequilibrado a la derecha (FE > +1), y su hij

T2

T4

T3

B

C A

T4 T3 T1 T2

118

Page 119: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

119

Page 120: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Rotaciones para equilibrar un árbol AVL

A

B

T1 T2

T3 B

A

T3T2

T1

Rotación derecha A

B

T1 T2

T3

Rotación izquierda

C

Rotación doble izquierda

Rotación doble derecha

A

T2

B

C

T3 T4T1

T4 B

A

T3 T2

T1

C

T3

T4 B

A

T1

T2

Nótense las equivalencias en:

la rotación sencilla (ej: derecha) ( (T1 A T2) B T3) = (T1 A (T2 B T3) ) y la rotación doble (ej: izquierda) ( T1 A ((T2 B T3) C T4) = (T1 A T2) B (T3 C T4) )

Cuando el nuevo nodo insertado es un valor intermedio entre el nodo que incumple la condición de equilibrio y el hijo del nodo desequilibrado por el que hemos pasado para insertar el nuevo nodo, se usara rotación doble para equilibrar el árbol. En los demás casos usaremos rotación simple. ALGORITMO DE ROTACIÓN

1. Iniciamos en el nodo insertado y subir en el árbol. 2. Actualizar la información de Factor de Equilibrio en cada nodo del camino. 3. Si se encuentra un nodo desequilibrado aplicar una rotación ajustando su

equilibrio. a. Si está desequilibrado a la izquierda (FE<-1) y si su hijo derecho tiene el

mismo signo (-) hacemos rotación sencilla a la derecha. b. Si está desequilibrado a la derecha (FE>+1)y su hijo izquierdo tiene el

mismo signo (+) hacemos rotación sencilla a la izquierda. c. Si está desequilibrado a la izquierda (FE<-1) y su hijo izquierdo tiene

distinto signo (+)hacemos rotación doble izquierda-derecha. 4. Si llegamos a la raíz, terminamos.

120

Page 121: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

A continuación se muestran los algoritmos para rotar a la izquierda y a la derecha. Implementación del algoritmo para rotar (Código en Java) public void RotarIzquierda(NodoBin x) {

NodoBin y; y = x.der;

x.der = y.izq; y.izq = x; x = y;

}

public void RotarDerecha(NodoBin x) {

NodoBin y; y = x.izq;

x.izq = y.der; y.der = x;

x = y; }

121

Page 122: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

5. APLICACIONES DE LOS TIPOS ABSTRACTOS DE DATOS 5.1 Almacenamiento disperso (hash)

El método de direccionamiento de acceso aleatorio, conocido como HASH (dispersión), es una útil e ingeniosa forma de convertir la clave de un registro (campo que lo identifica) en un número pseudo-aleatorio, que sirve para determinar la dirección física donde se almacena dicho registro. La figura siguiente ilustra tal idea.

Espacio

Físico

de

Direcciones

clave Algoritmo que

convierte la clave en un número

Registro Esto recuerda claramente la definición de función: función h O bien como: h: A B a h(a)

CONJUNTO A

CONJUNTO B

Así tenemos que h debe ser una función, tal como se define en matemáticas.

Ahora bien, todo parece indicar que el problema principal del hash consiste en la construcción de la función h. ¿ Qué resultados se esperan con tal función h ?

a) Que distribuya de la manera más uniforme posible las claves en todo el espacio de direccionamiento.

b) Que el valor obtenido, al aplicar h a alguna clave, no desborde el espacio de

direccionamiento.

Para el inciso (b) la solución es trivial si aplicamos al valor obtenido la función módulo n, donde n sería el tamaño del espacio de direcciones. Así tendríamos h mod n.

122

Page 123: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Llama la atención que este método requiere un espacio de direcciones continuo (almacenamiento secuencial) aunque su manejo parezca más de tipo dinámico.

Sobre el inciso (a), no existe método alguno que nos asegure que la función h distribuirá de manera totalmente uniforme las claves sobre el espacio de direcciones. Así puede llegar un momento en que: h(a) = h(b) con a <> b.

Cuando esto sucede se dice que existe una colisión : "a dos o más registros con clave diferente corresponde una misma dirección en el espacio de direcciones". ¿ Qué hacer en estos casos ? Existen varios métodos para resolver las colisiones. La elección de un método depende de la función h que se tenga implementada, la naturaleza del problema y la cantidad de memoria disponible. Antes de tratar los métodos para resolver colisiones, hablemos sobre los métodos para obtener la función h. La función h se puede clasificar en dos grupos: Numérica h Lógica

La función h es numérica si para su implementación se utilizan operaciones aritméticas (sumas, divisiones, etc.) y es lógica si se utilizan operaciones lógicas (and, or, etc.). Aunque en algunos casos la función h es híbrida, esto es, son combinación de una o varias operaciones aritméticas con una o varias operaciones lógicas. A continuación se presentan algunos de los métodos más comunes para desarrollar funciones hash. Centro de los cuadrados

La clave se multiplica por sí misma, se toman los dígitos centrales del cuadrado y se ajustan al espacio de direccionamiento. Ejemplo: Si la clave es 1722148 su cuadrado es 029634933904, la clave obtenida sería 3493 mod n. División

La clave se divide por un número aproximadamente igual al número de direcciones disponibles y el resto de la división es la dirección deseada. Por razones obvias como divisor se usa el número primo más cercano al tamaño del espacio de direcciones o uno que no incluya factores primos pequeños. Una de las razones por las cuales la división tiende a generar menos desbordes que los algoritmos generadores de números aleatorios es que en la mayoría de los casos las claves tienen valores consecutivos.

123

Page 124: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Desplazamiento

En este método los dígitos exteriores (en ambos extremos) de la clave se recorren hacia la derecha de tal forma que quedan trasladados en la medida de la longitud de la dirección, la suma de estos números es la clave deseada. Ejemplo: 1 2 7 2 0 7 3 5 9 | | ----->1720 | 7359 <----- ---- 9079 Conversión de raíz

Se escoge un número arbitrario (un primo) llamado raíz el cual servirá como nueva base, y en la serie de dígitos resultantes se suprimen los dígitos excedentes de mayor orden. Ejemplo: Adoptando la raíz 11, la clave 172148 se transforma en 1 x 11 5 + 7 x 11 + 2 x 11 + 1 x 11 + 4 x 111 + 8 = 266373 4 3 2

y suprimiendo el 26 la clave restante es 6373. División polinómica

Cada dígito de la clave es coeficiente de un polinomio, así la clave 172148 se contempla como: X 5 +7X 4 +2X 3 +X 2 +4X+8

El polinomio así obtenido se divide por otro polinomio (fijo). Los coeficientes del resto forman la dirección deseada.

Debe tomarse en cuenta que la transformación ideal no es la que distribuye el conjunto de claves aleatoriamente, sino tan uniformemente como sea posible en el espacio de direcciones. Para el tratamiento de los desbordes existen las siguientes alternativas:

a) La clave que provoca la colisión se almacena en un área de datos aparte. b) La clave de desborde se almacena en el área prima.

124

Page 125: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Para el primer caso (ver figura) tenemos una tabla de desbordes la cual se puede manejar bajo cualquiera de los TAD’s conocidos: listas ligadas, almacenamiento secuencial, mediante otra función hash (rehash), un árbol, etc. AREA PRIMA AREA DE DESBORDE h(a) h(b)

colisión

Para el segundo caso (siguiente figura) se busca en la misma tabla espacio para almacenar la clave. Esta se puede realizar con búsqueda secuencial, por bloques, por referencias ligadas etc.

h(a) h(b)

colisión

Es deseable que la tabla se declare llena cuando se tiene ocupado el 97% de la tabla (factor de carga). De modo que cuando ocurra una colisión no se dificulte dar acomodo a la clave que lo ocasionó. 5.2 TAD Tabla de Símbolos (TS)

Durante las etapas de compilación se requieren varias operaciones relacionadas con los identificadores, sean de variables, procedimientos, u otros. Por ejemplo los siguientes conceptos tienen relación con el tratamiento de los identificadores:

1. Referencias. Cuando se va a operar con alguna unidad léxica "nombre" a nivel semántico es necesario indicar con precisión cuál de las unidades léxicas en particular se considera. Así la etapa sintáctica no sólo conlleva al valor léxico sino además un número de identificación propio.

2. Alcance. La referencia a una variable, durante la traducción dirigida por

sintaxis y en la optimización, requiere del bloque donde se hizo la definición.

125

Page 126: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

3. Tipos. Es un atributo de un identificador. Una de sus utilidades es elegir la operación correcta, en la generación de código o bien para reservar áreas de memoria correspondientes a los valores que tomarán las variables, en la optimización.

En general es necesario manejar la información acerca de los identificadores

utilizados en un programa.

Podemos observar que cada identificador tiene asociados atributos, por tanto la estructura para manejar esta información debe ser una tabla asociativa, llamada Tabla de Símbolos : atributos identificador Las operaciones básicas en esta tabla son:

a) Determinar si un identificador está en la tabla. b) Añadir un nuevo identificador a la tabla. c) Obtener información asociada a cierto identificador. d) Añadir información a un nombre determinado. e) Eliminar un identificador o un grupo de identificadores.

Existen muchas posibilidades para representar este tipo de TAD. En todos los casos

la mejor selección se hace de acuerdo a las características del lenguaje de programación particular que se desea implantar. Es común emplear alguno de los TAD’s siguientes en una TS:

1) Arreglo secuencial de registros (nombre - atributo). 2) Listas autoorganizadas (lista ligada que redefine su cabeza con el último

elemento consultado). 3) Arbol binario balanceado. 4) Hash.

Uno de los ejemplos más completos del TAD Tabla de Símbolos es la estructura de

datos para la tabla de ALGOL. Analizando esta tabla concretaremos las ideas anteriores.

De acuerdo a la regla "la referencia a un identificador corresponde a la definición dentro del nivel más cercano de anidación", deberán considerarse dos tipos de identificadores durante la compilación. Aquellos que están en algún bloque actualmente activo y los que han dejado información para las subsecuentes etapas (bloques anteriores). Es por esto que en la tabla se forman dos secciones: nombres activos e inactivos. La TS de ALGOL utiliza los siguientes TAD’s:

- Tabla Hash para nombres - Arreglos separados (nombre - información)

126

Page 127: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

- Tabla de bloques activos e inactivos. - Tabla de cadenas.

El funcionamiento de estos TAD’s es como sigue: para cada nombre que se va a

analizar se obtiene el valor hash, las cadenas de equivalencia se encuentran en el arreglo de nombres almacenados de forma que el último se encuentre al inicio (los primeros al final de la lista), para ello se verifica el nombre en la tabla de cadenas. El nombre puede aparecer en alguno de los bloques activos, finalmente si se encuentra el nombre podemos consultar su información en el arreglo separado.

Para clarificar el funcionamiento de la TS. Supóngase que en la siguiente estructura de bloques de un programa el análisis se encuentra en la posición indicada: BEGIN \ BEGIN | | BEGIN \ B3 > B2 | END / | | END > B1 BEGIN \ | <---- > B4 | END / | END / En estas condiciones se tendría una tabla de símbolos como la siguiente: arreglo separado información arreglo de nombres tabla de bloques hash utilb util endb end B4tc utiltc

Inf. de

nom

B1 B4 apunts. de nom ...

B1 B4 B2 B3

nom

Tabla de Cadenas

127

Page 128: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Las operaciones que se realizan en este TAD no son exclusivamente el ingreso y consulta de nombres, además cada vez que se inicia o termina un bloque (begin, end) deben desalojarse áreas de cada parte del TAD. El arreglo de nombres funciona como "acordeón" para mantener las referencias a los nombres activos e inactivos, al igual que la tabla de bloques. Por su parte la tabla de cadenas es desalojada según el apuntador de inicio de un bloque, cada vez que se finaliza (end). Ejercicio Escriba los algoritmos para realizar los cambios en la TS de ALGOL cuando: a) Llega un nuevo nombre b) Empieza un bloque c) Termina un bloque d) Hay una referencia a un nombre 5.3 Recolección de basura

Supóngase que se desea construir un manejador de listas de propósito general, cuyos elementos pueden ser solicitados por otros programas (espacio libre). Existen dos métodos para tal fin: un contador de referencias y un colector de basura.

El primer esquema necesita un campo, por cada nodo, el cual contiene el número de apuntadores que referencían ese nodo, y cuando la cuenta es cero, el nodo está disponible.

La técnica de recolección de basura, requiere un campo, de un bit, por cada nodo. La idea básica del algoritmo es dejar que el programa corra y utilice los nodos de memoria sin liberarlos, hasta que no existan más nodos disponibles, entonces se efectúa un "reciclaje" de los nodos, recuperando, aquellos que se encuentran disponibles (bit de marca), una vez terminada la recolección el programa continua normalmente.

Los dos métodos anteriores tienen desventajas. El contador de referencias no permite liberar todos los nodos que están disponibles, ya que para el caso de listas lineales los nodos se liberan, pero no en el caso de listas recursivas (su contador nunca es cero pues apuntan a ellas mismas).

La recolección de basura cuando casi todo el espacio de memoria está en uso se torna cada vez más lenta, el reclamo de más memoria por parte del programa es atendido más lentamente (en cada nueva solicitud es más difícil encontrar nodos disponibles, verificando el bit de marca).

Una solución parcial al problema es dejar que el programador defina un mínimo n de nodos para la ejecución de su programa y si después de una recolección de basura no se cubre este n el programa es interrumpido.

128

Page 129: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

También se observa que en sistemas de tiempo real, la recolección de basura , presenta la gran desventaja de que el recolector entra en acción repetidas veces con el consiguiente consumo de tiempo.

Los algoritmos de recolección de basura son aprovechables para el caso en que se desea marcar todos los nodos referidos directa o indirectamente por otro (llamados entre rutinas). La recolección de basura tiene dos fases:

a) Se procede a marcar todos los nodos que no están disponibles empezando por los que tiene asignados el programa.

b) Recolecciona los nodos restantes (disponibles) en una lista de espacio libre (pool).

A continuación se plantea un algoritmo de recolección de basura. El algoritmo no

es único y para algunos casos concretos o algún tipo de máquina puede mejorarse. Se asume que los nodos tienen los siguientes campos:

MARCA (1 bit) ATOMO (1 bit) LIGA1 (apuntador) LIGA2 (apuntador)

Cuando ATOMO=0, LIGA1 y LIGA2 son nulos (igual a NIL) o contienen un apuntador a otro nodo de igual estructura.

Si ATOMO=1 el contenido de LIGA1 y LIGA2 no tiene importancia para el algoritmo.

Dado un apuntador P0 se inicializa MARCA=1 en NODO(P0) y en todos los nodos que se puedan accesar desde NODO(P0) a través de LIGA1 y LIGA2 y que tienen ATOMO=MARCA=0. Este algoritmo usa tres apuntadores P,Q y T. 1) T=NIL 2) P=P0 3) MARCA(P)=1 4) Si ATOMO(P) = 1, (9) 5) Q=LIGA1(P) 6) Si Q # NIL Y MARCA(Q) = 0, ATOMO(P)=1,LIGA1(P)=T, T=P,P=Q, (3) 7) Q=LIGA2(P) 8) Si Q # NIL Y MARCA(Q) = 0, LIGA2(P)=T, T=P,P=Q, (3) 9) Si T = NIL, FIN 10) Si ATOMO(Q) = 1 , ATOMO(Q)=0, T=LIGA1(Q), LIGA1(Q)=P P=Q, (7) 11) T=LIGA2(Q) 12) LIGA2(Q)=P

129

Page 130: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

13) P=Q 14) (7) Ejercicio Escriba el algoritmo para el contador de referencias. 5.4 Administración de memoria

Los programas de servicio cambian la fisonomía de la computadora de tal manera que facilitan el uso de la máquina. En particular, la imagen conceptual que hay sobre la memoria es tan sencilla que al usuario le permite centrarse en otros aspectos, sin embargo, tan sólo un asignamiento que hiciere el usuario de un lenguaje de programación obliga al análisis de opciones inimaginadas.

En esta sección presentaremos los algoritmos que introducen al estudio de la administración de memoria. Tomamos como base dos programas de servicio representativos, un sistema operativo y las rutinas de biblioteca de un compilador de un lenguaje de programación. Estos permiten al usuario efectuar dos operaciones fundamentales: - petición (allocate) - devolución (dispose)

Las peticiones de memoria pueden llevarse a cabo de manera regular o completamente aleatoria.

En el primer caso hay reglas inherentes a las operaciones de memoria. Por ejemplo, "la memoria ocupada por las variables locales de un procedimiento no puede desaparecer mientras no termine el procedimiento". Cada vez que se invoca un procedimiento sabemos que se almacena en una pila la dirección "de regreso" y se transfieren parámetros. Además, deben activarse las variables locales del procedimiento. Tales variables se definen contiguas a los parámetros, es decir en una zona específica de la pila. A esta zona de memoria (dinámica) se le conoce como registro de activación, y cada llamado a un procedimiento implica generar en la pila el registro de activación correspondiente. Así mismo cada regreso de procedimiento conlleva al desalojo del registro asociado. Existe por tanto un orden sobre las peticiones y devoluciones de memoria y su administración se puede realizar con una estructura de pila.

Cuando las peticiones son irregulares (por ejemplo "new" en PASCAL), tenemos que enfocar el problema de manera distinta. El problema es equivalente a tomar bloques de longitud variable de una zona de memoria disponible. Existen varios criterios para elegir estos bloques. Aquí sólo veremos el criterio que considera la "recolocación de la memoria". Dicho criterio se divide en dos algoritmos: - elección del bloque por orden (first fit) - elección del bloque por tamaño (best fit)

130

Page 131: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

Consideremos antes un caso especial más sencillo: los bloques de memoria se consideran fijos asignados por el operador al iniciar el trabajo con el sistema. Cada vez que se hace una petición de memoria, una tabla que indica el tamaño y origen de cada bloque disponible puede recorrerse para atender la petición. Como podrá observarse, en este caso hay pocas posibilidades de una atención eficiente. Por ejemplo la disposición fija de bloques puede desperdiciar memoria. Si a lo anterior le agregamos que se subutilicen bloques grandes, cuando una petición de un bloque grande se haga, si acaso puede atenderse (haciendo movimientos entre los diferentes bloques ocupados) esta acción disminuiría el rendimiento del sistema. 120 k 60 k partición de memoria 100 k en 4 bloques de diferente tamaño 48 k

/////////// ocupado / (48) ////////// ocupado / (60) //////////////////////////////// //////////////////////////////

Lo anterior es una implantación rudimentaria de la atención de peticiones de memoria. Es claro que por el dinamismo del proceso pensamos inmediatamente en una estructura de datos con almacenamiento encadenado. En efecto, cambiar lo anterior a esta representación conduce a la siguiente caracterización:

1. Los bloques se eligen del tamaño requerido, "separando" del área libre la memoria pedida.

2. La memoria restante se mantiene organizada en una lista encadenada con "nodos" de longitud variable.

3. Las devoluciones de memoria se incorporan a la lista de memoria libre. Lo cual nos ofrece mayor libertad y además eficiencia.

Un algoritmo que concreta los puntos anteriores deber considerar nodos con un encabezado que indique su tamaño y el apuntador al siguiente nodo. Tendríamos entonces un nodo donde su primera localidad tiene el apuntador al siguiente nodo y en la segunda localidad su tamaño. El algoritmo entonces queda:

pide(n: integer; VAR p: pointer); BEGIN p:= lista[L]; q:= L; bus:= true; (*inicio*) WHILE (p <> nil) and bus DO BEGIN IF lista[p+1] >= n THEN BEGIN (*encuentra bloque*)

131

Page 132: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

lista[p+1]:= lista[p+1]-n; (*calcula resto*) IF lista[p+1]=0 THEN lista[q]:= lista[p](*bloque exacto*) ELSE p:= p+lista[p+1] (*incorpora resto*) END; bus:= false (*lo encontró*) END; IF bus THEN BEGIN q:=p; p:= lista[p] END (*pasa al sig*) END END {pide};

Analizando las operaciones de memoria observamos lo que se conoce como fragmentación; pequeños bloques libres que no pueden ser reutilizados. Una posible solución es la compactación: todos aquellos bloques libres que están contiguos pueden constituirse en uno sólo, de esta manera disminuye la cantidad de bloques (se acelera la búsqueda) y a la vez pueden satisfacer demandas más amplias. La compactación sólo es un paliativo de la fragmentación, no la elimina y es común incorporarla a la operación de devolución. Para lograr este planteamiento debe modificarse ligeramente el encabezado de los nodos. Incluimos cuatro campos más: dos marcas, apuntador al nodo anterior y apuntador al inicio del bloque: p un nodo que representa un bloque de memoria

izq der tam marca

...

marca inicio

La distribución de estos campos en la forma indicada en la figura (marca superior, inferior e inicio) no es casual. La marca indica si el bloque está ocupado de tal forma que un bloque que es devuelto tiene acceso, a través de su apuntador y tamaño, a los bloques adyacentes y conocer mediante sus marcas si está libre. El apuntador "inicio" que aparece al final ayuda a compactar el nodo con otro que es devuelto. En términos de un algoritmo tendríamos: petición(n: integer; VAR p: pointer); BEGIN p:=lista[L]; REPEAT IF lista[p+1] >= n THEN BEGIN (*lo encuentra*) delta:= lista[p+1]-n; IF delta < epsilon THEN BEGIN (*evita fragmentar*)

132

Page 133: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

lista[lista[p-1]]:= lista[p]; (*actualiza*) lista[lista[p]-1]:= lista[p-1]; lista[p+2]:= lista[p+2+lsita[p+1]-1]:= 1; L:= lista[p-1] END ELSE BEGIN lista[p+1]:= delta; lista[p+delta-1]:= p; (*inicio*) lista[p+delta]:= 0; L:= p; (*fin de búsqueda*) p:= p+delta; lista[p+1]:= n; lista[p+2]:= lista[p+2+n-1]:= 1 END; p:= lista[p] (*siguiente*) END UNTIL p:=lista[L] END {petición}; Por otra parte, abreviando la notación del algoritmo de devolución tenemos: devolución(p: pointer); BEGIN n:= lista[p+1]; CASE OF :lista[p-3]=1 AND lista[p+n+2]=1: (*adyacentes en uso*) lista[p+2]:= lista[p+n-2]:= 0 (*marcas libre*) lista[p+n-3]:= p (*inicio*) lista[p-1]:= L; (*inserta*) lista[p]:= lista[L]; lista[lista[p]-1]:= p; lista[L]:= p :lista[p+n+2]=1 AND lista[p-3]=0: (*libre izquierda*) q:= lista[p-2]; (*inicio izquierda*) lista[q+1]:=lista[q+1]+n (*aumenta tama$o*) lista[p+n-3]:= q; (*inicio*) lista[p+n-2]:= 0 (*marca final*) :lista[p+n+2]=0 AND lista[p-3]=1: (*libre derecho*) lista[lista[p+n-1]]:= p; (*inserta*) lista[lista[p+n]-1]:= p; lista[p-1]:= lista[p+n-1]; (*actualiza encabez*) lista[p]:= lista[p+n]; lista[p+1]:= n+lista[p+n+1]; lista[p+lista[p+1]]:= p lista[p+2]:= 0; IF L=(p+n) THEN L:=p

133

Page 134: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

:ELSE: (*ambos libres*) lista[lista[p+n-1]]:= lista[p+n]; lista[lista[p+n]-1]:= lista[p+n-1]; q:= lista[p-2]; lista[q+1]:= lista[q+1]+n+lista[p+n+1] lista[q+lista[q+1]]:= q; IF L=(p+n) THEN L:= lista[p+n-1] END {devolución};

Las anteriores operaciones representan el criterio de elegir el bloque pedido según el orden en que aparecen en la lista de libres. Sin embargo esto puede ser "mejorado", en el sentido de obtener el más pequeño que satisfaga la demanda. A este último criterio se le conoce como "best-fit" y constituye una variación de los anteriores Ejercicio Escribir el algoritmo "best-fit".

134

Page 135: Notas Del Curso Alg. E.D

Algoritmos y Estructuras de Datos

135

Bibliografía

1. Aho, Alfred V., Hopcroft, John E., Ullaman, Jeffrey D. Estructuras de Datos y Algoritmos. Ed. Addison -Wesley Iberoamericana, 1988.

2. Allen, Weiss Mark, Estructuras de Datos en JAVA, Addison Wesley 1998. 3. Allen, Weiss Mark, Estructuras de datos y algoritmos, Ed. Addison -Wesley

Iberoamericana, 1998 4. Kruse L., Estructuras de Datos y Diseño de Programas, Prentice Hall, 1996 5. Cormen H. Thomas., Leiserson E. Charles, Rivest L. Ronald, Introduction to

Algorithms, MIT Press, 1990 6. Wirth, N., Algorithms + Data Structures = Programs, Prentice Hall, 1976 7. Jean-Paul Tremblay, Paul G., An Introductions to Data Structures with

Applications, Mc Graw Hill International Ed. 8. Wirth N., Algoritmos y Estructuras de Datos, Prentice Hall Hispanoamericana

México, 1987. 9. G. Brassard, T. Bratley, Fundamentos de Algoritmia, Prentice Hall, 1997 10. Deitel y Deitel, Como programar en JAVA, Prentice Hall, 2000. 11. Herbert Shildt, Java, Manual de referencia, Mc Graw Hill, 1997 12. Sisa, Alberto J. Estructuras de Datos y Algoritmos (con énfasis en POO). Ed.

Prentice Hall.

Sitios Web Visitados

http://delfosis.uam.mx/~sgb/grafos/default.html