Rekurzió Működése, programtranszformációk előadás http://nik.uni-obuda.hu/prog2 Szénási Sándor szenasi.sandor@nik.uni-obuda.hu Óbudai Egyetem,Neumann János Informatikai Kar
Rekurzió Rekurzió alapjai Rekurzív algoritmusok végrehajtása Programtranszformációk
Rekurzió alapelve szenasi.sandor@nik.uni-obuda.hu 3 Ahhoz, hogy megérthessük a rekurziót, először meg kell értenünk a rekurziót (ismeretlen szerző) Rekurzív algoritmusokat általában akkor használunk, ha az alapfeladat túl bonyolult, azonban rekurzív hívásokkal ezt vissza tudjuk vezetni egyszerűbb (rész)feladatok megoldására Ennek megfelelően egy rekurzív feladatot általában ehhez hasonlóan definiálunk: triviális megoldása általános eset egyszerűsítése Technikai értelemben rekurzív algoritmusnak azt tekintjük, ami közvetve (közvetlen rekurzió) vagy más függvények közbeiktatásával (közvetett rekurzió) meghívja önmagát
Rekurzív függvény felépítése szenasi.sandor@nik.uni-obuda.hu 4 Az előzőek alapján egy lehetséges definíció: f(x) = g(x) h f i(x) ha t(x) ha t(x) f(x) a rekurzív függvény t(x) triviális esethez jutottunk-e? g(x) triviális esetben a megoldás h(x) utólagos feldolgozás i(x) előzetes feldolgozás Feltételezzük, hogy bármelyik bemenetből a triviális megoldás biztosan, véges lépésből elérhető A függvény általában nem csak önmagát hívja, hanem ez előtt és után van lehetősége műveleteket végezni a továbbítandó paramétereken
5 Rekurzív függvény általános pszeudokódja Az előző oldalon látottaknak megfelelően: függvény Rekurzív(x) ha t(x) akkor Rekurzív g(x) különben a i(x) b Rekurzív(a) c h(b) Rekurzív c elágazás vége A pszeudokódban található függvényhívások értelemszerűen feladattól függően jelenthetnek tetszőleges összetett műveletet A fenti csak egy általános minta, a konkrét feladatok ettől egészen eltérő szerkezeteket is igényelhetnek (pl. több hívás, ezek eredményei között műveletek)
6 Rekurzív függvény általános pszeudokódja Rekurzív feladat megoldásának lehetőségei Rekurzív specifikáció Nemrekurzív specifikáció Rekurzív algoritmus Nemrekurzív algoritmus Rekurzív programnyelv Nemrekurzív programnyelv Számítógép Az implementált programot minden esetben Neumann elvű (tehát nem rekurzív) gépen hajtjuk végre
Rekurzió Rekurzió alapjai Rekurzív algoritmusok végrehajtása Programtranszformációk
8 Verem Verem adatszerkezet Adatok tárolására szolgáló szerkezet Mindig az utoljára belehelyezett elemet (push) tudjuk belőle kiolvasni (pop) (LIFO Last In First Out) Kiolvasáskor az elem egyben törlődik is 5 4 3 7 BE 5 BE 4 BE 3 5 BE 7 KI 7 KI 3 BE 5 Később részletesebben tárgyaljuk Processzor verem A verem gyakran használt adatszerkezet az operációs rendszer, illetve a processzor működése során eljárások hívásakor a verem tárolja el a hívó utasítást követő utasítás címét (ide kell majd visszatérni a befejezés után) Szintén a veremben tárolódnak a meghívott eljárás paraméterei, lokális változói stb. Ez természetesen implementációtól függ, a mi számunkra azonban ez az egyszerűsített működési elv is megfelelő
eljárások hívásának (egy lehetséges) módja szenasi.sandor@nik.uni-obuda.hu 9... E1(1, 2)... eljárás E1(a, b) x lokális változó... E2(a+b)... eljárás E2(d) y lokális változó... C 1 2 x C 3 y verem aktuális állapota Minden függvényhíváskor eltárolódik a visszatérési cím, illetve a függvény lokális változói Visszatéréskor ezek törlődnek a veremből Ez alapján látható, hogy az általuk látott adatok szempontjából az egymás után hívott függvények valójában egymástól függetlenek Lásd változók élettartama
Rekurzív eljáráshívás szenasi.sandor@nik.uni-obuda.hu 10... függvény Fakt(a) x = Fakt(3) Ha a = 0 függvény akkor Fakt(a) a = 3... Ha a = 0 függvény akkor Fakt(a) a = 2 Fakt 1 Ha a = 0 függvény akkor Fakt(a) a = 1 különben Fakt 1 ha a = 0 akkor különben Fakt 1 (6) Fakt a * Fakt(a-1) különben (1) Fakt 1 elágazás vége (2) Fakt a * Fakt(a-1) különben függvény vége elágazás vége (1) Fakt a * Fakt(a-1) függvény vége elágazás vége Fakt a * Fakt(a-1) függvény vége elágazás vége függvény vége a = 0 x C 3.. C 2.. C 1.. C 0.. verem aktuális állapota Látható, hogy rekurzív hívás esetén is egymástól független adatokon dolgoznak az egyes függvények Az ugyanolyan nevű lokális változók emiatt nincsenek egymásra hatással Ennek figyelembevételével kell megoldanunk az egyes függvények közötti adatcserét
11 Rekurzív algoritmus bemenete Biztosítani kell, hogy minden meghívott függvény hozzáférjen a működéséhez szükséges bemenő paraméterekhez Bemenet biztosítása külső változókon keresztül Az egyes futó függvény példányok nem látják egymás lokális adatait, ezért a mindegyik számára szükséges bemeneti adatokat nem tudjuk ezek között tárolni A függvényen kívüli adatokat azonban a rekurzió minden szintjéről elérjük (globális változók, objektum adattagjai stb.) Tárhely/futásidő szempontjából optimális megoldás Bemenet biztosítása paramétereken keresztül Mint minden más függvénynél, a rekurzív függvényeknél is van lehetőség minden bemenő paramétert átadni a függvény hívásakor Ebben az esetben értelemszerűen a következő rekurzív híváskor mindezt újra át kell adni Ez jóval áttekinthetőbb, bár kevésbé hatékony megoldást nyújt
12 Rekurzív algoritmus kimenete Rekurzív hívási lánc során problémát jelenthet az eredmény visszaadás, mivel az eredeti hívó, illetve a végeredményt elérő függvény között számos függvényhívás állhat Eredmény visszaadás külső változókon keresztül A bemenethez hasonlóan itt is van arra lehetőség, hogy a végeredményig eljutó szint egy külső változóban eltárolja az eredményt, a hívó pedig majd innen kiolvassa Eredmény visszaadás függvény visszatérési értékkel Hagyományos függvényekhez hasonlóan a rekurzív függvények is rendelkezhetnek visszatérési értékkel Az önmagát meghívó függvénynek azonban biztosítania kell, hogy az (önmagától) visszakapott értéket továbbítsa a hívója felé Eredmény visszaadás paraméterekkel Amennyiben az újrahíváskor is mindig címszerinti paraméterátadás történt, akkor bármelyik szint változtatja meg a paraméter értékét, az a hívó szintjén is változni fog
13 Rekurzió jellemzői Előnyök Gyakran elegáns, jól érthető, áttekinthető kódot ad Bizonyos feladatoknál (pl. rekurzív adatszerkezetek feldolgozása esetén) jóval egyszerűbbek a rekurzív megoldások Hátrányok Gyakran áttekinthetetlen, ember számára nagyon nehezen értelmezhető kódot ad Nagyszámú újrahívás esetén a nyomkövetés nehézkes, nehezen áttekinthető A függvényhívás általában meglehetősen költséges művelet, emiatt a rekurzív algoritmusok nem hatékonyak Egy rekurzívan megadott algoritmusban gyakran észrevétlenek maradnak elhibázott döntések (bár ez nem a rekurzió, hanem a tervező hibája) Átalakítások Később látni fogjuk, hogy a rekurzív és iteratív megoldások általában egymásba alakíthatók
Rekurzió Rekurzió alapjai Rekurzív algoritmusok végrehajtása Programtranszformációk
15 Elemi konstrukciók függvények segítségével A strukturált programozásnál megismert három konstrukció egyszerűen felírható függvényekkel is Szekvencia Elágazás ciklus f(x) = g h(x) f(x) = f(x) = g(x) h(x) g(x) h f i(x) ha p(x) ha p(x) ha p(x) ha p(x) Egyszerű szabályokat követve így megadhatjuk a programozási tételek rekurzív formáját S 1 S 2 Ha L akkor S 1 különben S 2 elágazás vége ciklus amíg L S ciklus vége
16 R I Rekurzív formában megadott fv. Ha egy rekurzív függvény az alábbiak szerint számítható ki: g(f(i-1), f(i-2),..., f(i-k)) f(i) = h(i) Az iteratív megoldás: eljárás f(n) ciklus i 0-tól (K-1)-ig F[i] h[i] ciklus vége ciklus i K-tól N-ig F[i] g(f[i-1], F[i-2],..., F[i-K]) ciklus vége f F[N] ha i > K ha 0 i < K Optimálisabb változat is készíthető, hiszen a tömbből mindig csak az utolsó K elemre van szükségünk
17 R I Jobbrekurzió átírása Jobbrekurzió általános esete (a rekurzív hívást követően már nincs szükség a függvény lokális változóira): eljárás JobbRek(X, Y) Q(X, Y) ha p(x, Y) akkor S(X, Y) JobbRek(f(X), Y) elágazás vége Az iteratív megoldás: eljárás JobbRek(X, Y) Q(X, Y) ciklus amíg p(x, Y) S(X, Y) X f(x) Q(X, Y) ciklus vége
18 R I Balrekurzió átírása 1. Balrekurzió során a rekurzív függvény meghívása után is szükség van a lokális változók értékeire, azok módosulhatnak is Balrekurzió általános esete: eljárás BalRek(X, Y) ha p(x, Y) akkor BalRek(f(X), Y) S(X, Y) különben T(X, Y) elágazás vége A hívás előtt is szerepelhet valamilyen művelet, az egyszerűség kedvéért ezzel nem foglalkozunk Tipikusan akkor célszerű ezt használni, ha egy sorozatot visszafelé szeretnénk feldolgozni (verem?)
19 R I Balrekurzió átírása 2. Balrekurzió egy lehetséges iteratív átirata: eljárás BalRek(X, Y) N 0 ciklus amíg p(x, Y) Verembe(X) X f(x) N N + 1 ciklus vége T(X, Y) ciklus i 1-től N-ig Veremből(X) S(X, Y) ciklus vége Ha a sorozat elemszámát előre ismerjük, értelemszerűen nincs szükség az N számolásra Ha az f(x) függvénynek van inverze, egyszerűbb (veremnélküli) algoritmus is adható
20 Iteratív Rekurzív átalakítás Az elöl és hátultesztelős ciklusok átírása eljárás ElölTeszt(X) ciklus amíg P(X) S(X) ciklus vége eljárás HátulTeszt(X) ciklus S(X) ciklus amíg P(X) eljárás ElölTeszt(X) ha p(x) akkor S(X) ElölTeszt(X) elágazás vége eljárás HátulTeszt(X) S(X) ha p(x) akkor HátulTeszt(X) elágazás vége Számlálós ciklus egyszerűen átírható elöltesztelőssé Amennyiben a ciklus előtt vagy után további műveletek szerepelnek, azokat célszerű egy másik függvényben (a rekurzió 0. szintjén ) elvégezni
21 Irodalomjegyzék Javasolt/felhasznált irodalom Pap, Szlávi, Zsakó: μlógia4 Módszeres programozás: Rekurzió ELTE TTK, 2004 S. Harris, J. Ross: Kezdőkönyv az algoritmusokról SZAK Kiadó, 2006 Wikipedia.org megfelelő szócikkek http://en.wikipedia.org/wiki/recursion