7-bo‘lim
Xesh-jadval (Hash Table)
Xesh funksiya, to'qnashuvlar va ularni hal qilish usullari, yuk koeffitsienti va Python dict ning ichki tuzilishi.
Ushbu bo‘lim mundarijasi
- Muammo
- G'oya: kalitdan indeksga
- Xesh funksiya
- Yaxshi xesh funksiyaning talablari
- To'qnashuvlar (Collisions)
- Usul 1: zanjirlash (chaining)
- Usul 2: ochiq adreslash (open addressing)
- O'z xesh-jadvalimiz
- Yuk koeffitsienti
- Python dict va set
- Kalit bo'la oladigan turlar
- Amaliy misol: anagrammalarni guruhlash
- Xulosa
Xesh-jadval - dasturlashdagi eng ta'sirchan g'oyalardan biri. U qidiruvni O(n) dan O(1) ga tushiradi.
Muammo #
Telefon kitobida ismga qarab raqam topish kerak. Ro'yxatda bu O(n):
kitob = [("Husanboy", "901234567"), ("Sardor", "912345678"), ("Malika", "935678901")]
def raqamni_top(kitob, ism):
for saqlangan_ism, raqam in kitob: # har birini tekshiramiz
if saqlangan_ism == ism:
return raqam
return None
Million ta kontakt bo'lsa - million ta solishtirish. Bunga chidab bo'lmaydi.
G'oya: kalitdan indeksga #
Massivda royxat[5] O(1) ishlaydi. Agar ismni raqamga aylantira olsak, uni to'g'ridan-to'g'ri indeks sifatida ishlatishimiz mumkin.
Aynan shu ishni xesh funksiya bajaradi.
Xesh funksiya #
Xesh funksiya istalgan ma'lumotni belgilangan oraliqdagi songa aylantiradi:
def oddiy_xesh(kalit, hajm):
"""Juda sodda xesh: belgilar kodlari yig'indisi."""
yigindi = 0
for belgi in str(kalit):
yigindi += ord(belgi)
return yigindi % hajm
for ism in ["Husanboy", "Sardor", "Malika", "Bekzod"]:
print(f"{ism:<10} -> indeks {oddiy_xesh(ism, 8)}")
Husanboy -> indeks 1
Sardor -> indeks 3
Malika -> indeks 7
Bekzod -> indeks 7
Yaxshi xesh funksiyaning talablari #
| Talab | Ma'nosi |
|---|---|
| Determinantlik | Bir xil kalit har doim bir xil natija beradi |
| Tez | Hisoblash O(1) bo'lishi kerak |
| Bir tekis taqsimlanish | Indekslar butun massivga tarqalsin |
| Kichik o'zgarish - katta farq | "abc" va "abd" turli backetga tushsin |
ord() yig'indisi harflar tartibini hisobga olmaydi:
print(oddiy_xesh("abc", 100), oddiy_xesh("cba", 100))
94 94
"abc" va "cba" bir xil indeksga tushadi. Haqiqiy xesh funksiyalar ancha murakkab
(Python SipHash ishlatadi) va bunday muammolardan xoli.
To'qnashuvlar (Collisions) #
Ikki xil kalit bir xil indeksga tushishi muqarrar. Massiv hajmi cheklangan, kalitlar esa cheksiz.
23 kishilik xonada ikki kishining tug'ilgan kuni mos kelish ehtimoli 50% dan yuqori. Xesh-jadvalda ham xuddi shunday: to'qnashuvlar kutilganidan ancha tez paydo bo'ladi. Shuning uchun har bir xesh-jadval ularni hal qilish usuliga ega bo'lishi shart.
Usul 1: zanjirlash (chaining) #
Har bir backetda bitta qiymat emas, ro'yxat saqlanadi.
Usul 2: ochiq adreslash (open addressing) #
To'qnashuv bo'lsa, keyingi bo'sh backetga joylashtiriladi. Bunda ro'yxat kerak emas, lekin o'chirish murakkablashadi.
O'z xesh-jadvalimiz #
class XeshJadval:
"""Zanjirlash usuli bilan ishlaydigan xesh-jadval."""
def __init__(self, hajm=8):
self._hajm = hajm
self._backetlar = [[] for _ in range(hajm)]
self._soni = 0
def _indeks(self, kalit):
"""Kalitni backet indeksiga aylantiradi."""
return hash(kalit) % self._hajm
def qoy(self, kalit, qiymat):
"""Qiymat qo'yadi yoki yangilaydi. O'rtacha O(1)."""
backet = self._backetlar[self._indeks(kalit)]
for i, (saqlangan_kalit, _) in enumerate(backet):
if saqlangan_kalit == kalit:
backet[i] = (kalit, qiymat) # mavjudini yangilaymiz
return
backet.append((kalit, qiymat))
self._soni += 1
if self.yuk_koeffitsienti() > 0.75:
self._kengaytir()
def ol(self, kalit, standart=None):
"""Qiymatni qaytaradi. O'rtacha O(1)."""
backet = self._backetlar[self._indeks(kalit)]
for saqlangan_kalit, qiymat in backet:
if saqlangan_kalit == kalit:
return qiymat
return standart
def ochir(self, kalit):
"""Kalitni o'chiradi."""
backet = self._backetlar[self._indeks(kalit)]
for i, (saqlangan_kalit, _) in enumerate(backet):
if saqlangan_kalit == kalit:
del backet[i]
self._soni -= 1
return True
return False
def yuk_koeffitsienti(self):
"""Elementlar soni / backetlar soni."""
return self._soni / self._hajm
def _kengaytir(self):
"""Hajmni ikki barobar oshiradi va hammasini qayta joylashtiradi. O(n)."""
eski_backetlar = self._backetlar
self._hajm *= 2
self._backetlar = [[] for _ in range(self._hajm)]
self._soni = 0
for backet in eski_backetlar:
for kalit, qiymat in backet:
self.qoy(kalit, qiymat)
def __len__(self):
return self._soni
def __contains__(self, kalit):
backet = self._backetlar[self._indeks(kalit)]
return any(saqlangan == kalit for saqlangan, _ in backet)
def holat(self):
"""Backetlarning to'lish holatini ko'rsatadi."""
for i, backet in enumerate(self._backetlar):
if backet:
kalitlar = ", ".join(str(k) for k, _ in backet)
belgi = "#" * len(backet)
print(f" [{i}] {belgi:<6} {kalitlar}")
kitob = XeshJadval()
kitob.qoy("Husanboy", "901234567")
kitob.qoy("Sardor", "912345678")
kitob.qoy("Malika", "935678901")
kitob.qoy("Bekzod", "946789012")
print("Backetlar holati:")
kitob.holat()
print(f"\nSardor raqami: {kitob.ol('Sardor')}")
print(f"Yuk koeffitsienti: {kitob.yuk_koeffitsienti():.2f}")
kitob.ochir("Sardor")
print(f"O'chirgandan keyin: {kitob.ol('Sardor', 'topilmadi')}")
Backetlar holati:
[2] ## Husanboy, Sardor
[3] # Malika
[6] # Bekzod
Sardor raqami: 912345678
Yuk koeffitsienti: 0.50
O'chirgandan keyin: topilmadi
Yuk koeffitsienti #
Yuk koeffitsienti = elementlar soni / backetlar soni. U xesh-jadvalning "gavjumligini" ko'rsatadi.
| Yuk | Holat | Qidiruv tezligi |
|---|---|---|
| 0.25 | Bo'sh, xotira isrof | O(1) |
| 0.75 | Optimal | O(1) |
| 1.5 | Gavjum, zanjirlar uzun | Sekinlashadi |
| 10 | Deyarli ro'yxat | O(n) |
Yuk 0.75 dan oshganda jadval hajmini ikki barobar oshirib, hamma narsani qayta joylashtiradi. Bu O(n) amal, lekin kamdan-kam sodir bo'ladi - massivdagi kabi amortizatsiyalangan O(1).
Agar barcha kalitlar bitta backetga tushsa, xesh-jadval oddiy ro'yxatga aylanadi:
class YomonKalit:
def __init__(self, qiymat):
self.qiymat = qiymat
def __hash__(self):
return 42 # hamma bir xil!
def __eq__(self, boshqa):
return self.qiymat == boshqa.qiymat
Bu nazariy muammo emas: 2011-yilda hash collision DoS hujumlari real veb-serverlarni ishdan chiqargan. Shu sababli Python 3.3 dan boshlab xesh qiymatlari har bir ishga tushirishda tasodifiy "tuz" bilan aralashtiriladi.
# Buni o'zingiz sinab ko'ring - har safar boshqa natija chiqadi
print(hash("salom"))
Python dict va set #
Python'ning dict va set turlari - yuqori darajada optimallashtirilgan xesh-jadvallar.
# dict - kalit/qiymat
narxlar = {"olma": 12000, "anor": 25000}
print(narxlar["olma"]) # O(1)
print("anor" in narxlar) # O(1)
# set - faqat kalitlar
korilgan = {"Husanboy", "Sardor"}
print("Malika" in korilgan) # O(1)
Kalit bo'la oladigan turlar #
Kalit xeshlanuvchi (hashable), ya'ni o'zgarmas bo'lishi kerak:
| Tur | Kalit bo'la oladimi | Sabab |
|---|---|---|
str | Ha | O'zgarmas |
int, float | Ha | O'zgarmas |
tuple | Ha* | O'zgarmas (ichi ham o'zgarmas bo'lsa) |
bool | Ha | O'zgarmas |
list | Yo'q | O'zgaruvchan |
dict | Yo'q | O'zgaruvchan |
set | Yo'q | O'zgaruvchan |
lugat = {}
lugat[(41.311, 69.240)] = "Toshkent" # kortej - ishlaydi
print(lugat)
try:
lugat[[1, 2]] = "xato" # ro'yxat - ishlamaydi
except TypeError as xato:
print(f"Xato: {xato}")
{(41.311, 69.24): 'Toshkent'}
Xato: unhashable type: 'list'
Tasavvur qiling: ro'yxatni kalit qilib qo'ydingiz, so'ng uni o'zgartirdingiz. Endi uning xeshi boshqa - jadval qiymatni boshqa backetda qidiradi va hech qachon topa olmaydi. Shu sababli Python o'zgaruvchan turlarni kalit sifatida umuman qabul qilmaydi.
Amaliy misol: anagrammalarni guruhlash #
from collections import defaultdict
def anagrammalarni_guruhla(sozlar):
"""Harflari bir xil so'zlarni guruhlaydi. O(n * k log k)."""
guruhlar = defaultdict(list)
for soz in sozlar:
kalit = "".join(sorted(soz.lower())) # anagrammalar uchun bir xil
guruhlar[kalit].append(soz)
return list(guruhlar.values())
sozlar = ["olma", "malo", "amol", "kitob", "bitok", "daftar"]
for guruh in anagrammalarni_guruhla(sozlar):
print(guruh)
['olma', 'malo', 'amol']
['kitob', 'bitok']
['daftar']
Xesh-jadvalsiz bu masalani yechish uchun har bir so'zni har biri bilan solishtirish - O(n²) kerak bo'lardi.
XeshJadvalklassigakalitlar(),qiymatlar()vajuftliklar()metodlarini qo'shing.- Ochiq adreslash usuli bilan ikkinchi xesh-jadval yozing va zanjirlash bilan solishtiring.
- Xesh-jadval yordamida ikkita ro'yxatning umumiy elementlarini O(n) da toping.
- Matndagi birinchi takrorlanmaydigan harfni bitta aylanishda toping.
Xulosa #
- Xesh funksiya kalitni massiv indeksiga aylantiradi - shundan O(1) qidiruv kelib chiqadi.
- To'qnashuvlar muqarrar; ular zanjirlash yoki ochiq adreslash bilan hal qilinadi.
- Yuk koeffitsienti 0.75 dan oshganda jadval kengaytiriladi (amortizatsiyalangan O(1)).
- Eng yomon holatda murakkablik O(n) ga tushadi - shuning uchun xesh sifati muhim.
- Kalit o'zgarmas bo'lishi shart:
str,int,tuplemumkin;list,dict,setmumkin emas. - Python
dictvaset- tayyor va juda tez xesh-jadvallar.
Keyingi bo'limda ierarxik ma'lumotlar uchun mo'ljallangan tuzilma - daraxtlar bilan tanishamiz.
O‘qish tarixini saqlamoqchimisiz?
Tizimga kirsangiz, tugatgan bo‘limlaringiz saqlanadi va qoldirgan joyingizdan davom etasiz.
Xatolik topdingizmi?
Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.