15 β Hash jadval (hash table)¶
β¬ οΈ Oldingi: 14 β Stack, Queue va Deque Β· π README Β· Keyingi: 16 β Daraxtlar va traversal β‘οΈ
Bu bobda: Hash jadval β qidirish, qo'shish va o'chirishni o'rtacha O(1) da bajaradigan ma'lumotlar strukturasi. Asosiy g'oya bilan tanishamiz: kalitni hash funksiya orqali to'g'ridan-to'g'ri massiv indeksiga aylantirish. Kolliziya (turli kalitlar bir indeksga tushishi) muqarrarligini ko'ramiz va uni yechishning ikki klassik usulini β zanjir (chaining) va ochiq adreslash (open addressing) ni β o'rganamiz. Load factor, rehashing va eng yomon holatning nega O(n) ekanini tahlil qilamiz; Python
dict/setaslida shu strukturadir.Halollik / Eslatma: "O(1)" bu yerda amortizatsiyalangan o'rtacha baho β yaxshi hash funksiya va past load factor sharti bilan. Bu kafolat emas: eng yomon holatda bitta amal O(n) bo'ladi. Buni bobda ataylab ochib beramiz. Barcha Python namunalari haqiqatan ishga tushirib tekshirilgan β chiqishlar kod bergan haqiqiy natijalar.
Muammo: qidiruvni qanchalik tezlashtirsa bo'ladi?¶
Sizda kalit-qiymat juftlari bor deylik β masalan, mahsulot nomi va uning narxi. Yangi kalitni qo'shish, kalit bo'yicha qiymatni topish, va kalitni o'chirish kerak. Bu uchta amalni qanchalik tez bajarsa bo'ladi?
Avvalgi boblardan bilamiz:
| Struktura | Qidirish | Qo'shish | O'chirish |
|---|---|---|---|
| Tartibsiz massiv | O(n) | O(1) oxiriga | O(n) |
| Saralangan massiv | O(log n) binar qidiruv | O(n) | O(n) |
| Bog'langan ro'yxat | O(n) | O(1) boshiga | O(n) topib o'chirish |
Saralangan massivdagi binar qidiruv ancha tez β O(log n) β lekin qo'shish/o'chirish baribir O(n), chunki saralanishni saqlash kerak. Hammasi n ga bog'liq.
Endi jasur savol: qidiruvni n ga umuman bog'liq bo'lmaydigan qilib bo'ladimi? Ya'ni million element bo'lsin yoki o'n element β qidiruv bir xil tez bo'lsin. Bu "sehrli" tezlikka o'xshaydi, lekin hash jadval aynan shuni β o'rtacha O(1) ni β beradi.
Asosiy g'oya: kalitni indeksga aylantirish¶
Sehrning siri oddiy. Massivda elementga indeks bo'yicha murojaat O(1) (a[5] β bir qadam). Demak, agar biz kalitdan to'g'ridan-to'g'ri indeksni hisoblay olsak, qidiruv ham O(1) bo'ladi β qidirishning hojati yo'q, to'g'ri katakka boramiz.
Buni qiladigan funksiya β hash funksiya h:
Bu yerda m β massivning sig'imi (bucket'lar soni). Massivning har bir katagi bucket (savatcha) deb ataladi.
Eng oddiy misol β modulo hash (butun son kalit uchun):
def h(kalit, m):
return kalit % m
m = 10
kalitlar = [25, 35, 17, 9, 45, 7]
for k in kalitlar:
print(k, "->", h(k, m))
# 25 -> 5
# 35 -> 5 <- 25 bilan kolliziya!
# 17 -> 7
# 9 -> 9
# 45 -> 5 <- yana 5
# 7 -> 7 <- 17 bilan kolliziya!
kalit % m har doim [0, m) oralig'ida bo'ladi β aynan bizga kerak indeks. E'tibor bering: 25, 35, 45 β uchalasi ham 5 ga tushdi. Bu kolliziya (collision) β keyin ko'ramiz.
Kalit satr bo'lsa-chi? Satrni avval songa aylantirib, keyin modulo olamiz. Klassik usul β polinomial hash: har bir belgini p ning darajalariga ko'paytirib qo'shamiz (Horner sxemasi bilan):
def satr_hash(s, m, p=31):
h = 0
for ch in s:
h = (h * p + ord(ch)) % m
return h
m = 1000
for s in ["it", "ot", "uy", "olma"]:
print(s, "->", satr_hash(s, m))
# it -> 371
# ot -> 557
# uy -> 748
# olma -> 65
Eslatma:
podatda alifbo hajmidan kattaroq tub son (31, 53, 257...) tanlanadi β bu belgilar bir tekisroq aralashishiga yordam beradi.mham ko'pincha tub son qilinadi, chunki tub bo'lmaganmba'zi kalitlar uchun yomon (notekis) taqsimot berishi mumkin.
Yaxshi hash funksiyaning xossalari¶
Hash funksiya yaxshi bo'lishi uchun to'rt xossa kerak:
- Deterministik β bir kalit har doim bir xil indeks beradi. Aks holda qo'ygan joyimizni topa olmaymiz.
- Tez β hash hisoblashning o'zi O(1) (yoki satr uchun uning uzunligiga chiziqli) bo'lishi kerak, aks holda butun foyda yo'qoladi.
- Bir tekis taqsimot (uniform) β kalitlar bucket'lar bo'ylab teng tarqalsin. Agar hamma 5-bucketga tushsa, struktura oddiy ro'yxatdan farq qilmaydi.
- Kichik o'zgarish -> katta farq β "olma" va "olmacha" butunlay boshqa indeksga tushishi yaxshi (lavina effekti).
Kolliziya: muqarrar haqiqat¶
Yuqorida 25 va 35 ikkalasi ham 5 ga tushdi. Bu tasodif emas β kolliziya muqarrar. Buning sababini matematik aniq ayta olamiz.
Kaptarxona (pigeonhole) printsipi: Agar
kta kaptarnimta uyaga joylasangiz vak > mbo'lsa, kamida bitta uyada ikki yoki undan ortiq kaptar bo'ladi.
Bizning holatda kalitlar β kaptarlar, bucket'lar β uyalar. Kalitlar koinoti odatda bucket'lardan juda katta: masalan, barcha mumkin bo'lgan satrlar cheksiz ko'p, lekin m chekli. Demak ikki turli kalit bir indeksga tushishi muqarrar.
Xulosa: kolliziyani oldini olib bo'lmaydi, uni boshqarish kerak. Buning ikki asosiy yechimi bor.
1-yechim: zanjir usuli (separate chaining)¶
Eng tabiiy g'oya: har bir bucket bitta element emas, balki elementlar ro'yxati (zanjir) bo'lsin. Bir indeksga tushgan barcha kalitlar o'sha bucketdagi bog'langan ro'yxatga qo'shiladi.
Amallar shunday ishlaydi:
- Qo'shish:
i = h(kalit)ni hisoblaymiz,buckets[i]ro'yxatiga juftni qo'shamiz (kalit bor bo'lsa β yangilaymiz). - Qidirish:
i = h(kalit), keyinbuckets[i]zanjirini boshidan oxirigacha yurib, kalitni qidiramiz. - O'chirish:
i = h(kalit), zanjirdan kalitni topib o'chiramiz.
Mana to'liq amalga oshirilgan hash jadval (chaining bilan, noldan):
class HashJadval:
def __init__(self, sigim=8):
self.m = sigim
self.n = 0
self.buckets = [[] for _ in range(self.m)]
def _indeks(self, kalit):
return hash(kalit) % self.m
def _rehash(self):
eski = self.buckets
self.m *= 2
self.buckets = [[] for _ in range(self.m)]
self.n = 0
for bucket in eski:
for k, v in bucket:
self.put(k, v)
def put(self, kalit, qiymat):
i = self._indeks(kalit)
for j, (k, v) in enumerate(self.buckets[i]):
if k == kalit:
self.buckets[i][j] = (kalit, qiymat) # yangilash
return
self.buckets[i].append((kalit, qiymat))
self.n += 1
if self.n / self.m > 0.75: # load factor chegarasi
self._rehash()
def get(self, kalit):
i = self._indeks(kalit)
for k, v in self.buckets[i]:
if k == kalit:
return v
raise KeyError(kalit)
def remove(self, kalit):
i = self._indeks(kalit)
for j, (k, v) in enumerate(self.buckets[i]):
if k == kalit:
self.buckets[i].pop(j)
self.n -= 1
return
raise KeyError(kalit)
hj = HashJadval()
hj.put("olma", 3)
hj.put("nok", 5)
hj.put("uzum", 12)
print(hj.get("nok")) # -> 5
hj.put("nok", 9) # yangilash
print(hj.get("nok")) # -> 9
hj.remove("olma")
print(hj.n, "ta element") # -> 2 ta element
Eslatma: Bu yerda Python'ning o'rnatilgan
hash()funksiyasidan foydalandik (u har xil tip uchun yaxshi hash beradi), keyin% self.mbilan indeksga aylantirdik. Haqiqiydictham shu g'oyaga asoslangan, lekin C tilida ancha optimallashtirilgan.
Afzalligi: sodda, o'chirish oson, load factor 1 dan oshsa ham ishlay beradi (zanjir uzayadi, xolos). Kamchiligi: har bir tugun uchun qo'shimcha xotira (pointer'lar), va keshda yomonroq joylashish.
2-yechim: ochiq adreslash (open addressing)¶
Boshqa g'oya: hech qanday tashqi ro'yxat yo'q β barcha kalitlar massivning o'zida saqlanadi. Agar h(kalit) katagi band bo'lsa, biror qoida bo'yicha keyingi bo'sh katakni qidiramiz. Bu jarayon probing deyiladi.
Probing turlari:
- Linear probing (chiziqli):
i, i+1, i+2, ...β ketma-ket keyingi kataklarni sinab ko'ramiz. Sodda, kesh-do'st, lekin klasterlanish (kalitlar to'planib qolishi) muammosi bor. - Quadratic probing (kvadratik):
i+1, i+4, i+9, ...β sakrashlar kattalashadi, klasterlanish kamayadi. - Double hashing (ikkilamchi hash):
i + kΒ·hβ(kalit)β qadamning o'zi ikkinchi hash funksiyaga bog'liq. Eng yaxshi taqsimot.
O'chirishning nozikligi: tombstone¶
Ochiq adreslashda o'chirish β eng noziq qism. Katakni shunchaki "bo'sh" qilib bo'lmaydi! Sababini ko'raylik:
"nok" [2] da, "anor" esa [2] band bo'lgani uchun [3] da turibdi (linear probing). Endi "nok" ni o'chirib, [2] ni "bo'sh" qilsak β keyin "anor" ni qidirsak, [2] ga kelamiz, "bo'sh" ko'ramiz va "yo'q ekan" deb to'xtaymiz. Lekin "anor" bor β biz uni yo'qotib qo'ydik, chunki qidiruv "bo'sh" katakda to'xtaydi.
Yechim β tombstone (qabr toshi): o'chirilgan katakni "bo'sh" emas, balki maxsus "o'chirilgan" belgisi bilan belgilaymiz. Qidiruv tombstone'dan o'tib ketadi (to'xtamaydi), qo'shish esa uni qayta ishlatishi mumkin.
Mana linear probing'ni tombstone bilan amalga oshirish:
_BOSH = object() # hech qachon ishlatilmagan katak
_OCHIRILGAN = object() # tombstone
class OchiqHash:
def __init__(self, sigim=8):
self.m = sigim
self.n = 0
self.kalitlar = [_BOSH] * self.m
self.qiymatlar = [None] * self.m
def _rehash(self):
eski_k, eski_v = self.kalitlar, self.qiymatlar
self.m *= 2
self.kalitlar = [_BOSH] * self.m
self.qiymatlar = [None] * self.m
self.n = 0
for k, v in zip(eski_k, eski_v):
if k is not _BOSH and k is not _OCHIRILGAN:
self.put(k, v)
def put(self, kalit, qiymat):
if (self.n + 1) / self.m > 0.5: # ochiq adreslashda a < 1 SHART
self._rehash()
i = hash(kalit) % self.m
birinchi_tombstone = -1
while self.kalitlar[i] is not _BOSH:
if self.kalitlar[i] == kalit:
self.qiymatlar[i] = qiymat
return
if self.kalitlar[i] is _OCHIRILGAN and birinchi_tombstone == -1:
birinchi_tombstone = i
i = (i + 1) % self.m # linear probing
joy = birinchi_tombstone if birinchi_tombstone != -1 else i
self.kalitlar[joy] = kalit
self.qiymatlar[joy] = qiymat
self.n += 1
def get(self, kalit):
i = hash(kalit) % self.m
while self.kalitlar[i] is not _BOSH:
if self.kalitlar[i] == kalit:
return self.qiymatlar[i]
i = (i + 1) % self.m # tombstone'dan o'tib ketadi
raise KeyError(kalit)
def remove(self, kalit):
i = hash(kalit) % self.m
while self.kalitlar[i] is not _BOSH:
if self.kalitlar[i] == kalit:
self.kalitlar[i] = _OCHIRILGAN # tombstone qo'yamiz
self.qiymatlar[i] = None
self.n -= 1
return
i = (i + 1) % self.m
raise KeyError(kalit)
oh = OchiqHash()
oh.put("a", 1); oh.put("b", 2); oh.put("c", 3)
print(oh.get("b")) # -> 2
oh.remove("b")
oh.put("d", 4)
print(oh.get("d")) # -> 4
print(oh.n, "element") # -> 3 element
try:
oh.get("b")
except KeyError:
print("b yo'q") # -> b yo'q
Trade-off: Ochiq adreslash xotirani tejaydi (pointer yo'q) va kesh-do'st, lekin load factor
Ξ± < 1bo'lishi shart (joy massivning o'zida) vaΞ±0.7 dan oshsa keskin sekinlashadi. Chaining esaΞ± > 1ga ham toqat qiladi, lekin qo'shimcha xotira oladi.
Load factor va rehashing¶
Load factor (to'lganlik darajasi):
Bu β o'rtacha bitta bucketga nechta element to'g'ri kelishini bildiradi. Ξ± qancha katta bo'lsa, kolliziya shuncha ko'p, qidiruv shuncha sekin:
- Chaining'da o'rtacha zanjir uzunligi aynan
Ξ±β demak qidiruv o'rtachaO(1 + Ξ±). - Ochiq adreslashda
Ξ±1 ga yaqinlashganda probing juda uzayadi.
Yechim β rehashing: Ξ± belgilangan chegaradan (masalan 0.75) oshganda, massivni kattalashtirib (odatda 2 barobar) barcha elementlarni qayta joylashtiramiz. Yangi m bilan har bir kalit qaytadan hash qilinadi (chunki indeks % m ga bog'liq).
Quyida rehash qachon ishga tushishini kuzatamiz (chegara 0.75, boshlang'ich m = 4):
class HJ:
def __init__(self, sigim=4):
self.m = sigim; self.n = 0
self.buckets = [[] for _ in range(self.m)]
def _i(self, k): return hash(k) % self.m
def _rehash(self):
eski = self.buckets; self.m *= 2
self.buckets = [[] for _ in range(self.m)]; self.n = 0
for b in eski:
for k, v in b: self.put(k, v)
def put(self, k, v):
i = self._i(k)
for j, (kk, vv) in enumerate(self.buckets[i]):
if kk == k: self.buckets[i][j] = (k, v); return
self.buckets[i].append((k, v)); self.n += 1
if self.n / self.m > 0.75: self._rehash()
hj = HJ(4)
for x in range(10):
hj.put(x, x * x)
print("n =", hj.n, " m =", hj.m, " load =", round(hj.n / hj.m, 2))
# n = 1 m = 4 load = 0.25
# n = 2 m = 4 load = 0.5
# n = 3 m = 4 load = 0.75
# n = 4 m = 8 load = 0.5 <- rehash! m 4->8
# n = 5 m = 8 load = 0.62
# n = 6 m = 8 load = 0.75
# n = 7 m = 16 load = 0.44 <- rehash! m 8->16
# n = 8 m = 16 load = 0.5
# n = 9 m = 16 load = 0.56
# n = 10 m = 16 load = 0.62
E'tibor bering: rehash m ikki barobar oshganda yuz beradi va Ξ± ni yana pasaytiradi. Bitta rehash O(n) β barcha elementni ko'chirish kerak. Lekin u kamdan-kam sodir bo'ladi (har 2x o'sishda bir marta), shuning uchun amortizatsiyalangan tahlil bo'yicha bitta put baribir amortized O(1) β xuddi dinamik massivga append kabi.
Murakkablik: O(1) β lekin qaysi ma'noda?¶
Bu β bobning eng muhim nuance'i. Hash jadvalning murakkabligini bir so'z bilan "O(1)" deyish noto'g'ri va xavfli. Aniq jadval:
| Amal | O'rtacha (yaxshi hash, past Ξ±) | Eng yomon holat |
|---|---|---|
Qidirish (get) |
O(1) | O(n) |
Qo'shish (put) |
O(1) amortized | O(n) |
O'chirish (remove) |
O(1) | O(n) |
Nega o'rtacha O(1)? Agar hash bir tekis taqsimlasa va Ξ± kichik (chegaralangan) bo'lsa, o'rtacha zanjir uzunligi O(1), demak qidiruv O(1).
Nega eng yomon holat O(n)? Agar hash funksiya yomon bo'lsa (yoki dushman ataylab kalitlarni tanlasa) va hamma kalit bitta bucketga tushsa, struktura oddiy bog'langan ro'yxatga aylanadi β qidiruv O(n). Buni ko'rsataylik:
def yomon_hash(k, m):
return 0 # har doim 0 β barcha kalit bitta bucketda!
m = 8
buckets = [[] for _ in range(m)]
for k in [10, 20, 30, 40, 50]:
buckets[yomon_hash(k, m)].append(k)
print(buckets[0]) # -> [10, 20, 30, 40, 50]
print("0-bucket uzunligi:", len(buckets[0])) # -> 5
# qidiruv endi O(n): bitta uzun zanjirni yurish kerak
Diqqat: "Hash jadval O(1)" deganda, doimo o'rtacha va amortized nazarda tutiladi β bu kafolat emas, taxmin (ekspektatsiya). Real-time tizimda, har bir amal qattiq vaqt chegarasiga sig'ishi shart bo'lsa, hash jadval (rehash sababli bitta amal O(n) bo'lishi mumkinligi uchun) ehtiyotkorlik talab qiladi.
Xotira: struktura O(n + m) joy oladi. Ξ± ni past ushlash uchun m ni n ga taqqoslanadigan qilib saqlaymiz, demak xotira O(n).
Qo'llanmalar: ko'p masala O(n) ga tushadi¶
Hash jadval shunchaki "kalit-qiymat saqlash" emas β u algoritmik qurol. Avvalgi boblarda O(nΒ²) yoki O(n log n) bo'lgan ko'p masala hash bilan O(n) ga tushadi.
1. Lug'at / map β kalitdan qiymatga moslik (Python dict).
2. To'plam (set) β faqat kalitlar, takrorsiz (Python set).
3. Sanash (frequency) β har element necha marta uchraganini sanash:
matn = "olma nok olma uzum nok olma"
sanoq = {}
for soz in matn.split():
sanoq[soz] = sanoq.get(soz, 0) + 1
print(sanoq) # -> {'olma': 3, 'nok': 2, 'uzum': 1}
from collections import Counter
print(Counter(matn.split())) # -> Counter({'olma': 3, 'nok': 2, 'uzum': 1})
4. Takroriy element bormi (O(n)) β set bilan bir o'tishda:
def takror_bormi(a):
korilgan = set()
for x in a:
if x in korilgan:
return True
korilgan.add(x)
return False
print(takror_bormi([3, 1, 4, 1, 5])) # -> True
print(takror_bormi([3, 1, 4, 5])) # -> False
Saralash bilan bu O(n log n) bo'lardi; hash bilan O(n).
5. Ikki ro'yxat kesishmasi (O(n+m)) β bittasini set qilib, ikkinchisini bir o'tishda tekshiramiz:
a = [1, 2, 3, 4, 5]
b = [4, 5, 6, 7]
s = set(a)
kesishma = [x for x in b if x in s]
print(kesishma) # -> [4, 5]
Ikki ichma-ich sikl bilan bu O(nΒ·m) bo'lardi; hash bilan O(n+m).
6. Keshlash (memoization) β hisoblangan natijalarni kalit bo'yicha saqlash (rekursiyada takror hisoblashning oldini olish).
Python dict va set β aslida hash jadval¶
Python'ning dict va set ichida aynan hash jadval ishlaydi (CPython'da ochiq adreslash + maxsus optimizatsiyalar). Shuning uchun:
| Amal | dict / set o'rtacha |
|---|---|
d[k] (qidirish) |
O(1) |
d[k] = v (qo'shish) |
O(1) amortized |
del d[k] (o'chirish) |
O(1) |
k in d (bor-yo'qligi) |
O(1) |
Shuning uchun x in lst (ro'yxatda) O(n), lekin x in s (set/dict'da) O(1) β katta ma'lumotda farq ulkan.
Eslatma:
dict/setga faqat hashlanadigan (hashable) obyektlarni qo'yish mumkin: son, satr, tuple (o'zgarmaslar).listyokidictkalit bo'la olmaydi β chunki ular o'zgaruvchan, demak hash'i barqaror emas.
Xavfsizlik: hash kolliziya hujumi¶
Qisqacha bir xavf haqida. Agar hash funksiya oldindan ma'lum va o'zgarmas bo'lsa, hujumchi bir bucketga tushadigan minglab kalitlarni ataylab tanlashi mumkin β bunda har bir amal O(n) ga aylanadi va server "muzlab" qoladi (DoS hujumi, "hash flooding").
Himoya β hash randomizatsiyasi: har bir dastur ishga tushganda hash funksiyaga tasodifiy "tuz" (seed) qo'shiladi, shuning uchun hujumchi qaysi kalitlar kolliziya berishini oldindan bilolmaydi. Python str/bytes hash'i 3.3 versiyadan beri shunday randomizatsiyalangan (PYTHONHASHSEED).
Asosiy g'oyalar (bobni qisqacha)¶
- Hash jadval kalitni hash funksiya orqali massiv indeksiga aylantirib, qidirish/qo'shish/o'chirishni o'rtacha O(1) da bajaradi.
- Yaxshi hash funksiya: deterministik, tez, bir tekis taqsimot, kichik o'zgarishga sezgir.
- Kolliziya muqarrar (kaptarxona printsipi: kalitlar bucket'lardan ko'p). Ikki yechim: zanjir (chaining) β bucketda bog'langan ro'yxat; ochiq adreslash β keyingi bo'sh katakni probing bilan qidirish.
- Ochiq adreslashda o'chirish tombstone talab qiladi, aks holda zanjir uzilib, kalitlar yo'qoladi.
- Load factor
Ξ± = n/mβ to'lganlik. KattaΞ±-> ko'p kolliziya. Rehashing (2x kattalashtirish)Ξ±ni past ushlaydi; bittaputshu sababli amortized O(1). - Eng yomon holat O(n) β hamma kalit bir bucketga tushsa. "O(1)" β o'rtacha/amortized, kafolat emas.
- Python
dict/setβ hash jadval;intekshiruv O(1). Ko'p masala (sanash, takror topish, kesishma) hash bilan O(n) ga tushadi.
Mashqlar¶
Oson¶
1-mashq. m = 7 bo'lsin. Quyidagi kalitlar uchun h(k) = k mod m ni hisoblang: 14, 8, 21, 9, 2. Qaysilari kolliziya beradi?
2-mashq. h(k) = k mod 6 bilan 12, 18, 7, 25 kalitlari qaysi bucket'larga tushadi? Bo'sh qoladigan bucket'lar qaysilar?
3-mashq. Load factor ta'rifini ayting. n = 12 element, m = 16 bucket bo'lsa, Ξ± nechaga teng? Bu "salomat" (sog'lom) qiymatmi?
O'rta¶
4-mashq. Bo'sh chaining-jadval, m = 5, h(k) = k mod 5. Quyidagi kalitlarni shu tartibda qo'shing: 10, 15, 7, 22, 12, 17. Har bir bucketning oxirgi holatini (zanjirlar bilan) chizib bering.
5-mashq. 4-mashqdagi yakuniy jadval uchun load factor Ξ± ni hisoblang. Eng uzun zanjir qancha?
6-mashq. [4, 2, 7, 2, 9, 4, 4, 7] ro'yxatida har bir son necha marta uchraganini hash jadval (dict) yordamida sanaydigan g'oyani so'z bilan tushuntiring va murakkabligini ayting.
Qiyin¶
7-mashq. Linear probing'li hash jadval, m = 7, h(k) = k mod 7. Kalitlar shu tartibda qo'shiladi: 10, 17, 24, 3, 31. Har bir kalit qaysi indeksga tushishini probing qadamlari bilan ko'rsating (10 -> 3, ...).
8-mashq. Ikkita ro'yxat a va b berilgan. Ularning umumiy elementlarini (kesishmasini) O(n + m) da topadigan algoritmni yozing va nega ichma-ich sikldan (O(nΒ·m)) tez ekanini tushuntiring.
9-mashq. Rehashing tahlili: m = 1 dan boshlab, har 2x kattalashtirsh bilan n ta element qo'shamiz. Barcha rehash'larning jami narxi O(n) ekanini geometrik qator orqali isbotlang. Demak bitta put amortized qancha?
10-mashq. Hash jadvalning eng yomon holat qidiruvi nega O(n) ekanini tushuntiring. Bu qaysi vaziyatda yuz beradi va undan qanday himoyalanish mumkin (ikki usul ayting)?
Yechimlar
1-mashq yechimi¶
h(k) = k mod 7:
- 14 mod 7 = 0
- 8 mod 7 = 1
- 21 mod 7 = 0 β 14 bilan kolliziya (ikkalasi 0)
- 9 mod 7 = 2
- 2 mod 7 = 2 β 9 bilan kolliziya (ikkalasi 2)
Kolliziyalar: {14, 21} indeks 0 da, {9, 2} indeks 2 da.
2-mashq yechimi¶
h(k) = k mod 6:
- 12 mod 6 = 0
- 18 mod 6 = 0 β 12 bilan kolliziya
- 7 mod 6 = 1
- 25 mod 6 = 1 β 7 bilan kolliziya
Band bucket'lar: 0 ({12,18}), 1 ({7,25}). Bo'sh qoladigan bucket'lar: 2, 3, 4, 5.
3-mashq yechimi¶
Load factor Ξ± = n / m β o'rtacha bitta bucketga to'g'ri keladigan elementlar soni.
Ξ± = 12 / 16 = 0.75. Ha, bu sog'lom qiymat β ko'p amalga oshirishlar aynan 0.75 ni rehash chegarasi qilib oladi. Ξ± 0.75 dan oshsa, kolliziya ko'payadi va rehash kerak bo'ladi.
4-mashq yechimi¶
h(k) = k mod 5:
- 10 -> 0, 15 -> 0, 7 -> 2, 22 -> 2, 12 -> 2, 17 -> 2
Qo'shilish tartibida (yangilari zanjir oxiriga):
5-mashq yechimi¶
n = 6 element, m = 5 bucket. Ξ± = 6 / 5 = 1.2.
Eng uzun zanjir β bucket [2] da: 7 -> 22 -> 12 -> 17, ya'ni 4 ta element. E'tibor: Ξ± = 1.2 > 1 β chaining'da bu mumkin (ochiq adreslashda mumkin emas bo'lardi), lekin zanjir [2] juda uzun, demak rehash foydali bo'lardi.
6-mashq yechimi¶
G'oya: bo'sh dict ochamiz. Ro'yxatni bir marta aylanib chiqamiz; har bir son uchun sanoq[son] = sanoq.get(son, 0) + 1. dictga murojaat o'rtacha O(1), umumiy O(n).
a = [4, 2, 7, 2, 9, 4, 4, 7]
sanoq = {}
for x in a:
sanoq[x] = sanoq.get(x, 0) + 1
print(sanoq) # -> {4: 3, 2: 2, 7: 2, 9: 1}
Murakkablik: O(n) vaqt, O(k) xotira (k β turli qiymatlar soni).
7-mashq yechimi¶
h(k) = k mod 7, linear probing (band bo'lsa i+1):
10 mod 7 = 3-> [3] bo'sh -> [3]17 mod 7 = 3-> [3] band -> [4] bo'sh -> [4]24 mod 7 = 3-> [3] band -> [4] band -> [5] bo'sh -> [5]3 mod 7 = 3-> [3],[4],[5] band -> [6] bo'sh -> [6]31 mod 7 = 3-> [3]..[6] band -> [0] (aylanib) bo'sh -> [0]
Yakuniy massiv:
Bu klasterlanishning yorqin misoli: hammasi 3 ga tushgani uchun uzun blok hosil bo'ldi.
8-mashq yechimi¶
def kesishma(a, b):
s = set(a) # O(n): a ni set qilamiz
return [x for x in b if x in s] # O(m): har bir b elementini O(1) tekshiramiz
print(kesishma([1, 2, 3, 4, 5], [4, 5, 6, 7])) # -> [4, 5]
Murakkablik O(n + m): set(a) O(n), keyin b bo'yicha bir o'tish, har bir in s tekshiruvi o'rtacha O(1).
Ichma-ich sikl (har a uchun butun b ni tekshirish) O(nΒ·m) bo'lardi. Hash in tekshiruvini O(m) qidiruvdan O(1) ga tushirgani uchun tezroq.
9-mashq yechimi¶
m 1, 2, 4, 8, ..., n bo'lib o'sadi. Har rehash o'sha paytdagi barcha elementni ko'chiradi, ya'ni 1 + 2 + 4 + ... + n/2 + n ta ko'chirish. Bu geometrik qator:
(Isbot: S = 1+2+...+2^k bo'lsa, 2S - S = 2^(k+1) - 1, ya'ni S = 2^(k+1) - 1 = 2n - 1.)
Demak n ta putning jami narxi = n ta oddiy qo'shish (O(n)) + < 2n ta ko'chirish (O(n)) = O(n). Bitta putga: O(n) / n = amortized O(1). (Batafsil: 11-bob.)
10-mashq yechimi¶
Eng yomon holat qidiruvi O(n) bo'ladi, agar hamma n ta kalit bitta bucketga tushsa (chaining'da) yoki bitta uzun probing klasteriga to'planib qolsa (ochiq adreslashda). Bunda struktura oddiy bog'langan ro'yxatga aylanadi: kalitni topish uchun n ta elementni ketma-ket tekshirish kerak.
Bu yuz beradi, agar: (a) hash funksiya yomon (bir tekis taqsimlamaydi), yoki (b) dushman kolliziya beradigan kalitlarni ataylab tanlasa.
Himoya ikki usul:
1. Yaxshi, bir tekis taqsimlaydigan hash funksiya tanlash (tub p, tub m, lavina effekti) β bu o'rtacha holatni O(1) saqlaydi.
2. Hash randomizatsiyasi (tasodifiy seed) β hujumchi qaysi kalitlar kolliziya berishini oldindan bilolmaydi, demak ataylab eng yomon holatni keltirib chiqara olmaydi.
(Qo'shimcha: ba'zi tillar bucketdagi zanjir uzaysa, uni qizil-qora daraxtga aylantiradi β bu eng yomon holatni O(n) dan O(log n) ga tushiradi. Java HashMap shunday qiladi.)
β¬ οΈ Oldingi: 14 β Stack, Queue va Deque Β· π README Β· Keyingi: 16 β Daraxtlar va traversal β‘οΈ