Note til Programmeringsteknologi Akademiuddannn i Informationsteknologi Rekursion C#-version Finn Nordbjerg 1
Rekursion Rekursionsbegrebet bygger på, at man beskriver noget ved "sig selv". Fx. kan tallet n! ("n-fakultet") defineres ved følgende formel: n! = 1 for n = 0; n! = n*(n-1)*(n-2)*...*2*1 for n 1 altså produktet af alle hele tal fra n og ned til 1. Tallet n! kan imidlertid også defineres på en anden måde, som er baseret på det forhold, at der gælder (1) n! = 1 for n = 0 (2) n! = n*(n-1)! for n > 1 En sådan definition kaldes rekursiv og er karakteriseret ved, at begrebet selv anvendes i definitionen. (1) kaldes definitionens basisdel og (2) dens rekursionsdel. Denne definition kan implementeres direkte ved følgende metode: public long FakRek( int n) {//PRE n>=0 if(n == 0) return 1; //basisdel return n*fakrek(n-1); //rekursionsdel I programmering anvendes betegnn rekursiv om metoder, som kalder sig selv. Ved rekursion forstås altså, at en problemløsningsmetode beskrives ved en basisdel, og at et givet skridt defineres udfra resultatet af et eller flere af de foregående skridt. Bemærk, at der skal altid være en basisdel og rekursionsdelen skal formuleres, så basisdelen altid nås til sidst. Ellers har man lavet sig en ikke-terminerende algoritme. Øv 1 Lav en ikke-rekursiv (dvs. iterativ) metode til beregning af n!. Øv 2 Lav et testprogram, som afprøver de to løsninger. Som for fakultetsberegning kan rekursive algoritmer altid omformes til en iterativ (overvej!!!), men for en lang række problemer er iterative løsninger meget mere komplekse og uoverskuelige. 2
Prisen for at anvende rekursive metoder (man får jo intet gratis) er et ekstra lagerforbrug, idet hver aktivering af metoden kræver lagring af data (parametre, lokale variable mv.). Disse data skal gemmes til kaldet af sluttes. For fakultetsberegnings vedkommende betyder dette, at der ved beregning af fx 10! skal gemmes oplysninger om 10 aktiveringer af metoden, inden kaldene begynder at returnere. Dette gøres automatisk på den såkaldte rekursionsstak, men lageret optages altså. I det følgende skal et par klassiske eksempler på rekursive løsninger kort beskrives. Tårnene i Hanoi Dette er et eksempel, hvor det er langt lettere at udlede en rekursiv algoritme end en iterativ. I følge sagnet har munkene i en buddhistorden i Hanoi længe været optaget af følgende opgave: Der er tre stænger og 64 skiver af forskellig størr. Hver skive har et hul i midten, så den kan placeres på en af stængerne. I starten var alle 64 skiver placeret på een stang. Skiverne er sorteret efter størr og største skive ligger nederst. Se figur: Kilde Hjælp Mål Opgaven går ud på at flytte tårnet fra denne stang (kilden) til en anden stang (målet). Den tredje stang (hjælp) kan undervejs anvendes til midlertidig opbevaring af skiver. For at gøre opgaven interessant (den siges at være lutrende for sjælen), skal følgende regler overholdes: - der må kun flyttes een skive ad gangen - en større skive må aldrig lægge ovenpå en mindre (For at imødegå dumsmarte løsninger skal det pointeres, at skiverne selvfølgeligt kun må placeres på en af de tre stænger ikke på jorden ved siden af!). Sagnet fortæller endvidere at fuldførn af opgaven vil indvarsle Verdens undergang. Løsningen af denne opgave er ikke indlysende, men lad se: For at flytte tårnet må den nederste skive (den største) flyttes fra kilden til målet. Dette kan ikke lade sig gøre før de 63 skiver, som ligger ovenpå, er flyttet til hjælpestangen, men så er det simpelt. Derefter må de 63 skiver flyttes fra hjælpestangen til målet. Men hvordan skal vi flytte de 63 skiver fra kilden til hjælpestangen? Dette kan gøres ved at flytte 62 skiver fra kilden til målet, derefter flytte den sidste skive til hjælpestangen, og til sidst flytte de 62 skiver tilbage igen. 3
Men hvordan flytter vi 62 skiver? Ved at flytte de 61 øverste. Dette er en rekursiv strategi, hvor vi opdeler vores problem i stadig mindre problemer (at flytte 63 skiver er et mindre problem end at flytte 64 etc.) indtil vi når basisdelen: at flytte én skive. Vi kan altså formulere følgende løsning: - flyt 63 skiver fra kilde til hjælpstang (målet kan bruges som arbejdsplads) - flyt een skive fra kilde til mål - flyt 63 skiver fra hjælpestang til mål (kilden kan bruges som arbejdsplads) På tilsvarende måde kan 63 skiver flyttes ved at flytte 62 skiver og derefter flytte een og så flytte 62 igen etc. etc. Generelt kan løsningen formuleres ved følgende rekursive metode (n er antal skiver i tårnet): public void FlytTaarn(int n,string kilde, string maal, string hjlp) { if (n==1) Console.WriteLine( Flyt skive fra +kilde+ til +maal); { FlytTaarn(n-1,kilde,hjlp,maal); Console.WriteLine( Flyt skive fra +kilde+ til +maal); FlytTaarn(n-1, hjlp, maal, kilde); Til berolig for evt. læsere, der bekymrer sig for Verdens snarlige undergang, kan følgende anføres: Udfør med n=64 resulterer i to kald med n=63, der igen hver resulterer i to kald med n=62 etc. I alt 2 64-1 kald. I hvert kald flyttes een skive. Dvs., at hvis munkene kan flytte een skive i sekundet, aldrig laver fejl og arbejder døgnet rundt året rundt, så vil det tage ca 600 000 000 000 år at flytte tårnet (det kan vises, at der ikke findes nogen hurtigere algoritme), og til den tid skal Verden såmænd nok være gået under alligevel. Det skal understreges, at dette er en meget koncis og elegant løsning på et ikke-trivielt problem. Der findes iterative løsninger, men de er komplekse og vanskelige at udlede (prøv!). Øv 3 Lav et testprogram, som afprøver ovenstående. (Lad være med at teste med 64 skiver!) Binær søgning Binær søgning er et eksempel på en meget elegant løsning på et problem. Normalt udledes algoritmen for binær søgning fra søgeskabelonen (se note om algoritmeskabeloner), men løsningen kan også formuleres rekursivt, hvilket giver en enklere og elegantere, men ikke helt så effektiv løsning. 4
Følgende metode undersøger om et givet element, x, findes i et sorteret array, T. Elementtypen er blot int: public bool BinSearch(int l, int h, int x, int[] T) {//PRE T er sorteret int m; if(l>h) return false; { m = (l+h)/2; if(x==t[m]) return true; if(x>t[m]) return BinSearch(m+1,h,x,T); return BinSearch(l,m-1,x,T); Prøv at ændre metoden, så den returnerer indekset på det søgte element. Øv 4 Lav en ikke-rekursiv formulering af BinSearch. Øv 5 Lav et test program, som afprøver de to udgaver af BinSearch. Øv 6 Ved Fibonacci-tallene forstås en følge af hele positive tal, hvor et givet Fibonaccital findes som summen af de to foregående. Rekursiv definition: Udarbejd (1) Fib(n) = n for n= 0 og n= 1 (2) Fib(n) = Fib(n-1) + Fib(n-2) for n>1 a) en rekursiv funktion til beregning af et givet Fibonaccital, og b) en iterativ funktion til beregning af et givet Fibonaccital. 5
Øv 7 Lav et testprogram, som afprøver de to udgaver af Fib(-). Kan du registrere forskel I deres effektivitet? (Lad vær at teste den rekursive udgave med store tal (dvs. ikke meget over 40). 6