La presentazione è in caricamento. Aspetta per favore

La presentazione è in caricamento. Aspetta per favore

Algoritmi Avanzati Liste e Puntatori

Presentazioni simili


Presentazione sul tema: "Algoritmi Avanzati Liste e Puntatori"— Transcript della presentazione:

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

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? Rappresentare una sequenza come una lista consente di:
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: BOOLEANBOOLEAN  BOOLEAN, 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) 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

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), EMPTYLIST) 5 ) ); } 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 20 20

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 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 void *malloc(size_t size); void free(void *p);
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 = ? 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 = ? 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 = ? 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 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 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. 42 42

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
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) Inserzione di un elemento in coda O(n) Rimozione di un elemento in testa Rimozione di un elemento in coda Concatenazione di liste Rimozione di un elemento dato qualunque 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

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 testata el.1 el.2 ••• el.n testata el.1 el.2 ••• el.n liberi el.1 el.2 ••• el.n

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) testata el.1 el.2 ••• el.n

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 - 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 <= (*l)->value) { // caso 2: inserisco elemento minimo, al primo posto struct node *t = *l; (*l)->next = t; } 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. testata lLink dummy rLink lLink el.1 rLink lLink el.2 rLink lLink el.n rLink

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) { 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 : 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);


Scaricare ppt "Algoritmi Avanzati Liste e Puntatori"

Presentazioni simili


Annunci Google