TAREA 4 Ejercicios Algunas de las cuestiones siguientes hacen referencia al “computador del enano ” y a su conjunto de instrucciones. La cuestión 1 tiene por objeto familiarizarte con las técnicas de búsqueda. 1. Programa uno de los dos métodos de ordenación para números enteros: ordenación por inserción u ordenación mediante el método de la burbuja. Cualquiera de los dos programas anteriores te proporcionarán la cadena siguiente ordenada de manera ascendente: 3 4 6 6 2 6 5 1 3 −1 −4 −8 9 −5 −2 7 1 0 5 −5 Imagina que tienes además el número 4 y lo que quieres es determinar (la posición que ocupa) el primero de los veinte números anteriores mayor que 4. Para ello te puedes ayudar del siguiente algoritmo: 1 i=1 j=n ⌋ indice = ⌊ i+j 2 if a(indice) ≤ l then i = indice + 1 else if indice = 1 or a(indice − 1) ≤ l then return (indice encontrado) endif j = indice − 1 endif if i > j then indice = n + 1(FIN. Todos los ai ≤ l, i = 1, ..., n) else goto 1 endif Escribe un programa en C que determine la posición exacta del valor buscado. ¿Podrı́as determinar la complejidad computacional (número de operaciones) del algoritmo de búsqueda descrito? ¿Y del proceso completo de ordenación y búsqueda? 2. Se dice que un algoritmo de ordenación es conservador del orden si, para cualesquiera i, j el orden de los elementos xi y xj en la disposición original no queda alterado a menos que sea imprescindible hacerlo a causa de los respectivos valores del campo de ordenación (la “clave”). Por ejemplo, imaginemos un fichero tal como: ··· LLODIO LLODIO BILBAO LLODIO ··· ··· PEREZ ORTIZ ORTEGA OSTOLAZA ··· ··· GONZALEZ GONZALEZ BERMUDEZ PEREZ ··· 1 ··· TERESA ALBERTO FRANCISCO AITOR ··· Supongamos que ordenamos el fichero por (exclusivamente) municipio. Si el algoritmo de ordenación es conservador del orden, la lı́nea correspondiente a ORTEGA BERMUDEZ pasará por delante de las otras tres, pero éstas, que no se diferencian por el valor de ordenación (LLODIO en las tres) quedarán en el mismo orden relativo en el que están: primero PEREZ, luego ORTIZ y luego OSTOLAZA. Examina los algoritmos de inserción y burbuja y decide si son conservadores de orden. ¿Qué trascendencia tiene este hecho? (Ayuda: piensa en lo que ocurrirı́a si quisieras hacer una ordenación por múltiples claves: por ejemplo, por municipio y, dentro de cada municipio, por primer apellido). 3. Se supone que al entrar en el siguiente fragmento de programa, la posición de memoria 50 contiene el valor 10, la 51 el valor 1 y la 52 un valor cualquiera. ¿A qué sentencia de control de flujo corresponderı́a entonces este fragmento? 20 21 22 23 24 25 26 27 552 250 827 552 151 352 621 ··· LOAD 52 SUB 50 BP 27 LOAD 52 ADD 51 STORE 52 BP 21 4. Ahora has de hacer lo contrario. Imagina que eres el compilador, y has de traducir una sentencia como: for{i=1;i<10;i++){ j=j+1; } a instrucciones ejecutables en el “computador del enano”. Muestra el código resultante. Ayudas, comentarios,... 1. Un algoritmo como el presentado en la Cuestión 1 es un algoritmo de busqueda, que necesita que el conjunto sobre el que se busca esté ordenado. El presentado en dicha cuestión se conoce como algoritmo de búsqueda binaria, y es el más eficiente. 2. Las cuestiones 3 y 4 son simples una vez que se entiende que nada esencialmente distingue una instrucción de un dato: ambos son números binariamente representados en la memoria de la maquina. Las instrucciones, por tanto, pueden ser modificadas por el programa del que forman parte -aunque es una práctica muy poco recomendable: más abajo encontrarás la explicación-. Considera por ejemplo el siguiente programita de (de [8], p.10): 2 00 01 02 03 04 05 901 360 901 106 902 000 READ STORE 06 READ ADD 06 PRINT STOP Podrı́amos, por ejemplo, sumar 100 al contenido de la posición de memoria 03 y el ADD se convertirı́a en un SUBTRACT. 3. El siguiente ejemplo te ayudará a responder a las cuestiones 3 y 4. Supón que deseas programar el “computador del enano” de modo que hiciera la suma de diez números, situados en posiciones de memoria consecutivas a partir de la 50. Tu programa tendrı́a una forma ası́ (se omite el comienzo y el final para concentrar la atención en la parte que nos interesa). Se supone que en la posición de memoria 83 y 84 hay al comienzo ceros y en la 82 un 1. 11 12 13 14 15 16 17 18 19 ··· 583 114 314 550 184 384 283 182 383 ··· LOAD 83 ADD 14 STORE 14 LOAD 50 ADD 84 STORE 84 LOAD 83 ADD 82 STORE 83 ;En 83 hay un ı́ndice que crece en uno cada iteración ; Se añade el ı́ndice a 14, que contiene una instrucción ;... y se modifica dicha instrucción. ; En la primera pasada, carga lo que hay en 50 ; Lo añade al total en 84... ;... y actualiza dicho total. ; Las instrucciones 17-19 actualizan el ı́ndice. A este fragmento de programa habrı́a que añadir lógica que controlara la terminación (cuando el ı́ndice 83 alcanza el valor 10), etc. Los siguientes detalles son de interés: a) El programa se automodifica: la instrucción 14 cambia de una iteración a otra. Se dice que el código es impuro. Esto es muy poco conveniente: si lo hubiéramos de ejecutar repetidamente, habrı́amos de recargar una copia “frescaçada vez. Además, en un sistema operativo multiusuario, dos o más usuarios no podrı́an compartir la misma copia del programa (¿ves por qué?), lo que es muy ineficiente: por cada usuario habrı́a que cargar una vez el programa, desperdiciando memoria. b) La mayorı́a de los sistemas operativos modernos impiden que un programa se modifique a sı́ mismo del modo ilustrado en el ejemplo. Por ejemplo HP-UX (la versión de Unix que utilizas) fuerza una rı́gida separación entre programas y datos. Un programa no puede alterarse. Se dice entonces que el código es puro ( se habla también de código reentrante), y puede ser compartido por múltiples usuarios. En sistema DOS, todavı́a en uso en muchos ordenadores personales, no es reentrante. c) La descripción del computador del enano es una simplificación extrema. Observa que un programa como el que parcialmente se recoge en 4 requerirı́a una carga 3 absoluta: necesitarı́a ser cargado en una zona precisa de memoria y en ninguna otra. En otro caso, al modificar la posición de memoria 14 no estarı́amos modificando el LOAD que nos interesa, sino otra instrucción diferente, con resultados casi con seguridad catastróficos. d) Observa que el programar directamente en lenguaje máquina es horrendo: ¡Si por azar necesitamos modificar una instrucción de nuestro programa, puede que necesitemos acabar modificando todo el programa! Por ejemplo supón que tuvieramos que emplear la posición de memoria 83 para otra cosa distinta del ı́ndice que guardamos en ella. Imagina que el ı́ndice pasa a la posición 87. Todas las referencias a dicha posición de memoria han de ser sustituı́das por 87. En la práctica, tenemos la ayuda de los llamados ensambladores. Un ensamblador es un compilador de bajo nivel que traduce nombres de instrucciones relativamente mnemónicos a sus respectivos códigos - por ejemplo, transforma STOP en 000 en el caso del computador del enano. Además, permite escribir nombres simbólicos de variables en lugar de posiciones de memoria, o sea: STORE X LOAD Y en lugar de STORE 33 LOAD 34 El ensamblador lee el programa fuente y asigna a cada variable simbólica una ubicación, sin que hayamos de preocuparnos nosotros de hacerlo. 4. Es frecuente que los compiladores permitan traducir un programa fuente en un lenguaje de alto nivel (como C) a código máquina, o a lenguaje de ensamblado. Si deseas ver un programa en ensamblador, puedes tomar cualquiera de los que has hecho en C y compilarlo ası́: cc -Aa -S prog.c Obtendrás como resultado un prog.s que puedes editar y mirar. Cuando lo hagas, verás que dispones de un compilador de C. Ahora que ya lo sabes hacer... ...no es preciso que lo hagas nunca más. 1. No es preciso que programes procedimientos de ordenación en general. Es una labor tan usual que todos los sistemas operativos y paquetes como hojas de cálculo, gestores de bases de datos, etc. incluyen facilidades para la ordenación. Tanto DOS como UNIX disponen de un mandato sort invocable desde el sistema operativo, que permite ordenar ficheros. 2. Si hubieras de hacer una ordenación en el seno de un programa en C, puedes invocar directamente funciones predefinidas; necesitas para ello al comienzo de tu programa un 4 #include <stdlib.h> Dos funciones son de especial utilidad: bsearch y qsort. La primera un procedimiento de búsqueda binaria. La segunda implementando Quicksort, método de ordenación de complejidad O(nlog n), el más rápido que se conoce. Puedes consultar [3] y [4] para ver el modo de utilizar dichas funciones. Referencias [1] A. Aho, J. Hopcroft, and J. Ullman, The Design and Analysis of Computer Algorithms, Addison Wesley, Reading, Ma., 1974. [2] T. Doan, Rats 4.0, Estima, Inc., Evanston, I1., 1992. [3] Hewlett-Packard, HP C/HP-UX Reference Maunal. HP 9000 Series 600/700/800 Computers, Hewlett- Packard Co., Cupertino, Ca., 1991. [4] P. J. Plauger and J. Brodie, C estandar, Microsoft Press/Anaya Multimedia, Madrid, 1990. [5] B. Kernighan and D. Ritchie, The C Programming Language (ANSI C), Prentice Hall, Englewood Cliffs, second edition, 1978. [6] L. Kronsjo, Algorithms: Their complexity and efficiency, Wiley, Chichester, 1986. [7] S. S. U. Ltd., S-Plus Users’s Manual, Statistical Sciences U.K. Ltd., Oxford, 1990. [8] S. Wolfram, Mathematica: A System for doing Mathematics by Computer, AddisonWesley, 1991. 5