scarica lucidi a cura del dott. Salvatore Cristofaro in formato pdf

annuncio pubblicitario
CORSO SPECIALE DI DURATA ANNUALE PER IL CONSEGUIMENTO DELL’ABILITAZIONE
ALL’INSEGNAMENTO NELLA SCUOLA SECONDARIA DI I e II GRADO
Indirizzo Fisico - Informatico - Matematico a.a. 2006/07 - Classe 42A - Informatica
ALGORITMI
Docente: Prof. Domenico Cantone
Modulo: STRUTTURE DATI ELEMENTARI
Lucidi a cura del Dott. Salvatore Cristofaro
Strutture dati elementari
Liste
• Una lista L è una sequenza finita (ed ordinata) di oggetti
x0 x1 x2
...
xn−1
chiamati elementi (della lista).
• Il primo elemento, x0, è chiamato la testa (head ) della lista.
• Il numero n di oggetti contenuti nella lista è la lunghezza della lista.
• Generalmente, ogni elemento xi di una lista è un oggetto costituito da diversi campi, che contengono vari
tipi di informazioni. Uno dei campi viene usualmente distinto come la chiave, indicata con Key(xi), che,
in alcuni casi, individua in maniera univoca l’oggetto stesso. (Es. lista degli utenti di una compagnia
telefonica, dove la chiave potrebbe essere il codice fiscale.)
• Operazioni su una lista
– Query (o interrogazioni): sono operazioni che permettono di carpire informazioni pertinenti alla
lista (non ne modificano in alcun modo la struttura)
Es. determinare la lunghezza di una lista, controllare se un dato oggetto è un elemento della lista,
determinare il massimo di una lista, etc.
– Operazioni di Modifica: sono operazioni che permettono di manipolare una lista, a seguito delle
quali ne viene modificata o soltanto l’organizzazione dei suoi elementi (Es. ordinamento di una lista),
o ne viene cambiata anche la lunghezza (Es. inserimenti e cancellazioni)
• Una lista su cui sono previsti inserimenti e/o cancellazioni è chiamata lista dinamica.
Strutture dati elementari
• Operazioni su una lista (cont.)
– Query:
1. Visita (o Attraversamento): Visit(L)
2. Ricerca di un elemento: Search(L, x)
– Operazioni di Modifica:
3. Inserimento di un nuovo elemento: Insert(L, x)
4. Eliminazione di un elemento: Delete(L, x)
• Implementazioni di una lista
– Array: implementazione statica in cui la lunghezza massima della lista viene fissata a priori.
Permette di implementare in maniera semplice ed efficiente varie operazioni su una lista. Poco efficiente per inserimenti e cancellazioni. Può comportare notevole spreco di spazio nel caso di liste con
lunghezza molto variabile.
– Linked-List (Liste-Linkate): implementazione dinamica in cui, in linea di principio, non viene
fissato alcun limite massimo sulla lunghezza della lista (dipende dalla memoria fisica che si ha a
disposizione).
Molto flessibile e permette di implementare efficientemente operazioni di inserimento e cancellazione.
Altre operazioni possono tuttavia risultare più dispendiose (e difficili da implementare) che non nel
caso degli array. Non comporta spreco di spazio.
Strutture dati elementari
Array
• Un array A è un insieme di “celle contigue” contenenti tutte lo stesso tipo di dato (Es. “array di interi”,
“array di caratteri”, “array di stringhe”, etc.)
• Il numero delle celle che compongono un array viene fissato al momento in cui l’array stesso viene creato.
• Ogni cella è indicizzata da un numero intero a partire da 0.
• Un array è una struttura dati ad accesso diretto, nel senso che si può accedere ai suoi elementi semplicemente conoscendo la relativa posizione nell’array (indice della cella che contiene quell’elemento).
L’elemento che si trova nella cella i è A[i].
• La lunghezza di un array sarà indicata con L[A].
Pertanto, ogni riferimento alle celle A[L[A]], A[L[A] + 1], A[L[A] + 2], . . . comporterà un errore.
A
A[0]
0
1
A[1]
A[2]
2
L[A] − 1
A[L[A] − 1]
• Rappresenteremo una lista mediante un array A dotato dell’attributo Last[A] che indicizza l’ultimo elemento della lista. Pertanto, gli elementi della lista verranno memorizzati nelle celle A[0], . . . , A[Last[A]].
La lista è vuota quando Last[A] = −1.
Strutture dati elementari
Operazioni su Array
• Attraversamento (o visita)
Visit(A)
1. i := 0
2. while (i ≤ Last[A]) do
3.
print (A[i])
4.
i := i + 1
• Ricerca
Search(A, key)
1. i := 0
2. while ((i ≤ Last[A]) and (A[i] 6= key)) do
3.
i := i + 1
4. if (i > Last[A]) then
5.
return (−1)
6. else
7.
return (i)
Strutture dati elementari
• Complessità di un algoritmo
Visit(A)
1. i := 0
(costo c1)
2. while (i ≤ Last[A]) do
(costo c2)
3.
print (A[i])
(costo c3 )
4.
i := i + 1
(costo c4 )
Sia n il numero di elementi della lista correntemente contenuta nell’array A (cioè n = Last[A] + 1)
1. La condizione in linea 2. viene testata n + 1 volte, e quindi contribuisce con un costo totale pari a
(n + 1) · c2
2. Le istruzioni delle linee 3. e 4. vengono eseguite ciascuna n volte, per un costo totale pari a n·c3 +n·c4
3. Considerando anche l’istruzione in linea 1., si ha quindi che il “costo totale dell’algoritmo”, o runningtime, è dato da
T (n) = c1 + (n + 1) · c2 + n · c3 + n · c4 = n · (c2 + c3 + c4) + (c1 + c2) .
Poiché
T (n)
= c2 + c3 + c4 ,
n−→∞ n
esiste una constante c tale che T (n) ≤ c · n definitivamente, e quindi T (n) = O(n).
lim
Notazioni Asintotiche
In generale, date due finzioni f (n) e g(n), scriviamo f (n) = O(g(n)) (risp., f (n) = Ω(g(n))) se esistono
una costante c e un intero ν tali che f (n) ≤ c · g(n) (risp., c · g(n) ≤ f (n)) per ogni n ≥ ν. Scriviamo
f (n) = Θ(g(n)) se f (n) = O(g(n)) e f (n) = Ω(g(n)).
Strutture dati elementari
• Inserimento
Insert(A, key)
1. if Last[A] = L[A] − 1 then
2.
print (ERROR)
3. else
4.
Last[A] := Last[A] + 1
5.
A[Last[A]] := key
T (n) = O(1)
Insert∗ (A, key, position)
1. for i := Last[A] downto position do
2.
A[i + 1] := A[i]
3. A[position] := key
4. Last[A] := Last[A] + 1
T (n) = O(n)
• Eliminazione
Delete(A, key)
1. i := Search(A, key)
2. if i = −1 then
3.
print (ERROR)
4. else
5.
for j := i + 1 to Last[A] do
6.
A[j − 1] := A[j]
7. Last[A] := Last[A] − 1
T (n) = O(n)
Strutture dati elementari
Esempi
• Determinare il massimo di un array di numeri interi (si assuma che Last[A] ≥ 0)
Max(A)
1. max := A[0]
2. i := 1
3. while i ≤ Last[A] do
4.
if A[i] > max then
5.
max := A[i]
6.
i := i + 1
7. return (max)
T (n) = O(n)
• Determinare la somma degli elementi di un array di numeri interi
Somma(A)
1. somma := 0
2. i := 0
3. while i ≤ Last[A] do
4.
somma := somma + A[i]
5.
i := i + 1
6. return (somma)
T (n) = O(n)
RS(A, i)
1. somma := 0
2. if (i ≤ Last[A]) then
3.
somma := A[i]+ RS(A, i + 1)
4. return (somma)
Somma(A)
1. return (RS(A, 0))
Ricorsione
Strutture dati elementari
• Contare il numero di volte che un oggetto di chiave key occorre in un array A
Count(A, key)
1. count := 0
2. i := 0
3. while i ≤ Last[A] do
4.
if A[i] = key then
5.
count := count + 1
6.
i := i + 1
7. return (count)
T (n) = O(n)
• Controllare se un array A contiene due elementi consecutivi uguali
Check(A)
1. i := 0
2. while i < Last[A] do
3.
if A[i] = A[i + 1] then
4.
return (true)
5.
i := i + 1
6. return (false)
T (n) = O(n)
Strutture dati elementari
• Controllare se un array A contiene doppioni
isNotSet(A)
1. i := 0
2. while i < Last[A] do
3.
j := i + 1
4.
while j ≤ Last[A] do
5.
if A[i] = A[j] then
6.
return (true)
7.
j := j + 1
8.
i := i + 1
9. return (false) T (n) = O(n2)
isNotSet(A)
1. for i := 0 to Last[A] − 1 do
2.
for j := i + 1 to Last[A] do
3.
if A[i] = A[j] then
4.
return (true)
5. return (false)
• Controllare se un array A di numeri interi è ordinato (in senso non decrescente)
isOrd(A)
1. i := 0
2. while i < Last[A] do
3.
j := i + 1
4.
while j ≤ Last[A] do
5.
if A[i] > A[j] then
6.
return (false)
7.
j := j + 1
8.
i := i + 1
9. return (true) T (n) = O(n2)
isOrd(A)
1. for i := 0 to Last[A] − 1 do
2.
for j := i + 1 to Last[A] do
3.
if A[i] > A[j] then
4.
return (false)
5. return (true)
Strutture dati elementari
Operazioni su Array-Ordinati
Un lista
x0 x1 x2
...
xn−1
è ordinata rispetto alle chiavi, o, semplicemente, ordinata, se Key(x0) ≤ Key(x1) ≤ · · · ≤ Key(xn−1 ).
• Inserimento
InsertOrd(A, key)
1. i := 0
2. while ((i ≤ Last[A]) and (A[i] < key)) do
3.
i := i + 1
4. for j := Last[A] downto i do
5.
A[j + 1] := A[j]
6. A[i] := key
7. Last[A] := Last[A] + 1
T (n) = O(n)
InsertOrd(A, key)
1. i := 0
2. while ((i ≤ Last[A]) and (A[i] < key)) do
3.
i := i + 1
4. Insert∗ (A, key, i)
Strutture dati elementari
Linked-List
• In una linked-list L gli elementi di una lista vengono memorizzati in celle (o nodi) collegate tra loro
mediante dei link (o puntatori)
• Ogni nodo p contiene due campi: Key[p] e N ext[p]
– Key[p] contiene un elemento della lista
– N ext[p] contiene il link al nodo successivo
Il campo N ext dell’ultimo nodo della lista contiene un valore speciale, il N IL, che indica “fine-lista”
L
Head[L]
Key[p]
p
N ext[p]
N IL
• Rappresenteremo una lista mediante una linked-list L dotata dell’attributo HEAD[L] che ne indica il
primo nodo. La lista è vuota quando HEAD[L] = N IL
Strutture dati elementari
Operazioni su Linked-List
• Attraversamento (o visita)
Visit(L)
1. p := HEAD[L]
2. while (p 6= N IL) do
3.
print (Key[p])
4.
p := N ext[p]
T (n) = O(n)
VL(q)
1. if (q 6= N IL) then
2.
print (Key[q])
3.
VL(N ext[q])
Visit(L)
1. VL(HEAD[L])
Ricorsione
• Ricerca
Search(L, key)
1. p := HEAD[L]
2. while ((p 6= N IL) and (Key[p] 6= key)) do
3.
p := N ext[p]
4. return (p)
T (n) = O(n)
• Inserimento
Insert(L, key)
1. p := AllocateN ode()
// crea un nuovo nodo della lista
2. Key[p] := key
3. N ext[p] := HEAD[L]
T (n) = O(n)
4. HEAD[L] := p
Strutture dati elementari
• Eliminazione
Delete(L, key)
1. p := HEAD[L]
2. pred := N IL
3. while ((p 6= N IL) and (Key[p] 6= key)) do
4.
pred := p
5.
p := N ext[p]
6. if p 6= N IL then
7.
if pred 6= N IL then
8.
N ext[pred] := N ext[p]
9.
else HEAD[L] := N ext[p]
10. else print (ERROR)
T (n) = O(n)
Esempi
• Numero di elementi di una linked-list
Length(L)
1. q := HEAD[L]
2. count := 0
3. while (q 6= N IL) do
4.
count := count + 1
5.
q := N ext[q]
6. return (count)
RL(q)
1. count := 0
2. if (q 6= N IL) then
3.
count := 1+ RL(N ext[q])
4. return (count)
Length(L)
1. return RL(HEAD[L])
Ricorsione
Strutture dati elementari
• Controllare se due Linkde-List L1 e L2 sono uguali
CheckEq(L1, L2)
1. q := HEAD[L1]
2. p := HEAD[L2]
3. while (q 6= N IL and p 6= N IL and Key[q] = Key[p]) do
4.
q := N ext[q]
5.
p := N ext[p]
6. if (q = N IL and p = N IL) then
7.
print (Y ES)
8. else print (N O)
• Appiattimento di una linked-list: eliminare tutti gli elementi consecutivi uguali lasciandone uno solo
Flat(L)
1. q := HEAD[L]
2. while q 6= N IL do
3.
p := N ext[q]
4.
while (p 6= N IL and Key[p] = Key[q]) do
5.
p := N ext[p]
6.
N ext[q] := p
7.
q := p
Strutture dati elementari
• Inversione di una lista: stampare gli elementi di una lista in ordine inverso
Invert(L)
1. q := HEAD[L]
2. h := N IL (Creiamo una nuova lista, di testa h, che contiene L in ordine inverso)
3. while (q 6= N IL) do
4.
p := AllocateN ode()
5.
Key[p] := Key[q]
6.
N ext[p] := h
7.
h := p
8.
q := N ext[q]
9. p := h (Attraversiamo h)
10. while (p 6= N IL) do
11.
print (Key[p])
12.
p := N ext[p]
Ricorsione
RI(q)
1. if (q 6= N IL) then
2.
RI(N ext[q])
3.
print(Key[q])
Invert(L)
1. RI(HEAD[L])
Strutture dati elementari
Doubly-Linked-List
L
Head[L]
P rev[p]
p
Key[p]
N ext[p]
N IL
• Ogni nodo p contiene tre campi: P rev[p], Key[p] e N ext[p]
– P rev[p] contiene il link al nodo precedente.
Il campo P rev del primo nodo della lista contiene il valore speciale N IL.
– Key[p] contiene un elemento della lista.
– N ext[p] contiene il link al nodo successivo.
Il campo N ext dell’ultimo nodo della lista contiene il valore speciale N IL.
• La lista L è dotata dell’attributo HEAD[L] che ne indica il primo nodo. La lista è vuota quando
HEAD[L] = N IL.
Strutture dati elementari
Operazioni su Doubly-Linked-List
• Inserimento / Eliminazione
Insert(L, key)
1. p := AllocateN ode()
2. Key[p] := key
3. N ext[p] := HEAD[L]
4. P rev[p] := N IL
5. if HEAD[L] 6= N IL then
6.
P rev[HEAD[L]] := p
7. HEAD[L] := p
Delete(L, key)
1. p := HEAD[L]
2. while ((p 6= N IL) and (Key[p] 6= key)) do
3.
p := N ext[p]
4. if p 6= N IL then
5.
if P rev[p] 6= N IL then
6.
N ext[P rev[p]] := N ext[p]
7.
else
8.
HEAD[L] := N ext[p]
9.
if N ext[p] 6= N IL then
10.
P rev[N ext[p]] := P rev[p]
11. else
12.
print (ERROR)
Strutture dati elementari
• Eliminazione (2)
Delete∗ (L, p)
1. if P rev[p] 6= N IL then
2.
N ext[P rev[p]] := N ext[p]
3. else
4.
HEAD[L] := N ext[p]
5. if N ext[p] 6= N IL then
6.
P rev[N ext[p]] := P rev[p]
Delete(L, key)
1. p := Search(L, key)
2. if p 6= N IL then
3.
Delete∗ (L, p)
4. else
5.
print (ERROR)
Esempi
• Inversione di una doubly-linked-list
Invert(L)
1. q := HEAD[L]
2. while (q 6= N IL and N ext[q] 6= N IL) do
3.
q := N ext[q]
4. while (q 6= N IL) do
5.
print (Key[q])
6.
q := P rev[q]
Strutture dati elementari
Confronto tra Array e Linked-List
• Nell’implementazione di una lista usando un array bisogna specificare il massimo numero di elementi che
dovrà contenere la lista a tempo di compilazione (cioè quando l’array viene creato). Se non si conosce
a priori un bound sulla lunghezza massima che può raggiungere la lista è più consigliabile usare una
linked-list invece di un array.
• Certe operazioni risultano più dispendiose in una implementazione invece che in un’altra. Ad esempio,
inserimenti e cancellazioni in testa possono essere eseguite in tempo costante su di una linked-list, ma
richiedono tempo proporzionale al numero di elementi che seguono nel caso in cui is usi un array. Viceversa,
operazioni come
– EndList(L) che mi ritorna l’ultimo elemento di una lista, o
– Retrieve(L, p) che mi ritorna il p-esimo elemento di una lista
richiedono tempo costante con gli array ma risultano molto dispendiose con le linked-list (se p è grande).
• L’implementazione con array può sprecare molto spazio, dato che essa usa l’ammontare massimo di
spazio allocato per l’array indipendentemente dal numero degli elementi della lista attualmente contenuti
nell’array stesso. L’implementazione con linked-list usa invece spazio proporzionale al numero degli
elementi correntemente contenuti nella lista.
Strutture dati elementari
Stack
• Uno stack è una lista in cui inserimenti e cancellazioni avvengono tutti ad una stessa estremità chiamata
TOP. (Es. pila di libri, pila di piatti, etc.)
• Uno stack implementa una politica di tipo LIFO (LAST-IN-FIRST-OUT), ovvero, l’elemento ad
essere rimosso è sempre quello che è stato inserito per ultimo.
• Le operazioni di inserimento e di eliminazione in uno stack vengono chiamate PUSH e POP, rispettivamente:
– P ush(S, x) inserisce l’elemento x al TOP dello stack S
– P op(S) elimina (e ritorna) l’elemento che si trova al TOP dello stack S
• Implementeremo uno stack usando un array S dotato di un attributo T op[S] che ne indica il TOP.
Strutture dati elementari
Esempio
A
B
C
T op
P op(S)
P ush(S, a)
P ush(S, c)
a
A
B
C
c
a
A
B
C
T op
P op(S)
a
A
B
C
T op
A
B
C
T op
T op
Strutture dati elementari
Operazioni su Stack
• Emptyness
EmptyStack(S)
1. if T op[S] = −1 then
2.
return (true)
3. else
4.
return (false)
• Inserimento
Push(S, x)
1. T op[S] := T op[S] + 1
2. S[T op[S]] := x
• Eliminazione
Pop(S)
1. if EmptyStack(S) then
2.
print (ERROR: Stack “underflow”)
3. else
4.
T op[S] := T op[S] − 1
5.
return (S[T op[S] + 1])
Strutture dati elementari
Esempio: valutazione di espressioni aritmetiche
• Formato delle espressioni aritmetiche
– Forma Infissa (o standard/tradizionale): l’operatore viene posto tra gli operandi
Es.
1+2
(1 + 2) + 3
4 × ((1 + 2) + 3)
– Forma Postfissa (o polacca): l’operatore viene posto subito dopo gli operandi
Es.
1 2+
1 2+3 +
4 1 2+3 +×
– Forma Prefissa: l’operatore viene posto subito prima degli operandi
Es.
+12
++ 1 2 3
×4++ 1 2 3
• Considereremo i seguenti problemi
– Valutare un’espressione aritmetica che si presenta in forma Postfissa
– Convertire un’espressione aritmetica dalla forma Infissa alla forma Postfissa
– Valutare un’espressione aritmetica che si presenta in forma Infissa
– Convertire un’espressione aritmetica dalla forma Postfissa alla forma Infissa
Strutture dati elementari
• Valutare un’espressione aritmetica che si presenta in forma Postfissa
1. Assumiamo che l’espressione sia rappresentata da un array Expr[0 .. L − 1]
2. Esaminiamo l’espressione da sinistra verso destra
3. Usiamo uno stack S per mantenere i risultati parziali
4. Assumiamo una funzione Val(x, y, oper) che fornisce il risultato dell’operazione rappresentata da
“oper” e applicata agli operandi x e y (Es. Val(2, 3, +) = 5, Val(2, 3, ×) = 6, etc.)
PostfixVal(Expr)
1. for i := 0 to L − 1 do
2.
if hExpr[i] è un “operando”i then
3.
Push(S, Expr[i])
4.
else
5.
x := Pop(S)
6.
y := Pop(S)
7.
Push(S, Val(x, y, Expr[i]))
8. return (S[T op[S]])
Strutture dati elementari
• Convertire un’espressione aritmetica dalla forma Infissa alla forma Postfissa
1. Usiamo uno stack S per mantenere gli operatori
2. Se incontriamo un operando lo stampiamo
3. Se incontriamo un operatore lo “pushiamo” nello stack
4. Se incontriamo una parentesi chiusa, “poppiamo” lo stack e stampiamo l’operatore
InfixToPostfix(Expr)
1. for i := 0 to L − 1 do
2.
if hExpr[i] è un “operando”i then
3.
print (Expr[i])
4.
else
5.
if hExpr[i] è un “operatore”i then
6.
Push(S, Expr[i])
7.
else
8.
if hExpr[i] = “ ) ”i then
9.
oper := Pop(S)
10.
print (oper)
Strutture dati elementari
• Valutare un’espressione aritmetica che si presenta in forma Infissa
1. Usiamo uno stack S per mantenere i risultati parziali
2. Usiamo uno stack S ′ per mantenere gli operatori
3. Assumiamo una funzione Val(x, y, oper) che fornisce il risultato dell’operazione rappresentata da
“oper” e applicata agli operandi x e y (Es. Val(2, 3, +) = 5, Val(2, 3, ×) = 6, etc.)
InfixVal(Expr)
1. for i := 0 to L − 1 do
2.
if hExpr[i] è un “operando”i then
3.
Push(S, Expr[i])
4.
else
5.
if hExpr[i] è un “operatore”i then
6.
Push(S ′ , Expr[i])
7.
else
8.
if hExpr[i] = “ ) ”i then
9.
x := Pop(S)
10.
y := Pop(S)
11.
oper := Pop(S ′ )
12.
Push(S, Val(x, y, oper))
13. return (S[T op[S]])
Strutture dati elementari
• Convertire un’espressione aritmetica dalla forma Postfissa alla forma Infissa
1. Usiamo uno stack S per mantenere le “sotto-espressioni” parziali
2. Se incontriamo un operando lo “pushiamo” nello stack
3. Se incontriamo un operatore, “poppiamo” due volte lo stack, formiamo l’espressione corretta e la
“pushiamo” nello stack
PostfixToInfix(Expr)
1. for i := 0 to L − 1 do
2.
if hExpr[i] è un “operando”i then
3.
Push(S, Expr[i])
4.
else
5.
x := Pop(S)
6.
y := Pop(S)
7.
Push(S, “(y Expr[i] x)”)
8. return (S[T op[S]])
Strutture dati elementari
Coda
• Una coda (queue) è una lista in cui gli inserimenti avvengono ad una estremità chiamata tail e le cancellazioni all’estremità opposta, chiamata head. (Es. fila di persone ad uno sportello postale)
• Una coda implementa una politica di tipo FIFO (FIRST-IN-FIRST-OUT), ovvero, l’elemento ad
essere rimosso è sempre quello che è stato inserito per primo.
• Le operazioni di inserimento e di eliminazione in una coda vengono chiamate ENQUEUE e DEQUEUE, rispettivamente:
– Enqueue(Q, x) inserisce l’elemento x nel tail della coda Q
– Dequeue(Q) rimuove (e ritorna) l’elemento che si trova nell’head della coda Q
• Implementeremo una coda usando un “array circolare” Q dotato dei due attributi Head[Q] e T ail[Q]
che indicizzano l’head e il tail della coda, rispettivamente. In particolare ...
Strutture dati elementari
(1) Gli elementi della coda saranno quelli “compresi” tra Head[Q] e T ail[Q] − 1:
Q
0
Head[Q]
T ail[Q]
n−1
0
T ail[Q]
Head[Q]
n−1
Q
dove n è la lunghezza dell’array.
(2) La coda è vuota quando Head[Q] = T ail[Q]. Inizialmente Head[Q] = T ail[Q] = 0;
(3) La coda è piena quando Head[Q] = T ail[Q] + 1;
Strutture dati elementari
Operazioni sulle Code
• Emptyness
EmptyQueue(Q)
1. if Head[Q] = T ail[Q] then
2.
return (true)
3. else
4.
return (false)
• Inserimento / Eliminazione
Enqueue(Q, x)
1. Q[T ail[Q]] := x
2. if T ail[Q] = L[Q] − 1 then
3.
T ail[Q] := 0
4. else
5.
T ail[Q] := T ail[Q] + 1
Dequeue(Q)
1. if EmptyQueue(Q) then
2.
print (ERROR: Queue “underflow”)
3. else
4.
x := Q[Head[Q]]
5.
if Head[Q] = L[Q] − 1 then
6.
Head[Q] := 0
7.
else
8.
Head[Q] := Head[Q] + 1
9.
return (x)
Strutture dati elementari
Insiemi Dinamici
• Un insieme dinamico è un insieme il cui contenuto può cambiare nel corso del tempo a seguito della
esecuzione di certe operazioni sull’insieme stesso (Es. l’insieme S(L) degli elementi di una lista L quando
sulla lista vengono eseguite operazioni di inserimento e/o cancellazione.)
• Un insieme può essere agevolmente rappresentato come una lista, semplicemente ordinando gli elementi
dell’insieme in una qualche maniera prefissata. Pertanto risulta naturale implementare un insieme dinamico usando Array o Linked-List come abbiamo visto in precedenza per le liste. Tuttavia, tali tipi di
implementazioni possono rivelarsi poco efficienti quando la dimensione dell’insieme è molto grande e/o
quando gli elementi dell’insieme sono organizzati mediante certe relazioni (Es. l’insieme dei discendenti
di un’antica famiglia organizzati dalla relazione hP adre, F iglioi).
• Una stessa operazione su un insieme dinamico verrà implementata in maniera diversa a seconda che si usi
una certa struttura dati per rappresentare l’insieme piuttosto che un’altra. Molto spesso, diverse strutture
dati suggeriscono (e comportano) implementazioni molto differenti, dal punto di vista dell’efficienza, di
una stessa operazione sull’insieme.
• Come per le liste, gli elementi di un insieme dinamico sono caratterizzati da vari campi, uno dei quali si
distingue come la chiave. Se tutte le chiavi sono differenti, possiamo identificare ogni elemento dell’insieme
con la relativa chiave, e quindi riguardare l’insieme come un insieme di chiavi. Alcuni insiemi dinamici
presuppongono che le chiavi provengano tutte da uno stesso insieme (o universo) totalmente ordinato.
In questo caso è possibile definire, ad esempio, il minimo (o il massimo) di un insieme di chiavi, o di
confrontare le chiavi mediante una relazione di successore (e di predecessore).
Strutture dati elementari
Operazioni su Insiemi Dinamici
Come per le liste, anche per gli insiemi dinamici, si parlerà di Query e di Operazioni di Modifica.
• Query
– Visit(S): “esamina” tutti gli elementi dell’insieme dinamico S;
– Search(S, k): ritorna un elemento x di S contenente la chiave k;
– Minimum(S): ritorna un elemento di S avente chiave minima;
– Maximum(S): ritorna un elemento di S avente chiave massima;
– Successor(S, x): ritorna il successore di x in S;
– Predecessor(S, x): ritorna il predecessore di x in S;
• Operazioni di Modifica
– Insert(S, x): aggiunge l’elemento x all’insieme dinamico S;
– Delete(S, x): rimuove l’elemento x dall’insieme dinamico S;
Una struttura dati che supporta efficientemente le varie operazioni su un insieme dinamico, e che serve come
base per altri tipi di strutture dati, è la struttura dati ad Albero Binario di Ricerca (Binary-SearchTree.)
Strutture dati elementari
Alberi Binari
= Foglia
Radice
A
= Nodo Interno
Padre di D ed E
Figlio Sinistro di B
D
G
Figlio Destro di B
B
E
C
F
H
I
L
Sottoalbero Sinistro (di A)
Sottoalbero Destro (di A)
• La lunghezza di un cammino ρ è ℓ(ρ) = n − 1 dove n è il numero di nodi su ρ.
• L’altezza di un albero T è h(T ) = max{ℓ(ρ) : “ ρ è un cammino che parte dalla radice ”}.
L’albero rappresentato sopra ha altezza 4: il “cammino rosso” è un cammino di lunghezza massima pari a 4.
Strutture dati elementari
Alcune definizioni e proprietà
Sia T un albero binario.
• Un nodo x di T è una foglia se x non ha ne figlio sinistro e neppure figlio destro.
• Un nodo che non sia una foglia è chiamato nodo interno.
• Un cammino in T è una sequenza di nodi ρ = (x0, x1, . . . , xn ), con n ≥ 0, in cui xi+1 è un figlio di xi,
per i = 0, 1, . . . , n − 1. La lunghezza di ρ è ℓ(ρ) = n.
• La profondità p(x) di un nodo x è la lunghezza dell’unico cammino dalla radice fino a quel nodo. La
radice ha profondità 0.
• L’altezza di T è h(T ) = max{ℓ(ρ) : “ ρ è un cammino che parte dalla radice ”}.
• L’albero T è pieno se per ogni p < h(T ) ogni nodo di T di profondità p ha esattamente due figli.
Lemma 1. Se T è pieno, allora ci sono esattamente 2h nodi a profondità h, per ogni h ≤ h(T ).
Lemma 2. Se T è pieno, allora esso contiene esattamente 2h(T )+1 − 1 nodi.
Teorema 1. Se T contiene n nodi, allora
log2
n+1
≤ h(T ) ≤ n − 1 .
2
Strutture dati elementari
Rappresentazione di alberi binari
Un albero binario sarà rappresentato mediante un insieme T di nodi in cui ogni nodo x è formato dai seguenti
campi:
1. Key[x] che contiene la chiave memorizzata in x;
2. Lef t[x] che contiene un link che punta la figlio sinistro di x;
3. Right[x] che contiene un link che punta al figlio destro di x;
4. P arent[x] che contiene un link che punta al padre x.
Se un nodo x non ha figlio sinistro allora il campo Lef t[x] contiene il valore speciale N IL, e analogamente
per Right[x]. Se x è la radice di T allora il campo P arent[x] contiene N IL. (Si noti che la radice è l’unico
nodo il cui campo P arent contiene il N IL.)
Pertanto, un nodo x
• è la radice di T se e solo se P arent[x] = N IL;
• è una foglia se e solo se Lef t[x] = Right[x] = N IL;
• è un nodo interno se e solo se Lef t[x] 6= N IL o Right[x] 6= N IL.
La radice di T sarà indicata con Root[T ]. Inoltre, se x è un nodo di T indicheremo con Tx il sottoalbero di
T avente radice x.
Strutture dati elementari
Visita di alberi binari
Considereremo tre differenti procedure di visita di un albero binario:
• Visita Inorder (Inorder-Tree-Walk)
• Visita Postorder (Postorder-Tree-Walk)
• Visita Preorder (Preorder-Tree-Walk)
• Visita Inorder
La visita inorder di un albero binario T può essere definita ricorsivamente come segue:
1. Se T è l’albero vuoto (cioè se Root[T ] = N IL), non si esegue nessuna operazione e la visita termina;
2. Se T non è vuoto, detta r la radice di T , si eseguono le seguenti tre operazioni (nell’ordine indicato):
(Op.1) si esegue la visita inorder del sottoalbero sinistro di r
(Op.2) si stampa la chiave Key[r]
(Op.3) si esegue la visita inorder del sottoalbero destro di r
Strutture dati elementari
• Visita Postorder
La visita postorder di un albero binario T può essere definita ricorsivamente come segue:
1. Se T è l’albero vuoto (cioè se Root[T ] = N IL), non si esegue nessuna operazione e la visita termina;
2. Se T non è vuoto, detta r la radice di T , si eseguono le seguenti tre operazioni (nell’ordine indicato)
(Op.1) si esegue la visita postorder del sottoalbero sinistro di r
(Op.2) si esegue la visita postorder del sottoalbero destro di r
(Op.3) si stampa la chiave Key[r]
• Visita Preorder
La visita preorder di un albero binario T può essere definita ricorsivamente come segue:
1. Se T è l’albero vuoto (cioè se Root[T ] = N IL), non si esegue nessuna operazione e la visita termina;
2. Se T non è vuoto, detta r la radice di T , si eseguono le seguenti tre operazioni (nell’ordine indicato)
(Op.1) si stampa la chiave Key[r]
(Op.2) si esegue la visita preorder del sottoalbero sinistro di r
(Op.3) si esegue la visita preorder del sottoalbero destro di r
Strutture dati elementari
Esempio: visite di alberi binari
A
B
C
D
E
F
• INORDER
D
B
E
A
F
C
D
E
B
F
C
A
A
B
D
E
C
F
• POSTORDER
• PREORDER
Strutture dati elementari
Implementazione delle operazioni di visita
• La seguenti tre procedure ricorsive ricevono in input un nodo x di un albero binario T ed effettuano le
visite inorder, postorder e preorder del sottoalbero Tx :
Inorder-Tree-Walk(x)
1. if (x 6= N IL) then
2.
Inorder-Tree-Walk(Lef t[x])
3.
print (Key[x])
4.
Inorder-Tree-Walk(Right[x])
Postorder-Tree-Walk(x)
1. if (x 6= N IL) then
2.
Postorder-Tree-Walk(Lef t[x])
3.
Postorder-Tree-Walk(Right[x])
4.
print (Key[x])
Preorder-Tree-Walk(x)
1. if (x 6= N IL) then
2.
print (Key[x])
3.
Preorder-Tree-Walk(Lef t[x])
4.
Preorder-Tree-Walk(Right[x])
• Una chiamata a Inorder-Tree-Walk(Root[T ]) produrrà la visita inorder dell’intero albero binario T e
similmente per Postorder-Tree-Walk(Root[T ]) e Preorder-Tree-Walk(Root[T ]).
Strutture dati elementari
Alberi Binari di Ricerca (Binary-Search-Trees)
Un Albero Binario di Ricerca è un albero binario T che soddisfa la seguente proprietà:
Per ogni coppia di nodi x e y di T :
• se y è un nodo nel sottoalbero di radice Lef t[x], allora Key[y] ≤ Key[x];
• se y è un nodo nel sottoalbero di radice Rigth[x], allora Key[y] ≥ Key[x].
10
6
3
0
15
9
5
10
11
Si osservi che la visita inorder dell’albero binario di ricerca rappresentato sopra produce l’ordinamento in
senso non decrescente delle chiavi contenute nei nodi dell’albero: 0 3 5 6 9 10 10 11 15
Questà proprietà è valida in generale.
Strutture dati elementari
Operazioni su Alberi Binari di Ricerca
• Ricerca: dato un nodo x di un albero T e data una chiave k, la seguente procedura ritorna un nodo y
nel sottoalbero Tx tale che Key[y] = k. Se un tale nodo non esiste, allora la procedura ritorna N IL
Tree-Search(x, k)
1. y := x
2. while (y 6= N IL) and (k 6= Key[y]) do
3.
if (k < Key[y]) then
4.
y := Lef t[y]
5.
else
6.
y := Right[y]
7. return (y)
Tree-Search(x, k)
1. y := x
2. if (y = N IL) or (k = Key[y]) then
3.
return (y)
4. if (k < Key[y]) then
5.
return Tree-Search(Lef t[y], k)
6. else
7.
return Tree-Search(Right[y], k)
Ricorsione
• Nel caso peggiore bisogna esaminare tutti i nodi presenti su un cammino che parte da x e termina in una
foglia. Pertanto la complessità di Tree-Search(x, k) è O(h(Tx )), dove h(Tx ) è l’altezza del sottoalbero
Tx .
• Per effettuare la ricerca di k nell’intero albero T si effettua una chiamata a Tree-Search(Root[T ], k).
Strutture dati elementari
• Minimum / Maximum: Tree-Minimum(x) ritorna il primo nodo (in inorder) avente chiave minima
nel sottoalbero Tx . Tree-Maximum(x) ritorna l’ultimo nodo (in inorder) avente chiave massima nel
sottoalbero Tx .
Tree-Minimum(x)
1. y := x
2. while Lef t[y] 6= N IL do
3.
y := Lef t[y]
4. return (y)
Complessità = O(h(Tx ))
Tree-Maximum(x)
1. y := x
2. while Right[y] 6= N IL do
3.
y := Right[y]
4. return (y)
Complessità = O(h(Tx ))
• Successor / Predecessor: Tree-Successor(x) ritorna il successore del nodo x relativamente all’ordinamento
determinato dalla visita inorder (analogamente per Tree-Predecessor(x))
Tree-Successor(x)
1. if Right[x] 6= N IL then
2. return Tree-Minimum(Right[x])
3. y := P arent[x]
4. while y 6= N IL and x = Right[y] do
5.
x := y
6.
y := P arent[y]
7. return (y)
Complessità = O(h(Tx))
Tree-Predecessor(x)
1. if Lef t[x] 6= N IL then
2. return Tree-Maximum(Lef t[x])
3. y := P arent[x]
4. while y 6= N IL and x = Lef t[y] do
5.
x := y
6.
y := P arent[y]
7. return (y)
Complessità = O(h(Tx ))
Strutture dati elementari
• Inserimento: inserire la chiave k nell’albero T
Tree-Insert(T , k)
1. z := AllocateT reeN ode()
// crea un nuovo nodo dell’albero
2. Key[z] := k
3. Lef t[z] := Right[z] := N IL
4. x := Root[T ]
5. y := N IL
6. while (x 6= N IL) do
7.
y := x
8.
if k < Key[x] then
9.
x := Lef t[x]
10.
else
11.
x := Right[x]
12. P arent[z] := y
13. if y = N IL then
14.
Root[T ] := z
15. else
16.
if k < Key[y] then
17.
Lef t[y] := z
18.
else
19.
Right[y] := z
La complessità della procedura Tree-Insert(T , k) è O(h), dove h è l’altezza dell’albero T .
Strutture dati elementari
• Eliminazione: eliminare il nodo z dall’albero T
Tree-Delete(T , z)
1. if Lef t[z] = N IL or Right[z] = N IL then
2.
y := z
3. else
4.
y := Tree-Successor(z)
5. if Lef t[y] 6= N IL then
6.
x := Lef t[y]
7. else
8.
x := Right[y]
9. if x 6= N IL then
10.
P arent[x] := P arent[y]
11. if P arent[y] = N IL then
12.
Root[T ] := x
13. else
14.
if y = Lef t[P arent[y]] then
15.
Lef t[P arent[y]] := x
16.
else
17.
Right[P arent[y]] := x
18. if y 6= z then
19.
Key[z] := Key[y]
La complessità della procedura Tree-Delete(T , z) è O(h), dove h è l’altezza dell’albero T .
Strutture dati elementari
(A)
(B)
10
z
6
10
z
6
15
×
y
3
8
10
x
9
5
(C)
11
5
y
8
5
10
15
x
9
3
11
11
z
8
15
10
10
x
9
(D)
z
6
3
3
10
x
9
15
5
10
11
Scarica