Ordenamiento de un arreglo Algoritmos y Estructuras de Datos I Tenemos un arreglo de elementos de un tipo T . Queremos modificar el arreglo Segundo cuatrimestre de 2011 Departamento de Computación - FCEyN - UBA I para que sus elementos queden en orden creciente I vamos a hacerlo permutando elementos Si pre(a) == [2, 1, 2, 1], debo terminar con a == [1, 1, 2, 2]. Programación imperativa - clase 4 Algoritmos de ordenamiento 1 La especificación 2 El algoritmo Upsort Usamos ≤ para denotar una relación de orden entre elementos de T . problema sort<T> (a : [T], n : Int){ requiere n == |a| ∧ n ≥ 1; modifica a; asegura mismos(a, pre(a)) ∧ (∀j ∈ [0..n − 1)) aj ≤ aj+1 ; } I ordenamos de derecha a izquierda I el segmento a ordenar va desde el principio hasta la posición que vamos a llamar actual I comenzamos con actual = n − 1 mientras actual > 0 I aux cuenta(x : T, a : [T]) : Int = |[y | y ∈ a, y == x]|; I I aux mismos(a, b : [T]) : Bool = |a| == |b| ∧ (∀x ∈ a) cuenta(x, a) == cuenta(x, b); I 3 encontrar el mayor elemento del segmento intercambiarlo con el de la posición actual decrementar actual 4 Ejemplo de Upsort Especificación del ciclo arreglo a[] de dimensión n, variable actual 9 2 2 2 2 1 5 5 5 1 1 2 2 2 2 2 2 2 7 7 1 5 5 5 1 1 7 7 7 7 2 9 9 9 9 9 PC : a == pre(a) ∧ actual == n − 1 QC : mismos(a, pre(a)) ∧ (∀j ∈ [0..n − 1)) aj ≤ aj+1 B : actual > 0 invariante I : 0 ≤ actual ≤ n − 1 ∧ mismos(a, pre(a)) ∧ (∀k ∈ (actual..n − 1)) ak ≤ ak+1 ∧ actual < n − 1 → (∀x ∈ a[0..actual]) x ≤ aactual+1 variante v : actual 6 5 El programa Correctitud de Upsort void upsort (int a[], int n) { void upsort (int a[], int n) { int m, actual = n-1; while (actual > 0) { m = maxPos(a,0,actual); swap(a[m],a[actual]); actual--; } } // vale P : 1 ≤ n == |a| (es la precondición del problema) int m, actual = n-1; // vale PC : a == pre(a) ∧ actual == n − 1 (es la precondición del ciclo) while (actual > 0) { m = maxPos(a,0,actual); swap(a[m],a[actual]); actual--; } problema maxPos(a : [Int], desde, hasta : Int) = pos : Int{ requiere 0 ≤ desde ≤ hasta ≤ |a| − 1; asegura desde ≤ pos ≤ hasta ∧ (∀x ∈ a[desde..hasta]) x ≤ apos ; } // vale QC : mismos(a, pre(a)) ∧ (∀j ∈ [0..n − 1)) aj ≤ aj+1 (es la poscondición del ciclo) // vale Q : mismos(a, pre(a)) ∧ (∀j ∈ [0..n − 1)) aj ≤ aj+1 (es la poscondición del problema) problema swap(x, y : Int){ modifica x, y ; asegura x == pre(y ) ∧ y == pre(x); } } Como QC ≡ Q, lo que queda es probar que el ciclo es correcto para su especificación. 7 8 Especificación del ciclo Correctitud del ciclo void upsort (int a[], int n) { int m, actual = n-1; // vale PC // implica I while (actual > 0) { // vale PC : a == pre(a) ∧ actual == n − 1 // estado E // vale I ∧ actual > 0 while (actual > 0) { 1. El cuerpo del ciclo preserva el invariante 2. La función variante decrece m = maxPos(a,0,actual); 3. Si la función variante pasa la cota, el ciclo termina: swap(a[m],a[actual]); actual--; v ≤ 0 ⇒ ¬(actual > 0) // invariante I : 0 ≤ actual ≤ n − 1 ∧ mismos(a, pre(a)) ∧ (∀k ∈ (actual..n − 1)) ak ≤ ak+1 ∧ actual < n − 1 → (∀x ∈ a[0..actual]) x ≤ aactual+1 // variante v : actual // vale I // vale v < v @E m = maxPos(a,0,actual); swap(a[m],a[actual]); actual--; 4. La precondición del ciclo implica el invariante } 5. La poscondición vale al final // vale I ∧ ¬(actual > 0) // implica QC El Teorema del Invariante nos garantiza que si valen 1, 2, 3, 4, 5 el ciclo termina y es correcto con respecto a su especificación. } // vale QC : mismos(a, pre(a)) ∧ (∀j ∈ [0..n − 1)) aj ≤ aj+1 } 9 1. El cuerpo del ciclo preserva el invariante 10 1. El cuerpo del ciclo preserva el invariante (cont.) // estado E (invariante + guarda del ciclo) // vale 0 < actual ≤ n − 1 ∧ mismos(a, pre(a)) ∧(∀k ∈ (actual..n − 1)) ak ≤ ak+1 ∧(actual < n − 1 → (∀x ∈ a[0..actual]) x ≤ aactual+1 ) // // // // // m = maxPos(a,0,actual); swap(a[m],a[actual]); // estado E1 Recordar especificación de MaxPos: PMP : 0 ≤ desde ≤ hasta ≤ |a| − 1, se cumple porque 0 < atual ≤ n − 1 QMP : desde ≤ pos ≤ hasta ∧ (∀x ∈ a[desde..hasta]) x ≤ apos // // // // // estado E1 vale 0 ≤ m ≤ actual ∧ (∀x ∈ a[0..actual]) x ≤ am implica 0 < actual ≤ n − 1 ∧ mismos(a, pre(a)) implica (∀k ∈ (actual..n − 1)) ak ≤ ak+1 implica actual < n − 1 → (∀x ∈ a[0..actual]) x ≤ aactual+1 vale 0 ≤ m ≤ actual ∧ (∀x ∈ a[0..actual]) x ≤ am vale a == a@E ∧ actual == actual@E implica 0 < actual ≤ n − 1 ∧ mismos(a, pre(a)) implica (∀k ∈ (actual..n − 1)) ak ≤ ak+1 implica (actual < n − 1 → (∀x ∈ a[0..actual]) x ≤ aactual+1 ) Justificación de los implica: actual y a no cambiaron Lo dice el segundo vale de este estado 11 // // // // // // // estado E2 vale am == (a@E1 )actual ∧ aactual == (a@E1 )m (por poscon. de swap) vale (∀i ∈ [0..n), i 6= m, i 6= actual) ai == (a@E1 )i (idem) vale actual == actual@E1 ∧ m == m@E1 implica 0 < actual ≤ n − 1 (actual no se modificó) implica mismos(a, pre(a)) (el swap no agrega ni quita elementos) implica (∀k ∈ (actual..n − 1)) ak ≤ ak+1 (a(actual..n − 1] no se modificó porque m ≤ actual) // implica (∀k ∈ (actual − 1..n − 1)) ak ≤ ak+1 del tercer vale de E2 : m == m@E1 y actual == actual@E1 ; del primer vale de y último implica de E1 : (a@E1 )m ≤ (a@E1 )actual+1 ; del primer y segundo vale de E2 : aactual ≤ aactual+1 ) // implica (∀x ∈ a[0..actual]) x ≤ aactual (del primer vale de E1 y E2 ) 12 1. El cuerpo del ciclo preserva el invariante (cont.) // // // // // 2 y 3 son triviales estado E2 implica 0 < actual ≤ n − 1 implica mismos(a, pre(a)) implica (∀k ∈ (actual − 1..n − 1)) ak ≤ ak+1 implica (∀x ∈ a[0..actual]) x ≤ aactual 2. La función variante decrece: // estado E (invariante + guarda del ciclo) // vale I ∧ B actual--; m = maxPos(a,0,actual); swap(a[m],a[actual]); actual--; // // // // // // // estado E3 vale actual == actual@E2 − 1 ∧ a == a@E2 implica 0 ≤ actual@E2 − 1 < n − 1 (de primer vale de E3 ) implica 0 ≤ actual ≤ n − 1 (reemplazando actual@E2 − 1 por actual) implica mismos(a, pre(a)) (de segundo implica de E2 y a == a@E2 ) implica (∀k ∈ (actual@E2 − 1..n − 1)) ak ≤ ak+1 (por E2 ) implica (∀k ∈ (actual..n − 1)) ak ≤ ak+1 (reemplazo actual@E2 − 1 por actual) // implica (∀x ∈ a[0..actual + 1]) x ≤ aactual+1 (por E2 + reemplazo) // implica (∀x ∈ a[0..actual]) x ≤ aactual+1 (por ser un selector más acotado) // implica actual < n − 1 → (∀x ∈ a[0..actual]) x ≤ aactual+1 (pues q implica p → q) // estado F // vale actual == actual@E − 1 // implica v @F == v @E − 1 < v @E 3. Si la función variante pasa la cota, el ciclo termina: actual ≤ 0 es ¬B 14 13 4. La precondición del ciclo implica el invariante 5. La poscondición vale al final Quiero probar que: (¬B ∧ I ) ⇒ QC Recordemos Recordar que I I I P : 1 ≤ n == |a| PC : a == pre(a) ∧ actual == n − 1 I : 0 ≤ actual ≤ n − 1 ∧ mismos(a, pre(a)) ∧(∀k ∈ (actual..n − 1)) ak ≤ ak+1 ∧actual < n − 1 → (∀x ∈ a[0..actual]) x ≤ aactual+1 ¬B ∧ I mismos(a, pre(a)) ∧ (∀k ∈ (actual..n − 1)) ak ≤ ak+1 ∧ actual < n − 1 → (∀x ∈ a[0..actual + 1]) x ≤ aactual+1 ∧ actual ≤ 0 Demostración de que PC ⇒ I : I 1 ≤ n ∧ actual == n − 1 ⇒ I a == pre(a) I I ⇒ : 0 ≤ actual ≤ n − 1 ∧ QC : mismos(a, pre(a)) ∧ (∀j ∈ [0..n − 1)) aj ≤ aj+1 0 ≤ actual ≤ n − 1 Veamos que vale cada parte de QC : I mismos(a, pre(a)) I actual == n − 1 ⇒ (∀k ∈ (actual..n − 1)) ak ≤ ak+1 porque el selector actúa sobre una lista vacı́a mismos(a, pre(a)): trivial porque está en I (∀j ∈ [0..n − 1)) aj ≤ aj+1 : I I I actual == n − 1 ⇒ actual < n − 1 → (∀x ∈ a[0..actual]) x ≤ aactual+1 porque el antecedente es falso primero observar que actual == 0 si n == 1, no hay nada que probar porque [0..n − 1) == [] si n > 1 I I I 15 sabemos (∀k ∈ (0..n − 1)) ak ≤ ak+1 sabemos que (∀x ∈ a[0..1]) x ≤ a1 , entonces a0 ≤ a1 concluimos (∀j ∈ [0..n − 1)) aj ≤ aj+1 16 Implementación de maxPos Correctitud de maxPos problema maxPos (a : [Int], desde, hasta : Int) = pos : Int{ requiere PMP : 0 ≤ desde ≤ hasta ≤ |a| − 1; asegura QMP : desde ≤ pos ≤ hasta ∧ (∀x ∈ a[desde..hasta]) x ≤ apos ; } problema maxPos (a : [Int], desde, hasta : Int) = pos : Int{ requiere PMP : 0 ≤ desde ≤ hasta ≤ |a| − 1; asegura QMP : desde ≤ pos ≤ hasta ∧ (∀x ∈ a[desde..hasta]) x ≤ apos ; } int maxPos(const int a[], int desde, int hasta) { //vale PMP : 0 ≤ desde ≤ hasta ≤ |a| − 1 (precondición del problema) int mp = desde, i = desde; //vale PC : mp == i == desde (precondición del ciclo) while (i < hasta) { i++; if (a[i] > a[mp]) mp = i; } //vale QC : desde ≤ mp ≤ hasta ∧ (∀x ∈ a[desde..hasta]) x ≤ amp (poscondición del ciclo) return mp; int maxPos(const int a[], int desde, int hasta) { int mp = desde, i = desde; while (i < hasta) { i++; if (a[i] > a[mp]) mp = i; } return mp; } 17 Especificación del ciclo //vale QMP : desde ≤ pos ≤ hasta ∧ (∀x ∈ a[desde..hasta]) x ≤ apos (poscondición del problema) } 18 Correctitud del ciclo // vale PC // implica I // vale PC : mp == i == desde while (i < hasta) { // estado E // vale I ∧ i < hasta while (i < hasta) { // invariante I : desde ≤ mp ≤ i ≤ hasta ∧ (∀x ∈ a[desde..i]) x ≤ amp // variante v : hasta − i i++; if (a[i] > a[mp]) mp = i; // vale I // vale v < v @E i++; if (a[i] > a[mp]) mp = i; } } // vale I ∧ i ≥ hasta // implica QC // vale QC : desde ≤ mp ≤ hasta ∧ (∀x ∈ a[desde..hasta]) x ≤ amp 1. El cuerpo del ciclo preserva el invariante 2. La función variante decrece 3. Si la función variante pasa la cota, el ciclo termina: v ≤ 0 ⇒ i ≥ hasta 4. La precondición del ciclo implica el invariante 5. La poscondición vale al final El Teorema del Invariante nos garantiza que si valen 1, 2, 3, 4 y 5, el ciclo termina y es correcto con respecto a su especificación. 19 20 1. El cuerpo del ciclo preserva el invariante Especificación y correctitud del If Recordar que desde, hasta y a no cambian porque son variables de entrada que no aparecen en local ni modifica Pif : desde ≤ mp ≤ i ≤ hasta ∧ (∀x ∈ a[desde..i)) x ≤ amp Qif : desde ≤ mp ≤ i ≤ hasta ∧ (∀x ∈ a[desde..i]) x ≤ amp // estado E (invariante + guarda del ciclo) // vale desde ≤ mp ≤ i < hasta ∧ (∀x ∈ a[desde..i]) x ≤ amp i++; // estado E1 // vale i == i@E + 1 ∧ mp = mp@E // implica Pif : desde ≤ mp ≤ i ≤ hasta ∧ (∀x ∈ a[desde..i)) x ≤ amp de E , reemplazando i@E por i − 1 y cambiando el lı́mite del selector adecuadamente // estado Eif // vale desde ≤ mp ≤ i ≤ hasta ∧ (∀x ∈ a[desde..i)) x ≤ amp if (a[i] > a[mp]) mp = i; // vale (ai > amp@Eif ∧ mp == i@Eif ∧ i == i@Eif ) ∨(ai ≤ amp@Eif ∧ mp == mp@Eif ∧ i == i@Eif ) // implica desde ≤ mp ≤ i ≤ hasta operaciones lógicas; observar que desde, hasta y a no pueden modificarse porque son variables de entrada que no aparecen ni en modifica ni en local; observar que i == i@Eif if (a[i] > a[mp]) mp = i; // vale Qif : desde ≤ mp ≤ i ≤ hasta ∧ (∀x ∈ a[desde..i]) x ≤ amp observar que en este punto, tratamos al if como una sola gran instrucción que convierte Pif en Qif . La justificación de este paso es la transformación de estados de la hoja siguiente // implica I observar que I es igual que Qif , pero en general alcanzarı́a con que Qif implique I // implica amp@Eif ≤ amp ∧ ai ≤ amp // implica (∀x ∈ a[desde..i]) x ≤ amp // implica Qif 22 21 2. La función variante decrece 3, 4 y 5 son triviales // estado E (invariante + guarda del ciclo) // vale desde ≤ i < hasta 3. Si la función variante pasa la cota, el ciclo termina: i++; hasta − i ≤ 0 // estado E1 // vale i == i@E + 1 // implica desde ≤ i ≤ hasta ⇒ hasta ≤ i 4. La precondición del ciclo implica el invariante: Recordar que la precondición de maxPos dice if (a[i] > a[mp]) mp = i; PMP : 0 ≤ desde ≤ hasta ≤ |a| − 1 // estado F // vale i == i@E1 // implica i == i@E + 1 y que desde y hasta no cambian de valor. Entonces PC : i == desde ∧ mp == desde ⇓ I : desde ≤ mp ≤ i ≤ hasta ∧ (∀x ∈ a[desde..i]) x ≤ amp ¿Cuánto vale v = hasta − i en el estado F ? v @F (Justificar...) == (hasta − i)@F == hasta − i@F == hasta − (i@E + 1) 5. La poscondición vale al final: es fácil ver que I ∧ i ≥ hasta implica QC == hasta − i@E − 1 < hasta − i@E == v @E Entonces el ciclo es correcto con respecto a su especificación. 23 24 Complejidad de maxPos Complejidad de Upsort I el ciclo de Upsort itera n veces ¿Cuántas comparaciones hacemos como máximo? I El ciclo itera hasta − desde veces Cada iteración hace: I I una comparación (en la guarda) I una asignación (el incremento) I otra comparación (guarda del condicional) I otra asignación (si se cumple esa guarda) I I una iteración hace I I I I I Antes del ciclo se hacen dos asignaciones. I I MaxPos es invocada la primera vez con desde == 0 y hasta == |a| − 1, luego con segmentos sucesitamente más cortos hasta llegar a tener longitud 1, I I I I una búsqueda de maxPos un swap un decremento de estos pasos, el único que no es O(1), constante, es el primero ¿cuántos pasos hace maxPos en cada iteración? Luego, la cantidad de operaciones de MaxPos es O(longitud del segmento) = O(hasta − desde + 1) . I empieza con actual == n − 1 termina con actual == 0 en cada iteración decrementa actual en uno siempre O(hasta − desde + 1) = O(actual + 1) en la primera hace n (busca el máximo del segmento a ordenar) en la segunda hace n − 1 (busca el segundo mayor) en la tercera hace n − 2 y ası́ (podrı́amos verlo por inducción) hasta 2 el total de pasos es O(n + (n − 1) + . . . + 2) = O(n ∗ (n + 1)/2 − 1) = O(n2 ) los mejores algoritmos de ordenamiento son O(n log n) 26 25 Cota inferior de complejidad tiempo para sorting 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 la operaciones de comparar elementos y permutarlos. Teorema: Sea cualquier método de ordenamiento (inventado o por inventarse). Tomará al menos log2 n! pasos para ordenar n elementos. 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 Concluı́mos que la ejecución debe ser una sucesión de pasos distinta para cada permutación posible de la secuencia de entrada. 27 28 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 el 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 bebe 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. 29 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 de elementos. 31 30