Il 0% ha trovato utile questo documento (0 voti)
62 visualizzazioni45 pagine

01 Algoritmi e Loro Implementazione Java

Caricato da

ptfshyzm7w
Copyright
© © All Rights Reserved
Per noi i diritti sui contenuti sono una cosa seria. Se sospetti che questo contenuto sia tuo, rivendicalo qui.
Formati disponibili
Scarica in formato PDF, TXT o leggi online su Scribd
Il 0% ha trovato utile questo documento (0 voti)
62 visualizzazioni45 pagine

01 Algoritmi e Loro Implementazione Java

Caricato da

ptfshyzm7w
Copyright
© © All Rights Reserved
Per noi i diritti sui contenuti sono una cosa seria. Se sospetti che questo contenuto sia tuo, rivendicalo qui.
Formati disponibili
Scarica in formato PDF, TXT o leggi online su Scribd
Sei sulla pagina 1/ 45

Laboratorio di Algoritmi e Strutture Dati

a.a. 2023/2024
ALGORITMI E LORO IMPLEMENTAZIONE IN JAVA:
Introduzione

Giovanna Melideo
Università degli Studi dell’Aquila
DISIM
Premessa
▪ Ricordiamo che ogni algoritmo che risolve un determinato
problema può essere tradotto in un programma scritto in un
linguaggio di programmazione e che tale programma verrà
trasformato in un processo a tempo di esecuzione

2
Algoritmo
▪ Da un punto di vista computazionale, un algoritmo è una
procedura che prende dei dati in input e, dopo averli
elaborati, restituisce dei dati in output
 I dati devo essere organizzati e strutturati in modo tale
che la procedura che li elabora sia “parsimoniosa”
(efficiente)
 Il concetto di algoritmo è inscindibile da quello di dato

3
Complessità computazionale
▪ L’esecuzione di un algoritmo su un dato input richiede
risorse di tempo e di spazio il cui ammontare prende
il nome di complessità computazionale
▪ Un consumo eccessivo di risorse può pregiudicare la
possibilità di utilizzo di un algoritmo
▪ È di fondamentale importanza saper trovare una
soluzione algoritmica efficiente e possibilmente ottimale,
a specifici problemi ben formalizzati.

4
Obiettivo
“Dato un problema trovare un algoritmo corretto e funzionante che
ne descriva il relativo procedimento risolutivo e codificarlo in un
determinato linguaggio di programmazione”

“Dato un problema trovare tra i vari algoritmi risolutivi quello migliore


possibile confrontandoli dal punto di vista dell’«efficienza»
• Ogni algoritmo è caratterizzato da una complessità temporale e
spaziale rispetto alle «dimensioni dei dati di ingresso» (concetto
introdotto e approfondito in modo formale nel modulo di ASD)

5
Ciclo di sviluppo di codice algoritmico

▪ Lo sviluppo di software robusto ed efficiente per la


soluzione di problemi di calcolo segue uno schema
semplificato a due fasi che si avvicendano in un processo
ciclico:
• Fase progettuale
• Fase realizzativa
▪ Richiede (tra le altre cose): capacità di astrazione, familiarità
di strumenti matematici, padronanza del linguaggio di
programmazione, creatività.
6
Fase progettuale (1 di 5)
A. Si definiscono i requisiti del problema di calcolo che si
intende affrontare:
• Definire in modo preciso e non ambiguo il problema di
calcolo che si intende risolvere
• Identificare i requisiti dei dati in ingresso e di quelli in uscita
prodotti dall’algoritmo
• Già in questa fase è possibile valutare se un problema
complesso può essere decomposto in sottoproblemi
risolvibili in modo separato e indipendente

7
Fase progettuale: problema di ordinamento

Definizione dei requisiti di un problema di ordinamento:


▪ Input: un insieme di elementi qualsiasi A={a1, . . . , an}
su cui sia possibile definire una relazione di ordine totale
≤ (ossia una relazione riflessiva, antisimmetrica e
transitiva definita su ogni coppia di elementi
dell’insieme)
▪ Output: una permutazione degli elementi dell’insieme,
in modo tale che aih≤aik per ogni h≤k (h, k=1, 2, . . . ,n)
8
Fase progettuale: problema di ricerca
Definizione dei requisiti di un problema di ricerca:
▪ Input: un insieme di elementi qualsiasi A={a1, . . . , an}
e un elemento k (chiave)
▪ Output: indice i tale che:
• se k∈A , i ∈ {1, . . . ,n} e ai=k
• se k∉ A, i=-1

9
Fase progettuale (2 di 5)
B. Si studia la difficoltà intrinseca del problema, ossia
la quantità minima di risorse di calcolo (tempo e
memoria di lavoro) di cui qualsiasi algoritmo ha
bisogno per risolvere una generica istanza del problema
dato.
 Per molti problemi importanti non sono ancora noti limiti
inferiori precisi che ne caratterizzano la difficoltà intrinseca,
per cui non è ancora possibile stabilire se un algoritmo
risolutivo sia ottimo o meno

10
Fase progettuale (3 di 5)
C. Si progetta un algoritmo risolutivo, verificandone
formalmente la correttezza e stimandone le
prestazioni teoriche
• La complessità dell’algoritmo viene espressa in funzione
della dimensione n dell’istanza (analisi asintotica)
• Tra i vari algoritmi risolutivi, l’obiettivo è trovare l’algoritmo
che faccia il miglior uso possibile delle risorse di calcolo
disponibili (tempo di esecuzione ed occupazione di
memoria)
11
Fase progettuale (4 di 5)
▪ In grossi progetti software è fondamentale stimare le
prestazioni già a livello progettuale.
▪ Scoprire solo dopo la codifica che i requisiti prestazionali
non sono stati raggiunti potrebbe portare a conseguenze
disastrose o per lo meno molto costose.

12
Notazione asintotica

13
Fase progettuale (5 di 5)
▪ Il tempo/spazio di calcolo necessario alla risoluzione di un dato
problema (difficoltà intrinseca del problema): la quantità
minima di risorse di calcolo necessarie al caso peggiore per ogni
algoritmo che risolve una generica istanza del problema dato
▪ Il tempo/spazio di calcolo sufficiente alla risoluzione di un dato
problema: la quantità di risorse di calcolo necessarie al caso
peggiore ad uno specifico algoritmo che risolve una generica
istanza del problema dato
▪ Qualora la verifica della correttezza rilevi problemi o la stima delle
prestazioni risulti poco soddisfacente si torna al passo C (se non al
passo B…)
14
Fase realizzativa
▪ Si codifica l’algoritmo progettato in un linguaggio di
programmazione e lo si collauda per identificare eventuali errori
implementativi
▪ Si effettua un’analisi sperimentale del codice prodotto e se ne
studiano le prestazioni pratiche
▪ Si ingegnerizza il codice, migliorandone la struttura e l’efficienza
pratica attraverso opportuni accorgimenti
▪ Non è raro che l’analisi sperimentale fornisca suggerimenti utili
per ottenere algoritmi più efficienti anche a livello teorico.

15
Il problema dei duplicati (A)
Formulato come un problema di decisione
Input: una sequenza S di elementi qualsiasi S={s1, … , sn}
Output: true se esiste in S una coppia di elementi
duplicati (cioè esiste in S una coppia di indici distinti i, j ∈
{1, … ,n} tale che si = sj), false altrimenti.

16
Il problema dei duplicati (B)
▪ Difficoltà intrinseca del problema Ω(n): la
delimitazione inferiore banale di ogni algoritmo è
dell’ordine di grandezza di n (almeno la lettura dei dati
in ingresso)

17
Il problema dei duplicati (C)
▪ Analisi della correttezza e del tempo di esecuzione di
verificaDup per una generica istanza di dimensione n
(per n→∞)

Algoritmo verificaDup (sequenza S)


for each elemento x della sequenza S do
for each elemento y che segue in S do
if x=y then return true
return false

18
verificaDup: correttezza
▪ L’algoritmo confronta almeno una volta ogni coppia di
elementi, per cui se esiste un elemento che si ripete in S
verrà sicuramente trovato.

19
verificaDup: complessità (1 di 3)

Stima delle prestazioni: “Quanto tempo richiede l’algoritmo?”


▪ La metrica deve essere indipendente dalle tecnologie e dalle
piattaforme utilizzate (il numero di passi richiesto dall’algoritmo)
• “Misuriamo il tempo in secondi?” La risposta cambierebbe negli anni
o anche semplicemente su piattaforme diverse
▪ La metrica deve essere indipendente dalla particolare istanza
(tempo espresso in funzione della dimensione n dell’istanza,
notazione asintotica)
• “Lo sforzo richiesto per analizzare 10 elementi e per analizzarne 1
milione è lo stesso?”

20
verificaDup: complessità (2 di 3)

▪ Informalmente, per valutare l’ordine di grandezza “O(⋅)” o


tasso di crescita del tempo di esecuzione dell’algoritmo
verificaDup, possiamo contare quanti confronti
(“operazione dominante”) si eseguono al crescere di n
• O(1) (ordine di grandezza “costante”) per istanze più favorevoli
per l’algoritmo (caso migliore)
• O(n*n) (ordine di grandezza “quadratico” di n2) per istanze più
sfavorevoli (c.p. - caso peggiore)
▪ Esistono algoritmi più efficienti di verificaDup?
21
verificaDup: complessità (3 di 3)

▪ Osserviamo che se la sequenza in ingresso è ordinata


possiamo risolvere il problema più efficientemente
▪ gli eventuali duplicati sono in posizione consecutiva
▪ è sufficiente scorrere l’intera sequenza

22
Il problema dei duplicati (C-2)
Idea nuovo algoritmo:
▪ Ordinare la sequenza (sarà argomento del corso!)
• θ(n·log n), ordine di grandezza pseudo-polinomiale
▪ Cercare due elementi duplicati consecutivi
• O(n) nel c.p., ordine di grandezza lineare
▪ tempo di esecuzione complessivo: O(n·log n) nel c.p.

23
Il problema dei duplicati (C-2 continua)
Algoritmo verificaDupOrd (sequenza S)
ordina S in modo non-decrescente
for each elemento x della sequenza ordinata S,
tranne l’ultimo do
sia y l’ elemento che segue x in S
do if x=y then return true
return false

24
T(n)=O(n·log n) vs T(n)=O(n2)
n n log(n) n^2
10 33,22 100
100 664,39 10000
1000 9965,78 1000000
10000 132877,12 100000000
100000 1660964,05 10000000000
1000000 19931568,57 1000000000000
10000000 232534966,64 100000000000000
100000000 2657542475,91 10000000000000000
1000000000 29897352853,99 1000000000000000000
10000000000 332192809488,74 100000000000000000000
100000000000 3654120904376,10 10000000000000000000000
1000000000000 39863137138648,40 1000000000000000000000000
10000000000000 431850652335357,00 100000000000000000000000000
100000000000000 4650699332842310,00 10000000000000000000000000000
1000000000000000 49828921423310400,00 1000000000000000000000000000000
10000000000000000 531508495181978000,00 100000000000000000000000000000000

25
Altre funzioni a confronto

26
Misura delle prestazioni – cenni (1 di 4)
▪ Tempo di CPU relativo ad un programma: tempo effettivo
durante il quale la CPU lavora su quel programma
• Non comprende i tempi per l’accesso al disco, l’input/output,
• Non comprende il tempo speso dalla CPU per altri programmi
gestiti in contemporanea
▪ La velocità o frequenza di clock della CPU indica il numero
di operazioni elementari che la CPU è in grado di eseguire in
un secondo e si misura in Hertz
fCLOCK = # operazioni_elementari / tempo [Hertz]
• Giga Hertz: 1GHz = 109Hz = 109 cicli/s
27
Misura delle prestazioni – cenni (2 di 4)

Ordini di grandezza:
▪ La velocità di clock del primo
microprocessore della storia,
l’Intel 4004, era di 740 KHz
▪ Le CPU dei computer
moderni raggiungono i 5
GHz.

28
132.877

29
Misura delle prestazioni – cenni (4 di 4)
▪ Se un elaboratore esegue ~109 operazioni/sec:
N Time (N) Time (N2) Time (N3) Time (2N)
50 … 25⋅10-7= 2,5 125⋅10-6 = > 106 sec > 10 gg
µs 125 µs
100 10-7 sec= 0,1 10-5 sec= 10 10-3 sec = 1 > 1021 sec ~ 1016 gg >
µs µs ms 1013 anni

1000 10-6 sec = 1 10-3 sec = 1 1 sec


µs ms

30
Il problema dei duplicati: realizzazione
▪ Fase realizzativa: Alcune scelte, se non ben ponderate,
potrebbero avere un impatto cruciale sui tempi di esecuzione
▪ Implementazione dell’algoritmo verificaDup mediante
liste: S è rappresentata tramite un oggetto della classe
LinkedList che implementa l’interfaccia java.util.List
fornita come parte del Java Collections Framework
▪ Il metodo get() consente l’accesso agli elementi di S in base
alla loro posizione nella lista.

31
Implementazione: verificaDupList
public static boolean verificaDupList (LinkedList S){
for (int i=0; i<S.size(); i++){
Object x=S.get(i);
for (int j=i+1; j<S.size(); j++){
Object y=S.get(j);
if (x.equals(y)) return true;
}
}
return false;
}

32
Implementazione basata su ordinamento
▪ Utilizziamo anche la classe java.util.Collections, che
fornisce metodi statici che operano su collezioni di oggetti
▪ In particolare fornisce il metodo sort, che si basa su una
variante dell’algoritmo mergesort

33
Implementazione: verificaDupOrdList
public static boolean verificaDupOrdList (LinkedList S){
Collections.sort(S);
for (int i=0; i<S.size()-1; i++)
if (S.get(i).equals(S.get(i+1))) return true;
return false;
}

34
Collaudo e analisi sperimentale (1 di 7)
▪ L’implementazione di un algoritmo va collaudata in modo
da identificare eventuali errori implementativi, ed
analizzata sperimentalmente, possibilmente su dati di
test reali
▪ L’analisi sperimentale delle prestazioni va condotta
seguendo una corretta metodologia per evitare
conclusioni errate o fuorvianti

35
Collaudo e analisi sperimentale (2 di 7)
Obiettivi dell’analisi sperimentale:
▪ Come raffinamento dell’analisi teorica o in sostituzione dell’analisi teorica
quando questa non può essere condotta con sufficiente accuratezza
▪ Per effettuare un confronto più preciso tra algoritmi apparentemente simili
(stimare quali sono le costanti nascoste dalla notazione asintotica)
▪ Per studiare le prestazioni su dati di test derivanti da applicazioni pratiche o da
scenari di caso peggiore. Spesso si ottengono risultati sorprendenti che la cui
spiegazione consente di raffinare e migliorare l’analisi teorica
▪ Se un risultato sembra in contraddizione con l’analisi teorica può essere utile
condurre ulteriori esperimenti

36
Collaudo e analisi sperimentale (3 di 7)
▪ Misurazione dei tempi (a scopo didattico in base all’orologio di
sistema e basato sul clock del processore): un aspetto cruciale è la
granularità delle funzioni di sistema usate per misurare i tempi. Se i
tempi di esecuzione sono troppo bassi per ottenere stime significative,
basta misurare il tempo totale di una serie di esecuzioni identiche dello
stesso codice e dividere il tempo totale per il numero di esecuzioni
▪ Usiamo il metodo java.lang.System.nanoTime() che fornisce un
valore di tipo long (nanosecondi) per prendere i tempi prima e dopo
l’esecuzione secondo il seguente schema:
long tempoInizio = System.nanoTime();
[porzione di codice da misurare]
long tempo=System.nanoTime()- tempoInizio;

37
Collaudo e analisi sperimentale (4 di 7)
▪ Siamo interessati alla relazione generale esistente tra tempo di
esecuzione e la dimensione dei dati da elaborare
▪ Si eseguono esperimenti indipendenti con molti diversi dati in
ingresso di diverse dimensioni
▪ Si visualizzano i risultati dell’esecuzione sotto forma di grafico
cartesiano dove la coordinata x rappresenta la dimensione n dei
dati in ingresso e la coordinata y il tempo di esecuzione t
▪ Il grafico ottenuto consente spesso di intuire la relazione esistente
tra la dimensione del problema ed il tempo di esecuzione
dell’algoritmo che lo risolve

38
Collaudo e analisi sperimentale (5 di 7)
▪ Un’analisi sperimentale condotta su sequenze di numeri interi
distinti generati in modo casuale ha evidenziato il vantaggio
derivante dal progetto di algoritmi efficienti:
• verificaDupOrdList molto più efficiente di verificaDupList

▪ I tempi di esecuzione predetti teoricamente sono rispettati? No


• La curva dei tempi di esecuzione relativa al metodo verificaDupList
somiglia alla funzione c*n3 (non a c*n2)
• La curva dei tempi di esecuzione relativa al metodo verificaDupOrdpList
somiglia alla funzione c*n2 (non a c*n*logn)
▪ Perché ?
39
Collaudo e analisi sperimentale (6 di 7)
▪ La contraddizione è solo apparente!
▪ Nell’analisi teorica abbiamo tacitamente assunto che procurarsi gli
elementi in posizione i e j richiedesse tempo «costante» O(1)
▪ Controllando i dettagli dell’implementazione di get() ci si accorge
che il metodo, avendo a disposizione solo la posizione di un
elemento e non il puntatore ad esso, per raggiungere l’elemento
in quella posizione è costretto a scorrere la lista dall’inizio
(esattamente i elementi)
▪ Raggiungere l’elemento i-mo costa un tempo lineare θ(i)

40
Collaudo e analisi sperimentale (7 di 7)
▪ Dunque il tempo di esecuzione di verificaDupList diventa
proporzionale a n3 , cioè:
Σi=1..n(i+Σj=(i+1)..n j)=O(n3)
▪ Vedremo che è possibile migliorare l’implementazione (tempo di
esecuzione quadratico O(n2) ) !
▪ Discorso analogo vale per il metodo verificaDupOrdList.

41
Messa a punto e ingegnerizzazione
▪ Richiede in particolare di decidere l’organizzazione e la
modalità di accesso ai dati
▪ In riferimento al nostro esempio, dove la sequenza S è
rappresentata mediante un oggetto LinkdList, l’uso
incauto del metodo get() ha reso le implementazioni
inefficienti
▪ Eliminare questa fonte di inefficienza: convertire la lista
in array!

42
Implementazione: verificaDupArray
public static boolean verificaDupArray (List S){
Object[] T = S.toArray();
for (int i=0; i<T.length(); i++){
Object x=T[i];
for (int j=i+1; j<T.length; j++){
Object y=T[j];
if (x.equals(y)) return true;
}
}
return false;
}
43
Implementazione: verificaDupOrdArray
public static boolean verificaDupOrdArray (List S){
Object[] T = S.toArray();
Arrays.sort(T);
for (int i=0; i<T.length(); i++){
if (T[i].equals(T[i+1])) return true;
}
}
return false;
}
▪ I tempi di esecuzione in questo caso sono perfettamente allineati con la
predizione teorica!
44
Domande?

Giovanna Melideo
Università degli Studi dell’Aquila
DISIM

Potrebbero piacerti anche