Como mencionamos anteriormente, un algoritmo está compuesto por una sucesión ordenada de comandos que se ejecutan uno detrás de otro. Sin embargo, con frecuencia es necesario recurrir a comandos especiales que alteran o controlan el orden en el que se ejecutan las acciones. Llamamos estructuras de control del flujo de las acciones al conjunto de reglas que permiten controlar el flujo de las acciones de un algoritmo o programa. Las mismas pueden clasificarse en secuenciales, condicionales e iterativas.
Las estructuras secuenciales están compuestas por un número definido de acciones que se ubican en un orden específico y se suceden una tras otra. Los ejemplos que hemos discutido anteriormente están conformados por este tipo de estructura.
En algunas partes de un algoritmo puede ser útil detenerse a hacer
una pregunta porque se llegó a una situación en la que puede haber una o
más opciones disponibles para continuar. Dependiendo de la respuesta a
la pregunta, que siempre deberá ser VERDADERO
(TRUE
) o FALSO
(FALSE
), el
algoritmo seguirá ciertas acciones e ignorará otras. Estas preguntas y
respuestas representan procesos de toma de decisión que conducen a
diferentes caminos dentro del algoritmo, permitiéndonos que la solución
para el problema en cuestión sea flexible y se adapte a distintas
situaciones. Este tipo de estructuras de control de las acciones reciben
el nombre de condicionales (o estructuras de
selección) y pueden ser simples,
dobles y múltiples.
Postulan una evaluación lógica y, si su resultado es
TRUE
, se procede a ejecutar las acciones delimitadas entre
las llaves que definen el cuerpo de esta estructura. se expresan en R
con la siguiente sintaxis:
La palabra if
indica el comando de evaluación lógica,
condición
indica la evaluación a realizar y entre llaves se
detallas las instrucciones que se realizarán sólo si se cumple la
condición, es decir, si la evaluación resulta en TRUE
. Si
la condición no se verifica, no se ejecuta ninguna acción y el programa
sigue su estructura secuencial con el código que prosigue a la última
llave.
Karel nos va a ayudar a ejemplificar esto. La siguiente tabla muestra
las evaluaciones lógicas que la robot puede realizar acerca de su mundo.
Por ejemplo, si ejecutamos la función frente_abierto()
,
obtendremos el valor TRUE
si efectivamente no hay una pared
enfrente de Karel o el valor FALSE
si hay una pared.
Función en R | Devuelve VERDADERO (TRUE) si… |
---|---|
frente_abierto() | …no hay una pared enfrente de Karel |
frente_cerrado() | … hay una pared enfrente de Karel |
izquierda_abierto() | …no hay una pared a la izquierda de Karel |
izquierda_cerrado() | …hay una pared a la izquierda de Karel |
derecha_abierto() | …no hay una pared a la derecha de Karel |
derecha_cerrado() | …hay una pared a la derecha de Karel |
hay_cosos() | …hay cosos donde se encuentra Karel |
no_hay_cosos() | …no hay cosos donde se encuentra Karel |
karel_tiene_cosos() | …Karel tiene cosos en su mochila |
karel_no_tiene_cosos() | …Karel no tiene cosos en su mochila |
mira_al_este() | …Karel está mirando al este |
mira_al_norte() | …Karel está mirando al norte |
mira_al_oeste() | …Karel está mirando al oeste |
mira_al_sur() | …Karel está mirando al sur |
Podemos usar una estructura condicional para modificar la función
llenar_agujero()
que creamos anteriormente para que Karel
coloque un coso sólo si no había ya uno presente en el
agujero:
# ------------ Definición de funciones auxiliares-----------
llenar_agujero <- function() {
girar_derecha()
avanzar()
if (no_hay_cosos()) {
poner_coso()
}
darse_vuelta()
avanzar()
girar_derecha()
}
# ------------------- Programa principal -------------------
generar_mundo("mundo002")
avanzar()
llenar_agujero()
ejecutar_acciones()
Notar que si bien el uso de sangrías en el código es opcional, decidimos emplearlo para facilitar su lectura. Mantener la claridad en nuestros programas es esencial.
Este tipo de estructura añade una acción a ejecutarse en el caso de
que la condición evaluada no se verifique (es decir, devuelve el valor
FALSE
). La sintaxis es:
Dentro del primer bloque de llaves se escriben las acciones que se
realizan si se cumple la condición, mientras que en el segundo, luego de
la expresión else
, se incluyen las que se realizan si no se
verifica la misma.
Imaginemos que queremos crear un algoritmo para revertir el estado de una celda, es decir, que Karel ponga un coso si no hay o lo quite si es que hay:
Para esto podemos usar una estructura condicional doble:
generar_mundo("mundo001")
if (hay_cosos()) {
juntar_coso()
} else {
poner_coso()
}
avanzar()
if (hay_cosos()) {
juntar_coso()
} else {
poner_coso()
}
avanzar()
if (hay_cosos()) {
juntar_coso()
} else {
poner_coso()
}
ejecutar_acciones()
Dado que repetimos 3 veces, exactamente de la misma forma, el proceso de controlar si hay o no un coso para decidir quitarlo o poner uno, otra vez podemos recurrir al principio de la descomposición algorítmica y definir una función que se encargue de ellos, se forma que la acción de invertir el estado de una celda se haga de manera más sencilla. Nuestro archivo de código quedaría así:
# ------------ Definición de funciones auxiliares-----------
invertir <- function() {
if (hay_cosos()) {
juntar_coso()
} else {
poner_coso()
}
}
# ------------------- Programa principal -------------------
generar_mundo("mundo001")
invertir()
avanzar()
invertir()
avanzar()
invertir()
ejecutar_acciones()
Permiten combinar varias estructuras condicionales para establecer controles más complejos sobre el flujo de las acciones, representando una toma de decisión múltiple. Podemos ejemplificar la sintaxis de la siguiente forma:
if (condición 1) {
...Primer conjunto de acciones...
} else if (condición 2) {
...Segundo conjunto de acciones...
} else {
...Tercer conjunto de acciones...
}
En la estructura anterior, hay una primera evaluación lógica en la
cual si el resultado es VERDADERO
, se ejecuta el primer
conjunto de acciones y nada más. En cambio, si su resultado es
FALSO
, se procede a realizar una segunda evaluación lógica,
que da lugar a la ejecución del segundo o del tercer bloque de acciones,
dependiendo de que su resultado sea VERDADERO
o
FALSO
, respectivamente.
Las estructuras de control iterativas son útiles cuando la solución de un problema requiere que se ejecute repetidamente un determinado conjunto de acciones. El número de veces que se debe repetir dicha secuencia de acciones puede ser fijo o variable dependiendo de algún dato en el algoritmo.
Se aplican cuando se conoce de antemano el número exacto de veces que se debe repetir una secuencia de acciones. Por ejemplo, consideremos el siguiente problema donde hay agujeros distribuidos equiespaciadamente en las avenidas pares.
Tenemos que escribir un programa para que Karel llene los 5 agujeros. Podríamos planear algo como:
# ------------------- Programa principal -------------------
generar_mundo("mundo003")
avanzar()
llenar_agujero()
avanzar()
avanzar()
llenar_agujero()
avanzar()
avanzar()
llenar_agujero()
avanzar()
avanzar()
llenar_agujero()
avanzar()
avanzar()
llenar_agujero()
avanzar()
ejecutar_acciones()
Es evidente que no tiene sentido escribir exactamente lo mismo 5 veces. Por eso vamos a hacer uso de una estructura iterativa:
generar_mundo("mundo003")
for (i in 1:5) {
avanzar()
llenar_agujero()
avanzar()
}
ejecutar_acciones()
La letra i
se usa para representar la cantidad de
repeticiones. En este ejemplo, su única función es guiar la serie de
pasos. El bloque de instrucciones se repite tantas veces como
i
tarde en llegar a 5 partiendo desde 1 (esto se define con
un rango de valores en R usando la nomenclatura 1:5
).
Podríamos haber elegido otra letra u otra palabra en su lugar, pero
emplear i
es bastante común. De manera general, la sintaxis
para este tipo de estructuras es:
for (i in <valor_inicial>:<valor_final>) {
...Acción/es...
}
En otras circunstancias se puede necesitar repetir un bloque de
acciones sin conocer con exactitud cuántas veces, si no que esto depende
de algún otro aspecto del ALGORITMO. Las iteraciones pueden continuar
mientras que se verifique alguna condición. En este
tipo de estructuras, el conjunto de instrucciones se repite mientras que
se siga evaluando como VERDADERO
una condición declarada al
inicio del bloque. Cuando la condición ya no se cumple, el proceso deja
de ejecutarse. La sintaxis es:
Observaciones:
FALSO
inicialmente, entonces las acciones en el cuerpo de la estructura no se
ejecutan nunca.FALSO
en algún punto durante la ejecución de un bloque, el
programa no lo nota hasta que se termine de ejecutar el bloque y la
condición sea evaluada antes de comenzar la próxima iteración.Por ejemplo, sería interesante escribir un programa para llenar agujeros como el anterior, pero que sirva de manera general para otras situaciones donde puede haber cualquier cantidad de agujeros en la calle, como estas:
En vez de usar un for
en el cual hay que especificar la
cantidad de veces que el proceso debe repetirse, podemos usar un
while
para que Karel siga rellenando agujeros mientras que
no haya una pared enfrente suyo, lo cual indicaría que llegó al final y
debe detenerse.
# ------------------- Programa principal -------------------
generar_mundo("mundo003")
while (frente_abierto()) {
avanzar()
llenar_agujero()
avanzar()
}
ejecutar_acciones()
Hay que tener mucho cuidado a la hora de escribir este tipo de estructura, para asegurarse de no producir un loop infinito, es decir, un proceso iterativo que nunca finaliza. Esto ocurriría, por ejemplo, si estando en el mundo anterior, le pedimos a Karel que gire mientras que no haya cosas donde está parada:
# No correr esto! (o sí, para ver cómo no anda!)
generar_mundo("mundo003")
while (no_hay_cosos()) {
girar_izquierda()
}
ejecutar_acciones()
Algunos ejemplos presentados en este tutorial fueron adaptados de Karel the robot learns Java (Eric Roberts, 2005).