HTML CSS JAVASCRIPT SQL MYSQL PYTHON JAVA PHP C C++ C# PACKET TRACER CYBERSECURITY REACT TYPESCRIPT NODE.JS GIT & GITHUB DATA SCIENCE BOOTSTRAP GO KOTLIN JQUERY DJANGO VUE ANGULAR MONGODB POSTGRESQL SASS NUMPY PANDAS SCIPY DSA RUST SWIFT BASHXMLW3.CSSANGULARJSAIAWSPROGRAMMAZIONEEXCEL WORD POWERPOINT
Corso gratuito

Corso DSA in italiano

Strutture dati e algoritmi spiegati da zero: notazione Big O, algoritmi di ordinamento e ricerca, liste, stack, code, hash table, alberi, grafi, cammino minimo e programmazione dinamica. Con esempi in Python ed esercizi.

Introduzione a Strutture Dati e Algoritmi

DSA sta per Data Structures and Algorithms (Strutture Dati e Algoritmi). È uno dei pilastri dell'informatica: insegna come organizzare i dati (strutture dati) e come risolvere problemi in modo efficiente (algoritmi).

ConcettoDefinizione
Struttura datiUn modo di organizzare e memorizzare i dati per usarli efficientemente (array, liste, alberi, grafi...)
AlgoritmoUna sequenza precisa di passi per risolvere un problema (ordinare, cercare, calcolare...)

Perché studiare DSA? Perché lo stesso problema può essere risolto in modi molto diversi: alcuni impiegano un secondo, altri ore. Conoscere le strutture dati e gli algoritmi giusti è ciò che distingue un programmatore esperto.

💡 DSA è anche l'argomento più richiesto nei colloqui tecnici delle grandi aziende tech (Google, Amazon, Meta...). Gli esempi di questo corso sono in Python, ma i concetti valgono per qualsiasi linguaggio.

Cos'è un algoritmo

Un algoritmo è un insieme finito di istruzioni ben definite per risolvere un problema. Deve essere: corretto (produce il risultato giusto), finito (termina) e idealmente efficiente.

Un algoritmo semplice: trovare il massimo
def trova_massimo(numeri):
    massimo = numeri[0]          # ipotesi: il primo è il max
    for n in numeri[1:]:         # scorri gli altri
        if n > massimo:          # se ne trovi uno maggiore
            massimo = n          # aggiornalo
    return massimo

print(trova_massimo([3, 7, 2, 9, 4]))   # 9

Questo algoritmo segue uno schema chiaro: inizializza, itera, confronta, aggiorna, restituisci. Analizzarne il costo (quante operazioni esegue al crescere dei dati) è il prossimo passo fondamentale.

Complessità: la notazione Big O

La complessità temporale misura come cresce il numero di operazioni di un algoritmo al crescere della dimensione dell'input n. Si esprime con la notazione Big O, che descrive il caso peggiore.

Big ONomeEsempio
O(1)CostanteAccesso a un elemento di un array
O(log n)LogaritmicaRicerca binaria
O(n)LineareRicerca lineare
O(n log n)LinearitmicaMerge Sort, Quick Sort
O(n²)QuadraticaBubble Sort, Selection Sort
O(2ⁿ)EsponenzialeFibonacci ricorsivo ingenuo
Confronto pratico di complessità
# O(1) — costante: una sola operazione
def primo(arr):
    return arr[0]

# O(n) — lineare: un ciclo
def somma(arr):
    tot = 0
    for x in arr:        # n iterazioni
        tot += x
    return tot

# O(n^2) — quadratica: ciclo dentro ciclo
def coppie(arr):
    for i in arr:        # n
        for j in arr:    # n  →  n*n
            print(i, j)
🎯 Regola pratica: per input grandi, O(n log n) è enormemente più veloce di O(n²). Con n = 1.000.000, il primo fa ~20 milioni di operazioni, il secondo mille miliardi.

Array

L'array è la struttura dati più fondamentale: una collezione di elementi memorizzati in posizioni di memoria contigue, accessibili tramite un indice numerico.

Operazioni di base sugli array
# In Python gli array sono le liste
arr = [10, 20, 30, 40, 50]

# Accesso per indice → O(1)
print(arr[2])         # 30

# Aggiornamento → O(1)
arr[2] = 99

# Inserimento in fondo → O(1) ammortizzato
arr.append(60)

# Inserimento all'inizio → O(n) (sposta tutti)
arr.insert(0, 5)

# Ricerca di un valore → O(n)
print(30 in arr)      # True

# Lunghezza → O(1)
print(len(arr))
OperazioneComplessità
Accesso per indiceO(1)
AggiornamentoO(1)
Inserimento/rimozione in fondoO(1)
Inserimento/rimozione all'inizioO(n)
RicercaO(n)

Bubble Sort

Il Bubble Sort confronta coppie di elementi adiacenti e li scambia se sono nell'ordine sbagliato. Ad ogni passata, l'elemento più grande "risale" come una bolla fino alla sua posizione finale.

Bubble Sort — O(n²)
def bubble_sort(arr):
    n = len(arr)
    for i in range(n - 1):
        scambiato = False
        for j in range(n - 1 - i):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
                scambiato = True
        if not scambiato:      # già ordinato → esci
            break
    return arr

print(bubble_sort([5, 2, 8, 1, 9, 3]))
# [1, 2, 3, 5, 8, 9]
💡 Il Bubble Sort è semplice da capire ma lento: O(n²). Si usa solo a scopo didattico o su liste piccolissime. L'ottimizzazione con scambiato lo rende O(n) se l'array è già ordinato.

Selection Sort

Il Selection Sort trova ripetutamente l'elemento minimo della parte non ordinata e lo sposta all'inizio.

Selection Sort — O(n²)
def selection_sort(arr):
    n = len(arr)
    for i in range(n):
        # trova l'indice del minimo nella parte non ordinata
        min_idx = i
        for j in range(i + 1, n):
            if arr[j] < arr[min_idx]:
                min_idx = j
        # scambia il minimo con l'elemento in posizione i
        arr[i], arr[min_idx] = arr[min_idx], arr[i]
    return arr

print(selection_sort([64, 25, 12, 22, 11]))
# [11, 12, 22, 25, 64]

A differenza del Bubble Sort, esegue al massimo n scambi, ma il numero di confronti resta O(n²).

Insertion Sort

L'Insertion Sort costruisce l'array ordinato un elemento alla volta, inserendo ogni nuovo elemento nella posizione corretta tra quelli già ordinati — come quando si ordinano le carte in mano.

Insertion Sort — O(n²)
def insertion_sort(arr):
    for i in range(1, len(arr)):
        chiave = arr[i]
        j = i - 1
        # sposta a destra gli elementi maggiori della chiave
        while j >= 0 and arr[j] > chiave:
            arr[j + 1] = arr[j]
            j -= 1
        arr[j + 1] = chiave    # inserisci la chiave
    return arr

print(insertion_sort([12, 11, 13, 5, 6]))
# [5, 6, 11, 12, 13]
🎯 L'Insertion Sort è molto efficiente su array quasi ordinati (vicino a O(n)) ed è usato internamente dagli algoritmi ibridi su piccoli sotto-array.

Quick Sort

Il Quick Sort usa la strategia divide et impera: sceglie un elemento pivot, partiziona l'array in elementi minori e maggiori del pivot, poi ordina ricorsivamente le due parti.

Quick Sort — O(n log n) medio
def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr) // 2]
    minori   = [x for x in arr if x < pivot]
    uguali   = [x for x in arr if x == pivot]
    maggiori = [x for x in arr if x > pivot]
    return quick_sort(minori) + uguali + quick_sort(maggiori)

print(quick_sort([3, 6, 8, 10, 1, 2, 1]))
# [1, 1, 2, 3, 6, 8, 10]
CasoComplessità
Migliore / medioO(n log n)
Peggiore (pivot pessimo)O(n²)
💡 Il Quick Sort è uno degli algoritmi di ordinamento più usati nella pratica grazie alla sua velocità media e all'ordinamento "in-place" (poca memoria extra).

Merge Sort

Il Merge Sort divide ricorsivamente l'array a metà finché ogni parte ha un solo elemento, poi fonde (merge) le parti in ordine. Garantisce sempre O(n log n).

Merge Sort — O(n log n) garantito
def merge_sort(arr):
    if len(arr) <= 1:
        return arr
    meta = len(arr) // 2
    sinistra = merge_sort(arr[:meta])
    destra   = merge_sort(arr[meta:])
    return merge(sinistra, destra)

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

print(merge_sort([38, 27, 43, 3, 9, 82, 10]))
# [3, 9, 10, 27, 38, 43, 82]
🎯 A differenza del Quick Sort, il Merge Sort è stabile (mantiene l'ordine relativo degli elementi uguali) e ha complessità garantita, ma richiede memoria extra O(n).

Counting Sort

Il Counting Sort non confronta gli elementi: conta quante volte appare ogni valore e ricostruisce l'array ordinato. Funziona solo su interi in un intervallo limitato, ma è velocissimo: O(n + k).

Counting Sort — O(n + k)
def counting_sort(arr):
    if not arr:
        return arr
    massimo = max(arr)
    conteggi = [0] * (massimo + 1)

    # conta le occorrenze di ogni valore
    for x in arr:
        conteggi[x] += 1

    # ricostruisci l'array ordinato
    risultato = []
    for valore, n in enumerate(conteggi):
        risultato.extend([valore] * n)
    return risultato

print(counting_sort([4, 2, 2, 8, 3, 3, 1]))
# [1, 2, 2, 3, 3, 4, 8]
⚠️ Il Counting Sort è efficiente solo se l'intervallo dei valori (k) è paragonabile al numero di elementi (n). Con valori molto grandi e sparsi diventa inefficiente in memoria.

Ricerca lineare

La ricerca lineare scorre l'array elemento per elemento finché trova il valore cercato. Funziona su qualsiasi array, ordinato o no, ma è O(n).

Ricerca lineare — O(n)
def ricerca_lineare(arr, target):
    for i in range(len(arr)):
        if arr[i] == target:
            return i          # trovato: restituisci l'indice
    return -1                 # non trovato

numeri = [4, 2, 7, 1, 9, 3]
print(ricerca_lineare(numeri, 9))   # 4
print(ricerca_lineare(numeri, 5))   # -1

Ricerca binaria

La ricerca binaria trova un valore in un array ordinato dimezzando ripetutamente lo spazio di ricerca. È estremamente veloce: O(log n).

Ricerca binaria — O(log n)
def ricerca_binaria(arr, target):
    sinistra, destra = 0, len(arr) - 1
    while sinistra <= destra:
        meta = (sinistra + destra) // 2
        if arr[meta] == target:
            return meta            # trovato
        elif arr[meta] < target:
            sinistra = meta + 1    # cerca a destra
        else:
            destra = meta - 1      # cerca a sinistra
    return -1                      # non trovato

ordinato = [1, 3, 5, 7, 9, 11, 13, 15]
print(ricerca_binaria(ordinato, 11))   # 5
print(ricerca_binaria(ordinato, 4))    # -1
💡 Con 1 milione di elementi, la ricerca lineare può richiedere 1.000.000 di confronti, la ricerca binaria al massimo 20. Ma attenzione: l'array deve essere ordinato.

Liste concatenate (Linked List)

Una lista concatenata è una sequenza di nodi, dove ogni nodo contiene un valore e un riferimento (puntatore) al nodo successivo. A differenza degli array, i nodi non sono contigui in memoria.

Lista concatenata semplice
class Nodo:
    def __init__(self, valore):
        self.valore = valore
        self.next = None        # riferimento al prossimo nodo

class ListaConcatenata:
    def __init__(self):
        self.testa = None

    def aggiungi(self, valore):           # inserimento in testa O(1)
        nuovo = Nodo(valore)
        nuovo.next = self.testa
        self.testa = nuovo

    def stampa(self):
        corrente = self.testa
        while corrente:
            print(corrente.valore, end=" -> ")
            corrente = corrente.next
        print("None")

lista = ListaConcatenata()
lista.aggiungi(3); lista.aggiungi(2); lista.aggiungi(1)
lista.stampa()    # 1 -> 2 -> 3 -> None
OperazioneArrayLinked List
Accesso per indiceO(1)O(n)
Inserimento in testaO(n)O(1)
Memoria contiguaNo

Stack e Queue

Lo Stack (pila) segue il principio LIFO (Last In, First Out): l'ultimo elemento inserito è il primo a uscire. La Queue (coda) segue il principio FIFO (First In, First Out): il primo inserito è il primo a uscire.

Stack (LIFO)
# Uno stack si implementa con una lista
stack = []

stack.append(1)    # push
stack.append(2)
stack.append(3)
print(stack)        # [1, 2, 3]

cima = stack.pop()  # pop → rimuove l'ultimo
print(cima)         # 3
print(stack)        # [1, 2]

# Casi d'uso: undo/redo, ricorsione, parsing espressioni
Queue (FIFO)
from collections import deque

# deque è ottimizzato per inserimenti/rimozioni alle estremità
queue = deque()

queue.append("a")     # enqueue
queue.append("b")
queue.append("c")

primo = queue.popleft()   # dequeue → rimuove il primo
print(primo)              # a
print(queue)              # deque(['b', 'c'])

# Casi d'uso: code di stampa, BFS, task scheduling
🎯 Usa sempre collections.deque per le code: rimuovere dall'inizio di una lista normale è O(n), mentre con deque è O(1).

Hash Table

Una Hash Table (tabella hash) memorizza coppie chiave-valore e usa una funzione hash per calcolare dove memorizzare ogni elemento. Questo permette accesso, inserimento e ricerca in tempo medio O(1).

Hash Table = dizionario Python
# In Python le hash table sono i dizionari
rubrica = {}

# Inserimento → O(1) medio
rubrica["Anna"] = "333-1234"
rubrica["Luca"] = "347-5678"

# Ricerca → O(1) medio
print(rubrica["Anna"])        # 333-1234

# Verifica presenza chiave → O(1)
print("Luca" in rubrica)      # True

# Anche i set sono hash table (solo chiavi)
visti = set()
visti.add("x")
print("x" in visti)           # True
Come funziona internamente (concetto)
def hash_semplice(chiave, dimensione):
    # somma i codici dei caratteri, poi modulo
    return sum(ord(c) for c in chiave) % dimensione

# Una tabella con 10 "secchielli" (bucket)
print(hash_semplice("Anna", 10))   # es. 8
print(hash_semplice("Luca", 10))   # es. 5

# Quando due chiavi finiscono nello stesso bucket
# si ha una COLLISIONE, gestita con liste concatenate
# (chaining) o indirizzamento aperto.
OperazioneCaso medioCaso peggiore
InserimentoO(1)O(n)
RicercaO(1)O(n)
RimozioneO(1)O(n)

Alberi (Trees)

Un albero è una struttura dati gerarchica composta da nodi collegati. Ha un nodo radice (root), e ogni nodo può avere nodi figli. Un albero binario è un albero in cui ogni nodo ha al massimo due figli.

Albero binario e attraversamenti
class Nodo:
    def __init__(self, valore):
        self.valore = valore
        self.sinistra = None
        self.destra = None

# Costruiamo questo albero:
#        1
#       / \
#      2   3
#     / \
#    4   5
radice = Nodo(1)
radice.sinistra = Nodo(2)
radice.destra = Nodo(3)
radice.sinistra.sinistra = Nodo(4)
radice.sinistra.destra = Nodo(5)

def in_order(nodo):       # sinistra → radice → destra
    if nodo:
        in_order(nodo.sinistra)
        print(nodo.valore, end=" ")
        in_order(nodo.destra)

in_order(radice)          # 4 2 5 1 3
AttraversamentoOrdine
Pre-orderRadice → Sinistra → Destra
In-orderSinistra → Radice → Destra
Post-orderSinistra → Destra → Radice

Alberi binari di ricerca (BST)

Un Binary Search Tree è un albero binario con una proprietà speciale: per ogni nodo, tutti i valori nel sottoalbero sinistro sono minori, e tutti quelli nel sottoalbero destro sono maggiori. Questo rende la ricerca O(log n).

BST: inserimento e ricerca
class Nodo:
    def __init__(self, valore):
        self.valore = valore
        self.sinistra = self.destra = None

def inserisci(radice, valore):
    if radice is None:
        return Nodo(valore)
    if valore < radice.valore:
        radice.sinistra = inserisci(radice.sinistra, valore)
    else:
        radice.destra = inserisci(radice.destra, valore)
    return radice

def cerca(radice, valore):
    if radice is None or radice.valore == valore:
        return radice
    if valore < radice.valore:
        return cerca(radice.sinistra, valore)
    return cerca(radice.destra, valore)

radice = None
for v in [50, 30, 70, 20, 40, 60, 80]:
    radice = inserisci(radice, v)

print(cerca(radice, 40) is not None)   # True
print(cerca(radice, 45) is not None)   # False
💡 Un BST sbilanciato (es. inserendo valori già ordinati) degenera in una lista e perde l'efficienza. Gli alberi AVL e i Red-Black Tree si ribilanciano automaticamente per garantire O(log n).

Grafi (Graphs)

Un grafo è un insieme di nodi (vertici) connessi da archi (edge). Modella relazioni: social network, mappe stradali, reti di computer. Può essere orientato o non orientato, pesato o no.

Grafo con lista di adiacenza + BFS
from collections import deque

# Lista di adiacenza
grafo = {
    "A": ["B", "C"],
    "B": ["A", "D", "E"],
    "C": ["A", "F"],
    "D": ["B"],
    "E": ["B", "F"],
    "F": ["C", "E"]
}

def bfs(grafo, partenza):
    visitati = set([partenza])
    coda = deque([partenza])
    ordine = []
    while coda:
        nodo = coda.popleft()
        ordine.append(nodo)
        for vicino in grafo[nodo]:
            if vicino not in visitati:
                visitati.add(vicino)
                coda.append(vicino)
    return ordine

print(bfs(grafo, "A"))    # ['A', 'B', 'C', 'D', 'E', 'F']
DFS (ricerca in profondità)
def dfs(grafo, nodo, visitati=None):
    if visitati is None:
        visitati = set()
    visitati.add(nodo)
    print(nodo, end=" ")
    for vicino in grafo[nodo]:
        if vicino not in visitati:
            dfs(grafo, vicino, visitati)

dfs(grafo, "A")   # A B D E F C
VisitaStrategiaStruttura
BFS (ampiezza)Esplora per livelliCoda (queue)
DFS (profondità)Va a fondo poi tornaStack / ricorsione

Cammino minimo (Shortest Path)

Il problema del cammino minimo cerca il percorso più breve tra due nodi di un grafo pesato. L'algoritmo più famoso è quello di Dijkstra.

Algoritmo di Dijkstra
import heapq

def dijkstra(grafo, partenza):
    # distanza iniziale: infinito per tutti tranne partenza
    dist = {nodo: float("inf") for nodo in grafo}
    dist[partenza] = 0
    coda = [(0, partenza)]    # (distanza, nodo)

    while coda:
        d, nodo = heapq.heappop(coda)
        if d > dist[nodo]:
            continue
        for vicino, peso in grafo[nodo].items():
            nuova = d + peso
            if nuova < dist[vicino]:
                dist[vicino] = nuova
                heapq.heappush(coda, (nuova, vicino))
    return dist

grafo = {
    "A": {"B": 1, "C": 4},
    "B": {"C": 2, "D": 5},
    "C": {"D": 1},
    "D": {}
}
print(dijkstra(grafo, "A"))
# {'A': 0, 'B': 1, 'C': 3, 'D': 4}
⚠️ Dijkstra non funziona con archi di peso negativo. In quel caso si usa l'algoritmo di Bellman-Ford, che è O(V·E) ma gestisce i pesi negativi.

Albero ricoprente minimo (MST)

Un Minimum Spanning Tree è il sottoinsieme di archi che collega tutti i nodi di un grafo con il peso totale minimo, senza formare cicli. Utile per progettare reti (cavi, strade) al costo minimo.

Algoritmo di Kruskal
def kruskal(nodi, archi):
    # archi = lista di (peso, nodo1, nodo2)
    archi.sort()              # ordina per peso crescente
    padre = {n: n for n in nodi}

    def trova(x):             # find con compressione
        while padre[x] != x:
            padre[x] = padre[padre[x]]
            x = padre[x]
        return x

    mst = []
    for peso, a, b in archi:
        ra, rb = trova(a), trova(b)
        if ra != rb:          # non crea ciclo
            padre[ra] = rb
            mst.append((a, b, peso))
    return mst

nodi = ["A", "B", "C", "D"]
archi = [(1,"A","B"), (3,"A","C"), (4,"B","C"),
         (2,"C","D"), (5,"B","D")]
print(kruskal(nodi, archi))
# [('A','B',1), ('C','D',2), ('A','C',3)]
💡 Esistono due algoritmi classici per l'MST: Kruskal (ordina gli archi e usa Union-Find) e Prim (cresce l'albero da un nodo usando una coda di priorità).

Memoization e Programmazione Dinamica

La programmazione dinamica (DP) risolve problemi complessi suddividendoli in sottoproblemi più semplici, e memorizzando i risultati per non ricalcolarli. Trasforma algoritmi esponenziali in polinomiali.

Fibonacci: ingenuo vs memoization
# Versione ingenua → O(2^n), lentissima
def fib_lento(n):
    if n < 2:
        return n
    return fib_lento(n-1) + fib_lento(n-2)

# Versione con memoization → O(n)
def fib_veloce(n, memo={}):
    if n < 2:
        return n
    if n not in memo:
        memo[n] = fib_veloce(n-1, memo) + fib_veloce(n-2, memo)
    return memo[n]

print(fib_veloce(50))   # 12586269025 (istantaneo)
# fib_lento(50) impiegherebbe molti minuti!
DP bottom-up (tabulation)
def fib_tabulation(n):
    if n < 2:
        return n
    tabella = [0] * (n + 1)
    tabella[1] = 1
    for i in range(2, n + 1):
        tabella[i] = tabella[i-1] + tabella[i-2]
    return tabella[n]

print(fib_tabulation(10))   # 55
ApproccioDescrizione
Memoization (top-down)Ricorsione + cache dei risultati
Tabulation (bottom-up)Riempie una tabella iterativamente
🎯 La DP si applica quando un problema ha sottoproblemi sovrapposti e sottostruttura ottima: knapsack, cammini minimi, distanza di edit, e molti problemi da colloquio.

Esercizi pratici

🏋️ Esercizio 1 — Ricerca binaria

Implementa una funzione che, dato un array ordinato e un target, restituisca True se il target è presente, usando la ricerca binaria. Testala con valori presenti e assenti.

Soluzione Esercizio 1
def contiene(arr, target):
    lo, hi = 0, len(arr) - 1
    while lo <= hi:
        mid = (lo + hi) // 2
        if arr[mid] == target:
            return True
        elif arr[mid] < target:
            lo = mid + 1
        else:
            hi = mid - 1
    return False

dati = [2, 4, 6, 8, 10, 12]
print(contiene(dati, 8))    # True
print(contiene(dati, 7))    # False
🏋️ Esercizio 2 — Bilanciamento parentesi (Stack)

Usa uno stack per verificare se una stringa di parentesi è bilanciata: ogni parentesi aperta deve essere chiusa nell'ordine corretto. Es: "({[]})" → valido.

Soluzione Esercizio 2
def bilanciata(s):
    stack = []
    coppie = {")": "(", "]": "[", "}": "{"}
    for c in s:
        if c in "([{":
            stack.append(c)
        elif c in ")]}":
            if not stack or stack.pop() != coppie[c]:
                return False
    return len(stack) == 0

print(bilanciata("({[]})"))   # True
print(bilanciata("([)]"))     # False
print(bilanciata("(("))       # False
🏋️ Esercizio 3 — Fibonacci efficiente

Scrivi una funzione che calcoli l'n-esimo numero di Fibonacci in tempo O(n) usando solo due variabili (senza ricorsione né tabella).

Soluzione Esercizio 3
def fibonacci(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a

print(fibonacci(10))   # 55
print(fibonacci(20))   # 6765
print([fibonacci(i) for i in range(10)])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

E ora?

Complimenti! Hai completato il corso di Strutture Dati e Algoritmi. Ora conosci gli strumenti fondamentali per scrivere codice efficiente e affrontare con sicurezza i colloqui tecnici.

Cosa hai imparato:
  • Analisi della complessità con la notazione Big O
  • Algoritmi di ordinamento: Bubble, Selection, Insertion, Quick, Merge, Counting
  • Ricerca lineare e binaria
  • Strutture dati: array, liste concatenate, stack, queue, hash table
  • Alberi, BST e grafi (BFS/DFS)
  • Cammino minimo (Dijkstra), MST (Kruskal) e programmazione dinamica

Il passo successivo? Mettere in pratica questi concetti risolvendo problemi reali e approfondendo con il corso di Python o quello di Data Science.

Vai al corso Python → Torna ai Corsi 📅 Prenota una lezione