Complessità computazionale
In questa pagina 8
Idea
Si misura il numero di operazioni elementari (assegnamenti, confronti, operazioni aritmetiche, accessi per indice) in funzione della dimensione dell'input (lunghezza della lista, numero di cifre...). Il tempo in secondi dipende dalla macchina; la crescita al crescere di no.
- Caso peggiore: massimo numero di operazioni tra gli input di dimensione (è quello che si dà di solito: è una garanzia).
- Caso migliore: minimo.
- Caso medio: media su una distribuzione degli input.
Esempio, ricerca lineare di x in una lista di elementi: caso migliore 1 confronto (primo elemento), caso peggiore (assente o ultimo).
Notazione O-grande
se esistono e tali che per ogni : cresce al più come , a meno di costanti.
Regole pratiche:
- si tiene solo il termine dominante e si tolgono le costanti: ;
- passi in sequenza: si sommano (vince il maggiore);
- cicli annidati: si moltiplicano.
Esistono anche (limite inferiore: cresce almeno come) e (stessa crescita: sia sia ).
Classi di crescita
| Classe | Nome | Esempio |
|---|---|---|
| costante | accesso v[i], append |
|
| logaritmica | ricerca binaria | |
| lineare | ricerca lineare, somma, massimo | |
| quasi lineare | merge sort, sorted |
|
| quadratica | selection sort, insertion sort, tutte le coppie | |
| esponenziale | fib ricorsivo ingenuo, tutti i sottoinsiemi |
Con e operazioni al secondo: richiede circa 0,02 s, circa 17 minuti, è impossibile.
Contare i passi
for i in range(n): # n volte
s += v[i] # O(1)
# totale O(n)
for i in range(n):
for j in range(i + 1, n): # n-1, n-2, ..., 0 volte
... # totale n(n-1)/2 = O(n^2)
i = n
while i > 1:
i //= 2 # i si dimezza: log2(n) iterazioni -> O(log n)Ricorsione: si scrive l'equazione di ricorrenza del costo .
fattoriale: → .- ricerca binaria: → .
- merge sort: → .
fibingenuo: → esponenziale.
Costi nascosti in Python
Una riga di Python non è sempre :
| Operazione | Costo |
|---|---|
x in lista, lista.index(x), lista.remove(x), min, max, sum |
|
lista.insert(0, x), lista.pop(0) |
|
slicing v[a:b], v.copy(), s + t |
proporzionale alla lunghezza del risultato |
x in insieme, d[k], k in d |
in media (vedi Dizionari e insiemi in PythonADT mappa e insieme; dict con chiavi hashable, accesso, get, iterazione, conteggi e raggruppamenti; set e operazioni insiemistiche; tabelle hash e costo O(1) medio; Counter e defaultdict.Dizionari e insiemi in Python →) |
sorted(v), v.sort() |
# O(n^2): "in" su una lista dentro un ciclo
dup = any(v[i] in v[i+1:] for i in range(len(v)))
# O(n): insieme
dup = len(set(v)) != len(v)Misurare i tempi
import time
t0 = time.perf_counter()
risultato = funzione(dati)
print(f"{time.perf_counter() - t0:.4f} s")Raddoppiando : un algoritmo lineare raddoppia il tempo, uno quadratico lo quadruplica.
Spazio
Analogamente si misura la memoria aggiuntiva: se si usano poche variabili, se si crea una copia della lista; la ricorsione usa spazio di stack proporzionale alla profondità.
Errori tipici
- Considerare un
insu lista o uno slicing. - Confondere caso migliore e caso peggiore.
- Pensare che le costanti non contino mai: a parità di classe contano, ma per grande la classe vince.