Anda di halaman 1dari 262

Análisis de Algoritmos

en Java

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

Paulo César Carmona Tabares


Adscrito al
Programa de Fı́sica
Facultad de Ciencias Básicas y Tecnologı́as
Universidad del Quindı́o
ii

Análisis de
Algoritmos en Java

No está permitida la reproducción total o parcial


de esta obra, ni su tratamiento o transmisión por
cualquier método sin autorización escrita del
editor.

Derechos reservados
c Noviembre de 2007
°

ISBN: 978-958-44-2384-9

Este libro fue editado usando LATEX 2ε

Conceptos Gráficos Ltda


Calle 19 13-30
Teléfono: (6) 741 07 91 - 741 14 02
Armenia, Quindı́o

Armenia, Quindı́o - Colombia


Presentación

Este libro es el resultado de varios años de trabajo docente impartiendo


el curso de análisis de algoritmos I en el programa de ingenierı́a de sistemas
de la Universidad del Quindı́o. El propósito de esta nueva edición es seguir
ofreciendo a los lectores los elementos fundamentales sobre algoritmia. Esta
edición tiene una gran cantidad de ejemplos que resuelven problemas con-
cretos y los cuales cuentan con su correspondiente explicación.

Objetivos

El libro tiene los siguientes objetivos:

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 matematicos con su aplicación en el contexto de
las ciencias de la computación.

Mostrar al lector las técnicas básicas utilizadas para establecer la com-


plejidad computacional y la eficiencia en términos de tiempo de ejecu-
ció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 una gran cantidad de ejemplos y aplicaciones de algoritmos.


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. Se utilizarán
convenciones que son recomendadas para la escritura de programas.
iv PRESENTACIÓN

Desarrollar en el estudiante habilidades para la construcción de algo-


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

Este libro pretende ser flexible en la forma como puede impartirse a las
personas interesadas. La comprensión de los temas, depende fundamental-
mente de la preparación de los estudiantes. Se presentan conceptos basicos
fundamentales e intermedios, los cuales se pueden aplicar en la práctica,
ası́ como también realizar un análisis riguroso de los conceptos teoricos que
se imparten.

El texto esta orientado al estudiante, y hemos puesto el mayor empeño


para explicar cada tema tan claramente como sea posible.

Es de suponer que los lectores de este libro tienen los conocimientos


básicos de algoritmia y esta familizarizado con algún lenguaje de progra-
mació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 es-
cenciales, sin sobrecargar al lector en temas que pueden ser objeto de estudio
en otros libros relacionados con la programación.

Metodologı́a de trabajo

El material de este libro, puede ser cubierto en un semestre académico.


La forma en la cual se puede asignar el tiempo para impartir cada uno de
los temas es la siguiente:

Capı́tulo Semanas Dependencia


Capı́tulo 1 1 Sin dependencia
Capı́tulo 2 2 Sin dependencia
Capı́tulo 3 2 Capı́tulo 2
Capı́tulo 4 3 Capı́tulo 2 y 3
Capı́tulo 5 2 Capı́tulo 3 y 4
Capı́tulo 6 2 Capı́tulo 3 y 4
Capı́tulo 7 2 4

Es necesario programar actividades prácticas, en las cuales los estudi-


antes puedan implementar los diferentes algoritmos y verifique su tiempo de
ejecución.
v

Organización del libro

El libro está estructurado en ocho capı́tulos los cuales pretenden de


forma progresiva, mostrar diferentes temas, que son abordados de forma
simple y coherente, a continuaciòn, se muestra un breve resumen de los
temas que se analizan:

En el capı́tulo 1, presentamos aspectos fundamentales de los algoritmos,


algunas de sus propiedades y de los buenos hábitos a la hora de diseñarlos
e implementarlos. Para los lectores que consideren tienen los conocimientos
necesarios tanto en el lenguaje de programación java, como en conceptos de
algoritmia básica, se recomienda a estas personas, iniciar en el capı́tulo 2
del libro.

El capı́tulo 2, corresponde al análisis de los algoritmos por medio de la


técnica de programación, en los cuales para un problema dado, se muestra
su solución por medio de la comparación en la cantidad de instrucciones
que se ejecuta. Se presenta tambı́én, diferentes técnicas de optimización de
código tales como el desenvolvimiento de ciclos y la fusión de ciclos entre
otras.

En el capı́tulo 3, analizamos el tiempo de ejecución de los algoritmos


y presentamos su forma de estimarlo. Se muestra una amplia variedad de
ejemplos los cuales permitirán al lector conocer y entender los diferentes
tiempos de ejecución que se pueden obtener de los algoritmos.

En el capı́tulo 4, se compara los tiempos de ejecución de los algorit-


mos basándonos en su orden de crecimiento. También se muestra y anali-
za la complejidad computacional, ası́ como las propiedades de la notación
asintótica. Se presenta variedad de ejercicios los cuales permitirán al lector
conocer y entender los diferentes ordenes de complejidad de los algoritmos.

En el capı́tulo 5, discutimos la recursividad, iniciando con los conceptos


básicos de la recursión y terminando con ejemplos clásicos que muestran
esta forma de programación. También se realiza el análisis matemático de
los algoritmos recursivos, el cual nos permite realizar estimaciones del tiem-
po de ejecución y del orden de complejidad. En este capı́ tulo, se procura
que el lector conozca varias implementaciones de algoritmos recursivos de
diferentes caracterı́sticas.

En el capı́tulo 6 damos a conocer los principales algoritmos de orde-


namiento. Hemos decidido dedicarle un capı́tulo debido a su extensión y a
vi PRESENTACIÓN

la importancia que en otros ámbitos adquiere el problema del ordenamien-


to. Se muestran diferentes algoritmos de ordenamiento tanto iterativos como
recursivos.

En el capı́tulo 7 presentamos los principales algoritmos de búsqueda,


haciendo énfasis en la búsqueda lineal y la búsqueda binaria, puesto son
algoritmos tı́picos que son analizados frecuentemente dentro de la algoritmia.

En el capı́tulo 8 se muestra la implementación de las estructuras de datos


lineales con su respectivo análisis de orden de complejidad. Se analizan los
ordenes de complejidad de los métodos mas importantes para las listas, pilas
y colas.

Nuevas caracterı́sticas

El libro análisis de algoritmos en java es una edición actualizada y mejo-


rada del libro fundamentos de análisis de algoritmos. 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 cuen-
ta las observaciones y crı́ticas tanto de los estudiantes como de profesores
que usaron el libro, se realizaron cambios para este libro, entre los cuales
tenemos:

Se propone una amplia variedad de ejercicios, con los cuales se pretende


el refuerzo de cada uno de los temas.

Se ha agregado un capı́tulo para tratar la recursividad. Se tiene una


amplia gama de ejericios que permitirán al estudiante conocer y enten-
der la forma de resolución de problemas en los cuales se puede aplicar
la recursión

Se tiene un capı́tulo destinado al analisis de las estructuras de datos


lineales. Se muestra la implementación de los métodos mas importantes
para las listas, pilas y colas.

Al finalizar cada capı́tulo, se proponen lecturas complemetarias para


la investigación y refuerzo de cada uno de los temas. Estas lecturas se
seleccionaron de referentes bibliograficos de relevancia en las ciencias
de la computación (ACM).
Índice general

Presentación III

1. Conceptos básicos 1

1.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1

1.2. Definición de algoritmo . . . . . . . . . . . . . . . . . . . . . 2

1.3. Caracterı́sticas de los algoritmos . . . . . . . . . . . . . . . . 6

1.4. Recomendación de diseño e implementación . . . . . . . . . . 6

1.5. Fases de Desarrollo de un Programa . . . . . . . . . . . . . . 7

1.6. Elementos de la calidad del software . . . . . . . . . . . . . . 8

1.7. Conclusiones . . . . . . . . . . . . . . . . . . . . . . . . . . . 9

1.8. Autoevaluación . . . . . . . . . . . . . . . . . . . . . . . . . . 10

1.8.1. Conceptos Básicos . . . . . . . . . . . . . . . . . . . . 10

1.8.2. Correctitud de los algoritmos . . . . . . . . . . . . . . 10

1.8.3. Calidad del Software . . . . . . . . . . . . . . . . . . . 18

1.8.4. Lecturas complementarias . . . . . . . . . . . . . . . . 18

vii
viii ÍNDICE GENERAL

2. Análisis de Algoritmos 21

2.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21

2.2. Análisis de algoritmos . . . . . . . . . . . . . . . . . . . . . . 22

2.2.1. Caso 0: Máximo común divisor . . . . . . . . . . . . . 22

2.2.2. Caso 1: Potencia de un número entero . . . . . . . . . 25

2.2.3. Caso 3: Números primos . . . . . . . . . . . . . . . . . 26

2.2.4. Caso 3: Serie de Fibonacci . . . . . . . . . . . . . . . . 28

2.3. Técnicas de optimización . . . . . . . . . . . . . . . . . . . . 30

2.3.1. Desenvolvimiento de ciclos . . . . . . . . . . . . . . . . 30

2.3.2. Reducción de esfuerzo . . . . . . . . . . . . . . . . . . 31

2.3.3. Tipos de Variables . . . . . . . . . . . . . . . . . . . . 33

2.3.4. Fusión de ciclos . . . . . . . . . . . . . . . . . . . . . . 34

2.4. Conclusiones . . . . . . . . . . . . . . . . . . . . . . . . . . . 36

2.5. Autoevaluación . . . . . . . . . . . . . . . . . . . . . . . . . . 36

2.5.1. Análisis de algoritmos . . . . . . . . . . . . . . . . . . 36

2.5.2. Lecturas Complementarias . . . . . . . . . . . . . . . 40

2.5.3. Optimización de Código . . . . . . . . . . . . . . . . . 41

3. Tiempo de ejecución 43

3.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43

3.2. Tiempo de ejecución . . . . . . . . . . . . . . . . . . . . . . . 44

3.3. Técnicas para estimar el tiempo de ejecución . . . . . . . . . 45

3.4. Tiempo de ejecución con llamada a métodos . . . . . . . . . . 57


ÍNDICE GENERAL ix

4. Complejidad Computacional 65

4.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65

4.2. Comparación del tiempo de ejecución . . . . . . . . . . . . . . 66

4.2.1. Talla del problema . . . . . . . . . . . . . . . . . . . . 69

4.3. Complejidad Computacional . . . . . . . . . . . . . . . . . . . 70

4.3.1. Notación asintótica . . . . . . . . . . . . . . . . . . . . 71

4.3.2. Regla del Lı́mite . . . . . . . . . . . . . . . . . . . . . 73

4.4. Ordenes de Complejidad . . . . . . . . . . . . . . . . . . . . . 78

4.4.1. Complejidad Constante . . . . . . . . . . . . . . . . . 78

4.4.2. Complejidad logarı́tmica . . . . . . . . . . . . . . . . . 80

4.4.3. Complejidad lineal . . . . . . . . . . . . . . . . . . . . 82

4.4.4. Complejidad nlog(n) . . . . . . . . . . . . . . . . . . . 82

4.4.5. Complejidad cuadrática . . . . . . . . . . . . . . . . . 83

4.4.6. Complejidad cúbica . . . . . . . . . . . . . . . . . . . 84

4.4.7. Complejidad exponencial . . . . . . . . . . . . . . . . 85

4.4.8. Propiedades de la Función O . . . . . . . . . . . . . . 86

4.4.9. Complejidad para los condicionales . . . . . . . . . . . 87

4.4.10. Complejidad de algoritmos con llamados a métodos . 88

4.5. Conclusiones . . . . . . . . . . . . . . . . . . . . . . . . . . . 92

4.6. Autoevaluación . . . . . . . . . . . . . . . . . . . . . . . . . . 93

4.6.1. Comparación de tiempos de ejecución . . . . . . . . . 93

4.6.2. Complejidad . . . . . . . . . . . . . . . . . . . . . . . 93

4.6.3. Complejidad con llamados a métodos . . . . . . . . . 99

4.6.4. Lecturas Complementarias . . . . . . . . . . . . . . . 101


x ÍNDICE GENERAL

5. Recursividad 103
5.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
5.2. Concepto de recursividad . . . . . . . . . . . . . . . . . . . . 104
5.3. Algoritmos recursivos . . . . . . . . . . . . . . . . . . . . . . 105
5.3.1. Algoritmo recursivo: sumatoria . . . . . . . . . . . . . 106
5.3.2. Algoritmo recursivo: multiplicación . . . . . . . . . . . 107
5.3.3. Algoritmo recursivo: suma de cifras de un número . . 109
5.3.4. Algoritmo recursivo: potencia . . . . . . . . . . . . . . 111
5.3.5. Algoritmo recursivo: cantidad de cifras de un número 112
5.3.6. Algoritmo recursivo: máximo común divisor . . . . . . 114
5.3.7. Algoritmo recursivo: números armónicos . . . . . . . . 115
5.3.8. Algoritmo recursivo: módulo . . . . . . . . . . . . . . 117
5.3.9. Algoritmo recursivo: Contar ceros arreglo . . . . . . . 118
5.3.10. Algoritmo recursivo: Número menor arreglo . . . . . . 119
5.3.11. Algoritmo recursivo: Número de apariciones . . . . . . 120
5.3.12. Algoritmo recursivo número mayor arreglo . . . . . . . 121
5.3.13. Algoritmo recursivo suma elementos de un arreglo . . 122
5.4. Análisis de algoritmos recursivos . . . . . . . . . . . . . . . . 123
5.4.1. Análisis del algoritmo recursivo: factorial . . . . . . . 124
5.4.2. Análisis del algoritmo recursivo: multiplicación . . . . 127
5.4.3. Análisis del algoritmo recursivo: Fibonacci . . . . . . . 128
5.5. Resolver recurrencias por inducción . . . . . . . . . . . . . . . 130
5.5.1. Ejemplo 1 . . . . . . . . . . . . . . . . . . . . . . . . . 131
5.5.2. Ejemplo 2 . . . . . . . . . . . . . . . . . . . . . . . . . 132
5.5.3. Ejemplo 3 . . . . . . . . . . . . . . . . . . . . . . . . . 133
5.5.4. Ejemplo 4 . . . . . . . . . . . . . . . . . . . . . . . . . 134
ÍNDICE GENERAL xi

5.5.5. Ejemplo 5 . . . . . . . . . . . . . . . . . . . . . . . . . 135


5.5.6. Ejemplo 6 . . . . . . . . . . . . . . . . . . . . . . . . . 136
5.5.7. Ejemplo 7 . . . . . . . . . . . . . . . . . . . . . . . . . 138
5.5.8. Ejemplo 8 . . . . . . . . . . . . . . . . . . . . . . . . . 140
5.6. Resolver recurrencias por sustitución . . . . . . . . . . . . . . 140
5.6.1. Algoritmo Divisibles . . . . . . . . . . . . . . . . . . . 141
5.6.2. Recursividad exponencial . . . . . . . . . . . . . . . . 142
5.6.3. Ejemplo recursivo logarı́tmico . . . . . . . . . . . . . . 143
5.6.4. Recursividad cuadrática . . . . . . . . . . . . . . . . . 143
5.7. Recurrencias suponiendo una solución . . . . . . . . . . . . . 144
5.8. Conclusiones . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
5.9. Autoevaluación . . . . . . . . . . . . . . . . . . . . . . . . . . 146
5.9.1. Algoritmos recursivos . . . . . . . . . . . . . . . . . . 146
5.9.2. Implementaciones de recursión . . . . . . . . . . . . . 149
5.9.3. Tiempo de ejecución . . . . . . . . . . . . . . . . . . . 150
5.9.4. Orden de complejidad . . . . . . . . . . . . . . . . . . 153
5.9.5. Lecturas Complementarias . . . . . . . . . . . . . . . 158

6. Ordenamiento 159
6.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
6.2. Método de la Burbuja . . . . . . . . . . . . . . . . . . . . . . 160
6.3. Método de la burbuja bidireccional . . . . . . . . . . . . . . . 162
6.4. Ordenamiento por selección . . . . . . . . . . . . . . . . . . . 167
6.5. Método de Inserción . . . . . . . . . . . . . . . . . . . . . . . 171
6.6. ShellSort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
6.7. MergeSort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
6.8. QuickSort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
6.9. StoogeSort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
6.10. RadixSort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187
6.11. Conclusiones . . . . . . . . . . . . . . . . . . . . . . . . . . . 194
6.12. Autoevaluación . . . . . . . . . . . . . . . . . . . . . . . . . . 194
6.12.1. Algoritmos de ordenamiento . . . . . . . . . . . . . . . 194
6.12.2. Lecturas Complementarias . . . . . . . . . . . . . . . 197
xii ÍNDICE GENERAL

7. Algoritmos de búsqueda 199


7.1. Búsqueda lineal iterativa . . . . . . . . . . . . . . . . . . . . . 200
7.2. Búsqueda lineal limitada . . . . . . . . . . . . . . . . . . . . . 202
7.3. Búsqueda lineal iterativa con extremos . . . . . . . . . . . . . 205
7.4. Búsqueda lineal recursiva . . . . . . . . . . . . . . . . . . . . 207
7.5. Búsqueda binaria . . . . . . . . . . . . . . . . . . . . . . . . . 208
7.6. Búsqueda Binaria iterativa . . . . . . . . . . . . . . . . . . . 210
7.7. Búsqueda Binaria Recursiva . . . . . . . . . . . . . . . . . . . 210
7.8. Conclusiones . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
7.9. Autoevaluación . . . . . . . . . . . . . . . . . . . . . . . . . . 213
7.9.1. Algoritmos de búsqueda . . . . . . . . . . . . . . . . . 213
7.9.2. Lecturas complemetarias . . . . . . . . . . . . . . . . . 214

8. Análisis de Estructura de datos 215


8.1. Listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216
8.1.1. Lista sencillamente enlazada . . . . . . . . . . . . . . 216
8.1.2. Lista Sencilla Circular . . . . . . . . . . . . . . . . . . 220
8.1.3. Lista Doblemente Enlazada . . . . . . . . . . . . . . . 223
8.2. Pila . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229
8.2.1. Pilas mediante arreglos . . . . . . . . . . . . . . . . . 230
8.2.2. Representación de pilas mediante listas . . . . . . . . 234
8.3. Cola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 237
8.3.1. Representación de colas mediante arreglos . . . . . . 237
8.4. Conclusiones . . . . . . . . . . . . . . . . . . . . . . . . . . . 241
8.5. Autoevaluación . . . . . . . . . . . . . . . . . . . . . . . . . . 243
8.5.1. Implementación de cola . . . . . . . . . . . . . . . . . 243
8.5.2. Lecturas Complementarias . . . . . . . . . . . . . . . 245

Bibliografı́a 247
1
Conceptos básicos

1.1. Introducción

Cuando nos referimos al concepto de algoritmo, hacemos referencia a


los pasos encaminados a la consecución de un objetivo.

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

Un problema es resuelto algorı́tmicamente, si se puede escribir un pro-


grama 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.
2 CAPÍTULO 1. CONCEPTOS BÁSICOS

1.2. Definición de algoritmo

Se conoce como algoritmo a una secuencia de instrucciones, que son eje-


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

En el contexto computacional, estos pasos son instrucciones que reciben


datos de entrada, los procesa y como resultado de ello, se obtiene un conjunto
de datos de salida.

Un algoritmo puede ser caracterizado por una función lo cual asocia una
salida: s = f(E) a cada entrada E 1 .

Figura 1.1: Caracterización de un algoritmo

Se dice que un algoritmo calcula una función f, donde la entrada es una


variable independiente básica en relación a la que se producen las salidas
del algoritmo.

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 en-
trada 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
cual será su resultado.

Algoritmos Adaptativos: Son algoritmos con alguna capacidad de


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

1
MSc Vı́ctor Alfonso Valenzuela Ruz - Universidad de Chile - Facultad de Ingenierı́a
1.2. DEFINICIÓN DE ALGORITMO 3

Un algoritmo debe ser en todos los casos:

Correcto: Un algoritmo es correcto, si para toda instancia del con-


junto de entrada se obtiene la salida correcta.

Eficiente: Debe ser rápido y usar la menor cantidad de recursos. Es


decir, 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 cumple con los siguientes


elementos, que resueltos de forma afirmativa dan noción de lo que es un buen
algoritmo.

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 contienen aquellos números que son divisibles
exactamente por sı́ mismos y por la unidad. Son números primos entre otros:
1, 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61 ,67.

De acuerdo a los algoritmos desarrollados en los Cuadros 1 y 2, respon-


deremos las siguientes preguntas:

1. ¿Cumple el algoritmo el objetivo para el cual está pensado?

2. ¿El algoritmo resuelve el problema en el menor tiempo posible?

3. ¿El algoritmo hace uso adecuado de los recursos?

4. ¿Permite el algoritmo identificar posibles errores?

5. ¿El algoritmo es fácil de modificar para añadir funcionalidad?


4 CAPÍTULO 1. CONCEPTOS BÁSICOS

Cuadro 1: Es primo 1

public void esprimo(int numero)


{
int resultado = 0;
int i = 2;
while( i < numero )
{
if ( numero % i == 0 )
{
resultado = 1;
}
i = i + 1;
}
if ( resultado == 0 )
{
imprimir("El numero " + numero +" " + "es primo");
}
else
{
imprimir("El numero " + numero +" " + "no es primo");
}
}

Considerando los anteriores puntos, podemos responder:

¿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 com-
parar 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.
1.2. DEFINICIÓN DE ALGORITMO 5

Una segunda solución para determinar si el número es primo esta en


Cuadro 2.

Cuadro 2: Es primo 2

public void esprimo(int numero)


{
int i;
for ( i = 2 ; i <= numero / 2 ; i++ )
{
if ( numero % i == 0 )
{
break;
}
}
if ( i > numero / 2 )
{
imprimir("El numero " + numero +" " + "es primo");
}
else
{
imprimir("El numero " + numero +" " + "no es primo");
}
}

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

¿Resuelva 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.
6 CAPÍTULO 1. CONCEPTOS BÁSICOS

1.3. Caracterı́sticas de los algoritmos

Cuando que se desarrolle una solución a un problema desde la perspecti-


va 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 proble-


ma de la mejor manera posible (en términos de tiempo). En el ambiente
computacional, lo son el buen uso de los recursos de hardware.

1.4. Recomendación de diseño e implementación

Al diseñar un algoritmo y posteriormente implementarlo en un lengua-


je de programación, se deben tener en cuenta buenos hábitos y técnicas
de diseño. Por anterior tanto todo desarrollador de software deberı́a a la
solución de problemas.

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

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


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

Evaluar posibles soluciones: Un desarrollador conoce todos los re-


cursos algorı́tmicos de que dispone y las distintas metodologı́as de
diseño para plantear la solución adecuada. Una vez hecho esto, de-
berá optar por la mejor de ellas para su posterior implementación.
1.5. FASES DE DESARROLLO DE UN PROGRAMA 7

Diseñar la solución: Esta actividad se refiere a un modelamiento de


lo que será la solución definitiva. En la actualidad existen lenguajes
de modelamiento que permiten crear los planos de software. Bajo el
paradigma orientado a objetos, lo ideal podrı́a ser especificar la jerar-
quı́a de clases con una descripción completa del diagrama de clases.
También se recomienda al momento de 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: Este es un factor de gran importancia, ya


que con frecuencia los algoritmos adolecen de documentación y en al-
gunos casos esta es inexistente. Este tipo de situaciones perjudican
el mantenimiento y reutilización de los mismos, y dificultan su en-
tendimiento. Los comentarios propiamente dichos son pequeños frag-
mentos de tipo explicativo, aclaratorio o de advertencia que se inter-
calan entre las instrucciones del programa.

Manejo de versiones: Es necesario el uso de herramientas que per-


mitan el control de versiones. Teniendo en cuenta el caracter evolutivo
y progresivo de los proyectos de desarrollo de software.

Estandar de programación: Es recomendable establecer un linea-


miento particular en la forma en la cual se construyen los programas.
Se deben estandarizar por ejemplo los nombres de variables, de las
clases y de los métodos, todos estos deben estar acordes a las tareas
que realizan.

1.5. Fases de Desarrollo de un Programa

Dentro del entorno de la informática no siempre los problemas - necesi-


dades de las personas o las organizaciones, se encuentran claramente definidas,
es necesario en muchas ocasiones realizar una clara formulación del proble-
ma. Solo partiendo de una correcta formulación de este, será posible especi-
ficar una metodologı́a para su solución. En general el análisis de un problema
tiene dos actividades definidas y relacionadas:

Análisis del problema.

Descripción de la solución.
8 CAPÍTULO 1. CONCEPTOS BÁSICOS

Una buena planificación de las tareas a realizar para desarrollar un pro-


grama favorece el éxito en la implementación. La planificación debe estar
basada en el establecimiento de fases. Las fases, tanto para realizar progra-
mas sencillos como para llevar a cabo proyectos de envergadura de construc-
ción de aplicaciones informáticas se pueden ver en la figura 1.2

Figura 1.2: Fases de desarrollo de un programa

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


espacio del problema, se identifican las posibles necesidades y restric-
ciones 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 algorimos 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 construı́-
do, estos se deben somenter a un conjunto de pruebas.

Finalmente se tiene una aplicación de software, que debe dar la solu-


ción esperada de acuerdo a los requerimientos planteados incialmente.

1.6. Elementos de la calidad del software

Dentro de las ciencias de la computación, existe un área fundamental lla-


mada la Ingenierı́a de software, la cual entre otras se ocupa de los elementos
1.7. CONCLUSIONES 9

que contribuyen al éxito en la implementación de programas para resolver


problemas. También contempla los 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 interactue con el
programa.
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
rutinas de prueba.
Exactitud: Es el atributo que determina el nivel de precisión de los
resultados obtenidos por un programa.

1.7. Conclusiones
La construcción de algoritmos es una destreza que se debe adquirir,
y es de un gran significado para los profesionales inmersos en esta área.
Computadoras con mayor capacidad de procesamiento, no necesariamente
significa mayor velocidad de los algoritmos, en muchos proyectos de desarro-
llo de software se puede encontrar que el hardware no es el mayor problema
como si lo puede ser las aplicaciones inefectivas.
El desarrollo de una aplicación de software involucra múltiples fases que
van desde la especificación del problema, hasta la implementación de la solu-
ción, es en este punto en donde merece especial atención analizar el impacto
de la eficiencia de los algoritmos que implementamos, dado que lo que nece-
sitamos son tiempos de respuesta rápidos de acuerdo a los requerimientos
de los usuarios finales.
De acuerdo a lo anterior, gran parte del trabajo de los ingenieros es
saber qué problema se va a resolver, la otra parte corresponde a la forma
como se va solucionar, y es en este punto en donde el análisis de algoritmos
es fundamental para diseñar e implementar algoritmos correctos y eficientes.
La algoritmia es uno de los pilares de la programación y su relevancia
se muestra en el desarrollo de cualquier aplicación, más allá de la mera
construcción de programas.
10 CAPÍTULO 1. CONCEPTOS BÁSICOS

1.8. Autoevaluación

1.8.1. Conceptos Básicos

1. Investigar la definición de:

Algoritmos Voraces.
Algoritmos Paralelos.
Algoritmos Probabilistas.
Algoritmos Cuánticos.

1.8.2. Correctitud de los algoritmos

1. El siguiente algoritmo lee un número entero positivo y retorna la can-


tidad de dı́gitos que este tiene. Ver Cuadro 3. Determine de acuerdo a
los criterios dados, si el algoritmo es bueno.

Cuadro 3: cantidad cifras

public void calcularCifras (int num)


{
int aux = num, con = 0;
if(num==0)
{
JOptionPane.showMessageDialog(null,"Cifras de "+num+" es : "+1);
}
for(int i=10;num!=0;i+=10)
{
con = con + 1;
num = num/i;
i = i-10;
}
if(con!=0)
{
JOptionPane.showMessageDialog(null,"Cifras de "+aux+" es : "+con);
}
}
}

2. El siguiente algoritmo lee un número entero positivo y retorna la can-


tidad de dı́gitos que este tiene. Ver Cuadro 4. Determine de acuerdo a
los criterios dados, si el algoritmo es bueno.
1.8. AUTOEVALUACIÓN 11

Cuadro 4: cantidad cifras

public void calcularCifras(int numero)


{
int acum = 0;
if(numero<10)
{
JOptionPane.showMessageDialog(null,"Cantidad de cifras: " + 1);
}
else if(numero>=10 && numero<=99)
{
JOptionPane.showMessageDialog(null,"Cantidad de cifras: " + 2);
}
else
{
for(int i=numero; i>10; i-=10)
{
numero= numero/10;
acum++;
if(numero == 0)
{
break;
}
}
JOptionPane.showMessageDialog(null,"Cantidad de cifras: " + acum);
}
}
}

3. Entre las varias sucesiones interesantes de números que existen en las


matemáticas discretas y combinatoria, están los números armónicos,
los cuales tienen la forma: H1 + H2 + H3 + . . . donde:
H1 = 1
1
H2 = 1 +
2
1 1
H3 = 1+ +
2 3
1 1 1
Y, en general, Hn = 1 + 2 + 3 + ... + n para cualquier n → Z+ [2] .
La sucesión de los números armónicos se puede definir como:
H1 = 1, n=1
µ ¶
1
Hn+1 = Hn + , n≥1
n+1
12 CAPÍTULO 1. CONCEPTOS BÁSICOS

En el Cuadro 5 mostramos el algoritmo que permite encontrar un


número de la sucesión de los números armónicos. Determine de acuerdo
a los criterios dados, si el algoritmo es bueno.
Cuadro 5: Armonico 1

public void armonico(int numero)


{
double aux, result = 1;

for(int i=2; i<=numero; i++)


{
aux = i;
result = result + (1/aux);
}

imprimir("El numero armonico de " + numero +" " + "es: " + result);
}

En el Cuadro 6 mostramos el segundo algoritmo que permite encontrar


un número de la sucesión de los números armónicos. Determine de
acuerdo a los criterios dados, si el algoritmo es bueno.
Cuadro 6: Armonico 2

public void armonico(int numero)


{
int arreglo[]= new int[numero];
int aux, result = 0;

for (int i = 1; i <= arreglo.length; i++)


{
aux = i;
arreglo[i-1] = 1/aux;
result = result + arreglo[i-1];
}

imprimir("El numero armonico de " + numero +" " + "es: " + result);
}
1.8. AUTOEVALUACIÓN 13

4. En el Cuadro 7 mostramos un algoritmo que determina si una palabra


es o no es palindroma .
Cuadro 7: Palindroma

public void palindroma(String cad, int n)


{
boolean estado = false;

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


{
for(int j=(n-1); j>=(n/2)+1 ;j--)
{
if(cad.charAt(i)==cad.charAt(j))
{
estado = true;
i=i+1;
}
else
{
estado = false;
j=0;
}
}
if(estado == false)
{
i=i+1;
}
}

if(estado == false)
{
JOptionPane.showMessageDialog(null," "+cad+" NO es palindroma");
}
else
{
JOptionPane.showMessageDialog(null," "+cad+" SI es Palindroma");
}
}

5. Escriba un algoritmo diferente al anterior, que determine si una pala-


bra determinada es palindroma o no lo es.
14 CAPÍTULO 1. CONCEPTOS BÁSICOS

6. El siguiente algoritmo determina si una matriz es simetrica o no lo


es. Ver Cuadro 8. Determine de acuerdo a los criterios dados, si el
algoritmo es bueno.
Cuadro 8: simetrica 1

public void simetrica (int matriz [][])


{
int i, j, contador = 0;
int tamano = matriz.length * matriz.length;

for (i = 0; i < matriz.length; i++)


{
for (j = 0; j < matriz.length; j++)
{
if(matriz[i][j] == matriz[j][i])
{
contador++;
}
}
}
if(contador != tamano)
{
JOptionPane.showMessageDialog (null,"La matriz es no Simetrica");
}
else
{
JOptionPane.showMessageDialog (null,"La matriz es Simetrica");
}
}

7. La sucesión de los números de Lucas está muy relacionada con los


números de Fibonacci, en el Cuadro 1.1 se muestra los primeros 10
números en la sucesión de Lucas. Aquı́ n es el número ingresado por
parámetro y Ln es el número correspondiente en la serie de Lucas.

n Ln n Ln
0 2 5 11
1 1 6 18
2 3 7 29
3 4 8 47
4 7 9 76

Cuadro 1.1: Números de Lucas


Los siguientes métodos retornan un valor entero en la correspondiente
1.8. AUTOEVALUACIÓN 15

sucesión de Lucas de acuerdo al número n ingresado por parámetro.


Se debe analizar si ambos algoritmos son correctos.
En el Cuadro 9 se muestra una primera solución, en la que encon-
tramos:
Cuadro 9: Lucas

public int lucas( int n )


{
int x, y, i, z;

x = 2;
y = 1;

i = 1;
while( i <= n )
{
z = x + y;
x = y;
y = z;
i++;
}
return x;
}

En el Cuadro 10 se muestra una segunda solución para los números de


Lucas.
Cuadro 10: Lucas 1

public int lucas1( int n )


{
int x, y, z, i;

x = 2;
y = 1;
for ( i = 1 ; i <= n ; i++ )
{
z = x + y;
x = y;
y = z;
}
return x;
}
16 CAPÍTULO 1. CONCEPTOS BÁSICOS

8. Vefique si el siguiente algoritmo determina si se retorna la suma de los


elementos que no son del borde de la matriz. Ver Cuadro 11. Determine
de acuerdo a los criterios dados, si el algoritmo es bueno.

Cuadro 11: suma

public int suma(int matriz [][])


{
int i, j, n=matriz.length, suma = 0;

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


{
for(j = 0; j < n; j++)
{
if (!((i==0) || i==(n-1)) && !(j==0 || j==(n-1)) )
{
suma= suma + matriz[i][j];
}
}
}
return suma;
}

9. Determinar el funcionamiento del siguiente algoritmo. Ver Cuadro 12.

Cuadro 12: palabra

public void construirPalabra (String palabra)


{
String cadena = " ";
int i,j;

for ( i=0; i<palabra.length();i++ )


{
j = 0;
while (j<=i)
{
cadena+=palabra.charAt(j);
j++;
}
cadena+="\n";
}
JOptionPane.showMessageDialog ( null, "Resultado\n"+ cadena );
}
1.8. AUTOEVALUACIÓN 17

10. Verifique si el algoritmo del Cuadro 13 retorna el número equivalente


en la serie de fibonacci. Determine de acuerdo a los criterios dados, si
el algoritmo es bueno.
Cuadro 13: fibonacci

public void fiboIterativa(int num1)


{
int penult = 0, ult =1 , prox =0, i;

i=1;
while(i<num1)
{
prox= penult+ult;
penult=ult;
ult=prox;
i++;
}
JOptionPane.showMessageDialog(null,"Resultado es: " + prox);
}

11. Dado el siguiente algoritmo, verifique que muestra el resultado de la


división de dos números enteros. Ver Cuadro14
Cuadro 14: division con restas

public void division (int num1, int num2)


{
int i, temp;

temp = num1;

for ( i = 0 ; temp >= num2 ; temp -= num2 )


{
i++;
}

JOptionPane.showMessageDialog(null," Resultado es :"+i);


}

}
18 CAPÍTULO 1. CONCEPTOS BÁSICOS

12. Dado el siguiente algoritmo, verifique que este suma cada una de las
cifras de un número entero positivo. Determine de acuerdo a los crite-
rios dados, si el algoritmo es bueno. Ver Cuadro 15
Cuadro 15: suma cifras

public void sumaCifraIterativo (int num)


{
int i;
int col = num;
int contador =0;

if(num==0)
{
JOptionPane.showMessageDialog ( null, "Resultado es : " + num);
}

for(i=10;num!=0;i+=10)
{
contador+= (num%i);
num/=i;
i-=10;
}

if(contador!=0)
{
JOptionPane.showMessageDialog ( null,"Resultado es : "+contador);
}
}
}

1.8.3. Calidad del Software

1. Investigar los atributos de calidad del software:


Portabilidad
Integridad
Robustez

1.8.4. Lecturas complementarias


Las siguientes lecturas tienen como objetivo el complemento conceptual
de los temas tratados. Se busca con ellos que el lector adquiera un vocab-
ulario más especı́fico en estos temas y conozca sobre el estado del arte de
diferentes temas relacionados con la algoritmia.
1.8. AUTOEVALUACIÓN 19

El primer artı́culo, da conceptos fundamentales sobre algoritmia. El se-


gundo es un link web, en el cual SUN Microsistem, recomienda unas con-
venciones para la escritura de código Java. El tercer documento es un caso
de éxito en la construcción de algoritmos. El cuarto documento muestra los
10 mejores algoritmos de la historia según Barry Arthur Cipra. El último
documento es una reflexión sobre la importancia del análisis de algoritmos.

1. Algoritmia. Ricardo Baeza Yates, Dpto. de Cs. de la Computación,


Universidad de Chile. http://www.dcc.uchile.cl/ rbaeza/inf/algoritmia.pdf[3]

2. Leer el texto: Code Conventions for the Java Programming Language.


http : //java.sun.com/docs/codeconv/html/CodeConvT OC.doc.html.

3. Leer el artı́culo: The Anatomy of a Large-Scale Hypertextual Web


Search Engine. http : //inf olab.stanf ord.edu/pub/papers/google.pdf

4. El artı́culo The Best of the 20th Century: Editors Name Top 10 Algo-
rithms. SIAM News, Volume 33, Number 4. Escrito por Barry Arthur
Cipra Ph.D. Mathematics. University of Maryland College Park. Cita
los algoritmos que bajo su perspectiva, son los más relevantes en la
historia de las ciencias de la computación. http : //www.siam.org

5. Leer el articulo: ¿Porque necesitamos algoritmos eficientes?. Autor:


Vladimir Estivill Castro. Doctor en Computación de la Universidad
de Waterloo, Canadá. Investigador Titular en el Laboratorio Nacional
de Informática Avanzada (LANIA), en Xalapa.
20 CAPÍTULO 1. CONCEPTOS BÁSICOS
2
Análisis de Algoritmos

2.1. Introducción

La actividad de la programación está relacionada directamente con la


tarea de diseñar e implementar algoritmos que resuelvan problemas con efi-
ciencia; es interesante comprobar las múltiples formas como se puede resolver
un mismo problema y las ventajas que se obtienen, en términos de eficiencia,
al buscar soluciones alternativas a las tradicionales.

Anteriormente, existı́an restricciones importantes a nivel de hardware,


especialmente a nivel de memoria. Evaluar un algoritmo en cuanto a con-
sumo de recursos de memoria era bastante importante, ya que dependiendo
de ella los esfuerzos de programación eran grandes.[4]. En la actualidad,
seguimos teniendo limitaciones de hardware, salvo que ahora las fronteras
están más lejanas, es por eso que muchos programadores actuales ignoran o
ven de manera poco relevante el hecho de tener que pensar en el consumo
real de memoria.
22 CAPÍTULO 2. ANÁLISIS DE ALGORITMOS

2.2. Análisis de algoritmos

El análisis de algoritmos, tiene como objetivo fundamental medir la efi-


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

2.2.1. Caso 0: Máximo común divisor

Suponga que 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 mas simple como el número más grande posible, que permite dividir
a esos números. En el Cuadro 16 se presenta una primera solución.
Cuadro 16: maximo 1

public void mcd(int a,int b)


{
int valora, valorb;
valora = a;
valorb = b;

while (valora!=valorb)
{
if(valora<valorb)
{
valorb = valorb - valora;
}
else
{
valora = valora - valorb;
}
}
imprimir("El MCD de: \n "+ a +" y "+ b +" es: "+valora);
}

En el Cuadro 17 se presenta una segunda solución al problema del máxi-


mo común divisor. Para este caso, se asume que el valor a, es el valor mayor
entre ambos parámetros.
2.2. ANÁLISIS DE ALGORITMOS 23

Cuadro 17: maximo 2

public void mcd(int a, int b)


{
int resultado = 1;

for(int i = b; i > 0; i--)


{
if(a%i==0 && b%i==0)
{
resultado = i;
break;
}
}
imprimir("El MCD de: \n "+ a +" y "+ b +" es: "+resultado);
}

Compararemos ambos algoritmos asumiendo que los valores que se envı́an


por parámetro al método son: a = 30 y b = 18.

En el Cuadro 2.1 se observan las instrucciones necesarias para la dedu-


cció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%7==0
6

Cuadro 2.1: Matriz de datos


En el Cuadro 2.2 se observan las instrucciones que son necesarias para
la deducción del MCD del primer algoritmo.
24 CAPÍTULO 2. ANÁLISIS DE ALGORITMOS

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

Cuadro 2.2: Matriz de datos

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 Cuadro 2.3 se observan las instrucciones necesarias para la deduc-
ció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

Cuadro 2.3: Matriz de datos


En el Cuadro 2.4 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 ........

Cuadro 2.4: Matriz de datos

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 concluı́do anteriormente. Este caso claramente nos muestra que para
poder decir cuando un algoritmo es mas eficiente que otro, es necesario
analizarlo por medio de diferentes conjuntos de datos de entrada.
2.2. ANÁLISIS DE ALGORITMOS 25

2.2.2. Caso 1: Potencia de un número entero

Los siguientes algoritmos calculan la potencia de un número. Cuadros


18 y 19. Se conoce a la potencia de un número como al producto de tomar
el número como factor tantas veces como se quiera, es una multiplicación
en la que los factores son siempre el mismo número. El análisis de los ejem-
plos se realizará por medio de comparaciones entre pares de algoritmos que
resuelven el mismo problema.Veremos que ambas soluciones tienen la mis-
ma cantidad de instrucciones, por lo que se puede afirmar que un problema
puede ser resuelto de forma diferente, y por lo tanto diferentes implementa-
ciones resuelven un mismo problema.
Cuadro 18: Potencia 1

public void calcularPotencia(int base,int potencia)


{
int acum = 1;
for(int i = potencia ; i >= 1; i--)
{
acum= acum * base;
}
imprimir("Potencia de: "+ base+" elevado a la "+potencia+" es: "+acum);
}

Cuadro 19: Potencia 2

public void calcularPotencia(int base,int potencia)


{
int acum = 1;
for(int i = 1 ; i <= potencia; i++)
{
acum= acum * base;
}
imprimir("Potencia de:"+ base+" elevado a la "+potencia+" es: "+acum);
}

Puede observarse que ambas soluciones son correctas, si el valor ingre-


sado es un número entero positivo.

El análisis de ambos algoritmos en cuanto a la cantidad de instrucciones


que se procesan es el siguiente: se asumen los siguientes valores que llegan por
parámetro a los métodos: base = 5 y potencia = 3, en el primer algoritmo,
Cuadro 2.5, se tienen las siguientes instrucciones:
26 CAPÍTULO 2. ANÁLISIS DE ALGORITMOS

acum i i >= 1
1 3 3 >= 1
5 2 2 >= 1
25 1 1 >= 1
125 0 0 >= 1

Cuadro 2.5: Matriz de datos


acum i i <= potencia
1 1 1 <= 3
5 2 2 <= 3
25 3 3 <= 3
125 4 4 <= 3

Cuadro 2.6: Matriz de datos

En el segundo algoritmo, Cuadro 2.6 se tienen las instrucciones:

La cantidad de comparaciones que se realizan dentro del ciclo f or son


iguales, entonces ambos algoritmos son igualmente eficientes.

2.2.3. Caso 3: Números primos

Para el problema de los números primos, se muestran dos soluciones


más a este problema, los cuales pueden apreciarse en los Cuadros 20 y 21.

En el Cuadro 20, se tiene una solución para este problema.


Cuadro 20: Es primo 3

public boolean esPrimo3( int numero )


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

Del anterior método podemos afirmar:


2.2. ANÁLISIS DE ALGORITMOS 27


El ciclo for itera numero − 1 veces, una vez se cumple el condicional
if, se rompe el ciclo a través de la instrucción break y retorna falso.

En el Cuadro 21, se tiene una última solución para este problema.

Cuadro 21: Es primo 4

public boolean esPrimo4( int numero )


{
int i, resultado;

resultado = 0;
for ( i = 2 ; i <= numero / 2 ; i++ )
{
if ( numero % i == 0 )
{
resultado = 1;
}
}

if ( resultado == 0 )
{
return true;
}
else
{
return false;
}
}

Del anterior método podemos afirmar:

numero
El ciclo for itera 2 − 1 veces.

Podemos deducir bajo un análisis del número de veces que se repite el


ciclo de cada uno de los métodos, que el orden de mayor a menor eficiencia
de acuerdo al Cuadro 2.7 es:

Eficiencia Método Número iteraciones



1 esPrimo4 numero − 1
numero
2 esPrimo3 2 −1

Cuadro 2.7: Matriz de datos


28 CAPÍTULO 2. ANÁLISIS DE ALGORITMOS

2.2.4. Caso 3: Serie de 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, a continuación se muestran los primeros 12 números de
Fibonacci. Ver Cuadro 5.1. 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.

n Fn n Fn
0 0 6 8
1 1 7 13
2 1 8 21
3 2 9 34
4 3 10 55
5 5 11 89

Cuadro 2.8: Números de Fibonacci


Los siguientes métodos retornan un valor entero en la correspondiente
sucesión de Fibonacci de acuerdo al número n ingresado por parámetro. Al
igual que el caso de los números primos, en este ejemplo se analizará la
cantidad de veces que se repite cada ciclo.

En el Cuadro 22 se muestra una primera implementación para la serie


de Fibonacci.
Cuadro 22: Fibonacci 1

public void fibo(int n)


{
int penult = 0;
int ult = 1;
int sig = 0;
for (int i = 1;i < n; i++)
{
sig = penult + ult;
penult = ult;
ult = sig;
}
imprimir("el número "+n+ " en la serie es : "+ sig);
}
2.2. ANÁLISIS DE ALGORITMOS 29

En el Cuadro 23 se muestra una segunda solución, en la que encon-


tramos:
Cuadro 23: Fibonacci 1

public void fibo1(int n)


{
int penult = 0;
int ult = 1;
int sig = 0;

i = 1;
while(i < n)
{
sig = penult + ult;
penult = ult;
ult = sig;
i++;
}
imprimir("el número "+n+ " en la serie es : "+ sig);
}

Para analizar los anteriores métodos es necesario establecer cuál o cuáles


son las instrucciones que tienen la mayor carga en el proceso de solución del
problema.

El método del Cuadro 22 utiliza un ciclo for

El método del Cuadro 23 utiliza un ciclo while

Cada uno de ellos itera la misma cantidad de veces, por lo tanto se


puede concluı́r que los 2 son igualmente eficientes, difiere la implementación
de cada uno de ellos en cuanto el número de lı́neas de código y en cuanto a
las instrucciones propias del lenguaje utilizadas. En términos generales, un
ciclo lo podemos programar tanto con un for o con un while. A pesar de
que el ciclo while, requiere más instrucciones (en cuanto número de lı́neas
de código) que el ciclo for, el número de instrucciones es el mismo.

El número de lı́neas de código necesarias para resolver un determinado


problema, no necesariamente implica mayor o menor eficiencia del método,
este concepto de eficiencia implica aspectos más complejos que requiere el
análisis detallado de las instrucciones.
30 CAPÍTULO 2. ANÁLISIS DE ALGORITMOS

2.3. Técnicas de optimización

El objetivo de las técnicas de optimización es mejorar el codigo 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 opti-
mizaciones 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).

Sustitución de tipos de datos grandes por pequeños.

Unión de ciclos (loop jamming).

2.3.1. Desenvolvimiento de ciclos

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, Cuadro 24.
Cuadro 24: Manejo de ciclos 1

public ejemplo1()
{
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)
{
k = 0;
for (int j = 0 ; j < 6; j++)
{
solucion[j] = prueba[j] + 5;
}
}
}
}
2.3. TÉCNICAS DE OPTIMIZACIÓN 31

La aplicación de la técnica de desenvolvimiento, permitirı́a modificar el


código de la siguiente manera. Cuadro 25.
Cuadro 25: Manejo de ciclos 2

public 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)
{
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 ejemplo1(), se elimina


por el equivalente en el código, de forma tal que el acceso a cada indice
del arreglo se realiza directamente. El algoritmo del Cuadro 24 es menos
eficiente que el que aparece en el Cuadro 25. De forma experimental, se
puede afirmar que la correcta aplicación de la técnica mejora el tiempo de
ejecución.

2.3.2. Reducción de esfuerzo

La técnica de reducción de esfuerzo (Strength Reduction), sugiere que


una expresión puede reemplazarse por otra, siempre y cuando cumpla la
misma función, siempre garantizando una mejora en la eficiencia de su eje-
cución. A continuación se muestra un caso en el cual se aplica esta técnica.
El codigo original se observa en el Cuadro 26.
32 CAPÍTULO 2. ANÁLISIS DE ALGORITMOS

Cuadro 26: Reducción de esfuerzo 1

public ejemplo2 ()
{
int prueba [] = new int [5000];
int solucion [] = new int [5000];

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


{
prueba[i] = (int)(Math.random () * 101);
}
for (int j = 0; i < 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. Ver Cuadro 27.

Cuadro 27: Reducción de esfuerzo 2

public 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);
}
for (int j = 0; j < solucion.length; j++)
{
solucion [j] = prueba [j] * prueba [j] * prueba [j];
}
}

La instrucción solucion[j] = M ath.pow(prueba[j], 3) del método ejem-


plo2, 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, el algoritmo del Cuadro 26 es más costoso que el que aparece en el
Cuadro 27, a causa de que la potencia es mucho más lenta que el producto.
2.3. TÉCNICAS DE OPTIMIZACIÓN 33

2.3.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. Ver Cuadro 28.

Cuadro 28: Tipos de variables 1

public ejemplo3()
{
double prueba[]= new double [2500000];
double solucion [] = new double [2500000];
for (int i=0; i < prueba.length; i++){
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 varibale int. Ver Cuadro 29.

Cuadro 29: Tipos de variables 2

public tiposVariables()
{
int prueba1[]= new int [2500000];
int solucion1 [] = new int [2500000];
for (int i=0; i < prueba1.length; i++){
prueba1[i] = (i * 5) + 345;
}
for (int j = 0; j < solucion1.length; j++)
{
solucion1[j] = prueba1[j] * j;
}
}
34 CAPÍTULO 2. ANÁLISIS DE ALGORITMOS

La figura 2.1, muestra los rango de valores y la cantidad de bits que cada
una de estas variables necesita para su ejecución. Se observa que cuando se
utiliza una variable de tipo int son necesarios 32 bits, mientras que si se
utiliza una variable de tipo double, son necesarios 64 bits.

Figura 2.1: Tipos de datos en Java2

Si se ejecutan ambos algoritmos en un computador que posee un proce-


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

2.3.4. Fusión de ciclos

Cuando en una implementación se tienen ciclos que iteran la misma


cantidad de veces. Ver Cuadro 30, 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 ins-
trucciones en un solo ciclo. Esta operación de fusión mejora la ejecución
de las instrucciones en cuanto a su tiempo.
2.3. TÉCNICAS DE OPTIMIZACIÓN 35

Cuadro 30: Fusión de ciclos 1

public ejemplo4 ()
{
int prueba [] = new int [2500000];
int prueba1 [] = new int [2500000];
int solucion [] = new int [2500000];

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


{
prueba[i] = (i * 2) * (35 - i);
}
for (int j = 0; j < solucion.length; j++)
{
solucion[j] = prueba[j] * j;
}
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 opera-
ciones que se realizan en este único ciclo. Lo anterior para evitar resultados
erroneos. Cuadro 31: Fusión de ciclos 2

public fusionCiclos()
{
int prueba [] = new int [2500000];
int prueba1 [] = new int [2500000];
int solucion [] = new int [2500000];

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


{
prueba[i] = (i * 2) * (35 - i);
solucion[i] = prueba[i] * i;
prueba1[i] = prueba[i] - i;
}
}
36 CAPÍTULO 2. ANÁLISIS DE ALGORITMOS

2.4. Conclusiones

Cuando se esta analizando la eficiencia de los algoritmos, se observa que


su tiempo de ejecución depende de las iteraciones de los ciclos. Estos ciclos
son los que tienen la mayor incidencia dentro de cada uno de los métodos,
en cuanto a cantidad de instrucciones que procesan.

Es importante al momento de decidir entre un algoritmo u otro, cual


es el conjunto de datos de entrada, pues no en todas las situaciones tiene
el mismo comportamiento, y de allı́ la importancia de la escogencia del
algoritmo.

Las técnicas de optimización mejoran el tiempo de ejecución de los al-


goritmos, pero se recomienda tener cuidado al momento de aplicarlas, ya
que muchas de estas dependen de modificaciones directas al código, lo que
en ocaciones puede generar errores involuntarios.

2.5. Autoevaluación

2.5.1. Análisis de algoritmos

1. Deduzca que problema resuelve el siguiente algoritmo Cuadro 32, y


verifique que este algoritmo sea correcto. Construya dos soluciones
diferentes a este problema.

Cuadro 32: Ejercicio 1

public void misterio (int numero1, int numero2)


{
int i, aux = numero1;

i = 0;
while (aux >= numero2)
{
i++;
aux = aux - numero2;
}
imprimir("La operación entre "+numero1+" y "+ numero2+ " es : "+i);
}
2.5. AUTOEVALUACIÓN 37

2. Analizar el método del Cuadro 33. Para este ejemplo, se utiliza el


método Math.pow, el cual recibe la base y el exponente por parámetro
y retorna la potencia de un número. ¿Que pasa en este algoritmo
cuando n = 100?.
.
Cuadro 33: Exponencial

public int exponencial( int n, int x )


{
int m, i;

m = 1;

for ( i = 1 ; i <= (int) Math.pow( 2, n ) ; i++ )


{
m = m * x;
}
return m;
}

3. Dado el siguiente algoritmo Cuadro 34, indique si es correcto afirmar


que retorna el maximo común divisor. En caso de ser necesario, realice
su correspondiente corrección, y posteriormente diga si es mas eficiente
que el algoritmo del cuadro 16.

Cuadro 34: Exponencial

public void mcd(int a, int b)


{
int resultado = 1;

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


{
if(a%i==0 && b%i==0)
{
resultado = i;
break;
}
}
imprimir("El MCD de: \n "+ a +" y "+ b +" es: "+resultado);
}
38 CAPÍTULO 2. ANÁLISIS DE ALGORITMOS

4. Dado el siguiente algoritmo Cuadro 35, indique si es correcto afirmar


que determina si un número es primo o no lo es. Compare este algo-
ritmos con los algoritmos 20 y 21. Determine en una escala, el orden
de eficiencia todos estos algoritmos.
Cuadro 35: Primo 5

public void esprimo5(int numero)


{
int auxiliar = 0;

if (numero==2 || numero==1)
{
imprimir("El numero " + numero +" " + "es primo");
}
else if (numero%2 == 0)
{
imprimir("El numero " + numero +" " + "es primo");
}
else
{
for (int i=3; i<=Math.sqrt(numero); i+=2)
{
if (numero%i==0)
{
auxiliar = 1;
break;
}
}
}
if(auxiliar==1)
{
imprimir("El numero " + numero +" " + "no es primo");
}
else
{
imprimir("El numero " + numero +" " + "es primo");
}
}

5. Dado el siguiente algoritmo Cuadro 36, indique si es correcto afirmar


que determina si un número es primo o no lo es. Compare este al-
goritmos con los algoritmos del Cuadro 20, Cuadro21 y Cuadro35.
Determine en una escala, el orden de eficiencia todos estos algoritmos.
2.5. AUTOEVALUACIÓN 39

Cuadro 36: Primo 5

public void esprimo6(int numero)


{
int raiz;
int primo = 0;

if (numero==1 || numero==2 || numero==3)


{
imprimir("El numero " + numero +" " + "es primo");
}
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)
{
imprimir("El numero " + numero +" " + "no es primo");
primo=1;
break;
}
}
}
}
if (primo == 0)
{
imprimir("El numero " + numero +" " + "es primo");
}
}

6. Dado el siguiente algoritmo Cuadro 37, indique si es correcto afirmar


que calcula el mı́nimo común múltiplo.
40 CAPÍTULO 2. ANÁLISIS DE ALGORITMOS

Cuadro 37: Ejercicio

public void mcm (int a, int b)


{
int aux = 1;
int num =2;

if (a%b==0)
{
aux = a;
}
else
{
for (int i=a;num==2;i+=a)
{
for (int n=b; (n<=i && num==2);n+=b)
{
if(i==n)
{
num = 3;
aux = i;
}
}
}
}
JOptionPane.showMessageDialog(null,"El m.c.m es: " + aux);
}

2.5.2. Lecturas Complementarias

1. Leer el artı́culo: A Definition Optimization Technique Used In A Code


Translation Algorithm. De Jean, D. Zobrist. ACM.

2. Leer el artı́culo: La optimización: una mejora en la ejecución de pro-


gramas. José M. Garcı́a Carrasco. Universidad de Castilla-La Mancha

3. Leer el artı́culo: Teaching roles of variables in elementary programming


courses. Marja Kuittinen y Jorma Sajaniemi. University of Joensuu,
Joensuu, Finland. ACM.

4. Leer el artı́culo: Identifying loops in almost linear time. G. Rama-


lingam IBM T. J. Watson Research Center, Yorktown Heights, NY.
ACM.
2.5. AUTOEVALUACIÓN 41

2.5.3. Optimización de Código

1. Dado el siguiente programa, Ver Cuadro 38, aplique las técnicas nece-
sarias para optimizarlo. Adicionalmente, describa su funcionamiento e
indique que problema resuelve.
Cuadro 38: Manejo de ciclos 1

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

2. Dado el siguiente fragmento de código java, Ver Cuadro 39, aplique


las técnicas necesarias para optimizarlo.
42 CAPÍTULO 2. ANÁLISIS DE ALGORITMOS

Cuadro 39: Ejercicio

public ejercicio ()
{
int prueba [] = new int [2500];
int prueba1 [] = new int [2000];
int solucion [] = new int [2500];
for (int i = 1; i < prueba.length; i++){
prueba[i] = i * 5;
}
for (int j = 0; j < solucion.length; j++){
solucion[j] = j * 4;
}
for (int k = 10; k < prueba1.length - 1 ; k++){
prueba1[k] = prueba[k] - k;
}
}

3. Dado el siguiente código java, Ver Cuadro 40, aplique las técnicas
necesarias para optimizarlo.
Cuadro 40: Manejo de ciclos 1

public 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);
}
}
}
}
Tiempo de ejecución
3
3.1. Introducción

Cuando se busca la solución de un problema computacional general-


mente tenemos la opción de seleccionar entre varios algoritmos, normalmente
deberı́amos escoger el que resulte más conveniente de acuerdo a nuestras
necesidades, dicha elección se realiza basándose fundamentalmente en los
siguientes aspectos:

El primero, referido a la facilidad de entendimiento y de implementación,


esto es importante cuando el programa será usado pocas veces. En tal
caso, el tiempo de programación es más importante que el costo de eje-
cución del programa y por lo tanto, debemos centrar nuestra atención
en optimizar la escritura del programa.

Por el contrario, ante un problema cuya solución se va a utilizar muchas


veces, debemos centrar nuestra atención en optimizar el tiempo de
ejecución del programa.
44 CAPÍTULO 3. TIEMPO DE EJECUCIÓN

La regla general para seleccionar el método es “escoja el más sencillo


de entender e implementar”, esta regla no es del todo equivocada, siempre
y cuando el desempeño del algoritmo no sea lo importante. Si el desempeño
del algoritmo no es lo mas importante, entonces se puede escoger el método
más sencillo de implementar.
Al intentar abordar la solución de un problema computacional tenemos
siempre la disyuntiva de seleccionar entre varios algoritmos y dicha elección
se realiza basándose fundamentalmente en los siguientes aspectos: el algo-
ritmo fácil de entender, codificar, depurar versus el algoritmo que se ejecute
con la mayor rapidez posible, haciendo un uso eficiente de los recursos de la
máquina. Lograr uno de estos objetivos generalmente implica entrar en con-
tradicción con el cumplimiento del otro, por ello debemos valorar los casos
en que se debe priorizar, el uno o el otro.
En el análisis de algoritmos para calcular el tiempo de ejecución, es
fundamental establecer 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 esta 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, sólo tendremos en cuenta el análisis para el peor de los
casos.

3.2. Tiempo de ejecución


Tradicionalmente han existido tres formas de para calcular el tiempo
de ejecución de un algoritmo, estas técnicas son conocidas como la técnica
por comparación o benchmarking, la técnica de análisis [5] y profiling. A
continuación daremos una descripción de cada una de ellas.

La técnica de benchmarking consiste en comparar dos o más algorit-


mos 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. Para este tipo de comparación
es necesario que el hardware sobre el cual se realiza la prueba a los
algoritmos debe tener las mismas caracterı́sticas.
3.3. TÉCNICAS PARA ESTIMAR EL TIEMPO DE EJECUCIÓN 45

La técnica de Profiling consiste en asociar a cada instrucción de un pro-


grama, 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 cono-
cidas (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.1 . De acuerdo a lo ante-
rior, 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, y de ahı́ la importancia de una correcta optimización del
código que forma parte de los bucles.

La técnica de análisis considera aspectos más rigurosos para la es-


timación del tiempo de ejecución. Su objetivo es analizar de forma
matemática y detallada cada uno de los pasos que ejecuta el algorit-
mo. 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 ningu-


na 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 con-
junto de datos de entrada.

3.3. Técnicas para estimar el tiempo de ejecución

Para estimar el tiempo de ejecución, se utilizará la técnica de análisis.


Esta estimación, se hará en función del número de operaciones elementales
que realiza dicho algoritmo para un tamaño de entrada dado. Entendiendo
por operaciones elementales[6], como aquellas operaciones cuyo tiempo de
ejecución se puede acotar superiormente por una constante. Ası́, se consid-
eran como operaciones elementales:

Operaciones aritméticas básicas.

Asignaciones a variables.
1
Vı́ctor Valenzuela Ruz, Universidad de Chile - Facultad de Ingenierı́a
46 CAPÍTULO 3. TIEMPO DE EJECUCIÓN

Comparaciones lógicas o relacionales.


Acceso a estructuras de datos estáticas básicas.
Parámetros que llegan a los métodos.
Instrucciones de salto.
Retorno de valores.
Para los siguientes ejemplos, se debe tener en cuenta que no analizare-
mos que tarea especı́fica realizan los algoritmos, el objetivo fundamental es
una estimación del tiempo de ejecución.
El concepto de operación elemental se aplicará de la siguiente forma,
por ejemplo si se tiene la expresión a[i − 1] = a[i], ésta contará como una
sola instrucción.

Ejemplo 1
Vamos a calcular el tiempo de ejecución del método que aparece en el
Cuadro 41:
Cuadro 41: Ejemplo 1

public void ejemplo1( int x )


{
int y, z, w;

x += 2;
y = x + 3;
z = x + y + 2;
w = x + y + z;
}

La deducción del tiempo de ejecución se muestra en el Cuadro 3.1:


El método ejemplo1 recibe un valor por parámetro, por lo tanto, se
cuenta como una instrucción
La linea de código int y, z, w, no cuenta para el tiempo de ejecución
dado que son declaraciones de variables.
El tiempo de ejecución de una secuencia consecutiva de instrucciones se
calcula sumando los tiempos de ejecución de cada una de las instrucciones.Con
base en lo anterior, en la implementación del algoritmo denominado ejemplo1,
el tiempo de ejecución es T (n) = 5.
3.3. TÉCNICAS PARA ESTIMAR EL TIEMPO DE EJECUCIÓN 47

int x Se ejecuta 1 vez


x+ = 2 Se ejecuta 1 vez
y =x+3 Se ejecuta 1 vez
z =x+y+2 Se ejecuta 1 vez
w =x+y+z Se ejecuta 1 vez

Cuadro 3.1: Tabla resumen

Ejemplo 2

Se desea calcular el tiempo de ejecución del método del Cuadro 42.

Cuadro 42: Ejemplo 2

public void ejemplo2 ( int n)


{
int i, x, y;

i = 0;
while ( i < n )
{
x = i + 3;
y = x + 2;
i = i + 1;
}
}

La estimación del tiempo de ejecución se muestra en el Cuadro 3.2:

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
y =x+2 Se ejecuta n veces
i=i+1 Se ejecuta n veces

Cuadro 3.2: Tabla resumen

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.


48 CAPÍTULO 3. TIEMPO DE EJECUCIÓN

Por lo tanto tenemos que el tiempo de ejecución T (n) = 4n + 3.

Se verifica la respuesta utilizando datos de prueba, tenemos que el con-


junto de datos de entrada del ejemplo2 es la variable n. Si el tiempo de
ejecución está determinado por T (n) = 4n + 3, reemplazaremos n = 1, 2, 3.

T (0) = 3
T (1) = 7
T (2) = 11

Ejemplo 3

Vamos a estimar el tiempo de ejecución del método que aparece en el


Cuadro 43.
Cuadro 43: Ejemplo 3

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


{
int w;

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

El método ejemplo3 recibe tres valores por parámetro, entonces se


cuentan tres instrucciones.

El ciclo for se repite n − 1 veces, i + + se repite n − 1 veces, y se debe


adicionar la vez que la comparación de i <= n es falsa para salir del
ciclo.

Para una mejor comprensión de la deducción del tiempo de ejecución,


se muestra el Cuadro 3.3:
3.3. TÉCNICAS PARA ESTIMAR EL TIEMPO DE EJECUCIÓN 49

int x Se ejecuta 1 vez


int y Se ejecuta 1 vez
int n Se ejecuta 1 vez
w=2 Se ejecuta 1 vez
i=3 Se ejecuta 1 vez
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

Cuadro 3.3: Tabla resumen

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

Ejemplo 4

Estimar el tiempo de ejecución del método que aparece en el Cuadro 44.


Cuadro 44: Ejemplo 4

public void ejemplo4( int n, int arreglo[] )


{
int temp;

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


{
if ( i < n )
{
temp = arreglo[ i ];
arreglo [ i-1 ] = arreglo[ i ];
arreglo [ i ] = temp;
}
}
}

Cuando se analiza la instrucción if (i < n), consideraremos que siem-


pre ésta será verdadera pues estamos teniendo en cuenta el peor caso.
Se dice peor caso, dado que la evaluación del condicional al asumirse
verdadera, necesariamente, implica una mayor cantidad de instrucciones
que se deben procesar.
50 CAPÍTULO 3. TIEMPO DE EJECUCIÓN

Para la estimación del tiempo de ejecución, se tiene el Cuadro 3.4, la


cual permite la estimación de la cantidad de instrucciones.

int n Se ejecuta 1 vez


int arreglo[] Se ejecuta 1 vez
i=0 Se ejecuta 1 vez
i <= n Se ejecuta (n+1) + 1veces
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
arreglo[i] = temp Se ejecuta (n+1) veces
i++ Se ejecuta (n+1) veces

Cuadro 3.4: Tabla resumen

T (n) = 4 + 6(n + 1) = 6n + 10, n >= −1

El siguiente Cuadro 3.5, 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.

Forma del Bucle 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

Cuadro 3.5: Cantidad de iteraciones


3.3. TÉCNICAS PARA ESTIMAR EL TIEMPO DE EJECUCIÓN 51

Ejemplo 5

Vamos a estimar el tiempo de ejecución del método que aparece en el


Cuadro 45. Este es un ejemplo que implica un análisis detallado de los ciclos,
dado que estos se encuentran anidados.
Cuadro 45: Ejemplo 5

public void ejemplo5 ( int n, int arreglo[], int arreglo1[] )


{
for ( int i = 0 ; i < n; i++ )
{
for ( int j = 0 ; j < n ; j++ )
{
arreglo[ j ] = arreglo1[ i ];
}
}
}

El ciclo más interno del código se ejecuta n veces, y se debe establecer


cual es la cantidad de iteraciones del ciclo mas externo a él. Dependiendo de
las veces que itere el ciclo externo, se estimará el tiempo de ejecución total.
Para este caso el ciclo externo se ejecuta n veces.

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

Tint (n) = 3n + 2

El ciclo externo se ejecuta n veces por lo tanto multiplicaremos n por el


Tint (n) y adicionaremos el tiempo de ejecución del ciclo externo. Utilizare-
mos un Ttotal (n) para indicar el tiempo de ejecución total del algoritmo.

Ttotal (n) = ((3n + 2) ∗ n) + 2n + 2 + 3


Ttotal (n) = 3n2 + 4n + 5

En este ejemplo nos permite afirmar que cuando se tienen dos ciclos
anidados que inician en 0 e iteran hasta n, 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.
52 CAPÍTULO 3. TIEMPO DE EJECUCIÓN

Ejemplo 6
El método del Cuadro 46 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 0 hasta n, por lo que el
orden de la función de tiempo de ejecución debe ser cúbico.
Cuadro 46: Ejemplo 6

public void ejemplo6( int n )


{
int x, i, j, k;

x = 0;
for ( i = 0 ; i < n ; i++ )
{
for ( j = 0 ; j < n; j++ )
{
for ( k = 0 ; k < n ; k++ )
{
x++;
}
}
}
}

Se comenzará con el análisis del ciclo más interno, éste lo llamaremos


Tint (n).

Tint (n) = 1 + n + n + n + 1 = 3n + 2
A continuación, analizaremos el ciclo del medio, éste lo llamaremos
Tmedio (n).

Tmedio (n) = 1 + n + n + 1 = 2n + 2
Vamos a calcular el tiempo de ejecución de los dos ciclos más internos,
al cual llamaremos Tparcial (n).

Tparcial (n) = ((3n + 2) ∗ n) + 2n + 2 = 3n2 + 4n + 2


Finalmente, estimaremos el tiempo de ejecución de todo el fragmento
de código, utilizaremos una función Ttotal (n).

Ttotal (n) = ((3n2 + 4n + 2) ∗ n) + 2n + 2 + 2 = 3n3 + 4n2 + 4n + 4


3.3. TÉCNICAS PARA ESTIMAR EL TIEMPO DE EJECUCIÓN 53

Ejemplo 7

A continuación se muestra una forma de estimar la función T (n) cuando


trabajamos con condicionales. Para este ejemplo (ver Cuadro 47) el valor
del parámetro n, debe ser n > 1.
Cuadro 47: Ejemplo 7

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


{
int temp, x, k, i;

k = 0;

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


{
if ( a[i] < a[i+1] )
{
temp = a[i];
a[i+1] = temp;
}
else
{
temp = a[i+1];
x = a[i];
k++;
}
}
}

En el for se realizan n − 2 comparaciones.

En la lı́nea (5), asumiremos el peor caso, por lo tanto siempre la eva-


luación del condicional será falsa, esta instrucción se repite n−2 veces.

Para este ejemplo se hace necesario analizar tanto el if como el else,


según las sintaxis de los lenguajes de programación, cuando se cumple el
if, el else no se analiza. Observamos que el else tienen en sus cuerpos de
implementación mas cantidad de lı́neas de código.
Por lo tanto el peor de los casos para este ejemplo, es que la condición
if al momento de su evaluación sea falsa y el flujo del programa continué en
el else. Cuando ambos (if o else) tienen igual número de lı́neas de imple-
mentación, cualquiera de los dos se pueden escoger.
54 CAPÍTULO 3. TIEMPO DE EJECUCIÓN

if else
if (a[i] < a[i + 1]) if (a[i] < a[i + 1])
temp = a[i] temp = a[i + 1]
a[i + 1] = temp x = a[i]
- k++

Cuadro 3.6: Tabla resumen

El análisis del tiempo de ejecución se puede ver en el Cuadro 3.6, como


puede observarse el bloque del else tiene más instrucciones.

El tiempo de ejecución total es: T (n) = 6n − 7, para n > 1.

Ejemplo 8

A continuación se mostrará el análisis de un algoritmo con un tiempo


de ejecución logarı́tmico. Este código se observa en el Cuadro 48.

Cuadro 48: Ejemplo 8

public void ejemplo8 ( )


{
int temp = 0, x = 0, y = 1, i;

i = 32;
while( i > 0 )
{
x++;
temp++;
y = y * 1;
i = 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 aplicar entonces la cantidad de
veces que itera el ciclo: f or(i = 1; i < n; i∗ = 2), entonces el ciclo se repite:
log2 (n) + 1.

Para una mejor comprensión de la deducción del tiempo de ejecución,


se muestra el Cuadro 3.7:
3.3. TÉCNICAS PARA ESTIMAR EL TIEMPO DE EJECUCIÓN 55

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
temp + + Se ejecuta log2 (i) + 1 veces
y =y∗1 Se ejecuta log2 (i) + 1 veces
i = i/2 Se ejecuta log2 (i) + 1 veces

Cuadro 3.7: Tabla resumen

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


paració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 valor de i = 32. Se
puede determinar que 25 = 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.

El tiempo de ejecución del algoritmo es: T (n) = 5(log2 (i) + 1) + 5

Ejemplo 9

A continuación se mostrará el análisis de un algoritmo con un tiempo


de ejecución logarı́tmico. Para este caso se aplica el caso en el cual el ciclo
f or(i = 1; i<n; i∗ = 2), y por lo tanto el ciclo se repite log2 n. Ver Cuadro
49.
Cuadro 49: Ejemplo 9

public void ejemplo9 ( int n)


{
int x = 0, i;
i = 1;
while( i < n ){
x++;
i = i * 2;
}
}

Para una mejor comprensión de la deducción del tiempo de ejecución,


se muestra el Cuadro 3.8:
56 CAPÍTULO 3. TIEMPO DE EJECUCIÓN

int n Se ejecuta 1 vez


x=0 Se ejecuta 1 vez
i=1 Se ejecuta 1 vez
while(j < n) Se ejecuta log2 (n) +1 veces
x++ Se ejecuta log2 (n) veces
i=i∗2 Se ejecuta log2 (n) veces

Cuadro 3.8: Tabla resumen

El tiempo de ejecución del algoritmo es: T (n) = 3(log2 (n)) + 4

Ejemplo 10

El siguiente ejemplo permite analizar una sección de código cuando el


flujo normal del programa es forzado a realizar una salida inmediata de
un ciclo. Este es el caso de la declaración break, la cual obliga una salida
que sobrepasa cualquier código restante en el cuerpo del ciclo y la prueba
condicional del ciclo. Cuando una declaración break se encuentre dentro del
ciclo, éste se termina y el control del programa continúa en la declaración
siguiente al ciclo [7]. Ver Cuadro 50
Vamos a estimar el tiempo de ejecución del siguiente fragmento de códi-
go, analizando el caso en el cual el condicional if ( w / i == 1) es ver-
dadero en la última iteración.
Cuadro 50: Ejemplo 10

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


{
for ( int i = 3 ; i < n + 2 ; i++ )
{
if ( x / i == 1)
{
a[i] = i * 2;
x++;
break;
}
a[i] = 5;
}
}

Para un mejor entendimiento del tiempo de ejecución, se muestra el


Cuadro 3.9:
3.4. TIEMPO DE EJECUCIÓN CON LLAMADA A MÉTODOS 57

int n Se ejecuta 1 vez


int a[] Se ejecuta 1 vez
int x Se ejecuta 1 vez
int i = 3 Se ejecuta 1 vez
i<n+2 Se ejecuta (n-1) veces
if (x/i == 1) Se ejecuta (n-1) veces
a[i] = i ∗ 2 Se ejecuta 1 vez
x++ Se ejecuta 1 vez
break Se ejecuta 1 vez
a[i] = 5 Se ejecuta (n-2) veces
i++ Se ejecuta (n-2) veces

Cuadro 3.9: Tabla resumen

Se tiene un condicional, en el cual se asume que se cumple en la última


iteración (n − 1), por lo tanto las instrucciones dentro del bloque del
if se ejecutan 1 sola vez.
Se ven alteradas por la declaración break todas las siguientes ins-
trucciones dado que se rompe el ciclo, estas instrucciones quedan en
la iteración n − 2.

El tiempo de ejecución es: T (n) = 4 + 2(n − 1) + 3 + 2(n − 2)

T (n) = 4n + 1

3.4. 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 las llamados a los métodos en las clases. A continuación,
se mostrará una serie de ejemplos en los cuales se muestra como 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éto-
dos, es necesario analizar el tiempo de cada uno de los métodos, y de esta
forma estabalecer cual es el peor caso, siempre considerando valores en-
teros grandes. Se recomienda para este tipo de análisis realizar pruebas con
herramientas que permitan graficar el comportamiento de cada una de las
funciones.
A continuación, se muestran algunos ejemplos que permiten establecer
el tiempo de ejecución con llamadas a métodos.
58 CAPÍTULO 3. TIEMPO DE EJECUCIÓN

Ejemplo 11

Se debe estimar el tiempo de ejecucion para el programa del Cuadro 51,


Cuadro 51: llamado a metodos

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


{
int i;

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


{
if(x == y)
{
metodo1();
}
else
{
metodo2()
}
}
}

Asuma que los tiempo de ejecucion para los metodos son:

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


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

El análisis para estimar el tiempo de ejecución del método se puede ver


en el Cuadro3.10. Es necesario analizar para un valor n, cual es el resultado
de evaluar este en la función T(n).

T(n) Método Valor n = 1000


3n + 10 3010
2n + 500 2500

Cuadro 3.10: Cantidad de iteraciones


Dado que el metodo1 es mayor que el metodo2, se establece entonces el
tiempo de ejecución con metodo1. Se procede de la misma forma en la cual
se estaba calculando el tiempo de ejecución.
El tiempo de ejecución es: T (n) = ((3n + 10) ∗ n) + (3n + 2) + 3
T (n) = 3n2 + 13n + 5
3.4. TIEMPO DE EJECUCIÓN CON LLAMADA A MÉTODOS 59

Ejemplo 12

Vamos a estimar el tiempo de ejecución del programa del Cuadro 52.


Siempre se debe considerar el peor caso.
Cuadro 52: llamado a métodos

public void llamado2( int n, int a, int b, int c)


{
if (n>b)
{
if (n>c)
{
for ( int i=1; i <= n; i++)
{
metodo1();
}
}
else
{
for (int i=0; i <= n+1; i++)
{
metodo2();
}
}
}
else
{
metodo3();
}
}

Asuma que los tiempos de ejecución para los métodos son:

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


metodo2() —> T (n) = 3n + 120
metodo3() —> T (n) = 2n2 + 10

Dado que metodo1 y metodo2 están dentro de ciclos, se debe realizar el


cálculo de estos y la incidencia que tiene cada ciclo sobre los métodos.
El siguiente cuadro 3.11 permite ver cual es el tiempo de los métodos
junto con sus ciclos. De acuerdo al análisis, se escoje el método con el mayor
tiempo de ejecución.
60 CAPÍTULO 3. TIEMPO DE EJECUCIÓN

T(n) Método Cálculo T(n) total


3n + 100 (3n + 100) ∗ n + 2n + 2 3n2 + 102n + 2
3n + 120 (3n + 120) ∗ (n + 2) + 2(n + 2) + 2 3n2 + 128n + 246
2n2 + 10 2n2 + 10

Cuadro 3.11: Cantidad de iteraciones

El tiempo de ejecución es: T (n) = 3n2 + 128n + 252

Ejemplo 13

El programa del Cuadro 53, muestra otro caso de tiempo de ejecución


con llamada a métodos.
Cuadro 53: llamado a métodos

public void llamado3( int n, int a, int b)


{
if (n>a && b>= d)
{
for (int i=0 ; i<2n ; i+=2)
{
if (n>c)
{
metodo1();
metodo2();
}
}
}
else
{
metodo3();
}
}

Asuma que los tiempos de ejecución de los métodos son:

metodo1() —> T (n) = n3

metodo2() —> T (n) = n2

metodo3() —> T (n) = n4 + 5


3.4. TIEMPO DE EJECUCIÓN CON LLAMADA A MÉTODOS 61

Dado que metodo1 y metodo2 se encuentran en el mismo bloque, se


deben sumar para establecer el tiempo total de ese bloque de código. Estos
valores junto con la incidencia del ciclo, son mayores que el T(n) del metodo3.
Es fundamental entender y conocer el comportamiento asintotico de cada
uno de los tiempos de ejecución de los métodos.

El tiempo de ejecución es: T (n) = n4 + n3 + 3n + 6

Ejemplo 14

El programa Cuadro 54, muestra otro caso de tiempo de ejecución con


llamada a métodos.
Cuadro 54: llamado a métodos

public void llamado4( int n)


{
int i;
for ( i=0; i < n; i++)
{
metodo1();
metodo2();
}
for (int i=2; i <= n+1; i++)
{
metodo3();
}
for ( i=1; i <= n; i++)
{
metodo4();
}
}

Asuma que los tiempo de ejecución para los métodos son:

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

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

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

metodo4() —> T (n) = n + 1


62 CAPÍTULO 3. TIEMPO DE EJECUCIÓN

El análisis para estimar el tiempo de ejecución del anterior método se


muestra a continuación.
Tiempo de ejecución del primer ciclo for:

T (n) = ((3n + 3) + (n2 + 10)) ∗ n + 2n + 2 = n3 + 3n2 + 15n + 2

Tiempo de ejecución del segundo ciclo for:

T (n) = (5n2 + n) ∗ n + 2n + 2 = 5n3 + n2 + 2n + 2

Tiempo de ejecución del tercer ciclo for:

T (n) = (n + 1) ∗ n + 2n + 2 = n2 + 3n + 2

Tiempo de ejecución total del métodor: T (n) = 6n3 + 5n2 + 20n + 7.

Ejemplo 15

El programa Cuadro 55, muestra otro caso de tiempo de ejecución con


llamada a métodos.
Cuadro 55: llamado a métodos

{
switch (0)
{
case 0 : metodo1();
case 1 : metodo2();
case 2 : metodo3();
}
if(m==x+2)
{
metodo4();
}
}

Asuma que los tiempo de ejecución para los métodos son:

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

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


3.4. TIEMPO DE EJECUCIÓN CON LLAMADA A MÉTODOS 63

metodo3() —> T (n) = 4n2

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

En este caso debe considerar la forma de funcionamiento de la instruc-


ción switch. El valor que esta recibiendo esta instrucción es un cero, por lo
tanto, al momento de evaluar el caso 0, se ejecuta el metodo1(), luego se eval-
ua el caso 1 y se ejecuta el metodo2() y finalmente se ejecuta el metodo3().
Por lo anterior, el tiempo de ejecución para esta situación particular es la
suma de cada uno de los tiempos de ejecución de cada uno de los métodos,
adicionalmente se suma uno de la evaluación del switch

Tiempo de ejecución: T (n) = (n + 3) + (n2 + 5) + (4n2 ) + (4n3 + 8) + 2

T (n) = 4n3 + 5n2 + n + 18

Ejemplo 16

El programa Cuadro 56, muestra otro caso de tiempo de ejecución con


llamada a métodos.
Cuadro 56: llamado a métodos

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

Asuma que los tiempo de ejecución para los métodos son:

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

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

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

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


64 CAPÍTULO 3. TIEMPO DE EJECUCIÓN

En este caso debe considerar la forma de funcionamiento de la instruc-


ción switch. El valor que esta recibiendo esta instrucción es un dos. Al
momento de evaluar tanto el caso 0 como el caso 1, estos no se cumplen.
Solo se cumple el caso 2, entonces el tiempo de ejecución de este método es
la suma de metodo3() y metodo4().

Tiempo de ejecución: T (n) = n3 + n2 + n + 8

Ejemplo 17

El programa Cuadro 57, muestra otro caso de tiempo de ejecución con


llamada a métodos.
Cuadro 57: llamado a métodos

{
switch (0)
{
case 0 : metodo1();
break;
case 1 : metodo2();
case 2 : metodo3();
case 3 : metodo4();
}
}

Asuma que los tiempo de ejecución para los métodos son:

metodo1() —> T (n) = n + 300

metodo2() —> T (n) = n5 + 4

metodo3() —> T (n) = n6 + 7

metodo4() —> T (n) = n7 + 5

En este caso debe considerar la forma de funcionamiento de la instruc-


ción switch. El valor que esta recibiendo esta instrucción es un cero. Al
momento de evaluar tanto el caso 0 se ejecuta el metodo1, a continuación se
tiene la instrucción break y por lo tanto la ejecución del switch termina.

Tiempo de ejecución: T (n) = n + 302


4
Complejidad Computacional

4.1. Introducción

En los capı́tulos anteriores, 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 de-
cidir cual algoritmo escoger bajo unos criterios razonables y sustentables.

No necesariamente por tener en la actualidad altas capacidades de proce-


samiento, quiere decir que no sea de gran utilidad medir la complejidad de
los algoritmos. Esto sigue siendo muy relevante, pues el tiempo que requieren
muchos de ellos, aún con la tecnologı́a actual es significativo.

Este capı́tulo contiene los conceptos fundamentales relacionados con la


complejidad de los algoritmos. Desde la comparación de los tiempos de eje-
cución, asi como la teorı́a para establecer la complejidad computacional de
los algoritmos.
66 CAPÍTULO 4. COMPLEJIDAD COMPUTACIONAL

n log(n) n n log(n) n2 n3 2n
2 1 1,4 2 4 8 4
4 2 2,0 8 16 64 16
8 3 2,8 24 64 512 256
16 4 4,0 64 256 4096 65536
32 5 5,7 160 1024 32768 4294967296
64 6 8,0 384 4096 262144 1,84 ∗ 1019
128 7 11,3 896 16384 2097152 3,4 ∗ 1038
256 8 16,0 2048 65536 16777216 1,15 ∗ 1077
512 9 22,6 4608 262144 134217728 1,34 ∗ 10154

Cuadro 4.1: Comparación del crecimiento de algunas funciones

4.2. Comparación del tiempo 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 Cuadro 4.1 permite establecer una comparación del cre-
cimiento de algunas funciones, en las cuales se muestra su comportamiento
y diferencias con otras funciones de acuerdo a un valor n particular.
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. A continuación se muestran algunas funciones comunes.

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

n2 (Cuadrático). Habitual cuando se su usan el ciclo anidado doble.

n3 (Cúbico). Habitual cuando se su usan el ciclo anidado triple.

2n (Exponencial). No suelen ser muy útiles en la práctica por el elevado


tiempo de ejecución.
4.2. COMPARACIÓN DEL TIEMPO DE EJECUCIÓN 67

Los siguientes ejemplos permiten comparaciones de los tiempos de eje-


cución. Considerando su crecimiento, se recomendará alguno de ellos.

Ejemplo 1

Se tienen dos algoritmos los cuales tienen un tiempo de ejecución dado


por 2n + 10 y n log(n), para el mismo problema, cual de ellos recomendar y
por que?. En la Figura 4.1 se observa que a partir de el valor 15, la función
n log(n) esta por encima de la función 2n + 10, por lo que se recomienda
que apartir de 15, se debe recomendar el algoritmo con tiempo de ejecución
2n + 10.

Figura 4.1: Comparación de tiempo de ejecución 2n + 10 vs n log(n)

Ejemplo 2

En la Figura 4.2 se ve el comportamiento de dos algoritmos que tienen


tiempos de ejecución 2n2 + n + 5 y log(n). Según su comportamiento, se
observa que en ningún caso la función logarı́tmica log(n) nunca esta por
debajo de la cuadrática, por ello se recomienda log(n).
68 CAPÍTULO 4. COMPLEJIDAD COMPUTACIONAL

Figura 4.2: Comparación de tiempo de ejecución 2n2 + n + 5 vs log(n)

Ejemplo 3

En la figura 4.3 se ve el comportamiento de dos algoritmos que tienen


tiempos de ejecución 2n2 y log(n). Según su comportamiento, se observa que
en ningún caso la función logarı́tmica nunca esta por debajo de la cuadrática
y es por ello que se debe recomendar log(n).

Figura 4.3: Comparación de tiempo de ejecución 2n2 vs log(n)


4.2. COMPARACIÓN DEL TIEMPO DE EJECUCIÓN 69

Ejemplo 4

En la Figura 4.4 se puede observar el comportamiento de dos algoritmos


que tienen los tiempos de ejecución n3 y n2 + 5. Según el comportamiento
de ambas funciones, se observa que después del valor 2,en ningún caso la
función cúbica esta por debajo de la cuadrática y es por ello que se debe
recomendar n2 + 5.

Figura 4.4: Comparación de tiempo de ejecución n3 y n2 + 5

Como se pudo observar, es fundamental estimar y entender el compor-


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

4.2.1. Talla del problema

El tamaño del problema depende del conjunto de datos de entrada,


como se ha indicado, la carga en tiempo de ejecución está dada por el dato
sobre el cual el algoritmo debe realizar el mayor trabajo, para solucionar
el problema. En el Cuadro 4.2 se muestran las medidas significativas de
problemas comunes en la algoritmia.
70 CAPÍTULO 4. COMPLEJIDAD COMPUTACIONAL

Problema Medida Tamaño


Búsqueda de un elemento en un arreglo. Número de comparaciones Número de elementos.
con un elemento.
Multiplicación de dos matrices Número de sumas y Dimensión de las
de números reales. productos. matrices.
Resolución de un sistema de Número de operaciones Número de ecuaciones
ecuaciones lineales. aritméticas. y de incógnitas.
Recorrido de un árbol binario. Número de nodos Número de nodos del
procesados. árbol.
Ordenación de una serie de Número de comparaciones Número de elementos.
elementos y de intercambios.

Cuadro 4.2: Medidas significativas de problemas comunes

4.3. Complejidad Computacional

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

Criterios orientados a minimizar el costo de desarrollo: claridad, sen-


cillez y facilidad de implantación, depuración y mantenimiento.
Criterios orientados a disminuir el costo de ejecución: tiempo de proce-
sador y cantidad de memoria utilizados.

Cuando se resuelve un problema, normalmente hay necesidad de ele-


gir entre varios algoritmos, tı́picamente existen dos objetivos que suelen
contradecirse[9]:

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 presiciones:
1
René Mac Kinney- Romero, Ph.D. in Computer Science
4.3. COMPLEJIDAD COMPUTACIONAL 71

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.

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

A continuación se muestran notaciones asintóticas que indica como cre-


cen las funciones, para valores suficientemente grandes (asintóticamente) sin
considerar constantes.

Notación Big Oh (O grande)


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.
72 CAPÍTULO 4. COMPLEJIDAD COMPUTACIONAL

Figura 4.5: Comparación de O(f (n)) con relación a T (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))”.
Se dice entonces que 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 4.5 se realiza una comparación de O(f (n))
con relación a T (n) [10].
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 verdadera-
mente 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[11].
A continuación se muestran convenciones o reglas para escribir la no-
tación O.

• Primero: es una práctica común cuando escribimos expresiones


Big Oh, eliminar todos los términos menos el término más signi-
ficativo. Por ejemplo si tenemos una función f (n) = 4n5 − 2n3 ,
simplemente escribimos O(n5 ).
4.3. COMPLEJIDAD COMPUTACIONAL 73

• Segundo: es común eliminar coeficientes constantes. Por ejemplo


si se tiene O(6n3 ), simplemente escribimos O(n3 ). Como caso
especial de esta regla, si la función es una constante y si tenemos
O(8), se escribe O(1).

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)). (Ver Figura
4.6).

Figura 4.6: Comparación de T (n) con relación a O(f (n))

4.3.2. Regla del Lı́mite

Una herramienta potente y versátil para demostrar que algunas fun-


ciones 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: IN → IR+ .
[12].

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 el orden de la otra, por ejemplo,
si queremos demostrar que n está en el orden de n2 , escribiremos n es O(n2 ).
74 CAPÍTULO 4. COMPLEJIDAD COMPUTACIONAL

f (n)
lı́m ∈ IR+ ⇒ f (n) ∈ O(g(n)) y g(n) ∈ O(f (n))
n→∞ g(n)
f (n)
lı́m =0 ⇒ f (n) ∈ O(g(n)) y g(n) ∈
/ O(f (n))
n→∞ g(n)

f (n)
lı́m = +∞ ⇒ f (n) ∈
/ O(g(n)) y g(n) ∈ O(f (n))
n→∞ g(n)

Ejemplo 1

La función 2n3 − 1 es O(n2 + 2n + 1) Solución

2n3 − 1
lı́m , por L’Hôpital
n→∞ n2 + 2n + 1

6n2
= lı́m , por L’Hôpital
n→∞ 2n + 2

12n
= lı́m
n→∞ 2
= +∞

Cuando n → +∞, la evaluación de este lı́mite tiende al infinito, por


tanto n3 − 1 no es del orden de n2 + 2n + 1 y n2 + 2n + 1 está en el orden
de n3 − 1.

Ejemplo 2

La función n3 − 1 es O(log2 (n) + 3n)

Solución
n3 − 1
lı́m , por L’Hôpital
n→∞ log2 (n) + 3n

3n2
= lı́m , simplificando
n→∞ 1
+3
n ln(2)
4.3. COMPLEJIDAD COMPUTACIONAL 75

3n2
= lı́m , simplificando
n→∞ 1 + 3n ln(2)
n ln(2)

3n3 ln(2)
= lı́m , por L’Hôpital
n→∞ 1 + 3n ln(2)

9n2 ln(2)
= lı́m , simplificando
n→∞ 3 ln(2)

= lı́m 3n2
n→∞

= +∞

Cuando n → +∞, la evaluación de este lı́mite tiende al infinito, n3 − 1


no es del orden de log(n)+3n, pero, log(n)+3n pertenece al orden de n3 −1.

Ejemplo 3

La función n3 − 1 es O(n log(3n))


Solución
n3 − 1
lı́m , por L’Hôpital
n→∞ n log2 (3n)

3n2
= lı́m , simplificando
n→∞ 3n
+ log2 (3n)
3n ln(2)

3n2
= lı́m , simplificando
n→∞ 1
+ log2 (3n)
ln(2)

3n2
= lı́m , simplificando
n→∞ 1 + ln(2) log2 (3n)
ln(2)

3 ln(2)n2
= lı́m , por L’Hôpital
n→∞ 1 + ln(2) log2 (3n)
76 CAPÍTULO 4. COMPLEJIDAD COMPUTACIONAL

6n ln(2)
= lı́m , simplificando
n→∞ 3
ln(2)
3n ln(2)

= lı́m 6 ln(2)n2
n→∞

= +∞

Cuando n → +∞ tiende al infinito, la evaluación de este lı́mite tiende al


infinito +∞, por lo tanto n3 − 1 no es del orden de n log(3n), pero n log(3n)
pertenece al orden de n3 − 1.

Ejemplo 4

La función n3 − 1 es O(2n−5 )

Solución

n3 − 1
lı́m , por L’Hôpital
n→∞ 2n−5

3n2
= lı́m , por L’Hôpital
n→∞ 2n−5 ln(2)

6n
= lı́m , por L’Hôpital
n→∞ 2n−5 ln(2)2

6
= lı́m
n→∞ 2n−5 ln(2)3

= 0

Cuando n → +∞, la evaluación de este lı́mite tiende a cero, por lo tanto


n3 − 1 está en el orden de 2n−5 , pero 2n−5 no pertenece al orden de n3 − 1.
4.3. COMPLEJIDAD COMPUTACIONAL 77

Ejemplo 5

La función n2 + 2n + 1 es O(log2 (n) + 3n)

Solución
n2 + 2n + 1
lı́m , por L’Hôpital
n→∞ log2 (n) + 3n

2n + 2
= lı́m , simplificando
n→∞ 1
+3
n ln(2)

2(n + 1)n ln(2)


= lı́m , por L’Hôpital
n→∞ 1 + 3n ln(2)

2n2 ln(2) + 2n ln(2)


= lı́m , por L’Hôpital
n→∞ 1 + 3n ln(2)

4n ln(2) + 2 ln(2)
= lı́m , simplificando
n→∞ 3 ln(2)

4n + 2
= lı́m , simplificando
n→∞ 3
= +∞

Cuando n → +∞, la evaluación de este lı́mite tiende al infinito +∞,


por lo tanto n2 + 2n + 1 no es del orden de log(n) + 3n.

Entre mientras más complejo sea un algoritmo, se puede afirmar que


este es menos eficiente.
78 CAPÍTULO 4. COMPLEJIDAD COMPUTACIONAL

4.4. Ordenes de Complejidad


Es importante entonces definir el orden de la complejidad de un algorit-
mo. 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.

4.4.1. Complejidad Constante


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.
Es la complejidad considerada como “ideal”, pero un problema conside-
rado 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.
 100

 3
 5
4
Complejidad constante O(1)

 5000
 3
8
En [13], en el capı́tulo “the Basic Axioms”, se afirma:
En su primer axioma establece que, el tiempo requerido para recupe-
rar 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 el cual tiene sentencias que son
consideradas del orden de O(1).
4.4. ORDENES DE COMPLEJIDAD 79

Ejemplo

Dado el método del Cuadro 58, vamos a identificar las instrucciones que
tengan un orden de complejidad constante.
Cuadro 58: Constante

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


{
int cantidad = n;

if ( a[n-1] == a[n-2] )
{
cantidad++;
}
else
{
cantidad--;
}
}

Cada instrucción de este método tiene un orden de complejidad O(1).


En la siguiente tabla se muestra un análisis más detallado, ver Cuadro 4.3.

instruccion Orden
int a[] O(1)
int n O(1)
int cantidad = n O(1)
if (a[n − 1] == a[n − 2]) O(1)
cantidad + + O(1)
cantidad − − O(1)

Cuadro 4.3: Orden O(1)

La variable cantidad = n se está asignado, esta operación es constan-


te (Primer axioma).
El condicional if (a[n−1] == a[n−2]) el cual es de orden O(1) (Primer
axioma).
Las asignaciones y las operaciones aritméticas son de O(1), (Segundo
axioma).

El método llamado constante, tiene orden de complejidad de O(1).


80 CAPÍTULO 4. COMPLEJIDAD COMPUTACIONAL

4.4.2. Complejidad logarı́tmica

La complejidad logarı́tmica tiene un mayor orden que la complejidad


constante, este orden de complejidad se obtiene entre otros en algunos al-
goritmos recursivos clásicos como la búsqueda binaria y los árboles binarios
de búsqueda con sus operaciones de eliminación, búsqueda e inserción. Este
orden de complejidad está relacionado con algoritmos que son consideramos
muy eficientes. A continuación se muestran algunas funciones con orden lo-
garı́tmico.

 10
 2 + log(n)
Complejidad logarı́tmica O(log(n)) 500 + log(n)
 2
10 + log(4n)

El método del Cuadro 59, tiene una complejidad logarı́tmica, a contin-


uación, mostraremos su análisis.

Cuadro 59: Logarı́tmica

public void logaritmica( int n )


{
int x = 0;
for ( int i = 1 ; i < n ; i*=2 )
{
x++;
}
}

Por ejemplo si el valor n que se ingresa es igual a n = 32, la cantidad


de instrucciones que se deben ejecutar son las siguientes. Ver Cuadro 4.4.

instrucción Cantidad de veces


int n 1 vez
int x = 0 1 vez
int i = 1 1 vez
i<n log2 (n)+1
x++ log2 (n)
i*=2 log2 (n)

Cuadro 4.4: Orden O(log2 )


4.4. ORDENES DE COMPLEJIDAD 81

Se realizan 5 comparaciones en el ciclo f or más la comparación que


termina 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 com-
paraciones del ciclo, (5 comparaciones) y el valor de j = 32. Se puede
determinar que 25 = 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 (n) y el
orden de complejidad es O(log2 (n)).

Otro ejemplo de complejidad logarı́tmica, se muestra en el método del


Cuadro 60.
Cuadro 60: Logaritmico

public int multiplicacion (int m, int n)


{
if (n==0)
{
return 1;
}
else
{
if(n==1)
{
return m;
}
else
{
if ( n % 2==0 )
{
return exp(m + m, n / 2);
}
else
{
return exp(m + m, n / 2) + m;
}
}
}
}

En este caso se puede observar que se reciben los valores m y n, ambos


se modifican, pero el criterio de parada para este algoritmo, esta dado por
la variable n. Esta variable se va dividiendo de 2 en 2, y según la tabla 3.5,
se puede afirmar que el orden de complejidad es O(log2 ).
82 CAPÍTULO 4. COMPLEJIDAD COMPUTACIONAL

4.4.3. Complejidad lineal

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.


 n + log(n)
Complejidad lineal O(n) 5n + log(n)

3n + 400

Para el método que aparece en el Cuadro 61, tenemos que su complejidad


esta relacionada directamente con la implementación de ciclos sencillos. Este
método retorna la sumatoria de los elementos de un arreglo de enteros.
Cuadro 61: Arreglo

public int sumarElementos( int prueba[] )


{
int sumatoria = 0;

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


{
sumatoria += prueba[i];
}
return sumatoria;
}

Se tiene un ciclo for, en este ciclo se inicializa con una variable i que
empieza en uno, se incrementa de uno en uno y el ciclo termina cuando
el valor se llega al tamaño del arreglo, por lo tanto se tiene un orden
de complejidad O(n).

4.4.4. Complejidad nlog(n)

La complejidad n log(n) tiene un orden mayor que la complejidad cons-


tante, 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 n log(n).
4.4. ORDENES DE COMPLEJIDAD 83



 n log(n) + 89n

log(n) + n log(n)
Complejidad O(n log(n))
 2n + 3n log(n)


9n + n log(n) + 1

Dado el método del Cuadro 62, vamos determinar su orden de comple-


jidad.
Cuadro 62: nlogn

public void nlogn( int n )


{
int x, i, j;

x = 0;
for ( i = 1 ; i < n ; i++ )
{
for ( j = 1 ; j < n ; j*=2 )
{
x = x+2;
}
}
}

Inicialmente, se analiza el ciclo más interno y posteriormente se analiza


el ciclo externo.

El ciclo interno se repite log2 veces.


El ciclo externo se repite n veces.
Multiplicando el ciclo interno por el ciclo externos, se tiene un orden
de complejidad n log(n).

4.4.5. Complejidad cuadrática


La complejidad cuadrática tiene un orden mayor que la complejidad
constante, logarı́tmica, lineal y n log(n). Este orden de complejidad normal-
mente se obtiene cuando se tienen dos ciclos anidados.
 2

 n + n + log(n) + 89n

log(5n) + 3n2
Complejidad Cuadrática O(n2 )

 9n2 + 12n

88n + 8n2 + 19 log(n) + 1
84 CAPÍTULO 4. COMPLEJIDAD COMPUTACIONAL

La complejidad cuadrática está relacionada con el uso de dos ciclos


anidados. En el Cuadro 63 mostramos un ejemplo en el cual se suman los
elementos de un arreglo bidimensional de enteros.
Cuadro 63: Arreglos

public int sumarElementos(int prueba[][])


{
int sumatoria = 0;

for (int i = prueba.length-1; i >=0 ; i--)


{
for (int j = prueba [ 0 ].length-1; j >=0; j--)

sumatoria += prueba[i][j];
}
return sumatoria;
}

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


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

El orden de complejidad para este método es O(n2 ).

4.4.6. Complejidad cúbica

La complejidad cúbica tiene un orden mayor que la complejidad cons-


tante, logarı́tmica, lineal, n log(n) y la cuadrática. Este orden de complejidad
normalmente se obtiene cuando se tiene tres ciclos anidados.
 3

 n + n2 + log(n)

4n log(n) + n3
Complejidad Cúbica O(n3 )

 n2 + 112n3 + 1

8n2 + 12n3 + 33

La complejidad cúbica está relacionada normalmente con el uso de tres


ciclos anidados los cuales tienen normalmente una variable que lo inicializa,
un incremento y un tope hasta donde se ejecuta. En el Cuadro 64 mostramos
un ejemplo de este tipo de complejidad.
4.4. ORDENES DE COMPLEJIDAD 85

Cuadro 64: Cubica

public void cubica( int a[] )


{
int x;

x = 0;
for ( int i = 0 ; i < a.length ; i++ )
{
for ( int j = 0 ; j < a.length; j++ )
{
for ( int k = 0 ; k < a.length ; k++ )
{
a[i] = i + k - j;
}
}
}
}

Encontramos tres ciclos anidados, las variables i, j, k, se incicializan


en cero y se incrementan de uno en uno, estas iteraciones llegan hasta
el tope máximo del tamaño del arreglo. Cada ciclo se ejecuta n veces,
al estar anidado, se tiene un orden de complejidad O(n3 ).

4.4.7. Complejidad exponencial

La complejidad exponencial es la menos deseable de todas las comple-


jidades, tiene un orden mayor que la complejidad constante, logarı́tmica,
lineal, n log(n), cuadrática y la cúbica. Este orden de complejidad normal-
mente se obtiene en algunos algoritmos recursivos como el de Fibonacci.
 3

 n + 2n

log(n) + 3n
Complejidad Exponencial O(2n )

 n2 + 2n3 + 5n

2334n

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. Un ejemplo se puede ver en el Cuadro
65.
86 CAPÍTULO 4. COMPLEJIDAD COMPUTACIONAL

Cuadro 65: Potencia

public int potencia( int n )


{
int m, x, i;

m = 1;
x = 1;

for ( i = 1 ; i <= Math.pow( 2, n ) ; i++ )


{
m = m * x;
}

return m;
}

La cantidad de veces que se repite el ciclo es 2n veces, el orden de


complejidad de este algoritmo es O(2n ).

4.4.8. Propiedades de la Función O

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 ésto 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: ak nk + ak−1 nk−1 + ak−2 nk−2 + . . . + a1 n1 + a0 , entonces
es posible, eliminar todos los términos con exponente inferior k. Por
la regla anterior ak nk es O(nk )[14].
Regla de la Suma Suponga que un algoritmo está formado por dos
secciones, una de ellas con O(n2 ) y la otra con O(n3 ). Entonces es
posible sumar estos dos órdenes de complejidad para obtener la com-
plejidad total del algoritmo. La regla es la siguiente:
Suponga que para T 1(n) se sabe que es O(f 1(n)) y T 2(n) es O(f 2(n))
y suponga, además, que f 1 crece más rápido que f 2. Esto se traduce en
que f 2(n) es O(f 1(n)). En consecuencia se puede concluı́r que T 1(n)+
T 2(n) es O(f 1(n))[15].
4.4. ORDENES DE COMPLEJIDAD 87

4.4.9. Complejidad para los condicionales

A continuación se muestra uno de los casos especiales para el análisis de


la complejidad computacional. En el Cuadro 66 se tiene la forma completa
de la declaración if.
Cuadro 66: Estructura de un if

if ( expresionlógica )
{
// se ejecuta si la expresionalógica es verdadera con T(n) = f(n)
}
else
{
// se ejecuta si la expresionalógica es falsa con T(n) = g(n)
}

Son cuatro las situaciones que deben ser analizadas cuando se estudia
la complejidad para los condicionales.

Que f (n) es O(g(n)), en este caso entonces se tomará O(g(n)) co-


mo el lı́mite superior del tiempo de ejecución del condicional. Esto se
concluye por tres razones:
• El orden del condicional se ignora O(1).
• Si se ejecuta el bloque que corresponde al else, el tiempo de
ejecución es g(n).
• Si se ejecuta el bloque que corresponde al if, el tiempo de ejecu-
ción en f (n) que es del O(g(n)).
De manera análoga si g(n) es O(f (n)), entonces se puede concluir que
el condicional es de O(f (n)).
Si el condicional no tiene bloque asociado al else, entonces g(n) =
0, como consecuencia el condicional es de O(f (n)), ya que g(n) es
O(f (n)).
El último caso es en el que ninguna de las dos funciones f (n) y g(n) son
O grande una de la otra. Se sabe que en un condicional se ejecuta o el
bloque asociado al if o el bloque asociado al else, pero nunca ambos.
Para este caso, se ha asumido entonces que el orden de complejidad
del condicional estará dado por el máximo entre f (n) y g(n), esto se
escribe de la forma O(maximo(f (n), g(n))).
88 CAPÍTULO 4. COMPLEJIDAD COMPUTACIONAL

Ejemplo

En el Cuadro 67 se identificará el orden de complejidad del condicional.


Se considerará el análisis del orden de complejidad del condicional.
Cuadro 67: Intercambio

public void ciclo( int a[] )


{
int j, temp;
j = 1;
while( j <= n )
{
if ( a [ j - 1 ] > a[ j ] )
{
temp = a[j - 1];
a[j - 1] = a[ j ];
a[ j ] = temp;
}
j = j + 1;
}
}

Se observa que no existe un cuerpo para una sentencia else, por lo tanto
g(n) = 0. A continuación se explica el orden de complejidad del condicional:

temp = a[j − 1] tiene un orden de complejidad O(1).


a[j − 1] = a[j] tiene un orden de complejidad O(1).
a[j] = temp tiene un orden de complejidad O(1).

Por la regla de la suma se tiene: O(1) + O(1) + O(1) = O(1). El orden de


complejidad de todo el método es O(n)

4.4.10. Complejidad de algoritmos con llamados a métodos


Generalmente cuando se desarrollan aplicaciones de tamaño considera-
ble, es necesario realizar una descomposición de las funcionalidades del sis-
tema, cuando esto sucede lo más común es crear métodos que llamen a otros
métodos. A continuación se explicará como calcular el orden de compleji-
dad cuando existe llamado a los métodos.Se recomienda analizar el método
que es invocado por otro método y ası́ sucesivamente. Una vez analizado
el método más “interno”, se debe analizar el método que lo invoca, esto se
hace hasta que termine la ejecución de todos los métodos.
4.4. ORDENES DE COMPLEJIDAD 89

Ejemplo
En los Cuadros 68 y 69 se muestra un primer ejemplo, en el cual se hace
un llamado a métodos. Todo este ejemplo se encuentra implementando una
clase en java.
Cuadro 68: Sumatoria 1

public class sumatoria1


{
public static void main( String args[ ] )
{
String cadena;
int valor, resultado;

cadena = JOptionPane.showInputDialog(null, "Ingrese número:");


valor = Integer.parseInt ( cadena );
resultado = sumatoria( a );

Dentro del método main se realiza un llamado al método sumatoria(int


x), en este momento debemos entrar a analizar el orden de complejidad del
método invocado. El orden de complejidad del método main es O(1).
Cuadro 69: Sumatoria

public static int sumatoria( int x )


{
int sumatoria = 1;

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


{
sumatoria = sumatoria + i;
}
return sumatoria;
}
}

El método que se invoca es el método sumatoria, este método recibe


por parámetro un valor entero. Como se trabaja con el peor caso, asumimos
que el número recibido es el más grande posible. Se deduce que el orden de
complejidad para el algoritmo factorial es O(n).
La deducción del orden de complejidad para el algoritmo factorial es:
O(1) + O(n) por la regla de la suma entonces el orden es O(n).
90 CAPÍTULO 4. COMPLEJIDAD COMPUTACIONAL

Ejemplo

En los Cuadros 70, 71 y 72, mostraremos otro ejemplo en el cual se


realizan llamados a métodos. Debemos agregar que si el llamado al método
está en la condición, o en el cuerpo o en el incremento, entonces debe sumarse
el tiempo que consume la función, al tiempo que consume cada iteración,
luego se continúa el análisis.
Cuadro 70: Pruebas

class pruebas
{
int matriz [ ][ ];
int suma = 0;

public void main ( String args[ ] )


{
int valor;
int entero;

entero = ingresar( "Ingrese numero entero: " );

crearMatriz ( entero );
mostrarMatriz ( entero );
}

Se realiza un llamado al método ingresar(), de acuerdo a sus lı́neas


de implementación, podemos afirmar que el orden de complejidad para el
método ingresar es O(1).
Cuadro 71: Ingresar

public int ingresar( String numero )


{
String cadena;
int value;

cadena = JOptionPane.showInputDialog ( null, numero );


value = Integer.parseInt ( cadena );

return value;
}

Desde el método main(), se realiza un llamado al método crearMatriz()


y mostrarMatriz(). Estos métodos tiene dos ciclos for anidados los cuales
4.4. ORDENES DE COMPLEJIDAD 91

iteran desde cero hasta el mismo valor. De acuerdo a ello, podemos afirmar
que el orden de complejidad para los ambos métodos es O(n2 ).

Cuadro 72: Crear y mostrar matriz

public void crearMatriz ( int cantidad )


{
int i, j;
matriz = new int [ cantidad ] [ cantidad ];

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


{
for ( j = 0 ; j < cantidad ; j++ )
{
matriz[ i ][ j ] = i + j;
suma += matriz[ i ][ j ];
}
}
}

public void mostrarMatriz ( int cantidad2 )


{
int i, j;
String resultado = "";

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


{
for ( j = 0 ; j < cantidad2; j++ )
{
resultado += matriz [ i ][ j ];
resultado += " ";
}
resultado += "\n";
}
resultado += "\n" + "La suma es: " + suma;

JOptionPane.showMessageDialog( null, resultado );


}
}

El método mostrarMatriz() tiene dos ciclos for anidados los cuales it-
eran hasta el mismo valor, de acuerdo a lo anterior, podemos afirmar que el
orden de complejidad para el método ingresar es O(n2 ). Las otras lı́neas de
implementación tienen un orden de complejidad O(1), los cuales se descar-
tan, esto por la regla de la suma. Por la regla de la suma, el programa
anterior tiene un orden de complejidad O(n2 ).
92 CAPÍTULO 4. COMPLEJIDAD COMPUTACIONAL

4.5. Conclusiones

Cuando se construye un algoritmo que es necesario usarlo en repetidas


ocasiones, el costo de su implementación es fundamental, para esta situación
se debe dar importancia a los criterios encaminados a la optimización de los
recursos.

Si un algoritmo se va a usar de forma frecuente, es mas importante el


costo de su ejecución, y por lo tanto, se debe escoger un algoritmo que use
de forma eficiente los recursos computacionales.

De acuerdo a la cantidad de datos que se deseen procesar en un deter-


minado rango de tiempo, entonces, procederemos a escoger el algoritmo más
apropiado de acuerdo a los requerimientos del usuario.

En general, la mayorı́a de los problemas tienen un parámetro de entra-


da que es el número de datos que hay que tratar, esto es, n. La cantidad
de recursos del algoritmo es tratada como una función de n. De esta ma-
nera puede establecerse un tiempo de ejecución del algoritmo que suele ser
proporcional a las funciones mostradas en el Cuadro 4.5.

El siguiente Cuadro 4.5, resume los nombres comunes asignados al tiem-


po de ejecución en términos de la función O.

Orden N ombre
O(1) Constante
O(log(n)) Logaritmica
O(n) Lineal
O(n log(n)) n log(n)
O(n2 ) Cuadratica
O(n3 ) Cúbica
O(nn ) Exponencial

Cuadro 4.5: Ordenes de Complejidad

De acuerdo a estos ordenes de complejidad, se recomienda realizar im-


plementación con el menor orden posible, dado que a menor orden mayor es
la eficiencia de los algoritmos. Lo anterior siempre considerando el conjunto
de datos de entrada que se procesarán.
4.6. AUTOEVALUACIÓN 93

4.6. Autoevaluación

4.6.1. Comparación de tiempos de ejecución

1. Suponga que han calculado los tiempos de ejecución de tres algoritmos.


Se desea saber para qué rango de datos de entrada el primer algoritmo
es más eficiente que el segundo, el segundo algoritmo es más eficiente
que el tercero y el tercer algoritmo es menos eficiente que el primero.
También se desea saber si hay algún punto en el que los tres algoritmos
son igualmente eficientes. Las funciones T (n) correspondientes son:

a) 3n3
b) 10n + 2
c) n2 + 50n + 2

2. Para las siguientes funciones de tiempo de ejecución, demuestre si cada


una de ellas es del orden de las demás.

a) n2 + 2n− + 10
b) n3 + 100
c) 2n4 + 5n + 10
d ) 5n5 + +1500

4.6.2. Complejidad

1. Para el fragmento de código mostrado en el Cuadro 73, identifique el


orden de complejidad de cada una de sus instrucciones.
Cuadro 73: Ejercicio 1

public void ejercicio ( int n)


{
int i, w, x;
for(i = 0; i <= n; i++)
{
x = i + 3;
w = x + i;
}
}
94 CAPÍTULO 4. COMPLEJIDAD COMPUTACIONAL

2. Para el fragmento de código mostrado en el Cuadro 74, identifique


el orden de complejidad de cada una de sus instrucciones. Se debe
considerar si estas estan dentro de ciclos.
Cuadro 74: Ejercicio 2

public void ejercicio( int n, int arreglo[] )


{
int temp, x = 1;

for ( int i = 4 ; i <= n+2; i++ )


{
if ( i <= n )
{
temp = arreglo[ i ];
arreglo [ i-1 ] = arreglo[ i ];
arreglo [ i ] = temp;
x++;
}
}
}

3. Para el fragmento de código mostrado en el Cuadro 75, determine su


orden de complejidad.

Cuadro 75: Ejercicio 3

public void ejercicio ( int n, int arreglo[] )


{
for ( int i = 2 ; i < n; i++ )
{
for ( int j = 3 ; j < n ; j++ )
{
for ( int j = 3 ; j < n ; j++ )
{
arreglo[ j ] = arreglo[ i ];
arreglo[ k ] = arreglo[ j ];
}
}
}
}

4. Para el código mostrado en el Cuadro 76, identifique el orden de com-


plejidad de cada una de sus instrucciones.
4.6. AUTOEVALUACIÓN 95

Cuadro 76: Ejercicio 4

public void ejercicio ( )


{
int temp = 1, x = 0, y = 1, z = 5, i;

i = 128;
while( i > 0 )
{
x++;
temp++;
y = y * 1;
z = x + temp;
i = i / 2;
}
}

5. Para los fragmentos de código mostrados en los Cuadros 77, identifique


el orden de complejidad de cada una de sus instrucciones y el orden
de complejidad de todo el método.

Cuadro 77: Ejercicio 5

public void ejercicio( int n )


{
int y, i, j;

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

j = 1;
while (j < n )
{
x = x+2;
j *=2
}
}
}

6. Para los fragmentos de código mostrados en los Cuadros 78, identifique


el orden de complejidad de cada una de sus instrucciones y el orden
de complejidad de todo el método.
96 CAPÍTULO 4. COMPLEJIDAD COMPUTACIONAL

Cuadro 78: Ejercicio 5

public void ejercicio ( int binar[], int tam, int n, int a[] )
{
int i, k;

tam = (int) Math.pow( 2, n );


k = 1;

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


{
binar [ i ] = a [ k ];
k = k + 1;
}
}

7. ¿Cúal es el orden de complejidad del código mostrado en el Cuadro


83?
Cuadro 79: Ejercicio 6

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


{
int tam, l, k;

tam = (int) Math.pow( 3, n );


l = 0;
k = 41;

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


{
binar[ i ] = a[ k ];
k = k + 1;
l++;
}
}

8. Estime el orden de complejidad del condicional que aparece en el


Cuadro 82.
4.6. AUTOEVALUACIÓN 97

Cuadro 80: Ejercicio 7

public void ejercicio( int m[][], int b[][], int n )


{
if ( m[ 0 ][ 0 ] = 0 )
{
for ( int i = 0 ; i < n ; i++ )
{
for ( int j = 0 ; j < n ; j++ )
{
b[ i ][ j ] = 0;
}
}
}
else
{
for ( int i = 0 ; i < n ; i++ )
{
b[ i ][ i ] = 1;
}
}
}

9. ¿Cúal es el orden de complejidad del código mostrado en el Cuadro


83?
Cuadro 81: Ejercicio 8

public void ejercicio ( )


{
int x = 0, y = 1, i;

i = 64;
while( i > 1 )
{
x++;
y = y * 1;
i = i / 2;
}
}

10. ¿Cúal es el orden de complejidad del código mostrado en el Cuadro


82?
98 CAPÍTULO 4. COMPLEJIDAD COMPUTACIONAL

Cuadro 82: Ejercicio 9

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


{
int temp, x, y, i;
y = 0;
for ( i = 0 ; i < n ; i++ )
{
if ( a[i] > a[i+1] )
{
temp = a[i];
}
else
{
temp = a[i+1];
x = a[i];
}
}
}

11. ¿Cúal es el orden de complejidad del código mostrado en el Cuadro


83?

Cuadro 83: Ejercicio 10

public void ejercicio ( )


{
int f = 0, i,j;
j = 256;
while( j > 1 )
{
f++;
j = j / 2;
}
for (i = 2; i <= n+3; i++)
{
for (j = n; j >= 0; j--)
{
f--;
}
}
}
4.6. AUTOEVALUACIÓN 99

4.6.3. Complejidad con llamados a métodos

1. Cual es el orden de complejidad del siguiente algoritmo Cuadro 84.


Cuadro 84: Ejercicio 11

public void ejercicio( )


{
switch (1)
{
case 0 : metodo1();
case 1 : metodo2();
}
}

Asuma que los tiempo de ejecución para los metodos son:

metodo1() —> T (n) = n4 + 9


metodo2() —> T (n) = 2n + 7

2. Estime el tiempo de ejecución del método del cuadro 87.


Cuadro 85: Ejercicio 12

public void ejercicio( int n, int m, int x)


{
for (int i=3; i<=n+2 ; i++)
{
switch (0)
{
case 0 : metodo1();
}
if(m==x+2)
{
metodo2();
}
else{
for (int i = n; i>0 ; i--)
{
Metodo1();
}
}
}

Asuma que los tiempo de ejecución para los metodos son:


100 CAPÍTULO 4. COMPLEJIDAD COMPUTACIONAL

metodo1() —> T (n) = n7 + n3 + 10


metodo2() —> T (n) = 3n + 5
metodo3() —> T (n) = 8n6 + 16

3. Estime el tiempo de ejecución del método del cuadro 87.


Cuadro 86: Ejercicio 12

public void ejercicio( int n, int m, int x)


{

for (int i=3; i<=n+2 ; i++)


{
switch (0)
{
case 0 : metodo1();
case 1 : metodo2();
case 2 : metodo3();
}
if(m==x+2)
{
metodo4();
}
else
{
for (int i=n; i>0 ; i--)
{
Metodo1();
Metodo2();
}
}
}

Asuma que los tiempo de ejecución para los metodos son:

metodo1() —> T (n) = n2 + 9


metodo2() —> T (n) = n3 + 10
metodo3() —> T (n) = 2n2 + 7
metodo4() —> T (n) = 11n

4. Estime el tiempo de ejecución del método del cuadro 87.


4.6. AUTOEVALUACIÓN 101

Cuadro 87: Ejercicio 12

public void ejercicio( int n, int m, int x)


{

switch (2)
{
case 0 : metodo1();
break;
case 1 : metodo2();
case 2 : metodo3();
}
for (int i=2; i<n+2 ; i+=2)
{
Metodo1();
Metodo2();
}
}
}

Asuma que los tiempo de ejecución para los metodos son:

metodo1() —> T (n) = n4


metodo2() —> T (n) = n5 + 11
metodo3() —> T (n) = n2

4.6.4. Lecturas Complementarias


a) Leer el artı́culo: Teaching growth of functions using equivalence
classes: an alternative to big O notation. Autor: Constantine
Roussos. Lynchburg College, Lynchburg, VA. ACM.
b) Leer el artı́culo: A big-O experiment: which function is it?. San-
dria N. Kerr Winston-Salem State University, Winston-Salem,
NC. ACM.
c) Leer el artı́culo: Measuring empirical computational complexity.
Autores: Simon F. Goldsmith - UC Berkeley, Alex S. Aiken Stan-
ford University, Stanford, CA. Daniel S. Wilkerson Berkeley, CA.
ACM.
d ) Leer el artı́culo: On computational complexity and the nature of
computer science. Autor: Juris Hartmanis Cornell Univ., Ithaca,
NY. ACM.
102 CAPÍTULO 4. COMPLEJIDAD COMPUTACIONAL

e) Leer el artı́culo: The complexity of control structures and data


structures. R. J. Lipton, S. C. Eisenstat y R. A. DeMillo. ACM.
f ) Leer el artı́culo: The future of computational complexity theo-
ry. E. Allender Dept. of Computer Science, Rutgers University.
J. Feigenbaum Communication and Information Systems ATyT
Research Murray Hill, NJ. J. Goldsmith Department of Compu-
ter Science, University of Kentucky. T. Pitassi Dept. of Computer
Science, University of Arizona. S. Rudich Computer Science De-
partment, Carnegie Mellon University, Pittsburgh, PA. ACM.
5
Recursividad

5.1. Introducción

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[16].
La recursión es una forma alternativa, para la resolución de problemas algo-
ritmicos. Muchos algoritmos se pueden expresar más fácilmente usando una
formulación recursiva.
En toda definición recursiva de un problema se debe establecer un esta-
do 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, un solución recursiva esta com-
puesta por:

Caso base: El problema se resuelve directamente (generalmente valores


pequeños).

Paso recursivo: Se divide el problema en una 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.
104 CAPÍTULO 5. RECURSIVIDAD

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

5.2. Concepto de recursividad

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 continuación, se explicará la incidencia que tiene el uso de la recus-
rividad a nivel del computador. 1
La memoria de un computador se organiza en cuatro partes lógicas:

El segmento donde se guardan las instrucciones en código de máquina.

El segmento de datos, donde se almacenan las variables estáticas.

El montı́culo (heap) destinado a las variables dinámicas.

La pila del programa (stack) destinada a las variables locales y pará-


metros de los procedimientos y funciones que se están ejecutando

Las llamadas recursivas a un programa se gestionan con el uso de varias


pilas (stack) individuales. Se crean las siguientes pilas.

Una pila para almacenar la dirección o punto de retorno.

Una pila para cada parámetro de llamada.

Una pila para cada variable local.

Para emular las llamadas recursivas de un método se sigue el proceso:


1
José Andrés Moreno Pérez, Doctor en matematicas, Departamento de Estadı́stica y
Computación. Universidad de La Laguna
5.3. ALGORITMOS RECURSIVOS 105

Se crea una pila para cada variable local, una pila para cada argumento
y una pila para la dirección de retorno.
En cada llamada recursiva del procedimiento:
• Insertar en cada pila de variable local el valor actual
• Insertar en cada pila de argumento el valor actual.
• Insertar en la pila de direcciones, la siguiente sentencia a la lla-
mada.
Al acabar cada ejecucion recursiva del procedimiento:
• Si la pila de direcciones esta vacia, devolver el control al programa
de llamada.
• Sacar de las pilas de variables locales y parámetros los valores de
las cimas.
• Devolver la ejecución a la sentencia extraı́da de la pila de dire-
cciones.

Puede observarse que de acuerdo a todas las implicaciones de usar la


recursividad, esta consume muchos recursos de memoria, y por lo tanto su
aplicación en muchas situaciones genera algoritmos ineficientes. A nivel del
diseño de los algoritmos, el uso de la recursividad puede presentar solu-
ciones cortas y claras, además de permitir en muchas ocasiones la solución
de problemas complejos.
Como metodologı́a de trabajo en este capı́tulo, primero se mostrará la
implementación de algunos algoritmos recursivos considerados tradicionales,
y posteriormente se explicará el análisis de los algoritmos recursivos. Dado
que es un tema que requiere un cambio de pensamiento en cuanto a la forma
en la cual se pueden resolver los problemas, se recomienda a los lectores,
realizar cada una de las pruebas de los ejemplos que se dan.

5.3. Algoritmos recursivos

A continuación mostraremos una serie de algoritmos recursivos consi-


derados como clásicos dentro de la algorı́tmia y los cuales permitirán una
mejor compresión de este tema. Es de tener en cuenta que cada algoritmo
recursivo puede tener uno o muchos casos base, en estos ejemplos, se tienen
en cuenta los más importantes y por ello no se consideran todos los casos
base.
106 CAPÍTULO 5. RECURSIVIDAD

5.3.1. Algoritmo recursivo: sumatoria

En el Cuadro 88 se muestra la implementación del algoritmo recursivo


sumatoria. Este método recursivo calcula la suma de los n primeros números,
el método recibe un valor como parámetro. Todos los algoritmos recursivos
si están bien diseñados, deben proveer un mecanismo de parada.
Cuadro 88: Ejemplo sumatoria

public int sumatoria ( int n )


{
if ( n == 0 )
{
return 0;
}
else
{
if ( n == 1 )
{
return 1;
}
else
{
return n + sumatoria( n - 1 );
}
}
}

Para este algoritmo que calcula la sumatoria de los n primeros números


enteros, se tienen dos condiciones de parada o casos base.
A continuación, se muestra una prueba para este algoritmo recursivo.

sumatoria(5) = 5 + sumatoria(4)
= 5 + (4 + (sumatoria(3)))
= 5 + (4 + (3 + sumatoria(2)))
= 5 + (4 + (3 + (2 + sumatoria(1))))
= 5 + (4 + (3 + (2 + 1)))
= 5 + (4 + (3 + 3))
= 5 + (4 + 6)
= 5 + 10
= 15
5.3. ALGORITMOS RECURSIVOS 107

En la Figura 5.1 se muestra una de las formas como se puede representar


este algoritmo recursivo. Se puede observar que cada vez que se invoca el
método, este tiene instancias menores del problema.

Figura 5.1: Algoritmo recursivo sumatoria

5.3.2. Algoritmo recursivo: multiplicación

El siguiente método retona la multiplicación de dos números enteros


de forma recursiva. Estos números se reciben por parámetro. Definamos un
caso de prueba que permita multiplicar los números cinco por tres.

Obtener 4 * 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
108 CAPÍTULO 5. RECURSIVIDAD

La siguiente es la implementacion del método multiplicar. Cuadro 89 .


Cuadro 89: Ejemplo multiplicación

public int multiplicar ( int a, int b )


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

Este algoritmo recursivo calcula la multiplicación de dos números (a∗b),


el método recibe dos parámetros. Para este algoritmo que multiplica dos
números enteros, se tienen 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 Figura 5.2 se muestra una de las formas como se puede representar
este algoritmo recursivo.
5.3. ALGORITMOS RECURSIVOS 109

Figura 5.2: Algoritmo recursivo multiplicación

5.3.3. Algoritmo recursivo: suma de cifras de un número

En el Cuadro 90 mostramos el algoritmo recursivo que suma las cifras


de un número entero. Este método recibe un número entero por parámetro
y de acuerdo al valor de cada uno de sus dı́gitos, se retorna su suma. Para
este caso, unicamente se considera un criterio de parada el cual es suficiente
para terminar la ejecución del algoritmo.
Cuadro 90: Algoritmo recursivo suma cifras de un número

public int sumaCifras ( int n )


{
if ( n < 10 )
{
return n;
}
else
{
return sumaCifras ( n / 10 ) + n % 10;
}
}
110 CAPÍTULO 5. RECURSIVIDAD

En la Figura 5.3 se muestra una forma como se puede representar este


algoritmo recursivo.

Figura 5.3: Algoritmo recursivo suma de cifras de un entero

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

sumacif ras(983) = 3 + sumacif ras(98)


= 3 + (8 + sumacif ras(9))
= 3 + (8 + 9)
= 3 + 17
= 20

Otro ejemplo de prueba es el siguiente:

sumacif ras(1942) = 2 + sumacif ras(194)


= 2 + (4 + sumacif ras(19))
= 2 + (4 + (9 + sumacif ras(1)))
= 2 + (4 + (10))
= 2 + (14)
= 16
5.3. ALGORITMOS RECURSIVOS 111

5.3.4. Algoritmo recursivo: potencia

En el Cuadro 91 mostramos el algoritmo recursivo potencia, permite


realizar operación de potencia de dos números enteros. Este algoritmo recibe
dos parámetros enteros, y tiene varios mecanismos de parada.
Cuadro 91: Algoritmo recursión potencia

public int potencia (int a, int b )


{
if ( a == 0 )
{
return 0;
}
if ( b == 0 )
{
return 1;
}
else
{
if ( b == 1 )
{
return a;
}
else
{
return potencia ( a, b - 1 ) * a;
}
}
}

A continuación, se muestra una prueba para el algoritmo recursivo po-


tencia.

potencia(3, 4) = 3 ∗ potencia(3, 3)
= 3 ∗ (3 ∗ (potencia(3, 2)))
= 3 ∗ (3 ∗ (3 ∗ potencia(3, 1))
= 3 ∗ (3 ∗ (3 ∗ 3))
= 3 ∗ (3 ∗ 9)
= 3 ∗ 27
= 81
112 CAPÍTULO 5. RECURSIVIDAD

En la Figura 5.4 se muestra una traza de este método recursivo.

Figura 5.4: Algoritmo recursivo potencia

5.3.5. Algoritmo recursivo: cantidad de cifras de un número

En el Cuadro 92 mostramos el algoritmo recursivo que permite hallar la


cantidad de cifras de un número entero, el método recibe como parámetro
un número entero.
Cuadro 92: Algoritmo recursivo número de cifras de un número

public int cifras ( int n )


{
if( n < 10 )
{
return 1;
}
else
{
return cifras ( n / 10 ) + 1;
}
}

Si el número recibido por parámetro es menor que 10, retorna uno y


termina.
5.3. ALGORITMOS RECURSIVOS 113

En caso de que la condición inicial no se cumpla, se hace un llamado


recursivo cifras, pero se divide el dato de entrada en 10 y se almacena
el valor de 1. Este valor de 1, permite ir llevando el conteo de la
cantidad de cifras del número recibido por parámetro.

En la Figura 5.5 se muestra una forma como se puede representar este


algoritmo recursivo.

Figura 5.5: Algoritmo recursivo cantidad de cifras de un número

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

cif ras(23454) = 1 + cif ras(2345)


= 1 + (1 + cif ras(234))
= 1 + (1 + (1 + cif ras(23)))
= 1 + (1 + (1 + (1 + cif ras(2))))
= 1 + (1 + (1 + (1 + (1 + 1))))
= 1 + (1 + (1 + (1 + 2)))
= 1 + (1 + (1 + 2))
= 1 + (1 + 3)
= 1+4
= 5
114 CAPÍTULO 5. RECURSIVIDAD

5.3.6. Algoritmo recursivo: máximo común divisor

En el Cuadro 93 mostramos el algoritmo recursivo que permite hallar el


máximo común divisor (MCD) de dos números enteros. Un algoritmo clásico
para calcular el MCD es el algoritmo de Euclides, se considerarán dos aspectos
fundamentales:

Si b = 0 el máximo común divisor es a

Si b 6= 0, el nuevo MCD es el MCD de b y el resto de dividir a por b.

Cuadro 93: Algoritmo recursivo MCD

public int MCD ( int a, int b )


{
if( b == 0 )
{
return a;
}
else
{
return MCD ( b, a % b );
}
}

En la Figura 5.6 se muestra una traza de este método recursivo.

Figura 5.6: Algoritmo recursivo mcd

Ejemplo del algoritmo recursivo MCD.


5.3. ALGORITMOS RECURSIVOS 115

M CD(110, 13) = M CD(13, 6)


= M CD(6, 1)
= M CD(1, 0)
= 1

M CD(623, 422) = M CD(422, 201)


= M CD(201, 20)
= M CD(20, 1)
= M CD(1, 0)
= 1

5.3.7. Algoritmo recursivo: números armónicos

Entre las varias sucesiones interesantes de números que existen en las


matemáticas discretas y combinatoria, están los números armónicos, los
cuales tienen la forma: H1 + H2 + H3 + . . . donde:

H1 = 1
1
H2 = 1 +
2
1 1
H3 = 1+ +
2 3
1 1 1
Y, en general, Hn = 1 + 2 + 3 + ... + n para cualquier n → Z+ .

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

H1 = 1, n=1
µ ¶
1
Hn+1 = Hn + , n≥1
n+1
116 CAPÍTULO 5. RECURSIVIDAD

En el Cuadro 94 mostramos el algoritmo recursivo que permite encontrar


un número de la sucesión de números armónicos.
Cuadro 94: Algoritmo recursivo números Armónicos

public double numerosArmonicos( double n )


{
if ( n == 0.0 )
{
return 0.0;
}
else
{
if ( n == 1.0 )
{
return 1.0;
}
else
{
return ( 1.0 / n ) + numerosArmonicos( n - 1.0 );
}
}
}

Este algoritmo recursivo retorna un valor en la equivalente serie armónica.


Para este algoritmo únicamente se tienen dos casos base.
Se tiene un condicional que verifica si el número recibido por parámetro
es igual a cero, si la condición es verdadera, retorna cero.
Si el número recibido por parámetro es igual a uno, retorna uno.
En caso de no cumplirse ninguno de los casos base, se realiza la llamada
recursiva, la cual se disminuye de uno en uno hasta terminar.
Un ejemplo del algoritmo recursivo numerosArmonicos es el siguiente.
µ ¶
1
numerosArmonicos(4) = + numerosArmonicos(3)
4
µ ¶ µ ¶
1 1
= + + numerosArmonicos(2)
4 3
µ ¶ µ ¶ µ ¶
1 1 1
= + + + numerosArmonicos(1)
4 3 2
µ ¶ µ ¶ µ ¶
1 1 1
= + + + (1)
4 3 2
= 2,08333
5.3. ALGORITMOS RECURSIVOS 117

5.3.8. Algoritmo recursivo: módulo

En el Cuadro 95 mostramos el algoritmo recursivo para calcular el módu-


lo de dos números. También se conoce como el resto de dividir a por b. Vamos
a considerar dos aspectos fundamentales:

Si a < b entonces es el resto es a.

Si a ≥ b, el resto es el mismo que el de dividir a − b por b.

Cuadro 95: Algoritmo recursivo módulo

public int modulo ( int a, int b )


{
if( a == 0 && b == 0 )
{
return 0;
}
else
{
if( a < b )
{
return a;
}
else
{
return modulo( a - b, b );
}
}
}

A continuación, se da una prueba para el algoritmo recursivo módulo.

modulo(58, 12) = modulo(46, 12)


= modulo(34, 12)
= modulo(22, 12)
= modulo(10, 12)
= 10

En la Figura 5.7 se muestra una traza de este método recursivo.


118 CAPÍTULO 5. RECURSIVIDAD

Figura 5.7: Algoritmo recursivo modulo

5.3.9. Algoritmo recursivo: Contar ceros arreglo

En el Cuadro 96 mostramos el algoritmo recursivo contar la cantidad


de ceros que se encuentran dentro de un arreglo unidimensional de enteros.
Cada vez que sea verdadero el condicional, se sumará en uno la cantidad de
números ceros.
Cuadro 96: Algoritmo recursivo ceros arreglo

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


{
if (n == 0)
{
return 0;
}
else
{
if (arreglo[n - 1] == 0)
{
return 1 + cantidadCeros (arreglo, n - 1);
}
else
{
return cantidadCeros (arreglo, n - 1);
}
}
}
5.3. ALGORITMOS RECURSIVOS 119

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.
En la Figura 5.8 se muestra una traza de este método recursivo.

Figura 5.8: Algoritmo recursivo modulo

5.3.10. Algoritmo recursivo: Número menor arreglo

En el Cuadro 97 mostramos el algoritmo recursivo que retorna el número


menor de un arreglo unidimensional de elementos. El recorrido del arreglo se
realiza de principio del mismo hasta su final. El criterio de parada es cuando
inicio y f in son iguales.
Los parámetros que recibe este método son los siguientes:

vector[] : Es el arreglo en el cual se encuentran los elementos.

inicio: Es el inicio del arreglo, y su valor es cero.

f in : Es el fin del arreglo y su valor es el tamaño del arreglo - 1.


120 CAPÍTULO 5. RECURSIVIDAD

Cuadro 97: Algoritmo recursivo número menor arreglo

public int numeroMenor(int vector[],int inicio, int fin)


{

if(inicio == fin)
{
return vector[inicio];
}
else
{
int menor = numeroMenor(vector,inicio+1,fin);

if(menor<vector[inicio])
{
return menor;
}
else
{
return vector[inicio];
}
}
}

5.3.11. Algoritmo recursivo: Número de apariciones

En el Cuadro 98 mostramos el algoritmo recursivo que retorna el número


de apariciones que tiene un elemento dentro de un arreglo unidimensional.
En este tipo de problemas, lo más importante es establecer tanto un mecanis-
mo de parada como un mecanismo para recorrer el arreglo. Para este caso, se
recorrerá el algoritmo desde la última posición del arreglo, hasta la primera
posición del arreglo.

Los parámetros que recibe este método son los siguientes:

vector[] : es el arreglo en el cual se encuentran los elementos.

inicio : es el inicio del vector.

f in : es el fin del vector.

elem : es el número a buscar dentro del arreglo.


5.3. ALGORITMOS RECURSIVOS 121

Cuadro 98: Algoritmo recursivo número de apariciones

public int ocurrencias(int arreglo1[],int inicio,int fin, int elem)


{
if(inicio == fin)
{
if(arreglo1[inicio]==elem)
{
return 1;
}
else
{
return 0;
}
}
else
{
if(arreglo1[fin]!=elem)
{
return ocurrencias(arreglo1,inicio,fin-1,elem);
}
else
{
return 1+ ocurrencias(arreglo1,inicio,fin-1,elem);
}
}
}

Cuando la instrucción if (arreglo1[f in]! = elem) sea verdadera, se tiene


en la recursión la adición de cada unidad.

5.3.12. Algoritmo recursivo número mayor arreglo

En el Cuadro 99 mostramos el algoritmo recursivo mayor, el cual permite


obtener el número mayor de un arreglo de enteros positivos.
Los parámetros que recibe este método son los siguientes:

arreglo[] : es el arreglo en el cual se encuentran los elementos.


inicio : es el inicio del arreglo.
f in : almacena el tamaño del arreglo-1
mayor : es el número a buscar dentro del arreglo.
122 CAPÍTULO 5. RECURSIVIDAD

Cuadro 99: Algoritmo recursivo mayor

public int mayor(int arreglo[],int inicio, int fin, int mayor)


{
if (inicio == fin)
{
if (arreglo[inicio] > mayor)
{
mayor = arreglo[inicio];
}
return mayor;
}
else
{
if (arreglo[inicio] > mayor)
{
mayor = arreglo[inicio];
}
return mayor (arreglo, inicio + 1, fin , mayor);
}
}

5.3.13. Algoritmo recursivo suma elementos de un arreglo

En el Cuadro 100 mostramos el algoritmo recursivo suma, permite rea-


lizar la suma entera del contenido de todos los elementos de un arreglo
unidimensional.
Cuadro 100: Algoritmo recursivo suma

public int suma( int x[], int n )


{
if( n == 1 )
{
return x[ 0 ];
}
else
{
return x[ n - 1 ] + suma( x, n - 1 );
}
}
5.4. ANÁLISIS DE ALGORITMOS RECURSIVOS 123

5.4. Análisis 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 re-
cursividad 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 re-
cursivas pueden ocurrir, dado que el problema no se puede resolver
directamente.

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.

Comúnmente estas formas para calcular el tiempo de ejecución y orden


de complejidad de los algoritmos recursivos, se conocen como:

Inducción
Repeticiones sucesivas

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


complejidad para algoritmos recursivos.
124 CAPÍTULO 5. RECURSIVIDAD

5.4.1. Análisis del algoritmo recursivo: factorial

A continuación se muestra un algoritmo recursivo que permite hallar el


factorial de un número entero. Inicialmente se muestra un deducción infor-
mal de cómo hallar un número factorial cualquiera.

Definición de factorial para n ≥ 0.

0! = 1
1! = 1
2! = 2 ∗ 1 = 2 ∗ 1!
3! = 3 ∗ 2 ∗ 1 = 3 ∗ 2!
4! = 4 ∗ 3 ∗ 2 ∗ 1 = 4 ∗ 3!
···
n! = n ∗ (n − 1)!

En el Cuadro 101 mostramos el algoritmo recursivo que calcula el factorial


de un entero positivo.

Cuadro 101: Algoritmo recursivo factorial

public int factorial( int n )


{
if (n == 1 || n == 0)
{
return 1;
}
else
{
return n * factorial(n - 1);
}
}

Si el número recibido por parámetro es igual a uno, retorna uno y


termina.

Se hace un llamado recursivo al método factorial, se almacena el valor


de n y se disminuye en uno el conjunto de datos de entrada.

En la Figura 5.9 se muestra un ejemplo del algoritmo factorial.


5.4. ANÁLISIS DE ALGORITMOS RECURSIVOS 125

Figura 5.9: Algoritmo recursivo factorial

A continuación se muestra una instancia del algoritmo factorial.

f actorial(3) = 3 ∗ (f actorial(2))
= 3 ∗ (2 ∗ f actorial(1))
= 3 ∗ (2 ∗ 1)
= 3∗2
= 6

El siguiente es el análisis del tiempo de ejecución del algoritmo factorial.

Para el análisis del algoritmo recursivo, se debe inicialmente, analizar el


tiempo de ejecución para el caso base. El tiempo de ejecución para el caso
base lo denotaremos T (1), el caso base se da cuando n = 1, y no existe
recursión. Se ejecuta el caso base, cuyo tiempo de ejecución es T (1) = 2, y
por lo tanto su orden de complejidad es de orden O(1). Con lo anterior se
puede decir que T (1) = O(1). El siguiente código corresponde al caso base.

if ((n == 1) || (n == 0))
{
return 1;
}
126 CAPÍTULO 5. RECURSIVIDAD

A continuación, se debe tener en cuenta cuando el valor ingresado por


parámetro es n > 1. Es decir cuando se realiza la llamada recursiva.

if ((n == 1) || n == 0))
{
return 1;
}
else
{
return n * factorial(n - 1);
}

El caso base que tiene el condicional if (n == 1)||n == 0) se analiza


y se supone falso, entonces se ejecuta la llamada al método recursivo. El
caso base es de orden O(1) y return n ∗ f actorial(n − 1) es de O(1) por
la multiplicación más el T (n − 1) de la llamada recursiva. Por lo tanto es
posible definir el caso inductivo de la siguiente forma:
Caso inductivo: T (n) = O(1) + T (n − 1), para n > 1.
Ahora se debe reemplazar el orden O(1) del caso base por una constan-
te a y el término orden O(1) de la inducción por otra constante b. Estos
cambios generan la siguiente relación de recurrencia, la cual esta expresada
en términos de un caso base y un caso inductivo.
Caso Base: T (1) = a,
Inducción: T (n) = b + T (n − 1),
El siguiente paso consiste en asignar valores a n y reemplazando el caso
base en la inducción. A continuación se muestra la forma de asignar estos
valores.

T (2) = b + T (1) = b + a
T (3) = b + T (2) = b + (b + a) = 2b + a
T (4) = b + T (3) = b + (2b + a) = 3b + a
T (5) = b + T (4) = b + (3b + a) = 4b + a
T (6) = b + T (5) = b + (4b + a) = 5b + a
···
T (n) = b + T (n − 1) = (n − 1)b + a
5.4. ANÁLISIS DE ALGORITMOS RECURSIVOS 127

Se debe encontrar una generalización del tiempo de ejecución en termi-


nos de n, por lo tanto se puede decir que T (n) = b + T (n − 1) = (n − 1)b + a,
para todo n ≥ 1. Esta generalización es equivalente a afirmar que el caso
de inducción es T (n) = nb + (a − b), evaluando esta función de tiempo de
ejecución, se tiene que nb es de orden O(n) y (a − b) es de orden O(1), por la
regla de la suma se deduce que T (n) es de orden O(n). La implementación
del algoritmo recursivo factorial es de orden lineal.

5.4.2. Análisis del algoritmo recursivo: multiplicación

En el Cuadro 102 mostramos el algoritmo recursivo con un tiempo de eje-


cución de orden logarı́tmico, retorna el resultado de multiplicar dos números.
Cuadro 102: Algoritmo recursivo multiplicación

public int multiplicacion (int m, int n)


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

Este algoritmo recursivo permite hallar la multiplicación entre dos números


enteros, el análisis del algoritmo recursivo, consiste inicialmente en analizar
el tiempo de ejecución para el caso base.
128 CAPÍTULO 5. RECURSIVIDAD

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


so base se da para dos casos: cuando el parámetro n = 0 o cuando el
parámetro n = 1, retornan respectivamente los valores 0 y m que es el
segundo parámetro. Para este caso que no existe 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).

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

Se deduce por tanto que el tiempo de ejecución es de la forma:

T (n) = b log(n) + a.

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

f ib(n) = 0 , n = 0
f ib(n) = 1 , n = 1
f ib(n) = f ib(n − 1) + f ib(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.
Ver Cuadro 5.1.
5.4. ANÁLISIS DE ALGORITMOS RECURSIVOS 129

n Fn n Fn
0 0 6 8
1 1 7 13
2 1 8 21
3 2 9 34
4 3 10 55
5 5 11 89

Cuadro 5.1: Números de Fibonacci

En el Cuadro 103 mostramos el algoritmo recursivo que encuentra el


n-ésimo número de la sucesión Fibonacci.
Cuadro 103: Algoritmo recursivo Fibonacci

public int fibo( int n )


{
if ( n == 0 )
{
return 0;
}
else
{
if ( n == 1 || n == 2 )
{
return 1;
}
else
{
return ( fibo ( n - 2 ) + fibo ( n - 1 ) );
}
}
}

Ejemplo del algoritmo recursivo Fibonacci

f ibo(4) = f ibo(2) + f ibo(3)


= (f ibo(0) + f ibo(1)) + f ibo(3)
= (0 + f ibo(1)) + f ibo(3)
= (0 + 1) + f ibo(3)
= 1 + f ibo(3)
= 1 + (f ibo(1) + f ibo(2))
= 1 + (1 + f ibo(2))
130 CAPÍTULO 5. RECURSIVIDAD

= 1 + (1 + (f ibo(0) + f ibo(1))
= 1 + (1 + (0 + f ibo(1)))
= 1 + (1 + (0 + 1))
= 1 + (1 + 1)
= 1+2
= 3
Análisis del algoritmo recursivo
Base: T (1) = a
Inducción: T (n) = b + T (n − 2) + T (n − 1)

Para simplificar el análisis, se toma el peor de los casos entre T (n − 2)


y T (n − 1). Se puede afirmar que T (n − 1) tomará más tiempo, ya que n − 1
es más grande que n − 2. Se asume entonces que:

T (n) = b + 2T (n − 1)
Se asignan valores a n:
T (1) = a
T (2) = b + 2T (1) = b + 2a
T (3) = b + 2T (2) = b + 2(b + 2a) = (22 − 1)b + 22 a
T (4) = b + 2T (3) = b + 2(3b + 4a) = (23 − 1)b + 23 a
T (5) = b + 2T (4) = b + 2(7b + 8a) = (24 − 1)b + 24 a
T (6) = b + 2T (5) = b + 2(15b + 16a) = (25 − 1)b + 25 a
Se concluye entonces que:
T (n) = (2n−1 − 1)b + 2n−1 a, como a y b son de orden O(1) esta ecuación
se puede volver de la forma: T (n) = 2 ∗ 2n−1 − 1 = 2n − 1, en consecuencia
es de orden O(2n ).

5.5. Resolver recurrencias por inducción


A continuación, 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, unicamente se estudiará su tiempo de ejecución y su orden de
complejidad.
5.5. RESOLVER RECURRENCIAS POR INDUCCIÓN 131

5.5.1. Ejemplo 1

En el Cuadro 104 mostramos un algoritmo recursivo que retorna la


potencia de un número.
Cuadro 104: Ejemplo recursivo Calcular potencia

public int calcularPotencia (int x, int n)


{
if (n==0)
{
return 1;
}
else
{
if (n%2==0)
{
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
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)
132 CAPÍTULO 5. RECURSIVIDAD

5.5.2. Ejemplo 2

En el Cuadro 105 mostramos un 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.

Cuadro 105: Ejemplo recursivo2

public int calcularPotencia (int x, int n)


{
if (n==0)
{
return 1;
}
else
{
if (n%2==0)
{
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:

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
5.5. RESOLVER RECURRENCIAS POR INDUCCIÓN 133

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)

5.5.3. Ejemplo 3

En este ejemplo, 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 cual es su tiempo de ejecución y cual 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) = 32a
···
T (n) = 2T (n) ⇒ T (n) = 2n−1 a

El orden complejidad para un algoritmo con estas caracterı́sticas es O(2n ).


134 CAPÍTULO 5. RECURSIVIDAD

5.5.4. Ejemplo 4

En el Cuadro 106 mostramos un algoritmo recursivo del cual se va a


deducir cual es su tiempo de ejecución y su orden de complejidad.
Cuadro 106: Ejemplo recursivo4

public int recursivo4( int n )


{
int top = 4;

if (n <= 1)
{
return 1;
}
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
T (n) = 3T (n − 1) + b ⇒ T (n) = 3n−1 a + [3n−2 b + 3n−3 b + . . . + b]
···
5.5. RESOLVER RECURRENCIAS POR INDUCCIÓN 135

Generalizando, tenemos:

n
X
T (n) = 3T (n − 1) + b ⇒ T (n) = 3n+1 a + (3n−i b)
i=2

El orden complejidad para un algoritmo con este caso base e inductivo


es de orden O(3n )

5.5.5. Ejemplo 5

En el Cuadro 107 mostramos un algoritmo recursivo al cual una vez


analizado, permitirá determinar su orden de complejidad.

Cuadro 107: Ejemplo recursivo4

public int recursivo5( int n )


{
if (n <= 1)
{
return 1;
}
else
{
return n*(recursivo5(n-1) + recursivo5(n-1) + recursivo5(n-1));
}
}

En el siguiente ejemplo, se muestra una forma de representar la respuesta


de forma diferente.

En el caso base se tiene que para el peor de los casos el tiempo de


ejecución es: T (1) = 3. 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)+O(1).

Generalizando tenemos, que el caso base y el caso inductivo, quedan


expresados de la siguiente manera:

T (1) = O(1)
T (n) = 3T (n − 1) + O(1)
136 CAPÍTULO 5. RECURSIVIDAD

Reemplazando por constantes tenemos que:

T (1) = a
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
···
T (n) = 3T (n − 1) + b ⇒ T (n) = 3n−1 a + [3n−2 b + 3n−3 b + . . . + b]

Generalizando, tenemos:

3n−1 − 1
T (n) = 3T (n − 1) + b ⇒ T (n) = 3n−1 a + b
2

El orden complejidad para un algoritmo con estas caracterı́sticas es


O(3n ).

De acuerdo a los dos anteriores ejemplos, es totalmente válido deducir


el orden de complejidad de los algoritmos recursivos utilizando diferentes
herramientas matemáticas.

5.5.6. Ejemplo 6

En el Cuadro 108 mostramos un algoritmo recursivo al cual se le de-


ducirá su orden de complejidad. Se puede observa que en el caso inductivo
se tiene una llamada recursiva la cual se encuetra dentro de un ciclo. Esta
situación, necesariamente implica un análisis de la incidencia de los ciclos
dentro de los algoritmos recursivos.
5.5. RESOLVER RECURRENCIAS POR INDUCCIÓN 137

A continuación, se muestra un algoritmo recursivo que involucra un caso


inductivo con un ciclo.
Cuadro 108: Ejemplo recursivo6

public int recursivo6( int n )


{
int j = 0;
if ( n <= 1 )
{
return 1;
}
else
{
int k = 1
while ( k <= n)
{
j = j + 1;
k++;
}

return ( recursivo6(n-1) + recursivo6(n-1) + recursivo6(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 tiene que en el else, se
cuenta con un ciclo while el cual para el peor de los casos es de O(n), más
tres llamadas recursivas, cada una de (n − 1), por lo tanto el caso inductivo
es: T (n) = 3T (n − 1) + O(n).

Generalizando tenemos, que el caso base y el caso inductivo, quedan


expresados de la siguiente manera:

T (1) = O(1)
T (n) = 3T (n − 1) + O(n)

Reemplazando por constantes, tenemos que:

T (1) = a
T (n) = 3T (n − 1) + bn
138 CAPÍTULO 5. RECURSIVIDAD

Reemplazando la base en la inducción, tenemos:

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


T (3) = 3T (2) + bn ⇒ T (3) = 32 a + 32 b
T (4) = 3T (3) + bn ⇒ T (4) = 3n−1 a + 3n−2 2b + 3n−2 b + 4b
T (5) = 3T (4) + bn ⇒ T (5) = 3n−1 a + 3n−2 2b + 3n−2 b + 3 ∗ 4b + 5b
···

Generalizando, tenemos:

T (n) = 3T (n − 1) + bn ⇒
n−4
X ¡ i ¢
T (n) = 3n−1 a + 3n−1 b + 3 (n − i)b , ∀n ≥ 4
i=0

El orden complejidad es O(3n )

5.5.7. Ejemplo 7

En el Cuadro 109 se analiza otro algoritmo recursivo.

Cuadro 109: Ejemplo recursivo7

public int recursivo7( int n )


{
if ( n <= 1 )
{
return 1;
}
else
{
for( i = 1 ; i <= n ; i++ )
{
j = j + 1;
}
return ( recursivo7(n - 1) + recursivo7(n - 1) );
}
}
5.5. RESOLVER RECURRENCIAS POR INDUCCIÓN 139

En el caso base se tiene que para el peor de los casos el tiempo de


ejecución es: T (1) = 3. Para el caso inductivo se tiene que en el else, se
cuenta con un ciclo for el cual para el peor de los casos es del orden O(n),
más 2 llamadas recursivas, cada una de (n−1), por lo tanto el caso inductivo
es: T (n) = 2T (n − 1) + O(n).

Generalizando tenemos, que el caso base y el caso inductivo, quedan


expresados de la siguiente manera:

T (1) = O(1)
T (n) = T (n − 1) + T (n − 1) + O(n)

Reemplazando por constantes tenemos que:

T (1) = a
T (n) = 2T (n − 1) + bn

Reemplazando la base en la inducción, tenemos:

T (1) = a
T (2) = 2T (1) + 2b ⇒ T (2) = 2a + 2b
T (3) = 2T (2) + 4b ⇒ T (3) = 2(2a + 2b) + 3b = 4a + 7b
T (4) = 2T (3) + 4b ⇒ T (4) = 2(4a + 7b) + 4b = 8a + 14b + 4b = 8a + 18b
···

Generalizando, tenemos:

n−1
X
n−1
¡ n−1−i ¢
T (n) = 2T (n − 1) + bn ⇒ T (n) = 2 a+ 2 (i + 1) b + 2n−1 b
i=2

El orden complejidad de este algoritmo es O(2n ).


140 CAPÍTULO 5. RECURSIVIDAD

5.5.8. Ejemplo 8

Suponga que se tiene las siguiente función base y de inducción. ¿Cuál


es su orden de complejidad?

T (1) = O(1)
T (n) = 4T (n/2) + 2n2

Reemplazando por constantes se tiene:

T (1) = O(1) = a
T (n) = 4T (n/2) + bn2

T (2) = 4T (1) + bn2 ⇒ T (2) = 4a + 4b


2
T (4) = 4T (2) + bn ⇒ T (4) = 16a + 16b + 16b
2
T (8) = 4T (4) + bn ⇒ T (8) = 64a + 64b + 64b + 64b
2
T (16) = 4T (8) + bn ⇒ T (16) = 256a + 256b + 256b + 256b + 256b
···

Generalizando, tenemos:

T (n) = 4T (n/2) + bn2 ⇒ T (n) = n2 a + log2 (n) ∗ n2 b ∈ O(log2 (n) ∗ n2 )

Este ejemplo, permite observar que no siempre se puede determinar de


forma simple el orden de complejidad de los algoritmos. Esta estimación,
requiere bases matemáticas solidas para su deducción.

5.6. Resolver 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.
5.6. RESOLVER RECURRENCIAS POR SUSTITUCIÓN 141

5.6.1. Algoritmo Divisibles

Dado el algoritmo recursivo (Cuadro 110), y usando esta forma para


resolver recurrencias, podemos deducir su tiempo de ejecución y orden de su
complejidad. Este algoritmo retorna la cantidad de números divisibles tanto
por 2 y por 3.
Cuadro 110: Ejemplo divisble

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


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

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
142 CAPÍTULO 5. RECURSIVIDAD

= (T (n − 5) + c1 ) + 4c1 = T (n − 5) + 5c1
= (T (n − 6) + c1 ) + 5c1 = T (n − 6) + 6c1
···
= (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).

5.6.2. Recursividad exponencial

En el Cuadro 111 mostramos un algoritmo recursivo del cual se va a


deducir cual es su tiempo de ejecución y su orden de complejidad.
Cuadro 111: Ejemplo recursivo

public int recursivo( int n )


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

A continuación se muestra otra forma de encontrar el orden de comple-


jidad de algún algoritmo recursivo.

T (n) = 1 , si n ≤ 1
2T (n − 1) + 1 , si n > 1

T (n) = 2 ∗ T (n − 1) + 1
= 2 ∗ (2 ∗ T (n − 2) + 1) + 1 = 22 ∗ T (n − 2) + (22 − 1)
···
= 2k ∗ T (n − k) + (2k + 1)

Para k = n − 1, T (n) = 2n−1 , T (1) + 2n−1 − 1 y por lo tanto T (n) es O(2n )


5.6. RESOLVER RECURRENCIAS POR SUSTITUCIÓN 143

5.6.3. Ejemplo recursivo logarı́tmico

En el Cuadro 112 mostramos un algoritmo recursivo del cual se va a


deducir cual es su tiempo de ejecución y su orden de complejidad.
Cuadro 112: Ejemplo recursivo1

public int recursivo1( int n )


{
if ( n <= 1 )
{
return 1;
}
else
{
return ( 2 * recursivo1( n / 2 ) );
}
}

A continuación se muestra otra relación de recurrencia que permite en-


contrar 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
= T (n/22 ) + 1
= T (n/23 ) + 1
= T (n/24 ) + 1
= T (n/2k ) + n

n/2k = 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)).

5.6.4. Recursividad cuadrática

En el Cuadro 113 mostramos un algoritmo recursivo del cual se va a


deducir cual es su tiempo de ejecución y su orden de complejidad.

T (n) = 1, si n ≤ 1
T (n − 1) + n, si n > 1
144 CAPÍTULO 5. RECURSIVIDAD

Cuadro 113: Ejemplo recursiv02

public int recursivo2 ( int n, int b )


{
if ( n <= 1)
{
return 1;
}
else
{
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
···
k−1
X
= T (n − k) + (n − i)
i=0

Si k = n − 1,
n−2
X n−2
X n−2
X
T (n) = T (1)+ (n−i) = 1+( (n)+ (i)) = 1+n(n−1)−(n−2)(n−1)/2.
i=0 i=0 i=0

Por lo tanto este algoritmo recursivo tiene un orden de complejidad


O(n2 )

5.7. Recurrencias suponiendo una solución


Otra forma de resolver relaciones de recurrencia es suponer una solución.
Este método no ofrece una solución exacta de T (n), pero ofrece una solución
cercana que puede llegar a ser satisfactoria.
5.8. CONCLUSIONES 145

El Cuadro 5.2, generaliza las soluciones más comunes que aplican a las
funciones recursivas. Se asume para todos los casos que la ecuación base es
T (1) = a y que k ≥ 0. Para todos los casos bnk es reemplazado por cualquier
polinomio de grado k [17].

Ecuación Inductiva T(n)


T (n) = T (n − 1) + bnk O(nk+1 )
T (n) = cT (n − 1) + bnk , para c > 1 O(cn )
T (n) = cT (n/d) + bnk , para c > dk O(n log(n)c)
T (n) = cT (n/d) + bnk , para c < dk O(nk )
T (n) = cT (n/d) + bnk , para c = dk O(nk log(n))

Cuadro 5.2: Soluciones más comunes par las funciones recursivas

5.8. Conclusiones

La conclusión evidente es que los algoritmos recursivos son apropiados


para problemas recursivos. Escribir estos algoritmos para algunos proble-
mas que no necesitan la recursividad puede traer malas consecuencias y las
soluciones no siempre son las esperadas.

Siempre se debe diseñar un método recursivo de forma que se garantice


su terminación en algún punto, siempre con llamadas recursivas a instancias
“menores” del problema, y manejando las instancias “mı́nimas” en forma
no recursiva como casos especiales.

Se pueden considerar elementos en favor de la recursividad tales como


que son a menudo intuitivos y mas elegantes. En algunas ocasiones una
solución recursiva puede resultar en una algoritmo muy rápido.

Como aspectos en contra de la recursividad, se utiliza mucha memoria


en su ejecución y en algunas ocasiones no es tan rápida como la versión
iterativa del algoritmo.

La estimación del orden de complejidad de los algoritmos recursivos, se


puede obtener mediante diferentes técnicas. Cada una de ella requiere de
un procedimientos, pero aplicada correctamente, permite establecer cotas
superiores de este tipo de algoritmos.
146 CAPÍTULO 5. RECURSIVIDAD

5.9. Autoevaluación

5.9.1. Algoritmos recursivos

1. Dado el siguiente algoritmo recursivo (Ver Cuadro 114) , verifique que


este determina si un número es perfecto o no lo es. Los valores de los
parámetros son: aux = 1 e i = 2.

Cuadro 114: Ejercicio 1

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


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

2. Dado el siguiente algoritmo recursivo (Ver Cuadro 115), verifique que


este retorna la suma de los elementos de un arreglo unidimensional de
enteros.
Cuadro 115: Ejercicio 2

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


{
if(i==matriz.length)
{
return 0;
}
if(j==matriz.length)
{
j = 0;
return sumaMatriz(matriz,++i,j);
}
return sumaMatriz(matriz,i,++j)+ matriz[i][j];
}
5.9. AUTOEVALUACIÓN 147

3. Dado el siguiente algoritmo recursivo (Ver Cuadro 116), describa su


funcionamiento.

Cuadro 116: Ejercicio 3

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


{
if (j<0)
{
i=i-1;
j=matriz.length-1;
}
if (i<0)
return 0;
return matriz[i][j]+sumarNumeros(matriz, i, --j);
}

4. Dado el siguiente algoritmo recursivo (Ver Cuadro 117), verifique que


este determina si una palabra es palindroma o no. La variable i = 0 y
j = pal.length() − 1

Cuadro 117: Ejercicio 4

public boolean palindroma(String pal, int i, int j)


{
if(pal.charAt(i)!=pal.charAt(j))
{
return false;
}
if(i==j)
{
return true;
}
if(j==i+1 && pal.charAt(i)==pal.charAt(j))
{
return true;
}
else
{
return palindroma(pal,i+1,j-1);
}
}
148 CAPÍTULO 5. RECURSIVIDAD

5. Dado el siguiente algoritmo recursivo (Ver Cuadro 118), describa su


funcionamiento y diga que problema resuelve.
Cuadro 118: Ejercicio 5

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)
{
return matrizSimetrica(matriz,i+1,0);
}
else
{
return matrizSimetrica(matriz,i,j+1);
}
}
else
{
return false;
}
}

6. Dado el siguiente algoritmo recursivo (Ver Cuadro 119), investigar que


problema resuelve. (pista: Investigar triángulo de Pascal)

Cuadro 119: Ejercicio 6

public int coef(int m,int n)


{
if(n==0||m==n)
{
return 1;
}
return coef(m-1,n)+coef(m-1,n-1);
}
5.9. AUTOEVALUACIÓN 149

7. Dado el siguiente algoritmo recursivo (Ver Cuadro 120), investigar que


problema resuelve.

Cuadro 120: Ejercicio 7

public int misterio( int x, int y )


{
if ( y == 0 )
{
return 1;
}
else
{
if ( y == 1 )
{
return x;
}
else
{
if ( y % 2 == 0 )
{
return misterio( x * x, y / 2 );
}
else
{
return misterio( x * x, y / 2 ) * x;
}
}
}
}

5.9.2. Implementaciones de recursión

1. El Newton-Raphson es un método eficaz de encontrar raı́ces de ecua-


ciones. Se usa en las bibliotecas numéricas para computar muchas fun-
ciones normales. Investigar en que consiste el método e implementar
el método de Newton-Raphson utilizando un algoritmo recursivo.

2. Escribir un programa que permita ingresar un texto y luego lo reescriba


eliminando los comentarios, entendiendo por comentario todo aquel
texto que aparece entre paréntesis.
Ası́ al ingresar el siguiente texto: Juan (el mejor jugador) entra como
titular el dı́a domingo.
150 CAPÍTULO 5. RECURSIVIDAD

Se debe reescribir: Juan entra como titular el dı́a domingo.


Debe tenerse en cuenta que los comentarios pueden estar .anidados”,
como por ejemplo en el siguiente texto:
Carlos (el hermano de Juan (mi mejor amigo) que regresa a la Ar-
gentina el dı́a 20) irá a esperarlo al aeropuerto.
En este caso, el programa debe producir la siguiente ”salida”: Carlos
irá a esperarlo al aeropuerto.

3. Escriba un algoritmo que dado un arreglo bidimensional de números


enteros positivos, retorne la cantidad de elementos divisibles por cinco.

4. Implemente un algoritmo que reciba por parámetro una lista de en-


teros y utilizando un método recursivo, devuelva la lista de enteros de
enteros de forma inversa a la lista original (al revés).

5. Escriba un algoritmo recursivo que muestre la cantidad de números


primos existentes en un arreglo bidimensional de enteros.

6. Escriba un algoritmo recursivo que sume los elementos de la diagonal


secundaria de un arreglo bidimensional de enteros.

5.9.3. Tiempo de ejecución

1. Dado el siguiente algoritmo recursivo del Cuadro 121, hallar su tiempo


de ejecución para el peor caso.
Cuadro 121: Ejercicio 8

public int MCD ( int a, int b )


{
if ( b == 0 )
{
return a;
}
else
{
return MCD ( b, a % b );
}
}
5.9. AUTOEVALUACIÓN 151

2. Dado el siguiente algoritmo recursivo del Cuadro 122, hallar su tiempo


de ejecución para el peor caso.

Cuadro 122: Ejercicio 9

public int medio ( int a, int b )


{
if ( a == 1 )
{
return 1;
}
else
{
if ( b == 2 )
{
return a;
}
else
{
return medio ( a, b - 2 ) * a;
}
}
}

3. Dado el siguiente algoritmo recursivo del Cuadro 123, hallar su tiempo


de ejecución para el peor caso.

Cuadro 123: Ejercicio 10

public int misterio ( int n )


{
if( n < 5 )
{
return 1;
}
if( n < 10 )
{
return n;
}
else
{
return misterio ( n / 10 ) + n / 10;
}
}
152 CAPÍTULO 5. RECURSIVIDAD

4. Dado el siguiente algoritmo recursivo (Cuadro 124), hallar su tiempo


de ejecución, siempre para el peor caso. Explique que esta realizando
este algoritmo. Los parametros que recibe este método respectivamente
son: el arreglo de elementos enteros, inicio = 0 y f in que toma el
tamaño del arreglo-1.
Cuadro 124: Ejercicio 11

public int misterio(int vector[],int inicio,int fin)


{
if(inicio == fin)
{
if(vector[inicio]>0)
{
return 1;
}
else
{
return 0;
}
}
else
{
if(vector[inicio]>0)
{
return numerosPositivos(vector,inicio+1,fin);
}
else
{
return 0;
}
}
}

5. Dado el siguiente algoritmo recursivo, explique cual es la solución que


este genera. En este método, se observa como el método esPar, una
vez que analiza su caso base, invoca al métodoesImpar. Para este tipo
de recursión, un método llama al otro y viceversa hasta llegar a la
solución. Este recursión, se llama recursión indirecta. Este algoritmo
se observa en el Cuadro 125.
5.9. AUTOEVALUACIÓN 153

Cuadro 125: Ejercicio 12

public boolean esPar ( int n )


{
if ( n == 0 )
{
return true;
}
else
{
return esImpar( n - 1 );
}
}

public boolean esImpar ( int n )


{
if ( n == 0 )
{
return false;
}
else
{
return esPar( n - 1 );
}
}

5.9.4. Orden de complejidad

1. Dado el siguiente algoritmo recursivo, hallar su orden de complejidad.


Ver Cuadro 126
Cuadro 126: Ejercicio 13

public int razon ( int a, int b )


{
if ( b == 0 )
{
return a;
}

return razon ( b, a - 1 );
}
154 CAPÍTULO 5. RECURSIVIDAD

2. Dado el siguiente algoritmo recursivo (Cuadro 127), hallar orden de


complejidad. (Algoritmo recursivo números de Lucas)

Cuadro 127: Ejercicio 14

public int lucas( int n )


{
if ( n == 0 )
{
return 2;
}
else
{
if ( n == 1 )
{
return 1;
}
else
{
if( n == 2 )
{
return 3;
}
else
{
return lucas( n - 2 ) + lucas ( n - 1 );
}
}
}
}

3. Dado el siguiente algoritmo recursivo, hallar su orden de complejidad.


Ver Cuadro 128
Cuadro 128: Ejercicio 13

public int razon ( int n, int m )


{
if ( m == 0 )
{
return a;
}
return razon ( b, n/2 ) + razon ( b, n/2 );
}
5.9. AUTOEVALUACIÓN 155

4. Dado el siguiente algoritmo recursivo, hallar su orden de complejidad.


Ver Cuadro 129
Cuadro 129: Ejercicio 14

public int medio ( int a, int b )


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

5. Dado el siguiente algoritmo recursivo, hallar su tiempo de ejecución,


siempre para el peor caso. Ver Cuadro 130

Cuadro 130: Ejercicio 15

public int cifras ( int n )


{
if ( n < 10 )
{
return n;
}
else
{
return cifras ( n / 5 ) + n % 5;
}
}

6. Dado el siguiente algoritmo recursivo, hallar su tiempo de ejecución,


siempre para el peor caso. Adicionalmente describa su funcionamiento
y determine que problema resuelve. Ver Cuadro 131
156 CAPÍTULO 5. RECURSIVIDAD

Cuadro 131: Ejercicio 16

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


{

if (n==0 || n==1)
{
arreglo[n] = 1;
return arreglo[n];
}
else
{
arreglo[n] = misterio(n-1,arreglo) * n;
return arreglo[n];
}
}

7. Dado el siguiente algoritmo recursivo, hallar su tiempo de ejecución,


siempre para el peor caso. Adicionalmente describa su funcionamiento
y determine que problema resuelve. Ver Cuadro 132
Cuadro 132: Ejercicio 17

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


{
if (n==0)
{
arreglo[n]=0;
return(arreglo[n]);
}
if (n==1 || n==2)
{
arreglo[n]=1;
return(arreglo[n]);
}
else
{
arreglo[n]= fibo(n-1,arreglo)+fibo(n-2,arreglo);
return(arreglo[n]);
}
}
5.9. AUTOEVALUACIÓN 157

8. Dado el siguiente algoritmo recursivo, hallar su tiempo de ejecución,


siempre para el peor caso. Adicionalmente, investigue si resuelve el
problema de los coeficientes binomiales y determine si el siguiente al-
goritmo lo resuelve. Ver Cuadro 133

Cuadro 133: Ejercicio 17

public int binomial (int n, int k)


{
if(n>k)
{
if(k==n || k==0)
{
return 1;
}
else
{
return binomial(n-1, k-1) + binomial(n-1, k);
}
}
else
{
return 0;
}
}

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.

Por ejemplo si se calcula

C(5,3)= C(4,2)+C(4,3) =
C(3,1)+C(3,2)+C(3,2)+C(3,3)=
C(2,0)+C(2,1)+C(2,1)+C(2,2)+C(2,1)+C(2,2)+C(2,2)+C(2,3)=
1+3C(2,1)+3C(2,2)+C(1,2)+C(1,3)=
1+3(C(1,0)+C(1,1))+3(C(1,1)+C(1,2))+C(1,2)+C(0,2)+C(0,3)=
1+3C(1,0)+3C(1,1)+3C(1,1)+3C(1,2)+C(1,2)+C(0,2)+C(0,3)=
1+3+6C(1,1)+4C(1,2)+C(0,2)+C(0,3)=
1+3+6(C(0,0)+C(0,1))+4(C(0,1)+C(0,2))+C(0,2)+C(0,3)=
1+3+6+10C(0,1)+5C(0,2)+C(0,3)=
10+10C(-1,0)+10C(-1,1)+5C(-1,1)+5C(-1,2)+C(-1,2)+C(-1,3)=10
158 CAPÍTULO 5. RECURSIVIDAD

5.9.5. Lecturas Complementarias


a) Leer el artı́culo: Evaluating the output of recursive routines using
successive queues. James L. Boettler, Dr. Department of Mathe-
matics and Computer Science, South Carolina State University,
Orangeburg, SC. ACM.
b) Leer el artı́culo: On classifying recursive algorithms. L. Carl Lein-
bach y Alex L. Wijesinha. Gettysburg College, Gettysburg, PA.
ACM.
c) Leer el artı́culo: Visual representations for recursive algorithms.
Linda Stern y Lee Naish. The University of Melbourne, Mel-
bourne, Victoria, Australia. ACM.
d ) Leer el artı́culo: Stack size reduction of recursive programs. Stefan
Schaeckeler y Weijia Shang Santa Clara University. ACM.
e) Leer el artı́culo: Computer science students’difficulties with proofs
by induction: an exploratory study. Irene Polycarpou Florida In-
ternational University, University Park, Miami, FL. ACM.
f ) Leer el artı́culo: Teaching and viewing recursion as delegation.
Jeffrey Edgington University of Denver, Denver, Colorado. ACM.
Ordenamiento
6
6.1. Introducción
El objetivo de los algoritmos de ordenamiento es organizar una secuen-
cia de datos, sea cual sea su tipo. Este tema ha sido ampliamente estu-
diado y aplicado en diferentes contextos de las ciencias de la computaió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 de-
ducción de sus órdenes de complejidad.
Suponga que se tiene una lista de enteros (3, 6, 9, 4, 2, 4, 2, 1, 2). Se
ordena la lista original por medio de una permutación en la siguiente lista
(1, 2, 2, 3, 4, 4, 6, 9). Observe que ordenamos para este ejemplo la lista en
orden ascendente de acuerdo a los valores de la lista original. Es importante
aclarar que se debe tener en cuenta que en la lista ordenada aparecen la
misma cantidad de elementos de la lista original, ası́ estos valores estén
repetidos.
Algunos de ejemplos de aplicación de estos algoritmos pueden ser:
Lista ordenada por apellido de estudiantes del programa de Ingenierı́a
de Sistemas.
Un directorio telefónico.
Un reporte de las ventas totales para cada uno de los meses del año.
A continuación se muestran algoritmos clásicos de ordenamiento:
160 CAPÍTULO 6. ORDENAMIENTO

6.2. Método de la Burbuja

Este método consiste en la comparación por parejas adyacentes e in-


tercambiarlas de acuerdo al orden que desee darse. Este proceso se repite
hasta que el conjunto de datos está completamente ordenado. Se llama bur-
buja, pues consiste en flotar hasta el final del arreglo el elemento mayor
en cada interación. Las siguientes figuras, muestran como es la forma de
funcionamiento de este método de ordenamiento.

El siguiente es el arreglo que se desea ordenar, ver Figura 6.1.

Figura 6.1: burbuja 1

Inicialmente, se compara el primer elemento con el segundo elemento


del arreglo. (Ver Figura 6.2)

Figura 6.2: burbuja 2

Como el primer elemento es mayor al segundo, se realiza el intercambio.


(Ver Figura 6.3)

Figura 6.3: burbuja 3

Se continua comparando el segundo elemento con el tercer elemento del


arreglo. (Ver Figura 6.4).

Figura 6.4: burbuja 4


6.2. MÉTODO DE LA BURBUJA 161

Como en este caso no se cumple la condición, se incrementa el ı́ndice


i, y se compara el tercer elemento con el cuarto elemento del arreglo. (Ver
Figura 6.5).

Figura 6.5: burbuja 5

Como el número 91 es mayor que el 9, se intercambian estos valores.


(Ver Figura 6.6)

Figura 6.6: burbuja 6

Al finalizar en su totalidad la primera iteración del ciclo mas interno, el


elemento 91 queda al final del arreglo. ( Ver Figura 6.7)

Figura 6.7: burbuja 7

En este punto, se tiene que el mayor elemento dentro del arreglo, se


encuentra en la última posición del mismo. Se repite nuevamente las com-
paraciones de los elementos del arreglo de forma similar como se realizo en
la Figura 6.2).

Las iteraciones se dan como se muestran en la Figura 6.8.

Figura 6.8: burbuja 8

Como se puede apreciar en la Figura 6.8, el arreglo ya tiene ordenado


los elementos asecendentemente. Las iteraciones 3, 4, 5 y 6 poseen el arreglo
162 CAPÍTULO 6. ORDENAMIENTO

ya ordenado. Estas 5 iteraciones de más, consumen tanto memoria y tiempo


de ejecución al recorrer 5 veces más el arreglo y generando un intervalo de
tiempo igual a 5n o en otro caso de h ∗ n donde h es el número de iteraciones
en el que el arreglo ya se encuentra ordenado. Este método de ordenamiento
es considerado uno de las formas mas ineficientes de resolver el ordenamiento
de elementos.

A continuación se muestra la implementación del método de la búrbuja.


(Ver Cuadro 134).

Cuadro 134: burbuja

public void burbuja( int arreglo[] )


{
int temp;
for( int j = 1 ; j < arreglo.length ; j++ )
{
for( int i = 0 ; i < arreglo.length - 1 ; i++ )
{
if ( arreglo[ i ] > arreglo[i + 1] )
{
temp = arreglo[ i ];
arreglo[ i ] = arreglo[i + 1];
arreglo[i+1] = temp;
}
}
}
}

El tiempo de ejecución del método de ordenamiento de la burbuja es:


T (n) = 8n2 − 12n + 9. El orden de complejidad de este método de orde-
namiento, en el peor caso, es de O(n2 ).

6.3. Método de la burbuja bidireccional

Este método es una mejora al método de la burbuja. La idea básica 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 se trasladan los elementos más pequeños hacia la parte
de incio del arreglo, almacenando en una variable la posición del último
elemento intercambiado.
6.3. MÉTODO DE LA BURBUJA BIDIRECCIONAL 163

En la segunda etapa se trasladan los elementos más grandes hacia la


parte final del arreglo, almacenando en otra variable la posición del últi-
mo elemento intercambiado. Las siguientes pasadas trabajan con los com-
ponentes del arreglo comprendidos entre las posiciones almacenadas en las
variables. El algoritmo termina cuando en una etapa no se producen inter-
cambios, o bien cuando el contenido de la variable que almacena el extremo
de arriba del arreglo es mayor que el contenido de la variable que almacena
el extremo de abajo.
A continuación se muestra el arreglo que se desea ordenar, este se puede
ver en la Figura 6.9.

Figura 6.9: bidireccional 1

El algoritmo se ejecuta en varias etapas: en la primera etapa, en cada


iteración se mueven los elementos de abajo hacia arriba, comparando ele-
mentos adyacentes. La comparación se realiza por pares. Si el elemento a la
izquierda es mayor al de la derecha se realiza el cambio. (Ver Figura 6.10).
Como el elemento de la izquierda no es mayor al elemento de la derecha,
entonces no se realiza ningún cambio. Se decrementa el valor de la i.

Figura 6.10: bidireccional 2

Al decrementar el valor de la variable i, los elementos que se van a


comparar se muestran en la Figura 6.11).

Figura 6.11: bidireccional 3

El elemento x[i − 1] es mayor que el elemento x[i], entonces se debe


realizar un intercambio, el cual al ejecutarse genera la siguiente configuración
del arreglo. (Ver Figura 6.12). Se decrementa en uno la variable i.
164 CAPÍTULO 6. ORDENAMIENTO

Figura 6.12: bidireccional 4

A continuación se realiza nuevamente la comparación y se encuentra que


el elemento de la izquierda no es mayor que el de la derecha, entonces no
se realiza ningún cambio y se decrementa en uno la variable i. (Ver Figura
6.13)

Figura 6.13: bidireccional 5

Continua comparando posiciones adyacentes y cada vez que se cumpla


la condición, se realiza un intercambio hasta ubicar el elemento menor en la
primera posición del arreglo. De acuerdo a lo anterior, la configuración del
arreglo al terminar estas iteraciones se observa en la Figura 6.14.

Figura 6.14: bidireccional 6

La primera fase consistió en bajar a la primera posición el elemento


menor del arreglo. La siguiente fase consiste en empezar desde el subarre-
glo que aún no ha sido ordenado. La Figura 6.15, muestra desde donde se
empieza la iteración para el segundo ciclo.

Figura 6.15: bidireccional 7

Esta vez los elementos mayores “flotan” hacia la derecha, se “sacude”


el arreglo a la derecha. Al final de esta iteración el arreglo queda como se
6.3. MÉTODO DE LA BURBUJA BIDIRECCIONAL 165

muestra en la Figura 6.16, donde el elemento mayor del arreglo en la última


posición corresponde al número 40.

Figura 6.16: bidireccional 8

Se continúa realizando el ordenamiento, pero ahora solo son tenidos en


cuenta los elementos que se encuentran en el rango de la forma punteada.
(Ver Figura 6.17)

Figura 6.17: bidireccional 9

La siguiente iteración completa del ciclo permitirá encontrar el menor


elemento dentro del rango de valores que esta punteado en la Figura 6.17. Se
compara nuevamente el elemento x[i − 1] con el elemento x[i]. (Ver Figura
6.18).

Figura 6.18: bidireccional 10

En esta primera fase, se sacude el elemento más pequeño hasta el inicio


del subarreglo, y este elemento se ubicará al inicio de este rango. (Ver Figura
6.19)

Figura 6.19: bidireccional 11


166 CAPÍTULO 6. ORDENAMIENTO

En la segunda fase. se sacude el elemento más grande hasta el final del


subarreglo, pero teniendo en cuenta únicamente el rango dado por la zona
punteada. (Ver Figura 6.20)

Figura 6.20: bidireccional 12

El subarreglo ya se encuentra ordenado, pues la comparación es con


el operador relacional (>), sin embargo el algoritmo continua ejecutandose
hasta el final de sus iteraciones. Se puede concluir que en cada iteración que
tanto el menor elemento como el mayor elemento, se van ubicando en la
primera y ultima posición respectivamente, de acuerdo al rango dado por el
subarreglo. La siguiente figura, permite entender este concepto. (Ver Figura
6.21)

Figura 6.21: bidireccional 13

A continuación, se muestra la implementación del método de la burbuja


bidireccional.(Ver Cuadro 135). El número de elementos comparados en la
primera etapa es (n − 1) + (n − 2), en la segunda fase es (n − 3) + (n − 4),
y ası́ sucesivamente. Sumando todas las fases se obtiene (suma desde i =
0 hasta n − 1), i = n(n−1)
2 . El orden de complejidad de este método de
ordenamiento, en el peor caso, es de orden O(n2 ).
6.4. ORDENAMIENTO POR SELECCIÓN 167

Cuadro 135: burbuja bidireccional

public void burbujaBidi( int arreglo[ ] )


{
int i, temp;
int inicio=1, fin = arreglo.length-1, estado = arreglo.length-1;

while ( fin >= inicio )


{
i = fin;
while(i >= inicio)
{
if ( arreglo [i - 1] > arreglo[ i ] )
{
temp = arreglo[i - 1];
arreglo[i - 1] = arreglo[ i ];
arreglo[ i ] = temp;
estado = i;
}
i--;
}
inicio = estado + 1;

i = inicio;
while(i <= fin)
{
if ( arreglo [i - 1] > arreglo[ i ] )
{
temp = arreglo[i - 1];
arreglo[i - 1] = arreglo[ i ];
arreglo[ i ] = temp;
estado = i;
}
i++;
}
}
}

6.4. Ordenamiento por selección

Este método de selección tiene este nombre puesto que la manera de rea-
lizar el ordenamiento es seleccionando en cada iteración el menor elemento
del arreglo e intercambiarlo en la posición correspondiente. Inicialemte se en-
cuentra el menor elemento dentro del arreglo y se intercambia con el primer
168 CAPÍTULO 6. ORDENAMIENTO

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.
Ası́ sucesivamente se busca cada elemento de acuerdo a su ı́ndice i y se in-
tercambia con el elemento que tiene el ı́ndice k. Se ordenarán los elementos
del arreglo de forma asecendente.

El siguiente arreglo, servirá para entender este método de ordenamiento.


(Ver Figura 6.22)

Figura 6.22: seleccion 1

Este método de ordenamiento inicialmente define la variable i = 0, que


servira para ubicar el menor elemento que se encuentra dentro del arreglo.

Por lo anterior entonces se empieza comparando cada uno de los elemen-


tos del arreglo, desde la posición inicial hasta la posición final. Al terminar
cada interación, se identificará la posición del elemento menor del arreglo, y
se intercambiara con el elemento que se encuentra en la primera posición.

El menor elemento dentro de este arreglo es el número 0, y se intercam-


biara con el elemento 111. (Ver Figura 6.23)

Figura 6.23: seleccion 2

Se incrementa la variable i = 1, para poder insertar el próximo número


mas pequeño dentro del arreglo. En este punto se garantiza en el algoritmo
que el menor valor se encuentra en la primera posición del arreglo. (Ver
Figura 6.24)
6.4. ORDENAMIENTO POR SELECCIÓN 169

Figura 6.24: seleccion 3

Se incrementa la variable i = 2, para poder insertar el proximo número


mas pequeño dentro del arreglo. (Ver Figura 6.25)

Figura 6.25: seleccion 4

Se incrementa la variable i = 3, para poder insertar el proximo número


mas pequeño dentro del arreglo. (Ver Figura 6.26)

Figura 6.26: seleccion 5

Se incrementa la variable i = 4, para poder insertar el proximo número


mas pequeño dentro del arreglo. (Ver Figura 6.27)

En este punto el arreglo ya se encuentra ordenado, pero todavia queda


pendiente una iteración.
170 CAPÍTULO 6. ORDENAMIENTO

Figura 6.27: seleccion 6

El tiempo de ejecución de este método es O(n2 ) para el peor caso.

A continuación, se muestra la implementación para el método de orde-


namiento de selección. (Ver Cuadro 136)

Cuadro 136: Selección

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


{
if (arreglo [j] < menor )
{
menor = arreglo [j];
k = j;
}
}
arreglo [k] = arreglo[i];
arreglo [i] = menor;
i++;
}
}
6.5. MÉTODO DE INSERCIÓN 171

6.5. Método de 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. (Ver Cuadro 137). Se considera un buen algoritmo cuando
son pocos los elementos a ordenar [18].
Cuadro 137: inserción

public void insertionSort( int arreglo[] )


{
int i, llave;

for ( int j = 1 ; j < arreglo.length ; j++ )


{
llave = arreglo[ j ];
i = j - 1;

while( i >= 0 && arreglo[ i ] > llave )


{
arreglo[i + 1] = arreglo[ i ];
i--;
}

arreglo[i + 1] = llave;
}
}

En la Figura 6.28 se muestra el arreglo que se desea ordenar por medio


del método de inserción:

Figura 6.28: inserción 1

Se tienen la variable j, la cual se desplazará a lo largo del resto del


arreglo. En la primera iteración j = 1, se almacena en la variable llave el
valor que se encuentra en la posición arreglo[1]. La variable i toma el valor
i = 0, en la comparación del while se cumplen ambas condiciones y en la
posición arreglo[1] se almacena el valor de arreglo[0]. Se decrementa el valor
de i quedando i = −1. Al volver al ciclo no se cumple la primer condición y
172 CAPÍTULO 6. ORDENAMIENTO

se asigna el valor almacenado en la variable llave en la primera posición del


arreglo, es decir, se realizo un intercambio. (Ver Figura 6.29).

Figura 6.29: inserción 2

Se incrementa la variable j = 2, la variable llave toma el valor existente


en la posición 2. La variable i toma el valor de 1, en el while se verifica que
1 >= 0 y que arreglo[1] > llave, por lo tanto arreglo[i + 1] almacena el
valor 111. Se decrementa el valor de i y queda i = 0, en el while se verifica
que 0 >= 0 y que arreglo[0] > llave, por lo tanto arreglo[i + 1] almacena
el valor 20. La variable i queda en i = −1, no se puede entrar al cuerpo del
ciclo while, en la posición 0, se inserta el menor elemento para este caso el
valor 10.(Ver Figura 6.30)

Figura 6.30: inserción 3

Tercera iteración j = 3, la variable llave=91 y la variable i = 2. Se


compara la condición del while: 2 >= 0 y arreglo[2] > llave, como se
cumple la condición entonces el 111 se ubica en la posición arreglo[3]. Se
decrementa la variable i a i = 1, se vuelve a comparar pero en este caso el
valor 20 no es mayor que 91, no se entra al cuerpo del ciclo, y se ubica la
llave en la posicion arreglo[i + 1], es decir arreglo[2] = 91.

Se garantiza entonces que todos los elementos anteriores ya se encuen-


tran ordenados. (Ver Figura6.31)
6.5. MÉTODO DE INSERCIÓN 173

Figura 6.31: inserción 4

Cuarta iteración j = 4, la variable llave=0 y la variable i = 3. En este


caso se puede observar que la variable llave = 0, es el valor mas pequeño den-
tro del arreglo y por lo tanto al momento de hacer la comparación con este
elemento se realiza el intercambio de los elementos a la posición arreglo[i+1].
Finalmente el elemento 0 queda en la primera posición del arreglo. (Ver Figu-
ra 6.32)

Figura 6.32: inserción 5

Quinta iteración j = 5, la variable llave=9 y la variable i = 4. Se


repiten los pasos anteriormente descritos, en esta iteración el valor de la
variable llave se insertar en la segunda posición del arreglo. (Ver Figura
6.33)

Figura 6.33: inserción 6

Finalmente encontramos un arreglo ordenado ascendentemente. De acuer-


do al funcionamiento del insertionSort( ), podemos afirmar con base en
su funcionamiento que:
174 CAPÍTULO 6. ORDENAMIENTO

Se selecciona el primer elemento como subarreglo ya ordenado.

El siguiente elemento al subarreglo es el elemento llave a ordenar.

El subarreglo se comienza a desplazar hacia la derecha hasta encontrar


la posición en la que se puede ubicar al elemento llave.

Se ubica el elemento y éste ya hará parte del subarreglo.

Se continúa con las iteraciones hasta llegar al final del arreglo.

El análisis de complejidad para el algoritmo de ordenamiento inserción


es O(n2 ).

6.6. 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 [19].

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.

Al principio del proceso se escoge la secuencia de decrecimiento de incre-


mentos; el último valor debe ser 1. Cuando el incremento toma un valor de 1,
todos los elementos pasan a formar parte del subgrupo y se aplica inserción
directa. El método se basa en tomar como salto n2 (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.
6.6. SHELLSORT 175

El método Shell se basa generalmente en la idea de la ordenación por


inserción (la cual hace cambios adyacentes), pero haciendo intercambios le-
janos, consiguiendo de esta forma deshacer varios intercambios al mismo
tiempo. (Ver Cuadro 138). Esta implementación es de la autoria de Pat
Morin. 1
Cuadro 138: ShellSort

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 ];
a[ j ] = a[j+incr];
a[j+incr] = T;
j -= incr;
}
else
{
j = -1;
}
}
}
}
}

A continuación se muestra un ejemplo de cómo se realiza el ordenamien-


to por shellSort, el arreglo que se desea ordenar se puede ver en la Figura
6.34.

Figura 6.34: shellSort 1

1
PhD (Computer Science) from Carleton University (http://www.cccg.ca/ morin/)
176 CAPÍTULO 6. ORDENAMIENTO

En la primera iteración, se determina el valor de la variable que reali-


zará los incrementos: salto = 62 = 3. Por lo tanto, inicialmente se compararan
las posiciones 0 con 3, 1 con 4 y 2 con 5.

Estas posiciones estan dadas por las variables j y j + salto (Ver Figura
6.35).

También se utiliza una variable i que se inicializa en el mismo valor


de la variable salto, y esta variable i, nos permite controlar la cantidad de
comparaciones y controlar el limite superior del arreglo, evitando desbor-
damiento.

Figura 6.35: shellSort 2

Se comparan los valores dentro del arreglo en las posiciones anterior-


mente mencionadas, y se realiza intercambio en caso de cumplirse la condi-
ción arreglo[j] > arreglo[j + salto], también la variable j se vuelve negativa
y el while deja de ejecutarse.

En este punto cuando la variable i = 6, indicara que el ciclo for interno


termina su ejecución y se divide en dos la variable salto.

El arreglo una vez terminada esta primera iteración completa queda con
la siguiente configuración. (Ver Figura 6.36)

Figura 6.36: shellSort 3

Segunda iteración: salto = 32 = 1. La variable i toma el valor de salto.


La variable j, toma el valor de cero y nos indica que solo una vez se puede
hacer la comparación arreglo[j] > arreglo[j + salto]. Para este caso, se
compararán las posiciones arreglo[0] > arreglo[1], como puede apreciarse
en la Figura 6.37).
6.6. SHELLSORT 177

Como se cumple la comparación, se realiza el intercambio y la variable


j = −1, lo que termina el while. La variable i toma el valor de i = 2

Figura 6.37: shellSort 4

En este punto la variable i = 2, y por lo tanto la variable j toma


inicialmente el valor j = 1, lo que quiere decir que en el caso en el que la
comparación arreglo[j] > arreglo[j + salto] sea verdadera siempre en su
evaluación, el ciclo while permitira dos intercambios.

Primero se comparan los elementos de las posiciones arreglo[1] > arreglo[2],


como 92 es mayor que el 1, se realiza el intercambio.

Se decrementa en 1 la variable j, quedando j = 0, como la comparación


del while es verdadera 0 >= 0, entonces se comparan los elementos de las
posiciones arreglo[0] > arreglo[1], como 4 es mayor que 1, se realiza el
intercambio, quedando la siguiente configuración de elementos en el arreglo.
(Ver Figura 6.38).

La variable j, toma el valor j = −1, por lo tanto se deja de ejecutar el


ciclo while dado que −1 >= 0 resulta falso . La variable i se incrementa en
i = 3.

Figura 6.38: shellSort 5


178 CAPÍTULO 6. ORDENAMIENTO

La variable i = 3 y la variable j = 2, se van por lo tanto a comparar los


elementos de las posiciones arreglo[2] y arreglo[3], entonces se comparan los
valores 92 y 112, como no es verdadera esta comparación, entra al else que
hay dentro del while y asigna j = −1, termina el while y se incrementa la
variable i. En esta iteración, no se realizo intercambio alguno. (Ver Figura
6.39)

Figura 6.39: shellSort 6

Se incrementa la variable i a i = 4, y la variable j toma el valor j = 3. En


la comparación del ciclo while, se tiene que 3 >= 0, entonces se comparan
los elementos de las posiciones arreglo[3] > arreglo[4], como es verdadero,
se realiza el intercambio.

Se decrementa en 1 la variable j, quedando j = 2, como la comparación


del while es verdadera 2 >= 0, se comparan los elementos de las posiciones
arreglo[2] > arreglo[3], como es verdadero, se realiza el intercambio. Nueva-
mante se decrementa la variable j y su valor queda en j = 1, la comparación
del while es verdadera 1 >= 0 y se comparan los elementos de las posiciones
arreglo[1] > arreglo[2], como esta comparación es falsa, se entra al else del
ciclo y la variable j = −1.

El while no se ejecuta puesto que −1 >= 0 es falso. Queda la siguiente


configuración de elementos en el arreglo. (Ver Figura 6.40). Se incrementa
la variable i.

Figura 6.40: shellSort 7


6.6. SHELLSORT 179

Se incrementa la variable i a i = 5, y la variable j toma el valor j = 4. En


la comparación del ciclo while, se tiene que 4 >= 0, entonces se comparan
los elementos de las posiciones arreglo[4] > arreglo[5], como es verdadero,
se realiza el intercambio.

Se decrementa en 1 la variable j, quedando j = 3, como la comparación


del while es verdadera 3 >= 0, se comparan los elementos de las posiciones
arreglo[3] > arreglo[4], como es verdadero, se realiza el intercambio.

Nuevamente se decrementa la variable j y su valor queda en j = 2, la


comparación del while es verdadera 2 >= 0 y se comparan los elementos
de las posiciones arreglo[2] > arreglo[3], como es verdadero, se realiza el
intercambio. Se continua comparando desplazando el valor de j hacia atrás,
hasta encontrar un elemento que no sea menor.

La i llegará al final de arreglo y ya no será posible hacer más particiones


del arreglo, pues además éste ya quedó ordenado ascendentemente.

La siguiente configuración de elementos en el arreglo. (Ver Figura 6.41).


Finalmente el arreglo queda ordenado ascendentemente.

Figura 6.41: shellSort 8


180 CAPÍTULO 6. ORDENAMIENTO

6.7. 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. (Ver Cuadros 139,
140 y 141). Esta implementación es de la autoria de Pat Morin. 2

Cuadro 139: Ejemplo mergeSort

public void mergesort( int a[] )


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

Cuadro 140: Ejemplo mergeSort

public void mergesort( int a[], int bajo, int alto )


{
int len, pivote, m1, m2;
if ( bajo == alto )
{
return;
}
else
{
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];
}

2
PhD Computer Science - Carleton University). (http://www.cccg.ca/ morin/
6.7. MERGESORT 181

Cuadro 141: Ejemplo mergeSort

m1 = 0;
m2 = pivote - bajo + 1;

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

A continuación, se realizará un análisis del algoritmo recursivo mergeSort.

En el caso base no existe llamada recursiva. Cuando el primer if es


verdadero (el tamaño del arreglo a ordenar en este momento es 1), el
algorimo retorna, por lo tanto se tiene O(1).Entonces el caso base es
T (1) = O(1).

Las asignaciones del Cuadro 140 son asignaciones cuyo orden de com-
plejidad es O(1). Estas asignaciones representan el tamaño del arreglo
y un elemento pivote el cual almacena la mitad del tamaño del arreglo.

Se realiza un primer llamado recursivo el cual va desde el primer ele-


mento del arreglo hasta el elemento pivote calculado anteriormente.
182 CAPÍTULO 6. ORDENAMIENTO

Esta llamada recursiva va disminuyendo el conjunto de datos a la mi-


tad.

Se realiza un segundo llamado recursivo el cual va desde el pivote + 1


hasta el final del arreglo. Esta llamada recursiva va disminuyendo el
conjunto de datos a la mitad. De acuerdo a lo analizado, se tiene que
se realizan dos llamadas recursivas cada una de ellas a la mitad, por
lo tanto se tiene que 2 ∗ T ( n2 ).

Se tiene un orden de complejidad O(n), se está utilizando un arreglo


auxiliar el cual almacena la misma informacion de los elementos del
arreglo que se recibió por parámetro.

En el Cuadro 141 es en donde se realiza el ordenamiento, las com-


paraciones se realizan en el arreglo auxiliar, pero finalmente el arreglo
ordenado queda en el arreglo que se recibio por parámetro. El orden
de complejidad comprendido entre estas lı́neas es de O(n).

De acuerdo al anterior análisis, 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.

Utilizando el reemplazo de la base en la inducción, se tiene que:

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 + n log2 (n)b
El término an es de orden O(n) y el término n log2 (n)b es de orden
O(n log2 (n)). Se observa que la función orden n log(n) crece más rápida-
mente que la función n, por lo tanto es correcto decir que la función n es de
orden O(n log2 (n)).
6.8. QUICKSORT 183

Figura 6.42: MergeSort

La Figura 6.42 , permite un mejor entendimiento del funcionamiento el


método de ordenamiento MergeSort.

A continuación, se muestra la forma en la que se van realizando los


ordenamientos. Esto corresponde a cada una de las llamadas recursivas. Ver
Figura 6.43.

Figura 6.43: MergeSort

6.8. QuickSort

En general este método implica la selección de un elemento de una lista


y a continuación reordenar todos los elementos restantes de la sublista, de
tal modo que todos los elementos que son más pequeños que el elemento
dado, se ponen en una sublista, y todos los elementos que son más grandes
184 CAPÍTULO 6. ORDENAMIENTO

que el elemento dado, se ponen en una segunda lista. Por lo tanto el método
elige un elemento denominado pivote y pone en una lista o arreglo, todos los
elementos mayores que el pivote y los elementos menores en otra lista. 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. (Cuadro 142).

Cuadro 142: Ejemplo 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--;
}
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 ); }
}
6.8. QUICKSORT 185

Para nuestro caso tenemos que el funcionamiento del método de orde-


namiento se puede describir ası́:
Si el número de elementos de la lista es 0 ó 1, el algoritmo quickSort
termina.
Después se debe entrar a partir la lista, esto se realiza cuando se es-
tablece el elemento pivote x al valor de un elemento arbitrario que se
encuentra en la lista.
Se debe empezar a recorrer la lista, por la sublista izquierda, y se
empieza a buscar un valor ≥ x. Este recorrido se cumple mientras sea
la condición verdadera.
Se debe empezar a recorrer la lista, por la sublista izquierda, y se
empieza a buscar un valor ≤ x. Este recorrido se cumple mientras sea
la condición verdadera.
Si los valores están desordenados, entonces se intercambian sus valores,
esto se realiza recursivamente hasta terminar de recorrer la lista.
De acuerdo al anterior análisis, se tienen las siguientes expresiones de
base y de inducción.
T (1) = a
³n´
T (n) = 2T + O(n).
2
Reemplazando en el caso inductivo por una constante, tenemos que:
T (1) = a
³n´
T (n) = 2T + bn.
2
Utilizando el reemplazo de la base en la inducción, se tiene que:
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 + n log2 (n)b
El término an es de orden O(n) y el término bn es de orden log2 (n) es
de orden O(n log2 (n)). Se observa que la función n log(n) crece más rápida-
mente que la función n, por lo tanto es correcto decir que la función n es de
orden O(n log2 (n)).
186 CAPÍTULO 6. ORDENAMIENTO

6.9. StoogeSort

Se trata de un algoritmo recursivo que realiza una partición en tres


llamadas recursivas para llevar a cabo el ordenamiento. (Ver Cuadro 143).

Cuadro 143: Ejemplo stoogeSort

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

Este algoritmo de ordenamiento recursivo posee un orden de compleji-


log(3) 3
dad de O(n2,7 ). El valor exponencial exacto es log(1,5) .

3
Sanchit Karve
6.10. RADIXSORT 187

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 actual, entonces:

Ordena los dos tercios iniciales de la lista.


Ordena los dos tercios finales de la lista.
Ordena los dos tercios iniciales de la lista nuevamente.

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 algoritmo fue propuesto por los profesores Howard, Fine, Besser.
Este es un algoritmo de Dividir y conquistar. El caso general para el algo-
ritmo dividir y conquistar tiene el siguiente principio base.
Principio Base: El arreglo se clasifica en pedazos de 23 de los elemen-
tos totales (primer 32 , último 23 , primer 32 ) y el tamaño del arreglo que es
calificado se trae abajo a dos elementos recursivamente.

6.10. RadixSort
El método de radix sort es un método eficiente de ordenamiento, que en
su esencia no lleva a cabo comparaciones de posiciones adyacentes dentro
de una arreglo. Esta solución propuesta se compone de tres clases de imple-
mentación. La clase radixSort que contiene la logica del problema, la clase
cola enlazada y la clase nodo. Cada una de ellas se describira más a fondo.
A continuación se muestra un ejemplo de cómo se realiza el ordenamien-
to por medio del radix sort, el arreglo que se desea ordenar se puede ver en
la Figura 6.44.

Figura 6.44: radixsort 1

En el Cuadro 144, se puede observar que se declara un arreglo de colas,


en las cuales se almacena cero, uno o mas nodos que componen la cola
enlazada. Se tienen 10 colas para insetar elementos de acuerdo a un dı́gito
determinado. Cada una de estas colas almacenará los datos de acuerdo a
un radical, entendiendo como radical un número entero que se calculará. Se
tiene por lo tanto una cola para cada uno de los 10 dı́gitos.
188 CAPÍTULO 6. ORDENAMIENTO

Cuadro 144: Ejemplo radixSort

public class RadixSort {

ColaEnlazada[] Q =
{
new ColaEnlazada(), // Radical 0
new ColaEnlazada(), // Radical 1
new ColaEnlazada(), // Radical 2
new ColaEnlazada(), // Radical 3
new ColaEnlazada(), // Radical 4
new ColaEnlazada(), // Radical 5
new ColaEnlazada(), // Radical 6
new ColaEnlazada(), // Radical 7
new ColaEnlazada(), // Radical 8
new ColaEnlazada() // Radical 9
};

La Figura 6.45, permite entender la forma en la cual se representa este


arreglo de colas enlazadas. En cada una de ellas, se almacenara uno o mas
dı́gitos, dependiendo de su radical.

Figura 6.45: radixsort 2

El método sort(int a[] ) recibe el arreglo que contiene todos los elementos
que se desean ordenar. Este método tiene como responsabilidad, encontrar
el máximo elemento dentro del arreglo, y posteriormente determinar la can-
tidad de digitos que tiene este elemento mayor. (Ver Cuadro 145). El ciclo
f or de este método permite identificar el elemento mayor del arreglo.

La instrucción numeroDigitos = cadena.length(), permite obtener la


cantidad de dı́gitos del número mayor del arreglo. Posteriormente, se realiza
una invocación al método sort(a, numeroDigitos), al cual se le envian el
mismo arreglo y la cantidad de digitos del elemento mas grande del arreglo.
6.10. RADIXSORT 189

Cuadro 145: Ejemplo radixSort

void sort(int a[])


{
int mayor = 0, numeroDigitos;
String cadena = " ";
for (int i = 0; i < a.length; i++)
{
if (mayor < a[i])
{
mayor = a[i];
}
}
Integer maximo = new Integer(mayor);
cadena = String.valueOf(maximo);
numeroDigitos = cadena.length();
sort(a, numeroDigitos);
}
}

El método sort(int a[], int numeroDigitos), es responsable tanto de in-


gresar los elementos a la cola, como eliminar elementos de la cola. La ins-
trucción Q[obtenerRadical(a[j], i)].encolar(a[j]), es la encargada de insertar
los elementos del arreglo int a[], estos elementos se insertan de acuerdo al
radical. El segundo f or es el encargado de sacar los elementos de la cola, la
instrucción a[posArreglo] = Q[j].decolar(), los saca de la cola y los vuelve
a ingresar en la posición posArreglo.

En sı́ntesis, el método pone valores en la cola de acuerdo al radical,


luego, el dı́gito menos significante primero,luego el segundo y asi sucesiva-
mente. Después, se reune las colas y las coloca de nuevo en el arreglo, estas
colas contienen listas parcialmente ordenadas despues de la primera pasa-
da, por último, moviendo al siguiente dı́gito significativo se conservará el
ordenamiento.

A continuación, se muestra la implementación del método sort(int a[],


int numeroDigitos), se sume que todos los números son enteros positivos,
numeroDigitos es el número de digitos del número más grande. (Ver Cuadro
146). Desde esta método, se observa que para encolar o decolar los elementos
de la cola, es necesario la implementación de los mismos, Ver Cuadros 149
y 150.
190 CAPÍTULO 6. ORDENAMIENTO

Cuadro 146: Ejemplo radixSort

void sort(int[] a, int numeroDigitos)


{
int posArreglo;

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


{
posArreglo = 0;
for (int j = 0; j < a.length; j++)
{
Q[obtenerRadical(a[j], i)].encolar(a[j]);
}

for (int j = 0; j < Q.length; j++)


{
while (!Q[j].estaVacia())
{
a[posArreglo] = Q[j].decolar();
posArreglo++;
}
}
}
}

El método sort(int[] a, int numeroDigitos), tiene en su primer ciclo for()


más anidado, un orden de complejidad O(n), dado que se debe recorrer la
totalidad de arreglo, asumiendo que se tienen n elementos en el arreglo. El
método sort(int[] a, int numeroDigitos), tiene en su segundo ciclo for() más
anidado, un orden de complejidad O(1), dado que se definieron 10 colas en
total.

El ciclo while para el peor caso, se asume que se construye una lista de
n elementos para cada una de las colas. Por lo tanto, por la regla de la suma,
se tiene que el orden de complejidad para este ciclo es O(n) + O(1). Para el
análisis del ciclo for mas externo, asumiendo que el tamaño del número mas
grande del arreglo es n, el orden de complejidad del método es n, supuesto
que no es práctico en la vida real.

Para efectos reales y aplicables, el número de dı́gitos de los números que


utilizamos es constante y por lo tanto el orden de complejidad del método
sort( int[] a, int numeroDigitos) es O(n) * numeroDigitos.
6.10. RADIXSORT 191

El método obtenerRadical(int numero, int radical), retorna el radical de


un número dado. Por ejemplo el segundo radical de 79981 es 8, el primer
radical de 79981 es 1, el tercer radical de 4643 es 6. (Ver Cuadro 147).

Cuadro 147: Ejemplo radixSort

public static int obtenerRadical(int numero, int radical)


{
return (int)(numero / Math.pow(10, radical - 1)) % 10;
}

La clase ColaEnlazada posee 4 métodos: el constructor ColaEnlazada(),


el método encolar(int num), el cual inserta un elemento a la cola de acuerdo
a su respectivo ı́ndice, el método decolar(), el cual retorna el valor a ser
insertado en el arreglo, y finalmente el método estaVacia, que verifica si la
cola contiene o no nodos. Todos lo métodos de la clase ColaEnlazada tienen
un orden de complejidad O(1).

A continuación en el Cuadro 148, se muestra la implementación de la


clase ColaEnlazada, la cual tiene en su implementación los métodos para
crear, insertar y eliminar un elemento dentro de la cola. El constructor de
la cola tiene dos referencias a la misma y el tamano indica la cantidad de
elementos que esta posee en cada una.
Cuadro 148: Cola Enlazada

public class ColaEnlazada


{
nodoEntero inicio;
nodoEntero fin;
int tamano;

ColaEnlazada()
{
inicio = null;
fin = null;
tamano = 0;
}

El método encolar de la clase ColaEnlazada, adiciona un item al final de


la cola. (Ver Cuadro 149). Al momento de realizar esta operación, se incre-
menta el número de objetos, luego se crea un nuevo nodo con la informacion
dada. Si es la primera vez que el condicional if es verdadero, se crea un
192 CAPÍTULO 6. ORDENAMIENTO

nodo y este queda como el primero de la cola, en caso contrario se adiciona


este elemento al final de la cola.
Cuadro 149: Cola Enlazada

public void encolar(int num) {

tamano++;
nodoEntero temp = new nodoEntero(num);

if (inicio == null)
{
inicio = temp;
fin = inicio;
}
else
{
fin.siguiente = temp;
fin = temp;
}
temp = null;
}

El método decolar retorna el entero en el nodo que se encuentra en el


frente de la cola. Asume que la cola no esta vacia. (VerCuadro 150). Al final
de la ejecución del mismo, se elimina el nodo y su contenido, por lo tanto se
elimina de la cola.
Cuadro 150: Cola Enlazada

public int decolar() {

int temp;
tamano--;
temp = inicio.valor;
nodoEntero nodoTemp;
nodoTemp = inicio;
inicio = inicio.siguiente;
nodoTemp = null;

return temp;
}
6.10. RADIXSORT 193

El método estaVacia() retorna verdadero si la cola esta vacı́a, falso en


caso contrario. Ver Cuadro151.

Cuadro 151: Cola Enlazada

public boolean estaVacia()


{
return (tamano == 0);
}
} // Fin de clase

La clase nodoEntero es necesaria para la contrucción de cada uno de los


nodos de la clase ColaEnlazada. La clase nodoEntero, posee un constructor
que permite la creación del mismo, el cual tiene un nodo que posee una
referencia al nodo llamada siguiente y un valor que se puede asignar al
nodo. El orden de complejidad de los métodos de la clase nodoEntero, son
de orden de complejidad O(1). (Ver Cuadro 152).

Cuadro 152: Cola Enlazada

public class nodoEntero


{

public int valor;


public nodoEntero siguiente, anterior;

nodoEntero(int a)
{
valor = a;
siguiente = null;
anterior = null;
}

nodoEntero()
{
siguiente = null;
anterior = null;
valor = 0;
}
}
194 CAPÍTULO 6. ORDENAMIENTO

6.11. Conclusiones

De acuerdo al análisis de los algoritmos de ordenamiento, la siguiente


tabla muestra el orden de complejidad de cada uno de ellos considerando su
peor caso. Ver Cuadro 6.1.

M etodo Orden
Burbuja O(n2 )
Seleccion O(n2 )
Inserción O(n2 )
ShellSort O(n2 )
Sharker O(n2 )
MergeSort n log(n)
QuickSort O(n log(n))
RadixSort O(n)
StoogrSort O(n2,7 )

Cuadro 6.1: Ordenes de Complejidad

Los métodos de ordenamiento se pueden clasificar en diferentes cate-


gorı́as. Cada una de ellas tiene formas diferentes para resolver el problema
de el ordenamientos, las mas comunes son las siguientes:

Se tienen los métodos de ordenamiento por inserción en los cuales los


elementos que se van a ordenar son considerados uno a la vez. Cada uno
de estos elementos deberá ser insertado en una posición con respecto
a los elementos que ya se encuentran ordenados.

Los métodos de ordenamiento por selección en los cuales se selecciona


el elemento menor de todos los elementos y se pone en la posición
adecuada.

Los métodos de ordenamiento por intecambio en los cuales se reali-


zan comparaciones entre dos elementos y se intecambian en caso de
cumplirse una determinada condición.

6.12. Autoevaluación

6.12.1. Algoritmos de ordenamiento

Suponga que se tiene una lista de enteros (3, 5, 3, 23, 7, 1, -3).


6.12. AUTOEVALUACIÓN 195

1. Dibuje paso a paso el ordenamiento de esta lista usando:

a) InsertionSort
b) ShellSort
c) ShakerSort
d ) BubbleSort
e) StoogeSort
f ) MergeSort
g) QuickSort

2. Construya una tabla en donde se muestren cuántos pasos realizó cada


método de ordenamiento para ordenar la lista del punto anterior y
escriba una conclusión.

3. Dado el siguiente método de ordenamiento, explique cual es su fun-


cionamiento y determine su orden de complejidad. Ver Cuadro 153.
Cuadro 153: gnomeSort

public int[] gnomeSort(int a[])


{
int i = 1, aux= 0;
while(i < a.length)
{
if (i == 0 || a [i-1] <= a [i])
{
i++;
}
else
{
aux = a [i - 1];
a[i - 1] = a[i];
a[i] = aux ;
i --;
}
}
return a;
}
196 CAPÍTULO 6. ORDENAMIENTO

4. Dado el siguiente método de ordenamiento, explique cual es su fun-


cionamiento y determine su orden de complejidad. La implementación
de este método se observa en los Cuadros 154 y 155. Esta imple-
mentación es de la autoria de Mary E. Ramı́rez Cano 4 .
Cuadro 154: Heap

public void Heap(int []a)


{
int i;
int[]A = Asigna_longitud_arreglo(a,a.length);
Construir_heap(A);

for (i=a.length; i>1 ; i--)


{
a[i-1]=A[0];
A[0]=A[i-1];
con++;
//A = Asigna_longitud_arreglo(A,A.length-1);
Organiza_elementos(A,1);
}
a[0]=A[0];
}

public int[] Asigna_longitud_arreglo(int a[], int longitud)


{
int [] Temporal = new int[longitud];
for (int i=0; i<longitud ; i++)
{
Temporal[i]=a[i];
}
return Temporal;
}

public void Construir_heap(int[] a)


{

for (int i=a.length/2 ; i>0 ; i--)


{
Organiza_elementos(a,i);
}
}

4
Magister En Ciencias Computacionales
6.12. AUTOEVALUACIÓN 197

Cuadro 155: Heap

public void Organiza_elementos(int[] a, int papa)


{
int I = 2*papa;
int D = 2*papa+1;
int mayor;
int temp;

if (I <= a.length-con && a[I-1] > a[papa-1])


{
mayor = I;
}
else
{
mayor = papa;
}

if (D <= a.length-con && a[D-1] > a[mayor-1])


{
mayor = D;
}
if (mayor != papa)
{
temp = a[papa-1];
a[papa-1] = a[mayor-1];
a[mayor-1] =temp;
Organiza_elementos(a,mayor);
}
}

5. Investigue en que consiste el método de ordenamiento llamado buck-


etsort y escriba su implementación.

6.12.2. Lecturas Complementarias


a) Leer el artı́culo: A lower bound on the average-case complexity of
shellsort. Autores: Tao Jiang - Univ. of California, Ming Li - Univ.
of California y Paul Vitányi CWI, Amsterdam, The Netherlands.
ACM.
b) Leer el artı́culo: A sorting problem and its complexity. Autor: Ira
Pohl Univ. of California, Santa Cruz, Santa Cruz. ACM.
198 CAPÍTULO 6. ORDENAMIENTO

c) Leer el artı́culo: On generating worst-cases for the insertion sort.


Julie Tillison y Ching-Kuang Shene Department of Mathemat-
ics and Computer Science, Northern Michigan University, Mar-
quette, MI. ACM.
d ) Leer el artı́culo: A new sort algorithm: self-indexed sort. Sunny
Y. Wang Computing Laboratory, Oxford University, Oxford OXl
3QD, UK. ACM.
e) Leer el artı́culo: A transition from bubble to shell sort. Joseph B.
Klerlein y Curtis Fullbright Western Carolina Univ., Cullowhee,
NC. ACM.
f ) Leer el artı́culo: Bubble sort: an archaeological algorithmic anal-
ysis. Owen Astrachan - Duke University, Durham, NC. ACM.
g) Leer el artı́culo: Sorting in linear time - variations on the bucket
sort. Edward Corwin y Antonette Logar South Dakota School of
Mines and Technology, Rapid City, SD. ACM.
h) Leer el artı́culo: Best sorting algorithm for nearly sorted lists.
Curtis R. Cook Oregon State Univ., Corvallis Do Jin Kim Na-
tional Semi-Conductor, Sunnyvale, CA. ACM.
i ) Leer el artı́culo: Determination of order of an algorithm. S. O’Daniel.
G. Clark y K. Cooper. Department of Computer Science, Eastern
Kentucky University, Wallace 402, Richmond, KY. ACM.
Algoritmos de búsqueda
7
Los algoritmos de búsqueda tienen como objetivo fundamental el ubicar
un objeto dentro de un conjunto de datos existentes y determinar su ubi-
cació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 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 elemen-
tos dentro de la estructura de datos esten 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 ellos tendrá un análisis de su funcionamiento.
200 CAPÍTULO 7. ALGORITMOS DE BÚSQUEDA

7.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 vedadero 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 unidi-
mensional de elementos. Pero la búsqueda también puede ser extendida a
otros tipos de datos, ası́ como también a objetos.

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 Figura 7.1 muestra los números que se encuentran
dentro del arreglo.

Figura 7.1: Búsqueda lineal 1

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. (Ver
Figura 7.2)

Figura 7.2: Búsqueda lineal 2

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. (Ver Figura 7.3)

Figura 7.3: Búsqueda lineal 3


7.1. BÚSQUEDA LINEAL ITERATIVA 201

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


encuentra en la posición 2, como aún no se encuentra el elemento, continua
la búsqueda. (Ver Figura 7.4)

Figura 7.4: Búsqueda lineal 4

Continua la búsqueda hacia el cuarto elemento del arreglo, el cual se en-


cuentra 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 indicará que
la búsqueda fue satisfactoria. (Ver Figura 7.5)

Figura 7.5: Búsqueda lineal 5

Esta implementación tiene que aún encontrando el elemento dentro del


arreglo, se continua realizando las comparaciones que permite el tope supe-
rior del lı́mite del ciclo. (Ver Figura 7.6 y Ver Figura 7.7). Por lo anterior se
puede pensar que esta solución es suceptible de ser mejorada.

Figura 7.6: Búsqueda lineal 6

Figura 7.7: Búsqueda lineal 7


202 CAPÍTULO 7. ALGORITMOS DE BÚSQUEDA

En el Cuadro 156 se muestra la implementación iterativa del algoritmo


de búsqueda lineal. El método recibe por parámetro el arreglo y el dato que
se desea buscar.
Cuadro 156: Búsqueda Lineal Iterativa

boolean busquedaLinealIterativa ( int arreglo [], int dato )


{
boolean estado = false;

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


{
if ( arreglo [ i ] == dato )
{
estado = true;
}
}
return estado;
}

El análisis de la implementación de la búsqueda lineal es:

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

El condicional contiene una comparación del elemento en la posición


del arreglo y del elemento que se busca. El peor caso, será que esta
comparación sea verdadera en la última comparación, ó que nunca sea
verdadera.

En el caso que el elemento que se esta buscando este en las primeras


posiciones del arreglo, el orden de complejidad es O(1), pero esto nunca
sucede en esta implementación, el orden de complejidad de la búsqueda
lineal es O(n).

7.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
7.2. BÚSQUEDA LINEAL LIMITADA 203

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.

La siguiente secuencia permite entender mejor el funcionamiento de la


busqueda lineal limitada. Inicialmente se tiene un arreglo con 8 elementos,
y el elemento que se esta buscando es el que tiene el número 21. La Figura
7.8 muestra los número que tiene el arreglo.

Figura 7.8: Búsqueda lineal limitada 1

La búsqueda del número 21, empieza por el primer elemento del arreglo,
al momento de comparar sus valores, se encuentra que no son iguales. (Ver
Figura 7.9)

Figura 7.9: Búsqueda lineal limitada 2

Se pasa al segundo elemento del arreglo, el cual se encuentra en la posi-


ción 1, al compararse se tiene que no son iguales. (Ver Figura 7.10)

Figura 7.10: Búsqueda lineal limitada 3

Continua la búsqueda al tercer elemento del arreglo, el cual se encuentra


en la posición 2, como aún no se encuentra el elemento, continua la búsqueda.
(Ver Figura 7.11)

Figura 7.11: Búsqueda lineal limitada 4


204 CAPÍTULO 7. ALGORITMOS DE BÚSQUEDA

Continua la búsqueda hacia el cuarto elemento del arreglo, el cual se


encuentra en la posición 3, como al comparar estos elemento se encontró que
son iguales, la búsqueda termina. (Ver Figura 7.12)

Figura 7.12: Búsqueda lineal limitada 5

En el Cuadro 157 se muestra la implementación del método de búsqueda


lineal limitada.
Cuadro 157: Búsqueda Lineal limitada

boolean busquedaLimitada ( int arreglo [], int dato )


{
for( int i = 0 ; i < arreglo.length ; i++ )
{
if ( arreglo [ i ] == dato )
{
return true;
}
}
return false;
}

El análisis de complejidad de esta implementación de la búsqueda lineal


limitada es el siguiente:

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 se tiene
O(n).

Para el caso en el cual consideramos que el elementos que se esta


buscando esta en las primeras posiciones del arreglo, se tiene un orden
de complejidad O(1).

Este método por lo tanto se puede considerar más eficiente que el ante-
rior, dado que en caso de encontrarse el elemento no se realizan más itera-
ciones, retorna verdadero indicando que si esta el dato.
7.3. BÚSQUEDA LINEAL ITERATIVA CON EXTREMOS 205

7.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 esta buscando. Con estas
comparaciones se puede evitar realizar una comparación si los valores se
encuentran por fuera del rango de valores.

Inicialmente se tiene un arreglo con 8 elementos, y el elemento que se


esta buscando es el que tiene el número 4. La Figura 7.13 muestra los número
que tiene el arreglo.

Figura 7.13: Búsqueda lineal con extremos 1

Inicialmente se verifica que el 4 no sea menor que el primer elemento


del arreglo, y se verifica que el 4 no sea mayor que el último elemento del
arreglo. Como no se cumple ninguna de las anteriores condiciones, entonces
se procede a comparar los elementos dentro del arreglo, compara 4 con 1.
(Ver Figura 7.14)

Figura 7.14: Búsqueda lineal con extremos 2

Continua la búsqueda al segundo elemento del arreglo, se realiza la com-


paración y como aún no se encuentra el elemento, continua la búsqueda. (Ver
Figura 7.15)

El tercer elemento del arreglo, coincide con el elemento que se esta


buscando, por lo tanto la búsqueda termina. (Ver Figura 7.16)
206 CAPÍTULO 7. ALGORITMOS DE BÚSQUEDA

Figura 7.15: Búsqueda lineal con extremos 3

Figura 7.16: Búsqueda lineal con extremos 4

En el Cuadro 158 se muestra la implementación del método de búsqueda


lineal iterativa con extremos. Se observa que una vez que se encuentra el
dato, se rompe el ciclo y termina la ejecución del método.
Cuadro 158: Búsqueda Lineal con extremos

boolean busquedaLinealExtremos ( int arreglo [], int dato )


{
boolean estado = false;
int n = arreglo.length;

if (dato < arreglo[0])


{
return false;
}

if ( dato > arreglo[n-1])


{
return false;
}

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


{
if(arreglo[i] == dato)
{
estado = true;
break;
}
}
return estado;
}
7.4. BÚSQUEDA LINEAL RECURSIVA 207

El análisis de la búsqueda lineal con extremos es:

En el caso base se tienen dos condicionales que evaluan 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)

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 O(n). Lo anterior
asumiendo que el elemento se encuentra en las últimas posiciones del
arreglo, o que no se encuentre dentro de este.

7.4. Búsqueda lineal recursiva

Otra forma de resolver el problema de la búsqueda lineal es por medio


de una implementación recursiva. En el Cuadro 159 se muestra la solución
recursiva. Para esta implementación no es necesario que los elementos den-
tro del arreglo se encuentren ordenados de alguna forma predeterminada.

Cuadro 159: Búsqueda lineal recursivo

boolean busquedaLinealRecursivo ( int arreglo [], int dato )


{
return busquedaLinealRecursivo (arreglo, dato, arreglo.length-1 );
}
boolean busquedaLinealRecursivo ( int arreglo [], int dato, int pos )
{
if ( pos < 0 )
{
return false;
}
if ( arreglo [ pos ] == dato )
{
return true;
}
else
{
return busquedaLinealRecursivo ( arreglo, dato, pos - 1 );
}
}
208 CAPÍTULO 7. ALGORITMOS DE BÚSQUEDA

Parea el análisis de este algoritmo se tiene: el caso base tiene que ambos
condicionales solo tienen instrucciones constantes, por lo tanto el orden es
O(1). Cuando no se cumple las condiciones del caso base, se puede deducir
para el caso inductivo: T (n) = O(1) + T (n − 1),para n > 1. Lo anterior
debido a que la variable pos se decrementa de uno en uno. Se tiene por lo
tanto:

Caso Base: T (1) = a, Inducción: T (n) = b + T (n − 1),

Reemplazando la base en la inducción tenemos:

T (2) = b + T (1) = b + a
T (3) = b + T (2) = b + (b + a) = 2b + a
T (4) = b + T (3) = b + (2b + a) = 3b + a
T (5) = b + T (4) = b + (3b + a) = 4b + a
···
T (n) = b + T (n − 1) = (n − 1)b + a

Se puede deducir que T (n) = b + T (n − 1) = (n − 1)b + a, para todo


n ≥ 1. Esta generalización es equivalente a afirmar que el caso de inducción
es T (n) = nb + (a − b), evaluando esta función de tiempo de ejecución, se
tiene que nb es de orden O(n) y (a − b) es de orden O(1), por la regla de la
suma se deduce que T (n) es de orden O(n).

7.5. 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 tiene un arreglo con 8 elementos y se quiere buscar el


número 1. La Figura 7.17.

Figura 7.17: Búsqueda Binaria 1


7.5. BÚSQUEDA BINARIA 209

Figura 7.18: Búsqueda Binaria 2

Las posiciones en la cuales se realizará la búsqueda en el arreglo, son:


inferior, la cual determina la primera posición del arreglo, superior que de-
termina la última posición del arreglo y centro que determina el elemento
del centro del arreglo. (Ver Figura 7.18)

Se busca en la posición del centro del arreglo, si el elemento que se busca


está en esa posición, la búsqueda termina, si no, se compara si el elemento
que se busca es mayor o menor que el que se encuentra en el centro. (Ver
Figura 7.19)

Figura 7.19: Búsqueda Binaria 3

Como el número que se busca es menor, se redefine la posición superior


y centro dentro del arreglo. En caso de que en esta comparación el número
que se busca es mayor, se redefine la posición inferior (centro + 1). (Ver
Figura 7.20)

Figura 7.20: Búsqueda Binaria 4

Como el número que se busca todavı́a es menor, se redefine las posición


superior y centro dentro del arreglo. En este punto se tiene que tanto centro
inferior están en la misma posición; esto indica que ya no se redefinirá más
veces el arreglo. Como el elemento que se está buscando está en el arreglo,
la búsqueda binaria termina. (Ver Figura 7.21)
210 CAPÍTULO 7. ALGORITMOS DE BÚSQUEDA

Figura 7.21: Búsqueda Binaria 5

7.6. Búsqueda Binaria iterativa

En el Cuadro 160 se muestra la implementación de forma iterativa de


la búsqueda binaria.

En esta implementación, se han definido unos limites para el arreglo, el


lı́mite inferior que define la posición del primer elemento del arreglo, el lı́mite
superior que define la última posición del arreglo y el centro del arreglo.

Inicialmente se verifica si el elemento que se esta buscando se encuentra


en la mitad del arreglo, en caso de no ser este dato el que se esta buscando,
entonces se compara con el elemento que se esta buscando. Si el elemento que
se busca es mayor, se redefine el limite 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 estan por fuera del rango. El ciclo se deja de ejecutar cuando se retorna
un valor ya sea falso o verdadero.

7.7. Búsqueda Binaria Recursiva

En el Cuadro 161 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 éste se halle en uno de los extremos del arreglo.
7.7. BÚSQUEDA BINARIA RECURSIVA 211

Cuadro 160: Ejemplo búsqueda binaria iterativo

boolean busquedaBinariaIterativo ( int arreglo [], int dato )


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

while ( true )
{
centro = (limSup + limInf) / 2;

if( limInf > limSup )


{
return false;
}
else
{
if ( arreglo[ centro ] < dato )
{
limInf = centro + 1;
}
else
{
if ( arreglo [centro] > dato )
{
limSup = centro - 1;
}
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 n2 , n4 , . . ., 2nn , 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)).
212 CAPÍTULO 7. ALGORITMOS DE BÚSQUEDA

Cuadro 161: Ejemplo búsqueda binaria recursivo

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 );
}
else
{
if (arreglo [centro]<dato)
{
return binRecursiva( arreglo, dato, centro + 1, limSup );
}
else
{
return true;
}
}
}
}

7.8. Conclusiones

Los algoritmos de búsqueda son aquellos algoritmos diseñados para en-


contrar un elemento dentro de una secuencia determinada. Esta busqueda
se puede aplicar para diferentes estructuras de dados. En este capı́tulo, se
aplicaron diferentes algoritmos utilizando los arreglos unidimensionales.
7.9. AUTOEVALUACIÓN 213

Todos los algoritmos de búsqueda al terminar su ejecución, deben indicar


si un elemento predeterminado se encuentra o no dentro de la estructura, y
adicionalmente, podrı́a indicar en que parte de la misma se encuentra.

Para aplicar la búsqueda lineal no es necesario que los elementos dentro


de la estructura de datos se encuentren en un determinado orden, es decir, no
existe una precondición previa para su uso. Por el contrario, si se va aplicar
la busqueda binaria, se hace necesario garantizar que los elementos dentro
del arreglo se encuentren ordenados en un determinado orden, generalmente
en orden ascendente.

De acuerdo al análisis de los algoritmos de búsqueda, la siguiente tabla


muestra el orden de complejidad de cada uno de ellos considerando su peor
caso. Ver Cuadro 7.1.
M etodo Orden
Lineal iterativa O(n)
Lineal limitada O(n)
Lineal con extremos O(n)
Lineal recursiva O(n)
Binaria iterativa O(n log(n))
Binaria recursiva O(n log(n))

Cuadro 7.1: Ordenes de Complejidad

7.9. Autoevaluación

7.9.1. Algoritmos de búsqueda

1. Estimar el tiempo de ejecución para los algoritmos:

a) Búsqueda lineal iterativo.


b) Búsqueda lineal limitada .
c) Búsqueda binaria recursivo.

2. Suponga que se tiene una lista de enteros (3, 5, 13, 21, 73, 81, 93).
Dibuje paso a paso la búsqueda en esta lista de los números 81 y 75
usando:

a) Búsqueda lineal iterativo.


b) Búsqueda lineal recursivo.
214 CAPÍTULO 7. ALGORITMOS DE BÚSQUEDA

c) Búsqueda binaria iterativo.


d ) Búsqueda binaria recursivo.
3. Construya una tabla en donde se muestren cuántos pasos realizó en
cada método de búsqueda del punto anterior y escriba una conclusión.
4. Dado el siguiente algoritmo del el Cuadro 162, analice si es un algo-
ritmo correcto para realizar busquedas, en caso de serlo, explique la
forma de funcionamiento de dicho algoritmos.
Cuadro 162: Ejercicio de busqueda

public boolean busqueda ( int arreglo [], int dato )


{
boolean estado = false;
int n = arreglo.length,i;

if (dato < arreglo[0])


{
return false;
}
if ( dato > arreglo[n-1])
{
return false;
}
i = 0;
while (i < n && arreglo[i]<=dato)
{
if(arreglo[i] == dato){
estado = true;
break;
}
i++;
}
return estado;
}

7.9.2. Lecturas complemetarias


a) Leer el artı́culo: Comment on average binary search length. Peter
R. Jones. ACM.
b) Leer el artı́culo: Using binary search on a linked list. Firooz Khos-
raviyani. Department of Computer Science, University of Texas.
ACM.
Análisis de Estructura de datos
8
Las estructuras de datos dinámicas se constituyen de estructuras es-
peciales, compuestas por un número no fijo de elementos (conocidos como
nodos), cada uno almacenando la información de un elemento de datos e
indicando la posición de los otros nodos con los cuales se relaciona. Esta
situación hace que las estructuras puedan albergar un número indetermi-
nado de nodos, ya que es posible crear nuevos nodos durante la ejecución
de la aplicación y ampliar la colección de elementos. (La única limitación
en el tamaño será establecida por la cantidad de memoria disponible en el
computador en el cual se ejecuta la aplicación).

Las estructuras objeto de análisis serán en su orden:

Listas

Pilas

Colas

Comenzaremos con la estructura de datos dinámica lista.


216 CAPÍTULO 8. ANÁLISIS DE ESTRUCTURA DE DATOS

8.1. Listas

Las listas son las estructuras de datos lineales más comunes. 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 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 los cuales presentan
problemas en el diseño de algoritmos, y estos ocasionan con frecuencia erro-
res 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 la implementación de cada tipo de lista, se considerará el nodo
cabecera.

8.1.1. Lista sencillamente enlazada

Una lista enlazada también recibe el nombre de “lista concatenada -


lineal”. Una lista enlazada 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.
La Figura 8.1 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. El primer nodo de la lista se
llama nodo cabecera.

Figura 8.1: Lista

El cuadro 163 muestra la implementación de la clase N odo, cada nodo


se compone de un dato y una referencia al siguiente nodo de la lista. La
clase Nodo, tiene un método constructor llamado Nodo(Object dato), cuyo
orden de complejidad es O(1). Este método crea el nodo, el cual recibe un
elemento por parámetro de tipo Object.
8.1. LISTAS 217

Cuadro 163: Clase Nodo

public class Nodo


{
Object info;
Nodo siguiente;

public Nodo(Object elemento)


{
info = elemento;
siguiente = null;
}
}

A continuación se muestra una de muchas implementaciones para una


lista sencillamente enlazada, se analizará el orden de complejidad de cada
uno de los métodos de la clase.
Se tiene la clase, y la definición de dos referencias Nodo. Se define el
constructor de la clase listaSencilla el cual define un nodo cabecera. El nodo
cabecera, referenciará el primer nodo de la lista. El orden de complejidad
del mé todo constructor es O(1). Ver Cuadro 164

Cuadro 164: Lista sencilla 1

public class listaSencilla


{
int tama~
no;
Nodo cabecera,ultimo;

listaSencilla()
{
cabecera = new Nodo(null);
ultimo = cabecera;
tama~
no = 0;
}
// Metodos de la clase

El método estaVacia() indica si la lista esta o no vacı́a, se considera que


la lista esta vacı́a cuando en esta se encuentra únicamente el nodo cabecera.
Ver cuadro 165. El orden de complejidad del método estaVacia() es O(1).
El método tamaño(), retorna la cantidad de nodos que contiene la lista, su
orden es O(1).
218 CAPÍTULO 8. ANÁLISIS DE ESTRUCTURA DE DATOS

Cuadro 165: Lista sencilla 2

public boolean estaVacia()


{
if ( cabecera.siguiente==null)
{
return true;
}
else
{
return false;
}
}
public int tama~
no()
{
return tama~
no;
}

El método insertar permite adicionar un elemento a la lista. Ver cuadro


166. Se crea un nuevo nodo y en cualquier caso, el nodo se insertará al lado
del nodo cabecera, es decir independiente si la lista tiene o no nodos. Todas
las instrucciones son de orden constantes, incluyendo el metodo estaVacia().
El orden de compejidad es O(1).

Cuadro 166: Lista sencilla 3

public void insertar(Object elemento )


{
Nodo nuevo = new Nodo(elemento);
if( estaVacia())
{
cabecera.siguiente = nuevo;
ultimo=nuevo;
tama~
no++;
}
else
{
nuevo.siguiente=cabecera.siguiente;
cabecera.siguiente=nuevo;
tama~
no++;
}
}

El método eliminar permite suprimir un nodo de la lista. Verifica ini-


8.1. LISTAS 219

cialemte si la lista esta vacı́a, muestra un mensaje de advertencia indicando


que no se tiene ningún nodo dentro de la lista. En caso que la lista contenga
nodos, se verifica si solo se tiene un nodo, en caso de ser positivo la referencia
de cabecera.siguiente se asignará como nula y se borrará el nodo siguien-
te a cabecera. Finalmente si la lista contiene mas de un nodo, la referencia
siguiente se actualizará al segundo nodo de la lista, eliminando la referencia
al primer nodo de la lista. Ver cuadro 167. El orden de complejidad de este
método es O(1).

Cuadro 167: Lista sencilla 4

public void eliminar()


{
Nodo temp = cabecera.siguiente;

if(estaVacia())
{
JOptionPane.showMessageDialog(null,"Lista Vacı́a");
}
else
{
if(temp==ultimo)
{
cabecera.siguiente=null;
tama~
no--;
}
else
{
cabecera.siguiente=temp.siguiente;
tama~
no--;
}
}
}

Se debe aclarar al lector que existen muchos más métodos asociados a


las listas, en este libro, consideramos los más comunes, sin dejar de resaltar
la importancia de cada uno de ellos.

Es importante obtener la información de cada uno de los elementos de


la lista, el método imprimir(), es un método que permite obtener el valor
de cada uno de los nodos y posteriormente se muestra el contenido de cada
uno de ellos. El orden de complejidad es O(n), dado que necesita recorrer n
elementos de la lista para obtener la información. Ver Cuadro 168.
220 CAPÍTULO 8. ANÁLISIS DE ESTRUCTURA DE DATOS

Cuadro 168: Lista sencilla 5

public String imprimir()


{
Nodo movil = cabecera.siguiente;

Object info;
String cadena = "";

for(int i=0;i<tama~
no();i++ )
{
if(movil.info!=null)
{
info = movil.info;
cadena += info +" ";
movil = movil.siguiente;
}
}
return cadena;
}

8.1.2. Lista Sencilla Circular

Una lista sencilla circular es una colección de elementos llamados nodos,


organizados de tal manera que el último nodo de la lista apunta al nodo
cabecera. La Figura 8.2, muestra una lista circular. Se puede observar que
lista contiene un nodo cabecera, el cual nos sirve para referenciar el inicio
de la lista.

Figura 8.2: Lista 2

En el Cuadro 169, se muestra la definición de la clase listaCircular. Se


tiene el constructor, en el cual se define un nodo cabecera, se inicializa la
variable tamano = 0, la cual indicará la cantidad de nodos que tiene la
lista en deteminado momento. Finalmente, se tiene una variable ultimo de
tipo N odo que tendrá inicialmente una referencia al nodo cabecera. Esta
referencia ultimo se define, puesto que para este tipo de listas, se tiene un
método que permite insertar un nodo en la última posición de la lista.
8.1. LISTAS 221

Cuadro 169: Lista Circular

public class listaCircular


{
int tama~
no;
Nodo cabecera,ultimo;

listaCircular()
{
cabecera = new Nodo(null);
ultimo = cabecera;
tama~
no=0;
}

//Implementación de métodos

El método del Cuadro 170, inserta un nodo en la primera posición de la


lista circular. Tanto en el caso de estar o no la lista vacı́a. Las operaciones
que se realizan para llevar a cabo esta inserción son cambios de referencias
entre nodos. Cada una de estas instrucciones se consideran todas constantes.
Por lo anterior, el orden de complejidad de este método es O(1).

Cuadro 170: Lista Circular 1

public void insertarPrimero(Object elemento )


{
Nodo nuevo = new Nodo(elemento);

if( estaVacia())
{
cabecera.siguiente = nuevo;
nuevo.siguiente=cabecera;
ultimo=nuevo;
tama~
no++;
}
else
{
nuevo.siguiente=cabecera.siguiente;
cabecera.siguiente=nuevo;
tama~
no++;
}
}
222 CAPÍTULO 8. ANÁLISIS DE ESTRUCTURA DE DATOS

El método del Cuadro 171, elimina siempre el primer nodo de la lista,


las instrucciones de las cuales se compone este método son todas de orden
constante. El orden de complejidad del método es O(1).

Si la lista esta vacı́a, muestra un mensaje de advertencia indicando que


no se tiene ningún nodo dentro de la lista. En caso que la lista contenga un
solo nodo, la referencia de cabecera.siguiente se asignará como nula y se
borrará el nodo siguiente a cabecera. Por último, si la lista contiene más de
un nodo, la referencia siguiente se actualizará al segundo nodo de la lista,
eliminando la referencia al primer nodo de la lista.
Cuadro 171: Lista Circular 2

public void eliminar()


{

Nodo temp = cabecera.siguiente;

if(estaVacia())
{
JOptionPane.showMessageDialog(null,"Lista Vacia");
}
else
{
if(temp==ultimo)
{
cabecera.siguiente=null;
tama~
no--;
}
else
{
cabecera.siguiente=temp.siguiente;
tama~
no--;
}
}
}

En el Cuadro 172, se muestra la implementación del método insertarU ltimo.


En el constructor se tiene una referencia de la variable ultimo a el nodo
cabecera, lo que hace el método es actualizar la referencia ultimo al nuevo
nodo de la lista. Orden de complejidad O(1).
8.1. LISTAS 223

Cuadro 172: Lista Circular 3

public void insertarUltimo(Object elemento)


{
Nodo nuevo = new Nodo (elemento);
if(estaVacia())
{
ultimo.siguiente=nuevo;
nuevo.siguiente=ultimo;
ultimo= nuevo;
}
else
{
ultimo.siguiente=nuevo;
nuevo.siguiente=cabecera;
ultimo = nuevo;
}
tama~no++;
}

8.1.3. 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 refer-
encias 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 co-
mo 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 Figura 8.3, muestra una lista doblemente enlazada.

Figura 8.3: Lista Doblemente Enlazada

Para este tipo de lista es necesario definir la clase nodo. Se definen dos
variables de tipo Nodo que sirven como refencias para cada nodo. Estas
referencias son las que permiten que la lista se pueda recorrer de izquierda
a derecha, o que se pueda insertar un nodo tambien a la derecha o a la
224 CAPÍTULO 8. ANÁLISIS DE ESTRUCTURA DE DATOS

izquierda. La implementación de la clase N odo se puede ver en el cuadro


173.
Cuadro 173: Clase Nodo

public class Nodo


{
Object dato;
Nodo izquierda, derecha;

public Nodo(Object o)
{
izquierda = null;
derecha = null;
dato = o;
}
}

En el Cuadro 174, se tiene la definción de la clase listaDoble, en la cual


se define el constructor de la clase, el cual tiene dos variables de tipo N odo
que referencian al nodo cabecera. Estas referencias son importantes, porque
determinados métodos necesitan tener la referencia de un determinado nodo
para realizar su operación, por ejemplo, cuando se desea insertar un nodo a
a la derecha o insertar un nodo a la izquierda.
Cuadro 174: Lista Doble 1

public class listaDoble


{
int tama~
no;
Nodo cabecera,ultimo,actual;

listaDoble()
{
cabecera = new Nodo(null);
ultimo = cabecera;
actual = cabecera;
tama~
no = 0;
}
// Implementación de métodos

El método insertarDerecha, permite insertar un nodo a la derecha del


nodo que tiene en el momento la referencia llamada actual, la refencia actual
siempre me indicará cual fue el último nodo que fue insertado a la lista y
8.1. LISTAS 225

a partir de él, se podra realizar la correspondiente inserción. La referencia


actual se utiliza para saber siempre cual fue el último nodo insertado en la
lista. Ver Cuadro 175. El orden de complejidad de este método es O(1).

Cuadro 175: Lista Doble 2

public void insertarDerecha(Object elemento)


{
Nodo nuevo = new Nodo (elemento);
Nodo temporal = actual.derecha;

if(estaVacia())
{
actual.derecha=nuevo;
nuevo.izquierda=actual;
ultimo= nuevo;
actual = nuevo;
tama~no++;
}
else
{
if(actual == ultimo)
{
actual.derecha=nuevo;
nuevo.izquierda=actual;
actual = nuevo;
ultimo = nuevo;
tama~
no++;
}
else
{
nuevo.derecha=actual.derecha;
nuevo.izquierda=actual;
temporal.izquierda=nuevo;
actual.derecha=nuevo;
actual = nuevo;
tama~
no++;
}
}
}

El siguiente método permite insertar un nodo a la izquierda del nodo


que tiene en el momento la referencia actual, la referencia actual siempre
indicará cual fue el último nodo que fue insertado a la lista y a partir de él,
se podrá realizar la correspondiente inserción. Ver Cuadro 176.

El orden de complejidad de este método es O(1), dado que cada una de


226 CAPÍTULO 8. ANÁLISIS DE ESTRUCTURA DE DATOS

las instrucciones son constantes y además no se encuentra en su cuerpo de


implementación, ninguna clase de ciclo.

Cuadro 176: Lista Doble 3

public void insertarIzquierda(Object elemento)


{

Nodo nuevo = new Nodo (elemento);


Nodo temporal = actual.izquierda;

if(estaVacia())
{
actual.derecha=nuevo;
nuevo.izquierda=actual;
ultimo= nuevo;
actual = nuevo;
tama~no++;
}
else
{
if(actual == ultimo)
{
nuevo.derecha=actual;
nuevo.izquierda=temporal;
temporal.derecha = nuevo;
actual.izquierda = nuevo;
actual = nuevo;
tama~
no++;
}
else
{

nuevo.derecha=actual;
nuevo.izquierda=temporal;
temporal.derecha=nuevo;
actual.izquierda=nuevo;
actual = nuevo;
tama~
no++;
}
}
}

En el Cuadro 177, se tiene la implementación que permite insertar un


elemento al lado del nodo cabecera. Considera tanto el caso en el cual se
tienen nodos o no se tienen nodos. En ambas situaciones, se tienen operacio-
nes elementales cuyo orden de complejidad es O(1). Inicialmente si la lista
8.1. LISTAS 227

se encuentra vacı́a, se crea un nuevo nodo, el cual quedará a la derecha del


nodo cabecera, y el tamaño de la lista se incrementará en uno. Si ya hay
nodos en la lista, se actualizarán las referencias del primer nodo de la lista
y se insertará este nuevo nodo como el primero de la lista.
Cuadro 177: Lista Doble 4

public void insertarPrimero(Object elemento)


{
Nodo nuevo = new Nodo(elemento);
Nodo temporal = cabecera.derecha;

if (estaVacia())
{
cabecera.derecha=nuevo;
nuevo.izquierda=cabecera;
ultimo = nuevo;
actual = nuevo;
tama~
no++;
}
else
{
nuevo.derecha=cabecera.derecha;
nuevo.izquierda=cabecera;
temporal.izquierda=nuevo;
cabecera.derecha=nuevo;
actual = nuevo;
tama~
no++;
}
}

El método del Cuadro 178, muestra el método insertarU ltimo, dado que
se tiene una referencia ultimo, esta permitirá insertar un elemento al final
de la lista con un orden de complejidad O(1). En ningún caso es necesario
recorrer toda la lista para realizar esta inserción.

En caso que no se tuviera la referencia al último nodo de la lista, segu-


ramente, hay que recorrerla toda para encontrar el último nodo de la lista,
para esta situación, el orden de complejidad para insertar un elemento al
final de una lista doblemente enlazada es de O(n).
228 CAPÍTULO 8. ANÁLISIS DE ESTRUCTURA DE DATOS

Cuadro 178: Lista Doble 5

public void insertarUltimo(Object elemento)


{
Nodo nuevo = new Nodo (elemento);

if(estaVacia())
{
ultimo.derecha=nuevo;
nuevo.izquierda=ultimo;
ultimo= nuevo;
actual = nuevo;
tama~
no++;
}
else
{
ultimo.derecha=nuevo;
nuevo.izquierda=ultimo;
ultimo = nuevo;
actual = nuevo;
tama~
no++;
}
}

El método eliminarInicio(), (Ver Cuadro 179) permite eliminar el nodo


siguiente del nodo cabecera, es decir, el primer nodo de la lista, si la lista
se encuentra vacı́a, se muestra un mesaje indicando esta situación, de lo
contrario, se elimina el primer nodo de la lista.
Cada uno de los métodos implementados en esta sección, tienen un orden
de complejidad, es de aclarar al lector que esta no es la única implementación
existente para estos métodos. Cada lector como ejercicio práctico podrı́a
desarrollar sus propias soluciones y determinar a cada una de ellas su orden
de complejidad.
8.2. PILA 229

Cuadro 179: Lista Doble 6

public void eliminarInicio()


{
Nodo borrar = cabecera.derecha;

if(estaVacia())
{
JOptionPane.showMessageDialog(null,"Lista Vacia");
}
else
{
if(borrar==ultimo)
{
cabecera.derecha=null;
borrar.izquierda=null;
tama~
no--;
actual=cabecera;
ultimo=actual;
}
else
{
Nodo temporal=borrar.derecha;

cabecera.derecha=temporal;
temporal.izquierda=cabecera;
tama~
no--;
actual=cabecera.derecha;
}
}
}

8.2. Pila

La pila es una estructura de datos en la cual el último elemento que


es insertado, es el primero en ser eliminado. Por lo tanto, una pila es una
colección de elementos organizados en una secuencia donde unicamente el
primer elemento (llamado el tope de la pila) puede ser accesado. De acuerdo
a lo anterior, en una pila, los datos salen en orden inverso a la entrada, es
decir, el último en entrar es el primero en salir (Estructura LIFO).

Una pila no se puede recorrer, porque solo hay acceso a un elemento


de la pila, el que se encuentre en la primera posición. Para que sea posible
230 CAPÍTULO 8. ANÁLISIS DE ESTRUCTURA DE DATOS

atender otro elemento, es necesario que el primer elemento de la pila sea


eliminado de ella.
Las operaciones tradicionales asociadas a la pila son las siguientes:

push(e): inserta el elemento e en el tope de la pila


pop(): remueve el tope de la pila
top(): retorna el tope de la pila
isEmpty(): retorna si la pila esta vacia o no

Existen diferentes formas de implementar el concepto de pila. Las dos


siguientes secciones muestran la implementación de un pila por medio de
arreglos y por medio de listas.

8.2.1. Pilas mediante arreglos

Una forma de implementar el concepto de pila es utilizando como es-


tructura base un arreglo unidimensional de elementos. Los arreglos son es-
tructuras de datos donde todos los elementos están en un espacio contiguo
de memoria, y en donde los diferentes elementos de datos se hallan unos
junto a otros.
Cada elemento puede accederse de manera directa indicando el ı́ndice en
la cual se encuentra dentro del arreglo. Esta caracterı́stica hace que el manejo
de los arreglos resulte muy simple en sus operaciones y funcionamiento.
La principal desventaja de los arreglos es que su tamaño es fijo. Una vez
que sea compilado el programa o halla sido asignado el espacio de memoria,
el arreglo debe tener asignada memoria suficiente para albergar el mayor
número de elementos que la aplicación colocará allı́ en cualquier momento.
Esta situación ocasiona que muchos programas reserven más memoria
que la que realmente utilizan, debido a que provisionan para el caso con
más datos, o se quedan cortos en las reservas de memoria, cuando por algu-
na razón el programa requiere más datos que los contemplados durante la
construcción del programa.
En el Cuadro 180 se muestra la clase pila [20], y la declaración de las
variables y métodos constructores necesarios para la implementación de esta
estructura de datos. En ellos se define el tamaño del arreglo declarado como
constante.
8.2. PILA 231

Implementación basada en la desarrollada por Goodrich y Tamassia 1 .

Al considerar que los arreglos comienzan en java con el ı́ndice 0, se ini-


cializa tope en -1, y se usa este valor de tope para indicar cuando está vacı́a la
pila. De igual forma, se puede usar esta variable para determinar la cantidad
de elementos (tope+1) en una pila. Ver Figura 8.4.

Figura 8.4: Cola con arreglo

Cuadro 180: Métodos de pila

public class Stack


{
static final int TAMA~
NO = 10;
int capacidad;
Object Arreglo [];
int tope = -1, cantidad=0;

public Stack ()
{
this (TAMA~
NO);
}
public Stack (int cap)
{
capacidad = cap;
Arreglo = new Object [capacidad] ;
}

En el Cuadro 181 se muestra la implementación del método isEmpty, el


cual determina si la pila se encuentra o no vacı́a. La variable cantidad que es
la que controla la cantidad de elementos en la pila. La variable cantidad = 0
indica que no hay elementos, pero al momento de la inserción esta se va
incrementando. Orden de Complejidad O(1).

1
Estructuras de datos y algoritmos en JAVA, segunda edición
232 CAPÍTULO 8. ANÁLISIS DE ESTRUCTURA DE DATOS

Cuadro 181: Métodos de pila 1

public boolean isEmpty ()


{
if (cantidad==0)
{
return true;
}
else
{
return false;
}
}

En el Cuadro 182 se tiene el método push, el cual permite insertar un


elemento a la pila, inicialmente, se debe verificar si hay espacio disponible
para insertar el nuevo elemento. Orden de Complejidad O(1).

Cuadro 182: Métodos de pila 2

public void push (Object elemento )


{
if (size () == capacidad)
{
JOptionPane.showMessageDialog(null,"Pila llena ");
}
tope++;
Arreglo [tope] = elemento;
cantidad++;
}

El método top retorna (en caso de estarlo) el elemento que se encuentra


en el tope de la pila pero sin eliminarlo de esta. Si la pila se encuentra vacı́a,
es muestra un mensaje indicando esta situación. Ver Cuadro 183. El orden
de complejidad de este método es O(1).
8.2. PILA 233

Cuadro 183: Métodos de pila

public Object top ()


{
if (isEmpty())
{
JOptionPane.showMessageDialog(null,"Pila Vacı́a ");
}
return Arreglo[tope];
}

En el Cuadro 184 se muestra la implementación del método pop, el


retorna el elemento tope de la pila y lo elimina de esta. Si la pila se encuentra
vacı́a, el metodo envia un mensaje. El orden de complejidad de este método
es O(1).

Cuadro 184: Métodos de pila

public Object pop ()


{
Object elemento ;

if (isEmpty ())
{
JOptionPane.showMessageDialog(null,"Pila Vacı́a ");
}
elemento = Arreglo [tope];
Arreglo[tope] = null;
tope --;
cantidad--;
return elemento;
}

En el Cuadro 185 se tiene el método size, el cual retorna la cantidad de


elementos que tiene la pila. El orden de complejidad de este método es O(1).
Se debe tener en cuenta que la cantidad de elementos puede ser diferente a
la cantidad de posiciones definidas previamente para el arreglo.
234 CAPÍTULO 8. ANÁLISIS DE ESTRUCTURA DE DATOS

Cuadro 185: Métodos de pila

public int size ()


{
return cantidad;
}

8.2.2. Representación de pilas mediante listas

Una forma de implementar el concepto de pila es utilizando las listas,


en el cual una pila se compone de nodos en los cuales se pueden almacenar
referencias a objetos.

En el Cuadro 186 se muestra la clase pila, se define un método construc-


tor, en el cual se define un nodo tope que me referenciará en todo momento
la cima de la pila. Desde este elemento tope, será posible realizar operaciones
para insertar y para eliminar elementos de la misma.
Cuadro 186: Métodos de pila

public class pila


{
Nodo tope;

pila()
{
tope = new Nodo(null);
}

// implementación de métodos

En el Cuadro 187 se muestra la implementación del método isEmpty,


el cual determina si la pila se encuentra o no vacı́a. Si la referencia siguiente
del nodo tope, quiere decir que la pila se encuentra vacı́a, entonces el método
retornará verdadero. En caso contrario es falso. El orden de complejidad de
este método es O(1).
8.2. PILA 235

Cuadro 187: Métodos de pila

public boolean isEmpty()


{
if(tope.siguiente == null)
{
return true;
}
else
{
return false;
}
}

En el Cuadro 188 se tiene el método push, el cual permite insertar un el-


emento a la pila, inicialmente, se debe verificar si hay espacio disponible para
insertar el nuevo elemento. En todo caso, el elemento a insertar quedará ubi-
cado como siguiente al nodo cabecera. El orden de complejidad de este méto-
do es O(1).

Cuadro 188: Métodos de pila

public void push(Object elemento){

Nodo n = new Nodo(elemento);

if (tope.siguiente != null)
{
n.siguiente = tope.siguiente;
}
else
{
tope.siguiente = n;
}
}
236 CAPÍTULO 8. ANÁLISIS DE ESTRUCTURA DE DATOS

En el Cuadro189 se muestra la implementación del método top, el cual


retorna el valor del elemento tope de la pila, si la pila se encuentra vacı́a, el
método envia un mensaje de advertencia. El orden de complejidad de este
método es O(1).

Cuadro 189: Métodos de pila

public Object top()


{
Object dato = null;

if (tope.siguiente == null)
{
JOptionPane.showMessageDialog(null,"Pila Vacı́a ");

}
else{
dato = tope.siguiente.info;
}
return dato;
}

En el Cuadro 190 se muestra la implementación del método pop, el cual


elimina el elemento tope de la pila, si la pila se encuentra vacı́a, el método
envia una excepción.

Cuadro 190: Métodos de pila

public Object pop()


{
Object dato = null;

if (tope.siguiente == null)
{
JOptionPane.showMessageDialog(null,"Pila Vacı́a ");
}
else
{
dato = tope.siguiente.info;
tope.siguiente = tope.siguiente.siguiente;
}
return dato;
}
8.3. COLA 237

8.3. Cola

Una cola es una colección de elementos homogeneos organizados en una


secuencia. Estos elementos unicamente pueden ser insertados por el fin y
removidos por el frente de la cola.

Las operaciones tradicionales asociadas a la cola son las siguientes:

Enqueue(e): Inserta el elemento e en el fin de la cola

Dequeue(): remueve el elemento del frente de la cola

Front(): Retorna el elemento del frente de la cola

IsEmpty(): retorna si la cola esta vacia o no

8.3.1. Representación de colas mediante arreglos

Una forma de implementar el concepto de cola es utilizando arreglos.


La siguiente implementación, trabaja con el concepto de arreglo circular.

Es necesario para 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 se definen dos variables, f y


r. Dende f es un ı́ndice dentro del arreglo que guarda el primer elemento
de la cola y r es un ı́ndice dentro del arreglo que indica la siguiente celda
disponible de arreglo para insertar un elemento.

Inicialmente se asignará f = r = 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. [21]

Uso del arreglo en forma circular, la configuración normal con f <= r.


Ver Figura 8.5

Uso del arreglo en forma circular, la configuración envuelta con r < f. Se


resaltan las celdas que guardan los elementos de la cola. Ver Figura 8.6
238 CAPÍTULO 8. ANÁLISIS DE ESTRUCTURA DE DATOS

Figura 8.5: Cola con arreglo

Figura 8.6: Cola con arreglo

En el Cuadro 191 se muestra la implementación de los métodos mas


importantes de la cola, utilizando un arreglo unidimensional de elementos.
El tamaño del arreglo se define como un valor constante.

Implementación basada en la desarrollada por Goodrich y Tamassia 2 .


Cuadro 191: Métodos de cola

class cola
{
static final int CAPACIDAD =10;
Object arreglo[];
int n, f =0, r =0;

public cola ()
{
this (CAPACIDAD);
}

public cola (int capacidad)


{
n = capacidad;
arreglo = new Object [n];
}

//implementación de métodos

En el Cuadro 192 se muestra el método isEmpty, el cual retorna ver-


dadero en caso de que la pila se encuentra vacı́a. El orden de complejidad
de este método es O(1).

2
Estructuras de datos y algoritmos en JAVA, segunda edición
8.3. COLA 239

Cuadro 192: Métodos de cola 1

public boolean isEmpty ()


{
boolean lleno = false;

if(f==r)
{
lleno= true;
return lleno;
}
return lleno;
}

En el Cuadro 193 tiene la implementación del método que permite in-


sertar un elemento a la cola, este verifica incialmente si la cola se encuentra
llena, en caso de estarlo, se muestra un mensaje. En caso contrario, se inserta
el elemento a la cola. El orden de complejidad de este método es O(1).

Cuadro 193: Métodos de cola 2

public void enqueue (Object nuevoElemento)


{
if (size() == n)
{
JOptionPane.showMessageDialog(null,"Pila llena ");
}
arreglo[r] = nuevoElemento;
r = (r+1) % n;
}

En el Cuadro 194 retorna la cantidad de elementos de la cola.


Cuadro 194: Métodos de cola 4

public int size ()


{
return ( n-f+r) % n;
}
240 CAPÍTULO 8. ANÁLISIS DE ESTRUCTURA DE DATOS

En el Cuadro 195 muestra el metodo f ront, el cual retona el elemento


que se encuentra en frente de la cola pero sin eliminarlo de la misma. El
orden de complejidad de este método es O(1).

Cuadro 195: Métodos de cola 3

public Object front()


{
if (isEmpty ())
{
JOptionPane.showMessageDialog(null,"Cola Vacı́a ");
}
return arreglo[f] ;
}

En el Cuadro 196 elimina el elemento del frente de la cola. El orden de


complejidad de este método es O(1).

Cuadro 196: Métodos de cola 5

public Object dequeue ()


{
Object temp;

if (isEmpty ())
{
temp = null;
JOptionPane.showMessageDialog(null,"Cola Vacı́a ");
}
else
{
temp = arreglo[f];
arreglo[f] = null;
f = (f+1) % n ;
}
return temp;
}
8.4. CONCLUSIONES 241

8.4. Conclusiones

Una estructura de datos puede ser implementada diferentes colecciones,


para el caso de las pilas y las colas, se observa que ambos conceptos pueden
ser implementados a partir del conceto de una lista de nodos y apartir de
un arreglo unidimensional de elementos. También es válido afirmar que ca-
da método de las estructura lista, pila y cola puede ser implementado de
diversas formas, y por lo tanto su orden de complejidad depende de su con-
strucción.

Los siguientes son los ordenes de complejidad de cada uno de los métodos
que se implementaron. Para la clase Lista sencillamente enlazada, se tienen
los siguientes: Ver Cuadro 8.1.

M etodo Orden
estaVacia() O(1)
insertar() O(1)
eliminar() O(1)
imprimir() O(n)

Cuadro 8.1: Ordenes de Complejidad

Los siguientes son los ordenes de complejidad de cada uno de los métodos
que se implementaron. Para la clase Lista Sencilla Circular, se tienen los
siguientes: Ver Cuadro 8.2.

M etodo Orden
insertarPrimero() O(1)
eliminar() O(1)
insertarUltimo() O(1)
imprimir() O(n)

Cuadro 8.2: Ordenes de Complejidad

Los siguientes son los ordenes de complejidad de cada uno de los métodos
que se implementaron. Para la clase Lista Doblemente Enlazada, se tienen
los siguientes: Ver Cuadro 8.3.
242 CAPÍTULO 8. ANÁLISIS DE ESTRUCTURA DE DATOS

M etodo Orden
insertarDerecha() O(1)
insertarIzquierda() O(1)
insertarPrimero() O(1)
insertarUltimo() O(1)
eliminarInicio() O(1)

Cuadro 8.3: Ordenes de Complejidad

Los siguientes son los ordenes de complejidad de cada uno de los métodos
que se implementaron. Para la clase Pila (con arreglos y listas), se tienen
los siguientes: Ver Cuadro 8.4.

M etodo Orden
isEmpty () O(1)
push () O(1)
top () O(1)
size () O(1)
pop () O(1)

Cuadro 8.4: Ordenes de Complejidad

Los siguientes son los ordenes de complejidad de cada uno de los méto-
dos que se implementaron. Para la clase Cola, se tienen los siguientes: Ver
Cuadro 8.5.

M etodo Orden
enqueue() O(1)
dequeue() O(1)
front() O(1)
isEmpty() O(1)

Cuadro 8.5: Ordenes de Complejidad


8.5. AUTOEVALUACIÓN 243

8.5. Autoevaluación

8.5.1. Implementación de cola

Verique que el siguiente código, corresponde a la implementación los


métodos más importantes de una cola. La cola se implementa mediante
nodos. Verifique la correctitud de cada método.

En el Cuadro 197, se tiene la implementación de la clase Nodo. Esta es


fundamental dado que la cola se compone de nodos.
Cuadro 197: Clase Nodo

public class Nodo


{
public Object info;
public Nodo siguiente;

public Nodo ()
{

}
public Nodo (Object info)
{
this.info = info;
siguiente = null;
}
}
244 CAPÍTULO 8. ANÁLISIS DE ESTRUCTURA DE DATOS

En el Cuadro 198 se tiene la implementación de la clase Cola con su


método constructor Cola() y el método para eliminar de la cola dequeue()
y el método cantidad().

Cuadro 198: Clase Cola

public class Cola


{
Nodo inicio;
Nodo fin;
int nodos;

Cola()
{
inicio = new Nodo();
fin = new Nodo();
nodos = 0;
}

public Object dequeue()


{
Object dato = null;
if (inicio.siguiente == null)
{
JOptionPane.showMessageDialog( null, "Cola Vacı́a");
}
else
{
dato = inicio.siguiente.info;
inicio.siguiente = inicio.siguiente.siguiente;
nodos--;
}
return dato;
}
8.5. AUTOEVALUACIÓN 245

En el Cuadro 199 se tiene la implementación de la clase Cola con el méto-


do que inserta en la cola queue(), el método top() y el método cantidad().

Cuadro 199: Clase Cola

void queue(Object elemento)


{
Nodo n = new Nodo(elemento);

if (inicio.siguiente == null)
{
inicio.siguiente = n;
fin.siguiente = n;
}
else
{
fin.siguiente.siguiente = n;
fin.siguiente = n;
}
nodos++;
}

Nodo top()
{
return inicio.siguiente;
}

int cantidad()
{
return nodos;
}

8.5.2. Lecturas Complementarias

1. Leer el artı́culo: Analysis and performance of inverted data base struc-


tures. Alfonso F. Cárdenas IBM Research Lab, San Jose, CA. ACM.

2. Leer el artı́culo: The complexity of priority queue maintenance. Mark


R. Brown. ACM.
246 CAPÍTULO 8. ANÁLISIS DE ESTRUCTURA DE DATOS
Bibliografı́a

[1] Mary E. Ramı́rez Cano. Módulo de clase análisis de algoritmos. Espe-


cialización en procesos para el desarrollo de software. Universidad De
San Buenaventura. pag 12. 2003.

[2] Rhalph P, Grimaldi. Matemática discreta y combinatoria


Addison Wesley Longman. ISBN: 968-444-324-2. pagina 192. 1998.

[3] Baeza-Yates. Ricardo: Personal Home Page. Universidad


de Chile. Algorithmics: An article in Spanish about ba-
sic algorithms including some nice simple problems. pag 1.
http://www.dcc.uchile.cl\_rbaeza\inf\algoritmia.pdf. 1995

[4] Flórez R. Roberto. Algoritmos, estructuras de datos y programación


orientada a objetos. ECOE ediciones. ISBN: 958-648-394-0. 2005.

[5] Alfred V. Aho, Jeffrey D. Ullman. Foundation of Computer Science.


Computer Science Press, ISBN 0-7167-8284-72. Addison Wesley Long-
man, Capı́tulo 3, pag 91. 1995.

[6] R. Guerequeta y A. Vallecillo. Técnicas de Diseño de Algoritmos. Uni-


versidad de Málaga, Segunda Edición: Mayo 2000. ISBN: 84-7496-666-3.
Capitulo 1. 1998. http://www.lcc.uma.es/~av/Libro/

[7] Schildt H. Fundamentos de programación en java 2. Osborne Mcgraw


Hill. Pag 124. ISBN: 958-41-0228-1. 2002.

[8] Bruno R. Preiss. Data Structures and Algorithms with Object-Oriented


Design Patterns in Java. Addison Wesley Publishing Company. ISBN
0471-34613-6. 1998.

[9] A. Aho, J. Hopcrof y J. Ullman. The Design and Analysis of Computer


Algorithms. Addison-Wesley, 1974.

[10] Jeffrey J. McConnell. Analysis of Algorithms: An Active Learning Ap-


proach. Jones and Bartlett Publishers. ISBN: 0-7637-1634-0. 2001.

247
248 BIBLIOGRAFÍA

[11] Cormen. Thomas H, Leiserson. Charles E, Rivest. Ronald L y Stein Clif-


ford. Introduction to Algorithms,. McGraw-Hill Book Company,Second
Edition. 2001.

[12] G. Brassard y P. Bratley. Fundamentos de Algoritmia. Prentice Hall.


pag 96. ISBN: 84-896600-00. 1997

[13] Bruno R. Preiss. Data Structures and Algorithms with Object-Oriented


Design Patterns in Java. Addison Wesley Publishing Company. ISBN
0471-34613-6. 1998.

[14] Alfred V. Aho, Jeffrey D. Ullman. Foundation of Computer Science.


Computer Science Press, ISBN 0-7167-8284-72. Addison Wesley Long-
man, Capı́tulo 3, pag 98. 1995.

[15] Mary E. Ramı́rez Cano. Módulo de clase análisis de algoritmos. Espe-


cialización en procesos para el desarrollo de software. Universidad De
San Buenaventura. pag 36 2003.

[16] Douglas Baldwin, Greg W. Scragg. Algorithms and Data Structures:


The Science of Computing. Charles river media, Inc. ISBN: 1-58450-
250-9. 2004.

[17] Alfred V. Aho, Jeffrey D. Ullman. Foundation of Computer Science.


Computer Science Press, ISBN 0-7167-8284-72. Addison Wesley Long-
man, Capı́tulo 3, pag 151. 1995.

[18] Mark A. Weiis. Florida International University. Estructuras de datos


en java. Addison Wesley. ISBN: 84-7829-035-4. 2002, pag 84.

[19] Mark A. Weiis. Florida International University. Estructuras de datos


en java. Addison Wesley. ISBN: 84-7829-035-4. 2002, pag 116.

[20] Goodrich. Michael T, Tamassia Roberto. Estructuras de datos y algo-


ritmos en java. Cecsa. ISBN: 970-24-0330-8. 2002.

[21] Goodrich. Michael T, Tamassia Roberto. Estructuras de datos y algo-


ritmos en java. Cecsa. ISBN: 970-24-0330-8. 2002.

[22] Peña M. Ricardo. Diseño de programas, formalismo y abstracción.


Prentice Hall. Segunda edición. ISBN: 84-8322-003-2. 1997.
Índice alfabético

algoritmo, 2, 44 suma cifras, 109


adaptativos, 2 suma matriz, 146
análisis, 44 sumatoria, 106
correcto, 3 análisis, 44
definición, 1 algoritmos recursivos, 123
determinı́stico, 2 asintótica, 71
diseño, 21
efectivo, 6 búsqueda
eficiente, 3 binaria, 208
finito, 6 binaria iterativa, 210
Lucas, 15 binaria recursiva, 210
no determinı́stico, 2 lineal extremos, 205
orden, 72 lineal iterativa, 200
palindroma, 13 lineal limitada, 202
preciso, 6 lineal recursiva, 207
algoritmo recursivo
número menor arreglo, 119 Calidad software
numero de apariciones, 120 compatibilidad, 9
cero arreglo, 118 eficiencia, 9
cifras número, 112 exactitud, 9
divisibles, 141 extensibilidad, 9
factorial, 124 facilidad de uso, 9
Fibonacci, 129 verificabilidad, 9
módulo, 117 caso
matriz simetrica, 148 mejor, 44
MCD, 114 peor, 44
multiplicación, 107, 127 Colas
número mayor, 121 arreglos, 237
números armónicos, 115 complejidad
números de Fibonacci, 128 cúbica, 84
numero perfecto, 146 condicionales, 87
palindroma, 147 constante, 78
potencia, 111 cuadrática, 83
suma arreglo, 122 exponencial, 85

249
250 ÍNDICE ALFABÉTICO

lineal, 82 n log(n), 66
llamada a métodos, 88 n2 , 66
logarı́tmica, 80 n3 , 66
ordenamiento
eficiencia burbuja, 160
espacio, 70 burbuja bidireccional, 162
Estructuras gnomeSort, 195
cola, 237 inserción, 171
pila, 229 mergeSort, 180
quickSort, 183
instrucción radixSort, 187
break, 56 selección, 167
Math. pow, 37 shellSort, 174
stoogeSort, 186
Lista
circular, 220 Pilas
doble, 223 arreglos, 230
sencilla, 216 pilas con listas, 234
Listas potencia número, 25
sencillamente enlazada, 216 propiedades
logaritmo, 55, 81 orden inferior, 86
regla de la suma, 86
mcd, 22
transitividad, 86
número recursividad
primos, 3 caso base, 103
números directa, 104
armónicos, 11, 115 indirecta, 104
notación recursión, 103
asintótica, 71 regla del lı́mite, 73
Big Oh, 71
Omega, 73 Serie Fibonacci, 14

operaciones elementales, 45 técnicas


optimización, 30 análisis, 44
desenvolvimiento, 30 comparación, 44
reducción esfuerzo, 31 profiling, 44
tipos de variables, 33 tiempo de ejecución, 44, 45, 70
orden de complejidad llamada a métodos, 57
2n , 66
log(n), 66
n, 66

Anda mungkin juga menyukai