Le liste




 




Ripartiamo dal prototipo di struttura utilizzato ieri:


struct ntemp{char titolo[100];
                   char *autore;
                   int collocazione;
                   struct ntemp *next ;};

typedef struct ntemp nlibro;




L'obiettivo è quello di preparare una lista di strutture utilizzando strutture aventi il prototipo indicato qui sopra.

Per preparare una lista devo:

(1) saper creare un nuovo elemento
(2) saperlo inizializzare
(3) saperlo agganciare alla lista (o crearne una nuova, nel caso essa sia vuota)


(1) e (2) agiscono sulla SINGOLA STRUTTURA e quindi tali operazioni possono essere effettuate come se stessimo agendo su una struttura qualsiasi mediante il puntatore alla struttura stessa.










Attenzione: gli elementi di una lista vengono gestiti mediante puntatori, quindi vengono sempre passati alle funzioni per riferimento !













(1) creazione di un nuovo elemento

nlibro *nuovo (void);

nlibro *nuovo (void)
{nlibro * temp;
  temp = (nlibro *) malloc (sizeof(nlibro));
  temp -> next = NULL;
  return temp;
 }




















(2) inizializzazione di un nuovo elemento

void assegna( nlibro *);

void assegna( nlibro * elemento)
{
      printf("Inserisci il titolo del libro (max 99 char)");
      gets (elemento -> titolo);

      elemento -> autore = (char *) malloc (51 * sizeof(char));
      printf("Inserisci il nome dell'autore (max 50 char) ");
      gets (elemento -> autore);
  
      printf("Inserisci la collocazione del volume");
      scanf("%d", & ( elemento -> collocazione ) );
}
























(3) l'operazione di "agganciare" un elemento suppone che ci sappiamo muovere all'interno di una lista.


Facciamo il paragone con i vettori; cerchiamo di raggiungere l'ultimo elemento di un vettore usando un indice per scorrere il vettore stesso. Il numero di elementi contenuti nel vettore è pari a 40.



int *vettore;
vettore = (int *) malloc (40 * sizeof(int));
int indice = 0;

while(1)
  {if(indice==39)
     {printf("questo è l'indirizzo dell'ultimo elemento %p", vettore + indice);
       break;}
    indice ++;
  }


L'operazione che svolgo è la seguente: sposto in avanti l'indice che mi permette di scorrere l'intero vettore; quando trovo la condizione che mi indica che sono arrivato alla fine mi fermo.


Eseguiamo quindi la stessa cosa con una lista.





Nel caso della lista bisogna ridefinire:

chi sia l'indice
come lo incremento
quale sia la condizione che mi indica quando sono arrivato in fondo alla lista




indice per scorrere la lista:
tutti gli elementi di una lista sono gestibili mediante il corrispondente indirizzo, quindi un buon indice è rappresentato da una variabile di tipo puntatore a struttura


metodo di incremento:
l'unico modo che abbiamo a disposizione per passare da un elemento all'altro è quello di utilizzare il valore del puntatore next dell'elemento cui fa riferimento l'indice


condizione di raggiungimento del fondo della lista:
                      trovo un elemento il cui membro next valga NULL







Supponiamo di avere una lista di 4 elementi: come individuo il quarto ?


nlibro *testa; // puntatore al primo elemento

testa->next;      // puntatore al secondo elemento

(testa->next) -> next;      // puntatore al terzo elemento

((testa -> next) -> next) -> next;      // puntatore al quarto elemento






















Un po' scomodo ... usiamo l'indice !

nlibro *testa; // puntatore al primo elemento
nlibro *indice;

indice = testa;    // dico all'indice di far riferimento al primo elemento

indice -> next;    // puntatore al secondo elemento

indice = indice -> next;     // ora indice diventa il puntatore al secondo elemento

indice = indice -> next;    // ora indice diventa il puntatore al terzo elemento

indice = indice -> next;    // ora indice diventa il puntatore al quarto elemento



Il metodo di incremento, quindi, consiste nell'assegnare all'indice l'indirizzo contenuto nel membro next della struttura.















Cerchiamo ora di individuare, usando questo metodo, chi sia il puntatore all'ultima struttura della lista.




nlibro *testa;
nlibro *indice;

... // istruzioni per la creazione della lista (v. dopo)

indice = testa;    // ora indice fa riferimento al primo elemento della lista

while( 1 )
  {if(indice-> next == NULL)     // ecco la condizione di raggiungimento del fondo della lista
     {
       printf(" questo è l'indirizzo dell'ultimo elemento della lista %p", indice );
       break;
      }
    indice = indice -> next;   // metodo di incremento
  }


















Sapere quale sia l'ultimo elemento di una lista è utile nel caso in cui intendiamo agganciare alla lista un eventuale nuovo elemento.

Immaginando che la variabile indice contenga l'indirizzo dell'ultimo elemento come ci aggancio un nuovo elemento ?





nlibro *aggiungi;
aggiungi = nuovo();

.. // istruzioni

indice -> next = aggiungi;

/* aggiungi -> next è stato inizializzato a NULL da nuovo(), quindi aggiungi è un buon ultimo elemento */


















A questo punto possiamo implementare la funzione che aggancia un nuovo elemento alla lista e che crea la lista nel caso in cui essa sia vuota.


Compiti che la funzione deve svolgere:

(1) se la lista è vuota, crearla (ovvero inserire un elemento)
(2) se la lista non è vuota agganciare un nuovo elemento in coda
(3) restituire il puntatore al primo elemento della lista



















/* testa rappresenta il puntatore al primo elemento della lista; se la lista è vuota testa vale NULL */


nlibro *aggancia(nlibro * testa,nlibro * nuovo_elemento)
{nlibro * indice;

    if(testa == NULL)     // lista vuota
     {return nuovo_elemento;}  // il nuovo elemento diventa il primo (e unico) della lista
    else
     {indice = testa;
       while( indice -> next != NULL )
         {indice = indice -> next;}

       // ora indice fa riferimento all'ultimo elemento
       indice -> next = nuovo_elemento;
       return testa; // puntatore al primo elemento della lista
     }
 }



















A questo punto abbiamo tutti i mezzi per poter costruire una lista ed inizializzarne gli elementi. Il numero di elementi presenti nella lista sarà ESATTAMENTE quello richiesto dall'utente per memorizzare i dati relativi ai libri.




// inizio codice

#include <stdlib.h>
#include <stdio.h>
#include "myfun.h"
   // in questo file posso dichiarare il prototipo di struttura


int main()
{// inizializzo SUBITO i puntatori !!
   nlibro * testa = NULL;
   nlibro * da_inserire = NULL;
   char temp[4];

  while(1)
   {printf("vuoi inserire i dati relativi ad un libro ? (s/n)");
     gets(temp);
     if(temp[0] == 'n') break;
     
     da_inserire = nuovo();
     assegna(da_inserire);
     testa=aggancia(testa,da_inserire);
    }

 return 0;}
// fine codice












Supponiamo ora di voler effettuare una ricerca all'interno della lista: vogliamo cercare se sono stati inseriti dei libri scritti da un autore in particolare.




Compito della funzione (da svolgere in ogni elemento della lista)

(1) controllare se nella stringa cui fa riferimento il membro autore di ciascuna struttura sono contenuti i caratteri inseriti dall'utente
(2) stampare un messaggio nel caso la ricerca abbia esito positivo
(3) passare alla struttura successiva all'interno della lista
(4) la funzione si deve fermare dopo aver controllato anche l'ultimo elemento


Quindi non userò come condizione di terminazione

indice -> next != NULL

altrimenti mi fermo prima di leggere dentro l'ultimo. Userò perciò

indice != NULL

ovvero mi fermo perché l'indice NON fa riferimento ad un elemento valido (questa condizione, tra l'altro, previene accessi illegali alla memoria nel caso in cui la lista sia vuota (v. dopo))




















#include <string.h>


void ricerca (nlibro * );

void ricerca ( nlibro * testa )
{nlibro * indice;
  char temp[100];

  printf("\n Modulo di ricerca per nome dell'autore \n");
  printf("Inserisci il nome dell'autore (max 100 car)");
  gets(temp);

  /* inizia la ricerca */
  indice = testa;



  while( indice != NULL )
    {
      /* effettuo il confronto */

     if( strcmp(temp, indice -> autore ) == 0 )
      {printf("il titolo del libro scritto da %s e' : %s", indice-> autore, indice -> titolo);
        printf("\n la collocazione del testo e' : %d", indice-> collocazione ); }
      
       indice = indice -> next ;   /* passo alla struttura successiva */

     }
 }













Supponiamo ora di voler visualizzare l'elenco delle collocazioni di tutti i libri

void stampa(nlibro * );

void stampa(nlibro * testa)
{
  while(testa != NULL)
    {printf("La collocazione del libro %s e' %d", testa-> titolo,testa-> collocazione);
      testa = testa -> next;}
}

























La funzione, fino a quando non è giunta in fondo alla lista, continua a svolgere sempre il medesimo compito in modo iterativo. Tale funzione è un buon prototipo di funzione da implementare in modalità ricorsiva.



void stampa(nlibro * );

void stampa(nlibro * testa)
{
  if(testa != NULL)
    {printf("La collocazione del libro %s e' %d", testa-> titolo,testa-> collocazione);
      stampa(testa -> next);}
}


















Domanda

Studiando le strutture abbiamo visto che, all'atto della definizione di una struttura, è possibile inserire come membro della struttura un puntatore ad una struttura dello stesso tipo di quella che stiamo definendo.
Domanda: pensando un po' al perché questo sia possibile vi viene in mente come mai si possa implementare una funzione in modo ricorsivo ?