Clase 6: Ciclos y arreglos - Departamento de Computación

Anuncio
Programación imperativa
Entidad fundamental: variables
Algoritmos y Estructuras de Datos I
I
Corresponden a posiciones de memoria (RAM).
I
Cambian explı́citamente de valor a lo largo de la ejecución de un
programa.
Segundo cuatrimestre de 2014
Tres instrucciones:
Departamento de Computación - FCEyN - UBA
I
asignación (y llamadas a funciones),
Programación imperativa - clase 6
I
condicional,
Ciclos y arreglos
I
ciclo
Un programa es una lista de instrucciones, que se ejecutan una tras otra.
La semántica de un programa se define mediante la composición de la
semántica de las instrucciones por transformación de estados.
1
Programación imperativa
2
Ciclos
Aunque muchos lenguajes imperativos permiten implementar funciones
recursivas, el mecanismo fundamental de cómputo no es la recursión.
while (B) {cuerpo del ciclo};
Es
Es
Es
Es
Es
Es
Es
Es
Es
Es
Es
Es
Es
Es
la
la
la
la
la
la
la
la
la
la
la
la
la
la
iteración
iteración
iteración
iteración
iteración
iteración
iteración
iteración
iteración
iteración
iteración
iteración
iteración
iteración
(o
(o
(o
(o
(o
(o
(o
(o
(o
(o
(o
(o
(o
(o
repetición
repetición
repetición
repetición
repetición
repetición
repetición
repetición
repetición
repetición
repetición
repetición
repetición
repetición
o
o
o
o
o
o
o
o
o
o
o
o
o
o
ciclos).
ciclos).
ciclos).
ciclos).
ciclos).
ciclos).
ciclos).
ciclos).
ciclos).
ciclos).
ciclos).
ciclos).
ciclos).
ciclos).
B: expresión booleana del lenguaje de programación. Se la llama guarda.
{cuerpo de ciclo}: es un bloque de instrucciones entre llaves. Se repite
mientras valga la guarda B, cero o más veces.
La ejecución del ciclo termina si y solo si el ciclo se repite una cantidad
finita de veces. Esto ocurre si y solo si la guarda B llega a ser falsa.
Si el ciclo termina, el estado resultante es el estado posterior a la última
instrucción del cuerpo del ciclo.
3
4
Ejemplo de un ciclo
Correctitud de un ciclo respecto de una especificación
problema fact(x : Int) = r : Int{
requiere x ≥ 0 Q
asegura r == [1..x]
}
Introducimos en el lenguaje de especificación
PC : precondición del ciclo
QC : postcondición del ciclo
B:
guarda
donde B corresponde a la guarda B del lenguaje de programación.
En funcional:
fact :: Int -> Int
fact 0 = 1
fact i | i>0 = i * (fact (i-1))
int g(int x) {
....
// estado antesdelciclo
// vale PC
while (B) {
cuerpo del ciclo (una o más instrucciones);
}
// estado despuesdelciclo
// vale QC
....
}
En imperativo:
int fact (int x) {
int f = 1;
int i = 1;
while (i ≤ x) {
f = f * i;
i = i + 1;
}
return f;
}
5
Correctitud de un ciclo
PC :
QC :
B:
I:
6
Invariante
precondición del ciclo,
postcondición del ciclo,
guarda
invariante
int g (int x) {
....
// estado antesdelciclo
// vale PC
// implica I
while (B) {
// estado antes
// vale B ∧ I
cuerpo del ciclo (una o más instrucciones)
// estado despues
// implica I
}
// vale I ∧¬B; implica QC
....
}
Expresión booleana del lenguaje de especificación que se mantiene verdadera
en los estados antes, durante y después de la ejecución de un ciclo.
7
I
es análogo a la hipótesis inductiva que usamos para demostrar la
correctitud de un programa recursivo en programación funcional.
I
conviene darlo al escribir el ciclo porque expresa la idea del ciclo.
I
no hay forma algorı́tmica de encontrarlo
8
Un ejemplo
Ejemplo sumat(int x)
int sumat (int x) {
int s = 0, i = x;
while (i > 0) {
// estado a
s = s + i;
i = i - 1;
// estado b
}
return s;
}
problema sumat(x : Int) = r : Int{
requiere x ≥ 0 P
asegura r == [0..x] }
En funcional:
sumat :: Int -> Int
sumat 0 = 0
sumat i = i + (sumat (i-1))
En imperativo:
int sumat (int x) {
int s = 0, i =x;
while (i ≥ 0) {
s = s + i;
i = i - 1;
}
return s;
}
Estados para x == 4
i@a
s@b
i@b
4
4
3
3
7
2
2
9
1
1
10
0
P
i@a
([email protected]] i@b
P
En estado a y b se cumple 0 ≤ i ≤ x y s == (i..x] (pero no en el medio)
Cuando i == 0, la ejecución del ciclo termina.
s@a
0
4
7
9
P
([email protected]]
9
Ejemplo sumat(int x)
10
Transformación de estados dentro del ciclo
La x no cambia porque es de entrada y no aparece en modifica ni en local.
//estado e1
//vale B: i >0
P
//vale I : 0 ≤ i ≤ x ∧ s == (i..x]
s = s + i;
//estado e2
//vale i = i@e1 ∧ s == s@e1 + i@e1
i = i - 1;
//estado e3
//vale i == i@e2 − 1 ∧ s == s@e2
//implica i == i@e1 − 1 ∧ s == s@e1 + i@e1
porque i@e2 == i@e1 y s@e2 == s@e1 + i@e1.
//implica 0 ≤ i ≤ x
porque 0 < i@e1 ≤ x y i@e3 == i@e1 − 1
P
//implica s == (i..x]
P
porque s@e1 == ([email protected]] y
int sumat (int x) {
int s = 0; i = x;
//vale PC : s == 0 ∧ i == x
while (i > 0) {
P
//invariante I : 0 ≤ i ≤ x ∧ s == (i..x]
//estado e1
s = s + i;
//estado e2
i = i - 1;
//estado e3
}
P
//vale QC : i == 0 ∧ s == (0..x]
return s;
P
//vale result == [0..x]
}
s@e3 == s@e2 == s@e1 + i@e1 ==
P
([email protected]] 11
P
P
([email protected]] + i@e1 == (i@e1 − 1..x] ==
12
¿Cómo demostramos que el ciclo termina?
Terminación y correctitud de un ciclo respecto de una especificación
Acotamos superiormente la cantidad de iteraciones restantes del ciclo
mediante una expresión variante (v )
//vale PC ;
while (B) {
//invariante I ;
cuerpo
}
//vale QC ;
Es una expresión del lenguaje de especificación, de tipo Int
I
definida a partir de las variables del programa
I
debe decrecer estrictamente en cada iteración
//estado e;
//vale B ∧ I ;
cuerpo
//vale v < v @e;
Un ciclo termina si después de una cantidad finita de iteraciones se
cumple la negación de la guarda.
Un ciclo es correcto respecto a la especificación si comenzando su
ejecución en un estado que cumple PC el ciclo termina en un estado que
cumple QC .
Damos una cota (c) (valor entero fijo, por ejemplo 0 o −8) tal que si es
alcanzado por la expresión variante, está garantizado que la ejecución
alcanza un estado donde vale la negación de la guarda, y por lo tanto
sale del ciclo.
13
Ejemplo de expresión variante
14
Ejemplo de demostración de expresión variante decreciente.
Recordemos el programa sumat y la transformación de estados
P
//invariante I : 0 ≤ i ≤ x ∧ s == (i..x]
//variante v : i
//estado e1
//vale B ∧ I
P
//implica 0 < i ≤ x ∧ s == (i0..x]
s = s + i;
//estado e2
//vale i == @e1 ∧ s == s@e1 + e@1
i = i - 1;
//estado e3
//vale i == i@e2 − 1 ∧ s == s@e2
int sumat (int x) {
int s = 0; i = x;
//vale PC : s == 0 ∧ i == x
while (i > 0) {
P
//invariante I : 0 ≤ i ≤ x ∧ s == (i..x]
s = s + i;
i = i - 1;
}
P
//vale QC : i == 0 ∧ s == (0..x]
return s;
P
//vale result == [0..x]
}
Debemos ver que la expresión variante v : i es decreciente.
Es decir, debemos ver que v @e1 > v @e3.
Pero esto es inmediato, porque
v @e1 == i@e1 y v @e3 == i@3 == i@e2 − 1 == i@e1 − 1.
La expresión variante es un indicador de la distancia a la terminación del ciclo.
//variante v : i, cota 0
15
16
Teorema de terminación
Observaciones sobre terminación
Sea el siguiente programa:
Sea I el invariante de un ciclo con guarda B, v una expresión variante v
(expresión entera decreciente) y c una cota tal que (I ∧ v ≤ c) → ¬B.
Entonces el ciclo termina.
int dec1(int x) {
while (x > 0)
x = x − 1;
return x;
}
Demostración.
Sea vj el valor que toma v en el estado que resulta de ejecutar el cuerpo
del ciclo por j-ésima vez. Dado que v es de tipo Int, para todo j,
vj ∈ Int.
Supongamos
Como v es estrictamente decreciente, v1 > v2 > v3 > . . . , existe un k tal
que vk ≤ c. Dado que (I ∧ v ≤ c) → ¬B, en el estado alcanzado luego
de k iteraciones vale ¬B. Por lo tanto el ciclo termina.
Pc : x ≥ 0.
I :x ≥0
v = x es estrictamente decreciente, cota c = 0.
Dado que B : x > 0, se cumple v ≤ c → ¬B
y por simple cálculo proposicional, (I ∧ v ≤ c) → ¬B.
Es decir, no fue necesario usar el invariante I .
¿Qué pasarı́a si v fuese de tipo Float? ¿Funcionarı́a esta demostración?
17
Observaciones sobre terminación
18
Expresión variante y cota
int dec2(int x) {
while (x != 0)
x = x − 1;
return x;
}
Sea v una función variante y sea c su cota asociada.
I
v 0 = v − c es una función variante con cota asociada 0.
Sin pérdida de generalidad, podemos suponer siempre una función
variante y cota asociada 0.
Supongamos
Pc : x ≥ 0
I :x ≥0
v = x, cota c = 0.
Dado que B : not(x == 0), (I ∧ v ≤ c) → ¬B
I
v es estrictamente decreciente en las sucesivas iteraciones del ciclo.
En cada iteración decrece en 1 o más unidades. Por lo tanto el valor
de la expresión variante no necesariamente mide la cantidad de
iteraciones (restantes) de la ejecución del ciclo.
Notemos que en este caso sı́ necesitamos usar I
ya que x ≤ 0 6→ x == 0
pero (x ≥ 0 ∧ x ≤ 0) → x == 0.
19
20
Teorema de correctitud de un ciclo
Teorema del Invariante
Sea while (B) { cuerpo } un ciclo con guarda B, precondición PC y
poscondición QC . Sea I un predicado booleano, v una expresión variante, c una
cota, y sean los estados e1 y e2 ası́:
while (B) {
//estado e1
cuerpo
//estado e2}
Si valen:
Sea PC , QC , I , B, v la especificación de un ciclo que termina.
Si se cumplen las relaciones de fuerza PC → I y (I ∧ ¬B) → QC
entonces el ciclo es correcto con respecto a su especificación.
Demostración. Debemos ver que para variables que satisfagan PC , el estado
alcanzado cuando el ciclo termina satisface QC .
//estado e1
//vale PC ;
while (B) {
//vale I ∧ B;
cuerpo
//vale I ;
}
//estado e2
//vale QC ;
Supongamos que las variables en el estado e1 satisfacen PC como PC → I ,
entonces en el estado e1 se cumple I . Supongamos que el ciclo ejecuta 0 o más
veces. Por definición de invariante en cada iteración, el invariante se restablece.
Por hipótesis, el ciclo termina y en el estado e2 vale ¬B. Además, en el estado
e2 vale I .
Como (I ∧ ¬B) → QC entonces en el estado e2 vale QC .
1. PC → I
2. (I ∧ ¬B) → QC
3. el invariante se preserva en la ejecución del cuerpo, i.e. si I ∧ B vale en el
estado e1 entonces I vale en el estado e2
4. v es decreciente, i.e. v @e1 > v @e2
5. (I ∧ v ≤ c) → ¬B
entonces para cualquier valor de las variables del programa que haga verdadera
PC , el ciclo termina y hace verdadera QC , es decir, el ciclo es correcto para su
especificación (PC , QC ).
Demostración. Inmediata del Teorema de Terminación (p. 17) y el Teorema de
Correctitud (p. 21).
22
21
¿Cuales de los 5 puntos entran en el primer parcial?
Retomamos ejemplo sumat
Teoema del Invariante. Sea while (B) { cuerpo } un ciclo con guarda B,
precondición PC y poscondición QC . Sea I un predicado booleano, v una
expresión variante, c una cota. Si valen:
1. PC → I
problema sumat(x : Int) = r : Int{
requiere P : x ≥
P0
asegura r == [0..x] }
2. (I ∧ ¬B) → QC
1. PC → I
3. el invariante se preserva en la ejecución del cuerpo, i.e. si I ∧ B vale en el
estado e1 entonces I vale en el estado e2
//vale PC : s == 0 ∧ i == x
P
//invariante I : 0 ≤ i ≤ x ∧ s == (i..x]
4. v es decreciente, i.e. v @e1 > v @e2
Supongamos que vale PC y veamos que vale I
5. (I ∧ v ≤ c) → ¬B
1. i == x implica i ≥ x.
entonces para cualquier valor de las variables del programa que haga verdadera
PC , el ciclo termina y hace verdadera QC , es decir, el ciclo es correcto para su
especificación (PC , QC ).
2. x ≥ 0 implica i ≥ 0.
P
P
(i..x] == (x..x] == 0 == s.
P
Por lo tanto, (i == x ∧ s == 0) → (0 ≤ i ≤ x ∧ s == (i..x]).
3. s == 0 y i == x implica
Los puntos 1,2 y 5 se demuestran usando lógica proposicional exclusivamente.
Los puntos 3 y 4 quedan para después del primer parcial.
23
24
Retomamos ejemplo sumat
Retomamos ejemplo sumat
2. (I ∧ ¬B) → QC
5. (I ∧ v ≤ c) → ¬B
P
// invariante I : 0 ≤ i ≤ x ∧ s == (i..x]
// ¬B : ¬(i > 0)
P
//vale QC : i == 0 ∧ s == (0..x]
//invariante I : 0 ≤ i ≤ x ∧ s ==
//v ≤ c : i ≤ 0
// ¬(B) : ¬(i > 0)
Supongamos que vale I ∧ ¬B y veamos que vale cada parte de QC .
Por I sabemos que 0 P
≤ i. Por ¬B sabemos i ≤ 0. Entonces, i == 0.
Por I sabemos
s
==
(i..x], y recién mostramos que i == 0, luego
P
s == (0..x]. P
(i..x]
Supongamos que vale I ∧ (v ≤ c). Debemos ver que vale ¬B.
Esto es inmediato ya que (v ≤ c) == i ≤ 0 == ¬B. 26
25
Arreglos
Arreglos versus listas
I
Secuencias de una cantidad fija de variables del mismo tipo.
Se declaran con un nombre, un tamaño y un tipo.
I
Ejemplo:
I
En el lenguaje de especificación tratamos a ambos con listas por
comprensión.
int a[10]; // arreglo de 10 elementos enteros
int a[]={0,1,2,3,4,5,6,7,8,9}; //Inicializado
2
I
-1
1
0
0
2
-1
1
0
0
Nos referimos a los elementos a través del nombre del arreglo y un
ı́ndice, entre corchetes, que va de 0 a la dimensión menos uno:
Una referencia a una posición fuera del rango (usualmente) da error
(en tiempo de ejecución).
I
El tamaño y el tipo de un arreglo se mantienen invariantes a lo largo
de la ejecución.
I
Longitud
Los arreglos tienen longitud fija; las listas, no.
I
Acceso
I
I
a[0], a[1],..., a[9] //acceso aleatorio
I
Ambos son secuencias de elementos de un tipo dado.
I
27
Los elementos de un arreglo se acceden de manera directa, e
independiente de los demás.
a[i] accede al elemento en posición i-ésima del arreglo. Tiene
dirección de memoria propia, y por lo tanto se le puede asignar
un valor.
Los elementos de una lista se acceden secuencialmente,
empezando por la cabeza. Para acceder al i-ésimo elemento de
una lista, hay que obtener i veces la cola y luego la cabeza.
28
Arreglos en C++
I
Leyendo un arreglo
problema sumarray(a : [Int], tam : Int) = res : Int{
requiere P: tam ==P
|a|;
asegura Q: res ==
a[0..|a|); }
Los arreglos en C++ son referencias.
I
No hay forma de averiguar su tamaño una vez que fueron creados;
el programa tiene que encargarse de almacenarlo de alguna forma.
I
Al ser pasados como argumentos, en la declaración de un parámetro
no se indica su tamaño.
int sumarray(int a[], int tam){
int j = 0;
int s = 0;
P
// Pc: j == 0 ∧ s == a[0..j)
while (j <tam) {
P
// invariante 0 ≤ j ≤ tam ∧ s ==
a[0..j)
// variante tam − j;
s = s + a[j];
j++;
I
Cuando se usan como argumentos de funciones en C y C++, pasan
automáticamente como referencia (¡no por copia!).
}
P
// Qc : s == a[0..|a|)
return s;
P
// vale Q: res == a[0..|a|);
}
29
Inicialización de arreglos
30
Modificando un arreglo
problema init (a : [Int], x : Int, n : Int){
requiere n == |a| ∧ n > 0;
modifica a;
asegura todos([a[j] == x, j ∈ [0..n)]);
asegura todos([a[j] == x, j ∈ [0..n)]);
}
problema ceroPorUno(a : [Int], tam : Int){
requiere tam == |a|;
modifica a;
asegura a == [if i == 0 then 1 else i | i ← pre(a)[0..tam)]; }
void ceroPorUno(int a[], int tam) {
int j = 0;
// vale Pc : j == 0
while (j < tam) {
// invariante a[0..j) == [if i == 0 then 1 else i | i ← pre(a)[0..j)];
// ∧0 ≤ j ≤ tam ∧ a[j..tam) == pre(a)[j..tam)
// variante tam − j;
if (a[j] == 0) a[j] = 1;
j++;
}
}
void init (int a[], int x, int n) {
int i = 0;
// vale Pc : i == 0
while (i < n) {
a[i]=x;
i = i + 1;
// invariante I: 0 ≤ i ≤ n ∧todos([a[j] == x, j ∈ [0..i)])
// variantev : n − i
}
// vale Qc : i == n ∧ todos([a[j] == x, j ∈ [0..i)])
}
31
32
Descargar