Pila/stack , code/queue , lista doppiamente concatenata.
Complessità liste:
- Search
$T(n)=O(n)$ - Insert
$T(n)=O(1)$ - Delete
$T(n)=O(1)$
Quando usare array:
- Conosco la dimensione in partenza
- Mi serve accesso diretto di valori vicini
- Il merge di k array ordinati di n elementi mi costa:
$O(nk^2)$ : scorro tutte le teste degli array O(k), trovo il min e lo aggiungo l’elemento trovato al nuovo array.
Quando usare liste:
- Il costo della costruzione non mi interessa (per liste ordinate)
- Voglio accedere sempre il min/max (liste ordinate), il primo/ultimo inserito (queue/stack)
Agli oggetti di un dizionario si accede tramite le chiavi, che sono numeri interi. Implementare un dizionario è molto semplice: vettore di
Caratteristiche:
- molto semplici
-
$OR$ ,$AND$ etc. sui bit mi permette di unire insiemi etc. - dizionari però sprecano spazio se gli elementi non sono continui.
Complessità dizionari:
- Search
$T(n)=O(1)$ - Insert
$T(n)=O(1)$ - Delete
$T(n)=O(1)$
Utilizzate da Python per implementare ad esempio i dizionari. Il concetto: data una chiave
Complessità Tabelle di Hashing:
- Search di k a tempo proporzionale alla lunghezza della lista di chiavi nella posizione f(k).
- Insert
$O(1)$ (se lo slot non è già occupato, altrimenti come il search) - Delete
$O(1)$ nel caso di liste doppiamente concatenate, altrimenti se concatenate solo da un lato è proporzionale alla lunghezza della lista di chiavi nella posizione f(k).
Quando usare hashing table:
-
Voglio accesso diretto a elementi sparsi o non accessibili facilmente (n numeri in un intervallo >> n, stringhe, altri dati non facilmente indicizzabili)
-
Le keywords "elementi distribuiti uniformemente", "accesso diretto" e "caso medio" implicano spesso l’utilizzo di una hast table.
Due tipi di implementazioni:
-
Tabelle di hashing ad indirizzamento aperto (closed hashing) I dati stanno sempre dentro, la tabella è quindi 'chiusa' e non ci sono liste che 'escono' da tale tabella. Per questo motivo l'indirizzamento è aperto, infatti una chiave non è per forza indirizzata in un indirizzo specifico ma può l'indirizzamento è aperto a 'qualsiasi' cella della tabella.
-
Tabelle di hashing ad indirizzamento chiuso (open hashing) " 'open' perchè la tabella è aperta letteralmente e dal vettore 'escono' le liste per ciascuna cella nel caso di sovrapposizione di chiavi". Di conseguenza si dice 'indirizzamento chiuso' perchè ogni chiave è indirizzata sempre e comunque ad una sola cella (chiuso in questo senso) e al più non sarà la prima della lista ma sarà in quella lista.
Calcolo h(k) che mi restituisce una cella.
Collisione?
Calcolo
Allocare per aggiungere liste è più dispendioso di semplici ispezioni. Problema delle ispezioni:
- ci potrebbe essere il clustering (raggruppamento): se continuo ad avere collisioni negli stessi punti, finisco che poi avrò continue collisioni nei posti vicini, creando un effetto a catena.
- La cancellazione in caso di indirizzamento aperto è un po' più complicata, in quanto non possiamo limitarci a mettere lo slot desiderato a
$NIL$ , altrimenti non riusciremmo più a trovare le chiavi inserite dopo quella cancellata. Una soluzione è quella di mettere nello slot, invece che$NIL$ , un valore convenzionale: una "tombstone" .
Esistono diverse funzioni di hashing e di ispezione.
Calcolo
-
Metodo della divisione
$$h(k) = (k)mod(m)$$ Facile da realizzare e veloce ma con l'accortezza di sui valori di$m$ : - evitare potenze di 2
-$m$ un numero primo non vicino ad una potenza esatta di 2. Per esempio$m$ =701 ci darebbe, se n=2000, in media 3 elementi per lista concatenata. -
Metodo della moltiplicazione $$h(k) = \lfloor m((kA)mod(1)) \rfloor$$dove
$(x)mod(1)$ è la parte frazionaria di$x$ . Spesso come$m$ si prende una potenza di 2 (per 'facilitare' i conti per il calcolatore) ed è utile prendere come A il valore proposto da Knuth:$$A = \frac{\sqrt{5} -1}{2}$$
Tecniche utilizzare in caso di collisioni.
Tre tecniche:
-
ispezione lineare (linear probing) linear probing, dopo la prima collisione per evitare clustering si fa
$h(k,i)=(h(k,0) + i)mod(m)$ come candidato per l'$i$ -esimo inserimento. Soffre di clustering primario: lunghe sequenze di celle occupate consecutive, che rallentano la ricerca. -
ispezione quadratica
$h(k,i)=(h(k,0)+c_{1}i^2+c_{2}i)mod(m)$ per evitare clustering banale all'intorno di alcuni elementi. Non è più garantito però che la sequenza di ispezioni tocchi tutte le celle. -
doppio hashing (double hashing)
$h(k,i)=(h_1(k)+h_2(k)i )mod(m)$ dove$h_1$ e$h_2$ sono funzioni hash di supporto.$h_2$ deve generare solo numeri dispari e che non mai$0$ (per non avere una sequenza di ispezione degenere).
NOTA: nessuna di queste tecniche produce le giuste permutazioni necessarie a soddisfare l'ispezione uniforme/perfetta: cioè in pratica visitare tutte le celle una ad una senza mai ripeterle... tuttavia, nella pratica si rivelano efficaci.
Con HashTable con indirizzamento chiuso, nel caso pessimo in cui tutti gli
Dati
Ogni chiave ha la stessa probabilità
Quindi la complessità temporale è
Dato
Il numero medio di ispezioni necessarie per effettuare l'inserimento di un nuovo oggetto nella tabella è
Il numero medio di ispezioni necessarie per trovare un elemento presente in tabella è