












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
Descrizione degli algoritmi notevoli e loro complessità
Tipologia: Appunti
1 / 20
Questa pagina non è visibile nell’anteprima
Non perderti parti importanti!













**1. c
Tra i principali tipi di algoritmi notevoli troviamo quelli di ORDINAMENTO, di RICERCA e di FUSIONE, preposti a svolgere le seguenti funzioni: ● ORDINAMENTO : elencare gli elementi di un insieme di dati omogenei seguendo una relazione d’ordine di tipo crescente o decrescente, in modo che ogni elemento sia minore o maggiore di quello che lo segue ● RICERCA : trovare all’interno di un insieme di dati omogenei un elemento avente determinate caratteristiche ● FUSIONE : unire insiemi diversi di dati omogenei in un unico insieme seguendo criteri stabiliti a monte
In Figura 1 è riportato uno schema con i principali tipi di algoritmi notevoli, che saranno trattati nei capitoli a seguire. In particolare per ciascun tipo di algoritmo verrà data una descrizione, un esempio, lo pseudocodice di implementazione e la complessità .
Figura 1 - Principali tipi di algoritmi notevoli
Tra gli algoritmi di ordinamento troviamo i seguenti: ● Selection sort: ordinamento per selezione ● Insertion sort: ordinamento per inserimento ● Bubble sort: ordinamento per scambio ● Shell sort: ordinamento evoluto ideato da Donald L. Shell basato sul confronto di elementi posti a una certa distanza
SELECTION SORT SEQUENZIALE /LINEARE
BINARIA / DICOTOMICA
SHELL SORT
QUICK SORT
BUBBLE SORT
INSERTION SORT
MERGE SORT
● Quick sort: ordinamento evoluto di tipo divide et impera basato sulla scelta di un elemento cardine (pivot) ● Merge sort: ordinamento evoluto di tipo divide et impera basato sulla fusione di insiemi ordinati
2.1 SELECTION SORT
2.1.1 DESCRIZIONE Nell’algoritmo di ordinamento per selezione si eseguono i seguenti passi:
Array di 4 elementi interi al quale applicare un ordinamento crescente per selezione:
Prima iterazione: il primo elemento viene confrontato con tutti i successivi e vengono fatti 2 scambi.
L’elemento in prima posizione (18) viene confrontato con il successivo (10) e subito scambiato ottenendo:
A questo punto è il 10 (elemento in prima posizione) a essere confrontato con i successivi rimanenti. In terza posizione c’è il 12 e quindi non viene scambiato, in quarta posizione c’è il 6 e quindi avviene lo scambio, ottenendo:
Seconda iterazione: il secondo elemento viene confrontato con tutti i successivi e vengono fatti 2 scambi.
Il 18 viene subito confrontato con 12 e avviene lo scambio:
Il 12 (nuovo valore in seconda posizione) viene confrontato con 10 e avviene lo scambio:
La linea blu delimita la sottosequenza ordinata (in testa) rispetto a quella da ordinare (in coda). All’inizio la sottosequenza ordinata è composta dal solo primo elemento (18). Il primo elemento della sottosequenza da ordinare (10) viene confrontato con il 18 e viene inserito alla sua sinistra per procedere con l’ordinamento crescente, ottenendo:
Il 12 viene confrontato con 18 e 10 per decidere dove inserirlo in ottica crescente, ottenendo:
Infine il 6 viene confrontato con 18, 12 e 10 per decidere dove inserirlo in ottica crescente, ottenendo:
NOTA: Nel confronto con gli elementi della sottosequenza ordinata ci si ferma non appena l’elemento da posizionare risulta maggiore dell’elemento della sottosequenza ordinata. Proprio quella sarà la posizione in cui inserirlo. La comodità del procedere con la sequenza di partenza partizionata nelle due sottosequenze è proprio questa: si può limitare il numero di confronti necessari per l’ordinamento.
2.2.3 PSEUDOCODICE A seguire un esempio di implementazione in pseudocodice di un algoritmo di ordinamento crescente per inserimento.
count1 e count2 sono due variabili di tipo intero, rappresentano i contatori e permettono di scorrere rispettivamente le sottosequenze non ordinata (in coda) e ordinata (in testa). Array è la struttura dati contenente elementi di tipo intero, da ordinare in modo crescente. Ha dimensione N, il primo elemento è in posizione 0 e l’ultimo in N-1. temp è una variabile di tipo intero, di comodo, per permettere lo scambio dei valori.
for (count1 = 1; count1 <= N-1; count1 ++) count2 = count1 -1; while (count2 >= 0 AND Array[count2]> Array [count1]) { temp = Array [count2]; Array [count2] = Array [count1]; Array [count1] = temp; count2 --; }
La complessità di questo tipo di algoritmo dipende da se e quanto la sequenza di partenza è già ordinata. Nel caso migliore, in cui la sequenza è già ordinata, si devono eseguire N-1 confronti e quindi la complessità è O(N). Nel caso peggiore invece la quantità di confronti necessari ha ordine di grandezza: N*(N- 1)/2 e quindi la complessità è O(N^2 ). Se ne deduce facilmente che è un algoritmo che conviene laddove la sequenza di partenza è ‘quasi ordinata’.
Nell’algoritmo di ordinamento a bolle si eseguono i seguenti passi:
2.3.2 ESEMPIO
Array di 4 elementi interi al quale applicare un ordinamento crescente per scambio a bolle:
Prima iterazione: il primo elemento viene confrontato con il secondo, il secondo con il terzo, il terzo con il quarto.
L’elemento in prima posizione (18) viene confrontato con il successivo (10) e subito scambiato ottenendo:
Il secondo elemento (18) viene confrontato con il terzo (12) e quindi scambiato ottenendo:
Il terzo elemento (18) viene confrontato con il quarto (6) e quindi scambiato ottenendo:
Seconda iterazione: si ripete il confronto a coppie ripartendo dall’inizio. 10 viene confrontato con 12 e non vengono scambiati. 12 viene confrontato con 6 e vengono scambiati, ottenendo:
Nell’algoritmo evoluto ideato da Shell nel 1959 si parte dall’idea di confrontare NON gli elementi adiacenti di una struttura dati, ma quelli a distanza maggiore. La sequenza di partenza viene quindi spezzata in diverse sottosequenze più piccole, generate da elementi equidistanti d. Sulle sottosequenze più piccole, iterativamente, viene poi applicato uno degli algoritmi di ordinamento visti in precedenza, partendo dal presupposto che questi lavorano meglio con un numero inferiore di dati da ordinare, dal momento che la loro complessità è dell’ordine di N^2. La descrizione dell’algoritmo risulterà maggiormente chiara grazie all’esempio di seguito riportato.
2.4.2 ESEMPIO
Array iniziale di 8 elementi da ordinare:
L’array viene scomposto in 4 sotto-array, ciascuno di 2 elementi, ricavati dall’array di partenza con distanza d=4 , ottenendo:
A ciascuno dei sotto-array viene adesso applicato uno degli algoritmi di ordinamento semplici visti in precedenza (es. insertion sort), ottenendo:
Il risultato complessivo rimettendo insieme l’array è:
L’array viene adesso scomposto in 2 sotto-array, ciascuno di 4 elementi, ricavati dall’array di partenza con distanza d=2 , ottenendo:
A ciascuno dei sotto-array viene adesso applicato uno degli algoritmi di ordinamento semplici visti in precedenza (es. insertion sort), ottenendo:
Il risultato complessivo rimettendo insieme l’array è:
A questo punto l’array è ‘quasi ordinato’ e viene direttamente applicato uno degli algoritmi di ordinamento semplici visti in precedenza (es. insertion sort), ottenendo:
A seguire un esempio di implementazione in pseudocodice di un algoritmo di ordinamento crescente di tipo Shell.
Array è la struttura dati contenente elementi di tipo intero, da ordinare in modo crescente. Ha dimensione N, il primo elemento è in posizione 0 e l’ultimo in N-1. temp è una variabile di tipo intero, di comodo, per permettere lo scambio dei valori. i e j sono variabili di tipo intero che permettono lo scorrimento degli array. distanza è appunto la distanza degli elementi da prelevare per comporre i sotto array.
for (distanza= N / 2; distanza > 0; distanza=distanza/2) {
for (i= distanza; i < N; i++) { temp= Array [i]; for (j= i; j >= distanza AND Array[j-distanza]>temp; j= j-distanza) Array [j]= Array [j-distanza]; Array [j]= temp; } }
2.4.4 COMPLESSITA’ Applicando più volte gli algoritmi di ordinamento semplici, la complessità dello Shell Sort nel caso peggiore è O(N^2 ). Sperimentalmente è stata verificata una complessità media di O(N1,25^ ).
2.5 QUICK SORT
2.5.1 DESCRIZIONE
Di seguito un’implementazione in pseudocodice vicino al linguaggio C. Inizialmente il pivot scelto, anziché essere centrale, è il primo elemento dell’array da ordinare. Array è la struttura dati contenente elementi di tipo intero, da ordinare in modo crescente. Ha dimensione N, il primo elemento è in posizione 0 e l’ultimo in N-1. Le variabili intere i, j, primo e ultimo permettono di scorrere l’array; temp è una variabile di tipo intero, di comodo, per permettere lo scambio dei valori; pivot è l’indice appunto dell’elemento pivot scelto. Vediamo che quicksort richiama se stessa in ottica ricorsiva.
void quicksort (int Array[N], int primo, int ultimo) { int i, j, pivot, temp;
if (primo < ultimo) { pivot= primo; i= primo; j= ultimo;
while (i < j) { while (Array[i] <= Array[pivot] AND i < ultimo) i++; while (Array[j] > Array[pivot]) j--; if (i < j) { temp= Array[i]; Array[i]= Array[j]; Array[j]= temp; } }
temp= Array[pivot]; Array[pivot]= Array[j]; Array[j]= temp; quicksort(Array, primo, j-1); quicksort(Array, j+1, ultimo); } }
Nel caso peggiore la complessità dell’algoritmo, analogamente agli algoritmi precedenti è O(N^2 ) ma a differenza degli altri casi, la scelta del pivot può influire notevolmente sulla complessità. E’ dimostrabile che il caso migliore coincide con il caso medio, quando il pivot spacca la sequenza iniziale in due parti di dimensioni simili e in tal caso la complessità è O(NlogN).
Il merge sort è un algoritmo di ordinamento basato su confronti che utilizza un processo di risoluzione ricorsivo, sfruttando la tecnica del Divide et Impera, che consiste nella suddivisione del problema in sottoproblemi della stessa natura di dimensione via via più piccola. Idea dell'algoritmo: –se la sequenza da ordinare ha meno di 2 elementi, è ordinata per definizione –altrimenti: •si divide l'array in 2 sotto sequenza, ognuna con la metà degli elementi di quella originaria •si ordinano le 2 sotto sequenze applicando di nuovo l'algoritmo •si fondono (merge) le 2 sotto sequenza che ora sono ordinate
Supponiamo di voler applicare l’algoritmo di merge sort per ordinare la sequenza 42,16,28,36,26,78,84,8. L’applicazione ricorsiva dell’algoritmo genererà le seguenti sotto sequenze:
La ricerca sequenziale prevede di confrontare l’elemento cercato con tutti gli elementi dell’insieme. I confronti vengono fatti in sequenza ossia partendo dal primo elemento dell’insieme, per poi procedere con il secondo, terzo, quarto e così via (da qui il nome di ricerca sequenziale). L’esito della ricerca può ovviamente essere negativo (elemento non trovato) o positivo (elemento trovato). L’algoritmo si interrompe dopo aver trovato nell’array l’elemento cercato.
Array di 4 elementi interi sul quale effettuare una ricerca sequenziale dell’elemento 12:
Prima iterazione: il primo elemento (18) viene confrontato con l’elemento cercato (12) ma non c’è corrispondenza
Seconda iterazione: il secondo elemento (10) viene confrontato con l’elemento cercato (12) ma non c’è corrispondenza
Terza iterazione: il terzo elemento (12) viene confrontato con l’elemento cercato (12) e c’è corrispondenza; a questo punto possiamo continuare a confrontare i restanti elementi e solo alla fine dei confronti restituire la posizione nell’array dell’elemento che soddisfa la condizione di uguaglianza con l’elemento cercato.
A seguire un esempio di implementazione in pseudocodice dell’algoritmo di ricerca sequenziale di un elemento in un vettore di elementi omogenei di dimensione N: Key è una variabile di tipo intero che rappresenta l’elemento da cercare. Array è la struttura dati contenente elementi di tipo intero fra cui cercare l’elemento key. pos è una variabile di tipo intero che rappresenta la posizione all’interno dell’array dell’elemento per il quale c’è corrispondenza con l’elemento cercato. count è una variabile di tipo intero e rappresenta un contatore.
for (counter= 0; counter < = N; counter++) if (Array[counter] == Key) return counter; // elemento trovato return -1; // elemento non trovato }
3.1.4 COMPLESSITA’
E’ piuttosto evidente che utilizzando questo algoritmo di ricerca saranno necessari un numero di confronti variabile in base alla posizione che l’elemento cercato occupa all’interno dell’insieme. Possiamo dire che nel caso peggiore di elemento non presente o presente in ultima posizione, il numero di confronto è pari alla cardinalità dell’insieme, nel caso migliore avremmo bisogno di un solo confronto, in media il numero di confronti necessari sarà (1+2+3+…+N)/2 ossia (N+1)/2, se si assume che tutti gli elementi dell’array possano essere
cercati con uguale probabilità. Considerando il caso peggiore si può pertanto concludere che la complessità computazionale del metodo cresce in modo lineare con la dimensione del problema e che la complessità asintotica del programma è O(N), cioè è dello stesso ordine di grandezza di N.
La ricerca di un determinato elemento all’interno di una sequenza ordinata può essere effettuata in modo molto più efficiente rispetto alla ricerca lineare, utilizzando un algoritmo di ricerca binaria o dicotomica, che deve il suo nome al fatto di essere basato su un ciclo che, ad ogni passo, dimezza la porzione di sequenza in cui effettuare la ricerca. L’algoritmo può essere illustrato informalmente nel modo seguente:
In ogni caso, la ricerca continua restringendo l’analisi alla sola metà della sequenza potenzialmente in grado di contenere l’elemento cercato, e ripetendo i passi 1 e 2, fino alla individuazione dell’elemento cercato o finché la porzione di sequenza indagata si riduce ad un solo elemento che non è quello cercato.
3.2.2 ESEMPIO
Vediamo un esempio pratico in cui si applica l’algoritmo di ricerca binaria per cercare il numero 21 in un vettore composto da 18 numeri interi, i cui indici superiore ed inferiore sono rispettivamente inferiore=0 e superiore=17.
L'indice dell’elemento centrale del vettore sarà centro=(inferiore+superiore)/2 = (0+17)/2= (se sono rimaste un numero pari di celle per convenzione si prende fra le due celle centrali
.
Il nuovo indice centrale dell'intervallo il cui indice inferiore=4 e superiore=7 sarà centro=(inferiore+superiore)/2 = 5 ed avremo pertanto vettore[centro]=vettore[5] =14. Poichè 21>14 si continua la ricerca nella parte superiore, spostando l’indice inferiore al valore vettore[centro+1]=6 e mantenendo l’indice superiore =7.
Il nuovo indice centrale dell'intervallo con indici inferiore=6 e superiore=7 sarà centro= 6, da cui vettore[centro]=vettore[6]= 21. Poiché abbiamo trovato il valore cercato, l’algoritmo della ricerca binaria termina e restituisce la posizione del numero 21 all'interno del vettore.
L'algoritmo di ricerca binaria o dicotomica ha due possibili implementazioni: iterativa e ricorsiva.
Partiamo dallo pseudocodice per l'implementazione iterativa: Key è una variabile di tipo intero che rappresenta l’elemento da cercare.
Array è la struttura dati di dimensione N contenente elementi di tipo intero fra cui cercare l’elemento chiave. low è una variabile di tipo intero che rappresenta l’indice inferiore della porzione di vettore analizzata high è una variabile di tipo intero che rappresenta l’indice superiore della porzione di vettore analizzata middle è una variabile di tipo intero che rappresenta l’indice del punto centrale della porzione di vettore analizzata
low= 0; high= N - 1;
while (low <= high) { middle= (low + high) / 2; if (Key == Array[middle]) return middle; //ricerca terminata con successo else if (Key < Array[middle]) high= middle – 1; //vai nella prima metà else low= middle + 1; // vai nella seconda metà } // fine ciclo di analisi return –1; // elemento non trovato
Riportiamo adesso lo pseudocodice per l'implementazione ricorsiva della ricerca binaria:
low= 0 high= N - 1
ricerca_bin (Array, low, high, Key) { if (low > high) return -1; middle= (low + high) / 2; if (Key == Array[middle]) return middle; if (Key > Array[middle]) return ricerca_bin(Array, middle+1, high, Key); else return ricerca_bin(Array, low, middle-1, Key); }
3.2.4 COMPLESSITA’ Algoritmo iterativo di ricerca binaria: Per determinare il valore della complessità dell’algoritmo iterativo di ricerca binaria in funzione dei dati di ingresso, si osservi che nel caso migliore il numero di iterazioni è uguale a 1, in quanto viene eseguita una sola iterazione del ciclo, mentre nel caso peggiore, che è quello di ricerca infruttuosa, si esce dal ciclo quando high diventa più grande di low, cioè dopo un numero di dimezzamenti della porzione di sequenza da indagare uguale a logN. Riferendosi al caso peggiore si può concludere quindi che la complessità asintotica della ricerca binaria è O(log(N)). Algoritmo ricorsivo di ricerca binaria:
index1 0; index2 0; index3 0;
/* fusione delle due sequenze (merge) */ do { if(array1[index1] <= array2[index2]) array3[index3++]= array1[index1++]; else array3[index3++]= array2[index2++]; } while(index1<lenght1 && index2<lenght2);
If (index1<lenght1) do { array3[index3++]= array1[index1++]); }while(index1<lenght1) else do { array3[index3++]= array2[index2++]); }while(index2<lenght2)
La complessità dell’algoritmo di merge è funzione della lunghezza dei due vettori da fondere, pertanto avremo che la complessità asintotica dell’algoritmo può essere espressa come O(N) e di conseguenza cresce linearmente con le dimensioni dei vettori iniziali.
https://www.codingcreativo.it/quick-sort-in-c/
https://www.geeksforgeeks.org/shellsort/