46
6. Array e Algoritmi sugli Array
Oltre ai tipi semplici, i linguaggi di programmazione offrono modi per aggregare dati.
Gli array sono una struttura dati che ci permette di aggregare elementi che hanno lo
stesso tipo. Un array è una sequenza di celle che contengono dati (dello stesso tipo).
La dichiarazione di un array specifica nome e dimensione (cioè numero di celle) e tipo
degli elementi: Ad esempio la dichiarazione
int a[100];
dichiara che l’array di nome a avrà 100 elementi di tipo intero. Ogni cella di a è
numerata a partire da 0, e può essere acceduta e/o modificata specificando l’array
seguito dal suo numero d’ordine fra parentesi quadre. Ad esempio a[0] è il primo
elemento di a, a[7] l’ottavo.
NB – Gli elementi dell’array a[i] sono variabili del tipo dichiarato all’atto
della dichiarazione dell’array e possono essere utilizzate come tali. Notare che
ad a[100] non corrisponde alcun elemento dell’array; sia in fase di
compilazione che in fase di esecuzione questo tipo di errore (indice fuori del
range) non viene rilevato.
Nel seguito presentiamo alcuni algoritmi su arrays sia mediante il diagramma di
flusso che il frammento di codice C che presenta le dichiarazioni di varabili e i cicli
principali.
6.1. Algoritmi di Ricerca
Consideriamo dapprima un problema fondamentale che usa la struttura di array
unidimensionale (un vettore). Questo problema, detto problema di Ricerca di un
elemento in un vettore, può esser formulato e risolto in più modi, che analizzeremo
nel seguito. Nella sua generalità, il problema della ricera di un elemento può essere
formulato come segue: "Dato un vettore A di n elementi e un elemento y, determinare
se y occorre in A".
6.1.1. Ricerca di un elemento x in un array a (int a[100] ) di n
elementi, sapendo che x compare in a. L’algoritmo deve ritornare
nella variabile j l’indice a cui si trova x.
∃i ∈ [0, 99] [a[i] = x]
a[j] = x
Asserzione iniziale:
Asserzione finale:
j←0
( i 0 i (j-1) a[i]
0 = j = 99
no
a[j]≠ x
acc
j
si
j←j+1
a[j ] = x
x)
0
j
99
47
Frammento di codice C corrispondente al precedente diagramma:
int j, x;
int a[100];
j = 0;
//INV:(per ogni i, 0≤i≤(j-1) a[i]≠ x) AND (0 ≤ j ≤ 9 9)
while (a[j] != x) j++;
printf(%d,j);
Notare che se non sapessimo che l’elemento x compare nell’array, il precedente
frammento di programma non sarebbe corretto .
Complessità : L’algoritmo è lineare nella dimensione dell’array.
6.1.2. Ricerca di un elemento x in un array a. L’algoritmo deve ritornare
nella variabile j l’indice a cui si trova x se c'è, opppure j = 100 se x
non occorre nell’array.
Asserzione finale ((j < 100) → a[j] = x) ∧ ( j = 100 → x∉ a[0..99])
j←0
( i 0 i
si
j←j+1
(j-1) a[i]
x)
(0
j
99)
m =∧ nj < 99
a[j]≠x
no
a[j]= x
(x
a[0..(j-1)]
j = 99)
si
a[j]= x
a[j ]= x
no
x
a[0..j]
0
j
99
0
j
j = 100
j←j+1
(x
a[0..j]
j
j = 100)
(a[j]= x
Frammento di codice C corrispondente al precedente diagramma:
int j, x;
int a[100];
j = 0;
//INV:(per ogni i, 0 ≤ i ≤(j-1) a[i]≠ x) AND (0≤j≤99)
while ((a[j] != x) && (j< 99)) j++;
if (a[j] != x) j++;
printf(%d,j);
Complessità : Il precedente algoritmo è lineare nella dimensione dell’array.
99)
48
Se gli elementi del vettore non sono ordinati, questa ricerca lineare non può essere
evitata. Il ciclo while fa interrompere la ricerca quando si è trovata una occorrenza di
x. Notiamo, che se x può occorrere più di una volta in a, ed è richiesto di trovarne
tutte le occorrenze, sia questo algoritmo che quello del Paragrafo 6.1.1 non sono
corretti, perchè trovano solo la prima occorrenza di x. In questo caso occorre usare un
ciclo for j = 1, n ed esaminare sempre tutte le caselle del vettore.
6.1.3. Ricerca di un elemento x in un array a mediante la Pesiera
Binaria. L’algoritmo deve ritornare in una variable trovato il valore
True se x c'è, e il valore False se x non occorre nell’array. Il vettore
è ordinato in modo crescente e tutti gli elementi sono diversi.
Abbiamo visto che la ricerca sequenziale di un elemento in un array ha complessità
lineare nella dimensione dell’array. Nel caso in cui si sappia che l’array è ordinato, si
può fare di meglio. Per lo meno, possiamo sfruttare l’informazione che l’array è
ordinato per interrompere la ricerca appena si trova un elemento maggiore di quello
cercato. Anche in questo caso, comunque, la complessità dell’algoritmo sarebbe
lineare nel caso peggiore.
Per vedere come possiamo procedere, consideriamo come esempio un array A di
dieci elementi:
A = 1 | 3 | 5 | 6 | 8 | 9 | 10 | 15 | 20 | 25
Supponiamo di voler cercare y = 15. Se confrontiamo 15 con il quinto elemento di A,
siamo sicuri che, poichè 15 è maggiore di 8, esso, se c'è, si troverà certamente nella
metà destra dell’array. Possiamo quindi continuare la ricerca (con lo stesso metodo)
nella porzione di array:
9 | 10 | 15 | 20| 25
Il confronto con un elemento dell’array ha fatto (all'incirca) dimezzare la porzione di
array da controllare. Confrontando di nuovo 15 con l’elemento di mezzo dell’array,
che contiene 15, possiamo concludere che 15 è presente in A nella sua settima
posizione. Quindi invece dei nove confronti che avrei fatto con la ricerca sequenziale,
la ricerca binaria ha effettuato solo tre confronti.
Supponiamo ora di cercare un elemento che non è nell’array, ad esempio y = 17. Il
confronto con 8 dice che 17, se c’è, si deve trovare nella porzione superiore
dell’array; il confronto con 15 dice che 17 si deve trovare in
20 | 25
Dal confronto con 20, infine, scopriamo che 17 non può essere nell’array. In questo
caso ho confrontato 17 tre volte, mentre la ricerca sequenziale l’avrebbe confrontato
con tutti gli elementi dell’array minori di 17 che sono 8.
Cerchiamo adesso di analizzare l'algoritmo in modo più sistematico. L'algoritmo si
basa sul fatto che x non può occorrere in segmenti del vettore che contengono solo
numeri o tutti maggiori o tutti minori di x. Quindi, dato un tratto del vettore a, che
contiene le caselle da j a k, possiamo confrontare con x la casella centrale e poi , se x
non c'è, limitare la ricerca successiva solo a metà del tratto. Nella Figura 19(a) è
illustrata la situazione in cui la lunghezza del vettore è dispari, mentre nella Figura
19(b) la lunghezza del vettore è pari.
49
a[j]
a[j+1]
j
j+1
….
….
……..
a[m-1]
m-1
a[m]
a[m+1]
m
m+1
….
….
………..
a[k-1]
k-1
a[k]
k
(a)
a[j]
a[j+1]
j
j+1
….
….
……..
a[m]
m
a[m+1]
m+1
….
….
a[k-1]
a[k]
………..
k-1
k
(b)
a[m]
j=k=m
(c)
Figura 19 – Passo base dell'algoritmo della Pesiera Binaria. Il vettore
considerato ha lunghezza l jk = k - j + 1. (a) Lunghezza del vettore dispari: m
è la posizione dell'elemento centrale. (b) Lunghezza del vettore pari: m è la
posizione dell'elemento centrale più a sinistra. (c) Lunghezza del vettore
unitaria: la posizione centrale coincide con l'elemento.
Quando la lunghezza del vettore è dispari, c'è un solo elemento centrale, mentre, nel
caso pari ce ne sono due. Possiamo definire (le parentesi quadre denotano la parte
intera dell'argomento):
m = [(j + k)/2]
Possiamo allora considerare il seguente algoritmo, scritto in linguaggio pseudonaturale:
Search(a,x)
j=1, k = n, trovato = False
while j≠k and trovato = False do
m = [(j+k)/2]
if a(m) = x then trovato = True
else if x < a(m) then k = m-1
else j = m+1
endif
endif
end
if trovato = False
then if a(j) = x then trovato = True
return trovato
Analizziamo l'algoritmo: esso può fermarsi quando x viene trovato oppure quando il
vettore da analizzare si è ridotto ad una singola cella che non contiene x.
ji-1, ki-1, trovatoi-1 Condizione iniziale:
j0 = 1, k0 = n, trovato0 = False
Condizione terminale:
i
(trovatot = True) ∨( (trovatot = False) ∧ (jt = kt) )
mi = (ji + ki)/2
ji, ki, trovatoi
li = ki - ji + 1
Asserzione finale: ((j < 100) → a[j] = x) ∧
( j = 100 → x∉ a[0..99])
50
Avremo inoltre:
trovatoi = if a(mi-1) = x then True else trovatoi-1
ji = if x ≤ a(mi-1) = then ji-1 else (mi-1+1)
ki = if x ≥ a(mi-1) = then ki-1 else (mi-1−1)
li = if x = a(mi-1) = then li-1 else ki - ji + 1
Terminazione : Guardando le relazioni tra le lunghezze del vettore in ingresso e in
uscita, osserviamo che la lunghezza si riduce sempre, a meno che sia x = a(mi-1).
Quindi, o trovato diventa vero (e il ciclo termina) o la lunghezza del tratto di vettore
considerato diminuisce. Assumendo che x non occorra nel vettore, allora trovato non
diventa mai vero e la lunghezza del vettore diminuirà ad ogni ciclo, arrivando quindi
al valore 1, che corrisponde alla condizione j = k (e il ciclo termina).
Correttezza : L'algoritmo non può trovare un x che non c'è, quindi può sbagliare solo
perdendo il valore x. La correttezza si può provare dimostrando che, se x occorre nel
vettore, ad ogni riduzione della lunghezza del vettore, il tratto considerato contiene
ancora x. Usiamo il principio di induzione matematica.
Passo base:
B0 : Quando i = 0, il vettore considerato contiene x. Infatti, in questo caso j0 = 1, k 0 =
n e quindi il vettore considerato è tutto il vettore a, in cui, per ipotesi, x occorre.
Ipotesi induttiva:
Hp : Il vettore (ji-1, ki-1) e tutti quelli considerati in precedenza contengono il valore x
Tesi:
Ts : Il vettore (ji, ki) contiene ancora il valore x.
Osservando il ciclo, vediamo che si hanno tre casi:
1. a(mi-1) = x In questo caso il valore di x è stato trovato e l'algoritmo è corretto
2. a(mi-1) > x In questo caso si considera il vettore (ji-1, m i-1-1). Ma essendo il vettore
a ordinato in modo crescente, il valore di x non può trovarsi nella parte
di vettore eliminata, perchè questa contiene solo valori maggiori di x.
Quindi il nuovo vettore contiene ancora x.
3. a(mi-1) < x In questo caso si considera il vettore (mi-1+1, ki-1). Ma essendo il vettore
a ordinato in modo crescente, il valore di x non può trovarsi nella parte
di vettore eliminata, perchè questa contiene solo valori minori di x.
Quindi il nuovo vettore contiene ancora x.
Quindi il valore di x è sempre contenuto nei vettori considerati all'interno del ciclo.
Quando si esce dal ciclo, c'è ancora un caso da considerare, quello per cui j = k. Se x
è in questa casella, esso viene trovato dal confronto all'uscita dal ciclo.
Complessità : Il caso peggiore è quello in cui x non occorre in a, perchè si devono
fare tutte le riduzioni fino al vettore di lunghezza 1. Quindi, all'ingresso del ciclo, il
vettore considerato ha lunghezza n, dopo il primo ciclo ha lunghezza all'incirca n/2,
dopo il secondo ha lunghezza n/22, e dopo l'i-mo avrà lunghezza n/2i. Quindi,
all'uscita, per i = t, si avrà:
n/2t = 1
à t = lg2n
Quindi l'algoritmo ha complessità C(n) = O(lg2n).
Vediamo infine il frammento di programma C che implementa l'algoritmo:
51
int inf = 0;
int sup = DIM-1;
int med = (sup+inf)/2;
/*INV: x∈a[0..sup] se e solo se x ∈ a[inf..sup] AND
0<=inf,sup<DIM and inf<=med<=sup */
while ( (inf <= sup) && (a[med] != x) )
{
if (a[med] > x)
sup = med-1;
else inf = med+1;
med = (sup+inf)/2;
}
if (inf > sup)
return(-1);
else
return(med);
6.1.4. Dato un array a (int a[100]) ed un intero x, restituire nella
variabile occ il numero di occorrenze di x in a.
Questo algoritmo di ricerca non si limita a dire se x occorre in a, ma conta
anche il numero di volte ( ≥ 0) che x occorre.
j←0
occ ←0
occ è il numero di occorrenze di x in a[0..(j-1)]
occ
j< 100
no
si
acc
a[j]= x
no
si
occ ← occ+1
occ è il numero di volte che x occorre in a[0..j]
j←j+1
Asserzione finale: occ è il numero di volte che x occorre in a[0..99]
52
Frammento di codice C corrispondente al precedente diagramma:
int j, occ;
int a[100];
occ = 0;
// INV: occ e’ il numero di occorrenze di x in a[0..(j-1)]
for (j=0; j<=99; j++)
if (a[j] == x) occ++;
printf(%d,occ);
Si noti la somiglianza fra questo algoritmo ed il precedente. La complessità è lineare
nella dimensione dell’array.
6.2. Algoritmi su Array
In questo paragrafo analizziamo degli algoritmi semplici che si possono applicare
come parti di algoritmi più complessi, come vedremo nel prossimo paragrafo.
6.2.1. Ricerca del massimo elemento di un array a. L’algoritmo deve
ritornare nella variabile max il valore del massimo elemento.
Asserzione finale:
i 0
j←0
i
99 | a[i]=max
j 0
j
99 a[j]
max
max ←a[0]
( i 0 i 99 a[i]=max)
( k 0 k j-1 a[k] max)
max
j < 100
si
no
no
a[j]> max
max ← a[j]
( i 0 i
99| a[i]=max)
( k 0 k
j| a[k]
j←j+1
Frammento di codice C corrispondente al precedente diagramma:
int j, max;
int a[100];
max = a[0];
/*INV: (∃i∈[0,99]| max=a[i]) AND (∀k∈[0,j-1], a[k]≤ max)*/
for (j=0; j<=99; j++)
if (a[j] > max) max = a[j]; printf(%d,max);
Complessità : Il precedente algoritmo è lineare nella dimensione dell’array.
max)
53
6.2.2. Dato un array a (int a[100]) dire se l’array è ordinato oppure
no. A questo scopo, restituire la varibile ordinato, che vale 1 se
l’array è ordinato e 0 altrimenti.
Asserzione finale: ( (ordinato = 1) →∀i ∈[0, 98]| a[i] ≤ a[i+1]) ) ∧
( ( ordinato = 0) → (∃ i | 0 ≤ i ≤ 98 |a[i] > a[i+1]) )
acc←0
j←0
( i 0
i
(j-1) a[i]
a[i+1] )
0
j
98
si
j←j+1
m = n j< 98
a[j]≤a[j+1]∧
no
(a[j] > a[j+1] )
a[j]≤a[j+1]
i 0
i
( i 0
97 a[i]
a[i+1]
j = 98)
si
i 0
98 a[i] > a[i+1]
i
i
98 a[i]
a[i+1]
no
ordinato←0
ordinato←1
ordinato
Frammento di codice C corrispondente al precedente diagramma:
int j, ordinato;
int a[100];
j = 0;
//INV: (per ogni i, 0≤ i ≤(j-1)
a[i]≤ a[i+1]) AND 0≤ j≤ 99
while (a[j]<=a[j+1] && j< 98) j++;
if (a[j] ]<=a[j+1])
ordinato=1;
else
ordinato=0;
printf(%d,j);
Complessità : Il precedente algoritmo è lineare nella dimensione dell’array.
6.3. Algoritmi di Ordinamento
Ordinare gli elementi di un vettore (per sempio, in modo crescente) è un problema
molto importante, per cui esistono diversi algoritmi con caratteristiche differenti.
54
6.3.1. Ordinamento per Selezione
Il modo più semplice per ordinare un array in modo crescente è quello di cercare il
minimo elemento e scambiarlo con il primo elemento, poi cercare il minimo
dell’array dalla seconda posizione in poi e scambiarlo con il secondo elemento e così
via. Ad una generica iterazione avremo:
i
sup
La porzione dell’array da 0 a (i-1) è ordinata e contiene gli elementi più piccoli
dell’array. Cerco il minimo dell’array da i a sup e lo scambio con l’elemento i-esimo.
A questo punto l’array da 0 a i è oridinato e contiene gli elementi più piccoli
dell’array. Il ciclo principale dell’algoritmo è il seguente:
int i;
int sup = DIM-1;
/* INV: sia a’ l’array iniziale,
a[0..(i-1)] è ordinata AND 0<=i<=sup AND
per ogni j, i<=j<=sup a[i-1]<=a[j] (cioè gli elementi
di a[0..(i-1)] sono tutti più piccoli di quelli di
a[i..sup])
*/
for (i=0; i<sup; i++)
scambia(a,i,IndiceMinimo(a,i,sup);
Le funzioni Scambia e IndiceMinimo operano come descritto nel seguito.
/* PRE: sup<=DIM-1 (dove DIM è il numero degli elementi di b)
AND 0<=i<=sup
POST: sia m il valore ritornato da indiceMinimo(b,i,sup)
per ogni j, 0≤j≤sup, m≤a[j]
*/
int indiceMinimo(int b[],int i,int sup)
{
int temp=a[i];
int j;
for(j=i; j<=sup; j++)
if temp>a[j]
temp = a[j];
}
/* PRE: 0<=i,j<=DIM-1 (dove DIM è il numero degli elementi di b)
POST:
sia b’[0..DIM-1] l’array iniziale alla fine
per ogni k tale che 0≤k≤DIM-1
((k!=i AND k!=j) implica b[k] = b’[k]) AND
b[i] = b’[j]
AND b[j] = b’[i]
*/
void scambia(int b[],int i,int j)
{
int temp=b[i];
b[i] = b[j];
b[j] = temp;
}
55
Complessità : Il ciclo for viene eseguito DIM volte (dove DIM è il numero degli
elementi dell’array). La funzione di ricerca del minimo la prima volta eseguirà DIM
operazioni, la seconda DIM-1 e così via. Per cui il numero di operazioni fatte è
DIM + (DIM-1) + ….. + 2 + 1 = O(DIM2)
6.3.2. Ordinamento per Inserzione
Il modo più semplice per ordinare un array in modo crescente è quello di cercare il
Nell’algoritmo di ordinamento per inserzione presupponiamo che la porzione iniziale
dell’array (tratteggiata) sia ordinata
a:
i
Per ordinare la porzione inizile più la posizione i-esima, dobbiamo inserire l’elemento
a[i] nella posizione giusta in a[0..i] spostando tutti gli elementi che seguono (fino ad i)
di una posizione a destra. Quindi il ciclo principale dell’algoritmo sarà il seguente:
int i;
int sup = DIM-1;
/* INV: sia a’ l’array iniziale,
a[0..(i-1)] è ordinata e contiene tutti e soli gli
elementi di a’[0..(i-1)]
*/
for (i=1; i<=sup; i++)
inserisci(a,i);
La funzione inserisci scambia l’elemento i-mo con tutti quelli che lo precedono e
che sono maggiori (in questo modo gli elementi che lo seguono sono shiftati di una
posizione).
/* PRE: 0<=i<=DIM-1 AND
sia v’[0..(DIM-1)] l’array iniziale,
v’[0..(i-1)] è ordinata.
POST: v[0..i] è ordinata e contiene tutti e soli gli elementi di
V’[0..i]
*/
void inserisci(int v[],int i)
{
int j = i - 1;
/* sia v’[0..(DIM-1)] l’array iniziale (v’[0..(i-1)] è
ordinata),
v[0..i] contiene gli stessi elementi di v’[0..i] AND
v[0..j] e v[(j+1)..i] sono ordinate AND
0<=j<=i-1
*/
while ((j>=0) && (v[j]>v[j+1]))
scambia(v,j,j+1);
}
La porzione dell’array da 0 a (i-1) è ordinata e contiene gli elementi più piccoli
dell’array. Cerco il minimo dell’array da i a sup e loscambio con l’elemento i-esimo.
56
A questo punto l’array da 0 a i è oridinato e contiene gli elementi più piccoli
dell’array.
Complessità : Il ciclo for viene eseguito DIM volte (dove DIM è il numero degli
elementi dell’array). La funzione di inserzione esegue tante operazione quanti sono
gli elementi dell’array che seguono l’elemento da inserire. Nel caso migliore in cui
l’array è già ordinata ogni chiamata dell’inserzione eseguirà un numero costante di
operazioni. Per cui la complessità è lineare nella dimensione dell’aray. Nel caso
peggiore in cui l’array è ordinata in modo decrescente (come per l’algoritmo
precedente) la prima volta inserisci eseguirà 1 operazione la seconda 2 e così via fino
all’ultima in cui vengono eseguite DIM operazioni. Per cui avrò
Dim + (DIM-1) + ….. + 2 + 1 = O(DIM2)
Per cui la complessità dell’algoritmo è quadratica nella dimensione dell’array.
6.3.3. Ordinamento per Interscambio (Bubble Sort)
Questo algortimo di ordinamento si basa sul fatto che un array a[0..sup] è ordinato se
∀i 0 ≤ i < sup a[i] ≤ a[i+1].
Quindi, se scorriamo ripetutamente l’array, possiamo scambiare gli elementi adiacenti
a[i] e a[i+1], nel caso in cui a[i] > a[i+1]. Ad un certo punto, quando non ci sono più
elementi adiacenti fuori posto, l’array sarà ordinato. La considerazione che possiamo
inoltre fare è che il ciclo
for (i=0; i<sup; i++)
if (a[i]>a[i+1])
scambia(a,i,i+1)
ha come invariante:
a[i] è il massimo di a[0..i].
Infatti, gli scambi fanno affiorare il massimo elemento in ultima posizione. Alla fine
del ciclo avremo:
a[sup] è il massimo di a[0..sup].
Quindi la seconda volta che scorriamo l’array possiamo applicare l’algoritmo solo
alla porzione di a[0..(sup-1)] e così via. Otteniamo quindi il seguente.
int i,j;
int sup = DIM-1;
/* INV: sia a’ l’array iniziale,
a[j..sup] è ordinata AND 0<=j<=sup AND
per ogni k, 0<=k<j a[k]<=a[j] (cioè gli elementi
di a[j..sup] sono tutti più grandi di quelli di
a[0..(j-1)])
*/
for (j=sup; j>0; j--)
/* INV: a[i] è il massimo di a[0..i] AND 0<=i<=j AND
l’invarinate precedente */
for (i=0; i<j; i++)
if (a[i]>a[i+1])
scambia(a,i,i+1);
Complessità : Il ciclo for esterno viene eseguito DIM-2 volte (dove DIM è il numero
degli elementi dell’array). Per ogni iterazione si esegue il ciclo interno che, la prima
volta, richiede DIM-2 iterazioni, la seconda DIM-3 e così via. Di nuovo, la
complessità è quadratica nel numero degli elementi dell’array, cioè O(DIM2).