Título de Grado en Ingeniería Informática Curso 2009/10 Fernando Jiménez Barrionuevo Gracia Sánchez Carpena Mari Carmen Garrido Carrera Departamento de Ingeniería de la Información de las Comunicaciones Universidad de Murcia
Temario 1.1 Definición de recursividad 1.2 Recursividad en C 1.3 Esquemas recursivos 1.4 Tiempo de ejecución de los algoritmos recursivos 1.5 Recursión vs iteración 2
1.1 Definición de Recursividad Se dice que un objeto es recursivo si forma parte de sí mismo o se define en función de sí mismo. Técnica particularmente potente en las definiciones matemáticas. La potencia de la recursión reside evidentemente en la posibilidad de definir un número infinito de objetos mediante un enunciado finito. De igual forma, un número infinito de operaciones de cálculo puede describirse mediante un programa recursivo finito. En general, un programa recursivo P puede expresarse como una composición de instrucciones básicas S i (que no contienen a P) y el propio P. P [S i,p] 3
1.1 Definición de Recursividad Ejemplos: Números naturales: 1 es un número natural El siguiente de un número natural es un número natural Función factorial, n! (para números enteros no negativos): 0! = 1 Si n>0 entonces n! = n (n 1)! 3! = 3 (3-1)! = 3 2! = 3 2 (2-1)! = 3 2 1! = 3 2 1 (1-1)! = 3 2 1 0! = 3 2 1 1 = 6 4
1.1 Definición de Recursividad Un algoritmo es recursivo si se invoca a sí mismo al menos una vez. Recursividad directa: El algoritmo contiene una llamada explícita a sí mismo, es decir, A invoca a A. Recursividad indirecta: El algoritmo se invoca a sí mismo de forma indirecta, es decir, A invoca a B y B invoca a A. Recursividad de cola o extremo final: La llamada recursiva es la última instrucción que ejecuta el algoritmo. 5
1.1 Definición de Recursividad Un algoritmo recursivo está bien construido si: 1. Contiene, al menos, un caso base, en el cual el algoritmo no se llama a sí mismo. 2. Los valores de los parámetros del algoritmo en las sucesivas llamadas recursivas están más cerca del caso base que el valor con el que se llamó inicialmente al algoritmo, garantizando que el caso base se alcanza. 6
1.2 Recursividad en C Multiplicación n m (m 0) Caso Base: (m = 0) n 0 = 0 Caso General: (m > 0) n m = n + n (m - 1) // m>=0 int multiplica(int n, int m) if (m==0) return 0; return n+multiplica(n,m-1); // m>=0 (version comprimida) int multiplica(int n, int m) return(m==0?0:n+multiplica(n,m-1)); 7
1.2 Recursividad en C División entera n / m (n 0 y m>0) Caso Base: (n < m) n / m = 0 Caso General: (n m) n / m = 1 + ( n m ) / m // n>=0, m>0 int divide(int n, int m) if (n<m) return 0; return 1+divide(n-m,m); // n>=0 y m>0 (version comprimida) int divide(int n, int m) return (n<m?0:1+divide(n-m,m)); 8
1.2 Recursividad en C Módulo n % m (n 0 y m > 0) Caso Base: Caso General: (n < m) n % m = n (n m) n % m = ( n m ) % m // n>=0, m>0 int modulo(int n, int m) if (n<m) return n; return modulo(n-m,m); // n>=0 y m>0 (version comprimida) int modulo(int n, int m) return (n<m?n:modulo(n-m,m)); 9
1.2 Recursividad en C Potencia n m (m 0) Caso Base: (m = 0) n 0 = 1 Caso General: (m > 0) n m = n n m - 1 // m>=0 int potencia(int n, int m) if (m==0) return 1; return n*potencia(n,m-1); // m>=0 (version comprimida) int potencia(int n, int m) return(m==0?1:n*potencia(n,m-1)); 10
1.2 Recursividad en C Factorial n! (n 0) Caso Base: (n = 0) 0! = 1 Caso General: (n > 0) n! = n ( n 1 )! // n>=0 int factorial(int n) if (n==0) return 1; return n*factorial(n-1); // n>=0 (version comprimida) int factorial(int n) return(n==0?1:n*factorial(n-1)); 11
1.2 Recursividad en C Fibonacci fib(n) (n 0) Caso Base: Caso General: (n 1) fib(n) = n (n > 1) fib(n) = fib(n-2) + fib(n-1) // n>=0 int fibonacci(int n) if (n<=1) return n; return fibonacci(n-2)+fibonacci(n-1); // n>=0 (version comprimida) int fibonacci(int n) return(((n<=1))?n:fibonacci(n-2)+fibonacci(n-1)); 12
1.2 Recursividad en C Hanoi h(n,a,b,c) (n 1) Caso Base: (n = 1) h(1,a,b,c): a c Caso General: (n > 1) h(n,a,b,c): h(n-1,a,c,b), a c, h(n-1,b,a,c) // n>=1 void hanoi(int n, char a, char b, char c) if (n>1) hanoi(n-1,a,c,b); printf( Mover de %d a %d\n,a,c); if (n>1) hanoi(n-1,b,a,c); 13
1.3 Esquemas recursivos Divide y Vencerás función DV(x) devuelve solución si x es suficientemente pequeño o sencillo entonces (Caso Base): devolver ad hoc (x) en otro caso (Caso General): descomponer x en casos más pequeños x 1,x 2,...,x n para i=1 hasta n hacer y i = DV(x i ) combinar los y i para obtener una solución y de x devolver y 14
1.3 Esquemas recursivos Divide y Vencerás: Búsqueda Binaria Busca un elemento e en un vector v ordenado. Devuelve el índice de una ocurrencia del elemento e en del vector v. Si el vector v no contiene ninguna ocurrencia del elemento e, devuelve -1. // v está ordenado int buscabin(int v[], int ini, int fin, int e) if (ini>fin) return -1; int med = (ini+fin)/2; if (v[med]==e) return med; if (v[med]>e) return buscabin(v,ini,med-1,e); if (v[med]<e) return buscabin(v,med+1,fin,e); // v está ordenado, tam es el numero de elementos de v int buscabin(int v[], int tam, int e) return buscabin(v,0,tam,e); 15
1.3 Esquemas recursivos Divide y Vencerás: Ordenación por Mezcla Ordena un vector v desde el índice ini hasta el índice fin. void mergesort(int v[], int ini, int fin) if ((fin-ini)>0) int med = (ini+fin)/2; mergesort(v,ini,med); mergesort(v,med+1,fin); mezcla(v,ini,fin,med); // tam es el numero de elementos de v void mergesort(int v[], int tam) mergesort(v,0,tam); 16
1.3 Esquemas recursivos Vuelta Atrás: Una solución Devuelve cierto si encuentra una solución a partir de un paso, y falso en caso contrario. Si encuentra una solución la devuelve en el parámetro solucion. funcion VA(entrada: paso, salida: solución) devuelve booleano si solución completa entonces devolver cierto fin si para cada posibilidad de solución si posibilidad aceptable en solución entonces anotar posibilidad en solución si VA(siguiente paso, solución) entonces devolver cierto fin si cancelar posibilidad en solución fin si fin para devolver falso 17
1.3 Esquemas recursivos Vuelta Atrás: Una solución Solución compuesta por n pasos y m posibilidades para cada paso. El parámetro solucion debe pasarse como apuntador porque es de salida. bool VA(int i, solucion * s) if (i==n) return true; for(int j=0;j<m;j++) if (aceptable(s,i,j)) anotar(s,i,j); if (VA(i+1,s)) return true; cancelar(s,i,j); return false; LLamada inicial al método VA: solucion * s = new solucion(); inicia(s); if (VA(0,s)) imprime(s); 18
1.3 Esquemas recursivos Vuelta Atrás: Todas las soluciones Obtiene todas las soluciones posibles a partir de un paso. funcion VA(entrada: paso, salida: solución) si solución completa entonces anotar solución en otro caso para cada posibilidad de solución si posibilidad aceptable en solución entonces anotar posibilidad en solución VA(siguiente paso, solución) cancelar posibilidad en solución fin si fin para fin si 19
1.3 Esquemas recursivos Vuelta Atrás: Todas las soluciones Solución compuesta por n pasos y m posibilidades para cada paso. void VA(int i, solucion * s) if (i==n) imprime(s); else for(int j=0;j<m;j++) if (aceptable(s,i,j)) anotar(s,i,j); VA(i+1,s); cancelar(s,i,j); LLamada inicial al método VA: solucion s = new solucion(); inicia(s); VA(0,s)); 20
1.3 Esquemas recursivos Vuelta Atrás: Ocho Reinas n = 8 (8 pasos, un paso por cada reina) m = 8 (8 posibilidades para cada reina) x[i]: indica la posición en columna de la reina en la fila i-ésima a[i]: true si la columna i-ésima está libre b[i]: true si la diagonal derecha i-ésima está libre c[i]: true si la diagonal izquierda i-ésima está libre struct solucion int x[8]; bool a[8]; bool b[15]; bool c[15]; ; 21
1.3 Esquemas recursivos Vuelta Atrás: Ocho Reinas void inicia(solucion * s) for(int j=0;j<8;j++) s->a[j] = true; for(int j=0;j<15;j++) s->b[j] = true; s->c[j] = true; void imprime(solucion * s) for(int j=0;j<8;j++) printf("%d ",s->x[j]); printf("\n"); bool aceptable(solucion * s,int i,int j) return s->a[j]&&s->b[i+j]&&s->c[i-j+7]; void anotar(solucion * s, int i, int j) s->x[i]=j; s->a[j]=false; s->b[i+j]=false; s->c[i-j+7]=false; void cancelar(solucion * s, int i, int j) s->a[j]=true; s->b[i+j]=true; s->c[i-j+7]=true; 22
1.4 Tiempo de ejecución de los algoritmos recursivos Expansión de recurrencias Utiliza la recurrencia misma para sustituir m<n por cualquier T(m) en la derecha, hasta que todos los términos T(m) para m>1 se hayan reemplazado por fórmulas que impliquen sólo T(1). Como T(1) siempre es constante, se tiene una fórmula para T(n) en función de n y de algunas constantes. A esta fórmula se le denomina forma cerrada para T(n). 23
1.4 Tiempo de ejecución de los algoritmos recursivos Ejemplo: Factorial Ecuación de recurrencia: T(0) = c 1 T(n) = T(n-1) + c 2 si n>0 Expansión de recurrencias: T(n) = T(n-1)+c 2 = [T(n-2)+c 2 ]+c 2 = T(n-2)+2c 2 = [T(n-3)+c 2 ]+2c 2 = T(n-3)+3c 2 Forma cerrada para T(n) (por inducción en i): T(n) = T(n-i) + i c 2 La expansión terminará cuando se alcance T(0) en lado derecho de la forma cerrada, es decir, cuando n-i=0, por tanto i=n. Sustituyendo en la forma cerrada i por n se obtiene: T(n) = T(0) + n c 2 = c 1 + n c 2 Esto demuestra que T(n) es O(n). 24
1.4 Tiempo de ejecución de los algoritmos recursivos Ejemplo: Búsqueda Binaria Ecuación de recurrencia: T(1) = c 1 T(n) = T(n/2) + c 2 si n>1 Expansión de recurrencias: T(n) = T(n/2)+c 2 = [T(n/4)+c 2 ]+c 2 = T(n/4)+2c 2 = [T(n/8)+c 2 ]+2c 2 = T(n/8)+3c 2 Forma cerrada para T(n) (por inducción en i): T(n) = T(n/2 i ) + ic 2 La expansión terminará cuando se alcance T(1) en lado derecho de la forma cerrada, es decir, cuando n/2 i =1, por tanto i=log n. Sustituyendo en la forma cerrada i por n se obtiene: T(n) = T(1) + log n c 2 = c 1 + log n c 2 Esto demuestra que T(n) es O(log n). 25
1.4 Tiempo de ejecución de los algoritmos recursivos Ejemplo: Mergesort Ecuación de recurrencia: T(1) = c 1 T(n) = 2 T(n/2) + n c 2 si n>1 Expansión de recurrencias: T(n) = 2T(n/2)+nc 2 = 2[2T(n/4)+(n/2)c 2 ]+nc 2 = 4T(n/4)+2nc 2 = 8T(n/8)+3nc 2 Forma cerrada para T(n) (por inducción en i): T(n) = 2 i T(n/2 i ) + i n c 2 La expansión terminará cuando se alcance T(1) en lado derecho de la forma cerrada, es decir, cuando n/2 i =1, por tanto i=log n. Sustituyendo en la forma cerrada i por n se obtiene: T(n) = nt(1) + (log n) n c 2 = nc 1 + n log n c 2 Esto demuestra que T(n) es O(n log n). 26
1.5 Recursión vs Iteración La recursión implica múltiples llamadas y el uso de la pila interna para almacenar, en cada llamada recursiva, los parámetros de llamada, variables locales y dirección de retorno, lo que hace que en general sea más ineficiente que el diseño iterativo. Siempre es posible sustituir un algoritmo recursivo por un algoritmo iterativo que utilice una pila. Casos en los que no se debe utilizar la recursión: 1. Recursividad de cola 2. Árbol de recursión lineal (ejemplo, factorial) 3. Árbol de recursión ramificado, pero con nodos repetidos (ejemplo Fibonacci) Conclusión: la recursión se deberá utilizar cuando, además de facilitar el diseño del algoritmo, genere un árbol de recursión ramificado y con nodos no repetidos. 27
1.5 Recursión vs Iteración Multiplicación n m (m 0) Caso Base: (m = 0) n 0 = 0 Caso General: (m > 0) n m = n + n (m - 1) // m>=0 (versión recursiva) int multiplica(int n, int m) if (m==0) return 0; return n+multiplica(n,m-1); // m>=0 (versión iterativa) int multiplica(int n, int m) int res = 0; for(;m>0;m--) res+=n; return res; 28
1.5 Recursión vs Iteración División entera n / m (n 0 y m > 0) Caso Base: (n < m) n / m = 0 Caso General: (n m) n / m = 1 + ( n m ) / m // n>=0, m>0 (versión recursiva) int divide(int n, int m) if (n<m) return 0; return 1+divide(n-m,m); // n>=0, m>0 (versión iterativa) int divide(int n, int m) int res = 0; for(;n>=m;n-=m) res++; return res; 29
1.5 Recursión vs Iteración Módulo n % m (n 0 y m > 0) Caso Base: Caso General: (n < m) n % m = n (n m) n % m = ( n m ) % m // n>=0, m>0 (versión recursiva) int modulo(int n, int m) if (n<m) return n; return modulo(n-m,m); // n>=0, m>0 (versión iterativa) int modulo(int n, int m) for(;n>=m;n-=m); return n; 30
1.5 Recursión vs Iteración Potencia n m (m 0) Caso Base: (m = 0) n 0 = 1 Caso General: (m > 0) n m = n n m - 1 // m>=0 (versión recursiva) int potencia(int n, int m) if (m==0) return 1; return n*potencia(n,m-1); // m>=0 (versión iterativa) int potencia(int n, int m) int res = 1; for(;m>0;m--) res*=n; return res; 31
1.5 Recursión vs Iteración Factorial n! (n 0) Caso Base: (n = 0) 0! = 1 Caso General: (n > 0) n! = n ( n 1 )! // n>=0 (versión recursiva) int factorial(int n) if (n==0) return 1; return n*factorial(n-1); // n>=0 (version iterativa) int factorial(int n) int res = 1; for(;n>0;n--) res*=n; return res; 32
1.5 Recursión vs Iteración Fibonacci fib(n) (n 0) Caso Base: Caso General: (n 1) fib(n) = n (n > 1) fib(n) = fib(n-2) + fib(n-1) // n>=0 (versión recursiva) int fibonacci(int n) if ((n<=1)) return n; return fibonacci(n-2)+fibonacci(n-1); // n>=0 (version iterativa) int fibonacci(int n) if (n<=1) return n; int * aux = (int *)malloc((n+1)*sizeof(int)); aux[0] = 0; aux[1] = 1; for(int i=2; i<=n; i++) aux[i] = aux[i-2] + aux[i-1]; int res = aux[n]; free(aux); return res; 33
1.5 Recursión vs Iteración Búsqueda Binaria // v está ordenado (versión recursiva) int buscabin(int v[], int ini, int fin, int e) if (ini>fin) return -1; int med = (ini+fin)/2; if (v[med]==e) return med; if (v[med]>e) return buscabin(v,ini,med-1,e); if (v[med]<e) return buscabin(v,med+1,fin,e); // v está ordenado (versión iterativa) int buscabin(int v[], int ini, int fin, int e) while(ini<=fin) int med = (ini+fin)/2; if (v[med]==e) return med; if (v[med]>e) fin = med-1; else ini = med+1; return -1; 34
Ejercicios Propuestos Máximo Común Divisor mcd(n,m) (n 0, m 0) Caso Base: (m == 0) mcd(n,m) = n Caso General: (m > 0) mcd(n,m) = mcd(m,n%m) Monedas: Calcular el número mínimo de monedas necesarias para una determinada cantidad. Suponemos que tenemos monedas de 1, 5, 10, 20 y 50 céntimos y que la cantidad es menor de 1 euro. 35