Rekursion Rekursion Rekursion, implementation i Java Rekursiva ...
Rekursion Rekursion Rekursion, implementation i Java Rekursiva ...
Rekursion Rekursion Rekursion, implementation i Java Rekursiva ...
Create successful ePaper yourself
Turn your PDF publications into a flip-book with our unique Google optimized e-Paper software.
<strong>Rekursion</strong><br />
<strong>Rekursion</strong> är ett grundläggande begrepp inom matematik och<br />
datavetenskap och används för att göra definitioner och<br />
konstruera algoritmer. Exempel:<br />
Potensfunktionen<br />
x 0 = 1<br />
x n =x * x n–1 , n heltal >0<br />
(Basfall)<br />
(<strong>Rekursiva</strong> steget)<br />
Fakultetsfunktionen<br />
0! = 1 (Basfall)<br />
n! = n * (n–1)!, n heltal > 0 (<strong>Rekursiva</strong> steget)<br />
<strong>Rekursion</strong><br />
• Dela upp problemet i delproblem (instanser) som har<br />
samma struktur som ursprungliga problemet men är<br />
mindre till sin omfattning (rekursiva steget).<br />
Ex: n! = n * (n–1)!<br />
• Fortsätt uppdelningen tills du fått ett problem som är<br />
trivialt att lösa (basfall).<br />
Ex: 0!<br />
AD, <strong>Rekursion</strong> 1<br />
AD, <strong>Rekursion</strong> 2<br />
<strong>Rekursion</strong>, <strong>implementation</strong> i <strong>Java</strong><br />
/** Beräkna n! */<br />
public long fac(int n) {<br />
if (n==0) {<br />
return 1;<br />
} else {<br />
return n*fac(n-1);<br />
}<br />
}<br />
<strong>Rekursiva</strong> metoder<br />
En rekursiv metod måste ha<br />
• En eller flera parametrar som bestämmer problemets<br />
storlek<br />
• Ett eller flera basfall som löses direkt.<br />
• Ett eller flera rekursiva anrop. Det rekursiva anropet måste<br />
leda till att ett basfall så småningom nås.<br />
Lita på att det rekursiva anropet ger rätt resultat!<br />
AD, <strong>Rekursion</strong> 3<br />
AD, <strong>Rekursion</strong> 4
<strong>Rekursion</strong>, exempel ur boken (7.3.1)<br />
Problem: Antag att <strong>Java</strong>s System.out.print(...) enbart klarar<br />
av att skriva ut tecken (char) och strängar. Implementera en<br />
metod för utskrift av ett godtyckligt icke-negativt tal.<br />
Idé: Ett n siffrigt tal t = s 1 s 2 ....s n-1 s n kan delas upp enligt<br />
s 1 s 2 ....s n-1<br />
t/10<br />
s n<br />
t%10<br />
<strong>Rekursion</strong>, exempel ur boken (7.3.1)<br />
Vi kan enkelt skriva en hjälpmetod som skriver ut ett<br />
ensiffrigt tal:<br />
/** Skriv ut det ensiffriga talet d */<br />
private void printDigit(int d) {<br />
char ch = (char) ('0' + d);<br />
System.out.print(ch);<br />
}<br />
OBS: denna metod är osäker. Ingen kontroll av att talet är<br />
ensiffrigt!<br />
AD, <strong>Rekursion</strong> 5<br />
AD, <strong>Rekursion</strong> 6<br />
<strong>Rekursion</strong>, exempel ur boken (7.3.1)<br />
Lösningen för ett godtyckligt tal ÿ 0 kan nu implementeras<br />
rekursivt enligt:<br />
public void printNumber(int n) {<br />
if (n=10) {<br />
printNumber(n/10);<br />
}<br />
printDigit(n%10);<br />
}<br />
Även här anropas printDigit enbart för ensiffriga tal.<br />
OBS att här anropas printDigit enbart för ensiffriga tal!<br />
AD, <strong>Rekursion</strong> 7<br />
AD, <strong>Rekursion</strong> 8
Exekvering av rekursiva metoder<br />
Anrop: printNumber(123)<br />
n=123<br />
n>=10<br />
printNumber(12)<br />
printDigit(3)<br />
n=12<br />
n>=10<br />
printNumber(1)<br />
printDigit(2)<br />
Gäller för alla metodanrop (inte enbart rekursiva):<br />
Ett anrop av en metod skapar en ”aktiveringspost” med plats för<br />
parametrar, återhoppsadress m.m.<br />
När en metod når sitt slut sker återhopp till satsen närmast efter<br />
den där den avslutade upplagan anropades.<br />
n=1<br />
n=base) {<br />
printIntRec(n/base,base);<br />
}<br />
printDigit(n%base); AD, <strong>Rekursion</strong> 11<br />
}<br />
<strong>Rekursion</strong>, exempel ur boken (7.3.1)<br />
printIntRec är inte en robust <strong>implementation</strong>:<br />
• Anrop med base>16 ger ”index out of range”<br />
• Anrop med base=0 ger exekveringsfel pga<br />
division med 0<br />
• Anrop med base=1 ger upphov till en oändlig<br />
kedja av rekursiva anrop pga att det rekursiva<br />
anropet printInt(n/base,base) har sin<br />
första parameter = n<br />
AD, <strong>Rekursion</strong> 12
<strong>Rekursion</strong>, exempel ur boken (7.3.1)<br />
Bättre lösning. Robust. Kollar bara en gång att base har rimligt värde genom att<br />
ha en speciell "driver"-rutin. Klarar nu dessutom även negativa tal:<br />
public void printInt(int n, int base) {<br />
if (basedigitTable.length()) {<br />
throw new RuntimeException();<br />
} else {<br />
if (n
Ex: Skriva ut lista baklänges<br />
Element i listan:<br />
class ListNode {<br />
char ch;<br />
ListNode next;<br />
}<br />
Lösning:<br />
public void reverseList( ListNode list) {<br />
if (list!=null) {<br />
reverseList(list.next);<br />
System.out.print(list.ch);<br />
}<br />
}<br />
(Här har vi valt basfallet = tom lista då ingenting skall skrivas.)<br />
AD, <strong>Rekursion</strong> 17<br />
Hanois torn<br />
1<br />
2<br />
3<br />
4<br />
5<br />
pinne 1 pinne 2 pinne 3<br />
n skivor finns i avtagande storlek på en pinne (1). Flytta<br />
dem så att de kommer i samma inbördes ordning på en<br />
av de andra pinnarna (t ex 3). Även den tredje pinnen<br />
(2) får utnyttjas för mellanlagring.<br />
AD, <strong>Rekursion</strong> 18<br />
Regler:<br />
Hanois torn<br />
• Bara en skiva i taget får flyttas<br />
• En skiva som tas från en pinne måste genast läggas på en<br />
av de andra pinnarna<br />
• Det får aldrig inträffa under flyttningarnas gång att en<br />
större skiva hamnar ovanför en mindre (på samma pinne).<br />
AD, <strong>Rekursion</strong> 19<br />
Hanois torn, rekursiv lösning<br />
Flytta först de n-1 översta under iakttagande av alla regler till pinne 2 (3<br />
kan utnyttjas för mellanlagring).<br />
Flytta därefter den största skivan från 1 till 3.<br />
Till sist flyttas de n-1 skivorna från 2 till 3, igen under iakttagande av alla<br />
regler (och med 1 som plats för mellanlagring)<br />
1<br />
2<br />
3<br />
4<br />
5<br />
pinne 1 pinne 2 pinne 3<br />
AD, <strong>Rekursion</strong> 20
Hanois torn, rekursiv lösning i <strong>Java</strong><br />
Hanois torn, rekursiv lösning i <strong>Java</strong><br />
Följande metod skriver ut hur man skall flytta:<br />
public void move(int n, int start, int finish, int temp) {<br />
if (n==1) {<br />
System.out.println("Move from "+start+" to "+finish);<br />
} else {<br />
move(n-1, start, temp, finish);<br />
System.out.println("Move from "+start+” to "+finish);<br />
move(n-1, temp, finish, start);<br />
}<br />
}<br />
Anrop:<br />
move(2,1,3,2)<br />
...<br />
move(1,1,2,3)<br />
"Move from 1 to 3"<br />
move(1,2,3,1)<br />
"Move from 1 to 2”<br />
"Move from 2 to 3"<br />
AD, <strong>Rekursion</strong> 21<br />
AD, <strong>Rekursion</strong> 22<br />
Samband mellan rekursion och induktion<br />
Matematisk induktion används ofta för att bevisa samband<br />
som gäller för positiva heltal n. Bevisen görs i två steg:<br />
1. Visa att sambandet gäller för ett eller flera små värden<br />
på n.<br />
2. Visa att om man gör antagandet att sambandet håller<br />
för alla heltal n upp till ett visst värde k, så gäller det<br />
även för närmast större värde k+1.<br />
Samband mellan rekursion och induktion<br />
Ex: Visa att 1+2+3+... + n = n(n+1)/2 för alla nÿ1<br />
• n = 1.<br />
Vänsterledet = 1.<br />
Högerledet = 1*2/2=1. Stämmer.<br />
• Vi antar nu att 1+2+....+ n = n(n+1)/2 för 1 n k (*)<br />
Visa att 1+2+...+ k+k+1= (k+1)(k+2)/2.<br />
Vänsterledet = (1+2+...+k) + k+1 = [enligt (*)] =<br />
k(k+1)/2 + k+1= (k 2 +3k+2)/2<br />
Högerledet = (k+1)(k+2)/2 = (k 2 +3k + 2)/2 = Vänsterledet.<br />
AD, <strong>Rekursion</strong> 23<br />
AD, <strong>Rekursion</strong> 24
Samband mellan rekursion och induktion<br />
<strong>Rekursiva</strong> algoritmer kan bevisas ge korrekt resultat<br />
med induktionsbevis.<br />
Ex: Fakultetsberäkningen<br />
public int fac(int n) {<br />
if (n==0) {<br />
return 1;<br />
} else {<br />
return n*fac(n-1);<br />
}<br />
}<br />
Samband mellan rekursion och induktion<br />
1. När n=0 blir resultatet 1 vilket är korrekt enligt def av n!<br />
2. Antag att algoritmen ger korrekt resultat för 0 n k. (*)<br />
Visa att den då också ger korrekt reultat för n=k+1.<br />
Anrop fac(k+1) ger (eftersom k+1>0) resultatet<br />
(k+1)*fac(k+1-1) = (k+1)*fac(k).<br />
Enligt induktionsantagandet (*) ger anropet fac(k) korrekt<br />
resultat dvs k!. Vi får därför resultatet (k+1)*k! = (k+1)!<br />
V.S.B.<br />
AD, <strong>Rekursion</strong> 25<br />
AD, <strong>Rekursion</strong> 26<br />
Analys av rekursiva algoritmer, ex;<br />
fakultetsberäkningen<br />
public int fac(int n) {<br />
if (n == 0) {<br />
return 1;<br />
} else {<br />
return n*fac(n–1);<br />
}<br />
}<br />
Sätt T(n) = tidskomplexiteten för ett anrop av fac(n) (*)<br />
AD, <strong>Rekursion</strong> 27<br />
Analys av rekursiva algoritmer, ex;<br />
fakultetsberäkningen<br />
För n=0 utförs ett arbete som tar konstant tid, c1 (kontroll<br />
n==0 samt return-satsen). Alltså blir T(0) = c1<br />
För nÿ1 utförs dels ett konstant arbete, c2 (kontroll n==0 som<br />
misslyckas, en multiplikation samt en return-sats) dels det<br />
arbete som görs vid ett anrop av fac(n-1). Det senare är<br />
T(n–1) enligt vår ansats (*)<br />
Vi får ett rekursivt uttryck på tidskomplexiteten (rekursionsformel):<br />
T(0) = c1<br />
T(n) = c2 + T(n–1) för nÿ1<br />
AD, <strong>Rekursion</strong> 28
Analys av rekursiva algoritmer, ex;<br />
fakultetsberäkningen<br />
<strong>Rekursion</strong>sformeln kan lösas med återsubstitution, dvs man<br />
använder formeln för n, n–1,....0:<br />
T(n) = c2+T(n–1) = c2+c2+T(n–2) = c2+c2+c2+T(n–3) = ...=<br />
i*c2+T(n–i) = ... = n*c2+T(0) = n*c2 + c1 = O(n)<br />
Om vi bara är intresserade av storleksordningen kan vi ersätta c1<br />
ochc2med1:<br />
T(0) = 1<br />
T(n) = 1 + T(n–1) för nÿ1<br />
==> T(n) = 1+T(n–1) = 1+1+T(n–2) = ... = i+T(n–i) = ...<br />
=n+T(0)=n+1=O(n)<br />
AD, <strong>Rekursion</strong> 29<br />
Binärsökning<br />
Algoritm för att söka i en sorterad vektor<br />
Jämför det sökta med mittelementet i vektorn.<br />
Om likhet, avbryt.<br />
Om det sökta är mindre än mittelementet, fortsätt<br />
sökningen i vänster halva av vektorn<br />
Om det sökta är större än mittelementet, fortsätt<br />
sökningen i höger halva av vektorn<br />
/** Undersök om x finns i den sorterade vektorn a */<br />
public boolean binarySearch(int[] a, int x) {<br />
return binarySearch(a,x,0,a.length–1);<br />
}<br />
AD, <strong>Rekursion</strong> 30<br />
Binärsökning<br />
/* Privat hjälpmetod. Söker i delvektorn a[low]..a[high] */<br />
private boolean binarySearch(int[] a, int x,<br />
int low, int high) {<br />
if (low>high) {<br />
return false;<br />
} else {<br />
int mid = (low+high)/2; // heltalsdivision!<br />
if (x == a[mid]) {<br />
return true;<br />
} else if (x a[mid]<br />
W(0) = 1<br />
AD, <strong>Rekursion</strong> 32
Analys av rekursiva algoritmer, ex:<br />
binärsökning<br />
Förenklande antagande: n = 2 k – 1 för något heltal k>0. Då<br />
går alla halveringar jämnt upp.<br />
Då blir mid = (0 + 2 k –1–1)/2=2 k-1 – 1 och därmed<br />
Storleken av vänster halva = mid = 2 k-1 –1och<br />
Storleken av höger halva = n-mid-1 = 2 k –1 –2 k-1 +1–1=<br />
2 k-1 –1.<br />
<strong>Rekursion</strong>sformeln för tidskomplexiteten blir därför:<br />
W(2 k –1)=1+W(2 k-1 –1) omxa[mid]<br />
Analys av rekursiva algoritmer, ex:<br />
binärsökning<br />
W(2 k –1)=1+W(2 k-1 –1) omk>0<br />
W(0) = 1<br />
Återsubstitution ger:<br />
W(2 k –1)=1+W(2 k-1 –1) =1+1+W(2 k-2 – 1) = ...<br />
1+1+1+W(2 k-3 – 1) = ... = i + W(2 k-i –1)=...(fortsätttillsi<br />
=k) ...=k+W(0)=k+1<br />
Eftersom antagandet var att n = 2 k – 1 så följer att k = 2 log (n+1)<br />
Därmed W(n) = 2 log (n+1) + 1 = O(log n)<br />
W(0) = 1<br />
AD, <strong>Rekursion</strong> 33<br />
AD, <strong>Rekursion</strong> 34<br />
Analys av rekursiva algoritmer, ex:<br />
binärsökning<br />
Enklare hade vi kunnat resonera så här för värstafallsanalysen:<br />
Värsta fallet är att testet x == a[mid] misslyckas varje gång.<br />
Halveringarna kommer då utföras ända tills basfallet (tom delvektor) inträffar.<br />
Hur många halveringar kan det högst bli?<br />
Vi har från början en vektor av storlek n. Efter en halvering är storleken n/2,<br />
efter 2 halveringar är den n/4 och efter k halveringar<br />
n/2 k . När vi kommit ner till vektorstorlek 1 ger nästa halvering storlek 0.<br />
Hur många halveringar krävs då för att nå vektorstorlek 1?<br />
Sätt n/2 k =1=>k=logn.<br />
I varje upplaga av rekursionen utförs ett konstant arbete = O(1).<br />
I värsta fall blir det alltså O(1)*log n = O(log n)<br />
AD, <strong>Rekursion</strong> 35<br />
Söndra och härska (divide and conquer)<br />
Teknik för konstruktion av rekursiva algoritmer.<br />
Söndra: Mindre delproblem (två eller flera) löses<br />
rekursivt (utom basfallen).<br />
Härska: Lösningen till det ursprungliga problemet<br />
konstrueras med hjälp av lösningarna till<br />
delproblemen.<br />
Söndra- och härska-algoritmer leder till rekursiva metoder<br />
som gör minst två rekursiva anrop.<br />
• Är ibland en framkomlig väg när det är svårt att<br />
konstruera iterativ algoritm<br />
• Leder ibland till effektivare algoritm i fall där även<br />
iterativ algoritm finns<br />
AD, <strong>Rekursion</strong> 36
Söndra och härska, ex<br />
Problem: sortera en vektor a med n tal.<br />
Söndra och härska teknik:<br />
1. Dela vektorn i två lika stora halvor<br />
2. Sortera (rekursivt) första halvan a[0..n/2]<br />
Söndra!<br />
Sortera (rekursivt) andra halvan a[n/2+1..n-1]<br />
3. Slå samman de båda sorterade delvektorerna så att<br />
Härska!<br />
hela vektorn a[0..n–1] blir sorterad<br />
Det är i härska-steget som själva algoritmkonstruktionen ligger.<br />
Söndra-stegen är bara rekursiva anrop.<br />
I detta fall visar det sig vara enkelt att hitta en (linjär) metod för steg 3.<br />
Sorteringsmetoden, som kallas Mergesort, blir mycket effektiv.<br />
Vi återkommer med detaljer senare i kursen.<br />
AD, <strong>Rekursion</strong> 37<br />
Söndra och härska, ex: Fibonaccitalen<br />
Söndra- och härska-algoritmer är rekursiva algoritmer i<br />
vilka man (utom i basfallen) gör minst två rekursiva<br />
anrop för mindre instanser av samma problem. Detta kan<br />
ibland (men inte alltid!) ge upphov till ineffektivitet. Man<br />
kan komma att beräkna lösningen till samma delproblem<br />
många gånger.<br />
Ex: Fibonaccitalen definieras<br />
F n =F n-1 +F n-2 för nÿ2<br />
F 0 =0<br />
F 1 =1<br />
AD, <strong>Rekursion</strong> 38<br />
Söndra och härska, ex: Fibonaccitalen<br />
<strong>Java</strong>metod direkt enligt definitionen:<br />
public long fib(int n) {<br />
if (n
Dynamisk programmering<br />
Dynamisk programmering (i samband med <strong>implementation</strong><br />
av rekursiva algoritmer) innebär att man i en tabell<br />
håller reda på vilka instanser av problemet man redan löst.<br />
Varje gång man behöver lösningen till en viss instans<br />
kontrollerar man först i tabellen om den redan beräknats. I<br />
så fall hämtas lösningen där, dvs man gör inget rekursivt<br />
anrop. Om lösningen inte finns i tabellen gör man ett<br />
rekursivt anrop och sätter sedan in den beräknade<br />
lösningen i tabellen.<br />
AD, <strong>Rekursion</strong> 41<br />
Dynamisk programmering, ex:<br />
Fibonaccitalen<br />
public long fib(int n) {<br />
long[] table = new long[n+1]; //skapa en tabell<br />
for (int i=0; i