Clase 12 - Departamento de Computación

Anuncio
Ordenamiento de un arreglo
Tenemos un arreglo de un tipo T con una relaciı̈¿ 12 n de orden (≤) y
queremos modificar el arreglo para que sus elementos queden en orden
creciente.
Algoritmos y Estructuras de Datos I
problema sort<T> (a : [T], n : Int) {
requiere n == |a| > 1;
modifica a;
asegura mismos(a, pre(a)) ∧ ordenado(a);
}
Segundo cuatrimestre de 2014
Departamento de Computación - FCEyN - UBA
Algoritmos - clase 12
Adoptamos como estrategia que modificamos el arreglo permutando
elementos, para garantizar que nuestro algoritmo no pierde ningún
elemento del arreglo.
Algoritmos de ordenamiento, segunda parte
Conocemos el algoritmo upsort, con complejidad O(n2 ).
1
Cota inferior de complejidad tiempo para sorting
2
Demostración
Supongamos que ordenamos esta secuencia: 3, 2, 4, 5, 1.
El método comparará los números, y según sean los resultados de las
comparaciones permutará los elementos. El efecto neto será:
Consideremos algoritmos de ordemiento basados, exclusivamente, en
operaciones de comparar elementos y permutarlos.
Teorema: Sea cualquier método de ordenamiento
(inventado o por inventarse).
Ordenar n elementos toma al menos log2 (n!) pasos.
El método no puede ejecutar exactamente de la misma manera si la
secuencia es otra, por ejemplo: 2, 3, 4, 5, 1
¿Por qué? Si lo hiciera, el resultado neto serı́a
3
Concluı́mos que la ejecución debe ser una sucesión de pasos distinta para
cada permutación posible de la secuencia de entrada.
4
Visualicemos todas las ejecuciones como un árbol de ejecuciones.
Cada nodo es, o bien una asignación, o bien una comparación.
El árbol se ramifica en cada comparación.
Cada camino desde la raı́z hasta una hoja es una ejecución del programa.
La altura del árbol da el peor caso de cantidad de instrucciones
ejecutadas por el programa.
Debe tener al menos n! hojas.
Sabemos que un árbol binario con L hojas tiene altura al menos log2 L.
Por lo tanto nuestro árbol tiene altura al menos log2 n!.
Si éste es el árbol de ejecuciones de un programa de ordenamiento,
que ordena n elementos, para cada una de las n! permutaciones de la
secuencia de entrada, la ejecución debe alcanzar una hoja diferente.
El árbol debe tener al menos n! hojas.
Concluı́mos que el ordenamiento, en el peor caso, requiere al menos
log2 n! pasos.
5
ı̈¿ 12 Ordenamiento mı̈¿ 21 s rı̈¿ 12 pido?
6
Algoritmo sort count
En la materia Algoritmos y Estructuras de Datos II verı̈¿ 21 n algoritmos
que tienen complejidad O(n log n). Y estudiarı̈¿ 12 n a fondo las ventajas y
desventajas de cada uno.
sort count es un algoritmo eficiente para ordenar un arreglo de enteros de
dimensión n, cuyos valores están entre 0 y n − 1.
Para poder hacer un ordenamiento más rápido necesitamos...
saber más acerca de la secuencia de entrada, de forma tal de poder
ahorrarnos comparaciones entre elementos.
7
8
Algoritmo sort count
Algoritmo sort count
problema sort count(a : [Int], n : Int) {
requiere n == |a| ≥ 1;
requiere (∀ i ∈ [0..|a|)) 0 ≤ ai < |a|;
modifica a;
asegura mismos(a, pre(a)) ∧ ordenado(a);
}
Sea a el arreglo de entrada.
aux ordenado(c) = (∀i ∈ [0..|c| − 1))c[i] ≤ c[i + 1]
El algoritmo sort count usa una estructura auxiliar b, de la misma
dimensiı̈¿ 12 n que a, donde contaremos la cantidad de ocurrencias de cada
elemento. En b[i] indicaremos la cantidad de ocurrencias de i en a.
Esto es posible porque los elementos de a son enteros mayores o iguales
que 0 y menores que la dimensiı̈¿ 21 n de a.
aux
mismos(b, c) = |b| == |c| ∧ todos([cuenta(x, b) == cuenta(x, c)|x ∈ b])
Luego, ordenaremos a escribiendo de izquierda a derecha cada uno de sus
elementos con su correspondiente cantidad de ocurrencias registrada en b.
10
9
Algoritmo sort count
Algoritmo sort count
problema cuenta ocurrencias(a : [Int], n : Int, b : [Int]){
requiere |a| == n;
requiere |b| == n;
requiere todos([0 ≤ a[i] < n | i ∈ [0..n)]);
modifica b;
asegura b == [cuenta(i,
P a) | i ∈ [0..n)];
aux cuenta(x, c) = [β(x == c[j]) | j ∈ [0..|c|)];
}
problema init(a : [Int], val : Int, n : Int){
requiere n == |a| ∧ n > 0;
modifica a;
asegura todos([a[j] == val, j ∈ [0..n)]);
}
problema asigna(a : [Int], desde : Int, cant : Int, val : Int){
requiere desde ≥ 0 ∧ desde + cant ≤ |a|;
modifica a;
asegura todos([a[j] == val, j ∈ [desde..desde + cant)]);
}
problema reconstruye(a : [Int], n : Int, b : [Int]){
requiere |a| == n;
requiere |b| == n;
modifica a;
asegura ordenado(a);
asegura (∀i ∈ [0..n))cuenta(i,
a) == b[i];
P
aux cuenta(x, c) = [β(x == c[j]) | j ∈ [0..|c|)];
}
11
12
Cuenta ocurrencias
Reconstruye el arreglo ordenado a partir de las ocurrencias
void cuenta ocurrencias(int a[], int b[], int n){
init(b,0,n);
int i=0;
// Pc:i == 0 ∧ (∀j ∈ [0..|b|))b[j] == 0;
while (i< n) {
// invariante I: 0 ≤ i ≤ |a|∧
//
(∀j ∈ [0..i))b[a[j]] == cuenta(a[j], a[0..i));
// variante v: n − i, cota 0
b[a[i]]=b[a[i]]+1;
i++;
}
// Qc: (∀i ∈ [0..|a|))cuenta(i, a) == b[i];
}
13
Algoritmo sort count
void reconstruye(int a[], int b[], int n){
int i=0;
int j=0;
// Pc: i == 0 ∧ j == 0;
while (i<n) {
P
// invariante I: (0 ≤ i ≤ n) ∧ (0 ≤ P
j ≤ n) ∧ ordenado(a[0.. b[0..i))∧
//
(∀k ∈ [0..i))cuenta(k, a[0.. b[0..i))) == b[k];
// variante v:n − i
asigna(a,j,b[i], i);
j=j+b[i];
i++;
}
// Qc: ordenado(a) ∧ (∀k ∈ [0..|a|))cuenta(k, a) == b[k];
}
void asigna(int a[], int desde, int cant, int val){
int k = desde;
// Pc: k == desde;
while (k < desde+cant){
// invariante I: (desde ≤ k ≤ desde + cant) ∧ todos([a[m] == val|m ∈ [desde..k)])
// variante v: desde + cant − k, cota 0;
a[k] = val;
k++;
}
// Qc:todos([a[m] == val|m ∈ [desde..desde + cant)]);
}
14
sort count tiene complejidad lineal
void sort count(int a[], int n) {
int b[n];
// estado e0 ;
// vale P: a == pre(a) ∧ n == |a| ≥ 1 ∧ (∀ i ∈ [0..|a|)) 0 ≤ ai < |a|;
cuenta ocurrencias(a,b,n);
// estado e1 ;
// vale a== a@e0;
// vale (∀k ∈ [0..|a|)) cuenta(k, a) == b[k];
reconstruye(a,b,n);
// estado e2 ;
// vale ordenado(a) ∧ (∀k ∈ [0..|a|)) cuenta(k, a) == b[k];
// implica ordenado(a) ∧ (∀k ∈ [0..|a|)) cuenta(k, a) == cuenta(k, a@e0);
porque b[k] == cuenta(k, a@e0) ∧ (∀ i ∈ [0..|a|)) 0 ≤ (a@e0)i < |a|]);
// implica Q: ordenado(a) ∧ mismos(a, pre(a));
}
Los elementos de a son mayores o iguales que 0 y menores que la
longitud de a.
El algoritmo usa un arreglo auxiliar b de la misma longitud que a.
15
I
Para contar ocurrencias de los elementos de a leemos a una sola
vez, de izquierda a derecha, mientras acumulamos en b.
I
Para reconstruir a necesitamos leer b una sola vez, de izquierda a
derecha, mientras escribimos a tambiı̈¿ 12 n de izquierda a derecha.
I
El algoritmo tiene complejidad O(|a|).
16
Problema de la bandera holandesa
Algoritmo de la bandera holandesa
La postcondición se puede escribir de la siguiente forma equivalente:
Supongamos que tenemos la siguiente hipótesis adicional:
asegura mismos(a, pre(a)) ∧
(∃ i, j ∈ [0..|a|), i ≤ j) a[0..i) == 0 ∧ a[i..j) == 1 ∧ a[j..|a|) == 2
problema dfp(a : [Int], n : Int) {
requiere n == |a| ≥ 1;
requiere (∀ i ∈ [0..|a|)) 0 ≤ ai ≤ 2;
modifica a;
asegura mismos(a, pre(a)) ∧ ordenado(a);
}
Abusando notación, definimos:
a[d, h) == x ⇔ (∀` ∈ [d, h)) a` == x
En forma gráfica:
Queremos un algoritmo de complejidad lineal, O(n) para n = |a|, pero
que no use una estructura de datos adicional para contar ocurrencias.
Queremos modificar el arreglo haciendo swaps.
0
1
i
2
j
18
17
Algoritmo de la bandera holandesa
Programa de la bandera holandesa
El invariante se obtiene haciendo variar las variables i, j, k:
void dfp(int a[], int n) {
int i = 0;
int j = 0;
int k = n;
while( j != k ) {
if( a[j] == 1 )
j++;
else if( a[j] == 2 ) {
swap(a,j,k-1); k--;
}
else if( a[j] == 0 ) {
swap(a,i,j); i++; j++;
}
}
I: mismos(a, pre(a)) ∧
0 ≤ i ≤ j ≤ k ≤ |a| ∧ a[0..i) = 0 ∧ a[i..j) == 1 ∧ a[k..|a|) == 2
En forma gráfica:
0
1
i
?
j
2
k
El invariante es equivalente a la postcondición cuando j==k. Con ésto
deducimos la guarda del ciclo.
Para inicializar trivialmente un estado que cumple el invariante hacemos:
i = 0;
j = 0;
k = n;
asumiendo |a| == n.
19
20
Programa de la bandera holandesa
Caso 1: aj == 1
0
void dfp(int a[], int n) {
int i = 0;
int j = 0;
int k = n;
while( j != k ) {
I ∧ B : ... ∧ a[0..i) == 0 ∧ a[i..j) == 1 ∧ a[k..n) == 2;
if( a[j] == 1 )
I...
∧ j 6= k ∧ aj == 1;
else if( a[j] == 2 )
I...
∧ j 6= k ∧ aj == 2;
else if( a[j] == 0 )
I...
∧ j 6= k ∧ aj == 0;
}
}
1
i
1
j
2
k
E1 : mismos(a, pre(a)) ∧ 0 ≤ i ≤ j < k ≤ |a| ∧ aj == 1 ∧
a[0..i) == 0 ∧ a[i..j) == 1 ∧ a[k..|a|) == 2
j++;
E2 : a = a@E1 ∧ j = j@E1 + 1 ∧ i = i@E1 ∧ k = k@E1
implica mismos(a, pre(a))
implica 0 ≤ i ≤ j ≤ k ≤ |a|
implica aj−1 == 1 ∧ a[i..j − 1) == 1
implica a[i..j − 1] == 1
implica I
I: mismos(a, pre(a)) ∧ 0 ≤ i ≤ j ≤ k ≤ |a| ∧
a[0..i) == 0 ∧ a[i..j) == 1 ∧ a[k..|a|) == 2
22
21
Caso 2: aj == 2
Caso 3: aj == 0
0
1
i
2
j
2
0
k
E1 : mismos(a, pre(a)) ∧ 0 ≤ i ≤ j < k ≤ |a| ∧ aj == 2 ∧
a[0..i) == 0 ∧ a[i..j) == 1 ∧ a[k..|a|) == 2
0
j
2
k
E1 : mismos(a, pre(a)) ∧ 0 ≤ i ≤ j < k ≤ |a| ∧ aj == 0 ∧
a[0..i) == 0 ∧ a[i..j) == 1 ∧ a[k..|a|) == 2
swap(a,j,k-1);
swap(a,i,j);
E2 : aj == (a@E1 )i ∧ ai == (a@E1 )j
∧ (∀t ∈ [0..|a|), t 6= i, t 6= j)at == at @E1
∧ i == i@E1 ∧ j == j@E1 ∧ k == k@E1
implica mismos(a, pre(a)) ∧ 0 ≤ i ≤ j < k ≤ |a|
implica ai == 0 ∧ (i < j ⇒ aj == 1)
implica a[0..i] == 0 ∧ a(i..j) == 1 ∧ a[k..|a|) == 2
i++;
E3 : a == a@E2 ∧ i == i@E2 + 1 ∧ j == j@E2 ∧ k = k@E2
implica mismos(a, pre(a))
implica 0 ≤ i − 1 ≤ j < k ≤ |a|
implica (i − 1 < j ⇒ aj == 1)
implica a[0..i) == 0 ∧ a[i..j) == 1 ∧ a[k..|a|) == 2
E2 : aj == (a@E1 )k−1 ∧ ak−1 == (a@E1 )j
∧ (∀t ∈ [0..|a|), t 6= j, t 6= k − 1) at == (a@E1 )t
∧ i == i@E1 ∧ j == j@E1 ∧ k == k@E1
implica mismos(a, pre(a)) ∧ 0 ≤ i ≤ j < k ≤ |a|
implica ak−1 == 2
implica a[0..i) == 0 ∧ a[i..j) == 1 ∧ a[k − 1..|a|) == 2
k--;
E3 : a == a@E2 ∧ i == i@E2 ∧ j == j@E2 ∧ k = k@E2 − 1
implica mismos(a, pre(a))
implica 0 ≤ i ≤ j ≤ k ≤ |a|
implica a[0..i) == 0 ∧ a[i..j) == 1 ∧ a[k..|a|) == 2
implica I
1
i
23
24
Caso 3: aj == 0
Correctitud del programa la bandera holandesa
0
1
i
1
j
Tenemos el cuerpo del ciclo, que (1) preserva el invariante.
i = 0;
j = 0;
k = n;
while( j != k ) {
I ∧ B : ... ∧ a[0..i) == 0 ∧ a[i..j) == 1 ∧ a[k..|a|) == 2
if( a[j] == 1 )
j++;
else if( a[j] == 2 ) {
swap(a,j,k-1); k--;
}
else if( a[j] == 0 ) {
swap(a,i,j); i++; j++;
}
}
2
k
E3 : mismos(a, pre(a)) ∧
0 ≤ i − 1 ≤ j < k ≤ |a| ∧
(i − 1 < j ⇒ aj == 1) ∧
a[0..i) == 0 ∧ a[i..j) == 1 ∧ a[k..|a|) == 2
j++;
E4 : a == a@E2 ∧ i == i@E2 ∧ j == j@E2 + 1 ∧ k == k@E2
implica mismos(a, pre(a))
implica 0 ≤ i ≤ j ≤ k ≤ |a|
implica (i < j⇒ aj−1 == 1)
implica a[0..i) == 0 ∧ a[i..j) == 1 ∧ a[k..|a|) == 2
implica I
25
Correctitud del programa la bandera holandesa
26
Correctitud del programa la bandera holandesa
En cada una de las tres ramas, (4) el variante decrece: k − j; cota 0
E1 : mismos(a, pre(a)) ∧ 0 ≤ i ≤ j < k ≤ |a| ∧ aj == 1 ∧ ...
j++;
E2 : k == k@E1 ∧ j == j@E1 + 1
implica k − j < (k − j)@E1
Por construcción, (2) la inicialización establece el invariante:
Pc : i == 0 ∧ j == 0 ∧ k == |a| ∧ a == pre(a)
implica
I : mismos(a, pre(a)) ∧ 0 ≤ i ≤ j ≤ k ≤ |a| ∧
a[0..i) == 0 ∧ a[i..j) == 1 ∧ a[k..|a|) == 2
E1 : mismos(a, pre(a)) ∧ 0 ≤ i ≤ j < k ≤ |a| ∧ aj == 2 ∧ ...
swap(a,j,k-1);
k--;
E2 : k == k@E1 − 1 ∧ j == j@E1
implica k − j < (k − j)@E1
Además, (3) al salir del ciclo vale Qc:
I ∧ ¬B: mismos(a, pre(a)) ∧ 0 ≤ i ≤ j == k ≤ |a| ∧
a[0..i) == 0 ∧ a[i..j) == 1 ∧ a[k..|a|) == 2
implica
Qc: mismos(a, pre(a)) ∧
(∃ i, j ∈ [0..|a|), i ≤ j) a[0..i) == 0 ∧ a[i..j) == 1 ∧ a[j..|a|) == 2;
E1 : mismos(a, pre(a)) ∧ 0 ≤ i ≤ j < k ≤ |a| ∧ aj == 0 ∧ ...
swap(a,i,j);
i++;
j++;
E2 : k == k@E1 ∧ j == j@E1 + 1
implica k − j < (k − j)@E1
27
28
Correctitud del programa la bandera holandesa
Complejidad del algoritmo de la bandera holandesa
¿Cuál es la complejidad del algoritmo?
Finalmente, (5) cuando el variante pasa la cota, el ciclo termina, ya que
1. Antes de comenzar las iteraciones, v = |a|.
k − j ≤ 0 ⇔ ¬B.
2. En cada iteración, el variante decrece estrictamente.
3. Cuando v ≤ 0, el ciclo termina.
Por lo tanto, el ciclo es correcto respecto de su especificación.
Entonces, el algoritmo realiza a lo sumo |a| iteraciones
(realiza exactamente |a| iteraciones, dado que en cada paso el variante
decrece en exactamente una unidad).
Luego, como Qc implica las poscondición del problema (son iguales en
este caso), el algoritmo es correcto respecto de su especificación.
El algoritmo tiene complejidad O(n).
29
30
Descargar