Qual è il significato del fattore di carico in HashMap?


233

HashMapha due proprietà importanti: sizee load factor. Ho esaminato la documentazione Java e dice che 0.75fè il fattore di carico iniziale. Ma non riesco a trovarne l'effettivo utilizzo.

Qualcuno può descrivere quali sono i diversi scenari in cui è necessario impostare il fattore di carico e quali sono alcuni valori ideali di esempio per casi diversi?

Risposte:


267

La documentazione lo spiega abbastanza bene:

Un'istanza di HashMap ha due parametri che influiscono sulle sue prestazioni: capacità iniziale e fattore di carico. La capacità è il numero di bucket nella tabella hash e la capacità iniziale è semplicemente la capacità al momento della creazione della tabella hash. Il fattore di carico è una misura della quantità massima consentita dalla tabella hash prima di aumentare automaticamente la sua capacità. Quando il numero di voci nella tabella hash supera il prodotto del fattore di carico e la capacità corrente, la tabella hash viene ridisegnata (ovvero, le strutture di dati interne vengono ricostruite) in modo che la tabella hash abbia circa il doppio del numero di bucket.

Come regola generale, il fattore di carico predefinito (.75) offre un buon compromesso tra tempo e costi di spazio. Valori più alti riducono il sovraccarico di spazio ma aumentano i costi di ricerca (riflessi nella maggior parte delle operazioni della classe HashMap, incluso get e put). Il numero previsto di voci nella mappa e il relativo fattore di carico devono essere presi in considerazione quando si imposta la sua capacità iniziale, in modo da ridurre al minimo il numero di operazioni di rehash. Se la capacità iniziale è maggiore del numero massimo di voci diviso per il fattore di carico, non si verificherà mai alcuna operazione di rehash.

Come per tutte le ottimizzazioni delle prestazioni, è una buona idea evitare di ottimizzare prematuramente le cose (cioè senza dati concreti su dove si trovano i colli di bottiglia).


14
Altre risposte suggeriscono di specificare capacity = N/0.75di evitare il reinserimento, ma il mio pensiero iniziale era appena stato impostato load factor = 1. Ci sarebbero svantaggi di questo approccio? Perché il fattore di carico influirebbe get()e i put()costi operativi?
supermitch

19
Un fattore di carico = 1 hashmap con numero di voci = capacità avrà statisticamente un numero significativo di collisioni (= quando più chiavi producono lo stesso hash). Quando si verifica una collisione, il tempo di ricerca aumenta, poiché in un bucket ci saranno> 1 voci corrispondenti, per le quali la chiave deve essere controllata individualmente per l'uguaglianza. Alcuni calcoli dettagliati: preshing.com/20110504/hash-collision-probabilities
atimb

8
Non ti sto seguendo @atimb; La proprietà loadset viene utilizzata solo per determinare quando aumentare le dimensioni della memoria, giusto? - In che modo avere un loadset di uno aumenterebbe la probabilità di collisioni hash? - L'algoritmo di hashing non è a conoscenza di quanti elementi sono presenti nella mappa o di quanto spesso acquisisce nuovi "bucket" di archiviazione, ecc. Per qualsiasi set di oggetti della stessa dimensione, indipendentemente da come sono memorizzati, dovresti avere il stessa probabilità di valori hash ripetuti ...
BrainSlugs83

19
La probabilità della collisione dell'hash è inferiore, se la dimensione della mappa è maggiore. Ad esempio, gli elementi con i codici hash 4, 8, 16 e 32 verranno inseriti nello stesso bucket, se la dimensione della mappa è 4, ma ogni elemento avrà un bucket, se la dimensione della mappa è superiore a 32. La mappa con dimensione iniziale 4 e fattore di carico 1,0 (4 secchi, ma tutti i 4 elementi in un singolo secchio) sarà in questo esempio in media due volte più lenta di un'altra con il fattore di carico 0,75 (8 secchi, due secchi riempiti - con l'elemento "4" e con gli elementi "8", "16", "32").
30

1
Il costo di ricerca di @Adelin è aumentato per fattori di carico più elevati perché ci saranno più collisioni per valori più alti e il modo in cui Java gestisce le collisioni è inserendo gli articoli con lo stesso hashcode nello stesso bucket usando una struttura di dati. A partire da Java 8, questa struttura di dati è un albero di ricerca binario. Questo rende la ricerca nel caso peggiore della complessità O (lg (n)) con il caso peggiore che si verifica se tutti gli elementi aggiunti hanno lo stesso hashcode.
Gigi Bayte 2

141

La capacità iniziale predefinita dei HashMapTake è 16 e il fattore di carico è 0,75f (ovvero il 75% della dimensione attuale della mappa). Il fattore di carico rappresenta a quale livello la HashMapcapacità dovrebbe essere raddoppiata.

Ad esempio prodotto di capacità e fattore di carico come 16 * 0.75 = 12. Ciò significa che dopo aver memorizzato la dodicesima coppia chiave-valore in HashMap, la sua capacità diventa 32.


3
Sebbene la tua risposta sia chiara, puoi dire se, dopo aver memorizzato 12 coppie chiave-valore, la capacità diventa 32 o è che quando viene aggiunta la 13a voce, in quel momento la capacità cambia e quindi la voce viene inserita.
userab

significa che il numero di bucket è aumentato di 2?
LoveMeow,

39

In realtà, dai miei calcoli, il fattore di carico "perfetto" è più vicino al registro 2 (~ 0,7). Sebbene qualsiasi fattore di carico inferiore a questo produrrà prestazioni migliori. Penso che .75 sia stato probabilmente tirato fuori da un cappello.

Prova:

È possibile evitare il concatenamento e sfruttare la previsione del ramo prevedendo se un secchio è vuoto o meno. Un bucket è probabilmente vuoto se la probabilità che sia vuoto supera 0,5.

Rappresentiamo la dimensione e n il numero di chiavi aggiunte. Usando il teorema binomiale, la probabilità che un bucket sia vuoto è:

P(0) = C(n, 0) * (1/s)^0 * (1 - 1/s)^(n - 0)

Pertanto, un secchio è probabilmente vuoto se ce ne sono meno di

log(2)/log(s/(s - 1)) keys

Quando s raggiunge l'infinito e se il numero di chiavi aggiunte è tale che P (0) = .5, allora n / s si avvicina rapidamente al registro (2):

lim (log(2)/log(s/(s - 1)))/s as s -> infinity = log(2) ~ 0.693...

4
Math nerds FTW! Probabilmente è .75stato arrotondato alla frazione più facile da capire log(2)e sembra meno di un numero magico. Mi piacerebbe vedere un aggiornamento del valore predefinito JDK, con detto commento sopra la sua implementazione: D
Decodificato il

2
Voglio davvero apprezzare questa risposta, ma sono uno sviluppatore JavaEE, il che significa che la matematica non è mai stata la mia forza maggiore, quindi capisco molto poco di quello che hai scritto lol
searchengine27

28

Cos'è il fattore di carico?

La quantità di capacità che deve essere esaurita affinché HashMap ne aumenti la capacità?

Perché caricare il fattore?

Il fattore di carico è di default 0,75 della capacità iniziale (16), quindi il 25% dei bucket sarà libero prima che si verifichi un aumento della capacità e questo fa sì che molti nuovi bucket con nuovi hashcode che puntano a loro esistano subito dopo l'aumento del numero di secchi.

Ora perché dovresti conservare molti bucket gratuiti e qual è l'impatto del mantenimento dei bucket gratuiti sulle prestazioni?

Se imposti il ​​fattore di caricamento su 1.0, potrebbe succedere qualcosa di molto interessante.

Supponi di aggiungere un oggetto x alla tua hashmap il cui hashCode è 888 e nella tua hashmap il bucket che rappresenta l'hashcode è gratuito, quindi l' oggetto x viene aggiunto al bucket, ma ora di nuovo dì se stai aggiungendo un altro oggetto y il cui hashCode è anche 888 quindi il tuo oggetto y verrà sicuramente aggiunto MA alla fine del bucket ( perché i bucket non sono altro che linkList dell'implementazione che memorizza chiave, valore e successivo ) ora questo ha un impatto sulle prestazioni! Poiché il tuo oggetto non è più presente nella testa del bucket se esegui una ricerca, il tempo impiegato non sarà O (1) questa volta dipende da quanti oggetti ci sono nello stesso secchio. Questo è chiamato hash collision tra l'altro e questo accade anche quando il fattore di caricamento è inferiore a 1.

Correlazione tra prestazioni, collisione hash e fattore di caricamento?

Fattore di carico inferiore = più secchi liberi = minori possibilità di collisione = alte prestazioni = ingombro elevato.

Correggimi se sbaglio da qualche parte.


2
Potresti aggiungere un po 'di come l'hashCode viene ridotto a un numero con l'intervallo 1- {count bucket}, e quindi non è di per sé il numero di bucket, ma quel risultato finale dell'algoritmo hash copre un gamma più ampia. HashCode non è l'algoritmo di hash completo, è solo abbastanza piccolo da poter essere facilmente rielaborato. Quindi non esiste il concetto di "bucket gratuiti", ma di "numero minimo di bucket gratuiti", dal momento che è possibile archiviare tutti gli elementi nello stesso bucket. Piuttosto, è lo spazio-chiave del tuo hashcode, che è uguale alla capacità * (1 / load_factor). 40 elementi, fattore di carico 0,25 = 160 secchi.
user1122069,

Penso che il tempo di ricerca di un oggetto dal LinkedListsia indicato Amortized Constant Execution Timee indicato con un +asO(1)+
Raf

19

Dalla documentazione :

Il fattore di carico è una misura della quantità massima consentita dalla tabella hash prima di aumentare automaticamente la sua capacità

Dipende molto dalle tue esigenze particolari, non esiste una "regola empirica" ​​per specificare un fattore di carico iniziale.


La documentazione dice anche; "Come regola generale, il fattore di carico predefinito (.75) offre un buon compromesso tra tempo e costi di spazio.". Quindi, per chiunque non sia sicuro, il valore predefinito è una buona regola empirica.
Ferekdoley,


2

Se i secchi diventano troppo pieni, allora dobbiamo guardare attraverso

un lungo elenco di link.

E questo è un po 'come sconfiggere il punto.

Quindi, ecco un esempio in cui ho quattro secchi.

Finora ho un elefante e un tasso nel mio HashSet.

Questa è una situazione abbastanza buona, vero?

Ogni elemento ha zero o un elemento.

Ora inseriamo altri due elementi nel nostro HashSet.

     buckets      elements
      -------      -------
        0          elephant
        1          otter
         2          badger
         3           cat

Neanche questo è male.

Ogni secchio ha solo un elemento. Quindi, se voglio saperlo, contiene panda?

Posso guardare molto velocemente il secchio numero 1 e non lo è

lì e

Sapevo che non era nella nostra collezione.

Se voglio sapere se contiene un gatto, guardo il secchio

numero 3,

Trovo il gatto, so molto rapidamente se è nel nostro

collezione.

E se aggiungessi il koala, beh, non è poi così male.

             buckets      elements
      -------      -------
        0          elephant
        1          otter -> koala 
         2          badger
         3           cat

Forse ora invece che nel secchio numero 1 solo guardando

un elemento,

Ho bisogno di guardare due.

Ma almeno non devo guardare elefante, tasso e

gatto.

Se sto di nuovo cercando un panda, può essere solo nel secchio

numero 1 e

Non devo guardare altro che lontra e

koala.

Ma ora ho messo il coccodrillo nel secchio numero 1 e puoi farlo

vedi forse dove sta andando.

Che se il secchio numero 1 continua a diventare sempre più grande e

più grande, quindi devo praticamente esaminare tutto

quegli elementi da trovare

qualcosa che dovrebbe essere nel secchio numero 1.

            buckets      elements
      -------      -------
        0          elephant
        1          otter -> koala ->alligator
         2          badger
         3           cat

Se inizio ad aggiungere stringhe ad altri bucket,

giusto, il problema diventa sempre più grande in ogni

secchio singolo.

Come possiamo evitare che i nostri secchi si riempiano troppo?

La soluzione qui è quella

          "the HashSet can automatically

        resize the number of buckets."

C'è che HashSet si rende conto che stanno ottenendo i secchi

troppo pieno.

Sta perdendo questo vantaggio di tutta questa ricerca

elementi.

E creerà solo più secchi (generalmente due volte come prima) e

quindi posizionare gli elementi nel secchio corretto.

Quindi ecco la nostra implementazione di base di HashSet con separata

concatenamento. Ora creerò un "HashSet auto-ridimensionante".

Questo HashSet si renderà conto che i secchi lo sono

diventando troppo pieno e

ha bisogno di più secchi.

loadFactor è un altro campo nella nostra classe HashSet.

loadFactor rappresenta il numero medio di elementi per

secchio,

sopra il quale vogliamo ridimensionare.

loadFactor è un equilibrio tra spazio e tempo.

Se i secchi si riempiono troppo, ridimensioneremo.

Ci vuole tempo, ovviamente, ma

potrebbe farci risparmiare tempo lungo la strada se i secchi sono a

un po 'più vuoto.

Vediamo un esempio.

Ecco un HashSet, finora abbiamo aggiunto quattro elementi.

Elefante, cane, gatto e pesce.

          buckets      elements
      -------      -------
        0          
        1          elephant
         2          cat ->dog
         3           fish
          4         
           5

A questo punto, ho deciso che loadFactor, il

soglia,

il numero medio di elementi per secchio che sto bene

con, è 0,75.

Il numero di bucket è buckets.length, che è 6 e

a questo punto il nostro HashSet ha quattro elementi, quindi il

la dimensione attuale è 4.

Ridimensioneremo il nostro HashSet, ovvero aggiungeremo altri bucket,

quando il numero medio di elementi per bucket supera

il loadFactor.

Cioè quando la dimensione attuale divisa per bucket.length è

maggiore di loadFactor.

A questo punto, il numero medio di elementi per bucket

è 4 diviso per 6.

4 elementi, 6 secchi, questo è 0.67.

Questo è inferiore alla soglia che ho impostato di 0,75, quindi lo siamo

va bene.

Non abbiamo bisogno di ridimensionare.

Ma ora diciamo che aggiungiamo marmotta.

                  buckets      elements
      -------      -------
        0          
        1          elephant
         2        woodchuck-> cat ->dog
         3           fish
          4         
           5

Woodchuck sarebbe finito nel secchio numero 3.

A questo punto, currentSize è 5.

E ora il numero medio di elementi per bucket

è la dimensione corrente divisa per buckets.length.

Quello è 5 elementi divisi per 6 secchi è 0,83.

E questo supera il loadFactor che era 0,75.

Al fine di affrontare questo problema, al fine di rendere il

secchi forse un po '

più vuoto in modo che operazioni come determinare se a

contiene secchio

un elemento sarà un po 'meno complesso, voglio ridimensionare

il mio HashSet.

Il ridimensionamento di HashSet richiede due passaggi.

Per prima cosa raddoppierò il numero di secchi, ne avevo 6,

ora avrò 12 secchi.

Nota qui che il loadFactor che ho impostato su 0,75 rimane lo stesso.

Ma il numero di bucket modificati è 12,

il numero di elementi è rimasto lo stesso, è 5.

5 diviso 12 è circa 0,42, che è ben al di sotto della nostra

fattore di carico,

quindi ora stiamo bene.

Ma non abbiamo finito perché alcuni di questi elementi sono presenti

il secchio sbagliato ora.

Ad esempio, elefante.

Elephant era nel secchio numero 2 perché il numero di

personaggi in elefante

aveva 8 anni.

Abbiamo 6 secchi, 8 meno 6 è 2.

Ecco perché è finito nel numero 2.

Ma ora che abbiamo 12 secchi, 8 mod 12 è 8, quindi

l'elefante non appartiene più al secchio numero 2.

L'elefante appartiene al secchio numero 8.

Che dire di marmotta?

Woodchuck è stato colui che ha iniziato l'intero problema.

Woodchuck è finito nel secchio numero 3.

Perché 9 mod 6 è 3.

Ma ora facciamo 9 mod 12.

9 mod 12 è 9, woodchuck va al secchio numero 9.

E vedi il vantaggio di tutto questo.

Ora il secchio numero 3 ha solo due elementi mentre prima ne aveva 3.

Quindi ecco il nostro codice,

dove abbiamo avuto il nostro HashSet con questo incatenamento separato

non ha fatto alcun ridimensionamento.

Ora, ecco una nuova implementazione in cui utilizziamo il ridimensionamento.

Gran parte di questo codice è lo stesso,

determineremo ancora se contiene il file

valore già.

In caso contrario, scopriremo quale secchio

dovrebbe andare in e

quindi aggiungilo a quel bucket, aggiungilo a quell'elenco collegato.

Ma ora incrementiamo il campo currentSize.

currentSize era il campo che teneva traccia del numero

di elementi nel nostro HashSet.

Lo incrementeremo e poi guarderemo

al carico medio,

il numero medio di elementi per bucket.

Faremo quella divisione qui.

Dobbiamo fare un po 'di casting qui per essere sicuri

che otteniamo un doppio.

E poi, confronteremo quel carico medio con il campo

che ho impostato come

0.75 quando ho creato questo HashSet, ad esempio, che era

il loadFactor.

Se il carico medio è maggiore di loadFactor,

ciò significa che ci sono troppi elementi per secchio

nella media e devo reinserirlo.

Quindi, ecco la nostra implementazione del metodo per reinserirla

tutti gli elementi.

Innanzitutto, creerò una variabile locale chiamata oldBuckets.

Che si riferisce ai secchi così come sono attualmente

prima di iniziare a ridimensionare tutto.

Nota Non sto ancora creando un nuovo array di elenchi collegati.

Sto solo rinominando i bucket come oldBuckets.

Ora ricordo che i secchi erano un campo nella nostra classe, vado

per ora creare un nuovo array

di elenchi collegati ma questo avrà il doppio degli elementi

come ha fatto la prima volta.

Ora devo effettivamente reinserire,

Esaminerò tutti i vecchi secchi.

Ogni elemento in oldBuckets è un LinkedList di stringhe

quello è un secchio.

Esaminerò quel secchio e ne prenderò ogni elemento

secchio.

E ora lo reinserirò nei newBuckets.

Prenderò il suo hashCode.

Capirò quale indice è.

E ora ho il nuovo bucket, il nuovo LinkedList di

stringhe e

Lo aggiungerò a quel nuovo secchio.

Quindi, per ricapitolare, gli Hashset come abbiamo visto sono array di Linked

Elenchi o secchi.

Un HashSet auto-ridimensionante può realizzare usando un certo rapporto o


1

Sceglierei una dimensione della tabella di n * 1.5 o n + (n >> 1), questo darebbe un fattore di carico di .66666 ~ senza divisione, che è lento sulla maggior parte dei sistemi, specialmente su sistemi portatili in cui non c'è divisione in l'hardware.

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.