Skip to content

Latest commit

 

History

History
132 lines (78 loc) · 7.37 KB

File metadata and controls

132 lines (78 loc) · 7.37 KB

Strutture dati

Array

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)

Dizionari

Agli oggetti di un dizionario si accede tramite le chiavi, che sono numeri interi. Implementare un dizionario è molto semplice: vettore di $n$ bit in cui inserisco $1$ o meno all' $n$-esimo bit se il corrispondente numero è presente nel dizionario.

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)$

Tabelle di Hashing

Utilizzate da Python per implementare ad esempio i dizionari. Il concetto: data una chiave $k$, 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 $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.

Tabelle di hashing ad indirizzamento chiuso (open):

Calcolo h(k) che mi restituisce una cella. Collisione? $\rightarrow$ concateno: gli oggetti che vengono mappati sullo stresso slot vengono messi in una lista concatenata.

Tabelle di hashing ad indirizzamento aperto (closed):

Calcolo $h(k)$, ovvero una funzione che data $k$ mi restituisce una cella. Collisione? $\rightarrow$ 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 $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" .

Hashing

Esistono diverse funzioni di hashing e di ispezione. Calcolo $h(k)$, ovvero una funzione che data $k$ mi restituisce una cella. Diversi metodi di hashing:

  • 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 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 $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.

Analisi di complessità e fattore di carico

Indirizzamento chiuso

Con HashTable con indirizzamento chiuso, nel caso pessimo in cui tutti gli $n$ elementi memorizzati finiscono nello stesso slot la complessità è quella di una ricerca in una lista di $n$ elementi, cioè $O(n)$. In media, però, le cose non vanno così male.

Dati $m$ la dimensione della tabella ed $n$ il numero di elementi e $\alpha$ il fattore di carico, $\alpha = \frac{n}{m}$ . Siccome $0 \le n \le |U|$ avremo $0 \le \alpha \le \frac{|U|}{m}$ .

Ogni chiave ha la stessa probabilità $\frac{1}{m}$ di finire in una qualsiasi delle $m$ celle, indipendentemente dalle chiavi precedentemente inserite. Quindi la lunghezza media di una lista è il tempo medio per cercare una chiave $k$ non presente nella lista: $\Theta (1+\alpha)$ dove $O(1)$ è il tempo per calcolare $h(k)$, che si suppone sia costante.

Quindi la complessità temporale è $O(1)$ per tutte le operazioni (INSERT, SEARCH, DELETE) .

Indirizzamento aperto

Dato $\alpha = \frac{n}{m}$ , siccome abbiamo al massimo un oggetto per slot della tabella, $n \le m$, e $0 \le a \le 1$ .

Il numero medio di ispezioni necessarie per effettuare l'inserimento di un nuovo oggetto nella tabella è $m$ se $a = 1$ (se la tabella è piena), e non più di $\frac{1}{(1-a)}$ se $a<1$ .

Il numero medio di ispezioni necessarie per trovare un elemento presente in tabella è $(m+1)/2$ se $a=1$, e non più di $\frac{1}{a} log(\frac{1}{(1-a)})$ se $a<1$.