Cap1y2.Textoguiapdf
Upcoming SlideShare
Loading in...5
×
 

Cap1y2.Textoguiapdf

on

  • 942 views

Guia sobre algoritmo

Guia sobre algoritmo

Statistics

Views

Total Views
942
Views on SlideShare
942
Embed Views
0

Actions

Likes
0
Downloads
11
Comments
0

0 Embeds 0

No embeds

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

Cap1y2.Textoguiapdf Cap1y2.Textoguiapdf Document Transcript

  • Cap´ ıtulo 1 Problemas, programas, estructuras y algoritmos El proceso de resoluci´n de problemas, en programaci´n, est´ compuesto por una o o a serie de pasos que se aplican de forma met´dica. Empieza con el an´lisis de las carac- o a ter´ ısticas del problema, contin´a con el dise˜o de una soluci´n, la implementaci´n u n o o del dise˜o y termina con la verificaci´n y evaluaci´n del programa resultante. Los dos n o o ejes b´sicos en los que se articula el desarrollo de un programa son las estructuras de a datos y los algoritmos. Las estructuras de datos determinan la forma de almacenar la informaci´n necesaria para resolver el problema. Los algoritmos manipulan esa o informaci´n, para producir unos datos de salida a partir de unos datos de entrada. o El objetivo ultimo de la algor´ ´ ıtmica es encontrar la mejor forma de resolver los problemas, en t´rminos de eficiencia y de calidad del software. e Objetivos del cap´ ıtulo: Entender el desarrollo de programas como un proceso met´dico e ingenieril, formado por o una serie de etapas con distintos niveles de abstracci´n, frente a la idea de la programaci´n o o como arte. Tomar conciencia de la importancia de realizar siempre un an´lisis y dise˜ o previos del a n problema, como pasos anteriores a la implementaci´n en un lenguaje de programaci´n. o o Motivar el estudio de los algoritmos y las estructuras de datos, como una disciplina fun- damental de la inform´tica. a Repasar algunos de los principales conceptos de programaci´n, que el alumno debe conocer o de cursos previos como: tipos de datos, estructuras de datos, tipos abstractos, algoritmos, complejidad algor´ıtmica y eficiencia. Entender la importancia del an´lisis de algoritmos, el objetivo del an´lisis, los factores a a que influyen y el tipo de notaciones utilizadas para su estudio. Distinguir entre algoritmos y esquemas algor´ıtmicos, presentando brevemente los princi- pales esquemas algor´ıtmicos que ser´n estudiados. a 1
  • 2 Cap´ ıtulo 1. Problemas, programas, estructuras y algoritmos Contenido del cap´ ıtulo: 1.1. Resoluci´n de problemas . . . . . . . . . . . . . . . . . . . . . . o . . . . . . . . . . 3 1.1.1. An´lisis de requisitos del problema . . . . . . . . . . . . a . . . . . . . . . . 3 1.1.2. Modelado del problema y algoritmos abstractos . . . . . . . . . . . . . . . 4 1.1.3. Dise˜o de la soluci´n . . . . . . . . . . . . . . . . . . . . n o . . . . . . . . . . 5 1.1.4. Implementaci´n del dise˜o . . . . . . . . . . . . . . . . . o n . . . . . . . . . . 6 1.1.5. Verificaci´n y evaluaci´n de la soluci´n . . . . . . . . . . o o o . . . . . . . . . . 7 1.2. Tipos de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 1.2.1. Definici´n de tipo de datos, tipo abstracto y estructura o . . . . . . . . . . 8 1.2.2. Tipos de tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 1.2.3. Repaso de tipos y pseudolenguaje de definici´n . . . . . o . . . . . . . . . . 12 1.3. Algoritmos y algor´ıtmica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 1.3.1. Definici´n y propiedades de algoritmo . . . . . . . . . . o . . . . . . . . . . 15 1.3.2. An´lisis de algoritmos . . . . . . . . . . . . . . . . . . . a . . . . . . . . . . 17 1.3.3. Dise˜o de algoritmos . . . . . . . . . . . . . . . . . . . . n . . . . . . . . . . 21 1.3.4. Descripci´n del pseudoc´digo utilizado . . . . . . . . . . o o . . . . . . . . . . 25 1.4. Consejos para una buena programaci´n . . . . . . . . . . . . . o . . . . . . . . . . 26 1.4.1. Importancia del an´lisis y dise˜ o previos . . . . . . . . . a n . . . . . . . . . . 26 1.4.2. Modularidad: encapsulaci´n y ocultamiento . . . . . . . o . . . . . . . . . . 28 1.4.3. Otros consejos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 Ejercicios propuestos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 Cuestiones de autoevaluaci´n . . . . . . . . . . . . . . . . . . . . . . o . . . . . . . . . . 31 Referencias bibliogr´ficas . . . . . . . . . . . . . . . . . . . . . . . . a . . . . . . . . . . 32
  • 1.1. Resoluci´n de problemas o 3 1.1. Resoluci´n de problemas o La habilidad de programar ordenadores constituye una de las bases necesarias de to- do inform´tico. Pero, por encima del nivel de programaci´n, la caracter´ a o ıstica fundamental de un inform´tico es su capacidad para resolver problemas. La resoluci´n de problemas a o inform´ticos en la vida real implica otras muchas m´s habilidades que unicamente saber a a ´ programar –habilidades que distinguen a un ingeniero de un simple programador. Implica comprender y analizar las necesidades de los problemas, saber modelarlos de forma abs- tracta diferenciando lo importante de lo irrelevante, disponer de una variada bater´ de ıa herramientas conceptuales que se puedan aplicar en su resoluci´n, trasladar un dise˜o abs- o n tracto a un lenguaje y entorno concretos y, finalmente, ser capaz de evaluar la correcci´n, o prestaciones y posibles inconvenientes de la soluci´n planteada. o Las herramientas que surgen en el desarrollo de programas son esencialmente de dos clases: estructuras de datos y algoritmos. Las estructuras de datos representan la parte est´tica de la soluci´n al problema, el componente almacenado, donde se encuentran a o los datos de entrada, de salida y los necesarios para posibles c´lculos intermedios. Los a algoritmos representan la parte din´mica del sistema, el componente que manipula los a datos para obtener la soluci´n. Algoritmos y estructuras de datos se relacionan de forma o muy estrecha: las estructuras de datos son manipuladas mediante algoritmos que a˜aden o n modifican valores en las mismas; y cualquier algoritmo necesita manejar datos que estar´n a almacenados en cierta estructura. La idea se puede resumir en la f´rmula magistral de o Niklaus Wirth: Algoritmos + Estructuras de datos = Programas. En ciertos ´mbitos de aplicaci´n predominar´ la componente algor´ a o a ıtmica –como, por ejemplo, en problemas de optimizaci´n y c´lculo num´rico– y en otros la componente o a e de estructuras –como en entornos de bases de datos y sistemas de informaci´n–, pero o cualquier aplicaci´n requerir´ siempre de ambos. Esta dualidad aparece en el nivel abs- o a tracto: un tipo abstracto est´ formado por un dominio de valores (estructura abstracta) y a un conjunto de operaciones (algoritmos abstractos). Y la dualidad se refleja tambi´n en el e nivel de implementaci´n: un m´dulo (en lenguajes estructurados) o una clase (en lengua- o o jes orientados a objetos) est´n compuestos por una estructura de atributos o variables, y a una serie de procedimientos de manipulaci´n de los anteriores. o Antes de entrar de lleno en el estudio de los algoritmos y las estructuras de datos, vamos a analizar los pasos que constituyen el proceso de resoluci´n de problemas. o 1.1.1. An´lisis de requisitos del problema a El proceso de resoluci´n de problemas parte siempre de un problema, de un enun- o ciado m´s o menos claro que alguien plantea porque le vendr´ bien que estuviera resuelto1 . a ıa El primer paso es la comprensi´n del problema, entender las caracter´ o ısticas y peculiari- dades de lo que se necesita. Este an´lisis de los requisitos del problema puede ser, a de por s´ una de las grandes dificultades; sobre todo en grandes aplicaciones, donde los ı, documentos de requisitos son ambiguos, incompletos y contradictorios. 1 Esta afirmaci´n puede parecer irrelevante por lo obvia. Pero no debemos perder de vista el objetivo o ultimo de la utilidad pr´ctica, en el estudio de los algoritmos y las estructuras de datos. ´ a
  • 4 Cap´ ıtulo 1. Problemas, programas, estructuras y algoritmos El an´lisis debe producir como resultado un modelo abstracto del problema. El a modelo abstracto es un modelo conceptual, una abstracci´n del problema, que reside o exclusivamente en la mente del individuo y donde se desechan todas las cuestiones irre- levantes para la resoluci´n del problema. Supongamos, por ejemplo, los dos siguientes o problemas, con los cuales trabajaremos en el resto de los puntos. Ejemplo 1.1 El problema de las cifras. Dado un conjunto de seis n´meros enteros, u que pueden ser del 1 al 10, o 25, 50, 75 o 100, encontrar la forma de conseguir otro entero ´ ´ dado entre 100 y 999, combinando los n´meros de partida con las operaciones de suma, u resta, producto y divisi´n entera. Cada uno de los seis n´meros iniciales s´lo se puede o u o usar una vez. Ejemplo 1.2 El problema de detecci´n de caras humanas. Dada una imagen en o color en un formato cualquiera (por ejemplo, bmp, jpeg o gif) encontrar el n´mero de u caras humanas presentes en la misma y la posici´n de cada una de ellas. o Figura 1.1: Ejemplos del problema de las cifras (izquierda) y detecci´n de caras (derecha). o Ambos enunciados representan categor´ de problemas muy distintas. El proble- ıas ma 1.1 tiene un enunciado m´s o menos claro, aunque cabr´ algunas dudas. Es de a ıan tipo matem´tico, se puede modelar formalmente y es previsible que exista un algoritmo a adecuado. El problema 1.2 tiene un enunciado m´s corto pero mucho m´s ambiguo, no a a est´ claro exactamente lo que se pide. Es m´s, incluso teni´ndolo bien claro, el problema a a e parece m´s adecuado para ser resuelto de cabeza por un humano, pero dif´ de imple- a ıcil mentar en un ordenador. Aun as´ ambos problemas tienen algo en com´n: son de inter´s, ı, u e as´ que necesitamos resolverlos de la mejor forma que podamos. ı 1.1.2. Modelado del problema y algoritmos abstractos El primer paso ser´ crear un modelo abstracto, en el que nos quedamos con lo ıa esencial del problema. Normalmente, este modelo se crea a trav´s de una analog´ con e ıa algo conocido previamente. Por ejemplo, para ense˜ar a un alumno inexperto lo que es un n algoritmo (concepto para ´l desconocido) se utiliza la analog´ con una receta de cocina e ıa
  • 1.1. Resoluci´n de problemas o 5 (concepto conocido, esperemos), y las estructuras de datos se asimilan con la disposici´n de o armarios, cajones y recipientes donde se guardan los ingredientes y el bizcocho resultante. Ejemplo 1.1 (cont.) Modelo abstracto. Para el problema de las cifras, el modelo abstracto puede prescindir del hecho de que los n´meros sean seis, de que est´n en ciertos u e rangos y que se trabaje s´lo con enteros. Una soluci´n ser´ una expresi´n matem´tica o o ıa o a –usando los n´meros de partida– que produce el n´mero buscado. Entonces, el problema u u se podr´ resolver generando todas las posibles expresiones que contengan los n n´meros ıa u iniciales, o menos, y qued´ndonos la que obtenga un resultado m´s pr´ximo al buscado. a a o Pero el modelo de un problema no tiene por qu´ ser unico. Otra persona podr´ e ´ ıa interpretarlo como un conjunto de cajas, que se combinan entre s´ Ahora tendr´ ı. ıamos un modelo “temporal”: tenemos que buscar dos cajas que combin´ndolas nos den el objetivo a o un valor pr´ximo. Si no llegamos al resultado, buscar otra caja y as´ sucesivamente. La o ı idea resultante ser´ similar a lo que aparece en la figura 1.1, abajo a la izquierda. ıa Ejemplo 1.2 (cont.) Modelo abstracto. En el problema de detecci´n de caras, el o modelo abstracto elimina el hecho de que las im´genes tengan distinto formato o incluso a que los objetos buscados sean caras humanas. Un posible modelo ser´ un problema de ıa patrones –patrones como los de un sastre– que admiten cierta variabilidad: se pueden mover, estirar, cambiar el tama˜o, rotar, etc. El problema consistir´ en buscar esos pa- n ıa trones en las im´genes, en todos los sitios que se pueda. Grosso modo, un algoritmo podr´ a ıa consistir en generar todas las posibles colocaciones, rotaciones y tama˜os de los patrones n y quedarnos con las que “encajen” con lo que hay en la imagen de partida. 1.1.3. Dise˜ o de la soluci´n n o Una vez que tengamos un modelo completo y adecuado, significar´ que hemos en- a tendido bien el problema. Como hemos visto en los ejemplos, el modelo conlleva tambi´n e un algoritmo informal, que no es m´s que una vaga idea de c´mo resolver el problema. El a o siguiente paso de refinamiento consistir´ en dise˜ar una soluci´n. Usando otra analog´ ıa n o ıa, el dise˜ o de un programa es equivalente a los planos de un arquitecto: el dise˜o des- n n cribe el aspecto que tendr´ el programa, los materiales que se necesitan y d´nde y c´mo a o o colocarlos. El dise˜o es siempre un paso previo a la implementaci´n, pero que se olvida n o muy a menudo por los programadores primerizos. Un ingeniero inform´tico deber´ ser a ıa tan consciente del error de programar sin partir de un buen dise˜o previo, como lo es un n arquitecto del fallo de hacer un puente sin tener antes los planos. El dise˜o de un programa consta de dos clases de elementos: tipos de datos y algo- n ritmos. Los tipos de datos usados a nivel de dise˜o son tipos abstractos, en los cuales n lo importante son las operaciones que ofrecen y no la representaci´n en memoria. Los o algoritmos son una versi´n refinada de los algoritmos abstractos del paso anterior. No o obstante, son a´n algoritmos en pseudoc´digo, donde se dan cosas por supuestas. u o Definir los tipos de datos para almacenar la soluci´n suele ser mucho m´s dif´ que o a ıcil definirlos para los datos de entrada. Adem´s, modificaciones en los algoritmos pueden a requerir cambios en los tipos de datos y viceversa. Ejemplo 1.1 (cont.) Dise˜o de la soluci´n. En el problema de las cifras, los tipos n o de datos para la entrada podr´ ser una tabla de 6 enteros y otro entero para el ıan
  • 6 Cap´ ıtulo 1. Problemas, programas, estructuras y algoritmos n´mero buscado. Pero ¿c´mo representar la soluci´n? En este caso, la soluci´n es una u o o o serie de operaciones sobre los n´meros del array. Cada operaci´n podr´ ser una tupla u o ıa (indice1 , indice2 , operacion), que significa: “operar el n´mero de indice1 en el array con u el de indice2 usando operacion”. La representaci´n de la soluci´n ser´ una lista de es- o o ıa tas tuplas. Con esta representaci´n, el algoritmo deber´ tener en cuenta que indice1 e o ıa indice2 ya est´n usados. Adem´s aparece otro nuevo n´mero, que puede usarse con pos- a a u terioridad. Entonces, puede ser necesario definir otros tipos o modificar los existentes, por ejemplo haciendo que la tupla signifique: “operar el n´mero ..., almacenando el resultado u en indice1 y colocando un 0 en indice2 ”. Ejemplo 1.2 (cont.) Dise˜o de la soluci´n. En el problema de detecci´n de caras, n o o la entrada ser´ una imagen, que se puede representar con matrices de enteros, de cierto ıa ancho y alto. Adem´s, hay otro dato oculto, el modelo de lo que se considera una cara. a ¿C´mo se puede modelar la forma de todas las posibles caras? ¿Qu´ informaci´n se deber´ o e o ıa almacenar? La representaci´n no es nada trivial. Por ejemplo, una soluci´n sencilla ser´ o o ıa tener un conjunto variado de im´genes de caras de ejemplo. La representaci´n de la a o soluci´n debe indicar el n´mero de caras y la posici´n de cada una; por ejemplo centro, o u o (xi , yi ), escala, si , y rotaci´n, αi , de cada cara. Podr´ o ıamos usar una lista de tuplas de tipo (xi , yi , si , αi ). Con todo esto, habr´ que dise˜ar un algoritmo en pseudolenguaje que ıa n pruebe todos los valores de los par´metros anteriores, y diga d´nde se encuentra una cara. a o 1.1.4. Implementaci´n del dise˜ o o n El siguiente paso en la resoluci´n del problema es la implementaci´n. La implementa- o o ci´n parte del dise˜o previo, que indica qu´ cosas se deben programar, c´mo se estructura o n e o la soluci´n del problema, d´nde colocar cada funcionalidad y qu´ se espera en concreto de o o e cada parte en la que se ha descompuesto la soluci´n. Esta descripci´n de lo que debe hacer o o cada parte es lo que se conoce como la especificaci´n. Una implementaci´n ser´ correcta o o a si cumple su especificaci´n. o La dualidad tipos/algoritmos del dise˜o se traslada en la implementaci´n a estruc- n o turas/algoritmos. Para cada uno de los tipos de datos del dise˜o, se debe elegir una n estructura de datos adecuada. Por ejemplo, los dos ejemplos de problemas analizados necesitan listas. Una lista se puede representar mediante una variedad de estructuras: lis- tas enlazadas con punteros o cursores, mediante celdas adyacentes en un array, enlazadas simple o doblemente, con nodos cabecera o no, etc. La elecci´n debe seguir los criterios o de eficiencia –en cuanto a tiempo y a uso de memoria– que se hayan especificado para el problema. En cada caso, la estructura m´s adecuada podr´ ser una u otra. a a Por otro lado, la implementaci´n de los algoritmos tambi´n se puede ver como o e una serie de tomas de decisiones, para trasladar un algoritmo en pseudoc´digo a un o lenguaje concreto. Por el ejemplo, si se necesita repetir un c´lculo varias veces, se puede a usar un bucle for, un while, se puede usar recursividad e incluso se puede poner varias veces el mismo c´digo si el n´mero de ejecuciones es fijo2 . o u Realmente, la implementaci´n de estructuras de datos y algoritmos no va por sepa- o rado. Ambas son simult´neas. De hecho, as´ ocurre con los mecanismos de los lenguajes a ı 2 Algo nada aconsejable, de todos modos.
  • 1.2. Tipos de datos 7 de programaci´n –m´dulos o clases– en los que se asocia cada estructura con los algo- o o ritmos que la manejan. La organizaci´n temporal de la implementaci´n es, m´s bien, o o a por funcionalidades: cuestiones de interface con el usuario, accesos a disco, funcionalidad matem´tica, etc. a ANÁLISIS DEL MODELO DISEÑO DE TIPOS DE DATOS IMPLEMEN- LA SOLUCIÓN ESTRUCTURAS PROBLEMA PROBLEMA ABSTRACTO ABSTRACTOS TACIÓN DE DATOS Y ALGORITMO ALGORITMO EN ALGORITMOS INFORMAL PSEUDOCÓDIGO VERIFICACIÓN Y EVALUACIÓN Figura 1.2: Esquema del proceso c´ ıclico de desarrollo de software. 1.1.5. Verificaci´n y evaluaci´n de la soluci´n o o o La resoluci´n de un problema no acaba con la implementaci´n de un programa3 . Los o o programas deber´ ser comprobados de forma exhaustiva, verificando que se ajustan a los ıan objetivos deseados, obtienen los resultados correctos sin provocar fallos de ejecuci´n. En la o pr´ctica, hablamos de un proceso c´ a ıclico de desarrollo: implementar, verificar, corregir la implementaci´n, volver a verificar, volver a corregir, etc´tera. En cierto momento, puede o e que tengamos que cambiar el dise˜o, e incluso puede que lo que est´ fallando sea el modelo n e abstracto. Cuanto m´s atr´s tengamos que volver, m´s tiempo habremos perdido haciendo a a a cosas que luego resultan in´tiles. u La evaluaci´n de los programas incluye no s´lo la correcci´n del resultado sino o o o tambi´n la medici´n de las prestaciones, lo que normalmente se denomina el an´lisis de e o a eficiencia. La eficiencia es una proporci´n entre los resultados obtenidos y los recursos o consumidos. Fundamentalmente, un programa consume recursos de tiempo y de memoria. Aunque usualmente hablamos de “an´lisis de algoritmos”, tanto las estructuras de datos a como los algoritmos influyen en el consumo de recursos de un programa. Incluso antes de llegar a la implementaci´n de un programa, es posible y adecuado o hacer el an´lisis del algoritmo en pseudoc´digo. Este an´lisis ser´ una previsi´n de la a o a a o eficiencia que obtendr´ la soluci´n dise˜ada si la implementamos. Se trata, por lo tanto, ıa o n de un estudio te´rico. Cuando el estudio se realiza sobre una implementaci´n concreta, o o hablamos normalmente de estudios experimentales. 1.2. Tipos de datos A modo de breve repaso, vamos a ver en esta secci´n las definiciones de tipo de datos, o tipo abstracto, estructura de datos, las distintas clases de tipos y algunos de los principales 3 O no deber´ Desafortunadamente, estamos tan acostumbrados a ver programas que se cuelgan, ıa. como coches que fallan justo al salir del garaje, ¡exactamente por lo mismo: falta de pruebas!
  • 8 Cap´ ıtulo 1. Problemas, programas, estructuras y algoritmos tipos de datos usados en programaci´n. Se supone que el alumno ya est´ familiarizado o a con todos estos conceptos. Es m´s, ser´ un interesante ejercicio pararse en este punto, a ıa cerrar el libro y tratar recordar todo lo que se pueda de los anteriores temas. 1.2.1. Definici´n de tipo de datos, tipo abstracto y estructura o Posiblemente, la primera duda del programador no ingeniero ser´ ¿qu´ necesidad ıa: e hay de tantos t´rminos para referirse a lo mismo? Los conceptos de tipo abstracto de datos, e tipo de datos y estructura de datos no son, en absoluto, redundantes ni incoherentes. Co- mo ya hemos visto, aparecen en los distintos niveles de desarrollo. Un tipo abstracto es un concepto de dise˜o y por lo tanto previo e independiente de cualquier implementaci´n. n o Un tipo de datos es un concepto de programaci´n y se puede ver como la realizaci´n o o o materializaci´n de un tipo abstracto. La estructura de datos es la organizaci´n o disposi- o o ci´n en memoria de la informaci´n almacenada para cierto tipo de datos. Veamos, por o o partes, las definiciones. Definici´n 1.1 Un Tipo Abstracto de Datos (TAD) est´ compuesto por un dominio o a abstracto de valores y un conjunto de operaciones definidas sobre ese dominio, con un comportamiento espec´ ıfico. Por ejemplo, los booleanos constituyen un TAD, formado por el dominio de valores {verdadero, falso} y las operaciones sobre ese dominio: NO, Y, O, XOR, IMPLICA, etc. Decimos que el dominio de valores es abstracto porque no se le supone ninguna repre- sentaci´n ni ning´n significado. Es decir, el valor verdadero no tiene por qu´ ser un 1, ni o u e debe almacenarse necesariamente en un bit. Pero, es m´s, ¿qu´ significa verdadero? Es a e un concepto hueco, vac´ sin ning´n significado expl´ ıo, u ıcito. El unico significado le viene ´ dado impl´ ıcitamente por su relaci´n con las operaciones del tipo. Por ejemplo, verdadero o Y cualquier cosa es siempre igual a cualquier cosa. En el otro extremo de complejidad, otro ejemplo de TAD podr´ ser las ventanas de ıan un entorno Windows o XWindows. En este caso, el dominio de valores estar´ compuesto ıa por el conjunto –pr´cticamente infinito– de todas las ventanas en sus distintas formas, a tama˜os, colores y contenidos. Las operaciones sobre el dominio ser´ del tipo: crear n ıan ventana, mover, cambiar de tama˜o, a˜adir botones, etc. n n Las operaciones del TAD son abstractas, en el sentido de que sabemos lo que hacen pero no c´mo lo hacen. A la hora de programar el TAD, esto da lugar a un principio o conocido como ocultaci´n de la implementaci´n: lo importante es que se calcule el o o resultado esperado y no c´mo se calcule. Volveremos a insistir en esto m´s adelante. o a En cuanto que el TAD es un concepto abstracto, reside exclusivamente como un ente et´reo en la mente del programador. La realizaci´n del TAD en un lenguaje concreto, en e o cierto entorno de programaci´n y por determinado programador, es lo da lugar a un tipo o de datos, ahora sin el apellido “abstracto”. Definici´n 1.2 El tipo de datos de una variable, un par´metro o una expresi´n, deter- o a o mina el conjunto de valores que puede tomar esa variable, par´metro o expresi´n, y las a o operaciones que se pueden aplicar sobre el mismo.
  • 1.2. Tipos de datos 9 El tipo de datos es un concepto de programaci´n, de implementaci´n, mientras o o que el TAD es un concepto matem´tico previo a la programaci´n. Un tipo de datos debe a o ajustarse al comportamiento esperado de un TAD. Por ejemplo, el tipo integer de Pascal o el tipo int de C son dos tipos de datos, que corresponden al TAD de los enteros, Z. Adem´s de los tipos de datos elementales existentes en un lenguaje de programaci´n, a o los lenguajes suelen ofrecer mecanismos para que el usuario se defina sus propios tipos. Estos mecanismos pueden ser m´s o menos avanzados. En lenguajes como C o Pascal, la a definici´n del tipo indica los atributos que se almacenan para las variables de ese tipo. o Las operaciones se definen por separado. Por ejemplo, en C una pila de enteros ser´ ıa: struct PilaEnteros { int datos[]; int tope, maximo; }; En lenguajes orientados a objetos, como C++ o Java, los tipos definidos por el usuario se llaman clases, y su definici´n incluye tanto los atributos almacenados como o las operaciones sobre los mismos. Por ejemplo, en C++ la definici´n de las pilas podr´ o ıa ser la siguiente: class PilaEnteros { private: // Atributos int datos[]; int tope, maximo; public: // Operaciones PilaEnteros (int max); Push (int valor); int Pop(); }; Volveremos a tratar los mecanismos de definici´n de tipos en el siguiente cap´ o ıtulo. En la programaci´n de un tipo de datos surgen dos cuestiones: c´mo almacenar en o o memoria los valores del dominio y c´mo programar las operaciones del tipo de datos. o La primera cuesti´n implica dise˜ar una estructura de datos para el tipo. La segunda o n est´ relacionada con la implementaci´n de las operaciones de manipulaci´n y, evidente- a o o mente, estar´ en funci´n de la primera. a o Definici´n 1.3 La estructura de datos de un tipo de datos es la disposici´n en memo- o o ria de los datos necesarios para almacenar los valores de ese tipo. Por ejemplo, para representar las pilas podemos usar una estructura de arrays como la que aparece antes, o una lista enlazada de enteros, o una lista de arrays de enteros, o un array de listas de enteros, y as´ sucesivamente. Por lo tanto, un tipo de datos puede ı ser implementado utilizando diferentes estructuras.
  • 10 Cap´ ıtulo 1. Problemas, programas, estructuras y algoritmos De forma an´loga, una misma estructura de datos puede corresponder a diferentes a tipos. Por ejemplo, una estructura de arrays puede usarse para implementar listas, pilas o colas de tama˜o limitado. Ser´ la forma de actuar de las operaciones la que determine n a si el array almacena una lista, una pila o una cola. La utilizaci´n de estructuras de datos adecuadas para cada tipo de datos es una o cuesti´n fundamental en el desarrollo de programas. La elecci´n de una estructura ten- o o dr´ efectos no s´lo en la memoria utilizada por el programa, sino tambi´n en el tiempo a o e de ejecuci´n de los algoritmos. Por ejemplo, la representaci´n de colas con arrays puede o o producir desperdicio de memoria, ya que el tama˜o del array debe ser el tama˜o de la n n m´xima ocupaci´n prevista de la cola. En una representaci´n con punteros se podr´ usar a o o a menos memoria, pero posiblemente el tiempo de ejecuci´n ser´ sensiblemente mayor. o a 1.2.2. Tipos de tipos Dentro de este apartado vamos a hacer un repaso de terminolog´ clases y propie- ıa, dades relacionadas con los tipos de datos. Podemos hacer una primera clasificaci´n de los tipos teniendo en cuenta si el tipo o es un tipo est´ndar del lenguaje o no. Encontramos dos categor´ a ıas: Tipos primitivos o elementales. Son los que est´n definidos en el lenguaje de a programaci´n. Por ejemplo, en C son tipos primitivos los tipos int, long, char y o float, entre otros. Tipos definidos por el usuario. Son todos los tipos no primitivos, definidos por la misma persona que los usa, o bien por otro programador. Idealmente, un buen lenguaje deber´ hacer transparente el uso de tipos de una u ıa otra categor´ Es decir, al usar una variable de cierto tipo T deber´ ser lo mismo que ıa. ıa T sea un tipo primitivo o definido por el usuario. Ambos deber´ ser conceptualmente ıan equivalentes. Pero esto no siempre ocurre. Por ejemplo, en C es posible hacer asignaciones entre tipos primitivos, pero no entre ciertos tipos definidos por el usuario. La asignaci´n o “a= b;” funcionar´ o no, dependiendo de que los tipos de a y b sean primitivos o no. a Por otro lado, podemos hacer otra clasificaci´n –seg´n el n´mero de valores alma- o u u cenados– en tipos simples y compuestos: Tipo simple. Una variable de un tipo simple contiene un unico valor en cierto ´ instante. Por ejemplo, los enteros, reales, caracteres y booleanos son tipos simples. Tipo compuesto. Un tipo compuesto es aquel que se forma por la uni´n de varios o tipos simples o compuestos. Por lo tanto, una variable de tipo compuesto contiene varios valores de tipo simple o compuesto. Los lenguajes de programaci´n suelen ofrecer distintos mecanismos de composici´n o o para crear tipos compuestos. Los m´s habituales son los arrays y los registros. Un a array contiene una serie de posiciones consecutivas de variables del mismo tipo, cada una identificada con un ´ındice dentro del rango del array. Los registros se definen enumerando los campos que forman el tipo compuesto, cada uno de los cuales tiene un nombre y un
  • 1.2. Tipos de datos 11 tipo (que, a su vez, puede ser simple o compuesto). Las clases de lenguajes orientados a objetos tambi´n se pueden ver como una manera de crear tipos compuestos, en la cual se e pueden a˜adir atributos y operaciones al tipo. n Normalmente los tipos simples son tipos elementales y los tipos compuestos son definidos por el usuario. No obstante, tambi´n pueden existir tipos compuestos definidos e en el lenguaje de programaci´n, y tipos simples definidos por el usuario. Un mecanismo o que permite al usuario definir nuevos tipos simples son los enumerados. Un enumerado se define listando de forma completa el dominio del tipo. Por ejemplo, el tipo Sexo se puede definir como un tipo enumerado con el dominio {masculino, femenino}. Un tipo contenedor, o colecci´n, es un tipo compuesto que puede almacenar un o n´mero indefinido de valores de otro tipo cualquiera. Por ejemplo, una lista de enteros o un u array de Sexo son tipos contenedores. Un registro con dos enteros no ser´ un contenedor. ıa En general, interesa que los tipos contenedores puedan almacenar valores de otro tipo cualquiera; por ejemplo, que las listas puedan ser de enteros, de reales, de registros o de cualquier otra cosa. Un tipo se dice que es gen´rico o parametrizado si su significado e depende de otro tipo que es pasado como par´metro. Por ejemplo, una lista gen´rica a e tendr´ la forma Lista[T], donde T es un par´metro que indica el tipo de los objetos ıa a almacenados. Podr´ ıamos tener una variable a de tipo Lista[entero] y otra variable b de tipo Lista[real]; estos casos concretos se llaman instanciaciones del tipo gen´rico. e Realmente son muy pocos los lenguajes que permiten la definici´n de tipos para- o metrizados. Entre ellos se encuentra C++, que permite parametrizar un tipo mediante el uso de plantillas template. Por ejemplo, un tipo gen´rico Pila[T] ser´ e ıa: template <class T> class Pila { private: // Atributos T datos[]; int tope, maximo; public: // Operaciones Pila (int max); Push (T valor); T Pop(); }; En la parte de pr´cticas se profundizar´ m´s en el uso de plantillas como una manera a a a de crear tipos gen´ricos. e Finalmente, podemos hacer una distinci´n entre tipos mutables e inmutables. Deci- o mos que un tipo de datos es inmutable cuando los valores del tipo no cambian despu´s e de haber sido creados. El tipo ser´ mutable si los valores pueden cambiar. ¿Qu´ impli- a e caciones tiene que un tipo sea mutable o inmutable? Supongamos que queremos definir el tipo Lista[T]. Si el tipo se define con una representaci´n mutable, podemos incluir o operaciones que a˜aden o suprimen elementos de la lista. n operaci´n Inserta (var lista: Lista[T]; elemento: T) o operaci´n SuprimePrimero (var lista: Lista[T]) o
  • 12 Cap´ ıtulo 1. Problemas, programas, estructuras y algoritmos Donde lista es un par´metro que se puede modificar dentro de las operaciones. Pero a si el tipo es inmutable no ser´ posible que un par´metro de entrada se modificara. En ese ıa a caso, las operaciones de inserci´n y eliminaci´n deber´ devolver una nueva lista, con el o o ıan elemento correspondiente insertado o eliminado. operaci´n Inserta (lista: Lista[T]; elemento: T): Lista[T] o operaci´n SuprimePrimero (lista: Lista[T]): Lista[T] o Seg´n nos interese, definiremos los tipos como mutables o inmutables. Normalmente, u la mayor´ de los tipos ser´n mutables, aunque en algunos casos nos interesar´ usar tipos ıa a a inmutables: la suposici´n de que los valores del tipo no cambian puede simplificar, o hacer o m´s eficiente, la implementaci´n de la estructura de datos para el tipo. a o 1.2.3. Repaso de tipos y pseudolenguaje de definici´n o La mayor´ de los lenguajes ofrecen al programador un conjunto similar de tipos de ıa datos simples (enteros, reales, booleanos, etc.), con las operaciones necesarias para mane- jarlos. Estos tipos tienen una correspondencia con los tipos manejados por el procesador, por lo que su implementaci´n es muy eficiente. Vamos a ver los que nos podemos encon- o trar m´s habitualmente. Para cada uno de ellos, indicaremos tambi´n la notaci´n que se a e o usar´ en el resto del libro para referirse a ese tipo, cuando usemos el pseudolenguaje4 . a Enteros. Con signo o sin signo, con precisi´n simple, doble o reducida (un byte). o En nuestro pseudoc´digo supondremos un unico tipo entero, con las operaciones o ´ habituales. Reales. Con precisi´n simple o doble. Para los n´meros reales se suelen usar re- o u presentaciones mantisa-exponente. Cuando lo usemos, pondremos el tipo real. Caracteres. Lo denotaremos como caracter. Cadenas. En algunos lenguajes las cadenas de caracteres son tipos elementales. En otros, como en C, las cadenas no son un tipo elemental sino que son simplemente arrays de caracteres. En nuestro caso, suponemos que tenemos el tipo cadena con operaciones para concatenar o imprimir por pantalla. Booleanos. Igual que antes, no existen en C –donde se usan enteros en su lugar–, aunque s´ en C++ y Pascal. Supondremos que tenemos el tipo booleano. ı En cuanto a los tipos contenedores (listas, pilas, colas, arboles, etc.), normalmente ´ no forman parte del lenguaje de programaci´n, sino que se definen en librer´ de utili- o ıas dades que se pueden incluir en un programa o no. No obstante, s´ que se suelen incluir ı mecanismos de composici´n de tipos como arrays, registros y tipos enumerados. En nues- o tro pseudolenguaje tenemos la posibilidad de definir nuevos tipos, mediante una cl´usula a tipo. Adem´s, permitimos que se definan tipos parametrizados. Por ejemplo, podemos a tener los siguientes tipos: 4 Este pseudolenguaje de definici´n de tipos s´lo indica la estructura de datos del tipo. Las operaciones o o son descritas aparte. Al implementar los tipos –sobre todo usando un lenguaje orientado a objetos– no hay que olvidar que las estructuras y las operaciones deben ir juntos, en el mismo m´dulo o clase. o
  • 1.2. Tipos de datos 13 tipo DiasMeses = array [1..12] de entero Pila[T] = registro datos: array [1..MAXIMO] de T tope, maximo: entero finregistro Sexo = enumerado (masculino, femenino) Para los arrays, se indica entre corchetes, [min..max], el rango m´ ınimo y m´ximo de a ındices del array5 . Para los registros, se listan sus atributos, cada uno con su nombre los ´ y tipo. Y, por otro lado, los enumerados se forman listando los valores del dominio. En algunos sitios usaremos tambi´n registros con variantes. Un registro con va- e riantes es un registro que contiene un campo especial, en funci´n del cual una variable o podr´ tener unos atributos u otros (pero nunca ambos a la vez). Por ejemplo, si en a una aplicaci´n que almacena empleados de una empresa, queremos almacenar diferente o informaci´n para los hombres y las mujeres podemos tener algo como lo siguiente: o tipo Empleado = registro nombre: cadena edad: entero seg´n sexo: Sexo u masculino: (direcci´n: cadena; sueldo: entero) o femenino: (salario: entero; tel´fono: entero) e finseg´nu finregistro En cuanto a los tipos colecciones, adem´s de los arrays daremos por supuesto que a disponemos de los siguientes: Listas. Tendremos listas parametrizadas, Lista[T], y que guardan una posici´n actu- o al. De esta forma, la inserci´n, eliminaci´n, etc., se hacen siempre sobre la posici´n o o o actual. Por conveniencia, en algunos sitios no se dar´n por supuestas las listas. a Pilas. Una pila es una lista LIFO: last in, first out (´ltimo en entrar, primero en u salir). Suponemos el tipo gen´rico Pila[T], con las operaciones push, pop y tope. e Colas. Una cola es una lista FIFO: first in, first out (primero en entrar, primero en salir). Suponemos el tipo gen´rico Cola[T], con las operaciones meter, sacar y cabeza. e Por ultimo, pero no menos importante, tenemos el tipo de datos puntero. Consi- ´ derado como un tipo non-grato por algunos autores6 por tratarse de un concepto cercano al bajo nivel, lo cierto es que los punteros son esenciales en la definici´n de estructuras de o datos. Los punteros son un tipo parametrizado. Una variable de tipo Puntero[T] contiene una referencia, o direcci´n de memoria, donde se almacena una variable de tipo T. o 5 Ojo, hay que tener en cuenta que en lenguajes como C/C++ el ´ ındice m´ınimo es siempre 0, as´ que ı s´lo se indica el tama˜o del array. o n 6 Por ejemplo, seg´n C.A.R. Hoare, “su introducci´n en los lenguajes de alto nivel fue un paso atr´s u o a del que puede que nunca nos recuperemos”. En el lenguaje Java se decidi´ no permitir el uso de punteros, o aunque dispone de referencias, que vienen a ser lo mismo.
  • 14 Cap´ ıtulo 1. Problemas, programas, estructuras y algoritmos En nuestro pseudolenguaje suponemos que tenemos el tipo Puntero[T] con las si- guientes operaciones y valores especiales: Operador de indirecci´n. Si a es de tipo Puntero[T], entonces la variable apuntada o por a se obtiene con el operador de indirecci´n flecha, ↑, esto es: a↑ es de tipo T. o Obtenci´n de la direcci´n. Si t es una variable de tipo T, entonces podemos o o obtener la direcci´n de esa variable con la funci´n PunteroA(t: T), que devuelve un o o puntero de tipo Puntero[T]. Puntero nulo. Existe un valor especial del tipo Puntero[T], el valor predefinido NULO, que significa que el puntero no tiene una referencia v´lida o no ha sido a inicializado. Creaci´n de una nueva variable. Para crear una nueva variable apuntada por un o puntero, suponemos la funci´n gen´rica Nuevo(T: Tipo), que devuelve un Puntero[T] o e que referencia a una nueva variable creada. Esta variable se borrar´ con la funci´n a o Borrar(a: Puntero[T]). Comparaci´n. Suponemos que hay definidos operadores de comparaci´n de igual- o o dad y desigualdad entre punteros (=, =). No consideramos otras operaciones sobre punteros, al contrario de lo que ocurre con C/C++, donde los punteros son consideramos como enteros, pudiendo aplicar sumas, restas, etc. Por esta raz´n, entre otras, decimos que lenguajes como C/C++ son lenguajes o d´bilmente tipados: existen pocas restricciones en la asignaci´n, mezcla y manipulaci´n e o o de tipos distintos. Como contraposici´n, los lenguajes fuertemente tipados, como Pas- o cal, tienen reglas m´s estrictas en el sistema de compatibilidad de tipos. En la pr´ctica, a a esta flexibilidad de C/C++ le proporciona una gran potencia pero es fuente de numerosos errores de programaci´n.o 1.3. Algoritmos y algor´ ıtmica Como ya hemos visto, los algoritmos junto con las estructuras de datos constituyen los dos elementos imprescindibles en el proceso de resoluci´n de problemas. Los primeros o definen el componente manipulador y los segundos el componente almacenado, y se com- binan estrechamente para crear soluciones a los problemas. No est´ de m´s recordar que la historia de los algoritmos es mucho anterior a la a a aparici´n de los ordenadores. De hecho, podr´ o ıamos datar la aparici´n de los algoritmos o al primer momento en que los seres humanos se plantearon resolver problemas de forma gen´rica. Hist´ricamente uno de los primeros algoritmos inventados, para la resoluci´n e o o de problemas de tipo matem´tico, es el algoritmo de Euclides de Alejandr´ Egipto, a ıa, propuesto alrededor del a˜o 300 a.C. Euclides propone el siguiente m´todo para calcular n e el m´ximo com´n divisor de dos n´meros enteros. a u u Ejemplo 1.3 Algoritmo de Euclides para calcular el m´ximo com´n divisor de dos a u n´meros enteros, a y b, siendo a > b. u
  • 1.3. Algoritmos y algor´ ıtmica 15 1. Hacer r := a m´dulo b o 2. Si r = 0 entonces el m´ximo com´n divisor es b a u 3. En otro caso: 3.1. Hacer a:= b 3.2. Hacer b:= r 3.3. Volver al paso 1 Por ejemplo, si aplicamos el algoritmo con a = 20 y b = 15, entonces r = 5. No se cumple la condici´n del paso 2 y aplicamos el paso 3. Obtenemos a = 15, b = 5. Volvemos o al punto 1, llegando a r = 0. Al ejecutar el paso 2 se cumple la condici´n, con lo que el o resultado es 5. Pero el honor de dar nombre al t´rmino algoritmo lo recibe el matem´tico arabe e a ´ del siglo IX Muhammad ibn Musa al-Khwarizmi (que significa algo as´ como Mo- ı hamed hijo de Mois´s de Khorezm, en Oriente Medio). Sus tratados sobre la resoluci´n e o de ecuaciones de primer y segundo grado ponen de relieve la idea de resolver c´lculos a matem´ticos a trav´s de una serie de pasos predefinidos. Por ejemplo, para resolver ecua- a e ciones de segundo grado del tipo: x2 + bx = c, define la siguiente serie de pasos7 . Ejemplo 1.4 Algoritmo de al-Khwarizmi para resolver la ecuaci´n x2 + bx = c. Se o 2 muestra el ejemplo x + 10x = 39, el mismo que al-Khwarizmi utiliza en su libro “Hisab al-jabr w’al-muqabala” (“El c´lculo de reducci´n y restauraci´n”), sobre el a˜o 825 d.C. a o o n 1. Tomar la mitad de b. (En el ejemplo, 10/2 = 5) 2. Multiplicarlo por s´ mismo. (En el ejemplo, 5 ∗ 5 = 25) ı 3. Sumarle c. (En el ejemplo, 25 + 39 = 64) √ 4. Tomar la ra´ cuadrada. (En el ejemplo, 64 = 8) ız 5. Restarle la mitad de b. (En el ejemplo, 8 − 10/2 = 8 − 5 = 3) El resultado es: (b/2)2 − c−b/2, expresado algor´ ıtmicamente. El matem´tico bag- a dad´ es tambi´n considerado como el introductor del sistema de numeraci´n ar´bigo en ı e o a occidente, y el primero en usar el n´mero 0 en el sistema posicional de numeraci´n. u o 1.3.1. Definici´n y propiedades de algoritmo o ¿Qu´ es un algoritmo? En este punto, el lector no deber´ tener ning´n problema para e ıa u dar su propia definici´n de algoritmo. Desde los ejemplos de algoritmos m´s antiguos – o a como los de Euclides y al-Khwarizmi, mostrados antes– hasta los m´s modernos algoritmos a –como los usados por el buscador Google o en compresi´n de v´ o ıdeo con DivX– todos ellos se caracterizan por estar constituidos por una serie de pasos, cuya finalidad es producir unos datos de salida a partir de una entrada. Adem´s, para que ese conjunto de reglas tenga a sentido, las reglas deben ser precisas y terminar en alg´n momento. As´ pues, podemos u ı dar la siguiente definici´n informal de algoritmo. o Definici´n 1.4 Un algoritmo es una serie de reglas que dado un conjunto de datos o de entrada (posiblemente vac´ produce unos datos de salida (por lo menos una) y ıo) cumple las siguientes propiedades: 7 La notaci´n es adaptada, ya que al-Khwarizmi no utiliza s´ o ımbolos, expresiones ni variables, sino una descripci´n completamente textual. Por ejemplo, a b lo llama “las ra´ o ıces” y a c “las unidades”.
  • 16 Cap´ ıtulo 1. Problemas, programas, estructuras y algoritmos Definibilidad. El conjunto de reglas debe estar definido sin ambig¨edad, es decir, u no pueden existir dudas sobre su interpretaci´n. o Finitud. El algoritmo debe constar de un n´mero finito de pasos, que se ejecuta en u un tiempo finito y requiere un consumo finito de recursos. En la figura 1.3 se muestra una interpretaci´n de los algoritmos como cajas ne- o gras, donde se destaca la visi´n del algoritmo como algo que dado unos datos de entrada o produce una salida. La propiedad de definibilidad se refiere a los algoritmos “en papel”, que tambi´n son algoritmos si est´n expresados sin ambig¨edades. Un trozo de c´digo e a u o implementado en un lenguaje de programaci´n no tendr´ ambig¨edad, pero tambi´n de- o a u e ber´ tener una duraci´n finita para ser considerado un algoritmo. El problema general a o de demostrar la terminaci´n de un conjunto de reglas es un problema complejo y no o computable, es decir no puede existir ning´n programa que lo resuelva. u ENTRADA SALIDA ENTRADA SALIDA palabras a ALGORITMO clave ALGORITMO DE conjunto mcd(a,b) DE EUCLIDES idioma BUSCADOR WEB de páginas b conjunto resultantes de páginas Figura 1.3: Interpretaci´n de los algoritmos como cajas negras. o No todo lo que aparece en inform´tica son algoritmos. Un ejemplo t´ a ıpico de algo que no es un algoritmo es un sistema operativo. El sistema operativo consta de una serie de instrucciones, pero no se le prev´ una ejecuci´n finita, sino que deber´ poder ejecutarse e o ıa mientras no se requiera lo contrario. Algoritmos deterministas y no deterministas Seg´n un algoritmo produzca o no siempre los mismos valores de salida para las u mismas entradas, clasificamos los algoritmos en dos tipos: Algoritmos deterministas. Para los mismos datos de entrada producen siempre los mismos datos de salida. Algoritmos no deterministas. Para los mismos datos de entrada pueden producir diferentes salidas. El no determinismo puede chocar un poco con la idea de definibilidad. Sin embar- go, ambas propiedades no son contradictorias. Por ejemplo, el resultado de un algoritmo paralelo, que se ejecuta en distintos procesadores, puede depender de cuestiones de tem- porizaci´n que no forman parte de los datos del problema. o La existencia del no determinismo hace que un algoritmo no siempre se pueda ver como una funci´n –en sentido matem´tico– de la forma f : ENTRADA → SALIDA. o a
  • 1.3. Algoritmos y algor´ ıtmica 17 Definici´n formal de algoritmo o Donald E. Knuth, uno de los grandes pioneros en el estudio de los algoritmos y la programaci´n, propone la siguiente definici´n formal de algoritmo. o o Definici´n 1.5 Un m´todo de c´lculo es una cuaterna (Q, I, W, f ), en la cual: o e a I es el conjunto de estados de entrada; W es el conjunto de estados de salida; Q es el conjunto de estados de c´lculo, que contiene a I y W ; y a f es una funci´n: f : Q → Q, con f (w) = w; ∀ w ∈ W . o Definici´n 1.6 Una secuencia de c´lculo es una serie de estados: x0 , x1 , x2 , ..., xn , ..., o a donde x0 ∈ I y ∀k ≥ 0; f (xk ) = xk+1 . Se dice que la secuencia de c´lculo acaba en n pasos a si n es el menor entero con xn ∈ W . Una definici´n formal como esta nos puede servir para comprobar, de manera ri- o gurosa, si un algoritmo cumple las propiedades de finitud y definibilidad. Sin embargo, intentar resolver un problema definiendo la funci´n f puede resultar una tarea bastante o compleja. Y, lo que es peor, el resultado puede que nos aporte pocas ideas sobre c´mo o implementar el algoritmo en un lenguaje de programaci´n. En lo sucesivo, intentaremos o seguir una visi´n m´s pr´ctica, pero formalizando en aquellos sitios donde sea conveniente. o a a La disciplina de la algor´ ıtmica Si bien, como hemos visto, la historia de los algoritmos se remonta muy atr´s, el estu- a dio de los algoritmos no se concibi´ como una disciplina propia hasta bien entrada la mitad o del pasado siglo XX. Actualmente, entendemos la algor´ ıtmica como la disciplina, dentro del ´mbito de la inform´tica, que estudia t´cnicas para construir algoritmos eficientes y a a e t´cnicas para medir la eficiencia de los algoritmos. En consecuencia, la algor´ e ıtmica consta de dos grandes ´reas de estudio: el an´lisis y el dise˜o de algoritmos. a a n Pero, ¿cu´l es el objetivo ultimo de la algor´ a ´ ıtmica?, ¿qu´ es lo que motiva su estudio? e El objetivo ultimo es dado un problema concreto ser capaz de resolverlo de la mejor forma ´ posible, de forma r´pida, corta, elegante y f´cil de programar. Y recordemos que en esta a a resoluci´n entran tambi´n en juego las estructuras de datos, que son manejadas por los o e algoritmos. En definitiva, todos los ejemplos, t´cnicas, m´todos y esquemas que vamos e e a estudiar tienen como finalidad ultima servir de herramientas utiles en el momento de ´ ´ afrontar la resoluci´n de un problema completamente nuevo y desconocido. o 1.3.2. An´lisis de algoritmos a El an´lisis de algoritmos es la parte de la algor´ a ıtmica que estudia la forma de medir la eficiencia de los algoritmos. Pero ¿cu´ndo decimos que un algoritmo es m´s o menos a a eficiente? Utilizando un punto de vista empresarial, podemos definir la eficiencia como la relaci´n entre recursos consumidos y productos obtenidos. Se trata, por lo tanto, de una o
  • 18 Cap´ ıtulo 1. Problemas, programas, estructuras y algoritmos nueva formulaci´n de la bien conocida ley del m´ o ınimo esfuerzo: maximizar los resultados, minimizando el esfuerzo. Los resultados que ofrece un algoritmo pueden ser de distintos tipos: Un algoritmo puede resolver el problema de forma muy precisa, si es de tipo mate- m´tico, o garantizar el optimo, en problemas de optimizaci´n, o encontrar siempre a ´ o la soluci´n, si es de satisfacci´n de restricciones. o o Otro algoritmo puede ser que s´lo encuentre soluciones aproximadas, o m´s o menos o a cercanas al optimo, pero siempre encuentre alguna. ´ Finalmente, otro algoritmo puede que encuentre soluciones s´lo en determinados o casos, buenas o malas, y en otros casos acabe sin devolver ninguna respuesta. Por otro lado, los recursos que consume un algoritmo pueden ser de distintos tipos. Entre los m´s importantes tenemos: a Tiempo de ejecuci´n, desde el inicio del programa hasta que acaba. o Utilizaci´n de memoria principal. o N´mero de accesos a disco, a la red o a otros perif´ricos externos. u e N´mero de procesadores usados, en el caso de los algoritmos paralelos, etc. u En aplicaciones distintas puede que el recurso cr´ ıtico sea diferente. En la mayor´ ıa de los casos, el recurso clave es el tiempo de ejecuci´n, por lo que muchas veces se asocia o eficiencia con tiempo. Pero, recordemos, ni el tiempo es el unico recurso, ni el tiempo por ´ s´ solo mide la eficiencia, si no que se debe contrastar con los resultados obtenidos. ı En conclusi´n, un algoritmo ser´ mejor cuanto m´s eficiente sea. No obstante, el o a a sentido com´n nos dice que habr´n otros muchos criterios para decidir lo bueno que es un u a algoritmo. Por ejemplo, ser´ importante: que sea f´cil de entender y de programar, que a a sea corto en su extensi´n, que sea robusto frente a casos extra˜os, que se pueda reutilizar o n en otros problemas, que se pueda adaptar, etc. Factores en la medici´n de recursos o Supongamos el siguiente algoritmo sencillo, que realiza una b´squeda secuencial con u centinela dentro de un array de enteros. La cuesti´n que se plantea es ¿cu´ntos recursos, o a de tiempo de ejecuci´n y memoria, consume el algoritmo? o operaci´n BusquedaSec (x, n: entero; a: array [1..n+1] de entero): entero o i:= 0 a[n+1]:= x repetir i:= i + 1 hasta a[i] = x devolver i Por muy perdido que ande el alumno, dif´ ıcilmente habr´ contestado algo como “tarda a 3 segundos” o “requiere 453 Kbytes...”. Como m´ ınimo, est´ claro que el tiempo y la a
  • 1.3. Algoritmos y algor´ ıtmica 19 memoria dependen de n, de los datos de entrada, del procesador, de que estemos grabando un CD al mismo tiempo y de otras mil cosas m´s. Podemos clasificar todos los factores a que intervienen en el consumo de recursos en tres categor´ ıas. Factores externos. No indican nada relevante sobre las caracter´ ısticas del algo- ritmo; son, m´s bien, cuestiones externas al algoritmo, relacionadas con el entorno a en el que se implementa y ejecuta. Entre los principales factores externos tenemos: el ordenador donde se ejecuta el algoritmo, el uso de procesador por parte de otras tareas, el sistema operativo, el lenguaje de programaci´n usado, el compilador (in- o cluyendo las opciones de compilaci´n usadas), la implementaci´n concreta que se o o haga del algoritmo, etc. Tama˜ o del problema. El tama˜o es el volumen de datos de entrada que debe n n manejar el algoritmo. Normalmente, el tama˜o viene dado por uno o varios n´meros n u enteros. La relaci´n del tiempo con respecto al tama˜o s´ que resulta interesante o n ı sobre las caracter´ ısticas del algoritmo. Contenido de los datos de entrada. Para un tama˜o de entrada fijo y bajo las n mismas condiciones de ejecuci´n, un algoritmo puede tardar distinto tiempo para o diferentes entradas. Hablamos de diferentes casos: mejor caso, es el contenido de los datos de entrada que produce la ejecuci´n m´s r´pida del algoritmo (incluso o a a para tama˜os de entrada grandes); peor caso, es el que da lugar a la ejecuci´n n o m´s lenta para un mismo tama˜o; caso promedio, es el caso medio de todos los a n posibles valores de entrada. Por ejemplo, en el anterior algoritmo de b´squeda secuencial con centinela, el tama˜o u n del problema viene dado por n. No es lo mismo aplicar el algoritmo con n = 5 que con n = 5.000.000. Pero la ejecuci´n con n = 5.000.000 puede tardar menos que con n = 5, si el o x buscado se encuentra en la primera posici´n. As´ pues, el mejor caso ser´ que a[1] = x, o ı a independientemente de lo que valga n. El peor caso ser´ que x no se encuentre en el array, a con lo cual la b´squeda acabar´ al llegar a a[n + 1]. El caso promedio ser´ la media de u a ıa todas las posibles entradas. Supongamos que x est´ en a con probabilidad p y que, en a caso de estar, todas las posiciones tienen la misma probabilidad. Entonces se recorrer´n a n/2 posiciones con probabilidad p, y n + 1 posiciones con probabilidad 1 − p. Notaciones en el an´lisis de algoritmos a Nos interesa que el an´lisis de los recursos consumidos por un algoritmo sea inde- a pendiente de los factores externos. En consecuencia, el objetivo es estudiar la variaci´n o del tiempo y la memoria utilizada, en funci´n del tama˜o de la entrada y, posiblemente, o n para los casos mejor, peor y promedio, en caso de ser distintos. Esta dependencia se expresa como una o varias funciones matem´ticas. El tiempo a ser´ normalmente una funci´n t : N → R , y de forma parecida la memoria m : N → R+ . a o + Pero si el algoritmo tarda distinto tiempo para la misma n, entonces no se puede decir que t sea una funci´n de N en R+ . En ese caso, definimos los tiempos en los casos mejor, o peor y promedio; denotaremos estos tiempos como tm , tM y tp , respectivamente. En concreto, ¿qu´ se mide con t(n)? Pues lo que m´s nos interese. e a
  • 20 Cap´ ıtulo 1. Problemas, programas, estructuras y algoritmos Podemos medir tiempos de ejecuci´n, en unidades de tiempo, asignando cons- o tantes indefinidas a cada instrucci´n o l´ o ınea de c´digo. En el algoritmo de b´squeda o u secuencial, tendr´ıamos los siguientes tiempos: tm (n) = c1 + c2 + c4 + c5 + c6 ; tM (n) = c1 + c2 + c6 + (c4 + c5 )(n + 1); tp (n) = c1 + c2 + c6 + (c4 + c5 )(1 + (1 − p)n + p n/2) Siendo cada ci el tiempo de ejecuci´n de la instrucci´n de la l´ o o ınea i-´sima. e Tambi´n podemos medir simplemente el n´ mero de instrucciones. Tendr´ e u ıamos: tm (n) = 5; tM (n) = 5 + 2n; tp (n) = 5 + 2((1 − p)n + p n/2) No todas las instrucciones tardan el mismo tiempo, pero sabemos que todas ellas tardan como m´ximo un tiempo que est´ acotado por una constante. a a Puede que lo que nos interese medir sea unicamente alg´ n tipo de instrucci´n, ´ u o que sabemos que es la m´s relevante. Por ejemplo, si contamos el n´mero de com- a u paraciones en el algoritmo de b´squeda secuencial tenemos: u tm (n) = 1; tM (n) = 1 + n; tp (n) = 1 + (1 − p/2)n Las tres versiones de cada una de las funciones tm , tM y tp anteriores, nos vienen a decir lo mismo: en el mejor caso el tiempo es constante, en el peor es proporcional a n, y en promedio depende de n y de p. Pero seguro que la ultima forma, contando s´lo ´ o comparaciones, se puede interpretar m´s f´cilmente, porque es la m´s sencilla. a a a Para simplificar las expresiones de t(n) usamos las notaciones asint´ticas. Las no- o taciones asint´ticas son notaciones que s´lo tienen en cuenta el crecimiento asint´tico o o o de una funci´n –es decir, para valores tendiendo a infinito– y son independientes de las o constantes multiplicativas. Tenemos las siguientes notaciones: O-grande: orden de complejidad. Establece una cota superior de la funci´n, o asint´ticamente e independiente de constantes multiplicativas. o Ω: omega. Establece una cota inferior, asint´tica e independiente de constantes. o Θ: orden exacto. Se usa cuando la cota inferior y superior de una funci´n coinciden. o o-peque˜ a. Es similar a la notaci´n Θ, pero teniendo en cuenta el factor que n o multiplica al t´rmino de mayor grado. e Podemos encontrar diferentes utilidades a las notaciones asint´ticas: o Simplificar la notaci´n de una f´rmula con expresi´n larga y complicada. Por o o √o ejemplo, dado un t(n) = 2n2 + 7,2n log3 n − 21πn + 2 n + 3, podemos quedarnos simplemente con que t(n) ∈ Θ(n2 ), o tambi´n t(n) ∈ o(2n2 ). e Acotar una funci´n que no se puede calcular f´cilmente. Esto puede ocurrir en o a algunos algoritmos cuyo comportamiento es dif´ de predecir. Supongamos, por ıcil ejemplo, la funci´n t(n) = 8n5−cos(n) , similar a la mostrada en la figura 1.4a). Se o puede acotar superiormente con t(n) ∈ O(n6 ) e inferiormente con t(n) ∈ Ω(n4 ).
  • 1.3. Algoritmos y algor´ ıtmica 21 Delimitar el rango de variaci´n del tiempo de un algoritmo, cuando no siem- o pre tarda lo mismo. Por ejemplo, en el algoritmo de b´squeda secuencial tenemos u distintos casos mejor y peor. Podemos decir que tiene un O(n) y un Ω(1). En la figura 1.4 se muestran dos ejemplos de aplicaci´n de las notaciones asint´ticas, o o para los dos ultimos casos. ´ a) t(n)ÎO(f(n)); t(n)ÎW(g(n)) b) t(n)ÎO(tM(n)); t(n)ÎW(tm(n)) 150 15 125 12.5 f(n) tM(n) 100 10 t(n) t(n) 75 t(n) 7.5 50 5 25 2.5 g(n) tm(n) 0 0 0 2 4 6 8 10 0 20 40 60 80 100 n n Figura 1.4: Distintas aplicaciones de las notaciones asint´ticas. a) Para acotar una funci´n o o oscilante o dif´ de calcular con precisi´n. b) Para acotar un algoritmo que no siempre ıcil o tarda lo mismo para el mismo n. Las notaciones asint´ticas pueden servir tambi´n para comparar f´cilmente los tiem- o e a pos de dos algoritmos. Pero hay que tener en cuenta sus limitaciones, derivadas de la inde- pendencia de las constantes y de los t´rminos de menor grado. Por ejemplo, un algoritmo e 0,01 puede tener un tiempo t1 (n) = 10n + 2 y otro t2 (n) = 2 log2 n + 30. Las notaciones nos dicen que O(log n) < O(n0,01 ). Sin embargo, ¡t1 (n) s´lo ser´ mayor que t2 (n) a partir de o a 216 valores de n mayores que 4,5 ∗ 10 ! 1.3.3. Dise˜ o de algoritmos n El dise˜o de algoritmos es la parte de la algor´ n ıtmica que estudia t´cnicas generales e de construcci´n de algoritmos eficientes. El objetivo no es, por lo tanto, estudiar unos o cuantos algoritmos dados, sino desarrollar t´cnicas generales que se pueden aplicar sobre e diversos tipos de problemas. Por ejemplo, divide y vencer´s no es un algoritmo, sino una a t´cnica que puede ser aplicada sobre una amplia variedad de problemas. e En primer lugar, cada t´cnica de dise˜o de algoritmos propone una interpretaci´n e n o particular del problema, es decir, una forma de ver o modelar el objetivo del problema. Esta interpretaci´n consta de una serie de elementos gen´ricos, que deben ser concre- o e tados en cada caso. Por ejemplo, divide y vencer´s interpreta que el problema trabaja a con una serie de datos, que se pueden dividir para tener problemas m´s peque˜os. Los a n elementos gen´ricos de la t´cnica ser´ e e ıan: la manera de hacer la divisi´n de los datos, la o forma de juntar las soluciones, una forma de resolver casos de tama˜o peque˜o, etc. Los n n componentes pueden ser tanto operaciones como estructuras de datos.
  • 22 Cap´ ıtulo 1. Problemas, programas, estructuras y algoritmos Habiendo resuelto los componentes gen´ricos del problema, la t´cnica propone un e e esquema algor´ ıtmico, que es una gu´ de c´mo combinar los elementos para cons- ıa o truir algoritmos basados en esa t´cnica. El esquema algor´ e ıtmico puede ser m´s o menos a detallado. Idealmente, el esquema algor´ıtmico ser´ como un “algoritmo con huecos”, co- ıa rrespondientes a los elementos gen´ricos. El programador simplemente deber´ insertar el e ıa c´digo y los tipos adecuados en los huecos que correspondan, y ya tendr´ un algoritmo o ıa que resuelve el problema. La idea se muestra en la figura 1.5. ALGORITMO Voraz (C: ConjuntoCandidatos ; var S: ConjuntoSolución ) S:= Ø mientras (C ≠ Ø) Y NO SOLUCION(S) hacer Insertar tipos AQUÍ x:= SELECCIONAR(C) C:= C - {x} Insertar si FACTIBLE(S, x) entonces código AQUÍ INSERTAR(S, x) finsi finmientras Figura 1.5: Interpretaci´n de los esquemas algor´ o ıtmicos como “algoritmos con huecos”. Esquema de algoritmo voraz. En la pr´ctica, esta idea de construir algoritmos “rellenando huecos de un esquema” a no suele ser tan simple ni extendida, por varios motivos. En primer lugar, la variedad de situaciones que nos podemos encontrar en distintos problemas es, normalmente, mucho mayor que la prevista en los esquemas de algoritmos. Pero, por otro lado, en esquemas excesivamente flexibles el problema suele ser la ineficiencia; un algoritmo escrito desde cero para un problema particular puede incluir c´digo optimizado para ese caso dado. De o todas formas, los esquemas algor´ ıtmicos son una ayuda muy util en la construcci´n de ´ o algoritmos, por lo menos para la obtenci´n de una primera versi´n. o o Otra cuesti´n fundamental en la resoluci´n de problemas es decidir qu´ t´cnica se o o e e puede aplicar en cada caso. Aunque puede que ocurra en un examen, en un problema de la vida real nadie nos dir´ “progr´mame esto utilizando backtracking” o “calcula aquello a a con programaci´n din´mica”; el que lo resuelve debe decidir tambi´n c´mo. Puede que se o a e o puedan aplicar varias t´cnicas, que alguna de ellas se pueda aplicar de diferentes modos, e que distintas t´cnicas den diferentes resultados –por ejemplo, una aproxima la soluci´n y e o otra encuentra el ´ptimo– o que no se pueda aplicar ninguna t´cnica. En el ultimo caso, o e ´ entrar´ en juego la habilidad de la persona para enfrentarse a situaciones nuevas. a Vamos a adelantar algunas de las principales t´cnicas generales de dise˜o de al- e n goritmos, que veremos a lo largo de este libro. Se supone que el alumno est´ un poco a familiarizado con alguna de ellas –como divide y vencer´s, o backtracking–, aunque no a resulta imprescindible.
  • 1.3. Algoritmos y algor´ ıtmica 23 Divide y vencer´s a La t´cnica de divide y vencer´s aporta una idea muy general: para resolver un e a problema primero div´ ıdelo en partes, luego resu´lvelas por separado y finalmente junta e las soluciones de cada parte. La idea es tan abierta que se podr´ aplicar casi en cualquier ıa ´mbito8 . En programaci´n, los subproblemas suelen ser del mismo tipo que el problema a o original, por lo que se resuelven aplicando recursividad. Cualquier recursividad necesita un caso base (o varios), que act´e como punto de u finalizaci´n. Los casos base en divide y vencer´s ser´n problemas de tama˜o peque˜o, o a a n n para los cuales se aplica una soluci´n directa. o Por lo tanto, para que se pueda aplicar divide y vencer´s: el problema se debe poder a descomponer en partes, las soluciones de las partes deben ser independientes, tiene que haber alg´n m´todo directo de resoluci´n y debe haber una manera de combinar las u e o soluciones parciales. Habr´ muchos casos donde no se cumplan estas condiciones. a Algoritmos voraces La interpretaci´n de un problema bajo la perspectiva de los algoritmos voraces o es b´sicamente la siguiente: tenemos un conjunto de candidatos y la soluci´n se construye a o seleccionando algunos de ellos en cierto orden. Con este modelo del problema, el esquema voraz propone que la soluci´n se construya paso a paso. Se empieza con una soluci´n o o vac´ En cada paso se selecciona un candidato y se decide si se a˜ade a la soluci´n o no. ıa. n o Una vez tomada una decisi´n (a˜adir o descartar un candidato) nunca se puede deshacer. o n El algoritmo acaba cuando tengamos una soluci´n o cuando no queden candidatos. o El esquema corresponde al viejo algoritmo de “comprar patatas en el mercado”: te- nemos un mont´n de patatas candidatas y una bolsa vac´ lo que hacemos es ir llenando o ıa; la bolsa con las mejores patatas, una a una, hasta completar 5 kilos. Los componentes gen´ricos que habr´ que estudiar en cada problema son: cu´les son los candidatos, qu´ cri- e ıa a e terio se usa para seleccionar un candidato del mont´n, el criterio para decir si se a˜ade el o n candidato seleccionado, el criterio de finalizaci´n, etc. o Por la propia estructura del algoritmo, se puede intuir que los algoritmos voraces ser´n normalmente bastante r´pidos. Como contrapartida, puede que no siempre encuen- a a tren soluci´n ´ptima, aunque algunos de ellos s´ que la garantizan. o o ı Programaci´n din´mica o a La programaci´n din´mica coincide con divide y vencer´s en la idea de definir o a a un problema recursivamente en base a subproblemas de menor tama˜o. Sin embargo, en n programaci´n din´mica no se utiliza la recursividad de forma directa. En su lugar, se o a resuelven primero los problemas de tama˜o peque˜o, anotando la soluci´n en una tabla. n n o Usando las soluciones de la tabla, vamos avanzando hacia problemas cada vez m´s grandes, a hasta llegar al problema original. La memoria ocupada por la tabla se convierte en uno de los factores cr´ ıticos de los algoritmos de programaci´n din´mica. o a 8 De hecho, la expresi´n “divide y vencer´s” se atribuye a Napole´n Bonaparte, que fue fiel a su o a o aplicaci´n en sus estrategias militares, hasta que en Waterloo, en 1815, se convirti´ en “divide y perder´s”. o o a
  • 24 Cap´ ıtulo 1. Problemas, programas, estructuras y algoritmos Para obtener la descomposici´n recursiva, el problema se puede interpretar como o una serie de toma de decisiones. Despu´s de cada decisi´n, nos queda un problema de e o menor tama˜o. Por ejemplo, el problema de comprar patatas podr´ interpretarse como: n ıa decidir para cada patata del mont´n si se toma o no (true o false). La decisi´n se hace o o resolviendo los subproblemas de usar el mont´n con una patata menos, suponiendo que o la otra ya se ha tomado o no9 . El resultado ser´ la mejor de ambas posibilidades. a Un algoritmo de programaci´n din´mica encontrar´ la soluci´n ´ptima a un pro- o a a o o blema si cumple el principio de optimalidad de Bellman: una secuencia de decisiones ´ptimas se forma uniendo subsecuencias ´ptimas. No en todos los problemas se puede o o aplicar el principio, en muchos de ellos es dif´ encontrar la definici´n recursiva y en ıcil o otros demasiado ineficiente. Backtracking La t´cnica de backtracking se puede ver como la aplicaci´n de una estrategia e o opuesta a los algoritmos voraces. Si en los algoritmos voraces nunca se deshacen las decisiones tomadas, en backtracking se pueden deshacer, quitar elementos que hemos a˜adido, probar otros. Y as´ hasta haber comprobado virtualmente todas las posibilidades. n ı En un algoritmo de backtracking, las soluciones al problema suelen representarse mediante tuplas (s1 , s2 , ..., sn ), donde cada si significar´ algo concreto y podr´ tomar a a ciertos valores. El esquema gen´rico de backtracking consiste b´sicamente en generar todos e a los posibles valores consistentes de la tupla anterior –haciendo un recorrido exhaustivo y sistem´tico– y quedarse con la optima o la que solucione el problema. a ´ El orden de generaci´n de los distintos valores de la tupla se puede representar o mediante un ´rbol –el ´rbol impl´ a a ıcito de backtracking, no necesariamente almacenado– donde la ra´ es el comienzo del algoritmo y los hijos de un nodo son las tuplas que se ız generan a partir de una soluci´n parcial. La t´cnica de backtracking es muy flexible y se o e puede aplicar sobre una gran variedad de problemas. Desafortunadamente, los algoritmos de backtracking suelen ser extremadamente costosos en tiempo de ejecuci´n. o Ramificaci´n y poda o En cierto sentido, la ramificaci´n y poda se puede ver como una generalizaci´n o o y mejora de backtracking. Realiza la misma interpretaci´n del problema, a trav´s del uso o e de una tupla soluci´n (s1 , s2 , ..., sn ), cuyos valores son generados mediante un recorrido o impl´ıcito en un ´rbol. Pero difiere de backtracking en dos aspectos. ¿Cu´les? a a Pues, obviamente, en la ramificaci´n y en la poda. En cuanto a la ramificaci´n, o o permite definir distintas estrategias para recorrer el arbol, es decir, determinar qu´ ramas ´ e explorar primero. En cuanto a la poda, define un proceso de eliminaci´n de nodos del o ´rbol por los cuales sabemos que no se encuentra la soluci´n ´ptima. a o o La t´cnica se basa en ir haciendo estimaciones, para cada soluci´n parcial, de la e o mejor soluci´n que podemos encontrar a partir de la misma. Si la mejor soluci´n que o o podemos obtener para un nodo no es mejor que lo que ya tengamos por otro lado, entonces ser´ posible eliminar ese nodo. a 9 Notar que un algoritmo voraz decidir´ si se usa cada patata de forma independiente. ıa
  • 1.3. Algoritmos y algor´ ıtmica 25 Estrategia minimax y poda alfa-beta M´s que una t´cnica general de dise˜o de algoritmos, la estrategia minimax es a e n una estrategia para la resoluci´n de problemas de juegos. Consideramos exclusivamente los o juegos de tablero, donde participan dos rivales –que llamamos A y B– de forma alternativa y no interviene el azar. Por ejemplo, dentro de esta categor´ podemos encontrar las tres ıa en raya, las damas, el ajedrez, el Nim, etc. La evoluci´n de todos los posibles estados del juego es representada mediante un o ´rbol. La ra´ representa el tablero inicial. Los hijos de la ra´ son todos los posibles a ız ız movimientos de A. Los hijos de estos son los movimientos de B, y as´ sucesivamente. Las ı hojas son situaciones donde acaba el juego y gana uno u otro jugador. La estrategia minimax hace un recorrido del arbol de juego, intentando plasmar una ´ idea bastante razonable: en los movimientos de A intenta ganar ´l y que pierda B, y en e los de B intentar´ ganar ´l y que pierda A. a e 1.3.4. Descripci´n del pseudoc´digo utilizado o o Las t´cnicas de dise˜o de algoritmos son independientes del lenguaje e incluso del e n paradigma de programaci´n que se utilice. Un algoritmo dise˜ado en papel puede ser o n implementado en Java, Pascal, Basic, C, Smalltalk o cualquier otro lenguaje. Por este motivo, en el estudio te´rico de los algoritmos y las estructuras de datos utilizaremos un o pseudoc´digo independiente del lenguaje de programaci´n10 . El pseudoc´digo usado es o o o bastante intuitivo y su traducci´n a un lenguaje concreto resulta directa. A continuaci´n o o vamos a comentar sus principales caracter´ ısticas. Para los tipos de datos elementales, o definidos por el usuario, se utiliza el formato presentado en la secci´n 1.2.3. Las funciones y procedimientos se definen con la palabra o operaci´n, seguida por el nombre, y la lista de los par´metros formales entre par´ntesis. o a e Si un par´metro se puede modificar, se indicar´ colocando var delante de su nombre. Si a a la operaci´n devuelve un valor, pondremos dos puntos, ’:’, y a continuaci´n el tipo del o o valor devuelto. Por ejemplo, podemos tener las siguientes cabeceras de funciones y procedimientos: operaci´n SuprimeDos (var C : Conjunto[T]; t, p: T) o operaci´n Miembro (C : Conjunto[T]; t: T): booleano o Normalmente, los nombres de las variables aparecer´n en cursiva (C , x) y las pala- a bras reservadas en negrita (operaci´n, var). o Incluimos en nuestro pseudolenguaje las siguientes instrucciones: Asignaci´n. Se expresa mediante ’:=’. Por ejemplo, C := 4+3. Para la comparaci´n o o se usa ’=’ y ’=’. Condicional. La instrucci´n tendr´ la forma: “si Condici´n entonces Acci´n finsi”. o a o o Tambi´n puede haber una acci´n que se ejecute en caso de no cumplirse la condici´n. e o o La notaci´n ser´ o ıa: si Condici´n entonces o Acci´nSiCierto o 10 Aunque, como se podr´ ver, la estructura general tiene ciertas similitudes con Pascal. a
  • 26 Cap´ ıtulo 1. Problemas, programas, estructuras y algoritmos sino Acci´nSiFalso o finsi Valor devuelto. En las funciones, se devolver´ el resultado con “devolver Valor”. a Iteradores. Utilizaremos los iteradores para, mientras y repetir. El formato ser´ co- a mo el siguiente: para v := Rango hacer mientras Condici´n hacer o repetir Acci´n o Acci´n o Acci´n o finpara finmientras hasta Condici´n o Dentro del c´digo se podr´n utilizar variables locales que no hayan sido definidas o a previamente. Normalmente estar´ claro el tipo de esas variables. En caso contrario, se a expresar´ la definici´n de la variable en una secci´n var, despu´s de la cabecera de la ope- a o o e raci´n y antes del c´digo. Tambi´n podr´n encontrarse operaciones que no est´n definidas o o e a e previamente, pero cuyo significado est´ bastante claro, como error(Texto) para indicar a un error o escribir(Texto) para mostrar un resultado por pantalla. Para los comentarios dentro del c´digo usaremos: // Comentario o (* Comentario *). o 1.4. Consejos para una buena programaci´n o Aun con la corta experiencia de programaci´n que se le supone al alumno, seguro o que habr´ vivido una situaci´n similar a la siguiente. Se empieza a escribir un programa, a o que previsiblemente no ser´ muy largo, as´ que se coloca todo dentro del procedimiento a ı principal. Poco a poco, se van a˜adiendo cosas, nuevos casos, m´s funcionalidad o un n a interfaz m´s completo. Cuando el programa principal es demasiado grande, se pasan a algunos trozos de c´digo como procedimientos. Pero no est´ claro d´nde se hace cada o a o cosa, mientras el programa contin´a creciendo. u Al final el c´digo se convierte en un gigante con pies de barro. Ocupa unas decenas o de p´ginas pero carece de una estructura l´gica clara. A posteriori se detectan cosas que se a o repiten y algunos trozos que deber´ modificarse. Pero tratar de hacerlo bien requerir´ ıan ıa tantos cambios que se va montando chapuza sobre chapuza. Pero eso no es nada. Cuando vuelves a retomar el programa, varios meses despu´s, resulta imposible saber d´nde, por e o qu´ y c´mo se hac´ cada cosa. Aun as´ lo intentas modificar. Pero el programa ya no e o ıa ı, compila; ni volver´ a hacerlo nunca m´s. a a Por lo menos, la experiencia habr´ servido para conocer qu´ se debe hacer y qu´ no, a e e para construir un programa de tama˜o mediano-grande. Vamos a ver, a continuaci´n, una n o serie de consejos pr´cticos que deber´ observar todo buen programador. a ıa 1.4.1. Importancia del an´lisis y dise˜o previos a n Usualmente, el gran error de los programadores inexpertos es empezar a teclear c´digo cuando ni si quiera se ha acabado de entender el enunciado de un problema. La o
  • 1.4. Consejos para una buena programaci´n o 27 programaci´n, como una disciplina ingenieril –frente a la programaci´n “como arte”11 – o o requiere plantearse de forma met´dica la construcci´n de programas. El inform´tico debe o o a hacer un dise˜o previo de sus programas, de la misma forma que un arquitecto hace los n planos antes de construir la casa. Ya hemos visto, en el punto 1.1, los pasos que deben aplicarse sistem´ticamente en a la resoluci´n de problemas. El estudio de los m´todos, procesos, t´cnicas y herramientas o e e para el an´lisis12 y dise˜o de programas es lo que constituye el area de investigaci´n de a n ´ o la ingenier´ del software. Aun sin haber estudiado ingenier´ del software, resulta ıa ıa siempre conveniente hacer un minucioso estudio previo del problema y de la soluci´n o propuesta, antes de empezar a programar. A nivel de an´lisis y dise˜o se trabajar´ con a n a conceptos abstractos, olvidando los detalles menos relevantes. Consejo pr´ctico a Para un futuro ingeniero en inform´tica, puede resultar un h´bito interesante tomar a a medidas cuantitativas de su propio proceso de desarrollo de programas. Para ello, se puede utilizar una tabla como la mostrada en la figura 1.6. Dentro de esta tabla, para cada proyecto concreto se va anotando la dedicaci´n temporal –en n´mero de minutos, o u por ejemplo– que se emplea en cada fase de desarrollo, desde el an´lisis hasta la validaci´n. a o Proyecto: Algoritmo de búsqueda secuencial con centinela Fecha de inicio: 3/5/2003 Programador: Ginés García Mateos Fecha de fin: 4/5/2003 Día/Mes Análisis Diseño Implement. Validación TOTAL 3/5 4 6 10 '' 3 5 6 14 4/5 2 9 11 TOTAL 4 9 7 15 35 (minutos) MEDIAS 11,4% 25,7% 20% 42,9% 100% (porcentaje) Figura 1.6: Tabla para contar la dedicaci´n temporal de una persona a un proyecto soft- o ware, en las diferentes fases de desarrollo. Las cantidades son minutos, medidos de forma aproximada. Muchas veces la distinci´n entre una u otra fase no est´ muy clara. De modo orien- o a tativo, podemos diferenciarlas de la siguiente forma: 11 “El proceso de escribir programas [...] puede llegar a ser una experiencia est´tica similar a componer e m´ sica o escribir poes´ Donald E. Knuth, The Art of Computer Programming, 1997. u ıa”, 12 No confundir an´lisis de problemas con an´lisis de algoritmos. Analizar un problema es estudiar sus a a necesidades y caracter´ısticas. Analizar un algoritmo es medir su consumo de recursos.
  • 28 Cap´ ıtulo 1. Problemas, programas, estructuras y algoritmos An´lisis. Cuenta desde que se empieza a leer el enunciado del problema. Incluye a la comprensi´n del problema, b´squeda de informaci´n (libros de teor´ apuntes, o u o ıa, manuales, c´digo reutilizado, etc.) y modelado conceptual del problema. o Dise˜ o. Dentro de esta fase podemos contar todo el desarrollo “en papel”. Incluye: n definir los bloques en los que se divide el problema, escribir un esquema de los algoritmos, simular mentalmente el esquema, etc. Implementaci´n. Abarca desde que comenzamos a teclear en el ordenador hasta o que se acaba el programa y se compila por primera vez, produzca fallos o no. Validaci´n. Empieza cuando se hace la primera compilaci´n. Incluye la correc- o o ci´n de errores de compilaci´n, la realizaci´n de pruebas, detecci´n de fallos en los o o o o resultados y su correcci´n. Acaba cuando se finaliza el proyecto. o Seguir un control detallado de este tipo puede tener varios objetivos. En primer lugar, sirve para ser conscientes del propio proceso de desarrollo de software, analizando a qu´ cosas se dedica proporcionalmente m´s y menos tiempo. En segundo lugar, puede ser e a util para estudiar comparativamente la evoluci´n del proceso a lo largo del tiempo. Esto ´ o podr´ utilizarse para detectar deficiencias o puntos d´biles. Finalmente, es interesante ıa e para extraer conclusiones fundadas: ¿hasta qu´ punto ayuda dedicar m´s tiempo a an´lisis e a a y dise˜o a reducir el tiempo de implementaci´n y validaci´n? ¿Es f´cil distinguir entre las n o o a distintas fases? ¿Qu´ fase requiere m´s tiempo y por qu´? ¿C´mo reducirlo? e a e o 1.4.2. Modularidad: encapsulaci´n y ocultamiento o El resultado de un buen dise˜o de software debe ser la definici´n de una estructura n o general de la aplicaci´n. Esta estructura consta de una serie de partes separadas, que se o relacionan entre s´ a trav´s de los interfaces definidos. Por lo tanto, resulta conveniente ı e utilizar los mecanismos que ofrezca el entorno de programaci´n para agrupar funcionali- o dades: m´dulos, paquetes, clases, unidades. o Un m´dulo, clase o paquete, agrupa un conjunto de funcionalidades relacionadas. o Esto es lo que llamamos encapsulaci´n: el m´dulo tiene un nombre descriptivo, bajo el o o cual se agrupan todas las utilidades relacionadas con el mismo. Por ejemplo, una clase Pila contiene tanto las estructuras de datos como las operaciones necesarias para manejar las pilas. La ventaja de encapsular es que sabemos d´nde se encuentra cada cosa. En caso de o haber fallos podemos encontrar qui´n es el responsable, y corregirlo. Si la funcionalidad e se utiliza en otras aplicaciones, s´lo tendremos que usar el mismo m´dulo. o o Los paquetes son, normalmente, un nivel de encapsulaci´n superior a los otros. Un o paquete puede contener varias clases o m´dulos relacionados. Por ejemplo, un paquete o Colecciones podr´ contener las clases Pila, Lista, Cola, Conjunto, etc. ıa Otro principio fundamental asociado a la modularidad es la ocultaci´n de la im- o plementaci´n. Cuando se crea un m´dulo, una clase o un paquete, se definen una serie o o de operaciones para manejar la funcionalidad creada. Esto es lo que se llama la interface del m´dulo, clase o paquete. Los usuarios del m´dulo deben utilizar las operaciones del o o interface y s´lo esas operaciones. Todo lo dem´s es inaccesible, est´ oculto. Es m´s, el o a a a
  • 1.4. Consejos para una buena programaci´n o 29 usuario s´lo conoce cu´l es el resultado de las operaciones, pero no c´mo lo hacen. Por o a o ejemplo, para el usuario de la clase Pila, el interface contiene las operaciones push, pop, etc., de las cuales conoce su significado, pero no c´mo se han implementado. o Consejo pr´ctico a El concepto de software se entiende normalmente como algo mucho m´s amplio que a simplemente un programa; incluye tanto el programa como el documento de requisitos del problema, el dise˜o realizado y las especificaciones de cada parte. Ser´ adecuado que todos n ıa estos documentos se fueran completando en las mismas fases en las que deben hacerse, y no despu´s de acabar la implementaci´n del programa. e o No estudiaremos aqu´ la forma de crear documentos de requisitos y dise˜os de soft- ı n ware, que caen dentro de las asignaturas de ingenier´ del software. El alumno puede ıa utilizar la notaci´n que le parezca m´s adecuada. En cuanto a la especificaci´n, en el o a o siguiente cap´ıtulo haremos un repaso de las especificaciones informales e introduciremos dos m´todos de especificaci´n formal. Es conveniente documentar siempre, con alguno de e o estos m´todos, todas las abstracciones (tipos y operaciones) que se desarrollen. e 1.4.3. Otros consejos Las recomendaciones anteriores tienen m´s sentido cuando se trata de desarrollar a aplicaciones de tama˜o medio o grande; aunque siempre deber´ aplicarse en mayor n ıan o menor medida. En este apartado vamos a ver algunos otros consejos m´s generales, a aplicables tanto a problemas peque˜os como de mayor tama˜o. n n Reutilizar programas existentes. Un nuevo programa no debe partir desde cero, como si no existiera nada fuera del propio desarrollo. La reutilizaci´n se puede dar a o distintos niveles. Se pueden reutilizar librer´ implementaciones de TAD, esquemas ıas, de programas, etc. Reutilizar implica no s´lo pensar qu´ cosas tenemos hechas que podamos volver a o e usar, sino tambi´n qu´ cosas que estamos haciendo podremos usar en el futuro. Una e e buena reutilizaci´n no se debe basar en “copiar y pegar” c´digo, sino en compartir o o m´dulos, clases o paquetes, en distintos proyectos. o Resolver casos generales. Si no supone un esfuerzo adicional, se deber´ intentar a resolver los problemas m´s generales en vez de casos particulares. Por ejemplo, a en lugar de implementar un tipo pilas de enteros, ser´ m´s util definir pilas que ıa a ´ puedan almacenar cualquier cosa. De esta manera, se favorece la posibilidad de que las funcionalidades desarrolladas se puedan reutilizar en el futuro. Repartir bien la funcionalidad. A todos los niveles (a nivel de paquetes, de m´dulos y de procedimientos) se debe intentar repartir la complejidad del problema o de manera uniforme. De esta forma, hay que evitar crear procedimientos muy largos y complejos. En su lugar, ser´ mejor detectar los componentes que lo forman y usar ıa subrutinas. Un procedimiento corto, bien estructurado y tabulado, usando funciones y variables con nombres descriptivos adecuados, resulta m´s legible y, por lo tanto, a
  • 30 Cap´ ıtulo 1. Problemas, programas, estructuras y algoritmos m´s f´cil de comprender y mantener. De forma similar, si un m´dulo es muy grande, a a o puede que estemos mezclando funcionalidades separadas y sea mejor crear dos o m´s a m´dulos. o Simplificar. No es mejor un programa cuanto m´s largo y complejo, sino m´s a a bien todo lo contrario. Una soluci´n corta y directa es, a menudo, la soluci´n m´s o o a elegante y adecuada. Una soluci´n enrevesada suele incluir c´digo repetido, casos o o innecesarios, desperdicio de memoria y falla con m´s facilidad. Para simplificar se a debe aplicar la l´gica y el sentido com´n. Por ejemplo, suponiendo una funci´n que o u o devuelve un booleano, es muy t´ ıpico encontrar cosas del siguiente estilo o similares: si Condici´n = verdadero entonces o devolver verdadero sino devolver falso finsi Toda la construcci´n anterior no es m´s que un largo rodeo para decir simplemente: o a devolver Condici´n o O, por ejemplo, cuando tenemos una variable i que debe ir oscilando entre dos valores, pongamos por caso los valores 1 y 2, tambi´n se suelen hacer an´lisis de e a casos if-else. El problema se soluciona con una simple asignaci´n del tipo: i:= 3-i. o Consejo pr´ctico a No perdamos de vista que el inter´s principal es que los programas desarrollados e sean correctos y eficientes. Dentro de eso, resulta muy conveniente que se cuiden los otros criterios de una buena programaci´n: legibilidad, simplicidad, reparto de la funcionalidad, o reutilizaci´n. La mejor forma de evaluarse uno mismo en estos par´metros es tomar me- o a didas cuantitativas y que sean significativas, al estilo de lo que se propone en el apartado 1.4.1. En la figura 1.7 se propone un formato para el tipo de informaci´n que ser´ in- o ıa teresante recoger en cada proyecto realizado. El n´mero de operaciones de la interface se u refiere a las operaciones que son accesibles fuera del m´dulo o clase. Las privadas son las o que no son accesibles desde fuera. Por simplicidad, la longitud del c´digo se debe expresar o en una unidad que sea f´cil de medir, como por ejemplo l´ a ıneas de c´digo. Por otro lado, o dentro del campo reutilizado se indica si el m´dulo ha sido reutilizado de un proyecto o anterior, o si se han reutilizado partes con modificaciones. Est´ claro que una tabla como la de la figura 1.7 s´lo tendr´ sentido cuando el a o a programa est´ completamente acabado y adem´s sea correcto. Una vez con la tabla, cabr´ e a ıa plantearse diferentes cuestiones. ¿Es uniforme el reparto de operaciones entre los m´dulos? o ¿Hay m´dulos que destaquen por su tama˜o grande o peque˜o? ¿Est´ justificado? ¿Es o n n a razonable la longitud media de las operaciones? ¿Hay alguna operaci´n demasiado larga? o ¿Se ha reutilizado mucho o poco? E, intentado ir un poco m´s all´, ¿existe alg´n otro a a u par´metro de calidad que hayamos encontrado, pero que no est´ recogido en la tabla? En a a ese caso, ¿c´mo se podr´ cuantificar? o ıa
  • Ejercicios propuestos 31 Proyecto: Algoritmo ficticio Fecha de inicio: 5/5/2003 Programador: Ginés García Mateos Fecha de fin: 7/5/2003 Nombre del Número de operaciones Líneas de Reutili Líneas por operación Módulo/Clase Interface Privadas Total código -zada Min Max Media Pilas 4 2 6 53 no 5 13 8,8 Principal 1 3 4 39 no 7 24 9,8 TOTAL 5 5 10 92 0 5 24 9,2 MEDIAS 2,5 2,5 5 46 0 6 18,5 9,2 Figura 1.7: Tabla para tomar diferentes medidas relacionadas con la calidad del software. Ejercicios propuestos Ejercicio 1.1 Busca uno o varios programas –preferiblemente de tama˜o m´s o menos n a grande– que hayas escrito con anterioridad. Rellena una tabla como la de la figura 1.7. Si el programa no utiliza m´dulos, unidades o clases, intenta agrupar las operaciones por fun- o cionalidad. Extrae conclusiones de los resultados obtenidos, respondiendo las cuestiones que se plantean al final del apartado 1.4.3. Haz una valoraci´n cr´ o ıtica y comparativa (res- pecto a alg´n compa˜ero o respecto a otros programas). u n Consejo: Para facilitar el rellenado de la tabla, es adecuado utilizar alguna herramienta como una hoja de c´lculo. Normalmente, en este tipo de tablas las celdas claras las intro- a duce el usuario y las oscuras se calculan autom´ticamente a partir de otras (aunque en el a ejemplo no siempre es as´ ı). Ejercicio 1.2 A partir de ahora, para los siguientes problemas de tama˜o mediano que n resuelvas, intenta hacer un seguimiento del tiempo que dedicas, utilizando una tabla como la de la figura 1.6. Igual que antes, se aconseja automatizarlo usando una hoja de c´lculo. a Estos documentos, junto con las especificaciones y las medidas de calidad de la tabla 1.7, deber´ acompa˜ar la documentaci´n del programa final. ıan n o Ejercicio 1.3 Anota en una hoja todos los tipos de datos, estructuras y t´cnicas de e dise˜o que conozcas. Para cada tipo indica las variantes que puede tener y las formas n de implementarlos. Por ejemplo, se supone que debes conocer el tipo lista, que pueden ser circulares, enlazadas simple o doblemente, se pueden implementar con punteros o con arrays, etc. Cuestiones de autoevaluaci´n o Ejercicio 1.4 A lo largo de todo el cap´ ıtulo, se hace ´nfasis en lo importante que resulta e no empezar el desarrollo de programas directamente tecleando c´digo. ¿Est´s de acuerdo o a con esta afirmaci´n? ¿Qu´ otras cosas deber´ hacerse antes? ¿En qu´ fases? o e ıan e
  • 32 Cap´ ıtulo 1. Problemas, programas, estructuras y algoritmos Ejercicio 1.5 ¿Qu´ relaci´n existe entre los algoritmos y las estructuras de datos? ¿Cu´l e o a va primero? ¿Cu´l es m´s importante en la resoluci´n de problemas? ¿Qu´ papel juega a a o e cada uno? Ejercicio 1.6 ¿Cu´les son las diferencias entre tipo abstracto de datos, tipo de datos y a estructura de datos? ¿Son conceptos redundantes? Por ejemplo, una lista ¿qu´ es? e Ejercicio 1.7 Enumera las propiedades fundamentales de un algoritmo. Dado un trozo de c´digo, ¿se pueden demostrar formalmente las propiedades? ¿Y si es pseudoc´digo? o o Un programa de edici´n de texto, al estilo Word, ¿se puede considerar un algoritmo? ¿En o qu´ se diferencian un algoritmo, un programa, un problema y una aplicaci´n? e o Ejercicio 1.8 En el an´lisis de algoritmos, identificamos distintos factores que influyen a en el consumo de recursos. Algunos resultan de inter´s y otros no. Dist´ e ınguelos y justifican por qu´ se consideran o no interesantes. e Ejercicio 1.9 Todas las notaciones asint´ticas (excepto la o-peque˜a) son independien- o n tes de constantes multiplicativas. Comprueba que los factores externos estudiados en el consumo de recursos de un algoritmo solo influyen en t´rminos multiplicativos. e Ejercicio 1.10 ¿Para qu´ sirve un esquema algor´ e ıtmico? ¿En qu´ se diferencia de un e algoritmo? ¿Cumple las propiedades de definibilidad y finitud? ¿Por qu´ no? Enumera los e distintos esquemas algor´ ıtmicos que conozcas. Referencias bibliogr´ficas a Antes de entrar de lleno en el contenido del libro, ser´ adecuado repasar los concep- ıa tos fundamentales que el alumno debe conocer de programaci´n de primer curso. Para ello o la mejor referencia es, sin duda, la que se usara entonces. Algunos libros de algoritmos y estructuras de datos incluyen cap´ ıtulos de repaso de los tipos fundamentales –listas, pilas, colas y ´rboles– como los cap´ a ıtulos 2 y 3 de [Aho88], y los cap´ ıtulos 3 y 4 de [Weis95], y el libro [Wirth80]; adem´s de las definiciones relacionadas con estructuras y algoritmos, a en los cap´ ıtulos iniciales. El proceso de resoluci´n de problemas, planteado en el primer punto, es una versi´n o o resumida y simplificada del ciclo cl´sico de desarrollo de software. Pocos libros de la a bibliograf´ inciden en este tema, que est´ m´s relacionado con el ambito de la ingenier´ ıa a a ´ ıa del software. Una breve introducci´n a estos temas, as´ como a otros conceptos tratados o ı en este libro, se pueden consultar en el cap´ ıtulo 1 de [Aho88]. Algunos de los consejos de programaci´n de la secci´n 1.4, han sido extra´ o o ıdos del apartado 1.6 de [Aho88]. Las tablas de las figuras 1.6 y 1.7 para medir el proceso personal de desarrollo de software, son una adaptaci´n del proceso propuesto en [Humphrey01]. Son o una simplificaci´n, ya que este libro est´ m´s orientado hacia las cuestiones de ingenier´ o a a ıa del software. Adem´s de la bibliograf´ en papel, que se detallar´ en cada uno de los cap´ a ıa a ıtulos sucesivos, Internet resulta una valiosa fuente de informaci´n adicional. Por ejemplo, un o
  • Referencias bibliogr´ficas a 33 completo compendio de todos los temas relacionados con los algoritmos y las estructuras de datos es el “Dictionary of Algorithms and Data Structures”: http://www.nist.gov/dads/ Adem´s de la documentaci´n escrita, se pueden encontrar proyectos dirigidos a crear a o animaciones interactivas de algoritmos y estructuras de datos. Un portal interesante con numerosos enlaces es: http://www.cs.hope.edu/~alganim/ccaa/
  • 34 Cap´ ıtulo 1. Problemas, programas, estructuras y algoritmos
  • Cap´ ıtulo 2 Abstracciones y especificaciones Una abstracci´n es una simplificaci´n de un objeto o fen´meno del mundo real. Las o o o abstracciones son b´sicas en la construcci´n de aplicaciones de cierto tama˜o, donde a o n es necesario distinguir entre distintos niveles de abstracci´n, desde la visi´n global o o y gen´rica del sistema hasta, posiblemente, el nivel de c´digo m´quina. Una especi- e o a ficaci´n, en este contexto, es una descripci´n del significado o comportamiento de la o o abstracci´n. Las especificaciones nos permiten comprender el efecto de un procedi- o miento o el funcionamiento de un tipo abstracto de datos, sin necesidad de conocer los detalles de implementaci´n. En este cap´ o ıtulo se tratan los mecanismos de abs- tracci´n soportados por los lenguajes de programaci´n, y c´mo estas abstracciones o o o son descritas mediante especificaciones m´s o menos rigurosas. a Objetivos del cap´ ıtulo: Reconocer la importancia de la abstracci´n y conocer los tipos de abstracciones que apare- o cen en programaci´n: funcional, de datos y de iteradores. o Conocer los distintos mecanismos de los lenguajes de programaci´n que dan soporte al o uso de abstracciones, desde los m´dulos hasta las clases y objetos. o Concienciarse de la utilidad de desarrollar especificaciones completas y precisas, enten- diendo la especificaci´n como un punto de acuerdo entre el usuario y el implementador de o una abstracci´n. o Estudiar una notaci´n informal para la especificaci´n de abstracciones. o o Comprender el m´todo de especificaci´n formal algebraico o axiom´tico (basado en una e o a definici´n mediante axiomas) y el m´todo constructivo u operacional (basado en el uso de o e precondiciones y postcondiciones). Aplicar las notaciones analizadas en la especificaci´n de una amplia variedad de tipos o abstractos de datos. 35
  • 36 Cap´ ıtulo 2. Abstracciones y especificaciones Contenido del cap´ ıtulo: 2.1. Las abstracciones en programaci´n . . . . . . . . . . . . . . . . . . . . . o . . . . . 37 2.1.1. Dise˜o mediante abstracciones . . . . . . . . . . . . . . . . . . . n . . . . . 38 2.1.2. Mecanismos de abstracci´n: especificaci´n y parametrizaci´n . . o o o . . . . . 39 2.1.3. Tipos de abstracciones: funcional, de datos e iteradores . . . . . . . . . . 40 2.1.4. Mecanismos de abstracci´n en los lenguajes de programaci´n . . o o . . . . . 42 2.2. Especificaciones informales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 2.2.1. Especificaci´n informal de abstracciones funcionales . . . . . . . o . . . . . 47 2.2.2. Especificaci´n informal de abstracciones de datos . . . . . . . . . o . . . . . 48 2.2.3. Especificaci´n informal de abstracciones de iteradores . . . . . . o . . . . . 51 2.3. Especificaciones formales algebraicas . . . . . . . . . . . . . . . . . . . . . . . . . 52 2.3.1. Propiedades, notaci´n y ventajas de las especificaciones formales o . . . . . 53 2.3.2. Especificaciones algebraicas o axiom´ticas . . . . . . . . . . . . . a . . . . . 54 2.3.3. Taxonom´ de las operaciones de un TAD . . . . . . . . . . . . . ıa . . . . . 56 2.3.4. Completitud y correcci´n de la especificaci´n . . . . . . . . . . . o o . . . . . 57 2.3.5. Reducci´n de expresiones algebraicas . . . . . . . . . . . . . . . . o . . . . . 59 2.4. Especificaciones formales constructivas . . . . . . . . . . . . . . . . . . . . . . . . 61 2.4.1. Precondiciones y postcondiciones . . . . . . . . . . . . . . . . . . . . . . . 61 2.4.2. Especificaci´n como contrato de una operaci´n . . . . . . . . . . o o . . . . . 62 2.4.3. Necesidad de un modelo subyacente . . . . . . . . . . . . . . . . . . . . . 64 2.4.4. Ejecuci´n de especificaciones constructivas . . . . . . . . . . . . . o . . . . . 67 Ejercicios resueltos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 Ejercicios propuestos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 Cuestiones de autoevaluaci´n . . . . . . . . . . . . . . . . . . . . . . . . . . . o . . . . . 77 Referencias bibliogr´ficas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . a . . . . . 78
  • 2.1. Las abstracciones en programaci´n o 37 2.1. Las abstracciones en programaci´n o Supongamos que recibimos el encargo de crear una aplicaci´n para la gesti´n de una o o gran cadena de supermercados, que incluye las compras a los proveedores, el control de las cajas, la gesti´n de empleados, el inventario del almac´n, la contabilidad de la empresa, las o e campa˜as publicitarias, la comunicaci´n entre los distintos centros, etc. Los documentos n o de la aplicaci´n ocupan unos 2.000 folios –aunque no est´n muy claros– y hay en torno a o a quince personas implicadas en el proyecto. Ni cortos ni perezosos, empezamos a escribir el programa: ¡Imposible seguir por ese camino! ¡Debe de haber una manera m´s razonable de a abordar la construcci´n de la aplicaci´n! ¿Cu´l es el problema? Uno no puede tener todo o o a en la cabeza al mismo tiempo: los 2.000 folios de requisitos, los doce departamentos de la empresa –cada uno con sus necesidades e intereses–, la estructura final de la aplicaci´n, o los tipos de datos necesarios, el flujo de informaci´n, etc. Debe haber un m´todo o un o e proceso, que nos permita ir poco a poco, desde el punto de vista m´s amplio y gen´rico a e del sistema hasta llegar a los peque˜os subproblemas. n El estudio de estas t´cnicas para el desarrollo de grandes aplicaciones es parte del e ´rea conocida como ingenier´a del software, como ya comentamos en el cap´ a ı ıtulo 1. Pr´cti- a camente todas ellas se basan en la aplicaci´n del concepto de abstracci´n: dejar a un o o lado lo irrelevante y centrarse en lo importante, en la esencia de las cosas. Pero, ¿qu´ es lo importante? Obviamente, en cada momento ser´ una cosa distinta. e a En cierto estado inicial de desarrollo lo que importa ser´ la estructura general de la a aplicaci´n. En otra fase, ser´ la implementaci´n de una operaci´n concreta y, en una o a o o posible fase de optimizaci´n de c´digo, lo importante puede ser el c´digo m´quina del o o o a programa. As´ tenemos distintos niveles de abstracci´n: seg´n el nivel de detalle, ı, o u decimos que estamos en un nivel de abstracci´n mayor o menor. o La abstracci´n no s´lo aparece en el desarrollo de programas, sino que es usada o o en todos los ordenes de la vida, cuando intentamos abordar un problema complejo. Ser ´ capaz de distinguir los aspectos superfluos de los que forman la esencia de un problema es una de las claves de la inteligencia humana. En programaci´n, las abstracciones son o aplicadas de forma continua, dando lugar a conceptos b´sicos como las rutinas, funciones, a procedimientos y los tipos abstractos de datos. Pero la cosa no acaba ah´ y la aplicaci´n ı, o de mecanismos de abstracci´n en los lenguajes de programaci´n da lugar a conceptos y o o principios fundamentales, como los m´dulos, las clases, la genericidad, la ocultaci´n de o o informaci´n, la encapsulaci´n. Gracias a ellos, es posible construir aplicaciones cada vez o o m´s complejas y fiables, con un menor coste. El propio proceso de dise˜o de programas a n est´ basado en el uso de abstracciones. a
  • 38 Cap´ ıtulo 2. Abstracciones y especificaciones 2.1.1. Dise˜ o mediante abstracciones n Consideremos la aplicaci´n de gesti´n de la gran cadena de supermercados, planteada o o en la introducci´n. Vamos a ver c´mo usando abstracciones se puede abordar su resoluci´n o o o de forma ordenada y sistem´tica. a Un ejemplo de dise˜o usando abstracciones n Dejando a un lado todos los detalles irrelevantes, podemos distinguir los grandes bloques en los que se divide el problema. Por un lado tenemos una base de datos, en la que estar´ almacenada toda la informaci´n que maneje la empresa. Por otro lado, tendremos a o una aplicaci´n de administraci´n y mantenimiento del sistema. Y finalmente tendremos o o una o varias aplicaciones de usuarios, que acceden a la parte que les corresponde de la base de datos. De esta forma, se puede establecer una descomposici´n inicial del problema en tres o grandes subproblemas: Base de Datos, Administraci´n y Usuarios. Cada uno de los sub- o problemas es una abstracci´n de una parte del sistema. Existe una relaci´n entre ellos, o o pero b´sicamente se pueden abordar de forma independiente. La estructura global del a sistema se muestra en la figura 2.1. 8VXDULRV $SOLFDFLRQHV 8VXDULRV %DVH GH $GPLQLVWUDGRUHV $SOLFDFLRQHV 'DWRV $GPLQLVWUDFLyQ Figura 2.1: Una visi´n muy abstracta de un sistema inform´tico gen´rico. o a e ¿C´mo resolver ahora los subproblemas? Pues volviendo a aplicar la abstracci´n, evi- o o dentemente. Para ello ser´ necesario especificar, de forma abstracta, la funcionalidad que a es responsabilidad de cada parte. Por ejemplo, si nos centramos en la Base de Datos, su misi´n es almacenar toda la informaci´n manejada en la empresa, permitiendo su consulta o o y modificaci´n. Podemos distinguir, a su vez, cuatro grandes partes: productos, provee- o dores, clientes y empleados. De esta manera podemos seguir descomponiendo en partes cada vez m´s peque˜as, hasta llegar al nivel de programaci´n. Luego se van combinando a n o las soluciones de cada parte y se compone la soluci´n para el problema original. o El proceso de dise˜ o mediante abstracciones n Analizando el ejemplo anterior, se observa que existe un modo ordenado y sis- tem´tico de abordar el problema, que se va repitiendo en los distintos niveles de abs- a tracci´n. Este proceso aparece siempre en la resoluci´n de problemas complejos, y es lo o o
  • 2.1. Las abstracciones en programaci´n o 39 que se denomina dise˜ o mediante abstracciones. El proceso est´ compuesto b´sica- n a a mente por los siguientes cuatro pasos. Pasos en el proceso de dise˜o mediante abstracciones n 1. Identificar los subproblemas en los que se divide el problema original. 2. Especificar cada subproblema de forma abstracta, usando alguna notaci´n. o 3. Resolver cada subproblema de forma separada. 4. Unir las soluciones y verificar la soluci´n para el problema original. o En conclusi´n, se puede apreciar que se trata de un proceso c´ o ıclico de especificar / implementar / verificar. Tras el paso de verificaci´n puede ser necesario volver al paso 3 o (corregir la implementaci´n), al paso 2 (cambiar la especificaci´n) o incluso al 1 (hacer o o otra descomposici´n m´s adecuada de los subproblemas). Este ciclo constituye la idea o a b´sica del proceso cl´sico de desarrollo de programas, como vimos en el cap´ a a ıtulo 1. Por otro lado, existe una clara analog´ con el m´todo cient´ ıa e ıfico –aplicado en las ciencias experimentales– compuesto por los pasos de: observaci´n, proposici´n de o o hip´tesis, experimentaci´n y verificaci´n de la hip´tesis. El proceso se puede ver, por o o o o lo tanto, como una aplicaci´n del m´todo cient´ o e ıfico a la disciplina de la programaci´n. En o nuestro caso, las abstracciones de objetos del mundo real juegan el papel de hip´tesis que o deben ser especificadas, implementadas y verificadas. 2.1.2. Mecanismos de abstracci´n: especificaci´n y o o parametrizaci´n o Las abstracciones utilizadas en programaci´n (procedimientos, funciones y tipos de o datos) son descritas mediante especificaciones, que determinan su comportamiento espera- do. Adem´s, esas abstracciones pueden estar parametrizadas, de forma que su significado a est´ dado en funci´n de ciertos par´metros. La especificaci´n y la parametrizaci´n son a o a o o mecanismos de abstracci´n independientes, pueden aparecer juntos o no. o Abstracci´n por especificaci´n o o Como vimos en la introducci´n, la abstracci´n consiste en distinguir lo que es rele- o o vante de lo que no lo es. En la abstracci´n por especificaci´n lo relevante es la descripci´n o o o del efecto o resultado, y lo irrelevante es la forma de calcularlo. La mayor parte de las abstracciones usadas en el desarrollo de programas son abs- tracciones por especificaci´n. La especificaci´n de un procedimiento, o de un tipo de datos, o o describe su comportamiento, pero no dice nada acerca de c´mo funciona internamente, o de c´mo est´ implementado. o a Consideremos, por ejemplo, las librer´ del sistema operativo para crear procesos, ıas abrir ventanas, pedir memoria, etc. La especificaci´n describe el significado, el modo de o uso y el efecto de las funciones existentes, pero las cuestiones de implementaci´n quedan o
  • 40 Cap´ ıtulo 2. Abstracciones y especificaciones completamente ocultas. Un programador que use esas librer´ no s´lo no necesita conocer ıas o la implementaci´n, sino que conocerla supondr´ una complejidad excesiva e innecesaria. o ıa Esto es lo que se conoce como el principio de ocultaci´n de la implementaci´n: o o el usuario de una abstracci´n no puede ni debe conocer la implementaci´n de la misma. o o Este principio implica que el lenguaje tenga mecanismos que soporten la ocultaci´n, y que o el programador de la abstracci´n los use de forma adecuada para garantizar la ocultaci´n. o o La abstracci´n por especificaci´n tambi´n da lugar al concepto de encapsulaci´n: o o e o un conjunto de operaciones, o tipos, relacionados son agrupados en un mismo m´dulo o o paquete. En consecuencia, el m´dulo se puede considerar como una abstracci´n de nivel o o superior, compuesta por varias abstracciones que proporcionan un conjunto de funcionali- dades relacionadas. Por ejemplo, las librer´ del sistema operativo podr´ ser separadas ıas ıan en distintos m´dulos: uno para la parte gr´fica, otro para el manejo de ficheros, otro para o a los procesos, etc. Abstracci´n por parametrizaci´n o o Las abstracciones pueden estar parametrizadas, de manera que el significado de la abstracci´n no es fijo, sino que depende del valor de uno o m´s par´metros. Una clase o a a muy importante es la parametrizaci´n de tipo, en la cual el par´metro es un tipo de datos. o a Supongamos, por ejemplo, que queremos implementar un conjunto de funciones para ordenar los elementos de un array, sean del tipo que sean. Deber´ ıamos definir una funci´n o por cada posible tipo de entrada: operaci´n OrdenaEntero (A : array [min..max] de entero) o operaci´n OrdenaReal (A : array [min..max] de real) o operaci´n OrdenaCaracter (A : array [min..max] de caracter) o .... Resultar´ mucho m´s interesante crear una abstracci´n m´s gen´rica, que se pueda ıa a o a e aplicar sobre cualquier tipo de datos del array. Para ello, factorizamos todas las operacio- nes anteriores creando una nueva operaci´n, donde el tipo de datos de los elementos del o array es un par´metro m´s de la funci´n: a a o operaci´n Ordena[T: tipo comparable](A : array [min..max] de T) o La operaci´n Ordena[T] es una abstracci´n de nivel superior, que permite ordenar o o un array de cosas de cualquier tipo. Se dice que Ordena[T] es una abstracci´n gen´rica o o e parametrizada. La abstracci´n por parametrizaci´n se aplica tambi´n sobre abstracciones de datos, o o e permitiendo definir tipos de datos gen´ricos. Por ejemplo, un tipo Conjunto[T: tipo] podr´ e ıa almacenar un conjunto de elementos de tipo T, mientras que Pareja[K: tipo] almacenar´ ıa un par de valores de cualquier tipo K. 2.1.3. Tipos de abstracciones: funcional, de datos e iteradores De forma consciente o inconsciente, los mecanismos de abstracci´n son usados cons- o tantemente en programaci´n. Podemos distinguir entre distintos tipos o categor´ de o ıas abstracciones, en funci´n de qu´ es lo que se abstrae. Los dos tipos fundamentales son o e la abstracci´n de datos y la funcional. Adicionalmente, encontramos las abstracciones de o
  • 2.1. Las abstracciones en programaci´n o 41 iteradores, que permiten definir un recorrido abstracto sobre los elementos de una colec- ci´n. o Abstracciones funcionales En la abstracci´n funcional abstraemos un trozo de c´digo, m´s o menos largo, al o o a cual le asignamos un nombre. El concepto resultante es la idea de rutina, procedimiento o funci´n. La especificaci´n de un procedimiento determina cu´les son los par´metros o o a a de entrada, los de salida y el efecto que tendr´ su ejecuci´n, pero no se indica nada a o sobre la implementaci´n. Para el usuario del procedimiento es indiferente c´mo se haya o o implementado, siempre que se garantice el efecto descrito en la especificaci´n. o Por ejemplo, el procedimiento de ordenaci´n OrdenaEntero(A: array [min..max] de o entero) es una abstracci´n funcional, de la cual sabemos que su resultado es que los o elementos del array de enteros A son ordenados en la salida de menor a mayor. Pero, respetando el principio de ocultaci´n de la implementaci´n, no sabemos nada acerca de o o c´mo lo hace. Los algoritmos de ordenaci´n por selecci´n, inserci´n, burbuja, mezcla, etc., o o o o son posibles implementaciones de la abstracci´n OrdenaEntero. o Los procedimientos y funciones son generalizaciones del concepto de operador de un lenguaje de programaci´n. En lugar de usar los operadores existentes en el lenguaje, un o programador puede definir su propio conjunto de operadores, con comportamientos m´s a complejos y adaptados a sus necesidades. Abstracciones de datos En la abstracci´n de datos se abstrae un dominio de valores, junto con un conjunto o de operaciones sobre ese dominio, que poseen un significado particular. Esta abstracci´n o da lugar al concepto de tipo abstracto de datos (TAD). La especificaci´n del TAD o determina el comportamiento de las operaciones definidas sobre el tipo, pero no dice nada sobre c´mo son almacenados los valores del tipo o c´mo est´n implementadas las o o a operaciones. Por ejemplo, los n´meros naturales forman un TAD, al que le asignamos el nombre u Natural. Cada natural puede tomar valores dentro de un dominio {0, 1, 2, 3, ...} y puede ser manipulado a trav´s de un conjunto de operaciones definido (suma, multiplicaci´n, e o resta, etc.) con un significado conocido. En este caso el principio de ocultaci´n de la o implementaci´n es aplicado tanto sobre las operaciones como sobre la estructura que se o usa para almacenar los datos del dominio. Igual que con los procedimientos, los TAD se pueden considerar como generaliza- ciones del concepto de tipo de datos primitivo. En lugar de usar los tipos primitivos definidos en un lenguaje (enteros, reales, caracteres, etc.) el usuario se puede definir sus propios tipos, combinando los tipos primitivos. O puede usar los TAD implementados por otro programador y que sean de utilidad para su problema. Tipos contenedores y parametrizados Una clase importante de TAD son los tipos contenedores o colecciones. Un tipo contenedor es un TAD que est´ compuesto por un n´mero no fijo de valores de otro tipo. a u
  • 42 Cap´ ıtulo 2. Abstracciones y especificaciones Por ejemplo, un array, una cola, una lista o un arbol son tipos contenedores. Las imple- ´ mentaciones de listas mediante cursores, punteros o tablas, son ejemplos de estructuras de datos usadas para almacenar los valores del tipo. Se puede decir que la estructura de datos es la concretizaci´n del tipo abstracto. o Para conseguir una definici´n lo m´s general posible, se debe permitir que el tipo de o a valores almacenados en el contenedor sea un TAD cualquiera, es decir, tener listas, arrays o colas de cualquier cosa. En consecuencia, los TAD colecciones deben estar parametriza- dos, incluyendo un par´metro que indica el tipo de objetos almacenados. Por ejemplo, a el TAD parametrizado Lista [T: tipo] define las listas de valores de tipo T. Dos posibles instanciaciones del tipo ser´ ıan: Lista [Natural], o Lista [Lista [Natural]]. Abstracciones de iteradores Un tipo interesante de abstracci´n –aunque menos frecuente– es la abstracci´n de o o iteradores. Un iterador permite realizar un recorrido sobre los elementos de una colecci´n o de forma abstracta. En un iterador lo relevante es que se recorren todos los elementos de una colecci´n, independientemente de cu´l sea la forma de almacenar los valores del tipo. o a Los iteradores son una generalizaci´n de los iteradores elementales de los lenguajes o de programaci´n. Por ejemplo, un iterador para, o for, tiene la forma: o para i:= 1, ..., n hacer Acci´n sobre i o Esta instrucci´n permite recorrer todos los enteros entre 1 y n, aplicando determi- o nada operaci´n sobre cada valor. Si en lugar de enteros tenemos listas, pilas, arboles o o ´ cualquier otra colecci´n, ser´ muy util disponer de una operaci´n similar a la anterior, o ıa ´ o que nos permita recorrer todos los elementos de forma abstracta. La operaci´n podr´ o ıa tener la forma: para cada elemento i de la lista L hacer Acci´n sobre i o para cada elemento i del ´rbol A hacer Acci´n sobre i a o Los dos ejemplos anteriores son abstracciones de iteradores: permiten procesar to- dos los elementos de una colecci´n, independientemente de los detalles de representaci´n o o del tipo de datos. Otros ejemplos de iteradores pueden incluir una condici´n sobre los o elementos a iterar, devolver otra colecci´n con los elementos procesados o fijar un orden o concreto de recorrido. El formato podr´ ser del siguiente tipo: ıa para cada elemento i de la colecci´n C que cumpla P(i) hacer Acci´n sobre i o o res:= Seleccionar los elementos i de la colecci´n C que cumplan P(i) o Eliminar los elementos i de la colecci´n C que cumplan P(i) o para cada elemento i del ´rbol A en pre-orden hacer Acci´n sobre i a o 2.1.4. Mecanismos de abstracci´n en los lenguajes o de programaci´n o La evoluci´n de los lenguajes de programaci´n se puede considerar, en gran medida, o o como un intento por incluir y dar soporte a mecanismos de abstracci´n cada vez m´s o a avanzados. En particular, la forma de proveer un mecanismo de definici´n y uso de Tipos o Abstractos de Datos ha sido una de las principales motivaciones para la aparici´n de o nuevos conceptos. La evoluci´n va casi siempre en la direcci´n de ofrecer caracter´ o o ısticas
  • 2.1. Las abstracciones en programaci´n o 43 m´s pr´ximas al concepto te´rico de TAD: posibilidad de usar un TAD como un tipo a o o cualquiera, principio de ocultaci´n de la implementaci´n, encapsulaci´n y modularidad, o o o genericidad, reutilizaci´n de TAD, posibilidad de usar iteradores. o Pero, ¿por qu´ es tan importante el concepto de Tipo Abstracto de Datos? Un e lenguaje que no permite usar TAD –o un entorno de programaci´n para el que no o disponemos de TAD– es como una casa sin muebles y sin decoraci´n. Es posible vivir o en ella y realizar tareas rutinarias, aunque tendr´ ıamos que hacerlo de manera muy rudi- mentaria. Por ejemplo, deber´ ıamos dormir en el suelo o beber agua chupando del grifo. Los TAD son como los muebles de la casa, no son estrictamente necesarios para vivir pero facilitan mucho las cosas. Los muebles, que hacen el papel de tipos abstractos: Aportan una funcionalidad extra fundamental, un valor a˜adido a la casa, permi- n tiendo realizar las tareas comunes de manera m´s sencilla y c´moda. a o Pueden cambiarse por otros, moverse de sitio, arreglarlos en caso de rotura, etc., lo cual no es posible hacerlo –o resulta mucho m´s costoso– con la casa en s´ a ı. Los muebles y la decoraci´n est´n adaptados a las necesidades, gustos y caprichos o a de cada uno, mientras que la casa es la misma para todo el bloque de apartamentos. Hay una separaci´n clara entre la persona que fabrica el mueble y la que lo utiliza. o Para usar un mueble no es necesario conocer nada de carpinter´ıa. En definitiva, los TAD son uno de los conceptos m´s importantes en programaci´n. a o Su uso permite construir aplicaciones cada vez m´s complejas, simplificando la utilizaci´n a o de funcionalidades adicionales. Los TAD pueden ser, por ejemplo, desde los enteros y los booleanos, hasta las ventanas de Windows, las conexiones a Internet o el propio sistema operativo. Lenguajes de programaci´n primitivos o Los lenguajes de programaci´n primitivos (como Fortran o BASIC) ofrec´ un o ıan conjunto predefinido de tipos elementales: entero, real, car´cter, cadena, array, etc. Pero a no exist´ la posibilidad de definir tipos nuevos. Si, por ejemplo, un usuario necesitaba ıa usar una pila, deb´ definir y manejar las variables adecuadas de formal “manual”. Por ıa ejemplo, el programa en notaci´n Pascal ser´ algo como lo siguiente. o ıa var PilaUno, PilaDos: array [1..100] of integer; TopePilaUno, TopePilaDos: integer; begin PilaUno[TopePilaUno]:= 52; Write(PilaDos[TopePilaDos]); TopePilaUno:= TopePilaUno + 1; ...
  • 44 Cap´ ıtulo 2. Abstracciones y especificaciones Utilizar las pilas de esta manera resulta dif´ y engorroso, y el c´digo generado ıcil o no se puede reutilizar en otras aplicaciones. Adem´s, el programador deber´ tener en a ıa cuenta que PilaUno se corresponde con TopePilaUno y PilaDos con TopePilaDos. En estas circunstancias, es muy f´cil equivocarse o acceder a posiciones no v´lidas del array. a a Tipos definidos por el usuario Los tipos definidos por el usuario son un primer paso hacia los TAD. Aparecen en lenguajes como C y Pascal, y permiten agrupar un conjunto de variables bajo un nombre del tipo. Estos tipos se pueden usar en el mismo contexto que un tipo elemental, es decir, para definir variables, par´metros o expresiones de ese tipo nuevo. En el ejemplo anterior, a podr´ıamos definir las pilas de enteros como un nuevo tipo definido por el usuario. type Pila= record Tope: integer; Datos: array [1..100] of integer; end; var PilaUno, PilaDos: Pila; procedure Push (valor: integer; var pila: Pila); procedure Pop (var pila: Pila); ... El gran inconveniente de este mecanismo es que no permite garantizar el ocultamien- to de la implementaci´n. En teor´ las pilas s´lo deber´ ser usadas con las operaciones o ıa, o ıan Push, Pop, etc., definidas sobre ellas. Pero, en su lugar, un programador podr´ hacer ıa sin problemas: PilaUno.Datos[72]:=36; o PilaDos.Tope:=-12;. El lenguaje deber´ ıa impedir las anteriores instrucciones, que acceden directamente a la representaci´n del o tipo. M´dulos y encapsulaci´n o o Los m´dulos o paquetes son un mecanismo de encapsulaci´n; permiten tener agru- o o pados bajo un mismo nombre una serie de tipos y operaciones relacionados. Y lo que es m´s interesante, en algunos casos, como en el lenguaje Modula-2, los m´dulos ofrecen a o ocultaci´n de la implementaci´n: los tipos definidos dentro de un m´dulo s´lo se pueden o o o o usar a trav´s de las operaciones de ese mismo m´dulo. De esta forma se evitar´ los e o ıan problemas vistos en el apartado anterior. El programa ser´ algo del siguiente tipo. ıa MODULE ModuloPilasEnt; EXPORT PilaEntero, Push, Pop, Top, Nueva; TYPE PilaEntero= RECORD
  • 2.1. Las abstracciones en programaci´n o 45 Tope: INTEGER; Datos: ARRAY [1..100] OF INTEGER; END; PROCEDURE Push (valor: integer; VAR pila: PilaEntero); PROCEDURE Pop (var pila: PilaEntero); ... USE ModuloPilasEnt; VAR PilaUno, PilaDos: PilaEntero; ... Push(23, PilaUno); Pop(PilaDos); ... PilaUno.Datos[72]:= 36; ----> ERROR EN TIEMPO DE COMPILACI´N O PilaDos.Tope:= -12; ----> ERROR EN TIEMPO DE COMPILACI´N O ... El compilador impide las dos ultimas instrucciones, indicando que los atributos ´ Datos y Tope no son accesibles. Clases y objetos Hoy por hoy, las clases son el mecanismo m´s completo y extendido para definir y a utilizar TAD. De hecho, su utilizaci´n implica tantos conceptos nuevos que hablamos de o un paradigma de programaci´n propio: la programaci´n orientada a objetos. Una clase o o es, al mismo tiempo, un tipo definido por el usuario y un m´dulo donde se encapsulan los o datos y operaciones de ese tipo. Un objeto es una instancia particular de una clase, un valor concreto. Las clases permiten usar ocultamiento de la implementaci´n. Para ello, en la defini- o ci´n de la clase es posible decir qu´ partes son visibles desde fuera (secci´n public) o e o y cu´les no (secci´n private). En el ejemplo de las pilas, los atributos Datos y Tope a o deben ser privados, y Push, Pop, etc., p´blicos. La definici´n del tipo, usando el lenguaje u o ObjectPascal1 , ser´ como la siguiente. ıa type PilaEntero= class private: Tope: integer; Datos: array [1..100] of integer; public: procedure Inicializar; procedure Push (valor: integer); procedure Pop; function Top: integer; end; 1 Utilizamos aqu´ ObjectPascal para facilitar la comparaci´n con las definiciones de los apartados ı o anteriores. En las pr´cticas se usar´ el lenguaje C++. a a
  • 46 Cap´ ıtulo 2. Abstracciones y especificaciones ... var PilaUno, PilaDos: PilaEntero; ... PilaUno.Push(23); PilaDos.Pop; Write(PilaUno.Top); ... PilaUno.Datos[72]:= 36; ----> ERROR EN TIEMPO DE COMPILACI´N O PilaDos.Tope:= -12; ----> ERROR EN TIEMPO DE COMPILACI´N O ... Las pilas, y en general cualquier objeto de una clase, s´lo pueden ser manipuladas o a trav´s de las operaciones p´blicas de su definici´n. De esta forma, es posible conseguir e u o la ocultaci´n de la implementaci´n. o o Hay que notar que en la definici´n de las operaciones con pilas, la propia pila no o aparece como un par´metro. Por ejemplo, donde antes ten´ a ıamos: Pop(PilaUno) ahora tenemos: PilaUno.Pop. Esto es lo que se conoce como el principio de acceso uniforme: los atributos y las operaciones de una clase est´n conceptualmente al mismo nivel, y se a accede a ellos usando la notaci´n punto “.”. En la llamada PilaUno.Pop, el valor PilaUno o (llamado “objeto receptor”) es un par´metro impl´ a ıcito de la operaci´n Pop. o Algunos lenguajes orientados a objetos permiten aplicar parametrizaci´n de tipo, o dando lugar a la definici´n de TAD gen´ricos o parametrizados. Por ejemplo, usando el o e lenguaje C++ es posible definir el tipo gen´rico Pila<T>, de pilas de cualquier tipo T. La e definici´n se hace a trav´s de la sentencia template. o e template <class T> class Pila { private: T Datos [ ]; int Maximo, Tope; public: Pila (int max); // Operaci´n de inicializaci´n de una pila o o ~Pila (); // Operaci´n de eliminaci´n de una pila o o Push (T valor); Pop (); T Tope (); }; ... Pila<int> PilaUno(100); // Instanciaci´n a pila de enteros, con tama~o 100 o n Pila<char> PilaDos(200); // Instanciaci´n a pila de caracteres, con tama~o 200 o n ... PilaUno.Push(5); PilaDos.Push(’w’); ... Dentro de las pr´cticas profundizaremos en el uso de las clases como un mecanismo a de definici´n de tipos abstractos, y en la utilizaci´n de patrones para conseguir clases o o parametrizadas.
  • 2.2. Especificaciones informales 47 2.2. Especificaciones informales En una especificaci´n informal, normalmente la descripci´n de una abstracci´n se o o o realiza textualmente utilizando el lenguaje natural. En consecuencia, las especificaciones informales pueden resultar ambiguas e imprecisas. Lo que una persona entiende de una forma, otra lo puede entender de manera distinta. Es m´s, puede que la especificaci´n no a o sea completa, quedando parte del comportamiento sin especificar. Una notaci´n de especificaci´n, formal o informal, define las partes que debe o o tener la especificaci´n, las partes que pueden ser opcionales, y el significado y tipo de o descripci´n de cada parte. Existe una gran variedad de notaciones de especificaci´n in- o o formal, muchas de ellas adaptadas a lenguajes de programaci´n y aplicaciones concretos. o En este punto se estudia un formato no ligado a ning´n lenguaje particular. u 2.2.1. Especificaci´n informal de abstracciones funcionales o La especificaci´n informal de una abstracci´n funcional –es decir, de una rutina, o o funci´n o un procedimiento– debe contener la siguiente informaci´n: o o Nombre de la operaci´n. o Lista de par´metros, tipo de cada par´metro y del valor devuelto, en su caso. a a Requisitos necesarios para ejecutar la operaci´n. o Descripci´n del resultado de la operaci´n. o o La notaci´n propuesta para la especificaci´n informal de abstracciones funcionales tiene o o el siguiente formato: Operaci´n <nombre> (ent <id>:<tipo>; <id>:<tipo>;... ; sal <tipo resultado>) o Requiere: Descripci´n textual, donde se establecen los requisitos y restricciones de uso o de la operaci´n. o Modifica: Lista de par´metros de entrada que se modifican o pueden modificarse. a Calcula: Descripci´n textual del resultado de la operaci´n. o o Las cl´usulas Requiere y Modifica son opcionales, al igual que es posible que no a existan par´metros de entrada o de salida. Si la funci´n es parametrizada, entonces el a o formato ser´ del siguiente tipo: ıa Operaci´n <nombre>[<id>:<tipo>;...] (ent <id>:<tipo>; ... ; sal <tipo result>) o ... Aunque no resulta muy frecuente, la funci´n puede estar parametrizada por m´s de o a un tipo gen´rico. Adem´s, la cl´usula Requiere puede contener restricciones sobre estos e a a tipos gen´ricos. A continuaci´n se muestran algunos ejemplos de especificaciones infor- e o males de abstracciones funcionales, usando el formato definido. Ejemplo 2.1 Especificaci´n informal de una operaci´n QuitarDuplicados, para eliminar o o los elementos repetidos de un array de enteros.
  • 48 Cap´ ıtulo 2. Abstracciones y especificaciones Operaci´n QuitarDuplicados (ent A: array [] de entero; sal entero) o Requiere: El tama˜o de A debe ser mayor que cero. n Modifica: A Calcula: Elimina los elementos duplicados del array A, manteniendo el orden de los elementos que no se repiten. Devuelve el n´mero de los elementos no repetidos u existentes. Ejemplo 2.2 Especificaci´n informal de una operaci´n parametrizada RecorridoPreorden, o o para hacer el recorrido en preorden de un arbol binario de elementos de cualquier tipo. ´ Operaci´n RecorridoPreorden [T: tipo] (ent A: ArbolBinario[T]; sal Lista[T]) o Calcula: Devuelve como resultado una lista con los elementos del ´rbol A recorridos en a preorden. Ejemplo 2.3 Especificaci´n informal de una operaci´n parametrizada BuscarEnArray, o o para buscar un valor dentro de un array de cualquier tipo T. Operaci´n BuscarEnArray [T: tipo] (ent A: array [] de T; valor : T; sal entero) o Requiere: El tipo T debe tener definida una operaci´n de comparaci´n Igual(ent T, T; o o sal booleano). Calcula: Devuelve el menor valor de ´ ındice i, tal que Igual(valor, A[i]) = verdadero. Si no existe tal valor, entonces devuelve un n´mero fuera del rango del array A. u Normalmente, las caracter´ ısticas del formato usado (partes de la especificaci´n, nivel o de detalle de la explicaci´n, cosas que se describen m´s o menos) suelen variar mucho de o a una aplicaci´n a otra. En la figura 2.2 se muestran dos ejemplos concretos de especifica- o ciones informales en aplicaciones reales. En ambos casos, se puede decir que la descripci´n o est´ centrada en los par´metros; se hace especial ´nfasis en el efecto de cada uno de los a a e par´metros sobre el resultado final de la operaci´n. a o 2.2.2. Especificaci´n informal de abstracciones de datos o La especificaci´n informal de un TAD describe el significado y uso del tipo, enu- o merando las operaciones que se pueden aplicar. La especificaci´n deber´ contener al o ıa menos la siguiente informaci´n: o Nombre del TAD. Si el TAD es un tipo parametrizado, n´mero de par´metros. En ese caso, se pueden u a imponer restricciones sobre los tipos que se usen como par´metros en la instan- a ciaci´n. o Lista de operaciones definidas sobre los valores del TAD. Especificaci´n de cada una de las operaciones de la lista anterior. o La notaci´n propuesta para la especificaci´n informal de abstracciones de datos tiene o o la siguiente forma.
  • 2.2. Especificaciones informales 49 Nombre de la operación Descripción corta Sintaxis Explicación de los parámetros Descripción detallada Nombre de la operación Descripción corta Sintaxis Explicación de los parámetros Explicación del valor devuelto Figura 2.2: Especificaciones informales como documentaci´n del c´digo. Arriba: procedi- o o miento “cvEllipse” de la librer´ Intel OpenCV. Abajo: funci´n “DrawDibStart” de las ıa o librer´ Microsoft Multimedia API. ıas
  • 50 Cap´ ıtulo 2. Abstracciones y especificaciones TAD <nombre> es <lista de operaciones> Descripci´n o Descripci´n textual del significado, comportamiento y modo de uso del tipo abstracto. o Operaciones Especificaci´n informal de cada una de las operaciones de <lista de operaciones>. o Fin <nombre>. Vamos a ver algunas especificaciones de ejemplo, sobre tipos parametrizados y no parametrizados, con representaciones mutables o inmutables. Esta ultima distinci´n se ´ o puede realizar teniendo en cuenta la forma de las operaciones: en la representaci´n in- o mutable los par´metros de entrada nunca se modifican. a Ejemplo 2.4 Especificaci´n informal del TAD Polinomio. o TAD Polinomio es Crear, Grado, Coef, Sumar, Prod, Igual Descripci´n o Los valores de tipo Polinomio son polinomios de coeficientes enteros no modificables. Operaciones Operaci´n Crear (ent c, n: entero; sal Polinomio) o Requiere: n ≥ 0. Calcula: Devuelve el polinomio cxn . Operaci´n Grado (ent P: Polinomio; sal entero) o Calcula: Devuelve el grado del polinomio P. Operaci´n Coef (ent P: Polinomio; n: entero; sal entero) o Calcula: Devuelve el coeficiente del polinomio P asociado al t´rmino xn . Si no existe e ese t´rmino, devuelve 0. e Operaci´n Sumar (ent P, Q: Polinomio; sal booleano) o Calcula: Devuelve la suma de los polinomios P+Q. Operaci´n Prod (ent P, Q: Polinomio; sal booleano) o Calcula: Devuelve el producto de los polinomios P × Q. Operaci´n Igual (ent P, Q: Polinomio; sal booleano) o Calcula: Devuelve verdadero si P = Q y falso en caso contrario. Fin Polinomio. Ejemplo 2.5 Especificaci´n informal del TAD parametrizado Asociacion, que almacena o un par de valores (clave, valor). TAD Asociacion[tclave, tvalor: tipo] es Crear, Clave, Valor, PonValor, Igual Requiere Los tipos tclave y tvalor deben tener definidas sendas operaciones de comparaci´n Igual(ent o tclave, tclave; sal booleano) e Igual(ent tvalor, tvalor; sal booleano). Descripci´n o Los valores de tipo Asociaci´n[tclave, tvalor] son tuplas modificables, compuestas por el o par (clave: tclave, valor: tvalor), que asocia una clave con un valor correspondiente. Operaciones Operaci´n Crear (ent c: tclave; v : tvalor; sal Asociacion[tclave,tvalor]) o Calcula: Devuelve la asociaci´n compuesta por el par (c, v). o Operaci´n Clave (ent A: Asociacion[tclave,tvalor]; sal tclave) o
  • 2.2. Especificaciones informales 51 Calcula: Devuelve el primer elemento de la tupla A, es decir si A = (c, v), devuelve c. Operaci´n Valor (ent A: Asociacion[tclave,tvalor]; sal tvalor) o Calcula: Devuelve el segundo elemento de la tupla A, es decir si A = (c, v), devuelve v. Operaci´n PonValor (ent A: Asociacion[tclave,tvalor]; valor: tvalor) o Requiere: El nuevo valor debe ser distinto del antiguo, es decir Igual(Valor(A), valor) = falso. Modifica: A Calcula: Cambia el segundo elemento de la tupla A, poniendo valor. Operaci´n Igual (ent A1, A2 : Asociacion[tclave,tvalor]; sal booleano) o Calcula: Devuelve verdadero si A1 y A2 contienen la misma clave y valor. Fin Asociacion. 2.2.3. Especificaci´n informal de abstracciones de iteradores o A diferencia de lo que ocurre con las abstracciones funcionales y de datos, no existe un claro consenso sobre el formato de las abstracciones de iteradores y la forma de imple- mentarlas en los lenguajes de programaci´n. Recordemos que la abstracci´n de iteradores o o implica un procesamiento repetido sobre los elementos de una colecci´n.o Si utilizamos un lenguaje de programaci´n que permita pasar funciones como par´me- o a tros, entonces la abstracci´n de iteradores se puede expresar como una abstracci´n fun- o o cional, donde un par´metro es la colecci´n sobre la que iterar y el otro es la acci´n a a o o aplicar. Por lo tanto, la especificaci´n tendr´ un formato similar a la especificaci´n de o a o abstracciones funcionales. Iterador <nombre> (ent <id>:<tipo>; <id>:<tipo>;... ; sal <tipo resultado>) Requiere: Descripci´n textual de los requisitos del iterador. o Modifica: Indicaci´n de si se modifica la colecci´n de entada o no. o o Calcula: Descripci´n textual del resultado del iterador. o Ejemplo 2.6 Especificaci´n informal de un iterador ParaTodoHacer, para iterar sobre los o elementos de un conjunto de cualquier tipo. Iterador ParaTodoHacer [T: tipo] (ent C : Conjunto[T]; accion: Operacion) Requiere: accion debe ser una operaci´n que recibe un par´metro de tipo T y no devuelve o a nada, accion(ent T). Calcula: Recorre todos los elementos c del conjunto C, aplicando sobre ellos la operaci´n o accion(c). Ejemplo 2.7 Especificaci´n informal de un iterador Seleccionar, que devuelve todos los o elementos de una colecci´n que cumplen cierta condici´n. o o Iterador Seleccionar [T : tipo; Coleccion: tipo coleccion] (ent C : Coleccion[T]; condicion: Operacion; sal Coleccion[T]) Requiere: condicion debe ser una operaci´n que recibe un par´metro de tipo T y de- o a vuelve un booleano, condicion (ent T; sal booleano). tipo coleccion debe ser un TAD parametrizado, que incluya un iterador ParaTodoHacer.
  • 52 Cap´ ıtulo 2. Abstracciones y especificaciones Calcula: Devuelve una colecci´n que contiene todos los elementos c de C tal que condi- o cion(c) es verdadero. Si el lenguaje no permite pasar procedimientos como par´metros, la abstracci´n a o de iteradores puede ser realizada a trav´s de un TAD que proporcione operaciones para e iniciar la iteraci´n, obtener el elemento actual, avanzar al siguiente y comprobar si hemos o procesado todos los elementos. En este caso, la notaci´n toma una forma similar a la o especificaci´n de abstracciones de datos. o TipoIterador <nombre> es <lista de operaciones> Requiere Requisitos del iterador. Descripci´n o Descripci´n textual del significado y modo de uso del iterador. o Operaciones Especificaci´n informal de cada una de las operaciones de <lista de operaciones>. o Fin <nombre>. Ejemplo 2.8 Especificaci´n informal de un iterador IteradorPreorden, que permite reco- o rrer todos los elementos de un arbol binario en preorden. ´ TipoIterador IteradorPreorden [T: tipo] es Iniciar, Actual, Avanzar, EsUltimo Descripci´n o Los valores de tipo IteradorPreorden[T] son iteradores definidos sobre ´rboles binarios de a cualquier tipo T. Los elementos del ´rbol son devueltos en preorden. El iterador se debe a inicializar con Iniciar. Operaciones Operaci´n Iniciar (ent A: ArbolBinario[T]; sal IteradorPreorden) o Calcula: Devuelve un iterador nuevo, colocado sobre la ra´ de A. ız Operaci´n Actual (ent iter : IteradorPreorden; sal T) o Calcula: Devuelve el elemento actual en la iteraci´n. o Operaci´n Avanzar (ent iter : IteradorPreorden) o Requiere: EsUltimo(iter) debe ser falso. Modifica: iter Calcula: Avanza el iterador iter al siguiente elemento en preorden, dentro del ´rbol a binario sobre el que fue creado. Operaci´n EsUltimo (ent iter : IteradorPreorden; sal booleano) o Calcula: Devuelve verdadero si y s´lo si el elemento actual de la iteraci´n es el o o ultimo. ´ Fin IteradorPreorden. 2.3. Especificaciones formales algebraicas En una especificaci´n formal, el comportamiento de una operaci´n o de un TAD o o se describe de forma rigurosa utilizando una notaci´n matem´tica no ambigua. En las o a