Algoritmi: diagrammi di flusso, programmazione strutturata e complessità
Proprietà di un algoritmo, diagrammi di flusso e pseudocodice con l'algoritmo di Euclide, teorema di Böhm-Jacopini, notazione O e ricerca binaria.
Un algoritmo è una sequenza finita di istruzioni non ambigue che risolve un problema. La parola viene dal nome del matematico persiano al-Khwarizmi, e il primo algoritmo che conosciamo, quello di Euclide per il massimo comun divisore, ha più di duemila anni. Prima di scrivere codice conviene progettare l’algoritmo: è lì che si decide se il programma sarà corretto e veloce. In questa lezione vediamo come si descrive un algoritmo, le strutture di base della programmazione strutturata e come si misura il costo, cioè la complessità.
Le proprietà di un algoritmo
- Finitezza: termina dopo un numero finito di passi.
- Non ambiguità: ogni passo ha un solo significato.
- Ingresso e uscita: riceve dei dati e produce un risultato.
- Efficacia: ogni passo si può eseguire davvero, con le operazioni disponibili.
- Generalità: risolve una classe di problemi, non un solo caso.
Come si descrive: diagrammi di flusso e pseudocodice
Il diagramma di flusso rappresenta l’algoritmo con blocchi collegati da frecce: ovali per inizio e fine, parallelogrammi per ingresso e uscita, rettangoli per le operazioni, rombi per le decisioni.

Il pseudocodice descrive lo stesso algoritmo con un linguaggio a metà tra l’italiano e un linguaggio di programmazione, più compatto del diagramma:
leggi a, b
finché b ≠ 0:
r ← a mod b
a ← b
b ← r
scrivi a
Perché funziona. Ogni divisore comune di
e
divide anche
, e viceversa: quindi
. Poiché
, il secondo numero diminuisce a ogni passo e arriva a 0 in un numero finito di passi, e
.
La programmazione strutturata
Il teorema di Böhm-Jacopini (1966, due informatici italiani) dimostra che qualsiasi algoritmo si può scrivere usando solo tre strutture:
- sequenza: un’istruzione dopo l’altra;
- selezione: se una condizione è vera fai una cosa, altrimenti un’altra (
if); - iterazione: ripeti finché una condizione è vera (
while).
Non servono i salti arbitrari (goto), che rendono il codice difficile da seguire: è la base della programmazione strutturata, e del C che vedremo dalla prossima lezione.
La complessità
Due algoritmi corretti per lo stesso problema possono avere tempi molto diversi. La complessità computazionale misura come cresce il numero di operazioni al crescere della dimensione
dei dati. Si usa la notazione
: un algoritmo è
se per
grande fa al massimo un numero di operazioni proporzionale a
.
| Complessità | Esempio | ||
|---|---|---|---|
| accedere a un elemento di un array | 1 | 1 | |
| ricerca binaria in un array ordinato | 10 | 20 | |
| trovare il massimo di un array | |||
| merge sort, FFT | |||
| ordinamento a bolle, DFT diretta | |||
| provare tutti i sottoinsiemi | impossibile |
A un miliardo di operazioni al secondo,
operazioni richiedono un quarto d’ora;
due centesimi di secondo. Migliorare l’algoritmo conta molto più che comprare un computer più veloce.
Esempio: quanti passi fa Euclide. Ogni due passi il primo numero almeno si dimezza (si dimostra con poca fatica), quindi i passi sono al massimo circa
del numero più piccolo:
. Per numeri di 300 cifre, come quelli della crittografia RSA, servono qualche migliaio di passi, una frazione di millisecondo. Provare tutti i divisori sarebbe impossibile.
Ricerca lineare e ricerca binaria
Cercare un valore in un array di
elementi non ordinati richiede di guardarli uno a uno:
. Se l’array è ordinato si può fare molto meglio: si guarda l’elemento centrale, e a seconda che sia più grande o più piccolo del valore cercato si scarta metà dell’array. Dopo
passi resta
elementi, quindi bastano
passi: 20 confronti per un milione di elementi, 30 per un miliardo.
/* restituisce l'indice di x nell'array ordinato v di n elementi, oppure -1 */
int ricerca_binaria(const int v[], int n, int x)
{
int lo = 0, hi = n - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2; /* evita di calcolare lo + hi, che potrebbe andare in overflow */
if (v[mid] == x) return mid;
if (v[mid] < x) lo = mid + 1;
else hi = mid - 1;
}
return -1;
}
Il calcolo di mid come lo + (hi - lo) / 2 invece di (lo + hi) / 2 non è una pignoleria: con indici vicini al massimo valore di un int, la somma andrebbe in overflow. È un bug rimasto per anni nelle librerie standard di diversi linguaggi.




