Föreläsning 13 Rekursion Rekursion • En rekursiv metod är en metod som anropar sig själv. • Rekursion används som alternativ till iteration. Det finns programspråk som stödjer - enbart iteration (FORTRAN) eller - enbart rekursion (tidiga versioner av LISP). Java stödjer båda. • Rekursion är en teknik för att lösa problem som innehåller en eller flera problemdelar av liknande sort men som är lättare att lösa. 2 För att använda rekursion krävs: • Ett problem som innehåller ett eller flera delproblem som liknar problemet självt. • Ett enkelt fall av problemet som är lätt att lösa (utan rekursion). Detta kallas basfallet. • Ett sätt att förenkla problemet så att det kommer närmare basfallet. 3 Två problem som kan lösas med rekursion • n-fakultet, n! = n * (n – 1) * (n – 2) * ...... * 1 1! = 1 2! = 2 * 1 = 2 3! = 3 * 2 * 1 = 6 4! = 4 * 3 * 2 * 1 = 24 ...... • Tornen i Hanoi (lek från 1800-talet av franske matematikern Edouard Lucas). 1) Flytta en ring i taget. 2) Ingen ring får placeras ovanpå en mindre ring. 3) Alla ringar måste flyttas till pinnen till höger. 4 n-fakultet uppfyller kraven för rekursion • Problemet ska bestå av en eller flera delproblem som liknar problemet självt: n! består av ett enklare problem, nämligen problemet att beräkna produkten av de n – 1 första talen, dvs (n – 1)!. • Det måste finnas ett basfall av problemet som är enkelt att lösa (dvs utan rekursion): Basfallet kan väljas som 1! = 1. • Det måste finnas ett sätt att förenkla problemet så att det kommer närmare basfallet: Man kan på ett enkelt sätt komma från n! till (n – 1)!, nämligen genom att inse att n! = n * (n - 1)! 5 n-fakultet i Java-kod //Pre: n >= 1 //Post: resultat = n! public long factorial(int n) { if (n == 1) return 1; else return n * factorial(n – 1); } Rekursionens djup är fyra i detta exempel där factorial(4) räknas ut. factorial(4) 4 * factorial(3) 3 * factorial(2) 2 * factorial(1) 24 6 2 1 6 Tornen i Hanoi 1 ring 2 ringar 3 ringar 3 steg 3 steg 7 Tornen i Hanoi uppfyller kraven för rekursion • Problemet ska bestå av en eller flera delproblem som liknar problemet självt: Problemet att flytta en hög med ringar innehåller delproblemet att flytta en hög med färre antal ringar. • Det måste finnas ett basfall av problemet som är enkelt att lösa (dvs utan rekursion): Basfallet utgörs av problemet med 1 ring. Då är det självklart vad man ska göra: Flytta ringen till höger pinne. • Det måste finnas ett sätt att förenkla problemet så att det kommer närmare basfallet: För att flytta n ringar från pinne A till B, gör så här: - flytta n-1 ringar från pinne A till annanPinne(A,B) - flytta ring n till pinne B - flytta n-1 ringar från annanPinne(A,B) till B 8 Pseudokod för Tornen i Hanoi //Pre: n >= 1 //Post: Ringarna på pinne aSource har flyttats till //pinne aDestination public void hanoi(int n, Pole aSource, Pole aDestination) { if (n == 1) move(aSource, aDestination); else { Pole otherPole = otherPole(aSource, aDestination); hanoi(n – 1, aSource, otherPole); move(aSource, aDestination); hanoi(n – 1, otherPole, aDestination); } } 9 Iteration eller rekursion • Rekursionen i n-fakultet-problemet är en speciell sorts rekursion som kallas svansrekursion(tail rekursion), vilket innebär att sista operationen i en funktion är ett rekursivt anrop. En sådan rekursion kan lätt omvandlas till en iteration. Rekursiv variant Iterativ variant //Pre: n >= 1 //Post: resultat = n! public long factorial(int n) { if (n == 1) return 1; else return n * factorial(n – 1); } //Pre: n >= 1 //Post: resultat = n! public long factorial(int n) { long result = 1; for (int i = 2; i <= n; i++) result *= i; } return result; 10 Iteration eller rekursion • Att omvandla en svansrekursion till en iteration är lätt, så lätt att till och med kompilatorer kan göra det. • Andra typer av rekursion kan också omvandlas till iteration, men omvandlingsprocessen och koden är i allmänhet mer komplex. • Den omvända processen att omvandla en iteration till en rekursion går alltid att göra. Exempel: Iterativ variant Rekursiv variant public void fillCookieBag( CookieBag aBag) { while (!aBag.isFull()) aBag.addCookie(); } public void fillCookieBag( CookieBag aBag) { //base case: bag is full if (aBag.isFull()) return; //general case aBag.addCookie(); fillCookieBag(aBag); } 11 Iteration eller rekursion • Om det alltid är möjligt att använda antingen iteration eller rekursion vilken ska man då välja? Detta är en designfråga och för att göra rätt val bör man ställa sig följande två frågor: - passar iteration eller rekursion bäst till problemet - om rekursion är bäst lämpat är det värt kostnaden i minskad effektivetet (tid, minnesutrymme) • Rekursion passar bäst för problem som är naturligt självupprepande som tornen i Hanoi. Koden blir lättare att skriva och läsa och som en följd av detta lättare att felsöka och underhålla. • Rekursion är långsammare eftersom flera metodanrop är aktiverade samtidigt och systemet måste hantera alla variabler som är inblandade i de olika metodanropen. 12 Rekursiva sorteringsmetoder • Merge sort. Denna sorteringsalgoritm är självupprepande och har följande steg: 1) Dela upp listan i två ungefär lika stora listor. 2) Sortera rekursivt de två dellistorna, dvs med just denna algoritm. 3) Foga samman (merge) de två sorterade dellistorna till en sorterad lista. 3 13 2 8 11 1) 5 13 2 8 11 3 17 9 1 2) 2 5 8 11 13 1 3 9 17 3) 1 2 3 5 8 9 17 11 9 Lista att sortera. 5 13 1 17 13 Rekursiva sorteringsmetoder • Quicksort. Denna sorteringsalgoritm är självupprepande och har följande steg: 1) Välj ett element, kallat pivotelementet, ur listan. 2) Ordna om listan så att alla element som är mindre än pivotelementet kommer före detta, och så att alla element som är större än pivotelementet kommer efter detta. Pivotelementet har nu fått sin rätta plats. 3) Sortera rekursivt de två dellistorna, dvs med just denna algoritm. 5 13 2 8 11 3 17 9 1 Lista att sortera. 1) 5 13 2 8 11 3 17 9 1 Första elementet väljs som pivotelement. 2) 2 3 1 5 13 8 11 17 9 Pivotelementet placeras på rätt plats. 3) 1 2 3 5 8 9 11 13 17 14 SpiralApp • Problemet att rita en spiral består av delproblemet att rita en mindre spiral. • Basfallet består av problemet att rita en spiral som består av enbart ett streck. • Man kan förenkla problemet genom att först rita ett streck av spiralen, vrida 45 grader och sedan rita resten av spiralen. public Spiral(double x, double y, double length, double lengthChange, double angle, double angleChange, java.awt.Color aColor) { .... _firstLine = new java.awt.geom.Line2D.Double( x, y, endX, endY); if (_length <= 3) //base case _rest = null; else _rest = new Spiral(endX, endY, length-lengthChange, lengthChange, angle-angleChange, angleChange, aColor); ... } 15 TreeApp • Problemet att rita ett träd består av delproblemen att rita mindre träd. • Basfallet består av problemet att rita ett träd som består av enbart ett streck. • Man kan förenkla problemet genom att först rita ett streck av trädet, vrida en aning åt vänster och rita ett träd samt vrida en aning åt höger och rita ett träd. public Tree(double x, double y, double length, double lengthChange, double angle, double angleChange, Color aColor) { .... _firstLine = new Line2D.Double(x, y, endX, endY); if (_length <= 3) { //base case _leftSubtree = null; _rightSubTree = null; } else { _leftSubTree = new Tree(endX, endY, length-lengthChange, lengthChange, angle-angleChange, angleChange, aColor); _rightSubTree = new Tree(endX, endY, length-lengthChange, lengthChange, angle+angleChange, angleChange, aColor); }... 16 }