Salta al contenuto
Note per Studenti Complessità computazionale

Complessità computazionale

In questa pagina 8
In questa pagina 7

Idea

Definizione (caso peggiore, migliore e medio). Si misura il numero di operazioni elementari (assegnamenti, confronti, operazioni aritmetiche, accessi per indice) in funzione della dimensione dell'input nn (lunghezza della lista, numero di cifre...). Il tempo in secondi dipende dalla macchina; la crescita al crescere di nn no.

  • Caso peggiore: massimo numero di operazioni tra gli input di dimensione nn (è quello che si dà di solito: è una garanzia).
  • Caso migliore: minimo.
  • Caso medio: media su una distribuzione degli input.

Esempio. Nella ricerca lineare su una lista di n=100n=100 elementi il caso migliore costa 11 confronto e il caso peggiore 100100.

Esempio, ricerca lineare di x in una lista di nn elementi: caso migliore 1 confronto (primo elemento), caso peggiore nn (assente o ultimo).

Notazione O-grande

Definizione (notazione O-grande). f(n)=O(g(n))f(n) = O(g(n)) se esistono c>0c > 0 e n0n_0 tali che f(n)≤c⋅g(n)f(n) \le c \cdot g(n) per ogni n≥n0n \ge n_0: ff cresce al più come gg, a meno di costanti.

Regole pratiche:

  • si tiene solo il termine dominante e si tolgono le costanti: 3n2+10n+7=O(n2)3n^2 + 10n + 7 = O(n^2);
  • passi in sequenza: si sommano (vince il maggiore);
  • cicli annidati: si moltiplicano.

Esempio. 3n2+10n+7≤20n23n^2+10n+7\le20n^2 per n≥1n\ge1, quindi 3n2+10n+7=O(n2)3n^2+10n+7=O(n^2) con c=20c=20 e n0=1n_0=1.

Esistono anche Ω\Omega (limite inferiore: cresce almeno come) e Θ\Theta (stessa crescita: sia OO sia Ω\Omega).

Classi di crescita

Definizione (classi di crescita)

Classe Nome Esempio
O(1)O(1) costante accesso v[i], append
O(log⁡n)O(\log n) logaritmica ricerca binaria
O(n)O(n) lineare ricerca lineare, somma, massimo
O(nlog⁡n)O(n \log n) quasi lineare merge sort, sorted
O(n2)O(n^2) quadratica selection sort, insertion sort, tutte le coppie
O(2n)O(2^n) esponenziale fib ricorsivo ingenuo, tutti i sottoinsiemi

Esempio. Con n=106n=10^6 e 10910^9 operazioni al secondo, nlog⁡2n≈2⋅107n\log_2n\approx2\cdot10^7 operazioni richiedono circa 0,020{,}02 s e n2=1012n^2=10^{12} circa 1717 minuti.

Con n=106n = 10^6 e 10910^9 operazioni al secondo: nlog⁡nn \log n richiede circa 0,02 s, n2n^2 circa 17 minuti, 2n2^n è impossibile.

Contare i passi

python
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)

Proprietà (equazioni di ricorrenza del costo). Ricorsione: si scrive l'equazione di ricorrenza del costo T(n)T(n).

  • fattoriale: T(n)=T(n−1)+cT(n) = T(n-1) + c → O(n)O(n).
  • ricerca binaria: T(n)=T(n/2)+cT(n) = T(n/2) + c → O(log⁡n)O(\log n).
  • merge sort: T(n)=2T(n/2)+cnT(n) = 2T(n/2) + cn → O(nlog⁡n)O(n \log n).
  • fib ingenuo: T(n)=T(n−1)+T(n−2)+cT(n) = T(n-1) + T(n-2) + c → esponenziale.

Esempio. fattoriale(5) fa 66 chiamate (n=5,4,3,2,1,0n=5,4,3,2,1,0), coerente con T(n)=T(n−1)+c∈O(n)T(n)=T(n-1)+c\in O(n).

Costi nascosti in Python

Una riga di Python non è sempre O(1)O(1):

Operazione Costo
x in lista, lista.index(x), lista.remove(x), min, max, sum O(n)O(n)
lista.insert(0, x), lista.pop(0) O(n)O(n)
slicing v[a:b], v.copy(), s + t proporzionale alla lunghezza del risultato
x in insieme, d[k], k in d O(1)O(1) 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(nlog⁡n)O(n \log n)
python
# 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

python
import time
t0 = time.perf_counter()
risultato = funzione(dati)
print(f"{time.perf_counter() - t0:.4f} s")

Proprietà (effetto del raddoppio di n sul tempo). Raddoppiando nn: un algoritmo lineare raddoppia il tempo, uno quadratico lo quadruplica.

Esempio. Se con n=1000n=1000 un algoritmo quadratico impiega 22 s, con n=2000n=2000 ne servono circa 2⋅4=82\cdot4=8.

Spazio

Analogamente si misura la memoria aggiuntiva: O(1)O(1) se si usano poche variabili, O(n)O(n) se si crea una copia della lista; la ricorsione usa spazio di stack proporzionale alla profondità.

Errori tipici

  • Considerare O(1)O(1) un in su lista o uno slicing.
  • Confondere caso migliore e caso peggiore.
  • Pensare che le costanti non contino mai: a parità di classe contano, ma per nn grande la classe vince.

Versione ripasso

Idea

Numero di operazioni elementari in funzione della dimensione dell'input nn.

  • Caso peggiore: massimo numero di operazioni (garanzia).
  • Caso migliore: minimo.
  • Caso medio: media sulla distribuzione degli input.

Notazione O-grande

f(n)=O(g(n))f(n) = O(g(n)) se esistono c>0c > 0 e n0n_0 tali che f(n)≤c⋅g(n)f(n) \le c \cdot g(n) per ogni n≥n0n \ge n_0: ff cresce al più come gg.

  • Si tiene solo il termine dominante e si tolgono le costanti: 3n2+10n+7=O(n2)3n^2 + 10n + 7 = O(n^2).
  • Passi in sequenza: si sommano (vince il maggiore).
  • Cicli annidati: si moltiplicano.
  • Esistono anche Ω\Omega (limite inferiore) e Θ\Theta (stessa crescita).

Classi di crescita

Classe Nome Esempio
O(1)O(1) costante accesso v[i], append
O(log⁡n)O(\log n) logaritmica ricerca binaria
O(n)O(n) lineare ricerca lineare, somma, massimo
O(nlog⁡n)O(n \log n) quasi lineare merge sort, sorted
O(n2)O(n^2) quadratica selection sort, insertion sort
O(2n)O(2^n) esponenziale fib ricorsivo ingenuo

Contare i passi e Ricorrenze

  • Ciclo semplice: O(n)O(n).
  • Cicli annidati triangolari: O(n2)O(n^2).
  • Dimezzamento (i //= 2): O(log⁡n)O(\log n).
  • Ricorrenze T(n)T(n):
    • fattoriale: T(n)=T(n−1)+c→O(n)T(n) = T(n-1) + c \to O(n).
    • ricerca binaria: T(n)=T(n/2)+c→O(log⁡n)T(n) = T(n/2) + c \to O(\log n).
    • merge sort: T(n)=2T(n/2)+cn→O(nlog⁡n)T(n) = 2T(n/2) + cn \to O(n \log n).
    • fib ingenuo: T(n)=T(n−1)+T(n−2)+c→T(n) = T(n-1) + T(n-2) + c \to esponenziale.

Costi nascosti in Python

Spazio

Memoria aggiuntiva: O(1)O(1) se poche variabili, O(n)O(n) per copie o strutture dati; la ricorsione usa stack proporzionale alla profondità.

Errori tipici

Considerare O(1)O(1) un in su lista o uno slicing; confondere caso migliore e peggiore.

Esercizi su questo argomento

Teoria collegata