Pràctica 2

Anuncio
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
Descargar