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 kotak m
tetap tetapi O(n)
, di mana n
jumlah elemen dalam input.
function get(a: Table with m buckets, k: Key being looked up)
bucket <- compute hash(k) modulo m
for each (key,value) in a[bucket]
return value if k == key
return not_found
Seperti yang ditunjukkan oleh jawaban lain, ini berjalan dalam O(1)
kasus rata - rata dan terburuk O(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 sama hash modulo m
, misalnya dengan menghitung hash dari sekelompok elemen secara acak; dan kemudian di (3) mereka dapat memberi Anda daftar itu. Tapi lihatlah, karena semua n
elemen memiliki hash ke keranjang yang sama, algoritme Anda akan membutuhkan O(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 terburuk O(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
, dan r
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.
function get(a: Table with m buckets and seed r, k: Key being looked up)
rHash <- H[r]
bucket <- compute rHash(k) modulo m
for each (key,value) in a[bucket]
return value if k == key
return not_found
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 bergantung r
. Nilai dari r
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 fungsi hash
secara H
acak, dia kemudian membuat daftar n
tabrakan di bawah hash modulo m
, dan mengirimkannya untuk langkah (3), menyilangkan jari bahwa pada waktu proses H[r]
akan sama dengan yang hash
mereka pilih.
Ini adalah taruhan serius bagi musuh, daftar yang dia buat bertabrakan hash
, tetapi hanya akan menjadi input acak di bawah fungsi hash lainnya H
. Jika dia memenangkan taruhan ini, run time kami akan menjadi kasus terburuk O(n)
seperti sebelumnya, tetapi jika dia kalah maka kami hanya diberi input acak yang membutuhkan O(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 sebenarnya O(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.