Sei sulla pagina 1di 9

Algoritmi e Strutture Dati

Algoritmi e Complessità a cura di M. Cecilia Verri

Indice
1 Introduzione 2

2 Complessità degli Algoritmi 3

3 Ricorrenze di base 6

Algoritmi e Complessità, a cura di M. Cecilia Verri 1


1 Introduzione
La soluzione di un problema con il computer può essere schematizzata in tre attività fondamentali:
analisi, programmazione ed elaborazione. Solo l’ultima fase è demandata alla macchina, mentre
le prime due sono interamente a carico dell’utente.
La fase di analisi richiede che il problema sia analizzato logicamente in modo da assicurarsi che
sia ben posto e completo, ovvero che non richieda dati non disponibili o metodi di risoluzione
sconosciuti, e che le informazioni necessarie siano formalizzate in strutture (astratte) di dati e in
metodi di risoluzione (algoritmi). Uno degli obiettivi di questa fase è dunque la correttezza, cioè
la progettazione di un algoritmo e di strutture dati che operino in modo corretto su tutti i possibili
dati che possono presentarsi nel dominio del problema considerato. Un secondo fondamentale
obiettivo della fase di analisi è costituito dall’efficienza: il metodo di risoluzione deve essere anal-
izzato dal punto di vista della complessità ovvero delle risorse computazionali che esso richiede
(quantità di memoria necessaria e tempo di calcolo). Obiettivo di questa fase è dunque progettare
un metodo corretto ed efficiente per la soluzione di un problema. In genere il metodo non deve
dipendere né dalla macchina su cui verrà implementato né dal linguaggio di programmazione che
verrà utilizzato.
‘In informatica, il termine algoritmo si riferisce proprio al metodo per la soluzione di un problema
adatto ad essere implementato sotto forma di programma. Gli algoritmi sono il pane dell’infor-
matica, in quanto rappresentano l’oggetto di studio principale in molte aree del settore (se non
della totalità)’ [Sedgewick, 2003].
La fase di programmazione vede invece la traduzione delle strutture astratte in strutture concrete
gestibili dall’elaboratore e la traduzione degli algoritmi in programmi eseguibili. Obiettivi di
questa fase sono la robustezza, l’adattabilità e la riusabilità.
Il soggetto principale del corso saranno proprio le strutture dati, gli algoritmi e la loro complessità.
Parlando in modo informale, una struttura dati è un modo per organizzare e accedere ai dati, men-
tre un algoritmo è una procedura descritta passo per passo per eseguire un certo compito in una
quantità finita di tempo. Questi due concetti sono fondamentali in informatica e dunque più di
un corso viene dedicato alla discussione di schemi e di principi per lo sviluppo di buone strutture
dati e buoni algoritmi in modo da fornire la conoscenza e l’esperienza necessarie allo sviluppo
di soluzioni ai più frequenti problemi informatici. Lo studio degli algoritmi e delle strutture più
idonee a rappresentare i dati del problema procede di pari passo e, spesso, la scelta di una strut-
tura dati adeguata è di fondamentale importanza per l’organizzazione di un buon algoritmo. Una
possibile definizione formale di algoritmo è la seguente:

Definizione 1.1 Un algoritmo è un insieme finito di istruzioni definite e non ambigue che, eseguito
a partire da assegnate condizioni iniziali, produce il risultato corrispondente e termina in tempo
finito.

Questa definizione sottolinea le tre proprietà fondamentali di un algoritmo:

• finitezza: ogni istruzione deve poter essere eseguita in una quantità finita di tempo, e deve
essere eseguita un numero finito di volte;

Algoritmi e Complessità, a cura di M. Cecilia Verri 2


• generalità: l’algoritmo deve poter fornire la soluzione a tutti i problemi appartenenti ad una
data classe, perciò deve poter essere applicabile a qualsiasi insieme di dati appartenenti al
dominio del problema;

• non ambiguità: devono essere definiti in modo non ambiguo i passi da eseguire per ottenere
i risultati voluti; il significato delle istruzioni deve essere interpretabile in modo univoco.

Talvolta esistono diversi algoritmi per risolvere uno stesso problema: la scelta dell’algoritmo
migliore può essere un compito molto complicato e può richiedere una analisi matematica sofisti-
cata. Lo studio di tali questioni è detto analisi degli algoritmi: analizzare un algoritmo ha il
significato di prevedere le risorse di tempo e di quantità di memoria che l’algoritmo richiede. Con
l’evolversi della tecnologia, si sono rese disponibili memorie sempre più capienti e a prezzi sem-
pre più bassi (anche se, nello stesso tempo, è cresciuta la dimensione dei problemi che vengono
affrontati), perciò risulta spesso più interessante misurare il tempo di calcolo. I problemi che si
può pensare di risolvere con un computer sono ovviamente infiniti e di conseguenza infiniti sono
i corrispondenti algoritmi. L’esperienza ha però dimostrato che esistono un certo numero di prob-
lemi fondamentali che si presentano molto frequentemente come sottoproblemi di altri: perciò è
utile studiare in modo approfondito i metodi di risoluzione di tali problemi, sia per riutilizzarli
all’interno di problemi più articolati, sia per acquisire l’esperienza necessaria a sviluppare metodi
per la risoluzione di altri problemi.

2 Complessità degli Algoritmi


Nella progettazione di un algoritmo è necessario poter valutare quanto ‘buona’ è la soluzione
proposta. La bontà di un algoritmo può essere valutata sia in senso qualitativo sia in senso
quantitativo.
Alcune buone qualità di un algoritmo possono essere le seguenti:

1. essere corretto;

2. essere una soluzione semplice ma generale;

3. possedere una implementazione chiara e concisa;

4. essere facilmente modificabile;

5. non essere legato nella sua implementazione ad un particolare computer.

Questi aspetti qualitativi sono importanti, ma è molto importante anche stabilire delle misure quan-
titative che consentano di misurare a priori le prestazioni di un algoritmo, sia per poterlo con-
frontare con altri algoritmi che risolvono lo stesso problema, sia per poter predire quali saranno le
risorse di tempo e di spazio necessari alla sua esecuzione.
Per misurare le prestazioni di un algoritmo è necessario fissare un modello di calcolo che consenta
di valutare, in funzione della dimensione del problema, gli aspetti fondamentali del calcolo. Nor-
malmente i problemi hanno una dimensione naturale, che viene indicata con n e corrisponde alla

Algoritmi e Complessità, a cura di M. Cecilia Verri 3


quantità dei dati che vengono processati, dunque quello che dobbiamo riuscire a fare è descrivere
le risorse usate (tempo e spazio) al variare di n. Per valutare la complessità di un algoritmo è nec-
essario stabilire qual è l’operazione astratta principale su cui è basato l’algoritmo stesso e questo
deve prescindere dalla sua particolare implementazione: ad esempio verrà valutato il numero di
confronti fatti da un algoritmo di ordinamento e non il tempo impiegato da una particolare macchi-
na per eseguire una operazione di confronto. Per rappresentare le funzioni che delimitano il tempo
di esecuzione di un algoritmo si utilizza la notazione d’ordine nota come notazione O grande. Un
algoritmo la cui operazione dominante è eseguita cn2 volte, con c costante non nulla, si dice che
ha complessità di ordine n2 e si indica con O(n2 ). Formalmente:

Definizione 2.1 Una funzione g(n) ∈ O( f (n)) se esiste una costante c 6= 0 ed un valore n0 > 0 per
cui g(n) ≤ c f (n) per ogni n > n0

Ad esempio, secondo questa definizione, la funzione g(n) = 2n2 + 3n + 1 ∈ O(n2 ): basta infatti
prendere c = 6 e già per n = 1 si ha g(n) ≤ 6n2 . In pratica, g(n) è O( f (n)) se g cresce al massimo
quanto f .
Nell’analizzare un algoritmo si considerano normalmente due misure: il comportamento nel caso
peggiore e nel caso medio ed entrambe le misure possono riferirsi sia alla complessità in spazio sia
a quella in tempo.

Definizione 2.2 La complessità nel caso peggiore per una certa dimensione n del problema, cor-
risponde alla massima complessità incontrata in tutti i problemi di dimensione n.

L’analisi del caso peggiore è in generale abbastanza semplice: per molti algoritmi non è difficile
immaginare quale configurazione dei dati richiede il maggior numero di operazioni. Essa inoltre
rappresenta un limite superiore alle prestazioni offerte dall’algoritmo e quindi consente di stabilire
le risorse massime necessarie. Spesso però è utile sapere il comportamento dell’algoritmo mediato
su tutti i problemi di dimensione n; quando si confrontano due algoritmi che risolvono lo stesso
problema, in generale si preferisce l’algoritmo che ha complessità media più bassa. Purtroppo
però, mentre è in generale facile immaginare la configurazione dei dati che porta al comportamento
peggiore, l’analisi del caso medio richiede spesso un’analisi combinatoria molto complessa. Come
esempio molto semplice consideriamo il problema di ricercare un elemento x in una lista di n
elementi (x1 , . . . xn ). Possiamo confrontare x con x1 , poi con x2 , e cosı̀ via finché non si trova
l’elemento cercato (ricerca con successo) oppure si è scorso tutto l’insieme senza trovarlo (ricerca
senza successo). Il caso peggiore della ricerca con successo si ha quando x = xn e in questo caso
l’algoritmo dovrà esaminare tutti gli n elementi prima di terminare, e dunque il caso pessimo ha
una complessità proporzionale ad n (la stessa complessità si ha per la ricerca senza successo).
L’esame del caso medio richiede un tipo di analisi probabilistica, in quanto è necessario sapere con
che probabilità il valore x può trovarsi nella prima posizione della lista, nella seconda, nella terza e
cosı̀ via. Facendo l’ipotesi di equiprobabilità, ovvero supponendo che la probabilità che x si trovi
nella i-esima posizione della lista sia pari a 1/n per ogni valore di i compreso tra 1 ed n, avremo
che il costo medio della ricerca Cn è dato dalla somma di tutti i possibili costi di ricerca, ciascuno
moltiplicato per la corrispondente probabilità:
1 1 1 1 1 n(n + 1) 1 n + 1
Cn = 1 · + 2 · + 3 · + · · · + n · = (1 + 2 + 3 + · · · + n) = =
n n n n n 2 n 2

Algoritmi e Complessità, a cura di M. Cecilia Verri 4


L’analisi di complessità nel caso medio è dunque in generale più complicata rispetto all’analisi nel
caso peggiore. Inoltre può succedere che la complessità mediata su tutti i problemi di dimensione
n non corrisponda alla realtà ovvero ai dati che mediamente si incontrano in pratica. Ad esempio,
l’ipotesi di equiprobabilità fatta nel caso del problema della ricerca di un elemento, potrebbe non
essere realistica.
La dipendenza da n può essere di vario tipo: fra i casi migliori possiamo includere la dipendenza
logaritmica, mentre fra i casi peggiori c’è sicuramente la dipendenza esponenziale:
log2 n n n log2 n n2 2n
1 2 2 4 4
3,322 10 33,22 100 > 103
6,644 100 664,4 10.000 >> 1030
9,966 1.000 9.966 1.000.000 >> 10300
13,287 10.000 132.877 100.000.000 >> 103000

Supponiamo di utilizzare un computer che esegue 1 milione di operazioni al secondo: un problema


di dimensione n = 10.000 affrontato con un algoritmo di complessità logaritmica viene risolto in 13
microsecondi mentre un problema con n = 100 affrontato con un algoritmo esponenziale richiede
circa 1016 anni.
Il grosso guaio degli algoritmi esponenziali è che essi sfidano tutti i nostri sforzi per risolvere prob-
lemi di dimensioni sempre più grandi. Un’argomentazione classica è la seguente. Supponiamo di
avere tre problemi risolti da tre algoritmi di complessità lineare, quadratica ed esponenziale rispet-
tivamente; supponiamo inoltre che con uno degli attuali elaboratori più veloci si arrivi a risolvere,
in un’ora di calcolo, ciascun problema con n = 30. Ad esempio, il primo problema può essere
il complicato calcolo dei parametri di certe travi, ed ogni trave richiede 2 minuti di elaborazione.
Il secondo problema richiede la somma di matrici 30 × 30 e il calcolo di ciascuna componente
richiede 40 secondi. Il terzo problema, infine, può essere quello del commesso viaggiatore che in
un mese deve visitare 30 città minimizzando il percorso complessivo. Supponiamo che un giorno,
un gruppo di celebri scienziati inventi un elaboratore 1000 volte più veloce di tutti quelli esisten-
ti. Questo elaboratore viene impiegato per risolvere gli stessi tre problemi e, per ciascuno, lo si
fa lavorare per un’ora. A quale dimensione potrà arrivare per ciascuno dei tre problemi? Per il
problema lineare, il conto è semplice e da n = 30 si passa di botto a n = 30.000 con grande soddis-
fazione per gli inventori del nuovo elaboratore. Per il secondo problema, poiché un’ora del nuovo
elaboratore equivale a 1000 ore del vecchio e poiché in un’ora il vecchio elaboratore faceva circa
302 operazioni, il nuovo valore di n deve essere tale che 302 · 1000 = n2 , ovvero n ≈ 950, e anche
in questo caso il miglioramento è evidente. Per il terzo problema, un’ora di lavoro corrispondeva a
230 operazioni, e dunque deve essere 230 · 1000 = 2n , da cui, passando ai logaritmi si trova n ≈ 40.
In questo caso il nuovo elaboratore ha prodotto un miglioramento quasi trascurabile, con profonda
delusione del gruppo di scienziati.
In generale, è possibile definire in modo preciso cosa si intende per algoritmi ‘facili’, cioè di bassa
complessità, e cosa si intende invece per algoritmi ‘difficili’, o di alta complessità di esecuzione.
Si dicono trattabili, cioè si considerano facili, gli algoritmi che hanno complessità minore o uguale
a O(n p ) per qualche valore di p, mentre si dicono intrattabili tutti quelli per i quali un p del genere
non esiste. Si dice anche che gli algoritmi trattabili hanno complessità polinomiale, e questo deriva

Algoritmi e Complessità, a cura di M. Cecilia Verri 5


da quell’n p che sta nella definizione.
Quelli intrattabili, allora, non sono polinomiali e la loro complessità cresce più di qualsiasi poli-
nomio. Come si sa, una tra le più piccole funzioni con questa proprietà è la funzione esponenziale
en , e quindi gli algoritmi intrattabili si dicono anche esponenziali. In effetti, la loro complessità
può essere anche più che esponenziale, ma la definizione deve essere presa in questi termini. I
problemi trattabili e intrattabili si dicono decidibili perché, se prescindiamo dal tempo richiesto
dalla loro soluzione, siamo sicuri che la soluzione una volta o l’altra la troveremo.
Non per tutti i problemi la situazione è cosı̀ favorevole. Ad esempio, supponiamo di voler trovare
la 10100 -esima coppia di numeri gemelli. Nella sequenza dei numeri primi appaiono con una
certa frequenza delle coppie (p, p + 2), dette gemellari, come 5 e 7 oppure 29 e 31. Nessuno è
riuscito a dimostrare se le coppie di numeri gemelli sono in numero finito o infinito; con l’elab-
oratore se ne sono trovate in grandissimo numero, ma ovviamente questo non dimostra che siano
infinite. Scrivere un programma per trovare l’n-esima coppia gemellare è semplice e anche se la
sua complessità è molto alta (purtroppo, i metodi sicuri per verificare se un numero è primo sono
piuttosto lenti) possiamo immaginare di aver di fronte a noi l’eternità e far partire il programma
con n = 10100 . Chiaramente, se il numero di coppie gemellari è infinito o è maggiore di 10100 ,
fra qualche secolo o qualche millennio il nostro calcolatore si fermerà e ci dirà qual è la coppia
gemellare trovata. Ma se per caso il numero delle coppie gemellari è inferiore a 10100 , il program-
ma non si fermerà mai e continuerà a cercare all’infinito, o almeno fino a quando l’elaboratore
avrà energia per seguitare a macinare numeri. Allo stato attuale delle nostre conoscenze, possiamo
dire che la decidibilità o meno di questo problema non è nota in questo momento, ma dipende
dalla nostra abilità a dimostrare che è vera o falsa l’affermazione che le coppie gemellari sono in
numero infinito.
Tuttavia esistono delle affermazioni, che si dicono indecidibili, per le quali non possiamo dire
né che sono vere né che sono false, nel senso che non può esistere alcuna dimostrazione del-
l’asserzione né della sua negazione. Ma tutto questo va oltre gli scopi di questo corso.

3 Ricorrenze di base
Come vedremo in seguito, molti algoritmi sono basati sul principio di scomporre ricorsivamente
un problema in problemi più piccoli, e di comporre poi le soluzioni dei sottoproblemi per ottenere
la soluzione del problema di partenza (divide et impera). Perciò la complessità dell’algoritmo è
determinata dalla complessità e dal numero dei sottoproblemi e dal costo della decomposizione.
Per problemi tipicamente ricorsivi si ha che la complessità Cn relativa ad un input di grandezza n,
dipende dalla complessità relativa ad input di dimensioni inferiori: questo porta a scrivere delle
relazioni di ricorrenza. Alcune di queste relazioni sono molto frequenti, e quindi è interessante
vedere come possono essere risolte.
Relazione 1 - è tipica di algoritmi ricorsivi che ciclano sui dati in input eliminandone uno alla
volta:
Cn = Cn−1 + n, n≥2
C1 = 1

Algoritmi e Complessità, a cura di M. Cecilia Verri 6


Soluzione
Applichiamo il metodo noto come espansione telescopica, ovvero sostituiamo al posto di Cn−1 nel
membro destro della relazione, la stessa espressione calcolata per n − 1: infatti se Cn = Cn−1 + n
allora Cn−1 = Cn−2 + n − 1, e quindi sostituendo avremo:

Cn = Cn−1 + n = Cn−2 + (n − 1) + n

Ripetendo questa sostituzione:

Cn = Cn−1 + n = Cn−2 + (n − 1) + n = Cn−3 + (n − 2) + (n − 1) + n =

= Cn−4 + (n − 3) + (n − 2) + (n − 1) + n = . . . = C1 + 2 + 3 + . . . + (n − 3) + (n − 2) + (n − 1) + n =
n(n + 1)
= 1 + 2 + 3 + . . . + (n − 3) + (n − 2) + (n − 1) + n =
2
Perciò
n2
Cn ≈
2
Relazione 2 - è tipica di algoritmi ricorsivi in cui ad ogni passo, con una sola operazione, viene
dimezzato l’insieme dei dati su cui procedere:

Cn = C n2 + 1, n≥2

C1 = 0
Soluzione
Facciamo un’ipotesi che ci semplifica i calcoli, ovvero supponiamo che n sia una potenza esatta di
2 e quindi n = 2N . Sostituendo nella relazione questa espressione di n e applicando l’espansione
telescopica, otteniamo:

C2N = C2N−1 + 1 = C2N−2 + 1 + 1 = C2N−3 + 3 = . . . = C20 + N = N

Dunque, poiché N = log2 n,


Cn ≈ log2 n

Relazione 3 - anche in questo caso l’input viene dimezzato ad ogni passo ma per eseguire questa
operazione è necessario esaminare tutti i dati:

Cn = C 2n + n, n≥2

C1 = 1
Soluzione
Anche in questo caso semplifichiamo supponendo che n = 2N :

C2N = C2N−1 + 2N = C2N−2 + 2N−1 + 2N =

= C2N−3 + 2N−2 + 2N−1 + 2N = . . . = C20 + 21 + 22 + . . . + 2N−2 + 2N−1 + 2N =

Algoritmi e Complessità, a cura di M. Cecilia Verri 7


N
= ∑ 2k = 2N+1 − 1 = 2n − 1
k=0
Perciò
Cn ≈ 2n

Relazione 4 - si presenta in algoritmi che analizzano tutti i dati dividendoli in due parti e poi
proseguono separatamente su entrambi:

Cn = 2C n2 + n, n≥2

C1 = 0
Soluzione
Anche in questo caso semplifichiamo supponendo che n = 2N ; sostituendo otteniamo:

C2N = 2C2N−1 + 2N

dividiamo entrambi i membri per 2N e poi facciamo l’espansione:


C2N C2N−1 C2N−2 C20
= + 1 = + 2 = . . . = +N
2N 2N−1 2N−2 20
quindi:
C2N = 2N · N
perciò
Cn ≈ n log2 n

Relazione 5 - l’operazione di divisione in due metà avviene con costo costante e poi l’algoritmo
prosegue su entrambi le parti:
Cn = 2C n2 + 1, n≥2

Soluzione
Come nel caso precedente sostituiamo n = 2N :

C2N = 2C2N−1 + 1

e poi dividiamo per 2N :


C2N C2N−1 1 C2N−2 1 1
N
= N−1 + N = N−2 + N−1 + N =
2 2 2 2 2 2
C20 1 1 1 1
= ... = + + + . . . + +
20 2 22 2N−1 2N
Perciò, se C1 = 1 avremo:
N  k
C2N 1 1 1 1 1
= 1+ + 2 +...+ N = ∑ = =2
2 N 2 2 2 k=0 2 1 − 12

Algoritmi e Complessità, a cura di M. Cecilia Verri 8


e quindi:
Cn ≈ 2n
Invece, se C1 = 0:
N  k
C2N 1 1 1 1
N
= + 2 +...+ N = ∑ −1 = 2−1 = 1
2 2 2 2 k=0 2

e quindi:
Cn ≈ n

Algoritmi e Complessità, a cura di M. Cecilia Verri 9