Sunteți pe pagina 1din 43

Solución Taller 2 - Análisis y Diseño de

Algoritmos

Santiago Alonso Perez Rubiano - Código: 257872


Juan Sebastian Otálora Montenegro - Código: 257867

16 de octubre de 2010

1. Solución Problemas del [Cormen01]


Resolver los siguientes problemas del [Cormen01].

1. 4.2-3 pág 72 y 4.2-5 pág 72.

4.2-3 : La recurrencia T (n) = 4T (bn/2c) + cn, donde c es una constante, es del


orden de Θ(n2 ). Esto lo pudimos deducir gracias al árbol de recursión (Figura
6) que se muestra en el apéndice. Ahora procedemos con la comprobación de
T (n) = Θ(n2 ) mediante el método de sustitución: Para hacer nuestra hipótesis
de inducción mas fuerte, y poder sustraer los términos de menor orden, la enun-
ciamos así: Hipótesis de Inducción: T (k) ≤ c1 kn2 − c2 kn, ∀k ≤ n

Probemos primero que T (n) = O(n2 )

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 .

Paso inductivo Demostramos que si T (n) = O(n2 ) se cumple para n, entonces


en particular se cumple para k = n/2, lo que nos lleva a:

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.

Ahora probemos que T (n) = Ω(n2 )

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:

cαn + c(1 − α)n = cαn + cn − cαn = cn

Ahora veamos la suma de los nodos del tercer nivel:

cα2 n + cα(1 − α)n + cα(1 − α)n + c(1 − α)2 n =

cα2 n + cαn − cα2 n + cαn − cα2 n + c(1 − 2α + α2 )n =


cn
Con esto podemos suponer que cada nivel del árbol tiene un costo de cn = O(n).
Ahora debemos buscar la altura del árbol, sin embargo para esto tendremos que
tener en cuenta varios hechos:

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−α

4) αi (1 − α)j = 1 , con i > j


Con un razonamiento parecido al anterior llegamos a que j < i < log 1 n
α
y por lo tanto en este caso la altura del árbol es menor a log 1 n
α

5) Sabiendo que si c > d ⇒ logd n > logc n, puesto que:

ln c > ln d

ln c ln n > ln d ln n , con n > 1


ln n ln n
>
ln c ln d
logd n > logc n
6) A partir de los hechos anteriores y trabajando sólo sobre el caso resalta-
do en el Hecho (a), podemos decir que la máxima altura del árbol es de

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):

T (n) = Θ(n log n)

Ahora usaremos el método de substitución para demostrar que esto es cierto.

Probemos primero que T (n) = O(n lg 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:

T (n) = T (αn) + T ((1 − α)n) + cn


≤ dαnlg(αn) + d(1 − α)nlg((1 − α)n) + cn
= dαnlg(αn) + dnlg((1 − α)n) − dαnlg((1 − α)n) + cn
= dαn(lg(αn) − lg((1 − α)n)) + dn(lg(n) + lg(1 − α)) + cn
αn
= dαn(lg( ) + dnlg(n) + dnlg(1 − α)) + cn
(1 − α)n
α
= dnlg(n) + dαn(lg( ) + dnlg(1 − α)) + cn
1−α
α
= dnlg(n) + dn(αlg( ) + lg(1 − α)) + cn
1−α
≤ nlg(n)
α
Para que esto último se cumpla necesitamos que dnlg(n) + dn(αlg( 1−α ) + lg(1 −
α)) + cn ≤ nlg(n):
α
dnlg(n) + dn(α(lg( )) + lg(1 − α)) + cn ≤ nldog(n)
1−α
α
dn(αlg( ) + lg(1 − α)) + cn ≤ 0
1−α
α
dn(αlg( ) + lg(1 − α)) ≤ −cn
1−α
1
Lo suficientemente pequeño

4
−c
d≤ α
αlg( 1−α ) + lg(1 − α)
Esto conserva la propiedad de d > 0 pues log α < 0 y log 1 − α < 0.

Probemos ahora que T (n) = Ω(nlgn)

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).

2. 4.3-2 pág 75 y 4.3-4 pág 75.

4.3-2 Primero encontramos el orden de la recursión T (n) = 7T ( n2 ) + n2 usando


el primer caso del método maestro con a = 7, b = 2 y f(n) = n2 :

f(n) = n2 = O(nlog2 7− ) = O(n2,81...−0,81... ) = O(n2 )

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í:

a) Utilizar el primer caso del método maestro:


Necesitamos entonces que f(n) = n2 = O(nlogb a− ) = O(nlog4 a − ) para
que T 0 (n) = O(nlogb a ) = O(O(nlog4 a ). Además tenemos que garantizar que
el nuevo algoritmo sea asintóticamente más rápido que T (n), es decir que
nlog4 a < nlog2 7 ⇒ log4 a < log2 7. Para garantizar la primera parte necesita-
mos que log4 a ≥ 2 +  ⇒ log4 a > 2 pues de otra forma n2 = O(na ) , con
2
a < 2 implica nna ≤ c ⇒ n2−a ≤ c, lo que termina siendo una contradicción,
pues 2 − a > 0 y decir que nb ≤ c con b > 0 es encontrar un máximo en los
naturales, lo cual claramente no es posible.
Así pues necesitamos que:

2 < log4 a < log2 7

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).

4.3-4 Para la fórmula de recursión T (n) = 4T ( n2 )+n2 lg n definimos a = 4, b = 2


y f(n) = n2 lg n. Vemos que logb a = log2 4 = 2 y dado que f(n) = Ω(n2 ) (pues
lg n es una función creciente y positiva para todo n lo suficientemente grande),
podemos pensar en aplicar la tercera parte del teorema maestro, sin embargo
antes debemos verificar que af( nb ) ≤ cf(n), con c < 1:
n
af( ) ≤ cf(n)
b
n
4f( ) ≤ cf(n)
2
n 2 n
4( ) lg ( ) ≤ cn2 lg n
2 2
2
4n
(lg n − lg 2) ≤ cn2 lg n
22
n2 (lg n − 1) ≤ cn2 lg n
lg n − 1 ≤ c lg n
−1 ≤ c lg n − lg n
1 ≥ lg n − c lg n
1 ≥ (1 − c) lg 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

T (n) ≈ ( n2 lg n − in2 ) + Θ(4lg n )


i=0

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.

Paso Inductivo Suponemos que T (n) = O(n2 lg2 n) se cumple para n


2
e inten-
tamos concluir que también se cumple para n:

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

Para que esto último se cumpla necesitamos que:

cn2 lg2 n − 2cn2 lg n + cn2 + n2 lg n ≤ cn2 lg2 n


cn2 − n2 lg n(2c − 1) ≤ 0
c − lg n(2c − 1) ≤ 0
c ≤ lg n(2c − 1)
c
≤ lg n
2c − 1
Es decir que si se cumple c
2c−1
≤ lg n0 , tenemos que T (n) = O(n2 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:

T (n) = Θ(nlogb a ) = Θ(nlog2 7 ) = Θ(n2,8073... )



f ) T (n) = 2T ( n4 ) + n
1
Usamos el teorema maestro con a = 2, b = 4 y f(n) = n 2 :
1 1 1
Como nlogb a = nlog4 2 = n 2 , tenemos que n 2 = Θ(nn 2 ) y por lo tanto pode-
mos aplicar el segundo caso del método maestro:

T (n) = Θ(nlogb a lg n) = Θ( n lg n)

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 )

Probamos nuestra asunción T (n) = Θ(n2 ) por inducción:

Probemos primero 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.

Paso inductivo Asumiendo que T (n) = O(n2 ) se cumple para n − 1

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 :

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 ).

Ahora probemos que T (n) = Ω(n2 )

Caso Base El caso base T (1) ≥ c se tiene gracias a que T (n) es constante
para n ≤ 2.

Paso inductivo Asumiendo que T (n) = Ω(n2 ) se cumple para n − 1

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 :

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

Es decir que si c ≤ 21 se cumple que T (n) = Ω(n2 )


Hemos demostrado entonces que T (n) = Ω(n2 ) y que T (n) = O(n2 ) y por
lo tanto T (n) = Θ(n2 )

h) T (n) = T ( n) + 1
Usaremos el método de cambio de variable ilustrado en la página 66 de
[CLRS01]. Para conseguir una fórmula de recursión más manejable defin-
imos m = log2 n. Luego definimos S(m) = T (2m ), con lo que podemos
expresar:
m
)+1
T (n) = S(m) = S(
2
La fórmula de recursión de S(m) puede resolverse con ayuda del método
maestro, pues a = 1 ≥ 1 , b = 2 > 1 y f(n) = 1 = O(nlog2 1 ) = O(1).
Con esto podemos decir que S(m) = Θ(log m) para luego reemplazar m y
obtener que: T (n) = Θ(log log n).

2. Merge-Sort Con 3 Particiones


Proponga una extensión del algoritmo de ordenamiento mergesort que utilice tres parti-
ciones en vez de dos. Escriba el algoritmo en Python y haga una análisis de su tiempo de
ejecución estudiando su recursión.
Para la extensión del M ERGE S ORT, utilizamos la misma idea del algoritmo con 2 par-
ticiones, solo que al tener en cuenta las 3 particiones[fig. 1] de cada subarray (todo el
array en la primera iteración) en el procedimiento M ERGE había que hacer un par de
comparaciones mas, gracias al árbol de decisión que mostramos a continuación [fig.
2], pudimos codificar las respectivas comparaciones:

Figura 1: Las 3 particiones del Array A

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)

Esto nos dará lugar a la recurrencia siguiente:


n
T (n) = 3T ( ) + n
3
En donde el costo de cada tiempo de corrida depende de 3 veces el tiempo de corrida
con un input de tamaño n3 mas el costo de la operación M ERGE 3 que se muestra a
continuación:

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.

Figura 2: Decisión Tree Para las comparaciones en M ERGE 3

2.1. Análisis de la recurrencia


Al analizar el tiempo de ejecución del algoritmo 3PARTITIONS -M ERGE S ORT, lleg-
amos a la siguiente recurrencia:

Θ(1) :n=1
T (n) = n
3T ( 3 ) + n : n > 1
Ya que cada vez generamos 3 subproblemas que tienen tamaño n3 y a esto debemos
añadirle el costo de cada llamado a M ERGE 3 el cual esta acotado por el tamaño de
entrada. Es claro que esta recurrencia cumple con las condiciones del Teorema Maestro
parte 2, con a = 3, b = 3, f(n) = n y además f(n) = Θ(nlog3 3 ) = Θ(n) luego podemos
concluir que:
T (n) = Θ(nlog3 3 lg n) = Θ(n lg n)
Con esto podemos darnos cuenta de que el aumentar el numero de particiones no
mejora asintoticamente el tiempo de corrida de M ERGE -S ORT, esto se ve claramente
en las gráficas del punto 4.

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.

Quicksort to the fore En teoría QuickSort es de orden Ω(n log2 n) en promedio


y Ω(n2 log n) en el peor caso, mientras RadixExchange es de orden Ω(n log n) en
cualquier caso. Pero aunque en teoría RadixExchange es mucho más rápido que Quick-
Sort, en la práctica QuickSort aprovecha ciertas facilidades de los procesadores y vence
a RadixExchange en la mayoría de los casos. Una de las ventajas de QuickSort es
que puede usar instrucciones de máquina para comparar bytes o palabras (grupos
de bytes) enteras, mientras Radix necesita examinar cada bit. Por otra parte QuickSort
puede ordenar cualquier tipo de cosas, pues lo único que necesita es una rutina de
comparación para funcionar, mientras RadixExchange necesita que la información este
bien representada en memoria para poder realizar su trabajo. Pero a parte de algunas
diferencias prácticas, ambos algoritmos son muy parecidos, tienen las mismas desven-
tajas cuando las cadenas a comparar tiene diferentes longitudes distintas, o cuando las
cadenas son largas, para esto señalan los autores, es mejor utilizar un mapeo de las di-
recciones de las cadena a un descriptor pequeño y ordenarlos organizando pequeños
descriptores, en ves de las gigantescas cadenas.
Concluyen esta seccion con un párrafo en el que se nombra la idea central del paper y

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.

Ordenamiento basado en listas (List-base sort) En este ordenamiento intentamos


aprovechar el hecho de que en los computadores actuales el acceso a un byte de memo-
ria es económico. Lo difícil aquí no es construir y manejar los baldes, sino ordenarlos
finalmente en una sola lista. Para lidiar con este problema este algoritmo hará una lista
para cada balde. Cada lista tendrá entonces cadenas que tienen el mismo valor en la
b-ésima posición.
Con un array de apuntadores a la primera posición de cada lista/balde , podemos or-
denar cada lista llamando recursivamente al mismo procedimiento de división en lis-
tas sobre cada balde. Ahora debemos definir un caso base para el algoritmo recursivo,
pero antes debemos mencionar que al algoritmo debe llamárselo con 3 argumentos,
la lista y el caracter con respecto al que se ordenará, como es de esperar, y una lista
ordenada en la que sólo haya cadenas más grandes que las de la primera lista, es decir
una lista que deba ir al final de la lista desordenada, una vez este ordenada claro esta,
este tercer argumento permite que cuando se estén ordenando los baldes se pase co-
mo tercer argumento la lista de los baldes que ya han sido ordenados. Ahora podemos
definir los dos casos base, uno es cuando la lista esté vacía, es decir que devolveré-
mos la lista de elementos mayores, pues no hay elementos que ordenar; y el otro es
cuando el último carácter con respecto al que se ordeno fue el caractér que indica la
terminación de la cadena, pues en este caso estamos tratando con un balde del carácter
de terminación, que no puede ser ordenado más, ahora devolvemos este balde/lista
seguida de los elementos de la lista de elementos mayores, es decir, devolvemos una
lista ordenada.
En una implementación práctica de este algoritmo nos encontramos con varios prob-
lemas:

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.

Evitar examinar los baldes vacíos.

Solamente manejando la pila manualmente y evitando dividir los baldes de un sólo


elemento se logra que el algoritmo corra hasta 15 veces más rápido. Tener en cuenta
todos estos factores puede decrementar el tiempo de corrida del algoritmo en un factor
de hasta 100.
Este algoritmo no ordena de forma estable pero si in-place, el siguiente algoritmo que
veremos ordena de forma estable pero no in-place.

Ordenamiento con dos arreglos (TwoArraySort) En este algoritmo intentamos ex-


plotar las ventajas del RadixExchange con un alfabeto Σ correspondiente a las 256
letras del alfabeto. En esta ocasión en vez de crear listas para cada balde, asignaremos
una seccion del array a cada balde, sin embargo para lograr que esta estrategia tenga
efecto debemos llevar la cuenta la ubicación de cada balde.
En este algoritmo además de mantener los baldes en el arreglo inicial, los manten-
dremos en orden, de tal forma que aquellos baldes con sólo un elemento no necesiten
ser reordenados. Con esto en mente sabemos que cada balde al que pertenezca más
de una cadena debe ser reordenado, por lo que llevaremos en todo momento una pi-
la con información acerca de aquellos baldes que deben ser reordenados (simulando
una recursión sin el sobrepeso de retornos de función y paso de argumentos), con re-
specto a la siguiente letra. Por otra parte para obtener la posición de cada balde y para
ordenar los baldes usaremos la misma rutina que se usa en Counting Sort, a saber, con-
tar las ocurrencias de cada letra y sumar consecutivamente las cuentas para obtener
la posición de cada balde. Con estas posiciones se realiza una copia del array original
(haciendo honor al nombre del algoritmo “TwoArray”) del que tomaremos los ele-
mentos para reingresarlos a cada balde.
Al final de la primera pasada de la rutina CountingSort, todos los baldes están orde-
nados y la pila tiene información de aquellos arreglos a reordenar con respecto a la
siguiente letra.
El algoritmo terminará cuando no haya más elementos en la pila, es decir cuando no
sea necesario ordenar más baldes.
Una mejora importante a este algoritmo es usar un algoritmo de ordenamiento rápido
para baldes pequeños (como insertion sort para n<20 como vimos antes en el taller) y
no examinar las pilas vacías. Con respecto a esto último hablaremos en un apartado al
final del artículo.

Ordenamiento de la bandera Estadounidense En contraste con el problema de la


bandera alemana, presentamos el Ordenamiento de la bandera Estadounidense. En
este algoritmo debemos dividir el arreglo en más de 2 partes (256 para nuestro caso) y

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:

1. Seleccionamos el primer balde sin ordenar y lo llamamos b(que en un principio


es la parte inicial del arreglo).

2. Seleccione el primer elemento del balde b y lo almacenamos en un espacio tem-


poral r. A la posición de la que tomamos este elemento la llamaremos ak.

3. Buscamos el apuntador al final del balde c al que pertenece la palabra almace-


nada en r. Si esta posición es igual a ak, es por que sólo falta un elemento por
ordenar del balde b2 y por lo tanto pasamos al paso 6.

4. Intercambiamos el elemento en la posición apuntada por c con el elemento en r y


decrementamos el apuntador a la última posición del balde para indicar que ya
hemos ubicado uno de los elementos del balde en su posición. Note que si en un
momento dado sólo falta un elemento del balde por insertar, el apuntador al final
del balde estará apuntando (valga la redundancia) hacia la primera posición del
balde (la posición de la que sacamos el elemento para iniciar el procedimiento),
pues se habrá decrementado lo suficiente en este paso.

5. Volvemos a al paso 3.

6. Hemos llegado a la posición de la que en un inicio sacamos el elemento para r,


es decir que el contenido de esta posición es irrelevante, su contenido ha sido
trasladado a su correspondiente balde. Así que podemos almacenar el contenido
actual de r en esta posición descartando su antiguo contenido. Ahora todo el
balde b esta lleno y podemos pasar a ordenar el siguiente balde. Seleccionamos
el siguiente balde y lo hacemos el nuevo b para ir al paso 2.

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.

Mantener árboles de ocupación. Sin embargo esta estrategia no supera el tiempo


de corrida de los algoritmos aquí descritos.

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.

Rendimiento y Discusión final En las sección final se discuten algunas considera-


ciones practicas y que el rendimiento de los algoritmos expuestos es el mismo: T (n) =
O(S), siendo S el tamaño de los datos medidos en bytes, pero que sin embargo el
rendimiento con respecto uno del otro, varia con el hardware en donde se implemente
y de que optimizaciones haga o no el compilador que se use.
Para comparar los diferentes algoritmos con Q UICK S ORT se uso una versión especial-
izada de este por los autores (sin embargo no optimizada para que corra a la máxima
velocidad posible en C), y la idea consistía en organizar 73000 palabras de un famoso
diccionario Británico, a continuación muestran una gráfica en la cual comparan el
rendimiento de los algoritmos discutidos para inputs con tamaño de n = 10000 hasta
n = 100000, algo que deja de mostrar claramente la gráfica es el n2 lg(n) del tiempo
de corrida de Q UICK S ORT, sin embargo, como comentan los autores, el rango del ex-
perimento es muy pequeño, para revelar la no linealidad del algoritmo, pero lo que
si podemos apreciar es que los Radix se comportan mejor siempre en ese rango de
inputs, cabe destacar como se ve afectado el rendimiento de Q UICK S ORT al ordenar
dígitos aleatorios, en el cual, esta implementación, tiene un rendimiento bastante in-
ferior al de los demás algoritmos, también en la tabla 5.1 muestran cuales fueron las
maquinas y compiladores utilizados en el experimento, los autores resaltan como al
correr los algoritmos en varias arquitecturas de Hardware ( un VAX 8550, un MIPS y
un Cray) se obtuvieron resultados diferentes, mostrando claramente la dependencia

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).

Con estas consideraciones en mente se presenta la implementación del programa


3.1 de [MBM93] en Python:

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]

4. Implementación de todos los Sorting Algorithms Vis-


tos
Implemente en Python todos los algoritmos de ordenamiento que se han discutido en clase.
Haga una evaluación experimental de sus tiempos de ejecución (incluyendo el algoritmo del
punto 2), mostrando con gráficas las diferencias entre ellos. Discuta los resultados obtenidos.
Los algoritmos de ordenamiento que implementamos fueron Q UICK S ORT, R ANDOMIZED -
Q UICK S ORT, M ERGE S ORT, 3PARTITIONS -M ERGE S ORT, C OUNTING S ORT, R ADIX S ORT
y H EAPSORT. Todos los algoritmos fueron implementados en Python 2.6.5 y probados
con ayuda el módulo timeit6 . Durante la evaluación de los algoritmos medimos los
tiempos de respuesta de cada algoritmo con respecto al tamaño de entrada para final-
mente graficar el rendimiento de cada algoritmo con Gnuplot7 . La evaluación de los
6
La motivación a utilizar este módulo viene de http : //diveintopython.org/performancet uning/timeit.html
7
Esto fue posible gracias al módulo Gnuplot.py (http://gnuplot-py.sourceforge.net/)

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)

Comparaciones de tiempo de ejecucion de QuickSort y Randomized QuickSort


2
Quick Sort
Randomized Quick Sort
1.8

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

Figura 3: QS vs RQS - Large Inputs

En la Figura 3 mostramos una gráfica que compara el rendimiento de Q UICK -


S ORT y su versión aleatoria, con inputs relativamente grandes (10000-100000):
Nótese que la diferencia entre uno y otro es casi nula, debido a que las entradas
utilizadas son generadas aleatoriamente. En el caso en que las entradas fuesen
diseñadas especialmente para los mejores y peores casos, las diferencias serían
notables seguramente a favor del Quick Sort determinista que tiene un “mejor
caso“ predecible.
Desde un punto de vista práctico es evidente que si no se tiene información sobre
las entradas, el uso de un algoritmo u otro es indiferente.

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

4. 3PARTITIONS -M ERGE S ORT: Como se vio en el punto 2. utilizamos la misma idea


de llamar recursivamente a M ERGE S ORT, pero esta vez con 3 particiones en vez
de 2, haciendo las comparaciones necesarias en el procedimiento M ERGE, que se
derivan del arbol de decision ya comentado anteriormente:

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

En la Figura 4 mostramos una grafica que compara el rendimiento de M ERGE -


S ORT y su versión con 3 particiones, con inputs de tamaño (10000-100000): Aqui
podemos observar como el comportamiento de la version con 3 particiones es
ligeramente mejor que la normal, sin embargo, como se podria suponer, esto no
es gratis, ya que pasa una situación similar a la del algoritmo C OUNTING S ORT,
el cual en todas las comparaciones que hicimos salia como el mas rápido, pero
lo que no se muestra en las gráficas es la complejidad espacial, la cual en el caso

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

Figura 4: MergeSort vs Merge3Sort

de 3PARTITIONS -M ERGE S ORT es un poco mas grande que la de su versión con


2 particiones pero gracias a esto le permite ser un poco mas rápido y en el caso
de C OUNTING S ORT que se discutirá a continuación se tiene que tiene una com-
plejidad espacial de O(d) donde d es el número mas grande en a lista a ordenar
.

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[:]

Como se puede observar en la gráfica el desempeño de C OUNTING S ORT es su-


perior al de R ADIX S ORT, esto es de esperar ya que aunque los 2 algoritmos no se
rigen por el lower bound de los algoritmos que utilizan comparaciones (O(n lg n)),
Radix se demora un poco mas, ya que tiene que hacer mas iteraciones de ciclo
que las que hace Counting, y como hemos podido experimentar las operaciones
mas elementales en python tienen un costo eleveado, en comparación con otros
lenguajes como C.

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

Figura 5: RadixSort vs CountingSort

7. H EAP S ORT:

def Parent(i): #Padre del nodo


return i/2
def Left(i): #Hijo izquierdo del nodo
return 2*i
def Right(i): #Hijo derecho del nodo
return 2*i+1

def Max_Heapify(A, i, n):


"""Mantiene la propiedad del Heap"""
l = Left(i)
r = Right(i)
if l <= n and A[l] > A[i]: largest = l
else: largest = i
if r <= n and A[r] > A[largest]:
largest = r
if largest != i:
A[i], A[largest] = A[largest], A[i]

29
Max_Heapify(A, largest, n) #Mantenemos el Heap

def BuildHeap(A): # Construimos el Heap


n = len(A)-1
for i in range(n/2,-1,-1):
Max_Heapify(A,i,n)

def HeapSort(A): #Construye el Heap y saca las raices


BuildHeap(A) #Construimos el Heap
HeapSize=len(A)-1
for i in range(HeapSize,0,-1): #Llenamos el arreglo ordenado
#Ponemos la raiz del Heap al final del array
A[0],A[i]=A[i],A[0]
HeapSize=HeapSize-1 # El nuevo tamaño del heap
Max_Heapify(A,0,HeapSize)#Mantenemos la propiedad del Hea

Veamos a continuación una comparación de HEAPSORT con un algoritmo de su


mismo orden, MERGESORT, y en especial el 3MERGESORT que tiene constantes
menores a las de MERGESORT:

Comparaciones de tiempos de ejecucion de MERGE3 SORT y HEAPSORT


3
Merge 3 Sort
Heap Sort

2.5
Tiempo de ejecucion (segundos)

1.5

0.5

0
10000 20000 30000 40000 50000 60000 70000 80000 90000 100000
Tamano del array

Figura 6: Heapsort vs MergeSort

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

Figura 7: Todos los algoritmos comparados

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

8. I NSERTION S ORT: El código es prácticamente el mismo que se presento en el taller


1:
def insertionsort(A):
for j in range(1,len(A)):
key=A[j]
i=j-1
while (i>=0 and A[i]>key):
A[i+1]=A[i]
i=i-1
A[i+1]=key

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

Figura 8: InsertionSort vs otros algoritmos de ordenamiento en entradas medianas

Veamos la comparación de InsertionSort con todos los algoritmos antes descritos:

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

Figura 9: InsertionSort vs otros algoritmos de ordenamiento en entradas pequeñas

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

Aunque el algoritmo que se usa es bastante bueno el resultado no es muy alentador,


así que esperando una considerable mejoría se implementa el programa en C. Con esto
se evita la traducción de letras a ASCII y se mejoran los tiempos de estructuras como
el stack, además de que se aprovechan las ventajas de rendimiento que provee C al ser
un lenguaje de compilado no interpretado.

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;
}

Aunque la mejoría es notable8 de un algoritmo a otro, el juez no da muchas esper-


anzas sobre el algoritmo con un puntaje de 45.45.
Con la colaboración de otro compañero tenemos la posibilidad de revisar los tiem-
pos de ejecución de algoritmos que usando las facilidades de Python para comparar
Strings ordenan los prefijos con QuickSort o incluso con HeapSort. Y aunque estos úl-
timos algoritmos en pruebas locales se comportan mejor que el algoritmo de TwoAr-
ray9 , para el juez la puntuación que merecen es de 27.27 todos.
Con las opciones más simples descartadas se procede a la búsqueda del estado del arte
en el tema, pues al parecer las mejoras en tiempo no implican mejoras en puntaje.
Una buena descripción del estado del arte en algoritmos de construcción de arreglos
de sufijos puede encontrarse en el artículo A taxonomy of suffix array construction
algorithms [PST07]. En este artículo se encuentran entre otros el algoritmo de Ko and
Aluru [KA05] que describe un algoritmo de orden lineal para la construcción de un
suffix array, sin embargo no se implementa el algoritmo pues este no es un aspecto
que entre dentro del alcance del taller10 .
8
La prueba de los algoritmos se hizo localmente con una cadena aleatoria de 10000 caracteres
9
Esto seguramente se debe a que la comparación de strings en Python es económica con respec-
to a la traducción a ASCII y el direccionamiento en listas. Python utiliza directamente la función
memcmp para comparar cadenas, no realiza traducciones ni pilas ni baldes, ni peculiaridades de
Python, simplemente se realiza la comparación byte a byte proporcionada por C. Fuente: http :
//svn.python.org/view/python/trunk/Objects/stringobject.c?view = markup
10
Durante la realización del taller se intentó realizar una implementación del algoritmo pero fue im-
posible conseguir el resultado final para la fecha de entrega

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])

t=timeit.Timer("a=[random.randint(0,"+str(x)+") for i in xrange("+


str(x)+")];sorts.mergesort(a,0,"+str((x-1))+")","import random,sorts")
MS.append([x,t.timeit(n)/n])

t=timeit.Timer("a=[random.randint(0,"+str(x)+") for i in xrange("+


str(x)+")];sorts.mergesort3(a,0,"+str((x-1))+")","import random,sorts")
MS3.append([x,t.timeit(n)/n])

t=timeit.Timer("a=[random.randint(0,"+str(x)+") for i in xrange("+


str(x)+")];sorts.countingsort(a,"+str(x)+")","import random,sorts")
CS.append([x,t.timeit(n)/n])

t=timeit.Timer("a=[random.randint(0,"+str(x)+") for i in xrange("+


str(x)+")];sorts.quicksort(a,0,"+str((x-1))+")","import random,sorts")
QS.append([x,t.timeit(n)/n])

t=timeit.Timer("a=[random.randint(0,"+str(x)+") for i in xrange("+


str(x)+")];sorts.quicksort_r(a,0,"+str((x-1))+")","import random,sorts")

37
RQS.append([x,t.timeit(n)/n])

t=timeit.Timer("a=[random.randint(0,"+str(x)+") for i in xrange("+


str(x)+")];sorts.heapSort(a)","import random,sorts")
HS.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"))

g("set output ’QUICK.eps’")


g( ’ set style data lp ’ )
g.title("Comparaciones de tiempo de ejecucion de QuickSort y Randomized
QuickSort")
g.xlabel("Tamano del array")
g.ylabel("Tiempo de ejcucion (segundos)")
g.plot(Gnuplot.Data(QS,title="Quick Sort"),Gnuplot.Data(RQS,title="
Randomized Quick Sort"))

g("set output ’MERGE.eps’")


g( ’ set style data lp ’ )
g.title("Comparaciones de tiempo de ejecucion de MergeSort y MergeSort
con 3 particiones")
g.xlabel("Tamano del array")
g.ylabel("Tiempo de ejcucion (segundos)")
g.plot(Gnuplot.Data(MS3,title="3Merge Sort"),Gnuplot.Data(MS,title="
Merge Sort"))

g("set output ’RADIXCOUNTING.eps’")


g( ’ set style data lp ’ )
g.title("Comparaciones de tiempo de ejecucion de RadixSort y CountingSort")
g.xlabel("Tamano del array")
g.ylabel("Tiempo de ejcucion (segundos)")
g.plot(Gnuplot.Data(RS,title="Radix Sort"),Gnuplot.Data(CS,title="
Counting Sort"))

g("set output ’HEAPMERGE.eps’")

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"))

g("set output ’TODOS2.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,title="Merge3 Sort"),Gnuplot.Data(MS,title="Merge Sort
"),Gnuplot.Data(CS,title="Counting 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 )

Figura 10: Árbol de recursion Punto 4.2-3


cn ⇒ cn

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

Figura 11: (Continuación) Árbol de recursión Punto 4.2-3


cn = O0 (n)
+
cα + c(1 − α)n = O1 (n)
+

cα2 n + cα(1 − α)n + cα(1 − α)n + c(1 − α)2 n = O2 (n)


+
..
.
..
.
..
.
..
.
..
.
..
.
..
.
..
. ··· ..
.
+

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)

Figura 12: Árbol de recursión Punto 4.2-5


Referencias
[CLRS01] T. H. Cormen, C. E. Leiserson, R. L. Rivest, and C. Stein. Introduction to
Algorithms. MIT Press, Cambridge, MA, 2001.

[Duq08] Raúl Gonzalez Duque. Python para todos. 2008.

[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.

[PST07] Simon J. Puglisi, W. F. Smyth, and Andrew H. Turpin. A taxonomy of suffix


array construction algorithms. ACM Comput. Surv., 39(2):4, 2007.

43

S-ar putea să vă placă și