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 .