Nivel 8 · Quicksort verificado (experto)
El algoritmo y su terminación
Quicksort ( (Hoare, 1961) ; (Hoare, 1962) ) es un algoritmo de divide y vencerás. Dada una lista no vacía, elige un elemento como pivote (se toma la cabeza) y reparte los elementos restantes en los que son el pivote y los que son el pivote. Cada parte se ordena recursivamente y las partes ordenadas se concatenan con el pivote en medio. La lista vacía ya está ordenada.
Sobre un tipo linealmente ordenado , se define por
donde, a partir de la cola , las dos mitades de la partición son
Aquí es el pivote, es «cons» (anteponer) y es la concatenación. Nótese que recoge exactamente los elementos que fallan la prueba , es decir, los que cumplen .
El hecho estructural clave, el que impulsará la prueba de ordenamiento, es que todo lo que queda a la izquierda del pivote es y todo lo que queda a su derecha es . Desplegar la recursión sobre una entrada concreta produce un árbol binario: cada nodo lleva su pivote, el hijo izquierdo trata los elementos el pivote y el derecho los elementos el pivote. Leer las hojas y los pivotes en orden (subárbol izquierdo, pivote, subárbol derecho) produce la salida ordenada. Para el pivote raíz es ; la mitad izquierda recurre con pivote , la derecha con pivote , y el recorrido en orden da .
Explóralo tú mismo
El widget despliega el árbol de recursión de cualquier entrada y comprueba en cada nodo los tres invariantes del desarrollo: la partición separa (), cada llamada recursiva es estrictamente más corta, y el recorrido en orden está ordenado.
Quicksort verificado, paso a paso
Los tres ingredientes de la corrección (terminación, permutación, cotas de separación) comprobados en cada nodo del árbol de recursión.
Lean: la definición y sus obligaciones
El texto de Lean siguiente, de
Thesis/Sort/Quicksort.lean,
es la definición literal. La prueba booleana decide (x ≤ p) convierte la proposición decidible
en un Bool para que pueda dirigir List.filter; su negación ! decide (x ≤ p)
selecciona la mitad complementaria.
def quicksort : List α → List α | [] => [] | p :: rest => quicksort (rest.filter (fun x => decide (x ≤ p))) ++ p :: quicksort (rest.filter (fun x => ! decide (x ≤ p))) termination_by l => l.length decreasing_by all_goals simp_wf all_goals exact le_trans (length_filter_le _ _) (le_of_eq (by simp))Las dos líneas finales no son decoración. Quicksort no recurre sobre un subtérmino estructural: las mitades se computan por filtrado, no se obtienen por ajuste de patrones. Como se vio en el módulo sobre el CIC, una definición que no sigue la estructura de un tipo inductivo no es estructuralmente recursiva y exige una justificación de terminación frente al recursor bien fundamentado; antes de poder siquiera hablar de como función total, hay que probar que la recursión termina. Aquí la medida es la longitud del argumento.
Para todo predicado y lista , .
A probar: , para todo predicado y toda lista . Por inducción sobre .
- . , luego y .
- . Por definición del filtrado se distinguen dos casos según :
- Si se cumple: , luego
- Si falla: , luego
Cada llamada recursiva de es sobre una lista estrictamente más corta, de modo que es una función total.
A probar: cada argumento recursivo de es una lista estrictamente más corta que .
- Los dos argumentos recursivos son y .
- Por el Lema 12.5, y .
- Como , se sigue
Ambos argumentos son, pues, estrictamente menores en la medida . Esto justifica la recursión bien fundamentada y hace de una función total.
Lean: descargar las obligaciones de terminación
Tras termination_by l => l.length, Lean pide probar que cada argumento recursivo es menor en el
orden bien fundamentado. La táctica simp_wf reduce cada meta a
, cerrada por length_filter_le (el
Lema 12.5); el le_of_eq (by simp) final absorbe una
reescritura contable de la longitud (el attach interno, usado para recordar que los elementos
filtrados provienen de , satisface |r.attach| = |r|).
decreasing_by all_goals simp_wf all_goals exact le_trans (length_filter_le _ _) (le_of_eq (by simp))Como la definición usa recursión bien fundamentada (no estructural), la ecuación
y el desplegado para no valen por mera computación del núcleo;
Lean los provee como lemas de reescritura, que se nombran para usarlos en las pruebas de las dos
páginas siguientes. También de
Thesis/Sort/Quicksort.lean:
@[simp] theorem quicksort_nil : quicksort ([] : List α) = [] := by rw [quicksort]
theorem quicksort_cons (p : α) (rest : List α) : quicksort (p :: rest) = quicksort (rest.filter (fun x => decide (x ≤ p))) ++ p :: quicksort (rest.filter (fun x => ! decide (x ≤ p))) := by rw [quicksort]Con la función total y sus ecuaciones a la mano, quedan las dos mitades de la Definición 12.1: la permutación y el ordenamiento.