ADII 00/01. Práctica 3ª: Estudio experimental de algoritmos recursivos. ALGORITMOS Y ESTRUCTURAS DE DATOS II PRÁCTICA 2ª ESTUDIO EXPERIMENTAL DE ALGORITMOS RECURSIVOS (Sesiones de laboratorio = 2, Total = 3 horas. Curso 2000/01) ÍNDICE 1.- Introducción. 2.- Soporte operacional de la recursión. Coste espacial 2.1.- Estructura de la pila de registros de activación. Paso de parámetros 2.2. – Estudio de algoritmos recursivos. Coste espacial de la recursión 3. – Depuración de programas: gdb, xxgdb 4.- Actividades de laboratorio 4.1.- Mecanismo de llamadas a subprogramas. 4.1.1 Estudio de la función recursiva factorial 4.1.2 Estudio del procedimiento recursivo potencia 4.2.- Implementación y estudio de un algoritmo recursivo: Hanoi 4.2.1 Implementación 4.2.2 Estudio de la complejidad del algoritmo 5.- Cuestiones relativas a la práctica APÉNDICE. Comandos del gdb OBJETIVOS • Conocer el soporte que proporciona el lenguaje PASCAL para la ejecución de subprogramas y su aplicación a la ejecución de rutinas recursivas. • Estudiar el paso de parámetros en las llamadas a subrutinas. • Estudiar el coste espacial asociado a la ejecución de una rutina recursiva. • Conocer el uso de un depurador. BIBLIOGRAFÍA • Wirth, N. Algoritmos + Estructuras de Datos = Programas. Edit. del Castillo, Madrid, 1976. Capítulo 3: Algoritmos recursivos. • Rohl, J.S., Recursion via Pascal. Edit. Cambridge University Press, 1984. En el capítulo 1 se estudia en detalle el funcionamiento de los registros de activación. • Manuales de los comandos gdb y xxgdb, que se pueden obtener utilizando el comando man del sistema operativo. El depurador proporciona una ayuda interactiva (help). 1 ADII 00/01. Práctica 3ª: Estudio experimental de algoritmos recursivos. 1.- INTRODUCCIÓN Se dice que un algoritmo es recursivo si entre sus instrucciones aparece algún tipo de referencia a sí mismo, esto es, si la computación que el algoritmo realiza está parcialmente expresada en términos de sí mismo. EJEMPLO. Una definición habitual de la función factorial para un número n mayor que 0 es: n!=n·(n-1) ·(n-2) ·....2·1 (1) Además, 0! es 1 por definición. Esta función se puede implementar mediante una iteración: {n≥0} función factorialI(n:entero) devuelve entero; var f,i: entero fvar f:=1; {f=1!=0!} Para i:=2 hasta n hacer {f=(i-1)!} f:=i*f {f=i(i-1)!=i!}; factorialI:=f ffunción {factorialI(n)=n!} cuya transcripción a un lenguaje de programación estructurado como PASCAL es inmediata. En la definición (1) dada para n!, y tal como se ha tenido en cuenta en los asertos que se ha incluido en la función anterior, se puede notar que, excepto para n=0, se tiene n!=n·(n-1)! Ello hace pensar que en lenguaje algorítmico, se podría definir una función recursiva factorialR, cuyo código tuviera la siguiente estructura: {n≥0} función factorialR(n:entero) devuelve entero; opción n>0: devuelve n*factorialR(n-1) n=0: devuelve 1 fopción ffunción {factorialR(n)=n!} La transcripción de esta función a un lenguaje de programación puede parecer problemática, dado que se está haciendo uso de la función factorialR para dar la definición de la propia función. Sin embargo, lenguajes como PASCAL , ADA y C sí que permiten y soportan este tipo de definiciones recursivas. En el apartado 2 se recuerda cuál es el mecanismo que resuelve las llamadas a procedimiento o función, y en el apartado 3 se muestra el uso de un depurador, que entre otras cosas permitirá observar el estado de la pila de llamadas a lo largo de la ejecución de un programa. Mediante un ejemplo se verá cómo este mecanismo es la base de la ejecución de una rutina recursiva. Las actividades 4.1.1 y 4.1.2 servirán para ilustrar este mecanismo. Finalmente, en la actividad 4.2 se propone la implementación y el estudio de la solución recursiva al problema de las Torres de Hanoi. 2 ADII 00/01. Práctica 3ª: Estudio experimental de algoritmos recursivos. 2.- SOPORTE OPERACIONAL DE LA RECURSIÓN. COSTE ESPACIAL 2.1.- Estructura de la pila de registros de activación. Paso de parámetros La invocación de un subprograma, función o procedimiento, implica la ejecución de las instrucciones asociadas a dicho subprograma con el entorno (información local y global) definido para el mismo. Para la información local (parámetros y variables) de la llamada, el compilador reserva un espacio de memoria destinado a mantener esta información sobre la que opera el subprograma. Al finalizar la ejecución del subprograma la zona de memoria asignada es liberada, tras lo cual puede ser reutilizada posteriormente. Es importante señalar que la reserva de memoria para la información local está asociada a la llamada al subprograma y no al propio subprograma. El programa sólo tiene acceso a la memoria asociada a la llamada activa y a la de los subprogramas de menor nivel. Tal como se estudió en la práctica 3 de IPRAD1, “Subprogramación”, esta información local se estructura en una pila de registros de activación. En dicha práctica se mostraba el siguiente ejemplo. x x! . Se podría escribir un EJEMPLO. Considérese la función combina(x, y) = = y y! (x − y )! programa Pcombina que contuviese las siguientes definiciones, y que efectuase la llamada combina(5,2,Nocomb), en donde Nocomb sería una variable del programa. {n≥0} function factorialI(n:integer): integer; var f,i: integer; begin {4} f:=1; For i:=2 to n do f:=i*f; factorialI:=f {5} end; {factorialI(n)=n!} {x>=y>=0} procedure combina(x,y:integer; var res:integer); var i,j,k: integer; begin {1} i:=factorialI(x); {2} j:=factorialI(x-y); {3} k:=factorialI(y); res:=i div (j*k) end; {combina} {res=x sobre y=x!/[(x-y)!y!]} ADII 00/01. Práctica 3ª: Estudio experimental de algoritmos recursivos. En la figura se ha sombreado el registro que en este estado permanece inaccesible al programa. Es especialmente interesante recordar y subrayar los siguientes puntos: 1. 2. El registro de activación asociado a la llamada a cualquier función o procedimiento permanece en memoria hasta que esta llamada acaba. Por ejemplo, en el estado mostrado en la figura anterior, el registro asociado a combina permanece en memoria hasta que se completa su ejecución, después de completarse las tres llamadas factorialI(x), factorialI(x-y), factorialI(y). En el caso en que dos o más subrutinas tengan una variable local o parámetro con el mismo nombre, no hay ambigüedad sobre su uso. En el ejemplo anterior, a pesar de que combina y factorialI tienen una variable local con el mismo nombre –i-, el programa entiende que cualquier acción sobre la variable i se refiere a la que está ubicada en el registro activo. Son estas características de la pila las que van a permitir la correcta ejecución de funciones y procedimientos definidos recursivamente. 2.2. – Estudio de algoritmos recursivos. Coste espacial de la recursión Mediante un ejemplo se va a estudiar el comportamiento de la pila de registros cuando se invoca a una rutina definida recursivamente. La llamada a la rutina desencadena una serie de registros de activación en la pila. Es importante señalar que cada registro de activación está asociado a la llamada al subprograma y no al propio subprograma, por lo que distintas llamadas a un mismo subprograma, como ocurre en el caso en que hay recursión, provocan la reserva de distintas registros de activación, uno para cada llamada. EJEMPLO. Supóngase la transcripción a PASCAL de la función recursiva factorialR definida en el apartado 1. Se desea estudiar la secuencia de registros de activación que se produce para ejecutar la instrucción num:=factorialR(2) Se observará el estado de la pila en los puntos {1} y {2} del código de la función: Supóngase que se dispusiese de algún mecanismo para marcar los puntos {1},{2},{3},{4} y {5} para que la ejecución se detuviese al alcanzar dichos puntos, y que se pudiese examinar la pila. Entonces, por ejemplo, alcanzado el punto {5} después de pasar por {1}, y antes de llegar a {2}, se podría observar lo siguiente : FactorialI(n=5) f=120, i=5 FactorialI=120 Combina(x=5,y=2,res=@) { n >= 0 } function factorialR(n: integer): integer; begin {1} if n > 0 then factorialR:= n * factorialR(n - 1) else factorialR:=1 {2} end; { factorialR(n) = n! } La invocación a factorialR(2), hará que se cargue el registro de activación correspondiente, y que la ejecución del programa continúe por el código de dicha función. Si al llegar al punto {1} se consulta el estado de la pila aparecen dos registros, que se numeran en orden inverso a su antigüedad en la pila. i=?, j=?, k=? Pcombina() @: Nocomb=? 3 Al proseguir la ejecución y ser n>0, se invoca a factorialR(n-1). Esta nueva llamada supone otro registro de activación en la pila, y que la ejecución vuelva a entrar en el código de la función factorialR. Dado que el programa sólo tiene acceso a la memoria asociada a la llamada activa y a la de los subprogramas de menor nivel al de dicha llamada, cuando se 4 ADII 00/01. Práctica 3ª: Estudio experimental de algoritmos recursivos. nombra una variable o parámetro de una rutina recursiva, no hay ambigüedad, y se sabe que se está referenciando las variables y parámetros asociados a la llamada. Las sucesivas llamadas producirían la siguiente secuencia de estados: n=1, factorialR=? n=1, factorialR=? n=2, factorialR=? n=2, factorialR=? n=2, factorialR=? num=? num=? num=? Para la última llamada, se da valor a la variable factorialR en el registro activo, y al pasar la ejecución por el punto {2} se podría observar el cambio en dicho registro: n=0, factorialR=1 n=1, factorialR=? n=1, factorialR=1 n=2, factorialR=? n=2, factorialR=? n=2, factorialR=2 num=? num=? num=? num=2 Se puede definir el coste espacial de una llamada a función o procedimiento como el tamaño de la zona de memoria que se necesita para ejecutar dicha llamada. Si la rutina es recursiva, completar la ejecución de una llamada exige disponer en memoria su propio registro de activación, así como los asociados a las llamadas sucesivas que se generan. En ese caso: coste espacial = p∗ tamaño del registro de activación en donde p es el máximo número de registros que llegan a existir simultáneamente en memoria a lo largo de la ejecución de la llamada. <----- factorialR:=1 n=1, factorialR=? n=2, factorialR=? num=? Entonces, acaba la ejecución de la llamada activa, se libera su registro de activación, y continúa la ejecución de la llamada factorialR(1): n=0, factorialR=1 ------------------> n=1, factorialR=? factorialR:=n*factorial(n-1) n=1, factorialR=1 n=2, factorialR=? En el registro activo: n=1 n=2, factorialR=? num=? La secuencia de retornos que se obtendría sería: n=0, factorialR=1 n=0, factorialR=? num=? ADII 00/01. Práctica 3ª: Estudio experimental de algoritmos recursivos. num=? 3.- DEPURACIÓN DE PROGRAMAS: gdb, xxgdb Un depurador (debugger) es una herramienta software que permite la ejecución de un programa instrucción a instrucción, al tiempo que hace posible examinar aspectos relevantes de la ejecución, como el estado de las variables en un instante dado, las llamadas y los retornos de los subprogramas, etc. Para programas escritos en PASCAL, en el laboratorio se dispone de un depurador, el gdb, (gnu debugger). Con gdb se pueden depurar los programas compilados con los compiladores de GNU, como gpc y gcc. Fundamentalmente, un depurador permite: 1. Ejecutar un programa instrucción a instrucción (ver comandos de control de la ejecución en el apéndice). También se puede detener la ejecución en un punto dado, estableciendo lo que se denomina un punto de ruptura (ver comandos de gestión de los puntos de ruptura en el apéndice). 2. Una vez detenida momentáneamente la ejecución del programa, se puede consultar el estado de las variables (ver comandos de visualización de variables en el apéndice). 3. También se pueden examinar las llamadas y retornos de los subprogramas. 4. Examinar los registros de activación existentes en la pila en un momento dado de la ejecución del programa memoria (ver comandos de visualización de la pila en el apéndice). EJEMPLO. Considérese la figura del apartado 2.1, en la que se mostraba un estado de la pila de llamadas desencadenadas por combina(5,2,Nocomb). Lo que se muestra a continuación correponde a una ejecución real del programa, detenida en el mismo estado que se muestra en 5 6 ADII 00/01. Práctica 3ª: Estudio experimental de algoritmos recursivos. dicha en figura. Si en dicho estado se pide al depurador que muestre la pila de registros, (comando bt) proporciona la siguiente información: #0 #1 #2 #3 #4 Factorial_i (N=5) at Pcombina.p:11 0x804941f in Combina(X=5,Y=2,Res=@0x805b674) at Pcombina.p:21 0x8049573 in program_Pcombina() at Pcombina.p:32 0x80495e8 in main (argc=1,argv=0xbffffac4,envp=0xbffffacc) at Pcombina.p:35 0x4004bcb3 in __libc_start_main ......... Los registros aparecen numerados en orden inverso a su aparición, siendo el registro 0 el activo, correspondiente a Factorial_I(5). El registro 1 corresponde a la llamada combina(5,2,nocomb), y el registro 2 al programa principal Pcombina. Los dos últimos registros (3 y 4) corresponden a información propia del compilador. Cabe destacar que en el registro 2, aparece como valor del parámetro Res la dirección de la variable Nocomb que se ha pasado por referencia en esa llamada. Ello se puede comprobar interrogando al depurador por la dirección de Nocomb (ver comando printf), que confirmaría que es 0x805b674. El depurador permite examinar en detalle cada uno de los registros. Por ejemplo el examen del registro 0 indicaría que sus variables locales son (ver comando info locals): retval_Factorial_i = 120 F = 120 I = 5 Nótese que se usa una variable no declarada explícitamente retval_Factorial_i en la que se dispone el valor que retorna la función. También puede dar información más detallada de los parámetros que se pasan por referencia. Por ejemplo, aunque el registro activo siempre es el más reciente en la pila (#0), se le puede pedir al depurador que se disponga a inspeccionar el registro 1 (ver comando frame). Entonces, si se escribe el contenido del parámetro formal Res, (ver comando print) la respuesta es: (Integer &) @0x805b674: 0 indicando que el parámetro real es una variable entera que ocupa la dirección 0x805b674, y cuyo contenido es 0. En este caso, el valor del parámetro todavía no ha sido calculado, y el valor 0 corresponde a lo que el depurador encuentra en esa posición de memoria, interpretado como un entero. En el laboratorio, en el entorno de ventanas X del sistema operativo, se podrá usar el depurador gdb desde la interfaz gráfica xxgdb, la cual proporciona diversas facilidades, como el uso de botones para algunos de los comandos citados en los párrafos anteriores, el manejo del ratón para recorrer las líneas de código y situar el cursor, etc... 7 ADII 00/01. Práctica 3ª: Estudio experimental de algoritmos recursivos. 4.- ACTIVIDADES DE LABORATORIO 4.1 Mecanismo de llamadas a subprogramas Las actividades a realizar en el laboratorio consisten en la ejecución desde el depurador xxgdb de dos subprogramas recursivos (factorial y potencia). La resolución de las llamadas a los subprogramas se examinará visualizando la sucesión de registros de activación que se generan. En /practicas/asignaturas/practicas/ad2alum/pract3 se dispondrá de los ficheros Pfactorial.p y Ppotencia.p con los programas que definen y usan estos subprogramas. 4.1.1 Estudio de la función recursiva factorial Se compilará el programa Pfactorial.p con las opciones adecuadas (ver el Apéndice) y se ejecutará desde el depurador, examinando la pila en las entradas y retornos de cada llamada, y poder comparar el coste espacial del factorial recursivo con el de la versión iterativa descrita en el apartado 2.1 (ver la cuestión 3 del boletín). El subprograma que se estudiará es el descrito en el apartado 2.2 de este boletín. 4.1.2 Estudio del procedimiento potencia Se compilará el programa Ppotencia.p y se ejecutará desde el depurador, para poder examinar la pila de llamadas y los resultados obtenidos (ver la cuestión 4 de este boletín). El subprograma cuya ejecución se estudiará, es un procedimiento que calcula la potencia entera de un número entero, sugiriéndose probar el cálculo de 3^2. Dicho subprograma se muestra a continuación: { x > 0 y >= 0 } Procedure potencia(x,y: Integer; var result: Integer); var aux:integer; Begin if y = 0 then result := 1 else begin potencia(x,y-1,aux); result := x * aux end End; Obsérvese que el tercer parámetro de este procedimiento se pasa por referencia, ya que es de salida. Por lo tanto, para hacer una llamada inicial a este procedimento desde el programa principal, será necesario utilizar una variable global en la posición del parámetro pasado por referencia. En la práctica se deberá estudiar la evolución de esta variable, y relacionarla con la evolución de la variable local aux y del parámetro variable result de cada registro de activación. 4.2.- Implementación y estudio de un algoritmo recursivo: Torres de Hanoi El problema se plantea de la manera siguiente: Se dispone de tres torres numeradas de 1 a 3, y de N discos de diferentes tamaños. Al principio todos los discos se encuentran en una de las torres (torre 1) apilados en forma decreciente según su diámetro. Se deben desplazar a la torre número 3, utilizando como auxiliar la torre 2. Se imponen las siguientes limitaciones: 1. sólo se puede desplazar un disco cada vez, y 2. en cualquier situación intermedia, no puede haber un disco de mayor diámetro situado sobre uno más pequeño. 8 ADII 00/01. Práctica 3ª: Estudio experimental de algoritmos recursivos. Una solución recursiva a este problema la da el siguiente algoritmo: {n>=1} Procedimiento Hanoi (n:integer; orig, dest, aux:torre); Opción n=1: mueve_disco(orig,dest); n>1: Hanoi (n-1, orig, aux, dest); mueve_disco(orig,dest); Hanoi(n-1,aux,dest,orig); Fopción fprocedimiento; 4.2.1 Implementación Se debe escribir un programa en PASCAL que contenga este procedimiento, y se ejecutará Hanoi(N,1,3,2), para un número N de discos que se leerá del input. Para visualizar la resolución del problema, la acción mueve_disco(torre1,torre2) deberá implementarse para que escriba por pantalla entre qué dos torres se realiza el movimiento. El tipo torre se definirá como un subrango del integer desde 1 hasta 3. Se puede comprobar que el programa funciona correctamente con un número de discos pequeño (2 ó 3). 4.2.2 Estudio de la complejidad del algoritmo Una alternativa a la medida de tiempos del sistema consiste en contabilizar los pasos de programa que en función de la talla, realiza el algoritmo para resolver el problema planteado. En el caso de algoritmos recursivos puede ser interesante estudiar el número de operaciones significativas que se realizan y el número de llamadas recursivas que se desencadenan en la resolución del problema. Por ejemplo, para el problema de las torres de Hanoi se propone estudiar: • Número total de movimientos de discos, • Número total de llamadas recursivas realizadas, • Número total de llamadas recursivas al caso base. El método a seguir constará de los siguientes pasos: 1. Cambiar la cabecera del procedimiento añadiendo tres parámetros : Procedure Hanoi(n:integer;orig,dest,aux:torre; var nmov, nllam,nllamcb:integer); que deberán modificarse adecuadamente dentro del procedimiento para que cuenten, repectivamente, el número de movimientos (nmov), el número de llamadas (nllam) y el número de llamadas con el caso base (nllamcb). Estos contadores deberán de estar iniciados a 0 antes de la primera llamada. 2. Ejecutar el programa, con tallas 2, 3, 4, ...., 20. Los cálculos realizados se presentarán en una tabla con un formato similar al siguiente: # Talla No.Mov # -----------2 3 3 7 ................. No.Llam ------3 7 No.LlamCB --------2 4 3. Comparar los resultados obtenidos con lo que se prevé teóricamente (ver cuestiones 8, 9 y 10 del boletín). 5.- CUESTIONES RELATIVAS A LA PRÁCTICA 9 ADII 00/01. Práctica 3ª: Estudio experimental de algoritmos recursivos. 1.- Suponer que en el algoritmo de búsqueda binaria de la práctica 2 se modifica el parámetro de tipo vector y se pasa por valor. ¿Cómo repercute esta modificación en el coste temporal del algoritmo?. ¿Qué ocurriría si la modificación se realiza sobre el algoritmo de búsqueda secuencial?. ¿Cómo afectaría al coste en memoria en ambos algoritmos?. 2.- En la siguiente tabla se muestran los tiempos de ejecución que se obtienen para el producto escalar de dos vectores que se ha implementado en la primera práctica. En la columna Tref se muestran los tiempos cuando la función se ha implementado con el perfil Function ProdEsc(var u,v:vector): real; mientras que los tiempos de la columna Tval corresponden a una implementación con el perfil Function ProdEsc(u,v: vector): real; Teniendo en cuenta cómo se tratan los parámetros en el registro de activación correspondiente a una llamada a subrutina, según se pasen por valor o por referencia, y que el tipo vector se definió como array[1..10000] of real, explicar la diferencia entre los tiempos obtenidos. #Medidas de # Talla # ----1000 2000 3000 4000 5000 6000 7000 8000 9000 10000 tiempo: Producto escalar de vectores TRef Tval ------130 1210 300 1430 450 1640 600 1800 760 2010 890 2190 1070 2390 1210 2610 1350 2830 1510 3050 3. – Comparar el coste espacial y el coste temporal en función de n, del cálculo de n!, cuando se implementa con la función factorialI(n) y con la función factorialR(n), descritas en la práctica. 4.- Considérese el programa Ppotencia de la práctica, en el que se hubieran marcado {1} y {2} como puntos de ruptura dentro del código de potencia: {x>0, y>=0} procedure potencia(x, y: integer; var res: integer); var aux: integer; begin {1} if y = 0 then res:=1 else begin potencia(x,y-1,aux); res:=x*aux end {2} end; { res = x ^ y } Supóngase que la ejecución del programa se hubiera detenido en {2}. Escribir en los recuadros al efecto, la respuesta que daría el depurador gdb a los comandos dados. Justificar la respuesta. (gdb) bt #0 Potencia (X=4, Y=0, Res=@0xbffffa40) at Ppotencia.p:14 #1 0x80493e2 in Potencia (X=4, Y=1, Res=@0xbffffa58) at Ppotencia.p:16 #2 0x80493e2 in Potencia (X=4, Y=2, Res=@0x805b59c) at Ppotencia.p:16 #3 0x80494af in program_Ppotencia () at Ppotencia.p:25 10 ADII 00/01. Práctica 3ª: Estudio experimental de algoritmos recursivos. #4 0x8049548 in main (argc=1, argv=0xbffffac4, envp=0xbffffacc) at Ppotencia.p:27 #5 0x4004bcb3 in __libc_start_main ......... (gdb) continue Continuing. Breakpoint 2, Potencia (X=4, Y=0, Res=@0xbffffa40) at Ppotencia.p:19 end; {potencia} (gdb) print Res $1 = (Integer &) @0xbffffa40: 1 (gdb) frame 1 #1 0x80493e2 in Potencia (X=4, Y=1, Res=@0xbffffa58) at Ppotencia.p:16 potencia(x,y-1,aux); (gdb) print Aux ADII 00/01. Práctica 3ª: Estudio experimental de algoritmos recursivos. function fib(n: integer):integer; var f1, f2:Integer; Begin If (n=1) or (n=0) then fib:= n else Begin f1:=fib(n-1); f2:=fib(n-2); fib:=f1+f2 End End; El cálculo de fib(n) se despliega o “ramifica” en el cálculo de fib(n-1) y de fib(n-2), lo que se puede representar gráficamente como: fib(n) . . . (gdb) continue Continuing. Breakpoint 2, Potencia (X=4, Y=2, Res=@0x805b59c) at Ppotencia.p:19 end; {potencia} (gdb) bt #0 Potencia (X=4, Y=2, Res=@0x805b59c) at Ppotencia.p:19 #1 0x80494af in program_Ppotencia () at Ppotencia.p:25 #2 0x8049548 in main (argc=1, argv=0xbffffac4, envp=0xbffffacc) at Ppotencia.p:27 #3 0x4004bcb3 in __libc_start_main (main=0x804952c ...... (gdb) print Res fib(n-1) fib(n-2) Nótese que antes de ejecutar fib(n-2) se debe terminar la ejecución de fib(n-1), lo que quiere decir que los registros de activación asociados a ambas llamadas no llegan a coexistir en memoria. Con esta representación en árbol, la estructura de llamadas asociada al cálculo de fib(4) quedaría: fib(4) fib(3) fib(2) fib(2) fib(1) fib(1) fib(0) (gdb) print Resultado fib(1) (gdb) continue Continuing. fib(0) a) Numerar el orden en que se generan las sucesivas llamadas de la figura anterior. b) ¿Cuántos registros de activación llegan a existir simultáneamente en memoria para fib(4)?, ¿y para fib(n)?. 4 elevado a 2 es 16 Program exited normally. 5.- Otro posible método para contabilizar el número de movimientos y de llamadas del procedimiento Hanoi consistiría en definir los contadores nmov, nllam, nllamcb como variables globales del programa principal en lugar de parámetros del subprograma, y que éste las modificara como efecto lateral. Reescribir el programa del apartado 4.2.2 siguiendo este método. ¿Podrían haberse definido estos contadores como variables locales del subprograma?. 6.- Representar gráficamente con gnuplot (o herramienta equivalente) los valores obtenidos en el apartado 4.2.2 en función de la talla. Escribir unas relaciones de recurrencia que expresen el número de movimientos de discos realizados en función de la talla del problema. Resolver la recurrencia y comparar el resultado obtenido con los obtenidos en el laboratorio. 7.- Supóngase la siguiente implementación en PASCAL de la función de Fibonacci: { n >= 0} 11 8.- Estudiar el coste espacial del algoritmo recursivo Hanoi para N discos. Dibujar el árbol de llamadas que se producen para N=4. Demostrar analíticamente que, para cualquier N, el número total de llamadas es una menos que el doble de llamadas al caso base. 9.- Suponer que el algoritmo Hanoi gobernara un robot que pudiese mover un disco de una torre a otra en una décima de segundo. ¿Cuánto tiempo le costaría al robot trasladar una torre de 64 discos?. 10.- El siguiente procedimiento es una variante correcta del algoritmo Hanoi presentado: {n>=0} Procedimiento Hanoi2(n: integer; orig,dest,aux: torre); Opción n=0: instrucción vacía; n>0: Hanoi2(n-1,orig,aux,dest); mueve_disco(orig,dest); 12 ADII 00/01. Práctica 3ª: Estudio experimental de algoritmos recursivos. Hanoi2(n-1,aux,dest,orig); fopción fprocedimiento; Este algoritmo se diferencia del anterior en que se admite como subproblema más pequeño posible el de la torre vacía (n=0), que se toma como caso base, y se resuelve trivialmente, dado que no hay ningún disco que trasladar. Con este tratamiento de Hanoi(0,t1,t2,t3), el caso general del algoritmo también trata correctamente el caso n=1, y por ello se ha ADII 00/01. Práctica 3ª: Estudio experimental de algoritmos recursivos. • Como prerrequisito a la depuración, se debe añadir la opción -g al compilar el código gpc -g -o programa.out programa.p fuente: • Para invocar al depurador, se pasa como parámetro el nombre del ejecutable: xxgdb programa.out Comandos de control de la ejecución run Ejecuta una instancia del programa desde el principio hasta el final, o hasta el siguiente punto de ruptura A esta solución, se le puede objetar que es peor que la anterior, porque se hacen llamadas con un argumento n=0 que antes no se hacían. Se puede pensar que al resolverse con la continue Continúa la ejecución hasta el final, o hasta el siguiente punto de ruptura step Se ejecuta una línea de código instrucción vacía, estas llamadas recursivas deben ser muy poco costosas y su contribución al stepi Se ejecuta una instrucción coste temporal total del algoritmo se puede despreciar. Para comprobarlo, se ha medido el tiempo de ejecución de los dos algoritmos en función de la talla, con el siguiente resultado: kill Termina (mata) la ejecución del proceso en curso quit Salir del depurador englobado en dicho caso general. # Talla Hanoi Hanoi2 # ----------------------------10 0.5 0.8 11 1.0 1.6 12 2.1 3.1 13 4.2 6.3 14 10.0 10.0 15 20.0 20.0 16 30.0 60.0 17 60.0 100.0 18 140.0 200.0 19 270.0 400.0 20 530.0 810.0 21 1070.0 1630.0 22 2150.0 3300.0 23 4320.0 6470.0 24 8630.0 12840.0 (tiempo en mseg.) Para explicar este aumento de tiempo de Hanoi2, contar cuántas llamadas recursivas más hace Hanoi2 con repecto a Hanoi, para una n dada. ¿Cómo aumenta el coste espacial de Hanoi2 con respecto a Hanoi?. 11.- ¿Por qué el siguiente algoritmo recursivo no resuelve correctamente el problema de las torres de Hanoi? {n>=1} Procedimiento HanoiE(n:integer; orig, dest, aux:torre); Opción n=1: mueve_disco(orig,dest); n>1: mueve_disco(orig,dest); HanoiE(n-1, orig, aux, dest); HanoiE(n-1,aux,dest,orig); fopción fprocedimiento; Comandos de visualización de variables print var Visualiza el estado de la variable var print expresión Evalúa y visualiza expresión printf "%p", &var Escribe la dirección de var, en hexadecimal Comandos de visualización de la pila bt (botón stack) Muestra el estado de la pila frame n El depurador se sitúa en el registro nº n, para su posible inspección, y muestra su contenido. Por defecto, es el registro activo info locals Muestra las variables locales del registro en el que se ha situado el depurador info args Muestra los parámetros del registro en el que se ha situado el depurador Comandos de gestión de los puntos de ruptura break n Establece un punto de ruptura en la línea n delete Borra el punto de ruptura indicado Los comandos sombreados aparecen como botones en la interfaz xxgdb. APÉNDICE. Comandos del gdb. 13 14