Strutture dati
Array
Pila/stack , code/queue , lista doppiamente concatenata.
Complessità liste:
- Search
- Insert
- Delete
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: : 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)
Dizionari
Agli oggetti di un dizionario si accede tramite le chiavi, che sono numeri interi. Implementare un dizionario è molto semplice: vettore di bit in cui inserisco o meno all’ -esimo bit se il corrispondente numero è presente nel dizionario.
Caratteristiche:
- molto semplici
- , etc. sui bit mi permette di unire insiemi etc.
- dizionari però sprecano spazio se gli elementi non sono continui.
Complessità dizionari:
- Search
- Insert
- Delete
Tabelle di Hashing
Utilizzate da Python per implementare ad esempio i dizionari. Il concetto: data una chiave , so la posizione in cui si trova nel mio array tale chiave e ci accedo direttamente. Dentro ogni cella vuota, mi serve un valore di garbage, che non deve appartenere al dominio delle mie key! Quindi il vettore iniziale lo inizializzo con i simboli di garbage.
Complessità Tabelle di Hashing:
- Search di k a tempo proporzionale alla lunghezza della lista di chiavi nella posizione f(k).
- Insert (se lo slot non è già occupato, altrimenti come il search)
- Delete 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.
Tabelle di hashing ad indirizzamento chiuso (open):
Calcolo h(k) che mi restituisce una cella. Collisione? concateno: gli oggetti che vengono mappati sullo stresso slot vengono messi in una lista concatenata.
Tabelle di hashing ad indirizzamento aperto (closed):
Calcolo , ovvero una funzione che data mi restituisce una cella. Collisione? ispeziono con una tecnica di ispezione, cioè cerco un’altra cella con una formula.
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 , altrimenti non riusciremmo più a trovare le chiavi inserite dopo quella cancellata. Una soluzione è quella di mettere nello slot, invece che , un valore convenzionale: una “tombstone” .
Hashing
Esistono diverse funzioni di hashing e di ispezione. Calcolo , ovvero una funzione che data mi restituisce una cella. Diversi metodi di hashing:
-
Metodo della divisione Facile da realizzare e veloce ma con l’accortezza di sui valori di : - evitare potenze di 2
- un numero primo non vicino ad una potenza esatta di 2. Per esempio =701 ci darebbe, se n=2000, in media 3 elementi per lista concatenata. -
Metodo della moltiplicazione dove è la parte frazionaria di . Spesso come si prende una potenza di 2 (per ‘facilitare’ i conti per il calcolatore) ed è utile prendere come A il valore proposto da Knuth:
Tecniche di ispezione
Tecniche utilizzare in caso di collisioni.
Tre tecniche:
-
ispezione lineare (linear probing) linear probing, dopo la prima collisione per evitare clustering si fa come candidato per l’ -esimo inserimento. Soffre di clustering primario: lunghe sequenze di celle occupate consecutive, che rallentano la ricerca.
-
ispezione quadratica 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) dove e sono funzioni hash di supporto. deve generare solo numeri dispari e che non mai (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.
Analisi di complessità e fattore di carico
Indirizzamento chiuso
Con HashTable con indirizzamento chiuso, nel caso pessimo in cui tutti gli elementi memorizzati finiscono nello stesso slot la complessità è quella di una ricerca in una lista di elementi, cioè . In media, però, le cose non vanno così male.
Dati la dimensione della tabella ed il numero di elementi e il fattore di carico, . Siccome avremo .
Ogni chiave ha la stessa probabilità di finire in una qualsiasi delle celle, indipendentemente dalle chiavi precedentemente inserite. Quindi la lunghezza media di una lista è il tempo medio per cercare una chiave non presente nella lista: dove è il tempo per calcolare , che si suppone sia costante.
Quindi la complessità temporale è per tutte le operazioni (INSERT, SEARCH, DELETE) .
Indirizzamento aperto
Dato , siccome abbiamo al massimo un oggetto per slot della tabella, , e .
Il numero medio di ispezioni necessarie per effettuare l’inserimento di un nuovo oggetto nella tabella è se (se la tabella è piena), e non più di se .
Il numero medio di ispezioni necessarie per trovare un elemento presente in tabella è se , e non più di se .