Bloque de Colores

Algunas notas sobre Explosión Combinatoria, los Lenguajes de Scripting y el Uso de Memoria

Icono de Post March 1 2012 :: c, Javascript ::

Hace un par de meses estuve trabajando en un problema de combinatoria que consistía en los siguiente: Dado un alfabeto A de tamaño k, cuantas cadenas de tamaño n pueden construirse con exactamente z símbolos de A. Busque en bastantes libros y no pude encontrar una solución, por lo que decidí investigarlo por mi cuenta, para esto, decidí hacer una serie de experimentos con diferentes valores de k, n y z para intentar llegar a una formula general (que sera el tema de un próximo post).

Lo interesante fue que en este proceso fui probando los limites de mi PC y tuve que buscar diferentes soluciones para lograr los resultados que buscaba, ese es el tema de este post, cuyo código se puede encontrar aquí.

Primer Intento en Javascript

Mi primer intento fue simplemente hacer una variedad de diferentes loops en Javascript generando las diferentes combinaciones. Use Javascript porque ingenuamente pensé que era un problema simple y lo que quería resolver rápido.

No hay nada muy complejo en esta solución y se pueden ver una versión para 4 y 5 elementos en la carpeta de Github.

El problema es obviamente que hay que hacer un nested loop para cada tamaño de cadena y alfabeto distinto, era evidente que necesitaba una versión mejor.

Segundo Intento en Javascript

El segundo intento es una versión general que usando recursividad busca generar las diferentes combinaciones y además cuenta la cantidad de símbolos que tiene cada cadena y genera un reporte. De nuevo, fue una versión rápida codificada sin mucho razonamiento previo, pero que era lo suficientemente general como para poder resolver el problema.

El problema fue que cuando intente correrla con valores mayores a 6 o 7, mi navegador se tildaba y tenía que reiniciar la maquina. Bueno, es bien sabido que javascript sobre un browser no es un entorno muy confiable cuando se le demanda demasiado, por otro lado todas las cadenas de tamaño 7 con un alfabeto de 7 son 77=823543 variaciones. Además por como funciona javascript en la función donde se generan estas cadenas, se copia un array, pero cuando se agrega un elemento a un array o se concatenan dos array en javascript (a diferencia de por ejemplo Python, donde las listas se concatenan por referencia) se genera un array nuevo con el consiguiente uso de memoria que esto implica.


function generarCombinaciones(depth, size) {
	if(depth==1) {
		var arr = [];
		for(var x=0; x<size; x++) {
			arr[x] = x;
		} 
		return arr;
	} else {
		var arr = new Array();
		var pre = generarCombinaciones(depth-1, size);
		for(var x=0; x<size; x++) {
			for(var y=0; y<pre.length; y++) {
				var arr2 = new Array();
				arr2.push(x);
				arr2 = arr2.concat(pre[y]);
				arr.push(arr2);
			}
		}
		return arr;
	}
}

Decidí para mejorar la performance pasar a C.

Primer intento en C

El primer intento en C tenía en cuenta este problema y lo que hacia para resolverlo era a través de punteros ir generando recursivamente las nuevos elementos como referenciando a los elementos anteriores. Por ejemplo: primero se generaba la lista [1,2] y luego para generar [[1,1], [1,2], [2,1], [2,2]] se utilizaba el mismo [1] para [1,1] y para [2,1] o sea el puntero de estas dos listas apuntan al mismo nodo en las dos listas. De esta manera se va generando recursivamente un árbol en el que cada sublista es reutilizada por la lista que las listas que la contienen.

Esta versión se puede encontra aquí y se puede correr, haciendo gcc contar_combinaciones.c -lm -o contar_combinaciones. Además esta versión genera una página HTML con las tablas de resultados y también una tabla con los resultados factorizados. La porción de código a la que se hace referencia es:



Result_list generar_combinaciones(int depth, int size) {
	Result_list results = NULL;
	Result_list cursor;

	int x;	

	if(depth == 0) {
		for(x=0; x<size; x++) {
			List temp;
			temp = (List) malloc(sizeof(struct Node));
			temp->value = x;
			temp->next = NULL;

			Result_list temp_result;
			temp_result = (Result_list) malloc(sizeof(struct Result));
			temp_result->list = temp;
			temp_result->next = NULL;

			if(results == NULL) {
				results = temp_result;
				cursor = temp_result;
			} else {
				cursor->next = temp_result;
				cursor = cursor->next;
			}
		}
		return results;
	} else {
		Result_list previous_results;
		Result_list previous_temp_cursor;

		previous_results = generar_combinaciones(depth-1, size);
		
		while(previous_results != NULL) {
			for(x=0; x<size; x++) {
				List temp;
				temp = (List) malloc(sizeof(struct Node));
				temp->value = x;
				temp->next = previous_results->list;

				Result_list temp_result;
				temp_result = (Result_list) malloc(sizeof(struct Result));
				temp_result->list = temp;
				temp_result->next = NULL;

				if(results == NULL) {
					results = temp_result;
					cursor = temp_result;
				} else {
					cursor->next = temp_result;
					cursor = cursor->next;
				}
			}
			previous_temp_cursor = previous_results->next;
			free(previous_results);
			previous_results = previous_temp_cursor;			
		}
		return results;
	}
} 


Me resulto muy interesante de hacer el tener que generar una lista de resultados en cada iteración recursiva que va hilvanando las listas finales.

Sin embargo había otro problema, cuando se van generando cada una de las combinaciones se mantienen en memoria antes de ser computados la cantidad de símbolos que contienen. Bueno con parámetros lo suficientemente grandes, me quedaba sin memoria! y empezaba a tocar el swap (a pesar de que mi maquina de desarrollo tiene 16 gigas de ram). Por lo que tuve que buscar otra solución que fue la solución final.

Solución final en C

En la versión final se me ocurrió simplemente contar la cantidad de símbolos luego de generar la lista. Como las listas se van generando recursivamente me pareció una solución bastante elegante.



void generar_contar_combinaciones(int depth, int size, List lista, int arr[MAX_ALPHABET_SIZE]) {
	int x, c;	
	List cursor_ultimo = lista;

	if(cursor_ultimo != NULL) {
		while(cursor_ultimo->next != NULL) cursor_ultimo = cursor_ultimo->next;
	}

	if(depth == 0) {
		for(x=0; x<size; x++) {
			List temp;
			temp = (List) malloc(sizeof(struct Node));
			temp->value = x;
			temp->next = NULL;

			if(cursor_ultimo == NULL) {
				c = contar_elementos_distintos(temp, size);
				arr[ c ]++;
				free(temp);
			} else {
				cursor_ultimo->next = temp;
				c = contar_elementos_distintos(lista, size);
				arr[ c ]++;
				free(temp);
				cursor_ultimo->next = NULL;
			}
		}
	} else {
		for(x=0; x<size; x++) {
			List temp;
			temp = (List) malloc(sizeof(struct Node));
			temp->value = x;
			temp->next = NULL;

			if(cursor_ultimo == NULL) {
				generar_contar_combinaciones(depth-1, size, temp, arr);
				free(temp);
			} else {
				cursor_ultimo->next = temp;
				generar_contar_combinaciones(depth-1, size, lista, arr);
				free(temp);
				cursor_ultimo->next = NULL;
			}
		}
	}
}

Resultados

Los resultados de esta exploración pueden encontrarse aquí y proximamente voy a hacer un post con la respuesta al problema.

Problema de Algoritmia Básico: Mejoras a la Solución Básica

Icono de Post February 28 2012 :: Algoritmia, c, Estructuras de datos ::

Este post continua con el problema planteado en este post. Habiendo ya analizado la complejidad de la solución básica , el siguiente paso es tratar de mejorarla proponiendo diferentes estructuras de datos para la función esta_en_lista().

Usar un array

La solución mas inmediata es cambiar la lista pasados por un array. Esta version se puede ver en solucion-array.c.

La gran diferencia entre ambas versiones es la función esta_en_lista(), que ahora nos queda:


int esta_en_lista(int value, char pasados[ MAXINT ]) {	

	if( pasados[value] == 1) return 1;
	pasados[value] = 1;
	return -1;

}

Código en solucion-array.c

Porque es una mejora?

El acceso a un array es siempre O(1). Por consiguiente el costo total del algoritmo seria O(n) * O(1), que es equivalente a O(n).

Cuales son las desventajas?

La desventaja principal es que lo que ganamos en la mejora de la complejidad de la función, lo perdemos en la complejidad espacial. Basicamente porque ahora necesitamos alocar e inicializar memoria para un array del tamaño del mayor int posible en la lista.

La otra desventaja es que es necesario inicializar el array a 0. Lo cual hace que el algoritmo de por si tenga una cota minima de O( MAXINT ), en el caso (esperable) de que la lista sea de tamaño mayor que el MAXINT posible este no es un problema ya que la porción del algoritmo que es O(n) es mayor que el MAXINT, pero si estuvieramos trabajando con un input de listas chicas, la solución anterior podria ser preferible.

En definitiva, esta solución es mas efectiva cuando el mayor int posible es pequeño y la lista es muy larga.

Usar un Binary Search Tree

Esta versión se puede ver en solucion-binary-search-tree.c. El código nos queda:


int esta_en_lista(int value, BSTree *pasados) {	
  BSTree cursor = *pasados;

  //El nodo esta vacio
  if( cursor == NULL) {
    BSTree temp = (BSTree) malloc(sizeof(struct BST_Node));
    temp->value = value;
    temp->right = NULL;
    temp->left = NULL;
    *pasados = temp;
    return -1;
  }

  //El nodo tiene el mismo valor
  if( cursor->value == value ) return 1;

  if( cursor->value < value) {
      //Fijarse en el nodo derecho
      return esta_en_lista(value, &(cursor->right));
   } else {
      //Fijarse en el nodo izquierdo
      return esta_en_lista(value, &(cursor->left));
   }
}

La ventaja inmediata de un Binary Search Tree es que la inserción y la busqueda tienen para un input promedio, complejidad O(log(n)). Esto nos mejoraría el algoritmo, llevandolo a O(n log(n)).

Sigue existiendo, sin embargo, el problema de que en el peor caso posible de una lista ordenada ascendentemente: [1,2,3,4,..]; el binary search tree se comporta exactamente igual que una lista. Esto se puede arreglar usando un Balanced Binary Search Tree, aunque es una estructura de datos un poco compleja para acordarse de su implementación y codificarla en una entrevista.

Soluciones esotéricas

Usar un bit array

Si se quiere tener una performance similar a la de un array pero con un uso menor de memoria, se puede usar un Bit Array, aunque tampoco escala bien a nivel espacio.

Usar un hashtable de binary trees

Esta es la ultima opción que codifique y se puede encontrar en solucion-hashtable-de-binary-search-trees.c. Analizar las ventajas y desventajas de esta solución se las dejo al lector.

Referencias

Foundations of Computer Science: C Edition; Aho, Ullman; W. H. Freeman; 1994

Introduction to Algorithms, 2nd Edition; Cormen, Leierson, Rivest, Stein; MIT Press; 2009

Autor: Emiliano Martínez Luque.

Comments Off on Problema de Algoritmia Básico: Mejoras a la Solución Básica

Problema de Algoritmia Básico: Complejidad Algorítmica

Icono de Post February 28 2012 :: Algoritmia, c ::

Bueno, este post continua con el problema del post anterior, lo que voy a hacer en este post es analizar la Complejidad del algoritmo.

Codigo con anotaciones

Para el análisis voy a hacer algunas anotaciones sobre el código, básicamente con comentarios sobre el mismo para dividirlo en diferentes partes. El código que se va a usar en el post es el siguiente:



int esta_en_lista(int value, List *pasados) {

/* B0 */
	List rList, prev;

	rList = *pasados;
	prev = NULL;

	if(rList == NULL) {
/* /B0 */
/* B01	*/
		//Agregarlo antes
		List temp;
		temp = (List) malloc(sizeof(struct Node));
		temp->value = value;
		temp->next = *pasados;
		*pasados = temp;
		return -1;
/* /B01 */
	}

/* B1 */
	while(rList != NULL) {
/* /B1 */

/* B11 */
		if(rList->value == value) return 1;
/* /B11 */
/* B12 */
		if(rList->value > value) {
/* /B12 */
/* B121 */
			List temp;
			temp = (List) malloc(sizeof(struct Node));
			temp->value = value;
			temp->next = rList;
			if(prev==NULL) {
				*pasados = temp;
			} else {
				prev->next = temp;
			}
			return -1;
/* /B121 */
		}
/* B13 */
		prev = rList;
		rList = rList->next;
/* /B13 */
	}
/* B3 */
	//Agregarlo al final
	List temp;
	temp = (List) malloc(sizeof(struct Node));
	temp->value = value;
	temp->next = NULL;
	prev->next = temp;
	return -1;
/* B3 */
}

List * sacar_duplicados(List *lista, List *pasados) {
	//Caso Base
/* A0 */
	if( (*lista) == NULL) {
/* /A0 */
/* A01 */
		return lista;
/* /A01 */
	} else {
	//PASO INDUCTIVO
/* A1 */
		if( esta_en_lista( (*lista)->value, pasados) > 0 ) {
/* /A1 */
/* A10 */
			List next = (*lista)->next;
			free( (*lista) );
			return sacar_duplicados( &(next), pasados);
/* /A10 */
		} else {
/* A11 */
			(*lista)->next = *( sacar_duplicados( &((*lista)->next), pasados) );
			return lista;
/* /A11 */
		}
	}
}


Se ve que estan separadas en el codigo las secciones que se ejecutan independientemente. Una aclaración, jamas en la practica se hace una análisis de complejidad con el nivel de detalle que tiene este post y dudo fuertemente que fuera lo que buscaba el entrevistador (la parte intuitiva de cada caso, alcanzaria), pero ya que estoy haciendo un post y considerando que hay pocos ejemplos en castellano, lo vamos a hacer relativamente riguroso y pedagógico.

Mejores Casos

Lista vacia

Este caso es trivial, pero de todas maneras es importante marcarlo. Se llama a la función sacar_duplicados() con lista igual a NULL, se llega al caso base y se retorna NULL. No hay mucho que analizar.

Lista con todos el mismo elemento

Ej: [1,1,1,1,1,1,1,1,1 …] .

Vamos a pensarlo intuitivamente:

Básicamente lo que pasa en este caso es que en el primer llamado a sacar_duplicados(), pasados esta vacio por consiguiente en la llamada a esta_en_lista() se agrega el elemento a pasados y se devuelve -1.

Ahora, lo interesante es que en todas las sucesivas llamadas, cada vez que se llame a esta_en_lista() esta función va a iterar por pasados, encontrarlo en el primer intento y devolver 1.

Es evidente que salvo para el primer elemento de la lista, para todos los demas el código va a tardar exactamente lo mismo (O sea que las llamadas a esta funcion son O(1) ). Como la función sacar_duplicados() pasa recursivamente por cada uno de los n elementos de la lista, la complejidad en este caso para todo el algoritmo es O(n).

Pensemoslo más detalladamente. En la primer llamada se ejecutan: A0, A1, A11, B0, B01 y la llamada recursiva al siguiente elemento. En cada una de las siguientes llamadas se ejecuta: A0 + A1 + A10 + B0 + B1 + B11 y la llamada recursiva al próximo elemento. En la ultima llamada, la que se hace cuando ya se recorrio toda la lista y se pasa el ultimo nodo que es NULL se ejecutan: A0 + A01 .

Podemos establecer una relación de recurrencia para analizar el algoritmo pensando en cuantas veces se ejecuta cada una de las partes. Llamemos T(n+1) a la ultima llamada, T(n) a la ultima llamado con un elemento, T(n-1) a la anterior, hasta llegar a T(1) la primer llamada. Nos queda entonces, la siguiente ecuación:

T(n+1) = A0 + A01 + T(n)
T(n) = A0 + A1 + A10 + B0 + B1 + B11 + T(n-1)
T(n-1) = A0 + A1 + A10 + B0 + B1 + B11 + T(n-2)
...
T(2) = A0 + A1 + A10 + B0 + B1 + B11 + T(1)
T(1) =  A0 + A1 + A11 + B0 + B01

La relación de recurrencia es la que va de T(n) a T(2) ya que esta es la parte recurrente, por otro lado, se podría haber planteado todo el problema solo pensando en esto, ya que es aquí donde se hace la parte del procesamiento que crece al crecer el tamaño de la lista y como tal es la que afecta la complejidad del algoritmo.

Es evidente que de 2 a n hay n-1 llamados, entonces la relacion de recurrencia nos queda resuelta como: (n-1)(A0 + A1 + A10 + B0 + B1 + B11) . El total de toda la ecuación ( sumando T(1) y T(n+1) y llamandola f(n) ) es: f(n) = (n-1)(A0 + A1 + A10 + B0 + B1 + B11) + ( A0 + A01) + (A0 + A1 + A11 + B0 + B01) o f(n) = (n+1)(A0)+(n)(A1+B0)+(n-1)(A10+B1+B11)+A01+A11+B01 .

Es evidente que esta función crece linealmente, o sea que en este caso es O(n).

Lista ordenada descendentemente

Ej: [n, n-1, n-2, n-3, …] .

Este caso es exactamente igual al anterior con la diferencia en que en cada llamado a esta_en_lista() se encuentra que el elemento nuevo es menor a todos los elementos anteriores de pasados en la primera iteración del while y se lo agrega a la misma. De nuevo, las llamadas a esta función son O(1) y el total es O(n)

Peor Caso

Lista ordenada ascendentemente

Ej: [1,2,3,4,5,6 …]

Bueno, este caso es bastante mas interesante. Intuitivamente, en este caso en cada llamado a esta_en_lista(), el while itera por cada uno de los elementos anteriores que son menores que el nuevo elemento, hasta que se termina el while y se agrega el nuevo elemento. Claramente en cada nueva llamada a la función el numero de iteraciones crece en 1. Intuitivamente podemos pensaer que como el numero de elementos de pasados es función del tamaño de la lista, esta parte es O(n) y como en sacar_duplicados() se itera por cada uno de los elementos de la lista el algoritmo total es O(n^2).

Analizemoslo más en profundidad y hagamos la recurrencia, los casos para el T(1) y T(n+1) son iguales. Luego para cada llamado en un elemento en posicion j de la lista se ejecuta (en orden): A0 + A1 + B0 + (j-1)(B1 + B11 + B12 + B13) + B3 + A10 ( que incluye la llamada recursiva al proximo elemento). La parte a ver y que es la que cambia radicalmente en relación con los casos anteriores es (j-1)(B1 + B11 + B12 + B13), que es la porcion del codigo donde se itera por cada elemento de pasados que preexiste al elemento actual. Esta parte ya no es constante para cada llamada a esta_en_lista(), efectivamente esta parte crece (linealmente) con cada nuevo llamado y luego de un suficiente numero de llamados es donde se concentrara la mayor parte de las operaciones de la función y de todo el algoritmo. Es fácil ver que para un j lo suficientemente grande, el resto de las operaciones se vuelve despreciable en relación al tiempo que se pasa en esta parte. Por ejemplo supongamos que j=1000, nos queda: A0 + A1 + B0 + (999)(B1 + B11 + B12 + B13) + B3 + A10 .

Hagamos la relación de recurrencia:

T(n+1) = A0 + A01 + T(n)
T(n) = A0 + A1 + A10 + B0 + B3 + (n-1)(B1 + B11 + B12 + B13) + T(n-1)
T(n-1) = A0 + A1 + A10 + B0 + B3 + (n-2)(B1 + B11 + B12 + B13) + T(n-2)
...
T(2) = A0 + A1 + A10 + B0 + B3 + (1)(B1 + B11 + B12 + B13) + T(1)
T(1) =  A0 + A1 + A11 + B0 + B01

Centremonos en la parte recurrente, llamemos k a (B1 + B11 + B12 + B13), l a (A0 + A1 + A10 + B0 + B3) y z a T(1) nos queda:

T(n) = l + (n-1)k + T(n-1)
T(n-1) = l + (n-2)k + T(n-2)
...
T(2) = l + (1)k + z 

Para poder pensar la recurrencia vamos a ir asignando los valores en cada paso y tratar de ver si existe algún patrón, intentemoslo:

T(2) = l + k + z
Total = l + k + z

T(3) = l + 2k + T(2)
Total = 2l + (1 + 2)k + z


T(4) = l + 3k + T(3)
Total = 3l + (1 + 2 + 3)k + z

T(5) = l + 4k + T(3)
Total = 4l + (1 + 2 + 3 + 4)k + z

...

T(n) = (n-1)l + (1 + 2 + 3 + 4 + ... + n-1)k + z

La parte del k es simplememnte una sumatoria hasta (n-1), existe una formula bastante básica para la misma que se puede encontrar acá y existe una anecdota muy simpática sobre Gauss sobre la misma. Aplicando equivalencias de sumatoria nos queda para toda esa parte (n * (n-1))/2 = (n^2 – n)/2 = (n²)/2 – n/2.

Veamos entonces como nos queda la recurrencia total (llamando w a T(n+1)): f(n) = (n-1)l + ((n^2)/2)k – (n/2)k + z + w . Esta función es cuadratica y el algoritmo total nos queda O(n^2).

Caso Medio

Pensar sobre el caso medio es todavia mas interesante, porque antes que nada tendriamos que definir exactamente que significa el caso medio y este seria el momento en el que le preguntariamos al entrevistador sobre más precisiones para poder entender el problema. Las preguntas básicas serian:

  • El numero de int posibles es finito?
  • Cuan ordenada esta la lista?
  • Cual es la probabilidad de que que se repitan elementos?
  • Es una distribución uniforme?

Intuitivamente, cuando pense el problema originalmente, mi idea era considerando:

1. si la lista no es ordenada se puede dar que en la primera aparicion de un elemento este sea mayor a x elementos que vengan despues que fueran menores en este elemento no se tendria que iterar por todos los anteriores sino solamente por una porción de estos. Ej:

[5,1,6,3,4,2,7]

Iteraciones para cada elemento y como queda pasados:
5 : 0
[5]

1 : 0
[1,5]

6 : 2
[1,5,6]

3 : 1
[1,3,5,6]

4 : 2
[1,3,4,5,6]

2 : 1
[1,2,3,4,5,6]

7 : 6
[1,2,3,4,5,6,7]

Se ven claramente dos cosas, el numero de iteraciones no coincide directamente ni con la posición n del elemento en lista, ni con su orden dentro de pasados; lo otro es que cuando un elemento es mayor a todos los elementos anteriores, si o si, se debe iterar por todos los elementos n-1 de pasados y ahí si, el numero de iteraciones coincide con la posición n del elemento en lista (menos 1).

2. Cuando un elemento se repita, independientemente del factor de ordenamiento de la lista, solo debe iterar hasta encontrarse a si mismo dentro de la lista.

Considerando estas 2 cosas habia pensado ingenuamente en considerar arbitrariamente una constante k, llamemosla factor de atenuación por ordenamiento y otra constante l llamemosla factor de atenuación por duplicados; que combinadas darian una constante c que se podria considerar como afectando la funcion anterior quedando el algoritmo dentro de alguna funcion del tipo f(n): c * n^2; 0 < c < 1 o sea terminando en O(n^2). Especificamente porque quedaria en una funcion cuadratica, porque todos los casos en que un elemento sea mayor a todos los anteriores, si o si, la función esta_en_lista() es lineal.

El problema es que cuando me puse a pensar un poco mas sobre el problema para escribir el post, me surgieron algunas preguntas y honestamente es un problema mucho más complicado de lo que pensaba. Voy a escribir un cuarto post con algunas de las conclusiones a las que llegue, incluyendo la muy sorprendente de que si el número de ints posibles es finito y el tamaño de la lista es mayor a este numero, la función puede ser expresada como una función lineal.

Pero bueno, siendo estrictos una función lineal esta acotada por una función cuadratica y por consiguiente sigue siendo O(n^2).

Referencias

Foundations of Computer Science: C Edition; Aho, Ullman; W. H. Freeman; 1994

Problema de Algoritmia Básico: Primera Solución

Icono de Post February 28 2012 :: Algoritmia, c, Estructuras de datos, Lógica ::

Este es una serie de posts sobre un ejemplo básico de analisis de algoritmos. Va a constar de 3 partes: Descripción del problema y solución básica, Análisis de complejidad de la solución básica y Posibles mejoras. El codigo de estos posts se puede encontrar aquí .

Hace unos días estuve hablando con un ex-compañero de la facultad sobre una entrevista de trabajo que había tenido en una empresa muy grande de estados unidos. Me comento muy rapidamente y sin muchos detalles sobre el problema que le habían presentado y me resultó interesante como un buen ejercicio de Algoritmia básica.

Descripción del problema

El problema consiste en dado una lista de numeros enteros, remover los duplicados de la lista. Asumí al hacer el codigo, que interesa mantener la estructura de la lista ( O sea dado una lista: [5,3,4,5,3,2]; encontrar la misma lista pero con los duplicados removidos: [5,3,4,2] ).

Obviamente el problema no es solamente solucionar este problema, sino razonar sobre la corrección y eficiencia de la solución encontrada y ademas codificarlo en algun lenguaje ( especificamente C ).

Algunos pasos previos

Antes de comenzar a trabajar en el problema, me parecio conveniente hacer algunas funciones para poder testear y probar el codigo.

Struct de la lista

La estructura de datos que se va a utilizar en el ejercicio


typedef struct Node *List;
struct Node {
  int value;
  struct Node *next;
};

Función para mostrar la lista

Una función para mostrar la lista por pantalla.


void mostrar_lista(List lista) {
	//Este primer paso es para el pretty print de la lista
	if(lista != NULL) {
		printf("%d", lista->value);
		lista = lista->next;
	}
	while(lista != NULL) {
		printf("-%d", lista->value);
		lista = lista->next;
	}
}

Función para cargar una lista de prueba

Una función que dado un string con una lista de enteros separados por comas, lo parsea a un linked list.

Esta es una versión mejorada sobre la que hice originalmente, esta versión es forgiving con aquellos caracteres que pueden haber sido mal tipeados por el usuario o por input malintencionado, tomando solo los caracteres que representan dígitos ( el string: “1,a2,3b,d4d,xxxx,5,,,,6,a8b0,10” se tranforma en la lista: [1,2,3,4,5,6,80,10] ). La versión original era un poco más simple y no anticipaba malas entradas.


List crear_lista(List lista, char str[2000]) {
	int i=0;
	List rList = lista;

	//Es forgiving, solo lee los digitos y descarta lo demas.
	while( str[i] != '' ) {
		while( str[i] != '' && (str[i] < '0' || str[i] > '9') ) i++;
		if(str[i] == '') break;

		int pre = 0;
	    
		//Calcular el int
		while( str[i] != ',' && str[i] != '' ) {
			if(!(str[i] < '0' || str[i] > '9')) { 
				pre = pre * 10;
				pre = pre + (str[i] - '0');
			} 
			i++; 
		}
	    
		//Crear el nodo nuevo    
		List temp;
		temp = (List) malloc(sizeof(struct Node));
		temp->value = pre;
		temp->next = NULL;

		//Primera iteracion
		if(lista==NULL) {
			//Agregar nodo a lista
			lista = temp;
			//Lista de retorno
			rList = lista;
		} else {
			//Agregar nodo, mover cursor.
			lista->next = temp;
			lista = lista->next;
		} 
		i++;
	}
	return rList;
}

Ejemplo de uso:


	char str[2000] = "";
	scanf("%s", str);
	lista = crear_lista(lista, str);
	printf("La lista es: n");
	mostrar_lista(lista);

Solución básica

La solución básica consiste de 2 funciones. Una función int esta_en_lista(int value, List *pasados) que dado un entero value y una lista pasados verifica si el valor existe en la lista. Si ya existe devuelve 1, si no, devuelve -1 (podría haber usado constantes TRUE y FALSE como macros y el codigo seria mas bonito) y agrega este valor a la lista pasados en orden ascendente. La lista pasados se pasa por referencia ( efectivamente es un puntero a puntero ) dado que como side effect de la función se modifica esta lista, agregandosele ordenadamente cualquier valor que no pre-existe. O sea, en sucesivas llamadas a la funcion se va creando una lista ordenada de valores. La otra función List * sacar_duplicados(List *lista, List *pasados), recibe 2 punteros a listas, la lista de enteros de la que queremos sacar duplicados y una lista donde se guardan los valores que ya existen en la lista. Esta es la función que devuelve la primer lista con los valores removidos. Esta pensado para ser llamada con pasados siendo una lista vacia.


//Version basica a ser mejorada
//Se pasa un puntero al puntero de lista para hacer los side effects
int esta_en_lista(int value, List *pasados) {

	List rList, prev;

	rList = *pasados;
	prev = NULL;

	if(rList == NULL) {
		//Agregarlo antes
		List temp;
		temp = (List) malloc(sizeof(struct Node));
		temp->value = value;
		temp->next = *pasados;
		*pasados = temp;
		return -1;
	}

	while(rList != NULL) {
		if(rList->value == value) return 1;
		if(rList->value > value) {
			List temp;
			temp = (List) malloc(sizeof(struct Node));
			temp->value = value;
			temp->next = rList;
			if(prev==NULL) {
				*pasados = temp;
			} else {
				prev->next = temp;
			}
			return -1;
		}
		prev = rList;
		rList = rList->next;
	}
	//Agregarlo al final
	List temp;
	temp = (List) malloc(sizeof(struct Node));
	temp->value = value;
	temp->next = NULL;
	prev->next = temp;
	return -1;
}



//ESTA ES LA FUNCION QUE IMPORTA EN UN PRIMER PASO
//EN UN SEGUNDO PASO LO QUE IMPORTA ES MEJORAR LA FUNCION ANTERIOR
List * sacar_duplicados(List *lista, List *pasados) {
	//Caso Base
	if( (*lista) == NULL) {
		return lista;
	} else {
	//PASO INDUCTIVO
		if( esta_en_lista( (*lista)->value, pasados) > 0 ) {
			List next = (*lista)->next;
			free( (*lista) );
			return sacar_duplicados( &(next), pasados);
		} else {
			(*lista)->next = *( sacar_duplicados( &((*lista)->next), pasados) );
			return lista;
		}
	}
}

Se puede testear el programa compilando y corriendo solucion-basica.c .

Razonando sobre la corrección del algoritmo

Primera Parte

Lo primero que habria que considerar es si la función esta_en_lista() es correcta. No voy a hacer un análisis muy formal, pero supongamos los posibles casos:

  • pasados es una lista vacia: Se ejecuta el primer if: if(rList == NULL) , se crea un nodo nuevo que se agrega a pasados y se devuelve -1.
  • pasados no es una lista vacia y el valor existe en la lista:, se ejecuta el primer if y devuelve falso, se ejecuta la sentencia: while(rList != NULL) , se itera por todos los nodos hasta que se encuentra el nodo en el que pre-existe el valor, se llega a la condición: if(rList->value == value) y se devuelve 1.
  • pasados no es una lista vacia, el valor no existe en la lista y es menor a cualquier valor de la lista: se llega al while, en la primera iteración se llega a la condición: if(rList->value > value), se crea el nuevo nodo, se le agrega el resto de la lista como nodo hijo y se evalua la condición: if(prev==NULL), como prev fue inicializado a NULL y es la primera iteración, se asigna este nuevo nodo a pasados y se devuelve -1.
  • pasados no es una lista vacia, el valor no existe en la lista y esta entre medio de 2 valores de la lista: se llega al while, se itera por los nodos hasta que se llega a la condición: if(rList->value > value), se crea el nuevo nodo, se le agrega el resto de la lista como nodo hijo y se evalua la condición: if(prev==NULL), como prev esta apuntando al nodo pàdre del actual, se asigna este nuevo nodo como hijo de prev y se devuelve -1.
  • pasados no es una lista vacia, el valor no existe en la lista y es mayor a cualquier valor de la lista: se llega al while, se itera por los nodos hasta que se recorre toda la lista y se sale del while, se crea el nuevo nodo, se le agrega null como nodo hijo y se evalua la condición, se asigna este nuevo nodo como hijo de prev (que en este momento es el ultimo nodo de la lista) y se devuelve -1.

Razonar sobre la corrección del valor que devuelve (1 o -1) es trivial. Asi como razonar que no agrega un valor que ya existe.

Lo que es importante ver es que si se llama a la función con una lista ordenada y un valor que no existe en la misma, por los 3 ultimos casos (y el primero siendo estrictos, ya que una lista vacia es una lista ordenada), a esta lista se le agrega el valor en orden.

Segunda parte

Asumiendo que la función anterior es correcta y que la función esta_en_lista() devuelve un valor correcto y asi mismo que en sucesivas llamadas a la lista pasados se guardan en esta todos los elementos anteriores, podemos razonar sobre la función sacar_duplicados() que es la que nos importa.

La función es recursiva, por lo que para pensar sobre su corrección alcanza con hacer induccion para demostrar que la función retorna la misma lista sin duplicados. Voy a hacer un esquema del razonamiento que nos llevaria a una demostracion.

Paso base

El paso base es que la lista esta vacia. En ese caso se retorna una lista vacia, como una lista vacia es una lista sin duplicados, no hay nada más que demostrar. El codigo que soporta este caso es:


	//Caso Base
	if( (*lista) == NULL) {
		return lista;
	} 

Paso Inductivo

Ahora tenemos que demostrar que si llamamos a la funcion con una lista con n elementos sin duplicados entonces llamando a la función con la lista con n+1 elementos tambien se retorna una lista sin duplicados.

Para razonar sobre esto alcanza pensar que en el caso en que se llama a la función con (n+1) elementos, se llega a la siguiente porción de codigo:


		if( esta_en_lista( (*lista)->value, pasados) > 0 ) {
			List next = (*lista)->next;
			free( (*lista) );
			return sacar_duplicados( &(next), pasados);
		} else {
			(*lista)->next = *( sacar_duplicados( &((*lista)->next), pasados) );
			return lista;
		}

Analizemos los dos casos:

  • Si el valor (n+1) ya existe en la lista, se devuelve la lista hasta n. Como por la hipótesis inductiva, la lista hasta n es una lista sin duplicados y se devuelve esta, la lista que se devuelve en la llamado con n+1 elementos es así mismo una lista sin duplicados.
  • Si el valor (n+1) no existe en la lista, se devuelve la lista hasta n mas el elemento n+1. Como por la hipótesis inductiva, la lista hasta n es una lista sin duplicados y por asunción de caso el elemento nuevo no existe en la lista hasta n, la lista que se devuelve en la llamado con n+1 elementos es una lista sin duplicados.

Por exhaustación de casos, se comprueba que se cumple el paso inductivo.

Algunas observaciones sobre el algoritmo

El proximo paso y va ser el siguiente post es analizar la complejidad del algoritmo y despues buscar formas de mejorarlo. Como observaciones basicas se puede ver que llamando a la función sacar_duplicados() con una lista con n elementos, se itera (recursivamente) por cada uno de los n elementos de la lista, por otro lado la lista pasados va creciendo con cada llamado a esta_en_lista() que tiene un elemento nuevo y en todo caso de que este elemento sea mayor a cualquiera de los j elementos anteriores de pasados, esta_en_lista() itera por cada uno de los j elementos de pasados. Es pensando en estos temas que vamos a encarar el siguiente post.

Referencias

Foundations of Computer Science: C Edition; Aho, Ullman; W. H. Freeman; 1994

Autor: Emiliano Martínez Luque.

Comments Off on Problema de Algoritmia Básico: Primera Solución

Usando linked lists para trabajar con polinomios

Icono de Post August 13 2009 :: c, Estructuras de datos ::

Uno de las cosas fascinantes del Mathematica y ( ahora del wolfram alpha ) es como resuelve y además simplifica expresiones matemáticas (por ejemplo: derivando funciones y dando cada uno de los pasos). Se usa de esta manera la computadora no para hacer cálculos numéricos ( que arrojen como resultado un número ) sino para computación simbólica ( cálculos que arrojan como resultado una expresión matemática ).

La pregunta es como una maquina que guarda resultados en memoria en forma de expresiones binarias ( esencialmente números, aunque pueden representar otra cosa ) para hacer esto. La respuesta es que la representación de formulas matemáticas puede ser realizada a través de diferentes estructuras de datos y es manipulando estas estructuras que se pueden realizar estos cómputos.

Como un ejemplo básico de esto acá va un ejemplo sacado de C & Data Structures sobre como se pueden hacer operaciones sobre polinomios.

Una estructura de datos para guardar un polinomio y las funciones para llenarlo y mostrarlo.



# include <stdio.h>

# include <stdlib.h>

struct pnode

   {

      int exp;

      int coeff;

      struct pnode *link;

   };

struct pnode *insert(struct pnode *p, int e,int c) {

   struct pnode *temp;

   temp = p;

   if(p==NULL) {

      p=(struct pnode *)malloc(sizeof(struct pnode));

      if(p==NULL) {

         printf("Error de alocación de memorian");

         exit(0);

      }

      p-> exp = e;

      p->coeff = c;

      p-> link = NULL;

   } else {

        //Si tienen el mismo exponente se suman los coeficientes en vez de agregar un nodo.

       if(p->exp == e) {

            p->coeff = p->coeff + c;

       } else {

            p->link = insert(p->link, e, c);

       }

   }

   return (p);

}



void printpoly ( struct pnode *p ) {

      printf("The polynomial isn");

      while (p!= NULL) {

        if(p->exp != 1) {

            printf("%dx^%dt", p->coeff, p-> exp);

        } else {

            printf("%dxt", p->coeff);

        }

        p = p-> link;

      }

      printf("n");

}

Nota: en la función de insertar si un nuevo termino tiene el mismo exponente que uno anterior se lo suma en vez de agregarlo. Esto es una de las modificaciones al código del libro.

Ordenando el polinomio



/* a function to sort a list */

struct pnode *sortpoly(struct pnode *p)

{

   struct pnode *temp1,*temp2,*max,*prev,*q;

   q = NULL;

   while(p != NULL) {

      prev = NULL;

      max = temp1 = p;

      temp2 = p -> link;

      while ( temp2 != NULL ) {

         if(max -> exp < temp2 -> exp) {

            max = temp2;

            prev = temp1;

         }

         temp1 = temp2;

         temp2 = temp2-> link;

      }

      if(prev == NULL) {

            p = max -> link;

       }  else {

            prev -> link = max -> link;

       }

       max -> link = NULL;

        if( q == NULL) {

            q = max;

        } else {

            temp1 = q;

            while( temp1 -> link != NULL) {

                temp1 = temp1 -> link;

            }

            temp1 -> link = max;

        }

    }

    return (q);

}

Una Función para sumar 2 polinomios



/* A function to add two polynomials */

   struct pnode *polyadd(struct pnode *p, struct pnode *q)

   {

      struct pnode *r = NULL;

      int e;

      int c;

      while((p!=NULL) && (q != NULL)) {

            //p mayor exponente que q

            if(p->exp > q->exp) {

                r = insert(r,p->exp,p->coeff);

                p = p->link;



            //q mayor exponenete que p

            } else if(p->exp < q->exp) {

                r = insert(r,q->exp,q->coeff);

                q = q->link;



            //mismo exponente

            } else {

                c = p->coeff + q->coeff;

                e = q->exp;

                r = insert( r, e,c);

                p = p->link;

                q = q->link;

            }

      }

   //Lo que quede de ambos polinomios

    while(p != NULL) {

        r = insert( r, p->exp,p->coeff);

        p = p->link;

    }

    while(q!=NULL) {

        r = insert( r, q->exp,q->coeff);

        q = q->link;

    }

   return(r);

}

Una función para multiplicar polinomios.

Teniendo una función insertar que sume los términos del mismo exponente y una función de suma es muy fácil agregar una función que multiplique polinomios.



struct pnode *polymult(struct pnode *p, struct pnode *q) {

    struct pnode *r, *r2, *temp;

    r = NULL;

    temp = q;

    while(p != NULL) {

        temp = q;

        while(temp != NULL) {

            r = insert(r, (temp->exp + p->exp), (temp->coeff * p->coeff));

            temp = temp->link;

        }

        p = p->link;

    }

    return r;

}

Probando el programa



int main()

   {

         int e;

         int n,i, c;

         struct pnode *poly1 = NULL ;

         struct pnode *poly2=NULL;

         struct pnode *result;

         printf("Enter the terms in the polynomial1 n");

         scanf("%d",&n);

         i=1;

         while ( n-- > 0 )

         {

      printf( "Enter the exponent and coefficient of the term number %dn",n);

               scanf("%d %d",&e,&c);

               poly1 = insert ( poly1,e,c);

         }

        printf("Enter the terms in the polynomial2 n");

         scanf("%d",&n);

         i=1;

         while ( n-- > 0 )

         {

      printf( "Enter the exponent and coefficient of the term number %dn",n);

               scanf("%d %d",&e,&c);

               poly2 = insert ( poly2,e,c);

         }

         poly1 = sortpoly(poly1);

         poly2 = sortpoly(poly2);

         printf("The polynomial 1 isn");

         printpoly ( poly1 );

         printf("The polynomial 2 isn");

         printpoly ( poly2 );

         result = polyadd(poly1,poly2);

         printf("The result of addition isn");

         printpoly ( result );

         result = polymult(poly1, poly2);

         printf("The result of multiplication isn");

         printpoly ( result );

         return 0;

   }

Resultados


Enter the terms in the polynomial1 
5
Enter the exponent and coefficient of the term number 4
5 5
Enter the exponent and coefficient of the term number 3
4 4
Enter the exponent and coefficient of the term number 2
3 3
Enter the exponent and coefficient of the term number 1
2 2
Enter the exponent and coefficient of the term number 0
1 1
Enter the terms in the polynomial2 
2
Enter the exponent and coefficient of the term number 1
2 2
Enter the exponent and coefficient of the term number 0
1 1
sdfsdfThe polynomial 1 is
The polynomial is
5x^5	4x^4	3x^3	2x^2	1x	
The polynomial 2 is
The polynomial is
2x^2	1x	
The result of addition is
The polynomial is
5x^5	4x^4	3x^3	4x^2	2x	
The result of multiplication is
The polynomial is
10x^7	13x^6	10x^5	7x^4	4x^3	1x^2	

Pensando en todo esto es interesante en como en MathML se usan estructuras arboreas ( MathML esta basado en XML ) para representar expresiones matematicas y las implicaciones que puede tener a futuro para la automatización de determinado tipo de calculos.

Referencias

C & Data Structures, Deshpande, Kakde; Charles River Media, 2004

Autor: Emiliano Martínez Luque.

Comments Off on Usando linked lists para trabajar con polinomios

Analizando los punteros de una linked list

Icono de Post August 10 2009 :: c, Estructuras de datos ::

Uno de los temas que es difícil de entender en C es el tema de los punteros y como se relacionan con las estructuras de datos. Lo difícil ( o por lo menos fue difícil para mi que venia de lenguajes dinámicos ) es entender que los punteros guardan direcciones a memoria y a que a partir de esto se puede hacer el entrelazamiento de diferentes estructuras apuntadas. Voy a usar los ejemplo de linked list de C & Data Structures para mostrar como se da esto.

Primero creamos la estructura y una función para insertar elementos en la misma:

struct node {

   int data;

   struct node *link;

};



/*Insert en versión recursiva */

struct node *insert(struct node *p, int n) {

   if(p==NULL) {

      p=(struct node *)malloc(sizeof(struct node));

      if(p==NULL) {

        printf("Allocation Errorn");

         exit(0);

      }

      p-> data = n;

      p-> link = NULL;

   } else {

       p->link = insert(p->link,n);

    }

      return(p);

}

Nota: Se hace la búsqueda del ultimo nodo para insertar en forma recursiva.

Luego hago una función para mostrar la lista:

void printnode( struct node *p) {

    printf("Memory Space: %pt Value: %d t Link: %ptn",p, p-> data, p-> link);

}



void printlist ( struct node *p ) {

      printf("The data values in the list aren");

      while (p!= NULL) {

         printnode(p);

         p = p-> link;

      }

}

La función printnode, muestra primero la dirección de memoria de la estructura, después su valor y finalmente la dirección de memoria a la que apunta, de esta manera podemos ver como se enlazan los nodos dentro de la lista.

Agregando y borrando nodos.

   /* a function which inserts a newly created node after the specified

node */

    //node_no empieza en 0 como en los arrays

   struct node * newinsert ( struct node *p, int node_no, int value ) {

      struct node *temp, * temp1;

      int i;

      temp = p;



      for(i=0; i<node_no && temp->link != NULL; i++) {

            temp = temp->link;

      }

        if(i != node_no ) {

            printf("Se pide insertar en un nodo mayor a la cantidad existente, se agrega tras el ultimo nodo existente.n");

        }

        temp1 = ( struct node * )malloc ( sizeof ( struct node ));

        if ( temp1 == NULL ) {

            printf( " Cannot allocate n");

            exit (0);

        }

        temp1->data = value;

        temp1->link = temp->link;

        printf("The newly created node to be inserted is:n");

        printnode(temp1);

        temp->link = temp1;

        return(p);

   }



   struct node * erasenode( struct node *p, int node_no) {

        struct node *temp, *prev;

        int i;

        temp = p;

        if(node_no == 0) {

            temp = p->link;

            free(p);

            p = temp;

        } else {

            i = 0;

            while(i<node_no && temp->link != NULL) {

                prev = temp;

                temp = temp->link;

                i++;

            }

            if(i != (node_no)) {

                printf("Se pide borrar un nodo que no existe.n");

                exit(0);

            }

            prev->link = temp->link;

            free(temp);

        }

        return(p);

   }

Corriéndolo en el main:

int main()

{

      int n;

      int x;

      struct node *start = NULL ;

      printf("Enter the nodes to be created n");

      scanf("%d",&n);

      while ( n-- > 0 )

      {

              printf( "Enter the data values to be placed in a noden");

         scanf("%d",&x);

         start = insert ( start,x);

      }

      printf("The created list isn");

      printlist ( start );

      start = newinsert(start, 2, 7);

      printlist (start);

      start = erasenode(start, 2);

      printlist ( start );

      return 0;

}

Resultado en pantalla:

Enter the nodes to be created 
5
Enter the data values to be placed in a node
5
Enter the data values to be placed in a node
4
Enter the data values to be placed in a node
3
Enter the data values to be placed in a node
2
Enter the data values to be placed in a node
1
The created list is
The data values in the list are
Memory Space: 0x81a5008	 Value: 5 	 Link: 0x81a5018	
Memory Space: 0x81a5018	 Value: 4 	 Link: 0x81a5028	
Memory Space: 0x81a5028	 Value: 3 	 Link: 0x81a5038	
Memory Space: 0x81a5038	 Value: 2 	 Link: 0x81a5048	
Memory Space: 0x81a5048	 Value: 1 	 Link: (nil)	
The newly created node to be inserted is:
Memory Space: 0x81a5058	 Value: 7 	 Link: 0x81a5038	
The data values in the list are
Memory Space: 0x81a5008	 Value: 5 	 Link: 0x81a5018	
Memory Space: 0x81a5018	 Value: 4 	 Link: 0x81a5028	
Memory Space: 0x81a5028	 Value: 3 	 Link: 0x81a5058	
Memory Space: 0x81a5058	 Value: 7 	 Link: 0x81a5038	
Memory Space: 0x81a5038	 Value: 2 	 Link: 0x81a5048	
Memory Space: 0x81a5048	 Value: 1 	 Link: (nil)	
The data values in the list are
Memory Space: 0x81a5008	 Value: 5 	 Link: 0x81a5018	
Memory Space: 0x81a5018	 Value: 4 	 Link: 0x81a5058	
Memory Space: 0x81a5058	 Value: 7 	 Link: 0x81a5038	
Memory Space: 0x81a5038	 Value: 2 	 Link: 0x81a5048	
Memory Space: 0x81a5048	 Value: 1 	 Link: (nil)	

Se ve aca como en la primera corrida los nodos estan ubicados uno tras otro en la memoria, pero al agregar un nuevo nodo se salta en el tercer nodo ( en este caso, Memory Space: 0x81a5028 Value: 3 Link: 0x81a5058 ) se salta al agregado ( Memory Space: 0x81a5058 Value: 7 Link: 0x81a5038 ) y del agregado al que antes existia contiguamente ( Memory Space: 0x81a5038 Value: 2 Link: 0x81a5048 ). Algo similar ocurre cuando se borra un nodo.

Revirtiendo y ordenando la lista.

/* a function to sort reverse list */

struct node *reverse(struct node *p) {

   struct node *prev, *curr;

   prev = NULL;

   curr = p;

   while (curr != NULL) {

      p = p-> link;

      curr-> link = prev;

      prev = curr;

      curr = p;

   }

   return(prev);

}



/* a function to sort a list */

struct node *sortlist(struct node *p)

{

   struct node *temp1,*temp2,*min,*prev,*q;

   q = NULL;

   while(p != NULL) {

      //Encontrar el menor de la lista p

      prev = NULL;

      min = temp1 = p;

      temp2 = p -> link;

      while ( temp2 != NULL ) {

              if(min -> data > temp2 -> data) {

                     min = temp2;

                     prev = temp1;

            }

         temp1 = temp2;

         temp2 = temp2-> link;

      }



      //Cambiar el link en la lista p del previo al menor al siguiente del menor

      if(prev == NULL) {

             p = min -> link;

      } else {

             prev -> link = min -> link;

      }



      //Agregar el menor a q

      min -> link = NULL;

      if( q == NULL){

            q = min; /* moves the node with lowest data value in the list pointed to by p to the list pointed to by q as a first node*/

       } else {

            temp1 = q;

            /* traverses the list pointed to by q to get pointer to its last node */

            while( temp1 -> link != NULL) {

                         temp1 = temp1 -> link;

            }

            temp1 -> link = min; /* moves the node with lowest data value in the list pointed to by p to the list pointed to by q at the end of list pointed by q*/

      }

   }

   return (q);

}

Vemos la impresión en pantalla de como quedan ordenados los nodos:

The sorted list is
The data values in the list are
Memory Space: 0x8c03048	 Value: 1 	 Link: 0x8c03038	
Memory Space: 0x8c03038	 Value: 2 	 Link: 0x8c03018	
Memory Space: 0x8c03018	 Value: 4 	 Link: 0x8c03008	
Memory Space: 0x8c03008	 Value: 5 	 Link: 0x8c03058	
Memory Space: 0x8c03058	 Value: 7 	 Link: (nil)	
The reversed list is
The data values in the list are
Memory Space: 0x8c03058	 Value: 7 	 Link: 0x8c03008	
Memory Space: 0x8c03008	 Value: 5 	 Link: 0x8c03018	
Memory Space: 0x8c03018	 Value: 4 	 Link: 0x8c03038	
Memory Space: 0x8c03038	 Value: 2 	 Link: 0x8c03048	
Memory Space: 0x8c03048	 Value: 1 	 Link: (nil)	

Referencias

C & Data Structures, Deshpande, Kakde; Charles River Media, 2004

Autor: Emiliano Martínez Luque.

Comments Off on Analizando los punteros de una linked list

Implementación de una Cola ( Quee ) sobre un array

Icono de Post March 13 2009 :: c, Estructuras de datos ::

Esta es una variación de la implementación en el libro C & Data Structures de una cola ( FIFO ) construida sobre un array. Me intereso esa estructura de datos por 2 cosas. Primero, el hecho de que la estructura de datos era más que el array donde se guardaban los datos, necesitándose una serie de variables asociados para usarlo y segundo el uso de la aritmética mod( n ) para moverse entre las posiciones del array.

Sin embargo, existen una serie de errores en la implementación del ejemplo y además considerando que se utilizan un conjunto de variables para definir el funcionamiento de la estructura, lo lógico sería agrupar todas estas variables dentro de un struct de C. Así que esta es mi implementación, que además fue un buen ejercicio para repasar la sintaxis de punteros y de structs en C:


#include <stdio.h>
#include <stdlib.h>
#define MAX 10 /* The maximum size of the queue */

struct ca {
	int cola[ MAX ];
	int fin;
	int frente;
	int num_elems;
};
typedef struct ca cola_array;


void insert( cola_array *carr, int value ) {
	if(carr->frente == carr->fin && carr->num_elems > 0) {
		printf("Cola Llenan");
	} else {
		carr->cola[ carr->fin ] = value;
		carr->fin = ( carr->fin + 1) % MAX;
		carr->num_elems ++;
	}
}

int delete( cola_array *carr ) {
	int temp;
	if( carr->num_elems == 0 ) {
		printf("Cola Vacian");
		return -1;
	} else {
		temp = carr->cola[ carr->frente ];
		carr->frente = ( carr->frente + 1) % MAX;
		carr->num_elems--;
		return temp;
	}
}

void listar( cola_array *carr ) {
	int x;
	if( carr->num_elems == 0 ) {
		printf("Cola Vacia");
	} else {
		printf("Numero de Elementos: %dt Frente: %dt Fin: %dn", carr->num_elems, carr->frente, carr->fin);
		for( x = 0; x < carr->num_elems; x++) {
			printf("%dt", carr->cola[ ( x + carr->frente ) % MAX  ]);		
		}
	}
	printf("n");
}

int main()
{
	int menu, value;
	cola_array carr;
	carr.fin = carr.frente = carr.num_elems = 0;
	do {
		printf("Seleccione:n");
		printf("1. Insertarn");
		printf("2. Borrarn");
		printf("3. Listarn");
		printf("4. Salirn");
		scanf("%d", &menu);
		switch(menu) {
			case 1: 
				printf("Valor a Insertar (Mayor o Igual que 0): n");
				scanf("%d", &value);
				insert( &carr, value);
			break;
			case 2: 
				value = delete( &carr );
				if(value >= 0) {
					printf("Valor borrado: %d n", value);
				}
			break;
			case 3:
				listar( &carr );
			break;
			case 4: 
				return(0);
			break;
			default:
				printf("Opcion no Valida.n");
			break;
		}
	} while( menu != 4 );
	return(0);
}

La parte que me gusto hacer es:


		for( x = 0; x < carr->num_elems; x++) {
			printf("%dt", carr->cola[ ( x + carr->frente ) % MAX  ]);		
		}

Y en particular ( x + carr->frente ) % MAX por el uso de la aritmética mod( n ) que me hizo recordar al concepto de clases de equivalencias de Algebra.

Explicación

Supongamos que la cola tiene 5 elementos (sobre un máximo de 10), pero como la cola ya se estuvo utilizando quedaron ubicados los 2 primeros en las posiciones 8 y 9 del array y los 3 últimos en las posiciones 0 a 2. El loop va a iterar a x de 0 a 4, cuando x este en 0 ( x + carr->frente ) % MAX va a equivaler a 8, cuando este en 1 a 9; pero cuando llegue a 2, ( x + carr->frente ) equivale a 10 y 10 % MAX equivale a 0.

Esto es así porque en la 0/10, 10/10, 20/10.. etc, tienen exactamente el mismo resto, 0; de la misma manera que 1/10, 11/10, 21/10.. etc tienen todos resto 1. ( La operación % establece clases de equivalencia sobre N). Lo que permite moverse circularmente sobre el array.

Autor: Emiliano Martínez Luque.

Comments Off on Implementación de una Cola ( Quee ) sobre un array

Resumen de Sintaxis de Estructuras en C

Icono de Post December 31 2008 :: Apuntes, c ::

Declaración de estructuras y acceso a miembros.


#include <string.h>
#include <stdio.h>

struct ejemplo {
	int miembro1;
	char miembro2[30];
};

main () {

	struct ejemplo var1;

	var1.miembro1 = 10;
	strcpy(var1.miembro2, "Hola que Tal.");
	printf("miembro 1: %d t miembro 2: %sn", var1.miembro1, var1.miembro2);
}

Usando Typedef para simplificar la declaración de estructuras.


#include <string.h>
#include <stdio.h>

struct ejemplo {
	int miembro1;
	char miembro2[30];
};
typedef struct ejemplo ejemp;

main () {

	ejemp var2;

	var2.miembro1 = 20;
	strcpy(var2.miembro2, "Otro string.");
	printf("miembro 1: %d t miembro 2: %sn", var2.miembro1, var2.miembro2); 

}

Se puede hacer la misma declaración del typedef de la siguiente manera:


typedef struct ejemplo {
	int miembro1;
	char miembro2[30];
} ejemp;

Punteros a estructuras


#include <string.h>
#include <stdio.h>

struct ejemplo {
	int miembro1;
	char miembro2[30];
};

main () {

	struct ejemplo var1, *var2;

	//var2 es un puntero a var1
	var2 = &var1;

	var1.miembro1 = 10;
	strcpy(var1.miembro2, "Hola que Tal.");

	printf("miembro 1: %d t miembro 2: %sn", (*var2).miembro1, (*var2).miembro2);
	//O lo que es equivalente y mas usual
	printf("miembro 1: %d t miembro 2: %sn", var2->miembro1, var2->miembro2); 

}

Nota: (*var2).miembro1 es equivalente a var2->miembro1

Asignar memoria para un puntero a estructura.


#include <string.h>
#include <stdio.h>
#include <stdlib.h>

struct ejemplo {
	int miembro1;
	char miembro2[30];
};

main () {

	struct ejemplo *var1;

	var1 = (struct ejemplo*)malloc( sizeof(struct ejemplo));

	var1->miembro1 = 10;
	strcpy(var1->miembro2, "Hola que Tal.");

	printf("miembro 1: %d t miembro 2: %sn", var1->miembro1, var1->miembro2); 		

}

Estructuras dentro de estructuras.



#include <string.h>
#include <stdio.h>
#include <stdlib.h>

struct ejemplo {
	int miembro1;
	char miembro2[30];
};

struct ejemplo2 {
	struct ejemplo ej;
}

main () {

	struct ejemplo2 var1, *var2;

	var1.ej.miembro1 = 10;
	strcpy(var1.ej.miembro2, "Hola que Tal.");

	printf("miembro 1: %d t miembro 2: %sn", var1.ej.miembro1, var1.ej.miembro2); 		

	//var2 es puntero a var1
	var2 = &var1;

	printf("miembro 1: %d t miembro 2: %sn", var2->ej.miembro1, var2->ej.miembro2); 		

}

Espacio de memoria ocupado por una estructura.


#include <string.h>
#include <stdio.h>

struct ejemplo {
	int miembro1;
	int miembro2;
	int miembro3;
	char miembro4[30];
	char miembro5[30];
};

main () {

	struct ejemplo var1;

	var1.miembro1 = 10;
	printf("Dirección de miembro 1: %u t Dirección de miembro 2: %u t Dirección de miembro 3: %un", &var1.miembro1, &var1.miembro2, &var1.miembro3);
	printf("Dirección de miembro 4: %u t Dirección de miembro 5: %un", &var1.miembro4, &var1.miembro5);
}

Resultados


Dirección de miembro 1: 3219667864 	 Dirección de miembro 2: 3219667868 	 Dirección de miembro 3: 3219667872
Dirección de miembro 4: 3219667876 	 Dirección de miembro 5: 3219667906

Explicación: Las direcciones utilizadas por una estructura son continuas. En este caso tomo una dirección especifica 3219667864 para el comienzo de la misma, la dirección del primer miembro. Como los ints en mi implementación de C en Ubuntu tienen como tamaño 4 bytes, el segundo, tercer y cuarto miembro tienen las direcciones 3219667868, 3219667872, 3219667876 respectivamente. Como el cuarto miembro es un array de chars y los chars tiene tamaño de 1 byte, el quinto miembro tiene dirección 30 bytes luego del cuarto en 3219667906.

Referencias

The C Programming Language, Segunda Edición; Kernighan, Ritchie; Prentice Hall Hispanoamerica, 1991

C & Data Structures, Deshpande, Kakde; Charles River Media, 2004

Autor: Emiliano Martínez Luque.

Comments Off on Resumen de Sintaxis de Estructuras en C

Resumen de sintaxis de Punteros en C

Icono de Post October 15 2008 :: Apuntes, c ::

Copiado directamente de K & R.


	int x = 1, y = 2, z[10];
	int *ip; /* ip es un puntero a int */

	//Se usa &var para referenciar a la dirección de la variable a la que se quiere apuntar
	ip = &x; /* ip apunta a x */

	//Se usa *puntero para referenciar el valor de la dirección a la que apunta el puntero
	y = *ip; /* y es ahora 1 ( el valor de x) */

	*ip = 0; /* x es ahora 0 */

	ip = &z[0]; /* ip ahora apunta a z[0] */

Un ejemplo de una función que toma punteros como parámetros


void swap( int *px, int *py) {
	int temp;

	temp = *px;

	*px = *py;

	*py = temp;
}

Llamando a la función y pasando los valores por referencia


	swap(&x, &y);

	printf("n");
	printf("%d %dn", x, y);
	//Imprime 1, 0

Que pasa si imprimo un puntero directamente sin usar * ?


....
test1(&x, &y);
....

void test1( int *px, int *py) {
	//Imprime 1 0
	printf("%d %dn", *px, *py);
	//Imprime la dirección de x y casteada a Int
	printf("%d %dn", px, py);
}

Arreglos, Punteros y aritmética de punteros


#include 

int main() {
	int a[10], x;
	int *pa;
	//Relleno el array a con [0-9]
	for( x=0; x<10; x++ ) {
		a[x] = x;
		printf("%dn", a[x]);
	}

	pa = &a[0];
	//pa es ahora un puntero que apunta a la primer dirección del arreglo

	printf("%d %dn", a[7], *(pa+7) );
	//Imprime 7 7.. porque luego de pa= &a[0], a[i] es igual a *(pa + i)
}

Punteros y funciones


//Toma como parámetro 2 punteros a char
int strlen( char *s, char *t) {
	while((*s++ = *t++) != '');
}
....

//Función que devuelve un apuntador a int
int *f();

Referencias

The C Programming Language, Segunda Edición; Kernighan, Ritchie; Prentice Hall Hispanoamerica, 1991

Autor: Emiliano Martínez Luque.

Comments Off on Resumen de sintaxis de Punteros en C

Macros en C

Icono de Post September 21 2008 :: c ::

Los Macros (asi como los includes) se manejan en el preprocesador de C que es un paso anterior separado de la compilación. Y permiten basicamente la definición de un termino que será remplazando siempre en el momento de la compilación por la expresión que se defina. El ejemplo típico es la definición de constantes, ej:


#define MAX_SIZE 1000

Macros con Variables

También pueden usarse Macros con variables, ej:


#include <stdio.h>
#define max(A, B) ((A) > (B) ? (A):(B))
int main() {
	int x = 20;
	int y = 10;
	int z;

	//Gets transformed to z = ((x + y) > (y - 10) ? (x + y):(y - 10));
	z = max(x + y, x - 10);
	//prints 30
	printf("%dn", z);

	//Gets transformed to z = ((++x) > (++y) ? (++x):(++y);
	z = max( ++x, ++y);
	// ++ is doubly evaluated
	//So x and y are doubly incremented
	//prints 22
	printf("%dn", z);

}

Nota: Hay que tener cuidado con los efectos secundarios que se pueden causar ver ejemplo de ++x

Cuidado con comerse los parentesis y los efectos secundarios que se pueden causar


#include <stdio.h>
#define square(x) x * x
#define square_works(x) (x) * (x);

int main() {
	int x = 20;
	int y = 10;
	int z;

	//Gets transformed to z = x + y * x + y
	//And because of precedence order is z = x + (y * x) + y
	z = square(x + y);
	//prints 230
	printf("%dn", z);

	//Gets transformed to z = (x + y) * (x + y)
	z = square_works(x + y);
	//prints 900
	printf("%dn", z);

}

Para desdefinir un Macro se puede usar undef y se puede usar ( también para includes o defines ) estructuras condicionales con if, ej:


#if SYSTEM_BITS == 64
	#define MAX_SIZE = 2^63
#elif
	#define MAX_SIZE = 2^31
#endif

Referencias

The C Programming Language, Segunda Edición; Kernighan, Ritchie; Prentice Hall Hispanoamerica, 1991