Академический Документы
Профессиональный Документы
Культура Документы
1. Introducción
En este cápitulo veremos los algoritmos de ordenamiento más conocidos.
Partiremos planteando el modelo sobre el cuál trabajaremos y las restriccio-
nes que éste impone. Luego veremos algoritmos de ordenamiento propiamente
tales, partiendo por los métodos más básicos, normalmente de O(n2 ), has-
ta llegar a métodos que tengan una complejidad temporal de O(n log2 (n)).
Finalmente, como cierre del cápitulo veremos algunos métodos de ordena-
miento que no caen dentro del esquema común y por lo tanto no tienen las
mismas limitaciones.
2. Definiciones y el modelo
Para comenzar a trabajar en este cápitulo definiremos formalmente el
problema de ordenamiento. Éste corresponde a tomar un conjunto de llaves
X de tamaño n y entregar una n-tupla que contiene los elementos de X or-
denados según algún criterio. Asumiremos la existencia de una relación de
orden total sobre el conjunto X denotada por 0 <0 (su opuesto será 0 >0 ) y
que las llaves no se repiten. Simétricamente se puede definir el problema de
ordenar un conjunto X descendentemente.
¿Es x < y?
1
¿Es x > y?
Frente a esto, notamos que sólo podemos hacer preguntas binarias, es de-
cir, con respuestas SI o NO (bajo este modelo). Esto induce una cota inferior
para el problema de ordenamiento. Notemos que la n-tupla que esperamos
obtener tras ordenar es una permutación del conjunto. Tenemos en total n!
permutaciones distintas y mediante preguntas binarias queremos determinar
cuál de todas las permutaciones corresponde a aquella en la que el conjunto
está ordenado. Es claro que si representáramos la información sobre cuál per-
mutación es la buscada como una secuencia de bits, podrı́amos determinar
máximo un bit de esta secuencia con cada pregunta binaria. De aquı́ nace
la cota, pues en el mejor caso cada pregunta aporta un bit y la cantidad de
bits necesarios para distinguir una permutación de las n! existentes corres-
ponde a log2 (n!), que aproximado usando Stirling corresponde a O(n log2 (n)).
Ésta es una limitación que estará presente durante la mayor parte del
capı́tulo. No existe un algoritmo que mediante comparaciones bi-
narias de llaves pueda ordenar en menos de O(n log2 (n)) compara-
ciones. Al final de este cápitulo veremos algunos modelos que rompen este
esquema y permiten alcanzar mejores tiempos.
2
Seleccion (D[1 . . . n])
1. For i ← 1 to n Do
2. min ← i
3. For j ← i + 1 to n Do
4. If D[min] > D[j] Then
5. min ← j
6. aux ← D[min]
7. D[min] ← D[i]
8. D[i] ← aux
T (n) = T (n − 1) + n − 1
3
T (n) − T (n − 1) = n − 1
n
X n
X
T (i) − T (i − 1) = i−1
i=1 i=1
n(n + 1)
T (n) − T (0) = −n
2
n(n − 1)
T (n) = + T (0)
2
El único término faltante es T (0), que corresponde a la cantidad de com-
paraciones entre elementos que hacemos para ordenar un conjunto de 0 ele-
mentos. Esto corresponde a 0 comparaciones de llaves, por lo que finalmente
sabemos que la cantidad de comparaciones T (n) corresponde a n(n−1) 2
.
n
X n
X
C(i) − C(i − 1) = 1/i
i=1 i=1
C(n) = Hn
4
Luego C(n) = O(log n), si contamos las n iteraciones tenemos O(n log n)
intercambios en total.
3.2. Inserción
Este algoritmo trabaja bajo un enfoque similar al anterior, vale decir,
nuevamente mantendremos el comienzo del arreglo ordenado. La diferencia
es que no impondremos que ese sea el orden definitivo de la primera porción.
Es decir, tenemos las primeras k posiciones del arreglo ordenadas, pero cada
elemento que revisemos fuera de esas posiciones puede corresponder a un
elemento que en el arreglo ordenado irı́a en una de las primeras k posicio-
nes. Para aumentar el tamaño de la porción ordenada llevaremos ese nuevo
elemento a la posición que les corresponde, pero sólo tomando en cuenta los
primeros k elementos. El algoritmo se muestra en la figura 3. Por ejemplo,
para el arreglo [2, 4, 3, 8, 7, 6], el arreglo se ordenarı́a de la siguiente forma
(siguiendo el algoritmo de la Figura 3):
i = 2, j =2 2 4 3 8 7 6
i = 3, j =3 2 3 4 8 7 6
i = 4, j =4 2 3 4 8 7 6
i = 5, j =5 2 3 4 7 8 6
i = 6, j =6 2 3 4 7 6 8
i = 6, j =5 2 3 4 6 7 8
En otras palabras, lo que hacemos es tener una parte del arreglo ordenado
(las primeras i posiciones), cada vez que aumentamos i debemos mantener
el orden. El único elemento que puede violar nuestra regla de mantener los
primeros i ordenados es el i-ésimo elemento. Lo que hacemos es tomar ese
elemento y buscar donde insertarlo para que los elementos hasta su posición
queden ordenados. Esto es sin importar lo que se encuentra hacia la derecha
en el arreglo.
5
Inserción (D[1 . . . n])
1. For i ← 2 to n Do
2. j←i
3. While j > 1 And D[j] < D[j − 1] Do
4. aux ← D[j]
5. D[j] ← D[j − 1]
6. D[j − 1] ← aux
7. j ←j−1
T (n) = T (n − 1) + n − 1
6
los elementos se encuentran uniformemente distribuidos a lo largo del arreglo.
La ecuación que describe el costo esperado del algoritmo es:
1
T (n) = T (n − 1) + (n − 1)
2
Ya que puedo caer en cualquier posición dentro de los primeros k, por lo
que en promedio caeré en la mitad.
1
T (n) = T (n − 1) + (n − 1)
2
1
T (n) − T (n − 1) = (n − 1)
2
De esta ecuación es fácil ver que el resultado será como en la sección
anterior, T (n) = n(n−1)
4
. Lo importante es que en este caso, nuestro análisis
de caso promedio sı́ nos da la cantidad promedio de veces que el algoritmo
itera y ésta es diferente al peor caso.
3.3. Burbuja
El ordenamiento por burbuja es bastante iteresante (sı́, iteresante), la
idea es recorrer el arreglo de izquierda a derecha intercambiando el elemento
de la posición actual con la siguiente si es que el siguiente es menor. El
algoritmo se muestra en la figura 5. Por ejemplo, para el arreglo [2, 4, 3, 8, 7, 6],
tendrı́amos el siguiente resultado tras la primera pasada, j indica la posición
de la burbuja:
j =1 2 4 3 8 7 6
j =2 2 3 4 8 7 6
j =3 2 3 4 8 7 6
j =4 2 3 4 7 8 6
j =5 2 3 4 7 6 8
7
Burbuja (D[1 . . . n])
1. For i ← 1 to n Do
2. For j ← 1 to n − i Do
3. If D[j] > D[j + 1] Then
4. aux ← D[j]
5. D[j] ← D[j + 1]
6. D[j − 1] ← aux
8
3.4. Resultados experimentales
A continuación se muestran los resultados experimentales obtenidos a
partir de la implementación de los tres algoritmos anteriores. Para cada uno
se midió el tiempo de ejecución y las comparaciones de llaves. La Figura 8
muestra los tiempos de ejecución y la Figura 7 muestra la cantida de com-
paraciones de llaves.
9
Figura 8: Tiempo de ejecución de algoritmos básicos de ordenamiento
10
4. Algoritmos avanzados de ordenamiento
En esta sección mostraremos algoritmos avanzados de ordenamiento. Por
algoritmo avanzado entenderemos un algoritmo de ordenamiento cuyo caso
promedio es O(n log2 (n)), vale decir, óptimo bajo un modelo de comparacio-
nes binarias. Veremos principalmente dos casos, MergeSort y QuickSort. Otro
algoritmo interesante es HeapSort pero lo veremos en un próximo cápitulo
junto con la descripción de un Heap binario.
4.1. MergeSort
MergeSort es un algoritmo de ordenamiento óptimo, pues toma O(n log2 (n))
en el peor caso y el caso promedio. La principal gracia de este algoritmo se
encuentra en el método merge, el cuál toma dos arreglos ordenados y los mez-
cla en un solo arreglo ordenado. Por ejemplo para los dos arreglos [2, 4, 6, 9] y
[3, 7, 8, 10], merge retornarı́a el siguiente arreglo [2, 3, 4, 6, 7, 8, 9, 10]. La idea
general es utilizar divide y venceras, separando el arreglo en 2 mitades, or-
denando cada una de ellas y luego llevando a cabo un merge sobre los dos
trozos ordenados. Cuando llamamos a ordenar una mitad, lo hacemos con el
mismo procedimiento de forma recursiva, es decir, volvemos a dividir, hace-
mos merge y ası́ sucesivamente. El caso base lo encontramos cuando tenemos
que ordenar un arreglo de tamaño 0 ó 1, que ya está ordenado. La Figura 9
muestra el algoritmo MergeSort y la figura 10 muestra el método merge. La
Figura 11 muestra graficamente la lógica del algoritmo.
Figura 9: MergeSort
11
Merge (lef t[1..nl ], right[1..nr ])
1. ret ← array[1..nl + nr ]
2. i ← 1, j ← 1
3. While i ≤ nl And j ≤ nr Do
4. If lef t[i] < right[j] Then
5. ret[i + j − 1] ← lef t[i]
6. i←i+1
7. Else
8. ret[i + j − 1] ← right[j]
9. j ←j+1
10. While i ≤ nl Do
11. ret[i + j − 1] ← lef t[i]
12. i←i+1
13. While j ≤ nr Do
14. ret[i + j − 1] ← right[j]
15. j ←j+1
16. return ret
n
T (n) = 2T +n
2
Pues para ordenar un arreglo de tamaño n ordenamos dos arreglos de
tamaño n/2 y luego hacemos merge de las dos mitades, lo que cuesta n. La
12
solución de esta ecuación se puede obtener asumiendo n de la forma 2k con
k ≥ 0 y usando la función auxiliar G(k) = T (2k ), con esto, la recurrencia
puede reescribirse de la siguiente forma:
G(k) = 2G(k − 1) + 2k
T (n) = k2k
T (n) = n log2 (n)
4.2. QuickSort
QuickSort es un algoritmo que en promedio se comporta de manera ópti-
ma, pero en su peor caso es O(n2 ). Al igual que MergeSort, QuickSort va
dividiendo el problema en dos mitades cada vez. La diferencia principal es
que la manera en que QuickSort divide el problema. Para lograr esto, lo que
se hace es pivotear. Explicaremos primero el método pivotear y luego, ha-
biendo entendido éste, será más simple entender QuickSort.
13
los elementos mayores que el pivote, denominado max y otro al final de los
elementos menores o iguales al pivote, denominado min. Supondremos que el
pivote está al principio del arreglo y que iniciamos con min = 2, max = n.
Siempre verificaremos el elemento en la posición min. Si éste es menor o
igual al pivote, aumentaremos min, si no, intercambiaremos los elementos
en las posiciones min y max y decrementaremos max. Al finalizar esto, es
decir, cuando min y max se inviertan (min > max) bastará intercambiar los
elementos en las posiciones max y 1 para obtener un pivoteo completo.
Pivotear (D[1..n], p)
1. aux ← D[p]
2. D[p] ← D[1]
3. D[1] ← aux
4. min ← 2
5. max ← n
6. While min ≤ max Do
7. If D[min] ≤ D[1] Then
8. min ← min + 1
9. Else
10. aux ← D[min]
11. D[min] ← D[max]
12. D[max] ← aux
13. max ← max + 1
14. aux ← D[max]
15. D[max] ← D[1]
16. D[1] ← aux
17. return max
14
ver que el método pivotear toma O(n). La Figura 14 muestra graficamente
la idea general de QuickSort.
Para el peor caso debemos notar que si QuickSort siempre elige como
pivote el mayor elemento del arreglo 2 , la mitad derecha estará de por sı́ or-
denada. La mitad izquierda por otro lado será de tamaño n − 1, por lo que
si escribimos una ecuación de recurrencia para T (n) obtendremos T (n) =
T (n − 1) + n − 1, ecuación que ya hemos estudiado anteriormente y sabemos
que cumple T (n) = n(n − 1)/2.
15
en cualquier posición del arreglo es uniforme, es decir, n1 . La recurrencia para
el caso promedio queda entonces como:
n
1X
T (n) = (T (i − 1) + T (n − i)) + n − 1
n i=1
2 n−1
X
T (n) = T (i) + n − 1
n i=0
n
X n−1
X
(n + 1)T (n + 1) − nT (n) = 2 T (i) − 2 T (i) + 2n
i=0 i=0
(n + 1)T (n + 1) − (n + 2)T (n) = 2n
T (n + 1) T (n) 2n
− =
n+2 n+1 (n + 1)(n + 2)
n−1 n−1
!
X T (i + 1) T (i) X 2i
− =
i=0 i+2 i+1 i=0 (i + 1)(i + 2)
n−1 n−1
T (n) 2 X X 1
− T (0) = 2 −2
n+1 i=0 i + 2 i=0 i + 1
T (n)
= T (0) + 4Hn+1 − 4 − 2Hn
n+1
2
T (n) = (n + 1)(T (0) + 2Hn+1 + − 4)
n+1
T (n) = 2(n + 1)Hn+1 − 4(n + 1) + 2 + T (0)(n + 1)
T (n) = 2(n + 1)Hn+1 − 4n − 2
T (n) = 2(n + 1)Hn − 4n
16
Una forma de hacer menos probable el peor caso en QuickSort es modificar
la forma de elección del pivote. Lo que se utiliza normalmente es tomar la
mediana de k elementos, aumentando ası́ la probabilidad de que el pivote
se acerque a la mitad del arreglo. Es importante notar que QuickSort es un
algoritmo aleatorizado, esto influye en que el peor caso puede suceder pero
un adversario no puede obtener el peor caso de forma sistemática.
17
n QuickSort QuickSort med3 Mergesort
10000 156211,58 147534,34 120458,08
20000 339474,34 318846,82 260905,65
30000 533503,62 498816,58 408597,33
40000 732948,99 683826,77 561806,96
50000 942936,56 873323,04 718195,10
60000 1150326,34 1070173,77 877194,59
18
n QuickSort QuickSort med3 Mergesort
10000 0,0012 0,0012 0,0034
20000 0,0028 0,0027 0,0070
30000 0,0043 0,0042 0,0107
40000 0,0058 0,0058 0,0148
50000 0,0075 0,0073 0,0188
60000 0,0094 0,0092 0,0222
5. Otros modelos
Es importante recalcar que no siempre estaremos afectos a la restricción
de que un algoritmo de ordenamiento no puede realizar menos de O(n log2 n)
comparaciones en promedio. Por ejemplo si tuviesemos n elementos ente-
ros, todos en un rango [1, k] podrı́amos ordenar el conjunto manteniendo un
arreglo de k posiciones y contando cuantas veces aparece cada elemento. El
algoritmo, conocido como CountingSort no realiza comparaciones binarias
entre llaves del conjunto, sino que hace uso de la finitud del universo en el
que estamos trabajando. A continuación mostraremos este algoritmo y luego
veremos como se comporta en teorı́a. La Figura 17 muestra el algoritmo.
19
CountingSort (A1...n , B1...n , k)
1. For i ← 1 to k Do
2. C[i] ← 0
3. For i ← 1 to n Do
4. C[A[i]] ← C[A[i]] + 1
5. For i ← 2 to k Do
6. C[i] ← C[i] + C[i − 1]
7. For i ← n downto 1 Do
8. B[C[A[i]]] ← A[i]
9. C[A[i]] ← C[A[i]] − 1
Además de éste, existen otros algoritmos que hacen uso de finitud y al-
canzan complejidades temporales mejores que las alcanzables por métodos
basados en comparaciones binarias de llaves.
6. Resumen
Durante este cápitulo vimos que todo algoritmo de ordenamiento basa-
do en comparaciones binarias de llaves está sujeto a una restricción, ésta
es que no podrá realizar menos de O(n log2 n) comparaciones binarias entre
llaves en promedio. Dentro de estos algoritmos estudiamos primero los algo-
ritmos básicos: selección, inserción y burbuja. Luego estudiamos algoritmos
avanzados, que corresponde a aquellos que en promedio alcanzan el óptimo
O(n log2 n) comparaciones binarias.
20
7. Problemas
Problema 1
Ordene la siguiente lista utilizando los métodos 5 métodos vistos en este
capı́tulo.
Problema 2
Un servidor de datos nos permite actualizar la lista de clientes de la
empresa de la siguiente forma:
Problema 3
Considere que usted recibe una lista que incluye números y palabras. Se
le solicita que escriba un algoritmo que ordene e imprima esta lista de modo
tal que las palabras estÃ
n
c en orden alfabético creciente y los números en
orden creciente. Además, si el n-ésimo elemento en la lista de entrada es un
número, tiene que permanecer siendo un número, y si es una palabra, tiene
que seguir siendo una palabra.
21
Problema 4
Los profesores Moe, Larry y Curly, necesitados de recuperar la admiración
de sus alumnos de Algoritmos, han diseñado un nuevo método de ordena-
miento para impresionarlos. Este se invoca como StoogeSort (A,1,n):
StoogeSort(A[], i, j)
{
if (A[i] > A[j])
swap (A[i], A[j]);
if (i + 1 >= j) return;
k = floor((j - i + 1)/3);
StoogeSort(A, i, j - k); /* Primeros dos tercios */
StoogeSort(A, i + k, j); /* Ultimos dos tercios */
StoogeSort(A, i, j - k); /* Otra vez los primeros dos tercios */
}
Problema 5
Debido al buen desempeño de inserción para conjuntos de datos pequeños,
se propone el siguiente hı́brido entre QuickSort y el método de ordenamiento
por inserción:
function QSH(A[], i, j)
{
if(j-i<=M)
insercion(A,i,j)
else
p = pivotear(A, random(i..j))
QSH(A,i,p-1)
22
QSH(A,p+1,j)
}
23