Documentos de Académico
Documentos de Profesional
Documentos de Cultura
para Ingeniería de
Sistemas y Computación
1
Análisis de Algoritmos
para Ingeniería de
Sistemas y Computación
Derechos reservados
Diciembre de 2011
ISBN: 978-958-99930-2-6
ELIZCOM S.A.S
www.elizcom.com
ventas@elizcom.com
NIT 90004331-7
Armenia, Quindío – Colombia
Tel. 7493244
Cel: 57+3113349748
Java™ y todas las marcas y logos basados en Java™ son marcas comerciales o marcas registradas de ORACLE Sun,
La marca de Eclipse y sus logotipos aparecen de acuerdo con las condiciones legales de Eclipse, expuestas en
http://www.eclipse.org/legal/main.html
Microsoft Office y su logotipo es una marca registrada por Microsoft Corporation.
CMAP – Tools son marca registrada por ihmc - Florida institute for Human & Machine Cognition
2
ANÁLISIS DE ALGORITMOS PARA INGENIERÍA DE SISTEMAS Y
COMPUTACIÓN
3
2.2 Algoritmos Divide y Vencerás ................................................................... 107
2.2.1 Búsqueda Binaria...................................................................................... 107
2.2.2 Ordenamiento por el método MergeSort .................................................. 108
2.2.3 Multiplicación de Números Grandes ........................................................ 110
2.3 Algoritmos Devoradores ............................................................................ 114
2.3.1 El Problema de la mochila ........................................................................ 115
2.3.2 Elementos de los Algoritmos Voraces...................................................... 120
2.3.3 El problema de la Devuelta. ..................................................................... 121
2.4 Actividad Independiente: El problema de la Devuelta ........................... 123
2.5 Programación Dinámica ............................................................................ 126
2.5.1 Serie de Fibonacci .................................................................................... 126
2.5.2 Problema de la Mochila ............................................................................ 130
2.5.3 Problema de la Devuelta ........................................................................... 132
2.5.4 Algoritmo de Dijkstra ............................................................................... 134
2.5.5 Hoja de trabajo: Lotería ............................................................................ 135
3 ALGORITMOS APLICADOS A GRAFOS Y ARBOLES ................................. 141
3.1 Introducción ................................................................................................ 141
3.2 Arboles Binarios ......................................................................................... 141
3.2.1 Árboles Binarios de Expresión ................................................................. 143
3.2.2 Implementación de Arboles Binarios ....................................................... 146
3.2.3 Recorridos en Arboles Binarios ................................................................ 153
3.3 Caso de estudio: Agenda Telefónica ......................................................... 155
3.4 Hoja de trabajo: Graficador de árboles ................................................... 158
3.5 Grafos .......................................................................................................... 166
3.5.1 Conceptos fundamentales de los Grafos ................................................... 166
3.5.2 Grafos Dirigidos ....................................................................................... 166
3.5.3 Grafos No Dirigidos ................................................................................. 167
3.6 Arboles de Recubrimiento Mínimo ........................................................... 168
3.6.1 Algoritmo de Prim .................................................................................... 168
3.6.2 Algoritmo de Kruskal ............................................................................... 169
3.6.3 Algoritmo de Dijkstra ............................................................................... 170
3.7 Arboles n-arios ............................................................................................ 174
3.8 Actividad Independiente: Organigrama de empresa .............................. 177
3.9 Arboles AVL ............................................................................................... 178
3.9.1 Elementos de los Árboles AVL ................................................................ 178
3.9.2 Operaciones de los Árboles AVL ............................................................. 179
3.10 Caso de estudio Grupo de estudiantes ...................................................... 184
3.11 Backtracking ............................................................................................... 193
3.12 Caso de estudio: Reinas.............................................................................. 194
3.13 Actividad Independiente: Laberinto......................................................... 197
4 ANÁLISIS DE ESTRUCTURAS DE DATOS ................................................... 201
4.1 Introducción ................................................................................................ 201
4.2 Estructura de Datos Lista .......................................................................... 201
4.2.1 Lista Sencillamente Enlazada – Implementación 1. ................................. 201
4.2.2 Lista Sencillamente Enlazada – Implementación 2. ................................. 204
4.2.3 Lista Sencilla Circular .............................................................................. 209
4.2.4 Lista Doblemente Enlazada ...................................................................... 213
4
4.2.5 Lista Circular Doblemente Enlazada ........................................................ 217
4.3 Actividad Independiente: Ciudades .......................................................... 221
4.4 Estructura de Datos Pila ............................................................................ 224
4.4.1 Implementación Basada en un arreglo ..................................................... 224
4.4.2 Implementación utilizando Listas ............................................................. 227
4.5 Estructura de Datos Cola ........................................................................... 228
4.5.1 Implementación Basada en un arreglo ..................................................... 229
4.5.2 Implementación utilizando Listas ............................................................. 232
4.6 Estructura ArrayList ................................................................................. 233
4.7 Caso de estudio – Universidad................................................................... 234
5 OPTIMIZACIÓN, PRUEBAS Y LÍMITES DE LA LÓGICA ........................... 241
5.1 Introducción ................................................................................................ 241
5.2 Técnicas de Optimización .......................................................................... 241
5.2.1 Desenvolvimiento de ciclos ...................................................................... 241
5.2.2 Reducción de Esfuerzo ............................................................................. 243
5.2.3 Tipos de Variables .................................................................................... 244
5.2.4 Fusión de ciclos ........................................................................................ 245
5.2.5 Expresiones redundantes .......................................................................... 246
5.2.6 Folding ...................................................................................................... 247
5.3 El diseño por contrato ................................................................................ 249
5.4 Pruebas con JUnit ....................................................................................... 252
5.5 Límites de la Lógica .................................................................................... 256
5.5.1 Clase P ...................................................................................................... 256
5.5.2 Clase NP ................................................................................................... 256
6 BIBLIOGRAFÍA ................................................................................................. 257
5
PRESENTACIÓN
El Análisis de Algoritmos se considera como una temática fundamental dentro del proceso de
formación de los estudiantes de Ingeniería de Sistemas y Computación, pues posee una
estrecha relación con otras áreas de formación del ingeniero, como lo son la programación de
computadores, las estructuras de datos, las Matemáticas Discretas y la teoría de grafos entre
otras.
Este libro pretende ser flexible en la forma como puede impartirse a las personas interesadas.
La comprensión de los temas, depende fundamentalmente de la preparación de los
estudiantes. Se presentan conceptos básicos fundamentales e intermedios, los cuales se
pueden aplicar en la práctica, así como también realizar un análisis riguroso de los conceptos
teóricos que se imparten. El texto está orientado al estudiante, y hemos puesto el mayor
empeño para explicar cada tema tan claramente como sea posible.
6
INTRODUCCIÓN
El objetivo de este libro de texto consiste en mostrar los elementos fundamentales del análisis
de algoritmos, estrategias de programación, el análisis de algoritmos aplicados a estructuras de
datos, análisis de grafos y técnicas de optimización de código, los cuales son temas que tienen
una serie de aplicaciones en diferentes áreas de las ciencias computacionales. Se pretende
establecer una relación de aplicación entre los conceptos teóricos y su aplicación específica en
el campo de la Ingeniería de Sistemas y Computación.
Presentar casos de estudio y hojas de trabajo. En este aspecto, todos los ejemplos se
encuentran desarrollados en java, dado que es un lenguaje de programación de
aceptación mundial tanto en ámbito industrial como en el ámbito académico.
Es de suponer que los lectores de este libro tienen los conocimientos básicos de algoritmia y
está familiarizado con algún lenguaje de programación. Los ejemplos serán implementados en
su totalidad en el lenguaje de programación java y se dará importancia únicamente a los
aspectos más esenciales, sin sobrecargar al lector en temas que pueden ser objeto de estudio
en otros libros relacionados con la programación y la algoritmia.
El libro está estructurado en 5 capítulos los cuales pretenden de forma progresiva mostrar
diferentes temas que son abordados de forma simple y estructurada, a continuación, se
muestra un breve resumen de los temas que se presentan en el mismo.
7
problema de la devuelta; en los cuales es posible el uso de algoritmos divide y vencerás,
algoritmos devoradores o ávidos y algoritmos que aplican programación dinámica.
8
1 ANÁLISIS DE ALGORITMOS
1.1 Introducción
Es por ello que muchos de los productos de software deben ser construidos por personas que
tengan diversos tipos de habilidades al momento de construir software. En ese sentido la
actividad de la programación está relacionada directamente con la tarea de diseñar e
implementar algoritmos que resuelvan problemas con eficiencia.
El análisis de algoritmos, tiene como objetivo fundamental medir la eficiencia de uno o más
algoritmos en cuanto a consumo de memoria y tiempo. Es una actividad muy importante en el
proceso de desarrollo de software, especialmente en entornos con recursos restringidos. Por
ello, es necesario realizar estimaciones en cuanto al consumo de tiempo y de memoria que
puede requerir una aplicación para su ejecución.
En este capítulo se trabajarán temas como el tiempo de ejecución de los algoritmos iterativos,
la complejidad computacional y notaciones asintóticas, el análisis de algoritmos recursivos,
métodos de ordenamiento y búsqueda de datos.
Se conoce como algoritmo a una secuencia de instrucciones, que son ejecutadas con
esfuerzos finitos en un tiempo razonable, que recibe un conjunto de valores como entrada y
produce un conjunto de valores como salida. Para la ejecución de estas instrucciones es
necesario contar con una cantidad finita de recursos.
Según (Valenzuela, 2003), cuando nos referimos al concepto de algoritmo, hacemos referencia
a los pasos encaminados a la consecución de un objetivo. Un algoritmo puede ser
caracterizado por una función lo cual asocia una salida: s= f (E) a cada entrada E.
Se dice entonces que un algoritmo calcula una función f. Entonces la entrada es una variable
independiente básica en relación a la que se producen las salidas del algoritmo, y también los
análisis de tiempo y espacio (Valenzuela, 2003).
En las Ciencias de la Computación cuando se dice que un problema tiene solución, significa
que existe un algoritmo susceptible de implantarse en una computadora, capaz de producir la
respuesta correcta para cualquier instancia del problema en cuestión. De acuerdo a ello, la
construcción de un programa hace referencia directa a la implementación de uno o más
algoritmos.
9
Existen diferentes tipos de algoritmos, entre los cuales tenemos:
En la literatura asociada al tema se encuentra una cantidad muy amplia de tipos de algoritmos
entre los cuales adicionalmente se pueden citar los algoritmos paralelos, probabilísticos,
voraces, divide y vencerás, dinámicos. Estos tres últimos se tratarán en capítulos posteriores
del libro.
Correcta: Si para toda instancia del conjunto de entrada se obtiene la salida esperada,
es decir, que cumpla con el objetivo para el cual está pensado.
Eficiente: Debe ser rápido y usar la menor cantidad de recursos. Es una relación entre
los recursos consumidos, fundamentalmente tiempo y memoria versus los productos
obtenidos.
Si los anteriores elementos resueltos de forma positiva dan noción de lo que es un buen
algoritmo. Por lo tanto se pueden generar las siguientes frases que se convertirán en preguntas
cuando estemos analizando cada uno de nuestros programas.
Para aproximar mejor los elementos anteriores, observemos el siguiente ejemplo: se desea
determinar si un número entero positivo predeterminado es primo o no lo es. El conjunto de los
números primos es un subconjunto de los números naturales que contiene aquellos que son
divisibles por sí mismos y por la unidad.
Son entre otros números primos: 1, 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43. A
continuación se muestra una implementación del método que resuelve el problema en
mención.
10
public boolean esPrimo(int numero)
{
int resultado = 0;
int i = 2;
while( i < numero ){
if ( numero % i == 0 )
{
resultado = 1;
}
Implementación
i = i + 1;
del Método
}
if ( resultado == 0 )
{
return true;
}
else
{
return false;
}
}
¿Cumple el algoritmo el objetivo para el cual está pensado? Solo en el caso en cual se
ingresa un número entero positivo, el algoritmo genera una salida correcta y cumpliría
con su objetivo.
¿El algoritmo hace uso adecuado de los recursos? Si, dado que se usan las variables
estrictamente necesarias para la solución y se utiliza un tipo de dato con rango de
valores moderado.
¿Permite el algoritmo identificar posibles errores? No, dado que el programa no maneja
excepciones que controlen posibles errores.
¿El algoritmo es fácil de modificar para añadir funcionalidad? Si, dada la sencillez del
problema y de la solución propuesta.
11
else
{
return false;
}
}
¿Cumple el algoritmo el objetivo para el cual está pensado? Solo, en el caso en el que
se ingrese un número entero positivo, el algoritmo genera una salida correcta.
¿Hace el algoritmo uso adecuado de los recursos? Si, pues para algunos casos no
itera toda la cantidad de veces permitida por el ciclo.
¿El algoritmo permite identificar posibles errores? No, dado que el programa no maneja
excepciones que controlen posibles errores.
¿El algoritmo es fácil de modificar para añadir funcionalidad? Si, dada la sencillez del
problema y de la solución propuesta.
ACTIVIDAD
Pregunta Respuesta
¿Cumple el algoritmo el objetivo
para el cual está pensado?
12
¿Resuelve el algoritmo el problema
en el menor tiempo posible?
¿Hace el algoritmo uso adecuado
de los recursos?
¿El algoritmo permite identificar
posibles errores?
¿El algoritmo es fácil de modificar
para añadir funcionalidad?
Pregunta Respuesta
¿Cumple el algoritmo el objetivo
para el cual está pensado?
¿Resuelve el algoritmo el problema
en el menor tiempo posible?
¿Hace el algoritmo uso adecuado
de los recursos?
¿El algoritmo permite identificar
posibles errores?
¿El algoritmo es fácil de modificar
para añadir funcionalidad?
ACTIVIDAD
Entre las varias sucesiones interesantes de números que existen en las matemáticas discretas
y combinatorias, están los números armónicos, los cuales tienen la forma:
13
H1 + H2 + H3 +… donde:
( )
Implementación
del Método
Pregunta Respuesta
¿Cumple el algoritmo el objetivo
para el cual está pensado?
¿Resuelve el algoritmo el problema
en el menor tiempo posible?
¿El algoritmo permite identificar
posibles errores?
Realice una comparación entre el método que usted implementó, con el siguiente método y
posteriormente responda la pregunta:
Pregunta Respuesta
¿Cuál de los dos algoritmos es
más eficiente?
14
1.3 Características de los Algoritmos
Preciso: Un algoritmo preciso, posee un orden específico en cada uno de los pasos
que este ejecuta. Por esta razón cuando se ejecuta un paso del algoritmo, se conoce
con certeza cuál es el paso siguiente a ejecutar.
Finito: El algoritmo debe finalizar tras una secuencia o número finito de pasos.
Efectivo: La eficiencia hace alusión al logro de la solución a un problema de la mejor
manera posible (en términos de tiempo). En el ambiente computacional, lo son el buen
uso de los recursos de hardware.
Determine si la siguiente implementación del método, cumple con las características de los
algoritmos definidas anteriormente.
Pregunta Respuesta
Es preciso el método?
Es finito el método?
Es efectivo el método?
Para finalizar esta sección cabe mencionar que dentro de la Ingeniería de software, también se
contemplan factores fundamentales en la calidad del software, entre los cuales tenemos:
15
Compatibilidad: Facilidad para que un programa pueda usarse en unión con otros
programas.
Extensibilidad: Es la capacidad que tiene un programa de ampliar la funcionalidad de
acuerdo a nuevas necesidades o requerimientos.
Verificabilidad: Es la capacidad que tiene un programa para soportar casos de prueba
y para identificar posibles errores.
Exactitud: Es el atributo que determina el nivel de precisión de los resultados
obtenidos por un programa.
16
los nombres de variables, de las clases y de los métodos, todos estos deben estar
acordes a las tareas que realizan.
Para conocer las convenciones de código definidas por el proveedor oficial de java se
recomienda visitar el site (octubre de 2011), el cual contiene los elementos bases para
el uso de estándares: http://www.oracle.com/technetwork/java/codeconv-138413.html
Dentro del entorno de la informática no siempre los problemas – necesidades de las personas
o las organizaciones, se encuentran claramente definidas, es necesario en muchas ocasiones
realizar una clara formulación del problema. Solo partiendo de una correcta formulación de
este, será posible especificar una metodología para su solución.
Según (Cardona, Jaramillo, & Carmona, Análisis de Algoritmos en Java, 2007), una buena
planificación de las tareas a realizar para desarrollar un programa favorece el éxito en la
implementación. La planificación debe estar basada en el establecimiento de fases. Las fases,
tanto para realizar programas sencillos como para llevar a cabo proyectos de envergadura de
construcción de aplicaciones informáticas se pueden ver en la figura.
Una vez definido el problema, se realiza la especificación del problema, por medio de
los que se conoce como diseño detallado o algorítmico, en donde se definen y
documentan los detalles y algoritmos de cada una de las funciones
Con base en el diseño, se construyen los algoritmos necesarios para la solución del
problema, para posteriormente estos se implementados en un lenguaje de
programación. Cada algoritmo implementado, debe ser verificado para comprobar las
operaciones para las cuales fue construido, estos se deben someter a un conjunto de
pruebas.
Finalmente se tiene una aplicación de software, que debe dar la solución esperada de
acuerdo a los requerimientos planteados inicialmente.
17
1.6 Tiempo de Ejecución de los Algoritmos
Para calcular el tiempo de ejecución, se deben apropiar dos conceptos que fundamentan el
análisis de algoritmos, estos conceptos algorítmicos se conocen como el mejor y el peor caso.
Para este libro, tendremos en cuenta el análisis para el peor de los casos.
Más formalmente las siguientes técnicas se utilizan para estimar el tiempo de ejecución de un
programa (Aho & Ullman, 1995):
18
(Valenzuela, 2003). De acuerdo a lo anterior, un programa pasa la mayor parte del
tiempo de la ejecución en un trozo de código pequeño. Este 90 % del código suele
estar constituido por ciclos.
La técnica de análisis considera aspectos más rigurosos para la estimación del tiempo
de ejecución. Su objetivo es analizar de forma matemática y detallada cada uno de los
pasos que ejecuta el algoritmo. Consiste en asignar a cada una de las instrucciones un
costo en consumo de tiempo, sumar cada uno de estos y establecer la función de
consumo de tiempo. Esta técnica utiliza una función T(n), la cual representa el número
de unidades de tiempo que consume el algoritmo para cualquier entrada de tamaño n.
Por lo anterior, T(n) es el tiempo de ejecución del algoritmo. El tiempo de ejecución en
la función T(n) no se le está asignando ninguna unidad de medida del tiempo, lo que se
está haciendo es calculando el monto de instrucciones que el algoritmo ejecutará, dado
el tamaño del conjunto de datos de entrada
Después de realizar un estudio sobre las diversas técnicas para el análisis de algoritmos, se
utilizará la técnica de análisis. Esta técnica permitirá que la estimación, se realizará en función
del número de operaciones elementales que realiza dicho algoritmo para un tamaño de entrada
dado.
19
Objetivo: Comprender la estimación del tiempo de ejecución para
Ejemplo
métodos que tienen operaciones elementales.
ACTIVIDAD
T(n)
Muchos de los problemas que se resuelven a nivel de programación, están relacionados con el
uso de ciclos. A continuación, se muestran una serie de ejemplos que permitirán establecer el
tiempo de ejecución para métodos que poseen ciclos en su implementación. Para cada uno de
los métodos se considerará el peor caso.
20
Objetivo: Crear habilidad para calcular el tiempo de ejecución de
Ejemplo
un método cuando este tiene ciclos.
A continuación se muestra otro ejemplo en el cual se estima el tiempo de ejecución del método.
El método recibe tres parámetros por valor, lo que implica que cada uno de esos valores se
ejecuta una vez.
21
Objetivo: Crear habilidad para calcular el tiempo de ejecución del
Ejemplo
método cuando se utilizan condicionales.
public void metodo( int n, int arreglo[] )
{
int temp;
for ( int i = 0 ; i <= n; i++ )
{
if ( i < n )
Método
{
temp = arreglo[ i ];
arreglo [ i-1 ] = arreglo[ i ];
}
}
}
A continuación se muestra una forma de estimar la función T(n) cuando se tiene la estructura if-
else. Según la sintaxis de los lenguajes de programación, cuando se cumple el if el else no
se analiza, en el caso en el que no se cumpla el if el else se ejecuta.
22
Análisis del El else tiene en su bloque más cantidad de líneas de código. Por
Tiempo de lo tanto el peor de los casos, es que la condición if al momento de
ejecución su evaluación sea falsa y el flujo del programa continué en el else.
ACTIVIDAD
Estime y argumente el cálculo del tiempo de ejecución para los siguientes métodos:
T(n) T(n) =
23
Análisis del
Tiempo de
ejecución
T(n)
El uso de los ciclos anidados es una práctica muy frecuente y necesaria para la
implementación de diferentes tipos de algoritmos. A continuación vamos a estimar el tiempo de
ejecución del método que posee dos ciclos anidados.
Es común cuando se tienen dos ciclos anidados que inician en un valor entero positivo e iteran
hasta n (siendo n un entero positivo muy grande) o viceversa, encontramos que la función T(n)
tiene un orden cuadrático. Para verificar el correcto cálculo del tiempo de ejecución, se
recomienda asignar algún valor entero a la variable n y realizar un conteo de las instrucciones.
24
Para la estimación del tiempo de ejecución, se comenzará con el
análisis del ciclo más interno, para nuestro caso lo llamaremos
T1(n). T1(n) = 3n +2
Análisis del
Tiempo de
El ciclo externo se ejecuta n veces por lo tanto multiplicaremos n
ejecución
por el T1(n) y adicionaremos el tiempo de ejecución del ciclo
externo. Utilizaremos un T(n) para indicar el tiempo de ejecución
total del algoritmo.
T(n) = ( ( 3n + 2 ) * n ) + 2n + 2 + 3
T(n) 2
T(n) = 3n + 4n +5
El método tiene tres ciclos anidados, cada uno de los cuales se analizará por separado para
posteriormente estimar el tiempo de ejecución total. Cada uno de los ciclos itera desde un valor
pequeño hasta n, por lo que el orden de la función de tiempo de ejecución debería ser cúbico.
ACTIVIDAD
25
Objetivo: Crear habilidad en el cálculo de tiempo de ejecución para
Ejercicio
métodos.
public void ejercicio ( int n )
{
int x, y, i, j;
i = 1;
while ( i <= n )
{
j=1;
Método while( j <= n )
{
x++;
j++;
}
i++;
}
}
Análisis del
Tiempo de
ejecución
T(n)
T(n)
El siguiente cuadro, facilitará la forma para estimar la cantidad de veces que se repite un ciclo.
En ella se puede observar cual es la cantidad de iteraciones, teniendo claramente establecido:
26
Estructura del ciclo Cantidad de iteraciones
for (i=0; i < n; i++) n
for (i=0; i <= n; i++) n+1
for (i=k; i < n; i++) n-k
for (i=n; i > 0; i--) n
for (i=n; i >=0; i--) n+1
for (i=n; i > k; i--) n-k
for (i=0; i < n; i+=k) n/k
for (i=j; i < n; i+=k) (n-j)/k
for (i=1; i < n; i*=2) log2 (n)
for (i=n; i > 0; i/=2) log2 (n) + 1
A continuación se muestra un ejemplo que usa una estructura similar a la del ciclo for (i=n;
i > 0; i/=2). Para el análisis de este algoritmo, debemos tener en cuenta el valor asignado
inicialmente a la variable i = 32, esta variable dentro del ciclo se va dividiendo cada vez por 2.
Se puede afirmar entonces la cantidad de veces que itera el ciclo es: log2(n) + 1.
A continuación se muestra un ejemplo que usa una estructura similar a la del ciclo for (i=1;
i < n; i*=2).
27
Objetivo: Crear habilidad en el cálculo de tiempo de ejecución para
Ejemplo
métodos con tiempo de complejidad logarítmica.
ACTIVIDAD
Análisis del
Tiempo de
ejecución
T(n)
28
Objetivo: Crear habilidad en el cálculo de tiempo de ejecución para
Ejercicio
métodos.
public void ejercicio ( int n )
{
int t = 0, i, j=256;
while( j > 1 )
{
t++;
j = j / 2;
}
Método
for (i = 2; i <= n+3; i++)
{
for (j = n; j >= 0; j--)
{
t--;
}
}
}
Análisis del
Tiempo de
ejecución
T(n)
29
Análisis del
Tiempo de
ejecución
T(n)
Muchos de los problemas que se resuelven a nivel de programación están relacionados con los
llamados a los métodos de las clases. A continuación, se mostrarán ejemplos en los cuales se
calcula el tiempo de ejecución para algoritmos que realizan este tipo de llamado.
La estructura de selección múltiple switch permite elegir una ruta de entre varias rutas
posibles, usando para ello una variable denominada selector. El selector se compara con una
lista de constantes C1, C2, ..., Cn para cada una de las cuales hay una acción A1, A2, ..., An y:
Si el selector coincide con una constante de la lista, se ejecuta la acción correspondiente a
dicha constante. Si el selector no coincide con ninguna constante de la lista, se ejecuta la
acción default correspondiente al de lo contrario, si es que existe.
30
Las acciones A1, A2, A3, ..., An pueden ser acciones simples (una sola acción) o acciones
compuestas (un conjunto de acciones).
El tipo de la variable que se utiliza para controlar el switch es llamada selector. Cada uno de
los valores presentes en los casos (C1....Cn) debe ser de tipo compatible con el del selector.
Cada uno de estos valores debe ser único, es decir, no se aceptan rangos.
El default (por defecto) se utiliza cuando ninguno de los casos coincide con el selector. Sin
embargo, la sentencia default es opcional. Si ningún case coincide y no hay sentencia
default, no se hace nada.
El break se utiliza en sentencias switch para terminar una secuencia de acciones. Cuando
se encuentra un break, la ejecución salta a la primera línea de código que sigue a la sentencia
switch completa. Esto tiene el efecto de saltar fuera del switch.
ACTIVIDAD
31
public void ejercicio( int n, int a, int b)
{
if (n>a && b>= d)
{
for (int i=0 ; i<2n ; i+=2)
{
if (n>c)
{
metodo1();
metodo2();
Método }
}
}
else
{
metodo3();
}
T(n)
32
Objetivo: Crear habilidad en el cálculo de tiempo de ejecución para
Ejercicio llamados a métodos. Verificar que el tiempo de ejecución es el que
aparece al final del ejercicio.
public void ejercicio ( int n, int b, int c)
{
if (n>b)
{
if (n>c)
{
for ( int i=1; i <= n; i++)
{
metodo1();
}
}
else
{
Método for (int i=0; i <= n+1; i++)
{
metodo2();
}
}
}
else
{
metodo3();
}
T(n)
33
1.7 Caso de estudio: Biblioteca
Se desea crear una Aplicación para manejar la información de una Biblioteca o librería. En ella
hay libros. Cada libro tiene un código isbn, un valor y una editorial. Cada editorial tiene un
código y un nombre asignado. Se debe permitir agregar un nuevo libro, mostrar cuántos libros
se tienen de cada editorial (Solamente se tendrán 3 editoriales), listar los libros adquiridos y
mostrar el valor total de los libros que hay en la biblioteca.
a) Requerimientos funcionales
34
NOMBRE R4 - Mostrar el valor total de los libros que hay en la biblioteca.
RESUMEN Muestra el valor total de los libros que hay en la biblioteca.
ENTRADAS
RESULTADOS
El valor total de los libros.
CLASE DESCRIPCIÓN
Biblioteca Es la clase principal del mundo
Libro Permite manejar la información de un docente
Editorial Contiene la información de la Editorial
c) Modelo de clases
Para este caso de estudio se muestran las clases del principales del dominio, estas clases
contienen todos los métodos que cumplen con los requerimientos funcionales especificados en
el problema.
35
d) Implementación de métodos
Se mostraran los métodos más relevantes para la clase Biblioteca. Se debe calcular el
tiempo de ejecución para cada uno de los métodos.
T(n) =
T(n) =
T(n) =
/**
* Permite agregar un libro
*/
public void agregarLibro(String codEditorial,String isbn,double valor)
{
Libro unLibro = new Libro(codEditorial);
if(codEditorial.equals("001"))
{
contador1++;
}
else
{
if(codEditorial.equals("002"))
{
contador2++;
}
36
else
{
contador3++;
}
}
unLibro.setIsbn(isbn);
unLibro.setValor(valor);
mLibro.add(unLibro);
}
T(n) =
obtenerCantidaFabricantes()
/**
* Permite obtener la cantidad de fabricantes
* @return String Contiene la cantidad de artículos por fabricante
*/
public String obtenerCantidaFabricantes()
{
return "Addison Wesley: "+contador1+" Mc Graw Hill: "+contador2+
"Prentice Hall: "+contador3;
}
T(n) =
determinarValorMercancia()
/**
* determina el valor total de la mercancía que hay en la biblioteca.
* @return double valor de los libros en la biblioteca
*/
public double determinarValorMercancia()
{
double valor=0;
for(int i=0; i<mLibro.size(); i++)
{
valor+=mLibro.get(i).getValor();
}
return valor;
}
T(n) =
getInfoLibros()
/**
* Devuelve la información de los libros que hay en la biblioteca
* @return String[]
*/
public String[] getInfoLibros()
{
String []info=new String [mLibro.size()];
for(int i=0; i<mLibro.size(); i++)
{
info[i] = mLibro.get(i).toString();
}
return info;
}
T(n) =
37
Nuestro proceso no ha finalizado aquí puesto que debemos pasar del diseño a la
implementación. Esta fase se inicia con la evaluación (en términos de complejidad de
algoritmos y de espacio ocupado, de dificultad y de grado de desacoplamiento) de los diseños
que fueron propuestos, los cuales deberán refinarse hasta que sea posible argumentar por qué
ese diseño es el mejor, siendo importante llevar a cabo las comparaciones pertinentes respecto
a los diseños obtenidos, fruto de la cual se seleccionará uno de ellos.
ACTIVIDAD
Escriba los métodos para la clase Biblioteca que resuelven los problemas que se describen
a continuación.
Análisis del
Tiempo de
ejecución
T(n)
Análisis del
Tiempo de
ejecución
T(n)
38
Objetivo: Generar habilidad en la construcción de métodos para
Ejercicio posteriormente calcular su tiempo de ejecución.
Escriba un
método que
muestre las
posiciones de
los libros cuyo
código isbn
termina en el
carácter 5.
}
Análisis del
Tiempo de
ejecución
T(n)
Análisis del
Tiempo de
ejecución
T(n)
39
Cuando en una aplicación se van a procesar pocos datos de entrada, es poco importante
analizar un algoritmo independiente del crecimiento de la función. Por ejemplo, si se desea
procesar 50 datos, es casi indistinto que se utilice un algoritmo con tiempo de ejecución n o n
log(n), pero si se desea procesar 2000000 datos, el algoritmo con n log(n) crece
considerablemente con relación a n. Es por ello necesario entrar a realizar un estudio sobre el
crecimiento y comportamiento asintótico de los tiempos de ejecución de los algoritmos.
Función nombre
Log2(n) (Logarítmico)
n (Lineal)
n log(n) n log(n)
2
n Cuadrático
3
n Cúbico
n
2 Exponencial
A continuación se muestra una tabla con el crecimiento de algunas funciones polinómicas y las
cuales son las más frecuentemente usadas en el análisis de algoritmos.
Se tienen dos algoritmos los cuales tienen un tiempo de ejecución dado respectivamente por
2n+10 y n log(n), para el mismo problema, cuál de ellos recomendar y por qué?. En la siguiente
figura se observa que a partir del valor 15, la función n log(n) está por encima de la función 2n
+ 10, por lo que se recomienda que a partir del valor 15, se debe recomendar el algoritmo con
tiempo de ejecución 2n + 10.
40
En la siguiente figura se ve el comportamiento de dos algoritmos que tienen tiempos de
2
ejecución 2n + n + 5 y log(n). Según su comportamiento, se observa que en todo caso la
función logarítmica log(n) siempre está por debajo de la cuadrática, por ello se recomienda el
algoritmo que tiene la función log(n).
ACTIVIDAD
Dadas la siguiente grafica con sus correspondientes funciones, realice un análisis del
comportamiento de las mismas e indique cuando la una es más eficiente que la otra. Las
2
funciones a comparar son 2n y log(n).
Para una implementación completa que permita comparar dos algoritmos, se desea determinar
el máximo común denominador de dos números enteros positivos. El máximo común divisor
(MCD) se define de la forma más simple como el número más grande posible, que permite
dividir a esos números.
41
public int mcd(int a,int b)
{
int valora, valorb;
valora = a;
valorb = b;
while (valora!=valorb)
{
if(valora<valorb)
Método 1
{
Máximo común
valorb = valorb - valora;
Divisor
}
else
{
valora = valora - valorb;
}
}
return valora;
}
Se comparan ambos algoritmos asumiendo que los valores que se envían por parámetro al
método son: a = 30 y b = 18. En el siguiente cuadro se observan las instrucciones necesarias
para la deducción del MCD del segundo algoritmo.
En el siguiente cuadro se observan las instrucciones que son necesarias para la deducción del
MCD del primer algoritmo.
42
Considerando los cuadros anteriores, se puede deducir por los valores ingresados, que el
primer algoritmo es más eficiente que el segundo. Esto dada la cantidad de instrucciones que
se ejecutan. Pero es posible afirmar lo mismo si los valores que se ingresan son
respectivamente: a = 38 y b = 4.
En el siguiente cuadro se observan las instrucciones necesarias para la deducción del MCD del
segundo algoritmo.
En el siguiente cuadro se observan las instrucciones que son necesarias para la deducción del
MCD del primer algoritmo, con los valores anteriormente dados.
Se desea crear una Aplicación para manejar la información de una Agenda Telefónica.
Se debe permitir Agregar Persona, Eliminar Persona, mostrar el listado de todas las personas,
buscar persona por nombre y generar un listado de personas que están cumpliendo años el
día de hoy.
43
a) Requerimientos funcionales
ENTRADAS
RESULTADOS
ENTRADAS
RESULTADOS
ENTRADAS
RESULTADOS
44
c. Especificar las relaciones entre las clases ( AgendaTelefonica, Pesona, Fecha).
d. Construcción de la aplicación
Para las siguientes clases escriba en java las los métodos e instrucciones necesarias para el
correcto funcionamiento de la aplicación. Como estrategia se dejan parcialmente planteados
métodos de implementación y los cuales deben ser completados.
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
/**
* Constructor de la clase AgendaTelefonica
*/
public AgendaTelefonica()
{
contadorContactos = ___________;
contadorTotal = ___________;
contadorCumpleaños = ___________;
listaPersona = new _____________;
}
/**
* Permite inicializar el atributo listaPersona
* @param listaPersona contiene la información de los contactos
*/
public void setListaPersona( ArrayList <Persona> listaPersona )
{
this.listaPersona = ___________________;
}
45
/**
* @return ArrayList <Persona> devuelve la información de las
* personas contenidas en la agenda.
*/
public ArrayList <Persona> getListaPersona()
{
__________________________;
}
/**Constructor de la AgendendaTelefonica
* @param listaPersona */
/**
* @return String[] devuelve la información de los contactos
*/
public String[] obtenerContactos()
{
int i;
contadorTotal = 0;
String[] contactos;
Contactos = new String[ listaPersona.size() + 1 ];
contadorTotal = listaPersona.size();
/**
* Permite obtener un contacto que contenga a nombre.
* @return String[] información del contacto que coincide con el
* nombre especificado.
*/
46
public String[] obtenerContatosNombre( String nombre )
{
int i = 0;
contadorContactos = 0;
String[] contactos;
ArrayList<String> contactosNombre = new ArrayList<String>();
47
/** Permite eliminar una persona especificando la cédula */
public void eliminarPersona(String cedula)
{
}
}
ACTIVIDAD
Una vez escritos los métodos para la clase AgendaTelefonica, se debe calcular el tiempo de
ejecución de cada uno de ellos.
Tiempo de Ejecución
Método
T(n)
String[] obtenerContactos()
int getContadorContactos()
int getContadorTotal()
String[] obtenerContatosCumpleaños()
Los órdenes polinómicos más comunes de los tiempos de ejecución de los algoritmos son los
siguientes:
Log2(n) (Logarítmico). Los algoritmos con este tiempo de ejecución son considerados
como muy eficientes.
n (Lineal). Es común en aplicaciones que utilizan ciclos sencillos.
n log(n). Común encontrarla en algoritmos de ordenamiento eficientes.
2
n (Cuadrático). Habitual cuando se usan 2 ciclos anidados.
3
n (Cúbico). Habitual cuando se usan 3 ciclos anidados.
n
2 (Exponencial). No suelen ser muy útiles en la práctica por el elevado tiempo de
ejecución.
48
La elección de un buen algoritmo está orientada hacia la disminución del costo que implica la
solución del problema; considerando este enfoque es posible orientar esta elección en dos
criterios (Iparraguirre, 2009):
Debemos tener claro que los aspectos interesantes a reducir son el tiempo y el espacio en
memoria. En cuanto al tiempo nos hemos centrado en la función T(n), la cual determina la
cantidad de operaciones que efectúa un algoritmo. De acuerdo a la forma como se calcula el
T(n) es importante hacer dos precisiones:
Estas dos precisiones nos permiten dar una aproximación al concepto de complejidad
computacional de un algoritmo, como un concepto que recoge la naturaleza asintótica de la
función T(n) y no su valor exacto. Dicha naturaleza debe dar información de cómo crece la
función cuando aumenta el tamaño de n. Normalmente, el análisis de la complejidad de los
algoritmos está relacionado con conceptos matemáticos que son necesarios precisar.
En 1892, Paul Gustav Heinrich Bachmann inventó una notación para caracterizar el
comportamiento asintótico de funciones. Esta invención se conoce como notación O grande. El
autor define una función que mide el tiempo de ejecución de un algoritmo, y que debe cumplir
las siguientes condiciones:
Si f(n) es alguna función definida sobre los enteros no negativos n, se dice entonces que T(n)
es O(f(n)) (McConnell, 2007).
49
Comparación de O(f(n)) con relación a T(n)
T(n) es O(f(n)) si existe un entero n0 y una constante c > 0, tal que para todos los enteros
n >= n0 , tenemos que T(n) <= cf(n). En la Figura anterior se realiza una comparación de
O(f(n)) con relación a T(n)
Dada una función f, queremos estudiar aquellas funciones g que a lo sumo crecen tan de prisa
como f. Al conjunto de tales funciones se le llama cota superior de f y lo denominamos O(f).
Conociendo la cota superior de un algoritmo podemos asegurar que, en ningún caso, el tiempo
empleado será de un orden superior al de la cota (Cormen, Leiserson, Rivest, & Stein, 2001).
Es una práctica común cuando escribimos expresiones Big Oh, eliminar todos los
términos menos el término más significativo. Por ejemplo si tenemos una función
5 3 5
f(n)=4n -2n , simplemente escribimos O(n ).
3
Es común eliminar coeficientes constantes. Por ejemplo si se tiene O(6n ),
3
simplemente escribimos O(n ). Como caso especial de esta regla, si la función es una
constante y si tenemos O(8), se escribe O(1).
Según (Ramirez, 2003), las propiedades de la función O son la transitiva, los términos de
orden inferior y la regla de la suma, a continuación se muestra una descripción de cada uno de
ellos.
Transitividad
Esta regla está basada en la transitividad de la relación menor que “<”, ya que si A <= B y
B <=C se puede concluir que A<= C. Asociando esto a la función O se tiene que: Si f(n) es
O(g(n)) y g(n) es O(h(n)), se puede concluir que f(n) es O(h(n)).
Suponga que T(n) es de la forma polinomial: entonces es posible, eliminar todos los
k
términos con exponente inferior k. Por la regla anterior T(n) es O(n ) (Aho & Ullman,
1995)..
50
Regla de la Suma
2
Suponga que un algoritmo está formado por dos secciones, una de ellas con O(n ) y la otra
3
con O(n ). Entonces es posible sumar estos dos órdenes de complejidad para obtener la
complejidad total del algoritmo. La regla es la siguiente:
Suponga que para T1(n) se sabe que es O(f1(n)) y T2(n) es O(f2(n)) y suponga, además,
que f1 crece más rápido que f2. Esto se traduce en que f2(n) es O(f1(n)). En consecuencia
se puede concluir que T1(n) + T2(n) es O(f1(n)).
Dada una función f, queremos estudiar aquellas funciones g que a lo sumo crecen tan
lentamente como f. Al conjunto de tales funciones se le llama cota inferior de f y lo
denominamos f. Conociendo la cota inferior de un algoritmo podemos asegurar que, en ningún
caso, el tiempo empleado será de un orden inferior al de la cota.
Decir que T(n) es Ω (f(n)) se lee “omega grande de f(n)”, significa que existe una constante
positiva c y tal que para los n, no se cumple que T(n) ≥ cf(n), (f(n) es una cota inferior para
T(n)).
1.10.4 Notación
Dada una función g de los enteros no negativos a los números reales positivos. Entonces
(g)=O(g) Ω (g), es decir, el conjunto de funciones que están tanto en O(g) como en Ω (g).
Comunmente la forma de leer es “f a (g)”, es decir, “f es del orden de g”.
Asociada a las anteriores notaciones es posible el uso de otros conceptos matemáticos entre
las cuales encontramos las denominadas funciones de piso y techo. La función piso y techo
puede ser utilizada para cualquier número flotante o entero y.
La expresión denotada como y se conoce como la función piso de y corresponde al entero
más grande que es menor o igual que x. 2.1 = 2, 4.5 = 4 y 6.9 = 6.
La expresión denotada como y se conoce como la función techo de y corresponde al número
entero más pequeño que es mayor o igual que x. Por ejemplo, [2.8]=2 y [6.2]=7.
51
1.10.5 Regla del Límite
Una herramienta potente y versátil para demostrar que algunas funciones están en el orden de
otras y para demostrar lo contrario, es la regla de límite, la cual manifiesta que dadas las
funciones arbitrarias f y g: N Z (Brassard & Bratley, 1997).
+
De acuerdo al criterio dado anteriormente, evaluaremos cada una de las expresiones utilizando
la regla de L’ Hôpital. Para los siguientes ejemplos, demostraremos cuando una función está en
2
el orden de la otra, por ejemplo, si queremos demostrar que n está en el orden de n ,
2
escribiremos n es O(n ).
( )
( )
( ) ( )
A continuación se muestra una serie de ejemplos con lo que se pretende mostrar mediante la
regla del límite si una función está en la otra.
Ejemplo 1
La función es
, por L’ Hôpital
= , por L ’ Hôpital
=
Cuando , la evaluación de este límite tiende al infinito, por tanto no es del orden
de y está en el orden de
Ejemplo 2
La función es
, por L’ Hôpital
, simplificando
, simplificando
52
, por L’ Hôpital
, simplificando
Cuando , la evaluación de este límite tiende al infinito, por tanto no es del orden
de , pero pertenece al orden de .
Ejemplo 3
La función es
, por L’ Hôpital
, simplificando
, simplificando
, simplificando
, por L’ Hôpital
, simplificando
Cuando tiende al infinito, la evaluación de este límite tiende al infinito , por lo tanto
no es del orden de , pero pertenece al orden de .
Ejemplo 4
La función es
, por L’ Hôpital
, por L’ Hôpital
53
, por L’ Hôpital
, por L’ Hôpital
Ejemplo 5
La función es
, por L’ Hôpital
, simplificando
, por L’ Hôpital
, por L’ Hôpital
, simplificando
, simplificando
54
Es la complejidad considerada como “ideal”, pero un problema considerado de mediana
dificultad de solucionar, difícilmente tendrá este orden de complejidad. La razón por la que a
estas operaciones se les ha asignado un orden constante, es que cada una de ellas es resuelta
por un número muy pequeño de instrucciones de lenguaje de máquina. A continuación se
muestran valores asociados a la complejidad O(1).
Complejidad O(1)
50
2
50000
96
3
La complejidad logarítmica tiene un mayor orden que la complejidad constante, este orden de
complejidad se obtiene entre otros en algunos algoritmos recursivos clásicos como la búsqueda
binaria y los árboles binarios de búsqueda con sus operaciones de eliminación, búsqueda e
55
inserción. Este orden de complejidad está relacionado con algoritmos que son consideramos
muy eficientes.A continuación se muestran valores asociados a la complejidad log(n).
Complejidad O( logn)
5+
2 log(n)
log(n) + 150000
96
log(n) + 3
Orden de
O(log2(i) )
complejidad
56
Complejidad (n)
10 +
3 log(n) +n
n + 520000
9
n+4
Para el método que aparece en el Cuadro, tenemos que su complejidad está relacionada
directamente con la implementación de ciclos sencillos. Este método retorna la sumatoria de
los elementos de un arreglo de enteros.
Análisis del Inicialmente, se analiza el ciclo del método. Se observa que este
Orden de tiene un ciclo que itera desde cero hasta n. El tiempo de ejecución
Complejidad del algoritmo es: T(n) = 3n+5
Orden de O(n)
complejidad
La complejidad nlog(n) tiene un orden mayor que la complejidad constante, logarítmica, lineal,
encontramos este orden de complejidad en algunos algoritmos de ordenamiento como el
heapSort y el mergeSort. A continuación se muestran algunas funciones con orden nlog(n).
Complejidad (n)
nlog(n) +n+10
nlog(n) + log(n)
57
Inicialmente, se analiza el ciclo más interno y posteriormente se
analiza el ciclo externo.
Análisis del
Orden de El ciclo interno se repite log2 veces.
Complejidad El ciclo externo se repite n veces.
Multiplicando el ciclo interno por el ciclo externos, se tiene un
orden de complejidad nlog2(n).
Orden de O(nlog2(n))
complejidad
2
Complejidad Cuadrática O(n )
La complejidad cuadrática está relacionada con el uso de dos ciclos anidados. En el Cuadro
mostramos un ejemplo en el cual se suman los elementos de un arreglo bidimensional de
enteros.
Orden de 2
O(n )
Complejidad
3
Complejidad Cúbica O(n )
La complejidad cúbica tiene un orden mayor que la complejidad constante, logarítmica, lineal,
nlog(n) y la cuadrática. Este orden de complejidad normalmente se obtiene cuando se tiene
tres ciclos anidados.
58
A continuación se muestran algunas funciones con orden cúbico.
3
Complejidad (n )
3 2
n + n +n
3
n + log(n) + 500
3 2 15
n +n +5
Complejidad Exponencial
59
public int potencia( int n )
{
int m, i;
m = 1;
for ( i = 1 ; i <= Math.pow( 2, n ) ; i++ )
Método {
m = m * i;
}
return m;
}
n
Análisis del La cantidad de veces que se repite el ciclo es 2 veces. El tiempo
n
Orden de de ejecución de este algoritmos es: T(n) = 3(2 ) + 6
Complejidad
Orden de n
O(2 )
Complejidad
ACTIVIDAD
Dado el siguiente método explique qué problema resuelve y determine su tiempo de ejecución.
Análisis del
Orden de
Complejidad
Orden de
complejidad
60
{
suma= suma + matriz[i][j];
}
}
}
return suma;
}
Análisis del
Orden de
Complejidad
Orden de
complejidad
Dado el siguiente método explique qué problema resuelve y determine su tiempo de ejecución.
61
Dado el siguiente método explique qué problema resuelve y determine su tiempo de ejecución.
Ejemplo
public int misterio (int a, int b)
{
int resultado = 1;
for(int i = 1; i < a; i++)
{
if(a%i==0 && b%i==0)
Método {
resultado = i;
}
}
return resultado;
}
Análisis del
Tiempo de
ejecución
Orden de
complejidad
Se desea crear una Aplicación que permita manejar la información de un concurso docente. Se
debe permitir agregar un nuevo docente, calcular el puntaje obtenido por dicho docente,
ordenar por nombre a los docentes que están participando en el concurso y listar los docentes
de acuerdo al puntaje obtenido en orden ascendente. Se deberá permitir al seleccionar un
docente conocer su información básica (nombre, apellido y total de puntos obtenidos).
El docente que participe debe tener como mínimo título de pregrado. De lo contrario no podrá
ser aceptado en el concurso. Un pregrado da 178 puntos, una especialización da 30 puntos. Si
el docente tiene título de maestría se dan 60 puntos (esto en el caso de que tenga también
especialización). Si no tiene especialización se le otorgarán 120 puntos. Si el docente tiene ya
tiene maestría o estudios de especialización se le reconocerán por concepto de doctorado 90
puntos. Si no tiene estos estudios se le reconocerán 170 puntos.
a) Requerimientos funcionales
RESULTADOS
ENTRADAS
RESULTADOS
El puntaje del docente
62
NOMBRE R3 – Ordenar por nombre a los docentes.
RESUMEN
ENTRADAS
RESULTADOS
RESULTADOS
RESULTADOS
NOMBRE DESCRIPCIÓN
Profesor(String cedula, String
nombre, String apellido, String
foto, boolean pregrado,boolean
especializacion, boolean maestria,
boolean doctorado,
int cantidadLibros)
getNombre()
setNombre(String nombre)
getApellido()
setApellido(String apellido)
isPregrado()
setPregrado(boolean pregrado)
isEspecializacion()
setEspecializacion(boolean
especializacion)
isMaestria()
setMaestria(boolean maestria)
isDoctorado()
63
setDoctorado(boolean doctorado)
getCantidadLibros()
setCantidadLibros(int
cantidadLibros)
fijarPuntos()
getPuntos()
Los siguientes son los métodos más importantes para la clase profesor. Se omiten los métodos
accesores y modificadores.
64
fijarPuntos()
/** Permite fijar la cantidad de puntos */
if(especializacion==true)
{
puntos+=30;
}
if(_________________________)
{
puntos+=120;
}
if(_________________________)
{
puntos+=60;
}
if(doctorado==true)
{
if(______________________)
{
puntos+=170;
}
else
{
puntos+=80;
}
}
puntos += ( _________________*15);
ACTIVIDAD
1. Escriba una interpretación en torno a la forma como se calcula el puntaje de cada docente.
/**
* Permite agregar un nuevo Profesor
* @param cedula. Es la cédula del profesor. cedula!=null, cedula!=""
* @param nombre. Es el nombre del profesor. nombre!=null, nombre!=""
* @param apellido. Apellido del profesor. apellido!=null, apellido!=""
* @param pregrado. Indica si tiene pregrado.pregrado==true
65
* @param especialización. Indica si tiene especialización
* @param maestria. Indica si tiene maestría
* @param doctorado. Indica si tiene doctorado
* @param cantidadLibros. Indica la cantidad de libros.
* @return verdadero si se pudo agregar el concursante
*/
public boolean agregarParticipante (String cedula, String nombre,
String apellido, boolean pregrado,boolean especializacion, boolean
maestria, boolean doctorado, int cantidadLibros)
{
boolean esta=false;
if(miProfesor.size()>0)
{
esta = determinarSiExisteProfesor(cedula);
}
if(esta==false)
{
Profesor miProfe = new Profesor(_________________________
);
miProfe.________________
miProfesor.add(_______________);
return _______________;
}
return _______________;
}
ConcursoProfesor()
/**
* Este es el método constructor
*/
public ConcursoProfesor()
{
}
determinarSiExisteProfesor(String cedula)
/**
* Permite verificar si un profesor existe en el listado
* @param cedula. Es la cédula del profesor
* @return booleano que indica si existe o no
*/
public boolean determinarSiExisteProfesor(String cedula)
{
66
determinarSiNoHayRepeticiones()
/**
* Permite determinar si no hay profesores repetidos.
* @return booleano que indica si existe o no.
*/
public boolean determinarSiNoHayRepeticiones()
{
for(___________________________)
{
for(______________________________)
{
if(________________________________________________________)
{
return ____________;
}
}
}
return ____________;
}
generarListadoPorPuntaje()
/**
* Permite generar el listado de docentes por puntaje.
* @return un array de String con la información de los docentes.
*/
public String[] generarListadoPorPuntaje()
{
}
generarListado()
/**
* Permite generar el listado de los docentes.
* @return un array de String.
*/
public String[] generarListado()
{
String info[]=new String[miProfesor.size()];
67
generarListadoOrdenadoPorNombre()
/**
* Permite generar el listado ordenado por nombre.
* @return un array con la información de los docentes.
*/
public String[] generarListadoOrdenadoPorNombre()
{
String info[]=new String[miProfesor.size()];
for(int i=0; i<miProfesor.size(); i++)
{
for(int j=0; j<miProfesor.size()-1; j++)
{
}
}
return info;
}
ACTIVIDAD
Una vez escritos los métodos para la clase AgendaTelefonica, se debe calcular el tiempo de
ejecución de cada uno de ellos.
68
En toda definición recursiva de un problema se debe establecer un estado base, es decir, un
estado en el cual la solución no se presente de manera recursiva sino directamente. Esto
permite que las llamadas recursivas no continúen indefinidamente. Por lo anterior, una solución
recursiva está compuesta por:
La llamada a un algoritmo recursivo conduce a una nueva versión del método que comienza a
ejecutarse, también a una organización de la gestión de los parámetros y la memoria. Estos
datos deben estar organizados de forma que al terminar cada llamado que se está ejecutando,
se devuelvan los datos correctos y se actualice la información que permita su gestión.
A nivel del diseño de los algoritmos, el uso de la recursividad puede presentar soluciones
cortas y claras, además de permitir en muchas ocasiones la solución de problemas complejos.
Generalizando tenemos:
a*b=a , si b = 1
a * b = a + a * (b -1) , si b > 1
La siguiente es la implementación del método multiplicar, los valores se reciben por parámetro.
69
El algoritmo tiene dos condiciones de parada:
Este método recibe por parámetros un arreglo y una variable n la cual contiene el tamaño del
arreglo. La instrucción if(arreglo[n-1]==0) es la encargada de verificar si el elemento en
la posición n es igual a cero. El algoritmo termina cuando el valor de n es cero.
70
La siguiente es una representación para este algoritmo recursivo.
ACTIVIDAD
Dados los siguientes métodos recursivos, determinen si resuelven el problema que se plantea,
en el caso en el cual exista un error se debe organizar el método en la parte de observaciones
de forma tal que este quede correcto.
71
Problema: Este método determina si un número es perfecto o no lo
Ejercicio
es. Los valores de los parámetros son: aux = 1 e i = 2.
Observaciones
Observaciones
/**
* Determina si la palabra es o no palíndroma
* @param cadena contiene la cadena a analizar
* @param i toma el valor inicial i=0
Método * @param j toma el valor de cadena.length()-1
*/
public boolean palindroma(String pal,int i, int j)
{
72
}
/**
* calcula la suma de los elementos de un arreglo
* @param x[] contiene los elementos enteros
* @param n toma el tamaño del arreglo
*/
public int suma( int x[], int n )
{
Método
El valor de TF (n) se establece normalmente por una inducción de los argumentos de tamaño n.
Así es necesario seleccionar una noción correcta del tamaño de los argumentos, que garantice
que las funciones sean llamadas con un tamaño progresivamente menor de los mismos, a
medida que la recursividad tiene efecto. Una vez se establezca el tamaño del conjunto de datos
de entrada, se deben considerar dos casos:
73
De manera más formal, se plantean dos formas de resolver la relación de recurrencia:
Sustituir repetidamente la regla inductiva dentro de ella misma, hasta que se obtenga
alguna relación entre T(n) y T(1), es decir, la expresión de inducción y la base.
Suponer una solución y probar su correctitud sustituyéndola en la expresión base y la
inductiva.
Introducidos por el matemático Leonardo de Pisa, quien se llamaba a si mismo Fibonacci. Cada
número de la secuencia se obtiene sumando los dos anteriores.
fib(n) = 0, n=0
fib(n) = 1, n=1
fib(n) = fib(n -1) + fib(n - 2), n >= 2
Por definición, los dos primeros valores son 0 y 1 respectivamente. Los otros números de la
sucesión se calculan sumando los dos números que le preceden. A continuación se muestran
los primeros 13 números de Fibonacci. A continuación se muestra la implementación el
algoritmo recursivo que encuentra el n-ésimo número de la sucesión Fibonacci.
else
{
return (fibo (n-2) + fibo (n-1) );
}
}
}
Caso Base: T(1) = a
Inducción: T(n) = b + T(n - 2) + T(n - 1)
Análisis del
Para simplificar el análisis, se toma el peor de los casos entre
Tiempo de
T(n- 2) y T(n-1). Se puede afirmar que T(n-1) tomará más tiempo,
Ejecución
ya que n - 1 es más grande que n - 2. Se asume entonces que el
caso inductivo es: T(n) = b + 2T(n - 1)
74
Se asignan valores a n:
T(1) = a
T(2) = b + 2T(1) = b + 2a
2 2
T(3) = b + 2T(2) = b + 2(b + 2a) = ( 2 – 1 )b + 2 a
3 3
T(4) = b + 2T(3) = b + 2(3b + 4a) = ( 2 – 1 )b + 2 a
4 4
T(5) = b + 2T(4) = b + 2(7b + 8a) = ( 2 – 1 )b + 2 a
5 5
T(6) = b + 2T(5) = b + 2(15b + 16a) = ( 2 – 1 )b + 2 a
n-1 n-1
Se concluye entonces que: T(n) = ( 2 – 1)b + ( 2 )a
Orden de n
O(2 )
Complejidad
75
Caso base: T(1) = a
Inducción: T(n) = b + T(n/2)
Si n = 2, T(2) = b + T(1) = 1b + a
Si n = 4, T(4) = b + T(2) = b + b + a = 2b + a
Si n = 8, T(8) = b + T(4) = b + 2b + a = 3b + a
Si n = 16, T(16) = b + T(8) = b + 3b + a = 4b + a
Orden de
O(log(n))
Complejidad
else
{
return calcularPotencia(x, n/2) *
calcularPotencia(x, n/2) * x;
}
}
}
Se tiene que para el peor de los casos, el caso base es T(1) = 2.
Para el caso inductivo tenemos que el caso base es de orden
constante O(1) y se tienen 2 llamadas recursivas cada una de
2*(n/2), por lo tanto el caso inductivo T(n) = 2T( n/2 ) + O(1).
Generalizando tenemos, que el caso base
Análisis del y el caso inductivo, quedan expresados de la siguiente manera:
tiempo de
ejecución T(1) = O(1) = a (se reemplaza por una constante):
T(n) = 2T( n/2 ) + b
76
T(8) = 2T(4) + b T(8) = 8a + 7b
T(16) = 2T(8) + b T(16) = 16a + 15b
Orden de
O(n)
Complejidad
ACTIVIDAD
77
Análisis del
Tiempo de
Ejecución
Orden de
complejidad
El análisis del tiempo de ejecución de los algoritmos recursivos se puede realizar de forma
inductiva. En este apartado se mostrarán algoritmos con la característica de ser recursivos.
Dado que los aspectos en los cuales estamos más interesados en el análisis de algoritmos son
particularmente los conceptos de tiempo de ejecución y orden de complejidad, no nos
detendremos a analizar que hace el algoritmo, únicamente se estudiara su tiempo de ejecución
y su orden de complejidad.
Ejemplo 1
78
else
{
return calcularPotencia(x,n/2) *
calcularPotencia(x, n/2) * x;
}
}
}
Se tiene que para el peor de los casos, el caso base es T (1) = 2. Para el caso inductivo
tenemos que el caso base es de orden constante O (1) y se tienen 2 llamadas recursivas cada
una de 2*(n/2), por lo tanto el caso inductivo T(n) = 2T(n/2)+O (1). Generalizando tenemos, que
el caso base y el caso inductivo, quedan expresados de la siguiente manera:
Ejemplo 2
A continuación se muestra otro algoritmo recursivo que retorna la potencia de un número. Este
algoritmo debe tener un orden de complejidad inferior al anterior. Esto se deduce dada la
cantidad de llamados recursivos que se utilizan para resolver el problema.
En el análisis del caso base se tiene que para el peor de los casos el tiempo de ejecución es:
T (1) = 2. Para el caso inductivo se tienen una llamada recursiva de (n/2) y el caso base es de
orden constante, por lo tanto el caso inductivo T(n) = T(n/2) + O (1).
79
T (1) = 8
T(n) = T(n/2) + 8
Ejemplo 3
T (1) = O (1)
T(n) = T(n - 2) + T(n - 3)
Se considera el peor caso, y se tiene que T(n-2)+T(n-3) < 2 * T(n - 1), por lo tanto 2 * T(n - 1) es
el peor caso. Con esta comparación se puede garantizar una cota superior para este problema.
T (1) = a
T(n) = 2T(n - 1)
Ejemplo 4
80
else
{
return n*(recursivo4(n-1) +
recursivo4(n-1) + recursivo4(n-1));
}
}
En el caso base se tiene que para el peor de los casos el tiempo de ejecución es: T (1) = 4.
Para el caso inductivo se tienen 3 llamadas recursivas, cada una de (n-1), por lo tanto el caso
inductivo es: T(n) = 3T(n-1)+4.
T (1) = 4
T(n) = 3T(n - 1) + 4
Generalizando, tenemos:
n+1
T (n) = 3T (n - 1) + b T (n) = 3 a+∑
n
El orden complejidad para un algoritmo con este caso base e inductivo es de orden O (3 ).
ACTIVIDAD
81
Deduzca usando recurrencias por inducción el tiempo de ejecución y el orden de complejidad
del siguiente algoritmo recursivo.
Consiste en sustituir las recurrencias por su igualdad hasta llegar a cierta T(n) que sea
conocida. A continuación, se muestra una serie de algoritmos recursivos que permiten deducir
su orden de complejidad.
Ejemplo 1
Este algoritmo retorna la cantidad de números divisibles tanto por 2 y por 3. Se analizará su
tiempo de ejecución y su orden de complejidad.
82
Podemos deducir lo siguiente:
T(n) = c , si n ≥ 1
T(n - 1) = c1 , si n > 1
T (n) = T (n - 1) + c1
= (T (n - 2) + c1) + c1 = T (n - 2) + 2c1
= (T (n - 3) + c1) + 2c1 = T (n - 3) + 3c1
= (T (n - 4) + c1) + 3c1 = T (n - 4) + 4c1
= (T (n - 5) + c1) + 4c1 = T (n - 5) + 5c1
= (T (n - q) + nc1
Cuando q = n - 1, entonces T(n) = T (1) + c1(n - 1), por lo tanto el orden de complejidad una vez
resuelta la relación de recurrencia es O(n).
Ejemplo 2
T(n) = 1 , si n ≤ 1
2T(n - 1) + 1 , si n > 1
T(n) = 2 * T(n - 1) + 1
2 2
= 2 * (2 * T(n - 2) + 1) + 1 = 2 * T(n - 2) + (2 - 1)
...
k k
= 2 * T(n - k) + (2 + 1)
n-1 n-1 n
Para k = n - 1, T(n) = 2 , T (1) + 2 - 1 y por lo tanto T(n) es O (2 ).
Ejemplo 3
83
A continuación se muestra otra relación de recurrencia que permite encontrar el orden de
complejidad de algún algoritmo recursivo.
T(n) = 1 , si n ≤ 1
T(n/2) + 1 , si n > 1
T (n) = T (n/2) + 1
2
= T (n/2 ) + 1
3
= T (n/2 ) + 1
4
= T (n/2 ) + 1
k
= T (n/2 ) + n
k
n/2 = 1, se resuelve para k = log2(n), tenemos por lo tanto que el tiempo de ejecución es T(n) =
T(1)+log2(n-1) y por lo tanto T(n) es O(log2(n)).
Ejemplo 4
T(n) = T(n - 1) + n
= (T(n - 2) + (n - 1)) + n
= ((T(n - 3) + (n - 2)) + (n - 1) + n
...
= T (n - k) + ∑
Si k = n - 1,
T (n) = T (1) + ∑ = 1+ (∑ +∑ ) = 1+n (n-1)-(n-2) (n-1)/2.
2
Por lo tanto este algoritmo recursivo tiene un orden de complejidad O (n ).
84
ACTIVIDAD
El objetivo de los algoritmos de ordenamiento es organizar una secuencia de datos, sea cual
sea su tipo. Este tema ha sido ampliamente estudiado y aplicado en diferentes contextos de las
ciencias de la computación. Se analizarán los algoritmos de ordenamiento considerados como
clásicos, además se estudiará la eficiencia de cada uno de ellos por medio de la deducción de
sus órdenes de complejidad.
85
1.13.1 Método de Ordenamiento ShakerSort
En la segunda etapa “de abajo hacia arriba” se trasladan los elementos más grandes hacia la
parte de abajo del arreglo, almacenando en otra variable la posición del último elemento
intercambiado. Las pasadas sucesivas trabajan con los componentes del arreglo comprendidos
entre las posiciones almacenadas en las variables. El algoritmo termina cuando la variable que
almacena el extremo de arriba del arreglo es mayor que el contenido de la variable que
almacena el extremo de abajo.
Orden de 2
O(n )
complejidad
86
ACTIVIDAD
Este método de selección tiene este nombre puesto que la manera de realizar el ordenamiento
es seleccionando en cada iteración el menor elemento del arreglo e intercambiarlo en la
posición correspondiente. Inicialmente se encuentra el menor elemento dentro del arreglo y se
intercambia con el primer elemento del arreglo. Luego, desde la segunda posición del arreglo
se busca el menor elemento y se intercambia con el segundo elemento del arreglo.
87
Así sucesivamente se busca cada elemento de acuerdo a su índice i y se intercambia con el
elemento que tiene el índice k. Se ordenaran los elementos del arreglo de forma ascendente.
ACTIVIDAD
Este método consiste en tomar cada uno de los elementos e insertarlos en una sección del
conjunto de datos que ya se encuentra ordenado. El conjunto de datos que ya se encuentra
ordenado se inicia con el primer elemento del mismo. Este método comúnmente se asocia con
la forma de organizar un juego de cartas.
88
public void insercion( int arreglo[] )
{
int i, llave;
for ( int j = 1 ; j < arreglo.length ; j++ )
{
llave = arreglo[ j ];
i = j - 1;
Método while( i >= 0 && arreglo[ i ] > llave )
{
arreglo[i + 1] = arreglo[ i ];
i--;
}
arreglo[i + 1] = llave;
}
}
Análisis del
método de
ordenamiento
Orden de 2
O(n )
complejidad
ACTIVIDAD
89
Posteriormente, cada uno de los subarreglos es ordenado independientemente mediante
llamados recursivos. Cada subarreglo se ordena por aparte. Esta elección también puede
realizarse al inicio de la lista o arreglo, o al final del arreglo. Este algoritmo inicialmente
compara todos los elementos y después de ello empieza a dividir recursivamente la lista o el
arreglo.
T(1) = a
T(n) = 2T(n/2) + O(n)
Orden de
complejidad Reemplazando el caso base por una constante, tenemos que:
T(1) = a
T(n) = 2T(n/2) + bn
90
Utilizando el reemplazo de la base en la inducción:
T(1) = a
T(2) = 2T(1) + 2b = 2a + 2b
T(4) = 2T(2) + 4b = 4a + 8b
T(8) = 2T(4) + 8b = 8a + 24b
T(16) = 2T(8) + 4b = 16a + 64b
ACTIVIDAD
884 237 183 245 122 2305 1131 212 1398 658 23
El método de ordenamiento Shell es una versión mejorada del método de Inserción directa.
Recibe su nombre en honor a su autor, Donald L. Shell, quien lo propuso en 1959. El objetivo
de este método es ordenar un arreglo formado por n enteros. Inicialmente ordena subgrupos de
elementos separados k unidades (respecto de su posición en el arreglo) del arreglo original. El
valor k es llamado incremento (Weiis, 2002).
91
Después que los primeros k subgrupos han sido ordenados, se escoge un nuevo valor de k
más pequeño, y el arreglo es de nuevo partido entre el nuevo conjunto de subgrupos. Cada
uno de los subgrupos mayores es ordenado y el proceso se repite de nuevo con un valor más
pequeño de k. Eventualmente, el valor de k llega a ser 1, de tal manera que el subgrupo
consiste de todo el arreglo ya casi ordenado.
ACTIVIDAD
92
1.13.7 Método de Ordenamiento StoogeSort
Se trata de un algoritmo recursivo que realiza una partición en tres llamadas recursivas para
llevar a cabo el ordenamiento. A continuación se muestra la implementación de este método de
ordenamiento.
Los algoritmos de búsqueda tienen como objetivo fundamental el ubicar un objeto dentro de un
conjunto de datos existentes y determinar su ubicación dentro de ese conjunto en caso de ser
una búsqueda satisfactoria. Se considera a la búsqueda como un problema de recuperación de
93
datos. Existen diferentes algoritmos que resuelven el problema de la búsqueda, cada uno de
ellos tiene precondiciones que deben ser tenidas en cuenta para su aplicación. Por ejemplo
para muchos de ellos es necesario que los elementos dentro de la estructura de datos estén
ordenados, en cambio para otros algoritmos este orden no es importante.
Estos tipos de algoritmos son de uso frecuente en contextos en los cuales se lleva a cabo
procesamiento de información. Estos pueden usarse para solucionar diferentes problemas,
entre algunos de ellos podemos citar:
La búsqueda lineal se puede realizar sobre estructuras de datos lineales (arreglos, listas,
arrays), para nuestro caso, utilizaremos un arreglo unidimensional de elementos. Pero la
búsqueda también puede ser extendida a otros tipos de datos, así como también a objetos.
94
Se busca el número 21 y se empieza por el primer elemento del arreglo, se compara el valor
que se encuentra en la posición con el valor 21.
Continua la búsqueda hacia el cuarto elemento del arreglo, el cual se encuentra en la posición
3, al momento de realizar la comparación, se tiene que el elemento buscado se encuentra
dentro del arreglo, lo que nos indicara que la búsqueda fue satisfactoria.
Esta implementación tiene que aun encontrando el elemento dentro del arreglo, se continúa
realizando las comparaciones que permite el tope superior del límite del ciclo.
Existen otras implementaciones de la búsqueda lineal que pueden ser más eficientes en
términos de tiempos de ejecución.
Una segunda solución al problema de la búsqueda, permite incorporar una mejora, la cual
consiste en una vez que se encuentre el elemento dentro del arreglo, se termine la ejecución
del método. Así se garantiza que no se sigan realizando comparaciones innecesarias dado que
se encontró el dato.
95
boolean busquedaLineal(int arreglo[], int dato)
{
for( int i = 0 ; i < arreglo.length ; i++ )
{
if ( arreglo [ i ] == dato )
{
Método
return true;
}
}
return false;
}
Existen métodos de búsqueda en los cuales es necesario que los elementos que se encuentran
dentro del arreglo, conserven un determinado orden de relación entre ellos. Para aplicar la
búsqueda lineal analizando extremos es necesario que los elementos dentro del arreglo se
encuentren ordenados ascendentemente.
Para esta primera implementación si el elemento que se encuentra en la primera posición del
arreglo es mayor que el dato a buscar, el algoritmo termina y retorna falso. Lo mismo sucede si
el último elemento dentro del arreglo es menor que el dato que se está buscando. Con estas
comparaciones se puede evitar realizar una comparación si los valores se encuentran por fuera
del rango de valores.
96
En el caso base se tienen dos condicionales que evalúan si el
número a buscar se encuentra en el rango de los valores que se
encuentran dentro del arreglo. El orden de complejidad de ambos
condicionales es O(1)
Análisis del
método de
Se tiene un ciclo for que recorre el arreglo desde la posición 0,
búsqueda
hasta la posición n-1 del arreglo, por lo tanto se tiene O(n). Lo
anterior asumiendo que el elemento se encuentra en las últimas
posiciones del arreglo, o que no se encuentre dentro de este
Orden de
O(n)
complejidad
Para este tipo de búsqueda se debe trabajar con un arreglo ordenado de elementos (única
situación en la que es posible, aplicar la búsqueda binaria). Este algoritmo es más eficiente
que el de búsqueda lineal. Inicialmente se verifica si el elemento que se está buscando se
encuentra en la mitad del arreglo, en caso de no ser este dato el que se busca, entonces se
compara con el elemento que se está buscando.
Si el elemento que se busca es mayor, se redefine el límite del arreglo desde el siguiente
elemento al del centro hasta el final del arreglo. En el caso en que el elemento que se busca
sea menor, el intervalo se toma desde la mitad del arreglo hasta el principio del arreglo.
Cada vez que se realiza una comparación dentro del ciclo y mientras en límite inferior no sea
mayor que el límite superior, el reduce el conjunto de entrada a la mitad, es decir, el arreglo
descarta la mitad de los elementos que están por fuera del rango. El ciclo se deja de ejecutar
cuando se retorna un valor ya sea falso o verdadero.
while ( true )
{
centro = (limSup + limInf) / 2;
if( limInf > limSup )
{
return false;
Método
}
else
{
if ( arreglo[ centro ] < dato )
{
limInf = centro + 1;
}
else
{
if (arreglo [centro] > dato)
{
limSup = centro - 1;
97
}
else
{
return true;
}
}
}
}
}
Cuando se realiza una comparación en la cual se busca el número,
se encuentra en caso de no ser cierto, que el conjunto de datos se
Análisis del divide a la mitad. Si el tamaño del arreglo es n, se va reduciendo
n
método de por cada comparación a n/2, n/4, n/8,…., n/2 hasta llegar a
búsqueda cualquiera de los límites del arreglo, por lo anterior se puede
deducir que el algoritmo de búsqueda lineal tiene un orden de
complejidad de orden O(log(n)).
Orden de
complejidad
O(log(n)).
ACTIVIDAD
Se desea crear una Aplicación que permita a un docente llevar un registro completo de las
notas de sus estudiantes. Se requiere que se pueda agregar un estudiante. Un estudiante tiene
asociado un nombre, un apellido y dos notas parciales. Se debe permitir generar el listado de
98
estudiantes ordenado por nota 1, nota 2 o promedio. De igual forma se debe informar la nota
definitiva que más se repite (moda) y será posible localizar el primer estudiante que posea la
nota definitiva ingresada por el usuario.
NOMBRE DESCRIPCIÓN
calcularDefinitiva()
IDENTIFICACIÓN DE MÉTODOS
NOMBRE DESCRIPCIÓN
agregarEstudiante(String nombre,
String apellido, double nota1,
double nota2)
ordenarPorBurbuja()
ordenarPorInsercion()
ordenarPorSeleccion()
obtenerDefinitivaModa()
busquedaSecuencial(double nota)
busquedaBinaria(double nota)
99
d. Establecer las relaciones entre las clases.
e. Implementación
calcularDefinitiva()
/*Calcula la nota definitiva usando formato decimal de dos cifras */
public void calcularDefinitiva()
{
DecimalFormat formatoDecimal;
formatoDecimal = new DecimalFormat ( "0.0" );
String dato = formatoDecimal.format ( (nota1+nota2)/2 );
dato = dato.replace(',','.');
definitiva=Double.parseDouble(dato);
}
100
for(i=0; i<miEstudiante.size(); i++)
{
array[i]=miEstudiante.get(i).getNombre()+ " "+
miEstudiante.get(i).getApellido()+ " Nota 1: "+
miEstudiante.get(i).getNota1()+" Nota 2: "+
miEstudiante.get(i).getNota2();
}
return array;
}
ordenarPorInsercion()
miEstudiante.set(j,temp);
}
ordenarPorSeleccion()
/** @return un array de String */
public String [] ordenarPorSeleccion()
{
String array[]=new String[miEstudiante.size()];
Estudiante temp;
int i,j;
for (i=0; i<miEstudiante.size()-1; i++)
{
for (j=i+1; j<miEstudiante.size();j++)
{
if ( miEstudiante.get(i).getDefinitiva()>
miEstudiante.get(j).getDefinitiva())
{
101
temp = miEstudiante.get(i);
miEstudiante.set(i,miEstudiante.get(j));
miEstudiante.set(j,temp);
}
}
}
for(i=0; i<miEstudiante.size(); i++)
{
array[i]=miEstudiante.get(i).getNombre()+ "
"+miEstudiante.get(i).getApellido()+ " Nota 1:
"+miEstudiante.get(i).getNota1()+" Nota 2:
"+miEstudiante.get(i).getNota2()+" Def:
"+miEstudiante.get(i).getDefinitiva();
}
return array;
}
obtenerDefinitivaModa()
busquedaSecuencial(double nota)
102
dato = dato.replace(',','.');
nota = Double.parseDouble(dato);
busquedaBinaria(double nota)
while(inicio<=fin)
{
medio=(inicio+fin)/2;
if(nota==miEstudiante.get(medio).getDefinitiva())
{
return (miEstudiante.get(medio));
}
else
{
if(nota>miEstudiante.get(medio).getDefinitiva())
{
inicio=medio+1;
}
else
{
fin=medio-1;
}
}
}
return null;
}
ACTIVIDAD
Una vez escritos los métodos para la clase GrupoEstudiante, determinar su orden de
complejidad.
103
Método Orden de Complejidad
O( )
encontrarPersona()
O( )
ordenarPorBurbuja()
O( )
ordenarPorInsercion()
O( )
obtenerDefinitivaModa()
O( )
busquedaSecuencial(double nota)
O( )
busquedaBinaria(double nota)
Para el caso de estudio Registro notas, agregar el método ordenar por shellSort.
Una de las opciones que tenemos disponibles partiendo del hecho de que la información no
está ordenada es apoyarnos en el patrón de recorrido parcial. Un ejemplo de este tipo de
patrón es el siguiente:
104
Observe que el ciclo se rompe cuando encuentra una persona que posee un salario superior a
430000. Intente construir ahora el algoritmo que permita encontrar la información del primer
estudiante que posea una determinada nota definitiva.
105
106
2 ESTRATEGIAS DE PROGRAMACIÓN
2.1 Introducción
Para seguir aplicando técnicas correctas, se muestra a continuación técnicas conocidas para el
diseño de algoritmos. Los temas que se trabajarán en este capitulo son las técnicas divide y
vencerás, los algoritmos voraces y la programación dinámica. En cada uno de ellos se
realizarán aplicaciones de problemas clásicos.
boolean binRecursiva (int arreglo[], int dato, int limInf, int limSup)
{
int centro = (int)( (limSup + limInf) / 2);
if ( limInf > limSup )
{
return false;
}
else
{
if ( arreglo [ centro ] > dato )
{
return binRecursiva(arreglo, dato, limInf, centro-1 );
}
107
else
{
if (arreglo [centro]<dato)
{
return binRecursiva(arreglo,dato,centro+1,limSup );
}
else
{
return true;
}
}
}
}
Este método se basa en la técnica de “Divide y Vencerás” ya que toma el arreglo original de
datos y lo divide en dos partes del mismo tamaño, lo sigue dividiendo hasta que sólo se tenga
un elemento. Cada una de estas divisiones es ordenada de manera separada y posteriormente
fusionadas para formar el conjunto original ya ordenado. Este algoritmo divide inicialmente
la lista hasta su mínimo valor y luego si ordena el arreglo.
108
for( int i = 0 ; i < len ; i++ )
{
if( m2 <= alto - bajo )
{
if( m1 <= pivote - bajo )
{
if( temp[ m1 ]>temp[ m2 ] )
{
a[i+bajo]=temp[ m2++ ];
}
else
{
a[i+bajo]=temp[ m1++ ];
}
}
else
{
a[i+bajo] = temp[ m2++ ];
}
}
else
{
a[i+bajo] = temp[ m1++ ];
}
}
}
}
Se tienen las siguientes expresiones de base y de inducción.
T(1) = a
T(n) = 2T(n/2) + O(n)
ACTIVIDAD
109
Arreglo a Ordenar
5 1 4 7 1 3 2 6
La siguiente tabla, muestra los rangos de valores y la cantidad de bits que cada una de estas
variables necesita para su ejecución. Se observa que cuando se utiliza una variable de tipo int
son necesarios 32 bits.
Mientras que si se utiliza una variable de tipo double, son necesarios 64 bits.
Con base en los anteriores valores es posible en determinadas situaciones que el resultado de
una multiplicación de un número entero muy grande sobrepase la capacidad de un tipo de dato,
lo que puede llevar a resultados incorrectos. Cuando esta situación sucede es posible apoyarse
en estructuras de datos dinámicas que ayudan a que una operación de multiplicación se realice
correctamente, esto, utilizando la técnica de divide y vencerás.
110
cada una de ellas por el multiplicando. Existe para ello las formas tradicionales de
multiplicaciones conocidas como la americana y la iglesia.
2
La complejidad computacional para el algoritmo de multiplicación clásica es de O(n ), siendo n
el número de dígitos del numero mayor. Si ambos números tienen dos número enteros n muy
2 2
grandes, el número de multiplicaciones es n lo que nos da el orden de complejidad O(n ).
for(int y=0;y<=tam1+tam2-1;y++)
{
res[y]=0;
}
for( int i=tam1-1;i>=0;i--)
{
for( int j=tam2-1;j>=0;j--)
{
res[l]+=Num[i]*Num2[j];
Método
if(res[l]>9)
{
res[l-1]+=res[l]/10;
res[l]=res[l]%10;
}
l--;
}
l=pos;
pos--;
l--;
}
return res;
}
2
Análisis del El orden de complejidad de este método de multiplicación es O(n ),
método de esto teniendo en cuenta que los ciclos anidados iteran
ordenamiento completamente desde una posición inicial hasta una posición final.
La forma en la cual se lleva a cabo esta multiplicación, sugiere varias metodologías. La más
común de ellas es crear dos columnas en la cual se escriben los dos números a multiplicar. Se
debe repetir la siguiente secuencia hasta que el número de la columna izquierda sea un 1. Por
lo tanto la secuencia es la siguiente:
1. Dividir el número de la columna izquierda por dos, esta división es una división entera.
2. Una vez realizado el paso 1, duplicar el número de la columna derecha, sumándolo
consigo mismo.
3. Eliminar todas las filas en las cuales el número de la izquierda sea par.
4. Sumar los números que quedan en la columna de la derecha.
111
Numero 1 Numero 2 Suma
221 1194 1194
110 2388
55 4776 4776
27 9552 9552
13 19104 19104
6 38208
3 76416 76416
1 152832 152832
263874
La ventaja de este algoritmo es que solo se realizan operaciones de suma y de división por 2. A
continuación, se muestra una implementación de este método para multiplicar dos números.
return resultado;
}
Análisis del Este método se repite n, pero solo se realiza en los casos en los
método de cuales el multiplicador es un número impar. El orden de
2
ordenamiento complejidad para este caso es de O(n ).
Según (Ramirez, 2003) este algoritmo cuando se aplica la técnica de divide y vencerás,
consiste de dividir tanto multiplicando como multiplicador en dos mitades, ambas con el mismo
número de cifras y además que este número de cifras sea potencia de dos (si no los tienen, se
rellenan con ceros a la izquierda). El objetivo final de este algoritmo es efectuar a los más 4
multiplicaciones, siguiendo los pasos que a continuación se listan:
1. Multiplique la mitad izquierda del multiplicando (w), por la mitad izquierda del
multiplicador (y), el resultado desplácelo a la izquierda, tantas cifras tenga el
multiplicador.
2. Multiplique la mitad izquierda del multiplicando (w), por la mitad derecha del
multiplicador (z), el resultado desplácelo a la izquierda, la mitad de las cifras que tenga
el multiplicador.
3. Multiplique la mitad derecha del multiplicando (x), por la mitad izquierda del
multiplicador (y), el resultado desplácelo a la izquierda, la mitad de las cifras que tenga
el multiplicador.
4. Multiplique la mitad derecha del multiplicando (x), por la mitad derecha del multiplicador
(z), el resultado no lo desplace.
112
public int[] dv1(int vec1[],int vec2[],int n)
{
int x[],y[],z[],w[],s[],t[],u[],res[],res2[],res3[],re[],auxr[],
. auxs[], auxt[],auxu[]; int[] r;
for(int i=0;i<n/2;i++)
{
w[i]=vec1[i];
y[i]=vec2[i];
x[i]=vec1[i+(n/2)];
z[i]=vec2[i+(n/2)];
}
//r guarda la multiplicacion de los vectores "w" y "y"
r=new int[2*n];
auxr=new int[n];
for(int i=0;i<n;i++)
{
s[i]=auxs[i];
}
//t guarda la multiplicacion de los vectores "x" y "y"
t=new int[(n/2)+n];
auxt=new int[n];
iniceros(t,(n/2)+n);
auxt=dv1(x,y,n/2);
113
for(int i=0;i<n;i++)
{
t[i]=auxt[i];
}
res=new int[2*n];
iniceros(res,2*n);
res=suma(r,2*n,s,n+(n/2));//res guarda la suma de "r" y "s"
res2=new int[(n/2)+n];
iniceros(res2,(n/2)+n);
res2=suma(t,n+(n/2),u,n); //res2 guarda la suma de "t" y "u"
res3=new int[2*n];
iniceros(res3,2*n);
Los algoritmos devoradores son algoritmos que toman decisiones, partiendo de los datos que
tienen disponibles, ignorando las consecuencias que tales decisiones puedan tener. Esta
característica los hace sencillos de diseñar e implementar. Poseen los siguientes elementos
característicos:
Conjunto de candidatos: Son los posibles elementos que pueden ser considerados.
Algunos de ellos serán seleccionados mientras que otros rechazados.
Función de solución: Verifica si con los elementos seleccionados ya se ha completado
la solución
Función de factibilidad: verifica si hay posibilidad de completar la solución apoyándose
en los elementos seleccionados.
Función de selección: permite elegir el candidato más prometedor. Generalmente
existe una relación directa con la función objetivo relacionada con la función objetivo.
Por ejemplo si nuestra intención es minimizar costos elegiremos el candidato más
económico.
Función objetivo: Entrega la solución del problema
Los Algoritmos Voraces, también conocidos como algoritmos glotones, ávidos o algoritmos
greedy, se emplean en problemas de optimización en los cuales se pretende maximizar o
minimizar algún valor. La codificación de un algoritmo voraz se caracteriza por tener un bucle,
denominado bucle voraz.
114
Algunos algoritmos voraces se utilizan en problemas tales como:
Una empresa transportadora requiere de una Aplicación que permita llenar un camión con
objetos sin exceder el peso máximo soportado por éste, logrando una maximización de valor.
Cada objeto tiene un peso y un valor asociado. Es importante anotar, que los objetos pueden
fraccionarse. La aplicación debe permitir:
115
c. Implementación de las clases
import java.util.ArrayList;
import java.util.Collections;
116
public ArrayList<Objeto> getMisObjetos()
{
return misObjetos;
}
if(i<misObjetos.size()-1&& (peso +
misObjetos.get(i).getPeso() <= pesoMaximo))
{
misObjetos.get(i).setCantidad(1.0);
peso= peso + misObjetos.get(i).getPeso();
}
else
{
misObjetos.get(i).setCantidad((pesoMaximo-peso)/
misObjetos.get(i).getPeso());
peso=pesoMaximo;
}
}
}
117
public double obtenerValor()
{
double valor=0;
for(Objeto miObjeto:misObjetos)
{
valor+=(miObjeto.getValor()*miObjeto.getCantidad());
}
return valor;
}
118
ACTIVIDAD
Si se tienen los siguientes objetos y se desea llenar el camión de peso máximo 520 indique
cuáles y cuántos elementos fueron seleccionados y el valor de la carga obtenido gracias a ésta
selección.
w 20 30 15 40 80
v 15 50 20 55 92
Se pudo observar que la estrategia de selección utilizada en la clase Camion fue minimizar wi.
Ahora se debe, resolver el punto anterior partiendo del hecho de que se desea maximizar v i.
Repita este procedimiento maximizando vi/wi.. Indique cuál de las tres funciones de selección te
parece más apropiada.
119
public void realizarCambio()
{
Según (Hernández, 2004), los siguientes corresponden a los elementos de los algoritmos
voraces:
Los candidatos: Tenemos que resolver algún problema de forma óptima. Para construir
la solución de nuestro problema, disponemos de un conjunto o una lista de candidatos:
las monedas disponibles.
A medida que avanza el algoritmo, vamos acumulando dos conjuntos. Uno contiene
candidatos que ya han sido considerados y seleccionados, mientras que el otro
contiene candidatos que ya han sido considerados y rechazados.
Hay una segunda función que comprueba si incierto conjunto de candidatos es factible,
esto es, si es posible o no completar el conjunto añadiendo otros candidatos para
obtener al menos una solución de nuestro problema. Una vez mas no nos preocupa
aquí si esto es optimo o no.
Normalmente, se espera que el problema tenga al menos una solución que sea posible
obtener empleando candidatos del conjunto que estaba disponible inicialmente.
Hay otra función más, la función de selección, que indica en cualquier momento cual es
el más prometedor de los candidatos restantes, que no han sido seleccionados ni
rechazados.
Por último, existe una función objetivo que da el valor de la solución que hemos
hallado: el número de monedas que es utilizado para dar la vuelta, la longitud de la ruta
que hemos construido.
En todo algoritmo voraz existen las cuatro funciones mencionadas anteriormente, aunque
frecuentemente no están explicitas en la codificación del algoritmo.
120
ACTIVIDAD
Dada una colección de elementos enteros positivos de tamaño n, los cuales se almacenarán en
un arreglo c. ¿Cuál es el conjunto solución al aplicar el siguiente algoritmo voraz?
Los algoritmos voraces son usados espontáneamente por las personas en distintas ocasiones,
por ejemplo, existe un algoritmo voraz que las personas usan para conformar una devuelta. A
continuación se muestra el conocido caso del tendero.
Supongamos que un tendero tiene que devolver $ 700 y para ello tiene las siguientes 10
monedas.
121
Se acepta esa moneda, porque sirve para conformar los $100 restantes.
El algoritmo termina, porque las monedas seleccionadas y aceptadas suman $700 que
era lo que había que devolver.
ACTIVIDAD
Dado el siguiente método, explicar el funcionamiento del siguiente algoritmo voraz. El método
da el cambio de n unidades utilizando el menor número posible de monedas.
122
Según las características de los algoritmos voraces, es posible que el tendero analice varias
para dar la devuelta?
Un tendero requiere de una Aplicación que le permita dar a sus clientes las vueltas de tal
forma que ellos reciban la menor cantidad de monedas. Se debe permitir:
a) Requerimientos funcionales
NOMBRE R1 –
RESUMEN
ENTRADAS
RESULTADOS
NOMBRE R2 –
RESUMEN
ENTRADAS
RESULTADOS
123
NOMBRE R3 –
RESUMEN
ENTRADAS
RESULTADOS
NOMBRE DESCRIPCIÓN
Devuelta(double cambio)
void
fijarDenominaciones(ArrayList<
Integer> misMonedas)
double realizarCambio()
double getCambio()
String[] listar() throws
Exception
import java.util.ArrayList;
import java.util.Collections;
import javax.swing.JOptionPane;
124
public double realizarCambio()
{
return suma;
}
if(misMonedas.size()==0)
{
throw new Exception("Debe ingresar denominaciones
antes de poder realizar cambios");
}
else
{
double salida=realizarCambio();
String info[]=new String[misMonedas.size()+2];
125
ACTIVIDAD
Conjunto de candidatos:
Función de solución:
Función de factibilidad:
Función de selección:
Función objetivo:
Pruebe el método que construyó con las siguientes denominaciones: 580, 500, 200, 100, 25,
10, 5. Los valores a cambiar son: a) 1050 b) 610 c) 602
Gracias a esta estrategia es posible mejorar la eficiencia del cálculo puesto que logra que cada
subproblema se resuelva sola vez (y su resultado se almacene en una tabla), logrando de ésta
forma evitar tener que repetir cálculos cuando el subproblema aparezca nuevamente.
Según (Guerequeta & Vallecillo, 2000), “la Programación Dinámica no sólo tiene sentido
aplicarla por razones de eficiencia, sino porque además presenta un método capaz de resolver
de manera eficiente problemas cuya solución ha sido abordada por otras técnicas y ha
fracasado. Donde tiene mayor aplicación la Programación Dinámica es en la resolución de
problemas de optimización. En este tipo de problemas se pueden presentar distintas
soluciones, cada una con un valor, y lo que se desea es encontrar la solución de valor óptimo
(máximo o mínimo).
126
a) Cada pareja de conejos fértiles tiene una pareja de conejitos cada mes.
b) Cada pareja de conejos comienza a ser fértil a partir del segundo mes de vida
c) Ninguna pareja de conejos muere ni deja de reproducirse (Bohóquez, 2006).
La aplicación debe mostrar cuantos conejos hay al término del mes n, para que el granjero
pueda tomar decisiones en torno a si debe comprar más conejeras o empezar a vender
parejas.
La siguiente tabla muestra la cantidad conejos que debe haber al cabo de 6 meses
Mes 0 1 2 3 4 5
Cantidad de conejos 1 1 2 3 5 8
Observe que se inicia con una pareja de conejos (mes 0). Esta pareja (la cual llamaremos A) al
inicio del mes dos se reproduce con lo cual quedaría la pareja de padres y la pareja de hijos (la
cual llamaremos B). Al tercer mes la pareja B aun no es fértil pues solo tiene un mes de vida,
pero sus padres continuación siendo fértiles dando lugar a una nueva pareja (pareja C). Es
decir en el mes tres ya se tendrían 3 parejas. En el mes cuatro, la pareja A tiene un nuevo hijo
(D), al igual que la pareja B, mientras que la pareja C aun no es fértil pues solo tiene un mes de
vida, con lo cual quedaría cinco parejas.
Es posible mejorar el orden de complejidad para este algoritmo a un orden O(n), esto se puede
conseguir utilizando un arreglo que almacene los valores almacenados para posteriormente
reutilizarlos.
127
El método iterativo que calcula la sucesión de Fibonacci utilizando programación dinámica es el
siguiente:
ACTIVIDAD
¿Argumente cuál es más eficiente de la versión recursiva o la versión iterativa? Halle el T(n)
para cada una de estas versiones.
128
Termine el árbol que muestra gráficamente el comportamiento del algoritmo recursivo.
ACTIVIDAD
129
Compare las anteriores implementaciones y de acuerdo a su funcionamiento y orden de
complejidad determine de cuál es el más eficiente y cuál el menos eficiente.
Se va a retomar el problema del llenado del camión pero haciendo uso de programación
dinámica. Se debe permitir fijar el peso máximo, agregar un nuevo objeto, y arrojar el valor
máximo que puede llevar el camión.
130
public int realizarCambio()
{
String salida="";
int g[][]=new int[misObjetos.size()][(int)pesoMaximo+2];
int j,i;
}
for(j=0; j<misObjetos.size(); j++)
{
g[j][0]=0;
}
for( i=1; i<misObjetos.size();i++)
{
for( j=1; j<=(int)pesoMaximo; j++)
{
if(j<misObjetos.get(i).getPeso())
{
g[i][j]=g[i-1][j];
}
else
if(g[i-1][j]>=g[i-1][j-(int)misObjetos.
get(i).getPeso()]+(int)misObjetos.get(i).
getValor())
{
g[i][j]=g[i-1][j];
}
else
{
g[i][j]=g[i-1][j-(int)misObjetos.
get(i).getPeso()]+(int)misObjetos.get(i).
getValor();
}
}
}
131
public String[] listar() throws Exception
{
int i=0;
if(misObjetos.size()==0)
{
throw new Exception("Debe ingresar denominaciones
antes de poder realizar cambios");
}
else
{
String info[]=new String[1];
info[i]="Valor máximo "+realizarCambio();;
return info;
}
}
ACTIVIDAD
Limite
De peso 0 1 2 3 4 5 6 7 8 9
W 1=1
v1=1
W 1=3
v1=5
W 1=6
v1=17
W 1=8
v1=20
W 1=8
v1=25
Ahora deseamos retomar el problema del cambio de moneda tratado en el capítulo anterior. Se
debe permitir agregar una nueva denominación, fijar el total a cambiar e indicar con cuantas
monedas puede ser efectuado el cambio.
132
//Las denominaciones están ordenadas de menor a mayor
133
ACTIVIDAD
Cantidad 0 1 2 3 4 5 6 7 8 9
d1= 1
d2= 3
d3= 6
Modifique el método para que además de indicar la totalidad de monedas requeridas para
efectuar el cambio, especifique cuales y cuántas de cada una.
La idea fundamental en este algoritmo consiste en ir explorando todos los caminos más cortos
que parten del vértice origen y que llevan a todos los demás vértices; cuando se obtiene el
camino más corto desde el vértice origen, al resto de vértices que componen el grafo, el
algoritmo se detiene.
134
public void Dijkstra()
{
Sol=new boolean [n];
Def=new float [n-1];
for (int i=0;i<(n-1);i++)
{
Sol[i+1]=false;
Def[i]=L[0][i+1];
}
Sol[0]=true;
for (int i=0;i<(n-2);i++)
{
Método
Sol[pos]=true;
for (int j=0;j<(n-1);j++)
{
if (!Sol[j+1])
{
Def[j]=Math.min(Def[j],
Def[pos-1]+L[pos][j+1]);
}
}
}
}
Análisis del
método
Orden de
complejidad
Se desea crear una Aplicación que permita generar combinaciones ganadoras. La lotería debe
proporcionarnos 6 números para jugar a la lotería Primitiva. Evitando con ello evitar tener que
rellenar las columnas para conseguir un ahorro de tiempo.
Se debe permitir generar combinaciones aleatorias de seis números para la lotería Primitiva,
resetear la lotería y mostrar el total de combinaciones existentes.
135
a) Requerimientos funcionales
NOMBRE R1 –
RESUMEN
ENTRADAS
RESULTADOS
R2 –
NOMBRE
RESUMEN
ENTRADAS
RESULTADOS
NOMBRE R3 –
RESUMEN
ENTRADAS
RESULTADOS
c. Implementación de métodos
public Loteria()
{
arreglo=new int[TOTAL];
}
}
return arreglo;
}
136
public void resetear()
{
for(int i=0; i<TOTAL; i++)
{
ACTIVIDAD
Por ejemplo si se tiene C(6,2)=15, puesto que hay 15 formas de escoger 2 objetos a partir de
un conjunto con 6 elementos.
137
Por ejemplo si se calcula C(5,3) cual sería el resultado.
C(5,3) =
0 1 2 3 4 k
0
1
2
3
4
5
6
.
.
n
Con base en dicha tabla construya un algoritmo más eficiente, que haga uso de programación
dinámica.
138
Verifique si la siguiente implementación corresponde a la solución del problema de los
coeficientes binomiales.
}
}
return matriz;
}
139
140
3 ALGORITMOS APLICADOS A GRAFOS Y ARBOLES
3.1 Introducción
Los árboles son estructuras de datos dinámicas con una naturaleza diferente en cuanto a las
estructuras de datos anteriormente analizadas, esta naturaleza esta determinada directamente
con su estructura, la cual impone una jerarquía sobre una colección de objetos. Un árbol es
una colección de elementos llamados nodos, uno de los cuales se distingue como raíz. Sobre
ellos se encuentra definida una relación de “paternidad” que define la estructura jerárquica a la
que nos referimos anteriormente. Los nodos de un árbol contienen dos o más enlaces. El nodo
raíz es el primer nodo de un árbol. Cada enlace del nodo raíz hace referencia a un hijo. el hijo
izquierdo es el primer nodo del subárbol izquierdo, y el hijo derecho es el primer hijo del
subárbol derecho.
Este capítulo se basará fundamentalmente es los árboles binarios. Este tipo de árbol se
caracteriza por que tiene un nodo llamado padre y el cual puede tener cero, uno o dos hijos
como máximo. A su vez cada hijo puede verse como otro árbol.
Una definición recursiva de árbol es que un árbol puede verse como una estructura formada
por un nodo (la raíz de dicho árbol) y una lista de árboles. Este nodo (raíz) es el padre de las
raíces de los árboles que componen la lista, a partir de lo cual, se establece la relación de
paternidad entre ellos. La siguiente figura muestra una representación de un árbol binario.
Raiz
nodo nodo
o
nodo
A continuación se exponen las definiciones más relevantes asociadas a los árboles binarios.
141
nodo
Nodo de un árbol
Raíz: En un árbol se identifica el nodo raíz como el nodo principal en la jerarquía, así
como también se pueden distinguir la raíz otros subárboles. Por ejemplo en la siguiente
figura, la raíz principal es el nodo cuya información es 20. Podemos distinguir también
el árbol 60 – 90, cuya raíz es el nodo con información 60. El árbol de la derecha tiene
como raíz el nodo cuya información es 90.
20
10 60
8 90
Padre: Es un nodo que puede o no tener hijos. El nodo con información 34, tiene 2
hijos, el hijo izquierdo, cuya que contiene el valor 1, y el hijo derecho, cuya información
es 44.
34
1 44
2 99
Hoja: Una hoja es un nodo que no tiene hijo derecho ni izquierdo. Por ejemplo, en la
siguiente figura, el nodo 2 es una hoja mientras que el nodo con información 1 no se
puede considerar como hoja por que tiene ramificación por la izquierda.
20 30
20 40
142
Nivel: Cada nodo tiene un nivel dentro de un árbol binario. El nodo raíz tiene un nivel 0
y los demás nodos tienen el nivel de su padre mas 1. Por esto los nodos que son hijos
del nodo raíz tienen nivel 1. Según la figura: El nodo 34 tiene nivel 0, los nodos 2 y 44
tienen nivel 1 y los nodos 2 y 98, tienen nivel 2.
10
60 90
34 8
44 99 1
Niveles de un árbol
Altura: La altura de un árbol binario es el nivel de la hoja o de las hojas que están más
distantes de la raíz. Basándonos en la siguiente figura, la altura del árbol cuya raíz es
A, corresponde a la longitud del camino para llegar a la hoja mas distante de la raíz.
En este caso será 3.
34 1
99 44
44 34 3 2
Arbol Binario Completo: Un árbol binario completo es aquel en el que todo nodo no
terminal tiene sus dos hijos. Todos los nodos no terminales tienen sus dos hijos. El
máximo numero de nodos que puede tener un árbol de nivel n es: 20 + 21 + 22 + 23...
+ 2n . Si n es 3 entonces : 20 + 21 + 22 + 23 = 15
Los árboles binarios de expresión, tienen aplicaciones en diferentes contextos, entre las más
aplicadas en el contexto de las ciencias de la computación se encuentran la representación de
expresiones proposicionales y expresiones aritméticas con las cuales se pueden realizar
diferentes cálculos matemáticos.
143
Inicialmente se muestra su aplicación en el contexto de expresiones proposicionales. Por
ejemplo si se tiene la expresión: (p v ¬q) (¬q ^ r), el árbol de representación será el
siguiente:
En este caso el operador principal de la expresión es la implicación y por ello se ubica como el
padre del árbol, este árbol tiene dos subexpresiones que se pueden denominar hijo izquierdo e
hijo derecho, cada uno de los cuales se compone de una expresión.
En este caso el operador principal de la expresión es él bicondicional y por ello se ubica como
el padre del árbol, este árbol tiene dos subexpresiones que se pueden denominar hijo izquierdo
e hijo derecho, cada uno de los cuales se compone de una expresión. El hijo izquierdo tiene a
su vez una expresión compuesta de hijo izquierdo e hijo derecho, cuyo operador principal es la
conjunción. El hijo derecho tiene como operador principal es la disyunción.
ACTIVIDAD
p q r ^ q ¬ q ¬ p
q ¬ (p ^¬p) r v s p
(p q) ^ (p ¬q) ^ p ^ s ¬ q
(p ^ r) (¬p ^ q) ¬q
144
Con el objetivo de evitar las ambigüedades en la interpretación de las fórmulas proposicionales,
se debe establecer un orden de precedencia para aplicar los operadores de una fórmula
proposicional.
1. Operador de negación ¬
2. Operador de conjunción ^
condicional
3. Operador de disyunción v
bicondicional
4. Operador
5. Operador
Otra aplicación muy conocida de los árboles es la de los Arboles de expresión, en la cual cada
expresión (compuesta por dos operandos y un operador) se representa a través de esta
estructura. La raíz del árbol contiene un operador, mientras que los subárboles izquierdo y
derecho son los operandos izquierdo y derecho respectivamente. En la hojas solo pueden ir
operandos. Por ejemplo la expresión ((3+5)*(6-2)) puede expresarse tal como se muestra a
continuación.
145
9
8
2 9
9
7 1 1 9
7
7
Un árbol binario es un conjunto de elementos que está vacío o dividido en tres subconjuntos. El
primer subconjunto es la raíz, los otros dos son los subárboles izquierdo y derecho. Cada nodo
puede tener cero, uno o máximo dos hijos.
Para la implementación de Árboles Binarios, se utilizará una clase Nodo de la cual se pueden
utilizar tantos objetos como sean necesarios. A continuación, se puede apreciar la
implementación esta clase en Java:
También es necesario para la implementación de Árboles Binarios crear una clase Arbol. Esta
clase es la que contiene la implementación de las principales operaciones que se pueden llevar
a cabo con los árboles, estas operaciones son fundamentalmente de creación, búsqueda,
eliminación. A continuación, se puede apreciar la implementación esta clase en Java, así como
una explicación de los métodos más utilizados en los árboles, posteriormente, se analizará el
orden de complejidad de cada uno de los métodos.
146
public class Arbol
{
public Nodo raiz;
public int cantidad;
Método Arbol()
{
raiz = null;
cantidad = 0;
}
}
Análisis del El constructor de árbol, tiene en sus implementación únicamente
Orden de operaciones elementales, por lo tanto el orden de complejidad de
Complejidad este método es O(1).
Se declara una instancia de la Clase Nodo y una variable entera que servirá para contar los
nodos que se tienen en el árbol.
Buscar en un Arbol
147
La figura muestra un caso cuando se desea buscar un elemento cuyo valor en su campo de
datos es 3. La eficiencia de las operaciones sobre los árboles binarios estas asociada en la
mayoría de casos a la mitad del conjunto de datos de entrada.
34
4 44
2 98
3 97 99
Cuando se realiza la primera comparación, se encuentra que el valor que se busca es menor al
valor de la raíz y se debe realizar una búsqueda por el subárbol izquierdo.
34
4 44
2 98
3 97 99
La búsqueda continua por la mitad del subárbol, en este caso caso el subárbol derecho en
donde se encuentra el elemento que se esta buscando.
Insertar en un Árbol
148
public Nodo insertarNodo(Comparable dato, Nodo nodo)
{
if (nodo == null)
{
return new Nodo(dato);
}
else
{
if (dato.compareTo(nodo.info) <= 0)
{
nodo.izquierdo =
Método
insertar_nodo(dato,nodo.izquierdo);
}
else
{
nodo.derecho =
insertar_nodo(dato, nodo.derecho);
}
}
return nodo;
}
Cuando se hace cada comparación dentro del while, se está ignorando
Análisis del la mitad el árbol (ya sea la izquierda o la derecha), lo que implica que
Orden de el conjunto de datos de entrada se esta reduciendo a la mitad y se
Complejidad puede afirmar que el orden de complejidad de este método es
O(log n). Esta deducción es posible hacerla, pues en el capitulo
anterior por el método de sustitución ya se demostró.
Dado que se no está haciendo balanceo del árbol, es decir, se inserta el nodo donde le
corresponda sin tener en cuenta la proporción de las ramas del árbol, todo nodo que se inserte
se convertirá en una hoja del árbol. Esto implica que como máximo el recorrido que se hace es
la altura misma del árbol y por lo tanto la relación entre la altura y el número de nodos es del
orden de O(logn).
34
4 44
2 98
3 97 99
149
34
4 44
2 98
3 97 99
Finalmente se puede insertar el nodo garantizando siempre la propiedad del árbol binario
ordenado.
34
4 44
2 98
1 3 97 99
Eliminar en un Árbol
El método eliminar permite remover un nodo del árbol, recibe como parámetro un valor que
servirá para encontrar el nodo a eliminar. Una vez se halla este nodo se procede a eliminarlo
del árbol y se realizan los correspondientes cambios en el árbol.
El aspecto mas sencillo, si el nodo que deseamos remover es un nodo hoja, Se elimina
la referencia que tiene el nodo padre del nodo. Lo anterior colocando una referencia
nula.
150
Los siguientes son aspectos que deben ser tenidos en cuenta si el nodo que se desea eliminar
posee dos subárboles, estas se detallan a continuación:
Se puede sustituir el nodo a eliminar por el mayor elemento (nodo más a la derecha) del
subárbol izquierdo del nodo a eliminar.
Es similar al caso anterior. Se puede sustituir el nodo a eliminar por el menor elemento
(nodo más a la izquierda) del subárbol derecho del nodo a eliminar.
Se puede colocar el subárbol izquierdo a la izquierda del menor elemento (nodo más a
la izquierda) del subárbol derecho del nodo a eliminar.
Es similar al caso anterior. Se puede colocar el subárbol derecho a la derecha del mayor
elemento (nodo más a la derecha) del subárbol izquierdo del nodo a eliminar.
Realiza un recorrido por todo el subárbol izquierdo de acuerdo a un nodo específico, con el
objetivo de encontrar el menor elemento del subárbol.
151
public Nodo buscarMenor(Nodo nodo)
{
if (nodo == null)
{
return null;
}
Método while(nodo.izquierdo != null)
{
nodo = nodo.izquierdo;
}
return nodo;
}
Análisis del Cuando se hace cada comparación en la línea, se está ignorando la mitad el
Orden de árbol, más exactamente todo el subárbol derecho, por lo tanto el árbol se
Complejidad reduce a la mitad y se puede afirmar que el orden de complejidad de este
método es O(logn).
88
4 89
2 90
Método recursivo que elimina el menor elemento del árbol, realiza siempre un recorrido por el
subárbol izquierdo.
152
Análisis del Cuando se hace cada comparación en la línea, se está ignorando la mitad el
Orden de árbol, ya sea el subárbol izquierdo o el derecho, por lo tanto el árbol se
Complejidad reduce a la mitad y se puede afirmar que el orden de complejidad de este
método es O(logn).
ACTIVIDAD
Existen tres tipos de recorridos que pueden realizarse a un árbol. El primero de ellos es el
preorden, el cual consiste en recorrer primero la raíz, luego el subárbol izquierdo y finalmente el
derecho. El recorrido inorden a su vez procesa primero el subárbol izquierdo, después el raíz y
a continuación el subárbol derecho. Finalmente el recorrido postorden procesa primero los
subárboles izquierdo y derecho y finalmente procesa la raíz.
“ballena”
“ratón” “tití”
“cocodrilo”
153
Inorden: ballena, cocodrilo, pantera, pez, ratón, tigre, titi
Preorden: ratón, cocodrilo, ballena, pantera, pez, tigre, tití
Postorden: ballena, pez, pantera, cocodrilo, titi, tigre, ratón
El método recursivo que realiza el recorrido en inorden para el caso de estudio tratado es:
ACTIVIDAD
imprimirInorder(Nodo n) O( )
imprimirPostorder(Nodo n) O( )
imprimirPreorder(Nodo n) O( )
154
3.3 Caso de estudio: Agenda Telefónica
Se desea crear una Aplicación que permita manejar la información de una agenda telefónica.
Cada Contacto tiene un nombre, una dirección y un teléfono. Se debe permitir:
Agregar un contacto.
Eliminar la información de toda la agenda
Listar la información en preorden
Listar la información en inorden
Listar la información en postorden
155
/** Constructor del nodo */
public Nodo(Nodo izquierda, Nodo derecha, Object dato)
{
fijarALaIzquierda(izquierda);
fijarALaDerecha( derecha);
setData( dato );
}
156
else
{
esta=false;
throw new RepetidoException("elemento ya está");
}
}
}
if (tInfoNombre.compareTo(nInfoNombre) > 0)
{
// Prueba para adicionar a la derecha
if (n.obtenerDerecha() == null)
{
// halla un espacio para colocar el nodo
n.fijarALaDerecha(temp);
}
else
{
//intenta nuevamente hacia abajo
insertarNodo( n.obtenerDerecha(), temp);
}
}
else{
// prueba a la izquierda
if (n.obtenerIzquierda() == null)
{
n.fijarALaIzquierda(temp);
}
else
{
insertarNodo( n.obtenerIzquierda(), temp);
}
}
}
157
public void postorden(Nodo nodo)
{
if (nodo != null)
{
postorden(nodo.izquierdo);
postorden(nodo.derecho);
System.out.print(nodo.info + " ");
}
}
Se desea crear una aplicación que permita almacenar los nombres de varias personas, acorde
a las reglas seguidas en un árbol binario de búsqueda.
158
a) Requerimientos funcionales
NOMBRE R1 –
RESUMEN
ENTRADAS
RESULTADOS
NOMBRE R2 –
RESUMEN
ENTRADAS
RESULTADOS
NOMBRE R3 –
RESUMEN
ENTRADAS
RESULTADOS
159
d. Implementación de los métodos
A continuación se muestra la implementación de los métodos mas importantes del árbol binario
de búsqueda.
public ArbolBinarioDeBusqueda( )
{
root = null;
cadenaIngresada= new String();
}
160
public void recorrerEnInorder(Nodo t, int yCoordenada)
{
if (t != null)
{
recorrerEnInorder(t.getIzquierdo(), yCoordenada + 1);
//Agrega 1 a la coordenada en y
t.setPosicionX ( totalNodosHorizontalEscala++);
//Coordenada en x es el número de nodos contados con inorder
t.setPosicionY ( yCoordenada);
// se fija la coordenada en y con el valor de y
recorrerEnInorder(t.getDerecho(), yCoordenada + 1);
}
}
161
/** Permite remover un elemento del árbol
* @param x el elemento que se va a eliminar */
162
/** Inserta un nodo al árbol si es posible */
public Nodo insertarElemento( Comparable x, Nodo t ) throws
ElementoDuplicadoException
{
if( t == null )
{
t = new Nodo(x,null,null);
}
else if( x.compareTo( t.getInformacion() ) < 0 )
{
t.setIzquierdo(insertarElemento(x,t.getIzquierdo() ));
}
else if( x.compareTo( t.getInformacion() ) > 0 )
{
t.setDerecho(insertarElemento( x, t.getDerecho() ));
}
else
{
throw new ElementoDuplicadoException( x.toString( )+
" ya se encuentra en el árbol" );
}
return t;
}
163
/** Elimina el menor elemento dentro del árbol */
public Nodo eliminarElMinimo( Nodo t )throws
ElementoNoEncontradoException
{
if( t == null )
{
throw new ElementoNoEncontradoException("No Hallado");
}
else if( t.getIzquierdo() != null )
{
t.setIzquierdo (eliminarElMinimo( t.getIzquierdo ()));
return t;
}
else{
return t.getDerecho();
}
}
164
// Metodos set y get
public Nodo getRoot()
{
return root;
}
hallarAlturaDelArbol(Nodo t) O( )
obtenerMaximo(int a, int b) O( )
calcularPosicionesDeNodos() O( )
eliminarElMinimo( Nodo t ) O( )
hallarElMinimo( Nodo t ) O( )
hallarElMaximo( Nodo t ) O( )
165
Escriba la versión iterativa para el método hallarElMaximo( Nodo t )
3.5 Grafos
Las estructuras de datos no lineales se caracterizan por no existir una relación de adyacencia,
entre sus elementos, es decir, un elemento puede estar relacionado con cero, uno o más
elementos.
Entre las múltiples aplicaciones que tienen estas estructuras podemos mencionar (Besembel,
2006) :
Los grafos son estructuras que se utilizan para modelar diversas situaciones tales
como: sistemas de aeropuertos, flujo de tráfico.
Los grafos también son utilizados para realizar planificaciones de actividades, tareas
del computador, planificar operaciones en lenguaje de máquinas para minimizar tiempo
de ejecución.
Los grafos pueden ser utilizados como la estructura básica para múltiples aplicaciones
en el área de la Computación.
Existe una gran cantidad de problemas que se puede formular en términos de grafos. Para la
solución de estos es posible que sea necesario examinar todos los nodos o todas las aristas
del grafo. En algunos casos se imponen el orden en que deben ser examinados (recorridos)
estos nodos.
Con base en lo anterior, un grafo es un conjunto compuesto por nodos y los cuales están
conectados por aristas. Notacionalmente hablando si las aristas no tienen dirección
especificada mediante una flecha terminal, se afirma que es un grafo no dirigido. En caso
contrario se trata de un grafo dirigido.
166
Los elementos de E se llaman aristas, o aristas dirigidas, o arcos. Para la arista dirigida
en E, v es su cola y w es su cabeza; se representa en los diagramas con la flecha
.
En los casos para los que los grafos representan a nivel computacional la situación real,
comúnmente se tiene en cuenta otro tipo de variable dada por el peso de cada uno de los
arcos, este pero puede representar costo, distancia, tiempo u cualquier otra medida de
conexión entre los vértices.
167
Asociados a los grafos se adiciona una terminología frecuentemente utilizada, a continuación
se definen los más comunes.
Ciclo: Es un camino en el que el primer nodo es igual al último, este ciclo será simple
si el camino también lo es, para grafos dirigidos. En grafos no dirigidos es necesario
que los arcos sean distintos.
Entre los algoritmos mas comunes de recubrimiento mínimo se encuentra el algoritmo de Prim
y el de Kruskal. A continuación se analiza cada uno de ellos.
168
while ( minimo < ( nodos - 1 ) )
{
int i = 1;
while (i < nodos)
{
if ( graf[nodoact][i] <
graf[nodoact][menor]
&& !estaEn( Solucion, i ) )
{
menor = i;
}
i++;
}
minimo++;
nodoact = menor;
Solucion[minimo] = nodoact;
}
return Solucion;
}
Según (Colmenares, 2006), sea G=(V,A) un grafo ponderado y conexo; el conjunto de aristas
de candidatas es A. Sea G=(V,T) ; donde T es un conjunto de aristas que comienza vacío.
Luego se selecciona en cada etapa la arista de menor peso que todavía no haya sido
inspeccionada (seleccionada o rechazada) independientemente de donde se encuentra dicha
arista. Inicialmente el conjunto de aristas T está vacío y a medida que progresa el algoritmo se
van añadiendo aristas a T. Mientras no se haya encontrado la solución, el grafo parcial que
contiene los nodos de G y las aristas de T consta de varias componentes conexas. Si una
arista une a dos vértices de componentes conexas distintas, entonces la añadimos a T, y ahora
forman una sola componente conexa. En caso contrario se rechaza la arista. El algoritmo se
detiene cuando queda una sola componente conexa.
169
ACTIVIDAD
Implemente algoritmo de Kruskal de forma tal que pueda encontrar en camino mínimo en un
grafo.
Método
Análisis del
Orden de
complejidad
Se debe permitir:
170
a. Identificar las entidades o clases.
b. Diagrama de clase
c. Implementación
import java.util.Stack;
171
/*Método principal del algoritmo de Dijkstra */
public void aplicarDijkstra(int origen)
{
int cont,posicion;
int i;
distancia[origen]=0;
for(cont=primerNodo; cont<=ultimoVertice; cont++)
{
if(cont!=origen) distancia[cont]=INF;
arregloAuxiliar[cont]=false;
predecesor[cont]=noExiste;
}
if(distancia[posicion]==INF)
{
continue;
}
172
public String devolverRutaMasCorta(int origen, int llegada)
{
String salida="";
assert(origen!=noExiste && llegada!=noExiste);
aplicarDijkstra(origen);
miStack.push(origen);
while(!miStack.empty())
{
salida+=misNombres[miStack.pop()]+" -> ";
}
salida=salida.substring(0,salida.length()-3);
return salida;
ACTIVIDAD
173
3.7 Arboles n-arios
Un árbol n-ário es un árbol cuyos nodos tienen n o menos hijos. Es de anotar que n puede ser
cualquier valor entero positivo. En n-ário no es importante el orden, a diferencia de lo que
ocurre en el árbol binario de búsqueda.
“tití”
“tigre”
“titi”
Este tipo de árboles puede ser recorrido mediante preorden, logrando de esta forma que cada
nodo aparezca una sola vez al realizar el listado de los elementos del árbol. En este tipo de
árbol se requiere que se indique al momento de realizar una inserción, el sitio en el cual deberá
ser agregado.
Las definiciones expuestas para árboles binarios pueden perfectamente ajustarse a este tipo
de árboles.
Para iniciar la construcción de nuestra aplicación empezaremos con una porción de la clase
ArbolNArio. A continuación debe analizar el método recorrer y escribir una interpretación en
torno a la labor que realiza
174
public void asignar(String cedula, String nombre, String
direccion, String puesto)
{
asignado=false;
if((((Puesto)(raiz.getInformacion())).getNombre()).
equals(puesto))
{
raiz.asignarPersona(cedula, nombre, direccion, puesto);
if(raiz.isAsignado()==true)
{
asignado=true;
}
}
else
{
ArrayList <Nodo<T>> misNodos= new ArrayList<Nodo<T>>();
recorrerParaAsignar(raiz, misNodos,cedula, nombre,
direccion, puesto);
}
if(asignado==true)
{
asignado=false;
}
}
175
boolean asignado=false;
176
public String toString()
{
StringBuilder concatenador = new StringBuilder();
concatenador.append("{").append(((Puesto)informacion).
getMiPersona().getNombre().toString()).append(",[");
int i = 0;
if(hijos==null)
{
hijos=new ArrayList<Nodo<Tipo>>();
}
for (Nodo<Tipo> miNodo : hijos)
{
if (i > 0)
{
concatenador.append(",");
}
concatenador.append(((Puesto)miNodo.getInformacion()).
getMiPersona().getNombre().toString());
i++;
}
concatenador.append("]").append("}");
return concatenador.toString();
}
}
}
Se desea crear una aplicación que permita asignar personas de acuerdo al cargo que ocuparán
dentro del organigrama de una empresa.
La aplicación debe permitir crear un el organigrama por defecto (para ello se deben agregar los
cargos especificados en el gráfico anterior), asignar un puesto (un puesto tiene un nombre y
una persona asignada), consultar que persona labora en un puesto determinado y mostrar el
recorrido en preorden del organigrama. Es de anotar que una persona tiene una cedula, un
nombre y una dirección asociada.
177
Se puede ver que el organigrama que debemos trabajar no corresponde a un árbol binario, por
el contrario estamos trabajando un árbol n-ario.
La solución de este caso de estudio requiere de la comprensión del concepto de árbol AVL. Se
le denominó AVL por los nombres de sus creadores Adelson-Velskii y Landis. Es un árbol que
además de ser binario de búsqueda cumple con la condición de que para cada uno sus nodos,
la diferencia entre las alturas de sus subárboles es como máximo 1.
Gracias al equilibrio logrado es posible asegurarnos de que la profundidad del árbol sea
O(log(n)), logrando de esta forma que el tiempo de ejecución de las operaciones que se
ejecutan sobre dichos árboles sea como máximo (log(n)) en el peor caso, siendo n la cantidad
de elementos. No obstante, es de resaltar que mantener esta propiedad de equilibrio agregue
una dificultad adicional a la hora de insertar y eliminar datos.
Es importante que tengas claro que el factor de equilibrio (FE) es igual a restar a la altura del
subárbol derecho la altura del subárbol izquierdo, en caso de que de un valor diferente de 1, -1
ó 0 podemos concluir que no es AVL
Los árboles AVL son fundamentalmente arboles de búsqueda con una condición particular de
equilibrio entre sus subárboles. Esa condición se fundamenta en que la altura de cada uno de
sus subárboles (izquierdo y derecho) no pueden diferenciarse en a los sumo una unidad.
Lo condición de equilibrio entre los subárboles esta dado por un rango comprendido entre -1, 0
y 1. Si en determinado momento este rango no corresponde, se debe aplicar un procedimiento
de rotación que se muestra posteriormente en este libro.
178
El siguiente corresponde a un árbol AVL, ya que la condición de equilibrio se encuentra entre -
1, 0 y 1.
El siguiente árbol muestra que no es un árbol AVL, el motivo esta sustentado en que la
condición de equilibrio no se encuentra entre -1, 0 y 1. La diferencia entre la altura de los
subárboles derecho e izquierdo en de 2 unidades.
A continuación se muestran las operaciones fundamentales que se pueden realizar con los
arboles AVL.
1) Se inserta un nodo en el subárbol izquierdo del hijo izquierdo de Y, siendo Y el nodo que
ha perdido el equilibrio
2) Se inserta un nodo en el subárbol derecho del hijo izquierdo de Y
3) Se inserta un nodo en el subárbol izquierdo del hijo derecho de Y
4) Se inserta un nodo en el subárbol derecho del hijo derecho de Y.
Para resolver los casos 1 y 4 se utiliza una rotación simple, mientras que para los casos 2 y 3
(que son los más complejos, puesto que la inserción se realiza en el interior del árbol) se
resuelven mediante rotaciones dobles.
179
Caso 1: Para este caso se usa una rotación simple a la izquierda (RSI). Recuerda que el
problema surgió porque se insertó un nodo en el subárbol izquierdo del hijo izquierdo de Y,
siendo Y el nodo que ha perdido el equilibrio.
Para nuestro ejemplo particular: y= 13, puesto que fue quien perdió el equilibrio, x=12, A=10.
Entonces el árbol quedaría con la siguiente estructura luego de la rotación.
180
ACTIVIDAD
7 18
17 20
53 9
3 6
Caso 4: para este caso particular se usa una rotación simple a la derecha (RSD). Recuerda
que el problema surgió porque se insertó un nodo en el subárbol derecho del hijo derecho de Y.
A continuación se presenta un árbol en el cual se aplica este tipo de rotación.
181
ACTIVIDAD
Plantee un árbol y una posible inserción sobre éste que implique la pérdida de equilibrio y para
su solución requiera una rotación simple a la derecha. Ilustre el proceso seguido.
Caso 2: Para este caso particular se usa una rotación doble izquierda –derecha. Recuerde que
el problema surgió porque se insertó un nodo en el subárbol derecho del hijo izquierdo de Y.
182
Para este caso particular, se muestra a continuación el árbol después la inserción y
posteriormente la rotación.
Caso 3: Para este caso particular se usa una rotación doble. Recuerde que el problema surgió
porque se insertó un nodo en el subárbol izquierdo del hijo derecho de Y.
A continuación se muestra la secuencia desde el árbol original hasta el árbol con el nodo
insertado.
183
Se observa que en este caso se ha perdido el equilibrio en el elemento 15. Por lo tanto: y=15,
z=20 y x=19. Con lo cual nuestro árbol resultante sería:
ACTIVIDAD
Dada la secuencia de valores enteros 18, 10, 31, 5, 22, 12, 3, 37, 24, 11, 6, 2, representa
gráficamente el árbol AVL que surge e indica los momentos en los cuales debiste efectuar la
rotación.
Identificar cuáles de los siguientes árboles binarios de búsqueda son AVL. Los que no lo sea
deben marcarse e indicar todos los nodos que violen el equilibrio.
Se desea crear una aplicación que permita manejar el registro de notas de un grupo de 10
estudiantes. Cada estudiante tiene un código y dos notas parciales. Se debe permitir:
184
Es de anotar que el objetivo de este caso de estudio es introducir el concepto de árbol AVL.
Luego de haber entendido los conceptos referentes a árboles AVL, podemos proceder a la
construcción de la aplicación
185
/**Devuelve nota definitiva*/
public double getNotaDefinitiva()
{
return notaDefinitiva;
}
186
public int getAltura()
{
return altura;
}
}
public ArbolAvl( )
{
salida="";
raiz = null;
contador=0;
i=0;
}
187
else
{
miNodo = rotarDoubleHijosIzquierda( miNodo );
}
}
}
else if( resultadoComparacion > 0 )
{
miNodo.setDerecho(insertar(nuevo, miNodo.getDerecho ()));
if( darAltura( miNodo.getDerecho() ) - darAltura( miNodo.
getIzquierdo ()) == 2 )
{
if(nuevo.compareTo(miNodo.getDerecho().
getInformacion())>0)
{
miNodo = rotarConHijosALaDerecha( miNodo );
}
else
{
miNodo = rotarDobleHijosDerecha( miNodo );
}
}
}
}
else
{ //Se descartan los duplicados
throw new Exception("El elemento está duplicado: "+nuevo.
getCodigoEstudiante());
}
miNodo.setAltura (Math.max( darAltura( miNodo.getIzquierdo() ),
darAltura( miNodo.getDerecho() ) ) + 1);
return miNodo;
}
188
/**Halla el elemento más grande del árbol
* @throws Exception Se genera cuando el árbol está vacío */
public Persona hallarMaximo( ) throws Exception
{
if(! estaVacio( ) )
{
return hallarMaximo( raiz ).getInformacion();
}
else
{
throw new Exception( "El arbol está vacío ");
}
}
189
/** Elimina el árbol */
public void vaciarArbol( )
{
raiz = null;
}
if( !estaVacio( ) )
{
imprimirPorDefinitiva( raiz );
return array;
}
else
{
throw new Exception("El árbol está vacio");
}
}
190
/** Imprime la nota definitiva */
getCodigoEstudiante() +
"Definitiva"+miNodo.getInformacion().getNotaDefinitiva() ;
i++;
imprimirPorDefinitiva( miNodo.getDerecho());
}
}
if( !estaVacio( ) )
{
devolverPorcentajeGanan( raiz );
return (i*100)/devolverCantidadNodos(raiz);
}
else
{
throw new Exception("El árbol está vacio");
}
}
191
else
{
return miNodo.getAltura();
}
}
ACTIVIDAD
hallarMinimo( ) O( )
192
hallarMaximo( ) O( )
estaContenido( T elemento ) O( )
listarConDefinitivas( ) O( )
obtenerPorcentajeGanaron( ) O( )
Analice cual debe ser siempre el orden de complejidad de la altura de un árbol AVL.
Analice cual debe ser siempre el orden de complejidad de la inserción de un árbol AVL.
3.11 Backtracking
Ejemplos típicos de juegos en los cuales se aplica backtracking son el laberinto, la ubicación de
8 reinas en un tablero de ajedrez de forma que no se ataquen, pacman, juegos de carros, entre
otros.
193
3.12 Caso de estudio: Reinas
Se desea realizar una Aplicación que permita jugar “Ubicación de Reinas”. Dicho juego
pretende que el usuario coloque 8 reinas de forma que ellas no se ataquen entre sí.
La aplicación deberá examinar si las reinas puestas por el usuario se atacan, en cuyo caso
mostrará las posiciones de ataque. De igual forma se deberá brindar la opción de que el
computador solucione el juego.
b. Diagrama de Clases
194
c. Implementación de los métodos
195
/** Permite enumerar las posibilidades
* @param q. Es el arreglo q!=null
* @param n es la posición a obtener n>=0
* @param aleatorio. Para ubicar las reinas */
196
/**Permite obtener el array con las posiciones */
public int[] getResultado()
{
return resultado;
}
ACTIVIDAD
Si el número generado por la función random es 0, indique los valores con los cuales queda el
array a.
Si el número generado por la función random es 2, indique los valores con los cuales queda el
array a.
Se desea realizar una Aplicación para el juego del Laberinto. Cada vez que se inicie el juego
se deberá crear un laberinto diferente.
Cuando inicia el juego el usuario está ubicado en la posición 0,0. Es de anotar, que si él está
ubicado en una posición especifica podrá moverse hacia arriba, hacia abajo, hacia los lados
haciendo uso de las Flechas. El jugador deberá recorrer el laberinto en máximo 10 segundos,
de lo contrario perderá. En cuyo caso podrá obtener la solución del laberinto, obtenida a través
de backtracking, proporcionada por el computador
197
a. Clases o entidades
b. Diagrama de Clases
c. Implementación de métodos
public Laberinto()
{
matriz=new char[12][12];
rutaSolucion=new ArrayList<Posicion>();
}
198
/** Permite fijar la matriz */
public void setMatriz(char[][] matriz)
{
this.matriz = matriz;
}
}
}
199
200
4 ANÁLISIS DE ESTRUCTURAS DE DATOS
4.1 Introducción
Una estructura de datos es un conjunto de objetos que facilitan el uso dinámico de la memoria,
en consecuencia se logra una mejora en el desempeño de los algoritmos que las usan. La
mayoría de los algoritmos tratados aquí pueden ser implementados usando arreglos; sin
embargo, pueden no ser tan eficientes o requerir mayor cantidad de líneas de código para
lograr el mismo efecto.
Las estructuras de datos que analizaremos en este capítulo son las listas, las pilas, las colas y
los árboles. Para cada una de estas estructuras se mostraran algunas de las muchas
implementaciones posibles de estas, así como también el análisis del orden de complejidad de
los métodos que las implementan. Comenzaremos con la estructura de datos dinámica lista
Las listas son las estructuras de datos lineales más generales. Permiten generalmente el
acceso para consulta o modificación en cualquiera de los extremos de la estructura, e incluso
en un punto medio, es frecuente recorrer una lista una lista buscando cierto elemento y, una
vez hallado, eliminarlo, modificar el contenido o insertar un elemento a su izquierda o a su
derecha.
Cuando se habla del concepto de lista, existen casos especiales lo cuales presentan problemas
en el diseño de algoritmos, y estos ocasionan con frecuencia errores en el código. Por lo tanto
se debe procurar escribir código que evite esos casos especiales. Uno de estos casos es
implementar lo que se conoce como nodo cabecera. Un nodo cabecera es un nodo adicional
en la lista que no guarda ningún dato, pero que sirve para satisfacer el requerimiento de que
cada nodo que contenga un elemento tenga un nodo anterior.
Para nuestras implementaciones de cada una de las listas, tendremos siempre en cuenta la
implementación y el nodo cabecera.
Una lista enlazada también recibe el nombre de "lista concatenada", "lista eslabonada" o "lista
lineal". Es una colección de elementos llamados nodos, que en su conjunto forman un
ordenamiento lineal. Comúnmente cada nodo contiene un dato y una referencia al siguiente
nodo. Una lista enlazada es una estructura de datos dinámica que permite almacenar cualquier
cantidad de nodos (Bedoya, 2008).
La siguiente figura muestra una lista sencillamente enlazada, esta lista esta conformada por
nodos, los cuales tienen un campo que contiene el dato y una referencia al siguiente nodo de la
lista.
201
Inicialmente debemos partir del elemento fundamental de la lista: el nodo, a continuación, se
muestra la implementación de la clase Nodo, cada nodo se compone de un dato y una
referencia al siguiente nodo de la lista.
A continuación se muestra una primera implementación para una lista sencillamente enlazada, se
analizará para cada uno de los métodos de la clase lista su orden de complejidad, siempre teniendo en
cuenta y considerando el peor de los casos.
El método eliminar(), permite remover el primer nodo de la lista, verifica inicialmente si la lista
esta vacía, si se cumple el condicional se muestra un mensaje indicado que no hay nodos en la
lista. Si existen nodos en la lista, se actualiza la referencia siguiente del nodo cabecera, a la
referencia que tiene el nodo siguiente del siguiente de cabecera.
202
El método remover(Object elemento), permite eliminar uno o más nodos de la lista sencilla, de
acuerdo al valor enviado como parámetro, este método eliminará todos los nodos de la lista
que correspondan con el campo de dato que llega por parámetro al método. Para ello recorre la
lista desde el primer nodo de la lista, hasta el final de la misma.
Public void remover(Object elemento)
{
Nodo p = new Nodo();
Nodo q = new Nodo();
p = cabecera.siguiente;
q = cabecera.siguiente;
while (p != null)
{
if (elemento.equals(p.info))
{
if (p == cabecera.siguiente)
{
eliminar();
p=q= cabecera.siguiente;
Método
}
else
{
q.siguiente = p.siguiente;
p = p.siguiente;
}
cantidad--;
}
else
{
q = p;
p = p.siguiente;
}
}
}
Para el análisis del orden de complejidad de este método se debe
tener en cuenta el peor de los casos, asumiendo que se tiene una
Análisis del
lista con n nodos, si se desea remover todos los nodos con igual
Orden de
campo de dato, se debe realizar un recorrido lineal de toda la
Complejidad
estructura y nodo por nodo, por lo tanto el orden de complejidad
para este método es O(n).
203
Asumiendo que la lista tiene n nodos, el orden de complejidad de
Análisis del este método es O(n), asumiendo para el peor caso, que el elemento
Orden de que se esta buscando esta en la última posición o en su defecto no
Complejidad se encuentra el la lista sencillamente enlazada.
if (cabecera.siguiente == null)
{
cabecera.siguiente = n;
}
Método
else
{
n.siguiente = cabecera.siguiente;
cabecera.siguiente = n;
}
cantidad++;
}
El método Nodos(), retorna la cantidad de nodos que tenga la lista hasta ese momento.
204
La clase Nodo tiene un método constructor el cual recibe un objeto
Análisis del por parámetro, el orden de complejidad del método es O(1), y el
Orden de orden de complejidad de la clase también es O(1), esta clase posee
Complejidad únicamente operaciones elementales, mas exactamente
operaciones de asignación.
Para nuestro caso, la siguiente figura permite realizar una abstracción de un nodo.
null
null
ListaSencilla()
{
cabecera = new Nodo(null);
Método
ultimo = cabecera;
actual = cabecera;
}
Análisis del El orden de complejidad para el método lista sencilla es O(1), pues
Orden de solo tiene operaciones de asignación las cuales son todas de orden
Complejidad constante.
El método nuevo, permite inicializa un nodo, este nuevo nodo es el nodo cabecera el cual
contiene un campo de datos null y una referencia siguiente null, adicionalmente se inicializan
las referencias último y actual a cabecera.
public void nuevo()
{
cabecera.siguiente=null;
Método
ultimo=cabecera;
actual=cabecera;
}
Análisis del El orden de complejidad de este método es O(1), ya que todas las
Orden de instrucciones que la implementan son constantes.
Complejidad
205
Para el caso en la cual la lista solo tenga el nodo cabecera, el método estavacia() retorna
verdadero. La figura siguiente muestra una lista con esta precondición.
null
null
Lista Vacia.
Si la lista posee nodos, el método estavacia(), retorna falso. La figura muestra esta situación.
null 3 12
null
Lista con dos nodos. null
A continuación se muestra en la figura una situación cuando la lista está vacía, es decir, solo
está el nodo cabecera.
cabecera
null
null
Lista Vacia.
Una vez que se invoque el método insertarInicio(Object element), la lista queda con un nodo y
ese nodo tiene un nombre el cual llega por parámetro al mismo. La figura muestra como queda
la lista una vez se inserta un nodo siguiente al nodo cabecera. Para este caso el valor que llega
por parámetro al método es dat.
cabecera
null dat
null
insertarInicio.
206
En caso de que ya existan nodos en la lista, la referencia al siguiente nodo de la lista se
actualiza al nodo que se inserta. La siguiente figura muestra una precondición de una lista de
objetos, la lista tiene tres nodos.
null 3 12
La figura muestra la inserción de un nodo cuando ya existen nodos en la lista, el objeto que
llega por parámetro es el valor 5, por lo tanto la lista queda con tres nodos.
null 5 3 12
null
insertarInicio.
En la figura se muestra una situación cuando la lista está vacía, es decir solo esta el nodo
cabecera en la lista.
cabecera
null
null
Lista Vacia.
Una vez que se invoque el método insertarUltimo(Object elemento, la lista queda con un nodo
y ese nodo tiene un nombre el cual llega por parámetro al mismo. La siguiente figura muestra
como se inserta un nodo siguiente al nodo cabecera.
207
cabecera
null 10
null
InsertarUltimo
En caso de que ya existan nodos en la lista, como se sabe mediante la referencia al último
nodo, cual es el último nodo, se procede a insertar el nuevo nodo. La siguiente figura muestra
una precondición de una lista de objetos, la lista tiene dos nodos.
null 3 12
null
null
null 3 12 10
null
3 12 null
null
insertarUltimo. null
208
Análisis del El orden de complejidad de este método es O(1) ya que todas sus
Orden de instrucciones son constantes.
Complejidad
La siguiente figura, muestra una lista de con tres nodos, el método eliminar inicio, eliminará el
nodo siguiente al nodo cabecera, el nodo a ser eliminado es el nodo señalado por el óvalo.
null
Lista con tres nodos.
Una vez se elimine el nodo, la lista sencillamente enlazada quedará con dos nodos, la siguiente
figura, muestra el estado final de la lista.
null 12
null
null
EliminarInicio.
Una lista sencilla circular es una colección de elementos llamados nodos, organizados de tal
manera que el ´ultimo nodo de la lista apunta al nodo cabecera. La figura, muestra una lista
circular. Se puede observar que lista contiene un nodo cabecera, el cual nos sirve para
referenciar el inicio de la lista. Una lista sencilla circular es una lista en la cual el último nodo
apunta al primer nodo de la lista, para nuestro caso al nodo cabecera.
null 2 33 4
209
ListaSencillaCircular()
{
cabecera = new Nodo(null);
Método ultimo = cabecera;
actual = cabecera;
}
El método nuevo, permite crear un nuevo nodo a la lista, este nuevo nodo es el nodo cabecera
el cual contiene un campo de datos null y una referencia siguiente null. Se inicializa una
variable tamaño en cero pues el nodo cabecera no se cuenta dentro de la lista.
La figura, muestra una lista sencilla circular vacía, esta lista posee únicamente el nodo
cabecera. Por lo tanto el método estaVacia() para este caso retornaría trae.
null
Caso contrario al anterior se presenta cuando la lista tiene uno o mas nodos, la figura muestra
una lista sencilla circular con 3 nodos, para este caso el método estaVacia() retorna falso,
puesto que existen nodos en la lista.
210
El método insertarInicio() permite insertar un nodo como siguiente al nodo cabecera,
inicialmente verifica si la lista esta vacía, si esta vacía inserta un nodo como siguiente al
cabecera, de lo contrario ya existen nodos en la lista y lo inserta.
if( estaVacia()){
cabecera.siguiente = nuevo;
nuevo.siguiente=cabecera;
ultimo=nuevo;
Método actual= nuevo;
}
else
{
nuevo.siguiente=cabecera.siguiente;
cabecera.siguiente=nuevo;
actual = nuevo ;
}
}
Orden de El orden de complejidad de este método es O(1) ya que todas sus
Complejidad instrucciones son constantes.
La siguiente figura muestra una precondición en la cual se tiene una lista vacía, a esta lista se
le adicionara un nuevo nodo, el cual será el siguiente del nodo cabecera.
null
Una vez se inserte el nodo en la lista, esta quedará como se muestra en la figura a
continuación.
null 15
insertarInicio.
También existe el caso en el cual la lista circular tenga nodos, la figura a continuación muestra
esta situación.
null 11 15
insertarInicio.
Una vez se invoque el método insertarInicio(), se adiciona un nuevo nodo, el cual quedará
como siguiente del nodo cabecera. Para este caso se inserto un nuevo nodo cuyo contenido en
el campo info es 5.
null 5 11 15
insertarInicio.
211
El método insertarUltimo() permite insertar un nodo como el ultimo nodo de la lista, inicialmente
verifica si la lista esta vacía, si esta vacía inserta un nodo como siguiente al cabecera, de lo
contrario ya existen nodos en la lista y lo inserta al final.
La siguiente figura muestra lista con un nodo, a esta lista se le adicionará un nuevo nodo el
cual se convertirá en el ultimo nodo de las lista después de su inserción.
null pl
insertarUltimo.
Una vez se inserte este nodo en la lista, esta quedara como se muestra en la figura.
null pl ca
JOptionPane.showMessageDialog("Lista
Vacia");
}
212
else
{
if(borrar==ultimo)
{
cabecera.siguiente=null;
actual=cabecera;
ultimo=actual;
}
else
{
cabecera.siguiente=borrar.siguiente;
actual=cabecera.siguiente;
}
}
}
Análisis del El orden de complejidad de este método es O(1) ya que todas sus
Orden de instrucciones son constantes.
Complejidad
La siguiente figura, muestra una lista sencilla circular con tres nodos. El ovalo rojo muestra cual
es el nodo que será removido de la lista.
null w y t
Una vez se invoque el método eliminarInicio(), se elimina el nodo siguiente al nodo cabecera y
la lista queda como se muestra en la figura a continuación.
null y t
eliminarInicio.
Una lista doblemente enlazada es una colección de elementos llamados nodos, los cuales
tienen generalmente tres campos: un campo izquierda, un campo dato y un campo derecha.
Los campos izquierda y derecha son referencias a los nodos ubicados en cada nodo. Tiene la
ventaja de que estando en cualquier nodo se puede acceder al nodo que está tanto a la
izquierda como a la derecha. El tiempo para todas las operaciones es constante, excepto para
las operaciones que requieran un tiempo proporcional a la longitud de la lista.
null 10 33 85
null null
Lista Doblemente enlazada.
213
public class NodoDE
{
NodoDE izquierda;
Object dato;
NodoDE derecha;
ListaDoble()
{
cabecera = new NodoDE(null);
Método
ultimo = cabecera;
actual = cabecera;
}
Orden de El orden de complejidad para el método lista doble es O(1).
Complejidad
El método nuevo, permite crear un nuevo nodo a la lista, este nuevo nodo es el nodo cabecera
el cual contiene un campo de datos null y una referencia siguiente null. Se inicializa una
variable tamaño en cero pues el nodo cabecera no se cuenta dentro de la lista.
214
En la grafica a continuación se observa que la lista esta vacia, pues solo se tiene el nodo
cabecera con sus respectivas referencias y por lo tanto el método para este caso retorna
verdadero.
null
null null
Lista Doblemente enlazada.
En este caso la lista contiene un nodo cuyo contenido es el número 5 y por lo tanto el método
retorna falso.
null 5
null null
Lista Doblemente enlazada.
if (estaVacia())
{
cabecera.derecha=nuevo;
nuevo.izquierda=cabecera;
ultimo = nuevo;
Método actual = nuevo;
}
else
{
nuevo.derecha=cabecera.derecha;
nuevo.izquierda=cabecera;
temporal.izquierda=nuevo;
cabecera.derecha=nuevo;
actual = nuevo;
}
}
Orden de El orden de complejidad de este método es O(1) ya que todas sus
Complejidad instrucciones son constantes.
Para este caso se observa que inicialmente la lista esta vacía y posteriormente se inserta un
nodo, actualizándose la referencia derecha del nodo cabecera.
null
null null
Lista Doblemente enlazada.
null 96
null null
Lista Doblemente enlazada.
215
El método insertarUltimo() permite insertar un nodo como el ultimo nodo de la lista, inicialmente
verifica si la lista esta vacía, si esta vacía inserta un nodo como siguiente al cabecera, de lo
contrario ya existen nodos en la lista y lo inserta al final.
if(estaVacia())
{
ultimo.derecha=nuevo;
nuevo.izquierda=ultimo;
ultimo= nuevo;
Método actual = nuevo;
}
else
{
ultimo.derecha=nuevo;
nuevo.izquierda=ultimo;
ultimo = nuevo;
actual = nuevo;
}
}
Análisis del El orden de complejidad de este método es O(1), ya que todas sus
Orden de instrucciones son constantes.
Complejidad
Este método inserta un nodo al final de la lista, para este caso se pretende insertar el nodo con
valor 3.
null 1 2
null null
Lista Doblemente enlazada.
null 1 2 3
Método if(estaVacia())
{
JOptionPane.showMessageDialog(null,"Lista
Esta Vacia");
}
216
else
{
if(borrar==ultimo)
{
cabecera.derecha=null;
borrar.izquierda=null;
actual=cabecera;
ultimo=actual;
}
else{
NodoDE temporal=borrar.derecha;
cabecera.derecha=temporal;
temporal.izquierda=cabecera;
actual=cabecera.derecha;
}
}
}
Análisis del El orden de complejidad de este método es O(1) ya que todas sus
Orden de instrucciones son constantes.
Complejidad
El método elimina el primer nodo de la lista, para ello es necesaria la actualización de las
correspondientes referencias. Para es caso el nodo a eliminar es el que contiene el número 73.
null 73 233 69
null null
Lista Doblemente enlazada
null 233 69
null null
Lista Doblemente enlazada.
Una lista doblemente enlazada es una colección de elementos llamados nodos, los cuales
tienen generalmente tres campos: un campo izquierda, un campo dato y un campo derecha.
Los campos izquierda y derecha son referencias a los nodos ubicados en cada nodo. Tiene la
ventaja de que estando en cualquier nodo se puede acceder al nodo que esta tanto a la
izquierda como a la derecha. El tiempo para todas las operaciones es constante, excepto para
las operaciones que requieran un tiempo proporcional a la longitud de la lista.
null 34 21 78
217
A continuación, se mostrará la clase nodo a partir de la cual se puede empezar a construir la
lista doblemente enlazada circular.
ListaDoble()
{
cabecera = new NodoDE(null);
Método
ultimo = cabecera;
actual = cabecera;
}
Orden de El orden de complejidad para el método lista doble es O(1).
Complejidad
El método nuevo, permite crear un nuevo nodo a la lista, este nuevo nodo es el nodo cabecera
el cual contiene un campo de datos null y una referencia siguiente null. Se inicializa una
variable tamaño en cero pues el nodo cabecera no se cuenta dentro de la lista.
218
Orden de El orden de complejidad para este método es O(1) .
Complejidad
Para este primer caso el método retornará falso, teniendo en cuenta que únicamente la lista
tiene el nodo cabecera.
null
Para este segundo caso el método retornará verdadero, teniendo en cuenta que la lista se
compone de varios nodos.
null 500 21 64
El método insertarInicio permite insertar un nodo como siguiente al nodo cabecera, inicialmente
verifica si la lista esta vacía, si esta vacía inserta un nodo como siguiente al cabecera, de lo
contrario ya existen nodos en la lista y lo inserta.
if (estaVacia())
{
cabecera.derecha=nuevo;
nuevo.izquierda=cabecera;
ultimo = nuevo;
Método actual = nuevo;
}
else
{
nuevo.derecha=cabecera.derecha;
nuevo.izquierda=cabecera;
temporal.izquierda=nuevo;
cabecera.derecha=nuevo;
actual = nuevo;
}
}
Análisis del El orden de complejidad de este método es O(1) ya que todas sus
Orden de instrucciones son constantes.
Complejidad
Para este caso si se parte de una lista circular doblemente enlazada que se encuentra vacía, el
proceso de insertar un nodo se muestra a continuación.
null
219
Si se desea insertar un nodo, la lista queda de la siguiente manera si se va a insertar un nodo
con el valor 58.
El método insertarUltimo permite insertar un nodo como el ultimo nodo de la lista, inicialmente
verifica si la lista esta vacía, si esta vacía inserta un nodo como siguiente al cabecera, de lo
contrario ya existen nodos en la lista y lo inserta al final.
if(estaVacia())
{
ultimo.derecha=nuevo;
nuevo.izquierda=ultimo;
ultimo= nuevo;
Método actual = nuevo;
}
else
{
ultimo.derecha=nuevo;
nuevo.izquierda=ultimo;
ultimo = nuevo;
actual = nuevo;
}
}
Orden de El orden de complejidad de este método es O(1) ya que todas sus
Complejidad instrucciones son constantes.
if(estaVacia())
{
JOptionPane.showMessageDialog("Lista Vacia");
}
Método else
{
if(borrar==ultimo)
{
cabecera.derecha=null;
borrar.izquierda=null;
actual=cabecera;
ultimo=actual;
}
220
else
{
NodoDE temporal=borrar.derecha;
cabecera.derecha=temporal;
temporal.izquierda=cabecera;
actual=cabecera.derecha;
}
}
}
Análisis del El orden de complejidad de este método es O(1) ya que todas sus
Orden de instrucciones son constantes.
Complejidad
Para este caso se quiere eliminar el nodo que se encuentra al inicio de la lista. En este caso se
desea eliminar el nodo con valor 80.
null 80 21 98
221
a) Requerimientos funcionales
ENTRADAS
RESULTADOS
RESULTADOS
ENTRADAS
RESULTADOS
RESULTADOS
222
b. Identificar las entidades o clases.
ENTIDAD DESCRIPCIÓN
Ciudad
GrupoCiudades
d. Implementación
Se deben implementar las clases del dominio del problema asociadas con la estructura de lista
que considere más apropiada.
223
4.4 Estructura de Datos Pila
Uno de los conceptos más útiles en las ciencias de la computación es el de la pila. En este
apartado examinaremos esta estructura de datos engañosamente simple y veremos por qué
tiene una función tan importante en las áreas de programación y lenguajes de programación.
Se debe especificar algún tamaño máximo N para la pila; EJ= 1000 elementos. Entonces la pila
consiste en un arreglo S de N elementos mas una variable entera t, que expresa índice del
elemento superior del arreglo S.
0 1 2 t N-1
Al considerar que los arreglos comienzan en java con el índice 0, se inicializa t en –1, y se usa
este valor de t para indicar cuando está vacía la pila. De igual forma, se puede usar esta
variable para determinar la cantidad de elementos (t+1) en una pila. Se introduce la excepción
llamada StackFullException para indicar la condición de error que se produce si se trata de
insertar un elemento nuevo y la pila S se encuentra llena (Goodrich & Tamassia, 2002).
A continuación se muestra la implementación de una pila mediante un arreglo, mediante los
siguientes métodos fundamentales:
224
public Stack (){
this(CAPACIDAD);
}
public Stack (int cap)
{
capacidad = cap;
s= new Object [capacidad];
}
}
El orden de complejidad de los métodos constructores tiene
Orden de únicamente instrucciones constantes y por lo tanto el orden de
Complejidad complejidad es O(1).
Retorna si la pila esta vacía, como se esta trabajando con un índice que controla el tope de la
pila, cuando el valor del índice es menor que cero, retorna que la pila no tiene elementos,
como únicamente se esta retornando en el método un valor.
El método top retorna un objeto de la pila, el objeto que retorna en caso de encontrarse es el
elemento que esta en el tope de la pila. Si la pila se encuentra vacía, se muestra un mensaje
de advertencia indicando esta situación.
225
else
{
return s[top];
}
}
El método pop elimina el ultimo elemento de la pila y lo retorna, inicialmente verifica si la pila
contiene o no elementos, en caso de estar la pila vacía, muestra un mensaje de advertencia
indicando la situación, en caso contrario, actualiza la posición del índice mediante un
decremento y se retorna el elemento de la posición en la cual se encontraba el índice
inicialmente.
if (isEmpty())
{
throw new StackEmptyException ("Pila
Vacia");
Método }
else
{
elem=s[top];
s[top]=null;
top--;
return elem;
}
}
Análisis del Por la regla de la suma podemos deducir que el orden de
Orden de complejidad de este método es constante O(1).
Complejidad
Las siguientes dos clases son las que contienen las excepciones que se mencionan en la
implementación de los métodos de pila.
226
4.4.2 Implementación utilizando Listas
La implementación de una pila también es posible utilizando nodos, lo importante en este caso
es que se respete el principio FIFO. El constructor de la pila permite crear una pila vacía.
Método Pila()
{
tope = new Nodo(null);
}
}
Análisis del El orden de complejidad para este método es O(1), complejidad
Orden de constante.
Complejidad
El método pop() elimina el ultimo elemento de la pila y lo retorna, inicialmente verifica si la pila
contiene o no elementos, en caso de estar la pila vacía, muestra un mensaje de advertencia
indicando la situación, en caso contrario, actualiza la posición del índice mediante un
decremento y se retorna el elemento de la posición en la cual se encontraba el índice
inicialmente.
Object pop()
{
Object dato;
if (tope == null)
{
System.out.println("Pila esta Vacia");
dato = null;
}
Método
else
{
dato = tope.siguiente.info;
tope.siguiente =
tope.siguiente.siguiente;
}
return dato;
}
Orden de Por la regla de la suma podemos deducir que el orden de
Complejidad complejidad de este método es constante O(1).
227
Análisis del El orden de complejidad para este método es O(1), complejidad
Orden de constante.
Complejidad
El método mostrar permite realizar un recorrido de todos los elementos de la pila, cuando
realiza este recorrido, va mostrando el contenido de cada uno de los elementos de la pila, se
realiza el recorrido mediante la instrucción p = p.siguiente.
void mostrar()
{
Nodo p = new Nodo();
p = tope.siguiente;
while (p != null)
Método
{
System.out.print(" " + p.info + "\n");
p = p.siguiente;
}
}
Análisis del El análisis de este método para el peor de los casos asumiendo
Orden de que se tienen n nodos en la pila es de orden O(n) lineal, pues
Complejidad recorre la pila desde el primer nodo hasta el último.
Este método retorna si la pila esta vacía, como se esta trabajando con un índice que controla el
tope de la pila, cuando el valor del índice es menor que cero, retorna que la pila no tiene
elementos, como únicamente se esta retornando en el método un valor.
boolean pilaVacia()
{
Método return tope.siguiente == null
}
El método top() retorna un objeto de la pila, el objeto que retorna en caso de encontrarse es el
elemento que esta en el tope de la pila. Si la pila se encuentra vacía, se muestra un mensaje
de advertencia indicando esta situación.
Nodo cima()
{
Método
return tope.siguiente;
}
Análisis del Por la regla de la suma podemos deducir que el orden de
Orden de complejidad de este método es O(1).
Complejidad
228
eliminación solo se permiten en el extremo opuesto de la cola, que tradicionalmente
llamaremos el frente de la cola.
Inicialmente se muestra una implementación de una cola utilizando como estructura base una
arreglo unidimensional de elementos. Es necesario al momento de trabajar con colas definir
cual será el frente y cual será el final de la cola, lo anterior es fundamental para establecer por
donde insertarán elementos a la cola y por donde se eliminarán los elementos de la cola.
Para definir el frente y el final de la cola es necesario definir dos variables, frente y final. Donde
frente es un índice dentro del arreglo que guarda el primer elemento de la cola y final es un
índice dentro del arreglo que indica la siguiente celda disponible de arreglo para insertar un
elemento.
Al principio se asignará frente = final = 0, para indicar que la cola se encuentra vacía. Cuando
se saca un elemento del frente de la cola, solo incrementar frente para señalar la siguiente
celda. De igual manera, cuando se agrega un elemento, tan solo se incrementa final para
señalar la siguiente celda disponible en la cola. Sin embargo de acuerdo a lo anterior existe un
problema con este método.
Por ejemplo, considérese lo que sucede cuando se coloca y se quita de la cola N veces
distintas a un solo elemento. Se obtendría frente = final = N. Si a continuación se tratara de
insertar el elemento solo una vez mas, se obtendría un error de arreglo fuera de límites, aun
cuando haya suficiente lugar en la cola para ese caso. Para evitar ese problema, se hará que
los índices frente y final se repitan. Esto es, se considerará ahora que la cola es un arreglo
circular.
0 1 2 f r N-1
Uso del arreglo Q en forma circular, la configuración “envuelta” con r < f. Se resaltan las celdas
que guardan los elementos de la cola (Goodrich & Tamassia, 2002).
0 1 2 r f N-1
Cada vez que se incrementa frente o final, es posible calcular este incremento en la forma:
(frente+1) %n ó (final+1) % n, respectivamente, siendo n el tamaño total del arreglo
unidimensional. Lo anterior garantiza el manejo circular de la cola.
Se examinará ahora el caso que se presenta si se forman N objetos en la cola Q, sin sacar
ninguno de ellos. Se tendría que f=r, que es la misma condición que se presenta cuando la cola
esta vacía. Por consiguiente, no se podría decir cual es la diferencia entre una cola llena y una
vacía en este caso. La solución es insistir en que Q nunca puede contener mas de N-1 objetos
(Goodrich & Tamassia, 2002).
229
A continuación se muestra la implementación de los métodos de la cola. En esta
implementación se utilizaran dos excepciones las cuales indican cuando la cola esta llena o
cuando la cola esta vacía.
class queue
{
private Object Q[];
public static final int CAPACIDAD =50;
public int n, f =0, r =0;
public queue ()
{
Método
this (CAPACIDAD);
}
public queue (int cap)
{
n = cap;
Q = new Object [n];
}
}
Orden de El orden de complejidad para este método es O(1), complejidad
Complejidad constante.
El siguiente método llamado isEmpty() retorna si la cola esta vacía, como se esta trabajando
con un índice que controla el tope de la pila y el final de la pila, cuando estos valores son
iguales, se determina que la cola esta llena.
230
Orden de El orden de complejidad para este método es O(1), complejidad
Complejidad constante.
El método front() retorna un objeto de la cola, el objeto que retorna en caso de encontrarse es
el elemento que esta en el frente de la cola. Si la cola se encuentra vacía, se muestra un
mensaje de advertencia indicando esta situación.
El método size() retorna un valor entero, este valor entero determina la cantidad de elementos
que tiene la cola.
El método dequeue() elimina el ultimo elemento del frente de la cola y lo retorna, inicialmente
verifica si la cola contiene o no elementos, en caso de estar la cola vacía, muestra un mensaje
de advertencia indicando la situación, en caso contrario, actualiza la posición del índice
mediante f = (f+1) % n y se retorna el elemento de la posición en la cual se encontraba el índice
inicialmente.
231
Orden de Por la regla de la suma podemos deducir que el orden de
Complejidad complejidad de este método es constante O(1).
Inicialmente se declara la clase Cola con su respectivo método constructor. Se declaran tres
referencias con las cuales es posible realizar las diferentes operaciones sobre las colas
utilizando listas. El método inicializa dos referencias en null.
Método Cola()
{
frente = new Nodo(null);
ultimo = new Nodo(null);
cantidad = 0;
}
Orden de El orden de complejidad para este método es O(1).
Complejidad
El método dequeue, elimina el próximo nodo candidato a salir de la cola, si la cola está vacía,
muestra un mensaje de advertencia indicando esta situación, este método retorna el dato del
nodo removido.
Object dequeue()
{
Object dato = null;
if (frente.siguiente != null)
{
dato = frente.siguiente.info;
frente.siguiente =
frente.siguiente.siguiente;
Método
cantidad--;
}
else
{
System.out.println("Cola vacia !!!");
}
return dato;
}
Orden de El orden de complejidad para este método es O(1).
Complejidad
El método queue agrega un nuevo nodo a la cola, recibe por parámetro el valor que será
asignado al nodo.
232
void queue(Object e)
{
Nodo n = new Nodo(e);
Nodo p = new Nodo();
if (frente.siguiente == null)
{
frente.siguiente = n;
Método ultimo.siguiente = n;}
else
{
ultimo.siguiente.siguiente = n;
ultimo.siguiente = n;
}
catidad++;
}
Orden de El orden de complejidad para este método es O(1).
Complejidad
Nodo top()
{
Método
return frente.siguiente;
}
Orden de El orden de complejidad para este método es O(1).
Complejidad
Nodo ultimo()
{
Método
return ultimo.siguiente;
}
Orden de El orden de complejidad para este método es O(1).
Complejidad
int longitud()
{
Método
return cantidad;
}
Orden de El orden de complejidad para este método es O(1).
Complejidad
233
Los ArrayList, son una de las muchas estructuras que crecen o disminuyen dinámicamente.
En los ArrayList únicamente se pueden almacenar objetos, no es posible almacenar en el
tipos primitivos de datos.
Teniendo en cuenta que los anteriores son métodos predefinidos por el lenguaje de
programación, la implementación y análisis de los mismos se realizará en un contexto de
aplicación que se muestra en la siguiente sección.
Se quiere construir una Aplicación que maneje la información de los programas académicos de
una Universidad. Cada programa académico tiene nombre, código, teléfono y un número de
estudiantes. El código de cada programa debe ser único es por defecto la palabra clave para
su búsqueda.
234
a. Identificar las clases.
ENTIDAD DESCRIPCIÓN
Universidad Es la clase principal del problema.
Programa Contiene la información de cada programa académico.
b. Diagrama de clases.
235
Un ArrayList es una clase contenedora de objetos en java la forma en la cual se declara un
arreglo es similar a la que se realizo con la declaración de los arreglos. Para el caso de estudio
se tiene que la clase Universidad tiene una colección de programas, por lo tanto debemos
declarar un ArrayList de Programa y posteriormente crearlo.
import java.util.ArrayList;
La construcción del ArrayList se debe hacer dentro del método constructor, para el caso de
estudio la inicialización se realizaría de la siguiente manera:
public Universidad()
{
misProgramas = new ArrayList<Programa>();
}
Es necesario una vez se crea el ArrayList, es posible realizar varios tipos de operaciones,
inicialmente tenemos el método add(objeto), el cual permite adicionar un elementos a la
estructura contenedora. Para el caso de estudio, si deseamos agregar un programa es posible
adicionarlo al ArrayList misProgramas.
236
Inicialmente si tiene el método Constructor de la clase Universidad. En él se inicializa el arreglo
de programas académicos.
Este método una arreglo de cadenas con la información de todos los programas académicos.
Para esta operación utiliza el método predefinido get(i), en el cual i es el índice por medio del
cual se accede a cada programa.
}
return info;
}
El orden de complejidad del método se analiza asumiendo que el
Orden de ArrayList contiene n elementos. Por lo tanto se necesita de un ciclo
Complejidad que permita recorrer la totalidad del mismo, y su orden de
complejidad es O(n).
237
public Programa buscarPrograma(String codigo)
{
for (int i=0; i < misProgramas.size(); i++)
{
if((misProgramas.get(i).getCodigo()).
equals(codigo))
Método
{
return (misProgramas.get(i));
}
}
return null;
}
Para el caso de la búsqueda se tiene el mejor y el peor caso. Si el
elemento se encuentra en las primeras posiciones del arreglo, el
Orden de orden de complejidad del método es O(1), pero si el elemento a
Complejidad buscar se encuentra en las últimas posiciones del arreglo, el orden
de complejidad del método es O(n).
Este programa retorna el nombre del programa con más de 350 alumnos.
ACTIVIDAD
238
Investigue en que consiste el método set(objeto), el cual permita modificar la información
de un Programa Académico.
239
240
5 OPTIMIZACIÓN, PRUEBAS Y LÍMITES DE LA LÓGICA
5.1 Introducción
En este capítulo se mostrarán las técnicas básicas que pueden ser usadas para la optimización
de código. En capítulos anteriores se han mostrado algunas técnicas que son de utilizada para
el análisis y solución de problemas de programación. Adicional a estas técnicas se incorpora la
optimización de código es cual es un tema que correctamente aplicado puede ayudar a
maximizar la eficiencia en términos de tiempo de ejecución de un programa.
Con el propósito de analizar los aspectos relacionados con las pruebas al código. Java dispone
del Framework JUnit para la realización y automatización de pruebas y el cual se explicará
mediante un ejemplo de aplicación.
Finalmente en este capítulo se mostrarán los conceptos básicos de los problemas asociados a
los límites de la lógica, los cuales son conocidos como los problemas clase P y clase NP.
El objetivo de las técnicas de optimización es mejorar el código fuente para que nos dé un
rendimiento mayor. Muchas de estas técnicas vienen a compensar ciertas ineficiencias que
aparecen en el código fuente. Las optimizaciones en realidad proporcionan mejoras, pero no
aseguran el éxito de la aplicación. Algunas optimizaciones que se pueden aplicar a los ciclos
son:
Desenvolvimiento de ciclos (loop unrolling).
Reducción de esfuerzo.
Tipos de variables
Fusión de ciclos (loop jamming).
Esta técnica de optimización consiste en desenvolver el cuerpo del ciclo dos o más veces
incrementando los saltos de ciclo, con el fin de mejorar el reúso de registros, minimizar
recurrencias y de exponer más paralelismo a nivel instrucción. El número de desenvolvimiento
es determinado automáticamente por el compilador o bien por el programador mediante el
empleo de directivas (García, Delgado, & Castañeda, 2000).
Cuando se encuentra que la cantidad de iteraciones del ciclo es pequeña y constante, esta
técnica se puede utilizar. Por ejemplo, si se tiene el siguiente código.
Para este caso inicialmente se muestra un método el cual contiene un código que se va a
optimizar. En estos casos no se esta resolviendo un problema especifico se quiere hacer
énfasis en la aplicación de la técnica.
241
if (k % 13 == 0)
{
k = 0;
for (int j = 0 ; j < 6; j++)
{
solucion[j]= prueba[j]+5;
}
}
}
}
Se observa que el ciclo mas externo del método ejemplo(), se elimina por el equivalente en el
código, de forma tal que el acceso a cada índice del arreglo se realiza directamente. El
algoritmo primer método es menos eficiente que el que aparece optimizado. De forma
experimental, se puede afirmar que la correcta aplicación de la técnica mejora el tiempo de
ejecución.
242
for (int i=0;i<tam;i++)
{
arreglo[i]=temp[j]*temp[j++]+2000;
}
}
La técnica de reducción de esfuerzo (Strength Reduction), sugiere que una expresión puede
remplazarse por otra, siempre y cuando cumpla la misma función, siempre garantizando una
mejora en la eficiencia de su ejecución. A continuación se muestra un caso en el cual se aplica
esta técnica.
243
public void reduccionEsfuerzo()
{
int prueba [] = new int [5000];
int solucion [] = new int [5000];
for (int i = 0; i < prueba.length; i++)
{
prueba[i] = (int)(Math.random()* 101);
Método
}
for (int j = 0; j < solucion.length; j++)
{
solución[j] = prueba [j] * prueba[j] *
prueba[j];
}
}
También, para este caso el método denominado ejemplo2 es menos eficiente denominado
reducción de esfuerzo. Lo anterior se confirma mediante la experimentación en tiempo de
ejecución. En este caso se puede afirmar que la operación de potencia mediante un método de
clase predefinida, es mucho más lenta que la operación de producto.
Se ejecuta la misma operación pero con datos de tipo entero y reales. Adicionalmente el
concepto de tipo de dato se utiliza, entre otras cosas, para realizar las operaciones entre los
datos. Así, para un mismo procesador, efectuar el producto de dos números es mucho más
lento si estos números son de tipo real que si son de tipo entero; sin embargo ambos casos son
similares desde el punto de vista algorítmico. A continuación se muestra la implementación
usando tipos de datos double.
244
for (int j = 0; j < solucion1.length; j++)
{
solucion1[j] = prueba1[j] * j;
}
}
ACTIVIDAD
Cuando en una implementación se tienen ciclos que iteran la misma cantidad de veces, tal
como se muestra en el siguiente cuadro Dada esta situación, se puede optimizar el código,
aplicando la técnica de la fusión de ciclos. Para este ejemplo, se puede observar que el tamaño
de los tres arreglos definidos es igual. La operación de lectura de los datos en cada uno de
ellos se realiza por medio de un ciclo que itera desde cero hasta 2500000.
Cuando se puede verificar que los ciclos iteran la misma cantidad de veces y estos se
encuentran concatenados, es posible unir todas las instrucciones en un solo ciclo. Esta
operación de fusión mejora la ejecución de las instrucciones en cuanto a su tiempo.
245
for (int k = 0; k < prueba1.length; k++)
{
prueba1[k] = prueba[k] - k;
}
}
El anterior código, se puede optimizar de forma tal que estos se unan en un solo ciclo. Es
necesario verificar previamente la relación entre las operaciones que se realizan en este único
ciclo. Lo anterior para evitar resultados erróneos.
Otro ejemplo en el cual se muestra la fusión de ciclos. Para este caso se escribe un fragmento
de código que permite realizar la suma de los elementos que se encuentran en la diagonal
secundaria de una matriz.
int s;
for(int i=0; i<tam;i++)
{
Código
s=i;
resul=arreglo[i][s];
}
246
public void ejemplo5()
{
int lon,l,m;
double j;
lon = arreglo1.length;
Método
for (int i=0;i<lon;i++)
{
j=arreglo1[i]*Math.pow(i,3) +
Math.pow(i,3) ;
}
}
5.2.6 Folding
Es una de las técnicas de optimización más y de uso mas sencillo, la cual hace referencia a
que en muchas ocasiones las expresiones pueden se simplificadas. Por ejemplo si se tiene la
expresión:
int i = 20 + 58 – c;
El compilador de java directamente reemplaza los números 20 y 58 por 78. Este caso se
extiende a las expresiones que impliquen todo tipo de operación aritmética.
247
k=8500;
l=15250;
m=450;
for (i=0;i<lon;i++)
{
j=arreglo1[i]*((k+l)*m)/150;
}
}
for (i=0;i<lon;i++)
Método {
arreglo1[i]=(int)(Math.random()* 101);
}
for (i=0;i<lon;i++)
{
j=arreglo1[i]*71250;
}
}
ACTIVIDAD
Dado el siguiente fragmento de código, aplique todas las técnicas posibles para optimizar el
código.
for (i=0;i<longi;i++)
{
arreglo[i]= a[m++]+arreglo1[i]*Math.pow(i,2)+
Math.pow(i,2)+1;
if(m==6)
{
m=0;
}
}
for (i=0;i<longi;i++)
{
w=Math.pow(a[i],6)+ 500;
}
248
for (i=0;i<long;i++)
{
arreglo1[i]= arreglo1[i]+m+k/m*l;
}
Dado el siguiente fragmento de código, aplique todas las técnicas posibles para optimizar el
código.
a=16541;
b=24415;
c=86414;
for(int i=0; i<tam;i++)
{
arreglo[i]=(54*i)+((b+((int)Math.sqrt(b*b)+(4*a*c))/(2*a)));
}
Dado el siguiente fragmento de código, aplique todas las técnicas posibles para optimizar el
código.
El Diseño por contrato es una metodología que se basa en la idea de tener claramente definido
lo que se tiene en la entrada y lo que se debe proporcionar como resultado. A este conjunto de
términos lo denominaremos contrato entre las partes. En general, puede afirmarse que el
contrato es un acuerdo formal, en el que se expresan las obligaciones y derechos de los
participantes. Por lo tanto si ocurre algún error nos remitiremos al contrato para poder
determinar las responsabilidades. Tenga en cuenta que “para asignar responsabilidades se
puede apoyar en la técnica del experto (el dueño de la información es el responsable de ella) o
la técnica de descomposición por requerimientos (descomponer un requerimiento funcional en
subproblemas para poder satisfacer el requerimiento completo) (Villalobos & Casallas, 2006).
249
En este caso, las condiciones previas (que de ahora en adelante denominaremos
precondiciones) que deben cumplirse para que este método se ejecute son:
La precondición expresa las restricciones necesarias para que la operación funcione de forma
adecuada.
Se agregó al estudiante.
Se produjo un error y no se pudo efectuar la operación de agregar.
La postcondición se define como las condiciones que deben ser ciertas cuando termina la
ejecución del método.
Se considera entonces que la precondición son las condiciones impuestas para que se dé el
desarrollo del método, mientras que la postcondición se considera como los compromisos
aceptados.
Es muy importante realizar una adecuada documentación a los contratos, esto se logra
haciendo uso de javadoc. Tenga en cuenta que en general, se deben agregar los comentarios
a cada uno de los métodos, que componen un programa.
/**
* Estos son comentarios javadoc
*/
* @ version 1.0
* @ created Nov-2007
* @ author Carlos Perez
Para que eclipse nos ahorre un poco de trabajo con la generación de la documentación javadoc
se debe digitar /** y presionar la tecla enter.
/**
* Este método permite agregar un nuevo estudiante a un curso de
* programación
* <b>pre: </b> El array donde se guardan los estudiantes no es null.
* <b>post: </b> Se ha agregado un nuevo estudiante.
* @ param código. Código estudiante. codigo!=null, !codigo.equals("");
* @ param nombre. Nombre estudiante. nombre!=null, !nombre.equals("");
* @ param dirección. Dirección estudiante direccion!=null,!direccion.
* equals("");
* @throws Exception si un estudiante tiene el mismo código genera una
* excepción
*/
250
public void agregarEstudiante(String codigo, String nombre, String
direccion) throws Exception
{
/**
* Llama al método verificar invariante
*/
public void llamarVerificarInvariante()
{
try
{
verificarInvariante();
}
catch(AssertionError ae)
{
imprimirErrorInvariante(ae);
}
}
251
/** Este método permite verificar la invariante de la clase */
public void verificarInvariante( )
{
assert (n>=0) : "n debe ser mayor o igual a cero";
}
Java dispone del Framework JUnit para la realización y automatización de pruebas. Las
pruebas permiten verificar el funcionamiento de los métodos de la clase, para determinar si
funcionan como se espera. Es decir, permiten para identificar posibles errores y revelar el
grado de cumplimiento en relación a las especificaciones que inicialmente se plantearon para el
sistema.
Para mostrar el uso de JUnit se tendrá en consideración el caso de estudio para un estudiante.
A continuación se muestra la descripción del problema.
252
A continuación se procede a crear en eclipse el ambiente de pruebas. Se debe crear un caso
de prueba seleccionando JUnit Test Case.
En Class under Test se debe presionar el botón Browse y luego se debe escribir el nombre de
la clase a la que se le va a hacer el test. A continuación dar clic en Ok.
Algunas de las opciones disponibles respeto a etiquetas para realizar las pruebas son las que
se muestran a continuación:
253
setUpBeforeClass: Este método será ejecutado al inicio del lanzamiento de todas las
pruebas. Únicamente puede tenerse un método con esta opción. Se utiliza en general
para inicializar atributos que son comunes a todas las pruebas.
tearDownAfterClass: Únicamente puede haber un método con esta marca. Se ejecuta
una sóla vez cuando ha finalizado la ejecución de todas las pruebas.
tearDown: Se ejecuta después de ejecutar todas las pruebas de esta clase. Este
método se sustituye por la anotación @After. Esta etiqueta efectúa todo lo contrario de
la etiqueta @Before.
setUp: Se invoca antes de ejecutar cada prueba. Este método se sustituyó por la
anotación @Before
Test: Se le coloca @Test a la prueba que se va a ejecutar
Ignore: El método que tenga esta marca no será ejecutado.
o setCodigo(String)
o setNombre(String)
o setNota(double)
o calcularBonificacion()
254
{
fail("Not yet implemented");
}
@Test
public void testSetNota()
{
fail("Not yet implemented");
}
@Test
public void testCalcularBonificacion()
{
fail("Not yet implemented");
}
}
A continuación se debe usar la siguiente clase para verificar que los métodos de la clase
Estudiante estén correctamente implementados. Es necesario modificar el código para que
quede de la siguiente forma:
@Test
public void testSetNombre() {
assertEquals( "El nombre es
inválido.", "Juan",
miEstudiante.getNombre());
}
@Test
public void testSetNota() {
assertEquals( "La nota es
incorrecta.", 3.0,
miEstudiante.getNota(),1 );
}
255
@Test
public void testCalcularBonifiacion()
{
assertEquals( "La nota definitiva con
bonificación es incorrecta.", 3.1,
miEstudiante.calcularBonifcacion(),1 )
}
}
Como última sección de este libro se plantea una clasificación de los problemas según el tipo
de algoritmo. Se describirán los conceptos fundamentales de los algoritmos de clase P, clase
NP y NP completos.
5.5.1 Clase P
Los algoritmos de la clase P son conocidos como aquellos algoritmos cuyo tiempo de ejecución
y complejidad computacional se encuentra en un orden polinómico. Con base en lo analizado
en este libro se puede afirmar que los algoritmos con tiempo de ejecución polinómico son aun
considerados como eficientes y por lo tanto son tratables y manejables en la práctica.
5.5.2 Clase NP
Algunos ejemplos de problemas y soluciones que se encuentran en la clase NP, son los
siguientes:
Problema de aprendizaje
Problemas complejos con Grafos
Coloreado de Grafos
El problema del Agente Viajero
Programación automática de Tareas
256
6 BIBLIOGRAFÍA
Aho, A., & Ullman, J. (1995). Foundations of Computer Science. Computer Science Press,
1995.
Aho, A., Ullman, J., & Hopcrof, J. (1994). The Design and Analysis of Computer. Addison-
Wesley.
Baldwin, D., & Scragg, G. (2004). Algorithms & Data Structures. Massachusetts: Computer
Engineering Series.
Brassard, G., & Bratley, P. (1997). Fundamentos de Algoritmia. Madrid: Prentice Hall.
Cardona, S., Jaramillo, S., & Carmona, P. (2007). Análisis de Algoritmos en Java. Armenia:
Elizcom.
Cardona, S., Jaramillo, S., & Villegas, M. (2008). Introducción a la Programación en Java .
Armenia: Elizcom.
Cormen, T., Leiserson, C., Rivest, R., & Stein, C. (2001). Introduction to Algorithms.
Dunfermline, FIF, United Kingdom: The MIT Press.
Guerequeta, R., & Vallecillo, A. (2000). Técnicas de Diseño de Algoritmos . Málaga: Servicio de
Publicaciones de la Universidad de Málaga.
257
López, J. (2003). Ecuaciones de Recurrencia. Recuperado el 02 de 02 de 2010, de
http://ocw.univalle.edu.co/ocw/ingenieria-de-sistemas-telematica-y-afines/fundamentos-
de-analisis-y-diseno-de-algoritmos/material/Unidad1-4.pdf
Preiss, B. (1998). Data Structures and Algorithms with Object-Oriented. Addison Wesley
Publishing.
Villalobos, J., & Casallas, R. (2006). Fundamentos de Programación aprendizaje activo basado
en casos. Bogotá: Pearson Prentice Hall.
258