Um hashmap Java é realmente O (1)?


159

Eu já vi algumas afirmações interessantes sobre os hashmaps SO re Java e seu O(1)tempo de pesquisa. Alguém pode explicar por que isso é assim? A menos que esses hashmaps sejam muito diferentes de qualquer um dos algoritmos de hash em que eu comprei, sempre deve existir um conjunto de dados que contenha colisões.

Nesse caso, a pesquisa seria O(n)melhor que O(1).

Alguém pode explicar se eles são O (1) e, em caso afirmativo, como conseguem isso?


1
Eu sei que isso pode não ser uma resposta, mas eu lembro que a Wikipedia tem um artigo muito bom sobre isso. Não perca a seção de análise de desempenho
victor hugo

28
A notação Big O fornece um limite superior para o tipo específico de análise que você está fazendo. Você ainda deve especificar se está interessado nos piores casos, casos médios etc.
Dan Homerick 28/06/2009

Respostas:


127

Uma característica específica de um HashMap é que, diferentemente das árvores balanceadas, seu comportamento é probabilístico. Nesses casos, geralmente é mais útil falar sobre complexidade em termos da probabilidade de ocorrência de um evento de pior caso. Para um mapa de hash, é claro que esse é o caso de uma colisão com relação ao quão cheio o mapa está. É fácil estimar uma colisão.

p colisão = n / capacidade

Portanto, é provável que um mapa de hash com até um número modesto de elementos sofra pelo menos uma colisão. A notação Big O nos permite fazer algo mais atraente. Observe que para qualquer constante fixa e arbitrária k.

O (n) = O (k * n)

Podemos usar esse recurso para melhorar o desempenho do mapa de hash. Em vez disso, poderíamos pensar na probabilidade de no máximo 2 colisões.

p colisão x 2 = (n / capacidade) 2

Isso é muito menor. Como o custo de lidar com uma colisão extra é irrelevante para o desempenho do Big O, encontramos uma maneira de melhorar o desempenho sem realmente alterar o algoritmo! Podemos generalizar isso para

p colisão xk = (n / capacidade) k

E agora podemos desconsiderar um número arbitrário de colisões e acabar com uma probabilidade muito pequena de mais colisões do que estamos contabilizando. Você pode obter a probabilidade para um nível arbitrariamente pequeno escolhendo o k correto, tudo sem alterar a implementação real do algoritmo.

Falamos sobre isso dizendo que o mapa de hash tem acesso O (1) com alta probabilidade


Mesmo com HTML, ainda não estou muito feliz com as frações. Limpe-os se você puder pensar em uma boa maneira de fazê-lo.
SingleNegationElimination

4
Na verdade, o que diz acima é que os efeitos O (log N) são ocultados, para valores não extremos de N, pela sobrecarga fixa.
Hot Licks

Tecnicamente, esse número que você forneceu é o valor esperado do número de colisões, que pode ser igual à probabilidade de uma única colisão.
Simon Kuang

1
É semelhante à análise amortizada?
lostsoul29

1
@ OleV.V. O bom desempenho de um HashMap sempre depende de uma boa distribuição de sua função hash. Você pode trocar uma melhor qualidade de hash por velocidade de hash usando uma função de hash criptográfico em sua entrada.
SingleNegationElimination

38

Você parece misturar o pior comportamento com o tempo médio de execução (esperado). O primeiro é realmente O (n) para tabelas de hash em geral (isto é, não usando um hash perfeito), mas isso raramente é relevante na prática.

Qualquer implementação de tabela de hash confiável, juntamente com um hash meio decente, tem um desempenho de recuperação de O (1) com um fator muito pequeno (2, de fato) no caso esperado, dentro de uma margem de variação muito estreita.


6
Eu sempre pensei que o limite superior era o pior caso, mas parece que eu estava enganado - você pode ter o limite superior para o caso médio. Portanto, parece que as pessoas que reivindicam O (1) deveriam ter deixado claro que isso ocorreu em casos médios. O pior caso é um conjunto de dados em que há muitas colisões tornando O (n). Isso faz sentido agora.
21430

2
Provavelmente, você deve deixar claro que, ao usar a notação O grande para o caso médio, está falando de um limite superior na função de tempo de execução esperada, que é uma função matemática claramente definida. Caso contrário, sua resposta não faz muito sentido.
ldog

1
gmatt: Não sei se entendi sua objeção: a notação big-O é um limite superior da função por definição . O que mais eu poderia, portanto, dizer?
Konrad Rudolph

3
bem, geralmente na literatura de computador, você vê uma grande notação O representando um limite superior nas funções de tempo de execução ou complexidade espacial de um algoritmo. Nesse caso, o limite superior está na expectativa, que não é uma função, mas um operador nas funções (Variáveis ​​Aleatórias) e, na verdade, é uma integral (lebesgue.) O próprio fato de poder vincular uma coisa dessas não deve ser considerado por garantido e não é trivial.
Ldog

31

Em Java, o HashMap funciona usando o hashCode para localizar um bucket. Cada balde é uma lista de itens que residem nesse balde. Os itens são digitalizados, usando iguais para comparação. Ao adicionar itens, o HashMap é redimensionado quando uma certa porcentagem de carga é atingida.

Portanto, às vezes, ele deve ser comparado a alguns itens, mas geralmente é muito mais próximo de O (1) do que de O (n). Para fins práticos, é tudo o que você precisa saber.


11
Bem, como big-O deve especificar os limites, não faz diferença se está mais perto de O (1) ou não. Mesmo O (n / 10 ^ 100) ainda é O (n). Eu entendo seu ponto de vista de reduzir a eficiência, mas isso ainda coloca o algoritmo em O (n).
28411

4
A análise de mapas de hash geralmente é no caso médio, que é O (1) (com conluios) No pior caso, você pode ter O (n), mas esse geralmente não é o caso. com relação à diferença - O (1) significa que você obtém o mesmo tempo de acesso, independentemente da quantidade de itens no gráfico, e esse é geralmente o caso (desde que haja uma boa proporção entre o tamanho da tabela e 'n ')
Liran Orevi 28/06/09

4
Também é importante notar que ele ainda é exatamente O (1), mesmo que a varredura do balde demore um pouco, porque já existem alguns elementos nele. Desde que os baldes tenham um tamanho máximo fixo, esse é apenas um fator constante irrelevante para a classificação O (). Mas é claro que pode haver ainda mais elementos com chaves "semelhantes" adicionadas, para que esses buckets transbordem e você não possa mais garantir uma constante.
sth

@sth Por que os baldes teriam um tamanho máximo fixo?
Navin

31

Lembre-se de que o (1) não significa que cada pesquisa examina apenas um único item - significa que o número médio de itens verificados permanece constante em relação ao número de itens no contêiner. Portanto, se levar em média 4 comparações para encontrar um item em um contêiner com 100 itens, também será necessário em média 4 comparações para encontrar um item em um contêiner com 10000 itens e para qualquer outro número de itens (sempre há um pouca variação, especialmente em torno dos pontos em que a tabela de hash é repetida e quando há um número muito pequeno de itens).

Portanto, colisões não impedem que o contêiner tenha o (1) operações, desde que o número médio de chaves por bucket permaneça dentro de um limite fixo.


16

Eu sei que essa é uma pergunta antiga, mas na verdade há uma nova resposta para ela.

Você está certo que um mapa de hash não é realmente O(1), estritamente falando, porque como o número de elementos aumenta arbitrariamente, eventualmente você não poderá pesquisar em tempo constante (e a notação O é definida em termos de números que podem arbitrariamente grande).

Mas não se segue que a complexidade em tempo real seja - O(n)porque não há regra que diga que os buckets precisam ser implementados como uma lista linear.

De fato, o Java 8 implementa os buckets TreeMapsassim que eles excedem um limite, o que torna o tempo real O(log n).


4

Se o número de buckets (denominado b) for mantido constante (o caso usual), a pesquisa será realmente O (n).
À medida que n aumenta, o número de elementos em cada intervalo é em média n / b. Se a resolução de colisão for feita de uma das formas usuais (lista vinculada, por exemplo), a pesquisa será O (n / b) = O (n).

A notação O é sobre o que acontece quando n fica cada vez maior. Pode ser enganoso quando aplicado a certos algoritmos, e as tabelas de hash são um exemplo disso. Escolhemos o número de buckets com base em quantos elementos esperamos lidar. Quando n é aproximadamente do mesmo tamanho que b, a pesquisa é aproximadamente constante, mas não podemos chamá-lo de O (1) porque O é definido em termos de limite como n → ∞.



2

Estabelecemos que a descrição padrão das pesquisas de tabela de hash sendo O (1) refere-se ao tempo médio esperado, e não ao desempenho estrito do pior caso. Para uma tabela de hash que resolve colisões com encadeamento (como o mapa de hash de Java), isso é tecnicamente O (1 + α) com uma boa função de hash , onde α é o fator de carga da tabela. Ainda constante, desde que o número de objetos que você esteja armazenando não seja mais que um fator constante maior que o tamanho da tabela.

Também foi explicado que, estritamente falando, é possível construir entradas que exijam O ( n ) pesquisas para qualquer função hash determinística. Mas também é interessante considerar o pior tempo esperado , que é diferente do tempo médio de pesquisa. Usando encadeamento, isso é O (1 + o comprimento da cadeia mais longa), por exemplo Θ (log n / log log n ) quando α = 1.

Se você estiver interessado em maneiras teóricas para obter pesquisas de pior caso esperadas em tempo constante, pode ler sobre o hash perfeito dinâmico, que resolve colisões recursivamente com outra tabela de hash!


2

É O (1) somente se sua função de hash for muito boa. A implementação da tabela de hash Java não protege contra funções de hash incorretas.

Se você precisa aumentar a tabela quando adiciona itens ou não, isso não é relevante para a pergunta, porque se trata de tempo de pesquisa.


2

Os elementos dentro do HashMap são armazenados como uma matriz de lista vinculada (nó), cada lista vinculada na matriz representa um intervalo para o valor de hash exclusivo de uma ou mais chaves.
Ao adicionar uma entrada no HashMap, o código de hash da chave é usado para determinar a localização do bucket na matriz, algo como:

location = (arraylength - 1) & keyhashcode

Aqui, o & representa o operador AND bit a bit.

Por exemplo: 100 & "ABC".hashCode() = 64 (location of the bucket for the key "ABC")

Durante a operação get, ele usa a mesma maneira para determinar a localização do balde para a chave. No melhor dos casos, cada chave possui um código de hash exclusivo e resulta em um depósito exclusivo para cada chave; nesse caso, o método get gasta tempo apenas para determinar a localização do depósito e recuperar o valor que é constante O (1).

Na pior das hipóteses, todas as chaves têm o mesmo código de hash e armazenadas no mesmo bucket, o que resulta em percorrer toda a lista que leva a O (n).

No caso do java 8, o bucket da Lista Vinculada é substituído por um TreeMap se o tamanho aumentar para mais de 8, isso reduz a pior eficiência da pesquisa de casos para O (log n).


1

Isso basicamente vale para a maioria das implementações de tabelas de hash na maioria das linguagens de programação, pois o próprio algoritmo não muda realmente.

Se não houver colisões presentes na tabela, você só precisará fazer uma única pesquisa, portanto, o tempo de execução é O (1). Se houver colisões presentes, é necessário fazer mais de uma pesquisa, o que reduz o desempenho em direção a O (n).


1
Isso pressupõe que o tempo de execução seja limitado pelo tempo de pesquisa. Na prática, você vai encontrar um monte de situações em que a função hash fornece o limite (String)
Stephan Eggermont

1

Depende do algoritmo que você escolher para evitar colisões. Se a sua implementação usar encadeamento separado, o pior cenário acontecerá quando cada elemento de dados tiver um hash com o mesmo valor (má escolha da função de hash, por exemplo). Nesse caso, a pesquisa de dados não difere de uma pesquisa linear em uma lista vinculada, ou seja, O (n). No entanto, a probabilidade de que isso aconteça é insignificante e as pesquisas de casos melhores e médios permanecem constantes, ou seja, O (1).


1

Além dos acadêmicos, do ponto de vista prático, o HashMaps deve ser aceito como tendo um impacto inconseqüente no desempenho (a menos que seu criador de perfis diga o contrário).


4
Não em aplicações práticas. Assim que você usa uma string como chave, notará que nem todas as funções de hash são ideais e algumas são realmente lentas.
2111 Stephan Stephangermont

1

Somente no caso teórico, quando os códigos de hash são sempre diferentes e o intervalo para cada código de hash também é diferente, o O (1) existirá. Caso contrário, é de ordem constante, ou seja, no incremento do hashmap, sua ordem de pesquisa permanece constante.


0

Obviamente, o desempenho do hashmap dependerá da qualidade da função hashCode () do objeto especificado. No entanto, se a função for implementada de modo que a possibilidade de colisões seja muito baixa, ela terá um desempenho muito bom (isso não é estritamente O (1) em todos os casos possíveis, mas na maioria dos casos).

Por exemplo, a implementação padrão no Oracle JRE é usar um número aleatório (que é armazenado na instância do objeto para que não seja alterado - mas também desabilita o bloqueio parcial, mas isso é outra discussão), portanto a chance de colisões é maior. muito baixo


"é na maioria dos casos". Mais especificamente, o tempo total tenderá para K vezes N (onde K é constante) como N tende para o infinito.
28410 ChrisW

7
Isto está errado. O índice na tabela de hash será determinado por hashCode % tableSizemeio do que significa que certamente pode haver colisões. Você não está obtendo pleno uso dos 32 bits. Esse é o ponto das tabelas de hash ... você reduz um grande espaço de indexação para um pequeno.
FogleBird 28/06/09

1
"você está garantido que não haverá colisões" Não, você não é porque o tamanho do mapa é menor que o tamanho do hash: por exemplo, se o tamanho do mapa for dois, uma colisão será garantida (não importa qual é o hash) se / quando eu tentar inserir três elementos.
28909 ChrisW

Mas como você converte de uma chave para o endereço de memória em O (1)? Quero dizer como x = matriz ["chave"]. A chave não é o endereço de memória; portanto, ainda assim, é necessário procurar O (n).
28411

1
"Acredito que se você não implementar o hashCode, ele usará o endereço de memória do objeto". Poderia usar isso, mas o hashCode padrão para o Oracle Java padrão é na verdade um número aleatório de 25 bits armazenado no cabeçalho do objeto, portanto, 64/32 bits não tem importância.
Boann
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.