19. RECURSION 19.1. Conceptos. Cualquier procedimiento o función pueden contener, en su bloque de acciones ejecutables, llamados a otros procedimientos que sean accesibles. Si puede llamarse a cualquier procedimiento accesible, entonces un procedimiento puede llamarse a sí mismo. Esta autoreactivación se denomina recursión. Una cadena de llamados recursivos debe terminar en algún momento; por esta razón los subprogramas recursivos deben colocar dentro de una instrucción condicionada el llamado recursivo. El empleo de un identificador de subprograma dentro del texto del mismo subprograma implica la ejecución recursiva del mismo. Más específicamente, la ocurrencia de un llamado a una función, dentro de una expresión perteneciente al bloque asociado a la misma función, implica la ejecución recursiva de la función. Su aplicación es natural en aquellos algoritmos definidos recursivamente; y también cuando se emplean estructuras de datos que hayan sido definidas recursivamente. Sin embargo, siempre es posible desarrollar un algoritmo repetitivo en lugar del recursivo; lo cual trá ventajas en menor ocupación de memoria y mayor velocidad de ejecución. Cada vez que se realiza una autoinvocación, se crea espacio para variables y parámetros valor; es decir, a medida que aumentan los llamados recursivos existen varias instancias (o encarnaciones) de las variables. Así también habrá que considerar todas las instrucciones que permitan retornar a los diferentes puntos de llamado. Esto también implica que el programador debe asegurarse que los niveles de recursión no sean excesivamente altos; ya que esto podría copar la memoria disponible. Un objeto se dice recursivo si está definido en términos de sí mismo. 19.2. Ejemplos de definiciones recursivas. Prof. Leopoldo Silva Bijit. 07-07-2003 242
El concepto de recursión es particularmente poderoso en definiciones matemáticas; veamos algunos ejemplos: a) Número natural: i) 1 es un número natural. ii) el sucesor de un número natural es un número natural. b) La función factorial, para enteros no negativos: i) 0! = 1 ii) Si n>0 entonces n! = n*(n-1)! c) Máximo común divisor, para enteros positivos. mcd(m, n) i) mcd(m, 0) = m ii) Si n>0 entonces mcd(m, n) = mcd(n, m mod n) d) Números de Fibonacci: i) fib(0) = 1; fib(1) = 1 ii) fib(n) = fib(n-1)+fib(n-2) para n>1 En Pascal, pueden diseñarse, casi directamente las siguientes funciones: a) function fact(i:integer):integer; if i>0 then fact:=i*fact(i-1) else fact:=1 b) function mcd(m,n:integer):integer; if n=0 then mcd:=m else mcd:=mcd(n,m mod n) c) function fib(n:integer):integer; if n=0 then fib:=0 Prof. Leopoldo Silva Bijit. 07-07-2003 243
else if n=1 then fib:=1 else fib:=fib(n-1)+fib(n-2) UNIVERSIDAD TECNICA FEDERICO SANTA MARIA Algunos de estos ejemplos han sido desarrollados antes por algoritmos repetitivos. Nótese que los llamados recursivos están dentro de estructuras de control condicional. La potencia de la recursión se basa en la posibilidad de definir un conjunto infinito de objetos por una sentencia finita. Del mismo modo un número infinito de computaciones puede describirse por un programa finito recursivo. Las etapas previas de compilación, el análisis léxico y sintáctico pueden describirse recursivamente; por esta razón los programas que los implementan suelen ser recursivos. Se han ilustrado algunas funciones recursivas, pero también es posible desarrollar procedimientos recursivos. 19.3. Ejemplos. 19.3.1. Permutaciones. Una permutación de una secuencia de objetos es una redisposición de los mismos. Por ejemplo las permutaciones de 123 son: 123, 132, 213, 231, 312 y 321. Permutando las letras de una palabra se obtiene un anagrama. Por ejemplo: roma, amor, omar, mora, ramo. El número de permutaciones de n objetos es n!. En la primera posición es posible colocar cualesquiera de los n objetos; en la segunda existen n-1 posibilidades, ya que se asume utilizado un objeto en la primera posición. En la última posición sólo es posible colocar el que queda. El siguiente programa genera las permutaciones de 123. program permute(output); var i,j,k : integer; Prof. Leopoldo Silva Bijit. 07-07-2003 244
for i:=1 to 3 do for j:=1 to 3 do for k:=1 to 3 do if (i<>j) and (i<>k) and (j<>k) then writeln(i,j,k);. El programa efectúa 3*3*3 iteraciones; es decir, 27. Mientras que el número de permutaciones es 3! ; es decir, 6. Una forma de disminuir las iteraciones es por ejemplo, no realizar el tercer lazo (el más interno) si las dos variables anteriores son iguales. Pero si se desea obtener las permutaciones de un número de objetos mayor (y que no sean necesariamente números) debe pensarse en un algoritmo diferente, que sea más eficiente. Si se escriben las permutaciones de 1234 y se analiza en detalle la secuencia, podremos plantear para las permutaciones de n objetos distintos a[1], a[2],..., a[n] el siguiente algoritmo recursivo. Sea permute(n) la acción que obtiene las permutaciones de los n objetos. Su implementación se descompone en: a) Se mantiene a[n] en su lugar; y se generan todas las permutaciones de los n-1 objetos restantes. Es decir se invoca a permute(n-1). b) Se repite a) previo cambio de a[n] con a[1]. c) Se sigue repitio a) efectuando el cambio a[n] con a[i] para i=2 hasta n-1. Debe observarse que el procedimiento recursivo permute emplea un parámetro valor. También cuando el número de objetos a permutar sea igual a uno, se trá una permutación lista para ser impresa. procedure permute(k:integer); var i : integer; if k=1 then salida else {se mantiene a[k] en su lugar} Prof. Leopoldo Silva Bijit. 07-07-2003 245
permute(k-1); {se generan las permutaciones de los k-1 objetos restantes} for i:=1 to k-1 do cambio de a[k] con a[i]; {pasos b y c} permute(k-1); se vuelve a[k] a posicion original; El siguiente programa permite generar permutaciones de caracteres. program permutaciones(input,output); {$A- Esta es una orden para el Compilador Turbo-Pascal} var n : integer; a : array[1..5] of char; procedure lea(var ch:char); read(kbd,ch) {lee sin eco; depe del compilador} procedure entrada; {llena arreglo de largo variable} var ch : char; write('->entre una secuencia de caracteres, terminada en punto: ') n:=0; lea(ch); while ch <> '.' do n:=n+1; a[n]:=ch; write(ch); lea(ch) writeln; procedure salida; var i : integer; for i:=1 to n do write(a[i]); Prof. Leopoldo Silva Bijit. 07-07-2003 246
writeln procedure permute(k:integer); procedure con_el_i_en_k; procedure cambio_k_por_i; var t : char; t:=a[i]; a[i]:=a[k]; a[k]:=t; {La llamada a permute (k-1) equivale a ir decrementando k} var i : integer; {con el i en k} for i:=1 to k-1 do cambio_k_por_i; permute(k-1); cambio_k_por_i {permute} if k=1 then salida else permute(k-1); {cona[k]fijo,genera las permutaciones de los k- 1 objetos precedentes.} con_el_i_en_k {repite con a[i] en el lugar de 1. } {principal} a[k], para i desde 1 a k- Permutaciones Entrada Salida Permute con_el_i_en_k Cambio Prof. Leopoldo Silva Bijit. 07-07-2003 247
entrada; permute(n). El diagrama anterior muestra la estructura de los bloques. SALIDA debe estar antes de PERMUTE, para ser accesible desde este último. CAMBIO es accesible sólo desde con_el_i_en_k. 19.3.2. Coeficientes Binomiales. Los coeficientes del binomio, suelen describirse por el triángulo de Pascal. 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1... Emplearemos la notación: Se tienen: n Cnk (, ) = k Prof. Leopoldo Silva Bijit. 07-07-2003 248
C(n,0) = 1 n >=0 C(n,k) = 0 n < k C(n,k) = C(n-1,k)+C(n-1,k-1) 0<=k<=n Las relaciones anteriores definen recursivamente al coeficiente del binomio; es inmediata entonces, la codificación: function C(n,k:integer):integer; if k=0 then C:=1 else if n<k then C:=0 else C:=C(n-1,k)+C(n-1,k-1) Pero también: Que puede escribirse: C(n,k) = n! / k!(n-k)! C(n,k) = (n-k+1)(n-k+2)..(n) / 1*2*3*..*k La siguiente función desarrolla el coeficiente binomial en forma no recursiva: function C(n,k:integer):integer; var num,den : integer; num:=1; den:=1; for j:=1 to k do num:=num*(n+1-j); den:=den*j C:=num div den Prof. Leopoldo Silva Bijit. 07-07-2003 249
19.3.3. Generador de Combinaciones. Analizar el algoritmo y colocar comentarios. program combinaciones(kbd,output); var m,n : integer; a : array[1..10] of integer; b : array[1..10] of char; procedure entrada; var ch : char; write('->entre secuencia de caracteres, terminada en punto: '); m:=0; read(kbd,ch); while ch <> '.' do m:=m+1; b[m]:=ch; write(ch); read(kbd,ch); a[m]:=m writeln; repeat write('->elementos de la combinacion: '); read(n); if n>m then write('<-error debe ser menor que: ',m+1); writeln until n<=m; procedure salida; var i : integer; writeln; for i:=1 to n do write(b[a[i]]) procedure combine; var p,b : integer; salida; {escribe la inicial} p:=1; while p<=n do Prof. Leopoldo Silva Bijit. 07-07-2003 250
if a[n+1-p] < m+1-p then b:=a[n+1-p]; while p>=1 do a[n+1-p]:=b+1; b:=b+1; p:=p-1 salida; p:=1 else p:=p+1 UNIVERSIDAD TECNICA FEDERICO SANTA MARIA writeln('generacion de combinaciones'); entrada; combine. Prof. Leopoldo Silva Bijit. 07-07-2003 251
19.3.4. Análisis sintáctico. UNIVERSIDAD TECNICA FEDERICO SANTA MARIA Se desea saber si una determinada secuencia de caracteres cumple las reglas sintácticas siguientes : <expresión> ::= <término>{'+' '-'<término>} <término> ::= <factor>{ '*' '/'<factor>} <factor> ::= <letra> '('<expresión> ')' '['<expresión>']' Nótese que <expresión> está definida en forma recursiva. Además se desea transformar la expresión a notación polaca inversa. Esta puede describirse por las siguientes reglas: t1 + t2 --> t1t2+ t1 - t2 --> t1t2- f1 * f2 --> f1f2* f1 / f2 --> f1f2/ (e) --> e [e] --> e Donde : t1 y t2 son términos; f1 y f2 son factores; y e es una expresión. Nótese que la secuencia de salida no contiene paréntesis. Se aprovecha el programa que permite recorrer la sintaxis para generar código; en este caso, se produce una secuencia en notación polaca inversa que podría posteriormente ser evaluada en una pila. En el programa se lee sin eco; se detectan errores de sintaxis, no aceptando caracteres ilegales, de acuerdo a las producciones. La secuencia de salida se almacena temporalmente en un arreglo; luego se la escribe en la misma línea que la secuencia aceptada de entrada. Se toman providencias para mantener alineadas las columnas de resultado, esto a través de una variable que contabiliza los pares de paréntesis. Prof. Leopoldo Silva Bijit. 07-07-2003 252
El programa ilustra el diseño de procedimientos locales. Factor es local a término; y término es local a expresión. En este caso, debe cuidarse la definición de las variables locales; ya que debido al carácter recursivo es necesario disponer de las encarnaciones adecuadas de las variables locales. Se emplea la técnica de leer un carácter por adelantado; y al mismo tiempo se valida que el carácter recién leído pertenezca al conjunto válido de caracteres siguientes. Nótese que el texto del programa refleja las producciones. La generación de la secuencia polaca inversa se logra, no pasando los paréntesis hacia el arreglo de salida; y posponio el paso de los operadores, hasta haber encontrado el siguiente factor o término. Se emplean conjuntos, los que son tratados en el capítulo siguiente. program polaca; const largo=20; var ch : char; salida : array[1..largo] of char; cursor : integer; cpar : integer; procedure leach; read(kbd,ch) {no estándar} procedure error; write(chr(07)); leach(ch) procedure wrt(ch : char); cursor:=cursor+1; salida[cursor]:=ch procedure wrtpol; var i : integer; for i:=1 to largo+10-cursor-cpar do write(' '); Prof. Leopoldo Silva Bijit. 07-07-2003 253
{compensa los parentesis con cpar} for i:=1 to cursor do write(salida[i]) procedure expresion; var sumop : char; procedure termino; var mulop : char; UNIVERSIDAD TECNICA FEDERICO SANTA MARIA procedure factor; {factor} if ch='(' then write(ch); leach(ch); while not (ch in ['a'..'z','(','[']) do error; expresion; while ch <> ')' do error; cpar:=cpar+2 else if ch='[' then write(ch); leach(ch);whilenot(ch in['a'..'z','(','['])do error; expresion; while ch <> ']' do error; cpar:=cpar+2 else while (ch<'a') or (ch>'z') do error; wrt(ch) write(ch); leach(ch); whilenot (ch in ['*','/','-','+',')',']','.']) do error; {factor} {termino} Prof. Leopoldo Silva Bijit. 07-07-2003 254
factor; while (ch='*') or (ch='/') do write(ch); mulop:=ch; leach(ch); while not (ch in ['a'..'z','(','[']) do error; factor; wrt(mulop) {termino} {expresion} termino; while (ch='+') or (ch='-') do write(ch); sumop:=ch; leach(ch); while not (ch in ['a'..'z','(','[']) do error; termino; wrt(sumop) {expresion} {polaca} clrsrc; {no estándar} writeln('entre <expresion>"." Y la paso a polaca inversa.'); write('->'); leach(ch); while not (ch in ['a'..'z','(','[','.']) do error; while ch <> '.' do cursor:=0; cpar:=0; expresion; wrtpol; writeln; write('->'); leach(ch);whilenot(ch in['a'..'z','(','[','.']) do error;. Prof. Leopoldo Silva Bijit. 07-07-2003 255