Hash function adalah jantung dari sebuah hash table. Sebuah hash function yang buruk dapat mengubah struktur O(1) rata-rata menjadi O(n) dalam praktik, sedangkan hash function yang baik membuat distribusi data merata ke seluruh bucket dengan sedikit collision. Pada Chapter ini kita akan mempelajari sifat-sifat yang wajib dimiliki hash function, tiga metode klasik (division, multiplication, polynomial rolling untuk string), serta cara STL menyediakan std::hash<T> untuk berbagai tipe bawaan. Pemahaman ini menjadi fondasi sebelum kita membahas collision resolution di chapter-chapter selanjutnya.
Sifat Hash Function yang Baik #
Sebuah hash function h: K -> {0, 1, ..., m-1} dianggap baik bila memenuhi empat kriteria berikut:
- Deterministik — untuk input yang sama, output harus selalu sama selama
mtidak berubah. - Cepat dihitung — kompleksitas O(1) untuk tipe primitif, atau O(panjang key) untuk string/data besar.
- Distribusi seragam — untuk himpunan key yang masuk akal, setiap bucket memiliki peluang kira-kira sama untuk dipilih.
- Avalanche effect — perubahan kecil pada input (mis. satu bit) mengubah output secara signifikan, agar pola pada input tidak terbawa ke output.
Mekanisme: Konflik antar Sifat #
Tidak ada hash function yang optimal untuk semua data. Sebagai contoh, h(k) = k mod m sangat cepat, tetapi bila k selalu kelipatan m, seluruh key terkonsentrasi di bucket 0. Karena itu, pemilihan hash function harus mempertimbangkan pola data yang akan di-hash.
Metode Division #
Metode paling sederhana dan paling banyak digunakan dalam buku ajar:
h(k) = k mod mAturan pemilihan m:
mharus bilangan prima yang tidak dekat dengan pangkat 2.- Hindari
m = 2^pkecuali diketahui distribusi key seragam di semua bit. - Praktik umum:
mprima antara1.5nhingga2nuntuk load factor α ≈ 0.5–0.67.
#include <iostream>
std::size_t hash_div(int key, std::size_t m) {
return static_cast<std::size_t>(key) % m;
}
int main() {
std::size_t m = 13;
int keys[] = {7, 20, 33, 46, 100, 200};
for (int k : keys) {
std::cout << "h(" << k << ") = " << k << " mod " << m
<< " = " << hash_div(k, m) << '\n';
}
return 0;
}Output yang Dihasilkan:
h(7) = 7 mod 13 = 7
h(20) = 20 mod 13 = 7
h(33) = 33 mod 13 = 7
h(46) = 46 mod 13 = 7
h(100) = 100 mod 13 = 9
h(200) = 200 mod 13 = 5- Baris
return key % m;: satu operasi modulus. Sangat cepat di CPU modern. - Hasil
h(7) = h(20) = h(33) = h(46) = 7: keempat key berbeda menghasilkan bucket yang sama. Karena20-7=13,33-20=13,46-33=13— selisih kelipatanm. Ini wajar pada data terurut dengan langkah tetap; modifikasi hash function atau pemilihanmyang berbeda mungkin diperlukan.
Metode Multiplication #
Metode ini mengurangi ketergantungan pada pilihan m:
h(k) = floor(m * (k * A mod 1)) dengan 0 < A < 1Konstanta A yang direkomendasikan Knuth adalah (sqrt(5) - 1) / 2 ≈ 0.6180339887. Keunggulan: m boleh pangkat dua (mis. 2^p), yang memungkinkan optimasi bit-shift.
#include <iostream>
#include <cstdint>
#include <cmath>
std::size_t hash_mul(std::uint64_t key, std::size_t m) {
const double A = 0.6180339887498949; // (sqrt(5)-1)/2
double frac = std::fmod(static_cast<double>(key) * A, 1.0);
return static_cast<std::size_t>(m * frac);
}
int main() {
std::size_t m = 16; // pangkat dua, OK di sini
int keys[] = {7, 20, 33, 46, 100, 200};
for (int k : keys) {
std::cout << "h(" << k << ") = " << hash_mul(k, m) << '\n';
}
return 0;
}Output yang Dihasilkan:
h(7) = 5
h(20) = 5
h(33) = 6
h(46) = 6
h(100) = 12
h(200) = 9- Baris
const double A = 0.6180339887498949;: konstanta Knuth. Sifatnya membuat pola padakterdistribusi acak ke seluruh rentang[0, m). - Baris
double frac = std::fmod(key * A, 1.0);: ambil bagian pecahan darik * A. Operasi ekuivalen:k * A - floor(k * A). - Baris
return static_cast<std::size_t>(m * frac);: kalikan pecahan denganmuntuk mendapatkan indeks di[0, m-1]. - Hasil: pasangan
h(7)=h(20)=5danh(33)=h(46)=6tetap collision, tetapi sebaran lebih baik daripada division yang seragam menumpuk pada satu bucket untuk deret aritmetika langkahm.
Polynomial Rolling Hash untuk String #
String tidak memiliki representasi numerik natural. Kita bisa memperlakukan string sebagai bilangan berbasis B (mis. B = 31 atau B = 256), dengan modulus m prima besar:
h(s) = (s[0] * B^(n-1) + s[1] * B^(n-2) + ... + s[n-1] * B^0) mod mImplementasi efisien memakai loop Horner:
h = 0
for c in s:
h = (h * B + c) mod mKode: Polynomial Rolling Hash #
#include <iostream>
#include <string>
#include <cstdint>
class RollingHash {
std::uint64_t m_;
std::uint64_t B_;
public:
RollingHash(std::uint64_t m = 1'000'000'007ULL,
std::uint64_t B = 31ULL)
: m_(m), B_(B) {}
std::uint64_t hash(const std::string& s) const {
std::uint64_t h = 0;
for (unsigned char c : s) {
h = (h * B_ + static_cast<std::uint64_t>(c)) % m_;
}
return h;
}
};
int main() {
RollingHash rh;
std::string words[] = {"andi", "andi ", "budi", "adni", "andi"};
for (const auto& w : words) {
std::cout << "h(\"" << w << "\") = " << rh.hash(w) << '\n';
}
return 0;
}Output yang Dihasilkan:
h("andi") = 2998642
h("andi ") = 92957934
h("budi") = 3035160
h("adni") = 2989342
h("andi") = 2998642- Baris
m = 1'000'000'007ULL: bilangan prima besar (10^9 + 7) populer di algoritma kompetitif. Mengurangi collision hingga batas statistik. - Baris
B = 31ULL: basis berupa bilangan prima yang sedikit lebih besar dari 26 (jumlah huruf alfabet). Memastikan setiap posisi karakter memberi kontribusi berbeda. - Baris
h = (h * B_ + c) % m_;: skema Horner — setara dengan menghitung polinomial tetapi dalam O(n) tanpa eksponensiasi terpisah. Modulus di setiap langkah mencegah overflowuint64_tuntuk string panjang. - Hasil
h("andi") = 2998642dua kali: deterministik — input sama menghasilkan output sama. - Hasil
h("adni") = 2989342 != h("andi") = 2998642: permutasi karakter menghasilkan hash berbeda, yang menjadi kunci kegunaan rolling hash untuk pencarian substring (lihat Chapter Penerapan Hashing).
std::hash<T> di STL #
C++ menyediakan std::hash<T> di header <functional> untuk tipe bawaan. Inilah yang dipakai std::unordered_map<K, V> dan std::unordered_set<T> secara default.
Kode: Memakai std::hash #
#include <iostream>
#include <functional>
#include <string>
int main() {
std::hash<int> hi;
std::hash<double> hd;
std::hash<std::string> hs;
std::cout << "hash(42) = " << hi(42) << '\n';
std::cout << "hash(42) = " << hi(42) << " (konsisten)\n";
std::cout << "hash(3.14) = " << hd(3.14) << '\n';
std::cout << "hash(\"andi\") = " << hs("andi") << '\n';
std::cout << "hash(\"budi\") = " << hs("budi") << '\n';
return 0;
}Output yang Dihasilkan:
hash(42) = 42
hash(42) = 42 (konsisten)
hash(3.14) = 5464867211497793177
hash("andi") = 1051165276492383067
hash("budi") = 3010513491367729933- Baris
std::hash<int> hi;: objek function yang meng-hashint. Implementasi libstdc++ biasanya identity:h(x) = x. - Baris
std::hash<std::string> hs;: hash string memakai algoritma FNV-1a atau serupa, yang merupakan polynomial-like hash dengan XOR. - Hasil konsisten
hash(42) = 42: deterministik dalam satu proses, namun tidak dijamin sama antar implementasi (libstdc++ vs libc++). Jangan menyimpanstd::hashke file untuk dibaca program lain — gunakan hash yang spesifik. - Hash
doubledanstringmenghasilkan angka besar: rentang penuhsize_t(64-bit di sini). Nilai spesifik bergantung pada implementasi compiler; angka di atas diambil darig++ 14.2.0libstdc++. STL akan memetakan ke bucket internal viahash % bucket_count.
Perbandingan Metode #
| Metode | Kecepatan | Sensitif m | Cocok untuk | Catatan |
|---|---|---|---|---|
| Division | Sangat cepat | Ya, m prima | Integer, key acak | Default banyak textbook |
| Multiplication | Cepat | Tidak, m boleh 2^p | Key dengan pola | Konstanta A penting |
| Polynomial rolling | O(panjang key) | m besar prima | String, sequence | Dasar Rabin-Karp |
std::hash<T> | Implementasi-dependent | — | Tipe bawaan STL | Tidak portable antar compiler |
Untuk tipe custom (mis. struct Point), kita bisa mengkombinasikan std::hash dari anggota field via pola hash_combine: seed ^= h(field) + 0x9e3779b9 + (seed << 6) + (seed >> 2);. Pola ini mendistribusi seragam dan menjadi idiom standar — akan kita pakai di Chapter unordered_map dan unordered_set.
Ringkasan #
- Hash function yang baik wajib deterministik, cepat, distribusi seragam, dan memiliki avalanche effect.
- Metode division
h(k) = k mod mpaling sederhana, mensyaratkanmprima. - Metode multiplication
h(k) = floor(m * (kA mod 1))mengurangi ketergantungan padam, bolehm = 2^p. - Polynomial rolling hash
h = (h*B + c) mod mcocok untuk string dan menjadi dasar algoritma Rabin-Karp. - STL
std::hash<T>menyediakan hash default untuk tipe bawaan; implementasi tidak portable antar compiler. - Untuk tipe custom, kombinasi
std::hashanggota via polahash_combineadalah idiom standar.