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, insertion sort e bubble sort (quadratici), merge sort (n log n, divide et impera); stabilità, ordinamento sul posto, costo nei vari casi; sort e sorted con key.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, insertion sort e bubble sort (quadratici), merge sort (n log n, divide et impera); stabilità, ordinamento sul posto, costo nei vari casi; sort e sorted con key.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