Taller 2: Agoritmos de ordenamiento y estructuras de datos.

Juan David Uchuvo Gonz´lez, C´d: 257895 a o Jos´ David Bermeo L´pez, C´d: 257911 e o o May 4, 2011
1. Desarrolle los siguientes ejercicios del [Cormen09] (a) Exerc. 6.5-9 pag 166. Give an O(nlgk)-time algorithm to merge k sorted lists into one sorted list, where n is the total number of elements in all inputs list. # ! / usr / bin / python def left ( i ): return 2*( i +1) def right ( i ): return 2*( i +1)+1 def parent ( i ): return ( i +1)/2 def swap (A ,i , j ): A[i], A[j] = A[j], A[i] class heap : def __init__ ( self ,A , max ): self . heapsize = len ( A ) -1 self . A = A if max : self . build_max_heap () else : self . build_min_heap () def min_heapify ( self , i ): l = left ( i ) -1 r = right ( i ) -1 if l <= self . heapsize and self . A [ l ] < self . A [ i ]: smaller = l else : smaller = i if r <= self . heapsize and self . A [ r ] < self . A [ smaller ]: smaller = r if smaller != i : swap ( self .A ,i , smaller ) self . min_heapify ( smaller ) def max_heapify ( self , i ): l = left ( i ) -1 r = right ( i ) -1 if l <= self . heapsize and self . A [ l ] > self . A [ i ]: largest = l else : largest = i 1

def

def

def def

def

if r <= self . heapsize and self . A [ r ] > self . A [ largest ]: largest = r if largest != i : swap ( self .A ,i , largest ) self . max_heapify ( largest ) build_min_heap ( self ): self . heapsize = len ( self . A ) -1 for i in range (( len ( A ) -1)/2 , -1 , -1): self . min_heapify ( i ) build_max_heap ( self ): self . heapsize = len ( self . A ) -1 for i in range (( len ( A ) -1)/2 , -1 , -1): self . max_heapify ( i ) heap_min ( self ): # Only for minheaps return self . A [0] heap_extract_min ( self ): # Only for minheaps if self . heapsize < 1: return " Overflow " min = self . A [0] self . A [0] = self . A [ self . heapsize ] self . heapsize = self . heapsize -1 self . min_heapify (0) return min h e a p _ e x t r a c t _ m i n _ o f _ m i n ( self ): if self . heapsize < 1: return " Overflow " number = self . A [0]. pop (0) if len ( self . A [0]) < 1: self . A . pop (0) self . heapsize = self . heapsize -1 self . min_heapify (0) return number

def merge_sort (A , n ): # A is list of lists such that A [ k ] is a sorted list . result = [] C = heap (A , False ) # True is a maxheap , and False a minheap for i in range ( n ): result . append ( C . h e a p _ e x t r a c t _ m i n _ o f _ m i n ()) return result if __name__ == " __main__ " : A = [[1 ,3 ,5 ,7] ,[2 ,4 ,6 ,9] ,[8 ,10 ,11]] print A print merge_sort (A ,11) Usamos un minheap de listas ordenadas [A1 , A2 , · · · , Ak ] que mantiene su propiedad de heap usando como llave la primera posici´n de la lista Ai . La funci´n merge sort toma tiempo o o O(nlgk) ya que la llamada para construir un minheap con k elementos como m´ximo toma a tiempo O(k) (cuando a´n ninguna lista esta vac´ Y las n llamadas a heap extract min of min u ıa). toman tiempo O(nlg(k)). (b) Exerc. 7.2-3 pag 178. Show that the running time of quicksort is Θ(n2 ) when the array A cotains distinct elements and is sorted in decreasing order. El tiempo de ejecuci´n de partition(A, p, r) es Θ(n), y los arreglos resultantes para hacer o las nuevas lamadas recursivas a quicksort son A[p, · · · , n − 1] y A[], con tama˜o n − 1 y 0 n

2

respectivamente, por ende el tiempo de ejecuci´n T de quicksort por ´rbol de recursi´n es: o a o T (n) = = = =
i=1 n

T (n − 1) + T (0) + Θ(n) T (n − 1) + Θ(1) + Θ(n) T (n − 1) + Θ(n)
n

Θ(i) c
i=1

=

i = Θ(n2 )

Por m´todo de sustituci´n con T (n) = O(n2 ): e o Caso base: Suponemos, T (0) = c0 caso base con n = 1. Si T (n) = O(n2 ) entonces T (1) = O(1) → T (1) ≤ c T (n) = Θ(n) + T (n − 1) + T (0) T (1) = Θ(0) + T (0) + T (0) = 2c0
c Entonces 2 ≥ c0 que se cumple para c suficientemente grande. Paso inductivo: Suponemos que se cumple para n − 1 para concluir que tambi´n se cumple e para n. T (n) ≤ c(n − 1)2 + dn + c0 = cn2 − 2cn + c + dn + c0 = cn2 + (d − 2c)n + c + c0 ≤ cn2 ⇔ (d − 2c)n + c + c0 ≥ 0 1 ⇔ d + c0 ≤ (2 − n )c

Que se cumple para n suficientemente grande por hipot´sis de inducci´n. e o (c) Exerc. 8.2-4 pag 197. Describe an algorithm that, given n integers in the range 0 to k, preprocesses its input and then answers any query about how many of the n integers fall into a range [a · · · b] in O(1) time. Your algorithm should use Θ(n + k) preprocessing time. def preprocess (A , k ): C = [0]* k for j in range ( len ( A )): C [ A [ j ]] = C [ A [ j ]]+1 for j in range (1 , k ): C [ j ] = C [ j ]+ C [j -1] B = [0]* len ( A ) for j in range ( len ( A ) -1 , -1 , -1): B [ C [ A [ j ]] -1] = A [ j ] C [ A [ j ]] = C [ A [ j ]] -1 return C def amount (A ,k ,a , b ): C = preprocess (A , k ) if ( a ==0): return C [ b ] return C [ b ] - C [a -1] La ideaes usar parte del c´digo de counting-sort, hasta el momento en que se tiene guardado o en el array C la cantidad de elementos anteriores a un determinado n´mero, con esta cantiu dades almacenadas podemos saber la cantidad de elementos en un intervalo dado en tiempo constante. El tiempo de ejecuci´n de preprocess es Θ(n + k) y el resto del c´digo de amount o o gasta tiempo Θ(1). (d) Probl. 10-1 pag 249. Comparisons among lists For each of the four types of lists in the following table, what is the asymptotic wort-case running time for each dynamic-set operation listed. En todas las estructuras existe un apuntador a la cola y uno al inicio 3

SEARCH(L, k) IN SERT (L, x) DELET E(L, x) SU CCESSOR(L, x) P REDECESSOR(L, x) M IN U M U M (L) M AXIM U M (L)

Unsorted, singly linked O(n) O(1) O(1) O(n) O(n) O(n) O(n)

Sorted, singly linked O(n) O(n) O(n) O(1) O(n) O(1) O(1)

Unsorted, doubly linked O(n) O(1) O(1) O(n) O(n) O(n) O(n)

Sorted, doubly linked O(n) O(n) O(1) O(1) O(1) O(1) O(1)

(e) Exerc. 11.4-1 pag 277. Consider inserting the keys 10, 20, 22, 31, 4, 15, 28, 17, 88, 59 into hash table of length m = 11 using open addresing with the auxiliary hash function h (k) = k. Illustrate the result of inserting these keys using linear probing, using quadratic probing with c1 = 1 and c2 = 3, and using double hashing with h1 (k) = k and h2 (k) = 1 + (kmod(m − 1)). Linear hash: 22 88 Quadratic hash: 22 59 Double hashing: 22 / 59 17 4 15 28 88 / 31 10 88 17 4 / 28 / 15 31 10 / / 4 15 28 17 59 31 10

(f) Exerc. 12.2-8 pag 294. Prove that no matter what node we start at in a height-h binary search tree, k successive calls to tree-successor take O(k + h) time. Ya que el tiempo de ejecuci´n de tree-successor en un ´rbol de altura h es O(h), entonces el o a tiempo de k llamadas sucesivas al ´rbol desde cualquier nodo x est´ dado por la recurrencia a a T (n) = O(h)+T1 (1)+T2 (1)+· · ·+Tk (1), es decir el costo de ejecutar la llamada a tree-successor y el costo de llamarlo en general k veces para un nodo x. (g) Exerc. 13.3-2 pag 322. Show the red-blacks trees that result after succesively inserting the keys 41, 38, 31, 12, 19, 8 into a initially empty red-black tree. 38 19 12 8 N il N il N il N il 31 N il N il 41 N il

2. Desarrolle el ejercicio 7.4-5 pag 185 del [Cormen09]. We can improve the running time of quicksort in practice by taking advantage of the fast running time of insertion sort when its input is “nearly” sorted. Upon calling quicksort on a subarray with fewer than k elements, let it simply return without sorting the array. After top-level call to quicksort returns, run insertion sort on the entire array finish the sorting process. Argue that this sorting algorithm runs in O(nk + nlg(n/k)) expected time. How should we pick k, both in theory and in practice? (a) Muestre que el tiempo esperado es de O(nk+nlog( n )) (Utilice el hecho que el tiempo esperado k de quicksort es O(nlog(n)). El tiempo de ejecuci´n de este algoritmo depende del tiempo que tome ejecutando el quicko sort mas el tiempo que tome ejecutanto el insertion sort. Primero trataremos el tiempo del quicksort que est´ dado por el n´mero total de comparaciones X que se hace durante toda a u la ejecuci´n del algoritmo, teniendo en cuenta que en subarreglos de tama˜o menor a k el o n

4

algoritmo de quicksort no se ejecutar´. a
n−1 n

X

=
i=1 j=k+i+1 n−1 n−i−1

2 j−i+1

=
i=1 r=k n−1 n

2 r+2
n−1 n

<
i=1 r=k n−1

2 = r

i=1

r=1

2 2 − r r=1 r

k

=
i=1 n−1

O(lg(n)) − O(lg(k))

=
i=1 n−1

O(lg(n)) + O(lg(1/k)) n n O(lg( )) = O(nlg( )) = T Q(n, k) k k i=1

=

Ahora el arreglo en general est´ ordenado en subarreglos de tama˜o m´ximo k que pueden a n a estar no ordenados internamente. Asi que el tiempo de ejecuci´n del insertion sort es la suma o del tiempo de aplicaci´n de este algoritmo sobre cada uno de los subarreglos. El n´mero o u m´ximo de estos subarreglos es de n y por cada uno el tiempo promedio y en el peor caso es a k de O(k 2 ). T I(n, k) T (n, k) = = n O(k 2 ) = O(nk) k

n T Q(n, k) + T I(n, k) = O(nlg( )) + O(nk) = O(nk + nlg(n/k)) k

(b) De manera te´rica, encuentre un buen valor para k. o T (n, k) = O nk + nlg(n/k)

= dnk + dnlg(n/k) ∂ T (n, k) ∂k = dnk + d(lg(e))nln(n) − d(lg(e))nln(k) ∂ = dnk + d(lg(e))nln(n) − d(lg(e))nln(k) ∂k d(lg(e))n = dn − =0 k = d(lg(e))n =⇒ k = lg(e) d(lg(e))n dn = = >0 k2 lg(e)

dnk ∂ d(lg(e))n dn − ∂k k

Por medio del m´todo de la segunda derivada se demostro que con k = lg(e) la funci´n T (n, k) e o toma sus menores valores en funci´n de n teoricamente. o (c) Implemente el algoritmo en Python. 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 5

A [ i +1]= key def quicksort (A ,p ,r , k ): if p +k -1 < r : q = partition (A ,p , r ) quicksort (A ,p ,q -1 , k ) quicksort (A , q +1 ,r , k ) 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] return i +1 def QISORT (A , k ): quicksort (A ,0 , len ( A ) -1 , k ) insertionsort ( A ) (d) Encuentre experimentalmente un buen valor para k. Describa claramente el proceso y reporte los resultados usando gr´ficas. a El proceso utilizado para encontrar el valor de k es observar las gr´ficas resultantes con a diferentes valores de k y n, y ir aproximandonos a donde se encuentra el mejor valor de k. Las gr´ficas son n vs T (n, k) y cada una de las gr´ficas representa un valor diferente de k, a a los datos del arreglo fueron tomados aleatoriamente de 0 a n y se tom´ el promedio de 10 o experimentos para cada n y as´ tener un valor m´s acertado del tiempo. ı a a ´ a De la figura 1 (las figuras est´n en la ultima p´gina) podemos observar que el mejor valor para k esta en los alrrededores de los valores k = 10 y k = 20. De la figura 2 observamos que los peores valores son 8 y 22 y los mejores son 10 y 15 separados por muy poco, esto nos dice que el mejor valor para k esta entre 10 y 20. La figura 3 muestra que los mejores valores son 10, 12 y 14 dejando por fuera los valores de 16 en adelante por ende el mejor valor de k est´ entre 10, 11, 12, 13, 14 y 15. a Por ultimo en la figura 4 vemos que para valores muy grandes de n la diferencia entre los ´ tiempos de los diferentes valores k no es muy grande, en general la gr´fica de los mejores a valores de k son k = 11 y k = 12. 3. La idea del ejercicio es desarrollar un programa que permita realizar el chequeo individual de las palabras en un diccionario (comportamiento similar al de un programa ispell de linux). La idea es que cuando se digite una palabra, el programa tendr´ tres posibles respuestas.Una salida a ENCONTRADO significa que la se encontr´ en el diccionario. Una salida de NO ENCONTRADO o significa que la palabra no se encontr´ en el diccionario y que no hay palabras similares en el o mismo. De otra forma (tercera salida) se deben imprimir una lista ordenada con todas las palabras similares a la digitada por el usuario. Su programa deber reconocer las siguientes palabras similares: (a) Omisiones de una unica letra. (caonsado:cansado). ´ (b) Adiciones de una unica letra. (cnsado:cansado). ´ (c) Transposiciones de dos letras vecinas (casnado:cansado). (d) Sustituci´n de una letra por otra (catsado:cansado). o class Node : def __init__ ( self , Nodekey , NEWTREE , term ): self . Nodekey = Nodekey self . NEWTREE = NEWTREE self . term = term def __lt__ ( self , otro ): 6

return self . Nodekey < otro . Nodekey def __ne__ ( self , otro ): return self . Nodekey != otro . Nodekey class TREENODE : def __init__ ( self , left , key , right ,p , color ): self . key = key self . left = left self . right = right self . p = p self . color = color class RBTREE : def __init__ ( self ): self . NIL = TREENODE ( None , Node ( " NIL " , None ,0) , None , None ,1) self . root = self . NIL def LEFTROTATE ( self , x ): y = x . right x . right = y . left if y . left != self . NIL : y . left . p = x y.p=x.p if x . p == self . NIL : self . root = y elif x == x . p . left : x . p . left = y else : x . p . right = y y . left = x x.p= y def RIGHTROTATE ( self , x ): y = x . left x . left = y . right if y . right != self . NIL : y . right . p = x y.p=x.p if x . p == self . NIL : self . root = y elif x == x . p . left : x . p . left = y else : x . p . right = y y . right = x x.p= y def RBINSERTFIXUP ( self , z ): while z . p . color ==0: if z . p == z . p . p . left : y = z . p . p . right if y . color == 0: z . p . color =1 y . color =1 z . p . p . color =0 z=z.p.p 7

else : if z == z . p . right : z=z.p self . LEFTROTATE ( z ) z . p . color =1 z . p . p . color =0 self . RIGHTROTATE ( z . p . p ) else : y = z . p . p . left if y . color == 0: z . p . color =1 y . color =1 z . p . p . color =0 z=z.p.p else : if z == z . p . left : z=z.p self . RIGHTROTATE ( z ) z . p . color =1 z . p . p . color =0 self . LEFTROTATE ( z . p . p ) self . root . color =1 def INSERT ( self , z ): y = self . NIL x = self . root while x != self . NIL : y=x if z . key < ( x . key ): x = x . left else : x = x . right z.p=y if y == self . NIL : self . root = z elif z . key < ( y . key ): y . left = z else : y . right = z z . left = self . NIL z . right = self . NIL z . color = 0 self . RBINSERTFIXUP ( z ) def SEARCH ( self , key ): if self . root == self . NIL : return None node = self . root while ( node != self . NIL and node . key != key ): if ( node . key <( key )): node = node . right else : node = node . left if ( node == self . NIL ): return None return node 8

def INORDER ( self ): if self . root == self . NIL : return [] else : return inorder ( self . root , self . NIL ) def SUCCESSOR ( self , node ): if ( node == self . NIL ): return None if ( node . right != self . NIL ): node = node . right while ( node . left != self . NIL ): node = node . left return node p = node . p while ( p != self . NIL and node == p . right ): node = p p = node . p return p

def inorder ( node , NIL ): if node == NIL : return [] else : result = inorder ( node . left , NIL ) result += [ node . key ] result += inorder ( node . right , NIL ) return result def BUILD_TREE ( file , tree ): for line in file : Atree = tree for i in range (1 , len ( line ) -2): if len ( line ) -3== i : Nodetemp = Node ( line [ i ] , RBTREE () ,1) else : Nodetemp = Node ( line [ i ] , RBTREE () ,0) temp = Atree . SEARCH ( Nodetemp ) if temp == None : temp2 = TREENODE ( None , Nodetemp , None , None ,0) Atree . INSERT ( temp2 ) Atree = Nodetemp . NEWTREE else : Atree = temp . key . NEWTREE if ( len ( line ) -3== i ): temp . key . term =1 def SEARCH_WORD ( word , tree ): Atree = tree for letter in range ( len ( word )): Nodetemp = Node ( word [ letter ] , RBTREE () ,0) temp = Atree . SEARCH ( Nodetemp ) if temp == None : return -1 9

Atree = temp . key . NEWTREE if ( letter == len ( word ) -1 and temp . key . term ==1): return 1 return 0 def getalphabet (): alphabet = [] for i in range (26): alphabet +=( chr ( i +97)) return alphabet def addletter ( word , tree ): S = [] alphabet = getalphabet () for i in range ( len ( word )): for char in alphabet : similar = word [: i ]+ char + word [ i :] if ( SEARCH_WORD ( similar , tree )==1): S . append ( similar ) return S def removeletter ( word , tree ): S = [] for i in range ( len ( word )): similar = word [: i ]+ word [ i +1:] if ( SEARCH_WORD ( similar , tree )==1): S . append ( similar ) return S def transposeletters ( word , tree ): S = [] for i in range ( len ( word ) -1): similar = word [: i -1]+ word [ i ]+ word [i -1]+ word [ i +1:] if ( SEARCH_WORD ( similar , tree )==1): S . append ( similar ) return S def replaceletter ( word , tree ): S = [] alphabet = getalphabet () for i in range ( len ( word )): for char in alphabet : similar = word [: i ]+ char + word [ i +1:] if ( SEARCH_WORD ( similar , tree )==1): S . append ( similar ) return S def s e a r c h _ s i m i l a r _ w o r d s ( word , tree ): S = [] S += addletter ( word , tree ) S += removeletter ( word , tree ) S += transposeletters ( word , tree ) S += replaceletter ( word , tree ) return S if __name__ == ’ __main__ ’: try : file = open ( ’ spanish . lex ’ , ’r + ’) except IOError : print " NO SE PUDO CARGAR EL ARCHIVO " else : tree = RBTREE () 10

BUILD_TREE ( file , tree ) print " YA CARGO EL DICCIONARIO " i =20 while ( i ): word = raw_input () case = SEARCH_WORD ( word , tree ) if case == 1: print " Encontrado . " if case == -1: print " No encontrado , pero una lista de palabras similares es : print s e a r c h _ s i m i l a r _ w o r d s ( word , tree ) if case == 0: print " La palabra es una subcadena de otra palabra si encontra i = i -1 El diccionario debe ser llenado inicialmente con el conjunto de las palabras dado en la siguiente p´gina: http://www.umich.edu/~archive/linguistics/texts/lexica/(span-lex.zip. a 4. Resuelva el problema del juez en l´ ınea con nombre Christmas Play http://www.spoj.pl/problems/ AMR10G/. # ! / usr / bin / python def CHRISTMAS ( T ): for i in range ( T ): N , K = map ( int , raw_input (). split ()) A = map ( int , raw_input (). split ()) diferencia = 1000000000 if K == N : print max ( A ) - min ( A ) else : if ( K == 1): diferencia = 0 A . sort () for j in range (N - K +1): if (( A [ j +K -1] - A [ j ]) < diferencia ): diferencia = A [ j +K -1] - A [ j ] print diferencia while (1): try : CHRISTMAS ( int ( raw_input ())) except : break

11

Quick-Insertion-Sort

0.6

0.5

K=1 K=10 K=20 K=30 K=40 K=50 K=60 K=70

0.4

1

Tiempo de ejcucion

0.3

0.2

0.1

0 20000 30000 Tamano del array 40000 50000 60000

0

10000

Quick-Insertion-Sort

0.9 K=8 K=10 K=15 K=20 K=22

0.8

0.7

0.6

2

0.5

Tiempo de ejcucion

0.4

0.3

0.2

0.1

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

0

10000

20000

Quick-Insertion-Sort K=10 K=12 K=14 K=16 K=18

1.2

1

0.8

3

Tiempo de ejcucion

0.6

0.4

0.2

0 40000 60000 Tamano del array 80000 100000 120000

0

20000

Quick-Insertion-Sort

1.2

1

K=10 K=11 K=12 K=13 K=14 K=15

0.8

4

Tiempo de ejcucion

0.6

0.4

0.2

0 40000 60000 Tamano del array 80000 100000 120000

0

20000