Rekursiya
Tasavvur qiling, katta papkaning ichida fayllar va yana boshqa papkalar bor. Ichki papkalarning ichida ham fayl hamda papkalar bo‘lishi mumkin. Barcha fayllarni sanash uchun har bir papka darajasi uchun alohida algoritm yozish noqulay: tashqi papkani tekshirish, uning ichki papkasini tekshirish va undan ham ichkaridagi papkani tekshirish aslida bir xil ish.
Bu muammoni quyidagicha ifodalash tabiiy:
- joriy papkadagi fayllarni sanash;
- har bir ichki papka uchun ayni shu jarayonni takrorlash;
- ichki papka qolmaganda natijani qaytarish.
Algoritmning kichikroq ko‘rinishi yana o‘sha algoritm bilan yechilyapti. Rekursiya (recursion) — funksiya muammoning kichikroq yoki soddaroq nusxasini yechish uchun o‘zini bevosita yoki bilvosita chaqiradigan yondashuvdir.
FUNCTION COUNT_FILES(folder)
total = folder ichidagi oddiy fayllar soni
FOR har bir subfolder folder ichida
total = total + COUNT_FILES(subfolder)
RETURN total
Rekursiya “funksiya o‘zini chaqiradi” degan ta’rifdan kattaroq tushuncha. To‘g‘ri rekursiv yechimda:
- muammo bir xil shakldagi kichikroq qismlarga ajraladi;
- eng sodda holat qo‘shimcha chaqiruvsiz yechiladi;
- har bir chaqiruv shu sodda holatga yaqinlashadi;
- kichik qismlarning javoblari birlashib, dastlabki javobni hosil qiladi.
Shu shartlardan biri buzilsa, funksiya cheksiz chaqirilishi, noto‘g‘ri natija qaytarishi yoki xotiradagi chaqiruvlar steki to‘lib qolishi mumkin.
Ichma-ich qutilar analogiyasi
Bir-birining ichiga joylashtirilgan qutilarni ochayotganingizni tasavvur qiling. Har qutida yoki kichikroq quti, yoki izlanayotgan buyum bor. Jarayon quyidagicha:
katta qutini och
o‘rta qutini och
kichik qutini och
buyumni top
kichik qutini yop
o‘rta qutini yop
katta qutini yop
Ichkariga kirishda har bir qutini qayerdan ochganingizni eslab qolish kerak. Eng ichki qutiga yetgach, tashqariga teskari tartibda qaytasiz. Rekursiv funksiya ham xuddi shunday ishlaydi: yangi chaqiruv boshlanganda oldingi chaqiruv vaqtincha kutadi; eng sodda holat natija qaytargach, kutib turgan chaqiruvlar oxirgisidan boshlab davom etadi.
Bu analogiyada buyum topilgan eng ichki quti base case, ya’ni tayanch holatdir. Qutilarni ichkariga qarab ochish rekursiv tushish, ularni teskari tartibda yopish esa natijalarni qaytarish bosqichidir.
Asosiy terminlar
Rekursiv funksiya
O‘z bajarilishi davomida o‘zining yangi chaqiruvini boshlaydigan funksiya rekursiv funksiya deyiladi. Yangi chaqiruv avvalgisining aynan o‘zi emas: uning parametr qiymatlari va lokal o‘zgaruvchilari alohida bo‘ladi.
Tayanch holat
Tayanch holat (base case) — javobi qo‘shimcha rekursiv chaqiruvsiz ma’lum bo‘lgan eng sodda holat. U rekursiyaning to‘xtash nuqtasidir.
Masalan, n dan 1 gacha bo‘lgan sonlar yig‘indisini hisoblashda n = 0 bo‘lsa, natija 0:
Rekursiv holat
Rekursiv holat (recursive case) — javob kichikroq muammoning javobi orqali ifodalanadigan holat:
Bu yerda muammo n dan n - 1 ga kichrayadi. Faqat funksiya o‘zini chaqirishi yetarli emas; argument tayanch
holatga tomon o‘zgarishi kerak.
Rekursiya chuqurligi
Bir vaqtda yakunlanmay turib qolgan rekursiv chaqiruvlar soni rekursiya chuqurligi (recursion depth) deyiladi.
SUM(4) chaqiruvi SUM(3), SUM(2), SUM(1) va SUM(0)gacha tushsa, bir paytda bir nechta chaqiruv xotirada
turadi.
Call stack va stack frame
Call stack — bajarilayotgan funksiyalar qayerga qaytishi va ularning vaqtinchalik holatini saqlaydigan xotira tuzilmasi. Har bir funksiya chaqiruvi uchun unda stack frame, ya’ni chaqiruv freymi yaratiladi. Freym odatda quyidagilarning bir qismini saqlaydi:
- parametr qiymatlari;
- lokal o‘zgaruvchilar;
- funksiya tugagach bajarilish qayerdan davom etishi;
- hisoblash uchun zarur boshqa xizmat ma’lumotlari.
Aniq tarkib dasturlash tili, kompilyator va ishlash muhitiga bog‘liq. Fundamental g‘oya shuki, har chaqiruvning holati boshqalaridan alohida saqlanadi.
Note
Stack ma’lumotlar tuzilmasi umumiy LIFO modelidir: oxirgi qo‘shilgan element birinchi olinadi. Call stack esa dastur funksiyalar chaqiruvini boshqarish uchun ishlatadigan aniq stekdir. Ular bog‘liq, ammo bir xil atama emas.
Oddiy misol: yig‘indini rekursiv hisoblash
1 dan n gacha bo‘lgan sonlar yig‘indisini ko‘ramiz. Iterativ yechim takrorlash orqali yig‘ishi mumkin.
Rekursiv fikrlashda esa:
Qavs ichidagi qism ayni muammoning kichikroq nusxasi:
Bu algoritm n manfiy bo‘lmagan butun son deb qabul qiladi. Kirish talabi tekshirilmasa, manfiy n qiymati
0ga yaqinlashmay, undan uzoqlashishi mumkin.
Chaqiruvlar pastga tushishi
SUM(4) bajarilganda javob darhol tayyor bo‘lmaydi:
SUM(0) tayanch holatga yetadi va 0 qaytaradi. Shu paytgacha oldingi chaqiruvlarning har biri natijani kutib
turadi.
Natijalar yuqoriga qaytishi
Endi hisoblash teskari tartibda yakunlanadi:
Rekursiyaning ikki bosqichini ajratish muhim:
- tushish — muammo kichrayib, yangi chaqiruvlar yaratiladi;
- qaytish — tayanch holatdan olingan javoblar kutib turgan chaqiruvlarda ishlatiladi.
Boshlovchilar ko‘pincha faqat pastga tushishni ko‘radi va RETURNdan keyingi qiymatlar qanday yig‘ilishini
tasavvur qilishda qiynaladi. Dry runni ikki bosqichda yozish bu chalkashlikni kamaytiradi.
Call stack ichida nima sodir bo‘ladi?
SUM(3) misolida har chaqiruv uchun soddalashtirilgan freym yaratiladi:
1. SUM(3) chaqirildi
Call stack:
+---------------------------+
| SUM(3): n=3, SUM(2) kutadi|
+---------------------------+
SUM(3) yakunlanishidan oldin SUM(2)ni chaqiradi:
2. SUM(2) chaqirildi
Call stack:
+---------------------------+
| SUM(2): n=2, SUM(1) kutadi| ← eng yuqori freym
+---------------------------+
| SUM(3): n=3, SUM(2) kutadi|
+---------------------------+
Keyin SUM(1) va SUM(0) qo‘shiladi:
+---------------------------+
| SUM(0): n=0 | ← birinchi yakunlanadi
+---------------------------+
| SUM(1): n=1, SUM(0) kutadi|
+---------------------------+
| SUM(2): n=2, SUM(1) kutadi|
+---------------------------+
| SUM(3): n=3, SUM(2) kutadi|
+---------------------------+
Stack LIFO — “oxirgi kirgan, birinchi chiqadi” tartibida ishlaydi. Shu sababli avval SUM(0), keyin SUM(1),
SUM(2) va oxirida SUM(3) tugaydi.
Har bir chaqiruvdagi n alohida. Ichki chaqiruv nni o‘zgartirishi tashqi chaqiruvning freymidagi nni
almashtirmaydi. Funksiya qanday parametr uzatishi va obyektlar reference orqali boshqarilishi tilga bog‘liq nozik
tafsilotlarga ega, lekin alohida chaqiruv freymlari g‘oyasi saqlanadi.
Rekursiya nima sababdan kerak?
Har qanday rekursiv algoritmni “rekursiya chiroyli” degani uchun ishlatish kerak emas. Rekursiya muammoning o‘zi rekursiv tuzilishga ega bo‘lganda tabiiy ifoda beradi.
Masalan, daraxtning har bir node’i qiymat va yana kichik daraxtlarga olib boruvchi child reference’larini saqlaydi. “Daraxtni ko‘rib chiqish” quyidagicha ta’riflanadi:
Papka ichidagi papkalar, tashkilot bo‘limlari, HTML elementlari, arifmetik ifoda qismlari va divide-and-conquer algoritmlarida ham butun tuzilma o‘ziga o‘xshash kichik qismlardan iborat. Rekursiv funksiya shu tuzilmani kodda bevosita aks ettiradi.
Rekursiyaning kuchi — har bir darajada faqat joriy kichik muammo haqida fikrlash imkonidir. Funksiya kichikroq muammoni to‘g‘ri yechadi deb qabul qilib, joriy natijani undan qanday qurishni aniqlaymiz. Call stack qayerdan qaytish kerakligini o‘zi saqlaydi.
To‘g‘ri rekursiv yechimning uchta sharti
1. Tayanch holat mavjud bo‘lishi kerak
Tayanch holatsiz har bir chaqiruv yana yangisini yaratadi:
Bu funksiya qachon to‘xtashini bilmaydi.
2. Har bir qadam tayanch holatga yaqinlashishi kerak
Shartning mavjudligi yetarli emas:
Musbat n uchun qiymat 0dan tobora uzoqlashadi. Tayanch holat bor, lekin unga yetib bo‘lmaydi.
3. Kichik javobdan joriy javob to‘g‘ri qurilishi kerak
SUM(n) uchun kichik javob SUM(n - 1). Joriy n ham qo‘shilmasa, funksiya to‘xtashi mumkin, ammo natija
noto‘g‘ri bo‘ladi:
To‘xtash va to‘g‘rilik alohida talablar: algoritm tugashi uning to‘g‘ri javob berishini kafolatlamaydi.
Rekursiv algoritmni qanday loyihalash kerak?
Rekursiv yechim tuzishda quyidagi savollar yordam beradi:
- Eng sodda, javobi darhol ma’lum kirish qaysi?
- Katta muammoni ayni shakldagi qaysi kichik muammo orqali ifodalash mumkin?
- Har bir chaqiruv muammo o‘lchamini haqiqatan kamaytiradimi?
- Kichik muammoning javobidan joriy javob qanday olinadi?
- Maksimal rekursiya chuqurligi qancha bo‘lishi mumkin?
- Bir xil kichik muammolar takroran hisoblanadimi?
Masalan, linked listdagi node’lar sonini hisoblash:
- bo‘sh ro‘yxatning uzunligi
0; - bo‘sh bo‘lmagan ro‘yxatning uzunligi
1 + qolgan ro‘yxat uzunligi; - har chaqiruv
nextga o‘tadi, demak oxirgiNULLga yaqinlashadi.
Bu ta’rif matematik va kod nuqtayi nazaridan sodda. Ammo juda uzun linked listda iterativ traversal call stackni ishlatmagani uchun amaliyroq bo‘lishi mumkin.
Rekursiya turlari
Turlarni yodlashdan ko‘ra, har chaqiruv qayerda va nechta yangi chaqiruv yaratishini tushunish muhim.
Bevosita rekursiya
Funksiya o‘zini to‘g‘ridan-to‘g‘ri chaqirsa, bu bevosita rekursiya (direct recursion):
Fayl tizimi, linked list va daraxt traversalining sodda variantlari ko‘pincha shu ko‘rinishda yoziladi.
Bilvosita rekursiya
Bir funksiya ikkinchisini, ikkinchisi esa yana birinchisini chaqirsa, bilvosita rekursiya (indirect recursion) yuz beradi:
FUNCTION IS_EVEN(n)
IF n = 0
RETURN TRUE
RETURN IS_ODD(n - 1)
FUNCTION IS_ODD(n)
IF n = 0
RETURN FALSE
RETURN IS_EVEN(n - 1)
Bu ham call stackdan foydalanadi va umumiy chaqiruvlar zanjiri tayanch holatga yaqinlashishi kerak. Bilvosita rekursiyada cheksiz siklni ko‘rish qiyinroq, chunki o‘zini chaqirish bitta funksiya ichida ko‘rinmaydi.
Chiziqli rekursiya
Har bir chaqiruv ko‘pi bilan bitta keyingi rekursiv chaqiruv yaratsa, chiziqli rekursiya (linear recursion)
deyiladi. SUM(n), countdown va linked list traversal shunday ishlaydi. Chaqiruvlar soni odatda kirish hajmiga
mutanosib.
Daraxtsimon rekursiya
Har chaqiruv bir nechta rekursiv chaqiruv yaratsa, bajarilish rekursiya daraxti (recursion tree) hosil qiladi:
FIB(5) uchun chaqiruvlarning bir qismi:
Bu yerda FIB(3) va FIB(2) kabi bir xil kichik muammolar qayta-qayta hisoblanadi. Rekursiv yozuv qisqa bo‘lsa
ham, sodda variantning vaqt murakkabligi eksponentsial darajada o‘sadi. Memoization bu takroriy hisoblarni
kamaytirishi mumkin.
Tail recursion
Rekursiv chaqiruv funksiyaning eng oxirgi amali bo‘lsa va undan qaytgach boshqa hisob qolmasa, bu tail recursion, ya’ni oxirgi amal rekursiyasi deyiladi:
FUNCTION SUM_TAIL(n, accumulator)
IF n = 0
RETURN accumulator
RETURN SUM_TAIL(n - 1, accumulator + n)
accumulator oraliq natijani olib yuradi. SUM(n)ning oldingi variantida esa ichki chaqiruv qaytgach n + ...
amali bajarilishi kerak edi, shuning uchun u tail recursion emas.
Ayrim kompilyator yoki ishlash muhitlari tail call optimization orqali eski freymni qayta ishlatib, stack
chuqurligini oshirmasligi mumkin. Lekin barcha dasturlash tillari va barcha holatlarda bu optimizatsiya bor deb
bo‘lmaydi. Kafolat bo‘lmasa, tail recursion ham O(n) stack xotirasi va stack overflow xavfiga ega deb qaraladi.
Daraxt bo‘ylab rekursiv yurish
Rekursiya daraxtlar uchun ayniqsa tabiiy. Binary tree node’ida chap va o‘ng child bo‘lishi mumkin. Node’lar sonini hisoblash:
FUNCTION COUNT_NODES(node)
IF node = NULL
RETURN 0
left_count = COUNT_NODES(node.left)
right_count = COUNT_NODES(node.right)
RETURN 1 + left_count + right_count
Masalan:
Dry running mantiqiy ko‘rinishi:
COUNT_NODES(A)
= 1 + COUNT_NODES(B) + COUNT_NODES(C)
COUNT_NODES(B)
= 1 + COUNT_NODES(D) + COUNT_NODES(E)
Har bir NULL child
= 0
Natija
= A, B, C, D, E
= 5 ta node
Har bir haqiqiy node bir marta ko‘riladi, shuning uchun vaqt O(n). Stack chuqurligi daraxt balandligi hga
teng, yordamchi xotira O(h). Muvozanatlangan daraxtda h taxminan log n, bir tomonga cho‘zilgan daraxtda esa
h = n bo‘lishi mumkin. Demak, bir xil O(n) vaqtli traversalning stack xotirasi daraxt shakliga qarab farq
qiladi.
Divide and conquer
Bo‘lib yechish (divide and conquer) — muammoni kichikroq mustaqil qismlarga ajratish, har birini yechish va natijalarni birlashtirish yondashuvi. Rekursiya uni ifodalashning tabiiy usuli, ammo bu ikki tushuncha aynan bir narsa emas: rekursiya boshqaruv usuli, divide and conquer esa algoritm loyihalash strategiyasidir.
Binary search saralangan arrayning kerakli yarmini tanlab, qolgan yarmini tashlab yuboradi:
FUNCTION BINARY_SEARCH(array, target, left, right)
IF left > right
RETURN TOPILMADI
middle = left + FLOOR((right - left) / 2)
IF array[middle] = target
RETURN middle
IF target < array[middle]
RETURN BINARY_SEARCH(array, target, left, middle - 1)
RETURN BINARY_SEARCH(array, target, middle + 1, right)
Har chaqiruvda qidiriladigan oraliq taxminan yarmiga kamayadi. Shu sababli vaqt O(log n), rekursiv variantning
stack xotirasi ham O(log n). Iterativ binary search xuddi shu vaqt bilan O(1) yordamchi xotira ishlata oladi.
Warning
Binary search faqat qidiruv mezoni bo‘yicha saralangan va o‘rtadagi elementga samarali kirish mumkin bo‘lgan ma’lumotda shu afzallikni beradi. Oddiy linked listda o‘rtadagi node’ga kirishning o‘zi qimmat.
Merge sort ham arrayni qismlarga rekursiv ajratadi, qismlarni saralaydi va ularni birlashtiradi. Bu yerda bir
darajadagi umumiy birlashtirish ishi O(n), darajalar soni O(log n) bo‘lgani uchun umumiy vaqt O(n log n)
bo‘ladi. Rekursiya daraxti vaqt murakkabligini ko‘rishga yordam beradi.
Backtracking
Orqaga qaytib qidirish (backtracking) mumkin bo‘lgan tanlovlardan birini sinaydi, undan kelib chiqadigan holatlarni rekursiv tekshiradi va yo‘l yaramasa oldingi holatga qaytib boshqa tanlovni sinaydi.
Labirintda yo‘l qidirishning soddalashtirilgan g‘oyasi:
FUNCTION FIND_PATH(cell)
IF cell maqsad bo‘lsa
RETURN TRUE
cell ni tashrif buyurilgan deb belgilash
FOR har bir ruxsat etilgan qo‘shni cell ichida
IF qo‘shni hali tashrif buyurilmagan bo‘lsa
IF FIND_PATH(qo‘shni) = TRUE
RETURN TRUE
joriy tanlovdan qaytish
RETURN FALSE
Rekursiya bu yerda tanlovlar zanjirini call stackda saqlaydi. Sudoku, kombinatsiyalar, permutatsiyalar va yo‘l qidirish masalalarida shu model uchraydi. Mumkin bo‘lgan variantlar soni juda katta bo‘lishi mumkin, shuning uchun noto‘g‘ri yo‘llarni erta to‘xtatish — pruning — muhim.
Memoization va takroriy kichik muammolar
Rekursiya o‘zi avtomatik ravishda samarali emas. Agar bir xil parametrli chaqiruv ko‘p marta bajarilsa, natijani saqlab, keyingi safar qayta ishlatish mumkin. Bu memoization deyiladi.
Sodda Fibonacci:
Memoization bilan:
FUNCTION FIB(n, memo)
IF n <= 1
RETURN n
IF memo ichida n mavjud bo‘lsa
RETURN memo[n]
memo[n] = FIB(n - 1, memo) + FIB(n - 2, memo)
RETURN memo[n]
Har bir 0..n holat bir marta to‘liq hisoblanadi. Vaqt eksponentsial o‘sishdan O(n)ga tushadi. Buning evaziga
memo jadvali O(n) xotira oladi; call stack ham O(n) chuqurlikka yetishi mumkin.
Memoization rekursiyaning turi emas, takroriy hisobni cache orqali kamaytirish usulidir. U faqat bir xil kichik muammolar qayta uchraganda foyda beradi. Har holat noyob bo‘lsa, memo jadvali ortiqcha xotira sarflashi mumkin.
Vaqt murakkabligini qanday hisoblash kerak?
Rekursiv algoritm murakkabligini faqat kodda bitta yoki ikkita rekursiv chaqiruv borligiga qarab aytib bo‘lmaydi. Quyidagilarni aniqlash kerak:
- har chaqiruv nechta yangi chaqiruv yaratadi;
- muammo o‘lchami har safar qanchaga kamayadi;
- rekursiv chaqiruvlardan tashqari har darajada qancha ish bajariladi;
- bir xil kichik muammolar qayta hisoblanadimi.
Bitta chaqiruv va n - 1
SUM(n) har darajada bitta chaqiruv va doimiy ish bajaradi. n taxminan bir birlikdan kamayib 0ga yetadi,
demak n ta daraja bor. Umumiy vaqt O(n).
Bitta chaqiruv va yarmiga kamayish
Binary searchda maydon har safar yarmiga kamayadi. n ni necha marta ikkiga bo‘lganda 1 qolsa, shuncha daraja
hosil bo‘ladi — O(log n).
Ikkita chaqiruv va takroriy ish
Sodda Fibonacci taxminan quyidagi o‘sishga ega:
Chaqiruvlar daraxt shaklida ko‘payadi va bir xil holatlar takrorlanadi. Vaqt eksponentsial, odatda yuqori chegara
sifatida O(2^n) deb sodda ifodalanadi. Bu aniq 2^n ta amal bajariladi degani emas; o‘sishning juda tezligini
ko‘rsatadigan yuqori bahodir.
Har darajada chiziqli ish
Merge sortda ikki yarim muammo yechiladi va natijalarni birlashtirish O(n) ish talab qiladi:
Rekursiya daraxtining har darajasida jami O(n) ish, darajalar soni esa O(log n). Natija O(n log n).
Xotira murakkabligini qanday hisoblash kerak?
Rekursiyada vaqt davomida yaratilgan barcha chaqiruvlarni emas, bir vaqtda stackda turgan maksimal freymlar sonini hisoblash kerak.
Sodda Fibonacci juda ko‘p chaqiruv yaratadi, lekin ularning hammasi bir paytda stackda turmaydi. Bir tarmoq
yakunlangach, uning freymlari chiqariladi va keyingi tarmoq bajariladi. Maksimal chuqurlik O(n), shuning uchun
stack xotirasi O(n); vaqt esa eksponentsial bo‘lishi mumkin.
Umumiy chaqiruvlar soni → vaqtga ta’sir qiladi
Maksimal faol chaqiruvlar chuqurligi → stack xotirasiga ta’sir qiladi
Har bir freym ichida katta ma’lumot nusxalansa, faqat chuqurlikni aytish yetmaydi. Masalan, har darajada hajmi
nga bog‘liq yangi array yaratilsa, umumiy yordamchi xotira kattaroq bo‘ladi. Parametr sifatida obyekt reference’i
uzatiladimi yoki butun ma’lumot nusxalanadimi — bu til va implementatsiyaga bog‘liq.
Murakkablik jadvali
| Algoritm yoki naqsh | Vaqt | Stack xotirasi | Asosiy sabab |
|---|---|---|---|
SUM(n) |
O(n) |
O(n) |
n dan 0gacha bitta zanjir |
| Linked list traversal | O(n) |
O(n) |
Har node uchun bitta chaqiruv |
| Rekursiv binary search | O(log n) |
O(log n) |
Oraliq har safar yarmiga kamayadi |
| Muvozanatlangan tree traversal | O(n) |
O(log n) |
Barcha node ko‘riladi, chuqurlik kichik |
| Qiyshaygan tree traversal | O(n) |
O(n) |
Daraxt bitta uzun zanjirga o‘xshaydi |
| Sodda Fibonacci | O(2^n) yuqori baho |
O(n) |
Takroriy tarmoqlanuvchi chaqiruvlar |
| Memoizationli Fibonacci | O(n) |
O(n) |
Har holat bir marta hisoblanadi |
| Merge sort | O(n log n) |
Implementatsiyaga bog‘liq, odatda kamida O(log n) stack |
log n daraja, har darajada O(n) ish |
Jadval muayyan klassik implementatsiyalarni nazarda tutadi. Masalan, merge sortning birlashtirish uchun ishlatadigan
qo‘shimcha xotirasi ma’lumot turi va implementatsiyaga qarab O(n) bo‘lishi mumkin; jadvaldagi stack qiymati
butun yordamchi xotiraning hammasi emas.
Rekursiya va iteratsiya
Iteratsiya FOR yoki WHILE kabi takrorlash mexanizmi orqali ishni qayta bajaradi. Rekursiya esa yangi
funksiya chaqiruvlari orqali takrorlaydi. Ko‘p chiziqli rekursiv algoritmlarni siklga aylantirish mumkin.
SUM(n)ning iterativ ko‘rinishi:
Ikkala variant ham O(n) vaqt oladi. Rekursiv variant O(n) call stack xotirasi, iterativ variant esa O(1)
yordamchi xotira ishlatadi. Bu oddiy masalada iteratsiya amaliy jihatdan afzal.
Ammo daraxt traversalida iterativ variant stack ehtiyojini yo‘qotmaydi; call stack o‘rniga dasturchi boshqaradigan stack ma’lumotlar tuzilmasi kerak bo‘lishi mumkin:
Rekursiv traversal → holat call stackda saqlanadi
Iterativ traversal → holat explicit stackda saqlanadi
Shuning uchun “iteratsiya har doim O(1) xotira ishlatadi” degan umumlashtirish noto‘g‘ri. Muammoning keyin
qayerga qaytishni eslab qolish talabi bo‘lsa, bu holat qayerdadir saqlanishi kerak.
| Mezoni | Rekursiya | Iteratsiya |
|---|---|---|
| Boshqaruv | Funksiya chaqiruvlari | Sikl va aniq holat o‘zgaruvchilari |
| Xotira | Chuqurlikka qarab call stack | Oddiy siklda ko‘pincha kamroq; ba’zan explicit stack kerak |
| Kodning tabiiyligi | Daraxt va bo‘linuvchi muammolarda kuchli | Chiziqli takrorlashda odatda sodda |
| Chuqurlik xavfi | Stack overflow bo‘lishi mumkin | Oddiy siklda recursion depth cheklovi yo‘q |
| Funksiya chaqiruv xarajati | Har darajada mavjud | Ko‘pincha kamroq |
Tanlov “qaysi biri zamonaviyroq?” degan savolga emas, muammo shakli, maksimal chuqurlik, aniqlik va resurs cheklovlariga bog‘liq.
To‘g‘rilik mantig‘i
Rekursiv algoritmning to‘g‘riligini tushuntirish matematik induksiyaga o‘xshaydi, lekin uni sodda savollar bilan ko‘rish mumkin.
SUM(n) uchun:
- Tayanch holat to‘g‘rimi?
SUM(0) = 0— ha. - Kichik holat to‘g‘ri deb faraz qilsak nima bo‘ladi?
SUM(n - 1)1..n-1yig‘indisini to‘g‘ri qaytarsin. - Joriy holat undan to‘g‘ri quriladimi? Unga
nqo‘shilsa1..nyig‘indisi hosil bo‘ladi. - Jarayon tayanch holatga yetadimi? Manfiy bo‘lmagan
nhar safar bittadan kamayadi va0ga keladi.
Bu fikrlash usuli rekursiv funksiyani “ichki chaqiruvlarning hammasini miyada bir paytda bajarish” zaruratidan qutqaradi. Kichik chaqiruv o‘z shartnomasi bo‘yicha to‘g‘ri javob beradi deb qarab, joriy darajaga e’tibor qaratiladi.
Real qo‘llanishlar
Fayl tizimini ko‘rib chiqish
Papka ichida fayl va subfolderlar, subfolder ichida yana shu turdagi ma’lumot bor. Fayl qidirish, katalog hajmini hisoblash yoki daraxt ko‘rinishini chiqarishda funksiya joriy papkani ko‘radi va har bir subfolder uchun o‘zini chaqiradi. Tayanch holat — ichki papkasi yo‘q katalog yoki oddiy fayl.
Amaliy tizimda symbolic linklar sikl hosil qilishi, ruxsat xatolari yuz berishi yoki papka chuqurligi juda katta bo‘lishi mumkin. Shu sababli real dastur tashrif buyurilgan obyektlarni kuzatishi va xatolarni boshqarishi kerak.
HTML va interfeys daraxtlari
HTML elementlar boshqa elementlarni o‘z ichiga oladi. Sahifani ko‘rsatish, element qidirish yoki stilni avlodlarga tatbiq qilishda daraxt traversal ishlatiladi. Joriy element qayta ishlanadi, keyin child elementlar rekursiv ko‘riladi. Juda chuqur yoki tashqi manbadan kelgan tuzilmalarda iterativ traversal xavfsizroq bo‘lishi mumkin.
Parserlar va ifodalar
(2 + 3) * 4 kabi ifoda kichik ifodalardan tuziladi. Parser qavs ichidagi ifodani yana shu qoidalar bilan tahlil
qiladi. Hosil bo‘lgan expression tree’ni hisoblashda operator node’i chap va o‘ng kichik ifoda natijalarini
rekursiv oladi.
Qidiruv va o‘yin holatlari
Shaxmat yoki boshqa o‘yinda joriy yurishdan keyin bir nechta yangi holat paydo bo‘ladi. Algoritm ma’lum chuqurlikkacha mumkin bo‘lgan yurishlarni rekursiv tekshirishi mumkin. Holatlar soni juda tez ko‘paygani uchun chuqurlik chegarasi, pruning va natijalarni cachelash zarur bo‘ladi.
Saralash va qidirish
Merge sort, quicksort va binary search muammoni kichik qismlarga ajratish mantig‘ini rekursiv ifodalaydi. Bunda
rekursiya ma’lumotlar tuzilmasidan emas, algoritmning bo‘lib yechish tabiatidan kelib chiqadi. Quicksortda noto‘g‘ri
pivot tanlovi rekursiya chuqurligini O(n)gacha olib borishi mumkin; shu sababli o‘rtacha va eng yomon holatni
alohida baholash muhim.
Afzalliklari
- Rekursiv tuzilmani tabiiy ifodalaydi. Tree, papka va ichma-ich ifodalar kod modeliga bevosita mos tushadi.
- Murakkab boshqaruv holatini call stack saqlaydi. Qayerga qaytish kerakligini qo‘lda boshqarish kamayadi.
- Divide and conquer va backtrackingni tushunarli yozadi. Har daraja faqat joriy kichik muammoga e’tibor beradi.
- To‘g‘rilikni kichik holat orqali asoslash mumkin. Tayanch holat va rekursiv qadam aniq ajraladi.
Bu soddalik bepul emas. Call stack xotirasi, funksiya chaqiruv xarajati va chuqurlik chegarasi rekursiyaning asosiy narxidir.
Cheklovlari
- Har faol chaqiruv uchun stack frame kerak bo‘ladi.
- Chuqurlik katta bo‘lsa stack overflow yuz berishi mumkin.
- Oddiy chiziqli takrorlashda sikldan ko‘ra ko‘proq xotira va chaqiruv xarajati talab qilishi mumkin.
- Noto‘g‘ri tayanch holat yoki kamaymaydigan muammo cheksiz rekursiyaga olib keladi.
- Daraxtsimon rekursiyada bir xil kichik muammolar ko‘p marta hisoblanishi mumkin.
- Rekursiv bajarilish tartibini debuggerda kuzatish boshlovchi uchun qiyinroq bo‘lishi mumkin.
- Ba’zi tillarda ruxsat etilgan recursion depth nisbatan kichik va implementatsiyaga bog‘liq.
Keng tarqalgan xatolar va edge case’lar
Tayanch holatni unutish
Tayanch holatsiz chaqiruvlar davom etadi. Bu matematik ma’nodagi cheksiz jarayon amalda cheksiz ishlamaydi: call stack cheklangan, shuning uchun dastur odatda stack overflow yoki recursion depth xatosi bilan to‘xtaydi.
Tayanch holatga yetib bormaslik
n = 0 sharti borligi musbat, manfiy va noto‘g‘ri kirishlarning barchasi unga yetadi degani emas. Har bir ruxsat
etilgan kirish uchun o‘lchov belgilang: masalan, n, qolgan elementlar soni yoki qidiruv oralig‘i. Bu o‘lchov har
qadamda qat’iy kamayib, pastki chegaraga yetishi kerak.
Noto‘g‘ri chegara
n = 0 o‘rniga n = 1 tanlash ayrim formulalarda to‘g‘ri, boshqalarida bo‘sh kirishni hisobga olmaydi. Bo‘sh
array, bo‘sh string, NULL tree va bitta elementli tuzilmani alohida sinash kerak.
RETURNni unutish
Ichki chaqiruv natija qaytarsa ham, tashqi funksiya uni qaytarmasa yoki ishlatmasa javob yo‘qoladi:
Rekursiv chaqiruvdan kelgan qiymat joriy natijaga qanday qo‘shilishini aniq yozish kerak.
Lokal va umumiy holatni aralashtirish
Har chaqiruvning lokal o‘zgaruvchisi alohida, ammo umumiy mutable obyekt barcha chaqiruvlar orasida bo‘lishilishi mumkin. Backtrackingda tanlov qo‘shilib, qaytishda olib tashlanmasa, bir tarmoqdagi holat boshqasiga sizib o‘tadi.
Kirishni keraksiz nusxalash
Har chaqiruvda arrayning yangi bo‘lagini nusxalash kodni sodda ko‘rsatishi mumkin, ammo vaqt va xotira xarajatini oshiradi. Ko‘pincha boshlanish va tugash indexlarini uzatish yetarli. Nusxa olinishi yoki view/reference uzatilishi til va kutubxona implementatsiyasiga bog‘liq.
Eksponentsial rekursiyani chiziqli deb o‘ylash
Kodda n kamaygani uchun algoritm avtomatik O(n) bo‘lmaydi. Har chaqiruv ikkita yangi chaqiruv yaratsa,
chaqiruvlar soni daraxt bo‘ylab tez ko‘payishi mumkin. Recursion tree chizish va bir xil parametrlar takrorlanishini
tekshirish kerak.
Chuqurlikni faqat o‘rtacha holat bilan baholash
Muvozanatlangan tree O(log n) chuqurlikda bo‘lishi mumkin, ammo bir tomonga qiyshaygan tree O(n) chuqurlikka
ega. Tashqi foydalanuvchi yaratgan yoki nazorat qilinmaydigan ma’lumot uchun eng yomon holat stack overflowga sabab
bo‘lishi mumkin.
Siklli graphda tashriflarni kuzatmaslik
Tree’da har node’ga odatda bitta yo‘l bor. Graphda esa A → B → A kabi sikl bo‘lishi mumkin. Rekursiv DFS
visited to‘plamisiz bir node’lar orasida cheksiz aylanadi. Graph traversalida tayanch holatning bir qismi — node
avval ko‘rilganini tekshirish.
Natijani haddan tashqari katta hisoblash
Faktorial kabi rekursiv hisob to‘g‘ri tugasa ham, natija ma’lumot turiga sig‘masligi mumkin. Bu recursion xatosi emas, sonli overflow muammosi. Algoritmning kirish chegarasi va ishlatilayotgan son modelini alohida hisobga olish kerak.
Stack overflow nima?
Call stack uchun xotira cheklangan. Har rekursiv chaqiruv yangi freym qo‘shsa, juda katta chuqurlikda joy tugaydi. Bu stack overflow deyiladi.
Aniq chegarani universal son bilan aytib bo‘lmaydi. U til, runtime, operatsion tizim, freym hajmi va sozlamalarga bog‘liq. Muammoni hal qilish yo‘llari:
- chiziqli rekursiyani siklga aylantirish;
- tree yoki graph uchun explicit stack ishlatish;
- qayta ishlashni bo‘laklash;
- algoritm chuqurligini kamaytirish;
- faqat muhit kafolatlasa tail call optimizationga tayanish.
Stack limitini shunchaki kattalashtirish ba’zan vaqtinchalik yordam beradi, lekin cheklanmagan kirish uchun algoritmik xavfni yo‘qotmaydi.
Qachon rekursiya ishlatish kerak?
Rekursiya quyidagi vaziyatlarda kuchli tanlov:
- ma’lumot tree, graph yoki ichma-ich papkalar kabi rekursiv tuzilishga ega;
- muammo o‘ziga o‘xshash kichik qismlarga tabiiy ajraladi;
- divide and conquer algoritmi ishlatiladi;
- backtrackingda tanlovlar daraxti bo‘ylab yurish kerak;
- maksimal chuqurlik nazorat ostida va call stack uchun xavfsiz;
- rekursiv ifoda iterativ variantdan ancha tushunarli va xatoga kamroq moyil.
Rekursiyani tanlaganda faqat kod uzunligiga qaramang. Tayanch holat, progress o‘lchovi, maksimal chuqurlik va takroriy kichik muammolarni oldindan tekshiring.
Qachon rekursiya ishlatmaslik kerak?
Quyidagi holatlarda iteratsiya yoki explicit ma’lumotlar tuzilmasi yaxshiroq bo‘lishi mumkin:
- oddiy chiziqli takrorlash sikl bilan ravshan ifodalansa;
- kirish juda katta va rekursiya chuqurligi
O(n)bo‘lsa; - runtime recursion depth yoki stack xotirasini qattiq cheklasa;
- funksiya chaqiruvining qo‘shimcha xarajati kritik bo‘lsa;
- tail call optimization mavjudligi kafolatlanmasa;
- tashqi ma’lumot juda chuqur yoki ataylab zararli tuzilma yaratishi mumkin bo‘lsa;
- sodda rekursiya bir xil kichik muammolarni eksponentsial marta hisoblasa va memoization yoki dynamic programming qo‘llanmasa.
Iterativ yechim har doim tushunarliroq degani ham emas. Tree DFSni explicit stack bilan yozish call stack xavfini boshqaradi, ammo kodda stackni qo‘lda yuritish talab qilinadi. To‘g‘ri tanlov o‘qilish qulayligi va resurs xavfsizligi o‘rtasidagi muvozanatdir.
Rekursiv kodni tekshirish ro‘yxati
Rekursiv algoritm yozilgach, quyidagilarni tekshirish foydali:
- Bo‘sh yoki eng kichik kirish qanday natija qaytaradi?
- Tayanch holat rekursiv chaqiruvdan oldin tekshiriladimi?
- Har chaqiruvda qaysi miqdor kamayadi?
- Barcha ruxsat etilgan kirishlar tayanch holatga yetadimi?
- Ichki chaqiruv natijasi tashqi natijaga to‘g‘ri qo‘shiladimi?
- Maksimal chaqiruv chuqurligi qancha?
- Bir xil parametrli chaqiruvlar takrorlanadimi?
- Mutable holat qaytishda tiklanadimi?
- Graph bo‘lsa sikllar qanday aniqlanadi?
- Iterativ variant xavfsizroq yoki soddaroq emasmi?
Eng muhim xulosa
Rekursiya — muammoni o‘ziga o‘xshash kichikroq muammo orqali yechish usuli. Uning to‘g‘ri ishlashi uchun tayanch holat, unga tomon aniq progress va kichik javobdan joriy javobni to‘g‘ri qurish kerak.
Har rekursiv chaqiruv call stackda alohida freym yaratadi. Chaqiruvlar tayanch holatgacha pastga tushadi, keyin natijalar teskari tartibda yuqoriga qaytadi. Vaqt murakkabligi umumiy chaqiruvlar va har chaqiruvdagi ishga, xotira murakkabligi esa maksimal faol chuqurlik va har freymdagi ma’lumotga bog‘liq.
Rekursiya tree, graph, divide and conquer hamda backtrackingda muammoning tuzilishini juda tabiiy ifodalaydi. Oddiy chiziqli takrorlashda esa sikl ko‘pincha kamroq xotira va kichikroq amaliy xarajat bilan ishlaydi. Rekursiya maqsad emas — muammoning shakliga mos boshqaruv vositasidir.
Rekursiyani tushungach, tree va graph traversal, merge sort, quicksort, dynamic programming hamda backtracking kabi mavzulardagi algoritmlarning ichki mantiqini ko‘rish osonlashadi. Keyingi bosqichda call stack g‘oyasini umumiy stack ma’lumotlar tuzilmasi bilan solishtirish foydali: rekursiyada runtime yashirin boshqargan holatni iterativ algoritmda dasturchi explicit stack orqali o‘zi boshqarishi mumkin.