Complexidade de obter / colocar HashMap

131

Estamos acostumados a dizer que as HashMap get/putoperações são O (1). No entanto, depende da implementação do hash. O hash do objeto padrão é realmente o endereço interno no heap da JVM. Temos certeza de que é bom o suficiente afirmar que get/putsão O (1)?

A memória disponível é outro problema. Pelo que entendi nos javadocs, o valor HashMap load factordeve ser 0,75. E se não tivermos memória suficiente na JVM e load factorexcedermos o limite?

Portanto, parece que O (1) não é garantido. Faz sentido ou estou faltando alguma coisa?

Michael
fonte
1
Você pode procurar o conceito de complexidade amortizada. Veja, por exemplo, aqui: stackoverflow.com/questions/3949217/time-complexity-of-hash-table A pior complexidade do caso não é a medida mais importante para uma tabela de hash
Dr G
3
Correto - ele é amortizado O (1) - nunca se esqueça da primeira parte e você não terá esse tipo de pergunta :) #
Engineer 16 /
O pior caso de complexidade de tempo é O (logN) desde o Java 1.8, se não estiver errado.
Tarun Kolla

Respostas:

216

Depende de muitas coisas. É geralmente O (1), com um hash decente que em si é constante de tempo ... mas você poderia ter um hash que leva um longo tempo para computação, e se houver vários itens no mapa de hash que devolvem o mesmo código hash, getterá que iterar sobre eles, chamando equalscada um deles para encontrar uma correspondência.

Na pior das hipóteses, a HashMaptem uma pesquisa O (n) devido a percorrer todas as entradas no mesmo depósito de hash (por exemplo, se todas tiverem o mesmo código de hash). Felizmente, na minha experiência, esse cenário de pior caso não aparece com muita frequência na vida real. Portanto, não, O (1) certamente não é garantido - mas geralmente é o que você deve assumir ao considerar quais algoritmos e estruturas de dados usar.

No JDK 8, HashMapfoi ajustado para que, se as chaves puderem ser comparadas para pedidos, qualquer depósito densamente povoado seja implementado como uma árvore, para que, mesmo que haja muitas entradas com o mesmo código de hash, a complexidade seja O (log n) Isso pode causar problemas se você tiver um tipo de chave em que igualdade e ordem são diferentes, é claro.

E sim, se você não tiver memória suficiente para o mapa de hash, estará com problemas ... mas isso será verdade independentemente da estrutura de dados que você usar.

Jon Skeet
fonte
@marcog: Você assume O (n log n) para uma única pesquisa ? Isso parece idiota para mim. Depende da complexidade das funções de hash e igualdade, é claro, mas é improvável que dependa do tamanho do mapa.
Jon Skeet
1
@marcog: Então, o que você está assumindo como O (n log n)? Inserção de n itens?
quer
1
+1 para uma boa resposta. Você poderia fornecer links como esta entrada da wikipedia para tabela de hash em sua resposta? Dessa forma, o leitor mais interessado poderia entender por que você deu sua resposta.
David Weiser
2
@SleimanJneidi: Ainda é se a chave não implementar Comparable <T> `- mas atualizarei a resposta quando tiver mais tempo.
Jon Skeet
1
@ ip696: Sim, puté "amortizado O (1)" - geralmente O (1), ocasionalmente O (n) - mas raramente o suficiente para equilibrar.
Jon Skeet
9

Não tenho certeza se o código hash padrão é o endereço - li a fonte OpenJDK para geração de código hash há um tempo atrás e lembro que era algo um pouco mais complicado. Ainda não é algo que garanta uma boa distribuição, talvez. No entanto, isso é até certo ponto discutível, já que poucas classes que você usaria como chaves em um hashmap usam o código de hash padrão - elas fornecem suas próprias implementações, o que deve ser bom.

Além disso, o que você talvez não saiba (novamente, isso é baseado na fonte de leitura - não é garantido) é que o HashMap agita o hash antes de usá-lo, para misturar entropia de toda a palavra nos bits inferiores, que é onde está necessário para todos, exceto os hashmaps mais enormes. Isso ajuda a lidar com hashes que especificamente não fazem isso por si mesmos, embora eu não consiga pensar em nenhum caso comum em que você veria isso.

Finalmente, o que acontece quando a tabela está sobrecarregada é que ela se degenera em um conjunto de listas paralelas vinculadas - o desempenho se torna O (n). Especificamente, o número de links percorridos será, em média, metade do fator de carga.

Tom Anderson
fonte
6
Droga. Decido acreditar que, se não tivesse digitado isso em uma tela sensível ao toque de um celular, poderia ter batido Jon Sheet com força. Há um distintivo para isso, certo?
Tom Anderson
8

A operação do HashMap é fator dependente da implementação do hashCode. Para o cenário ideal, digamos que a boa implementação de hash que forneça código de hash exclusivo para cada objeto (sem colisão de hash), o melhor, o pior e o cenário de caso médio seria O (1). Vamos considerar um cenário em que uma implementação incorreta do hashCode sempre retorna 1 ou um hash que tenha colisão de hash. Nesse caso, a complexidade do tempo seria O (n).

Agora, chegando à segunda parte da pergunta sobre memória, sim, a restrição de memória seria tratada pela JVM.

Pranav
fonte
8

Já foi mencionado que os hashmaps são O(n/m)em média, se né o número de itens e mo tamanho. Também foi mencionado que, em princípio, tudo poderia entrar em uma lista vinculada com o O(n)tempo de consulta. (Isso tudo pressupõe que o cálculo do hash seja tempo constante).

No entanto, o que nem sempre é mencionado é que, com probabilidade pelo menos 1-1/n(portanto, para 1000 itens, há uma chance de 99,9%), o maior balde não será mais preenchido O(logn)! Portanto, corresponde à complexidade média das árvores de pesquisa binária. (E a constante é boa, um limite maior é (log n)*(m/n) + O(1)).

Tudo o que é necessário para esse limite teórico é que você use uma função hash razoavelmente boa (consulte Wikipedia: Hashing Universal . Pode ser tão simples quanto a*x>>m). E é claro que a pessoa que fornece valores para o hash não sabe como você escolheu suas constantes aleatórias.

TL; DR: com probabilidade muito alta, o pior caso é obter / colocar complexidade de um hashmap O(logn).

Thomas Ahle
fonte
(E notem que nada disso assume dados aleatórios A probabilidade surge puramente desde a escolha da função hash.)
Thomas Ahle
Eu também tenho a mesma pergunta sobre a complexidade do tempo de execução de uma pesquisa em um mapa de hash. Parece que é O (n), pois fatores constantes devem ser eliminados. O 1 / m é um fator constante e, portanto, é descartado deixando O (n).
nickdu
4

Eu concordo com:

  • a complexidade amortizada geral de O (1)
  • uma hashCode()implementação ruim pode resultar em várias colisões, o que significa que, na pior das hipóteses, todo objeto vai para o mesmo depósito, portanto, O ( N ) se cada depósito for apoiado por a List.
  • desde o Java 8, HashMapsubstitui dinamicamente os nós (lista vinculada) usados ​​em cada bloco pelos TreeNodes (árvore vermelho-preta quando uma lista fica maior que 8 elementos), resultando em um pior desempenho de O ( logN ).

Mas, isso NÃO é verdade, se queremos ser 100% precisos. A implementação hashCode()e o tipo de chave Object(imutável / armazenado em cache ou sendo uma coleção) também podem afetar a complexidade real em termos estritos.

Vamos assumir os três casos a seguir:

  1. HashMap<Integer, V>
  2. HashMap<String, V>
  3. HashMap<List<E>, V>

Eles têm a mesma complexidade? Bem, a complexidade amortizada do 1º é, como esperado, O (1). Mas, quanto ao resto, também precisamos calcular hashCode()o elemento de pesquisa, o que significa que talvez tenhamos que percorrer matrizes e listas em nosso algoritmo.

Vamos supor que o tamanho de todas as matrizes / listas acima seja k . Então, HashMap<String, V>e HashMap<List<E>, V>terá O (k) complexidade amortizada e, similarmente, O ( k + logN ) no pior caso em Java8.

* Observe que o uso de uma Stringchave é um caso mais complexo, porque é imutável e o Java armazena em cache o resultado de hashCode()uma variável privada hash, portanto é computado apenas uma vez.

/** Cache the hash code for the string */
    private int hash; // Default to 0

Mas, o acima exposto também está tendo seu pior caso, porque a String.hashCode()implementação do Java está verificando se hash == 0antes da computação hashCode. Mas ei, existem Strings não vazias que produzem um hashcodezero, como "f5a5a608", veja aqui ; nesse caso, a memorização pode não ser útil.

Kostas Chalkias
fonte
2

Na prática, é O (1), mas na verdade é uma simplificação terrível e matematicamente sem sentido. A notação O () diz como o algoritmo se comporta quando o tamanho do problema tende ao infinito. O Hashmap get / put funciona como um algoritmo O (1) para um tamanho limitado. O limite é bastante grande a partir da memória do computador e do ponto de vista do endereçamento, mas longe do infinito.

Quando alguém diz que o hashmap get / put é O (1), deve realmente dizer que o tempo necessário para o get / put é mais ou menos constante e não depende do número de elementos no hashmap até onde o hashmap pode ser apresentado no sistema de computação real. Se o problema ultrapassar esse tamanho e precisarmos de hashmaps maiores, certamente, depois de um tempo, certamente o número de bits que descrevem um elemento também aumentará à medida que ficarmos sem os possíveis elementos diferentes que podem ser descritos. Por exemplo, se usamos um mapa de hash para armazenar números de 32 bits e, posteriormente, aumentamos o tamanho do problema para termos mais de 2 ^ 32 bits no mapa de hash, então os elementos individuais serão descritos com mais de 32 bits.

O número de bits necessários para descrever os elementos individuais é log (N), onde N é o número máximo de elementos, portanto, get e put são realmente O (log N).

Se você compará-lo com um conjunto de árvores, que é O (log n), o conjunto de hash é O (longo (max (n)) e simplesmente sentimos que esse é O (1), porque em uma determinada implementação max (n) é fixo, não muda (o tamanho dos objetos que armazenamos medidos em bits) e o algoritmo que calcula o código hash é rápido.

Finalmente, se encontrar um elemento em qualquer estrutura de dados fosse O (1), criaríamos informações do nada. Tendo uma estrutura de dados de n elemento, posso selecionar um elemento de n maneiras diferentes. Com isso, eu posso codificar informações de log (n) bits. Se eu puder codificar isso em zero bit (é isso que O (1) significa)), então criei um algoritmo ZIP infinitamente compactado.

Peter Verhas
fonte
Não deveria ser a complexidade do conjunto de árvores O(log(n) * log(max(n)))? Embora a comparação em cada nó possa ser mais inteligente, na pior das hipóteses, ele precisa inspecionar todos os O(log(max(n))bits, certo?
Maaartinus 03/06/19