400 likes | 536 Views
Un esempio: le liste ordinate di interi. Liste ordinate. OrderedIntList lista ordinata di interi modificabile. Specifica di OrderedIntList 1. public class OrderedIntList { // OVERVIEW: una OrderedIntList è una lista // modificabile di interi ordinata
E N D
Liste ordinate • OrderedIntList • lista ordinata di interi • modificabile
Specifica di OrderedIntList 1 public class OrderedIntList { // OVERVIEW: una OrderedIntList è una lista // modificabile di interi ordinata // tipico elemento: [x1, ..., xn], xi<xj se i<j //costruttore public OrderedIntList () // EFFECTS: inizializza this alla lista vuota // metodi public void addEl (int el) throws DuplicateException // MODIFIES: this // EFFECTS: aggiunge el a this, se el non occorre in // this, altrimenti solleva DuplicateException public void remEl (int el) throws NotFoundException // MODIFIES: this // EFFECTS: toglie el da this, se el occorre in // this, altrimenti solleva NotFoundException
Specifica di OrderedIntList 2 public boolean isIn (int el) // EFFECTS: se el appartiene a this ritorna // true, altrimenti false public boolean isEmpty () // EFFECTS: se this è vuoto ritorna true, altrimenti // false public int least () throws EmptyException // EFFECTS: se this è vuoto solleva EmptyException // altrimenti ritorna l’elemento minimo in this public boolean repOk () public String toString () // metodi test standard }
Specifica di OrderedIntList 3 public class OrderedIntList { public OrderedIntList () public void addEl (int el) throws DuplicateException public void remEl (int el) throws NotFoundException public boolean isIn (int el) public boolean isEmpty () public int least () throws EmptyException public boolean repOk () public String toString ()} • DuplicateExceptione NotFoundException checked
Come implementarla? • Potremmo usare una struttura dati lineare: lista concatenata • lista concatenata: definizione ricorsiva • o e’ vuota • o e’ formata da un nodo, che contiene un valore (di tipo int) ed un puntatore al resto della lista • bisognerebbe mantenere i nodi ordinati in base al valore associato in ordine crescente (tipo esercizio di LIP)
Usiamo invece un albero binario • albero binario: struttura ricorsiva tipo lista • o e’ vuoto • o contiene un nodo (detto radice) a cui e’ associato un valore (in questo caso di tipo int) ed un puntatore a due alberi, detti sottoalbero sinistro e destro rispettivamente • Nota: si chiama binario esattamente perche’ ogni nodo ha due figli (si puo’ generalizzare)
Esempio • I nodi che puntano a sottoalberi vuoti si dicono foglie • Il nodo radice contiene 4 4 10 5 6 12 8 6 6 12 1 2 6 6
Commenti • Rispetto ai tipi di dato lineari, lista doppia • si può percorrere da ogni elemento in avanti o all’indietro • Mediamente piu’ efficiente la ricerca se i valori sono inseriti in modo opportuno • Complessita’ degli algoritmi di inserimento, ricerca, eliminazione etc… (LSD, Algoritimica)
Ordinamento • Per mantenere l’ordinamento richiediamo che, per ogni nodo a cui e’ associato n • tutti i valori contenuti nel sottoalbero sinistro abbiano valore strettamente minore di n, • tutti i valori contenuti nel sottoalbero destro abbiano valore strettamente maggiore di n.
Vantaggio • Se leggiamo i valori effettuando la visita dell’albero in ordine simmetrico otteniamo I valori ordinati in modo crescente • visita simmetrica: in modo ricorsivo, visitiamo prima il sottoalbero sinistro, poi la radice, poi il sottoalbero destro • Un esempio
Come lo implementiamo? public class OrderedIntList { // OVERVIEW: una OrderedIntList è una lista // modificabile di interi ordinata // tipico elemento: [x1, ..., xn], xi<xj se i<j private boolean vuota; private OrderedIntList prima, dopo; private int val; • la rep contiene • una variabilebooleanche ci dice se la lista è vuota • la variabile intera che contiene l’eventuale valore dell’elemento • due (puntatori a)OrderedIntListsche contengono la lista di quelli minori e quelli maggiori, rispettivamente • implementazione ricorsiva
Come è fatta ? • La lista vuota ha la variabile boolean true • Le foglie puntano alla lista vuota • Notate l’ordinamento 12 F 17 F 5 F 4 F 8 F T 19 F T T T T T T
Commenti • L’invariante di rappresentazione e la funzione di astrazione descrivono le caratteristiche dell’implementazione che abbiamo in mente • Dipendono dalla rappresentazione e devono essere inserite come commenti alla rappresentazione • Sono fondamentali per ragionare sulla correttezza dell’implementazione o per capire l’implementazione (se per esempio qualcuno la dovesse modificare)
Invariante private boolean vuota; private OrderedIntList prima, dopo; private int val; I(c) = c.vuota oppure (c.prima != null e c.dopo != null e I(c.prima) e I(c.dopo) e (!c.prima.isEmpty() -> c.prima.max() < c.val) e (!c.dopo.isEmpty() -> c.dopo.least() > c.val) ) • l’invariante esprime le proprieta’ che devono soddisfare gli oggetti concreti, ovvero le loro variabili d’istanza • In questo caso il ruolo di vuota e la relazione tra i valori contenuti nella radice e nei sottoalberi (necessarie per garantire l’ordinamento richiesto)
Funzione di astrazione private boolean vuota; private OrderedIntList prima, dopo; private int val; a(c) = se c.vuota allora [], altrimenti a(c) = a(c.prima) + [c.val] + a(c.dopo) • Mappa gli oggetti concreti, implementati con un albero binario, nella corrispondente lista ordinata in modo crescente, tipo [x1, ..., xn], xi<xj se i<j • La funzione di astrazione ricorsiva realizza la lettura dei valori in modo simmetrico (dato che la lista deve essere ordinata)
Esempio • Oggetto concreto x quello della figura • Oggetto astratto corrispondente e’ la seguente lista • a(x)= [4,5,12,17,19]
Nota • Oggetto concreto, implementato tramite un albero binario, non e’e non deve essere visibile a chi usa OrderedIntList • Oggetto astratto (a(this)) e’ visibile tramite il metodo toString () che implementa la funzione di astrazione • Fondamentale se vogliamo conoscere lo stato dell’oggetto su cui stiamo lavorando, pur non avendo accesso diretto alla rappresentazione • Permette di fare delle verifiche sul comportamento dei metodi (creo un oggetto, inserisco dei valori, vedo se il risultato e’ quello atteso)
Domanda • Se abbiamo due implementazioni diverse (una fatta con l’albero binario e l’altra con una lista concatenata) • Diverse invarianti e f. di astrazione • Ma gli oggetti astratti devono essere dello stesso tipo, quello descritto nella OVERVIEW [x1, ..., xn], xi<xj se i<j • Da fuori vediamo solo gli oggetti astratti perche’ non vediamo differenze tra le due implementazioni (a parte chiaramente che possono differire in efficienza) • Principio di base del tipo di dato astratto: la specifica e’ l’interfaccia con l’esterno e maschera l’implementazione
Esercizio • Fate l’implementazione con lista concatenata (tipo LIP) • Formalizzate la funzione di astrazione e l’invariante • Verificate che l’invariante valga • Verificate la correttezza dei metodi • Dal punto di vista di chi usa il tipo di dato non deve vedersi differenza
Esempio a(c) =a(c.prima) + [5] + a(c.dopo) = ([]+ [4]+[]) + [5] + ([]+ [8]+[])= [4, 5, 8] • Funzione di astrazione per il sottoalbero con radice 5 • Se l’oggetto concreto non soddisfa l’invariante (che impone l’ordinamento) pero’ non e’ detto che troviamo I valori ordinati nel modo richiesto • Facciamo vedere che l’invariante impone dei vincoli sufficienti a garantire le proprieta’ dei dati richieste nella specifica
Invariante e Dati Astratti • Facciamo vedere per induzione sulla lunghezza della lista che Se I(c) allora a(c) =[x1, ..., xn], xi<xj se i<j • Caso Base: vale c.vuota. • Quindi a(c) = []. La lista vuota [] e’ ordinata.
Invariante • Caso Induttivo: non vale c.vuota, ma (c.prima != null e c.dopo != null e I(c.prima) e I(c.dopo) e (!c.prima.isEmpty() -> c.prima.max() < c.val) e (!c.dopo.isEmpty() -> c.dopo.least() > c.val) ) • Abbiamo a(c) = a(c.prima) + [c.val] + a(c.dopo)= [y1, ..., yn]+ [c.val] + [z1, ..., zk] • Dato che c.prima che c.dopo sono piu’ piccole di c, e che soddisfano l’invariante, allora per ipotesi induttiva [y1, ..., yn] e [z1, ..., zk] sono ordinate in modo crescente • Le altre proprieta’ dell’invariante garantiscono che sia anche tutta ordinata
Invariante • L’invariante cattura le proprieta’ richieste nella specifica dei dati, in particolare l’ordinamento degli interi crescente (l’abbiamo fatto vedere) • Rimangono da implemenatre i metodi in modo che preservino l’invariante • Discutiamo anche la loro correttezza rispetto alla specifica, assumendo che l’invariante valga, e utilizzando la funzione di astrazione per collegare la specifica che parla di oggetti astratti e l’implementazione che si riferisce agli oggetti concreti
Implementazione public class OrderedIntList { // OVERVIEW: una OrderedIntList è una lista // modificabile di interi ordinata // tipico elemento: [x1, ..., xn], xi<xj se i<j private boolean vuota; private OrderedIntList prima, dopo; private int val; //costruttore public OrderedIntList () // EFFECTS: inizializza this alla lista vuota { vuota = true; } • il costruttore inizializza solo la variabile vuota • l’oggetto prodotto soddisfa l’invariante (c.vuota = true) • verifica la propria specifica (a(c) = [])
Inserire e rimuovere • Difficili perche’ bisogna preservare l’ordinamento • Le proprieta’ dell’ordinamento sono espresse dall’invariante (ci aiuta a non fare errori in una situazione un po’ complessa) • Vediamo un esempio
Inserire un elemento public void addEl (int el) throws DuplicateException // MODIFIES: this // EFFECTS: aggiunge el a this, se el non occorre in // this, altrimenti solleva DuplicateException {if(vuota) { prima = new OrderedIntList(); dopo = new OrderedIntList(); val = el; vuota = false; return; } if (el == val) throw new DuplicateException(“OrderedIntList.addEl”); if (el < val) prima.addEl(el); else dopo.addEl(el); } • Ricorsiva, inserisce rispettando l’ordinamento • Propaga automaticamente l’eventuale eccezione sollevata dalle chiamate ricorsive
Esempio • vediamo la lista prodotta dalla sequenza di comandi OrderedIntList ls = new OrderedIntList(); ls.addEl(12); ls.addEl(5); ls.addEl(17); ls.addEl(4);ls.addEl(8); ls.addEl(19); 12 F 17 F 5 F 4 F 8 F T 19 F T T T T T T
Invariante: per casi I(c) = c.vuota oppure (c.prima != null e c.dopo != null e I(c.prima) e I(c.dopo) e (!c.prima.isEmpty() -> c.prima.max() < c.val) e (!c.dopo.isEmpty() -> c.dopo.least() >= c.val) ) public void addEl (int el) throws DuplicateException {if(vuota) { prima = new OrderedIntList(); dopo = new OrderedIntList(); val = el; vuota = false; return; } ... prima != null e dopo != null (calcolati dal costruttore) I(prima) e I(dopo) (calcolati dal costruttore) le implicazioni sono vere perché la premessa è falsa
Invariante: this non vuoto I(c) = c.vuota oppure (c.prima != null e c.dopo != null e I(c.prima) e I(c.dopo) e (!c.prima.isEmpty() -> c.prima.max() < c.val) e (!c.dopo.isEmpty() -> c.dopo.least() >= c.val) ) public void addEl (int el) throws DuplicateException ... if (el < val) prima.addEl(el); else dopo.addEl(el); } • prima != null e dopo != null (this non e’ vuoto) I(prima) e I(dopo) (calcolati da una chiamata ricorsiva su una lista piu’ piccola) • ramo then: il nuovo massimo di prima è (induttivamente) minore di val • ramo else: il nuovo minimo di dopo è (induttivamente) maggiore di val
Specifica a(c) = se c.vuota allora [], altrimenti a(c.prima) + [c.val] + a(c.dopo) public void addEl (int el) throws DuplicateException // MODIFIES: this // EFFECTS: aggiunge el a this, se el non occorre in // this, altrimenti solleva DuplicateException {if(vuota) { prima = new OrderedIntList(); dopo = new OrderedIntList(); val = el; vuota = false; return; } ... • a(cpre) = [] • a(c.prima) = [] e a(c.dopo) = [] e [c.val] = [el] • a(c) = [el] (ha inserito l’elemento)
Specifica public class OrderedIntList { private boolean vuota; private OrderedIntList prima, dopo; private int val; // a(c) = se c.vuota allora [], altrimenti // a(c.prima) + [c.val] + a(c.dopo) public void addEl (int el) throws DuplicateException // MODIFIES: this // EFFECTS: aggiunge el a this, se el non occorre in // this, altrimenti solleva DuplicateException ... if (el == val) throw new DuplicateException(“OrderedIntList.addEl”); • se ci sono elementi duplicati solleva l’eccezione, eventualmente propagando eccezioni sollevate dalle chiamate ricorsive (vedi dopo)
Specifica // a(c) = se c.vuota allora [], altrimenti // a(c.prima) + [c.val] + a(c.dopo) public void addEl (int el) throws DuplicateException // MODIFIES: this // EFFECTS: aggiunge el a this, se el non occorre in // this, altrimenti solleva DuplicateException ... if (el < val) prima.addEl(el); else dopo.addEl(el); } • a(cpre) = a(c.primapre) + [c.val] + a(c.dopopre) • se el < val la chiamata ricorsiva solleva l’eccezione oppure produce • a(c.prima) = aggiunge el a primapre • a(c.dopo) = a(c.dopopre) • a(c) = aggiunge el a cpre • Analogo nel caso simmetrico
Rimuovere un elemento • Difficile, bisogna mantenere l’ordinamento • Supponiamo di dovere rimuovere 12 (e’ la radice) • Non possiamo semplicemente spostare nella radice uno dei nodi figli (ex. 5)
Idea • si prende il valore minimo del dopo (se c’e’) e si mette al posto della radice (e si rimuove dal dopo) • e’ di sicuro piu’ grande di tutti i valori di prima e piu’ piccolo di quelli di dopo • se dopo e’ vuoto allora si ricopia nel nodo radice il sottoalbero prima • in questo modo viene preservato l’ordinamento imposto dall’ordinamento
Implementazione di OrderedIntList 5 public void remEl (int el) throws NotFoundException // MODIFIES: this // EFFECTS: toglie el da this, se el occorre in // this, altrimenti solleva NotFoundException {if(vuota) throw new NotFoundException(“OrderedIntList.remEl”); if (el == val) try { val = dopo.least(); dopo.remEl(val); } catch (EmptyException e) { vuota = prima.vuota; val = prima.val; dopo = prima.dopo; prima = prima.prima; return;} else if (el < val) prima.remEl(el); else dopo.remEl(el); }
Implementazione di OrderedIntList 6 public boolean isIn (int el) // EFFECTS: se el appartiene a this ritorna // true, altrimenti false {if(vuota) return false; if (el == val) return true; if (el < val) return prima.isIn(el); else return dopo.isIn(el); } public boolean isEmpty () // EFFECTS: se this è vuoto ritorna true, altrimenti false {return vuota; } Si ricerca in base all’ordinamento (efficiente, non si visita tutto)
Implementazione di OrderedIntList 6.1 public class OrderedIntList { private boolean vuota; private OrderedIntList prima, dopo; private int val; // a(c) = se c.vuota allora [], altrimenti // a(c.prima) + [c.val] + a(c.dopo) public boolean isIn (int el) // EFFECTS: se el appartiene a this ritorna // true, altrimenti false {if(vuota) return false; if (el == val) return true; if (el < val) return prima.isIn(el); else return dopo.isIn(el); } public boolean isEmpty () // EFFECTS: se this è vuoto ritorna true, altrimenti false {return vuota; } • dimostrazioni di correttezza ovvie, visto che l’invariante che garantisce l’ordinamento
Implementazione di OrderedIntList 7.1 public class OrderedIntList { private boolean vuota; private OrderedIntList prima, dopo; private int val; public int least () throws EmptyException // EFFECTS: se this è vuoto solleva EmptyException // altrimenti ritorna l’elemento minimo in this {if(vuota) throw new EmptyException(“OrderedIntList.least”); try { return prima.least(); } catch (EmptyException e) {return val;} } • dimostrazione di correttezza ovvia usando l’invariante, che garantisce l’ordinamento • Ritorna proprio il primo elemento di a(this)
Implementazione di OrderedIntList 7.1 public class OrderedIntList { private boolean vuota; private OrderedIntList prima, dopo; private int val; public String toString(){ // EFFECTS:standard {if(vuota) return “”; return prima.toString() + val + dopo.toString(); } } • dimostrazione di correttezza ovvia usando l’invariante, che garantisce l’ordinamento • Implementa a(this) visita in ordine simmetrico