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