CS-A1140 Tietorakenteet ja algoritmit

Samankaltaiset tiedostot
Kierros 1: Algoritmianalyysin ja tietorakenteiden perusteita

A TIETORAKENTEET JA ALGORITMIT

Algoritmit 1. Luento 2 Ke Timo Männikkö

Tietorakenteet ja algoritmit Johdanto Lauri Malmi / Ari Korhonen

Tietorakenteet ja algoritmit - syksy

Algoritmit 1. Luento 3 Ti Timo Männikkö

4 Tehokkuus ja algoritmien suunnittelu

f(n) = Ω(g(n)) jos ja vain jos g(n) = O(f(n))

Algoritmit 1. Luento 4 Ke Timo Männikkö

811312A Tietorakenteet ja algoritmit II Perustietorakenteet

Pino S on abstrakti tietotyyppi, jolla on ainakin perusmetodit:

Tieto- ja tallennusrakenteet

58131 Tietorakenteet ja algoritmit (kevät 2016) Ensimmäinen välikoe, malliratkaisut

Algoritmit 2. Luento 14 Ke Timo Männikkö

Algoritmit 2. Luento 2 To Timo Männikkö

Tietorakenteet, laskuharjoitus 3, ratkaisuja

Algoritmit 2. Luento 8 To Timo Männikkö

Algoritmit 2. Luento 2 Ke Timo Männikkö

useampi ns. avain (tai vertailuavain) esim. opiskelijaa kuvaavassa alkiossa vaikkapa opintopistemäärä tai opiskelijanumero

Algoritmit 1. Demot Timo Männikkö

811312A Tietorakenteet ja algoritmit Kertausta kurssin alkuosasta

Algoritmit 1. Luento 5 Ti Timo Männikkö

2. Perustietorakenteet

18. Abstraktit tietotyypit 18.1

Algoritmit 2. Luento 7 Ti Timo Männikkö

Tietorakenteet ja algoritmit

Tietorakenteet ja algoritmit

Sisällys. 18. Abstraktit tietotyypit. Johdanto. Johdanto

ALGORITMIT 1 DEMOVASTAUKSET KEVÄT 2012

Ohjelmoinnin perusteet Y Python

Algoritmit 1. Luento 12 Ti Timo Männikkö

Algoritmit 1. Luento 14 Ke Timo Männikkö

811312A Tietorakenteet ja algoritmit, , Harjoitus 3, Ratkaisu

Algoritmit 2. Luento 1 Ti Timo Männikkö

1.4 Funktioiden kertaluokat

TIETORAKENTEET JA ALGORITMIT

Kaksiloppuinen jono D on abstrakti tietotyyppi, jolla on ainakin seuraavat 4 perusmetodia... PushFront(x): lisää tietoalkion x jonon eteen

2. Seuraavassa kuvassa on verkon solmujen topologinen järjestys: x t v q z u s y w r. Kuva 1: Tehtävän 2 solmut järjestettynä topologisesti.

(p j b (i, j) + p i b (j, i)) (p j b (i, j) + p i (1 b (i, j)) p i. tähän. Palaamme sanakirjaongelmaan vielä tasoitetun analyysin yhteydessä.

Algoritmit 1. Luento 10 Ke Timo Männikkö

Algoritmit 1. Luento 11 Ti Timo Männikkö

Kääreluokat (oppikirjan luku 9.4) (Wrapper-classes)

List-luokan soveltamista. Listaan lisääminen Listan läpikäynti Listasta etsiminen Listan sisällön muuttaminen Listasta poistaminen Listan kopioiminen

Algoritmit 1. Luento 12 Ke Timo Männikkö

Ohjelmoinnin peruskurssi Y1

Ohjelmoinnin perusteet Y Python

1.1 Pino (stack) Koodiluonnos. Graafinen esitys ...

Abstraktit tietotyypit. TIEA341 Funktio ohjelmointi 1 Syksy 2005

A TIETORAKENTEET JA ALGORITMIT

Diskreetin matematiikan perusteet Laskuharjoitus 2 / vko 9

lähtokohta: kahden O(h) korkuisen keon yhdistäminen uudella juurella vie O(h) operaatiota vrt. RemoveMinElem() keossa

Algoritmit 1. Demot Timo Männikkö

815338A Ohjelmointikielten periaatteet Harjoitus 6 Vastaukset

Kierros 2: Järjestämisalgoritmeja

58131 Tietorakenteet ja algoritmit (syksy 2015)

58131 Tietorakenteet (kevät 2009) Harjoitus 9, ratkaisuja (Antti Laaksonen)

Ohjelmoinnin peruskurssien laaja oppimäärä

Algoritmianalyysin perusteet

811312A Tietorakenteet ja algoritmit Kertausta kurssin alkuosasta

MS-A0401 Diskreetin matematiikan perusteet

Algoritmit 1. Demot Timo Männikkö

Ohjelmoinnin peruskurssien laaja oppimäärä

Algoritmit 2. Demot Timo Männikkö

Tietorakenteet (syksy 2013)

Kierros 6: Dynaaminen ohjelmointi ja ahneet algoritmit

5 Kertaluokkamerkinnät

Luku 3. Listankäsittelyä. 3.1 Listat

Tietorakenteet ja algoritmit syksy Laskuharjoitus 1

Kierros 4: Binäärihakupuut

Ohjelmoinnin perusteet Y Python

Harjoitustyö: virtuaalikone

811312A Tietorakenteet ja algoritmit, , Harjoitus 5, Ratkaisu

TKT20001 Tietorakenteet ja algoritmit Erilliskoe , malliratkaisut (Jyrki Kivinen)

CS-A1140 Tietorakenteet ja algoritmit

CS-A1140 Tietorakenteet ja algoritmit

Ohjelmoinnin perusteet Y Python

Tietorakenteet. JAVA-OHJELMOINTI Osa 5: Tietorakenteita. Sisällys. Merkkijonot (String) Luokka String. Metodeja (public)

Tietorakenteet, laskuharjoitus 1,

Algoritmit 1. Luento 10 Ke Timo Männikkö

Ohjelmoinnin peruskurssien laaja oppimäärä

Ohjelmoinnin peruskurssien laaja oppimäärä

1. (a) Seuraava algoritmi tutkii, onko jokin luku taulukossa monta kertaa:

Rakenteiset tietotyypit Moniulotteiset taulukot

Johdatus diskreettiin matematiikkaan Harjoitus 5, Ratkaise rekursioyhtälö

Operaattoreiden ylikuormitus. Operaattoreiden kuormitus. Operaattoreiden kuormitus. Operaattoreista. Kuormituksesta

ITKP102 Ohjelmointi 1 (6 op)

Ohjelmoinnin perusteet Y Python

Algoritmit 1. Demot Timo Männikkö

STL:n uudistukset. Seppo Koivisto TTY Ohjelmistotekniikka

ja λ 2 = 2x 1r 0 x 2 + 2x 1r 0 x 2

Kysymyksiä koko kurssista?

811312A Tietorakenteet ja algoritmit, , Harjoitus 7, ratkaisu

7/20: Paketti kasassa ensimmäistä kertaa

Tietorakenteet ja algoritmit

Ohjelmoinnin perusteet Y Python

Listarakenne (ArrayList-luokka)

Osoitin ja viittaus C++:ssa

811312A Tietorakenteet ja algoritmit , Harjoitus 2 ratkaisu

Jakso 4 Aliohjelmien toteutus

Ohjelmointi 1 C#, kevät 2013, 2. tentti

Transkriptio:

CS-A1140 Tietorakenteet ja algoritmit Kierros 1: Algoritmianalyysin ja tietorakenteiden perusteita Tommi Junttila Aalto-yliopisto Perustieteiden korkeakoulu Tietotekniikan laitos Syksy 2016

Materiaalia kirjassa Introduction to Algorithms, 3rd ed. (online via Aalto lib): Funktioiden kasvunopeuksista: Luku 3 Perustietorakenteita: Kappaleet 10.1 10.3 Dynaamiset taulukot: Kappale 17.4 2/55

Algoritmien kuvauksista Erittäin abstraktilla tasolla algoritmi voidaan nähdä menetelmänä, joka muuntaa syötteen halutuksi tulokseksi input algorithm output Esimerkiksi järjestämisalgoritmi muuntaa mielivaltaisen taulukon (mahdollisesti toiseksi) taulukoksi, jossa samat alkiot ovat suuruusjärjestyksessä Monissa tapauksissa on parempi kuvata algoritmi joko luonnollisella kielellä tai pseudokoodina sen sijaan, että esittäisi jollakin tietyllä ohjelmointikielellä tehdyn toteutuksen 3/55

Tarkastellaan esimerkkinä ICS-A1120 Ohjelmointi 2 -kurssilla esitettyä puolitushakualgoritmia Se saa syötteenään järjestetyn taulukon A ja alkion e ja selvittää, esiintyykö alkio taulukossa Voisimme kuvata algoritmin luonnollisella kielellä esimerkiksi seuraavasti: 1. Tarkastellaan aluksi koko taulukkoa 2. Jos taulukko on tyhjä, palauta false 3. Jos taulukon keskimmäinen alkio on e, palauta true 4. Muutoin jos keskimmäinen alkio oli suurempi kuin e, palaa kohtaan 2 mutta tarkastele vain osataulukkoa, joka sisältää nykyisen taulukon alkuosan alusta keskipisteeseen asti (muttei sisältäen keskipistettä) 5. Muutoin keskimmäinen alkio on pienempi kuin e ja palataan kohtaan 2 mutta tarkastellen osataulukkoa alkaen keskipisteestä seuraavasta alkiosta ja päättyen viimeiseen alkioon 4/55

Algoritmin kuvaus pseudokoodina: // Search for the element e in the sorted array A binary-search(a, e): i search(a, e, 1, A.length) if A[i] = e: return true else: return false // A helper function: search for the index of e in the sub-array A[lo,...,hi] search(a, e, lo, hi): if hi lo: return lo // sub-array of size 1 or less? mid lo + (hi lo)/2 if A[mid] = e: return mid else if A[mid] < e: return search(a, e, mid + 1, hi) else: return search(a, e, lo, mid 1) Huomioitavaa: monesti taulukon inddeksointi alkaa indeksillä 1 pseudokoodissa ja matemaattisessa notaatiossa mutta indeksillä 0 monessa oikeassa ohjelmointikielissä Tämä tulee ottaa huomioon toteutettaessa kirjasta, wikipediasta jne löytyviä algoritmeja 5/55

Eräs toteutus Scalalla: def binarysearch [ T <% Ordered [ T ] ] ( s : IndexedSeq [ T ], v : T ) : Boolean = { def i n n e r ( s t a r t : I n t, end : I n t ) : I n t = { i f (! ( s t a r t < end ) ) s t a r t else { val mid = s t a r t + ( ( end s t a r t ) / 2) val cmp = v compare s ( mid ) i f (cmp == 0) mid / / v == s ( mid ) else i f (cmp < 0) i n n e r ( s t a r t, mid 1) / / v < s ( mid ) else i n n e r ( mid+1, end ) / / v > s ( mid ) } } i f ( s. length == 0) false else s ( i n n e r ( 0, s. length 1) ) == v } 6/55

Algoritmianalyysin perusteita 7/55

Resurssityyppejä ja ominaisuuksia Tavoitteena on saada aikaan algoritmeja, jotka käyttävät annettuja resursseja tehokkaasti Tyypillisiä resursseja ja ominaisuuksia ovat ajoaika muistin käyttö rinnakkaistuvuus levyn ja verkon käyttö välimuistin käytön tehokkuus (esim. cache-oblivious algorithms)... Luonnollisesti joskus pitää tehdä kompromisseja: esimerkiksi algoritmi voi käyttää vähemmän muistia mutta olla ajoajaltaan hitaampi kuin toinen saman asian tekevä algoritmi Tällä kurssilla keskitytään pääsääntöisesti ajoaikaan Tarkastelemme algoritmin/ohjelman ajoaikaa funktiona f (n) suhteessa syötteen kokoon tai johonkin muuhun mielenkiintoiseen parametriin n Sekä n että f (n) oletetaan yleensä ei-negatiivisiksi ja n on yleensä kokonaisluku 8/55

O-notaatio Tarkastellaan funktioiden asymptoottista kasvua Tätä varten käytetään O-notaatiota ja sen variantteja Se on matemaattinen työkalu, joka abstrahoi pois Määritelmä pienet n:n arvot eli pienet syötteet, sekä vakiotekijät, jotka riippuvat esimerkiksi kellotaajuudesta tai muistiväylän leveydestä Merkitään f (n) = O(g(n)) jos on olemassa vakiot c,n 0 > 0 siten, että f (n) cg(n) kaikille n n 0. Tällöin f (n) kasvaa korkeintaan yhtä nopeasti kuin g(n) (kun tarkastelemme tarpeeksi suuria n:n arvoja) Muistutus: f (n) ja g(n) ovat ei-negatiivisia 9/55

10n + 1200 = O(n), 10n + 1200 = O(n 2 ) ja 10n + 1200 = O(2 0.1n ) n 2 + 5n + 1000 = O(n 2 ) ja n 2 + 5n + 1000 = O(2 0.1n ) mutta n 2 + 5n + 1000 O(n) 2 0.1n O(n) ja 2 0.1n O(n 2 ) 10/55

Tyypillisiä funktioluokkia: O(1) vakio O(log n) logaritminen O(n) lineaarinen O(nlogn) kvasilineaarinen, n log n (O(logn!) = O(nlogn)) O(n 2 ) neliöllinen O(n 3 ) kuutiollinen O(n k ) polynominen, k 0 O(c n ) eksponentiaalinen, c > 1 O(n!) kertoma Vakio-, logaritmiset, lineaariset, kvasilineaariset, neliölliset ja kuutiolliset funktiot ovat siis kaikki polynomisia funktioita Logaritmit ovat kannassa 2 jollei toisin mainita (mutta tällä ei yleisesti väliä koska log c n = log 2 n log 2 c kaikille c > 1) 11/55

Algoritmin/ohjelman pahimman tapauksen ajoaika on O(f (n)) jos g(n) = O(f (n)), missä funktio g(n) on pisin ajoaika mille tahansa syötteelle kokoa n Yleensä sanonta algoritmin ajoaika on O(f (n)) tarkoittaa pahimman tapauksen ajoaikaa Voidaan myös tarkastella parhaan tapauksen ajoaikaa, joka saadaan vastaavasti tarkastelemalla kaikkien n:n kokoisten syötteiden pienintä ajoaikaa keskimääräistä ajoaikaa, missä tarkastellaan n:n kokoisten syötteiden (joko kaikkien tai jostakin tyypillisestä joukosta valittujen) odotettua ajoaikaa 12/55

Esimerkki: Tarkastellaan lineaarihakualgoritmia, joka tarkastaa kuuluuko alkio e järjestämättömään taulukkoon A käymällä sen alkiot läpi kunnes e löytyy tai kaikki taulukon n alkiota on käyty läpi. Olettaen, että alkion haku taulukosta ja alkioiden vertailu ovat vakioaikaisia operaatioita, pahimman tapauksen ajoaika on O(n) koska e voi olla taulukon viimeinen alkio tai puuttua taulukosta, jolloin kaikki n alkiota käydään läpi parhaan tapauksen ajoaika on O(1) koska e voi olla taulukon ensimmäinen alkio 13/55

Esimerkki: neliömatriisien (suoraviivainen) kertominen Analysoidaan kertolaskumetodin * asymptoottista ajoaikaa Ensin tulosmatriisin result alustus vie ajan O(n 2 ) koska sen n n alkiota varataan ja alustetaan arvolla 0 class Matrix ( val n : I n t ) { r e q u i r e ( n > 0, " n must be p o s i t i v e " ) val e n t r i e s = new Array [ Double ] ( n * n ) / * * Access elements by w r i t i n g m( i, j ) * / def apply ( row : I n t, column : I n t ) = { e n t r i e s ( row * n + column ) } / * * Set elements by w r i t i n g m( i, j ) = v * / def update ( row : I n t, c o l : I n t, val : Double ) { e n t r i e s ( row * n + c o l ) = val } / * * Returns the product of t h i s and t h a t * / def * ( t h a t : M atrix ) : M atrix = { r e q u i r e ( n == t h a t. n ) val r e s u l t = new Matrix ( n ) for ( row < 0 u n t i l n ; column < 0 u n t i l n ) { var v = 0.0 for ( i < 0 u n t i l n ) v += this ( row, i ) * t h a t ( i, column ) r e s u l t ( row, column ) = v } r e s u l t } } 14/55

Esimerkki: neliömatriisien (suoraviivainen) kertominen Uloin silmukka käy läpi muuttujalle row arvot 0,1,...,n 1 Keskimmäinen: col arvot 0,1,...,n 1 Sisin: i arvot 01,...,n 1 Sisimmässä silmukassa arvojen hakeminen ja kertolasku ovat vakioaikaisia toimintoja Metodin * ajoaika on O(n 2 + n 3 ) = O(n 3 ) class Matrix ( val n : I n t ) { r e q u i r e ( n > 0, " n must be p o s i t i v e " ) val e n t r i e s = new Array [ Double ] ( n * n ) / * * Access elements by w r i t i n g m( i, j ) * / def apply ( row : I n t, column : I n t ) = { e n t r i e s ( row * n + column ) } / * * Set elements by w r i t i n g m( i, j ) = v * / def update ( row : I n t, c o l : I n t, val : Double ) { e n t r i e s ( row * n + c o l ) = val } / * * Returns the product of t h i s and t h a t * / def * ( t h a t : M atrix ) : M atrix = { r e q u i r e ( n == t h a t. n ) val r e s u l t = new Matrix ( n ) for ( row < 0 u n t i l n ; column < 0 u n t i l n ) { var v = 0.0 for ( i < 0 u n t i l n ) v += this ( row, i ) * t h a t ( i, column ) r e s u l t ( row, column ) = v } r e s u l t } } 15/55

Θ-, Ω, o- ja ω-notaatiot O-notaation lisäksi yleisesti käytettyjä ovat seuraavat: f (n) = Ω(g(n)) jos g(n) = O(f (n)) f kasvaa vähintään yhtä nopeasti kuin g f (n) = Θ(g(n)) jos f (n) = O(g(n)) ja g(n) = O(f (n)) f ja g kasvavat yhtä nopeasti Lisäksi Määritelmä Merkitään f (n) = o(g(n)) jos kaikille vakioille c > 0 on olemassa vakio n 0 > 0 siten että f (n) < cg(n) kaikille n n 0. Jos f (n) = o(g(n)), niin f kasvaa hitaammin kuin g f (n) = ω(g(n)) jos g(n) = o(f (n)), f kasvaa nopeammin kuin g 16/55

Rekursio ja rekursioyhtälöt 17/55

Rekursiivisille ohjelmille asymptoottisen ajoajan analysointi suoraan (pseudo)koodista voi olla haastavampaa Esimerkki: Ojelmointi 2-kurssilta: binäärihakualgoritmin toteutus Scalalla def binarysearch [ T <% Ordered [ T ] ] ( s : IndexedSeq [ T ], v : T ) : Boolean = { def i n n e r ( s t a r t : I n t, end : I n t ) : I n t = { i f (! ( s t a r t < end ) ) s t a r t else { val mid = s t a r t + ( ( end s t a r t ) / 2) val cmp = v compare s ( mid ) i f (cmp == 0) mid / / v == s ( mid ) else i f (cmp < 0) i n n e r ( s t a r t, mid 1) / / v < s ( mid ) else i n n e r ( mid+1, end ) / / v > s ( mid ) } } i f ( s. length == 0) false else s ( i n n e r ( 0, s. length 1) ) == v } 18/55

Toisaalta ajoaika-analyysi vastaavasta ei-rekursiivisesta koodista voi olla vähintään yhtä haastavaa, jos ei vaikeampaakin... Esimerkki: ei-rekursiivinen versio binäärihakualgoritmista: def b i n a r y S e a r c h I t e r [ T <% Ordered [ T ] ] ( s : IndexedSeq [ T ], v : T ) : Boolean = { i f ( s. length == 0) return false var s t a r t = 0 var end = s. length 1 while ( s t a r t < end ) { val mid = s t a r t + ( ( end s t a r t ) / 2) val cmp = v compare s ( mid ) i f (cmp == 0) { s t a r t = mid ; end = mid } else i f (cmp < 0) end = mid 1 / / v < s ( mid ) else s t a r t = mid+1 / / v > s ( mid ) } return s ( s t a r t ) == v } 19/55

Rekursiivisten ohjelmien ajoajoille T(n) voidaan usein muodostaa rekursioyhtälö (engl. recurrence equation) muotoa T(n) =... missä n on parametri, joka liittyy usein jotenkin sen hetkisen rekursiivisen kutsun tarkasteleman osasyötteen kokoon tms, ja yhtälön oikea puoli riippuu rekursiivisesti pienempien osaongelmien ajoajasta T(n ) ja muista termeistä Lisäksi täytyy luonnollisesti ilmaista perustapaukset, yleensä T(0) tai T(1), jotka eivät riipu muista arvoista T(n ) Saatu rekursioyhtälö ratkaistaan jollain tavalla Huom: T(n) ei ole funktion/metodin laskema arvo vaan (arvio) sen ajoajasta syötteellä, jonka koko on n 20/55

Esimerkki: Binäärihakualgoritmin rekursiivinen osa tarkastelee osataulukoita, joiden koko on n (end - start + 1 Scala-toteutuksessa). Kun n 1, niin rekursio päättyy n > 1, niin tehtdään alkioiden vertailu ja haku joko päättyy koska haettu alkio löytyi, tai kutsutaan samaa funktiota rekursiivisesti osataulukolla, jonka koko on n/2 Jos oletetaan taulukon alkioiden käsittely ja alkioiden vertailu vakioaikaisiksi operaatioiksi, niin saadaan rekursioyhtälöt T(0) = T(1) = d ja T(n) = c + T( n/2 ) missä d ja c ovat vakioita, jotka kertovat indeksien vertailuun, alkioden hakemiseen jne menevän ajan Huomaa, että tässä T(n) on pahimman tapauksen arvio ajoajalle (suoritetuille käskyille tms) koska olemme jättäneet siinä huomioimatta tapauksen, jossa alkio löytyy taulukosta ennen kuin tarkasteltavan osataulukon koko on 1 21/55

Ratkaistaan rekursiöyhtälöt T(0) = T(1) = d ja T(n) = c + T( n/2 ) Oletetaan, että n on kahden potenssi eli muotoa n = 2 k jollekin einegatiiviselle kokonaisluvulle k. Tällöin T(2 k ) = c + T(2 k 1 ) = c + (c + T(2 k 2 )) =... = kc + T(2 0 ) = ck + d Koska n = 2 k, joten k = log 2 n, ja c sekä d ovat vakioita, saadaan T(n) = T(2 k ) = ck + d = clog 2 n + d = O(log 2 n) Kun n ei ole kahden potenssi, niin T(n) T(2 log 2 n ) = d +c log 2 n d + c(1 + log 2 n) = O(log 2 n) koska T(n) on monotonisesti kasvava. 22/55

Käytettäessä O-notaatiota yksittäisiä vakioita voidaan usein poistaa jo alkuvaiheessa korvaamalla ne vakiolla 1. Binäärihakuesimerkissä rekursioyhtälö voisi siis olla T(0) = T(1) = 1 ja T(n) = 1 + T( n/2 ). Tämän ratkaisuksi saadaan jälleen T(n) = O(log 2 n). Usein käytetään myös seuraavaa merkintää T(0) = T(1) = O(1) ja T(n) = O(1) + T( n/2 ). 23/55

Ajoajan (suoritettujen käskyjen määrän) lisäksi myös muista algoritmien ominaisuuksista voidaan tehdä vastaavia analyyseja Jos esimerkiksi alkioiden vertailu ei olekaan vakioaikaista vaan vie enemmän resursseja, voi olla kiinnostavaa vertailla eri hakuja järjestämisalgoritmien suorittamaa vertailujen määrää Esimerkki: Binäärihaun suorittamien vertailujen asymptoottiselle lukumäärälle saadaan jälleen rekursioyhtälö T(0) = T(1) = 1 and T(n) = T( n/2 ) + 1 missä n on syötetaulukon koko. Täten T(n) = O(log 2 n). Järjestämisalgoritmien yhteydessä seuraavalla kierroksella nähdään hieman monimutkaisempia rekursioyhtälöitä 24/55

Esimerkki: Potenssiin korottaminen neliöön korottamalla Tarkastellaan algoritmia, joka suorittaa potenssiin korottamisen x n mod 2 64 käyttämällä yhtälöä x n = k 1 i=0 xn i2 i, missä n k 1 n k 2...n 1 n 0 on luvun n binääriesitys Esimerkiksi x 71 = x 1 x 2 x 4 x 64 = x 20 x 21 x 22 x 26 Algoritmi käyttää iteratiivista neliöön korottamista: alkaen arvosta x 20 = x, se muodostaa aina seuraavan neliön x 2i+1 = x 2i x 2i. def pow( x : Long, n : Long ) : Long = { r e q u i r e ( n >= 0) i f ( n == 0) 1 else i f ( n == 1) x else i f ( ( n & 0x01 ) == 1) x * pow( x * x, n / 2 ) else pow( x * x, n / 2 ) } Suhteessa muuttujan n arvoon saadaan ajoajalle rekursioyhtälöt T(0) = T(1) = 1 ja T(n) = 1 + T(n/2). Täten T(n) = O(log 2 n). 25/55

Esimerkki: Tarkastellaan Ohjelmointi 2-kurssilta tuttua osajoukko-ongelmaa Annettuna joukko S = {v 1,...,v n } kokonaislukuja ja tavoiteluku t. Onko olemassa osajoukkoa S S siten, että v S v = t? ja (erittäin yksinkertaista) rekursiivista Scala-ohjelmaa sen ratkaisemiseksi: def subsetsum ( s : L i s t [ I n t ], t : I n t ) : Option [ L i s t [ I n t ] ] = { i f ( t == 0) return Some( N i l ) i f ( s. isempty ) return None val solnotincluded = subsetsum ( s. t a i l, t ) i f ( solnotincluded. nonempty ) return solnotincluded val s o l I n c l u d e d = subsetsum ( s. t a i l, t s. head ) i f ( s o l I n c l u d e d. nonempty ) return Some( s. head : : s o l I n c l u d e d. get ) return None } 26/55

Muodostetaan rekursiivisen ohjelman ajoaikaa kuvaavat rekursioyhtälöt parametrin n ollessa joukon alkioiden lukumäärä Pahimman tapauksen analyysissä ratkaisua ei löydy ja rekursio päättyy aina vasta kun tarkasteltava joukko on tyhjä Jokaisella rekursiivisella kutsukerralla ohjelma suorittaa joitain vakioaikaisia operaatioita ja sitten kaksi rekursiivista kutsua joukolla, josta on poistettu yksi alkio Täten saadaan rekursioyhtälöt T(0) = 1 ja T(n) = 1 + T(n 1) + T(n 1) = 1 + 2T(n 1) Kirjoitetaan yhtälö T(n) suoraviivaisesti auki ja saadaan T(n) = 1 + 2T(n 1) = 1 + 2(1 + 2T(n 2)) = 1 + 2 + 4(1 + 2T(n 3)) =... = n i=0 2i = 2 n+1 1 = Θ(2 n ) Eli pahimmassa tapauksessa ohjelman ajoaika on eksponentiaalinen Parhaassa (oletettavasti tosin hyvin harvinaisessa) tapauksessa tavoiteluku t on 0 ja ohjelman suoritus päättyy vakioajassa 27/55

Kokeellinen analyysi 28/55

Matemaattisen analyysin lisäksi voidaan (ja kannattaa) suorittaa myös kokeellista suorituskykyanalyysiä Voidaan vaikkapa ottaa kaksi toteutusta samasta tai eri algoritmista, jotka laskevat saman asian, ajaa näitä totetuksia jollain hyvin valitulla syötejoukolla ja vertailla mitattuja ajoaikoja Voitaisiin myös vaikkapa instrumentoida algoritmin toteutuskoodia niin, että se laskee esimerkiksi kaikki tehdyt vertailuoperaatiot ajon aikana ja kokeellisesti tarkastella saatujen määrien minimi-, keski- ja maksimiarvoja käyttää jotain profilointityökalua (kuten valgrind C/C++/konekieliohjelmille) saadaksemme selville välimuistiviittausten epäonnistumismäärät jne 29/55

Yksinkertainen ajoajan mittaaminen Kuten nähtiin Ohjelmointi 2-kurssilla, funktioiden ajoajan mittaus on periaatteessa melko helppoa 1 import java. lang. management. { ManagementFactory, ThreadMXBean } package object t i m e r { val bean : ThreadMXBean = ManagementFactory. getthreadmxbean ( ) def getcputime = i f ( bean. iscurrentthreadcputimesupported ( ) ) bean. getcurrentthreadcputime ( ) else 0 def measurecputime [ T ] ( f : => T ) : ( T, Double ) = { val s t a r t = getcputime val r = f val end = getcputime ( r, ( end s t a r t ) / 1000000000.0) } def measurewallclocktime [ T ] ( f : => T ) : ( T, Double ) = { val s t a r t = System. nanotime val r = f val end = System. nanotime ( r, ( end s t a r t ) / 1000000000.0) } } 1 Prosessoriajan (CPU time) sijaan tulee käyttää seinäkelloaikaa (wall clock time) rinnakkaisohjelmien ajankäyttöä mitattaessa koska getcurrentthreadcputime mittaa vain yhden säikeen prosessoriaikaa 30/55

Valitettavasti asia ei ole ihan näin suoraviivainen tarkastellaan seuraavaa koodia, joka järjestää 20 taulukkoa, joissa jokaisessa 10000 kokonaislukua import timer. _ val a = new Array [ I n t ](10000) val rand = new scala. u t i l. Random ( ) val times = scala. c o l l e c t i o n. mutable. A r r a y B u f f e r [ Double ] ( ) for ( t e s t < 0 u n t i l 20) { for ( i < 0 u n t i l a. length ) a ( i ) = rand. n e x t I n t val (dummy, t ) = measurecputime { a. sorted } times += t } p r i n t l n ( times. mkstring ( ", " ) ) Tuloksena saadaan seuraavat ajoajat 0.024590893, 0.010173347, 0.005155264, 0.003917635, 0.002668954, 0.002594947, 0.002807369, 0.002570412, 0.004068068, 0.006141965, 0.005393813, 0.005688233, 0.005168036, 0.005198876, 0.005008517, 0.004631029, 0.002407551, 0.001865849, 0.001878646, 0.002058858 Mitataan saman koodin suoritusta samankaltaisilla syötteillä mutta ajoaika tippuu kymmenesosaan joidenkin toistojen jälkeen? 31/55

Yllä oleva käytös voidaan selittää seuraavasti Scala-ohjelmat käännetään Java-tavukoodiksi Java-tavukoodia ajetaan Java-virtuaalikoneessa (Java virtual machine, JVM) Suoritus voi alkaa tulkitussa moodissa Osia koodista voidaan jossain vaiheessa kääntää alla olevan koneen konekoodiksi suorituksen aikana (just-in-time compilation) Käännettyä koodia voidaan optimoida ajon aikana Roskankeruuta (garbage collection) voi tapahtua ajon aikana Erityisesti lyhyiden ajoaikojen tulkinnassa kannattaa olla varovainen Pidempien ajoaikojen yhteydessä ilmiöt eivät tule suhteellisesti niin voimakkaasti esille ajoajan heilumisena 32/55

ScalaMeter Kuten nähtiin, tulkittujen ja virtuaalikoneissa ajettavien ohjelmien suorituskykyanalyysin tekeminen luotettavasti voi olla vaikeaa, erityisesti jos ajoajat ovat lyhyitä Tällä kurssilla käytetään joissain paikoin ScalaMeter-työkalua (https://scalameter.github.io/) Se yrittää ottaa nämä virtuaalikoneisiin ja roskankeräämiseen liittyvät piirteet huomioon mm. lämmittelemällä virtuaalikonetta ja käyttämällä tilastollisia menetelmiä Lisää tietoa löytyy työkalun dokumentaatiosta ja artikkelista A. Georges et al: Statistically rigorous java performance evaluation, Proc. OOPSLA 2007, pp. 57-76 33/55

Joitakin perustietorakenteita 34/55

Kerrataan joitain tietorakenteita ja... tutustutaan joihinkin toisiin yksinkertaisiin lineaarisiin rakenteisiin Nämä eivät ole vain Scalassa käytettyjä rakenteita Tarkastellaan, minkä operaatioiden tehokasta toteutusta rakenteet tukevat Tällaista tehokkaiden operaatioiden joukkoa (ja niiden toiminnan määrittelyä) sanotaan usein abstraktiksi tietotyypiksi 35/55

Taulukot Sisäänrakennettu tyyppi useimmissa ohjelmointikielissä (Java-virtuaalikone, C/C++ jne) Sekvenssi, jossa n alkiota Vakioaikainen alkioiden luku ja kirjoitus Alkion etsiminen vie pahimmassa tapauksessa ajan Θ(n) (jos taulukkoa ei ole järjestetty binäärihakua varten tms) Taulukon koko n määrätään jo luomisvaiheessa, kun sille varataan yhtäjaksoinen muistijakso Alkioita ei voi lisätä tämän jälkeen alkuun tai loppuun taulukon kokoa kasvattaen. Jos näin tahdotaan tehdä, pitää varata uusi isompi taulukko ja kopioida vanha taulukko sekä uusi alkio siihen; tämän aikavaatimus on jälleen Θ(n) 36/55

Java- ja Scala-kielissä taulukon alkio ovat joko alkeistyyppejä (Int, Long jne) tai viittauksia olioihin, jotka sijaitsevat toisaalla muistissa Esimerkki: oliokaavio taulukolle kokonaislukuja (vasemmalla) ja taulukolle kokonaislukupareja (oikealla) Array[Int] Array[MinMaxPair] arr 0 20 1 42 2 10 3 11 4 20 5-3 arr 0 1 2 MinMaxPair MinMaxPair Oikeanpuoleisen taulukon kolmas alkio on null-viittaus, joka ei viittaa yhteenkään olioon Toiset ohjelmointikielet, kuten C ja C++, sallivat taulukoiden sisältää myös rakenteellisia olioita min 10 max 32 min 21 max 42 37/55

Dynaamiset taulukot ArrayBuffer Scalassa, vector C++-kielen standardikirjastossa Mahdollistavat kuoletetun vakioaikaisen alkioiden lisäämisen taulukon loppuun Idea: varataan hieman tarvetta suurempi taulukko a ja muistetaan taulukon a kapasiteetti c eli taulukon koko 2 ja taulukon tämänhetkinen koko s eli kuinka moni c:stä alkiosta on itse asiassa käytössä alla olevassa taulukossa a Alkion lisääminen dynaamisen taulukon loppuun: jos s < c, niin alkio kirjoitetaan taulukon a kohtaan s ja muuttujaa s kasvatetaan yhdellä, tai jos s = c, 1. varataan uusi taulukko kokoa c α, missä α > 1 on kasvukerroinvakio (yleensä 2), 2. kopioidaan vanhan taulukon alkiot uuden alkuun ja 3. asetetaan c = c α, kirjoitetaan alkio kohtaan s uudessa taulukossa ja kasvatetaan muuttujaa s ydellä 2 Java- ja Scala-kielissä tämä on sisäänrakennettu mutta esim. C/C++ -kielissä taulukot eivät sisällä mitään tämänkaltaista lisätietoa 38/55

Esimerkki: Alkion lisääminen täyteen dynaamiseen taulukkoon Luvun 21 lisääminen alla olevaan täyteen dynaamiseen taulukkoon ResizableArray[Int] cap size array arr 4 4 Array[Int] 0 1 20 42 2 10 3 11 käynnistää laajennusoperaation ja tuloksena saadaan ResizableArray[Int] cap size array arr 8 5 Array[Int] 0 1 20 42 2 10 3 11 Array[Int] 0 20 1 42 2 10 3 11 4 5 6 7 21 Vanhan taulukon muistin vapauttamisesta huolehtii joko roskankeräämisalgoritmi (Java, Scala, Python) tai lisäyksen suorittava kirjastokoodi (C, C++ jne) 39/55

Esimerkki: alkioiden lisääminen ArrayBuffer-luokassa, lähdekoodia luokista ArrayBuffer ja ResizableArray / * * Appends a s i n g l e element to t h i s b u f f e r and r e t u r n s * the i d e n t i t y of the b u f f e r. I t takes constant amortized time. * / def +=(elem : A) : this. type = { ensuresize ( size0 + 1) array ( size0 ) = elem. asinstanceof [ AnyRef ] size0 += 1 this } / * * Ensure t h a t the i n t e r n a l array has at l e a s t n c e l l s. * / protected def ensuresize ( n : I n t ) { val arraylength : Long = array. length / / Use a Long to prevent overflows i f ( n > arraylength ) { var newsize : Long = arraylength * 2 while ( n > newsize ) newsize = newsize * 2 / / Clamp newsize to I n t. MaxValue i f ( newsize > I n t. MaxValue ) newsize = I n t. MaxValue val newarray : Array [ AnyRef ] = new Array ( newsize. t o I n t ) scala. compat. Platform. arraycopy ( array, 0, newarray, 0, size0 ) array = newarray } } 40/55

Taulukon koon kasvattaminen kertoo sen koon vakiolla α ja täten taulukkojen koko kasvaa eksponentiaalisesti Eikö eksponentiaalinen kasvu ole huono asia? Ei tässä tapauksessa: jos vain lisäämme alkioita, niin taulukon täyttöaste on aina vähintään 1 α. Jos α = 2, niin tuhlataan vain korkeintaan sama määrä muistia mitä käytetään alkioiden tallentamiseen Verrataan tätä linkitettyihin listoihin, joissa jokainen solu sisältää ainakin yhden talletetun alkion ja viittauksen seuraavaan soluun listassa 3 linkitetyt listat tuhlaavat myös suurinpiirtein saman määrän muistia 3 Java- ja Scala-kielissä solu sisältää myös luokkatietoa 41/55

Miksi valittiin eksponentiaalinen kasvattaminen eikä vain taulukon koon kasvattamista jollain kohtuullisen isolla määrällä, esim. 1024, alkioita? Koska eksponentiaalinen kasvattaminen mahdollistaa lisäysten vaatiman ajoajan kuolettamisen vakioaikaiseksi Eli jos lisäämme n alkiota dynaamiseen taulukkoon, niin jotkut lisäämiset eli ne, joilla kasvattaminen tapahtuu, ovat ajoajallisesti kalliita koska joudutaan varaamaan muistia ja kopioimaan alkioita, mutta kokonaisajoaika kuoletettuna jokaiselle lisäämiselle on vakio. 42/55

Tarkastellaan jokaisella lisäyksellä tehtyjen muistioperaatioiden määrää (tämä on lineaarisessa suhteessa ajoaikaan) Oletetaan, että α = 2, ollaan jo lisätty n alkiota alkuaan tyhjään dynaamiseen taulukkoon ja taulukon kokoa joudutaan kasvattamaan kun lisätään seuraava alkio Niiden ylimääräisten muistioperaatioiden määrä, jotka joudutaan tekemään kopioitaessa edellisen taulukon alkioita uusiin tällä ja kaikilla edellisillä lisäyksillä yhteensä saadaan rekursioyhtälöstä T(d) = 0 ja T(n) = n + T(n/2), missä d on taulukon alkukoko. Nyt T(n) n + T(n/2) n + n/2 + n/4 + n/8... 2n Lisätään tähän vielä 1 muistioperaatio per lisäys (uuden alkion kirjoittaminen) ja saadaan, että muistioperaatioiden määrä kuoletettuna jokaiselle lisäykselle on 2n+n n = 3 Vastaavasti jokaiselle kasvukerroinvakiolle α > 1 saataisiin lisäkustannukseksi korkeintaan n i=0 1 = n 1 = n α α i 1 α 1 α 1 = O(n) ja kuoletettu kustannus täten jälleen vakio Esim. kun α = 1.1, niin kuoletettu kustannus lisäyksille on 11 + 1 = 12 muistioperaatiota 43/55

Kasvukerroinvakiolle 2 voidaan edellä esitettyä analyysiä visualisoida graafisestikun lisätään 9 alkiota dynaamiseen taulukkoon, jonka koko on alussa 1: arr ResizableArray[T ] cap size array c s Array[T ] 0 e0 1 e1 2 e2 3 e3 4 e4 5 e5 6 e6 7 e7 8 9 10 11 12 13 14 15 e8 Siniset pallurat ovat normaaleja muistiperaatioita, jotka johtuvat uuden alkio kirjoittamisesta, punaiset edellisten laajennusten kopiointioperaatioista ja pinkit nykyisen laajennuksen kopioinnista Jos romautetaan punaiset tornit oikealle ja pinkki vasemmalle, saadaan arr ResizableArray[T ] cap size array c s Array[T ] 0 e0 1 e1 2 e2 3 e3 4 e4 5 e5 6 e6 7 e7 8 9 10 11 12 13 14 15 e8 mistä näkyy, että jokaiselle lisäykselle kuoleentuu 3 palluraa 44/55

Tulisiko dynaamisen taulukon kokoa supistaa jos siinä on, poistojen jälkeen, vähän alkioita kapasiteettiin nähden? Kyllä, tämä voidaan tehdä niin, että sekä taulukon loppuun tehtyjen lisäysten että poistojen kuoletettu ajoaika pysyy vakiona Mutta supistuksen käynnistävä täyttöasteen c s ala-arvo saa olla korkeintaan kasvukerroinvakion käänteisluku. Eli esim. jos taulukon kokoa kasvatetaan kaksinkertaiseksi lisäyksen yhteydessä ja sen jälkeen heti poistetaan alkio lopusta, niin taulukon kokoa ei tule supistaa heti Jos kasvukerroinvakio on 2.0 ja supistusvakio 0.25, niin lisäys- ja poisto-operaatiot ovat kuoletetusti vakioaikaisia, ks. luku 17.4 kirjassa Introduction to Algorithms, 3rd ed. (online via Aalto lib) 45/55

Linkitetyt listat Kertausta Ohjelmointi 2-kurssin rekursio -kierrokselta: muuttumattomat linkitetyt listat Listoja ei muokata luomisen jälkeen, solut voivat olla jaettuja usean listan kesken Alkioiden lisääminen alkuun on vakioaikaista, loppuun lineaariaikaista Esimerkki: Muutumattomat linkitetyt listat Linkitetty lista l: Cons Cons Cons Nil value l 10 next value 21 next value 33 next 46/55

Linkitetyt listat Kertausta Ohjelmointi 2-kurssin rekursio -kierrokselta: muuttumattomat linkitetyt listat Listoja ei muokata luomisen jälkeen, solut voivat olla jaettuja usean listan kesken Alkioiden lisääminen alkuun on vakioaikaista, loppuun lineaariaikaista Esimerkki: Muutumattomat linkitetyt listat Lista k, joka on listan l häntä : Cons Cons Cons Nil value l 10 next value 21 next value 33 next k 47/55

Linkitetyt listat Kertausta Ohjelmointi 2-kurssin rekursio -kierrokselta: muuttumattomat linkitetyt listat Listoja ei muokata luomisen jälkeen, solut voivat olla jaettuja usean listan kesken Alkioiden lisääminen alkuun on vakioaikaista, loppuun lineaariaikaista Esimerkki: Muutumattomat linkitetyt listat Lista m, joka saatu listasta l lisäämällä 11 alkion 21 jälkeen: l Cons value 10 next Cons value 21 next Cons value 33 next Nil k m Cons Cons Cons value next value next value next 10 21 11 48/55

Linkitetyistä listoista on myös muuttuvatilainen versio Soluja ei nyt jaeta eri listojen kesken Alkioiden lisäys listaan voidaan tehdä varaamalla uusi solu ja muokkaamalla listan solujen viittauksia Jos viittaus listan viimeiseen soluun pidetään muistissa, voidaan myös listan loppuun lisätä alkioita vakioajassa Listan alkioiden lukumäärää voidaan myös pitää muistissa, jolloin pituuskyselyt ovat vakioaikaisia Esimerkki: Muuttuvatilaiset linkitetyt listat Muuttuvatilainen linkitetty lista: l List n first last 3 Entry Entry Entry value next value next value next 10 21 33 49/55

Linkitetyistä listoista on myös muuttuvatilainen versio Soluja ei nyt jaeta eri listojen kesken Alkioiden lisäys listaan voidaan tehdä varaamalla uusi solu ja muokkaamalla listan solujen viittauksia Jos viittaus listan viimeiseen soluun pidetään muistissa, voidaan myös listan loppuun lisätä alkioita vakioajassa Listan alkioiden lukumäärää voidaan myös pitää muistissa, jolloin pituuskyselyt ovat vakioaikaisia Esimerkki: Muuttuvatilaiset linkitetyt listat Sama lista sen jälkeen kun on lisätty 11 alkion 21 jälkeen: l List n first last 4 Entry Entry Entry Entry value next value next value next value next 10 21 11 33 50/55

Kahteen suuntaan linkitetyt listat ovat edellisen laajennus, jossa jokainen solu sisältää myös viittauksen edelliseen soluun Tällaisia listoja voidaan käydä läpi tehokkaasti myös käänteisessä järjestyksessä Esimerkki: Muuttuvatilainen kahteen suuntaan linkitetty lista Edellisen esimerkin listan kahteen suuntaan linkitetty versio: List l n first last 4 Entry Entry Entry Entry prev prev prev prev value next value next value next 10 21 11 value 33 next Jos saadaan viittaus johonkin listan soluun, niin solun poistaminen samoin kuin alkion lisääminen juuri ennen sitä tai sen jälkeen on helppoa johtuen viittauksista edeltävään ja seuraavaan soluun 51/55

Muuttuvatilaiset lista Scala-kielessä: ListBuffer (lähdekoodi) Aiemmat versiot LinkedList ja DoubleLinkedList eivät ole enää suositeltuja C++-kielessä luokka list toteuttaa kahteen suuntaan linkitetyt listat 52/55

Jonot Jonot (engl. queue) viittaavat yleensä ensin sisään, ensin ulos -jonoihin (engl. first-in-first-out, FIFO) Perusoperaatiot ovat enqueue, joka lisää alkion jonon loppuun ja dequeue, joka poistaa alkion jonon alusta Tällaiset jonot on helppo toteuttaa kahteen suuntaan linkitetyillä listoilla niin, että sekä enqueue- ja dequeue-operaatiot ovat vakioaikaisia Scalassa muutuvatilainen Queue on toteutettu linkitetyillä listoilla (sisäinen MutableList-luokka) C++-kielen standardikirjastossa queue-luoka toteuttaa jonot ( enqueue on push ja dequeue on pop ) 53/55

Pinot Pinot ovat jonojen tapaisia mutta viimeksi sisään, ensin ulos -jonokurilla (engl. last-in-first-out, LIFO) Perusoperaatiot ovat push, joka laittaa alkion pinon päälle ja pop, joka poistaa pinon päällimmäisen alkion Pinot on myös samoin helppo toteuttaa linkitettyjen listojen tai dynaamisten taulukoiden avulla Scalassa sekä muuttumattomat että muuttuvatilaiset pinot on toteutettu listojen avulla C++-kielessä: stack-luokka 54/55

Lisää tietorakenteita seuraavilla kierroksilla! 55/55