Hash Table daxili quruluşu
Hash table (JS-də Map/obyekt, Kotlin-də HashMap, Dart-da Map) açar–dəyər cütlərini saxlayır və orta halda O(1) axtarış verir. Bunun arxasında cəmi üç addım dayanır:
- Hash funksiyası açarı tam ədədə çevirir:
hash("email") → 2938471 - Bucket indeksi həmin ədədi massivin ölçüsünə sıxır:
index = hash % capacity - Bucket — daxili massivin həmin xanası, cütün faktiki yazıldığı yer
Yəni hash table sehr deyil — o, adi massivdir. Bütün fərq ondadır ki, indeksi sən yox, açardan hesablanan hash verir. Massivdə element axtarmaq üçün gəzmək lazım gəlmir: indeksi bir dəfə hesablayıb birbaşa oraya baxırsan.
Yaxşı hash funksiyası nə deməkdir? Üç tələb:
- Deterministik — eyni açar həmişə eyni hash verir. Əks halda yazdığını heç vaxt tapa bilməzsən.
- Bərabər paylanma — açarlar bucket-lar arasında yayılmalıdır. Hamısı bir bucket-a düşürsə, hash table adi siyahıya çevrilir.
- Sürətli — hash hesablamaq axtarışın özündən baha olmamalıdır.
Diqqət: kriptoqrafik güc burada tələb DEYİL. SHA-256 hash map üçün lazımsız bahadır; real runtime-lar MurmurHash, SipHash və ya sadə 31 * h + char kimi ucuz funksiyalardan istifadə edir.
Bir də çox vacib qayda: hash dəyişməz olmalıdır. Açar kimi istifadə etdiyin obyektin hash-ə təsir edən sahəsini map-a qoyduqdan sonra dəyişsən, element "itir" — o, köhnə bucket-də qalır, sən isə yeni bucket-də axtarırsan.
| Anlayış | Nə deməkdir | Praktik dəyər |
|---|---|---|
| Capacity | Daxili bucket massivinin ölçüsü | Adətən 2-nin qüvvəti (16, 32, 64...) |
| Size | Saxlanan cütlərin faktiki sayı | `map.size` / `map.length` |
| Load factor | size / capacity nisbəti | Java/Kotlin HashMap-də hədd 0.75 |
| Resize (rehash) | Capacity 2 dəfə artır, bütün elementlər yenidən yerləşdirilir | Tək əməliyyat O(n), amortizə olunmuş O(1) |
| Collision | İki fərqli açar eyni bucket indeksi alır | Qaçılmazdır — ayrıca mövzuda |
Niyə orta halda O(1), pis halda O(n)?
Orta hal: hash bərabər paylanıbsa, hər bucket-də orta hesabla 1-ə yaxın element olur. İndeksi hesablamaq O(1), bucket-in içində axtarmaq O(1) — cəmi O(1).
Pis hal: bütün açarlar eyni bucket-a düşürsə (pis hash funksiyası və ya bilərəkdən qurulmuş açarlar), bucket bir zəncirə çevrilir və axtarış O(n) olur. Bu nəzəri qorxu deyil — hash flooding hücumları məhz bunu edir, ona görə də müasir runtime-lar hash-i təsadüfi seed ilə "duzlayır".
Resizing ayrı bir nüansdır: capacity 0.75 həddini keçəndə map ikiqat böyüyür və bütün elementləri yenidən hash-ləyir — o bir put O(n) çəkir. Amma bu n əməliyyata bir dəfə baş verdiyi üçün amortizə olunmuş dəyər yenə O(1) qalır. Ölçünü qabaqcadan bilirsənsə (new HashMap(expectedSize)), bu təkrar rehash-lardan tamam yaxa qurtarırsan.
İnterview ipucu. "Hash map arxa planda necə işləyir?" sualı ilə müsahib səndən implementasiya deyil, mexanizmin ardıcıllığını eşitmək istəyir. Güclü cavabın skeleti: açar → hash funksiyası → bucket indeksi (hash % capacity) → bucket-də collision həlli → load factor keçiləndə resize. Sonra mütləq mürəkkəbliyi əlavə et: orta O(1), pis hal O(n), resize amortizə olunur.
Ən çox buraxılan iki şey: (1) namizədlər collision-dan heç danışmır, sanki hash həmişə unikaldır; (2) resize/load factor-u ümumiyyətlə bilmirlər. Bu ikisini özün gündəmə gətirsən, cavab dərhal middle səviyyəsində səslənir.
📚 Mənbələr və sənədlər
- Maprəsmideveloper.mozilla.org
- Kotlin HashMaprəsmikotlinlang.org