F6
f1pt
· Rekursion: Kap 8
– längden av ett tåg
= 1+längden av resten av tåget
– n! - iterativt och rekursivt
– antalet nollor i ett heltal,
– skriv ut en sträng i omvänd ordning.
– 3n+1 ”live”
– fibonacci, power(x,n), binärsökning
Nästa gång blir det kap 9, om att använda
färdiga klasser, ingår ej: 9,8 9,9
F7
1
n! iterativt
n!=
{
1
if n =1
1× 2 ×3 ×... × n if n 1
static int fakultet1(int n) {
int fak = 1;
for (int i=2; i<=n; i++) {
fak = fak*i;
}
return fak;
} // end fakultet1
Vad händer vid anropet fakultet(-2)?
n! rekursivt
n!=
{
if n = 1
n × n − 1! if n  1
1
F7
2
Induktiva definitioner
Använder självreferenser
· En kö är antingen tom
eller ett element följt av en kö.
· n! är 1 om n=1 eller n=0
eller n*(n-1)! om n>1
n!=
{
if n = 1
n × n − 1! if n  1
1
Induktiva definitioner består av
1. Ett basfall
2. Ett fall där definitionen (vanligen) minskar
problemets storlek och refererar till sig
själv.
F7
3
Induktionsbevis och rekursion
Induction poof:
to prove that a statement P(n) holds for all
integers n
Base case: Prove P(0) ( or P(small number))
Assumption:
assume that P(n-1) is true (or P(n), P(n/2)...is )
Induction step:
prove that P(n-1) implies P(n) for all n≥0
Recursive computations:
Base case: Solve the problem on inputs of
size 0 (or 1 or some small instance)
Assumption: assume you can solve the
problem on input size less than n (n-1, n/2,..)
Induction step: Show how you can solve it
also on inputs of size n for all n≥1.
Med stöd av detta kan problemet lösas med
hjälp av mindre likadana problem som är en
kopia av oss själva som får ett lite mindre
problem att lösa.
F7
4
n! rekursivt
n!=
{
if n = 1 eller n = 0
n × n − 1! if
n 1
1
static int fakultet( int n ) {
if (n == 1 || n == 0) {
return 1;
} else {
return n * fakultet(n-1);
}
} // end fakultet
------------------------fak = fakultet(3);
if 3==1 then ...
else return 3*fakultet(2)
if 2==1 then ...
else return 2*fakultet(1)
if 1==1 then return 1
else ...
F7
5
Hur sker exekveringen av rekursiva
program?
Vid anrop av fakultet med n>1 genereras ett nytt rekursivt
anrop med n-1 som parameter. Funktionsanrop utförs alltid
före aritmetiska operatorer så argumentet (n-1) beräknas och
funktionen anropas. Aritmetiska operationer i argumentet till
en metod går före allt alltså.
Vi får då en serie väntande instanser av fakultet, var och en
med sin uppskjutna multiplikation. När funktionsanropet
återvänder utförs multiplikationen och retur kan ske.
Varje instans av fakultet har sina egna formella parametrar
och lokala variabler. När en instans väntar lagras dessa i
minnet som en aktiveringspost (en för varje instans av
fakultet). Javas exekveringsmaskineri håller automatiskt reda
på alla aktiveringsposter. Själva programkoden finns bara i en
upplaga så den delas av instanserna.
Beräkningsordning för anropet fakultet(5):
fakultet(5) =
5 * fakultet(4) =
5 * (4 * fakultet(3)) =
5 * (4 * (3 * fakultet(2))) =
5 * (4 * (3 * (2 * (fakultet(1))))
5 * (4 * (3 * (2 * 1)))
5 * (4 * (3 * 2))
5 * (4 * 6)
5 * 24
120
F7
6
Vad händer vid anropet fakultet(-2)?
Satsen if (n == 1) { ... ger
Exception in thread "main"
java.lang.StackOverflowError
Använd tex if (n <= 1) {
eller kasta en exception
Overflow
ps... void main(String[] args) {
// println === System.out.println
println("fak2 = " + fakultet2(10));
println("fak2 = " + fakultet2(15));
println("fak2 = " + fakultet2(20));
} // end main
fak2 =
fak2 =
fak2 =
// med
fak2 =
fak2 =
fak3 =
3628800
2004310016
-2102132736 // Ooooops
long
3628800
1307674368000
2432902008176640000
Overflow har inget med rekursion att göra.
F7
7
Det måste inte vara n-1
och man har ofta flera slutvillkor
Uppgift: beräkna antalet nollor i ett tal.
Idé: tag bort sista siffran.
Om sista siffran är en nolla så
1 + antalet nollor i talet som är kvar
annars
0 + antalet nollor i talet som är kvar
*******************************
ps... int numberOfZeros(int n) {
if (n < 0) {
return numberOfZeros(-n);
} else if (n == 0) {
return 1;
} else if (n < 10) {
return 0;
} else if (n%10 == 0) {
return 1+ numberOfZeros(n/10);
} else {
return numberOfZeros(n/10);
}
}
F7
8
Skriv ut en sträng i omvänd ordning.
Induktionsantagande: Antag att vi kan lösa
problemet för n-1 tecken.
Vi kan välja ut de n-1 tecknen på n olika sätt
men 2 av dessa förefaller naturligast:
1. de n-1 första tecknen i strängen eller
2. de n-1 sista tecknen i strängen.
Alternativ 1:
reverse(str)
if str.length<=1 then print str
else
print (last character)
reverese(n-1 first chars in str)
Alternativ 2:
reverse(str)
if str.length<=1 then print str
else
reverse(n-1 last chars in str)
print (first character)
Vanligen bättre att börja i slutet
F7
9
3n+1 problemet
Ibland ser problemet ut att inte bli mindre
(tren = Tre N Plus Ett problemet)
{
nothing
if n = 1
tren  n = tren 3 × n  1 if n odd
if n even
tren  n /2
Antag att n skrivs ut i varje anrop.
Givet indata 22 så kommer följande sekvens
av tal att genereras:
22 11 34 17 52 26 13 40 20 10 5 16 8 4 2 1
Givet ett indata n så är det möjligt att
bestämma antalet tal som skrivs ut innan
algoritmen terminerar. Detta brukar kallas
cykellängden för n. I exemplet ovan är
cykellängden för 22 lika med 16 (inkl 22, 1).
Man tror att algoritmen ovan terminerar (och ettan skrivs ut)
för alla heltals indata. Men trots att algoritmen är ganska
enkel är det okänt om det verkligen är så. Man har dock
verifierat att det är så för alla heltal sådana att
6
0  n 10 och för många fler större tal.
Detta är en viktig egenskap eftersom vi måste veta att
rekursionen “tar slut”. Algoritmen fungerar alltså åtminstone
6
för tal 0  n 10
F7
10
3n+1 problemet rekursivt
{
nothing
if n = 1
tren  n = tren 3 × n  1 if n odd
if n even
tren  n /2
// Skriver ut 3n+1 sekvensen
static void treNPlusEtt1(int n) {
// en sidoeffekt
System.out.print(n + " ");
if (n == 1) {
; // do nothing
} else if (n%2 != 0) { // udda n
treNPlusEtt1(3*n+1);
} else {
treNPlusEtt1(n/2);
}
} // end treNPlusEtt1
Vid körning så skrivs
22 11 34 17 52 26 13 40 20 10 5 16 8 4 2 1
F7
11
3n+1 problemet: cykel-längden
Men nu var det cykellängden som var intressant
static int treNPlusEtt3(int n) {
// en sidoeffekt
System.out.print(n + " ");
if (n == 1) {
return 1;
} else if (n%2 != 0) { // udda n
return 1+ treNPlusEtt3(3*n+1);
} else {
return 1+ treNPlusEtt3(n/2);
}
} // end treNPlusEtt3
I huvudprogrammet:
System...println(treNPlusEtt3(22));
22 11 34 17 52 26 13 40 20 10 5 16 8 4 2 1
16
F7
12
Vad betyder algoritmen?
Skriv en metod som beräknar xn, n positivt
heltal dvs en egen pow(x, n) som finns
fördefinierad i java.lang.Math
power(x, n) iterativt, Idé: x*x* ... *x
static double power1(int x, int n) {
double tmp = 1;
for ( int i=1; i<=n; i++ ) {
// nbr1=nbr1+1; //counter
tmp = tmp * x;
}
return tmp;
} // end power1
power(x, n) rekursivt Idé: x∗x(n-1)
static double power2(int x, int n) {
if (n == 0) {
return 1;
} else {
// nbr2=nbr2+1; //counter
return x * power2(x, n-1);
}
} // end power2
F7
13
Hmmm, fortfarande n st multiplikationer dvs
egentligen samma algoritm som den iterativa
men rekursivt. Men man måste förstå
problemet. Hmmm
n
x =x
n /2
∗x
n/ 2
=x
n /2 2

power(x, n) effektiv rekursiv lösning
static double power3(int x, int n) {
if (n == 0) {
return 1;
} else {
// nbr3=nbr3+1; // counter
double tmp = power3(x, n/2);
if (n%2 == 0) { // n är jämnt
return tmp * tmp;
} else {
return tmp * tmp * x;
}
}
} // end power3
log n multiplikationer!
Vi kan naturligtvis skriva om den iterativt med
samma effektivitet också. Det handlar inte om
rekursion/iteration utan om algoritmen.
Negativa n? if (n<0) {return 1/power3(x,n);}
F7
14
Tid och att mäta den
I java.lang.System finns metoderna
static long currentTimeMillis()
Returns the current time in milliseconds.
static long nanoTime()
Returns the current value of the most
precise available system timer, in
nanoseconds.
Idé:
start = anrop av någon ovan
arbeta
tid = anrop av någon ovan - start
I java.util.Calendar finns metoden
long getTimeInMillis()
som “Returns this Calendar's time value in
milliseconds.”
(java.util.Calendar är en abstrakt klass och
ett singelton objekt (dvs kan bara finnas en)
se även GregorianCalendar som ärver den)
F7
15
Tid - Calendar
import java.util.*;
public class TestaCal {
ps... void main(String[] args){
Calendar rightNow =
Calendar.getInstance();
long x =
rightNow.getTimeInMillis();
System.out.println(x);
System.out.println(rightNow.get
(Calendar.DAY_OF_WEEK));
}
}
erland % java TestaCal
1125563911913 (tid i millisekunder från 1970)
5
(det var en torsdag, startar på söndag)
utan import-sats kunde vi skriva
java.util.Calendar rightNow =
java.util.Calendar.getInstance();
F7
16
Provkör
long t1 = System.currentTimeMillis();
for (int i = 1; i<= 16000; i++) {
// avkommentera en i taget
//svar = power1(2, 500);
//svar = power2(2, 500);
//svar = power3(2, 500);
//svar = Math.pow(2, 500);
}
long t2 = System.currentTimeMillis();
print("powerX= " + svar +" ");
println("tid = " + (t2-t1));
--------------------------------Antal varv= 16000 x= 2 n= 500
power1 (ite) = 3.2..E150
tid
power2 (rek) = 3.2..E150
tid
power3 (d&c) = 3.2..E150
tid
power4 (Math) = 3.2..E150
tid
=
=
=
=
527
1791
22
43
Antal varv= 16000 x= 2 n= 1000
power1 (ite) = 1.07..E301 tid = 975
power2 (rek) = 1.07..E301 tid = 3933
power3 (d&c) = 1.071..E301 tid = 24
power4 (Math) = 1.07..E301 tid = 67
Antal varv iterativt
= 16000*500.0
Antal anrop rekursivt = 16000*500.0
Antal anrop D&C
= 16000*9.0
F7
17
Binärsökning rekursiv idé
Fältet arr är sorterat och vi söker efter ett
element x i arr. Man jämför den sökta posten,
x, med det mittersta elementet.
Om den sökta posten är mindre än det
mittersta elementet så måste den sökta
posten ligga i den vänstra delen, annars i den
högra.
algoritm:
jämför det sökta elementet med
mittelementet
om x<arr(mitt) => x finns i den undre halvan
om x>arr(mitt) => x finns i den övre halvan
// pseudokod
if first > last then
return -1;
elsif x < arr(mid) then
return bins(arr(first..mid-1), x);
elsif x > arr(mid) then
return bins(arr(mid+1..last ), x);
else return mid;
F7
18
Binärsökning rekursivt kod
I Java måste vi skicka två index som
parametrar som visar i vilken del vi skall söka.
static int bs3 ( int[] arr,
int x,
int first,
int last) {
int mid = (first+last)/2;
if ( first > last ) {
return -1; // not found
} else if (x < arr[mid]) {
return bs3(arr, x, first, mid-1);
} else if (x > arr[mid]) {
return bs3(arr, x, mid+1, last);
} else {
return mid;
}
} // end bs3
och då behöver vi en ”wrapper” också
F7
19
Binärsökning iterativt
// iterativt, sluta vid träff
static int bs2(int[] arr, int x){
int first = 0;
int last = arr.length-1;
int mid = 0;
while (first <= last) {
mid = (first+last)/2;
if (x < arr[mid]) {
last = mid-1;
} else if (x > arr[mid]) {
first = mid+1;
} else {
return mid;
}
}
return -1;
} // end bs2
F7
20
Igen - Algoritmen spelar roll
Antag att ladok innehåller 30.000 studenter
men att bara 10.000 är aktiva och att vi vill
uppdatera de aktiva studenternas poster, en i
taget.
Databasen är sorterad efter tex
personnummer, studenterna som skall
uppdateras är osorterade.
2 algoritmer för att söka efter en student:
Sekvensiell sökning:
tag en studentpost och jämför med
databasens innehåll i tur och ordning, post
efter post. i medeltal krävs 15.000
jämförelser.
Antag att det krävs 1ms för att göra en
jämförelse =>
det tar 15 sek i genomsnitt att söka efter en
student.
8 timmar = 28.800 sekunder / 15sek = 1920
=> ca 2000 sökningar per dag
=> det tar en vecka att söka ut alla stud.
F7
21
Binärsökning:
Man jämför den sökta posten, x, med det
mittersta elementet.
Om den sökta posten är mindre än det
mittersta elementet så måste den sökta
posten ligga i den vänstra delen, annars i den
högra.
1 jämförelse “kastar” 15.000 poster!
=> det krävs 15 jämförelser att hitta en
student
=> en sökning tar 15ms = 0.015 sek
=> 10.000 sökningar tar 2,5 minuter
Effektivitet är alltså viktigt men när man
konstruerar algoritmer så försöker man inte
första hand konstruera en effektiv algoritm
utan en som fungerar. Behöver den sedan vara
effektivare så effektiviserar man dvs funderar
över val av datastrukturer och algoritmer.
F7
22
Ibland går det inte bra
{
om n  2
fib  n = 1
fib  n −1 fib  n −2
om n > 2
int fib(int n) { // pseudokod
if n <= 2 then
return 1;
else
return fib(n-1) + fib(n-2);
end if;
end fib;
1 1 2 3 5 8 13 21 34 55 89 ...
% time java Fibonacci
fib(20) = 10946 och tar 0.300u
fib(30) = 1346269 och tar 0.440u
fib(40) = 165580141 och tar 16.040u
fib(45) = 1836311903 och tar 173.990u
T(1)=1, T(n)=T(n–1)+T(n–2)
T  n =
1
5
n
Φ where Φ ≈ 1,62
F7
23
ett exekveringsträd för fib
Samma funktionvärde beräknas flera gånger!
I det här fallet är iteration vida överlägset
rekursion men det beror inte på rekursionen i
sig utan alltså på att samma delproblem
beräknades flera gånger.
Det finns en teknik för att hantera de
upprepade beräkningarna som ibland inträffar
med rekursiva algoritmer som kallas dynamisk
programmering (se en algoritmkurs).
F7
24