24.10.2014 Views

Rekursion Rekursion Rekursion, implementation i Java Rekursiva ...

Rekursion Rekursion Rekursion, implementation i Java Rekursiva ...

Rekursion Rekursion Rekursion, implementation i Java Rekursiva ...

SHOW MORE
SHOW LESS

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

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!