0Pricing
DSA Interview Prep · Lezione

La notazione Big-O dalle basi

Capisca perché è importante la crescita asintotica, come eliminare costanti e termini di ordine inferiore e come interpretare Big-O a colpo d'occhio

La notazione Big-O dalle basi è una lezione DSA Interview Prep gratuita su CoddyKit. Questa è la lezione 1 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento DSA Interview Prep, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso DSA Interview Prep include 4 lezioni in totale.

Perché misurare l'efficienza degli algoritmi?

Due programmi possono essere entrambi corretti, eppure uno termina in un attimo mentre l'altro impiega ore. La complessità temporale descrive come cresce il tempo di esecuzione all'aumentare dell'input.

# O(n) approach
def find_max_linear(nums):
    m = nums[0]
    for n in nums:
        if n > m: m = n
    return m

# O(n^2) approach (unnecessary double loop)
def find_max_quadratic(nums):
    for i in range(len(nums)):
        is_max = all(nums[i] >= nums[j] for j in range(len(nums)))
        if is_max: return nums[i]

print(find_max_linear([3, 1, 4, 1, 5, 9]))  # 9

Big-O: limite superiore asintotico

Big-O descrive il limite superiore nel caso peggiore della crescita del costo. Il punto è eliminare costanti e termini di ordine inferiore, perché su larga scala conta solo il termine dominante. Veda il codice.

# T(n) = 3n^2 + 5n + 100 is O(n^2)
# because the n^2 term dominates for large n

# T(n) = 2n + 1000 is O(n)
# the constant 1000 becomes negligible

# Rule: drop constants and lower-order terms
# 5n^3 + 2n^2 + n + 1  =>  O(n^3)
# 100 * log(n) + n      =>  O(n)
print('O(n^2) example: counting iterations')
n = 1000
count = sum(1 for i in range(n) for j in range(n))
print(count)  # 1_000_000 = n^2

Classi di complessità comuni

Dalla più veloce alla più lenta: O(1), O(log n), O(n), O(n log n), O(n^2), O(2^n), O(n!). Conoscerle consente di scegliere l'approccio giusto prima di scrivere una sola riga.

import math

n = 1000
print(f'O(1):       {1}')
print(f'O(log n):   {int(math.log2(n))}')
print(f'O(n):       {n}')
print(f'O(n log n): {int(n * math.log2(n))}')
print(f'O(n^2):     {n**2}')
# O(2^n) for n=1000 is astronomically large
# O(n!) even larger

Perché si eliminano le costanti

Eseguire 5n passaggi o 2n passaggi è comunque O(n): le costanti dipendono dall'hardware, non dall'algoritmo. La notazione Big-O le elimina, così è possibile confrontare la crescita in condizioni equivalenti.

# Both are O(n) — different constants
def count_a(n):
    total = 0
    for i in range(n):   # n ops
        total += 1
    for i in range(n):   # n ops
        total += 1
    return total  # T(n) = 2n  =>  O(n)

def count_b(n):
    total = 0
    for i in range(5 * n):  # 5n ops
        total += 1
    return total  # T(n) = 5n  =>  O(n)

print(count_a(10), count_b(10))  # 20 50

Casi migliore, medio e peggiore

Big-O descrive il caso peggiore; Omega descrive il caso migliore; Theta è un limite asintotico stretto per entrambi. Quando un intervistatore chiede "la complessità", quasi sempre intende il caso peggiore.

def linear_search(nums, target):
    for i, n in enumerate(nums):
        if n == target:
            return i  # best case: target at index 0 => O(1)
    return -1         # worst case: not found => O(n)

# Best case O(1): target is first element
print(linear_search([5,1,2,3], 5))   # 0

# Worst case O(n): target not in list
print(linear_search([1,2,3,4], 9))   # -1

O(log n): dimezzare lo spazio di ricerca

Un algoritmo è O(log n) quando dimezza l'input a ogni passaggio, come nella ricerca binaria. Anche con un miliardo di elementi servono solo circa 30 passaggi: è incredibilmente veloce. Veda il codice.

def binary_search(arr, target):
    lo, hi = 0, len(arr) - 1
    steps = 0
    while lo <= hi:
        steps += 1
        mid = (lo + hi) // 2
        if arr[mid] == target:
            return mid, steps
        elif arr[mid] < target:
            lo = mid + 1
        else:
            hi = mid - 1
    return -1, steps

import math
arr = list(range(1000))
idx, s = binary_search(arr, 999)
print(f'Found at {idx} in {s} steps (log2(1000)~={math.log2(1000):.1f})')

O(n log n): limite inferiore dell'ordinamento

Qualsiasi algoritmo di ordinamento per confronto richiede almeno O(n log n) nel caso peggiore: è un vero limite inferiore matematico. Quindi ordinare e poi scorrere ha complessità complessiva O(n log n), non O(n^2). Il codice mostra il merge sort.

# Merge sort: O(n log n)
def merge_sort(arr):
    if len(arr) <= 1:
        return arr
    mid = len(arr) // 2
    left  = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])
    return merge(left, right)

def merge(a, b):
    res, i, j = [], 0, 0
    while i < len(a) and j < len(b):
        if a[i] <= b[j]: res.append(a[i]); i+=1
        else:             res.append(b[j]); j+=1
    return res + a[i:] + b[j:]

print(merge_sort([5,2,8,1,9,3]))  # [1,2,3,5,8,9]

Complessità ammortizzata

L'analisi ammortizzata calcola il costo medio su molte operazioni. L'append di Python è O(1) ammortizzato: di solito è immediato, mentre il raro ridimensionamento O(n) viene distribuito su tutte le operazioni di append.

# Dynamic array append is O(1) amortised
import sys

lst = []
capacities = []
for i in range(16):
    lst.append(i)
    capacities.append(sys.getsizeof(lst))

# Size jumps show reallocation events
for i, c in enumerate(capacities):
    if i > 0 and capacities[i] != capacities[i-1]:
        print(f'Realloc at i={i}, new size={c} bytes')

Riconoscere la complessità nel codice

Una regola rapida: conti i cicli. Un ciclo è O(n), due cicli annidati sono O(n^2), un ciclo che dimezza l'intervallo è O(log n). Le scansioni indipendenti si sommano; solo i cicli annidati si moltiplicano. Veda il codice.

# Two independent passes: O(n) + O(n) = O(n)
def two_passes(nums):
    total = sum(nums)           # O(n)
    mean = total / len(nums)
    diffs = [abs(n - mean) for n in nums]  # O(n)
    return max(diffs)           # O(n)
# Overall: O(n) -- NOT O(n^2)

# Nested loops: O(n) * O(n) = O(n^2)
def all_pairs(nums):
    pairs = []
    for i in range(len(nums)):       # O(n)
        for j in range(i+1, len(nums)): # O(n)
            pairs.append((nums[i], nums[j]))
    return pairs  # O(n^2)

Fondamenti della complessità spaziale

La complessità spaziale misura la memoria aggiuntiva utilizzata oltre all'input. Un'inversione in-place è O(1); una mappa hash è O(n). Quando scambia tempo con spazio, indichi sempre entrambi.

# O(1) space: reverse in-place
def reverse_inplace(arr):
    l, r = 0, len(arr) - 1
    while l < r:
        arr[l], arr[r] = arr[r], arr[l]
        l += 1; r -= 1

# O(n) space: create reversed copy
def reverse_copy(arr):
    return arr[::-1]

a = [1, 2, 3, 4, 5]
reverse_inplace(a)
print(a)  # [5, 4, 3, 2, 1]

Parlare della complessità nei colloqui

Dichiari sempre la complessità senza aspettare che gliela chiedano: "Tempo O(n log n), spazio O(n)". Poi proponga un'alternativa più veloce. Questa abitudine segnala una reale esperienza.

# Example of explaining complexity step by step
def two_sum(nums, target):
    # O(n) time: one pass through nums
    # O(n) space: hash map stores up to n elements
    seen = {}  # value -> index
    for i, n in enumerate(nums):
        complement = target - n
        if complement in seen:   # O(1) lookup
            return [seen[complement], i]
        seen[n] = i
    return []

print(two_sum([2, 7, 11, 15], 9))  # [0, 1]

Verifica rapida

Verifica rapida: dimostri ciò che ha assimilato su Big-O e sulle classi di complessità. Una domanda: ce la può fare. 🎯

Riepilogo della lezione

Riepilogo: Big-O descrive la crescita nel caso peggiore eliminando le costanti; ora conosce le classi da O(1) a O(n!), e sa che i cicli indipendenti si sommano mentre quelli annidati si moltiplicano.

Domande Frequenti

La lezione «La notazione Big-O dalle basi» è gratuita?

Sì — il testo completo di «La notazione Big-O dalle basi» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso DSA Interview Prep, passa a CoddyKit PRO. Il corso DSA Interview Prep include 4 lezioni in totale.

Cosa imparerò in «La notazione Big-O dalle basi»?

Capisca perché è importante la crescita asintotica, come eliminare costanti e termini di ordine inferiore e come interpretare Big-O a colpo d'occhio Eserciti DSA Interview Prep con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.

Ho bisogno di esperienza per iniziare DSA Interview Prep?

Non è richiesta alcuna esperienza precedente. DSA Interview Prep su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 1 di 4.

Quanto tempo richiede la lezione «La notazione Big-O dalle basi»?

La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.

Posso scrivere ed eseguire codice in questa lezione DSA Interview Prep?

Sì. Ogni lezione DSA Interview Prep include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.

Tutte le lezioni di questo corso

  1. La notazione Big-O dalle basi
  2. Analizzare cicli e cicli annidati
  3. Ricorsione e metodo dell'albero ricorsivo
  4. Complessità spaziale e compromessi
← Torna a DSA Interview Prep