Innføring i matematisk analyse av algoritmer



Like dokumenter
PG4200 Algoritmer og datastrukturer Forelesning 2

PG4200 Algoritmer og datastrukturer forelesning 3. Lars Sydnes 29. oktober 2014

Algoritmer - definisjon

Algoritmer - definisjon

Ninety-nine bottles. Femte forelesning. I dagens forelesning: Mest matematiske verktøy. Først: Asymptotisk notasjon. Så: Rekurrensligninger.

NITH PG4200 Algoritmer og datastrukturer Løsningsforslag Eksamen 4.juni 2013

Kompleksitetsanalyse Helge Hafting Opphavsrett: Forfatter og Stiftelsen TISIP Lærestoffet er utviklet for faget LO117D Algoritmiske metoder

Algoritmer og datastrukturer Kapittel 1 - Delkapittel 1.8

Spenntrær, oppsummert: Kruskal: Traverserer ikke. Plukker kanter i hytt og vær Prim: Legger alltid til den noden som er nærmest treet

MAT1030 Diskret Matematikk

Forelesning 29: Kompleksitetsteori

Algoritmeanalyse. (og litt om datastrukturer)

Introduksjon til Algoritmeanalyse

Løsningsforslag til eksamen i PG4200 Algoritmer og datastrukturer 10. desember 2014

Algoritme-Analyse. Asymptotisk ytelse. Sammenligning av kjøretid. Konstanter mot n. Algoritme-kompeksitet. Hva er størrelsen (n) av et problem?

Når Merge sort og Insertion sort samarbeider

Rekursiv programmering

LO118D Forelesning 2 (DM)

MAT1030 Diskret Matematikk

Kompleksitetsanalyse

Matriser. Kapittel 4. Definisjoner og notasjon

Norges Informasjonsteknologiske Høgskole

PG4200 Algoritmer og datastrukturer Forelesning 3 Rekursjon Estimering

Forelesning 30: Kompleksitetsteori

Kjøretidsanalyse. Hogne Jørgensen

INF2220: Forelesning 2

Anbefalte forkunnskaper Studentene forutsettes å kunne programmere, for eksempel ved å ha tatt TDT4100 Objektorientert programmering.

Rekurrens. MAT1030 Diskret matematikk. Rekurrens. Rekurrens. Eksempel. Forelesning 16: Rekurrenslikninger. Dag Normann

Pensum: 3. utg av Cormen et al. Øvingstime: I morgen, 14:15

Forelesning 1 mandag den 18. august

TDT4105 Informasjonsteknologi, grunnkurs

Datastrukturer for rask søking

Enkel matematikk for økonomer 1. Innhold. Parenteser, brøk og potenser. Ekstranotat, februar 2015

LØSNINGSFORSLAG, EKSAMEN I ALGORITMER OG DATASTRUKTURER (IT1105)

Søkeproblemet. Gitt en datastruktur med n elementer: Finnes et bestemt element (eller en bestemt verdi) x lagret i datastrukturen eller ikke?

INF2220: Forelesning 1. Praktisk informasjon Analyse av algoritmer (kapittel 2) (Binær)trær (kapittel )

Hva er en algoritme? INF HØSTEN 2006 INF1020. Kursansvarlige Ragnar Normann E-post: Dagens tema

INF2220: Time 12 - Sortering

Analyse og metodikk i Calculus 1

Rekursiv programmering

NORGES INFORMASJONSTEKNOLOGISKE HØGSKOLE PG4200 Algoritmer og datastrukturer

PG4200 Algoritmer og datastrukturer forelesning 10. Lars Sydnes 21. november 2014

UNIVERSITETET I OSLO

MED TIDESTIMATER Løsningsforslag

Kontinuasjonseksamen i fag SIF8010 Algoritmer og Datastrukturer Torsdag 9. August 2001, kl

Lineære likningssystemer og matriser

NITH PG4200 Algoritmer og datastrukturer Løsningsforslag Eksamen 4.juni 2013

Divide-and-Conquer. Lars Vidar Magnusson

INF Algoritmer og datastrukturer. Hva er INF2220? Algoritmer og datastrukturer

Norges Informasjonsteknologiske Høgskole

Forelesning 30. Kompleksitetsteori. Dag Normann mai Informasjon. Oppsummering

4 Matriser TMA4110 høsten 2018

MAT1030 Diskret matematikk

Forelesning 31. Dag Normann mai Informasjon. Kompleksitetsteori

MAT1030 Forelesning 17

Analyse av Algoritmer

Enkel matematikk for økonomer. Del 1 nødvendig bakgrunn. Parenteser og brøker

Mengder, relasjoner og funksjoner

9 Potenser. Logaritmer

Utførelse av programmer, metoder og synlighet av variabler i JSP

Oppgave 1. Sekvenser (20%)

De hele tall har addisjon, multiplikasjon, subtraksjon og lineær ordning, men ikke divisjon.

INF1010 notat: Binærsøking og quicksort

Divide-and-Conquer II

INF oktober Dagens tema: Uavgjørbarhet. Neste uke: NP-kompletthet

MAT1030 Forelesning 30

Kompleksitetsteori reduksjoner

MAT1030 Diskret matematikk

PG 4200 Algoritmer og datastrukturer Innlevering 2

MAT1030 Diskret matematikk

Innledning. MAT1030 Diskret matematikk. Kapittel 11. Kapittel 11. Forelesning 33: Repetisjon

MAT1030 Forelesning 14

Definisjon: Et sortert tre

Kapittel 6: Funksjoner

MAT1030 Forelesning 13

Kapittel 6: Funksjoner

Repetisjon og mer motivasjon. MAT1030 Diskret matematikk. Repetisjon og mer motivasjon

Forelesning 14. Rekursjon og induksjon. Dag Normann februar Oppsummering. Oppsummering. Beregnbare funksjoner

INF2220: Time 8 og 9 - Kompleksitet, beregnbarhet og kombinatorisk søk

wxmaxima Brukermanual for Matematikk 1T Bjørn Ove Thue

Vekstrater og eksponentiell vekst ECON 2915 Vekst og næringsstruktur

Hans Petter Hornæs,

EKSAMEN med løsningsforslag

1 C z I G + + = + + 2) Multiplikasjon av et tall med en parentes foregår ved å multiplisere tallet med alle leddene i parentesen, slik at

Tallregning og algebra

Lær å bruke Microsoft Mathematics, Matematikk-tillegget i Word og WordMat. Av Sigbjørn Hals

Representasjon av tall på datamaskin Kort innføring for MAT-INF1100L

Kapittel 8. Potensregning og tall på standardform

MA1301 Tallteori Høsten 2014

Notat for oblig 2, INF3/4130 h07

PG4200 Algoritmer og datastrukturer Forelesning 10

INF Algoritmer og datastrukturer

Om Kurset og Analyse av Algoritmer

Øvingsforelesning 3: Splitt og hersk. Daniel Solberg

Forelesning 13. Funksjoner. Dag Normann februar Opphenting. Opphenting. Opphenting. Opphenting

INF2220: Forelesning 1

Forelesning 33. Repetisjon. Dag Normann mai Innledning. Kapittel 11

UNIVERSITETET I OSLO

MAT1030 Diskret matematikk

Transkript:

DUMMY Innføring i matematisk analyse av algoritmer Lars Sydnes September 2014 Dette er ment som et supplement til læreboka Algorithms, 4.utgave av Sedgewick & Wayne, heretter omtalt som læreboka. Etter innledningen tar notatet for seg lærebokas tildenotasjon, O-notasjon og noen viktige funksjoner. Deretter defineres ulike vekstklasser før vi runder av med noen eksempler på praktisk bruk av O-notasjon Innhold 1 Innledning 2 2 Vekstfunksjoner 4 3 Tilde-notasjon 5 4 O-notasjon 8 5 Noen matematiske funksjoner 10 6 Sammenheng mellom O-notasjon og -notasjon 12 7 De viktigste vekstklassene 16 8 Enkle estimater med O-notasjon 19

1 Innledning Dataprogrammer er prinsipielt sett forutsigbare 1 (deterministiske). Det betyr at hvis vi kjenner til hvilke input et dataprogram får, så kan vi tallfeste hvilke grunnleggende operasjoner som er involvert i gjennomføringen av programmet og deres frekvens, det vil si hvor mange ganger de forekommer. Ved fravær av ytre påvirkninger er det prinsipielt sett mulig å forutsi kjøretiden til dataprogrammer. Dessverre er det oftest meget vanskelig å sette dette ut i livet. Vi vil fokusere på å komme frem til analyser som det er mulig å gjennomføre analyser som gir nyttige svar. Det viser seg at disse to kravene går som hånd i hanske. Fellesnevneren er forenkling. 1.1 Tre ulike presisjonsnivåer I dette notatet møter vi tre ulike presisjonsnivåer, her illustrert med påstander om kjøretiden i koden beskrevet i notatet threesum.pdf. 1.1.1 Eksakt beregning Påstanden Frekvensen i den indre loopen i ThreeSum.java er n(n 1)(n 2) 6 er både vanskelig å begrunne og veldig presis. Samtidig inneholder den mye informasjon som vi ikke er interessert i. Hvem bryr seg vel om forskjellen mellom n(n 1)(n 2) og n(n 1)(n 3)? 1 Vi kan gjøre dataprogrammer uforutsigbare ved å åpne dem opp mot omverdenen: Eksempel 1: Interaktive programmer: Når et dataprogram er drevet av interaksjon med en bruker, er det vanskelig å forutsi hva som vil skje. Eksempel 2: Tilfeldige tall: Datamaskiner kan ikke lage tilfeldige tall, men de kan lage tallfølger som ser tilfeldige ut, for eksempel ved hjelp av java.util.random. En metode for å lage virkelig tilfeldige tall, er å lese inn data fra en eller annen støykilde. Et eksempel på dette i linux-verdenen er /dev/random og /dev/urandom. Se f.eks /dev/random på wikipedia. Dersom man definerer brukerinteraksjon og avlesning av støy som en del av dataprogrammets input, så kan vi fortsatt si at dataprogrammer er helt forutsigbare. En annen måte å forholde seg til dette er å isolere programelementer som interagerer direkte med omverdenen fra programelementer som ikke innebærer direkte interaksjon. De ikke-interagerende delene kan ofte underkastes en matematisk analyse.

1.1.2 Asymptotisk estimat Vi skal innføre et begrepsapparat som lar oss si at Frekvensen i den indre loopen i ThreeSum.java vokser som 1 6 n3 er noe enklere å komme fram til, samtidig som den inneholder informasjon som er enkel å forholde seg til. Denne tankegangen er knyttet til -notasjonen (tildenotasjon), som vi skal definere i dette notatet. Hovedpoenget med å innføre -notasjon er at den tillater oss å ignorere en del detaljer. 1.1.3 O-notasjon Påstanden TheeSum.java har kjøretid av orden O(n 3 ). er forankret i O-notasjon, som vi også skal se på i dette notatet. En hovedforskjell mellom -notasjon og O-notasjon er at vi ser bort fra konstante faktorer når vi bruker O-notasjon, så vi skiller ikke mellom O ( 1 6 n3) og O(n 3 ). 1.2 Typiske variabler Når vi skal drøfte slike spørsmål, isolerer vi én størrelse som vi ønsker å måle, estimere eller forutsi. Vi definerer en responsvariabel. I dette notatet kaller vi denne for T (oppkalt etter tid), men vi husker på at den kan representere ulike ting avhengig av hva vi er interessert i: Kjøretid Antall elementære operasjoner Forbruk av minne Siden dataprogrammets oppførsel er bestemt av programmets input, kan vi si at T er en funksjon av input: T = f(input) For å forenkle analysen vil vi innføre begrepet problemstørrelse. Problemstørrelsen er et tall som vi som regel vil kalle n, og det kan representere mange ulike ting, avhengig av sammenhengen: antall elementer i en liste, antall flyplasser i et nettverk av flyruter,

antall filer i et filsystem, antall spillere i et online-spill. Vi kunne ønske at responsen T kun avhang av problemstørrelsen n, slik at T = f(n). En slik sammenheng er det enkelt å forholde seg til, blant annet ved hjelp av grafiske metoder. Dessverre finnes det sjelden så enkle sammenhenger: Det finnes ofte mange ulike inputs med samme problemstørrelse (n). Ulike inputs med samme problemstørrelse kan gi ulik respons (T ). I dette notatet kan vi ha tre ulike muligheter i bakhodet: La f(n) være gjennomsnittsverdien for T blant inputs med størrelse n. På engelsk: Average case. La f(n) være den høyeste verdien for T blant inputs med størrelse n. På engelsk: Worst case. La f(n) være den laveste verdien for T blant inputs med størrelse n. På engelsk: Best case. Når vi i det følgende ser uttrykk av typen T = f(n) må vi huske på at det kan ligge mange ulike ting bak, for eksempel: n kan betegne antallet elementer i en liste. T kan betegne gjennomsnittlig kjøretid for søk i listen. T kan betegne det maksimale antallet kall av compareto når man sorterer en liste med n elementer. 2 Vekstfunksjoner I de neste avsnittene skal vi glemme alt det som ligger bak, og konsentrere oss om matematikken. Det betyr at vi vil studere funksjoner f(n) som ofte dukker opp i samband med analyse av algoritmer. De fleste algoritmer er effektive når problemstørrelsen er liten. Det spørsmålet vi vil fokusere på er Hvor raskt vokser responsen T (n) med problemstørrelsen n?

Vi vil spesielt se på funksjoner f(n) som er positive for store n. Det vil si at det finnes en nedre grense N slik at f(n) > 0 når n > N. I dette notatet vil vi kalle slike funksjoner for vekstfunksjoner. 3 Tilde-notasjon Her innfører vi en notasjon som lar oss skrive og vi skal se at dette gir oss T 1 6 n3 istedenfor T = mer oversiktlige formler formler som er enklere å tolke og sammenligne enklere regning. n(n 1)(n 2), 6 3.1 Definisjon Vi sier (jfr. læreboka) at f(n) g(n) dersom f(n) 1 når n vokser. (1) g(n) Uttrykket f(n) g(n) leser vi som en forkortelse for f(n) vokser som g(n). Det at f(n) vokser som g(n) betyr som regel at det er liten praktisk forskjell mellom de to funksjonene for store verdier av n. 3.2 Polynomer og tilde-notasjon Et polynom i n er et uttrykk av typen 3n 4 + 2n 3 3. Dette er et fjerdegradspolynom med tre ledd. Førstegradsleddet er 3, tredjegradsleddet er 2n 3, mens fjerdegradsleddet er 3n 4. Noe andregradsledd finnes ikke i dette polynomet(hvis man insisterer, så kan man gjerne si at annengradsleddet er 0n 2.).

Et polynom av grad d er et uttrykk av typen a 0 + a 1 n + a 2 n 2 + + a d 1 n d 1 + a d n d. (2) i-tegradsleddet i dette polynomet er a i i d. Når vi bruker -notasjon i forbindelse med polynomer er det kun det leddet som har høyest grad som har betydning. a 0 + a 1 n + a 2 n 2 + + a d 1 n d 1 + a d n d a d n d. Dette betyr at et polynom p(n) vokser som et annet polynom q(n) dersom leddene av høyest grad i de to polynomene er identiske. Vi har for eksempel 3n 4 + 2n 3 3 3n 4. 3.2.1 Bevis (frivillig) I avsnittet over så vi noen påstander som krever en begrunnelse som er forankret i definisjonen av -notasjonen. La oss først ta for oss det siste eksempelet. Vi sjekker rett og slett om definisjonen (1) holder. Her lar vi f(n) = 3n 4 + 2n 3 3 og g(n) = 3n 4. Da er f(n) g(n) = 3n4 + 2n 3 3 3n 4 = 3n4 3n 4 + 2n3 3n 4 3 3n 4 = 1 + 2 3 n 1 n 4 De to siste leddene 0 når n vokser. Dermed vil f(n)/g(n) 1 når n vokser. På samme måte kan vi vise at (2) holder: Nå er f(n) = a 0 + + a d n d og g(n) = a d n d, mens f(n) g(n) = a 0 + + a d n d a d n d = a 0 a d n d + a 1 Dette uttrykket går mot 1 når n vokser. a d n (d 1) + + a d 1 a d n 1 + a d a d = a 0 a d n d + a 1 a d n (d 1) + + a d 1 a d n 1 + 1. 3.3 Tilbake til ThreeSum.java I notatet threesum.pdf hevdes det at den indre løkken repeteres n(n 1)(n 2) 6

ganger. Det er en påstand som krever en del utregnig, og vi lar det ligge nå. Hvis vi ganger ut dette uttrykket, ser vi at Frekvens for indre løkke = Vi ser at det er n(n 1)(n 2) 6 = n3 6 n2 2 + n 3 1 6 n3 3.4 Regneregler for -notasjon 3.4.1 Grunnleggende egenskaper Den første egenskapen vi nevner her er at f(n) vokser som g(n) hvis g(n) vokser som f(n): f(n) g(n) hvis g(n) f(n). På fint sier vi at uttrykker en symmetrisk relasjon. Hvis f(n) vokser som g(n) og g(n) vokser som h(n), så vokser f(n) som h(n). f(n) h(n) hvis f(n) g(n) og g(n) h(n). Dette betyr at -notasjonen har mye til felles med =-notasjonen som vi kjenner fra vanlig matematikk: Hvis a = b og b = c, så er a = c. På fint sier vi at uttrykker en transitiv relasjon. 3.4.2 Algebraiske egenskaper Hvis a, b 0 er reelle tall og f 1 (n), f 2 (n), g 1 (n) og g 2 (n) er vektsfunksjoner, så er f 1 (n)f 2 (n), g 1 (n)g 2 (n), af 1 (n) + bf 2 (n) og ag 1 (n) + bg 2 (n) også vekstfunksjoner. Hvis nå så er f 1 (n) g 1 (n) og f 2 (n) g 2 (n), f 1 (n)f 2 (n) g 1 (n)g 2 (n) og af 1 (b) + bf 2 (n) ag 1 (n) + bg 2 (n). (3) Vi kan bruke dette til å forme om uttrykk. For eksempel, medfører (2) at n 2 n 1 n Kombinasjonrgeglene uttrykt i (3) gir nå 1 6 n(n 1)(n 2) 1 6 n3.

4 O-notasjon Her innfører vi en spesiell notasjon for å kunne snakke om at en positiv funksjon f(n) ikke vokser mer dramatisk enn en annen positiv funksjon g(n). Vi sier at f(n) er av orden O(g(n)) dersom det finnes et tall M slik at f(n) Mg(n) når n er tilstrekkelig stor. I denne situasjonen skriver vi f(n) = O(g(n)), eller f(n) g(n). Den følgende figuren illustrerer en situasjon der f(n) g(n). 12000 10000 f(n) g(n) 4g(n) 8000 6000 4000 2000 0 0 20 40 60 80 100 Det at f(n) g(n) kan vi begrunne utifra definisjonen over, med N = 50 og M = 4 (Forutsatt at det ikke oppstår problemer når n > 100). Slik disse begrepene er definert, har vi her tre ekvivalente utsagn: Det finnes tall N, M slik at f(n) Mg(n) når n N (4) f(n) = O(g(n)) f(n) g(n) 4.1 Polynomer og O-notasjon La oss først se på et eksempel: f(n) = 3n 2 + 2n + 4 g(n) = n 2

Hvis n 1, så er n 2 n 1. Dermed er f(n) = 3n 2 + 2n + 4 3n 2 + 2n 2 + 4n 2 = 9n 2 når n > 1. Vi ser altså at f(n) g(n), siden definisjonen (4) holder med M = 9 og N = 1. Vi skal generalisere dette i neste avsnitt. Det vi skal fram til her, er at a 0 + a 1 n + a 2 n 2 + + a k n k n k, altså a 0 + a 1 n + + a k n k = O(n k ). Dette kan begrunnes som over, ved å bruke definisjonen (4) med N = 1, M = a 0 + a 1 + a 2 + + a k : Siden n k n k 1 n 1 når n 1, er a 0 + a 1 n + + a k n k (a 0 + + a k )n k = Mn k når n 1. Dette resulatet kan vi faktisk trekke enda lenger: Hvis m k, så er a 0 + a 1 n + a 2 n 2 + + a k n k n m (5) 4.2 Regneregler for O-notasjon 4.2.1 Grunnleggende egenskaper Det er enklest å uttrykke regnereglene for bruk av O-notasjon ved hjelp av ulikhetstegnet : Hvis g(n) = af(n) så holder (4) for N = 1, M = a. Det betyr at f(n) af(n) altså f(n) = O(af(n)), for alle mulige verdier av tallet a. Vi ser altså at gir en grovere karakterisering enn : Når vi bruker, er konstante faktorer uvesentlige, i motsetning til når vi bruker. En annen viktig observasjon er at f(n) h(n) dersom f(n) g(n) og g(n) h(n). Dette fungerer akkurat som for ulikheter mellom vanlige tall: Hvis a b og b c, så er a c.

4.2.2 Algebraiske egenskaper Relasjonen respekterer også de vanlige algebraiske operasjonene: Hvis a, b 0 er reelle tall og f 1 (n), f 2 (n), g 1 (n) og g 2 (n) er vekstfunksjoner slik at så er f 1 (n) g 1 (n) og f 2 (n) g 2 (n), f 1 (n)f 2 (n) g 1 (n)g 2 (n) og af 1 (n) + bf 2 (n) ag 1 (n) + bg 2 (n). Vi kan nå bruke regnereglene og det vi vet om polynomer til å gjøre estimater som for eksempel n 2 n 1 n og 1 6 n(n 1)(n 2) 1 6 n3 n 3 4.3 Hva vi skal med O-notasjonen Hva skal vi så med O-notasjonen? Kan vi ikke bare bruke -notasjon hele tiden? Senere skal vi se at O-notasjonen forenkler mange beregninger, men her vil vi legge vekt på følgende: Vi kan se bort fra konstante faktorer når vi arbeider med O-notasjon. Dette er noe vi kan oppsummere i formelen f(n) af(n). Siden vi i en kjøretidsanalyse sjelden vil kjenne tidsbruken pr. instruksjon, er dette ofte en god idé: Notasjonen er omtrent like nøyaktig som kunnskapen den skal uttrykke. O-notasjonen støtter enkle worst case-estimater. Når vi skriver f(n) g(n), gir g(n) en øvre grense for hvor dramatisk f(n) kan vokse. Ligning (5) illustrerer dette på en fin måte: Et polynom av grad k m vokser mindre dramatisk enn n m. 5 Noen matematiske funksjoner 5.1 Potensfunksjoner 5.1.1 Repetisjon av potensregning Den n-te potensen av et tall a, a n = a a a (n ganger).

Vi kan også bruke følgende rekursive definisjon: a 0 = 1 a n = a a n 1 ( stoppbetingelse ) ( rekursivt steg ) Potensuttrykket er også definert når eksponenten n er negativ. Vi har for eksempel a 2 = 1 a 2 = 1 a a og 10 3 = 1 10 3 = 1 1000 = 0.001 Når grunntallet a er 0, så er potensuttrykket definert selv om eksponenten n ikke er et heltall. Vi har for eksempel a 1 2 = a, og a 1 4 = 1 4 a. Her har vi de vanlige reglene for potensuttrykk: 1. x a x b = x a+b 2. x a /x b = x a b 3. (x a ) b = x ab 4. x 1 = x 5. x 0 = 1 5.1.2 Potenser og tildenotasjon Det viktigste vi kan si er følgende: n k n m k = m a n b n a = b. 5.1.3 Potenser og O-notasjon Det viktigste vi kan si om potensuttrykk og O-notasjon er følgende: n k n m k m a n b n a b. n k a n a > 1 Uttrykk på formen n k representerer polynomisk vekst av grad k, mens uttrykk på formen a n, b n representerer eksponentiell vekst. Vi ser at grunntallet har mye å si for veksten: Høyere grunntall gir mer dramatisk vekst. Dessuten ser vi at eksponentiell vekst alltid er mer dramatisk enn polynomisk vekst.

6 Sammenheng mellom O-notasjon og -notasjon Sammenhengen mellom O-notajonen og -notasjonen er enkel og grei: men ikke omvendt. f(n) g(n) = f(n) g(n), Det betyr f.eks at hvis vi vet at f(n) 1 6 n3, så kan vi umiddelbart konkludere med at f(n) 1 6 n3 n 3, altså at f(n) har orden O(n 3 ). 6.1 Logaritmer 6.1.1 Repetisjon av logaritmeregning Logaritmen med grunntall a, log a (x) av et tall x er et tall slik at a log a (x) = x og log a a y = y Man kan si at logaritmefunksjonen svarer på spørsmålet: Hvilken eksponent skal jeg opphøye a i for å få verdien x. Vi kan si at svaret er log a (x). 2 Vi ser at logaritmen henger nøye sammen med potensuttrykk. Dette kommer klart til uttrykk i regnereglene for logaritmer, som kan se ut som et speilbilde av de fem regnereglene 1-5 for potenser: 1. log a (xy) = log a (x) + log b (y) 2. log a (x/y) = log a (x) log a (y) 3. log a (x y ) = y log a (x) 4. log a (a) = 1 5. log a (1) = 0 I dette kurset vil møte følgende logaritmefunksjoner. log 2 : Logaritmen med grunntall 2. Læreboka bruker symbolet lg for denne typen logaritmer. Det vil si at 2 lg y = lg(2 y ) = y. log e : Den naturlige logaritmen. Grunntall e 2.718. Vi bruker symbolet ln for denne logaritmen. Det vil si at e ln y = ln(e y ) = y. log 10 : Logaritmen med grunntall 10. Dette er den logaritmen som skjuler seg bak log-tasten på de fleste kalkulatorer. Her er 10 log y = log(10 y ) = y. 2 Dette er et spørsmål som det er overraskende vanlig å komme bort i. I løpet av 1600-tallet ble det laget enorme tabeller som gav svar på slike spørsmål. I nyere tid har vi fått log-knappen på kalkulatoren vår, samt metoder som java.lang.math.log.

Oppgave 1: Søk på log(10) hos google: google log(10). Gjør det samme også med lg(2), lg(4), ln(e), log(100) og lg(100)/lg(10). Avgjør hvilke grunntall de ulike logaritmefunksjonene har. Oppgave 2: Tast inn log(10) på kalkulatoren din. Hvilket grunntall opererer vi med her? 6.1.2 Eksempel: Logaritmer og bits Datamaskiner representerer dataene ved strenger av bits, der hver bit enten har verdien 0 eller 1. Et naturlig spørsmål er hvor mange bit-strenger det finnes med lengde n. Dersom vi skal behandle dette spørsmålet matematisk, er det fornuftig å innføre en symbolsk notasjon for det vi skal regne ut. Derfor lar vi N n betegne antallet ulike bit-strenger av lengde n: La oss først se på tilfellet n = 1. Det finnes 2 strenger med lengde 1: 0 og 1. Det vil si at N 1 = 2 Hvis vi ser på n = 2, finner vi strengene 00, 01, 10 og 11. D.v.s at N 2 = 4 La oss nå se på det generelle tilfellet: Først ser på strengene med lengde n der siste bit er 0. Det finnes N n 1 slike siden de n 1 første bitsene kan utgjøre en hvilken som helst streng av n 1 bits. På tilsvarende måte kan vi ser vi at antallet strenger med lengde n der siste bit er 1 er lik N n 1. Dette betyr at N n = N n 1 + N n 1 = 2N n 1 Siden dette gjelder helt generelt, må N n 1 = 2N n 2 o.s.v. Det gir N n = 2N n 1 = 2(2N n 2 ) = 2 2 N n 2 = 2 3 N n 3 = = 2 k N n k = 2 n 1 N 1 = 2 n siden N 1 = 2. Det vi har funnet ut her er at det finnes 2 n bit-strenger med lengde n. For eksempel er 2 32 4.3 milliarder. Det betyr at vi med 32 bits kan lage omtrent 4.3 milliarder ulike kombinasjoner. La oss si at vi ønsker å representere 10 000 objekter med ulike bit-strenger, og vi spør: Hvor mange bits behøver vi? Svaret er at vi må finne det minste tallet n slik at N n = 2 n 10 000

Hvis vi tar logaritmen på begge sider av denne ulikheten, får vi Ved reglene for logaritmeregning får vi dette gir log a (2 n ) log a (10 000). n log a (2) log a (10 000). n log a(10 000) log a (2) Svaret her er uavhengig av grunntallet a. Hvis man bruker en kalkulator, er det naturlig å regne dette ut med grunntall 10: n log(10 000) log(2) 13.3. Det betyr at n = 14 er det minste heltallet slik at 2 n 10 000. Konklusjonen er at vi trenger 14 bits for å kunne representere 10 000 ulike objekter. Vi kan gjøre beregningen med logaritmer med grunntall 2. Siden log 2 (2) = 1 blir utregningen slik: n log 2(10 0000) log 2 (2) = log 2 (10 000) = lg(10 000) 13.3. Vi kan uttrykke dette som et generelt resultat: La n N være antallet bits som kreves for å skille mellom N objekter. Da er n N log 2 (N) = lg(n). Vi kan altså tenke på 2-logaritmen lg på følgende måte: lg(n) angir omtrent hvor mange bits som behøves for å kunne skille mellom N ulike objekter. Oppgave 3: Regn ut log a (10 000)/ log a (2) ved å bruke logaritmen med grunntall a = e, d.v.s ved hjelp av ln. 6.1.3 Logaritmer og -notasjon Det viktigste vi kan si om logaritmer og -notasjon er at log a (n) log a (b) log b (n)

f(n) g(n) log a (f(n)) log a (g(n)), under visse tekniske betingelser 3 som som oftest er oppfylt. Den første påstanden følger av at log a (n) = log a (b log b (n) ) = log b (n) log a (b) = log a (b) log b (n). Den siste påstanden kan vi begrunne utifra definisjonen (1) og litt logaritme- og brøkregning: log a (f(n)) log a (g(n)) = log a(g(n)) + log a (f(n)) log a (g(n)) log a (g(n)) = log a(g(n)) log a (g(n)) + log a(f(n)) log a (g(n)) log a (g(n)) = 1 + 1 ) ( log f(n) a g(n) log a (g(n)) når n vokser, siden log a (f(n)/g(n)) 0 når f(n)/g(n) 1. 6.1.4 Logaritmer og O-notasjon Det viktigste vi kan si om logaritmer og O-notasjon er at 1 log a (n) n. n log a (n) 1 log a (n) log b (n) for alle a, b > 1. Den siste påstanden er nokså enkel å begrunne: log a (n) = log a (b log b (n) ) = log b (n) log a (b) = log a (b) log b (n). Vi ser altså at log a (n) er lik en konstant ganger log b (a). Dette betyr at logaritmisk vekst er et presist begrep, selv om vi ikke angir hvilket grunntall vi forholder oss til. (Dette står i kontrast til eksponentiell vekst, der grunntallet har stor betydning) En annen ting vi kan legge merke til er at hvis f(n) g(n), så er log a (f(n)) log a (g(n)) 3 Dette gjelder f.eks. ikke dersom både f(n) og g(n) går mot 1 når n vokser. For å være helt sikre, kan vi kreve at f(n) og g(n) alltid har funksjonverdier > 1.

7 De viktigste vekstklassene 7.1 Konstant vekst En funksjon f(n) 1, d.v.s av orden O(1) kalles konstant. Det betyr ikke nødvendigvis at funksjonen er konstant, men at funksjonverdien har en øvre grense. Typiske programelementer som regel har konstant kjøretid: Aritmetiske operasjoner. Oppslag i tabeller via indeks. Oppslag i Hash-tabeller (jfr. lærebokas kapittel 3.4). Det vil ofte være en forenkling å si at slike operasjoner har konstant kjøretid. Dersom vi arbeider med tall med ekstremt mange bits, f.eks. ved å bruke klassen BigInteger, kan man oppleve at kjøretiden til aritmetiske operasjoner avhenger av hvor store tall vi arbeider med. Men, i praksis arbeider vi gjerne med representasjoner med 32 eller 64 bits, og da blir kjøretiden for aritmetiske operasjoner konstant. 7.2 Logaritmisk vekst En vekstfunksjon f(n) log a (n), d.v.s av orden O(log a (n)) kalles logaritmisk. Vi sier at funksjonen har logaritmisk vekst. Typiske programelementer med logaritmisk kjøretid er: Binært søk Søk i binære søketrær (jfr. lærebokas kapittel 3.2) Modifikasjon av prioriteskøer (jfr. lærebokas kapittel 2.4) Algoritmer bygd på stegvis halvvering av problemet. Mange enkle for-løkker der iterasjonsvariabelen multipliseres med en konstant faktor i hver iterasjon. 7.3 Lineær vekst En vekstfunksjon f(n) n, d.v.s av orden O(n) kalles lineær. Vi si sier at funksjonen har lineær vekst. Typiske programelementer med lineær kjøretid er: Sekvensielt søk.

Sekvensiell behandling av data Mange enkle for-løkker der iterasjonsvariabelen har en fast økning i hver iterasjon. 7.4 Linearitmisk vekst En vekstfunksjon f(n) n log a (n), d.v.s av orden O(n log a (n)) kalles linearitmisk. Vi sier at funksjonen har linearitmisk vekst. Typiske programelementer med linearitmisk kjøretid er: Merge sort (jfr. lærebokas kapittel 2.2) Å bygge et binært søketre (jfr. lærebokas kapittel 3.2) Mange algoritmer som halvverer problemstørrelsen. I litteraturen brukes i blant termen loglineær, som oftest som et synonym til termen linearitmisk. 7.5 Kvadratisk vekst En vekstfunksjon f(n) n 2, d.v.s av orden O(n 2 )) kalles kvadratisk. Vi sier at funksjonen har kvadratisk vekst. Typiske programelementer med kvadratisk kjøretid er: Elementære søkemetoder (jfr. lærebokas kapittel 2.2) Mange doble for-løkker. Algoritmer som behandler alle par av objekter. 7.6 Kubisk vekst En vekstfunksjon f(n) n 3, d.v.s av orden O(n 3 )) kalles kubisk. Vi sier at funksjonen har kubisk vekst. Typiske programelementer med kubisk kjøretid er: Mange Trippel-løkker. Matrisemultiplikasjon (jfr. faget RF5100: Lineær algebra)

7.7 Eksponentiell vekst En vektsfunksjon f(n) a n, d.v.s. av orden O(a n ) kalles eksponentiell. Vi sier at funksjonen har eksponentiell vekst. Typiske programelementer med eksponentiell vekst er: Undersøk alle delmengder av en delmengde. Uttømmende søk i en stor mengde potensielle løsninger av et problem. Algoritmer som vi møter i lærebokas kapittel 6. 7.8 Hierarkiet av vekstklasser De mest kjente funksjonene lar seg organisere på en veldig oversiktlig måte når vi bruker -notasjon (O-notasjon): 1 log a (n) n n k n k log a (n) n k+1 2 n 3 n Her forutsetter vi at grunntallet a > 1. Legg merke til at dette bare er et lite utvalg av funksjoner. Hvordan kan vi underbygge slike relasjoner? Jo, hvis n > a, så er 1 < log a (n) < n. Det betyr at 1 log a (n) n. Siden n k n k, følger det av regnereglene for -notasjon at n k n k log a (n) n k+1 De vekstklassene vi har definert i dette notatet danner følgende hierarki: konstant logaritmisk lineær linearitmisk kvadratisk kubisk eksponentiell Vi kan illutrere det med følgende figur, som viser : 60 50 40 konstant logaritmisk lineær linearitmisk kvadratisk kubisk eksponentiell 30 20 10 0 5 10 15 20 25

Vi kan fremstille det samme plottet med logaritmiske akser: 1e+07 1e+06 100000 10000 konstant logaritmisk lineær linearitmisk kvadratisk kubisk eksponentiell 1000 100 10 1 1 100 10000 1e+06 1e+08 1e+10 Dette illustrerer at det er forholdsvis liten forskjell mellom logaritmiske vekstfunksjoner og konstante vekstfunksjoner, og mellom linearitmisk og lineær vekst. 7.8.1 Sidebemerkning: Rette linjer i log-log-plottet Legg merke til hvordan kurvene for lineær, kvadratisk og kubisk vekst ser ut i plottet med logaritmiske akser: De er rette linjer. Dette kan vi forklare på følgende måte: Hvis f(n) = An k skal plottes i et logaritmisk plott, er x-verdien tilhørende verdien n lik log(n), mens y-verdien tilhørende verdien n lik log(f(n)), og vi får y = log(f(n)) = log(an k ) = log(a) + log(n k ) = k log(n) + log(a) = kx + log(a) Her har vi en sammenheng av typen y = ax + b der y = log(f(n)), x = log(n), a = k og b = log(a). Det betyr at polynomisk vekst av grad k gir linjer med stigningstall k. 8 Enkle estimater med O-notasjon Her skal vi se på en praktisk metode for å lage grove worst case-estimater som vi gjerne skriver inn i koden. La oss ta et eksempel der vi ønsker å estimere kjøretiden. Den første grunnregelen er at instruksjoner som ikke involverer metodekall eller repetisjoner har konstant kjøretid, d.v.s. kjøretid av orden O(1).

public int sumofintegers(int n){ return n*(n+1)/2; // time << 1 } // time << 1 Denne funksjonen har konstant kjøretid. I starten, når vi skal lære dette, tar vi for oss én og én linje, skriver opp et estimat for kjøretiden. Når vi kommer til slutten av en kodeblokk, altså det som befinner seg mellom { og } skriver vi opp et estimat for hele kodeblokken. 8.1 Sekvensiell kode Hvis en kodeblokk består av flere elementer som følger sekvensielt etter hverandre, så er det elementet som har høyest kjøretid som dominerer. La oss si at vi vet at dosomethingmore(n) har kvadratisk kjøretid og doevenmore(n) har linearitmisk kjøretid (som funksjon av n). Da kan vi gjøre følgende analyse: public void dosomething(int n) { int a = sumofintegers(int n); // time << 1 (From analysis above) dosomethingmore(n); // time << n^2 (By assumption) doevenmore(n); // time << n log(n) (By assumption) } //time << n^2 Her er det funksjonskallet dosomethingmore(n) som er dominerende. På bakgrunn av estimatene for de tre elementene, vet vi at kjøretiden T (n) til dosomething(n) tilfredsstiller ulikheten T (n) = kjøretid linje 1 + kjøretid linje 2 + kjøretid linje 3 A 1 + B n 2 + C n log(n) n 2 Vi kan uttrykke dette som et generelt prinsipp: En kodeblokk med k elementer 1, 2,..., k med kjøretid av orden O(f 1 (n)), O(f 2 (n)),..., O(f k (n)) vil ha kjøretid av orden O(f 1 (n) + f 2 (n) + + f k (n)) = O(f i (n)), der i er valgt slik at f i (n) vokser minst like raskt som f 1 (n),..., f k (n). D.v.s at f j (n) f i (n) når j = 1, 2,..., k, altså når f i (n) dominerer. 8.2 Iterasjon Når vi ser på en for-løkke der frekvensen er av orden O(f(n)) og hver iterasjon har kjøretid av orden O(g(n)) har så har hele løkken kjøretid av orden O(f(n)g(n)):

for(int i = 0; i < f(n) ; i+= 2) {// frequency << f(n)/2 << f(n) somethingcompletelydifferent(i); // time << g(n) } // time << f(n)g(n) Dette er en direkte konsekvens av følgende analyse: La T k være kjøretiden i iterasjon k, og N n være eksakt antall iterasjoner. Da er den totale kjøretiden T = T 1 + T 2 + + T Nn g(n) + g(n) + + g(n) = N n g(n) f(n)g(n). (Til slutt har vi utnyttet at N n f(n).) Nå har vi en teknikk som lar oss utføre følgende analyse: public static int count(int[] a) { int N = a.length; // time << 1 int cnt = 0; // time << 1 for (int i = 0; i < N; i++) { // frequency << N for (int j = i+1; j < N; j++) { // frequency << N-i << N for (int k = j+1; k < N; k++) { // frequency << N-j << N if (a[i] + a[j] + a[k] == 0) { // frequency << 1 cnt++; // time 1 } // time << 1 } // time << 1*n = n } // time << n*n n^2 } // time << n*n^2 = n^3 return cnt; // time << 1 } // time: << n^3 Her er dekomponerer vi blokken i fire elementer, der de to første og det siste elementet har kjøretid av orden O(1), mens det tredje leddet, den dominerende triple for-løkken, har kjøretid av orden O(n 3 ). Legg merke til at estimatet i hver enkelt linje kun forholder seg til sin egen blokk, uten å relatere seg til utenforliggende blokker. I praksis betyr det at vi ser bort fra den ytre for-løkken når vi analyserer den nest ytterste løkken. Det vi har analysert her er metoden count i ThreeSum.java. I en håndvending har vi kommet fram til at den har kjøretid av orden O(n 3 ). Legg merke til at vi gjør en ganske grov forenkling her. Selv om variablene i og j ikke er konstante, bruker vi estimatet N i N og N i N. Det betyr at vi overestimerer N i og N j. Dette er helt i tråd med O-notasjonens filosofi, som nettopp går ut på å lage romslige worst case-estimater.

8.3 Iterasjon med logaritmisk og linearitmisk kjøretid I eksemplene over er endringen i iterasjonvariabelen konstant. Vi kan også tenke oss løkker der iterasjonsindeksen multipliseres for hvert steg. Et eksempel på dette er: for (int i = 1; i < n ; i *= 3){ StdOut.println(i); // time: O(1) } For å forenkle beregningen av løkkens frekvens antar vi at n = 3 k. Da vil i gjennomløpe verdiene i = 1, 3, 3 2,..., 3 k 1, helt til 3 k 1 < n 3 k. Dette medfører at altså log(3 k 1 ) < log(n), (k 1) log(3) < log(n), alståk < log n log 3 + 1 log n Det betyr at frekvensen har orden O(log n), og vi vil skrive for (int i = 1; i < n ; i *= 3){// frequency: << log(n) StdOut.println(i); // time << 1 } // time << 1 log(n) = (log(n) På tilsvarende måte kommer vi frem til følgende estimater: loop1: for(int i = 3; i < 3n; i*=5) { // frequency << log(n) for(int j = 0; j < i; j++) { // frequency << i << 3n << n StdOut.println(j); // time <<1; } // time: << n } // time: << n*log(n) loop2: for(int i = 10; i < n; i +=3){ // frequency: << n for(int j = 1; j < i; j*=2) { // frequency << log(i) << log(n) StdOut.println(j); // time << 1 } // time << log(n) } // time << n log(n) Her har vi altså to løkker med kjøretid av orden O(n log n). For løkker av typen loop1 kan vi dog skaffe oss skarpere estimater:

for (int i = 1; i < n ; i*=2) { for (int j = 0; j < i; j++){ StdOut.println(j); // time << 1 } } Her løper iterasjonsvariabelen i gjennom verdiene i = 1, 2, 2 2,..., 2 k, der k er det største heltallet slik at 2 k < n. D.v.s. at k log n. For hver i-verdi løper j gjennom i ulike verdier. Det totale antallet iterasjoner i den indre løkka blir derfor: N = Antall iterasjoner når i = 1. + Antall iterasjoner når i = 2 + Antall iterasjoner når i = 4 + Antall iterasjoner når i = 2 k = 1 + 2 + + 2 k = 2 k+1 1 2 2 k 2 k n Det vi ser her, er at det totale antallet iterasjoner i den indre løkka, N n. Konklusjonen er at hele denne doble løkka har kjøretid av orden O(n). Dette er et eksempel på at den groveste analysen med O-notasjon noen ganger kan forfines dersom vi er villige til å gå i detalj. Gjør vi en tilsvarende analyse av loop1 over, kommer vi frem til en kjøretid av orden O(n), mens en like detaljert analyse av loop2 fortsatt gir kjøretid av orden O(n log n) (Obs: Den sistnevnte analysen er ikke enkel å gjennomføre).