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

  • 377