Upload
susana-righi
View
216
Download
0
Embed Size (px)
Citation preview
Lez. 6 1
Universita' di FerraraFacolta' di Scienze Matematiche, Fisiche e Naturali
Laurea Specialistica in Informatica
Algoritmi Avanzati
Liste e Puntatori
Copyright © 2006-2009 by Claudio Salati.
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
LISTE
L’ordine 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 d’accesso 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 all’elemento 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 l’elemento appartiene, l’altra che rappresenta la colonna
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
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 l’inserimento (il primo elemento, se gia’ esiste, che sia maggiore di quello che voglio inserire oppure la posizione successiva a quella dell’ultimo 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
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
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
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: BOOLEANBOOLEAN 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
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
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
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
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
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
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
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
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
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
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
ADT LISTA: completezza
L'insieme delle operazioni primitive sopra indicato, unitamente all’operatore triadico del C (?:) e alla ricorsione (oltre che all’istruzione 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
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 anch’esse sulla coda della lista e dal significato ovvio
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
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
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
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 dell’implementatore.
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
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 dell’implementatore 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
LISTE: implementazione
Inserzione di un nuovo elemento nella lista:
Alloco un nodo inutilizzato
Gli assegno il valore dell’elemento 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 l’elemento 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
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 dell’ultimo elemento della lista)
Devo avere una testata che mi consenta di accedere alla lista (e.g. al suo “primo” elemento)
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
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
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
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 = ?
pi = 5
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 = ?
pi = 5
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 = ?
pi = 5
codice
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 definizionestruct 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
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
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
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
Stack LIFO
La disciplina di inserzione/cancellazione degli elementi e’ LIFO (Last In - First Out): l’ultimo 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 l’ultimo 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.n el.2 el.1 •••testata
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
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
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 (l’inserimento 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.1 el.2 el.n •••
Testata
- first
- last
42
Coda FIFO : doppio puntatore di testata Esaminando RadixSort abbiamo visto che c’e’ un’altra 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’ l’unione 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 dell’altra.
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)
L’implementazione dell’operazione di concatenazione e’ lasciata come esercizio
Un’altra operazione interessante e’ l'eliminazione di un elemento qualunque della lista (possedendone il riferimento): questa operazione e’ richiesta ad esempio dall’algoritmo 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 l’elemento precedente quello
che voglio eliminare.
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
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
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
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 testa O(1) O(1)
Inserzione di un elemento in coda O(1) O(n)
Rimozione di un elemento in testa O(1) O(1)
Rimozione di un elemento in coda O(n) O(n)
Concatenazione di liste O(1) O(n)
Rimozione di un elemento dato qualunque O(n) O(n)
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.1 el.2 el.n•••testata
el.1 el.2 el.n•••testata
el.1 el.2 el.n •••liberi
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.1 el.2 el.n•••testata
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
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.1 el.2 el.n •••testata
51
Eliminazione dei casi particolari - 2struct 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 <= (*l)->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
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
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.
dummy el.1 el.n •••testata
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
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
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
Lista Doppiamente Linkata Struttura di una lista
circolare, doppiamente linkata, con nodo dummy di testata.
dummy rLinklLink
el.1 rLinklLink
el.2 rLinklLink
el.n rLinklLink
testata
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
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
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
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
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
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
Ordinamento di liste per straight insertion: 2
struct node { int value; struct node *next; };
typedef struct node *list;
// vedi pag. 51-53: 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
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
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 < el->value) { scan = scan->next; } el->next = scan->next; scan->next = el; return(l);}