Complessità ed Algoritmi e Strutture Dati
Indice
- Complessità di un algoritmo
- Puntatori
- Liste
- Code
- Pile
- Dizionari
- HashMap
- Alberi
- Grafi
Complessità
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:
- il tempo richiesto per la sua esecuzione
- lo spazio utilizzato per la memorizzazione e manipolazione dei dati coinvolti
Complessità
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.
Complessità
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:
- aritmetiche
- logiche
- confronto
- assegnazione
Complessità
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
Complessità
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
Complessità
Consideriamo il seguente problema:
"si vuole ricercare il valore v all'interno di un array"
È possibile identificare le seguenti casistiche:
- caso migliore: è la presenza dell’elemento nell'array ed è nella prima posizione controllata.
- caso peggiore: è l’assenza o una sua presenza nell'ultima cella controllata. Implica il costo di scansione dell’intera struttura
Salvo rare eccezioni, si fa sempre riferimento al caso peggiore.
Complessità
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à.
- O(1)
- O(log n)
- O(n)
- O(n*log(n))
- O(n )
- ...
2
Puntatori
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;
Puntatori
int i = 5;
i
5
Puntatori
int i = 5;
int* p_i = &i;
i
5
p_i
Puntatori
int i = 5;
int* p_i = &i;
i = 7;
i
7
p_i
Puntatori
I puntatori sono fondamentali per la realizzazione degli strumenti informatici, in quanto sono alla base di:
- qualsiasi struttura dati (array, liste, alberi, ecc...).
- algoritmi ricorsivi
- programmazione orientata agli oggetti
- polimorfismo e riflessione
Array
Un array è un insieme ordinato di elementi omogenei (stesso tipo).
Caratteristiche:
- allocazione statica: si alloca in memoria un area atta a contenere un numero di elementi fissato.
- accesso mediante indice: è possibile accedere all'elemento i-esimo secondo la notazione array[i]
p_list
1
2
3
4
1
0
1
2
3
4
Array
Costi:
- accesso: O(1) (basato su indice)
- inserimento/cancellazione: O(n)
L'inserimento di un valore alla cella i ha un costo oneroso in quanto si divide in due problemi:
- spazio disponibile: traslo tutto il contenuto dalle celle di una posizione per j > i [O(n)] ed inserisco l'elemento [O(1)]
- spazio non disponibile: alloco una nuova struttura più grande e ricopio l'array inserendo il nuovo elemento [O(n)]
LinkedList
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
DoubleLinkedList
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
SkipList
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
Liste
Costi:
- inserimento/cancellazione: O(n)
- accesso: O(n)
Code
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(e): inserisce l'elemento e in coda
- dequeue(): preleva l'elemento affiorante
Code
enqueue(1)
1
enqueue(2)
1
2
enqueue(3)
1
2
3
dequeue()
2
3
dequeue()
3
Pile
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(e): inserisce l'elemento e in testa
- pop(): preleva l'elemento affiorante
Pile
push(1)
1
push(2)
2
1
push(3)
3
2
1
pop()
2
1
pop()
1
Dizionari
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
Hash
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.
HashTable
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
Alberi
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
Alberi
- Il nodo iniziale è detta radice (root).
- I nodi hanno un rapporto di padre-figlio (parent-child).
- Un nodo privo di figli è detto foglia (leaf).
- Profondità: lunghezza del cammino dal nodo alla radice.
- Livello: insieme dei nodi alla stessa profondità.
- L'altezza di un albero è il massimo livello delle foglie.
I principali tipi di albero sono i seguenti:
- alberi binari: ogni nodo possiede sempre due figli
- alberi m-ari: ogni albero possiede un numero di figli non noto a priori.
Alberi
La ricerca di un elemento prevede una delle seguenti metodologie:
- visita in profondita (depth first search DFS): visito prima tutti i nodi dei figli, uno per volta, prima di valutare un fratello.
- visita in ampiezza (breadth first search BFS): visito l'albero di livello in livello.
Alberi
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;
Alberi
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)
Grafi
Un grafo è una struttura dati costituita da elementi contententi l'informazione, detti nodi, collegati tra loro attraverso archi.
1
2
3
4
5
6
Grafi
- Un grafo si dice orientato se si stabilisce un verso di percorrenza degli archi, non orientato altrimenti.
- Un insieme di nodi collegati è definito percorso (path).
- Un grafo può ammettere cicli. Nel caso in cui non li ammetta è definito aciclico.
- Un grado di un nodo è il numero degli archi entranti.
- Un arco può ammettere un peso diverso dall'unitario.
Un grafo ammette diverse rappresentazioni, le principali sono le seguenti:
- G(V,A): il grafo è modellato dall'insieme dei vertici V e dall'insieme degli archi A (rappresentati come coppie di nodi).
- Matrice di adiacenza: esiste un nodo tra v1 e v2 se la cella m[v1][v2] assume un valore intero positivo
Grafi
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, ?).
Grazie per l'attenzione!
Complessità ed Algoritmi e Strutture Dati
By Andrea Iuliano
Complessità ed Algoritmi e Strutture Dati
- 383