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
}