Tietorakenteet ja algoritmit II



Samankaltaiset tiedostot
Algoritmit 1. Luento 2 Ke Timo Männikkö

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

Algoritmit 2. Luento 1 Ti Timo Männikkö

Algoritmit 2. Luento 14 Ke Timo Männikkö

Algoritmit 1. Luento 12 Ti Timo Männikkö

A TIETORAKENTEET JA ALGORITMIT

Algoritmit 1. Luento 3 Ti Timo Männikkö

3. Laskennan vaativuusteoriaa

Tietorakenteet ja algoritmit - syksy

4 Tehokkuus ja algoritmien suunnittelu

Tietorakenteet ja algoritmit Johdanto Lauri Malmi / Ari Korhonen

Algoritmit 1. Luento 12 Ke Timo Männikkö

Nopea kertolasku, Karatsuban algoritmi

Algoritmit 2. Luento 8 To Timo Männikkö

811312A Tietorakenteet ja algoritmit, , Harjoitus 3, Ratkaisu

58131 Tietorakenteet ja algoritmit (syksy 2015)

Tietorakenteet ja algoritmit II

1.4 Funktioiden kertaluokat

Algoritmit 1. Luento 1 Ti Timo Männikkö

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

Tietorakenteet, laskuharjoitus 3, ratkaisuja

Tietorakenteet, laskuharjoitus 7, ratkaisuja

AVL-puut. eräs tapa tasapainottaa binäärihakupuu siten, että korkeus on O(log n) kun puussa on n avainta

TKT20001 Tietorakenteet ja algoritmit Erilliskoe , malliratkaisut (Jyrki Kivinen)

58131 Tietorakenteet ja algoritmit (kevät 2014) Uusinta- ja erilliskoe, , vastauksia

Algoritmit 2. Luento 2 To Timo Männikkö

Algoritmit 2. Luento 2 Ke Timo Männikkö

A ja B pelaavat sarjan pelejä. Sarjan voittaja on se, joka ensin voittaa n peliä.

Algoritmianalyysin perusteet

Tietorakenteet ja algoritmit

Diskreetin matematiikan perusteet Laskuharjoitus 2 / vko 9

Algoritmit 2. Luento 13 Ti Timo Männikkö

Algoritmi on periaatteellisella tasolla seuraava:

Luku 6. Dynaaminen ohjelmointi. 6.1 Funktion muisti

Algoritmit 1. Luento 10 Ke Timo Männikkö

Tutkimusmenetelmät-kurssi, s-2004

ALGORITMIT 1 DEMOVASTAUKSET KEVÄT 2012

Algoritmit 2. Luento 13 Ti Timo Männikkö

Algoritmit 2. Luento 7 Ti Timo Männikkö

Tietorakenteet, laskuharjoitus 1,

Tietorakenteet, laskuharjoitus 2,

Algoritmit 1. Luento 14 Ke Timo Männikkö

Tietorakenteet ja algoritmit I TRAI /SJ Luentomuistiinpanoja

Algoritmit 1. Luento 8 Ke Timo Männikkö

Zeon PDF Driver Trial

Hakupuut. tässä luvussa tarkastelemme puita tiedon tallennusrakenteina

Esimerkkejä polynomisista ja ei-polynomisista ongelmista

Algoritmit 1. Luento 10 Ke Timo Männikkö

Algoritmit 2. Luento 6 To Timo Männikkö

Algoritmit 1. Luento 11 Ti Timo Männikkö

Tietorakenteet ja algoritmit I TRAI /SJ Luentomuistiinpanoja

58131 Tietorakenteet ja algoritmit (syksy 2015) Toinen välikoe, malliratkaisut

Esimerkkejä vaativuusluokista

58131 Tietorakenteet ja algoritmit Uusinta- ja erilliskoe ratkaisuja (Jyrki Kivinen)

Karteesinen tulo. Olkoot A = {1, 2, 3, 5} ja B = {a, b, c}. Näiden karteesista tuloa A B voidaan havainnollistaa kuvalla 1 / 21

T Syksy 2004 Logiikka tietotekniikassa: perusteet Laskuharjoitus 7 (opetusmoniste, kappaleet )

Algoritmit 1. Luento 13 Ti Timo Männikkö

Algoritmit 2. Luento 3 Ti Timo Männikkö

ITKP102 Ohjelmointi 1 (6 op)

Tietorakenteet (syksy 2013)

Algoritmit 2. Luento 13 Ti Timo Männikkö

LIITE 1 VIRHEEN ARVIOINNISTA

1. Osoita, että joukon X osajoukoille A ja B on voimassa toinen ns. de Morganin laki (A B) = A B.

2. Lisää Java-ohjelmoinnin alkeita. Muuttuja ja viittausmuuttuja (1/4) Muuttuja ja viittausmuuttuja (2/4)

LIITE 1 VIRHEEN ARVIOINNISTA

Reaalifunktioista 1 / 17. Reaalifunktioista

Datatähti 2019 loppu

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

(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ä.

811312A Tietorakenteet ja algoritmit III Lajittelualgoritmeista

Algoritmit 2. Luento 4 Ke Timo Männikkö

811312A Tietorakenteet ja algoritmit Kertausta kurssin alkuosasta

Algoritmit 2. Luento 6 Ke Timo Männikkö

Tietotekniikan valintakoe

Tietorakenteet ja algoritmit. Kertaus. Ari Korhonen

3.3 Paraabeli toisen asteen polynomifunktion kuvaajana. Toisen asteen epäyhtälö

Sisällys. 3. Muuttujat ja operaatiot. Muuttujat ja operaatiot. Muuttujat. Operaatiot. Imperatiivinen laskenta. Muuttujat. Esimerkkejä: Operaattorit.

verkkojen G ja H välinen isomorfismi. Nyt kuvaus f on bijektio, joka säilyttää kyseisissä verkoissa esiintyvät särmät, joten pari

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

3. Muuttujat ja operaatiot 3.1

Diskreetin matematiikan perusteet Laskuharjoitus 1 / vko 8

Matematiikan tukikurssi, kurssikerta 3

2.1. Tehtävänä on osoittaa induktiolla, että kaikille n N pätee n = 1 n(n + 1). (1)

V. V. Vazirani: Approximation Algorithms, luvut 3-4 Matti Kääriäinen

811312A Tietorakenteet ja algoritmit I Johdanto

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

Algoritmit 1. Demot Timo Männikkö

Algoritmit 2. Luento 3 Ti Timo Männikkö

811312A Tietorakenteet ja algoritmit Kertausta kurssin alkuosasta

2.2.1 Ratkaiseminen arvausta sovittamalla

Sekalaiset tehtävät, 11. syyskuuta 2005, sivu 1 / 13. Tehtäviä

Algoritmit 1. Luento 13 Ma Timo Männikkö

ITKP102 Ohjelmointi 1 (6 op)

13. Loogiset operaatiot 13.1

MS-A0401 Diskreetin matematiikan perusteet

REKURSIO. Rekursiivinen ohjelma Kutsuu itseään. Rekursiivinen rakenne. Rakenne sisältyy itseensä. Rekursiivinen funktio. On määritelty itsensä avulla

Luku 8. Aluekyselyt. 8.1 Summataulukko

Yhtälönratkaisusta. Johanna Rämö, Helsingin yliopisto. 22. syyskuuta 2014

TIE Tietorakenteet ja algoritmit 1. TIE Tietorakenteet ja algoritmit

LIITE 1 VIRHEEN ARVIOINNISTA

Transkriptio:

Tietorakenteet ja algoritmit II Luentomuistiinpanoja Simo Juvaste Asko Niemeläinen Itä-Suomen yliopisto Tietojenkäsittelytiede

Alkusanat Tämä uuden TRAII kurssin luentomateriaali on kutakuinkin edellisen TRA2 kurssin mukainen. Koska uusi kurssi on laajuudeltaan hieman pienempi, osa asioista jätetään käsittelemättä. Alustava suunnitelma näistä ylimääräisistä asioista on merkitty monisteeseen tähdillä (*). Tarkempi sisältö täsmentyy kurssin edetessä. Materiaalin alkupäässä on runsaasti kertausta TRAI -kurssin asioista. Simo Juvaste Asko Niemeläisen alkuperäiset TRA-kurssin luentomonisteen alkusanat Kokosin nyt käsillä olevat luentomuistiinpanot Joensuun yliopistossa syksyllä 1996 luennoimaani Tietorakenteiden ja algoritmien kurssia varten. Muistiinpanot pohjautuvat vuonna 1993 järjestämäni samannimisen kurssin luentoihin, jotka puolestaan noudattelivat pitkälti Alfred V. Ahon, John E. Hopcroftin ja Jeffrey D. Ullmanin ansiokasta oppikirjaa Data Structures and Algorithms (Addison-Wesley 1983). Kurssin laajuus oli vuonna 1993 vielä 56 luentotuntia, mutta nyttemmin kurssi on supistunut 40 luentotuntia käsittäväksi, minkä vuoksi jouduin karsimaan osan aiemmin järjestämäni kurssin asiasisällöstä. Muutenkaan nämä muistiinpanot eivät enää täysin noudattele mainitsemaani oppikirjaa, sillä käsittelen asiat oppikirjaan nähden eri järjestyksessä, osin eri tavoinkin. Radikaalein muutos aiempaan on se, että pyrin kuvailemaan abstraktit tietotyypit mieluummin käyttäjän kuin toteuttajan näkökulmasta. Lähestymistavan tarkoituksena on johdattaa kurssin kuulijat käyttämään abstrakteja tietotyyppejä liittymän kautta, toisin sanoen toteutusta tuntematta. Toteutuskysymyksiin paneudun vasta kurssin loppupuolella, silloinkin enimmäkseen kuvailevasti. Abstraktien tietotyyppien toteuttamisen ei näet mielestäni pitäisi Ohjelmoinnin peruskurssin jälkeen muodostua ongelmaksi, mikäli tietotyypin käyttäytyminen ja toteutusmallin keskeisimmät ideat ymmärretään. Käyttäjän näkökulman korostaminen on perusteltua myös siksi, että tietorakenne- ja algoritmikirjastojen käytön odotetaan lähivuosina merkittävästi kasvavan. Näiden kirjastojen myötä ohjelmoijat välttyvät samojen rakenteiden toistuvalta uudelleentoteuttamiselta, mutta joutuvat samalla sopeutumaan valmiina tarjolla olevien toteutusten määräämiin rajoituksiin. Tällaisen uuden ohjelmointikulttuurin omaksuminen ei käy hetkessä, vaan siihen on syytä ryhtyä sopeutumaan hyvissä ajoin. Painotan kurssilla algoritmien vaativuusanalyysiä, vaikka hyvin tiedänkin monien tietojenkäsittelytieteen opiskelijoiden aliarvioivan vaativuuden analysoinnin merkitystä. Algoritmiikan tutkimuksessa ei vaativuusanalyysiä voi välttää. Analysointitaitoa on myös helppo hyödyntää jopa hyvin yksinkertaisilta vaikuttavissa ohjelmointitehtävissä. Tahdon lausua kiitokseni kärsivällisille kuulijoilleni, jotka joutuivat keräämään nämä muistiinpanot osa osalta, joskus jopa sivu sivulta, muistiinpanojen valmistumisen myötä. Samoin kiitän perhettäni, joka on muistiinpanojen kirjoittamisen aikana tyytynyt toissijaiseen osaan. Kiitokset myös Tietojenkäsittelytieteen laitoksen kansliahenkilökunnalle sekä Yliopistopainolle tehokkaasta toiminnasta. Erityiset kiitokset ansaitsee vielä muistiinpanot tarkastanut FL Pirkko Voutilainen. Joensuussa 14. elokuuta 1997 Asko Niemeläinen

Sisällysluettelo (moniste 4.3.2011) Luku 1: Kertausta algoritmiikasta ja aikavaativuudesta 1 1.1 Kertausta algoritmien ja tietorakenteiden käsitteistä 2 1.2 Suorituksen vaativuus 3 1.3 Rekursiivisten algoritmien aikavaativuus 8 1.4 Aikavaativuuden rajojen todistaminen 14 1.5 Kokeellinen aikavaativuuden määrääminen 16 1.6 Abstraktit tietotyypit (*) 19 1.7 Kertaus abstrakteihin tietotyyppeihin (*) 21 1.8 Lista (*) 22 1.9 Puu (*) 27 1.10 Hakupuut 28 1.11 Joukot (*) 31 Luku 2: Suunnatut verkot 35 2.1 Käsitteitä 35 2.2 Suunnattu verkko abstraktina tietotyyppinä 37 2.3 Lyhimmät polut 39 2.4 Verkon läpikäynti 43 2.5 Muita verkko-ongelmia 45 Luku 3: Suuntaamattomat verkot 51 3.1 Määritelmiä 51 3.2 Pienin virittävä puu 52 3.3 Suuntaamattoman verkon läpikäynti 56 3.4 Leikkaussolmut ja 2-yhtenäiset komponentit (*) 58 3.5 Verkon kaksijakoisuus, sovitus 59 Luku 4: Algoritmistrategioita 61 4.1 Ahne eteneminen 61 4.2 Hajoita-ja-hallitse 62 4.3 Dynaaminen ratkaiseminen (ohjelmointi) 62 4.4 Tasoitettu aikavaativuus 63 4.5 Haun rajoittaminen 64 4.6 Satunnaistetut algoritmit 64 4.7 Kantalukulajittelu 65 4.8 Merkkijonon haku 67 Luku 5: Ulkoisen muistin käsittely 72 5.1 Tiedostokäsittely 72 5.2 Onko keskusmuisti ulkoista muistia? 75 5.3 Ulkoinen lajittelu (*) 77 5.4 Tiedosto kokoelmana (*) 79 5.5 B-puu 83 Luku 6: Abstraktien tietotyyppien toteuttaminen (*) 85 6.1 Kotelointi ja parametrointi 85 6.2 Verkot 87 Kirjallisuutta 90

Luku 1 Kertausta algoritmiikasta ja aikavaativuudesta Tietorakenteet ja algoritmit I -kurssin pääteemana oli toisaalta erilaisten abstraktien tietotyyppien käyttö ohjelman rakenteen yksinkertaistamiseksi ja toisaalta ohjelmien tehokkuuden arviointi. Tämä Tietorakenteet ja algoritmit II -kurssi kertaa, täydentää ja syventää näitä taitoja. Painotus on hieman enemmän algoritmeissa kuin tietorakenteissa. Uusina abstrakteina tietotyyppeinä esitellään verkot eli graafit ja vastaavasti esitellään muutamia niihin liittyviä algoritmeja. Toisena uutena kokonaisuutena ovat algoritmistrategiat, jotka toimivat myös johdantona Algoritmien suunnittelu ja analysointi -kurssille. Algoritmien aikavaativuuden puolella syvennetään rekursiivisten algoritmien analysoinnin osaamista, tutustutaan kokeellisen aikavaativuuden määrämisen periaatteisiin ja opitaan miten massamuistin käyttö on huomioitava aikavaativuutta analysoitaessa, ts. miten massamuistia kannattaa käyttää. Abstrakteja tietotyyppejä käytetään ohjelman suunnittelussa (ja tietorakenteita ohjelman toteutuksessa) helpottamaan ja selkeyttämään ohjelman toteutusta ja siten rakennetta. Kun tunnemme reaalimaailman kohteet (tiedon), niiden suhteet ja sen, miten niitä käsitellään, meidän tulisi pystyä valitsemaan vastaava abstrakti tietotyyppi johon tiedot ohjelmassa tallennamme. Valitun tietotyypin tulisi tukea tehokkaasti niitä operaatioita (esimerkiksi tietynlainen läpikäynti tai haku) joita tarvitsemme algoritmissamme. Oikeiden tietorakenteiden käyttö myös selkeyttää ja lyhentää ohjelmakoodia kun itse ohjelmassa keskitytään varsinaisen sovellustiedon käsittelyyn tarvitsematta huolehtia tietorakenteen toteutuksesta. Tämä modulaarisuus vähentää virheitä ja helpottaa testausta sekä aikavaativuuden analyysiä. Edelleen, valmiiden tietorakenteiden käyttö vähentää ohjelmointityötä (lisää komponenttien uudelleenkäyttöä). Nykyään näkee usein väitettävän, ettei algoritmien tehokkuus ole enää tärkeää prosessoreiden toimiessa GHz taajuuksilla. Tämä päteekin jos kyseessä ovat vain pienet syötteet joita ajetaan henkilökohtaisella tehokkaalla tietokoneella. Kuitenkin, jokainen meistä on vielä käyttänyt tarpeettoman hitaita ohjelmia ja/tai laitteita nopeutusmahdollisuuksia siis vielä on. Lisääntynyt laskentateho myös usein ulosmitataan käyttämällä suurempaa tai tarkempaa syötettä. Yhteiskäyttöisellä tietokoneella (esimerkiksi palvelimella) kaikki ajansäästö on lisäaikaa muille käyttäjille (ja siten palvelin voi palvella enemmän käyttäjiä). Tähän luokkaan kuuluvat myös erilaiset reitittimet, palomuurit, tukiasemat ja muuta vastaavat laitteet joissa kaikki nopeutus hyödyttää käyttäjiä. Ohjelmia tehdään yhä 1

1. KERTAUSTA ALGORITMIIKASTA JA AIKAVAATIVUUDESTA 2 enemmän myös muille alustoille kuin perinteisille tietokoneille, erityisesti kannettaville, akkukäyttöisille laitteille. Akkukäyttöisellä laitteella jokainen säästetty kellojakso merkitsee paitsi käyttömukavuutta (parempaa vasteaikaa), myös pidempää käyttöaikaa. Useimmissa akkukäyttöisissä laitteissa (ja nykyään myös PC:ssä) suoritusteho on (automaattisesti) säätyvä, jolloin nopeampaa algoritmia käyttäen virransäästötilaan päästään nopeammin ja huipputehoa tarvitaan harvemmin. Näin ollen nopeammalla algoritmilla saadaan pidempi akun kesto. Käytännössä arkipäivän ohjelmoinnissa valinnat ovat melko yksinkertaisia. Vähänkään suuremmalla syötteellä O(n) tai O(nlogn) on huomattavasti parempi kuin O(n 2 )ja O(logn) tai O(1) on huomattavasti parempi kuin O(n). Erot luonnollisesti korostuvat jos/kun algoritmia suoritetaan toistuvasti. 1.1 Kertausta algoritmien ja tietorakenteiden käsitteistä Algoritmi on toimintaohjeisto (käskyjono) joka ratkaisee annetun ongelman äärellisessä ajassa. Yleensä käsittelemme deterministisiä algoritmeja (ts. algoritmeja, jotka toimivat samalla syötteellä aina samalla tavalla). Tällä kurssilla kohdassa 4.6 (s. 64) esittelemme kuitenkin myös satunnaistettujen algoritmien käsitteet ja käyttöä. Algoritmien toivotaan olevan toisaalta tehokkaita (ts. suorittuvan kohtuullisessa ajassa ja kohtuullisella laitteistolla) ja toisaalta niiden tulisi olla toteutettavissa kohtuullisella ohjelmointityöllä. Tällä kurssilla opitaan algoritmien suunnittelumenetelmiä, esimerkkialgoritmeja sekä lukemaan valmiita algoritmikuvauksia. Se, miten ongelma määritellään, on paljolti muiden kurssien asia. Tällä kurssilla keskitytään pienehköihin, erillisiin ongelmiin. Tärkeää olisi oppia näkemään reaalimaailman ongelmasta se/ne yksinkertainen algoritminen ongelma joka voidaan ratkaista tällä kurssilla annettavalla opilla. Samoin tärkeää on oppia näkemään reaalimaailman ongelmasta se/ne abstraktit tietotyypit joita kannattaa käyttää. Esimerkki 1-1: Kokoelmaan lisätään (runsaasti) alkioita joilla on jokin lukuarvo. Alkiota lisättäessä pitäisi saada selville ko. alkion sijoitus lisäyshetkellä. Samoin pitäisi pystyä hakemaan nopeasti esimerkiksi sadas alkio. Minkälaista tietorakennetta kannattaa käyttää? Tietorakenne Abstrakti tietotyyppi on tapa järjestää tietoa. Tietorakenne on kokoelma toisiinsa kytkettyjä tietoja (muuttujia). Käytännössä näitä usein käytetään sekaisin. Tällä kurssilla käsittelemme erityisesti alkioiden välisiä suhteita ja näiden suhteiden ominaisuuksia. Tämän luvun lopuksi kertaamme TRAI -kurssilta tutut tietotyypit: taulukon, listan, puun ja joukon. Luvuissa 2 ja 3 käsittelemme verkkoja eli graafeja. 1.2 Suorituksen vaativuus Aikavaativuutta arvioitaessa on tärkeä ymmärtää, että vaativuudeltaan erilaistenkin algoritmien todellisten suoritusaikojen erot ovat merkityksettömiä pienten ongelmien käsitte-

1. KERTAUSTA ALGORITMIIKASTA JA AIKAVAATIVUUDESTA 3 lyssä. Vasta kooltaan suuret ongelmat paljastavat algoritmin hitauden. Erot voivat tällöin olla dramaattiset. Ongelman pienuus ja suuruus puolestaan ovat suhteellisia käsitteitä. Esimerkiksi lajitteluongelma on pieni, kun lajiteltavana on vain kymmeniä alkioita, mutta joillekin verkkoalgoritmeille 20-solmuinen verkkokin voi olla suuri käsiteltävä. Ohjelman suoritusaikaa voitaisiin mitata seinäkelloajalla. Tällainen mittaaminen ei kuitenkaan tuottaisi vertailukelpoisia tuloksia, koska kulloiseenkin suoritukseen kuluvan ajan pituuteen vaikuttavat monet sellaiset tekijät, joita on vaikea tai jopa mahdoton kontrolloida. Näitä ovat esimerkiksi käytetty kääntäjä ja laitteisto sekä laitteiston hetkelliset kuormituserot. Absoluuttisen suoritusajan mittaamisen asemesta onkin mielekkäämpää arvioida suhteellista suoritusaikaa, jonka arviointi onnistuu paitsi ohjelmille, myös algoritmeille. Tämä on algoritmin suunnittelijan kannalta merkittävä etu, koska sen ansiosta voidaan vertailla keskenään vielä toteuttamattomiakin algoritmeja ja siten välttää tehottomaksi todetun algoritmin toteuttamisesta aiheutuva turha työ. Suoritusajan yksikkönä käytetään joustavaa termiä "askel". Kuva 1-1 havainnollistaa erään tulkinnan, myöhemmin tarkennamme käsitettä edelleen. a = 1; 1 b = 3; 2 (n.) kaksi askelta for (i = 0; i < 1000; i++) 1 a = a + 1; 2 Kuva 1-1: Suoritusaskel. (n.) 1000 askelta Suoritusaika suhteutetaan tavallisesti algoritmin syötteen kokoon. Syöte on tässä ymmärrettävä laajasti: algoritmi voi saada syötteensä suoraan käyttäjältä, tiedostoista tai vaikkapa parametrien välityksellä. Joissakin tapauksissa varsinaista syötettä ei ole lainkaan olemassa. Silloin suorituksen kesto määräytyy esimerkiksi käytettyjen vakioiden arvoista. Satunnaisuuteen perustuvien algoritmien aikavaativuuden arvioinnissa puolestaan sovelletaan todennäköisyyslaskennan menetelmiä. Syötteen tai sen vastineen suoritusajan arvioimisen kannalta merkityksellisen osan tunnistamista varten ei voida antaa täsmällisiä ohjeita. Taito tähän tunnistamiseen kehittyy harjoituksen myötä. Varsin usein tunnistaminen itse asiassa onkin triviaali tehtävä. Syötteen koko saattaa vaihdella suorituskerrasta toiseen, mutta aikavaativuus tulisi pystyä arvioimaan yleisesti algoritmin mielivaltaiselle suoritukselle. Sen vuoksi suoritusaika esitetään syötteen koon funktiona. Jos syötteen kokoa merkitään n:llä, on luontevaa käyttää suoritusaikafunktiolle merkintää T(n). Näin ollen askelten määrä ilmaistaan syötteen funktiona, esimerkiksi: for (i = 0; i < n; i++) 1 a = a + 1; 2 (n.) n askelta Mikäli syöte koostuu useista toisistaan riippumattomista osasista, jotka kaikki ovat aikaarvioiden kannalta merkittäviä, käytetään syötteen koon kuvaamiseen useita muuttujia ja vastaavasti suoritusaikakin ilmaistaan usean muuttujan funktiona. Aina on muistettava varmistaa, että suoritusaikafunktiossa käytettävien tunnusten (esimerkiksi n) merkitys on selkeä, eli kertoa mitä syötteen ominaisuutta ne kuvaavat. Vastaavasti, jos annetun ohjelmanosan syötteen kokoa merkitään jollain muulla kirjaimella, myös aikavaativuusfunktio

1. KERTAUSTA ALGORITMIIKASTA JA AIKAVAATIVUUDESTA 4 annetaan sitä käyttäen. Jos sitten muuttuja korvataan toisella, muutos on tehtävä myös aikavaativuusfunktioon. Esimerkki 1-2: Esimerkiksi merkkijonon etsinnän toisesta merkkijonosta vaativuus riippuu sekä etsittävästä avaimesta, että läpikäytävästä kohdetekstistä. Kuvataan avaimen pituutta m:llä ja kohdetekstin pituutta n:llä. Erään yksinkertaisen etsintäalgoritmin suoritusaikafunktio on T(n, m) = cnm, missä c on vakio. Kertaluokkana O(nm). Funktion T mittayksikköä ei kiinnitetä. Voidaan ajatella, että lasketaan algoritmin suorittamien käskyjen tai muiden keskeisten perustoimintojen lukumäärä. Taito nähdä, minkä toimintojen lukumäärä kulloinkin on mielekästä laskea, kehittyy harjoituksen myötä samoin kuin syötteen koon tunnistamisen taitokin. Funktion lausekkeessa esiintyvien vakioiden todelliset arvot taas riippuvat käytettävästä kääntäjästä ja laitteistosta, joiden vaikutusta ei voida ennakoida. Sen vuoksi näiden vakioiden merkitykselle ei pidä antaa liian suurta painoa. Tärkeimpiä ovat syötteen kokoa sisältävät termit. Suoritusaika ei aina riipu pelkästään syötteen koosta, vaan myös syötteen laadusta. Kun tämä otetaan huomioon, voidaan tarkastelu eriyttää seuraaviin kolmeen tapaukseen: 1) T(n) tarkoittaa pahimman tapauksen suoritusaikaa eli pisintä mahdollista n:n kokoisen syötteen aiheuttamaa suoritusaikaa. 2) T avg (n) tarkoittaa keskimääräistä suoritusaikaa eli kaikkien n:n kokoisten syötteiden aiheuttamien suoritusaikojen keskiarvoa. 3) T best (n) tarkoittaa parhaan tapauksen suoritusaikaa eli lyhintä mahdollista n:n kokoisen syötteen aiheuttamaa suoritusaikaa. Kuten jo merkinnöistäkin nähdään, tarkastellaan yleensä aina pahinta tapausta, ellei erityisesti mainita jostakin muusta tapauksesta. Paras tapaus ei yleensä ole edes mielenkiintoinen. Esimerkiksi lajittelussa paras tapaus on yleensä valmiiksi lajiteltu kokoelma. Tämä tuskin kertoo lajittelualgoritmin hyvyydestä mitään. Keskimääräisen suoritusajan arviointi saattaa puolestaan osoittautua erittäin hankalaksi tehtäväksi, koska kaikki samankokoiset syötteet voidaan vain harvoin olettaa keskenään yhtä todennäköisiksi. Pahimman tapauksen analysointikin voi tosin olla vaivalloista. Jos esimerkiksi sama syöte ei ole pahin algoritmin kaikille osille, joudutaan ensin etsimään kokonaisvaikutukseltaan pahin syöte. Tällä kurssilla sivutaan hieman myös tasoitettua aikavaativuutta (kohta 4.4 (s. 63)). Kertaluokat Kun algoritmien suoritusajat ilmaistaan syötteen koon funktioina, voidaan aikoja vertailla toisiinsa funktioiden kasvunopeuksia vertailemalla. Vakiokerrointen todellisten arvojen häilyvyyden vuoksi tarkastelua ei viedä äärimmilleen, yksittäisten funktioiden tasolle, vaan tarkastellaan funktioiden kertaluokkia. Kertaluokkatarkastelussa käytetään apumerkintöjä O, Ω, Θ ja o, joiden merkitys määritellään seuraavasti:

1. KERTAUSTA ALGORITMIIKASTA JA AIKAVAATIVUUDESTA 5 Määritelmä 1-3: Kertaluokkamerkinnät O, Ω, Θ ja o 1) T(n) =O(f(n)), jos on olemassa positiiviset vakiot c ja n 0 siten, että T(n) cf(n), kun n n 0. [Lue: T(n) on kertaluokkaa f(n), iso-o, ordo; "rajoittaa ylhäältä"] 2) T(n) =Ω(g(n)), jos on olemassa positiiviset vakiot c ja n 0 siten, että T(n) cg(n), kun n n 0. [omega; "rajoittaa alhaalta"] 3) T(n) =Θ(h(n)), jos T(n) =O(h(n)) ja T(n) =Ω(h(n)). [theta; "rajoittaa ylhäältä ja alhaalta"] 4) T(n) =o(p(n)), jos T(n) =O(p(n)) ja T(n) Θ(p(n)). [pikku-o; "rajoittaa aidosti ylhäältä"] Ensimmäinen määritelmistä antaa suoritusaikafunktion T kasvunopeudelle ylärajan: kyllin suurilla n:n arvoilla funktio T kasvaa enintään yhtä nopeasti kuin vakiolla c kerrottu funktio f. Toinen määritelmä antaa vastaavasti kasvunopeuden alarajan: kyllin suurilla n:n arvoilla funktio T kasvaa vähintään yhtä nopeasti kuin vakiolla c kerrottu funktio g. Kolmas määritelmä sitoo funktion T kasvunopeuden samaksi kuin on funktion h kasvunopeus. Viimeinen määritelmä rajaa funktion T kasvunopeuden aidosti hitaammaksi kuin funktion p kasvunopeus, eli jollakin vakiolla c on T(n)<cp(n), kun n on kyllin suuri. Määritelmät esitetään joissakin lähteissä hieman eri muotoisina, mutta olennaisesti samaa tarkoittavina. Useimmissa lähteissä ja tällä kurssilla käytetään lähinnä vain ylärajan ilmaisevaa O-merkintää, vaikka usein tarkoitetaan täsmällisempää Θ-merkintää. Ylärajaominaisuuden käyttöä helpottaa sen transitiivisuus, toisin sanoen jos f(n) = O(g(n)) ja g(n) = O(h(n)), niin silloin myös f(n) = O(h(n)). Tämä merkitsee, että ylärajoja on aina useita. Θ-merkinnän määräämä rajafunktio sen sijaan on yksikäsitteinen: mahdollisimman tiukka. Ylärajoistakin pyritään aina löytämään tiukin, jotta kertaluokkavertailut vastaisivat tarkoitustaan. Joskus määritellään myös aito alarajaominaisuus ω (pikku-omega). Siis T(n) = ω(p(n)), jos T(n) = Ω(p(n)) ja T(n) Θ(p(n)). Tätä voidaan käyttää esimerkiksi ilmaisemaan, että jokin algoritmi on hitaampi kuin esittämämme alaraja. Esimerkki 1-4: Olkoon T(n) = 5n+2. Silloin T(n) = O(n), mikä nähdään vaikkapa valitsemalla c = 7 ja n 0 =1: 5n+2 5n+2n = 7n, kun n 1. Ylärajan n rinnalle kelpaisivat myös ylärajat n 2, n 3 ja niin edelleen. Koska T(n) = Ω(n), mikä nähdään esimerkiksi valitsemalla c = 5 ja n 0 = 1, on n myös alaraja, joten n on tiukin yläraja ja itse asiassa T(n)=Θ(n). Määritelmässä 1-3 esiintyvien epäyhtälöiden ei tarvitse päteä arvoilla n < n 0. Usein funktioiden keskinäinen järjestys pienillä n:n arvoilla poikkeaakin siitä järjestyksestä, joka vallitsee tarkasteltaessa suuria n:n arvoja. Esimerkiksi 5n+2 > 6n, kun n = 1, ja vasta kun n 2, on 5n+2 6n. Aikavaativuustarkastelussa pienet n:n arvot ovat merkityksettömiä, koska vasta suuret syötteet paljastavat algoritmien suoritusaikojen kertaluokkien erot. Kertaluokkaa arvioitaessa voidaan määritelmässä 1-3 esiintyvät vakiot c ja n 0 valita vapaasti, kunhan valinta vain toteuttaa määritelmän epäyhtälön. Esimerkin 1-4 yläraja n olisi löytynyt myös valitsemalla c =6 ja n 0 = 12, c =70 ja n 0 = 1 tai jollakin muulla tavoin. Ellei epäyhtälön toteuttavia vakioita laisinkaan löydy, on kertaluokkayrite tarkasteltavaan funktioon nähden väärä. Näytettäessä ettei funktio f(n) ole kertaluokkaa g(n) on

1. KERTAUSTA ALGORITMIIKASTA JA AIKAVAATIVUUDESTA 6 toisin sanoen osoitettava, ettei ole olemassa sellaisia positiivisia vakioita c ja n 0, että epäyhtälö f(n) cg(n) pätisi kaikilla arvoilla n n 0. Esimerkki 1-5: Näytetään, ettei funktio T(n) = 5n+2 ole kertaluokkaa n : Jos olisi T(n) =O( n ), niin määritelmän 1-3 nojalla olisi olemassa positiiviset vakiot c ja n 0 siten, että 5n+2 c n, kun n n 0. Tällöin olisi myös 5n c n. Tämä epäyhtälö voidaan järjestellä uudelleen: 5n c n 5 n c n ( c 5) 2. Koska c on vakio, on myös (c/5) 2 vakio. Tämä merkitsee, ettei epäyhtälö 5n+2 c n toteudu ainakaan silloin, kun n > max{n 0,(c /5) 2 }, mikä on vastoin oletusta. Törmäsimme ristiriitaan, eli väite on väärä. Kertaluokkatarkastelussa ei suoritusaikafunktioiden lausekkeissa esiintyvillä vakiokertoimilla ja vakiotermeillä ole merkitystä. Esimerkiksi funktiot 2n ja 1000000n ovat molemmat O(n), mikä nähdään esimerkiksi valitsemalla c = 1000001 ja n 0 = 1. Näiden funktioiden arvot toki poikkeavat toisistaan huomattavasti, mutta ne kasvavat samaa vauhtia. Sen sijaan esimerkiksi kertaluokat n ja n 2 ovat olennaisesti erilaiset: kun n kasvaa, kasvaa n 2 yhä vauhdikkaammin. Kahdesta eri kertaluokkaa olevasta algoritmista kertaluokaltaan pienempi on yleensä myös tehokkaampi. Pienillä syötteillä tilanne tosin voi kääntyä päinvastaiseksi, mutta pienillä syötteillä suoritusajalla ei yleensä ole merkitystä. Aikavaativuuksien luokittelu: TRAI -kurssilla opittiin vertailemaan ja järjestämään kertaluokkafunktioita. Tärkeintä onkin usein pystyä valitsemana kahdesta kertaluokkafunktiosta nopeampi ja hahmottaa niiden nopeuseron merkittävyys. Vertailujen ja algoritmien luokittelun helpottamiseksi funktiot jaetaan luokkiin, joista seuraavat ovat yleisimmät: Eksponentiaaliset aikavaativuudet esim 2 n, 3 n, 2 n /n, n! Käyttökelpoisia vain hyvin pienille syötteille Polynomiset aikavaativuudet n, n 2, n 5, n 12345, n n, nlogn Käytännössä yleisimpiä tehokkaita.. kohtuullisen tehokkaita n lineaarinen n 2 neliöllinen n alilineaarinen (mutta polynominen) Logaritmiset aikavaativuudet logn, loglogn, jne huom: logn on tällä kurssilla log 2 n kaikki o(n) on alilineaarista aikavaativuutta ei kokonaisissa peräkkäisalgoritmeissa Vakioaikainen: O(1) Luokkien erot ovat selkeitä, kaikille vakioille k pätee log k n = o(n) ja vastaavasti kaikille vakioille a >1,b > 0 pätee n b = o(a n ). Aikavaativuusluokkia käytettäessä on toki muistettava varmistaa funktion merkittävin tekijä. Esimerkiksi nlogn ei suinkaan ole logaritminen tai alilineaarinen, vaan nopeammin kasvava kuin n.

1. KERTAUSTA ALGORITMIIKASTA JA AIKAVAATIVUUDESTA 7 Suoritusajan laskeminen Mielivaltaisesti valitun ohjelman suoritusajan laskeminen voi osoittautua erittäin vaativaksi matemaattiseksi tehtäväksi. Onneksi useiden käytännössä esiintyvien ohjelmien suoritusaika on varsin helppo arvioida. Yleensä riittää tuntea aritmeettisen ja geometrisen sarjan summauskaavat, logaritmilaskennan peruslaskusäännöt sekä joitakin kertaluokkalaskennan periaatteita. Rekursiivisia ohjelmia analysoitaessa on lisäksi osattava ratkaista rekursioyhtälöitä. Esitetään seuraavaksi muutamia kertaluokkalaskennan sääntöjä. Niitä ei tässä todisteta, vaikka todistukset ovatkin yksinkertaisia, määritelmään 1-3 perustuvia. Jos ohjelmanosan P 1 suoritusaika T 1 (n)ono(f(n)) ja ohjelmanosan P 2 suoritusaika T 2 (n) vastaavasti O(g(n)), niin osien P 1 ja P 2 peräkkäiseen suoritukseen kuluva aika T 1 (n)+t 2 (n)ono(max{f(n), g(n)}). Tätä sääntöä kutsutaan summasäännöksi ja se merkitsee, että peräkkäissuorituksen aikavaativuuden määrää hitain osasuoritus. Summasäännön välittömänä seurauksena nähdään, että jos T(n) on astetta k oleva polynomi, niin T(n) =O(n k ). Itse asiassa pätee jopa T(n) =Θ(n k ). Summasääntö pätee lähes aina. Vain joissakin harvoissa tapauksissa funktiot f ja g ovat sellaiset, ettei niistä kumpikaan kelpaa maksimiksi. Siinä tapauksessa peräkkäiseen suoritukseen kuluvan ajan kertaluokka määrätään tarkemman analyysin avulla. Monimutkaiset suoritusaikafunktion lausekkeet voidaan kertaluokkatarkastelussa usein sieventää lyhyemmiksi juuri summasäännön nojalla, koska koko lausekkeen asemesta riittää tarkastella vain eniten merkitseviä termejä. Toinen kertaluokkalaskennan perussäännöistä on niin sanottu tulosääntö: äskeisiä merkintöjä käyttäen tulo T 1 (n)t 2 (n) =O(f(n)g(n)). Tätä sääntöä sovelletaan laskettaessa sisäkkäisten ohjelmanosien suoritusaikaa. Säännön välitön seuraus on se, että positiiviset vakiokertoimet voidaan kertaluokkatarkastelussa aina jättää huomiotta, minkä ansiosta tarkasteltavat lausekkeet entisestään yksinkertaistuvat. Esimerkki 1-6: Jos T(n) =3n 2 +5n+8, niin T(n) =O(n 2 ). Aikavaativuuden laskeminen algoritmista Sijoitus-, luku- ja tulostustoiminnot ovat yleensä O(1). Kokonaisen taulukon tai muun suuren tietorakenteen käsittelyyn kuluu kuitenkin enemmän aikaa. Niinpä esimerkiksi n alkiota sisältävän taulukon sijoittaminen toiseen taulukkoon vie aikaa O(n). Tämä on syytä muistaa arvoparametrien välittämisen yhteydessä. Myös lausekkeen arvottaminen onnistuu vakioajassa, ellei lauseke sisällä aikaa vaativia funktiokutsuja. Tavanomaiset taulukkoviittauksetkin ratkeavat vakioajassa. Toimintojonon eli peräkkäin suoritettavien toimintojen suoritusaika lasketaan summasäännön avulla. Tästä poiketaan vain silloin, kun sama syöte ei koskaan voi olla pahin tapaus kaikille toimintojonon osille. Ehdollisen toiminnon suoritusaikaa laskettaessa on otettava huomioon sekä ehdon arvottamiseen että valittavan osan suorittamiseen kuluva aika. Itse valinta voidaan katsoa vakioajassa tapahtuvaksi myös monihaaraisessa ehtorakenteessa, koska rakenne sisältää joka tapauksessa kiinteän määrän eri vaihtoehtoja. Vaihtoehtoisista osista tarkastellaan aina aikavaativuudeltaan pahinta, ellei ole perusteltua syytä jonkin vähemmän vaativan osan huomioon ottamiseen. Jos pahin tapaus on hyvin harvinainen, on sen käsittely irrotettava muista ja laskettava erikseen, esimerkiksi:

1. KERTAUSTA ALGORITMIIKASTA JA AIKAVAATIVUUDESTA 8 for (i = 0; i < n; i++) 1 if (i == n 1) 2 for (j = 0; j < n; j++) // tämä suoritetaan vain kerran, O(n) 3 a = a + 1; 4 else 5 x = x + 1; // tämä vakioaikainen suoritetaan useasti6 Toiston vaativuus lasketaan joko tulosäännöllä tai summaamalla toistettavan osan yksittäiset suoritusajat yli koko toiston. Jälkimmäistä menettelyä sovelletaan silloin, kun toistettavan osan aikavaativuus vaihtelee merkittävästi tai riippuu jostakin toistoon nähden ulkopuolisesta tekijästä. Säännöllisen toiston vaativuus voidaan usein laskea aivan suoraan kertomalla keskenään toistokertojen lukumäärä ja toistettavan osan pahimman tapauksen suoritusaika, mutta tässä on oltava tarkka, sillä pahin tapaus ei ehkä voi esiintyä joka kerralla saman toiston kuluessa. Toisto voidaan jakaa kahteen eri osaan ja analysoidaan ne erikseen kuten ylläolevassa esimerkissä. Toistokertojen todellisen lukumäärän selvittäminenkin voi joskus olla vaikea. Toiston hallinta vie myös oman aikansa, joka täytyy muistaa ottaa laskelmissa huomioon. Algoritmeihin ei rakenteisen ohjelmoinnin tai olio-ohjelmoinnin periaatteita noudatettaessa sisällytetä lainkaan hyppyjä. Jos hypyn vaikutusta aikavaativuuteen joudutaan analysoimaan, on huolellisesti tutkittava, mikä on suorituksen pahin tapaus ja täytyykö pahimmassa tapauksessa hypätä. Mikäli koko algoritmin toiminta perustuu hyppyihin, tulee analyysistä todennäköisesti erittäin hankala. Kunkin aliohjelman suoritusaika lasketaan erikseen parametrien tai muun mielekkään syötteen koon funktiona. Aliohjelmakutsua analysoitaessa otetaan aliohjelman oman aikavaativuuden lisäksi huomioon parametrien välityksen vaatima aika. Jos aliohjelma saadaan käyttöön valmiina eikä sen aikavaativuutta saada selville, on tämä erikseen mainittava analyysissä, mikäli kyseisellä ajalla oletetaan olevan merkitystä kokonaisuuden kannalta. 1.3 Rekursiivisten algoritmien aikavaativuus Kuten aiemmin todettiin, kukin aliohjelma lasketaan erikseen ja "lisätään" aliohjelmakutsun paikalle. Rekursiivisia aliohjelmia tarkasteltaessa, aikavaativuuden kaavastakin tulee rekursiivinen. Esimerkki 1-7: Kertoma voidaan laskea seuraavalla rekursiivisella algoritmilla: public static int factorial(int n) { 1 if (n <= 1) 2 return 1; 3 else 4 return i * factorial(n 1); 5 } 6 Algoritmin suoritukseen kuluva aika riippuu nyt parametrin n arvosta: mitä suurempi on parametrin arvo, sitä useampia rekursiivisia kutsuja aiheutuu ja sitä enemmän aikaa kuluu. Rivien 1-4 suoritusaika on selvästi O(1). Rivillä 5 suoritetaan kertolasku ja sijoi-

1. KERTAUSTA ALGORITMIIKASTA JA AIKAVAATIVUUDESTA 9 tus, joiden aikavaativuus on myös O(1), mutta kertolaskun suorittaminen edellyttää molempien operandien arvon tuntemista. Nyt jälkimmäisen operandin arvo lasketaan rekursiivisesti, mihin kulunee aikaa enemmän kuin O(1). Merkitköön T(n) koko algoritmin suoritusaikaa. Perustapauksessa n 1 suoritetaan rivit 1-3, mihin kuluu vakioaika. Sovitaan, että T(n) = d, kun n 1. Vakion todellista arvoahan ei voida tietää, joten symbolinen merkintä d riittää. Kun n >1, on T(n) = c + T(n 1). Tässä T(n 1) kuvaa rekursiivisesta kutsusta aiheutuvaa suoritusaikaa eli algoritmin suoritusaikaa n:ää yhtä pienemmällä parametrin arvolla. Vakio c taas kuvaa rivin 5 kertolaskuun ja sijoitukseen kuluvaa aikaa, jota ei saa unohtaa suoritusaikaa laskettaessa. Vakiothan väistyvät vasta kertaluokkatarkastelussa. Suoritusaikafunktio T on nyt määritelty kaikilla kelvollisilla parametrin n arvoilla: T(n)=d, kun n 1 T(n)=c+T(n 1), kun n >1. (c,dvakioita)(1-1) Koska funktio määritellään rekursiivisesti itsensä ja perustapauksen avulla, on kyseessä rekursioyhtälö. Funktio olisi toki tarkoituksenmukaisempaa esittää suljetussa muodossa, jonka löytämiseksi rekursioyhtälö täytyy ratkaista. Ryhdytään etsimään ratkaisua korvaamalla funktion lausekkeen rekursiivinen osa toistuvasti määrittelynsä mukaisella vastineellaan ja tarkkailemalla, esiintyykö tällöin jonkinlaista säännönmukaisuutta: Aloitetaan olettamalla, että n > 2. Silloin on määrittelyn mukaan T(n 1) = c+t(n 2), joten T(n) = c+t(n 1)= c+(c+t(n 2)) = 2c+T(n 2). Vastaavaan tapaan olettamalla n yhä suuremmaksi havaitaan pian, että T(n) = ic+t(n i), kun n > i. Asettamalla i = n 1 saadaan vihdoin ratkaisu T(n) =c(n 1)+T(1) = c(n 1)+d. Tämä merkitsee, että T(n) =O(n). Äskeinen rekursioyhtälö ratkesi varsin vaivattomasti. Päätellyn ratkaisun oikeellisuus tosin jäi todentamatta. Täydelliseen ratkaisuun tarvittaisiin vielä vaikkapa induktiotodistus, jota ei tässä esitetä. Erilaisia rekursioyhtälöitä voi periaatteessa olla äärettömästi erilaisia. Muuttuvia osia on kolme, kutsujen määrä, kutsujen koko ja työ rekursion lisäksi. Rekursiivisia kutsuja voi olla esimerkiksi 1, 2,..., logn, n,..., n/2, n kappaletta. Rekursion koko voi olla esimerkiksi n 1, n 2, n/2, logn, n. Rekursion lisäksi työtä voidaan tehdä esimerkiksi O(1), O(logn), O( n ),O(n), O(n 2 ). Rekursioyhtälö on siten esimerkiksi muotoa T(n)=cT(n/d) + O(n a ) tai (1-2) T(n)= n T( n ) + O( n ). Useimmissa tapauksissa ylläesitetty purkamalla ja kasaamalla päätteleminen toimii riittävän hyvin, kunhan emme tee hätäisiä johtopäätöksiä liian pienellä aineistolla. Muotoa T(n) = at(n/b) + f(n) (1-3) oleviin rekursioyhtälöihin löytyy valmis ratkaisu helpohkolla säännöstöllä (ns. Master theorem), mutta sitä ei käsitellä tällä kurssilla. Säännöstö löytyy mm. Cormet&al kirjasta ja mahdollisesti esitellään Algoritmien suunnittelu ja analysointi -kurssilla.

1. KERTAUSTA ALGORITMIIKASTA JA AIKAVAATIVUUDESTA 10 Esimerkki 1-8: Lomituslajittelun (ja monen muun hajoita-ja-hallitse -algoritmin) aikavaativuus: T(n) = 2T(n/2) + d, missä d = O(n) (1-4) T(n/2) = 2T(n/4) + d/2 T(n/4) = 2T(n/8) + d/4 T(n/8) = 2T(n/16) + d/8 T(n/4) = 2T(n/8) + d/4 = 2 ( 2T(n/16) + d/8 ) + d/4 = 4T(n/16) + d/4 + d/4 = 4T(n/16) + d/2 T(n/2) = 2T(n/4) + d/2 = 2 ( 4T(n/16) + d/2 ) + d/2 = 8T(n/16) + d + d/2 = 8T(n/16) + 3d/2 T(n) = 2T(n/2) + d = 2 ( 8T(n/16) + 3d/2 ) + d = 16T(n/16) + 3d + d/2 = 16T(n/16) + 4d Rekursioyhtälön tulosta ei ole enää yhtä helppo nähdä, mutta ylläolevasta voidaan päätellä vaiheita olevan logaritminen määrä, ja kullakin tasolla tehtävän lineaarinen määrä työtä, eli yhteensä O(nlogn). Esimerkki 1-9: Fibonaccin lukujen laskenta määritelmän mukaan public int fib(int n) { // Tehoton ratkaisu!!! 1 if (n < 2) 2 return 1; 3 else 4 return fib(n 1) + fib(n 2); 5 } 6 Huomaa, että tämä on tehoton tapa laskea Fibonaccin lukuja. Rekursiokutsuja on kaksi, muuta työtä vain vakioaika. Rekursioyhtälö on helppo muodostaa: T(n) = T(n 1) + T(n 2) + c, missä c on vakio. Analysointi on kuitenkin hieman hankalampaa. Rekursioyhtälöä purettaessa hieman enemmän, nähdään kutsujen määrän noudattavan Fibonaccin lukuja, mutta sen todistaminen on vaikeaa ja työläämpää. Tarkan tuloksen sijaan tarkastelemme haarukkaa johon aikavaativuus jää. Yläraja on helppo arvioida yksinkertaistamalla kaavaa: T(n 2) T(n 1) (miksi?) T(n) 2T(n 1) + c = 2(2T(n 2) + c) + c = 2(2(2T(n 3) + c) + c) + c = 2 i T(n i) + c(2 i 1) = O(2 n ). (1-5) Mutta onko tämä yläraja-arviointi liian väljä? Arviointi kertautuu joka kierroksella! Vastaavasti voidaan todistaa alaraja arvioimalla termiä T(n 2) alaspäin verrattuna 1 -- T(n 1):een: 2

1. KERTAUSTA ALGORITMIIKASTA JA AIKAVAATIVUUDESTA 11 1 T(n 2) -- T(n 1) ("arvaus") (1-6) 2 1 T(n 3) + T(n 4) + c -- (T(n 2) + T(n 3) + c) 2 1 1 1 -- T(n 3) + T(n 4) + -- c -- T(n 2) 2 2 2 1 1 1 -- T(n 3) + T(n 4) + -- c -- (T(n 3) + T(n 4) + c) 2 2 2 1 1 -- T(n 4) -- c 2 2 T(n 4) c, mikä varmasti pitää paikkansa kunhan n kasvaa riittävän suureksi.näin ollen alarajatarkastelussa T(n 2) voidaan korvata -- T(n 1):lla. 1 2 3 3 3 T(n) -- T(n 1) + c = -- (-- T(n 2) + c) + c (1-7) 2 2 2 3 3 3 3 = -- (-- (-- T(n 3) + c) + c) + c = -- T(n i) + c( 1) 2 2 2 2 i 3 -- 2 i 3 = Ω( -- ). 2 n 3 Aikavaativuus on siis Ω( -- )jao(2 n ), joka tapauksessa eksponentiaalinen. Arvatenkin tarkka kantaluku on kultaisen leikkauksen suhdeluku ϕ 1.618 2 n Rekursiopuu Jos/kun ylläesitetty rekursioyhtälön purkaminen tuntuu hankalalta, (tätäkin) asiaa voidaan havainnollistaa piirtämällä. Rekursiopuuhun piirretään kullekin tasolle solmu kutakin rekursiokutsua kohti ja solmuun siinä käytetty työ. Kullakin tasolla lasketaan solmujen työt yhteen. Koko työ on kaikkien tasojen töiden summa. Piirtoteknisesti solmuja joutuu yhdistelemään ja käyttämään " " yms. merkintöjä, mutta kunhan kukin merkittävä osa jollain tavalla huomioidaan, algoritmin aikavaativuus saadaan laskettua. Kuvan 1-2 puu lomituslajittelusta havainnollistaa rekursiota yksinkertaisessa tapauksessa. Kuvan 1-3 Fibonaccin funktiolle kuva ei ole aivan yhtä selkeä, mutta näyttää selvästi aikavaativuuden eksponentiaalisuuden. Sanapituuden vaikutus Jos käsittelemme tietokoneen sanaan (yleensä 32 tai 64 bittiä) mahtuvia lukuja, voimme olettaa yksinkertaisten operaatioiden (yhteenlasku, kertolasku, jne) olevan vakioaikaisia. Jos käsittelemme hyvin suuria lukuja, on luvun kokokin huomioitava. Suuren kokonaisluvun a esittämiseen tarvitaan loga bittiä. Kahden suuren luvun a ja b yhteenlaskuun menee siten aikaa O(log(a + b)). Esimerkki 1-10: Potenssiin korotus suurilla luvuilla x n. (*)

1. KERTAUSTA ALGORITMIIKASTA JA AIKAVAATIVUUDESTA 12 T(n): n Työ 1 n T(n/2): n/2 T(n/2): n/2 logn T(n/4): n/4 T(n/4): n/4 T(n/4): n/4 T(n/4): n/4 2 n/2 4 n/4 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 n 1 Kuva 1-2: Rekursiopuu yhtälölle T(n) = 2T(n/2) + n. Yhteensä Θ(nlogn) T(n) n T(n 1) T(n 2) T(n 2) T(n 3): T(n 3): T(n 4) 1 1 1 1 1 1 1 1 Kuva 1-3: Rekursiopuu Fibonaccin funktiolle T(n) = T(n 1) + T(n 2). n/2 BigInteger pow(biginteger x, int n) { // suoraviivainen algoritmi 1 BigInteger r = new BigInteger("1"); 2 for (int i = 0; i < n; i++) 3 r = r.multiply(x); 4 return r; 5 } 6 Aikavaativuus olisi O(n) jos BigInteger.multiply():n aikavaativuus olisi vakio, mutta se ei luonnollisestikaan voi olla vakio hyvin suurille luvuille. Java API:n dokumentaatio ei valitettavasti (tässäkään) anna mitään vihjettä aikavaativuudesta. Koulualgoritmia ("yhteenlasku alekkain") käyttäen yksittäisen kertolaskun aikavaativuus on helppo arvioida. Olkoot keskenään kerrottavat luvut a ja b ja lukujen pituudet siten loga ja logb bittiä. Kertolaskun aikavaativuus alekkainlaskussa on selkeästi O(loga logb). Lähdekoodista tarkistettuna Java API käyttää tätä algoritmia.

1. KERTAUSTA ALGORITMIIKASTA JA AIKAVAATIVUUDESTA 13 Yllä algoritmissa pow, r kasvaa ja on (lopussa) pituudeltaan O(nlogx) bittiä, x:n pituus on kiinteästi logx bittiä. Koko aikavaativuus on siten n O(nlogx) O(logx) O(n 2 log 2 x). Jos x on pieni vakio, niin aikavaativuus on O(n 2 ). Rekursiivinen hajoita-ja-hallitse -algoritmi: x n = ( xx) n 2 jos n on parillinen x n = xxx ( ) n 2 jos n on pariton BigInteger pow2(biginteger x, int n) { 1 if (n == 0) 2 return new BigInteger("1"); 3 if (n == 1) 4 return x; 5 if (n%2 == 0) // n parillinen 6 return (pow2(x.multiply(x), n/2 ) ); // pow2(x*x, n/2) 7 else 8 return (x.multiply(pow2(x.multiply(x), n/2 )));// x*pow2(x*x, n/2)9 } 10 T(n) = T(n/2) + 2T m (pahin tapaus kun n = 2 k 1) T(n) = T(n/2) + T m (paras tapaus kun n = 2 k ). Taas aikavaativuus riippuu vahvasti kertolaskun aikavaativuudesta T m. Jos kertolasku olisi O(1), aikavaativuus olisi O(logn), jos taas kertolasku olisi O(n) koko aikavaativuus olisi mukava O(nlogn). Selvittääksemme aikavaativuuden, kirjoitetaan kukin osa (rekursiokutsu, molemmat kertolaskut) tarkemmin: T(n, x) = T(n/2, x 2 ) + logx n logx + logx logx (1-8) = T(n/2, x 2 ) + logx (nlogx + logx) = T(n/2, x 2 ) + (n + 1)log 2 x n Kun rekursiossa edetään, x kasvaa ja n pienenee. Tasolla i on x i = x 2i ja n i = ---. Näin ollen koko aikavaativuus on 2 i logn T(n, x) = ( log x 2i ) 2 n ---. (1-9) 2 i + 1 i = 0 Kun summan termeistä viimeinen on suurempi kuin muut yhteensä, tarkastelemme vain sitä: T(n, x) 2( log x 2 logn ) 2 n ---------- + 1 (1-10) logn 2 = 2( log x n ) 2 n -- + 1 = 2( nlog x) 2 n (1-11) n n -- + 1 = O(n 2 log 2 x) Jos/kun kantaluku x on pienehkö vakio, on aikavaativuus tuttu O(n 2 ). Käytännössä kertolaskuja tulee hieman vähemmän kuin edellisessä suoraviivaisessa algoritmissa,

1. KERTAUSTA ALGORITMIIKASTA JA AIKAVAATIVUUDESTA 14 pahin tapaus ei aina toteudu, ja kaavan 1-9 summa ei mene aivan logn:ään asti. Toteutuksesta mitattuna rekursiivinen algoritmi on pienellä kantaluvulla 70-200 kertaa nopeampi, riippuen potenssista ja alustasta. Suurilla kantaluvuilla ero hieman pienenee. Myös Java API (1.6) käyttää tätä algoritmia. 1.4 Aikavaativuuden rajojen todistaminen Hankalassa tapauksessa voi olla helpompaa todeta (todistaa) algoritmille tai ongelmalle aikavaativuudelle erilliset ala- ja ylärajat. Esimerkiksi yllä Fibonaccin funktion aikavaativuudesta saatiin erilliset ala- ja ylärajat (molemmat eksponentiaalisia). Olisimme myös voineet halutessamme todistaa tarkemmatkin ala- ja ylärajat. Paitsi algoritmeja, voimme analysoida myös ongelmia. Tällöin tarkoitetaan parhaan mahdollisen ongelman ratkaisevan algoritmin aikavaativuuden rajoja. Hyödyllisintä on analysoida ongelman vaativuutta analysoimalla aikavaativuuden alaraja, jota nopeammin ongelmaa ei voi yleisessä tapauksessa ratkaista (deterministisesti). Toisaalta voidaan todistaa, että annettu ongelma on mahdollista ratkaista korkeintaan jonkin ylärajan mukaista algoritmia käyttäen. Tämä voidaan helpointen todistaa rakentamalla ko. algoritmi. Yleisesti ongelmien vaativuuden todistaminen on vaativampaa kuin algoritmien analysointi, eikä kovinkaan tarpeellista normaalissa ohjelmoinnissa. Näin ollen esitetään tässä vain esimerkkinä yksi tekniikka yhteen ongelmaan. Päätöspuut Osoitetaan, ettei pelkästään avainten vertailuun perustuvan lajittelun aikavaativuus voi olla parempi kuin O(nlogn). Todistuksessa tarvitaan apuna päätöspuun käsitettä, joka esitetään seuraavaksi. Päätöspuu on binääripuu, jolla kuvataan jonkin ongelman ratkaisun etsimistä. Puun juurisolmuun liitetään ongelman kaikki erilaiset tapaukset sekä jokin ehto, jonka avulla nämä tapaukset ositetaan kahteen ryhmään: tapauksiin, joissa ehto pätee, ja tapauksiin, joissa ehto ei päde. Molemmista ryhmistä muodostetaan erikseen omat päätöspuunsa, jotka asetetaan juurisolmun alipuiksi esimerkiksi siten, että juurisolmun ehdon täyttävien tapausten alipuusta tehdään juurisolmun vasen alipuu ja ehtoa täyttämättömien tapausten alipuusta oikea alipuu. Päätöspuun jokaiseen haarautumissolmuun liittyy vähintään kaksi eri tapausta ja ehto, lehtisolmuihin puolestaan täsmälleen yksi tapaus eikä laisinkaan ehtoa. Juuresta johonkin lehteen johtavaan polkuun liittyvistä ehdoista saadaan selville, millaisten ehtojen täytyy olla voimassa nimenomaan kyseisen lehden sisältämässä tapauksessa. Esimerkki 1-11: Kuva 1-4 esittää upotuslajittelun päätöspuun syötteen koolla n = 3. Alussa kaikki kuusi vaihtoehtoa ovat mahdollisia. Ensimmäisen vertailun < jälkeen vaihtoehdot jakautuvat kahtia. Jos vertailun tulos oli tosi, jäljelle jäävät vain ne vaihtoehdot, joissa edelsi :tä. Epätoden puolelle jäivät loput. Vertailuja jatketaan kunnes jäljellä on vain yksi vaihtoehto. Vertailuihin perustuvaa lajittelua kuvaavan päätöspuun haarautumissolmuun liittyvä ehto on aina kahden alkion keskinäinen vertailu. Koska lajittelun aikavaativuuden arviointikin

1. KERTAUSTA ALGORITMIIKASTA JA AIKAVAATIVUUDESTA 15 < < < < < < < < < < < < kyllä < ei < < < < < < < kyllä ei < < < < < < < kyllä ei < < < < < < < < < < < < < kyllä ei < kyllä ei < < < < < < < < Kuva 1-4: Upotuslajittelun päätöspuu, n =3,n!=6. useimmiten perustuu tarvittavien vertailujen lukumäärään, on päätöspuulla ja aikavaativuudella jotakin yhteistä: päätöspuun juuresta lehteen johtavan polun eli päätöspolun pituus on sama kuin kyseiseen lehteen liittyvän tapauksen lajittelemiseksi tarvittavien vertailujen lukumäärä. Jotta lajittelu onnistuisi mahdollisimman nopeasti, pitäisi vertailujen lukumäärän toisin sanoen päätöspolun pituuden olla aina mahdollisimman pieni. Lisäksi on muistettava, että mikä tahansa tapaus on lajittelussa yhtä todennäköinen, joten eri päätöspolkujen tulisi olla mahdollisimman tasapituisia. Jos näet jokin päätöspolku on kovin lyhyt, on toisaalla päätöspuussa todennäköisesti useita varsin pitkiä päätöspolkuja. Päätöspolkujen optimaalisen pituuden arvioimiseksi on ensiksi arvioitava päätöspuun lehtien lukumäärää. Lajiteltaessa n:ää alkiota on erilaisia mahdollisia järjestyksiä kaikkiaan peräti n! kappaletta. Vaikka osa lajiteltavista alkioista olisi keskenään samoja, on lajittelun kannalta silti olemassa n! erilaista järjestystä. Siksi lajittelua kuvaavassa päätöspuussa on aina n! lehteä. Tämä merkitsee, että haarautumissolmuja on n! 1 ja solmuja yhteensä 2n! 1. Näin monta solmua sisältävän päätöspuun korkeus on vähintään log(2n! 1) eli likimain sama kuin log(n!). Ollakseen näin matala täytyy puun kaikkien lehtisolmujen olla puun alimmalla tai kahdella alimmalla tasolla (miksi?), jolloin pisimmän samoin kuin lyhimmänkin päätöspolun pituus on O(log(n!)). Kertoman logaritmin arvioimiseksi annetaan [todistamatta] approksimaatio n! (n/e) n, (1-12) missä e 2.7183 on luonnollisen logaritmijärjestelmän kantaluku eli niin sanottu Neperin luku. Äskeinen approksimaatio on puolestaan yksinkertaistettu muoto hieman monimutkaisemmasta Stirlingin kaavasta kertoman arvioimiseksi. Nyt voidaan arvioida seuraavasti:

1. KERTAUSTA ALGORITMIIKASTA JA AIKAVAATIVUUDESTA 16 log(n!) log((n/e) n )=nlogn nloge (1-13) log(n!) = O(nlogn) Kertoman logaritmia voidaan arvioida myös alaspäin: log(n!) (n/2)log(n/2) = (n/2)logn (n/2) (1-14) log(n!) = Ω(nlogn) Optimaalisessa päätöspuussa jokaisen päätöspolun pituus on toisin sanoen Θ(nlogn), joten vertailuihin perustuvan lajittelun aikavaativuuskaan ei pahimmassa tapauksessa voi olla tätä parempi. On vielä syytä huomata, ettei aikavaativuudeltaan O(n) tehokkaampaa (peräkkäistä) lajittelualgoritmia voi olla olemassakaan. Jos tällainen algoritmi näet olisi olemassa, ei sen suoritusaikana ennätettäisi edes käsitellä kaikkia alkioita. Lajittelu perustuisi tällöin osittain arvaukseen eikä tuloksen oikeellisuutta voitaisi taata! Esimerkiksi kaukalolajittelun aikavaativuudeksi todettiin TRAI kurssilla O(n), mutta kaukalolajittelua voidaan soveltaa vain tilanteessa, jossa lajiteltavien alkioiden avainten joukko on rajoitettu. Topologista lajittelua lukuunottamatta kaikki muut näillä kursseilla esitettävistä lajittelualgoritmeista perustuvat oletukseen avainten lineaarisesta järjestyksestä, mutta esimerkiksi avainten suuruusluokasta ei näissä algoritmeissa oleteta mitään. 1.5 Kokeellinen aikavaativuuden määrääminen Joskus algoritmin asymptoottisen analyysin keinot eivät ole mahdollisia, järkeviä tai riittävän tarkkoja. Mahdollisia analyyttisen tuloksen saamisen esteitä voivat olla esimerkiksi: emme osaa analysoida algoritmia, emme tiedä tarpeeksi syötteen rakenteesta (tai sen vaikutuksesta), lähdekoodi (tai sen osa) ei ole käytettävissä (tai ymmärrettävissä). Joskus myös tarvitsemme tarkempia tuloksia aikavaativuuden vakiokertoimista. Tällaisissa tilanteissa voimme toteuttaa algoritmimme, suorittaa sitä ja mitata suorituksen aikavaativuuden tietokoneella. Se, mitä mitataan ja missä muodossa tuloksen haluamme, riippuu omista tarpeistamme. Yleensä voimme tehdä sivistyneen arvauksen aikavaativuusfunktiosta ja sitä käyttäen esimerkiksi ekstrapoloida aika-arviomme suuremmille syötteille. Tulos on yleensä varsin hyvä jos (ja vain jos) aikavaativuusfunktioarvauksemme osui oikeaan. Miten aikavaativuutta mitataan kokeellisesti? Kuten analyyttisessäkin arvioinnissa, aikavaativuus on helpointa tehdä pienehkölle ohjelmanosalle kerrallaan. Jotta tulokset olisivat vertailukelpoisia, on käytettävä vakioitua tai useaa satunnaista syötettä, riippuen algoritmista. Erityisesti on varmistettava, ettei syöte ole mikään erikoistapaus. Luotettavuuden parantamiseksi, kullakin syötekoolla algoritmi suoritetaan useamman kerran ja aikana käytetään ajojen keskiarvoa/mediaania (jos satunnainen syöte) tai minimiä (jos vakiosyöte). Syötteen kokoa kasvatetaan aluksi eksponentiaalisesti (esimerkiksi 2 tai 10). Kun aika alkaa kasvaa, käytetään tarkempia askeleita, erityisesti jos aikavaativuus on suuri (eksponentiaalinen). Jotta mittaustarkkuus olisi riittävä, kasvatetaan syötettä niin, että ajoaika on ainakin useita sekunteja. Nopeille algoritmeille tämä ei välttämättä ole mahdollista (syöte kasvaa liian suureksi). Tällöin varsinaista mitat-

1. KERTAUSTA ALGORITMIIKASTA JA AIKAVAATIVUUDESTA 17 tavaa algoritmia on suoritettava toistuvasti mittauksen aloituksen ja lopetuksen välissä riittävän tarkan mittauksen saavuttamiseksi. Mittauksissa tarkkaillaan peräkkäisten syötekokojen suoritusaikojen suhdetta. Esimerkiksi kaksinkertaistetaan syöte kullakin askeleella. Jos suoritusaika kaksinkertaistuu, kyseessä on todennäköisesti lineaarinen aikavaativuus. Jos aika hieman yli kaksinkertaistuu, kyseessä voi olla esimerkiksi O(nlogn) (voi olla vaikea erottaa lineaarisesta, vaatii pidemmän jänteen). Jos aika nelinkertaistuu: neliöllinen. Jos aika 8-kertaistuu: kuutiollinen. Jos syötteen kasvattaminen yhdellä kasvattaa aikavaativuuden kaksinkertaiseksi: eksponentiaalinen. Tulosten esittäminen käyränä ja vertailufunktioiden piirtäminen samaan asteikkoon sopivilla kertoimilla auttaa näkemään oleelliset muutokset (kts. esimerkit alla). Logaritminen asteikko on yleensä parempi kuin lineaarinen. Joissakin mittaustuloksissa esiintyy heittoja, lähinnä välimuistien ja roskienkeruun takia. Kun syöte kasvaa yli välimuistista, aikavaativuus alkiota kohti usein kaksin- kolminkertaistuu (katso esimerkki 5-5 s. 76). Tämän vuoksi pidempi sarja kuin 2-3 mittausta on ehdoton vaatimus. Ajan mittaaminen Varsinaisen algoritmin toteuttamisen lisäksi meidän on toteutettava ajan mittaaminen. Helpoin ratkaisu on käyttää käyttöjärjestelmän prosessin ajanmittausta (Unix ja sukulaiset: time java Ohjelma). Tämä on sikäli hyvä, että se mittaa erikseen käytetyn reaaliajan ja juuri tämän prosessin käyttämän ajan (jättäen huomiotta muiden prosessien ajan). Toisaalta tätä voidaan käyttää vain kokonaisen ohjelman mittaamiseen ja mukaan tulevat myös Java virtuaalikoneen oma alustus- ja lopetustyö, joka tosin on melko vakio (kymmeniä..satoja millisekunteja) yksinkertaisessa ohjelmassa. Tämä ei myöskään ole alustariippumaton mittaustapa. Helpompi olisi, jos ajanmittaus voitaisiin tehdä ohjelman sisällä. Valitettavasti JavaAPI ei suoraan mahdollista prosessin/säikeen ajankäytön mittaamista. C:ssä clock() palauttaa prosessin käyttämän ajan mikrosekunteina. Java-ohjelmaan on mahdollista sisällyttää kutsuja erillisiin mittaohjelmiin (verkosta löytyy valmiita HOWTO -ohjeita), mutta tekniikat eivät ole alustariippumattomia. Helpompaa on käyttää reaaliaikakelloa java.util.date -luokan avulla ja pyrkiä minimoimaan siitä aiheutuvat virheet. Date palauttaa "seinäkelloajan" millisekunteina. Jotta ajanmittaus antaisi järkeviä tuloksia, meidän on minimoitava muut samassa koneessa suorittuvat prosessit, emmekä voi tulostaa juuri mitään mittauksen aikana. Ohjelma ajetaan useaan kertaan samalla syötteellä ja suoritusajoista otetaan minimi. Mittaustarkkuuden vuoksi on käytettävä riittävän suuria syötteitä tai saman operaation toistamista useasti (suoritusaika vähintään 0,1s). Esimerkki aikamittarista: Ajastin.java kurssin wwwsivulla. Esimerkki 1-12: String vrt. StringBuffer (StringTest.java) Toteutetaan Vector<Integer>.toString() eri tavoin. java.util.vector<integer>.tostring() (valmis toteutus: AbstractCollection) String: s = s + v.get(i).tostring() + ", "; StringBuffer: sb.append(v.get(i).tostring() + ", ") StringBuilder: sb.append(v.get(i).tostring() + ", ") StringBuilder2: sb.append(v.get(i).tostring); sb.append(", ");