Académique Documents
Professionnel Documents
Culture Documents
Una lista se comporta como una cola si las inserciones las hacemos al final y las
extracciones las hacemos por el frente de la lista. También se las llama listas FIFO
(First In First Out - primero en entrar primero en salir)
Confeccionaremos un programa que permita administrar una lista tipo cola.
Desarrollaremos las funciones de insertar, extraer, vacia, imprimir y liberar.
Programa: programa184.c
#include<stdio.h>
#include<stdlib.h>
struct nodo {
int info;
struct nodo *sig;
};
void insertar(int x)
{
struct nodo *nuevo;
nuevo=malloc(sizeof(struct nodo));
nuevo->info=x;
nuevo->sig=NULL;
if (vacia())
{
raiz = nuevo;
fondo = nuevo;
}
else
{
fondo->sig = nuevo;
fondo = nuevo;
}
}
int extraer()
{
if (!vacia())
{
int informacion = raiz->info;
struct nodo *bor = raiz;
if (raiz == fondo)
{
raiz = NULL;
fondo = NULL;
}
else
{
raiz = raiz->sig;
}
free(bor);
return informacion;
}
else
return -1;
}
void imprimir()
{
struct nodo *reco = raiz;
printf("Listado de todos los elementos de la
cola.\n");
while (reco != NULL)
{
printf("%i - ", reco->info);
reco = reco->sig;
}
printf("\n");
}
void liberar()
{
struct nodo *reco = raiz;
struct nodo *bor;
while (reco != NULL)
{
bor = reco;
reco = reco->sig;
free(bor);
}
}
void main()
{
insertar(5);
insertar(10);
insertar(50);
imprimir();
printf("Extraemos uno de la cola: %i\n",
extraer());
imprimir();
liberar();
getch();
return 0;
}
La declaración del nodo es igual a la Pila. Luego definimos dos punteros externos
llamados raiz y fondo:
struct nodo {
int info;
struct nodo *sig;
};
Debemos enlazar el puntero sig del último nodo con el nodo recién creado:
fondo->sig=nuevo;
Y por último el puntero externo fondo debe apuntar al nodo apuntado por nuevo:
fondo=nuevo;
Con esto ya tenemos correctamente enlazados los nodos en la lista tipo cola.
Recordar que el puntero nuevo desaparece cuando se sale del método insertar,
pero el nodo creado no se pierde porque queda enlazado en la lista.
El funcionamiento del método extraer es similar al de la pila:
int extraer()
{
if (!vacia())
{
int informacion = raiz->info;
struct nodo *bor = raiz;
if (raiz == fondo)
{
raiz = NULL;
fondo = NULL;
}
else
{
raiz = raiz->sig;
}
free(bor);
return informacion;
}
else
return -1;
}
Si la lista no está vacía guardamos en una variable local la información del primer
nodo (el operador lógico ! invierte el valor devuelto por la función vacía, si retorna
un 1 luego el operador ! lo convierte a cero y viceversa y retorna un 1):
int informacion = raiz->info;
Definimos un puntero y lo inicializamos con el primero de la lista:
struct nodo *bor = raiz;
Para saber si hay un solo nodo verificamos si los dos punteros raiz y fondo
apuntan a la misma dirección de memoria:
if (raiz == fondo)
{
Luego hacemos:
raiz = NULL;
fondo = NULL;
raiz = raiz->sig;
Retornar
COLAS
1. INTRODUCCIÓN.
Una Cola es otro tipo especial de lista en el cual los elementos se insertan por un
extremo (el posterior) y se suprimen por el otro (el anterior o frente). Las colas se
conocen tambien como listas FIFO (primero en entrar,primero en salir). Las
operaciones para las colas son análogas a las de las pilas. Las diferencias
sustanciales consisten en que las inserciones se hacen al final de la lista, y no al
principio, y en que la terminología tradicional para colas y listas no es la misma.
Las primitivas que vamos a considerar para las colas son las siguientes.
CREAR()
DESTRUIR(C)
FRENTE(C)
PONER_EN_COLA(x,C)
QUITAR_DE_COLA(C)
VACIA(C)
ESPECIFICACIÓN SEMANTICA Y SINTACTICA.
cola crear ()
Argumentos: Ninguno.
Efecto: Devuelve una cola vacia preparada para ser usada.
void destruir(cola C)
Argumentos: Una cola C.
Efecto: Destruye el objeto C liberando los recursos que mantiene que
empleaba.Para volver a usarlo habrá que crearlo de nuevo.
typedef struct {
celda *ant,*post;
} tcola;
FUNCIÓN DE ABSTRACCIÓN.
Dado el objeto del tipo rep c, *c = (ant, post), el objeto abstracto que representa
es:
INVARIANTE DE LA REPRESENTACIÓN.
C = (tcola *) malloc(sizeof(tcola));
if (C == NULL)
error("Memoria insuficiente.");
C->ant = C->post = (celda *)malloc(sizeof(celda));
if (C->ant == NULL)
error("Memoria insuficiente.");
C->ant->siguiente = NULL;
return C;
}
if (VACIA(C))
error("Cola vacia.");
aux = C->ant;
C->ant = C->ant->siguiente;
free(aux);
}
C=CREAR(C);
PONER_EN_COLA(x,C);PONER_EN_COLA(y,C);
QUITAR_DE_COLA(C);
DESTRUIR(C);
Se puede observar que en el primer caso, la memoria que se obtiene del sistema
es la de la estructura de tipo celda que hace de cabecera y la memoria para ubicar
los dos punteros anterior y posterior. En los dos últimos casos, la línea punteada
indica la memoria que es liberada.
Ahora escribimos las primitivas de las colas usando esta representación para una
cola:
typedef struct {
tElemento *elementos;
int Lmax;
int ant,post;
} tipocola;
C = (cola) malloc(sizeof(tipocola));
if (C == NULL)
error("No hay memoria.");
C->Lmax = tamanoMax+1;
C->ant = 0;
C->post = C->Lmax-1;
C->elementos = (tElemento *) calloc((tamanoMax+1), sizeof(tElemento));
if (C->elementos == NULL)
error("No hay memoria.");
return C;
}
void DESTRUIR (cola *C)
{
free(*C->elementos);
free(*C);
*C == NULL;
}
En esta implementación podemos observar que se reserva una posicón más que la
especificada en el parametro de la función CREAR. La razón de hacerlo es que
no se podrán ocupar todos los elementos de la matriz ya que debemos distinguir
la cola llena de la cola vacía. Estas dos situaciones por lo tanto vienen
representadas tal y como se muestra en la figura siguiente.
Se puede observar en el caso de la cola llena en la figura como la posición
siguiente a post no es usada y por lo tanto es necesario crear una matriz de un
tamaño N+1 para tener una capacidad para almacenar N elementos en cola.
Pilas y colas
Pilas
Una pila (stack) es un objeto similar a una pila de platos, donde se puede agregar
y sacar datos sólo por el extremo superior. En computación esta estructura es
utilizada ampliamente, aunque muchas veces los usuarios ni siquiera se percaten.
Están tan arraigadas como concepto en computación, que una de las pocas cosas
que los procesadores (CPU) saben hacer, aparte de operaciones aritméticas, es usar
pilas.
La Pila se utiliza con las siguientes funciones:
Las pilas son estructuras LIFO (last in, first out), el último que entra es el primero
en salir.
Barbarismos: una pila NO ES la batería que usa el reloj del computador, como
alguna vez dijo un alumno de pocas luces. Tampoco es cierto lo que una vez salió
en un Condorito: la invención del auto a pila (a pila de "jetones" que lo empuja)
Propuesto: Implemente una pila y sus funciones usando arreglos y otra usando
listas enlazadas.
Colas
Cuando uno quiere ir a comer un sandwich a la cafetería se pone en una fila. Esta
estructura es una estructura FIFO (first in, first out), el primero en entrar es el
primero en salir. En computación se llama "colas" a las filas (en la práctica
también, quien no ha escuchado "¡a la cola patudo!").
La Cola se utiliza, en forma análoga a una Pila, con las siguientes funciones:
Propuesto: Implemente una cola y sus funciones usando arreglos y otra usando
listas enlazadas.
Algunos ejemplos
1. Pila p;
2. int numero;
3. printf ("ingresa números positivos, termina con –1\n");
4. scanf(“%d”,&numero);
5. while (numero!=-1)
6. {
7. poner(p,numero);
8. scanf(“%d”,&numero);
9. }
10. while (!vacia(p)) print (“%d ”,sacar(p));
Línea(s) Ejecución
Este método recibe una Pila (suponemos que se guardan enteros en ella) y la
devuelve invertida, usando una cola auxiliar.
1. void invertir(Pila p)
2. {
3. Cola c;
4. while (!vacia(p))
5. {
6. int n = sacar(p);
7. encolar(c,n);
8. }
9. while (!vacia(c))
10. poner(p,decolar(c));
11. }
Línea(s) Ejecución
Análogo al ejemplo anterior, pero usando dos pilas en vez de una cola.
1. void invertir(Pila p)
2. {
3. Pila aux1, aux2;
4. while (!vacia(p))
5. poner(aux1,sacar(p));
6. while (!vacia(aux1))
7. poner(aux2,sacar(aux1));
8. while (!vacia(aux2))
9. poner(p,sacar(aux2));
10. }
Línea(s) Ejecución
1. while (!vacia(establo1))
2. {
3. Hipo h=sacar(establo1);
4. poner(establo2,h);
5. while (!vacia(establo1))
6. {
7. Hipo h2=sacar(establo1);
8. h=sacar(establo2);
9. if (peso(h) < peso(h2))
10. {
11. poner(establo2,h);
12. poner(establo3,h2);
13. }
14. else
15. {
16. poner(establo3,h);
17. poner(establo2,h2);
18. }
19. }
20. while (!vacia(establo3))
21. poner(establo1,sacar(establo3));
22. }
Línea(s) Ejecución
Para ir a ver la película "Titanic II, la ira de Rose" hay dos filas de personas.
Un acomodador es el encargado de dejar entrar a la gente. Sus instrucciones
son simples: "deja entrar siempre a la persona que es mayor, si tienen la
misma edad, a gente de la fila 1 tiene preferencia". El siguiente código
muestra el comportamiento del acomodador. Suponemos que las colas
guardan referencias a Personas:
Línea(s) Ejecución
NUNCA saques elementos de una pila o una cola sin verificar antes que
tengan elementos.
Problemas propuestos
Problema 1: Escriba una función que utilizando una o más pilas, entregue el largo
(cantidad de elementos) de una cola. La función debe dejar la cola en el estado
original.
Problema 2: Una cola puede representarse utilizando dos pilas. Escriba las
funciones encolar, decolar y vacia utilizando las funciones de Pilas si el tipo de
dato Cola está definido como:
typedef struct c
Pila p1;
Pila p2;
} Cola;
Pilas y Colas C++
Publicado el junio 19, 2012 por Victor Hugo Choc Cac
/*
Programación
Choc Cac, Victor Hugo.
*/
#include <cstdlib>
#include <iostream>
//=======================================================
==
//==============Declaracion de la cola=====================
struct SCola{
int elemento;
struct SCola *siguiente;
};
typedef struct{
TCola *Adelante;
TCola *ElFin;
}Cola;
int Lleno(pila*);
int VacioPila(pila*);
void push(pila*, int);
void pop(pila*, int*);
//=======================================================
==
//=========declaraciones de cola===========================
Cola cola;
CrearCola(&cola);
int Cx;
//=========fin de cola=====================================
//=======================================================
==
/*
Una vez que se inserto
en Cola imprimimos
e eleminamos la cola.
*/
cout << “\n\n\t\tPrimeros en Entrar Primeros en Salir”;
cout << “\n\t\t\t\t===COLA===\n\n”;
for(int xy=0;xy<VALORMAXIMO;xy++)
{
cout << “Imprimiendo y elimando Cola: ” << RemoverCola(&cola) << “\n”;
}
//=============fin de impresion de cola
system(“pause”);
return 0;
}
//=======================================================
==
//==========Inicio funciones pila==========================
void push(pila *p,int dato)
{
if(!Lleno(p))
{
(p->tope)++;
p->item[p->tope]=dato; //elemento[1]=dato
}
else
{
cout << “\nDesbordamiento de bufer”;
}
}
void pop(pila *p,int *dato)
{
if(!VacioPila(p))
{
*dato=p->item[p->tope];
(p->tope)–;
}
else
{
cout << “\nDesbordamiento de bufer”;
}
}
//=======================================================
==
//==========Inicio funciones de cola=======================
void CrearCola(Cola *cola)
{
cola->Adelante=cola->ElFin=NULL;
}
.------------------------
alguien me puede ayudar con la funcion buscar?
se los agradeceria muchoo!
#include"stdio.h"
#include"iostream.h"
#include"conio.h"
#define localizar (struct nodo*)malloc(sizeof(struct nodo))
struct nodo
{
int info;
struct nodo *sig;
struct nodo *ant;
};
struct nodo *cab, *p,*aux;
void iniciar()
{
cab=new nodo;
cab->sig=cab;
}
void sumar()
{
struct nodo*nuevo;
nuevo = new nodo;
cout<<"digite un numero: ";
cin>>nuevo->info;
nuevo->sig=cab->sig;
cab->sig=nuevo;
cab=nuevo;
}
nodo *p ;
nodo *aux ;
if(cab!=NULL){
aux=cab->info
if(cab==NULL){
cab=NULL;
}
else{
cab=cab->sig;
return aux;
}
else
free(p);
}
}
void imprimir()
{
struct nodo*r;
if(cab==cab->sig)
{
cout<<"sin elemetos";
getch();
}
else
{
r=cab->sig;
do
{
r=r->sig;
cout<<r->info<<" ";
}while(r!=cab);
}
getch();
}
else
{
aux=cab->sig;
while(aux!=cab)
{z++;
aux=aux->sig;
}
cout<<"\nEl numero de elementos encontrados es : "<<z<<"\n";
}
getch();
}
if(cab==NULL)
{
cout<<"sin elemetos";
getch();
}
else
{
r=cab->sig;
do
{
r=r->sig;
cout<<r->info<<" numero "<<i<<endl;
i++;
}while(r!=cab);
}
getch();
}
int main()
{
int dato;
iniciar();
do
{
system("cls");
cout<<"1. adicionar \n";
cout<<"2. quitar \n";
cout<<"3. imprimir \n";
cout<<"4. contar nodos \n";
cout<<"5. buscar \n";
cout<<"6. salir \n";
cout<<"Digite dato \n";
cin>>dato;
switch(dato)
{
case 1:{ sumar();
break;
}
case 2:{retirar(cab);
break;
}
case 3:{imprimir();
break;
}
case 4:{
contar(cab);
getch();
break;
}
case 5:{
buscar(cab);
break;
}
}
} while(dato!=6);
return 0;
}
uando tenemos una llamada a una función que es lo último que hacemos en otra
función, decimos que esta llamada es una tail-call y entonces en este caso, se puede
aplicar la tail call optimization y ahorrarnos un frame en la pila de llamadas.
if (n == 0)
return 1 ;
else
return n * fact(n-1) ;
Esta función recursiva es básicamente la misma que la del artículo anterior, dedicado
a la recursividad. Es un ejemplo de recursividad clásica.
function fact(n) {
return tail_fact(n,1) ;
function tail_fact(n,a) {
if (n == 0)
return a ;
else
return tail_fact(n-1,n*a) ;
Por otro lado tail_fact lo último que hace antes de devolver es... llamar
a tail_fact. No multiplica el valor de tail_fact por una variable local (como
si hace fact), por lo tanto el estadio intermedio de todos los saltos recursivos no lo
necesitamos para nada.
Por supuesto para usar TCO es necesario que nuestro código use tail recursion pero
también que el compilador o motor del lenguaje lo soporte. P. ej. Scheme obliga a
que todas las implementaciones soporten TCO y lo mismo ocurre en… ¡ECMAScript
2015!
--
Procedimientos y estructuras
recursivas
Table of Contents
Bibliografía
Diseño de funciones recursivas
o Recursión
o Confía en la recursión
longitud
sumatorio
elemento?
nth
o Recursión mutua
Procesos recursivos e iterativos
o El coste espacial de la recursión
longitud
fibonacci
o Procesos recursivos e iterativos
o Procesos iterativos
o Fibonacci iterativo
o Memoization
o Triángulo de pascal
Estructuras de datos recursivas
o Expresiones-s
Definición
Estructura jerárquica
Funciones sobre expresiones-s como estructuras jerárquicas
o Árboles binarios
Barrera de abstracción
Barrera de abstracción de árbol binario
Implementación de los árboles binarios
Funciones de mayor nivel
to-list-bt
(member-bt? x bt)
(insert-bt x bt)
(insert-list-bt lista bt)
(list-to-bt lista)
o Árboles genéricos
Barrera de abstracción e implementación de árboles
genéricos
Funciones sobre árboles genéricos
(tree-to-list tree)
(cuadrado-tree tree)
map-tree
Bibliografía
El tema está basado en los siguientes materiales. Os recomendamos que los estudieis y,
si os interesa y os queda tiempo, que exploréis también en los enlaces que hemos dejado
en los apuntes para ampliar información.
Abelson & Sussman
o Capítulo 1.2 Procedures and the Processes They Generate (apartados
1.2.1 al 1.2.5)
o Capítulo 2.2: Hierarchical Data
Capítulo 6.6 Scott: Recursion
(dfeine (factorial x)
(if (= x 0)
1
(x * (factorial (- x 1)))))
Confía en la recursión
Ya sabemos cómo es un algoritmo recursivo. Hemos visto varios ejemplos utilizando
números, identificadores o listas. Pero… ¿cómo diseñar un algoritmo recursivo? Para
resolver un problema de forma recursiva debes:
longitud
Queremos diseñar la función (longitud lista) que devuelva la longitud de una lista.
Falta el caso base. Normalmente es la parte más sencilla. En este caso es trivial:
la longitud de la lista vacía es 0.
sumatorio
Queremos implementar la función (sumatorio n m) que devuelve la suma de los
números desde n hasta m, ambos incluidos.
Igual que en el ejemplo anterior, comenzamos con un caso sencillo y vemos cómo se
puede formular recursivamente.
Podemos considerar como caso base el caso en el que n/=/m y devolver n, o el caso en el
que n/>/m y devolver 0.
Versión 1:
(define (sumatorio n m)
(if (= n m)
0
(+ n (sumatorio (+ n 1) m))))
Versión 2:
(define (sumatorio n m)
(if (= n m)
0
(+ m (sumatorio n (- m 1)))))
elemento?
nth
Vamos a ver cómo encontrar el elemento n-ésimo de una lista de forma recursiva. Este va
a ser un ejemplo algo distinto a los anteriores, ya que aquí no tendremos que construir
ninguna solución a partir de la llamada recursiva, sino que debermos buscar esta solución
haciendo cada vez más sencillo el problema.
Vamos a llamar a la función (nth n lista), donde n es la posición del elemento que
queremos devolver (comenzando por 1) y lista es la lista que recorremos.
Podemos empezar por preguntarnos qué sabemos hacer con las listas:
¿Cómo podríamos usar la recursión para devolver un elemento que no está en primer
lugar? Esto es, ¿qué hacer cuando n es distinto de 1? Habrá que simplificar el problema y
pasarle a la llamada recursiva la lista sin el primer elemento y habiendo restado 1 a n:
¿Qué pasaría si se llama a la función con el número n siendo mayor que el número de
elementos de la lista? Tendríamos un error, ya que la se intentaría obtener el primer
elemento de una lista vacía. Para solucionarlo, basta con contemplar este caso como otra
posible condición de terminación de la recursión:
(define (nth n lista)
(cond
((null? lista) lista)
((= n 1) (car lista))
(else (nth (- n 1) (cdr lista)))))
Recursión mutua
A veces es útil usar un patrón de recursión mutua, en el que dos funciones se llaman una
a la otra. El patrón recuerda la famosa ilustración Drawing Hands de M.C. Escher:
(define (par? x)
(if (= 0 x)
#t
(impar? (- x 1))))
(define (impar? x)
(if (= 0 x)
#f
(par? (- x 1))))
Un ejemplo algo más complejo es la curva de Hilbert. La curva de Hilbert es una curva
fractal que tiene la propiedad de rellenar completamente el espacio. Su dibujo tiene una
formulación recursiva.
La función (h-der i w) dibuja una curva de Hilbert de orden i con una longitud
de trazo wa la derecha de la tortuga.
La función (h-izq i w) dibuja una curva de Hilbert de orden i con una longitud
de trazo wa la izquierda de la tortuga.
Algoritmo en Scheme:
(require graphics/turtles)
(turtles #t)
(define (h-der i w)
(if (> i 0)
(begin
(turn -90)
(h-izq (- i 1) w)
(draw w)
(turn 90)
(h-der (- i 1) w)
(draw w)
(h-der (- i 1) w)
(turn 90)
(draw w)
(h-izq (- i 1) w)
(turn -90))))
(define (h-izq i w)
(if (> i 0)
(begin
(turn 90)
(h-der (- i 1) w)
(draw w)
(turn -90)
(h-izq (- i 1) w)
(draw w)
(h-izq (- i 1) w)
(turn -90)
(draw w)
(h-der (- i 1) w)
(turn 90))))
(clear)
(h-izq 3 20)
(h-izq 6 5)
Veremos en este apartado que en estos casos es posible modificar la recursión y construir
una solución que crea un proceso iterativo utilizando lo que se denomina recursión por la
cola.
longitud
Cada llamada a la recursión deja en espera una llamada a la función + con los
parámetros 1 y el propio resultado de la recursión. Como en cualquier llamada a una
función, este estado local se almacena en una /pila de llamada/ de la que será recuperado
cuando la llamada termine. En el caso de la recursión la pila va creciendo hasta que la
recursión termina. Si por un error la recursión no termina y se entra en un bucle infinito en
algún momento se produce un stack overflow, un desbordamiento de la memoria
reservada por el sistema operativo para el programa que está en funcionamiento.
Estas llamadas en espera son las que determinan el coste espacial de la recursión. En
este caso el coste espacial depende linealmente de la longitud de la palabra. Se trata de
un coste O(n), siendo n la longitud de la palabra.
fibonacci
Veámoslo con una recursión que implementan de forma elegante, pero poco eficiente, la
serie de Fibonacci.
(define (fib n)
(cond ((= n 0) 0)
((= n 1) 1)
(else (+ (fib (- n 1))
(fib (- n 2))))))
La diferencia con las funciones recursivas vistas hasta ahora es que en el cuerpo de la
función se realizan dos llamadas recursivas. Esto genera un proceso recursivo en forma
de árbol, como se comprueba en la siguiente figura, extraída del Abelson & Sussman:
Cada llamada a la recursión produce otras dos llamadas, con lo que el número de
llamadas finales es 2n, siendo n el número que se pasa a la función. El coste espacial y
temporal es por tanto de O(2n). Podemos comprobar con el intérprete que es imposible
evaluar la función para números a partir de 30.
¿Es posible diseñar la función recursiva de otra forma, para evitar este coste? Vamos a
verlo.
Hemos visto en el apartado anterior que el proceso generado por una función recursiva
tiene a veces un alto coste espacial y temporal debido a las llamadas que permenacen en
espera en la pila de la recursión.
Es posible escribir programas recursivos que generen procesos que no dejan llamadas en
espera. A estos procesos los llamamos procesos iterativos, en contraposición con
los procesos recursivosque generan los programas vistos hasta ahora.
La siguiente versión de la función factorial está escrita de forma que el proceso generado
es iterativo y no deja llamadas en espera.
(define (factorial-iter n)
(fact-iter-aux n n))
(factorial-iter 4)
(factorial-iter-aux 4 4)
(factorial-iter-aux 12 3)
(factorial-iter-aux 24 2)
(factorial-iter-aux 24 1)
24
Veamos otro ejemplo más, la versión iterativa de la función longitud que calcula la
longitud de una lista.
La función longitud se redefine para que llame a longitud-iter con los valores
iniciales correctos.
Como podemos comprobar, tampoco se deja ninguna llamada en espera en la pila de
llamada. Cuando alcanzamos el caso base de la recursión tenemos la respuesta al
problema completo.
(longitud 'abcdef)
(longitud-iter 'abcdef 0)
(longitud-iter 'bcdef 1)
(longitud-iter 'cdef 2)
(longitud-iter 'def 3)
(longitud-iter 'ef 4)
(longitud-iter 'f 5)
(longitud-iter "" 6)
6
Procesos iterativos
Las dos funciones anteriores son ejemplos de funciones que generan procesos iterativos.
En general, las versiones iterativas son menos elegantes y más difíciles de entender que
las recursivas, pero son más eficientes.
En otros lenguajes de programación existen bucles (for, while) para definir procesos
iterativos. En un lenguaje funcional puro no existen bucles; el proceso iterativo se
implementa como hemos visto. Se utiliza uno o más parámetros auxiliares y se modifica
su valor en cada llamada a la recursión.
Esta recursión que no deja llamadas en espera también recibe el nombre de tail
recursión o recursión por la cola. El código del intérprete que evalua la recursión por la
cola puede tratar estas llamadas de una forma especial, sabiendo que no hay que
devolver el resultado para que vuelva a ser procesado. Se devuelve el último valor a la
llamada original y se puede eliminar la pila de la llamada.
Fibonacci iterativo
La versión recursiva de la función que devuelve el número de Fibonacci tenía un coste
exponencial, debido a las dos llamadas recursivas.
(define (fib n)
(cond ((= n 0) 0)
((= n 1) 1)
(else (+ (fib (- n 1))
(fib (- n 2))))))
(define (fib n)
(fib-iter 1 0 n))
Esta recursión tiene un coste lineal O(n) y permite calcular sin problemas valores de
fibonacci imposibles de obtener con la recursión anterior.
Memoization
La memoization es una técnica de optimización que permite mantener la elegancia de los
procesos recursivos sin incurrir en el coste de las llamadas repetidas a la recursión.
Por ejemplo, para calcular (fib 200) hay que llamar a (fib 199) y (fib 198). A su
vez, para calcular (fib 199) se llama a (fib 198) y (fib 197). ¿Por qué llamar dos
veces a (fib 198)? Estamos en programación funcional y el resultado de la llamada
siempre va a ser el mismo. ¿No podríamos guardar el valor devuelto por (fib 198) la
primera vez y utilizarlo la segunda vez? De esta forma no volveríamos a lanzar la
recursión para este valor (ni para ningún otro).
Esto es lo que hace la memoization, en una llamada recursiva guarda el valor devuelto
por cada una de las llamadas en alguna estructura de datos adecuada (una tabla hash por
ejemplo) y lo reutiliza en el resto de llamadas.
Triángulo de pascal
El último ejemplo de función recursiva que vamos a transformar en iterativa es el triángulo
de Pascal.
Cada elemento del triángulo de Pascal es la suma de los dos números que hay sobre él:
1
1 1
1 2 1
1 13 3
1 4 6 4 1
1 5 10 10 5 1
1 6 15 20 15 6 1
1 7 21 35 35 21 7 1
...
La función Scheme que calcula de forma recursiva el número de Pascal que se encuentra
en una determinada fila y columna es la siguiente:
Al igual que con la función fibonacci esta solución es muy poco eficiente. Para calcular un
valor de la recursión se hacen dos llamadas recursivas, lo que genera un proceso en
forma de árbol con un coste exponencial.
Una forma de solucionar el problema con una versión iterativa consiste en generar cada
una de las filas del triángulo a partir de la anterior. La fila se puede representar con una
lista.
Por último, la función (pascal-iter fila col) llama a la anterior para producir la fila
número fila, y devuelve el elemento número col:
Puedes probar los siguientes valores para comprobar la diferencia de rendimiento entre
las dos versiones:
Expresiones-s
Árboles binarios
Árboles genéricos
Expresiones-s
Las expresiones-s (expresión simbólica) son la base del diseño del lenguaje de
programación LISP. Fueron introducidas en 1958 por John McCarthy, en el
artículo Recursive Functions of Symbolic Expressions and Their Computation by Machine,
Part I, en el que define el lenguaje de programación LISP.
Definición
(A . B)
(A . (B . C))
((A . B) . (C . D))
(A. ((A . B) . (C . D)))
(A . B . C)
(A)
(() . B)
En la misma formulación, McCarthy define una lista como un tipo especial de expresión-s
que termina con el átomo NIL:
Esta formulación matemática fue el origen del concepto de pareja de LISP utilizando las
funciones cons, car y cdr (también definidas en el mismo artículo).
Cualquier expresión de Scheme correcta es una expresión-s, pero al revés no. Por
ejemplo, las siguientes expresionesn-s son expresiones correctas de Scheme:
(1 (2 (3)))
La consideración de una expresión-s como una lista nos lleva a su última definición
recursiva:
Una expresión-s es una lista cuyos elementos pueden ser datos atómicos o
expresiones-s
Una expresión-s vacía es una lista vacía
Con esta definición, considerando las expresiones-s como listas, vemos que se pueden
aplicar las funciones car y cdr para recorrerlas:
Estructura jerárquica
Las expresiones-s representan una estructura de niveles, en la que la lista inicial define el
nivel 1, y las siguientes expresiones anidadas van aumentando de nivel.
Las funciones que podemos utilizar para recorrer una expresion-s son:
Para remarcar el carácter jerárquico de las expresiones-s definimos la función hoja? que
comprueba si el elemento es una hoja o una expresión-s (lista)
(define (hoja? x)
(not (pair? x)))
La función (cuenta-hojas-exp exp) cuenta las hojas que hay en una expresión-s
La función (cuadrado-exp exp) devuelve una expresión-s formada por números con la
expresión-s con la misma estructura y los números elevados al cuadrado.
Por último (map-exp f exp) devuelve una expresión-s formada con la misma
estructura que la original con el resultado de aplicar a cada uno de sus átomos la
función f
Árboles binarios
Un árbol binario es una estructura que contiene tres elementos: un dato, un árbol binario
izquierdo y un árbol binario derecho.
Barrera de abstracción
Llamamos barrera de abstracción al conjunto de funciones que permiten trabajar con una
abstracción y aislan su implementación. Estas funciones definen la especificación de la
abstracción: qué nos permite hacer la abstracción.
Representamos la barrera de abstracción dibujando dos capas separadas por una lista de
funciones. La capa inferior representa el código que implementa la barrera de
abstracción. La capa superior representa el código que usa la barrera de abstracción.
(make-bt dato izq-bt der-bt): crea un árbol binario con un dato, un árbol
binario izquierdo y un árbol binario derecho
vacio-bt: constante que define el árbol binario vacío
(dato-bt bt): devuelve el dato de un árbol binario
(izq-bt bt): devuelve el árbol binario izquierdo
(der-bt bt): devuelve el árbol binario derecho
(hoja-bt? bt): comprueba si el árbol es una hoja (no tiene hijos)
(vacio-bt? bt): comprueba si el árbol es vacío
Usamos el convenio de que todas las funciones de la barrera terminan con el mismo
sufijo. En esta caso -bt de binary tree. También usamos el convenio de llamar make- a la
función que construye la estructura de datos.
Una vez que hemos definido las funciones (y suponiendo que alguien las haya
implementado) podemos usarlas para trabajar con la abstracción. Por ejemplo vamos a
construir un árbol binario y a obtener algunos de sus elementos.
*
/ \
+ 8
/ \
5 3
(define t1 (make-bt 5 vacio-bt vacio-bt))
(define t2 (make-bt 3 vacio-bt vacio-bt))
(define t3 (make-bt '+ t1 t2))
(define t4 (make-bt 8 vacio-bt vacio-bt))
(define t5 (make-bt '* t3 t4))
(dato-bt t5) -> *
(dato-bt (izq-bt t5)) -> +
(dato-bt (izq-bt (izq-bt t5))) -> 5
Necesitamos una estructura con la que podamos guardar 3 elementos: el dato de la raiz,
el sub-árbol izquierdo y el sub-árbol derecho.
La forma natural en Scheme es utilizar una lista que contiene a esos tres elementos. Y el
árbol binario vacio se puede representar entonces como una lista vacía.
Vemos que con esta definición un árbol binario se puede expresar como una expresión-s.
En el caso del ejemplo visto en la figura la expresión-s que define el árbol binario es:
(to-list-bt bt): devuelve una lista formada por los elementos del bt
(member-bt? x bt): busca el elemento x en un árbol binario ordenado
(insert-bt x bt): "inserta" (realmente, no modifica el árbol que se pasa como
parámetro, sino que crea otro) un dato en un árbol binario ordenado
(insert-list-bt lista bt): "inserta" una lista en un árbol binario ordenado
(list-to-bt list): construye un árbol binario ordenado a partir de una lista
to-list-bt
La función (bt-to-list bt) construye una lista con los elementos contenidos en el
árbol binario bt.
(member-bt? x bt)
Si el dato de la raíz del árbol es menor que x, hacemos una llamada recursiva
para comprobar si el dato está el subárbol derecho
Si el dato es mayor que x, buscamos x en el subárbol izquierdo
Si el dato de la raiz es igual que x devolvemos #t.
El coste de la búsqueda es log(n).
(insert-bt x bt)
Por último, la función list-to-bt construye una árbol binario ordenado a partir de una
lista de números utilizando la función anterior.
Árboles genéricos
A diferencia de los árboles binarios, los árboles genéricos pueden tener un número de
hijos variables. Se usan en un gran número de dominios en los que es necesario
representar estructuras jerárquicas: procesadores de lenguaje, documentos XML,
documentos HTML, etc.
Un árbol genérico está formado por un dato y una lista de árboles hijos (también
árboles)
La lista de árboles hijos puede ser vacía. De esta forma, a diferencia de los árboles
binarios, en los árboles genéricos no usamos el concepto de árbol-vacio. Un árbol o nodo
hoja es un árbol cuya lista de hijos es una lista vacía.
Una estructura muy relevante es la lista de hijos, una lista de árboles. En las siguientes
funciones vamos a utilizar esta estructura en bastantes ocasiones. Vamos por ello a darle
un nombre y denominarla bosque. Decimos, por tanto, que la función hijos-tree es
un bosque formado por los hijos del árbol.
¿Cómo implementamos los árboles? En Scheme es muy fácil, utilizando una lista. El
primer elemento de la lista es el dato del árbol y el resto de la lista (otra lista) es la lista de
hijos.
Vamos a construir las siguientes funciones que trabajan sobre árboles genéricos:
(tree-to-list tree): devuelve una lista con los datos del árbol original
(square-tree tree): eleva al cuadrado todos los datos de un árbol
manteniendo la estructura del árbol original
(map-tree f tree): devuelve un árbol con la estructura del árbol original
aplicando la función f a sud datos.
Vamos a ver que todas ellas comparten un mismo patrón de recursión mutua en la que la
función se divide en dos partes: una función sobre árboles y otra sobre bosques (listas de
árboles) que se aplica a los hijos.
(tree-to-list tree)
Comencemos con la función (tree-to-list tree) devuelve una lista con todos los
elementos del árbol.
La recursión de esta función es muy interesante, ya que se basa en una recursión mútua.
La función (to-list-tree tree) obtiene la lista de hijos del nodo actual y llama a la
función (to-list-bosque) con ella. Esta función devuelve una lista plana con todos los
datos del bosque que le pasamos como parámetro. Sólo falta entonces añadir el dato del
árbol inicial.
Es importante notar, al igual que hacíamos cuando hablábamos de árboles binarios, que
el árbol resultante es una copia del árbol que se pasa como parámetro y que no se
pueden modificar directamente los datos del árbol original porque nos encontramos en el
paradigma de programación funcional.
Ambas funciones tienen un patrón muy similar de recursión mutua al usado en la función
anterior tree-to-list.
La recursión se basa en aplicar la propia función a cada uno de los hijos. Para ello se
obtiene la lista de hijos con la función hijos-tree y se usa la función map para aplicar la
propia función a todos los elementos de esa lista (que son árboles). La lista de árboles
resultante se utiliza para construir el árbol que se devuelve añadiendo como dato el
cuadrado del dato original.
map-tree
Sitio web realizado con org-mode y el estilo CSS del proyecto Worg
Obsérvese que la definición recursiva de los números de Fibonacci difiere de las definiciones recursivas de la
función factorial y de la multiplicación . La definición recursiva de fib se refiere dos veces a sí misma . Por
ejemplo, fib (6) = fib (4) + fib (5), de tal manera que al calcular fib (6), fib tiene que aplicarse de manera
recursiva dos veces. Sin embargo calcular fib (5) también implica calcular fib (4), así que al aplicar la definición
hay mucha redundancia de cálculo. En ejemplo anterior, fib(3) se calcula tres veces por separado. Sería
mucho mas eficiente "recordar" el valor de fib(3) la primera vez que se calcula y volver a usarlo cada vez que
se necesite. Es mucho mas eficiente un método iterativo como el que sigue parar calcular fib (n).
If (n < = 1)
return (n);
lofib = 0 ;
hifib = 1 ;
for (i = 2; i < = n; i ++)
{
x = lofib ;
lofib = hifib ;
hifib = x + lofib ;
} /* fin del for*/
return (hifib) ;
Compárese el numero de adiciones (sin incluir los incrementos de la variable índice, i) que se ejecutan para
calcular fib (6) mediante este algoritmo al usar la definición recursiva. En el caso de la función factorial, tienen
que ejecutarse el mismo numero de multiplicaciones para calcular n! Mediante ambos métodos: recursivo e
iterativo. Lo mismo ocurre con el numero de sumas en los dos métodos al calcular la multiplicación. Sin
embargo, en el caso de los números de Fibonacci, el método recursivo es mucho mas costoso que el iterativo.
2.- Propiedades de las definiciones o algoritmos recursivos:
Un requisito importante para que sea correcto un algoritmo recursivo es que no genere una secuencia infinita
de llamadas así mismo. Claro que cualquier algoritmo que genere tal secuencia no termina nunca. Una
función recursiva f debe definirse en términos que no impliquen a f al menos en un argumento o grupo de
argumentos. Debe existir una "salida" de la secuencia de llamadas recursivas.
Si en esta salida no puede calcularse ninguna función recursiva. Cualquier caso de definición recursiva o
invocación de un algoritmo recursivo tiene que reducirse a la larga a alguna manipulación de uno o casos mas
simples no recursivos.
3.- Cadenas recursivas:
Una función recursiva no necesita llamarse a sí misma de manera directa. En su lugar, puede hacerlo de
manera indirecta como en el siguiente ejemplo:
a (formal parameters) b (formal parameters)
{{
..
b (arguments); a (arguments);
..
} /*fin de a*/ } /*fin de b*/
En este ejemplo la función a llama a b, la cual puede a su vez llamar a a, que puede llamar de nuevo a b. Así,
ambas funciones a y b, son recursivas, dado que se llamas a sí mismo de manera indirecta. Sin embargo, el
que lo sean no es obvio a partir del examen del cuerpo de una de las rutinas en forma individual. La rutina a,
parece llamar a otra rutina b y es imposible determinar que se puede llamar así misma de manera indirecta al
examinar sólo a a.
Pueden incluirse mas de dos rutinas en una cadena recursiva. Así, una rutina a puede llamar a b, que llama
a c, ..., que llama a z, que llama a a. Cada rutina de la cadena puede potencialmente llamarse a sí misma y,
por lo tanto es recursiva. Por supuesto, el programador debe asegurarse de que un programa de este tipo no
genere una secuencia infinita de llamadas recursivas.
4.- Definición recursiva de expresiones algebraicas:
Como ejemplo de cadena recursiva consideremos el siguiente grupo de definiciones:
a. una expresión es un término seguido por un signo mas seguido por un término, o un término solo
b. un término es un factor seguido por un asterisco seguido por un factor, o un factor solo.
c. Un factor es una letra o una expresión encerrada entre paréntesis.
Antes de ver algunos ejemplos, obsérvese que ninguno de los tres elementos anteriores está definido en
forma directa en sus propios términos. Sin embargo, cada uno de ellos se define de manera indirecta. Una
expresión se define por medio de un término, un término por medio de un factor y un factor por medio de una
expresión. De manera similar, se define un factor por medio de una expresión, que se define por medio de un
término que a su vez se define por medio de un factor. Así, el conjunto completo de definiciones forma una
cadena recursiva.
La forma mas simple de un factor es una letra. Así A, B, C, Q, Z y M son factores. También son términos,
dado que un término puede ser un factor solo. También son expresiones dado que una expresión puede ser
un término solo. Como A es una expresión, (A) es un factor y, por lo tanto, un término y una expresión. A + B
es un ejemplo de una expresión que no es ni un término ni un factor, sin embargo (A + B) es las tres cosas. A
* B es un término y, en consecuencia, una expresión, pero no es un factor. A * B + C es una expresión, pero
no es un factor.
Cada uno de los ejemplos anteriores es una expresión valida. Esto puede mostrarse al aplicar la definición de
una expresión de cada uno. Considérese, sin embargo la cadena A + * B. No es ni una expresión, ni un
término, ni un factor. Sería instructivo para el lector intentar aplicar la definición de expresión, término y factor
para ver que ninguna de ellas describe a la cadena A + * B. De manera similar, (A + B*) C y A + B + C son
expresiones nulas de acuerdo con las definiciones precedentes.
A continuación se codificará un programa que lea e imprima una cadena de caracteres y luego imprima
"valida" si la expresión lo es y "no valida" de no serlo. Se usan tres funciones para reconocer expresiones,
términos y factores, respectivamente. Primero, sin embrago se presenta una función auxiliar getsymb que
opera con tres parámetros: str, length y ppos. Str contiene la entrada de la cadena de cadena de
caracteres; length representa el número de caracteres en str. Ppos apunta a un puntero pos cuyo valor es la
posición str de la que obtuvimos un carácter la ultima vez. Si pos < length, getsymb regresa el
carácter cadena str [pos] e incrementa pos en 1. Si pos > = length, getsymb regresa un espacio en blanco.
getsymb (str, length, ppos)
char str[];
int length, *ppos;
{
char C;
if (*ppos < length)
c = str [*ppos];
else
c=‘‘;
(*ppos) ++;
return ( c );
} /* fin de getsymb*/
La función que reconoce una expresión se llama expr. Regresa TRUE (o1) (VERDADERO) si una expresión
valida comienza en la posición pos de str y FALSE (o0) FALSO en caso contrario. También vuelve a
colocar pos en la posición que sigue en la expresión de mayor longitud que puede encontrar. Suponemos
también una función readstr que lee una cadena de caracteres, poniendo la cadena en str y su largo
en length.
Una vez descritas las funciones expr y readst, puede escribirse la rutina principal como sigue.
La biblioteca estándar ctype.h incluye una función isalpha que es llamada por una de las funciones siguientes.
# include <stdio.h>
# include <ctype.h>
# define TRUE 1
# define FALSE =
# define MAXSTRINGSIZE 100
main ()
{
char str [MAXSTRINGSIZE];
int length, pos;
readstr (str, &length);
pos = 0;
if (expr (str, length, &pos) = = TRUE && por >= length)
printf ("%s", "valida");
else
printf ("%s", "no valida");
/* la condición puede fallar por una de dos razones (o ambas). Si expr(str, length, &pos) = = FALSE entonces
no hay una expresión valida al inicio de pos. Si pos < length puede que se encuentre una expresión valida,
comenzando en pos, pero no ocupa la cadena completa */
} /*fin del main*/
Las funciones factor y term se parecen mucho a expr excepto en que son responsables del reconocimiento de
factores y término, respectivamente. También reinicializan pos en la posición que sigue al factor o término de
mayor longitud que se encuentra en la cadena str.
Los códigos para estas rutinas se apegan bastantes a las definiciones dadas antes. Cada una intenta
satisfacer uno de los criterios para la entidad que se reconoce. Si se satisface uno de esos criterios el
resultado es TRUE (VERDADERO). Si no satisface ninguno, el resultado es FALSE (FALSO).
expr (str. length, ppos)
char str [];
int length, *ppos;
{
/* buscando un término */
if (term( str, length, ppos) = = FLASE)
return (FLASE);
/* se ha encontrado un término; revisar el siguiente símbolo */
if (getsymb(str, length, ppos) ! = ‘+’) {
/* se encontró la mayor expresión (un solo término). Reposicionar pos para que señale la última posición de la
expresión */
(*ppos) - - ;
return (TRUE);
} /* fin del if */
/* en este punto, se a encontrado un termino y su signo mas. Se deberá buscar otro termino */
return (term(str, length, ppos));
} /*fin de expr */
La rutina term que reconoce términos, es muy similar y será presentada sin comentarios.
term (str, length, ppos)
char str[];
int length, *ppos;
{
if (factor(str, length, ppos) = = FALSE)
return (FALSE);
if (getsymb (str, length, ppos) ! = ‘+’) {
(*ppos) -- ;
return (TRUE) ;
} /* fin del if */
return (factor(str, length, ppos));
} /* fin de term */
La función factor reconoce factores y debería ser ahora bastante sencilla. Usa el programa común de
biblioteca isalpha (esta función se encuentra en la biblioteca ctype.h), que regresa al destino de cero si su
carácter de parámetro es una letra y cero (o FALSO) en caso contrario.
factor (str, length, ppos)
char str[];
int length, *ppos;
{
int c;
if ((c = getsymb (str, length, ppos)) ! = ‘)’ )
return (isalpha(c));
return (expr(str, length, ppos) && getsymb (str, length, ppos) == ‘)’ );
} /* fin de factor */
Las tres rutinas son recursivas, dado que cada una puede llamar a sí misma da manera indirecta. Pos
ejemplo, si se sigue la acción del programa para la cadena de entrada " (a * b + c * d) + (e * (f) + g) " se
encontrará que cada una de las tres rutinas expr, term y factor se llama a sí misma.
5.- Programación Recursiva:
Es mucho mas difícil desarrollar una solución recursiva en C para resolver un problema especifico cuando no
se tiene un algoritmo. No es solo el programa sino las definiciones originales y los algoritmos los que deben
desarrollarse. En general, cuando encaramos la tarea de escribir un programa para resolver un problema no
hay razón para buscar una solución recursiva. La mayoría de los problemas pueden resolverse de una
manera directa usando métodos no recursivos. Sin embargo, otros pueden resolverse de una manera mas
lógica y elegante mediante la recursión.
Volviendo a examinar la función factorial. El factor es, probablemente, un ejemplo fundamental de un
problema que no debe resolverse de manera recursiva, dado que su solución iterativa es directa y simple. Sin
embargo, examinaremos los elementos que permiten dar una solución recursiva. Antes que nada, puede
reconocerse un gran número de casos distintos que se deben resolver. Es decir, quiere escribirse un
programa para calcular 0!, 1!, 2! Y así sucesivamente. Puede identificarse un caso "trivial" para el cual la
solución no recursiva pueda obtenerse en forma directa. Es el caso de 0!, que se define como 1. El siguiente
paso es encontrar un método para resolver un caso "complejo" en términos de uno mas "simple", lo cual
permite la reducción de un problema complejo a uno mas simple. La transformación del caso complejo al
simple resultaría al final en el caso trivial. Esto significaría que el caso complejo se define, en lo fundamental,
en términos del mas simple.
Examinaremos que significa lo anterior cuando se aplica la función factorial. 4! Es un caso mas complejo que
3!. La transformación que se aplica al numero a para obtener 3 es sencillamente restar 1. Si restamos 1 de 4
de manera sucesiva llegamos a 0, que es el caso trivial. Así, si se puede definir 4! en términos de 3! y, en
general, n! en términos de (n – 1)!, se podrá calcular 4! mediante la definición de n! en términos de (n – 1)! al
trabajar, primero hasta llegar a 0! y luego al regresar a 4!. En el caso de la función factorial se tiene una
definición de ese tipo, dado que:
n! = n * (n – 1)!
Asi, 4! = 4 * 3! = 4 * 3 * 2! = 4 * 3 * 2 * 1! = 4 * 3 * 2 * 1 * 0! = 4 * 3 * 2] * ] = 24
Estos son los ingredientes esenciales de una rutina recursiva: poder definir un caso "complejo" en términos de
uno más "simple" y tener un caso "trivial" (no recursivo) que pueda resolverse de manera directa. Al hacerlo,
puede desarrollarse una solución si se supone que se ha resuelto el caso más simple. La versión C de la
función factorial supone que esta definido (n –1)! y usa esa cantidad al calcular n!.
Otra forma de aplicar estas ideas a otros ejemplos antes explicados. En la definición de a * b, es trivial el caso
de b = 1, pues a * b es igual a a. En general, a + b puede definirse en términos de a * (b – 1) mediante la
definición a * b = a * (b – 1) + a. De nuevo, el caso complejo se transforma en un caso mas simple al restar 1,
lo que lleva, al final, al caso trivial de b = 1. Aquí la recursión se basa únicamente en el segundo parámetro, b.
Con respecto al ejemplo de la función de Fibonacci, se definieron dos casos triviales: fib(0) = 0 y fib(1) = 1. Un
caso complejo fib(n) se reduce entonces a dos más simples: fib(n –1) y fib(n –2). Esto se debe a la definición
de fib(n) como fib(n –1) + fib(n – 2), donde se requiere de dos casos triviales definidos de manera directa.
Fib(1) no puede definirse como fib(0) + fib(-1) porque la función de Fibonacci no está definida para números
negativos.
6.- Asignación estática y dinámica de memoria:
Hasta este momento solamente hemos realizado asignaciones estáticas del programa, y más concretamente
estas asignaciones estáticas no eran otras que las declaraciones de variables en nuestro programa. Cuando
declaramos una variable se reserva la memoria suficiente para contener la información que debe almacenar.
Esta memoria permanece asignada a la variable hasta que termine la ejecución del programa (función main).
Realmente las variables locales de las funciones se crean cuando éstas son llamadas pero nosotros no
tenemos control sobre esa memoria, el compilador genera el código para esta operación automáticamente.
En este sentido las variables locales están asociadas a asignaciones de memoria dinámicas, puesto que se
crean y destruyen durante la ejecución del programa.
Así entendemos por asignaciones de memoria dinámica, aquellas que son creadas por nuestro programa
mientras se están ejecutando y que por tanto, cuya gestión debe ser realizada por el programador.
El lenguaje C dispone, como ya indicamos con anterioridad, de una serie de librerías de funciones estándar.
El fichero de cabeceras stdlib.h contiene las declaraciones de dos funciones que nos permiten reservar
memoria, así como otra función que nos permite liberarla.
Las dos funciones que nos permiten reservar memoria son:
malloc (cantidad_de_memoria);
calloc (número_de_elementos, tamaño_de_cada_elemento);
Estas dos funciones reservan la memoria especificada y nos devuelven un puntero a la zona en cuestión. Si
no se ha podido reservar el tamaño de la memoria especificado devuelve un puntero con el valor 0 o NULL. El
tipo del puntero es, en principio void, es decir, un puntero a cualquier cosa. Por tanto, a la hora de ejecutar
estás funciones es aconsejable realizar una operación cast (de conversión de tipo) de cara a la utilización de
la aritmética de punteros a la que aludíamos anteriormente. Los compiladores modernos suelen realizar esta
conversión automáticamente.
Antes de indicar como deben utilizarse las susodichas funciones tenemos que comentar el operador sizeof.
Este operadores imprescindible a la hora de realizar programas portables, es decir, programas que puedan
ejecutarse en cualquier máquina que disponga de un compilador de C.
El operador sizeof (tipo_de_dato), nos devuelve el tamaño que ocupa en memoria un cierto tipo de dato, de
esta manera, podemos escribir programas independientes del tamaño de los datos y de la longitud de palabra
de la máquina. En resumen si no utilizamos este operador en conjunción con las conversiones de tipo cast
probablemente nuestro programa sólo funciones en el ordenador sobre el que lo hemos programado.
Por ejemplo, el los sistemas PC, la memoria está orientada a bytes y un entero ocupa 2 posiciones de
memoria, sin embargo puede que en otro sistema la máquina esté orientada a palabras (conjuntos de 2 bytes,
aunque en general una máquina orientada a palabras también puede acceder a bytes) y por tanto el tamaño
de un entero sería de 1 posición de memoria, suponiendo que ambas máquinas definan la misma precisión
para este tipo.
Con todo lo mencionado anteriormente veamos un ejemplo de un programa que reserva dinámicamente
memoria para algún dato.
#include <stdlib.h
#include <stdio.h>
main()
{
int *p_int;
float *mat;
p_int = (int *) malloc(sizeof(int));
mat = (float *)calloc(20,sizeof(float));
if ((p_int==NULL)||(mat==NULL))
{
printf ("\nNo hay memoria");
exit(1);
}
/* Aquí irían las operaciones sobre los datos */
/* Aquí iría el código que libera la memoria */
}
Este programa declara dos variables que son punteros a un entero y a un float. A estos punteros se le asigna
una zona de memoria, para el primero se reserva memoria para almacenar una variable entera y en el
segundo se crea una matriz de veinte elementos cada uno de ellos un float. Obsérvese el uso de los
operadores cast para modificar el tipo del puntero devuelto por malloc y calloc, así como la utilización del
operador sizeof.
Como se puede observar no resulta rentable la declaración de una variable simple (un entero, por ejemplo,
como en el programa anterior) dinámicamente, en primer lugar por que aunque la variable sólo se utilice en
una pequeña parte del programa, compensa tener menos memoria (2 bytes para un entero) que incluir todo el
código de llamada a malloc y comprobación de que la asignación fue correcta (esto seguro que ocupa más de
dos bytes).
En segundo lugar tenemos que trabajar con un puntero con lo cual el programa ya aparece un poco más
engorroso puesto que para las lecturas y asignaciones de las variables tenemos que utilizar el operador *.
Para termina un breve comentario sobre las funciones anteriormente descritas. Básicamente da lo mismo
utilizar malloc y calloc para reservar memoria es equivalente:
mat = (float *)calloc (20,sizeof(float));
mat = (float *)malloc (20*sizeof(float));
La diferencia fundamental es que, a la hora de definir matrices dinámicas calloc es mucho más claro y
además inicializa todos los elementos de la matriz a cero. Nótese también que puesto que las matrices se
referencian como un puntero la asignación dinámica de una matriz nos permite acceder a sus elementos con
instrucciones de la forma:
NOTA: En realidad existen algunas diferencias al trabajar sobre máquinas con alineamiento de palabras.
mat[0] = 5;
mat[2] = mat[1]*mat[6]/67;
Con lo cual el comentario sobre lo engorroso que resultaba trabajar con un puntero a una variable simple, en
el caso de las matrices dinámicas no existe diferencia alguna con una declaración normal de matrices.
La función que nos permite liberar la memoria asignada con malloc y calloc es free(puntero), donde puntero es
el puntero devuelto por malloc o calloc.
En nuestro ejemplo anterior, podemos ahora escribir el código etiquetado como: /* Ahora iría el código que
libera la memoria */
free (p_int);
free(mat);
Hay que tener cuidado a la hora de liberar la memoria. Tenemos que liberar todos los bloque que hemos
asignado, con lo cual siempre debemos tener almacenados los punteros al principio de la zona que
reservamos. Si mientras actuamos sobre los datos modificamos el valor del puntero al inicio de la zona
reservada, la función free probablemente no podrá liberar el bloque de memoria.
7.- Ejemplos:
7.1.- Las Torres de Hanoi:
A continuación se verá cómo pueden usarse técnicas recursivas para lograr una solución lógica y elegante de
un problema que no se especifica en términos recursivos. EL problema es el de "las torres de Hanoi", cuyo
planteamiento inicial se muestra en la figura a continuación...
Hay tres postes: A, B y C. En el poste A se ponen cinco discos de diámetro diferente de tal manera que un
disco de diámetro mayor siempre queda debajo de uno de diámetro menor. El objetivo es mover los discos al
poste C usando B como auxiliar. Sólo puede moverse el disco superior de cualquier poste a otro poste, y un
disco mayor jamás puede quedar sobre uno menor. Considérese la posibilidad de encontrar una solución. En
efecto, ni siquiera es claro que exista una.
Ahora se verá si se puede desarrollar una solución. En lugar de concentrar la atención en una solución para
cinco discos, considérese el caso general de n discos. Supóngase que se tiene una solución para n – 1 discos
y que en términos de ésta, se pueda plantear la solución para n – 1 discos. El problema se resolvería
entonces. Esto sucede porque en el caso trivial de un disco (al restar 1 de n de manera sucesiva se producirá,
al final, 1) la solución es simple: sólo hay que el único disco del poste A a C. Así se habrá desarrollado una
solución recursiva si se plantea una solución para n discos en términos de n – 1. Considérese la posibilidad de
encontrar tal relación. Para el caso de cinco discos en particular, supóngase que se conoce la forma de mover
cuatro de ellos del poste A al otro, de acuerdo con las reglas. ¿Cómo puede completarse entonces el
trabajo de mover el quinto disco? Cabe recordar que hay 3 postes disponibles.
Supóngase que se supo cómo mover cuatro discos del poste A al C. Entonces, se pondrá mover éstos
exactamente igual hacia B usando el C como auxiliar. Esto da como resultado la situación los cuatro primeros
discos en el poste B, el mayor en A y en C ninguno. Entonces podrá moverse el disco mayor de A a C y por
último aplicarse de nuevo la solución recursiva para cuatro discos para moverlo de B a C, usando el poste A
como auxilia. Por lo tanto, se puede establecer una solución recursiva de las torres de Hanoi como sigue:
Para mover n discos de A a C usando B como auxiliar:
1. Si n = = 1, mover el disco único de A a C y parar.
2. Mover el disco superior de A a B n – 1 veces, usando C como auxiliar.
3. Mover el disco restante de A a C.
4. Mover los disco n – 1 de B a C usando A como auxiliar
Con toda seguridad este algoritmo producirá una solución completa por cualquier valor de n. Si n = = , el paso
1 será la solución correcta. Si n = = 2, se sabe entonces que hay una solución para n – 1 = = 1, de manera tal
que los pasos 2 y 4 se ejecutaran en forma correcta. De manera análoga, cuando n = = 3 ya se habrá
producido una solución para n – 1 = = 2, por lo que los pasos 2 y 4 pueden ser ejecutados. De esta forma se
puede mostrar que la solución funciona para n = = 1, 2, 3, 4, 5,... hasta el valor para el que se desee encontrar
una solución. Adviértase que la solución se desarrollo mediante la identificación de un caso trivial (n = = 1) y
una solución para el caso general y complejo (n) en términos de un caso mas simple (n – 1).
Ya se demostró que las transformaciones sucesivas de una simulación no recursivas de una rutina recursiva
pueden conducir a un programa mas simple para resolver un problema. Ahora se simulara la recursión del
problema y se intentara simplificar la simulación no recursiva.
towers (n, frompeg, topeg, auxpeg)
int n;
char auxpeg, frompeg, topeg;
{
/* si es solo un disco, mover y regresar */
if (n = = 1) {
printf (" /n%s%c%s%c%", "mover disco 1 del poste",frompeg, "al poste", topeg);
return; } /* fin del if*/
/* mover los n – 1 discos de arriba de A a B, usando como auxiliar */
towers (n – 1, frompeg, auxpeg, tpoeg);
/* move remaining disk from A to C */
printf ("/n%s%d%s%c%s%c%", "mover disco", n, "del poste" frompeg, "al poste", topeg);
/* mover n – 1 discos de B hacia C empleando a A como auxiliar */
towers (n – 1, auxpeg, topeg, frompeg);} /* fin de towers */
En esta función, hay cuatro parámetros, cada uno de los cuales esta sujeto a cambios en cada llamada
recursiva. En consecuencia, el área de datos debe contener elementos que representen a los cuatro. No hay
variables locales. Hay un solo valor temporal que se necesita para guardar el valor de n – 1, pero esta se
puede representar por un valor temporal similar en el programa de simulación y no tiene que estar apilada.
Hay tres puntos posibles a los que regresa la función en varias llamadas: el programa de llamada y los dos
puntos que siguen a las llamadas recursivas. Por lo tanto, se necesitan cuatro etiquetas.
start:
Label1:
Label2:
Label3:
La dirección de regreso se codifica como un entero (1, 2 o 3) dentro de cada área de datos.
Considérese la siguiente simulación no recursiva de towers:
struct dataarea {
int nparam;
char fromparam;
char toparam;
char auxparam;
short int retaddr;
};
struct stack {
int top struct dataarea item [MAXSTACK]; };
simtowers (m, frompeg, topeg, auxpeg)
int n;
char auspeg, frompeg, topeg; {
struct stack s;
struct dataarea currarea;
char temp;
short int i;
s.top = -1;
currarea.nparam = 0;
currarea.fromparam = ‘ ‘ ;
currarea.toparam = ‘ ‘ ;
currarea. auxparam = ‘ ‘ ;
currarea.retaddr = 0;
/* colocar en la pila un área de datos simulados */
push (&s, &currarea);
/* asignar parámetros y direcciones de regreso del área de datos actual a sus valores apropiados */
currarea.nparam = n;
currarea,fromparam = frompeg;
currarea,toparam = topeg;
currarea.auxparam = auxpeg;
currarea.retaddr = 1;
start: /* Este es el inicio de la rutina simulada */
if (currarea.nparam = = 1) {
printf (" /n%s%c%s%c", "mover disco 1 del poste", currarea.fromparam, "al poste", currarea.toparam) ;
i = currarea.retaddr;
pop (&s, &currarea);
switch (i) {
case 1: goto label1;
case 2: goto label2;
case 3: goto label3; } /* fin del switch */
} /* fin del if */
/* Esta es la primera llamada recursiva */
push (&s, &currarea);
-- currarea.nparam;
temp = currarea.auxparam;
currarea.auxparam = currarea.toparam;
currarea.toparam = temp;
currarea.retaddr = 2;
got start;
label2: /* se regresa a este punto desde la primera llamada recursiva */
printf ("/n%s%d%s%c%s%c", "mover disco", currarea.nparam, "del poste", currarea.fromparam, "al poste",
currarea.toparam);
/* Esta es la segunda llamada recursiva */
push (&s, &currarea);
--currarea.nparam;
temp = currarea.fromparam;
currarea.fromparam = currarea.auxparam;
currarea.auxparam = temp;
currarea.rtaddr = 3;
got start;
label3: /* se regresa a este punto desde la segunda llamada recursiva */
i = currarea.retaddr;
pop (&s, &currarea);
swicth (i) {
case 1: goto label1;
case 2: goto label2;
case 3: goto label3; } /* fin del switch */
label1: return;
} /* fin de simtowers */
Ahora se simplificará el programa. En primer lugar, debe observarse que se usan tres etiquetas para indicar
direcciones de regreso; una para cada una de las dos llamadas recursivas y otra para el regreso al programa
principal. Sin embargo, el regreso al programa principal puede señalarse por un subdesborde en la pila, de la
misma for que en la segunda versión simfact. Esto deja dos etiquetas de regreso. Si pudiera eliminarse otra
mas, sería innecesario guardar en la pila la dirección de regreso, ya que solo restaría un punto al que se
podría transferir el control si se eliminan los elementos de la pila con éxito. Ahora dirijamos nuestra atención a
la segunda llamada recursiva y a la instrucción:
towers (n – 1, auxpeg, topeg, frompeg);
Las acciones que ocurren en la simulación de esta llamada son las siguientes:
1. Se coloca el área de datos vigente a 1 dentro de la pila.
2. En la nueva área de datos vigente a 2, se asignan los valores respectivos n – 1, auxpeg, topeg, y frompeg
a los parámetros.
3. En el área de datos vigente a 2, se fija la etiqueta de retorno a la dirección de la instrucción que sigue de
inmediato a la llamada.
4. Se salta hacia el principio de la rutina simulada.
5. Después de completar la rutina simulada, ésta queda lista parar regresar. Las siguientes acciones se
llevan a efecto:
6. Se salva la etiqueta de regreso, /, de área de datos vigentes a 2.
7. Se eliminan de la pila y se fija el área de datos vigente como el área de datos eliminada de la pila, a 1.
8. Se transfiere el control a /.
Sin embrago, / es la etiqueta del final del bloque del programa ya que la segunda llamada a towers aparece en
la última instrucción de la función. Por lo tanto, el siguiente paso es volver a eliminar elementos de la pila y
regresar. No se volverá a hacer uso de la información del área de datos vigente a 1, ya que ésta es destruida
en la eliminación de los elementos en la pila tan pronto como se vuelve a almacenar. Puesto que no hay razón
para volver a usar esta área de datos, tampoco hay razón para salvarla en la pila durante la simulación de la
llamada. Los datos se deben salvar en la pila sólo si se van a usar otra vez. En consecuencia, la seguida
llamada a towers puede simularse en forma simple mediante:
1. El cambio de los parámetros en el área de datos vigente a sus valores respectivos.
2. El "salto" al principio de la rutina simulada.
Cuando la rutina simulada regresa puede hacerlo en forma directa a la rutina que llamó a la versión vigente.
No hay razón para ejecutar un regreso a la versión vigente, sólo para regresar de inmediato a la versión
previa. Por lo tanto, se elimina la necesidad de guardar en la pila la dirección de regreso al simular la llamada
externa (ya que se puede señalar mediante subdesborde y simular la segunda llamada recursiva, ya que no
hay necesidad de salvar y volver a almacenar al área de datos de la rutina que llamada en este momento). La
única dirección de regreso que resta es la que sigue a la primera llamada recursiva.
Ya que sólo queda una dirección de regreso posible, no tiene caso guardarla en la pila para que se vuelva a
insertar y eliminar con el resto de los datos. Siempre se eliminan elementos de la pila con éxito, hay una solo
dirección hacia la que se puede ejecutar un "salto" (la instrucción que sigue a la primera llamada). Como los
nuevos valores de las variables del área de datos vigente se obtendrán a partir de los datos antiguos de área
de datos vigente, es necesario declarar una variable adicional, temp, de manera que los valores sean
intercambiables.
7.2.- El Problema de las Ocho Reinas:
El problema de las ocho reinas y en general de las N reinas, se basa en colocar 8 reinas en un tablero de 8´
8 (o N en un tablero de NxN, si el problema se generaliza), de forma que en no puede haber dos piezas en la
misma línea horizontal, vertical o diagonal, ver Figura 1.
Para ver el gráfico seleccione la opción "Descargar" del menú superior
Figura 1
Posible solución para el problema de las ocho reinas
Este programa has sido muy estudiado por los matemáticos. Se trata de un problema NP-Completo que no
tiene solución para N=2 y N=3. Para N=4 tiene una única solución. Para N=8 hay más de
80 soluciones dependiendo de las restricciones que se impongan.
Una forma de abordar el problema se lleva a cabo mediante la construcción de un predicado en Prolog del tipo
siguiente: reinas (N, Solución), donde N representa las dimensiones del tablero y el número de reinas y
Solución es una lista que contiene la permutación de la lista de números que resuelven el problema. Los
índices de dicha lista representan la columna en la que se encuentra una reina y el número que almacena la
posición o índice representa la fila donde la reina está colocada. Así, para el ejemplo mostrado en la Figura 1,
tenemos que R=[2,4,1,3].
Este problema es resuelto, de modo clásico, por el método de prueba y error, luego se adapta perfectamente
a un algoritmo de backtracking. Básicamente, el problema se reduce a colocar una reina, e intentar repetir
el proceso teniendo en cuenta la reina colocada. Si logramos poner todas las reinas el problema se ha
resuelto, en caso contrario, deshacemos lo que llevamos hasta ahora y probamos con otra combinación. Por
tanto, hemos de generar un conjunto de permutaciones y seleccionar aquella que cumpla los
requisitos impuestos por el juego.
Veamos el código que resuelve el problema:
rango(N, N, [N]).
rango(N, M, [N|Cola]):-N<M, Aux is N+1, rango(Aux, M, Cola).
dame(X,[X|Xs],Xs).
dame(X,[Y|Ys],[Y|Zs]):-dame(X,Ys,Zs).
permuta([],[]).
permuta(L,[Z|Zs]):-dame(Z,L,Ys), permuta(Ys,Zs).
atacada(X,Y):-atacada(X,1,Y).
atacada(X,N,[Y|Ys]):-X is Y+N; X is Y-N.
atacada(X,N,[Y|Ys]):-N1 is N+1, atacada(X,N1,Ys).
correcta([]).
correcta([X|Y]):-correcta(Y), not atacada(X,Y).
reina(N, Solucion):-rango(1,N,L1), permuta(L1,Solucion), correcta(Solucion).
Es muy importante comprender como funciona cada uno de los predicados para entender el funcionamiento
del algoritmo general.
Prolog permite implementar los programas casi directamente a partir de las especificaciones realizadas a
partir de un análisis y diseño de la solución desde un alto nivel de abstracción. Además el procedimiento de
backtracking está implícito en el propio motor de inferencia, luego este paradigma se adapta perfectamente a
nuestro problema.
Si analizamos y diseñamos nuestro problema tenemos que la forma de resolverlo se resume en los pasos
siguientes:
Para N, obtenemos una lista de números comprendidos entre 1 y N: [1,2,3,4,...,N].
Obtenemos una permutación del conjunto de números de la lista.
Comprobamos que la permutación es correcta.
Si la permutación no es correcta, lo que debemos hacer es volver al paso 2 para generar una permutación
nueva.
Comencemos a analizar la solución implementada. El problema se resuelve con el predicado reina(N,
Solución): rango(1,N,L1), permuta(L1,Solucion), correcta Solución).Como vemos es, sencillamente, una copia
de las especificaciones realizadas más arriba. Se genera el rango entre 1 y N, se obtiene una permutación y
se comprueba si la permutación es, o no, correcta. En el caso de que cualquier predicado del consecuente
falle, la propia máquina Prolog se encarga de realizar el proceso de backtracking. Con lo cual ya tenemos
cubiertos los cuatro pasos fundamentales del algoritmo. Para tener más claras las ideas, observemos el árbol
de ejecución general del objetivo reina(4,Solucion) en la Figura 2.
Figura 2
Árbol de ejecución para el objetivo reina(4,Solucion)
He aquí otro ejemplo sobre el problema de las ocho reinas. Primero se mostrara un pseudocodigo sobre el
mismo y luego su respectiva codificación en el lenguaje C.
<PRINCIPIO ensayar> (i: entero)
inicializar el conjunto de posiciones de la reina i-ésima
+-REPETIR hacer la selección siguiente
| +-SI segura ENTONCES
| | poner reina
| | +-SI i < 8 ENTONCES
| | | LLAMAR ensayar (i + 1)
| | | +-SI no acertado ENTONCES
| | | | quitar reina
| | | +-FINSI
| | +-FINSI
| +-FINSI
+-HASTA acertada O no hay más posiciones
<FIN>
Observaciones sobre el código:
1) Estudiar la función ensayar() a partir de este pseudocódigo.
2) Vectores utilizados:
int posiciones_en_columna[8]; RANGO: 1..8
BOOLEAN reina_en_fila[8]; RANGO: 1..8
BOOLEAN reina_en_diagonal_normal[15]; RANGO: -7..7
BOOLEAN reina_en_diagonal_inversa[15]; RANGO: 2..16
En C, el primer elemento de cada vector tiene índice 0, esto
es fácil solucionarlo con las siguientes macros:
#define c(i) posiciones_en_columna[(i)-1]
#define f(i) reina_en_fila[(i)-1]
#define dn(i) reina_en_diagonal_normal[(i)+7]
#define di(i) reina_en_diagonal_inversa[(i)-2]
Significado de los vectores:
c(i) : la posición de la reina en la columna i
f(j) : indicativo de que no hay reina en la fila j-ésima
dn(k): indicativo de que no hay reina en la diagonal normal
(\) k-ésima
di(k): indicativo de que no hay reina en la diagonal
invertida (/) k-ésima
Dado que se sabe, por las reglas del ajedrez, que una reina actúa sobre todas las piezas situadas en la
misma columna, fila o diagonal del tablero se deduce que cada columna puede contener una y sólo una reina,
y que la elección de la situación de la reina i-ésima puede restringirse a los cuadros de la columna i. Por tanto,
el parámetro i se convierte en el índice de columna, y por ello el proceso de selección de posiciones queda
limitado a los ocho
posibles valores del índice de fila j.
A partir de estos datos, la línea poner reina del pseudocódigo es:
c (i) = j; f (j) = di (i + j) = dn (i - j) = FALSE;
y la línea quitar reina del pseudocódigo:
f (j) = di (i + j) = dn (i - j) = TRUE;
y la condición segura del pseudocódigo:
f (i) && di (i + j) && dn (i - j)
/* Ficheros a incluir: */
#include <stdio.h> /* printf () */
#include <conio.h> /* getch () */
/* Macros: */
#define BOOLEAN int
#define TRUE 1
#define FALSE 0
/* Variables globales: */
BOOLEAN acertado;
int posiciones_en_columna[8];
BOOLEAN reina_en_fila[8];
BOOLEAN reina_en_diagonal_normal[15];
BOOLEAN reina_en_diagonal_inversa[15];
#define c(i) posiciones_en_columna[(i)-1]
/* rango de índice: 1..8 */
#define f(i) reina_en_fila[(i)-1]
/* rango de índice: 1..8 */
#define dn(i) reina_en_diagonal_normal[(i)+7]
/* rango de índice: -7..7 */
#define di(i) reina_en_diagonal_inversa[(i)-2]
/* rango de índice: 2..16 */
/* Prototipos de las funciones: */
void proceso (void);
void ensayar (int i);
/* Definiciones de las funciones: */
void main (void)
{
printf ("\n\nPROBLEMA DE LAS OCHO REINAS:\n ");
proceso ();
printf ("\n\nPulsa cualquier tecla para finalizar. ");
getch ();
}
void proceso (void)
{
register int i,j;
for (i = 1; i <= 8; i++)
f (i) = TRUE;
for (i = 2; i <= 16; i++)
di (i) = TRUE;
for (i = -7; i <= 7; i++)
dn (i) = TRUE;
ensayar (1);
if (acertado)
for (printf ("\n\nLA SOLUCION ES:\n\n"), i = 1; i <= 8; i++)
{
for (j = 1; j <= 8; j++)
printf ("%2d", c (j) == i ? 1 : 0);
printf ("\n");
}
else
printf ("\n\nNO HAY SOLUCION.\n");
}
void ensayar (int i)
{
int j = 0;
do
{
j++;
acertado = FALSE;
if (f (j) && di (i + j) && dn (i - j))
{
c (i) = j;
f (j) = di (i + j) = dn (i - j) = FALSE;
if (i < 8)
{
ensayar (i + 1);
if (! acertado)
f (j) = di (i + j) = dn (i - j) = TRUE;
}
else
acertado = TRUE;
}
} while (! acertado && j != 8);
favorito
1
Tengo que crear una función que elimine de una lista todos los elementos con dato numérico x.
Mi Código:
struct nodolista {
int elem;
nodolista *sig;
};
Mariano
19.1k124678
formulada el 6 abr. a las 22:27
Edu
1119
añade un comentario
1 respuesta
activasmás antiguas votos
Haz la llamada recursiva que devuelva el siguiente, de este modo a la vuelta de la llamada
recursiva lo puedes asignar al actual si no lo has eliminado. Es decir, al ir llamando al siguiente:
if (l == NULL) {
return l; // Devolvemos NULL
} else {
if (l->elem == x) {
lista tmp = l->sig; // Asignamos el siguiente a un temporal.
delete l; // Eliminamos el actual.
return remover(x, tmp); // Devolvemos lo que devuelva la llamada recursiva con el siguiente
}
else {
l->sig = remover(x, l->sig); // Asignamos lo que devuelva la llamada recursiva con el siguiente.
return l; // Devolvemos el actual;
}
}
}
Espero haberme explicado, sino no dudes en comentar la respuesta e intendo explicarme mejor.
Como comenta @Xam Puedes usar nullptr en vez de null a partir de C++11.
compartirmejorar esta respuesta
editada el 7 abr. a las 0:16
respondida el 7 abr. a las 0:10
WyrncaelLeer
más: http://www.monografias.com/trabajos14/recursividad/recursividad.shtml#ixzz5FWom7YZG