La presentazione è in caricamento. Aspetta per favore

La presentazione è in caricamento. Aspetta per favore

Lez. 61 Universita' di Ferrara Facolta' di Scienze Matematiche, Fisiche e Naturali Laurea Specialistica in Informatica Algoritmi Avanzati Liste e Puntatori.

Presentazioni simili


Presentazione sul tema: "Lez. 61 Universita' di Ferrara Facolta' di Scienze Matematiche, Fisiche e Naturali Laurea Specialistica in Informatica Algoritmi Avanzati Liste e Puntatori."— Transcript della presentazione:

1 Lez. 61 Universita' di Ferrara Facolta' di Scienze Matematiche, Fisiche e Naturali Laurea Specialistica in Informatica Algoritmi Avanzati Liste e Puntatori Copyright © by Claudio Salati.

2 2 LISTE astrazione di dato che cattura l'idea di sequenza di lunghezza indefinita (e dinamica) di elementi non necessariamente omogenei liste di liste (oltre che di elementi atomici) liste di elementi atomici eterogenei, eventualmente tipizzati dinamicamente sia di lunghezza indefinita, ma limitata a priori (da 0 a n), che di lunghezza massima teoricamente illimitata possibilmente con modalita' di accesso ristretta, e.g. sequenziale (non diretto), o solo al primo e/o all'ultimo elemento definibile ricorsivamente lista (definizione ricorsiva/costruttiva) o e' una lista vuota o e' un elemento (atomico o lista) seguito da una lista

3 3 LISTE Lordine degli elementi nella lista puo essere o no significativo, quindi una lista puo rappresentare: Un insieme (non ordinato) Una sequenza (ordinata), come un vettore Sono le procedure daccesso che stabiliscono cosa la lista rappresenta Una proprieta' fondamentale delle liste e' che la struttura logica non e collegata a quella fisica (come e' invece per gli array) Questo rende essenziale in una lista la nozione di riferimento esplicito da un elemento allelemento successivo In una lista la relazione di contiguita' logica tra elementi e' esplicita N.B.: Un elemento puo appartenere contemporaneamente a diverse liste e.g. rappresentazione mediante liste di una matrice sparsa: ogni elemento appartiene a due liste, una che rappresenta la riga cui lelemento appartiene, laltra che rappresenta la colonna

4 4 LISTE: implementazione Rappresentazione "normale" (che utilizzeremo di solito qui) in memoria centrale utilizzando puntatori e memoria dinamica ma esistono anche altre rappresentazioni: array con elementi (omogenei) liberi o occupati e indici come puntatori nell'array sono in effetti contenute (almeno) due liste, quella degli elementi liberi e quella/e degli elementi occupati file (file system ad allocazione linkata, FAT del DOS,...) Diverse rappresentazioni possono essere piu o meno adatte a supportare liste diversamente vincolate nei contenuti (e.g. una rappresentazione basata su array e' adatta solo per liste omogenee di elementi semplici) diverse modalita (ristrette) di accesso alla lista

5 5 LISTE: perche? Supponiamo di volere mantenere una sequenza ordinata di numeri, e di volere inserire in questa sequenza un nuovo numero. Supponiamo di rappresentare la sequenza tramite un vettore ordinato. Allora per inserire il nuovo numero devo: 1.Cercare la posizione corretta dove effettuare linserimento (il primo elemento, se gia esiste, che sia maggiore di quello che voglio inserire oppure la posizione successiva a quella dellultimo elemento significativo); 2.Spostare ordinatamente di una posizione tutti gli elementi maggiori di quello che voglio inserire (in ogni caso devo estendere il vettore); 3.Copiare nel buco cosi creato il nuovo elemento. Complessita analoga ha la cancellazione di un elemento dalla sequenza. Rappresentare una sequenza come una lista consente di: Evitare il passo 2 della procedura, dato che la contiguita logica non e associata alla contiguita fisica; Evitare di dovere necessariamente allocare a priori spazio di memoria per il numero massimo di elementi ipotizzabile.

6 6 LISTE Possiamo considerare le liste da due punti di vista: 1.un tipo di dato astratto a se stante 2.una struttura dati (meglio, come vedremo nel seguito: una famiglia di strutture dati) utilizzabile per la rappresentazione concreta di altri tipi di dato astratto normalmente ci limiteremo a considerare liste di elementi atomici omogenei

7 7 Tipi di Dati a livello macchina si hanno solo stringhe di bit che assumono ciascuno valore 0 o 1 siamo noi (e le istruzioni macchina che mettiamo nel programma) che decidiamo quale significato dare a ciascuna stringa di bit nei linguaggi di programmazione di livello medio e alto (dal C o Pascal in su) tutti i dati, variabili e valori, appartengono ciascuno ad un tipo, sono cioe' descritti non in termini di configurazione di una stringa di bit, ma attraverso proprieta' astratte: insieme (possibilmente strutturato) dei valori possibili (e.g. range INT_MIN..INT_MAX per il tipo int del C) possibilita' di denotare questi valori (e.g. 138, 0x20, 077, INT_MAX per il tipo int del C) insieme delle operazioni primitive possibili su ciascun tipo di dato, e semantica di ciascuna operazione.

8 8 Tipi di Dati, esempio: tipo BOOLEAN insieme dei valori possibili: {true, false} denotazione di questi valori: TRUE, FALSE operazioni: { NOT, AND, OR, PRED, SUCC, =,,,, ORD } NOT: BOOLEAN BOOLEAN, totalmente definita NOT FALSE TRUE NOT TRUE FALSE AND: BOOLEAN BOOLEAN BOOLEAN, totalmente definita FALSE AND FALSE FALSE FALSE AND TRUE FALSE TRUE AND FALSE FALSE TRUE AND TRUE TRUE SUCC: BOOLEAN BOOLEAN, parzialmente definita SUCC(FALSE) TRUE SUCC(TRUE) non definito

9 9 ADT: astrazione vs. implementazione non ci interessa come e' rappresentato un valore BOOLEAN in termini di stringa di bit (sizeof, bit-pattern per ciascun valore), e ciascun linguaggio o ciascun sistema di programmazione lo puo' rappresentare come gli pare, basta che il suo comportamento (behaviour) sia conforme alla definizione astratta: tipo di dato astratto o ADT esempio: chi sa come sono rappresentati i double nel C Microsoft per Windows? ovviamente un sistema di programmazione deve definire una rappresentazione concreta (stringa di bit, struttura dati) per ciascun valore di ciascun tipo; e.g.: un byte con tutti i bit a 0 significa FALSE, un byte con almeno un bit a 1 significa TRUE oppure un byte con tutti i bit a 0 significa FALSE, un byte con il solo bit piu' leggero a 1 significa TRUE, e tutte le altre configurazioni di bit sono illegali

10 10 ADT: citazione Antonio Natali: "una delle metodologie piu' incisive per la costruzione di sistemi SW corretti, modulari, documentabili, consiste nel mantenere quanto piu' possibile separate le parti del programma che accedono e manipolano la rappresentazione concreta in memoria dei dati (detti gestori), dalle parti (clienti) che trattano i dati come enti concettuali, ignorandone i dettagli interni e realizzativi."

11 11 ADT: cliente e implementatore tipo di dato astratto 2 punti di vista diversi gestore dell'ADT: vede la rappresentazione implementa l'astrazione (e.g. le operazioni primitive) basandosi sulla rappresentazione del tipo cliente dell'ADT: non vede la rappresentazione utilizza l'astrazione (e.g. le operazioni primitive) senza sapere come e' implementata puo' definire (implementare) nuove operazioni, ma solo operazioni non primitive, implementate a partire dalle operazioni gia' disponibili (e.g. le operazioni primitive) e che (ovviamente) non si basano sulla conoscenza della rappresentazione concreta dei valori (che e' ignota)

12 12 LISTE: definizione dell'ADT operazioni primitive su liste null() list BOOLEAN data una lista L, restituisce TRUE se L e' vuota. La lista vuota e' indicata come EMPTYLIST o () o <> precondizioni: nessuna cons() element list list dato un elemento E e una lista L, restituisce la lista (E L) precondizioni: nessuna N.B.: cons() e' un operatore funzionale come gli altri tre; parte da dei valori, e produce un valore: gli operandi non sono modificati

13 13 LISTE: definizione dell'ADT operazioni primitive su liste car() (detta anche head() ) list element data una lista L = (E RestOfL) ne restituisce il primo elemento E precondizioni: !null(L) cdr() (detta anche tail() ) list list data una lista L = (E RestOfL) ne restitusce la sottolista RestOfL (eventualmente vuota) degli elementi successivi al primo precondizioni: !null(L)

14 14 LISTE: definizione dell'ADT assiomi vincoli che le operazioni devono soddisfare descrizione della semantica delle operazioni (insieme alle precondizioni) null(EMPTYLIST) !null(cons(e, l)) car(cons(e, l)) == e cdr(cons(e, l)) == l

15 15 LISTE: definizione dell'ADT note N.B.: (E L) (E (L)) L non e' essa stessa un elemento di (E L), ma gli elementi di L costituiscono gli elementi di (E L) successivi ad E. Notare che le operazioni sono definite in modo indipendente dalla rappresentazione delle liste. Indipendentemente dalla rappresentazione che vorro' utilizzare per realizzare concretamente una lista, posso usare l'astrazione.

16 16 ADT LISTA: esempi.1 int length (list l) { return (null(l) ? 0 : 1 + length(cdr(l))); } la dimostrazione di correttezza e' per induzione matematica sulla lunghezza di l (per esercizio)

17 17 ADT LISTA: esempi.2 list append (list l1, list l2) { // date due liste l1 e l2 ritorna una lista che e' la // concatenazione delle due. // N.B.: non una lista di due elementi, l1 ed l2, ma // una lista contenente tutti gli elementi di l1, // nell'ordine, seguiti da tutti gli elementi di l2, // nell'ordine // N.B.: l1 e l2 non vengono in alcun modo alterate return (null(l1) ? l2 : cons(car(l1), append(cdr(l1), l2) ) ); } la dimostrazione di correttezza e' per induzione matematica sulla lunghezza di l1 (per esercizio, vedi esempio seguente)

18 18 ADT LISTA: esempi.3 list reverse (list l) { 1 return (null(l) ? l 2 : append(reverse(cdr(l)), 3 cons(car(l), 4 EMPTYLIST) 5 ) 6 ); 7 } 8 la dimostrazione di correttezza e' per induzione matematica sulla lunghezza di l se l =() la funzione ritorna l (cioe' ()), che e' ovviamente la lista inversa di se stessa (riga 2) se l () la funzione ritorna (righe 3-6) una lista formata dall'inverso di cdr(l) (per ipotesi induttiva, applicabile perche' cdr(l) contiene un elemento meno di l, e cdr(l) esiste perche' !null(l) ) seguita da car(l), cioe' proprio la lista inversa di l

19 19 ADT LISTA: completezza L'insieme delle operazioni primitive sopra indicato, unitamente alloperatore triadico del C ( ?: ) e alla ricorsione (oltre che allistruzione return ) e' funzionalmente completo: tutte le altre operazioni sulle liste possono essere realizzate utilizzando solo questo insieme di operazioni. In realta si puo dimostrare (vedi Z. Manna, Teoria matematica della computazione) che questo sistema di programmazione e equivalente ad una Macchina di Turing, e che quindi ci consente di calcolare qualunque cosa sia calcolabile.

20 20 ADT LISTA: Esercizio Esercizio: scrivere, basandosi solo sulle funzioni primitive prima definite, una funzione C che concateni un elemento in coda alla lista anziche' in testa (come fa cons() ). Dimostrarne anche la correttezza. Il suo prototipo deve essere: list tailCons(element e, list l); Quale e' la complessita' di questa operazione? O(n)! Scrivere anche le due funzioni: list tailCdr(list l); element tailCar(list l); che operano anchesse sulla coda della lista e dal significato ovvio

21 21 Esempio di implementazione: lista di element 1 typedef... element; struct listElement { element el; struct listElement *next; }; typedef struct listElement *list; // tipicamente: #define EMPTYLIST NULL poiche' secondo la definizione ricorsiva l'ultimo elemento della lista e' seguito dalla lista vuota, il valore del campo next dell'ultimo elemento della lista sara' NULL

22 22 Esempio di implementazione: lista di element 2 boolean null(list l) { return (l==NULL); // meglio l==EMPTYLIST } list cons(element e, list l) { // costruisce un altro valore list senza // alterare il valore dell'operando l list t = malloc(sizeof(listElem)); t->el = e; t->next = l; // structure sharing: // gli elementi di l sono condivisi da l // e dal nuovo valore return t; }

23 23 Esempio di implementazione: lista di element 3 element car(list l) { // N.B. car non altera l assert(!null(l)); return l->el; } list cdr(list l) { // N.B. cdr non altera l assert(!null(l)); return l->next; }

24 24 LISTE: implementazione.1 Mentre e vero che le operazioni primitive definite ci consentono di realizzare qualunque operazione su una lista, per quanto complessa, non e detto che ci consentano di farlo in modo efficiente. e.g. Inserire un elemento in testa alla lista ha costo costante, inserirlo in coda alla lista ha costo O(n) essendo n la dimensione della lista. Quello che ci interessa da qui in poi non e tanto il punto di vista del cliente, ma quello dellimplementatore. Non ci interessa utilizzare l'ADT lista, ma piuttosto implementare diversi ADT utilizzando una struttura dati "a lista" per la loro rappresentazione concreta. ADT che ci possono interessare: polinomi, vettori di lunghezza variabile, vettori e matrici sparse, insiemi,...

25 25 LISTE: implementazione.2 Diversi ADT offrono in generale operazioni molto diverse. Saranno quindi molto diverse le modalita' con cui diversi ADT dovranno accedere alla struttura dati lista utilizzata per la loro rappresentazione concreta. Quello che ci interessa qui non e il punto di vista del cliente dell'ADT lista, ma quello dellimplementatore di diversi generi di lista: Come realizzare in modo efficiente una lista una volta che siano note le modalita operative con cui voglio poterla manipolare. E.g.: Stack (pila) LIFO (Last-In-First-Out) Coda FIFO (Fist-In-First-Out) Potremo anche rimuovere il vincolo di operare in modo funzionale, cioe' producendo valori a partire da valori, senza modificare i valori in ingresso (in effetti e quello che faremo normalmente).

26 26 LISTE: implementazione Inserzione di un nuovo elemento nella lista: Alloco un nodo inutilizzato Gli assegno il valore dellelemento da inserire Lo inserisco nella lista nel posto desiderato Per fare cio devo manipolare i riferimenti espliciti tra gli elementi della lista adiacenti a quello che voglio inserire Rimozione di un elemento: Estraggo lelemento dalla lista Per fare cio devo manipolare i riferimenti espliciti tra gli elementi della lista adiacenti a quello che voglio eliminare Disalloco il nodo non piu utilizzato

27 27 LISTE: implementazione Devo possedere una riserva di nodi non utilizzati Devo avere dei meccanismi per acquisire nodi dalla riserva e per rilasciare nodi alla riserva Devo avere procedure per: Inizializzare una lista Inserire un elemento nella lista (in quale posizione?) Togliere un elemento dalla lista (da quale posizione?) Scandire gli elementi della lista Ogni nodo deve contenere almeno un campo riferimento che consenta di descrivere la strutturazione logica della lista Devo avere la possibilita di indicare che il campo riferimento di un nodo non e significativo (non riferisce nulla, e.g. nel caso dellultimo elemento della lista) Devo avere una testata che mi consenta di accedere alla lista (e.g. al suo primo elemento)

28 28 Puntatori una variabile o un valore che riferisce una variabile esiste in C una funzione/operatore che si applica ad una variabile (meglio, ad una denotazione di variabile) e ne ritorna l'indirizzo (e' l'operatore unario & ) un puntatore puo' riferire variabili di qualunque classe di allocazione (statica, automatica, dinamica) regole di compatibilita' di tipo? vedi relazioni tra tipi in C e' definita la costante NULL di tipo void*, compatibile con tutti i tipi puntatore: un puntatore che ha valore NULL ha un valore ben definito, ma non riferisce alcuna variabile – #define NULL ((void *) 0) – definita nella libreria standard: sia in stdio.h che in stdlib.h il tipo void* del C e' definito come compatibile con tutti i tipi puntatore

29 29 Puntatori operazioni su puntatori in C assegnamento confronto uguaglianza disuguaglianza relazioni d'ordine (con certi limiti) aritmetica dei puntatori somma con un intero sottrazione di un intero sottrazione di puntatori indiciamento funzioni della libreria standard (stdlib) void *malloc(size_t size); void free(void *p); calloc() e realloc() dereferenziamento

30 30 Allocazione e Inizializzazione di Variabili in C C++ prevede che ogni volta che viene creata una variabile, essa venga anche inizializzata tramite l'invocazione dell'opportuno costruttore di tipo pertanto tutte le variabili in C++ sono comunque inizializzate; un comportamento analogo si ha per il C con le variabili statiche (che sono quindi sempre inizializzate), per le quali sono previsti un costruttore di assegnamento (o copia: copia la rappresentazione del valore indicato), invocabile esplicitamente nel programma un costruttore di default (che "azzera" la variabile) che viene utilizzato in assenza di utilizzo esplicito del costruttore di copia per le variabili automatiche il costruttore (solo assegnamento) deve essere invocato esplicitamente (e non esiste il costruttore default) per le variabili dinamiche non esiste la nozione di costruttore, per cui esse sono sempre non inizializzate

31 31 Allocazione e Inizializzazione di Variabili in C int *pi; pi=malloc(sizeof(int)); *pi=5; memoria statica memoria dinamica o heap codice pi = NULL pi = ? 5

32 32 Allocazione e Inizializzazione di Variabili in C int *pi; pi=malloc(sizeof(int)); *pi=5; memoria automatica memoria dinamica o heap codice pi = ? pi = ? 5

33 33 Allocazione e Inizializzazione di Variabili in C int *pi=NULL; pi=malloc(sizeof(int)); *pi=5; memoria automatica memoria dinamica o heap pi = NULL pi = ? 5 codice

34 34 Definizione di Puntatori in C non sono consentiti riferimenti (in avanti) a nomi typedef non ancora definiti sono consentiti (ma deprecati) riferimenti (in avanti) a nomi tag non ancora definiti e' infatti possibile pre-dichiarare parzialmente un nome tag, per poterlo riferire prima della definizione struct nomeStruct; (pre-) dichiara che nomeStruct e' il nome tag di una struct. da questo momento il nome tag nomeStruct e' riferibile. la struttura e' ancora opaca: non si sa come e' fatta! Se mi basta solo riferirla tramite puntatori non e' nemmeno necessario (ne' opportuno) completarne la definizione quando si definisce un tipo puntatore, il tipo puntato e' bene che sia gia' stato definito/dichiarato (salvo quanto detto prima). la definizione di tipi (strutture dati) ricorsivi, in particolare di record ( struct ) uno dei cui campi riferisce il record stesso, avviene utilizzando il nome tag del record

35 35 Definizione di Strutture Dati Ricorsive in C typedef struct structTag {... struct structTag *next; // a questo punto il nome tag // della struct e' gia' definito } typedefName; sfruttando riferimenti in avanti e' possibile anche la definizione di tipi mutuamente ricorsivi (vedi anche dichiarazioni parziali!) struct s {... struct t *tRef; // riferimento in avanti }; struct t {... struct s *sRef; };

36 36 Memoria dinamica e puntatori il sistema di programmazione deve supportare la mancanza di una disciplina di dis/allocazione predefinita evitando frammentazione ricompattando blocchi di memoria al momento della loro liberazione in modo efficiente struttura dati detta heap: algoritmi ben noti (vedi lezione 7). una variabile dinamica puo' essere creata in una procedura e sopravviverle l'indirizzo delle variabili dinamiche e' definito solo a tempo di esecuzione le variabili dinamiche sono visibili se c'e' un puntatore statico o automatico visibile che, direttamente o indirettamente, le riferisce

37 37 Memoria dinamica e puntatori problemi garbage variabili dinamiche allocate, non piu' visibili nel programma, e quindi non rilasciabili... p = malloc(); p = NULL;... dangling reference puntatore che riferisce una variabile dinamica che e' gia' stata disallocata... p = malloc(); q = p; free(p);... N.B.: dato che in C il passaggio dei parametri e' per valore, dopo la chiamata free(p), p e' necessariamente un dangling reference!

38 38 Stack LIFO La disciplina di inserzione/cancellazione degli elementi e LIFO (Last In - First Out): lultimo elemento inserito e il primo elemento estratto E sufficiente: Un riferimento tra gli elementi della lista Un marker per indicare che un riferimento e non significativo (e.g. per lultimo elemento della lista) Una testata per accedere alla lista (alla sua testa) Le operazioni di inserimento e cancellazione (entrambe in testa!) hanno costo costante (O(1)) el.nel.2el.1 testata

39 39 Stack LIFO: definizione e implementazione struct node { element value; struct node *next; }; typedef struct node *stack; void stackInit(stack *s) { // P = {} *s = NULL; } boolean stackEmpty(stack s) { // P = { s e' gia stato inizializzato } return(s == NULL); }

40 40 Stack LIFO: definizione e implementazione void stackPush(stack *s, element v) { // P = { s e' gia stato inizializzato } struct node *t = malloc(sizeof(struct node)); t->value = v; t->next = *s; *s = t; } element stackPop(stack *s) { // P = { s e' gia stato inizializzato; // s e' non vuoto } assert(*s!=NULL); struct node *t = *s; element v = t->value; *s = t->next; free(t); return v; }

41 41 Coda FIFO : doppio puntatore di testata La disciplina di inserzione/cancellazione degli elementi e FIFO (First In - First Out): il primo elemento inserito e il primo elemento estratto Inserzione ed estrazione avvengono agli estremi opposti della lista L' operazione di cancellazione in testa ha costo costante (O(1)) (anche quella di inserimento in testa, ma non ci interessa!) L'operazione di inserimento in fondo ha costo costante (O(1)) Con un solo puntatore di testata una delle due operazioni (linserimento in fondo) avrebbe un costo O(n) (dovrei scandire tutta la lista) Con due puntatori di testata, uno al primo e l'altro all'ultimo elemento della lista, si riconducono entrambe le operazioni a complessita' costante el.1el.2el.n Testata - first - last

42 42 Coda FIFO : doppio puntatore di testata Esaminando RadixSort abbiamo visto che ce unaltra operazione su liste che sarebbe utile poter eseguire in tempo costante: la concatenazione di 2 liste. L'operazione di concatenazione porta alla scomparsa di entrambe le liste iniziali e alla creazione di una nuova lista che e lunione delle due. Gli elementi delle liste iniziali sono riutilizzati per la costruzione della lista finale. L' operazione di concatenazione e ordinata: nella lista risultato gli elementi di una lista seguono, in modo ordinato, gli elementi dellaltra. Quanto costa concatenare tra loro due liste fondendole in una singola? Se la lista e uno stack LIFO O(n) Se la lista e una coda FIFO con doppio puntatore di testata O(1) Limplementazione delloperazione di concatenazione e lasciata come esercizio Unaltra operazione interessante e l'eliminazione di un elemento qualunque della lista (possedendone il riferimento): questa operazione e richiesta ad esempio dallalgoritmo dei boundary tag. Quanto costa? Se la lista e una coda FIFO con doppio puntatore di testata O(n) Devo scandire tutta la lista fino ad identificare lelemento precedente quello che voglio eliminare.

43 43 Coda FIFO: definizione e implementazione struct node { element value; struct node *next; }; struct queue { struct node *first; struct node *last; }; typedef struct queue queue; void queueInit(queue *q) { q->first = q->last = NULL; } boolean queueEmpty(queue *q) { return(q->first == NULL); }

44 44 Coda FIFO: definizione e implementazione void queueInsert(queue *q, element v) { if (q->first == NULL) { // caso lista vuota q->first = malloc(sizeof(struct node)); q->last = q->first; } else { q->last->next = malloc(sizeof(struct node)); q->last = q->last->next; } q->last->value = v; q->last->next = NULL; }

45 45 Coda FIFO: definizione e implementazione element queueRemove(queue *q) { assert(q->first!=NULL); struct node *t = q->first; element v = t->value; q->first = t->next; if (q->first == NULL) // caso lista vuota q->last = NULL; // end if free(t); return v; }

46 46 Complessita' delle operazioni Perche' tanta parte del codice scritto e' dedicato a trattare il caso particolare di lista vuota? Si puo' migliorare questa situazione? Complessita temporale delle operazioni Casi particolari operazione lista semplicemente linkata con testata con doppio puntatore lista semplicemente linkata con testata con singolo puntatore Inserzione di un elemento in testaO(1) Inserzione di un elemento in codaO(1)O(n) Rimozione di un elemento in testaO(1) Rimozione di un elemento in codaO(n) Concatenazione di listeO(1)O(n) Rimozione di un elemento dato qualunqueO(n)

47 47 Coda FIFO : lista circolare Immaginiamo che l'ultimo elemento della lista, anziche' avere un riferimento marcato come non significativo, riferisca il primo elemento della lista Si possono liberare tutti i nodi di una lista in tempo O(1) inserendoli in una lista dei liberi Ma: la rimozione del primo elemento implica l'aggiornamento del riferimento nell'ultimo elemento ( scandire l'intera lista): O(n) Analogamente le altre operazioni, e.g. l'inserzione di un elemento in fondo alla coda el.1el.2el.n testata el.1el.2el.n testata el.1el.2el.n liberi

48 48 Coda FIFO : lista circolare con testata che riferisce l'ultimo elemento della lista Immaginiamo una lista circolare in cui la testata riferisca l'ultimo elemento anziche' il primo: Si puo' inserire in tempo costante un elemento sia in testa che in coda alla lista Si puo' cancellare in tempo costante l'elemento in testa alla lista Si possono liberare tutti i nodi di una lista in tempo costante inserendoli in una lista dei liberi La cancellazione dell'elemento in coda alla lista ha costo O(n) L'eliminazione di un elemento qualunque della lista (possedendone il riferimento) ha tempo O(n) el.1el.2el.n testata

49 49 Coda FIFO : lista circolare con testata che riferisce l'ultimo elemento della lista Esercizio: scrivere in C le procedure per: L'inserimento di un elemento in testa alla lista L'inserimento di un elemento in coda alla lista La cancellazione di un elemento in testa alla lista La liberazione di tutti i nodi di una lista inserendoli in una lista dei liberi L'eliminazione di un elemento qualunque della lista (possedendone il riferimento, che e' un parametro di ingresso alla procedura)

50 50 Eliminazione dei casi particolari- 1 Consideriamo di avere una lista ordinata di interi rappresentata come in figura: dove el.1 el.2 … el.n e di volere scrivere una procedura per l'inserzione ordinata di un nuovo elemento: struct node { int value; struct node *next; }; typedef struct node *sortedIntList; el.1el.2el.n testata

51 51 Eliminazione dei casi particolari- 2 struct node *ordInsert(sortedIntList *l, int newEl) { if (*l == NULL) { // caso 1: lista vuota *l = malloc(sizeof(struct node)); (*l)->value = newEl; (*l)->next = NULL; return (*l); } else if (newEl value) { // caso 2: inserisco elemento minimo, al primo posto struct node *t = *l; *l = malloc(sizeof(struct node)); (*l)->value = newEl; (*l)->next = t; return (*l); } else { // caso 3 : inserisco elemento non minimo // in lista non vuota // continua alla prossima pagina

52 52 Eliminazione dei casi particolari- 3 struct node *s = *l; // individua il nodo precedente il nuovo // elemento while (s->next != NULL && s->next->value < newEl) { s = s->next; } struct node *t = malloc(sizeof(struct node)); t->value = newEl; t->next = s->next; s->next = t; return (t); }

53 53 Eliminazione dei casi particolari- 4 I casi 1 e 2 rappresentano casi particolari, a fronte del caso 3 che e' il caso generale. (N.B.: in realta' la distinzione tra i casi 1 e 2 e' stata costruita ad arte a scopo didattico) Si potrebbero riassorbire i casi particolari in quello generale, semplificando cosi' la procedura? Si', basta inserire un nodo dummy in testa alla lista! Il valore del nodo dummy e' ovviamente irrilevante. Il nodo dummy e' sempre presente nella lista, anche quando questa e' vuota. d ummy el.1el.n testata

54 54 Eliminazione dei casi particolari- 5 struct node { int value; struct node *next; }; typedef struct node *sortedIntList; void listInit(sortedIntList *l) { *l = malloc(sizeof(struct node)); (*l)->value = 0; // un valore dummy (*l)->next = NULL; } boolean listEmpty(sortedIntList l) { return(l->next == NULL); }

55 55 Eliminazione dei casi particolari- 6 struct node *ordInsert(sortedIntList l, int newEl) { // N.B.: l puo' essere passata per valore perche' // non c'e' mai necessita' di modificare la // testata struct node *s = l; // individua il nodo precedente il nuovo elemento while (s->next != NULL && s->next->value < newEl) { s = s->next; } struct node *t = malloc(sizeof(struct node)); t->value = newEl; t->next = s->next; s->next = t; return (l); }

56 56 Lista Doppiamente Linkata Le liste i cui elementi sono collegati da un unico riferimento (link) hanno diversi problemi. Dato un elemento (il riferimento ad un nodo della lista) la lista puo' essere scandita solo in una direzione (e vero? Vedi esercizi!) non si puo' conoscere l'elemento precedente quello dato (e.g. per rimuovere l'elemento dato) se non scandendo tutta la lista quindi l'eliminazione di un elemento qualunque della lista (pur possedendone il riferimento) ha complessita' lineare (O(n)) Questi problemi sono risolti se dotiamo ogni elemento della lista di due riferimenti, uno in avanti e l'altro all'indietro, cosi' che la lista possa essere scandita in entrambe le direzioni. In questo modo inserzione e rimozione di un elemento in un punto dato della lista (in particolare all'inizio e alla fine) hanno sempre complessita' costante (O(1))

57 57 Lista Doppiamente Linkata Struttura di una lista circolare, doppiamente linkata, con nodo dummy di testata. dummyrLinklLink el.1rLinklLink el.2rLinklLink el.nrLinklLink testata

58 58 Lista circolare doppiamente linkata con nodo dummy di testata Esercizio: scrivere in C le procedure per: L'inserimento di un elemento in testa e in coda alla lista La cancellazione di un elemento in testa e in coda alla lista L'eliminazione di un elemento qualunque della lista (possedendone il riferimento, che e' un parametro di ingresso alla procedura)

59 59 Liste e Insiemi Esercizio: Una lista puo' essere utilizzata per rappresentare un insieme. Operazioni tipiche su insiemi sono: intersezione unione inserimento di un elemento in un insieme rimozione di un elemento (di cui si indica il riferimento) da un insieme. Definire una rappresentazione tramite lista per insiemi di numeri interi e scrivere delle funzioni C che realizzino queste operazioni. Quale e' la complessita' di queste funzioni? Come cambiano le cose a seconda del fatto che le liste siano mantenute ordinate o meno?

60 60 Insiemi: altra rappresentazione concreta Esercizio: Immaginiamo che l'universo su cui costruiamo i nostri insiemi sia finito e in effetti abbastanza limitato, e.g. U = { k N | 0 k sizeof(unsigned long long)*8-1 } Definire una rappresentazione che non si basi su liste per insiemi di numeri appartenenti a U e scrivere due funzioni C che realizzino le operazioni di intersezione e unione. Definire anche le operazioni di inserimento e eliminazione di un elemento. Quale e' la complessita' di queste funzioni?

61 61 Ordinamento di liste per straight insertion 3 procedure basate su diverse semantiche/modalita' implementative 1.Opera per valore: non altera la lista in ingresso, ma costrusice una nuova lista ordinata contenente gli stessi elementi della lista originaria. Si basa sull'utilizzo dell'ADT lista definito in precedenza (funzioni car(), cons(), …). 2.Opera per valore come la funzione 1, ma opera direttamente sulla rappresentazione della lista (che e' diversa da quella utilizzata nella rappresentazione dell'ADT lista utilizzato per la funzione 1). 3.Non opera per valore, e riutilizza gli stessi elementi della lista di ingresso per costruire la lista ordinata. Opera direttamente sulla rappresentazione.

62 62 Ordinamento di liste per straight insertion: 1a list insertSort1(list l) { return( null(l) ? l : sSort(cons(car(l), EMPTYLIST), cdr(l)) ); } list sSort(list sed, list unSed) { return( null(unSed) ? sed : sSort(sIns(car(unSed), sed), cdr(unSed)) ); }

63 63 Ordinamento di liste per straight insertion: 1b list sIns(element e, list l) { return( null(l) ? cons(e, l) : e<=car(l) ? cons(e, l) : cons(car(l), sIns(e, cdr(l))) ); }

64 64 Ordinamento di liste per straight insertion: 2 struct node { int value; struct node *next; }; typedef struct node *list; // vedi pag : lista con nodo dummy list insertSort2(list l) { list scan = l->next; list newL = malloc(sizeof(struct node)); newL->value = 0; newL->next = NULL; while (scan!=NULL) { newL = ordInsert(scan->value, newL); // vedi pag. 53 scan = scan->next; } return(newL); }

65 65 Ordinamento di liste per straight insertion: 3a struct node { come in insertSort2 }; typedef struct node *list; list insertSort3(list l) { list scan = l; list newL = malloc(sizeof(struct node)); newL->value = 0; newL->next = NULL; while (scan->next!=NULL) { struct node*temp = scan->next; newL = ordInsertM(temp, newL); } free(l); return(newL); }

66 66 Ordinamento di liste per straight insertion: 3b list ordInsertM(struct node *el, list l) { struct node *scan = l; while (scan->next!=NULL && scan->next->value value) { scan = scan->next; } el->next = scan->next; scan->next = el; return(l); }


Scaricare ppt "Lez. 61 Universita' di Ferrara Facolta' di Scienze Matematiche, Fisiche e Naturali Laurea Specialistica in Informatica Algoritmi Avanzati Liste e Puntatori."

Presentazioni simili


Annunci Google