La presentazione è in caricamento. Aspetta per favore

La presentazione è in caricamento. Aspetta per favore

Strutture dati elementari Parte 6 Vettori e ricerca binaria Matrici e triangolo di Tartaglia Records (cenni) Corso A: Prof. Stefano Berardi

Presentazioni simili


Presentazione sul tema: "Strutture dati elementari Parte 6 Vettori e ricerca binaria Matrici e triangolo di Tartaglia Records (cenni) Corso A: Prof. Stefano Berardi"— Transcript della presentazione:

1 Strutture dati elementari Parte 6 Vettori e ricerca binaria Matrici e triangolo di Tartaglia Records (cenni) Corso A: Prof. Stefano Berardi Corso B: Prof. Ugo de’ Liguoro

2 “Un quadrato magico di numeri” Albert Durer. Melencolia,1514 (dettaglio)

3 Indice Parte 6: i Vettori 1. Strutture dati: i vettori. 2. Esempi elementari: stampa, somma, test di uguaglianza, inversione per vettori. 3. Esempi più complessi: ricerca lineare e binaria per vettori. 4. Matrici: il triangolo di Tartaglia. 5. Records e vettori parzialmente riempiti (cenni).

4 1. Strutture dati: I Vettori In generale, le strutture dati sono un modo per rappresentare insiemi di informazioni nella memoria di un computer Una semplice variabile è un caso banale di una struttura dati, un modo per rappresentate una singola informazione. In questa parte del corso studieremo strutture dati per rappresentare insiemi di dati di tipo omogeneo, i vettori e le matrici, e per rappresentare dati di tipo eterogeneo, i record.

5 Vettori (o “array”) in C++ Un vettore v (array) è una sequenza di n oggetti tutti del medesimo tipo, detti elementi del vettore, per qualche n>0 detto la dimensione del vettore. Gli elementi del vettore sono indicizzati utilizzando interi positivi, da 0 fino a n- 1, immaginati disposti come segue: v[0], v[1], …, v[n-1]

6 Notazioni per i vettori Le notazioni che seguono NON sono codice C/C++, ma sono la notazione che usiamo quando parliamo dei vettori: i..j = {k  Z| i  k  j} intervallo di interi v[i..j] = {v[i], v[i+1], …, v[j]} Quindi, se i > j allora i..j = lista vuota; se un vettore v ha n elementi questi formano l’insieme v[0..n-1].

7 I vettori nella memoria RAM Gli elementi di un vettore occupano spazi di memoria consecutivi nella memoria RAM della macchina di Von Neumann. L’indirizzo &v[i] di ogni elemento si calcola dunque con la formula: &v[i] = &v[0] + i  d, dove d = sizeof (tipo di v[]) v[0] ind. b v[1] ind. b+d v[2] ind. b+2 d b = indirizzo base = &v[0] =indirizzo v[0] (es.:byte n. 1 milione) d = dimensione di ogni elemento (es.: 4 bytes) v[i] indirizzo b+i*d &v[i] = indirizzo v[i] = b+i  d i = indice di v[i] (detto anche “spiazzamento” ). Es.: i=100, indirizzo di v[i] = v

8 Vettori: dichiarazione Un vettore v in C++ è una costante di tipo indirizzo. Viene identificato con &v[0], l’indirizzo del suo primo elemento. La dichiarazione di v consiste nell’indicazione del nome e del tipo degli elementi del vettore, e nell’indicazione del loro numero, detto la dimensione del vettore. Con sizeof(v) si indica invece il numero di bytes occupati dal vettore: int v[100]; Tipo degli elementi Nome del vettore: v vale &v[0] 100 = numero degli elementi, o dimensione di v Invece: sizeof(v) = 100*sizeof(int) = 400

9 Vettori: l’errore più comune La dimensione di un vettore deve essere una costante o una variabile già assegnata. Quindi le seguenti righe sono errate (ma purtroppo il compilatore non lo segnala!): int dim; double w[dim]; /* ERRORE: la variabile dim, per il solo fatto di essere dichiarata, ha un valore, ma non si può prevedere quale. Quindi si crea un vettore di dimensione “casuale”, a volte di miliardi di elementi, producendo in quest’ultimo caso un “crash” di programma */

10 Accesso agli elementi di un vettore Per accedere ai singoli elementi di un vettore si usano gli indici: attenti a non confondere gli indici con la dimensione che compare nella dichiarazione. int v[100]; /* dichiarazione */ v[0] = 6; /* assegnazione, accesso in scrittura */ int n = v[0]; /* assegnazione, accesso in lettura */ Gli interi tra [ ] hanno diverso significato in una dichiarazione e in una assegnazione: nella dichiarazione, 100 è la dimensione, nell’assegnazione, 0 è un indice

11 Inizializzazione attraverso un ciclo Il programma che segue inizializza a 0 tutti gli elementi del vettore v: int v[100]; for (int i = 0; i < 100; i++) v[i] = 0; i dichiarato entro il for esisterà soltanto entro il for Nell’esecuzione del for, i assumerà tutti i valori tra 0 e 99 Prima dell’inizializzazione, tutti gli elementi di v, avendo un indirizzo di memoria, hanno comunque un valore, ma un valore “casuale”: provate a stamparli.

12 Inizializzazione attraverso una dichiazione Il C++ consente di definire (quindi anche di inizializzare) un vettore elencandone gli elementi. Non è necessario indicare il numero degli elementi, che viene calcolato: double a[] = {22.2, 44.4, 66.6}; // alloca un vettore a di 3 float con // a[0] = 22.2, a[1] = 44.4, a[2] = 66.6 Guardate l’esempio 6.3 del testo di Hubbard: spiega che la dimensione di a si calcola con la formula sizeof(a)/sizeof(double)

13 Attenti a non uscire dai limiti di un vettore Se un vettore ha dimensione d ed i<0, oppure i  d, allora v[i] non dovrebbe essere definito. Invece, in C/C++, v[i] esiste sempre, è per definizione il contenuto dell’indirizzo di memoria calcolato dalla formula: &v[0] + i  sizeof (tipo di v[]) ossia un valore a caso!! Questa convenzione è introdotta per semplicità di calcolo. Il compilatore non ci avvisa quando utilizziamo un v[i] con i<0, oppure i  d, sta a noi evitare che accada. Questo non accade in PASCAL: vedi Hubbard paragrafo 6.4

14 2. Alcuni semplici esempi: stampa di un vettore La stampa di un vettore deve avvenire “elemento per elemento” (dunque usando ad es. un ciclo for): int v[100]; for (int i = 0; i < 100; i++) cout << v[i]; Se si scrive invece: cout << v; dato che v è identificato con l’indirizzo di v[0] si stampa l’indirizzo di v[0] (in esadecimale)

15 Somma e media di un array di numeri double v[100]; double somma = 0; for (int i = 0; i < 100; i++) somma = somma + v[i]; double media = somma/100.0; /*dividiamo per un numero reale per evitare l’arrotondamento*/ Per tutte le sommatorie di cui non si conosca a priori nemmeno un termine, il valore iniziale dell’accumulatore è 0, l’elemento neutro della somma Anche la somma di un vettore deve avvenire “elemento per elemento” (dunque usando ad es. un ciclo for):

16 Un test di uguaglianza errato Se applichiamo il test == a due vettori distinti otteniamo risposta costantemente uguale a false : int v[100], w[100]; if (v == w) … /* v, w sono identificati con gli indirizzi dei loro primi elementi, dunque il test == confronta questi ultimi. Dato che v, w sono “allocati” in posizioni diverse della memoria, i loro primi elementi hanno diversi indirizzi, anche quando v, w hanno elementi di valore uguale. Dunque v==w vale sempre false. */

17 Un test di uguaglianza corretto Per decidere se due array di egual tipo e egual dimensione N hanno valori uguali per indici uguali, si devono confrontare tutti gli elementi usando un’iterazione. Ecco una soluzione con un WHILE: /* Il ciclo while trasporta il contatore i al primo indice per cui v[i]!=w[i], se ne esiste uno, altrimenti trasporta i fino ad N */ int i = 0; while (i < N && v[i] == w[i]) i++; if (i == N) cout << "v,w hanno elementi uguali per indici uguali"; else /* i < N */ cout << "v, w hanno diverso l’elemento di posto" << i;

18 Un test di uguaglianza corretto 2 int i; for (i=0; i < N && v[i] == w[i]; i++){ }; //il corpo del FOR e’ vuoto if (i == N) cout << "v,w hanno uguali per indici uguali"; else cout << "v, w hanno diverso l’elemento di posto" << i; i è dichiarata fuori del for perché l’ if che segue ne possa fare uso Per decidere se due array di egual tipo e egual dimensione N hanno valori uguali per indici uguali, si devono confrontare tutti gli elementi usando un’iterazione. Ecco la traduzione della soluzione precedente in un ciclo FOR:

19 Passaggio di un vettore ad una funzione Un array viene passato alle funzioni sempre per indirizzo. Attenzione: nella dichiarazione del parametro scriviamo int a[], nella chiamata non ripetiamo il tipo e scriviamo solo a: int sum( int a[], int n) /* prec.:0<=n<=dim.a post.cond:sum(a,n) = somma di a[0..n-1]*/ {int i,s; for (i=0,s=0; i

20 Modifica di un vettore Poiché un vettore viene passato per riferimento (indirizzo) se una chiamata di funzione modifica gli elementi del vettore, queste modifiche sono permanenti: void scambia (int v[], int i, int j) // pre: 0  i, j < dimensione di v // post: scambia v[i] con v[j] { int temp = v[i]; v[i] = v[j]; v[j] = temp; }

21 Inversione di un vettore v Idea. Poniamo due indici i, j agli estremi di v e muoviamo i, j uno verso l’altro. Scambiamo tra loro gli elementi di posto i e j, fino a che tutti gli elementi alla sinistra di v sono scambiati con tutti gli elementi alla destra di v. void inverti (int v[], int n) /* PRE: 0  n  dimensione di v. POST: elementi v[0..n-1] in ordine inverso */ {for (int i = 0, int j=n-1; i

22 3. Ricerca Lineare e Binaria Si vuole decidere se un intero n appartiene alla sequenza rappresentata dal vettore v. Il metodo più semplice è la ricerca lineare: confrontiamo n con v[0], v[1], v[2], … in quest’ordine. bool Member (int n, int v[], int dim) /* pre: la dimensione di v e’ >= dim >=0. post: restituiamo true  esiste i tale che v[i]==n */ {bool trovato = false; /* “trovato” indica se n e’ gia’ stato trovato. “trovato” puo’ cambiare solo da false a true, mai viceversa.*/ for(int i=0;i

23 Ricerca lineare con interruzione: uso di una variabile “flag” Per efficienza, potremmo voler interrompere la ricerca di n non appena troviamo n. A tal fine, definiamo un valore booleano “trovato” che parte da “vero”, e non appena “trovato” vale vero usciamo. Il test del ciclo quindi deve essere: continuiamo se “!trovato” è vero. Una variabile booleana che ci avvisa quando uscire da un ciclo è detta una “flag”. bool Member (int n, int v[], int dim) /* pre: la dim. di v e’ >= dim >=0. post: true sse esiste i t.c. v[i] == n*/ {bool trovato = false; //detta variabile “flag” for (int i=0; i

24 Ricerca lineare con interruzione: seconda soluzione Possiamo eliminare la “flag” trovato, spostando (la negazione di) v[i] == n nel test del for. In tal caso, dobbiamo dichiarare i fuori dal for: bool Member (int n, int v[], int dim) // pre: la dimensione di v e’ >= dim >= 0 // post: true sse esiste i t.c. v[i] == n { int i; for (i=0; i < dim && v[i] != n; i++){}; return (i < dim);} Se i < dim è vero, il for è terminato perchè v[i] == n, altrimenti è terminato perchè il vettore è finito senza trovare n

25 Ricerca binaria Se un vettore (per es. di interi) è ordinato in senso crescente, la ricerca di un elemento nel vettore può essere enormemente accelerata sfruttando il metodo della Ricerca Binaria: 1. Manteniamo due indici, i e j, a delimitazione della porzione v[i…j] di v in cui cercare 2. Ad ogni passo confrontiamo n con il valore di indice medio in i..j, cioè m=(i+j)/2 3. Se v[m] != n allora cerchiamo in v[i..m-1] o in v[m+1..i] a seconda che n < v[m] oppure v[m] < n. Daremo ora una descrizione dettagliata del funzionamento della ricerca binaria, in 5 tappe.

26 Ricerca binaria 1: pre- e post-condizioni 1. Definiamo il problema. Input: il “valore cercato” in v è n. Pre-condizione: il vettore v di dimensione dim è ordinato. Post-condizione: restituire l’indice del valore cercato (un i tale che v[i]=n, se esiste), altrimenti la lunghezza dim di v (dim sta per “non trovato”) 0 dim-1= dim-1= Valore cercato: n = 25. Indice del valore cercato: 7 ( v[7]=25) 7

27 Ricerca binaria 2: una proprietà invariante 2. Individuiamo una proprietà “invariante” della ricerca binaria, cioè una proprietà significativa che resta vera durante tutta la durata della ricerca binaria: Propr. Invariante: durante la ricerca binaria considero solo dei segmenti v[i…j] di v tali che: se n è in v, allora n è in v[i…j]. Per es.: se n=25 è in v, allora n è in v[i..j] = v[3..16]. indice i=3 j=16 indice 19 Cerco n=25 in v[i…j] v[i…j]

28 Ricerca binaria 3: il funzionamento 3. La ricerca binaria cerca un modo per avvicinarsi alla soluzione mantenendo vero l’invariante Passo generico: dividiamo il sottovettore in due parti (quasi) uguali. Caso 1. Se n si trova nel punto intermedio: restituisco m Se il valore cercato è n = 43 allora l’ho trovato Punto intermedio m di i…j 0dim-1= Spazio dove avviene la ricerca di n

29 Ricerca binaria 3: il funzionamento Valore cercato: n = 25 Punto intermedio m di i..j Spazio di ricerca Passo generico: dividiamo il sottovettore in due parti (quasi) uguali. Caso 2. Se il valore n cercato è < di quello nel punto intermedio, allora, dato che il vettore è ordinato, n si trova nella parte sinistra di v[i…j]. 3. La ricerca binaria cerca un modo per avvicinarsi alla soluzione mantenendo vero l’invariante

30 Ricerca binaria 3: il funzionamento Valore cercato: n = 60 Punto intermedio m di i..j Spazio di ricerca Passo generico: dividiamo il sottovettore in due parti (quasi) uguali. Caso 3. Se il valore cercato n è > di quello nel punto intermedio, allora n si trova nella parte destra di v[i…j]. 3. La ricerca binaria cerca un modo per avvicinarsi alla soluzione mantenendo vero l’invariante

31 Ricerca binaria 4: la fine della computazione 4. Definiamo in quale momento la computazione si deve fermare Quando si sia trovato il valore nel punto intermedio di indice m, oppure …. Valore cercato (e trovato): n = 43 Punto intermedio di indice m Spazio di ricerca

32 Ricerca binaria 4: la fine della computazione 4. Definiamo in quale momento la computazione si deve fermare …. oppure quando il sottovettore cui limitiamo la ricerca sia ridotto al vettore vuoto (cioe’ al vettore v[i…j] con i>j) Valore cercato (e non trovato): n=23 n=23 dovrebbe essere qui in mezzo, tra 21 e 25: ma questo intervallo è vuoto. Il valore n non viene trovato

33 Ricerca binaria 5: l’inizio della computazione 5. Definiamo le condizioni iniziali per la ricerca binaria All’inizio, il segmento v[i…j] di vettore in cui cercare n e’ l’intero vettore n Spazio di ricerca iniziale: v[0..n-1]

34 Ricerca binaria: i dettagli della codifica dei dati Stabiliamo ora i dettagli della codifica del segmento di vettore V[i..j] che usiamo durante la ricerca. Il sottovettore V[i..j], a cui limitiamo la ricerca, è compreso tra le posizioni i e j incluse ij Il punto medio m ha indice: (i + j) diviso 2 Se i > j allora il sottovettore V[i..j] è vuoto Spazio di ricerca in un passo generico della computazione m

35 Ricerca binaria: l’implementazione Scriviamo ora una funzione C++ che implementa la ricerca binaria, usando pre- e post-condizioni e l’invariante come commenti. La funzione ottenuta è decisamente breve rispetto a tutta la discussione che è servita a presentarla.

36 Ricerca binaria: l’implementazione int binsearch (int n, int v[], int dim) // pre: la dim. di v e’ >= dim e v[0..dim-1] è ordinato // post: i tale che v[i] == n se ne esiste uno, altrimenti: dim { int i = 0, j = dim - 1, m; while (i <= j) //inv. se n in v[0..n-1] allora n in V[i..j] {m = (i+j)/2; if (v[m] == n) return m; else if (n < v[m]) j = m - 1; else /* (v[m] < n) */ i = m + 1;} return dim; // dim sta per “non trovato” }

37 4. Matrici in C++ Una matrice a due dimensioni viene vista come un vettore bidimensionale, o di vettori, ognuno dei quali rappresenta una riga della matrice; la dichiarazione di una matrice è simile a quella vettore, eccetto che richiede due dimensioni: double A[10][20]; // matrice 10righe x 20colonne di double A[i][j] = 7.23; // se 0  i < 10 e 0  j < 20 allora // scrive 7.23 come valore della // i-esima riga e j-esima colonna di A

38 Passaggio di un vettore bidimensionale a funzione Nei parametri formali di una funzione un vettore V di dimensione 1 può figurare come void f(int V[], int n){…} senza l’indicazione della lunghezza di V dentro int V[] : la lunghezza n del vettore è a sua volta un parametro, che può cambiare da una chiamata all’altra. Nel caso di una matrice A di due o più dimensioni, invece, le dimensioni debbono essere costanti indicate dentro A stesso, come segue: void g(double A [10][20] ) {…}

39 Passaggio di un vettore bidimensionale a funzione Questo tipo di dichiarazione è molto scomoda: una funzione g che stampa una matrice A di 10x20 elementi non può essere utilizzata per stampare una matrice B di 20x20 elementi, perchè double A[10][20] e double B[20][20] sono due tipi diversi. La seconda dimensione di A, ovvero il numero 20 delle colonne di A, è purtroppo necessaria per ricostruire l’indirizzo della variabile A[i][j] nella macchina di Von Neumann, e non è ricostruibile a partire dalla sola variabile A. void g(double A [10][20] ) {…}

40 Un esempio di uso di matrici: il triangolo di Tartaglia Niccolò Tartaglia Scriveremo ora una funzione che assegna le prime n+1 righe del triangolo diTartaglia a una matrice (n+1)x(n+1). Per definizione, ogni elemento posto ai lati del triangolo di Tartaglia vale 1, e ogni altro elemento è la somma dell’elemento posto sopra e di quello posto sopra e a destra.

41 Un esempio di uso di matrici: il triangolo di Tartaglia Righe da 0 a 6 del “Triangolo” in una matrice A di 7x7. Per definizione: 1 = A[0][0] = A[1,0] = A[2][0] = … =A[i,0] = … e 1 = A[0][0] = A[1,1] = A[2][2] = … = A[i,i] = … e per 0

42 Il triangolo di Tartaglia usando una matrice void Tartaglia(int n) // stampa righe da 0 a n del triangolo di Tartaglia {int a[n+1][n+1]; //definisce matrice (n+1)x(n+1) for (int i = 0; i<=n; ++i) // costruzione “riga per riga” {a[i][0] = 1; // assegna 1 a tutta la colonna 0 for (int j = 1; j

43 Stampa del triangolo di Tartaglia void Tartaglia(int n) { // definisce righe da 0 a n … (vedi pagina precedente per sapere come) … // stampa righe da 0 a n for (int i = 0; i <= n; ++i) { for (int j = 0; j <= i; ++j) cout << setw(4) << a[i][j]; cout << endl;} } L’istruzione setw(4) esegue la prossima stampa con almeno 4 spazi. Richiede di includere la libreria: #include

44 5. I records (cenni) Un record è una tupla di valori di tipo possibilmente diverso (è questa la differenza con i vettori) a cui accediamo attraverso etichette anziché indici: struct { ;... ;} Come concetto matematico, un record corrisponde a un prodotto cartesiano: tipo 1 x … x tipo n mentre un vettore corrisponde a un insieme potenza: tipo n

45 I record nella memoria I record nella memoria di una macchina di Von Neumann sono rappresentati con celle adiacenti, ma di diversa dimensione. E’ necessario individuare ogni cella assegnadole un nome: Nome_1 Nome_k … Possiamo rappresentare i razionali come frazioni, e le frazioni come un record di due campi: numeratore e denominatore. Si tratta solo di un esempio: i razionali non sono una struttura dati abbastanza complessa da giustificare l’uso dei records.

46 Frazioni rappresentate da un record struct Ratio { int num; int den;}; Poiché qui i due campi hanno lo stesso tipo avremmo potuto scrivere: int num, den; Ratio si aggiunge ai tipi definiti nel programma Se r e’ un record che rappresenta una frazione, indichiamo numeratore e denominatore di r con r.num e r.den

47 Record come valori di funzioni Diversamente dagli array, le struct in C++ sono passate (e restituite) per valore: dunque ogni chiamata costruisce una copia del record e la assegna al parametro formale della funzione Ratio NewRatio(int n, int d) /* Pre-cond.: d!=0 Post-cond.:NewRatio(n,d) restituisce il record che rappresenta n/d */ { Ratio r; r.num = n; r.den = d; return r;} int main() { Ratio a = NewRatio(2,3); // a=2/3 }

48 Record come valori di funzioni Un altro esempio, la somma di frazioni. Le strutture in C++ sono passate per valore, creando delle copie dei valori passati. Ratio SumRatio(Ratio a, Ratio b) // post: restituisce il razionale a + b { Ratio r; r.num = a.num * b.den + a.den * a.num; r.den = a.den * b.den; return r;} int main() { Ratio a, b; …; Ratio c = SumRatio(a,b);} // c = a + b

49 Vettori parzialmente riempiti Di un vettore occorre ricordare la dimensione; se poi se ne usa solo una parte, come abbiamo fatto nell’esercizio sui numeri primi, bisogna sapere sin dove è “riempito”, ovvero, quale è il prossimo indirizzo libero. prox_libero v Se non ci sono indirizzi liberi, il prossimo indirizzo libero per definizione è la dimensione dim del vettore v.

50 Vettori parzialmente riempiti come record Possiamo definire un record per rappresentare vettori parzialmente riempiti. Usiamo un record con due campi: un vettore e il primo indirizzo ancora libero del vettore (se esiste) struct Array { int v[10]; int prox_libero; }; /* prox_libero = indice del prossimo indirizzo libero */

51 Funzioni su vettori parzialmente riempiti void Mostra(Array a) /*post: stampa la parte riempita di v, e cioé: (a.v)[0..a.prox_libera-1] */ { for (int i = 0; i < a.prox_libero; i++) cout << a.v[i] << " "; cout << endl;} Un esempio di una funzione che agisce sui vettori parzialmente riempiti: la stampa di tutti gli elementi del vettore effettivamente in uso (dunque fino alla prossima posizione libera esclusa).

52 Funzioni su vettori parzialmente riempiti bool Aggiungi(int n, Array& a) /* post: se a.prox_libera < 10, aggiunge n in ultima pos. e restituisce true; restituisce false altrimenti */ {if (a.prox_libero < 10) {a.v[a.prox_libero] = n; a.prox_libero++; return true;} else return false;} Un altro esempio di una funzione che agisce sui vettori parzialmente riempiti: come aggiungere un elemento a quelli effettivamente in uso

53 Riepilogo Vi sono due strutture dati predefinite per rappresentare collezioni finite di valori: vettori (array) e record (struct) I vettori hanno dimensione fissa, elementi omogenei, e sono passati per riferimento Vettori a due dimensioni rappresentano una matrice. I record hanno un numero fisso di campi individuati da nomi, hanno tipi eventualmente diversi di valori, e sono passati per valore.


Scaricare ppt "Strutture dati elementari Parte 6 Vettori e ricerca binaria Matrici e triangolo di Tartaglia Records (cenni) Corso A: Prof. Stefano Berardi"

Presentazioni simili


Annunci Google