Quali sono i vantaggi del sondaggio lineare su concatenazione separata o viceversa quando si implementano le tabelle hash?

6

Mi sono concentrato sugli algoritmi e ho esaminato questi due metodi di implementazione delle tabelle hash. Sembra che abbiano in gran parte caratteristiche di performance e requisiti di memoria simili.

Posso pensare ad alcuni svantaggi del sondaggio lineare - ovvero, che l'ampliamento dell'array potrebbe essere costoso (ma questo è fatto, cosa, 2 log N volte al massimo? Probabilmente non è un grosso problema) e che la gestione delle delezioni è un un po 'più difficile. Ma presumo che ci siano anche dei vantaggi, o che non sarebbero presentati nei libri di testo come un metodo attuabile di implementazione accanto all'implementazione più ovvia.

Perché sceglieresti l'uno rispetto all'altro?

    
posta Casey 07.04.2015 - 16:08
fonte

3 risposte

7

Con la scansione lineare (o qualsiasi sondaggio in realtà) una cancellazione deve essere "soft". Ciò significa che devi inserire un valore fittizio (spesso chiamato una pietra tombale) che non corrisponda a ciò che l'utente potrebbe cercare. O avresti bisogno di rifare ogni volta. Rialzo quando si accumulano troppe lapidi è ancora consigliato o qualche strategia per deframmentare il cimitero.

Il concatenamento separato (ciascun segmento è un puntatore a un elenco di valori collegato) ha lo svantaggio di finire alla ricerca in un elenco collegato con tutti i problemi relativi alla cache disponibili.

Un altro vantaggio del metodo di sondaggio è che tutti i valori vivono nello stesso array. Ciò semplifica la copia su scrittura semplicemente copiando solo la matrice. Se si può essere certi che l'originale non è modificato in modo invariante di classe, allora uno scatto è O (1) e può essere eseguito senza bloccare.

    
risposta data 07.04.2015 - 17:14
fonte
2

Dai un'occhiata a questa ottima risposta:

link

Citando qui:

I'm surprised that you saw chained hashing to be faster than linear probing - in practice, linear probing is typically significantly faster than chaining. In fact, that's the main reason it's used.

Although chained hashing is great in theory and linear probing has some known theoretical weaknesses (such as the need for five-way independence in the hash function to guarantee O(1) expected lookups), in practice linear probing is typically significantly faster due to locality of reference. Specifically, it's faster to access a series of elements in an array than it is to follow pointers in a linked list, so linear probing tends to outperform chained hashing even if it has to investigate more elements.

There are other wins in chained hashing. For example, insertions into a linear probing hash table don't require any new allocations (unless you're rehashing the table), so in applications like network routers where memory is scarce, it's nice to know that once the table is set up, the elements can be placed into it with no risk of a malloc fail.

    
risposta data 16.10.2015 - 08:30
fonte
1

Salterò con una risposta prevenuta dove in realtà preferisco il concatenamento separato con elenchi collegati singolarmente e lo trovo più semplice per ottenere prestazioni con loro (io sono non dicendo che sono ottimali, solo più facili per i miei casi d'uso), in quanto contraddittorio come sembra.

Ovviamente l'optimum teorico è ancora una tabella hash senza collisioni di sorta o una tecnica di sondaggio con clustering minimo. Tuttavia, la soluzione di concatenazione separata non deve necessariamente affrontare problemi di clustering.

Detto questo, la rappresentazione dei dati che utilizzo non richiama un'allocazione di memoria separata per nodo. Eccolo in C:

struct Bucket
{
    int head;
};

struct BucketNode
{
    int next;
    int element;
};

struct HashTable
{
    // Array of buckets, pre-allocated in advance.
    struct Bucket* buckets;

    // Array of nodes, pre-allocated assuming the client knows
    // how many nodes he's going to insert in advance. Otherwise
    // realloc using a similar strategy as std::vector in C++.
    struct BucketNode* nodes;

    // Number of bucket heads.
    int num_buckets;

    // Number of nodes inserted so far.
    int num_nodes;
};

I bucket sono solo indici a 32 bit (non utilizzo nemmeno una struct in realtà) ei nodi sono solo due indici a 32 bit. Spesso non ho nemmeno bisogno dell'indice element perché i nodi sono spesso memorizzati in parallelo con la matrice di elementi da inserire nella tabella, riducendo il sovraccarico della tabella hash a 32-bit per bucket e 32-bit per elemento inserito. La versione reale che uso più spesso assomiglia a questa:

struct HashTable
{
    // Array of head indices. The indices point to entries in the 
    // second array below.
    int* buckets;

    // Array of next indices parallel to the elements to insert.
    int* next_indices;

    // Number of bucket heads.
    int num_buckets;
};

Anche se la località spaziale si degrada, posso facilmente eseguire un passaggio di post-elaborazione in cui costruisco una nuova tabella hash in cui ciascun nodo del bucket è contiguo all'altro (funzione di copia banale che esegue semplicemente un passaggio lineare attraverso la tabella hash e i costrutti uno nuovo - a causa della natura in cui attraversa la tabella hash, la copia finisce con tutti i nodi vicini in un secchio contiguo tra loro).

Come per le tecniche di sondaggio, viene fornito con i vantaggi che la località spaziale è già lì dall'inizio senza pool di memoria o un backing array come io uso, e inoltre non hanno l'overhead a 32 bit per bucket e node , ma poi potresti dover affrontare problemi di clustering che possono iniziare ad accumularsi in modo vizioso con molte collisioni.

Trovo che la natura stessa del clustering sia un mal di testa che richiede molte analisi in caso di molte collisioni. Il vantaggio di questa soluzione è che posso ottenere spesso un risultato decente la prima volta senza analisi e test così approfonditi. Anche se la tabella si ridimensiona da sola in modo implicito, mi sono imbattuto in casi in cui tali progetti hanno finito per far esplodere l'utilizzo della memoria in modi che superano di gran lunga ciò che questa soluzione base che richiede un 32 bit per bucket e 32 bit per nodo anche nello scenario peggiore. È una soluzione che evita di diventare troppo male anche se ci sono un certo numero di collisioni.

La maggior parte della mia base di codice ruota intorno a strutture di dati che memorizzano indici e spesso memorizzano indici in parallelo con la matrice di elementi da inserire. Questo riduce le dimensioni della memoria, evita copie superflue e profonde degli elementi da inserire e rende molto facile ragionare sull'utilizzo della memoria. A parte questo, nel mio caso tendo a trarre beneficio dalle prestazioni prevedibili come prestazioni ottimali. Un algoritmo che è ottimale in molti scenari di casi comuni ma può funzionare in modo orribile negli scenari del caso peggiore è spesso meno preferibile per me rispetto a uno che funziona abbastanza bene tutto il tempo e non causa il rallentamento dei frame rate in momenti imprevedibili, e quindi tendono a favorire questo tipo di soluzioni.

    
risposta data 07.12.2017 - 12:17
fonte

Leggi altre domande sui tag