Salta al contenuto
Note per Studenti Algoritmi di ordinamento

Algoritmi di ordinamento

In questa pagina 7
In questa pagina 7

Problema: disporre gli elementi di una lista in ordine non decrescente. Tutti gli algoritmi qui sotto ordinano per confronti (<, <=), quindi funzionano con numeri, stringhe, tuple.

Proprietà da considerare:

Definizione (proprietà degli algoritmi di ordinamento)

Esempio. Ordinando [("b", 1), ("a", 1)] per il secondo campo con un algoritmo stabile, ("b", 1) resta prima di ("a", 1).

Selection sort

A ogni passo i si cerca il minimo di v[i:] e lo si scambia con v[i].

python
def selection_sort(v):
    n = len(v)
    for i in range(n - 1):
        im = i
        for j in range(i + 1, n):
            if v[j] < v[im]:
                im = j
        v[i], v[im] = v[im], v[i]

Proprietà (selection sort)

  • Invariante: v[:i] contiene gli i elementi più piccoli, in ordine.
  • Confronti: n(n−1)/2n(n-1)/2 sempre → O(n2)O(n^2) in tutti i casi. Al più n−1n - 1 scambi.
  • Sul posto, non stabile.

Esempio. Con v = [5, 2, 4] il selection sort esegue 3⋅2/2=33\cdot2/2=3 confronti e porta v a [2, 4, 5].

Insertion sort

Si scorre la lista; ogni elemento viene inserito al posto giusto nella parte iniziale già ordinata, spostando a destra i maggiori.

python
def insertion_sort(v):
    for i in range(1, len(v)):
        x = v[i]
        j = i - 1
        while j >= 0 and v[j] > x:
            v[j + 1] = v[j]
            j -= 1
        v[j + 1] = x

Proprietà (insertion sort)

  • Invariante: v[:i] è ordinata (ma contiene gli elementi originali di v[:i], non i più piccoli).
  • Caso peggiore (lista al contrario) O(n2)O(n^2); caso migliore (già ordinata) O(n)O(n): molto buono su liste quasi ordinate.
  • Sul posto, stabile (grazie a > stretto nel while).

Esempio. L'insertion sort su [1, 2, 3, 4] esegue 33 confronti (O(n)O(n)), su [4, 3, 2, 1] ne esegue 66 (O(n2)O(n^2)).

Bubble sort

Si scorrono le coppie adiacenti scambiando quelle fuori ordine; dopo il passo k gli ultimi k elementi sono definitivi. Con un flag ci si ferma se in un passo non ci sono scambi.

python
def bubble_sort(v):
    n = len(v)
    for k in range(n - 1):
        scambi = False
        for j in range(n - 1 - k):
            if v[j] > v[j + 1]:
                v[j], v[j + 1] = v[j + 1], v[j]
                scambi = True
        if not scambi:
            break

Proprietà (bubble sort). O(n2)O(n^2) nel caso peggiore, O(n)O(n) se già ordinata; sul posto e stabile. Più lento di insertion sort in pratica.

Esempio. Con il flag, bubble_sort([1, 2, 3]) si ferma dopo un solo passo di 22 confronti: costo O(n)O(n).

Merge sort

Divide et impera (RicorsioneFunzioni che chiamano se stesse: caso base e passo ricorsivo, stack delle chiamate, esempi su numeri, stringhe e liste, ricorsione multipla e suo costo, divide et impera, confronto con l'iterazione.Ricorsione →): si divide la lista a metà, si ordinano ricorsivamente le due metà, si fondono le due metà ordinate (fusione con due indici, vedi Pattern algoritmici iterativiSchemi ricorrenti nei cicli: accumulatore, contatore, massimo e minimo, ricerca con uscita anticipata, verifica universale, filtro e trasformazione, finestra scorrevole, due indici, elaborazione di coppie.Pattern algoritmici iterativi →).

python
def merge_sort(v):
    if len(v) <= 1:
        return v
    m = len(v) // 2
    a = merge_sort(v[:m])
    b = merge_sort(v[m:])
    # fusione
    out, i, j = [], 0, 0
    while i < len(a) and j < len(b):
        if a[i] <= b[j]:        # <= rende l'algoritmo stabile
            out.append(a[i]); i += 1
        else:
            out.append(b[j]); j += 1
    out.extend(a[i:])
    out.extend(b[j:])
    return out

Proprietà (merge sort: costo e memoria)

  • Costo T(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n) → O(nlog⁡n)O(n \log n) in tutti i casi: log⁡2n\log_2 n livelli di ricorsione, ciascuno con lavoro complessivo O(n)O(n) per le fusioni.
  • Memoria aggiuntiva O(n)O(n) (non sul posto); stabile.
  • Questa versione restituisce una lista nuova: v = merge_sort(v).

Esempio. merge_sort([5, 2, 4, 1]) restituisce [1, 2, 4, 5], con log⁡24=2\log_24=2 livelli di divisione.

Confronto

Algoritmo Peggiore Migliore Sul posto Stabile
Selection sort O(n2)O(n^2) O(n2)O(n^2) sì no
Insertion sort O(n2)O(n^2) O(n)O(n) sì sì
Bubble sort (con flag) O(n2)O(n^2) O(n)O(n) sì sì
Merge sort O(nlog⁡n)O(n \log n) O(nlog⁡n)O(n \log n) no sì

Teorema (limite inferiore per l'ordinamento per confronti). Nessun ordinamento per confronti può fare meglio di O(nlog⁡n)O(n \log n) nel caso peggiore.

Esempio. Per ordinare n=1000n=1000 elementi servono nel caso peggiore almeno log⁡21000!≈8529\log_21000!\approx8529 confronti.

In Python: sort e sorted

Definizione (sort e sorted in Python). Nella pratica si usa l'ordinamento predefinito (Timsort: O(nlog⁡n)O(n \log n), stabile, O(n)O(n) su dati già ordinati).

v.sort()                                  # sul posto, restituisce None
w = sorted(v)                             # nuova lista
sorted(v, reverse=True)                   # decrescente
sorted(parole, key=str.lower)             # ignorando maiuscole/minuscole
sorted(studenti, key=lambda s: (-s[1], s[0]))   # voto decrescente, poi nome

Esempio. sorted([3, 1, 2], reverse=True) restituisce [3, 2, 1] e sorted(["b", "A", "c"], key=str.lower) restituisce ['A', 'b', 'c'].

key è una funzione applicata a ogni elemento: si confrontano i risultati. La stabilità permette ordinamenti a più livelli in passaggi successivi.

Errori tipici

Versione ripasso

Problema: disporre gli elementi di una lista in ordine non decrescente (per confronti <,<=).

Proprietà:

Selection sort

Cerca il minimo di v[i:] e lo scambia con v[i].

  • Confronti: n(n−1)/2n(n-1)/2 sempre →O(n2)\to O(n^2) ovunque.
  • Sul posto, non stabile.

Insertion sort

Inserisce ogni elemento al posto giusto nella parte iniziale già ordinata, spostando a destra i maggiori.

  • Caso peggiore O(n2)O(n^2), migliore O(n)O(n) (liste quasi ordinate).
  • Sul posto, stabile.

Bubble sort

Scambia coppie adiacenti fuori ordine; con un flag si ferma se non ci sono scambi.

  • Peggiore O(n2)O(n^2), migliore O(n)O(n) (già ordinata).
  • Sul posto e stabile.

Merge sort

Divide et impera (RicorsioneFunzioni che chiamano se stesse: caso base e passo ricorsivo, stack delle chiamate, esempi su numeri, stringhe e liste, ricorsione multipla e suo costo, divide et impera, confronto con l'iterazione.Ricorsione →): divide a metà, ordina ricorsivamente e fonde (Pattern algoritmici iterativiSchemi ricorrenti nei cicli: accumulatore, contatore, massimo e minimo, ricerca con uscita anticipata, verifica universale, filtro e trasformazione, finestra scorrevole, due indici, elaborazione di coppie.Pattern algoritmici iterativi →).

  • Costo T(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n) →O(nlog⁡n)\to O(n \log n) in tutti i casi.
  • Memoria aggiuntiva O(n)O(n) (non sul posto), stabile.

Tabella riassuntiva

Algoritmo Peggiore Migliore Sul posto Stabile
Selection sort O(n2)O(n^2) O(n2)O(n^2) sì no
Insertion sort O(n2)O(n^2) O(n)O(n) sì sì
Bubble sort (con flag) O(n2)O(n^2) O(n)O(n) sì sì
Merge sort O(nlog⁡n)O(n \log n) O(nlog⁡n)O(n \log n) no sì

Nessun ordinamento per confronti supera O(nlog⁡n)O(n \log n) nel peggiore dei casi.

In Python: sort e sorted

Timsort: O(nlog⁡n)O(n \log n), stabile, O(n)O(n) su dati già ordinati.

  • v.sort(): sul posto, restituisce None.
  • w = sorted(v): nuova lista.
  • sorted(v, reverse=True): decrescente.
  • sorted(v, key=...): confronta i risultati della funzione key.

Errori tipici

Esercizi su questo argomento

Teoria collegata