Algoritmi di ordinamento
In questa pagina 9
Problema. Input: sequenza di chiavi confrontabili. Output: gli stessi elementi in ordine crescente (vedi Problemi computazionali e algoritmiProblema computazionale come insieme di coppie (istanza, soluzione); algoritmo e modello di calcolo RAM; pseudocodice; taglia di un'istanza; ADT e struttura dati concreta; esempio svolto con ricerca lineare e binaria in un array ordinato.Problemi computazionali e algoritmi →). La taglia è . Qui si studiano algoritmi basati su confronti (le chiavi si usano solo con , , , cioè l'ordine totale dei numeri reali: Campi ordinati (Q e R)Gli assiomi di somma, prodotto e ordine comuni a Q e R, da cui derivano le regole di equazioni e disequazioni.Campi ordinati (Q e R) →). Gli stessi problemi sono trattati in modo più elementare in Fondamenti di Informatica (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 →, Ordinare e cercare in C - qsort, bsearch e puntatori a funzionePuntatori a funzione e funzioni di confronto; qsort per ordinare array di interi, stringhe e struct; bsearch per la ricerca binaria; dizionario ordinato su array dinamico di struct con inserimento (memmove e realloc) e ricerca in O(log n).Ordinare e cercare in C - qsort, bsearch e puntatori a funzione →); qui l'attenzione è sull'analisi della complessità. Gli algoritmi ricorsivi seguono Algoritmi ricorsiviAlgoritmi ricorsivi come induzione eseguita; albero della ricorsione e record di attivazione nello stack; esempi ReverseArray, LinearSum, Power in tempo logaritmico, Fibonacci ricorsivo (esponenziale) e con memoizzazione; analisi della complessità con l'albero della ricorsione e correttezza per induzione.Algoritmi ricorsivi → (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 →) e le somme sono quelle di SommatorieIl simbolo di sommatoria, le sue proprietà (linearità, additività, cambio di indice) e le somme notevoli di Gauss e geometrica.Sommatorie →.
Per confrontare gli algoritmi si seguono tutti sullo stesso array (). L'ordine finale è .
Nota sulle fonti: insertion sort e selection sort compaiono nel materiale del corso (esercizi su complessità e invarianti); merge sort, quick sort, heap sort e il limite inferiore sono scritti dal programma ufficiale con conoscenze standard.
Selection sort
Si cerca il minimo della parte non ordinata e lo si porta in posizione .
Algoritmo selectionSort(S, n)
for i <- 0 to n-2 do
minIndex <- i
for k <- i+1 to n-1 do
if S[k] < S[minIndex] then minIndex <- k
swap(S[i], S[minIndex])Proprietà (selection sort: invariante e costo). Invariante all'inizio dell'iterazione : contiene gli elementi minori, già in ordine crescente (e ciascuno tutti gli elementi di ). Alla fine e tutto è ordinato. Costo: il ciclo interno fa confronti, in totale per ogni istanza, anche già ordinata. Corrisponde alla coda con priorità su lista non ordinata (vedi Code con prioritàEntry chiave-valore; ADT coda con priorità (insert, min, removeMin) con chiave minima = priorità massima; esempio di esecuzione; applicazioni; implementazioni con lista non ordinata e ordinata e relativi costi; ordinamento tramite coda con priorità.Code con priorità →).
Traccia (in grassetto la parte già ordinata; min è la posizione del minimo trovato):
| minimo di | array dopo lo scambio | confronti | |
|---|---|---|---|
| 0 | in posizione 3 | 4 | |
| 1 | in posizione 1 (resta) | 3 | |
| 2 | in posizione 3 | 2 | |
| 3 | in posizione 4 | 1 |
Totale . Il numero di scambi invece è al più (uno per iterazione): conta solo la parte di confronti.
Esempio. Con il selection sort fa confronti su qualunque array, anche già ordinato.
Insertion sort
Proprietà (insertion sort: caso pessimo e caso migliore). Si mantiene ordinato un prefisso e si inserisce al posto giusto spostando a destra i maggiori (pseudocodice e analisi in Complessità in tempo e caso pessimoPerché lo studio sperimentale non basta; complessità al caso pessimo come massimo sul numero di operazioni tra le istanze di una data taglia; stima con limiti superiore e inferiore senza trovare l'istanza peggiore; esempi arrayMax, prefixAverages e InsertionSort; efficienza asintotica e limiti dell'analisi.Complessità in tempo e caso pessimo →). Invariante: all'inizio dell'iterazione , è ordinato (e contiene gli elementi originali di ). Caso pessimo (array decrescente), caso migliore (array ordinato), veloce su sequenze quasi ordinate.
Esempio. Con servono confronti su e su . Traccia su :
curr |
prefisso ordinato prima | prefisso dopo | confronti | |
|---|---|---|---|---|
| 1 | 2 | 1 (, poi si esce dall'array) | ||
| 2 | 9 | 1 ( falso) | ||
| 3 | 1 | 3 (tutti maggiori di ) | ||
| 4 | 6 | 2 ( vero, falso) |
Totale confronti: tra il minimo e il massimo . Ogni confronto vero sposta un elemento a destra e corrisponde a una inversione (coppia fuori ordine) risolta: qui le inversioni sono , cioè . A queste si aggiunge al più un confronto falso per iterazione, quello che ferma lo scorrimento (nelle iterazioni e ; nelle altre il ciclo finisce perché si esce dall'array): . In generale confronti inversioni , per questo un array quasi ordinato, con poche inversioni, costa poco.
Merge sort
Divide et impera: si divide in due metà, si ordinano ricorsivamente, si fondono le due sequenze ordinate con un solo passaggio.
Algoritmo mergeSort(S)
Input: sequenza S di n elementi Output: S ordinata
if n <= 1 then return S
metà <- floor(n/2)
L <- mergeSort(S[0..metà-1]); R <- mergeSort(S[metà..n-1])
return merge(L, R)Formula (costo del merge sort).
merge(L, R)confronta le due teste e sposta la minore nell'output: . Costo: l'albero della ricorsione ha livelli e al livello le chiamate lavorano su sequenze di taglia , con costo totale per livello (il costo di un nodo è la fusione): per ogni istanza. Richiede di spazio ausiliario. È stabile se in caso di parità si prende l'elemento di sinistra (verificato: per già ordinato i confronti sono ).
Esempio. Per l'albero ha livelli e nel caso pessimo i confronti sono .
Come funziona merge. Si tengono due indici, uno per ogni sequenza ordinata; a ogni passo si confrontano le due teste e si copia la minore. Esempio con e : contro dà ; contro dà ; contro dà ; contro dà ; contro dà ; resta , copiato senza confronto: output con confronti, al più . Ogni confronto mette un elemento in output, quindi il costo è lineare.
Da dove viene . Sia il costo: due chiamate su metà dati più una fusione lineare, . Si "srotola": il livello ha fusione di taglia (costo ), il livello ha fusioni di taglia (costo ), il livello ha fusioni di taglia (costo ). Ogni livello costa e i livelli sono (la taglia si dimezza fino a : lo stesso conto della ricerca binaria, Problemi computazionali e algoritmiProblema computazionale come insieme di coppie (istanza, soluzione); algoritmo e modello di calcolo RAM; pseudocodice; taglia di un'istanza; ADT e struttura dati concreta; esempio svolto con ricerca lineare e binaria in un array ordinato.Problemi computazionali e algoritmi →), quindi . Traccia su : la ricorsione divide in e , poi e ; le fusioni sono ( confronto), (), (), (): totale confronti.
Quick sort
Si sceglie un pivot (ad esempio l'ultimo elemento), si partiziona in elementi e in loco con un passaggio, si mette il pivot al suo posto definitivo e si ordinano ricorsivamente le due parti.
Algoritmo quickSort(S, lo, hi)
if lo >= hi then return
p <- S[hi]; i <- lo
for j <- lo to hi-1 do
if S[j] < p then swap(S[i], S[j]); i <- i + 1
swap(S[i], S[hi])
quickSort(S, lo, i-1); quickSort(S, i+1, hi)Proprietà (costo del quick sort). . Costo: per partizione. Se il pivot divide in parti bilanciate l'albero ha altezza e il costo è ; se è sempre il minimo o il massimo (array già ordinato con pivot ultimo) il costo è . Con pivot scelto a caso il tempo atteso è : è l'esempio classico di caso pessimo patologico (vedi Complessità in tempo e caso pessimoPerché lo studio sperimentale non basta; complessità al caso pessimo come massimo sul numero di operazioni tra le istanze di una data taglia; stima con limiti superiore e inferiore senza trovare l'istanza peggiore; esempi arrayMax, prefixAverages e InsertionSort; efficienza asintotica e limiti dell'analisi.Complessità in tempo e caso pessimo →). Spazio ausiliario attesi (stack).
Esempio. Con pivot ultimo su un array già ordinato di elementi i confronti sono : . Il pivot è il massimo: tutti gli altri elementi vanno a sinistra, la parte destra è vuota e il problema scende solo di , quindi , che dà .
Traccia su con pivot ultimo: partizione con pivot : gli elementi sono e passano a sinistra, resta a destra: , pivot in posizione ( confronti). Si ordina ricorsivamente : pivot , nessuno è minore, il pivot va in testa: ( confronti); poi : pivot , , già a posto ( confronto). Totale confronti; il lato destro ha un solo elemento (caso base). Con pivot a caso la probabilità che il pivot cada nel mezzo (tra e ) è , e un pivot "buono" riduce il problema ad almeno : in media servono livelli di partizione da ciascuno, il che dà il tempo atteso (media rispetto alla scelta casuale del pivot: Valore attesoIl valore atteso E[X] = Σ x p_X(x) è la media dei valori di X pesata con le loro probabilità (esiste se la serie converge assolutamente); per una funzione g vale E[g(X)] = Σ g(x) p_X(x) senza trovare la legge di g(X), ed E è lineare: E[aX + bY + c] = aE[X] + bE[Y] + c.Valore atteso →).
Heap sort
Definizione (heap sort). Si costruisce uno heap sull'array e si estrae ripetutamente il massimo (heap sort con max-heap) mettendolo in fondo all'array: in loco.
bottom-upheap-construction: (vedi Costruzione di uno heapCostruire uno heap da un array di n entry: approccio top-down con n-1 insert, Theta(n log n); approccio bottom-up con down-heap dalle foglie verso la radice, Theta(n), con dimostrazione della somma; esempi svolti, in loco; unione di due alberi con heap-order.Costruzione di uno heap →);- per fino a : scambia con e fai down-heap su : passi da .
Esempio. Per : costruzione bottom-up e down-heap da al più passi, circa operazioni. Perché l'ordine è crescente con un max-heap: il massimo in radice viene scambiato con l'ultima posizione utile dell'array, che è la sua posizione definitiva; poi la parte ancora da sistemare si accorcia di e un down-heap ripristina l'heap (HeapAlbero binario completo e sua altezza floor(log2 n); heap = albero completo con heap-order property; proprietà (radice minima, cammini non decrescenti); rappresentazione su array con level numbering; insert con up-heap bubbling, removeMin con down-heap bubbling, rimozione di una entry qualsiasi; invarianti e costi Theta(log n); esempio svolto.Heap →). L'array è così riempito dal fondo, dal massimo al minimo, e a fine algoritmo risulta crescente senza memoria aggiuntiva.
Totale per ogni istanza e di spazio ausiliario. Equivale all'ordinamento con coda con priorità su heap (vedi HeapAlbero binario completo e sua altezza floor(log2 n); heap = albero completo con heap-order property; proprietà (radice minima, cammini non decrescenti); rappresentazione su array con level numbering; insert con up-heap bubbling, removeMin con down-heap bubbling, rimozione di una entry qualsiasi; invarianti e costi Theta(log n); esempio svolto.Heap →).
Ordinare senza confrontare
Formula (ordinamento per conteggio). Se le chiavi sono interi in si può ordinare (o contare) in con un array di contatori : una scansione che fa e una scansione di . Non usa confronti, quindi non è soggetto al limite sotto. Con è lineare (si usa nell'esercizio della mediana di chiavi in ). L'array dei contatori è una mappa con chiavi intere (Mappe e dizionariADT mappa (chiavi distinte) con get, put, remove, keySet/values/entrySet; famiglia mappa/mappa ordinata/dizionario (multimappa); applicazioni; implementazioni semplici ma poco efficienti (lista, array indicizzato dalle chiavi); dizionario realizzato con una mappa di liste.Mappe e dizionari →, array di taglia ) e il costo è la somma dei due passaggi: incrementi e una scansione di contatori.
Esempio. Con e si ottiene e quindi l'ordine : ( compare una volta), , , ; leggendo da sinistra si scrive una volta, una volta, nessun , due volte.
Limite inferiore
Teorema (limite inferiore per l'ordinamento per confronti). Ogni algoritmo di ordinamento basato su confronti richiede confronti al caso pessimo.
Esempio. Per ci sono permutazioni, quindi servono almeno confronti; per almeno .
Dimostrazione. L'esecuzione su ogni istanza è descritta da un albero di decisione binario: ogni nodo interno è un confronto , ogni foglia una permutazione dell'input prodotta come risposta. Per essere corretto l'algoritmo deve poter produrre ognuna delle permutazioni, quindi l'albero ha almeno foglie. Un albero binario con foglie ha altezza (relazione , vedi Alberi binariAlbero binario e albero binario proprio; interfaccia; relazioni tra nodi, foglie e altezza (m = n-m+1, h+1 <= m <= 2^h, 2h+1 <= n <= 2^(h+1)-1) con dimostrazioni; visita inorder; parse tree e valutazione di espressioni; heightSum come esempio di calcolo di un'informazione più ricca.Alberi binari →), perciò l'altezza è . Poiché , . La disuguaglianza si vede scrivendo e tenendo solo i primi fattori , ciascuno , scartando gli altri (tutti ); si usa il fattoriale e il numero di permutazioni di oggetti (Fattoriale e coefficienti binomialiFattoriale, permutazioni, disposizioni, combinazioni e coefficiente binomiale n su k, con il triangolo di Tartaglia.Fattoriale e coefficienti binomiali →, Calcolo combinatorio per la probabilitàNegli spazi uniformi bisogna contare: principio di moltiplicazione, disposizioni con ripetizione n^k, disposizioni semplici n!/(n−k)!, permutazioni n!, combinazioni (n su k); estrazioni con e senza reinserimento e distribuzione ipergeometrica.Calcolo combinatorio per la probabilità →). Poi del prodotto è la somma dei logaritmi, (Esponenziale e logaritmoLa funzione esponenziale a^x (base positiva diversa da 1) e la sua inversa, il logaritmo in base a, con grafici e proprietà.Esponenziale e logaritmo →) e . L'albero di decisione è un albero binario (proprio) e il legame tra foglie e altezza è la relazione di Alberi binariAlbero binario e albero binario proprio; interfaccia; relazioni tra nodi, foglie e altezza (m = n-m+1, h+1 <= m <= 2^h, 2h+1 <= n <= 2^(h+1)-1) con dimostrazioni; visita inorder; parse tree e valutazione di espressioni; heightSum come esempio di calcolo di un'informazione più ricca.Alberi binari →. Con : foglie, . L'altezza dell'albero è il numero di confronti nel caso peggiore.
Quindi merge sort e heap sort sono ottimi (asintoticamente) tra gli algoritmi basati su confronti. Il grafico seguente confronta il limite inferiore (con l'approssimazione di Stirling , accurata a meno di già da : contro ) con i confronti del merge sort: per il minimo teorico è e il merge sort ne fa al più , cioè solo l' in più.
Grafico interattivo: Limite inferiore log2(n!) per l'ordinamento per confronti (Stirling) e confronti del merge sort al caso pessimo n·log2 n − n + 1: a n = 64 sono 296 contro 321, quindi il merge sort è quasi ottimo
Grafico interattivo: Confronti al caso pessimo in funzione di n: selection sort e insertion sort fanno n(n−1)/2, il merge sort n·log₂n − n + 1 (esatto per n potenza di 2); a n = 64 sono 2016 contro 321, mentre l'insertion sort su un array già ordinato ne fa solo n − 1 = 63
Riepilogo
| Algoritmo | Caso pessimo | Caso migliore | Spazio ausiliario | In loco |
|---|---|---|---|---|
| selection sort | sì | |||
| insertion sort | sì | |||
| merge sort | no | |||
| quick sort | (atteso ) | sì | ||
| heap sort | sì |
Errori comuni
- Dire che il quick sort è al caso pessimo: lo è atteso (o con buon pivot); al caso pessimo è quadratico.
- Confondere il caso pessimo con il caso migliore dell'insertion sort (lineare solo su array ordinato).
- Dimenticare che l'invariante di selection sort riguarda anche i valori, non solo l'ordine: il prefisso contiene gli elementi più piccoli.
- Applicare il limite ad algoritmi che non usano confronti.
Versione ripasso
- Problema: ordinare con confronti (, , ); taglia (vedi Problemi computazionali e algoritmiProblema computazionale come insieme di coppie (istanza, soluzione); algoritmo e modello di calcolo RAM; pseudocodice; taglia di un'istanza; ADT e struttura dati concreta; esempio svolto con ricerca lineare e binaria in un array ordinato.Problemi computazionali e algoritmi →). Esempio di riferimento , ordine finale .
- Selection sort: per ogni si cerca il minimo di e lo si scambia in posizione . Invariante: contiene gli minimi, ordinati. Confronti sempre, ; su sono (coda con priorità su lista non ordinata, vedi Code con prioritàEntry chiave-valore; ADT coda con priorità (insert, min, removeMin) con chiave minima = priorità massima; esempio di esecuzione; applicazioni; implementazioni con lista non ordinata e ordinata e relativi costi; ordinamento tramite coda con priorità.Code con priorità →).
- Insertion sort: invariante ordinato; si inserisce spostando a destra i maggiori. Pessimo (array decrescente), migliore (ordinato), veloce su sequenze quasi ordinate (vedi Complessità in tempo e caso pessimoPerché lo studio sperimentale non basta; complessità al caso pessimo come massimo sul numero di operazioni tra le istanze di una data taglia; stima con limiti superiore e inferiore senza trovare l'istanza peggiore; esempi arrayMax, prefixAverages e InsertionSort; efficienza asintotica e limiti dell'analisi.Complessità in tempo e caso pessimo →). Confronti inversioni : su sono inversioni e confronti.
- Merge sort: se ritorna; altrimenti ordina ricorsivamente le metà e e restituisce
merge(L, R).mergeconfronta le due teste e copia la minore, costo . Ricorrenza : livelli, costo per livello, quindi per ogni istanza; spazio . Su : confronti. Con e : confronti. - Quick sort: pivot ; partizione in loco con ; pivot in posizione ; ricorsione su e . Partizione ; pivot bilanciato ; pessimo : array già ordinato con pivot ultimo dà e confronti. Con pivot casuale il tempo atteso è (vedi Valore attesoIl valore atteso E[X] = Σ x p_X(x) è la media dei valori di X pesata con le loro probabilità (esiste se la serie converge assolutamente); per una funzione g vale E[g(X)] = Σ g(x) p_X(x) senza trovare la legge di g(X), ed E è lineare: E[aX + bY + c] = aE[X] + bE[Y] + c.Valore atteso →). Spazio atteso. Su : confronti.
- Heap sort: bottom-up (vedi Costruzione di uno heapCostruire uno heap da un array di n entry: approccio top-down con n-1 insert, Theta(n log n); approccio bottom-up con down-heap dalle foglie verso la radice, Theta(n), con dimostrazione della somma; esempi svolti, in loco; unione di due alberi con heap-order.Costruzione di uno heap →), poi per fino a : scambia con e down-heap su con max-heap. , in loco, di spazio (vedi HeapAlbero binario completo e sua altezza floor(log2 n); heap = albero completo con heap-order property; proprietà (radice minima, cammini non decrescenti); rappresentazione su array con level numbering; insert con up-heap bubbling, removeMin con down-heap bubbling, rimozione di una entry qualsiasi; invarianti e costi Theta(log n); esempio svolto.Heap →). Per : down-heap da al più passi.
- Senza confronti: chiavi intere in : contatori con e scansione di ; (vedi Mappe e dizionariADT mappa (chiavi distinte) con get, put, remove, keySet/values/entrySet; famiglia mappa/mappa ordinata/dizionario (multimappa); applicazioni; implementazioni semplici ma poco efficienti (lista, array indicizzato dalle chiavi); dizionario realizzato con una mappa di liste.Mappe e dizionari →). Esempio , : , ordine .
- Limite inferiore: albero di decisione con almeno foglie; altezza (relazione di Alberi binariAlbero binario e albero binario proprio; interfaccia; relazioni tra nodi, foglie e altezza (m = n-m+1, h+1 <= m <= 2^h, 2h+1 <= n <= 2^(h+1)-1) con dimostrazioni; visita inorder; parse tree e valutazione di espressioni; heightSum come esempio di calcolo di un'informazione più ricca.Alberi binari →), quindi confronti nel caso pessimo. Per : . Merge sort e heap sort sono ottimi.
- Riepilogo: caso pessimo selection , insertion , merge , quick (atteso ), heap ; spazio ausiliario , , , , .
- Traccia selection sort su : : minimo dalla posizione , array ; : già al suo posto; : minimo , array ; : minimo , array .
- Traccia insertion sort su : il prefisso ordinato passa da a , , e infine , con confronti.
- Traccia merge sort su : la ricorsione divide in e , poi in e . Le fusioni sono ( confronto), (), () e (): totale .
- Traccia quick sort su : pivot con partizione e pivot in posizione ( confronti); poi con pivot dà ( confronti) e dà confronto. Totale .
- Caso migliore: quick sort con partizioni bilanciate, ; è il caso di pivot vicino alla mediana.
- Errori tipici: quick sort nel caso pessimo; caso migliore e pessimo dell'insertion sort invertiti; limite applicato ad algoritmi senza confronti.
Esercizi su questo argomento
- Esercizio - shortest-first job scheduling, proprietà SJF e sottostruttura (esercitazione greedy, es. 10.2)
- Esercizio 4 · ordinamento con scambi, traccia e invariante
- Esercizio 14 · k-esimo elemento più piccolo con uno heap
- Esercizio 29 · C, centralina meteo (lista ordinata)
- Esercizio - inversioni di una permutazione casuale e confronti dell'insertion sort
Teoria collegata
- Albero della ricorrenza e formula generale per le ricorrenze
- Paradigma greedy - scelta golosa, sottostruttura e selezione delle attività
- Code con priorità
- Costruzione di uno heap
- Minimum spanning tree
- Alberi di decisione
- Classificazione e k-nearest neighbors
- Algoritmi randomizzati - Monte Carlo, Las Vegas e amplificazione
- Analisi probabilistica di quicksort e quickselect
- Momenti e deviazioni - Markov, Chebyshev e mediana randomizzata