Docsity
Docsity

Prepara i tuoi esami
Prepara i tuoi esami

Studia grazie alle numerose risorse presenti su Docsity


Ottieni i punti per scaricare
Ottieni i punti per scaricare

Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium


Guide e consigli
Guide e consigli


Algoritmi notevoli - complessità, Appunti di Reti informatiche

Descrizione degli algoritmi notevoli e loro complessità

Tipologia: Appunti

2019/2020

Caricato il 02/10/2021

daniele-c-6
daniele-c-6 🇮🇹

5

(3)

4 documenti

1 / 20

Toggle sidebar

Questa pagina non è visibile nell’anteprima

Non perderti parti importanti!

bg1
1
1. c
2. ALGORITMI NOTEVOLI
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
2. ALGORITMI DI ORDINAMENTO
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
ALGORITMI
N
O
TEV
O
LI
FUSIONE
ORDINAMENTO RICERCA MERGE
SELECTION
SORT SEQUENZIALE /
LINEARE
BINARIA /
DICOTOMICA
SHELL
SORT
QUICK
SORT
BUBBLE
SORT
INSERTION
SORT
MERGE
SORT
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe
pff
pf12
pf13
pf14

Anteprima parziale del testo

Scarica Algoritmi notevoli - complessità e più Appunti in PDF di Reti informatiche solo su Docsity!

**1. c

  1. ALGORITMI NOTEVOLI**

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

2. ALGORITMI DI ORDINAMENTO

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

ALGORITMI

NOTEVOLI

FUSIONE

ORDINAMENTO RICERCA MERGE

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:

  1. Ogni elemento della sequenza da ordinare viene confrontato con tutti i successivi (prima il primo elemento, poi il secondo, il terzo e così via fino alla fine).
  2. Quando si trova un elemento minore (o maggiore) dell’elemento di riferimento, si esegue lo scambio.
  3. Se viene eseguito uno scambio, si procede confrontando il nuovo valore nella posizione dell’elemento di riferimento con gli elementi rimasti.

2.1.2 ESEMPIO

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 --; }

2.2.4 COMPLESSITA’

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’.

2.3 BUBBLE SORT

2.3.1 DESCRIZIONE

Nell’algoritmo di ordinamento a bolle si eseguono i seguenti passi:

  1. Si confrontano gli elementi della sequenza da ordinare a coppie: il primo con il secondo, il secondo con il terzo e così via.
  2. Si esegue uno scambio tra gli elementi confrontati della coppia in funzione dell’ordinamento che si vuole realizzare, crescente o decrescente.
  3. Arrivati in fondo, si riparte dal punto 1 finché l’ordinamento non è completo.

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:

2.4 SHELL SORT

2.4.1 DESCRIZIONE

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:

2.4.3 PSEUDOCODICE

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

2.5.3 PSEUDOCODICE

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); } }

2.5.4 COMPLESSITA’

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).

2.6 MERGE SORT

2.6.1 DESCRIZIONE

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

2.6.2 ESEMPIO

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:

3.1.1 DESCRIZIONE

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.

3.1.2 ESEMPIO

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.

3.1.3 PSEUDOCODICE

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.

3.2 ALGORITMI DI RICERCA BINARIA

3.2.1 DESCRIZIONE

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:

  1. Si esegue un primo confronto tra l’elemento cercato e l’elemento centrale della sequenza.
  2. Se l’elemento centrale coincide con l’elemento cercato, la ricerca termina con successo. Altrimenti si tratta uno dei due casi seguenti: 2.1. L’elemento centrale è maggiore dell’elemento cercato: ciò implica che, se l’elemento cercato esiste nella sequenza, non può che trovarsi nella sua prima metà. 2.2. L’elemento centrale è minore dell’elemento cercato: ciò implica che, se l’elemento cercato esiste nella sequenza, non può che trovarsi nella sua seconda metà.

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.

3.2.3 PSEUDOCODICE

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)

4.1.4 COMPLESSITA’

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.

BIBLIOGRAFIA

  1. Note introduttive su: Analisi della complessità degli algoritmi e Algoritmi di ordinamento Edoardo Ardizzone & Riccardo Rizzo, Università degli Studi di Palermo Facoltà di Ingegneria, A.A. 2004 - 2005.
  2. Lezione 12: Funzioni Ricorsive e Ricerca, Prof. Fabio Pellacini, Università degli Studi di ROMA "La Sapienza", A.A. 2011-
  3. Dispensa 9: Complessità Computazionale, Ordinamento e Ricerca, Prof. Domenico Rosaci, Università Mediterranea di Reggio Calabria, A.A. 2015-
  4. Manuale Cremonese Informatica e Telecomunicazioni, Seconda edizione per i nuovi Tecnici a indirizzo Informatica e Telecomunicazioni, Zanichelli, marzo 2015

SITOGRAFIA

https://www.codingcreativo.it/quick-sort-in-c/

https://www.geeksforgeeks.org/shellsort/