Ejercicios 2
Ejercicios 2
Ejercicios 2
Algoritmos
16 de octubre de 2010
Caso Base Para el caso base T (n) = c1 (1)2 − c2 (1) = Θ(1), ∀n < n0 , donde
n0 , es una constante adecuada. Para todo 1 ≤ k < n0 , tenemos “c=Θ(1)” ≤
cn2 , Si escogemos nuestra c lo suficientemente grande, esto es fácil al verlo por
inducción completa, en el que tanto el paso base: c ≤ cn2 como el paso inductivo
c ≤ cn2 − cn − c son válidos en cuanto tomemos a c como una constante lo
suficientemente grande .
1
n n
T (n) = 4T (bn/2c) + cn ≤ 4c1 ( )2 − c2 ( )n + cn
2 2
2 c2 n
= c1 n − ( − cn)
2
≤ cn2
Para que lo último se cumpla se necesita que: c22n − cn ≥ 0, lo cual implica que
c2 ≥ 2c, lo cual es completamente razonable, por ejemplo si tomamos c = 1
tendríamos c2 = 2, y se cumpliría la condición; es decir que si esta condición se
cumple T (n) = O(n2 ) que era lo que queríamos demostrar.
Caso Base El caso base es parecido al anterior, lo que cambia es que ahora
nuestro c tiene que ser lo suficientemente pequeño para que las desigualdad
c ≥ c − n2 − cn − c sea válida.
Paso inductivo Para el paso inductivo se sigue el mismo esquema que el segui-
do para mostrar la cota superior asintótica, cambiando solo la desigualdad de
las constantes, ya que si se desarrolla la desigualdad se obtiene que T (n) ≤
c1 n2 ⇐⇒ c2 ≤ 2c.
Lo que hemos acabado de mostrar se resume en que T (n) = O(n2 ) y T (n) =
Ω(n2 ) y por lo tanto T (n) = Θ(n2 )
4.2-5 Una primera mirada a la recursión T (n) = T (αn) + T ((1 − α)n) + cn nos
indica que no es posible aplicar el teorema maestro y que el uso del método de
sustitución no es muy útil en esta ocasión para las primeras aproximaciones a la
fórmula de la recursión, así que optamos por dibujar el árbol de recursión (Figura
12) que en primera instancia nos da pistas sobre el orden de cada nivel. Veamos
por ejemplo la suma de los nodos del segundo nivel:
2
Hechos
a) Si α > 1
2
⇒1−α< 1
2
⇒α>1−α⇒ 1
1−α
< α1
b) Si α < 1
2
1−α> 1
2
⇒⇒1−α>α⇒ 1
α
1
< 1−α
c) Podemos estudiar la recursión teniendo en cuenta sólo uno de los dos casos
anteriores, pues todo lo que se aplique sobre uno se aplica sobre el otro
reemplazando α por 1 − α.
d) La altura del árbol de recursión puede encontrarse a partir de 4 situaciones
distintas, como se puede observar en el árbol de recursión, en las que se
llega al caso base:
1) αi n = 1 ⇒ i = log 1 n
α
Si definimos α = k1 , podemos hacer αi n = n
ki
= 1 ⇒ n = ki ⇒ i =
logk n = log 1 n, pues k = α1
α
2) (1 − α) ⇒ i = log 1 n
i
1−α
Se sigue el mismo procedimiento que en el punto anterior para llegar a
esta conclusión.
3) αi (1 − α)j = 1 , con i ≤ j
Siguiendo el razonamiento del primer punto podemos decir que :
j = log 1 αi n
1−α
= i log 1 α + log 1 n
1−α 1−α
< log 1 n
1−α
Es decir que i ≤ j < log 1 n y por lo tanto en este caso la altura del
1−α
árbol es menor a log 1 n
1−α
ln c > ln d
3
log 1 n (en el caso en que α > 12 ). Puesto que:
α
1 1
< ⇒ log 1 n < log 1 n
1−α α 1−α α
Una vez tenemos la altura máxima del árbol podemos hacer nuestra primera
aproximación del orden de T (n):
Caso Base Podemos suponer que T (n) tiene un valor constante1 para toda n lo
suficientemente pequeña y confiar en que existe un caso base, para todo valor de
α, que verifique la hipótesis del límite asintótico. En otras palabras confiamos en
que T (n) será lo suficientemente pequeño cuando n sea pequeño.
Paso inductivo Demostraremos que si T (n) = O(n log n) se cumple para cualquier
n menor o igual a αn , entonces se cumple para n:
4
−c
d≤ α
αlg( 1−α ) + lg(1 − α)
Esto conserva la propiedad de d > 0 pues log α < 0 y log 1 − α < 0.
Caso Base Se sigue el mismo razonamiento que para el caso base de la prueba
anterior.
Paso inductivo Sigue el mismo esquema anterior cambiando los ≤ por ≥, para
llegar a la condición: d2 ≥ αlg( α −c
)+lg(1−α)
. Si esta condición se cumple y d2 > 0 ⇒
1−α
T (n) = Ω(nlgn).
Luego como T (n) = Ω(nlgn) y T (n) = O(nlgn) ⇒ T (n) = Θ(nlgn).
Entonces :
T (n) = Θ(n2,81... ) (1)
Luego analizamos la recursión T 0 (n) = aT 0 ( n4 ) + n2 con a = a ≥ 1, b = 4 > 1 ,
f(n) = n2 y d. Tenemos varias opciones aquí:
ln a ln 7
2< <
ln 4 ln 2
ln 4 ln 7
2 ln 4 < ln a <
ln 2
5
16 < a < 49
Es decir que con 16 < a < 49 garantizamos que T 0 (n) es asintóticamente
más rápido que T (n).
b) Utilizar el segundo caso del método maestro:
Necesitamos aquí que f(n) = n2 = Θ(nlog4 a ) para concluir que T 0 (n) =
Θ(nlog4 a lg n). Y la única forma de que esto el límite asintótico de f(n) se
cumpla es que a = 16. Sin embargo estamos interesados en el máximo valor
para a, que hasta ahora es 49, así que el segundo maestro no nos da infor-
mación que no tengamos sobre el problema.
c) Utilizar el tercer caso del método maestro:
En este caso, en contraste con el primer caso, para que f(n) = Ω(nlogb a+ )
necesitamos que log4 a ≤ 2 − ⇒ log4 a < 2. A esto se llega una vez más
por contradicción siguiendo el mismo esquema del caso 1: Si log4 a > 2 y
f(n) = Ω(nlogb a+ ), entonces 1c ≥ n lo que evidentemente es una contradic-
ción pues una vez más impone un máximo con el orden usual a los números
naturales.
Si log4 a < 2 ⇒ a < 16, y nuevamente terminamos con información que
no es de utilidad para la solución del problema pues buscamos el máximo
valor de a, que para el primer caso es de 49.
De acuerdo con lo anterior tenemos que el valor de a debe estar acotado superi-
ormente por 49 para lograr que T 0 (n) sea asintóticamente más rápido que T (n).
6
1
≥ lg n , pues c < 1 ⇒ 1 − c > 0
1−c
1
Si definimos c = m
< 1 , con m > 1 podemos escribir lo anterior como:
1
≥ lg n
1 − m1
1
m−1
≥ lg n
m
m
≥ lg n
m−1
m
2 m−1 ≥ 2lg n
m
2 m−1 ≥ n
Sin embargo esta desigualdad lleva a la contradicción de establecer un máximo
en el orden usual de los números naturales.
Por otra parte, aunque f(n) = Ω(n2 ), f(n) no es polinomialmente más grande que
n2 , pues el f(n)
n2
= lg n que es asintóticamente menor que n para cualquier
constante positivo. Es decir que el método maestro queda descartado para esta
recursión.
Por el método de substitución podemos ver:
n
T (n) = 4T ( ) + n2 lg n
2
n
n n
T (n) = 4(4T ( 2 ) + ( )2 lg ) + n2 lg n
2 2 2
2
n n
T (n) = 4(4T ( ) + (lg n − lg 2)) + n2 lg n
4 4
n n2
T (n) = 16T ( ) + 4 (lg n − 1) + n2 lg n
4 4
n
T (n) = 16T ( ) + n2 lg n − n2 + n2 lg n
4
n
n n
T (n) = 16(4T ( 4 ) + ( )2 lg ) + n2 lg n − n2 + n2 lg n
2 4 4
n n2
T (n) = 16(4T ( ) + 2 (lg n − lg 4)) + n2 lg n − n2 + n2 lg n
8 4
n n2
T (n) = 64T ( ) + 16 2 (lg n − 2) + n2 lg n − n2 + n2 lg n
8 4
n
T (n) = 64T ( ) + n2 lg n − 2n2 + n2 lg n − n2 + n2 lg n
8
7
Con estas fórmulas en mente podemos enunciar nuestra primera aproximación
a una fórmula para esta recursión:
X
lg n−1
X
lg n−1
X
lg n−1
2
≈ n lg n − in2 + Θ(n2 )
i=0 i=0
X
lg n−1
2 2
≈ (lg n − 1)n lg n − n i + Θ(n2 )
i=0
(lg n − 1) lg n
≈ n2 lg n lg n − n2 lg n − n2 ( ) + Θ(n2 )
2
lg2 n − lg n
≈ n2 lg2 n − n2 lg n − n2 ( ) + Θ(n2 )
2
2n2 lg n + n2 lg2 n − n2 lg n
≈ n2 lg2 n − + Θ(n2 )
2
n2 lg n + n2 lg2 n
≈ n2 lg2 n − + Θ(n2 )
2
≤ n2 lg2 n
Hemos llegado entonces a que T (n) = O(n2 lg2 n). Ahora probemos por induc-
ción matemática:
Caso Base Para el caso base suponemos que T (1) = c1 y escogemos n = 2 como
nuestro caso base. Si T (n) = O(n2 lg2 n) ⇒ T (2) = O(4) ⇒ T (2) ≤ 4c. Veamos
que sucede al evaluar T (2)
n
T (n) = 4T ( ) + n2 lg n
2
2
T (2) = 4T ( ) + (2)2 lg 2
2
T (2) = 4T (1) + 4
T (2) = 4c1 + 4
Mientras c1 ≤ c − 1, se cumple que T (2) ≤ 4c y tenemos nuestro caso base.
8
n
T (n) = 4T ( ) + n2 lg n
2
n 2 2n
≤ 4c( ) lg + n2 lg n
2 2
≤ cn2 (lg n − lg 2)2 + n2 lg n
≤ cn2 (lg2 n − 2 lg n + 1) + n2 lg n
≤ cn2 lg2 n − 2cn2 lg n + cn2 + n2 lg n
≤ cn2 lg2 n
3. Problema 4- 1.
4.1
a) T (n) = 2T ( n2 ) + n3
Usamos el teorema maestro con a = 2, b = 2 y f(n) = n3 :
Como nlogb a = nlog2 2 = n, tenemos que n3 = Ω(n1+ ) , con un > 0 muy
pequeño. Podemos entonces aplicar el tercer caso del método maestro de-
mostrando que:
n
af( ) ≤ cf(n)
b
n
2f( ) ≤ cf(n)
2
n3
2 3 ≤ cn3
2
n3 ≤ 4cn3
1
Si hacemos c = 4
tenemos
n3 ≤ n3
Que se cumple siempre, por lo tanto el caso 3 del método maestro es válido:
T (n) = Θ(n3 )
9
b) T (n) = T ( 9n
10
+ n)
10
Usamos el teorema maestro con a = 1, b = 9
y f(n) = n:
log 10 1
Como nlogb a = n 9 = n0 = 1, tenemos que n = Ω(n0+ ) , con un
0 < < 1. Podemos entonces aplicar el tercer caso del método maestro
demostrando que:
n
af( ) ≤ cf(n)
b
9n
f( ) ≤ cf(n)
10
9n
≤ cn
10
9
≤c
10
Por lo tanto si hacemos 0 ≥ c ≤ 109 se cumple la condición anterior y pode-
mos aplicar el caso 3 del método maestro:
T (n) = Θ(n)
c) T (n) = 16T ( n4 ) + n2
Usamos el teorema maestro con a = 16, b = 4 y f(n) = n2 :
Como nlogb a = nlog4 16 = n2 , tenemos que n2 = Θ(n2 ) podemos entonces
aplicar el segundo caso del método maestro:
T (n) = Θ(n2 lg n)
d) T (n) = 7T ( n3 ) + n2
Usamos el teorema maestro con a = 7, b = 3 y f(n) = n2 :
Como nlogb a = nlog3 7 = n1,771... , tenemos que n2 = Ω(n1,771...+ ) , con un
= 2 − 1,771.... Podemos entonces aplicar el tercer caso del método mae-
stro demostrando que:
n
af( ) ≤ cf(n)
b
n
7f( ) ≤ cf(n)
3
n2
7 2 ≤ cn2
3
7
≤c
9
Y dado que 97 < 1, tenemos que si 0 ≤ c ≤ 97 < 1 se cumple la condición
anterior y por lo tanto el caso 3 del método maestro es válido:
T (n) = Θ(n2 )
10
e) T (n) = 7T ( n2 ) + n2
Usamos el teorema maestro con a = 7, b = 2 y f(n) = n2 :
Como nlogb a = nlog2 7 = n2,8073... , tenemos que n2 = O(n2,8073...− ) , con un
= 2,8073... − 2, pues evidentemente n2 = O(n2 ) Por lo tanto el primer
caso del método maestro es válido:
g) T (n) = T (n − 1) + n
Utilizando el método de substitución tenemos que:
T (n) = T (n − 1) + n
= T (n − 2) + n − 1 + n
= T (n − 3) + n − 2 + n − 1 + n = T (n − 3) + 3n − 3 ≈ n2 − n = Θ(n2 )
Caso Base El caso base T (1) ≤ c se tiene gracias a que T (n) es constante
para n ≤ 2.
T (n) = T (n − 1) + n
≤ c(n − 1)2 + n
= cn2 − 2cn + 1 + n
= cn2 + n(1 − 2c) + 1
≤ cn2
Para que esto se cumpla necesitamos que cn2 + n(1 − 2c) + 1 ≤ cn2 :
n(1 − 2c) + 1 ≤ 0
11
n(1 − 2c) ≤ −1
n(2c − 1) ≥ 1
1
2c − 1 ≥
n
1
2c ≥ +1
n
1 1
c≥ +
2n 2
Esto último se cumple siempre que c > 1 y n0 = 1puesto que si n ≥ n0 se
tiene:
n≥1
1
1≥
n
1 1
≥
2 2n
1 1
1≥ +
2n 2
Hemos demostrado entonces que T (n) = O(n2 ).
Caso Base El caso base T (1) ≥ c se tiene gracias a que T (n) es constante
para n ≤ 2.
T (n) = T (n − 1) + n
≥ c(n − 1)2 + n
= cn2 − 2cn + 1 + n
= cn2 + n(1 − 2c) + 1
≥ cn2
Para que esto se cumpla necesitamos que cn2 + n(1 − 2c) + 1 ≥ cn2 :
n(1 − 2c) + 1 ≥ 0
n(1 − 2c) ≥ −1
n(2c − 1) ≤ 1
12
1
2c − 1 ≤
n
Pero sabiendo que n ≥ 0 ⇒ n ≥ 0 y por lo tanto si 2c − 1 ≤ 0 ⇒ n1 ≥ 2c − 1.
1
Al hacer las 3 particiones tenemos que llamar recursivamente 3 veces a M ERGE S ORT
con los índices correspondientes:
13
def MergeSort3(A,p,r):
q=0
if p<r:
q=(r-p)//3
MergeSort3(A,p,p+q)
MergeSort3(A,p+q+1,p+(2*q))
MergeSort3(A,p+(2*q)+1,r)
Merge3(A,p,q,r)
def Merge3(A,p,q,r):
n1=q+1
n2=q
n3=r-(p+(2*q))
L=[]
M=[]
R=[]
for x in range(n1):
L.append(A[p+x])
for x in range(n2):
M.append(A[p+q+1+x])
for x in range(n3):
R.append(A[p+(2*q)+1+x])
#A continuacion declaramos nuestros sentinelas
L.append(1e30000); R.append(1e30000); M.append(1e30000);
j=0; i=0; k=0;
for x in range(p,r+1):
if L[i]<=M[j]:
if L[i]<=R[k]:
A[x]=L[i]
i=i+1
else:
A[x]=R[k]
k=k+1
else:
if M[j]<=R[k]:
A[x]=M[j]
j=j+1
14
else:
A[x]=R[k]
k=k+1
Este código se utiliza llamando a M ERGE S ORT 3 con parámetros: el array a ordenar, la
pocicion inicial y la longitud del array menos uno, por ejemplo:
A = [random.randint(0,1000) for i in xrange(1000)]
MergeSort3(A, 0, 999)
print A
Nos serviría para organizar un array aleatorio de 1000 elementos.
15
3. Síntesis del articulo e implementación en Phyton de
Simple two-array sort
Lea el artículo [MBM93]: P. M. McIlroy, K. Bostic and M. D. McIlroy, Engineering
radix sort, Computing Systems 6 (1993) 5-27 http://reference.kfupm.edu.sa/
content/e/n/engineering_radix_sort_108171.pdf
(a): Síntesis:
Radix Exchange: Los autores empiezan sentando las ideas y algoritmos base del ar-
ticulo, explicando en el primer caso, como es posible organizar strings que solo con-
tegan caracteres del alfabeto binario (Σ = {0, 1}) y como en el programa 1.1 codifican
el algoritmo, conocido como R ADIX -E XCHANGE, en el cual se van organizando re-
cursivamente las partes del array desde A[0] hasta A[mid − 1] y desde A[mid] hasta
A[hi − 1] (mid es una función que devuelve a mitad de la longitud del array) nótese
que el máximo nivel de recursión alcanzado es log(n)), en estos intervalos las cadenas
a ordenar tendrán el mismo prefijo de b − bits, y la función de Split es la encargada
de mover las cadenas de 0’s en la mitad izquierda y los 1’s en la mitad derecha del ar-
ray, sin embargo este algoritmo tenia una fuerte limitación y es que solamente trabaja
con cadenas que tengan la misma longitud, lo cual los autores nombran y mejoran en
el programa 1.2 en el cual se usa otra rutina de Split con 3 indicadores(programa
1.3 y figura 1.1), que parten el array en 4 grandes particiones: las strings que se sabe
que ya acabaron de revisar (denotada por los autores como la parte ∅), las cadenas que
tienen un 0 en el bit b-ésimo, las cadenas que tienen un 1 en el bit b-ésimo y aquellas
de las cuales no se conoce su estado actual, luego, la idea es ir reduciendo esta ultima
partición hasta que solo queden las primeras 3 particiones.
16
es que ya que aunque a Q UICK S ORT le fue muy bien los años en los que las memorias
eran costosas y mas importante, (y delimitante) muy pequeñas como para utilizar un
algoritmo que utilizara una cantidad de espacio en memoria razonable, como lo hace
cualquiera basado en R ADIX S ORT, ahora (de 1993 en adelante, la fecha en que se pub-
lico el paper) las maquinas son mucho mas grandes en capacidad de memoria (y menos
costosas!), luego, esta brecha necesita ser reconsiderada de nuevo, examinando cuales
son las implementaciones aceptables del R ADIX S ORT (en el articulo, se concentran en
L IST- BASED S ORT, T WO A RRAY S ORT y A MERICAN F LAG S ORT), teniendo en cuenta
que ya no se tienen las restricciones de hace unos años.
En la implementación del caso base en el que la lista esta vacía dependemos del
valor que tenga el apuntador a la lista “vacía”. El valor de un apuntador a una
lista vacía debe ser 0 y por lo tanto la estructura que almacena los apuntadores a
cada balde debe ser inicializada en cada instancia. Esto es un problema pues sólo
deberíamos inicializar aquellos apuntadores que puedan tener contenido, pero
para esto necesitaremos manejar un sólo array durante toda la ejecución y estar
conscientes de cuales son los baldes vacíos y cuales los ocupados.
17
Dado que el stack no es muy grande durante la ejecución del algoritmo es preferi-
ble manejarlo manualmente.
Los baldes con un sólo elemento no deberían ser divididos en baldes pues ya
están ordenados.
18
ordenar cada elemento del arreglo en su correspondiente parte/balde usando siempre
una cantidad constante de memoria. Para lograrlo usaremos solamente una posición
temporal y 255 apuntadores a distintas posiciones del arreglo. Al array de apuntadores
lo llamaremos pile
En un principio realizamos la misma rutina de conteo que en el TwoArraySort para
poder calcular el valor de cada uno de los apuntadores en pile. El i-ésimo elemento
de pile apunta hacia el final del i-ésimo balde. Una vez tenemos estos apuntadores re-
alizaremos la siguiente rutina:
5. Volvemos a al paso 3.
El anterior algoritmo terminará cuando todos los baldes sean seleccionados y llenados,
pero tenga en cuenta que si sólo falta un balde por llenar, ese balde ya esta ordenado,
pues los elementos en las posiciones restantes son elementos que no pertenecen a los
demás baldes y por tanto sólo pueden pertenecer al balde restante, que queda formado
por las posiciones restantes. Para la parte de intercambio de valores se aprovecha el
hecho de que se intercambian “strings”, es decir apuntadores a caracteres en C, por
lo que no es necesario copiar toda la cadena a intercambiar, sino sólo el apuntador al
primer elemento de la cadena.
2
Según se explica en el siguiente paso
19
Crecimiento de la pila: En los algoritmos expuestos la pila crece linealmente con el
tiempo de corrida, sin embargo podemos acotar el tamaño de la pila logaritmicamente,
organizándolo para que cada pila al final pueda ser splitteada Generalmente el control
de la pila no es necesario en la práctica, además de que no afecta mucho al tiempo de
corrida de los algoritmos.
Trucos para baldes vacíos Algunas formas de evadir los problemas de los baldes
vacíos son:
Mantener una lista de los baldes ocupados. Cada vez que se ingresa un elemento
a un balde vacío, se guarda el número del balde en una lista. Luego ordene esta
lista con respecto al número de cada balde. Si la lista es muy larga es ignorada y
se escanean todos los baldes.
Aho, Hopcroft, and Ullman muestran como eliminar pilas vacías en algoritmos
little-endian (nuestros algoritmos son big-endian), preordenando las letras de
todos los elementos para predecir las pilas que se necesitarán.
Hay varios artículos relacionados con la evasión de las pilas vacías, sin embargo el
confiar en un alfabeto bien distribuido resulta ser un enfoque bastante eficiente.
20
del Hardware y particularmente de la cantidad de instrucciones que se pueden ejecu-
tar por unidad de tiempo (pipelining). En la discucion que se hace al final del paper,
los autores comentan como surgió el articulo y que personas ayudaron implícitamente
su creación, se comentan algunas conclusiones, por ejemplo que aunque un R ADIX -
S ORT es rápido, las modificaciones que se hacen en un L IST BASED - R ADIX S ORT, son
mas eficientes; también comentan algunas implementaciones importantes que se han
hecho utilizando los algoritmos descritos, siendo la que mas llama la atención la im-
plementación del T WO -A RRAY BASED S ORT en la utilidad de BSD 3 sort que es Posix
Standard.
(b): Implementación de Simple two-array sort:
En el paso de C a Python del programa 3.1 del artículo debemos tener en cuenta varias
cosas:
1. Python no permite el uso de apuntadores (al menos no en su funcionalidad bási-
ca 4 ) en arrays pues su forma de manejar arrays es muy distinta a la de C. Python
provee una estructura mucho más flexible, la lista, con la que se implementarán
todas las estructuras que se usan en el programa, incluido el stack para el que
usaremos listas de listas, renunciando a las mejoras que los apuntadores agregan
al programa.
2. Dado que las estructuras de Python son distintas a las de C tendremos que redis-
eñar la estructura stack. Mientras en el programa en C se almacena un apuntador
al inicio del subarray y el tamaño del mismo, en Python se almacenará la posición
inicial y la posición final del subarray. Este cambio es posible pues la referencia
a la estructura que almacena el grupo de cadenas a ordenar no se cambia en es-
ta implementación, y los subíndices que se almacenan se refieren siempre a la
misma estructura. La variable sb del stack, por otro lado, se seguirá usando para
indicar la posición de la letra que se esta escaneando y con respecto a la cual se
delimita el array.
3. Dado que Python es un lenguaje fuertemente tipado [Duq08], el uso de caractéres
como índices está limitado a los diccionarios. Sin embargo el uso de diccionarios
para el mantenimiento de estructuras como pile o count no es muy eficiente 5
, y llega a costar más que la traducción de caractéres a código ASCII. Por esto
se decide usar listas en conjunto con la función ord() que permite devuelve el
código ASCII del carácter que se pase como argumento.
4. El programa recibirá una lista de strings que (al igual que en C) será modificada
mientras el programa se ejecuta. Es decir que el único parámetro que recibe el
3
Actualmente no se usa la misma que se usaba en el 93, sin embargo, en el interior sigue siendo una
modificacion de R ADIX S ORT: http://bsdsort.sourceforge.net/
4
Python permite el manejo de apuntadores con la librería ctypes, sin embargo usar esta librería
significaría renunciar a muchas de las facilidades que ofrece Python para le manejo de listas por lo que
en esta ocasión no se usará, sin embargo vale la pena mencionarla
5
Esto ha sido comprobado experimentalmente implementando las estructuras mencionados tanto
con listas como con diccionarios y midiendo las diferencias de tiempo, sin embargo esta información
no se presentará pues no esta dentro del alcance de este documento
21
programa se recibe por referencia (esto es posible en Python pues las listas son
objetos mutables).
def twoarray(A):
stack=[]
count=[0]*256
countacc=[0]*256
B=[]*len(A)
stack.append([0,len(A),0])
while(stack):
inicio,final,letra=stack.pop()
for i in range(inicio,final):
count[ord(A[i][letra])]=count[ord(A[i][letra])]+1
acc=inicio
for i in range(256):
acc=acc+count[i]
if (i>0 and count[i]>1):
stack.append([acc-count[i],acc,letra+1])
countacc[i]=acc
count[i]=0
B[:]=A[:]
for i in range(final-1,inicio-1,-1):
t=ord(B[i][letra])
countacc[t]=countacc[t]-1
A[countacc[t]]=B[i]
22
algoritmos se realizó sobre un equipo con procesador AMD Turion(tm) 64 X2 Mobile
Technology TL-58 de 2.00 Ghz, memoria RAM de 2 GB de capacidad y sistema oper-
ativo ArchLinux. Durante la ejecución de las pruebas todo demonio fue desactivado,
al igual que todas las interfaces de red fueron deshabilitadas para reducir al máximo
la cantidad de interrupciones. Las entradas proporcionadas a cada algoritmo son gen-
eradas aleatoriamente y son diferentes en cada ejecución. Se realizaron 3 mediciones
por entrada para cada algoritmo de tal forma que cada dato recolectado (cada punto
graficado) fuese un promedio y no un caso específico.
Aquí sólo se mostrará el código de la(s) funciones específicas de cada algoritmo imple-
mentado, asumiendo que ya están incluidas las librerías necesarias y que se esta lla-
mando al algoritmo correctamente con un input válido. Para conocer el procedimiento
usado para la medición y graficación por favor revise los Apéndices. Debe tenerse en
cuenta que los arrays en Python empiezan desde 0 y no desde 1 como lo hacen en el
pseudocódigo libro:
1. Q UICK S ORT: Para este algoritmo lo que hicimos fue básicamente implementar el
Pseudocódigo que se encontraba en el [Cormen01]:
def QuickSort(A,p,r):
if p<r:
q=Partition(A,p,r)
QuickSort(A,p,q-1)
QuickSort(A,q+1,r)
def Partition(A,p,r):
x=A[r]
i=p-1
for j in range(p,r):
if A[j]<=x:
i=i+1
A[i],A[j]=A[j],A[i]
A[i+1],A[r]=A[r],A[i+1]#Swapping A[i+1] con A[r]
return i+1
2. R ANDOMIZED -Q UICK S ORT: Para la version aleatoria del Q UICK S ORT, importa-
mos la libreria random de Python la cual utilizamos para elejir el pivot aleatorio
del sub-array A[p . . . r]:
def QuickSort_r(A,p,r):
if p<r:
q=Partition_r(A,p,r)
QuickSort(A,p,q-1)
QuickSort(A,q+1,r)
def Partition_r(A,p,r):
i=random.randint(p,r) #Random sampling del pivot
23
A[r],A[i]=A[i],A[r]
return Partition(A,p,r)
1.6
Tiempo de ejecucion (segundos)
1.4
1.2
0.8
0.6
0.4
0.2
0
10000 20000 30000 40000 50000 60000 70000 80000 90000 100000
Tamano del array
24
3. M ERGE S ORT: Gracias a la clara sintaxis de Python, fue sencillo implementar el
M ERGE S ORT, siendo la única parte que cambia, la forma en la que codificamos
la idea del centinela utilizado en el pseudocódigo de M ERGE :
def MergeSort(A,p,r):
"""Recibe un array A y las posiciones
desde (p) y hasta (r) a ordenar en A."""
q=0
if (p<r):
q=(p+r)/2 #Python toma el piso
MergeSort(A,p,q)
MergeSort(A,q+1,r)
Merge(A,p,q,r)
def Merge(A,p,q,r):
n1=q-p+1
n2=r-q
L=[]
R=[]
for x in range(1,n1+1):
L.append(A[p+x-1])
for x in range(1,n2+1):
R.append(A[q+x])
L.append(1e30000)#Esto representaria un infinito en python
#pues,como lo senala el PEP754, es lo suficientemente grande
#como para generar un overflow en el formato de
#representacion decimal especificado por IEEE 754
R.append(1e30000)
i=0 #La 1ra pos. de un array es la 0
j=0
for x in range(p,r+1):
if L[i]<=R[j]:
A[x]=L[i]
i=i+1
else:
A[x]=R[j]
j=j+1
def MergeSort3(A,p,r):
q=0
if p<r:
25
q=(r-p)//3
MergeSort3(A,p,p+q)
MergeSort3(A,p+q+1,p+(2*q))
MergeSort3(A,p+(2*q)+1,r)
Merge3(A,p,q,r)
def Merge3(A,p,q,r):
n1=q+1
n2=q
n3=r-(p+(2*q))
L=[]
M=[]
R=[]
for x in range(n1):
L.append(A[p+x])
for x in range(n2):
M.append(A[p+q+1+x])
for x in range(n3):
R.append(A[p+(2*q)+1+x])
#A continuacion declaramos nuestros sentinelas
L.append(1e30000); R.append(1e30000); M.append(1e30000);
j=0; i=0; k=0;
for x in range(p,r+1):
if L[i]<=M[j]:
if L[i]<=R[k]:
A[x]=L[i]
i=i+1
else:
A[x]=R[k]
k=k+1
else:
if M[j]<=R[k]:
A[x]=M[j]
j=j+1
else:
A[x]=R[k]
k=k+1
26
Comparaciones de tiempo de ejecucion de MergeSort y MergeSort con 3 particiones
3.5
3Merge Sort
Merge Sort
3
Tiempo de ejecucion (segundos)
2.5
1.5
0.5
0
10000 20000 30000 40000 50000 60000 70000 80000 90000 100000
Tamano del array
5. C OUNTING S ORT:
def countingsort(A,k):
C=[0]*(k+1) #Llenamos de 0’c el array C
for x in range(len(A)):
C[A[x]]=C[A[x]]+1
for x in range(1,k+1):
C[x]=C[x]+C[x-1]
B=[0]*len(A) #Llenamos de 0’c el array B
for x in range(len(A)-1,-1,-1):
C[A[x]]=C[A[x]]-1
B[C[A[x]]-1]=A[x]
A[:]=B[:] #Copiamos el contenido del array B al array A
6. R ADIX S ORT: Para el caso de Radix utilizamos un algoritmo estable para or-
27
denar el digito i-ésimo del los numeros del array, este es una version modificada
del C OUNTING S ORT, la cual hemos llamado countingsort_r que tambien
mostramos a continuación:
def radixsort(A,b):
# b : Numero maximo de bits que puede
# tener un elemento del arreglo"""
r=int(math.log(len(A),2))
d=int(math.ceil(float(b)/r))
B=[]
for x in range (len(A)):
B.append([0,A[x]])
mask=int(’1’*r,2)
for x in range(d):
for i in range(len(A)):
B[i][0]=(mask&B[i][1])>>(x*r)
countingsort_r(B,2**r)
mask=mask<<r
for x in range(len(A)):
A[x]=B[x][1]
def countingsort_r(A,r):
C=[0]*r
for x in range(len(A)):
C[A[x][0]]=C[A[x][0]]+1
for x in range(1,r):
C[x]=C[x]+C[x-1]
B=[[0,0]]*len(A)
for x in range(len(A)-1,-1,-1):
C[A[x][0]]=C[A[x][0]]-1
B[C[A[x][0]]]=A[x]
A[:]=B[:]
28
Comparaciones de tiempo de ejecucion de RadixSort y CountingSort
1.2
Radix Sort
Counting Sort
1
Tiempo de ejecucion (segundos)
0.8
0.6
0.4
0.2
0
10000 20000 30000 40000 50000 60000 70000 80000 90000 100000
Tamano del array
7. H EAP S ORT:
29
Max_Heapify(A, largest, n) #Mantenemos el Heap
2.5
Tiempo de ejecucion (segundos)
1.5
0.5
0
10000 20000 30000 40000 50000 60000 70000 80000 90000 100000
Tamano del array
30
Comparaciones de tiempo de ejecucion de varios algoritmos de ordenamiento
14
Radix Sort
Insertion Sort
Quick Sort
12 Randomized Quick Sort
Merge3 Sort
Merge Sort
Counting Sort
Heap Sort
Tiempo de ejecucion (segundos)
10
0
0 1000 2000 3000 4000 5000 6000 7000 8000 9000 10000
Tamano del array
Al hacer la comparación vemos que los tiempos son muy parecidos, compro-
bando lo que nos dice la teoría, ya que asintoticamente tanto H EAP S ORT co-
mo M ERGE S ORT tienen un tiempo de corrida de O(n lg n), sin embargo, aunque
las constantes ocultas en 3PARTITIONS -M ERGE S ORT son menores, este sigue uti-
lizando una cantidad no constante de espacio adicional, mientras que H EAP S ORT
tiene la ventaja de ser un algoritmo inplace
31
Comparaciones de tiempo de ejecucion de varios algoritmos de ordenamiento
0.0018
Insertion Sort
Quick Sort
0.0016 Merge3 Sort
Counting Sort
Heap Sort
0.0014
Tiempo de ejecucion (segundos)
0.0012
0.001
0.0008
0.0006
0.0004
0.0002
0
0 10 20 30 40 50 60 70 80 90 100
Tamano del array
Para el caso cuando el numero de llaves es pequeño n ≤ 100 vemos que aunque
la diferencia no es tan significativa, se ve como I NSERTION S ORT los va pasando
uno a uno a medida que el tamaño de input crece, se alcanza a observar que
aproximadamente desde n > 85 ya ningún algoritmo esta por encima de I N -
SERTION S ORT , sin embargo cabe destacarlo por su facilidad de implementación
y análisis, por ejemplo si tuviéramos que organizar repetidamente una lista de
números que no exceden en cantidad a 50 llaves, podríamos pensar en Insertion
como un buen candidato, ya que implementar algoritmos mas sofisticados como
Q UICK S ORT no seria de gran ayuda. En el caso de los inputs grandes inmediata-
mente notamos que los tiempos de I NSERTION S ORT están muy por encima de los
demás, tanto que la escala del eje de las ordenadas pierde significancia para los
demás algoritmos, lo que nos indica que cualquier algoritmo de ordenamiento
que tenga complejidad O(n2 ) o mayor (B UBBLE S ORT, S ELECTION S ORT, o inclu-
sive B OGO S ORT que es ¡O(n!)!), en su peor caso, como lo es I NSERTION S ORT, son
en la practica, nada implementables, ya que al tener un numero de llaves razon-
ablemente grande (n > 100000), la diferencia de tiempos es muy grande.
32
Comparaciones de tiempo de ejecucion de varios algoritmos de ordenamiento
0.00045
Insertion Sort
Quick Sort
0.0004 Merge3 Sort
Counting Sort
Heap Sort
0.00035
Tiempo de ejcucion (segundos)
0.0003
0.00025
0.0002
0.00015
0.0001
5e-05
0
0 5 10 15 20 25 30
Tamano del array
5. Suffix Array
En un primer intento se implementó el algoritmo TwoArraySort [MBM93] teniendo
en cuenta varios aspectos:
1. El hecho de que se está ordenando un array de números con respecto a subcade-
nas de una cadena. Es decir, no se ordenarán cadenas sino números por lo que
el espacio temporal usado es mucho menor. Además no se necesita guardar un
arreglo de cadenas (sufijos) a ordenar pues es suficiente el arreglo de subíndices
para referenciar cada subcadena.
2. El alfabeto sobre el que se trabaja es de una longitud fija (letras y números) de 64,
mucho menor que el alfabeto de la implementación original del TwoArraySort.
Este cambio ahorra muchos ciclos inútiles del TwoArraySort original.
3. Es posible realizar la traducción de carácteres a códigos ASCII antes del proce-
samiento de la cadena, para evitar la ejecución repetitiva de la instrucción ord().
El programa que se presenta a continuación obtuvo un puntaje de sólo 27.27:
def sarray(S):
stack=[]
33
count=[0]*64
countacc=[0]*64
R=range(len(S))
B=[]*len(R)
A=[]
for x in S:
s=ord(x)
if s<58:
A.append(s-47)
elif s<91:
A.append(s-54)
elif s<123:
A.append(s-60)
A.append(0)
stack.append([0,len(R),0])
while(stack):
inicio,final,letra=stack.pop()
for i in range(inicio,final):
t=A[R[i]+letra]
count[t]=count[t]+1
acc=inicio
for i in range(63):
acc=acc+count[i]
if (i>0 and count[i]>1):
stack.append([acc-count[i],acc,letra+1])
countacc[i]=acc
count[i]=0
B[:]=R[:]
for i in range(final-1,inicio-1,-1):
t=A[B[i]+letra]
countacc[t]=countacc[t]-1
R[countacc[t]]=B[i]
return R
if __name__=="__main__":
c=raw_input()
S=sarray(c)
for i in S:
print i
34
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SIZE 100000
void get_sarray(unsigned char *string,int longitud, int *sarray){
#define push(string,longituds,letra) sp->sa=string,sp->sn=longituds,
(sp++)->sletra=letra
#define pop(string,longituds,letra) string=(--sp)->sa,longituds=sp->sn,
letra = sp->sletra
#define stackempty() (sp<=stack)
#define splittable(c) c>0 && count[c]>1
struct {int* sa;int sn,sletra;}stack[SIZE],*sp=stack;
int *pile[256],*ak;
static int count[256];
int i,letra,*tsarray;
tsarray=malloc(longitud*sizeof(int));
for (i=longitud;--i>=0; )
sarray[i]=i;
push(sarray,longitud,0);
while(!stackempty()){
pop(sarray,longitud,letra);
for (i=longitud;--i>=0;)
count[string[sarray[i]+letra]]++;
for (ak=sarray,i=0;i<256;i++){
if (splittable(i))
push(ak,count[i],letra+1);
pile[i] = ak += count[i];
count[i]=0;
}
for (i=longitud;--i>=0;)
tsarray[i]=sarray[i];
for (i=longitud;--i>=0;)
*--pile[string[tsarray[i]+letra]]=tsarray[i];
}
free(tsarray);
}
int main(){
int* sarray;
int i;
char cadena[100001];
scanf("%s",cadena);
i=strlen(cadena);
sarray = malloc(i*sizeof(int));
get_sarray(cadena,i,sarray);
int tmp=i;
for (i=0;i<tmp;i++)
printf("%d\n",sarray[i]);
35
return 0;
}
36
6. Apéndices
Programa escrito para Python 2.6.5 usado para generar las gráficas de tiempos de
los diferentes algoritmos de ordenamiento. En el script se supone que existe un archi-
vo sorts.py, en el mismo directorio de ejecución, que contiene los códigos de los difer-
entes algoritmos de ordenamiento. El programa hace uso de GnuplotPY, un módulo
que permite usar Gnuplot a través de Python. La variable n en el programa indica la
cantidad de datos a promediar por entrada. Las gráficas son exportadas en formato
PostScript para poder utilizarlas en este documento escrito en LaTex.
import random
import timeit
import Gnuplot
if __name__=="__main__":
RS=[]
MS=[]
MS3=[]
CS=[]
QS=[]
RQS=[]
HS=[]
n=3
for x in range(10000,100000,5000):
t=timeit.Timer("a=[random.randint(0,"+str(x)+") for i in xrange("+
str(x)+")];sorts.radixsort(a,"+str(math.log(x,2))+")","import random,sorts")
RS.append([x,t.timeit(n)/n])
37
RQS.append([x,t.timeit(n)/n])
g=Gnuplot.Gnuplot(debug=0)
g( ’ set terminal postscript ’ )
g("set output ’TODOS.eps’")
g( ’ set style data lp ’ )
g.title("Comparaciones de tiempo de ejecucion de varios algoritmos de
ordenamiento")
g.xlabel("Tamano del array")
g.ylabel("Tiempo de ejcucion (segundos)")
g.plot(Gnuplot.Data(RS,title="Radix Sort"),Gnuplot.Data(QS,title="Quick
Sort"),Gnuplot.Data(RQS,title="Randomized Quick Sort"),Gnuplot.Data(MS3,ti
tle="Merge3 Sort"),Gnuplot.Data(MS,title="Merge Sort"),Gnuplot.Data(CS,
title="Counting Sort"),Gnuplot.Data(HS,title="Heap Sort"))
38
g( ’ set style data lp ’ )
g.title("Comparaciones de tiempos de ejecucion de MERGE3 SORT y HEAPSORT")
g.xlabel("Tamano del array")
g.ylabel("Tiempo de ejcucion (segundos)")
g.plot(Gnuplot.Data(MS3,title="Merge 3 Sort"),Gnuplot.Data(HS,title="Heap
Sort"))
39
cn
T (n) T ( n2 ) T ( n2 ) T ( n2 ) T ( n2 )
(a) (b)
cn
40
c( n2 ) c( n2 ) c( n2 ) c( n2 )
T ( n4 ) T ( n4 ) T ( n4 ) T ( n4 ) T ( n4 ) T ( n4 ) T ( n4 ) T ( n4 ) T ( n4 ) T ( n4 ) T ( n4 ) T ( n4 ) T ( n4 ) T ( n4 ) T ( n4 ) T ( n4 )
c( n2 ) c( n2 ) c( n2 ) c( n2 ) ⇒ c( 4n
2
)
+
2
c( n4 ) c( n4 ) c( n4 ) c( n4 ) c( n4 ) c( n4 ) c( n4 ) c( n4 ) c( n4 ) c( n4 ) c( n4 ) c( n4 ) c( n4 ) c( n4 ) c( n4 ) c( n4 ) ⇒ c( 422n )
+
i
.. .. .. ..
....
.. .. .. ..
....
.. .. .. ..
....
.. .. .. ..
....
.. .. .. ..
....
.. .. .. ..
....
.. .. .. ..
....
.. .. .. ..
....
.. .. .. ..
....
.. .. .. ..
....
.. .. .. ..
....
.. .. .. ..
....
.. .. .. ..
....
.. .. .. ..
....
.. .. .. ..
....
.. .. .. ..
.... ⇒ c( 42in )
41
+
T (1) T (1) T (1) ... ... ... ... ... ... ... ... ... ... ... ... ⇒. . . Θ(nlog2 4 ) = Θ(n2 )
—–
lg(n)−1
P
(c(2i n)) + Θ(n2 )
i=0
42
T (αk n) + T (αi (1 − α)j n)· · · T (αi (1 − αj )n) + T (αl (1 − α)k n) + ··· + T ((1 − α)k n) + T (αl (1 − α)k n) = Ok=lg n (n)
Plog α1 n
i=1 cn
cn log 1 n
α
O(n · lg n)
[KA05] Pang Ko and Srinivas Aluru. Space efficient linear time construction of
suffix arrays. Journal of Discrete Algorithms, 3(2-4):143 – 156, 2005. Combina-
torial Pattern Matching (CPM) Special Issue.
[MBM93] Peter M. McIlroy, Keith Bostic, and M. Douglas McIlroy. Engineering radix
sort. COMPUTING SYSTEMS, 6, 1993.
43