16-bo‘lim

Rekursiya va "bo'l va hukmronlik qil"

Rekursiya anatomiyasi, bazaviy holat, chaqiruvlar daraxti, memoizatsiya va bo'l va hukmronlik qil yondashuvi.

🕑 13 daqiqa o‘qish 📄 715 so‘z 👁 7 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Rekursiyaning ikki qismi
  2. Chaqiruvlar daraxti
  3. Klassik rekursiv masalalar
  4. Rekursiyaning qimmatga tushishi
  5. Memoizatsiya - yechim
  6. Rekursiya yoki sikl?
  7. Bo'l va hukmronlik qil
  8. Master teorema
  9. Amaliy misol: Xanoy minoralari
  10. Rekursiya bilan fayl tizimini aylanish
  11. Xulosa

Rekursiya - boshlovchilar uchun eng qiyin, tajribalilar uchun esa eng chiroyli tushunchalardan biri. Uning mohiyati: masalani o'zining kichikroq nusxasi orqali yechish.

Rekursiyaning ikki qismi #

Har bir rekursiv funksiya ikkita majburiy qismdan iborat:

QismVazifasi
Bazaviy holatRekursiyani to'xtatadi
Rekursiv holatMasalani kichraytirib, o'zini chaqiradi
Python
def faktorial(n):
    if n <= 1:                    # bazaviy holat
        return 1
    return n * faktorial(n - 1)   # rekursiv holat


print(faktorial(5))
Natija
120
Bazaviy holatsiz rekursiya
Python
def yomon(n):
    return n * yomon(n - 1)       # to'xtash sharti yo'q!
Natija
RecursionError: maximum recursion depth exceeded

Har bir rekursiv funksiya yozganingizda birinchi navbatda bazaviy holatni yozing.

Chaqiruvlar daraxti #

Rekursiyani tushunishning eng yaxshi usuli - chaqiruvlar daraxtini chizish:

faktorial(4) = 4 × 6 = 24 faktorial(3) = 3 × 2 = 6 faktorial(2) = 2 × 1 = 2 faktorial(1) = 1 ← bazaviy holat natijalar qaytadi Pastga tushish: masala kichrayadi Yuqoriga qaytish: natija yig'iladi Chuqurlik = 4, ya'ni chaqiruvlar stekida bir vaqtda 4 ta ramka turadi
Rekursiya avval eng chuqurga tushadi, keyin natijalarni yig'ib qaytadi

Jarayonni ko'rish uchun izohlar qo'shamiz:

Python
def faktorial_izohli(n, chuqurlik=0):
    chekinish = "  " * chuqurlik
    print(f"{chekinish}-> faktorial({n}) chaqirildi")

    if n <= 1:
        print(f"{chekinish}<- bazaviy holat, 1 qaytdi")
        return 1

    natija = n * faktorial_izohli(n - 1, chuqurlik + 1)
    print(f"{chekinish}<- {n} x ... = {natija} qaytdi")
    return natija


faktorial_izohli(4)
Natija
-> faktorial(4) chaqirildi
  -> faktorial(3) chaqirildi
    -> faktorial(2) chaqirildi
      -> faktorial(1) chaqirildi
      <- bazaviy holat, 1 qaytdi
    <- 2 x ... = 2 qaytdi
  <- 3 x ... = 6 qaytdi
<- 4 x ... = 24 qaytdi

Klassik rekursiv masalalar #

Python
def yigindi(royxat):
    """Ro'yxat yig'indisi."""
    if not royxat:
        return 0
    return royxat[0] + yigindi(royxat[1:])


def teskari(matn):
    """Matnni teskari o'girish."""
    if len(matn) <= 1:
        return matn
    return teskari(matn[1:]) + matn[0]


def palindrommi(matn):
    """Palindrom tekshiruvi."""
    if len(matn) <= 1:
        return True
    if matn[0] != matn[-1]:
        return False
    return palindrommi(matn[1:-1])


def darajaga(asos, koeffitsient):
    """Tez darajaga ko'tarish. O(log n)."""
    if koeffitsient == 0:
        return 1
    if koeffitsient % 2 == 0:
        yarim = darajaga(asos, koeffitsient // 2)
        return yarim * yarim              # ikki marta emas, bir marta hisoblaymiz
    return asos * darajaga(asos, koeffitsient - 1)


print(yigindi([1, 2, 3, 4, 5]))
print(teskari("dasturlash"))
print(palindrommi("shosh"))
print(darajaga(2, 10))
Natija
15
hsalrutsad
True
1024
Tez darajaga ko'tarish

darajaga funksiyasiga e'tibor bering: 2^10 ni hisoblash uchun 10 ta ko'paytirish emas, atigi 4 ta kerak bo'ldi (2^10 = (2^5)^2, 2^5 = 2 × 2^4 va hokazo). Bu O(log n) va kriptografiyada juda muhim - u yerda daraja ko'rsatkichi 2048 bit bo'lishi mumkin.

Rekursiyaning qimmatga tushishi #

Rekursiya chiroyli, lekin bepul emas:

Python
def fibonachchi_sekin(n):
    """Sodda rekursiv Fibonachchi. O(2^n) - juda sekin!"""
    if n < 2:
        return n
    return fibonachchi_sekin(n - 1) + fibonachchi_sekin(n - 2)
f(5) f(4) f(3) f(3) f(2) f(2) f(1) f(2) f(1) f(3) ikki marta hisoblanmoqda f(2) uch marta hisoblanmoqda
Bir xil qiymatlar qayta-qayta hisoblanadi - shundan eksponensial sekinlik kelib chiqadi
Python
import time

for n in [20, 25, 30, 32]:
    boshlanish = time.perf_counter()
    natija = fibonachchi_sekin(n)
    davomiylik = time.perf_counter() - boshlanish
    print(f"fib({n}) = {natija:>8,}   {davomiylik:>7.3f} soniya")
Natija
fib(20) =      6,765     0.001 soniya
fib(25) =     75,025     0.014 soniya
fib(30) =    832,040     0.158 soniya
fib(32) =  2,178,309     0.412 soniya

Har 5 qadamda vaqt taxminan 11 barobar oshmoqda. fib(50) ni hisoblash bir necha kun davom etardi.

Memoizatsiya - yechim #

Hisoblangan natijalarni eslab qolamiz:

Python
def fibonachchi_kesh(n, kesh=None):
    """Memoizatsiya bilan. O(n)."""
    if kesh is None:
        kesh = {}
    if n in kesh:
        return kesh[n]
    if n < 2:
        return n

    kesh[n] = fibonachchi_kesh(n - 1, kesh) + fibonachchi_kesh(n - 2, kesh)
    return kesh[n]


# Yoki tayyor dekorator bilan
from functools import lru_cache


@lru_cache(maxsize=None)
def fibonachchi_tez(n):
    if n < 2:
        return n
    return fibonachchi_tez(n - 1) + fibonachchi_tez(n - 2)


import time

boshlanish = time.perf_counter()
natija = fibonachchi_tez(100)
print(f"fib(100) = {natija:,}")
print(f"Vaqt: {time.perf_counter() - boshlanish:.6f} soniya")
print(fibonachchi_tez.cache_info())
Natija
fib(100) = 354,224,848,179,261,915,075
Vaqt: 0.000068 soniya
CacheInfo(hits=98, misses=101, maxsize=None, currsize=101)

fib(32) uchun 0.4 soniya kerak edi; fib(100) esa 0.00007 soniyada hisoblandi.

Rekursiya + memoizatsiya = dinamik dasturlash

Bu aynan keyingi bo'limning mavzusi. Memoizatsiya - dinamik dasturlashning "yuqoridan pastga" (top-down) ko'rinishi.

Rekursiya yoki sikl? #

Har qanday rekursiyani sikl bilan yozish mumkin:

Python
def fibonachchi_sikl(n):
    """Iterativ variant. O(n) vaqt, O(1) xotira."""
    if n < 2:
        return n

    oldingi, joriy = 0, 1
    for _ in range(2, n + 1):
        oldingi, joriy = joriy, oldingi + joriy
    return joriy


print(fibonachchi_sikl(100))
Natija
354224848179261915075
MezonRekursiyaSikl
O'qish osonligiBa'zan yaxshiroqBa'zan yaxshiroq
XotiraO(chuqurlik) stekO(1)
TezlikSekinroq (chaqiruv narxi)Tezroq
Chuqurlik chekloviBor (~1000)Yo'q
Daraxt/graf uchunTabiiyNoqulay
Qachon rekursiya tanlanadi?

Ma'lumot tuzilmasining o'zi rekursiv bo'lganda: daraxtlar, graflar, fayl tizimi, ichma-ich JSON. Bunday hollarda rekursiv kod sikl bilan yozilganidan ancha tushunarli bo'ladi.

Oddiy chiziqli hisoblashlar uchun sikl afzal.

Bo'l va hukmronlik qil #

Bu rekursiyaga asoslangan kuchli yondashuv. Uchta qadam:

  1. Bo'l - masalani kichikroq mustaqil bo'laklarga ajrat
  2. Hukmronlik qil - har bir bo'lakni rekursiv yech
  3. Birlashtir - natijalarni yig'

Merge sort va quick sort - shu yondashuvning misollari. Yana bir misol:

Python
def eng_katta_va_kichik(massiv, chap=0, ong=None):
    """Bir o'tishda eng katta va eng kichikni topadi."""
    if ong is None:
        ong = len(massiv) - 1

    if chap == ong:                              # bitta element
        return massiv[chap], massiv[chap]

    if ong - chap == 1:                          # ikkita element
        if massiv[chap] < massiv[ong]:
            return massiv[chap], massiv[ong]
        return massiv[ong], massiv[chap]

    orta = (chap + ong) // 2
    chap_kichik, chap_katta = eng_katta_va_kichik(massiv, chap, orta)
    ong_kichik, ong_katta = eng_katta_va_kichik(massiv, orta + 1, ong)

    return min(chap_kichik, ong_kichik), max(chap_katta, ong_katta)


sonlar = [23, 5, 78, 12, 99, 3, 45, 67]
kichik, katta = eng_katta_va_kichik(sonlar)
print(f"Eng kichik: {kichik}, eng katta: {katta}")
Natija
Eng kichik: 3, eng katta: 99

Master teorema #

Bo'l va hukmronlik qil algoritmlarining murakkabligini tez baholash uchun:

T(n) = a · T(n/b) + O(n^d) bo'lsa:

ShartMurakkablikMisol
d > log_b(a)O(n^d)Ikkilik qidiruv daraxtida qidiruv
d = log_b(a)O(n^d · log n)Merge sort (a=2, b=2, d=1)
d < log_b(a)O(n^(log_b a))Karatsuba ko'paytirish

Merge sort uchun: har chaqiruvda 2 ta kichik masala (a=2), har biri yarim hajmda (b=2), birlashtirish O(n) (d=1). log₂(2) = 1 = dO(n log n).

Amaliy misol: Xanoy minoralari #

Rekursiyaning eng mashhur namoyishi:

Python
def xanoy(disklar, manba="A", manzil="C", yordamchi="B", qadamlar=None):
    """Xanoy minoralari masalasi. 2^n - 1 ta qadam talab qiladi."""
    if qadamlar is None:
        qadamlar = []

    if disklar == 1:
        qadamlar.append(f"{disklar}-diskni {manba} dan {manzil} ga")
        return qadamlar

    # 1. Yuqoridagi n-1 ta diskni yordamchi ustunga
    xanoy(disklar - 1, manba, yordamchi, manzil, qadamlar)
    # 2. Eng kattasini manzilga
    qadamlar.append(f"{disklar}-diskni {manba} dan {manzil} ga")
    # 3. n-1 ta diskni yordamchidan manzilga
    xanoy(disklar - 1, yordamchi, manzil, manba, qadamlar)

    return qadamlar


qadamlar = xanoy(3)
for raqam, qadam in enumerate(qadamlar, start=1):
    print(f"{raqam}. {qadam}")
print(f"\nJami: {len(qadamlar)} ta qadam (2^3 - 1 = 7)")
Natija
1. 1-diskni A dan C ga
2. 2-diskni A dan B ga
3. 1-diskni C dan B ga
4. 3-diskni A dan C ga
5. 1-diskni B dan A ga
6. 2-diskni B dan C ga
7. 1-diskni A dan C ga

Jami: 7 ta qadam (2^3 - 1 = 7)
Afsona

Rivoyatga ko'ra, Hindiston ibodatxonasida rohiblar 64 ta diskni ko'chirmoqda. Ish tugagach dunyo tugaydi. Xavotir olmang: 2^64 - 1 qadam, sekundiga bitta qadam bilan 584 milliard yil kerak bo'ladi - bu Quyosh yoshidan 40 barobar ko'p.

Rekursiya bilan fayl tizimini aylanish #

Amaliy va tabiiy rekursiv masala:

Python
from pathlib import Path


def papka_hajmi(yol, chuqurlik=0, korsatilsin=True):
    """Papka hajmini rekursiv hisoblaydi."""
    yol = Path(yol)

    if yol.is_file():
        return yol.stat().st_size

    jami = 0
    try:
        for element in sorted(yol.iterdir()):
            hajm = papka_hajmi(element, chuqurlik + 1, korsatilsin)
            jami += hajm

            if korsatilsin and chuqurlik < 2:
                chekinish = "  " * chuqurlik
                belgi = "[D]" if element.is_dir() else "   "
                print(f"{chekinish}{belgi} {element.name:<28}{hajm / 1024:>10,.1f} KB")
    except PermissionError:
        pass

    return jami


jami = papka_hajmi("app")
print(f"\nJami hajm: {jami / 1024:,.1f} KB")
Natija
[D] Controllers                        24.3 KB
[D] Core                               18.7 KB
[D] Models                             21.2 KB
[D] Support                            19.8 KB
[D] Views                              96.4 KB
    helpers.php                         4.1 KB

Jami hajm: 184.5 KB
Amaliy topshiriq
  1. Rekursiv ackermann(m, n) funksiyasini yozing va ackermann(3, 3) ni hisoblang. Nima uchun ackermann(4, 2) ishlamaydi?
  2. Ichma-ich ro'yxatni yassilashtiruvchi rekursiv funksiya yozing: [1, [2, [3, [4]]]][1,2,3,4].
  3. Berilgan to'plamning barcha qism to'plamlarini rekursiv hosil qiling.
  4. xanoy funksiyasini o'zgartiring - u har bir qadamdan keyin ustunlar holatini chizsin.

Xulosa #

  • Har bir rekursiv funksiyada bazaviy holat va rekursiv holat bo'lishi shart.
  • Chaqiruvlar daraxtini chizish rekursiyani tushunishning eng yaxshi usuli.
  • Sodda rekursiv Fibonachchi O(2ⁿ) - takroriy hisoblashlar sababli.
  • Memoizatsiya (@lru_cache) uni O(n) ga tushiradi.
  • Rekursiya daraxt va graflar uchun tabiiy, chiziqli hisoblash uchun sikl afzal.
  • Bo'l va hukmronlik qil: bo'l → yech → birlashtir. Merge sort, quick sort, ikkilik qidiruv shu asosda.

Keyingi bo'limda memoizatsiya g'oyasini rivojlantirib, dinamik dasturlashni o'rganamiz.

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.