Docsity
Docsity

Prepara i tuoi esami
Prepara i tuoi esami

Studia grazie alle numerose risorse presenti su Docsity


Ottieni i punti per scaricare
Ottieni i punti per scaricare

Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium


Guide e consigli
Guide e consigli


appunti sulle liste, Appunti di Fondamenti di informatica

Appunti sulle liste spiegazione

Tipologia: Appunti

2019/2020

Caricato il 11/09/2021

boom-baby
boom-baby 🇮🇹

4.5

(91)

230 documenti

1 / 11

Toggle sidebar

Questa pagina non è visibile nell’anteprima

Non perderti parti importanti!

bg1
LISTE
Una Lista, a differenza di un array, ha un numero variabile di elementi
ed è tale da allocare, in ogni istante, un quantitativo di memoria pari a
quella minima necessaria
struct Elemento {
int valore;
struct Elemento* prossimo;
};
typedef struct Elemento* Lista;
La definizione di lista coincide con quella di un indirizzo di un elemento
(il primo elemento della lista stessa, collegato al secondo, che è
collegato al terzo e così via)
prossimo è un puntatore all’elemento successivo della lista
L’ultimo elemento della lista punterà ad un elemento inesistente,
rappresentato dalla costante NULL (solitamente pari a 0, valore
sicuramente non utilizzabile per un indirizzo)
Nei nostri tipi strutturati noi abbiamo finora preso in considerazione il caso dei tipi scalari che quindi hanno
un tipo , un valore e un attributo, poi abbiamo considerato i dati strutturati che sono dati che hanno più
valori: il primo caso studiato sono gli array che hanno un solo tipo, un solo attributo e tanti valori(in
quantità prefissata)abbiamo detto che quando abbiamo un’array di dimensione N noi possiamo gestire
sia una sequenza di N dati, sia sequenze più piccole ma non più grandi.
Poi abbiamo detto che se i dati non sono omogenei ma sono eterogenei (di tipi diversi) facciamo uso delle
struct, però la quantità di dati in una struct è prefissata poiché definita proprio al momento della
dichiarazione . Il caso che ci manca è quello di dati omogenei o eterogenei in quantità variabile(ovvero
modificabile), inoltre dobbiamo fare in modo di occupare istante per istante il quantitativo giusto di
memoria questa soluzione è data dalle liste
Una lista è una sequenza di dati. La lunghezza della lista non è nota all'inizio e può variare liberamente
durante l'esecuzione del programma.
Le liste ci forniscono una soluzione per la riduzione di spreco di memoria nei casi in cui noi non sappiamo
quanto spazio ci servirà in memoria . La lista , siccome non sappiamo quanto essa sarà lunga all’inizio e al
contempo non vogliamo sprecare spazio in memoria, allora può essere inizializzata a NULL che sta ad
indicare proprio che la lista è vuota(viene solo memorizzata solo dove inizia anche se non inizia davvero
infatti per questo ci mettiamo NULL). Poi ammettiamo di memorizzare un numero nella listaandiamo alla
casella (142) (indirizzo) e memorizziamo il numero 1ora succede che per sapere che la nostra lista
comincia alla casella (142) dobbiamo ricordarci dove comincia. Quindi ci prenderemo una variabile, che
memorizziamo nella casella in memoria (1000), che si ricorda dove comincia la nostra lista(nel nostro caso
nella casella 1000 metteremo (memorizzeremo) 142 che ci dice che la nostra lista ha un elemento nella
casella 142).
ADESSO COME FACCIAMO A DIRE CHE LA NOSTRA LISTA POSSIEDE UN SOLO ELEMENTO?
Purtroppo alla casella 1000 non ritroviamo questa informazione poiché la casella 1000 ci dice solo
l’indirizzo di dove sta il primo elemento della lista. Se non ci sono altri elementi allora dobbiamo mettere da
qualche parte l’indicazione che non ci sono altri elementiquesta indicazione viene messa nella casella
(144) Perché non nella 1002? Perchè il nostra lista si trova nell’heap e non nello stackinoltre se
l’avessimo messa nello stack significava che quando noi andavamo a dichiarare un puntatore che sta in
pf3
pf4
pf5
pf8
pf9
pfa

Anteprima parziale del testo

Scarica appunti sulle liste e più Appunti in PDF di Fondamenti di informatica solo su Docsity!

LISTE

  • Una Lista, a differenza di un array, ha un numero variabile di elementi

ed è tale da allocare, in ogni istante, un quantitativo di memoria pari a

quella minima necessaria

struct Elemento {

int valore;

struct Elemento* prossimo;

typedef struct Elemento* Lista;

  • La definizione di lista coincide con quella di un indirizzo di un elemento

(il primo elemento della lista stessa, collegato al secondo, che è

collegato al terzo e così via)

  • prossimo è un puntatore all’elemento successivo della lista
  • L’ultimo elemento della lista punterà ad un elemento inesistente,

rappresentato dalla costante NULL (solitamente pari a 0, valore

sicuramente non utilizzabile per un indirizzo)

Nei nostri tipi strutturati noi abbiamo finora preso in considerazione il caso dei tipi scalari che quindi hanno un tipo , un valore e un attributo, poi abbiamo considerato i dati strutturati che sono dati che hanno più valori: il primo caso studiato sono gli array che hanno un solo tipo, un solo attributo e tanti valori(in quantità prefissata)abbiamo detto che quando abbiamo un’array di dimensione N noi possiamo gestire sia una sequenza di N dati, sia sequenze più piccole ma non più grandi. Poi abbiamo detto che se i dati non sono omogenei ma sono eterogenei (di tipi diversi) facciamo uso delle struct, però la quantità di dati in una struct è prefissata poiché definita proprio al momento della dichiarazione. Il caso che ci manca è quello di dati omogenei o eterogenei in quantità variabile(ovvero modificabile), inoltre dobbiamo fare in modo di occupare istante per istante il quantitativo giusto di memoria questa soluzione è data dalle liste Una lista è una sequenza di dati. La lunghezza della lista non è nota all'inizio e può variare liberamente durante l'esecuzione del programma. Le liste ci forniscono una soluzione per la riduzione di spreco di memoria nei casi in cui noi non sappiamo quanto spazio ci servirà in memoria. La lista , siccome non sappiamo quanto essa sarà lunga all’inizio e al contempo non vogliamo sprecare spazio in memoria, allora può essere inizializzata a NULL che sta ad indicare proprio che la lista è vuota(viene solo memorizzata solo dove inizia anche se non inizia davvero infatti per questo ci mettiamo NULL). Poi ammettiamo di memorizzare un numero nella listaandiamo alla casella (142) (indirizzo) e memorizziamo il numero 1ora succede che per sapere che la nostra lista comincia alla casella (142) dobbiamo ricordarci dove comincia. Quindi ci prenderemo una variabile, che memorizziamo nella casella in memoria (1000), che si ricorda dove comincia la nostra lista(nel nostro caso nella casella 1000 metteremo (memorizzeremo) 142 che ci dice che la nostra lista ha un elemento nella casella 142). ADESSO COME FACCIAMO A DIRE CHE LA NOSTRA LISTA POSSIEDE UN SOLO ELEMENTO? Purtroppo alla casella 1000 non ritroviamo questa informazione poiché la casella 1000 ci dice solo l’indirizzo di dove sta il primo elemento della lista. Se non ci sono altri elementi allora dobbiamo mettere da qualche parte l’indicazione che non ci sono altri elementiquesta indicazione viene messa nella casella (144)  Perché non nella 1002? Perchè il nostra lista si trova nell’heap e non nello stackinoltre se l’avessimo messa nello stack significava che quando noi andavamo a dichiarare un puntatore che sta in

1000 dovevamo dire che non volevamo un puntatore ma due puntatori perché potrebbe avere un elemento, solo se la nostra lista aveva 100 elementi noi avremmo dovuto dichiarare 100 puntatori quindi significa che avremmo dovuto dichiarare tanti puntatori quanti sono gli elementi della lista quindi dovevamo sapere in anticipo quanti puntatori avevamo bisogno(e di conseguenza quanto è lunga una listaarray)nel modo adottato invece stiamo andando dinamicamente a metterci gli elementi Lista vuota struct Elemento { int valore; struct Elemento prossimo;* } Per definire una lista noi abbiamo bisogno di un puntatore che rappresenta sostanzialmente un indirizzo del primo elemento della listase la nostra lista è vuota per indicare questo aspetto il modo più semplice è scrivere: struct Elemento* lista = NULL (in questo modo il compilatore crea una variabile nello stack che si chiama lista che è l’indirizzo di una struct Elemento e che all’inizio vale NULL) (1000) NULL La variabile nella casella 1000 ci dice che la lista inizia a NULL, cioè non inizia proprio (è vuota) Lista con un elemento struct Elemento* lista = NULL; --> viene allocata la variabile lista nello stack nella casella 1000 lista = new struct Elemento; --> crea una nuova struct Elemento nell'heap a partire dalla casella 142 e scrive 142 nello stack nella casella 1000 (lista).valore = 1; --> va nella struct Elemento della casella 142, trova il campo valore e ci scrive 1 (lista).prossimo=NULL; --> va nella struct Elemento della casella 142, trova il campo prossimo e ci scrive NULL Per semplificare potevamo anche scrivere lista->valore=1; lista->prossimo=NULL; (142) 1 (144) NULL

è vietata? Non è vietato esprimere una struttura in funzione di se stessa? Questa cosa infatti è vietata ma in tal caso non stiamo definendo una struttura in funzione di se stessa ma di una struttura in funzione di un intero e di un puntatore ad struttura Ciò non è vietato perché al compilatore interessa quanto spazio occupa la struttura in memoria e come deve rappresentare i suoi valori struct Elemento* occupa in memoria lo spazio necessario per un indirizzo (una macchina a 64 bit sarà 8byte), indipendentemente dal fatto che sia un puntatore proprio a struct Elemento ed è rappresentato come tutti gli indirizzi come un intero senza segno Con questa struttura noi possiamo definire tutta lista perché manca l’indirizzo del primo elemento che si trova nello stack struct Elemento* lista = NULL; lista = new struct Elemento; lista->valore=1; lista->prossimo = new struct Elemento; lista->prossimo->valore=13; lista->prossimo->prossimo = new struct Elemento; lista->prossimo->prossimo->valore = 71; lista->prossimo->prossimo->prossimo=NULL; LE LISTE CONCATENATE POSSONO ESSERE LETTE SOLTANTO IN MANIERA SEQUENZIALE, IN ORDINE GLI ARRAY, INVECE, POSSONO ESSERE ACCEDUTI IN MANIERA CASUALE Esempio: Abbiamo un elenco telefonico che potenzialmente può contenere 10000 numeri Spazio occupato con array : 10000 * sizeof(int) + sizeof(indirizzo) = (ad esempio) 100004+8= 40008 byte Spazio occupato con lista : Esempio: ho solo 3 contatti : 3sizeof(int) + 3 * sizeof(indirizzo) + sizeof(indirizzo) = 34+38+8 = 44 byte ^^^^^^^^^^^^ ^^^^^^^^^^^^^ sarebbe prossimo | sarebbe l’indirizzo che sta ognuno per | iniziale della lista ogni elemento |capita una volta e infatti essendoci 3 |si trova nello stack elementi c’è “3” | Caso generale: n contatti : n4 + n8 + 8* SE CONOSCO LA DIMENSIONE ESATTA E COSTANTE --> ARRAY SE NON CONOSCO LA DIMENSIONE E POTREBBE ESSERE ANCHE PICCOLA -> LISTA LISTA CONCATENATA OPPURE LINKEDLIST OPPURE LISTA SINGOLARMENTE CONCATENATA struct Elemento { int valore; struct Elemento* prossimo; } Al compilatore interessa quanto spazio occupa la struttura in memoria e come deve rappresentare i suoi valori

struct Elemento* occupa in memoria lo spazio necessario per un indirizzo, indipendentemente dal fatto che sia un puntatore proprio a struct Elemento ed è rappresentato come tutti gli indirizzi come un intero senza segno VISUALIZZAZIONE DI UNA LISTA (con funzioni) struct Elemento { int valore; struct Elemento prossimo; } //per poter conoscere tutti gli elementi della lista abbiamo bisogno dell’indirizzo iniziale della lista ,ma //non dobbiamo sapere quanto è lunga perché è presente alla fine NULL (cosi come le stringhe che //hanno il terminatore) void visualizza (struct Elemento lista){ //una lista si passa tramite puntatore che nel nostro caso si //chiama struct Elemento* lista(indirizzo in cui inizia la lista)**  possiamo anche omettere la parola //“struct”. //A questo punto abbiamo bisogno di un indice che cammina sugli elementi della listail nostro indice a differenza degli array(in cui usiamo un for) questa volta è un puntatore struct Elemento p = lista; //indice per scorrere la lista --- e' un alias*  quindi ora dove comincia la nostra // ce lo sa dire sia la variabile “lista” che la variabile “p” while (p!=NULL){ //Abbiamo bisogno di un ciclo per leggere tutti gli elementi che dura finchè non cout<< p->valore<<endl; // incontriamo il puntatore NULLla lista potrebbe anche essere vuota e p = p->prossimo; //quindi il ciclo giusto è il NULL } return; } Abbiamo introdotto una p e quindi un alias perché alla fine dell’esecuzione p vale NULL quindi chi si ricorda dove comincia la lista è struct Elemento listasenza l’uso di questo alias saremmo arrivati alla fine dell’algoritmo senza ricordarci più dove cominciava la lista e quindi la lettura si sarebbe compiuta ma subito dopo la lista si sarebbe autodistrutta. Mentre con questa soluzione si autodistrugge l’alias ma non la lista. Quindi bisogna sempre operare su di una copia (alias) del puntatore! void main(){ struct Elemento lista=NULL; lista = new struct Elemento; lista->valore=1; lista->prossimo = new struct Elemento; lista->prossimo->valore=13; lista->prossimo->prossimo = new struct Elemento; lista->prossimo->prossimo->valore = 71; lista->prossimo->prossimo->prossimo=NULL; visualizza (lista); }

RICERCA DI UN ELEMENTO IN UNA LISTA (DATA LA POSIZIONE): RESTITUISCE IL VALORE TROVATO

//in tal caso utilizziamo la funzione booleana perché l’elemento potrebbe sia esserci che non esserci data una posizione che va oltre e se c’è ci restituisce il valore trovato in quella posizioneecco allora spiegato il motivo per cui il valore è passato per riferimento in quanto non fisso e quindi modificabile in base alla posizione bool cerca (Lista lista, int posizione, int& valore){ Lista p = lista; for (int i=0;p!=NULL && i!=posizione; p=p->prossimo, i++) ; if (p==NULL) return false; else{ valore=p->valore; return true; } } VARIANTE CON IL PASSAGGIO PER PUNTATORE bool cerca (Lista lista, int posizione, int* valore){ Lista p = lista; for (int i=0;p!=NULL && i!=posizione; p=p->prossimo, i++) ; if (p==NULL) return false; else{ *valore=p->valore; return true; } } NEW E' UN OPERATORE INTRODOTTO CON IL C++ IN C NON ESISTEVA E AL SUO POSTO SI UTILIZZAVA LA FUNZIONE malloc (memory allocation)

ALGORITMO PER TRASFORMARE UN ARRAY IN UNA LISTA

//trasforma un array in una lista

void riempiLista(Lista& l, int v[],int riemp){

l=new struct Elemento;

Lista p=l;//in questo caso l’alias p serve per forza

for (int i=0;i<riemp;i++){//andava anche bene for(int i=0;i<riemp;i+

+ ,p->prossimo)

p->valore=v[i];

if (i<riemp-1)

p->prossimo=new struct Elemento;

else

p->prossimo=NULL;

p=p->prossimo;//però qui non

mettevamo nulla

return;

Noi abbiamo un array int v[] e il suo riempimento int riemp che sono fondamentali per copiare tale array nella lista. Prima abbiamo composto le funzioni visualizza e cerca, ed entrambe le funzioni comportano essenzialmente l’osservazione della lista ma non la sua modifica, e siccome non dovevamo modificare la lista abbiamo passato semplicemente il puntatore della lista ovvero l’indirizzo iniziale della lista e quindi ragionandoci se noi abbiamo passato l’indirizzo per valore significava che noi non potevamo modificare l’indirizzo iniziale della lista, dunque ad esempio non potevamo togliere elementi della lista. In tal caso essendo la nostra lista inizialmente vuota e vogliamo riempirla di elementi ciò significa che prima di chiamare la nostra funzione la nostra lista ha indirizzo NULL mentre alla fine dovrà avere l’indirizzo di un vero elementoquindi noi dobbiamo avere la possibilità di modificare l’indirizzo. Quindi in tal caso la variabile l di tipo Lista la dobbiamo passare per riferimento Lista passata per riferimento perchè ne debbiamo aggiugere elementi, cioè modificare l'indirizzo di partenza da NULL verso qualcos'altro L'alias p è fondamentale per non dimenticare l'indirizzo iniziale l che abbiamo dato alla lista appena riempita Analizziamo la funzione riempiLista: creiamo un primo elemento e quindi abbiamo scritto l=new struct elemento dove l è la nostra lista iniziale che valeva NULL e con questa istruzione gli assegniamo un nuovo elemento. Poi nel seguito ci creiamo un alias p per non dimenticare l’indirizzo iniziale di lpoi per ogni elemento del vettore(array) ci mettiamo tale elemento nella lista e poi dopo andiamo a creare un nuovo elemento alla lista quindi ci facciamo un ciclo su tutti gli elementi dell’array e ogni volta che troviamo un elemento dell’array aggiungiamo tale valore nella nostra lista (p->valore=v[i]) poi ci sono due casi : se i<riemp-1 cioè se l’array se non è finito allora iniziamo ad anticipare che abbiamo bisogno di un posto per il nuovo elemento della lista (p->prossimo=new struct Elemento) altrimenti nel caso in cui ci troviamo all’ultimo elemento dell’array allora metteremo p->prossimo=NULL In entrambi i casi p=p->prossimo ci spostiamo al prossimo per un nuovo inserimento(risulta chiaro che se l’elemento dell’array è l’ultimo allora al prossimo giro pur facendo p=p->prossimo nel for non sarà rispettata la condizione i<riemp anzi in quel caso i sarà proprio uguale a riemp, e quindi ci butterà fuori dal for); INSERIMENTO DI UN ELEMENTO ALL'INIZIO (IN TESTA) DI UNA LISTA Lista con due elementi (142) 1 (144) 246 (246) 13 (248) NULL (348) 23 (350) 142 (1000) 142 348 (1008) 348

  1. Creare un nuovo elemento (nel nostro caso l’elemento 23 all’indirizzo 348) chiaramente questo elemento ci piace pensarlo come primo elementoCOME FACCIAMO A RENDERLO IL PRIMO? 2) Il prossimo del nostro nuovo elemento deve essere quello che era il primo
  2. Il primo ora è quello appena messo(ovvero togliamo il 142 e al suo posto ci mettiamo 348)

numero 1(visto che viene eliminato) ma l’indirizzo del secondo elemento ovvero 13 che in questo caso diventa il primo

  1. Aggiorno il puntatore all'inizio
  2. Elimino l'elemento void eliminaInTesta (Lista& lista){ If (lista!=NULL){ struct Elemento p=lista;* //indirizzo dell'elemento da eliminare ci creiamo un’aliasprima ci mettiamo // da parte il valore dell’elemento che stiamo per distruggere lista = lista -> prossimo; //andava anche bene p->prossimo //poi possiamo aggiornare il nostro elemento //iniziale in funzione di quello che era prima delete p; } return; } ELIMINAZIONE DI UN ELEMENTO DATO IL VALORE
  3. trovare l'elemento
  4. se l'abbiamo trovato, eliminarlo Esempio: vogliamo eliminare il 13 (142) 1 (144) 246 (204) 71 (206) NULL (246) 13 (248) 204 (1000) 142 Esempio: l'elemento da distruggere è il secondo. Per distruggerlo:
  5. Il primo deve puntare al terzo(il precedente deve puntare al p->prossimo)
  6. Elimina il secondo Il problema è che l’algoritmo di ricerca ci restituisce la posizione del secondo, ma non ci restituisce la posizione del primo COME FACCIAMO A SAPERE DOVE STA IL PRIMO SAPENDO IL SECONDO? Il primo rispetto al secondo è precedentequindi dobbiamo fare la variante dell’algoritmo di ricerca che(ricorda) tiene pure il precedente

//elimina l'elemento valore dalla lista (se c'è)

void elimina(Lista &l, int valore){

Lista p=l; // ci serve l’alias p di l perché altrimenti ci dimentichiamo

l’originale

struct Elemento* precedente=NULL; //il precedente lo inizializziamo a

NULL

bool trovato=false;

while (p!=NULL && !trovato)

if (p->valore!=valore){ //se non è l’elemento cercato  andiamo avanti

con la ricerca

precedente=p; //per andare avanti scriveremo precedente=p ; p=p-

>prossimo

p=p->prossimo;

else//altrimenti restituiamo trovato=true;  cioè l’abbiamo trovato

trovato=true;

Cosi’ facendo quando l’abbiamo trovato succederà che p ci dice l’indirizzo dell’elemento da eliminare mentre precedente ci dice l’indirizzo dell’elemento prima da quello da eliminare. if (trovato){ //l'elemento trovato è p, preceduto da precedente if (precedente==NULL){ //E' il primo elemento l=p->prossimo; } else { precedente->prossimo=p->prossimo; } delete p; } return; }