Está en la página 1de 258

Análisis de Algoritmos

para Ingeniería de
Sistemas y Computación

Sergio Augusto Cardona Torres


Adscrito al
Programa de Ingeniería de Sistemas y Computación
Facultad de Ingeniería
Universidad del Quindío

Sonia Jaramillo Valbuena


Adscrita al
Programa de Ingeniería de Sistemas y Computación
Facultad de Ingeniería
Universidad del Quindío

Jorge Iván Triviño Arbeláez


Adscrito al
Programa de Ingeniería de Sistemas y Computación
Facultad de Ingeniería
Universidad del Quindío

1
Análisis de Algoritmos
para Ingeniería de
Sistemas y Computación

No está permitida importar, vender, difundir, distribuir y exportar total o parcialmente


esta obra, ni su tratamiento o transmisión por cualquier método sin autorización escrita
del editor. El contenido de la presente obra es exclusivo de los autores

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

1 ANÁLISIS DE ALGORITMOS .............................................................................. 9


1.1 Introducción .................................................................................................... 9
1.2 Definición de Algoritmo ................................................................................. 9
1.3 Características de los Algoritmos ................................................................ 15
1.4 Buenos hábitos de diseño de Algoritmos .................................................... 16
1.5 Fases de desarrollo de un programa ........................................................... 17
1.6 Tiempo de Ejecución de los Algoritmos ..................................................... 18
1.6.1 Análisis de Algoritmos Iterativos ............................................................... 19
1.6.2 Tiempo de ejecución para instrucciones simples ....................................... 19
1.6.3 Tiempo de ejecución para ciclos simples ................................................... 20
1.6.4 Tiempo de ejecución para ciclos anidados ................................................. 24
1.6.5 Tiempo de ejecución con llamada a métodos ............................................. 30
1.7 Caso de estudio: Biblioteca .......................................................................... 34
1.8 Comparación de tiempos de ejecución ....................................................... 39
1.9 Actividad Independiente: Agenda Telefónica ............................................ 43
1.10 Complejidad Computacional ....................................................................... 48
1.10.1 Notación Asintótica ................................................................................ 49
1.10.2 Notación Big Oh O. ................................................................................ 49
Notación Omega Ω ................................................................................. 51
Notación .............................................................................................. 51
1.10.3
1.10.4
1.10.5 Regla del Límite ..................................................................................... 52
1.10.6 Ordenes de Complejidad ........................................................................ 54
1.11 Actividad Independiente: Concurso Docente ............................................ 62
1.12 Análisis de Algoritmos Recursivos .............................................................. 68
1.12.1 Algoritmos Recursivos ........................................................................... 69
1.12.2 Tiempo de ejecución de algoritmos recursivos ...................................... 73
1.12.3 Resolución de Recurrencias por inducción ............................................ 78
1.12.4 Resolución de Recurrencias por sustitución ........................................... 82
1.13 Análisis de Métodos de Ordenamiento ....................................................... 85
1.13.1 Método de Ordenamiento ShakerSort .................................................... 86
1.13.2 Método de Ordenamiento Burbuja ......................................................... 87
1.13.3 Método de Ordenamiento por Selección ................................................ 87
1.13.4 Método de Ordenamiento Inserción ....................................................... 88
1.13.5 Método de Ordenamiento QuickSort. ..................................................... 89
1.13.6 Método de Ordenamiento ShellSort. ...................................................... 91
1.13.7 Método de Ordenamiento StoogeSort .................................................... 93
1.14 Métodos de Búsqueda................................................................................... 93
1.14.1 Búsqueda Lineal Iterativa ....................................................................... 94
1.14.2 Búsqueda Lineal Limitada ...................................................................... 95
1.14.3 Búsqueda Lineal Iterativa con extremos ................................................ 96
1.14.4 Búsqueda Binaria.................................................................................... 97
1.15 Caso de Estudio: Registro de notas ............................................................. 98
2 ESTRATEGIAS DE PROGRAMACIÓN ........................................................... 107
2.1 Introducción ................................................................................................ 107

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.

Las orientaciones sociológicas y pedagógicas que permitieron la elaboración de este libro y su


viabilidad en el campo profesional y teórico, están orientadas a fortalecer el objetivo o propósito
general de formación del Ingeniero de Sistemas y Computación. Desde la perspectiva
sociológica, quienes comparten los conceptos del análisis de algoritmos, son capaces de
emplearlo para: comprender, explicar, demostrar, solucionar problemas, crear o hacer
representaciones, orientados a compartir su significado con otras personas. Desde la
perspectiva pedagógica, se pretenden establecer estrategias de enseñanza que faciliten a los
estudiantes los aprendizajes requeridos en esta disciplina tanto en lo científico como en lo
formal, cotidiano y viceversa.

El presente libro pretende con su estrategia de enseñanza, la resolución de problemas usando


el análisis de algoritmos. En éste, se agregan actividades que contribuyen a la mejor
comprensión de los conceptos plasmados a lo largo de los capítulos. Al poseer una estructura
coherente, el estudiante podrá trabajar a través de cada uno de ellos, convirtiéndose en un
verdadero actor del proceso de aprendizaje, esto conlleva a que el rol del docente sufra una
profunda transformación, se ha migrado hacia la idea de un consultor. Teniendo en cuenta que
el análisis de algoritmos comprende una amplia variedad de temas, consideramos que es
fundamental que los estudiantes identifiquen la importancia de esta disciplina dentro de su
proceso de formación.

Por un lado el libro recoge nuestra experiencia en la enseñanza de las asignaturas


relacionadas con la algoritmia y la programación, y nos ha permitido ver la importancia que
tiene el disponer de una metodología de trabajo que permita abordar la resolución de los
problemas de una forma simple, coherente y estructurada. Por otro lado este libro es una
versión que retoma los conceptos fundamentales de los libros de análisis de algoritmos en java
y Técnicas de Diseño de Algoritmos en java, en el cual, continua siendo un objetivo de los
autores proporcionar una introducción comprensible y sólida de los elementos fundamentales
del análisis de algoritmos. Teniendo en cuenta las observaciones y las oportunidades de
mejora propuestas tanto por estudiantes como profesores que usaron estos libros, se
realizaron cambios para este libro, con el objetivo de aportar al proceso de formación de los
profesores. El libro se adaptó teniendo en cuenta el micro currículo vigente del curso análisis
de algoritmos I.

El texto fue escrito pensando principalmente en aquellos estudiantes de Ingeniería de Sistemas


y afines que tienen los conocimientos fundamentales de las Matemáticas Discretas y Lógica de
Programación y que deseen aprender los conceptos básicos a tener en cuenta en el análisis de
algoritmos. Este libro se utilizará como referencia para la asignatura Análisis del Algoritmos I
del programa de Ingeniería de Sistemas y Computación de la Universidad del Quindío.

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.

El libro tiene los siguientes objetivos:

 Mostrar la importancia del análisis de algoritmos como área de fundamentación que


aporta al proceso de formación de los estudiantes e ingeniería de sistemas y
computación.

 Presentar a los estudiantes de ingeniería de sistemas y computación, los temas


fundamentales de algoritmia y mostrar la relación existente entre los conceptos
matemáticos con su aplicación en el contexto de las ciencias de la computación.

 Mostrar al estudiante las técnicas básicas utilizadas para establecer la complejidad


computacional y la eficiencia en términos de tiempo de ejecución en algoritmos de
diferente naturaleza, y en paralelo, formalizar la idea de “mayor eficiencia”,
estableciendo el concepto de complejidad desde el punto de vista computacional.

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

 Desarrollar en el estudiante habilidades para la construcción de algoritmos recursivos


correctos, algoritmos ordenamiento y algoritmos de búsqueda entre otros.

 Aplicar a casos de estudio, técnicas de solución de problemas computacionales


complejos, considerados fundamentales en la formación del ingeniero de sistemas y
computación.

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.

En el primer capítulo denominado análisis de algoritmos, se analiza el tiempo de ejecución de


los algoritmos y presentamos su forma de calcularlo. Se muestra una amplia variedad de
ejemplos los cuales permitirán al estudiante conocer y entender los diferentes tiempos de
ejecución que se pueden obtener de los algoritmos. Adicionalmente 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, de los métodos de ordenamiento y los
algoritmos de búsqueda.

En el segundo capítulo llamado estrategias de programación, se trabajará con algoritmos que


se emplean en problemas de optimización en los cuales se pretende maximizar o minimizar
algún valor. Se mostrarán casos considerados típicos como el problema de la mochila, el

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.

El capítulo tercero denominado algoritmos aplicados a grafos y árboles, se muestra el análisis


de los algoritmos más comunes aplicados a las estructuras de datos no lineales. Se analizan
los órdenes de complejidad de los métodos de implementación de grafos y arboles más
importantes. Se explicarán las operaciones fundamentales aplicadas a los árboles y finalmente
se analizan conceptos de árbol n-ário, árbol AVL y la técnica de backtraking.

El capítulo cuarto muestra los algoritmos aplicados a estructuras de datos, se analiza la


implementación de las estructuras de datos lineales con su respectivo análisis de orden de
complejidad. Se explicarán los órdenes de complejidad de los métodos más importantes para
las listas, pilas y colas. Adicionalmente se muestra un caso de estudio que usa estructuras
predefinidas del lenguaje de programación conocidas como ArrayList.

En el último capítulo del libro se muestra la optimización y pruebas de algoritmos, se analizan


algunas técnicas que se pueden aplicar para la optimización de código. Algunas de las técnicas
que se trataran son el desenvolvimiento de ciclos, la fusión de ciclos, la eliminación de
expresiones redundantes, entre otras. También se muestra algunas herramientas
proporcionadas por el lenguaje de programación que permiten la realización de pruebas de
corrección las cuales buscan una mejor calidad en el código de programación y en su
funcionalidad. Adicionalmente, se muestran los conceptos asociados a los límites de la lógica y
de la complejidad computacional, se muestra el planteamiento de problemas asociados a
algoritmos que conforman la clase P, así mismo se muestran casos en los cuales se
consideran problemas intratables denominados clases NP.

8
1 ANÁLISIS DE ALGORITMOS

1.1 Introducción

Actualmente la construcción de aplicaciones informáticas debe responder a requerimientos


muy complejos y de carácter crítico de las organizaciones. La complejidad inmersa en los
proyectos de desarrollo de software está asociada a múltiples fuentes: metodologías utilizadas,
tecnologías de apoyo, capacidad y competencias de las personas, productividad de los equipos
de trabajo, requerimientos cambiantes de los clientes, presupuesto disponible, entre otras.

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.

1.2 Definición de Algoritmo

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.

Un problema es resuelto algorítmicamente, si se puede escribir un programa que pueda


producir la respuesta correcta, de forma que para cualquier posible entrada, el programa puede
ser ejecutado en tiempo finito, teniendo en cuenta los recursos computacionales para
resolverlo.

9
Existen diferentes tipos de algoritmos, entre los cuales tenemos:

 Algoritmos Determinísticos: Es un algoritmo en el cual, cada uno de sus pasos están


claramente definidos, y para cada conjunto de entrada es posible predecir una salida
exacta.

 Algoritmos No Determinísticos: Es un algoritmo en el cual, para un mismo conjunto


de datos de entrada, se pueden obtener diferentes salidas. No se puede previa a la
ejecución de estos algoritmos, afirmar cuál será su resultado.

 Algoritmos Adaptativos: Son algoritmos con alguna capacidad de aprendizaje. Por


ejemplo, los sistemas basados en conocimientos, las redes neuronales entre otras.

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.

La implementación de un algoritmo debe ser en todos los casos:

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

Por lo tanto, un algoritmo se considera bueno si considera los siguientes elementos.

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.

 Que cumpla con el objetivo para el cual está pensado.


 Que resuelva el problema en el menor tiempo posible.
 Que haga uso adecuado de los recursos.
 Que permita identificar posibles errores.
 Que sea fácil de modificar para añadir funcionalidad

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

Considerando los anteriores interrogantes, es posible responder para este caso:

 ¿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 resuelve el problema en el menor tiempo posible? No es posible resolver


el interrogante, para poder responder, se debe comparar con la eficiencia de otros
algoritmos que resuelven el mismo problema.

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

Una segunda solución para determinar si el número es primo, se muestra a continuación:

public boolean esPrimo(int numero)


{
int i;
for ( i = 2 ; i <= numero / 2 ; i++ )
{
if ( numero % i == 0 )
Implementación
{
del Método
break;
}
}
if ( i > numero / 2 )
{
return true;
}

11
else
{
return false;
}
}

Para esta implementación es posible dar respuesta a los siguientes interrogantes:

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

 ¿Resuelve el algoritmo el problema en el menor tiempo posible? Si se compara con el


algoritmo anterior, este algoritmo es mejor dado que una vez que encuentra un número
divisible, termina la ejecución y muestra el mensaje al usuario, teniendo en cuenta la
condición if.

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

A continuación se muestra la implementación de un método el cual lee un número entero


positivo y retorna la cantidad de dígitos que este tiene.

public int calcularCifras (int num)


{
int aux = 0, con = 0;
if(num==0)
{
return 1;
}
for(int i=10;num!=0;i+=10)
Implementación {
del Método con = con + 1;
num = num/i;
i = i-10;
}
if(con!=0)
{
aux = con;
}
return aux;
}

Para la anterior implementación, resuelva los siguientes interrogantes:

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?

A continuación se muestra otra implementación de un método el cual lee un número entero


positivo y retorna la cantidad de dígitos que este tiene.

public int calcularCifras (int numero)


{
int acum = 0;
if(numero<10)
{
return 1;
}
else if(numero>=10 && numero<=99)
{
return 2;
}
Implementación else
del Método {
for(int i=numero; i>10; i-=10)
{
numero= numero/10;
acum++;
if(numero == 0)
{
break;
}
}
return acum;
}
}

Para la anterior implementación, resuelva los siguientes interrogantes:

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:

Y en general, para cualquier (Grimaldi, 1998)

La sucesión de los números armónicos se puede definir como:

( )

public int armonico (int numero)


{

Implementación
del Método

Con bases en la implementación propuesta, responda los siguientes interrogantes.

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:

public int armonico(int numero)


{
double aux, result = 1;
for(int i=2; i<=numero; i++)
Implementación
{
del Método
aux = i;
result = result + (1/aux);
}
return result;
}

Pregunta Respuesta
¿Cuál de los dos algoritmos es
más eficiente?

En la siguiente sección se muestra las características fundamentales y sus algoritmos.

14
1.3 Características de los Algoritmos

Cuando que se desarrolla una solución a un problema desde la perspectiva algorítmica, se


debe procurar en la medida de lo posible que los algoritmos cumplan las siguientes
características:

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

public int calcularCifras (int numero)


{
int acum = 0;
if(numero<10)
{
return 1;
}
else if(numero>=10 && numero<=99)
{
return 2;
}
Implementación else
del Método {
for(int i=numero; i>10; i-=10)
{
numero= numero/10;
acum++;
if(numero == 0)
{
break;
}
}
return acum;
}
}

Para la anterior implementación, resuelva los siguientes interrogantes:

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:

 Eficiencia: Atributo que determina el buen aprovechamiento de los recursos que


utiliza.
 Facilidad de uso: Facilidad para que el usuario interactué con el programa.

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.

1.4 Buenos hábitos de diseño de Algoritmos

Al diseñar un algoritmo y posteriormente implementarlo en un lenguaje de programación, se


deben tener en cuenta buenos hábitos y técnicas de diseño. A continuación se especifican
algunos aspectos a considerar por los equipos de desarrollo de software y por los
desarrolladores de software al momento de diseñar e implementar algoritmos (Ramirez, 2003).

 Analizar el problema: La correcta resolución de un problema viene determinada en


gran medida por el planteamiento inicial. Un planteamiento correcto evitará perder
tiempo en la implementación de los algoritmos.

 Evaluar posibles soluciones: Es necesario conocer los recursos algorítmicos


disponibles y las distintas metodologías de diseño para plantear la solución adecuada.
Una vez hecho esto, deberá optar por la mejor de ellas para su posterior
implementación.

 Diseñar la solución: Esta actividad se refiere a un modelamiento de lo que será la


solución definitiva. Bajo el paradigma orientado a objetos, es posible especificar la
jerarquía de clases con una descripción completa del diagrama de clases. También se
recomienda al diseñar la solución, modelar diagramas de secuencia, los cuales
permiten visualizar la interacción de los objetos por medio del envío de mensajes.

 Documentar el código: Con frecuencia los algoritmos adolecen de documentación y


en algunos casos esta es inexistente. Este tipo de situaciones perjudican el
mantenimiento y reutilización de los mismos, y dificultan su entendimiento. Los
comentarios propiamente dichos son pequeños fragmentos de tipo explicativo,
aclaratorio o de advertencia que se intercalan entre las instrucciones del programa.

Cada programador debe aprender a escribir la especificación de su programa (o sea, la


documentación), antes de escribir el programa (Di Mare, 1998). Documentar permite
entender el programa a medida que crece y también, identificar posibles fuentes de
error. Javadoc es una herramienta creada para tal fin. Está pensado para lograr que la
arquitectura de la solución sea mucho más comprensible, es decir, su formato común
hace que los comentarios escritos sean más comprensibles por otro programador.

Una adecuada documentación requiere agregar comentarios a todas las clases y


métodos que componen el programa. Un comentario debe contener la información
suficiente para que otras personas puedan entender lo que se ha realizado y el porqué
de lo que se ha realizado.

 Manejo de versiones: Es necesario el uso de herramientas que permitan el control de


versiones. Teniendo en cuenta el carácter evolutivo y progresivo de los proyectos de
desarrollo de software.

 Estándar de programación: Es recomendable establecer un lineamiento particular en


la forma en la cual se construyen los programas. Se deben estandarizar por ejemplo

16
los nombres de variables, de las clases y de los métodos, todos estos deben estar
acordes a las tareas que realizan.

Existen estándares para usar convenciones de programación. Las convenciones de


código son importantes para los programadores por un gran número de razones
(Molpeceres, 2001) :

o El 80% del coste del código de un programa va a su mantenimiento.


o Casi ningún software lo mantiene toda su vida el auto original.
o Las convenciones de código mejoran la lectura del software, permitiendo
entender código nuevo mucho más rápidamente y más a fondo.
o Si distribuyes tu código fuente como un producto, necesitas asegurarte de que
está bien hecho y presentado como cualquier otro producto.

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

1.5 Fases de desarrollo de un programa

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.

 La etapa de definición del problema, se enfoca en el entendimiento del espacio del


problema, se identifican las posibles necesidades y restricciones sobre la solución del
problema. Al culminar esta actividad, se debe tener un completo entendimiento del
problema a solucionar.

 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

Cuando se aborda la solución de un problema computacional se tiene la disyuntiva de


seleccionar entre varios algoritmos y la cual se realiza basándose fundamentalmente en los
siguientes aspectos: el algoritmo fácil de entender, codificar, depurar versus el algoritmo que se
ejecute con la mayor rapidez posible, siempre con el objetivo de hacer un uso eficiente de los
recursos de la máquina. Lograr uno de estos objetivos generalmente implica entrar en
contradicción con el cumplimiento del otro, por ello se debe valorar los casos en que se debe
priorizar, el uno o el otro.

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.

 El peor caso se considera como la situación en la cual el algoritmo consume mayor


cantidad de tiempo o mayor cantidad de instrucciones para resolver un problema.
 El mejor caso está relacionado con la situación en la cual el algoritmo consume menor
cantidad de tiempo o menor cantidad de instrucciones para resolver un problema.

Para este libro, tendremos en cuenta el análisis para el peor de los casos.

Tradicionalmente se usan estrategias para la estimación de los tiempos de ejecución, el


siguiente mapa conceptual nos muestra los elementos fundamentales:

Más formalmente las siguientes técnicas se utilizan para estimar el tiempo de ejecución de un
programa (Aho & Ullman, 1995):

 La técnica de benchmarking consiste en comparar dos o más algoritmos con un mismo


conjunto de datos de entrada, se establece cual es el que resuelve el problema de
forma más eficiente; es decir, cual consumió menos tiempo al ejecutarse. Es necesario
que el hardware sobre el cual se realiza la prueba a los algoritmos tenga las mismas
características.

 La técnica de Profiling consiste en asociar a cada instrucción de un programa un


número que representa la fracción del tiempo total tomada para ejecutar esa
instrucción particular. Una de las técnicas más conocidas (e informal) es la Regla 90-
10, que afirma que el 90 % del tiempo de ejecución, se invierte en el 10 % del código

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

1.6.1 Análisis de Algoritmos Iterativos

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.

Entendiendo por operaciones elementales como aquellas operaciones cuyo tiempo de


ejecución se puede acotar superiormente por una constante (Guerequeta & Vallecillo, 2000).
Así, se consideran como operaciones elementales:

Nombre Operadores o instrucciones


Operaciones aritméticas básicas +,-,*,/,%
Asignaciones a variables =
Comparaciones lógicas o relacionales &&,||,<,<,>=,=<,==,!=
Acceso a estructuras de datos estáticas y dinámicas [], [][]
Parámetros que llegan a los métodos método(variable valor)
Instrucciones de salto break, continue
Retorno de valores return
Instrucciones condicionales if, if-else
Creación de Objetos new
Expresiones que con incrementos y decrementos ++, --
Referencias a objetos null, object
Operadores de acumulación +=,-=

A continuación se muestra la forma en la cual se realizará el cálculo del tiempo de ejecución


para instrucciones simples.

1.6.2 Tiempo de ejecución para instrucciones simples

El tiempo de ejecución para instrucciones simples consiste en el conteo de operaciones


elementales por línea de código. No se considera para este tipo de conteo la declaración de
variables, pues el conteo se hará sobre los variables en ejecución dinámica. A continuación se
mostrarán ejemplos en los cuales se calcula el tiempo de ejecución de instrucciones sencillas
dentro de los métodos.

Como estrategia se analizará el método en términos de la cantidad de instrucciones que se


llevan a cabo.

19
Objetivo: Comprender la estimación del tiempo de ejecución para
Ejemplo
métodos que tienen operaciones elementales.

public int metodo1( int n )


{
int x,y,z;
Método x = 2;
y = x++;
z = y + x;
return z;
}
El tiempo de ejecución de una secuencia consecutiva de
instrucciones se calcula sumando cada línea de código que posee
el método, independiente de la cantidad de operaciones que se
ejecuten en la línea. Adicinalmente se debe considerar el parámetro
Análisis del
que llega al método el cual se cuenta como una instrucción.
tiempo de
ejecución
int n, se ejecuta 1 vez
x = 2, se ejecuta 1 vez
y = x++, se ejecuta 1 vez
z = y + x, se ejecuta 1 vez
return z, se ejecuta 1 vez
T(n) T(n) = 5

ACTIVIDAD

Estime y argumente el cálculo del tiempo de ejecución para el siguiente método:

Ejercicio Objetivo: Estimar el tiempo de ejecución del método.

public int ejercicio( int x, int y, int z )


{
int r;
r = 0;
Método x += 2;
y = x + 3;
z = x + 2;
r = x + y + z;
return r
}
Explicación del
tiempo de
ejecución

T(n)

1.6.3 Tiempo de ejecución para ciclos simples

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.

public void metodo ( int n)


{
int i, x, y;
i = 0;
while ( i < n )
Método
{
x = i + 3;
y = x + 1;
i++;
}
}
int n Se ejecuta 1 vez
i = 0 Se ejecuta 1 vez
while ( i < n ) Se ejecuta n + 1 veces
x = i + 3 Se ejecuta n veces
Análisis del
Tiempo de y = x + 1 Se ejecuta n veces
ejecución i = i + 1 Se ejecuta n veces

La instrucción while(i < n) se repite n+1 veces, esto teniendo


en cuenta la última comparación del ciclo. Las instrucciones que
hay dentro del ciclo, se repiten n veces.

T(n) T(n) = 4n+3.

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.

Objetivo: Crear habilidad para calcular el tiempo de ejecución del


Ejemplo
método.

public void metodo ( int x, int y, int n )


{
for ( int i = 3 ; i <= n ; i++ )
Método {
x = i * j;
y = w * x;
}
}

int x Se ejecuta 1 vez


int y Se ejecuta 1 vez
Análisis del int n Se ejecuta 1 vez
Tiempo de i = 3 Se ejecuta 1 vez
ejecución i <= n Se ejecuta ((n + 1) - 3) + 1 veces
x = i * j Se ejecuta (n-2) veces
y = w * x Se ejecuta (n-2) veces
i++ Se ejecuta (n-2) veces

T(n) T(n) = 4+(n-1)+3(n-2) = 4n-3, n >=2

Es frecuente que dentro de los ciclos se implementen decisiones, a continuación se muestra un


caso el cual el condicional if se considera siempre como verdadero y con ello su peor caso.

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

Cuando se analiza la instrucción if(i<n), se asume para el peor


caso que ésta siempre será verdadero, lo que implica una mayor
cantidad de instrucciones que se deben procesar.
int n Se ejecuta 1 vez
Análisis del int arreglo[] Se ejecuta 1 vez
Tiempo de i = 0 Se ejecuta 1 vez
ejecución
i <= n Se ejecuta (n+1) + 1 veces
if ( i < n ) Se ejecuta (n+1) veces
temp = arreglo[ i ] Se ejecuta (n+1) veces
arreglo[i-1] = arreglo[i] Se ejecuta (n+1) veces
i++ Se ejecuta (n+1) veces

T(n) T(n) = 4 + 5(n+1) = 5n+9, n >=-1

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.

Cuando if o else tienen igual número de líneas de implementación, se puede escoger


cualquiera de los dos. Para este ejemplo el valor del parámetro n, debe ser n > 1.

Objetivo: Crear habilidad en el cálculo de tiempo de ejecución para


Ejemplo
condicionales.
public void metodo( int n, int a[] )
{
int temp, x, k=0, i;
for ( i = 0 ; i < n - 2 ; i++ )
{
if ( a[i] < a[i+1] )
{
temp = a[i];
a[i+1] = temp;
Método
}
else
{
temp = a[i+1];
x = a[i];
k++;
}
}
}

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.

T(n) T(n)= 6n-7, para n>1.

ACTIVIDAD

Estime y argumente el cálculo del tiempo de ejecución para los siguientes métodos:

Ejercicio Objetivo: Estimar el tiempo de ejecución del método.

public void metodo( int n, int a[] )


{
int temp, x, i;
i = 0;
while(i <= n+3)
{
if ( a[i-1] < a[i+1] )
{
temp = a[i];
Método
a[i+1] = temp;
x = a[i];
}
else
{
x = a[i];
}
i++;
}
}
Explicación del
tiempo de
ejecución

T(n) T(n) =

Estimar el tiempo de ejecución del siguiente método, analizando el caso en el cual el


condicional if (w % i == 1) es verdadero en la última iteración.

Objetivo: Conocer cómo se calcula el tiempo de ejecución cuando


Ejercicio
un ciclo tiene una instrucción de salto.
public void ejercicio ( int n, int a[], int x )
{
for ( int i = 3 ; i < n + 2 ; i++ )
{
if ( x % i == 1)
{
Método
a[i] = i * 2;
break;
}
a[i] = 5;
}
}

23
Análisis del
Tiempo de
ejecución
T(n)

Estimar el tiempo de ejecución del siguiente método, analizando el caso en el cual el


condicional if (w % i == 1) es verdadero en la última iteración.

Objetivo: Conocer cómo se calcula el tiempo de ejecución cuando


Ejercicio
un ciclo tiene una instrucción de salto.

public void ejercicio ( int n, int a[], int x )


{
int j = 0;
for ( int i = 0 ; i < n ; i++ )
{
if ( x % i == 0)
{
Método a[i] = i * 2;
j++;
break;
}
a[i] = 5;
a[j] = i * 2;
x--;
}
}
Análisis del
Tiempo de
ejecución
T(n)

1.6.4 Tiempo de ejecución para ciclos anidados

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.

Objetivo: Crear habilidad para calcular el tiempo de ejecución del


Ejemplo
método cuando se utilizan ciclos anidados.

public void metodo ( int n, int a1[], int a2[] )


{
for ( int i = 0 ; i < n; i++ )
{
Método for ( int j = 0 ; j < n ; j++ )
{
a1[ j ] = a2[ i ];
}
}
}

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.

Objetivo: Crear habilidad para calcular el tiempo de ejecución de


Ejemplo
un método cuando este utiliza ciclos anidados.

public void metodo( int n )


{
int x = 0;
for ( int i = 0 ; i < n ; i++ )
{
for ( int j = 0 ; j < n; j++ )
Método {
for ( int k = 0 ; k < n ; k++ )
{
x++;
}
}
}
}
Se analiza el ciclo más interno, a éste lo llamaremos T1(n).
 T1(n)= 3n + 2

A continuación, se analiza ciclo del medio, lo llamaremos T2(n).


Tiempo de  T2(n) = 2n+2
ejecución
Se calcula el tiempo de ejecución de los dos ciclos más internos, al
cual llamaremos T3(n).
 T3(n)= ((3n+2)*n) + 2n+2= 3n +4n+2
2

Finalmente me estima el tiempo de ejecución total del método


T(n)
2 3 2
T(n) = ((3n +4n+2)*n) + 2n+2+2= 3n +4n +4n+4

ACTIVIDAD

Estime y argumente el cálculo del tiempo de ejecución para el siguiente método:

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)

Estime y argumente el cálculo del tiempo de ejecución para el siguiente método:

Objetivo: Crear habilidad en el cálculo de tiempo de ejecución para


Ejercicio
métodos.

public void ejercicio ( int n )


{
int i, j, temp, resultado;
temp = 0;
resultado = 0;
for ( i = n ; i >= 0 ; i-- )
Método {
for ( j = n ; j > 0 ; j-- )
{
temp = 4;
resultado = 1;
}
}
}
Análisis del
Tiempo de
ejecució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:

 Límite inferior o superior del ciclo.


 Tope máximo o mínimo del ciclo.
 Incrementos o decrementos de la variable que controla las iteraciones.

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.

Objetivo: Crear habilidad en el cálculo de tiempo de ejecución para


Ejemplo
métodos con tiempo de complejidad logarítmica.

public void metodo ( )


{
int temp = 0, x = 0, y = 1, i;
i = 32;
while( i > 0 )
Método {
x++;
temp++;
y = y * 1;
i = i / 2;
}
}

temp = 0 Se ejecuta 1 vez


x = 0 Se ejecuta 1 vez
y = 1 Se ejecuta 1 vez
i = 32 Se ejecuta 1 vez
while(i > 0) Se ejecuta log2(i) + 2 veces
x++ Se ejecuta log2(i) + 1 veces
Análisis del temp++ Se ejecuta log2(i) + 1 veces
Tiempo de y = y * 1 Se ejecuta log2(i) + 1 veces
ejecución i = i/2 Se ejecuta log2(i) + 1 veces

Se realizan 6 comparaciones en el ciclo while(i > 0) más la


última comparación, basándose en la forma como cambia el
tamaño de i. En este caso encontramos una relación entre el
número de comparaciones del ciclo, (6 comparaciones) y el valor de
5
i = 32. Se puede determinar que 2 = 32, (5 es logaritmo en base 2
de 32), entonces las veces que se repite el ciclo es log2(i)+1.

T(n) T(n) = 5(log2(i) + 1) + 5

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.

public void metodo ( int n)


{
int x = 0, i;
i = 1;
Método while( i < n )
{
x++;
i= i * 2;
}
}
int n Se ejecuta 1 vez
x = 0 Se ejecuta 1 vez
Análisis del
i = 1 Se ejecuta 1 vez
Tiempo de
while(j < n) Se ejecuta log2 (n) +1 veces
ejecución
x + + Se ejecuta log2 (n) veces
i = i * 2 Se ejecuta log2 (n) veces

T(n) T(n) = 3(log2(n)) + 4

ACTIVIDAD

Calcular el tiempo de ejecución para el siguiente algoritmo iterativo.

Objetivo: Crear habilidad en el cálculo de tiempo de ejecución para


Ejercicio
métodos.

public void ejercicio ( )


{
int x = 0, y = 1, w = 0, i;
i = 128;
while( i > 0 )
{
x++;
Método y = y * 1;
i = i / 2;
}
for ( i = n ; i > 0 ; i-- )
{
w++;
y++;
}
}

Análisis del
Tiempo de
ejecución

T(n)

Calcular el tiempo de ejecución para el siguiente algoritmo iterativo.

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)

Explique detenidamente el funcionamiento del siguiente algoritmo y calcule su tiempo de


ejecución. Deduzca el problema que resuelve y compare el mismo con otras implementaciones.

public boolean misterio(int numero)


{
int raiz;
int valor = 0;
if (numero==1 || numero==2 || numero==3){
return true;
}
else
{
raiz = (int)Math.sqrt(numero);
for(int i=2; i<=raiz; i++)
{
for(int j=i; j<=raiz; j+=i)
{
if(numero%j == 0)
Método {
valor = 1;
break;
}
}
}
}
if (valor == 0)
{
return true
}
else
{
return false;
}
}

29
Análisis del
Tiempo de
ejecución
T(n)

1.6.5 Tiempo de ejecución con llamada a métodos

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.

Para la estimación del tiempo de ejecución realizando llamadas a métodos, es necesario


analizar el tiempo de cada uno de los métodos, y de esta forma establecer cuál es el peor caso,
siempre considerando valores enteros grandes.

Objetivo: Crear habilidad en el cálculo de tiempo de ejecución para


Ejemplo
llamados a métodos con un tiempo de ejecución predefinido.

public void metodo( int n, int x, int y)


{
int i;
for ( i = 0; i < n; i++ )
{
if(x == y)
{
metodo1();
Método }
else
{
metodo2()
}
}

 metodo1() —> T(n) = 3n + 10


}

 metodo2() —> T(n) = 2n + 500

Dado que el metodo1 es mayor que el metodo2 (para valores


Análisis del
grandes), se establece entonces el tiempo de ejecución con el
Tiempo de
metodo1. Se procede de la misma forma en la cual se estaba
ejecución
calculando el tiempo de ejecución.

El tiempo de ejecución es: T(n)= ((3n + 10) * n) + (3n + 2)+ 3


2
T(n) T(n)= 3n +13n+5

Es frecuente cuando se usan métodos el uso de instrucciones condicionales, a continuación se


muestra un ejemplo que utiliza la instrucción switch.

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.

Objetivo: Crear habilidad en el cálculo de tiempo de ejecución para


Ejemplo
llamados a métodos en que tengan la instrucción switch.
public void metodo( )
{
switch (0)
{
case 0 : metodo1();
case 1 : metodo2();
case 2 : metodo3();
}
if(m==x+2)
Método {
metodo4();
}

 metodo1() —> T(n) = n + 3


}

 metodo2() —> T(n) = n2 + 5


 metodo3() —> T(n) = 4n2
 metodo4() —> T(n) = 4n3 + 8

Dado que el valor del parámetro del switch es cero, se evalúan


Análisis del todos los case y por lo tanto el tiempo de ejecución es la sumatoria
Tiempo de de cada uno de los tiempos de ejecución.
ejecución
3 2
T(n) T(n) = 4n + 5n + n + 18

ACTIVIDAD

Calcular el tiempo de ejecución para el siguiente método.

Objetivo: Crear habilidad en el cálculo de tiempo de ejecución para


Ejercicio
llamados a métodos.

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

 metodo1() —> T(n) = n4


}

 metodo2() —> T(n) = n2


 metodo3() —> T(n) = n5 + 5
Análisis del
Tiempo de
ejecución
T(n)

Calcular el tiempo de ejecución para el siguiente método.

Objetivo: Crear habilidad en el cálculo de tiempo de ejecución para


Ejercicio llamados a métodos en los cuales se encuentre la instrucción
switch.

public void ejercicio( )


{
switch (2)
{
case 0 : metodo1();
case 1 : metodo2();
Método case 2 : metodo3();
}
metodo4();

 metodo1() —> T(n) = 2n +


}

 metodo2() —> T(n) = 10n2


30

 metodo3() —> T(n) = n2 +


+ 5

 metodo4() —> T(n) = n3 +


n + 3
5
Análisis del
Tiempo de
ejecución

T(n)

Calcular el tiempo de ejecución para el siguiente método.

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

 metodo1() ---> T(n)= 3n + 100


}

 metodo2() ---> T(n)= 3n + 120


 metodo3() ---> T(n)= 2n2 + 10
Análisis del
Tiempo de
ejecución
T(n)

Calcular el tiempo de ejecución para el siguiente método.

Objetivo: Crear habilidad en el cálculo de tiempo de ejecución para


Ejercicio llamados a métodos en los cuales se encuentre la instrucción
switch.
public void ejercicio( )
{
switch (0)
{
case 0 : metodo1();
Método case 1 : metodo2();
break;
case 2 : metodo3();
case 3 : metodo4();
}
}
Análisis del
Tiempo de
ejecución

T(n)

33
1.7 Caso de estudio: Biblioteca

A continuación se muestra un caso de estudio aplicado, en el cual se aplican actividades de un


proceso de desarrollo de software. El objetivo del caso es implementar los métodos más
importantes y su posterior análisis de tiempo de ejecución. A continuación se describe el caso
de estudio.

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.

Inicialmente se realiza una descripción de los requerimientos funcionales para la aplicación.

a) Requerimientos funcionales

NOMBRE R1 – Agregar libro al listado general de libros.


RESUMEN Permite agregar un nuevo libro.
ENTRADAS
Isbn, valor, codigo Editorial, nombre Editorial.
RESULTADOS
Un nuevo libro ha sido agregado.

NOMBRE R2 - Contar cantidad de libros por cada editorial.


RESUMEN Permite contar la cantidad la cantidad de libros que tiene cada editorial.
ENTRADAS
RESULTADOS
Un mensaje con la cantidad de libros por editorial.

NOMBRE R3- Mostrar el listado de todos los libros.


RESUMEN Genera un listado con todos los libros.
ENTRADAS
RESULTADOS
Listado con todos los libros

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.

b) Identificación de las clases

Las clases principales para el caso de estudio se identifican y se describen a continuación:

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.

Se continúa con la implementación de los métodos de las clases.

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.

Implementación de los métodos


public Biblioteca()
{
contador1=contador2=contador3=0;
mLibro=new ArrayList<Libro>();
}

T(n) =

void setMLibro(ArrayList <Libro> mLibro)


/**
* Permite fijar el array de libros
* @param mLibro se obtiene del archivo
*/
public void setMLibro(ArrayList <Libro> mLibro)
{
this.mLibro=mLibro;
}

T(n) =

ArrayList <Libro> getMLibro()


/**
* Devuelve el valor del array que contiene los libros
* @return ArrayList <Libro>
*/
public ArrayList <Libro> getMLibro()
{
return mLibro;
}

T(n) =

agregarLibro( String codEditorial, String isbn, double valor)

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

Objetivo: Generar habilidad en la construcción de métodos para


Ejercicio posteriormente calcular su tiempo de ejecución.

public int mayorPosicion()


{
Escriba un
método que
retorne la
posición en la
cual se
encuentra el
libro con el
mayor valor.

Análisis del
Tiempo de
ejecución

T(n)

Objetivo: Generar habilidad en la construcción de métodos para


Ejercicio posteriormente calcular su tiempo de ejecución.

public String mostrarIsbnPosicionesImpares()


Escriba un {
método que
muestre el isbn
de los libros que
se encuentran
las posiciones
impares del
ArrayList.

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.

public String mostrarPosicionesIsbn()


{

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)

Objetivo: Generar habilidad en la construcción de métodos y


Ejercicio
posteriormente calcular su tiempo de ejecución.
public String sumarElementosPares()
Escriba un {
método que
permita
determinar el
valor total de la
mercancía de
los elementos
que se
encuentran en
las posiciones
pares del
ArrayList. }

Análisis del
Tiempo de
ejecución

T(n)

1.8 Comparación de tiempos de ejecución

La comparación de los algoritmos proporciona una medida concreta de cuando un algoritmo es


más eficiente que otro de acuerdo a un conjunto de datos de entrada. El siguiente cuadro
permite establecer una comparación del crecimiento de algunas funciones, en las cuales se
muestra su comportamiento y diferencias con otras funciones de acuerdo a un valor n
particular.

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.

Los siguientes ejemplos permiten comparaciones de los tiempos de ejecución. Considerando


su crecimiento, se recomendara alguno de ellos considerando que en cada caso se resuelve el
mismo problema,

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

Como se pudo observar, es fundamental estimar y entender el comportamiento de los


algoritmos de acuerdo a su crecimiento. Una vez que se tenga claro cuál es la cantidad de
datos a procesar, es necesario tomar la decisión de cual algoritmo recomendar.

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.

A continuación se muestran dos implementaciones diferentes del mismo problema.

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

Una segunda implementación del máximo común divisor se muestra a continuación.

public int mcd(int a, int b)


{
int resultado = 1;
for(int i = b; i > 0; i--)
{
Método 2 if(a%i==0 && b%i==0)
Máximo común {
Divisor resultado = i;
break;
}
}
return resultado;
}

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.

A b resultado i i>0 If(a%i==0 b%i==0)


30 18 1 18 18>0 30%18==0 && 18%18==0
17 17>0 30%17==0 && 18%17==0
16 16>0 30%16==0 && 18%16==0
15 15>0 30%15==0 && 18%15==0
14 14>0 30&14==0 && 18%14==0
.. .. … .. .. ……………
7 7>0 30%7==0 && 18%7==0
6 6>0 30%6==0 && 18%6==0
6

En el siguiente cuadro se observan las instrucciones que son necesarias para la deducción del
MCD del primer algoritmo.

Valora Valorb While(valora!=valorb) If(valora<valorb)


30 18 30!=18 30<18
12 18 12!=18 12<18
12 6 12!=6 12<6
6 6 6!=6

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.

a B resultado i i>0 If(a%i==0 b%i==0)


38 4 1 4 4>0 38%4==0 && 4%4==0
3 3>0 38%3==0 && 4%3==0
2 2 2>0 38%2==0 && 4%2==0

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.

Valora Valorb While(valora != valorb) If(valora < valorb)


38 4 38 != 4 38<4
34 4 34 !=4 34<4
30 4 30 != 4 30<4
26 4 26 != 4 26<4
.. .. …. ..
6 4 6 != 4 6<4
2 4 2 != 4 2<4
2 2 2 != 2 ….

De acuerdo al anterior conjunto de datos de entrada, el primer algoritmo no es más eficiente


que el segundo algoritmo, lo que entra en contradicción con lo concluido anteriormente. Este
caso claramente nos muestra que para poder decir cuando un algoritmo es más eficiente que
otro, es necesario analizarlo por medio de diferentes conjuntos de datos de entrada.

1.9 Actividad Independiente: Agenda Telefónica

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

NOMBRE R1- Agregar Persona


RESUMEN

ENTRADAS

RESULTADOS

NOMBRE R2- Eliminar Persona


RESUMEN

ENTRADAS

RESULTADOS

NOMBRE R3- Mostrar el listado de todas las personas


RESUMEN

ENTRADAS

RESULTADOS

NOMBRE R4- Buscar una persona por nombre


RESUMEN
ENTRADAS
RESULTADOS

R5- generar un listado de personas que están cumpliendo años el


NOMBRE
día de hoy
RESUMEN
ENTRADAS
RESULTADOS

b. Identificar las entidades o clases.

ENTIDAD DEL MUNDO DESCRIPCIÓN


AgendaTelefonica
Persona
Fecha

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.

En la clase AgendaTelefonica puedes observar que se ha hecho uso de ArrayList. Un


ArrayList es una estructura contenedora dinámica que crece o disminuye dinámicamente.

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;

public class AgendaTelefonica implements Serializable


{
ArrayList <Persona> listaPersona ;

int contadorContactos, contadorTotal, contadorCumpleaños;

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

public AgendaTelefonica( ArrayList <Persona> listaPersona )


{
this.listaPersona = ____________________;
}

public void agregarPersona( String cedula, String nombre,


String direccion, int dia, int mes,
int anio, ArrayList telefono )
{
miPersona.____________________ ( cedula );
miPersona.____________________ ( nombre );
miPersona.setDireccion ( ___________________ );
miPersona.setMiFechaNacimiento ( ________________ );
miPersona._____________________( teléfono );
listaPersona.add(_________________);
}

/**
* @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();

for( i=0 ; i < listaPersona.size() ; i++ )


{
String infoT = listaPersona.get(i).getTelefono().
toString();
String info = listaPersona.get( i ).getCedula() + "" +
__________________________________+ infoT;
contactos[ i ] = info;
}

contactos[ i ] = "En total: " + contadorTotal + " contactos";


return contactos;
}

/**
* 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>();

/** Devuelve el contador de contactos */


public int getContadorContactos()
{
________________________

/** Devuelve el contadorTotal */


public int getContadorTotal()
{
__________________________

/** Contador de personas que están cumpliendo años */


public int getContadorCumpleaños()
{
_________________________

/** Devuelve la información de las personas que están cumpliendo


* años */

public String[] obtenerContatosCumpleaños()


{
String[] contactos;
int i;
GregorianCalendar go = new GregorianCalendar();

//sacamos los valores del dia, mes y año


int dia = go.get ( Calendar.DAY_OF_MONTH );
int mes = go.get ( Calendar.MONTH ) + 1;
contadorCumpleaños = 0;

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

String[] buscarContatosNombre(String nombre)

int getContadorContactos()

int getContadorTotal()

String[] obtenerContatosCumpleaños()

void eliminarPersona(String cedula)

1.10 Complejidad Computacional

En la sección anterior se ha trabajado el concepto de tiempo de ejecución de forma


experimental sobre cada uno de los algoritmos expuestos. Este análisis resulta particularmente
interesante y útil cuando debemos decidir cuál algoritmo escoger bajo unos criterios razonables
y sustentables.

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

 Criterios orientados a minimizar el costo de desarrollo: claridad, sencillez y facilidad de


implantación, depuración y mantenimiento.
 Criterios orientados a disminuir el costo de ejecución: tiempo de procesador y cantidad
de memoria utilizados.

Cuando se resuelve un problema, normalmente hay necesidad de elegir entre varios


algoritmos, típicamente existen dos objetivos que suelen contradecirse (Aho, Ullman, &
Hopcrof, The Design and Analysis of Computer, 1994):

 Que el algoritmo sea fácil de entender y codificar


 Que el algoritmo use eficientemente los recursos, y en especial, que se ejecute con la
mayor rapidez posible.

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:

 El tiempo de ejecución T(n), suele ser difícil de calcular exactamente.


 Si cada operación necesita una fracción de tiempo muy pequeña de tiempo para
ejecutarse, no se precisa una exactitud en el cálculo de T(n).

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.

1.10.1 Notación Asintótica

Debido a que la eficiencia de un algoritmo no se expresa en alguna unidad de tiempo


determinada debido a que se escoge una medida arbitraria T(n), se ha introducido una notación
especial llamada notación asintótica, porque tiene que ver con el comportamiento de dichas
funciones en el límite, o sea, para valores suficientemente grandes en sus parámetros.

1.10.2 Notación Big Oh O.

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:

 El argumento n es estrictamente positivo.


 La evaluación de la función T(n) nunca es negativa, para cualquier argumento n.

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)

Para determinar el orden de complejidad de un algoritmo a partir de su función de tiempo, se


eliminan todos los términos excepto el de mayor grado y se eliminan todos los coeficientes del
término mayor. Lo anterior es debido a que al aumentar el tamaño de la entrada, es más
significativo el incremento en el término de mayor orden, que el incremento de los demás
términos de la función. En conclusión, la notación O(n) nos permite conocer lo verdaderamente
importante en la complejidad de un algoritmo, eliminando los pequeños factores de él.

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

A continuación se muestran convenciones o reglas para escribir la notación O.

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

 Términos de Orden Inferior

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

1.10.3 Notación Omega Ω

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

La función f  a (g) si , para alguna constante c tal que 0 < c < .

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

Cuando , la evaluación de este límite tiende a cero, por lo tanto está en el


orden de , pero no pertenece al orden de .

 Ejemplo 5

La función es

, por L’ Hôpital

, simplificando

, por L’ Hôpital

, por L’ Hôpital

, simplificando

, simplificando

Cuando , la evaluación de este límite tiende al infinito , por lo tanto no


es del orden de . Entre más complejo sea un algoritmo, se puede afirmar que este
es menos eficiente.

1.10.6 Ordenes de Complejidad

Es importante entonces definir el orden de la complejidad de un algoritmo. A continuación, se


muestra la explicación de los órdenes de complejidad frecuente en el análisis de algoritmos,
cada uno de ellos tendrá su explicación teórica y ejemplos de aplicación.

 Complejidad Constante O(1)

En este orden de complejidad el tiempo de ejecución del algoritmo es independiente del


tamaño de la entrada, así se tenga un valor considerado como muy grande, siempre se
ejecutará en forma constante. Operaciones de este tipo son las operaciones aritméticas
básicas, asignaciones a variables, llamadas y retorno a métodos, comparaciones lógicas,
acceso a estructuras de datos estáticas y dinámicas.

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

En (Preiss, 1998), se definen tres axiomas asociados a la complejidad computacional


constante, a continuación se da una definición de cada uno de ellos:

 En su primer axioma establece que, el tiempo requerido para recuperar un operando


desde memoria es constante y así mismo el tiempo requerido para almacenar un
resultado en memoria es constante.
 En un segundo axioma, el tiempo requerido para realizar operaciones aritméticas, tales
como la adición, sustracción, multiplicación, división y comparación, son todas
constantes.
 El tercer axioma, dice que el tiempo requerido para llamar un método es constante y el
tiempo requerido para retornar desde un método también es constante. Finalmente el
tiempo requerido para pasar un argumento a un método es el mismo que el tiempo
requerido para almacenar un valor en memoria.

A continuación se muestra un ejemplo con instrucciones consideradas de orden de O(1).

Objetivo: Entender como se deduce el orden de complejidad para


Ejemplo
un algoritmo con orden constante.
public void constante( int a[], int n )
{
int cantidad = n;
if ( a[n-1] == a[n-2] )
{
cantidad++;
Método
}
else
{
cantidad--;
}
}
int a[], O(1)
Análisis del int n, O(1)
Orden de int cantidad = n, O(1)
Complejidad if (a[n - 1] == a[n - 2]) O(1)
cantidad++ O(1)
cantidad-- O(1)
Orden de El método llamado constante, tiene orden de complejidad de O(1).
complejidad

 Complejidad Logarítmica O(logn)

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

El método tiene una complejidad logarítmica, a continuación mostraremos su análisis mediante


la implementación de un método.

Objetivo: Entender como se deduce el orden de complejidad para


Ejemplo
un algoritmo con orden logarítmico.

public void orden ( )


{
int temp = 0, x = 0, y = 1, i;
i = 32;
while( i > 0 )
Método {
x++;
temp++;
y = y * 1;
i = i / 2;
}
}
temp = 0 Se ejecuta 1 vez
x = 0 Se ejecuta 1 vez
y = 1 Se ejecuta 1 vez
i = 32 Se ejecuta 1 vez
while(i > 0) Se ejecuta log2(i) + 2 veces
x++ Se ejecuta log2(i) + 1 veces
Análisis del temp++ Se ejecuta log2(i) + 1 veces
Orden de y = y * 1 Se ejecuta log2(i) + 1 veces
Complejidad i = i/2 Se ejecuta log2(i) + 1 veces
Se realizan 6 comparaciones en el ciclo while(i > 0) más la
comparación que rompe el ciclo. Esto basado en la forma como
cambia el tamaño de i. Para este caso encontramos una relación
entre el número de comparaciones del ciclo, (6 comparaciones) y el
5
valor de i = 32. Se puede determinar que 2 = 32, donde 5 es el
logaritmo en base dos de 32. Por lo tanto el número de veces que
se repite el ciclo es log2(i)+1.

Orden de
O(log2(i) )
complejidad

 Complejidad Lineal O(n)

La complejidad lineal tiene un orden mayor que la complejidad constante y la complejidad


logarítmica, este orden de complejidad normalmente se obtiene cuando se tiene un ciclo
sencillo. A continuación se muestran algunas funciones con orden lineal.

A continuación se muestran algunas funciones con orden lineal.

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.

Objetivo: Entender como se deduce el orden de complejidad para


Ejemplo
un algoritmo con orden lineal.

public int orden2( int prueba[] )


{
int sumatoria = 0;

for (int i = 0; i < prueba.length; i++)


Método
{
sumatoria += prueba[i];
}
return sumatoria;
}

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

 Complejidad nlogn O(nlogn)

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)

A continuación se muestra la implementación de un método con el orden de complejidad que


se está analizando.

Objetivo: Comprender como se deduce el orden de complejidad


Ejemplo
para un algoritmo con orden nlogn.

public void orden( int n )


{
int x, i, j;
x = 0;
for ( i = 0 ; i < n ; i++ )
Método {
for ( j = 1 ; j < n ; j*=2 )
{
x = x+2;
}
}
}

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 tiene un orden mayor que la complejidad constante, logarítmica,


lineal y nlog(n). Este orden de complejidad normalmente se obtiene cuando se tienen dos ciclos
anidados. A continuación se muestran algunas funciones con orden cuadrático.
2
Complejidad (n )
2
n + nlog(n) +n+10
2
n + log(n)
2 25
n +2

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.

Objetivo: Comprender como se deduce el orden de complejidad


Ejemplo
para un algoritmo con orden cuadrático.
public int orden(int prueba[][])
{
int sumatoria = 0;
for (int i = prueba.length-1; i >=0 ; i--)
{
for(int j = prueba[0].length-1; j>=0; j--)
Método {
sumatoria += prueba[i][j];
}
}
return sumatoria;
}

Se tienen dos ciclos anidados, los cuales iteran la misma cantidad


Análisis del de veces. Van desde la última posición de un arreglo, hasta la
Orden de primera posición del arreglo. Cuando se tienen 2 ciclos anidados, la
Complejidad ejecución del interno depende de la cantidad de veces que lo
permita el externo.

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

A continuación se muestra la implementación de un método con el orden de complejidad que


se está analizando.

Objetivo: Comprender como se deduce el orden de complejidad


Ejemplo
para un algoritmo con orden cúbico.

public void cubica( int a[] )


{
int x = 0;
for (int i = 0 ; i < a.length ; i++ )
{
for (int j = 0 ; j < a.length; j++ )
Método {
for (int k = 0 ; k < a.length ; k++ )
{
a[i] = i + k - j;
}
}
}
}
Encontramos tres ciclos anidados, las variables i, j, k, se
Análisis del
inicializan en cero y se incrementan de uno en uno, estas
Orden de
iteraciones llegan hasta el tope máximo del tamaño del arreglo.
Complejidad
Cada ciclo se ejecuta n veces, al estar anidado, se tiene un orden
3
de complejidad O(n ).
Orden de 3
O(n )
Complejidad

 Complejidad Exponencial

La complejidad exponencial es la menos deseable de todas las complejidades, tiene un orden


mayor que la complejidad constante, logarítmica, lineal, nlog(n), cuadrática y la cúbica. Este
orden de complejidad normalmente se obtiene en algunos algoritmos recursivos como el de
Fibonacci.

A continuación se muestran algunas funciones con exponencial.


n
Complejidad (2 )
n 3
2 + n +n
n+5
2 + log(n)
n 3 2 45
2 +n +n +7
n+2 100
2 +n

A continuación se muestra la implementación de un método con el orden de complejidad que


se está analizando.

Objetivo: Comprender como se deduce el orden de complejidad


Ejemplo
para un algoritmo con orden 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

Los algoritmos donde aparece esta complejidad se llaman algoritmos de explosión


combinatoria y sólo es recomendable utilizarlos con un conjunto de datos de entrada muy
pequeño.

ACTIVIDAD

Dado el siguiente método explique qué problema resuelve y determine su tiempo de ejecución.

Ejercicio public boolean misterio( int numero )


{
for(int i = 2; i<=(int)Math.sqrt(numero); i++)
{
if ( numero % i == 0 )
{
Método return false;
}
}
return true;
}

Análisis del
Orden de
Complejidad

Orden de
complejidad

Dado el siguiente método explique qué problema resuelve y determine su orden de


complejidad.

Ejercicio public int misterio(int matriz [][])


{
int i, j, n = matriz.length, suma = 0;
for(i = 0; i < n; i++)
{
Método for(j = 0; j < n; j++)
{
if (!((i==0)||i==(n-1))&&
!(j==0||j==(n-1)))

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 orden de


complejidad.

Ejercicio public int misterio (int num)


{
int i,col = num, contador=0;
if(num==0)
{
return num;
}
for(i = 10; num!=0; i+=10)
{
Método contador+= (num%i);
num/=i;
i-=10;
}
if(contador!=0)
{
return contador;
}
}
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.

Ejemplo public int misterio( int n, int x )


{
int m, i;
m = 1;
for ( i = 1 ; i<=Math.pow( 5, n ) ; i+=2 )
Método {
m = m * x;
}
return m;
}
Análisis del
Tiempo de
ejecución
Orden de
complejidad

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

1.11 Actividad Independiente: Concurso Docente

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

NOMBRE R1 – Crear un nuevo docente.


RESUMEN
ENTRADAS

RESULTADOS

NOMBRE R2 – Calcular el puntaje del docente.


RESUMEN

ENTRADAS

RESULTADOS
El puntaje del docente

62
NOMBRE R3 – Ordenar por nombre a los docentes.
RESUMEN
ENTRADAS

RESULTADOS

NOMBRE R4– Ordenar por puntaje.


RESUMEN
ENTRADAS

RESULTADOS

NOMBRE R5 – Mostrar la información de un docente.


RESUMEN
ENTRADAS

RESULTADOS

b. Identificar las entidades o clases.

ENTIDAD DEL MUNDO DESCRIPCIÓN


ConcursoProfesor
Profesor

c. Descripción de los métodos de la clase Profesor.

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

c. Relaciones entre las clases

Construya el diagrama de clases para Profesor y concursoProfesor.

d) Métodos de la clase Profesor

Los siguientes son los métodos más importantes para la clase profesor. Se omiten los métodos
accesores y modificadores.

public Profesor(String cedula, String nombre, String apellido, boolean


pregrado, boolean especializacion, boolean maestria, boolean doctorado,
int cantidadLibros)
/**
* @param cedula. Es la cédula del docente. cedula !=null cedula!=""
* @param nombre. Nombre del docente. nombre !=null nombre!=""
* @param apellido. Apellido del docente. apellido !=null apellido!=""
* @param pregrado. Indica si el docente tiene pregrado . pregrado == true
* @param especialización. Indica si el docente maestría.
* @param doctorado. Indica si el docente tiene doctorado .
* @param cantidadLibros. Cantidad de libros del docente. cantidaLibros>=0
*/
public Profesor(String cedula, String nombre, String apellido, boolean
pregrado, boolean especializacion, boolean maestria, boolean doctorado,
int cantidadLibros)
{

64
fijarPuntos()
/** Permite fijar la cantidad de puntos */

public void fijarPuntos()


{
puntos=178;

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.

A continuación se muestran los métodos para la clase ConcursoProfesor

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

Método Orden de Complejidad


O( )
fijarPuntos()
O( )
agregarParticipante()
O( )
ConcursoProfesor()
O( )
determinarSiExisteProfesor(String cedula)
O( )
determinarSiNoHayRepeticiones()
O( )
generarListadoPorPuntaje()
O( )
generarListado()
O( )
generarListadoOrdenadoPorNombre()

1.12 Análisis de Algoritmos Recursivos

Si un problema puede resolverse usando la solución de versiones más pequeñas de sí mismo,


y esas versiones más pequeñas, se reducen fácilmente a problemas solubles, entonces
nosotros tenemos un algoritmo recursivo (Baldwin & Scragg, 2004). La recursión es una forma
alternativa, para la resolución de problemas algorítmicos. Muchos algoritmos se pueden
expresar más fácilmente usando una formulación recursiva.

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:

 Caso base: El problema se resuelve directamente (generalmente valores pequeños).


 Paso recursivo: Se divide el problema en varias partes, luego se resuelve cada una de ellas
y finalmente se combinan las soluciones de las partes para dar una solución al problema.

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.

1.12.1 Algoritmos Recursivos

Los métodos recursivos se clasifican de acuerdo al lugar en donde se haga la llamada


recursiva, se tiene por lo tanto la recursividad directa, la cual se tiene cuando un método se
llama a sí mismo, y también existe la recursividad indirecta en la cual un método llama a otro
método y viceversa, siempre resolviendo el mismo problema. Los siguientes métodos usan
recursividad directa.

 Algoritmo recursivo: multiplicación

El siguiente método retorna la multiplicación de dos números enteros de forma recursiva.


Definamos un caso de prueba que permita multiplicar los (5 * 3) recursivamente:
5*3=5+5*2
5 * 2 = 5 + 5 = 10
5*1=5

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.

public int multiplicar ( int a, int b )


{
if ( a == 0 || b == 0 ){
return 0;
}
else
{
if( b == 1 )
Método {
return a;
}
else
{
return a + multiplicar ( a, b-1);
}
}
}

69
El algoritmo tiene dos condiciones de parada:

 Si algún valor que recibió por parámetro es cero retorna cero.


 Si el segundo parámetro es igual a uno, entonces retorna el valor del primer parámetro.

A continuación, se muestra una prueba para este algoritmo recursivo.

multiplicar(5, 4) = 5 + multiplicar (5, 3)


= 5 + (5 + (multiplicar (5,2)))
= 5 + (5 + (5 + multiplicar (5,1))
= 5 + (5 + (5 + 5))
= 5 + (5 + 10)
= 5 + 15
= 20
En la siguiente figura se muestra como se puede representar este algoritmo recursivo.

 Algoritmo recursivo: Contar ceros en el Arreglo

El siguiente algoritmo recursivo retorna la cantidad de ceros que se encuentran dentro de un


arreglo unidimensional de enteros.

public int cantidadCeros (int arreglo[], int n)


{
if (n == 0)
{
return 0;
}
else
{
if (arreglo[n - 1] == 0)
Método
{
return 1+cantidadCeros(arreglo,n-1);
}
else
{
return cantidadCeros (arreglo, n-1);
}
}
}

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.

Problema: Este método verifica si una matriz es simétrica o no lo


Ejercicio
es.

public boolean matrizSimetrica (int matriz[][],


int i, int j)
{
if(i==matriz.length-1 && j==matriz.length-1)
{
return (matriz[i][j]==matriz[j][i]);
}
if(matriz[i][j]==matriz[j][i])
{
if(matriz.length-1==j)
{
Método
return matrizSimetrica(matriz,i+1,0);
}
else
{
return matrizSimetrica(matriz,i,j+1);
}
}
else
{
return false;
}
}
Observaciones

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.

public int numeroPerfecto(int valor,int aux,int i)


{
if(i == valor-1)
{
return aux;
}
Método
if (valor%i==0)
{
aux= aux + i;
}
return numeroPerfecto(valor, aux, ++i);
}

Observaciones

Problema: Este método retorna la suma de los elementos de un


Ejercicio
arreglo bidimensional de enteros.

public int sumaNumeros(int matriz[][],int i,int j)


{
if (j<0)
{
i = i-1;
j = matriz.length-1;
Método
}
if (i<0)
{
return 0;
}
return matriz[i][j]+sumaNumeros(matriz,i,*--j);
}

Observaciones

Escriba los siguientes algoritmos recursivos:

Problema: Se necesita un método que determine si una cadena de


Ejercicio
caracteres es o no palíndroma.

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

Problema: Se necesita un método sume los elementos de un


Ejercicio
arreglo unidimensional de enteros.

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

1.12.2 Tiempo de ejecución de algoritmos recursivos

El análisis de funciones recursivas requiere que a cada función F se le asocie un tiempo de


ejecución TF(n). Una vez hecho esto, se establece una definición inductiva denominada
relación recurrente para TF(n), que relaciona TF (n) con otras funciones de la forma TG(k), para
otras funciones G del programa, cuyos datos de entrada son de tamaño k. Si F es la función
recursiva, entonces una o más de las funciones G serán la misma F.

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:

 El tamaño del parámetro recibido es tan pequeño que no se generan llamadas


recursivas.
 Para argumentos de un tamaño considerable, una o más llamadas recursivas pueden
ocurrir, dado que el problema no se puede resolver directamente.

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.

A continuación se muestran ejemplos que permiten hallar el tiempo de ejecución y el orden de


complejidad para algoritmos recursivos.

 Análisis del algoritmo recursivo: Fibonacci

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.

Objetivo: Entender la forma como se estima el tiempo de ejecución


Ejemplo y el orden de complejidad para algoritmos recursivos.

public int fibo( int n )


{
if ( n == 0 )
{
return 0;
}
else
{
if ( n == 1 || n == 2 )
Método {
return 1;
}

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

 Análisis del algoritmo recursivo: multiplicación

A continuación mostramos el algoritmo recursivo con un tiempo de ejecución de orden


logarítmico, este algoritmo retorna el resultado de multiplicar dos números enteros positivos.

Objetivo: Entender la forma como se estima el tiempo de ejecución


Ejemplo y el orden de complejidad para algoritmos recursivos.

public int multiplicacion (int m, int n)


{
if (n == 0)
{
return 0;
}
else
{
if(n == 1)
{
return m;
}
Método
else
{
if ( n % 2==0 )
{
return multiplicacion (m+m,n/2);
}
else
{
return multiplicacion (m+m,n/2)+ m;
}
}
}
}

El tiempo de ejecución para el caso base lo denotaremos T(1), el


caso base se da para dos casos: cuando el parámetro n = 0 o
Análisis del cuando el parámetro n = 1, retornan respectivamente los valores 0
Orden de y m que es el segundo parámetro. Para este caso que no existe
Complejidad recursión; se debe calcular el tiempo de ejecución para el peor caso
T(1) = 3, y por lo tanto su orden de complejidad es O(1). Con lo
anterior se puede decir que T(1) = O(1).

75
Caso base: T(1) = a
Inducción: T(n) = b + T(n/2)

Ahora se asignan valores n y, reemplace la base en la inducción.

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

El tiempo de ejecución es: T(n) = b log(n) + a

Orden de
O(log(n))
Complejidad

 Análisis del algoritmo recursivo: Potencia

Este algoritmo recursivo retorna la potencia de un numero X elevado a la Y.

Objetivo: Entender la forma como se estima el tiempo de ejecución


Ejemplo 3 y el orden de complejidad para algoritmos recursivos.

public int calcularPotencia (int x, int n)


{
if (n==0)
{
return 1;
}
else
{
if (n%2==0)
{
Método return calcularPotencia(x, n/2) *
calcularPotencia(x, n/2);
}

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

Reemplazando la base en la inducción, tenemos:

T(2) = 2T(1) + b  T(2) = 2a + b


T(4) = 2T(2) + b  T(4) = 4a + 3b

76
T(8) = 2T(4) + b  T(8) = 8a + 7b
T(16) = 2T(8) + b  T(16) = 16a + 15b

T(n) = 2T(n/2) + b  T(n) = na + (n - 1)b

Orden de
O(n)
Complejidad

ACTIVIDAD

Dado el siguiente método recursivo, determine su tiempo de ejecución y su orden de


complejidad.

Ejercicio Problema: Este método retorna la potencia de un número entero.

public int calcularPotencia (int x, int n)


{
if (n==0)
{
return 1;
}
else
{
if (n%2==0)
Método {
int y = calcularPotencia(x,n/2);
return y * y;
}
else
{
int y = calcularPotencia(x,n/2);
return y * y * x;
}
}
}
Análisis del
Tiempo de
Ejecución
Orden de
complejidad

Dado el siguiente método recursivo, determine su tiempo de ejecución y su orden de


complejidad.

Problema: Este método retorna la sumatoria de los n primeros


Ejercicio
números.
public int sumatoria( int n )
{
if (n == 1 ){
return 1;
}
Método
else
{
return n + sumatoria(n - 1);
}
}

77
Análisis del
Tiempo de
Ejecución
Orden de
complejidad

Dado el siguiente método recursivo, determine su tiempo de ejecución y su orden de


complejidad.

Problema: Calcular el tiempo de ejecución del siguiente algoritmo


Ejercicio
recursivo.
public int recursivo( int n )
{
if (n < 1)
{
return 1;
}
Método
else
{
return n *(recursivo(n-1) + recursivo(n-1)
+ recursivo(n-1));
}
}
Análisis del
Tiempo de
Ejecución
Orden de
complejidad

1.12.3 Resolución de Recurrencias por inducción

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

El siguiente algoritmo calcula la potencia de un número entero n.

public int calcularPotencia (int x, int n)


{
if (n==0)
{
return 1;
}
Método else
{
if (n%2==0)
{
return calcularPotencia(x,n/2) *
calcularPotencia(x, n/2);
}

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:

T (1) = O (1) = a (se reemplaza por una constante).


T(n) = 2T(n/2) + b

Reemplazando la base en la inducción, tenemos:

T (2) = 2T (1) + b T (2) = 2a + b


T (4) = 2T (2) + b T (4) = 4a + 3b
T (8) = 2T (4) + b T (8) = 8a + 7b
T (16) = 2T (8) + b T (16) = 16a + 15b
...
T(n) = 2T(n/2) + b T(n) = na + (n - 1) b ∊ O(n)

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

public int calcularPotencia (int x, int n)


{
if (n==0)
{
return 1;
}
else
{
if (n%2==0)
{
Método
int y = calcularPotencia(x,n/2);
return y * y;
}
else
{
int y = calcularPotencia(x,n/2);
return y * y * x;
}
}
}

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

Generalizando tenemos, que el caso base y el caso inductivo, quedan expresados de la


siguiente manera:

79
T (1) = 8
T(n) = T(n/2) + 8

T (1) = O (1) = a (se reemplaza por una constante)


T(n) = T(n/2) + b

Reemplazando la base en la inducción, tenemos:

T (2) = T (1) + b T (2) = a + b


T (4) = T (2) + b T (4) = a + 2b
T (8) = T (4) + b T (8) = a + 3b
T (16) = T (8) + b T (16) = a + 4b
...
T(n) = T(n/2) + b T(n) = a + b (log n) ∊ O (log n)

 Ejemplo 3

En el siguiente caso se analizará únicamente el algoritmo por medio de la técnica de inducción.


Suponga que se tienen las siguientes funciones base y de inducción. Determinar cuál es su
tiempo de ejecución y cuál es su orden de complejidad.

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)

Reemplazando la base en la inducción, tenemos:

T (2) = 2T (1) T (2) = 2a


T (3) = 2T (2) T (3) = 4a
T (4) = 2T (3) T (4) = 8a
T (5) = 2T (4) T (5) = 16a
T (6) = 2T (5) T (6) = 32ª
...
n-1
T(n) = 2T(n) T(n) = 2 a
n
El orden complejidad para un algoritmo con estas características es O (2 ).

 Ejemplo 4

A continuación se muestra un algoritmo recursivo del cual se va a deducir cual es su tiempo de


ejecución y su orden de complejidad.

public int recursivo4( int n )


{
int top = 4;
Método if (n <= 1)
{
return 1;
}

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.

Generalizando tenemos, que el caso base y el caso inductivo, quedan expresados de la


siguiente manera:

T (1) = 4
T(n) = 3T(n - 1) + 4

T (1) = a (se reemplaza por una constante)


T(n) = 3T(n - 1) + b

Reemplazando la base en la inducción, tenemos:

T (2) = 3T (1) + b T (2) = 3a + b


T (3) = 3T (2) + b T (3) = 3(3a + b) + b = 9a + 4b
T (4) = 3T (3) + b T (4) = 3[3(3a + b) + b] + b = 27a + 13b
T (5) = 3T (4) + b T (5) = 3[3[3(3a + b) + b] + b] + b
= 81a + 27b + 9b + 3b + b
n-1 n-2 n-3
T (n) = 3T (n - 1) + b T (n) = 3 a + [3 b + 3 b +: : : + b]

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

Deduzca usando recurrencias por inducción el tiempo de ejecución y el orden de complejidad


del siguiente algoritmo recursivo.

public int recursivo( int n )


{
if ( n <= 1 ){
return 1;
}
else
{
Método for( i = 1 ; i <= n ; i++ )
{
j = j + 1;
}
return ( recursivo(n - 1) +
recursivo(n - 1) );
}
}

81
Deduzca usando recurrencias por inducción el tiempo de ejecución y el orden de complejidad
del siguiente algoritmo recursivo.

public int recursivo( int n )


{
int j = 0;
if ( n <= 1 ){
return 1;
}
else
{
int k = 1
Método
while ( k <= n)
{
j = j + 1;
k++;
}
return ( recursivo(n-1) +
recursivo(n-1) + recursivo(n-1) );
}
}

1.12.4 Resolución de Recurrencias por sustitución

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.

public int divisible ( int datos [ ], int n )


{
if(n==0)
{
if(datos[n] % 2==0)||datos[n] % 3==0)
{
return 1;
}
else
{
return 0;
}
Método }
else
{
if(datos[n] % 2==0|| datos[n] % 3==0)
{
return 1 + divisible(datos,n-1);
}
else
{
return divisible(datos, n-1);
}
}
}

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

Se va a analizar el siguiente algoritmo recursivo.

public int recursivo( int n )


{
if ( n <= 1 )
{
return 1;
Método }
else
{
return (recursivo(n-1) + recursivo(n-1));
}
}

A continuación se analiza el orden de complejidad del algoritmo.

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

Se va a analizar el siguiente algoritmo recursivo.

public int recursivo( int n )


{
if ( n <= 1 ){
return 1;
}
Método
else
{
return ( 2 * recursivo( n / 2 ) );
}
}

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

Se va a analizar el siguiente algoritmo recursivo.

public int recursivo2 ( int n, int b )


{
if ( n <= 1)
{
return 1;
}
else
{
Método int i = 1
while ( i <= n)
{
b = b + 1;
i++;
}
return recursivo2 ( n - 1, b );
}
}

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

Finalmente se resalta la existencia de otro método para resolver recurrencias denominado el


método maestro. El cual según (López, 2003) es una receta para resolver recurrencias de la
forma T(n) = aT(n/b) + f(n), a >= 1, b > 1; las cuales son típicas de algoritmos de dividir
conquistar:

 Dividir problema de tamaño n en a subproblemas de tamaño n/b.


 Dividir y combinar = f(n)

84
ACTIVIDAD

Deduzca usando recurrencias por sustitución el tiempo de ejecución y el orden de complejidad


del siguiente algoritmo recursivo.

public int razon ( int n, int m )


{
if ( m == 0 )
{
Método
return a;
}
return razon ( b, n/2 ) + razon ( b, n/2 );
}

Deduzca usando recurrencias por sustitución el tiempo de ejecución y el orden de complejidad


del siguiente algoritmo recursivo.

public int medio ( int a, int b )


{
if ( a == 1 )
{
return 1;
}
else
{
if ( b == 2 )
Método
{
return a;
}
else
{
return medio ( a, b / 2 );
}
}
}

1.13 Análisis de Métodos de Ordenamiento

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.

Algunos de ejemplos de aplicación de estos algoritmos pueden ser:

o Lista ordenada por apellido de estudiantes del programa de Ingeniería de


Sistemas.
o Un directorio telefónico de usuarios.
o Un reporte de las ventas totales para cada uno de los meses del año.

A continuación se muestran algunos algoritmos de ordenamiento:

85
1.13.1 Método de Ordenamiento ShakerSort

Conocido como el método de la “Sacudida”, es una mejora del método de la burbuja. El


funcionamiento de este algoritmo consiste en mezclar las dos formas en que se puede realizar
el método de burbuja. En este algoritmo cada pasada tiene dos etapas. En la primera etapa “de
arriba hacia abajo” se trasladan los elementos más pequeños hacia la parte de arriba del
arreglo, almacenando en una variable la posición del último elemento intercambiado.

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.

public void shakerSort( int x[ ] )


{
int aux;
int primero = 1;
int ultimo = x.length - 1, dir = x.length - 1;

while ( ultimo >= primero )


{
for( int i = ultimo ; i >= primero ; i--)
{
if ( x [i - 1] > x[ i ] )
{
aux = x[i - 1];
x[i - 1] = x[ i ];
x[ i ] = aux;
dir = i;
Método
}
}
primero = dir + 1;
for( int i = primero ; i <= ultimo; i++ )
{
if ( x [i - 1] > x[ i ] )
{
aux = x[i - 1];
x[i - 1] = x[ i ];
x[ i ] = aux;
dir = i;
}
}
ultimo = dir - 1;
}
}

El número de elementos comparados en la primera etapa es (n - 1)


Análisis del + (n - 2), en la segunda fase es (n-3) + (n-4), y así sucesivamente.
método de Sumando todas las fases se obtiene (suma desde i = 0 hasta n - 1),
ordenamiento i=n*(n*1)/2.

Orden de 2
O(n )
complejidad

86
ACTIVIDAD

Ahora que conoce algunos algoritmos de ordenamiento, ordene la siguiente secuencia


basándose en el método de ordenamiento shakerSort.

148 73 485 485 202 3005 831 407

1.13.2 Método de Ordenamiento Burbuja

Este método consiste en la comparación por parejas adyacentes e intercambiarlas de acuerdo


al orden que desee darse. Este proceso se repite hasta que el conjunto de datos está
completamente ordenado. Se llama burbuja, pues consiste en flotar hasta el final del arreglo el
elemento mayor en cada iteración.

public void burbuja( int arreglo[] )


{
int temp, j, i;
for( j = 1 ; j < arreglo.length ; j++)
{
for( i = 0 ; i<arreglo.length-1 ; i++)
{
if ( arreglo[ i ] > arreglo[i+1] )
Método
{
temp = arreglo[ i ];
arreglo[i] = arreglo[i + 1];
arreglo[i+1] = temp;
}
}
}
}
Análisis del
2
método de T(n) = 8n + 12n + 9
ordenamiento
Orden de 2
O(n )
complejidad

1.13.3 Método de Ordenamiento por Selección

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.

public void seleccion (int arreglo[] )


{
int i, j, k, menor;
i = 0;
while( i < arreglo.length - 1)
{
menor = arreglo [i];
k = i;
for( j = i+1; j < arreglo.length; j++)
{
Método
if (arreglo [j] < menor )
{
menor = arreglo [j];
k = j;
}
}
arreglo [k] = arreglo[i];
arreglo [i] = menor;
i++;
}
}
Análisis del
método de
ordenamiento
Orden de 2
O(n )
complejidad

ACTIVIDAD

Ordene la siguiente secuencia basándose en el método de ordenamiento selección.

748 173 8425 85 221 305 6371 5407

1.13.4 Método de Ordenamiento Inserción

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

Establezca tres diferencias relativas en términos de eficiencia entre los métodos de


ordenamiento por selección y por inserción.

Ordenamiento por Inserción Ordenamiento por Selección

1.13.5 Método de Ordenamiento QuickSort.

Es uno de los métodos de ordenamiento más conocidos y utilizados para el ordenamiento. Su


eficiencia es reconocida y los recursos computacionales que consume es mejor que muchos
algoritmos de ordenamiento. La descripción del funcionamiento del algoritmo, consiste en que
inicialmente el arreglo a ordenar se divide en dos subarreglos los cuales se van ubicando de tal
forma que ellos todos los elementos del primer subarreglo son menores que los elementos del
segundo subarreglo. Se tiene un elemento pivote el cual se le asigna el valor del medio del
tamaño del arreglo, este elemento pivote realiza una actividad de partición en la cual los
elementos menores están a la izquierda y los elementos mayores esta su derecha.

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.

A continuación se muestra la implementación del método QuickSort.

public void quickSort( int a[] )


{
quickSort( a, 0, a.length - 1 );
}

public void quickSort( int a[], int limInferior,


int limSuperior )
{
int i = limInferior;
int j = limSuperior;
int pivote = a[(limInferior+limSuperior)/2];
do
{
while( a[ i ] < pivote )
{
i++;
}
while( a[ j ] > pivote )
{
j--;
Método
}
if (i <= j)
{
int aux = a[ i ];
a[ i ] = a[ j ];
a[ j ] = aux;
i++;
j--;
}
}
while (i <= j);
if ( j > limInferior )
{
quickSort(a, limInferior, j );
}
if ( i < limSuperior )
{
quickSort(a, i, limSuperior );
}
}
Se tienen las siguientes expresiones de base y de inducción.

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

Por lo tanto el tiempo de ejecución está dado por:


T(n) = an + nlog(n)b

ACTIVIDAD

Ordene la siguiente secuencia basándose en el método de ordenamiento quickSort.

884 237 183 245 122 2305 1131 212 1398 658 23

Investigue y posteriormente explique el funcionamiento y el tiempo de ejecución para el método


de ordenamiento RadixSort.

1.13.6 Método de Ordenamiento ShellSort.

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.

public void shellSort(int a[])


{
for (int incr = a.length/2; incr>0; incr/= 2 )
{
for (int i = incr ; i < a.length ; i++ )
{
int j = i - incr;
while (j >= 0)
{
if (a[j] > a[j + incr])
{
int T = a[ j ];
Método
a[ j ] = a[j+incr];
a[j+incr] = T;
j -= incr;
}
else
{
j = -1;
}
}
}
}
}
Al principio del proceso se escoge la secuencia de decrecimiento
de incrementos; el último valor debe ser 1. Cuando el incremento
Análisis del toma un valor de 1, todos los elementos pasan a formar parte del
método de subgrupo y se aplica inserción directa. El método se basa en tomar
ordenamiento como salto n/2 (siendo n el número de elementos) y luego se va
reduciendo a la mitad en cada repetición hasta que el salto o
distancia vale 1.
Orden de 2
O(n )
complejidad

ACTIVIDAD

Ordene la siguiente secuencia basándose en el método de ordenamiento shellSort.

2824 122 1832 435 128 3505 1599 776

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.

public void stoogeSort( int a[] )


{
stoogeSort( a, 0, a.length - 1 );
}
public void stoogeSort(int a[], int i, int j )
{
int k;
if( a[ i ] > a[ j ])
{
int temp;
temp = a[ i ];
Método a[ i ] = a[ j ];
a[ j ] = temp;
}
if( i + 1 >= j )
{
return;
}
k = ( j - i + 1 ) / 3;
stoogeSort( a, i , j - k );
stoogeSort( a, i + k, j );
stoogeSort( a, i , j - k );
}

Si el valor del final es más pequeño que el valor en el comienzo,


entonces, se intercambian. Si hay dos o más elementos en la lista
Análisis del actual, entonces:
 Ordena los dos tercios iniciales de la lista.
método de
 Ordena los dos tercios finales de la lista.
ordenamiento
 Ordena los dos tercios iniciales de la lista nuevamente.

Este algoritmo de ordenamiento recursivo posee un orden de
Orden de 2,7
complejidad de O(n ). El valor exponencial exacto es:
complejidad
O(log(3)/log(1,5)

El algoritmo StoogeSort es un algoritmo de ordenamiento ineficiente que cambia los elementos


de la parte superior e inferior si es necesario, luego, recursivamente, ordena las dos terceras
partes inferiores, las dos terceras partes superiores, y nuevamente las dos terceras partes
inferiores.

Este es un algoritmo de Dividir y conquistar. El caso general para el algoritmo dividir y


conquistar tiene el siguiente principio base. Principio Base: El arreglo se clasifica en pedazos
de 2/3 de los elementos totales (primer 2/3, último 2/3, primer 2/3 ) y el tamaño del arreglo que
es calificado se trae abajo a dos elementos recursivamente.

1.14 Métodos de Búsqueda

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:

 Encontrar un cliente de una empresa teniendo en cuenta el número de su cédula.


 Verificar si un estudiante se encuentra a paz y salvo con la universidad dando su
código.
 Encontrar el vendedor más eficiente de un producto en un determinado mes.

A continuación se tiene la implementación de algoritmos de búsqueda. Cada uno de los


algoritmos de búsqueda ellos tendrá un análisis de su funcionamiento.

1.14.1 Búsqueda Lineal Iterativa

La búsqueda lineal consiste en encontrar un elemento sobre un conjunto de datos


comparándolos uno a uno en el orden en el que estos se encuentren. En esta primera
implementación se recorre el arreglo de inicio a fin y se retorna verdadero si el elemento se
encuentra de lo contrario retornará falso.

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.

boolean busquedaIterativa(int arreglo[],int dato )


{
boolean estado = false;
for( int i = 0 ; i < arreglo.length ; i++ )
{
Método if ( arreglo [ i ] == dato )
{
estado = true;
}
}
}
Se observa que se tiene un ciclo for que recorre el arreglo desde la
posición 0, hasta la posición n - 1 del arreglo, por lo tanto se tiene
Análisis del
O(n). El condicional contiene una comparación del elemento en la
método de
posición del arreglo y del elemento que se busca. El peor caso,
búsqueda
será que esta comparación sea verdadera en la última comparación
o que nunca sea verdadera.
En el caso que el elemento que se esta buscando este en las
Orden de primeras posiciones del arreglo, el orden de complejidad es O(1),
complejidad pero esto nunca sucede en esta implementación, el orden de
complejidad de la búsqueda lineal es O(n).

A continuación, mostraremos como se busca un objeto utilizando la búsqueda lineal.


Inicialmente se tiene un arreglo con 8 elementos, y se esta buscando el número 21. La
siguiente figura muestra los números que se encuentran dentro del arreglo.

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.

Se sigue realizando la búsqueda al segundo elemento del arreglo, el cual se encuentra en la


posición 1, como aún no se encuentra el elemento, continua la búsqueda.

Se sigue realizando la búsqueda al tercer elemento del arreglo, el cual se encuentra en la


posición 2, como aun no se encuentra el elemento, continua la búsqueda.

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.

1.14.2 Búsqueda Lineal Limitada

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

Se tiene un ciclo for que recorre para el peor caso el arreglo


desde la posición 0, hasta la posición n - 1 del arreglo, por lo tanto
Análisis del
se tiene O(n).
método de
Para el caso en el cual consideramos que el elementos que se está
búsqueda
buscando está en las primeras posiciones del arreglo, se tiene un
orden de complejidad O(1).
Orden de
O(n)
complejidad

1.14.3 Búsqueda Lineal Iterativa con extremos

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.

boolean busquedaExtremos (int arreglo[],int dato )


{
boolean estado = false;
int n = arreglo.length;
if (dato < arreglo[0])
{
return false;
}
if ( dato > arreglo[n-1])
{
return false;
Método
}
for(int i = 0; i < n ; i++)
{
if(arreglo[i] == dato)
{
estado = true;
break;
}
}
return estado;
}

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

1.14.4 Búsqueda Binaria

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.

A continuación se muestra una implementación para la búsqueda binaria.

boolean busquedaBinaria( int arreglo[], int dato )


{
int centro;
int limSup=arreglo.length-1,;
int limInf=0;

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

Dado el siguiente algoritmo, analice si es un algoritmo correcto para realizar búsquedas, en


caso de serlo, explique la forma de funcionamiento de dicho algoritmo.

boolean busqueda( int arreglo [], int dato )


{
return busqueda(arreglo,dato,arreglo.length-1 );
}
boolean busqueda(int arreglo[],int dato,int pos )
{
if ( pos < 0 )
{
return false;
Método }
if ( arreglo [ pos ] == dato )
{
return true;
}
else
{
return busqueda(arreglo,dato,pos-1);
}
}
Análisis del
método de
búsqueda
Orden de
complejidad

1.15 Caso de Estudio: Registro de notas

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.

a. Identificar las entidades o clases.

ENTIDAD DEL MUNDO DESCRIPCIÓN


GrupoEstudiante
Estudiante

b. Métodos de la clase Estudiante.

NOMBRE DESCRIPCIÓN
calcularDefinitiva()

c. Métodos de la clase GrupoEstudiante.

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

A continuación se muestra el principal método de la clase Estudiante.

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

A continuación se muestran los principales métodos de la clase GrupoEstudiante.

Método que ordena comparando elementos en


ordenarPorBurbuja()
posiciones adyacentes
/**Permite ordenar por burbuja
* @return un array de String */

public String[] ordenarPorBurbuja()


{
int i,j;
String array[]=new String[miEstudiante.size()];
Estudiante temp;
for (i=0; i<miEstudiante.size(); i++)
{
for (j=0; j<miEstudiante.size()-1;j++)
{
if (miEstudiante.get(j).getNota1() >
miEstudiante.get(j+1).getNota1())
{
temp = miEstudiante.get(j);
miEstudiante.set(j,miEstudiante.get(j+1));
miEstudiante.set(j+1,temp);
}
}
}

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

/** @return un array de String */

public String[] ordenarPorInsercion()


{
String array[]=new String[miEstudiante.size()];
Estudiante temp;
int i,j;
for (i=0; i<miEstudiante.size(); i++)
{
temp = miEstudiante.get(i);
for (j=i; j>0;j--)
{
if( miEstudiante.get(j-1).getNota2()<=temp.getNota2())
{
break;
}
miEstudiante.set(j,miEstudiante.get(j-1));
}

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

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

public double obtenerDefinitivaModa()


{
int contadorOcurrencias=0,cantidad=0,i,j,posicion=0;
for (i=0; i<miEstudiante.size(); i++)
{
for(j=0; j<miEstudiante.size(); j++)
{
if(i!=j)
{
if(miEstudiante.get(i).getDefinitiva()==
miEstudiante.get(j).getDefinitiva())
{
contadorOcurrencias++;
}
}
}
if(cantidad<contadorOcurrencias)
{
posición =i;
cantidad=contadorOcurrencias;
}
contadorOcurrencias=0;
}
if(miEstudiante.size()>0)
{
JOptionPane.showMessageDialog(null,"La que más se repite "
+ miEstudiante.get(posicion).getDefinitiva());
return miEstudiante.get(posicion).getDefinitiva();
}
return -1;
}

busquedaSecuencial(double nota)

public Estudiante busquedaSecuencial(double nota)


{
DecimalFormat formatoDecimal;
formatoDecimal = new DecimalFormat ( "0.0" );
String dato = formatoDecimal.format ( nota );

102
dato = dato.replace(',','.');
nota = Double.parseDouble(dato);

for (int i=0; i<miEstudiante.size(); i++)


{
if(nota==miEstudiante.get(i).getDefinitiva())
{
return miEstudiante.get(i);
}
}
return null;
}

busquedaBinaria(double nota)

public Estudiante busquedaBinaria(double nota)


{
DecimalFormat formatoDecimal;
int inicio, fin, medio;
inicio=0;
fin=miEstudiante.size()-1;
ordenarPorSeleccion();

formatoDecimal = new DecimalFormat ( "0.0" );


String dato = formatoDecimal.format ( nota );
dato=dato.replace(',','.');
nota=Double.parseDouble(dato);

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.

En los requerimientos que se identificaron se pudo observar que es necesario implementar un


algoritmo que permita buscar un elemento dentro de un arreglo.

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:

public boolean encontrarPersona()


{
boolean centinela = false;

for( int i = 0; i < miEmpleado.length && centinela != true ; i++)


{
if( miEmpleado[ i ].getSalario() > 430000 )
{
centinela = true;
return centinela;
}
}
return centinela;
}

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.

public Estudiante busquedaSecuencial(double nota)


{

for (int i=0; i<miEstudiante.size(); i++)


{

105
106
2 ESTRATEGIAS DE PROGRAMACIÓN

2.1 Introducción

En los capítulos previos se resaltó la importancia de aplicar los conceptos matemáticos al


análisis de algoritmos. Al mimo tiempo es necesario reconocer que la construcción de un
algoritmo debe planearse adecuadamente de acuerdo a los parámetros definidos para tal fin.
La selección de un método de implementación de forma correcta es fundamental pues de ello
dependerá su desempeño en cuanto a eficiencia y efectividad.

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.

2.2 Algoritmos Divide y Vencerás

La técnica divide y vencerás se utiliza dentro de la algoritmia cuando es necesario


descomponer un conjunto de datos en subjconjuntos de ese mismo tamaño de entrada. Cada
uno de los subconjuntos se resuelve de forma independiente y finalmente de sus resultados se
construye la solución final del conjunto de datos original. Esta técnica resuelve los problemas
aplicando la recursividad.

A continuación se muestra la aplicación de algoritmos divide y vencerás en contextos de


aplicación considerados comunes dentro del análisis de algoritmos.

2.2.1 Búsqueda Binaria

A continuación se muestra la implementación de forma recursiva de la búsqueda binaria, este


método recibe por parámetro el arreglo ordenado y el elemento que se desea buscar dentro del
arreglo; además de los valores tanto del límite inferior del arreglo como del límite superior del
arreglo. Para analizar la búsqueda binaria usaremos también el número de comparaciones que
necesitamos, en el peor caso, que se dará cuando no encontremos el elemento buscado o
´este se halle en uno de los extremos del arreglo.

boolean binariaRecursiva ( int arreglo[], int dato )


{
return binRecursiva ( arreglo, dato, 0, arreglo.length-1);
}

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

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 divide a la mitad. Si el tamaño del arreglo es n, se va
reduciendo por cada comparación a n/2,n/4,n/8… n/2n, hasta llegar a 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)).

La anterior implementación se basa en la técnica divide y vencerás ya que para solucionar el


problema necesariamente se tienen que realizar llamadas recursivas al método de
implementación.

2.2.2 Ordenamiento por el método MergeSort

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.

public void mergesort( int a[] )


{
mergesort ( a, 0, a.length -1 );
}
public void mergesort(int a[],int bajo, int alto )
{
int len, pivote, m1, m2;
if ( bajo == alto )
{
return;
}
else
Método {
len = alto - bajo + 1;
pivote = (bajo + alto) / 2;
mergesort( a, bajo, pivote );
mergesort( a, pivote + 1, alto );
int temp[] = new int[ len ];

for( int i = 0 ; i < len ; i++ )


{
temp[ i ] = a[bajo + i];
}
m1 = 0;
m2 = pivote - bajo + 1;

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)

Reemplazando el caso base por una constante, tenemos que:


T(1) = a
T(n) = 2T(n/2) + bn
Análisis del
método de Utilizando el reemplazo de la base en la inducción:
ordenamiento 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

Por lo tanto el tiempo de ejecución está dado por:


T(n) = an + nlog(n)b

El término an es de orden O(n) y el término nlog(n)b es de orden


Orden de
O(nlog(n)). Se observa que la función orden nlog(n) crece más
complejidad
rápidamente que la función n, por lo tanto es correcto decir que la
función n es de orden O(nlog(n)).

ACTIVIDAD

Dado el siguiente gráfico, muestre la forma en la cual el método de ordenamiento mergeSort


organiza el arreglo original. Las fechas indican que en ese momento la recursividad esta
ejecutándose.

109
Arreglo a Ordenar

5 1 4 7 1 3 2 6

2.2.3 Multiplicación de Números Grandes

La multiplicación de enteros grandes es un tema de interés dentro del análisis de algoritmos, a


priori puede parecer un tema sencillo de analizar teniendo en cuenta la gran capacidad que
poseen los tipos de datos para almacenar valores.

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.

Nombre Rango de valores Tamaño en bits Declaración


byte -128 a 127 8 byte var1;
short -32768 a 32767 16 short var2
int -2147483648 a 2147483647 32 int var3;
-9223372036854775808 a
long 64 long var4;
9223372036854775807

Mientras que si se utiliza una variable de tipo double, son necesarios 64 bits.

Nombre Rango de valores Tamaño en bits Declaración


float 3,4E-38 a 3,4E38 32 float num1;
double 1.7E-308 a 1.7E308 64 double num2;

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.

A continuación se muestra de forma evolutiva como por medio de diferentes estrategias de


programación se puede resolver el problema de la multiplicación de números grandes.

 Algoritmo Clásico de Multiplicación

El funcionamiento de este algoritmo esta fundamentado en la forma en que tradicionalmente


realizamos la multiplicación, se toma cada una de las cifras del multiplicador y se multiplica

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

A continuación se muestra la implementación con arreglos de un algoritmo clásico de


multiplicación de números enteros. En este método se multiplica cada posición de los dos
arreglos teniendo en cuenta el orden de cada uno de los elementos.

int[] Mult(int Num[],int tam1,int Num2[],int tam2)


{
int l=tam1+tam2-1,pos=tam1+tam2-1;;
int res[]= new int[tam1+tam2];

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.

 Algoritmo Multiplicación a la Rusa

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.

A continuación, se muestra un ejemplo que realiza una multiplicación a la rusa.

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.

int multiplicacion(int m,int n)


{
int resultado = 0;
do{
if(m%2!=0)
{
resultado = resultado + n;
Método }
m = m/2;
n = n+n;
}while(m>=1);

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

 Algoritmo Multiplicación Divide y Vencerás

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.

A continuación se muestra la implementación de la Multiplicación divide y vencerás.

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;

//Caso trivial, donde se llama al algoritmo clasico


if(n==2)
{
re=new int[n+1];
re=Alg(vec1,n,vec2,n);
return re;
}
//Reserva memoria dinamica de la mitad del tamaño de los arreglos
//iniciales para cada uno de los vectores componentes del algoritmo
else
{
x=new int[n/2];
y=new int[n/2];
z=new int[n/2];
w=new int[n/2];

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

//inicializamos r con ceros garantizando los ceros


iniceros(r,2*n);
auxr=dv1(w,y,n/2);
for(int i=0;i<n;i++)
{
r[i]=auxr[i];
}

//s guarda la multiplicacion de los vectores "w" y "z"


s=new int[n+(n/2)];
auxs=new int[n+(n/2)];
iniceros(s,n+(n/2));
auxs=dv1(w,z,n/2); //la formula

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

//u guarda la multiplicación de los arreglos "x" y "z"


u=new int[n];
iniceros(u,n);
u=dv1(x,z,n/2);

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

//res3 guarda la suma de "res" y "res2", es el resultado final


res3=suma(res,2*n,res2,(n/2)+n);
return res3;
}
}

La implementación de este algoritmo se remite a la operación de multiplicación entre dos


números de n cifras en 4 multiplicaciones de números de n/2 cifras, pero su orden de
2
complejidad sigue siendo de O(n ) y por lo tanto no se evidencian mejoras en su eficiencia.

2.3 Algoritmos Devoradores

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:

 Encontrar el árbol de recubrimiento mínimo de un grafo: por ejemplo encontrar la forma


de unir por carreteras varias ciudades.
o El algoritmo de Kruskal.
o El algoritmo de Prim.
 Caminos mínimos en grafos: Consiste en encontrar la ruta más corta posible para ir de
un lugar a otro. (Algoritmo de Dijkstra)
 El problema de la mochila. Consiste en llenar una mochila de manera que el contenido
sea lo máximo posible.
 El problema de planificación de tareas. Estos problemas tratan de planificar varias
tareas o actividades de manera que se maximice la ganancia obtenida o el número de
tareas realizadas.
 El problema del cambio de la devuelta. Consiste en conformar una tarea con el menor
número posible de monedas.

2.3.1 El Problema de la mochila

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:

 Fijar el peso máximo soportado por el camión.


 Agregar un nuevo objeto
 Mostrar los objetos seleccionados
 Mostrar el valor de la carga

a. Identificar las entidades o clases.

ENTIDAD DEL MUNDO DESCRIPCIÓN


Camion Es la clase principal del problema
Objeto Elemento sobre el cual se realizan las
principales operaciones

b. Relaciones entre las clases

115
c. Implementación de las clases

A continuación se muestra la implementación de los métodos fundaméntales e la clase Objeto.

public class Objeto


{
private double peso;
private double valor;
private double cantidad;

public Objeto(double peso, double valor, double cantidad)


{
this.peso = _____________;
this.valor = ____________;
this.cantidad = _________;
}

public double getPeso()


{
return peso;
}
public void setPeso(double peso)
{
___________________
}
public double getValor()
{
_______________
}
public void setValor(double valor)
{
this.valor = valor;
}

public double getCantidad()


{
return cantidad;
}

public void setCantidad(double cantidad)


{
this.cantidad = cantidad;
}
}

A continuación se muestran los métodos más importantes de la clase Camión.

import java.util.ArrayList;
import java.util.Collections;

public class Camion


{
double valor;
ArrayList <Objeto> misObjetos;
int cantidad[];
double pesoMaximo;

116
public ArrayList<Objeto> getMisObjetos()
{
return misObjetos;
}

public void setMisObjetos(ArrayList<Objeto> misObjetos)


{
this.misObjetos = misObjetos;
}

public Camion(double cambio)


{
misObjetos=new ArrayList<Objeto>();
valor=0;
this.pesoMaximo=cambio;
}

public void agregarObjeto(double peso, double valor)


{
MyComparator miComparador=new MyComparator();
misObjetos.add(new Objeto(peso,valor,0));
Collections.sort(misObjetos,miComparador);
}

public void realizarCambio()


{
double peso;
int i=-1;
peso = 0;
valor=0;

/* Los candidatos son los objetos La función solución


verifica si el peso de los objetos elegidos es igual al
pesoMaximo. Un conjunto de objetos es factible si su peso
total no excede el peso máximo La función objetivo obtiene
el valor máximo de la mercancía del camión */

while(peso < pesoMaximo)


{
i++;
// misObjetos.get(i) Es la estrategia de selección se
// elige de menor a mayor peso

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

public double getCambio()


{
return pesoMaximo;
}

public void setCambio(double cambio)


{
this.pesoMaximo = cambio;
}

public String[] listar() throws Exception


{
int i=0;
if(misObjetos.size()==0)
{
throw new Exception("Debe ingresar denominaciones antes
de poder realizar cambios");
}
else
{
realizarCambio();
String info[]=new String[misObjetos.size()+2];

for(i=0; i<misObjetos.size(); i++)


{
info[i]="OBJETO : "+i+" CANTIDAD:
"+misObjetos.get(i).getCantidad()+" PESO:
"+misObjetos.get(i).getPeso()+ " VALOR :
"+misObjetos.get(i).getValor();
}
return info;
}
}

public double getPesoMaximo()


{
return pesoMaximo;
}

public void setPesoMaximo(double pesoMaximo)


{
this.pesoMaximo = pesoMaximo;
}

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.

3. Construya el método realizarCambio. Utilizar la función de selección minimizar wi

public void realizarCambio()


{

4. Construya el método realizarCambio. Utilizar la función de selección maximizar vi/wi..

119
public void realizarCambio()
{

2.3.2 Elementos de los Algoritmos Voraces

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.

 Existe una función que comprueba si un cierto conjunto de candidatos constituye en


una solución de nuestro problema, ignorando si es o no optima por el momento.
Algunos ejemplos de valores devueltos por la función solución en el problema de
devolver $700.

o Solución (200, 200,200,100) = verdadero


o Solución (200, 200, 100, 100, 100) = verdadero
o Solución (100,100) = falso.

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

public boolean[] algoritmo(int p, int s)


{
int n=s-p+1,k;
boolean[] solucion=new boolean[n]; //agregado solución vacío
for(int i=-1; i<n-1; )
{
k=i+1; //candidato
i++;
if(par(c[k+p])) //condición de prometedor
{
solucion[k]=true; //añadir candidato a la solución
}
}
return solucion;
}
public boolean par(int i)
{
return (i%2==0);
}
}

2.3.3 El problema de la Devuelta.

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.

200 200 200 200 100 100 100 100 50 50

El tendero tratará de utilizar el menor número posible de monedas y devolverá 3 monedas de


200 y una moneda de 100. Esta forma de selección ha utilizado un algoritmo voraz que
selecciona las monedas de mayor a menor (orden no creciente). Este algoritmo puede
detallarse de la siguiente manera:

 Se selecciona la primera moneda de $200.


 Se acepta esa moneda, porque sirve para conformar la devuelta de $700. Solo falta
conformar $500.
 Se selecciona la segunda moneda de $200.
 Se acepta esa moneda, porque sirve para conformar los $500 restantes. Falta
conformar $300.
 Se selecciona la tercera moneda de $200.
 Se acepta esa moneda, porque sirve para conformar los $300 restantes. Falta
conformar $100.
 Se selecciona la cuarta moneda de $200.
 Se rechaza esa moneda, porque solo falta conformar $100.
 Se selecciona la primera moneda de $100.

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.

De acuerdo a lo anterior, el problema del cambio en monedas contiene los siguientes


elementos:

 Se trata de devolver una cantidad con el menor número posible de monedas.


 Se parte de:
o Un conjunto de tipos de monedas válidas, de las que se supone que hay
cantidad suficiente para realizar el desglose.
o Un valor a retornar.

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.

public int [ ] devolverCambio (int n)


{
int[] denominacion = {100, 25, 10, 5, 1};
int solucion [ ];
int cantidad = 0;
int i=0, j=0;
while(cantidad!= n)
{
if( (cantidad + denominacion[ i ]) <= n)
{
solucion[ j ] = denominacion[ i ];
cantidad = cantidad + denominacion[ i ];
j++;
i--;
}
i++;
}
return solucion;
}

Considerando el caso del tendero, argumente las siguientes situaciones:

Que subconjunto se extrajo del conjunto dado?

Con que criterio se seleccionaron los elementos del concurso?

122
Según las características de los algoritmos voraces, es posible que el tendero analice varias
para dar la devuelta?

2.4 Actividad Independiente: El problema de 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:

 Agregar una nueva denominación


 Modificar el valor a cambiar
 Dar el cambio, especificando las monedas tomadas y la cantidad de ellas.

a) Requerimientos funcionales

NOMBRE R1 –
RESUMEN
ENTRADAS
RESULTADOS

NOMBRE R2 –
RESUMEN

ENTRADAS
RESULTADOS

123
NOMBRE R3 –
RESUMEN

ENTRADAS
RESULTADOS

b. Identificar las entidades o clases.

ENTIDAD DEL MUNDO DESCRIPCIÓN


Devuelta

c. Identificar los métodos de la clase

NOMBRE DESCRIPCIÓN
Devuelta(double cambio)
void
fijarDenominaciones(ArrayList<
Integer> misMonedas)
double realizarCambio()
double getCambio()
String[] listar() throws
Exception

A continuación se muestra la implementación de algunos métodos de la clase Devuelta. Debe


completar aquellos cuyo cuerpo de implementación esta vacío.

import java.util.ArrayList;
import java.util.Collections;
import javax.swing.JOptionPane;

public class Devuelta


{
double suma;
ArrayList<Integer> misMonedas;
int cantidad[];
double cambio;

public Devuelta(double cambio)


{
misMonedas=new ArrayList<Integer>();
suma=0;
this.cambio=cambio;
}

public void fijarDenominaciones(ArrayList< Integer> misMonedas)


{
MyComparator miComparador=new MyComparator();
this.misMonedas=misMonedas;
Collections.sort(misMonedas,miComparador);
}

124
public double realizarCambio()
{

return suma;
}

public double getCambio()


{
return cambio;
}

public void setCambio(double cambio)


{
this.cambio = cambio;
}

public String[] listar() throws Exception


{
int i=0;

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

for(i=0; i<misMonedas.size(); i++)


{
info[i]="Denominacion:"+misMonedas.get(i).
intValue()+" Cantidad: "+cantidad[i];
}
info[i]="Se cambio en total "+(int)salida+" pesos";
info[i+1]= "No se pudieron cambiar "+(int)(cambio-
salida)+" pesos";
return info;
}
}

125
ACTIVIDAD

Identificar en la clase Devuelta los elementos característicos de los algoritmos devoradores.

 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

2.5 Programación Dinámica

La Programación Dinámica (PD) descompone un problema en subproblemas de menor


tamaño. Esta estrategia es aplicable siempre y cuando los subproblemas no sean
independientes. Se apoya en el principio de optimalidad de Bellman: “Cualquier subsecuencia
de decisiones de una secuencia óptima de decisiones que resuelve un problema también debe
ser óptima respecto al subproblema que resuelve.”

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

En grandes líneas, el diseño de un algoritmo de Programación Dinámica consta de los


siguientes pasos:

1. Planteamiento de la solución como una sucesión de decisiones y verificación de que


ésta cumple el principio de óptimo.
2. Definición recursiva de la solución.
3. Cálculo del valor de la solución óptima mediante una tabla en donde se almacenan
soluciones a problemas parciales para reutilizar los cálculos.
4. Construcción de la solución óptima haciendo uso de la información contenida en la
tabla anterior”.

2.5.1 Serie de Fibonacci

En este apartado se aplicará la serie de Fibonacci al contexto de la reproducción de conejos.


En una granja han contratado un programador para que realice una aplicación que permita
modelar la reproducción de conejos mediante un esquema sencillo de suposiciones, para ello
se ha decido “poner en una coneja una pareja de conejos recién nacidos. Se sabe que la
reproducción de conejos sigue las siguientes reglas:

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.

Esta relación se puede expresar de la siguiente forma:

Fib(n)= Fib (n-1) + fib (n-2)

Donde Fib (0) = 1 y Fib (1) = 1

La clase Fibonacci permite resolver el problema de la reproducción de conejos.

public class Fibonacci


{
/**
* @param n Es la cantidad de términos de la serie. n>=0
* @return el valor en la serie fibonacci
*/
public int calcularFibonacci (int n)
{
if ((n == 0) || (n == 1))
{
return 1;
}
else
{
return calcularFibonacci(n-1) + calcularFibonacci(n-2);
}
}
}
n
El algoritmo en su versión recursiva tienen un orden de complejidad O(2 ), esto a que se
realizan dos llamadas recursivas y el caso base inductivo para este problema es: b+2T(n-1).

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.

Fib(0) Fib(1) Fib(2) .... Fib(n)

127
El método iterativo que calcula la sucesión de Fibonacci utilizando programación dinámica es el
siguiente:

public int fibo ( int datos[], int n )


{
if (n==0)
{
return 0;
}
if (n<=1)
{
return 1;
}
else
{
datos[0]=0;
datos[1]=1;
for (int i=2; i <= n; i++)
{
datos[i] = datos[i-1] + datos[i-2];
}
}
return datos[n];
}

ACTIVIDAD

Construya la versión iterativa para el problema de la serie de Fibonacci.

public int fibo ( int n )


{

¿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

Escriba la implementación para factorial de forma iterativa, recursiva y con programación


dinámica.

public int factorialIterativo ( )


{

public int factorialRecursivo ( )


{

public int factorialDinamico ( )


{

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.

2.5.2 Problema de la Mochila

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.

a. Identificar las entidades o clases.

ENTIDAD DEL MUNDO DESCRIPCIÓN


Camion

public class Camion


{
private double valor;
prívate ArrayList <Objeto> misObjetos;
private double pesoMaximo;

public ArrayList<Objeto> getMisObjetos()


{
return misObjetos;
}

public void setMisObjetos(ArrayList<Objeto> misObjetos)


{
this.misObjetos = misObjetos;
}

public Camion(double cambio)


{
misObjetos=new ArrayList<Objeto>();
valor=0;
this.pesoMaximo=cambio;
}

public void agregarObjeto(double peso, double valor)


{
MyComparator miComparador=new MyComparator();
misObjetos.add(new Objeto(peso,valor,0));
Collections.sort(misObjetos,miComparador);
}

130
public int realizarCambio()
{
String salida="";
int g[][]=new int[misObjetos.size()][(int)pesoMaximo+2];
int j,i;

for(i=1 ; i<=(int)pesoMaximo; i++)


{
g[0][i]=1;

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

}
}

for( i=0; i<misObjetos.size(); i++)


{
for( j=0; j<(int)pesoMaximo+1; j++)
{
salida+=g[i][j]+" ";
}
salida+="\n";
}
return g[i-1][j-1];
}

public double getCambio()


{
return pesoMaximo;
}

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

public double getPesoMaximo()


{
return pesoMaximo;
}

public void setPesoMaximo(double pesoMaximo)


{
this.pesoMaximo = pesoMaximo;
}
}

ACTIVIDAD

Pruebe el funcionamiento del método realizarCambio() llenando la siguiente tabla. Asuma


los siguientes valores y pesos:

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

2.5.3 Problema de la Devuelta

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

public int realizarCambio()throws Exception


{
int n=misMonedas.size(), i, j;
int c[][]=new int[n][(int)(cambio+1)];
String mensaje="";

for(i=0; i<n; i++)


{
c[i][0]=0;
}
for(i=0; i<n; i++)
{
mensaje+=c[i][0]+" ";

for(j=1; j<=cambio; j++)


{
if(i==0 && j<misMonedas.get(i))
{
c[i][j]=999999;
}
else
{
if(i==0)
{
c[i][j]=1+c[0][j-misMonedas.get(0)];
}
else
{
if(j<misMonedas.get(i))
{
c[i][j]=c[i-1][j];
}
else
{
if(c[i-1][j]<1+c[i][j-misMonedas.get(i)])
{
c[i][j]=c[i-1][j];
}
else
{
c[i][j]=1+c[i][j-misMonedas.get(i)];
}
}
mensaje+=c[i][j]+" ";
}
mensaje+=" \n";
}
if( c[n-1][(int)(cambio)]>999999)
{
throw new Exception("No se pudo completar el cambio
solicitado");
}
return c[n-1][(int)(cambio)];
}

133
ACTIVIDAD

Llene la siguiente tabla basándose en el método anterior.

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.

Cuál es el orden de complejidad del método realizarCambio, justifique su respuesta.

2.5.4 Algoritmo de Dijkstra

El algoritmo de Dijkstra, también llamado algoritmo de caminos mínimos, es un algoritmo para


la determinación del camino más corto dado un vértice origen al resto de vértices en un grafo
con pesos en cada arista. Su nombre se refiere a Edsger Dijkstra, quien lo describió por
primera vez en 1959.

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.

A continuación se muestra la implementación del algoritmo de Dijkstra en programación


dinámica. Se consideran tres métodos para la solución del problema.

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

2.5.5 Hoja de trabajo: Lotería

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

b. Identificar las entidades o clases.

ENTIDAD DEL MUNDO DESCRIPCIÓN


Loteria

c. Implementación de métodos

public class Loteria


{
prívate int arreglo[];
prívate final int TOTAL=6;

public Loteria()
{
arreglo=new int[TOTAL];
}

public int[] generarCombinacionGanadora()


{
int dato;
for(int i=0; i<TOTAL; i++)
{

}
return arreglo;
}

136
public void resetear()
{
for(int i=0; i<TOTAL; i++)
{

//Esta es la version recursiva


public double calcularCombinatorias(int n, int k)
{
if(k==0||k==n)
{
return 1;
}
else
{
if(k>0&&k<n)
{
return calcularCombinatorias(n-1, k-1)+
calcularCombinatorias(n-1, k);
}
else
{
return 0;
}
}

ACTIVIDAD

1. Construya un árbol que permita obtener el valor de calcularCombinatorias(6,4) y halle el T(n)


de dicha función.

2. Construya una tabla con los valores obtenidos de la prueba anterior

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

Complete la siguiente tabla con base en los valores establecidos.

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.

public double calcularCombinatorias(int n, int k)


{

138
Verifique si la siguiente implementación corresponde a la solución del problema de los
coeficientes binomiales.

public int[] binomial (int n, int k)


{
int matriz [][]= new int [ n ] [ k ];

for (int i=0; i<n;i++)


{
matriz [i][0] = 1;
}
for (int i=1; i<n;i++)
{
matriz [i][1] = i;
}
for (int i=2; i<k;i++)
{
matriz [i][i] = 1;
}
for (int i=3; i<n;i++)
{
for (int j=2; j<i;j++)
{
if (j<k)
{
matriz [i][j] = matriz[i-1][j-1]+matriz[i-1][j];
}

}
}
return matriz;
}

139
140
3 ALGORITMOS APLICADOS A GRAFOS Y ARBOLES

3.1 Introducción

Los árboles son frecuentemente utilizados en: la programación, en analizadores sintácticos, en


sistemas de organización, en los sistemas operativos, en la búsqueda de datos. En aspectos
del mundo real se trabaja a diario con árboles, como por ejemplo como organigramas
organizacionales. Dentro del contexto del análisis de algoritmos, los grafos permiten la solución
de problemas relacionados con caminos cortos, planeación de tareas.

Inicialmente se analizaran los conceptos fundamentales de los arboles y después se analizará


lo relacionado con los grafos.

3.2 Arboles Binarios

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.

 Nodo: Un árbol se compone de cero, uno o más nodos. Un nodo tiene


fundamentalmente información que almacena en su campo de datos, una referencia al
hijo izquierdo y una referencia al hijo derecho. Cuando se crea un nodo, se almacena
su dato y sus referencias son nulas. En la figura xx se muestra una abstracción de lo
que representa un nodo.

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

3.2.1 Árboles Binarios de Expresión

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.

La expresión: ((p v q) ^ (st))  (s v ¬t), tiene el siguiente árbol de representació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

Dadas las siguientes fórmulas proposicionales, dibuje su árbol de formación:

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

El siguiente será el orden en el cual se aplicará la precedencia de los conectivos lógicos:

1. Operador de negación ¬
2. Operador de conjunción ^

condicional 
3. Operador de disyunción v

bicondicional 
4. Operador
5. Operador

Dado el siguiente árbol de formación, escriba su correspondiente fórmula proposicional.

Dado el siguiente árbol de formación, escriba su correspondiente fórmula proposicional.

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

Algunas calculadoras comerciales se apoyan en este tipo de árboles y específicamente en la


notación postfijo para poder resolver las operaciones planteadas. Por ejemplo la expresión 3 5
+ 6 2 – * (equivalente al árbol anterior) se resolvería como 8 4 * dando como resultado 32.

3.2.2 Implementación de Arboles Binarios

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:

public class Nodo


{
public Comparable info;
public Nodo derecho;
public Nodo izquierdo;

Método public Nodo (Comparable dato)


{
info = dato;
derecho = null;
izquierdo = null;
}
}
Inicialmente se tiene la clase Nodo, la cual es define dos
referencias las cuales determinan sus hijos tanto derecho como
Análisis del
izquierdo y un campo llamado dato el cual la almacenará la
Orden de
información del nodo. Esta clase tiene un método constructor y si se
Complejidad
analiza su orden de complejidad se puede deducir que es
constante, es decir, O(1).

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.

A continuación se analizará el orden de complejidad de cada uno de los métodos que


implementan la estructura de datos árbol.

 Buscar en un Arbol

El método buscar(Comparable dato, Nodo nodo), es un método iterativo,


fundamentalmente el conjunto de datos de entrada para este algoritmo está determinada
por la referencia de los nodos del árbol. Para nuestro análisis se debe tener en cuenta el
peor de los casos, y este es que el nodo que se está buscando es un nodo hoja. El método
retorna un valor booleano, recibe por parámetro el valor del nodo a buscar y el nodo, este
método divide el árbol a la mitad cada vez que realiza una comparación.

public boolean buscar(Comparable dato, Nodo nodo)


{
boolean salida;
salida = false;
while (nodo != null && !salida)
{
if (dato.compareTo(nodo.info) < 0)
{
nodo = nodo.izquierdo;
}
Método else if (dato.compareTo(nodo.info)> 0)
{
nodo = nodo.derecho;
}
else
{
salida = true;
}
}
return salida;
}
Cuando se hace cada comparación dentro del while, se está
Análisis del ignorando la mitad el árbol (ya sea la izquierda o la derecha), lo que
Orden de implica que el conjunto de datos de entrada se esta reduciendo a la
Complejidad mitad y se 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ó.

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

El método insertar, fundamentalmente inserta un nodo en el árbol, recibe como parámetro


un valor, ese valor en caso de ser mayor al valor del nodo en el que se encuentre, seguirá
comparando con el siguiente nodo del subárbol derecho, en caso contrario lo hará en el
subárbol izquierdo.

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

La operación de inserción continúa por el subárbol izquierdo.

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 método eliminar(Comparable dato, Nodo nodo), es un método recursivo. Para nuestro


análisis se debe tener en cuenta el peor de los casos, y este es que el nodo que se está
buscando es un nodo hoja. En el caso de eliminar un nodo de un árbol, se deben tener en
cuenta los siguientes casos:

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

 En el caso en el cual el nodo a eliminar, solo tenga un descendiente, entonces se


substituye por dicho descendiente.

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.

public Nodo eliminar_nodo(Comparable dato, Nodo nodo)


{
if (nodo == null)
{
return null;
}
if (dato.compareTo(nodo.info) < 0)
{
nodo.izquierdo =
eliminar_nodo(dato, nodo.izquierdo);
}
else if (dato.compareTo(nodo.info) > 0)
{
nodo.derecho =
Método eliminar_nodo(dato,nodo.derecho);
}
else if(nodo.izquierdo!=null && nodo.dercho!= null)
{
nodo.info = buscarMenor(nodo.derecho).info;
nodo.derecho = eliminarMenor(nodo.derecho);
}
else
{
nodo =
(nodo.izquierdo!=null)?nodo.izquierdo:nodo.derecho;
}
cantidad--;
return nodo;
}
Análisis del Para el peor de los casos, el elemento menor será la hoja del árbol, en
Orden de consecuencia el orden es O(log n).
Complejidad

 Buscar el menor elemento del árbol

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

En el siguiente grafico se observa que buscar el menor elemento, corresponde a recorrer el


árbol siempre por el subárbol izquierdo, hasta encontrar su hoja izquierda.

88

4 89

2 90

 Eliminar el menor elemento del árbol

Método recursivo que elimina el menor elemento del árbol, realiza siempre un recorrido por el
subárbol izquierdo.

public Nodo eliminarMenor(Nodo nodo)


{
if (nodo == null)
{
return null;
}
if (nodo.izquierdo != null)
Método {
nodo.izquierdo = eliminarMenor (nodo.izquierdo);
else
{
nodo = nodo.derecho;
}
return nodo;
}

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

Analice el siguiente método de implementación de la altura del árbol y determine su orden de


complejidad.

public static int altura(Nodo nodo)


{
if (nodo == null)
{
return -1;
}
Método
else
{
return 1+ Math.max(altura(nodo.izquierdo),
altura(nodo.derecho));
}
}
Análisis del
Orden de
Complejidad

3.2.3 Recorridos en Arboles Binarios

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.

Para el siguiente árbol los recorridos son:

“ballena”

“ratón” “tití”

“pez” “tigre” “ballena”

“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:

public String imprimirInorder(Nodo n)


{
if (n != null)
{
imprimirInorder(n.obtenerIzquierda());
salida+=((Persona) n.getData()).getNombre() + "
"+((Persona) n.getData()).getDireccion()+" "+((Persona)
n.getData()).getTelefono()+" \n ";
imprimirInorder(n.obtenerDerecha());
}
return salida;
}

ACTIVIDAD

Construya los métodos para los recorridos faltantes

public String imprimirPostorder(Nodo n)


{

public String imprimirPreorder(Nodo n)


{

Determine el orden de complejidad para los métodos:

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

Para la implementación se hará uso de un árbol binario de búsqueda

a. Identificar las entidades o clases.

ENTIDAD DEL MUNDO DESCRIPCIÓN


ArbolBinario
Nodo
Persona

b. Implementación de las clases

Para nuestro caso de estudio la implementación tendrá la siguiente forma:

public class Nodo


{
private Nodo izquierda;
private Nodo derecha;
private Object dato;

155
/** Constructor del nodo */
public Nodo(Nodo izquierda, Nodo derecha, Object dato)
{
fijarALaIzquierda(izquierda);
fijarALaDerecha( derecha);
setData( dato );
}

public Nodo obtenerIzquierda()


{
return this.izquierda;
}
public Nodo obtenerDerecha()
{
return this.derecha;
}
public Object getDato()
{
return this.dato;
}
public void fijarALaIzquierda(Nodo n)
{
this.izquierda = n;
}
public void fijarALaDerecha(Nodo n)
{
this.derecha = n;
}
public void setDato(Object d)
{
this.dato = d;
}
}

La siguiente es la implementación para la clase Árbol Binario.

public class ArbolBinario


{
private Nodo raiz;
private String salida=" ";
private boolean esta=false;

public void adicionarNodo( String nombre, String direccion, String


telefono ) throws RepetidoException
{
Persona dato=new Persona(nombre, direccion,telefono);
// Crear nodo
Nodo temp = new Nodo(null, null, dato);
if ( raiz == null )
{
raiz = temp;
}
else
{
verificarRepetido(nombre,raiz);
if(esta==false)
{
insertarNodo( raiz, temp );
}

156
else
{
esta=false;
throw new RepetidoException("elemento ya está");
}
}
}

public void verificar( String nombre, Nodo n)


{
esta=false;
verificarRepetido( nombre, n);
}

public void verificarRepetido( String nombre, Nodo n)


{
if (n != null)
{
verificarRepetido(nombre,n.obtenerIzquierda());
if(((Persona) n.getData()).getNombre().equals(nombre))
{
esta=true;
}
verificarRepetido(nombre,n.obtenerDerecha());
}
}

public void insertarNodo(Nodo n, Nodo temp)


{
String nInfoNombre, tInfoNombre;
nInfoNombre = ((Persona)n.getDato()).getNombre();
tInfoNombre = ((Persona)temp.getDato()).getNombre();

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

public void preorden(Nodo nodo)


{
if(nodo != null)
{
System.out.print(nodo.info + " ");
preorden(nodo.izquierdo);
preorden(nodo.derecho);
}
}
}

3.4 Hoja de trabajo: Graficador de árboles

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.

La aplicación debe permitir agregar un nombre, eliminar un nombre, eliminar el mínimo


elemento, hallar el nombre de máximo elemento y borrar el árbol. De igual forma el árbol
deberá poder mostrarse gráficamente.

158
a) Requerimientos funcionales

NOMBRE R1 –
RESUMEN

ENTRADAS

RESULTADOS

NOMBRE R2 –
RESUMEN
ENTRADAS

RESULTADOS

NOMBRE R3 –
RESUMEN
ENTRADAS

RESULTADOS

b. Identificar las entidades o clases.

ENTIDAD DEL MUNDO DESCRIPCIÓN


ArbolBinarioDeBusqueda
Nodo
ElementoDuplicadoException
ElementoNoEncontradoException

c. Las Relaciones entre las clases

159
d. Implementación de los métodos

A continuación se muestran los métodos de la clase Nodo.

public class Nodo


{
private Comparable informacion;
private int posicionX, posicionY;
private Nodo izquierdo,derecho;

public Nodo(Comparable informacion, Nodo izquierdo, Nodo derecho)


{
this.izquierdo = izquierdo;
this.derecho = derecho;
this.informacion = (Comparable ) informacion;
}

public Nodo getDerecho()


{
return derecho;
}

public Nodo getIzquierdo()


{
return izquierdo;
}

public int getPosicionY()


{
return posicionY;
}

public int getPosicionX()


{
return posicionX;
}

public Comparable getInformacion()


{
return informacion;
}
}

A continuación se muestra la implementación de los métodos mas importantes del árbol binario
de búsqueda.

public class ArbolBinarioDeBusqueda


{
private int totalNodosHorizontalEscala = 0; int alturaMaxima=0;
private String cadenaIngresada;
private Nodo root;

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

/**Permite hallar la altura del árbol


* @param t el nodo donde inicia */
public int hallarAlturaDelArbol(Nodo t)
{
if(t==null)
{
return -1;
}
else
{
return 1 +
obtenerMaximo(hallarAlturaDelArbol(t.getIzquierdo()),
hallarAlturaDelArbol(t.getDerecho()));
}
}

/** Halla el máximo de dos valores */


public int obtenerMaximo(int a, int b)
{
if(a>b)
{
return a;
}
else
{
return b;
}
}

public void calcularPosicionesDeNodos()


{
int contador = 1;
recorrerEnInorder(root, contador);
}

/** Permite insertar un elemento en el árbol


* @param x Es el elemento */
public void insertar( Comparable x ) throws Exception
{
root = insertarElemento( x, root );
}

161
/** Permite remover un elemento del árbol
* @param x el elemento que se va a eliminar */

public void remover( Comparable x ) throws


ElementoNoEncontradoException
{
root = eliminarElemento( x, root );
}

/** Permite eliminar el menor elemento del árbol */

public void removerElMenor( ) throws Exception


{
root = eliminarElMinimo( root );
}

/** Devuelve el menor elemento */


public Comparable hallarElMenor( )
{
return elementAt( hallarElMinimo( root ) );
}

/** Permite halla el mayor elemento del árbol */


public Comparable hallarMaximo( )
{
return elementAt( hallarElMaximo( root ) );
}

public Comparable hallarElemento( Comparable x )


{
return elementAt( hallar( x, root ) );
}

/** Permite borrar el árbol*/


public void vaciarArbol( )
{
root = null;
}

/** Indica si el árbol está vacío */


public boolean estaVacio( )
{
return root == null;
}

private Comparable elementAt( Nodo t )


{
if(t==null)
{
return null;
}
else
{
return t.getInformacion();
}
}

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

/** Elimina un nodo del árbol */

public Nodo eliminarElemento( Comparable x, Nodo t )throws


ElementoNoEncontradoException
{
if( t == null )
{
throw new ElementoNoEncontradoException(x.toString( )+
" no fue encontrado" );
}
if( x.compareTo( t.getInformacion() ) < 0 )
{
t.setIzquierdo(eliminarElemento(x,t.getIzquierdo() ));
}
else if( x.compareTo( t.getInformacion ()) > 0 )
{
t.setDerecho (eliminarElemento( x, t.getDerecho()));
}
else if( t.getIzquierdo()!=null && t.getDerecho()!=null )
{
t.setInformacion(hallarElMinimo(t.getDerecho() ).
getInformacion() );
t.setDerecho (eliminarElMinimo( t.getDerecho() ));
}
else
{
t = ( t.getIzquierdo() != null ) ? t.getIzquierdo() :
t.getDerecho();
}
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();
}
}

/** Permite encontrar un elemento dentro del árbol */


private Nodo hallar( Comparable x, Nodo t )
{
while( t != null )
{
if( x.compareTo( t.getInformacion() ) < 0 )
{
t = t.getIzquierdo();
}
else if( x.compareTo( t.getInformacion() ) > 0 )
{
t = t.getDerecho();
}
else {
return t; // Coincide
}
}
return null; // No coincide
}
/** Encuentra el menor elemento dentro del árbol*/
public Nodo hallarElMinimo( Nodo t )
{
if( t != null ){
while( t.getIzquierdo ()!= null ){
t = t.getIzquierdo();
}
}
return t;
}

/** Encuentra el máximo elemento dentro del árbol */


private Nodo hallarElMaximo( Nodo t )
{
if( t != null ){
while( t.getDerecho() != null )
{
t = t.getDerecho();
}
}
return t;
}

164
// Metodos set y get
public Nodo getRoot()
{
return root;
}

public void setRoot(Nodo root)


{
this.root = root;
}

public int getAlturaMaxima()


{
return alturaMaxima;
}

public int getTotalNodosHorizontalEscala()


{
return totalNodosHorizontalEscala;
}
}

Determine el orden de complejidad para los métodos:

recorrerEnInorder(Nodo t, int yCoordenada) O( )

hallarAlturaDelArbol(Nodo t) O( )

obtenerMaximo(int a, int b) O( )

calcularPosicionesDeNodos() O( )

insertarElemento( Comparable x, Nodo t ) O( )

eliminarElemento( Comparable x, Nodo t ) O( )

eliminarElMinimo( Nodo t ) O( )

hallar( Comparable x, Nodo t ) O( )

hallarElMinimo( Nodo t ) O( )

hallarElMaximo( Nodo t ) O( )

Escriba la versión iterativa para el método hallarElMaximo( Nodo t )

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.

3.5.1 Conceptos fundamentales de los Grafos

Según (Hernández G. , 2004), un grafo es un par G = (V,A), donde V es un conjunto finito no


vacío (a cuyos elementos llamaremos vértices) y A es una familia finita de pares no ordenados
de vértices de V (a cuyos elementos llamaremos aristas o arcos). Un grafo simple es un par
G=(V,A) donde V es un conjunto finito no vacío y A es un conjunto finito de pares no ordenados
de vértices distintos de V.

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.

A continuación se muestran los conceptos básicos de los grafos dirigidos y no dirigidos.

3.5.2 Grafos Dirigidos

Según (Universidad Autónoma Metropolitana , 2010), Un grafo dirigido, o dígrafo, es un par


donde V es un conjunto cuyos elementos se llaman vértices y E es un conjunto de
pares no ordenados de elementos distintos de V. Los vértices también suelen llamarse nodos.

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
.

Gráficamente se muestra la estructura de un grafo dirigido.

Una alternativa respecto a la representación con matriz de adyacencia es un arreglo indizado


por número de vértice que contiene listas ligadas llamadas listas de adyacencia. La siguiente
es la representación gráfica.

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.

3.5.3 Grafos No Dirigidos

Según (Universidad Autónoma Metropolitana , 2010), un grafo no dirigido es un par


donde V es un conjunto cuyos elementos se llaman vértices y E es un conjuntos de pares no
ordenados de elementos distintos de V. Los vértices también suelen llamarse nodos. Los
elementales de E se llaman aristas, o aristas no dirigidas, para hacer hincapié. Cada arista se
puede considerar como un subconjunto de V que contiene dos elementos; así denota
una arista no dirigida. En los diagramas esta arista es la línea . En el texto escribiremos
simplemente .

167
Asociados a los grafos se adiciona una terminología frecuentemente utilizada, a continuación
se definen los más comunes.

 Camino: Se define como camino a una secuencia de vértices y que se encuentra


conectados por arcos. La longitud de este camino es n-1.

 Camino simple: Es un camino en el que ningún vértice es igual a otro.

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

Puesto que el número de posibles aristas es en un grafo dirigido, o de n (n-1)/2 en un grafo


no dirigido, la complejidad de tales algoritmos estará en .

3.6 Arboles de Recubrimiento Mínimo

Un árbol de recubrimiento mínimo de un grafo, se identifica como un conjunto de aristas que


permiten la conexión de todos los vértices del mismo, y cuya suma de los pesos de las aristas
es menor en relación con la suma de cualquier otro conjunto de aristas que conecten a todos
los vértices del grafo.

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.

3.6.1 Algoritmo de Prim

Es un algoritmo usado en la teoría de grafos y tienen como objetivo encontrar un árbol de


recubrimiento mínimo. El algoritmo analiza el grafo y encuentra una serie de aristas en la cual
el peso total de las mismas es el mínimo posible.

A continuación se muestra una implementación para el algoritmo de Prim.

Esta implementación permite encontrar el camino mínimo de un


Descripción
grafo.

public int[] caminoMinimo(int[][] graf, int valor)


{
int nodos = graf.length;
Método
int Solucion[] = new int[nodos];
int minimo = 0, menor = 0, nodoact = valor;
Solucion[0] = valor;

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

public boolean estaEn(int[] arreglo, int i)


{
int j;
for ( j = 0; j < arreglo.length; ++j)
{
if ( arreglo[j] == i )
{
break;
}
}
if ( j < arreglo.length )
{
return true;
}
else
{
return false;
}
}
Análisis del El bucle principal se ejecuta n - 1 veces; en cada iteración, el bucle
Orden de para anidado requiere un tiempo que está en O(n). Por lo tanto, el
2
complejidad algoritmo de Prim requiere un tiempo que está en O(n ).

3.6.2 Algoritmo de Kruskal

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

3.6.3 Algoritmo de Dijkstra

Como se mencionó en capítulos anteriores, el Algoritmo de Dijkstra, determina el camino más


corto entre un determinado vértice y el resto de vértices de un grafo, en donde cada una de las
aristas contiene un peso.

Para el análisis del algoritmo de Dijkstra se va a contextualizar en una aplicación en una


empresa de telecomunicaciones la cual requiere tender fibra óptica entre dos ciudades. Por ello
debe elegir el camino a seguir de tal forma que los costos se reduzcan.

Se debe permitir:

 Ingresar el grafo que corresponde a como las ciudades están comunicadas


 Borrar el grafo ingresado
 Arrojar el camino menos costoso, haciendo uso del algoritmo de Dikjstra

170
a. Identificar las entidades o clases.

ENTIDAD DEL MUNDO DESCRIPCIÓN


Dijkstra Es la clase principal del mundo

b. Diagrama de clase

c. Implementación

A continuación se muestra la implementación de los principales métodos del algoritmo de


Dijkstra en el contexto de la empresa de comunicaciones.

import java.util.Stack;

public class Dijkstra


{

//Constante para indicar que no hay camino


private final int INF=Integer.MAX_VALUE; //Infinito
private int noExiste=8;
public int numVertices, primerNodo=0, ultimoVertice;
private int[] predecessor, []distancia;
private String[] misNombres;
private boolean arregloAuxiliar[];
private int pesos[][];

/* Constructor de la clase Dijkstra*/


public Dijkstra(int pesos[][],int numVertices,String misNombres[])
{
this.pesos=pesos;
this.numVertices=numVertices;
this.misNombres=misNombres;
distancia=new int[numVertices];
arregloAuxiliar=new boolean[numVertices];
predecesor=new int[numVertices];
ultimoVertice=numVertices-1;
}

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

for(i=0; i<numVertices; i++)


{
posicion=encontrarMinima();
arregloAuxiliar[posicion]=true;

if(distancia[posicion]==INF)
{
continue;
}

for(cont=primerNodo; cont<=ultimoVertice; cont++)


{
if(devolverSucesor(posicion,cont) &&
!arregloAuxiliar[cont] && (distancia[posicion]+
pesos[posicion][cont]<distancia[cont]))
{
distancia[cont]=distancia[posicion]+
pesos[posicion][cont];
predecesor[contador]=posicion;
}
}
}
}
private int encontrarMinima()
{
int j,posicion;

for(j=primerNodo; j<ultimoVertice; j++)


{
if(!arregloAuxiliar[j])
{
break;
}
}
assert(j<=ultimoVertice);
posicion=j;
for(j++; j<=ultimoVertice; j++)
{
if(!arregloAuxiliar[j]&&distancia[j]<distancia[posicion])
{
posicion=j;
}
return posicion;
}

172
public String devolverRutaMasCorta(int origen, int llegada)
{
String salida="";
assert(origen!=noExiste && llegada!=noExiste);
aplicarDijkstra(origen);

salida+=" El camino menos costoso de "+misNombres[origen]+


" a "+misNombres[llegada]+" es:\n";

Stack<Integer> miStack=new Stack<Integer>();

for(int v=llegada; v!=origen; v=predecesor[v])


{
if(v==noExiste)
{
salida=("No existe camino,el grafo no está conectado");
return salida;
}
else
{
miStack.push(v);
}
}

miStack.push(origen);
while(!miStack.empty())
{
salida+=misNombres[miStack.pop()]+" -> ";
}
salida=salida.substring(0,salida.length()-3);
return salida;

private boolean devolverSucesor(int x, int y)


{
return ((pesos[x][y]!=INF) && x!=y);
}

ACTIVIDAD

Analice la clase anterior y escriba un comentario en torno a su funcionamiento.

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”

“cocodrilo” “pez” “pantera”

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

public class ArbolNArio<T>


{
private Nodo<T> raiz;
private boolean asignado=false;
private Puesto miPuesto=null;

public String recorrer()


{
ArrayList <Nodo<T>> misNodos = new ArrayList<Nodo<T>>();
recorrerEnInorden(raiz, misNodos);
return misNodos.toString();
}

private void recorrerEnInorden(Nodo<T> miNodo,List<Nodo<T>>miLista)


{
miLista.add(miNodo);
for (Nodo<T> info : miNodo.getHijos())
{
recorrerEnInorden(info, miLista);
}
}

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

private void recorrerParaAsignar(Nodo<T> miNodo, List<Nodo<T>>


miLista,String cedula, String nombre, String direccion,
String puesto)
{
miLista.add(miNodo);
for (Nodo<T> info : miNodo.getHijos())
{
info.asignarPersona(cedula, nombre, direccion, puesto);
if(info.isAsignado()==true)
{
asignado=true;
}
recorrerParaAsignar(info, miLista,cedula, nombre,
direccion, puesto);
}
}

public void setRaiz(Nodo<T> rootElement)


{
this.raiz = rootElement;
}
public Nodo<T> getRaiz()
{
return this.raiz;
}
}

Un fragmento de la clase Nodo se presenta a continuación, se debe completar la clase.

public class Nodo<Tipo>


{
public Tipo informacion;
public ArrayList<Nodo<Tipo>> hijos;

175
boolean asignado=false;

public Nodo(Tipo informacion)


{
this.informacion =________________________;
}

public boolean isAsignado()


{
return asignado;
}

public void setAsignado(boolean asignado)


{
this.asignado = asignado;
}

public void setHijos(ArrayList<Nodo<Tipo>> hijos)


{
this.hijos = hijos;
}

public Tipo getInformacion()


{
return informacion;
}

public void setInformacion(Tipo informacion)


{
this.informacion = informacion;
}

public ArrayList <Nodo<Tipo>> getHijos()


{
if (hijos != null)
{
return hijos;
}
else
{
return new ArrayList<Nodo<Tipo>>();
}
}

public void agregarHijo(Nodo<Tipo> hijo)


{
if (hijos == null)
{
________= new ArrayList<Nodo<Tipo>>();
}
hijos.add(hijo);
}

public void eliminarHijoPosicion(int posicion) throws


IndexOutOfBoundsException
{
hijos.remove(posicion);
}

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

public void asignarPersona(String cedula, String nombre, String


direccion, String puesto)
{
asignado = false;

}
}

3.8 Actividad Independiente: Organigrama de empresa

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.

3.9 Arboles AVL

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

3.9.1 Elementos de los Árboles 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 de los arboles AVL.

3.9.2 Operaciones de los Árboles AVL

A continuación se muestran las operaciones fundamentales que se pueden realizar con los
arboles AVL.

 Inserción en un árbol AVL

Pueden surgir cuatro problemas al momento de realizar una inserción, a continuación se


presentan cuatro posibles casos:

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.

El esquema de rotación ha usar se muestra a continuación:

Árbol antes de la rotación Árbol después de la rotación simple

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

Inserte 1 al árbol e identifique si ha perdido su condición de equilibrio. Si es así, entonces


realice una rotación para restaurarla.
151

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.

Árbol antes de la rotación Árbol después de la rotación simple del 7

Este problema fue resuelto siguiendo el esquema de rotación presente a continuación:

Se puede observar que el nodo que ha quedado desequilibrado es 7. De acuerdo a nuestro


gráfico anterior entonces los valores de los nodos son: y= 7, x=8 y C=9.

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.

El árbol original El Árbol luego de insertar 4

Se puede observar que la inserción ha afectado al nodo 6, ya que se insertó un nodo en el


subárbol derecho del hijo izquierdo de Y. Por lo tanto debemos seguir el siguiente esquema
que corresponde a una rotación doble:

Rotación doble izquierda – derecha que soluciona el caso dos

182
Para este caso particular, se muestra a continuación el árbol después la inserción y
posteriormente la rotación.

Árbol luego de la inserción Árbol después de la rotación


En este caso el los valores del árbol después de la rotación son: y = 6, z= 3 y x= 4

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.

Rotación doble derecha – izquierda para solucionar el caso 3

A continuación se muestra la secuencia desde el árbol original hasta el árbol con el nodo
insertado.

Árbol original Árbol después de la inserción de 19

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.

3.10 Caso de estudio Grupo de estudiantes

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:

 Permitir calcular la nota definitiva de cada estudiante


 Listar de forma ordenada por código las notas definitivas del grupo
 Calcular el porcentaje de estudiantes que aprobaron el curso

184
Es de anotar que el objetivo de este caso de estudio es introducir el concepto de árbol AVL.

a. Identificar las entidades o clases.

ENTIDAD DEL MUNDO DESCRIPCIÓN


Persona
Nodo
ArbolAvl

Luego de haber entendido los conceptos referentes a árboles AVL, podemos proceder a la
construcción de la aplicación

b. Implementación de las clases

public class Persona


{
private int codigoEstudiante;
private double nota1, nota2, notaDefinitiva;

/* Método constructor de la clase Estudiante */


public Persona(int codigoEstudiante, double nota1, double nota2 )
{
this.codigoEstudiante=codigoEstudiante;
this.nota1=nota1;
this.nota2=nota2;
}

/**Permite calcular la nota definitiva*/


public double calcularDefinitiva()
{
notaDefinitiva=(nota1+nota2)/2;
return notaDefinitiva;
}

185
/**Devuelve nota definitiva*/
public double getNotaDefinitiva()
{
return notaDefinitiva;
}

/**Devuelve un String con la información del estudiante


* @return String Puede tomar los valores de "Perdió" o "Ganó" */
public String determinarEstado()
{
if(notaDefinitiva<3)
{
return "Perdió";
}
else
{
return "Ganó";
}
}
public int getCodigoEstudiante(){
return codigoEstudiante;
}
}

La siguiente es la implementación de la clase Nodo para los arboles AVL.

public class Nodo<T>


{
private Persona miInformacion;
private Nodo<T> izquierdo, derecho;
private int altura;

public Nodo( Persona informacion ){


this( informacion, null, null );
}

public Nodo( Persona informacion, Nodo<T> izquierdo,


Nodo<T> derecho )
{
this.miInformacion = informacion;
this.izquierdo = izquierdo;
this.derecho = derecho;
this.altura = 0;
}

public Persona getInformacion()


{
return miInformacion;
}

public Nodo<T> getIzquierdo()


{
return izquierdo;
}
public Nodo<T> getDerecho()
{
return derecho;
}

186
public int getAltura()
{
return altura;
}
}

A continuación se muestra la implementación de los principales métodos de la clase Arbol AVL.

public class ArbolAvl<T extends Comparable<? super T>>


{
private Nodo<T> raiz;
private String salida, array[];
private int contador,i;

public ArbolAvl( )
{
salida="";
raiz = null;
contador=0;
i=0;
}

/** Inserta un nuevo elemento en el árbol.


* @param nuevo es el elemento que se desea insertar */
public void insertarElemento( Persona nuevo ) throws Exception
{
raiz = insertar( nuevo, raiz );
contador++;
}

/**Metodo para insertar un dato en un subárbol.


* @param nuevo La información a insertar.
* @param miNodo El nodo que es la raiz en el subárbol.
* @return La nueva raíz en el subárbol.
*/
private Nodo<T> insertar( Persona nuevo, Nodo<T> miNodo )
throws Exception
{
if( miNodo == null )
{
return new Nodo<T>( nuevo, null, null );
}

int resultadoComparacion = nuevo.compareTo( miNodo.


getInformacion() );
if( resultadoComparacion < 0 )
{
miNodo.setIzquierdo ( insertar( nuevo,
miNodo.getIzquierdo() ));
if( darAltura( miNodo.getIzquierdo ()) - darAltura( miNodo.
getDerecho() ) == 2 )
{
if(nuevo.compareTo(miNodo.getIzquierdo().
getInformacion())< 0 )
{
miNodo = rotarConHijosALaIzquierda( miNodo );
}

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

/** Halla el elemento más pequeño del árbol


* @throws Exception cuando el árbol esta vacío */
public Persona hallarMinimo( ) throws Exception
{
if( !estaVacio( ) ){
return hallarMinimo( raiz ).getInformacion();
}
else
{
throw new Exception("El árbol está vacío");
}
}

/** Halla el dato más pequeño en un subárbol


* @param miNodo El nodo raíz en el subárbol */
private Nodo<T> hallarMinimo( Nodo<T> miNodo )
{
if( miNodo == null ){
return miNodo;
}
while( miNodo.getIzquierdo() != null ){
miNodo = miNodo.getIzquierdo();
}
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 ");
}
}

/**Metodo para hallar el dato más grande en un subárbol


* @param miNodo El nodo raíz en el subarbol*/
private Nodo<T> hallarMaximo( Nodo<T> miNodo )
{
if( miNodo == null )
{
return miNodo;
}
while( miNodo.getDerecho() != null )
{
miNodo = miNodo.getDerecho();
}
return miNodo;
}

/** Halla un elemento en el árbol, true si lo encuentra.


* @param elemento Es el elemento buscado */
public boolean estaContenido( T elemento )
{
return contener( elemento, raiz );
}

/**Halla un item en el subárbol, true si es hallado


* @param buscado es el elemento que se quiere buscar.
* @param miNodo el nodo que es la raíz en el árbol*/
private boolean contener( T buscado, Nodo<T> miNodo )
{
while( miNodo != null )
{
int compareResult =
buscado.compareTo((T)miNodo.getInformacion());
if( compareResult < 0 )
{
miNodo = miNodo.getIzquierdo();
}
else if( compareResult > 0 )
{
miNodo = miNodo.getDerecho();
}
else{
return true; // Coincide
}
}
return false; // No coincide
}

189
/** Elimina el árbol */
public void vaciarArbol( )
{
raiz = null;
}

/** Indica si el árbol está vacío


* @return Si está vacío devuelve true, sino false */
public boolean estaVacio( )
{
return raiz == null;
}

/** Imprime el contenido del árbol */


public void imprimirArbol( ) throws Exception
{
if( !estaVacio( ) )
{
imprimir( (raiz) );
}
else
{
throw new Exception("El árbol está vacio");
}
}

/** Permite imprimir el árbol en inorder


* @param miNodo Es la raíz del árbol */
private void imprimir( Nodo<T> miNodo )
{
if( miNodo != null ){
imprimir( miNodo.getIzquierdo() );
System.out.println( "Codigo "+ miNodo.getInformacion().
getCodigoEstudiante()+" Nombre "+ miNodo.getInformacion().
getNotaDefinitiva() );
imprimir( miNodo.getDerecho() );
}
}

private int devolverCantidadNodos( Nodo<T> miNodo )


{
return contador;
}

public String [] listarConDefinitivas( ) throws Exception


{
i=0;
array=new String[devolverCantidadNodos(raiz)];

if( !estaVacio( ) )
{
imprimirPorDefinitiva( raiz );
return array;
}
else
{
throw new Exception("El árbol está vacio");
}
}

190
/** Imprime la nota definitiva */

private void imprimirPorDefinitiva( Nodo<T> miNodo )


{
if( miNodo != null )
{
imprimirPorDefinitiva( miNodo.getIzquierdo() );
array[i] ="Codigo "+ miNodo.getInformacion().

getCodigoEstudiante() +
"Definitiva"+miNodo.getInformacion().getNotaDefinitiva() ;
i++;
imprimirPorDefinitiva( miNodo.getDerecho());
}
}

/** Retorna el porcentaje de estudiantes que perdieron */


public double obtenerPorcentajePerdieron()throws Exception
{
return 100-obtenerPorcentajeGanaron();
}

/** Retorna el porcentaje de estudiantes que ganaron. */


public double obtenerPorcentajeGanaron( ) throws Exception
{
double porcentaje=0;
i=0;

if( !estaVacio( ) )
{
devolverPorcentajeGanan( raiz );
return (i*100)/devolverCantidadNodos(raiz);
}
else
{
throw new Exception("El árbol está vacio");
}
}

private void devolverPorcentajeGanan( Nodo<T> miNodo)


{
if( miNodo != null )
{
devolverPorcentajeGanan( miNodo.getIzquierdo() );
if(miNodo.getInformacion().getNotaDefinitiva()>=3)
{
i++;
}
devolverPorcentajeGanan( miNodo.getDerecho());
}
}

/** Devuelve la altura del nodo, si es null entonces manda -1 */


private int darAltura( Nodo<T> miNodo )
{
if(miNodo == null )
{
return -1;
}

191
else
{
return miNodo.getAltura();
}
}

/** Rotación simple caso 1 */


private Nodo<T> rotarConHijosALaIzquierda( Nodo<T> miNodo )
{
Nodo<T> k1 = miNodo.getIzquierdo();
miNodo.setIzquierdo ( k1.getDerecho());
k1.setDerecho ( miNodo);
miNodo.setAltura(Math.max( darAltura( miNodo.getIzquierdo()),
darAltura( miNodo.getDerecho()))+1);
k1.setAltura ( Math.max( darAltura( k1.getIzquierdo() ),
miNodo.getAltura() ) + 1);
return k1;
}

/** Rotacion simple caso 4 */


private Nodo<T> rotarConHijosALaDerecha( Nodo<T> miNodo )
{
Nodo<T> k2 = miNodo.getDerecho();
miNodo.setDerecho (k2.getIzquierdo());
k2.setIzquierdo( miNodo);
miNodo.setAltura(Math.max( darAltura( miNodo.getIzquierdo() ),
darAltura( miNodo.getDerecho() ) ) + 1);
k2.setAltura ( Math.max( darAltura( k2.getDerecho() ),
miNodo.getAltura())+1);
return k2;
}

/** Rotación doble caso 2 */


private Nodo<T> rotarDoubleHijosIzquierda( Nodo<T> miNodo )
{
miNodo.setIzquierdo(rotarConHijosALaDerecha(
miNodo.getIzquierdo() ));
return rotarConHijosALaIzquierda( miNodo );
}

/** Rotacion doble caso 3 */


private Nodo<T> rotarDobleHijosDerecha( Nodo<T> miNodo )
{
miNodo.setDerecho ( rotarConHijosALaIzquierda(
miNodo.getDerecho() ));
return rotarConHijosALaDerecha( miNodo );
}
}

ACTIVIDAD

Determine el orden de complejidad para los métodos:

hallarMinimo( ) O( )

192
hallarMaximo( ) O( )

estaContenido( T elemento ) O( )

listarConDefinitivas( ) O( )

imprimirPorDefinitiva( Nodo<T> miNodo ) O( )

obtenerPorcentajeGanaron( ) O( )

darAltura( Nodo<T> miNodo ) O( )

rotarConHijosALaIzquierda( Nodo<T> miNodo ) O( )

rotarConHijosALaDerecha( Nodo<T> miNodo ) O( )

rotarDoubleHijosIzquierda( Nodo<T> miNodo ) O( )

rotarDobleHijosDerecha( Nodo<T> miNodo ) 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

El backtracking (conocido también como vuelta atrás) permite encontrar soluciones a


problemas. Su funcionamiento podría verse como una búsqueda sistemática, en la cual
básicamente lo que se hace es probar todo lo posible hasta encontrar la solución o determinar
que no existe solución para dicho problema. Este tipo de algoritmos presentan naturaleza
recursiva. Se caracterizan además porque en caso de no hallar solución a una subtarea se
vuelva a la subtarea original y se ensaya por otra ruta diferente a las que ya se han probado.

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.

a. Identificar las entidades o clases.

ENTIDAD DEL MUNDO DESCRIPCIÓN


Reinas Es la clase principal del mundo

b. Diagrama de Clases

194
c. Implementación de los métodos

A continuación se muestra la implementación de los principales métodos de la clase Reinas.

public class Reinas


{
private int resultado[], contador;
private String salida;

/** Retorna verdadero si la ubicación no causa conflicto


* @param q[]. Arreglo que contiene las posiciones q!=null,n>=0
* @param n. Posición a examinar para determinar si es
* consistente */

public boolean determinarSiEsConsistente(int[] q, int n)


{
for (int i = 0; i < n; i++)
{
if (q[i] == q[n])
{
salida= q[i]+","+i+ ","+q[n] +","+n;
return false; // igual columna
}
if ((q[i] - q[n]) == (n - i))
{
salida= q[i]+","+i+ ","+q[n] +","+n;
return false; // igual diagonal principal
}
if ((q[n] - q[i]) == (n - i))
{
salida= q[i]+","+i+ ","+q[n] +","+n;
return false; // igual diagonal secundaria
}
}
return true;
}

/** Permite llenar el array con la ubicación de las reinas


* @param q: arreglo que contiene las posiciones q!=null */
public void obtenerUbicacionReinas(int[] q)
{
resultado=new int[8];
for (int i = 0; i <q.length; i++)
{
resultado[i]=q[i];
}
}

/** Considera las permutaciones posibles usando backtracking


* @param n. Es el tamaño del arreglo n>0 */
public void enumerarPosibilidar(int n)
{
int[] a = new int[n];
resultado=new int[8];
contador=0;
salida="";
enumerarPosibilidad(a, 0,(int)(Math.random()*8));
}

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

public void enumerarPosibilidad(int[] q, int n,


int aleatorio)
{
int N = q.length;
if (n == N)
{
contador++;
if(contador==aleatorio)
{
obtenerUbicacionReinas(q);
}
}
else
{
for (int i = 0; i < N; i++)
{
q[n] = i;
if (determinarSiEsConsistente(q, n))
{
enumerarPosibilidad(q, n+1,aleatorio);
}
}
}
}
/** Determinar si un arreglo lleno es consistente: se debe
examinar cada una de las posiciones y en caso de que una
falle entonces se devuelve falso.
* @param q es el arreglo. q>=0
* @param n es el tope del arreglo n>=0 */
public boolean determinarConsistencia (int[] q, int n)
{
for(int i=0; i<n; i++)
{
if(!determinarSiEsConsistente(q, i))
{
return false;
}
}
return true;
}

/** Devuelve la ruta con las posiciones erróneas */


public String getSalida()
{
return salida;
}

/**Permite fijar la ruta con las posiciones erróneas */


public void setSalida(String salida)
{
this.salida = salida;
llamarVerificarInvariante();
}

196
/**Permite obtener el array con las posiciones */
public int[] getResultado()
{
return resultado;
}

/**Permite fijar el array con el resultado*/


public void setResultado(int[] resultado)
{
this.resultado = resultado;
llamarVerificarInvariante();
}
}

ACTIVIDAD

Analice el método enumerarPosibilidar

public void enumerarPosibilidar(int n)


{
int[] a ;
resultado=new int[8];
contador=0;
salida="";
a=new int[n];
enumerarPosibilidad(a, 0,(int)(Math.random()*8));
}

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.

3.13 Actividad Independiente: Laberinto.

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

ENTIDAD DEL MUNDO DESCRIPCIÓN


Laberinto Es la clase principal del problema
Posicion

b. Diagrama de Clases

c. Implementación de métodos

public class Laberinto


{
private char[][] matriz;
private ArrayList<Posicion> rutaSolucion;
private final char noSePuede = '*';
private final char vacio = '_';
private final char ruta = 'c';
private final char muro = 'X';

public Laberinto()
{
matriz=new char[12][12];
rutaSolucion=new ArrayList<Posicion>();
}

/** Devuelve la matriz de caracteres*/


public char[][] getMatriz()
{
return matriz;
}

198
/** Permite fijar la matriz */
public void setMatriz(char[][] matriz)
{
this.matriz = matriz;
}

/** Este método permite crear el laberinto */


public void crearLaberinto()
{

}
}

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

4.2 Estructura de Datos 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.

4.2.1 Lista Sencillamente Enlazada – Implementación 1.

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.

A continuación se mostrará la implementación de los métodos más relevantes asociados a las


listas sencillamente enlazadas, con sus respectivos órdenes de complejidad.

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.

public class Nodo


{
Object info;
Nodo siguiente;
Método
Nodo (Object dato)
(Clase
{
Completa)
info = dato;
siguiente = null;
}
}
Análisis del La clase Nodo, tiene un método constructor llamado Nodo(Object
Orden de dato), cuyo orden de complejidad es O(1).
Complejidad

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.

public class Lista


{
Nodo cabecera;
int cantidad;
Método
(Clase Lista()
Completa) {
cabecera = new Nodo(null);
cantidad = 0;
}
}
Análisis del La clase Lista, tiene un método constructor llamado Lista( ), cuyo
Orden de orden de complejidad es O(1).
Complejidad

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.

public void eliminar()


{
if (cabecera.siguiente == null)
{
System.out.print("Lista esta vacia");
}
Método
else
{
cabecera.siguiente =
cabecera.siguiente.siguiente;
}
}
Análisis del El orden de complejidad para este método es O(1), puesto que solo
Orden de se tienen operaciones elementales en su implementación.
Complejidad

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

El método buscar(Object elemento), busca un nodo específico en la lista de acuerdo al valor


enviado por parámetro, realiza un recorrido de la lista desde el primer nodo de la lista hasta el
último nodo de la lista.
public boolean buscar(Object elemento)
{
Nodo p = new Nodo();
p = cabecera.siguiente;
while(p!= null && !p.info.equals(elemento))
{
Método p = p.siguiente;
if (p == null)
{
return false;
}
return true;
}

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.

El método insertar(Object elemento), permite insertar un nodo siguiente al nodo cabecera.


public void insertar(Object elemento)
{
Nodo n = new Nodo(elemento);

if (cabecera.siguiente == null)
{
cabecera.siguiente = n;
}
Método
else
{
n.siguiente = cabecera.siguiente;
cabecera.siguiente = n;
}
cantidad++;
}

El orden de complejidad de este método es O(1), por que solo se


Análisis del tienen operaciones elementales en su implementación tanto la
Orden de sentencia if como la sentencia else son de O(1) y por la regla de la
Complejidad suma se puede afirmar que su orden es constante.

El método Nodos(), retorna la cantidad de nodos que tenga la lista hasta ese momento.

public int Nodos()


{
Método
return cantidad;
}
Análisis del El orden de complejidad para este método es O(1), solo posee un
Orden de retorno de un valor, se considera una operación elemental.
Complejidad

4.2.2 Lista Sencillamente Enlazada – Implementación 2.

A continuación, se mostrará otra la implementación de la clase nodo a partir de la cual se


puede empezar a construir la lista sencillamente enlazada. Un nodo es la estructura básica a
partir de la cual podemos construir una lista.

public class Nodo


{
Object dato;
Nodo siguiente;
Método public Nodo(Object O){
dato = O;
siguiente = null;
}
}

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

El método constructor de la lista sencilla permite crear una lista vacía.

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

El método estaVacia(), retorna un valor booleano, si la referencia siguiente de la cabecera es


null quiere decir que la lista no tiene mas nodos y retorna verdadero, de lo contrario la lista
tiene nodos y retorna falso

public boolean estaVacia()


{
if ( cabecera.siguiente==null)
{
return true;
Método }
else
{
return false;
}
}
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

El método insertarInicio(Object element) 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, el nodo se inserta al lado del nodo cabecera.

public void insertarInicio(Object elemento)


{
Nodo nuevo = new Nodo(elemento);
if( estaVacia()){
cabecera.siguiente = nuevo;
ultimo=nuevo;
actual= nuevo;
Método }
else
{
nuevo.siguiente=cabecera.siguiente;
cabecera.siguiente=nuevo;
actual = nuevo ;
}
}
El orden de complejidad de este método es O(1), el bloque
Análisis del
constituido por el condicional if es de orden O(1) y el bloque
Orden de
constituido por la sentencia else también es de O(1), por lo tanto
Complejidad
O(1)+O(1) por la regla de la suma tenemos O(1).

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

Lista con dos nodos. null

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.

El método insertarUltimo(Object elemento) 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 si ya existen nodos en la lista lo inserta al final de la lista.

public void insertarUltimo(Object elemento)


{
Nodo nuevo = new Nodo (elemento);
if(estaVacia())
{
ultimo.siguiente=nuevo;
ultimo= nuevo;
actual = nuevo;
Método
}
else
{
ultimo.siguiente=nuevo;
ultimo = nuevo;
actual = nuevo;
}
}
El orden de complejidad de este método es O(1), el bloque
Análisis del
constituido por el condicional if es de orden O(1) y el bloque
Orden de
constituido por la sentencia else también es de O(1), por lo tanto
Complejidad
tenemos O(1).

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

A continuación se muestra la inserción de un nodo cuando ya existen nodos en la lista, el


parámetro para este ejemplo es 10, por lo tanto la lista queda con tres nodos.

null 3 12 10

null

La siguiente figura muestra la inserción de un nodo cuando ya existen nodos en la lista, el


parámetro para este ejemplo es la cadena Tel, por lo tanto la lista queda con tres nodos.

3 12 null
null

insertarUltimo. null

El método eliminarInicio() permite eliminar el nodo siguiente al nodo cabecera, inicialmente


verifica si la lista esta vacía, si esta vacía muestra un mensaje que advierte que la lista esta
vacía, de lo contrario ya existen nodos en la lista y lo que hace es eliminar el primer nodo de la
lista.

public void eliminarInicio()


{
Nodo borrar = cabecera.siguiente;
if(estaVacia())
{
JOptionPane.showMessageDialog("Lista vacia");
}
else
{
if(borrar==ultimo)
{
Método
cabecera.siguiente=null;
actual=cabecera;
ultimo=actual;
}
else
{
cabecera.siguiente=borrar.siguiente;
actual=cabecera.siguiente;
}
}
}

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 sun null


null

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.

4.2.3 Lista Sencilla Circular

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

A continuación, se mostrará la clase nodo a partir de la cual se puede empezar a construir la


lista sencillamente enlazada circular.

public class Nodo


{
Object dato;
Nodo siguiente;

Método public Nodo(Object O)


{
dato = O;
siguiente = null;
}
}
Análisis del El orden de complejidad para la clase Nodo es O(1).
Orden de
Complejidad

El método constructor de la lista sencilla se crea con una lista vacía.

209
ListaSencillaCircular()
{
cabecera = new Nodo(null);
Método ultimo = cabecera;
actual = cabecera;
}

Orden de El orden de complejidad para el método lista sencilla 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.

public void nuevo()


{
cabecera.siguiente=null;
Método
ultimo=cabecera;
actual=cabecera;
}
Orden de El orden de complejidad de este método es O(1).
Complejidad

El método estaVacia(), retorna un valor booleano, si la referencia siguiente de la cabecera es


null, quiere decir que la lista no tiene mas nodos y retorna verdadero.

public boolean estaVacia()


{
if ( cabecera.siguiente==null)
{
return true;
Método
}
else{
return false;
}
}
Orden de El orden de complejidad para este método es O(1) .
Complejidad

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

Lista circular vacía.

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.

null 234 21 312

Lista circular con tres nodos.

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.

public void insertarInicio(Object elemento )


{
Nodo nuevo = new Nodo(elemento);

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

Lista circular vacía.

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.

public void insertarUltimo(Object elemento)


{
Nodo nuevo = new Nodo (elemento);
if(estaVacia())
{
ultimo.siguiente=nuevo;
nuevo.siguiente=ultimo;
ultimo= nuevo;
actual = nuevo;
Método
}
else
{
ultimo.siguiente=nuevo;
nuevo.siguiente=cabecera;
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

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

Figura 20. insertarUltimo.

El método eliminarInicio() permite eliminar el nodo siguiente al nodo cabecera, inicialmente


verifica si la lista esta vacía, si esta vacía muestra un mensaje que advierte que la lista esta
vacía, de lo contrario ya existen nodos en la lista y lo que hace es eliminar el primer nodo de la
lista.

public void eliminarInicio()


{

Nodo borrar = cabecera.siguiente;


if(estaVacia())
Método
{

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

Lista sencilla circular.

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.

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

La siguiente figura muestra una lista doblemente enlazada.

null 10 33 85

null null
Lista Doblemente enlazada.

A continuación, se mostrará a clase nodo a partir de la cual se puede empezar a construir la


lista doblemente enlazada.

213
public class NodoDE
{
NodoDE izquierda;
Object dato;
NodoDE derecha;

Método public NodoDE(Object elemento)


{
izquierda = null;
dato = elemento;
derecha = null;
}
}
Orden de El orden de complejidad del método constructor es O(1)
Complejidad

El método constructor de la lista doble se crea con una lista vacía.

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.

public void nuevo()


{
cabecera.derecha=null;
Método ultimo=cabecera;
actual=cabecera;
tamaño=0;
}
Orden de El orden de complejidad de este método es O(1), ya que todas las
Complejidad instrucciones que la implementan son constantes.

El método estaVacia(), retorna un valor booleano, si la referencia siguiente de la cabecera es


null, quiere decir que la lista no tiene mas nodos y retorna verdadero.

public boolean estaVacia()


{
if ( cabecera.derecha==null)
{
return true;
Método }
else
{
return false;
}
}
Orden de El orden de complejidad para este método es O(1) .
Complejidad

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.

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.

public void insertarInicio(Object elemento)


{
NodoDE nuevo = new NodoDE(elemento);
NodoDE temporal = cabecera.derecha;

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.

El método siempre insertará un nodo a la derecha del nodo cabecera.

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.

public void insertarUltimo(Object elemento)


{
NodoDE nuevo = new NodoDE (elemento);

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.

La lista una vez insertado el nodo queda de la siguiente manera.

null 1 2 3

null Lista Doblemente enlazada. null

El método eliminarInicio() permite eliminar el nodo siguiente al nodo cabecera, inicialmente


verifica si la lista esta vacía, si esta vacía muestra un mensaje que advierte que la lista esta
vacía, de lo contrario ya existen nodos en la lista y lo que hace es eliminar el primer nodo de la
lista.

public void eliminarInicio()


{
NodoDE borrar = cabecera.derecha;

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

Posterior a la eliminación del nodo, la estructura de la lista queda de la siguiente manera.

null 233 69

null null
Lista Doblemente enlazada.

4.2.5 Lista Circular 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.

La siguiente figura muestra una lista doblemente enlazada.

null 34 21 78

Lista Circular Doblemente Enlazada

217
A continuación, se mostrará la clase nodo a partir de la cual se puede empezar a construir la
lista doblemente enlazada circular.

public class NodoDE


{
NodoDE izquierda;
Object dato;
NodoDE derecha;

public NodoDE(Object elemento)


Método
{
izquierda = null;
dato = elemento;
derecha = null;
}
}

Orden de El orden de complejidad para el método nodo doblemente enlazada


Complejidad es O(1).

El método constructor de la lista doble se crea con una lista vacía.

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.

public void nuevo()


{
cabecera.derecha=null;
Método ultimo=cabecera;
actual=cabecera;
tamaño=0;
}
Orden de El orden de complejidad de este método es O(1), ya que todas las
Complejidad instrucciones que la implementan son constantes.

El método estaVacia, retorna un valor booleano, si la referencia siguiente de la cabecera es


null, quiere decir que la lista no tiene más nodos y retorna verdadero.

public boolean estaVacia()


{
if ( cabecera.derecha==null){
return true;
}
Método
else
{
return false;
}
}

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

Lista vacia doblemente enlazada circular

Para este segundo caso el método retornará verdadero, teniendo en cuenta que la lista se
compone de varios nodos.

null 500 21 64

Lista doblemente enlazada circular

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.

public void insertarInicio(Object elemento)


{
NodoDE nuevo = new NodoDE(elemento);
NodoDE temporal = cabecera.derecha;

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

Lista doblemente enlazada circular

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.

Lista doblemente enlazada circular

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.

public void insertarUltimo(Object elemento)


{
NodoDE nuevo = new NodoDE (elemento);

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.

El método eliminarInicio permite eliminar el nodo siguiente al nodo cabecera, inicialmente


verifica si la lista esta vacía, si esta vacía muestra un mensaje que advierte que la lista esta
vacía, de lo contrario ya existen nodos en la lista y lo que hace es eliminar el primer nodo de la
lista.

public void eliminarInicio()


{
NodoDE borrar = cabecera.derecha;

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

Lista doblemente enlazada circular

Posterior a la eliminación del nodo, la lista queda configurada de la siguiente manera.

Lista doblemente enlazada circular

4.3 Actividad Independiente: Ciudades

La oficina de Planeación Nacional necesita una Aplicación que le permita manejar la


información fundamental de n ciudades del país. Cada una de las ciudades tiene un nombre,
una extensión en metros cuadrados y un número de habitantes.

Desde la aplicación se debe poder:

 Determinar cuál es la ciudad que tiene la menor cantidad de habitantes.


 Determinar cuál ciudad tiene la mayor cantidad de extensión en metros cuadrados.
 Buscar una ciudad por nombre.
 Determinar cuántas ciudades tienen más de 100000 habitantes.

221
a) Requerimientos funcionales

NOMBRE R1 – Mostrar Ciudad con más extensión en metros cuadrados.


RESUMEN

ENTRADAS

RESULTADOS

NOMBRE R2 – Mostrar Ciudad con menos cantidad de estudiantes.


RESUMEN
ENTRADAS

RESULTADOS

NOMBRE R3 – Buscar Ciudad por código


RESUMEN

ENTRADAS

RESULTADOS

R3 – Contar la cantidad de ciudades que tienen mas de 100000


NOMBRE
habitantes
RESUMEN
ENTRADAS

RESULTADOS

222
b. Identificar las entidades o clases.

ENTIDAD DESCRIPCIÓN
Ciudad
GrupoCiudades

c. Las relaciones entre las clases

d. Implementación

Se deben implementar las clases del dominio del problema asociadas con la estructura de lista
que considere más apropiada.

public class GrupoCiudades


{

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.

Una pila es un contenedor de objetos que se insertan y se eliminan siguiendo el principio


“Último en entrar, primero en salir” (LIFO: Last In First Out). Una pila es un conjunto ordenado
de elementos (los llamaremos nodos a partir de ahora), en el cual se pueden agregar y eliminar
elementos en un extremos que es llamado el tope de la pila. A continuación se muestra en la
figura una abstracción de de una pila.
tope
2

Pila con elementos

A diferencia del arreglo, la definición de la pila considera la inserción y eliminación de


elementos, por lo que una pila es un objeto dinámico en constante cambio. La definición
especifica que solo un extremo de la pila se designa como el tope. Pueden colocarse nuevos
elementos en el tope de la pila (en este caso el tope de la pila sube para corresponder al nuevo
elemento mas alto), o se pueden quitar elementos (en este caso el tope de la pila baja para
corresponder al nuevo elemento mas alto). Debemos decidir cual extremo de la pila se designa
como el tope – es decir, en cual extremo se agregan o suprimen elementos.

4.4.1 Implementación Basada en un arreglo

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:

public class Stack


{
public static final int CAPACIDAD = 2000;
Método public int capacidad, top = -1;
private Object s[];

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.

public boolean isEmpty()


{
Método
return(top<0);
}
Orden de El orden de complejidad para el método isEmpty() es constante,
Complejidad O(1).

Inserta un objeto en la pila, primero verifica si existe disponibilidad de espacio en la estructura


estática, si es cierta esta condición, se envía una excepción la cual muestra un mensaje de
advertencia, en caso de existir espacio en la estructura, se actualiza el índice del vector
mediante un incremento y se asigna en la posición el elemento que llega por parámetro en el
método.

public void push (Object obj)


throws StackFullException
{
Object elem;
if(top+1==capacidad)
{
Método throw new StackFullException ("Pila
Llena");
}
else{
s[++top]=obj;
}
}
Orden de El orden de complejidad para este método es O(1), complejidad
Complejidad constante.

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.

public Object top () throws StackEmptyException


{
if(isEmpty())
Método {
throw new StackEmptyException ("Pila
Vacia");
}

225
else
{
return s[top];
}
}

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

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.

public Object pop () throws StackEmptyException


{
Object elem;

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.

public class StackEmptyException extends Exception


{
StackEmptyException (String cadena)
{
System.out.println (cadena);
}
}
Método
public class StackFullException extends Exception
{
StackFullException (String cadena)
{
System.out.println(cadena);
}
}
Análisis del Estas clases tienen orden de complejidad constantes ya que
Orden de contienen llamados a métodos propios de lenguaje y por lo tanto su
Complejidad orden de complejidad es O(1).

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.

public class Pila


{
Nodo tope;

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

El método push(elemento) Inserta un objeto en la pila, como es una implementación diferente


basada en nodos, ya no se verifica la disponibilidad de espacio en memoria, pues esta
implementación esta basada en el concepto de estructura dinámica. Este método agrega un
nodo a la pila, y este nodo queda referenciado como el tope de la pila.

public void push(Object e)


{
Nodo n = new Nodo(e);
if (tope != null)
Método {
n.siguiente = tope.siguiente;
tope.siguiente = n;
}
}

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
}

Análisis del El orden de complejidad para el método isEmpty() es constante,


Orden de O(1)
Complejidad

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

4.5 Estructura de Datos Cola

La estructura de datos cola pertenece también a las estructuras lineales, y se caracteriza


porque las inserciones de nuevos elementos solo se permiten en uno de los extremos de la
estructura, que tradicionalmente se llama final de la cola, mientras que las consultas y

228
eliminación solo se permiten en el extremo opuesto de la cola, que tradicionalmente
llamaremos el frente de la cola.

Una cola es un contenedor de objetos que se insertan y se eliminan siguiendo el principio


“Primero en entrar, primero en salir” (FIFO: First In First Out). Al elemento de más tiempo en la
cola se le denomina frente de la cola.

4.5.1 Implementación Basada en un arreglo

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.

Uso del arreglo Q en forma circular, la configuración “normal” con f  r.

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.

public boolean isEmpty ()


{
boolean lleno = false;
if(f==r)
{
Método lleno= true;
return lleno;
}
return lleno;
}

Orden de Únicamente se esta retornando en el método un valor booleano, el


Complejidad orden de complejidad para el método isEmpty() es constante, O(1).

El método enqueue(Object nuevoElemento) inserta un objeto en la cola, primero verifica si


existe disponibilidad de espacio en la estructura estática, si es cierta esta condición, se envía
una excepción la cual muestra un mensaje de advertencia, en caso de existir espacio en la
estructura, se asigna en la posición el elemento que llega por parámetro en el método y se
actualiza el índice siempre garantizando que el vector se trabaje manera circular.

public void enqueue (Object nuevoElemento)


throws QueueFullException
{
if (size() == n-1)
{
Método throw new QueueFullException ("No se
puede insertar");
}
Q[r] = nuevoElemento;
r = (r+1) % n;
}

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.

public Object front ()throws QueueFullException


{
if (isEmpty ())
{
throw new QueveFullException ("Cola
Vacia");
Método
}
else
{
return Q[f];
}
}
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) y por lo tanto es constante.
Complejidad

El método size() retorna un valor entero, este valor entero determina la cantidad de elementos
que tiene la cola.

public int size ()


{
Método
return (n-f+r) % n;
}
Análisis del El orden de complejidad de este método es O(1) y por lo tanto es
Orden de constante.
Complejidad

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.

public Object dequeue ()throws QueueFullException


{
Object temp;
if (isEmpty ())
{
throw new QueueFullException ("Pila
Vacia");
}
Método else
{
temp = Q[f];
Q[f] = null;
f = (f+1) % n ;
return temp;
}
}
}

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

4.5.2 Implementación utilizando Listas

A continuación se muestra una implementación del concepto cola basándose en nodos,


inicialmente se explicará la funcionalidad de cada uno de los métodos de implementación, para
posteriormente entrar en detalle con el análisis y deducción del orden de complejidad.

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.

public class Cola{


Nodo frente;
Nodo ultimo;
int cantidad;

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

El método top retorna el frente de la cola.

Nodo top()
{
Método
return frente.siguiente;
}
Orden de El orden de complejidad para este método es O(1).
Complejidad

El método último retorna el elemento que se encuentra al final de la cola.

Nodo ultimo()
{
Método
return ultimo.siguiente;
}
Orden de El orden de complejidad para este método es O(1).
Complejidad

Método que retorna la cantidad de elementos que tiene la cola.

int longitud()
{
Método
return cantidad;
}
Orden de El orden de complejidad para este método es O(1).
Complejidad

4.6 Estructura ArrayList

En las estructuras contenedoras de tamaño fijo, de antemano se conoce cuál es la capacidad


que tendrá la estructura para almacenar elementos, es decir que cuando se está ejecutando la
aplicación si se necesita agregar un elemento y ya no exista capacidad, se genera un error.
Para solucionar este tipo de problemas es necesario utilizar una estructura contenedora flexible
que se pueda modificar durante la ejecución de la aplicación (Cardona, Jaramillo, & Villegas,
2008).

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.

Algunos métodos de los métodos predefinidos de la clase ArrayList se muestran a


continuación:

 size(): Devuelve el tamaño de la estructura, es decir, la cantidad de elementos que


contiene
 isEmpty(): Indica si hay o no elementos en el ArrayList.
 add(elemento): permite insertar un nuevo elemento al final de la estructura
contenedora.
 set(posición, elemento): remplaza el elemento que se hay en la posición indicada por
uno nuevo.
 contains(elemento): Si el elemento está contenido se devuelve true, de lo contrario
false.
 toArray(): Copia los elementos de la estructura a un arreglo de objetos.
 add(posición, elemento): permite insertar un elemento en la posición indicada. “Si ya
había un elemento en esa posición, el elemento que ya existe y todos los que se
encuentran a su derecha se correrán una posición hacia la derecha”.
 remove (posición): borra el elemento que está en la posición indicada, esto implica
que los elementos que estaban a la derecha del elemento eliminado se correrán hacia
la izquierda para ocupar el lugar eliminado. Esta operación hace que el tamaño de la
estructura se reduzca en 1.
 remove(elemento): en este caso se envía el objeto que se desea eliminar. Es
importante aclarar, que si se crea un nuevo elemento con los datos del objeto que se
desea eliminar no indica que sea el mismo elemento. Para que sean iguales deben
ocupar la misma posición de memoria, es decir, deben ser la mismas instancias.

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.

4.7 Caso de estudio – Universidad

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.

En el programa de la Universidad se debe poder: (1) agregar un nuevo programa académico,


(2) mostrar la lista completa de los programas académicos, (3) buscar programa académico
por el código, (4) mostrar el nombre de los programas que tengan más de 350 estudiantes.

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.

A continuación se muestra el diagrama de clases para el caso de estudio.

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.

La sintaxis para la declaración y creación de un ArrayList es la siguiente:

import java.util.ArrayList;

public class Universidad


{
private ArrayList <Programa> misProgramas ;
}

Es necesario para la declaración del ArrayList determinar que tipo de información se


almacenará en el, para este caso se tiene <Programa> y misProgramas es de tipo
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>();
}

A continuación se mostraran los métodos más comunes para la clase ArrayList.

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.

public void agregarPrograma( String nombre, String codigo,


String telefono, int estudiantes)
{
Programa miPrograma=new Programa(nombre,codigo,telefono,
estudiantes);
misProgramas.add(miPrograma);
}

En muchas ocasiones es necesario obtener un elemento de un ArrayList, para este caso se


utiliza el método get(i) , donde i indica la posición del elemento que se desea obtener. La
instrucción (misProgramas.get(i).getCodigo())obtiene el objeto subindicado i, y de
este objeto se obtiene el código. También es necesario obtener el tamaño del ArrayList por
ejemplo para establecer recorridos mediante ciclos, esta método es size(). Por ejemplo para
el ArrayList la instrucción misProgramas.size() retorna el tamaño de la estructura
contenedora.

c. Implementación de los métodos

La siguiente es la implementación de los métodos asociados a los requerimientos definidos


para el caso de estudio. Se analizará su orden de complejidad.

236
Inicialmente si tiene el método Constructor de la clase Universidad. En él se inicializa el arreglo
de programas académicos.

public class Universidad


{
private ArrayList <Programa> misProgramas ;

Método public Universidad()


{
misProgramas = new ArrayList<Programa>();
}

Orden de El orden de complejidad para este método es O(1).


Complejidad

Permite crear un programa académico al ArrayList y posteriormente adicionarlo a la colección


de programas mediante la operación add.

public void agregarPrograma( String nombre, String


codigo,String telefono, int estudiantes)
{
Método Programa miPrograma = new Programa(nombre,
codigo, telefono, estudiantes);
misProgramas.add(miPrograma);
}
Orden de El orden de complejidad para este método es O(1).
Complejidad

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.

public String[] getInfoProgramas()


{
String []info= new String
[misProgramas.size()];
for(int i=0; i<misProgramas.size(); i++)
Método
{
info[i]=misProgramas.get(i).toString();

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

El método buscarPrograma permite buscar un programa académico por el nombre. La clave


para realizar la búsqueda es el código del programa. En caso que exista el código en el
ArrayList, se retorna la referencia al programa con toda su información, en caso contrario, se
retorna una referencia nula.

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.

public String ProgramasConMasAlumnos()


{
String cadena = " ";
int i = 0;
while(i < misProgramas.size())
{
if((misProgramas.get(i).
getEstudiantes()>350))
Método
{
cadena+=misProgramas.get(i).
getNombre();
}
i++;
}
return cadena;
}
En este caso se debe realizar un recorrido por cada uno de los
Orden de programas académicos, entonces si se tienen n programas
Complejidad académicos, el orden de complejidad es O(n).

ACTIVIDAD

El método remove(objeto) permite eliminar un objeto de la estructura contenedora. Escriba


un método que permita eliminar todos los programas académicos cuya cantidad de estudiantes
sea inferior a 98.

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.

5.2 Técnicas de Optimización

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

5.2.1 Desenvolvimiento de ciclos

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.

public void ejemplo()


{
int prueba[]= new int [2000];
int solucion [] = new int [6];
Método int k = 0;
for (int i = 0; i < prueba.length; i++)
{
k++;
prueba[i]= (i*3) + 25;

241
if (k % 13 == 0)
{
k = 0;
for (int j = 0 ; j < 6; j++)
{
solucion[j]= prueba[j]+5;
}
}
}
}

La aplicación de la técnica de desenvolvimiento, permitiría modificar el código de la siguiente


manera:

public void Desenvolvimiento1()


{
int prueba[]= new int [2000];
int solucion [] = new int [6];
int k = 0;
for (int i = 0; i < prueba.length; i++)
{
k++;
prueba[i]= (i*3) + 25;
if (k % 13 == 0)
Método {
k=0;
solucion[0] = prueba[0] + 5;
solucion[1] = prueba[1] + 5;
solucion[2] = prueba[2] + 5;
solucion[3] = prueba[3] + 5;
solucion[4] = prueba[4] + 5;
solucion[5] = prueba[5] + 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.

A continuación se muestra otro ejemplo para mostrar la aplicación de la técnica de


desenvolvimiento. Por ejemplo si se tiene el siguiente fragmento de código:

public void ejemplo1()


{
arreglo= new int [5000000];
temp = new int[5000000];
tam= arreglo.length;
tam1 = temp.length;
Método
for(int j=0; j<=tam1; j++)
{
temp [j]= j+=5;
}
int j =0;

242
for (int i=0;i<tam;i++)
{
arreglo[i]=temp[j]*temp[j++]+2000;
}
}

La optimización del código mediante la técnica de desenvolvimiento se muestra a continuación.

public void desenvolvimiento1()


{
arreglo= new int [5000000];
temp = new int[5000000];
tam= arreglo.length;
tam1 = temp.length;

for(int j=0; j<=tam1; j++)


{
temp [j]= j+=5;
Método }
int j=0;
for (int i=0;i<tam;i+=5)
{
arreglo[i] = temp[j]*temp[j++]+2000;
arreglo[i+1]= temp[j]*temp[j++]+2000;
arreglo[i+2]= temp[j]*temp[j++]+2000;
arreglo[i+3]= temp[j]*temp[j++]+2000;
arreglo[i+4]= temp[j]*temp[j++]+2000;
}
}

5.2.2 Reducción de Esfuerzo

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.

public void ejemplo2 ()


{
int prueba [] = new int [5000];
int solucion [] = new int [5000];
for (int i = 0; i < prueba.length; i++)
{
Método prueba[i] = (Math.random () * 101);
}
for (int j = 0; j < solucion.length; j++)
{
solucion [j] = Math.pow(prueba[j],3);
}
}

La técnica de optimización de reducción de esfuerzo, sugiere entonces, eliminar los llamados a


métodos predefinidos de clase, y escribir directamente el código.

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

La instrucción solucion[j] = Math:pow(prueba[j]; 3) del método ejemplo2, se reemplaza por el


código equivalente, pero sin llamados a métodos predefinidos, y quedando así: solucion[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.

5.2.3 Tipos de Variables

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.

public void ejemplo3()


{
double prueba[]= new double [2500000];
double solucion [] = new double [2500000];
for (int i=0; i < prueba.length; i++)
{
Método prueba[i] = (i * 5) + 345;
}
for (int j = 0; j < solucion.length; j++)
{
solucion[j] = prueba[j] * j;
}
}

La siguiente implementación, corresponde a la optimización utilizando tipo de variable int.

public void tiposVariables()


{
int prueba1[]= new int [2500000];
int solucion1 [] = new int [2500000];
Método
for (int i=0; i < prueba1.length; i++)
{
prueba1[i] = (i * 5) + 345;
}

244
for (int j = 0; j < solucion1.length; j++)
{
solucion1[j] = prueba1[j] * j;
}
}

Si se ejecutan ambos algoritmos en un computador que posee un procesador de 32 bits,


necesariamente debe existir mayor carga computacional para el procesamiento de los tipos de
datos double. Lo anterior nos muestra que también la velocidad de procesamiento, incide en el
tiempo de ejecución de una aplicación.

ACTIVIDAD

Aplique la técnica de fusión de ciclos al siguiente método.

public void tiposVariables1()


{
double arreglo1[]= new double [5000000];
double operacion1[]= {1,2,3,4,5,6};
int longi=arreglo1.length , i, j=0;
for (i=0;i<longi;i++)
Método {
arreglo1[i]= operacion[j++] +i*i+ 150;
if (j==6){
j=0;
}
}
}

5.2.4 Fusión de ciclos

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.

public void ejemplo4()


{
int prueba [] = new int [2500000];
int prueba1 [] = new int [2500000];
int solucion [] = new int [2500000];
for (int i = 0; i < prueba.length; i++)
Método {
prueba[i] = (i * 2) * (35 - i);
}
for (int j = 0; j < solucion.length; j++)
{
solucion[j] = prueba[j] * j;
}

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.

public void fusionCiclos()


{
int prueba [] = new int [2500000];
int prueba1 [] = new int [2500000];
int solucion [] = new int [2500000];
for (int i = 0; i < prueba.length; i++)
Método
{
prueba[i] = (i * 2) * (35 - i);
solucion[i] = prueba[i] * i;
prueba1[i] = prueba[i] - i;
}
}

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.

for(int i=0; i<tam;i++)


{
for(int j=0;j<tam;j++)
if(i==j)
Código
{
resul=arreglo[i][j];
}
}

El código optimizado aplicando la técnica de fusión de ciclos se muestra a continuación.

int s;
for(int i=0; i<tam;i++)
{
Código
s=i;
resul=arreglo[i][s];
}

5.2.5 Expresiones redundantes

Otras de las técnicas más comunes de optimización en compiladores es la eliminación de


operaciones que son redundantes en sub-expresiones, lográndose una reducción de código.
Así, muchas de las veces a una variable se le asigna un valor y se reutiliza varias veces, ya
que dicha variable es común en otras operaciones (García, Delgado, & Castañeda, 2000).

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

El método optimizado aplicando la técnica de expresiones redundantes se muestra a


continuación.

public void redundantes()


{
int lon,l,m;
double j;
lon = arreglo1.length;
Método
for (int i=0;i<lon;i++)
{
j=Math.pow(i,3)*(arreglo1[i] + 1 );
}
}

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.

Para aplicar esta técnica es necesario que se apliquen conceptos fundamentales de


factorización y leyes aplicables a expresiones tales como la ley conmutativa y la ley asociativa.
Estas técnicas se recomienda que se apliquen a los métodos que contienen valores locales.

A continuación se muestra un ejemplo al cual se le aplicará la técnica de Folding.

public void ejemplo6()


{
int arreglo1[]= new int [3000000];
int i,k,lon,l,m,j;
lon = arreglo1.length;
Método
for (i=0;i<lon;i++)
{
arreglo1[i]=(int)(Math.random()* 101);
}

247
k=8500;
l=15250;
m=450;
for (i=0;i<lon;i++)
{
j=arreglo1[i]*((k+l)*m)/150;
}
}

Del método anterior se reemplazaron las expresiones k=8500; l=15250 y m=450, de


forma tal que se sustituyó directamente por el valor resultante de aplicarlos en la formula. De
esa manera la técnica muestra como queda el método.

public void folding()


{
int arreglo1[]= new int [3000000];
int i,k,lon,l,m,j;
lon = arreglo1.length;

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.

int arreglo[]= new int [5000000];


int arreglo1[]= new int [5000000];
int a[]= {10,20,30,40,50,60};

int longi=arreglo.length, i, m=0,w, k=50,l=25,m=50;

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.

public void ejercicio()


{
int prueba[]= new int [900];
int solucion [] = new int [8];
int k = 0;
for (int i = 2; i < prueba.length; i++)
{
k++;
prueba[i]= (i*3) + 25;
if (k % 2 == 0)
{
k = 13;
for (int j = 4 ; j < 11; j++)
{
solucion[j]= prueba[j]+Math.pow(prueba[j],2);
}
}
}
}

5.3 El diseño por contrato

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

Considere el siguiente método:

public void agregarEstudiante(String código, String nombre, String


direccion)

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:

 El array de estudiantes ya se declaró y se le reservó memoria


 El código no es null, ni vacio
 El nombre no es null, ni vacio
 La dirección no es null, ni vacio
 No se sabe si hay un estudiante con este código

La precondición expresa las restricciones necesarias para que la operación funcione de forma
adecuada.

El resultado de la ejecución del método (denominado postcondición) debe ser:

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

Cuando se usan comentarios javadoc, se puede especificar la versión, fecha de creación,


autor.

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

Ya enfocándonos en el contrato, la documentación debe tener la siguiente estructura:

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

Cuando ya se han documentado todos los métodos, se puede generar la documentación


Javadoc. Para ello es necesario dar clic en la barra de menú principal Generar Javadoc.

El diseño por contratos requiere también la incorporación del concepto de invariante. La


Invariante indica las condiciones que deben cumplirse en todo momento para las instancias de
una clase. Por lo tanto se considera como una aserción que expresa restricciones generales
que se cumplen para toda la clase. Algunas reglas referentes al uso de excepciones son las
siguientes:

No se deben usar a la entrada de métodos públicos (solo a la entrada de métodos privados) y


pueden usarse a la salida de métodos públicos o privados que modifican el estado del objeto.

La clase Fibonacci permite resolver el problema de la reproducción de conejos.

public class Fibonacci


{
int n;
/**
* Este método permite calcular la serie Fibonacci
* @param n Es la cantidad de términos de la serie. n>=0
* @return el valor en la serie fibonacci
*/
public int calcularFibonacci (int n)
{
this.n=n;
if ((n == 0) || (n == 1))
{
return 1;
}
else
{
llamarVerificarInvariante();
return calcularFibonacci(n-1) + calcularFibonacci(n-2);
}
}

/**
* 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";
}

/**Este método permite imprimir el error que ha ocurrido al


* momento de verificar la invariante
* @param ae assetionError es el error ocurrido */
public static void imprimirErrorInvariante( AssertionError ae )
{
StackTraceElement[] stack = ae.getStackTrace();
StackTraceElement stackTraceElement = stack[ 0 ];
System.err.println( "Error en el assert" );
System.err.println( " Clase= " +
stackTraceElement.getClassName() );
System.err.println( " Metodo= " +
stackTraceElement.getMethodName() );
System.err.println( " Mensaje= " + ae.getMessage() );
}
}

5.4 Pruebas con JUnit

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.

La realización de pruebas es fundamental a la hora de garantizar la calidad del software


construido. Para crear una clase de prueba es necesario ubicarse en el paquete test, el cual
debió crear previamente, luego dar clic sobre él y seleccionar JUnit Test Case.

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.

Se desea crear una aplicación para manejar la información de un estudiante. Un estudiante


tiene un código, un nombre y una nota definitiva. Se debe permitir crear un nuevo estudiante,
dar una bonificación de una décima en la nota definitiva y devolver el código del estudiante.

El diagrama de clases correspondiente es:

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.

Luego, en el campo Name, es necesario escribir el nombre que se le va a poner a la prueba,


por ejemplo EstudianteTest. El nombre del test debe iniciar con el nombre de la clase a la que
se le va a realizar la prueba seguida de la palabra Test.

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.

Continuando con la creación de la clase de prueba para Estudiante de clic en Next.


Posteriormente hay que seleccionar los métodos donde se cambia el estado del objeto: set,
constructores o métodos que realicen cálculos.

 Para este caso se seleccionarán los siguientes métodos:

o setCodigo(String)
o setNombre(String)
o setNota(double)
o calcularBonificacion()

A continuación es necesario dar clic en Finish. La siguiente ventana aparecerá:

Posteriormente aparece el siguiente código de implementación.

import static org.junit.Assert.*;


import org.junit.Test;

public class EstudianteTest


{
@Test
Método
public void testSetCodigo()
{
fail("Not yet implemented");
}
@Test
public void testSetNombre()

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:

import static org.junit.Assert.*;


import org.junit.Before;
import org.junit.Test;

public class EstudianteTest


{
private Estudiante miEstudiante;

/** Construye un nuevo estudiante*/


@Before
public void setupEscenario1( )
{
miEstudiante=new Estudiante();
miEstudiante.setCodigo("123");
miEstudiante.setNombre("Juan");
miEstudiante.setNota(3);
}

/** Prueba de los métodos setCodigo*/


Método
@Test
public void testSetCodigo(){
assertEquals( "El codigo es
inválido.", "123",
miEstudiante.getCodigo() );
}

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

5.5 Límites de la Lógica

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.

Algunos ejemplos de problemas y soluciones que se encuentran en la clase P, son los


siguientes:

 Multiplicación de número grandes


 Multiplicación de Matrices
 Resolución de Sistemas de Ecuaciones Lineales
 Procedimientos de ordenamiento y búsqueda.
 Aplicaciones típicas empresariales
 Sistemas de información para organizaciones.

5.5.2 Clase NP

Los algoritmos de la Clase NP están asociados a problemas complejos y de índoles


enumerativo, en donde su espectro de solución esta asociada a una búsqueda sistemática
dentro de un árbol; la característica fundamental del árbol es que es de profundidad limitada y
su anchura es a lo sumo exponencial.

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

Generalmente estos problemas que se consideran como intratables, desembocan en métodos


no determinísticos en los cuales se planean métodos que no necesariamente llevan a una
solución esperada y por el contrario llegan a soluciones hipotéticas. Los problemas de esta
clase se denominan NP (la N de no-deterministas y la P de polinómicos).

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.

Bedoya, O. (2008). Estructura de Datos y Algoritmia. Cali, Valle, Colombia.

Besembel, I. (2006). Diseño y Análisis de Algoritmos. Recuperado el 10 de 10 de 2011

Bohóquez, J. (2006). Diseño Efectivo de Programas Correctos. Bogotá: Lemoine Editores.

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.

Colmenares, J. (2006). Grafos. Recuperado el 10 de 11 de 2011, de


http://www.ica.luz.ve/juancol/eda/grafos/grafos.html

Cormen, T., Leiserson, C., Rivest, R., & Stein, C. (2001). Introduction to Algorithms.
Dunfermline, FIF, United Kingdom: The MIT Press.

Di Mare, A. (1998). Convenciones de Programación para Pascal. Recuperado el 1 de 10 de


2011, de http://www.di-mare.com/adolfo/p/convpas.htm

García, A., Delgado, J., & Castañeda, S. (2000). Metodologías de Optimización en


CICESE2000. Recuperado el 1 de 9 de 2011, de
http://telematica.cicese.mx/computo/super/cicese2000/optimiza/
Goodrich, M. T., & Tamassia, R. (2002). Estructuras de datos y algoritmos. Cecsa.

Grimaldi, R. (1998). Matemática discreta y combinatoria. Addison Wesley Longman.

Guerequeta, R., & Vallecillo, A. (2000). Técnicas de Diseño de Algoritmos . Málaga: Servicio de
Publicaciones de la Universidad de Málaga.

Hernández, G. (2004). Grafos. Madrid, España.

Hernández, L. (2004). Análisis de Algoritmos y Complejidad Computacional. Recuperado el 10


de 11 de 2011, de http://www.geocities.ws/leoher314/algorit.htm

Iparraguirre, J. (2009). Algoritmos para caminos mínimos. Lima, Perú.

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

Mañas, j. (1997). Análisis de Algoritmos: Complejidad. Recuperado el 15 de 1 de 2010, de


http://www.lab.dit.upm.es/~lprg/material/apuntes/o/index.html#s5

McConnell, J. J. (2007). Analysis of Algorithms: An Active Learning Approach. Sudbury: Jones


and Bartlett Publishers.

Molpeceres, A. (2001). Convenciones de Código para el lenguaje de programación.


Recuperado el 1 de 5 de 2011, de is-g7b-
2011.googlecode.com/files/EstándarCódigoJava.pdf

Preiss, B. (1998). Data Structures and Algorithms with Object-Oriented. Addison Wesley
Publishing.

Ramirez, M. (2003). Análisis de Algoritmos. Cali, Colombia.

Universidad Autónoma Metropolitana . (2010). Análsis de Algoritmo. Recuperado el 1 de 5 de


2011, de http://aniei.org.mx/paginas/uam/CursoAA/index.html

Valenzuela, V. (2003). Manual de Análisis y Diseño de Algoritmos. Santiago, Chile.

Villalobos, J., & Casallas, R. (2006). Fundamentos de Programación aprendizaje activo basado
en casos. Bogotá: Pearson Prentice Hall.

Weiis, M. (2002). Estructuras de datos. Florida: Addison Wesley.

258

También podría gustarte