Cap´ıtulo VI Algoritmos: Ordenamiento

Anuncio
Capı́tulo VI
Algoritmos: Ordenamiento
VI.1.
Algoritmos
Un algoritmo es una secuencia de instrucciones precisas para llevar a cabo una
tarea. Ya hemos visto algunos ejemplos: un algoritmo para resolver el problema
de la torre de Hanoi, y algunos algoritmos numéricos (multiplicación, división).
En este capı́tulo usamos el problema de ordenamiento como ejemplo para exponer
algunas temas relevantes: métodos generales, análisis del número de operaciones
que realizan, etc. Vamos a usar principalmente pseudocódigo para describir los
algoritmos de manera relativamente precisa. Esto se precisa un poco en la primera
sección. Además del pseudocódigo también se proveen los algoritmos en el lenguaje
python, el cual puede ser fácilmente descargado de la red (www.python.org).
VI.1.1.
Pseudocódigo
Instrucciones
El pseudocódigo consiste de instrucciones que se ejecutan en la secuencia que
aparecen. La instrucción más básica es una asignación: se asigna un valor a una
variable. Usamos una flecha para indicar una asignación (en lugar del signo igual,
el cual se reserva para comparaciones):
variable ← valor
Es esencial tener instrucciones especiales que afectan la secuencia de ejecución del
algoritmo:
si: si la condición se satisface se realiza el Bloque-A de instrucciones, y de lo
contrario se realza el Bloque-B. Cada uno de los bloques aparece indentado:
si hcondicioni
Bloque-A
si-no
Bloque-B
1
2
CAPÍTULO VI. ALGORITMOS: ORDENAMIENTO
Es posible tener más de dos casos:
si hcondicion-1i
Bloque-A
si-no si hcondicion-2i
Bloque-B
si-no
Bloque-C
para: se ejecuta el bloque de instrucciones que aparece a continuación indentado para cada valor de la variable en el rango (en orden):
para variable en rango
Bloque
mientras: mientras la condición es verdadera, se realiza el bloque de instrucciones que aparece a continuación indentado:
mientras hcondicioni
Bloque
Comentarios se escriben después del sı́mbolo B.
Variables
Además de variables que representan números u otros tipos de datos básicos
(carácteres, valores lógicos V ó F, etc), se tienen arreglos que son variables indexadas en un rango que tomamos desde 0 hasta su longitud menos 1. Por ejemplo
A[5] es la sexta posición del arreglo A. Un rango se especifica como i : j donde por
conveniencia adoptamos la convención en python de considerar rangos cerrados
por la izquierda y abiertos por la derecha. Es decir, “i : j” corresponde al rango
i, i + 1, i + 2, . . . , j − 1. Además “: j” y “i :” son rangos que van desde el comienzo
en el primer caso, y hasta el final en el segundo caso. Por ejemplo, A[4 : 8] es el
subarreglo que consiste de A[4], A[5], A[6], A[7]; A[: 3] es el subarreglo que consiste
de A[0], A[1], A[2]. En algunos casos es más conveniente manejar listas las cuales
permiten otras operaciones como eliminar el primer elemento (pop en python),
agregar un elemento al final (append en python), y concatenar dos listas (lo cual
denotamos L + L 0 como se hace en pyhton). Notamos que el tipo de variable lista
en python permite tanto las operaciones de un arreglo como las de una lista, y es
esta la que usamos en todos los ejemplos de código python.
VI.1. ALGORITMOS
VI.1.2.
3
Python
Para la mayorı́a de los algoritmos se incluye además de pseudocódigo también
una implementación en python, la cual en general es muy similar (pero con las
palabras claves de python). Aquı́ sólo mencionamos que lı́neas que comienzan con
el carácter # son comentarios. El interpretador de pyhton se puede descargar de
www.pyhton.org.
VI.1.3.
Ejemplo: Torres de Hanoi
Ya se ha discutido el problema y algoritmo anteriormente. Ahora lo escribimos
en pseudocódigo, y en código python como ejemplo.
Pseudocódigo
Hanoi
1. si
2.
3.
4.
(n, A, C, B)
n 6= 0
Hanoi (n − 1, A, B, C)
Mueva el anillo (de encima) de A a C
Hanoi (n − 1, B, C, A)
Código Python
#
# torres de Hanoi: Mueve n anillos del polo A
#
al polo C con polo B como intermedio
#
def Hanoi(n, A, C, B):
if n != 0:
Hanoi(n - 1, A, B, C)
print("Mueva el anillo de", A, "a", C)
Hanoi(n - 1, B, C, A)
#
4
CAPÍTULO VI. ALGORITMOS: ORDENAMIENTO
Ejemplo Ejecución (python)
>>> Hanoi(4,’A’,’C’,’B’)
Mueva el anillo de A a B
Mueva el anillo de A a C
Mueva el anillo de B a C
Mueva el anillo de A a B
Mueva el anillo de C a A
Mueva el anillo de C a B
Mueva el anillo de A a B
Mueva el anillo de A a C
Mueva el anillo de B a C
Mueva el anillo de B a A
Mueva el anillo de C a A
Mueva el anillo de B a C
Mueva el anillo de A a B
Mueva el anillo de A a C
Mueva el anillo de B a C
VI.2.
Búsqueda
Una motivación para el problema de ordenamiento, es que una lista ordenada
facilita la búsqueda de un ı́tem en la lista. Si la lista no está ordenada, no hay
mejor alternativa que simplementa escanear la lista secuencialmente, es decir, en
el orden que los ı́tems aparecen en ella (que no corresponde al orden de los ı́tems).
El siguiente pseudocódigo busca el ı́tem K en la lista L:
Buscar1 (K, L)
1. para i ← 0 a longitud(L) − 1
2.
si K = L[i]
3.
devolver (i)
4. devolver (−1)
Esta búsqueda secuencial requiere un número de comparaciones (lı́nea 2) igual
al tamaño de la lista en el peor de los casos. Esto sucede cuando el ı́tem que se
busca es el último ó no está en la lista (entonces se regresa −1 en la lı́nea 4). Si n
es la longitud de la lista, entonces el número de comparaciones es n en el peor de
los casos. Se dice que la búsqueda secuencial tiene “complejidad” O(n) (se lée O
de n, ó O grande de n, porque existe otra notación con o pequeña). (Puede leer
acerca de esta notación O en Johnsonbaugh, sec 4.3.)
VI.2.1.
Búsqueda Binaria: Versión Recursiva
Si la lista está ordenada, entonces búsqueda binaria es considerablemente más
eficiente. En la búsqueda binaria se examina el ı́tem en el centro de la lista, si
es el que se busca se devuelve el ı́ndice correspondiente, si no lo es, se continúa
la búsqueda recursivamente en la primera ó segunda mitad de la lista/arreglo,
dependiendo del resultado de la comparación. En el siguiente pseudocódigo, se
VI.2. BÚSQUEDA
5
tienen como parámetros, además de la lista L y el ı́tem a buscar K, los ı́ndices que
determinan el intervalo de búsqueda:
Buscar2 (K, A, i, j)
1. si i > j
2.
devolver (-1)
3. m ← b(i + j)/2c
4. si K = A[m]
5.
devolver (m)
6. si-no si K < A[m]
7.
devolver (Buscar2 (K, A, i, m − 1))
8. si-no
9.
devolver (Buscar2 (K, A, m + 1, j))
Invariante: El algoritmo mantiene en cada llamada recursiva la propiedad de
que si K está en la lista, entonces su celda L[`] satisface i ≤ ` ≤ j. Por esto
devuelve −1 en la lı́nea 1, si i > j (ya no es posible que K esté en L). El bloque
del si (lı́neas 4-9) se encarga de que se mantenga este invariante a la siguiente
llamada recursiva: para esto elimina la “mitad” del rango que no puede contener
K.
Código Python
# busqueda binaria recursiva:
#
busca llave K en el arreglo A entre indices i y j
#
def buscar(A,i,j,K):
if i>j:
return(-1)
m=(i+j)//2
if K==A[m]:
return(m)
elif K<A[m]:
return(buscar(A,i,m-1,K))
else:
return(buscar(A,m+1,j,K))
#
VI.2.2.
Búsqueda Binaria: Versión Iterativa
Como lo ilustra el algoritmo anterior, la recursión es una herramienta muy
poderosa. Sin embargo, recursión agrega carga computacional a la ejecución de
un algoritmo (el procesador debe tener una tabla que contiene la información
de que las ejecuciones recursivas que están pendientes de completar, y cual se
está realizando en el momento), y si se puede evitar es deseable hacerlo. En el
caso de la búsqueda binaria es muy fácil hacerlo:
6
CAPÍTULO VI. ALGORITMOS: ORDENAMIENTO
Buscar3 (K, A)
1. i ← 0; j ← longitud(A) − 1
2. mientras i ≤ j
3.
m ← b(i + j)/2c
4.
si K = A[m]
5.
devolver (m)
6.
si-no si K < A[m]
7.
j←m−1
8.
si-no
9.
i←m+1
10. devolver (−1)
B inicializa indices i, j
B calcula indice medio
B item K esta en posicion m
B busqueda en la sublista izquierda
B busqueda en la sublista derecha
B item K no esta en L
Código Python
#
# busqueda binaria no recursiva: busca llave K
#
en el arreglo A entre indices i y j
#
def buscar(K,A):
i=0; j=len(A)-1
while i<=j:
m=(i+j)//2
if K==A[m]:
return(m)
elif K<A[m]:
j=m-1
else:
i=m+1
return(-1)
#
VI.2.3.
Número de Comparaciones
Nos interesa analizar cuánto se demora el algoritmo de búsqueda binaria en
completar su tarea. Como indicativo de esto, vamos a determinar el número de
comparaciones que realiza (lı́nea 4 de la versión iterativa) en el peor de los casos.
Si la lista/arreglo es vacı́a, entonces el número de comparaciones es 0. Si no lo
es, después de hacer una comparación continúa buscando en una subarreglo de
longitud a lo más bn/2c (si la longitud del arreglo es impar entonces ambos subarreglos de elementos menores y mayores que el elemento medio tienen longitud
bn/2c; si la longitud del arreglo es par entonces el arreglo de elementos menores
tiene longitud bn/2c − 1 y el de elementos mayores tiene longitud bn/2c). Sea
an el número de comparaciones realizado en el peor de los casos para un arreglo
de longitud n. Continuando con el análisis, el peor caso se realiza cuando el ı́tem
que se busca es mayor que todos los elementos del arreglo (y ası́ la búsqueda es
siempre en la mitad superior). Por lo tanto, esto muestra entonces que se satisface
la recurrencia
an = abn/2c + 1
VI.2. BÚSQUEDA
7
con a1 = 1. La presencia de la función piso en la derecha dificulta un poco resolver
esta ecuación. Veamos primero el caso en que n = 2` , el cual es más fácil porque
sigue siendo entero al dividir por 2 iterativamente hasta que se llaga a 1 (y en un
paso más se llega a 0):
a2` = a2`−1 + 1
= (a2`−2 + 1) + 1
= a2`−2 + 2
..
.
= a2`−` + `
= a1 + `
= ` + 1.
Ası́ que
an = log2 n + 1
en el caso que n = 2` . Para generalizar esto a cualquier n es útil calcular varios
valores de an usando la recurrencia:
n : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
an : 1 2 2 3 3 3 3 4 4 4 4 4 4 4 4 5 5
Se observa que
an = blog2 nc + 1
Esto lo podemos verificar por inducción. El caso base es a1 = 1, y para n tenemos
(paso de inducción fuerte) dos casos (usando como hipótesis de inducción que
ak = blog kc + 1):
• si n es par: entonces n = 2k y se tiene que
an = a2k = ak +1 = blog2 kc+1+1 = blog2 k+1c+1 = blog2 (2k)c+1 = blog2 nc+1.
• si n es impar: entonces n = 2k + 1 y se tiene que
an = a2k+1 = ak +1 = blog2 kc+1+1 = blog2 k+1c+1 = blog2 (2k)c+1 = blog2 nc+1.
Para la última igualdad, note que blog2 (2k)c = blog2 (2k + 1)c porque el log
cambia al sigueinte entero en una potencia de 2, y 2k + 1 no es una potencia
de 2. Por lo tanto blog2 (2k)c = blog2 nc.
Concluı́mos que el número de comparaciones realizadas por el algoritmo de
búsqueda binaria con una arreglo de longitud n es
an = blog2 nc + 1
en el peor de los casos.
8
CAPÍTULO VI. ALGORITMOS: ORDENAMIENTO
Alternativa. Una forma alternativa de extender el análisis del caso n = 2` al
caso general, aunque ya no con igualdad es notar que an es creciente (es decir
an ≤ an+1 ), y que cualquier n está entre dos potencias de 2:
2`−1 < n ≤ 2`
En este caso dlog2 ne = `. Entonces
an ≤ a2` = ` + 1 = dlog2 ne + 1,
lo que es muy cercano al valor exacto obtenido arriba.
VI.3.
Ordenamiento
En el problema de ordenamiento se da como entrada una lista L de elementos,
números u otro tipo de dato que tiene un orden, y el objetivo es ordenarlos,
es decir, producir otra lista en la cual los mismos elementos aparecen en orden
creciente ó decreciente (pueden haber copias del mismo elemento, de tal manera
que el resultado puede no ser estrictamente creciente ó decreciente). Asumimos
el caso de salida creciente para la presentación de los algoritmos. A continuación
exploramos cuatro métodos diferentes de resolver este problema. Sea n el tamaño
(ó longitud) de la lista de entrada. Un parámetro representativo de que tanto se
demora el algoritmo en realizar el odenamiento es el número de comparaciones
que realiza entre elementos de la lista (no contamos otras posibles comparaciones
que son parte del control del algoritmo, es decir contadores de ciclos, etc). De los
algoritmos que estudiamos, los dos primeros realizan un número de comparaciones
O(n2 ) y el tercero realiza un número de comparaciones O(n log n). El último
realiza un número de comparaciones que es también O(n2 ) en el peor de los
casos, pero en promedio el número es O(n log n).
VI.3.1.
Ordenamiento por Inserción (Insertion Sort)
Este algoritmo iterativo que comienza la i-ésima iteración con los primeros i
elementos (es decir L[0], L[1], . . . , L[i − 1]) ya ordenados, y busca la posición que
debe ocupar el siguiente elemento (L[i]) y lo inserta en esa posición. En el proceso
de buscar la posición, se mueven hacia adelante los elementos para desocupar la
nueva posición de L[i].
VI.3. ORDENAMIENTO
9
Ordenar-por-Insercion (L)
1. para i ← 1 a longitud(L) − 1
2.
K ← L[i]
B L[i] es el item a insertar
B se busca la posicion donde L[i] debe ser insertado
3.
j←i−1
4.
mientras j ≥ 0 y L[j] > K
5.
L[j + 1] ← L[j]
B L[j] se mueve hacia adelante
6.
j←j−1
B L[i] es insertado en posicion
7.
L[j + 1] ← K
Ejemplo: A continuación se muestra un arreglo/lista inicial L, y esta misma al
final de cada una de las iteraciones del ciclo principal del algoritmo (para en la
lı́nea 1):
L=
1,
2,
3,
4,
5,
6,
7,
8,
9,
[9,
[7,
[3,
[3,
[2,
[2,
[1,
[0,
[0,
[0,
7,
9,
7,
5,
3,
3,
2,
1,
1,
1,
3,
3,
9,
7,
5,
4,
3,
2,
2,
2,
5,
5,
5,
9,
7,
5,
4,
3,
3,
3,
2,
2,
2,
2,
9,
7,
5,
4,
4,
4,
4,
4,
4,
4,
4,
9,
7,
5,
5,
5,
1,
1,
1,
1,
1,
1,
9,
7,
7,
6,
0,
0,
0,
0,
0,
0,
0,
9,
8,
7,
8,
8,
8,
8,
8,
8,
8,
8,
9,
8,
6]
6]
6]
6]
6]
6]
6]
6]
6]
9]
Se observa que
Invariante: Al terminar la i-ésima iteración del para (lı́nea 1), la porción del
arreglo L[0 : i + 1] está ordenada (recuerde que por convención, esto no incluye
L[i + 1]).
En la siguiente iteración, las lı́neas 3-6 determinan determinan la posición j + 1
donde K = L[i] es insertado, de tal manera que el invariante se mantiene.
Número de Comparaciones: Nos interesa contar el número de comparaciones
que el algoritmo realiza en el peor de los casos. Esto sucede, cuando la posición
en que se debe insertar L[i] es el comienzo del arreglo, y esto puede ocurrir en
cada iteración si el arreglo está inicialmente en orden decreciente. Por lo tanto el
número de comparaciones es a lo más
1
1
1
1 + 2 + 3 + 4 + · · · + (n − 1) = n(n − 1) = n2 − n.
2
2
2
Se dice que el número de comparaciones realizadas es cuadrático (ya que el término
n/2 es comparativamente menor a medida que n aumenta).
10
CAPÍTULO VI. ALGORITMOS: ORDENAMIENTO
Código Python
def ordenar_por_insercion(L):
for i in range(1,len(L)):
K = L[i]
j = i-1
while j >= 0 and L[j] > K:
L[j+1]=L[j]
j=j-1
L[j+1]=K
return(L)
VI.3.2.
Ordenamiento por Selección (Selection Sort)
Este algoritmo iterativo comienza la i-ésima iteración con los primeros i elementos (es decir L[0], L[1], . . . , L[i − 1]) ya ordenados y en su posición final (los
siguientes elementos no pueden ser menores que alguno de estos), y busca (selecciona) el menor de los restantes y lo intercambia con L[i]. (Note que inicialmente
i = 0 y por lo tanto la lista de elementos L[0], L[1], . . . , L[i − 1] es vacı́a.)
Ordenar-por-Seleccion (L)
1. para i ← 0 a longitud(L) − 2
B se determina k, indice de min en L[i :]
2.
k←i
B k es inicialmente i
3.
para j ← i a longitud(L) − 1
4.
si L[j] < L[k]
5.
k←j
B j es ahora indice de min
B min de L[i :] es colocado en L[i]
6.
intercambiar L[i] ↔ L[k]
Ejemplo: A coninuación se muestra un arreglo inicial L, y esta misma después
de cada una de las iteraciones del ciclo principal del algoritmo (para en la lı́nea
1):
L=
0,
1,
2,
3,
4,
5,
6,
7,
8,
[9,
[0,
[0,
[0,
[0,
[0,
[0,
[0,
[0,
[0,
Se observa que
7,
7,
1,
1,
1,
1,
1,
1,
1,
1,
3,
3,
3,
2,
2,
2,
2,
2,
2,
2,
5,
5,
5,
5,
3,
3,
3,
3,
3,
3,
2,
2,
2,
3,
5,
4,
4,
4,
4,
4,
4,
4,
4,
4,
4,
5,
5,
5,
5,
5,
1,
1,
7,
7,
7,
7,
7,
6,
6,
6,
0,
9,
9,
9,
9,
9,
9,
9,
7,
7,
8,
8,
8,
8,
8,
8,
8,
8,
8,
8,
6]
6]
6]
6]
6]
6]
6]
7]
9]
9]
VI.3. ORDENAMIENTO
11
Invariante: Al terminar la i-ésima iteración del para (lı́nea 1), la porción del
arreglo L[0 : i + 1] está ordenada, y los elementos restantes son mayores ó iguales
que L[i].
Las lı́neas 2 − 5 determinan un elemento mı́nimo entre los restantes (puede no ser
único) y lo intercambia con L[i] de tal manera que el invariante se preserva.
Número de Comparaciones: Con un arreglo de longitud n, en la i-ésima
iteración, la detección del mı́nimo puede requerir n − i − 1 comparaciones. Por lo
tanto el número de comparaciones es a lo más
1
1
1
(n − 1) + (n − 2) + (n − 3) + · · · + 2 + 1 = n(n − 1) = n2 − n.
2
2
2
Aquı́ también el número de comparaciones realizadas es cuadrático.
Código Python
def ordenar_por_seleccion(L):
for i in range(len(L)-1):
k=i
for j in range(i+1,len(L)):
if L[j]<L[k]:
k=j
temp=L[i]
L[i]=L[k]
L[k]=temp
return(L)
VI.3.3.
División y Conquista: Mezcla (Merge Sort)
La idea de ordenar con mezcla es dividir la lista a ordenar en dos partes (lo más
iguales posibles), ordenar cada una recursivamente, y luego mezclar ó combinar
las dos listas ordenadas (entrada) en una sola lista ordenada (salida). Mezclar
funciona de forma bastante intuitiva: iterativamente considera el primer elemento
de las dos listas, y toma el elemento menor de ellos (ó uno cualquiera de los dos si
son iguales) y lo coloca al final de la lista resultante, después de haberlo eliminado
de su lista de entrada. Al final cuando una lista de entrada llega a ser vacı́a, lo
que queda de la otra se agrega a la lista de salida. El seudocódigo a continuación,
asume la existencia de funciones para agregar al final de una lista y eliminar al
comienzo de una lista, y para concatenar dos listas (se usa + para representar
esta operación porque ese es el sı́mbolo usado en python). Note que en las lı́neas
9 y 10, una de las listas L1 ó L2 es vacı́a; sólo hay que concatenar la no vacı́a, pero
la operación con la otra no tiene efecto (y ası́ se simplifica algo el código).
12
CAPÍTULO VI. ALGORITMOS: ORDENAMIENTO
Mezclar (L1 , L2 )
1. L ← []
2. mientras L1 6= [] y L2 6= []
3.
si L1 [0] ≤ L2 [0]
4.
agregar L1 [0] a L
5.
eliminar L1 [0]
6.
si-no
7.
agregar L2 [0] a L
8.
eliminar L2 [0]
9. L ← L + L1
10. L ← L + L2
11. devolver (L)
B agrega L1 [0] al final de L
B elimina L1 [0] de L1
B
B
B
B
agrega L2 [0] al final de L
elimina L2 [0] de L2
concatena L y L1
concatena L y L2
Ordenar-por-Mezcla (L)
si longitud(L) ≤ 1
devolver (L)
si-no
m ← blongitud(L)/2c
L1 ← Ordenar-por-Mezcla(L[: m])
L2 ← Ordenar-por-Mezcla(L[m :])
L ← Mezclar(L1 , L2 )
devolver (L)
Ejemplo. El siguiente es un ejemplo de las listas L, L1 , L2 al final de cada una
de las iteraciones del mientras en Mezclar, y al final del algoritmo:
>>> mezclar([2,4,5,6,9,12],[1,1,4,7,8,10,13,14,15])
L= [1]
L1= [2, 4, 5, 6, 9, 12]
L2= [1, 4, 7, 8, 10, 13, 14, 15]
L= [1, 1]
L1= [2, 4, 5, 6, 9, 12]
L2= [4, 7, 8, 10, 13, 14, 15]
L= [1, 1, 2]
L1= [4, 5, 6, 9, 12]
L2= [4, 7, 8, 10, 13, 14, 15]
L= [1, 1, 2, 4]
L1= [5, 6, 9, 12]
L2= [4, 7, 8, 10, 13, 14, 15]
L= [1, 1, 2, 4, 4]
L1= [5, 6, 9, 12]
L2= [7, 8, 10, 13, 14, 15]
L= [1, 1, 2, 4, 4, 5]
L1= [6, 9, 12]
L2= [7, 8, 10, 13, 14, 15]
L= [1, 1, 2, 4, 4, 5, 6]
L1= [9, 12]
L2= [7, 8, 10, 13, 14, 15]
L= [1, 1, 2, 4, 4, 5, 6, 7]
L1= [9, 12]
L2= [8, 10, 13, 14, 15]
L= [1, 1, 2, 4, 4, 5, 6, 7, 8]
L1= [9, 12]
L2= [10, 13, 14, 15]
L= [1, 1, 2, 4, 4, 5, 6, 7, 8, 9]
L1= [12]
L2= [10, 13, 14, 15]
L= [1, 1, 2, 4, 4, 5, 6, 7, 8, 9, 10]
L1= [12]
L2= [13, 14, 15]
L= [1, 1, 2, 4, 4, 5, 6, 7, 8, 9, 10, 12]
L1= []
L2= [13, 14, 15]
L=[1, 1, 2, 4, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15]
Número de Comparaciones: Sea an el número de comparaciones entre elementos de la lista L realizadas por Ordenar-por-Mezcla en el peor de los
casos para listas L de longitud n. Decimos en el peor de los casos porque dependiendo de L se pueden realizar más o menos comparaciones. El algoritmo realiza
comparaciones en las ejecuciones recursivas con listas L1 y L2 , y en el algoritmo
VI.3. ORDENAMIENTO
13
Mezclar. Note que si n es la longitud de L, entonces las longitudes de L1 y
L2 son bn/2c y dn/2e. Ası́ que el número de comparaciones realizadas en estas
ejecuciones recursivas es menor ó igual a abn/2c y adn/2e . Por otra parte:
Observación. Mezclar realiza un número de comparaciones menor ó igual a
|L1 | + |L2 | − 1 si al menos una de las listas no es vacı́a, ó 0 is ambas listas son
vacı́as.
Prueba. Si inicialmente ambas listas son vacı́as, entonces Mezclar no ejecuta
el mientras y no realiza comparaciones, es decir realiza 0 comparaciones. Si inicialmente una lista es vacı́a y la otra no, entonces Mezclar tampoco ejecuta el
mientras y no realiza comparaciones; pero como |L1 | + |L2 | ≥ 1 entonces podemos
decir que realiza a lo más |L1 | + |L2 | − 1 comparaciones. Si inicialmente ninguna de
las listas es vacı́a, entonces se ejecuta el mientras, y cada vez que se realiza una
comparación en la lı́nea 2 de Mezclar (en el mientras), se elimina un elemento
de L1 ó de L2 (y se agrega a L), excepto al menos un elemento al final cuando
una lista llega a ser vacı́a) pero la la otra no lo es) y la ejecución del mientras
termina. Ası́ que el número de comparaciones realizadas es la lo más |L1 | + |L2 | − 1.
Note que es posible que se realicen menos de |L1 | + |L2 | − 1 comparaciones. Por
ejemplo, si L1 = [3, 5, 6, 8] y L2 = [9, 11, 15, 16], entonces se realizan 4 comparaciones hasta que L1 es vacı́a, luego se agrega L2 a L sin realizar más comparaciones.
Como se tiene que |L1 | + |L2 | = n, entonces el número de comparaciones realizadas por Ordenar-por-Mezcla con listas de longitud L en el peor de los
casos, lo que hemos denotado con an satisface:
an ≤ abn/2c + adn/2e + n − 1.
(∗)
(A diferencia de la recurrencia para la búsqueda binaria, donde tenı́amos igualdad,
aquı́ no escribimos igualdad porque no sabemos en general de un ejemplo que es
el peor de los casos para longitud n, y que en la recursión resulte en el peor de los
casos para los problemas de taño bn/2c y dn/2e, pero esos problemas ciertamente
no son peores que los peores.)
El caso base para esta recurrencia es a1 = 0 (note la recurrencia expresa los demás
en términos de a1 ; por ejemplo, a2 ≤ a1 + a1 + 1, a3 ≤ a1 + a2 + 2, etc.).
Al tratar de resolver esta recurrencia encontramos dos problemas. Primero, es
una desigualdad, mientras que estamos acostumbrados a resolver recurrencias en
la forma de igualdades. Segundo, la presencia de las funciones piso y techo van
complicar la solución. Manejamos estas dificultades de la siguiente manera:
Desigualdad: Resolvemos la ecuación reemplazando la desigualdad con igualdad. El resultado es una cota superior para an . Más precisamente, para el
ejemplo a mano, si reemplazamos la desigualdad con igualdad, cambiando
al mismo tiempo an por bn (porque al cambiar por igualdad ya no se trata
del mismo an ), se tiene
bn = bbn/2c + bdn/2e + n − 1,
14
CAPÍTULO VI. ALGORITMOS: ORDENAMIENTO
con b1 = 0. Si ahora resolvemos esta recurrencia y obtenemos una expresión
para bn , entonces podemos concluir que
an ≤ bn .
Por qué ? Esto se puede verificar por inducción (fuerte): Para n = 1 se tiene
a1 = b1 y por lo tanto a1 ≤ b1 , y para n < 1 se tiene
an ≤ abn/2c + adn/2e + n − 1
≤ bbn/2c + bdn/2e + n − 1
= bn
por recurrencia de an
por hipótesis de inducción, abn/2c ≤ bbn/2c
y adn/2e ≤ bdn/2e
por la recurrencia de bn .
Cuando se hace esto, normalmente por simplicidad de notación no se introduce bn sino que simplemente se asume igualdad en la recurrencia de an ,
con el conocimiento de que el resultado no es exacto sino una cota superior.
Pisos y techos: Se resuelve la ecuación primero para valores de n tal que el
piso y el techo que aparecen en ella se manejan fácilmente cuando se itera
la recurrencia. Esto es el caso por n = 2` donde ` ≥ 0 es un entero, porque
al dividir por 2 se sigue obteniendo una potencia de 2. Luego se usa que an
es creciente, es decir bn ≤ bn+1 para concluir una cota superior para el caso
general. Por qué bn ≤ bn+1 ? Esto se puede verificar por inducción: EL caso
base es b1 ≤ b2 (porque b1 = 0 y b2 = 1), y para n > 1 tenemos que
bn ≤ bbn/2c + bdn/2e + n − 1 por recurrencia de an
≤ bb(n+1)/2c + bd(n+1)/2e + (n + 1) − 1 por hipótesis de inducción
= bn+1 por recurrencia de bn
(note que bn/2c ≤ b(n + 1)/2c y dn/2e ≤ d(n + 1)/2e se ha usado en la
segunda lı́nea para usar la hipótesis de inducción).
VI.3. ORDENAMIENTO
15
Entonces resolvemos ahora (∗) con igualdad para n = 2` iterando la relación
de recurrencia (con igualdad)
a2` =
=
=
=
=
=
..
.
=
..
.
=
=
=
=
a2`−1 + a2`−1 + (2` − 1)
2a2`−1 + (2` − 1)
2(2a2`−2 + 2`−1 − 1) + (2` − 1)
22 a2`−2 + (2` − 2) + (2` − 1)
22 (2a2`−3 + (2`−2 − 1) + (2` − 2) + (2` − 1)
23 a2`−3 + (2` − 22 ) + (2` − 2) + (2` − 1)
2i a2`−i + (2` − 2i−1 ) + (2` − 2i−2 ) + · · · + (2` − 22 ) + (2` − 2) + (2` − 1)
2` a2`−` + (2` − 2`−1 ) + (2` − 2`−2 ) + · · · + (2` − 22 ) + (2` − 2) + (2` − 1)
` · 2` − (2`−1 + 2`−2 + · · · + 22 + 2 + 1)
` · 2` − (2` − 1)
(` − 1) · 2` + 1,
donde se ha usado que a1 = 0 y 2`−1 + 2`−2 + · · · + 22 + 2 + 1 = 2` − 1. En términos
de n, esto es
an = n · (log2 n − 1) + 1
si n es de la forma 2` . Veamos como extender esto a cualquier n: Si n no es de la
forma 2` entonces está entre dos potencias de 2:
2`−1 < n < 2`
(∗∗)
y puesto que an es creciente, entonces
an ≤ a2` = 2` (` − 1) + 1
Pero, teniendo en cuenta (∗∗),
2` = 2 · 2`−1 < 2n
y
` − 1 < log2 n.
Por lo tanto
an < 2n log2 n + 1,
lo cual es válido tambien cuando n es una potencia de 2. Se concluye entonces
que
an = O(n log2 n).
16
CAPÍTULO VI. ALGORITMOS: ORDENAMIENTO
Código Python
# ordenamiento:
#
division y mezcla
#
def mezclar(L1,L2):
L=[]
while L1!=[] and L2!=[]:
if L1[0]<=L2[0]:
L.append(L1[0])
L1.pop(0)
else:
L.append(L2[0])
L2.pop(0)
L=L+L1
L=L+L2
return(L)
VI.3.4.
#
def ordenar_por_mezcla(L):
if len(L)<=1:
return(L)
else:
m=int(len(L)/2)
L1=ordenar_por_mezcla(L[:m])
L2=ordenar_por_mezcla(L[m:])
L=mezclar(L1,L2)
return(L)
División y Conquista: Pivote y Separación
Este otro método de ordenar también usa el principio de división y conquista.
La idea es escoger un pivote, el cual se usa para separar la lista entre elementos
menores, iguales y mayores que el pivote. La primera y tercera lista se ordenan
recursivamente y el resultado es la concatenación de las tres.
Ordenar-por-Separacion (L)
si longitud(L) ≤ 1
devolver (L)
pivote ← Seleccione-Pivote(L)
L1 ← []; L2 ← []; Lp ← []
para x en L
si x < pivote
agregar x a L1
si-no si L[i] > pivote
agregar x a L2
si-no
agregar x a Lp
devolver (Ordenar-por-Separacion(L1 )+
Lp + Ordenar-por-Separacion(L2 ))
Ejemplo: La figura muestra el diagrama de recursión resultante para la lista
[3, 5, 1, 4, 8, 9, 2, 7, 6] cuando en cada caso el pivote es el primer elemento del subarreglo siendo ordenado.
Número de Comparaciones: El número de comparaciones realizadas depende
de la escogencia del pivote. En el peor de los casos, el pivote es siempre el máximo
ó mı́nimo elemento y entonces una recursión es sobre una lista vacı́a y la otra
sobre una lista con un elemento menos. Se obtiene una situación representada
VI.3. ORDENAMIENTO
17
(3,5,1,4,8,9,2,7,6)
(1,2)
(5,4,8,9,7,6)
(2)
(4)
(8,9,7,6)
(7,6)
(9)
(6)
por el diagrama en la figura (izquierda), donde el número en un nodo indica el
tamaño de la lista a ordenar. El número de comparaciones es entonces cuadrático
(los detalles son similares a los de ordenamiento por inserción y selección). Por
otra parte, si el pivote resulta ser el elemento medio, entonces la partición es
balanceada como se muestra en la figura (derecha). En este caso el análisis es
como el de ordenamiento por mezcla y el resultado es O(n log n). En el diagrama
se ignora que n/2, n/4 etc. no son necesariamente enteros (además hace falta
restar 1, lo que es menos importante ya que nos interesa una cota superior), pero
es útil para entender por qué el resultado es O(n log n): Si se suman los valores de
los nodos en un mismo nivel, el resultado es n (porque es n/2i sumado 2i veces).
Y el número de niveles es casi exactamente log2 n, porque este es el número de
veces que n debe ser dividido por 2 para llegar al caso base de la recursión. Por
lo tanto el número de comparaciones es O(n log n).
La siguiente sección (incluı́da sólo por información; no fué discutida en clase)
muestra que si el pivote se escoge aleatoriamente, entonces en promedio el número
de comparaciones realizadas es O(n log n).
n
n
0
n−1
0
n/2
n−2
0
n−3
n/4
0
n/2
n/4
n/4
n/4
n−4
0
n−5
n/8
n/8
n/8
n/8
n/8
n/8
n/8
n/8
18
CAPÍTULO VI. ALGORITMOS: ORDENAMIENTO
Código Python
def seleccion_pivote(L):
# selecciona un pivote
return(L[0])
#
def ordenar_por_separacion(L):
if len(L)<=1:
return(L)
pivote=seleccion_pivote(L)
L1=[]; L2=[]; Lp=[]
for item in L:
if item<pivote:
L1.append(item)
elif item>pivote:
L2.append(item)
elif item==pivote:
Lp.append(pivote)
return(ordenar_por_separacion(L1)+Lp+ordenar_por_separacion(L2))
#
VI.3.5.
Separación con Pivote Aleatorio: Quick Sort
QuickSort es el algoritmo de ordenamiento por separación, con la adición
de que el pivote se escoge aleatoriamente: cada elemento de la lista puede ser
escogido como pivote con igual probabilidad. El siguiente análisis muestra que el
número de comparaciones es O(n log n). (Nota: Esto no se hizo en clase y es sólo
por beneficio de quien quiera leerlo.)
Ecuación de Recurrencia
Sea T (n) el número esperado de comparaciones para una lista de entrada de
longitud n (el valor esperado es el promedio sobre todas las posibles elecciones
del pivote). Por simplicidad asumimos que los elementos de la lista son diferentes.
Supongamos que se escoge como pivote el k-ésimo elemento en orden creciente.
Entonces, después de compararlo a todos los otros n − 1 elementos, la ejecuciones
recursivas tienen listas de longitud k − 1 (elementos menores que el pivote) y de
longitud n − k (elementos mayores que el pivote). Ası́ que el número esperado de
comparaciones en este caso es
(n − 1) + T (k − 1) + T (n − k).
Puesto que cada uno de los elementos de la lista tiene igual probabilidad de ser
elegido como pivote, es decir probabilidad 1/n, entonces el valor esperado teniendo
en cuenta todas las posibles elecciones de pivote es
1X
((n − 1) + T (k − 1) + T (n − k))
T (n) =
n k=1
n
1X
(T (k − 1) + T (n − k)) ,
n k=1
n
= (n − 1) +
VI.3. ORDENAMIENTO
19
con T (0) = T (1) = 0, como casos base. Note que en la suma el primer término
resulta en la suma de los términos T (0), T (1), T (2), . . . , T (n − 1) y el segundo en
los términos T (n − 1), T (n − 2), . . . , T (0). Ası́ que esto se puede reescribir como
2X
T (k).
n k=1
n−1
T (n) = (n − 1) +
Ahora nos ocupamos de resolver esta ecuación de recurrencia.
Alternativa I: Solución aproximada de la ecuación de recurrencia
Una posible forma de resolver esta ecuación es verificar que para alguna constante C (por determinar), se tiene que
T (n) ≤ Cn ln n.
Esto se hace usando inducción (fuerte). El caso base n = 1 es claro puesto que
T (1) = 0. Entonces, usando la hipótesis de inducción para k = 1, 2, . . . , n − 1, es
decir que
T (k) ≤ Ck ln k,
se tiene (el paso a la segunda lı́nea se aclara adelante)
2C X
k ln k
n k=1
2
2C
n ln n n2 1
= (n − 1) +
·
−
+
n
2
4
4
1
= Cn ln n + (n − 1) − (C/2)n +
4
3
= Cn ln n + (1 − C/2)n −
4
≤ Cn ln n + (1 − C/2)n.
n−1
T (n) ≤ (n − 1) +
Si C ≥ 2, esto es menor ó igual a Cn ln n. En la segunda lı́nea hemos usado la
desigualdad
n−1
X
n2 ln n n2 1
k ln k ≤
−
+
2
4
4
k=1
la cual se puede obtener acotando la suma con una integral:
Zn
n−1
X
k ln k ≤
k ln k
k=1
1
n
1 2
1 2 =
k ln k − k 2
4
1
1 2
1 2 1
=
n ln n − n + .
2
4
4
20
CAPÍTULO VI. ALGORITMOS: ORDENAMIENTO
Alternativa II: Solución exacta de la ecuación de recurrencia.
Aunque la recurrencia no se ajusta a algún método de solución que ya conocemos, con algo de manipulación se puede transformar en una que si es fácil de
resolver. Primero, multiplicamos la recurrencia por n y restamos de ella la misma
expresión para n − 1 (en lugar de n):
nT (n) − (n − 1)T (n − 1) = (n − 1)n − (n − 2)(n − 1) + 2T (n − 1),
y entonces
T (n) = 2 −
2 n+1
n+1 n+1
+
T (n − 1) = 4 − 2
+
T (n − 1).
n
n
n
n
Ahora substituı́mos t(n) = T (n)/(n + 1) y entonces obtenemos
t(n) =
2
4
− + t(n − 1),
n+1 n
con t(0)
Pn= 10. De aquı́, iterando la recurrencia, obtenemos, usando la notación
Hn = i=1 i ,
t(n) = 4
n
X
i=1
X1
1
−2
i+1
i
i=1
n
= 4(Hn+1 − 1) − 2Hn
4
= 2Hn − 4 +
.
n+1
Finalmente,
T (n) = (n + 1)t(n) = 2(n + 1)Hn − 4(n + 1) + 4.
Hn es llamado el n-ésimo número armónico, y su valor es muy aproximadamente
log n, de hecho
log(n + 1) ≤ Hn ≤ log n + 1
lo que se puede obtener acotando con integrales. Por lo tanto se obtiene
T (n) = O(n log n).
Descargar