ALMA MATER STUDIORUM: Universita' di Bologna

Slides:



Advertisements
Presentazioni simili
Algoritmi e strutture Dati - Lezione 7 1 Algoritmi di ordinamento ottimali L’algoritmo Merge-Sort ha complessità O(n log(n))  Algoritmo di ordinamento.
Advertisements

Codici prefissi Un codice prefisso è un codice in cui nessuna parola codice è prefisso (parte iniziale) di un’altra Ogni codice a lunghezza fissa è ovviamente.
Prof.ssa Rossella Petreschi Lezione del 5/12/2012 del Corso di Algoritmi e Strutture Dati Riferimenti: Capitolo 11 del testo Anany Levitin “The design.
Fondamenti di Informatica A - Massimo Bertozzi ALBERI E ALBERI BINARI DI RICERCA (BST)
Huffman Canonico: approfondimento. Come abbiamo visto, Huffman canonico ci permette di ottenere una decompressione più veloce e con un uso più efficiente.
1 ELEMENTI DI INFORMATICA Università degli Studi di Cagliari Corso di Laurea in Ingegneria Elettronica Linguaggio C A.A. 2011/2012
Fondamenti di Informatica A - Massimo Bertozzi LE RAPPRESENTAZIONI CONCATENATE.
1 Simulazione Numerica dei Fenomeni di Trasporto Necessità di introduzione dei tensori  11  12  13  23  21  22 Vogliamo descrivere in un modo che.
INFORMATICA ALGORITMI, PROGRAMMI, E LINGUAGGI DI PROGRAMMAZIONE.
© 2007 SEI-Società Editrice Internazionale, Apogeo
Esercitazioni di Prog. II (esercizi su alberi ennari)
Progettare algoritmi veloci usando strutture dati efficienti
La rappresentazione delle informazioni
Algoritmi Avanzati a.a.2015/2016 Prof.ssa Rossella Petreschi
Algoritmi e Strutture dati a.a.2010/2011 Prof.ssa Rossella Petreschi
Branch and Bound Lezione n°14 Prof.ssa Rossella Petreschi
Dal problema al processo risolutivo
Progettare algoritmi veloci usando strutture dati efficienti
Alberi di ricerca di altezza logaritmica
Algoritmi Avanzati a.a.2014/2015 Prof.ssa Rossella Petreschi
IL CONCETTO DI ALGORITMO
Dal problema al processo risolutivo
10. Programmazione Ricorsiva Ing. Simona Colucci
Divide et Impera Quicksort Mergesort Charles Antony Richard Hoare
Sicurezza informatica
B-alberi e alberi autoaggiustanti
Excel 1 - Introduzione.
Le postcondizioni specificano l’output della funzione.
Algoritmi e Strutture Dati
Algoritmi Avanzati a.a.2011/2012 Prof.ssa Rossella Petreschi
Forme per rappresentare l’algoritmo:
Sulla complessità Lezione n°2
Tipo di dato: array Un array è un tipo di dato usato per memorizzare una collezione di variabili dello stesso tipo. Per memorizzare una collezione di 7.
PROGRAMMAZIONE BASH – ISTRUZIONE IF
Algoritmi e Strutture Dati
Algoritmi e Strutture Dati
Divide et Impera Quicksort Mergesort
Algoritmi e Strutture Dati
Statistica Scienza che studia i fenomeni collettivi.
Programmare.
Algoritmi per il flusso nelle reti
Lezione n°6 Prof.ssa Rossella Petreschi
Scrivere programmi corretti
Ricorsione 16/01/2019 package.
Introduzione agli Algoritmi e alle Strutture Dati
Schema generale, visita in ampiezza e profondità.
Alberi n-ary Lezioni di C.
Algoritmi e Strutture Dati
Algoritmi Avanzati a.a.2011/2012 Prof.ssa Rossella Petreschi
Corso di Laurea Ingegneria Informatica Fondamenti di Informatica
Progettare algoritmi veloci usando strutture dati efficienti
L’algoritmo MergeSort
* 07/16/96 Sez. 2: Ordinamento La consultazione di banche dati è sempre più cruciale in tutte le applicazioni dell’Informatica. Se vogliamo consultare.
APPUNTI SUL LINGUAGGIO C Implementazioni di Liste Concatenate
APPUNTI SUL LINGUAGGIO C Esercizi su File e Alberi Binari
Algoritmi e Strutture Dati
Algoritmi e Strutture Dati
Backtracking Lezione n°17 Prof.ssa Rossella Petreschi
Precorso di Statistica per le Lauree Magistrali
Processi decisionali e funzioni di controllo
Esercizio Dato un albero binario, definiamo altezza minimale di un nodo v la minima distanza di v da una delle foglie del suo sottoalbero, definiamo invece.
MergeSort Usa la tecnica del divide et impera:
Lezione n°7 Splay-Trees e Heaps Prof.ssa Rossella Petreschi
concetti ed applicazioni
Algoritmi e Strutture Dati
Ricerca 01/08/2019 package.
HeapSort Stesso approccio incrementale del selectionSort Tipo di dato
Corso di Fondamenti di Informatica
Script su vettori Realizza uno script che chiede in input una dimensione d e crea un vettore di d numeri interi casuali. Poi calcola la somma dei due numeri.
Corso di Fondamenti di Informatica
Transcript della presentazione:

ALMA MATER STUDIORUM: Universita' di Bologna PAS 2013-14 Renzo Davoli ALMA MATER STUDIORUM: Universita' di Bologna Bologna, 5 giugno 2014

Il percorso di oggi... Algoritmi di ricerca Lineare, binaria, hashing, Cenni su altri algoritmi di ricerca Esercizi Informatica e diritto di autore

Algoritmi di ricerca (searching algorithms) Scopo di questi algoritmi e' di trovare un elemento in una collezione Puo' essere usato per verificare se un elemento esiste o meno alla collezione (valore di ritorno booleano) L'elemento puo' essere una chiave di accesso ad altre informazioni. Dal punto di vista algoritmico non c'e' differenza: una volta trovato l'elemento o si restituisce True o l'informazione associata.

Ricerca lineare: Funziona con strutture dati ad accesso sequenziale Non richiede che I dati siano ordinati Vengono scanditi gli elementi ad uno ad uno, ci si ferma quando viene trovato l'elemento cercato o se sono stati scanditi tutti gli elementi

In python import random def linearsearch(x,seq): for i in range(len(seq)): if seq[i] == x: return True return False if __name__=="__main__": test=list(range(0,256,2)) random.shuffle(test) while True: n=int(input("numero da cercare: ")) print("C'e'" if linearsearch(n,test) else "non c'e'")

Complessita' Computazionale della Ricerca Lineare Se N e' la lunghezza della sequenza, quanti confronti vengono fatti? Nel caso ottimo: 1 Nel caso pessimo: N Nel caso medio: N/2

Verifichiamo statisticamente i valori della complessita' import random def contaconfronti(): contaconfronti.contatore+=1 contaconfronti.contatore=0 def linearsearch(x,seq): for i in range(len(seq)): contaconfronti() if seq[i] == x: return True return False if __name__=="__main__": test=list(range(0,256,2)) random.shuffle(test) ntest=10000 for i in range(ntest): n=random.randint(0,128)*2 linearsearch(n, test) print("media confronti (elementi esistenti)=",contaconfronti.contatore/ntest) n=random.randint(0,256) print("media confronti=",contaconfronti.contatore/ntest)

Ecco I risultati: media confronti (elementi esistenti)= 64.7061 media confronti= 96.2049 media confronti (elementi esistenti)= 64.5336 media confronti= 96.9843 media confronti (elementi esistenti)= 65.0259 media confronti= 96.2206 La lista contiene I numeri pari da 0 a 254. (128 elementi) In media impiega 64 passi a cercare un elemento esistente. Scegliendo un numero a caso fra 0 e 255 la probabilita' che un elemento cercato sia esistente e' 50% In media impiega 64 passi per quelli esistenti e 128 per quelli assenti, la media e' 96.

Tempo di elaborazione import random import timeit def linearsearch(x,seq): for i in range(len(seq)): if seq[i] == x: return True return False def search(test,maxrange): n=random.randint(0,maxrange-1) linearsearch(n,test) if __name__=="__main__": for logmax in range(1,10): rmax=2**logmax test=list(range(0,rmax,2)) random.shuffle(test) time=timeit.timeit( stmt="__main__.search(__main__.test,__main__.rmax)", setup="import __main__",number=10000) print(rmax,time)

Ecco I risultati: 2 0.04324199602706358 4 0.0456185259972699 8 0.04719773802207783 16 0.04939107102109119 32 0.05511521396692842 64 0.06748586101457477 128 0.09015233302488923 256 0.1374857670161873 512 0.2275397659977898 Viene calcolato il tempo di esecuzione per sequenze da 2 a 512 elementi

Grafico Con l'aiuto di un foglio elettronico ecco il grafico x-y del tempo necessario a trovare un elemento al variare del numero di elementi:

Ricerca binaria: E' detta anche dicotomica Funziona con strutture dati ad accesso diretto (occorre poter accedere direttamente a ogni elemento dato l'indice nella struttura dati) Richiede che I dati siano ordinati Si confronta l'elemento cercato con quello centrale: Se e' quello cercato: eureka Se quello cercato e' minore si ripete la ricerca nella sottosequenza degli elementi successivi a quello centrale Altrimenti la ricerca viene ripetuta usando la sottosequenza degli elementi precedenti.

In Python import random def binarysearch(x,seq): if seq: half=len(seq)//2 if x == seq[half]: return True else: if x > seq[half]: return binarysearch(x,seq[half+1:]) return binarysearch(x,seq[:half]) return False if __name__=="__main__": test=list(range(0,256,2)) while True: n=int(input("numero da cercare: ")) print("C'e'" if binarysearch(n,test) else "non c'e'")

Complessita' Computazionale della Ricerca Dicotomica Se N e' la lunghezza della sequenza quante chiamate ricorsive vengono fatte? Nel caso ottimo: 1 Nel caso pessimo: log2(N) Nel caso medio (elementi esistenti): log2(N)-1

Una versione piu' efficiente (Non copia le sequenze) import random def binarysearch(x,seq): def rseq(lo,hi): if lo<=hi: n=(lo+hi)//2 if x==seq[n]: return True else: if x > seq[n]: return rseq(n+1,hi) return rseq(lo,n-1) return False return rseq(0,len(seq)-1) if __name__=="__main__": test=list(range(0,256,2)) while True: n=int(input("numero da cercare: ")) print("C'e'" if binarysearch(n,test) else "non c'e'")

Proviamo a stimare il tempo di esecuzione import random import timeit def binarysearch(x,seq): # e' quella descritta prima... def search(test,maxrange): n=random.randint(0,maxrange-1) binarysearch(n,test) if __name__=="__main__": for logmax in range(1,10): rmax=2**logmax test=list(range(1,rmax,2)) random.shuffle(test) time=timeit.timeit( stmt="__main__.search(__main__.test,__main__.rmax)", setup="import __main__",number=10000) print(rmax,time)

Grafico: Come prima con un foglio elettronico e' stato possibile disegnare il grafico: ascisse=numero di elementi della sequenza, ordinata=tempo di esecuzione

Limiti della ricerca binaria Occorre una sequenza ordinata e la possibilita' di accesso diretto E' costoso mantenere queste proprieta' se la sequenza viene modificata Per esempio occorrerebbe ordinare nuovamente la sequenza ad ogni inserimento/cancellazione

DIVIDI ET IMPERA La migliore performance della ricerca binaria si deve alla capacita' dell'algoritmo di eliminare ad ogni iterazione la meta' degli elementi residui dallo spazio di ricerca. L'approccio che consiste nel dividere un problema in due o piu' sottoproblemi di dimensione inferiore si chiama Dividi et Impera (e' un motto attribuito a Filippo il macedone: fomentando le rivalita' all'interno di un popolo lo si governa meglio, separando un problema lo si risolve meglio).

Radix searching e i Faldoni... La tecnica di separare gli elementi da cercare in classi e' comune nella vita di tutti I giorni. Il “cassetto dei calzini” consente di non dover cercare in tutto l'armadio quando si ha necessita' di un paio di calzini. Negli uffici spesso ci sono faldoni contrassegnati dalle lettere dell'alfabeto (le pratiche dei clienti con cognome che inizia con la 'M' stanno nel faldone 'M') Non sara' ottimale come la ricerca binaria, ma si puo' velocizzare la ricerca. (diventerebbe ottimale se tutti I faldoni contenessero circa lo stesso numero di elementi. Chi ha esperienza di ufficio sa che I faldoni con la B e con la M sono piu' pieni di quelli con la Q o la H)

Radix tree, l'albero delle radici comuni Gli elementi vengono messi in un albero. Le radici comuni formano un nodo dell'albero La complessita' di ricerca e' proporzionale alla profondita' dell'albero (che puo' essere sbilanciato) CC BY-SA 2.5 Claudio Rocchini

Hashing (traduzione da vocabolario: pasticcio, guazzabuglio) Si creano N contenitori (N viene scelto a seconda della quantita' di risorse a disposizione) Si definisce una funzione hash che in qualche modo associa a ogni chiave uno dei contenitori Se la funzione “sparpaglia” uniformemente gli M elementi da cercare nei contenitori alla fine in ogni contenitore ci sono M/N elementi Teoricamente se la funzione di Hash sparpagliasse perfettamente gli elementi nel caso N=M l'elemento verrebbe trovato in complessita' 1 (al primo tentativo).

In Python Ricerca in Aptiva 2014 def hashfun(x): s=str(x) count=5381 for c in s: count=count*33+ord(c) return count NELEM=128 hashtable=[[] for i in range(NELEM)] def hash_insert(elem): h=hashfun(elem)%NELEM hashtable[h].append(elem) def hashtable_create(keys): for elem in keys: hash_insert(elem) def hash_search(elem): for scan in hashtable[h]: if scan == elem: return True return False aptiva14list=["Franca", "Marta", "Denis", "Anna", "Runa", "Arianna", "Domenico", "Alice", "Carmosina", "Francesca", "Melissa", "Agostino", "Carmela", "Silvia", "Gaetana", "Lara", "Chiara", "Michelangelo", "Adriana", "Davide", "Mariangela", "Aniello", "Renzo"] hashtable_create(aptiva14list) print(hashtable) while True: s=input("Scrivi un nome: ") if not s: break print(s,"" if hash_search(s) else "non","fa parte di Aptiva 2014") In Python Ricerca in Aptiva 2014

Proviamo il programma Il programma mostra la tabella e poi consente di provare la ricerca [[], [], [], [], [], ['Lara'], [], [], [], ['Aniello'], [], [], [], [], [], [], ['Franca'], [], ['Davide'], ['Melissa'], [], [], [], [], ['Denis'], [], ['Carmela'], [], [], [], [], [], [], [], [], ['Anna', 'Alice'], [], [], [], [], [], [], [], [], [], ['Silvia', 'Michelangelo'], [], [], [], [], [], [], [], ['Adriana'], [], [], [], [], [], [], [], [], [], ['Arianna'], [], [], [], [], [], [], [], [], [], ['Agostino'], [], [], [], ['Chiara'], [], [], [], [], [], ['Renzo'], [], [], ['Mariangela'], [], [], [], [], ['Runa'], [], [], [], [], [], [], ['Carmosina'], [], [], [], [], [], [], [], [], ['Francesca'], [], [], [], [], [], [], [], ['Domenico'], [], [], ['Gaetana'], [], [], [], ['Marta'], [], [], [], [], []] Scrivi un nome: Agostino Agostino fa parte di Aptiva 2014 Scrivi un nome: Lara Lara fa parte di Aptiva 2014 Scrivi un nome: Filippo Filippo non fa parte di Aptiva 2014 Scrivi un nome:

Quanto e' efficiente l'hashing Per provare l'efficienza della ricerca tramite tabelle di hash proviamo a inserire tutte le parole di un vocabolario della lingua inglese Ovviamente l'efficienza dipendera' dalla “bonta'” della funzione scelta e dalla distribuzione dei dati da memorizzare.

Il programma di prova def hashfun(x): s=str(x) count=5381 for c in s: count=count*33+ord(c) return count def hash_insert(elem): h=hashfun(elem)%NELEM hashtable[h].append(elem) def hash_evaluate(N): lentable=[len(hashtable[i]) for i in range(NELEM)] print("N= {:5} MIN= {:5} MAX= {:5} MEAN_VALUE= {:5}".format(N, min(lentable),max(lentable),sum(lentable)/len(lentable))) for N in range(16): NELEM=2**N hashtable=[[] for i in range(NELEM)] with open("/usr/share/dict/american-english",'r') as fd: for word in fd: word=word.strip() hash_insert(word) hash_evaluate(NELEM)

Ecco l'output N= 1 MIN= 99171 MAX= 99171 MEAN_VALUE= 99171.0

In forma grafica e' piu' chiaro

Ricerca di una stringa in un testo (string searching) Un algoritmo di ricerca che usiamo spesso nei word processor (o anche nei browser) e' la ricerca di un testo nel documento o nella pagina. Come funziona questa ricerca? Che complessita' ha?

Algoritmo di Forza Bruta E' l'algoritmo piu' semplice (e meno efficiente). Si confronta ogni sottostringa del testo di lunghezza uguale alla stringa da cercare con la stringa da cercare stessa Text: Sempre caro mi fu quest'ermo colle Ko: caro Ko: caro Ko: caro Ko: caro Ko: caro .... Ok: caro

In Python def bfsearch(s,t): for i in range(len(t)-len(s)+1): # if t[i:i+len(s)] == s: for j in range(len(s)): if t[i+j] != s[j]: break else: return i return None if __name__=="__main__": t=input("scrivi il testo: ") s=input("scrivi la stringa da cercare: ") n=bfsearch(s,t) if n != None: print("la stringa e' presente alla posizione: ",n) print("la stringa non e' presente")

Risultato scrivi il testo: sempre caro mi fu quest'ermo colle scrivi la stringa da cercare: caro la stringa e' presente alla posizione: 7 scrivi la stringa da cercare: montagna la stringa non e' presente La complessita' e' piu che proporzionale rispetto alla lunghezza del testo Se si controllasse sempre tutta la stringa da cercare sarebbe NxM dove N e' la lunghezza del testo, M la lunghezza della stringa da cercare Con l'ottimizzazione del break nel confronto fra stringhe, dipende dal numero di parole con prefissi comuni

SI puo' fare di piu'! Esistono algoritmi di ricerca per stringhe piu' veloci Ne voglio analizzare uno in particolare (Rabin-Karp) perche' fornisce un esempio di strategia risolutiva Divide et Impera. Si costruisce una funzione che abbia una bassa complessita' computazionale per controllare una ipotesi di soluzione. La funzione ha output binario: Vero: significa che “puo' essere una soluzione” Falso: significa che sicuramente non e' una soluzione Questo metodo consente di evitare nella maggior parte dei casi la verifica completa dell'ipotesi di soluzione.

Come si applica alla ricerca di stringhe in un testo Si costruisce una funzione hash. Si calcola il valore hash del testo da cercare. Si calcola il valore di hash della sottostringa da controllare. Se il valore di hash coincide se effettivamente e' stata trovata una corrispondenza.

La “furbizia” della funzione hash per Rabin-Karp La funzione di hash viene scelta in modo che sia facile da “ricalcolare”. Per cercare “caro” nel primo verso dell'infinito occorre confrontare l'hash di “caro” con l'hash di “Semp”, poi con quello di “empr” poi con “mpre” e cosi' via. Dobbiamo scegliere una funzione per la quale sia possibile aggiungere un elemento in coda e toglierne uno in testa senza ricalcolare tutta la funzione. Le funzioni Hash per Rabin Karp possono essere complesse, noi qui ci limiteremo alla funzione “somma della codifica dei caratteri”. L'hash di “Semp” sara': ord('S')+ord('e')+ord('m')+ord('p'). Chiameremo questa funzione sumhash sumhash(“empr”)==sumhash(“Semp”)- sumhash(“S”)+sumhash(“r”) Se l'hash coincide con l'hash di “caro” puo' darsi che siamo in presenza di una soluzione. Se l'hash e' diverso sicuramente non e' una soluzione.

In Python def sumhash(s): return sum([ord(c) for c in s]) # count=0 # for c in s: # count+=ord(c) # return count def bfsearch(s,t): hs=sumhash(s) ht=sumhash(t[:len(s)]) if hs==ht and s==t[:len(s)]: return 0 for i in range(len(t)-len(s)): ht+=ord(t[i+len(s)])-ord(t[i]) if hs==ht and s==t[i+1:i+1+len(s)]: return i+1 return None if __name__=="__main__": t=input("scrivi il testo: ") s=input("scrivi la stringa da cercare: ") n=bfsearch(s,t) if n != None: print("la stringa e' presente alla posizione: ",n) else: print("la stringa non e' presente") In Python

Complessita' di Rabin Karp Con una funzione hash che minimizzi I falsi positivi la ricerca di una stringa in un testo ha una complessita' poco piu' che lineare.

Esistono altri algoritmi di ricerca! Questo insieme di algoritmi e' stato scelto per la valenza didattica. Sono semplici e forniscono un'idea delle strategie da usare e utilizzano conoscenze matematiche alla portata degli studenti del biennio delle scuole superiori. La trattazione della complessita' e' stata fatta in modo intuitivo senza inutili formalizzazioni

We are still creating art and beauty on a computer: the art and beauty of revolutionary ideas translated into (libre) code... renzo, rd235, iz4dje