Sie sind auf Seite 1von 5

Introducción a la recursividad La recursividad es una técnica de programación potente que puede ser utilizada en lugar

de la iteración. Ésta permite diseñar algoritmos recursivos que ofrecen soluciones elegantes y simples a problemas
complejos. Los algoritmos recursivos suelen ser estructurados y modulares.

1 Caso base o de fin de la recursión:


Es un caso donde el problema puede resolverse sin
tener que hacer uso de una nueva llamada a sí
mismo. Evita la continuación indefinida de las partes
recursivas.
2 Parte recursiva:
Relaciona el resultado del algoritmo con resultados de
casos más simples. Se hacen nuevas llamadas a la
función, pero están más próximas al caso base.

Reglas de la recursión En el anterior ejemplo podemos observar que: Cada llamada a una función recursiva se hace con
un parámetro de menor valor que el de la anterior llamada. Así, cada vez se está invocando a otro problema idéntico de
menor tamaño. Existe un caso reducido o caso base en el que se actúa de forma diferente, esto es, ya no se emplea la
recursividad. Lo importante es que la forma en la que el tamaño del problema disminuye llegue a un caso reducido. Esto
es necesario, porque de lo contrario se efectuaría siempre la función recursiva.

El primer paso consiste en identificar un algoritmo recursivo para resolver el problema en cuestión. Descomponer el
problema de tal forma que su solución quede definida en función de ella misma pero para un tamaño menor (caso
general o recursivo) así como la tarea a realizar para un caso simple (caso base o reducido). Los casos generales siempre
deben avanzar hacia un caso base. Es decir, la llamada recursiva se convierte en un subproblema más pequeño (hasta
llegar al caso base). Así pues para resolver algún problema a través de un algoritmo recursivo se deben diseñar: casos
base, casos generales y la solución en términos de ellos.

¿Hay una salida no recursiva del algoritmo (caso base)?, ¿El algoritmo cumple con la especificación en este caso? ¿Cada
llamada recursiva se refiere a un caso más pequeño del problema original? Suponiendo que la(s) llamada(s) recursivas
cumplen la especificación (son correctas), ¿cumple la especificación el algoritmo completo?

SRec Herramienta de apoyo al aprendizaje de algoritmos recursivos Creada por el Laboratorio de Tecnologías de la
Información en la Educación de la Universidad Rey Juan Carlos (España)

Tipos de algoritmos recursivos Existen diferentes tipos de recursión que pueden implementarse como son: -Recursión
directa ◦ Lineal ◦ Final ◦ No final ◦ Múltiple ◦ Anidada -Recursión indirecta

Recursión directa. Cuando un algoritmo se llama a si mismo en su definición.


Recursión indirecta. Cuando un algoritmo no tiene una llamada a si mismo, sino que llama a otro algoritmo que le llama
a él. Recursión lineal. Cuando sólo hay una llamada recursiva en el algoritmo.
Recursión final. Es una recursión lineal donde lo último que se ejecuta es la llamada recursiva.
Recursión no final. El resultado obtenido de la llamada recursiva se combina para dar lugar al resultado de la función
que realiza la llamada.
Recursión múltiple. Cuando en la definición aparece más de una llamada recursiva.
Recursión anidada. Cuando alguno de los argumentos de la llamada es a su vez una llamada recursiva.

Eliminación de la recursividad La motivación de este tema es convertir un algoritmo recursivo en otro iterativo, con la
finalidad de conservar la semántica del algoritmo original.

Eliminación de la recursividad (cont.) A grandes rasgos: 1. Se diseña un algoritmo recursivo que resuelve el problema de
una forma clara. 2. Se convierte en un algoritmo iterativo que resuelve el problema de una forma eficiente, aunque
quizá oscura.

Eliminación de la recursividad múltiple redundante Cuando se tiene algún algoritmo que implementa recursividad
múltiple, como por ejemplo el cálculo de los números de Fibonacci. La definición directa es: public static int fib (int n) { if
(n==0 || n==1) return 1; else return fib(n-2) + fib(n-1); }

Eliminación de la recursividad múltiple redundante (cont.) Existen algunas estrategias para eliminar la recursividad
múltiple en un algoritmo, algunas de estas estrategias son: -Memorización (Memoization) -Tabulación (Tabulation)
Complejidad algorítmica

EFICIENCIA Y COMPLEJIDAD Para medir el rendimiento y comportamiento de un algoritmo se considera: ●Simplicidad


●Uso eficiente de los recursos

EFICIENCIA La eficiencia se puede medir en base a dos parámetros: El espacio = memoria que utiliza. El tiempo= lo que
tarda en ejecutarse.

Estos parámetros sirven para determinar el coste de la solución-algoritmo. Permiten comparar algoritmos entre sí, =>
más adecuado

EFICIENCIA TEMPORAL El tiempo de ejecución de un algoritmo depende de: ● Datos de entrada suministrados. ●
Complejidad intrínseca del compilador para crear el programa objeto, la naturaleza y rapidez de las instrucciones
máquina del procesador ● el algoritmo.

TIEMPO Se posee dos estudios del tiempo: ● A Priori: Proporciona una medida teórica, que consiste en obtener una
función que acote (por arriba o por abajo) el tiempo de ejecución del algoritmo para unos valores de entrada dados. ● A
Posteriori: Ofrece una medida real, consistente en medir el tiempo de ejecución del algoritmo para unos valores de
entrada dados y en un ordenador concreto.

Algunas consideraciones ● Tamaño de la entrada: Número de componentes sobre los que se va a ejecutar el algoritmo.
● Unidad de tiempo: No puede ser expresada en una unidad de tiempo concreta. ● Notación: T(n) El tiempo de
ejecución de un algoritmo para una entrada de tamaño n.

TIEMPO DE EJECUCIÓN ● T(n): Indica el número de instrucciones ejecutadas por un ordenador idealizado. ●
Independiente del ordenador a utilizar. (a priori)

En muchos programas el tiempo de ejecución es en realidad una función de la entrada específica, y no sólo del tamaño
de ésta. Casos posibles: ● Coste Peor. ● Coste Mejor. ● Coste promedio.

TIEMPO DE EJECUCIÓN ● T(n) Es una función que mide el número de operaciones elementales que realiza el algoritmo
para un tamaño de entrada dado

OPERACIONES ELEMENTALES OE: Son aquellas que el ordenador realiza en tiempo acotado por una constante. Estas
pueden ser: ● Operaciones aritméticas básicas. ● Asignaciones a variables de tipo predefinido por el compilador. ●
Saltos (llamadas a funciones y procedimientos, retorno desde ellos, etc.) ● Las comparaciones lógicas. ● El acceso a
estructuras indexadas básicas, como son los vectores y matrices.

Observaciones ● Caso mejor: Cuando el elemento está en la primera posición del vector. ● Caso peor: Cuando el
elemento no está en el vector. ● Caso medio: Cuando cada posición del vector tiene la misma probabilidad de poseer el
valor o que no se encuentre.

Coste esperado ● Tmax(n): Representa la complejidad temporal en el peor de los casos. ● Tmin(n): Representa la
complejidad en el mejor de los casos posibles. ● Tmed(n): Expresa la complejidad temporal en el caso promedio. Para su
cálculo se suponen que todas las entradas son equiprobables.

Técnicas de divide y vencerás En el ámbito del diseño de algoritmos, es una técnica que se aPasos para aplicar la técnica
La resolución de un problema mediante esta técnica en esencia consta de los siguientes pasos: 1. Plantear el problema
de forma que pueda ser descompuesto en k subproblemas del mismo tipo pero de menor tamaño. 2. Resolver de forma
independiente los subproblemas, bien directamente si son elementales o bien de forma recursiva. Como ya sabes esto
nos conduce hacia el caso base (condición de parada). 3. Combinar las soluciones obtenidas en el paso anterior para
construir la solución del problema original.

plica para resolver un problema a partir de la solución de subproblemas del mismo tipo, pero de menor tamaño. Si los
subproblemas son todavía relativamente grandes se aplicará de nuevo esta técnica hasta alcanzar subproblemas lo
suficientemente pequeños para ser solucionados directamente. Para la resolución de estos subproblemas se suele
emplear la recursión.
Aplicaciones Algunos ejemplos del uso de la técnica de divide y vencerás son los algoritmos de ordenación por mezcla y
quicksort, los algoritmos de búsqueda binaria y sus variantes, así como algoritmos para calcular el producto de matrices
cuadradas, entre otros

Notaciones asintóticas¿Qué son? Las notaciones asintóticas son lenguajes que nos permitan analizar el tiempo de
ejecución de un algoritmo identificando su comportamiento si el tamaño de entrada para el algoritmo aumenta. Esto
también se conoce como la tasa de crecimiento de un algoritmo. ¿El algoritmo de repente se vuelve increíblemente
lento cuando el tamaño de entrada crece? ¿Tiende a mantener un rápido tiempo de ejecución a medida que el tamaño
de entrada aumenta? La notación asintótica nos da la capacidad para responder a estas preguntas.
¿Hay alternativas que respondan a estas preguntas?
Una manera sería contar el número de operaciones primitivas en diferentes tamaños de entrada. Aunque esta es una
solución válida, la cantidad de trabajo que esto conlleva, incluso para los algoritmos simples, no justifica su uso.
Otra manera es medir físicamente la cantidad de tiempo que un algoritmo toma para completar su ejecución dados
diferentes tamaños de entrada. Sin embargo, la exactitud y la relatividad (los tiempos obtenidos sólo serían relativos a
la máquina sobre la cual se calcularon) de este método está ligado a variables ambientales tales como especificaciones
de hardware, capacidad de procesamiento, etc.
Tipos de Notación Asintótica
En la primera sección de este documento hemos descrito cómo una notación asintótica identifica el comportamiento de
un algoritmo ante los cambios en el tamaño de la entrada. Imaginemos un algoritmo como una función f, con tamaño
de entrada n, y f(n) siendo el tiempo de ejecución. Así que para un algoritmo f dado, con el tamaño de entrada n
obtenemos algún tiempo de ejecución resultante f(n). Esto resulta en un gráfico donde el eje Y es el tiempo de
ejecución, el eje X es el tamaño de la entrada y los puntos en el gráfico son los resultantes de la cantidad de tiempo para
un tamaño de entrada dado.
Puedes etiquetar una función, o un algoritmo, con una notación asintótica de muchas maneras diferentes. Algunos
ejemplos son describir un algoritmo por su mejor caso, su peor caso, o el caso promedio. Lo más común es analizar un
algoritmo por su peor caso. Por lo general, no se evalúa el mejor caso, porque no planeas el algoritmo para estas
condiciones. Un muy buen ejemplo de esto son los algoritmos de ordenamiento; específicamente, añadir elementos a
un árbol. El mejor caso para la mayoría de los algoritmos podría ser tan bajo como una sola operación. Sin embargo, en
la mayoría de los casos, el elemento que está añadiendo tendrá que ser ordenado adecuadamente a través del árbol, lo
que podría significar examinar toda una rama. Este es el peor de los casos, y para estos casos es que planeamos el
algoritmo.
Estas son algunas clasificaciones de funciones de crecimiento básicos utilizados en varias notaciones. La lista comienza
en la función de crecimiento menor (logarítmica, el tiempo de ejecución mas rápido) y pasa a la de mayor crecimiento
(exponencial, el tiempo de ejecución mas lento). Observe como al crecer ‘n’, o la entrada, en cada una de estas
funciones, el resultado aumenta claramente mucho más rápido en las cuadráticas, polinómicas y exponenciales, en
comparación con las logarítmicas y lineales.
Una anotación muy importante es que en las notaciones que se discutirán debes hacer tu mejor esfuerzo por utilizar los
términos más simples. Esto significa hacer caso omiso de las constantes y terminos de orden inferior, porque a medida
que el tamaño de entrada (o n en f(n)) aumenta hacia el infinito (límites matemáticos), los términos y constantes de
orden inferior se vuelven de poca o ninguna importancia. Dicho esto, si tienes constantes que son 2^9001, o alguna otra
cantidad ridícula, inimaginable, te daras cuenta de que la simplificación sesgará la exactitud de la notación.
Como queremos algo simplificado, vamos a modificarlo un poco…
O-grande (Big-O)
O-grande (Big-O), comúnmente escrito como O, es una notación asintótica para el peor caso, o el techo de crecimiento
para una función determinada. Si f (n) es el tiempo de ejecución del algoritmo, y g (n) es un tiempo de complejidad
arbitraria que relacionas con el algoritmo, entonces f (n) es O(g(n)), si por cualquier constante real c (c > 0), f (n) <= c
g(n) para cada tamaño de entrada n (n > 0 ).
Big-Omega
Big-Omega, comunmente escrito como Ω, es una notación asintótica para el mejor caso, o el piso en el crecimiento para
una función dada.
f(n) es Ω(g(n)), si para cualquier constante real c (c > 0), f(n) es >= c g(n) para cualquier tamaño de entrada n (n > 0).
No dudes en dirigirte a los recursos adicionales para ejemplos sobre esto. O-grande es la notación principal utilizada
para la complejidad general de tiempo algoritmico.
Notas finales
Es difícil mantener este tipo de tema corto, y sin duda deberias revisar los libros y recursos en línea en la lista. Entran en
mucha mayor profundidad con definiciones y ejemplos.
Recursión simple
int Fib( int n ) /* ej: Fibonacci */
{
if(n<=1) return(1);
return(Fib(n-1) + Fib(n-2));
}

Recursividad anidada: En algunos de los arg. de la llamada recursiva hay una nueva llamada a sí misma.

int Ack( int n, int m ) /* ej: Ackerman */


{
if(n==0 ) return(m+1);
else if(m==0) return(Ack(n-1,1));
return(Ack(n-1, Ack(n,m-1)));
}

Recursividad cruzada o indirecta: Son algoritmos donde una función provoca una llamada a sí misma de forma
indirecta, a través de otras funciones.

Ej: Par o Impar:


int par( int nump )
{
if(nump==0) return(1);
return( impar(nump-1));
}

int impar( int numi )


{
if(numi==0) return(0);
return( par(numi-1));
}

Ejercicio de Factorial
int factorial( int n)
{
if (n<2)
{
return 1;
}
else
{
return n*factorial(n-1);
}
}

No final

*ejemplo7_1.c
*/
#include <stdio.h>
int factorial(int numero){
if (numero > 1) return (numero*factorial(numero-1));
else return(1);
}
int main (){
int n;
printf("Introduce el número: ");
scanf("%d",&n);
printf("El factorial es %d", factorial(n));
}

int potencia(int ,int); int suma(int, int);


int main() int main()
{ {
int base,exp; int n1,n2;
cout<< "Ingresa la base : "<<endl; cout << "Introduzca primer numero: ";
cin>>base; cin >> n1;
cout<<"Ingrese el exponente: "<<endl; cout << "Introduzca segundo numero: ";
cin>>exp; cin >> n2;
cout<<"La potencia de "<<base<<" elevado a "<<exp<<" cout << "suma: " << suma(n1,n2) << endl;
es: getch();
"<<potencia(base,exp)<<endl; }
getch(); int suma(int a, int b)
return 0; {
} int resul;
int potencia(int n,int m) if(b==0) // caso base
{ return a;
int pot; else if(a==0)
if (m==1) // caso base return b;
{ else // caso general
pot= n; {
} return 1+suma(a,b-1);
else }
{ // caso general }
pot=n * potencia(n,m -1);
}
}
int producto(int, int);
int main()
{
int num1,num2,pot;
cout << "Introduzca el primer numero: ";
cin >> num1;
cout << "Introduzca el segundo numero: ";
cin >> num2;
p=producto(num1,num2);
cout << "producto: " << pot << endl;
system("pause");
}
int producto(int x, int y)
{ //caso base
if(x==0 or y==0)
return 0;
else
{//caso recursivo
return x+producto(x,y-1);
}
}

Das könnte Ihnen auch gefallen