23 de noviembre de 2016

Ejercicios selectos (arreglos).

  1. Modifique el Ejemplo 6.1 de tal forma que imprima los 15 elementos del arreglo arreglo antes del ciclo for de la línea 13, con la finalidad de verificar que los elementos no inicializados explícitamente, son implícitamente inicializados a 0, tal y como se describe en el blog.
  2. Para el Ejemplo 6.1 suponga que N es 5 (línea 7) y que los inicializadores (línea 10) son: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ¿Qué sucede si existen más inicializadores que número de elementos para el arreglo? Haga un programa que compruebe su respuesta.
  3. Considere el Ejemplo 6.2 ¿Qué sucede si en lugar de que N sea 5, se cambia para que sea 500 o 5000?
  4. Basándose en el Ejemplo 6.3, haga un programa que procese arreglos de tipo float, no olvide modificar también los especificadores de formato correspondientes.
  5. Basándose en el ejercicio anterior, haga un programa que almacene un grupo de n calificaciones (cambie el tipo de dato a float), y determine con funciones:
    1. Cuál es la menor (float menor(float a[ ], int n);).
    2. Cuál es la mayor (float mayor(float a[ ], int n);).
    3. Cuáles y cuántas son aprobatorias y reprobatorias (void imprimeAprobatoriasReprobatorias(float a[ ], int n, float criterio);).
    4. Su media aritmética o promedio (float promedio(float a[ ], int n);).
     6. Escriba un programa que defina y pruebe una función con el siguiente prototipo: void rellenaArreglo(int a[ ], int n, int rango);. La función deberá inicializar los n elementos del arreglo a, con números aleatorios definidos entre 0 y rango - 1. Así por ejemplo, si rango es 100, el arreglo a deberá contener n números entre 0 y 99.
     7. Escriba un programa que defina y pruebe una función con el siguiente prototipo: void inicializaArreglo(int a[ ], int n, int valor);. La función deberá inicializar los n elementos del arreglo a, con el valor valor.
     8. Escriba un programa que defina y pruebe una función con el siguiente prototipo: void imprimeArreglo(const int a[ ], int n, int c);. La función deberá imprimir en la salida estándar los n elementos del arreglo a, con c elementos por renglón.
     9. Escriba un programa que defina y pruebe una función con el siguiente prototipo: void copiaArreglo(const int original[ ], int copia[ ], int n);. La función deberá copiar los n elementos del arreglo original en el arreglo copia.
     10. Escriba un programa que mida el tiempo en segundos para cada una de las funciones del Ejemplo 6.4. La intención de este ejercicio es determinar si existe o no una mejora en la eficiencia con las modificaciones descritas en el blog, ya que el algoritmo de ordenamiento no cambia, sólo se ha hecho, en principio, más eficiente. Para ello, su programa deberá:
  • Declarar dos arreglos de al menos 50,000 elementos cada uno.
  • Utilizar una función para inicializar uno de ellos con números aleatorios (Ejercicio 6).
  • Utilizar una función para copiar el arreglo inicializado en el segundo arreglo declarado (Ejercicio 9).
  • Enviar uno de los arreglos a la función burbuja y medir el tiempo que le toma ordenar los elementos.
  • Enviar el otro arreglo (la copia) a la función burbujaMejorado y medir el tiempo que le toma ordenar los elementos.
     11. Escriba un programa que pruebe las funciones del Ejemplo 6.5. Pruebe con datos leídos desde el teclado, con datos aleatorios, y verifique que las funciones de búsqueda trabajan de acuerdo a lo esperado. Nota: No olvide que la búsqueda binaria tiene como pre requisito que el conjunto de datos esté ordenado.
     12. Realice una prueba de escritorio para la función busquedaBinaria del Ejemplo 6.5 para que busque las siguientes claves: -4, 22, 1 y 13. Utilice el siguiente conjunto de búsqueda: -20, -4, 0, -9, 7, 10, 15, 22.
     13. Una vez que haya realizado las pruebas de escritorio propuestas en el ejercicio anterior, realice una implementación recursiva de la búsqueda binaria con base en la descripción hecha en el blog respecto de ésta.

Cadenas.

      1. Siguiendo las ideas expuestas en el blog respecto a los arreglos de caracteres (cadenas), escriba un programa que defina y pruebe una función con el siguiente prototipo: void copiaCadena(const char original[ ], char copia[ ]);. La función deberá copiar los elementos de la cadena original, en la cadena copia.
     2. Escriba un programa que defina y pruebe una función con el siguiente prototipo: void invierteCadena(const char original[ ], char invertida[ ]);. La función deberá copiar invertidos o al revés los elementos de la cadena original, en la cadena invertida. Puede utilizar las funciones desarrolladas con anterioridad.
     3. Las cadenas (arreglos de caracteres) no pueden ser comparadas utilizando el operador "==". Para este ejercicio, construya una función con el siguiente prototipo: int esIgualCon(const char c1[ ], const char c2[ ]);. La función deberá comparar la cadena c1 con la cadena c2, y regresar 1 si c1 es exactamente igual a c2 y 0 en caso contrario. Nota: la función determina la coincidencia exacta, tome en cuenta que las cadenas "Hola" y "hola" son distintas aunque digan lo mismo.
     4. Un palíndromo es una expresión que representa o expresa lo mismo si se lee de izquierda a derecha que de derecha a izquierda, como por ejemplo:
  • Madam.
  • 2002.
  • Anita lava la tina.
  • Dabale arroz a la zorra el abad.
  • .  .  .
      Escriba un programa que defina y pruebe una función con el siguiente prototipo: int esPalindromo(const char c[ ]);. La función deberá determinar si los elementos de la cadena c constituyen (regresa 1) o no (regresa 0) un palíndromo. Puede apoyarse de las funciones desarrolladas hasta el momento y por ahora, asuma que los elementos de la cadena no contendrán espacios y que todos los caracteres serán introducidos en minúsculas.
      5.  Escriba un programa que defina y pruebe una función con el siguiente prototipo: int cuentaPalabras(const char c[ ]);. La función deberá contar y retornar el número de palabras de la cadena c. Considere y analice todas las formas posibles en las que puede aparecer una palabra en una frase: signos de puntuación, interrogación, admiración, etcétera.
     6. Escriba un programa que defina y pruebe una función con el siguiente prototipo: int cambiaOcurrencia(char c[ ], char viejo, char nuevo);. La función deberá cambiar el carácter viejo por el carácter nuevo en toda la cadena c todas las veces que aparezca. La función regresa el número de cambios que realizó o cero si no hubo ningún cambio.
     7. Escriba un programa que obtenga la CURP (Clave Única de Registro de Población). La CURP se obtiene de la siguiente forma (verifique estas reglas para su propia CURP):
  • Primera letra y primera vocal del primer apellido.
  • Primera letra del segundo apellido.
  • Primera letra del nombre de la persona, en casos como José o María, de ser posible, se tomará la primera letra del segundo nombre.
  • Últimas dos cifras del año de nacimiento.
  • Dos cifras del mes de nacimiento.
  • Dos cifras del día de nacimiento .
  • Sexo de la persona: H (Hombre) o M (Mujer).
  • Dos Letras, correspondientes a la entidad de nacimiento. La entidad de nacimiento se obtiene como la primera letra de la entidad y la última consonante. En caso de extranjeros es NE (Nacido en el Extranjero).
  • Consonante del primer apellido (la primera si el primer apellido empieza con vocal, la segunda si empieza en consonante).
  • Consonante del segundo apellido (la primera si el primer apellido empieza con vocal, la segunda si empieza en consonante).
  • Consonante del nombre de la persona (la primera si el primer apellido empieza con vocal, la segunda si empieza en consonante).
  • Dos dígitos para evitar duplicidad (genérelos aleatoriamente).
  • En casos en los que las personas no tienen segundo apellido, o que la letra que corresponde insertar sea la letra Ñ, se utilizará en su lugar la X. Esto lo puede hacer desde la captura de datos para facilitar las cosas.
     8. Escriba un programa que defina y pruebe una función con el siguiente prototipo: int terminaCon(const char c1[ ], const char c2[ ]);. La función deberá determinar si la cadena c1 termina con la cadena c2. Si c2 aparece al final de c1, la función regresa 1 y 0 en caso contrario. Ejemplos:
  • terminaCon("Esto es un ejemplo", "templo"); (regresa 0).
  • terminaCon("Esto es un ejemplo", "contemplo"); (regresa 0).
  • terminaCon("Esto es un ejemplo", "es un ejemplo"); (regresa 1).
  • terminaCon("Zona de desastre", "sastre"); (regresa 1).
  • terminaCon("Zona de desastre", "tu sastre"); (regresa 0).
  • terminaCon("Zona de desastre", "zona de desastre"); (regresa 0).
  • terminaCon("Zona de desastre", "La zona de desastre"); (regresa 0).
     9. Escriba un programa que defina y pruebe una función con el siguiente prototipo: int contiene(const char c1[ ], const char c2[ ]); . La función deberá determinar si la cadena c1 contiene a la cadena c2. Si c2 aparece en alguna parte de c1, la función regresa 1 y 0 en caso contrario.
     10. Escriba una función con el siguiente prototipo: void imprimeAlReves(const char c[ ]); . La función deberá imprimir al revés recursivamente la cadena referida por c. Sugerencia: utilice el modificador static.


16 de noviembre de 2016

Soluciones iterativas vs. recursivas.

   Existen dos funciones clásicas y claves que se resuelven con recursividad:
  1. La función que calcula el factorial de un número entero positivo.
  2. La función que calcula los términos de la serie de Fibonacci.
   Específicamente para estas dos funciones, el enfoque de la recursividad no es la manera más eficiente de resolverlas, pero ayudan significativamente a ilustrar el concepto de recursividad.

   Por otro lado, dichas funciones son tan clásicas que, para no incurrir en algún tipo de maldición del estilo del programa “Hola mundo!”, se revisarán necesaria y rápidamente, primero en su versión iterativa (ciclos) y posteriormente en su versión recursiva.

Factorial (Versión iterativa).
   El factorial de n denotado por n! (n >= 0) se obtiene en base a lo definido en la siguiente expresión:

n! = n * (n-1) * (n-2) * . . . * 1

de tal forma que, si n = 5 se tiene:

5! = 5 * 4 * 3 * 2 * 1 = 120

   De lo anterior puede observarse que la solución está dada por un ciclo. El Ejemplo 5.1 muestra la función que calcula de manera iterativa el factorial de un número. Note que la función asume que n >= 0.

   En base a todos los ejemplos anteriores todas las sentencias del Ejemplo 5.1 deberían ser completamente entendidas. Asegúrese de que así sea antes de continuar.

Factorial (Versión recursiva).
   Matemáticamente, la función factorial se define según lo expresado en la siguiente ecuación, para n >= 0.

   La función del Ejemplo 5.2 es esencialmente una traducción al lenguaje C de la ecuación anterior, donde a las líneas 25 y 26 se les conoce como criterio de paro o caso base de la recursividad, mientras que la línea 28 contiene el paso recursivo.

   Es importante hacer notar que en cada llamado recursivo se tiene una simplificación del problema original, lo que asegura que eventualmente se llegue al caso base de la recursividad, siempre que n >= 0.

   Las siguientes figuras ilustra el funcionamiento de la función factorial (líneas 24-29) del Ejemplo 5.2 para n = 4:
 
(a) Llamados recursivos para el factorial del número cuatro.
(b) Retornos recursivos para el factorial del número cuatro.
 
   En la figura (a) se muestran los llamados recursivos, note cómo en cada nivel de n existen valores pendientes de ser calculados debido precisamente al funcionamiento de la recursividad. Cuando finalmente se alcanza el caso base (n = 0), es posible determinar los valores que fueron quedando pendientes (las flechas de la figura (b) muestran dicho procedimiento), y calcular los resultados de n para cada nivel, los cuales son mostrados encerrados en círculos en la figura (b). Analice y estudie lo anterior hasta comprenderlo, ya que en buena medida, de ello depende la comprensión del Ejemplo 5.2.

   Es preciso mencionar que una vez que se tiene la relación de recurrencia (una relación de recurrencia es una ecuación que relaciona a los términos de una sucesión con alguno de sus predecesores) que establece tanto el criterio de paro como el (los) paso(s) recursivo(s), el proceso de traducción al lenguaje C es muy sencillo, por lo que la deducción de dicha relación de recurrencia es en realidad la parte difícil en el diseño de funciones recursivas. Para el caso de la función factorial, la relación de recurrencia está dada por la definición matemática de la expresión que la define.


   Por último, note que se ha utilizado el tipo de dato long debido a que el factorial de un número crece de manera exponencial. La salida de los Ejemplos 5.1 y 5.2 para n = 4 se muestra en la siguiente figura:

Salida de los Ejemplos 5.1 y 5.2 para n = 4.
 
Fibonacci (Versión iterativa).
   Otra función matemática clásica y clave es la función que genera la serie o sucesión de Fibonacci. La serie de Fibonacci, es la siguiente sucesión infinita de números:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, . . .

   Observe que, con excepción de los dos primeros términos, cada término de la serie se obtiene como la suma de los dos términos anteriores:

  • fib(0) = 0
  • fib(1) = 1
  • fib(2) = 1
  • fib(3) = 2
  • fib(4) = 3
  • fib(5) = 5
  • fib(6) = 8
  • fib(7) = 13
  • fib(8) = 21
  • fib(9) = 34
  • fib(10) = 55
  • fib(11) = 89
  • . . .

   De los elementos de la serie mostrados, es posible deducir que la solución puede obtenerse también mediante un ciclo.

   El Ejemplo 5.3 muestra la función que calcula de manera iterativa el número de Fibonacci para un término de la serie. Observe que la función asume que n >= 0.

   Asegúrese de entender el programa del Ejemplo 5.3 antes de continuar, particularmente la función fibonacci. Note que, al igual que antes, se ha utilizado el tipo de dato long.

Fibonacci (Versión recursiva).
   Ahora bien, matemáticamente, la función de Fibonacci que genera los términos de la sucesión de Fibonacci se define en base a lo expresado en la siguiente ecuación para n >= 0:


   Note que la función fibonacci del Ejemplo 5.4 es una traducción al lenguaje C de la ecuación anterior. Observe también que las líneas 25 y 26 implementan el criterio de paro o caso base de la recursividad, mientras que la línea 28 contiene los dos pasos recursivos involucrados en el cálculo de los términos de la serie.

   Para n >= 2 cada término de la serie se calcula como la suma de dos llamados recursivos, por lo que la función fibonacci del Ejemplo 5.4 genera un árbol de recursividad como el que se muestra en la siguiente figura para n = 4:
 
Árbol de recursividad de la serie de Fibonacci para n =4.
 
    Observe que el término fibonacci(2) se repite en la rama derecha y en la rama izquierda del árbol. Dicha repetición de cálculos aunada al overhead implicado en cada llamado de función, hace que las implementaciones recursivas sean en general ineficientes respecto del tiempo y del espacio en memoria.

   Lo anterior debe reafirmar en la mente del lector que las implementaciones recursivas de las funciones factorial y fibonacci son ineficientes, y por consiguiente, un mal enfoque de solución; sin embargo, ambas funciones ilustran de manera extraordinaria el concepto de recursividad. Por otro lado, basta con ver las definiciones matemáticas y compararlas con sus correspondientes versiones escritas en lenguaje C para darse cuenta de la elegancia, la correspondencia directa, y la simplicidad inherente a las implementaciones recursivas.

   La salida de los Ejemplos 5.3 y 5.4 para n = 4 se muestra en la siguiente figura:

Salida de los Ejemplos 5.3 y 5.4 para n = 4.
 
Investigación empírica.
   El Ejemplo 5.5 muestra un esqueleto de programa para medir el tiempo en segundos de un proceso largo.

   La línea 6 incluye la biblioteca de funciones time.h necesaria para usar las funciones time (líneas 11 y 13) y difftime (línea 16).

   La función time y el tipo time_t se han descrito brevemente con aterioridad  en el Ejemplo 3.16, basta ahora con recordar que sirven para leer la fecha y hora del sistema en donde se ejecuta el programa. Por otro lado, la función difftime calcula la diferencia en segundos que existe entre las variables comienzo y final (líneas 9, 11 y 13).

   La línea 12 representa el proceso al que se desea tomar el tiempo de ejecución en segundos. El Ejemplo 5.5 también sugiere que se incluya ahí el llamado a la función recursiva de la serie de Fibonacci con un argumento de 46 por proponer un determinado valor. Resultaría sumamente interesante probar éste y otros valores.

   El Ejemplo 5.5 como tal y por sí mismo no ejecuta ni determina nada. De hecho si lo ejecuta tal cual visualizará siempre una salida de cero segundos. Sin embargo, dicho ejemplo resultará de mucha utilidad para poder medir, de manera empírica el tiempo en segundos que le toma a una función recursiva por ejemplo, terminar sus cálculos. Aunque para los seres humanos un segundo es poco, para una computadora un segundo es muchísimo tiempo, ya que en un segundo se procesan millones de micro instrucciones en el procesador, sin embargo esta forma de medir el tiempo en segundos para algunos de procesos (como las funciones recursivas), nos proporcionarán una idea más asequible del esfuerzo requerido por parte de algunas funciones.

8 de noviembre de 2016

Bibliotecas personalizadas de funciones.

   El lenguaje de programación C incorpora muchas y muy útiles, además de variadas, funciones incluidas en sus diferentes bibliotecas pero además, a través de un mecanismo relativamente sencillo, es posible crear "bibliotecas" personalizadas de funciones, esto es, archivos que contengan funciones definidas por el programador y que puedan ser utilizadas en cualquier programa que las incluya, de manera similar a como se hace con las bibliotecas estándares del lenguaje.

   Estrictamente hablando, con el procedimiento que se describirá a continuación, no se estará creando ninguna biblioteca en el sentido más estricto del lenguaje de programación, sino que, haciendo uso del mecanismo de abstracción proporcionado por la directiva de inclusión de archivos #include, se proporciona al programador la sensación de incluir sus propias bibliotecas de funciones.

   En esta entrada se mostrará brevemente el procedimiento para crear bibliotecas personalizadas de funciones. La descripción se basará en el Ejemplo 4.2, por lo que se recomienda su revisión.

   El primer paso para crear una biblioteca personalizada de funciones, es tener las funciones definidas por el programador en un archivo nuevo e independiente. Dicho archivo debe contener al menos una función, mientras que el límite dependerá del espacio designado para el archivo en el disco que lo almacene, es decir: potencialmente ilimitado.

   Resulta sumamente conveniente crear bibliotecas personalizadas de funciones con funciones que tengan algún tipo de relación entre ellas, por lo que en realidad, la limitante en el número de funciones que pueda contener una biblioteca debería estar supeditada a la abstracción de los servicios que se espera proporcione dicha biblioteca.

   Las funciones mayor y menor del Ejemplo 4.2, se han colocado en un nuevo archivo tal y como se muestra en el Ejemplo 4.5. El nombre de archivo que contendrá la biblioteca personalizada de funciones no deberá contener espacios preferentemente, símbolos acentuados o compuestos como la letra ñ.

   Note que en esencia el Ejemplo 4.2 y el Ejemplo 4.5 son esencialmente iguales, la diferencia radica en la localización de las funciones que, para el caso de bibliotecas personalizadas, se encuentran en un archivo independiente con extensión “.h”.

   La línea 7 del Ejemplo 4.6 muestra la inclusión de la biblioteca personalizada de funciones del Ejemplo 4.5. Observe que en la directiva #include se han cambiado los corchetes angulares (< >) por comillas (“ ”) lo cual, además de diferenciar una biblioteca personalizada de funciones de una biblioteca del lenguaje, le indica al compilador que busque el archivo indicado entre comillas en la misma localidad (carpeta o directorio) que el archivo que incluye a dicha biblioteca.

   En resumen, tanto el archivo fuente que usará a la biblioteca personalizada de funciones como el archivo que contiene las funciones de biblioteca personalizada, deberán estar en el mismo directorio o carpeta. Cabe mencionar que es posible especificar una ruta para la localización de un archivo de biblioteca personalizada en particular, pero en general esto no es recomendable debido a que se sacrifica la portabilidad, ya que la especificación de rutas de archivos y directorios cambia de un sistema operativo a otro.

   Así como no hay un límite para el número de funciones que puede contener un archivo de biblioteca personalizada, se debe aclarar que tampoco hay un límite para el número de archivos de bibliotecas personalizadas que se pueden incluir dentro de un programa. Para el caso del Ejemplo 4.6, se han incluido una biblioteca del lenguaje (línea 6) y una biblioteca personalizada (línea 7), pero podrían ser un número arbitrario de cualquiera de ellas; la única recomendación en este sentido sería, incluir primero las bibliotecas del lenguaje, y después las bibliotecas personalizadas con las funciones definidas por el programador.

   Finalmente, note que si se proporcionan los mismos datos para el Ejemplo 4.2 y para el Ejemplo 4.6 se obtendrá la misma salida, como se muestra en la siguiente figura:

Una posible salida producida por los Ejemplos 4.2 y 4.6.

3 de noviembre de 2016

Ejercicios selectos (funciones).

  1. Modifique el Ejemplo 4.1 para que considere el caso cuando la ecuación cuadrática sólo tiene una raíz, i.e. cuando el discriminante es igual a cero; de tal forma que sólo se calcule y presente un resultado en la salida estándar.
  2. Modifique el ejercicio anterior para que tantos los coeficientes de la ecuación como las raíces, sean ahora tipos de dato double. No olvide hacer los ajustes correspondientes para los especificadores de formato.
  3. Modifique el Ejemplo 4.2 para que las variables de la función main sean x ,y y z ¿Compila?, ¿por qué? Suponiendo que compila, ¿cambia en algo el funcionamiento del programa?, ¿por qué? ¿Y si ahora se cambia el nombre de los parámetros de las funciones por a, b y c?
  4. Complete el Ejemplo 4.2 con la función medio, la cual devuelve el número de en medio en base a su valor ordinal. Utilice lo resuelto en el Ejercicio 5 (Primera Parte) de la entrada Ejercicios selectos para las estructuras de control.
  5. Escriba un programa que, a través de una función, calcule el cuadrado de un número double.
  6. Utilice la función desarrollada en el ejercicio anterior para resolver el problema del Ejemplo 4.1. Observe que en la línea 22 el discriminante lleva el cálculo de b al cuadrado (); utilice su función para calcular dicho valor.
  7. Escriba una función que permita convertir de millas a kilómetros. Una milla es aproximadamente igual a 1.61 kilómetros. Escriba también un programa que pruebe su función.
  8. Escriba una función que permita convertir de kilómetros a millas. Escriba también un programa que pruebe su función.
  9. El factorial de un número n (n >= 0) denotado por n! se obtiene según lo definido en la siguiente expresión: n! = n * (n - 1) * (n - 2) * . . . * 1; es decir, se multiplica el número, por su antecesor y éste por su respectivo antecesor y así sucesivamente hasta llegar al neutro multiplicativo. Escriba una función que calcule el factorial de un número.
  10.  Escriba un programa que a través de una función, calcule el producto de dos números enteros 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 ello:
    1. Resuelvalo primero con dos números enteros positivos.
    2. ¿Qué pasa si alguno de los números es negativo? ¿Cómo se comporta su algoritmo, representado en la función, ante dicha situación? Tome en cuenta que el resultado es un número negativo.
    3. ¿Qué pasa si ambos números son negativos? ¿Cómo se comporta su algoritmo, representado en la función, ante dicha situación? Tome en cuenta que para este caso el resultado es un número positivo.
   11.  Escriba un programa que a través de una función, calcule el cociente entero de dos números enteros positivos a y b por medio de restas sucesivas, esto es a / b ==>

  a - b = c1, si c1 >= b, entonces
c1 - b = c2, si c2 >= b, entonces
c2 - b = c3, si c3 >= b, entonces
        .     .     .                                
donde el cociente estará dado por el número de veces que se haya podido repetir el procedimiento descrito.
   12.  Escriba un programa que a través de una función, calcule la potencia de dos números 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.
 Para ello:
  • Resuélvalo primero con dos números enteros positivos.
  • ¿Qué pasa si a es negativo?
  • ¿Qué aspectos debe tomar en consideración si b es negativo?
  • ¿Qué pasa si a o b son cero? ¿Qué pasa si ambos lo son?
   13.  Repita el ejercicio anterior, pero en lugar de usar el operador de multiplicación (*), utilice la función que desarrolló en el Ejercicio 10.
   14.  Si para el programa del Ejemplo 4.4 se realizara una nueva ronda de llamados consecutivos a cada una de las tres funciones involucradas, ¿cuáles serían los valores que se visualizarían en la salida estándar?
   15.  Para el Ejemplo 4.6 elimine la línea 7, guarde el archivo, y compile. ¿Qué sucede?, ¿a qué se debe y por qué?
   16.  Incorpore a la biblioteca personalizada de funciones del Ejemplo 4.5 la función medio realizada en el Ejercicio 4 y pruebe su funcionamiento.
   17.  Escriba una función que determine si un número entero positivo es o no un número primo. Un número primo es aquél que es divisible únicamente por la unidad y sí mismo. Su función deberá regresar un 1 si el número enviado como argumento es primo y 0 si no lo es.
   18.  Escriba una función que determine si un número es perfecto o no. Un número perfecto es un entero positivo que es igual a la suma de sus divisores positivos sin considerarse él mismo como divisor.
   19.  Escriba un programa que, dado un número de hasta cinco cifras, exprese el valor de dicho número en palabras. Así por ejemplo, para el número 99 999, el programa dará como salida: “99 999: noventa y nueve mil novecientos noventa y nueve”. El castellano (español) es un idioma complejo, así que si la salida de su programa es “99 999: noventa y nueve mil nueve cientos noventa y nueve”, o genera expresiones por el estilo, es aceptable. El número debe ser leído como tal (no por dígitos separados), y su análisis inicia por "desmenuzarlo" en unidades, centenas, millares, etcétera e identificar los patrones convenientes para expresarlo en palabras. 
    La idea de este ejercicio es que haya funciones que se encarguen de procesar cada uno de los dígitos, y que se identifiquen patrones de repetición al expresar cantidades dependiendo del dígito y su posición.
   20.  Pruebe creando otras bibliotecas personalizadas de funciones con los ejemplos y ejercicios realizados hasta ahora. No importa por ahora que las funciones de dichas bibliotecas no tengan mucha relación, lo que importa por ahora es practicar la creación de bibliotecas personalizadas de funciones.