Salta al contenuto
Note per Studenti Esercizio 5 · crescita delle funzioni e costo degli algoritmi

Esercizio 5crescita delle funzioni e costo degli algoritmi

Esame
In questa pagina 10

Testo (questionario di dicembre 2022 e test d'esame di dicembre 2012 e gennaio 2014 del corso di Fondamenti di Informatica UniPD; i quesiti sono indipendenti dal linguaggio). Teoria: Complessità computazionaleCosto di un algoritmo in funzione della dimensione dell'input; caso peggiore, migliore e medio; notazione O-grande e classi di crescita; come contare i passi di cicli e ricorsioni; costo delle operazioni Python.Complessità computazionale →, Algoritmi di ordinamentoSelection sort e insertion sort (quadratici, con invarianti), merge sort (divide et impera, Theta(n log n)), quick sort (caso pessimo quadratico, medio n log n), heap sort (in loco, Theta(n log n)), ordinamento senza confronti per chiavi intere in un intervallo piccolo, limite inferiore Omega(n log n) per gli algoritmi basati su confronti.Algoritmi di ordinamento →, Ricerca lineare e binariaRicerca lineare su sequenze qualsiasi in O(n); ricerca binaria su sequenze ordinate in O(log n), versione iterativa e ricorsiva, invariante e errori di indice; modulo bisect.Ricerca lineare e binaria →.

  1. Qual è la miglior stima asintotica di f(n)=log⁡n+2nlog⁡(n+4)+(3n+1)(n−1)n2f(n) = \log n + 2n\log(n+4) + \dfrac{(3n+1)(n-1)}{n^2}?
  2. Della funzione T(n)=3n+10log⁡log⁡n+nlog⁡n+8T(n) = 3n + 10\log\log n + n\log n + 8 dire se è O(n)O(n), Ω(n)\Omega(n), Θ(nlog⁡n)\Theta(n\log n), O(n2)O(n^2).
  3. Ordinare per complessità crescente f(n)=nf(n) = n, g(n)=nlog⁡ng(n) = n\log n, h(n)=log⁡nh(n) = \log n.
  4. Se l'ordinamento per selezione di 10 00010\,000 elementi richiede 1010 secondi, quanto servirà per 100 000100\,000 elementi?
  5. Quale tra A0A_0 con tempo 5n25n^2 e A1A_1 con tempo n(n−1000)+7nn(n-1000) + 7n è migliore asintoticamente?
  6. Se T(n)=O(n5)T(n) = O(n^5), quali delle seguenti affermazioni sono sempre vere: T(n)=O(n6)T(n) = O(n^6); T(n)=O(n4)T(n) = O(n^4); T(n)=Ω(n5)T(n) = \Omega(n^5); T(n)=O(2n)T(n) = O(2^n)?
  7. Un vettore dinamico raddoppia la capacità quando è pieno. Quante copie di elementi si fanno in mm inserimenti? E se la capacità cresce di una sola unità alla volta? Che cosa significa "O(1)O(1) ammortizzato"?
  8. Un programma usa una lista Python come coda: inserisce con append ed estrae con pop(0). Quanto costa svuotarla, e con collections.deque?

1. f(n)f(n)

Si guarda il termine che cresce più in fretta e si trascurano costanti e termini di ordine inferiore:

  • log⁡n\log n cresce lentamente;
  • 2nlog⁡(n+4)∼2nlog⁡n2n\log(n+4) \sim 2n\log n: nlog⁡nn\log n;
  • (3n+1)(n−1)n2=3n2−2n−1n2→3\dfrac{(3n+1)(n-1)}{n^2} = \dfrac{3n^2 - 2n - 1}{n^2} \to 3: una costante.

Quindi f(n)=Θ(nlog⁡n)f(n) = \Theta(n\log n). Numericamente f(n)/(nlog⁡n)f(n)/(n\log n) tende a 22 (con n=106n = 10^6 vale 2,0000022{,}000002).

2. T(n)T(n)

Dominante: nlog⁡nn\log n (gli altri termini sono 3n3n, 10log⁡log⁡n10\log\log n e una costante: o(nlog⁡n)o(n\log n)). Dunque:

  • Θ(nlog⁡n)\Theta(n\log n): vera;
  • Ω(n)\Omega(n): vera (cresce almeno come nn);
  • O(n2)O(n^2): vera (è un limite superiore non stretto);
  • O(n)O(n): falsa, perché nlog⁡nn\log n cresce più di nn.

Un'affermazione O(⋅)O(\cdot) è un limite superiore, non necessariamente stretto: T(n)=O(n2)T(n) = O(n^2) è vero ma poco informativo.

3. Ordine

log⁡n≪n≪nlog⁡n\log n \ll n \ll n\log n: h(n), f(n), g(n)h(n),\ f(n),\ g(n).

4. Ordinamento per selezione

L'ordinamento per selezione fa n(n−1)2=Θ(n2)\dfrac{n(n-1)}{2} = \Theta(n^2) confronti. Con n→10nn \to 10n il tempo si moltiplica per ≈102=100\approx 10^2 = 100 (esattamente 100,009100{,}009): 10 s×100≈10\ \text{s} \times 100 \approx 1000 secondi. (Con mergesort, Θ(nlog⁡n)\Theta(n\log n), il fattore sarebbe circa 12,512{,}5.)

5. A0A_0 e A1A_1

A1(n)=n2−1000n+7n=n2−993nA_1(n) = n^2 - 1000n + 7n = n^2 - 993n: per nn grande il rapporto A1/A0→1/5A_1/A_0 \to 1/5 (0,19980{,}1998 per n=105n = 10^5; 0,00140{,}0014 per n=103n = 10^3, dove il termine −1000n-1000n pesa). Entrambi sono Θ(n2)\Theta(n^2): dal punto di vista asintotico sono equivalenti. Le costanti (55 contro 11) e i termini lineari non contano nella notazione OO.

6. Conseguenze di T(n)=O(n5)T(n) = O(n^5)

  • T=O(n6)T = O(n^6): sempre vera (un limite più largo);
  • T=O(2n)T = O(2^n): sempre vera;
  • T=O(n4)T = O(n^4): non è detto (T=n5T = n^5 la smentisce);
  • T=Ω(n5)T = \Omega(n^5): non è detto (T=nT = n è O(n5)O(n^5) ma non Ω(n5)\Omega(n^5)).

OO dà un limite dall'alto, Ω\Omega dal basso, Θ\Theta i due insieme.

7. Vettore dinamico e costo ammortizzato

Una append che trova il vettore pieno copia tutti gli nn elementi. Con raddoppio le copie avvengono quando n=1,2,4,8,…n = 1, 2, 4, 8, \dots: in mm inserimenti sono 1+2+4+⋯<2m1 + 2 + 4 + \cdots < 2m. Con incremento di 1 si copia a ogni inserimento: 1+2+⋯+(m−1)=Θ(m2)1 + 2 + \cdots + (m-1) = \Theta(m^2).

mm copie con raddoppio copie con +1+1 copie con +10+10 2m2m
100 127 4950 460 200
1000 1023 499 500 49 600 2000
10 000 16 383 49 995 000 4 996 000 20 000

Costo ammortizzato O(1)O(1): il costo totale di mm operazioni è O(m)O(m) (<2m< 2m copie più mm scritture), cioè O(1)O(1) per operazione in media sulla sequenza, anche se una singola operazione può costare O(n)O(n). Non è una media su input casuali: vale nel caso peggiore, per qualsiasi sequenza. Un incremento costante non basta: il totale resta quadratico.

8. Coda su lista

pop(0) rimuove il primo elemento e sposta tutti gli altri di un posto: O(n)O(n). Svuotare una lista di nn elementi costa (n−1)+(n−2)+⋯=Θ(n2)(n-1) + (n-2) + \cdots = \Theta(n^2) spostamenti (499 500499\,500 per n=1000n = 1000, ≈2⋅106\approx 2\cdot 10^6 per n=2000n = 2000: il doppio dei dati, quattro volte il lavoro). deque.popleft() è O(1)O(1) e svuotare la coda costa Θ(n)\Theta(n) (Pila e codaPila (LIFO) e coda (FIFO) come ADT: operazioni e costi; realizzazione in Python con list e collections.deque; realizzazione in C con array e indici, coda circolare; applicazioni (parentesi bilanciate, stack delle chiamate, visite).Pila e coda →).

Verifica

python
import math

def copie(m, nuova_cap):
    """numero di elementi copiati in m inserimenti; nuova_cap(cap) = capacità dopo la crescita"""
    cap, n, tot = 1, 0, 0
    for _ in range(m):
        if n == cap:
            tot += n
            cap = nuova_cap(cap)
        n += 1
    return tot

for m in (100, 1000, 10000):
    print(m, copie(m, lambda c: 2 * c), copie(m, lambda c: c + 1), copie(m, lambda c: c + 10), 2 * m)

f = lambda n: math.log(n) + 2 * n * math.log(n + 4) + (3 * n + 1) * (n - 1) / n ** 2
print(f(10 ** 6) / (10 ** 6 * math.log(10 ** 6)))              # 2.0000017962...

T = lambda n: 3 * n + 10 * math.log(math.log(n)) + n * math.log(n) + 8
print(T(10 ** 8) / (10 ** 8 * math.log(10 ** 8)))              # 1.1628...: tende a 1

sel = lambda n: n * (n - 1) // 2                                # confronti del selection sort
print(sel(10 ** 5) / sel(10 ** 4))                              # 100.009

A0 = lambda n: 5 * n * n
A1 = lambda n: n * (n - 1000) + 7 * n
print(A1(10 ** 7) / A0(10 ** 7))                                # 0.19998...: costante, stesso ordine

spostamenti = lambda n: sum(n - 1 - i for i in range(n))        # svuotare una lista con pop(0)
print(spostamenti(1000), spostamenti(2000))                     # 499500 1999000

Errori comuni

  • Tenere le costanti o i termini di ordine inferiore nella risposta (2nlog⁡n2n\log n invece di nlog⁡nn\log n).
  • Confondere OO (limite superiore) con Θ\Theta (limite stretto).
  • Dire che un algoritmo "da 5n25n^2" è peggiore di uno "da n2n^2": stessa classe di crescita.
  • Credere che O(1)O(1) ammortizzato voglia dire "veloce in media su dati casuali".
  • Usare list.pop(0) per fare una coda.

Versione ripasso

Questionario di dicembre 2022 e test di dicembre 2012 e gennaio 2014 (UniPD). Teoria: Complessità computazionaleCosto di un algoritmo in funzione della dimensione dell'input; caso peggiore, migliore e medio; notazione O-grande e classi di crescita; come contare i passi di cicli e ricorsioni; costo delle operazioni Python.Complessità computazionale →, Algoritmi di ordinamentoSelection sort e insertion sort (quadratici, con invarianti), merge sort (divide et impera, Theta(n log n)), quick sort (caso pessimo quadratico, medio n log n), heap sort (in loco, Theta(n log n)), ordinamento senza confronti per chiavi intere in un intervallo piccolo, limite inferiore Omega(n log n) per gli algoritmi basati su confronti.Algoritmi di ordinamento →.

  • Si tiene il termine dominante, senza costanti: log⁡n+2nlog⁡(n+4)+(3n+1)(n−1)n2=Θ(nlog⁡n)\log n + 2n\log(n+4) + \frac{(3n+1)(n-1)}{n^2} = \Theta(n\log n) (l'ultimo termine tende a 3). T(n)=3n+10log⁡log⁡n+nlog⁡n+8T(n) = 3n + 10\log\log n + n\log n + 8: Θ(nlog⁡n)\Theta(n\log n), Ω(n)\Omega(n), O(n2)O(n^2); non O(n)O(n).
  • Ordine crescente: log⁡n≪n≪nlog⁡n\log n \ll n \ll n\log n.
  • Selezione Θ(n2)\Theta(n^2): n→10nn \to 10n moltiplica il tempo per 100 (10 s→≈1000 s10\text{ s} \to \approx 1000\text{ s}); mergesort Θ(nlog⁡n)\Theta(n\log n) per ≈12,5\approx 12{,}5.
  • 5n25n^2 e n(n−1000)+7nn(n-1000)+7n sono entrambi Θ(n2)\Theta(n^2): equivalenti asintoticamente.
  • T=O(n5)T = O(n^5) implica T=O(n6)T = O(n^6) e O(2n)O(2^n), non O(n4)O(n^4) né Ω(n5)\Omega(n^5).
  • Vettore dinamico: raddoppio → in mm inserimenti <2m< 2m copie, O(1)O(1) ammortizzato (vale nel caso peggiore per ogni sequenza); incremento costante → Θ(m2)\Theta(m^2) copie (m=1000m = 1000: 1023 contro 499 500).
  • list.pop(0) è O(n)O(n): svuotare la lista costa Θ(n2)\Theta(n^2); deque.popleft() O(1)O(1) (Pila e codaPila (LIFO) e coda (FIFO) come ADT: operazioni e costi; realizzazione in Python con list e collections.deque; realizzazione in C con array e indici, coda circolare; applicazioni (parentesi bilanciate, stack delle chiamate, visite).Pila e coda →).

Errori comuni: costanti nel risultato; OO confuso con Θ\Theta; 5n25n^2 "peggiore" di n2n^2; ammortizzato inteso come caso medio; pop(0) per le code.

Teoria collegata