













Studia grazie alle numerose risorse presenti su Docsity
Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium
Prepara i tuoi esami
Studia grazie alle numerose risorse presenti su Docsity
Prepara i tuoi esami con i documenti condivisi da studenti come te su Docsity
Trova i documenti specifici per gli esami della tua università
Preparati con lezioni e prove svolte basate sui programmi universitari!
Rispondi a reali domande d’esame e scopri la tua preparazione
Riassumi i tuoi documenti, fagli domande, convertili in quiz e mappe concettuali
Studia con prove svolte, tesine e consigli utili
Togliti ogni dubbio leggendo le risposte alle domande fatte da altri studenti come te
Esplora i documenti più scaricati per gli argomenti di studio più popolari
Ottieni i punti per scaricare
Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium
varie tipologia di algoritmi di ordinamento
Tipologia: Dispense
1 / 21
Questa pagina non è visibile nell’anteprima
Non perderti parti importanti!














1 Introduzione
Il problema dell’ordinamento di un insieme è un problema classico dell’informatica che, oltre ad avere una indiscutibile valenza in ambito applicativo, tanto che spesso si ritrova il problema dell’ordinamento all’interno di problemi ben più complicati, è anche un utilissimo strumento didattico. Infatti il problema in sé è molto sempli- ce e chiunque è in grado di comprenderne i termini essenziali e per la sua risolu- zione sono stati proposti numerosissimi algoritmi molto eleganti che, se presenta- ti in successione in modo opportuno, consentono di evidenziare gli aspetti fonda- mentali della progettazione e della costruzione di un algoritmo efficiente, di bassa complessità computazionale. Il problema dell’ordinamento ( sort ) può dunque essere posto nei seguenti ter- mini: dato un insieme di elementi qualsiasi A = { a 1 , a 2 ,... , an } su cui sia possibile definire una relazione di ordine totale (ossia una relazione riflessiva, antisimmetrica e transitiva definita su ogni coppia di elementi dell’insieme), che indicheremo co- me di consueto con il simbolo “≤”, si richiede di produrre una permutazione degli elementi dell’insieme, in modo tale che aih ≤ aik per ogni h ≤ k , h , k = 1, 2,... , n. Ad esempio, se consideriamo l’insieme costituito da n = 4 numeri naturali A = { a 1 = 7, a 2 = 3, a 3 = 15, a 4 = 6}, allora la soluzione del problema dell’ordinamento è data dalla permutazione ( a 2 , a 4 , a 1 , a 3 ). La soluzione del problema è unica a meno di ele- menti uguali. Ad esempio nel caso dell’insieme A = { a 1 = 7, a 2 = 3, a 3 = 15, a 4 = 7}, il problema ammette due soluzioni equivalenti date dalle permutazioni ( a 2 , a 1 , a 4 , a 3 ) e ( a 2 , a 4 , a 1 , a 3 ). Nelle pagine seguenti esamineremo una serie di algoritmi via via sempre più so- fisticati (e dunque di complessità computazionale sempre più bassa) che risolvono il problema in modo efficiente. La maggior parte degli algoritmi presentati operano esclusivamente sulla base del confronto dei valori dell’insieme da ordinare, mentre altri, proposti al termine della rassegna, risolvono il problema in modo ancora più efficiente utilizzando alcune informazioni aggiuntive sull’insieme da ordinare (ad esempio sulla presenza di elementi duplicati, sul valore minimo e il valore massimo all’interno dell’insieme, o altre informazioni che potrebbero consentire di introdur-
∗[email protected] – http://www.mat.uniroma3.it/users/liverani. Ultima revisione del 25 novembre 2012.
Algoritmo Caso migliore Caso medio Caso peggiore SELECTIONSORT n^2 − n^2 INSERTIONSORT n − n^2 BUBBLESORT n − n^2 QUICKSORT n log 2 n n log 2 n n^2 MERGESORT n log 2 n n log 2 n n log 2 n HEAPSORT n log 2 n n log 2 n n log 2 n COUNTINGSORT n n n BUCKETSORT − − n
Tabella 1: Confronto della complessità computazionale degli algoritmi esaminati
re delle ottimizzazioni nell’algoritmo, in grado di ridurre in modo significativo la complessità dell’algoritmo stesso). I primi tre algoritmi analizzati (SELECTIONSORT, INSERTIONSORT e BUBBLESORT) sono estremamente elementari e consentono un’implementazione assai semplice; il costo da pagare alla semplicità di questi algoritmi sta ovviamente nell’elevata com- plessità computazionale, che in questi casi è O ( n^2 ). L’algoritmo QUICKSORT con- sente di raggiungere una complessità di O ( n log 2 n ) nel caso medio, mentre nel ca- so più sfavorevole ritorna ad una complessità di O ( n^2 ). Gli algoritmi MERGESORT e HEAPSORT consentono di raggiungere una complessità di O ( n log 2 n ) anche nel caso peggiore e, dal momento che è possibile dimostrare che il limite inferiore al- la complessità computazionale del problema dell’ordinamento mediante confronti (senza dunque poter sfruttare altre informazioni sull’insieme da ordinare) è proprio pari a n log 2 n , possiamo concludere che tali algoritmi sono ottimi. Gli algoritmi COUNTINGSORT e BUCKETSORT sono invece basati su altri criteri e strategie, diverse dal confronto fra i valori degli elementi dell’insieme da ordinare, e sfruttano pertan- to altre informazioni sui dati in input; grazie a questo consentono di ridurre ulte- riormente la complessità nel caso peggiore, arrivando ad una complessità lineare di O ( n ). Sotto a tale soglia è impossibile scendere, dal momento che per ordinare un insieme di n elementi è necessario esaminarli tutti almeno una volta. Nella Tabella 1 è riportata una sintesi della stima della complessità degli algoritmi analizzati in que- ste pagine. Nel seguito ci concentreremo soltanto nello studio della complessità nel caso peggiore, dal momento che è il parametro più conservativo per la valutazione dell’efficienza di un algoritmo. Gli algoritmi sono presentati in pseudo-codice, ma è molto semplice tradurre ta- le codifica in uno specifico linguaggio di programmazione, come C, Java, Pascal, Perl o altri ancora. Per la rappresentazione delle informazioni la via più semplice è quel- la di utilizzare degli array, ma in alcuni casi anche una semplice lista concatenata renderà possibile una codifica molto efficace.
L’Algoritmo 1 riporta una pseudo-codifica di SELECTIONSORT. L’insieme da or- dinare è l’insieme A e la variabile n rappresenta il numero di elementi di A : | A | = n.
SELECTIONSORT( A , n )
1: per i = 1, 2,... , n ripeti 2: per j = i + 1,... , n ripeti 3: se a (^) j < ai allora 4: scambia ai e a (^) j 5: fine-condizione 6: fine-ciclo 7: fine-ciclo Algoritmo 1: Selection sort
L’algoritmo produce un ordinamento corretto dell’insieme A. Infatti, se per as- surdo al termine dell’algoritmo esistessero due elementi ah e ak tali che h < k e ah > ak , allora al passo h del ciclo esterno controllato dalla variabile i , eseguen- do il ciclo interno, controllato dalla variabile j , al passo 3 si sarebbe evidenziata questa situazione anomala ( a (^) j = k < ai = h ) e dunque i due elementi sarebbero stati scambiati di posizione; il che contraddice l’ipotesi fatta per assurdo che al termi- ne dell’algoritmo i due elementi si trovassero in ordine reciproco opposto a quello dell’ordinamento. Dal punto di vista del numero di operazioni elementari svolte dall’algoritmo non esiste un caso particolarmente favorevole o, al contrario, sfavorevole: qualunque sia la disposizione iniziale degli elementi della sequenza da ordinare, se la cardinalità dell’insieme è pari a n allora il numero di operazioni effettuate dall’algoritmo per or- dinare la sequenza è O ( n^2 ). Infatti il ciclo esterno (quello controllato dalla variabile i nell’Algoritmo 1) esegue esattamente n iterazioni; ad ogni iterazione del ciclo ester- no il ciclo più interno (quello controllato dalla variabile j ) esegue n − i iterazioni. Dunque alla prima ripetizione del ciclo esterno vengono compiute n − 1 operazioni con il ciclo interno; alla seconda ripetizione del ciclo esterno vengono eseguite n − 2 operazioni con il ciclo interno, e così via fino a quando i = n. Dunque il numero di operazioni compiute nel complesso dall’algoritmo SELECTIONSORT è pari a
( n − 1) + ( n − 2) +... + ( n − n − 1) + ( n − n ) =
∑^ n i = 1
n − i =
n ( n − 1) 2
Quindi possiamo concludere che la complessità computazionale dell’algoritmo è O ( n^2 ). È interessante osservare che l’algoritmo SELECTIONSORT opera in modo “cieco”, senza sfruttare in alcun modo un eventuale ordinamento parziale degli elementi dell’insieme. Ad esempio, anche nel caso in cui l’insieme da ordinare sia già com- pletamente ordinato, l’algoritmo eseguirà tutte le iterazioni dei due cicli nidificati,
con il solo vantaggio di non scambiare mai tra loro gli elementi presi in esame. Tut- tavia anche in questo caso apparentemente così favorevole, l’algoritmo esegue un numero di operazioni dell’ordine di O ( n^2 ).
3 Insertion sort
L’idea su cui si fonda l’algoritmo INSERTIONSORT è quella di collocare uno dopo l’altro tutti gli elementi ai dell’insieme nella posizione corretta del sottoinsieme già ordinato costituito dagli elementi a 1 , a 2 ,... , ai − 1 , inserendolo facendo scorrere a destra gli elementi maggiori. Ad esempio supponiamo di voler ordinare utilizzando questa strategia l’insieme A = {5, 3, 2, 1, 4}. Di seguito riportiamo i passi necessari per risolvere il problema:
Per inserire nella posizione corretta l’elemento ai , si scorre “all’indietro” il sot- toinsieme a 1 , a 2 ,... , ai − 1 , ossia cominciando proprio da ai − 1 , fino ad arrivare even- tualmente ad a 1. Si itera quindi un ciclo controllato dalla variabile k = i − 1, i − 2,... , 2, 1 in cui, ad ogni iterazione, se ak > ak + 1 si scambiano i due elementi contigui ak e ak + 1. Il ciclo termina quando l’elemento ai è finito all’inzio del sottoinsieme, oppure quando si individua un elemento ak ≤ ak + 1. L’Algoritmo 2 riporta la pseudo- codifica di INSERTIONSORT; anche in questo caso con A indichiamo l’insieme con n elementi da ordinare ( n rappresenta la cardinalità di A ).
INSERTIONSORT( A , n )
1: per i = 2, 3,... , n ripeti 2: k = i − 1 3: fintanto che k ≥ 1 e ak > ak + 1 ripeti 4: scambia ak e ak + 1 5: k = k − 1 6: fine-ciclo 7: fine-ciclo Algoritmo 2: Insertion sort
Anche in questo caso l’algoritmo produce un ordinamento corretto dell’insieme; infatti, se così non fosse allora dovrebbero esistere due elementi at e at + 1 tali da non rispettare l’ordinamento: at > at + 1. Ma questo è impossibile, perché in tal caso i due elementi sarebbero stati scambiati durante l’iterazione del ciclo interno, invertendo la loro posizione reciproca. Calcoliamo il numero di operazioni eseguite dall’algoritmo a fronte di una istan- za del problema di dimensione n. Innanzi tutto osserviamo che, se l’insieme A da
Osserviamo che effettivamente alla fine di ognuna delle quattro scansioni del- la sequenza, l’elemento più grande del sottoinsieme ancora da ordinare, finisce in fondo alla sequenza, nella sua posizione definitivamente corretta, mentre la sotto- sequenza ancora indisordine si riduce di volta in volta di un elemento. Ad esempio alla fine della prima scansione l’elemento 5 finisce in ultima posizione; alla fine del- la seconda scansione l’elemento 4 (il massimo elemento nel sottoinsieme ancora da ordinare) finisce in fondo, in penultima posizione, e così via. Possiamo formalizzare la strategia risolutiva nell’Algoritmo 3 che fornisce una pseudo-codifica di BUBBLESORT; come al solito n rappresenta il numero di elementi della sequenza A da ordinare.
BUBBLESORT( A , n )
1: f l ag = 1 2: st op = n − 1 3: fintanto che f l ag = 1 ripeti 4: f l ag = 0 5: per i = 1, 2,... , st op ripeti 6: se ai > ai + 1 allora 7: scambia ai e ai + 1 8: f l ag = 1 9: fine-condizione 10: fine-ciclo 11: st op = st op − 1 12: fine-ciclo Algoritmo 3: Bubble sort
Anche in questo caso abbiamo un algoritmo strutturato su due cicli nidificati uno dentro l’altro, ma, a differenza dei casi visti nelle pagine precedenti, questa vol- ta il ciclo esterno non viene eseguito un numero prefissato di volte: l’algoritmo BUB- BLESORT sfrutta la condizione che controlla il ciclo più esterno per verificare la pos- sibilità di interrompere l’esecuzione dell’intero procedimento non appena dovesse risultare evidente che l’insieme è completamente ordinato. In questo modo, come vedremo tra breve, l’algoritmo è in grado di sfruttare a proprio vantaggio eventuali ordinamenti già presenti nelle sottosequenze dell’insieme da ordinare. Dal punto di vista tecnico gioca quindi un ruolo cruciale la variabile f l ag che assume il significato di indicatore dello stato di ordinamento della sequenza: il va- lore f l ag = 1 indica che l’insieme probabilmente ancora non è ordinato, mentre il valore f l ag = 0 segnala che la sequenza risulta ordinata. Pertanto all’inizio del- la procedura, dal momento che non è ancora chiaro se l’insieme è ordinato o me- no, la variabile f l ag viene impostata con il valore 1. Il ciclo esterno dell’algoritmo
viene conotrollato unicamente dal valore di questa variabile e viene ripetuto fino a quando la sequenza non risulterà completamente ordinata. Per stabilire se l’insieme è ordinato o meno, con il ciclo interno controllato dal- la variabile i , viene verificato l’ordinamento reciproco degli elementi contigui ai e ai + 1 , per i = 1,... , n − 1. Se due elementi vengono trovati in posizione reciproca non corretta, allora i due elementi vengono scambiati di posto e, siccome in questo modo l’ordine complessivo degli elementi della sequenza è stato modificato, allo- ra la variabile f l ag viene posta uguale a 1, ad indicare che probabilmente, avendo fallito un test sul controllo dell’ordine degli elementi, allora è possibile che l’intera sequenza non sia ancora completamente ordinata. Visto che prima di iniziare il ciclo interno, al passo 4 dell’Algoritmo 3, viene posto f l ag = 0, se al termine di una scansione completa della sequenza (ossia, al termine del ciclo più interno) la variabile f l ag è ancora uguale a zero, questo indica che tutti i test sull’ordinamento reciproco tra gli elementi contigui (passo 6) ha dato esito negativo, il che significa che ai < ai + 1 per ogni i = 1, 2,... , n −1 e dunque la sequenza è ordinata. Una condizione così forte per il controllo del ciclo esterno ci garantisce anche la correttezza dell’algoritmo: BUBBLESORT termina solo se la sequenza è ordinata. D’altra parte che l’algoritmo termini è certo, dal momento che il ciclo interno vie- ne eseguito ogni volta con un numero di iterazioni diminuito di uno rispetto alla volta precedente. Infatti, dal momento che ad ogni scansione della sequenza, vie- ne collocato nella posizione corretta l’elemento massimo del sottoinsieme ancora da ordinare, il valore finale della variabile i che controlla il ciclo interno, può essere ridotto ogni volta di un’unità; per questo motivo viene utilizzata la variabile st op che inizialmente è posta uguale a n − 1 e, ad ogni iterazione del ciclo esterno, viene decrementata di 1 (passo 11). Nel caso più favorevole, ossia quando la sequenza A è già ordinata, l’algoritmo esegue un’unica scansione dell’intera sequenza (passi 5–9) e quindi termina, dal momento che la variabile f l ag è rimasta impostata a zero. Dunque in questo caso il numero di operazioni eseguite è dell’ordine di n. Il caso meno favorevole, anche in questo caso, è costituito da una sequenza ini- zialmente ordinata al contrario. In tal caso BUBBLESORT ad ogni iterazione del ciclo esterno riesce a collocare nella posizione corretta soltanto un elemento dell’insie- me; dunque sono necessarie n − 1 iterazioni del ciclo esterno per ordinare l’intera sequenza. Per ogni iterazione del ciclo esterno il ciclo nidificato al suo interno ese- gue ogni volta una ripetizione in meno. Quindi il numero di operazioni svolte nel caso peggiore può essere calcolato ancora una volta con la seguente espressione:
( n − 1) + ( n − 2) +... + 2 + 1 =
n ∑− 1
i = 1
i =
n ( n − 1) 2
In conclusione anche l’algoritmo BUBBLESORT, nel caso peggiore, ha una comples- sità computazionale di O ( n^2 ).
anche la funzione PARTITION che si occupa di riposizionare gli elementi del sottoin- sieme che gli viene passato come argomento utilizzando come pivot l’elemento del selezionato dalla procedura FINDPIVOT nello stesso sottoinsieme.
QUICKSORT( A , p , r ) 1: pivot = FINDPIVOT( A , p , r )
3: q = PARTITION( A , p , r , pivot ) 4: QUICKSORT( A , p , q ) 5: QUICKSORT( A , q + 1, r ) 6: fine-condizione
PARTITION( A , p , r , pivot ) 1: i = p , j = r 2: ripeti 3: fintanto che a (^) j ≥ pivot ripeti 4: j = j − 1 5: fine-ciclo 6: fintanto che ai < pivot ripeti 7: i = i + 1 8: fine-ciclo 9: se i < j allora 10: scambia ai e a (^) j 11: fine-condizione 12: fino a quando i < j 13: restituisci j
FINDPIVOT( A , p , r ) 1: per k = p + 1,... , r ripeti 2: se ak > ap allora 3: restituisci ak 4: altrimenti se ak < ap allora 5: restituisci ap 6: fine-condizione 7: fine-ciclo 8: restituisci null Algoritmo 4: Quick sort
La procedura FINDPIVOT costituisce un accorgimento tecnico per assicurare che l’elemento selezionato consenta di suddividere l’array in due componenti non vuo- te, contenenti entrambe almeno un elemento: per far questo viene scelto come pivot il maggiore tra i primi due elementi differenti presenti nell’insieme; se gli elementi sono tutti uguali, allora FINDPIVOT restituisce il valore null per indicare che l’insie- me è ordinato. La scelta del pivot avviene in modo piuttosto arbitrario, tanto che, se gli elementi dell’insieme da ordinare sono tutti diversi, si può anche scegliere di vol- ta il volta in modo del tutto casuale il pivot nel sottoinsieme preso in considerazione
Figura 2: Quick sort dell’insieme A con 7 elementi
(ad esempio si potrebbe scegliere sempre il primo elemento del sottoinsieme). La suddivisione ricorsiva della sequenza in sottosequenze può essere rappre- sentata efficacemente con un albero binario. Consideriamo ad esempio il seguente insieme A = {3, 5, 7, 1, 4, 6, 2}. Le suddivisioni successive a cui è sottoposto l’insieme A sono rappresentate in Figura 2, dove sono riportati in grassetto gli elementi scelti di volta in volta come pivot. Alla prima chiamata della procedura QUICKSORT la sequenza iniziale composta da n elementi viene suddivisa in due sottosequenze sulla base del valore dell’ele- mento scelto come pivot (5). Si ottengono in questo modo due sottoinsiemi di 4 e 3 elementi: A^1 = {3, 2, 4, 1} e A^2 = {7, 6, 5}. Entrambi hanno più di un elemento e quindi viene invocata nuovamente la procedura PARTITION sui due sottoinsiemi. Nel primo caso viene scelto come pivot l’elemento 3, nel secondo l’elemento 7. Si ottengono così 4 sottoinsiemi: A^1 1 = {1, 2}, A^1 2 = {4, 3}, A^2 1 = {5, 6} e A^2 2 = {7}. Su ogni sot- tosequenza, tranne che su A^2 2 che è composta da un solo elemento, viene ancora
applicata la procedura PARTITION ottenendo quindi sette sottoinsiemi: A^1
11 = {1},
A^1 12 = {2}, A^1 21 = {3}, A^1 22 = {4}, A^2 11 = {5}, A^2 12 = {6} e A^2 21 = A^2 2 = {7}. In pratica la soluzione finale è già stata raggiunta: la chiusura ricorsiva delle pro- cedure QUICKSORT che sono state richiamate durante il procedimento di suddivi- sione dei sottoinsiemi e ridistribuzione degli elementi, completa l’algoritmo resti- tuendo alla fine la sequenza A perfettamente ordinata. Quante operazioni sono state necessarie per ordinare l’insieme di n = 7 elemen- ti? La procedura FINDPIVOT è lineare nel numero degli elementi della sequenza, anche se nella pratica spesso esegue solo una o due operazioni di confronto. La pro- cedura PARTITION applicata su una sequenza di m elementi impiega esattamente m = r − p + 1 operazioni (è lineare nella dimensione dell’input). Ogni coppia di spi- goli nell’albero binario riportato in Figura 2 rappresenta una chiamata della proce- dura PARTITION. Su ogni “livello” dell’albero la procedura opera complessivamente su tutti gli elementi dell’insieme (a prescindere dal numero di volte che viene ri- chiamata), impiegando quindi un tempo O ( n ) per ogni livello dell’albero binario. Moltiplicando O ( n ) per la profondità dell’albero otteniamo il numero di operazioni eseguite globalmente dall’algoritmo per ordinare la sequenza di n elementi.
MERGESORT( A , p , r ) 1: se p < r allora 2: q = p + 2 r 3: MERGESORT( A , p , q ) 4: MERGESORT( A , q + 1, r ) 5: MERGE( A , p , q , r ) 6: fine-condizione
MERGE( A , p , q , r ) 1: siano i = p e j = q + 1 2: fintanto che i ≤ q e j ≤ r ripeti 3: se ai < a (^) j allora 4: bk = ai e i = i + 1 5: altrimenti 6: bk = a (^) j e j = j + 1 7: fine-condizione 8: fine-ciclo 9: se i ≤ q allora 10: copia ( ai ,... , aq ) in B 11: altrimenti 12: copia ( a (^) j ,... , ar ) in B 13: fine-condizione 14: copia B in A Algoritmo 5: Merge sort
Partendo da questi presupposti, quindi, è possibile realizzare algoritmo che, do- po aver suddiviso l’insieme iniziale A in n sottoinsiemi costituiti da un solo elemen- to ciascuno, procede a riaggregare i sottoinsiemi fondendoli ordinatamente fino a ricostruire un’unica sequenza ordinata. L’Algoritmo 5 riporta una pseudo-codifica di MERGESORT. L’algoritmo è suddiviso in due procedure distinte: la procedura ricorsiva MER- GESORT e la procedura iterativa MERGE. In sostanza l’idea di fondo di questo algo- ritmo è quella di suddividere sempre a metà l’array iniziale, in modo tale che i due sotto-array ottenuti abbiano un ugual numero di elementi (a meno di uno, nel caso di insiemi di cardinalità non rappresentabile come una potenza di 2 o comunque dispari). La suddivisione nel QUICKSORT avveniva spostando a destra e a sinistra del pivot gli altri elementi dell’array in modo da ottenere una sorta di ordinamento parziale; nel MERGESORT invece durante il processo di suddivisione l’insieme non viene in alcun modo modificato, i suoi elementi non sono spostati né confronta- ti fra di loro. Semplicemente si opera una suddivisione dicotomica ricorsiva della sequenza iniziale da ordinare, fino ad ottenere n sequenze di dimensione 1. Di questa suddivisione ricorsiva si occupa la procedura ricorsiva MERGESORT, che riceve in input l’intera sequenza A insieme con i due indici p e r che consento- no di identificare l’elemento ap e l’elemento ar , rispettivamente il primo e l’ultimo elemento della sottosequenza su cui deve operare la procedura. La procedura indi-
vidua l’indice q dell’elemento che si trova a metà della sottosequenza delimitata da- gli elementi ap e ar (passo 2 della procedura MERGESORT) ed innesca due chiamate ricorsive della stessa procedura sulla prima metà della sequenza (dall’elemento di indice p fino all’elemento di indice q ) e sulla seconda metà (dall’elemento di indice q + 1 fino all’elemento di indice r ). La procedura ricorsiva si ripete fino a quando la sottosequenza da suddividere non è composta da un solo elemento: quando p = r (passo 1) la procedura termina senza compiere alcuna operazione. Nel backtracking della ricorsione sta il vero e proprio punto di forza di questo algoritmo. Durante il backtracking infatti vengono fusi a due a due i sottoinsiemi ottenuti con il processo di suddivisione ricorsiva. Durante la fusione gli elementi vengono posizionati in modo da ottenere da due sottosequenze ordinate un’unica sequenza ordinata. Infatti le sequenze composte da un solo elemento da cui inizia il procedimento di fusione sono di per sé ordinate. Della fusione ordinata di due sottosequenze contigue, A ′^ = { ap , ap + 1 ,... , aq } e A ′′^ = { aq + 1 , aq + 2 ,... , ar }, si occupa la procedura iterativa MERGE. Tale procedura esegue una scansione “in parallelo” di entrambe le sequenze (ciclo principale, righe 2–8, controllato dalle variabili i e j ), scegliendo e copiando nell’insieme di appoggio B di volta in volta l’elemento minore tra ai e a (^) j , rispettivamente appartenenti alla prima e alla seconda sequenza da fondere insieme. Quando una delle due sequen- ze è stata copiata completamente in B , il ciclo principale termina e la procedura prosegue copiando in B gli elementi rimanenti dell’altra sottosequenza. Infine gli elementi ordinati dell’insieme B vengono copiati nuovamente in A. Ad esempio supponiamo di dover fondere tra loro le due sequenze ordinate A ′^ = {3, 5, 15} e A ′′^ = {2, 4, 6}. I passi effettuati dalla procedura MERGE possono essere schematizzati come segue:
Complessivamente potremmo schematizzare l’intero procedimento di ordina- mento di una sequenza di n elementi come nel diagramma esemplificativo rappre- sentato in Figura 4, in cui viene tracciato il procedimento di scomposizione ricorsiva e di ricomposizione nella fase di backtracking della sequenza A = {3, 15, 6, 2, 7, 4, 12, 8}. Il procedimento di fusione di due sequenze di k elementi ciascuna richiede un tempo di esecuzione dell’ordine di 2 k , ossia lineare nel numero complessivo di ele- menti. In questo modo, per aggregare h sequenze ordinate costituite da n / h ele- menti ciascuna, ottenendo h /2 sequenze ordinate, si impiega un tempo dell’ordine di h ( n / h ) = n. Siccome suddividendo sempre a metà l’insieme iniziale si ottiene un albero binario perfettamente bilanciato, la profondità dell’albero con cui possia- mo rappresentare le suddivisioni dell’insieme da ordinare (vedi ad esempio la parte superiore della Figura 4) saranno esattamente log 2 n.
po lineare nel numero di elementi presenti nell’insieme. Con la struttura di heap entrambe le operazioni richiedono un tempo logaritmico nel numero di elementi. Un heap è un albero binario completo; se il numero di elementi non è pari a 2 k^ (per qualche k ≥ 0) gli elementi dell’ultimo livello dell’albero dovranno essere raggruppati sulla sinistra, senza lasciare posizioni vuote fra le foglie (vedi Figura 5).
Figura 5: Un albero binario (a sinistra) e un albero binario completo (a destra)
Gli elementi di un heap devono inoltre rispettare una specifica regola di ordina- mento reciproco: il vertice padre deve essere sempre maggiore o uguale ai vertici figli. In questo modo nella radice dell’albero è collocato il vertice il cui valore (la priorità) è massimo. L’elemento minimo si trova invece in una delle foglie dell’al- bero. Non c’è alcuna relazione di ordine specifica tra vertici che non sono tra loro discendenti (ad esempio non è richiesto che sia rispettato uno specifico ordine tra i figli di uno stesso vertice). Grazie alle sue proprietà (l’essere un albero binario completo) un heap può esse- re rappresentato utilizzando un array, utilizzando la seguente regola estremamente semplice: il vertice dell’albero che nell’array occupa la posizione i ha come figli (ri- spettivamente sinistro e destro) gli elementi che occupano le posizioni 2 i e 2 i + 1, e come padre il vertice che occupa la posizione b i 2 c. La radice dell’albero viene collo- cata nella posizione di indice 1. L’heap rappresentato in Figura 6 può quindi essere riportato in un array ponendo A = ( a 1 = 60, a 2 = 30, a 3 = 50, a 4 = 20, a 5 = 10, a 6 = 40, a 7 = 45, a 8 = 15, a 9 = 4): il padre dell’elemento a 5 = 10 è a b5/2c = a 2 = 30 e i figli di a 3 = 50 sono rispettivamente a 3 × 2 = a 6 = 40 e a 3 × 2 + 1 = a 7 = 45.
30
50
20