Linguaggio C Strutture dati lineari: Liste lineari, Stack e Code. Strutture dati non lineari: Alberi e Grafi. 1 Liste Lineari • Insieme ordinato di elementi di uno stesso tipo, tali che ogni elemento ha un predecessore e un successore, ad esclusione del primo e dell’ultimo; 1) x0 è il primo elemento della Lista ed è seguito da x1; 2) xi è preceduto da xi-1 e seguito da xi+1, ∀ 1≤ i ≤ n-2; 3) xn-1 è l’ultimo elemento della Lista ed è preceduto da xn-2; • n è la lunghezza della Lista; se n = 0 la lista è vuota; • La relazione di precedenza significa che per accedere ad un elemento bisogna accedere a tutti quelli che lo precedono (a differenza del vettore); 2 Liste vs Vettori • Rispetto ai Vettori, le Liste sono caratterizzate da: 1) Numero variabile di elementi durante l’esecuzione del Programma (strutture dati dinamiche); 2) Consentono una gestione più efficiente della memoria in termini di spazio allocato. 3) Modalità di accesso ai dati sequenziale (in un Vettore l’accesso al dato è diretto, in una Lista per accedere ad un dato occorre prima accedere a tutti gli elementi che lo precedono). 3 Realizzazione concreta di Liste mediante strutture concatenate • Il campo next è rappresentato da un “puntatore” ad una struttura “nodo” della Lista: struct nodo { tipoinfo info; struct nodo *next; }; • Si deve allocare complessivamente una struttura dati con la seguente configurazione: first info0 first è detto “testa della lista”: consente l’accesso alla Lista next info1 next … info(n-1) last NULL 4 Lista con strutture concatenate con dati letti da file struct studente { char nome[30]; char cognome[50]; int voto_informatica; struct studente *next; }; typedef struct studente *p_st; p_st stud, stud_last, stud_first; FILE *fp; file.dat Mario Rossi 25 Giorgio Bianchi 30 … … … if((fp = fopen("file.dat", "r")) == NULL) exit(0); stud_first è la testa stud_first = stud_last = NULL; della Lista. Inoltre while(!feof(fp)) { abbiamo stud_last che stud = (struct studente *)malloc(sizeof(struct studente)); punta all’ultimo elemento stud->next = NULL; della Lista. fscanf(fp, "%s %s %d\n", stud->nome, stud->cognome, &(stud->voto_informatica)); if (stud_first == NULL) stud_first = stud; Nota: stud_last->next = stud else “appende” in coda l’elemento appena allocato, stud_last->next = stud; stud_last = stud; } mentre stud_last = stud aggiorna il puntatore all’ultimo elemento. 5 Visualizzazione elementi di una Lista … stud = stud_first; //stud_first contiene l’indirizzo al primo //elemento della lista while(stud != NULL) { printf("%20s ", stud->nome); printf("%20s ", stud->cognome); printf("%2d\n", stud->voto_informatica); stud = stud->next; } 6 Stack (o Pila) • Lista lineare in cui gli inserimenti e le estrazioni avvengono da un estremo detto testa o top dello stack (si pensi ad una “pila di libri”); • Politica di gestione LIFO (Last In First Out); • Si può realizzare con Vettori o con Liste concatenate. Top dello stack (TOS) F E Recupero (pop) (operazione distruttiva) Inserimento (push) D C B A 7 Stack (o Pila) • Un’applicazione molto importante in Informatica è la gestione della chiamata delle funzioni (o procedure) nei Linguaggi di Programmazione. • Si immagini una struttura dove vengono memorizzati: – L’indirizzo dell’ultima istruzione eseguita nella funzione chiamante, – I parametri della funzione chiamata, inizializzati con una copia dei valori passati dalla funzione chiamante, – Le variabili dichiarate nella funzione chiamata. • Al momento della chiamata di una funzione, il Record relativo viene inserito (push) nello Stack. Quando la funzione termina, il Record viene rimosso (pop) e il controllo torna alla funzione che si trova nel TOS. 8 Realizzazione di stack mediante strutture concatenate: Algoritmi di inserimento (push) e estrazione (pop) void push(tipoinfo x) { static struct nodo *first_elem; struct nodo *elem; struct nodo *pop() { static struct nodo *first_elem; struct nodo *elem; elem = (struct nodo *) malloc(sizeof(struct nodo)); elem->info = x; elem->next = NULL; if (first_elem == NULL) return NULL; if (first_elem { first_elem } else { elem->next first_elem } == NULL) first_elem = first_elem->next; = elem; return(elem); elem = first_elem; } = first_elem; = elem; } 9 Coda • Lista lineare in cui le estrazioni avvengono da un lato e gli inserimenti dall’altro; • Politica di gestione FIFO (First In First Out); • Si può realizzare con un Vettore o con Liste concatenate. A Recupero (cancellazione) B C D E F Inserimento (operazione distruttiva) 10 Realizzazione di code mediante strutture concatenate: Algoritmi di inserimento e estrazione void push(tipoinfo x) { static struct nodo *first_elem,*last_elem; struct nodo *elem; struct nodo *pop() { static struct nodo *first_elem; struct nodo *elem; elem = (struct nodo *) malloc(sizeof(struct nodo)); elem->info = x; elem->next = NULL; if (first_elem == NULL) return NULL; if (last_elem == NULL) { last_elem = elem; first_elem = elem; } else { last_elem->next = elem; last_elem = last_elem->next; first_elem = first_elem->next; elem = first_elem; return(elem); } } } 11 Proprietà di Liste, Stack e Code • Stack e Code si possono realizzare anche mediante Vettori in cui ogni elemento contiene solo l’informazione, essendo note le posizioni di inserimento e di cancellazione; • Le Liste rappresentano una struttura dati molto più flessibile rispetto al vettore, in quanto inserimenti e cancellazioni possono essere effettuati in qualunque posizione: questa flessibilità è possibile grazie all’utilizzo dei puntatori. • Quando di deve privilegiare la velocità di accesso alle informazioni, meglio utilizzare i Vettori (o Tabelle nel caso di strutture). 12 Definizione di Albero • Un insieme A di elementi si dice Albero se: A = ∅; oppure: A = A (r, A0, A1, …, An-1); r = radice; Ai = Albero, detto “sottoalbero i-esimo di A”; • E’ una definizione ricorsiva: Struttura dati vuota, oppure costituita da un elemento detto radice e da altri elementi che formano insiemi disgiunti chiamati sottoalberi della radice, ciascuno dei quali è ancora un Albero; 13 Nomenclatura sugli Alberi • Grado di un nodo: • Foglia o nodo terminale: • Padre: • • • • • • n° di sottoalberi di quel nodo; nodo di grado 0; nodo non terminale da cui discendono i suoi sottoalberi; nodi che discendono da uno stesso padre; Figli: Radice: nodo che non discende da altri nodi. X è discendente di Y se ∃ x0, x1,…,xn-1: Y = x0 x1…xn-1= X e xi è discendente diretto di xi-1, tale successione si chiama cammino; Lunghezza del cammino = (n° nodi del cammino – 1); Livello (o profondità) di un nodo X: è i se il livello del padre di X è i-1, con la convenzione che la radice ha livello 0 (definizione ricorsiva); Altezza = livello massimo delle foglie + 1 (in pratica equivale al numero dei livelli dell’Albero); 14 A-B-D = cammino di lunghezza 2 B Foglie D A = Radice, Nodo di grado 2 e livello 0 A E C F C = Nodo di grado 0 e livello 1 B = Nodo di grado 3 e livello 1 D,E,F = Nodi di grado 0 e livello 2 Altezza dell’albero = (Livello max delle foglie + 1) = 3 15 Definizione di Albero binario • Un insieme A di elementi si dice Albero binario se: A = ∅; oppure: A = A (r, Al, Ar); r = radice; Al = Albero binario detto “sottoalbero sinistro”; Ar = Albero binario detto “sottoalbero destro”; • Struttura dati vuota, oppure costituita da un elemento detto radice e da due elementi disgiunti che sono a loro volta alberi binari detti sottoalbero sinistro e destro; • Albero binario bilanciato: albero nel quale le foglie sono distribuite al più su due livelli consecutivi k-1 e k, e il numero di nodi a livello k-1 deve essere 2k-1 (ossia, fino al livello k-1 l’Albero deve essere completo); 16 Proprietà di Alberi binari • • E’ possibile individuare un limite sulla profondità massima in funzione del numero dei nodi, nel caso di Alberi binari bilanciati. Infatti, se un Albero binario di profondità (massima) D è bilanciato (ossia completo fino al livello D-1), il numero totale N dei nodi contenuti rispetta la seguente disequazione: D −1 N ≥ 1 + ∑ 2d = 2 D d =0 • Segue quindi un Limite Superiore relativo alla profondità massima D dell’Albero binario bilanciato: D ≤ log2 ( N ) 17 Realizzazione concreta di un Albero binario • Mediante strutture concatenate contenenti – Un campo informazione; – Due campi puntatore ai nodi radice del sottoalbero sinistro e destro. r struct nodo { tipoinfo info; struct nodo* left; struct nodo* right; } struct nodo *r; left left info right info right left info right 18 Visita di Alberi binari • Attraversamento di tutti i nodi di un Albero binario, secondo particolari criteri: 1. 2. 3. Visita Anticipata (radice, sottoalbero sinistro, sottoalbero destro); Visita Simmetrica (sottoalbero sinistro, radice, sottoalbero destro); Visita Differita (sottoalbero sinistro, sottoalbero destro, radice). 19 Visita Anticipata • visita_anticipata(A) 1. Passo base: - se A = ∅ ⇒ esci; a A b 2. Passo di induzione: - • c d e f si visita la radice; visita_anticipata(Al) //Al sottoalbero sinistro; visita_anticipata(Ar) //Ar sottoalbero destro; Risultato visita anticipata: abdefc 20 Visita Anticipata void visita_anticipata(struct nodo *r) { if (r == NULL) return; printf(“%s\n”, r->info); visita_anticipata(r->left); visita_anticipata(r->right); } 21 Visita Simmetrica a A • b visita_simmetrica(A) 1. Passo base: - se A = ∅ ⇒ esci; 2. Passo di induzione: - • c d e f visita_simmetrica(Al) //Al sottoalbero sinistro; si visita la radice; visita_simmetrica(Ar) //Ar sottoalbero destro; Risultato visita simmetrica: defbca 22 Visita Simmetrica void visita_simmetrica(struct nodo *r) { if (r == NULL) return; visita_simmetrica(r->left); printf(“%s\n”, r->info); visita_simmetrica(r->right); } 23 Visita Differita a A • visita_differita(A) 1. Passo base: - b c d se A = ∅ ⇒ esci; e 2. Passo di induzione: - • f visita_differita(Al) //Al sottoalbero sinistro; visita_differita(Ar) //Ar sottoalbero destro; si visita la radice; Risultato visita differita: fedcba 24 Visita Differita void visita_differita(struct nodo *r) { if (r == NULL) return; visita_differita(r->left); visita_differita(r->right); printf(“%s\n”, r->info); } 25 Altezza Albero binario • altezza_albero (A) A b 1. Passo base: - se A = ∅ ⇒ restituisci 0; 2. Passo di induzione: - restituisci a c d e f altezza = 1 + max(altezza_albero(Ar), altezza_albero(Al)); • altezza_albero (A) ⇔ 5 26 Altezza Albero binario int altezza_albero(struct nodo *r) { if (r == NULL) //Passo base return 0; //Passo induzione return (1 + max(altezza_albero(r->left), altezza_albero(r->right))); } NOTA: max non è definita nella Libreria Standard. 27 Albero binario di ricerca Definizione: Un Albero binario si dice di ricerca se è vuoto oppure: è costituito da una radice e due sottoalberi di ricerca sinistro e destro tali che: – gli elementi del sottoalbero sinistro sono “minori” della radice; – gli elementi del sottoalbero destro sono “maggiori” della radice; F Es: B A 1 L D C 5 -2 7 3 4 28 Costruzione di un Albero binario di ricerca struct nodo { tipoinfo info; struct nodo *left; struct nodo *right; }; typedef struct nodo *p_n; p_n inserisci_nodo(p_n root, tipoinfo x) { if (root == NULL) { root = (struct nodo *)malloc(sizeof(struct nodo)); root->info = x; root->left = NULL; root->right = NULL; } else { if (x < root->info) root->left = inserisci_nodo(root->left, x); else root->right = inserisci_nodo(root->right, x); } return root; } 29 Grafi • Grafo diretto o orientato: – Insieme N di elementi detti nodi e insieme di archi. Un arco è una coppia ordinata (v,w) di nodi (v,w ∈ N); • Grafo non orientato: – Insieme N di nodi e insieme di archi. Un arco è una coppia non ordinata (v,w) di nodi (v,w ∈ N); 30 Nomenclatura sui Grafi • Se (v,w) ∈ N sono collegati da un arco, si dice che v è adiacente a w; • Cammino di un grafo: sequenza di nodi v0, v1, …, vn-1, tali che (v0, v1), (v1, v2), …, (vn-2, vn-1) sono archi; • Lunghezza del cammino: (n° di archi del cammino), oppure (n° nodi del cammino – 1); • Cammino semplice: cammino in cui tutti i nodi, eccetto eventualmente il primo e l’ultimo, sono distinti; 31 ABCD = cammino semplice A B C E Lunghezza(ABCD) = 3 D F 32 Realizzazioni concrete di un Grafo • Matrice di adiacenza A = A(aij) – aij = 1 ⇔ nodo i è connesso a nodo j – aij = 0 ⇔ nodo i non è connesso a nodo j Proprietà: la matrice di adiacenza di un grafo non diretto è simmetrica (aij = aji) • Liste di adiacenza: – Ogni nodo è descritto da una lista di elementi contenente il nome dei nodi ad esso adiacenti; 33 Realizzazione mediante Matrice di Adiacenza A B C D E F A B C 1 D E 1 F B 1 1 A C E 1 D 1 F 34 Realizzazione mediante Liste di Adiacenza A B E B C NULL C D D NULL E F F NULL E NULL A NULL B C NULL D E F 35 Algoritmi di visita per i Grafi • Si dice visita di un Grafo un insieme di cammini con origine in uno stesso vertice; ovvero: Attraversamento di tutti i nodi di un Grafo secondo un particolare criterio; • Si distinguono due tipi di visita di un Grafo: – In profondità; – In ampiezza. 36 Grafi: visita in profondità • visita_profondità(G) 1. Passo base se G = Ø ⇒ esci; 2. Passo di induzione visita il nodo G se non è stato visitato per ogni nodo adiacente G->adj visita_profondità(G->adj); • Osservazione: la visita in profondità è strettamente legata alla politica di gestione di uno Stack (LIFO). 37 Risultato visita in profondità A ABCDEF B C D E F 38 Grafi: visita in ampiezza • visita_ampiezza(G) si inserisce il nodo di partenza in una coda; per ogni nodo non visitato: - si estrae dalla coda un nodo e si visita; - si inseriscono in coda tutti gli elementi adiacenti al nodo corrente. • Osservazione: la visita in ampiezza è strettamente legata alla politica di gestione di una Coda (FIFO). 39 Risultato visita in ampiezza A ABECFD B C D E F 40