Un problema spesso può essere risolto utilizzando algoritmi diversi. Come si misura la loro "bontà", così da scegliere il più efficiente?
Le misure utilizzate si basano su due risorse principali:
Una buona misura dell’efficienza di un algoritmo deve prescindere dal calcolatore utilizzato.
Occorre una misura astratta che tenga conto del metodo di risoluzione con cui l’algoritmo effettua la computazione, così che questa sia un metodo di giudizio valido per qualsiasi calcolatore ed in qualsiasi contesto.
La metrica utilizzata consiste nel misurare il numero complessivo di operazioni elementari in funzione della dimensione n dei dati in ingresso.
Le seguenti operazioni sono considerate elementari:
Esempio:
n = 10; //costo 1
i = 0; //costo 1
while(i < n) { //costo n+1
i = i+1; //costo 2 * n
}
Costo totale: 1 + 1 + n+1 + 2*n = 3n+3
Esempio:
n = 10; //costo 1
m = 20; //costo 1
sum = 0; //costo 1
for (i=0; i<n; i++) { //costo 1+ n+1(test)+ n(incr)
for (j=0; j<m; j++) { //costo n*(1+m+1+m)
sum = sum+i+j; //costo 3*n*m
}
}
Costo totale: 3+(1+n+1+n)+n(m+1+m)+3*n*m =
= 5nm+3n+5
Consideriamo il seguente problema:
"si vuole ricercare il valore v all'interno di un array"
È possibile identificare le seguenti casistiche:
Salvo rare eccezioni, si fa sempre riferimento al caso peggiore.
Spesso non si è interessati a conoscere il polinomio associato al costo, ma si vuole conoscere la "scalabilità" dell'algoritmo in questione.
Gli <<O grandi>> sono un criterio matematico per partizionare gli algoritmi in classi di complessità.
2
Le informazioni sono memorizzate all'interno di variabili, alle quali è "associato" un tipo, che ne indica l'insieme di valori ed azioni ammissibili (dominio).
Il puntatore è un particolare "tipo" di dato, contenente l'indirizzo di memoria associato ad un altra variabile. Poiché referenzia un'altra variabile, la dichiarazione di un puntatore include il tipo dell'area a cui punta.
NB: Essendo un puntatore un tipo di dato, è possibile ottenere un puntatore ad un puntatore.
int* puntatore;
int i = 5;
i
5
int i = 5;
int* p_i = &i;
i
5
p_i
int i = 5;
int* p_i = &i;
i = 7;
i
7
p_i
I puntatori sono fondamentali per la realizzazione degli strumenti informatici, in quanto sono alla base di:
Un array è un insieme ordinato di elementi omogenei (stesso tipo).
Caratteristiche:
p_list
1
2
3
4
1
0
1
2
3
4
Costi:
L'inserimento di un valore alla cella i ha un costo oneroso in quanto si divide in due problemi:
Le liste concatenate (linked list), sono strutture dati dinamiche*, tali che ogni elemento detiene il puntatore all'elemento successivo.
p_list
1
2
4
*una struttura dati statica (array) prevede la dichiarazione del numero di elementi in essa contenuti
Le liste doppiamente concatenate (double linked list), sono una estensione delle liste semplicemente concatenate, tali che ogni elemento detiene sia il puntatore all'elemento successivo, che al precedente.
p_list
1
2
4
Le skip list, sono una estensione delle liste semplicemente concatenate, tali che ogni elemento detiene sia il puntatore all'elemento successivo, che ad un elemento "più lontano" della collezione.
Questa struttura è utilizzata per gestire agilmente una grande mole di dati.
p_list
1
2
4
Costi:
La coda (queue) è una struttura dati che prevede l'accesso alle informazioni in essa contenute mediante la strategia FIFO (First In First Out).
Questa può essere realizzata mediante un array (coda a dimensione fissa) o una lista (coda a dimensione illimitata).
Le operazioni di accesso alle risorse sono le seguenti:
enqueue(1)
1
enqueue(2)
1
2
enqueue(3)
1
2
3
dequeue()
2
3
dequeue()
3
La coda (stack) è una struttura dati che prevede l'accesso alle informazioni in essa contenute mediante la strategia LIFO (Last In First Out).
Questa è tendenzialmente realizzata mediante una lista, così da fornire una dimensione illimitata.
Le operazioni di accesso alle risorse sono le seguenti:
push(1)
1
push(2)
2
1
push(3)
3
2
1
pop()
2
1
pop()
1
Un dizionario, o mappa, è una struttura dati in grado di contenere una collezione di coppie (chiave, valore).
L'accesso alle risorse è quindi effettuato tramite chiave e non più basato sull'indice di memorizzazione.
k1
k2
v2
k3
v3
v1
Una funzione di hash è una particolare funzione tale che, dato in ingresso una stringa detta chiave, genera in maniera univoca una seconda stringa detta valore di dimensione ridotta.
Dal punto di vista matematico/insiemistico il valore, appartenente al codominio, è l'immagine della chiave fornita in ingresso, appartenente al dominio.
Un esempio è la funzione matematica resto.
Una HashTable è una struttura dati che gestisce l'ordinamento delle strutture mediante una funzione di hash. Di conseguenza l'accesso ai dati è effettuato in maniera trasparente
k1
k2
k3
f(k)
v2
v1
v3
0
2
1
Un albero è una particolare struttura dati in grado di gestire una parentela tra gli elementi in essa contenuti.
1
2
3
4
5
6
8
7
root
I principali tipi di albero sono i seguenti:
La ricerca di un elemento prevede una delle seguenti metodologie:
Pseudocodice DFS
dfs (root, val):
//Se è il nodo corrente lo riporto
if root.info == val:
return true;
//Approccio iterativo
bool found = false;
for child in childs.node && !found:
found |= dfs(child, val);
return found;
Pseudocodice BFS
bfs(root, val):
queue = [root]
return bfs_aux(queue, val)
bfs_aux(queue, val):
//Caso base della ricorsione: lista vuota
if isEmpty(queue):
return false;
//Prelevo il prossimo elemento da vedere (LIFO)
node = queue.dequeue()
//Se è lui arresto la ricorsione (verifica esistenziale)
if node.info == val:
return true;
//Inserisco i successivi elementi da visualizzare
for child in node.childs:
queue.enqueue(childs)
//Avvio la ricorsione
return binary_bfs_aux(queue, val)
Un grafo è una struttura dati costituita da elementi contententi l'informazione, detti nodi, collegati tra loro attraverso archi.
1
2
3
4
5
6
Un grafo ammette diverse rappresentazioni, le principali sono le seguenti:
Intuizione: per la visita di un grafo è possibile adoperare tecniche simili alla bsf vista per gli alberi, accompagnando lo pseudocodice con una lista dei nodi visitati (utile ad evitare di incappare all'interno di un ciclo).
In tal modo sarà possibile variare il tipo di visita semplicemente gestendo la struttura dati ospitante i nodi da visitare (FIFO, LIFO, ?).