la lista.

Introduzione.

In questo capitolo affronteremo lo studio di una struttura dati lineare fondamentale:

La lista.

La lista è una particolare struttura dati molto flessibile, che può ingrandirsi e ridursi "a piacimento", nella quale si possono inserire, cancellare e ritrovare gli elementi in qualunque posizione. Le liste possono a loro volta essere concatenate, oppure suddivise in sottoliste. Sono utilizzate comunemente in molte applicazioni, specialmente in problemi di simulazione, nei compilatori e nelle realizzazione di sistemi operativi. Vediamo allora la definizione di lista dal punto di vista matematico.

La lista è una sequenza di zero o più elementi di un dato tipo. Generalmente la lista viene rappresentata come una successione di elementi separati da una virgola:

a1, a2, a3, ....an
dove n >= 0 e ciascun elemento ai è dello stesso tipo.

Il numero n >= 1 è la lunghezza della lista, an è l'ultimo elemento, a1 il primo. Se n = 0 diciamo che la lista è la lista vuota, cioè una lista che non ha elementi.

Una proprietà importante della lista è che i suoi elementi sono ordinati linearmente in base alla posizione che occupano. Si dice che l'elemento ai precede l'elemento a(i+1) per i = 1, 2, ... n-1 e che ai segue a(i-1) per i = 2, 3, ... n. Si può anche dire che l'elemento ai è alla posizione i. Possiamo anche postulare la presenza di una posizione seguente alla posizione dell'ultimo elemento della lista, e una operazione end(l) che restituisce la posizione successiva all'ultimo elemento della lista l.

Partendo dalla nozione matematica di lista dobbiamo introdurre un insieme di operazioni, per realizzare ADT lista.Per fare questo supponiamo che sia:

  1. Abbiamo sicuramente necessità di un'operazione di inserimento di un elemento nella lista:
    	Lista listaInsert( Lista l, Elemento x, Posizione p )
    
    Se l è la lista 
    
    
    	a1, a2, a3, ... an
    
    dopo l'operazione
    
    	a1, a2, a3, a(p-1), x, ap, ... an
    
    Se p == end(l) allora la lista diventa
    
    	a1, a2, ..., an, x
    
    Se l non ha la posizione p allora l'operazione risulta indefinita.
    
    
  2. Abbiamo bisogno di una operazione che restituisce l'elemento x alla posizione p della lista l.
    
    	Elemento listaRetrive( Posizione p, Lista l );
    	
    Se p == end(l) o p è una posizione non presente in l, il risultato sarà indefinito.
    
    
  3. Abbiamo bisogno di una funzione che cancella l'elemento alla posizione p di l.

    Lista listaDelete( Posizione p, Lista l );

    Se l è la lista

    a1, a2, a3, ...., an

    Dopo l'operazione diventa

    a1, a2, a3, ..., a(p-1), a(p+1), ..., an

    Il risultato è indefinito se p == end(l) o p non è definito in l.

  4. Abbiamo bisogno di un'operazione che data una posizione p di l restituisce la posizione successiva.

    Posizione listaNext( Posizione p, Lista l );

    Se p == end(l) o p non è una posizione valida il risultato non è definito.

  5. Abbiamo bisogno di un costruttore che crea la lista vuota.

    Lista listaCrea( void );

  6. Abbiamo bisogno di due operazioni, primo e ultimo, che restituiscono la prima posizione e l'ultima della lista.
  7. Infine, anche se non indispensabile, abbiamo una operazione che stampa la lista.

    void listaPrint( Lista l );

Ora con queste operazioni elementari possiamo operare con la lista.

Esempi

  1. cancellare tutti gli elementi che compaiono più di una volta.
    	Lista cancellaDuplicati( Lista l ) {
    		Posizione p, q;
    		
    		p = listaFirst(l);
    		while( p != listaEnd(l) ) {
    			q = listaNext(p, l);
    			while( q != listaEnd(l) )
    				if( uguale(listaRetrieve(p, l), listaRetrieve(q, l)) )
    					l = listaDelete(q, l);
    				else
    					q = listaNext(q, l);
    			p = listaNext(p, l);
    		}
    		return l;
    	}
    
    La funzione uguale dipende dal tipo Elemento.

  2. estrarre da una lista di nomi tutti quelli che iniziano per una determinata lettera.
    
    	Lista estrarre( Lista l, char x ) {
    		Lista lx = listaCrea();
    		Posizione p = listaFirst(l);
    		Elemento e;
    		
    		while( p != listaEnd(l) ) {
    			if(iniziaPerX(e=listaRetrieve(p, l), x)
    					lx = listaInsert( l, e, listaEnd(lx) );
    			p = listaNext(p, l);
    		}
    		return lx;
    	}
    
    IniziaPer dipende dal tipo Elemento.

  3. estrarre da una lista di numeri interi i pari ed i dispari.

    	Lista* estrarrePariDispari(Lista l) {
    		Lista * padi = (Lista*)malloc(2*sizeof(Lista));
    		padi[0] = listaCrea(); padi[1] = listaCrea();
    		Posizione p;
    		Elemento x;
    		
    		for(p = listaFirst(l); p != listaEnd(l); p = listaNext(p, l) )
    			if( pari(x=listaRetrieve(p, l)) )
    				padi[0] = listaInsert(x, l, listaEnd(padi[0]);
    			else
    				padi[1] = listaInsert(x, l, listaEnd(padi[1]);
    		return padi;
    	}
    
    pari dipende da Elemento

  4. calcolare la media aritmetica degli elementi di una lista.

    Supponiamo che ogni elemento sia un float.

    	float media( Lista l ) {
    		float somma = 0;
    		int n = 0;
    		Posizione p;
    		
    		for(p = listaFirst(l); p != listaEnd(l); p = listaNext(p, l)) {
    			somma += listaRetrieve(p, l);
    			n++;
    		}
    		return somma / n;
    	}
    

Nota bene. In tutti gli esempi abbiamo utilizzato l'operazione Retrieve. Il dato restituito cambia in funzione della lista che si considera: se operiamo con una lista di interi sarà un intero, con una lista di stringhe sarà una stringa, con una lista di float sarà un float. In questo modo diventa impossibile realizzare un'operazione generica che ci restituisce il dato contenuto nella lista.

Per superare il problema facciamo in modo che retrieve restituisca l'indirizzo del dato, non il dato. Diventa possibile costruire un'operazione generica, ma sarà cura del programmatore indicare attraverso un'operazione di cast il tipo di dato che si sta utilizzando. Nell'esercizio precedente si deve sostituire


		somma += listaRetrieve(p, l);

con 
		somma += *(float*)listaRetrieve(p, l);

Esercizi:

utilizzando ADT lista risolvere i seguenti problemi:

  1. Data una lista di numeri determinare la somma degli elementi.
  2. Date due liste di uguale lunghezza calcolare il prodotto scalare.
  3. Ordinare una lista.
  4. Determinare il massimo tra i numeri contenuti in una lista.
  5. Data una lista di stringhe stamparla in ordine inverso.
  6. Date due liste di interi ordinate in ordine crescente, fonderle in una sola lista ordinata.
  7. Data una lista ordinata di stringhe inserire nella lista una nuova stringa rispettandone l'ordinamento.
  8. Data una lista l ed una posizione p, restituire la sottolista che si ottiene a partire da p.

Realizzazione ADT lista.

ADT lista può essere realizzato in vari modi. Di seguito ne proporremo alcuni.

  1. ADT LISTA CON UTILIZZO DI VETTORI.

    Gli indirizzi degli elementi contenuti nella lista sono memorizzati in un vettore. La lista è un puntatore ad un record composto da due elementi: il primo è il vettore di indirizzi, il secondo contiene last.

    
    typedef struct {
    	Elemento elementi[MAXLEN];	/* vettore di puntatori agli elementi della lista  */
    	Posizione last;
    } _Lista;
    
    typedef _Lista* Lista;
    
    
    #define MAXLEN 200
    #define Posizione int
    #define Elemento void*
    
    Di seguito sono realizzate le operazioni definite in precedenza.
    
    static void fatalError(char* s) {
    	printf("\nFATAL ERRORR: %s *****\n", s);
    	exit(1);
    }
    
    Lista listaInsert( Lista l, Elemento e, Posizione p ) {
    	Posizione q;
    	
    	if(l->last >= MAXLEN) fatalError("listaInsert: Lista piena.");
    	if((p < 0) || (p > l->last)) fatalError("listaInsert: Posizione errata.");
    	for(q = l->last; q != p; q--)
    		l->elementi[q+1] = l->elementi[q];
    	l->elementi[p] = e;
    	l->last++;
    	return l;
    }
    
    Elemento listaRetrieve( Posizione p, Lista l) {
    	if((p < 0) || (p >= l->last)) fatalError("listaRetrieve: Posizione errata.");
    	return l->elementi[p];
    }
    
    Lista listaDelete( Posizione p, Lista l ) {
    	Posizione q;
    	
    	if((p < 0) || (p >= l->last)) fatalError("listaDelete: Posizione errata.");
    	(l->last)--;
    	for(q = p; q < l->last; q++)
    		l->elementi[q] = l->elementi[q+1];
    	return l;
    }
    
    Posizione listaNext( Posizione p, Lista l) {
    	if((p < 0) || (p >= l->last)) fatalError("listaRetrieve: Posizione errata.");
    	return p+1;
    }
    	
    Lista listaCrea( void ) {
    	Lista l = (Lista)malloc(sizeof(_Lista));
    	l->last = 0;
    	return l;
    }
    
    Posizione listaFirst( Lista l ) {
    	return 0;
    }
    
    Posizione listaEnd( Lista l ) {
    	return l->last;
    }
    
    
  2. ADT LISTA DINAMICA.

    Ogni elemento della lista è un record composto da due campi puntatore: il primo campo è l'indirizzo del dato, il secondo è l'indirizzo dell'elemento successivo.

    #define Elemento void*
    
    struct _nodo {
    	Elemento e;
    	struct _nodo* next;
    };
    
    typedef struct _nodo* Posizione;
    
    typedef struct _nodo* Lista;
    
    

    La lista è l'indirizzo del primo elemento.

    La lista vuota è la lista che ha come dato NULL. Quando si deve far riferimento ad una posizione si indica sempre la posizione dell'elemento precedete a quello su cui si devono compiere le operazioni. Il primo elemento, che non ha precedenti, lo si indica con il puntatore NULL. Ogni funzione controlla se si deve operare sul primo elemento o sui successivi.

    
    /*
    	lista.c
    */
    #include 
    #include 
    #include "lista.h"
    
    Lista listaInsert( Lista l, Elemento e, Posizione p ) {
    	Posizione x = (Posizione)malloc(sizeof(struct _nodo));	/* creo elemento  */
    	
    	x->e = e;		/* inserimento del dato  */
    	if( p == NULL ) {  /* inserimento in testa? */
    		x->next = l; return x; 		/* modifico il puntatore iniziale l  */
    	}
    	x->next = p->next;		/* altrimenti il successivo di p diventa il nuovo elemento  */
    	p->next = x;
    	return l;
    }
    	
    	
    Elemento listaRetrieve( Posizione p, Lista l ){
    	if( p == NULL )/*  in testa? */
    		return l->e; 
    	return p->next->e;
    }
    
    Lista listaDelete( Posizione p, Lista l ){
    	Posizione x;
    	if( p == NULL ) {  /* in testa? */
    		x = l;
    		l = l->next;
    	} else {
    		x = p->next;
    		p->next = p->next->next;
    	}
    	free( x );
    	return l;
    }
    
    Posizione listaNext( Posizione p, Lista l ) {
    	if( p == NULL ) /* in testa? */
    		return l;
    	return p->next;
    }
    
    Lista listaCrea( void ) {
    	return NULL;
    }
    	
    Posizione listaFirst( Lista l ){
    	return NULL;
    }
    
    Posizione listaEnd( Lista l ){
    	Posizione p;
    	for(p = l; p != NULL && p->next != NULL; p = p->next)
    		;
    	return p;
    }
    
    

  3. LA realizzazione precedente ha due inconvenienti.

    Questi due inconvenienti possono essere risolti introducendo un elemento fittizio in testa alla lista. Si elimina l'eccezione del primo elemento poichè anche il primo avrà un precedente, che potrà essere utilizzato per memorizzare l'ultimo elemento della lista, rendendo costante anche l'operazione listaEnd.

    Le definizioni non cambiano ma cambiano le operazioni. In particolare cambia la creazione, che crea un elemento fittizio e lo inizializza opportunamente.

    Lista listaInsert( Lista l, Elemento e, Posizione p ) {
    	Posizione x = (Posizione)malloc(sizeof(struct _nodo));
    	
    	x->e = e;
    	x->next = p->next;
    	p->next = x;
    	if( p == l->e ) l->e = x; /* se inserisco in fondo alla lista modifico l'indirizzo ultimo elemento */
    	return l;
    }
    	
    	
    Elemento listaRetrieve( Posizione p, Lista l ){
    	return p->next->e;
    }
    
    Lista listaDelete( Posizione p, Lista l ){
    	Posizione x;
    
    	x = p->next;
    	p->next = p->next->next;
    	if( x == l->e ) l->e = p; /* se cancello ultimo elemento modifico indirizzo ultimo  */
    	free( x );
    	return l;
    }
    
    Posizione listaNext( Posizione p, Lista l ) {
    	return p->next;
    }
    
    Lista listaCrea( void ) {
    	Posizione x = (Posizione)malloc(sizeof(struct _nodo));
    	
    	x->e = x;   /* l'indirizzo dell'utimo elemento  */
    	x->next = NULL;
    	return x;
    }
    	
    Posizione listaFirst( Lista l ){
    	return l;
    }
    
    Posizione listaEnd( Lista l ){
    	return l->e;
    }
    
    
    NOTA BENE. Gli esempi mostrati in precedenza che utilizzano ADT sono indipendenti dalle diverse implementazioni dell'ADT lista.

  4. Liste doppiamente concatenate.

    Potrebbe essere importante poter percorrere una lista lineare in tutte e due i sensi. Per ogni elemento potrebbe essere necessario determinare sia il precedente che il successivo. Con una lista così pensata diventa possibile utilizzare come puntatore alla cella i-esima l'indirizzo della cella stessa.

    L'implementazione che segue si introduce anche un elemento fittizio iniziale, elemento che precede il primo e che segue l'ultimo. Così si evita di dover controllare sempre se l'elemento che si inserisce o si cancella è il primo o l'ultimo. Si eliminano tutte le eccezioni.

    Inoltre ricordo la funzione End restituisce la posizione successiva all'ultimo elemento.

    La struttura dati da utilizzare nella realizzazione dell'ADT è la seguente:

    
    #define Elemento void*
    
    struct _nodo {
    	Elemento e;
    	struct _nodo *next, *pre;
    };
    
    typedef struct _nodo* Posizione;
    
    typedef struct _nodo* Lista;
    
    Le operazioni sono realizzate dalle seguenti primitive:
    
    Lista listaCrea( void ) {
    	Posizione x = (Posizione)malloc(sizeof(struct _nodo));
    	
    	x->e = NULL;
    	x->next = x->pre = x;
    	return x;
    }
    
    Lista listaInsert( Lista l, Elemento e, Posizione p ) {
    	Posizione x = (Posizione)malloc(sizeof(struct _nodo));
    	
    	x->e = e;
    	x->next = p;
    	x->pre = p->pre;
    	p->pre = x;
    	x->pre->next = x;
    	return l;
    }
    
    Elemento listaRetrieve( Posizione p, Lista l ){
    	return p->e;
    }
    
    Posizione listaNext( Posizione p, Lista l ) {
    	return p->next;
    }
    
    Posizione listaPre( Posizione p, Lista l ) {
    	return p->pre;
    }
    	
    Posizione listaFirst( Lista l ){
    	return l->next;
    }
    
    Posizione listaEnd( Lista l ){
    	return l;
    }
    

    Si lascia come utile esercizio la realizzazione dell'operazione di cancellazione. Si può verificare come i programmi che utilizzano ADT lista lineare operano correttamente anche con ADT lista doppiamente concatenata.

    Come utile esercizio realizzare una funzione che stampa la lista in ordine inverso, dall'ultimo elemento al primo. (Si ricorda che la funzione END restituisce l'indirizzo dell'elemento successivo all'ultimo per cui .... ).