58131 Tietorakenteet ja algoritmit (syksy 2015) Harjoitus 2 (14. 18.9.2015) Huom. Sinun on tehtävä vähintään kaksi tehtävää, jotta voit jatkaa kurssilla. 1. Erään algoritmin suoritus vie 1 ms, kun syötteen koko on n = 10. Kuinka kauan suoritus kestää, kun syötteen koko on 1000 ja algoritmin aikavaativuus on suuruusluokkaa (a) Θ(logn) (b) Θ(nlogn) (c) Θ(n 2 ) (d) Θ(2 n )? Kussakin tapauksessa oleta, että alemman kertaluvun termit aikavaativuudessa voidaan jättää huomiotta; ts. esim. kohdassa (c) oleta, että aikavaativuus on cn 2 jollain vakiolla c. 2. Mitkä seuraavista väittämistä ovat tosia ja mitkä ovat epätosia? Todista käyttäen suoraan määritelmää. (a) 2n 2 +5 = O(n 3 ) (b) 10n 2 +7n+5 = Θ(n 3 ) (c) 5n 3 +n+5 = Ω(n 3 ) (d) 2nlogn+3n = O(n 2 ) (e) logn = O(5) (f) log(n 2 ) = Θ(logn) (g) 3 n = O(2 n ) 3. Analysoi seuraavien algoritmien aikavaativuus O-merkinnällä (ei tarvitse todistaa tarkasti): (a) (b) (c) if n <= 0 f(n-10) 1
(d) (e) (f) (g) (h) f(n/3) 4. O-filosofiaa (a) Tehtävänä on toteuttaa algoritmi, jolle annetaan n lukua sisältävä taulukko ja joka laskee taulukon lukujen summan. On helppoa keksiä algoritmi, jonka aikavaativuus on O(n): tavanomainen for-silmukka, joka käy luvut läpi ja laskee niiden summan muuttujaan. Perustele, miksi O(n) on myös paras mahdollinen aikavaativuus ongelman ratkaisevalle algoritmille. (b) Tilavaativuus tarkoittaa, kuinka paljon muistia algoritmi käyttää syötteen lisäksi. Ei ole harvinaista, että algoritmin aikavaativuus on O(n), mutta tilavaativuus on vain O(1). Tällöin algoritmi tulee toimeen kiinteällä määrällä apumuuttujia. Onko sen sijaan mahdollista, että algoritmin aikavaativuus on O(1), mutta tilavaativuus on O(n)? (c) Tarkastellaan taulukkoa, jossa on kokonaislukuja: 5 2 1 3 1 Koko taulukon sisällön voi kutistaa yhteen kokonaislukuun kertomalla peräkkäisiä alkulukuja, joiden potenssit ovat taulukon luvut: 2 5 3 2 5 1 7 3 11 1 = 5433120. Tästä luvusta saa selville alkuperäiset luvut etsimällä luvun alkutekijät ja lukemalla niiden potenssit. Luvuille tuntuu siis olevan tuhlaavaista varata viisi kohtaa taulukossa, kun yksikin riittäisi: 2
5433120 - - - - Sama menetelmä tepsii mille tahansa taulukolle, jossa on n kokonaislukua. Näyttää siltä, että alkuperäisen taulukon tilavaativuus on O(n), kun taas uuden taulukon tilavaativuus on vain O(1). Onko johtopäätös oikea? 5. Tarkastelemme erään funktion laskemista rekursiivisesti ja iteratiivisesti. (a) Sivulla 24 on annettu palautuskaava kertomalle n!. Kirjoita tämän perusteella rekursioon perustuva java-metodi. Määritä teoreettisesti algoritmin aika- ja tilavaativuus. Aja ohjelmasi ja tarkastele aikavaativuuden muutosta eri n:n arvoilla. Miten isoja n:n arvoja voit huoletta ajaa ohjelmallasi? (b) Kirjoita kertoman laskemiseen iteratiivinen java-metodi. Mikä on siinä esiintyvän silmukan invariantti? Perustele invariantin avulla, että algoritmi toimii oikein. Määritä teoreettisesti algoritmin aika- ja tilavaativuus. Aja ohjelmasi ja tarkastele aikavaativuuden muutosta eri n:n arvoilla. Miten isoja n:n arvoja voit huoletta ajaa ohjelmallasi? (c) Etsi internetistä suora kaava kertoman likimääräiselle laskemiselle (ns. Stirlingin kaava). Kirjoita kertoman likimääräiseen laskemiseen java-metodi, joka käyttää Stirlingin kaavaa. Määritä teoreettisesti algoritmin aikavaativuus. Miten isoja n:n arvoja voit huoletta ajaa ohjelmallasi? 6. (a) Tee metodi, joka tulostaa yhden rivin, jolla on parametrina annettu määrä tähtimerkkejä. Sen lisäksi funktio kutsuu itseään rekursiivisesti, jos parametrin arvo on positiivinen. Metodin runko näyttää seuraavalta: private static void tahtia(int lkm) { // tulosta lkm tahtea // kutsu funktiota rekursiivisesti tulostamaan lkm-1 tahteä Käyttöesimerkki: public static void main(string[] args) { tahtia(3); Ruudulle pitäisi tulostua Huom: yksittäinen metodin tahtia-komento-osan suoritus siis tulostaa ainoastaan yhden tähtirivin. Eli edellisessä esimerkissä metodi tulee kutsutuksi yhteensä 3 kertaa, vaikka main tekeekin ainoastaan yhden kutsun. (b) Pienellä muutoksella ohjelma saadaan tulostamaan tähdet päinvastaisessa järjestyksessä, eli esim. kutsulla tahtia(3) tulostuu 3
Kokeile, mikä muutos saa tämän aikaan. Muutos liittyy rekursiivisen metodikutsun paikkaan ja onnistuu yhtä koodiriviä siirtämällä. Et tarvitse esim. mitään apumuuttujia. (c) Yksittäinen metodi voi kutsua itseään rekursiivisesti useampaan kertaan. Kirjoita metodin runkoon kaksi rekursiivista kutsua. Laittamalla kutsut sopiviin paikkoihin saadaan pääohjelmassa kutsumalla tahtia(2) aikaan kuvio Ja kutsumalla tahtia(3) kuvio Kokeile, mikä muutos saa tämän aikaan. (d) Muokkaa edellistä ohjelmaa siten, että mukaan tulee staattinen muuttuja, jonka avulla voidaan laskea, kuinka monennesta rekursiivisesta kutsusta on kysymys. Tulosta jokaisen tähtirivin perään sen aiheuttaneen rekursiivisen kutsun järjestysnumero. Seuraavassa hahmotelma: static int kutsut; public static void main(string[] args) { kutsut = 1; tahtia(3); private static void tahtia(int lkm) { // otetaan talteen monesko kutsu itse ollaan ja kasvatetaan // kutsujen yhteenlaskettua lukumäärää int kutsunnumero = kutsut++; //... // tulosta lkm tahtea ja tulosta kutsunnumero //... 4
Edellisen kohdan kutsun tahtia(3) pitäisi näyttää suunilleen seuraavalta (huom. jos kutsut koodissasi rekursiivisesti myös tahtia(0) voi numerointi mennä hieman eri tavalla): 3 2 4 1 6 5 7 (e) Kun alat hallita rekursion toimintaperiaatteen, saat pienellä muokkauksella (lisäämällä kolmannen rekursiokutsun) saat ohjelmasi toimimaan esim. seuraavasi: 3 4 5 2 7 8 9 6 11 12 13 10 1 Tee tämä muutos. 5