dispense

Transcript

dispense
1
ISTITUTO TECNICO E LICEO SCIENTIFICO TECNOLOGICO “ANGIOY”
LE STRUTTURE DATI
PARTE 1: ARRAY
Prof. G. Ciaschetti
Una struttura dati è una collezione di dati in memoria alla quale è possibile dare un unico nome.
Ad esempio, la classe IV C rappresenta una collezione di studenti, ed è individuata da un unico
nome, appunto IV C. Oppure, ad esempio, la mia pagella è la collezione dei miei voti, che si chiama
Pagella di Gianfranco Ciaschetti.
Un‟array è una struttura dati omogenea e lineare. Omogenea significa che i dati che essa contiene
sono tutti dello stesso tipo (o tutti interi, o tutte stringhe, o tutti float, ecc.). Lineare significa che i
dati della collezione sono memorizzati in modo contiguo in memoria, uno dietro l‟altro.
Gli array possono avere una o più dimensioni. Un array monodimensionale (a una dimensione) si
chiama un vettore. Un array bidimensionale (a due dimensioni) si chiama una matrice. Un array
che ha 3 o più dimensioni non prende nomi particolari, si chiama genericamente array.
1. Vettori
Come abbiamo detto, un vettore è un array monodimensionale. Esso può essere definito in
linguaggio C nel seguente modo:
tipo nome[dimensione];
dove tipo indica il tipo degli elementi della collezione, nome è l‟identificatore che diamo alla
nostra variabile array, e dimensione è il numero di elementi.
Ad esempio, con la dichiarazione
int vet[10];
stiamo dichiarando una variabile array che si chiama vet e che contiene 10 elementi di tipo intero.
Ad esempio, con la dichiarazione
float pippo[30];
stiamo dichiarando una variabile array che si chiama pippo e che contiene 30 elementi di tipo float.
Ad esempio, con la dichiarazione
char titolo[100];
stiamo dichiarando una variabile array che si chiama titolo e che contiene 100 elementi di tipo char
(ci ricorda qualcosa?).
Un array è utile ogni qual volta dobbiamo memorizzare parecchi dati, tutti dello stesso tipo, e
troviamo sconveniente usare una variabile per ognuno di essi. Inoltre, se non sappiamo a priori
quanti dati effettivamente dovremo memorizzare (ad esempio, i voti in informatica di una classe: ne
abbiamo un diverso numero per ogni diversa classe, essendo diverso il numero degli studenti)
2
l‟unica possibilità che abbiamo è quella di usare un array che ha un numero di elementi
sufficientemente grande per ogni possibile situazione. Tornando all‟esempio dei voti di informatica
di una classe, potremmo usare un vettore
float voti[30];
sapendo che in ogni classe non ci sono più di 30 alunni. In questo modo, se in una classe abbiamo,
ad esempio, 21 alunni, useremo solo le prime 21 posizioni del nostro vettore, lasciando le 9
rimanenti inutilizzate.
E‟ possibile accedere ai singoli elementi del vettore specificando il nome del vettore e la posizione
dell‟elemento che ci interessa, tra parentesi quadre. Nell‟esempio dei voti, il primo elemento
dell‟array è individuato con voti[0] (si legge voti di zero), il secondo elemento con voti[1], e così
via, fino al trentesimo che è individuato con voti[29].
ATTENZIONE: Stiamo usando il vettore voti come esempio. Il nome del vettore, il tipo degli
elementi e il loro numero saranno scelti di volta in volta a seconda della particolare esigenza!
IMPORTANTE: In ogni istruzione del nostro programma, non è possibile mai fare operazioni
sull‟intero array, ma solo su un singolo elemento alla volta. Considerando l‟esempio dei voti, sono
istruzioni errate, ad esempio
printf(“%d”, voti); /* ERRORE */
voti = 3;
/* ERRORE */
in quanto nella printf si chiede di visualizzare sul monitor un intero, e voti non è un dato di tipo
intero, ma una collezione di interi! Allo stesso modo, nell‟istruzione di assegnamento il valore 3 è
un valore di tipo intero, e quindi può essere assegnato a una variabile di tipo intero, non a una
collezione di interi. Sono invece operazioni lecite, ad esempio, le seguenti:
printf(“%d”, voti[1]); /* visualizza il secondo elemento del vettore voti */
voti[0] = 3; /* assegna il valore 3 al primo elemento del vettore voti */
1.1 Caricamento di un vettore
Caricare un vettore significa farsi dare in input, uno per uno, gli elementi del vettore, per
memorizzarli nella RAM. Riprendendo la nostra discussione sul numero di elementi del vettore, che
deve essere sufficientemente grande per ogni possibile situazione, possiamo innanzitutto chiedere in
input quanti elementi andremo effettivamente a caricare, diciamo N, e successivamente chiedere in
input tutti gli elementi voti[0], voti[1], …, voti[N-1]. Il codice potrebbe essere il seguente:
int voti[30]; /* alloca in memoria uno spazio per 30 elementi di tipo intero */
int N;
/* indica il numero di elementi effettivamente caricati */
printf(“quanti elementi vuoi inserire? ”);
scanf(“%d”, &N);
/* qui inizia il caricamento delle prime N posizioni del vettore */
scanf(“%d”, &voti[0]);
scanf(“%d”, &voti[1]);
…
scanf(“%d”, &voti[N-1]);
3
Ora, non sapendo a priori quale sarà il numero N inserito a run time (durante l‟esecuzione del
programma), non sappiamo quante di queste scanf scrivere. Inoltre, ripetere tante volte la stessa
istruzione risulta piuttosto noioso: infatti esse sono tutte uguali, tranne la posizione dell‟elemento da
inserire.
Una soluzione al nostro problema consiste nell‟uso di un contatore e nel porre l‟istruzione scanf
all‟interno di un ciclo in cui si fa variare il contatore, facendogli assumere tutti i valori da 0 a N-1.
Tutte le istruzioni scanf del codice precedente possono allora essere sostituite dal seguente ciclo:
int i = 0;
while(i<N)
{
scanf(“%d”, &voti[i]);
i++;
}
In questo modo, qualunque sia il valore inserito N, si ripeterà N volte l‟istruzione scanf, con tutte le
posizioni da 0 a N-1.
Quando il numero di iterazioni da compiere è noto a priori, come in questo caso, al posto del ciclo
while è preferibile usare un ciclo for. Esso non è altro che un modo più sintetico di esprimere lo
stesso ciclo. Possiamo ottenere lo stesso effetto del codice qui sopra scrivendo
for(i=0; i<N; i++)
scanf(“%d”, &voti[i]);
Il significato dell‟istruzione for su descritta è il seguente: all‟inizio poni i=0. Poi se la condizione
i<N è vera, esegui la scanf, incrementa i di 1, e torna a valutare la condizione. E‟ esattamente come
il ciclo while, solo scritto in forma più sintetica!
Riassumendo, possiamo allora scrivere il caricamento del nostro vettore come segue:
int voti[30];
int i,N;
void Caricamento()
{
printf(“quanti elementi vuoi inserire? ”);
scanf(“%d”, &N);
for(i=0; i<N; i++)
scanf(“%d”, &voti[i]);
}
1.2 Visualizzazione di un vettore
Visualizzare un vettore equivale a mostrare a video, uno per uno, tutti gli elementi memorizzati nel
vettore. Si suppone quindi di conoscere già il numero N di elementi presenti, e di aver
precedentemente caricato il vettore. Il codice è pressappoco lo stesso del caricamento: dobbiamo
scrivere le N istruzioni
4
printf(“%d”, voti[0]);
printf (“%d”, voti[1]);
…
printf (“%d”, voti[N-1]);
quindi, di nuovo, utilizziamo un ciclo for come segue:
void Visualizzazione()
{
for(i=0; i<N; i++)
printf(“%d”, voti[i]);
}
1.2 Ricerca lineare
Per ricerca lineare si intende andare a cercare se è presente un particolare elemento nel vettore. Si
chiama lineare perché prevede di scandire tutto il vettore, posizione per posizione, per trovare
l‟elemento cercato. Si tratta allora di chiedere in input l‟elemento da cercare, salvandolo in una
variabile (dello stesso tipo degli elementi del vettore), e controllare se ogni elemento del vettore è
uguale a quello cercato. Più o meno, nell‟esempio dei voti, qualcosa del genere:
int v;
printf(“elemento da cercare? ”);
scanf(“%d”, &v);
for(i=0; i<N; i++)
if(voti[i] == v)
printf(“elemento presente nel vettore”);
Ora però, se volessimo rispondere anche nel caso di ricerca infruttuosa, ad esempio scrivere
“elemento non presente nel vettore”, dobbiamo modificare un po‟ le nostre istruzioni. Un errore che
si fa comunemente è quello di mettere un else all‟istruzione if, nel seguente modo:
int v;
printf(“elemento da cercare? ”);
scanf(“%d”, &v);
for(i=0; i<N; i++)
if(voti[i] == v)
printf(“elemento presente nel vettore”);
else
printf(“elemento non presente nel vettore”);
Chiaramente questo codice è sbagliato: all‟inizio i assume il valore 0, quindi si controlla se voti[0] è
uguale a v. Se si, ok, nessun problema, abbiamo trovato il nostro elemento. Se invece sono diversi,
non possiamo ancora rispondere che l‟elemento non è presente: dobbiamo prima controllare tutti gli
altri elementi del vettore! Quindi potremo dare una tale risposta solo alla fine del nostro ciclo, e non
al suo interno.
La soluzione prevede di utilizzare una variabile flag (bandiera, in inglese), cioè che assume valori 0
e 1 (quindi di tipo intero), per indicare se abbiamo trovato il nostro elemento oppure no. All‟inizio,
5
poniamo a 0 il nostro flag (bandiera abbassata), per indicare che non abbiamo ancora trovato
l‟elemento cercato. Quando accade che lo troviamo, poniamo a 1 il flag (bandiera alzata). Al
termine del ciclo, se per una qualche posizione è successo di trovare il nostro elemento, il flag avrà
valore 1, altrimenti il suo valore sarà rimasto pari a 0. Quindi, basta chiedersi quanto vale il flag al
termine del ciclo. Chiamiamo il nostro flag, per convenienza, con il nome trovato.
void RicercaLineare()
{
int trovato = 0;
int v;
printf(“elemento da cercare? ”);
scanf(“%d”, &v);
for(i=0; i<N; i++)
if(voti[i] == v)
trovato = 1;
if (trovato == 1)
/* al termine del ciclo */
printf(“elemento presente nel vettore”);
else
printf(“elemento non presente nel vettore”);
}
Una interessante variante è quella in cui si chiede anche la posizione nel vettore dell‟elemento
trovato. Per far questo, si può procedere in due modi: o si mette la printf(“presente”) dentro al ciclo
for, in modo da visualizzare il valore di i quando viene trovato l‟elemento, oppure si salva il valore
di i in un‟altra variabile, per poi utilizzarla fuori dal ciclo. Di seguito, sono riportate entrambe le
soluzioni
void RicercaLineareVariante1()
{
int trovato = 0;
int v;
printf(“elemento da cercare? ”);
scanf(“%d”, &v);
for(i=0; i<N; i++)
if(voti[i] == v)
{
printf(“elemento presente nel vettore in posizione %d”, i);
trovato = 1;
}
if (trovato == 0)
/* al termine del ciclo */
printf(“elemento non presente nel vettore”);
}
6
void RicercaLineareVariante2()
{
int trovato = 0;
int v,s;
printf(“elemento da cercare? ”);
scanf(“%d”, &v);
for(i=0; i<N; i++)
if(voti[i] == v)
{
s = i;
trovato = 1;
}
if (trovato == 1)
/* al termine del ciclo */
printf(“elemento presente nel vettore in posizione %d”, s);
else
printf(“elemento non presente nel vettore”);
}
Un‟altra interessante variante è quella che chiede di contare quanti elementi ci sono nel vettore
uguali a un dato elemento. In questo caso, basta far assumere alla variabile trovato anche altri
valori, e utilizzarla come contatore degli elementi uguali, nel seguente modo:
void ContaElementiUguali()
{
int trovato = 0;
int v;
printf(“elemento da cercare? ”);
scanf(“%d”, &v);
for(i=0; i<N; i++)
if(voti[i] == v)
trovato++;
printf(“sono stati trovati %d elementi nel vettore”, trovato);
}
Una ulteriore variante della ricerca lineare è quella in cui chiediamo di uscire dal ciclo non appena
abbiamo trovato l‟elemento cercato, evitando così di effettuare ulteriori confronti. In questo caso,
possiamo modificare l‟istruzione for del nostro codice come segue
void RicercaLineare()
{
…
for(i=0; (i<N)&&(trovato==0); i++)
…
}
Non appena la variabile trovato diventerà uguale a 1, il ciclo non verrà più ripetuto.
7
ESERCIZI: dopo aver caricato un vettore di (massimo) 50 interi,
 contare quanti elementi pari e quanti elementi dispari ci sono nel vettore
 visualizzare tutti gli elementi maggiori di 10
 trovare l‟elemento minimo
1.3 Shift a sinistra
To shift in inglese significa spostare. In questo contesto, sta a significare che vogliamo ruotare il
vettore spostando tutti gli elementi di una posizione a sinistra, rimettendo in ultima posizione il
primo elemento, come nel seguente esempio:
Per effettuare la rotazione a sinistra, bisogna innanzitutto salvare in un‟altra variabile il primo
elemento del vettore,
v = voti[0];
quindi assegnare, a partire da sinsitra, a ogni posizione l‟elemento in posizione successiva
voti[0]= voti[1];
vet[1]= voti[2];
…
voti[N-2]= voti[N-1]; /* ricordiamo che l’N-esimo elemento è in posizione N-1 */
e alla fine rimettere in posizione N-1 il primo elemento che avevamo salvato nella variabile v
voti[N-1]= v;
Di nuovo, ci viene in aiuto il ciclo for: possiamo riscrivere con un ciclo la sequenza degli
spostamenti e ottenere lo stesso
for(i=0; i<N-1; i++)
voti[i] = voti[i+1];
/* per tutti i valori di i da 0 a N-2 */
Il codice per la rotazione a sinistra è allora il seguente:
8
void LeftShift()
{
int v;
v = voti[0];
for(i=0; i<N-1; i++) /* per tutti i valori di i da 0 a N-2 */
voti[i] = voti[i+1];
voti[N-1]= v;
/* al termine del ciclo */
}
1.4 Shift a destra
Stavolta si tratta di far ruotare il vettore a destra, ossia spostare tutti gli elementi a destra di una
posizione, come nell‟esempio seguente:
Allo stesso modo della rotazione a sinistra, ora dobbiamo salvare in una variabile l‟ultimo elemento
del vettore.
v = voti[N-1];
quindi assegnare, a partire da destra, a ogni posizione l‟elemento in posizione precedente
voti[N-1]= voti[N-2]; /* ricordiamo che l’N-esimo elemento è in posizione N-1 */
…
voti[2]= voti[1];
voti[1]= voti[0];
e alla fine rimettere in posizione 0 l‟ultimo elemento che avevamo salvato nella variabile v
voti[0]= v;
Di nuovo, ci viene in aiuto il ciclo for, ma stavolta a ogni iterazione del ciclo dobbiamo
decrementare il nostro contatore i, anziché aumentarlo
for(i=N-1; i>0; i--)
voti[i] = voti[i-1];
/* per tutti i valori di i da N-1 a 1 */
Il codice per la rotazione a destra è allora il seguente:
9
void RightShift()
{
int v;
v = voti[0];
for(i=N-1; i>0; i--)
voti[i] = voti[i-1];
voti[0]= v;
}
/* per tutti i valori di i da N-1 a 1 */
ESERCIZIO: simulare una scritta che corre sullo schermo, ripetendo più volte la rotazione di un
vettore di caratteri.
1.4 Ordinamento di un vettore
Ordinare (to sort, in inglese) un vettore significa disporre i suoi elementi in ordine crescente o
decrescente. Ad esempio,
ordine crecente
ordine decrescente
Esistono diversi algoritmi di ordinamento di un vettore, tra i quali citiamo





Selection sort (ordinamento per selezione)
Bubble sort (ordinamento a bolle)
Insertion sort (ordinamento per inserimento – come con le carte a Scala40)
Quick sort (ordinamento veloce)
Merge sort (ordinamento per fusione)
Noi vedremo solo i primi due tra questi, decisamente più semplici degli altri. Tuttavia, dobbiamo
evidenziare il fatto che essi non sono i più veloci, che invece risultano essere il quick sort e il merge
sort. Torneremo su questo aspetto tra poco.
10
SELECTION SORT
Supponiamo di voler ordinare il vettore in ordine crescente. Questo algoritmo di ordinamento si
sviluppa in più fasi: in una prima fase prevede di selezionare il più piccolo elemento del vettore e
portarlo in prima posizione. Quindi, in una seconda fase, selezionare il secondo elemento più
piccolo e portarlo in seconda posizione, e così via.
Concentriamoci, per il momento, sulla prima fase. Si tratta di confrontare l‟elemento in prima
posizione con il secondo, con il terzo, e così via fino all‟N-esimo, ed eventualmente scambiarlo nel
caso in cui si trovi un elemento minore. Ad esempio,
Ricordando la funzione per scambiare due variabili (passaggio di parametri per indirizzo, e uso di
una variabile di appoggio, giusto?)
void Scambia(int *x, int *y)
{
int temp;
temp = *x;
*x = *y;
*y = temp;
}
Possiamo scrivere le istruzioni per questa prima fase nel seguente modo:
for(j=1; j<N; j++) /* per tutti i valori di j da 1 a N-1 */
if (voti[j] < voti[0])
Scambia(&voti[j], &voti[0]);
Passiamo ora alla seconda fase. Si tratta di ripetere, più o meno, lo stesso ciclo di sopra, ma stavolta
confrontando l‟elemento in seconda posizione con tutti i suoi successivi (il primo è stato già
sistemato!), per portare in seconda posizione il secondo elemento più piccolo.
11
Stavolta, le istruzioni che effettuano la seconda fase diventano
for(j=2; j<N; j++) /* per tutti i valori di j da 2 a N-1 */
if (voti[j] < voti[1])
Scambia(&voti[j], &voti[1]);
Dunque, si tratta di ripetere questo ciclo for per tutte le posizioni, ogni volta facendo partire
l‟elemento da confrontare dalla posizione successiva a quella “da aggiustare”. Generalizzando,
poiché si tratta di ripetere tante volte lo stesso ciclo for, lo inseriamo dentro un altro ciclo for in cui
usiamo un indice i per indicare la posizione dove stiamo mettendo l‟elemento più piccolo, che nella
prima fase sarà pari a 0, nella seconda pari a 1, e così via, quindi facciamo partire ogni volta il
nostro indice j dalla posizione successiva a i. Si noti che l‟ultimo confronto va fatto tra l‟elemento
in posizione N-2 e quello in posizione N-1, per cui l‟indice i assumerà come ultimo valore N-2.
void SelectionSort()
{
int i, j;
for(i=0; i<N-1; i++) /* per tutti i valori di i da 0 a N-2 */
for(j=i+1; j<N; j++) /* per tutti i valori di j da i+1 a N-1 */
if (voti[j] < voti[i])
Scambia(&voti[j], &voti[i]);
}
Se invece dobbiamo ordinare il vettore in modo decrescente, basta cambiare il verso della
disuguaglianza nell‟istruzione if, in modo da scambiare gli elementi quando ne troviamo uno più
grande.
BUBBLE SORT
Il nome di questo algoritmo (a bolle, in italiano) deriva dall‟osservazione che le bolle d‟aria in
acqua tendono a salire in superficie. Anche questo algoritmo di ordinamento si sviluppa in più fasi:
una prima fase prevede di individuare (sempre nell‟ipotesi di ordinare in modo crescente) il più
grande elemento del vettore, la cosiddetta bolla, e portarlo in ultima posizione, la superficie. Quindi,
in una seconda fase, si individua il secondo elemento più grande (la seconda bolla) e la si porta in
12
penultima posizione (la superficie ora è questa, visto che l‟ultimo elemento è già stato “messo a
posto”), e così via.
Concentriamoci, per il momento, sulla prima fase. Si tratta di confrontare tra loro il primo e il
secondo elemento, e trovare la bolla tra questi. Se la bolla (il più grande) è il primo, li scambiamo
per far salire la bolla in superficie, altrimenti sono già “a posto” tra loro. Successivamente,
confrontiamo il secondo elemento con il terzo, e portiamo il più grande, la bolla, verso la superficie:
se il secondo è il maggiore, li scambiamo, altrimenti stanno bene così. Continuiamo a confrontare il
terzo elemento con il quarto, il quarto con il quinto, e così via. Alla fine di questa fase, avremo
l‟elemento più grande del vettore in ultima posizione, come nell‟esempio seguente:
Possiamo scrivere le istruzioni per questa prima fase nel seguente modo:
for(i=0; i<N-1; i++) /* per tutti i valori di i da 0 a N-2 */
if (voti[i] > voti[i+1])
Scambia(&voti[i], &voti[i+1]);
Passiamo ora alla seconda fase. Si ricomincia a individuare la bolla e farla salire fino alla penultima
posizione, un po‟ come abbiamo fatto nella prima fase.
13
Stavolta, le istruzioni per la seconda fase sono
for(i=0; i<N-2; i++) /* per tutti i valori di i da 0 a N-3 */
if (voti[i] > voti[i+1])
Scambia(&voti[i], &voti[i+1]);
Il codice per la seconda fase è esattamente uguale a quello della prima fase, con la differenza che
dobbiamo fermarci una posizione prima. La terza fase è di nuovo uguale, fermandoci con i a N-4, e
così via, sistemando ogni volta la bolla in superficie. Nell‟ultima fase, si ha un solo confronto da
fare tra l‟elemento in posizione 0 e l‟elemento in posizione 1. Pertanto, i dovrà partire da 0 e
arrivare a 0. Poiché dobbiamo ripetere lo stesso ciclo un certo numero di volte, lo includiamo in un
altro ciclo for. Anche questo algoritmo, come il precedente, presenta due cicli for annidati, cioè
uno dentro l‟altro. Nel ciclo più esterno, facciamo crescere una variabile k che useremo per
decrementare le posizioni dove andranno a mettersi le bolle, che dovà essere N-1 la prima volta, N2 la seconda, N-3 la terza, e così via. In generale, scriveremo N-k, con k che parte da 1 e arriva a un
valore tale che N-k=0.
void BubbleSort()
{
int i, k;
for(k=1; k<N; k++) /* per tutti i valori di k da 1 a N-1 */
for(i=0; i<N-k; i++) /* per tutti i valori di i da 0 a N-k-1 */
if (voti[i] > voti[i+1])
Scambia(&voti[i], &voti[i+1]);
}
Complessità computazionale degli algoritmi di ordinamento
I due algoritmi che abbiamo visto, quello per selezione e quello a bolle, compiono un numero di
operazioni (confronti, più che altro) che è di circa N volte N, nel caso peggiore, per un vettore di N
elementi. Infatti, bisogna ripetere circa N volte il ciclo più esterno, che contiene un ciclo ripetuto
circa N volte. Si dice allora che la complessità computazionale di questi due algoritmi è dell‟ordine
di N2, e si scrive O(N2).
Come abbiamo già anticipato, questi non sono i più veloci algoritmi di ordinamento. In particolare,
il quicksort e il mergesort compiono un numero minore di operazioni. Il quicksort è una funzione
ricorsiva che ripete la divisione del vettore in due parti, una con elementi minori e una con elementi
maggiori di un dato numero. La ricorsione consta di due fasi: la separazione e la ricostruzione. In
questo algoritmo, la prima è più lenta della seconda, dovendo effettuare una selezione degli
elementi in fase di divisione dell‟array. Anche il mergesort è una funzione ricorsiva che divide
ricorsivamente l‟array a metà. Quest ultima, però, divide senza selezionare gli elementi, quindi più
velocemente del quicksort, per poi impiegare di più per riordinare (fondere) i pezzettini di array.
Entrambi questi due algoritmi effettuano un numero di operazioni che è di O(Nlog2N), che è molto
meno di O(N2), al crescere di N. [con N=64, ad esempio, N2 = 64 * 64 = 4096 mentre Nlog2N = 64
* log264 = 64*6 = 384].
La ragione di questa maggior velocità sta proprio nel fatto di dividere l‟array: per ogni divisione,
occorre fare circa N operazioni, ma quante sono le divisioni da fare? Il seguente esempio mostra la
situazione: se abbiamo un vettore con, supponiamo, 64 elementi, dopo la prima divisione ne avremo
14
due con 32, dopo la seconda divisione avremo 4 vettori con 16 elementi, dopo la terza divisione 8
vettori con 8 elementi, dopo la quarta 16 vettori con 4 elementi, dopo la quinta 32 vettori con 2
elementi, e dopo la sesta, infine, 64 vettori con un solo elemento, che sono banalmente già ordinati.
Abbiamo effettuato complessivamente 6 divisioni, ossia log264.
1.5 Ricerca binaria
Ora che sappiamo come ordinare un vettore, possiamo introdurre un altro modo di effettuare la
ricerca di un elemento, molto più veloce della ricerca lineare, il quale però richiede che il vettore sia
ordinato! Ricordiamo che la ricerca lineare prevede di controllare tutti gli elementi del vettore, uno
per uno, quindi effettua circa N operazioni, cioè impiega un tempo pari a O(N).
La ricerca binaria procede un po‟ come quando dobbiamo cercare un nome nell‟elenco telefonico:
supponiamo di cercare il cognome Cau, e apriamo a caso (diciamo, circa a metà) l‟elenco;
supponiamo che abbiamo trovato la lettera F. Sapendo che i nomi sono ordinati, possiamo andare a
cercare Cau solo nella prima parte dell‟elenco (quella prima della lettera F), tralasciando
completamente la seconda parte (quella dopo la lettera F). Si procede quindi allo stesso modo,
considerando solo la prima parte stavolta. Si prende a caso una pagina; supponiamo che esce la
lettera B; essendo l‟elenco ordinato, sappiamo che dobbiamo cercare il nostro Cau solo nella
seconda parte (dopo la lettera B, ma prima della F) tralasciando la prima (prima della lettera B).
In pratica, ogni volta lo spazio di ricerca viene ridotto a metà. Se dobbiamo cercare tra N elementi,
dopo la prima “apertura” a caso il prolema diventa di cercare tra N/2 elementi. Dopo la seconda
“apertura” dobbiamo cercare tra N/4 elementi, e così via. Seguendo l‟esempio fatto alla fine del
paragrafo precedente, possiamo concludere che la ricerca binaria effettua un numero di operazioni
pari a circa log2N, quindi O(log2N), molto meno di O(N) impiegato dalla ricerca lineare.
Il principio della ricerca binaria è allora il seguente: dati un inizio e una fine del vettore, calcola una
posizione centrale, e confronta l‟elemento al centro con quello cercato. Se sono uguali, stop,
l'abbiamo trovato. Altrimenti, se l'elemento cercato è minore di quello al centro, ripetiamo la ricerca
a sinistra; se l'elemento cercato è maggore di quello al centro, ripetiamo la ricerca a destra. La
situazione è spiegata nella figura seguente.
15
Osserviamo, dalla figura precedente, che se non troviamo l'elemento cercato, finiamo con l'indice
fine a sinistra dell'indice inizio (mentre per tutta la durata della precedura è a destra). Scriviamo il
codice per la nostra ricerca binaria:
void RicercaBinaria()
{
int trovato = 0;
int v, inizio = 0, fine = N-1, centro;
printf(“elemento da cercare? ”);
scanf(“%d”, &v);
while((!trovato) && (inizio <= fine))
{
/* calcola il centro */
centro = (inizio + fine)/2;
if (voti[centro] == v)
trovato = 1;
else
if (voti[centro] < v)
inizio = centro + 1;
else
fine = centro -1;
}
if (trovato == 1) /* al termine del ciclo */
printf(“elemento presente nel vettore”);
else
printf(“elemento non presente nel vettore”);
}
Ovviamente, è possibile anche scrivere una versione ricorsiva, e non iterativa, della ricerca binaria:
basta osservare che ogni volta che non troviamo l‟elemento cercato in posizione centrale,
eseguiamo di nuovo (cioè richiamiamo la funzione) con nuovi valori degli indici inizio e fine.
int RicercaBinariaRicorsiva(int v, int inizio, int fine) /* v è l’elemento da cercare */
{
int centro;
if (inizio>fine)
return 0;
else
{
centro = (inizio+fine)/2;
if (voti[centro] == v)
return 1;
else
if (voti[centro] < v)
return RicercaBinaria(v, centro+1, fine);
else
return RicercaBinaria(v, inizio, centro-1);
}
}
16
La funzione può essere allora richiamata nel main come nel seguente esempio (si notino i valori
iniziali degli indici inizio e fine)
printf(“elemento da cercare? ”);
scanf(“%d”, &v);
if (RicercaBinaria(v, 0, N-1) == 0)
printf(“elemento non presente”);
else
printf(“elemento presente”);
1.6 Vettori e puntatori
Esiste una stretta relazione tra i vettori e i puntatori: il nome di un vettore in linguaggio C
rappresenta anche un puntatore al primo elemento del vettore. Ad esempio, se abbiamo il vettore
int vet[10];
il nome vet memorizza l'indirizzo di memoria del primo elemento dell'array. In altre parole, vet e
&vet[0] sono la stessa cosa! Allo stesso modo, risulta *vet = vet[0]. Si può anche utilizzare
l'aritmetica dei puntatori per accedere ai diversi elementi dell'array: risulta *(vet+1) = vet[1],
*(vet+2) = vet[2], e così via, essendo vet+1 = &vet[1], vet+2 = &vet[2], e così via.
Il fatto che il nome di un vettore è anche il puntatore al primo elemento del vettore stesso torna
molto utile allorchè si voglia passare un vettore come parametro a una funzione: anziché passare
l‟intera struttura dati, si passa in realtà l‟inidirizzo di memoria del vettore. Dunque, il passaggio di
un vettore come parametro avviene sempre per indirizzo!
1.7 Stringhe
Se ricordiamo come si definisce una stringa in linguaggio C, troviamo un‟analogia con i vettori. Ad
esempio la dichiarazione
char titolo[10];
dichiara un vettore che si chiama titolo e che contiene 10 elementi di tipo char. Non è esattamente
come dichiaravamo anche le nostre stringhe? Infatti, in C una stringa non è altro che un vettore di
caratteri! Con una sola particolarità: l‟ultimo carattere della stringa è aggiunto automaticamente dal
compilatore, ed è sempre il carattere „\0‟, detto carattere di fine stringa. Quindi, se ad esempio dopo
la precedente dichiarazione facciamo una
scanf(“%s”, titolo);
e in fase di esecuzione del programma digitiamo sulla tastiera, ad esempio, pippo, avremo che nel
nostro vettore saranno memorizzati i seguenti caratteri
17
mentre se eseguiamo, invece, una printf, il carattere di fine stringa non verrà visualizzato e
comparirà semplicemente la parola pippo.
Riprendendo il paragrafo precedente, si capisce anche perché quando si fa una scanf di una stringa
non c‟è bisogno di usare l‟operatore &, come invece dobbiamo fare per i dati interi o reali: il nome
della stringa è il nome di un vettore, e dunque contiene già l‟indirizzo di memoria dove inizia il
nostro vettore, senza doverlo trovare con l‟operatore &.
scanf(“%d”, &a); /* se a è di tipo intero, scanf richiede il suo indirizzo di memoria */
scanf(“%s”, a); /* se a è di tipo stringa, contiene già l’indirizzo di memoria */
2. Matrici
All‟inizio di questa dispensa abbiamo definito una matrice come un array bidimensionale. Essa può
essere definita in linguaggio C nel seguente modo:
tipo nome[dimensione_righe][dimensione_colonne];
dove tipo indica il tipo degli elementi della collezione, nome è l‟identificatore che diamo alla
nostra variabile array, dimensione_righe è il numero di righe della matrice, e
dimensione_colonne è il numero di colonne della matrice. Ad esempio, con la dichiarazione
int mat[10][10];
stiamo dichiarando una variabile array che si chiama mat e che contiene 100 elementi di tipo intero,
disposti su 10 righe e 10 colonne. Ad esempio, con la dichiarazione
float pippo[30][5];
stiamo dichiarando una variabile array che si chiama pippo e che contiene 150 elementi di tipo
float, disposti su 30 righe e 5 colonne.
Anche per le matrici, per scrivere un programma che sia generale, nel momento della dichiarazione
indichiamo quante righe e quante colonne potrà avere al massimo la nostra matrice, ma non è detto
che le useremo tutte. Useremo quindi due variabili, diciamo N e M, per indicare il numero di righe e
il numero di colonne effettivamente utilizzate.
Per lavorare con una matrice, a differenza di un vettore, dobbiamo usare due indici: un indice che
rappresenta la posizione di riga, diciamo i, e un indice che rappresenta la posizione di colonna,
diciamo j, un po‟ come nella griglia della battaglia navale. Ad esempio, nella matrice mat
dichiarata sopra, l‟elemento di riga 0 e colonna 0 si chiamerà mat[0][0], l‟elemento di riga 0 e
colonna 1 si chiamerà mat[0][1], ecc. In generale, l‟elemento di riga i e colonna j sarà individuato
con mat[i][j]. Se ci sono N righe ed M colonne, allora ci saranno elementi nelle righe da 0 a N-1, ed
elementi nelle colonne da 0 a M-1.
18
j
i
Prestando particolare attenzione alla dichiarazione di una matrice, possiamo osservare che, ad
esempio, la dichiarazione
int mat[10][10];
può essere vista come la dichiarazione di un vettore di 10 elementi, ognuno dei quali è un vettore di
10 interi. E infatti, una matrice non è altro che un vettore di vettori. Abbiamo allora che gli elementi
di una matrice sono memorizzati in modo contiguo: prima tutti quelli della prima riga (il vettore
mat[0]), poi tutti quelli della seconda riga (il vettore mat[1]), ecc.
Prima di passare alle operazioni, un po‟ di definizioni:






una matrice si dice quadrata se N = M (stesso numero di righe e di colonne. In questo caso
basta una sola variabile per memorizzare entrambe).
una matrice quadrata si dice diagonale se mat[i][j] = 0 ogni volta che i ≠ j (contiene
elementi non nulli solo nella diagonale principale, ossia quando i = j).
una matrice quadrata si dice triangolare inferiore se mat[i][j] = 0 ogni volta che i<j
(contiene elementi non nulli solo al di sotto della diagonale principale).
una matrice quadrata si dice triangolare superiore se mat[i][j] = 0 ogni volta che i>j
(contiene elementi non nulli solo al di sopra della diagonale principale).
la matrice trasposta di una matrice si ottiene invertendo le righe con le colonne.
una matrice si dice simmetrica se è uguale alla sua matrice trasposta, ossia se mat[i][j] =
mat[j][i] ogni volta che i ≠ j (la parte triangolare superiore e la parte triangolare inferiore si
riflettono come in uno specchio rispetto alla diagonale principale).
Il caricamento, la visualizzazione, la ricerca e altre operazioni sulle matrici si fanno come nel caso
di un vettore, considerando però che dobbiamo spostarci stavolta lungo le due dimensioni, sulle
righe e sulle colonne.
Per caricare una matrice, ad esempio, occorre farsi dare in input tutti gli elementi della prima riga
(in pratica, il caricamento di un vettore), poi tutti gli elementi della seconda riga, ecc. Si tratta
quindi di ripetere, per ogni riga, il caricamento di un vettore. Di conseguenza, si dovrà avere due
19
cicli for annidati uno dentro l‟altro, uno per scorrere tutte le righe, e l‟altro per scorrere, per ogni
riga, tutte le colonne. Ad esempio, con la matrice mat definita sopra
CARICAMENTO DI UNA MATRICE
printf(“quante righe? ”);
scanf(“%d”, &N);
printf(“quante colonne? ”);
scanf(“%d”, &M);
for(i=0; i<N; i++) /* per tutti i valori di i da 0 a N-1 cioè per tutte le righe*/
for(j=0; j<M; j++) /* per tutti i valori di j da 0 a M-1 cioè per tutte le colonne*/
scanf(“%d”, &mat[i][j]);
Allo stesso modo, ogni volta che dovremo scorrere tutti gli elementi della matrice dovremo usare
due cicli for annidati (visualizzazione, ricerca). A differenza dei vettori, nelle matrici non esiste un
concetto di ordinamento e la possibilità di effettuare una ricerca binaria.
Si noti, infine, che è possibile anche caricare la matrice “per colonne”, anziché per righe,
semplicemente scambiando l‟ordine dei due cicli for.
ESERCIZI:
- dopo aver caricato una matrice di (massimo) 50 righe e 50 colonne,
 visualizzare tutti gli elementi della matrice per colonne
 visualizzare tutti gli elementi maggiori di 10
 trovare l‟elemento minimo
 dire se la matrice è diagonale, triangolare superiore o triangolare inferiore
 trovare la somma degli elementi di ogni riga della matrice
- creare la tavola pitagorica
3. Array con più di due dimensioni
Si possono utilizzare array con più di due dimensioni, a patto di tenere a mente che serve un indice
per ognuna delle dimensioni. Ad esempio, la seguente dichiarazione
int cubo[10][10][10];
dichiara un array tridimensionale che si chiama cubo e che contiene 1000 interi, disposti su 10 righe
10 colonne e 10 caselle in profondità (si può vedere un‟array tridimensionale come una matrice in
cui ogni elemento è un vettore). Un elemento di questo array sarà individuato stavolta specificando
la posizione per ognuna delle tre dimensioni, come ad esempio cubo[0][0][2]. Per scorrere tutto il
nostro array tridimensionale avremo bisogno allora di tre indici, diciamo i, j e k, uno per ogni
diversa dimensione.
20
Allo stesso modo, è possibile dichiarare un array con 4, 5, 6 dimensioni, ecc. Ad esempio,
int S[2][4][10][5];
dichiara un array che si chiama S, che ha 4 dimensioni, e che contiene 2*4*10*5 = 300 elementi di
tipo intero. E‟ evidente che una simile struttura (così come ogni altro array con più di tre
dimensioni) non ha nessun riscontro geometrico nella realtà, ma è possibile comunque utilizzarla in
informatica. L‟importante, ricordiamo, è di specificare sempre un diverso indice per ogni diversa
dimensione.