Datastrukturer föreläsning 2 1
De som vill ha en labkamrat möts här framme i pausen Övningsgrupper: efternamn som börjar på A-J: EC, Arnar Birgisson K-Ö: ED, Staffan Björnesjö 2
Förra gången Vi jämförde effektiviteten hos olika sorteringsalgoritmer genom att räkna antalet jämförelser. Vi diskuterade både bästa och värsta fallet, och även medelfallet. Vi införde begreppet asymptotisk komplexitet (O-komplexitet). Vi började diskutera hur man skriver polymorfa sorteringsalgoritmer i Java (deltypspolymorfism) 3
Polymorf insättningssortering void insertionsort(comparable[] cards) { for (int i = 1; i < cards.length; i++) for (j = i; j > 0 && cards[j].compareto(cards[j-1]) < 0; j--) swap(cards,j,j-1); } Denna generiska metod kan instantieras med alla deltyper till Comparable. Hur gör man om man vill sortera korten i en annan ordning? 4
Två gränssnitt för jämförelser Gränssnittet Comparable (i java.lang) Gränssnittet Comparator (i java.util) 5
Gränssnittet Comparator Gränssnittet Comparable (i java.lang) kräver att man definierar en jämförelsemetod compareto(obj o) den naturliga ordningen mellan objekten i klassen och andra objekt. Gränssnittet Comparator (i java.util) kräver att man definierar en jämförelsemetod compare(obj o1,o2) mellan två godtyckliga objekt. 6
Gränssnittet Comparator public interface Comparator { public int compare(object o1, Object o2); (det finns en metod till som vi inte bryr oss om) } compare(o1,o2) < 0 omm o1 är mindre än o2 = 0 omm o1 är lika med o2 > 0 omm o1 är större än o2 7
En komparator för kort class CardComparator implements Comparator{ int compare(object o1, Object o2) { Card c1 = (Card)o1; Card c2 = (Card)o2; return 13*(c1.suit c2.suit) + c1.value - c2.value; } } 8
Polymorf sortering med Comparator void insertionsort(object[] cards, Comparator com){ for (int i = 1; i < cards.length; i++) for (j = i; j > 0 && com.compare(cards[j],cards[j-1]) < 0; j--) swap(cards,j,j-1); } Vi kan nu sortera korten med avseende på olika ordningar genom att använda olika komparatorer! 9
Polymorf sortering i Haskell insertionsort :: Ord a => [a] -> [a] class Eq a => Ord a where compare :: a -> a -> Ordering (<) :: a -> a -> Bool data Ordering = LT EQ GT Klassen Ord har ungefär samma roll i Haskell som gränssnittet Comparable har i Java. Kan vi sortera med avseende på olika ordningar i Haskell (som med Comparator)? 10
Polymorf sortering i Haskell med komparator Om vi gör ordningen till ett argument kan vi sortera med avseende på godtycklig ordning: insertionsort2 :: (a -> a -> Ordering) -> [a] -> [a] compare -funktionen blir ett argument, vi får en högre ordningens funktion. Jfr Comparator-versionen i Java! Innan hade vi alltså insertionsort :: Ord a => [a] -> [a] data Ordering = LT EQ GT 11
Java 1.5 med generics Vi har hittills hållit oss till Java 1.4. I Java 1.5 har man introducerat generiska typer och generiska metoder ( the generics framework ). Se G&T 2.5.2. Med hjälp av dem kan man undvika många typkonverteringar ( casts ). Generiska typer och metoder i Java motsvarar ungefär polymorfa typer och funktioner i Haskell. (Ibland kallar man detta parametrisk polymorfism) 12
Generisk version av Comparable interface Comparable<E> { public int compareto(e o); } Indata måste ha referenstypen E. Jfr vår tidigare version där indata är godtyckligt objekt: interface Comparable { int compareto(object o); } 13
Generisk version av Comparable interface Comparable<E> {public int compareto(e o);} E är en formell typparameter. Den kan instantieras med godtycklig referenstyp (dock inte med primitiv typ) class CInt implements Comparable<Integer> { int j; public int compareto(integer o) { return j o.intvalue(); } } 14
Card i Java 1.5 class Card implements Comparable<Card> { int suit; int value; int compareto(card c) { return 13*(suit c.suit) + (value c.value); } } I Java 1.4 hade vi haft compareto(object c) och vi hade behövt konvertera c till Card. Det behöver vi inte längre! 15
Generisk sortering med Comparator <C> void insertionsort(c[] cards, Comparator<C> com) { for (int i = 1; i < cards.length; i++) for (j = i; j > 0 && com.compare(cards[j],cards[j-1]) < 0; j--) swap(cards,j,j-1); } 16
Syntax för generiska gränssnitt, klasser och metoder interface Interface<E> { } interface Interface<E extends RType> { } class Class<E> { } class Class<E extends RType> { } <E> OutType method(intype in) { } <E extends RType> OutType method(intype in) { } 17
Sammanfattning Vi har jämfört effektiviteten hos olika sorteringsalgoritmer genom att räkna antalet jämförelser. Vi har diskuterat både bästa och värsta fallet. Vi har bestämt asymptotiska komplexiteter (Okomplexiteter). Vi har skrivit polymorfa sorteringsmetoder genom att använda gränssnitten Comparable och Comparator. Vi har skrivit generiska versioner av dessa i Java 1.5. Vi har skrivit polymorfa sorteringsfunktioner i Haskell genom att använda klassen Ord och genom att ge jämförelsefunktionen som ett argument. Notera skillnaden mellan deltypspolymorfism (overloading i Haskell) och parametrisk polymorfism (generics i Java). 18
Pseudokod igen notation för att beskriva algoritmer utan att ge alla detaljer blandning av formell notation och naturligt språk mer lättläst än riktigt programspråk inte bundet till visst programspråk viktig abstraktionsnivå att behärska 19
Exempel på pseudokod i G&T Algorithm arraymax(a,n) Input: An array A of n elements, n > 0 Output: The maximum element of A cm A[0] for i 1 to n-1 do if A[i] > cm then cm A[i] return cm 20
Samma algoritm i Java int arraymax(int[] A, int n) { int cm = A[0]; for(int i = 1, i < n, i++) { if(a[i] > cm) cm = A[i]; } return cm; } inte så stor skillnad för Goodrich & Tamassias pseudokod är ganska Java-inspirerad! 21
Hur jämför man effektivitet? Mät tiden, vilken är snabbast? Mät på flera indata! Rita kurvor över hur tiden beror på indatastorleken! Är mätningarna jämförbara? Samma dator? Samma belastning? Samma kompilator? 22
Räkna maskininstruktioner int arraymax(int[] A, int n) { int cm = A[0]; // 2 instruktioner for(int i = 1, i < n, i++){ // 1 + n + (n - 1) if(a[i] > cm) // 2(n - 1) cm = A[i]; // 2(n - 1) bara ibland } // return cm; // 1 } Totalt 6n 1 instruktioner i värsta fall och 4n + 1 i bästa fall. Antag instruktionerna: i = n, A[i], i < j, i++, return 23
Tidsanalys av algoritmer Uppskatta exekveringstiden genom att räkna antalet primitiva operationer som exekveras för olika indata. Uppskatta hur exekveringstiden T(n) beror på storleken n på indata. Gör asymptotisk analys av T(n). Växer den linjärt, kvadratiskt, logaritmiskt, Analysera värsta fallet, bästa fallet, medelfallet. 24
T(n) är inte entydigt bestämt T(n) = exekveringtiden n = indatastorleken 25
Linjär tidskomplexitet En algoritm har linjär tidskomplexitet omm det finns c och n 0 så att T(n) < c n, n > n 0 exekveringstiden konstanten T(n) begränsas av en linjär funktion med avseende på indatastorleken n Ex: arraymax tag c=6 och n 0 = 0 26
Tillväxttakten för exekveringen Byt kompilator, dator, etc Påverkar T(n) med en konstant faktor En linjär algoritm förblir linjär! Bara konstanten c ändras. 27
Officiell definition av O- notation T(n) är O(f(n)) betyder: Det finns positiva heltal c, n 0 så att T(n) < c f(n) för alla n > n 0 28
O-notation Linjär T(n) är O(n): T(n) < c n Logaritmisk T(n) är O(log n) T(n) < c log n Kvadratisk T(n) är O(n 2 ) T(n) < c n 2 Allmänt T(n) är O(f(n)) T(n) < c f (n) för alla n > n 0 29
Stora-O regler Om f(n) är ett polynom av grad d, så är f(n) O(n d ), dvs vi kan 1. ta bort termer av lägre grad 2. ta bort konstanter Använd minsta möjliga komplexitetsklass Säg 2n är O(n) istället för 2n är O(n 2 ) 30
Vanliga komplexitetsklasser O(1) konstant tid O(log n) logaritmisk tid O(n) linjär tid O(n log n) n log n -tid O(n 2 ) O(n 3 ) O(2 n ) kvadratisk tid kubisk tid exponentiell tid 31
Sökning i ett fält Linjär sökning - O(n) Binärsökning i ett sorterat fält är logaritmisk O(log n) 32
Linjär och logaritmisk tid 33
Logaritmer Den reella logaritmfunktionen är invers till exponentialfunktionen: log 2 n = x omm n = 2 x Heltalslogaritmen log 2 n är antalet gånger som man kan halvera n. (Avrunda nedåt när man halverar udda tal.) Hur många gånger kan man halvera 9? 34
Logaritmer med olika bas log 3 n = x omm n = 3 x log 3 n är antalet gånger man kan tredela n log 10 n = x omm n = 10 x ln n = log e n = x omm n = e x 35
O(log n) är oberoende av basen Varför? O(log b n) = O(log c n) log b n = (log b c) (log c n) Skiljer bara med en konstant faktor log b c (den beror inte på n)! Exempel: log 2 n = (log 2 4) (log 4 n) = 2 log 4 n 36
Varför är log b n = (log b c) (log c n)? log b n = (log b c)(log c n) omm b (log b n) = b (log b c)(log c n) 37
Rekursion Rekursion: när en metod anropar sig själv Fakultetsfunktionen n! = 1 2 3 (n-1) n Rekursiv funktion i Haskell factorial n = if n == 0 then 1 else n * factorial(n-1) Rekursiv metod i Java public static int factorial(int n) { if (n == 0) return 1; // base case } else return n * factorial(n-1); // recursive case 38
Ett exempel på linjär rekursion Algorithm LinearSum(A, n): Input: An integer array A and an integer n, such that A has at least n elements Output: The sum of the first n integers in A if n = 1 then return A[0] else return LinearSum(A, n - 1) + A[n - 1] anropal return 15 + A [ 4 ] = 15 + 5 = 20 LinearSum ( A, 5 ) anropa return 13 + A [ 3 ] = 13 + 2 = 15 LinearSum ( A, 4 ) anropa return 7 + A [ 2 ] = 7 + 6 = 13 LinearSum ( A, 3 ) anropa return 4 + A [ 1 ] = 4 + 3 = 7 LinearSum ( A, 2 ) Haskell: linearsum [a] = a linearsum (a:as) = a + linearsum as anropa return A [ 0 ] = 4 LinearSum ( A, 1 ) A= 4 3 6 2 5 39
Ett exempel till på linjär rekursion Algorithm ReverseArray(A, i, j): Input: An array A and nonnegative integer indices i and j Output: The reversal of the elements in A starting at index i and ending at j if i < j then Swap A[i] and A[j] ReverseArray(A, i + 1, j - 1) return Beräkningen har bara ett rekursivt anrop och skillnaden mellan indexen i och j minskar hela tiden. När i < j terminerar algoritmen. 40
Binär Rekursion Binär rekursion inträffar när vi har två rekursiva anrop för varje icke-basfall. Risk för att antalet anrop växer exponentiellt! 41
Beräkna fibonaccital Fibonaccitalen är rekursivt definierade: F 0 = 0 F 1 = 1 F i = F i-1 + F i-2 för i > 1. Som en rekursiv algoritm i pseudokod Algorithm BinaryFib(k): Input: Nonnegative integer k Output: The kth Fibonacci number F k if k < 2 then return k else return BinaryFib(k - 1) + BinaryFib(k - 2) 42
Fibonacci i Haskell fib 0 = 1 fib 1 = 1 fib n = fib (n - 1) + fib (n 2) Tfib 0 = 1 Tfib 1 = 1 Tfib n = Tfib (n 1) Tfib (n 2) + 1 43
Analys Låt n k stå för antalet funktionsanrop som BinaryFib(k) gör. n 0 = 1 n 1 = 1 n 2 = n 1 + n 0 + 1 = 1 + 1 + 1 = 3 n 3 = n 2 + n 1 + 1 = 3 + 1 + 1 = 5 n 4 = n 3 + n 2 + 1 = 5 + 3 + 1 = 9 n 5 = n 4 + n 3 + 1 = 9 + 5 + 1 = 15 n 6 = n 5 + n 4 + 1 = 15 + 9 + 1 = 25 n 7 = n 6 + n 5 + 1 = 25 + 15 + 1 = 41 n 8 = n 7 + n 6 + 1 = 41 + 25 + 1 = 67. Vi ser att antalet anrop mer än fördubblas för vartannat värde på n k. D.v.s., n k > 2 k/2. Algoritmen är exponentiell! 44
Ett exempel på binär rekursion som trots allt är O(n) Problem: addera alla tal i fältet A: Algorithm BinarySum(A, i, n): Input: An array A and integers i and n Output: The sum of the n integers in A starting at index i if n = 1 then return A[i ] return BinarySum(A, i, n/ 2) + BinarySum(A, i + n/ 2, n/ 2) Exempel trace: 0, 8 0, 4 4, 4 0, 2 2, 2 4, 2 6, 2 0, 1 1, 1 2, 1 3, 1 4, 1 5, 1 6, 1 7, 1 45
En linjär fibonaccialgoritm Använd linjär rekursion: Algorithm LinearFibonacci(k): Input: A positive integer k Output: Pair of Fibonacci numbers (F k, F k-1 ) if k = 1 then return (k, 0) else (i, j) = LinearFibonacci(k - 1) return (i +j, i) Är O(k). Skriv denna algoritm i Haskell! 46