Kokeelliset tutkimukset 2. Algoritmien analysointimenetelmistä Tietokoneohjelmien suoritusaika on usein tärkeä kysymys, erityisesti käsiteltäessä paljon tietoa tai prosessin ollessa monimutkainen, so. runsaasti aikaavievä. Monesti toinen olennainen 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) Algoritmin suoritusaika riippuu useista tekijöistä. Näitä voidaan tutkia ohjelmatoteutuksilla kokeellisesti ja yleisillä menetelmillä syötealkioiden määrä n Kuva 2.1. Tapauksessa (a) oli hitaampi tietokone kuin tapauksessa (b). teoreettisesti. 2. luku 14 2. luku 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. luku 16 Tämän luvun 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. luku 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. luku 18 Algorithm arraymax(a,n): Input: Taulukko A sisältää n kokonaislukua. Output: Taulukon maksimialkio. currentmax A[0] for 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. luku 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. luku 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. luku 21
Mitä on pseudokoodi? Repeat-silmukat: repeat toiminnot until ehto. Toiminnot selvennetään sisennyksen avulla. 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. luku 22 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. luku 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). Vakiofunktio Yksinkertaisin mahdollinen funktio on vakiofunktio f(n)=c, jossa c on jokin (positiivinen) vakio. Tällöin c ei riipu argumentin arvosta n. Vakioaika tarvitaan esim. kahden luvun yhteenlaskussa tai vertaamisessa tai arvon asettamisessa muuttujalle. Lineaarinen funktio Lineaarinen funktio on seuraavaksi yksinkertaisin, f(n)=n. Kun on annettu arvo n, f määrittää arvon itsensä. Logaritmit ja eksponentit Logaritmit ja eksponentit ovat alituisesti käytössä suorituskykyanalyysissa. Muistetaan, että log b n = x, jos ja vain jos n = b x. 2. luku 24 2. luku 25
Kuutiollinen funktio ja muut polynomit 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). n log n -funktio Tämä on tärkeä funktio, jossa kasvunopeus on hieman nopeampaa kuin lineaarisessa, mutta hitaampaa kuin neliöllisessä. Neliöllinen funktio Usein esiintyvä funktio on niin ikään neliöllinen, f(n)=n 2. Kuutiollinen funktio on muotoa f(n)=n 3, joka määrittää syötteensä kolminkertaisena tulona. Yleisemmin polynomi yhdistää eriasteiset aina korkeimpaan kokonaislukupotenssiin d 0) asti, f(n)=a 0 + a 1 n + a 2 n 2 + a 3 n 3 + + a d n d, jossa a 0, a 1,, a d ovat vakioita ja a d 0. (Kompleksisuuslaskuissa a d >0 ja muutkin vakiot useimmiten ei-negatiivisia. Mieti, miksi näin on!) Tästä ovat esimerkkejä seuraavat. f(n)=2+5n+n 2 f(n)=1 f(n)= n 2 f(n)=1+ n 3 f(n)=n f(n)=3+4n 2. luku 26 2. luku 27 Eksponenttifunktio Viimeinen tarpeellinen funktio on eksponentiaalinen f(n)=b n, jossa b on positiivinen vakio (yleensä >1 ja usein yhtä kuin 2), kantaluku, ja argumentti n on eksponentti. Tämä määrittää argumentille arvon, joka saadaan kertomalla b nkertaa itsensä kanssa. Tämä hirmuinen funktio edustaa suurinta kasvunopeutta kompleksisuusfunktioilla. 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. geometristen ja aritmeettisten sarjojen summalausekkeet oletetaan tunnetuiksi (a>0, a1): 2. luku 28 2. luku 29
n : 3 2 1 Havainnollinen tapa visualisoida aritmeettista sarjaa on esittää jälkimmäinen edellisistä sarjoista oheisen kuvan mukaan (geometrisesti). 0 1 2 3.. n 0 1 2.. n/2 (a) (b) Kuva 2.2. Kummatkin kuviot esittävät aritmeettista summaa, (a) kaavan vasenta ja (b) oikeaa puolta. Edellisessä on n suorakulmiota korkeudeltaan (myös pinta-alaltaan) 1,, n ja jälkimmäisessä yksi suuri, leveydeltään n/2 ja korkeudeltaan n+1 (n tässä parillinen). n+1 n : 3 2 1 2. luku 30 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. luku 31 Vastaesimerkillä on helppo osoittaa, että 2 4-1=15=35, 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. luku 32 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 n1. 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. luku 33
2.5. Algoritmien analyysi 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. luku 34 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. luku 35 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. luku 36 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. 2. luku 37
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. 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. 2. luku 38 2. luku 39 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. suoritusaika pahin tapaus keskimääräinen aika? paras tapaus syötetapaukset Kuva 2.3. Jonkin algoritmin eri suoritusaikoja eri syötteille. 2. luku 40 2. luku 41
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 merkintäjärjestelmä Edellä mentiin suoraan algoritmin arraymax yksityiskohtiin, mutta sellainen analyysi herättää muutamia kysymyksiä: 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 (kuinka kääntäjä generoi niitä), 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. luku 42 2. luku 43 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. suoritusaika cg(n) f(n) 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. luku 44 n o syötteen koko Kuva 2.4. Ordo-merkintä: funktio f(n) on O(g(n)), sillä f(n) cg(n) kaikille kokonaisluvuille n n 0. 2. luku 45
Esim. 2.2. 7n - 2 on O(n). Perustelu: Määritelmän mukaan pitää löytää vakio c>0 ja kokonaisluku n 0 1 niin, että 7n -2cn 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! 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ä. 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 +520n 3 + 10n 3 +5n 3 = 35n 3, kun n1. Itse asiassa mikä tahansa polynomi a k n k + a k-1 n k-1 + +a 0 on aina O(n k ). Esim. 2.4. 3 log n + log log n on O(log n). 2. luku 46 2. luku 47 Perustelu: 3 log n + log log n 4 log n, kun n2. 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 n1. 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: 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. luku 48 2. luku 49
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 ). Muutamilla tärkeimmillä kompeksisuusfunktiotyypeillä on omat nimensä oheisen taulukon mukaisesti. Taulukko 2.1. Kompleksisuusfunktioiden luokkia. 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. luku 50 2. luku 51 Ordon sukulaisia Esim. 2.7. 3 log n + log log n on (log n). Kuten ordo-merkintä kuvasi funktion olevan pienempi tai yhtä suuri kuin toinen funktio, myös seuraavat merkinnät antavat asymptoottisen vertailutavan. Olkoot f(n) ja g(n) kokonaislukuja reaaliluvuiksi kuvaavia funktioita. Jos 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) (c c ), kun n n 0. Iso theta esittää tilannetta, jossa kaksi funktiota ovat asymptoottisesti yhtä suuria johonkin vakiotekijään asti. Perustelu: 3 log n + log log n 3 log n, kun n2. 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. 2. luku 52 2. luku 53
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 nn 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 nn 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): 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)). 2. luku 54 2. luku 55 Taulukko 2.2. Kasvunopeuksia. 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. 2. luku 56 2. luku 57
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. 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)). 2. luku 58 2. luku 59 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. luku 60 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. 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. luku 61
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. luku 62