Kokeelliset tutkimukset 2. Algoritmien analysointimenetelmistä Tietokoneohjelmien suoritusaika on usein tärkeä kysymys, erityisesti käsiteltäessä paljon tietoa tai prosessin ollessa monimutkainen, so. runsaasti aikaa vievä. Monesti toinen olenainen kysymys on tarvittavan muistitilan määrä. Näitä seikkoja pohditaan seuraavassa ja annetaan menetelmiä erilaisia tietorakenteita käyttävien algoritmien vaatimien suoritusaikojen arviointia varten. Tässä on olennaista arvioida suoritusaikoja matemaattisessa mielessä, ikään kuin algoritmin (matemaattisena) funktiona, joka on riippumaton käytettävästä tietokoneesta. Algoritmin toteutus jossakin koneessa antaa käytännössä tarkkoja suoritusaikoja. 2.1. Ohjelmien suoritusaika Jos algoritmi on toteutettu jossakin tietokoneessa, sitä voidaan ajaa ja tutkia suorituksen vaatimaa aikaa käyttäen erilaisia syötteitä ja syötemääriä. Yleisessä mielessä kiinnostavaa on selvittää suoritusajan riippuvuus syötetiedon eli -datan määrän suhteen. Tätä varten tehdään riittävä määrä testejä eri syötejoukoilla tilastollisesti edustavassa testitilanteessa. Tulokset kuvataan usein visuaalisesti tai taulukoin seuraavan kuvan tapaan. suoritusaika t [ms] (a) (b) syötealkioiden määrä n Algoritmin suoritusaika riippuu useista tekijöistä. Näitä voidaan tutkia ohjelmatoteutuksilla kokeellisesti ja yleisillä menetelmillä Kuva 2.1. Tapauksessa (a) oli hitaampi tietokone kuin teoreettisesti. tapauksessa (b). 2. kappale 14 2. kappale 15 Ohjelman suoritusaika kasvaa yleisesti syötteen koon kasvaessa, mutta saattaa jonkin verran vaihdella samankokoisilla, mutta erilaisilla syötejoukoilla. Luonnollisesti ajoympäristö ja laitteisto vaikuttavat asiaan, kuten myös käytetty ohjelmointikieli, kääntäjä yms. tekijät. Yleisen arviointimenetelmän tarve Vaikka kokeelliset suoritusaikatutkimukset ovat hyödyllisiä ja tarpeellisia, niillä on kolme rajoitusta: Testit voidaan tehdä ainoastaan rajoitetuille syötejoukoille, jotka eivät kenties ole edustavia kuvaamaan testien ulkopuolelle jääviä tilanteita. On vaikeaa verrata kahden algoritmin tehokkuutta, ellei niitä ole testattu samassa koneessa ja ohjelmistoympäristössä. On tarpeellista toteuttaa ja suorittaa algoritmi suoritusaikojen tutkimiseksi. 2. kappale 16 Tämän kappaleen loppuosassa esitetään yleisiä menetelmiä algoritmien suoritusaikojen arviointia varten: Ottaa huomioon kaikki mahdolliset syötteet. Sallii minkä tahansa kahden algoritmin suhteellisen tehokkuuden arvioinnin ja vertaamisen tavalla, joka on riippumaton laitteisto- ja ohjelmistoympäristöistä. On tehtävissä tutkimalla algoritmin korkeantason kuvausta ilman algoritmin toteutusta ja tämän testausta. Nämä menetelmät liittävät algoritmiin funktion f(n), joka luonnehtii algoritmin suoritusajan syötteen koon n funktiona. Tyypilliset funktiot sisältävät tekijöitä n ja n 2. Jos esim. algoritmin suoritusaika riippuu tekijästä n (lineaarinen), tämä tarkoittaa, että algoritmin suoritusaika minkä tahansa syötteen kooltaan n tapauksessa on enintään cn, jossa positiivinen vakio c riippuu algoritmin lisäksi käytetyistä ohjelmisto- ja laitteistoympäristöistä. Jos jonkin toisen algoritmin suoritusaika on suhteessa tekijään n 2 (neliöllinen), niin tämä on edellistä huomattavasti hitaampi muuttujan n arvon kasvaessa riittävän suureksi. 2. kappale 17
2.2. Pseudokoodi Tietojenkäsittelyssä pitää esittää algoritmeja ei vain koneen ymmärtämässä muodossa, vaan myös ihmisen ymmärtämällä selkeällä ja täsmällisellä tavalla. Ohjelmakoodi käsittää tavallisesti monia yksityiskohtia, jotka eivät ole keskeistä itse algoritmin toiminnan ymmärtämisessä. Luonnollisen kielen ja melko vapaamuotoisen (kuvitellun) ohjelmointikielen yhdistelmä, pseudokoodi, mahdollistaa selkeän, informatiivisen ja tiiviin esitysmuodon. Pseudokoodiesimerkki Taulukon maksimialkion suoraviivaista etsimistä varten käydään silmukassa läpi taulukon alkiot. 2. kappale 18 Algorithm arraymax(a,n): Input: Taulukko A sisältää n kokonaislukua. Output: Taulukon maksimialkio. currentmax A[0] for i 1 to n-1 do if currentmax < A[i] then currentmax A[i] return currentmax Koodi 2.1. Maksimialkion etsintä. Edellä oleva pseudokoodinen esitys saadaan Java-kielisenä esim. seuraavana. 2. kappale 19 Public class ArrayMaxProgram { // testaa maksimialkion taulukosta etsivän algoritmin static int arraymax(int[ ] A, int n) { //etsii maksimialkion kokonaislukutaulukosta A selaamalla //taulukon alkiot ja pitämällä kirjaa eri vaiheissa suurimmasta //löydetystä int currentmax = A[0]; //suoritetaan kerran for (int i=1; i<n; i++) //asetus suoritetaan kerran, vertailu n kertaa ja //lisäys n-1 kertaa if (currentmax < A[i]) //suoritetaan n-1 kertaa currentmax = A[i]; //suoritetaan enintään n-1 kertaa return currentmax; //suoritetaan kerran } 2. kappale 20 public static void main(string args[ ]) { //testausmetodia kutsutaan pääohjelmaa suoritettaessa int [ ] num = { 10, 15, 3, 5, 56, 107, 22, 16, 85 }; int n = num.length; System.out.print( taulukko: ); for (int i=0; i<n; i++) System.out.print( + num[i]); //tulostaa taulukon alkion System.out.println(. ); System.out.println( maksimialkio on + arraymax(num,n) +. ); } } Koodi 2.2. Algoritmi arraymax toteutettuna Javalla kommenttien selvittäessä lauseiden suorituskerrat. 2. kappale 21
Mitä on pseudokoodi? Pseudokoodi on luonnollisen kielen ja korkeantason ohjelmointikielen sekoitusta, joka kuvaa käytettävien tietorakenteiden ja algoritmin pääideat. Pseudokoodille ei ole mitään tarkkaa määrittelyä, mutta seuraavia merkintätapoja noudatetaan tällä kurssilla. Lausekkeet: Käytetään tavanomaisia matemaattisia symboleja numeeristen ja (Boolen) totuuslausekkeiden yhteydessä. Vasen nuoli vastaa asetusoperaatiota ja yksi yhtäsuuruusmerkki vertailuoperaatiota. Metodiesittelyt: Algorithm nimi(param1, param2, ) esittää metodin nimen ja parametrit. Ehtorakenteet: if ehto then toiminnot tosi-arvolle [else toiminnot epätosi-arvolle]. Sisennystä käytetään erottamaan näitä osia toisistaan. While-silmukat: while ehto do toiminnot. Jälleen sisennystä käytetään. 2. kappale 22 Repeat-silmukat: repeat toiminnot until ehto. Toiminnot selvennetään sisennyksen avulla. For-silmukat: for määrittely muuttujan lisäykselle do toiminnot. Jälleen on sisennys käytössä. Taulukon indeksointi: A[i] on taulukon i:s alkio. Alkiot on indeksoitu arvosta 0 arvoon n-1 Javan tapaan. Metodikutsut: olio.metodi(argumentit) (olio voidaan jättää pois, mikäli se on tunnettu). Metodipalautukset: return arvo. Tämä palauttaa metodin arvon metodin kutsuneelle. Pseudokoodia käytettäessä on hyvä muistaa tarkoituksen olevan korkeantason ideoiden esittäminen eikä toteutuskohtaisten yksityiskohtien, mutta näiden seikkojen välillä pitää vallita sopuisa tasapaino. 2. kappale 23 2.3. Suppea matemaattisten välineiden tarkastelu Seuraavaksi käydään läpi diskreetin matematiikan peruskäsitteitä, jotka ovat tarpeen tietorakenteiden ja algoritmien käyttöä tarkasteltaessa, varsinkin suorituskykyanalyysia varten (kompleksisuusanalyysi: suhteellinen suoritusaika ja tarvittava muistila). Logaritmit ja eksponentit Logaritmit ja eksponentit ovat alituisesti käytössä suorituskykyanalyysissa. Muistetaan, että log b a = c, jos a = b c. Kantaluku jätetään tavanomaiseen tapaan pois sen ollessa 2. Logaritmilaskennan normaalit säännöt oletetaan tunnetuiksi. Lyhennysmerkintöinä ovat käytössä log c n tarkoittaen funktiota (log n) c ja log log n tarkoittaen funktiota log (log n). 2. kappale 24 Käytössä ovat myös lattia- ja kattofunktiot eli x ja x. Näistä saadaan suurin kokonaisluku, joka on pienempi tai yhtä suuri kuin x, tai vastaavasti pienin kokonaisluku, joka on suurempi tai yhtä suuri kuin x. Silti kompleksisuusanalyysissa monesti käytetään reaaliarvoisia funktiota pyöristämättä niitä kokonaislukuarvoon. Summalausekkeet Sarjan summat ovat jatkuvasti tarpeen, joten esim. aritmeettisten ja geometristen sarjojen summalausekkeet oletetaan tunnetuiksi: n i 2 n 1 a a = 1+ a + a +... + a = i= 0 1 a n n( n + 1) i = 1+ 2 + 3 +.. + ( n 1) + n = 2 i= 1 n+ 1 2. kappale 25
2.4. Yksinkertaisia todistusmenetelmiä Jotta voitaisiin osoittaa vakuuttavasti jonkin algoritmin kompleksisuus, tarvitaan matemaattista symbolikieltä. Tällaista todistamista varten on olemassa muutamia erityisiä menetelmiä, joita seuraavassa lyhyesti tarkastellaan. Todistus esimerkin perusteella Jotkin väitteet ovat muotoa: On olemassa alkio x joukossa S, jolla on ominaisuus P. Tällöin riittää sellaisen alkion esittäminen. Edelleen on väitteitä muotoa. Jokaisella alkiolla x joukossa S on ominaisuus P. Tämän osoittamiseksi vääräksi pitää esittää sovelias alkio, jolla ei ole ominaisuutta P. Kysymyksessä on vastaesimerkki. Esim. 2.1. Väitetään, että 2 i -1 on alkuluku, kun i on 1:stä suurempi kokonaisluku. 2. kappale 26 Vastaesimerkillä on helppo osoittaa, että 2 4-1=15=3 5, joten luku ei ole alkuluku. Negaation käyttö Negaatiota voidaan hyödyntää todistamisessa. Kaksi päämenetelmää ovat kontrapositiivinen ja ristiriita. Edelliselle sovelletaan mm. tilannetta, jossa on väite: Jos p on tosi, niin q on tosi. Tämän sijasta sovelletaan väitettä: Jos q ei ole tosi, niin p ei ole tosi. Nämä ovat loogisesti tietysti samoja, mutta toisinaan jälkimmäinen, kontrapositiivinen, on helpompi osoittaa oikeaksi. De Morganin lait auttavat usein sen käsittelyssä. Myös ristiriidan osoittamiseksi De Morganin lait voivat olla käyttökelpoisia. Väite q osoitetaan vääräksi ensin olettamalla se oikeaksi ja todistamalla sitten, että oletus johtaa ristiriitaan. 2. kappale 27 Induktio ja silmukkainvariantti Useimmat algoritmien suorituskykyä koskevista väitteistä käsittävät kokonaislukuparametrin n (intuitiivistä käsitettä ongelman koko tarkoittaen). Tällaiset väitteet ovat ekvivalentteja sille, että jokin lause q(n) on tosi kaikille arvoille n 1. Kun tämä käsittää tarkkaan ottaen äärettömän määrän lukuja, väitettä olisi mahdotonta osoittaa todeksi suoraviivaisesti kaikkien alkioiden läpikäynnillä. Induktio on kätevä menetelmä, jolla voidaan osoittaa em. kaltaisia väitteitä oikeaksi. Oletetaan se tunnetuksi tällä kurssilla. Silmukkainvariantti on menetelmä, jolla analysoidaan ja todistetaan silmukkarakenteiden oikeellisuus. Se toimii muodollisesti seuraavaan tapaan. 2. kappale 28 Jotta jokin silmukan lause S saadaan osoitettua oikeaksi, määritellään lause induktiivisesti pienempien lauseiden S 0, S 1,, S k jonon suhteen: 1. Alkuväite S 0 on tosi ennen silmukan aloittamista. 2. Jos S i-1 on tosi ennen iteraation i aloittamista, niin voidaan osoittaa, että S i on tosi iteraation i suorittamisen jälkeen. 3. Lopullinen lause S k osoittaa, että lause S, joka piti todistaa, on tosi. Edellä tarkasteltiin algoritmia arraymax, jossa tavallaan sovellettiin silmukkainvarianttia. 2. kappale 29
2.5. Algoritmien analyysi Seuraavaksi esitetään eri tapoja algoritmien varsinaista kompleksisuusanalyysia varten. Alkeisoperaatiot Algoritmeja tutkittaessa analyyttisesti lähtökohtana ovat pohjimmiltaan seuraavat seikat: 1. Ohjelmoi algoritmi jollakin korkeantason kielellä. 2. Käännä ohjelma jollekin matalantason kielelle. 3. Määrää matalantason kielen jokaiselle käskylle i käskyn käyttämä suoritusaika t i. 4. Määrää matalantason kielen jokaiselle käskylle i suorituskertojen määrä n i, jotka suoritetaan algoritmissa. 5. Laske yhteen tulot n i t i kaikkien käskyjen yli, mikä antaa algoritmin suoritusajan. 2. kappale 30 Tämä menettelytapa antaa yleensä suoritusajan tarkan arvion, mutta menettelyä on vaikea noudattaa, koska se vaatii kääntäjän generoiman matalantason kielen ja käytetyn ohjelmointiympäristön yksityiskohtaista ymmärtämistä. Täten on mielekästä perustaa analyysi suoraan korkeantason koodiin tai pseudokoodiin. Määritellään korkeantason kielen alkeisoperaatioiden joukko, joka on jokseenkin riippumaton käytetystä ohjelmointikielestä ja soveltuu pseudokoodille niin ikään: arvon asetus muuttujalle metodikutsu aritmeettisen operaation suoritus (esim. kahden luvun yhteenlasku) kahden luvun vertaaminen toisiinsa taulukon indeksiarvon laskenta olioviitteen laskenta paluu metodista 2. kappale 31 Alkeisoperaatio vastaa matalantason kielen käskyä käsittäen suoritusajan, joka riippuu laitteistosta ja ohjelmasta, mutta joka on kuitenkin vakio. Sen sijaan, että yritettäisiin määrätä kunkin alkeisoperaation eksakti suoritusaika aikayksiköissä, lasketaan yksinkertaisesti käytettyjen alkeisoperaatioiden lukumäärä ja käytetään tätä lukumäärää t suoritusajan estimaattina eli arviona. Alkeisoperaatioiden määrä on suhteessa jonkin määrätyn tietokoneen ja ohjelmatoteutuksen todelliseen suoritusaikaan, sillä jokainen alkeisoperaatio vastaa vakioaikaista käskyä ja alkeisoperaatioita on ainoastaan rajoitettu määrä. Näin ollen alkeisoperaatioiden määrä t on suhteessa algoritmin todelliseen suoritusaikaan. Tarkastellaan edeltä algoritmin arraymax suoritusaikaa. Seuraava soveltuu sekä pseudokoodiseen että Java-kieliseen esitykseen. Muuttujan currentmax alustaminen arvoksi A[0] vaatii kaksi alkeisoperaatiota, taulukon indeksin asetuksen ja arvon asetuksen. Tämä suoritetaan vain kerran. Silmukan alussa muuttuja i alustetaan arvoksi 1. Tämä vaatii yhden operaation. Ennen silmukan runkoon siirtymistä pitää suorittaa ehdon i<n arvon laskenta eli suorittaa vertailu, joka käsittää yhden alkeisoperaation. Kaikkiaan tämä suoritetaan n kertaa. Silmukan runko suoritetaan n-1 kertaa. Kullakin iteraatiolla arvoa A[i] verrataan muuttujaan currentmax (kaksi operaatiota, indeksilaskenta ja vertailu). Suoritetaan asetus tapauksessa, että ehto toteutui (kaksi operaatiota, indeksilaskenta ja asetus). Sitten muuttujaa i lisätään yhdellä (kaksi operaatiota, yhteenlasku ja asetus). Täten jokaisella iteraatiolla on joko neljä tai kuusi alkeisoperaatiota riippuen vertailun tuloksesta, jolloin silmukan runko sisältää operaatioita ainakin 4(n-1), mutta enintään 6(n-1). Muuttujan currentmax arvon palauttaminen vastaa yhtä operaatiota, joka suoritetaan kerran. 2. kappale 32 2. kappale 33
Laskien alkeisoperaatioiden määrät yhteen saadaan algoritmille arraymax vähintään 2 + 1 + n + 4(n-1) + 1 = 5n ja enintään 2 + 1 + n + 6(n-1) +1 = 7n -2. Paras tapaus tulee silloin, kun A[0] sisältää maksimiarvon, jolloin muuttujan currentmax arvoa ei koskaan aseteta. Pahin tapaus sattuu, kun alkiot ovat kasvavassa järjestyksessä, jolloin ko. muuttujan asetus suoritetaan joka iteraatiolla. Keskimääräisen ja pahimman tapauksen analyysi Algoritmi voi toimia nopeammin tai hitaammin syötteestä riippuen. Mielenkiintoista olisi tällöin tietää keskimääräinen suoritusaika. Tämä on kuitenkin tavallisesti varsin vaikeaa laskea arvon riippuessa syötteiden todennäköisyysjakaumista. Seuraavan kuvan esimerkki havainnollistaa tätä seikkaa, kun suoritusaika saattaa olla mikä tahansa huonoimmasta parhaimpaan. 2. kappale 34 2. kappale 35 suoritusaika pahin tapaus keskimääräinen aika? paras tapaus Keskimääräisen suoritusajan analyysi vaatii suoritusaikojen odotusarvojen laskentaa oletetuille syötejakaumille, mikä voi olla hankala tehtävä. Näin ollen lasketaan tyypillisesti vain pahimman tapauksen suoritusaika. Esim. algoritmin arraymax pahin tapaus on 7n-2 alkeisoperaatiota riippuen syötteen määrästä n. Käsittelemällä juuri pahinta tapausta tiedetään tarkkaan, ettei algoritmi voi toimia sitä hitaammin. Käytännössä se usein toimii keskimäärin paremmin. Joskus näiden ero voi olla suurikin, mutta joskus sitä ei ole lainkaan. 2.6. Asymptoottinen notaatio eli merkkijärjestelmä syötetapaukset Kuva 2.2. Jonkin algoritmin eri suoritusaikoja eri syötteille. Edellä mentiin suoraan algoritmin arraymax yksityiskohtiin, mutta sellainen analyysi herättää muutamia kysymyksiä: 2. kappale 36 2. kappale 37
Tarvitaanko todella niin yksityiskohtaista lähestymistapaa? Miten tärkeää on laskea alkeisoperaatioiden tarkka lukumäärä? Miten tarkkaan alkeisoperaatiot on määriteltävä? Esim. asetuslauseen y = a*x + b alkeisoperaatioiden määrä riippuu tilanteesta (kääntäjästä itse asiassa), kun tekijä a*x voidaan tallettaa väliaikaismuuttujaan. Pseudokoodisen kuvauksen ja korkeantason kielen lauseen toteutus riippuu joka tapauksessa pienestä määrästä alkeisoperaatioita. Niinpä voidaan tehdä yksinkertaistettu analyysi, jossa käytetään operaatioiden määrän estimaattia (ylhäältäpäin) rajoitetulla vakiolla. Lasketaan yksinkertaisesti pseudokoodin vaiheet tai korkeantason kielen lauseet. 2. kappale 38 Hyödyllisempää, kuin juuttua yksityiskohtiin algoritmien suorituskyvyssä, on yleensä selvittää suuruusluokka tai kertaluku, jossa suhteessa syötteen kokoon n kompleksisuus on. Esim. algoritmin arraymax tilanteessa riittää yleisesti tietää sen kasvavan lineaarisessa suhteessa eli suoraan syötteen kokoon n nähden. Muut tekijät sen kompleksisuutta kuvaavassa funktiossa ovat vakioita, jotka vaihtelevat toteutusympäristöstä riippuen. Seuraavaksi esitetään notaatiotavat aika- ja muistitilavaatimuksille. Merkintä iso O Olkoot f(n) ja g(n) funktioita, jotka kuvaavat ei-negatiiviset kokonaisluvut reaaliluvuiksi. Silloin f(n) on O(g(n)), jos on olemassa vakio c>0 ja kokonaislukuvakio n 0 1, joille f(n) cg(n) kaikille kokonaisluvuille n n 0. Tämä merkintä luetaan iso O tai ordo, ja se ilmaisee suuruusluokkaa (ks. seur. kuvaa). 2. kappale 39 Esim. 2.2. 7n - 2 on O(n). suoritusaika cg(n) f(n) Perustelu: Määritelmän mukaan pitää löytää vakio c>0 ja kokonaisluku n 0 1 niin, että 7n - 2 cn jokaiselle kokonaisluvulle n n 0. Helposti nähdään, että ehdot täyttyvät, kun valitaan c=7 ja n 0 =1. Itse asiassa valintavaihtoehtoja on äärettömästi, mutta on mielekästä valita pienimmät mahdolliset, koska näin saadaan tarkat rajat kompleksisuudelle! n o syötteen koko Kuva 2.3. Ordo-merkintä: funktio f(n) on O(g(n)), sillä f(n) cg(n) kaikille kokonaisluvuille n n 0. Tämä merkintä sanoo, että eräs funktio on pienempi tai yhtä suuri kuin muuan toinen johonkin vakiokertoimeen asti ja erityisesti asymptoottisesti, kun n kasvaa kohti ääretöntä. Tämä keskeinen merkintä unohtaa kätevästi vakiotekijät antaen suuruusluokan - tärkeimmän. Algoritmin arraymax tapauksessa syötteen koko n on taulukon alkioiden määrä. 2. kappale 40 2. kappale 41
Lause 2.1. Algoritmin arraymax suoritusaika on O(n). Perustelu: Edellä osoitettiin alkeisoperaatioiden määräksi 7n-2 tälle algoritmille. On siis olemassa positiivinen vakio a, joka riippuu toteutusympäristöstä niin, että suoritusaika on enintään a(7n-2). Valitsemalla c=7a ja n 0 =1 algoritmin aikakompleksisuudeksi saadaan O(n). Esim. 2.3. 20n 3 + 10n log n + 5 on O(n 3 ). Perustelu: 20n 3 + 10n log n + 5 20n 3 + 10n 3 + 5n 3 = 35n 3, kun n 1. Itse asiassa mikä tahansa polynomi a k n k + a k-1 n k-1 + +a 0 on aina O(n k ). Perustelu: 3 log n + log log n 4 log n, kun n 2. Huomaa, että log log n ei olisi määritelty, jos olisi n=1. Esim. 2.5. 2 100 on O(1) (vastaa tässä vakiota ylipäänsä). Perustelu: 2 100 2 100 1 kaikille n 1. Huomaa, että muuttuja n ei esiinny ollenkaan epäyhtälössä, sillä kysymyksessä on vakiofunktioita. Ordo-merkinnän määrittelyn suoraan soveltamisen sijasta yleensä käytetään seuraavia sääntöjä. Lause 2.2. Olkoot f(n), g(n) ja h(n) ei-negatiivisia kokonaislukuja reaaliluvuiksi kuvaavia funktioita. Tällöin ovat voimassa: Esim. 2.4. 3 log n + log log n on O(log n). 2. kappale 42 2. kappale 43 1. f(n) on O(af(n)) kaikille vakioille a>0. 2. Jos f(n) g(n) ja g(n) on O(h(n)), niin f(n) on O(h(n)). 3. Jos f(n) on O(g(n)) ja g(n) on O(h(n)), niin f(n) on O(h(n)). 4. f(n) + g(n) on O(max{f(n), g(n)}). 5. Jos g(n) on O(h(n)), niin f(n) + g(n) on O(f(n) + h(n)). 6. Jos g(n) on O(h(n)), niin f(n)g(n) on O(f(n)h(n)). 7. Jos f(n) on polynomi astetta d (f(n) = a 0 + a 1 n + + a d n d ), niin f(n) on O(n d ). 8. n x on O(a n ) mille tahansa kiinnitetyille x>0 ja a>1. 2. kappale 44 9. log n x on O(log n) mille tahansa kiinnitetylle x>0. 10. log x n on O(n y ) mille tahansa kiinnitetyille vakioille x>0 ja y>0. Pyritään yleensä yksinkertaisimpaan mahdolliseen muotoon, joten vakiokertoimet jätetään esityksestä pois. Esim. 2.6. 2n 3 + 4n 2 log n on O(n 3 ). Perustelu: Käytetään edellisen lauseen sääntöjä: log n on O(n) (10. sääntö; ylöspäin arviointi on mielekäs, koska summalausekkeessa on suurempi tekijä, n 3 ). 4n 2 log n on O(4n 3 ) (6. sääntö). 2n 3 + 4n 2 log n on O(2n 3 + 4n 3 ) (5. sääntö). 2n 3 + 4n 3 on O(n 3 ) (7. sääntö) 2n 3 + 4n 2 log n on O(n 3 ) (3. sääntö). 2. kappale 45
Ordon sukulaisia Muutamilla tärkeimmillä kompeksisuusfunktiotyypeillä on omat nimensä oheisen taulukon mukaisesti. Kuten ordo-merkintä kuvasi funktion olevan pienempi tai yhtä suuri kuin toinen funktio, myös seuraavat merkinnät antavat asymptoottisen vertailutavan. Taulukko 2.1. Kompleksisuusfunktioiden luokkia. vakio logaritminen lineaarinen n log n neliöllinen polynomiaalinen O(1) O(log n) O(n) O(n log n) O(n 2 ) O(n k ) (k 1) eksponentiaalinen O(a n ) (a>1) 2. kappale 46 Olkoot f(n) ja g(n) kokonaislukuja reaaliluvuiksi kuvaavia funktioita. Jos g(n) on O(f(n)) eli on olemassa vakio c>0 ja kokonaislukuvakio n 0 1 niin, että f(n) cg(n), kun n n 0, silloin f(n) on Ω(g(n)) (luetaan f(n) on iso omega g(n) ). Tämä määrittelee asymptoottisesti, että funktio on suurempi tai yhtä suuri kuin toinen määrättyyn vakioon asti. Vastaavasti ilmaistaan f(n) on Θ(g(n)) (iso theta), jos f(n) on O(g(n)) ja f(n) on Ω(g(n)), so. on olemassa reaalilukuvakiot c >0 ja c >0 ja kokonaislukuvakio n 0 1 niin, että c g(n) f(n) c g(n), kun n n 0. Iso theta esittää tilannetta, jossa kaksi funktiota ovat asymptoottisesti yhtä suuria johonkin vakiotekijään asti. 2. kappale 47 Esim. 2.7. 3 log n + log log n on Ω(log n). Perustelu: 3 log n + log log n 3 log n, kun n 2. Tämä osoittaa, että alemman asteen termit eivät ole vallitsevia alarajaa muodostettaessa Ω-mielessä. Tilanne on samanlainen Θ-mielessä. Esim. 2.8. 3 log n + log log n on Θ(log n). Perustelu: Tulos seuraa esimerkeistä 2.4. ja 2.7. Ordon etäisiä serkkuja On esitystapoja sille, että funktio on aidosti pienempi tai aidosti suurempi asymptoottisesti kuin toinen funktio, mutta näitä käytetään harvemmin kuin edellisiä (ordo on tärkein). Määritellään ne silti. Olkoot f(n) ja g(n) kokonaislukuja reaaliluvuiksi kuvaavia funktioita. Tällöin f(n) on o(g(n)) ( f(n) on pikku o g(n) ), jos mille tahansa vakiolle c>0 on olemassa vakio n 0 >0 niin, että f(n) cg(n), kun n n 0. Vastaavasti f(n) on ω(g(n)) ( f(n) on pikku ω g(n) ), jos g(n) on o(f(n)), so. mille tahansa vakiolle c>0 on olemassa vakio n 0 >0 niin, että g(n) cf(n), kun n n 0. Nyt o on intuitiivisesti analoginen tapaukselle pienempi kuin asymptoottisessa mielessä ja ω tapaukselle suurempi kuin asymptoottisessa mielessä. Huomattakoon, että f(n) on o(g(n)) silloin ja vain silloin kun (rajarvon ollessa määritelty): f ( n) lim = 0 g( n) n 2. kappale 48 2. kappale 49
Pääero merkintöjen iso O ja pikku o välillä on, kun edelliselle määritellään, että on olemassa vakio c>0, ja jälkimmäiselle kaikille vakioille c>0. Funktio f(n) tulee merkityksettömäksi merkinnälle pikku o verrattuna funktioon g(n), kun n kasvaa kohti ääretöntä. 2.7. Asymptoottinen analyysi Jos jonkin algoritmin kompleksisuus on Θ(n) ja toisen Θ(n 2 ) samalle ongelmalle, niin edellinen on asymptoottisesti parempi. Jälkimmäinen saattaa kuitenkin joillekin yksittäisille arvoille n antaa paremman suorituskyvyn, esim. pienillä n:nnän arvoilla. Seuraavassa taulukossa funktiot on järjestetty kasvavan kasvunopeuden mukaan. Jos f(n) edeltää funktiota g(n) vasemmalta oikealle taulukossa siirryttäessä, f(n) on o(g(n)). Taulukko 2.2. Kasvunopeuksia. log n n n log n n 2 2 n 1 2 2 4 4 2 4 8 16 16 4 16 64 256 65 536 6 64 384 4 096 1.84 10 19 8 256 2 048 65 536 1.15 10 77 10 1 024 10 240 1 048 576 1.79 10 308 2. kappale 50 2. kappale 51 Asymptoottista näkökulmaa kuvataan myös seuraavassa taulukossa, jossa annetaan laskettavissa oleva ongelman maksimikoko, kun käytettävissä on määrätty suoritusaika, yksi sekunti, minuutti tai tunti ja olettaen kunkin alkeisoperaation vievän 1 µs ajan. Huomattavaa siinä on, miten esim. melko suuri kompleksisuus, kuten O(n 2 ), häviää pienemmille kompleksisuuksille, vaikka näiden vakiokertoimet ovat huomattavasti suuremmat kuin edellisen. Taulukko 2.3. Maksimaalinen ongelman koko käytettävissä olevin ajoin. suoritusaika 1 s 1 min 1 h 400n 2 500 150 000 9 000 000 20n log n 4 096 166 666 7 826 087 2n 2 707 5 477 42 426 n 4 31 88 244 2 n 19 25 31 2. kappale 52 Pohditaan vielä tilannetta, jossa kuvitellaan saadun aiempaa merkittävästi tehokkaampi tietokone prosessoimaan ongelmaa. Oletetaan uuden prosessoinnin olevan 256 kertaa niin tehokas kuin edeltävän arvon m. Taulukko 2.4. Maksimaalisen ongelman koon kasvaminen alkuperäisen suoristusarvon m funktiona, kun prosessointiteho on noussut 256- kertaiseksi. suoritusaika 400n 20n log n 2n 2 n 4 uusi maksimaalinen ongelman koko 256m likimäärin 256((log m)/(7+log m))m 16m 4m 2 n m+8 2. kappale 53
Ordon käytöstä On epätäsmällistä kirjoittaa, että f(n) O(g(n)), sillä ordo-merkintä pitää sisällään suhteen pienempi tai yhtä suuri. Tarkkaan ottaen myös merkintä f(n) = O(g(n)) ei ole täysin oikein, kun yhtäsuuruus ymmärretään tavanomaisessa merkityksessään. Täten on paras kirjoittaa f(n) on O(g(n)). Matemaattisesti oikein on esittää, että f(n) O(g(n)), koska muodollisesti ajatellen kysymyksessä on joukko funktioita. Ordo-lausekkeiden esittäminen on kuitenkin melko vapaata tilanteesta riippuen. Voidaan mm. esittää osa kompleksisuudesta ordon avulla ja osa täsmällisenä lausekkeena, kuten 2n log n + O(n), jossa alempaa astetta oleva tekijä on esitetty suuruusluokaltaan. Huomattakoon myös, että jos f(n) on O(g(n)) + O(h(n)), niin se on myös O(g(n) + h(n)) ja jos f(n) on O(g(n)) O(h(n)), niin se on myös O(g(n) h(n)). Varoitus! Asymptoottisuuden käsitteen kanssa pitää olla varovainen, sillä nämä merkinnät saattavat olla hiukan harhaanjohtavia piilottaessaan vakiotekijät - nämähän saattavat olla hyvin suuria arvoiltaan. Esim. lukua 10 100 tähtitieteilijät pitävät tunnetun maailmankaikkeuden atomien määrän estimattina. Funktio 10 100 n on silti muotoa Θ(n). Jos ongelmalle olisi tällaisen kompleksisuuden mukaisen algoritmin sijasta käytettävissä kompleksisuudeltaan 10n log n oleva algoritmi, tämä jälkimmäinen olisi luonnollisesti ainoa järjellinen valinta, vaikka edellinen on asymptoottisesti nopeampi. Herää kysymys, mikä on yleisessä mielessä nopea algoritmi. Hyvänä nopeudeltaan voidaan pitää sellaisia, joiden suoritusajat ovat enintään luokkaa O(n log n) (kohtuullisin vakiotekijöin). Joissakin yhteyksissä jopa O(n 2 ) voi olla riittävän nopea, kun n on melko pieni. Eksponentiaalinen tapaus O(2 n ) ei ole tehokas koskaan. 2. kappale 54 2. kappale 55 Asymptoottisen algoritmianalyysin esimerkki Sisältäköön taulukko X lukuja n kappaletta. Pitää laskea kumulatiivisesti keskiarvoja taulukkoon A, ts. A[i] on alkioiden X[0],, X[i] keskiarvo, missä i=0,, n-1. Algorithm prefixaverages1(x): Input: Taulukko X, jossa on n syötelukua. Output: Taulukko A, jossa on n keskiarvoa. Olkoon A syöteluvut käsittävä taulukko. for i 0 to n-1 do a 0 for j 0 to i do a a + X[j] A[i] a/(i+1) return taulukko A Koodi 2.3. Keskiarvoja laskeva algoritmi. 2. kappale 56 Tällaisella keskiarvolla on paljon sovelluksia mm. talous- ja tilastotieteissä. Algoritmi lienee idealtaan yksinkertaisin mieleen tuleva. Analysoidaan sen vaativuus. Taulukon A alustus ja palautus vaativat kullekin alkiolle vakiomäärän alkeisoperaatioita, jotka suoritetaan ajassa O(n). Kaksi sisäkkäistä silmukkaaa kontrolloidaaan muuttujilla i ja j. Ulommalla silmukalla on n suorituskertaa, joten ensimmäinen ja viimeinen asetuslause suoritetaan niin ikään n kertaa. Täten nämä asetuslauseet ja silmukkamuuttujan i lisääminen ovat O(n). Sisemmän silmukan runko suoritetaan i+1 kertaa riippuen muuttujan i arvosta. Tällöin sisemmän silmukan runko suoritetaan yhteensä 1+2+3+ +n kertaa, mikä on yhtä kuin n(n+1)/2. Sisemmän silmukan suoritusaika on siis O(n 2 ). Vastaavan ajan vaatii myös silmukkalaskurin muuttujan j lisääminen. Algoritmin kokonaissuoritusaika saadaan eo. kolmen osatuloksen summana, O(n) + O(n) + O(n 2 ). Tämä on O(n 2 ). Ongelmalle on olemassa tätä tehokkaampikin algoritmi. 2. kappale 57