HashMap alma / koyma karmaşıklığı


132

HashMap get/putİşlemlerin O (1) olduğunu söylemeye alışkınız . Ancak hash uygulamasına bağlıdır. Varsayılan nesne karması aslında JVM yığınındaki dahili adrestir. get/putO (1) olduğunu iddia edecek kadar iyi olduğundan emin miyiz ?

Kullanılabilir bellek başka bir sorundur. Javadocs'tan anladığım gibi, HashMap load factor0.75 olmalıdır. Ya JVM'de yeterli belleğimiz yoksa ve load factorbu sınırı aşarsa?

Yani, O (1) garanti edilmiyor gibi görünüyor. Mantıklı mı yoksa bir şey mi kaçırıyorum?


1
Amortize edilmiş karmaşıklık kavramına bakmak isteyebilirsiniz. Örneğin şuraya bakın: stackoverflow.com/questions/3949217/time-complexity-of-hash-table En kötü durum karmaşıklığı, bir hash tablosu için en önemli ölçü değildir
Dr G

3
Doğru - amortismana tabi O (1) - o ilk bölümü asla unutma ve bu tür soruların olmayacak :)
Engineer

Zaman karmaşıklığı en kötü durum O (logN) çünkü yanılmıyorsam Java 1.8.
Tarun Kolla

Yanıtlar:


216

Pek çok şeye bağlıdır. Bu var genellikle kendisi sürekli zamandır iyi bir karma ile, O (1) ... ama bilgi işlem için uzun zaman alan bir karma olabilir, ve aynı hash kodu döndürebilir karma haritasında birden çok öğe varsa, her birini bir eşleşme bulmaya getçağırarak equalsüzerinde yinelemek zorunda kalacak .

En kötü durumda, a HashMap, aynı karma kova içindeki tüm girişler arasında dolaşılması nedeniyle bir O (n) aramasına sahiptir (örneğin, hepsi aynı karma koduna sahipse). Neyse ki, bu en kötü durum senaryosu gerçek hayatta, benim deneyimime göre çok sık ortaya çıkmıyor. Yani hayır, O (1) kesinlikle garanti edilmez - ancak genellikle hangi algoritmaları ve veri yapılarını kullanacağınızı düşünürken varsaymanız gereken şeydir.

JDK 8'de, HashMapanahtarlar sıralama için karşılaştırılabiliyorsa, yoğun şekilde doldurulmuş herhangi bir kova bir ağaç olarak uygulanacak, böylece aynı karma kodlu çok sayıda giriş olsa bile karmaşıklık O (log n). Elbette eşitlik ve düzenin farklı olduğu bir anahtar türünüz varsa bu sorunlara neden olabilir.

Ve evet, karma harita için yeterli belleğiniz yoksa, başınız belada olacak ... ama hangi veri yapısını kullanırsanız kullanın bu doğru olacaktır.


@marcog: Tek bir arama için O (n log n) olduğunu varsayıyorsunuz ? Bu bana aptalca geliyor. Elbette, hash ve eşitlik fonksiyonlarının karmaşıklığına bağlı olacaktır, ancak bu muhtemelen haritanın boyutuna bağlı değildir.
Jon Skeet

1
@marcog: O halde neyin O (n log n) olduğunu varsayıyorsunuz? N öğe ekleniyor mu?
Jon Skeet

1
İyi bir cevap için +1. Cevabınızda hash tablosu için bu wikipedia girişi gibi bağlantılar sağlar mısınız? Bu şekilde, daha fazla ilgilenen okuyucu , cevabınızı neden verdiğinizi anlamanın zorluğunu anlayabilir .
David Weiser

2
@SleimanJneidi: Anahtar, Comparable <T> 'yi uygulamıyorsa hala geçerli - ancak daha fazla zamanım olduğunda cevabı güncelleyeceğim.
Jon Skeet

1
@ ip696: Evet, put"amorti edilmiş O (1)" - genellikle O (1), ara sıra O (n) - ama nadiren dengelemek için yeterli.
Jon Skeet

9

Varsayılan hashcode'un adres olduğundan emin değilim - bir süre önce hashcode üretimi için OpenJDK kaynağını okudum ve biraz daha karmaşık bir şey olduğunu hatırlıyorum. Yine de iyi bir dağıtımı garanti eden bir şey değil belki. Bununla birlikte, bu bir dereceye kadar tartışmalıdır, çünkü bir hashmap'te anahtar olarak kullanacağınız çok az sınıf varsayılan karma kodu kullanır - iyi olması gereken kendi uygulamalarını sağlarlar.

Bunun da ötesinde, bilmeyebileceğiniz şey (yine, bu okuma kaynağına dayanmaktadır - garanti edilmez), HashMap'in, kelime boyunca entropiyi alt bitlere karıştırmak için kullanmadan önce karmayı karıştırmasıdır. en büyük hashmap'ler hariç tümü için gerekli. Bu, özellikle bunu kendi başına yapmayan karmalarla başa çıkmaya yardımcı olur, ancak bunu görebileceğiniz yaygın durumlar düşünemiyorum.

Son olarak, tablo aşırı yüklendiğinde olan şey, bir dizi paralel bağlantılı listeye dönüşmesidir - performans O (n) olur. Spesifik olarak, geçilen bağlantıların sayısı ortalama olarak yük faktörünün yarısı olacaktır.


6
Kahretsin. Bunu çeviren bir cep telefonu dokunmatik ekranına yazmak zorunda kalmasaydım, Jon Sheet'i yumruk atabileceğime inanmayı seçtim. Bunun için bir rozet var, değil mi?
Tom Anderson

8

HashMap işlemi, hashCode uygulamasının bağımlı faktörüdür. İdeal senaryo için, her nesne için benzersiz bir hash kodu sağlayan (hash çarpışması yok) iyi bir hash uygulaması diyelim, o zaman en iyi, en kötü ve ortalama durum senaryosu O (1) olacaktır. Kötü bir hashCode uygulamasının her zaman 1 veya hash çarpışmasına sahip hash döndürdüğü bir senaryoyu düşünelim. Bu durumda zaman karmaşıklığı O (n) olacaktır.

Şimdi bellekle ilgili sorunun ikinci kısmına gelince, evet bellek kısıtlaması JVM tarafından halledilecektir.


8

Öğe sayısı ve boyutu O(n/m)ise n, hashmap'lerin ortalama olduğu belirtilmişti m. Prensipte her şeyin O(n)sorgu zamanı ile tekil bağlantılı bir listeye çökebileceği de belirtildi . (Bunların tümü, hash hesaplamasının sabit zaman olduğunu varsayar).

Bununla birlikte, sık sık bahsedilmeyen şey, en azından olasılıkla 1-1/n(yani% 99,9 şans olan 1000 öğe için) en büyük kovanın en fazla doldurulmayacağıdır O(logn)! Bu nedenle ikili arama ağaçlarının ortalama karmaşıklığı ile eşleşir. (Ve sabit iyidir, daha sıkı sınırdır (log n)*(m/n) + O(1)).

Bu teorik sınır için gerekli olan tek şey, oldukça iyi bir hash işlevi kullanmanızdır (bkz. Wikipedia: Universal Hashing . Bu kadar basit olabilir a*x>>m). Ve elbette size hash değerlerini veren kişi, rastgele sabitlerinizi nasıl seçtiğinizi bilmiyor.

TL; DR: Çok Yüksek Olasılıkla, bir hashmap'in en kötü durum alma / koyma karmaşıklığıdır O(logn).


(Ve bunların hiçbirinin rastgele veri varsaymadığına dikkat edin. Olasılık tamamen hash fonksiyonunun seçiminden kaynaklanmaktadır)
Thomas Ahle

Karma haritadaki bir aramanın çalışma zamanı karmaşıklığı ile ilgili olarak da aynı soruyu soruyorum. Sabit faktörlerin düşürülmesi gerektiği için O (n) olduğu anlaşılıyor. 1 / m sabit bir faktördür ve bu nedenle O (n) bırakılarak düşürülür.
nickdu

4

Katılıyorum:

  • O (1) 'in genel itfa edilmiş karmaşıklığı
  • kötü bir hashCode()uygulama birden fazla çarpışmaya neden olabilir, bu da en kötü durumda her nesnenin aynı kovaya gittiği anlamına gelir, dolayısıyla her bir kova a ile destekleniyorsa O ( N ) List.
  • Java 8'den beri HashMap, her bir grupta kullanılan Düğümleri (bağlantılı liste) dinamik olarak Ağaç Düğümleri (bir liste 8 öğeden daha büyük olduğunda kırmızı-siyah ağaç) ile değiştirerek O'nun ( logN ) en kötü performansına yol açar .

Ancak,% 100 kesin olmak istiyorsak, bu gerçek değil. hashCode()Anahtarın uygulanması ve türü Object(değişmez / önbelleğe alınmış veya Koleksiyon olma) da gerçek karmaşıklığı katı terimlerle etkileyebilir.

Aşağıdaki üç durumu varsayalım:

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

Aynı karmaşıklığa mı sahipler? Birincisinin amortize edilmiş karmaşıklığı, beklendiği gibi, O (1). Ancak, geri kalanı için, hashCode()arama elemanını da hesaplamamız gerekir , bu da algoritmamızdaki dizileri ve listeleri taramamız gerekebileceği anlamına gelir.

Yukarıdaki tüm dizilerin / listelerin boyutunun k olduğunu varsayalım . Daha sonra, HashMap<String, V>ve HashMap<List<E>, V>O (k) amortize edilmiş karmaşıklığa ve benzer şekilde, Java8'de O ( k + logN ) en kötü duruma sahip olacaktır.

* Bir Stringanahtar kullanmanın daha karmaşık bir durum olduğunu unutmayın, çünkü bu değişmezdir ve Java, sonucunu hashCode()özel bir değişkende önbelleğe alır hash, bu nedenle yalnızca bir kez hesaplanır.

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

Ancak, yukarıdaki durum da en kötü halini yaşıyor, çünkü Java'nın String.hashCode()uygulaması, hash == 0hesaplamadan önce olup olmadığını kontrol ediyor hashCode. Ama hey, hashcode"f5a5a608" gibi a çıktısı veren boş olmayan Dizeler var , buraya bakın , bu durumda not alma yardımcı olmayabilir.


2

Uygulamada, O (1), ama bu aslında korkunç ve matematiksel olarak anlamsız bir basitleştirmedir. O () notasyonu, problemin boyutu sonsuza eğilimli olduğunda algoritmanın nasıl davrandığını söyler. Hashmap get / put, sınırlı bir boyut için bir O (1) algoritması gibi çalışır. Sınır, bilgisayar belleğinden ve adresleme açısından oldukça büyüktür, ancak sonsuzdan uzaktır.

Biri, hashmap get / put'un O (1) olduğunu söylediğinde, get / put için gereken sürenin az çok sabit olduğunu ve hashmap'in olabildiğince hashmap'teki eleman sayısına bağlı olmadığını gerçekten söylemelidir. gerçek bilgi işlem sisteminde sunulmuştur. Sorun bu boyutun ötesine geçerse ve daha büyük hashmap'lere ihtiyacımız olursa, bir süre sonra, bir öğeyi tanımlayan bitlerin sayısı da olası tanımlanabilir farklı öğeler tükendikçe kesinlikle artacaktır. Örneğin, 32 bitlik sayıları saklamak için bir hashmap kullandıysak ve daha sonra problem boyutunu arttırırsak, hashmap'te 2 ^ 32'den fazla bit öğeye sahip olursak, bu durumda tek tek öğeler 32 bitten daha fazla tanımlanacaktır.

Bireysel elemanları tanımlamak için gereken bit sayısı log (N) 'dir, burada N maksimum eleman sayısıdır, bu nedenle get ve koy gerçekten O (log N)' dir.

O (log n) olan bir ağaç kümesiyle karşılaştırırsanız, hash kümesi O (long (max (n))) olur ve bunun O (1) olduğunu hissederiz, çünkü belirli bir uygulamada max (n) sabittir, değişmez (depoladığımız nesnelerin boyutu bit cinsinden ölçülür) ve hash kodunu hesaplayan algoritma hızlıdır.

Son olarak, herhangi bir veri yapısında bir eleman bulmak O (1) olsaydı, havadan bilgi yaratırdık. N elemanlı bir veri yapısına sahip olan bir elemanı farklı n şekilde seçebilirim. Bununla log (n) bit bilgilerini kodlayabilirim. Bunu sıfır bit olarak kodlayabilirsem (O (1) bunun anlamı budur) o zaman sonsuz sıkıştıran bir ZIP algoritması oluşturdum.


O zaman ağaç kümesinin karmaşıklığı olmamalı mı O(log(n) * log(max(n)))? Her düğümdeki karşılaştırma daha akıllı olsa da, en kötü durumda tüm O(log(max(n))bitleri incelemesi gerekir , değil mi?
maaartinus
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.