5  Programación procedimental

En esta sección se hará una revisión de lo que es la programación procedimental. La programación procedimental o programación por procedimientos usualmente es una manera de darle un ingrediente más al paradigma de programación imperativa estructurada. Muchas veces es aplicable tanto en lenguajes de programación de bajo nivel como en lenguajes de alto nivel. Esta técnica consiste en englobar una serie de instrucciones dentro de un procedimiento o función y llamarlo cada vez que se requiera. Por otra parte, la resolución de problemas complejos se facilita considerablemente si estos se dividen en problemas más pequeños (subproblemas). La solución de estos subproblemas se realiza mediante subalgoritmos. Estos subalgoritmos o subprogramas están diseñados para realizar alguna tarea específica y pueden ser de dos tipos: funciones o procedimientos.

En esta revisión se hace mención a la programación modular, a las funciones y procedimientos como subalgoritmos, al ámbito de las variables y a los subalgoritmos recursivos.

Se espera que al finalizar las actividades de esta sección, el estudiante tenga clara la manera en que se hace el diseño de un algoritmo bajo el paradigma de programación imperativa estructurada procedimental, ya sea mediante el uso de un diagrama de flujo o de pseudocódigo.

5.1 Programación modular

El diseño descendente consiste en solucionar un problema mediante su descomposición en problemas más pequeños y más sencillos. La programación modular consiste en resolver de forma independiente los subproblemas resultantes de una descomposición. La programación modular amplía y completa el diseño descendente como método de resolución de problemas y permite proteger la estructura de la información asociada a un subproblema.

Cuando se trabaja de este modo, existirá un algoritmo principal o conductor que transferirá el control a los distintos subalgoritmos, los cuales, cuando terminen su tarea, devolverán el control al algoritmo que los llamó. Los subalgoritmos deberán ser de menor tamaño y menor dificultad al algoritmo principal, seguirán todas las reglas de la programación estructurada y podrán ser representados con las herramientas de programación habituales.

El empleo de esta técnica facilita notoriamente el diseño de los programas. Algunas ventajas significativas son:

  • Varios programadores podrán trabajar simultáneamente en la confección de un algoritmo, repartiéndose las distintas partes del mismo, ya que los módulos son independientes.
  • Se podrá modificar un módulo sin afectar a los demás.
  • Las tareas o subalgoritmos sólo se escribirán una vez, aunque sean requeridos en distintas ocasiones en el cuerpo de uno o varios algoritmos.

5.2 Subalgoritmos

5.2.1 Funciones

Una función toma uno o más valores, denominados argumentos o parámetros y, según el valor de éstos, devuelve un resultado a nombre de la función. La definición de una función expresada en pseudocódigo tendría la siguiente forma:

tipo_de_dato_devuelve: función nombre_función(tipo_de_dato_1: parámetro_1, tipo_de_dato: demás_parámetros)
inicio_función
  …
  acciones
  …
  devolver(expresión)
fin_función

Para invocar a una función se utiliza su nombre seguido por los parámetros entre paréntesis. La llamada a una función se podrá colocar en cualquier instrucción en donde se pueda usar una expresión.

5.2.2 Procedimientos

La declaración de un procedimiento es similar a la de una función. Las pequeñas diferencias son debidas a que el nombre del procedimiento no se encuentra asociado a ningún resultado. La definición de un procedimiento expresada en pseudocódigo tendría la siguiente forma:

procedimiento nombre_procedimiento(tipo_de_dato_1: parámetro_1, tipo_de_dato: demás_parámetros)
inicio_procedimiento
  …
  acciones
  …
fin_procedimiento

Un procedimiento se invoca o se llama de la misma manera que una función, pero al no devolver un resultado, un procedimiento solamente se puede usar como instrucción del algoritmo principal y no como parte de una expresión.

5.3 Ambito de las variables

Una variable es global cuando el ámbito en el que dicha variable se conoce es el programa completo. Se consideran como variables globales aquellas que hayan sido declaradas en el algoritmo principal y como locales las declaradas dentro de algún subalgoritmo.

Toda variable que se utilice en un procedimiento o función debe haber sido declarada en el mismo. De esta forma todas las variables del procedimiento serán locales y la comunicación con el programa principal se realizará exclusivamente a través de los parámetros. Al declarar una variable en un subprograma no importa que ya existiera otra con el mismo nombre en el programa principal; ambas serán distintas y, cuando nos encontremos en el subprograma, sólo tendrá vigencia la declaración que hayamos efectuado en él. Trabajando de esta forma obtendremos la independencia modular o de algoritmos deseada (ya sean principales o subalgoritmos).

Ejemplo 5.1 Realice el análisis y diseño de:

  • Un subalgoritmo (función) que reciba un número real y que devuelva el valor absoluto (|x|) del número recibido. La única tarea de este subalgoritmo es obtener y devolver el valor absoluto del número que reciba, no le corresponde interactuar de manera alguna con el usuario.
  • Un subalgoritmo (procedimiento) que le pida al usuario su nombre, lo salude y le de la bienvenida.
  • Un algoritmo principal que le pida al usuario su nombre, lo salude, le dé la bienvenida y, tantas veces como desee el usuario, le pida un número real y le informe su respectivo valor absoluto. Este algoritmo principal debe utilizar los dos subalgoritmos anteriores y se debe encargar de todo aquello que no sea tarea de los subalgoritmos.

Para cualquier número real x, el valor absoluto de x se denota por |x| y se define como:

|x| = \begin{cases} x & \text{Si } x \geq 0 \\ -x & \text{Si } x < 0 \end{cases}

//Aquí pondré el subalgoritmo que obtiene el valor absoluto
real: función valAbs(real: x)
inicio_función
  si x < 0 entonces
    x <- -x
  fin_si
  devolver(x)
fin_función

//Aquí pondré el subalgoritmo que le pide al usuario su nombre, lo saluda y le da la bienvenida
procedimiento saludo()
var
  cadena: nombre
inicio_procedimiento
  escribir(“Por favor, ingrese su nombre”)
  leer(nombre)
  escribir(“Buen día” & nombre & “, sea usted bienvenido”)
fin_procedimiento

//Aquí pondré el programa principal
Algoritmo: Saluda y obtiene el valor absoluto de cada número que ingrese el usuario
var
  real: x
  cadena: continuar
inicio
  saludo()
  escribir(“A continuación se procede a probar el subalgoritmo que obtiene el valor absoluto de un número real dado”)
  continuar <- “”
  mientras continuar != “No” hacer
    escribir(“Por favor, ingrese un número real”)
    leer(x)
    escribir(“El valor absoluto de” & x & ” es ” & valAbs(x))
    escribir(“¿Desea continuar probando el subalgoritmo (para finalizar, ingrese la palabra: No)?”)
    leer(continuar)
  fin_mientras
fin

5.4 Recursión

Un elemento es recursivo si forma parte de sí mismo o interviene en su propia definición. El mecanismo necesario para poder expresar los programas recursivamente es el subalgoritmo. La mayoría de los lenguajes de programación admiten que un procedimiento o función haga referencia a sí mismo dentro de su definición, esto se denomina recursividad directa.

La recursión se puede considerar como una alternativa a la iteración y resulta muy útil cuando se trabaja con problemas o estructuras definidos en modo recursivo (sin embargo las soluciones iterativas son más cercanas a la estructura de funcionamiento de un computador).

Para comprender la recursividad se deben tener en cuenta las siguientes premisas:

  • Un método recursivo debe establecer la condición o condiciones de salida.
  • Cada llamada recursiva debe aproximar hacia el cumplimiento de la o las condiciones de salida.
  • Cuando se llama a un procedimiento o función los parámetros y las variables locales toman nuevos valores, y el procedimiento o función trabaja con estos nuevos valores y no con los de anteriores llamadas.
  • Cada vez que se llama a un procedimiento o función los parámetros de entrada y variables locales son almacenados en las siguientes posiciones libres de memoria y cuando termina la ejecución del procedimiento o función son accedidos en orden inverso a como se introdujeron.
  • El espacio requerido para almacenar los valores crece conforme a los niveles de anidamiento de las llamadas.
  • La recursividad puede ser directa e indirecta. La recursividad indirecta se produce cuando un procedimiento o función hace referencia a otro el cual contiene, a su vez, una referencia directa o indirecta al primero.

Todo algoritmo recursivo puede ser convertido en uno iterativo equivalente, aunque para ello se puede requerir la utilización de ciertas estructuras de datos que permitan el almacenamiento adecuado de ciertos datos.

Ejemplo 5.2 Realice el análisis y diseño de:

  • Un subalgoritmo RECURSIVO que reciba un número entero y que devuelva el factorial del número recibido. La única tarea de este subalgoritmo es obtener y devolver el factorial del número que reciba, no le corresponde interactuar de manera alguna con el usuario (¿qué considera que deba hacer el subalgoritmo cuando el número entero que recibe es negativo, teniendo en cuenta que no es trabajo del subalgoritmo interactuar con el usuario?).
  • Un algoritmo principal que, tantas veces como desee el usuario, le pida un número entero no negativo y le informe su respectivo factorial. Este algoritmo principal debe utilizar el subalgoritmo anterior y se debe encargar de todo aquello que no sea tarea del mismo.

El factorial de un número entero positivo k, denotado k! se puede definir como el producto de todos los números enteros positivos menores o iguales que k: \begin{aligned} k! &= (k)(k-1)(k-2) \cdots (3)(2) \\ &= (k) (k-1)! \end{aligned}

Además, es necesario tener en cuenta que, 0! = 1 y es buena idea tener en cuenta que, 1! = 1 \qquad 2! = 2

//Aquí pondré el subalgoritmo recursivo que obtiene el factorial
entero: función factRecur(entero: k)
var
  entero: res
inicio_función
  si k < 2 entonces
    res <- 1
  si_no
    res <- k * factRecur(k-1)
  fin_si
  devolver(res)
fin_función

//Aquí pondré el subalgoritmo que le pide al usuario su nombre, lo saluda y le da la bienvenida
procedimiento saludo()
var
  cadena: nombre
inicio_procedimiento
  escribir(“Por favor, ingrese su nombre”)
  leer(nombre)
  escribir(“Buen día” & nombre & “, sea usted bienvenido”)
fin_procedimiento

//Aquí pondré el programa principal
Algoritmo: Saluda y obtiene el factorial (subalgoritmo recursivo) de cada número que ingrese el usuario
var
  entero: k
  cadena: continuar
inicio
  saludo()
  escribir(“A continuación se procede a probar el subalgoritmo recursivo que obtiene el factorial de un número entero no negativo dado”)
  continuar <- “”
  mientras continuar != “No” hacer
    escribir(“Por favor, ingrese un número entero no negativo”)
    leer(k)
    escribir(“El factorial de” & k & ” es ” & factRecur(k))
    escribir(“¿Desea continuar probando el subalgoritmo (para finalizar, ingrese la palabra: No)?”)
    leer(continuar)
  fin_mientras
fin

5.5 Ejercicios

Realice el análisis y diseño de los subalgoritmos y el algoritmo principal requeridos, usando únicamente los elementos incluidos previamente en este material (por ejemplo, no use el operador ^ o ** para una potenciación, ya que ese elemento intencionalmente no fue incluido previamente):

  1. Un algoritmo principal para probar, tantas veces como desee el usuario, un subalgoritmo que reciba un número real y que devuelva la función signo \big(\mathrm{sign}(x)\big) evaluada en el número recibido.

  2. Un algoritmo principal para probar, tantas veces como desee el usuario, un subalgoritmo que reciba dos números reales y que devuelva el mayor de los dos. ¿Qué considera que deba hacer el subalgoritmo cuando los dos valores que recibe son iguales, teniendo en cuenta que no es trabajo del subalgoritmo interactuar con el usuario?.

  3. Un algoritmo principal para probar, tantas veces como desee el usuario:

    • Un subalgoritmo que recibe un valor asociado a grados Centigrados y devuelve su equivalente en grados Fahrenheit
    • Un subalgoritmo que recibe un valor asociado a grados Fahrenheit y devuelve su equivalente en grados Centigrados

    El algoritmo principal se debe encargar de pedir al usuario una temperatura y la escala de dicha temperatura, grados Centigrados o grados Fahrenheit, para luego obtener mediante el subalgoritmo respectivo la temperatura equivalente en la otra escala con respecto a la dada por el usuario (si recibe grados Centigrados devolverá grados Fahrenheit y viceversa).

  4. Un algoritmo principal para probar, tantas veces como desee el usuario, un subalgoritmo que reciba un número de día, un número de mes y un número de año, y que devuelva el valor booleano que corresponda dependiendo de si la fecha asociada a esos números es una fecha válida (True) o no (False) del calendario gregoriano.

  5. Un algoritmo principal para probar, tantas veces como desee el usuario, un subalgoritmo que reciba un número de día, un número de mes y un número de año de una fecha del calendario gregoriano, y que devuelva la cadena que corresponda a la fecha del día siguiente usando el formato: “año/mes/día”.

  6. Un algoritmo principal para probar, tantas veces como desee el usuario, un subalgoritmo que reciba un número entero y que devuelva un valor que indique si el número es primo o no lo es. ¿Qué considera que deba hacer el subalgoritmo cuando el número entero que recibe es menor que dos, teniendo en cuenta que no es trabajo del subalgoritmo interactuar con el usuario?

  7. Un algoritmo principal para probar, tantas veces como desee el usuario:

    • Un subalgoritmo ITERATIVO que reciba dos números enteros, y que devuelva el máximo común divisor de los números recibidos.
    • Un subalgoritmo RECURSIVO que reciba dos números enteros, y que devuelva el máximo común divisor de los números recibidos
    • Un subalgoritmo que reciba dos números enteros y que utilice uno de los subalgoritmos anteriores (por ejemplo el iterativo) para devolver el mínimo común múltiplo de los números recibidos.
  8. Un algoritmo principal para probar, tantas veces como desee el usuario: un subalgoritmo ITERATIVO y uno RECURSIVO que reciban un número entero k y que devuelvan el término k-ésimo de la sucesión de los números de Fibonacci https://youtu.be/B6ztvqvZTsk.

  9. Un algoritmo principal para probar, tantas veces como desee el usuario: un subalgoritmo ITERATIVO y uno RECURSIVO que reciban un número real x y un número entero k, y que devuelvan el número real x^k.

  10. Un algoritmo principal para probar, tantas veces como desee el usuario, un subalgoritmo que reciba dos números reales, x y \varepsilon, y que devuelva la aproximación de \sqrt{x} que corresponda a partir del valor dado para \varepsilon ¿Qué considera que deba hacer el subalgoritmo cuando el número real que recibe es negativo, teniendo en cuenta que no es trabajo del subalgoritmo interactuar con el usuario?

  11. Un algoritmo principal para probar, tantas veces como desee el usuario, un subalgoritmo que reciba dos números reales, asociados a un x y un \varepsilon, y que devuelva la aproximación de \sqrt[3]{x} dada por el valor de \varepsilon.

  12. Un algoritmo principal para probar, tantas veces como desee el usuario, un subalgoritmo que reciba dos números reales, asociados a un x y un \varepsilon, y que devuelva la aproximación de \sqrt[k]{x} dada por el valor de \varepsilon.