Tampaknya sudah menjadi pengetahuan umum bahwa tabel hash dapat mencapai O (1), tetapi itu tidak pernah masuk akal bagi saya. Bisakah seseorang menjelaskannya? Berikut dua situasi yang muncul di benak Anda:
A. Nilai int lebih kecil dari ukuran tabel hash. Oleh karena itu, nilainya adalah hashnya sendiri, jadi tidak ada tabel hash. Tetapi jika ada, itu akan menjadi O (1) dan tetap tidak efisien.
B. Anda harus menghitung hash dari nilai tersebut. Dalam situasi ini, urutannya adalah O (n) untuk ukuran data yang dicari. Pencariannya mungkin O (1) setelah Anda melakukan O (n) pekerjaan, tapi itu masih keluar ke O (n) di mata saya.
Dan kecuali Anda memiliki hash yang sempurna atau tabel hash yang besar, mungkin ada beberapa item per ember. Jadi, itu beralih ke pencarian linier kecil di beberapa titik.
Saya pikir tabel hash itu luar biasa, tapi saya tidak mendapatkan sebutan O (1) kecuali itu hanya seharusnya teoritis.
Artikel Wikipedia untuk tabel hash secara konsisten merujuk pada waktu pencarian yang konstan dan sama sekali mengabaikan biaya fungsi hash. Apakah itu tindakan yang adil?
Edit: Untuk meringkas apa yang saya pelajari:
Ini secara teknis benar karena fungsi hash tidak diperlukan untuk menggunakan semua informasi dalam kunci dan bisa jadi waktu yang konstan, dan karena tabel yang cukup besar dapat membawa tabrakan ke waktu yang hampir konstan.
Ini benar dalam praktiknya karena seiring waktu itu hanya berfungsi selama fungsi hash dan ukuran tabel dipilih untuk meminimalkan benturan, meskipun itu sering berarti tidak menggunakan fungsi hash waktu yang konstan.
sumber
hashCode()
metode Java diimplementasikan untukString
. grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/…Jawaban:
Anda memiliki dua variabel di sini, m dan n, di mana m adalah panjang input dan n adalah jumlah item dalam hash.
Klaim performa pencarian O (1) membuat setidaknya dua asumsi:
Jika objek Anda berukuran variabel dan pemeriksaan kesetaraan memerlukan melihat semua bit maka kinerja akan menjadi O (m). Namun fungsi hash tidak harus O (m) - bisa jadi O (1). Tidak seperti hash kriptografi, fungsi hash untuk digunakan dalam kamus tidak harus melihat setiap bit dalam input untuk menghitung hash. Implementasi bebas untuk melihat hanya sejumlah bit tetap.
Untuk cukup banyak item, jumlah item akan menjadi lebih besar dari jumlah kemungkinan hash dan kemudian Anda akan mendapatkan tabrakan yang menyebabkan kinerja meningkat di atas O (1), misalnya O (n) untuk traversal daftar tertaut sederhana (atau O (n * m) jika kedua asumsi salah).
Dalam prakteknya meskipun klaim O (1) sementara secara teknis salah, kira - kira benar untuk banyak situasi dunia nyata, dan khususnya situasi di mana asumsi di atas berlaku.
sumber
O(1)
klaim benar jika Anda sedang hashingint
s atau sesuatu yang lain yang cocok dalam kata mesin. Itulah yang diasumsikan oleh sebagian besar teori tentang hashing.std::hash
tombol tekstual menggabungkan 10 karakter yang ditempatkan secara merata di sepanjang teks ke dalam nilai hash, jadi O (1) terlepas dari panjang teks (tetapi secara masif lebih rentan benturan daripada GCC!). Secara terpisah, klaim O (1) memiliki asumsi lain (biasanya benar) bahwa m jauh lebih kecil dari n .Apa? Untuk melakukan hash, satu elemen membutuhkan waktu yang konstan. Mengapa menjadi yang lain? Jika Anda memasukkan
n
elemen, ya, Anda harus menghitungn
hash, dan itu membutuhkan waktu linier ... untuk mencari elemen, Anda menghitung satu hash dari apa yang Anda cari, lalu temukan bucket yang sesuai dengan itu . Anda tidak menghitung ulang hash dari semua yang sudah ada di tabel hash.Belum tentu. Bucket tidak harus berupa list atau array, bucket dapat berupa jenis container apa pun, seperti BST yang seimbang. Itu berarti
O(log n)
kasus terburuk. Namun inilah mengapa penting untuk memilih fungsi hashing yang baik untuk menghindari menempatkan terlalu banyak elemen ke dalam satu wadah. Seperti yang dikemukakan KennyTM, rata-rata, Anda tetap punyaO(1)
waktu, meski sesekali harus menggali melalui ember.Pertukaran tabel hash tentu saja adalah kompleksitas ruang. Anda menukar ruang dengan waktu, yang tampaknya merupakan kasus biasa dalam ilmu komputasi.
Anda menyebutkan menggunakan string sebagai kunci di salah satu komentar Anda yang lain. Anda khawatir tentang jumlah waktu yang diperlukan untuk menghitung hash string, karena terdiri dari beberapa karakter? Seperti yang ditunjukkan orang lain lagi, Anda tidak perlu melihat semua karakter untuk menghitung hash, meskipun itu mungkin menghasilkan hash yang lebih baik jika Anda melakukannya. Dalam hal ini, jika ada rata-rata
m
karakter di kunci Anda, dan Anda menggunakan semuanya untuk menghitung hash Anda, maka saya kira Anda benar, pencarian itu akan dilakukanO(m)
. Jikam >> n
kemudian Anda mungkin memiliki masalah. Anda mungkin akan lebih baik dengan BST dalam kasus itu. Atau pilih fungsi hashing yang lebih murah.sumber
O(n)
untuk tabrakan. Jika Anda sedang mengharapkan banyak dari tabrakan, maka Anda benar, mungkin lebih baik pergi dengan BST di tempat pertama.N
dalam hal ini adalah panjang string. Kita hanya perlu mencirikan satu string untuk menentukan 'keranjang' mana yang harus dimasuki - keranjang tidak bertambah dengan panjang peta hash .Hash adalah ukuran tetap - mencari keranjang hash yang sesuai adalah operasi biaya tetap. Ini berarti O (1).
Menghitung hash tidak harus menjadi operasi yang mahal - kita tidak membicarakan fungsi hash kriptografi di sini. Tapi begitulah. Kalkulasi fungsi hash sendiri tidak bergantung pada jumlah n elemen; sementara itu mungkin tergantung pada ukuran data dalam sebuah elemen, ini bukan yang dimaksud dengan n . Jadi perhitungan hash tidak bergantung pada n dan juga O (1).
sumber
logn
, lihat jawaban saya di stackoverflow.com/questions/4553624/hashmap-get-put-complexity/…Hashing adalah O (1) hanya jika hanya ada jumlah kunci yang konstan dalam tabel dan beberapa asumsi lain dibuat. Tetapi dalam kasus seperti itu, itu memiliki keuntungan.
Jika kunci Anda memiliki representasi n-bit, fungsi hash Anda dapat menggunakan 1, 2, ... n dari bit ini. Berpikir tentang fungsi hash yang menggunakan 1 bit. Evaluasi pasti O (1). Tetapi Anda hanya mempartisi ruang kunci menjadi 2. Jadi, Anda memetakan sebanyak 2 ^ (n-1) kunci ke dalam bin yang sama. menggunakan pencarian BST ini membutuhkan hingga n-1 langkah untuk menemukan kunci tertentu jika hampir penuh.
Anda dapat memperluas ini untuk melihat bahwa jika fungsi hash Anda menggunakan K bit, ukuran bin Anda adalah 2 ^ (nk).
jadi fungsi hash K-bit ==> tidak lebih dari 2 ^ K tempat sampah efektif ==> hingga 2 ^ (nK) kunci n-bit per bin ==> (nK) langkah (BST) untuk menyelesaikan tabrakan. Sebenarnya kebanyakan fungsi hash kurang "efektif" dan membutuhkan / menggunakan lebih dari K bit untuk menghasilkan 2 ^ k bin. Jadi ini pun optimis.
Anda dapat melihatnya dengan cara ini - Anda memerlukan ~ n langkah untuk dapat secara unik membedakan sepasang kunci dari n bit dalam kasus terburuk. Benar-benar tidak ada cara untuk menyiasati batasan teori informasi ini, tabel hash atau tidak.
Namun, ini BUKAN bagaimana / kapan Anda menggunakan tabel hash!
Analisis kompleksitas mengasumsikan bahwa untuk kunci n-bit, Anda dapat memiliki kunci O (2 ^ n) dalam tabel (misalnya 1/4 dari semua kunci yang mungkin). Tetapi sebagian besar jika tidak semua waktu kita menggunakan tabel hash, kita hanya memiliki sejumlah kunci n-bit yang konstan di tabel. Jika Anda hanya menginginkan sejumlah kunci konstan dalam tabel, katakanlah C adalah angka maksimum Anda, maka Anda dapat membentuk tabel hash dari O (C) bin, yang menjamin tumbukan konstan yang diharapkan (dengan fungsi hash yang baik); dan fungsi hash menggunakan ~ logC dari n bit kunci. Maka setiap kueri adalah O (logC) = O (1). Ini adalah cara orang mengklaim "akses tabel hash adalah O (1)" /
Ada beberapa batasan di sini - pertama, mengatakan Anda tidak membutuhkan semua bit mungkin hanya trik penagihan. Pertama, Anda tidak dapat benar-benar meneruskan nilai kunci ke fungsi hash, karena itu akan memindahkan n bit dalam memori yaitu O (n). Jadi, Anda perlu melakukan misal referensi lewat. Tetapi Anda masih perlu menyimpannya di suatu tempat yang merupakan operasi O (n); Anda hanya tidak menagihnya ke hashing; tugas komputasi Anda secara keseluruhan tidak dapat menghindari ini. Kedua, Anda melakukan hashing, menemukan bin, dan menemukan lebih dari 1 kunci; biaya Anda tergantung pada metode resolusi Anda - jika Anda melakukan perbandingan berdasarkan (BST atau Daftar), Anda akan memiliki operasi O (n) (kunci ingat adalah n-bit); jika Anda melakukan hash ke-2, Anda memiliki masalah yang sama jika hash kedua bertabrakan.
Pertimbangkan alternatifnya, misalnya BST, dalam kasus ini. ada tombol C, jadi BST yang seimbang akan menjadi O (logC) secara mendalam, jadi pencarian mengambil langkah O (logC). Namun perbandingan dalam kasus ini akan menjadi operasi O (n) ... jadi tampaknya hashing adalah pilihan yang lebih baik dalam kasus ini.
sumber
TL; DR: Tabel hash menjamin
O(1)
perkiraan waktu kasus terburuk jika Anda memilih fungsi hash secara seragam secara acak dari keluarga universal fungsi hash. Kasus terburuk yang diharapkan tidak sama dengan kasus rata-rata.Penafian: Saya tidak secara resmi membuktikan tabel hash
O(1)
, untuk itu lihat video dari coursera ini [ 1 ]. Saya juga tidak membahas aspek amortisasi dari tabel hash. Itu ortogonal untuk diskusi tentang hashing dan tabrakan.Saya melihat banyak sekali kebingungan seputar topik ini di jawaban dan komentar lain, dan akan mencoba memperbaiki beberapa di antaranya dalam jawaban panjang ini.
Penalaran tentang kasus terburuk
Ada berbagai jenis analisis kasus terburuk. Analisis yang paling banyak dijawab sejauh ini bukanlah kasus terburuk, melainkan kasus rata - rata [ 2 ]. Analisis kasus rata-rata cenderung lebih praktis. Mungkin algoritme Anda memiliki satu masukan kasus terburuk yang buruk, tetapi sebenarnya berfungsi dengan baik untuk semua masukan lain yang memungkinkan. Intinya adalah waktu proses Anda bergantung pada kumpulan data tempat Anda menjalankan.
Pertimbangkan pseudocode berikut dari
get
metode tabel hash. Di sini saya mengasumsikan kami menangani tabrakan dengan merangkai, jadi setiap entri tabel adalah daftar(key,value)
pasangan yang ditautkan . Kami juga mengasumsikan jumlah kotakm
tetap tetapiO(n)
, di manan
jumlah elemen dalam input.Seperti yang ditunjukkan oleh jawaban lain, ini berjalan dalam
O(1)
kasus rata - rata dan terburukO(n)
. Kita bisa membuat sketsa kecil bukti demi tantangan di sini. Tantangannya adalah sebagai berikut:(1) Anda memberikan algoritma tabel hash Anda kepada musuh.
(2) Musuh dapat mempelajarinya dan mempersiapkan selama dia mau.
(3) Akhirnya musuh memberi Anda masukan ukuran
n
untuk Anda masukkan ke dalam tabel Anda.Pertanyaannya adalah: seberapa cepat tabel hash Anda pada input musuh?
Dari langkah (1) musuh mengetahui fungsi hash Anda; selama langkah (2) musuh dapat membuat daftar
n
elemen yang samahash modulo m
, misalnya dengan menghitung hash dari sekelompok elemen secara acak; dan kemudian di (3) mereka dapat memberi Anda daftar itu. Tapi lihatlah, karena semuan
elemen memiliki hash ke keranjang yang sama, algoritme Anda akan membutuhkanO(n)
waktu untuk melintasi daftar tertaut di keranjang itu. Tidak peduli berapa kali kami mencoba ulang tantangan, musuh selalu menang, dan seberapa buruk algoritme Anda, kasus terburukO(n)
.Kenapa hashing adalah O (1)?
Apa yang membuat kami tersingkir di tantangan sebelumnya adalah bahwa musuh mengetahui fungsi hash kami dengan sangat baik, dan dapat menggunakan pengetahuan itu untuk membuat masukan yang paling buruk. Bagaimana jika alih-alih selalu menggunakan satu fungsi hash tetap, kami sebenarnya memiliki sekumpulan fungsi hash
H
, yang dapat dipilih algoritme secara acak saat runtime? Jika Anda penasaran,H
disebut keluarga universal fungsi hash [ 3 ]. Baiklah, mari kita coba menambahkan beberapa keacakan untuk ini.Pertama, misalkan tabel hash kami juga menyertakan benih
r
, danr
ditetapkan ke nomor acak pada waktu konstruksi. Kami menetapkannya sekali dan kemudian diperbaiki untuk contoh tabel hash itu. Sekarang mari kita lihat kembali pseudocode kita.Jika kita mencoba tantangannya sekali lagi: dari langkah (1) musuh dapat mengetahui semua fungsi hash yang kita miliki
H
, tetapi sekarang fungsi hash spesifik yang kita gunakan bergantungr
. Nilai darir
bersifat pribadi untuk struktur kita, musuh tidak dapat memeriksanya pada waktu proses, atau memprediksinya sebelumnya, jadi dia tidak dapat membuat daftar yang selalu buruk bagi kita. Mari kita asumsikan bahwa pada langkah (2) musuh memilih satu fungsihash
secaraH
acak, dia kemudian membuat daftarn
tabrakan di bawahhash modulo m
, dan mengirimkannya untuk langkah (3), menyilangkan jari bahwa pada waktu prosesH[r]
akan sama dengan yanghash
mereka pilih.Ini adalah taruhan serius bagi musuh, daftar yang dia buat bertabrakan
hash
, tetapi hanya akan menjadi input acak di bawah fungsi hash lainnyaH
. Jika dia memenangkan taruhan ini, run time kami akan menjadi kasus terburukO(n)
seperti sebelumnya, tetapi jika dia kalah maka kami hanya diberi input acak yang membutuhkanO(1)
waktu rata-rata . Dan memang seringkali musuh akan kalah, dia hanya menang sekali setiap|H|
tantangan, dan kita bisa|H|
menjadi sangat besar.Bandingkan hasil ini dengan algoritme sebelumnya di mana musuh selalu memenangkan tantangan. Sedikit melambaikan tangan di sini, tetapi karena sebagian besar waktu musuh akan gagal, dan ini berlaku untuk semua kemungkinan strategi yang dapat dicoba oleh musuh, hal ini mengikuti bahwa meskipun kasus terburuk adalah
O(n)
, kasus terburuk yang diharapkan sebenarnyaO(1)
.Sekali lagi, ini bukan bukti resmi. Jaminan yang kami dapatkan dari analisis kasus terburuk yang diharapkan ini adalah bahwa waktu proses kami sekarang tidak bergantung pada input spesifik apa pun . Ini adalah jaminan yang benar-benar acak, berbeda dengan analisis kasus rata-rata di mana kami menunjukkan bahwa musuh yang termotivasi dapat dengan mudah membuat masukan yang buruk.
sumber
Ada dua pengaturan di mana Anda bisa mendapatkan O (1) waktu terburuk.
Disalin dari sini
sumber
Tampaknya berdasarkan diskusi di sini, bahwa jika X adalah batas atas (# elemen dalam tabel / # tempat sampah), maka jawaban yang lebih baik adalah O (log (X)) dengan asumsi implementasi pencarian bin yang efisien.
sumber
Ini adalah kasus di mana Anda dapat dengan mudah memetakan kunci ke bucket yang berbeda, sehingga array tampaknya merupakan pilihan struktur data yang lebih baik daripada tabel hash. Namun, inefisiensi tidak bertambah dengan ukuran tabel.
(Anda mungkin masih menggunakan tabel hash karena Anda tidak mempercayai int untuk tetap lebih kecil dari ukuran tabel saat program berkembang, Anda ingin membuat kode berpotensi dapat digunakan kembali ketika hubungan itu tidak berlaku, atau Anda tidak ingin orang membaca / memelihara kode harus menyia-nyiakan upaya mental untuk memahami dan memelihara hubungan).
Kita perlu membedakan antara ukuran kunci (misalnya dalam byte), dan ukuran jumlah kunci yang disimpan dalam tabel hash. Klaim bahwa tabel hash menyediakan operasi O (1) berarti bahwa operasi (sisipkan / hapus / temukan) tidak cenderung melambat lebih jauh karena jumlah kunci meningkat dari ratusan menjadi ribuan menjadi jutaan menjadi milyaran (setidaknya tidak jika semua data diakses / diperbarui dalam penyimpanan yang sama cepatnya, baik itu RAM atau disk - efek cache mungkin ikut bermain tetapi bahkan biaya kehilangan cache kasus terburuk cenderung menjadi kelipatan konstan dari kasus terbaik hit).
Pertimbangkan sebuah buku telepon: Anda mungkin memiliki nama di sana yang cukup panjang, tetapi apakah buku tersebut memiliki 100 nama, atau 10 juta, panjang nama rata-rata akan cukup konsisten, dan kasus terburuk dalam sejarah ...
...
wc
memberitahu saya itu 215 karakter - itu bukan keras atas terikat dengan panjang kunci, tapi kami tidak perlu khawatir tentang ada menjadi besar-besaran lagi.Itu berlaku untuk sebagian besar tabel hash dunia nyata: rata-rata panjang kunci cenderung tidak bertambah seiring dengan jumlah kunci yang digunakan. Ada pengecualian, misalnya rutinitas pembuatan kunci mungkin mengembalikan string yang menyematkan bilangan bulat yang bertambah, tetapi meskipun demikian setiap kali Anda menambah jumlah kunci dengan urutan besarnya, Anda hanya menambah panjang kunci sebanyak 1 karakter: itu tidak signifikan.
Ini juga memungkinkan untuk membuat hash dari sejumlah data kunci berukuran tetap. Misalnya, Microsoft Visual C ++ dikirimkan dengan implementasi Pustaka Standar
std::hash<std::string>
yang membuat hash yang menggabungkan hanya sepuluh byte yang ditempatkan secara merata di sepanjang string, jadi jika string hanya bervariasi pada indeks lain Anda mendapatkan tabrakan (dan karenanya dalam praktiknya perilaku non-O (1) di sisi pencarian pasca-tabrakan), tetapi waktu untuk membuat hash memiliki batas atas yang sulit.Secara umum benar, tetapi hal yang mengagumkan tentang tabel hash adalah bahwa jumlah kunci yang dikunjungi selama "pencarian linier kecil" tersebut - untuk pendekatan rantai terpisah untuk tabrakan - fungsi dari faktor beban tabel hash (rasio kunci terhadap keranjang).
Misalnya, dengan faktor beban 1,0 ada rata-rata ~ 1,58 untuk panjang pencarian linier tersebut, terlepas dari jumlah kuncinya (lihat jawaban saya di sini ). Untuk hashing tertutup , ini sedikit lebih rumit, tetapi tidak lebih buruk jika faktor beban tidak terlalu tinggi.
Jenis ini melenceng. Setiap jenis struktur data asosiatif pada akhirnya harus melakukan operasi di setiap bagian kunci kadang-kadang (ketidaksetaraan kadang-kadang dapat ditentukan hanya dari sebagian kunci, tetapi kesetaraan umumnya mengharuskan setiap bit dipertimbangkan). Minimal, dapat melakukan hash kunci satu kali dan menyimpan nilai hash, dan jika menggunakan fungsi hash yang cukup kuat - misalnya MD5 64-bit - ia mungkin secara praktis mengabaikan kemungkinan dua kunci yang memiliki hash ke nilai yang sama (perusahaan Saya bekerja untuk melakukan persis seperti itu untuk database terdistribusi: waktu pembuatan hash masih tidak signifikan dibandingkan dengan transmisi jaringan seluruh WAN). Jadi, tidak ada gunanya terobsesi dengan biaya untuk memproses kunci: itu melekat dalam menyimpan kunci terlepas dari struktur datanya, dan seperti yang dikatakan di atas - tidak.
Adapun tabel hash yang cukup besar membawa tabrakan ke bawah, itu juga meleset. Untuk rangkaian terpisah, Anda masih memiliki panjang rantai tabrakan rata-rata yang konstan pada faktor beban apa pun - hanya lebih tinggi jika faktor beban lebih tinggi, dan hubungan tersebut non-linier. Komentar pengguna SO Hans atas jawaban saya juga ditautkan di atas :
Jadi, faktor beban saja yang menentukan jumlah rata-rata kunci yang bertabrakan yang harus Anda cari selama operasi sisipkan / hapus / temukan. Untuk rangkaian terpisah, tidak hanya mendekati konstan ketika faktor beban rendah - itu selalu konstan. Untuk pengalamatan terbuka, meskipun klaim Anda memiliki beberapa validitas: beberapa elemen yang bertabrakan dialihkan ke bucket alternatif dan kemudian dapat mengganggu operasi pada kunci lain, jadi pada faktor beban yang lebih tinggi (terutama> .8 atau .9) panjang rantai tabrakan menjadi lebih buruk secara dramatis.
Nah, ukuran tabel harus menghasilkan faktor beban yang waras mengingat pilihan hashing dekat atau rangkaian terpisah, tetapi juga jika fungsi hash agak lemah dan kuncinya tidak terlalu acak, memiliki bilangan prima bucket sering membantu mengurangi tabrakan juga (
hash-value % table-size
kemudian membungkus sedemikian rupa sehingga perubahan hanya ke satu atau dua orde tinggi dalam nilai hash masih menyelesaikan ke keranjang yang tersebar secara semu secara acak di berbagai bagian tabel hash).sumber