Mostrando entradas con la etiqueta programación. Mostrar todas las entradas
Mostrando entradas con la etiqueta programación. Mostrar todas las entradas

domingo, 10 de marzo de 2019

Ordenamiento mezcla natural: objetos

Como ejemplo final en esta serie, veremos cómo ordenar un objeto que nosotros definimos. Vamos usar un objeto Persona que tiene nombre y edad. El único requisito para poder ordenarlo es que podamos compararlo. Esto lo logramos implementando el método compareTo() de la interface Comparable, nuestra implementación ordenará primero por edad y luego por nombre.

Las implementaciones de Lector y Escritor son exactamente iguales a las del ejemplo anterior en donde ordenamos enteros grandes.

Para crear nuestro archivo de prueba, leemos nuestro archivo de texto con nombres y a cada nombre le asignamos una edad para crear un objeto Persona. Noten que la clase Persona también implementa la interface Serializable. Esta interface no requiere que implementemos ningún método y nos permite serializar el objeto para escribirlo y leerlo de archivos.

Espero que los ejemplos en esta serie sirvan como base por si desean ordenar otro tipo de datos.

$ javac MezclaNaturalGenerico.java MezclaNaturalEjemplo4.java
$ java MezclaNaturalEjemplo4
Error en el ordenamiento
Fusion 1
Fusion 2
...
EL ARCHIVO ESTA ORDENADO
MezclaNaturalEjemplo4.java

domingo, 3 de marzo de 2019

Ordenamiento mezcla natural: enteros grandes

Ahora veremos como ordenar enteros grandes con el algoritmo genérico de mezcla natural. Este ejemplo es muy parecido al primer ejemplo donde ordenamos cadenas binaria. Pero en lugar de usar Data[Input|Ouput]Stream ahora usamos Object[Input|Output]Stream. Esto nos permite leer y escribir objetos serializados en lugar de sólo datos primitivos.

La principal diferencia, que me tomó por sorpresa, es que el método available() no regresa 0 para indicar fin del archivo. En este caso, tenemos que capturar EOFException para detectar cuando acabamos de leer los datos.

El programa crea y ordena un archivo con 100,000 enteros.

$ javac MezclaNaturalGenerico.java MezclaNaturalEjemplo3.java
$ java MezclaNaturalEjemplo3
Error en el ordenamiento
Fusion 1
Fusion 2
...
EL ARCHIVO ESTA ORDENADO
MezclaNaturalEjemplo3.java

sábado, 2 de marzo de 2019

Ordenamiento mezcla natural: cadenas

La versión original de este algoritmo ordena cadena binarias porque fue un requerimiento del proyecto. Pero ahora que ya tenemos la versión genérica, podemos ordenar otros datos. Empecemos por ordenar cadena simples (no binarias). Nuestro archivo de entrada tiene un nombre por línea.

Noten que en el Lector tenemos que obtener la línea por adelantado para poder implementar adecuadamente hasNext().

Después de ejecutar el ejemplo, pueden corroborar el resultado abriendo el archivo nombres_copia.txt.

$ javac MezclaNaturalGenerico.java MezclaNaturalEjemplo2.java
$java MezclaNaturalEjemplo2
MezclaNaturalEjemplo2.java

domingo, 24 de febrero de 2019

Ordenamiento mezcla natural: cadenas binarias

Ahora veremos cómo usar el algoritmo genérico de mezcla natural con cadenas binarias. Tenemos que implementar las dos interfaces que permiten aislar los detalles de lectura y escritura de datos.

  public static class Lector implements MezclaNaturalGenerico.Lector<String> 
  public static class Escritor implements MezclaNaturalGenerico.Escritor<String>

La implementación de estas dos interfaces es bastante directa. Por ejemplo, el método next() ecapsula readUTF() y el método hasNext() encapsula dis.available() != 0. También comentaba que el algoritmo se inicializa con fábricas para generar Lectores y Escritores. Eso lo podemos ver en esta línea:

  new MezclaNaturalGenerico<>(Lector::new, Escritor::new);

Vale la pena explicar qué está pasando aquí. La forma extendida sería implementar la interface que está esperando el constructor.

  static class FabricaLectores implements
      Function<File, MezclaNaturalGenerico.Lector<String>> {
    @Override
    public MezclaNaturalGenerico.Lector apply(Object o) {
      return new Lector(o);
    }
  }

Y la usamos así:

  new MezclaNaturalGenerico<>(new FabricaLectores(), ...);

La parte principal de FabricaLectores es new Lector(o), lo demás sólo causa ruido. Dado que Function es una interface funcional, es decir, que sólo tiene un método abstracto, la podemos reemplazar con una lambda.

  new MezclaNaturalGenerico<>(o -> new Lector(o), ...)

Pero como la única instrucción de la lambda es un new, la podemos reemplazar por una referencia al constructor. Y así es cómo llegamos al resultado final.

new MezclaNaturalGenerico<>(Lector::new, ...);

Al ejecutar este ejemplo deben obtener lo siguiente:

$ javac MezclaNaturalGenerico.java MezclaNaturalEjemplo1.java
$ java MezclaNaturalEjemplo1
Error en el ordenamiento
Fusion 1
...
EL ARCHIVO ESTA ORDENADO
1) AARON
2) ABBEY
3) ABBIE
4) ABBY
5) ABDUL
...
MezclaNaturalEjemplo1.java

lunes, 18 de febrero de 2019

Ordenamiento mezcla natural: algoritmo genérico

Esta serie de posts está inspirado en uno de nuestros posts más populares: Ordenamiento externo: Mezcla Natural. En ese post recibimos algunas preguntas de cómo reusar el código para ordenar otro tipo de datos. El código original sólo maneja cadenas en formato binario. Por lo que decidí actualizarlo para manejar cualquier tipo de dato. En este post hablaré de los cambios necesarios y en los siguientes posts mostraré ejemplos de cómo usarlo. El algoritmo base no cambió, lo que tuve que aislar es el tipo de dato y la forma en la que lo leemos y escribimos.

Tipo de dato.

El tipo de dato lo podemos ver por las menciones del tipo String a lo largo del código original. Tenemos que reemplazar String con un tipo genérico. La única operación que necesitamos del tipo de dato es que lo podamos comparar. Eso lo podemos observar cuando invocamos el método compareTo. Esto nos lleva a cambiar la definición de la clase de:

public class MezclaNatural

a:

public class MezclaNaturalGenerico<T extends Comparable<T>>

Noten que exigimos que el tipo de datos genérico implemente la interface Comparable, la cual declara el método compareTo. Ahora podemos reemplazar String con el parámetro genérico. Por ejemplo:

    String actual = null;
    String anterior = null;

con:

    T actual = null;
    T anterior = null;

Lectura de datos.

Para leer datos estábamos usando los métodos available() y readUTF() de un DataInputStream. Además invocamos el método close() de este Stream cuando terminamos de usarlo. Vamos a reemplazar este DataInputStream con una interface que provea una funcionalidad similar. En lugar de crear una interface completamente nueva, decidí crear una interface que extiende dos interfaces de las librerías de Java:

  public interface Lector<T> extends Iterator<T>, Closeable { }

Un usuario tiene que implementar esta interface para que nuestro algoritmo no se preocupe de cómo leer datos. Observen cómo volvemos a usar el parámetro genérico en esta definición. Los métodos hasNext() y next() de la interface Iterator reemplazarán a los métodos available() y readUTF().

Un pequeño detalle de implementación es que nuestro algoritmo tiene que instanciar varios Lector(es) a lo largo de su ejecución. Por lo que un usuario debe proporcionar una fábrica de Lectores. Esto lo podemos ver en el siguiente parámetro de nuestro constructor:

    Function<File, Lector<T>> generaLector

La fábrica (Function) tomará como entrada un archivo abstracto e instanciará un Lector.

Escritura de datos.

Este caso es paralelo a la lectura de datos. Para escribir usamos el método writeUTF() de un DataOutputStream. Esto la abstraemos con la interface:

  public interface Escritor<T> extends Consumer<T>, Closeable { }

El método accept() de Consumer reemplazará al método writeUTF(). Y también necesitamos una fábrica de Escritores.

    Function<File, Escritor<T>> generaEscritor

Esto es todo por este post. Si lo comparan con el código original, verán que la lógica es la misma y la mayoría fueron reemplazos directos (por ejemplo: readUTF con next) En el siguiente post veremos cómo usarlo.

MezclaNaturalGenerico.java

domingo, 25 de octubre de 2015

Cómo aprender a resolver problemas en programación: Recomendaciones

  1. Introducción
  2. Recomendaciones

¿Cómo podemos empezar a aprender a resolver problemas de programación? En este post me gustaría analizar algunas de las estrategias propuestas en el capítulo 1 del libro Think Like a Programmer. An Introduction to Creative Problem Solving y cómo cada una de estas estrategias sí me han ayudado a mejorar mi habilidad para resolver problemas de programación.

Antes de intentar escribir código para cualquier problema de programación, es importante saber que existen algunas recomendaciones para ordenar nuestros pensamientos y posteriormente poner manos a la obra. Si organizamos nuestro entendimiento de lo que el problema está pidiendo, es mucho más fácil llegar a una primera solución, y aunque ésta sea una solución parcial o muy rudimentaria, es crucial entender que este es un muy buen primer paso y que con práctica, podremos ser capaces de saltarnos algunos pasos intermedios. Pero por ahora, lo más esencial es detenernos un poco y analizar qué se está pidiendo.

Algunas recomendaciones que he encontrado muy útiles en este primer paso de analizar un problema son las siguientes:

  1. Dividir el problema: Algunos problemas contienen muchos pasos o bien, es necesario hacer diferentes operaciones para encontrar la solución. Una buena estrategia es leer el problema más de una vez para poder identificar las diferentes partes en las que puede estar dividido. Dividir un problema nos permite hacer varias operaciones sencillas que combinadas, realizan una tarea compleja. Por ejemplo, supongamos que queremos contar el número de veces que un grupo de palabras aparece en un archivo de texto. Este problema lo podemos dividir en: leer el archivo, tokenizar el texto que contiene el archivo (es decir, desarrollar código que sea capaz de separar el texto del archivo en palabras individuales), leer palabra por palabra el texto del archivo, identificar si alguna palabra pertenece al grupo de palabras que queremos contar, llevar la cuenta de cuando una palabra de nuestro grupo aparece en el texto del archivo, repetir la cuenta para cada palabra de nuestro grupo de palabras y retornar nuestro resultado utilizando alguna estructura de datos. Si nos enfocamos en uno solo de estos pasos a la vez, resolver este problema es mucho más sencillo.
  2. Identificar las partes del problema que se nos hacen fáciles de resolver: Es posible que cuando estamos resolviendo un problema, existan ciertas partes que nos parecen más sencillas de resolver o con las que tenemos más experiencia. Esto es parecido a cuando resolvemos un examen: algunas preguntas son más sencillas que otras. En el libro Abre tu mente a los números: Cómo sobresalir en Ciencias aunque seas de Letras, aprendí que existe más de una manera de atacar las partes fáciles y difíciles de algo que queremos resolver. Volviendo al ejemplo del examen, algunas personas se sienten cómodas atacando las preguntas fáciles primero y dejando las difíciles para después. Pero otra estrategia que puede resultar exitosa de acuerdo con la autora es la siguiente: atacar lo más difícil primero y en cuanto comencemos a notar que estamos atorados, dirigir nuestra atención hacia preguntas o pasos fáciles y volver al problema difícil una vez que hayamos resuelto los fáciles. La idea detrás de esta técnica es que nuestro cerebro funciona en dos modos: en el primero se enfoca intensamente en una tarea y en el segundo procesa información de manera difusa (es decir, sin enfocarse particularmente en una idea). El modo difuso nos ayuda a seguir resolviendo el problema difícil mientras estamos resolviendo los problemas fáciles y es gracias a este modo de "pensar" que muchas veces recordamos información súbitamente o nos damos cuenta repentinamente de algún error que estábamos cometiendo.
  3. Reconocer o tratar de hacer analogías del problema a resolver: Con el paso del tiempo y con experiencia, reconocer problemas parecidos y recordar cómo los resolvimos se convierte en algo muy natural. En nuestro camino a adquirir esa experiencia es importante que podamos identificar si un problema se parece a algún otro o si podemos hacer alguna analogía. Las analogías nos ayudan a entender mejor el problema y en ocasiones nos pueden auxiliar a visualizarlo mejor. Una buena analogía es un excelente recurso que en un futuro cercano nos permite recordar más fácilmente aquéllo que aprendemos. Por ejemplo, en el problema de contar cuántas veces un grupo de palabras aparece en el texto de un archivo podemos visualizar una línea de producción en donde las palabras pasan frente a nosotros y nosotros tenemos que colocarlas en sus cajas correspondientes. Cada vez que clasificamos una de las palabras en las que estamos interesados, utilizamos un marcador para anotar cuántas palabras hay hasta el momento en cada caja tachando el valor anterior y escribiendo el nuevo valor que se incrementa en 1 unidad.
  4. Experimentar: El autor de Think Like a Programmer. An Introduction to Creative Problem Solving, deja muy claro que experimentar no significa solamente tomar piezas de código y comenzar a ver si alguna funciona de manera aleatoria. Experimentar se refiere a tratar de utilizar nuevo código de forma que parezca lógica y de acuerdo con nuestra (poca o mucha) experiencia. Supongamos que estamos tratando de generar algún tipo de gráfico y sabemos que existe una librería que puede contener la funcionalidad que estamos buscando. Para confirmar si podemos utilizar alguna función de la librería, podemos leer la documentación y hacer ejemplos simples para observar qué es lo que pasa. Si el resultado de estos experimentos es lo que nosotros necesitamos, entonces podemos utilizar esta librería para resolver nuestro problema de generar algún gráfico. Incluso en circunstancias en las que no necesitamos utilizar ninguna librería (como cuando estamos aprendiendo a utilizar un lenguaje de programación), experimentar haciendo pequeños ejemplos y observando el resultado es algo muy valioso que nos ayuda a entender realmente qué está pasando.
  5. Reducir la complejidad del problema: Imaginemos que estamos tratando de construir un programa para jugar al "gato" o "tic-tac-toe" en 3 dimensiones. Para poder resolverlo, es necesario que podamos manipular un arreglo en 3 dimensiones, sin embargo, quizá para familiarizarnos mejor con la forma de resolver este problema, es mejor enfocarnos en cómo resolveríamos el clásico juego en 2 dimensiones y posteriormente extender nuestra solución para tomar en cuenta un arreglo de 3 dimensiones.
  6. Evitar frustrarte: Esta es quizá solamente una recomendación, pero no por eso deja de ser valioso recordar que debemos respirar profundamente y no rendirnos. Si de verdad estás atorado resolviendo un problema, pide ayuda a alguien con más experiencia pero no te desesperes ni pienses que la programación no es para ti. Es muy válido pedir ayuda e incluso, es muy importante saber cuándo pedir ayuda y a quién.

Si bien estas no son las únicas estrategias para comenzar a resolver un problema, son las que más me han parecido útiles en mi camino a mejorar la manera en la que ataco los problemas de programación. Sin embargo, ninguna técnica puede reemplazar el hacer ejercicios. Es por esta razón que al terminar de leer este post, lo primero que debes hacer es resolver varios ejercicios. En el libro Think Like a Programmer el autor recomienda utilizar acertijos para practicar las recomendaciones que acabamos de analizar brevemente.

Las siguientes ligas contienen acertijos en español de varios tipos que pueden ayudarte a comenzar a utilizar estas estrategias. Trata de resolver aquéllos que no conozcas utilizando distintas formas de atacar el problema. Mientras más intentes, mejor:

http://www.parapensar.com/acertijos.html

http://www.elconfidencial.com/alma-corazon-vida/2014-07-28/10-acertijos-clasicos-que-pondran-a-prueba-tu-capacidad-logica_166413/

Si conoces otros sitios que contienen buenos acertijos, compártelos en los comentarios.

domingo, 30 de agosto de 2015

Cómo aprender a resolver problemas en programación: Introducción

  1. Cómo aprender a resolver problemas en programación: Introducción

Durante la última mitad de mis estudios en la universidad me dediqué casi exclusivamente a trabajar con hardware y dejé un poco de lado la programación. Después de graduarme descubrí que había perdido bastante práctica y decidí comenzar a resolver problemas utilizando C++. Aunque C++ es el lenguaje que utilicé en mi último proyecto, éste no requirió del uso de programación demasiado extensivo y cuando decidí regresar a utilizar la programación me sucedió algo muy curioso: era capaz de entender lo que un problema me estaba pidiendo e inclusive, era capaz de seguir la solución propuesta por algún autor o programador pero al intentar construir mi propia solución me encontré con gran dificultad para poder comenzar a escribirla.

Para tratar de solucionar esta situación realicé una búsqueda en Google y para mi sorpresa, encontré un libro que describía exactamente qué estaba pasando: Think Like a Programmer. An Introduction to Creative Problem Solving. Mi problema no era la falta de conocimiento acerca del uso del lenguaje de programación sino la falta de práctica en cómo resolver problemas.

Desarrollar una técnica para resolver problemas eficiente y elegantemente cuesta tiempo, esfuerzo y paciencia. Aunque esta es una habilidad fundamental para todo buen programador, desafortunadamente no se enseña de manera sistemática en la escuela y en muchas ocasiones, el programador comienza realmente a aprender cuando ya se encuentra trabjando en la industria. Sin embargo, resolver problemas de programación es un arte y al no contar con una forma organizada para desarrollar este arte, muchos programadores capaces e inteligentes pueden llegar a frustrarse y pensar erróneamente que no poseen las habilidades necesarias para ser buenos programadores. En esta serie de posts me gustaría hablar acerca de cómo aprender a resolver problemas con el propósito de ayudar a todos aquellos que han vivido una situación similar a la mía: la falta de práctica para resolver un problema. En especial, me gustaría discutir acerca de lo que me fue más útil después de haber comprado y estudiado el libro que acabo de mencionar.

De la misma forma en que el autor propone ejercicios después de cada capítulo, estaré dejando ejercicios que considero útiles para todo aquél que quiera aprender o mejorar su técnica de resolución de problemas. Tal y como el autor del libro lo menciona al final de cada capítulo: de nada sirve leer los conceptos si no se practican. Por lo tanto, si de verdad quieres aprender a resolver problemas entonces resuelve lo más que puedas de los ejercicios: mientras más resuelvas, mejor.

domingo, 25 de enero de 2015

Soundex

Breve implementación del algoritmo soundex. Soundex es un algoritmo fonético, es decir, se utiliza para indexar palabras de acuerdo con su pronunciación. La mayoría de estos algoritmos se desarrollaron para ser ocupados en el idioma inglés. A diferencia del español en el que prácticamente todas las palabras se escriben de la manera en la que suenan, en el inglés existen varias formas de escribir una palabra. Por ejemplo, el apellido Smyth and el apellido Smith se pronuncian exactamente de la misma forma pero su escritura es diferente. En este caso, nos vamos a enfocar en el uso de Soundex con apellidos.

Las reglas del soundex son las siguientes:

Guía de codificación de Soundex
Número Letra que representa
1 B, F, P, V
2 C, G, J, K, Q, S, X, Z
3 D, T
4 L
5 M, N
6 R

Para las letras A, E, I, O, U, H, W , Y se utiliza el cero (0) o bien, se ignoran completamente (ejemplo: Lee se codifica como L-000).

  1. Cada apellido codificado con soundex consiste en una letra seguida por tres números, por ejemplo, el apellido Smith se codifica como S-530
  2. Si el apellido tiene letras repetidas (como por ejemplo Higgins), la letra repetida se cuenta una sola vez: H-252 (H es la primera letra, la "i" se ignora, la "g" se toma en cuenta una sola vez, la segunda "i" se ignora y los dos últimos dígitos corresponden a la "n" y "s" respectivamente)
  3. En los apellidos que tienen letras que se codifican con el mismo número y se encuentran juntas, estas letras se toman en cuenta como un solo caracter. Por ejemplo, Jackson se codifica como J-250 (J, 2 para la "c", la "k" se ignora, la "s" también se ignora, la "o" se ignora, 5 para la "n" y finalmente, se completa con 0 para cumplir con la primera regla)
  4. Si el apellido tiene prefijos como Van, Con, De, Di, La, o Le, entonces el apellido se puede codificar de dos formas: tomando en cuenta el prefijo o ignorándolo. Por ejemplo, VanDeusen se puede codificar como V-532 (V, 5 para la "n", 3 para la "d", la "e" y la "u" se ignoran, 2 para la "s" y la última "n" se ignora para no romper la primera regla) o como D-250 (D, la "e" y la "u" se ignoran, 2 para la "s", la "e" se ignora, 5 para la última "n" y se completa con 0 para cumplir con la primera regla)
  5. Si una vocal se encuentra en medio de dos consonantes que se codifican con el mismo número, se toma en cuenta la consonante que se encuentra a la derecha de la vocal. Por ejemplo, Tymczak se codifica como T-522 (T, la "y" se ignora, 5 para la "m", la "c" se ignora, la "z" se ignora, la "a" se ignora y solamente se escribe 2 para la "k")
  6. Si "H" o "W" se encuentran enmedio de dos letras que se codifican con el mismo número, entonces se ignora la consonante de la derecha. Por ejemplo, Ashcraft se codifica como A-261 (A, 2 por la "s", la "h" se ignora, la "c" se ignora, 6 por la "r", 1 por la "f" y la "t" se ignora)

Para saber más acerca de Soundex, visita: The Soundex Indexing System.

La implementación es la siguiente:

sábado, 27 de diciembre de 2014

C++11 Sincronizacion de Hilos

El primer ejemplo muestra posibles errores que resultan cuando se modifica una variable compartida desde diferentes hilos sin ninguna protección. El programa laza dos hilos, los cuales ejecutan la misma función, la cual realiza 1000 incrementos unitarios a la variable compartida; idealmente el resultado debe ser 2,000.

Una posible salida de este programa es:
$ ./thread_race
305: 1375
417: 1937
419: 1208
534: 1393
619: 1037
655: 1934
688: 1805
699: 1457
Una forma de arreglar este programa es usar un mutex para proteger el acceso a esta variable. Además estamos midiendo el tiempo de ejecución.

Esta es una posible ejecución:
$ ./thread_mutex
tiempo= 1499 ms
Usar un mutex es la forma más directa de resolver este problema; sin embargo, en este caso, dado que sólo estamos compartiendo una variable entera, es posible usar una variable atómica, lo cual permite una ejecución correcta y más eficiente que usando un mutex.

Este es un ejemplo de ejecutar este código:
$ ./thread_atomic
tiempo= 560 ms
Saludos!

lunes, 22 de diciembre de 2014

C++11 Futures

Este programa es una variación del programa anterior. En el programa anterior usamos variables por referencia para obtener los resultados parciales de las sumas. En este ejemplo usamos futures, los cuales nos permiten especificar una tarea que se puede realizar de forma asíncrona, y podemos recuperar el resultado invocando el método get del future.
result= 5000000050000000
duration= 123 ms
Saludos!

domingo, 21 de diciembre de 2014

C++11 Hilos

Basados en el ejemplo anterior, ahora sumaremos los elementos usando varios hilos; veamos si eso ayuda en el tiempo de ejecución.
Este es el resultado en mi computadora que tiene 2 procesadores dual-core:
resultado= 5000000050000000
hilos=4
tiempo= 137 ms
Como podemos ver, el tiempo de ejecución no necesariamente es una cuarta parte del tiempo obtenido en la publicación anterior; esto nos permite observar que existe un costo por crear hilos, por lo cual debemos tener cuidado en balancear el número de hilos creados contra la cantidad de trabajo realizada por cada hilo.
De hecho, experimentado con el código, forcé el programa a usar sólo 2 o 3 hilos (línea 13), con 2 hilos el tiempo de ejecución fue alrededor de 150ms y con 3 hilos alrededor de 175ms, interesante!
Compilé este programa con:
g++ -std=c++11 sum_thread.cc -o sum_thread -pthread
Para poder obtener el resultado obtenido por cada hilo, pasamos una variable por referencia (línea 24), en la siguiente entrada veremos como podemos hacer esto más sencillo.
Saludos!

domingo, 7 de diciembre de 2014

Multiples ventanas con ncurses

Ncurses permite crear simples interfaces en una terminal de comandos. El siguiente código muestra como crear 4 ventanas independientes, se lanzan también 4 threads (hilos) y cada uno actualiza un contador en una de las ventanas.
Para compilar el programa:

g++ -std=c++11 10_thread.cc -lncurses -pthread



Estos los recursos que utilice para crear este programa de ejemplo:
Saludos!

sábado, 14 de septiembre de 2013

Programmando TI84 – ciclo infinito

Resulta que un programa para mi TI84 tenía un ciclo infinito, mi primer opción fue tratar de apagarla, pero no respondía. Por lo que mi segunda opción fue quitarle las pilas, pero eso fue una mala idea, porque al prenderla me apareció que la RAM se había corrompido y todos mis programas fueron borrados.

Lo único que tenía que hacer era presionar la tecla “ON”, lo cual genera un mensaje para interrumpir el programa.

Referencia:
http://www.dummies.com/how-to/content/the-while-end-command-on-the-ti84-plus.html

Saludos.

sábado, 7 de septiembre de 2013

Python no encuentra funciones en modulo

El siguiente error me pareció bastante curiosos, que decidí compartirlo. Cree un pequeño programa en Python para hacer unas pruebas con sockets. Se me ocurrió llamarle a mi archivo socket.py. De hecho al inicio sólo tenía dos líneas:

import socket
s = socket.socket( socket.AF_PACKET , socket.SOCK_RAW , socket.ntohs(0x0003))

Pero al tratar de ejecutarlo me marcaba errores del siguiente tipo:

AttributeError: 'module' object has no attribute 'AF_PACKET'

Lo que está pasando es que Python primero busca los módulos en el directorio actual, en este caso como mi archivo se llama socket.py, se importaba a si mismo, en lugar de importar el módulo de las librerías de Python.

Simplemente renombré mi archivo a mi_socket.py y me aseguré de borrar el archivo socket.pyc que se había generado en el directorio actual.

Referencia: http://stackoverflow.com/questions/13422356/socket-isnt-working-in-python

Saludos.

domingo, 9 de diciembre de 2012

Representación interna de un flotante

En este post vamos a hacer un pequeño experimento para desplegar la representación de un flotante, ya que a final de cuentas debe ser guardado como un conjunto de bits.

Primero, repasemos rápidamente uniones en C. En una unión todos sus miembros comparten la misma ubicación de memoria. Por ejemplo, en el siguiente programa, la unión contiene dos elementos, una estructura con cuatro enteros y un arreglo de cuatro enteros. Observen como inicializamos los valores de la estructura y posteriormente desplegamos esos valores usando los miembros del arreglo.

#include <stdio.h>

typedef struct {
  int byte1;
  int byte2;
  int byte3;
  int byte4;
} Mi_estructura;

typedef union {
  Mi_estructura estructura;
  int arreglo[4];
} Mi_union;

void main(void) {
  Mi_union u;
  int i;
  
  u.estructura.byte1 = 11;
  u.estructura.byte2 = 22;
  u.estructura.byte3 = 33;
  u.estructura.byte4 = 44;
  
  for(i = 0; i < 4; i++) {
    printf("%d\n",u.arreglo[i]);
  }
}

Este caso en específico puede ser útil cuando algunas partes del programa son más legibles usando los miembros de la estructura, por ejemplo al momento de asignar valores. Y otras veces es más eficiente usar un arreglo, como al querer copiar o desplegar los elementos.

Regresando al caso de un flotante. Usaremos una unión que contiene un float y un int, en la máquina que estoy usando ambos usan 4 bytes. Lo que haremos es modificar la unión usando el float y desplegar el contenido usando el int.

#include <stdio.h>

typedef union {
  int i;
  float f;
} Mi_union;

void main(void) {
  Mi_union u;
  printf("# bytes int: %zu\n",sizeof(int));
  printf("# bytes float: %zu\n",sizeof(float));
  
  u.f = 1234.5678f;
  printf("%.10f\n",u.f);
  printf("0x%x\n",u.i);
}

Podemos ver en la imagen anterior que el valor 1234.5678f es almacenado con el siguiente patrón de bits 0x449a522b. Este patrón sigue el estándar IEEE754, la siguiente página contiene una muy buena explicación.

Lo que haremos es extraer los bits de acuerdo a este formato y verificaremos que tengamos el valor 1234.5678f.

#include <stdio.h>

typedef union {
  unsigned int i;
  float f;
} Mi_union;

unsigned int extraer_bits(unsigned int valor, short inicio, 
                          short nbits);
void representacion_float(unsigned int valor);

void main(void) {
  Mi_union u;
  printf("# bytes int: %zu\n",sizeof(int));
  printf("# bytes float: %zu\n",sizeof(float));
  
  u.f = 1234.5678f;
  printf("%.10f\n",u.f);
  printf("0x%x\n",u.i);
  representacion_float(u.i);
}


void representacion_float(unsigned int valor){
  unsigned int signo = 0;
  unsigned int exponente_bias = 0;
  unsigned int exponente_unbias = 0;
  unsigned int mantisa = 0;
  unsigned int parte_entera = 0;
  unsigned int parte_decimal = 0;
  unsigned int parte_decimal_max = 0;
  
  signo = extraer_bits(valor, 31, 1);
  exponente_bias = extraer_bits(valor, 23, 8);
  exponente_unbias = exponente_bias - 127;
  mantisa = extraer_bits(valor, 0, 23);
  parte_decimal = extraer_bits(valor,0,23-exponente_unbias);
  parte_decimal_max = 1<<23-exponente_unbias;
  parte_entera = extraer_bits(valor,23-exponente_unbias,
                              exponente_unbias);
  // la parte entera tiene un bit 1 por default al inicio.
  parte_entera |= 1<<exponente_unbias;
  
  printf("Signo: %d\n", signo);
  printf("Exponente (bias): 0x%x (%d)\n", exponente_bias, 
                                          exponente_bias);
  printf("Exponente (unbias): %d\n", exponente_unbias);
  printf("Mantisa: 0x%x\n", mantisa);
  printf("Parte entera: 0x%x (%d)\n", parte_entera, 
                                      parte_entera);
  printf("Parte fraccional: 0x%x (%d) de 0x%x (%d)\n", 
    parte_decimal, parte_decimal, 
    parte_decimal_max, parte_decimal_max);
}


unsigned int extraer_bits(unsigned int valor, short inicio, 
                 short nbits){
  unsigned int mascara = 0;
  short i = 0;
  // primero recorremos el bit inicial al bit 0
  valor >>= inicio;
  // obtener mascara de los bits que deseamos
  for(i = 0; i < nbits; i++){
    mascara <<= 1;
    mascara |= 1;
  }
  // enmascaramos los bits que deseamos
  return valor & mascara;
}

Noten que el código no es muy robusto, ya que para números muy grandes (es decir, exponentes muy grandes) no funciona. Pero ayuda a ilustrar la representación de un flotante.

El programa obtiene los bits correspondientes al signo, el exponente y la mantisa. Además usa el exponente para extraer de la mantisa la parte entera y fraccionaria. La parte fraccionaria la podemos interpretar ya sea viendo los bit y calculando el valor. Por ejemplo, 0x122b en binario es 1 0010 0010 1011, lo cual lo podemos evaluar como 2^−1+2^−4+2^−8+2^−10+2^−12+2^−13 = 0.567749023. Otra forma de verlo, es que el máximo valor de la parte fraccionaria más uno es 0x2000 ó 8192 decimal (es decir esto equivale al valor 1). Por lo que nuestra parte fraccionaria es 4651/8192=0.567749023. Noten, que el valor que obtuvimos es ligeramente menor al valor inicial, esto es a lo que llamamos errores de precisión.

Espero que les haya resultado interesante ver de una forma práctica como guardamos número flotantes.

jueves, 14 de julio de 2011

ANSI C extra: Control de versiones con Mercurial

Durante la sesión anterior, se comentó lo peligroso que puede ser el mal uso de algunos comandos en la terminal que pueden ocasionar la pérdida de la información que contienen archivos que tal vez para nosotros son muy importantes y representan quizá el trabajo de varios días y varias noches.

Y aunque es inevitable equivocarse, afortunadamente existen herramientas que pueden ayudarnos a reducir el impacto de un error que se cometió bajo la presión de alguna fecha de entrega de proyectos y una alternativa para no perder completamente nuestra información es el uso de Control de Versiones.

Existen varias herramientas de Control de Versiones y una de las que he explorado es Mercurial. Para conocer más acerca de Mercurial, puedes echar un vistazo a los posts Mercurial y Bitbucket (parte 1) y Mercurial y Bitbucket (parte 2).

ANSI C extra: implementación de una cola circular

En el post ANSI C tarea: Pilas y Colas se mencionó la existencia de una estructura llamada Pila Circular. En esta ocasión quiero presentar un ejemplo de la implementación de la Cola Circular Estática.

Que sea estática significa que toma como base un arreglo, por lo que el número de elementos que contiene y la memoria que utiliza, están limitados por el tamaño del arreglo con el que se construye.

Para este ejemplo, construí una cola de 10 elementos, pero puede extenderse para que se utilice con cualquier número de elementos.

El código para la Cola Circular Estática es el siguiente:

#include 
#include 
#include "bool.h"

bool estaVacia(int* arreglo){
  
  int i;
  for(i=0; i < 10; i++ ){
    if(arreglo[i] > -1){
      return FALSE;
      break;
    }
  }
  return TRUE;
}

void insertar(int n, int**a, int* fr, int* fn){
  printf("%d, %d\n", *fr, *fn);
  int* esteArreglo = *a;
  if(estaVacia(esteArreglo)){
    *fr = *fn = 0;
  }
  else if(*fn == 9){
    (*fn) = 0;
  }
  else{
    (*fn)++;
  }
  esteArreglo[*fn] = n;
}

int remover(int**a, int*fr, int*fn){
  printf("%d, %d\n", *fr, *fn);
  int* esteArreglo = *a;
  int x = esteArreglo[*fr];
  if((*fr) == (*fn)){
    (*fr) = (*fn) = -1;
  }
  else if((*fr) == 9){
    (*fr) = 0;
  }
  else{
    (*fr)++;
  }
  return x;
}

void imprimir(int* arreglo, int principio){
  
  int i;
  for(i = principio; i < 10; i++){
    printf("%d ", arreglo[i]);
  }
  printf("\n");
}

int main(){
  //indices para controlar el final y el principio de la cola
  int frente = -1;
  int fin = -1;
  //arreglo que contendra los elementos de la cola (10 elementos)
  int* arreglo = NULL;
  arreglo = (int*)malloc(sizeof(int)*10);

  if(arreglo == NULL){
    printf("No se pudo reservar memoria para el arreglo\n");
  }
  else{
    int index;
    for(index = 0; index < 10; index ++){
      arreglo[index] = -1;
    }  
  }

  insertar(2, &arreglo, &frente, &fin);
  imprimir(arreglo, frente);

  insertar(4, &arreglo, &frente, &fin);
  imprimir(arreglo, frente);

  insertar(3, &arreglo, &frente, &fin);
  imprimir(arreglo, frente);

  insertar(10, &arreglo, &frente, &fin);

  imprimir(arreglo, frente);

  remover(&arreglo, &frente, &fin);
  imprimir(arreglo, frente);

  remover(&arreglo, &frente, &fin);

  imprimir(arreglo, frente);
  free(arreglo);
}

La salida de este ejemplo es la siguiente:

ANSI C tarea: Depth First Search

DFS (en inglés Depth First Search) es un algoritmo que nos sirve para movernos a través de la estructura de un grafo. Básicamente permite visitar los vértices de un grafo, pero existen muchos algoritmos que están basados en DFS para realizar sus distintas funcionalidades. Su principio de funcionamiento es bastante sencillo: hay que avanzar en profundidad visitando cada nodo mientras sea posible visitarlo, si no es posible visitar a un nodo, entonces se realiza un procedimiento llamado "backtracking", en el cual comenzamos a regresar por el camino donde llegamos.

Para saber si un vértice es "visitable", contiene tres colores que funcionan como identificadores de su estado:

1. Blanco: Significa que el vértice no ha sido visitado.

2. Gris: La visita a ese vértice está en progreso.

3. Negro: Ya se terminó la exploración de ese nodo y se dice que está visitado.

También se pueden tomar sólo dos colores o los valores verdadero y falso para determinar el estado de un nodo.

Inicialmente, todos los vértices son coloreados de blanco y conforme avanza la búsqueda o visita, sus colores van cambiando para representar su estado.

En esta ocasión deseaba mostrar un ejemplo de esta implementación, sin embargo, no logré mi objetivo pero de todas formas quiero dejar el código visto en clase para representar un grafo junto con las modificaciones que comencé a hacerle para intentar producir un DFS.

Este es el código del archivo grafo.c en donde se encuentran las funciones esArista y DFS que comencé a construir:

#include <stdio.h>
#include <stdlib.h>
#include "bool.h"

typedef struct algo {
  char caracter;
  int id;
  struct algo* sig;
  //Estado del nodo: 'b' = blanco, no visitado,
  //'g' = gris, visitado, 'n' = negro, ya no tiene aristas por visitar
  char estado;
} lista;

int etiqueta(lista** nombres, char a, 
      int* sigId) {
  lista* l = *nombres;
  lista* nuevo = NULL;
  int n = *sigId;
  if (l == NULL) { // no hay nada
    nuevo = (lista*)malloc(sizeof(lista));
    nuevo->caracter = a;
    nuevo->id = n++;
    *sigId = n;
    nuevo->estado = 'b';
    nuevo->sig = NULL;
    *nombres = nuevo;
    return nuevo->id;
  }
  while (1) { // mientras siempre
    if (l->caracter == a) {
      return l->id; // ya esta
    }
    if (l->sig != NULL) {
      l = l->sig; // avanzar en la lista
    } else { // lo ponemos al final
      nuevo = (lista*)malloc(sizeof(lista));
      nuevo->caracter = a;
      nuevo->id = n++;
      *sigId = n;
      nuevo->estado = 'b';
      nuevo->sig = NULL;
      l->sig = nuevo; // ligar en la lista
      return nuevo->id;
    }
  }
}

char recuperar(lista* l, int a) {
  while (l != NULL) { // mientras siempre
    if (l->id == a) {
      return l->caracter;
    }
    if (l->sig != NULL) {
      l = l->sig; // avanzar en la lista
    }
  }
  return '?';
}

//este es un metodo que intente construir para verificar que 
//hay una arista entre dos nodos dados
bool esArista(int** matriz, int a, int b){
  return(matriz[a][b] == 1);
}

//este es el metodo que intente construir para realizar un DFS
void DFS(lista** lista, int** matriz){
  lista* listaDFS = *lista;
  lista* aux = *lista;
  listaDFS -> estado = 'g';
  while(aux != NULL){
    if(esArista(matriz, listaDFS->id, aux->id) && 
       aux->estado == 'b'){
      print("%d\n", listaDFS->id);
      DFS(aux->siguiente, matriz);
    }
  }
  listaDFS->estado = 'n';
}

int main(int argc, char** args) {
  char* archivo = NULL;
  char* segundo = NULL;
  FILE* entrada = NULL;
  FILE* salida = NULL;
  lista* nombres = NULL;
  lista* aux = NULL;
  char a, b;
  int na, nb, i, j, n = 0;
  int** matriz = NULL;
  bool sinSalida = FALSE;

  if (argc < 3) {
    printf("Ponme los archivos guey.\n");
    return 0;
  }
  archivo = args[1];
  segundo = args[2];
  entrada = fopen(archivo, "r"); // r = lectura
  if (entrada == NULL) {
    printf("No se pudo con %s.\n", archivo);
    return 0;
  }
  salida = fopen(segundo, "w+"); // escribe o crea
  if (salida == NULL) {
    sinSalida = TRUE;
    printf("No habra salida.\n");
  }
  while (!feof(entrada)) {
    fscanf(entrada, "%c %c\n", &a, &b);
    na = etiqueta(&nombres, a, &n);
    nb = etiqueta(&nombres, b, &n);
    printf("Recuperado: (%c=%d, %c=%d).\n", 
    a, na, b, nb);
  }
  printf("Fueron en total %d nodos.\n", n);
  matriz = (int**)malloc(sizeof(int*)*n);
  for (i = 0; i < n; i++) {
    matriz[i] = (int*)malloc(sizeof(int)*n);
    for (j = 0; j < n; j++) {
      matriz[i][j] = 0; // no hay conexion
    }
  }
  rewind(entrada);
  while (!feof(entrada)) {
    fscanf(entrada, "%c %c\n", &a, &b);
    na = etiqueta(&nombres, a, &n);
    nb = etiqueta(&nombres, b, &n);
    // hay arista (na, nb) = (nb, na)
    matriz[na][nb] = 1;
    matriz[nb][na] = 1;
  }
  fclose(entrada);
  if (salida == NULL) {
    salida = stdout;
  }
  fprintf(salida, "    ");
  for (j = 0; j < n; j++) {
    fprintf(salida, "%c ", recuperar(nombres, j));
  }
  fprintf(salida, "\n   ");
  for (j = 0; j < n; j++) {
    fprintf(salida, "--", recuperar(nombres, j));
  }
  fprintf(salida, "\n");
  for (i = 0; i < n; i++) {
    fprintf(salida, 
     "%c | ", recuperar(nombres, i));
    for (j = 0; j < n; j++) {
      fprintf(salida, "%d ", matriz[i][j]);
    }
    fprintf(salida, "\n");
  }
  if (!sinSalida) {
    fclose(salida); // cerrar archivo
  }

  DFS(&nombres, matriz);

  // borrar la lista (una vez que ya no se ocupa)
  while (nombres != NULL) {
    aux = nombres->sig;
    free(nombres);
    nombres = aux;
  }

  for (i = 0; i < n; i++) {
    free(matriz[i]);
  }
  free(matriz);
  return 1;
}

Al compilar lo codificado, estos son los errores que aparecen:

Referencias

Algorithms and Data Structures with implementations in Java and C++. En http://www.algolist.net/Algorithms/Graph/Undirected/Depth-first_search. Visitado el 14 de Julio de 2011.

martes, 12 de julio de 2011

ANSI C tarea: Pilas y Colas

Pilas (Stacks)

Las pilas son estructuras de datos tipo LIFO (Last In First Out). Esto significa que funcionan de la siguiente manera: cuando se le agrega un nuevo elemento, éste siempre aparece al principio de la lista de elementos y cuando es cuestión de remover un elemento, siempre se remueve el último elemento agregado (que se encuentra al principio de la lista de elementos, en la cabecera de la pila).

Es de tipo lineal, es decir, sus elementos están acomodados en línea recta y además, puede generarse a partir de arreglos (de manera estática) o con listas enlazadas (de manera dinámica). La mayoría de los lenguajes de programación disponen de un dato tipo Pila, sin embargo, es necesario conocer y comprender su funcionamiento para poder hacer un uso adecuado de éste.

Se consideran 4 operaciones primitivas que pueden realizarse sobre una pila:

  1. Inserción. Consiste en agregar datos o elementos a una pila. Se le conoce como push
  2. Eliminación. Es la supresión de datos y se le conoce como pop
  3. Obtener elemento en el tope (es decir, el elemento primero). A esta operación se le conoce como stacktop y permite obtener el primer elemento de la pila, sin eliminarlo.
  4. Pila vacía. Es un método que regresa verdadero si la pila está vacía o falso de lo contrario. Se le conoce como empty.

Colas (Queues)

Son estructuras de datos tipo FIFO (First In First Out). Me agrada relacionarlas con la fila para comprar las tortillas: el primero en ser atendido e irse a casa es el primero que llega y el último en llegar se forma hasta atrás. Con las colas sucede lo mismo: cuando se saca un elemento de la cola, el elemento que sale es el primero que se colocó y que está al frente y cuando se agrega un elemento, éste se agrega hasta el final. Al igual que las pilas, las colas pueden generarse a partir de arreglos (de forma estática) o de listas enlazadas (de forma dinámica).

Existen diversos tipos de cola:

1. La cola circular. Representa a esta estructura de datos como un círculo y gracias a ello, no desaprovecha espacio como ocurre con una cola lineal estática.

2. La bicola o cola doble. Las inserciones y eliminaciones se pueden realizar tanto al inicio como al final. De ésta existen 2 tipos:

  • Bicola de entrada restringida: Acepta eliminaciones tanto al inicio como al final, pero solamente acepta inserciones al final.
  • Bicola de salida restringida: Acepta eliminaciones solo al inicio, pero las inserciones se pueden realizar tanto al inicio como al final.

3. Cola de prioridades. Es aquella en la que sus elementos tienen un cierto orden de acuerdo con el criterio que les asigna el programador. Hay dos tipos de cola de prioridades:

  • Cola de prioridad ascendente. Sus elementos se pueden insertar de manera arbitraria, pero se acomodan dentro de la cola de manera que lleven un orden ascendente.
  • Cola de prioridad descendente. Sus elementos se pueden insertar de forma arbitraria, pero se acomodan de manera descendente.

Estas son las operaciones primitivas o básicas que se pueden llevar a cabo con las colas:

  1. Insertar un elemento.El nombre puede variar, pero en general a esta operación se le llama insert
  2. Eliminación de un elemento. Operación remove. Cabe aclarar que con este nombre en particular tuve algunos problemas pues ya se encuentra definido en la libreria estándar stdio.h, así que usé la versión en español (remover) en el ejemplo que se encuentra al final de este post.
  3. Cola vacía. Se encarga de verificar si una cola está vacía. Operación empty.

Ejemplo de implementación

A continuación se encuentra un ejemplo de cómo se modificó el código de las listas enlazadas visto en clase para que se comporte tanto como una pila como una cola.

Empecemos por la pila. Así quedó declarada una librería para pilas (al que llamé pilas.h):

#ifndef PILAS_H

#define PILAS_H

#include "bool.h"

// estructura de un elemento de la lista
struct elemento_de_lista {
  int dato; // donde la info va
  // doblemente enlazada
  struct elemento_de_lista* siguiente; // puntero
  struct elemento_de_lista* anterior; // puntero
}; // <= ojo con el punto y coma

// redefinición por brevedad
typedef struct elemento_de_lista elem;

// eliminar todos los elementos de la lista
elem* borrar_pila(elem* esto);

// checar si la lista contiene un valor dado
// devuelve verdad o falso
// recibe un puntero a un elemento de la lista
// implementacion recursiva
bool buscar_en_pila(int valor, elem* aqui);

// devuelve si o no se pudo eliminar
// (no se puede eliminar si no esta)
// valor cuyo elemento hay que eliminar
// (unicamente elimina el primer elemento
// cuyo valor coincide)
// elemento en el cual estamos buscando = aqui
// direccion del inicio de la lista
bool eliminar_elemento_pila(elem* aqui, 
         elem** inicio);

// interface para llamadas mas bonitas
bool pop(elem** inicio);

void imprime_elemento(elem* esto);

// interface que agrega [ ... ] y el \n
void imprimir_pila(elem* lista);

// agregar un elemento en la posicion que
// le corresponde (valores de menor a mayor)
elem* push(int valor, elem* aqui);

#define MAX 30
#define MIN 1

// numeros pseudoaleatorios [MIN, MAX]
int pseudoaleatorio();

#endif

Esta es la implementación de los métodos declarados en la librería (pilas.c):

#include <stdio.h> // imprimir (printf)
#include <stdlib.h> // reservar memoria
#include "pilas.h"

// eliminar todos los elementos de la lista
// opcion vaciar
elem* borrar_pila(elem* esto) {
  elem* temp; // auxiliar 
  // iterativa
  while (esto != NULL) {
    temp = esto->siguiente;
    free(esto); // liberar
    esto = temp; // avanza al siguinte
  }
  return NULL; // que ya no hay lista
}

// checar si la lista contiene un valor dado
// devuelve verdad o falso
// recibe un puntero a un elemento de la lista
// implementacion recursiva
bool buscar_en_pila(int valor, elem* aqui) {
  if (aqui != NULL) {

    printf("Buscando por %d en %d.\n", 
    valor, aqui->dato);

    // si el valor buscado esta en este elemento
    if (aqui->dato == valor) {

      printf("Son iguales.\n");

      return TRUE; // busqueda exitosa

    }
    // pasar la pelota al siguiente elemento
    return buscar_en_pila(valor, aqui->siguiente);
  }
  else { // aqui es null
    // este elemento actual ya es null, o sea,
    // no en realidad es un elemento

    printf("Ya se acabo. No estuvo.\n");
    return FALSE; // busqueda fallida
  }
}

// devuelve si o no se pudo eliminar
// (no se puede eliminar si no esta)
// valor cuyo elemento hay que eliminar
// (unicamente elimina el primer elemento
// cuyo valor coincide)
// elemento en el cual estamos buscando = aqui
// direccion del inicio de la lista
bool eliminar_elemento_pila(elem* aqui, 
         elem** inicio) {
  if (aqui != NULL) { // si hay algo
    *inicio = aqui->siguiente;
    free(aqui); // borrame      return TRUE; // eliminacion exitosa
    return TRUE;
    } 
  return FALSE;
}

// interface para llamadas mas bonitas
bool pop(elem** inicio) {
  return 
    eliminar_elemento_pila(*inicio, inicio);
}

void imprime_elemento(elem* esto) {
  // iterativa
  while (esto != NULL) {
    printf("%d ", esto->dato);
    esto = esto->siguiente;
  }
  return;
}

// interface que agrega [ ... ] y el \n
void imprimir_pila(elem* lista) {
  printf("[ ");
  imprime_elemento(lista);
  printf("]\n");
  return;
}

// agregar un elemento en la posicion que
// le corresponde (valores de menor a mayor)
elem* push(int valor, elem* aqui) {
  elem* nuevo = NULL; // auxiliar
  // para crear el nuevo elemento
 
  if (aqui != NULL) {
    printf("Estoy en %d, insertando un %d.\n",
    aqui->dato, valor);
  } else {
    printf("No hay nada.\n");
  }
 
  if (aqui == NULL) { // no hay nadie
    nuevo = (elem*)malloc(sizeof(elem));
    nuevo->dato = valor; // asignar dato
    nuevo->siguiente = NULL; // el unico
    nuevo->anterior = NULL; // el unico
    return nuevo;
  } 
  else {
    nuevo = (elem*)malloc(sizeof(elem));
    nuevo->dato = valor; // pon el valor
    nuevo->siguiente = aqui;
    aqui->anterior = nuevo;
    nuevo->anterior = NULL; 
  }
  return nuevo;
}

// numeros pseudoaleatorios [MIN, MAX]
int pseudoaleatorio() {
  return ((rand() % (MAX - MIN + 1)) + MIN);
}

En el archivo prueba_pilas.c se encuentra el método principal que manda a llamar a las rutinas propias de la pila (contenidas en pilas.c):

#include "pilas.h"
#include "entrada.h"
#include  // printf
#include  // srand

// rutina principal
int main(int argc, char** args) {
  elem* lista = NULL; // vacia al inicio
  int valor; // auxiliares
  char op;
  bool basta = FALSE;

  while (!basta) {
    printf("Que quieres hacer?\n");
    printf("Agregar = a\nBuscar = b\n");
    printf("Eliminar = e\nVaciar = v\n");
    printf("Imprimir = i\nSalir = s\n> ");
    op = pide_opcion("abevis");
    switch (op) {
    case 'a':
      valor = pide_numero(MIN, MAX);
      lista = push(valor, lista);      
      break;
    case 'b':
      valor = pide_numero(MIN, MAX);
      printf("%d %s esta en la lista.\n",
      valor, (buscar_en_pila(valor, lista) ? 
       "SI" : "NO"));
      break;
    case 'e':
      if (pop(&lista)) {
 printf("%d eliminado.\n", valor);
      } else {
 printf("Pila vacia.No pudo eliminarse informacion de la pila");
      }
      break;
    case 'v':
      lista = borrar_pila(lista);
      break;
    case 'i':
      imprimir_pila(lista);
      break;
    case 's':
      basta = TRUE;
      break;
    default:
      printf("Esto no deberia pasar nunca.\n");
      break;
    }
  }
  return 1; // ya no hacemos nada
}

En la siguiente imagen se aprecia la salida del programa para las pilas:

A continuación se encuentra el código para las colas. Esta es la libreria que contiene la funcionalidad de las colas (colas.h):

#ifndef COLAS_H

#define COLAS_H

#include "bool.h"

// estructura de un elemento de la lista
struct elemento_de_lista {
  int dato; // donde la info va
  // doblemente enlazada
  struct elemento_de_lista* siguiente; // puntero
  struct elemento_de_lista* anterior; // puntero
}; // <= ojo con el punto y coma

// redefinicion por brevedad
typedef struct elemento_de_lista elem;

// eliminar todos los elementos de la lista
elem* borrar(elem* esto);

// checar si la lista contiene un valor dado
// devuelve verdad o falso
// recibe un puntero a un elemento de la lista
// implementacion recursiva
bool buscar(int valor, elem* aqui);

// devuelve si o no se pudo eliminar
// (no se puede eliminar si no esta)
// valor cuyo elemento hay que eliminar
// (unicamente elimina el primer elemento
// cuyo valor coincide)
// elemento en el cual estamos buscando = aqui
// direccion del inicio de la lista
bool eliminar_elemento_cola(elem* aqui, 
         elem** inicio);

// interface para llamadas mas bonitas
bool remover(elem** inicio);

void imprime_elemento(elem* esto);

// interfase que agrega [ ... ] y el \n
void imprimir_cola(elem* lista);

// agregar un elemento en la posicion que
// le corresponde (valores de menor a mayor)
elem* insert(int valor, elem* aqui);

#define MAX 30
#define MIN 1

// numeros pseudoaleatorios [MIN, MAX]
int pseudoaleatorio();

#endif

En este archivo (colas.c) se realizaron las modificaciones necesarias para obtener el comportamiento de una cola (colas.c)

#include  // imprimir (printf)
#include  // reservar memoria
#include "colas.h"

// eliminar todos los elementos de la lista
// opcion vaciar
elem* borrar(elem* esto) {
  elem* temp; // auxiliar 
  // iterativa
  while (esto != NULL) {
    temp = esto->siguiente;
    free(esto); // liberar
    esto = temp; // avanza al siguinte
  }
  return NULL; // que ya no hay lista
}

// checar si la lista contiene un valor dado
// devuelve verdad o falso
// recibe un puntero a un elemento de la lista
// implementacion recursiva
bool buscar(int valor, elem* aqui) {
  if (aqui != NULL) {

    printf("Buscando por %d en %d.\n", 
    valor, aqui->dato);

    // si el valor buscado esta en este elemento
    if (aqui->dato == valor) {
      printf("Son iguales.\n");
      return TRUE; // busqueda exitosa
    } 
    // pasar la pelota al siguiente elemento
    return buscar(valor, aqui->siguiente);
  } else { // aqui es null
    // este elemento actual ya es null, o sea,
    // en realidad no es un elemento
    printf("Ya se acabo. No estuvo.\n");
    return FALSE; // busqueda fallida
  }
}

// devuelve si o no se pudo eliminar
// (no se puede eliminar si no esta)
// valor cuyo elemento hay que eliminar
// (unicamente elimina el primer elemento
// cuyo valor coincide)
// elemento en el cual estamos buscando = aqui
// direccion del inicio de la lista
bool eliminar_elemento_cola(elem* aqui, 
         elem** inicio) {
  if (aqui != NULL) { // si hay algo
    *inicio = aqui->siguiente;
    free(aqui); // borrame
      return TRUE; // eliminacion exitosa
  }
  return FALSE;
}

// interface para llamadas mas bonitas
bool remover(elem** inicio) {
  return 
    eliminar_elemento_cola(*inicio, inicio);
}

void imprime_elemento(elem* esto) {
  // iterativa
  while (esto != NULL) {
    printf("%d ", esto->dato);
    esto = esto->siguiente;
  }
  return;
}

// interface que agrega [ ... ] y el \n
void imprimir_cola(elem* lista) {
  printf("[ ");
  imprime_elemento(lista);
  printf("]\n");
  return;
}

// agregar un elemento en la posicion que
// le corresponde (valores de menor a mayor)
elem* insert(int valor, elem* aqui) {
  elem* nuevo = NULL; // auxiliar
  // para crear el nuevo elemento
 
  if (aqui != NULL) {
    printf("Estoy en %d, insertando un %d.\n",
    aqui->dato, valor);
  } else {
    printf("No hay nada.\n");
  }

    nuevo = (elem*)malloc(sizeof(elem));
    nuevo->dato = valor; // asignar dato 

  if (aqui == NULL) { // no hay nadie
    nuevo->siguiente = NULL; // el unico
    nuevo->anterior = NULL; // el unico
    return nuevo;
  } 
  else {
    if(aqui->siguiente != NULL){
      insert(valor, aqui->siguiente);
    }
    else{
      printf("Anexando al final.\n");
     
      aqui->siguiente = nuevo;
      nuevo->anterior = aqui;
      nuevo->siguiente = NULL; // el ultimo
    }
  }
  return aqui;
}

// numeros pseudoaleatorios [MIN, MAX]
int pseudoaleatorio() {
  return ((rand() % (MAX - MIN + 1)) + MIN);
}

El siguiente código muestra el método principal y la llamada a los métodos propios de la cola (prueba_colas.c):

#include "pilas.h"
#include "entrada.h"
#include  // printf
#include  // srand

// rutina principal
int main(int argc, char** args) {
  elem* lista = NULL; // vacia al inicio
  int valor; // auxiliares
  char op;
  bool basta = FALSE;

  while (!basta) {
    printf("Que quieres hacer?\n");
    printf("Agregar = a\nBuscar = b\n");
    printf("Eliminar = e\nVaciar = v\n");
    printf("Imprimir = i\nSalir = s\n> ");
    op = pide_opcion("abevis");
    switch (op) {
    case 'a':
      valor = pide_numero(MIN, MAX);
      lista = push(valor, lista);      
      break;
    case 'b':
      valor = pide_numero(MIN, MAX);
      printf("%d %s esta en la lista.\n",
      valor, (buscar_en_pila(valor, lista) ? 
       "SI" : "NO"));
      break;
    case 'e':
      if (pop(&lista)) {
 printf("%d eliminado.\n", valor);
      } else {
 printf("Pila vacia.No pudo eliminarse informacion de la pila");
      }
      break;
    case 'v':
      lista = borrar_pila(lista);
      break;
    case 'i':
      imprimir_pila(lista);
      break;
    case 's':
      basta = TRUE;
      break;
    default:
      printf("Esto no deberia pasar nunca.\n");
      break;
    }
  }
  return 1; // ya no hacemos nada
}

Finalmente, este es el resultado de la compilación y ejecución de colas.c, prueba_colas.c y entrada.c. Éste último es necesario compilarlo junto con los otros dos pues se hace uso de sus métodos para requerir valores al hipotético usuario:

Referencias

"Queue (data structure)". Wikipedia. The Free Encyclopedia, 2011.

"Stack (data structure)." Wikipedia. The Free Encyclopedia, 2011.

Apuntes de Estructuras de Datos. Semestre Agosto-Diciembre de 2010.

jueves, 7 de julio de 2011

ANSI C tarea: Usando ciclos, apuntadores, arreglos y subrutinas

Existen tres tipos de ciclo en C: while, do..while y for. En el post ANSI C extra: while, do while y for pueden ser equivalentes se demostró su uso y además se compararon estos ciclos utilizando un pequeño ejemplo.

En esta ocasión, utilizaré otro ejemplo para ilustrar el uso que se puede da a apuntadores, arreglos y subrutinas en C.

En C, un apuntador es una variable "especial" que contiene la dirección de memoria de algún elemento que estamos utilizando en nuestro programa. Este elemento puede ser otra variable, un tipo definido por el programador e inclusive, otro apuntador.

Los arreglos utilizan apuntadores para navegar a través de los elementos que contienen. Así, cuando se crea un arreglo, el nombre del arreglo indica un apuntador al elemento 0 (primer elemento del arreglo) y a través de ciclos, podemos realizar alguna operación o simplemente conocer a los elementos que integran al arreglo. Un arreglo puede contener dentro de sí mismo a otros arreglos, lo que llamamos "Arreglo multidimensional", el cual es muy útil para representar por ejemplo, matrices en matemáticas.

Otro aspecto del lenguaje C que se ejemplifica en este post es el uso de subrutinas. Las subrutinas son métodos definidos cada uno en su propio archivo. De esta manera, a través del uso de subrutinas, el programa que creamos se vuelve más simple de manejar y adquiere un aspecto llamado "modularidad", esto quiere decir que dividimos las distintas funciones que queremos que realice en porciones o módulos que, debido a que se encuentran definidos cada uno en un archivo por separado, es posible reutilizarlos más adelante sin tener que volver a teclear el mismo código o pegarlo dentro de otro archivo.

El ejemplo que estoy presentando es la generación del Triángulo de Pascal. Este triángulo es una herramienta que nos permite conocer los coeficientes del polinomio que resulta al elevar un binomio a cierta potencia. El número de renglón corresponde a la potencia a la que se elevó el binomio. Por ejemplo, si elevamos el binomio (x + 1) al cuadrado obtendremos x² + 2x + 1. Luego, El triángulo de Pascal tiene la siguiente forma:

1

1 2 1

1 3 3 1

.

.

.

La segunda fila (1 2 1) corresponde a nuestro resultado de elevar el binomio al cuadrado: 1 es el coeficiente de x², 2 es el coeficiente de 2x y 1 es el coeficiente de 1².

Cada elemento después del primer 1 se obtiene sumando los dos elementos que se encuentran en la fila anterior y hacia la derecha del valor que deseamos obtener, así, en la fila 3 por ejemplo el 3 se forma de la suma de 1 y 2.

La implementación del ejemplo la dividí en varios módulos, cada uno en su propio archivo. Para poder compilarlos es necesario crear un encabezado donde se colocan las librerías a utilizar, las definiciones tanto de variables como de estructuras y los nombres y parámetros de los distintos métodos seguidos de punto y coma sin nada de su implementación.

El encabezado (al que llamé trianguloHeader.h) de este ejemplo es el siguiente:

#include 
#include 
#include 
#include 
#include 

#define TRUE 1
#define FALSE 0
typedef short boolean;

#define SIN_DEFINIR -1
#define LARGO_MAXIMO 12
#define MAX_DIMENSION 30

typedef struct una_matriz{
  int filas;
  int columnas;
  int** elementos;
}matriz;

char pide_opcion(char* permitidos);
int pide_numero(int minimo, int maximo);
void vacia(matriz* m);
void llena(matriz* m);
void imprime(matriz m);

Después, en otro archivo reutilicé el método para pedir un valor numérico al usuario. A este archivo lo llamé pedirNumero.c. Su contenido es el siguiente:

#include "trianguloHeader.h"

int pide_numero(int minimo, int maximo){
  int numero = SIN_DEFINIR;
  char* entrada = NULL;
  char actual;
  int posicion;
  double temporal;

  entrada = 
    (char*)malloc(sizeof(char)*LARGO_MAXIMO + 1);
  if(entrada == NULL){
    return SIN_DEFINIR;
  }
  do{
    printf("Dame un valor entre %d y %d: ", 
    minimo, maximo);
    posicion = 0;
    do{
      actual = getchar();
      if(posicion < LARGO_MAXIMO){
 if(isdigit(actual)){
   entrada[posicion++] = actual;
 }
      }
    }while(actual != '\n');
    entrada[posicion] = '\0';
    temporal = atof(entrada);
    if(temporal > INT_MAX - 1){
      continue;
    }
    numero = atoi(entrada);
  }while(numero < minimo || numero > maximo);
  free(entrada);
  return numero;
}

El siguiente archivo que creé contiene el método pedir_opcion, que también estoy reutilizando del material visto en clase. A este archivo lo nombré opcionSiNo.c. Este es su contenido:

#include "trianguloHeader.h"

char pide_opcion(char* permitidos){
  char actual;
  boolean listo = FALSE;

  while(TRUE){
    if(!listo){
      actual = tolower(getchar());
      if(strchr(permitidos, actual) != NULL){
 listo = TRUE;
      }
    } else{
      if(getchar() == '\n'){
 if(listo){
   break;
 }
      }
    }
  }
  return actual;
}

Después creé el archivo llenarTriangulo.c para almacenar la funcionalidad de llenado. La mayoría de este código también está basado en el material visto en clase:

#include "trianguloHeader.h"

void llena(matriz* m){
  int i, j, k;

  printf("Llenando el Triangulo de Pascal\n");
  printf("Cuantos renglones quieres que tenga?\n");
  m->filas = pide_numero(1, MAX_DIMENSION);
  
  m->elementos = (int**)malloc(sizeof(int*)*(m->filas));
  if(m->elementos == NULL){
    printf("No se pudo reservar memoria.\n");
    return;
  }
  for(i = 0; i < m->filas; i++){
    m->elementos[i] = 
      (int*)malloc(
     sizeof(int)*m->filas);
    for(j = 0; j < m->filas; j++){
      m->elementos[i][j] = 0;
    }
  }

  i = 0;
  j = 0;
  m->elementos[i][j] = 1;
   for(i = 1; i < m->filas; i++){
     j = 0;
     m->elementos[i][j] = 1;
     for(j = 1; j < m->filas;j++){
 m->elementos[i][j] = m->elementos[i-1][j-1]
     + m->elementos[i-1][j];
      }
   }
}

Otra funcionalidad que ocupé fue la de vaciar la matriz donde almacené los valores del triángulo de Pascal con el objetivo de liberar memoria. Este otro método también está basado en el material visto en clase. El archivo se llama vaciarTriangulo.c:

#include "trianguloHeader.h"

void vacia(matriz* m){
  int i;

  m->filas = SIN_DEFINIR;
  m->columnas = SIN_DEFINIR;

  for(i = 0; i < m->filas; i++){
    free(m->elementos[i]);
  }
  free(m->elementos);
  return;
}

La última funcionalidad que agregué es la de imprimir el triángulo reutilizando el método de imprimir una matriz bidimensional visto en la clase. El archivo se llama imprimeTriangulo.c:

#include "trianguloHeader.h"

void imprime(matriz m){
  int i, j, k;

  for(i = 0; i < m.filas; i++){
    for(j = 0; j <= i; j++){
      printf("%s%3d", (j == 0? "": "  "),
       m.elementos[i][j]);
    }
    printf("\n");
  }
  return;
}

Finalmente, el método principal, desde donde se mandan a llamar a casi todas mis funcionalidades se encuentra en el archivo trianguloPascal.c:

#include "trianguloHeader.h"

int main(int argc, char** args){

  boolean opcion = FALSE;
  do{
    matriz m;
    llena(&m);
    imprime(m);
    vacia(&m);

    printf("Deseas generar otro triangulo (s/n)?\n");
    opcion = (pide_opcion("sn") == 's');

  }while(opcion);
}

Es importante resaltar que todos los archivos, incluyendo el principal (trianguloPascal.c) deben incluir al encabezado (trianguloHeader.h) para poder compilarlos juntos. La forma de compilar y algunas salidas del programa se pueden observar en la siguiente imagen:

Referencias

"Pascal's Triangle". Wolfram MathWorld. 2011

Pointers.Te GNU C Programming Tutorial, Edition 4.1

Loops.Te GNU C Programming Tutorial, Edition 4.1