7-bo‘lim

Xesh-jadval (Hash Table)

Xesh funksiya, to'qnashuvlar va ularni hal qilish usullari, yuk koeffitsienti va Python dict ning ichki tuzilishi.

🕑 11 daqiqa o‘qish 📄 726 so‘z 👁 6 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Muammo
  2. G'oya: kalitdan indeksga
  3. Xesh funksiya
  4. Yaxshi xesh funksiyaning talablari
  5. To'qnashuvlar (Collisions)
  6. Usul 1: zanjirlash (chaining)
  7. Usul 2: ochiq adreslash (open addressing)
  8. O'z xesh-jadvalimiz
  9. Yuk koeffitsienti
  10. Python dict va set
  11. Kalit bo'la oladigan turlar
  12. Amaliy misol: anagrammalarni guruhlash
  13. 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):

Python
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.

Kalit Xesh funksiya Massiv (backet) "Husanboy" "Sardor" "Malika" hash(kalit) % hajm matn → son [0] 901234567 [1] bo'sh [2] 935678901 [3] bo'sh [4] 912345678 Kalitdan indeks hisoblanadi → massivga to'g'ridan-to'g'ri murojaat → O(1)
Xesh funksiya kalitni massiv indeksiga aylantiradi

Xesh funksiya #

Xesh funksiya istalgan ma'lumotni belgilangan oraliqdagi songa aylantiradi:

Python
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)}")
Natija
Husanboy   -> indeks 1
Sardor     -> indeks 3
Malika     -> indeks 7
Bekzod     -> indeks 7

Yaxshi xesh funksiyaning talablari #

TalabMa'nosi
DeterminantlikBir xil kalit har doim bir xil natija beradi
TezHisoblash O(1) bo'lishi kerak
Bir tekis taqsimlanishIndekslar butun massivga tarqalsin
Kichik o'zgarish - katta farq"abc" va "abd" turli backetga tushsin
Bizning sodda xeshimiz zaif

ord() yig'indisi harflar tartibini hisobga olmaydi:

Python
print(oddiy_xesh("abc", 100), oddiy_xesh("cba", 100))
Natija
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.

Tug'ilgan kun paradoksi

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.

[0] Bekzod: 9123 [1] Malika: 9356 Aziza: 9011 ← to'qnashuv! [2] bo'sh [3] Husanboy: 9012
To'qnashgan kalitlar bitta backetdagi zanjirda 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 #

Python
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}")
Python
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')}")
Natija
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.

YukHolatQidiruv tezligi
0.25Bo'sh, xotira isrofO(1)
0.75OptimalO(1)
1.5Gavjum, zanjirlar uzunSekinlashadi
10Deyarli ro'yxatO(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).

Eng yomon holat - baribir O(n)

Agar barcha kalitlar bitta backetga tushsa, xesh-jadval oddiy ro'yxatga aylanadi:

Python
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.

Python
# 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.

Python
# 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:

TurKalit bo'la oladimiSabab
strHaO'zgarmas
int, floatHaO'zgarmas
tupleHa*O'zgarmas (ichi ham o'zgarmas bo'lsa)
boolHaO'zgarmas
listYo'qO'zgaruvchan
dictYo'qO'zgaruvchan
setYo'qO'zgaruvchan
Python
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}")
Natija
{(41.311, 69.24): 'Toshkent'}
Xato: unhashable type: 'list'
Nima uchun o'zgaruvchan tur kalit bo'la olmaydi?

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 #

Python
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)
Natija
['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.

Amaliy topshiriq
  1. XeshJadval klassiga kalitlar(), qiymatlar() va juftliklar() metodlarini qo'shing.
  2. Ochiq adreslash usuli bilan ikkinchi xesh-jadval yozing va zanjirlash bilan solishtiring.
  3. Xesh-jadval yordamida ikkita ro'yxatning umumiy elementlarini O(n) da toping.
  4. 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, tuple mumkin; list, dict, set mumkin emas.
  • Python dict va set - tayyor va juda tez xesh-jadvallar.

Keyingi bo'limda ierarxik ma'lumotlar uchun mo'ljallangan tuzilma - daraxtlar bilan tanishamiz.

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.