16-bo‘lim
Rekursiya va "bo'l va hukmronlik qil"
Rekursiya anatomiyasi, bazaviy holat, chaqiruvlar daraxti, memoizatsiya va bo'l va hukmronlik qil yondashuvi.
Ushbu bo‘lim mundarijasi
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:
| Qism | Vazifasi |
|---|---|
| Bazaviy holat | Rekursiyani to'xtatadi |
| Rekursiv holat | Masalani kichraytirib, o'zini chaqiradi |
def faktorial(n):
if n <= 1: # bazaviy holat
return 1
return n * faktorial(n - 1) # rekursiv holat
print(faktorial(5))
120
def yomon(n):
return n * yomon(n - 1) # to'xtash sharti yo'q!
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:
Jarayonni ko'rish uchun izohlar qo'shamiz:
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)
-> 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 #
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))
15
hsalrutsad
True
1024
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:
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)
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")
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:
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())
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.
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:
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))
354224848179261915075
| Mezon | Rekursiya | Sikl |
|---|---|---|
| O'qish osonligi | Ba'zan yaxshiroq | Ba'zan yaxshiroq |
| Xotira | O(chuqurlik) stek | O(1) |
| Tezlik | Sekinroq (chaqiruv narxi) | Tezroq |
| Chuqurlik cheklovi | Bor (~1000) | Yo'q |
| Daraxt/graf uchun | Tabiiy | Noqulay |
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:
- Bo'l - masalani kichikroq mustaqil bo'laklarga ajrat
- Hukmronlik qil - har bir bo'lakni rekursiv yech
- Birlashtir - natijalarni yig'
Merge sort va quick sort - shu yondashuvning misollari. Yana bir misol:
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}")
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:
| Shart | Murakkablik | Misol |
|---|---|---|
| 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 = d → O(n log n).
Amaliy misol: Xanoy minoralari #
Rekursiyaning eng mashhur namoyishi:
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)")
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)
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:
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")
[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
- Rekursiv
ackermann(m, n)funksiyasini yozing vaackermann(3, 3)ni hisoblang. Nima uchunackermann(4, 2)ishlamaydi? - Ichma-ich ro'yxatni yassilashtiruvchi rekursiv funksiya yozing:
[1, [2, [3, [4]]]]→[1,2,3,4]. - Berilgan to'plamning barcha qism to'plamlarini rekursiv hosil qiling.
xanoyfunksiyasini 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.
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.