Hay una gran variedad de enfoques factibles. Cuál es el más adecuado depende de
- lo que intentas mostrar,
- cuántos detalles quieres o necesitas.
Si el algoritmo es ampliamente conocido y lo utiliza como subrutina, a menudo permanece en un nivel superior. Si el algoritmo es el objeto principal bajo investigación, probablemente quiera ser más detallado. Lo mismo puede decirse de los análisis: si necesita un límite de tiempo de ejecución superior aproximado, procederá de manera diferente a cuando desea un recuento preciso de declaraciones.
Le daré tres ejemplos para el conocido algoritmo Mergesort que, con suerte, ilustrarán esto.
Nivel alto
El algoritmo Mergesort toma una lista, la divide en dos (aproximadamente) partes igualmente largas, se repite en esas listas parciales y combina los resultados (ordenados) para que se ordene el resultado final. En listas simples o vacías, devuelve la entrada.
Este algoritmo es obviamente un algoritmo de clasificación correcto. Dividir la lista y fusionarla se puede implementar en el tiempo , lo que nos da una recurrencia para el peor tiempo de ejecución T ( n ) = 2 T ( nΘ ( n ). Según el teorema del Maestro, esto se evalúa comoT(n)∈Θ(nlogn).T( n ) = 2 T( n2) +Θ(n)T( n ) ∈ Θ ( n logn )
Nivel medio
El algoritmo Mergesort viene dado por el siguiente pseudocódigo:
procedure mergesort(l : List) {
if ( l.length < 2 ) {
return l
}
left = mergesort(l.take(l.length / 2)
right = mergesort(l.drop(l.length / 2)
result = []
while ( left.length > 0 || right.length > 0 ) {
if ( right.length == 0 || (left.length > 0 && left.head <= right.head) ) {
result = left.head :: result
left = left.tail
}
else {
result = right.head :: result
right = right.tail
}
}
return result.reverse
}
Probamos la corrección por inducción. Para listas de longitud cero o uno, el algoritmo es trivialmente correcto. Como hipótesis de inducción, suponga que mergesort
funciona correctamente en listas de longitud como máximo para algunos arbitrarios, pero fijos naturales n > 1 . Ahora dejemos que L sea una lista de longitud n + 1 . Por hipótesis de inducción, y mantener (sin disminución) versiones ordenadas de la primera resp. segunda mitad de L después de las llamadas recursivas. Por lo tanto, el ciclo selecciona en cada iteración el elemento más pequeño que aún no se ha investigado y lo agrega ; por lo tanto, es una lista no cada vez más ordenada que contiene todos los elementos denorten > 1Ln + 1left
right
Lwhile
result
result
left
y right
. El reverso es una versión ordenada no decreciente de , que es el resultado devuelto y deseado.L
En cuanto al tiempo de ejecución, cuentemos las comparaciones de elementos y las operaciones de lista (que dominan el tiempo de ejecución asintóticamente). Las listas de longitud inferior a dos no causan ninguna. Para las listas de longitud , tenemos esas operaciones causadas por la preparación de las entradas para las llamadas recursivas, las de las llamadas recursivas más el bucle y uno . Ambos parámetros recursivos se pueden calcular con a lo sumo n operaciones de lista cada uno. El bucle se ejecuta exactamente n veces y cada iteración causa como máximo una comparación de elementos y exactamente dos operaciones de lista. La final se puede implementar para usar 2 nn > 1while
reverse
nortewhile
nortereverse
2 noperaciones de lista: cada elemento se elimina de la entrada y se coloca en la lista de salida. Por lo tanto, el recuento de operaciones cumple la siguiente recurrencia:
T(0)=T(1)T(n)=0≤T(⌈n2⌉)+T(⌊n2⌋)+7n
Como es claramente no decreciente, es suficiente considerar n = 2 k para el crecimiento asintótico. En este caso , la recurrencia se simplifica aTn=2k
T(0)=T(1)T(n)=0≤2T(n2)+7n
Según el teorema del Maestro, obtenemos que se extiende al tiempo de ejecución de .T∈Θ(nlogn)mergesort
Nivel ultra bajo
Considere esta implementación (generalizada) de Mergesort en Isabelle / HOL :
types dataset = "nat * string"
fun leq :: "dataset \<Rightarrow> dataset \<Rightarrow> bool" where
"leq (kx::nat, dx) (ky, dy) = (kx \<le> ky)"
fun merge :: "dataset list \<Rightarrow> dataset list \<Rightarrow> dataset list" where
"merge [] b = b" |
"merge a [] = a" |
"merge (a # as) (b # bs) = (if leq a b then a # merge as (b # bs) else b # merge (a # as) bs)"
function (sequential) msort :: "dataset list \<Rightarrow> dataset list" where
"msort [] = []" |
"msort [x] = [x]" |
"msort l = (let mid = length l div 2 in merge (msort (take mid l)) (msort (drop mid l)))"
by pat_completeness auto
termination
apply (relation "measure length")
by simp+
Esto ya incluye pruebas de buena definición y terminación. Encuentre una prueba de corrección (casi) completa aquí .
Para el "tiempo de ejecución", es decir, el número de comparaciones, se puede configurar una recurrencia similar a la de la sección anterior. En lugar de usar el teorema del Maestro y olvidar las constantes, también puede analizarlo para obtener una aproximación que sea asintóticamente igual a la cantidad verdadera. Puede encontrar el análisis completo en [1]; Aquí hay un bosquejo (no necesariamente se ajusta al código Isabelle / HOL):
Como arriba, la recurrencia para el número de comparaciones es
f0=f1fn=0=f⌈n2⌉+f⌊n2⌋+en
donde es el número de comparaciones necesarias para fusionar los resultados parciales². Para deshacernos de los pisos y techos, realizamos una distinción entre mayúsculas y minúsculas sobre si n es par:enn
{f2mf2m+1=2fm+e2m=fm+fm+1+e2m+1
Usando diferencias anidadas hacia adelante / hacia atrás de y e n obtenemos quefnen
.∑k=1n−1(n−k)⋅Δ∇fk=fn−nf1
La suma coincide con el lado derecho de la fórmula de Perron . Definimos las series generadoras de Dirichlet de comoΔ∇fk
W(s)=∑k≥1Δ∇fkk−s=11−2−s⋅∑k≥1Δ∇ekks=: ⊟(s)
que junto con la fórmula de Perron nos lleva a
fn=nf1+n2πi∫3−i∞3+i∞⊟(s)ns(1−2−s)s(s+1)ds
⊟(s)
fn∼n⋅log2(n)+n⋅A(log2(n))+1
A[−1,−0.9]
- Transformaciones de Mellin y asintóticas: la recurrencia de mergesort por Flajolet y Golin (1992)
- en=⌊n2⌋
en=n−1
en=n−⌊n2⌋⌈n2⌉+1−⌈n2⌉⌊n2⌋+1