HashMap mendapatkan / menempatkan kompleksitas

131

Kami terbiasa mengatakan bahwa HashMap get/putoperasi adalah O (1). Namun itu tergantung pada implementasi hash. Hash objek default sebenarnya adalah alamat internal di tumpukan JVM. Apakah kami yakin cukup baik untuk mengklaim bahwa get/putO (1)?

Memori yang tersedia adalah masalah lain. Seperti yang saya pahami dari javadocs, HashMap load factorseharusnya 0,75. Bagaimana jika kita tidak memiliki cukup memori dalam JVM dan load factormelebihi batas?

Jadi, sepertinya O (1) tidak dijamin. Apakah masuk akal atau saya melewatkan sesuatu?

Michael
sumber
1
Anda mungkin ingin mencari konsep kompleksitas diamortisasi. Lihat misalnya di sini: stackoverflow.com/questions/3949217/time-complexity-of-hash-table Kompleksitas kasus terburuk bukanlah ukuran paling penting untuk tabel hash
Dr G
3
Benar - ini diamortisasi O (1) - jangan pernah lupa bagian pertama itu dan Anda tidak akan memiliki pertanyaan seperti ini :)
Engineer
Kompleksitas waktu, kasus terburuk adalah O (logN) sejak Java 1.8 jika saya tidak salah.
Tarun Kolla

Jawaban:

216

Itu tergantung pada banyak hal. Ini biasanya O (1), dengan hash yang layak yang itu sendiri adalah waktu yang konstan ... tapi Anda bisa memiliki hash yang membutuhkan waktu lama untuk menghitung, dan jika ada beberapa item dalam peta hash yang kembali kode hash yang sama, getharus mengulangi mereka memanggil equalsmasing-masing untuk menemukan kecocokan.

Dalam kasus terburuk, pencarian HashMapmemiliki O (n) karena berjalan melalui semua entri dalam ember hash yang sama (misalnya jika mereka semua memiliki kode hash yang sama). Untungnya, skenario terburuk itu tidak sering muncul dalam kehidupan nyata, menurut pengalaman saya. Jadi tidak, O (1) tentu saja tidak dijamin - tetapi biasanya apa yang harus Anda asumsikan ketika mempertimbangkan algoritma dan struktur data yang digunakan.

Dalam JDK 8, HashMaptelah di-tweak sehingga jika kunci dapat dibandingkan untuk pemesanan, maka setiap bucket yang padat penduduk diimplementasikan sebagai pohon, sehingga bahkan jika ada banyak entri dengan kode hash yang sama, kompleksitasnya adalah O (log n). Itu dapat menyebabkan masalah jika Anda memiliki tipe kunci di mana kesetaraan dan pemesanan berbeda, tentu saja.

Dan ya, jika Anda tidak memiliki cukup memori untuk peta hash, Anda akan berada dalam kesulitan ... tapi itu akan menjadi kenyataan apa pun struktur data yang Anda gunakan.

Jon Skeet
sumber
@marcog: Anda menganggap O (n log n) untuk pencarian tunggal ? Kedengarannya gila bagi saya. Ini akan tergantung pada kompleksitas fungsi hash dan kesetaraan, tentu saja, tetapi itu tidak mungkin tergantung pada ukuran peta.
Jon Skeet
1
@marcog: Jadi apa yang Anda anggap sebagai O (n log n)? Penyisipan n item?
Jon Skeet
1
+1 untuk jawaban yang bagus. Bisakah Anda memberikan tautan seperti entri wikipedia ini untuk tabel hash dalam jawaban Anda? Dengan begitu, pembaca yang lebih tertarik bisa sampai pada seluk beluk pemahaman mengapa Anda memberikan jawaban Anda.
David Weiser
2
@SleimanJneidi: Masih jika kuncinya tidak mengimplementasikan Comparable <T> `- tetapi saya akan memperbarui jawabannya ketika saya memiliki lebih banyak waktu.
Jon Skeet
1
@ ip696: Ya, put"diamortisasi O (1)" - biasanya O (1), kadang-kadang O (n) - tetapi jarang cukup untuk menyeimbangkan.
Jon Skeet
9

Saya tidak yakin kode hash default adalah alamatnya - saya membaca sumber OpenJDK untuk pembuatan kode hash beberapa waktu yang lalu, dan saya ingat itu sesuatu yang sedikit lebih rumit. Masih bukan sesuatu yang menjamin distribusi yang baik, mungkin. Namun, itu sedikit banyak diperdebatkan, karena beberapa kelas yang akan Anda gunakan sebagai kunci dalam hashmap menggunakan kode hash default - mereka menyediakan implementasi mereka sendiri, yang seharusnya bagus.

Selain itu, apa yang mungkin tidak Anda ketahui (sekali lagi, ini didasarkan pada sumber bacaan - tidak dijamin) adalah bahwa HashMap mengaduk hash sebelum menggunakannya, untuk mencampur entropi dari seluruh kata ke dalam bit bawah, yang merupakan tempat dibutuhkan untuk semua kecuali hashmaps terbesar. Itu membantu menangani hash yang secara khusus tidak melakukannya sendiri, walaupun saya tidak bisa memikirkan kasus umum di mana Anda akan melihatnya.

Akhirnya, apa yang terjadi ketika tabel kelebihan beban adalah bahwa ia merosot ke dalam serangkaian daftar tertaut paralel - kinerja menjadi O (n). Secara khusus, jumlah tautan yang dilalui rata-rata akan menjadi setengah dari faktor muatan.

Tom Anderson
sumber
6
Sialan. Saya memilih untuk percaya bahwa jika saya tidak harus mengetik ini pada layar sentuh ponsel membalik, saya bisa mengalahkan Jon Sheet ke pukulan. Ada lencana untuk itu, kan?
Tom Anderson
8

Operasi HashMap adalah faktor tergantung dari implementasi kode hash. Untuk skenario ideal katakanlah implementasi hash yang baik yang menyediakan kode hash unik untuk setiap objek (Tidak ada tabrakan hash) maka skenario kasus terbaik, terburuk dan rata-rata adalah O (1). Mari kita pertimbangkan sebuah skenario di mana implementasi hashCode yang buruk selalu mengembalikan 1 atau hash yang memiliki tabrakan hash. Dalam hal ini kompleksitas waktu adalah O (n).

Sekarang sampai pada bagian kedua dari pertanyaan tentang memori, maka ya kendala memori akan diurus oleh JVM.

Pranav
sumber
8

Telah disebutkan bahwa hashmaps O(n/m)rata-rata, jika njumlah item dan mukuran. Juga telah disebutkan bahwa pada prinsipnya semuanya dapat runtuh menjadi daftar yang terhubung secara tunggal dengan O(n)waktu permintaan. (Ini semua mengasumsikan bahwa menghitung hash adalah waktu yang konstan).

Namun yang tidak sering disebutkan adalah, bahwa dengan probabilitas setidaknya 1-1/n(jadi untuk 1000 item yang merupakan peluang 99,9%), ember terbesar tidak akan diisi lebih dari O(logn)! Karenanya cocok dengan kompleksitas rata-rata pohon pencarian biner. (Dan konstanta itu baik, terikat lebih ketat (log n)*(m/n) + O(1)).

Semua yang diperlukan untuk ikatan teoretis ini adalah bahwa Anda menggunakan fungsi hash yang cukup baik (lihat Wikipedia: Universal Hashing . Ini bisa sesederhana a*x>>m). Dan tentu saja orang yang memberi Anda nilai untuk hash tidak tahu bagaimana Anda memilih konstanta acak Anda.

TL; DR: Dengan Probabilitas Sangat Tinggi, kasus terburuk yang didapat adalah kompleksitas hashmap O(logn).

Thomas Ahle
sumber
(Dan perhatikan bahwa tidak satu pun dari ini mengasumsikan data acak. Probabilitas muncul murni dari pilihan fungsi hash)
Thomas Ahle
Saya juga memiliki pertanyaan yang sama tentang kompleksitas runtime dari pencarian di peta hash. Tampaknya itu O (n) karena faktor-faktor konstan seharusnya turun. 1 / m adalah faktor konstan dan dengan demikian dijatuhkan meninggalkan O (n).
nickdu
4

Saya setuju dengan:

  • kompleksitas umum yang diamortisasi dari O (1)
  • hashCode()implementasi yang buruk dapat mengakibatkan beberapa tabrakan, yang berarti bahwa dalam kasus terburuk setiap objek pergi ke ember yang sama, sehingga O ( N ) jika setiap ember didukung oleh a List.
  • sejak Java 8, HashMapsecara dinamis menggantikan Nodes (daftar tertaut) yang digunakan di setiap bucket dengan TreeNodes (pohon merah-hitam ketika daftar menjadi lebih besar dari 8 elemen) yang menghasilkan kinerja terburuk O ( logN ).

Tapi, ini BUKAN kebenaran sepenuhnya jika kita ingin 100% tepat. Implementasi hashCode()dan jenis kunci Object(tidak berubah / di-cache atau menjadi Koleksi) juga dapat memengaruhi kompleksitas nyata dalam istilah yang ketat.

Mari kita asumsikan tiga kasus berikut:

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

Apakah mereka memiliki kompleksitas yang sama? Nah, kompleksitas amortisasi yang pertama adalah, seperti yang diharapkan, O (1). Tetapi, untuk sisanya, kita juga perlu menghitung hashCode()elemen pencarian, yang berarti kita mungkin harus melintasi array dan daftar dalam algoritma kita.

Mari kita asumsikan bahwa ukuran semua array / daftar di atas adalah k . Kemudian, HashMap<String, V>dan HashMap<List<E>, V>akan ada O (k) kompleksitas diamortisasi dan juga, O ( k + logN ) kasus terburuk di Java8.

* Perhatikan bahwa menggunakan Stringkunci adalah kasus yang lebih kompleks, karena tidak dapat diubah dan Java menyimpan hasil cache hashCode()dalam variabel pribadi hash, jadi itu hanya dihitung sekali.

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

Tapi, di atas juga memiliki kasus terburuknya sendiri, karena String.hashCode()implementasi Java sedang memeriksa hash == 0sebelum komputasi hashCode. Tapi hei, ada String non-kosong yang menghasilkan a hashcodenol, seperti "f5a5a608", lihat di sini , dalam hal ini memoisasi mungkin tidak membantu.

Kostas Chalkias
sumber
2

Dalam praktiknya, ini O (1), tetapi ini sebenarnya adalah penyederhanaan yang mengerikan dan secara matematis tidak masuk akal. Notasi O () mengatakan bagaimana algoritma berperilaku ketika ukuran masalah cenderung tak terbatas. Hashmap get / put berfungsi seperti algoritma O (1) untuk ukuran terbatas. Batasnya cukup besar dari memori komputer dan dari sudut pandang pengalamatan, tetapi jauh dari tak terbatas.

Ketika seseorang mengatakan bahwa hashmap get / put adalah O (1) itu harus benar-benar mengatakan bahwa waktu yang dibutuhkan untuk get / put lebih atau kurang konstan dan tidak tergantung pada jumlah elemen dalam hashmap sejauh hashmap dapat disajikan pada sistem komputasi yang sebenarnya. Jika masalahnya melampaui ukuran itu dan kita membutuhkan hashmaps yang lebih besar maka, setelah beberapa saat, tentu jumlah bit yang menggambarkan satu elemen juga akan meningkat ketika kita kehabisan elemen berbeda yang mungkin dijelaskan. Sebagai contoh, jika kita menggunakan hashmap untuk menyimpan angka 32bit dan kemudian kita menambah ukuran masalahnya sehingga kita akan memiliki lebih dari 2 ^ 32 bit elemen dalam hashmap, maka elemen individual akan dijelaskan dengan lebih dari 32 bit.

Jumlah bit yang diperlukan untuk menggambarkan elemen individu adalah log (N), di mana N adalah jumlah maksimum elemen, oleh karena itu dapatkan dan letakkan benar-benar O (log N).

Jika Anda membandingkannya dengan set pohon, yang merupakan O (log n) maka hash set adalah O (panjang (maks (n)) dan kami hanya merasa bahwa ini adalah O (1), karena pada implementasi tertentu maks (n) diperbaiki, tidak berubah (ukuran objek yang kami simpan diukur dalam bit) dan algoritma yang menghitung kode hash cepat.

Akhirnya, jika menemukan elemen dalam struktur data adalah O (1) kami akan membuat informasi dari udara kosong. Memiliki struktur data n elemen saya dapat memilih satu elemen dengan cara yang berbeda. Dengan itu, saya dapat menyandikan informasi log (n) bit. Jika saya dapat menyandikannya dalam nol bit (itulah arti O (1) artinya) maka saya membuat algoritma ZIP kompresi yang jauh.

Peter Verhas
sumber
Bukankah seharusnya kompleksitas untuk set pohon O(log(n) * log(max(n))), lalu? Sementara perbandingan di setiap node mungkin lebih pintar, dalam kasus terburuk perlu memeriksa semua O(log(max(n))bit, kan?
maaartinus