Cómo formular algoritmos con refinamiento descendente paso a paso: Estudio del caso 2 (repetición controlada por centinela)

Generalicemos el problema de promedios de clase: considere el problema siguiente:

Desarrolle un programa de promedios de clase que pueda procesar un número arbitrario de notas, cada vez que se ejecute el programa.

En el primer ejemplo de promedio de clase (Estudio de caso 1), se sabía por anticipado el número de notas (5). En este ejemplo, no se da ninguna indicación de cuantas notas se tomarán. El programa debe ser capaz de procesar un número arbitrario de notas. ¿Cómo podrá el programa determinar cuándo parar la captura de notas? ¿Cuándo sabrá que debe calcular e imprimir el promedio de clase?

Una forma de resolver este problema es utilizar un valor especial llamado un valor centinela (también conocido como valor señal, un valor substituto, o un valor bandera) que indicará "fin de la captura de datos". El usuario escribirá notas hasta que haya capturado todas las notas. Entonces escribirá un valor centinela, a fin de indicar que ha sido introducida la última nota. La repetición controlada por centinela a menudo se llama repetición indefinida, porque antes de que se empiece a ejecutar el ciclo el número de repetición no es conocido.

Claramente, el valor centinela deberá ser seleccionado de tal forma que no se confunda con algún valor de entrada aceptable. Dado que normalmente las notas de un examen son enteros no negativos, para este problema, -1 resulta un valor centinela aceptable. Entonces, una ejecución del programa de promedios de clase pudiera procesar un flujo de entradas como 95, 96, 75, 74, y -1. El programa a continuación calcularía e imprimiría el promedio de clase para las notas 95, 96, 75, y 74 (-1 es el valor centinela y, por lo tanto, no debe entrar en el cálculo de promedio).

Error

Seleccionar un valor centinela que también pudiera resultar un valor legitima de datos.

Enfocamos el programa de promedios de la clase con una técnica conocida como refinación descendente paso a paso, técnica que resulta esencial al desarrollo de los programas bien estructurados. Empezamos con una representación en seudocódigo de lo más general:

Determinar el promedio de clase correspondiente al examen.

Lo más general es un enunciado que representa la función general del programa. Como tal, lo más general es en efecto, una representación completa de todo el programa. Desafortunadamente, lo más general (como en este caso) rara vez transmite una cantidad suficiente de detalle a partir del cual se pueda escribir un programa en C para calculadoras CASIO fx-9860G Series.

Por lo cual empezamos con el proceso de refinación. Dividimos lo más general en una serie de tareas más pequeñas y las enlistamos en el orden en el cual deberán ser ejecutadas. Esto da como resultado el siguiente primer refinamiento:

Inicializar variables
Captura, suma y cuenta de las notas del examen 
Calcular e imprimir el promedio de clase

Aquí, sólo se ha utilizado la estructura de secuencia -los pasos enlistados deberán ser ejecutados en orden, uno después del otro.

Observación

Cada refinamiento, al igual que la misma parte más general, debe ser una especificación completa del algoritmo; cambiando sólo el nivel de detalle.

Para seguir al siguiente nivel de refinamiento, es decir, el segundo nivel de refinamiento, utilizaremos variables específicas. Necesitamos un total acumulado de los números, una cuenta de cuántos números han sido procesados, una variable para recibir el valor de cada calificación conforme es introducida, y una variable que contenga el promedio calculado. El enunciado en seudocódigo será:

Inicializar variables

pudiera ser refinado como sigue:

Inicializar total a cero
Inicializar contador a cero

Note que sólo el total y el contador deben ser inicializados; las variables promedio y calificación (para el promedio calculado y la captura del usuario, respectivamente) no necesitan ser inicializados, porque sus valores se escribirán mediante el proceso de lectura destructiva. El enunciado en seudocódigo.

Captura, suma y cuenta de las notas del examen

requiere de una estructura de repetición (es decir, de un ciclo), que introduzca sucesivamente cada calificación. Dado que no sabemos por anticipado cuantas calificaciones serán procesadas, utilizaremos la repetición controlada por centinela. El usuario escribirá notas uno por uno. Una vez que haya escrito la última nota, el usuario escribirá el valor centinela. Después de haber capturado cada calificación el programa hará una prueba buscando este valor, y dará por terminado el ciclo, cuando el centinela haya sido escrito. La refinación del enunciado precedente en seudocódigo, resulta entonces

Escriba la primera nota
While el usuario no haya introducido todavía el centinela
    Añada esta nota al total acumulado
    Añada uno al contador de notas
    Introduzca la siguiente nota (posiblemente el centinela)

Note que en seudocódigo, para formar el cuerpo de la estructura while, no utilizamos llaves alrededor de un conjunto de enunciados. Simplemente hacemos una sangría de todos estos enunciados bajo while, para mostrar que todos ellos corresponden al while. Repetimos, el seudocódigo es sólo una ayuda informal de desarrollo de programas.

El enunciado en seudocódigo

Calcular e imprimir el promedio de clase

puede ser refinado como sigue:

if el contador no es igual a cero
    Haga que el promedio sea igual al total dividido por el contador 
    Imprima el promedio 
else
    Imprima "No se han escrito notas" 

Note que estamos teniendo cuidado aquí de probar la posibilidad de división entre cero un error fatal que, sí no es detectado causaría que el programa fallara (a menudo llamado *colapso del programa"). Entonces el segundo refinamiento completo será

Inicializar total a cero
Inicializar contador a cero

Escriba la primera nota
While el usuario no haya introducido todavía el centinela
    Añada esta nota al total acumulado
    Añada uno al contador de notas
    Introduzca la siguiente nota (posiblemente el centinela)


if el contador no es igual a cero
    Haga que el promedio sea igual al total dividido por el contador 
    Imprima el promedio 
else
    Imprima "No se han escrito notas" 

Error

Cualquier intento de dividir entre cero causará un error fatal.

Consejo

Al ejecutar una división por una expresión cuyo valor pudiera ser cero, pruebe de forma explícita este caso y manéjelo apropiadamente en su programa (como sería la impresión de un mensaje de error), en vez de permitir que ocurra un error fatal.

Dentro del seudocódigo aparecen algunas líneas totalmente en blanco para mayor legibilidad. De hecho, las líneas en blanco separan estos programas en sus diferentes fases.

Note

Muchos programas pueden ser divididos lógicamente en tres fases: una fase de inicialización, que inicializa las variables del programa; una fase de proceso, que captura valores de datos y ajusta las variables de programa correspondientemente; y una fase de terminación, que calcula e imprime los resultados finales.

El algoritmo en seudocódigo final resuelve el problema más general de promedio de clase. Este algoritmo fue desarrollado después de sólo dos niveles de refinamiento. A veces se requieren de más niveles.

Note

El programador termina el proceso de refinamiento descendente paso a paso cuando el algoritmo en seudocódigo queda especificado con suficiente detalle para que el programador pueda convertirlo de seudocódigo a lenguaje C para calculadoras CASIO. A partir de ahí la implantación del programa C para calculadoras CASIO es por lo regular sencilla.

A continuación aparece el programa en C, con su respectivo #include "inout.h" actualizado para calculadoras CASIO fx-9860G Series y una ejecución de muestra.

Archivo inoout.h


/* funciones de entrada y salida para Casio 9860G series*/

#include "fxlib.h"
#include "stdio.h"

/*lee desde la calculadora casio hasta presionar EXE,
se almacena como cadena*/
void Scan (char string[]){
    unsigned int i, key;
    char aux[1];

    i = 0;
    while(GetKey (&key)) {
    if (key == KEY_CHAR_MINUS){
        string[i++] = 0x2D; // 0x2D muestra en pantalla el caracter menos ' - '
        aux[0] = 0x2D;
    }
    else{
        string[i++] = key;
        aux[0] = key;
    }
    Print((unsigned char*) aux);
    if (key == KEY_CTRL_EXE)
        break;
    }
    string[i] = '\0';
}

El programa para la calculadora


/*****************************************************************/
/*                                                               */
/*   CASIO fx-9860G SDK Library                                  */
/*                                                               */
/*   File name : centinel.c                                 */
/*                                                               */
/*   Copyright (c) 2006 CASIO COMPUTER CO., LTD.                 */
/*                                                               */
/*****************************************************************/
#include "fxlib.h"
#include "stdio.h"
#include "inout.h"

//****************************************************************************
//  AddIn_main (Sample program main function)
//
//  param   :   isAppli   : 1 = This application is launched by MAIN MENU.
//                        : 0 = This application is launched by a strip in eACT application.
//
//              OptionNum : Strip number (0~3)
//                         (This parameter is only used when isAppli parameter is 0.)
//
//  retval  :   1 = No error / 0 = Error
//
//****************************************************************************

/* Programa de promedio de clase mediante repetición
controlada por centinela */

int AddIn_main(int isAppli, unsigned short OptionNum)
{
    float promedio;      /*nuevo tipo de dato*/
    int contador, nota, total;
    unsigned int key;
    char snota[17], spromedio[17];

    Bdisp_AllClr_DDVRAM();

    /*fase de inicialización */
    total = 0;
    contador = 1;

    /*fase de procesamiento */
    locate (1, contador);
    Print((unsigned char*) "Nota: -1 fin: ");
    Scan((unsigned char*) snota);

    while (atoi(snota) != -1){
        total = total + atoi(snota);
        contador = contador  + 1;
        locate (1, contador);
        Print((unsigned char*) "Nota: -1 fin: ");
        Scan((unsigned char*) snota);
    }

    /*fase final */
    if (contador != 0){
        promedio = (float) total / (contador - 1);
        sprintf(spromedio, "El promedio es %.2f", promedio);
        locate (1, contador + 1);
        Print((unsigned char*) spromedio);
    }
    else
        Print((unsigned char*) "No hay notas");
 
    while(1){
        GetKey(&key);
    }
}


//****************************************************************************
//**************                                              ****************
//**************                 Notice!                      ****************
//**************                                              ****************
//**************  Please do not change the following source.  ****************
//**************                                              ****************
//****************************************************************************


#pragma section _BR_Size
unsigned long BR_Size;
#pragma section


#pragma section _TOP

//****************************************************************************
//  InitializeSystem
//
//  param   :   isAppli   : 1 = Application / 0 = eActivity
//              OptionNum : Option Number (only eActivity)
//
//  retval  :   1 = No error / 0 = Error
//
//****************************************************************************
int InitializeSystem(int isAppli, unsigned short OptionNum)
{
    return INIT_ADDIN_APPLICATION(isAppli, OptionNum);
}

#pragma section



A pesar de que sólo se han escrito calificaciones en enteros, el cálculo de promedio probable producirá un número decimal con un punto decimal. El tipo int no puede representar tal número. Para manejar números con puntos decimales el programa introduce el tipo de datos float (también conocidos como números de punto flotante) así como introduce un operador especial conocido como un operador "cast" para manejar el cálculo de promedio. Estas características se explican en detalle después de presentar el programa.

Note el enunciado compuesto en el ciclo while, para que se| ejecuten los cinco enunciados dentro del ciclo son necesarias las llaves. Sin las llaves, los últimos tres enunciados en el cuerpo del ciclo quedarían fuera del mismo, haciendo que la computadora interpretase este código en forma incorrecta, como sigue:

while (atoi(snota) != -1)
    total = total + atoi(snota);
contador = contador  + 1;
locate (1, contador);
Print((unsigned char*) "Nota: -1 fin: ");
Scan((unsigned char*) snota);
 
Esto causaría un ciclo infinito, en tanto el usuario no introdujera -1 para la primera nota.

Consejo

En un ciclo controlado por centinela, las indicaciones solicitando la introducción de datos, deberían recordar de forma explícita al usuario cual es el valor centinela.

Los promedios no siempre resultan en valores enteros. A menudo, un promedio es un valor como 7.2 o -93.5, conteniendo una parte fraccionaria. Estos valores se conocen como números de punto flotante, y se representan por el tipo de datos float. La variable promedio se declara del tipo float, a fin de capturar el resultado fraccionario de nuestro cálculo. Sin embargo, el resultado del cálculo total / contador es un entero, porque tanto total como counter son ambas variables enteras. La división de dos enteros resulta en una división de enteros, en la cual se pierde la parte fraccionaria del cálculo (es decir, se trunca). Dado que este cálculo se ejecuta primero, se pierde la parte fraccionaria, antes de que el resultado sea asignado a promedio. A fin de producir un cálculo de punto flotante con valores enteros, debemos crear valores temporales que sean números de punto flotante para el cálculo. C proporciona un operador cast unario para esta tarea. El enunciado

    promedio = (float) total / (contador - 1);

Incluye el operador cast (float) que crea una copia temporal de punto flotante de su propio operando, total. El uso de un operador cast de esta forma se conoce como conversión explícita. El valor almacenado en total sigue siendo un entero. El cálculo ahora consiste en un valor de punto flotante (la versión temporal float de total) dividida por el valor entero almacenado en contador. El compilador de C para calculadoras CASIO sólo sabe como evaluar expresiones en donde los tipos de datos de los operandos sean idénticos. A fin de asegurarse que los operandos sean del mismo tipo, el compilador lleva a cabo una operación denominada promoción (también conocida como conversión implícita) sobre los operandos seleccionados. Por ejemplo, en una expresión que contenga los tipos de datos int y float, el estándar ANSI especifica que se hacen copias de los operandos int y se promueven a float. En nuestro ejemplo, después de hacer una copia de contador y promoverla a float, se lleva a cabo el cálculo y el resultado de la división de punto flotante se asigna a promedio. El estándar ANSI incluye un conjunto de reglas para la promoción de operandos de diferentes tipos.

Los operadores cast están disponibles para cualquier tipo de datos. El operador cast se forma colocando paréntesis alrededor del nombre de un tipo de datos. El operador cast es un operador unario, es decir, un operador que utiliza un operando. Anteriormente estudiamos los operadores aritméticos binarios. C también acepta versiones unarias de los operadores más (+) y menos (-), de tal forma que el programador puede escribir expresiones como -7 o +5. Los operadores cast se asocian de derecha a izquierda, y tienen la misma precedencia que los otros operadores unarios, como el unario + y el unario -. Esta precedencia es de un nivel superior al de los operadores multiplicativos *, /, y %, y de un nivel menor que el de los paréntesis.

El programa anterior utiliza un especificador de conversión para sprintf igual a %. 2f para la impresión del valor de promedio. La f indica que deberá ser impreso un valor de punto flotante. El .2 es la precisión con la cual dicho valor se deberá desplegar. Indica que el valor será desplegado con 2 decimales a la derecha del punto decimal. Si se utiliza el especificador de conversión %f (sin especificar precisión), se utilizará la precisión preestablecida por omisión de 6 exactamente igual a si se utilizara un especificador de conversión %.6f. Cuando los valores de punto flotante se imprimen con precisión, el valor impreso se redondea al número indicado de posiciones decimales. El valor en memoria se conserva sin alteración. Cuando son ejecutados los enunciados siguientes, se imprimen los valores 3.45 y 3.4

sprintf (spromedio, "%.2f", 3.446);  /* almacena en spromedio 3.45 */
sprintf (spromedio, "%.1f", 3.446);  /* almacena en spromedio 3.4   */

Error

Utilizar números de punto flotante, de tal forma de que se suponga que están representados con precisión, puede llevar a resultados incorrectos. Los números de punto flotante son representados sólo en forma aproximada por la mayor parte de las computadoras y calculadoras.

Precaución

No compare valores de punto flotante buscando igualdad.

A pesar del hecho de que los números de punto flotante "no son siempre 100% precisos", tienen muchas aplicaciones. Por ejemplo, cuando hablamos de una temperatura corporal "normal" de 98.6°F (37°C), no es necesario ser preciso hasta un gran número de dígitos. Cuando vemos la temperatura en un termómetro y la leemos como 98.6, pudiera en realidad ser 98.5999473210643. El punto aquí es que el expresar este número como 98.6 es suficiente para la mayor parte de las aplicaciones.

Otra forma en que se desarrollan los números de punto flotante es mediante la división. Cuando dividimos 10 entre 3, el resultado es 3.3333333...con la secuencia de 3 repitiéndose en forma infinita. La computadora asignará cierta cantidad de espacio para contener un valor como éste, por lo que claramente el valor de punto flotante almacenado sólo puede ser una aproximación.

No hay comentarios:

Publicar un comentario