Kami terbiasa mengatakan bahwa HashMap
get/put
operasi 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/put
O (1)?
Memori yang tersedia adalah masalah lain. Seperti yang saya pahami dari javadocs, HashMap
load factor
seharusnya 0,75. Bagaimana jika kita tidak memiliki cukup memori dalam JVM dan load factor
melebihi batas?
Jadi, sepertinya O (1) tidak dijamin. Apakah masuk akal atau saya melewatkan sesuatu?
Jawaban:
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,
get
harus mengulangi mereka memanggilequals
masing-masing untuk menemukan kecocokan.Dalam kasus terburuk, pencarian
HashMap
memiliki 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,
HashMap
telah 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.
sumber
put
"diamortisasi O (1)" - biasanya O (1), kadang-kadang O (n) - tetapi jarang cukup untuk menyeimbangkan.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.
sumber
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.
sumber
Telah disebutkan bahwa hashmaps
O(n/m)
rata-rata, jikan
jumlah item danm
ukuran. Juga telah disebutkan bahwa pada prinsipnya semuanya dapat runtuh menjadi daftar yang terhubung secara tunggal denganO(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 dariO(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)
.sumber
Saya setuju dengan:
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 aList
.HashMap
secara 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 kunciObject
(tidak berubah / di-cache atau menjadi Koleksi) juga dapat memengaruhi kompleksitas nyata dalam istilah yang ketat.Mari kita asumsikan tiga kasus berikut:
HashMap<Integer, V>
HashMap<String, V>
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>
danHashMap<List<E>, V>
akan ada O (k) kompleksitas diamortisasi dan juga, O ( k + logN ) kasus terburuk di Java8.* Perhatikan bahwa menggunakan
String
kunci adalah kasus yang lebih kompleks, karena tidak dapat diubah dan Java menyimpan hasil cachehashCode()
dalam variabel pribadihash
, jadi itu hanya dihitung sekali.Tapi, di atas juga memiliki kasus terburuknya sendiri, karena
String.hashCode()
implementasi Java sedang memeriksahash == 0
sebelum komputasihashCode
. Tapi hei, ada String non-kosong yang menghasilkan ahashcode
nol, seperti "f5a5a608", lihat di sini , dalam hal ini memoisasi mungkin tidak membantu.sumber
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.
sumber
O(log(n) * log(max(n)))
, lalu? Sementara perbandingan di setiap node mungkin lebih pintar, dalam kasus terburuk perlu memeriksa semuaO(log(max(n))
bit, kan?