Sei sulla pagina 1di 42

I Compendi OpenSource

di Giacomo Marciani

Ingegneria
degli
Algoritmi

Teoria, Formulario e Suggerimenti Pratici

Introduzione informale agli algoritmi

Un algoritmo un insieme di istruzioni, denite passo per passo, in modo


tale da poter essere eseguite meccanicamente, e tali da produrre un determinato
risultato. E' una sequenza di passi di calcolo che, dato un input restituisce un
output, prodotto dalla trasformazione dell'input stesso.
L'analisi di un algoritmo permette di determinare una stima prestazionale
dell'algoritmo, gi a livello progettuale. Essa esamina l'impiego di risorse, in termini di tempo di esecuzione e spazio di memoria in funzione della distribuzione

dell'input , orendo cos garanzie matematiche sulle prestazioni dell'algoritmo.


L'analisi di un algoritmo deve denire una metrica indipendente dalle tecnolo-

gie, dalle piattaforme, dai compilatori ed assemblatori utilizzati : misuriamo


dunque il tempo di esecuzione in termini di linee di codice eseguite.

Classi algoritmiche

Un algoritmo intende risolvere un problema. Uno prob-

lema pu essere risolto da algoritmi dierenti. Individuiamo anzitutto tre classi


algoritmiche:

algoritmo numerico: determina la soluzione di una funzione che es-

prime il problema.

Laddove si lavori con numeri reali, rischiamo errori

di arrotondamento dovuti alla limitata precisione di rappresentazione numerica nei calcolatori.

algoritmo ricorsivo: determina la soluzione di una funzione ricorsiva

che esprime il problema.

Un algoritmo ricorsivo pu essere analizzato

esprimendolo in forma di relazione di ricorrenza, la cui soluzione


esprime il tempo di esecuzione dell'algoritmo ricorsivo.

Una ricorsione

pu essere rappresentata da un albero della ricorsione radicato sulla


prima chiamata, i cui nodi hanno un glio per ogni chiamata ricorsiva, e
le cui foglie rappresentano la base della ricorsione. Un algoritmo ricorsivo
rischia di calcolare ripetutamente la soluzione di uno stesso sottoproblema.
Il tempo di esecuzione dell'algoritmo uguale al tempo speso all'interno
della routine pi il tempo speso per le chiamate ricorsive.

Lo spazio di

memoria occupato in un certo istante e dato dallo spazio totale usato da

tutte le chiamate ricorsive attive in quell'istante .

algoritmo iterativo: determina la soluzione del problema sfruttando

i cicli.

Un algoritmo iterativo pu realizzare agevolmente la program-

mazione dinamica , non rischiando dunque di calcolare ripetutamente la

1 la distribuzione dell'input denita in


2 una metrica basata sui secondi o sulle

termini di dimensione e probabilit dell'istanza.


istruzioni macchina dipenderebbe dai compilatori
ed assemblatori utilizzati. Altre metriche come tempi di programmazione e lunghezza del
codice sono invece arontate dall'ingegneria del software.
3 le chiamate ricorsive attive in un certo istante formano un cammino nell'albero della
ricorsione.
4 la programmazione dinamica una tecnica algoritmica che punta a risolvere un problema
P calcolando una sola volta le soluzioni dei sottoproblemi pi , memorizzandole in variabili
locali, ed usandole per risolvere interamente P .
2

soluzione di uno stesso sottoproblema. Il tempo di esecuzione dell'algoritmo


uguale al tempo speso all'interno della routine fuori dal ciclo pi il tempo
totale impiegato dal ciclo.

Lo spazio di memoria occupato dato dalla

dichiarazione delle variabili e dalle chiamate di allocazione.

Lemmi sull'albero della ricorsione

Dato un albero della ricorsione

Tn :

il numero di foglie in

Tn

dato esattamente dalla relazione di ricorrenza.

se si tratta di un albero binario, allora il numero di nodi interni sempre


uguale al numero di foglie diminuito di uno.

Notazione asintotica

La notazione asintotica un'astrazione matemat-

ica mutuata dal calcolo innitesimale che ci permette di confrontare algoritmi


diversi, stimando un ordine di grandezza delle prestazioni e trascurando i dettagli di basso livello. Quando lo pseudocodice si allontana eccessivamente dalle
primitive del linguaggio reale, la notazione asintotica pu nascondere dettagli
implementativi che possono incidere fortemente sulle prestazioni dell'algoritmo.
Deniamo tre ordini di grandezza in notazione asintotica:

g(n) O(f (n)):

g(n)

vuol dire che

cresce al pi come

f (n).

Si presta ad

esprimere un upper bound.

O(f (n)) = {g(n) : c > 0 n0 0|g(n) cf (n)n n0 }


g(n) (f (n)):

vuol dire che

g(n) cresce almeno

come

f (n).

(1)

Si presta ad

esprimere un lower bound.

(f (n)) = {g(n) : c > 0 n0 0|g(n) cf (n)n n0 }


g(n) (f (n))5 :

vuol dire che

g(n)

cresce esattamente come

(2)

f (n)6 .

(f (n)) = {g(n) : c1 , c2 > 0 n0 0|c1 f (n) g(n) c2 f (n)n n0 }


(3)

O(f (n)) ((f (n)))


n e rispetto ad una certa risorsa di
calcolo r , se la quantit di risorsa r suciente per eseguire A su una qualunque
istanza di dimensione n verica la relazione r(n) = O(f (n)) (r(n) = (f (n)))
[r(n) = (f (n))]. Analogamente diciamo che un problema P ha una complessit
computazionale O(f (n)) ((f (n))) [(f (n))] rispetto ad una certa risorsa di
calcolo r se esiste un algoritmo A che risolve P il cui costo di esecuzione rispetto
a quella risorsa O(f (n)) ((f (n))) [(f (n))]. Diciamo che un algoritmo A
un algoritmo ottimo se risolve un problema P con complessit (f (n))
rispetto ad una certa risorsa r , con un costo di esecuzione O(f (n)) rispetto a
Diciamo quindi che un algoritmo

ha costo di esecuzione

[(f (n))] su istanze di ingresso di dimensione

quella risorsa di calcolo.

5 pu essere denita come una relazione di equivalenza sulle funzioni, in quanto riessiva,
simmetrica e transitiva.
6 quindi g(n) (f (n)) g(n) O(f (n)) g(n) (f (n)).

Dimensione dell'istanza di input

La dimensione dell'istanza di input

indica il numero di bit necessari a rappresentare nel calcolatore una data istanza
di input. Poich sono necessari

plog2 nq bit per rappresentare l'intero n, diciamo

che la dimensione dell'input

|I| = O(log n)
Il tempo di esecuzione

Criteri di costo

(4)

T (n) pu dover tenere conto della dimensione dell'input.

Possiamo scegliere di misurare il costo di esecuzione di un

algoritmo in base a:

ciascuna operazione viene considerata

criterio di costo uniforme:

come un singolo passo, indipendentemente dalla dimensione degli operandi


coinvolti.

criterio di costo logaritmico:

dimensione

|I|

criterio di costo

|I|

ciascuna operazione dipende dalla

degli operandi coinvolti.

c log n:

ciascuna operazione dipende dalla dimensione

degli operandi coinvolti, ma tale dimensione limitata a

c log n

bit per

rappresentare numeri interi, restringendo cos le operazioni permesse sui


numeri in virgola mobile. Questo l'approccio che adotteremo d'ora in
avanti.

Modelli di calcolo e metodologie di analisi

Per poter analizzare e studiare l'ecienza di un algoritmo abbiamo bisogno di


denire un modello di calcolo appropriato. Citiamo alcuni modelli di calcolo:

macchina di Turing

macchina a registri

macchina a puntatori

modello di memoria esterna

Best case, Worst case, Middle case

E' utile misurare l'ecienza di un al-

goritmo in funzione della dimensione dell'istanza di ingresso. Eppure, potrebbe


accadere che istanze diverse, sebbene della stessa dimensione, implichino tempi
di esecuzione molto diversi. A parit di dimensione dell'istanza di ingresso, possiamo quindi analizzare il costo di esecuzione di un algoritmo su istanze pessime,
istanze ottime ed istanze tipiche. Per questo deniamo tre tipologie di analisi:

analisi worst case: quante operazioni eseguiamo per istanze di ingresso

che comportano il massimo lavoro per l'algoritmo.

Tw (n) = max|I|,n {tempo(I)}

(5)

E' la tipologia di analisi pi utilizzata non solo in quanto fornisce garanzie


prestazionali nel caso pi sfavorevole, ma anche perch restituisce informazioni utili sull'upper bound del middle case senza dover conoscere la
distribuzione delle istanze di ingresso.

analisi best case: quante operazioni eseguiamo per istanze di ingresso

che comportano il minimo lavoro per l'algoritmo.

Tb (n) = min|I|,n {tempo(I)}

(6)

analisi middle case: qual' il costo di esecuzione su istanze di ingresso

tipiche, cio istanze

di dimensione

Tm (n) =

Analisi di algoritmi ricorsivi

con probabilit

{P {I} tempo(I)}

P {I}.
(7)

Il tempo di esecuzione degli algoritmi ricorsivi

pu essere espresso tramite relazioni di ricorrenza: infatti il tempo richiesto


da una procedura uguale al tempo speso all'interno della procedura pi il
tempo speso per le chiamate ricorsive. Disponiamo di tre metodi per l'analisi di
algoritmi ricorsivi: i primi due si applicano a relazioni di ricorrenza di qualsiasi
tipo; il terzo si applica a relazioni di ricorrenza basate sulla tecnica divide et

impera .

7 la tecnica divide et impera, il cui nome deriva da un celebre principio politico, consiste nel
dividere un problema in sottoproblemi pi piccoli, risolvere i sottoproblemi separatamente, e
combinare le loro soluzioni per ottenere la soluzione del problema originario.

metodo dell'iterazione : consiste nello srotolare la relazione di ri-

correnza, riducendola ad una sommatoria dipendente solo dalla dimensione

del problema iniziale.

metodo della sostituzione:

sfrutta lo stretto legame tra induzione

e ricorsione; consiste infatti nell'intuire la soluzione di una relazione di


ricorrenza ed usare l'induzione matematica per dimostrarne la correttezza.
Pu richiedere molta esperienza per il passaggio intuitivo. Questo metodo
restituisce anche un'informazione precisa sulle costanti moltiplicative che
rimarrebbero altrimenti nascoste nella notazione asintotica.

metodo dell'albero della ricorsione: consiste nel disegnare l'albero

della ricorsione, indicando le dimensioni dei sottoproblemi di ogni chiamata ricorsiva ed analizzando la dimensione dei problemi ad ogni livello
dell'albero. Le relazioni di ricorrenza possono infatti essere rappresentate
dall'albero della ricorsione, di cui enunciamo alcune propriet utili per
scrivere la relazione di ricorrenza in forma chiusa.

i sottoproblemi al livello
sione

dell'albero della ricorsione hanno dimen-

n
bi .

il contributo di un nodo di livello

i al tempo di esecuzione,
 escludendo
f bni .

dunque il tempo speso nelle chiamate ricorsive,




il numero di livelli nell'albero della ricorsione


il numero di nodi al livello

logb n9 .

dell'albero della ricorsione

ai 10 .

Queste propriet ci permettono di riscrivere la relazione di ricorrenza nella


forma:

T (n) =

logb n 

ai f

i=0

 n 

(8)

bi

la cui soluzione data dal teorema fondamentale delle ricorrenze.

metodo del cambio di variabile: consiste in un semplice cambio di

variabile che possa esprimere la relazione di ricorrenza in una forma pi


familiare.

11 :

teorema fondamentale delle ricorrenze (o teorema master)

si applica ad algoritmi ricorsivi basati sulla tecnica divide et impera. Supponiamo che un problema di dimensione
di dimensione

n/b

n venga diviso in a sottoproblemi

ciascuno, e che dividere in sottoproblemi e ricombina-

rne le soluzioni abbia un costo di esecuzione

8 anche detto metodo dell'unfolding.


9 il fatto che i sottoproblemi nell'ultimo

f (n),

allora la relazione di

livello abbiano dimensione


e quindi i = logb n.
10 infatti ogni nodo interno ha esattamente a gli.
11 anche detto metodo dell'esperto.

n
bi

=1

implica n = bi

ricorrenza pu essere espressa in forma chiusa:

T (n) =

aT

n
b

+ f (n) se n > 1
(9)

se n = 1

con soluzione:

 T (n) = (nlogb a ),
prevale su

f (n) = O(nlogb a ) > 0,

se

cio se

nlogb a

f (n);

 T (n) = (nlogb a log n), se f (n) = O(nlogb a ), cio se nessuno dei due
termini prevale sull'altro;

 T (n) = (f (n)), se f (n) = (nlogb a+ ) > 0af


1 n n0 ,

cio se

f (n)

n
b cf (n)c
logb a
prevale polinomialmente su n
.

<

Ci sono alcuni casi in cui nessuna della tre precedenti soluzioni pu applicarsi, e siamo dunque costretti ad applicare uno dei metodi sopra descritti.

Analisi di algoritmi randomizzati

Se il costo worst case molto pi elevato

del costo middle case, potremmo preferire avere un algoritmo leggermente meno
eciente che riduca il costo worst case ntantoch non faccia crescere troppo il
costo middle case. La randomizzazione una tecnica algoritmica utile al raggiungimento di questo scopo: essa consiste nel denire alcuni dati come numeri
random.

Un algoritmo randomizzato un algoritmo che utilizza numeri

random. Un algoritmo deterministico un algoritmo non randomizzato.


Il tempo atteso il tempo di esecuzione di un algoritmo randomizzato,
cio il tempo richiesto su una certa istanza di ingresso e per una data sequenza
di numeri random:

Te (I) =

{P {R} T {I, R}}

(10)

seq random
Quindi il tempo atteso worst case:

Te (n) = max|I|,n

{P {R} Tw {I, R}}

(11)

seq random

Analisi ammortizzata

Un algoritmo potrebbe dover essere eseguito pi volte

passando operazioni di aggiornamento, cio operazioni che modicano


il contenuto informativo di una collezione di dati, ed operazioni di interrogazione, cio operazioni che esplorano i dati di una collezione senza modi-

carne il contenuto informativo. Pu dunque accadere che il costo di un'operazione


dipenda strettamente dall'esecuzione precedente di altre operazioni. Analizzare
l'ecienza di un algoritmo su una sequenza di esecuzioni pu quindi darne una
caratterizzazione pi ranata. L'analisi ammortizzata studia le prestazioni medie di una sequenza di esecuzioni su una collezione di dati, piuttosto che della

singola esecuzione di un algoritmo su un'unica istanza di ingresso. Il punto


che una singola esecuzione potrebbe essere ineciente per una congurazione
particolarmente svantaggiosa dell'istanza di ingresso.

Tuttavia, i dati stessi

potrebbero congurarsi in modo vantaggioso durante esecuzioni successive, per


cui in media il tempo richiesto per ciascuna esecuzione potrebbe essere inferiore
al quello che si avrebbe nel worst case. Nell'analisi ammortizzata le probabilit
non entrano in gioco. Il costo ammortizzato di un algoritmo su una sequenza
di

esecuzioni denito come:

Ta (n) =
dove

T (n, k)

T (n, k)
k

(12)

il tempo di esecuzione worst case per tutte le k esecuzioni su

istanze di ingresso di dimensione

n.

I metodi di analisi ammortizzata sono:

metodo dei crediti: calcola il costo ammortizzato senza dover entrare

nel dettaglio delle interdipendenze fra le esecuzioni.

Ci possibile as-

sociando dei crediti agli oggetti di una collezione. I crediti depositati da


alcune operazioni saranno in grado di pagare il costo di altre operazioni.
Il costo ammortizzato di una operazione secondo il metodo dei crediti
denito come:

Ta (n) = T (n) + deposito(n) prelievo(n)

(13)

T (n) il costo di esecuzione worst case eettivo dell'operazione,


deposito(n) sono i crediti che tale operazione deposita sugli oggetti, e
prelievo(n) il numero di crediti che la stessa operazione preleva dagli
dove

oggetti.

metodo del potenziale:

calcola il costo ammortizzato senza dover

entrare nel dettaglio delle interdipendenze fra le esecuzioni. Ci possibile


associando un potenziale (positivo) alla collezione di dati secondo una
funzione potenziale
il potenziale

(I).

che, applicata all'istanza di ingresso

I,

ne denisce

Il potenziale di una collezione di oggetti serve a pagare

il costo di certe operazioni sulla collezione stessa. Il costo ammortizzato


di un'operazione secondo il metodo del potenziale denito come:

Ta (n) = T (n) + (I 0 ) (I)


dove

T (n)

(14)

il costo di esecuzione worst case eettivo dell'operazione,

l'istanza di ingresso di dimensione

gresso all'operazione, e

I0

della collezione di dati in in-

l'istanza della collezione dati dopo l'esecuzione

dell'operazione.
Vale il seguente teorema:

Siano

I0 , ..., Ik le istanze di ingresso di dimensione n della collezione di


k operazioni a partire da un'istanza

dati ottenute su una sequenza di

I0 .Se (I0 ) = 0

(Ik ) 0,

allora

T (n, k)

k
X

Ta (n)

(15)

i=1
dove

T (n, k) il costo di esecuzione worst case totale richiesto dall'algoritmo


k operazioni su istanze di ingresso di dimensione n.

per tutte le

Strutture dati elementari

Un tipo di dato un insieme di operazioni, descritto da un'interfaccia, che


deniscono un tipo di oggetto, specicando dunque cosa un'operazione debba
fare. Una struttura dati una particolare realizzazione di un tipo di dato,
e specica dunque come un'operazione possa essere realizzata. La scelta della
struttura dati inuisce profondamente sull'ecienza delle operazioni che deniscono il tipo di dato.

Strutture indicizzate

Una struttura indicizzata una tipologia di strut-

tura dati con occupazione di memoria

M (n) = (n),

che realizza il tipo di dato

dizionario. Consiste in una sequenza di celle numerate


che possono contenere

A[0 : n1], detta array,

elementi di un tipo prestabilito. Il tempo di accesso in

lettura e scrittura ad una qualsiasi cella

T (n) = O(1), costante e indipendente

dalla dimensione dell'array. Valgono le seguenti propriet:

propriet forte: gli indici interni della array sono numeri interi con-

secutivi.

propriet debole: non possibile aggiungere nuove celle ad una array;

questa propriet implica che il ridimensionamento di una array possibile


solo mediante riallocazione.
Esistono due tipologie fondamentali di struttura indicizzata:

array ordinato: realizza semplicemente le propriet delle strutture dati

indicizzate, senza l'impiego di alcuna tecnica di ottimizzazione dell'ecienza.

array doubling-halving:

implementato con la tecnica doubling-

halving, che consiste nel mantenere un array

h-dimensionale che soddisfa

l'invariante

n h 4n

n > 0

e permette di mantenere ecientemente una array non ordinato di

(16)

el-

ementi soggetto ad operazioni di inserimento e cancellazione, in quanto

(n) operazioni. Ne consegue un


Tamm (n) = O(1) per le operazioni di inser-

eettua il processo di riallocazione ogni


costo ammortizzato costante

12 . Un array dubling-halving deve essere inizializ-

imento e cancellazione

zato monodimensionale per semplicarne l'implementazione, ed quindi


anche necessario assicurarsi che non diventi mai adimensionale, altrimenti
sarebbero compromesse le future operazioni di raddoppiamento.

Strutture collegate

Una struttura collegata una struttura dati con

occupazione di memoria

M (n) = (n), che realizza il tipo di dato dizionario.

Le

strutture collegate sono particolarmente ecienti nelle operazioni di inserimento


e cancellazione. I costituenti base di una struttura collegata sono:

12 nonostante la cancellazione eettiva richieda T


amm (n) = O(1), se la chiave non nota a
priori l'operazione implica una ricerca con tempo di esecuzione pari a O(n).

10

record: con numerazione non necessariamente consecutiva, ciascun record

contiene un oggetto della collezione. Un record pu essere rappresentato


mediante un oggetto.

Mentre la numerazione array locale al singolo

array, i numeri associati ai record sono tipicamente i loro indirizzi di


memoria. La distruzione di un record avviene automaticamente tramite

13 .

la garbage collection

puntatori: collegamenti tra due record.

Valgono le seguenti propriet:

propriet forte:

possibile aggiungere o togliere un record ad una

struttura collegata; Le operazioni di inserimento e cancellazione hanno


tempo di esecuzione worst case costante.

propriet debole:

gli indirizzi dei record in una struttura collegata

non sono necessariamente consecutivi; questa propriet impedisce l'uso

14 ; per questo l'operazione di ricerca gen-

tout-court della ricerca binaria

eralmente realizzata come ricerca sequenziale.


Esistono tre tipologie fondamentali di struttura collegata:

lista semplice: ogni record ha un solo puntatore al record successivo;

l'ultimo record della lista ha puntatore null.

Una variabile di istanza

contiene il riferimento al record di testa.


# l i n k e d l i s t . py
class

Record :
def

__init__ ( s e l f , e l e m ) :
s e l f . e l e m=e l e m
s e l f . n e x t=None

class

LinkedList :
def

__init__ ( s e l f ) :
s e l f . f i r s t =None
s e l f . l a s t =None

def

isEmpty ( s e l f ) :
return

def

( s e l f . f i r s t ==None )

getFirst ( self ):
if

s e l f . f i r s t ==None :
return

None

else :

13 la garbage collectionin dealloca implicitamente il record rimosso: basta scollegarlo dalla


lista per essere automaticamente rimosso dalla memoria.
14 anche detta ricerca dicotomica.

11

return
def

s e l f . f i r s t . elem

getLast ( s e l f ) :
if

s e l f . l a s t==None :
return

None

return

s e l f . l a s t . elem

else :

def

addAsLast ( s e l f , elem ) :
r e c=R e c o r d ( e l e m )
if

s e l f . f i r s t ==None :
s e l f . f i r s t = s e l f . l a s t =r e c

else :
s e l f . l a s t . n e x t=r e c
s e l f . l a s t =r e c
def

a dd AsFirst ( s e l f , elem ) :
r e c=R e c o r d ( e l e m )
if

s e l f . f i r s t ==None :
s e l f . f i r s t = s e l f . l a s t =r e c

else :
r e c . n e x t= s e l f . f i r s t
s e l f . f i r s t =r e c
def

popFirst ( s e l f ) :
if

s e l f . f i r s t ==None :
return

None

else :
r e s= s e l f . f i r s t . e l e m
s e l f . f i r s t=s e l f . f i r s t . next
if

s e l f . f i r s t ==None :
s e l f . l a s t =None

return
def

res

getFirstRecord ( s e l f ) :
if

s e l f . f i r s t ==None :
return

None

return

self . first

else :

def

getLastRecord ( s e l f ) :
if

s e l f . f i r s t ==None :
return

None

return

self . last

else :

12

def

show ( s e l f ) :
if

s e l f . f i r s t ==None :
print

"[]"

return
print

" Elements

in

the

collection

( ordered ) : "

s ="["
c u r r e n t= s e l f . f i r s t
while

c u r r e n t != None :
if

l e n ( s ) >1:
s +=", "

s+=s t r ( c u r r e n t . e l e m )
c u r r e n t=c u r r e n t . n e x t
s +="]"
print

lista doppiamente collegata: ogni record ha due puntatori, uno al

record precedente ed uno al record successivo; il primo record della lista


ha puntatore al predecessore null, mentre l'ultimo record della lista ha
puntatore al successore null. Una variabile contiene il riferimento al record
di testa.
#d o u b l e l i n k e d l i s t . py
import
class

linkedlist
DoubleRecord ( L i n k e d L i s t . Record ) :
def

__init__ ( s e l f , e l e m ) :
l i n k e d l i s t . R e c o r d . __init__ ( s e l f , e l e m )
s e l f . p r e v=None

class

DoubleLinkedList ( l i n k e d l i s t . LinkedList ) :
def

addAsLast ( s e l f , elem ) :
r e c= D o u b l e R e c o r d ( e l e m )
if

s e l f . f i r s t ==None :
s e l f . f i r s t = s e l f . l a s t =r e c

else :
r e c . prev =

self . last

s e l f . l a s t . next = r e c
self . last
def

= rec

a dd AsFirst ( s e l f , elem ) :
r e c=D o u b l e R e c o r d ( e l e m )
if

s e l f . f i r s t ==None :
s e l f . f i r s t = s e l f . l a s t =r e c

else :
s e l f . f i r s t . prev = r e c

13

r e c . next =
self . first
def

self . first
= rec

popFirst ( s e l f ) :
if

s e l f . f i r s t ==None :
return

None

else :
res =

s e l f . f i r s t . elem

self . first
if

s e l f . f i r s t . next

s e l f . f i r s t != None :
s e l f . f i r s t . p r e v = None

else :
self . last
return
def

= None

res

popLast ( s e l f ) :
if

s e l f . f i r s t ==None :
return

None

else :
res =

s e l f . l a s t . elem

self . last
if

s e l f . l a s t . prev

s e l f . l a s t != None :
s e l f . l a s t . n e x t = None

else :
self . first
return
def

= None

res

deleteRecord ( s e l f , rec ) :
if

r e c==None :

if

r e c . p r e v != None :

return
r e c . prev . next = r e c . next
else :
self . first
if

= r e c . next

r e c . n e x t != None :
r e c . next . prev = r e c . prev

else :
self . last

= r e c . prev

lista circolare doppiamente collegata: ogni record ha due punta-

tori, uno al record precedente ed uno al record successivo; una variabile


contiene il riferimento al record di testa. L'ultimo record inserito diventa
il record di testa.

14

Pile e code

15 .

La pila un tipo di dato con disciplina di accesso LIFO

#s t a c k . py
from
class

linkedlist

import

LinkedList

StackLinkedList ( LinkedList ) :
def

push ( s e l f ,

elem ) :

s e l f . addAsFirst ( elem )
def

pop ( s e l f ) :
return

def

top ( s e l f ) :
return

class

s e l f . popFirst ()

s e l f . getFirst ()

StackList_Dummy :
def

__init__ ( s e l f ) :
self . s =

def

push ( s e l f ,

[]

elem ) :

s e l f . s . insert (0 ,
def

pop ( s e l f ) :
if

l e n ( s e l f . s ) == 0 :
return

return
def

l e n ( s e l f . s ) == 0 :
return

return

l e n ( s e l f . s ) == 0

stampa ( s e l f ) :
print

class

None

self . s [0]

isEmpty ( s e l f ) :
return

def

None

s e l f . s . pop ( 0 )

top ( s e l f ) :
if

def

elem )

self . s

StackList :
def

__init__ ( s e l f ) :
self . s =

def

push ( s e l f ,

[]

elem ) :

s e l f . s . append ( e l e m )

15 ovvero

last-in-rst-out

15

def

pop ( s e l f ) :
if

l e n ( s e l f . s ) == 0 :
return

return
def

top ( s e l f ) :
if

l e n ( s e l f . s ) == 0 :
return

isEmpty ( s e l f ) :
return

def

None

s e l f . s [ 1]

return
def

None

s e l f . s . pop ( )

l e n ( s e l f . s ) == 0

stampa ( s e l f ) :
print

self . s

16 .

La coda un tipo di dato con disciplina di accesso FIFO


#q u e u e . py
from

linkedlist

from

collections

class

import
import

LinkedList
deque

QueueLinkedList ( LinkedList ) :
def

enqueue ( s e l f ,

elem ) :

s e l f . addAsLast ( elem )
def

dequeue ( s e l f ) :
return

class

s e l f . popFirst ()

QueueList :
def

__init__ ( s e l f ) :
self .q =

def

[]

enqueue ( s e l f ,

elem ) :

s e l f . q . append ( e l e m )
def

dequeue ( s e l f ) :
if

l e n ( s e l f . q ) == 0 :
return

return
def

getFirst ( self ):
if

16 ovvero

None

s e l f . q . pop ( 0 )

l e n ( s e l f . q ) == 0 :

rst-in-rst-out

16

return

None

return

self .q[0]

else :

def

isEmpty ( s e l f ) :
return

def

stampa ( s e l f ) :
print

class

l e n ( s e l f . q ) == 0

self .q

QueueList_Dequeue ( Q u e u e L i s t ) :
def

__init__ ( s e l f ) :
s e l f . q = deque ( )

def

dequeue ( s e l f ) :
if

l e n ( s e l f . q ) == 0 :
return

return

Strutture ad albero

None

s e l f . q . popleft ()

Una struttura ad albero una struttura dati par-

ticolarmente eciente, qualora si intenda operare con una collezione di oggetti


sulla quale sia denita una qualche relazione gerarchica. Diamo alcune denizioni:

albero radicato: un particolare tipo di grafo denito da una tripla

T (N, A, r) costituita da un insieme N

di nodi, da un insieme

A N N

di

coppie orientate di nodi, dette archi, e da un nodo speciale, detto radice.

genitore (o padre): in un albero radicato l'unico nodo

v 6= r,

tale che

u, per ogni nodo

(u, v) A.

figlo: qualunque nodo

v , per il quale esista un nodo u, tale che (u, v) A.

grado: il grado di un nodo

radice: nodo speciale

foglia: nodo senza gli.

nodo interno: nodi che non sono foglie.

antenati: gli antenati di un nodo

u la cardinalit dell'insieme {v|v = f iglio(u)}.

r T (N, A, r)

tale che

@padre(r).

l'insieme dei nodi raggiungibili a

partire da quel nodo, risalendo di padre in padre.

discendenti: i discendenti di un nodo

u l'insieme dei nodi raggiungibili

a partire da quel nodo, scendendo di glio in glio.

fratelli:

i fratelli di un nodo

padre(v)}.

17

l'insieme dei nodi

{w|padre(w) =

profondit

17 : la profondit di un nodo il numero di archi che bisogna

attraversare per raggiungerlo a partire dalla radice.

altezza: l'altezza di un albero la massima profondit a cui si trova una

foglia.

albero ordinato: un albero ordinato se esiste una relazione d'ordine

sulla lista dei gli di ogni nodo.

albero d-ario: albero in cui tutti i nodi tranne le foglie hanno grado

albero completo:

d.

albero d-ario in cui tutte le foglie sono alla stessa

profondit.
Una struttura ad albero pu essere realizzata sia come struttura indicizzata che
come struttura collegata.

strutture indicizzate: ogni nodo rappresentato da una cella di array

contenente il contenuto informativo associato al nodo, pi eventualmente


altri indici che permettono di raggiungere gli altri nodi. Tipicamente le
strutture indicizzate rendono dicoltose le operazioni di aggiornamento
dinamico dell'albero.

P n-dimensionale con occupazione di memoM (n) = O(n), tale che P [v] = u (u, v) A. Se v = r =
P [v] = N U LL. La ricerca del padre di un nodo ha tempo di esecuzione Tw (n) = O(1), ma la ricerca di un glio di un nodo richiede
la scansione dell'intero array, quindi Tw (n) = O(n).
vettore padri: vettore

ria

vettore posizionale: array P (n+1)-dimensionale tale che P [0] =


N U LL18 , P [1] = r, P [(d (p 1) + i)] = f iglioi (v), dove v un nodo
con indice p e d il limite superiore al grado dei nodi. Ne consegue

che ogni nodo ha una posizione prestabilita nella struttura, e che tutti
i nodi di uno stesso livello occuperanno posizioni contigue. Nel caso
di alberi

d-ari

completi con

nodi,

(n + 1)-dimensionale,

ma nel

19 e la

caso di alberi non completi alcune posizioni saranno inutilizzate

20
dimensione dovr essere superiore . Questo vettore molto utile a
rappresentare particolari tipi di alberi detti heap. La ricerca del padre
di un nodo

richiede tempo di esecuzione

di un glio di un nodo

proporzionale al grado del nodo

v,

l'aggiunta di un glio al nodo

Tb (n) = O(1)
Tw (n) = O(|P | d).

richiede tempo di esecuzione best case

esecuzione worst case

21

Tw (n) = O(1), la ricerca


Tw (n) = O(d)

richiede tempo di esecuzione

e tempo di

17 o livello.
18 per semplicit di indicizzazione.
19 questo perch non tutti i livelli sono completi, per denizione di albero non completo.
20 nel caso peggiore di alberi totalmente sbilanciati in cui ogni livello contiene un solo nodo,

la dimensione del vettore posizionale sar addirittura un esponenziale di n.


21 il nuovo nodo dovr occupare una posizione esistente utilizzata o inesistente: in tal caso
necessario un processo di riallocazione.
18

strutture collegate:

ogni nodo rappresentato da un record con-

tenente il contenuto informativo associato al nodo, pi i puntatori che


permettono di raggiungere gli altri nodi. Tipicamente le strutture collegate supportano ecientemente le operazioni di aggiornamento dinamico
dell'albero.

puntatori ai figli: struttura collegata con occupazione di memoria

M (n) = O(n),

in cui ogni nodo di un albero d-ario rappresentato

da un record con d puntatori ai gli. Se un glio assente il valore


del puntatore

N U LL.

Per rendere pi ecienti alcune operazioni,

manterremo per ogni nodo anche un puntatore al padre.


la radice si accede all'intera struttura.

Tramite

La ricerca del padre di un

nodo v, la lettura del suo contenuto informativo, del suo grado e la


ricerca di un glio sinistro o destro richiedono tempo di esecuzione

Tw (n) = O(1).

M (n) =
O(n), in cui ogni nodo di un albero non necessariamente d-ario rap-

lista figli: struttura collegata con occupazione di memoria

presentato da un record con un puntatore alla struttura addizionale


lista di gli, rappresentabile tramite una struttura indicizzata o una
struttura collegata.

primo figlio-fratello successivo: struttura collegata con occu-

M (n) = O(n), in cui ogni nodo di


d-ario rappresentato da un record

pazione di memoria

un albero non

necessariamente

con due pun-

tatori. Un puntatore avr il riferimento al primo glio, e l'altro al


fratello successivo. Qualora non esistessero il primo glio o il fratello
i puntatori assumono il valore

N U LL.

In questa rappresentazione

per visitare i gli di un nodo, si punta al primo glio e poi ai fratelli


successivi.
Diversamente dalle collezioni lineari di oggetti, la struttura gerarchica degli
alberi rende necessaria una visita della struttura che ne segua le ramicazioni
gerarchiche. Distinguiamo quindi tre tipologie di visita:

visita generica: visita tutti i nodi di un albero qualsiasi raggiungibili

a partire da un nodo pressato

23 dell'albero.
nodi aperti

22 . Mantiene un insieme

contenente i

24 pro-

L'algoritmo di ricerca generica chiude

gressivamente questi nodi e termina quando

S = .

Il generico passo di

visita estrae un nodo aperto chiudendolo, lo visita e apre tutti i suoi gli
aggiungendoli a

S.

Il tempo di esecuzione

Tw (n) = O(n).

25 ): i nodi vengono visitati con dis-

visita in profondit (o visita DFS

ciplina di accesso LIFO, quindi

viene rappresentato dalla struttura dati

22 tipicamente la radice.
23 nodi da cui la ricerca deve proseguire.
24 chiamiamo questi nodi: nodi chiusi.
25 ovvero Depth-First Search.

19

pila.

La visita prosegue dall'ultimo nodo lasciato in sospeso, impilando

26 . Cos facendo si scende in pro-

prima il glio destro poi quello sinistro

fondit no a raggiungere la prima foglia sinistra, cio si inizier la visita


del sottoalbero destro solo dopo aver completato la visita del sottoalbero
sinistro. Esistono tre varianti classiche di questa tiplogia di visita:

visita in profondit in preordine: si visita prima la radice, poi

si eettua la chiamata ricorsiva sul glio sinistro e destro.

visita in profondit simmetrica: si eettua prima la chiamata

ricorsiva sul glio sinistro, poi si visita la radice, ed inne si eettua


la chiamata ricorsiva sul glio destro.

visita in profondit in postordine: si eettuano prima le chia-

mate ricorsive sul glio sinistro e destro, poi si visita la radice.

visita in ampiezza (o visita BFS

plina di accesso FIFO, quindi

27 ): i nodi vengono visitati con disci-

viene rappresentato dalla struttura dati

coda. I nodi vengono visitati per livelli.

26 la variante simmetrica lecita


27 ovvero Breadth-First Search.

e produce un eetto simmetrico a quelli sotto esposti.

20

Ordinamento
n

Il problema dell'ordinamento consiste nell'ordinare una sequenza di


oggetti sui quali sia denita una relazione d'ordine totale.

Il teorema sul lower bound dell'ordinamento denisce il lower bound


della complessit computazionale del problema dell'ordinamento secondo il modello dei confronti:

Cwt (n) = (n log n)

Modello dei confronti

(17)

Il modello dei confronti un modello astratto

di analisi degli algoritmi che ha come metrica il confronto tra due oggetti

28 .

Ogni algoritmo di ordinamento basato sul modello dei confronti denisce


un albero di decisione

che lo descrive, illustrando le diverse sequenze di

confronti che l'algoritmo potrebbe fare su istanze di ingresso di dimensione

n.

un albero binario. Ad ogni nodo associata una coppia di indici indicanti

la coppia di elementi della sequenza coinvolti nel confronto. Ogni nodo ha al

29 , che rappresentano il comportamento dell'algoritmo in funzione

pi due gli

dell'esito del confronto precedente. Ogni cammino nell'albero di decisione descrive la sequenza di confronti ed il loro esito per una particolare esecuzione
dell'algoritmo. Valgono dunque le seguenti propriet dell'albero di decisione :

per l'ordinamento di

di permutazioni di

n elementi contiene almeno n! foglie, pari al numero


30 .

elementi

Un albero binario con

foglie ha un'altezza almeno pari a

log2 f 31 .

HT (n) = (log n!) (n log n)

(18)

Cw (n) = (HT ) = (n log n)

(19)

Cm (n) = (H T )

(20)

Algoritmi di ordinamento

Gli algoritmi di ordinamento si dividono in due

categorie:

algoritmi di ordinamento basati sul modello dei confronti

28 il modello dei confronti non stabilisce alcuna assunzione sulla tipologia di


29 un confronto ha generalmente un esito binario.
30 ogni foglia di T rappresenta una soluzione del problema dell'ordinamento.

oggetti.

Ogni soluzione
del problema una permutazione degli n elementi da ordinare. Quindi T deve contenere un
numero di foglie pari almeno al numero di permutazioni, n!.
31 per la disuguaglianza di De Moivre-Stirling :

2n

 n n
e

n!

2n

21


 n n 
1
1+
e
12n 1

algoritmi di ordinamento incrementale: operano secondo la

tecnica algoritmica incrementale/induttiva, estendendo in maniera


progressiva una sottosequenza ordinata nch essa non comprenda
tutti gli elementi.

Ordinano in loco, ma

Cw (n) = O(n2 ),

il peg-

gior tempo di esecuzione asintotico, in quanto confrontano tutti le

n
2


=

n(n1)
32
coppie di elementi .
2

Selection Sort
Insertion Sort
Bubble Sort
Heap Sort

algoritmi di ordinamento divide et impera: operano secondo

la tecnica algoritmica divide et impera, realizzando ricorsivamente


un passo divide di partizionamento dell'input, e un passo impera di
merging dei risultati parziali.

Merge Sort
Quick Sort

algoritmi di ordinamento non basati su confronti: operano non

esclusivamente secondo confronti fra oggetti, bens realizzano l'ordinamento


di specici tipi di oggetto, sfruttandone le caratteristiche interne.





Integer Sort
Bucket Sort
Radix Sort

Un algoritmo di ordinamento in loco un algoritmo di ordinamento che fa


uso della memoria necessaria per mantenere gli elementi dell'istanza di ingresso,
e di uno spazio addizionale costante. Un algoritmo di ordinamento stabile
un algoritmo di ordinamento che preserva l'ordine iniziale tra due elementi
dello stesso valore.

Noteremo che solo alcuni algoritmi sono spontaneamente

stabili, sebbene molti degli algoritmi di ordinamento basati su confronti possano


comunque essere resi stabili.

33 un algoritmo di ordinamento incre2 34


mentale in loco con costo di esecuzione Cw (n) = (n ) . L'idea di base ap-

Selection Sort

Il Selection Sort

32 realizzando

opportune strutture dati possibile rendere pi eciente l'apporccio incrementale.


33 ovvero ordinamento per selezione.
34 Il costo totale C (n) = P c , dove c il numero di confronti eettuati nella i-esima
w
i
i i
iterazione.
ci = n i 1

Quindi
C(n) =

n2
X
i=0

(n i 1) =

n1
X

i [0, n 2]

t=

t=1

22

(n 1)(n 1 + 1)
= (n2 )
2

pendere il minimo della sottosequenza non ordinata in cima alla sottosequenza


ordinata.
#s e l e c t i o n s o r t . py
def

s e l e c t i o n S o r t (A ) :
for

in

r a n g e ( l e n (A)
minPos =
for

1):

in

range ( i
if

+ 1,

l e n (A ) ) :

A [ j ] < A [ minPos ] :
minPos = j

A [ minPos ] ,

A[ i ]

= A[ i ] ,

A [ minPos ]

A n-dimensionale A[0 : n 1] non


i-esimo, trova il minimo in A[i : n 1] e lo scambia eventualmente con A[i]; A avr cos un sottoarray ordinato A[0 : i], ed un sottoarray
non ordinata A[i + 1 :], e cos via no ad aver ordinato tutto l'array, cio quando
i = n 2.
L'algoritmo prende in input un array

ordinato. Al passo

35 un algoritmo di ordinamento incre2 36


mentale in loco con costo di esecuzione Cw (n) = (n )
. L'idea di base

Insertion Sort

L'Insertion Sort

inserire ogni elemento del sottoarray non ordinato nel posto opportuno delsottoarray ordinato.
Ne esitono due varianti:

InsertionSort Down: riempie il sottoarray ordinato da sinistra verso

destra.

InsertionSort Up: riempie il sottoarray ordinato da destra verso sinis-

tra.

i n s e r t i o n s o r t . py
def

i n s e r t i o n S o r t D o w n (A ) :
for

in

range (1 ,

l e n (A ) ) :

v a l = A[ i ]
for

pos

in

range ( i ) :

if

A[ pos ] > v a l :
break

35 ovvero

ordinamento per inserimento. Ne esistono due varianti: InsertionSort Up, che


procede da destra a sinistra, edPInsertionSort Down, che procede da snistra a destra.
36 Il costo totale C (n) =
w
iterazioni ci , dove ci il numero di confronti eettuati nella
i-esima iterazione.

ci = n + 1

Quindi
Cw (n) =

n1
X
i=1

(i + 1) = (n 1) +

i [1, n 1]
n1
X
t=1

23

t=

(n 1)(n 1 + 1)
= (n2 )
2

if

pos <

i :
for

in

range ( i ,
A[ j ]

A[ pos ]

pos ,

= A[ j

1):

1]

= val

A n-dimensionale A[0 : n 1] non


S[0 : i1] il pi piccolo
elemento A[pos] maggiore di A[i] e se esiste, pone A[i] fra A[pos 1] e A[pos],
facendo scorrere a destra di una posizione tutti gli elementi A[pos :]; ora A avr
un sottoarray ordinato A[0 : i], ed un sottoarray non ordinato A[i + 1 : n 1],
e cos via no ad aver ordinato tutto l'array, cio quando i = n 1.
L'algoritmo prende in input un array

ordinato. Al passo i-esimo, trova nel sottoarray ordinato

37 un algoritmo di ordinamento incrementale


2 38
in loco con costo di esecuzione Cw (n) = (n )
. L'idea di base ordinare la

Bubble Sort

Il Bubble Sort

sequenza attraverso scambi fra coppie adiacenti di elementi.


b u b b l e s o r t . py
def

b u b b l e S o r t (A ) :
swapped = True
while

swapped :
swapped = F a l s e
for

in

r a n g e ( l e n (A)
if

A[ i ] > A[ i
A[ i ] ,

1):

1]:

A[ i

+ 1 ] = A[ i

+ 1] ,

swapped = True

A[0]...[n 1] n-dimensionale

L'algoritmo prende in input un array


nato, e vi esegue

O(n) scansioni complete:

non ordi-

in ogni scansione vengono confrontate

una ad una coppie di elementi adiacenti. Viene eettuato uno scambio tra i due
elementi in caso non rispettino l'ordinamento.
viene eettuato nessuno scambio, l'array

Se durante una scansione non

ordinato e l'algoritmo termina.

Valgono i seguenti lemmi:

l'i

esima scansione porta il massimo di A[0 : n i 1] in posizione


A[n i 1], tramite O(n i 2) confronti fra coppie di elementi adiacenti.

dopo l'i esima scansione, gli elementi

A[n i 1 : n 1]

sono corretta-

mente ordinati e occupano la loro posizione denitiva.

dopo

O(n 1)

scansioni l'intero array risulta ordinato.

37 ovvero ordinamento a bolle. a bolle perch spinge gli oggetti pi grandi verso la ne
dell'array, facendoli risalire verso
l'alto come bolle.
38 Il costo totale C (n) = P c , dove c il numero di confronti eettuati nella i-esima
w
i
i i
scansione.

Quindi

ci = n i

C(n) =

n2
X

i [0, n 2]

(n i) =

i=0

24

n2 n
= (n2 )
2

A[ i ]

Heap Sort

L'Heap Sort

39 un algoritmo di ordinamento incrementale in

40 che sfrutta un heap 41 basato sul massimo con struttura rafloco instabile
39 ovvero ordinamento a heap.
40 l'algoritmo di inizializzazione

distrugge l'ordine iniziale degli elementi della sequenza S ,


in quanto deve congurare la sequenza secondo la notazione posizionale con ordinamento a
heap.
41 un heap associato ad un insieme L di elementi un albero binario radicato con le seguenti
propriet:
Struttura: l'albero completo almeno no al penultimo livello.
Contenuto informativo: gli elementi di L sono memorizzati nei nodi dell'albero; ogni
nodo v memorizza un solo elemento, che denoteremo con chiave(v), e ogni elemento
memorizzato in un solo nodo.
Ordinamento a heap: il valore dell'elemento in un nodo sempre maggiore o uguale
al valore degli elementi nei gli.
Un heap con struttura rafforzata utile per l'ordinamento in loco mediante heap.
Un heap con struttura raorzata pu essere rappresentato in modo implicito da un
vettore posizionale, senza spreco di spazio.
In un vettore posizionale P la radice in P [1] e i gli di un generico nodo i, se esistono,
sono sin(i) = P [2i] des(i) = P [2i + 1].
Valgono il seguente lemma:
 Un heap con n nodi ha altezza (log n);

ed il seguente teorema:
 Un heap associato ad un insieme di n elementi pu essere costruito in tempo O(n)
e supporta la cancellazione del massimo in tempo O(log n). L'heap pu essere
rappresentato in loco.

25

42 , con costo di esecuzione C (n)


w

forzata

= (n log n)43 .

L'idea di base estrarre

ripetutamente il massimo da un heap, il quale rende pi eciente l'approccio


incrementale, rendendo eciente la selezione del massimo ad ogni iterazione.
#h e a p s o r t . py
def

h e a p S o r t (A ) :
h e a p = HeapMax (A ) ;
heap . h e a p i f y ( ) ;
while

not

heap . isEmpty ( ) :

heap . d el e t e M a x ( )
L'algoritmo prende in input un array

(n + 1)-dimensionale A[0]...[n]

non

ordinato, e lo ordina secondo la notazione posizionale heap con struttura rafforzata. Estrae dall'heap il massimo per

(n 1)

volte, sostituendolo ogni volta

con la foglia estrema destra e riordinando l'heap secondo l'ordinamento a heap.


L'algoritmo termina quando l'heap, contraendosi, rimane vuoto.
Ad ogni passo, estrae il massimo dalla radice
la foglia estrema destra
a heap per gli

(c 1)

A[c]

e la pone in

A[1].

A[1] dell'heap, rimuove dall'heap


Inne ristabilisce l'ordinamento

elementi rimanenti.

42 un heap con struttura raorzata un heap con


43 Il costo totale C (n) = O(t + nt ), dove t

le foglie compattate tutte a sinistra.


il costo di inizializzazione dell'heap, t2
il costo di cancellazione del massimo e ristabilimento dell'ordinamento a heap, che si ripete
su ognuno degli n elementi. Consideriamo un heap con struttura raorzata completo no
all'ultimo livello k .
w

t2 = O(h) = O(log n)

in quanto ad ogni livello dell'heap si eettua un confronto, quindi il numero di confronti sar
al pi pari all'altezza dell'heap h = O(log n).

t1 = C1 (n) = 2C1

n1
2


+ t2 2C1

n
2

+ O(log n) = (n)

per il teorema master delle ricorrenze. Quindi


C(n) = O(n + n log n) = (n log n)

Consideriamo ora un heap qualunque completo al pi no al penultimo livello k 1. I nodi


dell'heap sono n
n0 = 2k1 1 < n < 2k 1 = n00

Ci implica che

(n) = (n0 ) = (n00 )

Inoltre, per la monotonia dell'algoritmo di inizializzazione dell'heap


(n0 ) = T (n0 ) < T (n) < T (n00 ) = (n00 )

da cui, poich (n) = (n0 ) = (n00 )


T (n) = (n)

26

Merge Sort

Il Merge Sort un algoritmo di ordinamento divide et im-

44 potenzialmente stabile 45 con costo di esecuzione

pera

Cw (n) = (n log n)46 .

L'idea di base partizionare ricorsivamente la sequenza in sottosequenze di


taglia bilanciata, ordinarle separatamente e fonderle ordinatamente.
#m e r g e s o r t . py
def

m e r g e S o r t (A ) :
r e c u r s i v e M e r g e S o r t (A,

def

r e c u r s i v e M e r g e S o r t (A,
if

0,

left ,

l e n (A)

1)

right ):

l e f t >= r i g h t :
return

mid = i n t ( ( l e f t

+ right )

2)

left ,

r e c u r s i v e M e r g e S o r t (A,

mid + 1 ,

m e r g e P a r t i t i o n s (A,
def

r e c u r s i v e M e r g e S o r t (A,

m e r g e P a r t i t i o n s (A,
idLeft =

left ,

left ,

mid )

mid ,

mid ,

right )
right )

right ):

left

i d R i g h t = mid + 1
tempList =

[]

while

True
if

A[ i d L e f t ] < A[ i d R i g h t ] :
t e m p L i s t . append (A [ i d L e f t ] )
i d L e f t += 1

if

i d L e f t > mid :
for

i n A[ i dR ig ht : r i g h t +

1]:

t e m p L i s t . append ( v )
break
else :
t e m p L i s t . append (A [ i d R i g h t ] )
i d R i g h t += 1
if

idRight >
for

right :
i n A [ i d L e f t : mid +

1]:

t e m p L i s t . append ( v )

44 notiamo che la procedura impera pi complessa della procedura divide.


45 la stabilit del merge sort dipende dall'implementazione della fusione: sarebbe

qualora a parit di valori privilegiasse glielementi con indice minore.


46 Il costo totale C (n) = t + 2C n , dove t il costo della fusione.
w
1
1
2

stabile

t1 = O(n)

in quanto per fondere due sottosequenze l1 - ed l2 -dimensionali tali che (l1 + l2 ) = n vengono
eettuati (n 1) confronti. Quindi
Cw (n) = O(n) + 2Cw

per il teorema master delle ricorrenze.


27

n
2

= (n log n)

break
for

in

range ( l e f t ,
A[ i ]

right +

= tempList [ i

1):
left ]

partizionamento (procedura divide): il partizionamento divide ri-

corsivamente una sequenza

(l1 + l2 ) = n-dimensionale,

in due sottose-

quenze di taglia bilanciata rispettivamente l1 - ed l2 -dimensionali, tali che

n 47
2 . Il passo base si ha quando li
fusione delle sottosequenze.

l1 = l2 =

= 1,

allora potr iniziare la

fusione (procedura impera): la fusione estrae ripetutamente il min-

l1 ed l2 -dimensionali,
(l1 + l2 )-dimensionale. Quando

imo di due sottosequenze ordinate rispettivamente


e lo appende in una sequenza ausiliaria

una delle due sequenze diventa vuota, appende nella ausiliaria tutti gli
elementi rimanenti.

Quick Sort

Il Quick Sort un algoritmo di ordinamento divide et impera

in loco potenzialmente stabile

Ce (n) = O(n log n)50 :

48 con costo di esecuzione

Cw (n) = (n2 )49

ci vuol dire che l'algoritmo Quick Sort randomiz-

51 pi eciente dell'algoritmo Quick Sort deterministico.

zato

L'idea

di base partizionare ricorsivamente la sequenza intorno ad un perno, sia esso


randomizzato o deterministico.

47 se n dispari l = p n q e l = x n y.
1
2
2
2
48 dipende dall'implementazione del partizionamento:
49 Il costo totale C d (n) = t1 + C d (a) + C d (b), dove t
1
w
w
w

il costo del partizionamento


intorno al perno, mentre a e b sono le dimensioni delle sottosequenze. Notiamo che worst case
si ha quando il perno l'elemento minore (o maggiore) della sequenza, cio quando a = 0 e
b = n 1: ci crea infatti partizioni sbilanciate, aumentando il numero di confronti necessari
all'ordinamento.
t1 = (n 1)

in quanto ogni elemento della sequenza deve essere confrontato con il perno.Quindi
d
d
Cw
(n) = n 1 + Cw
(n 1)

n
X

in = O(n2 )

i=0

50 Assumendo ogni dimensione delle sottosequenze come equiprobabile in funzione della diPn1
mensione a della prima sottosequenza, il costo totale Cer (n) = a=0
p (t1 + Cer (a) + Cer (n
a 1)), dove p la probabilit di scegliere il perno nell'intervallo equiprobabile [0, n 1] e t1
il costo della fusione.

1
n
t1 = (n 1)
p=

Cer (a) = Cer (n a 1)

Quindi
Cer (n) =

n1
X
a=0

1
(n 1 + 2Cer (a)) 2n log n = O(n log n)
n

51 le migliori prestazioni sono raggiunte sperimentalemente scegliendo il perno come mediano


di tre valori randomizzati.

28

#q u i c k s o r t R e c . py
def

q u i c k S o r t (A,

d e t=F a l s e ) :

r e c u r s i v e Q u i c k S o r t (A,
def

r e c u r s i v e Q u i c k S o r t (A,
if

0,

l e n (A)

left ,

right ,

1,

det )

d e t=F a l s e ) :

l e f t >= r i g h t :
return

mid =

def

p a r t i t i o n (A,

left ,

right ,

r e c u r s i v e Q u i c k S o r t (A,

left ,

r e c u r s i v e Q u i c k S o r t (A,

mid + 1 ,

p a r t i t i o n (A,
inf

left ,

right ,

det )

mid

1,

right ,

det )
det )

d e t=F a l s e ) :

left

sup = r i g h t + 1
if

not

det :
mid = random . r a n d i n t ( l e f t ,
A[ l e f t ] ,

mid =

left

while

True :

A [ mid ]

= A [ mid ] ,

right )
A[ l e f t ]

i n f += 1
while

i n f <= r i g h t

and A [ i n f ] <= A [ l e f t ] :

i n f += 1

sup

w h i l e A[ sup ] > A[ l e f t ] :
sup
if

i n f < sup :
A[ i n f ] ,

A[ sup ]

= A[ sup ] ,

A[ i n f ]

else :
break
A[ l e f t ] ,
return

A[ sup ]

= A[ sup ] ,

A[ l e f t ]

sup

q u i c k s o r t I t e r . py
def

q u i c k S o r t I t e r (A,

d e t=F a l s e ) :

i t e r a t i v e Q u i c k S o r t (A,
def

i t e r a t i v e Q u i c k S o r t (A,

0,

left ,

l e n (A)
right ,

theStack = Stack ( )
t h e S t a c k . push ( l e f t )
t h e S t a c k . push ( r i g h t )
while

not

t h e S t a c k . isEmpty ( ) :

r i g h t = t h e S t a c k . pop ( )
left

= t h e S t a c k . pop ( )

29

1,

det )

d e t=F a l s e ) :

if

r i g h t <=

left :

continue
mid =

p a r t i t i o n (A,

left ,

right ,

det )

t h e S t a c k . push ( l e f t )
t h e S t a c k . p u s h ( mid

1)

t h e S t a c k . p u s h ( mid + 1 )
t h e S t a c k . push ( r i g h t )

partizionamento (procedura divide): il partizionamento divide ri-

corsivamente una sequenza

n-dimensionale in due sottosequenze di taglia


l1 - ed l2 -dimensionali tali che, scelto un

non necessariamente bilanciata


perno

appartenente alla sequenza, la sottosequenza di sinistra con-

tenga tutti gli elementi minori o uguali al perno, e la sottosequenza di


destra tutti gli elementi maggiori del perno. Il partizionamento in loco:
si scorre infatti la sequenza avanzando in parallelo da sinistra verso destra
e da destra verso sinistra, mettendo gli elementi minori o uguali al perno
nella parte inferiore dell'array, e quelli maggiori nella parte superiore. Al
passo

i-esimo

vale l'invariante

A[i : inf 1] x

A[sup + 1 : f ] > x.

fusione (procedura impera): la fusione concatena ripetutamente due

sottosequenze ordinate.

Integer Sort

L' Integer Sort un algoritmo di ordinamento lineare non in

loco che ordina

numeri interi eventualmente ripetuti nell'intervallo

52

con costo di esecuzione


una sequenza ausiliaria

Tw (n) = O(n + k)53 .

k -dimensionale

[0, k 1]

L'idea di base inserire in

le frequenze dei numeri nella sequenza

n-dimensionale.
A n-dimensionale A[0 : n 1]non ordiY k -dimensionale i cui indici corrispon-

L'algoritmo prende in input un array


nato. Utilizza una sequenza ausiliaria
dono agli interi in

A,

e gli elementi ne indicano le corrispondenti occorrenze.

52 Integer Sort non eettua confronti, quindi per analizzarlo dobbiamo considerare il tempo
di esecuzione piuttosto che applicare un modello dei confronti. Per questo non contraddice il
teorema del lower bound per il problema dell'ordinamento.
53 Il costo totale T (n) = t + t , dove t il costo dell'inizializzazione della sequenza
w
1
2
1
ausiliaria k -dimensionale, mentre t2 il costo della scrittura sulla sequenza n-dimensionale.

t1 = O(k)
t2 = O(n)

Quindi
Tw (n) = O(n + k)

Infatti il tempo speso per riazzerare tutti i contatori della sequenza ausiliaria pari al tempo
speso per incrementarli, cio O(n).
Trattandosi di due cicli innestati si potrebbe pensare a Tw (n) = O(n2 ), ma, sebbene il ciclo
interno possa essere eseguito n volte, quello esterno non esegue tutte le n iterazioni.
Notiamo che questo tempo lineare nella dimensione dell'istanza di ingresso se k = O(n),
ovvero se i numeri da ordinare assumono valori in un intervallo [0, k 1] non troppo grande.
qualora k = O(nc ) = (n) Radix Sort preferibile in quanto garantisce un tempo di
esecuzione comunque lineare a fronte di un intervallo potenziale in n.
30

scrittura, scrive

lettur a, legge A[i] ed incrementa


S[j : Y [A[i]] 1] = i .

Bucket Sort

Il Bucket Sort un algoritmo di ordinamento lineare non

Alla

i-esima

in loco stabile

54 che ordina

costo di esecuzione

n record con
Tw (n) = O(n + k)55 .

di uno

Y [A[i]].

Alla

chiavi intere nell'intervallo

j -esima

[1, k],

con

A n-dimensionale A[0 : n 1] non


[1, k]. Utilizza una
sequenza ausiliaria Y k -dimensionale con indici in [1, k], i cui elementi sono liste
di elementi di A partizionati per chiave. Inne concatena le liste di elementi in
ordine crescente di chiave, e copia il risultato in A.
L'algoritmo prende in input un array

ordinato, i cui elementi sono record con chiavi intere in

Radix Sort

Il Radix Sort un algoritmo di ordinamento lineare non in

56 che ordina

n numeri interi rappresentati in base b =


m
(n)
nell'intervallo [1, k], con k = O(n ), con costo di esecuzione Tw (n) =

 
log k
57 . L'idea di base di applicare il Bucket Sort con un approcO n 1 + log
n
cio bottom-up, eseguendolo sugli n numeri a partire dalla cifra meno signicativa

loco corretto e stabile

no a quella pi signicativa.


r a d i x s o r t . py
def

radixSort ( listOfIntegers ,
cifrek
for

k,

b):

= i n t ( math . c e i l ( math . l o g ( k + 1 ,
in

range (1 ,

cifrek

bucket =

[]

for

range (0 ,

in

b)))

1):

b):

54 aggiungendo ordinatamente elementi alle liste Y [i] della sequenza ausiliaria, la concatenazione nale ne preserva l'ordine.
55 Il costo totale T (n) = O(n + k) in quanto segue la stessa logica di Integer Sort.
w
56 l'approccio bottom-up del Radix Sort permette infatti di non suddividere mai la sequenza
di ingresso.
57 Il costo totale T (n) = t + ct , dove t il tempo necessario alla lettura della sequenza,
w
1
2
1
c il numero di cifre necessarie a rappresentare gli interi in [1, k] in base b, e t2 il tempo
necessario ad ogni Bucket Sort. Assumiamo k = O(nm ) con m > 1, e b = (n).

t1 = O(n)

in quanto la sequenza n-dimensionale, ed ogni elemento deve essere letto al pi una volta.
c = O(logb k)

in quanto ogni intero in [1, k] in base b necessita al pi delle cifre necessarie a rappresentare
in base b.

t2 = O(n)

in quanto il costo totale di ogni Bucket Sort O(n + k); in Radix Sort il Bucket Sort viene
utilizzato per ordinare n oggetti (interi in [1, k] in base b) con chiavi in [0, b 1], quindi il
costo di ogni Bucket Sort O(n + b), ma b = (n) per ipotesi. Quindi
 

 

logc k
log k
Tw (n) = O(n)+O(logn k)O(n) = O(n(1+logn k)) = O n 1 +
=O n 1+
logc n
log n

31

b u c k e t . append ( Queue ( ) )
for

in

range (0 ,

len ( listOfIntegers ) ) :

cifratj

cifratj

= int ( c i f r a t j

l i s t O f I n t e g e r s [ j ] % math . pow ( b ,
/

math . pow ( b ,

bucket [ c i f r a t j ] . enqueue ( l i s t O f I n t e g e r s [ j ] )
j = 0
for

in

bucket :
while

not

e . isEmpty ( ) :

listOfIntegers [ j ]

= e . dequeue ( )

j += 1

A n-dimensionale A[0 : n 1] non


[1, k], con k = O(nm ). Chiama Bucket Sort

L'algoritmo prende in input un array


ordinato, i cui elementi sono interi in

t rispetto alla quale eettuare l'ordinamento.


t-esima cifra in base b inizializza una sequenza
ausiliaria Y b-dimensionale Y [0 : b 1] di liste. Concatena ordinatamente le
liste in Y , sovrascrivendole in A. A questo punti gli elementi in A[0 : n 1]
saranno stati ordinati secondo la t-esima cifra meno signicativa.
Al passo i-esimo, i numeri, gi ordinati secondo le prime (i1) cifre meno signicative, vengono ordinati secondo l'i-esima cifra meno signicativa, attraverso
la i-esima chiamata a Bucket Sort.
su

indicando la base

e la cifra

Il generico Bucket Sort sulla

32

t)

1))

Selezione e statistiche di ordine

Il problema della selezione e delle statistiche d'ordine consiste nell'estrarre


da grandi quantit di dati un piccolo insieme di numeri che ne rappresentino
alcune caratteristiche statisticamente salienti.
Il problema della selezione consiste nel trovare, all'interno di un in-

n elementi, l'elemento che occuperebbe la k -esima posizione, con k


[1, n] Z, se l'insieme fosse ordinato. Il mediano il valore che occuperebbe la
n
posizione p q, se l'insieme degli n oggetti fosse ordinato; calcolabile in tempo
2
n
di esecuzione O(n log n); un caso particolare di selezione per k = p q.
2
sieme di

Heap Select

Heap Select un algoritmo di selezione operante su heap

basato sul minimo, con costo di esecuzione


di cancellare
ottenuto

59 .

Tw (n) = O(n + k log n)58 .

L'idea

k 1 volte il minimo dall'heap, restituendo poi il minimo dell'heap

L'algoritmo prende in input un array

n-dimensionale A[0]...[n 1]

non ordi-

nato, lo ordina secondo la notazione posizionale heap con struttura raorzata.


Cancella dall'heap il minimo per

(k 1) volte, e termina quando, dopo la (k 1)k -esimo minimo, ovvero il

esima cancellazione del minimo, la radice conterr il

k -esimo

elemento della sequenza ordinata.

Quick Select

Quick Select un algoritmo di selezione randomizzata basato

su ricorsione sull'opportuno partizionamento attorno ad un perno randomiz-

58 Il costo totale T (n) = O(t +(k1)t +t ), dove t


w
1
2
3
1

il costo di inizializzazione dell'heap,


il costo di cancellazione del minimo dall'heap, operazione che deve essere ripetuta (k 1)
volte, e t3 il costo di estrazione del minimo dall'heap.

t2

t1 = O(n)

in quanto il costo di inizializzazione di un heap con notazione posizionale O(n).


t2 = O(log n)

in quanto la cancellazione del minimo da un heap basato sul minimo O(1) +


dovuto alla cancellazione della radice e dal ristabilimento
dell'ordinamento a heap.

O(1) O(log n) = O(log n),

t3 = O(1)

in quanto l'estrazione del minimo una semplice indicizzazione sul vettore posizionale.
59 il minimo estratto dall'heap ottenuto dopo k 1 cancellazioni del minimo corrisponde
evidentemente al k-esimo elemento della sequenza ordinata.

33

zato, con costi di esecuzione

Tw (n) = O(n2 )60

Te (n) = O(n)61 .

L'idea di

partizionare la sequenza disordinata intorno ad un perno randomizzato, e di


ricorrere sul partizionamento contenente il

Select

k -esimo

elemento

62 .

Select un algoritmo di selezione deterministica basato su ricorsione

sull'opportuno partizionamento attorno ad un perno deterministico, determinato ricorsivamente come mediano dei mediani delle

5-tuple

dell'array, con

60 Il

costo totale Tw (n) = O(t1 ) + T (n1 ), dove t1 il costo del partizionamento attorno
ad un generico perno, ed n1 la dimensione worst case del partizionamento in cui ricorre
l'algoritmo.
t1 = O(n)

in quanto il costo di partizionamento intorno ad un generico perno O(n).


n1 = (n 1)

in quanto nel worst case il partizionamento produce una sottosequenza vuota, una contenente il perno ed una contenente tutti gli elementi rimanenti.
Quindi
Tw (n) = O(n) + T (n 1)

che, come visto nel Quicksort, a cui Quick Select si ispira, T (n) = O(n2 ).
61 Il costo atteso T (n) = t +P
e
1
i=|partizionamento| P Tw (i), dove t1 il costo del partizionamento attorno ad un generico perno, e P la probabilit di ricorrere su un partizionamento
di cardinalit i.
t1 = n 1

in quanto il partizionamento intorno ad un generico perno si eettua con (n 1) confronti.


P =

1
n
2

2
n

in quanto nel worst case il partizionamento su cui ricorrere i-dimensionale, con n2


; ne consegue che n2 il numero di partizionamenti i-dimensionali, ed essendo
equiprobabili,. allora P = n1 .
2
Quindi
i n1

Te (n) = n 1 +

n1
X

i= n
2

2
Tw (i)
n

che ha come soluzione Te (n) 4n, da cui


Te (n) = O(n)
62 la

cardinalit dei partizionamenti

fa capire in quale di essi si trova il k-esimo elemento.

34

costo di esecuzione

Tw (n) = O(n)63 .

L'idea di partizionare l'array non ordi-

nato intorno al mediano dei mediani delle


amento contenente il

k -esimo

5-tuple, e di ricorrere sul partizion-

elemento.

63 Il

costo totale T (n) = t1 + T (n1 ) + T (n2 ), dove t1 il costo del calcolo dei mediani delle
5-dimensionali, n1 la dimensione worst case della ricorsione per il calcolo
del mediano dei mediani, ed n2 la dimensione worst case della ricorsione per la selezione.
sottosequenze

n
6n
11n
t1 6p q =
+1=
5
5
5

in quanto il calcolo del mediano di una quintupla eettua 6 confronti worst case, e una
sequenze n-dimensionale contiene p n5 q quintuple.
n
n1 = p q
5

in quanto il calcolo del mediano dei mediani eettuato ricorrendo su una sequenza di p n5 q
mediani.
n2 =

7n
+3
10

in quanto, dato il mediano dei mediani e partizionata la sequenza intorno ad esso, vengono
n
scartati dalla ricorsione certamente p 12 p n5 qq = p 10
q mediani, quindi
n
p 10
q 1 quintuple contententi almeno 3 elementi scartabili, dunque la dimensione della
ricorsione deve essere al pi

 n
7n
+3
n2 = n 3 p q 1 =
10
10

Quindi
T (n) =



 n 
11n
7n
+T p q +T
+3
5
5
10

che risulta
T (n) 22n = O(n)

in quanto, applicando il metodo della sostituzione, ipotizziamo induttivamente che


T (n) (cn 4c)

risulterebbe che
T (n)

soddisfatta per

n

11n
+c
+ 1 4c + c
5
5


11
9c
+
5
10

7n
+3
10


4c =

11
9c
+
5
10


n 4c (cn 4c) c 22

quindi
T (n) 22n T (n) = O(n)

35


n 4c

Alberi di ricerca

Gli alberi di ricerca sono strutture dati che realizzano un dizionario

64 , sup-

portando le operazioni di inserimento, cancellazione e ricerca in tempo logaritmico

65 .

Alberi BST

66 un albero di ricerca che soddisfa le seguenti

Un albero bst

propriet:

associata una chiave

v contiene un elemento elem(v) cui


chiave(v) presa da un dominio totalmente ordinato.

propriet di struttura: ogni nodo

propriet di ricerca: la visita DFS simmetrica

67 restituisce le chiavi

in ordine non decrescente, ovvero:

 chiavi(sotto albero sinistro(v)) chiave(v).


 chiavi(sotto albero destro(v)) chiave(v).
Un albero binario di ricerca implementa le seguenti operazioni:

ricerca

T (h) = O(h)68 :

sfrutta su ogni nodo

la propriet di ricerca per

realizzare la ricerca binaria, ovvero decide ricorsivamente su ogni nodo


se proseguire la visita in

Tsin (v)

o in

Tdes (v).

64 un dizionario un tipo di dato su cui sono denite almeno inserimento, cancellazione


e ricerca. di elementi associati ad una chiave presa da un dominio su cui sia denita una
relazione d'ordine totale.
65 la realizzazione di un dizionario tramite strutture dati indicizzate o collegate comporterebbe un tempo lineare per inserimento e cancellazione.
66 ovvero Binary Search Tree, cio albero binario di ricerca.
67

visita in profondit (o visita DFS): i nodi vengono visitati con disciplina di accesso LIFO. A questo scopo l'insieme S viene rappresentato dalla struttura dati pila.
L'agoritmo di visita in profondit prosegue dall'ultimo nodo lasciato in sospeso, inpilando prima il glio destro poi quello sinistro. Cos facendo si scende in profondit
no a raggiungere la prima foglia sinistra, cio si inizier la visita del sottoalbero destro solo dopo aver completato la visita del sottoalbero sinistro. Esistono tre varianti
classiche di questa tiplogia di visita:
visita in profondit in preordine: si visita prima la radice, poi si eettua la
chiamata ricorsiva sul glio sinistro e destro.
visita in profondit simmetrica: si eettua prima la chiamata ricorsiva sul
glio sinistro, poi si visita la radice, ed inne si eettua la chiamata ricorsiva sul
glio destro.
visita in profondit in postordine: si eettuano prima le chiamate ricorsive
sul glio sinistro e destro, poi si visita la radice.

68 Il costo della ricerca T (h) = O(h), in quanto ad ogni confronto si scende di un livello
nell'albero. Nel caso peggiore attraversiamo ogni livello dell'albero a partire dalla radice.

36

genitore

T (h) = O(h)69 :

crea il nuovo nodo v , ne cerca l'opportuno


u, e lo appende come glio sinistro o destro in modo da conservare

inserimento

la propriet di ricerca.

massimo

T (h) = O(h)70 :

scende dalla radice verso destra nch possibile,

resituendo la foglia estrema destra.

71

predecessore

T (h) = O(h)72 :

Se

ha un glio sinistro, il suo prede-

cessore il massimo nel sottoalbero sinistro radicato in


glio sinistro, il predecessore il pi basso antenato di
anch'esso antenato di

v:

v.

Se

non ha un

il cui glio destro

in pratica, risaliamo quindi da

verso la radice

no ad incontrare la prima svolta a sinistra.

v ha un
v e w diventa la nuova radice,
altrimenti elimina v collegando w al padre u di v . Se v ha due gli, scambia
v con il suo predecessore lo elimina.
cancellazione

unico glio

w,

T (h) = O(h):

Se

una foglia, lo elimina. Se

nel caso sia radice elimina

h supporta operazioni di inserimento, cancellazione e


T (h) = O(h). Se l'albero bilanciato h = O(log n), altrimenti,
73
molto profondo e sbilanciato , h = O(n).

Un albero BST di altezza


ricerca in tempo
nel caso sia

Alberi AVL

74 un albero di ricerca che estende l'albero

Un albero AVL

BST al bilanciamento in altezza

75 basato sulla tecnica delle rotazioni di base.

Questi alberi hanno dunque lo scopo di ridurre il costo delle operazioni dipendenti da

H(n),

mantenendosi bilanciati in altezza anche dopo operazioni di

inserimento o cancellazione.
Il bilanciamento in altezza: viene realizzato associando a ciascun nodo
un fattore di bilanciamento

(v) = altezza(sin(v)) altezza(des(v))


Un albero bilanciato in altezza se per ogni nodo

(v) 1
69 Il

(21)

v
(22)

costo dell'inserimento T (h) = O(h), in quanto un inserimento comporta la ricerca


e la modica di un numero costante di puntatori per conservare la propriet
di ricerca dell'albero, rispettivamente di costo O(h) e O(1). Quindi il costo totale di ogni
inserimento T (h) = O(h).
70 Il costo della ricerca del massimo T (h) = O(h), in quanto bisogna attraversare tutto
l'albero no all'ultimo livello.
71 il predecessore u del nodo v quel nodo avente massima chiave chiave(u) chiave(v).
72 Il costo della ricerca del predecessore T (h) = O(h), in quanto nel caso peggiore bisogna
risalire tutto l'albero no alla radice.
73 l'intrinseca dinamicit dell'albero potrebbe comportarne una crescita in una direzione
privilegiata, il che equivale a rappresentarlo con una struttura indicizzata o collegata disordinata.
74 dai nomi degli ideatori Adel'son-Vel'skii e Landis, che li hanno introdotti nel 1962.
75 il bilanciamento in altezza garantisce un altezza logaritmica del numero di nodi, h(n) =
O(log n).
di un padre

37

Un nodo critico un nodo il cui fattore di bilanciamento non rispetta tale


condizione.
Le rotazioni di base permettono di ristabilire il bilanciamento in altezza
in un albero AVL. Dati un nodo critico

e un sottoalbero

responsabile dello

sbilanciamento, esse possono essere:

S = Tsin (sin(c))

rotazione SS: se

applichiamo sul nodo critico una

rotazione verso destra.

rotazione DD: se

S = Tdes (des(c))

applichiamo sul nodo critico una

rotazione verso sinistra.

rotazione SD: se

S = Tdes (sin(c))

applichiamo sul glio sinistro del

nodo critico una rotazione semplice verso sinistra, poi applichiamo al nodo
critico una rotazione semplice verso destra.

rotazione DS: se

S = Tsin (des(c)) applichiamo sul glio destro del nodo

critico una rotazione semplice verso destra, poi applichiamo al nodo critico
una rotazione semplice verso sinistra.
Se

unico, una rotazione SS, DD, SD o DS applicata ad un nodo critico,

fa decrescere di uno l'altezza del sottoalbero radicato nel nodo critico: quindi
una rotazione semplice o doppia sul nodo critico suciente a ristabilire il

bilanciamento in altezza. Se

non unico, l'applicazione di rotazioni semplici

permette di ribilanciare il sottoalbero radicato nel nodo critico, ma l'altezza del


sottoalbero rimane costante.
Un albero AVL con

nodi ha altezza

h(n) = O(log n)76 .

Un albero AVL supporta le seguenti operazioni:

ricerca

inserimento

T (n) = O(log n):

ereditata dagli alberi BST.

T (n) = O(log n):

ereditata dagli alberi BST, ed estesa per

ricalcolare i fattori di bilanciamento dei nodi appartenenti al cammino


dalla radice a

v;

conserva il bilanciamento in altezza tramite opportune

rotazioni applicate ai nodi con fattore di bilanciamento

(v) 2.

Un

inserimento pu causare uno sbilanciamento su un unico sottoalbero:

76 l'altezza
bonacci

di un albero AVL h(n) = O(log n), lo dimostriamo attraverso l'albero

di Fi-

Un albero di Fibonacci Tf ib l'albero dell'insieme degli alberi AVL di altezza h, con


il minimo numero di nodi. Il numero di nodi e l'altezza di un albero di Fibonacci sono
rispettivamente h(n) = (log n) e nh = 1 + nh1 + nh2 .
Considerati un generico albero AVL T di altezza h ed n nodi, e un albero di Fibonacci Tf ib
di altezza h ed nf ib nodi. Poich
h(n) = h(nf ib ) = (log n)

e per denizione
nf ib n

allora
h(n) = O(log n)

38

cancellazione

T (n) = O(log n):

ereditata dagli alberi BST, ed estesa

per ricalcolare i fattori di bilanciamento dei nodi appartenenti al cammino


dalla radice al padre di

v;

conserva il bilanciamento in altezza tramite

opportune rotazioni applicate ai nodi con fattore di bilanciamento

2.

(v)

Una cancellazione pu causare uno sbilanciamento su pi sottoalberi.

n nodi supporta
T (n) = O(log n).

Un albero AVL con


ricerca in tempo

Alberi 2-3

operazioni di inserimento, cancellazione e

Un albero 2-3 un albero di ricerca in cui ogni nodo interno

ha 2 o 3 gli e tutti i cammini radice-foglia hanno la stessa lunghezza.


foglie contengono gli elementi del dizionario, e ogni nodo interno

Le

mantiene

due informazioni supplementari:

S[v] = max(Tsin (v)).


M [v] = max(Tcent (v)).
Un albero 2-3 con

nodi,

Presente solo se
foglie e altezza

2h+1 1 n

grad(v) = 3.

soddisfa le seguenti relazioni:

(3h+1 1)
2

(23)

2h f 3h
L'altezza di un albero 2-3 dunque

(24)

h(n) = (log n).

A fronte di inserimenti e cancellazioni, il grado dei nodi interni potrebbe


divenire superiore a 3 o inferiore a 2. Ovviamo a queste condizioni, rispettivamente, con l'operazione split e l'operazione fuse.

split

v,

T (n) = O(1):

se

grad(v) > 3, crea un nuovo nodo w a destra di


v con chiavi minime restano gli di v ,
massime diventano gli di w . Ad ogni chiamata,

sul suo stesso livello; i gli di

mentre quelli con chiavi


aggiorna

di

v, w

ed eventualmente dei loro antenati.

Ogni split

crea un nuovo nodo nei livelli intermedi, quindi potrebbe essere necessario
eseguirla ricorsivamente anche sugli antenati di

v.

T (n) = O(1): se grad(v) < 2 e il fratello l sinistro (destro) di v


grad(l) = 3, sposta il glio destro (sinistro) di l come glio sinistro
(destro) di v . Se grad(l) = 2, sposta l'unico glio di v come glio destro
(sinistro) di l, ed elimina v . Ad ogni chiamata, aggiorna S e M di v , l ed
fuse

ha

eventualmente dei loro antenati.


Un albero 2-3 supporta le seguenti operazioni:

77 il

prosegue

T (n) = (log n)77 :


la ricerca in Tsin (v),

Tcent (v),

altrimenti prosegue la

ricerca

ereditata dagli alberi BST. Se

S[v] k M [v]
ricerca in Tdes (v).

se

costo della ricerca T (n) = (log n), in quanto proporzionale all'altezza.


39

k S[v]

prosegue la ricerca in

inserimento

il genitore

T (n) = O(log n)78 :

eventualmente

M [u],

aggiunge quindi il nuovo nodo

ha due gli, inserisce

ha gi tre gli, aggiunge


split sul nodo

u,

e ne identica

con

S[u]

ed

come foglia. Se

opportunamente come glio sinistro, centrale, o

destro, eventualmente aggiornando

crea un nuovo nodo

nel penultimo livello confrontando la chiave

di

e dei suoi antenati. Se

come quarto glio di

ed esegue l'operazione

eventualmente anche sugli antenati.

T (n) = O(log n): Se v la radice, lo rimuove rendendo


u di v ha tre gli, rimuove v , eventualmente
aggiornando S e M di u e dei suoi antenati. Se u ha due gli, qualora
sia u sia radice, elimina sia v che u, lasciando l'altro glio di u come
radice; qualora invece u non sia radice, esegue l'operazione fuse su u ed
cancellazione

vuoto l'albero; se il padre

eventualmente sui suoi antenati.

n nodi supporta
T (n) = O(log n).

Un albero 2-3 con


ricerca in tempo

B-alberi

operazioni di inserimento, cancellazione e

Un B-albero un albero di ricerca che intende minimizzzare gli

accessi alla memoria secondaria aumentando l'informazione a carico di ciascun


nodo, proporzionalmente alla dimensioni dei blocchi di I/O. Fissato il grado
minimo

t 279 ,

un B-albero denito dalle seguenti propriet:

tutte le foglie hanno la stessa profondit.

la radice mantiene

ogni nodo diverso dalla radice mantiene

kr

chiavi ordinate,

1 kr 2t 1.
kv

chiavi ordinate,

t 1 kv

2t 1.

ogni nodo interno, compresa la radice, ha

le chiavi separano gli intervalli di chiavi memorizzati in ciascun sottoal-

ci una qualunque chiave


v , e chiavej una qualunque chiave
ci+1
i [1, kv + 1], j [1, kv ].
bero: se

Un nodo pieno un nodo contenente


vuoto un nodo contenente

(t 1)

kv + 1

gli.

dell'i-esimo sottoalbero di del nodo


del nodo

(2t 1)

allora

ci chiavej

chiavi; mentre un nodo quasi

chiavi.

Inserimenti e cancellazioni di elementi comportano inserimenti e cancellazioni di chiavi.

A fronte di inserimenti e cancellazioni di chiavi, un nodo

interno potrebbe avere

2t

chiavi o

t2

chiavi. Ovviamo a queste condizioni,

rispettivamente, con l'operazione split e l'operazione fuse.

78 il costo dell'inserimento T (n) = O(log n), in quanto nel caso peggiore tutti i nodi hanno
gi tre gli e la split dovr dunque essere eseguita su tutti i nodi no alla radice, comportando
inoltre l'aggiunta di una nuova radice.
79 il grado minimo di un B-albero viene scelto proporzionalmente alla dimensione dei blocchi
di I/O.

40

split

T (n) = (t): ereditata dagli


kv chiavi. Divide il

alberi 2-3, ed estesa per supportare

nodi interni con


stesso livello.
destro le

Il nodo sinistro mantiene le

t-esima

chiavi massime, mentre la

l'alto a separare i due nodi.

2t chiavi in due nodi sullo


t 1 chiavi minime, quello

nodo con

chiave vieni spinta verso

Eventualmente propaghiamo la split verso

l'alto.

fuse

T (n) = (t): ereditata dagli alberi


kv chiavi. Elimina il nodo

nodi interni con

2-3, ed estesa per supportare


con

t2

chiavi, cedendone le

chiavi e i gli al fratello con il minor numero di chiavi.


Un B-albero supporta le seguenti operazioni:

T (n) = O(log n)80 : ereditata dagli alberi BST. In ogni nodo v


trova la pi piccola chiave kj , tale che la chiave cercata k sia k kj , e
prosegue la ricerca nel sottoalbero di v contenuto tra le chiavi kj1 e kj .
Altrimenti prosegue nel sottoalbero estremo destro di v .
ricerca

inserimento

il genitore

T (n) = O(t logt n)81 :

crea una nuova foglia

nel penultimo livello.

Se

mente la nuova chiave, e posiziona opportunamente


di

u.

Se

e ne identica

non pieno inserisce ordinata-

come nuovo glio

pieno inserisce ordinatamente la nuova chiave, posiziona op-

portunamente

come nuovo glio di

u,

ed esegue l'operazione split su

u,

propando eventualmente le split verso l'alto.

cancellazione

rimuove

v,

T (n) = O(t logt n):

se il padre

e uno dei fratelli sinistro e destro di

v
u.

di

eliminandone la corrispondente chiave in

non quasi vuoto,


Se

u quasi vuoto

non quasi vuoto, ridistribuisce le

80 il costo della ricerca T (n) = O(t t ), dove t il costo della ricerca di una chiave
1
2
1
all'interno di un nodo, e t2 il numero di volte in cui operiamo questa ricerca.

t1 = O(log t)

in quanto eseguiamo una ricerca (t)-aria nelle chiavi di un nodo.


t2 = O(logt n)

in quanto operiamo questa ricerca (t)-aria su ogni nodo visitato, a partire dalla radice;
dovendo visitare un nodo per ogni livello, nel caso peggiore visiteremo un numero di nodi pari
all'altezza del B-albero, la quale
h logt

n+1
h = O(logt n)
2

Quindi
T (n) = O(logt logt n)

e applicando il cambiamento di base dei logaritmi, per cui loga b =

logc b
logc a

, otteniamo

T (n) = O(log n)
81 il costo dell'inserimento T (n) = O(log n), in quanto nel caso peggiore tutti i nodi hanno
gi tre gli e la split dovr dunque essere eseguita su tutti i nodi no alla radice, comportando
inoltre l'aggiunta di una nuova radice.

41

chiavi di u tra i due fratelli. Se

quasi vuoto, e lo sono anche i fratelli

sinistro e destro, esegue l'operazione fuse su

u, propagando eventualmente

le fuse verso l'alto.

t ed n nodi supporta operazioni di inseriT (n) = O(t logt n), mentre la ricerca in tempo
T (n) = O(log n). Tutte e tre le operazioni sono supportate con T (n) = O(logt n)
Un B-albero con grado minimo

mento e cancellazione in tempo


operazioni di I/O.

42