Perché Misurare la Complessità?

Immagina di dover cercare un nome in un elenco telefonico di 1.000.000 di nomi. Potresti scorrere ogni nome dall'inizio (lento) o aprire il libro a metà e dimezzare lo spazio di ricerca a ogni passo (veloce). La differenza è enorme: 1.000.000 operazioni vs ~20.

La notazione Big O descrive come il tempo di esecuzione (o lo spazio in memoria) cresce al crescere dell'input N, ignorando le costanti.

Crescita al variare di N=1000

O(1)
1 operazione — costante
O(log n)
~10 operazioni — logaritmico
O(n)
1.000 operazioni — lineare
O(n log n)
~10.000 operazioni — lineare-logaritmico
O(n²)
1.000.000 operazioni — quadratico
O(2ⁿ)
10^300 operazioni — esponenziale (IMPOSSIBILE!)

Le Complessità Principali

O(1) — Costante

Il tempo non dipende dalla dimensione dell'input. Ideale.

CEsempi di O(1)
// Accedere a un elemento di un array → O(1)
int x = arr[5];       // Sempre 1 operazione, indipendentemente da |arr|

// Push/pop su uno stack → O(1)
stack[top++] = valore;

// Operazioni matematiche → O(1)
int somma = a + b;
double area = 3.14 * r * r;

// Hash map lookup (caso medio) → O(1)

O(log n) — Logaritmico

Ogni passo dimezza lo spazio del problema. Con N=1 miliardo, bastano ~30 passi.

CEsempio O(log n) — Binary Search
// Ricerca binaria: a ogni iterazione taglia a metà
int binary_search(int arr[], int n, int target) {
    int sx = 0, dx = n - 1;
    while (sx <= dx) {
        int mid = sx + (dx - sx) / 2;  // Evita overflow
        if (arr[mid] == target) return mid;
        if (arr[mid] < target) sx = mid + 1;
        else dx = mid - 1;
    }
    return -1;
}
// Con N=1.000.000: al massimo log₂(1.000.000) ≈ 20 iterazioni!

O(n) — Lineare

Devi esaminare ogni elemento almeno una volta. Raddoppiando N, il tempo raddoppia.

CEsempio O(n)
// Cerca il massimo → deve vedere tutti gli N elementi
int trova_max(int arr[], int n) {
    int max = arr[0];
    for (int i = 1; i < n; i++) {   // Esegue n-1 volte
        if (arr[i] > max) max = arr[i];
    }
    return max;
}

// Somma di tutti gli elementi → O(n)
long somma(int arr[], int n) {
    long s = 0;
    for (int i = 0; i < n; i++) s += arr[i];
    return s;
}

O(n log n) — Lineare-Logaritmico

Il costo degli algoritmi di ordinamento efficienti (Merge Sort, Quick Sort). Molto buono.

CMerge Sort — O(n log n)
// Merge Sort divide il problema a metà ricorsivamente (log n livelli)
// e ad ogni livello fa O(n) lavoro → totale O(n log n)
void merge_sort(int arr[], int sx, int dx) {
    if (sx >= dx) return;
    int mid = (sx + dx) / 2;
    merge_sort(arr, sx, mid);    // Divide a sinistra  → log n livelli
    merge_sort(arr, mid+1, dx);  // Divide a destra
    merge(arr, sx, mid, dx);     // Unisce → O(n) per livello
}

O(n²) — Quadratico

Un ciclo dentro un ciclo. Con N=10.000 hai 100 milioni di operazioni. Evita per N grandi.

CEsempio O(n²) — Bubble Sort
// Bubble Sort: due cicli annidati → O(n²)
void bubble_sort(int arr[], int n) {
    for (int i = 0; i < n-1; i++) {           // n-1 volte
        for (int j = 0; j < n-i-1; j++) {     // n-i-1 volte
            if (arr[j] > arr[j+1]) {
                int tmp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = tmp;
            }
        }
    }
}
// Con n=1000: ~500.000 confronti
// Con n=10000: ~50.000.000 confronti (lento!)

O(2ⁿ) — Esponenziale

Cresce a dismisura. Praticabile solo per N molto piccoli (≤30).

CFibonacci naive — O(2ⁿ)
// Fibonacci ricorsivo naive: ogni chiamata fa 2 chiamate ricorsive
long long fib(int n) {
    if (n <= 1) return n;
    return fib(n-1) + fib(n-2);  // Albero binario di profondità n → 2^n nodi
}
// fib(10)  → 177 chiamate
// fib(30)  → 2.692.537 chiamate
// fib(50)  → ~2^50 = 1.125.899.906.842.624 chiamate → ANNI di attesa!

Tabella di Riferimento

NotazioneNomeN=10N=100N=1.000N=1M
O(1)Costante1111
O(log n)Logaritmico371020
O(√n)Radice310311.000
O(n)Lineare101001.0001M
O(n log n)Lin-Log336649.96620M
O(n²)Quadratico10010.0001M10¹²
O(n³)Cubico1.0001M10⁹10¹⁸
O(2ⁿ)Esponenziale1.02410³⁰impossibileimpossibile

Regole per Calcolare la Complessità

CRegole pratiche
// REGOLA 1: Ignora le costanti
// f(n) = 3n + 5 → O(n)   (non O(3n+5))
for (int i = 0; i < 3*n; i++) { ... }   // O(n)
x = a + b + c + d;                       // O(1) (non O(4))

// REGOLA 2: Istruzioni consecutive → si sommano (prendi il più grande)
for (int i = 0; i < n; i++) { ... }     // O(n)
for (int i = 0; i < n*n; i++) { ... }   // O(n²)
// Totale: O(n) + O(n²) = O(n²)

// REGOLA 3: Cicli annidati → si moltiplicano
for (int i = 0; i < n; i++) {           // O(n)
    for (int j = 0; j < n; j++) {       // O(n) × O(n) = O(n²)
        ...
    }
}

// REGOLA 4: Ciclo che dimezza → logaritmico
int i = n;
while (i > 1) { i /= 2; }              // O(log n)

// REGOLA 5: Funzione ricorsiva con 2 chiamate → O(2^profondità)
void f(int n) {
    if (n == 0) return;
    f(n-1); f(n-1);                     // O(2ⁿ)
}

// REGOLA 6: Ricorsione che dimezza
void g(int n) {
    if (n == 0) return;
    g(n/2);                             // O(log n)
}

// ESEMPIO COMPLESSO: Qual è la complessità?
void esempio(int arr[], int n) {
    // Parte 1: O(n)
    for (int i = 0; i < n; i++) printf("%d", arr[i]);

    // Parte 2: O(n²)
    for (int i = 0; i < n; i++)
        for (int j = i; j < n; j++)
            printf("%d %d", arr[i], arr[j]);

    // Parte 3: O(log n)
    int k = 1;
    while (k < n) k *= 2;
}
// Totale: O(n) + O(n²) + O(log n) = O(n²)

Complessità Spaziale

Oltre al tempo, contiamo la memoria aggiuntiva usata dall'algoritmo (non quella dell'input).

CComplessità spaziale
// Bubble Sort: O(1) spazio aggiuntivo (in-place)
void bubble_sort(int arr[], int n) {
    for (int i = 0; i < n-1; i++)
        for (int j = 0; j < n-i-1; j++)
            if (arr[j] > arr[j+1]) {
                int tmp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = tmp;  // Solo 1 variabile tmp
            }
}

// Merge Sort: O(n) spazio aggiuntivo (serve array temporaneo)
void merge(int arr[], int sx, int mid, int dx) {
    int *tmp = malloc((dx - sx + 1) * sizeof(int));  // Alloca O(n) spazio
    // ... merge logic ...
    free(tmp);
}

// Fibonacci ricorsivo: O(n) spazio (stack di chiamate profondo n)
long long fib_ricorsivo(int n) {
    if (n <= 1) return n;
    return fib_ricorsivo(n-1) + fib_ricorsivo(n-2);  // Stack profondo n
}

// Fibonacci iterativo: O(1) spazio
long long fib_iterativo(int n) {
    if (n <= 1) return n;
    long long a = 0, b = 1;  // Solo 2 variabili!
    for (int i = 2; i <= n; i++) {
        long long c = a + b;
        a = b; b = c;
    }
    return b;
}
🏋️ Esercizi sulla Complessità
Analizza e misura la complessità di vari algoritmi.
1
Identifica le complessità
FacileAnalisi

Per ognuna di queste funzioni, determina la complessità temporale Big O: a) funzione con un for da 0 a n, b) due for annidati entrambi da 0 a n, c) for che incrementa i di i*2 ogni volta, d) ricorsione che chiama se stessa con n-1, e) ricorsione che chiama se stessa con n/2.

C
// a) O(n) — un ciclo da 0 a n
for (int i = 0; i < n; i++) printf("%d ", i);

// b) O(n²) — due cicli annidati
for (int i = 0; i < n; i++)
    for (int j = 0; j < n; j++) printf("(%d,%d) ", i, j);

// c) O(log n) — i raddoppia ogni volta
for (int i = 1; i < n; i *= 2) printf("%d ", i);
// Conta quante volte puoi raddoppiare prima di superare n → log₂(n)

// d) O(n) — ricorsione lineare
void f(int n) { if (n==0) return; printf("%d ", n); f(n-1); }

// e) O(log n) — ricorsione che dimezza
void g(int n) { if (n<=1) return; printf("%d ", n); g(n/2); }
2
Conta le operazioni
FacileMisura

Modifica la ricerca lineare e la ricerca binaria per contare quante "comparazioni" effettuano. Cerca il numero 9999 in un array da 10.000 elementi ordinati. Stampa il numero di operazioni per ognuna e calcola il rapporto.

C
#include <stdio.h>

int ricerca_lineare(int arr[], int n, int target, int *ops) {
    *ops = 0;
    for (int i = 0; i < n; i++) {
        (*ops)++;
        if (arr[i] == target) return i;
    }
    return -1;
}

int ricerca_binaria(int arr[], int n, int target, int *ops) {
    *ops = 0;
    int sx = 0, dx = n-1;
    while (sx <= dx) {
        (*ops)++;
        int mid = (sx+dx)/2;
        if (arr[mid] == target) return mid;
        if (arr[mid] < target) sx = mid+1;
        else dx = mid-1;
    }
    return -1;
}

int main() {
    int n = 10000;
    int arr[10000];
    for (int i = 0; i < n; i++) arr[i] = i;

    int ops_lin, ops_bin;
    int target = 9999;

    ricerca_lineare(arr, n, target, &ops_lin);
    ricerca_binaria(arr, n, target, &ops_bin);

    printf("Target: %d  (array di %d elementi)\n", target, n);
    printf("Lineare:  %d operazioni\n", ops_lin);
    printf("Binaria:  %d operazioni\n",  ops_bin);
    printf("Rapporto: %.0fx più veloce\n", (double)ops_lin/ops_bin);
    return 0;
}
3
Benchmark pratico
MedioMisurazione tempo

Implementa Bubble Sort (O(n²)) e Merge Sort (O(n log n)). Genera array casuali di dimensione N=1000, 5000, 10000. Misura il tempo di esecuzione di entrambi usando clock(). Stampa una tabella dei risultati e verifica che il rapporto segua la teoria.

C
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>

void bubble_sort(int arr[], int n) {
    for (int i=0; i<n-1; i++)
        for (int j=0; j<n-i-1; j++)
            if (arr[j]>arr[j+1]) { int t=arr[j]; arr[j]=arr[j+1]; arr[j+1]=t; }
}

void merge(int a[], int sx, int mid, int dx) {
    int n1=mid-sx+1, n2=dx-mid;
    int *L=malloc(n1*sizeof(int)), *R=malloc(n2*sizeof(int));
    for(int i=0;i<n1;i++) L[i]=a[sx+i];
    for(int j=0;j<n2;j++) R[j]=a[mid+1+j];
    int i=0,j=0,k=sx;
    while(i<n1&&j<n2) a[k++]=(L[i]<=R[j])?L[i++]:R[j++];
    while(i<n1) a[k++]=L[i++];
    while(j<n2) a[k++]=R[j++];
    free(L); free(R);
}
void merge_sort(int a[], int sx, int dx) {
    if(sx<dx){int m=(sx+dx)/2; merge_sort(a,sx,m); merge_sort(a,m+1,dx); merge(a,sx,m,dx);}
}

double misura(void (*sort)(int[],int), int n) {
    int *arr = malloc(n*sizeof(int));
    for(int i=0;i<n;i++) arr[i]=rand()%10000;
    clock_t t=clock();
    sort(arr,n);
    double ms=(double)(clock()-t)/CLOCKS_PER_SEC*1000;
    free(arr); return ms;
}
void merge_sort_wrap(int arr[], int n) { merge_sort(arr,0,n-1); }

int main() {
    srand(42);
    int sizes[]={1000,3000,5000,8000,10000};
    printf("%-8s  %12s  %12s\n","N","BubbleSort(ms)","MergeSort(ms)");
    printf("--------------------------------------\n");
    for(int i=0;i<5;i++){
        int n=sizes[i];
        double b=misura(bubble_sort,n), m=misura(merge_sort_wrap,n);
        printf("%-8d  %12.2f  %12.2f  (%.0fx)\n",n,b,m,b/m);
    }
    return 0;
}
4
Trovare duplicati — 3 approcci
DifficileOttimizzazione

Implementa 3 soluzioni per trovare se un array contiene duplicati: a) O(n²) con doppio ciclo, b) O(n log n) con ordinamento + scan, c) O(n) con hash table (usa array di flag per valori 0..MAX). Confronta tempi e operazioni.

C
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#define MAX_VAL 100000

// O(n²)
int ha_duplicati_n2(int arr[], int n) {
    for (int i=0; i<n; i++)
        for (int j=i+1; j<n; j++)
            if (arr[i]==arr[j]) return 1;
    return 0;
}

// O(n log n)
int cmp(const void *a, const void *b) { return (*(int*)a)-(*(int*)b); }
int ha_duplicati_nlogn(int arr[], int n) {
    int *copia = malloc(n*sizeof(int));
    memcpy(copia, arr, n*sizeof(int));
    qsort(copia, n, sizeof(int), cmp);
    int dup = 0;
    for (int i=0; i<n-1; i++) if (copia[i]==copia[i+1]) { dup=1; break; }
    free(copia);
    return dup;
}

// O(n) con flag array
int ha_duplicati_on(int arr[], int n) {
    char visti[MAX_VAL] = {0};
    for (int i=0; i<n; i++) {
        if (arr[i]>=0 && arr[i]<MAX_VAL) {
            if (visti[arr[i]]) return 1;
            visti[arr[i]] = 1;
        }
    }
    return 0;
}

int main() {
    int n = 5000;
    int *arr = malloc(n*sizeof(int));
    for (int i=0; i<n; i++) arr[i] = rand() % MAX_VAL;

    clock_t t;
    t=clock(); ha_duplicati_n2(arr,n);     printf("O(n²):     %.3f ms\n",(double)(clock()-t)/CLOCKS_PER_SEC*1000);
    t=clock(); ha_duplicati_nlogn(arr,n);  printf("O(n logn): %.3f ms\n",(double)(clock()-t)/CLOCKS_PER_SEC*1000);
    t=clock(); ha_duplicati_on(arr,n);     printf("O(n):      %.3f ms\n",(double)(clock()-t)/CLOCKS_PER_SEC*1000);

    free(arr);
    return 0;
}