Tema 10 Estructuras iterativas

Las estructuras iterativas, también llamadas repetitivas o cíclicas, ejecutan un conjunto de instrucciones de manera reiterada. En este tema estudiaremos las distintas instrucciones de R relacionadas con la iteración.

10.1 La sentencia for

La instrucción for itera por los elementos de un vector o lista, ejecutando un conjunto de instrucciones en cada iteración. Su sintaxis es:

for (item in vector) {
  conjunto_de_instrucciones
}

El conjunto de instrucciones se va a ejecutar tantas veces como elementos tenga el vector. La variable item toma en cada ejecución el valor de un elemento del vector. Por ejemplo:

for (x in c(2, 7, 9)) {
  print(x)
}
## [1] 2
## [1] 7
## [1] 9

Observa que el conjunto de instrucciones se ha ejecutado una vez por cada elemento del vector y que la variable x ha ido tomando en cada ejecución uno de los valores del vector, del primero al último.

La instrucción for nos permite procesar los elementos de un vector. El siguiente ciclo, por ejemplo, recorre los elementos de un vector para sumarlos, algo que podríamos hacer de forma más breve con la función sum:

v <- c(2, 9, -23, 5.5)
suma <- 0
for (x in v) {
  suma <- suma + x
}
suma
## [1] -6.5
sum(v)
## [1] -6.5

Si nos interesa iterar por los índices de un vector, podemos usar la función seq_along para generar sus índices:

v <- c(6, 10, 12, 9)
seq_along(v)
## [1] 1 2 3 4
for (ind in seq_along(v)) {
  cat(ind, '--', v[ind], '\n')
}
## 1 -- 6 
## 2 -- 10 
## 3 -- 12 
## 4 -- 9

Un ciclo for se puede utilizar para ejecutar un número fijo de veces un conjunto de instrucciones. Por ejemplo, supongamos que queremos leer 4 números y calcular su cuadrado, eso lo podríamos hacer con el siguiente código:

# Lee 4 números y los eleva al cuadrado
for (ind in 1:4) {
  n <- as.numeric(readline("Introduce un número: "))
  cat(n, "al cuadrado vale", n ^ 2, '\n')
}

Se ha utilizado como vector 1:4, pero realmente la variable ind no se utiliza en el cuerpo del ciclo. Lo importante es poner un vector con 4 elementos, para que el ciclo se ejecute 4 veces. Se puede obtener el mismo resultado usando el vector 2:5 o c("a", "x", "b", "f") en lugar de 1:4, aunque con 1:4 el programa es más fácil de leer.

10.2 La sentencia while

La instrucción for ejecuta un conjunto de instrucciones un número fijo de veces (tantas como la longitud del vector o lista usado). La instrucción while permite ejecutar un conjunto de instrucciones un número indeterminado de veces, pues el conjunto de instrucciones se ejecutan mientras que se verifica una condición lógica. La sintaxis del ciclo while es la siguiente:

while (condicion_logica) {
  conjunto_de_instrucciones
}

El funcionamiento es sencillo. Cuando el flujo de ejecución de un programa llega a un ciclo while, se evalúa su condición lógica. Si el resultado de la evaluación es verdadero, se ejecuta el conjunto de instrucciones asociado y se vuelve a evaluar la condición lógica. Así hasta que la condición lógica es falsa, entonces se pasa a ejecutar la instrucción que siga en el programa a la instrucción while. Como ejemplo, vamos a ver un fragmento de código que calcula si un número entero positivo es primo:

num <- 19 # queremos ver si num es primo
encontrado_divisor <- FALSE
posible_divisor <- 2
while (!encontrado_divisor && posible_divisor < num) {
  if (num %% posible_divisor == 0) { # ¿num múltiplo de posible_divisor?
    encontrado_divisor <- TRUE
  } else {
    posible_divisor <- posible_divisor + 1
  }
}
if (encontrado_divisor) {
  cat(num, "no es un número primo")
} else {
  cat(num, "es un número primo")
}
## 19 es un número primo

La idea utilizada en el programa para comprobar si un número num es primo es probar con todos los números desde el 2 hasta num - 1 para ver si alguno de esos números divide a num. Si existe algún divisor en el rango \([2, num-1]\), num no es primo, en otro caso sí lo es. Inicialmente no conocemos ningún divisor, por lo que la variable encontrado_divisor se inicia a FALSE. La condición del ciclo hace que se itere mientras que no se ha encontrado un divisor (!encontrado_divisor) y no hayamos agotado todos los posibles divisores del número. Mientras que se verifican estas dos condiciones se ejecutan las instrucciones asociadas al ciclo. Estas comprueban si el actual divisor (posible_divisor) divide exactamente al número. Si es así, se actualiza la variable encontrado_divisor a TRUE, porque hemos hallado un divisor. En otro caso se incrementa en uno la variable posible_divisor para que en la siguiente iteración se pruebe con otro divisor. El ciclo termina cuando encontrado_divisor es verdadero (se ha encontrado un divisor, luego el número no es primo) o la variable posible_divisor es igual a num (no se ha encontrado ningún divisor, luego el número es primo).

Por cierto, el programa anterior se puede hacer más eficiente si se cambia la condición del ciclo while a !encontrado_divisor && posible_divisor <= sqrt(num) porque no es preciso iterar hasta num - 1 para ver que num no tiene divisores, basta con hacerlo hasta sqrt(num), piensa por qué.

La instrucción while es más general que la instrucción for. Cualquier cómputo que pueda realizarse con un for se puede hacer con un while, pero no a la inversa. Como ejemplo, vamos a sumar los elementos de un vector usando un ciclo while:

v <- c(2, 9, -23, 5.5)
suma <- 0
ind <- 1     # índice para recorrer los elementos de v
while (ind <= length(v)) {
  suma <- suma + v[ind]
  ind <- ind + 1
}
suma
## [1] -6.5
sum(v)
## [1] -6.5

No obstante, para recorrer los elementos de un vector es más elegante usar un for, pues la sentencia for está expresamente pensada para ello.

Si en el ciclo anterior se olvida escribir la última instrucción del conjunto de instrucciones: ind <- ind + 1 tendríamos lo que se conoce como un ciclo infinito. Es decir, un ciclo que se ejecuta indefinidamente. Esto ocurre cuando en un ciclo el conjunto de instrucciones asociado no hace las modificaciones oportunas en variables para que la condición del ciclo se vuelva falsa. Puedes comentar la instrucción ind <- ind + 1 y observa qué ocurre.

10.2.1 Lectura con centinela

La lectura con centinela es un tipo de lectura de datos en la que el usuario introduce un valor especial, llamado centinela, para indicar que no quiere introducir más datos. Veamos un ejemplo con un programa que acumula los números solicitados al usuario. El usuario debe introducir el valor cero cuando no quiera introducir más datos, es decir, el centinela es el cero.

# Lectura con centinela
suma <-  0
n <-  as.numeric(readline("Introduce número (0 para acabar): "))
while (n != 0) {
  suma <- suma + n
  n <- as.numeric(readline("Introduce número (0 para acabar): "))
}
cat('La suma de los números es', suma)  

Observa que la primera instrucción readline sólo se ejecuta una vez y sirve para leer el primer número. El resto de lecturas se realizan con la última sentencia del ciclo while. En R se puede obtener una versión más sencilla del programa usando scan (el centinela de scan es escribir una línea en blanco):

cat("Introduce valores (línea en blanco para terminar): ")
v <- scan()
cat('La suma de los números es', sum(v))

10.2.2 Lectura con verificación

El ciclo while también se puede usar para comprobar de forma reiterada si la entrada proporcionada por el usuario se ajusta a unas especificaciones. Por ejemplo, supongamos que leemos una edad y queremos asegurarnos de que la edad leída está en el rango [18, 60]:

# Lectura con verificación
edad <- as.numeric(readline("Introduce edad: "))
while (edad < 18 || edad > 60) {
  edad <- as.numeric(readline("Reintroduce edad (rango [18, 60]: "))
}
cat('La edad es', edad)

Mientras que la edad no cumple los criterios de estar en el rango [18, 60] se solicita su introducción.

10.3 Instrucciones break y next

Las instrucciones break y next están ligadas a la ejecución de los ciclos. La instrucción break sirve para salirse de un ciclo. En caso de que se encuentre en un ciclo anidado (uno dentro de otro) se saldrá del ciclo más interno. Recomendamos que esta instrucción se use con mesura. El ejemplo de ver si un número es primo se puede codificar usando break de la siguiente manera:

num <- 19
encontrado_divisor <- FALSE
posible_divisor <- 2
while (posible_divisor < num) {
  if (num %% posible_divisor == 0) { # ¿num múltiplo de posible_divisor?
    encontrado_divisor <- TRUE
    break
  } 
  posible_divisor <- posible_divisor + 1
}
if (encontrado_divisor) {
  cat(num, "no es un número primo")
} else {
  cat(num, "es un número primo")
}
## 19 es un número primo

En el interior del ciclo, si se encuentra un divisor se actualiza la variable encontrado_divisor a TRUE y se usa break para salir directamente de la ejecución del ciclo, porque ya hemos encontrado un divisor y sabemos que el número no es primo.

La instrucción next se utiliza con mucha menos frecuencia que break. Esta instrucción termina la ejecución del bloque de código asociado al ciclo e inicia una nueva ejecución (en el caso del ciclo while vuelve a evaluar la condición lógica asociada al ciclo). Por ejemplo:

# Suma los elementos positivos de un vector
v <- c(-2, 3, 5, -4)
suma <- 0
for (x in v) {
  if (x < 0) {
    next
  }
  suma <- suma + x
}
suma
## [1] 8

Este ciclo suma los elementos no negativos de un vector. Observa que en el interior del ciclo se usa next para dejar de ejecutar instrucciones y pasar a la siguiente iteración del ciclo en caso de que el número sea negativo (por lo tanto, si se ejecuta next no se ejecutará la instrucción suma <- suma + x). En este caso, el uso de next no resulta particulamente interesante, el código se podría haber escrito así:

# Suma los elementos positivos de un vector
v <- c(-2, 3, 5, -4)
suma <- 0
for (x in v) {
  if (x > 0) {
    suma = suma + x
  }
}
suma
## [1] 8

Sin embargo, hay circunstancias en las que el uso de la instrucción next puede producir un código más legible.

10.4 La sentencia repeat

El sentencia repeat ejecuta repetida e incondicionalmente un conjunto de instrucciones. Para que un ciclo implementado con repeat no se convierta en un ciclo infinito debe ejecutar en algún momento una instrucción break o, si el ciclo forma parte del código de una función, la instrucción return (la codificación de funciones se estudiará en el siguiente capítulo).

x <- 1
repeat {
  print(x)
  if (x == 5) break
  x <- x + 1
}
## [1] 1
## [1] 2
## [1] 3
## [1] 4
## [1] 5

En este curso recomendamos usar ciclos while o for antes que un ciclo repeat.

10.5 Escribir ciclos en una línea

Si el código asociado a un ciclo sólo contiene una instrucción, no es preciso usar llaves para delimitar sus instrucciones. Por ejemplo:

for (x in 1:5)
  cat(x^2, "\n")
## 1 
## 4 
## 9 
## 16 
## 25

Incluso se puede escribir todo en una línea:

for (x in 1:5) cat(x^2, "\n")
## 1 
## 4 
## 9 
## 16 
## 25

10.6 Código vectorizado versus ciclos

R tiene muchas funciones y operadores que actúan sobre todos los elementos de un vector, matriz, etc. Esto implica que muchos cálculos que en algunos lenguajes de programación han de realizarse usando ciclos se pueden expresar en R usando estas funciones y operadores, sin necesidad de escribir un ciclo. Por ejemplo, el calcular si un número es primo se podría resolver con el siguiente código:

num <- 19
primo <- num > 2 && all(num %% 2:(num-1) != 0)
if (primo) {
  cat(num, "es un número primo")
} else {
  cat(num, "no es un número primo")
}
## 19 es un número primo

A este tipo de código que usa funciones, operadores e indexación para realizar cálculos sobre vectores y demás estructuras de datos se le llama código vectorizado. El código vectorizado es muy compacto. Vamos a analizar en detalle las operaciones realizadas en el ejemplo previo. En primer lugar se generan los posibles divisores de num:

2:(num-1)
##  [1]  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18

Después se genera un vector con el resto de dividir num entre los posibles divisores:

num %% 2:(num-1)
##  [1] 1 1 3 4 1 5 3 1 9 8 7 6 5 4 3 2 1

A continuación se comprueba si las divisiones no son exactas:

num %% 2:(num-1) != 0
##  [1] TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE
## [16] TRUE TRUE

Por último, se comprueba si todas las divisiones no son exactas (es decir, no tiene divisores):

all(num %% 2:(num-1) != 0)
## [1] TRUE

¿Qué código es mejor, el que usa ciclos o el vectorizado? No existe una respuesta simple. En términos de claridad el código vectorizado es más conciso, pero a veces es difícil de entender. En términos de eficiencia dependerá de cada caso y habrá que cronometrar para ver cuál es más rápido (habría que probar con distintos tamaños de datos para ver cómo escalan los códigos). En general, si los códigos realizan las mismas operaciones el código vectorizado es más rápido, pues las funciones como sum, mean, etc suelen estar compiladas y se ejecutan más rápido que el código equivalente en R con ciclos, que es interpretado. No obstante, a veces el código vectorizado, pese a su brevedad, realiza más operaciones. En el ejemplo de ver si un número es primo, el código con ciclos deja de buscar divisores al encontrar uno, lo que no se puede expresar en el código vectorizado.

A continuación mostramos otro ejemplo de código con ciclos frente a código vectorizado. Se trata de sumar los elementos positivos de un vector, un código que ya hemos visto previamente:

# Suma los elementos positivos de un vector
v <- c(-2, 3, 5, -4)

# Versión con ciclos
suma <- 0
for (x in v) {
  if (x > 0) {
    suma <- suma + x
  }
}
suma
## [1] 8
sum(v[v>0]) # versión vectorizada
## [1] 8

10.7 Ejercicios

  1. Observa los siguientes fragmentos de código y piensa qué resultado producirán. Ejecútalos y comprueba si has acertado.

    # Ciclo simple
    var <- 0
    for (x in 1:10) {
      var <- var + 1
    }
    var
    # Ciclo anidado
    var <- 0
    for (x in 1:10) {
      for (y in 1:5) {
        var <- var + 1
      }
    }
    var
  2. Realiza un programa que solicite del teclado un entero del 1 al 10 y muestre en la pantalla su tabla de multiplicar.

  3. Realice un programa que calcule y muestre en la pantalla la suma de los cuadrados de los números enteros del 1 al 10 (la solución es \(385 = 1^{2} + 2^{2} + \ldots + 10^{2}\)). Escribe una versión con ciclos y otra vectorizada.

  4. Realice un programa que lea del teclado 10 números e indique en la pantalla si el número cero estaba entre los números leídos.

  5. Realiza un programa que lea del teclado números hasta que se introduzca un cero (es decir, es una lectura con centinela donde el centinela es el valor 0). En ese momento el programa debe terminar y mostrar en la pantalla la cantidad de valores mayores que cero leídos.

  6. Existen muchos métodos numéricos capaces de proporcionar aproximaciones al número \(\pi\). Uno de ellos es el siguiente:

    \[ \pi = \sqrt{\sum_{i=1}^{\infty} \frac{6}{i^{2}}} \]

    Crea un programa que lea el grado de aproximación (número de términos de la sumatoria) y devuelva un valor aproximado de \(\pi\). Escribe una versión con ciclos y otra vectorizada.

  7. Escribe un programa que lea un número entero no negativo n y dibuje un triángulo rectángulo con base y altura n, como el que se muestra a continuación para \(n = 4\). Observa que debe aparecer un espacio entre cada asterisco situado en la misma línea:

    *
    * *
    * * *
    * * * *
  8. El algoritmo de Euclides es un procedimiento para calcular el máximo común divisor de dos números naturales. Los pasos son:

    • Se divide el número mayor (M) entre el menor (m).
    • Si:
      • La división es exacta, entonces m es el máximo común divisor.
      • La división no es exacta, entonces \(mcd(M, m) = mcd(m, M \%\% m)\).

    Por ejemplo, vamos a calcular el máximo común divisor de 93164 y 5826:

    • 93164 entre 5826 es 15 y sobran 5774, luego \(mcd(93164, 5826) = mcd(5826, 5774)\).
    • 5826 entre 5774 es 1 y sobran 52, luego \(mcd(5826, 5774) = mcd(5774, 52)\).
    • 52 entre 2 es 26 y sobran 0, luego \(mcd(52, 2) = 2 = mcd(93164, 5826)\).

    Escribe un programa que implemente este algoritmo.

  9. Realice un programa que solicite al usuario un entero positivo e indique en la pantalla si el entero leído es una potencia de dos.

  10. Un número perfecto es un número natural que es igual a la suma de sus divisores positivos, sin incluirse él mismo. Por ejemplo, 6 es un número perfecto porque sus divisores positivos son: 1, 2 y 3 y 6 = 1 + 2 + 3. El siguiente número perfecto es el 28. Escribe un programa que lea un número natural e indique si es perfecto o no. Haz una versión con ciclos y otra vectorizada.

  11. Realiza un programa que calcule los cuatro primeros números perfectos. Nota: el resultado es 6, 28, 496, 8128.

  12. Realiza un programa que, dado un vector (por ejemplo, sample(10)), muestre sus elementos del último al primero en la pantalla. Usa un ciclo que genere los índices en orden inverso para obtener los elementos del vector.

  13. Realiza un programa que, dado el siguiente vector:

    set.seed(4)
    v <- sample(10)

    Calcule el primer elemento del vector que es mayor al elemento que le precede en el vector.

  14. Realiza un programa que, dada la siguiente matriz:

    set.seed(8)
    m <- matrix(sample(20), nrow = 2)

    Calcule el primer elemento de la segunda fila de la matriz que es mayor que el elemento de la primera fila ubicado en la misma columna.

10.8 Soluciones a los ejercicios

# Ciclo simple
var <- 0
for (x in 1:10) {
 var <- var + 1
}
var
## [1] 10
# Ciclo anidado
var <- 0
for (x in 1:10) {
 for (y in 1:5) {
   var <- var + 1
 }
}
var
## [1] 50
# Tabla de multiplicar
n <- as.integer(readline('Introduce entero del 1 al 10: '))
for (factor in 1:10) {
  cat(n, "x", factor, "=", n*factor, "\n")
}
# Suma de los cuadrados del 1 al 10
suma <- 0
for (n in 1:10) {
  suma <- suma + n^2
}
cat("La suma es", suma, "\n")
## La suma es 385
cat("La suma es", sum((1:10)^2), "\n") # versión vectorizada
## La suma es 385
# Comprobar si se ha leído el cero
esta <- FALSE
for (x in 1:10) {
  n <- as.numeric(readline('Introduce un número: '))
  if (n == 0) {
    esta <- TRUE
  }
}

if (esta) {
  cat("El 0 está entre los números leídos\n")
} else {
  cat("El 0 no está entre los números leídos\n")
}
# Contar valores positivos

positivos <- 0
valor <- as.numeric(readline('Valor (0 para terminar): '))
while (valor != 0) {
  if (valor > 0) positivos <- positivos + 1
  valor <- as.numeric(readline('Valor (0 para terminar): '))
}
cat("Cantidad de valores positivos:", positivos, "\n")
# Aproximación al número PI

n <- as.integer(readline('Número de términos: '))
suma <- 0
for (x in 1:n) {
  suma <- suma + 6 / x^2
}
cat("Aproximación con", n, "términos:", sqrt(suma), "\n")
# Versión vectorizada
cat("Aproximación con", n, "términos:", sqrt(sum(6 / (1:n)^2)), "\n")
# Rectángulo de asteriscos
altura <- as.integer(readline('Altura: '))

for (n in 1:altura) {
  for (x in 1:n) {
    cat("* ")
  }
  cat("\n")
}
# Algoritmo de Euclides MCD

n1 <- as.integer(readline('Introduce número natural: '))
n2 <- as.integer(readline('Introduce otro número natural: '))

if (n1 > n2) {
  M <- n1
  m <- n2
} else {
  M <- n2
  m <- n1
}

while (M %% m != 0) {
  tmp <- M
  M <- m
  m <- tmp %% m
}
cat("El MCD es", m, "\n")
# Potencia de dos
n <- as.integer(readline('Introduce entero positivo: '))

potencia <- 0
while (2^potencia < n) {
  potencia <- potencia + 1
}
if (2^potencia == n) {
  cat(n, "es una potencia de 2\n")
} else {
  cat(n, "no es una potencia de 2\n")
}
# Número perfecto
n <- as.integer(readline('Introduce entero positivo: '))

suma <- 0
for (d in 1:(n-1)) {
  if (n %% d == 0) suma <- suma + d
}

if (suma == n) {
  cat(n, "es un número perfecto\n")
} else {
  cat(n, "no es un número perfecto\n")
}

# versión vectorizada
d <- 1:(n-1)
if (sum(d[n %% d == 0]) == n) {
  cat(n, "es un número perfecto\n")
} else {
  cat(n, "no es un número perfecto\n")
}
# 4 primeros números perfectos

numeros_perfectos <- 0 # cantidad de números perfectos calculados
candidato <- 2
while (numeros_perfectos < 4) {
  suma <- 0
  for (d in 1:(candidato-1)) {
    if (candidato %% d == 0) suma <- suma + d
  }
  if (suma == candidato) {
    print(candidato)
    numeros_perfectos <- numeros_perfectos + 1
  }  
  candidato <- candidato + 1
}
# Muestra los elementos de un vector en orden inverso
v <- sample(10)
cat("Del primero al último: ", v, "\n")
cat("En orden inverso:\n")
ind <- length(v)
while (ind >= 1) {
  cat(v[ind], "\n")
  ind <- ind - 1
}
# Primer elemento de un vector que es mayor a su predecesor

set.seed(4)
v <- sample(10)

ind <- 2
encontrado <- FALSE
while (!encontrado && ind <= length(v)) {
  if (v[ind] > v[ind-1]) {
    encontrado <- TRUE
    sol <- v[ind]
  } else {
    ind <- ind + 1
  }
}
if (encontrado) {
  cat(sol, "es el primer número mayor que su predecesor\n")
} else {
  cat("No existe ningún elemento en el vector mayor a su predecesor\n")
}
# Primer elemento de la segunda fila mayor que el elemento de la
# misma columna de la primera fila

set.seed(8)
m <- matrix(sample(20), nrow = 2)

col <- 1
encontrado <- FALSE
while (!encontrado && col <= ncol(m)) {
  if (m[2, col] > m[1, col]) {
    encontrado <- TRUE
    sol <- m[2, col]
  } else {
    col <- col + 1
  }
}
if (encontrado) {
  cat(sol, "\n")
} else {
  cat("Ningún elemento verifica la condición\n")
}