Next Up Previous Hi Index

Chapter 19

Code

Questo capitolo presenta due tipi di dati astratti (TDA): la Coda e la Coda con priorit�. Nella vita reale un esempio di coda pu� essere la linea di clienti in attesa di un servizio di qualche tipo. Nella maggior parte dei casi il primo cliente della fila � quello che sar� servito per primo, anche se ci possono essere delle eccezioni. All'aeroporto ai clienti il cui volo sta per partire pu� essere concesso di passare davanti a tutti, indipendentemente dalla loro posizione nella fila. Al supermercato un cliente pu� scambiare per cortesia il suo posto con qualcuno che deve pagare solo pochi prodotti.

La regola che determina chi sar� il prossimo ad essere servito si chiama politica di accodamento. Quella pi� semplice � la FIFO ("first in, first out") dove il primo che arriva � il primo ad essere servito. La politica di accodamento pi� generale � l' accodamento con priorit� dove a ciascun cliente � assegnata una priorit� ed il cliente con la massima priorit� viene servito per primo indipendentemente dall'ordine di arrivo. Diciamo che questa politica di accodamento � la pi� generale perch� la priorit� pu� essere basata su qualsiasi fattore: l'orario di partenza dell'aereo, la quantit� di prodotti da pagare ad una cassa, l'importanza del cliente (!), la gravit� dello stato di un paziente al pronto soccorso. Logicamente non tutte le politiche di accodamento sono "giuste"...

I tipi di dati astratti Coda e Coda con priorit� condividono lo stesso insieme di operazioni. La differenza sta soltanto nella loro semantica: una Coda usa la politica FIFO, mentre la Coda con priorit�, come suggerisce il nome stesso, usa la politica di accodamento con priorit�.

19.1 Il TDA Coda

Il TDA Coda � definito dalle operazioni seguenti:

__init__
Inizializza una nuova coda vuota.
Inserimento
Aggiunge un nuovo elemento alla coda.
Rimozione
Rimuove e ritorna un elemento dalla coda. L'elemento ritornato � il primo inserito nella coda in ordine di tempo.
EVuota
Controlla se la coda � vuota.

19.2 Coda linkata

La prima implementazione del TDA Coda a cui guarderemo � chiamata coda linkata perch� � composta di oggetti Nodo linkati. Ecco una definizione della classe:

class Coda:
  def __init__(self):
    self.Lunghezza = 0
    self.Testa = None

  def EVuota(self):
    return (self.Lunghezza == 0)

  def Inserimento(self, Contenuto):
    NodoAggiunto = Nodo(Contenuto)
    NodoAggiunto.ProssimoNodo = None
    if self.Testa == None:
      # se la lista e' vuota il nodo e' il primo
      self.Testa = Nodo
    else:
      # trova l'ultimo nodo della lista
      Ultimo = self.Testa
      while Ultimo.ProssimoNodo: Ultimo = Ultimo.ProssimoNodo
      # aggiunge il nuovo nodo
      Ultimo.ProssimoNodo = NodoAggiunto
    self.Lunghezza = self.Lunghezza + 1

  def Rimozione(self):
    Contenuto = self.Testa.Contenuto
    self.Testa = self.Testa.ProssimoNodo
    self.Lunghezza = self.Lunghezza - 1
    return Contenuto

I metodi EVuota e Rimozione sono identici a quelli usati in ListaLinkata. Il metodo Inserimento � nuovo ed un po' pi� complicato.

Vogliamo inserire nuovi elementi alla fine della lista: se la coda � vuota facciamo in modo che Testa si riferisca al nuovo nodo.

Altrimenti attraversiamo la lista fino a raggiungere l'ultimo nodo e attacchiamo a questo il nuovo nodo. Possiamo identificare facilmente l'ultimo nodo della lista perch� � l'unico il cui attributo ProssimoNodo vale None.

Ci sono due invarianti per un oggetto Coda ben formato: il valore di Lunghezza dovrebbe essere il numero di nodi nella coda e l'ultimo nodo dovrebbe avere l'attributo ProssimoNodo uguale a None. Prova a studiare il metodo implementato verificando che entrambi gli invarianti siano sempre soddisfatti.

19.3 Performance

Normalmente quando invochiamo un metodo non ci interessa quali siano i dettagli della sua implementazione. Ma c'� uno di questi dettagli che invece dovrebbe interessarci: le performance del metodo. Quanto impiega ad essere eseguito? Come cambia il tempo di esecuzione man mano che la collezione aumenta di dimensioni?

Diamo un'occhiata a Rimozione. Non ci sono cicli o chiamate a funzione, e ci� suggerisce che il tempo di esecuzione sar� lo stesso ogni volta. Questo tipo di metodo � definito operazione a tempo costante. In realt� il metodo potrebbe essere leggermente pi� veloce quando la lista � vuota dato che tutto il corpo della condizione viene saltato, ma la differenza in questo caso non � molto significativa e pu� essere tranquillamente trascurata.

La performance di Inserimento � molto diversa. Nel caso generale dobbiamo attraversare completamente la lista per trovarne l'ultimo elemento.

Questo attraversamento impiega un tempo che � proporzionale alla grandezza della lista: dato che il tempo di esecuzione in funzione lineare rispetto alla lunghezza, diciamo che questo metodo � un'operazione a tempo lineare. Se confrontato ad un'operazione a tempo costante il suo comportamento � decisamente peggiore.

19.4 Lista linkata migliorata

Logicamente un'implementazione del TDA Coda che pu� eseguire tutte le operazioni in un tempo costante � preferibile, dato che in questo caso il tempo di esecuzione � indipendente dalla grandezza della lista elaborata. Un modo per fare questo � quello di modificare la classe Coda per fare in modo che venga tenuta traccia tanto del primo che dell'ultimo elemento della lista, come mostrato in questa figura:

L'implementazione di CodaMigliorata potrebbe essere:

class CodaMigliorata:
  def __init__(self):
    self.Lunghezza = 0
    self.Testa = None
    self.UltimoNodo = None

  def EVuota(self):
    return (self.Lunghezza == 0)

Finora l'unico cambiamento riguarda l'aggiunta dell'attributo UltimoNodo.
Questo attributo � usato dai metodi Inserimento e Rimozione:

class CodaMigliorata:
  ...
  def Inserimento(self, Contenuto):
    NodoAggiunto = Nodo(Contenuto)
    NodoAggiunto.ProssimoNodo = None
    if self.Lunghezza == 0:
      # se la lista e' vuota il nuovo nodo e'
      # sia la testa che la coda
      self.Testa = self.UltimoNodo = NodoAggiunto
    else:
      # trova l'ultimo nodo
      Ultimo = self.UltimoNodo
      # aggiunge il nuovo nodo
      Ultimo.ProssimoNodo = NodoAggiunto
      self.UltimoNodo = NodoAggiunto
    self.Lunghezza = self.Lunghezza + 1

Dato che UltimoNodo tiene traccia dell'ultimo nodo non dobbiamo pi� attraversare la lista per cercarlo. Come risultato abbiamo fatto diventare questo metodo un'operazione a tempo costante.

Comunque dobbiamo pagare un prezzo per questa modifica: quando dobbiamo rimuovere l'ultimo nodo con Rimozione dovremo assegnare None a UltimoNodo:

class CodaMigliorata:
  ...
  def Rimozione(self):
    Contenuto = self.Testa.Contenuto
    self.Testa = self.Testa.ProssimoNodo
    self.Lunghezza = self.Lunghezza - 1
    if self.Lunghezza == 0:
      self.UltimoNodo = None
    return Contenuto

Questa implementazione � pi� complessa di quella della coda linkata ed � pi� difficile dimostrare che � corretta, Il vantaggio che abbiamo comunque ottenuto � l'aver reso sia Inserimento che Rimozione operazioni a tempo costante.

Esercizio: scrivi un'implementazione del TDA Coda usando una lista di Python. Confronta le performance di questa implementazione con quelle di CodaMigliorata per una serie di lunghezze diverse della coda.

19.5 Coda con priorit�

Il TDA Coda con priorit� ha la stessa interfaccia del TDA Coda ma una semantica diversa. L'interfaccia � sempre:

__init__
Inizializza una nuova coda vuota.
Inserimento
Aggiungi un elemento alla coda.
Rimozione
Rimuovi un elemento dalla coda. L'elemento da rimuovere e ritornare � quello con la priorit� pi� alta.
EVuota
Controlla se la coda � vuota.

La differenza di semantica � che l'elemento da rimuovere non � necessariamente il primo inserito in coda, ma quello che ha la priorit� pi� alta. Cosa siano le priorit� e come siano implementate sono fatti non specificati dall'implementazione, dato che questo dipende dal genere di elementi che compongono la coda.

Per esempio se gli elementi nella coda sono delle stringhe potremmo estrarle in ordine alfabetico. Se sono punteggi del bowling dal pi� alto al pi� basso, e viceversa nel caso del golf. In ogni caso possiamo rimuovere l'elemento con la priorit� pi� alta da una coda soltanto se i suoi elementi sono confrontabili tra di loro.

Questa � un'implementazione di una coda con priorit� che usa una lista Python come attributo per contenere gli elementi della coda:

class CodaConPriorita:
  def __init__(self):
    self.Elementi = []

  def EVuota(self):
    return self.Elementi == []

  def Inserimento(self, Elemento):
    self.Elementi.append(Elemento)

I metodi __init__, EVuota e Inserimento sono tutte maschere delle operazioni su liste. L'unico metodo "interessante" � Rimozione:

class CodaConPriorita:
  ...
  def Rimozione(self):
    Indice = 0
    for i in range(1,len(self.Elementi)):
      if self.Elementi[i] > self.Elementi[Indice]:
        Indice = i
    Elemento = self.Elementi[Indice]
    self.Elementi[Indice:Indice+1] = []
    return Elemento

All'inizio di ogni iterazione Indice contiene l'indice dell'elemento con priorit� massima. Ad ogni ciclo viene confrontato questo elemento con l'i-esimo elemento della lista: se il nuovo elemento ha priorit� maggiore (nel nostro caso � maggiore), il valore di Indice diventa i.

Quando il ciclo for � stato completato Indice � l'indice dell'elemento con priorit� massima. Questo elemento � rimosso dalla lista e ritornato.

Testiamo l'implementazione:

>>> q = CodaConPriorita()
>>> q.Inserimento(11)
>>> q.Inserimento(12)
>>> q.Inserimento(14)
>>> q.Inserimento(13)
>>> while not q.EVuota(): print q.Rimozione()
14
13
12
11

Se la coda contiene solo numeri o stringhe questi vengono rimossi in ordine numerico o alfabetico, dal pi� alto al pi� basso. Python pu� sempre trovare il numero o la stringa pi� grande perch� pu� confrontare coppie di questi operandi con operatori di confronto predefiniti.

Se la coda contenesse un oggetto di tipo non predefinito � necessario fornire anche un metodo __cmp__ per poter effettuare il confronto. Quando Rimozione usa l'operatore > per confrontare gli elementi in realt� invoca __cmp__ per uno degli operandi e passa l'altro come parametro. La Coda con priorit� funziona come ci si aspetta solo se il metodo __cmp__ opera correttamente.

19.6 La classe Golf

Come esempio di oggetto con una definizione inusuale di priorit� implementiamo una classe chiamata Golf che tiene traccia dei nomi e dei punteggi di un gruppo di golfisti. Partiamo con la definizione di __init__ e __str__:

class Golf:
  def __init__(self, Nome, Punteggio):
    self.Nome = Nome
    self.Punteggio = Punteggio

  def __str__(self):
    return "%-16s: %d" % (self.Nome, self.Punteggio)

__str__ usa l'operatore di formato per stampare i nomi ed i punteggi in forma tabellare su colonne ordinate.

Poi definiamo una versione di __cmp__ dove il punteggio minore ottiene la priorit� pi� alta: come abbiamo gi� visto in precedenza __cmp__ ritorna 1 se self � pi� grande di Altro, -1 se self � minore di Altro, e 0 se i due valori sono uguali.

class Golf:
  ...
  def __cmp__(self, Altro):
    if self.Punteggio < Altro.Punteggio: return  1
    if self.Punteggio > Altro.Punteggio: return -1
    return 0

Ora siamo pronti a testare la coda con priorit� sulla classe Golf:

>>> tiger = Golf("Tiger Woods",    61)
>>> phil  = Golf("Phil Mickelson", 72)
>>> hal   = Golf("Hal Sutton",     69)
>>>
>>> pq = CodaConPriorit�()
>>> pq.Inserimento(tiger)
>>> pq.Inserimento(phil)
>>> pq.Inserimento(hal)
>>> while not pq.EVuota(): print pq.Rimozione()
Tiger Woods    : 61
Hal Sutton     : 69
Phil Mickelson : 72

Esercizio: scrivi un'implementazione di un TDA Coda con priorit� facendo uso di una lista linkata. Dovrai tenere la lista sempre ordinata per fare in modo che la rimozione di un elemento sia un'operazione a tempo costante. Confronta le performance di questa implementazione con l'implementazione delle liste in Python.

19.7 Glossario

Coda
insieme di oggetti in attesa di un servizio di qualche tipo; abbiamo implementato un TDA Coda che esegue le comuni operazioni su una coda.
Politica di accodamento
regole che determinano quale elemento di una coda debba essere rimosso per primo.
FIFO
"First In, First Out" (primo inserito, primo rimosso) politica di accodamento nella quale il primo elemento a essere rimosso � il primo ad essere stato inserito.
Coda con priorit�
politica di accodamento nella quale ogni elemento ha una priorit� determinata da fattori esterni. L'elemento con la priorit� pi� alta � il primo ad essere rimosso. Abbiamo implementato un TDA Coda con priorit� che definisce le comuni operazioni richieste da una coda con priorit�.
Coda linkata
implementazione di una coda realizzata usando una lista linkata.
Operazione a tempo costante
elaborazione il cui tempo di esecuzione non dipende (o dipende in minima parte) dalla dimensione della struttura di dati da elaborare.
Operazione a tempo lineare
elaborazione il cui tempo di esecuzione � proporzionale alla dimensione della struttura di dati da elaborare.


Next Up Previous Hi Index