In che modo HashTables gestiscono le collisioni?


98

Ho sentito nelle mie classi di laurea che un HashTableinserirà una nuova voce nel secchio "successivo disponibile" se la nuova voce chiave entra in collisione con un'altra.

Come HashTablerestituirebbe ancora il valore corretto se questa collisione si verifica quando si richiama uno indietro con la chiave di collisione?

Sto assumendo che la Keysstanno Stringdigitare e le hashCode()restituisce il valore di default generato da dire Java.

Se implemento la mia funzione di hashing e la utilizzo come parte di una tabella di ricerca (ad esempio un HashMapor Dictionary), quali strategie esistono per gestire le collisioni?

Ho persino visto note relative ai numeri primi! Informazioni non così chiare dalla ricerca su Google.

Risposte:


93

Le tabelle hash gestiscono le collisioni in due modi.

Opzione 1: facendo in modo che ogni bucket contenga un elenco collegato di elementi sottoposti a hashing in quel bucket. Questo è il motivo per cui una cattiva funzione hash può rendere le ricerche nelle tabelle hash molto lente.

Opzione 2: se le voci della tabella hash sono tutte piene, la tabella hash può aumentare il numero di bucket di cui dispone e quindi ridistribuire tutti gli elementi nella tabella. La funzione hash restituisce un numero intero e la tabella hash deve prendere il risultato della funzione hash e modificarlo rispetto alla dimensione della tabella in modo che possa essere sicuro che arriverà al bucket. Quindi, aumentando la dimensione, eseguirà il rimaneggiamento ed eseguirà i calcoli del modulo che, se sei fortunato, potrebbero inviare gli oggetti a bucket diversi.

Java utilizza sia l'opzione 1 che 2 nelle sue implementazioni della tabella hash.


1
Nel caso della prima opzione, c'è qualche motivo per cui viene utilizzato un elenco collegato invece di un array o anche un albero di ricerca binario?

1
la spiegazione di cui sopra è di alto livello, non penso che faccia molta differenza tra lista collegata e array. Penso che un albero di ricerca binario sarebbe eccessivo. Inoltre penso che se approfondisci cose come ConcurrentHashMap e altri ci sono molti dettagli di implementazione di basso livello che possono fare una differenza di prestazioni, che la spiegazione di alto livello sopra non tiene conto.
AMS

2
Se viene utilizzato il concatenamento, quando viene fornita una chiave, come si fa a sapere quale elemento restituire?
ChaoSXDemon

1
@ChaoSXDemon puoi attraversare l'elenco nella catena per chiave, le chiavi duplicate non sono il problema, il problema è che due chiavi diverse hanno lo stesso hashcode.
ams

1
@ams: quale è il preferito? c'è un limite per la collisione hash, dopo di che il 2 ° punto viene eseguito da JAVA?
Shashank Vivek

78

Quando hai parlato di "Hash Table inserirà una nuova voce nel bucket" successivo disponibile "se la nuova voce Key entra in collisione con un'altra.", Stai parlando della strategia di indirizzamento aperta della risoluzione delle collisioni della tabella hash.


Esistono diverse strategie per la tabella hash per risolvere la collisione.

Il primo tipo di metodo grande richiede che le chiavi (o i puntatori ad esse) siano memorizzate nella tabella, insieme ai valori associati, che include inoltre:

  • Concatenamento separato

inserisci qui la descrizione dell'immagine

  • Indirizzamento aperto

inserisci qui la descrizione dell'immagine

  • Hashing coalizzato
  • Hashing del cuculo
  • Hashing di Robin Hood
  • Hash a 2 scelte
  • Hashing della campana

Un altro metodo importante per gestire la collisione è il ridimensionamento dinamico , che ha inoltre diversi modi:

  • Ridimensionamento copiando tutte le voci
  • Ridimensionamento incrementale
  • Tasti monotoni

EDIT : quanto sopra è preso in prestito da wiki_hash_table , dove dovresti andare a dare un'occhiata per avere maggiori informazioni.


3
"[...] richiede che le chiavi (o puntatori ad esse) siano memorizzate nella tabella, insieme ai valori associati". Grazie, questo è il punto che non è sempre immediatamente chiaro quando si legge sui meccanismi di memorizzazione dei valori.
mtone

27

Sono disponibili più tecniche per gestire la collisione. Ve ne spiegherò alcuni

Concatenamento: nel concatenamento utilizziamo gli indici di array per memorizzare i valori. Se il codice hash del secondo valore punta anche allo stesso indice, sostituiamo quel valore di indice con un elenco collegato e tutti i valori che puntano a quell'indice vengono memorizzati nell'elenco collegato e l'indice dell'array effettivo punta all'inizio dell'elenco collegato. Ma se c'è un solo codice hash che punta a un indice di array, il valore viene memorizzato direttamente in quell'indice. La stessa logica viene applicata durante il recupero dei valori. Viene utilizzato in Java HashMap / Hashtable per evitare collisioni.

Sondaggio lineare: questa tecnica viene utilizzata quando abbiamo più indice nella tabella rispetto ai valori da memorizzare. La tecnica del sondaggio lineare funziona sul concetto di continuare a incrementare fino a trovare uno slot vuoto. Lo pseudo codice ha questo aspetto:

index = h(k) 

while( val(index) is occupied) 

index = (index+1) mod n

Tecnica di doppio hashing: in questa tecnica usiamo due funzioni di hashing h1 (k) e h2 (k). Se lo slot in h1 (k) è occupato, la seconda funzione di hashing h2 (k) viene utilizzata per incrementare l'indice. Lo pseudo-codice ha questo aspetto:

index = h1(k)

while( val(index) is occupied)

index = (index + h2(k)) mod n

Le tecniche di sondaggio lineare e doppio hashing fanno parte della tecnica di indirizzamento aperto e possono essere utilizzate solo se gli slot disponibili sono superiori al numero di elementi da aggiungere. Richiede meno memoria rispetto al concatenamento perché qui non viene utilizzata alcuna struttura aggiuntiva, ma è lento a causa di molti movimenti fino a quando non troviamo uno slot vuoto. Anche nella tecnica di indirizzamento aperto quando un oggetto viene rimosso da uno slot mettiamo una lapide per indicare che l'oggetto viene rimosso da qui, motivo per cui è vuoto.

Per ulteriori informazioni vedere questo sito .


18

Ti consiglio caldamente di leggere questo post del blog apparso di recente su HackerNews: Come funziona HashMap in Java

In breve, la risposta è

Cosa succederà se due diversi oggetti chiave HashMap hanno lo stesso codice hash?

Verranno archiviati nello stesso bucket ma nessun nodo successivo dell'elenco collegato. E il metodo keys equals () verrà utilizzato per identificare la coppia di valori chiave corretta in HashMap.


3
Le HashMaps sono molto interessanti e vanno in profondità! :)
Alex

1
Penso che la domanda riguardi HashTables non HashMap
Prashant Shubham,

10

Ho sentito nelle mie classi di laurea che una tabella hash inserirà una nuova voce nel bucket "successivo disponibile" se la nuova voce chiave entra in collisione con un'altra.

Questo in realtà non è vero, almeno per Oracle JDK ( è un dettaglio di implementazione che potrebbe variare tra le diverse implementazioni dell'API). Ogni bucket contiene invece un elenco collegato di voci precedenti a Java 8 e un albero bilanciato in Java 8 o superiore.

allora come l'HashTable restituirebbe ancora il valore corretto se questa collisione si verifica quando ne richiama uno con il tasto di collisione?

Usa il equals()per trovare la voce effettivamente corrispondente.

Se implemento la mia funzione di hashing e la utilizzo come parte di una tabella di ricerca (cioè una HashMap o un dizionario), quali strategie esistono per gestire le collisioni?

Esistono varie strategie di gestione delle collisioni con diversi vantaggi e svantaggi. La voce di Wikipedia sulle tabelle hash fornisce una buona panoramica.


È vero per entrambi Hashtablee HashMapin jdk 1.6.0_22 di Sun / Oracle.
Nikita Rybak

@Nikita: non sono sicuro di Hashtable e non ho accesso ai sorgenti in questo momento, ma sono sicuro al 100% che HashMap utilizzi il concatenamento e non il sondaggio lineare in ogni singola versione che abbia mai visto nel mio debugger.
Michael Borgwardt

@Michael Bene, sto guardando la fonte di HashMap in public V get(Object key)questo momento (stessa versione di sopra). Se trovi una versione precisa in cui appaiono quegli elenchi collegati, sarei interessato a saperlo.
Nikita Rybak

@Niki: ora sto guardando lo stesso metodo, e vedo che usa un ciclo for per scorrere un elenco collegato di Entryoggetti:localEntry = localEntry.next
Michael Borgwardt

@Michael Scusa, è un mio errore. Ho interpretato il codice in modo sbagliato. naturalmente e = e.nextnon lo è ++index. +1
Nikita Rybak

7

Aggiornamento da Java 8: Java 8 utilizza un albero autobilanciato per la gestione delle collisioni, migliorando il caso peggiore da O (n) a O (log n) per la ricerca. L'uso di un albero autobilanciato è stato introdotto in Java 8 come miglioramento rispetto al concatenamento (utilizzato fino a java 7), che utilizza un elenco collegato e ha un caso peggiore di O (n) per la ricerca (poiché deve attraversare la lista)

Per rispondere alla seconda parte della tua domanda, l'inserimentoèfatto mappando un dato elemento a un dato indice nell'array sottostante della hashmap, tuttavia, quando si verifica una collisione, tutti gli elementi devono ancora essere preservati (memorizzati in una struttura dati secondaria e non solo sostituito nell'array sottostante). Questo di solito viene fatto rendendo ogni componente dell'array (slot) una struttura dati secondaria (aka bucket) e l'elemento viene aggiunto al bucket che risiede sull'indice dell'array dato (se la chiave non esiste già nel bucket, in in questo caso viene sostituito).

Durante la ricerca, la chiave viene sottoposta ad hashing al corrispondente indice di array e viene eseguita la ricerca di un elemento che corrisponde alla chiave (esatta) nel bucket specificato. Poiché il bucket non ha bisogno di gestire le collisioni (confronta direttamente le chiavi), questo risolve il problema delle collisioni, ma lo fa a costo di dover eseguire l'inserimento e la ricerca sulla struttura dati secondaria. Il punto chiave è che in una hashmap, sia la chiave che il valore vengono archiviati, quindi anche se l'hash si scontra, le chiavi vengono confrontate direttamente per l'uguaglianza (nel bucket) e quindi possono essere identificate in modo univoco nel bucket.

La gestione delle collisioni porta le prestazioni di inserimento e ricerca nel caso peggiore da O (1) in caso di mancata gestione delle collisioni a O (n) per il concatenamento (un elenco collegato viene utilizzato come struttura dati secondaria) e O (log n) per albero autobilanciato.

Riferimenti:

Java 8 include i seguenti miglioramenti / modifiche degli oggetti HashMap in caso di forti collisioni.

  • La funzione hash String alternativa aggiunta in Java 7 è stata rimossa.

  • I bucket contenenti un numero elevato di chiavi in ​​conflitto memorizzeranno le loro voci in un albero bilanciato anziché in un elenco collegato dopo che viene raggiunta una determinata soglia.

Le modifiche sopra riportate garantiscono le prestazioni di O (log (n)) negli scenari peggiori ( https://www.nagarro.com/en/blog/post/24/performance-improvement-for-hashmap-in-java-8 )


Puoi spiegare come l'inserimento nel caso peggiore per una HashMap di una lista collegata sia solo O (1) e non O (N)? Mi sembra che se hai un tasso di collisione del 100% per le chiavi non duplicate, finisci per dover attraversare ogni oggetto in HashMap per trovare la fine dell'elenco collegato, giusto? Cosa mi sto perdendo?
mbm29414

Nel caso specifico dell'implementazione dell'hashmap hai effettivamente ragione, ma non perché devi trovare la fine della lista. In un caso generale di implementazione di una lista collegata, un puntatore viene memorizzato sia in testa che in coda, e quindi l'inserimento può essere effettuato in O (1) attaccando il nodo successivo alla coda direttamente, ma nel caso di hashmap, il metodo di inserimento deve garantire l'assenza di duplicati, e quindi deve cercare nell'elenco per verificare se l'elemento esiste già, e quindi si finisce con O (n). E quindi è la proprietà set imposta su una lista collegata che causa O (N). Farò una correzione alla mia risposta :)
Daniel Valland

4

Utilizzerà il metodo uguale per vedere se la chiave è presente anche e soprattutto se ci sono più di un elemento nello stesso bucket.


4

Poiché vi è una certa confusione su quale algoritmo HashMap di Java sta utilizzando (nell'implementazione Sun / Oracle / OpenJDK), qui i frammenti di codice sorgente rilevanti (da OpenJDK, 1.6.0_20, su Ubuntu):

/**
 * Returns the entry associated with the specified key in the
 * HashMap.  Returns null if the HashMap contains no mapping
 * for the key.
 */
final Entry<K,V> getEntry(Object key) {
    int hash = (key == null) ? 0 : hash(key.hashCode());
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}

Questo metodo (la citazione è dalle righe 355 alla 371) viene chiamato quando si cerca una voce nella tabella, ad esempio da get(), containsKey()e alcuni altri. Il ciclo for qui passa attraverso l'elenco collegato formato dagli oggetti di ingresso.

Di seguito il codice per gli oggetti di ingresso (righe 691-705 + 759):

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    final int hash;

    /**
     * Creates new entry.
     */
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }

  // (methods left away, they are straight-forward implementations of Map.Entry)

}

Subito dopo arriva il addEntry()metodo:

/**
 * Adds a new entry with the specified key, value and hash code to
 * the specified bucket.  It is the responsibility of this
 * method to resize the table if appropriate.
 *
 * Subclass overrides this to alter the behavior of put method.
 */
void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    if (size++ >= threshold)
        resize(2 * table.length);
}

Questo aggiunge la nuova voce sulla parte anteriore del bucket, con un collegamento alla vecchia prima voce (o null, se non esiste). Allo stesso modo, il removeEntryForKey()metodo passa attraverso l'elenco e si occupa di eliminare solo una voce, lasciando intatto il resto dell'elenco.

Quindi, ecco un elenco di voci collegate per ogni bucket, e dubito molto che sia cambiato da _20a _22, poiché era così da 1.2 in poi.

(Questo codice è (c) 1997-2007 Sun Microsystems, e disponibile sotto GPL, ma per copiare meglio usare il file originale, contenuto in src.zip in ogni JDK da Sun / Oracle, e anche in OpenJDK.)


1
L'ho contrassegnato come wiki della comunità , poiché non è davvero una risposta, più un po 'di discussione sulle altre risposte. Nei commenti semplicemente non c'è abbastanza spazio per tali citazioni in codice.
Paŭlo Ebermann

3

ecco un'implementazione della tabella hash molto semplice in java. solo in implementa put()e get(), ma puoi facilmente aggiungere quello che vuoi. si basa sul hashCode()metodo java implementato da tutti gli oggetti. potresti facilmente creare la tua interfaccia,

interface Hashable {
  int getHash();
}

e costringilo a essere implementato dai tasti, se lo desideri.

public class Hashtable<K, V> {
    private static class Entry<K,V> {
        private final K key;
        private final V val;

        Entry(K key, V val) {
            this.key = key;
            this.val = val;
        }
    }

    private static int BUCKET_COUNT = 13;

    @SuppressWarnings("unchecked")
    private List<Entry>[] buckets = new List[BUCKET_COUNT];

    public Hashtable() {
        for (int i = 0, l = buckets.length; i < l; i++) {
            buckets[i] = new ArrayList<Entry<K,V>>();
        }
    }

    public V get(K key) {
        int b = key.hashCode() % BUCKET_COUNT;
        List<Entry> entries = buckets[b];
        for (Entry e: entries) {
            if (e.key.equals(key)) {
                return e.val;
            }
        }
        return null;
    }

    public void put(K key, V val) {
        int b = key.hashCode() % BUCKET_COUNT;
        List<Entry> entries = buckets[b];
        entries.add(new Entry<K,V>(key, val));
    }
}

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.