Estamos acostumados a dizer que as HashMap
get/put
operaçõ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/put
são O (1)?
A memória disponível é outro problema. Pelo que entendi nos javadocs, o valor HashMap
load factor
deve ser 0,75. E se não tivermos memória suficiente na JVM e load factor
excedermos o limite?
Portanto, parece que O (1) não é garantido. Faz sentido ou estou faltando alguma coisa?
java
data-structures
hashmap
complexity-theory
Michael
fonte
fonte
Respostas:
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,
get
terá que iterar sobre eles, chamandoequals
cada um deles para encontrar uma correspondência.Na pior das hipóteses, a
HashMap
tem 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,
HashMap
foi 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.
fonte
put
é "amortizado O (1)" - geralmente O (1), ocasionalmente O (n) - mas raramente o suficiente para equilibrar.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.
fonte
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.
fonte
Já foi mencionado que os hashmaps são
O(n/m)
em média, sen
é o número de itens em
o tamanho. Também foi mencionado que, em princípio, tudo poderia entrar em uma lista vinculada com oO(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 preenchidoO(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)
.fonte
Eu concordo com:
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 aList
.HashMap
substitui 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 chaveObject
(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:
HashMap<Integer, V>
HashMap<String, V>
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>
eHashMap<List<E>, V>
terá O (k) complexidade amortizada e, similarmente, O ( k + logN ) no pior caso em Java8.* Observe que o uso de uma
String
chave é um caso mais complexo, porque é imutável e o Java armazena em cache o resultado dehashCode()
uma variável privadahash
, portanto é computado apenas uma vez.Mas, o acima exposto também está tendo seu pior caso, porque a
String.hashCode()
implementação do Java está verificando sehash == 0
antes da computaçãohashCode
. Mas ei, existem Strings não vazias que produzem umhashcode
zero, como "f5a5a608", veja aqui ; nesse caso, a memorização pode não ser útil.fonte
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.
fonte
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 osO(log(max(n))
bits, certo?