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