Föreläsning 5: Rekursion Vi har tidigare sett att man kan dela upp problem i mindre bitar med hjälp av underprogram, vilket är ett utmärkt sätt att lösa problem. Detta är ganska lätt att rita upp för sig själv. Tänk dig ett problem P som har "underproblemet" Q. För en programmerare innebär detta ett program P med ett underprogram Q. Ibland är det dock så att "underproblemet" är identiskt (eller nästan identiskt, det kanske skiljer någon parameter) med P. I detta fall är det inte ett annat program Q som skall anropas, utan P. Detta kanske låter lite knasigt nu i början, för vilket problem skulle kunna bara på detta sätt. Tja, t.ex. detta: Ett sätt att uttrycka det matematiska konceptet n-fakultet (N!) är att skriva det på följande vis: N!= 1 då N =0 N (N 1)! dån >0 Detta brukar kallas för den rekursiva definitionen, det finns även en annan definition som ser ut så här: N! = 1* 2 *... (N-1)*N då N > 0 0! = 1 Vi tänker oss nu att jag får i uppdrag av någon viktig person, t.ex. hans majestät kung Carl XVI Gustav, att skriva ett program som löser detta. Kungen kanske själv har kommit så långt så att han har ett huvudprogram, men tänker nu "outsource" resten av jobbet till mig. procedure Kungens_Program is K : Positive; K := Fakultet(3); Put(K); end Kungens_Program; Kungen ringer upp mig och befaller mig att koda detta underprogram Fakultet. Jag får resten av dagen på mig att göra detta. Som sann rojalist svarar jag artigt att detta skall jag göra. Har kommer få sitt program senast vid midnatt, och sedan lägger jag på luren. Jag sätter direkt igång med arbetet, kungen väntar ju! Jag vet vad programmet skall heta, och att det är en funktion (anropet står ju inte på egen rad). Vi kan börja med att fylla i detta. funktion Fakultet(... ) return... is Vi vet även att det som skall returneras alltid är ett positivt tal. Inparametern är - ja vadå? Vi kan ju skriva natural, men vad händer om man skulle skicka in ett negativt tal till fakultetsprogrammet? Här får vi gå tillbaka till definitionen - men det står ju inget! I detta fall är det alltså inte definierat vad som händer då man tar fakulteten av något negativt. Vi behöver alltså inte bry oss om detta fall! Om vi nu säger att inparametern har datatyp natural så vet vi i och för sig att programmet kommer att krascha när någon skickar in något negativt, men det får vi se som helt okej i detta läge.
funktion Fakultet(N : in Natural) return Positive is Nu kan vi börja fylla på med kod. Det finns ett lätt fall som vi kan ta hand om först, vi börjar med det. Här kan man fråga sig om man inte skall ha "else" istället för "end if", men om man funderar en stund så ser man att detta inte spelar någon roll. Om jag går in i denna if-sats kommer jag inte fortsätta nedåt i programmet i alla fall, eftersom det står return inuti if-satsen. Nu är frågan hur vi fortsätter. Hur räknar vi nu ut N-fakultet när N inte är 0? Vi vet att vi vill ta N gånger N-1-fakultet, men hur skriver vi detta egentligen? Vi kommer ungefär så här långt: funktion Fakultet(N : in Natural) return Positive is Om nu bara var N-1-fakultet så skulle ju detta lösa sig. Desvärre kommer det nog inte bli nånting om vi inte skriver något där. Nu börjar klockan närma sig 11 på kvällen och jag börjar få lite smått panik. Kungen vill ha sitt program om endast en timme, och jag har fortfarande inte löste problemet... Det bästa vore om det redan fanns ett program som löste N-1-fakultet, som jag kunde luta mig mot i detta läge. Alltså ungefär så här: funktion Fakultet(N : in Natural) return Positive is return N * Fakultet_2(N 1) ; Jag tänker mig här alltså att underprogrammet Fakultet_2 skulle lösa hela problematiken kring N-1- fakultet, så länge som det fungerar som det skall så fungerar ju mitt program. Då får jag plötsligt en strålande idé. Jag ringer upp min kompis Bengan som är en hejare på att koda. Jag förklarar noga situationen med Kungen, och att jag sitter lite i klistret här. Jag undrar om Bengan kanske skulle kunna skriva detta fakultet_2 program åt mig, jag har ju ändå gjort en del av jobbet redan. Trots att det bara är 45 minuter kvar lovar Bengan att lösa detta, och jag lägger på luren.
Bengan sätter igång med jobbet direkt. Han har fått veta av mig att programmet skall heta Fakultet_2, och att det skall vara en funktion. Han vet dessutom att returtypen måste vara ett positivt tal och att inparametern är en natural (eftersom mitt N inte var 0). Eftersom hans parameter bara existerar inuti Fakultet_2 kan han kalla sin parameter för N om han vill, även om han vet att den motsvarar min N-1. Han kommer så här långt: funktion Fakultet_2(N : in Natural) return Positive is end Fakultet_2; Hoppsan, tänker Bengan. "Vad gör jag nu. Jag lovade att göra ett N-1-fakultetsprogram, men får nu samma besvär som Erik hade". Tillslut kommer han fram till samma lösning som jag gjorde. Om det nu bara fanns ett program Fakultet_3 som räknade ut N-2-fakultet. Han kan ringa sin kompis! Men nu är det jäkligt brådis, bara 10 minuter till midnatt. Kompisen sätter igång direkt och kommer till, ja ni vet ju vad som händer: funktion Fakultet_3(N : in Natural) return Positive is end Fakultet_3; Men om man nu tittar noggrant på funktionerna Fakultet och Fakultet_2 så ser man något besynnerligt. De ser nästan exakt lika dana ut! Om man bara tar bort "_2" på den senare så får man ju tillbaka precis den funktion man hade från början. Om vi då leker med tanken att vi istället för att anropa Fakultet_2 anropar Fakultet, så blir ju resultatet faktiskt det samma. Dessutom kan vi ju göra detta hur många gånger som vi vill, mycket fler än vad vi har kompisar förmodligen. Som ni kanske förstår så gör man inte många kopior varje gång man skall göra en rekursiv funktion. Men man kan tänk sig att datorn gör det varje gång den rekursivt anropar sig själv. Man kan tänka sig det som en klon som får sina egna data. Varje klon kommer att göra sin beräkning och sedan returnera till sin föregångare. Vi kan nu gå igenom detta för vad som händer när man anropar funktionen fakultet med argumentet 3.
Här kommer nu lite tips på vad man bör tänka på när man skriver en rekursiv funktion: 1. En rekursiv funktion "hoppar alltid tillbaka" till den som anropade den (precis som alla underprogram). Man hoppar aldrig direkt tillbaka till huvudprogrammet. 2. Varje "klon" får sitt eget data. 3. När en "klon" anropar en annan "klon" så väntar den första tills den andra har kört klart. Övning på lektion Uppgift 1: Skriv en funktion som returnerar det N:te talet i Fibonacci-serien som definieras enligt: fib(1) = fib(2) = 1 fib(n) = fib(n - 1) + fib(n - 2) Gäller endast för positiva N. Lösning: function fib(n : in positive) return positive is if n <= 2 then y := 1; else y = fib(n 1) + fib(n 2); end fib; Styr så att det blir "exakt" på detta sätt. Varför? Jo, det kan vara så att de tycker att rekursion är svårt (!) När man sen returnerat sitt värde kan de vara bra att ta bort kopian av funktionen från tavlan. Att det kan vara extra "otrevligt" med just Fibonacci är att det är två anrop i samma uttryck. Detta gör att man måste poängtera att det kommer att ske samma sak igen för det andra uttrycket. Uppgift 2 : Skriv en funktion som beräknar x upphöjt till n. Här gäller det att studenterna skall komma fram till hur den rekursiva definitionen av funktionen ser ut. 1, n = 0 x^n = x * x^(n-1), n > 0 1 / x^(-n), n < 0 Denna är den kluriga!
Lösning: funktion Power(X, N : in float) return Float is x upphöjt till n (utan inbyggda operatorn). Vi tar inte hänsyn till "x = 0" fallet. if n = 0 then y := 1; elseif n > 0 then y := x * x_n(x, n 1); else y := 1 / x_n(x, n); end Power; OBS! I Ada finns en operator för "upphöjt till", ** för två heltal given, men vi skall givetvis inte använda denna. Det finns även ** för flyttal i Ada.Numerics.Elementary_Functions, inte heller något som behövs här alltså. Lösning: Uppgift 3: Skriv ett program som uppmanar användaren att mata in ett antal tal. Därefter skall talen matas in utan ledtext. Inmatningen avslutas då talet noll matas in. Funktionen skall dessutom skriva ut talen (men i omvänd ordning). procedure Main is procedure in_out is X : Integer; Get(X); if X /= 0 then in_out; Put(X); end in_out; Put("Mata in ett antal tal. Avsluta med 0"); in_out; end Main; Denna funktion utför saker både på vägen "in" och på vägen "ut". Trevligt. :-) Det man bör poängtera är att den rekursiva funktionen inte behöver vara själva huvudfunktionen. Detta kan t.ex. inträffa om rekursionen kräver fler parametrar än huvudfunktionen (kommer att vara så i sista uppgiften i laborationen).