12 de diciembre de 2018

Errores frecuentes.

   La siguiente es una lista de errores frecuentes observados en mis estudiantes o lectores del blog. Es una lista necesariamente incompleta y en frecuente actualización; sin embargo la considero de utilidad para el lector interesado en aprender de las experiencias de otros. Sobre todo si se considera que muchas veces se aprende más de los errores que de los aciertos.

   He tratado de clasificar los errores para facilitar al lector su ubicación, espero que ésta sea también de utilidad.

Sintaxis:
  • Omitir el punto y coma (;)  al final de las sentencias.
  • Olvidar las llaves de bloque asociadas a una estructura de control, cuando el número de sentencias es mayor a uno.
 Lectura de datos:
  • Olvidar el ampersand (&) antes de la variable: 
    • Incorrecto: scanf("%d", variable_int);
    • Correcto:   scanf("%d", &variable_int);
  • Utilizar secuencias de escape:
    • Incorrecto: scanf("%d\n", &variable_int);
    • Correcto:   scanf("%d", &variable_int); 
  • Utilizar el especificador de formato incorrecto:
    • Incorrecto: scanf("%d",  &variable_float);
    • Correcto:   scanf("%f",  &variable_float);
  • Utilizar el especificador de precisión para flotantes:
    • Incorrecto: scanf("%.2f", &variable_float); 
    • Correcto:   scanf("%f", &variable_float);
 Impresión de datos:
  • Utilizar el especificador de formato incorrecto:
    • Incorrecto: printf("El valor es %d\n", variable_float);
    • Correcto:   printf("El valor es %.2f\n", variable_float);
Estructuras de control:
  • Omitir las llaves del bloque cuando el cuerpo de la estructura de control tiene más de una sentencia.
  • Utilizar el operador de asignación "=" en lugar del operador de comparación "==".
Variables:
  • Pensar que las variables son automáticamente inicializadas con algún valor cuando no es así, utilizando en consecuencia valores "basura".
  • No inicializar (normalmente con cero) variables de tipo acumulador. 
 Funciones:
  • Olvidar colocar el tipo de dato a cada variable en la lista de parámetros.
  • Invocar a una función con más o menos parámetros de los que necesita, o con tipos de datos distintos a los especificados en la lista de parámetros.
  • Colocar corchetes a los arreglos cuando se pasan como argumento a una función (invocación o llamado de función): los arreglos se pasan como argumentos solamente con su identificador. Consulte más abajo la sección referente a Arreglos.
  • Hacer lectura de datos dentro de una función: si la función NO es de lectura de datos, la función no debe leer datos en su definición. Esta es una consideración de diseño más que un error en sí mismo.
  • Hacer impresión de datos dentro de una función: si la función NO es de impresión de datos, la función no debe imprimir los datos que gestiona en su definición. Esta es una consideración de diseño más que un error en sí mismo.
  • En una "biblioteca" personalizada de funciones, si una función A utiliza a una función B, B debe estar definida antes que A.
  • Invocar funciones que regresan un apuntador con el operador de des referencia. Consulte más abajo la sección referente a Apuntadores.
Arreglos:
  • No definir el tamaño de un arreglo.
  • Definir el tamaño de un arreglo estático con el valor de una variable. La mayoría de los compiladores modernos soportan esta característica sin problema; sin embargo, en el estándar ANSI C hacer esto es incorrecto, por lo que su programa estaría sacrificando su portabilidad.
  • No inicializar los elementos del arreglo cuando su valor inicial será utilizado.
  • Colocar más inicializadores que elementos en el arreglo.
  • Omitir el ampersand (&) en la lectura de elementos individuales en el arreglo (para un arreglo de enteros):
    • Incorrecto: scanf("%d", arreglo[i]); 
    • Correcto:   scanf("%d", &arreglo[i]);
  • Colocar corchetes al pasarle un arreglo a una función:
    • Incorrecto: funcion(arreglo[ ], n);
    • Correcto:   funcion(arreglo, n);
Apuntadores:
  • No utilizar el operador de des referencia cuando se quiere acceder al valor de a lo que apunta el apuntador (el valor al que hace referencia el apuntador): 
    • Incorrecto: printf("El valor es: %d\n", intPtr);
    • Correcto:   printf("El valor es: %d\n", *intPtr);
  • Utilizar un apuntador y asumir que se tiene espacio de almacenamiento como en un arreglo. Error común con cadenas:
    • char *s1; es distinto de char s2[100]
      • Incorrecto: fgets(s1, 100, stdin);
      • Correcto:   fgets(s2, 100, stdin); 
  • Utilizar el operador de des referencia en la invocación a funciones que regresan un apuntador. Ejemplo, considere (vea el Ejercicio 12 de la entrada Ejercicios selectos (apuntadores)):
    • int *arreglo, original[100]; (apuntador a entero y arreglo) y el siguiente prototipo int *copia(const int *original, int n); 
      • Incorrecto: *arreglo = *copia(original, n);
      • Incorrecto: *arreglo = copia(original, n);
      • Incorrecto:  arreglo = *copia(original, n);  
      • Correcto:    arreglo = copia(original, n);
Estructuras (struct):
  • Acceder a los elementos miembro de una estructura con el operador incorrecto:
    • Si la variable es de tipo estructura, se utiliza el operador punto (.).
    • Si la variable es de tipo apuntador a estructura, se utiliza el operador flecha (->).
  • Para una estructura definir elementos miembros de otra estructura que no está definida:
    • Sean e1 y e2 dos variables de tipo struct distintos: e2 puede tener elementos miembro del tipo de e1 si y sólo si el tipo struct de e1 está definido antes que e2.
Archivos:
  • Utilizar las funciones fwrite y fread para archivos de texto. Los archivos de texto se usan con fprintf, fscanf, fputs, fgets, etc.
  • Utilizar las funciones fprintf, fscanf, fputs, fgets, etc. para archivos binarios. Los archivos de binarios se usan con las funciones fwrite y fread.
Errores más frecuentes del curso
Errores más frecuentes que se cometen al tomar el curso de forma presencial.



3 de diciembre de 2018

Arreglos de dos dimensiones (matrices).

   Los arreglos de dos dimensiones también reciben el nombre de arreglos bidimensionales o simplemente matrices.

Conceptos, representación y estructura.
   Los arreglos bidimensionales son una estructura de datos de dos dimensiones de elementos contigüos, relacionados por un mismo tipo de datos y un mismo nombre, donde los elementos son distinguidos por dos índices.

(a) Representación (abstracción) de una matriz.
(b) Representación en memoria de una matriz.
 
    Las figuras anteriores muestran en (a), la representación de una matriz m de cuatro renglones y cuatro columnas (cuadrada). Los renglones y columnas de una matriz en C siempre empiezan en el índice 0, y terminan en el índice tamaño - 1 (3), dónde tamaño se refiere al tamaño, dimensión o longitud del renglón o columna de la matriz respectivamente. Por otro lado, la figura (b) muestra la representación física de la matriz m en la memoria principal de la computadora. Observe que la memoria de la computadora es una estructura lineal y no una estructura cuadrada (bidimensional), por lo que los elementos de la matriz son almacenados linealmente, de tal forma que el elemento m[1][0] es el inmediato posterior del elemento m[0][3], y que el elemento m[2][0] es el inmediato posterior del elemento m[1][3], y así sucesivamente.

   En general, la declaración de un arreglo bidimensional en el lenguaje de programación C tiene la siguiente estructura:

tipo_de_dato nombre_de_la_matriz[TAMAÑO1][TAMAÑO2];

donde:

  • tipo_de_dato es cualquier tipo de dato válido en C.
  • nombre_de_la_matriz es un identificador válido en C.
  • TAMAÑO1 es el número de renglones que tendrá la matriz.
  • TAMAÑO2 es el número de columnas que tendrá la matriz.

Declaración, inicialización, recorrido y uso con funciones.
   El Ejemplo 6.7 muestra la declaración, la inicialización, el recorrido, y el uso de matrices con funciones. Las líneas 6 y 7 definen dos constantes simbólicas M y N para el número de renglones y columnas de la matriz respectivamente. Estas constantes determinarán las dimensiones reales o físicas de matriz1, matriz2 y matriz3, declaradas en las líneas 14 y 15. Note que estas tres matrices tiene la forma de las figuras anteriores.

   Observe también que matriz2 y matriz3 han sido inicializadas en su declaración, esto quiere decir que en tiempo de compilación le son asignados los valores especificados. Los valores asignados están representados en las siguientes figuras:

(a) Inicialización de la matriz2.
(b) Inicialización de la matriz3.
 
    Ahora bien, la línea 14 muestra un tipo de inicialización en secuencia, la cual es posible debido a la forma en que se representa la matriz en la memoria de la computadora.

   En base a lo anterior, los primeros cinco elementos físicos de matriz2 tendrán los valores que se muestran en la figura (a). Los elementos que no son inicializados explícitamente, son puestos en 0 por omisión.

   Por otro lado, la línea 15 muestra una inicialización por renglones para matriz3; este tipo de inicialización se explica mejor con la figura (b) en donde se observa que los tres primeros elementos del renglón 0 están inicializados con los valores especificados (línea 15), mientras que el último de ellos ha sido inicializado por omisión con 0. Antes de continuar, asegúrese de entender lo que sucede con el segundo renglón de inicialización de la matriz3 y su correspondiente representación en la figura (b).

   El ciclo do-while de las líneas 17-20 valida (la validación sólo considera matrices cuyas dimensiones sean de al menos 2x2 y de a lo más MxN. Si bien es cierto que existen matrices de 3x1 o de 1x4 por ejemplo, también es cierto que técnicamente se les puede denominar vectores, por lo que por simplicidad y para motivos de ilustración se ha hecho este tipo  de validación) las dimensiones para la matriz1 específicamente, debido a que el Ejemplo 6.7 define unas dimensiones físicas (líneas 14 y 15), pero controla unas dimensiones lógicas (líneas 21 y 24), y dichas dimensiones lógicas estarán determinadas por las variables m y n (línea 19) para el número de renglones y columnas respectivamente.

   Las figuras de inicialización anteriores  indican también las dimensiones lógicas (indicadas con una elipse) para la matriz2 y la matriz3 respectivamente, mientras que las dimensiones físicas están representadas por la malla (matriz) de 4x4.

   La línea 21 hace el llamado a la función leeMatriz definida en la líneas 33-44. El llamado envía como argumentos la matriz1, y las dimensiones m y n obtenidas en la línea 19.

   Ahora bien, la función leeMatriz define tres parámetros: matriz[ ][N], m y n. Observe que matriz será el nombre con el que la función hará referencia al arreglo bidimensional que reciba, y que la notación [ ][N] le indica al compilador que lo que recibirá será precisamente un arreglo bidimensional. Note también que los primeros corchetes están vacíos mientras que los segundos tienen la dimensión física N declarada para el arreglo (líneas 14 y 15).

   En C, los arreglos de más de una dimensión deben llevar siempre, con posibilidad de omitir únicamente el tamaño de la primera dimensión, el tamaño físico de cada una de las dimensiones restantes, y esto está estrechamente relacionado con la representación física en la memoria de la computadora de los arreglos de n dimensiones.

   La especificación del tamaño de la segunda dimensión para las matrices le indica al compilador cuántos elementos hay por renglón, para así poder determinar en dónde empiezan cada unos de los renglones de la matriz.

   Continuando con el Ejemplo 6.7, la función leeMatriz se encarga de leer de la entrada estándar (línea 40) los elementos que conformarán la matriz lógica determinada por los parámetros m y n. Note cómo los ciclos for de las líneas 36 y 38 están en función de dichos parámetros.

   Por otro lado, las líneas 24, 26 y 28 hacen el llamado a la función imprimeMatriz definida en la líneas 33-44. El primer llamado envía como argumentos a la matriz1 y a las dimensiones obtenidas en la línea 19; mientras que el segundo y tercer llamado envían a la matriz2 y a la matriz3 respectivamente ambas con dimensiones lógicas 2 y 4.

   Finalmente, la función imprimeMatriz definida en las líneas 46-54 define una lista de parámetros análoga a la de la función leeMatriz. La función imprimeMatriz realiza un recorrido por renglones de la matriz matriz para su impresión en la salida estándar. Una posible salida del Ejemplo 6.7 se  muestra en la siguiente figura:

Una posible salida del Ejemplo 6.7.

22 de noviembre de 2018

Arreglos de una dimensión (vectores).

   Los arreglos de una dimensión se denominan comúnmente arreglos o vectores, en este texto se utilizará la primera denominación.

Conceptos, representación y estructura.
   Los arreglos son una estructura de datos lineal de elementos contigüos relacionados por un mismo tipo de datos, un mismo nombre (identificador), y distinguidos por un índice.

(a) Representación de un arreglo (Abstracción).  





(b) Representación de un arreglo en la memoria.

   Las figuras anteriores muestran la representación de un arreglo de tamaño cuatro de nombre arreglo. En (a) se observa la representación abstracta de dicho arreglo, mientras que en (b) se presenta la forma en que dicho arreglo es almacenado en la memoria principal de la computadora. Note que los arreglos se representan de forma lineal directa en la memoria de la computadora.

   Los arreglos en C siempre empiezan en el índice 0 y terminan en tamaño - 1, dónde tamaño se refiere al tamaño, dimensión o longitud del arreglo.

   Un aspecto importante a considerar respecto a la conveniencia en el uso de arreglos es la siguiente: no es lo mismo declarar 100 variables de tipo entero que un arreglo de 100 enteros ¿Puede imaginar lo terriblemente tedioso que sería declarar 100 variables de tipo entero?

   Otra ventaja de utilizar arreglos en lugar de una tediosa declaración masiva de variables, es que el grupo de datos relacionados por el nombre del arreglo están contigüos en la memoria, mientras que no necesariamente sería así para el grupo de variables declaradas individualmente.

   En general, la declaración de un arreglo en el lenguaje de programación C tiene la siguiente estructura general:

tipo_de_dato   nombre_del_arreglo[TAMAÑO];

en donde:
  • tipo_de_dato es cualquier tipo de dato válido en C.
  • nombre_del_arreglo es un identificador válido en C.
  • TAMAÑO es la longitud o tamaño que tendrá el arreglo.

Declaración, inicialización y recorrido.
   El Ejemplo 6.1 muestra la declaración, inicialización y recorrido de un arreglo de enteros.

   En la línea 10 se está declarando un arreglo de nombre arreglo de tamaño N. Observe que el arreglo está siendo inicializado en la declaración o en tiempo de compilación; lo que hace el compilador es asignarle 0 al elemento arreglo[0] y 1 al elemento arreglo[1], todos los demás elementos del arreglo se inicializan por omisión en 0, es decir:

arreglo[ 0 ] = 0
arreglo[ 1 ] = 1
arreglo[ 2 ] = 0
arreglo[ 3 ] = 0 
. . . 

arreglo[N-1] = 0

   Si un arreglo, independientemente de su tipo de dato, se declara pero no se inicializa, el contenido de cada unos de sus elementos no está determinado a un valor concreto o específico, lo cual implica que contendrán lo que en el argot computacional se conoce como basura.

   La línea 13 está realizando un recorrido del arreglo; note que para dicho recorrido, el índice se inicializa en 2, mostrando así que es posible iniciar el recorrido de un arreglo en donde se necesite, y no siempre desde 0.

   Por otro lado, la línea 14 está asignándole valores al arreglo a partir de la posición 2. Observe que el valor i-ésimo del arreglo se define como la suma de los dos valores anteriores, por lo que en realidad lo que hace el ciclo for es inicializar el arreglo con los valores de la serie de Fibonacci que se estudió en la entrada referente a la recursividad.

   Finalmente, las líneas 18 y 19 son lo que se conoce como un recorrido para impresión del arreglo, en el cual se imprime por renglón en la salida estándar, cada uno de los elementos del arreglo arreglo. La salida del Ejemplo 6.1 se  muestra en la siguiente figura:

Salida del Ejemplo 6.1.
 
    Ahora bien, el Ejemplo 6.2 muestra otra variante de inicialización de arreglos y un doble recorrido para un arreglo de tipo float.

   Observe que el arreglo a de la línea 10 no tiene un tamaño explícito. Cuando se declara un arreglo en C, éste debe tener una dimensión o tamaño definidos explícita o implícitamente; para el caso del Ejemplo 6.2 el tamaño está implícito, y es determinado por el número de inicializadores (5), los cuales son calculados por el compilador y no están relacionados con el #define de la línea 7, al menos en cuanto a la definición del tamaño del arreglo se refiere.

   La estructura de repetición for de la línea 15, utiliza dos variables de control para hacer un recorrido ascendente (i) y descendente (j) del arreglo a, con la finalidad de mostrar que no siempre ni necesariamente los arreglos se recorren de manera ascendente. La salida del Ejemplo 6.2 muestra en la siguiente figura:

Salida del Ejemplo 6.2.

16 de noviembre de 2018

Arreglos.

   Antes de iniciar con los conceptos y la descripción de los arreglos, considere el siguiente problema:
Se desea almacenar un grupo de cuatro calificaciones, y determinar:
  • Cuál es la menor.
  • Cuál es la mayor.
  • Cuáles y cuántas son aprobatorias y reprobatorias en base a un determinado criterio.
  • Su media aritmética (promedio).
  • Si existe o no una calificación específica.
Finalmente, las calificaciones deben ser ordenadas de manera ascendente.
   Con lo que se ha presentado hasta ahora en las entradas anteriores, ¿puede dar solución a este problema?, y si en lugar de cuatro calificaciones se tuvieran que procesar diez, ¿qué cambios tendría que hacer?, ¿y si en lugar de diez fueran 100?, ¿y si fueran 1000 o más?

   La situación presentada con anterioridad se puede resolver sin importar el número de calificaciones; el "problema" es que por cada nuevo incremento en el número de calificaciones tendrían que agregarse nuevas variables con sus respectivos identificadores, además de modificar el código que realiza las tareas solicitadas (lectura de datos, procesamiento, etcétera) para considerar a estas nuevas variables. Dicha labor, además de tediosa es ardua, pero sobre todo, ineficiente.

   Con base en lo anterior, debería ser claro que se requiere de algo más, se necesita otra cosa, algo que permita manejar mejor y de manera general este tipo de problemas. Pues bien, eso que hace falta es lo que se conoce como arreglos.

   Un arreglo es una estructura de datos con un tamaño determinado que agrupa elementos del mismo tipo de datos, los cuales están relacionados por un nombre (identificador) y se distinguen por uno o más índices.

   En entradas subsecuentes se analizarán los arreglos de una dimensión, también conocidos como vectores, los arreglos de dos dimensiones o matrices, los arreglos de tres dimensiones, y una generalización para los arreglos de n dimensiones.


15 de noviembre de 2018

Torres de Hanoi.

   Existen varias versiones y variantes del problema de las torres de Hanoi, pero la versión clásica y más común es la siguiente:
Se tienen tres postes, normalmente denotados como A, B y C, y en uno de ellos (poste de origen, usualmente A) se tienen apilados n discos de diferentes diámetros. 
El problema consiste en pasar los n discos del poste origen al poste destino (usualmente C) manteniendo en todo momento las siguientes reglas:
  1. Sólo se puede mover un disco cada vez.
  2. Un disco de mayor diámetro no puede estar sobre uno de menor diámetro.
  3. Sólo se puede mover el disco que se encuentre en la parte superior de cada poste.
   Ahora bien, para el caso de n = 1, si A es el poste origen, C el poste destino, y B el poste auxiliar, la solución es directa:

Mover el disco 1 del poste A al poste C.

   Para el caso de n = 2, si A es el poste origen, C el poste destino, y B el poste auxiliar, la solución está dada por los siguientes movimientos de discos, donde el disco 1 es el más pequeño y el disco 2 es el más grande:

Mover el disco 1 del poste A al poste B
Mover el disco 2 del poste A al poste C
Mover el disco 1 del poste B al poste C

   Para el caso de n = 3, si A es el poste origen, C el poste destino, y B el poste auxiliar, la solución está dada por los siguientes movimientos de discos, donde el disco 1 es el más pequeño y el disco 3 es el más grande:

Mover el disco 1 del poste A al poste C
Mover el disco 2 del poste A al poste B
Mover el disco 1 del poste C al poste B
Mover el disco 3 del poste A al poste C
Mover el disco 1 del poste B al poste A
Mover el disco 2 del poste B al poste C
Mover el disco 1 del poste A al poste C

   Para visualizar mejor la solución, se sugiere como ejercicio realizar los dibujos que reflejen la naturaleza de estos movimientos. Adicionalmente, se sugiere realizar la misma labor para cuatro y cinco discos. Hágalo antes de continuar.

   Si realizó lo sugerido en el párrafo anterior, quizá haya identificado algunos patrones:
  • Para el caso de cuatro discos, se tiene la situación inicial sugerida por la siguiente figura:
  • Para el caso de cuatro discos (en general para el caso de n discos), después de algunos movimientos se tiene la situación presentada en la siguiente figura, en la que se observan los n - 1 discos colocados en la posición auxiliar B, y el disco de mayor diámetro listo para ser movido a su posición final:
  • El siguiente paso es el movimiento del disco de mayor diámetro de la posición origen A hacia su posición final de destino (C). Observe que los discos en la posición auxiliar no se afectan, como lo muestra la siguiente figura:
  • Ahora bien, de la figura anterior puede notarse lo siguiente: en la posición B están los n - 1 discos restantes, es decir, el problema original de n discos pero simplificado (al menos por un disco), por lo que, si se toma como nuevo origen la posición B, se mantiene el destino en la posición C y se hace que la posición auxiliar sea ahora A, se tiene la misma situación del problema original pero con menos discos y con nuevos roles respecto a la posición de origen y la auxiliar.
   En resumen el problema se reduce ahora a mover los n - 1 discos de la posición B a la posición C utilizando a la posición A como auxiliar, y si se repite el procedimiento, se obtendrá eventualmente la situación presentada en la siguiente figura:


   De lo anterior se pueden deducir las relaciones de recurrencia y el caso base para dar solución recursivamente al problema de las torres de Hanoi. De manera general, se presenta a continuación el algoritmo de las torres de Hanoi:

Algoritmo mover_disco(n: entero, origen: character, destino: character, auxiliar: character)
Inicio
            if (n == 1)
                        imprimir mover el disco n de la posición origen a la posición destino
            else
                       
mover_disco(n - 1, origen, auxiliar, destino)
                       
imprimir mover el disco n de la posición origen a la posición destino
                       
mover_disco(n - 1, auxiliar, destino, origen)
            end if
Fin

   Observe que el algoritmo anterior es una descripción en pseudo código de los patrones identificados en el texto. Por otro lado, si se realizó el ejercicio sugerido con anterioridad, un análisis cuidadoso deberá llevar al lector a la identificación de dichos patrones.

Consideraciones adicionales.
   En resumen, las implementaciones recursivas son en general ineficientes en tiempo y espacio, pero muy elegantes y simples una vez que se tiene identificada la relación de recurrencia. En este sentido, siempre que pueda identificar un enfoque iterativo que sea sencillo de implementar, opte por él.

   Por otro lado, el problema de las torres de Hanoi es un ejemplo ineludible de que por medio de la recursividad es posible solucionar problemas complejos de manera bastante sencilla, aunque el precio a pagar sea la eficiencia en tiempo y espacio de memoria. Intente resolver el problema de las torres de Hanoi de manera iterativa para tener una mejor comprensión de lo anterior, y experimente directamente y de primera mano el eterno conflicto (tradeoff) computacional entre espacio vs. tiempo, y eficiencia vs. simplicidad.

9 de noviembre de 2018

Ejercicios selectos (recursividad).

  1. En la entrada Recursividad (Definición y conceptos), se presenta y describe brevemente una figura geométrica generada mediante un proceso recursivo potencialmente infinito. Siguiendo lo descrito en dicha entrada, dibuje en papel la figura geométrica correspondiente de Nivel 5.
  2. Haga un análisis de los llamados y retornos recursivos como los realizados en la entrada "Soluciones iterativas vs. recursivas" para los llamados de función factorial(5) y factorial(6).
  3. Escriba un programa que, a través de una función recursiva, calcule el producto de dos números enteros positivos a y b por medio de sumas sucesivas, esto es: a * b = a + a + . . . + a, donde a se suma tantas veces como lo indique b. Para este ejercicio, asuma que b siempre será positivo, la idea de toda esta sección es poner en práctica la recursividad más que el análisis de todos los casos posibles para a y b.
  4. Realice en una hoja de papel el árbol de recursividad que se genera para el cálculo de fibonacci(5) y fibonacci(6) como el que  se mostró en "Soluciones iterativas vs. recursivas".
  5. Escriba un programa que, a través de una función recursiva, calcule el cociente entero de dos números enteros positivos a y b por medio de restas sucesivas, esto es a / b ==>
    1.   a - b = c1, si c1 >= b, entonces
    2. c1 - b = c2, si c2 >= b, entonces
    3. c2 - b = c3, si c3 >= b, entonces
    4.         .     .     .                                         donde el cociente estará dado por el número de veces que se haya podido repetir el procedimiento descrito.
  6. Escriba un programa que, a través de una función recursiva, calcule la potencia de dos números enteros positivos a y b por medio de productos sucesivos, esto es: a^b = a * a * .  .  . * a, donde a se multiplica tantas veces como lo indique b.
  7. Compare las versiones recursivas con las versiones iterativas descritas en el blog y analícelas. Simplemente por la lectura del código, ¿qué versión representa una mejor abstracción de las definiciones matemáticas? Realice diferentes ejecuciones y determine qué versión tiene un mejor rendimiento.
  8. Tanto el factorial como la serie de Fibonacci crecen de manera exponencial y no hay tipo de dato que las pueda contener. En el lenguaje de programación C, los tipos de datos numéricos dividen su rango tanto en números positivos como en números negativos, por lo que su capacidad de almacenamiento se ve, por decirlo de alguna manera, dividida. Sin embargo, existe el modificador unsigned (sin signo), que instruye al compilador para usar todo el rango de bits del tipo de dato para representar números positivos incluyendo el cero. Pruebe con este modificador de tipo de datos, documéntese en su funcionamiento y su especificador de formato, y amplíe la capacidad de representación para los resultados de los ejemplos estudiados en el blog.
  9. En base a lo indicado en el ejercicio anterior, calcule con la versión iterativa y recursiva respectivamente, fibonacci(52), ¿qué observa? ¿qué puede deducir?
  10. En base a lo expuesto en el blog y basándose en el Ejemplo 5.5, determine el tiempo en segundos que tarda la función de Fibonacci en determinar, por ejemplo, fibonacci(49) en sus respectivas versiones: iterativa y recursiva.
  11. Escriba un programa que, en base al algoritmo descrito para las Torres de Hanoi, resuelva dicho problema por medio de una función recursiva. Note que es un proceso de simple traducción al lenguaje C, ya que tanto las relaciones de recurrencia, como el criterio de paro están dados en el algoritmo.
  12. Basándose en el ejercicio anterior y en el Ejemplo 5.5, determine el tiempo en segundos que tarda la función que da solución al problema de las torres de Hanoi para 20, 35 y 50 discos por ejemplo.


24 de octubre de 2018

Estructuras de control combinadas.

   A continuación se presenta un programa un poco más elaborado. El Ejemplo 3.17, utiliza la estructura de selección switch anidada dentro de una estructura de repetición while; también se utiliza la función getchar (línea 13), la cual regresa un carácter leído desde la entrada estándar.

   El prompt de la línea 11 indica que para terminar la entrada de datos se escriba EOF, el cual es un acrónimo de End Of File.

   Si los datos se procesan desde un archivo (y para este ejemplo es posible sin realizar cambio alguno en el código), la marca EOF se encontrará al final del mismo; pero si los datos se procesan desde el teclado, la marca de EOF tendrá que ser simulada: en GNU/Linux se simula con la combinación de teclas Ctrl+D, mientras que en Windows la combinación es Ctrl+Z.

   Observe la sentencia de la línea 13, la cual es un ejemplo muy usual de la forma de escribir expresiones y sentencias en C, ya que la expresión condicional del ciclo while está combinada con una expresión de asignación. La sentencia podría interpretarse en nuestro lenguaje como: “Obtén el siguiente carácter de la entrada estándar, asígnalo a simbolo, y si es distinto de EOF, ejecuta el grupo de sentencias del ciclo”.

   Note que la variable simbolo es de tipo entero (int). Esto se debe a que la función getchar regresa el número entero que representa al símbolo (carácter) procesado.

   Por otro lado, la estructura switch se encarga de hacer una comparación del dato contenido en simbolo, contra los casos (case) de interés: las vocales mayúsculas o minúsculas. Note también la forma en que se han expresado los casos (case), ya que no se ha hecho uso de ningún operador lógico (de hecho, intencionalmente no han sido presentados), pero las sentencias de las líneas 15, 16 y 17 podrían interpretarse en nuestro lenguaje como: “En caso de que simbolo sea una 'A'  o una 'a', incrementa el contador de a's y termina la comparación”. Las líneas 18-29 tendrían una interpretación análoga.

   Observe también que los símbolos 'A' y 'a' han sido puestos entre comillas simples, lo cual, además de auto documentar mejor el código fuente, sirve para que el compilador traduzca el código de dichos símbolos, en su correspondiente representación numérica, y puedan así ser comparados con el valor regresado por la función getchar.

   En resumen, la estructura while se encarga de repetir la lectura de datos y de enviarle los datos a la estructura switch mientras el dato procesado sea distinto de EOF; por otro lado, la estructura switch se encarga de determinar si el dato procesado es o no una vocal. Si lo es, la contabiliza en el contador correspondiente, y si no, lo ignora (default de la línea 30).

   Una muestra de la ejecución del Ejemplo 3.17 con datos procesados desde el teclado se muestra en la siguiente figura:

Una posible salida del Ejemplo 3.17.
 
   Hasta ahora ha sido posible resolver todos los ejemplos y ejercicios sin el uso de operadores lógicos, de hecho esa ha sido la intención, sin embargo, el uso de operadores lógicos resulta indispensable para resolver de manera más sencilla diferentes tipos de problemas. La tabla siguiente muestra los operadores lógicos en C.

Operador    Descripción
               &&          Conjunción (And)
              |  |           Disyunción (Or)
              !            Negación (Not)

   Finalmente, compare el Ejemplo 3.17 con el Ejemplo 3.17_1. Este último hace exactamente lo mismo que el primero pero con una estructura de control if-else anidada y muestra también el uso de los operadores lógicos dentro de una expresión de tipo condicional.
 

13 de septiembre de 2018

Consideraciones adicionales con archivos de texto.

   Todos los ejemplos anteriores relacionados con archivos han recorrido de principio a fin el archivo que procesan pero, ¿qué sucede si se necesita volver a procesar nuevamente los datos del archivo?

   Dentro de la información indispensable que se encuentra representada en la estructura FILE para el control del archivo asociado se encuentra, como se mencionó con anterioridad, un indicador de posición, el cual hace referencia a la posición del último dato procesado en el archivo.

   Cuando un archivo ha sido procesado de principio a fin, el indicador de posición se encontrará ubicado al final del mismo, de tal forma que si por alguna razón se procesara de nuevo el archivo para lectura por ejemplo, se estaría en la situación de una lectura vacía, dando la impresión de que el archivo no contiene datos aún cuando sí los tenga, debido precisamente a que el indicador de posición del archivo se encuentra al final.

   Una posible solución a la problemática anterior sería cerrar el archivo, abrirlo nuevamente y procesarlo; sin embargo, el abrir y cerrar un archivo sólo para regresar el indicador de posición a su posición inicial es, además de ineficiente, un pésimo enfoque de solución: ¿qué sucede si el archivo tendrá un uso intensivo?, ¿es práctico estarlo abriendo y cerrando frecuentemente?

   La función rewind coloca el indicador de posición del archivo al inicio del archivo. Así, un llamado como "rewind(archivoPtr);" coloca el indicador de posición del archivo referido por archivoPtr al inicio de éste, de tal forma que se puedan procesar nuevamente el conjunto de datos almacenados en él, tal y como si el archivo se hubiera abierto por primera vez.

   Se deja como ejercicio para el lector que modifique el programa del Ejemplo 9.3 para que presente en la salida estándar dos veces, una seguida de otra, el archivo procesado. La idea de este ejercicio es que pueda practicar el uso de la función rewind descrita brevemente con anterioridad.

   Finalmente, debido a que la función fprintf es equivalente a la función printf, quizá se haya preguntado si es posible escribir en un archivo de texto otros tipos de datos además de cadenas, esto es, ¿es posible escribir en un archivo de texto otros tipos de datos como los siguientes:

fprintf(archivoPtr, "%d\n%s\n%s\n%.2f\n", num, nombre, direccion, salario);

donde num es de tipo int, nombre y direccion son cadenas, y salario es de tipo float?

   La respuesta es sí. Sin embargo, debe tomar en cuenta que cualquier tipo de dato, al ser almacenado en un archivo de texto, pierde su representación como tipo de dato y pasa a ser sólo una sucesión de caracteres dentro del archivo, de tal forma que al leer num o salario por ejemplo, se podrían leer como cadenas, y esto sería igual de válido que leerlos como un int o como un float. Por lo tanto, las sentencias:

            fscanf(archivoPtr, "%d %s %s %f", &num, nombre, direccion, &salario);
           fscanf(archivoPtr, "%f %s %s %f", &numero, nombre, direccion, &salario);
            fscanf(archivoPtr, "%s %s %s %s", cad1, nombre, direccion, cad2);

serían igualmente válidas, suponiendo que numero fuera de tipo float y que cad1 y cad2 fueran cadenas; aunque la más congruente sería la primera, en función de los datos que, según la idea planteada en esta entrada, se escribieron en el archivo referido por archivoPtr.

   Cuando trabaje con archivos de texto, no olvide tener en cuenta todas las consideraciones mencionadas en esta entrada.


3 de agosto de 2018

Comentarios y sugerencias.

   Esta entrada tiene como intención principal, recolectar aquellos comentarios que los lectores deseen hacer acerca de algún punto o tema relacionado con el blog.

   En general he tratado de cuidar, en todo el blog, aquellos aspectos que hagan más accesible tanto la lectura como su correspondiente comprensión; sin embargo, es posible que algunas cosas que podrían parecer claras no lo sean del todo para la amplia diversidad de lectores, por lo que agradeceré de manera anticipada al lector bien intencionado, me informe de cualquier error identificado o sugerencia que permita mejorar el contenido.

   Dejo abierta la sección de comentarios para recibirlos y poder dar respuesta, en la medida de mis posibilidades, a cada uno de ellos.

   Quedo en deuda con todas las personas que me han escrito y ayudado a mejorar con sus comentarios, así como con los estudiantes que me han hecho valiosas aportaciones en más de un sentido.
 
    Agradezco particular y encarecidamente con el más profundo de mis afectos a la Ing. Thelma García Reyes, quien ha sido piedra angular tanto en el desarrollo inicial como en la revisión de lo que en su momento fue un libro y ahora un blog; sin su inestimable ayuda y entusiasmo cuando el desánimo se montaba en mis espaldas, seguiría atorado en los escollos del desarrollo y elaboración de imágenes, revisión, correcciones, y un largo etcétera. 
 
    En este mismo sentido agradezco y aprecio el apoyo del Dr. Tomás Balderas Contreras, quien no sólo me hizo el favor de escribir el prólogo del libro, sino que sus valiosos comentarios respecto a algunos de los aspectos técnicos del contenido, son insalvables.
 
    Aún con lo anterior, cualquier error que persista en este blog y en cualquier parte de su contenido es únicamente atribuible al autor.